refactor(files): migrate from deprecated useNavigation to activeStore

Small preparation for upcoming Vue 3 migration of the files app.

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
Ferdinand Thiessen 2026-01-05 18:25:05 +01:00
parent f42493bf1e
commit 47acb66b9c
17 changed files with 216 additions and 262 deletions

View file

@ -1,9 +1,10 @@
/**
/*!
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { IFilePickerButton } from '@nextcloud/dialogs'
import type { Folder, Node } from '@nextcloud/files'
import type { IFolder, INode } from '@nextcloud/files'
import type { FileStat, ResponseDataDetailed, WebDAVClientError } from 'webdav'
import type { MoveCopyResult } from './moveOrCopyActionUtils.ts'
@ -14,7 +15,7 @@ import { FilePickerClosed, getFilePickerBuilder, showError, showInfo, TOAST_PERM
import { emit } from '@nextcloud/event-bus'
import { FileAction, FileType, getUniqueName, NodeStatus, Permission } from '@nextcloud/files'
import { defaultRootPath, getClient, getDefaultPropfind, resultToNode } from '@nextcloud/files/dav'
import { translate as t } from '@nextcloud/l10n'
import { t } from '@nextcloud/l10n'
import { hasConflict, openConflictPicker } from '@nextcloud/upload'
import { basename, join } from 'path'
import Vue from 'vue'
@ -28,7 +29,7 @@ import { canCopy, canMove, getQueue, MoveCopyAction } from './moveOrCopyActionUt
* @param nodes The nodes to check against
* @return The action that is possible for the given nodes
*/
function getActionForNodes(nodes: Node[]): MoveCopyAction {
function getActionForNodes(nodes: INode[]): MoveCopyAction {
if (canMove(nodes)) {
if (canCopy(nodes)) {
return MoveCopyAction.MOVE_OR_COPY
@ -76,7 +77,7 @@ function createLoadingNotification(mode: MoveCopyAction, source: string, destina
* @param overwrite Whether to overwrite the destination if it exists
* @return A promise that resolves when the copy/move is done
*/
export async function handleCopyMoveNodeTo(node: Node, destination: Folder, method: MoveCopyAction.COPY | MoveCopyAction.MOVE, overwrite = false) {
export async function handleCopyMoveNodeTo(node: INode, destination: IFolder, method: MoveCopyAction.COPY | MoveCopyAction.MOVE, overwrite = false) {
if (!destination) {
return
}
@ -217,13 +218,13 @@ export async function handleCopyMoveNodeTo(node: Node, destination: Folder, meth
async function openFilePickerForAction(
action: MoveCopyAction,
dir = '/',
nodes: Node[],
nodes: INode[],
): Promise<MoveCopyResult | false> {
const { resolve, reject, promise } = Promise.withResolvers<MoveCopyResult | false>()
const fileIDs = nodes.map((node) => node.fileid).filter(Boolean)
const filePicker = getFilePickerBuilder(t('files', 'Choose destination'))
.allowDirectories(true)
.setFilter((n: Node) => {
.setFilter((n: INode) => {
// We don't want to show the current nodes in the file picker
return !fileIDs.includes(n.fileid)
})
@ -234,7 +235,7 @@ async function openFilePickerForAction(
.setMimeTypeFilter([])
.setMultiSelect(false)
.startAt(dir)
.setButtonFactory((selection: Node[], path: string) => {
.setButtonFactory((selection: INode[], path: string) => {
const buttons: IFilePickerButton[] = []
const target = basename(path)
@ -246,9 +247,9 @@ async function openFilePickerForAction(
label: target ? t('files', 'Copy to {target}', { target }, { escape: false, sanitize: false }) : t('files', 'Copy'),
variant: 'primary',
icon: CopyIconSvg,
async callback(destination: Node[]) {
async callback(destination: INode[]) {
resolve({
destination: destination[0] as Folder,
destination: destination[0] as IFolder,
action: MoveCopyAction.COPY,
} as MoveCopyResult)
},
@ -276,9 +277,9 @@ async function openFilePickerForAction(
label: target ? t('files', 'Move to {target}', { target }, undefined, { escape: false, sanitize: false }) : t('files', 'Move'),
variant: action === MoveCopyAction.MOVE ? 'primary' : 'secondary',
icon: FolderMoveSvg,
async callback(destination: Node[]) {
async callback(destination: INode[]) {
resolve({
destination: destination[0] as Folder,
destination: destination[0] as IFolder,
action: MoveCopyAction.MOVE,
} as MoveCopyResult)
},

View file

@ -1,9 +1,9 @@
/**
/*!
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Folder, Node } from '@nextcloud/files'
import type { IFolder, INode } from '@nextcloud/files'
import type { ShareAttribute } from '../../../files_sharing/src/sharing.ts'
import { Permission } from '@nextcloud/files'
@ -36,24 +36,26 @@ export enum MoveCopyAction {
}
export type MoveCopyResult = {
destination: Folder
destination: IFolder
action: MoveCopyAction.COPY | MoveCopyAction.MOVE
}
/**
* Check if the given nodes can be moved
*
* @param nodes
* @param nodes - The nodes to check
*/
export function canMove(nodes: Node[]) {
export function canMove(nodes: INode[]) {
const minPermission = nodes.reduce((min, node) => Math.min(min, node.permissions), Permission.ALL)
return Boolean(minPermission & Permission.DELETE)
}
/**
* Check if the given nodes can be downloaded
*
* @param nodes
* @param nodes - The nodes to check
*/
export function canDownload(nodes: Node[]) {
export function canDownload(nodes: INode[]) {
return nodes.every((node) => {
const shareAttributes = JSON.parse(node.attributes?.['share-attributes'] ?? '[]') as Array<ShareAttribute>
return !shareAttributes.some((attribute) => attribute.scope === 'permissions' && attribute.value === false && attribute.key === 'download')
@ -61,10 +63,11 @@ export function canDownload(nodes: Node[]) {
}
/**
* Check if the given nodes can be copied
*
* @param nodes
* @param nodes - The nodes to check
*/
export function canCopy(nodes: Node[]) {
export function canCopy(nodes: INode[]) {
// a shared file cannot be copied if the download is disabled
if (!canDownload(nodes)) {
return false

View file

@ -50,9 +50,10 @@ import NcBreadcrumb from '@nextcloud/vue/components/NcBreadcrumb'
import NcBreadcrumbs from '@nextcloud/vue/components/NcBreadcrumbs'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import { useFileListWidth } from '../composables/useFileListWidth.ts'
import { useNavigation } from '../composables/useNavigation.ts'
import { useViews } from '../composables/useViews.ts'
import logger from '../logger.ts'
import { dataTransferToFileTree, onDropExternalFiles, onDropInternalFiles } from '../services/DropService.ts'
import { useActiveStore } from '../store/active.ts'
import { useDragAndDropStore } from '../store/dragging.ts'
import { useFilesStore } from '../store/files.ts'
import { usePathsStore } from '../store/paths.ts'
@ -76,22 +77,24 @@ export default defineComponent({
},
setup() {
const draggingStore = useDragAndDropStore()
const activeStore = useActiveStore()
const filesStore = useFilesStore()
const pathsStore = usePathsStore()
const draggingStore = useDragAndDropStore()
const selectionStore = useSelectionStore()
const uploaderStore = useUploaderStore()
const fileListWidth = useFileListWidth()
const { currentView, views } = useNavigation()
const views = useViews()
return {
activeStore,
draggingStore,
filesStore,
pathsStore,
selectionStore,
uploaderStore,
currentView,
fileListWidth,
views,
}
@ -134,7 +137,7 @@ export default defineComponent({
// used to show the views icon for the first breadcrumb
viewIcon(): string {
return this.currentView?.icon ?? HomeSvg
return this.activeStore.activeView?.icon ?? HomeSvg
},
selectedFiles() {
@ -152,12 +155,12 @@ export default defineComponent({
},
getFileSourceFromPath(path: string): FileSource | null {
return (this.currentView && this.pathsStore.getPath(this.currentView.id, path)) ?? null
return (this.activeStore.activeView && this.pathsStore.getPath(this.activeStore.activeView.id, path)) ?? null
},
getDirDisplayName(path: string): string {
if (path === '/') {
return this.currentView?.name || t('files', 'Home')
return this.activeStore.activeView?.name || t('files', 'Home')
}
const source = this.getFileSourceFromPath(path)
@ -169,7 +172,7 @@ export default defineComponent({
if (dir === '/') {
return {
...this.$route,
params: { view: this.currentView?.id },
params: { view: this.activeStore.activeView?.id },
query: {},
}
}
@ -233,7 +236,8 @@ export default defineComponent({
const fileTree = await dataTransferToFileTree(items)
// We might not have the target directory fetched yet
const contents = await this.currentView?.getContents(path)
const controller = new AbortController()
const contents = await this.activeStore.activeView?.getContents(path, { signal: controller.signal })
const folder = contents?.folder
if (!folder) {
showError(this.t('files', 'Target folder does not exist any more'))
@ -275,14 +279,12 @@ export default defineComponent({
} else if (index === 0) {
return t('files', 'Go to the "{dir}" directory', section)
}
return null
},
ariaForSection(section) {
if (section?.to?.query?.dir === this.$route.query.dir) {
return t('files', 'Reload current directory')
}
return null
},
t,

View file

@ -38,9 +38,9 @@ import { UploadStatus } from '@nextcloud/upload'
import debounce from 'debounce'
import { defineComponent } from 'vue'
import TrayArrowDownIcon from 'vue-material-design-icons/TrayArrowDown.vue'
import { useNavigation } from '../composables/useNavigation.ts'
import logger from '../logger.ts'
import { dataTransferToFileTree, onDropExternalFiles } from '../services/DropService.ts'
import { useActiveStore } from '../store/active.ts'
export default defineComponent({
name: 'DragAndDropNotice',
@ -57,10 +57,10 @@ export default defineComponent({
},
setup() {
const { currentView } = useNavigation()
const activeStore = useActiveStore()
return {
currentView,
activeStore,
}
},
@ -177,7 +177,8 @@ export default defineComponent({
const fileTree = await dataTransferToFileTree(items)
// We might not have the target directory fetched yet
const contents = await this.currentView?.getContents(this.currentFolder.path)
const controller = new AbortController()
const contents = await this.activeStore.activeView?.getContents(this.currentFolder.path, { signal: controller.signal })
const folder = contents?.folder
if (!folder) {
showError(this.t('files', 'Target folder does not exist any more'))
@ -211,13 +212,7 @@ export default defineComponent({
...this.$route.params,
fileid: String(lastUpload.response!.headers['oc-fileid']),
},
query: {
...this.$route.query,
},
}
// Remove open file from query
delete location.query?.openfile
this.$router.push(location)
}

View file

@ -75,14 +75,15 @@ 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 { t } from '@nextcloud/l10n'
import { useHotKey } from '@nextcloud/vue/composables/useHotKey'
import { defineComponent } from 'vue'
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
import FilesListTableHeaderButton from './FilesListTableHeaderButton.vue'
import { useNavigation } from '../composables/useNavigation.ts'
import { useRouteParameters } from '../composables/useRouteParameters.ts'
import logger from '../logger.ts'
import filesSortingMixin from '../mixins/filesSorting.ts'
import { useActiveStore } from '../store/active.ts'
import { useFilesStore } from '../store/files.ts'
import { useSelectionStore } from '../store/selection.ts'
@ -126,15 +127,17 @@ export default defineComponent({
},
setup() {
const activeStore = useActiveStore()
const filesStore = useFilesStore()
const selectionStore = useSelectionStore()
const { currentView } = useNavigation()
const { directory } = useRouteParameters()
return {
activeStore,
filesStore,
selectionStore,
currentView,
directory,
}
},
@ -144,12 +147,12 @@ export default defineComponent({
if (this.filesListWidth < 512) {
return []
}
return this.currentView?.columns || []
return this.activeStore.activeView?.columns || []
},
dir() {
// Remove any trailing slash but leave root slash
return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1')
return this.directory.replace(/^(.+)\/$/, '$1')
},
selectAllBind() {
@ -195,11 +198,10 @@ export default defineComponent({
},
methods: {
ariaSortForMode(mode: string): 'ascending' | 'descending' | null {
ariaSortForMode(mode: string): 'ascending' | 'descending' | undefined {
if (this.sortingMode === mode) {
return this.isAscSorting ? 'ascending' : 'descending'
}
return null
},
classForColumn(column) {
@ -207,7 +209,7 @@ export default defineComponent({
'files-list__column': true,
'files-list__column--sortable': !!column.sort,
'files-list__row-column-custom': true,
[`files-list__row-${this.currentView?.id}-${column.id}`]: true,
[`files-list__row-${this.activeStore.activeView?.id}-${column.id}`]: true,
}
},

View file

@ -45,7 +45,7 @@ import { defineComponent } from 'vue'
import { Fragment } from 'vue-frag'
import NcAppNavigationItem from '@nextcloud/vue/components/NcAppNavigationItem'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import { useNavigation } from '../composables/useNavigation.js'
import { useActiveStore } from '../store/active.js'
import { useViewConfigStore } from '../store/viewConfig.js'
const maxLevel = 7 // Limit nesting to not exceed max call stack size
@ -77,10 +77,10 @@ export default defineComponent({
},
setup() {
const { currentView } = useNavigation()
const activeStore = useActiveStore()
const viewConfigStore = useViewConfigStore()
return {
currentView,
activeStore,
viewConfigStore,
}
},
@ -89,7 +89,7 @@ export default defineComponent({
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) => view.params?.dir.startsWith(this.parent.params?.dir))
.filter((view) => this.parent.params && view.params?.dir.startsWith(this.parent.params.dir))
}
return this.filterVisible(this.views[this.parent.id] ?? [])
},
@ -106,7 +106,7 @@ export default defineComponent({
methods: {
filterVisible(views: View[]) {
return views.filter(({ id, hidden }) => id === this.currentView?.id || hidden !== true)
return views.filter(({ id, hidden }) => id === this.activeStore.activeView?.id || hidden !== true)
},
hasChildViews(view: View): boolean {

View file

@ -12,11 +12,11 @@ import NcActions from '@nextcloud/vue/components/NcActions'
import NcAppNavigationSearch from '@nextcloud/vue/components/NcAppNavigationSearch'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import { onBeforeNavigation } from '../composables/useBeforeNavigation.ts'
import { useNavigation } from '../composables/useNavigation.ts'
import { useActiveStore } from '../store/active.ts'
import { useSearchStore } from '../store/search.ts'
import { VIEW_ID } from '../views/search.ts'
const { currentView } = useNavigation(true)
const activeStore = useActiveStore()
const searchStore = useSearchStore()
/**
@ -49,7 +49,7 @@ onBeforeNavigation((to, from, next) => {
* Are we currently on the search view.
* Needed to disable the action menu (we cannot change the search mode there)
*/
const isSearchView = computed(() => currentView.value.id === VIEW_ID)
const isSearchView = computed(() => activeStore.activeView?.id === VIEW_ID)
/**
* Different searchbox label depending if filtering or searching

View file

@ -1,99 +0,0 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Navigation, View } from '@nextcloud/files'
import * as nextcloudFiles from '@nextcloud/files'
import { mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { defineComponent } from 'vue'
import { useNavigation } from './useNavigation.ts'
// 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 = vi.spyOn(nextcloudFiles, 'getNavigation')
let navigation: Navigation
describe('currentView', () => {
beforeEach(() => {
navigation = new nextcloudFiles.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 nextcloudFiles.View({ getContents: () => Promise.reject(new Error()), icon: '<svg></svg>', id: 'view-1', name: 'My View 1', order: 0 })
navigation.register(view)
navigation.setActive(view.id)
// 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 nextcloudFiles.View({ getContents: () => Promise.reject(new Error()), 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.id)
// 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 nextcloudFiles.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 nextcloudFiles.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])
})
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 })
// 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

@ -1,57 +0,0 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { View } from '@nextcloud/files'
import type { ShallowRef } from 'vue'
import { subscribe } from '@nextcloud/event-bus'
import { getNavigation } from '@nextcloud/files'
import { onMounted, onUnmounted, shallowRef, triggerRef } from 'vue'
/**
* Composable to get the currently active files view from the files navigation
*
* @param _loaded If set enforce a current view is loaded
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function useNavigation<T extends boolean>(_loaded?: T) {
type MaybeView = T extends true ? View : (View | null)
const navigation = getNavigation()
const views: ShallowRef<View[]> = shallowRef(navigation.views)
/** @deprecated use activeStore.activeView instead */
const currentView: ShallowRef<MaybeView> = shallowRef(navigation.active as MaybeView)
/**
* Event listener to update the `currentView`
*
* @param event The update event
*/
function onUpdateActive(event: CustomEvent<View | null>) {
currentView.value = event.detail as MaybeView
}
/**
* Event listener to update all registered views
*/
function onUpdateViews() {
views.value = navigation.views
triggerRef(views)
}
onMounted(() => {
navigation.addEventListener('update', onUpdateViews)
navigation.addEventListener('updateActive', onUpdateActive)
subscribe('files:navigation:updated', onUpdateViews)
})
onUnmounted(() => {
navigation.removeEventListener('update', onUpdateViews)
navigation.removeEventListener('updateActive', onUpdateActive)
})
return {
currentView,
views,
}
}

View file

@ -0,0 +1,63 @@
/*!
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Navigation, View } from '@nextcloud/files'
import * as nextcloudFiles from '@nextcloud/files'
import { enableAutoDestroy, mount } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { defineComponent } from 'vue'
import { useViews } from './useViews.ts'
// Just a wrapper so we can test the composable
const TestComponent = defineComponent({
template: '<div></div>',
setup() {
return {
views: useViews(),
}
},
})
enableAutoDestroy(afterEach)
describe('Composables: useViews', () => {
const spy = vi.spyOn(nextcloudFiles, 'getNavigation')
let navigation: Navigation
beforeEach(() => {
navigation = new nextcloudFiles.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 nextcloudFiles.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])
})
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 })
// 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 reactively added
navigation.register(view2)
expect((wrapper.vm as unknown as { views: View[] }).views).toStrictEqual([view, view2])
})
})

View file

@ -0,0 +1,36 @@
/*!
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { getNavigation } from '@nextcloud/files'
import { createSharedComposable } from '@vueuse/core'
import { onUnmounted, shallowRef, triggerRef } from 'vue'
/**
* Composable to get the currently available views
*/
export const useViews = createSharedComposable(useInternalViews)
/**
* 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)
}
navigation.addEventListener('update', onUpdateViews)
onUnmounted(() => {
navigation.removeEventListener('update', onUpdateViews)
})
return views
}

View file

@ -1,18 +1,19 @@
import { mapState } from 'pinia'
/**
/*!
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { mapState } from 'pinia'
import Vue from 'vue'
import { useNavigation } from '../composables/useNavigation.ts'
import { useActiveStore } from '../store/active.ts'
import { useViewConfigStore } from '../store/viewConfig.ts'
export default Vue.extend({
setup() {
const { currentView } = useNavigation()
const activeStore = useActiveStore()
return {
currentView,
activeStore,
}
},
@ -23,8 +24,8 @@ export default Vue.extend({
* Get the sorting mode for the current view
*/
sortingMode(): string {
return this.getConfig(this.currentView.id)?.sorting_mode as string
|| this.currentView?.defaultSortKey
return this.getConfig(this.activeStore.activeView?.id)?.sorting_mode as string
|| this.activeStore.activeView?.defaultSortKey
|| 'basename'
},
@ -32,7 +33,7 @@ export default Vue.extend({
* Get the sorting direction for the current view
*/
isAscSorting(): boolean {
const sortingDirection = this.getConfig(this.currentView.id)?.sorting_direction
const sortingDirection = this.getConfig(this.activeStore.activeView?.id)?.sorting_direction
return sortingDirection !== 'desc'
},
},
@ -41,11 +42,11 @@ export default Vue.extend({
toggleSortBy(key: string) {
// If we're already sorting by this key, flip the direction
if (this.sortingMode === key) {
this.toggleSortingDirection(this.currentView.id)
this.toggleSortingDirection(this.activeStore.activeView?.id)
return
}
// else sort ASC by this new key
this.setSortingBy(key, this.currentView.id)
this.setSortingBy(key, this.activeStore.activeView?.id)
},
},
})

View file

@ -1,9 +1,9 @@
/**
/*!
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Folder, Node } from '@nextcloud/files'
import type { IFolder, INode } from '@nextcloud/files'
import type { Upload } from '@nextcloud/upload'
import type { RootDirectory } from './DropServiceUtils.ts'
@ -96,7 +96,7 @@ export async function dataTransferToFileTree(items: DataTransferItem[]): Promise
* @param destination - The destination folder
* @param contents - The contents of the destination folder
*/
export async function onDropExternalFiles(root: RootDirectory, destination: Folder, contents: Node[]): Promise<Upload[]> {
export async function onDropExternalFiles(root: RootDirectory, destination: IFolder, contents: INode[]): Promise<Upload[]> {
const uploader = getUploader()
// Check for conflicts on root elements
@ -172,13 +172,14 @@ export async function onDropExternalFiles(root: RootDirectory, destination: Fold
}
/**
* Handle dropping internal files
*
* @param nodes
* @param destination
* @param contents
* @param isCopy
* @param nodes - The nodes being dropped
* @param destination - The destination folder
* @param contents - The contents of the destination folder
* @param isCopy - Whether the operation is a copy
*/
export async function onDropInternalFiles(nodes: Node[], destination: Folder, contents: Node[], isCopy = false) {
export async function onDropInternalFiles(nodes: INode[], destination: IFolder, contents: INode[], isCopy = false) {
const queue = [] as Promise<void>[]
// Check for conflicts on root elements

View file

@ -1,14 +1,15 @@
import type { Folder, Node } from '@nextcloud/files'
/**
/*!
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { IFolder, INode } from '@nextcloud/files'
import type { FileStat, ResponseDataDetailed } from 'webdav'
import { showInfo, showWarning } from '@nextcloud/dialogs'
import { emit } from '@nextcloud/event-bus'
import { getClient, getDefaultPropfind, resultToNode } from '@nextcloud/files/dav'
import { translate as t } from '@nextcloud/l10n'
import { t } from '@nextcloud/l10n'
import { openConflictPicker } from '@nextcloud/upload'
import logger from '../logger.ts'
@ -131,8 +132,9 @@ function readDirectory(directory: FileSystemDirectoryEntry): Promise<FileSystemE
}
/**
* Create a directory if it does not exist
*
* @param absolutePath
* @param absolutePath - the absolute path of the directory to create
*/
export async function createDirectoryIfNotExists(absolutePath: string) {
const davClient = getClient()
@ -146,20 +148,21 @@ export async function createDirectoryIfNotExists(absolutePath: string) {
}
/**
* Resolve conflicts between existing files and incoming files
*
* @param files
* @param destination
* @param contents
* @param files - incoming files
* @param destination - destination folder
* @param contents - existing contents of the destination folder
*/
export async function resolveConflict<T extends ((Directory | File) | Node)>(files: Array<T>, destination: Folder, contents: Node[]): Promise<T[]> {
export async function resolveConflict<T extends ((Directory | File) | INode)>(files: Array<T>, destination: IFolder, contents: INode[]): Promise<T[]> {
try {
// List all conflicting files
const conflicts = files.filter((file: File | Node) => {
return contents.find((node: Node) => node.basename === (file instanceof File ? file.name : file.basename))
}).filter(Boolean) as (File | Node)[]
const conflicts = files.filter((file: File | INode) => {
return contents.find((node: INode) => node.basename === (file instanceof File ? file.name : file.basename))
}).filter(Boolean) as (File | INode)[]
// List of incoming files that are NOT in conflict
const uploads = files.filter((file: File | Node) => {
const uploads = files.filter((file: File | INode) => {
return !conflicts.includes(file)
})

View file

@ -169,7 +169,7 @@ import { getCurrentUser } from '@nextcloud/auth'
import { getCapabilities } from '@nextcloud/capabilities'
import { showError, showSuccess, showWarning } from '@nextcloud/dialogs'
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
import { Folder, getFileListActions, getSidebar, Permission, sortNodes } from '@nextcloud/files'
import { Folder, getFileListActions, Permission, sortNodes } from '@nextcloud/files'
import { getRemoteURL, getRootPath } from '@nextcloud/files/dav'
import { loadState } from '@nextcloud/initial-state'
import { translate as t } from '@nextcloud/l10n'
@ -178,7 +178,7 @@ import { ShareType } from '@nextcloud/sharing'
import { UploadPicker, UploadStatus } from '@nextcloud/upload'
import { useThrottleFn } from '@vueuse/core'
import { normalize, relative } from 'path'
import { defineComponent } from 'vue'
import { computed, defineComponent } from 'vue'
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
import NcActions from '@nextcloud/vue/components/NcActions'
import NcAppContent from '@nextcloud/vue/components/NcAppContent'
@ -196,7 +196,6 @@ import BreadCrumbs from '../components/BreadCrumbs.vue'
import DragAndDropNotice from '../components/DragAndDropNotice.vue'
import FilesListVirtual from '../components/FilesListVirtual.vue'
import { useFileListWidth } from '../composables/useFileListWidth.ts'
import { useNavigation } from '../composables/useNavigation.ts'
import { useRouteParameters } from '../composables/useRouteParameters.ts'
import logger from '../logger.ts'
import filesSortingMixin from '../mixins/filesSorting.ts'
@ -205,6 +204,7 @@ import { useFilesStore } from '../store/files.ts'
import { useFiltersStore } from '../store/filters.ts'
import { usePathsStore } from '../store/paths.ts'
import { useSelectionStore } from '../store/selection.ts'
import { useSidebarStore } from '../store/sidebar.ts'
import { useUploaderStore } from '../store/uploader.ts'
import { useUserConfigStore } from '../store/userconfig.ts'
import { useViewConfigStore } from '../store/viewConfig.ts'
@ -249,12 +249,7 @@ export default defineComponent({
},
setup() {
const sidebar = getSidebar()
const { currentView } = useNavigation()
const { directory, fileId } = useRouteParameters()
const fileListWidth = useFileListWidth()
const sidebar = useSidebarStore()
const activeStore = useActiveStore()
const filesStore = useFilesStore()
const filtersStore = useFiltersStore()
@ -264,9 +259,14 @@ export default defineComponent({
const userConfigStore = useUserConfigStore()
const viewConfigStore = useViewConfigStore()
const fileListWidth = useFileListWidth()
const { directory, fileId } = useRouteParameters()
const enableGridView = (loadState('core', 'config', [])['enable_non-accessible_features'] ?? true)
const forbiddenCharacters = loadState<string[]>('files', 'forbiddenCharacters', [])
const currentView = computed(() => activeStore.activeView)
return {
currentView,
directory,
@ -274,6 +274,7 @@ export default defineComponent({
fileListWidth,
t,
sidebar,
activeStore,
filesStore,
filtersStore,
@ -284,7 +285,6 @@ export default defineComponent({
viewConfigStore,
// non reactive data
sidebar,
enableGridView,
forbiddenCharacters,
ShareType,
@ -319,7 +319,8 @@ export default defineComponent({
}
// If not found in the files store (cache)
// use the current view to fetch the content for the requested path
return (await view.getContents(normalizedPath)).contents
const controller = new AbortController()
return (await view.getContents(normalizedPath, { signal: controller.signal })).contents
}
},

View file

@ -58,8 +58,9 @@ import FilesNavigationItem from '../components/FilesNavigationItem.vue'
import FilesNavigationSearch from '../components/FilesNavigationSearch.vue'
import NavigationQuota from '../components/NavigationQuota.vue'
import FilesAppSettings from './FilesAppSettings.vue'
import { useNavigation } from '../composables/useNavigation.ts'
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'
@ -89,18 +90,19 @@ export default defineComponent({
setup() {
const sidebar = useSidebarStore()
const activeStore = useActiveStore()
const filtersStore = useFiltersStore()
const viewConfigStore = useViewConfigStore()
const { currentView, views } = useNavigation()
return {
currentView,
t,
views,
sidebar,
activeStore,
filtersStore,
viewConfigStore,
views: useViews(),
}
},
@ -138,7 +140,7 @@ export default defineComponent({
watch: {
currentViewId(newView, oldView) {
if (this.currentViewId !== this.currentView?.id) {
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

View file

@ -3,7 +3,7 @@
"include": ["./apps/**/*.ts", "./apps/**/*.vue", "./core/**/*.ts", "./core/**/*.vue", "./*.d.ts"],
"exclude": ["./**/*.cy.ts"],
"compilerOptions": {
"lib": ["DOM", "ESNext"],
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"types": ["node", "vue", "vue-router"],
"outDir": "./dist/",
"target": "ESNext",