diff options
author | Christopher Ng <chrng8@gmail.com> | 2024-08-07 20:52:29 -0700 |
---|---|---|
committer | Christopher Ng <chrng8@gmail.com> | 2024-08-08 11:15:52 -0700 |
commit | 44bc57bc57d9129ab15041b631aca5d48c52124a (patch) | |
tree | c979d2744c3271cd8579b1e10ea91459311a699c /apps/files/src | |
parent | cfec6fcb1a9eaa57bef5bba757b5b9dbda387c96 (diff) | |
download | nextcloud-server-44bc57bc57d9129ab15041b631aca5d48c52124a.tar.gz nextcloud-server-44bc57bc57d9129ab15041b631aca5d48c52124a.zip |
feat: Load limited depth tree
Signed-off-by: Christopher Ng <chrng8@gmail.com>
Diffstat (limited to 'apps/files/src')
-rw-r--r-- | apps/files/src/components/FilesNavigationItem.vue | 14 | ||||
-rw-r--r-- | apps/files/src/composables/useNavigation.ts | 2 | ||||
-rw-r--r-- | apps/files/src/services/FolderTree.ts | 46 | ||||
-rw-r--r-- | apps/files/src/views/Navigation.vue | 24 | ||||
-rw-r--r-- | apps/files/src/views/folderTree.ts | 48 |
5 files changed, 101 insertions, 33 deletions
diff --git a/apps/files/src/components/FilesNavigationItem.vue b/apps/files/src/components/FilesNavigationItem.vue index 75507803957..f05ad389f50 100644 --- a/apps/files/src/components/FilesNavigationItem.vue +++ b/apps/files/src/components/FilesNavigationItem.vue @@ -9,6 +9,7 @@ :key="view.id" class="files-navigation__item" allow-collapse + :loading="view.loading" :data-cy-files-navigation-item="view.id" :exact="useExactRouteMatching(view)" :icon="view.iconClass" @@ -17,11 +18,14 @@ :pinned="view.sticky" :to="generateToNavigation(view)" :style="style" - @update:open="onToggleExpand(view)"> + @update:open="(open) => onOpen(open, view)"> <template v-if="view.icon" #icon> <NcIconSvgWrapper :svg="view.icon" /> </template> + <!-- Hack to force the collapse icon to be displayed --> + <li v-if="view.loadChildViews && !view.loaded" style="display: none" /> + <!-- Recursively nest child views --> <FilesNavigationItem v-if="hasChildViews(view)" :parent="view" @@ -142,14 +146,18 @@ export default defineComponent({ /** * Expand/collapse a a view with children and permanently * save this setting in the server. - * @param view View to toggle + * @param open True if open + * @param view View */ - onToggleExpand(view: View) { + async onOpen(open: boolean, view: View) { // Invert state const isExpanded = this.isExpanded(view) // Update the view expanded state, might not be necessary view.expanded = !isExpanded this.viewConfigStore.update(view.id, 'expanded', !isExpanded) + if (open && view.loadChildViews) { + await view.loadChildViews(view) + } }, /** diff --git a/apps/files/src/composables/useNavigation.ts b/apps/files/src/composables/useNavigation.ts index 2fff5633e23..714b3a6d7b2 100644 --- a/apps/files/src/composables/useNavigation.ts +++ b/apps/files/src/composables/useNavigation.ts @@ -6,6 +6,7 @@ import type { View } from '@nextcloud/files' import type { ShallowRef } from 'vue' import { getNavigation } from '@nextcloud/files' +import { subscribe } from '@nextcloud/event-bus' import { onMounted, onUnmounted, shallowRef, triggerRef } from 'vue' /** @@ -35,6 +36,7 @@ export function useNavigation() { onMounted(() => { navigation.addEventListener('update', onUpdateViews) navigation.addEventListener('updateActive', onUpdateActive) + subscribe('files:navigation:updated', onUpdateViews) }) onUnmounted(() => { navigation.removeEventListener('update', onUpdateViews) diff --git a/apps/files/src/services/FolderTree.ts b/apps/files/src/services/FolderTree.ts index 87c3aaa7db7..2422b2bb3b9 100644 --- a/apps/files/src/services/FolderTree.ts +++ b/apps/files/src/services/FolderTree.ts @@ -13,23 +13,17 @@ import { import axios from '@nextcloud/axios' import { generateOcsUrl } from '@nextcloud/router' import { getCurrentUser } from '@nextcloud/auth' -import { dirname, encodePath } from '@nextcloud/paths' +import { dirname, encodePath, joinPaths } from '@nextcloud/paths' import { getContents as getFiles } from './Files.ts' -export const folderTreeId = 'folders' -export const sourceRoot = `${davRemoteURL}/files/${getCurrentUser()?.uid}` - -interface TreeNodeData { +// eslint-disable-next-line no-use-before-define +type Tree = Array<{ id: number, + basename: string, displayName?: string, - // eslint-disable-next-line no-use-before-define - children?: Tree, -} - -interface Tree { - [basename: string]: TreeNodeData, -} + children: Tree, +}> export interface TreeNode { source: string, @@ -39,27 +33,35 @@ export interface TreeNode { displayName?: string, } -const getTreeNodes = (tree: Tree, nodes: TreeNode[] = [], currentPath: string = ''): TreeNode[] => { - for (const basename in tree) { - const path = `${currentPath}/${basename}` +export const folderTreeId = 'folders' + +export const sourceRoot = `${davRemoteURL}/files/${getCurrentUser()?.uid}` + +const getTreeNodes = (tree: Tree, currentPath: string = '/', nodes: TreeNode[] = []): TreeNode[] => { + for (const { id, basename, displayName, children } of tree) { + const path = joinPaths(currentPath, basename) const node: TreeNode = { source: `${sourceRoot}${path}`, path, - fileid: tree[basename].id, + fileid: id, basename, - displayName: tree[basename].displayName, + } + if (displayName) { + node.displayName = displayName } nodes.push(node) - if (tree[basename].children) { - getTreeNodes(tree[basename].children, nodes, path) + if (children.length > 0) { + getTreeNodes(children, path, nodes) } } return nodes } -export const getFolderTreeNodes = async (): Promise<TreeNode[]> => { - const { data: tree } = await axios.get<Tree>(generateOcsUrl('/apps/files/api/v1/folder-tree')) - const nodes = getTreeNodes(tree) +export const getFolderTreeNodes = async (path: string = '/', depth: number = 1): Promise<TreeNode[]> => { + const { data: tree } = await axios.get<Tree>(generateOcsUrl('/apps/files/api/v1/folder-tree'), { + params: new URLSearchParams({ path, depth: String(depth) }), + }) + const nodes = getTreeNodes(tree, path) return nodes } diff --git a/apps/files/src/views/Navigation.vue b/apps/files/src/views/Navigation.vue index cca38824d2c..7d617879c6a 100644 --- a/apps/files/src/views/Navigation.vue +++ b/apps/files/src/views/Navigation.vue @@ -39,10 +39,11 @@ <script lang="ts"> import type { View } from '@nextcloud/files' +import type { ViewConfig } from '../types.ts' -import { emit } from '@nextcloud/event-bus' -import { translate as t, getCanonicalLocale, getLanguage } from '@nextcloud/l10n' import { defineComponent } from 'vue' +import { emit, subscribe } from '@nextcloud/event-bus' +import { translate as t, getCanonicalLocale, getLanguage } from '@nextcloud/l10n' import IconCog from 'vue-material-design-icons/Cog.vue' import NcAppNavigation from '@nextcloud/vue/dist/Components/NcAppNavigation.js' @@ -144,6 +145,11 @@ export default defineComponent({ }, }, + created() { + subscribe('files:folder-tree:initialized', this.loadExpandedViews) + subscribe('files:folder-tree:expanded', this.loadExpandedViews) + }, + beforeMount() { // This is guaranteed to be a view because `currentViewId` falls back to the default 'files' view const view = this.views.find(({ id }) => id === this.currentViewId)! @@ -152,6 +158,20 @@ export default defineComponent({ }, methods: { + async loadExpandedViews() { + const viewConfigs = this.viewConfigStore.getConfigs() + const viewsToLoad: View[] = (Object.entries(viewConfigs) as Array<[string, ViewConfig]>) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .filter(([viewId, config]) => config.expanded === true) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .map(([viewId, config]) => this.$navigation.views.find(view => view.id === viewId)) + .filter(Boolean) // Only registered views + .filter(view => view.loadChildViews && !view.loaded) + for (const view of viewsToLoad) { + await view.loadChildViews(view) + } + }, + /** * Set the view as active on the navigation and handle internal state * @param view View to set active diff --git a/apps/files/src/views/folderTree.ts b/apps/files/src/views/folderTree.ts index 0e6c00937ce..4ae07c0d79b 100644 --- a/apps/files/src/views/folderTree.ts +++ b/apps/files/src/views/folderTree.ts @@ -5,9 +5,10 @@ import type { TreeNode } from '../services/FolderTree.ts' +import PQueue from 'p-queue' import { Folder, Node, View, getNavigation } from '@nextcloud/files' import { translate as t } from '@nextcloud/l10n' -import { subscribe } from '@nextcloud/event-bus' +import { emit, subscribe } from '@nextcloud/event-bus' import { isSamePath } from '@nextcloud/paths' import { loadState } from '@nextcloud/initial-state' @@ -29,6 +30,41 @@ const isFolderTreeEnabled = loadState('files', 'config', { folder_tree: true }). const Navigation = getNavigation() +const queue = new PQueue({ concurrency: 5, intervalCap: 5, interval: 200 }) +const registerQueue = new PQueue({ concurrency: 5, intervalCap: 5, interval: 200 }) + +const registerTreeNodes = async (path: string = '/') => { + await queue.add(async () => { + const nodes = await getFolderTreeNodes(path) + const promises = nodes.map(node => registerQueue.add(() => registerTreeNodeView(node))) + await Promise.allSettled(promises) + }) +} + +const getLoadChildViews = (node: TreeNode | Folder) => { + return async (view: View): Promise<void> => { + // @ts-expect-error Custom property on View instance + if (view.loaded) { + return + } + // @ts-expect-error Custom property + view.loading = true + try { + await registerTreeNodes(node.path) + } catch (error) { + // Skip duplicate view registration errors + } + // @ts-expect-error Custom property + view.loading = false + // @ts-expect-error Custom property + view.loaded = true + // @ts-expect-error No payload + emit('files:navigation:updated') + // @ts-expect-error No payload + emit('files:folder-tree:expanded') + } +} + const registerTreeNodeView = (node: TreeNode) => { Navigation.register(new View({ id: encodeSource(node.source), @@ -40,6 +76,7 @@ const registerTreeNodeView = (node: TreeNode) => { order: 0, // TODO Allow undefined order for natural sort getContents, + loadChildViews: getLoadChildViews(node), params: { view: folderTreeId, @@ -60,6 +97,7 @@ const registerFolderView = (folder: Folder) => { order: 0, // TODO Allow undefined order for natural sort getContents, + loadChildViews: getLoadChildViews(folder), params: { view: folderTreeId, @@ -133,14 +171,12 @@ const registerFolderTreeRoot = () => { } const registerFolderTreeChildren = async () => { - const nodes = await getFolderTreeNodes() - for (const node of nodes) { - registerTreeNodeView(node) - } - + await registerTreeNodes() subscribe('files:node:created', onCreateNode) subscribe('files:node:deleted', onDeleteNode) subscribe('files:node:moved', onMoveNode) + // @ts-expect-error No payload + emit('files:folder-tree:initialized') } export const registerFolderTreeView = async () => { |