aboutsummaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
authorskjnldsv <skjnldsv@protonmail.com>2024-03-14 08:56:52 +0100
committerskjnldsv <skjnldsv@protonmail.com>2024-04-04 17:04:30 +0200
commit4f27c0bb257d1142f9a319381c01388a0e8ff1ab (patch)
treea0d06786053dc55322bebfa71cbd2d6ddc542ab1 /apps
parent3127999a44a21dbe3ff54d19e17865e3d1be6989 (diff)
downloadnextcloud-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.vue80
-rw-r--r--apps/files/src/components/FileEntryMixin.ts71
-rw-r--r--apps/files/src/services/DropService.ts75
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'))
+}