diff options
author | skjnldsv <skjnldsv@protonmail.com> | 2025-07-04 16:11:25 +0200 |
---|---|---|
committer | skjnldsv <skjnldsv@protonmail.com> | 2025-07-08 09:13:50 +0200 |
commit | 92f034ecd5c6f4507f5b541b8a666f911519b8ab (patch) | |
tree | 2c257ae619336801a5df1b44c43d5a78ccf2cbdc | |
parent | e5572abb512f343fef85c3fd79112de14c21ee3b (diff) | |
download | nextcloud-server-92f034ecd5c6f4507f5b541b8a666f911519b8ab.tar.gz nextcloud-server-92f034ecd5c6f4507f5b541b8a666f911519b8ab.zip |
fix(files): make sure the FilesList is always mounted
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
-rw-r--r-- | apps/files/src/components/FilesListHeader.vue | 8 | ||||
-rw-r--r-- | apps/files/src/components/FilesListVirtual.vue | 17 | ||||
-rw-r--r-- | apps/files/src/components/VirtualList.vue | 11 | ||||
-rw-r--r-- | apps/files/src/views/FilesList.vue | 161 |
4 files changed, 126 insertions, 71 deletions
diff --git a/apps/files/src/components/FilesListHeader.vue b/apps/files/src/components/FilesListHeader.vue index cc8dafe344e..c5d8ef0c326 100644 --- a/apps/files/src/components/FilesListHeader.vue +++ b/apps/files/src/components/FilesListHeader.vue @@ -12,6 +12,8 @@ import type { Folder, Header, View } from '@nextcloud/files' import type { PropType } from 'vue' +import logger from '../logger.ts' + /** * This component is used to render custom * elements provided by an API. Vue doesn't allow @@ -51,8 +53,12 @@ export default { }, }, mounted() { - console.debug('Mounted', this.header.id) + logger.debug(`Mounted ${this.header.id} FilesListHeader`, { header: this.header }) this.header.render(this.$refs.mount as HTMLElement, this.currentFolder, this.currentView) }, + + destroyed() { + logger.debug(`Destroyed ${this.header.id} FilesListHeader`, { header: this.header }) + }, } </script> diff --git a/apps/files/src/components/FilesListVirtual.vue b/apps/files/src/components/FilesListVirtual.vue index 7ed774f6020..156bc5fe161 100644 --- a/apps/files/src/components/FilesListVirtual.vue +++ b/apps/files/src/components/FilesListVirtual.vue @@ -48,6 +48,11 @@ :nodes="nodes" /> </template> + <!-- Body replacement if no files are available --> + <template #empty> + <slot name="empty" /> + </template> + <!-- Tfoot--> <template #footer> <FilesListTableFooter :current-view="currentView" @@ -474,6 +479,8 @@ export default defineComponent({ --icon-preview-size: 32px; --fixed-block-start-position: var(--default-clickable-area); + display: flex; + flex-direction: column; overflow: auto; height: 100%; will-change: scroll-position; @@ -570,6 +577,16 @@ export default defineComponent({ top: var(--fixed-block-start-position); } + // Empty content + .files-list__empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + } + tr { position: relative; display: flex; diff --git a/apps/files/src/components/VirtualList.vue b/apps/files/src/components/VirtualList.vue index 5ae8220d594..d9000a38073 100644 --- a/apps/files/src/components/VirtualList.vue +++ b/apps/files/src/components/VirtualList.vue @@ -20,7 +20,16 @@ <slot name="header-overlay" /> </div> - <table class="files-list__table" :class="{ 'files-list__table--with-thead-overlay': !!$scopedSlots['header-overlay'] }"> + <div v-if="dataSources.length === 0" + class="files-list__empty"> + <slot name="empty" /> + </div> + + <table v-show="dataSources.length > 0" + :aria-hidden="dataSources.length === 0" + :inert="dataSources.length === 0" + class="files-list__table" + :class="{ 'files-list__table--with-thead-overlay': !!$scopedSlots['header-overlay'] }"> <!-- Accessibility table caption for screen readers --> <caption v-if="caption" class="hidden-visually"> {{ caption }} diff --git a/apps/files/src/views/FilesList.vue b/apps/files/src/views/FilesList.vue index fcfa14cdd21..1b52bb61e68 100644 --- a/apps/files/src/views/FilesList.vue +++ b/apps/files/src/views/FilesList.vue @@ -73,87 +73,92 @@ <!-- Drag and drop notice --> <DragAndDropNotice v-if="!loading && canUpload && currentFolder" :current-folder="currentFolder" /> - <!-- Initial loading --> - <NcLoadingIcon v-if="loading && !isRefreshing" + <!-- + Initial current view loading0. This should never happen, + views are supposed to be registered far earlier in the lifecycle. + In case the URL is bad or a view is missing, we show a loading icon. + --> + <NcLoadingIcon v-if="!currentView" class="files-list__loading-icon" :size="38" :name="t('files', 'Loading current folder')" /> - <!-- Empty content placeholder --> - <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> - <NcButton type="secondary" @click="fetchContent"> - <template #icon> - <IconReload :size="20" /> - </template> - {{ t('files', 'Retry') }} - </NcButton> - </template> - <template #icon> - <IconAlertCircleOutline /> - </template> - </NcEmptyContent> - <!-- Custom empty view --> - <div v-else-if="currentView?.emptyView" class="files-list__empty-view-wrapper"> - <div ref="customEmptyView" /> - </div> - <!-- Default empty directory view --> - <NcEmptyContent v-else - :name="currentView?.emptyTitle || t('files', 'No files in here')" - :description="currentView?.emptyCaption || t('files', 'Upload some content or sync with your devices!')" - data-cy-files-content-empty> - <template v-if="directory !== '/'" #action> - <!-- Uploader --> - <UploadPicker v-if="canUpload && !isQuotaExceeded" - allow-folders - class="files-list__header-upload-button" - :content="getContent" - :destination="currentFolder" - :forbidden-characters="forbiddenCharacters" - multiple - @failed="onUploadFail" - @uploaded="onUpload" /> - <NcButton v-else :to="toPreviousDir" type="primary"> - {{ t('files', 'Go back') }} - </NcButton> - </template> - <template #icon> - <NcIconSvgWrapper :svg="currentView.icon" /> - </template> - </NcEmptyContent> - </template> - - <!-- File list --> + <!-- File list - always mounted --> <FilesListVirtual v-else ref="filesListVirtual" :current-folder="currentFolder" :current-view="currentView" :nodes="dirContentsSorted" - :summary="summary" /> + :summary="summary"> + <template #empty> + <!-- Initial loading --> + <NcLoadingIcon v-if="loading && !isRefreshing" + class="files-list__loading-icon" + :size="38" + :name="t('files', 'Loading current folder')" /> + + <!-- Empty due to error --> + <NcEmptyContent v-else-if="error" :name="error" data-cy-files-content-error> + <template #action> + <NcButton type="secondary" @click="fetchContent"> + <template #icon> + <IconReload :size="20" /> + </template> + {{ t('files', 'Retry') }} + </NcButton> + </template> + <template #icon> + <IconAlertCircleOutline /> + </template> + </NcEmptyContent> + + <!-- Custom empty view --> + <div v-else-if="currentView?.emptyView" class="files-list__empty-view-wrapper"> + <div ref="customEmptyView" /> + </div> + + <!-- Default empty directory view --> + <NcEmptyContent v-else + :name="currentView?.emptyTitle || t('files', 'No files in here')" + :description="currentView?.emptyCaption || t('files', 'Upload some content or sync with your devices!')" + data-cy-files-content-empty> + <template v-if="directory !== '/'" #action> + <!-- Uploader --> + <UploadPicker v-if="canUpload && !isQuotaExceeded" + allow-folders + class="files-list__header-upload-button" + :content="getContent" + :destination="currentFolder" + :forbidden-characters="forbiddenCharacters" + multiple + @failed="onUploadFail" + @uploaded="onUpload" /> + <NcButton v-else :to="toPreviousDir" type="primary"> + {{ t('files', 'Go back') }} + </NcButton> + </template> + <template #icon> + <NcIconSvgWrapper :svg="currentView?.icon" /> + </template> + </NcEmptyContent> + </template> + </FilesListVirtual> </NcAppContent> </template> <script lang="ts"> -import type { ContentsWithRoot, FileListAction, Folder, INode } from '@nextcloud/files' +import type { ContentsWithRoot, FileListAction, INode } from '@nextcloud/files' import type { Upload } from '@nextcloud/upload' import type { CancelablePromise } from 'cancelable-promise' import type { ComponentPublicInstance } from 'vue' import type { Route } from 'vue-router' import type { UserConfig } from '../types.ts' +import { getCurrentUser } from '@nextcloud/auth' import { getCapabilities } from '@nextcloud/capabilities' import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus' -import { Node, Permission, sortNodes, getFileListActions } from '@nextcloud/files' +import { Folder, Node, Permission, sortNodes, getFileListActions } from '@nextcloud/files' +import { getRemoteURL, getRootPath } from '@nextcloud/files/dav' import { translate as t } from '@nextcloud/l10n' import { join, dirname, normalize, relative } from 'path' import { showError, showSuccess, showWarning } from '@nextcloud/dialogs' @@ -181,7 +186,6 @@ import ViewGridIcon from 'vue-material-design-icons/ViewGrid.vue' import { action as sidebarAction } from '../actions/sidebarAction.ts' import { getSummaryFor } from '../utils/fileUtils.ts' import { humanizeWebDAVError } from '../utils/davUtils.ts' -import { useFileListHeaders } from '../composables/useFileListHeaders.ts' import { useFileListWidth } from '../composables/useFileListWidth.ts' import { useFilesStore } from '../store/files.ts' import { useFiltersStore } from '../store/filters.ts' @@ -195,7 +199,6 @@ import { useUserConfigStore } from '../store/userconfig.ts' import { useViewConfigStore } from '../store/viewConfig.ts' import BreadCrumbs from '../components/BreadCrumbs.vue' import DragAndDropNotice from '../components/DragAndDropNotice.vue' -import FilesListHeader from '../components/FilesListHeader.vue' import FilesListVirtual from '../components/FilesListVirtual.vue' import filesSortingMixin from '../mixins/filesSorting.ts' import logger from '../logger.ts' @@ -208,7 +211,6 @@ export default defineComponent({ components: { BreadCrumbs, DragAndDropNotice, - FilesListHeader, FilesListVirtual, LinkIcon, ListViewIcon, @@ -259,7 +261,6 @@ export default defineComponent({ directory, fileId, fileListWidth, - headers: useFileListHeaders(), t, activeStore, @@ -325,12 +326,23 @@ export default defineComponent({ /** * The current folder. */ - currentFolder(): Folder | undefined { - if (!this.currentView) { - return + currentFolder(): Folder { + // Temporary fake folder to use until we have the first valid folder + // fetched and cached. This allow us to mount the FilesListVirtual + // at all time and avoid unmount/mount and undesired rendering issues. + const dummyFolder = new Folder({ + id: 0, + source: getRemoteURL() + getRootPath(), + root: getRootPath(), + owner: getCurrentUser()?.uid || null, + permissions: Permission.NONE, + }) + + if (!this.currentView?.id) { + return dummyFolder } - return this.filesStore.getDirectoryByPath(this.currentView.id, this.directory) + return this.filesStore.getDirectoryByPath(this.currentView.id, this.directory) || dummyFolder }, dirContents(): Node[] { @@ -342,7 +354,7 @@ export default defineComponent({ /** * The current directory contents. */ - dirContentsSorted() { + dirContentsSorted(): INode[] { if (!this.currentView) { return [] } @@ -598,9 +610,20 @@ export default defineComponent({ if (!currentView) { logger.debug('The current view doesn\'t exists or is not ready.', { currentView }) + + // If we still haven't a valid view, let's wait for the page to load + // then try again. Else redirect to the default view + window.addEventListener('DOMContentLoaded', () => { + if (!this.currentView) { + logger.warn('No current view after DOMContentLoaded, redirecting to the default view') + window.OCP.Files.Router.goToRoute(null, { view: 'files' }) + } + }, { once: true }) return } + logger.debug('Fetching contents for directory', { dir, currentView }) + // If we have a cancellable promise ongoing, cancel it if (this.promise && 'cancel' in this.promise) { this.promise.cancel() |