From d5f663ea9e7c953e60b4a1faf0b45061ab4bb57d Mon Sep 17 00:00:00 2001 From: Pascal Wengerter Date: Mon, 4 Apr 2022 11:11:53 +0200 Subject: [PATCH 01/23] WIP --- .../unreleased/enhancement-resumeable-uploads | 9 + packages/web-app-files/package.json | 8 +- .../src/components/AppBar/CreateAndUpload.vue | 184 ++++++-- .../src/components/AppBar/Upload/FileDrop.vue | 132 ------ .../components/AppBar/Upload/FileUpload.vue | 25 +- .../components/AppBar/Upload/FolderUpload.vue | 22 +- .../src/components/Upload/ProgressBar.vue | 7 +- packages/web-app-files/src/mixins.js | 403 ------------------ packages/web-app-files/src/store/actions.js | 14 - packages/web-app-files/src/store/getters.js | 4 - packages/web-app-files/src/store/mutations.js | 48 --- packages/web-app-files/src/store/state.js | 2 - .../web-app-files/src/views/FilesDrop.vue | 156 +++++-- .../components/Upload/ProgressBar.spec.js | 3 +- .../tests/unit/views/views.setup.js | 1 - packages/web-runtime/package.json | 2 - packages/web-runtime/src/defaults/vue.js | 5 - packages/web-runtime/src/plugins/upload.js | 65 --- yarn.lock | 183 ++++++-- 19 files changed, 441 insertions(+), 832 deletions(-) create mode 100644 changelog/unreleased/enhancement-resumeable-uploads delete mode 100644 packages/web-app-files/src/components/AppBar/Upload/FileDrop.vue delete mode 100644 packages/web-runtime/src/plugins/upload.js diff --git a/changelog/unreleased/enhancement-resumeable-uploads b/changelog/unreleased/enhancement-resumeable-uploads new file mode 100644 index 00000000000..a5957628026 --- /dev/null +++ b/changelog/unreleased/enhancement-resumeable-uploads @@ -0,0 +1,9 @@ +Enhancement: Resumeable uploads + +Draft: +- Introduced resumeable (depending on backend capabilities) +- Improved rendering of uploadProgress-visualization +- Removed `vue2-dropzone` and `vue-drag-drop` libraries. + +https://github.com/owncloud/web/pull/6202 +https://github.com/owncloud/web/issues/6268 diff --git a/packages/web-app-files/package.json b/packages/web-app-files/package.json index efc2da258ab..6ebc709d445 100644 --- a/packages/web-app-files/package.json +++ b/packages/web-app-files/package.json @@ -4,7 +4,11 @@ "description": "ownCloud web files", "license": "AGPL-3.0", "dependencies": { - "copy-to-clipboard": "^3.3.1", - "vue2-dropzone": "^3.6.0" + "@uppy/core": "^2.1.7", + "@uppy/drop-target": "^1.1.1", + "@uppy/status-bar": "^2.1.3", + "@uppy/tus": "^2.2.2", + "@uppy/xhr-upload": "^2.0.7", + "copy-to-clipboard": "^3.3.1" } } diff --git a/packages/web-app-files/src/components/AppBar/CreateAndUpload.vue b/packages/web-app-files/src/components/AppBar/CreateAndUpload.vue index 704034a1865..a6d6a5c837f 100644 --- a/packages/web-app-files/src/components/AppBar/CreateAndUpload.vue +++ b/packages/web-app-files/src/components/AppBar/CreateAndUpload.vue @@ -70,16 +70,11 @@ New folder - - + + + - - diff --git a/packages/web-app-files/src/components/AppBar/Upload/FileUpload.vue b/packages/web-app-files/src/components/AppBar/Upload/FileUpload.vue index 3303719fdf9..a9e8eb7796d 100644 --- a/packages/web-app-files/src/components/AppBar/Upload/FileUpload.vue +++ b/packages/web-app-files/src/components/AppBar/Upload/FileUpload.vue @@ -7,41 +7,18 @@ + + diff --git a/packages/web-runtime/src/layouts/Application.vue b/packages/web-runtime/src/layouts/Application.vue index 5880ff5cbcf..548844f9ad3 100644 --- a/packages/web-runtime/src/layouts/Application.vue +++ b/packages/web-runtime/src/layouts/Application.vue @@ -18,6 +18,7 @@ /> + @@ -26,6 +27,7 @@ import { mapActions, mapGetters } from 'vuex' import TopBar from '../components/Topbar/TopBar.vue' import MessageBar from '../components/MessageBar.vue' import SidebarNav from '../components/SidebarNav/SidebarNav.vue' +import UploadInfo from '../components/UploadInfo.vue' import { useActiveApp, useRoute } from 'web-pkg/src/composables' import { watch, defineComponent } from '@vue/composition-api' @@ -33,7 +35,8 @@ export default defineComponent({ components: { MessageBar, TopBar, - SidebarNav + SidebarNav, + UploadInfo }, setup() { // FIXME: we can convert to a single router-view without name (thus without the loop) and without this watcher when we release v6.0.0 From 2475b9394097a6696c8582bc4922efef82be4f1c Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Wed, 13 Apr 2022 15:29:33 +0200 Subject: [PATCH 03/23] Refactor uppy code --- .../src/components/AppBar/CreateAndUpload.vue | 321 +++--------------- .../components/AppBar/Upload/FileUpload.vue | 6 + .../components/AppBar/Upload/FolderUpload.vue | 6 + .../src/composables/upload/index.ts | 1 + .../composables/upload/useUploadHelpers.ts | 169 +++++++++ .../web-app-files/src/helpers/user/types.ts | 1 + .../web-app-files/src/views/FilesDrop.vue | 203 ++++------- .../composables/capability/useCapability.ts | 8 + packages/web-pkg/src/services/index.ts | 1 - packages/web-pkg/src/services/uppy.ts | 102 ------ packages/web-pkg/src/uppy/customTus.js | 15 - .../web-runtime/src/components/UploadInfo.vue | 28 +- .../src/composables/upload/index.ts | 1 + .../upload/uppyPlugins/customDropTarget.ts} | 14 +- .../upload/uppyPlugins/customTus.ts | 14 + .../src/composables/upload/useUpload.ts | 132 +++++++ .../web-runtime/src/container/bootstrap.ts | 10 + packages/web-runtime/src/index.ts | 4 +- .../web-runtime/src/services/uppyService.ts | 225 ++++++++++++ 19 files changed, 716 insertions(+), 545 deletions(-) create mode 100644 packages/web-app-files/src/composables/upload/index.ts create mode 100644 packages/web-app-files/src/composables/upload/useUploadHelpers.ts delete mode 100644 packages/web-pkg/src/services/uppy.ts delete mode 100644 packages/web-pkg/src/uppy/customTus.js create mode 100644 packages/web-runtime/src/composables/upload/index.ts rename packages/{web-pkg/src/uppy/customDropTarget.js => web-runtime/src/composables/upload/uppyPlugins/customDropTarget.ts} (53%) create mode 100644 packages/web-runtime/src/composables/upload/uppyPlugins/customTus.ts create mode 100644 packages/web-runtime/src/composables/upload/useUpload.ts create mode 100644 packages/web-runtime/src/services/uppyService.ts diff --git a/packages/web-app-files/src/components/AppBar/CreateAndUpload.vue b/packages/web-app-files/src/components/AppBar/CreateAndUpload.vue index a5a15520c99..9c61a69dceb 100644 --- a/packages/web-app-files/src/components/AppBar/CreateAndUpload.vue +++ b/packages/web-app-files/src/components/AppBar/CreateAndUpload.vue @@ -100,7 +100,7 @@ - diff --git a/packages/web-pkg/src/composables/capability/useCapability.ts b/packages/web-pkg/src/composables/capability/useCapability.ts index fcb3380c0d2..56cd451b8dd 100644 --- a/packages/web-pkg/src/composables/capability/useCapability.ts +++ b/packages/web-pkg/src/composables/capability/useCapability.ts @@ -30,3 +30,11 @@ export const useCapabilityFilesSharingResharing = createCapabilityComposable( ) export const useCapabilitySpacesEnabled = createCapabilityComposable('spaces.enabled', false) +export const useCapabilityFilesTusSupportHttpMethodOverride = createCapabilityComposable( + 'files.tus_support.http_method_override', + false +) +export const useCapabilityFilesTusSupportMaxChunkSize = createCapabilityComposable( + 'files.tus_support.max_chunk_size', + 0 +) diff --git a/packages/web-pkg/src/services/index.ts b/packages/web-pkg/src/services/index.ts index 784b3ad68eb..83dae7638cc 100644 --- a/packages/web-pkg/src/services/index.ts +++ b/packages/web-pkg/src/services/index.ts @@ -1,2 +1 @@ export * from './client' -export * from './uppy' diff --git a/packages/web-pkg/src/services/uppy.ts b/packages/web-pkg/src/services/uppy.ts deleted file mode 100644 index 70cb802f286..00000000000 --- a/packages/web-pkg/src/services/uppy.ts +++ /dev/null @@ -1,102 +0,0 @@ -import XHRUpload from '@uppy/xhr-upload' -import Uppy from '@uppy/core' -import StatusBar from '@uppy/status-bar' - -// @TODO Initialize uppy globally with capabilities and configuration -export class UppyService { - public getUppyInstance({ - uploadPath, - configuration, - capabilities, - headers, - $gettext, - customTus // @FIXME Remove here, import the plugin instead... is not working for some reason - }): Uppy { - const uppy = new Uppy({ - debug: true, - autoProceed: true - }) - - const tusSupport = UppyService.tusSupport(capabilities) - - // TODO: What about flaky capability loading and its implications? - if (tusSupport) { - const chunkSize = - tusSupport && configuration.uploadChunkSize !== Infinity - ? Math.max(capabilities.files.tus_support.max_chunk_size, configuration.uploadChunkSize) - : configuration.uploadChunkSize - - uppy.use(customTus, { - endpoint: uploadPath, - headers, - chunkSize: chunkSize, - removeFingerprintOnSuccess: true, - overridePatchMethod: !!capabilities.files.tus_support.http_method_override, - retryDelays: [0, 3000, 5000, 10000, 20000] - }) - } else { - uppy.use(XHRUpload, { - endpoint: uploadPath, - method: 'put', - headers, - getResponseData() { - return {} - } - }) - } - - uppy.use(StatusBar, { - id: 'StatusBar', - target: '.upload-info-status-bar', - hideAfterFinish: true, - showProgressDetails: true, - hideUploadButton: false, - hideRetryButton: false, - hidePauseResumeButton: false, - hideCancelButton: false, - doneButtonHandler: null, - locale: { - strings: { - uploading: $gettext('Uploading'), - complete: $gettext('Complete'), - uploadFailed: $gettext('Upload failed'), - paused: $gettext('Paused'), - retry: $gettext('Retry'), - cancel: $gettext('Cancel'), - pause: $gettext('Pause'), - resume: $gettext('Resume'), - done: $gettext('Done'), - filesUploadedOfTotal: { - 0: $gettext('%{complete} of %{smart_count} file uploaded'), - 1: $gettext('%{complete} of %{smart_count} files uploaded') - }, - dataUploadedOfTotal: $gettext('%{complete} of %{total}'), - xTimeLeft: $gettext('%{time} left'), - uploadXFiles: { - 0: $gettext('Upload %{smart_count} file'), - 1: $gettext('Upload %{smart_count} files') - }, - uploadXNewFiles: { - 0: $gettext('Upload +%{smart_count} file'), - 1: $gettext('Upload +%{smart_count} files') - }, - upload: $gettext('Upload'), - retryUpload: $gettext('Retry upload'), - xMoreFilesAdded: { - 0: $gettext('%{smart_count} more file added'), - 1: $gettext('%{smart_count} more files added') - }, - showErrorDetails: $gettext('Show error details') - } - } - }) - - return uppy - } - - private static tusSupport(capabilities) { - return capabilities.files?.tus_support?.max_chunk_size > 0 - } -} - -export const uppyService = new UppyService() diff --git a/packages/web-pkg/src/uppy/customTus.js b/packages/web-pkg/src/uppy/customTus.js deleted file mode 100644 index 7c34a208dbb..00000000000 --- a/packages/web-pkg/src/uppy/customTus.js +++ /dev/null @@ -1,15 +0,0 @@ -const Tus = require('@uppy/tus') - -/** - * Custom Tus plugin - * - * It can set the upload endpoint dynamically per file. - */ -module.exports = class CustomTus extends Tus { - upload(file) { - if (file.meta.tusEndpoint) { - this.opts.endpoint = file.meta.tusEndpoint - } - return super.upload(file) - } -} diff --git a/packages/web-runtime/src/components/UploadInfo.vue b/packages/web-runtime/src/components/UploadInfo.vue index f5c5dc6bedb..70b8c8ff550 100644 --- a/packages/web-runtime/src/components/UploadInfo.vue +++ b/packages/web-runtime/src/components/UploadInfo.vue @@ -91,20 +91,26 @@ export default { return this.$gettext('Upload completed') } }, + mounted() { + this.$uppyService.useStatusBar({ + targetSelector: '.upload-info-status-bar', + getText: this.$gettext + }) + }, created() { - this.$root.$on('fileUploadStarted', () => { + this.$uppyService.$on('uploadStarted', () => { this.showInfo = true this.filesUploading = this.filesUploading + 1 this.uploadCancelled = false }) - this.$root.$on('fileUploadCompleted', () => { + this.$uppyService.$on('uploadCompleted', () => { this.filesUploading = this.filesUploading - 1 }) - this.$root.$on('fileUploadsCancelled', () => { + this.$uppyService.$on('uploadCancelled', () => { this.filesUploading = 0 this.uploadCancelled = true }) - this.$root.$on('fileUploadedSuccessfully', (file, route) => { + this.$uppyService.$on('fileUploadedSuccessfully', (file, route) => { this.successfulUploads.push({ ...file, targetRoute: route }) }) }, @@ -136,15 +142,21 @@ export default { return {} } - return { + const strippedPath = path.replace(/^\//, '') + const route = { name: targetRoute.name, query: targetRoute.query, params: { - item: path.replace(/^\//, ''), - ...targetRoute.params, - ...(storageId && { storageId }) + ...(storageId && path && { storageId }) } } + + if (strippedPath) { + route.params = { ...targetRoute.params } + route.params.item = strippedPath + } + + return route } } } diff --git a/packages/web-runtime/src/composables/upload/index.ts b/packages/web-runtime/src/composables/upload/index.ts new file mode 100644 index 00000000000..7e3692274ba --- /dev/null +++ b/packages/web-runtime/src/composables/upload/index.ts @@ -0,0 +1 @@ +export * from './useUpload' diff --git a/packages/web-pkg/src/uppy/customDropTarget.js b/packages/web-runtime/src/composables/upload/uppyPlugins/customDropTarget.ts similarity index 53% rename from packages/web-pkg/src/uppy/customDropTarget.js rename to packages/web-runtime/src/composables/upload/uppyPlugins/customDropTarget.ts index e065e50c8d5..79591547abe 100644 --- a/packages/web-pkg/src/uppy/customDropTarget.js +++ b/packages/web-runtime/src/composables/upload/uppyPlugins/customDropTarget.ts @@ -1,14 +1,18 @@ -const DropTarget = require('@uppy/drop-target') +import DropTarget from '@uppy/drop-target' /** * Custom Drop Target plugin * * It adds the possibility to have a custom method for adding files to uppy. */ -module.exports = class CustomDropTarget extends DropTarget { +export class CustomDropTarget extends DropTarget { addFiles = (files) => { - if (this.opts.addFilesToUppy) { - this.opts.addFilesToUppy(files) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + if (this.opts.uppyService) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + this.opts.uppyService.$emit('filesSelected', files) return } @@ -25,6 +29,8 @@ module.exports = class CustomDropTarget extends DropTarget { }) try { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore this.uppy.addFiles(descriptors) } catch (err) { this.uppy.log(err) diff --git a/packages/web-runtime/src/composables/upload/uppyPlugins/customTus.ts b/packages/web-runtime/src/composables/upload/uppyPlugins/customTus.ts new file mode 100644 index 00000000000..049a48affaf --- /dev/null +++ b/packages/web-runtime/src/composables/upload/uppyPlugins/customTus.ts @@ -0,0 +1,14 @@ +import Tus from '@uppy/tus' + +export class CustomTus extends Tus { + upload(file) { + if (file.meta.tusEndpoint) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + this.opts.endpoint = file.meta.tusEndpoint + } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return super.upload(file) + } +} diff --git a/packages/web-runtime/src/composables/upload/useUpload.ts b/packages/web-runtime/src/composables/upload/useUpload.ts new file mode 100644 index 00000000000..798c35f042b --- /dev/null +++ b/packages/web-runtime/src/composables/upload/useUpload.ts @@ -0,0 +1,132 @@ +import { Route } from 'vue-router' +import { ClientService } from 'web-pkg/src/services' +import { + useCapabilityFilesTusSupportHttpMethodOverride, + useCapabilityFilesTusSupportMaxChunkSize, + useClientService, + useStore +} from 'web-pkg/src/composables' +import { computed, Ref, unref, watch } from '@vue/composition-api' +import { useActiveLocation } from 'files/src/composables' +import { isLocationPublicActive } from 'files/src/router' +import { UppyService } from '../../services/uppyService' + +// FIXME: properly extend type from AddFileOptions = Record> +export interface UppyResource { + id?: string + source: string + name: string + type: string + data: Blob + meta: { + currentFolder: string + relativeFolder: string + relativeFilePath: string + route: Route + tusEndpoint: string + webDavPath: string + } +} + +interface UploadOptions { + uppyService: UppyService +} + +interface UploadResult { + createDirectoryTree(files: UppyResource[]): void +} + +export function useUpload(options: UploadOptions): UploadResult { + const store = useStore() + const publicLinkPassword = computed((): string => store.getters['Files/publicLinkPassword']) + const isPublicLocation = useActiveLocation(isLocationPublicActive, 'files-public-files') + const clientService = useClientService() + const getToken = computed((): string => store.getters.getToken) + + const tusHttpMethodOverride = useCapabilityFilesTusSupportHttpMethodOverride() + const tusMaxChunkSize = useCapabilityFilesTusSupportMaxChunkSize() + const uploadChunkSize = computed((): number => store.getters.configuration.uploadChunkSize) + + const headers = computed((): { [key: string]: string } => { + if (unref(isPublicLocation)) { + const password = unref(publicLinkPassword) + + if (password) { + return { Authorization: 'Basic ' + Buffer.from('public:' + password).toString('base64') } + } + + return {} + } + return { + Authorization: 'Bearer ' + unref(getToken) + } + }) + + const uppyOptions = computed(() => { + const isTusSupported = unref(tusMaxChunkSize) > 0 + + if (isTusSupported) { + return { + isTusSupported, + tusMaxChunkSize: unref(tusMaxChunkSize), + uploadChunkSize: unref(uploadChunkSize), + tusHttpMethodOverride: unref(tusHttpMethodOverride), + headers: unref(headers) + } + } + + return { isTusSupported, headers: unref(headers) } + }) + + watch( + uppyOptions, + () => { + if (unref(uppyOptions).isTusSupported) { + options.uppyService.useTus(unref(uppyOptions) as any) + return + } + options.uppyService.useXhr(unref(uppyOptions).headers as any) + }, + { immediate: true } + ) + + return { + createDirectoryTree: createDirectoryTree({ + clientService, + isPublicLocation, + publicLinkPassword + }) + } +} + +const createDirectoryTree = ({ + clientService, + isPublicLocation, + publicLinkPassword +}: { + clientService: ClientService + isPublicLocation: Ref + publicLinkPassword?: Ref +}) => { + return async (files: UppyResource[]) => { + const { owncloudSdk: client } = clientService + const createdFolders = [] + for (const file of files) { + const currentFolder = file.meta.currentFolder + const directory = file.meta.relativeFolder + + // @TODO check common prefixes + if (!directory || createdFolders.includes(directory)) { + continue + } + + if (unref(isPublicLocation)) { + await client.publicFiles.createFolder(currentFolder, directory, unref(publicLinkPassword)) + } else { + await client.files.createFolder(file.meta.webDavPath) + } + + createdFolders.push(directory) + } + } +} diff --git a/packages/web-runtime/src/container/bootstrap.ts b/packages/web-runtime/src/container/bootstrap.ts index f1c3875f0a4..d5f8717b4ef 100644 --- a/packages/web-runtime/src/container/bootstrap.ts +++ b/packages/web-runtime/src/container/bootstrap.ts @@ -14,6 +14,7 @@ import { useLocalStorage } from 'web-pkg/src/composables' import { unref } from '@vue/composition-api' import { useDefaultThemeName } from '../composables' import { clientService } from 'web-pkg/src/services' +import { UppyService } from '../services/uppyService' /** * fetch runtime configuration, this step is optional, all later steps can use a static @@ -249,6 +250,15 @@ export const announceClientService = ({ vue.prototype.$clientService.owncloudSdk = sdk } +/** + * announce uppyService and owncloud SDK and inject it into vue + * + * @param vue + */ +export const announceUppyService = ({ vue }: { vue: VueConstructor }): void => { + vue.prototype.$uppyService = new UppyService() +} + /** * announce runtime defaults, this is usual the last needed announcement before rendering the actual ui * diff --git a/packages/web-runtime/src/index.ts b/packages/web-runtime/src/index.ts index 4eb8b8b6b7c..6706b51d4c1 100644 --- a/packages/web-runtime/src/index.ts +++ b/packages/web-runtime/src/index.ts @@ -19,12 +19,14 @@ import { announceTheme, announceTranslations, announceVersions, - applicationStore + applicationStore, + announceUppyService } from './container' export const bootstrap = async (configurationPath: string): Promise => { const runtimeConfiguration = await requestConfiguration(configurationPath) announceClientService({ vue: Vue, runtimeConfiguration }) + announceUppyService({ vue: Vue }) await announceClient(runtimeConfiguration) await announceStore({ vue: Vue, store, runtimeConfiguration }) await announceApplications({ diff --git a/packages/web-runtime/src/services/uppyService.ts b/packages/web-runtime/src/services/uppyService.ts new file mode 100644 index 00000000000..0ec9002032b --- /dev/null +++ b/packages/web-runtime/src/services/uppyService.ts @@ -0,0 +1,225 @@ +import Uppy from '@uppy/core' +import { CustomTus } from '../composables/upload/uppyPlugins/customTus' +import XHRUpload, { XHRUploadOptions } from '@uppy/xhr-upload' +import { CustomDropTarget } from '../composables/upload/uppyPlugins/customDropTarget' +import StatusBar from '@uppy/status-bar' +import { UppyResource } from '../composables/upload' +import Vue from 'vue' + +export class UppyService extends Vue { + uppy: Uppy + uploadInputs: HTMLInputElement[] = [] + + constructor() { + super() + this.uppy = new Uppy({ + debug: true, // @TODO + autoProceed: true + }) + this.setUpEvents() + } + + useTus({ + tusMaxChunkSize, + uploadChunkSize, + tusHttpMethodOverride, + headers + }: { + tusMaxChunkSize: number + uploadChunkSize: number + tusHttpMethodOverride: boolean + headers: { [key: string]: string } + }) { + const chunkSize = + tusMaxChunkSize > 0 && uploadChunkSize !== Infinity + ? Math.max(tusMaxChunkSize, uploadChunkSize) + : uploadChunkSize + + const tusPluginOptions = { + headers: headers, + chunkSize: chunkSize, + removeFingerprintOnSuccess: true, + overridePatchMethod: !!tusHttpMethodOverride, + retryDelays: [0, 3000, 5000, 10000, 20000] + } + + const tusPlugin = this.uppy.getPlugin('Tus') + if (tusPlugin) { + tusPlugin.setOptions(tusPluginOptions) + return + } + + this.uppy.use(CustomTus, tusPluginOptions) + } + + useXhr({ headers }: { headers: { [key: string]: string } }) { + const xhrPluginOptions: XHRUploadOptions = { + endpoint: '', + method: 'put', + headers, + getResponseData() { + return {} + } + } + + const tusPlugin = this.uppy.getPlugin('XHRUpload') + if (tusPlugin) { + tusPlugin.setOptions(xhrPluginOptions) + return + } + + this.uppy.use(XHRUpload, xhrPluginOptions) + } + + useDropTarget({ + targetSelector, + uppyService + }: { + targetSelector: string + uppyService: UppyService + }) { + if (this.uppy.getPlugin('DropTarget')) { + return + } + this.uppy.use(CustomDropTarget, { + target: targetSelector, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + uppyService + }) + } + + useStatusBar({ + targetSelector, + getText + }: { + targetSelector: string + getText: (msgid: string) => string + }) { + if (this.uppy.getPlugin('StatusBar')) { + return + } + + this.uppy.use(StatusBar, { + id: 'StatusBar', + target: targetSelector, + hideAfterFinish: true, + showProgressDetails: true, + hideUploadButton: false, + hideRetryButton: false, + hidePauseResumeButton: false, + hideCancelButton: false, + doneButtonHandler: null, + locale: { + strings: { + uploading: getText('Uploading'), + complete: getText('Complete'), + uploadFailed: getText('Upload failed'), + paused: getText('Paused'), + retry: getText('Retry'), + cancel: getText('Cancel'), + pause: getText('Pause'), + resume: getText('Resume'), + done: getText('Done'), + filesUploadedOfTotal: { + 0: getText('%{complete} of %{smart_count} file uploaded'), + 1: getText('%{complete} of %{smart_count} files uploaded') + }, + dataUploadedOfTotal: getText('%{complete} of %{total}'), + xTimeLeft: getText('%{time} left'), + uploadXFiles: { + 0: getText('Upload %{smart_count} file'), + 1: getText('Upload %{smart_count} files') + }, + uploadXNewFiles: { + 0: getText('Upload +%{smart_count} file'), + 1: getText('Upload +%{smart_count} files') + }, + upload: getText('Upload'), + retryUpload: getText('Retry upload'), + xMoreFilesAdded: { + 0: getText('%{smart_count} more file added'), + 1: getText('%{smart_count} more files added') + }, + showErrorDetails: getText('Show error details') + } + } + }) + } + + setUpEvents() { + this.uppy.on('upload', () => { + this.$emit('uploadStarted') + }) + this.uppy.on('cancel-all', () => { + this.$emit('uploadCancelled') + }) + this.uppy.on('complete', (result) => { + this.$emit('uploadCompleted') + result.successful.forEach((file) => { + this.$emit('uploadSuccess', file) + console.log('SUCCESS FOR: ', file.name) + this.uppy.removeFile(file.id) + }) + + this.uploadInputs.forEach((item) => { + item.value = null + }) + }) + this.uppy.on('file-removed', () => { + this.$emit('uploadRemoved') + this.uploadInputs.forEach((item) => { + item.value = null + }) + }) + this.uppy.on('file-added', (file) => { + this.$emit('fileAdded') + const addedFile = file as unknown as UppyResource + if (this.uppy.getPlugin('XHRUpload')) { + this.uppy.setFileState(addedFile.id, { + xhrUpload: { + endpoint: `${addedFile.meta.tusEndpoint.replace(/\/+$/, '')}/${addedFile.name}` + } + }) + } + }) + this.uppy.on('upload-error', () => { + this.$emit('uploadError') + }) + } + + registerUploadInput(el: HTMLInputElement) { + const listenerRegistered = el.getAttribute('listener') + if (listenerRegistered !== 'true') { + el.setAttribute('listener', 'true') + el.addEventListener('change', (event) => { + const target = event.target as HTMLInputElement + const files = Array.from(target.files) + this.$emit('filesSelected', files) + }) + this.uploadInputs.push(el) + } + } + + removeUploadInput(elementId: string) { + this.uploadInputs = this.uploadInputs.filter((el) => el.id !== elementId) + } + + uploadFiles(files: UppyResource[]) { + files.forEach((file) => { + try { + console.log('START UPLOAD FOR: ', file.name) + this.uppy.addFile(file) + } catch (err) { + console.error('error upload file:', file) + if (err.isRestriction) { + // handle restrictions + console.error('Restriction error:', err) + } else { + // handle other errors + console.error(err) + } + } + }) + } +} From b75b450b936e6d0d7f0d62c20deebb8efb7012d8 Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Wed, 13 Apr 2022 15:43:45 +0200 Subject: [PATCH 04/23] Update changelog item, move uppy dependencies to web-runtime package --- .../unreleased/enhancement-resumeable-uploads | 11 ++-- packages/web-app-files/package.json | 5 -- packages/web-runtime/package.json | 5 ++ yarn.lock | 50 +++++++++---------- 4 files changed, 37 insertions(+), 34 deletions(-) diff --git a/changelog/unreleased/enhancement-resumeable-uploads b/changelog/unreleased/enhancement-resumeable-uploads index a5957628026..df429090a42 100644 --- a/changelog/unreleased/enhancement-resumeable-uploads +++ b/changelog/unreleased/enhancement-resumeable-uploads @@ -1,9 +1,12 @@ -Enhancement: Resumeable uploads +Enhancement: Resumable uploads -Draft: -- Introduced resumeable (depending on backend capabilities) +We've implemented Uppy as a library for handling uploads. This concludes the following features and changes: + +- Resumable uploads when the backend supports the Tus-protocol +- A nice looking overview for all files that have been uploaded successfully or failed to upload +- Navigation across Web while uploads are in progress - Improved rendering of uploadProgress-visualization -- Removed `vue2-dropzone` and `vue-drag-drop` libraries. +- Removed `vue2-dropzone` and `vue-drag-drop` libraries https://github.com/owncloud/web/pull/6202 https://github.com/owncloud/web/issues/6268 diff --git a/packages/web-app-files/package.json b/packages/web-app-files/package.json index 6ebc709d445..c8acbc83df6 100644 --- a/packages/web-app-files/package.json +++ b/packages/web-app-files/package.json @@ -4,11 +4,6 @@ "description": "ownCloud web files", "license": "AGPL-3.0", "dependencies": { - "@uppy/core": "^2.1.7", - "@uppy/drop-target": "^1.1.1", - "@uppy/status-bar": "^2.1.3", - "@uppy/tus": "^2.2.2", - "@uppy/xhr-upload": "^2.0.7", "copy-to-clipboard": "^3.3.1" } } diff --git a/packages/web-runtime/package.json b/packages/web-runtime/package.json index 9073e873f6f..c6fc268ea13 100644 --- a/packages/web-runtime/package.json +++ b/packages/web-runtime/package.json @@ -6,6 +6,11 @@ "dependencies": { "@fortawesome/fontawesome-free": "^6.1.1", "@popperjs/core": "^2.4.0", + "@uppy/core": "^2.1.7", + "@uppy/drop-target": "^1.1.1", + "@uppy/status-bar": "^2.1.3", + "@uppy/tus": "^2.2.2", + "@uppy/xhr-upload": "^2.0.7", "@vue/composition-api": "^1.4.9", "axios": "^0.26.1", "cross-fetch": "^3.0.6", diff --git a/yarn.lock b/yarn.lock index 566fcdba470..39745305292 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2747,39 +2747,39 @@ __metadata: linkType: hard "@uppy/companion-client@npm:^2.0.4, @uppy/companion-client@npm:^2.0.5": - version: 2.0.5 - resolution: "@uppy/companion-client@npm:2.0.5" + version: 2.0.6 + resolution: "@uppy/companion-client@npm:2.0.6" dependencies: - "@uppy/utils": ^4.0.5 + "@uppy/utils": ^4.0.6 namespace-emitter: ^2.0.1 - checksum: a68bbd4d7bb65b9eb2901ad409e4704662029e7b5f5c4d350cbbfeaf6e4995a0ff66cedfdaa31b1c46806398f446a196f33bab2877222feed0b10627cb539158 + checksum: 2a60c70a280b4c7d898d5c0b4b58b0088dc4dc2937b7cf426c6a29a9a404a9db218fa6b909f892eb8a886106882943e2ccfc05b309fa760d90bef76039bc7b52 languageName: node linkType: hard "@uppy/core@npm:^2.1.7": - version: 2.1.7 - resolution: "@uppy/core@npm:2.1.7" + version: 2.1.8 + resolution: "@uppy/core@npm:2.1.8" dependencies: "@transloadit/prettier-bytes": 0.0.7 "@uppy/store-default": ^2.0.3 - "@uppy/utils": ^4.0.5 + "@uppy/utils": ^4.0.6 lodash.throttle: ^4.1.1 mime-match: ^1.0.2 namespace-emitter: ^2.0.1 nanoid: ^3.1.25 preact: ^10.5.13 - checksum: 161ecf4315fb3ff2db3ad7beceac5b94ca42e766d98658b62da9d10645c1fc9324f8c1949ff00637aef74adaac88a51e1f06cf47fbd47888679a0cc87871fa89 + checksum: 2c4e1febef1cd7ef841aa9634773fa7eccc9737b34412152964ff384a0ced32b85cadc3532de7de7c74ae7da8887d2e8accd7e5b9f1df40cdec72e65441f3aff languageName: node linkType: hard "@uppy/drop-target@npm:^1.1.1": - version: 1.1.1 - resolution: "@uppy/drop-target@npm:1.1.1" + version: 1.1.2 + resolution: "@uppy/drop-target@npm:1.1.2" dependencies: - "@uppy/utils": ^4.0.3 + "@uppy/utils": ^4.0.5 peerDependencies: - "@uppy/core": ^2.0.0 - checksum: 33825bbe77ac780c939886f0a9be91c6ce5a5b8f93b239b27ed428d78ccd57906e934ffcf2b9524fc0d2df1f6e31e353612fe26a56ed82dad4a1a527eca7f664 + "@uppy/core": ^2.1.6 + checksum: fc237303e0377182ec72288ade6fca9b3a6ee4344ff4930844efb460d3768b6273be3982c28726c8b134fc2966ea98d129e003ffaa96f1254d0f004d75726b2a languageName: node linkType: hard @@ -2818,12 +2818,12 @@ __metadata: languageName: node linkType: hard -"@uppy/utils@npm:^4.0.3, @uppy/utils@npm:^4.0.4, @uppy/utils@npm:^4.0.5": - version: 4.0.5 - resolution: "@uppy/utils@npm:4.0.5" +"@uppy/utils@npm:^4.0.4, @uppy/utils@npm:^4.0.5, @uppy/utils@npm:^4.0.6": + version: 4.0.6 + resolution: "@uppy/utils@npm:4.0.6" dependencies: lodash.throttle: ^4.1.1 - checksum: 55c2522ae1bad09b8bb706c90bdb754e3c74c48738f8cb88437e726d8fe7103380caea8daa7016ce7d4dc73e99862f60a585a052291393a2d76ef67b3d5b44d8 + checksum: 8773c4b1d742d65237c48c0575f1a933a20ffdcd8da462141b19649c027b0b88e4aa5dcb26ead1e830c967b904e51315fb61ea72ee0273614b463122d2b63066 languageName: node linkType: hard @@ -6424,11 +6424,6 @@ __metadata: version: 0.0.0-use.local resolution: "files@workspace:packages/web-app-files" dependencies: - "@uppy/core": ^2.1.7 - "@uppy/drop-target": ^1.1.1 - "@uppy/status-bar": ^2.1.3 - "@uppy/tus": ^2.2.2 - "@uppy/xhr-upload": ^2.0.7 copy-to-clipboard: ^3.3.1 languageName: unknown linkType: soft @@ -10861,9 +10856,9 @@ __metadata: linkType: hard "preact@npm:^10.5.13": - version: 10.6.4 - resolution: "preact@npm:10.6.4" - checksum: 09c496bb3cbe231fb6ef218b44d29af53a1f4bf1ae2e5929ef0faf70f9ecffaa475b4459ef72dd8f0f5396c255172c7c26de5c7d2eb6f6849e36e724c7aa86b7 + version: 10.7.1 + resolution: "preact@npm:10.7.1" + checksum: e99d08bd38372e78ce1c84e68685cad66adf076bd95069bc8bcd32c9903a4df09c01d5b04aad849527362b67cc2c09446b647f72de4e67d0578fdea0f904f28f languageName: node linkType: hard @@ -13928,6 +13923,11 @@ __metadata: "@popperjs/core": ^2.4.0 "@types/luxon": ^2.3.1 "@types/semver": ^7.3.8 + "@uppy/core": ^2.1.7 + "@uppy/drop-target": ^1.1.1 + "@uppy/status-bar": ^2.1.3 + "@uppy/tus": ^2.2.2 + "@uppy/xhr-upload": ^2.0.7 "@vue/composition-api": ^1.4.9 axios: ^0.26.1 cross-fetch: ^3.0.6 From c885318b659a68e16ae3f9adf19761dd7d94bfbb Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Wed, 13 Apr 2022 16:12:00 +0200 Subject: [PATCH 05/23] Fix unit tests --- .../src/composables/upload/index.ts | 2 +- .../components/AppBar/Upload/FileDrop.spec.js | 110 ------------- .../AppBar/Upload/FileUpload.spec.js | 32 +--- .../AppBar/Upload/FolderUpload.spec.js | 31 +--- .../web-app-files/tests/unit/mixins.spec.js | 37 ----- .../tests/unit/views/FilesDrop.spec.js | 130 +++------------ .../__snapshots__/FilesDrop.spec.js.snap | 151 +----------------- 7 files changed, 39 insertions(+), 454 deletions(-) delete mode 100644 packages/web-app-files/tests/unit/components/AppBar/Upload/FileDrop.spec.js delete mode 100644 packages/web-app-files/tests/unit/mixins.spec.js diff --git a/packages/web-app-files/src/composables/upload/index.ts b/packages/web-app-files/src/composables/upload/index.ts index 7e3692274ba..a0ccc7142e7 100644 --- a/packages/web-app-files/src/composables/upload/index.ts +++ b/packages/web-app-files/src/composables/upload/index.ts @@ -1 +1 @@ -export * from './useUpload' +export * from './useUploadHelpers' diff --git a/packages/web-app-files/tests/unit/components/AppBar/Upload/FileDrop.spec.js b/packages/web-app-files/tests/unit/components/AppBar/Upload/FileDrop.spec.js deleted file mode 100644 index f8adca056cd..00000000000 --- a/packages/web-app-files/tests/unit/components/AppBar/Upload/FileDrop.spec.js +++ /dev/null @@ -1,110 +0,0 @@ -import { createLocalVue, shallowMount } from '@vue/test-utils' -import Vuex from 'vuex' -import vue2DropZone from 'vue2-dropzone' -import FileDrop from '../../../../../src/components/AppBar/Upload/FileDrop.vue' - -const localVue = createLocalVue() -localVue.use(Vuex) -localVue.use(vue2DropZone) - -describe('FileDrop component', () => { - afterEach(() => { - jest.clearAllMocks() - }) - - describe('when dropzone is disabled', () => { - it('should not render component', () => { - const store = createStore({ - dropzone: jest.fn(() => false) - }) - const wrapper = createWrapper({ store }) - const dropZone = wrapper.findComponent(vue2DropZone) - - expect(dropZone.exists()).toBeFalsy() - }) - }) - - describe('when dropzone is enabled', () => { - const spyOcUploadAddDropToQueue = jest - .spyOn(FileDrop.mixins[0].methods, '$_ocUpload_addDropToQueue') - .mockImplementation() - const spyOcDropzoneDragEnd = jest - .spyOn(FileDrop.methods, '$_ocDropzone_dragEnd') - .mockImplementation() - const spyOcDropzoneRemoveFiles = jest - .spyOn(FileDrop.methods, '$_ocDropzone_removeFiles') - .mockImplementation() - - const store = createStore({ - dropzone: jest.fn(() => true) - }) - const wrapper = createWrapper({ store }) - const dropZone = wrapper.findComponent(vue2DropZone) - - it('should render component', () => { - wrapper.setData({ - ocDropzone_options: { - url: '#', - clickable: false, - autoQueue: false - } - }) - expect(dropZone.exists()).toBeTruthy() - expect(dropZone.props().options.url).toEqual('#') - expect(dropZone.props().options.clickable).toEqual(false) - expect(dropZone.props().options.autoQueue).toEqual(false) - expect(dropZone.text()).toEqual('Drag and drop to upload content into current folder') - }) - it('should call "$_ocUpload_addDropToQueue" if file-drop event is triggered', () => { - dropZone.vm.$emit('vdropzone-drop') - - expect(spyOcUploadAddDropToQueue).toHaveBeenCalledTimes(1) - }) - it('should call "$_ocDropzone_dragEnd" if files-added event is triggered', () => { - dropZone.vm.$emit('vdropzone-files-added') - - expect(spyOcDropzoneDragEnd).toHaveBeenCalledTimes(1) - }) - it('should call "$_ocDropzone_dragEnd" if drag-leave event is triggered', () => { - dropZone.vm.$emit('vdropzone-drag-leave') - - expect(spyOcDropzoneDragEnd).toHaveBeenCalledTimes(1) - }) - it('should call "$_ocDropzone_removeFiles" if file remove event is triggered', () => { - dropZone.vm.$emit('vdropzone-file-added') - - expect(spyOcDropzoneRemoveFiles).toHaveBeenCalledTimes(1) - }) - }) -}) - -function createWrapper(options = {}) { - return shallowMount(FileDrop, { - localVue, - stubs: { translate: true, 'oc-dropzone': true }, - propsData: { - rootPath: '/', - path: '/' - }, - computed: { - hasSidebarNavItems: jest.fn(() => true) - }, - ...options - }) -} - -function createStore(getters) { - return new Vuex.Store({ - state: { - navigation: { - closed: false - } - }, - modules: { - Files: { - namespaced: true, - getters - } - } - }) -} diff --git a/packages/web-app-files/tests/unit/components/AppBar/Upload/FileUpload.spec.js b/packages/web-app-files/tests/unit/components/AppBar/Upload/FileUpload.spec.js index 756a99c3cfd..5000efd3932 100644 --- a/packages/web-app-files/tests/unit/components/AppBar/Upload/FileUpload.spec.js +++ b/packages/web-app-files/tests/unit/components/AppBar/Upload/FileUpload.spec.js @@ -1,4 +1,4 @@ -import { mount, shallowMount, createLocalVue } from '@vue/test-utils' +import { mount, createLocalVue } from '@vue/test-utils' import FileUpload from '../../../../../src/components/AppBar/Upload/FileUpload.vue' import DesignSystem from 'owncloud-design-system' @@ -32,28 +32,6 @@ describe('File Upload Component', () => { expect(spyTriggerUpload).toHaveBeenCalledTimes(1) expect(fileUploadInput.element.click).toHaveBeenCalledTimes(1) }) - - describe('when file is selected for upload', () => { - const event = new Event('change') - - it('should call "$_ocUpload_addFileToQueue"', async () => { - const wrapper = shallowMount(FileUpload, { - ...getOptions(), - stubs: { - 'oc-button': true, - 'oc-icon': true - } - }) - wrapper.vm.$_ocUpload_addFileToQueue = jest.fn() - await wrapper.vm.$forceUpdate() - - const fileUploadInput = wrapper.find('#fileUploadInput') - await fileUploadInput.trigger('change') - - expect(wrapper.vm.$_ocUpload_addFileToQueue).toHaveBeenCalledTimes(1) - expect(wrapper.vm.$_ocUpload_addFileToQueue).toHaveBeenCalledWith(event) - }) - }) }) }) @@ -65,6 +43,12 @@ function getOptions() { path: '' }, localVue, - directives: { Translate } + directives: { Translate }, + mocks: { + $uppyService: { + registerUploadInput: jest.fn(), + removeUploadInput: jest.fn() + } + } } } diff --git a/packages/web-app-files/tests/unit/components/AppBar/Upload/FolderUpload.spec.js b/packages/web-app-files/tests/unit/components/AppBar/Upload/FolderUpload.spec.js index 37da0e08832..940d0f15427 100644 --- a/packages/web-app-files/tests/unit/components/AppBar/Upload/FolderUpload.spec.js +++ b/packages/web-app-files/tests/unit/components/AppBar/Upload/FolderUpload.spec.js @@ -1,4 +1,4 @@ -import { mount, shallowMount, createLocalVue } from '@vue/test-utils' +import { mount, createLocalVue } from '@vue/test-utils' import FolderUpload from '@files/src/components/AppBar/Upload/FolderUpload.vue' import DesignSystem from 'owncloud-design-system' @@ -17,7 +17,13 @@ describe('FolderUpload Component', () => { path: '/' }, localVue, - directives: { translate: jest.fn() } + directives: { translate: jest.fn() }, + mocks: { + $uppyService: { + registerUploadInput: jest.fn(), + removeUploadInput: jest.fn() + } + } } describe('when upload folder button is clicked', () => { @@ -34,26 +40,5 @@ describe('FolderUpload Component', () => { expect(spyTriggerUpload).toHaveBeenCalledTimes(1) expect(spyClickUploadInput).toHaveBeenCalledTimes(1) }) - - describe('when folder is selected for upload', () => { - it('should call "$_ocUpload_addDirectoryToQueue"', async () => { - const spyOcUploadAddDirectoryToQueue = jest - .spyOn(FolderUpload.mixins[0].methods, '$_ocUpload_addDirectoryToQueue') - .mockImplementation() - const wrapper = shallowMount(FolderUpload, { - ...mountOptions, - stubs: { - 'oc-icon': true, - 'oc-resource-icon': true, - 'oc-button': true - } - }) - - const folderUploadInput = wrapper.find(selector.uploadInput) - await folderUploadInput.trigger('change') - - expect(spyOcUploadAddDirectoryToQueue).toHaveBeenCalledTimes(1) - }) - }) }) }) diff --git a/packages/web-app-files/tests/unit/mixins.spec.js b/packages/web-app-files/tests/unit/mixins.spec.js deleted file mode 100644 index db29ff7d9e7..00000000000 --- a/packages/web-app-files/tests/unit/mixins.spec.js +++ /dev/null @@ -1,37 +0,0 @@ -import Vuex from 'vuex' -import mixins from '../../src/mixins' -import { createLocalVue, mount } from '@vue/test-utils' - -const localVue = createLocalVue() -localVue.use(Vuex) - -describe('mixins', () => { - describe('checkIfElementExists', () => { - const Component = { - render() {} - } - const wrapper = mount(Component, { - localVue, - mixins: [mixins], - store: new Vuex.Store({ - modules: { - Files: { - namespaced: true, - getters: { - files: () => [{ name: 'file1', size: 1220 }, { name: 'file2' }] - } - } - } - }) - }) - it('should return the first found element if it exists in store files list', () => { - expect(wrapper.vm.checkIfElementExists({ name: 'file1' })).toMatchObject({ name: 'file1' }) - }) - it('should return the first found element with provided name if it exists in store files list', () => { - expect(wrapper.vm.checkIfElementExists('file1')).toMatchObject({ name: 'file1' }) - }) - it("should return undefined if the element doesn't exist in store files list", () => { - expect(wrapper.vm.checkIfElementExists({ name: 'file3' })).toBe(undefined) - }) - }) -}) diff --git a/packages/web-app-files/tests/unit/views/FilesDrop.spec.js b/packages/web-app-files/tests/unit/views/FilesDrop.spec.js index 74ce36958ea..78e674fd381 100644 --- a/packages/web-app-files/tests/unit/views/FilesDrop.spec.js +++ b/packages/web-app-files/tests/unit/views/FilesDrop.spec.js @@ -1,12 +1,12 @@ import FilesDrop from '@files/src/views/FilesDrop.vue' import { shallowMount } from '@vue/test-utils' import GetTextPlugin from 'vue-gettext' -import vue2DropZone from 'vue2-dropzone' import { getStore, localVue } from './views.setup.js' import { DavProperty } from 'web-pkg/src/constants' import { linkRoleUploaderFolder } from '@files/src/helpers/share' +import VueCompositionAPI from '@vue/composition-api/dist/vue-composition-api' -localVue.use(vue2DropZone) +localVue.use(VueCompositionAPI) localVue.use(GetTextPlugin, { translations: 'does-not-matter.json', silent: true @@ -45,8 +45,6 @@ const selectors = { } const ocSpinnerStubSelector = 'oc-spinner-stub' -const ocTableSimpleStubSelector = 'oc-table-simple-stub' -const ocTableRowStubSelector = 'oc-tr-stub' describe('FilesDrop', () => { it('should call "resolvePublicLink" method on wrapper mount', () => { @@ -71,8 +69,6 @@ describe('FilesDrop', () => { }) describe('when "loading" is set to false', () => { - const spyDropZoneFileAdded = jest.spyOn(FilesDrop.methods, 'dropZoneFileAdded') - const wrapper = getShallowWrapper() it('should not show spinner and loading header', () => { @@ -85,25 +81,8 @@ describe('FilesDrop', () => { }) it('should show vue drop zone with given options', () => { - const dropZone = wrapper.findComponent(vue2DropZone) - + const dropZone = wrapper.find('#files-drop-zone') expect(dropZone.exists()).toBeTruthy() - expect(dropZone.props()).toMatchObject({ - id: 'oc-dropzone', - options: { - // from the mocked function - url: 'http://some-url/abc123def456/', - clickable: true, - createImageThumbnails: false, - autoQueue: false, - previewsContainer: '#previews' - }, - includeStyling: false, - awss3: null, - destroyDropzone: true, - duplicateCheck: false, - useCustomSlot: true - }) }) it('should show error message if only it has truthy value', () => { @@ -114,92 +93,6 @@ describe('FilesDrop', () => { expect(wrapper).toMatchSnapshot() }) - - it('should not show files table if uploaded files list is empty', () => { - expect(wrapper.find(selectors.filesEmpty).exists()).toBeTruthy() - expect(wrapper.find(selectors.filesEmpty)).toMatchSnapshot() - expect(wrapper.find(ocTableSimpleStubSelector).exists()).toBeFalsy() - }) - - it('should show files list of uploaded files', async () => { - const uploadedFiles = new Map() - const files = [ - { name: 'file1', size: 300, status: 'error' }, - { name: 'file2', size: 600, status: 'done' }, - { name: 'file3', size: 900, status: 'init' }, - { name: 'file4', size: 1200, status: 'uploading' } - ] - uploadedFiles.set(1, files[0]) - uploadedFiles.set(2, files[1]) - uploadedFiles.set(3, files[2]) - uploadedFiles.set(4, files[3]) - - const wrapper = shallowMount(FilesDrop, { - localVue, - store: createStore(), - mocks: { - $route - }, - data() { - return { - uploadedFiles: uploadedFiles, - uploadedFilesChangeTracker: 1 - } - } - }) - - // table is visible only after two next-ticks - await wrapper.vm.$nextTick() - await wrapper.vm.$nextTick() - - const tableElement = wrapper.find(ocTableSimpleStubSelector) - const rows = tableElement.findAll(ocTableRowStubSelector) - - expect(rows.length).toBe(4) - - expect(wrapper).toMatchSnapshot() - }) - - it('should call "dropZoneFileAdded" method if "vdropzone-file-added" event is emitted', async () => { - const event = { - upload: { - uuid: 'abc123' - }, - name: 'file1.txt', - size: '300' - } - const expectedMap = new Map() - const expectedDoneMap = expectedMap.set(event.upload.uuid, { - name: event.name, - size: event.size, - status: 'done' - }) - - const dropZone = wrapper.findComponent(vue2DropZone) - - expect(spyDropZoneFileAdded).not.toHaveBeenCalled() - - // drag and drop cannot be performed in unit tests - // only checking how wrapper responds when dropzone emits the event specified below - await dropZone.vm.$emit('vdropzone-file-added', event) - - expect(spyDropZoneFileAdded).toHaveBeenCalledTimes(1) - expect(wrapper.vm.uploadedFilesChangeTracker).toBe(1) - - await wrapper.vm.$nextTick() - - expect(wrapper.vm.uploadedFilesChangeTracker).toBe(2) - expect(wrapper.vm.uploadedFiles).toMatchObject(expectedDoneMap) - - await wrapper.vm.$nextTick() - - const tableElement = wrapper.find(ocTableSimpleStubSelector) - const rows = tableElement.findAll(ocTableRowStubSelector) - - expect(rows.length).toBe(1) - - expect(wrapper).toMatchSnapshot() - }) }) }) @@ -212,7 +105,22 @@ function getShallowWrapper({ store = createStore(), loading = false, errorMessag localVue, store, mocks: { - $route + $route, + $router: { + currentRoute: { name: 'some-route' }, + resolve: (r) => { + return { href: r.name } + }, + afterEach: jest.fn() + }, + $uppyService: { + $on: jest.fn(), + useDropTarget: jest.fn(), + useXhr: jest.fn() + } + }, + setup: () => { + return {} }, data() { return { diff --git a/packages/web-app-files/tests/unit/views/__snapshots__/FilesDrop.spec.js.snap b/packages/web-app-files/tests/unit/views/__snapshots__/FilesDrop.spec.js.snap index eb26a8d83ae..69b0ef58120 100644 --- a/packages/web-app-files/tests/unit/views/__snapshots__/FilesDrop.spec.js.snap +++ b/packages/web-app-files/tests/unit/views/__snapshots__/FilesDrop.spec.js.snap @@ -7,17 +7,9 @@ exports[`FilesDrop should show page title and configuration theme general slogan

