mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-19 09:22:12 +00:00
fix sortable
This commit is contained in:
1 parent
970cf7d1e1
commit
01443943b3
6 files changed
+108
-10
No files matched your search
@@ -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"]);
|
assert_eq!(names, ["track (1).gpx", "track (2).gpx", "track.gpx"]);
|
||||||
std::fs::remove_dir_all(folder).unwrap();
|
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]])
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -18,7 +18,8 @@
|
|||||||
"width": 1400,
|
"width": 1400,
|
||||||
"height": 900,
|
"height": 900,
|
||||||
"minWidth": 800,
|
"minWidth": 800,
|
||||||
"minHeight": 600
|
"minHeight": 600,
|
||||||
|
"dragDropEnabled": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": { "csp": null }
|
"security": { "csp": null }
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
nohover = false,
|
nohover = false,
|
||||||
slotInsideTrigger = true,
|
slotInsideTrigger = true,
|
||||||
sticky = false,
|
sticky = false,
|
||||||
|
treeState,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
defaultState?: 'open' | 'closed';
|
defaultState?: 'open' | 'closed';
|
||||||
@@ -21,10 +22,12 @@
|
|||||||
* like its node.
|
* like its node.
|
||||||
*/
|
*/
|
||||||
sticky?: boolean;
|
sticky?: boolean;
|
||||||
|
/** Which nodes are open, to keep it or to open nodes from the outside. */
|
||||||
|
treeState?: CollapsibleTreeState;
|
||||||
children: Snippet;
|
children: Snippet;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let open = $state(new CollapsibleTreeState(defaultState));
|
let open = $state(treeState ?? new CollapsibleTreeState(defaultState));
|
||||||
|
|
||||||
setContext('collapsible-tree-state', open);
|
setContext('collapsible-tree-state', open);
|
||||||
setContext('collapsible-tree-side', side);
|
setContext('collapsible-tree-side', side);
|
||||||
|
|||||||
@@ -1,29 +1,93 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { untrack } from 'svelte';
|
||||||
import { CollapsibleTree, CollapsibleTreeNode } from '$lib/components/collapsible-tree/index';
|
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 { ScrollArea } from '$lib/components/ui/scroll-area/index';
|
||||||
import ExplorerTree from './ExplorerTree.svelte';
|
import ExplorerTree from './ExplorerTree.svelte';
|
||||||
|
import { clickedInExplorer } from './state.svelte';
|
||||||
import { i18n } from '$lib/i18n/i18n.svelte';
|
import { i18n } from '$lib/i18n/i18n.svelte';
|
||||||
import { engine } from '$lib/engine';
|
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);
|
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>
|
</script>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
The tree of the open folder: its folders and GPX files, one level read at a time. The files are
|
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).
|
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="flex flex-col h-full">
|
||||||
<div class="px-2 py-1 text-sm font-semibold shrink-0">{i18n._('menu.explorer')}</div>
|
<div class="px-2 py-1 text-sm font-semibold shrink-0">{i18n._('menu.explorer')}</div>
|
||||||
<ScrollArea class="grow min-h-0">
|
<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 -->
|
<!-- starts over when another folder is opened -->
|
||||||
{#key folder}
|
{#key folder}
|
||||||
<!-- the folder itself, which is open at first -->
|
<!-- the folder itself -->
|
||||||
<CollapsibleTree side="left" defaultState="open" sticky>
|
<CollapsibleTree side="left" treeState={trees.root} sticky>
|
||||||
<CollapsibleTreeNode id="folder">
|
<CollapsibleTreeNode id="folder">
|
||||||
{#snippet trigger()}
|
{#snippet trigger()}
|
||||||
<span class="grow truncate text-left" title={folder}>
|
<span class="grow truncate text-left" title={folder}>
|
||||||
@@ -34,8 +98,7 @@
|
|||||||
<!-- what is in it is only read when it is open -->
|
<!-- what is in it is only read when it is open -->
|
||||||
{#if opened}
|
{#if opened}
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
<!-- its folders are closed at first -->
|
<CollapsibleTree side="left" treeState={trees.folders}>
|
||||||
<CollapsibleTree side="left" defaultState="closed">
|
|
||||||
<ExplorerTree />
|
<ExplorerTree />
|
||||||
</CollapsibleTree>
|
</CollapsibleTree>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
import { CollapsibleTreeNode } from '$lib/components/collapsible-tree/index';
|
import { CollapsibleTreeNode } from '$lib/components/collapsible-tree/index';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { FileText } from '@lucide/svelte';
|
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 ExplorerTree from './ExplorerTree.svelte';
|
||||||
import { openPath } from '$lib/actions/file-actions';
|
import { openPath } from '$lib/actions/file-actions';
|
||||||
import { engine, type FolderEntry } from '$lib/engine';
|
import { engine, type FolderEntry } from '$lib/engine';
|
||||||
@@ -16,6 +19,8 @@
|
|||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
const { workspace, selection, folderVersion } = engine;
|
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([]);
|
let entries: FolderEntry[] = $state([]);
|
||||||
|
|
||||||
@@ -35,6 +40,9 @@
|
|||||||
|
|
||||||
/** Opens the file, or selects it if it is open already. */
|
/** Opens the file, or selects it if it is open already. */
|
||||||
function open(entry: FolderEntry, id: string | undefined) {
|
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) {
|
if (id !== undefined) {
|
||||||
engine.select([id]);
|
engine.select([id]);
|
||||||
} else {
|
} else {
|
||||||
@@ -67,6 +75,8 @@
|
|||||||
? 'bg-accent'
|
? 'bg-accent'
|
||||||
: ''} {id === undefined ? 'text-muted-foreground' : ''}"
|
: ''} {id === undefined ? 'text-muted-foreground' : ''}"
|
||||||
title={entry.path}
|
title={entry.path}
|
||||||
|
data-path={entry.path}
|
||||||
|
style="scroll-margin-top: {level.top}px"
|
||||||
onclick={() => open(entry, id)}
|
onclick={() => open(entry, id)}
|
||||||
>
|
>
|
||||||
<FileText size="16" class="shrink-0" />
|
<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 });
|
||||||
Reference in new issue
Block a user