Pārlūkot izejas kodu

Upload files dropped onto the file tree (#8064)

GitOrigin-RevId: 97043661e344b72ff742326c9dc2809e46d0bb9c
Alf Eaton 4 gadi atpakaļ
vecāks
revīzija
44b241b5ca

+ 18 - 0
package-lock.json

@@ -34542,6 +34542,7 @@
         "@uppy/core": "^1.15.0",
         "@uppy/dashboard": "^1.11.0",
         "@uppy/react": "^1.11.0",
+        "@uppy/utils": "^4.0.7",
         "@uppy/xhr-upload": "^1.6.8",
         "abort-controller": "^3.0.0",
         "accepts": "^1.3.7",
@@ -34819,6 +34820,14 @@
         "lodash": "^4.17.15"
       }
     },
+    "services/web/node_modules/@uppy/utils": {
+      "version": "4.0.7",
+      "resolved": "https://registry.npmjs.org/@uppy/utils/-/utils-4.0.7.tgz",
+      "integrity": "sha512-nKViMT8XchKy+NWpb3DtVKuzZBmW7au26LrMq89EsvTwIOT6UR9+7bmz/+zr3+lc7UC7vMgNChIC6G+/Ya9wWQ==",
+      "dependencies": {
+        "lodash.throttle": "^4.1.1"
+      }
+    },
     "services/web/node_modules/ansi-styles": {
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -42084,6 +42093,7 @@
         "@uppy/core": "^1.15.0",
         "@uppy/dashboard": "^1.11.0",
         "@uppy/react": "^1.11.0",
+        "@uppy/utils": "^4.0.7",
         "@uppy/xhr-upload": "^1.6.8",
         "abort-controller": "^3.0.0",
         "accepts": "^1.3.7",
@@ -42330,6 +42340,14 @@
             "lodash": "^4.17.15"
           }
         },
