Skip to content

Commit

Permalink
feat(files): Migrate drop handling to Uploader functions
Browse files Browse the repository at this point in the history
So upload and drag-and-drop use the same API now.
Also this de-duplicates some code by providing a shared directive for
elements that support dropping files.

Signed-off-by: Ferdinand Thiessen <[email protected]>
  • Loading branch information
susnux authored and nfebe committed Nov 27, 2024
1 parent c877210 commit 03d15b4
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 519 deletions.
39 changes: 30 additions & 9 deletions apps/files/src/components/DragAndDropNotice.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
-->
<template>
<div v-show="dragover"
class="files-list__drag-drop-notice"
data-cy-files-drag-drop-area
v-files-drop.stop.prevent="{
enable: canUpload,
targetFolder: currentFolder,
}">
callback: onFilesUploaded,
}"
class="files-list__drag-drop-notice"
data-cy-files-drag-drop-area>
<div class="files-list__drag-drop-notice-wrapper">
<template v-if="canUpload && !isQuotaExceeded">
<TrayArrowDownIcon :size="48" />
Expand All @@ -29,7 +30,7 @@
</template>

<script lang="ts">
import type { Folder } from '@nextcloud/files'
import { File, Folder } from '@nextcloud/files'

Check failure on line 33 in apps/files/src/components/DragAndDropNotice.vue

View workflow job for this annotation

GitHub Actions / NPM lint

'/home/runner/actions-runner/_work/server/server/node_modules/@nextcloud/files/dist/index.d.ts' imported multiple times
import type { PropType } from 'vue'

import { Permission } from '@nextcloud/files'

Check failure on line 36 in apps/files/src/components/DragAndDropNotice.vue

View workflow job for this annotation

GitHub Actions / NPM lint

'/home/runner/actions-runner/_work/server/server/node_modules/@nextcloud/files/dist/index.d.ts' imported multiple times
Expand All @@ -43,6 +44,8 @@ import TrayArrowDownIcon from 'vue-material-design-icons/TrayArrowDown.vue'
import { useNavigation } from '../composables/useNavigation'
import vFilesDrop from '../directives/vFilesDrop.ts'
import logger from '../logger.ts'
import { getCurrentUser } from '@nextcloud/auth'
import { emit } from '@nextcloud/event-bus'

