]> source.dussan.org Git - nextcloud-server.git/commitdiff
refactor(files): Provide `useFileListWidth` composable
authorFerdinand Thiessen <opensource@fthiessen.de>
Fri, 15 Nov 2024 00:51:28 +0000 (01:51 +0100)
committerFerdinand Thiessen <opensource@fthiessen.de>
Wed, 20 Nov 2024 18:08:21 +0000 (19:08 +0100)
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>
12 files changed:
apps/files/src/components/BreadCrumbs.vue
apps/files/src/components/FileEntry.vue
apps/files/src/components/FileEntry/FileEntryActions.vue
apps/files/src/components/FileEntry/FileEntryName.vue
apps/files/src/components/FileEntryGrid.vue
apps/files/src/components/FilesListTableHeaderActions.vue
apps/files/src/components/FilesListVirtual.vue
apps/files/src/components/VirtualList.vue
apps/files/src/composables/useFileListWidth.cy.ts [new file with mode: 0644]
apps/files/src/composables/useFileListWidth.ts [new file with mode: 0644]
apps/files/src/mixins/filesListWidth.ts [deleted file]
apps/files/src/views/FilesList.vue

index c423b698d40d87f55ae57c73d8eceea13a6c1fe0..3569228dbde0fc5bcba3402d9ece239985cecc89 100644 (file)
@@ -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
index fc5c7fb97f62516d14ed4248e9d37166037e9b85..7af76c87c43182f8fa7fcf5132700f07da9f394b 100644 (file)
@@ -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,
                }
        },
 
index 8c150b780876b4d02c81f1b780d3620b5a37a115..f8fde7842a879998059f7b7c2f7a6cb8f8113e8b 100644 (file)
@@ -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,
                }
        },
 
index e4cffba32b79d920a67f0faec693198400a5388c..1eff841738b546994d65a2994f6302d83dcea0b9 100644 (file)
@@ -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,
                }
index f0b086ac891d8868da4947a260530910975867a4..0b0344afb99aeb5a48226a9a4d3c34e0dbd3ef86 100644 (file)
@@ -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"
index fa5f7d4bd5f63bdc4364ba38e3044e2d118f4052..9f5724dc80f94538df6422d680146a98b03b562b 100644 (file)
@@ -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
index d4c3d9495b7173d57b031ce148eeba3c186fd246..52ba69d8b9794a6026d4bb8a67b66a274b90b859 100644 (file)
@@ -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)
index 4c047a76a4e47747a8b066a8818186a2e0506ec5..d2b436344a5cf72146828fce2c0cadd480e42d22 100644 (file)
 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 (file)
index 0000000..b0d42c4
--- /dev/null
@@ -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 (file)
index 0000000..621ef20
--- /dev/null
@@ -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 (file)
index 7d7ec59..0000000
+++ /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()
-       },
-})
index 56907db3febd1652f156132cba26ffbf00eb29f6..6cbaecfa023026fce9ed1cc2b01ddad827dfffd7 100644 (file)
@@ -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,