diff --git a/apps/web/src/components/chat/workspaceFileDrop.test.ts b/apps/web/src/components/chat/workspaceFileDrop.test.ts index ec5d074a3eb7..3a81e35c2a2d 100644 --- a/apps/web/src/components/chat/workspaceFileDrop.test.ts +++ b/apps/web/src/components/chat/workspaceFileDrop.test.ts @@ -8,6 +8,7 @@ import { function makeDragEvent(options?: { types?: string[]; files?: File[]; + items?: NonNullable; movedWithinTarget?: boolean; }) { const preventDefault = vi.fn(); @@ -16,6 +17,7 @@ function makeDragEvent(options?: { types: options?.types ?? ["Files"], files: options?.files ?? [], dropEffect: "none", + ...(options?.items === undefined ? {} : { items: options.items }), }, relatedTarget: options?.movedWithinTarget ? ({} as EventTarget) : null, currentTarget: { @@ -75,4 +77,42 @@ describe("makeWorkspaceFileDropHandlers", () => { expect(setDragActive).toHaveBeenCalledWith(false); expect(addFiles).toHaveBeenCalledWith([file]); }); + + it("skips dropped directories in mixed drops", () => { + const file = new File(["contents"], "example.txt", { type: "text/plain" }); + const directory = { + kind: "file", + getAsFile: () => null, + webkitGetAsEntry: () => ({ isDirectory: true }), + }; + const { host, addFiles } = makeHost(); + const { event } = makeDragEvent({ + items: [ + directory, + { + kind: "file", + getAsFile: () => file, + webkitGetAsEntry: () => ({ isDirectory: false }), + }, + ], + }); + + makeWorkspaceFileDropHandlers(host).onDrop(event); + + expect(addFiles).toHaveBeenCalledWith([file]); + }); + + it("passes an empty list when a drop only contains a directory", () => { + const directory = { + kind: "file", + getAsFile: () => null, + webkitGetAsEntry: () => ({ isDirectory: true }), + }; + const { host, addFiles } = makeHost(); + const { event } = makeDragEvent({ items: [directory] }); + + makeWorkspaceFileDropHandlers(host).onDrop(event); + + expect(addFiles).toHaveBeenCalledWith([]); + }); }); diff --git a/apps/web/src/components/chat/workspaceFileDrop.ts b/apps/web/src/components/chat/workspaceFileDrop.ts index 132a8051e159..3e6dbf3570f4 100644 --- a/apps/web/src/components/chat/workspaceFileDrop.ts +++ b/apps/web/src/components/chat/workspaceFileDrop.ts @@ -2,6 +2,11 @@ export interface WorkspaceFileDragEvent { readonly dataTransfer: { readonly types: ReadonlyArray; readonly files: Iterable; + readonly items?: Iterable<{ + readonly kind: string; + getAsFile(): File | null; + webkitGetAsEntry(): { readonly isDirectory: boolean } | null; + }>; dropEffect: string; }; readonly relatedTarget: EventTarget | null; @@ -24,6 +29,18 @@ function movedWithinDropTarget(event: WorkspaceFileDragEvent): boolean { return event.relatedTarget !== null && event.currentTarget.contains(event.relatedTarget as Node); } +function droppedFiles(dataTransfer: WorkspaceFileDragEvent["dataTransfer"]): File[] { + if (dataTransfer.items === undefined) return Array.from(dataTransfer.files); + + const files: File[] = []; + for (const item of dataTransfer.items) { + if (item.kind !== "file" || item.webkitGetAsEntry()?.isDirectory === true) continue; + const file = item.getAsFile(); + if (file !== null) files.push(file); + } + return files; +} + export function makeWorkspaceFileDropHandlers(host: WorkspaceFileDropHost) { return { onDragEnter(event: WorkspaceFileDragEvent) { @@ -48,7 +65,7 @@ export function makeWorkspaceFileDropHandlers(host: WorkspaceFileDropHost) { if (!isFileDrag(event)) return; event.preventDefault(); host.setDragActive(false); - host.addFiles(Array.from(event.dataTransfer.files)); + host.addFiles(droppedFiles(event.dataTransfer)); }, }; }