Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/components/FilePicker/FileList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
</template>

<script setup lang="ts">
import type { FilesSortingMode, INode } from '@nextcloud/files'
import type { INode } from '@nextcloud/files'
import type { FileListViews } from '../../composables/filesSettings.ts'
import type { IFilePickerCanPick } from '../types.ts'

Expand Down Expand Up @@ -174,7 +174,7 @@ const sortedFiles = computed(() => {
sortFoldersFirst: true,
sortFavoritesFirst: sortFavoritesFirst.value,
sortingOrder: sortingConfig.value.order === 'descending' ? 'desc' : 'asc',
sortingMode: sortingConfig.value.sortBy as FilesSortingMode,
sortingMode: sortingConfig.value.sortBy,
})
})

Expand Down
10 changes: 5 additions & 5 deletions lib/components/FilePicker/FilePicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
</template>

<script setup lang="ts">
import type { Node } from '@nextcloud/files'
import type { INode } from '@nextcloud/files'
import type { IFilesViewId } from '../../composables/views.ts'
import type { IDialogButton, IFilePickerButton, IFilePickerButtonFactory, IFilePickerCanPick, IFilePickerFilter } from '../types.ts'

Expand Down Expand Up @@ -153,7 +153,7 @@ const props = withDefaults(defineProps<{
})

const emit = defineEmits<{
(e: 'close', v?: Node[]): void
(e: 'close', v?: INode[]): void
}>()

