diff options
author | Ferdinand Thiessen <opensource@fthiessen.de> | 2024-11-15 01:51:28 +0100 |
---|---|---|
committer | Ferdinand Thiessen <opensource@fthiessen.de> | 2024-11-20 19:08:21 +0100 |
commit | 675e7a953f15e22578da5e37054fe6a2f34f7de5 (patch) | |
tree | 0918e1438c9c9e34fd784ffdfe21a1932a958865 /apps | |
parent | 3822db51742eb12c67b525cab80ec0699e011684 (diff) | |
download | nextcloud-server-675e7a953f15e22578da5e37054fe6a2f34f7de5.tar.gz nextcloud-server-675e7a953f15e22578da5e37054fe6a2f34f7de5.zip |
refactor(files): Provide `useFileListWidth` composable
Replace the mixin with a composable, this is better typed and works in
both: Options- and Composition API.
Also added component tests for it.
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
Diffstat (limited to 'apps')
-rw-r--r-- | apps/files/src/components/BreadCrumbs.vue | 16 | ||||
-rw-r--r-- | apps/files/src/components/FileEntry.vue | 5 | ||||
-rw-r--r-- | apps/files/src/components/FileEntry/FileEntryActions.vue | 8 | ||||
-rw-r--r-- | apps/files/src/components/FileEntry/FileEntryName.vue | 7 | ||||
-rw-r--r-- | apps/files/src/components/FileEntryGrid.vue | 2 | ||||
-rw-r--r-- | apps/files/src/components/FilesListTableHeaderActions.vue | 14 | ||||
-rw-r--r-- | apps/files/src/components/FilesListVirtual.vue | 18 | ||||
-rw-r--r-- | apps/files/src/components/VirtualList.vue | 21 | ||||
-rw-r--r-- | apps/files/src/composables/useFileListWidth.cy.ts | 56 | ||||
-rw-r--r-- | apps/files/src/composables/useFileListWidth.ts | 50 | ||||
-rw-r--r-- | apps/files/src/mixins/filesListWidth.ts | 33 | ||||
-rw-r--r-- | apps/files/src/views/FilesList.vue | 9 |
12 files changed, 155 insertions, 84 deletions
diff --git a/apps/files/src/components/BreadCrumbs.vue b/apps/files/src/components/BreadCrumbs.vue index c423b698d40..3569228dbde 100644 --- a/apps/files/src/components/BreadCrumbs.vue +++ b/apps/files/src/components/BreadCrumbs.vue @@ -14,7 +14,7 @@ v-bind="section" dir="auto" :to="section.to" - :force-icon-text="index === 0 && filesListWidth >= 486" + :force-icon-text="index === 0 && fileListWidth >= 486" :title="titleForSection(index, section)" :aria-description="ariaForSection(section)" @click.native="onClick(section.to)" @@ -46,15 +46,15 @@ import NcBreadcrumb from '@nextcloud/vue/dist/Components/NcBreadcrumb.js' import NcBreadcrumbs from '@nextcloud/vue/dist/Components/NcBreadcrumbs.js' import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js' -import { useNavigation } from '../composables/useNavigation' -import { onDropInternalFiles, dataTransferToFileTree, onDropExternalFiles } from '../services/DropService' +import { useNavigation } from '../composables/useNavigation.ts' +import { onDropInternalFiles, dataTransferToFileTree, onDropExternalFiles } from '../services/DropService.ts' +import { useFileListWidth } from '../composables/useFileListWidth.ts' import { showError } from '@nextcloud/dialogs' import { useDragAndDropStore } from '../store/dragging.ts' import { useFilesStore } from '../store/files.ts' import { usePathsStore } from '../store/paths.ts' import { useSelectionStore } from '../store/selection.ts' import { useUploaderStore } from '../store/uploader.ts' -import filesListWidthMixin from '../mixins/filesListWidth.ts' import logger from '../logger' export default defineComponent({ @@ -66,10 +66,6 @@ export default defineComponent({ NcIconSvgWrapper, }, - mixins: [ - filesListWidthMixin, - ], - props: { path: { type: String, @@ -83,6 +79,7 @@ export default defineComponent({ const pathsStore = usePathsStore() const selectionStore = useSelectionStore() const uploaderStore = useUploaderStore() + const fileListWidth = useFileListWidth() const { currentView, views } = useNavigation() return { @@ -93,6 +90,7 @@ export default defineComponent({ uploaderStore, currentView, + fileListWidth, views, } }, @@ -129,7 +127,7 @@ export default defineComponent({ wrapUploadProgressBar(): boolean { // if an upload is ongoing, and on small screens / mobile, then // show the progress bar for the upload below breadcrumbs - return this.isUploadInProgress && this.filesListWidth < 512 + return this.isUploadInProgress && this.fileListWidth < 512 }, // used to show the views icon for the first breadcrumb diff --git a/apps/files/src/components/FileEntry.vue b/apps/files/src/components/FileEntry.vue index fc5c7fb97f6..7af76c87c43 100644 --- a/apps/files/src/components/FileEntry.vue +++ b/apps/files/src/components/FileEntry.vue @@ -36,7 +36,6 @@ <FileEntryName ref="name" :basename="basename" :extension="extension" - :files-list-width="filesListWidth" :nodes="nodes" :source="source" @auxclick.native="execDefaultAction" @@ -47,7 +46,6 @@ <FileEntryActions v-show="!isRenamingSmallScreen" ref="actions" :class="`files-list__row-actions-${uniqueId}`" - :files-list-width="filesListWidth" :loading.sync="loading" :opened.sync="openedMenu" :source="source" /> @@ -91,6 +89,7 @@ import { formatFileSize } from '@nextcloud/files' import moment from '@nextcloud/moment' import { useNavigation } from '../composables/useNavigation.ts' +import { useFileListWidth } from '../composables/useFileListWidth.ts' import { useRouteParameters } from '../composables/useRouteParameters.ts' import { useActionsMenuStore } from '../store/actionsmenu.ts' import { useDragAndDropStore } from '../store/dragging.ts' @@ -135,6 +134,7 @@ export default defineComponent({ const filesStore = useFilesStore() const renamingStore = useRenamingStore() const selectionStore = useSelectionStore() + const filesListWidth = useFileListWidth() // The file list is guaranteed to be only shown with active view - thus we can set the `loaded` flag const { currentView } = useNavigation(true) const { @@ -152,6 +152,7 @@ export default defineComponent({ currentDir, currentFileId, currentView, + filesListWidth, } }, diff --git a/apps/files/src/components/FileEntry/FileEntryActions.vue b/apps/files/src/components/FileEntry/FileEntryActions.vue index 8c150b78087..f8fde7842a8 100644 --- a/apps/files/src/components/FileEntry/FileEntryActions.vue +++ b/apps/files/src/components/FileEntry/FileEntryActions.vue @@ -94,6 +94,7 @@ import ArrowLeftIcon from 'vue-material-design-icons/ArrowLeft.vue' import CustomElementRender from '../CustomElementRender.vue' import { useNavigation } from '../../composables/useNavigation' +import { useFileListWidth } from '../../composables/useFileListWidth.ts' import logger from '../../logger.ts' export default defineComponent({ @@ -110,10 +111,6 @@ export default defineComponent({ }, props: { - filesListWidth: { - type: Number, - required: true, - }, loading: { type: String, required: true, @@ -135,11 +132,14 @@ export default defineComponent({ setup() { // The file list is guaranteed to be only shown with active view - thus we can set the `loaded` flag const { currentView } = useNavigation(true) + + const filesListWidth = useFileListWidth() const enabledFileActions = inject<FileAction[]>('enabledFileActions', []) return { currentView, enabledFileActions, + filesListWidth, } }, diff --git a/apps/files/src/components/FileEntry/FileEntryName.vue b/apps/files/src/components/FileEntry/FileEntryName.vue index e4cffba32b7..1eff841738b 100644 --- a/apps/files/src/components/FileEntry/FileEntryName.vue +++ b/apps/files/src/components/FileEntry/FileEntryName.vue @@ -48,6 +48,7 @@ import { defineComponent, inject } from 'vue' import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' import { useNavigation } from '../../composables/useNavigation' +import { useFileListWidth } from '../../composables/useFileListWidth.ts' import { useRouteParameters } from '../../composables/useRouteParameters.ts' import { useRenamingStore } from '../../store/renaming.ts' import { getFilenameValidity } from '../../utils/filenameValidity.ts' @@ -75,10 +76,6 @@ export default defineComponent({ type: String, required: true, }, - filesListWidth: { - type: Number, - required: true, - }, nodes: { type: Array as PropType<Node[]>, required: true, @@ -97,6 +94,7 @@ export default defineComponent({ // The file list is guaranteed to be only shown with active view - thus we can set the `loaded` flag const { currentView } = useNavigation(true) const { directory } = useRouteParameters() + const filesListWidth = useFileListWidth() const renamingStore = useRenamingStore() const defaultFileAction = inject<FileAction | undefined>('defaultFileAction') @@ -105,6 +103,7 @@ export default defineComponent({ currentView, defaultFileAction, directory, + filesListWidth, renamingStore, } diff --git a/apps/files/src/components/FileEntryGrid.vue b/apps/files/src/components/FileEntryGrid.vue index f0b086ac891..0b0344afb99 100644 --- a/apps/files/src/components/FileEntryGrid.vue +++ b/apps/files/src/components/FileEntryGrid.vue @@ -38,7 +38,6 @@ <FileEntryName ref="name" :basename="basename" :extension="extension" - :files-list-width="filesListWidth" :grid-mode="true" :nodes="nodes" :source="source" @@ -58,7 +57,6 @@ <!-- Actions --> <FileEntryActions ref="actions" :class="`files-list__row-actions-${uniqueId}`" - :files-list-width="filesListWidth" :grid-mode="true" :loading.sync="loading" :opened.sync="openedMenu" diff --git a/apps/files/src/components/FilesListTableHeaderActions.vue b/apps/files/src/components/FilesListTableHeaderActions.vue index fa5f7d4bd5f..9f5724dc80f 100644 --- a/apps/files/src/components/FilesListTableHeaderActions.vue +++ b/apps/files/src/components/FilesListTableHeaderActions.vue @@ -43,10 +43,10 @@ import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js' import { useRouteParameters } from '../composables/useRouteParameters.ts' +import { useFileListWidth } from '../composables/useFileListWidth.ts' import { useActionsMenuStore } from '../store/actionsmenu.ts' import { useFilesStore } from '../store/files.ts' import { useSelectionStore } from '../store/selection.ts' -import filesListWidthMixin from '../mixins/filesListWidth.ts' import logger from '../logger.ts' // The registered actions list @@ -62,10 +62,6 @@ export default defineComponent({ NcLoadingIcon, }, - mixins: [ - filesListWidthMixin, - ], - props: { currentView: { type: Object as PropType<View>, @@ -81,10 +77,12 @@ export default defineComponent({ const actionsMenuStore = useActionsMenuStore() const filesStore = useFilesStore() const selectionStore = useSelectionStore() + const fileListWidth = useFileListWidth() const { directory } = useRouteParameters() return { directory, + fileListWidth, actionsMenuStore, filesStore, @@ -126,13 +124,13 @@ export default defineComponent({ }, inlineActions() { - if (this.filesListWidth < 512) { + if (this.fileListWidth < 512) { return 0 } - if (this.filesListWidth < 768) { + if (this.fileListWidth < 768) { return 1 } - if (this.filesListWidth < 1024) { + if (this.fileListWidth < 1024) { return 2 } return 3 diff --git a/apps/files/src/components/FilesListVirtual.vue b/apps/files/src/components/FilesListVirtual.vue index d4c3d9495b7..52ba69d8b97 100644 --- a/apps/files/src/components/FilesListVirtual.vue +++ b/apps/files/src/components/FilesListVirtual.vue @@ -12,7 +12,7 @@ isMtimeAvailable, isSizeAvailable, nodes, - filesListWidth, + fileListWidth, }" :scroll-to-index="scrollToIndex" :caption="caption"> @@ -39,7 +39,7 @@ <template #header> <!-- Table header and sort buttons --> <FilesListTableHeader ref="thead" - :files-list-width="filesListWidth" + :files-list-width="fileListWidth" :is-mtime-available="isMtimeAvailable" :is-size-available="isSizeAvailable" :nodes="nodes" /> @@ -48,7 +48,7 @@ <!-- Tfoot--> <template #footer> <FilesListTableFooter :current-view="currentView" - :files-list-width="filesListWidth" + :files-list-width="fileListWidth" :is-mtime-available="isMtimeAvailable" :is-size-available="isSizeAvailable" :nodes="nodes" @@ -69,6 +69,7 @@ import { subscribe, unsubscribe } from '@nextcloud/event-bus' import { defineComponent } from 'vue' import { action as sidebarAction } from '../actions/sidebarAction.ts' +import { useFileListWidth } from '../composables/useFileListWidth.ts' import { useRouteParameters } from '../composables/useRouteParameters.ts' import { getSummaryFor } from '../utils/fileUtils' import { useSelectionStore } from '../store/selection.js' @@ -79,7 +80,6 @@ import FileEntryGrid from './FileEntryGrid.vue' import FilesListHeader from './FilesListHeader.vue' import FilesListTableFooter from './FilesListTableFooter.vue' import FilesListTableHeader from './FilesListTableHeader.vue' -import filesListWidthMixin from '../mixins/filesListWidth.ts' import VirtualList from './VirtualList.vue' import logger from '../logger.ts' import FilesListTableHeaderActions from './FilesListTableHeaderActions.vue' @@ -97,10 +97,6 @@ export default defineComponent({ FilesListTableHeaderActions, }, - mixins: [ - filesListWidthMixin, - ], - props: { currentView: { type: View, @@ -119,10 +115,12 @@ export default defineComponent({ setup() { const userConfigStore = useUserConfigStore() const selectionStore = useSelectionStore() + const fileListWidth = useFileListWidth() const { fileId, openFile } = useRouteParameters() return { fileId, + fileListWidth, openFile, userConfigStore, @@ -151,14 +149,14 @@ export default defineComponent({ isMtimeAvailable() { // Hide mtime column on narrow screens - if (this.filesListWidth < 768) { + if (this.fileListWidth < 768) { return false } return this.nodes.some(node => node.mtime !== undefined) }, isSizeAvailable() { // Hide size column on narrow screens - if (this.filesListWidth < 768) { + if (this.fileListWidth < 768) { return false } return this.nodes.some(node => node.size !== undefined) diff --git a/apps/files/src/components/VirtualList.vue b/apps/files/src/components/VirtualList.vue index 4c047a76a4e..d2b436344a5 100644 --- a/apps/files/src/components/VirtualList.vue +++ b/apps/files/src/components/VirtualList.vue @@ -55,10 +55,9 @@ import type { File, Folder, Node } from '@nextcloud/files' import type { PropType } from 'vue' +import { useFileListWidth } from '../composables/useFileListWidth.ts' +import { defineComponent } from 'vue' import debounce from 'debounce' -import Vue from 'vue' - -import filesListWidthMixin from '../mixins/filesListWidth.ts' import logger from '../logger.ts' interface RecycledPoolItem { @@ -70,11 +69,9 @@ type DataSource = File | Folder type DataSourceKey = keyof DataSource -export default Vue.extend({ +export default defineComponent({ name: 'VirtualList', - mixins: [filesListWidthMixin], - props: { dataComponent: { type: [Object, Function], @@ -101,7 +98,7 @@ export default Vue.extend({ default: false, }, /** - * Visually hidden caption for the table accesibility + * Visually hidden caption for the table accessibility */ caption: { type: String, @@ -109,6 +106,14 @@ export default Vue.extend({ }, }, + setup() { + const fileListWidth = useFileListWidth() + + return { + fileListWidth, + } + }, + data() { return { index: this.scrollToIndex, @@ -151,7 +156,7 @@ export default Vue.extend({ if (!this.gridMode) { return 1 } - return Math.floor(this.filesListWidth / this.itemWidth) + return Math.floor(this.fileListWidth / this.itemWidth) }, /** diff --git a/apps/files/src/composables/useFileListWidth.cy.ts b/apps/files/src/composables/useFileListWidth.cy.ts new file mode 100644 index 00000000000..b0d42c4a2d6 --- /dev/null +++ b/apps/files/src/composables/useFileListWidth.cy.ts @@ -0,0 +1,56 @@ +/*! + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { defineComponent } from 'vue' +import { useFileListWidth } from './useFileListWidth.ts' + +const ComponentMock = defineComponent({ + template: '<div id="test-component" style="width: 100%;background: white;">{{ fileListWidth }}</div>', + setup() { + return { + fileListWidth: useFileListWidth(), + } + }, +}) +const FileListMock = defineComponent({ + template: '<main id="app-content-vue" style="width: 100%;"><component-mock /></main>', + components: { + ComponentMock, + }, +}) + +describe('composable: fileListWidth', () => { + + it('Has initial value', () => { + cy.viewport(600, 400) + + cy.mount(FileListMock, {}) + cy.get('#app-content-vue') + .should('be.visible') + .and('contain.text', '600') + }) + + it('Is reactive to size change', () => { + cy.viewport(600, 400) + cy.mount(FileListMock) + cy.get('#app-content-vue').should('contain.text', '600') + + cy.viewport(800, 400) + cy.screenshot() + cy.get('#app-content-vue').should('contain.text', '800') + }) + + it('Is reactive to style changes', () => { + cy.viewport(600, 400) + cy.mount(FileListMock) + cy.get('#app-content-vue') + .should('be.visible') + .and('contain.text', '600') + .invoke('attr', 'style', 'width: 100px') + + cy.get('#app-content-vue') + .should('contain.text', '100') + }) +}) diff --git a/apps/files/src/composables/useFileListWidth.ts b/apps/files/src/composables/useFileListWidth.ts new file mode 100644 index 00000000000..621ef204836 --- /dev/null +++ b/apps/files/src/composables/useFileListWidth.ts @@ -0,0 +1,50 @@ +/*! + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import type { Ref } from 'vue' +import { onMounted, readonly, ref } from 'vue' + +/** The element we observe */ +let element: HTMLElement | undefined + +/** The current width of the element */ +const width = ref(0) + +const observer = new ResizeObserver((elements) => { + if (elements[0].contentBoxSize) { + // use the newer `contentBoxSize` property if available + width.value = elements[0].contentBoxSize[0].inlineSize + } else { + // fall back to `contentRect` + width.value = elements[0].contentRect.width + } +}) + +/** + * Update the observed element if needed and reconfigure the observer + */ +function updateObserver() { + const el = document.querySelector<HTMLElement>('#app-content-vue') ?? document.body + if (el !== element) { + // if already observing: stop observing the old element + if (element) { + observer.unobserve(element) + } + // observe the new element if needed + observer.observe(el) + element = el + } +} + +/** + * Get the reactive width of the file list + */ +export function useFileListWidth(): Readonly<Ref<number>> { + // Update the observer when the component is mounted (e.g. because this is the files app) + onMounted(updateObserver) + // Update the observer also in setup context, so we already have an initial value + updateObserver() + + return readonly(width) +} diff --git a/apps/files/src/mixins/filesListWidth.ts b/apps/files/src/mixins/filesListWidth.ts deleted file mode 100644 index 7d7ec598673..00000000000 --- a/apps/files/src/mixins/filesListWidth.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { defineComponent } from 'vue' - -export default defineComponent({ - data() { - return { - filesListWidth: 0, - } - }, - - mounted() { - const fileListEl = document.querySelector('#app-content-vue') - this.filesListWidth = fileListEl?.clientWidth ?? 0 - - // @ts-expect-error The resize observer is just now attached to the object - this.$resizeObserver = new ResizeObserver((entries) => { - if (entries.length > 0 && entries[0].target === fileListEl) { - this.filesListWidth = entries[0].contentRect.width - } - }) - // @ts-expect-error The resize observer was attached right before to the this object - this.$resizeObserver.observe(fileListEl as Element) - }, - - beforeDestroy() { - // @ts-expect-error mounted must have been called before the destroy, so the resize - this.$resizeObserver.disconnect() - }, -}) diff --git a/apps/files/src/views/FilesList.vue b/apps/files/src/views/FilesList.vue index 56907db3feb..6cbaecfa023 100644 --- a/apps/files/src/views/FilesList.vue +++ b/apps/files/src/views/FilesList.vue @@ -9,7 +9,7 @@ <BreadCrumbs :path="directory" @reload="fetchContent"> <template #actions> <!-- Sharing button --> - <NcButton v-if="canShare && filesListWidth >= 512" + <NcButton v-if="canShare && fileListWidth >= 512" :aria-label="shareButtonLabel" :class="{ 'files-list__header-share-button--shared': shareButtonType }" :title="shareButtonLabel" @@ -63,7 +63,7 @@ <!-- Secondary loading indicator --> <NcLoadingIcon v-if="isRefreshing" class="files-list__refresh-icon" /> - <NcButton v-if="filesListWidth >= 512 && enableGridView" + <NcButton v-if="fileListWidth >= 512 && enableGridView" :aria-label="gridViewButtonLabel" :title="gridViewButtonLabel" class="files-list__header-grid-button" @@ -176,6 +176,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 { useFileListWidth } from '../composables/useFileListWidth.ts' import { useRouteParameters } from '../composables/useRouteParameters.ts' import { useFilesStore } from '../store/files.ts' import { useFiltersStore } from '../store/filters.ts' @@ -186,7 +187,6 @@ import { useUserConfigStore } from '../store/userconfig.ts' import { useViewConfigStore } from '../store/viewConfig.ts' import BreadCrumbs from '../components/BreadCrumbs.vue' import FilesListVirtual from '../components/FilesListVirtual.vue' -import filesListWidthMixin from '../mixins/filesListWidth.ts' import filesSortingMixin from '../mixins/filesSorting.ts' import logger from '../logger.ts' import DragAndDropNotice from '../components/DragAndDropNotice.vue' @@ -219,7 +219,6 @@ export default defineComponent({ }, mixins: [ - filesListWidthMixin, filesSortingMixin, ], @@ -239,6 +238,7 @@ export default defineComponent({ const userConfigStore = useUserConfigStore() const viewConfigStore = useViewConfigStore() const { currentView } = useNavigation() + const fileListWidth = useFileListWidth() const { directory, fileId } = useRouteParameters() const enableGridView = (loadState('core', 'config', [])['enable_non-accessible_features'] ?? true) @@ -248,6 +248,7 @@ export default defineComponent({ currentView, directory, fileId, + fileListWidth, t, filesStore, |