Skip to content

Commit 199367f

Browse files
authored
Merge pull request #69 from abdel-17/file-upload-toast
preview: show toast when file upload is complete
2 parents 9c3fbfb + 5e0b47e commit 199367f

File tree

7 files changed

+214
-147
lines changed

7 files changed

+214
-147
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script lang="ts">
2+
import { FolderIcon } from "@lucide/svelte";
3+
4+
export let description: string;
5+
</script>
6+
7+
<div class="mt-1 flex items-center gap-2">
8+
<FolderIcon role="presentation" size={20} class="fill-blue-300" />
9+
<span class="!text-base !font-normal">{description}</span>
10+
</div>

sites/preview/src/lib/components/tree/Tree.svelte

+76-65
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
<script lang="ts">
2-
import { FileNode, FolderNode, type FileTreeNode } from "$lib/tree.svelte.js";
2+
import { FileNode, FileTree, FolderNode, type FileTreeNode } from "$lib/tree.svelte.js";
33
import { composeEventHandlers, formatSize } from "$lib/utils.js";
4-
import { FolderIcon } from "@lucide/svelte";
54
import { ContextMenu } from "bits-ui";
65
import {
76
Tree,
@@ -12,18 +11,19 @@
1211
import { toast } from "svelte-sonner";
1312
import type { EventHandler } from "svelte/elements";
1413
import { SvelteSet } from "svelte/reactivity";
15-
import { fly } from "svelte/transition";
14+
import FileDropToastDescription from "./FileDropToastDescription.svelte";
1615
import NameConflictDialog from "./NameConflictDialog.svelte";
1716
import NameFormDialog from "./NameFormDialog.svelte";
1817
import TreeContextMenu from "./TreeContextMenu.svelte";
1918
import TreeItem from "./TreeItem.svelte";
2019
import {
2120
createContextMenuState,
21+
createFileDropToastState,
2222
createFileInputState,
2323
createNameConflictDialogState,
2424
createNameFormDialogState,
2525
} from "./state.svelte.js";
26-
import type { FileDropState, TreeItemState, TreeProps, UploadFilesArgs } from "./types.js";
26+
import type { TreeItemState, TreeProps, UploadFilesArgs } from "./types.js";
2727
2828
let {
2929
tree,
@@ -67,14 +67,6 @@
6767
let fileInput: HTMLInputElement | null = $state.raw(null);
6868
6969
let focusedItemId: string | undefined = $state.raw();
70-
let fileDropState: FileDropState | undefined = $state.raw();
71-
72-
const nameConflictDialogState = createNameConflictDialogState();
73-
const nameFormDialogState = createNameFormDialogState();
74-
const fileInputState = createFileInputState({
75-
ref: () => fileInput,
76-
});
77-
7870
const pasteDirection = $derived.by(() => {
7971
if (pasteOperation === undefined || focusedItemId === undefined) {
8072
return;
@@ -87,6 +79,12 @@
8779
return "After";
8880
});
8981
82+
const nameConflictDialogState = createNameConflictDialogState();
83+
const nameFormDialogState = createNameFormDialogState();
84+
const fileInputState = createFileInputState({
85+
ref: () => fileInput,
86+
});
87+
9088
function showAlreadyExistsToast(name: string) {
9189
toast.error(`An item with the name "${name}" already exists`);
9290
}
@@ -117,7 +115,7 @@
117115
});
118116
}
119117
120-
async function handleUploadFiles({ target, files }: UploadFilesArgs) {
118+
function handleUploadFiles({ target, files }: UploadFilesArgs) {
121119
for (const child of target.children) {
122120
for (const file of files) {
123121
if (child.name === file.name) {
@@ -127,10 +125,21 @@
127125
}
128126
}
129127
130-
const didUpload = await onUploadFiles({ target, files });
131-
if (didUpload) {
132-
// TODO: show toast after upload is done
133-
}
128+
const uploadPromise = Promise.resolve(onUploadFiles({ target, files }));
129+
const name = target instanceof FileTree ? "/" : target.name;
130+
toast.promise(uploadPromise, {
131+
loading: `Uploading ${files.length} file(s) to ${name}`,
132+
success: (didUpload) => {
133+
if (!didUpload) {
134+
return "Failed to upload files";
135+
}
136+
137+
return `Uploaded ${files.length} file(s) to ${name}`;
138+
},
139+
error: (error) => {
140+
throw error;
141+
},
142+
});
134143
}
135144
136145
const contextMenuState = createContextMenuState({
@@ -164,19 +173,23 @@
164173
},
165174
});
166175
167-
const handleTriggerContextMenu: EventHandler<Event, HTMLDivElement> = (event) => {
168-
if (event.defaultPrevented) {
169-
// A tree item handled the event.
170-
return;
171-
}
172-
173-
if (event.target instanceof Element && event.target.closest("[role='treeitem']") === null) {
174-
contextMenuState.setTarget({
175-
type: "tree",
176-
tree: () => tree,
176+
const fileDropToastState = createFileDropToastState({
177+
onShow: ({ target, toastId }) => {
178+
return toast("Drop files to upload them to:", {
179+
description: FileDropToastDescription as any,
180+
componentProps: {
181+
description: target.type === "tree" ? "/" : target.item().node.name,
182+
},
183+
classes: {
184+
toast: "pointer-events-none !bg-blue-100",
185+
title: "!text-sm !font-semibold",
186+
},
187+
id: toastId,
188+
duration: Number.POSITIVE_INFINITY,
177189
});
178-
}
179-
};
190+
},
191+
onDismiss: ({ toastId }) => toast.dismiss(toastId),
192+
});
180193
181194
function handleResolveNameConflict({ operation, name }: ResolveNameConflictArgs) {
182195
return new Promise<NameConflictResolution>((resolve) => {
@@ -223,18 +236,34 @@
223236
focusedItemId = undefined;
224237
};
225238
239+
function isOrInsideTreeItem(target: EventTarget | null) {
240+
return target instanceof Element && target.closest("[role='treeitem']") !== null;
241+
}
242+
243+
const handleTriggerContextMenu: EventHandler<Event, HTMLDivElement> = (event) => {
244+
if (isOrInsideTreeItem(event.target)) {
245+
return;
246+
}
247+
248+
contextMenuState.setTarget({
249+
type: "tree",
250+
tree: () => tree,
251+
});
252+
};
253+
226254
const handleTriggerDragOver: EventHandler<DragEvent, HTMLDivElement> = (event) => {
227-
if (event.defaultPrevented) {
228-
// A tree item handled the event.
255+
if (isOrInsideTreeItem(event.target)) {
229256
return;
230257
}
231258
232-
if (event.dataTransfer?.types.includes("Files")) {
233-
fileDropState = {
234-
type: "tree",
235-
};
236-
event.preventDefault();
259+
if (!event.dataTransfer?.types.includes("Files")) {
260+
return;
237261
}
262+
263+
fileDropToastState.setTarget({
264+
type: "tree",
265+
});
266+
event.preventDefault();
238267
};
239268
240269
const handleTriggerDragLeave: EventHandler<DragEvent, HTMLDivElement> = (event) => {
@@ -243,14 +272,11 @@
243272
return;
244273
}
245274
246-
fileDropState = undefined;
275+
fileDropToastState.dismiss();
247276
};
248277
249278
const handleTriggerDrop: EventHandler<DragEvent, HTMLDivElement> = (event) => {
250-
fileDropState = undefined;
251-
252-
if (event.defaultPrevented) {
253-
// A tree item handled the event.
279+
if (isOrInsideTreeItem(event.target)) {
254280
return;
255281
}
256282
@@ -259,6 +285,7 @@
259285
return;
260286
}
261287
288+
fileDropToastState.dismiss();
262289
handleUploadFiles({
263290
target: tree,
264291
files,
@@ -271,14 +298,15 @@
271298
focusedItemId = undefined;
272299
}
273300
274-
if (fileDropState?.type === "item" && fileDropState.item() === target) {
275-
fileDropState = undefined;
276-
}
277-
278301
const menuTarget = contextMenuState.target();
279302
if (menuTarget?.type === "item" && menuTarget.item() === target) {
280303
contextMenuState.close();
281304
}
305+
306+
const fileDropToastTarget = fileDropToastState.target();
307+
if (fileDropToastTarget?.type === "item" && fileDropToastTarget.item() === target) {
308+
fileDropToastState.dismiss();
309+
}
282310
}
283311
</script>
284312

@@ -327,7 +355,9 @@
327355
{#snippet item({ item })}
328356
<TreeItem
329357
{item}
330-
bind:fileDropState
358+
fileDropToastTarget={fileDropToastState.target()}
359+
onFileDropToastTargetChange={fileDropToastState.setTarget}
360+
onDismissFileDropToast={fileDropToastState.dismiss}
331361
onExpand={handleExpand}
332362
onCollapse={handleCollapse}
333363
onRename={handleRename}
@@ -338,25 +368,6 @@
338368
/>
339369
{/snippet}
340370
</Tree>
341-
342-
{#if fileDropState !== undefined}
343-
<div
344-
class="absolute start-4 bottom-4 z-50 rounded-lg bg-blue-200 px-6 py-3 shadow"
345-
transition:fly={{ x: "-100%" }}
346-
>
347-
<div>Drop files to upload them to</div>
348-
<div class="mt-1 flex items-center justify-center gap-2">
349-
<FolderIcon role="presentation" size={16} strokeWidth={3} />
350-
<span class="font-semibold">
351-
{#if fileDropState.type === "tree"}
352-
/
353-
{:else}
354-
{fileDropState.item().node.name}
355-
{/if}
356-
</span>
357-
</div>
358-
</div>
359-
{/if}
360371
</ContextMenu.Trigger>
361372
</TreeContextMenu>
362373

sites/preview/src/lib/components/tree/TreeContextMenu.svelte

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { ContextMenu } from "bits-ui";
33
import type { Snippet } from "svelte";
44
import TreeContextMenuItem from "./TreeContextMenuItem.svelte";
5-
import type { TreeContextMenuTarget } from "./state.svelte.js";
5+
import type { ContextMenuTarget } from "./state.svelte.js";
66
77
const {
88
target,
@@ -16,7 +16,7 @@
1616
onUploadFiles,
1717
onClose,
1818
}: {
19-
target: TreeContextMenuTarget | undefined;
19+
target: ContextMenuTarget | undefined;
2020
children: Snippet;
2121
onRename: () => void;
2222
onCopy: () => void;

sites/preview/src/lib/components/tree/TreeItem.svelte

+30-20
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,27 @@
33
import { ChevronDownIcon, FileIcon, FolderIcon, FolderOpenIcon } from "@lucide/svelte";
44
import { TreeItem, type TreeItemProps } from "svelte-file-tree";
55
import type { EventHandler } from "svelte/elements";
6-
import type { TreeContextMenuTarget } from "./state.svelte.js";
7-
import type { FileDropState, TreeItemState, UploadFilesArgs } from "./types.js";
6+
import type { ContextMenuTarget, FileDropToastTarget } from "./state.svelte.js";
7+
import type { TreeItemState, UploadFilesArgs } from "./types.js";
88
99
interface Props extends Omit<TreeItemProps, "children"> {
1010
item: TreeItemState;
11-
fileDropState: FileDropState | undefined;
11+
fileDropToastTarget: FileDropToastTarget | undefined;
12+
onFileDropToastTargetChange: (value: FileDropToastTarget) => void;
13+
onDismissFileDropToast: () => void;
1214
onExpand: (target: TreeItemState) => void;
1315
onCollapse: (target: TreeItemState) => void;
1416
onRename: (target: TreeItemState) => void;
15-
onContextMenuTargetChange: (value: TreeContextMenuTarget) => void;
17+
onContextMenuTargetChange: (value: ContextMenuTarget) => void;
1618
onUploadFiles: (args: UploadFilesArgs) => void;
1719
onCleanup: (target: TreeItemState) => void;
1820
}
1921
2022
let {
2123
item,
22-
fileDropState = $bindable(),
24+
fileDropToastTarget,
25+
onFileDropToastTargetChange,
26+
onDismissFileDropToast,
2327
onExpand,
2428
onCollapse,
2529
onRename,
@@ -35,8 +39,8 @@
3539
...rest
3640
}: Props = $props();
3741
38-
const isFileDropTarget = $derived(
39-
fileDropState?.type === "item" && fileDropState.item() === item,
42+
const isFileDropToastTarget = $derived(
43+
fileDropToastTarget?.type === "item" && fileDropToastTarget.item() === item,
4044
);
4145
4246
const handleKeyDown: EventHandler<KeyboardEvent, HTMLDivElement> = (event) => {
@@ -63,34 +67,40 @@
6367
};
6468
6569
const handleDragOver: EventHandler<DragEvent, HTMLDivElement> = (event) => {
66-
if (item.disabled || item.node.type === "file") {
70+
if (!event.dataTransfer?.types.includes("Files")) {
6771
return;
6872
}
6973
70-
if (event.dataTransfer?.types.includes("Files")) {
71-
fileDropState = {
72-
type: "item",
73-
item: () => item,
74-
};
75-
event.preventDefault();
76-
}
77-
};
74+
event.preventDefault();
7875
79-
const handleDrop: EventHandler<DragEvent, HTMLDivElement> = (event) => {
8076
if (item.disabled || item.node.type === "file") {
77+
onDismissFileDropToast();
8178
return;
8279
}
8380
81+
onFileDropToastTargetChange({
82+
type: "item",
83+
item: () => item,
84+
});
85+
};
86+
87+
const handleDrop: EventHandler<DragEvent, HTMLDivElement> = (event) => {
8488
const files = event.dataTransfer?.files;
8589
if (files === undefined || files.length === 0) {
8690
return;
8791
}
8892
93+
onDismissFileDropToast();
94+
event.preventDefault();
95+
96+
if (item.disabled || item.node.type === "file") {
97+
return;
98+
}
99+
89100
onUploadFiles({
90101
target: item.node,
91102
files,
92103
});
93-
event.preventDefault();
94104
};
95105
96106
const handleToggleClick: EventHandler<MouseEvent, HTMLButtonElement> = (event) => {
@@ -123,10 +133,10 @@
123133
{
124134
"opacity-50": item.dragged,
125135
"before:pointer-events-none before:absolute before:-inset-0 before:rounded-[inherit] before:border-2":
126-
dropPosition !== undefined || isFileDropTarget,
136+
dropPosition !== undefined || isFileDropToastTarget,
127137
"before:border-neutral-300 before:border-t-red-500": dropPosition === "before",
128138
"before:border-neutral-300 before:border-b-red-500": dropPosition === "after",
129-
"before:border-red-500": dropPosition === "inside" || isFileDropTarget,
139+
"before:border-red-500": dropPosition === "inside" || isFileDropToastTarget,
130140
},
131141
className,
132142
]}

0 commit comments

Comments
 (0)