const isOpen = ref(true)
Expand Down Expand Up @@ -191,7 +191,7 @@ const currentPath = computed({
/**
* All currently selected files
*/
const selectedFiles = shallowRef<Node[]>([])
const selectedFiles = shallowRef<INode[]>([])

const {
files,
Expand Down Expand Up @@ -245,7 +245,7 @@ const dialogButtons = computed(() => {
* @param callback - Callback of the button
* @param nodes - Currently selected nodes
*/
async function handleButtonClick(callback: IFilePickerButton['callback'], nodes: Node[]) {
async function handleButtonClick(callback: IFilePickerButton['callback'], nodes: INode[]) {
await callback(nodes)
emit('close', nodes)
// Unlock close
Expand Down Expand Up @@ -286,7 +286,7 @@ const filteredFiles = computed(() => {
filtered = filtered.filter((file) => file.basename.toLowerCase().includes(filterString.value.toLowerCase()))
}
if (props.filterFn) {
filtered = filtered.filter((f) => props.filterFn!(f as Node))
filtered = filtered.filter((f) => props.filterFn!(f as INode))
}
return filtered
})
Expand Down
8 changes: 4 additions & 4 deletions lib/components/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { INode, Node } from '@nextcloud/files'
import type { INode } from '@nextcloud/files'

export type IDialogSeverity = 'info' | 'warning' | 'error'

Expand Down Expand Up @@ -49,15 +49,15 @@ export interface IFilePickerButton extends Omit<IDialogButton, 'callback'> {
*
* @param nodes Array of `@nextcloud/files` Nodes that were selected
*/
callback: (nodes: Node[]) => void | Promise<void>
callback: (nodes: INode[]) => void | Promise<void>
}

export type IFilePickerButtonFactory = (selectedNodes: Node[], currentPath: string, currentView: string) => IFilePickerButton[]
export type IFilePickerButtonFactory = (selectedNodes: INode[], currentPath: string, currentView: string) => IFilePickerButton[]

/**
* Type of filter functions to filter the FilePicker's file list
*/
export type IFilePickerFilter = (node: Node) => boolean
export type IFilePickerFilter = (node: INode) => boolean

/**
* Type of functions to allow or not picking a node
Expand Down
2 changes: 1 addition & 1 deletion lib/composables/dav.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,6 @@ describe('dav composable', () => {
await nextTick()
view.value = 'favorites'
await waitRefLoaded(isLoading)
expect(abort).toBeCalledTimes(2)
expect(abort).toBeCalledTimes(1)
})
})
64 changes: 34 additions & 30 deletions lib/composables/dav.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/**
/*
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { ContentsWithRoot, Folder, Node } from '@nextcloud/files'
import type { CancelablePromise } from 'cancelable-promise'

import type { IFolder, INode } from '@nextcloud/files'
import type { ComputedRef, Ref } from 'vue'

import { defaultRootPath, getClient, getFavoriteNodes } from '@nextcloud/files/dav'
Expand All @@ -29,12 +29,12 @@ export function useDAVFiles(
/**
* All files in current view and path
*/
const files = shallowRef<Node[]>([] as Node[]) as Ref<Node[]>
const files = shallowRef<INode[]>([] as INode[]) as Ref<INode[]>

/**
* The current folder
*/
const folder = shallowRef<Folder | null>(null)
const folder = shallowRef<IFolder | null>(null)

/**
* Loading state of the files
Expand All @@ -44,7 +44,7 @@ export function useDAVFiles(
/**
* The cancelable promise used internally to cancel on fast navigation
*/
const promise = ref<null | CancelablePromise<Node[] | ContentsWithRoot>>(null)
let abortController: AbortController | undefined

/**
* Create a new directory in the current path
Expand All @@ -53,11 +53,11 @@ export function useDAVFiles(
* @param name Name of the new directory
* @return The created directory
*/
async function createDirectory(name: string): Promise<Folder> {
async function createDirectory(name: string): Promise<IFolder> {
const path = join(currentPath.value, name)

await client.createDirectory(join(defaultRootPath, path))
const directory = await getFile(client, path) as Folder
const directory = await getFile(client, path) as IFolder
files.value = [...files.value, directory]
return directory
}
Expand All @@ -66,31 +66,35 @@ export function useDAVFiles(
* Force reload files using the DAV client
*/
async function loadDAVFiles() {
if (promise.value) {
promise.value.cancel()
if (abortController) {
abortController.abort()
abortController = undefined
}
isLoading.value = true

if (currentView.value === 'favorites') {
promise.value = getFavoriteNodes(client, currentPath.value)
} else if (currentView.value === 'recent') {
promise.value = getRecentNodes(client)
} else {
promise.value = getNodes(client, currentPath.value)
}
const content = await promise.value
if (!content) {
return
} else if ('folder' in content) {
folder.value = content.folder
files.value = content.contents
} else {
folder.value = null
files.value = content
abortController = new AbortController()
isLoading.value = true
try {
if (currentView.value === 'favorites') {
files.value = await getFavoriteNodes({ client, path: currentPath.value, signal: abortController.signal })
folder.value = null
} else if (currentView.value === 'recent') {
files.value = await getRecentNodes({ client, signal: abortController.signal })
folder.value = null
} else {
const content = await getNodes({ client, path: currentPath.value, signal: abortController.signal })
folder.value = content.folder
files.value = content.contents
}
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
// ignore abort errors
return
}
throw error
} finally {
abortController = undefined
isLoading.value = false
}

promise.value = null
isLoading.value = false
}

/**
Expand Down
4 changes: 2 additions & 2 deletions lib/filepicker-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { Node } from '@nextcloud/files'
import type { INode } from '@nextcloud/files'
import type { IFilePickerButton, IFilePickerButtonFactory, IFilePickerCanPick, IFilePickerFilter } from './components/types.ts'

import IconMove from '@mdi/svg/svg/folder-move.svg?raw'
Expand Down Expand Up @@ -73,7 +73,7 @@ export class FilePicker<IsMultiSelect extends boolean> {
*
* @return Promise with array of picked files or rejected promise on close without picking
*/
public async pickNodes(): Promise<Node[]> {
public async pickNodes(): Promise<INode[]> {
const { default: FilePickerVue } = await import('./components/FilePicker/FilePicker.vue')

const nodes = await spawnDialog(FilePickerVue, {
Expand Down
66 changes: 26 additions & 40 deletions lib/utils/dav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,59 +8,45 @@ import type { FileStat, ResponseDataDetailed, SearchResult, WebDAVClient } from

import { defaultRootPath, getDefaultPropfind, getRecentSearch, resultToNode } from '@nextcloud/files/dav'
import { join } from '@nextcloud/paths'
import { CancelablePromise } from 'cancelable-promise'

/**
* Get the recently changed nodes from the last two weeks
*
* @param client - The WebDAV client
* @param context - The context
* @param context.client - The WebDAV client
* @param context.signal - The abort signal to cancel the request
*/
export function getRecentNodes(client: WebDAVClient): CancelablePromise<Node[]> {
const controller = new AbortController()
export async function getRecentNodes({ client, signal }: { client: WebDAVClient, signal: AbortSignal }): Promise<Node[]> {
// unix timestamp in seconds, two weeks ago
const lastTwoWeek = Math.round(Date.now() / 1000) - (60 * 60 * 24 * 14)
return new CancelablePromise(async (resolve, reject, onCancel) => {
onCancel(() => controller.abort())
try {
const { data } = await client.search('/', {
signal: controller.signal,
details: true,
data: getRecentSearch(lastTwoWeek),
}) as ResponseDataDetailed<SearchResult>
const nodes = data.results.map((result: FileStat) => resultToNode(result))
resolve(nodes)
} catch (error) {
reject(error)
}
})
const { data } = await client.search('/', {
signal,
details: true,
data: getRecentSearch(lastTwoWeek),
}) as ResponseDataDetailed<SearchResult>
return data.results.map((result: FileStat) => resultToNode(result))
}

/**
* Get the directory content
*
* @param client - The WebDAV client
* @param directoryPath - The path to fetch
* @param context - The context
* @param context.client - The WebDAV client
* @param context.path - The path to fetch
* @param context.signal - The abort signal to cancel the request
*/
export function getNodes(client: WebDAVClient, directoryPath: string): CancelablePromise<ContentsWithRoot> {
const controller = new AbortController()
return new CancelablePromise(async (resolve, reject, onCancel) => {
onCancel(() => controller.abort())
try {
const results = await client.getDirectoryContents(join(defaultRootPath, directoryPath), {
signal: controller.signal,
details: true,
includeSelf: true,
data: getDefaultPropfind(),
}) as ResponseDataDetailed<FileStat[]>
const nodes = results.data.map((result: FileStat) => resultToNode(result))
resolve({
contents: nodes.filter(({ path }) => path !== directoryPath),
folder: nodes.find(({ path }) => path === directoryPath) as Folder,
})
} catch (error) {
reject(error)
}
})
export async function getNodes({ client, path, signal }: { client: WebDAVClient, path: string, signal: AbortSignal }): Promise<ContentsWithRoot> {
const results = await client.getDirectoryContents(join(defaultRootPath, path), {
signal,
details: true,
includeSelf: true,
data: getDefaultPropfind(),
}) as ResponseDataDetailed<FileStat[]>
const nodes = results.data.map((result: FileStat) => resultToNode(result))
return {
contents: nodes.filter(({ path: nodePath }) => nodePath !== path),
folder: nodes.find(({ path: nodePath }) => path === nodePath) as Folder,
}
}

/**
Expand Down
Loading