From 6bc4a4323eae2f3eb418a8bca3f0df4f84b36fc7 Mon Sep 17 00:00:00 2001 From: Zhang Kris Date: Mon, 11 May 2026 22:55:52 +0800 Subject: [PATCH] feat: drag-and-drop .md/.txt/.html files to workspace creates new docs - Added dropFilesInWorkspace handler to useCloudDnd hook - Modified dropInWorkspace to detect file drops vs resource drops - Workspace onDrop handler now routes files to dropFilesInWorkspace - createDoc receives file content as initial doc body (for .md/.txt/.html) - Closes BoostIO/BoostNote-App#1151 (IssueHunt bounty) --- src/cloud/lib/hooks/sidebar/useCloudDnd.ts | 63 +++++++++++++++++++ .../lib/hooks/sidebar/useCloudSidebarTree.tsx | 12 +++- 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/src/cloud/lib/hooks/sidebar/useCloudDnd.ts b/src/cloud/lib/hooks/sidebar/useCloudDnd.ts index 292b694103..f99a6651e5 100644 --- a/src/cloud/lib/hooks/sidebar/useCloudDnd.ts +++ b/src/cloud/lib/hooks/sidebar/useCloudDnd.ts @@ -23,6 +23,10 @@ import { SerializedDocWithSupplemental } from '../../../interfaces/db/doc' import { SidebarDragState } from '../../../../design/lib/dnd' import { useToast } from '../../../../design/lib/stores/toast' import { getMapFromEntityArray } from '../../../../design/lib/utils/array' +import { SerializedTeam } from '../../../interfaces/db/team' + +const SUPPORTED_FILE_EXTENSIONS = ['.md', '.txt', '.html'] +const FILE_ENCODING = 'utf-8' export function useCloudDnd() { const { @@ -47,11 +51,17 @@ export function useCloudDnd() { body: UpdateDocRequestBody ) => Promise ) => { + const files = event.dataTransfer?.files + if (files != null && files.length > 0) { + return 'files' + } + const draggedResource = getDraggedResource(event) if (draggedResource === null) { return } + if (draggedResource.type === 'folder') { const folder = draggedResource.resource await updateFolder(folder, { @@ -72,6 +82,58 @@ export function useCloudDnd() { [] ) + const dropFilesInWorkspace = useCallback( + async ( + event: any, + workspaceId: string, + team: SerializedTeam, + createDoc: ( + team: SerializedTeam, + body: { workspaceId: string; title: string; content?: string } + ) => Promise<{ id: string }> + ) => { + const files = event.dataTransfer?.files + if (files == null || files.length === 0) { + return + } + + for (let i = 0; i < files.length; i++) { + const file = files[i] + const ext = getFileExtension(file.name) + if (!SUPPORTED_FILE_EXTENSIONS.includes(ext)) { + continue + } + + try { + const content = await readFileAsText(file) + const title = file.name.replace(ext, '') + await createDoc(team, { + workspaceId, + title, + content, + }) + } catch (err) { + console.warn('Failed to create doc from file:', file.name, err) + } + } + }, + [] + ) + + function getFileExtension(filename: string): string { + const lastDot = filename.lastIndexOf('.') + return lastDot >= 0 ? filename.slice(lastDot).toLowerCase() : '' + } + + async function readFileAsText(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = () => resolve(reader.result as string) + reader.onerror = () => reject(reader.error) + reader.readAsText(file, FILE_ENCODING) + }) + } + const dropInDocOrFolder = useCallback( async ( event: any, @@ -173,6 +235,7 @@ export function useCloudDnd() { return { dropInWorkspace, dropInDocOrFolder, + dropFilesInWorkspace, saveFolderTransferData, saveDocTransferData, clearDragTransferData, diff --git a/src/cloud/lib/hooks/sidebar/useCloudSidebarTree.tsx b/src/cloud/lib/hooks/sidebar/useCloudSidebarTree.tsx index aaee5fd9e3..b903ff4451 100644 --- a/src/cloud/lib/hooks/sidebar/useCloudSidebarTree.tsx +++ b/src/cloud/lib/hooks/sidebar/useCloudSidebarTree.tsx @@ -111,6 +111,7 @@ export function useCloudSidebarTree() { const { dropInDocOrFolder, dropInWorkspace, + dropFilesInWorkspace, saveFolderTransferData, saveDocTransferData, clearDragTransferData, @@ -262,8 +263,14 @@ export function useCloudSidebarTree() { currentUserIsCoreMember ? { dropIn: true, - onDrop: (event: any) => - dropInWorkspace(event, wp.id, updateFolder, updateDoc), + onDrop: (event: any) => { + const files = event.dataTransfer?.files + if (files != null && files.length > 0) { + dropFilesInWorkspace(event, wp.id, team, createDoc) + } else { + dropInWorkspace(event, wp.id, updateFolder, updateDoc) + } + }, controls: [ { icon: mdiTextBoxPlus, @@ -1117,4 +1124,5 @@ type CloudTreeItem = { onDragStart?: (event: any) => void onDrop?: (event: any, position?: SidebarDragState) => void onDragEnd?: (event: any) => void + onDropFiles?: (event: any) => void }