diff --git a/apps/files/src/components/FilesNavigationItem.vue b/apps/files/src/components/FilesNavigationItem.vue
deleted file mode 100644
index f6c3a1e2828..00000000000
--- a/apps/files/src/components/FilesNavigationItem.vue
+++ /dev/null
@@ -1,186 +0,0 @@
-
-
-
-
- onOpen(open, view)">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/files/src/components/FilesNavigationList.vue b/apps/files/src/components/FilesNavigationList.vue
new file mode 100644
index 00000000000..ec911e14366
--- /dev/null
+++ b/apps/files/src/components/FilesNavigationList.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/files/src/components/FilesNavigationListItem.vue b/apps/files/src/components/FilesNavigationListItem.vue
new file mode 100644
index 00000000000..0eed07c64e4
--- /dev/null
+++ b/apps/files/src/components/FilesNavigationListItem.vue
@@ -0,0 +1,162 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/files/src/composables/useViews.spec.ts b/apps/files/src/composables/useViews.spec.ts
index 9ab4d126c13..c263dbc0221 100644
--- a/apps/files/src/composables/useViews.spec.ts
+++ b/apps/files/src/composables/useViews.spec.ts
@@ -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: '
',
+ template: '',
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: '', id: 'view-1', name: 'My View 1', order: 0 })
+ const view = new View({ getContents: () => Promise.reject(new Error()), icon: '', 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: '', id: 'view-1', name: 'My View 1', order: 0 })
- const view2 = new nextcloudFiles.View({ getContents: () => Promise.reject(new Error()), icon: '', id: 'view-2', name: 'My View 2', order: 1 })
+ const view = new View({ getContents: () => Promise.reject(new Error()), icon: '', id: 'view-1', name: 'My View 1', order: 0 })
+ const view2 = new View({ getContents: () => Promise.reject(new Error()), icon: '', 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: '', 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: '', 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: '', 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: '', 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: '', id: 'view-1', name: 'My View 1', order: 0 })
+ const view2 = new View({ getContents: () => Promise.reject(new Error()), icon: '', 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) {
+ const vm = wrapper.vm as unknown as InstanceType
+ 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) {
+ const vm = wrapper.vm as unknown as InstanceType
+ return vm.visibleViews.map((view) => view.id)
+}
diff --git a/apps/files/src/composables/useViews.ts b/apps/files/src/composables/useViews.ts
index 8f5e231f488..ca411756d22 100644
--- a/apps/files/src/composables/useViews.ts
+++ b/apps/files/src/composables/useViews.ts
@@ -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([])
+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
}
diff --git a/apps/files/src/views/FilesNavigation.vue b/apps/files/src/views/FilesNavigation.vue
index f070049923d..293e4176bb0 100644
--- a/apps/files/src/views/FilesNavigation.vue
+++ b/apps/files/src/views/FilesNavigation.vue
@@ -11,17 +11,13 @@
-
-
-
+
+ @close="settingsOpened = false" />
@@ -34,7 +30,7 @@
+ @click.prevent.stop="settingsOpened = true">
@@ -42,163 +38,43 @@
-
@@ -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;
}