aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files/src/components/FilesNavigationItem.vue
blob: 372a83e1441ec6889ab562271ae0088d26935481 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc 
<!--
  - 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
			:loading="view.loading"
			: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="(open) => onOpen(open, view)">
			<template v-if="view.icon" #icon>
				<NcIconSvgWrapper :svg="view.icon" />
			</template>

			<!-- Hack to force the collapse icon to be displayed -->
			<li v-if="view.loadChildViews && !view.loaded" style="display: none" />

			<!-- 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/components/NcAppNavigationItem'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'

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 open True if open
		 * @param view View
		 */
		async onOpen(open: boolean, 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)
			if (open && view.loadChildViews) {
				await view.loadChildViews(view)
			}
		},

		/**
		 * 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>
>("More details can be found in the webserver log.\n"); throw $ex; } OC_Template::printExceptionErrorPage($ex, 500); }