admin shared this folder with you for uploading

- -
- - Drop files here to upload or click to select file -
-
+
-
- -
@@ -44,53 +36,6 @@ exports[`FilesDrop should show spinner with loading text if wrapper is loading 1 `; -exports[`FilesDrop when "loading" is set to false should call "dropZoneFileAdded" method if "vdropzone-file-added" event is emitted 1`] = ` -
-

page route title

-
-
-
-

admin shared this folder with you for uploading

- -
- - Drop files here to upload or click to select file -
-
- -
-
- - - - file1.txt - - - - - - - - - - - -
- -
-
-
-

some slogan

-
-
-`; - -exports[`FilesDrop when "loading" is set to false should not show files table if uploaded files list is empty 1`] = ` -
- -
-`; - exports[`FilesDrop when "loading" is set to false should show error message if only it has truthy value 1`] = `

page route title

@@ -98,17 +43,9 @@ exports[`FilesDrop when "loading" is set to false should show error message if o

admin shared this folder with you for uploading

- -
- - Drop files here to upload or click to select file -
-
+
-
- -

An error occurred while loading the public link @@ -123,80 +60,6 @@ exports[`FilesDrop when "loading" is set to false should show error message if o

`; -exports[`FilesDrop when "loading" is set to false should show files list of uploaded files 1`] = ` -
-

page route title

-
-
-
-

admin shared this folder with you for uploading

- -
- - Drop files here to upload or click to select file -
-
- -
-
- - - - file1 - - - - - - - - - - - file2 - - - - - - - - - - - file3 - - - - - - - - - - - file4 - - - - - - - - - - - -
- -
-
-
-

some slogan

-
-
-`; - exports[`FilesDrop when "loading" is set to false should show share information title 1`] = `

page route title

@@ -204,17 +67,9 @@ exports[`FilesDrop when "loading" is set to false should show share information

admin shared this folder with you for uploading

- -
- - Drop files here to upload or click to select file -
-
+
-
- -
From 640f23645aa44bbe054c7d1b3ef043c74e213e60 Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Wed, 13 Apr 2022 16:43:34 +0200 Subject: [PATCH 06/23] Remove logs and unnecessary events --- .../src/components/AppBar/CreateAndUpload.vue | 1 - packages/web-app-files/src/views/FilesDrop.vue | 6 ------ packages/web-runtime/src/components/UploadInfo.vue | 8 ++++++-- packages/web-runtime/src/services/uppyService.ts | 2 -- 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/web-app-files/src/components/AppBar/CreateAndUpload.vue b/packages/web-app-files/src/components/AppBar/CreateAndUpload.vue index 9c61a69dceb..030eea7670b 100644 --- a/packages/web-app-files/src/components/AppBar/CreateAndUpload.vue +++ b/packages/web-app-files/src/components/AppBar/CreateAndUpload.vue @@ -280,7 +280,6 @@ export default defineComponent({ } resource = buildResource(resource) - this.$uppyService.$emit('fileUploadedSuccessfully', resource, fileRoute) // Update table only if the file was uploaded to the current directory if (fileIsInCurrentPath) { diff --git a/packages/web-app-files/src/views/FilesDrop.vue b/packages/web-app-files/src/views/FilesDrop.vue index 3106f1ffc61..0053429af68 100644 --- a/packages/web-app-files/src/views/FilesDrop.vue +++ b/packages/web-app-files/src/views/FilesDrop.vue @@ -57,7 +57,6 @@ export default { onMounted(() => { uppyService.$on('filesSelected', instance.onFilesSelected) - uppyService.$on('uploadSuccess', instance.onFileSuccess) uppyService.$on('uploadError', instance.onFileError) uppyService.useDropTarget({ @@ -68,7 +67,6 @@ export default { onUnmounted(() => { uppyService.$off('filesSelected', instance.onFilesSelected) - uppyService.$off('uploadSuccess', instance.onFileSuccess) uppyService.$off('uploadError', instance.onFileError) }) @@ -167,10 +165,6 @@ export default { this.$uppyService.uploadFiles(uppyResources) }, - onFileSuccess(file) { - this.$uppyService.$emit('fileUploadedSuccessfully', file) - }, - onFileError(error) { console.error(error) this.showMessage({ diff --git a/packages/web-runtime/src/components/UploadInfo.vue b/packages/web-runtime/src/components/UploadInfo.vue index 70b8c8ff550..032b49ad004 100644 --- a/packages/web-runtime/src/components/UploadInfo.vue +++ b/packages/web-runtime/src/components/UploadInfo.vue @@ -110,8 +110,12 @@ export default { this.filesUploading = 0 this.uploadCancelled = true }) - this.$uppyService.$on('fileUploadedSuccessfully', (file, route) => { - this.successfulUploads.push({ ...file, targetRoute: route }) + this.$uppyService.$on('uploadSuccess', (file) => { + this.successfulUploads.push({ + ...file, + path: `${file.meta.relativeFolder}/${file.name}`, + targetRoute: file.meta.route + }) }) }, methods: { diff --git a/packages/web-runtime/src/services/uppyService.ts b/packages/web-runtime/src/services/uppyService.ts index 0ec9002032b..7665b109fe2 100644 --- a/packages/web-runtime/src/services/uppyService.ts +++ b/packages/web-runtime/src/services/uppyService.ts @@ -158,7 +158,6 @@ export class UppyService extends Vue { this.$emit('uploadCompleted') result.successful.forEach((file) => { this.$emit('uploadSuccess', file) - console.log('SUCCESS FOR: ', file.name) this.uppy.removeFile(file.id) }) @@ -208,7 +207,6 @@ export class UppyService extends Vue { uploadFiles(files: UppyResource[]) { files.forEach((file) => { try { - console.log('START UPLOAD FOR: ', file.name) this.uppy.addFile(file) } catch (err) { console.error('error upload file:', file) From 5951d9d39554ab4bdbcd316ad8b72b6c40e28b6c Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Thu, 14 Apr 2022 11:23:53 +0200 Subject: [PATCH 07/23] Minor fixes and improvements --- .../src/components/AppBar/CreateAndUpload.vue | 3 +-- .../components/AppBar/Upload/FileUpload.vue | 2 +- .../components/AppBar/Upload/FolderUpload.vue | 2 +- .../src/composables/upload/useUploadHelpers.ts | 7 ++++++- packages/web-app-files/src/views/FilesDrop.vue | 4 ++-- .../web-runtime/src/components/UploadInfo.vue | 18 ++++++++++++++++-- .../src/composables/upload/useUpload.ts | 5 ++--- .../web-runtime/src/services/uppyService.ts | 6 +++--- 8 files changed, 32 insertions(+), 15 deletions(-) diff --git a/packages/web-app-files/src/components/AppBar/CreateAndUpload.vue b/packages/web-app-files/src/components/AppBar/CreateAndUpload.vue index 030eea7670b..1a58461154e 100644 --- a/packages/web-app-files/src/components/AppBar/CreateAndUpload.vue +++ b/packages/web-app-files/src/components/AppBar/CreateAndUpload.vue @@ -257,7 +257,6 @@ export default defineComponent({ pathFileWasUploadedTo += file.meta.relativeFolder } - const fileRoute = file.meta.route const fileIsInCurrentPath = pathFileWasUploadedTo === this.currentPath await this.$nextTick() @@ -612,7 +611,7 @@ export default defineComponent({ const conflicts = [] const uppyResources: UppyResource[] = this.inputFilesToUppyFiles(files) for (const file of uppyResources) { - const relativeFilePath = file.meta.relativeFilePath + const relativeFilePath = file.meta.relativePath if (relativeFilePath) { const rootFolder = relativeFilePath.replace(/^\/+/, '').split('/')[0] const exists = this.files.find((f) => f.name === rootFolder) diff --git a/packages/web-app-files/src/components/AppBar/Upload/FileUpload.vue b/packages/web-app-files/src/components/AppBar/Upload/FileUpload.vue index 7c2d67a1e2f..1a22d1217a0 100644 --- a/packages/web-app-files/src/components/AppBar/Upload/FileUpload.vue +++ b/packages/web-app-files/src/components/AppBar/Upload/FileUpload.vue @@ -37,7 +37,7 @@ export default { this.$uppyService.registerUploadInput(this.$refs.input) }, beforeDestroy() { - this.$uppyService.removeUploadInput(this.$refs.input.id) + this.$uppyService.removeUploadInput(this.$refs.input) }, methods: { triggerUpload() { diff --git a/packages/web-app-files/src/components/AppBar/Upload/FolderUpload.vue b/packages/web-app-files/src/components/AppBar/Upload/FolderUpload.vue index 4712415c67b..e28b78f1f2c 100644 --- a/packages/web-app-files/src/components/AppBar/Upload/FolderUpload.vue +++ b/packages/web-app-files/src/components/AppBar/Upload/FolderUpload.vue @@ -25,7 +25,7 @@ export default { this.$uppyService.registerUploadInput(this.$refs.input) }, beforeDestroy() { - this.$uppyService.removeUploadInput(this.$refs.input.id) + this.$uppyService.removeUploadInput(this.$refs.input) }, methods: { triggerUpload() { diff --git a/packages/web-app-files/src/composables/upload/useUploadHelpers.ts b/packages/web-app-files/src/composables/upload/useUploadHelpers.ts index 026c4d1b8eb..a7b4733c96f 100644 --- a/packages/web-app-files/src/composables/upload/useUploadHelpers.ts +++ b/packages/web-app-files/src/composables/upload/useUploadHelpers.ts @@ -125,11 +125,15 @@ const inputFilesToUppyFiles = ({ route, uploadPath, currentPath, user }: inputFi const currentFolder = unref(currentPath) for (const file of files) { + // Get the relative path of the file when the file was inside a directory on the client computer const relativeFilePath = file.webkitRelativePath || (file as any).relativePath || null + // Directory without filename const directory = relativeFilePath ? relativeFilePath.substring(0, relativeFilePath.lastIndexOf('/')) : '' + // Build tus endpoint to dynamically set it on file upload. + // Looks something like: https://localhost:9200/remote.php/dav/files/admin let tusEndpoint if (directory) { tusEndpoint = `${unref(uploadPath).replace(/\/+$/, '')}/${directory.replace(/^\/+/, '')}` @@ -137,6 +141,7 @@ const inputFilesToUppyFiles = ({ route, uploadPath, currentPath, user }: inputFi tusEndpoint = unref(uploadPath) } + // Build the route object for this file. This is used later by the upload-info-box const item = params.item ? `${params.item}/${directory}` : directory const fileRoute = { ...unref(route), @@ -156,7 +161,7 @@ const inputFilesToUppyFiles = ({ route, uploadPath, currentPath, user }: inputFi meta: { currentFolder, relativeFolder: directory, - relativeFilePath, + relativePath: relativeFilePath, // uppy needs this property to be named relativePath route: fileRoute, tusEndpoint, webDavPath diff --git a/packages/web-app-files/src/views/FilesDrop.vue b/packages/web-app-files/src/views/FilesDrop.vue index 0053429af68..b47abc8fbf4 100644 --- a/packages/web-app-files/src/views/FilesDrop.vue +++ b/packages/web-app-files/src/views/FilesDrop.vue @@ -83,8 +83,8 @@ export default { } }, computed: { - ...mapGetters(['capabilities', 'configuration', 'newFileHandlers', 'user']), - ...mapGetters('Files', ['currentFolder', 'publicLinkPassword']), + ...mapGetters(['configuration']), + ...mapGetters('Files', ['publicLinkPassword']), pageTitle() { return this.$gettext(this.$route.meta.title) }, diff --git a/packages/web-runtime/src/components/UploadInfo.vue b/packages/web-runtime/src/components/UploadInfo.vue index 032b49ad004..7a511962c77 100644 --- a/packages/web-runtime/src/components/UploadInfo.vue +++ b/packages/web-runtime/src/components/UploadInfo.vue @@ -33,7 +33,7 @@ :key="item.path" :resource="item" :is-path-displayed="true" - :is-thumbnail-displayed="true" + :is-thumbnail-displayed="displayThumbnails" :is-resource-clickable="false" :parent-folder-name-default="defaultParentFolderName(item)" :folder-link="folderLink(item)" @@ -60,6 +60,7 @@ import '@uppy/core/dist/style.css' import '@uppy/status-bar/dist/style.css' import path from 'path' import { useCapabilitySpacesEnabled } from 'web-pkg/src/composables' +import { mapGetters } from 'vuex' export default { setup() { @@ -74,6 +75,8 @@ export default { successfulUploads: [] }), computed: { + ...mapGetters(['configuration']), + uploadInfoTitle() { if (this.filesUploading) { return this.$gettextInterpolate( @@ -89,6 +92,9 @@ export default { return this.$gettext('Upload cancelled') } return this.$gettext('Upload completed') + }, + displayThumbnails() { + return !this.configuration.options.disablePreviews } }, mounted() { @@ -111,9 +117,17 @@ export default { this.uploadCancelled = true }) this.$uppyService.$on('uploadSuccess', (file) => { + // @TODO we need the storage ID here... maybe fetch the file via DAV and call buildResources()? + + let path = file.meta.currentFolder + if (file.meta.relativePath) { + path += file.meta.relativePath + } + path += file.name + this.successfulUploads.push({ ...file, - path: `${file.meta.relativeFolder}/${file.name}`, + path, targetRoute: file.meta.route }) }) diff --git a/packages/web-runtime/src/composables/upload/useUpload.ts b/packages/web-runtime/src/composables/upload/useUpload.ts index 798c35f042b..51a1bb81ec4 100644 --- a/packages/web-runtime/src/composables/upload/useUpload.ts +++ b/packages/web-runtime/src/composables/upload/useUpload.ts @@ -11,7 +11,6 @@ import { useActiveLocation } from 'files/src/composables' import { isLocationPublicActive } from 'files/src/router' import { UppyService } from '../../services/uppyService' -// FIXME: properly extend type from AddFileOptions = Record> export interface UppyResource { id?: string source: string @@ -21,7 +20,7 @@ export interface UppyResource { meta: { currentFolder: string relativeFolder: string - relativeFilePath: string + relativePath: string route: Route tusEndpoint: string webDavPath: string @@ -85,7 +84,7 @@ export function useUpload(options: UploadOptions): UploadResult { options.uppyService.useTus(unref(uppyOptions) as any) return } - options.uppyService.useXhr(unref(uppyOptions).headers as any) + options.uppyService.useXhr(unref(uppyOptions) as any) }, { immediate: true } ) diff --git a/packages/web-runtime/src/services/uppyService.ts b/packages/web-runtime/src/services/uppyService.ts index 7665b109fe2..3bbc49fb1fd 100644 --- a/packages/web-runtime/src/services/uppyService.ts +++ b/packages/web-runtime/src/services/uppyService.ts @@ -147,7 +147,7 @@ export class UppyService extends Vue { }) } - setUpEvents() { + private setUpEvents() { this.uppy.on('upload', () => { this.$emit('uploadStarted') }) @@ -200,8 +200,8 @@ export class UppyService extends Vue { } } - removeUploadInput(elementId: string) { - this.uploadInputs = this.uploadInputs.filter((el) => el.id !== elementId) + removeUploadInput(el: HTMLInputElement) { + this.uploadInputs = this.uploadInputs.filter((input) => input !== el) } uploadFiles(files: UppyResource[]) { From 648da581acf0e7297b7ed091de88ca1da6daca0d Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Thu, 14 Apr 2022 14:16:21 +0200 Subject: [PATCH 08/23] Fix ocis e2e tests --- packages/web-runtime/src/components/UploadInfo.vue | 2 +- tests/e2e/support/objects/app-files/resource/actions.ts | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/web-runtime/src/components/UploadInfo.vue b/packages/web-runtime/src/components/UploadInfo.vue index 7a511962c77..597b5b93f70 100644 --- a/packages/web-runtime/src/components/UploadInfo.vue +++ b/packages/web-runtime/src/components/UploadInfo.vue @@ -16,7 +16,7 @@ - +
diff --git a/tests/e2e/support/objects/app-files/resource/actions.ts b/tests/e2e/support/objects/app-files/resource/actions.ts index e0b2cbd02a5..1299c624fe4 100644 --- a/tests/e2e/support/objects/app-files/resource/actions.ts +++ b/tests/e2e/support/objects/app-files/resource/actions.ts @@ -74,13 +74,12 @@ export const uploadResource = async (args: uploadResourceArgs): Promise => await page.locator('#fileUploadInput').setInputFiles(resources.map((file) => file.path)) if (createVersion) { - const fileName = resources.map((file) => path.basename(file.name)) - await Promise.all([ - page.waitForResponse((resp) => resp.url().endsWith(fileName[0]) && resp.status() === 204), - page.locator('.oc-modal-body-actions-confirm').click() - ]) + await page.locator('.oc-modal-body-actions-confirm').click() + // @TODO check if upload was successful } + await page.locator('#close-upload-info-btn').click() + await waitForResources({ page: page, names: resources.map((file) => path.basename(file.name)) From 39f8ff4e970c14e723afcc22c23eb4923ad044d7 Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Thu, 14 Apr 2022 14:33:18 +0200 Subject: [PATCH 09/23] Fix more e2e tests --- tests/e2e/support/objects/app-files/page/public.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/support/objects/app-files/page/public.ts b/tests/e2e/support/objects/app-files/page/public.ts index 56198384fad..2f5e85a3338 100644 --- a/tests/e2e/support/objects/app-files/page/public.ts +++ b/tests/e2e/support/objects/app-files/page/public.ts @@ -23,7 +23,7 @@ export class Public { async upload({ resources }: { resources: File[] }): Promise { await this.#page - .locator('//input[@id="file_upload_start" or @class="dz-hidden-input"]') + .locator('//input[@id="fileUploadInput"]') .setInputFiles(resources.map((file) => file.path)) } } From 7f1b2ea8b0d61ef998b7bf3c7cbf8c55fff30a1b Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Mon, 25 Apr 2022 10:57:16 +0200 Subject: [PATCH 10/23] Prevent form fields from being added to uploaded file contents --- packages/web-runtime/src/services/uppyService.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/web-runtime/src/services/uppyService.ts b/packages/web-runtime/src/services/uppyService.ts index 3bbc49fb1fd..5491b7d1e0a 100644 --- a/packages/web-runtime/src/services/uppyService.ts +++ b/packages/web-runtime/src/services/uppyService.ts @@ -57,6 +57,7 @@ export class UppyService extends Vue { endpoint: '', method: 'put', headers, + formData: false, getResponseData() { return {} } From 435ad36179bcda03ba31419392b9c2c162b44b12 Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Mon, 25 Apr 2022 12:45:41 +0200 Subject: [PATCH 11/23] Properly encode file name for XHR uploads --- packages/web-runtime/src/services/uppyService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/web-runtime/src/services/uppyService.ts b/packages/web-runtime/src/services/uppyService.ts index 5491b7d1e0a..891a939c2e4 100644 --- a/packages/web-runtime/src/services/uppyService.ts +++ b/packages/web-runtime/src/services/uppyService.ts @@ -176,9 +176,10 @@ export class UppyService extends Vue { this.$emit('fileAdded') const addedFile = file as unknown as UppyResource if (this.uppy.getPlugin('XHRUpload')) { + const escapedName = encodeURIComponent(addedFile.name) this.uppy.setFileState(addedFile.id, { xhrUpload: { - endpoint: `${addedFile.meta.tusEndpoint.replace(/\/+$/, '')}/${addedFile.name}` + endpoint: `${addedFile.meta.tusEndpoint.replace(/\/+$/, '')}/${escapedName}` } }) } From c1620022d8ab9f253726822f636ab0e579cc4326 Mon Sep 17 00:00:00 2001 From: saw-jan Date: Mon, 25 Apr 2022 15:07:13 +0545 Subject: [PATCH 12/23] update fileDrop acceptance tests --- .../shareByPublicLinkDifferentRoles.feature | 4 ++-- tests/acceptance/pageObjects/filesDropPage.js | 6 +++--- tests/acceptance/pageObjects/personalPage.js | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/acceptance/features/webUISharingPublicDifferentRoles/shareByPublicLinkDifferentRoles.feature b/tests/acceptance/features/webUISharingPublicDifferentRoles/shareByPublicLinkDifferentRoles.feature index acf0005e7a2..f80efaa6dd3 100644 --- a/tests/acceptance/features/webUISharingPublicDifferentRoles/shareByPublicLinkDifferentRoles.feature +++ b/tests/acceptance/features/webUISharingPublicDifferentRoles/shareByPublicLinkDifferentRoles.feature @@ -71,7 +71,7 @@ Feature: Share by public link with different roles | name | Public link | And a link named "Public link" should be listed with role "Uploader" in the public link list of folder "simple-folder" on the webUI When the public uses the webUI to access the last public link created by user "Alice" in a new session - Then there should be no resources listed on the webUI + Then the user should be redirected to the files-drop page @skipOnOC10 @issue-ocis-reva-383 #after fixing the issue delete this scenario and use the one above by deleting the @skipOnOCIS tag there @@ -87,7 +87,7 @@ Feature: Share by public link with different roles | path | /simple-folder | And a public link with the last created link share token as name should be listed for resource "simple-folder" on the webUI When the public uses the webUI to access the last public link created by user "Alice" in a new session - Then there should be no resources listed on the webUI + Then the user should be redirected to the files-drop page @issue-4582 @disablePreviews Scenario: creating a public link with "Editor" role makes it possible to delete files via the link diff --git a/tests/acceptance/pageObjects/filesDropPage.js b/tests/acceptance/pageObjects/filesDropPage.js index 8f9bdd68a9f..707736792cf 100644 --- a/tests/acceptance/pageObjects/filesDropPage.js +++ b/tests/acceptance/pageObjects/filesDropPage.js @@ -44,7 +44,7 @@ module.exports = { const files = [] for (const { ELEMENT } of elements) { await this.api.elementIdText(ELEMENT, function (result) { - files.push(result.value) + files.push(result.value.replace('\n', '')) }) } return files @@ -56,11 +56,11 @@ module.exports = { locateStrategy: 'css selector' }, uploadedFiles: { - selector: 'table tr > td:first-child', + selector: '.upload-info-successful-uploads span.oc-resource-name', locateStrategy: 'css selector' }, fileDropzone: { - selector: '#oc-dropzone', + selector: '#files-drop-container', locateStrategy: 'css selector' } } diff --git a/tests/acceptance/pageObjects/personalPage.js b/tests/acceptance/pageObjects/personalPage.js index e3b566924c1..480dd78611c 100644 --- a/tests/acceptance/pageObjects/personalPage.js +++ b/tests/acceptance/pageObjects/personalPage.js @@ -168,7 +168,7 @@ module.exports = { false ) .waitForElementNotVisible('@fileUploadProgress') - .click('@newFileMenuButton') + .click('@uploadFilesButton') }, /** * This uploads a folder that is inside the selenium host, From fedb11b3c05e671221144756af29bbdc0781a112 Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Mon, 25 Apr 2022 13:20:25 +0200 Subject: [PATCH 13/23] Remove Uppy drop target plugin on unmount --- .../src/components/AppBar/CreateAndUpload.vue | 3 ++- packages/web-runtime/src/services/uppyService.ts | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/web-app-files/src/components/AppBar/CreateAndUpload.vue b/packages/web-app-files/src/components/AppBar/CreateAndUpload.vue index 1a58461154e..7cf25f07138 100644 --- a/packages/web-app-files/src/components/AppBar/CreateAndUpload.vue +++ b/packages/web-app-files/src/components/AppBar/CreateAndUpload.vue @@ -117,7 +117,7 @@ import FileUpload from './Upload/FileUpload.vue' import FolderUpload from './Upload/FolderUpload.vue' import { defineComponent, getCurrentInstance, onMounted, onUnmounted } from '@vue/composition-api' import { UppyResource, useUpload } from 'web-runtime/src/composables/upload' -import { useUploadHelpers } from '../../composables/upload/useUploadHelpers' +import { useUploadHelpers } from '../../composables/upload' export default defineComponent({ components: { @@ -144,6 +144,7 @@ export default defineComponent({ uppyService.$off('filesSelected', instance.onFilesSelected) uppyService.$off('uploadSuccess', instance.onFileSuccess) uppyService.$off('uploadError', instance.onFileError) + uppyService.removeDropTarget() }) return { diff --git a/packages/web-runtime/src/services/uppyService.ts b/packages/web-runtime/src/services/uppyService.ts index 891a939c2e4..f3326b8af0a 100644 --- a/packages/web-runtime/src/services/uppyService.ts +++ b/packages/web-runtime/src/services/uppyService.ts @@ -90,6 +90,13 @@ export class UppyService extends Vue { }) } + removeDropTarget() { + const dropTargetPlugin = this.uppy.getPlugin('DropTarget') + if (dropTargetPlugin) { + this.uppy.removePlugin(dropTargetPlugin) + } + } + useStatusBar({ targetSelector, getText From 0684dff20d2a734e922635bdbf65134f2b6316ac Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Mon, 25 Apr 2022 16:26:59 +0200 Subject: [PATCH 14/23] Fix issues when uploading nested folders --- .../composables/upload/useUploadHelpers.ts | 46 +++++++++---------- .../src/composables/upload/useUpload.ts | 35 ++++++++++---- 2 files changed, 48 insertions(+), 33 deletions(-) diff --git a/packages/web-app-files/src/composables/upload/useUploadHelpers.ts b/packages/web-app-files/src/composables/upload/useUploadHelpers.ts index a7b4733c96f..dcc4fd2218f 100644 --- a/packages/web-app-files/src/composables/upload/useUploadHelpers.ts +++ b/packages/web-app-files/src/composables/upload/useUploadHelpers.ts @@ -86,33 +86,29 @@ const updateStoreForCreatedFolders = ({ const { owncloudSdk: client } = clientService const fetchedFolders = [] for (const file of files) { - const currentFolder = file.meta.currentFolder const directory = file.meta.relativeFolder + // Only care about the root folders, no need to fetch nested folders + const rootFolder = file.meta.relativeFolder.split('/').slice(0, 2).join('/') + const rootFolderPath = `${file.meta.webDavBasePath}${rootFolder}` - if (!directory || fetchedFolders.includes(directory)) { + if (!directory || fetchedFolders.includes(rootFolderPath)) { continue } - const relativeParts = directory.replace(/^\/+/, '').split('/') - // No need to load folder when it is either deep nested or the user changed paths in between - const buildFolderResource = relativeParts.length === 1 - - if (buildFolderResource) { - let resource - - if (unref(isPublicLocation)) { - resource = await client.publicFiles.getFileInfo( - `${currentFolder}${directory}`, - unref(publicLinkPassword), - DavProperties.PublicLink - ) - } else { - resource = await client.files.fileInfo(file.meta.webDavPath, DavProperties.Default) - } - resource = buildResource(resource) - store.commit('Files/UPSERT_RESOURCE', resource) - fetchedFolders.push(directory) + let resource + if (unref(isPublicLocation)) { + const rootFolder = directory.split('/').slice(0, 2).join('/') + resource = await client.publicFiles.getFileInfo( + `${file.meta.currentFolder}${rootFolder}`, + unref(publicLinkPassword), + DavProperties.PublicLink + ) + } else { + resource = await client.files.fileInfo(rootFolderPath, DavProperties.Default) } + resource = buildResource(resource) + store.commit('Files/UPSERT_RESOURCE', resource) + fetchedFolders.push(rootFolderPath) } } } @@ -149,9 +145,9 @@ const inputFilesToUppyFiles = ({ route, uploadPath, currentPath, user }: inputFi } const storageId = params.storageId - const webDavPath = storageId - ? buildWebDavSpacesPath(storageId, `${currentFolder}${directory}`) - : buildWebDavFilesPath(unref(user)?.id, `${currentFolder}${directory}`) + const webDavBasePath = storageId + ? buildWebDavSpacesPath(storageId, currentFolder) + : buildWebDavFilesPath(unref(user)?.id, currentFolder) uppyFiles.push({ source: 'file input', @@ -164,7 +160,7 @@ const inputFilesToUppyFiles = ({ route, uploadPath, currentPath, user }: inputFi relativePath: relativeFilePath, // uppy needs this property to be named relativePath route: fileRoute, tusEndpoint, - webDavPath + webDavBasePath // WebDAV base path where the files will be uploaded to } }) } diff --git a/packages/web-runtime/src/composables/upload/useUpload.ts b/packages/web-runtime/src/composables/upload/useUpload.ts index 51a1bb81ec4..1101e16053b 100644 --- a/packages/web-runtime/src/composables/upload/useUpload.ts +++ b/packages/web-runtime/src/composables/upload/useUpload.ts @@ -23,7 +23,7 @@ export interface UppyResource { relativePath: string route: Route tusEndpoint: string - webDavPath: string + webDavBasePath: string } } @@ -114,18 +114,37 @@ const createDirectoryTree = ({ const currentFolder = file.meta.currentFolder const directory = file.meta.relativeFolder - // @TODO check common prefixes if (!directory || createdFolders.includes(directory)) { continue } - if (unref(isPublicLocation)) { - await client.publicFiles.createFolder(currentFolder, directory, unref(publicLinkPassword)) - } else { - await client.files.createFolder(file.meta.webDavPath) + const folders = directory.split('/') + let createdSubFolders = '' + for (const subFolder of folders) { + if (!subFolder) { + continue + } + + const folderToCreate = `${createdSubFolders}/${subFolder}` + if (createdFolders.includes(folderToCreate)) { + createdSubFolders += `/${subFolder}` + createdFolders.push(createdSubFolders) + continue + } + + if (unref(isPublicLocation)) { + await client.publicFiles.createFolder( + currentFolder, + folderToCreate, + unref(publicLinkPassword) + ) + } else { + await client.files.createFolder(`${file.meta.webDavBasePath}/${folderToCreate}`) + } + + createdSubFolders += `/${subFolder}` + createdFolders.push(createdSubFolders) } - - createdFolders.push(directory) } } } From 5182cba23f4ac5d8ba3f991ab895bc7d10d98941 Mon Sep 17 00:00:00 2001 From: Florian Schade Date: Mon, 25 Apr 2022 16:57:48 +0200 Subject: [PATCH 15/23] change rollup plugin order to apply injections for ts files also --- rollup.config.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/rollup.config.js b/rollup.config.js index 71eff617204..777e3fa9fe8 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -60,9 +60,6 @@ const plugins = [ css: false }), nodePolyfills(), - inject({ - Buffer: ['buffer', 'Buffer'] - }), resolve({ include: 'node_modules/**', dedupe: ['@vue/composition-api'], @@ -83,6 +80,9 @@ const plugins = [ ts({ browserslist: false }), + inject({ + Buffer: ['buffer', 'Buffer'] + }), json(), copy({ watch: !production && ['./config', './packages/web-runtime/themes'], @@ -150,7 +150,9 @@ const plugins = [ const fp = path.parse(f.fileName) const lastDash = fp.name.lastIndexOf('-') acc[c][ - production ? fp.name.slice(0, lastDash !== -1 ? lastDash : 0) || fp.name : fp.name + production + ? fp.name.slice(0, lastDash !== -1 ? lastDash : 0) || fp.name + : fp.name ] = c === 'js' ? fp.name : f.fileName }) From 52934d6e9030e0fa791d8c45e2e865727c8cc582 Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Tue, 26 Apr 2022 09:01:28 +0200 Subject: [PATCH 16/23] Fix issue with missing slashes when uploading --- .../src/composables/upload/useUploadHelpers.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/web-app-files/src/composables/upload/useUploadHelpers.ts b/packages/web-app-files/src/composables/upload/useUploadHelpers.ts index dcc4fd2218f..47a04ee1654 100644 --- a/packages/web-app-files/src/composables/upload/useUploadHelpers.ts +++ b/packages/web-app-files/src/composables/upload/useUploadHelpers.ts @@ -86,18 +86,17 @@ const updateStoreForCreatedFolders = ({ const { owncloudSdk: client } = clientService const fetchedFolders = [] for (const file of files) { - const directory = file.meta.relativeFolder + const relativeFolder = `/${file.meta.relativeFolder.replace(/^\/+/, '')}` // Only care about the root folders, no need to fetch nested folders - const rootFolder = file.meta.relativeFolder.split('/').slice(0, 2).join('/') - const rootFolderPath = `${file.meta.webDavBasePath}${rootFolder}` + const rootFolder = relativeFolder.split('/').slice(0, 2).join('/') + const rootFolderPath = `${file.meta.webDavBasePath}/${rootFolder}` - if (!directory || fetchedFolders.includes(rootFolderPath)) { + if (fetchedFolders.includes(rootFolderPath)) { continue } let resource if (unref(isPublicLocation)) { - const rootFolder = directory.split('/').slice(0, 2).join('/') resource = await client.publicFiles.getFileInfo( `${file.meta.currentFolder}${rootFolder}`, unref(publicLinkPassword), @@ -106,6 +105,7 @@ const updateStoreForCreatedFolders = ({ } else { resource = await client.files.fileInfo(rootFolderPath, DavProperties.Default) } + resource = buildResource(resource) store.commit('Files/UPSERT_RESOURCE', resource) fetchedFolders.push(rootFolderPath) From 99aba33b9117d2a59720057c804b17f7cab0c77f Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Tue, 26 Apr 2022 09:07:40 +0200 Subject: [PATCH 17/23] Ensure that uppy uses only one upload plugin at a time --- packages/web-runtime/src/services/uppyService.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/web-runtime/src/services/uppyService.ts b/packages/web-runtime/src/services/uppyService.ts index f3326b8af0a..14288d2ff97 100644 --- a/packages/web-runtime/src/services/uppyService.ts +++ b/packages/web-runtime/src/services/uppyService.ts @@ -43,6 +43,11 @@ export class UppyService extends Vue { retryDelays: [0, 3000, 5000, 10000, 20000] } + const xhrPlugin = this.uppy.getPlugin('XHRUpload') + if (xhrPlugin) { + this.uppy.removePlugin(xhrPlugin) + } + const tusPlugin = this.uppy.getPlugin('Tus') if (tusPlugin) { tusPlugin.setOptions(tusPluginOptions) @@ -63,9 +68,14 @@ export class UppyService extends Vue { } } - const tusPlugin = this.uppy.getPlugin('XHRUpload') + const tusPlugin = this.uppy.getPlugin('Tus') if (tusPlugin) { - tusPlugin.setOptions(xhrPluginOptions) + this.uppy.removePlugin(tusPlugin) + } + + const xhrPlugin = this.uppy.getPlugin('XHRUpload') + if (xhrPlugin) { + xhrPlugin.setOptions(xhrPluginOptions) return } From 495aa91353d0db93acefdbab9517b0ebd364ac01 Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Tue, 26 Apr 2022 10:02:00 +0200 Subject: [PATCH 18/23] Fix issue when updating the store for newly created folders --- .../web-app-files/src/composables/upload/useUploadHelpers.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/web-app-files/src/composables/upload/useUploadHelpers.ts b/packages/web-app-files/src/composables/upload/useUploadHelpers.ts index 47a04ee1654..5a248e7802b 100644 --- a/packages/web-app-files/src/composables/upload/useUploadHelpers.ts +++ b/packages/web-app-files/src/composables/upload/useUploadHelpers.ts @@ -86,6 +86,10 @@ const updateStoreForCreatedFolders = ({ const { owncloudSdk: client } = clientService const fetchedFolders = [] for (const file of files) { + if (!file.meta.relativeFolder) { + continue + } + const relativeFolder = `/${file.meta.relativeFolder.replace(/^\/+/, '')}` // Only care about the root folders, no need to fetch nested folders const rootFolder = relativeFolder.split('/').slice(0, 2).join('/') From ae25b614a297648a0541f751766c3b2a3e788563 Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Wed, 27 Apr 2022 10:29:59 +0200 Subject: [PATCH 19/23] Remove Uppy debug state --- packages/web-runtime/src/services/uppyService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/web-runtime/src/services/uppyService.ts b/packages/web-runtime/src/services/uppyService.ts index 14288d2ff97..785075e7345 100644 --- a/packages/web-runtime/src/services/uppyService.ts +++ b/packages/web-runtime/src/services/uppyService.ts @@ -13,7 +13,6 @@ export class UppyService extends Vue { constructor() { super() this.uppy = new Uppy({ - debug: true, // @TODO autoProceed: true }) this.setUpEvents() From f887105e10f4c002235be867ed3707eafec583e5 Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Wed, 27 Apr 2022 13:11:00 +0200 Subject: [PATCH 20/23] Small fixes and polishing --- packages/web-app-files/src/App.vue | 3 +-- .../components/AppBar/CreateAndUpload.spec.js | 3 +-- .../web-runtime/src/components/UploadInfo.vue | 25 +++++++++++-------- .../src/composables/upload/useUpload.ts | 3 ++- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/web-app-files/src/App.vue b/packages/web-app-files/src/App.vue index efec0633844..b9e1d445c57 100644 --- a/packages/web-app-files/src/App.vue +++ b/packages/web-app-files/src/App.vue @@ -20,7 +20,7 @@ - diff --git a/packages/web-runtime/src/composables/upload/useUpload.ts b/packages/web-runtime/src/composables/upload/useUpload.ts index 63ed41ce7c1..b425717a652 100644 --- a/packages/web-runtime/src/composables/upload/useUpload.ts +++ b/packages/web-runtime/src/composables/upload/useUpload.ts @@ -39,6 +39,7 @@ export function useUpload(options: UploadOptions): UploadResult { const store = useStore() const publicLinkPassword = computed((): string => store.getters['Files/publicLinkPassword']) const isPublicLocation = useActiveLocation(isLocationPublicActive, 'files-public-files') + const isPublicDropLocation = useActiveLocation(isLocationPublicActive, 'files-public-drop') const clientService = useClientService() const getToken = computed((): string => store.getters.getToken) @@ -47,9 +48,8 @@ export function useUpload(options: UploadOptions): UploadResult { const uploadChunkSize = computed((): number => store.getters.configuration.uploadChunkSize) const headers = computed((): { [key: string]: string } => { - if (unref(isPublicLocation)) { + if (unref(isPublicLocation) || unref(isPublicDropLocation)) { const password = unref(publicLinkPassword) - if (password) { return { Authorization: 'Basic ' + Buffer.from('public:' + password).toString('base64') } } diff --git a/packages/web-runtime/src/services/uppyService.ts b/packages/web-runtime/src/services/uppyService.ts index 785075e7345..74e2f342571 100644 --- a/packages/web-runtime/src/services/uppyService.ts +++ b/packages/web-runtime/src/services/uppyService.ts @@ -39,7 +39,7 @@ export class UppyService extends Vue { chunkSize: chunkSize, removeFingerprintOnSuccess: true, overridePatchMethod: !!tusHttpMethodOverride, - retryDelays: [0, 3000, 5000, 10000, 20000] + retryDelays: [0] } const xhrPlugin = this.uppy.getPlugin('XHRUpload') @@ -177,7 +177,9 @@ export class UppyService extends Vue { this.$emit('uploadSuccess', file) this.uppy.removeFile(file.id) }) - + result.failed.forEach((file) => { + this.$emit('uploadError', file) + }) this.uploadInputs.forEach((item) => { item.value = null }) @@ -200,9 +202,6 @@ export class UppyService extends Vue { }) } }) - this.uppy.on('upload-error', () => { - this.$emit('uploadError') - }) } registerUploadInput(el: HTMLInputElement) { diff --git a/tests/e2e/filesForUpload/Folder,With,Comma/sunday,monday.txt b/tests/e2e/filesForUpload/Folder,With,Comma/sunday,monday.txt index e69de29bb2d..7305cfdd379 100644 --- a/tests/e2e/filesForUpload/Folder,With,Comma/sunday,monday.txt +++ b/tests/e2e/filesForUpload/Folder,With,Comma/sunday,monday.txt @@ -0,0 +1 @@ +sunday,monday From e38c3a2bc2d28eaccbc87d35b6238cc588637434 Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Wed, 27 Apr 2022 16:19:19 +0200 Subject: [PATCH 22/23] Undo change in sunday,monday.txt file --- tests/e2e/filesForUpload/Folder,With,Comma/sunday,monday.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/e2e/filesForUpload/Folder,With,Comma/sunday,monday.txt b/tests/e2e/filesForUpload/Folder,With,Comma/sunday,monday.txt index 7305cfdd379..e69de29bb2d 100644 --- a/tests/e2e/filesForUpload/Folder,With,Comma/sunday,monday.txt +++ b/tests/e2e/filesForUpload/Folder,With,Comma/sunday,monday.txt @@ -1 +0,0 @@ -sunday,monday From 9198b0751d23f173fb412f9ce4a3d0bc8e00251d Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Wed, 27 Apr 2022 16:22:58 +0200 Subject: [PATCH 23/23] Bump owncloud-test-middleware docker image version to 1.5.0 --- .drone.star | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.star b/.drone.star index 2a54cdf6252..c343d151c5a 100644 --- a/.drone.star +++ b/.drone.star @@ -20,7 +20,7 @@ OC_CI_HUGO = "owncloudci/hugo:0.89.4" OC_CI_NODEJS = "owncloudci/nodejs:14" OC_CI_PHP = "owncloudci/php:7.4" OC_CI_WAIT_FOR = "owncloudci/wait-for:latest" -OC_TESTING_MIDDLEWARE = "owncloud/owncloud-test-middleware:1.4.2" +OC_TESTING_MIDDLEWARE = "owncloud/owncloud-test-middleware:1.5.0" OC_UBUNTU = "owncloud/ubuntu:20.04" PLUGINS_DOCKER = "plugins/docker:18.09" PLUGINS_DOWNSTREAM = "plugins/downstream"