Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(files): Allow to drag and drop new files also on empty directories #41693

Merged
merged 4 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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({
artonge marked this conversation as resolved.
Show resolved Hide resolved
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'
susnux marked this conversation as resolved.
Show resolved Hide resolved

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 @@ -214,12 +197,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 @@ -273,9 +259,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 @@ -295,21 +279,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
Loading