|
| 1 | +<!-- |
| 2 | + - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors |
| 3 | + - SPDX-License-Identifier: AGPL-3.0-or-later |
| 4 | +--> |
| 5 | + |
| 6 | +<template> |
| 7 | + <Fragment> |
| 8 | + <NcAppNavigationItem v-for="view in currentViews" |
| 9 | + :key="view.id" |
| 10 | + class="files-navigation__item" |
| 11 | + allow-collapse |
| 12 | + :data-cy-files-navigation-item="view.id" |
| 13 | + :exact="useExactRouteMatching(view)" |
| 14 | + :icon="view.iconClass" |
| 15 | + :name="view.name" |
| 16 | + :open="isExpanded(view)" |
| 17 | + :pinned="view.sticky" |
| 18 | + :to="generateToNavigation(view)" |
| 19 | + :style="style" |
| 20 | + @update:open="onToggleExpand(view)"> |
| 21 | + <template v-if="view.icon" #icon> |
| 22 | + <NcIconSvgWrapper :svg="view.icon" /> |
| 23 | + </template> |
| 24 | + |
| 25 | + <!-- Recursively nest child views --> |
| 26 | + <FilesNavigationItem v-if="hasChildViews(view)" |
| 27 | + :parent="view" |
| 28 | + :level="level + 1" |
| 29 | + :views="filterView(views, parent.id)" /> |
| 30 | + </NcAppNavigationItem> |
| 31 | + </Fragment> |
| 32 | +</template> |
| 33 | + |
| 34 | +<script lang="ts"> |
| 35 | +import type { PropType } from 'vue' |
| 36 | +import type { View } from '@nextcloud/files' |
| 37 | +
|
| 38 | +import { defineComponent } from 'vue' |
| 39 | +import { Fragment } from 'vue-frag' |
| 40 | +
|
| 41 | +import NcAppNavigationItem from '@nextcloud/vue/dist/Components/NcAppNavigationItem.js' |
| 42 | +import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js' |
| 43 | +
|
| 44 | +import { useNavigation } from '../composables/useNavigation.js' |
| 45 | +import { useViewConfigStore } from '../store/viewConfig.js' |
| 46 | +
|
| 47 | +const maxLevel = 7 // Limit nesting to not exceed max call stack size |
| 48 | +
|
| 49 | +export default defineComponent({ |
| 50 | + name: 'FilesNavigationItem', |
| 51 | +
|
| 52 | + components: { |
| 53 | + Fragment, |
| 54 | + NcAppNavigationItem, |
| 55 | + NcIconSvgWrapper, |
| 56 | + }, |
| 57 | +
|
| 58 | + props: { |
| 59 | + parent: { |
| 60 | + type: Object as PropType<View>, |
| 61 | + default: () => ({}), |
| 62 | + }, |
| 63 | + level: { |
| 64 | + type: Number, |
| 65 | + default: 0, |
| 66 | + }, |
| 67 | + views: { |
| 68 | + type: Object as PropType<Record<string, View[]>>, |
| 69 | + default: () => ({}), |
| 70 | + }, |
| 71 | + }, |
| 72 | +
|
| 73 | + setup() { |
| 74 | + const { currentView } = useNavigation() |
| 75 | + const viewConfigStore = useViewConfigStore() |
| 76 | + return { |
| 77 | + currentView, |
| 78 | + viewConfigStore, |
| 79 | + } |
| 80 | + }, |
| 81 | +
|
| 82 | + computed: { |
| 83 | + currentViews(): View[] { |
| 84 | + if (this.level >= maxLevel) { // Filter for all remaining decendants beyond the max level |
| 85 | + return (Object.values(this.views).reduce((acc, views) => [...acc, ...views], []) as View[]) |
| 86 | + .filter(view => view.params?.dir.startsWith(this.parent.params?.dir)) |
| 87 | + } |
| 88 | + return this.views[this.parent.id] ?? [] // Root level views have `undefined` parent ids |
| 89 | + }, |
| 90 | +
|
| 91 | + style() { |
| 92 | + 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 |
| 93 | + return null |
| 94 | + } |
| 95 | + return { |
| 96 | + 'padding-left': '16px', |
| 97 | + } |
| 98 | + }, |
| 99 | + }, |
| 100 | +
|
| 101 | + methods: { |
| 102 | + hasChildViews(view: View): boolean { |
| 103 | + if (this.level >= maxLevel) { |
| 104 | + return false |
| 105 | + } |
| 106 | + return this.views[view.id]?.length > 0 |
| 107 | + }, |
| 108 | +
|
| 109 | + /** |
| 110 | + * Only use exact route matching on routes with child views |
| 111 | + * Because if a view does not have children (like the files view) then multiple routes might be matched for it |
| 112 | + * 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 |
| 113 | + * @param view The view to check |
| 114 | + */ |
| 115 | + useExactRouteMatching(view: View): boolean { |
| 116 | + return this.hasChildViews(view) |
| 117 | + }, |
| 118 | +
|
| 119 | + /** |
| 120 | + * Generate the route to a view |
| 121 | + * @param view View to generate "to" navigation for |
| 122 | + */ |
| 123 | + generateToNavigation(view: View) { |
| 124 | + if (view.params) { |
| 125 | + const { dir } = view.params |
| 126 | + return { name: 'filelist', params: { ...view.params }, query: { dir } } |
| 127 | + } |
| 128 | + return { name: 'filelist', params: { view: view.id } } |
| 129 | + }, |
| 130 | +
|
| 131 | + /** |
| 132 | + * Check if a view is expanded by user config |
| 133 | + * or fallback to the default value. |
| 134 | + * @param view View to check if expanded |
| 135 | + */ |
| 136 | + isExpanded(view: View): boolean { |
| 137 | + return typeof this.viewConfigStore.getConfig(view.id)?.expanded === 'boolean' |
| 138 | + ? this.viewConfigStore.getConfig(view.id).expanded === true |
| 139 | + : view.expanded === true |
| 140 | + }, |
| 141 | +
|
| 142 | + /** |
| 143 | + * Expand/collapse a a view with children and permanently |
| 144 | + * save this setting in the server. |
| 145 | + * @param view View to toggle |
| 146 | + */ |
| 147 | + onToggleExpand(view: View) { |
| 148 | + // Invert state |
| 149 | + const isExpanded = this.isExpanded(view) |
| 150 | + // Update the view expanded state, might not be necessary |
| 151 | + view.expanded = !isExpanded |
| 152 | + this.viewConfigStore.update(view.id, 'expanded', !isExpanded) |
| 153 | + }, |
| 154 | +
|
| 155 | + /** |
| 156 | + * Return the view map with the specified view id removed |
| 157 | + * |
| 158 | + * @param viewMap Map of views |
| 159 | + * @param id View id |
| 160 | + */ |
| 161 | + filterView(viewMap: Record<string, View[]>, id: string): Record<string, View[]> { |
| 162 | + return Object.fromEntries( |
| 163 | + Object.entries(viewMap) |
| 164 | + // eslint-disable-next-line @typescript-eslint/no-unused-vars |
| 165 | + .filter(([viewId, _views]) => viewId !== id), |
| 166 | + ) |
| 167 | + }, |
| 168 | + }, |
| 169 | +}) |
| 170 | +</script> |
0 commit comments