Skip to content

Commit

Permalink
fixup! [WIP] feat: Add folder tree to sidebar
Browse files Browse the repository at this point in the history
Signed-off-by: Christopher Ng <[email protected]>
  • Loading branch information
Pytal committed Jul 19, 2024
1 parent 8301f7e commit 5d7f6f0
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 58 deletions.
58 changes: 35 additions & 23 deletions apps/files/src/components/FilesNavigationItemList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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)">
<!-- Sanitized icon as svg if provided -->
<template v-if="view.icon" #icon>
<NcIconSvgWrapper :svg="view.icon" />
</template>

<!-- Child views if any -->
<component :is="'FilesNavigationItemList'"
v-if="hasChildViews(view)"
<!-- Recursively nested child views -->
<FilesNavigationItemList v-if="hasChildViews(view)"
:parent="view.id"
:level="level + 1"
:views="views.filter(view => view.parent !== parent)" />
:views="filterView(views, parent)" />
</NcAppNavigationItem>
</Fragment>
</template>
Expand All @@ -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',
Expand All @@ -63,38 +64,27 @@ export default defineComponent({
default: 0,
},
views: {
type: Array as PropType<View[]>,
default: () => [],
type: Object as PropType<Record<string, View[]>>,
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<string, View[]> {
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<string, View[]>)
return this.views[this.parent] ?? []
},
style() {
if (this.level === 0) {
if (this.level === 0 || this.level === 1) {
return null
}
return {
Expand All @@ -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
},
/**
Expand Down Expand Up @@ -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<string, View[]>, id: string): Record<string, View[]> {
return Object.fromEntries(
Object.entries(viewMap)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.filter(([viewId, _views]) => viewId !== id),
)
},
},
})
</script>
3 changes: 3 additions & 0 deletions apps/files/src/eventbus.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down
1 change: 1 addition & 0 deletions apps/files/src/newMenu/newFolder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
})

Expand Down
18 changes: 13 additions & 5 deletions apps/files/src/services/FolderTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { getContents, resultToNode } from './Files.ts'
export const folderTreeId = 'folders'

export const getFolders = (): CancelablePromise<Node[]> => {
const userId = getCurrentUser()?.uid
const userId = getCurrentUser()?.uid as string
const searchPayload = `
<?xml version="1.0"?>
<d:searchrequest ${getDavNameSpaces()}>
Expand Down Expand Up @@ -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}`
}
44 changes: 35 additions & 9 deletions apps/files/src/views/Navigation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<NcAppNavigation data-cy-files-navigation
:aria-label="t('files', 'Files')">
<template #list>
<FilesNavigationItemList :views="views" />
<FilesNavigationItemList :views="viewsMap" />
</template>

<!-- Non-scrollable navigation bottom elements -->
Expand Down Expand Up @@ -88,17 +88,33 @@ export default defineComponent({
currentViewId() {
return this.$route?.params?.view || 'files'
},
/**
* Map of parent ids to views
*/
viewsMap(): Record<string, View[]> {
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<string, View[]>)
},
},
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) {

Check failure on line 112 in apps/files/src/views/Navigation.vue

View workflow job for this annotation

GitHub Actions / NPM lint

'_newView' is defined but never used

Check failure on line 112 in apps/files/src/views/Navigation.vue

View workflow job for this annotation

GitHub Actions / NPM lint

'_oldView' is defined but never used
this.updateView()
},
views(_newViews, _oldViews) {

Check failure on line 116 in apps/files/src/views/Navigation.vue

View workflow job for this annotation

GitHub Actions / NPM lint

'_newViews' is defined but never used

Check failure on line 116 in apps/files/src/views/Navigation.vue

View workflow job for this annotation

GitHub Actions / NPM lint

'_oldViews' is defined but never used
this.updateView()
},
},
Expand All @@ -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
Expand Down
72 changes: 57 additions & 15 deletions apps/files/src/views/folderTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,73 @@
* 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'

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()

Expand All @@ -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)
}
12 changes: 7 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 5d7f6f0

Please sign in to comment.