FEATURE: Buffer file names of failed uploads when bulk uploading (#25068)

Currently, when bulk uploading and multiple uploads fail, we show a number of dialogs in quick succession. This is of course a terrible user experience.

With this change, we buffer the error messages until there are no more pending uploads. Then we combine the buffered errors and display a single dialog with a list of failed files.
This commit is contained in:
Ted Johansson 2024-01-03 10:29:23 +08:00 committed by GitHub
parent b4a89ea610
commit a0fbce996a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 95 additions and 2 deletions

View File

@ -4,6 +4,7 @@ import { sanitize as textSanitize } from "pretty-text/sanitizer";
import deprecated from "discourse-common/lib/deprecated"; import deprecated from "discourse-common/lib/deprecated";
import { getURLWithCDN } from "discourse-common/lib/get-url"; import { getURLWithCDN } from "discourse-common/lib/get-url";
import { helperContext } from "discourse-common/lib/helpers"; import { helperContext } from "discourse-common/lib/helpers";
import I18n from "discourse-i18n";
async function withEngine(name, ...args) { async function withEngine(name, ...args) {
const engine = await import("discourse/static/markdown-it"); const engine = await import("discourse/static/markdown-it");
@ -136,3 +137,18 @@ export function excerpt(cooked, length) {
return result; 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(" ");
}
}

View File

@ -1,3 +1,4 @@
import { humanizeList } from "discourse/lib/text";
import { isAppleDevice } from "discourse/lib/utilities"; import { isAppleDevice } from "discourse/lib/utilities";
import deprecated from "discourse-common/lib/deprecated"; import deprecated from "discourse-common/lib/deprecated";
import { getOwnerWithFallback } from "discourse-common/lib/get-owner"; 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) { export function displayErrorForUpload(data, siteSettings, fileName) {
if (!fileName) { if (!fileName) {
deprecated( deprecated(

View File

@ -11,6 +11,7 @@ import { cacheShortUploadUrl } from "pretty-text/upload-short-url";
import { updateCsrfToken } from "discourse/lib/ajax"; import { updateCsrfToken } from "discourse/lib/ajax";
import { import {
bindFileInputChangeListener, bindFileInputChangeListener,
displayErrorForBulkUpload,
displayErrorForUpload, displayErrorForUpload,
getUploadMarkdown, getUploadMarkdown,
validateUploadedFile, validateUploadedFile,
@ -99,6 +100,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
_bindUploadTarget() { _bindUploadTarget() {
this.set("inProgressUploads", []); this.set("inProgressUploads", []);
this.set("bufferedUploadErrors", []);
this.placeholders = {}; this.placeholders = {};
this._preProcessorStatus = {}; this._preProcessorStatus = {};
this.editorEl = this.element.querySelector(this.editorClass); this.editorEl = this.element.querySelector(this.editorClass);
@ -352,6 +354,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
this.appEvents.trigger( this.appEvents.trigger(
`${this.composerEventPrefix}:all-uploads-complete` `${this.composerEventPrefix}:all-uploads-complete`
); );
this._displayBufferedErrors();
this._reset(); this._reset();
} }
} }
@ -403,11 +406,11 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
file.meta.error = error; file.meta.error = error;
if (!this.userCancelled) { if (!this.userCancelled) {
displayErrorForUpload(response || error, this.siteSettings, file.name); this._bufferUploadError(response || error, file.name);
this.appEvents.trigger(`${this.composerEventPrefix}:upload-error`, file); this.appEvents.trigger(`${this.composerEventPrefix}:upload-error`, file);
} }
if (this.inProgressUploads.length === 0) { if (this.inProgressUploads.length === 0) {
this._displayBufferedErrors();
this._reset(); 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() { _setupPreProcessors() {
const checksumPreProcessor = { const checksumPreProcessor = {
pluginClass: UppyChecksum, pluginClass: UppyChecksum,
@ -561,6 +582,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
isProcessingUpload: false, isProcessingUpload: false,
isCancellable: false, isCancellable: false,
inProgressUploads: [], inProgressUploads: [],
bufferedUploadErrors: [],
}); });
this._resetPreProcessors(); this._resetPreProcessors();
this.fileInputEl.value = ""; this.fileInputEl.value = "";

View File

@ -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) { acceptance("Uppy Composer Attachment - Upload Handler", function (needs) {
needs.user(); needs.user();
needs.pretender(pretender); needs.pretender(pretender);

View File

@ -163,6 +163,7 @@ en:
word_connector: word_connector:
comma: ", " comma: ", "
last_item: "and"
action_codes: action_codes:
public_topic: "Made this topic public %{when}" public_topic: "Made this topic public %{when}"