aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files
diff options
context:
space:
mode:
authorFerdinand Thiessen <opensource@fthiessen.de>2025-02-28 19:28:04 +0100
committerFerdinand Thiessen <opensource@fthiessen.de>2025-03-05 11:15:16 +0100
commit9a71e1b8c290b0f3297ab3f304b39146a1677f18 (patch)
treec32ae981e5d48d45cda5e6f0a2baa777d78af549 /apps/files
parentb9a166a48fbb063de7a5746fc70ea00ea3b09743 (diff)
downloadnextcloud-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.vue13
-rw-r--r--apps/files/src/components/FilesListVirtual.vue21
-rw-r--r--apps/files/src/composables/useFileListHeaders.spec.ts41
-rw-r--r--apps/files/src/composables/useFileListHeaders.ts19
-rw-r--r--apps/files/src/views/FilesList.vue25
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%;