Skip to content

Commit f258c66

Browse files
susnuxCarlSchwan
authored andcommitted
perf(files): initialize folder tree from current path and store
Initialize the folder tree based on the current directory. Also only include views needed. If possible reuse nodes from files store to prevent API call. Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
1 parent 52df429 commit f258c66

File tree

2 files changed

+155
-103
lines changed

2 files changed

+155
-103
lines changed

apps/files/src/services/FolderTree.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,5 +112,5 @@ export function getSourceParent(source: string): string {
112112
if (parent === sourceRoot) {
113113
return folderTreeId
114114
}
115-
return encodeSource(parent)
115+
return `${folderTreeId}::${encodeSource(parent)}`
116116
}

apps/files/src/views/folderTree.ts

Lines changed: 154 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
/**
1+
/*!
22
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
55

6-
import type { Folder, Node } from '@nextcloud/files'
6+
import type { IFolder, INode, IView } from '@nextcloud/files'
77
import type { TreeNode } from '../services/FolderTree.ts'
88

99
import FolderMultipleSvg from '@mdi/svg/svg/folder-multiple-outline.svg?raw'
1010
import FolderSvg from '@mdi/svg/svg/folder-outline.svg?raw'
11-
import { emit, subscribe } from '@nextcloud/event-bus'
11+
import { subscribe } from '@nextcloud/event-bus'
1212
import { FileType, getNavigation, View } from '@nextcloud/files'
1313
import { loadState } from '@nextcloud/initial-state'
14-
import { translate as t } from '@nextcloud/l10n'
14+
import { t } from '@nextcloud/l10n'
1515
import { isSamePath } from '@nextcloud/paths'
1616
import PQueue from 'p-queue'
1717
import {
@@ -21,70 +21,109 @@ import {
2121
getSourceParent,
2222
sourceRoot,
2323
} from '../services/FolderTree.ts'
24+
import { useFilesStore } from '../store/files.ts'
25+
import { getPinia } from '../store/index.ts'
2426

25-
const isFolderTreeEnabled = loadState('files', 'config', { folder_tree: true }).folder_tree
27+
interface IFolderTreeView extends IView {
28+
loading?: boolean
29+
loaded?: boolean
30+
}
2631

32+
const Navigation = getNavigation()
33+
const queue = new PQueue({ concurrency: 5, intervalCap: 5, interval: 200 })
34+
const isFolderTreeEnabled = loadState('files', 'config', { folder_tree: true }).folder_tree
2735
let showHiddenFiles = loadState('files', 'config', { show_hidden: false }).show_hidden
2836

29-
const Navigation = getNavigation()
37+
const folderTreeView: IFolderTreeView = new View({
38+
id: folderTreeId,
3039

31-
const queue = new PQueue({ concurrency: 5, intervalCap: 5, interval: 200 })
40+
name: t('files', 'Folder tree'),
41+
caption: t('files', 'List of your files and folders.'),
3242

33-
const registerQueue = new PQueue({ concurrency: 5, intervalCap: 5, interval: 200 })
43+
icon: FolderMultipleSvg,
44+
order: 50, // Below all other views
45+
46+
getContents,
47+
48+
async loadChildViews(view) {
49+
const treeView = view as IFolderTreeView
50+
if (treeView.loading || treeView.loaded) {
51+
return
52+
}
53+
54+
treeView.loading = true
55+
try {
56+
const dir = new URLSearchParams(window.location.search).get('dir') ?? '/'
57+
const tree = await getFolderTreeNodes(dir, 1, true)
58+
registerNodeViews(tree, dir)
59+
treeView.loaded = true
60+
61+
subscribe('files:node:created', onCreateNode)
62+
subscribe('files:node:deleted', onDeleteNode)
63+
subscribe('files:node:moved', onMoveNode)
64+
subscribe('files:config:updated', onUserConfigUpdated)
65+
} finally {
66+
treeView.loading = false
67+
}
68+
},
69+
})
3470

3571
/**
36-
*
37-
* @param path
72+
* Register the folder tree feature
3873
*/
39-
async function registerTreeChildren(path: string = '/') {
40-
await queue.add(async () => {
41-
// preload up to 2 depth levels for faster navigation
42-
const nodes = await getFolderTreeNodes(path, 2)
43-
const promises = nodes.map((node) => registerQueue.add(() => registerNodeView(node)))
44-
await Promise.allSettled(promises)
45-
})
74+
export async function registerFolderTreeView() {
75+
if (!isFolderTreeEnabled) {
76+
return
77+
}
78+
Navigation.register(folderTreeView)
4679
}
4780

4881
/**
82+
* Helper to register node views in the navigation.
4983
*
50-
* @param node
84+
* @param nodes - The nodes to register
85+
* @param path - The path to expand by default, if any
5186
*/
52-
function getLoadChildViews(node: TreeNode | Folder) {
53-
return async (view: View): Promise<void> => {
54-
// @ts-expect-error Custom property on View instance
55-
if (view.loading || view.loaded) {
56-
return
87+
async function registerNodeViews(nodes: (TreeNode | IFolder)[], path?: string) {
88+
const views: IView[] = []
89+
for (const node of nodes) {
90+
const isRegistered = Navigation.views.some((view) => view.id === `${folderTreeId}::${node.encodedSource}`)
91+
// skip hidden files if the setting is disabled
92+
if (!showHiddenFiles && node.basename.startsWith('.')) {
93+
if (isRegistered) {
94+
// and also remove any existing views for hidden files if the setting was toggled
95+
Navigation.remove(`${folderTreeId}::${node.encodedSource}`)
96+
}
97+
continue
98+
}
99+
100+
// skip already registered views to avoid duplicates when loading multiple levels
101+
if (isRegistered) {
102+
continue
57103
}
58-
// @ts-expect-error Custom property
59-
view.loading = true
60-
await registerTreeChildren(node.path)
61-
// @ts-expect-error Custom property
62-
view.loading = false
63-
// @ts-expect-error Custom property
64-
view.loaded = true
65-
// @ts-expect-error No payload
66-
emit('files:navigation:updated')
67-
// @ts-expect-error No payload
68-
emit('files:folder-tree:expanded')
104+
105+
views.push(generateNodeView(
106+
node,
107+
path === node.path || path?.startsWith(node.path + '/') ? true : undefined,
108+
))
69109
}
110+
Navigation.register(...views)
70111
}
71112

72113
/**
114+
* Generates a navigation view for a given folder tree node or folder.
73115
*
74-
* @param node
116+
* @param node - The folder tree node or folder for which to generate the view.
117+
* @param expanded - Whether the view should be expanded by default.
75118
*/
76-
function registerNodeView(node: TreeNode | Folder) {
77-
const registeredView = Navigation.views.find((view) => view.id === node.encodedSource)
78-
if (registeredView) {
79-
Navigation.remove(registeredView.id)
80-
}
81-
if (!showHiddenFiles && node.basename.startsWith('.')) {
82-
return
83-
}
84-
Navigation.register(new View({
85-
id: node.encodedSource,
119+
function generateNodeView(node: TreeNode | IFolder, expanded?: boolean): IView {
120+
return {
121+
id: `${folderTreeId}::${node.encodedSource}`,
86122
parent: getSourceParent(node.source),
87123

124+
expanded,
125+
loaded: expanded,
126+
88127
// @ts-expect-error Casing differences
89128
name: node.displayName ?? node.displayname ?? node.basename,
90129

@@ -98,60 +137,109 @@ function registerNodeView(node: TreeNode | Folder) {
98137
fileid: String(node.fileid), // Needed for matching exact routes
99138
dir: node.path,
100139
},
101-
}))
140+
}
141+
}
142+
143+
/**
144+
* Generates a function to load child views for a given folder tree node or folder.
145+
* This function is used as the `loadChildViews` callback in the navigation view.
146+
*
147+
* @param node - The folder tree node or folder for which to generate the child view loader function.
148+
*/
149+
function getLoadChildViews(node: TreeNode | IFolder) {
150+
return async (view: IView): Promise<void> => {
151+
const treeView = view as IFolderTreeView
152+
if (treeView.loading || treeView.loaded) {
153+
return
154+
}
155+
156+
treeView.loading = true
157+
try {
158+
await updateTreeChildren(node.path)
159+
treeView.loaded = true
160+
} finally {
161+
treeView.loading = false
162+
}
163+
}
102164
}
103165

104166
/**
167+
* Registers child views for the given path. If no path is provided, it registers the root nodes.
105168
*
106-
* @param folder
169+
* @param path - The path for which to register child views. Defaults to '/' for root nodes.
107170
*/
108-
function removeFolderView(folder: Folder) {
171+
async function updateTreeChildren(path: string = '/') {
172+
await queue.add(async () => {
173+
const filesStore = useFilesStore(getPinia())
174+
const cachedNodes = filesStore.getNodesByPath(Navigation.active!.id, path)
175+
if (cachedNodes.length > 0) {
176+
// if there are nodes loaded in the path we dont need to fetch from API
177+
const folders = cachedNodes.filter((node) => node.type === FileType.Folder) as IFolder[]
178+
registerNodeViews(folders, path)
179+
} else {
180+
// otherwise we need to fetch the tree nodes for the path
181+
const nodes = await getFolderTreeNodes(path, 2)
182+
registerNodeViews(nodes)
183+
}
184+
})
185+
}
186+
187+
/**
188+
* Remove a folder view from the navigation.
189+
*
190+
* @param folder - The folder for which to remove the view
191+
*/
192+
function removeFolderView(folder: IFolder) {
109193
const viewId = folder.encodedSource
110194
Navigation.remove(viewId)
111195
}
112196

113197
/**
198+
* Remove a folder view from the navigation by its source URL.
114199
*
115-
* @param source
200+
* @param source - The source URL of the folder for which to remove the view
116201
*/
117202
function removeFolderViewSource(source: string) {
118203
Navigation.remove(source)
119204
}
120205

121206
/**
207+
* Handle node creation events to add new folder tree views to the navigation.
122208
*
123-
* @param node
209+
* @param node - The node that was created
124210
*/
125-
function onCreateNode(node: Node) {
211+
function onCreateNode(node: INode) {
126212
if (node.type !== FileType.Folder) {
127213
return
128214
}
129-
registerNodeView(node)
215+
registerNodeViews([node as IFolder])
130216
}
131217

132218
/**
219+
* Handle node deletion events to remove the corresponding folder tree views from the navigation.
133220
*
134-
* @param node
221+
* @param node - The node that was deleted
135222
*/
136-
function onDeleteNode(node: Node) {
223+
function onDeleteNode(node: INode) {
137224
if (node.type !== FileType.Folder) {
138225
return
139226
}
140-
removeFolderView(node)
227+
removeFolderView(node as IFolder)
141228
}
142229

143230
/**
231+
* Handle node move events to update the folder tree views accordingly.
144232
*
145-
* @param root0
146-
* @param root0.node
147-
* @param root0.oldSource
233+
* @param context - the event context
234+
* @param context.node - The node that was moved
235+
* @param context.oldSource - the old source URL of the moved node
148236
*/
149237
function onMoveNode({ node, oldSource }) {
150238
if (node.type !== FileType.Folder) {
151239
return
152240
}
153241
removeFolderViewSource(oldSource)
154-
registerNodeView(node)
242+
registerNodeViews([node as IFolder])
155243

156244
const newPath = node.source.replace(sourceRoot, '')
157245
const oldPath = oldSource.replace(sourceRoot, '')
@@ -165,58 +253,22 @@ function onMoveNode({ node, oldSource }) {
165253
return view.params.dir.startsWith(oldPath)
166254
})
167255
for (const view of childViews) {
168-
// @ts-expect-error FIXME Allow setting parent
169256
view.parent = getSourceParent(node.source)
170-
// @ts-expect-error dir param is defined
171-
view.params.dir = view.params.dir.replace(oldPath, newPath)
257+
view.params!.dir = view.params!.dir!.replace(oldPath, newPath)
172258
}
173259
}
174260

175261
/**
262+
* Handle user config updates, specifically for the "show hidden files" setting,
263+
* to show hidden folders in the folder tree when enabled and hide them when disabled.
176264
*
177-
* @param root0
178-
* @param root0.key
179-
* @param root0.value
265+
* @param context - the event context
266+
* @param context.key - the key of the updated config
267+
* @param context.value - the new value of the updated config
180268
*/
181269
async function onUserConfigUpdated({ key, value }) {
182270
if (key === 'show_hidden') {
183271
showHiddenFiles = value
184-
await registerTreeChildren()
185-
// @ts-expect-error No payload
186-
emit('files:folder-tree:initialized')
187-
}
188-
}
189-
190-
/**
191-
*
192-
*/
193-
function registerTreeRoot() {
194-
Navigation.register(new View({
195-
id: folderTreeId,
196-
197-
name: t('files', 'Folder tree'),
198-
caption: t('files', 'List of your files and folders.'),
199-
200-
icon: FolderMultipleSvg,
201-
order: 50, // Below all other views
202-
203-
getContents,
204-
}))
205-
}
206-
207-
/**
208-
*
209-
*/
210-
export async function registerFolderTreeView() {
211-
if (!isFolderTreeEnabled) {
212-
return
272+
await updateTreeChildren()
213273
}
214-
registerTreeRoot()
215-
await registerTreeChildren()
216-
subscribe('files:node:created', onCreateNode)
217-
subscribe('files:node:deleted', onDeleteNode)
218-
subscribe('files:node:moved', onMoveNode)
219-
subscribe('files:config:updated', onUserConfigUpdated)
220-
// @ts-expect-error No payload
221-
emit('files:folder-tree:initialized')
222274
}

0 commit comments

Comments
 (0)