mirror of
https://github.com/nextcloud/server.git
synced 2026-04-21 14:23:17 -04:00
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>
This commit is contained in:
parent
e5c1d80a00
commit
95d7b5608b
2 changed files with 160 additions and 108 deletions
|
|
@ -112,5 +112,5 @@ export function getSourceParent(source: string): string {
|
|||
if (parent === sourceRoot) {
|
||||
return folderTreeId
|
||||
}
|
||||
return encodeSource(parent)
|
||||
return `${folderTreeId}::${encodeSource(parent)}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
/**
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { Folder, Node } from '@nextcloud/files'
|
||||
import type { IFolder, INode, IView } from '@nextcloud/files'
|
||||
import type { TreeNode } from '../services/FolderTree.ts'
|
||||
|
||||
import FolderMultipleSvg from '@mdi/svg/svg/folder-multiple-outline.svg?raw'
|
||||
import FolderSvg from '@mdi/svg/svg/folder-outline.svg?raw'
|
||||
import { emit, subscribe } from '@nextcloud/event-bus'
|
||||
import { subscribe } from '@nextcloud/event-bus'
|
||||
import { FileType, getNavigation, View } from '@nextcloud/files'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { isSamePath } from '@nextcloud/paths'
|
||||
import PQueue from 'p-queue'
|
||||
import {
|
||||
|
|
@ -21,70 +21,109 @@ import {
|
|||
getSourceParent,
|
||||
sourceRoot,
|
||||
} from '../services/FolderTree.ts'
|
||||
import { useFilesStore } from '../store/files.ts'
|
||||
import { getPinia } from '../store/index.ts'
|
||||
|
||||
const isFolderTreeEnabled = loadState('files', 'config', { folder_tree: true }).folder_tree
|
||||
|
||||
let showHiddenFiles = loadState('files', 'config', { show_hidden: false }).show_hidden
|
||||
interface IFolderTreeView extends IView {
|
||||
loading?: boolean
|
||||
loaded?: boolean
|
||||
}
|
||||
|
||||
const Navigation = getNavigation()
|
||||
|
||||
const queue = new PQueue({ concurrency: 5, intervalCap: 5, interval: 200 })
|
||||
const isFolderTreeEnabled = loadState('files', 'config', { folder_tree: true }).folder_tree
|
||||
let showHiddenFiles = loadState('files', 'config', { show_hidden: false }).show_hidden
|
||||
|
||||
const registerQueue = new PQueue({ concurrency: 5, intervalCap: 5, interval: 200 })
|
||||
const folderTreeView: IFolderTreeView = new View({
|
||||
id: folderTreeId,
|
||||
|
||||
/**
|
||||
*
|
||||
* @param path
|
||||
*/
|
||||
async function registerTreeChildren(path: string = '/') {
|
||||
await queue.add(async () => {
|
||||
// preload up to 2 depth levels for faster navigation
|
||||
const nodes = await getFolderTreeNodes(path, 2)
|
||||
const promises = nodes.map((node) => registerQueue.add(() => registerNodeView(node)))
|
||||
await Promise.allSettled(promises)
|
||||
})
|
||||
}
|
||||
name: t('files', 'Folder tree'),
|
||||
caption: t('files', 'List of your files and folders.'),
|
||||
|
||||
/**
|
||||
*
|
||||
* @param node
|
||||
*/
|
||||
function getLoadChildViews(node: TreeNode | Folder) {
|
||||
return async (view: View): Promise<void> => {
|
||||
// @ts-expect-error Custom property on View instance
|
||||
if (view.loading || view.loaded) {
|
||||
icon: FolderMultipleSvg,
|
||||
order: 50, // Below all other views
|
||||
|
||||
getContents,
|
||||
|
||||
async loadChildViews(view) {
|
||||
const treeView = view as IFolderTreeView
|
||||
if (treeView.loading || treeView.loaded) {
|
||||
return
|
||||
}
|
||||
// @ts-expect-error Custom property
|
||||
view.loading = true
|
||||
await registerTreeChildren(node.path)
|
||||
// @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')
|
||||
|
||||
treeView.loading = true
|
||||
try {
|
||||
const dir = new URLSearchParams(window.location.search).get('dir') ?? '/'
|
||||
const tree = await getFolderTreeNodes(dir, 1, true)
|
||||
registerNodeViews(tree, dir)
|
||||
treeView.loaded = true
|
||||
|
||||
subscribe('files:node:created', onCreateNode)
|
||||
subscribe('files:node:deleted', onDeleteNode)
|
||||
subscribe('files:node:moved', onMoveNode)
|
||||
subscribe('files:config:updated', onUserConfigUpdated)
|
||||
} finally {
|
||||
treeView.loading = false
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Register the folder tree feature
|
||||
*/
|
||||
export async function registerFolderTreeView() {
|
||||
if (!isFolderTreeEnabled) {
|
||||
return
|
||||
}
|
||||
Navigation.register(folderTreeView)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to register node views in the navigation.
|
||||
*
|
||||
* @param node
|
||||
* @param nodes - The nodes to register
|
||||
* @param path - The path to expand by default, if any
|
||||
*/
|
||||
function registerNodeView(node: TreeNode | Folder) {
|
||||
const registeredView = Navigation.views.find((view) => view.id === node.encodedSource)
|
||||
if (registeredView) {
|
||||
Navigation.remove(registeredView.id)
|
||||
async function registerNodeViews(nodes: (TreeNode | IFolder)[], path?: string) {
|
||||
const views: IView[] = []
|
||||
for (const node of nodes) {
|
||||
const isRegistered = Navigation.views.some((view) => view.id === `${folderTreeId}::${node.encodedSource}`)
|
||||
// skip hidden files if the setting is disabled
|
||||
if (!showHiddenFiles && node.basename.startsWith('.')) {
|
||||
if (isRegistered) {
|
||||
// and also remove any existing views for hidden files if the setting was toggled
|
||||
Navigation.remove(`${folderTreeId}::${node.encodedSource}`)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// skip already registered views to avoid duplicates when loading multiple levels
|
||||
if (isRegistered) {
|
||||
continue
|
||||
}
|
||||
|
||||
views.push(generateNodeView(
|
||||
node,
|
||||
path === node.path || path?.startsWith(node.path + '/') ? true : undefined,
|
||||
))
|
||||
}
|
||||
if (!showHiddenFiles && node.basename.startsWith('.')) {
|
||||
return
|
||||
}
|
||||
Navigation.register(new View({
|
||||
id: node.encodedSource,
|
||||
Navigation.register(...views)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a navigation view for a given folder tree node or folder.
|
||||
*
|
||||
* @param node - The folder tree node or folder for which to generate the view.
|
||||
* @param expanded - Whether the view should be expanded by default.
|
||||
*/
|
||||
function generateNodeView(node: TreeNode | IFolder, expanded?: boolean): IView {
|
||||
return {
|
||||
id: `${folderTreeId}::${node.encodedSource}`,
|
||||
parent: getSourceParent(node.source),
|
||||
|
||||
expanded,
|
||||
loaded: expanded,
|
||||
|
||||
// @ts-expect-error Casing differences
|
||||
name: node.displayName ?? node.displayname ?? node.basename,
|
||||
|
||||
|
|
@ -98,60 +137,109 @@ function registerNodeView(node: TreeNode | Folder) {
|
|||
fileid: String(node.fileid), // Needed for matching exact routes
|
||||
dir: node.path,
|
||||
},
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a function to load child views for a given folder tree node or folder.
|
||||
* This function is used as the `loadChildViews` callback in the navigation view.
|
||||
*
|
||||
* @param folder
|
||||
* @param node - The folder tree node or folder for which to generate the child view loader function.
|
||||
*/
|
||||
function removeFolderView(folder: Folder) {
|
||||
function getLoadChildViews(node: TreeNode | IFolder) {
|
||||
return async (view: IView): Promise<void> => {
|
||||
const treeView = view as IFolderTreeView
|
||||
if (treeView.loading || treeView.loaded) {
|
||||
return
|
||||
}
|
||||
|
||||
treeView.loading = true
|
||||
try {
|
||||
await updateTreeChildren(node.path)
|
||||
treeView.loaded = true
|
||||
} finally {
|
||||
treeView.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers child views for the given path. If no path is provided, it registers the root nodes.
|
||||
*
|
||||
* @param path - The path for which to register child views. Defaults to '/' for root nodes.
|
||||
*/
|
||||
async function updateTreeChildren(path: string = '/') {
|
||||
await queue.add(async () => {
|
||||
const filesStore = useFilesStore(getPinia())
|
||||
const cachedNodes = filesStore.getNodesByPath(Navigation.active!.id, path)
|
||||
if (cachedNodes.length > 0) {
|
||||
// if there are nodes loaded in the path we dont need to fetch from API
|
||||
const folders = cachedNodes.filter((node) => node.type === FileType.Folder) as IFolder[]
|
||||
registerNodeViews(folders, path)
|
||||
} else {
|
||||
// otherwise we need to fetch the tree nodes for the path
|
||||
const nodes = await getFolderTreeNodes(path, 2)
|
||||
registerNodeViews(nodes)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a folder view from the navigation.
|
||||
*
|
||||
* @param folder - The folder for which to remove the view
|
||||
*/
|
||||
function removeFolderView(folder: IFolder) {
|
||||
const viewId = folder.encodedSource
|
||||
Navigation.remove(viewId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a folder view from the navigation by its source URL.
|
||||
*
|
||||
* @param source
|
||||
* @param source - The source URL of the folder for which to remove the view
|
||||
*/
|
||||
function removeFolderViewSource(source: string) {
|
||||
Navigation.remove(source)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle node creation events to add new folder tree views to the navigation.
|
||||
*
|
||||
* @param node
|
||||
* @param node - The node that was created
|
||||
*/
|
||||
function onCreateNode(node: Node) {
|
||||
function onCreateNode(node: INode) {
|
||||
if (node.type !== FileType.Folder) {
|
||||
return
|
||||
}
|
||||
registerNodeView(node)
|
||||
registerNodeViews([node as IFolder])
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle node deletion events to remove the corresponding folder tree views from the navigation.
|
||||
*
|
||||
* @param node
|
||||
* @param node - The node that was deleted
|
||||
*/
|
||||
function onDeleteNode(node: Node) {
|
||||
function onDeleteNode(node: INode) {
|
||||
if (node.type !== FileType.Folder) {
|
||||
return
|
||||
}
|
||||
removeFolderView(node)
|
||||
removeFolderView(node as IFolder)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle node move events to update the folder tree views accordingly.
|
||||
*
|
||||
* @param root0
|
||||
* @param root0.node
|
||||
* @param root0.oldSource
|
||||
* @param context - the event context
|
||||
* @param context.node - The node that was moved
|
||||
* @param context.oldSource - the old source URL of the moved node
|
||||
*/
|
||||
function onMoveNode({ node, oldSource }) {
|
||||
if (node.type !== FileType.Folder) {
|
||||
return
|
||||
}
|
||||
removeFolderViewSource(oldSource)
|
||||
registerNodeView(node)
|
||||
registerNodeViews([node as IFolder])
|
||||
|
||||
const newPath = node.source.replace(sourceRoot, '')
|
||||
const oldPath = oldSource.replace(sourceRoot, '')
|
||||
|
|
@ -165,58 +253,22 @@ function onMoveNode({ node, oldSource }) {
|
|||
return view.params.dir.startsWith(oldPath)
|
||||
})
|
||||
for (const view of childViews) {
|
||||
// @ts-expect-error FIXME Allow setting parent
|
||||
view.parent = getSourceParent(node.source)
|
||||
// @ts-expect-error dir param is defined
|
||||
view.params.dir = view.params.dir.replace(oldPath, newPath)
|
||||
view.params!.dir = view.params!.dir!.replace(oldPath, newPath)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle user config updates, specifically for the "show hidden files" setting,
|
||||
* to show hidden folders in the folder tree when enabled and hide them when disabled.
|
||||
*
|
||||
* @param root0
|
||||
* @param root0.key
|
||||
* @param root0.value
|
||||
* @param context - the event context
|
||||
* @param context.key - the key of the updated config
|
||||
* @param context.value - the new value of the updated config
|
||||
*/
|
||||
async function onUserConfigUpdated({ key, value }) {
|
||||
if (key === 'show_hidden') {
|
||||
showHiddenFiles = value
|
||||
await registerTreeChildren()
|
||||
// @ts-expect-error No payload
|
||||
emit('files:folder-tree:initialized')
|
||||
await updateTreeChildren()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function registerTreeRoot() {
|
||||
Navigation.register(new View({
|
||||
id: folderTreeId,
|
||||
|
||||
name: t('files', 'Folder tree'),
|
||||
caption: t('files', 'List of your files and folders.'),
|
||||
|
||||
icon: FolderMultipleSvg,
|
||||
order: 50, // Below all other views
|
||||
|
||||
getContents,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export async function registerFolderTreeView() {
|
||||
if (!isFolderTreeEnabled) {
|
||||
return
|
||||
}
|
||||
registerTreeRoot()
|
||||
await registerTreeChildren()
|
||||
subscribe('files:node:created', onCreateNode)
|
||||
subscribe('files:node:deleted', onDeleteNode)
|
||||
subscribe('files:node:moved', onMoveNode)
|
||||
subscribe('files:config:updated', onUserConfigUpdated)
|
||||
// @ts-expect-error No payload
|
||||
emit('files:folder-tree:initialized')
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue