From b0f47e12019ed191152d1d6726b8321e2f38176d Mon Sep 17 00:00:00 2001 From: Christopher Ng Date: Thu, 11 Apr 2024 10:07:05 -0700 Subject: [PATCH 1/5] fix: Fix incorrect directory contents when navigating quickly Signed-off-by: Christopher Ng --- lib/composables/dav.ts | 74 ++++++++++++++++++++++++++++++++---------- package-lock.json | 6 ++++ package.json | 1 + 3 files changed, 63 insertions(+), 18 deletions(-) diff --git a/lib/composables/dav.ts b/lib/composables/dav.ts index 84ca5cf6..e1417c00 100644 --- a/lib/composables/dav.ts +++ b/lib/composables/dav.ts @@ -27,6 +27,7 @@ import { davGetClient, davGetDefaultPropfind, davGetRecentSearch, davRemoteURL, import { generateRemoteUrl } from '@nextcloud/router' import { join } from 'path' import { computed, onMounted, ref, watch } from 'vue' +import { CancelablePromise } from 'cancelable-promise' /** * Handle file loading using WebDAV @@ -68,6 +69,48 @@ export const useDAVFiles = function( const resultToNode = (result: FileStat) => davResultToNode(result, defaultRootPath.value, defaultRemoteUrl.value) + const getRecentNodes = (): CancelablePromise => { + const controller = new AbortController() + // 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.value.search('/', { + signal: controller.signal, + details: true, + data: davGetRecentSearch(lastTwoWeek), + }) as ResponseDataDetailed + const nodes = data.results.map(resultToNode) + resolve(nodes) + } catch (error) { + reject(error) + } + }) + } + + const getNodes = (): CancelablePromise => { + const controller = new AbortController() + return new CancelablePromise(async (resolve, reject, onCancel) => { + onCancel(() => controller.abort()) + try { + const results = await client.value.getDirectoryContents(`${defaultRootPath.value}${currentPath.value}`, { + signal: controller.signal, + details: true, + data: davGetDefaultPropfind(), + }) as ResponseDataDetailed + let nodes = results.data.map(resultToNode) + // Hack for the public endpoint which always returns folder itself + if (isPublicEndpoint.value) { + nodes = nodes.filter((file) => file.path !== currentPath.value) + } + resolve(nodes) + } catch (error) { + reject(error) + } + }) + } + /** * All files in current view and path */ @@ -78,6 +121,11 @@ export const useDAVFiles = function( */ const isLoading = ref(true) + /** + * The cancelable promise + */ + const promise = ref>(null) + /** * Create a new directory in the current path * @param name Name of the new directory @@ -112,31 +160,21 @@ export const useDAVFiles = function( * Force reload files using the DAV client */ async function loadDAVFiles() { + if (promise.value) { + promise.value.cancel() + } isLoading.value = true if (currentView.value === 'favorites') { - files.value = await getFavoriteNodes(client.value, currentPath.value, defaultRootPath.value) + promise.value = getFavoriteNodes(client.value, currentPath.value, defaultRootPath.value) } else if (currentView.value === 'recent') { - // unix timestamp in seconds, two weeks ago - const lastTwoWeek = Math.round(Date.now() / 1000) - (60 * 60 * 24 * 14) - const { data } = await client.value.search('/', { - details: true, - data: davGetRecentSearch(lastTwoWeek), - }) as ResponseDataDetailed - files.value = data.results.map(resultToNode) + promise.value = getRecentNodes() } else { - const results = await client.value.getDirectoryContents(`${defaultRootPath.value}${currentPath.value}`, { - details: true, - data: davGetDefaultPropfind(), - }) as ResponseDataDetailed - files.value = results.data.map(resultToNode) - - // Hack for the public endpoint which always returns folder itself - if (isPublicEndpoint.value) { - files.value = files.value.filter((file) => file.path !== currentPath.value) - } + promise.value = getNodes() } + files.value = await promise.value as Node[] + promise.value = null isLoading.value = false } diff --git a/package-lock.json b/package-lock.json index 1fc70f8c..b6c32adf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@nextcloud/typings": "^1.8.0", "@types/toastify-js": "^1.12.3", "@vueuse/core": "^10.9.0", + "cancelable-promise": "^4.3.1", "toastify-js": "^1.12.0", "vue-frag": "^1.4.3", "webdav": "^5.5.0" @@ -5260,6 +5261,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cancelable-promise": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/cancelable-promise/-/cancelable-promise-4.3.1.tgz", + "integrity": "sha512-A/8PwLk/T7IJDfUdQ68NR24QHa8rIlnN/stiJEBo6dmVUkD4K14LswG0w3VwdeK/o7qOwRUR1k2MhK5Rpy2m7A==" + }, "node_modules/caniuse-lite": { "version": "1.0.30001571", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001571.tgz", diff --git a/package.json b/package.json index 0a1b3563..f4fc6b1c 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "@nextcloud/typings": "^1.8.0", "@types/toastify-js": "^1.12.3", "@vueuse/core": "^10.9.0", + "cancelable-promise": "^4.3.1", "toastify-js": "^1.12.0", "vue-frag": "^1.4.3", "webdav": "^5.5.0" From 453a22dff566c6376d3a442705d5a58450dd0dd0 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Mon, 15 Apr 2024 14:23:29 +0200 Subject: [PATCH 2/5] chore: Update `@nextcloud/files` to 3.2.0 Signed-off-by: Ferdinand Thiessen --- package-lock.json | 11 ++++++----- package.json | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index b6c32adf..2bc2cc15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@nextcloud/auth": "^2.2.1", "@nextcloud/axios": "^2.4.0", "@nextcloud/event-bus": "^3.1.0", - "@nextcloud/files": "^3.1.1", + "@nextcloud/files": "^3.2.0", "@nextcloud/initial-state": "^2.1.0", "@nextcloud/l10n": "^2.2.0", "@nextcloud/router": "^3.0.0", @@ -2485,17 +2485,18 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/@nextcloud/files": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.1.1.tgz", - "integrity": "sha512-PwGxh/AcKeDehYSf/L+OpYNzZ2eK5xA1l/lVjufwa7I+u2onCo6qjYSqvc9Dh4Myzixjmt5YiA+Um/gx/Kq4NA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.2.0.tgz", + "integrity": "sha512-3EQBR758bzvqcNRzcp1etHGGkCZgK6wS9or8iQpzIOKf4B2tAe1O+hXA8GzPiQ5ZlGIPblOlMOEMlRg1KD49hg==", "dependencies": { "@nextcloud/auth": "^2.2.1", "@nextcloud/l10n": "^2.2.0", "@nextcloud/logger": "^2.7.0", "@nextcloud/paths": "^2.1.0", "@nextcloud/router": "^3.0.0", + "cancelable-promise": "^4.3.1", "is-svg": "^5.0.0", - "webdav": "^5.4.0" + "webdav": "^5.5.0" }, "engines": { "node": "^20.0.0", diff --git a/package.json b/package.json index f4fc6b1c..6172462b 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "@nextcloud/auth": "^2.2.1", "@nextcloud/axios": "^2.4.0", "@nextcloud/event-bus": "^3.1.0", - "@nextcloud/files": "^3.1.1", + "@nextcloud/files": "^3.2.0", "@nextcloud/initial-state": "^2.1.0", "@nextcloud/l10n": "^2.2.0", "@nextcloud/router": "^3.0.0", From 5e64a5d84f9ca2541f2e1f47b3309b3735915105 Mon Sep 17 00:00:00 2001 From: Christopher Ng Date: Mon, 15 Apr 2024 15:38:43 -0700 Subject: [PATCH 3/5] test(dav): Update tests Signed-off-by: Christopher Ng --- lib/composables/dav.spec.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/composables/dav.spec.ts b/lib/composables/dav.spec.ts index 2f755b8d..8be7af86 100644 --- a/lib/composables/dav.spec.ts +++ b/lib/composables/dav.spec.ts @@ -20,6 +20,7 @@ * */ +import type { Ref } from 'vue' import { describe, it, expect, vi, afterEach } from 'vitest' import { shallowMount } from '@vue/test-utils' import { defineComponent, ref, toRef } from 'vue' @@ -44,6 +45,14 @@ const waitLoaded = (vue: ReturnType) => new Promise((resolv w() }) +const waitRefLoaded = (isLoading: Ref) => new Promise((resolve) => { + const w = () => { + if (isLoading.value) window.setTimeout(w, 50) + else resolve(true) + } + w() +}) + const TestComponent = defineComponent({ props: ['currentView', 'currentPath', 'isPublic'], setup(props) { @@ -209,16 +218,14 @@ describe('dav composable', () => { expect(isLoading.value).toBe(true) await loadFiles() expect(isLoading.value).toBe(false) - expect(client.getDirectoryContents).toBeCalledWith(`${nextcloudFiles.davRootPath}/`, { details: true }) + expect(client.getDirectoryContents).toBeCalledWith(`${nextcloudFiles.davRootPath}/`, expect.objectContaining({ details: true })) view.value = 'recent' - await loadFiles() - expect(isLoading.value).toBe(false) - expect(client.search).toBeCalled() + await waitRefLoaded(isLoading) + expect(client.search).toBeCalledWith('/', expect.objectContaining({ details: true })) view.value = 'favorites' - await loadFiles() - expect(isLoading.value).toBe(false) + await waitRefLoaded(isLoading) expect(nextcloudFiles.getFavoriteNodes).toBeCalled() }) }) From 00ed78eceda3e1b8df77876b6f368ec8ee4b236a Mon Sep 17 00:00:00 2001 From: Christopher Ng Date: Mon, 15 Apr 2024 15:40:05 -0700 Subject: [PATCH 4/5] test(dav): Add test for request cancelation Signed-off-by: Christopher Ng --- lib/composables/dav.spec.ts | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/lib/composables/dav.spec.ts b/lib/composables/dav.spec.ts index 8be7af86..ec9f9c2e 100644 --- a/lib/composables/dav.spec.ts +++ b/lib/composables/dav.spec.ts @@ -23,7 +23,7 @@ import type { Ref } from 'vue' import { describe, it, expect, vi, afterEach } from 'vitest' import { shallowMount } from '@vue/test-utils' -import { defineComponent, ref, toRef } from 'vue' +import { defineComponent, ref, toRef, nextTick } from 'vue' import { useDAVFiles } from './dav' const nextcloudFiles = vi.hoisted(() => ({ @@ -228,4 +228,33 @@ describe('dav composable', () => { await waitRefLoaded(isLoading) expect(nextcloudFiles.getFavoriteNodes).toBeCalled() }) + + it('request cancelation works', async () => { + const client = { + stat: vi.fn((v) => ({ data: { path: v } })), + getDirectoryContents: vi.fn((p, o) => ({ data: [] })), + search: vi.fn((p, o) => ({ data: { results: [], truncated: false } })), + } + nextcloudFiles.davGetClient.mockImplementationOnce(() => client) + nextcloudFiles.davResultToNode.mockImplementationOnce((v) => v) + + const view = ref<'files' | 'recent' | 'favorites'>('files') + const path = ref('/') + const { loadFiles, isLoading } = useDAVFiles(view, path, ref(false)) + + const abort = vi.spyOn(AbortController.prototype, 'abort') + + loadFiles() + view.value = 'recent' + await waitRefLoaded(isLoading) + expect(abort).toBeCalledTimes(1) + + view.value = 'files' + await nextTick() + view.value = 'recent' + await nextTick() + view.value = 'favorites' + await waitRefLoaded(isLoading) + expect(abort).toBeCalledTimes(2) + }) }) From 0a883bd12896dbcf7aa172d0d6dd7483acce11e7 Mon Sep 17 00:00:00 2001 From: Christopher Ng Date: Mon, 15 Apr 2024 16:03:04 -0700 Subject: [PATCH 5/5] fix(tests): Fix commonjs cancelable-promise module error Signed-off-by: Christopher Ng --- vite.config.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index d981cc40..fe9d2016 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -41,10 +41,12 @@ export default defineConfig((env) => { classNameStrategy: 'non-scoped', }, }, - // Fix unresolvable .css extension for ssr server: { deps: { - inline: [/@nextcloud\/vue/], + inline: [ + /@nextcloud\/vue/, // Fix unresolvable .css extension for ssr + /@nextcloud\/files/, // Fix CommonJS cancelable-promise not supporting named exports + ], }, }, },