fix sortable

This commit is contained in:
vcoppe committed 2026-10-17 17:21:56 +02:00
1 parent 970cf7d1e1
commit 01443943b3
6 files changed
+108 -10

No files matched your search

+19
View File
@@ -531,3 +531,22 @@ fn the_parts_of_a_split_are_numbered_from_one() {
assert_eq!(names, ["track (1).gpx", "track (2).gpx", "track.gpx"]);
std::fs::remove_dir_all(folder).unwrap();
}
#[test]
fn files_can_be_reordered() {
let app = App::new();
let mut ids = vec![];
for name in ["a", "b", "c"] {
let outcome = app.call("new_file", json!({ "name": name }));
ids.push(outcome["added"][0].as_str().unwrap().to_string());
}
assert_eq!(app.call("file_order", json!({})), json!(ids));
let outcome = app.call("reorder", json!({ "fileIds": [ids[2]], "index": 0 }));
assert_eq!(outcome["changed"], true);
assert_eq!(outcome["orderChanged"], true);
assert_eq!(
app.call("file_order", json!({})),
json!([ids[2], ids[0], ids[1]])
);
}
+2 -1
View File
@@ -18,7 +18,8 @@
"width": 1400,
"height": 900,
"minWidth": 800,
"minHeight": 600
"minHeight": 600,
"dragDropEnabled": false
}
],
"security": { "csp": null }
@@ -9,6 +9,7 @@
nohover = false,
slotInsideTrigger = true,
sticky = false,
treeState,
children,
}: {
defaultState?: 'open' | 'closed';
@@ -21,10 +22,12 @@
* like its node.
*/
sticky?: boolean;
/** Which nodes are open, to keep it or to open nodes from the outside. */
treeState?: CollapsibleTreeState;
children: Snippet;
} = $props();
let open = $state(new CollapsibleTreeState(defaultState));
let open = $state(treeState ?? new CollapsibleTreeState(defaultState));
setContext('collapsible-tree-state', open);
setContext('collapsible-tree-side', side);
@@ -1,29 +1,93 @@
<script lang="ts">
import { untrack } from 'svelte';
import { CollapsibleTree, CollapsibleTreeNode } from '$lib/components/collapsible-tree/index';
import { CollapsibleTreeState } from '$lib/components/collapsible-tree/utils.svelte';
import { ScrollArea } from '$lib/components/ui/scroll-area/index';
import ExplorerTree from './ExplorerTree.svelte';
import { clickedInExplorer } from './state.svelte';
import { i18n } from '$lib/i18n/i18n.svelte';
import { engine } from '$lib/engine';
import { folderName } from '$lib/engine/workspace';
import { selectedFileIds } from '$lib/engine/selection-helpers';
import { folderName, subfolder } from '$lib/engine/workspace';
const { workspace } = engine;
const { workspace, selection } = engine;
let folder = $derived($workspace?.folder);
// which folders are open: new ones when another folder is opened, the folder itself is open at
// first and its folders are closed
let trees = $derived(
folder
? {
root: new CollapsibleTreeState('open'),
folders: new CollapsibleTreeState('closed'),
}
: undefined
);
let container: HTMLElement | undefined = $state();
// the file that is selected, if it is in the folder
let selectedPath = $derived.by(() => {
const id = selectedFileIds($selection)[0];
const path = id === undefined ? undefined : $workspace?.files[id]?.path;
return subfolder(folder, path) === undefined ? undefined : path;
});
$effect(() => {
// not what the click that was just made depends on, only the selection and the folder
const clicked = untrack(() => clickedInExplorer.path);
if (selectedPath === clicked) {
return;
}
clickedInExplorer.path = undefined;
if (folder && selectedPath !== undefined) {
return reveal(folder, selectedPath);
}
});
/**
* Opens the folders that hold a file and scrolls to the file, if it is not in view already. Its
* row may not be there yet, because the content of a folder is read when it is opened.
*/
function reveal(folder: string, path: string) {
if (!trees) {
return;
}
trees.root.get('root.folder').current = true;
let id = 'root';
for (const name of subfolder(folder, path)?.split('/').filter(Boolean) ?? []) {
id += `.${name}`;
trees.folders.get(id).current = true;
}
let frame = 0;
let attempts = 0;
const scroll = () => {
const row = container?.querySelector(`[data-path="${CSS.escape(path)}"]`);
if (row) {
row.scrollIntoView({ block: 'nearest' });
} else if (attempts++ < 120) {
frame = requestAnimationFrame(scroll);
}
};
frame = requestAnimationFrame(scroll);
return () => cancelAnimationFrame(frame);
}
</script>
<!--
The tree of the open folder: its folders and GPX files, one level read at a time. The files are
opened from it (the file list has the ones that are open).
-->
{#if folder}
{#if folder && trees}
<div class="flex flex-col h-full">
<div class="px-2 py-1 text-sm font-semibold shrink-0">{i18n._('menu.explorer')}</div>
<ScrollArea class="grow min-h-0">
<div class="flex flex-col pl-1 pr-3 pb-1">
<div bind:this={container} class="flex flex-col pl-1 pr-3 pb-1">
<!-- starts over when another folder is opened -->
{#key folder}
<!-- the folder itself, which is open at first -->
<CollapsibleTree side="left" defaultState="open" sticky>
<!-- the folder itself -->
<CollapsibleTree side="left" treeState={trees.root} sticky>
<CollapsibleTreeNode id="folder">
{#snippet trigger()}
<span class="grow truncate text-left" title={folder}>
@@ -34,8 +98,7 @@
<!-- what is in it is only read when it is open -->
{#if opened}
<div class="ml-4">
<!-- its folders are closed at first -->
<CollapsibleTree side="left" defaultState="closed">
<CollapsibleTree side="left" treeState={trees.folders}>
<ExplorerTree />
</CollapsibleTree>
</div>
@@ -2,6 +2,9 @@
import { CollapsibleTreeNode } from '$lib/components/collapsible-tree/index';
import { Button } from '$lib/components/ui/button';
import { FileText } from '@lucide/svelte';
import { getContext } from 'svelte';
import { clickedInExplorer } from './state.svelte';
import { stickyKey, type StickyLevel } from '$lib/components/collapsible-tree/sticky';
import ExplorerTree from './ExplorerTree.svelte';
import { openPath } from '$lib/actions/file-actions';
import { engine, type FolderEntry } from '$lib/engine';
@@ -16,6 +19,8 @@
} = $props();
const { workspace, selection, folderVersion } = engine;
// the rows of the folders that stick above the files of this one
const level = getContext<StickyLevel>(stickyKey);
let entries: FolderEntry[] = $state([]);
@@ -35,6 +40,9 @@
/** Opens the file, or selects it if it is open already. */
function open(entry: FolderEntry, id: string | undefined) {
// it is in view, the explorer does not have to scroll to it (which would move the rows
// under the pointer, and leave the one it was on highlighted)
clickedInExplorer.path = entry.path;
if (id !== undefined) {
engine.select([id]);
} else {
@@ -67,6 +75,8 @@
? 'bg-accent'
: ''} {id === undefined ? 'text-muted-foreground' : ''}"
title={entry.path}
data-path={entry.path}
style="scroll-margin-top: {level.top}px"
onclick={() => open(entry, id)}
>
<FileText size="16" class="shrink-0" />
@@ -0,0 +1,2 @@
/** The file that the user just selected by clicking it in the explorer, until the selection changes. */
export const clickedInExplorer: { path: string | undefined } = $state({ path: undefined });