|
@@ -1,37 +1,80 @@
|
|
|
import { useTranslation } from 'react-i18next'
|
|
import { useTranslation } from 'react-i18next'
|
|
|
-import { Button } from 'react-bootstrap'
|
|
|
|
|
-import { useCallback, useEffect, useState } from 'react'
|
|
|
|
|
|
|
+import { useCallback, useEffect, useRef, useState } from 'react'
|
|
|
import Uppy from '@uppy/core'
|
|
import Uppy from '@uppy/core'
|
|
|
import XHRUpload from '@uppy/xhr-upload'
|
|
import XHRUpload from '@uppy/xhr-upload'
|
|
|
import { Dashboard } from '@uppy/react'
|
|
import { Dashboard } from '@uppy/react'
|
|
|
import { useFileTreeActionable } from '../../../contexts/file-tree-actionable'
|
|
import { useFileTreeActionable } from '../../../contexts/file-tree-actionable'
|
|
|
import { useProjectContext } from '../../../../../shared/context/project-context'
|
|
import { useProjectContext } from '../../../../../shared/context/project-context'
|
|
|
import * as eventTracking from '../../../../../infrastructure/event-tracking'
|
|
import * as eventTracking from '../../../../../infrastructure/event-tracking'
|
|
|
-
|
|
|
|
|
import '@uppy/core/dist/style.css'
|
|
import '@uppy/core/dist/style.css'
|
|
|
import '@uppy/dashboard/dist/style.css'
|
|
import '@uppy/dashboard/dist/style.css'
|
|
|
import { refreshProjectMetadata } from '../../../util/api'
|
|
import { refreshProjectMetadata } from '../../../util/api'
|
|
|
import ErrorMessage from '../error-message'
|
|
import ErrorMessage from '../error-message'
|
|
|
import { debugConsole } from '@/utils/debugging'
|
|
import { debugConsole } from '@/utils/debugging'
|
|
|
|
|
+import { isAcceptableFile } from '@/features/file-tree/util/is-acceptable-file'
|
|
|
|
|
+import { findByNameInFolder } from '@/features/file-tree/util/is-name-unique-in-folder'
|
|
|
|
|
+import { useFileTreeData } from '@/shared/context/file-tree-data-context'
|
|
|
|
|
+import { FileTreeEntity } from '../../../../../../../types/file-tree-entity'
|
|
|
|
|
+import { UploadConflicts } from '@/features/file-tree/components/file-tree-create/file-tree-upload-conflicts'
|
|
|
|
|
|
|
|
export default function FileTreeUploadDoc() {
|
|
export default function FileTreeUploadDoc() {
|
|
|
- const { parentFolderId, cancel, isDuplicate, droppedFiles, setDroppedFiles } =
|
|
|
|
|
|
|
+ const { parentFolderId, cancel, droppedFiles, setDroppedFiles } =
|
|
|
useFileTreeActionable()
|
|
useFileTreeActionable()
|
|
|
|
|
+ const { fileTreeData } = useFileTreeData()
|
|
|
const { _id: projectId } = useProjectContext()
|
|
const { _id: projectId } = useProjectContext()
|
|
|
|
|
|
|
|
const [error, setError] = useState<string>()
|
|
const [error, setError] = useState<string>()
|
|
|
|
|
|
|
|
- const [conflicts, setConflicts] = useState<any[]>([])
|
|
|
|
|
|
|
+ const [conflicts, setConflicts] = useState<FileTreeEntity[]>([])
|
|
|
|
|
+ const [folderConflicts, setFolderConflicts] = useState<FileTreeEntity[]>([])
|
|
|
const [overwrite, setOverwrite] = useState(false)
|
|
const [overwrite, setOverwrite] = useState(false)
|
|
|
|
|
|
|
|
const maxNumberOfFiles = 40
|
|
const maxNumberOfFiles = 40
|
|
|
const maxFileSize = window.ExposedSettings.maxUploadSize
|
|
const maxFileSize = window.ExposedSettings.maxUploadSize
|
|
|
|
|
|
|
|
// calculate conflicts
|
|
// calculate conflicts
|
|
|
- const buildConflicts = (files: Record<string, any>) =>
|
|
|
|
|
- Object.values(files).filter(file =>
|
|
|
|
|
- isDuplicate(file.meta.targetFolderId ?? parentFolderId, file.meta.name)
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ const buildConflicts = (files: Record<string, any>) => {
|
|
|
|
|
+ const conflicts = new Set<FileTreeEntity>()
|
|
|
|
|
+
|
|
|
|
|
+ for (const file of Object.values(files)) {
|
|
|
|
|
+ const { name, relativePath } = file.meta
|
|
|
|
|
+
|
|
|
|
|
+ if (!relativePath) {
|
|
|
|
|
+ const targetFolderId = file.meta.targetFolderId ?? parentFolderId
|
|
|
|
|
+ const duplicate = findByNameInFolder(fileTreeData, targetFolderId, name)
|
|
|
|
|
+ if (duplicate) {
|
|
|
|
|
+ conflicts.add(duplicate)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return [...conflicts]
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const buildFolderConflicts = (files: Record<string, any>) => {
|
|
|
|
|
+ const conflicts = new Set<FileTreeEntity>()
|
|
|
|
|
+
|
|
|
|
|
+ for (const file of Object.values(files)) {
|
|
|
|
|
+ const { relativePath } = file.meta
|
|
|
|
|
+
|
|
|
|
|
+ if (relativePath) {
|
|
|
|
|
+ const [rootName] = relativePath.replace(/^\//, '').split('/')
|
|
|
|
|
+ if (!conflicts.has(rootName)) {
|
|
|
|
|
+ const targetFolderId = file.meta.targetFolderId ?? parentFolderId
|
|
|
|
|
+ const duplicateEntity = findByNameInFolder(
|
|
|
|
|
+ fileTreeData,
|
|
|
|
|
+ targetFolderId,
|
|
|
|
|
+ rootName
|
|
|
|
|
+ )
|
|
|
|
|
+ if (duplicateEntity) {
|
|
|
|
|
+ conflicts.add(duplicateEntity)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return [...conflicts]
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
const buildEndpoint = (projectId: string, targetFolderId: string) => {
|
|
const buildEndpoint = (projectId: string, targetFolderId: string) => {
|
|
|
let endpoint = `/project/${projectId}/upload`
|
|
let endpoint = `/project/${projectId}/upload`
|
|
@@ -43,6 +86,11 @@ export default function FileTreeUploadDoc() {
|
|
|
return endpoint
|
|
return endpoint
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ const overwriteRef = useRef(overwrite)
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ overwriteRef.current = overwrite
|
|
|
|
|
+ }, [overwrite])
|
|
|
|
|
+
|
|
|
// initialise the Uppy object
|
|
// initialise the Uppy object
|
|
|
const [uppy] = useState(() => {
|
|
const [uppy] = useState(() => {
|
|
|
const endpoint = buildEndpoint(projectId, parentFolderId)
|
|
const endpoint = buildEndpoint(projectId, parentFolderId)
|
|
@@ -55,24 +103,21 @@ export default function FileTreeUploadDoc() {
|
|
|
maxNumberOfFiles,
|
|
maxNumberOfFiles,
|
|
|
maxFileSize: maxFileSize || null,
|
|
maxFileSize: maxFileSize || null,
|
|
|
},
|
|
},
|
|
|
- onBeforeUpload: files => {
|
|
|
|
|
- let result = true
|
|
|
|
|
-
|
|
|
|
|
- setOverwrite(overwrite => {
|
|
|
|
|
- if (!overwrite) {
|
|
|
|
|
- setConflicts(() => {
|
|
|
|
|
- const conflicts = buildConflicts(files)
|
|
|
|
|
-
|
|
|
|
|
- result = conflicts.length === 0
|
|
|
|
|
-
|
|
|
|
|
- return conflicts
|
|
|
|
|
- })
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return overwrite
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- return result
|
|
|
|
|
|
|
+ onBeforeFileAdded(file) {
|
|
|
|
|
+ if (!isAcceptableFile(file)) {
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ onBeforeUpload(files) {
|
|
|
|
|
+ if (overwriteRef.current) {
|
|
|
|
|
+ return true
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const conflicts = buildConflicts(files)
|
|
|
|
|
+ const folderConflicts = buildFolderConflicts(files)
|
|
|
|
|
+ setConflicts(conflicts)
|
|
|
|
|
+ setFolderConflicts(folderConflicts)
|
|
|
|
|
+ return conflicts.length === 0 && folderConflicts.length === 0
|
|
|
|
|
+ }
|
|
|
},
|
|
},
|
|
|
autoProceed: true,
|
|
autoProceed: true,
|
|
|
})
|
|
})
|
|
@@ -135,6 +180,7 @@ export default function FileTreeUploadDoc() {
|
|
|
source: 'Local',
|
|
source: 'Local',
|
|
|
isRemote: false,
|
|
isRemote: false,
|
|
|
meta: {
|
|
meta: {
|
|
|
|
|
+ relativePath: file.relativePath,
|
|
|
targetFolderId: droppedFiles.targetFolderId,
|
|
targetFolderId: droppedFiles.targetFolderId,
|
|
|
},
|
|
},
|
|
|
})
|
|
})
|
|
@@ -163,7 +209,8 @@ export default function FileTreeUploadDoc() {
|
|
|
}, [uppy])
|
|
}, [uppy])
|
|
|
|
|
|
|
|
// whether to show a message about conflicting files
|
|
// whether to show a message about conflicting files
|
|
|
- const showConflicts = !overwrite && conflicts.length > 0
|
|
|
|
|
|
|
+ const showConflicts =
|
|
|
|
|
+ !overwrite && (conflicts.length > 0 || folderConflicts.length > 0)
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
<>
|
|
<>
|
|
@@ -175,7 +222,9 @@ export default function FileTreeUploadDoc() {
|
|
|
<UploadConflicts
|
|
<UploadConflicts
|
|
|
cancel={cancel}
|
|
cancel={cancel}
|
|
|
conflicts={conflicts}
|
|
conflicts={conflicts}
|
|
|
|
|
+ folderConflicts={folderConflicts}
|
|
|
handleOverwrite={handleOverwrite}
|
|
handleOverwrite={handleOverwrite}
|
|
|
|
|
+ setError={setError}
|
|
|
/>
|
|
/>
|
|
|
) : (
|
|
) : (
|
|
|
<Dashboard
|
|
<Dashboard
|
|
@@ -186,16 +235,19 @@ export default function FileTreeUploadDoc() {
|
|
|
width="100%"
|
|
width="100%"
|
|
|
showLinkToFileUploadResult={false}
|
|
showLinkToFileUploadResult={false}
|
|
|
proudlyDisplayPoweredByUppy={false}
|
|
proudlyDisplayPoweredByUppy={false}
|
|
|
|
|
+ // allow files or folders to be selected
|
|
|
|
|
+ fileManagerSelectionType="both"
|
|
|
locale={{
|
|
locale={{
|
|
|
strings: {
|
|
strings: {
|
|
|
// Text to show on the droppable area.
|
|
// Text to show on the droppable area.
|
|
|
// `%{browse}` is replaced with a link that opens the system file selection dialog.
|
|
// `%{browse}` is replaced with a link that opens the system file selection dialog.
|
|
|
// TODO: 'drag_here' or 'drop_files_here_to_upload'?
|
|
// TODO: 'drag_here' or 'drop_files_here_to_upload'?
|
|
|
// dropHereOr: `${t('drag_here')} ${t('or')} %{browse}`,
|
|
// dropHereOr: `${t('drag_here')} ${t('or')} %{browse}`,
|
|
|
- dropPasteFiles: `Drag here, paste an image or file, or %{browseFiles}`,
|
|
|
|
|
|
|
+ dropPasteBoth: `Drop or paste your files, folder, or images here. %{browseFiles} or %{browseFolders} from your computer.`,
|
|
|
// Used as the label for the link that opens the system file selection dialog.
|
|
// Used as the label for the link that opens the system file selection dialog.
|
|
|
// browseFiles: t('select_from_your_computer')
|
|
// browseFiles: t('select_from_your_computer')
|
|
|
- browseFiles: 'select from your computer',
|
|
|
|
|
|
|
+ browseFiles: 'Select files',
|
|
|
|
|
+ browseFolders: 'select a folder',
|
|
|
},
|
|
},
|
|
|
}}
|
|
}}
|
|
|
/>
|
|
/>
|
|
@@ -227,45 +279,3 @@ function UploadErrorMessage({
|
|
|
return <ErrorMessage error={error} />
|
|
return <ErrorMessage error={error} />
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
-function UploadConflicts({
|
|
|
|
|
- cancel,
|
|
|
|
|
- conflicts,
|
|
|
|
|
- handleOverwrite,
|
|
|
|
|
-}: {
|
|
|
|
|
- cancel: () => void
|
|
|
|
|
- conflicts: any[]
|
|
|
|
|
- handleOverwrite: () => void
|
|
|
|
|
-}) {
|
|
|
|
|
- const { t } = useTranslation()
|
|
|
|
|
-
|
|
|
|
|
- return (
|
|
|
|
|
- <div className="small modal-new-file--body-conflict">
|
|
|
|
|
- <p className="text-center mb-0">
|
|
|
|
|
- {t('the_following_files_already_exist_in_this_project')}
|
|
|
|
|
- </p>
|
|
|
|
|
-
|
|
|
|
|
- <ul className="text-center list-unstyled row-spaced-small mt-1">
|
|
|
|
|
- {conflicts.map((conflict, index) => (
|
|
|
|
|
- <li key={index}>
|
|
|
|
|
- <strong>{conflict.meta.name}</strong>
|
|
|
|
|
- </li>
|
|
|
|
|
- ))}
|
|
|
|
|
- </ul>
|
|
|
|
|
-
|
|
|
|
|
- <p className="text-center row-spaced-small">
|
|
|
|
|
- {t('do_you_want_to_overwrite_them')}
|
|
|
|
|
- </p>
|
|
|
|
|
-
|
|
|
|
|
- <p className="text-center">
|
|
|
|
|
- <Button bsStyle={null} className="btn-secondary" onClick={cancel}>
|
|
|
|
|
- {t('cancel')}
|
|
|
|
|
- </Button>
|
|
|
|
|
-
|
|
|
|
|
- <Button bsStyle="danger" onClick={handleOverwrite}>
|
|
|
|
|
- {t('overwrite')}
|
|
|
|
|
- </Button>
|
|
|
|
|
- </p>
|
|
|
|
|
- </div>
|
|
|
|
|
- )
|
|
|
|
|
-}
|
|
|