diff options
author | skjnldsv <skjnldsv@protonmail.com> | 2024-03-22 14:39:51 +0100 |
---|---|---|
committer | skjnldsv <skjnldsv@protonmail.com> | 2024-04-04 13:33:10 +0200 |
commit | 15bf34dac8f66e1d034590338b49cfa4ad4ff37e (patch) | |
tree | 9a274d994edf4993673448d552b293b3997e934e /apps/files/src | |
parent | f28157e91b3f7d4eac69788a2c4fd839a601e1f0 (diff) | |
download | nextcloud-server-15bf34dac8f66e1d034590338b49cfa4ad4ff37e.tar.gz nextcloud-server-15bf34dac8f66e1d034590338b49cfa4ad4ff37e.zip |
feat(files): unify drag and drop methods
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
Diffstat (limited to 'apps/files/src')
-rw-r--r-- | apps/files/src/components/BreadCrumbs.vue | 36 | ||||
-rw-r--r-- | apps/files/src/components/DragAndDropNotice.vue | 80 | ||||
-rw-r--r-- | apps/files/src/components/FileEntryMixin.ts | 31 | ||||
-rw-r--r-- | apps/files/src/services/DropService.ts | 359 |
4 files changed, 355 insertions, 151 deletions
diff --git a/apps/files/src/components/BreadCrumbs.vue b/apps/files/src/components/BreadCrumbs.vue index 5f72aac11af..0c15f88fd13 100644 --- a/apps/files/src/components/BreadCrumbs.vue +++ b/apps/files/src/components/BreadCrumbs.vue @@ -36,7 +36,7 @@ :aria-description="ariaForSection(section)" @click.native="onClick(section.to)" @dragover.native="onDragOver($event, section.dir)" - @dropped="onDrop($event, section.dir)"> + @drop.native="onDrop($event, section.dir)"> <template v-if="index === 0" #icon> <NcIconSvgWrapper :size="20" :svg="viewIcon" /> @@ -61,7 +61,7 @@ import NcBreadcrumb from '@nextcloud/vue/dist/Components/NcBreadcrumb.js' import NcBreadcrumbs from '@nextcloud/vue/dist/Components/NcBreadcrumbs.js' import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js' -import { onDropExternalFiles, onDropInternalFiles } from '../services/DropService' +import { onDropInternalFiles, dataTransferToFileTree, onDropExternalFiles } from '../services/DropService' import { showError } from '@nextcloud/dialogs' import { useDragAndDropStore } from '../store/dragging.ts' import { useFilesStore } from '../store/files.ts' @@ -70,8 +70,6 @@ import { useSelectionStore } from '../store/selection.ts' import { useUploaderStore } from '../store/uploader.ts' import filesListWidthMixin from '../mixins/filesListWidth.ts' import logger from '../logger' -import { debug } from '../../../../core/src/OC/debug.js' -import { F } from 'lodash/fp' export default defineComponent({ name: 'BreadCrumbs', @@ -143,7 +141,13 @@ export default defineComponent({ // Hide breadcrumbs if an upload is ongoing shouldShowBreadcrumbs(): boolean { - return this.filesListWidth > 400 && !this.isUploadInProgress + // If we're uploading files, only show the breadcrumbs + // if the files list is greater than 768px wide + if (this.isUploadInProgress) { + return this.filesListWidth > 768 + } + // If we're not uploading, we have enough space from 400px + return this.filesListWidth > 400 }, // used to show the views icon for the first breadcrumb @@ -200,16 +204,22 @@ export default defineComponent({ async onDrop(event: DragEvent, path: string) { // skip if native drop like text drag and drop from files names - if (!this.draggingFiles && !event.dataTransfer?.files?.length) { + if (!this.draggingFiles && !event.dataTransfer?.items?.length) { return } + // Do not stop propagation, so the main content + // drop event can be triggered too and clear the + // dragover state on the DragAndDropNotice component. + event.preventDefault() + // Caching the selection const selection = this.draggingFiles - const files = event.dataTransfer?.files || new FileList() + const items = [...event.dataTransfer?.items || []] as DataTransferItem[] - event.preventDefault() - event.stopPropagation() + // We need to process the dataTransfer ASAP before the + // browser clears it. This is why we cache the items too. + const fileTree = await dataTransferToFileTree(items) // We might not have the target directory fetched yet const contents = await this.currentView?.getContents(path) @@ -228,17 +238,17 @@ export default defineComponent({ return } - logger.debug('Dropped', { event, folder, selection }) + logger.debug('Dropped', { event, folder, selection, fileTree }) // Check whether we're uploading files - if (files.length > 0) { - await onDropExternalFiles(folder, files) + if (fileTree.contents.length > 0) { + await onDropExternalFiles(fileTree, folder, contents.contents) return } // Else we're moving/copying files const nodes = selection.map(fileid => this.filesStore.getNode(fileid)) as Node[] - await onDropInternalFiles(folder, nodes, isCopy) + await onDropInternalFiles(nodes, folder, contents.contents, isCopy) // Reset selection after we dropped the files // if the dropped files are within the selection diff --git a/apps/files/src/components/DragAndDropNotice.vue b/apps/files/src/components/DragAndDropNotice.vue index f9b830ca755..d591f6ee934 100644 --- a/apps/files/src/components/DragAndDropNotice.vue +++ b/apps/files/src/components/DragAndDropNotice.vue @@ -46,14 +46,14 @@ <script lang="ts"> import { defineComponent } from 'vue' import { Folder, Permission } from '@nextcloud/files' -import { showError, showSuccess } from '@nextcloud/dialogs' +import { showError } from '@nextcloud/dialogs' import { translate as t } from '@nextcloud/l10n' import { UploadStatus } from '@nextcloud/upload' import TrayArrowDownIcon from 'vue-material-design-icons/TrayArrowDown.vue' import logger from '../logger.js' -import { handleDrop } from '../services/DropService' +import { dataTransferToFileTree, onDropExternalFiles } from '../services/DropService' export default defineComponent({ name: 'DragAndDropNotice', @@ -76,6 +76,10 @@ export default defineComponent({ }, computed: { + currentView() { + return this.$navigation.active + }, + /** * Check if the current folder has create permissions */ @@ -146,8 +150,6 @@ export default defineComponent({ }, async onDrop(event: DragEvent) { - logger.debug('Dropped on DragAndDropNotice', { event }) - // cantUploadLabel is null if we can upload if (this.cantUploadLabel) { showError(this.cantUploadLabel) @@ -161,38 +163,50 @@ export default defineComponent({ event.preventDefault() event.stopPropagation() - if (event.dataTransfer && event.dataTransfer.items.length > 0) { - // Start upload - logger.debug(`Uploading files to ${this.currentFolder.path}`) - // Process finished uploads - const uploads = await handleDrop(event.dataTransfer) - logger.debug('Upload terminated', { uploads }) - - if (uploads.some((upload) => upload.status === UploadStatus.FAILED)) { - showError(t('files', 'Some files could not be uploaded')) - const failedUploads = uploads.filter((upload) => upload.status === UploadStatus.FAILED) - logger.debug('Some files could not be uploaded', { failedUploads }) - } else { - showSuccess(t('files', 'Files uploaded successfully')) - } - - // Scroll to last successful upload in current directory if terminated - const lastUpload = uploads.findLast((upload) => upload.status !== UploadStatus.FAILED - && !upload.file.webkitRelativePath.includes('/') - && upload.response?.headers?.['oc-fileid']) - - if (lastUpload !== undefined) { - this.$router.push({ - ...this.$route, - params: { - view: this.$route.params?.view ?? 'files', - fileid: parseInt(lastUpload.response!.headers['oc-fileid']), - }, - }) - } + // Caching the selection + const items: DataTransferItem[] = [...event.dataTransfer?.items || []] + + // We need to process the dataTransfer ASAP before the + // browser clears it. This is why we cache the items too. + const fileTree = await dataTransferToFileTree(items) + + // We might not have the target directory fetched yet + const contents = await this.currentView?.getContents(this.currentFolder.path) + const folder = contents?.folder + if (!folder) { + showError(this.t('files', 'Target folder does not exist any more')) + return + } + + // If another button is pressed, cancel it. This + // allows cancelling the drag with the right click. + if (event.button !== 0) { + return + } + + logger.debug('Dropped', { event, folder, fileTree }) + + // Check whether we're uploading files + const uploads = await onDropExternalFiles(fileTree, folder, contents.contents) + + // Scroll to last successful upload in current directory if terminated + const lastUpload = uploads.findLast((upload) => upload.status !== UploadStatus.FAILED + && !upload.file.webkitRelativePath.includes('/') + && upload.response?.headers?.['oc-fileid']) + + if (lastUpload !== undefined) { + this.$router.push({ + ...this.$route, + params: { + view: this.$route.params?.view ?? 'files', + fileid: parseInt(lastUpload.response!.headers['oc-fileid']), + }, + }) } + this.dragover = false }, + t, }, }) diff --git a/apps/files/src/components/FileEntryMixin.ts b/apps/files/src/components/FileEntryMixin.ts index 077b82d8f34..255f0204914 100644 --- a/apps/files/src/components/FileEntryMixin.ts +++ b/apps/files/src/components/FileEntryMixin.ts @@ -32,8 +32,9 @@ import Vue, { defineComponent } from 'vue' import { action as sidebarAction } from '../actions/sidebarAction.ts' import { getDragAndDropPreview } from '../utils/dragUtils.ts' import { hashCode } from '../utils/hashUtils.ts' -import { onDropExternalFiles, onDropInternalFiles } from '../services/DropService.ts' +import { dataTransferToFileTree, onDropExternalFiles, onDropInternalFiles } from '../services/DropService.ts' import logger from '../logger.js' +import { showError } from '@nextcloud/dialogs' Vue.directive('onClickOutside', vOnClickOutside) @@ -306,16 +307,28 @@ export default defineComponent({ async onDrop(event: DragEvent) { // skip if native drop like text drag and drop from files names - if (!this.draggingFiles && !event.dataTransfer?.files?.length) { + if (!this.draggingFiles && !event.dataTransfer?.items?.length) { return } + event.preventDefault() + event.stopPropagation() + // Caching the selection const selection = this.draggingFiles - const files = event.dataTransfer?.files || new FileList() + const items = [...event.dataTransfer?.items || []] as DataTransferItem[] - event.preventDefault() - event.stopPropagation() + // We need to process the dataTransfer ASAP before the + // browser clears it. This is why we cache the items too. + const fileTree = await dataTransferToFileTree(items) + + // We might not have the target directory fetched yet + const contents = await this.currentView?.getContents(this.source.path) + const folder = contents?.folder + if (!folder) { + showError(this.t('files', 'Target folder does not exist any more')) + return + } // If another button is pressed, cancel it. This // allows cancelling the drag with the right click. @@ -326,17 +339,17 @@ export default defineComponent({ const isCopy = event.ctrlKey this.dragover = false - logger.debug('Dropped', { event, selection }) + logger.debug('Dropped', { event, folder, selection, fileTree }) // Check whether we're uploading files - if (files.length > 0) { - await onDropExternalFiles(this.source as Folder, files) + if (fileTree.contents.length > 0) { + await onDropExternalFiles(fileTree, folder, contents.contents) return } // Else we're moving/copying files const nodes = selection.map(fileid => this.filesStore.getNode(fileid)) as Node[] - await onDropInternalFiles(this.source as Folder, nodes, isCopy) + await onDropInternalFiles(nodes, folder, contents.contents, isCopy) // Reset selection after we dropped the files // if the dropped files are within the selection diff --git a/apps/files/src/services/DropService.ts b/apps/files/src/services/DropService.ts index a2a551f8f5e..e5f806d9f0b 100644 --- a/apps/files/src/services/DropService.ts +++ b/apps/files/src/services/DropService.ts @@ -12,7 +12,7 @@ * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of + * but WITHOUT ANY WARRANTY without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * @@ -26,9 +26,10 @@ import type { FileStat, ResponseDataDetailed } from 'webdav' import { emit } from '@nextcloud/event-bus' import { Folder, Node, NodeStatus, davGetClient, davGetDefaultPropfind, davResultToNode, davRootPath } from '@nextcloud/files' -import { getUploader } from '@nextcloud/upload' +import { getUploader, hasConflict, openConflictPicker } from '@nextcloud/upload' +import { join } from 'path' import { joinPaths } from '@nextcloud/paths' -import { showError, showSuccess } from '@nextcloud/dialogs' +import { showError, showInfo, showSuccess, showWarning } from '@nextcloud/dialogs' import { translate as t } from '@nextcloud/l10n' import Vue from 'vue' @@ -36,95 +37,102 @@ import { handleCopyMoveNodeTo } from '../actions/moveOrCopyAction' import { MoveCopyAction } from '../actions/moveOrCopyActionUtils' import logger from '../logger.js' -export const handleDrop = async (data: DataTransfer): Promise<Upload[]> => { - // TODO: Maybe handle `getAsFileSystemHandle()` in the future +/** + * This represents a Directory in the file tree + * We extend the File class to better handling uploading + * and stay as close as possible as the Filesystem API. + * This also allow us to hijack the size or lastModified + * properties to compute them dynamically. + */ +class Directory extends File { - const uploads = [] as Upload[] - // we need to cache the entries to prevent Blink engine bug that clears the list (`data.items`) after first access props of one of the entries - const entries = [...data.items] - .filter((item) => { - if (item.kind !== 'file') { - logger.debug('Skipping dropped item', { kind: item.kind, type: item.type }) - return false - } - return true - }) - .map((item) => { - // MDN recommends to try both, as it might be renamed in the future - return (item as unknown as { getAsEntry?: () => FileSystemEntry|undefined})?.getAsEntry?.() ?? item.webkitGetAsEntry() ?? item - }) + /* eslint-disable no-use-before-define */ + _contents: (Directory|File)[] - for (const entry of entries) { - // Handle browser issues if Filesystem API is not available. Fallback to File API - if (entry instanceof DataTransferItem) { - logger.debug('Could not get FilesystemEntry of item, falling back to file') - const file = entry.getAsFile() - if (file === null) { - logger.warn('Could not process DataTransferItem', { type: entry.type, kind: entry.kind }) - showError(t('files', 'One of the dropped files could not be processed')) - } else { - uploads.push(await handleFileUpload(file)) - } - } else { - logger.debug('Handle recursive upload', { entry: entry.name }) - // Use Filesystem API - uploads.push(...await handleRecursiveUpload(entry)) + constructor(name, contents: (Directory|File)[] = []) { + super([], name, { type: 'httpd/unix-directory' }) + this._contents = contents + } + + set contents(contents: (Directory|File)[]) { + this._contents = contents + } + + get contents(): (Directory|File)[] { + return this._contents + } + + get size() { + return this._computeDirectorySize(this) + } + + get lastModified() { + if (this._contents.length === 0) { + return Date.now() } + return this._computeDirectoryMtime(this) } - return uploads -} -const handleFileUpload = async (file: File, path: string = '') => { - const uploader = getUploader() + /** + * Get the last modification time of a file tree + * This is not perfect, but will get us a pretty good approximation + * @param directory the directory to traverse + */ + _computeDirectoryMtime(directory: Directory): number { + return directory.contents.reduce((acc, file) => { + return file.lastModified > acc + // If the file is a directory, the lastModified will + // also return the results of its _computeDirectoryMtime method + // Fancy recursion, huh? + ? file.lastModified + : acc + }, 0) + } - try { - return await uploader.upload(`${path}${file.name}`, file) - } catch (e) { - showError(t('files', 'Uploading "{filename}" failed', { filename: file.name })) - throw e + /** + * Get the size of a file tree + * @param directory the directory to traverse + */ + _computeDirectorySize(directory: Directory): number { + return directory.contents.reduce((acc: number, entry: Directory|File) => { + // If the file is a directory, the size will + // also return the results of its _computeDirectorySize method + // Fancy recursion, huh? + return acc + entry.size + }, 0) } + } -const handleRecursiveUpload = async (entry: FileSystemEntry, path: string = ''): Promise<Upload[]> => { - if (entry.isFile) { - return [ - await new Promise<Upload>((resolve, reject) => { - (entry as FileSystemFileEntry).file( - async (file) => resolve(await handleFileUpload(file, path)), - (error) => reject(error), - ) - }), - ] - } else { - const directory = entry as FileSystemDirectoryEntry - - // TODO: Implement this on `@nextcloud/upload` - const absolutPath = joinPaths(davRootPath, getUploader().destination.path, path, directory.name) - - logger.debug('Handle directory recursively', { name: directory.name, absolutPath }) - - const davClient = davGetClient() - const dirExists = await davClient.exists(absolutPath) - if (!dirExists) { - logger.debug('Directory does not exist, creating it', { absolutPath }) - await davClient.createDirectory(absolutPath, { recursive: true }) - const stat = await davClient.stat(absolutPath, { details: true, data: davGetDefaultPropfind() }) as ResponseDataDetailed<FileStat> - emit('files:node:created', davResultToNode(stat.data)) - } +type RootDirectory = Directory & { + name: 'root' +} - const entries = await readDirectory(directory) - // sorted so we upload files first before starting next level - const promises = entries.sort((a) => a.isFile ? -1 : 1) - .map((file) => handleRecursiveUpload(file, `${path}${directory.name}/`)) - return (await Promise.all(promises)).flat() +/** + * Traverse a file tree using the Filesystem API + * @param entry the entry to traverse + */ +const traverseTree = async (entry: FileSystemEntry): Promise<Directory|File> => { + // Handle file + if (entry.isFile) { + return new Promise<File>((resolve, reject) => { + (entry as FileSystemFileEntry).file(resolve, reject) + }) } + + // Handle directory + logger.debug('Handling recursive file tree', { entry: entry.name }) + const directory = entry as FileSystemDirectoryEntry + const entries = await readDirectory(directory) + const contents = (await Promise.all(entries.map(traverseTree))).flat() + return new Directory(directory.name, contents) } /** * Read a directory using Filesystem API * @param directory the directory to read */ -function readDirectory(directory: FileSystemDirectoryEntry) { +const readDirectory = (directory: FileSystemDirectoryEntry): Promise<FileSystemEntry[]> => { const dirReader = directory.createReader() return new Promise<FileSystemEntry[]>((resolve, reject) => { @@ -146,51 +154,210 @@ function readDirectory(directory: FileSystemDirectoryEntry) { }) } -export const onDropExternalFiles = async (destination: Folder, files: FileList) => { +const createDirectoryIfNotExists = async (absolutePath: string) => { + const davClient = davGetClient() + const dirExists = await davClient.exists(absolutePath) + if (!dirExists) { + logger.debug('Directory does not exist, creating it', { absolutePath }) + await davClient.createDirectory(absolutePath, { recursive: true }) + const stat = await davClient.stat(absolutePath, { details: true, data: davGetDefaultPropfind() }) as ResponseDataDetailed<FileStat> + emit('files:node:created', davResultToNode(stat.data)) + } +} + +const resolveConflict = async <T extends ((Directory|File)|Node)>(files: Array<T>, destination: Folder, contents: Node[]): Promise<T[]> => { + try { + // List all conflicting files + const conflicts = files.filter((file: File|Node) => { + return contents.find((node: Node) => node.basename === (file instanceof File ? file.name : file.basename)) + }).filter(Boolean) as (File|Node)[] + + // List of incoming files that are NOT in conflict + const uploads = files.filter((file: File|Node) => { + return !conflicts.includes(file) + }) + + // Let the user choose what to do with the conflicting files + const { selected, renamed } = await openConflictPicker(destination.path, conflicts, contents) + + logger.debug('Conflict resolution', { uploads, selected, renamed }) + + // If the user selected nothing, we cancel the upload + if (selected.length === 0 && renamed.length === 0) { + // User skipped + showInfo(t('files', 'Conflicts resolution skipped')) + logger.info('User skipped the conflict resolution') + return [] + } + + // Update the list of files to upload + return [...uploads, ...selected, ...renamed] as (typeof files) + } catch (error) { + console.error(error) + // User cancelled + showError(t('files', 'Upload cancelled')) + logger.error('User cancelled the upload') + } + + return [] +} + +/** + * This function converts a list of DataTransferItems to a file tree. + * It uses the Filesystem API if available, otherwise it falls back to the File API. + * The File API will NOT be available if the browser is not in a secure context (e.g. HTTP). + * ⚠️ When using this method, you need to use it as fast as possible, as the DataTransferItems + * will be cleared after the first access to the props of one of the entries. + * + * @param items the list of DataTransferItems + */ +export const dataTransferToFileTree = async (items: DataTransferItem[]): Promise<RootDirectory> => { + // Check if the browser supports the Filesystem API + // We need to cache the entries to prevent Blink engine bug that clears + // the list (`data.items`) after first access props of one of the entries + const entries = items + .filter((item) => { + if (item.kind !== 'file') { + logger.debug('Skipping dropped item', { kind: item.kind, type: item.type }) + return false + } + return true + }).map((item) => { + // MDN recommends to try both, as it might be renamed in the future + return (item as unknown as { getAsEntry?: () => FileSystemEntry|undefined })?.getAsEntry?.() + ?? item.webkitGetAsEntry() + ?? item + }) as (FileSystemEntry | DataTransferItem)[] + + let warned = false + const fileTree = new Directory('root') as RootDirectory + + // Traverse the file tree + for (const entry of entries) { + // Handle browser issues if Filesystem API is not available. Fallback to File API + if (entry instanceof DataTransferItem) { + logger.warn('Could not get FilesystemEntry of item, falling back to file') + + const file = entry.getAsFile() + if (file === null) { + logger.warn('Could not process DataTransferItem', { type: entry.type, kind: entry.kind }) + showError(t('files', 'One of the dropped files could not be processed')) + continue + } + + // Warn the user that the browser does not support the Filesystem API + // we therefore cannot upload directories recursively. + if (file.type === 'httpd/unix-directory' || !file.type) { + if (!warned) { + showWarning(t('files', 'Your browser does not support the Filesystem API. Directories will not be uploaded.')) + warned = true + } + continue + } + + fileTree.contents.push(file) + continue + } + + // Use Filesystem API + try { + fileTree.contents.push(await traverseTree(entry)) + } catch (error) { + // Do not throw, as we want to continue with the other files + logger.error('Error while traversing file tree', { error }) + } + } + + return fileTree +} + +export const onDropExternalFiles = async (root: RootDirectory, destination: Folder, contents: Node[]): Promise<Upload[]> => { const uploader = getUploader() - // Check whether the uploader is in the same folder - // This should never happen™ - if (!uploader.destination.path.startsWith(uploader.destination.path)) { - logger.error('The current uploader destination is not the same as the current folder') - showError(t('files', 'An error occurred while uploading. Please try again later.')) - return + // Check for conflicts on root elements + if (await hasConflict(root.contents, contents)) { + root.contents = await resolveConflict(root.contents, destination, contents) } - const previousDestination = uploader.destination - if (uploader.destination.path !== destination.path) { - logger.debug('Changing uploader destination', { previous: uploader.destination.path, new: destination.path }) - uploader.destination = destination + if (root.contents.length === 0) { + logger.info('No files to upload', { root }) + showInfo(t('files', 'No files to upload')) + return [] } - logger.debug(`Uploading files to ${destination.path}`) + // Let's process the files + logger.debug(`Uploading files to ${destination.path}`, { root, contents: root.contents }) const queue = [] as Promise<Upload>[] - for (const file of files) { - // Because the uploader destination is properly set to the current folder - // we can just use the basename as the relative path. - queue.push(uploader.upload(file.name, file)) + + const uploadDirectoryContents = async (directory: Directory, path: string) => { + for (const file of directory.contents) { + // This is the relative path to the resource + // from the current uploader destination + const relativePath = join(path, file.name) + + // If the file is a directory, we need to create it first + // then browse its tree and upload its contents. + if (file instanceof Directory) { + const absolutePath = joinPaths(davRootPath, destination.path, relativePath) + try { + console.debug('Processing directory', { relativePath }) + await createDirectoryIfNotExists(absolutePath) + await uploadDirectoryContents(file, relativePath) + } catch (error) { + showError(t('files', 'Unable to create the directory {directory}', { directory: file.name })) + logger.error('', { error, absolutePath, directory: file }) + } + continue + } + + // If we've reached a file, we can upload it + logger.debug('Uploading file to ' + join(destination.path, relativePath), { file }) + + // Overriding the root to avoid changing the current uploader context + queue.push(uploader.upload(relativePath, file, destination.source)) + } } + // Pause the uploader to prevent it from starting + // while we compute the queue + uploader.pause() + + // Upload the files. Using '/' as the starting point + // as we already adjusted the uploader destination + await uploadDirectoryContents(root, '/') + uploader.start() + // Wait for all promises to settle const results = await Promise.allSettled(queue) - // Reset the uploader destination - uploader.destination = previousDestination - // Check for errors const errors = results.filter(result => result.status === 'rejected') if (errors.length > 0) { logger.error('Error while uploading files', { errors }) showError(t('files', 'Some files could not be uploaded')) - return + return [] } logger.debug('Files uploaded successfully') showSuccess(t('files', 'Files uploaded successfully')) + + return Promise.all(queue) } -export const onDropInternalFiles = async (destination: Folder, nodes: Node[], isCopy = false) => { +export const onDropInternalFiles = async (nodes: Node[], destination: Folder, contents: Node[], isCopy = false) => { const queue = [] as Promise<void>[] + + // Check for conflicts on root elements + if (await hasConflict(nodes, contents)) { + nodes = await resolveConflict(nodes, destination, contents) + } + + if (nodes.length === 0) { + logger.info('No files to process', { nodes }) + showInfo(t('files', 'No files to process')) + return + } + for (const node of nodes) { Vue.set(node, 'status', NodeStatus.LOADING) // TODO: resolve potential conflicts prior and force overwrite |