aboutsummaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
authorFerdinand Thiessen <opensource@fthiessen.de>2024-11-15 01:51:28 +0100
committerFerdinand Thiessen <opensource@fthiessen.de>2024-11-20 19:08:21 +0100
commit675e7a953f15e22578da5e37054fe6a2f34f7de5 (patch)
tree0918e1438c9c9e34fd784ffdfe21a1932a958865 /apps
parent3822db51742eb12c67b525cab80ec0699e011684 (diff)
downloadnextcloud-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.vue16
-rw-r--r--apps/files/src/components/FileEntry.vue5
-rw-r--r--apps/files/src/components/FileEntry/FileEntryActions.vue8
-rw-r--r--apps/files/src/components/FileEntry/FileEntryName.vue7
-rw-r--r--apps/files/src/components/FileEntryGrid.vue2
-rw-r--r--apps/files/src/components/FilesListTableHeaderActions.vue14
-rw-r--r--apps/files/src/components/FilesListVirtual.vue18
-rw-r--r--apps/files/src/components/VirtualList.vue21
-rw-r--r--apps/files/src/composables/useFileListWidth.cy.ts56
-rw-r--r--apps/files/src/composables/useFileListWidth.ts50
-rw-r--r--apps/files/src/mixins/filesListWidth.ts33
-rw-r--r--apps/files/src/views/FilesList.vue9
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,