diff options
author | Ferdinand Thiessen <opensource@fthiessen.de> | 2025-02-28 19:28:04 +0100 |
---|---|---|
committer | Ferdinand Thiessen <opensource@fthiessen.de> | 2025-03-05 11:15:16 +0100 |
commit | 9a71e1b8c290b0f3297ab3f304b39146a1677f18 (patch) | |
tree | c32ae981e5d48d45cda5e6f0a2baa777d78af549 /apps/files | |
parent | b9a166a48fbb063de7a5746fc70ea00ea3b09743 (diff) | |
download | nextcloud-server-9a71e1b8c290b0f3297ab3f304b39146a1677f18.tar.gz nextcloud-server-9a71e1b8c290b0f3297ab3f304b39146a1677f18.zip |
fix(files): also show file list headers on empty views
It is needed, e.g. for the note-to-recipient, that the headers
are also shown when there is no content (yet).
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
Diffstat (limited to 'apps/files')
-rw-r--r-- | apps/files/src/components/FilesListHeader.vue | 13 | ||||
-rw-r--r-- | apps/files/src/components/FilesListVirtual.vue | 21 | ||||
-rw-r--r-- | apps/files/src/composables/useFileListHeaders.spec.ts | 41 | ||||
-rw-r--r-- | apps/files/src/composables/useFileListHeaders.ts | 19 | ||||
-rw-r--r-- | apps/files/src/views/FilesList.vue | 25 |
5 files changed, 97 insertions, 22 deletions
diff --git a/apps/files/src/components/FilesListHeader.vue b/apps/files/src/components/FilesListHeader.vue index 96d465a23d2..cc8dafe344e 100644 --- a/apps/files/src/components/FilesListHeader.vue +++ b/apps/files/src/components/FilesListHeader.vue @@ -9,6 +9,9 @@ </template> <script lang="ts"> +import type { Folder, Header, View } from '@nextcloud/files' +import type { PropType } from 'vue' + /** * This component is used to render custom * elements provided by an API. Vue doesn't allow @@ -19,21 +22,21 @@ export default { name: 'FilesListHeader', props: { header: { - type: Object, + type: Object as PropType<Header>, required: true, }, currentFolder: { - type: Object, + type: Object as PropType<Folder>, required: true, }, currentView: { - type: Object, + type: Object as PropType<View>, required: true, }, }, computed: { enabled() { - return this.header.enabled(this.currentFolder, this.currentView) + return this.header.enabled?.(this.currentFolder, this.currentView) ?? true }, }, watch: { @@ -49,7 +52,7 @@ export default { }, mounted() { console.debug('Mounted', this.header.id) - this.header.render(this.$refs.mount, this.currentFolder, this.currentView) + this.header.render(this.$refs.mount as HTMLElement, this.currentFolder, this.currentView) }, } </script> diff --git a/apps/files/src/components/FilesListVirtual.vue b/apps/files/src/components/FilesListVirtual.vue index 43e043b2c3c..3042f54a319 100644 --- a/apps/files/src/components/FilesListVirtual.vue +++ b/apps/files/src/components/FilesListVirtual.vue @@ -27,7 +27,7 @@ <template #before> <!-- Headers --> - <FilesListHeader v-for="header in sortedHeaders" + <FilesListHeader v-for="header in headers" :key="header.id" :current-folder="currentFolder" :current-view="currentView" @@ -62,7 +62,7 @@ import type { Node as NcNode } from '@nextcloud/files' import type { ComponentPublicInstance, PropType } from 'vue' import type { Location } from 'vue-router' -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' @@ -70,12 +70,13 @@ 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 { useActiveStore } from '../store/active.ts' import { useSelectionStore } from '../store/selection.js' import { useUserConfigStore } from '../store/userconfig.ts' +import { getSummaryFor } from '../utils/fileUtils.ts' import FileEntry from './FileEntry.vue' import FileEntryGrid from './FileEntryGrid.vue' @@ -84,8 +85,8 @@ 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' +import logger from '../logger.ts' export default defineComponent({ name: 'FilesListVirtual', @@ -125,6 +126,7 @@ export default defineComponent({ return { fileId, fileListWidth, + headers: useFileListHeaders(), openDetails, openFile, @@ -140,7 +142,6 @@ export default defineComponent({ return { FileEntry, FileEntryGrid, - headers: getFileListHeaders(), scrollToIndex: 0, openFileId: null as number|null, } @@ -170,14 +171,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 }, diff --git a/apps/files/src/composables/useFileListHeaders.spec.ts b/apps/files/src/composables/useFileListHeaders.spec.ts new file mode 100644 index 00000000000..c407156412b --- /dev/null +++ b/apps/files/src/composables/useFileListHeaders.spec.ts @@ -0,0 +1,41 @@ +/*! + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { Header } from '@nextcloud/files' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { useFileListHeaders } from './useFileListHeaders.ts' + +const getFileListHeaders = vi.hoisted(() => vi.fn()) + +vi.mock('@nextcloud/files', async (originalModule) => { + return { + ...(await originalModule()), + getFileListHeaders, + } +}) + +describe('useFileListHeaders', () => { + beforeEach(() => vi.resetAllMocks()) + + it('gets the headers', () => { + const header = new Header({ id: '1', order: 5, render: vi.fn(), updated: vi.fn() }) + getFileListHeaders.mockImplementationOnce(() => [header]) + + const headers = useFileListHeaders() + expect(headers.value).toEqual([header]) + expect(getFileListHeaders).toHaveBeenCalledOnce() + }) + + it('headers are sorted', () => { + const header = new Header({ id: '1', order: 10, render: vi.fn(), updated: vi.fn() }) + const header2 = new Header({ id: '2', order: 5, render: vi.fn(), updated: vi.fn() }) + getFileListHeaders.mockImplementationOnce(() => [header, header2]) + + const headers = useFileListHeaders() + // lower order first + expect(headers.value.map(({ id }) => id)).toStrictEqual(['2', '1']) + expect(getFileListHeaders).toHaveBeenCalledOnce() + }) +}) diff --git a/apps/files/src/composables/useFileListHeaders.ts b/apps/files/src/composables/useFileListHeaders.ts new file mode 100644 index 00000000000..b57bcbb1432 --- /dev/null +++ b/apps/files/src/composables/useFileListHeaders.ts @@ -0,0 +1,19 @@ +/*! + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import type { Header } from '@nextcloud/files' +import type { ComputedRef } from 'vue' + +import { getFileListHeaders } from '@nextcloud/files' +import { computed, ref } from 'vue' + +/** + * Get the registered and sorted file list headers. + */ +export function useFileListHeaders(): ComputedRef<Header[]> { + const headers = ref(getFileListHeaders()) + const sorted = computed(() => [...headers.value].sort((a, b) => a.order - b.order) as Header[]) + + return sorted +} diff --git a/apps/files/src/views/FilesList.vue b/apps/files/src/views/FilesList.vue index 253a3ec8196..c582bdf4cfa 100644 --- a/apps/files/src/views/FilesList.vue +++ b/apps/files/src/views/FilesList.vue @@ -71,7 +71,7 @@ </div> <!-- Drag and drop notice --> - <DragAndDropNotice v-if="!loading && canUpload" :current-folder="currentFolder" /> + <DragAndDropNotice v-if="!loading && canUpload && currentFolder" :current-folder="currentFolder" /> <!-- Initial loading --> <NcLoadingIcon v-if="loading && !isRefreshing" @@ -80,7 +80,15 @@ :name="t('files', 'Loading current folder')" /> <!-- Empty content placeholder --> - <template v-else-if="!loading && isEmptyDir"> + <template v-else-if="!loading && isEmptyDir && currentFolder && currentView"> + <div class="files-list__before"> + <!-- Headers --> + <FilesListHeader v-for="header in headers" + :key="header.id" + :current-folder="currentFolder" + :current-view="currentView" + :header="header" /> + </div> <!-- Empty due to error --> <NcEmptyContent v-if="error" :name="error" data-cy-files-content-error> <template #action> @@ -169,6 +177,7 @@ import ViewGridIcon from 'vue-material-design-icons/ViewGrid.vue' import { action as sidebarAction } from '../actions/sidebarAction.ts' import { useNavigation } from '../composables/useNavigation.ts' +import { useFileListHeaders } from '../composables/useFileListHeaders.ts' import { useFileListWidth } from '../composables/useFileListWidth.ts' import { useRouteParameters } from '../composables/useRouteParameters.ts' import { useFilesStore } from '../store/files.ts' @@ -178,12 +187,13 @@ 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 { humanizeWebDAVError } from '../utils/davUtils.ts' import BreadCrumbs from '../components/BreadCrumbs.vue' +import FilesListHeader from '../components/FilesListHeader.vue' import FilesListVirtual from '../components/FilesListVirtual.vue' import filesSortingMixin from '../mixins/filesSorting.ts' import logger from '../logger.ts' import DragAndDropNotice from '../components/DragAndDropNotice.vue' -import { humanizeWebDAVError } from '../utils/davUtils.ts' const isSharingEnabled = (getCapabilities() as { files_sharing?: boolean })?.files_sharing !== undefined @@ -193,6 +203,7 @@ export default defineComponent({ components: { BreadCrumbs, DragAndDropNotice, + FilesListHeader, FilesListVirtual, LinkIcon, ListViewIcon, @@ -241,6 +252,7 @@ export default defineComponent({ directory, fileId, fileListWidth, + headers: useFileListHeaders(), t, filesStore, @@ -814,6 +826,13 @@ export default defineComponent({ } } + &__before { + display: flex; + flex-direction: column; + gap: calc(var(--default-grid-baseline) * 2); + margin-inline: calc(var(--default-clickable-area) + 2 * var(--app-navigation-padding)); + } + &__empty-view-wrapper { display: flex; height: 100%; |