Skip to content

Commit 9803647

Browse files
committed
feat: Load limited depth tree
Signed-off-by: Christopher Ng <chrng8@gmail.com>
1 parent caf3b42 commit 9803647

File tree

5 files changed

+116
-31
lines changed

5 files changed

+116
-31
lines changed

apps/files/src/components/FilesNavigationItem.vue

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
<NcAppNavigationItem v-for="view in currentViews"
99
:key="view.id"
1010
class="files-navigation__item"
11+
:show-collapse="view.onToggleOpen && !view.loaded"
1112
allow-collapse
13+
:loading="view.loading"
1214
:data-cy-files-navigation-item="view.id"
1315
:exact="useExactRouteMatching(view)"
1416
:icon="view.iconClass"
@@ -17,7 +19,7 @@
1719
:pinned="view.sticky"
1820
:to="generateToNavigation(view)"
1921
:style="style"
20-
@update:open="onToggleExpand(view)">
22+
@update:open="(open) => onOpen(open, view)">
2123
<template v-if="view.icon" #icon>
2224
<NcIconSvgWrapper :svg="view.icon" />
2325
</template>
@@ -142,14 +144,19 @@ export default defineComponent({
142144
/**
143145
* Expand/collapse a a view with children and permanently
144146
* save this setting in the server.
145-
* @param view View to toggle
147+
* @param open True if open
148+
* @param view View
146149
*/
147-
onToggleExpand(view: View) {
150+
async onOpen(open: boolean, view: View) {
148151
// Invert state
149152
const isExpanded = this.isExpanded(view)
150153
// Update the view expanded state, might not be necessary
151154
view.expanded = !isExpanded
152155
this.viewConfigStore.update(view.id, 'expanded', !isExpanded)
156+
if (!view.onToggleOpen) {
157+
return
158+
}
159+
await view.onToggleOpen(open, view)
153160
},
154161
155162
/**

apps/files/src/composables/useNavigation.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { View } from '@nextcloud/files'
66
import type { ShallowRef } from 'vue'
77

88
import { getNavigation } from '@nextcloud/files'
9+
import { subscribe } from '@nextcloud/event-bus'
910
import { onMounted, onUnmounted, shallowRef, triggerRef } from 'vue'
1011

1112
/**
@@ -35,6 +36,7 @@ export function useNavigation() {
3536
onMounted(() => {
3637
navigation.addEventListener('update', onUpdateViews)
3738
navigation.addEventListener('updateActive', onUpdateActive)
39+
subscribe('files:navigation:updated', onUpdateViews)
3840
})
3941
onUnmounted(() => {
4042
navigation.removeEventListener('update', onUpdateViews)

apps/files/src/services/FolderTree.ts

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,17 @@ import {
1313
import axios from '@nextcloud/axios'
1414
import { generateOcsUrl } from '@nextcloud/router'
1515
import { getCurrentUser } from '@nextcloud/auth'
16-
import { dirname, encodePath } from '@nextcloud/paths'
16+
import { dirname, encodePath, joinPaths } from '@nextcloud/paths'
1717

1818
import { getContents as getFiles } from './Files.ts'
1919

20-
export const folderTreeId = 'folders'
21-
export const sourceRoot = `${davRemoteURL}/files/${getCurrentUser()?.uid}`
22-
23-
interface TreeNodeData {
20+
// eslint-disable-next-line no-use-before-define
21+
type Tree = Array<{
2422
id: number,
23+
basename: string,
2524
displayName?: string,
26-
// eslint-disable-next-line no-use-before-define
27-
children?: Tree,
28-
}
29-
30-
interface Tree {
31-
[basename: string]: TreeNodeData,
32-
}
25+
children: Tree,
26+
}>
3327

3428
export interface TreeNode {
3529
source: string,
@@ -39,27 +33,35 @@ export interface TreeNode {
3933
displayName?: string,
4034
}
4135

42-
const getTreeNodes = (tree: Tree, nodes: TreeNode[] = [], currentPath: string = ''): TreeNode[] => {
43-
for (const basename in tree) {
44-
const path = `${currentPath}/${basename}`
36+
export const folderTreeId = 'folders'
37+
38+
export const sourceRoot = `${davRemoteURL}/files/${getCurrentUser()?.uid}`
39+
40+
const getTreeNodes = (tree: Tree, currentPath: string = '/', nodes: TreeNode[] = []): TreeNode[] => {
41+
for (const { id, basename, displayName, children } of tree) {
42+
const path = joinPaths(currentPath, basename)
4543
const node: TreeNode = {
4644
source: `${sourceRoot}${path}`,
4745
path,
48-
fileid: tree[basename].id,
46+
fileid: id,
4947
basename,
50-
displayName: tree[basename].displayName,
48+
}
49+
if (displayName) {
50+
node.displayName = displayName
5151
}
5252
nodes.push(node)
53-
if (tree[basename].children) {
54-
getTreeNodes(tree[basename].children, nodes, path)
53+
if (children.length > 0) {
54+
getTreeNodes(children, path, nodes)
5555
}
5656
}
5757
return nodes
5858
}
5959

60-
export const getFolderTreeNodes = async (): Promise<TreeNode[]> => {
61-
const { data: tree } = await axios.get<Tree>(generateOcsUrl('/apps/files/api/v1/folder-tree'))
62-
const nodes = getTreeNodes(tree)
60+
export const getFolderTreeNodes = async (path: string = '/', depth: number = 1): Promise<TreeNode[]> => {
61+
const { data: tree } = await axios.get<Tree>(generateOcsUrl('/apps/files/api/v1/folder-tree'), {
62+
params: new URLSearchParams({ path, depth: String(depth) }),
63+
})
64+
const nodes = getTreeNodes(tree, path)
6365
return nodes
6466
}
6567

apps/files/src/views/Navigation.vue

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,11 @@
3939

4040
<script lang="ts">
4141
import type { View } from '@nextcloud/files'
42+
import type { ViewConfig } from '../types.ts'
4243
43-
import { emit } from '@nextcloud/event-bus'
44-
import { translate as t, getCanonicalLocale, getLanguage } from '@nextcloud/l10n'
4544
import { defineComponent } from 'vue'
45+
import { emit, subscribe } from '@nextcloud/event-bus'
46+
import { translate as t, getCanonicalLocale, getLanguage } from '@nextcloud/l10n'
4647
4748
import IconCog from 'vue-material-design-icons/Cog.vue'
4849
import NcAppNavigation from '@nextcloud/vue/dist/Components/NcAppNavigation.js'
@@ -144,6 +145,11 @@ export default defineComponent({
144145
},
145146
},
146147
148+
created() {
149+
subscribe('files:folder-tree:initialized', this.loadExpandedViews)
150+
subscribe('files:folder-tree:expanded', this.loadExpandedViews)
151+
},
152+
147153
beforeMount() {
148154
// This is guaranteed to be a view because `currentViewId` falls back to the default 'files' view
149155
const view = this.views.find(({ id }) => id === this.currentViewId)!
@@ -152,6 +158,39 @@ export default defineComponent({
152158
},
153159
154160
methods: {
161+
async loadExpandedViews() {
162+
const viewConfigs = this.viewConfigStore.getConfigs()
163+
let viewsToLoad = (Object.entries(viewConfigs) as Array<[string, ViewConfig]>)
164+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
165+
.filter(([viewId, config]) => config.expanded === true)
166+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
167+
.filter(([viewId, config]) => {
168+
const view = this.$navigation.views.find(view => view.id === viewId)
169+
if (view === undefined) {
170+
return false // Only registered views
171+
}
172+
return view.onToggleOpen && !view.loaded
173+
})
174+
175+
let i = 0
176+
while (viewsToLoad.length > 0) {
177+
const viewConfig = viewsToLoad.at(i)
178+
if (viewConfig === undefined) {
179+
i = 0
180+
continue
181+
}
182+
const [viewId, config] = viewConfig
183+
const view = this.$navigation.views.find(view => view.id === viewId)
184+
if (view === undefined) {
185+
++i
186+
continue
187+
}
188+
await view.onToggleOpen(config.expanded, view)
189+
viewsToLoad = viewsToLoad.toSpliced(i, 1)
190+
++i
191+
}
192+
},
193+
155194
/**
156195
* Set the view as active on the navigation and handle internal state
157196
* @param view View to set active

apps/files/src/views/folderTree.ts

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55

66
import type { TreeNode } from '../services/FolderTree.ts'
77

8+
import PQueue from 'p-queue'
89
import { Folder, Node, View, getNavigation } from '@nextcloud/files'
910
import { translate as t } from '@nextcloud/l10n'
10-
import { subscribe } from '@nextcloud/event-bus'
11+
import { emit, subscribe } from '@nextcloud/event-bus'
1112
import { isSamePath } from '@nextcloud/paths'
1213
import { loadState } from '@nextcloud/initial-state'
1314

@@ -29,6 +30,37 @@ const isFolderTreeEnabled = loadState('files', 'config', { folder_tree: true }).
2930

3031
const Navigation = getNavigation()
3132

33+
const queue = new PQueue({ concurrency: 5, intervalCap: 5, interval: 200 }) // Limit number of concurrent view registrations as this could potentially be very large
34+
35+
const getOnToggleOpen = (node: TreeNode | Folder) => {
36+
return async (open: boolean, view: View): Promise<void> => {
37+
if (!open) {
38+
return
39+
}
40+
// @ts-expect-error Custom property
41+
if (view.loaded) {
42+
return
43+
}
44+
// @ts-expect-error Custom property
45+
view.loading = true
46+
const nodes = await getFolderTreeNodes(node.path)
47+
try {
48+
const promises = nodes.map(node => queue.add(() => registerTreeNodeView(node)))
49+
await Promise.allSettled(promises)
50+
} catch (error) {
51+
// Skip duplicate view registration errors
52+
}
53+
// @ts-expect-error Custom property
54+
view.loading = false
55+
// @ts-expect-error Custom property
56+
view.loaded = true
57+
// @ts-expect-error No payload
58+
emit('files:navigation:updated')
59+
// @ts-expect-error No payload
60+
emit('files:folder-tree:expanded')
61+
}
62+
}
63+
3264
const registerTreeNodeView = (node: TreeNode) => {
3365
Navigation.register(new View({
3466
id: encodeSource(node.source),
@@ -40,6 +72,7 @@ const registerTreeNodeView = (node: TreeNode) => {
4072
order: 0, // TODO Allow undefined order for natural sort
4173

4274
getContents,
75+
onToggleOpen: getOnToggleOpen(node),
4376

4477
params: {
4578
view: folderTreeId,
@@ -60,6 +93,7 @@ const registerFolderView = (folder: Folder) => {
6093
order: 0, // TODO Allow undefined order for natural sort
6194

6295
getContents,
96+
onToggleOpen: getOnToggleOpen(folder),
6397

6498
params: {
6599
view: folderTreeId,
@@ -134,13 +168,14 @@ const registerFolderTreeRoot = () => {
134168

135169
const registerFolderTreeChildren = async () => {
136170
const nodes = await getFolderTreeNodes()
137-
for (const node of nodes) {
138-
registerTreeNodeView(node)
139-
}
171+
const promises = nodes.map(node => queue.add(() => registerTreeNodeView(node)))
172+
await Promise.allSettled(promises)
140173

141174
subscribe('files:node:created', onCreateNode)
142175
subscribe('files:node:deleted', onDeleteNode)
143176
subscribe('files:node:moved', onMoveNode)
177+
// @ts-expect-error No payload
178+
emit('files:folder-tree:initialized')
144179
}
145180

146181
export const registerFolderTreeView = async () => {

0 commit comments

Comments
 (0)