From 5d7f6f0eb2d161f4d462d7cad9a522d837dccd45 Mon Sep 17 00:00:00 2001 From: Christopher Ng Date: Fri, 19 Jul 2024 14:05:36 -0700 Subject: [PATCH] fixup! [WIP] feat: Add folder tree to sidebar Signed-off-by: Christopher Ng --- .../components/FilesNavigationItemList.vue | 58 +++++++++------ apps/files/src/eventbus.d.ts | 3 + apps/files/src/newMenu/newFolder.ts | 1 + apps/files/src/services/FolderTree.ts | 18 +++-- apps/files/src/views/Navigation.vue | 44 +++++++++--- apps/files/src/views/folderTree.ts | 72 +++++++++++++++---- package-lock.json | 12 ++-- package.json | 2 +- 8 files changed, 152 insertions(+), 58 deletions(-) diff --git a/apps/files/src/components/FilesNavigationItemList.vue b/apps/files/src/components/FilesNavigationItemList.vue index 33289c49b1570..c3403d06eeb39 100644 --- a/apps/files/src/components/FilesNavigationItemList.vue +++ b/apps/files/src/components/FilesNavigationItemList.vue @@ -14,20 +14,19 @@ :name="view.name" :open="isExpanded(view)" :pinned="view.sticky" + :active="isActive(view)" :to="generateToNavigation(view)" :style="style" @update:open="onToggleExpand(view)"> - - - + + :views="filterView(views, parent)" /> @@ -43,6 +42,8 @@ import NcAppNavigationItem from '@nextcloud/vue/dist/Components/NcAppNavigationI import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js' import { useViewConfigStore } from '../store/viewConfig.js' +import { useNavigation } from '../composables/useNavigation.js' +import { folderTreeId } from '../services/FolderTree.js' export default defineComponent({ name: 'FilesNavigationItemList', @@ -63,38 +64,27 @@ export default defineComponent({ default: 0, }, views: { - type: Array as PropType, - default: () => [], + type: Object as PropType>, + default: () => ({}), }, }, setup() { + const { currentView } = useNavigation() const viewConfigStore = useViewConfigStore() return { + currentView, viewConfigStore, } }, computed: { currentViews(): View[] { - return this.views - .filter(view => view.parent === this.parent) - .toSorted((a, b) => a.order - b.order) - }, - - childViews(): Record { - return this.views - .filter(view => view.parent !== this.parent) - // create a map of parents and their children - .reduce((list, view) => { - list[view.parent!] = [...(list[view.parent!] || []), view] - list[view.parent!].sort((a, b) => a.order - b.order) - return list - }, {} as Record) + return this.views[this.parent] ?? [] }, style() { - if (this.level === 0) { + if (this.level === 0 || this.level === 1) { return null } return { @@ -105,7 +95,15 @@ export default defineComponent({ methods: { hasChildViews(view: View): boolean { - return this.childViews[view.id]?.length > 0 + return this.views[view.id]?.length > 0 + }, + + isActive(view: View): boolean { + // FIXME + if (!view.id.startsWith(folderTreeId)) { // If not navigating within the folder tree + return false + } + return view.params?.path === this.$route?.query?.dir }, /** @@ -153,6 +151,20 @@ export default defineComponent({ view.expanded = !isExpanded this.viewConfigStore.update(view.id, 'expanded', !isExpanded) }, + + /** + * Return the view map with the specified view id removed + * + * @param viewMap Map of views + * @param id View id + */ + filterView(viewMap: Record, id: string): Record { + return Object.fromEntries( + Object.entries(viewMap) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .filter(([viewId, _views]) => viewId !== id), + ) + }, }, }) diff --git a/apps/files/src/eventbus.d.ts b/apps/files/src/eventbus.d.ts index 8d57d82c034dc..b1ff00e5285eb 100644 --- a/apps/files/src/eventbus.d.ts +++ b/apps/files/src/eventbus.d.ts @@ -10,6 +10,9 @@ declare module '@nextcloud/event-bus' { 'files:favorites:removed': Node 'files:favorites:added': Node 'files:node:renamed': Node + 'files:node:created': Node + 'files:node:deleted': Node + 'files:node:updated': Node } } diff --git a/apps/files/src/newMenu/newFolder.ts b/apps/files/src/newMenu/newFolder.ts index a570fa71c6101..54fd689e10a9f 100644 --- a/apps/files/src/newMenu/newFolder.ts +++ b/apps/files/src/newMenu/newFolder.ts @@ -62,6 +62,7 @@ export const entry = { 'mount-type': context.attributes?.['mount-type'], 'owner-id': context.attributes?.['owner-id'], 'owner-display-name': context.attributes?.['owner-display-name'], + parentid: context.fileid, }, }) diff --git a/apps/files/src/services/FolderTree.ts b/apps/files/src/services/FolderTree.ts index 4d8faba37d072..3f5a77cf22a32 100644 --- a/apps/files/src/services/FolderTree.ts +++ b/apps/files/src/services/FolderTree.ts @@ -17,7 +17,7 @@ import { getContents, resultToNode } from './Files.ts' export const folderTreeId = 'folders' export const getFolders = (): CancelablePromise => { - const userId = getCurrentUser()?.uid + const userId = getCurrentUser()?.uid as string const searchPayload = ` @@ -70,13 +70,21 @@ export const getFolderContents = (folder: Folder, path = '/'): CancelablePromise return getContents(joinPaths(folder.path, path)) } -export const generateFolderTreeId = (folder: Folder): string => { - return `${folderTreeId}-${folder.fileid}` // FIXME fileid collision +export const getFolderTreeViewId = (folder: Folder): string => { + const mountType = folder.attributes['mount-type'] + if (mountType !== '') { // If not local mount + return `${folderTreeId}-${mountType}-${folder.fileid}` // Include mount type as fileids may conflict across storage mounts + } + return `${folderTreeId}-${folder.fileid}` } -export const generateFolderTreeParentId = (folder: Folder): string => { +export const getFolderTreeParentId = (folder: Folder): string => { if (folder.dirname === '/') { return folderTreeId } - return `${folderTreeId}-${folder.attributes.parentid}` // FIXME parentid collision + const mountType = folder.attributes['mount-type'] + if (mountType !== '') { // If not local mount + return `${folderTreeId}-${mountType}-${folder.attributes.parentid}` // Include mount type as fileids may conflict across storage mounts + } + return `${folderTreeId}-${folder.attributes.parentid}` } diff --git a/apps/files/src/views/Navigation.vue b/apps/files/src/views/Navigation.vue index ecb0d88e42b58..44f32c0a43b2a 100644 --- a/apps/files/src/views/Navigation.vue +++ b/apps/files/src/views/Navigation.vue @@ -6,7 +6,7 @@ @@ -88,17 +88,33 @@ export default defineComponent({ currentViewId() { return this.$route?.params?.view || 'files' }, + + /** + * Map of parent ids to views + */ + viewsMap(): Record { + return this.views + .reduce((map, view) => { + map[view.parent!] = [...(map[view.parent!] || []), view] + // TODO Allow undefined order + map[view.parent!].sort((a, b) => { + if (typeof a.order === 'number' && typeof b.order === 'number') { + return a.order - b.order + } + return a.name.localeCompare(b.name) + }) + return map + }, {} as Record) + }, }, watch: { - currentViewId(newView, oldView) { - if (this.currentViewId !== this.currentView?.id) { - // 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)! - // The the new view as active - this.showView(view) - logger.debug(`Navigation changed from ${oldView} to ${newView}`, { to: view }) - } + currentViewId(_newView, _oldView) { + this.updateView() + }, + + views(_newViews, _oldViews) { + this.updateView() }, }, @@ -112,6 +128,16 @@ export default defineComponent({ methods: { t, + updateView() { + if (this.currentViewId !== this.currentView?.id) { + // 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)! + // The the new view as active + this.showView(view) + logger.debug(`Navigation changed from ${oldView} to ${newView}`, { to: view }) + } + }, + /** * Set the view as active on the navigation and handle internal state * @param view View to set active diff --git a/apps/files/src/views/folderTree.ts b/apps/files/src/views/folderTree.ts index 33777f92da88f..dfc29d2899947 100644 --- a/apps/files/src/views/folderTree.ts +++ b/apps/files/src/views/folderTree.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { Folder, View, getNavigation, registerDavProperty } from '@nextcloud/files' +import { Folder, Node, View, getNavigation, registerDavProperty } from '@nextcloud/files' import { translate as t } from '@nextcloud/l10n' import FolderSvg from '@mdi/svg/svg/folder.svg?raw' @@ -11,17 +11,65 @@ import FolderSvg from '@mdi/svg/svg/folder.svg?raw' import { getContents } from '../services/Files.ts' import { folderTreeId, - generateFolderTreeId, - generateFolderTreeParentId, + getFolderTreeViewId, + getFolderTreeParentId, getFolderContents, getFolders, } from '../services/FolderTree.ts' +import { subscribe } from '@nextcloud/event-bus' registerDavProperty('nc:parentid', { nc: 'http://nextcloud.org/ns' }) // FIXME Fix `dir` query param // FIXME Duplicate root breaccrumbs +const registerFolderView = (folder: Folder) => { + const Navigation = getNavigation() + Navigation.register(new View({ + id: getFolderTreeViewId(folder), + parent: getFolderTreeParentId(folder), + + name: folder.attributes.displayname ?? folder.basename, + + icon: FolderSvg, + order: 0, // TODO Allow undefined order + + getContents: (path) => getFolderContents(folder, path.replace(folder.path, '')), + + params: { + path: folder.path, + }, + })) +} + +const removeFolderView = (folder: Folder) => { + const Navigation = getNavigation() + const viewId = getFolderTreeViewId(folder) + Navigation.remove(viewId) +} + +const onCreateNode = (node: Node) => { + if (!(node instanceof Folder)) { + return + } + registerFolderView(node) +} + +const onDeleteNode = (node: Node) => { + if (!(node instanceof Folder)) { + return + } + removeFolderView(node) +} + +const onUpdateNode = (node: Node) => { + if (!(node instanceof Folder)) { + return + } + removeFolderView(node) + registerFolderView(node) +} + export const registerFolderTreeView = async () => { const Navigation = getNavigation() @@ -34,23 +82,17 @@ export const registerFolderTreeView = async () => { caption: t('files', 'List of your files and folders.'), icon: FolderSvg, - order: 30, + order: 50, getContents, })) const folders = await getFolders() as Folder[] for (const folder of folders) { - Navigation.register(new View({ - id: generateFolderTreeId(folder), - parent: generateFolderTreeParentId(folder), - - name: folder.attributes.displayname ?? folder.basename, - - icon: FolderSvg, - order: 0, - - getContents: (path) => getFolderContents(folder, path.replace(folder.path, '')), - })) + registerFolderView(folder) } + + subscribe('files:node:created', onCreateNode) + subscribe('files:node:deleted', onDeleteNode) + subscribe('files:node:updated', onUpdateNode) } diff --git a/package-lock.json b/package-lock.json index fede735628107..1081732eca822 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@nextcloud/capabilities": "^1.2.0", "@nextcloud/dialogs": "^5.3.5", "@nextcloud/event-bus": "^3.3.1", - "@nextcloud/files": "^3.5.1", + "@nextcloud/files": "^3.6.0", "@nextcloud/initial-state": "^2.2.0", "@nextcloud/l10n": "^3.1.0", "@nextcloud/logger": "^3.0.2", @@ -4440,16 +4440,18 @@ } }, "node_modules/@nextcloud/files": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.5.1.tgz", - "integrity": "sha512-GkVWUgkBSVt27Carmp/DbnDiqHq03w3VQWt8xszacp/IQSB9G+8/KCvi8zxldac2q7lQ8NpHlB/Bqy8o+OOc0A==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.6.0.tgz", + "integrity": "sha512-/3kzEJ1TsCgjkSVhjdI+FnF0c2rvYtiTAQPoNqkNQYFa7Vbor+XPuypBQIJZFMDMzEgUexAL4QuQT3YmeSfBAA==", + "license": "AGPL-3.0-or-later", "dependencies": { "@nextcloud/auth": "^2.3.0", + "@nextcloud/capabilities": "^1.2.0", "@nextcloud/l10n": "^3.1.0", "@nextcloud/logger": "^3.0.2", "@nextcloud/paths": "^2.1.0", "@nextcloud/router": "^3.0.1", - "@nextcloud/sharing": "^0.2.1", + "@nextcloud/sharing": "^0.2.2", "cancelable-promise": "^4.3.1", "is-svg": "^5.0.1", "typescript-event-target": "^1.1.1", diff --git a/package.json b/package.json index 7949b954ee77b..6bd7fee220922 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "@nextcloud/capabilities": "^1.2.0", "@nextcloud/dialogs": "^5.3.5", "@nextcloud/event-bus": "^3.3.1", - "@nextcloud/files": "^3.5.1", + "@nextcloud/files": "^3.6.0", "@nextcloud/initial-state": "^2.2.0", "@nextcloud/l10n": "^3.1.0", "@nextcloud/logger": "^3.0.2",