diff --git a/app/assets/javascripts/discourse/app/lib/text.js b/app/assets/javascripts/discourse/app/lib/text.js index 3de5338080e..544a71953e7 100644 --- a/app/assets/javascripts/discourse/app/lib/text.js +++ b/app/assets/javascripts/discourse/app/lib/text.js @@ -4,6 +4,7 @@ import { sanitize as textSanitize } from "pretty-text/sanitizer"; import deprecated from "discourse-common/lib/deprecated"; import { getURLWithCDN } from "discourse-common/lib/get-url"; import { helperContext } from "discourse-common/lib/helpers"; +import I18n from "discourse-i18n"; async function withEngine(name, ...args) { const engine = await import("discourse/static/markdown-it"); @@ -136,3 +137,18 @@ export function excerpt(cooked, length) { return result; } + +export function humanizeList(listItems) { + const items = Array.from(listItems); + const last = items.pop(); + + if (items.length === 0) { + return last; + } else { + return [ + items.join(I18n.t("word_connector.comma")), + I18n.t("word_connector.last_item"), + last, + ].join(" "); + } +} diff --git a/app/assets/javascripts/discourse/app/lib/uploads.js b/app/assets/javascripts/discourse/app/lib/uploads.js index 7f010b23be5..6e7efed8f2a 100644 --- a/app/assets/javascripts/discourse/app/lib/uploads.js +++ b/app/assets/javascripts/discourse/app/lib/uploads.js @@ -1,3 +1,4 @@ +import { humanizeList } from "discourse/lib/text"; import { isAppleDevice } from "discourse/lib/utilities"; import deprecated from "discourse-common/lib/deprecated"; import { getOwnerWithFallback } from "discourse-common/lib/get-owner"; @@ -302,6 +303,12 @@ export function getUploadMarkdown(upload) { } } +export function displayErrorForBulkUpload(errors) { + const fileNames = humanizeList(errors.mapBy("fileName")); + + dialog.alert(I18n.t("post.errors.upload", { file_name: fileNames })); +} + export function displayErrorForUpload(data, siteSettings, fileName) { if (!fileName) { deprecated( 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 0a8438b6c5b..8315d344543 100644 --- a/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js +++ b/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js @@ -11,6 +11,7 @@ import { cacheShortUploadUrl } from "pretty-text/upload-short-url"; import { updateCsrfToken } from "discourse/lib/ajax"; import { bindFileInputChangeListener, + displayErrorForBulkUpload, displayErrorForUpload, getUploadMarkdown, validateUploadedFile, @@ -99,6 +100,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { _bindUploadTarget() { this.set("inProgressUploads", []); + this.set("bufferedUploadErrors", []); this.placeholders = {}; this._preProcessorStatus = {}; this.editorEl = this.element.querySelector(this.editorClass); @@ -352,6 +354,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { this.appEvents.trigger( `${this.composerEventPrefix}:all-uploads-complete` ); + this._displayBufferedErrors(); this._reset(); } } @@ -403,11 +406,11 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { file.meta.error = error; if (!this.userCancelled) { - displayErrorForUpload(response || error, this.siteSettings, file.name); + this._bufferUploadError(response || error, file.name); this.appEvents.trigger(`${this.composerEventPrefix}:upload-error`, file); } - if (this.inProgressUploads.length === 0) { + this._displayBufferedErrors(); this._reset(); } }, @@ -419,6 +422,24 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { ); }, + _displayBufferedErrors() { + if (this.bufferedUploadErrors.length === 0) { + return; + } else if (this.bufferedUploadErrors.length === 1) { + displayErrorForUpload( + this.bufferedUploadErrors[0].data, + this.siteSettings, + this.bufferedUploadErrors[0].fileName + ); + } else { + displayErrorForBulkUpload(this.bufferedUploadErrors); + } + }, + + _bufferUploadError(data, fileName) { + this.bufferedUploadErrors.push({ data, fileName }); + }, + _setupPreProcessors() { const checksumPreProcessor = { pluginClass: UppyChecksum, @@ -561,6 +582,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { isProcessingUpload: false, isCancellable: false, inProgressUploads: [], + bufferedUploadErrors: [], }); this._resetPreProcessors(); this.fileInputEl.value = ""; 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 5b7266fa748..66e8b9c6fa7 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 @@ -530,6 +530,53 @@ acceptance("Uppy Composer Attachment - Upload Error", function (needs) { }); }); +acceptance( + "Uppy Composer Attachment - Multiple Upload Errors", + function (needs) { + needs.user(); + needs.pretender((server, helper) => { + server.post("/uploads.json", () => { + return helper.response(500, { + success: false, + }); + }); + }); + needs.settings({ + simultaneous_uploads: 2, + allow_uncategorized_topics: true, + }); + + test("should show a consolidated message for multiple failed uploads", async function (assert) { + await visit("/"); + await click("#create-topic"); + const appEvents = loggedInUser().appEvents; + const image = createFile("meme1.png"); + const image1 = createFile("meme2.png"); + const done = assert.async(); + + appEvents.on("composer:upload-error", async () => { + await settled(); + + if (!query(".dialog-body")) { + return; + } + + assert.strictEqual( + query(".dialog-body").textContent.trim(), + "Sorry, there was an error uploading meme1.png and meme2.png. Please try again.", + "it should show a consolidated error dialog" + ); + + await click(".dialog-footer .btn-primary"); + + done(); + }); + + appEvents.trigger("composer:add-files", [image, image1]); + }); + } +); + acceptance("Uppy Composer Attachment - Upload Handler", function (needs) { needs.user(); needs.pretender(pretender); diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 8403f7a606b..a2c57341b3e 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -163,6 +163,7 @@ en: word_connector: comma: ", " + last_item: "and" action_codes: public_topic: "Made this topic public %{when}"