diff options
Diffstat (limited to 'apps/files/src/views/Navigation.vue')
-rw-r--r-- | apps/files/src/views/Navigation.vue | 292 |
1 files changed, 127 insertions, 165 deletions
diff --git a/apps/files/src/views/Navigation.vue b/apps/files/src/views/Navigation.vue index 9e7a630128e..0f3c3647c6e 100644 --- a/apps/files/src/views/Navigation.vue +++ b/apps/files/src/views/Navigation.vue @@ -1,54 +1,24 @@ <!-- - - @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com> - - - - @author John Molakvoæ <skjnldsv@protonmail.com> - - - - @license GNU AGPL version 3 or any later version - - - - This program is free software: you can redistribute it and/or modify - - it under the terms of the GNU Affero General Public License as - - published by the Free Software Foundation, either version 3 of the - - License, or (at your option) any later version. - - - - This program is distributed in the hope that it will be useful, - - but WITHOUT ANY WARRANTY; without even the implied warranty of - - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - - GNU Affero General Public License for more details. - - - - You should have received a copy of the GNU Affero General Public License - - along with this program. If not, see <http://www.gnu.org/licenses/>. - - - --> + - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> <template> <NcAppNavigation data-cy-files-navigation + class="files-navigation" :aria-label="t('files', 'Files')"> - <template #list> - <NcAppNavigationItem v-for="view in parentViews" - :key="view.id" - :allow-collapse="true" - :data-cy-files-navigation-item="view.id" - :exact="true" - :icon="view.iconClass" - :name="view.name" - :open="isExpanded(view)" - :pinned="view.sticky" - :to="generateToNavigation(view)" - @update:open="onToggleExpand(view)"> - <!-- Sanitized icon as svg if provided --> - <NcIconSvgWrapper v-if="view.icon" slot="icon" :svg="view.icon" /> - - <!-- Child views if any --> - <NcAppNavigationItem v-for="child in childViews[view.id]" - :key="child.id" - :data-cy-files-navigation-item="child.id" - :exact="true" - :icon="child.iconClass" - :name="child.name" - :to="generateToNavigation(child)"> - <!-- Sanitized icon as svg if provided --> - <NcIconSvgWrapper v-if="child.icon" slot="icon" :svg="child.icon" /> - </NcAppNavigationItem> - </NcAppNavigationItem> + <template #search> + <FilesNavigationSearch /> + </template> + <template #default> + <NcAppNavigationList class="files-navigation__list" + :aria-label="t('files', 'Views')"> + <FilesNavigationItem :views="viewMap" /> + </NcAppNavigationList> + + <!-- Settings modal--> + <SettingsModal :open.sync="settingsOpened" + data-cy-files-navigation-settings + @close="onSettingsClose" /> </template> <!-- Non-scrollable navigation bottom elements --> @@ -58,60 +28,73 @@ <NavigationQuota /> <!-- Files settings modal toggle--> - <NcAppNavigationItem :aria-label="t('files', 'Open the files app settings')" - :name="t('files', 'Files settings')" + <NcAppNavigationItem :name="t('files', 'Files settings')" data-cy-files-navigation-settings-button @click.prevent.stop="openSettings"> - <Cog slot="icon" :size="20" /> + <IconCog slot="icon" :size="20" /> </NcAppNavigationItem> </ul> </template> - - <!-- Settings modal--> - <SettingsModal :open="settingsOpened" - data-cy-files-navigation-settings - @close="onSettingsClose" /> </NcAppNavigation> </template> <script lang="ts"> -import { emit, subscribe } from '@nextcloud/event-bus' -import { translate } from '@nextcloud/l10n' -import Cog 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 type { View } from '@nextcloud/files' +import type { ViewConfig } from '../types.ts' -import { setPageHeading } from '../../../../core/src/OCP/accessibility.js' -import { useViewConfigStore } from '../store/viewConfig.ts' -import logger from '../logger.js' -import type { Navigation, View } from '@nextcloud/files' +import { emit, subscribe } from '@nextcloud/event-bus' +import { getNavigation } from '@nextcloud/files' +import { t, getCanonicalLocale, getLanguage } from '@nextcloud/l10n' +import { defineComponent } from 'vue' + +import IconCog from 'vue-material-design-icons/CogOutline.vue' +import NcAppNavigation from '@nextcloud/vue/components/NcAppNavigation' +import NcAppNavigationItem from '@nextcloud/vue/components/NcAppNavigationItem' +import NcAppNavigationList from '@nextcloud/vue/components/NcAppNavigationList' import NavigationQuota from '../components/NavigationQuota.vue' import SettingsModal from './Settings.vue' +import FilesNavigationItem from '../components/FilesNavigationItem.vue' +import FilesNavigationSearch from '../components/FilesNavigationSearch.vue' -export default { +import { useNavigation } from '../composables/useNavigation' +import { useFiltersStore } from '../store/filters.ts' +import { useViewConfigStore } from '../store/viewConfig.ts' +import logger from '../logger.ts' + +const collator = Intl.Collator( + [getLanguage(), getCanonicalLocale()], + { + numeric: true, + usage: 'sort', + }, +) + +export default defineComponent({ name: 'Navigation', components: { - Cog, + IconCog, + FilesNavigationItem, + FilesNavigationSearch, + NavigationQuota, NcAppNavigation, NcAppNavigationItem, - NcIconSvgWrapper, + NcAppNavigationList, SettingsModal, }, - props: { - // eslint-disable-next-line vue/prop-name-casing - Navigation: { - type: Object as Navigation, - required: true, - }, - }, - setup() { + const filtersStore = useFiltersStore() const viewConfigStore = useViewConfigStore() + const { currentView, views } = useNavigation() + return { + currentView, + t, + views, + + filtersStore, viewConfigStore, } }, @@ -123,102 +106,77 @@ export default { }, computed: { + /** + * The current view ID from the route params + */ currentViewId() { return this.$route?.params?.view || 'files' }, - currentView(): View { - return this.views.find(view => view.id === this.currentViewId) - }, - - views(): View[] { - return this.Navigation.views - }, - - 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(): View[] { + /** + * Map of parent ids to views + */ + viewMap(): Record<string, View[]> { 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 + .reduce((map, view) => { + map[view.parent!] = [...(map[view.parent!] || []), view] + map[view.parent!].sort((a, b) => { + if (typeof a.order === 'number' || typeof b.order === 'number') { + return (a.order ?? 0) - (b.order ?? 0) + } + return collator.compare(a.name, b.name) }) - return list - }, {}) + return map + }, {} as Record<string, View[]>) }, }, watch: { - currentView(view, oldView) { - if (view.id !== oldView?.id) { - this.Navigation.setActive(view) - logger.debug('Navigation changed', { id: view.id, view }) - + currentViewId(newView, oldView) { + if (this.currentViewId !== this.currentView?.id) { + // This is guaranteed to be a view because `currentViewId` falls back to the default 'files' view + const view = this.views.find(({ id }) => id === this.currentViewId)! + // The new view as active this.showView(view) + logger.debug(`Navigation changed from ${oldView} to ${newView}`, { to: view }) } }, }, + created() { + subscribe('files:folder-tree:initialized', this.loadExpandedViews) + subscribe('files:folder-tree:expanded', this.loadExpandedViews) + }, + beforeMount() { - if (this.currentView) { - logger.debug('Navigation mounted. Showing requested view', { view: this.currentView }) - this.showView(this.currentView) - } + // This is guaranteed to be a view because `currentViewId` falls back to the default 'files' view + const view = this.views.find(({ id }) => id === this.currentViewId)! + this.showView(view) + logger.debug('Navigation mounted. Showing requested view', { view }) }, methods: { - showView(view: View) { - // Closing any opened sidebar - window?.OCA?.Files?.Sidebar?.close?.() - this.Navigation.setActive(view) - setPageHeading(view.name) - emit('files:navigation:changed', view) - }, - - /** - * Expand/collapse a a view with children and permanently - * save this setting in the server. - */ - 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. - */ - isExpanded(view: View): boolean { - return typeof this.viewConfigStore.getConfig(view.id)?.expanded === 'boolean' - ? this.viewConfigStore.getConfig(view.id).expanded === true - : view.expanded === true + async loadExpandedViews() { + const viewsToLoad: View[] = (Object.entries(this.viewConfigStore.viewConfigs) as Array<[string, ViewConfig]>) + .filter(([, config]) => config.expanded === true) + .map(([viewId]) => this.views.find(view => view.id === viewId)) + // eslint-disable-next-line no-use-before-define + .filter(Boolean as unknown as ((u: unknown) => u is View)) + .filter((view) => view.loadChildViews && !view.loaded) + for (const view of viewsToLoad) { + await view.loadChildViews(view) + } }, /** - * Generate the route to a view + * Set the view as active on the navigation and handle internal state + * @param view View to set active */ - generateToNavigation(view: View) { - if (view.params) { - const { dir, fileid } = view.params - return { name: 'filelist', params: view.params, query: { dir, fileid } } - } - return { name: 'filelist', params: { view: view.id } } + showView(view: View) { + // Closing any opened sidebar + window.OCA?.Files?.Sidebar?.close?.() + getNavigation().setActive(view) + emit('files:navigation:changed', view) }, /** @@ -234,26 +192,20 @@ export default { onSettingsClose() { this.settingsOpened = false }, - - t: translate, }, -} +}) </script> <style scoped lang="scss"> -// TODO: remove when https://github.com/nextcloud/nextcloud-vue/pull/3539 is in -.app-navigation::v-deep .app-navigation-entry-icon { - background-repeat: no-repeat; - background-position: center; -} - -.app-navigation::v-deep .app-navigation-entry.active .button-vue.icon-collapse:not(:hover) { - color: var(--color-primary-element-text); -} - -.app-navigation > ul.app-navigation__list { - // Use flex gap value for more elegant spacing - padding-bottom: var(--default-grid-baseline, 4px); +.app-navigation { + :deep(.app-navigation-entry.active .button-vue.icon-collapse:not(:hover)) { + color: var(--color-primary-element-text); + } + + > ul.app-navigation__list { + // Use flex gap value for more elegant spacing + padding-bottom: var(--default-grid-baseline, 4px); + } } .app-navigation-entry__settings { @@ -263,4 +215,14 @@ export default { // Prevent shrinking or growing flex: 0 0 auto; } + +.files-navigation { + &__list { + height: 100%; // Fill all available space for sticky views + } + + :deep(.app-navigation__content > ul.app-navigation__list) { + will-change: scroll-position; + } +} </style> |