aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorskjnldsv <skjnldsv@protonmail.com>2025-07-04 16:11:25 +0200
committerskjnldsv <skjnldsv@protonmail.com>2025-07-08 09:13:50 +0200
commit92f034ecd5c6f4507f5b541b8a666f911519b8ab (patch)
tree2c257ae619336801a5df1b44c43d5a78ccf2cbdc
parente5572abb512f343fef85c3fd79112de14c21ee3b (diff)
downloadnextcloud-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.vue8
-rw-r--r--apps/files/src/components/FilesListVirtual.vue17
-rw-r--r--apps/files/src/components/VirtualList.vue11
-rw-r--r--apps/files/src/views/FilesList.vue161
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()