diff --git a/apps/files/src/components/FilesNavigationItemList.vue b/apps/files/src/components/FilesNavigationItemList.vue new file mode 100644 index 0000000000000..33289c49b1570 --- /dev/null +++ b/apps/files/src/components/FilesNavigationItemList.vue @@ -0,0 +1,158 @@ + + + + + diff --git a/apps/files/src/composables/useNavigation.ts b/apps/files/src/composables/useNavigation.ts index f410aec895fa8..2bf25f84fa344 100644 --- a/apps/files/src/composables/useNavigation.ts +++ b/apps/files/src/composables/useNavigation.ts @@ -3,16 +3,17 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ import type { View } from '@nextcloud/files' +import type { Ref, ShallowRef, UnwrapRef } from 'vue' import { getNavigation } from '@nextcloud/files' -import { onMounted, onUnmounted, shallowRef, type ShallowRef } from 'vue' +import { onMounted, onUnmounted, ref, shallowRef } from 'vue' /** * Composable to get the currently active files view from the files navigation */ export function useNavigation() { const navigation = getNavigation() - const views: ShallowRef = shallowRef(navigation.views) + const views: Ref> = ref(navigation.views) const currentView: ShallowRef = shallowRef(navigation.active) /** diff --git a/apps/files/src/init.ts b/apps/files/src/init.ts index 25bcc1072f0f4..9e55aa2ee4e2a 100644 --- a/apps/files/src/init.ts +++ b/apps/files/src/init.ts @@ -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' @@ -48,6 +49,7 @@ registerFavoritesView() registerFilesView() registerRecentView() registerPersonalFilesView() +registerFolderTreeView() // Register preview service worker registerPreviewServiceWorker() diff --git a/apps/files/src/services/FolderTree.ts b/apps/files/src/services/FolderTree.ts new file mode 100644 index 0000000000000..4d8faba37d072 --- /dev/null +++ b/apps/files/src/services/FolderTree.ts @@ -0,0 +1,82 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { ResponseDataDetailed, SearchResult } from 'webdav' +import type { ContentsWithRoot, Folder, Node } from '@nextcloud/files' + +import { CancelablePromise } from 'cancelable-promise' +import { getCurrentUser } from '@nextcloud/auth' +import { getDavNameSpaces, getDavProperties } from '@nextcloud/files' +import { joinPaths } from '@nextcloud/paths' + +import { client } from './WebdavClient.ts' +import { getContents, resultToNode } from './Files.ts' + +export const folderTreeId = 'folders' + +export const getFolders = (): CancelablePromise => { + const userId = getCurrentUser()?.uid + const searchPayload = ` + + + + + + ${getDavProperties()} + + + + + + /files/${userId} + infinity + + + + + + + + + `.trim() + + const controller = new AbortController() + return new CancelablePromise(async (resolve, reject, onCancel) => { + onCancel(() => controller.abort()) + + try { + const { data } = await client.search('/', { + signal: controller.signal, + details: true, + data: searchPayload, + }) as ResponseDataDetailed + const nodes = data.results.map(resultToNode) + resolve(nodes) + } catch (error) { + reject(error) + } + }) +} + +/** + * Get contents at path relative to folder + * + * @param folder The folder + * @param path The path + */ +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 generateFolderTreeParentId = (folder: Folder): string => { + if (folder.dirname === '/') { + return folderTreeId + } + return `${folderTreeId}-${folder.attributes.parentid}` // FIXME parentid collision +} diff --git a/apps/files/src/views/Navigation.vue b/apps/files/src/views/Navigation.vue index b69c6d5f7f2b1..ecb0d88e42b58 100644 --- a/apps/files/src/views/Navigation.vue +++ b/apps/files/src/views/Navigation.vue @@ -6,32 +6,7 @@ @@ -67,9 +42,9 @@ import { defineComponent } from 'vue' import IconCog from 'vue-material-design-icons/Cog.vue' import NcAppNavigation from '@nextcloud/vue/dist/Components/NcAppNavigation.js' import NcAppNavigationItem from '@nextcloud/vue/dist/Components/NcAppNavigationItem.js' -import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js' import NavigationQuota from '../components/NavigationQuota.vue' import SettingsModal from './Settings.vue' +import FilesNavigationItemList from '../components/FilesNavigationItemList.vue' import { useNavigation } from '../composables/useNavigation' import { useViewConfigStore } from '../store/viewConfig.ts' @@ -80,11 +55,11 @@ export default defineComponent({ components: { IconCog, + FilesNavigationItemList, NavigationQuota, NcAppNavigation, NcAppNavigationItem, - NcIconSvgWrapper, SettingsModal, }, @@ -113,31 +88,6 @@ export default defineComponent({ currentViewId() { return this.$route?.params?.view || 'files' }, - - parentViews(): 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 { - 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 - }) - return list - }, {} as Record) - }, }, watch: { @@ -162,16 +112,6 @@ export default defineComponent({ methods: { t, - /** - * 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 @@ -183,42 +123,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 */ diff --git a/apps/files/src/views/folderTree.ts b/apps/files/src/views/folderTree.ts new file mode 100644 index 0000000000000..33777f92da88f --- /dev/null +++ b/apps/files/src/views/folderTree.ts @@ -0,0 +1,56 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { Folder, 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, + getFolderContents, + getFolders, +} from '../services/FolderTree.ts' + +registerDavProperty('nc:parentid', { nc: 'http://nextcloud.org/ns' }) + +// FIXME Fix `dir` query param +// FIXME Duplicate root breaccrumbs + +export const registerFolderTreeView = async () => { + const Navigation = getNavigation() + + // TODO Update active view when navigating in tree + + Navigation.register(new View({ + id: folderTreeId, + + name: t('files', 'All folders'), + caption: t('files', 'List of your files and folders.'), + + icon: FolderSvg, + order: 30, + + 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, '')), + })) + } +}