export default defineComponent({
name: 'DragAndDropNotice',
Expand Down Expand Up @@ -174,12 +177,30 @@ export default defineComponent({
return
}

// uploads in the current folder with file id returned
const localFileUploads = uploads.filter((upload) => (
upload.source.replace(this.currentFolder.source, '').split('/').length === 2
&& upload.response?.headers?.['oc-fileid'] !== undefined
))

for (const upload of localFileUploads) {
const Cls = upload.file.type === 'httpd/unix-directory'
? Folder
: File
emit('files:node:created', new Cls({
owner: getCurrentUser()?.uid ?? 'anonymous',
source: upload.source,
root: this.currentFolder.root!,
id: Number.parseInt(upload.response?.headers?.['oc-fileid']),
permissions: this.currentFolder.permissions,
crtime: new Date(),
mtime: new Date(),
}))
}

// Scroll to last successful upload in current directory if terminated
const lastUpload = uploads.findLast((upload) => upload.status !== UploadStatus.FAILED
&& !upload.file.webkitRelativePath.includes('/')
&& upload.response?.headers?.['oc-fileid']
// Only use the last ID if it's in the current folder
&& upload.source.replace(this.currentFolder.source, '').split('/').length === 2)
const lastUpload = localFileUploads.findLast((upload) => upload.status !== UploadStatus.FAILED
&& !upload.file.webkitRelativePath.includes('/'))

if (lastUpload !== undefined) {
logger.debug('Scrolling to last upload in current folder', { lastUpload })
Expand Down
52 changes: 20 additions & 32 deletions apps/files/src/composables/useFileListWidth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,34 @@
import type { Ref } from 'vue'
import { onMounted, readonly, ref } from 'vue'

/** The element we observe */
// Currently observed element
let element: HTMLElement | undefined

/** The current width of the element */
// Reactive width
const width = ref(0)

const observer = new ResizeObserver((elements) => {
if (elements[0].contentBoxSize) {
// use the newer `contentBoxSize` property if available
width.value = elements[0].contentBoxSize[0].inlineSize
} else {
// fall back to `contentRect`
width.value = elements[0].contentRect.width
}
// The resize observer for the file list
const observer = new ResizeObserver(([el]) => {
width.value = el.contentRect.width
})

/**
* Update the observed element if needed and reconfigure the observer
*/
function updateObserver() {
const el = document.querySelector<HTMLElement>('#app-content-vue') ?? document.body
if (el !== element) {
// if already observing: stop observing the old element
if (element) {
observer.unobserve(element)
}
// observe the new element if needed
observer.observe(el)
element = el
}
}

/**
* Get the reactive width of the file list
*/
export function useFileListWidth(): Readonly<Ref<number>> {
// Update the observer when the component is mounted (e.g. because this is the files app)
onMounted(updateObserver)
// Update the observer also in setup context, so we already have an initial value
updateObserver()
onMounted(() => {
// Check if the element for the file list has changed
// this can only happen if this composable is used within the files root app
// or the root app was recreated for some reason
const el = document.querySelector<HTMLElement>('#app-content-vue') ?? document.body
// If the element changed (or initial call) we need to observe it
if (el !== element) {
observer.observe(el)
// If there was a previous element we need to unobserve it
if (element) {
observer.unobserve(element)
}
element = el
}
})

return readonly(width)
}
75 changes: 75 additions & 0 deletions apps/files/src/directives/vFilesDrop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import type { IFolder, INode } from '@nextcloud/files'
import type { Upload } from '@nextcloud/upload'
import type { DirectiveHook } from 'vue'
import type { VNode } from 'vue/types/umd'
import {
onDropExternalFiles,
onDropInternalFiles,
} from '../services/DropService'
import { useDragAndDropStore } from '../store/dragging'
import { useFilesStore } from '../store/files'

interface OnFileDropProperties {
disabled?: boolean,
/** Folder where to upload */
targetFolder: IFolder
/** Optional callback called after uploading files - even if disabled */
callback?: (uploads: INode[]|Upload[]) => void
}

/**
* Vue directive to handle uploading files on drop events.
*
* @param el The element where to bound to
* @param bindings Directive bindings
* @param bindings.modifiers Modifiers used on the component - e.g. ".stop"
* @param bindings.value The value passed through the component
*/
const onFileDrop: DirectiveHook<HTMLElement, VNode | null, OnFileDropProperties> = function(
el,
{
modifiers,
value: options,
},
) {
// We need to use `ondrop` instead of addEventListener as we have no reference to previous
// event listener to remove it from the component
el.ondrop = async (event: DragEvent) => {
// Stop the event if called with "v-on-file-drop.stop"
if (modifiers.stop) {
event.stopPropagation()
}
// Prevent default drop behavior if called with "v-on-file-drop.prevent"
if (modifiers.prevent) {
event.preventDefault()
}
// Skip any drop handling if disabled or aborted (right click)
if (options.disabled || event.button > 0) {
return options.callback?.([])
}

let result: INode[]|Upload[] = []
const draggingStore = useDragAndDropStore()
if (draggingStore.isDragging) {
// Internal files are being dragged
const filesStore = useFilesStore()
const nodes = filesStore.getNodes(draggingStore.dragging)
await onDropInternalFiles(
nodes,
options.targetFolder,
event.ctrlKey,
)
result = nodes
} else if (event.dataTransfer) {
const uploads = await onDropExternalFiles(
event.dataTransfer,
options.targetFolder,
)
result = uploads
}

return options.callback?.(result)
}
}

export default onFileDrop
Loading

0 comments on commit 03d15b4

Please sign in to comment.