From db4c52ca26ce7e82fb28998c787460623d005229 Mon Sep 17 00:00:00 2001 From: Martin Brennan Date: Tue, 23 Nov 2021 14:00:23 +1000 Subject: [PATCH] DEV: Add single file progress and cancel for uppy in composer (#15053) This commit adds handlers for the composer uppy mixin to allow for cancelling individual file uploads, not just all of them at once. This is also combined with better tracking of in progress uploads along with their progress percentage, for UI that needs to be able to display the progress for individual files and also cancel individual files. To use this, a cancel button in the UI should call a function like this: ```javascript cancelSingleUpload(fileId) { this.appEvents.trigger(`${this.eventPrefix}:cancel-upload`, { fileId, }); }, ``` Additionally, the `inProgressUploads` can be shown in the UI. It is an array of objects with the file name, ID, and the progress percentage. We can add more data to this if needed down the line. --- .../app/mixins/composer-upload-uppy.js | 68 +++++++++++++++++-- .../discourse/app/mixins/uppy-s3-multipart.js | 6 ++ .../discourse/app/mixins/uppy-upload.js | 33 +++++++-- 3 files changed, 96 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js b/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js index b31587a7025..316518f9dee 100644 --- a/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js +++ b/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js @@ -1,5 +1,6 @@ import Mixin from "@ember/object/mixin"; import ExtendableUploader from "discourse/mixins/extendable-uploader"; +import EmberObject from "@ember/object"; import UppyS3Multipart from "discourse/mixins/uppy-s3-multipart"; import { deepMerge } from "discourse-common/lib/object"; import UppyChecksum from "discourse/lib/uppy-checksum-plugin"; @@ -36,6 +37,11 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { uploadRootPath: "/uploads", uploadTargetBound: false, + @bind + _cancelSingleUpload(data) { + this._uppyInstance.removeFile(data.fileId); + }, + @observes("composerModel.uploadCancelled") _cancelUpload() { if (!this.get("composerModel.uploadCancelled")) { @@ -61,6 +67,10 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { this.element.removeEventListener("paste", this.pasteEventListener); this.appEvents.off(`${this.eventPrefix}:add-files`, this._addFiles); + this.appEvents.off( + `${this.eventPrefix}:cancel-upload`, + this._cancelSingleUpload + ); this._reset(); @@ -79,13 +89,17 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { }, _bindUploadTarget() { + this.set("inProgressUploads", []); this.placeholders = {}; - this._inProgressUploads = 0; this._preProcessorStatus = {}; this.fileInputEl = document.getElementById(this.fileUploadElementId); const isPrivateMessage = this.get("composerModel.privateMessage"); this.appEvents.on(`${this.eventPrefix}:add-files`, this._addFiles); + this.appEvents.on( + `${this.eventPrefix}:cancel-upload`, + this._cancelSingleUpload + ); this._unbindUploadTarget(); this.fileInputEventListener = bindFileInputChangeListener( @@ -181,6 +195,37 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { this.set("uploadProgress", progress); }); + this._uppyInstance.on("file-removed", (file, reason) => { + file.meta.cancelled = true; + + // we handle the cancel-all event specifically, so no need + // to do anything here + if (reason === "cancel-all") { + return; + } + + this._removeInProgressUpload(file.id); + this._resetUpload(file, { removePlaceholder: true }); + if (this.inProgressUploads.length === 0) { + this.set("userCancelled", true); + this._uppyInstance.cancelAll(); + } + }); + + this._uppyInstance.on("upload-progress", (file, progress) => { + if (this.isDestroying || this.isDestroyed) { + return; + } + + const upload = this.inProgressUploads.find((upl) => upl.id === file.id); + if (upload) { + const percentage = Math.round( + (progress.bytesUploaded / progress.bytesTotal) * 100 + ); + upload.set("progress", percentage); + } + }); + this._uppyInstance.on("upload", (data) => { this._addNeedProcessing(data.fileIDs.length); @@ -194,7 +239,13 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { }); files.forEach((file) => { - this._inProgressUploads++; + this.inProgressUploads.push( + EmberObject.create({ + fileName: file.name, + id: file.id, + progress: 0, + }) + ); const placeholder = this._uploadPlaceholder(file); this.placeholders[file.id] = { uploadPlaceholder: placeholder, @@ -205,7 +256,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { }); this._uppyInstance.on("upload-success", (file, response) => { - this._inProgressUploads--; + this._removeInProgressUpload(file.id); let upload = response.body; const markdown = this.uploadMarkdownResolvers.reduce( (md, resolver) => resolver(upload) || md, @@ -262,7 +313,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { @bind _handleUploadError(file, error, response) { - this._inProgressUploads--; + this._removeInProgressUpload(file.id); this._resetUpload(file, { removePlaceholder: true }); file.meta.error = error; @@ -272,11 +323,18 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { this.appEvents.trigger(`${this.eventPrefix}:upload-error`, file); } - if (this._inProgressUploads === 0) { + if (this.inProgressUploads.length === 0) { this._reset(); } }, + _removeInProgressUpload(fileId) { + this.set( + "inProgressUploads", + this.inProgressUploads.filter((upl) => upl.id !== fileId) + ); + }, + _setupPreProcessors() { const checksumPreProcessor = { pluginClass: UppyChecksum, diff --git a/app/assets/javascripts/discourse/app/mixins/uppy-s3-multipart.js b/app/assets/javascripts/discourse/app/mixins/uppy-s3-multipart.js index 2c4d72bedd5..e2d75eb06c8 100644 --- a/app/assets/javascripts/discourse/app/mixins/uppy-s3-multipart.js +++ b/app/assets/javascripts/discourse/app/mixins/uppy-s3-multipart.js @@ -122,6 +122,10 @@ export default Mixin.create({ @bind _completeMultipartUpload(file, data) { + if (file.meta.cancelled) { + return; + } + this._uppyInstance.emit("complete-multipart", file.id); const parts = data.parts.map((part) => { return { part_number: part.PartNumber, etag: part.ETag }; @@ -159,6 +163,8 @@ export default Mixin.create({ return; } + file.meta.cancelled = true; + return ajax(getUrl(`${this.uploadRootPath}/abort-multipart.json`), { type: "POST", data: { diff --git a/app/assets/javascripts/discourse/app/mixins/uppy-upload.js b/app/assets/javascripts/discourse/app/mixins/uppy-upload.js index d2de36205cf..05c145d6450 100644 --- a/app/assets/javascripts/discourse/app/mixins/uppy-upload.js +++ b/app/assets/javascripts/discourse/app/mixins/uppy-upload.js @@ -1,4 +1,5 @@ import Mixin from "@ember/object/mixin"; +import EmberObject from "@ember/object"; import { ajax } from "discourse/lib/ajax"; import { bindFileInputChangeListener, @@ -26,7 +27,7 @@ export default Mixin.create(UppyS3Multipart, { uploadProgress: 0, _uppyInstance: null, autoStartUploads: true, - _inProgressUploads: 0, + inProgressUploads: null, id: null, uploadRootPath: "/uploads", @@ -59,6 +60,7 @@ export default Mixin.create(UppyS3Multipart, { fileInputEl: this.element.querySelector(".hidden-upload-field"), }); this.set("allowMultipleFiles", this.fileInputEl.multiple); + this.set("inProgressUploads", []); this._bindFileInputChange(); @@ -143,11 +145,22 @@ export default Mixin.create(UppyS3Multipart, { }); this._uppyInstance.on("upload", (data) => { - this._inProgressUploads += data.fileIDs.length; + const files = data.fileIDs.map((fileId) => + this._uppyInstance.getFile(fileId) + ); + files.forEach((file) => { + this.inProgressUploads.push( + EmberObject.create({ + fileName: file.name, + id: file.id, + progress: 0, + }) + ); + }); }); this._uppyInstance.on("upload-success", (file, response) => { - this._inProgressUploads--; + this._removeInProgressUpload(file.id); if (this.usingS3Uploads) { this.setProperties({ uploading: false, processing: true }); @@ -157,13 +170,13 @@ export default Mixin.create(UppyS3Multipart, { deepMerge(completeResponse, { file_name: file.name }) ); - if (this._inProgressUploads === 0) { + if (this.inProgressUploads.length === 0) { this._reset(); } }) .catch((errResponse) => { displayErrorForUpload(errResponse, this.siteSettings, file.name); - if (this._inProgressUploads === 0) { + if (this.inProgressUploads.length === 0) { this._reset(); } }); @@ -171,13 +184,14 @@ export default Mixin.create(UppyS3Multipart, { this.uploadDone( deepMerge(response?.body || {}, { file_name: file.name }) ); - if (this._inProgressUploads === 0) { + if (this.inProgressUploads.length === 0) { this._reset(); } } }); this._uppyInstance.on("upload-error", (file, error, response) => { + this._removeInProgressUpload(file.id); displayErrorForUpload(response || error, this.siteSettings, file.name); this._reset(); }); @@ -316,4 +330,11 @@ export default Mixin.create(UppyS3Multipart, { }); this.fileInputEl.value = ""; }, + + _removeInProgressUpload(fileId) { + this.set( + "inProgressUploads", + this.inProgressUploads.filter((upl) => upl.id !== fileId) + ); + }, });