Skip to content

Commit 77970de

Browse files
authored
Merge pull request #41990 from nextcloud/fix/backport-28-drag-n-drop
[stable28] fix(files): Allow to drag and drop new files also on empty directories
2 parents 034241b + b1a6031 commit 77970de

File tree

8 files changed

+134
-128
lines changed

8 files changed

+134
-128
lines changed

apps/files/src/components/DragAndDropNotice.vue

Lines changed: 50 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
-
2121
-->
2222
<template>
23-
<div class="files-list__drag-drop-notice"
24-
:class="{ 'files-list__drag-drop-notice--dragover': dragover }"
23+
<div v-show="dragover"
24+
class="files-list__drag-drop-notice"
2525
@drop="onDrop">
2626
<div class="files-list__drag-drop-notice-wrapper">
2727
<TrayArrowDownIcon :size="48" />
@@ -33,18 +33,16 @@
3333
</template>
3434

3535
<script lang="ts">
36-
import type { Upload } from '@nextcloud/upload'
37-
import { join } from 'path'
38-
import { showSuccess } from '@nextcloud/dialogs'
36+
import { showError, showSuccess } from '@nextcloud/dialogs'
3937
import { translate as t } from '@nextcloud/l10n'
4038
import { getUploader } from '@nextcloud/upload'
41-
import Vue from 'vue'
39+
import { defineComponent } from 'vue'
4240
4341
import TrayArrowDownIcon from 'vue-material-design-icons/TrayArrowDown.vue'
4442
4543
import logger from '../logger.js'
4644
47-
export default Vue.extend({
45+
export default defineComponent({
4846
name: 'DragAndDropNotice',
4947
5048
components: {
@@ -56,16 +54,43 @@ export default Vue.extend({
5654
type: Object,
5755
required: true,
5856
},
59-
dragover: {
60-
type: Boolean,
61-
default: false,
62-
},
57+
},
58+
59+
data() {
60+
return {
61+
dragover: false,
62+
}
63+
},
64+
65+
mounted() {
66+
// Add events on parent to cover both the table and DragAndDrop notice
67+
const mainContent = window.document.querySelector('main.app-content') as HTMLElement
68+
mainContent.addEventListener('dragover', this.onDragOver)
69+
mainContent.addEventListener('dragleave', this.onDragLeave)
70+
},
71+
72+
beforeDestroy() {
73+
const mainContent = window.document.querySelector('main.app-content') as HTMLElement
74+
mainContent.removeEventListener('dragover', this.onDragOver)
75+
mainContent.removeEventListener('dragleave', this.onDragLeave)
6376
},
6477
6578
methods: {
66-
onDrop(event: DragEvent) {
67-
this.$emit('update:dragover', false)
79+
onDragOver(event: DragEvent) {
80+
const isForeignFile = event.dataTransfer?.types.includes('Files')
81+
if (isForeignFile) {
82+
// Only handle uploading
83+
this.dragover = true
84+
}
85+
},
6886
87+
onDragLeave(/* event: DragEvent */) {
88+
if (this.dragover) {
89+
this.dragover = false
90+
}
91+
},
92+
93+
onDrop(event: DragEvent) {
6994
if (this.$el.querySelector('tbody')?.contains(event.target as Node)) {
7095
return
7196
}
@@ -79,8 +104,13 @@ export default Vue.extend({
79104
80105
// Start upload
81106
logger.debug(`Uploading files to ${this.currentFolder.path}`)
82-
const promises = [...event.dataTransfer.files].map((file: File) => {
83-
return uploader.upload(file.name, file) as Promise<Upload>
107+
const promises = [...event.dataTransfer.files].map(async (file: File) => {
108+
try {
109+
return await uploader.upload(file.name, file)
110+
} catch (e) {
111+
showError(t('files', 'Uploading "{filename}" failed', { filename: file.name }))
112+
throw e
113+
}
84114
})
85115
86116
// Process finished uploads
@@ -91,12 +121,13 @@ export default Vue.extend({
91121
// Scroll to last upload if terminated
92122
const lastUpload = uploads[uploads.length - 1]
93123
if (lastUpload?.response?.headers?.['oc-fileid']) {
94-
this.$router.push(Object.assign({}, this.$route, {
124+
this.$router.push({
125+
...this.$route,
95126
params: {
96127
// Remove instanceid from header response
97128
fileid: parseInt(lastUpload.response?.headers?.['oc-fileid']),
98129
},
99-
}))
130+
})
100131
}
101132
})
102133
}
@@ -108,12 +139,7 @@ export default Vue.extend({
108139

109140
<style lang="scss" scoped>
110141
.files-list__drag-drop-notice {
111-
position: absolute;
112-
z-index: 9999;
113-
top: 0;
114-
right: 0;
115-
left: 0;
116-
display: none;
142+
display: flex;
117143
align-items: center;
118144
justify-content: center;
119145
width: 100%;
@@ -123,11 +149,7 @@ export default Vue.extend({
123149
user-select: none;
124150
color: var(--color-text-maxcontrast);
125151
background-color: var(--color-main-background);
126-
127-
&--dragover {
128-
display: flex;
129-
border-color: black;
130-
}
152+
border-color: black;
131153
132154
h3 {
133155
margin-left: 16px;
@@ -144,12 +166,6 @@ export default Vue.extend({
144166
border: 2px var(--color-border-dark) dashed;
145167
border-radius: var(--border-radius-large);
146168
}
147-
148-
&__close {
149-
position: absolute !important;
150-
top: 10px;
151-
right: 10px;
152-
}
153169
}
154170
155171
</style>

apps/files/src/components/FilesListVirtual.vue

Lines changed: 52 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -20,91 +20,79 @@
2020
-
2121
-->
2222
<template>
23-
<Fragment>
24-
<!-- Drag and drop notice -->
25-
<DragAndDropNotice v-if="canUpload && filesListWidth >= 512"
26-
:current-folder="currentFolder"
27-
:dragover.sync="dragover"
28-
:style="{ height: dndNoticeHeight }" />
29-
30-
<VirtualList ref="table"
31-
:data-component="userConfig.grid_view ? FileEntryGrid : FileEntry"
32-
:data-key="'source'"
33-
:data-sources="nodes"
34-
:grid-mode="userConfig.grid_view"
35-
:extra-props="{
36-
isMtimeAvailable,
37-
isSizeAvailable,
38-
nodes,
39-
filesListWidth,
40-
}"
41-
:scroll-to-index="scrollToIndex"
42-
:caption="caption"
43-
@scroll="onScroll">
44-
<template #before>
45-
<!-- Headers -->
46-
<FilesListHeader v-for="header in sortedHeaders"
47-
:key="header.id"
48-
:current-folder="currentFolder"
49-
:current-view="currentView"
50-
:header="header" />
51-
</template>
52-
53-
<!-- Thead-->
54-
<template #header>
55-
<!-- Table header and sort buttons -->
56-
<FilesListTableHeader ref="thead"
57-
:files-list-width="filesListWidth"
58-
:is-mtime-available="isMtimeAvailable"
59-
:is-size-available="isSizeAvailable"
60-
:nodes="nodes" />
61-
</template>
62-
63-
<!-- Tfoot-->
64-
<template #footer>
65-
<FilesListTableFooter :files-list-width="filesListWidth"
66-
:is-mtime-available="isMtimeAvailable"
67-
:is-size-available="isSizeAvailable"
68-
:nodes="nodes"
69-
:summary="summary" />
70-
</template>
71-
</VirtualList>
72-
</Fragment>
23+
<VirtualList ref="table"
24+
:data-component="userConfig.grid_view ? FileEntryGrid : FileEntry"
25+
:data-key="'source'"
26+
:data-sources="nodes"
27+
:grid-mode="userConfig.grid_view"
28+
:extra-props="{
29+
isMtimeAvailable,
30+
isSizeAvailable,
31+
nodes,
32+
filesListWidth,
33+
}"
34+
:scroll-to-index="scrollToIndex"
35+
:caption="caption">
36+
<template #before>
37+
<!-- Headers -->
38+
<FilesListHeader v-for="header in sortedHeaders"
39+
:key="header.id"
40+
:current-folder="currentFolder"
41+
:current-view="currentView"
42+
:header="header" />
43+
</template>
44+
45+
<!-- Thead-->
46+
<template #header>
47+
<!-- Table header and sort buttons -->
48+
<FilesListTableHeader ref="thead"
49+
:files-list-width="filesListWidth"
50+
:is-mtime-available="isMtimeAvailable"
51+
:is-size-available="isSizeAvailable"
52+
:nodes="nodes" />
53+
</template>
54+
55+
<!-- Tfoot-->
56+
<template #footer>
57+
<FilesListTableFooter :files-list-width="filesListWidth"
58+
:is-mtime-available="isMtimeAvailable"
59+
:is-size-available="isSizeAvailable"
60+
:nodes="nodes"
61+
:summary="summary" />
62+
</template>
63+
</VirtualList>
7364
</template>
7465

7566
<script lang="ts">
7667
import type { Node as NcNode } from '@nextcloud/files'
7768
import type { PropType } from 'vue'
78-
import type { UserConfig } from '../types.ts'
69+
import type { UserConfig } from '../types'
7970
80-
import { Fragment } from 'vue-frag'
81-
import { getFileListHeaders, Folder, View, Permission, getFileActions } from '@nextcloud/files'
71+
import { getFileListHeaders, Folder, View, getFileActions } from '@nextcloud/files'
8272
import { showError } from '@nextcloud/dialogs'
8373
import { loadState } from '@nextcloud/initial-state'
8474
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
85-
import Vue from 'vue'
75+
import { defineComponent } from 'vue'
8676
8777
import { action as sidebarAction } from '../actions/sidebarAction.ts'
8878
import { useUserConfigStore } from '../store/userconfig.ts'
89-
import DragAndDropNotice from './DragAndDropNotice.vue'
79+
9080
import FileEntry from './FileEntry.vue'
9181
import FileEntryGrid from './FileEntryGrid.vue'
9282
import FilesListHeader from './FilesListHeader.vue'
9383
import FilesListTableFooter from './FilesListTableFooter.vue'
9484
import FilesListTableHeader from './FilesListTableHeader.vue'
9585
import filesListWidthMixin from '../mixins/filesListWidth.ts'
96-
import logger from '../logger.js'
9786
import VirtualList from './VirtualList.vue'
87+
import logger from '../logger.js'
9888
99-
export default Vue.extend({
89+
export default defineComponent({
10090
name: 'FilesListVirtual',
10191
10292
components: {
103-
DragAndDropNotice,
10493
FilesListHeader,
10594
FilesListTableFooter,
10695
FilesListTableHeader,
107-
Fragment,
10896
VirtualList,
10997
},
11098
@@ -140,7 +128,6 @@ export default Vue.extend({
140128
FileEntryGrid,
141129
headers: getFileListHeaders(),
142130
scrollToIndex: 0,
143-
dragover: false,
144131
dndNoticeHeight: 0,
145132
}
146133
},
@@ -192,10 +179,6 @@ export default Vue.extend({
192179
return [...this.headers].sort((a, b) => a.order - b.order)
193180
},
194181
195-
canUpload() {
196-
return this.currentFolder && (this.currentFolder.permissions & Permission.CREATE) !== 0
197-
},
198-
199182
caption() {
200183
const defaultCaption = t('files', 'List of files and folders.')
201184
const viewCaption = this.currentView.caption || defaultCaption
@@ -215,12 +198,15 @@ export default Vue.extend({
215198
// Add events on parent to cover both the table and DragAndDrop notice
216199
const mainContent = window.document.querySelector('main.app-content') as HTMLElement
217200
mainContent.addEventListener('dragover', this.onDragOver)
218-
mainContent.addEventListener('dragleave', this.onDragLeave)
219201
220202
this.scrollToFile(this.fileId)
221203
this.openSidebarForFile(this.fileId)
222204
this.handleOpenFile()
205+
},
223206
207+
beforeDestroy() {
208+
const mainContent = window.document.querySelector('main.app-content') as HTMLElement
209+
mainContent.removeEventListener('dragover', this.onDragOver)
224210
},
225211
226212
methods: {
@@ -274,9 +260,7 @@ export default Vue.extend({
274260
// Detect if we're only dragging existing files or not
275261
const isForeignFile = event.dataTransfer?.types.includes('Files')
276262
if (isForeignFile) {
277-
this.dragover = true
278-
} else {
279-
this.dragover = false
263+
return
280264
}
281265
282266
event.preventDefault()
@@ -296,21 +280,6 @@ export default Vue.extend({
296280
this.$refs.table.$el.scrollTop = this.$refs.table.$el.scrollTop + 25
297281
}
298282
},
299-
onDragLeave(event: DragEvent) {
300-
// Counter bubbling, make sure we're ending the drag
301-
// only when we're leaving the current element
302-
const currentTarget = event.currentTarget as HTMLElement
303-
if (currentTarget?.contains(event.relatedTarget as HTMLElement)) {
304-
return
305-
}
306-
307-
this.dragover = false
308-
},
309-
310-
onScroll() {
311-
// Update the sticky position of the thead to adapt to the scroll
312-
this.dndNoticeHeight = (this.$refs.thead.$el?.getBoundingClientRect?.()?.top ?? 0) + 'px'
313-
},
314283
315284
t,
316285
},

apps/files/src/components/VirtualList.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,10 @@
4141

4242
<script lang="ts">
4343
import type { File, Folder, Node } from '@nextcloud/files'
44+
import type { PropType } from 'vue'
45+
4446
import { debounce } from 'debounce'
45-
import Vue, { PropType } from 'vue'
47+
import Vue from 'vue'
4648
4749
import filesListWidthMixin from '../mixins/filesListWidth.ts'
4850
import logger from '../logger.js'

apps/files/src/mixins/filesListWidth.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export default Vue.extend({
3030
},
3131
mounted() {
3232
const fileListEl = document.querySelector('#app-content-vue')
33+
this.filesListWidth = fileListEl?.clientWidth ?? null
34+
3335
this.$resizeObserver = new ResizeObserver((entries) => {
3436
if (entries.length > 0 && entries[0].target === fileListEl) {
3537
this.filesListWidth = entries[0].contentRect.width

0 commit comments

Comments
 (0)