diff options
Diffstat (limited to 'apps/files/src/components/FilesListVirtual.vue')
-rw-r--r-- | apps/files/src/components/FilesListVirtual.vue | 255 |
1 files changed, 168 insertions, 87 deletions
diff --git a/apps/files/src/components/FilesListVirtual.vue b/apps/files/src/components/FilesListVirtual.vue index 2442dd98190..47b8ef19b19 100644 --- a/apps/files/src/components/FilesListVirtual.vue +++ b/apps/files/src/components/FilesListVirtual.vue @@ -9,6 +9,7 @@ :data-sources="nodes" :grid-mode="userConfig.grid_view" :extra-props="{ + isMimeAvailable, isMtimeAvailable, isSizeAvailable, nodes, @@ -20,14 +21,16 @@ </template> <template v-if="!isNoneSelected" #header-overlay> - <span class="files-list__selected">{{ t('files', '{count} selected', { count: selectedNodes.length }) }}</span> + <span class="files-list__selected"> + {{ n('files', '{count} selected', '{count} selected', selectedNodes.length, { count: selectedNodes.length }) }} + </span> <FilesListTableHeaderActions :current-view="currentView" :selected-nodes="selectedNodes" /> </template> <template #before> <!-- Headers --> - <FilesListHeader v-for="header in sortedHeaders" + <FilesListHeader v-for="header in headers" :key="header.id" :current-folder="currentFolder" :current-view="currentView" @@ -39,15 +42,22 @@ <!-- Table header and sort buttons --> <FilesListTableHeader ref="thead" :files-list-width="fileListWidth" + :is-mime-available="isMimeAvailable" :is-mtime-available="isMtimeAvailable" :is-size-available="isSizeAvailable" :nodes="nodes" /> </template> + <!-- Body replacement if no files are available --> + <template #empty> + <slot name="empty" /> + </template> + <!-- Tfoot--> <template #footer> <FilesListTableFooter :current-view="currentView" :files-list-width="fileListWidth" + :is-mime-available="isMimeAvailable" :is-mtime-available="isMtimeAvailable" :is-size-available="isSizeAvailable" :nodes="nodes" @@ -57,24 +67,25 @@ </template> <script lang="ts"> -import type { ComponentPublicInstance, PropType } from 'vue' -import type { Node as NcNode } from '@nextcloud/files' import type { UserConfig } from '../types' +import type { Node as NcNode } from '@nextcloud/files' +import type { ComponentPublicInstance, PropType } from 'vue' -import { defineComponent } from 'vue' -import { getFileListHeaders, Folder, Permission, View, getFileActions, FileType } from '@nextcloud/files' +import { Folder, Permission, View, getFileActions, FileType } from '@nextcloud/files' import { showError } from '@nextcloud/dialogs' import { subscribe, unsubscribe } from '@nextcloud/event-bus' -import { translate as t } from '@nextcloud/l10n' -import { useHotKey } from '@nextcloud/vue/dist/Composables/useHotKey.js' +import { n, t } from '@nextcloud/l10n' +import { useHotKey } from '@nextcloud/vue/composables/useHotKey' +import { defineComponent } from 'vue' import { action as sidebarAction } from '../actions/sidebarAction.ts' -import { getSummaryFor } from '../utils/fileUtils' import { useActiveStore } from '../store/active.ts' +import { useFileListHeaders } from '../composables/useFileListHeaders.ts' import { useFileListWidth } from '../composables/useFileListWidth.ts' import { useRouteParameters } from '../composables/useRouteParameters.ts' import { useSelectionStore } from '../store/selection.js' import { useUserConfigStore } from '../store/userconfig.ts' +import logger from '../logger.ts' import FileEntry from './FileEntry.vue' import FileEntryGrid from './FileEntryGrid.vue' @@ -83,7 +94,6 @@ import FilesListHeader from './FilesListHeader.vue' import FilesListTableFooter from './FilesListTableFooter.vue' import FilesListTableHeader from './FilesListTableHeader.vue' import FilesListTableHeaderActions from './FilesListTableHeaderActions.vue' -import logger from '../logger.ts' import VirtualList from './VirtualList.vue' export default defineComponent({ @@ -111,6 +121,10 @@ export default defineComponent({ type: Array as PropType<NcNode[]>, required: true, }, + summary: { + type: String, + required: true, + }, }, setup() { @@ -124,6 +138,7 @@ export default defineComponent({ return { fileId, fileListWidth, + headers: useFileListHeaders(), openDetails, openFile, @@ -131,6 +146,7 @@ export default defineComponent({ selectionStore, userConfigStore, + n, t, } }, @@ -139,9 +155,7 @@ export default defineComponent({ return { FileEntry, FileEntryGrid, - headers: getFileListHeaders(), scrollToIndex: 0, - openFileId: null as number|null, } }, @@ -150,10 +164,16 @@ export default defineComponent({ return this.userConfigStore.userConfig }, - summary() { - return getSummaryFor(this.nodes) + isMimeAvailable() { + if (!this.userConfig.show_mime_column) { + return false + } + // Hide mime column on narrow screens + if (this.fileListWidth < 1024) { + return false + } + return this.nodes.some(node => node.mime !== undefined || node.mime !== 'application/octet-stream') }, - isMtimeAvailable() { // Hide mtime column on narrow screens if (this.fileListWidth < 768) { @@ -169,14 +189,6 @@ export default defineComponent({ return this.nodes.some(node => node.size !== undefined) }, - sortedHeaders() { - if (!this.currentFolder || !this.currentView) { - return [] - } - - return [...this.headers].sort((a, b) => a.order - b.order) - }, - cantUpload() { return this.currentFolder && (this.currentFolder.permissions & Permission.CREATE) === 0 }, @@ -188,7 +200,7 @@ export default defineComponent({ caption() { const defaultCaption = t('files', 'List of files and folders.') const viewCaption = this.currentView.caption || defaultCaption - const cantUploadCaption = this.cantUpload ? t('files', 'You don’t have permission to upload or create files here.') : null + const cantUploadCaption = this.cantUpload ? t('files', 'You do not have permission to upload or create files here.') : null const quotaExceededCaption = this.isQuotaExceeded ? t('files', 'You have used your space quota and cannot upload files anymore.') : null const sortableCaption = t('files', 'Column headers with buttons are sortable.') const virtualListNote = t('files', 'This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list.') @@ -208,38 +220,26 @@ export default defineComponent({ isNoneSelected() { return this.selectedNodes.length === 0 }, + + isEmpty() { + return this.nodes.length === 0 + }, }, watch: { - fileId: { - handler(fileId) { - this.scrollToFile(fileId, false) - }, - immediate: true, + // If nodes gets populated and we have a fileId, + // an openFile or openDetails, we fire the appropriate actions. + isEmpty() { + this.handleOpenQueries() }, - - openFile: { - handler() { - // wait for scrolling and updating the actions to settle - this.$nextTick(() => { - if (this.fileId && this.openFile) { - this.handleOpenFile(this.fileId) - } - }) - }, - immediate: true, + fileId() { + this.handleOpenQueries() }, - - openDetails: { - handler() { - // wait for scrolling and updating the actions to settle - this.$nextTick(() => { - if (this.fileId && this.openDetails) { - this.openSidebarForFile(this.fileId) - } - }) - }, - immediate: true, + openFile() { + this.handleOpenQueries() + }, + openDetails() { + this.handleOpenQueries() }, }, @@ -269,6 +269,33 @@ export default defineComponent({ }, methods: { + handleOpenQueries() { + // If the list is empty, or we don't have a fileId, + // there's nothing to be done. + if (this.isEmpty || !this.fileId) { + return + } + + logger.debug('FilesListVirtual: checking for requested fileId, openFile or openDetails', { + nodes: this.nodes, + fileId: this.fileId, + openFile: this.openFile, + openDetails: this.openDetails, + }) + + if (this.openFile) { + this.handleOpenFile(this.fileId) + } + + if (this.openDetails) { + this.openSidebarForFile(this.fileId) + } + + if (this.fileId) { + this.scrollToFile(this.fileId, false) + } + }, + openSidebarForFile(fileId) { // Open the sidebar for the given URL fileid // iif we just loaded the app. @@ -276,7 +303,9 @@ export default defineComponent({ if (node && sidebarAction?.enabled?.([node], this.currentView)) { logger.debug('Opening sidebar on file ' + node.path, { node }) sidebarAction.exec(node, this.currentView, this.currentFolder.path) + return } + logger.warn(`Failed to open sidebar on file ${fileId}, file isn't cached yet !`, { fileId, node }) }, scrollToFile(fileId: number|null, warn = true) { @@ -292,6 +321,7 @@ export default defineComponent({ } this.scrollToIndex = Math.max(0, index) + logger.debug('Scrolling to file ' + fileId, { fileId, index }) } }, @@ -303,7 +333,7 @@ export default defineComponent({ delete query.openfile delete query.opendetails - this.activeStore.clearActiveNode() + this.activeStore.activeNode = undefined window.OCP.Files.Router.goToRoute( null, { ...this.$route.params, fileid: String(this.currentFolder.fileid ?? '') }, @@ -329,30 +359,40 @@ export default defineComponent({ * Handle opening a file (e.g. by ?openfile=true) * @param fileId File to open */ - handleOpenFile(fileId: number|null) { - if (fileId === null) { - return - } - + async handleOpenFile(fileId: number) { const node = this.nodes.find(n => n.fileid === fileId) as NcNode - if (node === undefined || node.type === FileType.Folder) { + if (node === undefined) { return } - logger.debug('Opening file ' + node.path, { node }) - this.openFileId = fileId - const defaultAction = getFileActions() - // Get only default actions (visible and hidden) - .filter(action => !!action?.default) - // Find actions that are either always enabled or enabled for the current node - .filter((action) => !action.enabled || action.enabled([node], this.currentView)) - // Sort enabled default actions by order - .sort((a, b) => (a.order || 0) - (b.order || 0)) - // Get the first one - .at(0) - // Some file types do not have a default action (e.g. they can only be downloaded) - // So if there is an enabled default action, so execute it - defaultAction?.exec(node, this.currentView, this.currentFolder.path) + if (node.type === FileType.File) { + const defaultAction = getFileActions() + // Get only default actions (visible and hidden) + .filter((action) => !!action?.default) + // Find actions that are either always enabled or enabled for the current node + .filter((action) => !action.enabled || action.enabled([node], this.currentView)) + .filter((action) => action.id !== 'download') + // Sort enabled default actions by order + .sort((a, b) => (a.order || 0) - (b.order || 0)) + // Get the first one + .at(0) + + // Some file types do not have a default action (e.g. they can only be downloaded) + // So if there is an enabled default action, so execute it + if (defaultAction) { + logger.debug('Opening file ' + node.path, { node }) + return await defaultAction.exec(node, this.currentView, this.currentFolder.path) + } + } + // The file is either a folder or has no default action other than downloading + // in this case we need to open the details instead and remove the route from the history + logger.debug('Ignore `openfile` query and replacing with `opendetails` for ' + node.path, { node }) + window.OCP.Files.Router.goToRoute( + null, + this.$route.params, + { ...this.$route.query, openfile: undefined, opendetails: '' }, + true, // silent update of the URL + ) }, onDragOver(event: DragEvent) { @@ -425,7 +465,7 @@ export default defineComponent({ delete query.openfile delete query.opendetails - this.activeStore.setActiveNode(node) + this.activeStore.activeNode = node // Silent update of the URL window.OCP.Files.Router.goToRoute( @@ -441,15 +481,17 @@ export default defineComponent({ <style scoped lang="scss"> .files-list { - --row-height: 55px; + --row-height: 44px; --cell-margin: 14px; --checkbox-padding: calc((var(--row-height) - var(--checkbox-size)) / 2); --checkbox-size: 24px; --clickable-area: var(--default-clickable-area); - --icon-preview-size: 32px; + --icon-preview-size: 24px; --fixed-block-start-position: var(--default-clickable-area); + display: flex; + flex-direction: column; overflow: auto; height: 100%; will-change: scroll-position; @@ -497,6 +539,13 @@ export default defineComponent({ // Hide the table header below the overlay margin-block-start: calc(-1 * var(--row-height)); } + + // Visually hide the table when there are no files + &--hidden { + visibility: hidden; + z-index: -1; + opacity: 0; + } } .files-list__filters { @@ -528,6 +577,7 @@ export default defineComponent({ background-color: var(--color-main-background); border-block-end: 1px solid var(--color-border); height: var(--row-height); + flex: 0 0 var(--row-height); } .files-list__thead, @@ -536,7 +586,6 @@ export default defineComponent({ flex-direction: column; width: 100%; background-color: var(--color-main-background); - } // Table header @@ -547,6 +596,16 @@ export default defineComponent({ top: var(--fixed-block-start-position); } + // Empty content + .files-list__empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + } + tr { position: relative; display: flex; @@ -716,8 +775,8 @@ export default defineComponent({ // File and folder overlay &-overlay { position: absolute; - max-height: calc(var(--icon-preview-size) * 0.5); - max-width: calc(var(--icon-preview-size) * 0.5); + max-height: calc(var(--icon-preview-size) * 0.6); + max-width: calc(var(--icon-preview-size) * 0.6); color: var(--color-primary-element-text); // better alignment with the folder icon margin-block-start: 2px; @@ -822,22 +881,28 @@ export default defineComponent({ margin-inline-end: 7px; } + .files-list__row-mime, .files-list__row-mtime, .files-list__row-size { color: var(--color-text-maxcontrast); } + .files-list__row-size { - width: calc(var(--row-height) * 1.5); + width: calc(var(--row-height) * 2); // Right align content/text justify-content: flex-end; } .files-list__row-mtime { - width: calc(var(--row-height) * 2); + width: calc(var(--row-height) * 2.5); + } + + .files-list__row-mime { + width: calc(var(--row-height) * 3.5); } .files-list__row-column-custom { - width: calc(var(--row-height) * 2); + width: calc(var(--row-height) * 2.5); } } } @@ -853,12 +918,11 @@ export default defineComponent({ <style lang="scss"> // Grid mode -tbody.files-list__tbody.files-list__tbody--grid { - --half-clickable-area: calc(var(--clickable-area) / 2); +.files-list--grid tbody.files-list__tbody { --item-padding: 16px; --icon-preview-size: 166px; - --name-height: 32px; - --mtime-height: 16px; + --name-height: var(--default-clickable-area); + --mtime-height: calc(var(--font-size-small) + var(--default-grid-baseline)); --row-width: calc(var(--icon-preview-size) + var(--item-padding) * 2); --row-height: calc(var(--icon-preview-size) + var(--name-height) + var(--mtime-height) + var(--item-padding) * 2); --checkbox-padding: 0px; @@ -940,15 +1004,32 @@ tbody.files-list__tbody.files-list__tbody--grid { .files-list__row-mtime { width: var(--icon-preview-size); height: var(--mtime-height); - font-size: calc(var(--default-font-size) - 4px); + font-size: var(--font-size-small); } .files-list__row-actions { position: absolute; - inset-inline-end: calc(var(--half-clickable-area) / 2); + inset-inline-end: calc(var(--clickable-area) / 4); inset-block-end: calc(var(--mtime-height) / 2); width: var(--clickable-area); height: var(--clickable-area); } } + +@media screen and (max-width: 768px) { + // there is no mtime + .files-list--grid tbody.files-list__tbody { + --mtime-height: 0px; + + // so we move the action to the name + .files-list__row-actions { + inset-block-end: var(--item-padding); + } + + // and we need to keep space on the name for the actions + .files-list__row-name-text { + padding-inline-end: var(--clickable-area) !important; + } + } +} </style> |