]> source.dussan.org Git - nextcloud-server.git/commitdiff
fix(files): breadcrumbs dnd
authorskjnldsv <skjnldsv@protonmail.com>
Thu, 14 Mar 2024 07:56:52 +0000 (08:56 +0100)
committerbackportbot[bot] <backportbot[bot]@users.noreply.github.com>
Thu, 4 Apr 2024 11:58:34 +0000 (11:58 +0000)
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
apps/files/src/components/BreadCrumbs.vue
apps/files/src/components/FileEntryMixin.ts
apps/files/src/services/DropService.ts

index d76ed714f20cdc9de53b1b92b54fa9fe0a854b48..5f72aac11af7f0ae73c19f9cb446403521754563 100644 (file)
@@ -34,7 +34,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" />
 </template>
 
 <script lang="ts">
-import type { Node } from '@nextcloud/files'
+import { Permission, type Node } from '@nextcloud/files'
 
-import { translate as t} from '@nextcloud/l10n'
 import { basename } from 'path'
-import homeSvg from '@mdi/svg/svg/home.svg?raw'
+import { defineComponent } from 'vue'
+import { translate as t} from '@nextcloud/l10n'
+import HomeSvg from '@mdi/svg/svg/home.svg?raw'
 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 { defineComponent } 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'
+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',
@@ -73,6 +82,10 @@ export default defineComponent({
                NcIconSvgWrapper,
        },
 
+       mixins: [
+               filesListWidthMixin,
+       ],
+
        props: {
                path: {
                        type: String,
@@ -80,18 +93,18 @@ export default defineComponent({
                },
        },
 
-       mixins: [
-               filesListWidthMixin,
-       ],
-
        setup() {
+               const draggingStore = useDragAndDropStore()
                const filesStore = useFilesStore()
                const pathsStore = usePathsStore()
+               const selectionStore = useSelectionStore()
                const uploaderStore = useUploaderStore()
 
                return {
+                       draggingStore,
                        filesStore,
                        pathsStore,
+                       selectionStore,
                        uploaderStore,
                }
        },
@@ -110,7 +123,7 @@ export default defineComponent({
                },
 
                sections() {
-                       return this.dirs.map((dir: string) => {
+                       return this.dirs.map((dir: string, index: number) => {
                                const fileid = this.getFileIdFromPath(dir)
                                const to = { ...this.$route, params: { fileid }, query: { dir } }
                                return {
@@ -118,6 +131,8 @@ export default defineComponent({
                                        exact: true,
                                        name: this.getDirDisplayName(dir),
                                        to,
+                                       // disable drop on current directory
+                                       disableDrop: index === this.dirs.length - 1,
                                }
                        })
                },
@@ -133,8 +148,16 @@ export default defineComponent({
 
                // used to show the views icon for the first breadcrumb
                viewIcon(): string {
-                       return this.currentView?.icon ?? homeSvg
-               }
+                       return this.currentView?.icon ?? HomeSvg
+               },
+
+               selectedFiles() {
+                       return this.selectionStore.selected
+               },
+
+               draggingFiles() {
+                       return this.draggingStore.dragging
+               },
        },
 
        methods: {
@@ -160,6 +183,71 @@ export default defineComponent({
                        }
                },
 
+               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')
index 5834b058f121de34ced756b283bc9697595f5d36..6310001532c4905daf75ef66846d59fd24ffe27d 100644 (file)
 
 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)
@@ -313,11 +310,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
                        }
@@ -325,63 +326,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()
                        }
index b3371f44337af3248b32d61aee412f0fee468162..a2a551f8f5e12b58f1247dde1998e8ef1d7c241e 100644 (file)
@@ -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
  *
 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'))
+}