diff --git a/src/cloud/components/ContentManager/index.tsx b/src/cloud/components/ContentManager/index.tsx index 946e75a32c..30e8ffc6cb 100644 --- a/src/cloud/components/ContentManager/index.tsx +++ b/src/cloud/components/ContentManager/index.tsx @@ -42,6 +42,8 @@ const ContentManager = ({ folders, workspacesMap, currentUserIsCoreMember, + currentWorkspaceId, + currentFolderId, page, }: ContentManagerProps) => { const { preferences, setPreferences } = usePreferences() @@ -118,8 +120,12 @@ const ContentManager = ({ [setPreferences] ) - const { dropInDocOrFolder, saveDocTransferData, clearDragTransferData } = - useCloudDnd() + const { + dropFilesAsDocs, + dropInDocOrFolder, + saveDocTransferData, + clearDragTransferData, + } = useCloudDnd() const onDragStartDoc = useCallback( (event: any, doc: SerializedDocWithSupplemental) => { @@ -145,8 +151,28 @@ const ContentManager = ({ [clearDragTransferData] ) + const onDragOverFiles = useCallback((event: React.DragEvent) => { + if (event.dataTransfer.types.includes('Files')) { + event.preventDefault() + } + }, []) + + const onDropFiles = useCallback( + (event: React.DragEvent) => { + if (currentWorkspaceId == null) { + return + } + + dropFilesAsDocs(event, team, { + workspaceId: currentWorkspaceId, + parentFolderId: currentFolderId, + }) + }, + [currentFolderId, currentWorkspaceId, dropFilesAsDocs, team] + ) + return ( - +
diff --git a/src/cloud/lib/hooks/sidebar/useCloudDnd.ts b/src/cloud/lib/hooks/sidebar/useCloudDnd.ts index 292b694103..75dc0d9380 100644 --- a/src/cloud/lib/hooks/sidebar/useCloudDnd.ts +++ b/src/cloud/lib/hooks/sidebar/useCloudDnd.ts @@ -1,5 +1,9 @@ import { useCallback } from 'react' -import { UpdateDocRequestBody } from '../../../api/teams/docs' +import { + createDoc, + CreateDocRequestBody, + UpdateDocRequestBody, +} from '../../../api/teams/docs' import { UpdateFolderRequestBody } from '../../../api/teams/folders' import { moveResource } from '../../../api/teams/resources' import { @@ -20,23 +24,107 @@ import { } from '../../utils/patterns' import { SerializedFolderWithBookmark } from '../../../interfaces/db/folder' import { SerializedDocWithSupplemental } from '../../../interfaces/db/doc' +import { SerializedTeam } from '../../../interfaces/db/team' import { SidebarDragState } from '../../../../design/lib/dnd' import { useToast } from '../../../../design/lib/stores/toast' import { getMapFromEntityArray } from '../../../../design/lib/utils/array' +const textFileExtensions = new Set(['.md', '.txt', '.html', '.htm']) + +function getDroppedFiles(event: any) { + return Array.from(event.dataTransfer?.files || []).filter((file) => { + const lowerCaseName = file.name.toLowerCase() + return Array.from(textFileExtensions).some((extension) => + lowerCaseName.endsWith(extension) + ) + }) +} + +function getDocTitleFromFileName(fileName: string) { + const matchingExtension = Array.from(textFileExtensions).find((extension) => + fileName.toLowerCase().endsWith(extension) + ) + + if (matchingExtension == null) { + return fileName + } + + return fileName.slice(0, -matchingExtension.length) || fileName +} + +function readTextFile(file: File) { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = () => resolve(String(reader.result || '')) + reader.onerror = () => reject(reader.error) + reader.readAsText(file) + }) +} + export function useCloudDnd() { const { updateFoldersMap, updateDocsMap, updateWorkspacesMap, + updateParentFolderOfDoc, + updateParentWorkspaceOfDoc, setCurrentPath, } = useNav() const { pageDoc, pageFolder } = usePage() const { pushApiErrorMessage } = useToast() + const dropFilesAsDocs = useCallback( + async ( + event: any, + team: SerializedTeam, + destination: Pick + ) => { + const files = getDroppedFiles(event) + if (files.length === 0) { + return false + } + + event.preventDefault() + event.stopPropagation() + + try { + for (const file of files) { + const content = await readTextFile(file) + const { doc } = await createDoc( + { id: team.id }, + { + ...destination, + title: getDocTitleFromFileName(file.name), + content, + } + ) + + updateDocsMap([doc.id, doc]) + + if (doc.parentFolder != null) { + updateParentFolderOfDoc(doc) + } else if (doc.workspace != null) { + updateParentWorkspaceOfDoc(doc) + } + } + } catch (error) { + pushApiErrorMessage(error) + } + + return true + }, + [ + pushApiErrorMessage, + updateDocsMap, + updateParentFolderOfDoc, + updateParentWorkspaceOfDoc, + ] + ) + const dropInWorkspace = useCallback( async ( event: any, + team: SerializedTeam, workspaceId: string, updateFolder: ( folder: FolderDataTransferItem, @@ -47,6 +135,11 @@ export function useCloudDnd() { body: UpdateDocRequestBody ) => Promise ) => { + const droppedFiles = await dropFilesAsDocs(event, team, { workspaceId }) + if (droppedFiles) { + return + } + const draggedResource = getDraggedResource(event) if (draggedResource === null) { return @@ -69,7 +162,7 @@ export function useCloudDnd() { }) } }, - [] + [dropFilesAsDocs] ) const dropInDocOrFolder = useCallback( @@ -171,6 +264,7 @@ export function useCloudDnd() { }, []) return { + dropFilesAsDocs, dropInWorkspace, dropInDocOrFolder, saveFolderTransferData, diff --git a/src/cloud/lib/hooks/sidebar/useCloudSidebarTree.tsx b/src/cloud/lib/hooks/sidebar/useCloudSidebarTree.tsx index aaee5fd9e3..7be81c5caa 100644 --- a/src/cloud/lib/hooks/sidebar/useCloudSidebarTree.tsx +++ b/src/cloud/lib/hooks/sidebar/useCloudSidebarTree.tsx @@ -109,6 +109,7 @@ export function useCloudSidebarTree() { } = useSidebarCollapse() const { + dropFilesAsDocs, dropInDocOrFolder, dropInWorkspace, saveFolderTransferData, @@ -263,7 +264,7 @@ export function useCloudSidebarTree() { ? { dropIn: true, onDrop: (event: any) => - dropInWorkspace(event, wp.id, updateFolder, updateDoc), + dropInWorkspace(event, team, wp.id, updateFolder, updateDoc), controls: [ { icon: mdiTextBoxPlus, @@ -339,15 +340,24 @@ export function useCloudSidebarTree() { const coreRestrictedFeatures: Partial = currentUserIsCoreMember ? { - onDrop: (event: any, position: SidebarDragState) => - dropInDocOrFolder( + onDrop: async (event: any, position: SidebarDragState) => { + const droppedFiles = await dropFilesAsDocs(event, team, { + parentFolderId: folder.id, + workspaceId: folder.workspaceId, + }) + if (droppedFiles) { + return + } + + return dropInDocOrFolder( event, { type: 'folder', resource: folderToDataTransferItem(folder), }, position - ), + ) + }, onDragStart: (event: any) => { saveFolderTransferData(event, folder) }, @@ -941,6 +951,7 @@ export function useCloudSidebarTree() { treeSendingMap, sideBarOpenedFolderIdsSet, dropInDocOrFolder, + dropFilesAsDocs, saveFolderTransferData, clearDragTransferData, toggleFolderBookmark,