mirror of
https://github.com/nextcloud/server.git
synced 2026-06-06 07:13:23 -04:00
fix(files): breadcrumbs dnd
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
This commit is contained in:
parent
3127999a44
commit
4f27c0bb25
3 changed files with 167 additions and 59 deletions
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'))
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue