refactor: Use composable for currentView and views to make it reactive when shared with other Vue apps

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
Ferdinand Thiessen 2024-06-21 15:48:37 +02:00
parent dea5559d35
commit 3ed32ffbb4
No known key found for this signature in database
GPG key ID: 45FAE7268762B400
13 changed files with 325 additions and 136 deletions

View file

@ -35,6 +35,7 @@
<script lang="ts">
import type { Node } from '@nextcloud/files'
import type { FileSource } from '../types.ts'
import { basename } from 'path'
import { defineComponent } from 'vue'
@ -45,6 +46,7 @@ import NcBreadcrumb from '@nextcloud/vue/dist/Components/NcBreadcrumb.js'
import NcBreadcrumbs from '@nextcloud/vue/dist/Components/NcBreadcrumbs.js'
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
import { useNavigation } from '../composables/useNavigation'
import { onDropInternalFiles, dataTransferToFileTree, onDropExternalFiles } from '../services/DropService'
import { showError } from '@nextcloud/dialogs'
import { useDragAndDropStore } from '../store/dragging.ts'
@ -54,7 +56,6 @@ import { useSelectionStore } from '../store/selection.ts'
import { useUploaderStore } from '../store/uploader.ts'
import filesListWidthMixin from '../mixins/filesListWidth.ts'
import logger from '../logger'
import type { FileSource } from '../types.ts'
export default defineComponent({
name: 'BreadCrumbs',
@ -82,6 +83,7 @@ export default defineComponent({
const pathsStore = usePathsStore()
const selectionStore = useSelectionStore()
const uploaderStore = useUploaderStore()
const { currentView } = useNavigation()
return {
draggingStore,
@ -89,14 +91,12 @@ export default defineComponent({
pathsStore,
selectionStore,
uploaderStore,
currentView,
}
},
computed: {
currentView() {
return this.$navigation.active
},
dirs(): string[] {
const cumulativePath = (acc: string) => (value: string) => (acc += `${value}/`)
// Generate a cumulative path for each path segment: ['/', '/foo', '/foo/bar', ...] etc
@ -150,15 +150,15 @@ export default defineComponent({
getNodeFromSource(source: FileSource): Node | undefined {
return this.filesStore.getNode(source)
},
getFileSourceFromPath(path: string): FileSource | undefined {
return this.pathsStore.getPath(this.currentView?.id, path)
getFileSourceFromPath(path: string): FileSource | null {
return (this.currentView && this.pathsStore.getPath(this.currentView.id, path)) ?? null
},
getDirDisplayName(path: string): string {
if (path === '/') {
return this.$navigation?.active?.name || t('files', 'Home')
}
const source: FileSource | undefined = this.getFileSourceFromPath(path)
const source: FileSource | null = this.getFileSourceFromPath(path)
const node: Node | undefined = source ? this.getNodeFromSource(source) : undefined
return node?.attributes?.displayname || basename(path)
},
@ -170,6 +170,10 @@ export default defineComponent({
},
onDragOver(event: DragEvent, path: string) {
if (!event.dataTransfer) {
return
}
// Cannot drop on the current directory
if (path === this.dirs[this.dirs.length - 1]) {
event.dataTransfer.dropEffect = 'none'

View file

@ -26,16 +26,18 @@
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { Folder, Permission } from '@nextcloud/files'
import type { Folder } from '@nextcloud/files'
import { Permission } from '@nextcloud/files'
import { showError } from '@nextcloud/dialogs'
import { translate as t } from '@nextcloud/l10n'
import { UploadStatus } from '@nextcloud/upload'
import { defineComponent, type PropType } from 'vue'
import TrayArrowDownIcon from 'vue-material-design-icons/TrayArrowDown.vue'
import logger from '../logger.js'
import { useNavigation } from '../composables/useNavigation'
import { dataTransferToFileTree, onDropExternalFiles } from '../services/DropService'
import logger from '../logger.js'
export default defineComponent({
name: 'DragAndDropNotice',
@ -46,11 +48,19 @@ export default defineComponent({
props: {
currentFolder: {
type: Folder,
type: Object as PropType<Folder>,
required: true,
},
},
setup() {
const { currentView } = useNavigation()
return {
currentView,
}
},
data() {
return {
dragover: false,
@ -58,10 +68,6 @@ export default defineComponent({
},
computed: {
currentView() {
return this.$navigation.active
},
/**
* Check if the current folder has create permissions
*/

View file

@ -86,9 +86,10 @@
<script lang="ts">
import { defineComponent } from 'vue'
import { Permission, formatFileSize } from '@nextcloud/files'
import { formatFileSize } from '@nextcloud/files'
import moment from '@nextcloud/moment'
import { useNavigation } from '../composables/useNavigation'
import { useActionsMenuStore } from '../store/actionsmenu.ts'
import { useDragAndDropStore } from '../store/dragging.ts'
import { useFilesStore } from '../store/files.ts'
@ -140,12 +141,16 @@ export default defineComponent({
const filesStore = useFilesStore()
const renamingStore = useRenamingStore()
const selectionStore = useSelectionStore()
const { currentView } = useNavigation()
return {
actionsMenuStore,
draggingStore,
filesStore,
renamingStore,
selectionStore,
currentView,
}
},
@ -179,21 +184,22 @@ export default defineComponent({
},
size() {
const size = parseInt(this.source.size, 10)
if (typeof size !== 'number' || isNaN(size) || size < 0) {
const size = this.source.size
if (!size || size < 0) {
return this.t('files', 'Pending')
}
return formatFileSize(size, true)
},
sizeOpacity() {
const maxOpacitySize = 10 * 1024 * 1024
const size = parseInt(this.source.size, 10)
const size = this.source.size
if (!size || isNaN(size) || size < 0) {
return {}
}
const ratio = Math.round(Math.min(100, 100 * Math.pow((this.source.size / maxOpacitySize), 2)))
const ratio = Math.round(Math.min(100, 100 * Math.pow((size / maxOpacitySize), 2)))
return {
color: `color-mix(in srgb, var(--color-main-text) ${ratio}%, var(--color-text-maxcontrast))`,
}

View file

@ -76,11 +76,13 @@
</template>
<script lang="ts">
import type { PropType } from 'vue'
import type { PropType, ShallowRef } from 'vue'
import type { FileAction, Node, View } from '@nextcloud/files'
import { DefaultType, FileAction, Node, NodeStatus, View, getFileActions } from '@nextcloud/files'
import { DefaultType, NodeStatus, getFileActions } from '@nextcloud/files'
import { showError, showSuccess } from '@nextcloud/dialogs'
import { translate as t } from '@nextcloud/l10n'
import { defineComponent } from 'vue'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
@ -88,8 +90,8 @@ import NcActionSeparator from '@nextcloud/vue/dist/Components/NcActionSeparator.
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
import ArrowLeftIcon from 'vue-material-design-icons/ArrowLeft.vue'
import Vue, { defineComponent } from 'vue'
import { useNavigation } from '../../composables/useNavigation'
import CustomElementRender from '../CustomElementRender.vue'
import logger from '../../logger.js'
@ -132,6 +134,15 @@ export default defineComponent({
},
},
setup() {
const { currentView } = useNavigation()
return {
// The file list is guaranteed to be only shown with active view
currentView: currentView as ShallowRef<View>,
}
},
data() {
return {
openedSubmenu: null as FileAction | null,
@ -143,9 +154,6 @@ export default defineComponent({
// Remove any trailing slash but leave root slash
return (this.$route?.query?.dir?.toString() || '/').replace(/^(.+)\/$/, '$1')
},
currentView(): View {
return this.$navigation.active as View
},
isLoading() {
return this.source.status === NodeStatus.LOADING
},
@ -269,7 +277,7 @@ export default defineComponent({
try {
// Set the loading marker
this.$emit('update:loading', action.id)
Vue.set(this.source, 'status', NodeStatus.LOADING)
this.$set(this.source, 'status', NodeStatus.LOADING)
const success = await action.exec(this.source, this.currentView, this.currentDir)
@ -289,7 +297,7 @@ export default defineComponent({
} finally {
// Reset the loading marker
this.$emit('update:loading', '')
Vue.set(this.source, 'status', undefined)
this.$set(this.source, 'status', undefined)
// If that was a submenu, we just go back after the action
if (isSubmenu) {

View file

@ -37,7 +37,7 @@
</template>
<script lang="ts">
import type { Node, View } from '@nextcloud/files'
import type { Node } from '@nextcloud/files'
import type { PropType } from 'vue'
import { showError, showSuccess } from '@nextcloud/dialogs'
@ -46,10 +46,11 @@ import { FileType, NodeStatus, Permission } from '@nextcloud/files'
import { loadState } from '@nextcloud/initial-state'
import { translate as t } from '@nextcloud/l10n'
import axios, { isAxiosError } from '@nextcloud/axios'
import Vue, { defineComponent } from 'vue'
import { defineComponent } from 'vue'
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
import { useNavigation } from '../../composables/useNavigation'
import { useRenamingStore } from '../../store/renaming.ts'
import logger from '../../logger.js'
@ -90,17 +91,17 @@ export default defineComponent({
},
setup() {
const { currentView } = useNavigation()
const renamingStore = useRenamingStore()
return {
currentView,
renamingStore,
}
},
computed: {
currentView(): View {
return this.$navigation.active as View
},
isRenaming() {
return this.renamingStore.renamingNode === this.source
},
@ -282,7 +283,7 @@ export default defineComponent({
}
// Set loading state
Vue.set(this.source, 'status', NodeStatus.LOADING)
this.$set(this.source, 'status', NodeStatus.LOADING)
// Update node
this.source.rename(newName)
@ -327,7 +328,7 @@ export default defineComponent({
// Unknown error
showError(t('files', 'Could not rename "{oldName}"', { oldName }))
} finally {
Vue.set(this.source, 'status', undefined)
this.$set(this.source, 'status', undefined)
}
},

View file

@ -60,6 +60,7 @@
<script lang="ts">
import { defineComponent } from 'vue'
import { useNavigation } from '../composables/useNavigation'
import { useActionsMenuStore } from '../store/actionsmenu.ts'
import { useDragAndDropStore } from '../store/dragging.ts'
import { useFilesStore } from '../store/files.ts'
@ -93,12 +94,16 @@ export default defineComponent({
const filesStore = useFilesStore()
const renamingStore = useRenamingStore()
const selectionStore = useSelectionStore()
const { currentView } = useNavigation()
return {
actionsMenuStore,
draggingStore,
filesStore,
renamingStore,
selectionStore,
currentView,
}
},

View file

@ -48,10 +48,6 @@ export default defineComponent({
},
computed: {
currentView(): View {
return this.$navigation.active as View
},
currentDir() {
// Remove any trailing slash but leave root slash
return (this.$route?.query?.dir?.toString() || '/').replace(/^(.+)\/$/, '$1')

View file

@ -54,17 +54,21 @@
</template>
<script lang="ts">
import { translate as t } from '@nextcloud/l10n'
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
import { defineComponent, type PropType } from 'vue'
import type { Node } from '@nextcloud/files'
import type { PropType } from 'vue'
import type { FileSource } from '../types.ts'
import { translate as t } from '@nextcloud/l10n'
import { defineComponent } from 'vue'
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
import FilesListTableHeaderButton from './FilesListTableHeaderButton.vue'
import { useNavigation } from '../composables/useNavigation'
import { useFilesStore } from '../store/files.ts'
import { useSelectionStore } from '../store/selection.ts'
import FilesListTableHeaderButton from './FilesListTableHeaderButton.vue'
import filesSortingMixin from '../mixins/filesSorting.ts'
import logger from '../logger.js'
import type { Node } from '@nextcloud/files'
import type { FileSource } from '../types.ts'
export default defineComponent({
name: 'FilesListTableHeader',
@ -100,17 +104,17 @@ export default defineComponent({
setup() {
const filesStore = useFilesStore()
const selectionStore = useSelectionStore()
const { currentView } = useNavigation()
return {
filesStore,
selectionStore,
currentView,
}
},
computed: {
currentView() {
return this.$navigation.active
},
columns() {
// Hide columns if the list is too small
if (this.filesListWidth < 512) {

View file

@ -0,0 +1,98 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { beforeEach, describe, expect, it, jest } from '@jest/globals'
import { Navigation, View } from '@nextcloud/files'
import { mount } from '@vue/test-utils'
import { defineComponent, nextTick } from 'vue'
import { useNavigation } from './useNavigation'
import nextcloudFiles from '@nextcloud/files'
// Just a wrapper so we can test the composable
const TestComponent = defineComponent({
template: '<div></div>',
setup() {
const { currentView, views } = useNavigation()
return {
currentView,
views,
}
},
})
describe('Composables: useNavigation', () => {
const spy = jest.spyOn(nextcloudFiles, 'getNavigation')
let navigation: Navigation
describe('currentView', () => {
beforeEach(() => {
navigation = new Navigation()
spy.mockImplementation(() => navigation)
})
it('should return null without active navigation', () => {
const wrapper = mount(TestComponent)
expect((wrapper.vm as unknown as { currentView: View | null}).currentView).toBe(null)
})
it('should return already active navigation', async () => {
const view = new View({ getContents: () => Promise.reject(), icon: '<svg></svg>', id: 'view-1', name: 'My View 1', order: 0 })
navigation.register(view)
navigation.setActive(view)
// Now the navigation is already set it should take the active navigation
const wrapper = mount(TestComponent)
expect((wrapper.vm as unknown as { currentView: View | null}).currentView).toBe(view)
})
it('should be reactive on updating active navigation', async () => {
const view = new View({ getContents: () => Promise.reject(), icon: '<svg></svg>', id: 'view-1', name: 'My View 1', order: 0 })
navigation.register(view)
const wrapper = mount(TestComponent)
// no active navigation
expect((wrapper.vm as unknown as { currentView: View | null}).currentView).toBe(null)
navigation.setActive(view)
// Now the navigation is set it should take the active navigation
expect((wrapper.vm as unknown as { currentView: View | null}).currentView).toBe(view)
})
})
describe('views', () => {
beforeEach(() => {
navigation = new Navigation()
spy.mockImplementation(() => navigation)
})
it('should return empty array without registered views', () => {
const wrapper = mount(TestComponent)
expect((wrapper.vm as unknown as { views: View[]}).views).toStrictEqual([])
})
it('should return already registered views', () => {
const view = new View({ getContents: () => Promise.reject(), 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])
})
it('should be reactive on registering new views', () => {
const view = new View({ getContents: () => Promise.reject(), icon: '<svg></svg>', id: 'view-1', name: 'My View 1', order: 0 })
const view2 = new View({ getContents: () => Promise.reject(), 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])
// now register view 2 and check it is reactivly added
navigation.register(view2)
expect((wrapper.vm as unknown as { views: View[]}).views).toStrictEqual([view, view2])
})
})
})

View file

@ -0,0 +1,46 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { View } from '@nextcloud/files'
import { getNavigation } from '@nextcloud/files'
import { onMounted, onUnmounted, shallowRef, type ShallowRef } from 'vue'
/**
* Composable to get the currently active files view from the files navigation
*/
export function useNavigation() {
const navigation = getNavigation()
const views: ShallowRef<View[]> = shallowRef(navigation.views)
const currentView: ShallowRef<View | null> = shallowRef(navigation.active)
/**
* Event listener to update the `currentView`
* @param event The update event
*/
function onUpdateActive(event: CustomEvent<View|null>) {
currentView.value = event.detail
}
/**
* Event listener to update all registered views
*/
function onUpdateViews() {
views.value = navigation.views
}
onMounted(() => {
navigation.addEventListener('update', onUpdateViews)
navigation.addEventListener('updateActive', onUpdateActive)
})
onUnmounted(() => {
navigation.removeEventListener('update', onUpdateViews)
navigation.removeEventListener('updateActive', onUpdateActive)
})
return {
currentView,
views,
}
}

View file

@ -107,7 +107,7 @@
</template>
<script lang="ts">
import type { View, ContentsWithRoot } from '@nextcloud/files'
import type { ContentsWithRoot } from '@nextcloud/files'
import type { Upload } from '@nextcloud/upload'
import type { CancelablePromise } from 'cancelable-promise'
import type { ComponentPublicInstance } from 'vue'
@ -137,6 +137,7 @@ import AccountPlusIcon from 'vue-material-design-icons/AccountPlus.vue'
import ViewGridIcon from 'vue-material-design-icons/ViewGrid.vue'
import { action as sidebarAction } from '../actions/sidebarAction.ts'
import { useNavigation } from '../composables/useNavigation.ts'
import { useFilesStore } from '../store/files.ts'
import { usePathsStore } from '../store/paths.ts'
import { useSelectionStore } from '../store/selection.ts'
@ -186,10 +187,13 @@ export default defineComponent({
const uploaderStore = useUploaderStore()
const userConfigStore = useUserConfigStore()
const viewConfigStore = useViewConfigStore()
const { currentView } = useNavigation()
const enableGridView = (loadState('core', 'config', [])['enable_non-accessible_features'] ?? true)
return {
currentView,
filesStore,
pathsStore,
selectionStore,
@ -228,10 +232,6 @@ export default defineComponent({
return this.userConfigStore.userConfig
},
currentView(): View {
return this.$navigation.active || this.$navigation.views.find((view) => view.id === (this.$route.params?.view ?? 'files'))!
},
pageHeading(): string {
return this.currentView?.name ?? t('files', 'Files')
},
@ -475,7 +475,7 @@ export default defineComponent({
subscribe('files:node:deleted', this.onNodeDeleted)
subscribe('files:node:updated', this.onUpdatedNode)
subscribe('nextcloud:unified-search.search', this.onSearch)
subscribe('nextcloud:unified-search.reset', this.onSearch)
subscribe('nextcloud:unified-search.reset', this.resetSearch)
// reload on settings change
this.unsubscribeStoreCallback = this.userConfigStore.$subscribe(() => this.fetchContent(), { deep: true })
@ -485,7 +485,7 @@ export default defineComponent({
unsubscribe('files:node:deleted', this.onNodeDeleted)
unsubscribe('files:node:updated', this.onUpdatedNode)
unsubscribe('nextcloud:unified-search.search', this.onSearch)
unsubscribe('nextcloud:unified-search.reset', this.onSearch)
unsubscribe('nextcloud:unified-search.reset', this.resetSearch)
this.unsubscribeStoreCallback()
},
@ -656,6 +656,9 @@ export default defineComponent({
* Reset the search query
*/
resetSearch() {
// Reset debounced calls to not set the query again
this.onSearch.clear()
// Reset filter query
this.filterText = ''
},
@ -668,7 +671,7 @@ export default defineComponent({
if (window?.OCA?.Files?.Sidebar?.setActiveTab) {
window.OCA.Files.Sidebar.setActiveTab('sharing')
}
sidebarAction.exec(this.currentFolder, this.currentView, this.currentFolder.path)
sidebarAction.exec(this.currentFolder, this.currentView!, this.currentFolder.path)
},
toggleGridView() {
this.userConfigStore.update('grid_view', !this.userConfig.grid_view)

View file

@ -2,22 +2,38 @@
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import FolderSvg from '@mdi/svg/svg/folder.svg'
import ShareSvg from '@mdi/svg/svg/share-variant.svg'
import type { Navigation } from '@nextcloud/files'
import FolderSvg from '@mdi/svg/svg/folder.svg?raw'
import { createTestingPinia } from '@pinia/testing'
import NavigationView from './Navigation.vue'
import router from '../router/router'
import { useViewConfigStore } from '../store/viewConfig'
import { Folder, View, getNavigation } from '@nextcloud/files'
import Vue from 'vue'
import router from '../router/router'
const resetNavigation = () => {
const nav = getNavigation()
;[...nav.views].forEach(({ id }) => nav.remove(id))
nav.setActive(null)
}
const createView = (id: string, name: string, parent?: string) => new View({
id,
name,
getContents: async () => ({ folder: {} as Folder, contents: [] }),
icon: FolderSvg,
order: 1,
parent,
})
describe('Navigation renders', () => {
delete window._nc_navigation
const Navigation = getNavigation()
let Navigation: Navigation
before(() => {
delete window._nc_navigation
Navigation = getNavigation()
Vue.prototype.$navigation = Navigation
cy.mockInitialState('files', 'storageStats', {
@ -44,29 +60,31 @@ describe('Navigation renders', () => {
})
describe('Navigation API', () => {
delete window._nc_navigation
const Navigation = getNavigation()
let Navigation: Navigation
before(async () => {
delete window._nc_navigation
Navigation = getNavigation()
before(() => {
Vue.prototype.$navigation = Navigation
await router.replace({ name: 'filelist', params: { view: 'files' } })
})
beforeEach(() => resetNavigation())
it('Check API entries rendering', () => {
Navigation.register(new View({
id: 'files',
name: 'Files',
getContents: async () => ({ folder: {} as Folder, contents: [] }),
icon: FolderSvg,
order: 1,
}))
Navigation.register(createView('files', 'Files'))
console.warn(Navigation.views)
cy.mount(NavigationView, {
global: {
plugins: [createTestingPinia({
createSpy: cy.spy,
})],
},
router,
global: {
plugins: [
createTestingPinia({
createSpy: cy.spy,
}),
],
},
})
cy.get('[data-cy-files-navigation]').should('be.visible')
@ -76,21 +94,16 @@ describe('Navigation API', () => {
})
it('Adds a new entry and render', () => {
Navigation.register(new View({
id: 'sharing',
name: 'Sharing',
getContents: async () => ({ folder: {} as Folder, contents: [] }),
icon: ShareSvg,
order: 2,
}))
Navigation.register(createView('files', 'Files'))
Navigation.register(createView('sharing', 'Sharing'))
cy.mount(NavigationView, {
router,
global: {
plugins: [createTestingPinia({
createSpy: cy.spy,
})],
},
router,
})
cy.get('[data-cy-files-navigation]').should('be.visible')
@ -100,22 +113,17 @@ describe('Navigation API', () => {
})
it('Adds a new children, render and open menu', () => {
Navigation.register(new View({
id: 'sharingin',
name: 'Shared with me',
getContents: async () => ({ folder: {} as Folder, contents: [] }),
parent: 'sharing',
icon: ShareSvg,
order: 1,
}))
Navigation.register(createView('files', 'Files'))
Navigation.register(createView('sharing', 'Sharing'))
Navigation.register(createView('sharingin', 'Shared with me', 'sharing'))
cy.mount(NavigationView, {
router,
global: {
plugins: [createTestingPinia({
createSpy: cy.spy,
})],
},
router,
})
cy.wrap(useViewConfigStore()).as('viewConfigStore')
@ -143,23 +151,18 @@ describe('Navigation API', () => {
})
it('Throws when adding a duplicate entry', () => {
expect(() => {
Navigation.register(new View({
id: 'files',
name: 'Files',
getContents: async () => ({ folder: {} as Folder, contents: [] }),
icon: FolderSvg,
order: 1,
}))
}).to.throw('View id files is already registered')
Navigation.register(createView('files', 'Files'))
expect(() => Navigation.register(createView('files', 'Files')))
.to.throw('View id files is already registered')
})
})
describe('Quota rendering', () => {
delete window._nc_navigation
const Navigation = getNavigation()
let Navigation: Navigation
before(() => {
delete window._nc_navigation
Navigation = getNavigation()
Vue.prototype.$navigation = Navigation
})

View file

@ -45,7 +45,7 @@
: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>
@ -61,22 +61,26 @@
import type { View } from '@nextcloud/files'
import { emit } from '@nextcloud/event-bus'
import { translate } from '@nextcloud/l10n'
import Cog from 'vue-material-design-icons/Cog.vue'
import { translate as t } from '@nextcloud/l10n'
import { defineComponent } from 'vue'
import IconCog 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 { useViewConfigStore } from '../store/viewConfig.ts'
import logger from '../logger.js'
import NavigationQuota from '../components/NavigationQuota.vue'
import SettingsModal from './Settings.vue'
export default {
import { useNavigation } from '../composables/useNavigation'
import { useViewConfigStore } from '../store/viewConfig.ts'
import logger from '../logger.js'
export default defineComponent({
name: 'Navigation',
components: {
Cog,
IconCog,
NavigationQuota,
NcAppNavigation,
NcAppNavigationItem,
@ -86,7 +90,12 @@ export default {
setup() {
const viewConfigStore = useViewConfigStore()
const { currentView, views } = useNavigation()
return {
currentView,
views,
viewConfigStore,
}
},
@ -98,18 +107,13 @@ 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
@ -137,24 +141,27 @@ export default {
},
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 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)
}
// 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: {
t,
/**
* 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
@ -165,9 +172,13 @@ export default {
return this.childViews[view.id]?.length > 0
},
/**
* 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?.()
window.OCA?.Files?.Sidebar?.close?.()
this.$navigation.setActive(view)
emit('files:navigation:changed', view)
},
@ -221,10 +232,8 @@ export default {
onSettingsClose() {
this.settingsOpened = false
},
t: translate,
},
}
})
</script>
<style scoped lang="scss">