aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files/src
diff options
context:
space:
mode:
authorChristopher Ng <chrng8@gmail.com>2024-08-07 20:52:29 -0700
committerChristopher Ng <chrng8@gmail.com>2024-08-08 11:15:52 -0700
commit44bc57bc57d9129ab15041b631aca5d48c52124a (patch)
treec979d2744c3271cd8579b1e10ea91459311a699c /apps/files/src
parentcfec6fcb1a9eaa57bef5bba757b5b9dbda387c96 (diff)
downloadnextcloud-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.vue14
-rw-r--r--apps/files/src/composables/useNavigation.ts2
-rw-r--r--apps/files/src/services/FolderTree.ts46
-rw-r--r--apps/files/src/views/Navigation.vue24
-rw-r--r--apps/files/src/views/folderTree.ts48
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 () => {