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'
77import type { TreeNode } from '../services/FolderTree.ts'
88
99import FolderMultipleSvg from '@mdi/svg/svg/folder-multiple-outline.svg?raw'
1010import FolderSvg from '@mdi/svg/svg/folder-outline.svg?raw'
11- import { emit , subscribe } from '@nextcloud/event-bus'
11+ import { subscribe } from '@nextcloud/event-bus'
1212import { FileType , getNavigation , View } from '@nextcloud/files'
1313import { loadState } from '@nextcloud/initial-state'
14- import { translate as t } from '@nextcloud/l10n'
14+ import { t } from '@nextcloud/l10n'
1515import { isSamePath } from '@nextcloud/paths'
1616import PQueue from 'p-queue'
1717import {
@@ -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
2735let 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 */
117202function 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 */
149237function 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 */
181269async 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