aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files/src/components/FilesNavigationItem.vue
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files/src/components/FilesNavigationItem.vue')
-rw-r--r--apps/files/src/components/FilesNavigationItem.vue170
1 files changed, 170 insertions, 0 deletions
diff --git a/apps/files/src/components/FilesNavigationItem.vue b/apps/files/src/components/FilesNavigationItem.vue
new file mode 100644
index 00000000000..75507803957
--- /dev/null
+++ b/apps/files/src/components/FilesNavigationItem.vue
@@ -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>