зеркало из https://github.com/nextcloud/server.git
feat: allow external drop and add dropzone
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
This commit is contained in:
Родитель
9de246d74f
Коммит
35aed73ede
|
@ -237,9 +237,11 @@ class ViewController extends Controller {
|
|||
if ($fileid && $dir !== '') {
|
||||
$baseFolder = $this->rootFolder->getUserFolder($userId);
|
||||
$nodes = $baseFolder->getById((int) $fileid);
|
||||
$relativePath = dirname($baseFolder->getRelativePath($nodes[0]->getPath()));
|
||||
// If the requested path is different from the file path
|
||||
if (count($nodes) === 1 && $relativePath !== $dir) {
|
||||
$nodePath = $baseFolder->getRelativePath($nodes[0]->getPath());
|
||||
$relativePath = $nodePath ? dirname($nodePath) : '';
|
||||
// If the requested path does not contain the file id
|
||||
// or if the requested path is not the file id itself
|
||||
if (count($nodes) === 1 && $relativePath !== $dir && $nodePath !== $dir) {
|
||||
return $this->redirectToFile((int) $fileid);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,155 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
-
|
||||
- @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
-
|
||||
- @license GNU AGPL version 3 or any later version
|
||||
-
|
||||
- This program is free software: you can redistribute it and/or modify
|
||||
- it under the terms of the GNU Affero General Public License as
|
||||
- published by the Free Software Foundation, either version 3 of the
|
||||
- License, or (at your option) any later version.
|
||||
-
|
||||
- This program is distributed in the hope that it will be useful,
|
||||
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
- GNU Affero General Public License for more details.
|
||||
-
|
||||
- You should have received a copy of the GNU Affero General Public License
|
||||
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-
|
||||
-->
|
||||
<template>
|
||||
<div class="files-list__drag-drop-notice"
|
||||
:class="{ 'files-list__drag-drop-notice--dragover': dragover }"
|
||||
@drop="onDrop">
|
||||
<div class="files-list__drag-drop-notice-wrapper">
|
||||
<TrayArrowDownIcon :size="48" />
|
||||
<h3 class="files-list-drag-drop-notice__title">
|
||||
{{ t('files', 'Drag and drop files here to upload') }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 TrayArrowDownIcon from 'vue-material-design-icons/TrayArrowDown.vue'
|
||||
|
||||
import logger from '../logger.js'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'DragAndDropNotice',
|
||||
|
||||
components: {
|
||||
TrayArrowDownIcon,
|
||||
},
|
||||
|
||||
props: {
|
||||
currentFolder: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
dragover: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onDrop(event: DragEvent) {
|
||||
this.$emit('update:dragover', false)
|
||||
|
||||
if (this.$el.querySelector('tbody')?.contains(event.target as Node)) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
if (event.dataTransfer && event.dataTransfer.files?.length > 0) {
|
||||
const uploader = getUploader()
|
||||
uploader.destination = this.currentFolder
|
||||
|
||||
// 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>
|
||||
})
|
||||
|
||||
// Process finished uploads
|
||||
Promise.all(promises).then((uploads) => {
|
||||
logger.debug('Upload terminated', { uploads })
|
||||
showSuccess(t('files', 'Upload successful'))
|
||||
|
||||
// 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, {
|
||||
params: {
|
||||
// Remove instanceid from header response
|
||||
fileid: parseInt(lastUpload.response?.headers?.['oc-fileid']),
|
||||
},
|
||||
}))
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
t,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.files-list__drag-drop-notice {
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
// Breadcrumbs height + row thead height
|
||||
min-height: calc(58px + 55px);
|
||||
margin: 0;
|
||||
user-select: none;
|
||||
color: var(--color-text-maxcontrast);
|
||||
background-color: var(--color-main-background);
|
||||
|
||||
&--dragover {
|
||||
display: flex;
|
||||
border-color: black;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-left: 16px;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 15vh;
|
||||
max-height: 70%;
|
||||
padding: 0 5vw;
|
||||
border: 2px var(--color-border-dark) dashed;
|
||||
border-radius: var(--border-radius-large);
|
||||
}
|
||||
|
||||
&__close {
|
||||
position: absolute !important;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
|
@ -189,12 +189,13 @@
|
|||
<script lang="ts">
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { extname } from 'path'
|
||||
import { emit, subscribe } from '@nextcloud/event-bus'
|
||||
import { extname, join } from 'path'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { getFileActions, DefaultType, FileType, formatFileSize, Permission, Folder, File, FileAction, NodeStatus, Node } from '@nextcloud/files'
|
||||
import { getFileActions, DefaultType, FileType, formatFileSize, Permission, Folder, File as NcFile, FileAction, NodeStatus, Node } from '@nextcloud/files'
|
||||
import { getUploader } from '@nextcloud/upload'
|
||||
import { showError, showSuccess } from '@nextcloud/dialogs'
|
||||
import { translate } from '@nextcloud/l10n'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { Type as ShareType } from '@nextcloud/sharing'
|
||||
import { vOnClickOutside } from '@vueuse/components'
|
||||
import axios from '@nextcloud/axios'
|
||||
|
@ -278,7 +279,7 @@ export default Vue.extend({
|
|||
default: false,
|
||||
},
|
||||
source: {
|
||||
type: [Folder, File, Node] as PropType<Node>,
|
||||
type: [Folder, NcFile, Node] as PropType<Node>,
|
||||
required: true,
|
||||
},
|
||||
index: {
|
||||
|
@ -369,7 +370,7 @@ export default Vue.extend({
|
|||
size() {
|
||||
const size = parseInt(this.source.size, 10) || 0
|
||||
if (typeof size !== 'number' || size < 0) {
|
||||
return this.t('files', 'Pending')
|
||||
return t('files', 'Pending')
|
||||
}
|
||||
return formatFileSize(size, true)
|
||||
},
|
||||
|
@ -391,7 +392,7 @@ export default Vue.extend({
|
|||
if (this.source.mtime) {
|
||||
return moment(this.source.mtime).fromNow()
|
||||
}
|
||||
return this.t('files_trashbin', 'A long time ago')
|
||||
return t('files_trashbin', 'A long time ago')
|
||||
},
|
||||
mtimeOpacity() {
|
||||
const maxOpacityTime = 31 * 24 * 60 * 60 * 1000 // 31 days
|
||||
|
@ -457,7 +458,7 @@ export default Vue.extend({
|
|||
linkTo() {
|
||||
if (this.source.attributes.failed) {
|
||||
return {
|
||||
title: this.t('files', 'This node is unavailable'),
|
||||
title: t('files', 'This node is unavailable'),
|
||||
is: 'span',
|
||||
}
|
||||
}
|
||||
|
@ -475,7 +476,7 @@ export default Vue.extend({
|
|||
return {
|
||||
download: this.source.basename,
|
||||
href: this.source.source,
|
||||
title: this.t('files', 'Download file {name}', { name: this.displayName }),
|
||||
title: t('files', 'Download file {name}', { name: this.displayName }),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -508,7 +509,7 @@ export default Vue.extend({
|
|||
|
||||
try {
|
||||
const previewUrl = this.source.attributes.previewUrl
|
||||
|| generateUrl('/core/preview?fileid={fileid}', {
|
||||
|| generateUrl('/core/preview?fileId={fileid}', {
|
||||
fileid: this.fileid,
|
||||
})
|
||||
const url = new URL(window.location.origin + previewUrl)
|
||||
|
@ -699,13 +700,13 @@ export default Vue.extend({
|
|||
}
|
||||
|
||||
if (success) {
|
||||
showSuccess(this.t('files', '"{displayName}" action executed successfully', { displayName }))
|
||||
showSuccess(t('files', '"{displayName}" action executed successfully', { displayName }))
|
||||
return
|
||||
}
|
||||
showError(this.t('files', '"{displayName}" action failed', { displayName }))
|
||||
showError(t('files', '"{displayName}" action failed', { displayName }))
|
||||
} catch (e) {
|
||||
logger.error('Error while executing action', { action, e })
|
||||
showError(this.t('files', '"{displayName}" action failed', { displayName }))
|
||||
showError(t('files', '"{displayName}" action failed', { displayName }))
|
||||
} finally {
|
||||
// Reset the loading marker
|
||||
this.loading = ''
|
||||
|
@ -803,15 +804,15 @@ export default Vue.extend({
|
|||
isFileNameValid(name) {
|
||||
const trimmedName = name.trim()
|
||||
if (trimmedName === '.' || trimmedName === '..') {
|
||||
throw new Error(this.t('files', '"{name}" is an invalid file name.', { name }))
|
||||
throw new Error(t('files', '"{name}" is an invalid file name.', { name }))
|
||||
} else if (trimmedName.length === 0) {
|
||||
throw new Error(this.t('files', 'File name cannot be empty.'))
|
||||
throw new Error(t('files', 'File name cannot be empty.'))
|
||||
} else if (trimmedName.indexOf('/') !== -1) {
|
||||
throw new Error(this.t('files', '"/" is not allowed inside a file name.'))
|
||||
throw new Error(t('files', '"/" is not allowed inside a file name.'))
|
||||
} else if (trimmedName.match(OC.config.blacklist_files_regex)) {
|
||||
throw new Error(this.t('files', '"{name}" is not an allowed filetype.', { name }))
|
||||
throw new Error(t('files', '"{name}" is not an allowed filetype.', { name }))
|
||||
} else if (this.checkIfNodeExists(name)) {
|
||||
throw new Error(this.t('files', '{newName} already exists.', { newName: name }))
|
||||
throw new Error(t('files', '{newName} already exists.', { newName: name }))
|
||||
}
|
||||
|
||||
const toCheck = trimmedName.split('')
|
||||
|
@ -859,7 +860,7 @@ export default Vue.extend({
|
|||
const oldEncodedSource = this.source.encodedSource
|
||||
const newName = this.newName.trim?.() || ''
|
||||
if (newName === '') {
|
||||
showError(this.t('files', 'Name cannot be empty'))
|
||||
showError(t('files', 'Name cannot be empty'))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -870,7 +871,7 @@ export default Vue.extend({
|
|||
|
||||
// Checking if already exists
|
||||
if (this.checkIfNodeExists(newName)) {
|
||||
showError(this.t('files', 'Another entry with the same name already exists'))
|
||||
showError(t('files', 'Another entry with the same name already exists'))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -894,7 +895,7 @@ export default Vue.extend({
|
|||
// Success 🎉
|
||||
emit('files:node:updated', this.source)
|
||||
emit('files:node:renamed', this.source)
|
||||
showSuccess(this.t('files', 'Renamed "{oldName}" to "{newName}"', { oldName, newName }))
|
||||
showSuccess(t('files', 'Renamed "{oldName}" to "{newName}"', { oldName, newName }))
|
||||
|
||||
// Reset the renaming store
|
||||
this.stopRenaming()
|
||||
|
@ -908,15 +909,15 @@ export default Vue.extend({
|
|||
|
||||
// TODO: 409 means current folder does not exist, redirect ?
|
||||
if (error?.response?.status === 404) {
|
||||
showError(this.t('files', 'Could not rename "{oldName}", it does not exist any more', { oldName }))
|
||||
showError(t('files', 'Could not rename "{oldName}", it does not exist any more', { oldName }))
|
||||
return
|
||||
} else if (error?.response?.status === 412) {
|
||||
showError(this.t('files', 'The name "{newName}" is already used in the folder "{dir}". Please choose a different name.', { newName, dir: this.currentDir }))
|
||||
showError(t('files', 'The name "{newName}" is already used in the folder "{dir}". Please choose a different name.', { newName, dir: this.currentDir }))
|
||||
return
|
||||
}
|
||||
|
||||
// Unknown error
|
||||
showError(this.t('files', 'Could not rename "{oldName}"', { oldName }))
|
||||
showError(t('files', 'Could not rename "{oldName}"', { oldName }))
|
||||
} finally {
|
||||
this.loading = false
|
||||
Vue.set(this.source, 'status', undefined)
|
||||
|
@ -945,8 +946,6 @@ export default Vue.extend({
|
|||
onDragOver(event: DragEvent) {
|
||||
this.dragover = this.canDrop
|
||||
if (!this.canDrop) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
event.dataTransfer.dropEffect = 'none'
|
||||
return
|
||||
}
|
||||
|
@ -959,9 +958,13 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
onDragLeave(event: DragEvent) {
|
||||
if (this.$el.contains(event.target) && event.target !== this.$el) {
|
||||
// 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
|
||||
},
|
||||
|
||||
|
@ -990,7 +993,7 @@ export default Vue.extend({
|
|||
.map(fileid => this.filesStore.getNode(fileid)) as Node[]
|
||||
|
||||
const image = await getDragAndDropPreview(nodes)
|
||||
event.dataTransfer.setDragImage(image, -10, -10)
|
||||
event.dataTransfer?.setDragImage(image, -10, -10)
|
||||
},
|
||||
onDragEnd() {
|
||||
this.draggingStore.reset()
|
||||
|
@ -999,6 +1002,9 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
async onDrop(event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
// If another button is pressed, cancel it
|
||||
// This allows cancelling the drag with the right click
|
||||
if (!this.canDrop || event.button !== 0) {
|
||||
|
@ -1010,6 +1016,16 @@ export default Vue.extend({
|
|||
|
||||
logger.debug('Dropped', { event, selection: this.draggingFiles })
|
||||
|
||||
// Check whether we're uploading files
|
||||
if (event.dataTransfer?.files?.length > 0) {
|
||||
const uploader = getUploader()
|
||||
event.dataTransfer.files.forEach((file: File) => {
|
||||
uploader.upload(join(this.source.path, file.name), file)
|
||||
})
|
||||
logger.debug(`Uploading files to ${this.source.path}`)
|
||||
return
|
||||
}
|
||||
|
||||
const nodes = this.draggingFiles.map(fileid => this.filesStore.getNode(fileid)) as Node[]
|
||||
nodes.forEach(async (node: Node) => {
|
||||
Vue.set(node, 'status', NodeStatus.LOADING)
|
||||
|
@ -1019,9 +1035,9 @@ export default Vue.extend({
|
|||
} catch (error) {
|
||||
logger.error('Error while moving file', { error })
|
||||
if (isCopy) {
|
||||
showError(this.t('files', 'Could not copy {file}. {message}', { file: node.basename, message: error.message || '' }))
|
||||
showError(t('files', 'Could not copy {file}. {message}', { file: node.basename, message: error.message || '' }))
|
||||
} else {
|
||||
showError(this.t('files', 'Could not move {file}. {message}', { file: node.basename, message: error.message || '' }))
|
||||
showError(t('files', 'Could not move {file}. {message}', { file: node.basename, message: error.message || '' }))
|
||||
}
|
||||
} finally {
|
||||
Vue.set(node, 'status', undefined)
|
||||
|
@ -1036,7 +1052,7 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
|
||||
t: translate,
|
||||
t,
|
||||
formatFileSize,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -159,7 +159,6 @@ export default Vue.extend({
|
|||
<style scoped lang="scss">
|
||||
// Scoped row
|
||||
tr {
|
||||
padding-bottom: 300px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
// Prevent hover effect on the whole row
|
||||
background-color: transparent !important;
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
<template>
|
||||
<NcButton :aria-label="sortAriaLabel(name)"
|
||||
:class="{'files-list__column-sort-button--active': sortingMode === mode}"
|
||||
:alignment="mode !== 'size' ? 'start-reverse' : ''"
|
||||
:alignment="mode !== 'size' ? 'start-reverse' : 'center'"
|
||||
class="files-list__column-sort-button"
|
||||
type="tertiary"
|
||||
@click.stop.prevent="toggleSortBy(mode)">
|
||||
|
|
|
@ -20,62 +20,76 @@
|
|||
-
|
||||
-->
|
||||
<template>
|
||||
<VirtualList :data-component="FileEntry"
|
||||
:data-key="'source'"
|
||||
:data-sources="nodes"
|
||||
:item-height="56"
|
||||
:extra-props="{
|
||||
isMtimeAvailable,
|
||||
isSizeAvailable,
|
||||
nodes,
|
||||
filesListWidth,
|
||||
}"
|
||||
:scroll-to-index="scrollToIndex">
|
||||
<!-- Accessibility description and headers -->
|
||||
<template #before>
|
||||
<!-- Accessibility description -->
|
||||
<caption class="hidden-visually">
|
||||
{{ currentView.caption || t('files', 'List of files and folders.') }}
|
||||
{{ t('files', 'This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list.') }}
|
||||
</caption>
|
||||
<Fragment>
|
||||
<!-- Drag and drop notice -->
|
||||
<DragAndDropNotice v-if="canUpload && filesListWidth >= 512"
|
||||
:current-folder="currentFolder"
|
||||
:dragover.sync="dragover"
|
||||
:style="{ height: dndNoticeHeight }" />
|
||||
|
||||
<!-- Headers -->
|
||||
<FilesListHeader v-for="header in sortedHeaders"
|
||||
:key="header.id"
|
||||
:current-folder="currentFolder"
|
||||
:current-view="currentView"
|
||||
:header="header" />
|
||||
</template>
|
||||
<VirtualList ref="table"
|
||||
:data-component="FileEntry"
|
||||
:data-key="'source'"
|
||||
:data-sources="nodes"
|
||||
:item-height="56"
|
||||
:extra-props="{
|
||||
isMtimeAvailable,
|
||||
isSizeAvailable,
|
||||
nodes,
|
||||
filesListWidth,
|
||||
}"
|
||||
:scroll-to-index="scrollToIndex"
|
||||
@scroll="onScroll">
|
||||
<!-- Accessibility description and headers -->
|
||||
<template #before>
|
||||
<!-- Accessibility description -->
|
||||
<caption class="hidden-visually">
|
||||
{{ currentView.caption || t('files', 'List of files and folders.') }}
|
||||
{{ t('files', 'This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list.') }}
|
||||
</caption>
|
||||
|
||||
<!-- Thead-->
|
||||
<template #header>
|
||||
<FilesListTableHeader :files-list-width="filesListWidth"
|
||||
:is-mtime-available="isMtimeAvailable"
|
||||
:is-size-available="isSizeAvailable"
|
||||
:nodes="nodes" />
|
||||
</template>
|
||||
<!-- Headers -->
|
||||
<FilesListHeader v-for="header in sortedHeaders"
|
||||
:key="header.id"
|
||||
:current-folder="currentFolder"
|
||||
:current-view="currentView"
|
||||
:header="header" />
|
||||
</template>
|
||||
|
||||
<!-- Tfoot-->
|
||||
<template #footer>
|
||||
<FilesListTableFooter :files-list-width="filesListWidth"
|
||||
:is-mtime-available="isMtimeAvailable"
|
||||
:is-size-available="isSizeAvailable"
|
||||
:nodes="nodes"
|
||||
:summary="summary" />
|
||||
</template>
|
||||
</VirtualList>
|
||||
<!-- 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>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { PropType } from 'vue'
|
||||
import type { Node } from '@nextcloud/files'
|
||||
import type { Node as NcNode } from '@nextcloud/files'
|
||||
|
||||
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
|
||||
import { getFileListHeaders, Folder, View } from '@nextcloud/files'
|
||||
import { Fragment } from 'vue-frag'
|
||||
import { getFileListHeaders, Folder, View, Permission } from '@nextcloud/files'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
|
||||
import Vue from 'vue'
|
||||
|
||||
import { action as sidebarAction } from '../actions/sidebarAction.ts'
|
||||
import DragAndDropNotice from './DragAndDropNotice.vue'
|
||||
import FileEntry from './FileEntry.vue'
|
||||
import FilesListHeader from './FilesListHeader.vue'
|
||||
import FilesListTableFooter from './FilesListTableFooter.vue'
|
||||
|
@ -88,9 +102,11 @@ export default Vue.extend({
|
|||
name: 'FilesListVirtual',
|
||||
|
||||
components: {
|
||||
DragAndDropNotice,
|
||||
FilesListHeader,
|
||||
FilesListTableHeader,
|
||||
FilesListTableFooter,
|
||||
FilesListTableHeader,
|
||||
Fragment,
|
||||
VirtualList,
|
||||
},
|
||||
|
||||
|
@ -108,7 +124,7 @@ export default Vue.extend({
|
|||
required: true,
|
||||
},
|
||||
nodes: {
|
||||
type: Array as PropType<Node[]>,
|
||||
type: Array as PropType<NcNode[]>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
@ -118,6 +134,8 @@ export default Vue.extend({
|
|||
FileEntry,
|
||||
headers: getFileListHeaders(),
|
||||
scrollToIndex: 0,
|
||||
dragover: false,
|
||||
dndNoticeHeight: 0,
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -163,9 +181,18 @@ export default Vue.extend({
|
|||
|
||||
return [...this.headers].sort((a, b) => a.order - b.order)
|
||||
},
|
||||
|
||||
canUpload() {
|
||||
return this.currentFolder && (this.currentFolder.permissions & Permission.CREATE) !== 0
|
||||
},
|
||||
},
|
||||
|
||||
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)
|
||||
|
||||
// Scroll to the file if it's in the url
|
||||
if (this.fileId) {
|
||||
const index = this.nodes.findIndex(node => node.fileid === this.fileId)
|
||||
|
@ -176,15 +203,11 @@ export default Vue.extend({
|
|||
}
|
||||
|
||||
// Open the file sidebar if we have the room for it
|
||||
if (document.documentElement.clientWidth > 1024) {
|
||||
// Don't open the sidebar for the current folder
|
||||
if (this.currentFolder.fileid === this.fileId) {
|
||||
return
|
||||
}
|
||||
|
||||
// but don't open the sidebar for the current folder
|
||||
if (document.documentElement.clientWidth > 1024 && this.currentFolder.fileid !== this.fileId) {
|
||||
// Open the sidebar for the given URL fileid
|
||||
// iif we just loaded the app.
|
||||
const node = this.nodes.find(n => n.fileid === this.fileId) as Node
|
||||
const node = this.nodes.find(n => n.fileid === this.fileId) as NcNode
|
||||
if (node && sidebarAction?.enabled?.([node], this.currentView)) {
|
||||
logger.debug('Opening sidebar on file ' + node.path, { node })
|
||||
sidebarAction.exec(node, this.currentView, this.currentFolder.path)
|
||||
|
@ -197,6 +220,49 @@ export default Vue.extend({
|
|||
return node.fileid
|
||||
},
|
||||
|
||||
onDragOver(event: DragEvent) {
|
||||
// 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
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
// If reaching top, scroll up
|
||||
const firstVisible = this.$refs.table?.$el?.querySelector('.files-list__row--visible') as HTMLElement
|
||||
const firstSibling = firstVisible?.previousElementSibling as HTMLElement
|
||||
if ([firstVisible, firstSibling].some(elmt => elmt?.contains(event.target as Node))) {
|
||||
this.$refs.table.$el.scrollTop = this.$refs.table.$el.scrollTop - 25
|
||||
return
|
||||
}
|
||||
|
||||
// If reaching bottom, scroll down
|
||||
const lastVisible = [...(this.$refs.table?.$el?.querySelectorAll('.files-list__row--visible') || [])].pop() as HTMLElement
|
||||
const nextSibling = lastVisible?.nextElementSibling as HTMLElement
|
||||
if ([lastVisible, nextSibling].some(elmt => elmt?.contains(event.target as Node))) {
|
||||
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,
|
||||
},
|
||||
})
|
||||
|
@ -232,6 +298,15 @@ export default Vue.extend({
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
.files-list__thead,
|
||||
.files-list__tfoot {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
background-color: var(--color-main-background);
|
||||
|
||||
}
|
||||
|
||||
// Table header
|
||||
.files-list__thead {
|
||||
// Pinned on top when scrolling
|
||||
|
@ -240,12 +315,9 @@ export default Vue.extend({
|
|||
top: 0;
|
||||
}
|
||||
|
||||
.files-list__thead,
|
||||
// Table footer
|
||||
.files-list__tfoot {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
background-color: var(--color-main-background);
|
||||
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
tr {
|
||||
|
|
|
@ -152,11 +152,8 @@ export default Vue.extend({
|
|||
onScroll() {
|
||||
// Max 0 to prevent negative index
|
||||
this.index = Math.max(0, Math.round((this.$el.scrollTop - this.beforeHeight) / this.itemHeight))
|
||||
this.$emit('scroll')
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
|
|
@ -437,6 +437,7 @@ export default Vue.extend({
|
|||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
max-height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
$margin: 4px;
|
||||
|
|
|
@ -20,3 +20,7 @@
|
|||
*
|
||||
*/
|
||||
import './commands.ts'
|
||||
|
||||
// Fix ResizeObserver loop limit exceeded happening in Cypress only
|
||||
// @see https://github.com/cypress-io/cypress/issues/20341
|
||||
Cypress.on('uncaught:exception', err => !err.message.includes('ResizeObserver loop limit exceeded'))
|
||||
|
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Загрузка…
Ссылка в новой задаче