aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files/src/views/Navigation.vue
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files/src/views/Navigation.vue')
-rw-r--r--apps/files/src/views/Navigation.vue323
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>