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 @@ - - - - - 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 @@ @@ -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; }