refactor(files): only load navigation views needed

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
Ferdinand Thiessen 2026-02-11 23:27:58 +01:00
parent 2f29ad8ff8
commit 69f2c17675
No known key found for this signature in database
GPG key ID: 7E849AE05218500F
6 changed files with 365 additions and 377 deletions

View file

@ -1,186 +0,0 @@
<!--
- 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 { View } from '@nextcloud/files'
import type { PropType } from 'vue'
import { defineComponent } from 'vue'
import { Fragment } from 'vue-frag'
import NcAppNavigationItem from '@nextcloud/vue/components/NcAppNavigationItem'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import { useActiveStore } from '../store/active.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 activeStore = useActiveStore()
const viewConfigStore = useViewConfigStore()
return {
activeStore,
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) => this.parent.params && view.params?.dir.startsWith(this.parent.params.dir))
}
return this.filterVisible(this.views[this.parent.id] ?? [])
},
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: {
filterVisible(views: View[]) {
return views.filter(({ id, hidden }) => id === this.activeStore.activeView?.id || hidden !== true)
},
hasChildViews(view: View): boolean {
if (this.level >= maxLevel) {
return false
}
return this.filterVisible(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>

View file

@ -0,0 +1,56 @@
<!--
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<script setup lang="ts">
import type { IView } from '@nextcloud/files'
import { getCanonicalLocale, getLanguage, t } from '@nextcloud/l10n'
import { computed } from 'vue'
import NcAppNavigationList from '@nextcloud/vue/components/NcAppNavigationList'
import FilesNavigationListItem from './FilesNavigationListItem.vue'
import { useVisibleViews } from '../composables/useViews.ts'
const views = useVisibleViews()
const rootViews = computed(() => views.value
.filter((view) => !view.parent)
.sort(sortViews))
const collator = Intl.Collator(
[getLanguage(), getCanonicalLocale()],
{ numeric: true, usage: 'sort' },
)
/**
* Sort views by their order property if available, otherwise sort alphabetically by name.
*
* @param a - first view
* @param b - second view
*/
function sortViews(a: IView, b: IView): number {
if (a.order !== undefined && b.order === undefined) {
return -1
} else if (a.order === undefined && b.order !== undefined) {
return 1
}
return collator.compare(a.name, b.name)
}
</script>
<template>
<NcAppNavigationList
:class="$style.filesNavigationList"
:aria-label="t('files', 'Views')">
<FilesNavigationListItem
v-for="view in rootViews"
:key="view.id"
:view="view" />
</NcAppNavigationList>
</template>
<style module>
.filesNavigationList {
height: 100%; /* Fill all available space for sticky views */
}
</style>

View file

@ -0,0 +1,162 @@
<!--
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<script setup lang="ts">
import type { IView } from '@nextcloud/files'
import { getCanonicalLocale, getLanguage } from '@nextcloud/l10n'
import { computed, onMounted, ref } from 'vue'
import NcAppNavigationItem from '@nextcloud/vue/components/NcAppNavigationItem'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import { useVisibleViews } from '../composables/useViews.ts'
import { folderTreeId } from '../services/FolderTree.ts'
import { useViewConfigStore } from '../store/viewConfig.ts'
const props = withDefaults(defineProps<{
view: IView
level?: number
}>(), {
level: 0,
})
/**
* Load child views on mount if the view is expanded by default
* but has no child views loaded yet.
*/
onMounted(() => {
if (isExpanded.value && !hasChildViews.value) {
loadChildViews()
}
})
const maxLevel = 6 // Limit nesting to not exceed max call stack size
const viewConfigStore = useViewConfigStore()
const viewConfig = computed(() => viewConfigStore.viewConfigs[props.view.id])
const isExpanded = computed(() => viewConfig.value
? (viewConfig.value.expanded === true)
: (props.view.expanded === true))
const views = useVisibleViews()
const childViews = computed(() => {
if (props.level < maxLevel) {
return views.value.filter((v) => v.parent === props.view.id)
} else {
return views.value.filter((v) => isDescendant(v, props.view.id))
}
/**
* Check if a view is a descendant of another view by recursively traversing up the parent chain.
*
* @param view - The view to check
* @param parent - The parent view id to check against
*/
function isDescendant(view: IView, parent: string): boolean {
if (!view.parent) {
return false
} else if (view.parent === parent) {
return true
}
const parentView = views.value.find((v) => v.id === view.parent)
return !!parentView && isDescendant(parentView, parent)
}
})
const sortedChildViews = computed(() => childViews.value.slice().sort((a, b) => {
if (a.order !== undefined && b.order === undefined) {
return -1
} else if (a.order === undefined && b.order !== undefined) {
return 1
}
return collator.compare(a.name, b.name)
}))
const hasChildViews = computed(() => childViews.value.length > 0)
const navigationRoute = computed(() => {
if (props.view.params) {
const { dir } = props.view.params
return { name: 'filelist', params: { ...props.view.params }, query: { dir } }
}
return { name: 'filelist', params: { view: props.view.id } }
})
const isLoading = ref(false)
const childViewsLoaded = ref(false)
/**
* Handle expanding/collapsing the navigation item.
*
* @param expanded - The expanded state
*/
async function onExpandCollapse(expanded: boolean) {
if (viewConfig.value) {
viewConfig.value.expanded = expanded
} else if (expanded) {
viewConfigStore.viewConfigs[props.view.id] = { expanded: true }
}
// folder tree should only show current directory by default,
// so we don't want to persist the expanded state in the store for its views
if (!props.view.id.startsWith(`${folderTreeId}::`)) {
viewConfigStore.update(props.view.id, 'expanded', expanded)
}
if (expanded) {
await loadChildViews()
}
}
/**
* Load child views if a loader function is provided and child views haven't been loaded yet.
*/
async function loadChildViews() {
if (props.view.loadChildViews && !childViewsLoaded.value) {
isLoading.value = true
try {
await props.view.loadChildViews(props.view)
childViewsLoaded.value = true
} finally {
isLoading.value = false
}
}
}
</script>
<script lang="ts">
const collator = Intl.Collator(
[getLanguage(), getCanonicalLocale()],
{ numeric: true, usage: 'sort' },
)
</script>
<template>
<NcAppNavigationItem
class="files-navigation__item"
allow-collapse
:loading="isLoading"
:data-cy-files-navigation-item="view.id"
:exact="hasChildViews /* eslint-disable-line @nextcloud/vue/no-deprecated-props */"
:name="view.name"
:open="isExpanded"
:pinned="view.sticky"
:to="navigationRoute"
@update:open="onExpandCollapse">
<template v-if="view.icon" #icon>
<NcIconSvgWrapper :svg="view.icon" />
</template>
<!-- Hack to force the collapse icon to be displayed -->
<li
v-if="!hasChildViews && !childViewsLoaded && view.loadChildViews"
v-show="false"
role="presentation" />
<!-- Recursively nest child views -->
<FilesNavigationListItem
v-for="childView in sortedChildViews"
:key="childView.id"
:level="level + 1"
:view="childView" />
</NcAppNavigationItem>
</template>

View file

@ -3,20 +3,19 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Navigation, View } from '@nextcloud/files'
import * as nextcloudFiles from '@nextcloud/files'
import { getNavigation, View } from '@nextcloud/files'
import { enableAutoDestroy, mount } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { defineComponent } from 'vue'
import { useViews } from './useViews.ts'
import { useViews, useVisibleViews } from './useViews.ts'
// Just a wrapper so we can test the composable
const TestComponent = defineComponent({
template: '<div></div>',
template: '<div />',
setup() {
return {
views: useViews(),
visibleViews: useVisibleViews(),
}
},
})
@ -24,40 +23,121 @@ const TestComponent = defineComponent({
enableAutoDestroy(afterEach)
describe('Composables: useViews', () => {
const spy = vi.spyOn(nextcloudFiles, 'getNavigation')
let navigation: Navigation
const navigation = getNavigation()
beforeEach(() => {
navigation = new nextcloudFiles.Navigation()
spy.mockImplementation(() => navigation)
const views = [...navigation.views]
for (const view of views) {
navigation.remove(view.id)
}
})
it('should return empty array without registered views', () => {
const wrapper = mount(TestComponent)
expect((wrapper.vm as unknown as { views: View[] }).views).toStrictEqual([])
expect(getViewsInWrapper(wrapper)).toStrictEqual([])
})
it('should return already registered views', () => {
const view = new nextcloudFiles.View({ getContents: () => Promise.reject(new Error()), icon: '<svg></svg>', id: 'view-1', name: 'My View 1', order: 0 })
const view = new View({ getContents: () => Promise.reject(new Error()), icon: '<svg></svg>', id: 'view-1', name: 'My View 1', order: 0 })
// register before mount
navigation.register(view)
// now mount and check that the view is listed
const wrapper = mount(TestComponent)
expect((wrapper.vm as unknown as { views: View[] }).views).toStrictEqual([view])
expect(getViewsInWrapper(wrapper)).toStrictEqual([view.id])
})
it('should be reactive on registering new views', () => {
const view = new nextcloudFiles.View({ getContents: () => Promise.reject(new Error()), icon: '<svg></svg>', id: 'view-1', name: 'My View 1', order: 0 })
const view2 = new nextcloudFiles.View({ getContents: () => Promise.reject(new Error()), icon: '<svg></svg>', id: 'view-2', name: 'My View 2', order: 1 })
const view = new View({ getContents: () => Promise.reject(new Error()), icon: '<svg></svg>', id: 'view-1', name: 'My View 1', order: 0 })
const view2 = new View({ getContents: () => Promise.reject(new Error()), icon: '<svg></svg>', id: 'view-2', name: 'My View 2', order: 1 })
// register before mount
navigation.register(view)
// now mount and check that the view is listed
const wrapper = mount(TestComponent)
expect((wrapper.vm as unknown as { views: View[] }).views).toStrictEqual([view])
expect(getViewsInWrapper(wrapper)).toStrictEqual([view.id])
// now register view 2 and check it is reactively added
navigation.register(view2)
expect((wrapper.vm as unknown as { views: View[] }).views).toStrictEqual([view, view2])
expect(getViewsInWrapper(wrapper)).toStrictEqual([view.id, view2.id])
})
})
describe('Composables: useVisibleViews', () => {
const navigation = getNavigation()
beforeEach(() => {
const views = [...navigation.views]
for (const view of views) {
navigation.remove(view.id)
}
})
it('should return empty array without registered views', () => {
const wrapper = mount(TestComponent)
expect(getVisibleViewsInWrapper(wrapper)).toStrictEqual([])
})
it('should return already registered views', () => {
const view = new View({ getContents: () => Promise.reject(new Error()), icon: '<svg></svg>', id: 'view-1', name: 'My View 1', order: 0 })
// register before mount
navigation.register(view)
// now mount and check that the view is listed
const wrapper = mount(TestComponent)
expect(getVisibleViewsInWrapper(wrapper)).toStrictEqual([view.id])
})
it('should ignore hidden views', () => {
const view = new View({ getContents: () => Promise.reject(new Error()), icon: '<svg></svg>', id: 'view-1', name: 'My View 1', order: 0, hidden: true })
// register before mount
navigation.register(view)
// now mount and check that the view is listed
const wrapper = mount(TestComponent)
expect(getVisibleViewsInWrapper(wrapper)).toStrictEqual([])
})
it('should ignore hidden views', () => {
const hiddenView = new View({ getContents: () => Promise.reject(new Error()), icon: '<svg></svg>', id: 'hidden', name: 'My hidden view', order: 0, hidden: true })
navigation.register(hiddenView)
const wrapper = mount(TestComponent)
const visibleView = new View({ getContents: () => Promise.reject(new Error()), icon: '<svg></svg>', id: 'view-1', name: 'My View 1', order: 0 })
navigation.register(visibleView)
expect(getVisibleViewsInWrapper(wrapper)).toStrictEqual([visibleView.id])
})
it('should be reactive on registering new views', () => {
const view = new View({ getContents: () => Promise.reject(new Error()), icon: '<svg></svg>', id: 'view-1', name: 'My View 1', order: 0 })
const view2 = new View({ getContents: () => Promise.reject(new Error()), icon: '<svg></svg>', id: 'view-2', name: 'My View 2', order: 1 })
// register before mount
navigation.register(view)
// now mount and check that the view is listed
const wrapper = mount(TestComponent)
expect(getVisibleViewsInWrapper(wrapper)).toStrictEqual([view.id])
// now register view 2 and check it is reactively added
navigation.register(view2)
expect(getVisibleViewsInWrapper(wrapper)).toStrictEqual([view.id, view2.id])
})
})
/**
* Get the view ids from the wrapper's component instance.
*
* @param wrapper - The wrapper
*/
function getViewsInWrapper(wrapper: ReturnType<typeof mount>) {
const vm = wrapper.vm as unknown as InstanceType<typeof TestComponent>
return vm.views.map((view) => view.id)
}
/**
* Get the visible (non-hidden) view ids from the wrapper's component instance.
*
* @param wrapper - The wrapper
*/
function getVisibleViewsInWrapper(wrapper: ReturnType<typeof mount>) {
const vm = wrapper.vm as unknown as InstanceType<typeof TestComponent>
return vm.visibleViews.map((view) => view.id)
}

View file

@ -3,34 +3,38 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { IView } from '@nextcloud/files'
import { getNavigation } from '@nextcloud/files'
import { createSharedComposable } from '@vueuse/core'
import { onUnmounted, shallowRef, triggerRef } from 'vue'
import { computed, shallowRef } from 'vue'
const allViews = shallowRef<IView[]>([])
const visibleViews = computed(() => allViews.value?.filter((view) => !view.hidden) ?? [])
let initialized = false
/**
* Composable to get the currently available views
* Get all currently registered views.
* Unline `Navigation.views` this is reactive and will update when new views are added or existing views are removed.
*/
export const useViews = createSharedComposable(useInternalViews)
export function useViews() {
if (!initialized) {
const navigation = getNavigation()
navigation.addEventListener('update', () => {
allViews.value = [...navigation.views]
})
/**
* Composable to get the currently available views
*/
export function useInternalViews() {
const navigation = getNavigation()
const views = shallowRef(navigation.views)
/**
* Event listener to update all registered views
*/
function onUpdateViews() {
views.value = navigation.views
triggerRef(views)
allViews.value = [...navigation.views]
initialized = true
}
navigation.addEventListener('update', onUpdateViews)
onUnmounted(() => {
navigation.removeEventListener('update', onUpdateViews)
})
return views
return allViews
}
/**
* Get all non-hidden views.
*/
export function useVisibleViews() {
useViews()
return visibleViews
}

View file

@ -11,17 +11,13 @@
<FilesNavigationSearch />
</template>
<template #default>
<NcAppNavigationList
class="files-navigation__list"
:aria-label="t('files', 'Views')">
<FilesNavigationItem :views="viewMap" />
</NcAppNavigationList>
<FilesNavigationList />
<!-- Settings modal-->
<FilesAppSettings
:open.sync="settingsOpened"
data-cy-files-navigation-settings
@close="onSettingsClose" />
@close="settingsOpened = false" />
</template>
<!-- Non-scrollable navigation bottom elements -->
@ -34,7 +30,7 @@
<NcAppNavigationItem
:name="t('files', 'Files settings')"
data-cy-files-navigation-settings-button
@click.prevent.stop="openSettings">
@click.prevent.stop="settingsOpened = true">
<IconCog slot="icon" :size="20" />
</NcAppNavigationItem>
</ul>
@ -42,163 +38,43 @@
</NcAppNavigation>
</template>
<script lang="ts">
import type { View } from '@nextcloud/files'
import type { ViewConfig } from '../types.ts'
import { emit, subscribe } from '@nextcloud/event-bus'
<script setup lang="ts">
import { emit } from '@nextcloud/event-bus'
import { getNavigation } from '@nextcloud/files'
import { getCanonicalLocale, getLanguage, t } from '@nextcloud/l10n'
import { defineComponent } from 'vue'
import { t } from '@nextcloud/l10n'
import { computed, ref, watchEffect } from 'vue'
import { useRoute } from 'vue-router/composables'
import NcAppNavigation from '@nextcloud/vue/components/NcAppNavigation'
import NcAppNavigationItem from '@nextcloud/vue/components/NcAppNavigationItem'
import NcAppNavigationList from '@nextcloud/vue/components/NcAppNavigationList'
import IconCog from 'vue-material-design-icons/CogOutline.vue'
import FilesNavigationItem from '../components/FilesNavigationItem.vue'
import FilesNavigationList from '../components/FilesNavigationList.vue'
import FilesNavigationSearch from '../components/FilesNavigationSearch.vue'
import NavigationQuota from '../components/NavigationQuota.vue'
import FilesAppSettings from './FilesAppSettings.vue'
import { useViews } from '../composables/useViews.ts'
import logger from '../logger.ts'
import { useActiveStore } from '../store/active.ts'
import { useFiltersStore } from '../store/filters.ts'
import { useSidebarStore } from '../store/sidebar.ts'
import { useViewConfigStore } from '../store/viewConfig.ts'
const collator = Intl.Collator(
[getLanguage(), getCanonicalLocale()],
{
numeric: true,
usage: 'sort',
},
)
const sidebar = useSidebarStore()
const activeStore = useActiveStore()
export default defineComponent({
name: 'FilesNavigation',
const settingsOpened = ref(false)
components: {
IconCog,
FilesAppSettings,
FilesNavigationItem,
FilesNavigationSearch,
const allViews = useViews()
NavigationQuota,
NcAppNavigation,
NcAppNavigationItem,
NcAppNavigationList,
},
setup() {
const sidebar = useSidebarStore()
const activeStore = useActiveStore()
const filtersStore = useFiltersStore()
const viewConfigStore = useViewConfigStore()
return {
t,
sidebar,
activeStore,
filtersStore,
viewConfigStore,
views: useViews(),
}
},
data() {
return {
settingsOpened: false,
}
},
computed: {
/**
* The current view ID from the route params
*/
currentViewId() {
return this.$route?.params?.view || 'files'
},
/**
* Map of parent ids to views
*/
viewMap(): Record<string, View[]> {
return this.views
.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 map
}, {} as Record<string, View[]>)
},
},
watch: {
currentViewId(newView, oldView) {
if (this.currentViewId !== this.activeStore.activeView?.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() {
// 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: {
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))
.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) {
this.sidebar.close()
const route = useRoute()
const currentViewId = computed(() => route?.params?.view || 'files')
watchEffect(() => {
if (currentViewId.value !== activeStore.activeView?.id) {
logger.debug(`Route view id ${currentViewId.value} is different from active view id ${activeStore.activeView?.id}, updating active view...`)
const view = allViews.value.find(({ id }) => id === currentViewId.value)!
if (view) {
sidebar.close()
getNavigation().setActive(view.id)
emit('files:navigation:changed', view)
},
/**
* Open the settings modal
*/
openSettings() {
this.settingsOpened = true
},
/**
* Close the settings modal
*/
onSettingsClose() {
this.settingsOpened = false
},
},
}
}
})
</script>
@ -223,10 +99,6 @@ export default defineComponent({
}
.files-navigation {
&__list {
height: 100%; // Fill all available space for sticky views
}
:deep(.app-navigation__content > ul.app-navigation__list) {
will-change: scroll-position;
}