+        "@uppy/utils": {
+          "version": "4.0.7",
+          "resolved": "https://registry.npmjs.org/@uppy/utils/-/utils-4.0.7.tgz",
+          "integrity": "sha512-nKViMT8XchKy+NWpb3DtVKuzZBmW7au26LrMq89EsvTwIOT6UR9+7bmz/+zr3+lc7UC7vMgNChIC6G+/Ya9wWQ==",
+          "requires": {
+            "lodash.throttle": "^4.1.1"
+          }
+        },
         "ansi-styles": {
           "version": "4.3.0",
           "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",

+ 45 - 7
services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-upload-doc.js

@@ -1,6 +1,6 @@
 import { Trans } from 'react-i18next'
 import { Alert, Button } from 'react-bootstrap'
-import { useCallback, useState } from 'react'
+import { useCallback, useEffect, useState } from 'react'
 import PropTypes from 'prop-types'
 import Uppy from '@uppy/core'
 import XHRUpload from '@uppy/xhr-upload'
@@ -14,7 +14,8 @@ import { refreshProjectMetadata } from '../../../util/api'
 import ErrorMessage from '../error-message'
 
 export default function FileTreeUploadDoc() {
-  const { parentFolderId, cancel, isDuplicate } = useFileTreeActionable()
+  const { parentFolderId, cancel, isDuplicate, droppedFiles, setDroppedFiles } =
+    useFileTreeActionable()
   const { _id: projectId } = useProjectContext(projectContextPropTypes)
 
   const [error, setError] = useState()
@@ -28,17 +29,23 @@ export default function FileTreeUploadDoc() {
   // calculate conflicts
   const buildConflicts = files =>
     Object.values(files).filter(file =>
-      isDuplicate(parentFolderId, file.meta.name)
+      isDuplicate(file.meta.targetFolderId ?? parentFolderId, file.meta.name)
     )
 
-  // initialise the Uppy object
-  const uppy = useUppy(() => {
+  const buildEndpoint = (projectId, targetFolderId) => {
     let endpoint = `/project/${projectId}/upload`
 
-    if (parentFolderId) {
-      endpoint += `?folder_id=${parentFolderId}`
+    if (targetFolderId) {
+      endpoint += `?folder_id=${targetFolderId}`
     }
 
+    return endpoint
+  }
+
+  // initialise the Uppy object
+  const uppy = useUppy(() => {
+    const endpoint = buildEndpoint(projectId, parentFolderId)
+
     return (
       new Uppy({
         // logger: Uppy.debugLogger,
@@ -113,6 +120,37 @@ export default function FileTreeUploadDoc() {
     )
   })
 
+  useEffect(() => {
+    if (uppy && droppedFiles) {
+      uppy.setOptions({
+        autoProceed: false,
+      })
+      for (const file of droppedFiles.files) {
+        const fileId = uppy.addFile({
+          name: file.name,
+          type: file.type,
+          data: file,
+          source: 'Local',
+          isRemote: false,
+          meta: {
+            targetFolderId: droppedFiles.targetFolderId,
+          },
+        })
+        const uppyFile = uppy.getFile(fileId)
+        uppy.setFileState(fileId, {
+          xhrUpload: {
+            ...uppyFile.xhrUpload,
+            endpoint: buildEndpoint(projectId, droppedFiles.targetFolderId),
+          },
+        })
+      }
+    }
+
+    return () => {
+      setDroppedFiles(null)
+    }
+  }, [uppy, droppedFiles, setDroppedFiles, projectId])
+
   // handle forced overwriting of conflicting files
   const handleOverwrite = useCallback(() => {
     setOverwrite(true)

+ 1 - 1
services/web/frontend/js/features/file-tree/components/file-tree-draggable-preview-layer.js

@@ -20,7 +20,7 @@ function FileTreeDraggablePreviewLayer({ isOver }) {
     ? ref.current.getBoundingClientRect()
     : null
 
-  if (!isDragging) {
+  if (!isDragging || !item.title) {
     return null
   }
 

+ 5 - 0
services/web/frontend/js/features/file-tree/contexts/file-tree-actionable.js

@@ -5,6 +5,7 @@ import {
   useReducer,
   useContext,
   useEffect,
+  useState,
 } from 'react'
 import PropTypes from 'prop-types'
 
@@ -132,6 +133,8 @@ export function FileTreeActionableProvider({ children }) {
   const { fileTreeData, dispatchRename, dispatchMove } = useFileTreeData()
   const { selectedEntityIds } = useFileTreeSelectable()
 
+  const [droppedFiles, setDroppedFiles] = useState(null)
+
   const startRenaming = useCallback(() => {
     dispatch({ type: ACTION_TYPES.START_RENAME })
   }, [])
@@ -363,6 +366,8 @@ export function FileTreeActionableProvider({ children }) {
     finishCreatingDoc,
     finishCreatingLinkedFile,
     cancel,
+    droppedFiles,
+    setDroppedFiles,
   }
 
   return (

+ 22 - 5
services/web/frontend/js/features/file-tree/contexts/file-tree-draggable.js

@@ -1,9 +1,14 @@
 import { useRef, useEffect, useState } from 'react'
 import PropTypes from 'prop-types'
 import { useTranslation } from 'react-i18next'
+import getDroppedFiles from '@uppy/utils/lib/getDroppedFiles'
 
 import { DndProvider, createDndContext, useDrag, useDrop } from 'react-dnd'
-import { HTML5Backend, getEmptyImage } from 'react-dnd-html5-backend'
+import {
+  HTML5Backend,
+  getEmptyImage,
+  NativeTypes,
+} from 'react-dnd-html5-backend'
 
 import {
   findAllInTreeOrThrow,
@@ -119,20 +124,32 @@ const editorContextPropTypes = {
 }
 
 export function useDroppable(droppedEntityId) {
-  const { finishMoving } = useFileTreeActionable()
+  const { finishMoving, setDroppedFiles, startUploadingDocOrFile } =
+    useFileTreeActionable()
 
   const [{ isOver }, dropRef] = useDrop({
-    accept: DRAGGABLE_TYPE,
+    accept: [DRAGGABLE_TYPE, NativeTypes.FILE],
     canDrop: (item, monitor) => {
       const isOver = monitor.isOver({ shallow: true })
       if (!isOver) return false
-      if (item.forbiddenFolderIds.has(droppedEntityId)) return false
+      if (
+        item.type === DRAGGABLE_TYPE &&
+        item.forbiddenFolderIds.has(droppedEntityId)
+      )
+        return false
       return true
     },
     drop: (item, monitor) => {
       const didDropInChild = monitor.didDrop()
       if (didDropInChild) return
-      finishMoving(droppedEntityId, item.draggedEntityIds)
+      if (item.type === DRAGGABLE_TYPE) {
+        finishMoving(droppedEntityId, item.draggedEntityIds)
+      } else {
+        getDroppedFiles(item).then(files => {
+          setDroppedFiles({ files, targetFolderId: droppedEntityId })
+          startUploadingDocOrFile()
+        })
+      }
     },
     collect: monitor => ({
       isOver: monitor.canDrop(),

+ 1 - 0
services/web/package.json

@@ -76,6 +76,7 @@
     "@uppy/core": "^1.15.0",
     "@uppy/dashboard": "^1.11.0",
     "@uppy/react": "^1.11.0",
+    "@uppy/utils": "^4.0.7",
     "@uppy/xhr-upload": "^1.6.8",
     "abort-controller": "^3.0.0",
     "accepts": "^1.3.7",