fix(files): breadcrumbs dnd

Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
pull/44409/head
skjnldsv 3 months ago
parent 908d7a5fe1
commit f28157e91b

@ -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" />
@ -49,20 +51,27 @@
</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')

@ -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)
@ -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()
}

@ -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…
Cancel
Save