Skip to content

Commit 6f3d46f

Browse files
authored
Merge pull request #1299 from nextcloud-libraries/fix/fetch-folder-content
fix: Fix incorrect directory contents when navigating quickly
2 parents 24a60e6 + 0a883bd commit 6f3d46f

File tree

5 files changed

+117
-33
lines changed

5 files changed

+117
-33
lines changed

lib/composables/dav.spec.ts

+43-7
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@
2020
*
2121
*/
2222

23+
import type { Ref } from 'vue'
2324
import { describe, it, expect, vi, afterEach } from 'vitest'
2425
import { shallowMount } from '@vue/test-utils'
25-
import { defineComponent, ref, toRef } from 'vue'
26+
import { defineComponent, ref, toRef, nextTick } from 'vue'
2627
import { useDAVFiles } from './dav'
2728

2829
const nextcloudFiles = vi.hoisted(() => ({
@@ -44,6 +45,14 @@ const waitLoaded = (vue: ReturnType<typeof shallowMount>) => new Promise((resolv
4445
w()
4546
})
4647

48+
const waitRefLoaded = (isLoading: Ref<boolean>) => new Promise((resolve) => {
49+
const w = () => {
50+
if (isLoading.value) window.setTimeout(w, 50)
51+
else resolve(true)
52+
}
53+
w()
54+
})
55+
4756
const TestComponent = defineComponent({
4857
props: ['currentView', 'currentPath', 'isPublic'],
4958
setup(props) {
@@ -209,16 +218,43 @@ describe('dav composable', () => {
209218
expect(isLoading.value).toBe(true)
210219
await loadFiles()
211220
expect(isLoading.value).toBe(false)
212-
expect(client.getDirectoryContents).toBeCalledWith(`${nextcloudFiles.davRootPath}/`, { details: true })
221+
expect(client.getDirectoryContents).toBeCalledWith(`${nextcloudFiles.davRootPath}/`, expect.objectContaining({ details: true }))
213222

214223
view.value = 'recent'
215-
await loadFiles()
216-
expect(isLoading.value).toBe(false)
217-
expect(client.search).toBeCalled()
224+
await waitRefLoaded(isLoading)
225+
expect(client.search).toBeCalledWith('/', expect.objectContaining({ details: true }))
218226

219227
view.value = 'favorites'
220-
await loadFiles()
221-
expect(isLoading.value).toBe(false)
228+
await waitRefLoaded(isLoading)
222229
expect(nextcloudFiles.getFavoriteNodes).toBeCalled()
223230
})
231+
232+
it('request cancelation works', async () => {
233+
const client = {
234+
stat: vi.fn((v) => ({ data: { path: v } })),
235+
getDirectoryContents: vi.fn((p, o) => ({ data: [] })),
236+
search: vi.fn((p, o) => ({ data: { results: [], truncated: false } })),
237+
}
238+
nextcloudFiles.davGetClient.mockImplementationOnce(() => client)
239+
nextcloudFiles.davResultToNode.mockImplementationOnce((v) => v)
240+
241+
const view = ref<'files' | 'recent' | 'favorites'>('files')
242+
const path = ref('/')
243+
const { loadFiles, isLoading } = useDAVFiles(view, path, ref(false))
244+
245+
const abort = vi.spyOn(AbortController.prototype, 'abort')
246+
247+
loadFiles()
248+
view.value = 'recent'
249+
await waitRefLoaded(isLoading)
250+
expect(abort).toBeCalledTimes(1)
251+
252+
view.value = 'files'
253+
await nextTick()
254+
view.value = 'recent'
255+
await nextTick()
256+
view.value = 'favorites'
257+
await waitRefLoaded(isLoading)
258+
expect(abort).toBeCalledTimes(2)
259+
})
224260
})

lib/composables/dav.ts

+56-18
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { davGetClient, davGetDefaultPropfind, davGetRecentSearch, davRemoteURL,
2727
import { generateRemoteUrl } from '@nextcloud/router'
2828
import { join } from 'path'
2929
import { computed, onMounted, ref, watch } from 'vue'
30+
import { CancelablePromise } from 'cancelable-promise'
3031

3132
/**
3233
* Handle file loading using WebDAV
@@ -68,6 +69,48 @@ export const useDAVFiles = function(
6869

6970
const resultToNode = (result: FileStat) => davResultToNode(result, defaultRootPath.value, defaultRemoteUrl.value)
7071

72+
const getRecentNodes = (): CancelablePromise<Node[]> => {
73+
const controller = new AbortController()
74+
// unix timestamp in seconds, two weeks ago
75+
const lastTwoWeek = Math.round(Date.now() / 1000) - (60 * 60 * 24 * 14)
76+
return new CancelablePromise(async (resolve, reject, onCancel) => {
77+
onCancel(() => controller.abort())
78+
try {
79+
const { data } = await client.value.search('/', {
80+
signal: controller.signal,
81+
details: true,
82+
data: davGetRecentSearch(lastTwoWeek),
83+
}) as ResponseDataDetailed<SearchResult>
84+
const nodes = data.results.map(resultToNode)
85+
resolve(nodes)
86+
} catch (error) {
87+
reject(error)
88+
}
89+
})
90+
}
91+
92+
const getNodes = (): CancelablePromise<Node[]> => {
93+
const controller = new AbortController()
94+
return new CancelablePromise(async (resolve, reject, onCancel) => {
95+
onCancel(() => controller.abort())
96+
try {
97+
const results = await client.value.getDirectoryContents(`${defaultRootPath.value}${currentPath.value}`, {
98+
signal: controller.signal,
99+
details: true,
100+
data: davGetDefaultPropfind(),
101+
}) as ResponseDataDetailed<FileStat[]>
102+
let nodes = results.data.map(resultToNode)
103+
// Hack for the public endpoint which always returns folder itself
104+
if (isPublicEndpoint.value) {
105+
nodes = nodes.filter((file) => file.path !== currentPath.value)
106+
}
107+
resolve(nodes)
108+
} catch (error) {
109+
reject(error)
110+
}
111+
})
112+
}
113+
71114
/**
72115
* All files in current view and path
73116
*/
@@ -78,6 +121,11 @@ export const useDAVFiles = function(
78121
*/
79122
const isLoading = ref(true)
80123

124+
/**
125+
* The cancelable promise
126+
*/
127+
const promise = ref<null | CancelablePromise<unknown>>(null)
128+
81129
/**
82130
* Create a new directory in the current path
83131
* @param name Name of the new directory
@@ -112,31 +160,21 @@ export const useDAVFiles = function(
112160
* Force reload files using the DAV client
113161
*/
114162
async function loadDAVFiles() {
163+
if (promise.value) {
164+
promise.value.cancel()
165+
}
115166
isLoading.value = true
116167

117168
if (currentView.value === 'favorites') {
118-
files.value = await getFavoriteNodes(client.value, currentPath.value, defaultRootPath.value)
169+
promise.value = getFavoriteNodes(client.value, currentPath.value, defaultRootPath.value)
119170
} else if (currentView.value === 'recent') {
120-
// unix timestamp in seconds, two weeks ago
121-
const lastTwoWeek = Math.round(Date.now() / 1000) - (60 * 60 * 24 * 14)
122-
const { data } = await client.value.search('/', {
123-
details: true,
124-
data: davGetRecentSearch(lastTwoWeek),
125-
}) as ResponseDataDetailed<SearchResult>
126-
files.value = data.results.map(resultToNode)
171+
promise.value = getRecentNodes()
127172
} else {
128-
const results = await client.value.getDirectoryContents(`${defaultRootPath.value}${currentPath.value}`, {
129-
details: true,
130-
data: davGetDefaultPropfind(),
131-
}) as ResponseDataDetailed<FileStat[]>
132-
files.value = results.data.map(resultToNode)
133-
134-
// Hack for the public endpoint which always returns folder itself
135-
if (isPublicEndpoint.value) {
136-
files.value = files.value.filter((file) => file.path !== currentPath.value)
137-
}
173+
promise.value = getNodes()
138174
}
175+
files.value = await promise.value as Node[]
139176

177+
promise.value = null
140178
isLoading.value = false
141179
}
142180

package-lock.json

+12-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,14 @@
5858
"@nextcloud/auth": "^2.2.1",
5959
"@nextcloud/axios": "^2.4.0",
6060
"@nextcloud/event-bus": "^3.1.0",
61-
"@nextcloud/files": "^3.1.1",
61+
"@nextcloud/files": "^3.2.0",
6262
"@nextcloud/initial-state": "^2.1.0",
6363
"@nextcloud/l10n": "^2.2.0",
6464
"@nextcloud/router": "^3.0.0",
6565
"@nextcloud/typings": "^1.8.0",
6666
"@types/toastify-js": "^1.12.3",
6767
"@vueuse/core": "^10.9.0",
68+
"cancelable-promise": "^4.3.1",
6869
"toastify-js": "^1.12.0",
6970
"vue-frag": "^1.4.3",
7071
"webdav": "^5.5.0"

vite.config.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,12 @@ export default defineConfig((env) => {
4141
classNameStrategy: 'non-scoped',
4242
},
4343
},
44-
// Fix unresolvable .css extension for ssr
4544
server: {
4645
deps: {
47-
inline: [/@nextcloud\/vue/],
46+
inline: [
47+
/@nextcloud\/vue/, // Fix unresolvable .css extension for ssr
48+
/@nextcloud\/files/, // Fix CommonJS cancelable-promise not supporting named exports
49+
],
4850
},
4951
},
5052
},

0 commit comments

Comments
 (0)