From 2c492a97d4630714bf666b4a65188b4f123e818a Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Mon, 17 Nov 2025 16:33:52 +0100 Subject: [PATCH 01/14] feat: provide `NcUploadPicker` previously contained in `@nextcloud/upload` Signed-off-by: Ferdinand Thiessen --- l10n/messages.pot | 27 + package-lock.json | 295 ++++++++- package.json | 4 + .../NcUploadPicker/NcUploadPicker.vue | 590 ++++++++++++++++++ src/components/NcUploadPicker/index.ts | 6 + 5 files changed, 894 insertions(+), 28 deletions(-) create mode 100644 src/components/NcUploadPicker/NcUploadPicker.vue create mode 100644 src/components/NcUploadPicker/index.ts diff --git a/l10n/messages.pot b/l10n/messages.pot index 4a5d5e11fc..89d2537bf0 100644 --- a/l10n/messages.pot +++ b/l10n/messages.pot @@ -43,6 +43,9 @@ msgstr "" msgid "Anything shared with the same group of people will show up here" msgstr "" +msgid "assembling" +msgstr "" + msgid "Avatar of {displayName}" msgstr "" @@ -82,6 +85,9 @@ msgstr "" msgid "Cancel changes" msgstr "" +msgid "Cancel uploads" +msgstr "" + msgid "Change name" msgstr "" @@ -133,6 +139,9 @@ msgstr "" msgid "Copy to clipboard" msgstr "" +msgid "Create new" +msgstr "" + #. TRANSLATORS: Ctrl key on keyboard (Windows/Linux) msgid "Ctrl" msgstr "" @@ -314,6 +323,9 @@ msgstr "" msgid "Neutral skin color" msgstr "" +msgid "New" +msgstr "" + msgid "Next" msgstr "" @@ -393,6 +405,9 @@ msgstr "" msgid "Pause slideshow" msgstr "" +msgid "paused" +msgstr "" + msgid "People & Body" msgstr "" @@ -585,6 +600,9 @@ msgstr "" msgid "Undo changes" msgstr "" +msgid "Upload" +msgstr "" + msgid "Upload file" msgstr "" @@ -594,6 +612,15 @@ msgstr "" msgid "Upload folder" msgstr "" +msgid "Upload folders" +msgstr "" + +msgid "Upload from device" +msgstr "" + +msgid "Upload progress" +msgstr "" + msgid "Uploading …" msgstr "" diff --git a/package-lock.json b/package-lock.json index fc7ea853f8..98a3ed734e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -100,6 +100,10 @@ }, "engines": { "node": "^20.11.0 || ^22 || ^24" + }, + "optionalDependencies": { + "@nextcloud/dialogs": "^7.4.0", + "@nextcloud/files": "^4.1.0-beta.0" } }, "node_modules/@asamuzakjp/css-color": { @@ -3433,7 +3437,7 @@ "version": "7.4.47", "resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz", "integrity": "sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@mdi/svg": { @@ -3663,6 +3667,98 @@ "node": "^20.0.0 || ^22.0.0 || ^24.0.0" } }, + "node_modules/@nextcloud/dialogs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@nextcloud/dialogs/-/dialogs-7.4.0.tgz", + "integrity": "sha512-XAkRls7T1xIxH+E7ArW5+gY0CrqebT+3Pxo30u7XBc21UNgc8vhl4mv5SRiDe9OgSK6/QI0bPNMvCasSaSSVpA==", + "license": "AGPL-3.0-or-later", + "optional": true, + "dependencies": { + "@mdi/js": "^7.4.47", + "@nextcloud/auth": "^2.6.0", + "@nextcloud/axios": "^2.6.0", + "@nextcloud/browser-storage": "^0.5.0", + "@nextcloud/event-bus": "^3.3.3", + "@nextcloud/files": "^4.0.0", + "@nextcloud/initial-state": "^3.0.0", + "@nextcloud/l10n": "^3.4.1", + "@nextcloud/paths": "^3.1.0", + "@nextcloud/router": "^3.1.0", + "@nextcloud/sharing": "^0.4.0", + "@nextcloud/vue": "^9.8.0", + "@types/toastify-js": "^1.12.4", + "@vueuse/core": "^14.3.0", + "p-queue": "^9.1.2", + "toastify-js": "^1.12.0", + "vue": "^3.5.34", + "webdav": "^5.10.0" + }, + "engines": { + "node": "^20 || ^22 || ^24" + } + }, + "node_modules/@nextcloud/dialogs/node_modules/@nextcloud/files": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-4.0.0.tgz", + "integrity": "sha512-TmecnZIS+PGWGtRh7RpGEboCT4K6iTbHULUcfR6hs3eEzjDVsCc1Ldf8popGY/70lbpdlfYle8xbXnPIo3qaXA==", + "license": "AGPL-3.0-or-later", + "optional": true, + "dependencies": { + "@nextcloud/auth": "^2.5.3", + "@nextcloud/capabilities": "^1.2.1", + "@nextcloud/l10n": "^3.4.1", + "@nextcloud/logger": "^3.0.3", + "@nextcloud/paths": "^3.0.0", + "@nextcloud/router": "^3.1.0", + "@nextcloud/sharing": "^0.3.0", + "is-svg": "^6.1.0", + "typescript-event-target": "^1.1.2", + "webdav": "^5.9.0" + }, + "engines": { + "node": "^24.0.0" + } + }, + "node_modules/@nextcloud/dialogs/node_modules/@nextcloud/files/node_modules/@nextcloud/files": { + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.12.2.tgz", + "integrity": "sha512-vBo8tf3Xh6efiF8CrEo3pKj9AtvAF6RdDGO1XKL65IxV8+UUd9Uxl2lUExHlzoDRRczCqfGfaWfRRaFhYqce5Q==", + "license": "AGPL-3.0-or-later", + "optional": true, + "dependencies": { + "@nextcloud/auth": "^2.5.3", + "@nextcloud/capabilities": "^1.2.1", + "@nextcloud/l10n": "^3.4.1", + "@nextcloud/logger": "^3.0.3", + "@nextcloud/paths": "^3.0.0", + "@nextcloud/router": "^3.1.0", + "@nextcloud/sharing": "^0.3.0", + "cancelable-promise": "^4.3.1", + "is-svg": "^6.1.0", + "typescript-event-target": "^1.1.1", + "webdav": "^5.8.0" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } + }, + "node_modules/@nextcloud/dialogs/node_modules/@nextcloud/files/node_modules/@nextcloud/sharing": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.3.0.tgz", + "integrity": "sha512-kV7qeUZvd1fTKeFyH+W5Qq5rNOqG9rLATZM3U9MBxWXHJs3OxMqYQb8UQ3NYONzsX3zDGJmdQECIGHm1ei2sCA==", + "license": "GPL-3.0-or-later", + "optional": true, + "dependencies": { + "@nextcloud/initial-state": "^3.0.0", + "is-svg": "^6.1.0" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + }, + "optionalDependencies": { + "@nextcloud/files": "^3.12.0" + } + }, "node_modules/@nextcloud/eslint-config": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/@nextcloud/eslint-config/-/eslint-config-9.0.1.tgz", @@ -3751,43 +3847,28 @@ } }, "node_modules/@nextcloud/files": { - "version": "3.12.2", - "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.12.2.tgz", - "integrity": "sha512-vBo8tf3Xh6efiF8CrEo3pKj9AtvAF6RdDGO1XKL65IxV8+UUd9Uxl2lUExHlzoDRRczCqfGfaWfRRaFhYqce5Q==", + "version": "4.1.0-beta.0", + "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-4.1.0-beta.0.tgz", + "integrity": "sha512-0V6yO3yVUkYb5hzo4FAl1Rp/ysSr6D56fvq+5W/mhIoha0hTecfvx05Q+yzQJAziCBxLeMrWIuXrY4epDE/rZQ==", "license": "AGPL-3.0-or-later", "optional": true, "dependencies": { - "@nextcloud/auth": "^2.5.3", + "@nextcloud/auth": "^2.6.0", + "@nextcloud/axios": "^2.6.0", "@nextcloud/capabilities": "^1.2.1", "@nextcloud/l10n": "^3.4.1", "@nextcloud/logger": "^3.0.3", - "@nextcloud/paths": "^3.0.0", + "@nextcloud/paths": "^3.1.0", "@nextcloud/router": "^3.1.0", - "@nextcloud/sharing": "^0.3.0", - "cancelable-promise": "^4.3.1", + "@nextcloud/sharing": "^0.4.0", + "axios-retry": "^4.5.0", "is-svg": "^6.1.0", - "typescript-event-target": "^1.1.1", - "webdav": "^5.8.0" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@nextcloud/files/node_modules/@nextcloud/sharing": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.3.0.tgz", - "integrity": "sha512-kV7qeUZvd1fTKeFyH+W5Qq5rNOqG9rLATZM3U9MBxWXHJs3OxMqYQb8UQ3NYONzsX3zDGJmdQECIGHm1ei2sCA==", - "license": "GPL-3.0-or-later", - "optional": true, - "dependencies": { - "@nextcloud/initial-state": "^3.0.0", - "is-svg": "^6.1.0" + "p-queue": "^9.3.0", + "typescript-event-target": "^1.1.2", + "webdav": "^5.10.0" }, "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - }, - "optionalDependencies": { - "@nextcloud/files": "^3.12.0" + "node": "^24.0.0 || >=26.0.0" } }, "node_modules/@nextcloud/initial-state": { @@ -3865,6 +3946,68 @@ "@nextcloud/files": "^3.12.2 || ^4.0.0" } }, + "node_modules/@nextcloud/sharing/node_modules/@nextcloud/files": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-4.0.0.tgz", + "integrity": "sha512-TmecnZIS+PGWGtRh7RpGEboCT4K6iTbHULUcfR6hs3eEzjDVsCc1Ldf8popGY/70lbpdlfYle8xbXnPIo3qaXA==", + "license": "AGPL-3.0-or-later", + "optional": true, + "dependencies": { + "@nextcloud/auth": "^2.5.3", + "@nextcloud/capabilities": "^1.2.1", + "@nextcloud/l10n": "^3.4.1", + "@nextcloud/logger": "^3.0.3", + "@nextcloud/paths": "^3.0.0", + "@nextcloud/router": "^3.1.0", + "@nextcloud/sharing": "^0.3.0", + "is-svg": "^6.1.0", + "typescript-event-target": "^1.1.2", + "webdav": "^5.9.0" + }, + "engines": { + "node": "^24.0.0" + } + }, + "node_modules/@nextcloud/sharing/node_modules/@nextcloud/sharing": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.3.0.tgz", + "integrity": "sha512-kV7qeUZvd1fTKeFyH+W5Qq5rNOqG9rLATZM3U9MBxWXHJs3OxMqYQb8UQ3NYONzsX3zDGJmdQECIGHm1ei2sCA==", + "license": "GPL-3.0-or-later", + "optional": true, + "dependencies": { + "@nextcloud/initial-state": "^3.0.0", + "is-svg": "^6.1.0" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + }, + "optionalDependencies": { + "@nextcloud/files": "^3.12.0" + } + }, + "node_modules/@nextcloud/sharing/node_modules/@nextcloud/sharing/node_modules/@nextcloud/files": { + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.12.2.tgz", + "integrity": "sha512-vBo8tf3Xh6efiF8CrEo3pKj9AtvAF6RdDGO1XKL65IxV8+UUd9Uxl2lUExHlzoDRRczCqfGfaWfRRaFhYqce5Q==", + "license": "AGPL-3.0-or-later", + "optional": true, + "dependencies": { + "@nextcloud/auth": "^2.5.3", + "@nextcloud/capabilities": "^1.2.1", + "@nextcloud/l10n": "^3.4.1", + "@nextcloud/logger": "^3.0.3", + "@nextcloud/paths": "^3.0.0", + "@nextcloud/router": "^3.1.0", + "@nextcloud/sharing": "^0.3.0", + "cancelable-promise": "^4.3.1", + "is-svg": "^6.1.0", + "typescript-event-target": "^1.1.1", + "webdav": "^5.8.0" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } + }, "node_modules/@nextcloud/stylelint-config": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@nextcloud/stylelint-config/-/stylelint-config-3.2.2.tgz", @@ -3924,6 +4067,62 @@ "vite": "^7.3.6" } }, + "node_modules/@nextcloud/vue": { + "version": "9.8.2", + "resolved": "https://registry.npmjs.org/@nextcloud/vue/-/vue-9.8.2.tgz", + "integrity": "sha512-bkU6E/AyMXzNHk4BnUCL8QGfpT0BZkS7fooKhClHa9VK8wR6Mml/yO6/1Nd+E71dEiDKJP8B7TsvEXgytZsVQQ==", + "license": "AGPL-3.0-or-later", + "optional": true, + "dependencies": { + "@ckpack/vue-color": "^1.6.0", + "@floating-ui/dom": "^1.7.6", + "@nextcloud/auth": "^2.6.0", + "@nextcloud/axios": "^2.6.0", + "@nextcloud/browser-storage": "^0.5.0", + "@nextcloud/capabilities": "^1.2.1", + "@nextcloud/event-bus": "^3.3.3", + "@nextcloud/initial-state": "^3.0.0", + "@nextcloud/l10n": "^3.4.1", + "@nextcloud/logger": "^3.0.3", + "@nextcloud/router": "^3.1.0", + "@nextcloud/sharing": "^0.4.0", + "@nextcloud/vue-select": "^4.1.0", + "@vuepic/vue-datepicker": "^11.0.3", + "@vueuse/components": "^14.3.0", + "@vueuse/core": "^14.3.0", + "blurhash": "^2.0.5", + "clone": "^2.1.2", + "debounce": "^3.0.0", + "dompurify": "^3.4.7", + "emoji-mart-vue-fast": "^15.0.5", + "escape-html": "^1.0.3", + "floating-vue": "^5.2.2", + "focus-trap": "^8.2.1", + "linkifyjs": "^4.3.3", + "mdast-util-to-string": "^4.0.0", + "p-queue": "^9.3.0", + "rehype-external-links": "^3.0.0", + "rehype-highlight": "^7.0.2", + "rehype-react": "^8.0.0", + "remark-breaks": "^4.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "remark-unlink-protocols": "^1.0.0", + "splitpanes": "^4.0.4", + "striptags": "^3.2.0", + "tabbable": "^6.4.0", + "tributejs": "^5.1.3", + "ts-md5": "^2.0.1", + "unified": "^11.0.5", + "unist-builder": "^4.0.0", + "unist-util-visit-parents": "^6.0.2", + "vue": "^3.5.18", + "vue-router": "^5.0.7" + }, + "engines": { + "node": "^20.11.0 || ^22 || ^24" + } + }, "node_modules/@nextcloud/vue-select": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@nextcloud/vue-select/-/vue-select-4.1.0.tgz", @@ -5684,6 +5883,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/toastify-js": { + "version": "1.12.4", + "resolved": "https://registry.npmjs.org/@types/toastify-js/-/toastify-js-1.12.4.tgz", + "integrity": "sha512-zfZHU4tKffPCnZRe7pjv/eFKzTVHozKewFCKaCjZ4gFinKgJRz/t0bkZiMCXJxPhv/ZoeDGNOeRD09R0kQZ/nw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -7502,6 +7708,19 @@ "proxy-from-env": "^2.1.0" } }, + "node_modules/axios-retry": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz", + "integrity": "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "is-retry-allowed": "^2.2.0" + }, + "peerDependencies": { + "axios": "0.x || 1.x" + } + }, "node_modules/babel-loader": { "version": "9.2.1", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", @@ -15133,6 +15352,19 @@ "node": ">=0.10.0" } }, + "node_modules/is-retry-allowed": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", + "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-root": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", @@ -24930,6 +25162,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/toastify-js": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/toastify-js/-/toastify-js-1.12.0.tgz", + "integrity": "sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ==", + "license": "MIT", + "optional": true + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", diff --git a/package.json b/package.json index 5a2dc68f26..5abd7e1fd2 100644 --- a/package.json +++ b/package.json @@ -164,6 +164,10 @@ "webpack": "^5.110.1", "webpack-merge": "^6.0.1" }, + "optionalDependencies": { + "@nextcloud/dialogs": "^7.4.0", + "@nextcloud/files": "^4.1.0-beta.0" + }, "engines": { "node": "^20.11.0 || ^22 || ^24" }, diff --git a/src/components/NcUploadPicker/NcUploadPicker.vue b/src/components/NcUploadPicker/NcUploadPicker.vue new file mode 100644 index 0000000000..7846815e2a --- /dev/null +++ b/src/components/NcUploadPicker/NcUploadPicker.vue @@ -0,0 +1,590 @@ + + + + + + + + + +## Requirements + +To use the `NcUploadPicker` component, you need to install the following packages: +- `@nextcloud/dialogs` +- `@nextcloud/files` + +## Usage + +**TODO**: Add usage example here + diff --git a/src/components/NcUploadPicker/index.ts b/src/components/NcUploadPicker/index.ts new file mode 100644 index 0000000000..513b7c5ad0 --- /dev/null +++ b/src/components/NcUploadPicker/index.ts @@ -0,0 +1,6 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +export { default } from './NcUploadPicker.vue' From b68faac1b663d2152e1d5747bf5c1d6c96371714 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Mon, 17 Nov 2025 16:34:53 +0100 Subject: [PATCH 02/14] chore: drop hotkey handling - this should be handled by each app Signed-off-by: Ferdinand Thiessen --- src/components/NcUploadPicker/NcUploadPicker.vue | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/components/NcUploadPicker/NcUploadPicker.vue b/src/components/NcUploadPicker/NcUploadPicker.vue index 7846815e2a..52393f40c5 100644 --- a/src/components/NcUploadPicker/NcUploadPicker.vue +++ b/src/components/NcUploadPicker/NcUploadPicker.vue @@ -188,7 +188,6 @@ import NcActionSeparator from '../NcActionSeparator/NcActionSeparator.vue' import NcButton from '../NcButton/NcButton.vue' import NcIconSvgWrapper from '../NcIconSvgWrapper/NcIconSvgWrapper.vue' import NcProgressBar from '../NcProgressBar/NcProgressBar.vue' -import { useHotKey } from '../../composables/useHotKey/index.ts' import { t } from '../../l10n.ts' import { logger } from '../../utils/logger.ts' @@ -390,18 +389,6 @@ export default defineComponent({ // Update data on upload progress this.uploadManager.addNotifier(this.onUploadCompletion) - // Register hotkeys - useHotKey('u', this.onKeyDown, { - stop: true, - prevent: true, - shift: true, - }) - - useHotKey('Escape', this.onKeyDown, { - stop: true, - prevent: true, - }) - logger.debug('UploadPicker initialised') }, From 053495835b4a11018c9c48eed59d24d17098fea6 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Mon, 17 Nov 2025 17:20:15 +0100 Subject: [PATCH 03/14] refactor(NcUploadPicker): migrate component to script-setup Signed-off-by: Ferdinand Thiessen --- .../NcUploadPicker/NcUploadPicker.vue | 548 ++++++++---------- 1 file changed, 246 insertions(+), 302 deletions(-) diff --git a/src/components/NcUploadPicker/NcUploadPicker.vue b/src/components/NcUploadPicker/NcUploadPicker.vue index 52393f40c5..96b103f44a 100644 --- a/src/components/NcUploadPicker/NcUploadPicker.vue +++ b/src/components/NcUploadPicker/NcUploadPicker.vue @@ -168,15 +168,16 @@ - From 67799a7cd38d6732682a298c0ab0a80b91c68183 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Sun, 28 Jun 2026 00:06:05 +0200 Subject: [PATCH 07/14] refactor(NcUploadPicker): migrate to nextcloud-files v4.1.0 Signed-off-by: Ferdinand Thiessen --- l10n/messages.pot | 8 +- package-lock.json | 282 ++++++------ package.json | 14 +- src/components/NcFilePicker/NcFilePicker.vue | 1 + .../NcUploadPicker/NcUploadPicker.vue | 431 +++++++++--------- src/components/NcUploadPicker/index.ts | 6 + src/components/index.ts | 1 + 7 files changed, 381 insertions(+), 362 deletions(-) diff --git a/l10n/messages.pot b/l10n/messages.pot index 813d0bae27..26cd172910 100644 --- a/l10n/messages.pot +++ b/l10n/messages.pot @@ -43,6 +43,7 @@ msgstr "" msgid "Anything shared with the same group of people will show up here" msgstr "" +#. TRANSLATORS: State of the current upload - chunks of the uploaded files are being assembled into the final file msgid "assembling" msgstr "" @@ -139,9 +140,6 @@ msgstr "" msgid "Copy to clipboard" msgstr "" -msgid "Create new" -msgstr "" - #. TRANSLATORS: Ctrl key on keyboard (Windows/Linux) msgid "Ctrl" msgstr "" @@ -207,6 +205,9 @@ msgstr "" msgid "Escape" msgstr "" +msgid "Estimating …" +msgstr "" + msgid "External documentation" msgstr "" @@ -405,6 +406,7 @@ msgstr "" msgid "Pause slideshow" msgstr "" +#. TRANSLATORS: State of the current upload - it is paused msgid "paused" msgstr "" diff --git a/package-lock.json b/package-lock.json index 98a3ed734e..7470d2a09e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,7 +63,9 @@ "@mdi/svg": "^7.4.47", "@nextcloud/babel-config": "^1.3.0", "@nextcloud/browserslist-config": "^3.1.2", + "@nextcloud/dialogs": "^7.5.0", "@nextcloud/eslint-config": "^9.0.1", + "@nextcloud/files": "^4.1.0-beta.2", "@nextcloud/stylelint-config": "^3.2.2", "@nextcloud/vite-config": "^2.5.0", "@nextcloud/webpack-vue-config": "github:nextcloud/webpack-vue-config#vue3", @@ -101,9 +103,17 @@ "engines": { "node": "^20.11.0 || ^22 || ^24" }, - "optionalDependencies": { + "peerDependencies": { "@nextcloud/dialogs": "^7.4.0", - "@nextcloud/files": "^4.1.0-beta.0" + "@nextcloud/files": "^4.1.0-beta.2" + }, + "peerDependenciesMeta": { + "@nextcloud/dialogs": { + "optional": true + }, + "@nextcloud/files": { + "optional": true + } } }, "node_modules/@asamuzakjp/css-color": { @@ -510,12 +520,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.29.7" + "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -1819,9 +1829,9 @@ } }, "node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.29.7", @@ -1858,8 +1868,8 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/@buttercup/fetch/-/fetch-0.2.1.tgz", "integrity": "sha512-sCgECOx8wiqY8NN1xN22BqqKzXYIG2AicNLlakOAI4f0WgyLVUbAigMf8CZhBtJxdudTcB1gD5lciqi44jwJvg==", + "devOptional": true, "license": "MIT", - "optional": true, "optionalDependencies": { "node-fetch": "^3.3.0" } @@ -3437,7 +3447,7 @@ "version": "7.4.47", "resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz", "integrity": "sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==", - "devOptional": true, + "dev": true, "license": "Apache-2.0" }, "node_modules/@mdi/svg": { @@ -3668,16 +3678,17 @@ } }, "node_modules/@nextcloud/dialogs": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@nextcloud/dialogs/-/dialogs-7.4.0.tgz", - "integrity": "sha512-XAkRls7T1xIxH+E7ArW5+gY0CrqebT+3Pxo30u7XBc21UNgc8vhl4mv5SRiDe9OgSK6/QI0bPNMvCasSaSSVpA==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@nextcloud/dialogs/-/dialogs-7.5.0.tgz", + "integrity": "sha512-2gmv0V3ObphxRd7ZvXwTsPvn/XSf2vummdXb2LRFpkoSamWbk+41SWv7cnDQPC+Ve9p46WebDuqyG+wbefkhEw==", + "dev": true, "license": "AGPL-3.0-or-later", - "optional": true, "dependencies": { "@mdi/js": "^7.4.47", "@nextcloud/auth": "^2.6.0", "@nextcloud/axios": "^2.6.0", "@nextcloud/browser-storage": "^0.5.0", + "@nextcloud/capabilities": "^1.2.1", "@nextcloud/event-bus": "^3.3.3", "@nextcloud/files": "^4.0.0", "@nextcloud/initial-state": "^3.0.0", @@ -3685,12 +3696,10 @@ "@nextcloud/paths": "^3.1.0", "@nextcloud/router": "^3.1.0", "@nextcloud/sharing": "^0.4.0", - "@nextcloud/vue": "^9.8.0", - "@types/toastify-js": "^1.12.4", - "@vueuse/core": "^14.3.0", - "p-queue": "^9.1.2", - "toastify-js": "^1.12.0", - "vue": "^3.5.34", + "@nextcloud/vue": "^9.9.0", + "@vueuse/core": "^14.4.0", + "p-queue": "^9.3.3", + "vue": "^3.5.41", "webdav": "^5.10.0" }, "engines": { @@ -3701,8 +3710,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-4.0.0.tgz", "integrity": "sha512-TmecnZIS+PGWGtRh7RpGEboCT4K6iTbHULUcfR6hs3eEzjDVsCc1Ldf8popGY/70lbpdlfYle8xbXnPIo3qaXA==", + "dev": true, "license": "AGPL-3.0-or-later", - "optional": true, "dependencies": { "@nextcloud/auth": "^2.5.3", "@nextcloud/capabilities": "^1.2.1", @@ -3723,6 +3732,7 @@ "version": "3.12.2", "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.12.2.tgz", "integrity": "sha512-vBo8tf3Xh6efiF8CrEo3pKj9AtvAF6RdDGO1XKL65IxV8+UUd9Uxl2lUExHlzoDRRczCqfGfaWfRRaFhYqce5Q==", + "dev": true, "license": "AGPL-3.0-or-later", "optional": true, "dependencies": { @@ -3746,8 +3756,8 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.3.0.tgz", "integrity": "sha512-kV7qeUZvd1fTKeFyH+W5Qq5rNOqG9rLATZM3U9MBxWXHJs3OxMqYQb8UQ3NYONzsX3zDGJmdQECIGHm1ei2sCA==", + "dev": true, "license": "GPL-3.0-or-later", - "optional": true, "dependencies": { "@nextcloud/initial-state": "^3.0.0", "is-svg": "^6.1.0" @@ -3847,11 +3857,11 @@ } }, "node_modules/@nextcloud/files": { - "version": "4.1.0-beta.0", - "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-4.1.0-beta.0.tgz", - "integrity": "sha512-0V6yO3yVUkYb5hzo4FAl1Rp/ysSr6D56fvq+5W/mhIoha0hTecfvx05Q+yzQJAziCBxLeMrWIuXrY4epDE/rZQ==", + "version": "4.1.0-beta.2", + "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-4.1.0-beta.2.tgz", + "integrity": "sha512-tJlhPT8oAmdgyYJtrh4ufrRXfFS+LRZzZdTio64S8izTBXML0Gu/7PVUcOUglY0x/fFgUS5euc0GwNsZZecjbw==", + "dev": true, "license": "AGPL-3.0-or-later", - "optional": true, "dependencies": { "@nextcloud/auth": "^2.6.0", "@nextcloud/axios": "^2.6.0", @@ -3863,7 +3873,7 @@ "@nextcloud/sharing": "^0.4.0", "axios-retry": "^4.5.0", "is-svg": "^6.1.0", - "p-queue": "^9.3.0", + "p-queue": "^9.3.3", "typescript-event-target": "^1.1.2", "webdav": "^5.10.0" }, @@ -3912,8 +3922,8 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@nextcloud/paths/-/paths-3.1.0.tgz", "integrity": "sha512-vtFYA/kthaUDzu6KejTOL1OwnOy7/yynq5zdB/UBpYacAWjUX5Ddh4OMWx3rEavkBJ9/QGhrFryNJLjNfe8OQA==", + "devOptional": true, "license": "GPL-3.0-or-later", - "optional": true, "engines": { "node": "^20.0.0 || ^22.0.0 || ^24.0.0" } @@ -4068,14 +4078,14 @@ } }, "node_modules/@nextcloud/vue": { - "version": "9.8.2", - "resolved": "https://registry.npmjs.org/@nextcloud/vue/-/vue-9.8.2.tgz", - "integrity": "sha512-bkU6E/AyMXzNHk4BnUCL8QGfpT0BZkS7fooKhClHa9VK8wR6Mml/yO6/1Nd+E71dEiDKJP8B7TsvEXgytZsVQQ==", + "version": "9.11.0", + "resolved": "https://registry.npmjs.org/@nextcloud/vue/-/vue-9.11.0.tgz", + "integrity": "sha512-LRsyU9Mxs0b2xWqbxygps8O7/i1z4QhNQD9/QDni+VqFY3V0N95mAgmKJm6vfoFuxTAMXERp9bjqFn3UV542gw==", + "dev": true, "license": "AGPL-3.0-or-later", - "optional": true, "dependencies": { "@ckpack/vue-color": "^1.6.0", - "@floating-ui/dom": "^1.7.6", + "@floating-ui/dom": "^1.8.0", "@nextcloud/auth": "^2.6.0", "@nextcloud/axios": "^2.6.0", "@nextcloud/browser-storage": "^0.5.0", @@ -4088,19 +4098,19 @@ "@nextcloud/sharing": "^0.4.0", "@nextcloud/vue-select": "^4.1.0", "@vuepic/vue-datepicker": "^11.0.3", - "@vueuse/components": "^14.3.0", - "@vueuse/core": "^14.3.0", + "@vueuse/components": "^14.4.0", + "@vueuse/core": "^14.4.0", "blurhash": "^2.0.5", "clone": "^2.1.2", "debounce": "^3.0.0", - "dompurify": "^3.4.7", + "dompurify": "^3.4.14", "emoji-mart-vue-fast": "^15.0.5", "escape-html": "^1.0.3", "floating-vue": "^5.2.2", - "focus-trap": "^8.2.1", + "focus-trap": "^8.2.2", "linkifyjs": "^4.3.3", "mdast-util-to-string": "^4.0.0", - "p-queue": "^9.3.0", + "p-queue": "^9.3.3", "rehype-external-links": "^3.0.0", "rehype-highlight": "^7.0.2", "rehype-react": "^8.0.0", @@ -4117,7 +4127,7 @@ "unist-builder": "^4.0.0", "unist-util-visit-parents": "^6.0.2", "vue": "^3.5.18", - "vue-router": "^5.0.7" + "vue-router": "^5.2.0" }, "engines": { "node": "^20.11.0 || ^22 || ^24" @@ -5883,13 +5893,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/toastify-js": { - "version": "1.12.4", - "resolved": "https://registry.npmjs.org/@types/toastify-js/-/toastify-js-1.12.4.tgz", - "integrity": "sha512-zfZHU4tKffPCnZRe7pjv/eFKzTVHozKewFCKaCjZ4gFinKgJRz/t0bkZiMCXJxPhv/ZoeDGNOeRD09R0kQZ/nw==", - "license": "MIT", - "optional": true - }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -6503,13 +6506,13 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.34", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.34.tgz", - "integrity": "sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==", + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.41.tgz", + "integrity": "sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.3", - "@vue/shared": "3.5.34", + "@babel/parser": "^7.29.8", + "@vue/shared": "3.5.41", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" @@ -6528,40 +6531,40 @@ } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.34", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.34.tgz", - "integrity": "sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==", + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.41.tgz", + "integrity": "sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.34", - "@vue/shared": "3.5.34" + "@vue/compiler-core": "3.5.41", + "@vue/shared": "3.5.41" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.34", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.34.tgz", - "integrity": "sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==", + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.41.tgz", + "integrity": "sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.3", - "@vue/compiler-core": "3.5.34", - "@vue/compiler-dom": "3.5.34", - "@vue/compiler-ssr": "3.5.34", - "@vue/shared": "3.5.34", + "@babel/parser": "^7.29.8", + "@vue/compiler-core": "3.5.41", + "@vue/compiler-dom": "3.5.41", + "@vue/compiler-ssr": "3.5.41", + "@vue/shared": "3.5.41", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", - "postcss": "^8.5.14", + "postcss": "^8.5.19", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.34", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.34.tgz", - "integrity": "sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==", + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.41.tgz", + "integrity": "sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.34", - "@vue/shared": "3.5.34" + "@vue/compiler-dom": "3.5.41", + "@vue/shared": "3.5.41" } }, "node_modules/@vue/compiler-vue2": { @@ -6654,53 +6657,51 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.5.34", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.34.tgz", - "integrity": "sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==", + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.41.tgz", + "integrity": "sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.34" + "@vue/shared": "3.5.41" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.34", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.34.tgz", - "integrity": "sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==", + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.41.tgz", + "integrity": "sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.34", - "@vue/shared": "3.5.34" + "@vue/reactivity": "3.5.41", + "@vue/shared": "3.5.41" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.34", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.34.tgz", - "integrity": "sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==", + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.41.tgz", + "integrity": "sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.34", - "@vue/runtime-core": "3.5.34", - "@vue/shared": "3.5.34", + "@vue/reactivity": "3.5.41", + "@vue/runtime-core": "3.5.41", + "@vue/shared": "3.5.41", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.34", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.34.tgz", - "integrity": "sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==", + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.41.tgz", + "integrity": "sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.34", - "@vue/shared": "3.5.34" - }, - "peerDependencies": { - "vue": "3.5.34" + "@vue/compiler-ssr": "3.5.41", + "@vue/runtime-dom": "3.5.41", + "@vue/shared": "3.5.41" } }, "node_modules/@vue/shared": { - "version": "3.5.34", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.34.tgz", - "integrity": "sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==", + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.41.tgz", + "integrity": "sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==", "license": "MIT" }, "node_modules/@vue/test-utils": { @@ -7712,8 +7713,8 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz", "integrity": "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==", + "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "is-retry-allowed": "^2.2.0" }, @@ -7845,8 +7846,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==", - "license": "MIT", - "optional": true + "devOptional": true, + "license": "MIT" }, "node_modules/base/node_modules/define-property": { "version": "1.0.0", @@ -8653,8 +8654,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/byte-length/-/byte-length-1.0.2.tgz", "integrity": "sha512-ovBpjmsgd/teRmgcPh23d4gJvxDoXtAzEL9xTfMU8Yc2kqCDb7L9jAG0XHl1nzuGl+h3ebCIF1i62UFyA9V/2Q==", - "license": "MIT", - "optional": true + "devOptional": true, + "license": "MIT" }, "node_modules/bytes": { "version": "3.1.2", @@ -9050,8 +9051,8 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "devOptional": true, "license": "BSD-3-Clause", - "optional": true, "engines": { "node": "*" } @@ -10262,8 +10263,8 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "devOptional": true, "license": "BSD-3-Clause", - "optional": true, "engines": { "node": "*" } @@ -10414,8 +10415,8 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "devOptional": true, "license": "MIT", - "optional": true, "engines": { "node": ">= 12" } @@ -12631,6 +12632,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "devOptional": true, "funding": [ { "type": "github", @@ -12642,7 +12644,6 @@ } ], "license": "MIT", - "optional": true, "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" @@ -13252,8 +13253,8 @@ "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "fetch-blob": "^3.1.2" }, @@ -14237,8 +14238,8 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/hot-patcher/-/hot-patcher-2.0.1.tgz", "integrity": "sha512-ECg1JFG0YzehicQaogenlcs2qg6WsXQsxtnbr1i696u5tLUjtJdQAh0u2g0Q5YV45f263Ta1GnUJsc8WIfJf4Q==", - "license": "MIT", - "optional": true + "devOptional": true, + "license": "MIT" }, "node_modules/hpack.js": { "version": "2.1.6", @@ -15356,8 +15357,8 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", + "dev": true, "license": "MIT", - "optional": true, "engines": { "node": ">=10" }, @@ -16081,8 +16082,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/layerr/-/layerr-3.0.0.tgz", "integrity": "sha512-tv754Ki2dXpPVApOrjTyRo4/QegVb9eVFq4mjqp4+NM5NaX7syQvN5BBNfV/ZpAHCEHV24XdUVrBAoka4jt3pA==", - "license": "MIT", - "optional": true + "devOptional": true, + "license": "MIT" }, "node_modules/leven": { "version": "2.1.0", @@ -16520,8 +16521,8 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "devOptional": true, "license": "BSD-3-Clause", - "optional": true, "dependencies": { "charenc": "0.0.2", "crypt": "0.0.2", @@ -16544,8 +16545,8 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "license": "MIT", - "optional": true + "devOptional": true, + "license": "MIT" }, "node_modules/mdast-squeeze-paragraphs": { "version": "6.0.0", @@ -18315,8 +18316,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/nested-property/-/nested-property-4.0.0.tgz", "integrity": "sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA==", - "license": "MIT", - "optional": true + "devOptional": true, + "license": "MIT" }, "node_modules/nice-try": { "version": "1.0.5", @@ -18350,6 +18351,7 @@ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", "deprecated": "Use your platform's native DOMException instead", + "devOptional": true, "funding": [ { "type": "github", @@ -18361,7 +18363,6 @@ } ], "license": "MIT", - "optional": true, "engines": { "node": ">=10.5.0" } @@ -18370,8 +18371,8 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", @@ -19328,8 +19329,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz", "integrity": "sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA==", - "license": "ISC", - "optional": true + "devOptional": true, + "license": "ISC" }, "node_modules/path-scurry": { "version": "2.0.2", @@ -25162,13 +25163,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/toastify-js": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/toastify-js/-/toastify-js-1.12.0.tgz", - "integrity": "sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ==", - "license": "MIT", - "optional": true - }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -25527,8 +25521,8 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/typescript-event-target/-/typescript-event-target-1.1.2.tgz", "integrity": "sha512-TvkrTUpv7gCPlcnSoEwUVUBwsdheKm+HF5u2tPAKubkIGMfovdSizCTaZRY/NhR8+Ijy8iZZUapbVQAsNrkFrw==", - "license": "MIT", - "optional": true + "devOptional": true, + "license": "MIT" }, "node_modules/ufo": { "version": "1.6.4", @@ -25998,8 +25992,8 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz", "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==", + "devOptional": true, "license": "MIT", - "optional": true, "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } @@ -26896,16 +26890,16 @@ "license": "MIT" }, "node_modules/vue": { - "version": "3.5.34", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.34.tgz", - "integrity": "sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==", + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.41.tgz", + "integrity": "sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.34", - "@vue/compiler-sfc": "3.5.34", - "@vue/runtime-dom": "3.5.34", - "@vue/server-renderer": "3.5.34", - "@vue/shared": "3.5.34" + "@vue/compiler-dom": "3.5.41", + "@vue/compiler-sfc": "3.5.41", + "@vue/runtime-dom": "3.5.41", + "@vue/server-renderer": "3.5.41", + "@vue/shared": "3.5.41" }, "peerDependencies": { "typescript": "*" @@ -29309,8 +29303,8 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "devOptional": true, "license": "MIT", - "optional": true, "engines": { "node": ">= 8" } @@ -29319,8 +29313,8 @@ "version": "5.10.0", "resolved": "https://registry.npmjs.org/webdav/-/webdav-5.10.0.tgz", "integrity": "sha512-fVPuRLtcduVGvSO7Tn/6TQCzIvI/g6BO/+xPRctCvi/GytYpjn4czxWbh4HsArsdom9qz9BI63k9/v2HBUui1A==", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "@buttercup/fetch": "^0.2.1", "base-64": "^1.0.0", @@ -29345,8 +29339,8 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -29355,8 +29349,8 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "devOptional": true, "license": "BSD-2-Clause", - "optional": true, "engines": { "node": ">=0.12" }, @@ -29368,8 +29362,8 @@ "version": "9.0.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "devOptional": true, "license": "ISC", - "optional": true, "dependencies": { "brace-expansion": "^2.0.2" }, diff --git a/package.json b/package.json index 5abd7e1fd2..84286901a8 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,9 @@ "@mdi/svg": "^7.4.47", "@nextcloud/babel-config": "^1.3.0", "@nextcloud/browserslist-config": "^3.1.2", + "@nextcloud/dialogs": "^7.5.0", "@nextcloud/eslint-config": "^9.0.1", + "@nextcloud/files": "^4.1.0-beta.2", "@nextcloud/stylelint-config": "^3.2.2", "@nextcloud/vite-config": "^2.5.0", "@nextcloud/webpack-vue-config": "github:nextcloud/webpack-vue-config#vue3", @@ -164,9 +166,17 @@ "webpack": "^5.110.1", "webpack-merge": "^6.0.1" }, - "optionalDependencies": { + "peerDependencies": { "@nextcloud/dialogs": "^7.4.0", - "@nextcloud/files": "^4.1.0-beta.0" + "@nextcloud/files": "^4.1.0-beta.2" + }, + "peerDependenciesMeta": { + "@nextcloud/dialogs": { + "optional": true + }, + "@nextcloud/files": { + "optional": true + } }, "engines": { "node": "^20.11.0 || ^22 || ^24" diff --git a/src/components/NcFilePicker/NcFilePicker.vue b/src/components/NcFilePicker/NcFilePicker.vue index 3bf1a0dfef..8fa267fc02 100644 --- a/src/components/NcFilePicker/NcFilePicker.vue +++ b/src/components/NcFilePicker/NcFilePicker.vue @@ -129,6 +129,7 @@ defineSlots<{ defineExpose({ reset, + triggerPickFiles, }) const formElement = useTemplateRef('form') diff --git a/src/components/NcUploadPicker/NcUploadPicker.vue b/src/components/NcUploadPicker/NcUploadPicker.vue index b5691d7b15..af94cdc947 100644 --- a/src/components/NcUploadPicker/NcUploadPicker.vue +++ b/src/components/NcUploadPicker/NcUploadPicker.vue @@ -3,145 +3,32 @@ - SPDX-License-Identifier: AGPL-3.0-or-later --> - - + + +``` diff --git a/styleguide.config.cjs b/styleguide.config.cjs index 040a2177cb..6d850c68bd 100644 --- a/styleguide.config.cjs +++ b/styleguide.config.cjs @@ -64,6 +64,13 @@ module.exports = async () => { alias: { vue$: 'vue/dist/vue.esm-browser.js', }, + byDependency: { + // The code of the documentation examples is transpiled to CommonJS, + // so without the `import` condition ESM only packages - like `@nextcloud/files` - cannot be imported by them + commonjs: { + conditionNames: ['require', 'import', 'module', 'browser', 'default'], + }, + }, }, }), diff --git a/styleguide/global.requires.js b/styleguide/global.requires.js index 04a4f135e5..06a1be4e47 100644 --- a/styleguide/global.requires.js +++ b/styleguide/global.requires.js @@ -10,6 +10,7 @@ import { isA11yActivation } from '../src/functions/a11y/index.ts' import { spawnDialog } from '../src/functions/dialog/index.ts' import { emojiAddRecent, emojiSearch, EmojiSkinTone, getCurrentSkinTone, setCurrentSkinTone } from '../src/functions/emoji/index.ts' import { usernameToColor } from '../src/functions/usernameToColor/index.ts' +import { mockWebDav } from './webdav.mock.js' import 'regenerator-runtime/runtime.js' import 'core-js/stable/index.js' @@ -31,8 +32,13 @@ function mockRequests(error) { const { request, config } = error let data = null + // Failures without a response, like aborted requests, cannot be mocked + if (!request?.responseURL) { + return Promise.reject(error) + } + // Mock resolved link references for NcRichText - const resolveReferences = request.responseURL.match(/references\/resolvePublic/) + const resolveReferences = request.responseURL.match(/references\/resolve(Public)?\?/) if (resolveReferences) { const referenceValue = new URL(request.responseURL).searchParams.get('reference') data = { @@ -82,6 +88,9 @@ function mockRequests(error) { axios.interceptors.response.use((r) => r, (e) => mockRequests(e)) +// Fake the WebDAV endpoints used for uploading files +mockWebDav() + // app name fallback window.appName = 'nextcloud-vue' diff --git a/styleguide/webdav.mock.js b/styleguide/webdav.mock.js new file mode 100644 index 0000000000..b90e2ff016 --- /dev/null +++ b/styleguide/webdav.mock.js @@ -0,0 +1,142 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import axios from '@nextcloud/axios' + +/** Requests to this endpoint are answered by the fake WebDAV server */ +const DAV_ENDPOINT = /\/(remote|public)\.php\/dav\// + +/** Simulated transfer speed in bytes per second */ +const UPLOAD_SPEED = 4 * 1024 * 1024 + +/** Minimum duration of a simulated upload in milliseconds, so the progress stays visible for small files */ +const MIN_UPLOAD_DURATION = 2500 + +/** Delay between two simulated upload progress events in milliseconds */ +const PROGRESS_INTERVAL = 100 + +/** Simulated duration of assembling the chunks of a chunked upload in milliseconds */ +const ASSEMBLE_DURATION = 1500 + +/** Simulated duration of all other requests in milliseconds */ +const REQUEST_DURATION = 250 + +/** + * Fake the WebDAV endpoints used for uploading files. + * + * There is no Nextcloud server behind the styleguide, so all requests of the uploader + * (`@nextcloud/files/upload`, used by `NcUploadPicker`) are answered locally + * by simulating a slow but successful upload. + */ +export function mockWebDav() { + axios.interceptors.request.use((config) => { + if (DAV_ENDPOINT.test(config.url ?? '')) { + config.adapter = davAdapter + } + return config + }) +} + +/** + * Custom Axios adapter answering WebDAV requests without any network access. + * + * @param {object} config - The Axios request config + * @return {Promise} The simulated Axios response + */ +async function davAdapter(config) { + switch (config.method?.toUpperCase()) { + case 'HEAD': + // The upload destination exists, any other directory (e.g. an uploaded one) has to be created first + await wait(REQUEST_DURATION, config.signal) + return respond(config, config.url.endsWith('/') ? 200 : 404) + case 'PUT': + // Uploading a file or one chunk of it + await simulateUpload(config) + return respond(config, 201) + case 'MOVE': + // Assembling the chunks of a chunked upload + await wait(ASSEMBLE_DURATION, config.signal) + return respond(config, 204) + default: + // MKCOL to create a directory or the temporary workspace of a chunked upload + await wait(REQUEST_DURATION, config.signal) + return respond(config, 201) + } +} + +/** + * Simulate transferring the data of a request, including the upload progress events. + * + * @param {object} config - The Axios request config + */ +async function simulateUpload(config) { + const total = config.data?.size ?? 0 + const duration = Math.max(MIN_UPLOAD_DURATION, (total / UPLOAD_SPEED) * 1000) + const steps = Math.ceil(duration / PROGRESS_INTERVAL) + + let loaded = 0 + for (let step = 1; step <= steps; step++) { + await wait(PROGRESS_INTERVAL, config.signal) + const bytes = Math.round((total * step) / steps) - loaded + loaded += bytes + config.onUploadProgress?.({ bytes, loaded, total, lengthComputable: true }) + } +} + +/** + * Create the response of a simulated request. + * Just like Axios, responses with an error status are rejected. + * + * @param {object} config - The Axios request config + * @param {number} status - The HTTP status to respond with + * @return {object} The simulated Axios response + */ +function respond(config, status) { + const response = { + config, + data: '', + headers: {}, + request: { responseURL: config.url }, + status, + statusText: '', + } + + if (status >= 200 && status < 300) { + return response + } + + // Mimic an `AxiosError` so the failure is handled like the one of a real request + return Promise.reject(Object.assign(new Error(`Request failed with status code ${status}`), { + config, + isAxiosError: true, + request: response.request, + response, + status, + })) +} + +/** + * Wait for the given duration, but reject as soon as the request is aborted. + * + * @param {number} duration - Time to wait in milliseconds + * @param {AbortSignal} [signal] - Signal used to abort the request + * @return {Promise} + */ +function wait(duration, signal) { + if (signal?.aborted) { + return Promise.reject(signal.reason) + } + + const { promise, resolve, reject } = Promise.withResolvers() + const onAbort = () => reject(signal.reason) + + const timeout = setTimeout(resolve, duration) + signal?.addEventListener('abort', onAbort, { once: true }) + + return promise.finally(() => { + clearTimeout(timeout) + signal?.removeEventListener('abort', onAbort) + }) +} diff --git a/styleguide/window.js b/styleguide/window.js index f038bf97b5..c7181fabf1 100644 --- a/styleguide/window.js +++ b/styleguide/window.js @@ -6,6 +6,11 @@ // Global variables window._oc_webroot = '' +// Simulate a logged in user, this is what `getCurrentUser()` of `@nextcloud/auth` reads +document.head.setAttribute('data-user', 'admin') +document.head.setAttribute('data-user-displayname', 'Administrator') +window._oc_isadmin = true + window.OC = { debug: true, getCurrentUser() { From f3e809cba3080c239e4aa3023f34f53a4cbdd644 Mon Sep 17 00:00:00 2001 From: skjnldsv Date: Fri, 4 Sep 2026 08:48:08 +0200 Subject: [PATCH 13/14] test(NcUploadPicker): make the upload test helpers reusable Adds the shared pieces the upload specs kept repeating: - `UploadStatus` mirrors the upstream constant from `@nextcloud/files/upload`, which cannot be imported at runtime here because it touches `window` on import while the test bodies run in Node. The `satisfies` clause makes the type checker fail if the upstream values ever change, so the mirror cannot silently drift like the hand-written numbers it replaces. - `skipWithoutFilePicker()` centralises the WebKit skip and its reason. - `expectNoMore()` asserts that no *further* matching request arrives. A plain `received(...)` check only looks at what happened so far, so it can pass simply because a request the uploader is about to send has not been sent yet. - `createFile()` takes the MIME type, and `mockDav()` can `fail()` a request with a network error, which is needed to cover the retry behaviour. The WebDAV root pattern is made non-greedy so a path that repeats `/remote.php/dav` is not over-stripped. Assisted-by: ClaudeCode:claude-opus-5 Signed-off-by: skjnldsv --- .../NcUploadPicker/upload-helpers.ts | 68 +++++++++++++++++-- 1 file changed, 63 insertions(+), 5 deletions(-) diff --git a/tests/component/components/NcUploadPicker/upload-helpers.ts b/tests/component/components/NcUploadPicker/upload-helpers.ts index e1628467fb..31bf9e76a2 100644 --- a/tests/component/components/NcUploadPicker/upload-helpers.ts +++ b/tests/component/components/NcUploadPicker/upload-helpers.ts @@ -3,11 +3,39 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ +import type { UploadStatus as UpstreamUploadStatus } from '@nextcloud/files/upload' import type { Page } from '@playwright/test' -import { expect } from '@playwright/experimental-ct-vue' +import { expect, test } from '@playwright/experimental-ct-vue' -const DAV_ROOT = /^.*\/remote\.php\/dav/ +const DAV_ROOT = /^.*?\/remote\.php\/dav/ + +/** + * Mirror of `UploadStatus` from `@nextcloud/files/upload`. + * + * The upstream module cannot be imported at runtime here, as it is only usable in a browser + * (it accesses `window` on import), while the test bodies themselves run in Node. + * The `satisfies` clause makes the type checker fail if the upstream values ever change, + * so this mirror cannot silently drift. + */ +export const UploadStatus = { + INITIALIZED: 0, + SCHEDULED: 1, + UPLOADING: 2, + ASSEMBLING: 3, + FINISHED: 4, + CANCELLED: 5, + FAILED: 6, +} as const satisfies typeof UpstreamUploadStatus + +/** + * Skip the surrounding tests on browsers that cannot drive a file picker. + * + * Call this at the top of a `test.describe` that picks files. + */ +export function skipWithoutFilePicker(): void { + test.skip(({ browserName }) => browserName === 'webkit', 'WebKit does not support file pickers in Playwright yet') +} export interface FakeFile { name: string @@ -20,11 +48,12 @@ export interface FakeFile { * * @param name - Name of the file * @param sizeInMiB - Size of the file in MiB + * @param mimeType - MIME type of the file */ -export function createFile(name: string, sizeInMiB: number): FakeFile { +export function createFile(name: string, sizeInMiB: number, mimeType = 'text/plain'): FakeFile { return { name, - mimeType: 'text/plain', + mimeType, buffer: Buffer.alloc(sizeInMiB * 1024 * 1024), } } @@ -78,6 +107,13 @@ export interface DavMockOptions { * @default 200 for `HEAD` on the destination, 404 for any other `HEAD`, 204 for `MOVE` and 201 otherwise */ status?: (request: DavRequest) => number + + /** + * Abort matching requests with a network error instead of responding to them. + * + * @default false + */ + fail?: (request: DavRequest) => boolean } export interface DavMock { @@ -102,6 +138,18 @@ export interface DavMock { */ waitFor(method: string, options?: { count?: number, path?: RegExp, timeout?: number }): Promise + /** + * Assert that no further matching requests arrive within the given duration. + * + * Unlike `received()`, which only looks at the requests received so far, this waits, + * so the assertion does not pass just because a request the uploader is about to send + * has not been sent yet. + * + * @param method - The HTTP method + * @param options - Pattern the path has to match and how long to wait in milliseconds + */ + expectNoMore(method: string, options?: { path?: RegExp, timeout?: number }): Promise + /** * Stop holding back responses: * all requests that are currently hold back are answered, as are all following requests. @@ -150,7 +198,11 @@ export async function mockDav(page: Page, options: DavMockOptions = {}): Promise await promise try { - await route.fulfill({ status: options.status?.(request) ?? defaultStatus(request) }) + if (options.fail?.(request)) { + await route.abort('connectionreset') + } else { + await route.fulfill({ status: options.status?.(request) ?? defaultStatus(request) }) + } } catch { // The request was aborted, e.g. because the upload was cancelled, or the page is already closed } @@ -171,6 +223,12 @@ export async function mockDav(page: Page, options: DavMockOptions = {}): Promise return received(method, path) }, + async expectNoMore(method, { path, timeout = 2000 } = {}) { + const count = received(method, path).length + await page.waitForTimeout(timeout) + expect(received(method, path), `Expected no further ${method} request`).toHaveLength(count) + }, + releaseAll() { holdResponses = false requests.forEach((request) => request.respond()) From a07312f14ac8beff625607442c715b101411605e Mon Sep 17 00:00:00 2001 From: skjnldsv Date: Fri, 4 Sep 2026 08:48:19 +0200 Subject: [PATCH 14/14] test(NcUploadPicker): cover upload retries and harden request assertions Adds the retry scenarios that were covered by the Cypress suite in `@nextcloud/upload` but had no equivalent after the migration. The `retryCondition` in `@nextcloud/files` (`lib/upload/utils/upload.ts`) has no test of its own, so all three branches were untested: - a network error is retried, - a locked file (423) is retried, because it is usually a preview being generated, - insufficient storage (507) is permanent and must not be retried. Also asserts that retrying an upload keeps the progress of the uploads that already finished instead of starting over. The retry is held back so the progress is observed while it is actually running, rather than racing the progress bar being hidden once the queue is done. Finally, the negative request assertions now use `expectNoMore()` and only run after a request that proves the uploader got far enough, so they cannot pass just because nothing has happened yet. Byte level progress within a single file is deliberately not covered: it is driven by upload progress events that a mocked route never emits, so it would need real network throttling to observe. Assisted-by: ClaudeCode:claude-opus-5 Signed-off-by: skjnldsv --- .../NcUploadPicker/NcUploadPicker.spec.ts | 15 ++-- .../components/NcUploadPicker/cancel.spec.ts | 12 ++- .../NcUploadPicker/conflicts.spec.ts | 11 ++- .../NcUploadPicker/progress.spec.ts | 39 +++++++++- .../components/NcUploadPicker/retry.spec.ts | 78 +++++++++++++++++++ 5 files changed, 132 insertions(+), 23 deletions(-) create mode 100644 tests/component/components/NcUploadPicker/retry.spec.ts diff --git a/tests/component/components/NcUploadPicker/NcUploadPicker.spec.ts b/tests/component/components/NcUploadPicker/NcUploadPicker.spec.ts index 96288b590a..71d665be47 100644 --- a/tests/component/components/NcUploadPicker/NcUploadPicker.spec.ts +++ b/tests/component/components/NcUploadPicker/NcUploadPicker.spec.ts @@ -6,12 +6,7 @@ import { expect, test } from '@playwright/experimental-ct-vue' import { join } from 'node:path' import NcUploadPickerStory from './NcUploadPicker.story.vue' -import { createFile, mockDav, pickFiles } from './upload-helpers.ts' - -/** The upload of a file finished successfully */ -const UPLOAD_FINISHED = 4 -/** The upload of a file failed */ -const UPLOAD_FAILED = 6 +import { createFile, mockDav, pickFiles, skipWithoutFilePicker, UploadStatus } from './upload-helpers.ts' test.describe('NcUploadPicker: rendering', () => { test('shows a button with the default label', async ({ mount, page }) => { @@ -49,7 +44,7 @@ test.describe('NcUploadPicker: rendering', () => { }) test.describe('NcUploadPicker: uploading', () => { - test.skip(({ browserName }) => browserName === 'webkit', 'WebKit does not support file pickers in Playwright yet') + skipWithoutFilePicker() test('shows the progress while uploading and hides it afterwards', async ({ mount, page }) => { const dav = await mockDav(page, { hold: ({ method }) => method === 'PUT' }) @@ -85,7 +80,7 @@ test.describe('NcUploadPicker: uploading', () => { // the file upload and the upload of the destination folder itself are both reported await expect.poll(() => finished.length).toBe(2) expect(finished.find(({ source }) => source.endsWith('/file.txt'))) - .toMatchObject({ status: UPLOAD_FINISHED }) + .toMatchObject({ status: UploadStatus.FINISHED }) }) test('uploads multiple files', async ({ mount, page }) => { @@ -133,7 +128,7 @@ test.describe('NcUploadPicker: uploading', () => { await expect.poll(() => finished.length).toBe(2) expect(finished.find(({ source }) => source.endsWith('/file.txt'))) - .toMatchObject({ status: UPLOAD_FAILED }) + .toMatchObject({ status: UploadStatus.FAILED }) await expect(page.getByRole('progressbar')).toBeHidden() }) @@ -174,7 +169,7 @@ test.describe('NcUploadPicker: uploading', () => { }) test.describe('NcUploadPicker: destination handling', () => { - test.skip(({ browserName }) => browserName === 'webkit', 'WebKit does not support file pickers in Playwright yet') + skipWithoutFilePicker() test('uploads to the destination of the current props', async ({ mount, page }) => { const dav = await mockDav(page) diff --git a/tests/component/components/NcUploadPicker/cancel.spec.ts b/tests/component/components/NcUploadPicker/cancel.spec.ts index 17211ff999..4cc7887811 100644 --- a/tests/component/components/NcUploadPicker/cancel.spec.ts +++ b/tests/component/components/NcUploadPicker/cancel.spec.ts @@ -5,13 +5,10 @@ import { expect, test } from '@playwright/experimental-ct-vue' import NcUploadPickerStory from './NcUploadPicker.story.vue' -import { createFile, mockDav, pickFiles } from './upload-helpers.ts' - -/** The upload of a file was cancelled */ -const UPLOAD_CANCELLED = 5 +import { createFile, mockDav, pickFiles, skipWithoutFilePicker, UploadStatus } from './upload-helpers.ts' test.describe('NcUploadPicker: cancelling uploads', () => { - test.skip(({ browserName }) => browserName === 'webkit', 'WebKit does not support file pickers in Playwright yet') + skipWithoutFilePicker() test('cancels a running upload', async ({ mount, page }) => { const finished: { source: string, status: number }[] = [] @@ -28,7 +25,7 @@ test.describe('NcUploadPicker: cancelling uploads', () => { await expect(page.getByRole('progressbar')).toBeHidden() await expect(page.getByRole('button', { name: 'Cancel uploads' })).toBeHidden() await expect.poll(() => finished.find(({ source }) => source.endsWith('/file.txt'))) - .toMatchObject({ status: UPLOAD_CANCELLED }) + .toMatchObject({ status: UploadStatus.CANCELLED }) }) test('cancels a chunked upload before the chunks are assembled', async ({ mount, page }) => { @@ -43,7 +40,7 @@ test.describe('NcUploadPicker: cancelling uploads', () => { await expect(page.getByRole('progressbar')).toBeHidden() // The chunks are never assembled into the destination file - expect(dav.received('MOVE')).toHaveLength(0) + await dav.expectNoMore('MOVE') }) test('cancels uploads that are queued while paused', async ({ mount, page }) => { @@ -58,6 +55,7 @@ test.describe('NcUploadPicker: cancelling uploads', () => { // The queue is emptied, so the uploader is neither uploading nor paused anymore await expect(page.getByRole('progressbar')).toBeHidden() await expect(page.getByText('paused')).toBeHidden() + await dav.expectNoMore('PUT') expect(dav.requests).toHaveLength(0) }) diff --git a/tests/component/components/NcUploadPicker/conflicts.spec.ts b/tests/component/components/NcUploadPicker/conflicts.spec.ts index ee35e136dc..ee7e5f8b88 100644 --- a/tests/component/components/NcUploadPicker/conflicts.spec.ts +++ b/tests/component/components/NcUploadPicker/conflicts.spec.ts @@ -7,10 +7,10 @@ import type { Locator, Page } from '@playwright/test' import { expect, test } from '@playwright/experimental-ct-vue' import NcUploadPickerStory from './NcUploadPicker.story.vue' -import { createFile, mockDav, pickFiles } from './upload-helpers.ts' +import { createFile, mockDav, pickFiles, skipWithoutFilePicker } from './upload-helpers.ts' test.describe('NcUploadPicker: conflicting files', () => { - test.skip(({ browserName }) => browserName === 'webkit', 'WebKit does not support file pickers in Playwright yet') + skipWithoutFilePicker() test('asks which version of a conflicting file to keep', async ({ mount, page }) => { const dav = await mockDav(page) @@ -44,11 +44,14 @@ test.describe('NcUploadPicker: conflicting files', () => { await pickFiles(page, createFile('file.txt', 1)) + // The destination was checked, so the uploader got as far as resolving conflicts + await dav.waitFor('HEAD') + // Conflicts cannot be resolved without the content of the destination, // so nothing is uploaded instead of silently overwriting existing files await expect(page.getByRole('dialog')).toBeHidden() await expect(page.getByRole('progressbar')).toBeHidden() - expect(dav.received('PUT')).toHaveLength(0) + await dav.expectNoMore('PUT') }) test('failing to check for conflicts does not block later uploads', async ({ mount, page }) => { @@ -59,7 +62,7 @@ test.describe('NcUploadPicker: conflicting files', () => { await pickFiles(page, createFile('file.txt', 1)) // The destination was checked, but nothing was uploaded await dav.waitFor('HEAD') - expect(dav.received('PUT')).toHaveLength(0) + await dav.expectNoMore('PUT') // The cancelled upload does not keep the uploader busy await component.update({ props: { ...props, failContent: false } }) diff --git a/tests/component/components/NcUploadPicker/progress.spec.ts b/tests/component/components/NcUploadPicker/progress.spec.ts index 97f17938c7..dd736eda71 100644 --- a/tests/component/components/NcUploadPicker/progress.spec.ts +++ b/tests/component/components/NcUploadPicker/progress.spec.ts @@ -5,10 +5,10 @@ import { expect, test } from '@playwright/experimental-ct-vue' import NcUploadPickerStory from './NcUploadPicker.story.vue' -import { createFile, mockDav, pickFiles } from './upload-helpers.ts' +import { createFile, mockDav, pickFiles, skipWithoutFilePicker } from './upload-helpers.ts' test.describe('NcUploadPicker: progress', () => { - test.skip(({ browserName }) => browserName === 'webkit', 'WebKit does not support file pickers in Playwright yet') + skipWithoutFilePicker() test('shows that the remaining time is unknown when the upload starts', async ({ mount, page }) => { const dav = await mockDav(page, { hold: ({ method }) => method === 'PUT' }) @@ -170,6 +170,7 @@ test.describe('NcUploadPicker: progress', () => { await expect(progress).toBeVisible() // The remaining chunk is not uploaded and the chunks are not assembled expect(dav.received('PUT')).toHaveLength(1) + await dav.expectNoMore('PUT') expect(dav.received('MOVE')).toHaveLength(0) await page.getByRole('button', { name: 'Resume uploads' }).click() @@ -178,4 +179,38 @@ test.describe('NcUploadPicker: progress', () => { await dav.waitFor('MOVE') await expect(progress).toBeHidden() }) + + test('does not lose the progress of finished uploads when an upload is retried', async ({ mount, page }) => { + let failUpload = true + const dav = await mockDav(page, { + // Hold back the second file, so its retry can be observed while it is still running + hold: ({ method, path }) => method === 'PUT' && path.endsWith('/second.txt'), + status: ({ method, path }) => { + if (method === 'PUT' && path.endsWith('/second.txt') && failUpload) { + failUpload = false + return 423 + } + return method === 'MOVE' ? 204 : 201 + }, + }) + // Upload one file after the other to have a deterministic progress + await mount(NcUploadPickerStory, { props: { multiple: true, maxParallelUploads: 1 } }) + + await pickFiles(page, createFile('first.txt', 1), createFile('second.txt', 1)) + + // The first of the two equally sized files is done + const progress = page.getByRole('progressbar') + await expect(progress).toHaveAttribute('value', '50') + + // Let the second upload fail, so it gets retried + const [attempt] = await dav.waitFor('PUT', { path: /\/second\.txt$/ }) + attempt.respond() + + // While the retry is running the progress of the finished file is kept instead of starting over + const attempts = await dav.waitFor('PUT', { count: 2, path: /\/second\.txt$/, timeout: 30000 }) + await expect(progress).toHaveAttribute('value', '50') + + attempts[1].respond() + await expect(progress).toBeHidden() + }) }) diff --git a/tests/component/components/NcUploadPicker/retry.spec.ts b/tests/component/components/NcUploadPicker/retry.spec.ts new file mode 100644 index 0000000000..f5829e0bc9 --- /dev/null +++ b/tests/component/components/NcUploadPicker/retry.spec.ts @@ -0,0 +1,78 @@ +/*! + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { expect, test } from '@playwright/experimental-ct-vue' +import NcUploadPickerStory from './NcUploadPicker.story.vue' +import { createFile, mockDav, pickFiles, skipWithoutFilePicker, UploadStatus } from './upload-helpers.ts' + +const FILE = /\/file\.txt$/ + +test.describe('NcUploadPicker: retrying failed requests', () => { + skipWithoutFilePicker() + + test('retries an upload that failed with a network error', async ({ mount, page }) => { + let failRequest = true + const dav = await mockDav(page, { + fail: (request) => { + if (request.method === 'PUT' && failRequest) { + failRequest = false + return true + } + return false + }, + }) + await mount(NcUploadPickerStory) + + await pickFiles(page, createFile('file.txt', 1)) + + // The upload is retried and succeeds the second time + const uploads = await dav.waitFor('PUT', { count: 2, path: FILE, timeout: 30000 }) + expect(uploads).toHaveLength(2) + await expect(page.getByRole('progressbar')).toBeHidden() + }) + + test('retries an upload of a locked file', async ({ mount, page }) => { + // A locked file is often just a preview being generated, so the upload is retried + let failRequest = true + const dav = await mockDav(page, { + status: (request) => { + if (request.method === 'PUT' && failRequest) { + failRequest = false + return 423 + } + return request.method === 'MOVE' ? 204 : 201 + }, + }) + await mount(NcUploadPickerStory) + + await pickFiles(page, createFile('file.txt', 1)) + + const uploads = await dav.waitFor('PUT', { count: 2, path: FILE, timeout: 30000 }) + expect(uploads).toHaveLength(2) + await expect(page.getByRole('progressbar')).toBeHidden() + }) + + test('does not retry an upload that exceeded the quota', async ({ mount, page }) => { + // Insufficient storage is permanent, so retrying would only delay the failure + const dav = await mockDav(page, { + status: (request) => request.method === 'PUT' ? 507 : 201, + }) + const finished: { source: string, status: number }[] = [] + await mount(NcUploadPickerStory, { + on: { 'upload:finished': (upload) => finished.push(upload) }, + }) + + await pickFiles(page, createFile('file.txt', 1)) + + // The upload is reported as failed + await expect + .poll(() => finished.find(({ source }) => source.endsWith('/file.txt'))) + .toMatchObject({ status: UploadStatus.FAILED }) + // and it is not attempted again + expect(dav.received('PUT', FILE)).toHaveLength(1) + await dav.expectNoMore('PUT', { path: FILE }) + await expect(page.getByRole('progressbar')).toBeHidden() + }) +})