Skip to content

Commit

Permalink
feat: Load limited depth tree
Browse files Browse the repository at this point in the history
Signed-off-by: Christopher Ng <chrng8@gmail.com>
  • Loading branch information
Pytal committed Aug 8, 2024
1 parent 5eadd12 commit 35ae859
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 31 deletions.
13 changes: 10 additions & 3 deletions apps/files/src/components/FilesNavigationItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
<NcAppNavigationItem v-for="view in currentViews"
:key="view.id"
class="files-navigation__item"
:show-collapse="view.onToggleOpen && !view.loaded"
allow-collapse
:loading="view.loading"
:data-cy-files-navigation-item="view.id"
:exact="useExactRouteMatching(view)"
:icon="view.iconClass"
Expand All @@ -17,7 +19,7 @@
: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>
Expand Down Expand Up @@ -142,14 +144,19 @@ 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 (!view.onToggleOpen) {
return
}
await view.onToggleOpen(open, view)
},
/**
Expand Down
2 changes: 2 additions & 0 deletions apps/files/src/composables/useNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

/**
Expand Down Expand Up @@ -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)
Expand Down
46 changes: 24 additions & 22 deletions apps/files/src/services/FolderTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
}

Expand Down
43 changes: 41 additions & 2 deletions apps/files/src/views/Navigation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)!
Expand All @@ -152,6 +158,39 @@ export default defineComponent({
},
methods: {
async loadExpandedViews() {
const viewConfigs = this.viewConfigStore.getConfigs()
let viewsToLoad = (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
.filter(([viewId, config]) => {
const view = this.$navigation.views.find(view => view.id === viewId)
if (view === undefined) {
return false // Only registered views
}
return view.onToggleOpen && !view.loaded
})
let i = 0
while (viewsToLoad.length > 0) {
const viewConfig = viewsToLoad.at(i)
if (viewConfig === undefined) {
i = 0
continue
}
const [viewId, config] = viewConfig
const view = this.$navigation.views.find(view => view.id === viewId)
if (view === undefined) {
++i
continue
}
await view.onToggleOpen(config.expanded, view)
viewsToLoad = viewsToLoad.toSpliced(i, 1)
++i
}
},
/**
* Set the view as active on the navigation and handle internal state
* @param view View to set active
Expand Down
43 changes: 39 additions & 4 deletions apps/files/src/views/folderTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

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

const Navigation = getNavigation()

const queue = new PQueue({ concurrency: 5, intervalCap: 5, interval: 200 }) // Limit number of concurrent view registrations as this could potentially be very large

const getOnToggleOpen = (node: TreeNode | Folder) => {
return async (open: boolean, view: View): Promise<void> => {
if (!open) {
return
}
// @ts-expect-error Custom property
if (view.loaded) {
return
}
// @ts-expect-error Custom property
view.loading = true
const nodes = await getFolderTreeNodes(node.path)
try {
const promises = nodes.map(node => queue.add(() => registerTreeNodeView(node)))
await Promise.allSettled(promises)
} 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),
Expand All @@ -40,6 +72,7 @@ const registerTreeNodeView = (node: TreeNode) => {
order: 0, // TODO Allow undefined order for natural sort

getContents,
onToggleOpen: getOnToggleOpen(node),

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

getContents,
onToggleOpen: getOnToggleOpen(folder),

params: {
view: folderTreeId,
Expand Down Expand Up @@ -134,13 +168,14 @@ const registerFolderTreeRoot = () => {

const registerFolderTreeChildren = async () => {
const nodes = await getFolderTreeNodes()
for (const node of nodes) {
registerTreeNodeView(node)
}
const promises = nodes.map(node => queue.add(() => registerTreeNodeView(node)))
await Promise.allSettled(promises)

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 () => {
Expand Down

0 comments on commit 35ae859

Please sign in to comment.