Merge pull request #51148 from nextcloud/fix/files-header-empty-view

fix(files): also show file list headers on empty views
This commit is contained in:
Ferdinand Thiessen 2025-03-05 11:55:31 +01:00 committed by GitHub
commit 62e7e293f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 126 additions and 25 deletions

View file

@ -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>

View file

@ -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
},

View file

@ -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()
})
})

View file

@ -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
}

View file

@ -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%;

View file

@ -24,6 +24,12 @@ const searchForActionInRow = (row: JQuery<HTMLElement>, actionId: string): Cypre
// Else look in the action menu
const menuButtonId = row.find('button[aria-controls]').attr('aria-controls')
if (menuButtonId === undefined) {
return cy.wrap(Cypress.$())
}
// eslint-disable-next-line no-unused-expressions
expect(menuButtonId).not.to.be.undefined
return cy.get(`#${menuButtonId} [data-cy-files-list-row-action="${CSS.escape(actionId)}"]`)
}

View file

@ -40,6 +40,26 @@ describe('files_sharing: Note to recipient', { testIsolation: true }, () => {
.and('contain.text', 'Hello, this is the note.')
})
it('displays the note to the sharee even if the file list is empty', () => {
cy.mkdir(user, '/folder')
cy.login(user)
cy.visit('/apps/files')
// share the folder
createShare('folder', sharee.userId, { read: true, download: true, note: 'Hello, this is the note.' })
cy.logout()
// Now for the sharee
cy.login(sharee)
// visit shared files view
cy.visit('/apps/files')
navigateToFolder('folder')
cy.get('.note-to-recipient')
.should('be.visible')
.and('contain.text', 'Hello, this is the note.')
})
/**
* Regression test for https://github.com/nextcloud/server/issues/46188
*/

4
dist/files-main.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long