diff options
Diffstat (limited to 'apps/files/src/views/Navigation.vue')
-rw-r--r-- | apps/files/src/views/Navigation.vue | 265 |
1 files changed, 120 insertions, 145 deletions
diff --git a/apps/files/src/views/Navigation.vue b/apps/files/src/views/Navigation.vue index 71e9bf38068..0f3c3647c6e 100644 --- a/apps/files/src/views/Navigation.vue +++ b/apps/files/src/views/Navigation.vue @@ -4,34 +4,21 @@ --> <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="useExactRouteMatching(view)" - :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-path="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 --> @@ -41,52 +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 type { View } from '@nextcloud/files' +import type { ViewConfig } from '../types.ts' -import { emit } 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 { emit, subscribe } from '@nextcloud/event-bus' +import { getNavigation } from '@nextcloud/files' +import { t, getCanonicalLocale, getLanguage } from '@nextcloud/l10n' +import { defineComponent } from 'vue' -import { useViewConfigStore } from '../store/viewConfig.ts' -import logger from '../logger.js' +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, - NcIconSvgWrapper, + NcAppNavigationList, SettingsModal, }, setup() { + const filtersStore = useFiltersStore() const viewConfigStore = useViewConfigStore() + const { currentView, views } = useNavigation() + return { + currentView, + t, + views, + + filtersStore, viewConfigStore, } }, @@ -98,117 +106,80 @@ 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(): Record<string, 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 from ${oldView.id} to ${view.id}`, { from: oldView, to: 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: { - /** - * 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.childViews[view.id]?.length > 0 + 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) + } }, + /** + * Set the view as active on the navigation and handle internal state + * @param view View to set active + */ showView(view: View) { // Closing any opened sidebar - window?.OCA?.Files?.Sidebar?.close?.() - this.$navigation.setActive(view) + window.OCA?.Files?.Sidebar?.close?.() + getNavigation().setActive(view) emit('files:navigation:changed', view) }, /** - * 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) - }, - - /** - * 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 - }, - - /** - * 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 } } - }, - - /** * Open the settings modal */ openSettings() { @@ -221,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 { @@ -250,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> |