diff options
Diffstat (limited to 'apps/files/src/views/Navigation.vue')
-rw-r--r-- | apps/files/src/views/Navigation.vue | 323 |
1 files changed, 137 insertions, 186 deletions
diff --git a/apps/files/src/views/Navigation.vue b/apps/files/src/views/Navigation.vue index 040e1482e32..0f3c3647c6e 100644 --- a/apps/files/src/views/Navigation.vue +++ b/apps/files/src/views/Navigation.vue @@ -1,45 +1,24 @@ <!-- - - @copyright Copyright (c) 2019 Gary Kim <gary@garykim.dev> - - - - @author Gary Kim <gary@garykim.dev> - - - - @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> - <template #list> - <NcAppNavigationItem v-for="view in parentViews" - :key="view.id" - :allow-collapse="true" - :data-cy-files-navigation-item="view.id" - :icon="view.iconClass" - :open="view.expanded" - :pinned="view.sticky" - :title="view.name" - :to="generateToNavigation(view)" - @update:open="onToggleExpand(view)"> - <NcAppNavigationItem v-for="child in childViews[view.id]" - :key="child.id" - :data-cy-files-navigation-item="child.id" - :exact="true" - :icon="child.iconClass" - :title="child.name" - :to="generateToNavigation(child)" /> - </NcAppNavigationItem> + <NcAppNavigation data-cy-files-navigation + class="files-navigation" + :aria-label="t('files', 'Files')"> + <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 --> @@ -49,54 +28,75 @@ <NavigationQuota /> <!-- Files settings modal toggle--> - <NcAppNavigationItem :aria-label="t('files', 'Open the files app settings')" - :title="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> -import { emit, subscribe } from '@nextcloud/event-bus' -import { generateUrl } from '@nextcloud/router' -import { translate } from '@nextcloud/l10n' - -import axios from '@nextcloud/axios' -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' +<script lang="ts"> +import type { View } from '@nextcloud/files' +import type { ViewConfig } from '../types.ts' -import logger from '../logger.js' -import Navigation from '../services/Navigation.ts' +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' + +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 { +export default defineComponent({ name: 'Navigation', components: { - Cog, + IconCog, + FilesNavigationItem, + FilesNavigationSearch, + + NavigationQuota, NcAppNavigation, NcAppNavigationItem, + NcAppNavigationList, SettingsModal, - NavigationQuota, }, - props: { - // eslint-disable-next-line vue/prop-name-casing - Navigation: { - type: Navigation, - required: true, - }, + setup() { + const filtersStore = useFiltersStore() + const viewConfigStore = useViewConfigStore() + const { currentView, views } = useNavigation() + + return { + currentView, + t, + views, + + filtersStore, + viewConfigStore, + } }, data() { @@ -106,134 +106,77 @@ export default { }, computed: { + /** + * The current view ID from the route params + */ currentViewId() { return this.$route?.params?.view || 'files' }, - /** @return {Navigation} */ - currentView() { - return this.views.find(view => view.id === this.currentViewId) - }, - - /** @return {Navigation[]} */ - views() { - return this.Navigation.views - }, - - /** @return {Navigation[]} */ - parentViews() { - return this.views - // filter child views - .filter(view => !view.parent) - // sort views by order - .sort((a, b) => { - return a.order - b.order - }) - }, - - /** @return {Navigation[]} */ - childViews() { + /** + * 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) { - logger.debug('View changed', { id: view.id, view }) - this.showView(view, oldView) + 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 }) + } }, }, - beforeMount() { - if (this.currentView) { - logger.debug('Navigation mounted. Showing requested view', { view: this.currentView }) - this.showView(this.currentView) - } + created() { + subscribe('files:folder-tree:initialized', this.loadExpandedViews) + subscribe('files:folder-tree:expanded', this.loadExpandedViews) + }, - subscribe('files:legacy-navigation:changed', this.onLegacyNavigationChanged) + beforeMount() { + // 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: { - /** - * @param {Navigation} view the new active view - * @param {Navigation} oldView the old active view - */ - showView(view, oldView) { - // Closing any opened sidebar - window?.OCA?.Files?.Sidebar?.close?.() - - if (view.legacy) { - const newAppContent = document.querySelector('#app-content #app-content-' + this.currentView.id + '.viewcontainer') - document.querySelectorAll('#app-content .viewcontainer').forEach(el => { - el.classList.add('hidden') - }) - newAppContent.classList.remove('hidden') - - // Triggering legacy navigation events - const { dir = '/' } = OC.Util.History.parseUrlQuery() - const params = { itemId: view.id, dir } - - logger.debug('Triggering legacy navigation event', params) - window.jQuery(newAppContent).trigger(new window.jQuery.Event('show', params)) - window.jQuery(newAppContent).trigger(new window.jQuery.Event('urlChanged', params)) - - } - - this.Navigation.setActive(view) - emit('files:navigation:changed', view) - }, - - /** - * Coming from the legacy files app. - * TODO: remove when all views are migrated. - * - * @param {Navigation} view the new active view - */ - onLegacyNavigationChanged({ id } = { id: 'files' }) { - const view = this.Navigation.views.find(view => view.id === id) - if (view && view.legacy && view.id !== this.currentView.id) { - // Force update the current route as the request comes - // from the legacy files app router - this.$router.replace({ ...this.$route, params: { view: view.id } }) - this.Navigation.setActive(view) - this.showView(view) + 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) } }, /** - * Expand/collapse a a view with children and permanently - * save this setting in the server. - * - * @param {Navigation} view the view to toggle - */ - onToggleExpand(view) { - // Invert state - view.expanded = !view.expanded - axios.post(generateUrl(`/apps/files/api/v1/toggleShowFolder/${view.id}`), { show: view.expanded }) - }, - - /** - * Generate the route to a view - * - * @param {Navigation} view the view to toggle + * Set the view as active on the navigation and handle internal state + * @param view View to set active */ - generateToNavigation(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) }, /** @@ -249,22 +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 > 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 { @@ -274,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> |