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, '')),
+ }))
+ }
+}