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 8d8f7c9678c..d87ea6a46a9 100644 --- a/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js +++ b/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js @@ -339,20 +339,18 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { file.name, upload ); + + if (this.inProgressUploads.length === 0) { + this.appEvents.trigger( + `${this.composerEventPrefix}:all-uploads-complete` + ); + this._reset(); + } }); }); this._uppyInstance.on("upload-error", this._handleUploadError); - this._uppyInstance.on("complete", () => { - run(() => { - this.appEvents.trigger( - `${this.composerEventPrefix}:all-uploads-complete` - ); - this._reset(); - }); - }); - this._uppyInstance.on("cancel-all", () => { // uppyInstance.reset() also fires cancel-all, so we want to // only do the manual cancelling work if the user clicked cancel @@ -553,6 +551,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { isUploading: false, isProcessingUpload: false, isCancellable: false, + inProgressUploads: [], }); this._resetPreProcessors(); this.fileInputEl.value = ""; diff --git a/app/assets/javascripts/discourse/app/mixins/extendable-uploader.js b/app/assets/javascripts/discourse/app/mixins/extendable-uploader.js index ca2a312d3b1..8a20a5858ba 100644 --- a/app/assets/javascripts/discourse/app/mixins/extendable-uploader.js +++ b/app/assets/javascripts/discourse/app/mixins/extendable-uploader.js @@ -139,6 +139,7 @@ export default Mixin.create(UploadDebugging, { _addNeedProcessing(fileCount) { this._eachPreProcessor((pluginName, status) => { status.needProcessing += fileCount; + status.allComplete = false; }); }, @@ -167,6 +168,7 @@ export default Mixin.create(UploadDebugging, { ) { preProcessorStatus.allComplete = true; preProcessorStatus.needProcessing = 0; + preProcessorStatus.completeProcessing = 0; if (this._allPreprocessorsComplete()) { callback(true); diff --git a/app/assets/javascripts/discourse/app/mixins/uppy-upload.js b/app/assets/javascripts/discourse/app/mixins/uppy-upload.js index 9428e4aea89..11f849bf122 100644 --- a/app/assets/javascripts/discourse/app/mixins/uppy-upload.js +++ b/app/assets/javascripts/discourse/app/mixins/uppy-upload.js @@ -221,6 +221,9 @@ export default Mixin.create(UppyS3Multipart, ExtendableUploader, { ); this._triggerInProgressUploadsEvent(); + if (this.inProgressUploads.length === 0) { + this._allUploadsComplete(); + } }) .catch((errResponse) => { displayErrorForUpload(errResponse, this.siteSettings, file.name); @@ -235,7 +238,11 @@ export default Mixin.create(UppyS3Multipart, ExtendableUploader, { upload ); this.uploadDone(deepMerge(upload, { file_name: file.name })); + this._triggerInProgressUploadsEvent(); + if (this.inProgressUploads.length === 0) { + this._allUploadsComplete(); + } } }); @@ -260,17 +267,6 @@ export default Mixin.create(UppyS3Multipart, ExtendableUploader, { }); }); - this._uppyInstance.on("complete", () => { - run(() => { - if (this.isDestroying || this.isDestroyed) { - return; - } - - this.appEvents.trigger(`upload-mixin:${this.id}:all-uploads-complete`); - this._reset(); - }); - }); - // TODO (martin) preventDirectS3Uploads is necessary because some of // the current upload mixin components, for example the emoji uploader, // send the upload to custom endpoints that do fancy things in the rails @@ -485,4 +481,13 @@ export default Mixin.create(UppyS3Multipart, ExtendableUploader, { _uploadDropTargetOptions() { return { target: this.element }; }, + + _allUploadsComplete() { + if (this.isDestroying || this.isDestroyed) { + return; + } + + this.appEvents.trigger(`upload-mixin:${this.id}:all-uploads-complete`); + this._reset(); + }, }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-uploads-uppy-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-uploads-uppy-test.js index 0ff88838705..2f62ab4cad8 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/composer-uploads-uppy-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/composer-uploads-uppy-test.js @@ -13,6 +13,8 @@ import { skip, test } from "qunit"; import { Promise } from "rsvp"; import sinon from "sinon"; +let uploadNumber = 1; + function pretender(server, helper) { server.post("/uploads/lookup-urls", () => { return helper.response([ @@ -21,27 +23,53 @@ function pretender(server, helper) { short_path: "/uploads/short-url/yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg", short_url: "upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg", }, + { + url: "/images/discourse-logo-sketch-small.png", + short_path: "/uploads/short-url/sdfljsdfgjlkwg4328.jpeg", + short_url: "upload://sdfljsdfgjlkwg4328.jpeg", + }, ]); }); server.post( "/uploads.json", () => { - return helper.response({ - extension: "jpeg", - filesize: 126177, - height: 800, - human_filesize: "123 KB", - id: 202, - original_filename: "avatar.PNG.jpg", - retain_hours: null, - short_path: "/uploads/short-url/yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg", - short_url: "upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg", - thumbnail_height: 320, - thumbnail_width: 690, - url: "/images/discourse-logo-sketch-small.png", - width: 1920, - }); + let response = null; + if (uploadNumber === 1) { + response = { + extension: "jpeg", + filesize: 126177, + height: 800, + human_filesize: "123 KB", + id: 202, + original_filename: "avatar.PNG.jpg", + retain_hours: null, + short_path: "/uploads/short-url/yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg", + short_url: "upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg", + thumbnail_height: 320, + thumbnail_width: 690, + url: "/images/discourse-logo-sketch-small.png", + width: 1920, + }; + uploadNumber += 1; + } else { + response = { + extension: "jpeg", + filesize: 4322, + height: 800, + human_filesize: "566 KB", + id: 202, + original_filename: "avatar2.PNG.jpg", + retain_hours: null, + short_path: "/uploads/short-url/sdfljsdfgjlkwg4328.jpeg", + short_url: "upload://sdfljsdfgjlkwg4328.jpeg", + thumbnail_height: 320, + thumbnail_width: 690, + url: "/images/discourse-logo-sketch-small.png", + width: 1920, + }; + } + return helper.response(response); }, 500 // this delay is important to slow down the uploads a bit so we can click elements in the UI like the cancel button ); @@ -54,6 +82,9 @@ acceptance("Uppy Composer Attachment - Upload Placeholder", function (needs) { simultaneous_uploads: 2, enable_rich_text_paste: true, }); + needs.hooks.afterEach(() => { + uploadNumber = 1; + }); test("should insert the Uploading placeholder then the complete image placeholder", async function (assert) { await visit("/"); @@ -62,7 +93,8 @@ acceptance("Uppy Composer Attachment - Upload Placeholder", function (needs) { const appEvents = loggedInUser().appEvents; const done = assert.async(); - appEvents.on("composer:all-uploads-complete", () => { + appEvents.on("composer:all-uploads-complete", async () => { + await settled(); assert.strictEqual( query(".d-editor-input").value, "The image:\n![avatar.PNG|690x320](upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg)\n" @@ -81,6 +113,35 @@ acceptance("Uppy Composer Attachment - Upload Placeholder", function (needs) { appEvents.trigger("composer:add-files", image); }); + test("should handle adding one file for upload then adding another when the first is still in progress", async function (assert) { + await visit("/"); + await click("#create-topic"); + await fillIn(".d-editor-input", "The image:\n"); + const appEvents = loggedInUser().appEvents; + const done = assert.async(); + + appEvents.on("composer:all-uploads-complete", async () => { + await settled(); + assert.strictEqual( + query(".d-editor-input").value, + "The image:\n![avatar.PNG|690x320](upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg)\n![avatar2.PNG|690x320](upload://sdfljsdfgjlkwg4328.jpeg)\n" + ); + done(); + }); + + let image2Added = false; + appEvents.on("composer:upload-started", () => { + if (!image2Added) { + appEvents.trigger("composer:add-files", image2); + image2Added = true; + } + }); + + const image1 = createFile("avatar.png"); + const image2 = createFile("avatar2.png"); + appEvents.trigger("composer:add-files", image1); + }); + test("should handle placeholders correctly even if the OS rewrites ellipses", async function (assert) { const execCommand = document.execCommand; sinon.stub(document, "execCommand").callsFake(function (...args) { @@ -96,7 +157,8 @@ acceptance("Uppy Composer Attachment - Upload Placeholder", function (needs) { const appEvents = loggedInUser().appEvents; const done = assert.async(); - appEvents.on("composer:all-uploads-complete", () => { + appEvents.on("composer:all-uploads-complete", async () => { + await settled(); assert.strictEqual( query(".d-editor-input").value, "The image:\n![avatar.PNG|690x320](upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg)\n" @@ -221,7 +283,8 @@ acceptance("Uppy Composer Attachment - Upload Placeholder", function (needs) { ); }); - appEvents.on("composer:all-uploads-complete", () => { + appEvents.on("composer:all-uploads-complete", async () => { + await settled(); assert.strictEqual( query(".d-editor-input").value, "The image:\n![avatar.PNG|690x320](upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg)\n" @@ -251,7 +314,8 @@ acceptance("Uppy Composer Attachment - Upload Placeholder", function (needs) { ); }); - appEvents.on("composer:all-uploads-complete", () => { + appEvents.on("composer:all-uploads-complete", async () => { + await settled(); assert.strictEqual( query(".d-editor-input").value, "The image:\n![avatar.PNG|690x320](upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg)\n Text after the image." @@ -284,7 +348,8 @@ acceptance("Uppy Composer Attachment - Upload Placeholder", function (needs) { ); }); - appEvents.on("composer:all-uploads-complete", () => { + appEvents.on("composer:all-uploads-complete", async () => { + await settled(); assert.strictEqual( query(".d-editor-input").value, "The image:\n![avatar.PNG|690x320](upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg)\n Text after the image." @@ -309,7 +374,8 @@ acceptance("Uppy Composer Attachment - Upload Placeholder", function (needs) { ); }); - appEvents.on("composer:all-uploads-complete", () => { + appEvents.on("composer:all-uploads-complete", async () => { + await settled(); assert.strictEqual( query(".d-editor-input").value, "![avatar.PNG|690x320](upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg)\n" @@ -335,7 +401,8 @@ acceptance("Uppy Composer Attachment - Upload Placeholder", function (needs) { ); }); - appEvents.on("composer:all-uploads-complete", () => { + appEvents.on("composer:all-uploads-complete", async () => { + await settled(); assert.strictEqual( query(".d-editor-input").value, "The image:\n![avatar.PNG|690x320](upload://yoj8pf9DdIeHRRULyw7i57GAYdz.jpeg)\n"