Skip to content

Commit 4033659

Browse files
authored
Merge pull request #1378 from nextcloud-libraries/fix/current-folder
fix(FilePicker): Cleanup DAV handling and properly handle `currentFolder`
2 parents 00e605d + b3c0fc6 commit 4033659

File tree

5 files changed

+144
-97
lines changed

5 files changed

+144
-97
lines changed

lib/components/FilePicker/FilePicker.vue

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,13 +146,19 @@ const isOpen = ref(true)
146146
* Map buttons to Dialog buttons by wrapping the callback function to pass the selected files
147147
*/
148148
const dialogButtons = computed(() => {
149-
const nodes = selectedFiles.value.length === 0 && props.allowPickDirectory && currentFolder.value ? [currentFolder.value] : selectedFiles.value
149+
const nodes = selectedFiles.value.length === 0
150+
&& props.allowPickDirectory
151+
&& currentFolder.value
152+
? [currentFolder.value]
153+
: selectedFiles.value
154+
150155
const buttons = typeof props.buttons === 'function'
151156
? props.buttons(nodes, currentPath.value, currentView.value)
152157
: props.buttons
153158
154159
return buttons.map((button) => ({
155160
...button,
161+
disabled: button.disabled || isLoading.value,
156162
callback: () => {
157163
// lock default close handling
158164
isHandlingCallback = true
@@ -203,9 +209,9 @@ const navigatedPath = ref('')
203209
watch([navigatedPath], () => {
204210
if (props.path === undefined && navigatedPath.value) {
205211
window.sessionStorage.setItem('NC.FilePicker.LastPath', navigatedPath.value)
206-
// Reset selected files
207-
selectedFiles.value = []
208212
}
213+
// Reset selected files
214+
selectedFiles.value = []
209215
})
210216
211217
/**

lib/composables/dav.spec.ts

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ describe('dav composable', () => {
7171
expect(Array.isArray(vue.vm.files)).toBe(true)
7272
expect(vue.vm.files.length).toBe(0)
7373
// functions
74-
expect(typeof vue.vm.getFile === 'function').toBe(true)
7574
expect(typeof vue.vm.loadFiles === 'function').toBe(true)
7675
})
7776

@@ -153,23 +152,6 @@ describe('dav composable', () => {
153152
expect(client.search).toBeCalledTimes(1)
154153
})
155154

156-
it('getFile works', async () => {
157-
const client = {
158-
stat: vi.fn((v) => ({ data: { path: v } })),
159-
getDirectoryContents: vi.fn(() => ({ data: [] })),
160-
}
161-
nextcloudFiles.davGetClient.mockImplementation(() => client)
162-
nextcloudFiles.davResultToNode.mockImplementation((v) => v)
163-
164-
const { getFile } = useDAVFiles(ref('files'), ref('/'))
165-
166-
const node = await getFile('/some/path/file.ext')
167-
expect(node).toEqual({ path: `${nextcloudFiles.davRootPath}/some/path/file.ext` })
168-
// Check mock usage
169-
expect(client.stat).toBeCalledWith(`${nextcloudFiles.davRootPath}/some/path/file.ext`, { details: true })
170-
expect(nextcloudFiles.davResultToNode).toBeCalledWith({ path: `${nextcloudFiles.davRootPath}/some/path/file.ext` })
171-
})
172-
173155
it('createDirectory works', async () => {
174156
const client = {
175157
stat: vi.fn((v) => ({ data: { path: v } })),
@@ -189,11 +171,12 @@ describe('dav composable', () => {
189171
it('loadFiles work', async () => {
190172
const client = {
191173
stat: vi.fn((v) => ({ data: { path: v } })),
192-
getDirectoryContents: vi.fn((p, o) => ({ data: [] })),
193-
search: vi.fn((p, o) => ({ data: { results: [], truncated: false } })),
174+
getDirectoryContents: vi.fn((_p, _o) => ({ data: [] })),
175+
search: vi.fn((_p, _o) => ({ data: { results: [], truncated: false } })),
194176
}
195177
nextcloudFiles.davGetClient.mockImplementationOnce(() => client)
196178
nextcloudFiles.davResultToNode.mockImplementationOnce((v) => v)
179+
nextcloudFiles.getFavoriteNodes.mockImplementationOnce(() => Promise.resolve([]))
197180

198181
const view = ref<'files' | 'recent' | 'favorites'>('files')
199182
const path = ref('/')
@@ -216,8 +199,8 @@ describe('dav composable', () => {
216199
it('request cancelation works', async () => {
217200
const client = {
218201
stat: vi.fn((v) => ({ data: { path: v } })),
219-
getDirectoryContents: vi.fn((p, o) => ({ data: [] })),
220-
search: vi.fn((p, o) => ({ data: { results: [], truncated: false } })),
202+
getDirectoryContents: vi.fn((_p, _o) => ({ data: [] })),
203+
search: vi.fn((_p, _o) => ({ data: { results: [], truncated: false } })),
221204
}
222205
nextcloudFiles.davGetClient.mockImplementationOnce(() => client)
223206
nextcloudFiles.davResultToNode.mockImplementationOnce((v) => v)

lib/composables/dav.ts

Lines changed: 20 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
5-
import type { Folder, Node } from '@nextcloud/files'
5+
import type { ContentsWithRoot, Folder, Node } from '@nextcloud/files'
66
import type { ComputedRef, Ref } from 'vue'
7-
import type { FileStat, ResponseDataDetailed, SearchResult } from 'webdav'
87

9-
import { davGetClient, davGetDefaultPropfind, davGetRecentSearch, davResultToNode, davRootPath, getFavoriteNodes } from '@nextcloud/files'
10-
import { join } from 'path'
11-
import { onMounted, ref, shallowRef, watch } from 'vue'
8+
import { davGetClient, davRootPath, getFavoriteNodes } from '@nextcloud/files'
129
import { CancelablePromise } from 'cancelable-promise'
10+
import { join } from 'node:path'
11+
import { onMounted, ref, shallowRef, watch } from 'vue'
12+
import { getFile, getNodes, getRecentNodes } from '../utils/dav'
1313

1414
/**
1515
* Handle file loading using WebDAV
@@ -27,48 +27,6 @@ export const useDAVFiles = function(
2727
*/
2828
const client = davGetClient()
2929

30-
const resultToNode = (result: FileStat) => davResultToNode(result)
31-
32-
const getRecentNodes = (): CancelablePromise<Node[]> => {
33-
const controller = new AbortController()
34-
// unix timestamp in seconds, two weeks ago
35-
const lastTwoWeek = Math.round(Date.now() / 1000) - (60 * 60 * 24 * 14)
36-
return new CancelablePromise(async (resolve, reject, onCancel) => {
37-
onCancel(() => controller.abort())
38-
try {
39-
const { data } = await client.search('/', {
40-
signal: controller.signal,
41-
details: true,
42-
data: davGetRecentSearch(lastTwoWeek),
43-
}) as ResponseDataDetailed<SearchResult>
44-
const nodes = data.results.map(resultToNode)
45-
resolve(nodes)
46-
} catch (error) {
47-
reject(error)
48-
}
49-
})
50-
}
51-
52-
const getNodes = (): CancelablePromise<Node[]> => {
53-
const controller = new AbortController()
54-
return new CancelablePromise(async (resolve, reject, onCancel) => {
55-
onCancel(() => controller.abort())
56-
try {
57-
const results = await client.getDirectoryContents(join(davRootPath, currentPath.value), {
58-
signal: controller.signal,
59-
details: true,
60-
data: davGetDefaultPropfind(),
61-
}) as ResponseDataDetailed<FileStat[]>
62-
let nodes = results.data.map(resultToNode)
63-
// Hack for the public endpoint which always returns folder itself
64-
nodes = nodes.filter((file) => file.path !== currentPath.value)
65-
resolve(nodes)
66-
} catch (error) {
67-
reject(error)
68-
}
69-
})
70-
}
71-
7230
/**
7331
* All files in current view and path
7432
*/
@@ -77,49 +35,33 @@ export const useDAVFiles = function(
7735
/**
7836
* The current folder
7937
*/
80-
const folder = shallowRef<Folder>()
81-
watch([currentPath], async () => {
82-
folder.value = (files.value.find(({ path }) => path === currentPath.value) ?? await getFile(currentPath.value)) as Folder
83-
}, { immediate: true })
38+
const folder = shallowRef<Folder|null>(null)
8439

8540
/**
8641
* Loading state of the files
8742
*/
8843
const isLoading = ref(true)
8944

9045
/**
91-
* The cancelable promise
46+
* The cancelable promise used internally to cancel on fast navigation
9247
*/
93-
const promise = ref<null | CancelablePromise<unknown>>(null)
48+
const promise = ref<null | CancelablePromise<Node[] | ContentsWithRoot>>(null)
9449

9550
/**
9651
* Create a new directory in the current path
52+
* The directory will be added to the current file list
9753
* @param name Name of the new directory
9854
* @return {Promise<Folder>} The created directory
9955
*/
10056
async function createDirectory(name: string): Promise<Folder> {
10157
const path = join(currentPath.value, name)
10258

10359
await client.createDirectory(join(davRootPath, path))
104-
const directory = await getFile(path) as Folder
60+
const directory = await getFile(client, path) as Folder
10561
files.value = [...files.value, directory]
10662
return directory
10763
}
10864

109-
/**
110-
* Get information for one file
111-
*
112-
* @param path The path of the file or folder
113-
* @param rootPath DAV root path, defaults to '/files/USERID'
114-
*/
115-
async function getFile(path: string, rootPath: string = davRootPath) {
116-
const { data } = await client.stat(join(rootPath, path), {
117-
details: true,
118-
data: davGetDefaultPropfind(),
119-
}) as ResponseDataDetailed<FileStat>
120-
return resultToNode(data)
121-
}
122-
12365
/**
12466
* Force reload files using the DAV client
12567
*/
@@ -132,11 +74,18 @@ export const useDAVFiles = function(
13274
if (currentView.value === 'favorites') {
13375
promise.value = getFavoriteNodes(client, currentPath.value)
13476
} else if (currentView.value === 'recent') {
135-
promise.value = getRecentNodes()
77+
promise.value = getRecentNodes(client)
78+
} else {
79+
promise.value = getNodes(client, currentPath.value)
80+
}
81+
const content = await promise.value
82+
if ('folder' in content) {
83+
folder.value = content.folder
84+
files.value = content.contents
13685
} else {
137-
promise.value = getNodes()
86+
folder.value = null
87+
files.value = content
13888
}
139-
files.value = await promise.value as Node[]
14089

14190
promise.value = null
14291
isLoading.value = false
@@ -157,7 +106,6 @@ export const useDAVFiles = function(
157106
files,
158107
folder,
159108
loadFiles: loadDAVFiles,
160-
getFile,
161109
createDirectory,
162110
}
163111
}

lib/utils/dav.spec.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { beforeEach, describe, expect, it, vi } from 'vitest'
7+
8+
const nextcloudFiles = vi.hoisted(() => ({
9+
davResultToNode: vi.fn((v) => v),
10+
davGetDefaultPropfind: vi.fn(() => 'propfind content'),
11+
davRootPath: '/root/path',
12+
}))
13+
vi.mock('@nextcloud/files', () => nextcloudFiles)
14+
15+
describe('DAV utils', () => {
16+
beforeEach(() => {
17+
vi.resetModules()
18+
})
19+
20+
it('getFile works', async () => {
21+
const client = {
22+
stat: vi.fn((v) => Promise.resolve({ data: { path: v } })),
23+
getDirectoryContents: vi.fn(() => ({ data: [] })),
24+
}
25+
26+
const { getFile } = await import('./dav')
27+
28+
const node = await getFile(client, '/some/path/file.ext')
29+
expect(node).toEqual({ path: `${nextcloudFiles.davRootPath}/some/path/file.ext` })
30+
// Check mock usage
31+
expect(client.stat).toBeCalledWith(`${nextcloudFiles.davRootPath}/some/path/file.ext`, { details: true, data: 'propfind content' })
32+
expect(nextcloudFiles.davResultToNode).toBeCalledWith({ path: `${nextcloudFiles.davRootPath}/some/path/file.ext` })
33+
})
34+
})

lib/utils/dav.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import type { ContentsWithRoot, Node } from '@nextcloud/files'
7+
import type { FileStat, ResponseDataDetailed, SearchResult, WebDAVClient } from 'webdav'
8+
9+
import { davGetDefaultPropfind, davGetRecentSearch, davResultToNode, davRootPath } from '@nextcloud/files'
10+
import { CancelablePromise } from 'cancelable-promise'
11+
import { join } from 'node:path'
12+
13+
/**
14+
* Get the recently changed nodes from the last two weeks
15+
* @param client The WebDAV client
16+
*/
17+
export function getRecentNodes(client: WebDAVClient): CancelablePromise<Node[]> {
18+
const controller = new AbortController()
19+
// unix timestamp in seconds, two weeks ago
20+
const lastTwoWeek = Math.round(Date.now() / 1000) - (60 * 60 * 24 * 14)
21+
return new CancelablePromise(async (resolve, reject, onCancel) => {
22+
onCancel(() => controller.abort())
23+
try {
24+
const { data } = await client.search('/', {
25+
signal: controller.signal,
26+
details: true,
27+
data: davGetRecentSearch(lastTwoWeek),
28+
}) as ResponseDataDetailed<SearchResult>
29+
const nodes = data.results.map((result: FileStat) => davResultToNode(result))
30+
resolve(nodes)
31+
} catch (error) {
32+
reject(error)
33+
}
34+
})
35+
}
36+
37+
/**
38+
* Get the directory content
39+
* @param client The WebDAV client
40+
* @param directoryPath The path to fetch
41+
*/
42+
export function getNodes(client: WebDAVClient, directoryPath: string): CancelablePromise<ContentsWithRoot> {
43+
const controller = new AbortController()
44+
return new CancelablePromise(async (resolve, reject, onCancel) => {
45+
onCancel(() => controller.abort())
46+
try {
47+
const results = await client.getDirectoryContents(join(davRootPath, directoryPath), {
48+
signal: controller.signal,
49+
details: true,
50+
includeSelf: true,
51+
data: davGetDefaultPropfind(),
52+
}) as ResponseDataDetailed<FileStat[]>
53+
const nodes = results.data.map((result: FileStat) => davResultToNode(result))
54+
resolve({
55+
contents: nodes.filter(({ path }) => path !== directoryPath),
56+
folder: nodes.find(({ path }) => path === directoryPath),
57+
})
58+
} catch (error) {
59+
reject(error)
60+
}
61+
})
62+
}
63+
64+
/**
65+
* Get information for one file
66+
*
67+
* @param client The WebDAV client
68+
* @param path The path of the file or folder
69+
*/
70+
export async function getFile(client: WebDAVClient, path: string) {
71+
const { data } = await client.stat(join(davRootPath, path), {
72+
details: true,
73+
data: davGetDefaultPropfind(),
74+
}) as ResponseDataDetailed<FileStat>
75+
return davResultToNode(data)
76+
}

0 commit comments

Comments
 (0)