mirror of
https://github.com/nextcloud/server.git
synced 2026-06-06 15:23:17 -04:00
fix(files): Allow to drag and drop new files also on empty directories
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
parent
c13b748dea
commit
e30ce44dac
5 changed files with 119 additions and 122 deletions
|
|
@ -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" />
|
||||
|
|
@ -34,17 +34,16 @@
|
|||
|
||||
<script lang="ts">
|
||||
import type { Upload } from '@nextcloud/upload'
|
||||
import { join } from 'path'
|
||||
import { 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: {
|
||||
|
|
@ -56,16 +55,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
|
||||
}
|
||||
|
|
@ -91,12 +117,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']),
|
||||
},
|
||||
}))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -108,12 +135,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%;
|
||||
|
|
@ -123,11 +145,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;
|
||||
|
|
@ -144,12 +162,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>
|
||||
|
|
|
|||
|
|
@ -20,79 +20,68 @@
|
|||
-
|
||||
-->
|
||||
<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">
|
||||
<template #before>
|
||||
<!-- Headers -->
|
||||
<FilesListHeader v-for="header in sortedHeaders"
|
||||
:key="header.id"
|
||||
:current-folder="currentFolder"
|
||||
:current-view="currentView"
|
||||
:header="header" />
|
||||
</template>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- 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>
|
||||
<!-- 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 { action as sidebarAction } from '../actions/sidebarAction.ts'
|
||||
import { useUserConfigStore } from '../store/userconfig.ts'
|
||||
import DragAndDropNotice from './DragAndDropNotice.vue'
|
||||
import { action as sidebarAction } from '../actions/sidebarAction.js'
|
||||
import { useUserConfigStore } from '../store/userconfig.js'
|
||||
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 filesListWidthMixin from '../mixins/filesListWidth.js'
|
||||
import logger from '../logger.js'
|
||||
import VirtualList from './VirtualList.vue'
|
||||
|
||||
|
|
@ -100,11 +89,9 @@ export default Vue.extend({
|
|||
name: 'FilesListVirtual',
|
||||
|
||||
components: {
|
||||
DragAndDropNotice,
|
||||
FilesListHeader,
|
||||
FilesListTableFooter,
|
||||
FilesListTableHeader,
|
||||
Fragment,
|
||||
VirtualList,
|
||||
},
|
||||
|
||||
|
|
@ -140,7 +127,6 @@ export default Vue.extend({
|
|||
FileEntryGrid,
|
||||
headers: getFileListHeaders(),
|
||||
scrollToIndex: 0,
|
||||
dragover: false,
|
||||
dndNoticeHeight: 0,
|
||||
}
|
||||
},
|
||||
|
|
@ -192,10 +178,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
|
||||
|
|
@ -214,12 +196,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: {
|
||||
|
|
@ -273,9 +258,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()
|
||||
|
|
@ -295,21 +278,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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -62,6 +62,10 @@
|
|||
<NcLoadingIcon v-if="isRefreshing" class="files-list__refresh-icon" />
|
||||
</div>
|
||||
|
||||
<!-- Drag and drop notice -->
|
||||
<DragAndDropNotice v-if="!loading && canUpload"
|
||||
:current-folder="currentFolder" />
|
||||
|
||||
<!-- Initial loading -->
|
||||
<NcLoadingIcon v-if="loading && !isRefreshing"
|
||||
class="files-list__loading-icon"
|
||||
|
|
@ -121,26 +125,28 @@ import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
|
|||
import ShareVariantIcon from 'vue-material-design-icons/ShareVariant.vue'
|
||||
import ViewGridIcon from 'vue-material-design-icons/ViewGrid.vue'
|
||||
|
||||
import { action as sidebarAction } from '../actions/sidebarAction.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 { useUserConfigStore } from '../store/userconfig.ts'
|
||||
import { useViewConfigStore } from '../store/viewConfig.ts'
|
||||
import { action as sidebarAction } from '../actions/sidebarAction.js'
|
||||
import { useFilesStore } from '../store/files.js'
|
||||
import { usePathsStore } from '../store/paths.js'
|
||||
import { useSelectionStore } from '../store/selection.js'
|
||||
import { useUploaderStore } from '../store/uploader.js'
|
||||
import { useUserConfigStore } from '../store/userconfig.js'
|
||||
import { useViewConfigStore } from '../store/viewConfig.js'
|
||||
import BreadCrumbs from '../components/BreadCrumbs.vue'
|
||||
import FilesListVirtual from '../components/FilesListVirtual.vue'
|
||||
import filesListWidthMixin from '../mixins/filesListWidth.ts'
|
||||
import filesSortingMixin from '../mixins/filesSorting.ts'
|
||||
import filesListWidthMixin from '../mixins/filesListWidth.js'
|
||||
import filesSortingMixin from '../mixins/filesSorting.js'
|
||||
import logger from '../logger.js'
|
||||
import DragAndDropNotice from '../components/DragAndDropNotice.vue'
|
||||
|
||||
const isSharingEnabled = getCapabilities()?.files_sharing !== undefined
|
||||
const isSharingEnabled = (getCapabilities() as { files_sharing?: boolean })?.files_sharing !== undefined
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'FilesList',
|
||||
|
||||
components: {
|
||||
BreadCrumbs,
|
||||
DragAndDropNotice,
|
||||
FilesListVirtual,
|
||||
LinkIcon,
|
||||
ListViewIcon,
|
||||
|
|
@ -342,9 +348,16 @@ export default Vue.extend({
|
|||
: this.t('files', 'Switch to grid view')
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the current folder has create permissions
|
||||
*/
|
||||
canUpload() {
|
||||
return this.currentFolder && (this.currentFolder.permissions & Permission.CREATE) !== 0
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if current folder has share permissions
|
||||
*/
|
||||
canShare() {
|
||||
return isSharingEnabled
|
||||
&& this.currentFolder && (this.currentFolder.permissions & Permission.SHARE) !== 0
|
||||
|
|
|
|||
Loading…
Reference in a new issue