Skip to content

Commit

Permalink
feat: Navigate via folder tree
Browse files Browse the repository at this point in the history
Signed-off-by: Christopher Ng <[email protected]>
  • Loading branch information
Pytal committed Jul 25, 2024
1 parent e73ded9 commit a9c145b
Show file tree
Hide file tree
Showing 12 changed files with 564 additions and 102 deletions.
19 changes: 17 additions & 2 deletions apps/files/src/actions/openFolderAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { Permission, Node, FileType, View, FileAction, DefaultType } from '@nextcloud/files'
import { Permission, Node, FileType, View, FileAction, DefaultType, Folder } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import FolderSvg from '@mdi/svg/svg/folder.svg?raw'

import { folderTreeId, getFolderTreeViewId } from '../services/FolderTree.ts'

export const action = new FileAction({
id: 'open-folder',
displayName(files: Node[]) {
Expand Down Expand Up @@ -36,8 +38,21 @@ export const action = new FileAction({
return false
}

if (view.id === folderTreeId || view.params?.isFolderTreeChild === 'true') { // Navigate into folder tree views directly
if (!(node instanceof Folder)) {
return
}
const viewId = getFolderTreeViewId(node)
window.OCP.Files.Router.goToRoute(
'filelist',
{ view: viewId, fileid: String(node.fileid) },
{ dir: node.path },
)
return null
}

window.OCP.Files.Router.goToRoute(
null,
'filelist',
{ view: view.id, fileid: String(node.fileid) },
{ dir: node.path },
)
Expand Down
47 changes: 44 additions & 3 deletions apps/files/src/components/BreadCrumbs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,19 @@ import { defineComponent } from 'vue'
import { Permission } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import HomeSvg from '@mdi/svg/svg/home.svg?raw'
import FolderMultipleSvg from '@mdi/svg/svg/folder-multiple.svg?raw'
import NcBreadcrumb from '@nextcloud/vue/dist/Components/NcBreadcrumb.js'
import NcBreadcrumbs from '@nextcloud/vue/dist/Components/NcBreadcrumbs.js'
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
import { useNavigation } from '../composables/useNavigation'
import { onDropInternalFiles, dataTransferToFileTree, onDropExternalFiles } from '../services/DropService'
import { folderTreeId, getFolderTreeViewId } from '../services/FolderTree.js'
import { showError } from '@nextcloud/dialogs'
import { useDragAndDropStore } from '../store/dragging.ts'
import { useFilesStore } from '../store/files.ts'
import { usePathsStore } from '../store/paths.ts'
import { useFolderTreeStore } from '../store/folderTree.ts'
import { useSelectionStore } from '../store/selection.ts'
import { useUploaderStore } from '../store/uploader.ts'
import filesListWidthMixin from '../mixins/filesListWidth.ts'
Expand Down Expand Up @@ -81,6 +84,7 @@ export default defineComponent({
const draggingStore = useDragAndDropStore()
const filesStore = useFilesStore()
const pathsStore = usePathsStore()
const folderTreeStore = useFolderTreeStore()
const selectionStore = useSelectionStore()
const uploaderStore = useUploaderStore()
const { currentView } = useNavigation()
Expand All @@ -89,6 +93,7 @@ export default defineComponent({
draggingStore,
filesStore,
pathsStore,
folderTreeStore,
selectionStore,
uploaderStore,
Expand All @@ -109,18 +114,21 @@ 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,
}
})
},
isInFolderTree() {
return this.currentView?.id === folderTreeId || this.currentView?.params?.isFolderTreeChild === 'true'
},
isUploadInProgress(): boolean {
return this.uploaderStore.queue.length !== 0
},
Expand All @@ -134,6 +142,9 @@ export default defineComponent({
// used to show the views icon for the first breadcrumb
viewIcon(): string {
if (this.isInFolderTree) {
return FolderMultipleSvg
}
return this.currentView?.icon ?? HomeSvg
},
Expand All @@ -151,10 +162,13 @@ export default defineComponent({
return this.filesStore.getNode(source)
},
getFileSourceFromPath(path: string): FileSource | null {
return (this.currentView && this.pathsStore.getPath(this.currentView.id, path)) ?? null
return (this.currentView && this.pathsStore.getPath(this.currentView.id, path)) ?? this.folderTreeStore.getPath(path)
},
getDirDisplayName(path: string): string {
if (path === '/') {
if (this.isInFolderTree) {
return t('files', 'All folders')
}
return this.$navigation?.active?.name || t('files', 'Home')
}
Expand All @@ -163,6 +177,33 @@ export default defineComponent({
return node?.displayname || basename(path)
},
getTo(dir: string, node?: Node): Record<string, unknown> {
if (this.isInFolderTree && dir === '/') {
return {
name: 'filelist',
params: { view: folderTreeId },
}
}
if (node === undefined) {
return {
...this.$route,
query: { dir },
}
}
if (this.isInFolderTree) {
return {
name: 'filelist',
params: { view: getFolderTreeViewId(node), fileid: String(node.fileid) },
query: { dir: node.path },
}
}
return {
...this.$route,
params: { fileid: node.fileid },
query: { dir: node.path },
}
},
onClick(to) {
if (to?.query?.dir === this.$route.query.dir) {
this.$emit('reload')
Expand Down
6 changes: 4 additions & 2 deletions apps/files/src/components/FileEntry/FileEntryName.vue
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,10 @@ export default defineComponent({
})
// Success 🎉
emit('files:node:updated', this.source)
emit('files:node:renamed', this.source)
const node = this.source
node.attributes['old-name'] = oldName
emit('files:node:updated', node)
emit('files:node:renamed', node)
showSuccess(t('files', 'Renamed "{oldName}" to "{newName}"', { oldName, newName }))
// Reset the renaming store
Expand Down
170 changes: 170 additions & 0 deletions apps/files/src/components/FilesNavigationItem.vue
Original file line number Diff line number Diff line change
@@ -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>
4 changes: 3 additions & 1 deletion apps/files/src/composables/useNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,6 +29,7 @@ export function useNavigation() {
*/
function onUpdateViews() {
views.value = navigation.views
triggerRef(views)
}

onMounted(() => {
Expand Down
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
2 changes: 2 additions & 0 deletions apps/files/src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,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'
Expand All @@ -48,6 +49,7 @@ registerFavoritesView()
registerFilesView()
registerRecentView()
registerPersonalFilesView()
registerFolderTreeView()

// Register preview service worker
registerPreviewServiceWorker()
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
Loading

0 comments on commit a9c145b

Please sign in to comment.