Skip to content

Commit

Permalink
Merge pull request #41990 from nextcloud/fix/backport-28-drag-n-drop
Browse files Browse the repository at this point in the history
[stable28] fix(files): Allow to drag and drop new files also on empty directories
  • Loading branch information
susnux authored Dec 5, 2023
2 parents 034241b + b1a6031 commit 77970de
Show file tree
Hide file tree
Showing 8 changed files with 134 additions and 128 deletions.
84 changes: 50 additions & 34 deletions apps/files/src/components/DragAndDropNotice.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
-
-->
<template>
<div class="files-list__drag-drop-notice"
:class="{ 'files-list__drag-drop-notice--dragover': dragover }"
<div v-show="dragover"
class="files-list__drag-drop-notice"
@drop="onDrop">
<div class="files-list__drag-drop-notice-wrapper">
<TrayArrowDownIcon :size="48" />
Expand All @@ -33,18 +33,16 @@
</template>

<script lang="ts">
import type { Upload } from '@nextcloud/upload'
import { join } from 'path'
import { showSuccess } from '@nextcloud/dialogs'
import { showError, showSuccess } from '@nextcloud/dialogs'
import { translate as t } from '@nextcloud/l10n'
import { getUploader } from '@nextcloud/upload'
import Vue from 'vue'
import { defineComponent } from 'vue'

import TrayArrowDownIcon from 'vue-material-design-icons/TrayArrowDown.vue'

import logger from '../logger.js'

