: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',
NcIconSvgWrapper,
},
+ mixins: [
+ filesListWidthMixin,
+ ],
+
props: {
path: {
type: String,
},
},
- mixins: [
- filesListWidthMixin,
- ],
-
setup() {
+ const draggingStore = useDragAndDropStore()
const filesStore = useFilesStore()
const pathsStore = usePathsStore()
+ const selectionStore = useSelectionStore()
const uploaderStore = useUploaderStore()
return {
+ draggingStore,
filesStore,
pathsStore,
+ selectionStore,
uploaderStore,
}
},
},
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 {
exact: true,
name: this.getDirDisplayName(dir),
to,
+ // disable drop on current directory
+ disableDrop: index === this.dirs.length - 1,
}
})
},
// 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: {
}
},
+ 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')
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)
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
}
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()
}
* @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[]> => {
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'))
+}