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.
This commit is contained in:
Martin Brennan 2021-11-23 14:00:23 +10:00 committed by GitHub
parent 52532758f7
commit db4c52ca26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 96 additions and 11 deletions

View File

@ -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,

View File

@ -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: {

View File

@ -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)
);
},
});