export default Vue.extend({
export default defineComponent({
name: 'DragAndDropNotice',

components: {
Expand All @@ -56,16 +54,43 @@ export default Vue.extend({
type: Object,
required: true,
},
dragover: {
type: Boolean,
default: false,
},
},

data() {
return {
dragover: false,
}
},

mounted() {
// Add events on parent to cover both the table and DragAndDrop notice
const mainContent = window.document.querySelector('main.app-content') as HTMLElement
mainContent.addEventListener('dragover', this.onDragOver)
mainContent.addEventListener('dragleave', this.onDragLeave)
},

beforeDestroy() {
const mainContent = window.document.querySelector('main.app-content') as HTMLElement
mainContent.removeEventListener('dragover', this.onDragOver)
mainContent.removeEventListener('dragleave', this.onDragLeave)
},

methods: {
onDrop(event: DragEvent) {
this.$emit('update:dragover', false)
onDragOver(event: DragEvent) {
const isForeignFile = event.dataTransfer?.types.includes('Files')
if (isForeignFile) {
// Only handle uploading
this.dragover = true
}
},

onDragLeave(/* event: DragEvent */) {
if (this.dragover) {
this.dragover = false
}
},

onDrop(event: DragEvent) {
if (this.$el.querySelector('tbody')?.contains(event.target as Node)) {
return
}
Expand All @@ -79,8 +104,13 @@ export default Vue.extend({

// Start upload
logger.debug(`Uploading files to ${this.currentFolder.path}`)
const promises = [...event.dataTransfer.files].map((file: File) => {
return uploader.upload(file.name, file) as Promise<Upload>
const promises = [...event.dataTransfer.files].map(async (file: File) => {
try {
return await uploader.upload(file.name, file)
} catch (e) {
showError(t('files', 'Uploading "{filename}" failed', { filename: file.name }))
throw e
}
})

// Process finished uploads
Expand All @@ -91,12 +121,13 @@ export default Vue.extend({
// Scroll to last upload if terminated
const lastUpload = uploads[uploads.length - 1]
if (lastUpload?.response?.headers?.['oc-fileid']) {
this.$router.push(Object.assign({}, this.$route, {
this.$router.push({
...this.$route,
params: {
// Remove instanceid from header response
fileid: parseInt(lastUpload.response?.headers?.['oc-fileid']),
},
}))
})
}
})
}
Expand All @@ -108,12 +139,7 @@ export default Vue.extend({

<style lang="scss" scoped>
.files-list__drag-drop-notice {
position: absolute;
z-index: 9999;
top: 0;
right: 0;
left: 0;
display: none;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
Expand All @@ -123,11 +149,7 @@ export default Vue.extend({
user-select: none;
color: var(--color-text-maxcontrast);
background-color: var(--color-main-background);

&--dragover {
display: flex;
border-color: black;
}
border-color: black;

h3 {
margin-left: 16px;
Expand All @@ -144,12 +166,6 @@ export default Vue.extend({
border: 2px var(--color-border-dark) dashed;
border-radius: var(--border-radius-large);
}

&__close {
position: absolute !important;
top: 10px;
right: 10px;
}
}

</style>
135 changes: 52 additions & 83 deletions apps/files/src/components/FilesListVirtual.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,91 +20,79 @@
-
-->
<template>
<Fragment>
<!-- Drag and drop notice -->
<DragAndDropNotice v-if="canUpload && filesListWidth >= 512"
:current-folder="currentFolder"
:dragover.sync="dragover"
:style="{ height: dndNoticeHeight }" />

<VirtualList ref="table"
:data-component="userConfig.grid_view ? FileEntryGrid : FileEntry"
:data-key="'source'"
:data-sources="nodes"
:grid-mode="userConfig.grid_view"
:extra-props="{
isMtimeAvailable,
isSizeAvailable,
nodes,
filesListWidth,
}"
:scroll-to-index="scrollToIndex"
:caption="caption"
@scroll="onScroll">
<template #before>
<!-- Headers -->
<FilesListHeader v-for="header in sortedHeaders"
:key="header.id"
:current-folder="currentFolder"
:current-view="currentView"
:header="header" />
</template>

<!-- Thead-->
<template #header>
<!-- Table header and sort buttons -->
<FilesListTableHeader ref="thead"
:files-list-width="filesListWidth"
:is-mtime-available="isMtimeAvailable"
:is-size-available="isSizeAvailable"
:nodes="nodes" />
</template>

<!-- Tfoot-->
<template #footer>
<FilesListTableFooter :files-list-width="filesListWidth"
:is-mtime-available="isMtimeAvailable"
:is-size-available="isSizeAvailable"
:nodes="nodes"
:summary="summary" />
</template>
</VirtualList>
</Fragment>
<VirtualList ref="table"
:data-component="userConfig.grid_view ? FileEntryGrid : FileEntry"
:data-key="'source'"
:data-sources="nodes"
:grid-mode="userConfig.grid_view"
:extra-props="{
isMtimeAvailable,
isSizeAvailable,
nodes,
filesListWidth,
}"
:scroll-to-index="scrollToIndex"
:caption="caption">
<template #before>
<!-- Headers -->
<FilesListHeader v-for="header in sortedHeaders"
:key="header.id"
:current-folder="currentFolder"
:current-view="currentView"
:header="header" />
</template>

<!-- Thead-->
<template #header>
<!-- Table header and sort buttons -->
<FilesListTableHeader ref="thead"
:files-list-width="filesListWidth"
:is-mtime-available="isMtimeAvailable"
:is-size-available="isSizeAvailable"
:nodes="nodes" />
</template>

<!-- Tfoot-->
<template #footer>
<FilesListTableFooter :files-list-width="filesListWidth"
:is-mtime-available="isMtimeAvailable"
:is-size-available="isSizeAvailable"
:nodes="nodes"
:summary="summary" />
</template>
</VirtualList>
</template>

<script lang="ts">
import type { Node as NcNode } from '@nextcloud/files'
import type { PropType } from 'vue'
import type { UserConfig } from '../types.ts'
import type { UserConfig } from '../types'

import { Fragment } from 'vue-frag'
import { getFileListHeaders, Folder, View, Permission, getFileActions } from '@nextcloud/files'
import { getFileListHeaders, Folder, View, getFileActions } from '@nextcloud/files'
import { showError } from '@nextcloud/dialogs'
import { loadState } from '@nextcloud/initial-state'
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
import Vue from 'vue'
import { defineComponent } from 'vue'

import { action as sidebarAction } from '../actions/sidebarAction.ts'
import { useUserConfigStore } from '../store/userconfig.ts'
import DragAndDropNotice from './DragAndDropNotice.vue'

import FileEntry from './FileEntry.vue'
import FileEntryGrid from './FileEntryGrid.vue'
import FilesListHeader from './FilesListHeader.vue'
import FilesListTableFooter from './FilesListTableFooter.vue'
import FilesListTableHeader from './FilesListTableHeader.vue'
import filesListWidthMixin from '../mixins/filesListWidth.ts'
import logger from '../logger.js'
import VirtualList from './VirtualList.vue'
import logger from '../logger.js'

export default Vue.extend({
export default defineComponent({
name: 'FilesListVirtual',

components: {
DragAndDropNotice,
FilesListHeader,
FilesListTableFooter,
FilesListTableHeader,
Fragment,
VirtualList,
},

Expand Down Expand Up @@ -140,7 +128,6 @@ export default Vue.extend({
FileEntryGrid,
headers: getFileListHeaders(),
scrollToIndex: 0,
dragover: false,
dndNoticeHeight: 0,
}
},
Expand Down Expand Up @@ -192,10 +179,6 @@ export default Vue.extend({
return [...this.headers].sort((a, b) => a.order - b.order)
},

canUpload() {
return this.currentFolder && (this.currentFolder.permissions & Permission.CREATE) !== 0
},

caption() {
const defaultCaption = t('files', 'List of files and folders.')
const viewCaption = this.currentView.caption || defaultCaption
Expand All @@ -215,12 +198,15 @@ export default Vue.extend({
// Add events on parent to cover both the table and DragAndDrop notice
const mainContent = window.document.querySelector('main.app-content') as HTMLElement
mainContent.addEventListener('dragover', this.onDragOver)
mainContent.addEventListener('dragleave', this.onDragLeave)

this.scrollToFile(this.fileId)
this.openSidebarForFile(this.fileId)
this.handleOpenFile()
},

beforeDestroy() {
const mainContent = window.document.querySelector('main.app-content') as HTMLElement
mainContent.removeEventListener('dragover', this.onDragOver)
},

methods: {
Expand Down Expand Up @@ -274,9 +260,7 @@ export default Vue.extend({
// Detect if we're only dragging existing files or not
const isForeignFile = event.dataTransfer?.types.includes('Files')
if (isForeignFile) {
this.dragover = true
} else {
this.dragover = false
return
}

event.preventDefault()
Expand All @@ -296,21 +280,6 @@ export default Vue.extend({
this.$refs.table.$el.scrollTop = this.$refs.table.$el.scrollTop + 25
}
},
onDragLeave(event: DragEvent) {
// Counter bubbling, make sure we're ending the drag
// only when we're leaving the current element
const currentTarget = event.currentTarget as HTMLElement
if (currentTarget?.contains(event.relatedTarget as HTMLElement)) {
return
}

this.dragover = false
},

onScroll() {
// Update the sticky position of the thead to adapt to the scroll
this.dndNoticeHeight = (this.$refs.thead.$el?.getBoundingClientRect?.()?.top ?? 0) + 'px'
},

t,
},
Expand Down
4 changes: 3 additions & 1 deletion apps/files/src/components/VirtualList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@

<script lang="ts">
import type { File, Folder, Node } from '@nextcloud/files'
import type { PropType } from 'vue'
import { debounce } from 'debounce'
import Vue, { PropType } from 'vue'
import Vue from 'vue'
import filesListWidthMixin from '../mixins/filesListWidth.ts'
import logger from '../logger.js'
Expand Down
2 changes: 2 additions & 0 deletions apps/files/src/mixins/filesListWidth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export default Vue.extend({
},
mounted() {
const fileListEl = document.querySelector('#app-content-vue')
this.filesListWidth = fileListEl?.clientWidth ?? null

this.$resizeObserver = new ResizeObserver((entries) => {
if (entries.length > 0 && entries[0].target === fileListEl) {
this.filesListWidth = entries[0].contentRect.width
Expand Down
Loading

0 comments on commit 77970de

Please sign in to comment.