mirror of
https://github.com/nextcloud/server.git
synced 2026-06-08 08:16:43 -04:00
feat: Navigate via folder tree
Signed-off-by: Christopher Ng <chrng8@gmail.com>
This commit is contained in:
parent
855a45650e
commit
3fabc4f733
9 changed files with 469 additions and 98 deletions
|
|
@ -109,12 +109,11 @@ export default defineComponent({
|
|||
return this.dirs.map((dir: string, index: number) => {
|
||||
const source = this.getFileSourceFromPath(dir)
|
||||
const node: Node | undefined = source ? this.getNodeFromSource(source) : undefined
|
||||
const to = { ...this.$route, params: { node: node?.fileid }, query: { dir } }
|
||||
return {
|
||||
dir,
|
||||
exact: true,
|
||||
name: this.getDirDisplayName(dir),
|
||||
to,
|
||||
to: this.getTo(dir, node),
|
||||
// disable drop on current directory
|
||||
disableDrop: index === this.dirs.length - 1,
|
||||
}
|
||||
|
|
@ -163,6 +162,20 @@ export default defineComponent({
|
|||
return node?.displayname || basename(path)
|
||||
},
|
||||
|
||||
getTo(dir: string, node?: Node): Record<string, unknown> {
|
||||
if (node === undefined) {
|
||||
return {
|
||||
...this.$route,
|
||||
query: { dir },
|
||||
}
|
||||
}
|
||||
return {
|
||||
...this.$route,
|
||||
params: { fileid: String(node.fileid) },
|
||||
query: { dir: node.path },
|
||||
}
|
||||
},
|
||||
|
||||
onClick(to) {
|
||||
if (to?.query?.dir === this.$route.query.dir) {
|
||||
this.$emit('reload')
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import { showError, showSuccess } from '@nextcloud/dialogs'
|
|||
import { emit } from '@nextcloud/event-bus'
|
||||
import { FileType, NodeStatus } from '@nextcloud/files'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { dirname } from '@nextcloud/paths'
|
||||
import { defineComponent, inject } from 'vue'
|
||||
|
||||
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
|
||||
|
|
@ -269,6 +270,10 @@ export default defineComponent({
|
|||
// Success 🎉
|
||||
emit('files:node:updated', this.source)
|
||||
emit('files:node:renamed', this.source)
|
||||
emit('files:node:moved', {
|
||||
node: this.source,
|
||||
oldSource: `${dirname(this.source.source)}/${oldName}`,
|
||||
})
|
||||
showSuccess(t('files', 'Renamed "{oldName}" to "{newName}"', { oldName, newName }))
|
||||
|
||||
// Reset the renaming store
|
||||
|
|
|
|||
170
apps/files/src/components/FilesNavigationItem.vue
Normal file
170
apps/files/src/components/FilesNavigationItem.vue
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<template>
|
||||
<Fragment>
|
||||
<NcAppNavigationItem v-for="view in currentViews"
|
||||
:key="view.id"
|
||||
class="files-navigation__item"
|
||||
allow-collapse
|
||||
:data-cy-files-navigation-item="view.id"
|
||||
:exact="useExactRouteMatching(view)"
|
||||
:icon="view.iconClass"
|
||||
:name="view.name"
|
||||
:open="isExpanded(view)"
|
||||
:pinned="view.sticky"
|
||||
:to="generateToNavigation(view)"
|
||||
:style="style"
|
||||
@update:open="onToggleExpand(view)">
|
||||
<template v-if="view.icon" #icon>
|
||||
<NcIconSvgWrapper :svg="view.icon" />
|
||||
</template>
|
||||
|
||||
<!-- Recursively nest child views -->
|
||||
<FilesNavigationItem v-if="hasChildViews(view)"
|
||||
:parent="view"
|
||||
:level="level + 1"
|
||||
:views="filterView(views, parent.id)" />
|
||||
</NcAppNavigationItem>
|
||||
</Fragment>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { PropType } from 'vue'
|
||||
import type { View } from '@nextcloud/files'
|
||||
|
||||
import { defineComponent } from 'vue'
|
||||
import { Fragment } from 'vue-frag'
|
||||
|
||||
import NcAppNavigationItem from '@nextcloud/vue/dist/Components/NcAppNavigationItem.js'
|
||||
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
|
||||
|
||||
import { useNavigation } from '../composables/useNavigation.js'
|
||||
import { useViewConfigStore } from '../store/viewConfig.js'
|
||||
|
||||
const maxLevel = 7 // Limit nesting to not exceed max call stack size
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FilesNavigationItem',
|
||||
|
||||
components: {
|
||||
Fragment,
|
||||
NcAppNavigationItem,
|
||||
NcIconSvgWrapper,
|
||||
},
|
||||
|
||||
props: {
|
||||
parent: {
|
||||
type: Object as PropType<View>,
|
||||
default: () => ({}),
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
views: {
|
||||
type: Object as PropType<Record<string, View[]>>,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
|
||||
setup() {
|
||||
const { currentView } = useNavigation()
|
||||
const viewConfigStore = useViewConfigStore()
|
||||
return {
|
||||
currentView,
|
||||
viewConfigStore,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
currentViews(): View[] {
|
||||
if (this.level >= maxLevel) { // Filter for all remaining decendants beyond the max level
|
||||
return (Object.values(this.views).reduce((acc, views) => [...acc, ...views], []) as View[])
|
||||
.filter(view => view.params?.dir.startsWith(this.parent.params?.dir))
|
||||
}
|
||||
return this.views[this.parent.id] ?? [] // Root level views have `undefined` parent ids
|
||||
},
|
||||
|
||||
style() {
|
||||
if (this.level === 0 || this.level === 1 || this.level > maxLevel) { // Left-align deepest entry with center of app navigation, do not add any more visual indentation after this level
|
||||
return null
|
||||
}
|
||||
return {
|
||||
'padding-left': '16px',
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
hasChildViews(view: View): boolean {
|
||||
if (this.level >= maxLevel) {
|
||||
return false
|
||||
}
|
||||
return this.views[view.id]?.length > 0
|
||||
},
|
||||
|
||||
/**
|
||||
* Only use exact route matching on routes with child views
|
||||
* Because if a view does not have children (like the files view) then multiple routes might be matched for it
|
||||
* Like for the 'files' view this does not work because of optional 'fileid' param so /files and /files/1234 are both in the 'files' view
|
||||
* @param view The view to check
|
||||
*/
|
||||
useExactRouteMatching(view: View): boolean {
|
||||
return this.hasChildViews(view)
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate the route to a view
|
||||
* @param view View to generate "to" navigation for
|
||||
*/
|
||||
generateToNavigation(view: View) {
|
||||
if (view.params) {
|
||||
const { dir } = view.params
|
||||
return { name: 'filelist', params: { ...view.params }, query: { dir } }
|
||||
}
|
||||
return { name: 'filelist', params: { view: view.id } }
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a view is expanded by user config
|
||||
* or fallback to the default value.
|
||||
* @param view View to check if expanded
|
||||
*/
|
||||
isExpanded(view: View): boolean {
|
||||
return typeof this.viewConfigStore.getConfig(view.id)?.expanded === 'boolean'
|
||||
? this.viewConfigStore.getConfig(view.id).expanded === true
|
||||
: view.expanded === true
|
||||
},
|
||||
|
||||
/**
|
||||
* Expand/collapse a a view with children and permanently
|
||||
* save this setting in the server.
|
||||
* @param view View to toggle
|
||||
*/
|
||||
onToggleExpand(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)
|
||||
},
|
||||
|
||||
/**
|
||||
* 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,9 +3,10 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import type { View } from '@nextcloud/files'
|
||||
import type { ShallowRef } from 'vue'
|
||||
|
||||
import { getNavigation } from '@nextcloud/files'
|
||||
import { onMounted, onUnmounted, shallowRef, type ShallowRef } from 'vue'
|
||||
import { onMounted, onUnmounted, shallowRef, triggerRef } from 'vue'
|
||||
|
||||
/**
|
||||
* Composable to get the currently active files view from the files navigation
|
||||
|
|
@ -28,6 +29,7 @@ export function useNavigation() {
|
|||
*/
|
||||
function onUpdateViews() {
|
||||
views.value = navigation.views
|
||||
triggerRef(views)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
|
|
|||
5
apps/files/src/eventbus.d.ts
vendored
5
apps/files/src/eventbus.d.ts
vendored
|
|
@ -11,7 +11,12 @@ declare module '@nextcloud/event-bus' {
|
|||
|
||||
'files:favorites:removed': Node
|
||||
'files:favorites:added': Node
|
||||
|
||||
'files:node:created': Node
|
||||
'files:node:deleted': Node
|
||||
'files:node:updated': Node
|
||||
'files:node:renamed': Node
|
||||
'files:node:moved': { node: Node, oldSource: string }
|
||||
|
||||
'files:filter:added': IFileListFilter
|
||||
'files:filter:removed': string
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import registerFavoritesView from './views/favorites'
|
|||
import registerRecentView from './views/recent'
|
||||
import registerPersonalFilesView from './views/personal-files'
|
||||
import registerFilesView from './views/files'
|
||||
import { registerFolderTreeView } from './views/folderTree.ts'
|
||||
import registerPreviewServiceWorker from './services/ServiceWorker.js'
|
||||
|
||||
import { initLivePhotos } from './services/LivePhotos'
|
||||
|
|
@ -53,6 +54,7 @@ registerFavoritesView()
|
|||
registerFilesView()
|
||||
registerRecentView()
|
||||
registerPersonalFilesView()
|
||||
registerFolderTreeView()
|
||||
|
||||
// Register file list filters
|
||||
registerHiddenFilesFilter()
|
||||
|
|
|
|||
90
apps/files/src/services/FolderTree.ts
Normal file
90
apps/files/src/services/FolderTree.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { ContentsWithRoot } from '@nextcloud/files'
|
||||
|
||||
import { CancelablePromise } from 'cancelable-promise'
|
||||
import {
|
||||
davRemoteURL,
|
||||
Folder,
|
||||
} from '@nextcloud/files'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import { dirname, encodePath } from '@nextcloud/paths'
|
||||
|
||||
import { getContents as getFiles } from './Files.ts'
|
||||
|
||||
export const folderTreeId = 'folders'
|
||||
export const sourceRoot = `${davRemoteURL}/files/${getCurrentUser()?.uid}`
|
||||
|
||||
interface TreeNodeData {
|
||||
id: number,
|
||||
displayName?: string,
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
children?: Tree,
|
||||
}
|
||||
|
||||
interface Tree {
|
||||
[basename: string]: TreeNodeData,
|
||||
}
|
||||
|
||||
export interface TreeNode {
|
||||
source: string,
|
||||
path: string,
|
||||
fileid: number,
|
||||
basename: string,
|
||||
displayName?: string,
|
||||
}
|
||||
|
||||
const getTreeNodes = (tree: Tree, nodes: TreeNode[] = [], currentPath: string = ''): TreeNode[] => {
|
||||
for (const basename in tree) {
|
||||
const path = `${currentPath}/${basename}`
|
||||
const node: TreeNode = {
|
||||
source: `${sourceRoot}${path}`,
|
||||
path,
|
||||
fileid: tree[basename].id,
|
||||
basename,
|
||||
displayName: tree[basename].displayName,
|
||||
}
|
||||
nodes.push(node)
|
||||
if (tree[basename].children) {
|
||||
getTreeNodes(tree[basename].children, nodes, path)
|
||||
}
|
||||
}
|
||||
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)
|
||||
return nodes
|
||||
}
|
||||
|
||||
export const getContents = (path: string): CancelablePromise<ContentsWithRoot> => getFiles(path)
|
||||
|
||||
export const encodeSource = (source: string): string => {
|
||||
const { origin } = new URL(source)
|
||||
return origin + encodePath(source.slice(origin.length))
|
||||
}
|
||||
|
||||
export const getSourceParent = (source: string): string => {
|
||||
const parent = dirname(source)
|
||||
if (parent === sourceRoot) {
|
||||
return folderTreeId
|
||||
}
|
||||
return encodeSource(parent)
|
||||
}
|
||||
|
||||
export const getFolderTreeViewId = (folder: Folder): string => {
|
||||
return folder.encodedSource
|
||||
}
|
||||
|
||||
export const getFolderTreeParentId = (folder: Folder): string => {
|
||||
if (folder.dirname === '/') {
|
||||
return folderTreeId
|
||||
}
|
||||
return dirname(folder.encodedSource)
|
||||
}
|
||||
|
|
@ -4,38 +4,14 @@
|
|||
-->
|
||||
<template>
|
||||
<NcAppNavigation data-cy-files-navigation
|
||||
class="files-navigation"
|
||||
:aria-label="t('files', 'Files')">
|
||||
<template #search>
|
||||
<NcAppNavigationSearch v-model="searchQuery" :label="t('files', 'Filter filenames…')" />
|
||||
</template>
|
||||
<template #default>
|
||||
<NcAppNavigationList :aria-label="t('files', 'Views')">
|
||||
<NcAppNavigationItem v-for="view in parentViews"
|
||||
:key="view.id"
|
||||
:allow-collapse="true"
|
||||
:data-cy-files-navigation-item="view.id"
|
||||
:exact="useExactRouteMatching(view)"
|
||||
:icon="view.iconClass"
|
||||
:name="view.name"
|
||||
:open="isExpanded(view)"
|
||||
:pinned="view.sticky"
|
||||
:to="generateToNavigation(view)"
|
||||
@update:open="onToggleExpand(view)">
|
||||
<!-- Sanitized icon as svg if provided -->
|
||||
<NcIconSvgWrapper v-if="view.icon" slot="icon" :svg="view.icon" />
|
||||
|
||||
<!-- Child views if any -->
|
||||
<NcAppNavigationItem v-for="child in childViews[view.id]"
|
||||
:key="child.id"
|
||||
:data-cy-files-navigation-item="child.id"
|
||||
:exact-path="true"
|
||||
:icon="child.iconClass"
|
||||
:name="child.name"
|
||||
:to="generateToNavigation(child)">
|
||||
<!-- Sanitized icon as svg if provided -->
|
||||
<NcIconSvgWrapper v-if="child.icon" slot="icon" :svg="child.icon" />
|
||||
</NcAppNavigationItem>
|
||||
</NcAppNavigationItem>
|
||||
<FilesNavigationItem :views="viewMap" />
|
||||
</NcAppNavigationList>
|
||||
|
||||
<!-- Settings modal-->
|
||||
|
|
@ -65,7 +41,7 @@
|
|||
import type { View } from '@nextcloud/files'
|
||||
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { translate as t, getCanonicalLocale, getLanguage } from '@nextcloud/l10n'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
import IconCog from 'vue-material-design-icons/Cog.vue'
|
||||
|
|
@ -73,9 +49,9 @@ import NcAppNavigation from '@nextcloud/vue/dist/Components/NcAppNavigation.js'
|
|||
import NcAppNavigationItem from '@nextcloud/vue/dist/Components/NcAppNavigationItem.js'
|
||||
import NcAppNavigationList from '@nextcloud/vue/dist/Components/NcAppNavigationList.js'
|
||||
import NcAppNavigationSearch from '@nextcloud/vue/dist/Components/NcAppNavigationSearch.js'
|
||||
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
|
||||
import NavigationQuota from '../components/NavigationQuota.vue'
|
||||
import SettingsModal from './Settings.vue'
|
||||
import FilesNavigationItem from '../components/FilesNavigationItem.vue'
|
||||
|
||||
import { useNavigation } from '../composables/useNavigation'
|
||||
import { useFilenameFilter } from '../composables/useFilenameFilter'
|
||||
|
|
@ -83,18 +59,26 @@ import { useFiltersStore } from '../store/filters.ts'
|
|||
import { useViewConfigStore } from '../store/viewConfig.ts'
|
||||
import logger from '../logger.ts'
|
||||
|
||||
const collator = Intl.Collator(
|
||||
[getLanguage(), getCanonicalLocale()],
|
||||
{
|
||||
numeric: true,
|
||||
usage: 'sort',
|
||||
},
|
||||
)
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Navigation',
|
||||
|
||||
components: {
|
||||
IconCog,
|
||||
FilesNavigationItem,
|
||||
|
||||
NavigationQuota,
|
||||
NcAppNavigation,
|
||||
NcAppNavigationItem,
|
||||
NcAppNavigationList,
|
||||
NcAppNavigationSearch,
|
||||
NcIconSvgWrapper,
|
||||
SettingsModal,
|
||||
},
|
||||
|
||||
|
|
@ -129,28 +113,21 @@ export default defineComponent({
|
|||
return this.$route?.params?.view || 'files'
|
||||
},
|
||||
|
||||
parentViews(): View[] {
|
||||
/**
|
||||
* Map of parent ids to views
|
||||
*/
|
||||
viewMap(): Record<string, View[]> {
|
||||
return this.views
|
||||
// filter child views
|
||||
.filter(view => !view.parent)
|
||||
// sort views by order
|
||||
.sort((a, b) => {
|
||||
return a.order - b.order
|
||||
})
|
||||
},
|
||||
|
||||
childViews(): Record<string, View[]> {
|
||||
return this.views
|
||||
// filter parent views
|
||||
.filter(view => !!view.parent)
|
||||
// create a map of parents and their children
|
||||
.reduce((list, view) => {
|
||||
list[view.parent!] = [...(list[view.parent!] || []), view]
|
||||
// Sort children by order
|
||||
list[view.parent!].sort((a, b) => {
|
||||
return a.order - b.order
|
||||
.reduce((map, view) => {
|
||||
map[view.parent!] = [...(map[view.parent!] || []), view]
|
||||
// TODO Allow undefined order for natural sort
|
||||
map[view.parent!].sort((a, b) => {
|
||||
if (typeof a.order === 'number' || typeof b.order === 'number') {
|
||||
return (a.order ?? 0) - (b.order ?? 0)
|
||||
}
|
||||
return collator.compare(a.name, b.name)
|
||||
})
|
||||
return list
|
||||
return map
|
||||
}, {} as Record<string, View[]>)
|
||||
},
|
||||
},
|
||||
|
|
@ -175,16 +152,6 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Only use exact route matching on routes with child views
|
||||
* Because if a view does not have children (like the files view) then multiple routes might be matched for it
|
||||
* Like for the 'files' view this does not work because of optional 'fileid' param so /files and /files/1234 are both in the 'files' view
|
||||
* @param view The view to check
|
||||
*/
|
||||
useExactRouteMatching(view: View): boolean {
|
||||
return this.childViews[view.id]?.length > 0
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the view as active on the navigation and handle internal state
|
||||
* @param view View to set active
|
||||
|
|
@ -196,42 +163,6 @@ export default defineComponent({
|
|||
emit('files:navigation:changed', view)
|
||||
},
|
||||
|
||||
/**
|
||||
* Expand/collapse a a view with children and permanently
|
||||
* save this setting in the server.
|
||||
* @param view View to toggle
|
||||
*/
|
||||
onToggleExpand(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)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a view is expanded by user config
|
||||
* or fallback to the default value.
|
||||
* @param view View to check if expanded
|
||||
*/
|
||||
isExpanded(view: View): boolean {
|
||||
return typeof this.viewConfigStore.getConfig(view.id)?.expanded === 'boolean'
|
||||
? this.viewConfigStore.getConfig(view.id).expanded === true
|
||||
: view.expanded === true
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate the route to a view
|
||||
* @param view View to generate "to" navigation for
|
||||
*/
|
||||
generateToNavigation(view: View) {
|
||||
if (view.params) {
|
||||
const { dir } = view.params
|
||||
return { name: 'filelist', params: view.params, query: { dir } }
|
||||
}
|
||||
return { name: 'filelist', params: { view: view.id } }
|
||||
},
|
||||
|
||||
/**
|
||||
* Open the settings modal
|
||||
*/
|
||||
|
|
@ -272,4 +203,10 @@ export default defineComponent({
|
|||
// Prevent shrinking or growing
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.files-navigation {
|
||||
:deep(.app-navigation__content > ul.app-navigation__list) {
|
||||
will-change: scroll-position;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
147
apps/files/src/views/folderTree.ts
Normal file
147
apps/files/src/views/folderTree.ts
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { TreeNode } from '../services/FolderTree.ts'
|
||||
|
||||
import { Folder, Node, View, getNavigation } from '@nextcloud/files'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { subscribe } from '@nextcloud/event-bus'
|
||||
import { isSamePath } from '@nextcloud/paths'
|
||||
|
||||
import FolderSvg from '@mdi/svg/svg/folder.svg?raw'
|
||||
import FolderMultipleSvg from '@mdi/svg/svg/folder-multiple.svg?raw'
|
||||
|
||||
import {
|
||||
encodeSource,
|
||||
folderTreeId,
|
||||
getContents,
|
||||
getFolderTreeNodes,
|
||||
getFolderTreeParentId,
|
||||
getFolderTreeViewId,
|
||||
getSourceParent,
|
||||
sourceRoot,
|
||||
} from '../services/FolderTree.ts'
|
||||
|
||||
const Navigation = getNavigation()
|
||||
|
||||
const registerTreeNodeView = (node: TreeNode) => {
|
||||
Navigation.register(new View({
|
||||
id: encodeSource(node.source),
|
||||
parent: getSourceParent(node.source),
|
||||
|
||||
name: node.displayName ?? node.basename,
|
||||
|
||||
icon: FolderSvg,
|
||||
order: 0, // TODO Allow undefined order for natural sort
|
||||
|
||||
getContents,
|
||||
|
||||
params: {
|
||||
view: folderTreeId,
|
||||
fileid: String(node.fileid), // Needed for matching exact routes
|
||||
dir: node.path,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
const registerFolderView = (folder: Folder) => {
|
||||
Navigation.register(new View({
|
||||
id: getFolderTreeViewId(folder),
|
||||
parent: getFolderTreeParentId(folder),
|
||||
|
||||
name: folder.displayname,
|
||||
|
||||
icon: FolderSvg,
|
||||
order: 0, // TODO Allow undefined order for natural sort
|
||||
|
||||
getContents,
|
||||
|
||||
params: {
|
||||
view: folderTreeId,
|
||||
fileid: String(folder.fileid),
|
||||
dir: folder.path,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
const removeFolderView = (folder: Folder) => {
|
||||
const viewId = getFolderTreeViewId(folder)
|
||||
Navigation.remove(viewId)
|
||||
}
|
||||
|
||||
const removeFolderViewSource = (source: string) => {
|
||||
const Navigation = getNavigation()
|
||||
Navigation.remove(source)
|
||||
}
|
||||
|
||||
const onCreateNode = (node: Node) => {
|
||||
if (!(node instanceof Folder)) {
|
||||
return
|
||||
}
|
||||
registerFolderView(node)
|
||||
}
|
||||
|
||||
const onDeleteNode = (node: Node) => {
|
||||
if (!(node instanceof Folder)) {
|
||||
return
|
||||
}
|
||||
removeFolderView(node)
|
||||
}
|
||||
|
||||
const onMoveNode = ({ node, oldSource }) => {
|
||||
if (!(node instanceof Folder)) {
|
||||
return
|
||||
}
|
||||
removeFolderViewSource(oldSource)
|
||||
registerFolderView(node)
|
||||
|
||||
const newPath = node.source.replace(sourceRoot, '')
|
||||
const oldPath = oldSource.replace(sourceRoot, '')
|
||||
const childViews = Navigation.views.filter(view => {
|
||||
if (!view.params?.dir) {
|
||||
return false
|
||||
}
|
||||
if (isSamePath(view.params.dir, oldPath)) {
|
||||
return false
|
||||
}
|
||||
return view.params.dir.startsWith(oldPath)
|
||||
})
|
||||
for (const view of childViews) {
|
||||
// @ts-expect-error FIXME Allow setting parent
|
||||
view.parent = getFolderTreeParentId(node)
|
||||
// @ts-expect-error dir param is defined
|
||||
view.params.dir = view.params.dir.replace(oldPath, newPath)
|
||||
}
|
||||
}
|
||||
|
||||
const registerFolderTreeRoot = () => {
|
||||
Navigation.register(new View({
|
||||
id: folderTreeId,
|
||||
|
||||
name: t('files', 'All folders'),
|
||||
caption: t('files', 'List of your files and folders.'),
|
||||
|
||||
icon: FolderMultipleSvg,
|
||||
order: 50, // Below all other views
|
||||
|
||||
getContents,
|
||||
}))
|
||||
}
|
||||
|
||||
const registerFolderTreeChildren = async () => {
|
||||
const nodes = await getFolderTreeNodes()
|
||||
for (const node of nodes) {
|
||||
registerTreeNodeView(node)
|
||||
}
|
||||
|
||||
subscribe('files:node:created', onCreateNode)
|
||||
subscribe('files:node:deleted', onDeleteNode)
|
||||
subscribe('files:node:moved', onMoveNode)
|
||||
}
|
||||
|
||||
export const registerFolderTreeView = async () => {
|
||||
registerFolderTreeRoot()
|
||||
await registerFolderTreeChildren()
|
||||
}
|
||||
Loading…
Reference in a new issue