diff options
author | skjnldsv <skjnldsv@protonmail.com> | 2024-03-14 08:56:52 +0100 |
---|---|---|
committer | skjnldsv <skjnldsv@protonmail.com> | 2024-04-04 17:04:30 +0200 |
commit | 4f27c0bb257d1142f9a319381c01388a0e8ff1ab (patch) | |
tree | a0d06786053dc55322bebfa71cbd2d6ddc542ab1 /apps | |
parent | 3127999a44a21dbe3ff54d19e17865e3d1be6989 (diff) | |
download | nextcloud-server-4f27c0bb257d1142f9a319381c01388a0e8ff1ab.tar.gz nextcloud-server-4f27c0bb257d1142f9a319381c01388a0e8ff1ab.zip |
fix(files): breadcrumbs dnd
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
Diffstat (limited to 'apps')
-rw-r--r-- | apps/files/src/components/BreadCrumbs.vue | 80 | ||||
-rw-r--r-- | apps/files/src/components/FileEntryMixin.ts | 71 | ||||
-rw-r--r-- | apps/files/src/services/DropService.ts | 75 |
3 files changed, 167 insertions, 59 deletions
diff --git a/apps/files/src/components/BreadCrumbs.vue b/apps/files/src/components/BreadCrumbs.vue index 5e4188370c4..dd59b8332ee 100644 --- a/apps/files/src/components/BreadCrumbs.vue +++ b/apps/files/src/components/BreadCrumbs.vue @@ -11,7 +11,9 @@ :force-icon-text="true" :title="titleForSection(index, section)" :aria-description="ariaForSection(section)" - @click.native="onClick(section.to)"> + @click.native="onClick(section.to)" + @dragover.native="onDragOver($event, section.dir)" + @dropped="onDrop($event, section.dir)"> <template v-if="index === 0" #icon> <NcIconSvgWrapper :size="20" :svg="viewIcon" /> @@ -34,6 +36,9 @@ import NcBreadcrumbs from '@nextcloud/vue/dist/Components/NcBreadcrumbs.js' import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js' import Vue from 'vue' +import { onDropExternalFiles, onDropInternalFiles } from '../services/DropService' +import { showError } from '@nextcloud/dialogs' +import { useDragAndDropStore } from '../store/dragging.ts' import { useFilesStore } from '../store/files.ts' import { usePathsStore } from '../store/paths.ts' @@ -46,6 +51,10 @@ export default Vue.extend({ NcIconSvgWrapper, }, + mixins: [ + filesListWidthMixin, + ], + props: { path: { type: String, @@ -54,9 +63,11 @@ export default Vue.extend({ }, setup() { + const draggingStore = useDragAndDropStore() const filesStore = useFilesStore() const pathsStore = usePathsStore() return { + draggingStore, filesStore, pathsStore, } @@ -84,6 +95,8 @@ export default Vue.extend({ exact: true, name: this.getDirDisplayName(dir), to, + // disable drop on current directory + disableDrop: index === this.dirs.length - 1, } }) }, @@ -117,6 +130,71 @@ export default Vue.extend({ } }, + onDragOver(event: DragEvent, path: string) { + // Cannot drop on the current directory + if (path === this.dirs[this.dirs.length - 1]) { + event.dataTransfer.dropEffect = 'none' + return + } + + // Handle copy/move drag and drop + if (event.ctrlKey) { + event.dataTransfer.dropEffect = 'copy' + } else { + event.dataTransfer.dropEffect = 'move' + } + }, + + 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) { + return + } + + // Caching the selection + const selection = this.draggingFiles + const files = event.dataTransfer?.files || new FileList() + + event.preventDefault() + event.stopPropagation() + + // We might not have the target directory fetched yet + const contents = await this.currentView?.getContents(path) + const folder = contents?.folder + if (!folder) { + showError(this.t('files', 'Target folder does not exist any more')) + return + } + + const canDrop = (folder.permissions & Permission.CREATE) !== 0 + const isCopy = event.ctrlKey + + // If another button is pressed, cancel it. This + // allows cancelling the drag with the right click. + if (!canDrop || event.button !== 0) { + return + } + + logger.debug('Dropped', { event, folder, selection }) + + // Check whether we're uploading files + if (files.length > 0) { + await onDropExternalFiles(folder, files) + return + } + + // Else we're moving/copying files + const nodes = selection.map(fileid => this.filesStore.getNode(fileid)) as Node[] + await onDropInternalFiles(folder, nodes, isCopy) + + // Reset selection after we dropped the files + // if the dropped files are within the selection + if (selection.some(fileid => this.selectedFiles.includes(fileid))) { + logger.debug('Dropped selection, resetting select store...') + this.selectionStore.reset() + } + }, + titleForSection(index, section) { if (section?.to?.query?.dir === this.$route.query.dir) { return t('files', 'Reload current directory') diff --git a/apps/files/src/components/FileEntryMixin.ts b/apps/files/src/components/FileEntryMixin.ts index a1e90121c92..93d188b67d9 100644 --- a/apps/files/src/components/FileEntryMixin.ts +++ b/apps/files/src/components/FileEntryMixin.ts @@ -22,20 +22,17 @@ import type { PropType } from 'vue' -import { extname, join } from 'path' +import { extname } from 'path' import { FileType, Permission, Folder, File as NcFile, NodeStatus, Node, View } from '@nextcloud/files' import { generateUrl } from '@nextcloud/router' -import { showError, showSuccess } from '@nextcloud/dialogs' import { translate as t } from '@nextcloud/l10n' -import { Upload, getUploader } from '@nextcloud/upload' import { vOnClickOutside } from '@vueuse/components' import Vue, { defineComponent } from 'vue' import { action as sidebarAction } from '../actions/sidebarAction.ts' import { getDragAndDropPreview } from '../utils/dragUtils.ts' -import { handleCopyMoveNodeTo } from '../actions/moveOrCopyAction.ts' import { hashCode } from '../utils/hashUtils.ts' -import { MoveCopyAction } from '../actions/moveOrCopyActionUtils.ts' +import { onDropExternalFiles, onDropInternalFiles } from '../services/DropService.ts' import logger from '../logger.js' Vue.directive('onClickOutside', vOnClickOutside) @@ -310,11 +307,15 @@ export default defineComponent({ return } + // Caching the selection + const selection = this.draggingFiles + const files = event.dataTransfer?.files || new FileList() + event.preventDefault() event.stopPropagation() - // If another button is pressed, cancel it - // This allows cancelling the drag with the right click + // If another button is pressed, cancel it. This + // allows cancelling the drag with the right click. if (!this.canDrop || event.button !== 0) { return } @@ -322,63 +323,21 @@ export default defineComponent({ const isCopy = event.ctrlKey this.dragover = false - logger.debug('Dropped', { event, selection: this.draggingFiles }) + logger.debug('Dropped', { event, selection }) // Check whether we're uploading files - if (event.dataTransfer?.files - && event.dataTransfer.files.length > 0) { - 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 - } - - logger.debug(`Uploading files to ${this.source.path}`) - const queue = [] as Promise<Upload>[] - for (const file of event.dataTransfer.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(join(this.source.basename, file.name), file)) - } - - const results = await Promise.allSettled(queue) - 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 - } - - logger.debug('Files uploaded successfully') - showSuccess(t('files', 'Files uploaded successfully')) + if (files.length > 0) { + await onDropExternalFiles(this.source as Folder, files) return } - const nodes = this.draggingFiles.map(fileid => this.filesStore.getNode(fileid)) as Node[] - nodes.forEach(async (node: Node) => { - Vue.set(node, 'status', NodeStatus.LOADING) - try { - // TODO: resolve potential conflicts prior and force overwrite - await handleCopyMoveNodeTo(node, this.source, isCopy ? MoveCopyAction.COPY : MoveCopyAction.MOVE) - } catch (error) { - logger.error('Error while moving file', { error }) - if (isCopy) { - showError(t('files', 'Could not copy {file}. {message}', { file: node.basename, message: error.message || '' })) - } else { - showError(t('files', 'Could not move {file}. {message}', { file: node.basename, message: error.message || '' })) - } - } finally { - Vue.set(node, 'status', undefined) - } - }) + // 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) // Reset selection after we dropped the files // if the dropped files are within the selection - if (this.draggingFiles.some(fileid => this.selectedFiles.includes(fileid))) { + if (selection.some(fileid => this.selectedFiles.includes(fileid))) { logger.debug('Dropped selection, resetting select store...') this.selectionStore.reset() } diff --git a/apps/files/src/services/DropService.ts b/apps/files/src/services/DropService.ts index b3371f44337..a2a551f8f5e 100644 --- a/apps/files/src/services/DropService.ts +++ b/apps/files/src/services/DropService.ts @@ -2,6 +2,7 @@ * @copyright Copyright (c) 2023 Ferdinand Thiessen <opensource@fthiessen.de> * * @author Ferdinand Thiessen <opensource@fthiessen.de> + * @author John Molakvoæ <skjnldsv@protonmail.com> * * @license AGPL-3.0-or-later * @@ -23,13 +24,16 @@ import type { Upload } from '@nextcloud/upload' import type { FileStat, ResponseDataDetailed } from 'webdav' -import { davGetClient, davGetDefaultPropfind, davResultToNode, davRootPath } from '@nextcloud/files' import { emit } from '@nextcloud/event-bus' +import { Folder, Node, NodeStatus, davGetClient, davGetDefaultPropfind, davResultToNode, davRootPath } from '@nextcloud/files' import { getUploader } from '@nextcloud/upload' import { joinPaths } from '@nextcloud/paths' -import { showError } from '@nextcloud/dialogs' +import { showError, showSuccess } from '@nextcloud/dialogs' import { translate as t } from '@nextcloud/l10n' +import Vue from 'vue' +import { handleCopyMoveNodeTo } from '../actions/moveOrCopyAction' +import { MoveCopyAction } from '../actions/moveOrCopyActionUtils' import logger from '../logger.js' export const handleDrop = async (data: DataTransfer): Promise<Upload[]> => { @@ -141,3 +145,70 @@ function readDirectory(directory: FileSystemDirectoryEntry) { getEntries() }) } + +export const onDropExternalFiles = async (destination: Folder, files: FileList) => { + 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 + } + + 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 + } + + logger.debug(`Uploading files to ${destination.path}`) + 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)) + } + + // 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 + } + + logger.debug('Files uploaded successfully') + showSuccess(t('files', 'Files uploaded successfully')) +} + +export const onDropInternalFiles = async (destination: Folder, nodes: Node[], isCopy = false) => { + const queue = [] as Promise<void>[] + for (const node of nodes) { + Vue.set(node, 'status', NodeStatus.LOADING) + // TODO: resolve potential conflicts prior and force overwrite + queue.push(handleCopyMoveNodeTo(node, destination, isCopy ? MoveCopyAction.COPY : MoveCopyAction.MOVE)) + } + + // Wait for all promises to settle + const results = await Promise.allSettled(queue) + nodes.forEach(node => Vue.set(node, 'status', undefined)) + + // Check for errors + const errors = results.filter(result => result.status === 'rejected') + if (errors.length > 0) { + logger.error('Error while copying or moving files', { errors }) + showError(isCopy ? t('files', 'Some files could not be copied') : t('files', 'Some files could not be moved')) + return + } + + logger.debug('Files copy/move successful') + showSuccess(isCopy ? t('files', 'Files copied successfully') : t('files', 'Files moved successfully')) +} |