DEV: Switch to using uppy uploads in composer by default (#15058)

This is a big change to change over to using the uppy
upload mixin in the composer by default. This gets rid
of the temporary composer-editor-uppy component, as well
as removing the old ComposerUpload mixin and copying over
any missing functions that were not yet implemented by
ComposerUploadUppy. This has been working well on our
hosting for some time now and has led us to several
bug fixes.

This commit also deletes the old plugin API for adding
preprocessors for the uploads. The accepted method of doing
this now is via an uppy preprocessor plugin, which we have
several examples of in the core codebase.

Leaving the `enable_experimental_composer_uploader` site setting
intact for now because some plugins still rely on it, this
will be removed at a later date.

One step closer to ending the jQuery file uploader saga...
This commit is contained in:
Martin Brennan 2021-11-30 08:33:06 +10:00 committed by GitHub
parent 433f9a4dc9
commit f70e6c302f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 92 additions and 792 deletions

View File

@ -1,14 +0,0 @@
import ComposerEditor from "discourse/components/composer-editor";
import { alias } from "@ember/object/computed";
import ComposerUploadUppy from "discourse/mixins/composer-upload-uppy";
export default ComposerEditor.extend(ComposerUploadUppy, {
layoutName: "components/composer-editor",
fileUploadElementId: "file-uploader",
eventPrefix: "composer",
uploadType: "composer",
uppyId: "composer-editor-uppy",
composerModel: alias("composer"),
composerModelContentKey: "reply",
editorInputClass: ".d-editor-input",
});

View File

@ -3,6 +3,7 @@ import {
authorizesAllExtensions,
authorizesOneOrMoreImageExtensions,
} from "discourse/lib/uploads";
import { alias } from "@ember/object/computed";
import { BasePlugin } from "@uppy/core";
import { resolveAllShortUrls } from "pretty-text/upload-short-url";
import {
@ -27,7 +28,7 @@ import {
import { later, next, schedule, throttle } from "@ember/runloop";
import Component from "@ember/component";
import Composer from "discourse/models/composer";
import ComposerUpload from "discourse/mixins/composer-upload";
import ComposerUploadUppy from "discourse/mixins/composer-upload-uppy";
import EmberObject from "@ember/object";
import I18n from "I18n";
import { ajax } from "discourse/lib/ajax";
@ -71,17 +72,6 @@ export function cleanUpComposerUploadHandler() {
uploadHandlers.length = 0;
}
let uploadProcessorQueue = [];
let uploadProcessorActions = {};
export function addComposerUploadProcessor(queueItem, actionItem) {
uploadProcessorQueue.push(queueItem);
Object.assign(uploadProcessorActions, actionItem);
}
export function cleanUpComposerUploadProcessor() {
uploadProcessorQueue = [];
uploadProcessorActions = {};
}
let uploadPreProcessors = [];
export function addComposerUploadPreProcessor(pluginClass, optionsResolverFn) {
if (!(pluginClass.prototype instanceof BasePlugin)) {
@ -107,18 +97,22 @@ export function cleanUpComposerUploadMarkdownResolver() {
uploadMarkdownResolvers = [];
}
export default Component.extend(ComposerUpload, {
export default Component.extend(ComposerUploadUppy, {
classNameBindings: ["showToolbar:toolbar-visible", ":wmd-controls"],
fileUploadElementId: "file-uploader",
mobileFileUploaderId: "mobile-file-upload",
eventPrefix: "composer",
uploadType: "composer",
uppyId: "composer-editor-uppy",
composerModel: alias("composer"),
composerModelContentKey: "reply",
editorInputClass: ".d-editor-input",
shouldBuildScrollMap: true,
scrollMap: null,
processPreview: true,
uploadMarkdownResolvers,
uploadProcessorActions,
uploadProcessorQueue,
uploadPreProcessors,
uploadHandlers,

View File

@ -296,15 +296,6 @@ export default Controller.extend({
return option;
},
@discourseComputed()
composerComponent() {
const defaultComposer = "composer-editor";
if (this.siteSettings.enable_experimental_composer_uploader) {
return "composer-editor-uppy";
}
return defaultComposer;
},
@discourseComputed("model.requiredCategoryMissing", "model.replyLength")
disableTextarea(requiredCategoryMissing, replyLength) {
return requiredCategoryMissing && replyLength === 0;

View File

@ -1,7 +1,4 @@
import {
addComposerUploadPreProcessor,
addComposerUploadProcessor,
} from "discourse/components/composer-editor";
import { addComposerUploadPreProcessor } from "discourse/components/composer-editor";
import UppyMediaOptimization from "discourse/lib/uppy-media-optimization-plugin";
export default {
@ -10,30 +7,18 @@ export default {
initialize(container) {
let siteSettings = container.lookup("site-settings:main");
if (siteSettings.composer_media_optimization_image_enabled) {
if (!siteSettings.enable_experimental_composer_uploader) {
addComposerUploadProcessor(
{ action: "optimizeJPEG" },
{
optimizeJPEG: (data, opts) =>
addComposerUploadPreProcessor(
UppyMediaOptimization,
({ isMobileDevice }) => {
return {
optimizeFn: (data, opts) =>
container
.lookup("service:media-optimization-worker")
.optimizeImage(data, opts),
}
);
} else {
addComposerUploadPreProcessor(
UppyMediaOptimization,
({ isMobileDevice }) => {
return {
optimizeFn: (data, opts) =>
container
.lookup("service:media-optimization-worker")
.optimizeImage(data, opts),
runParallel: !isMobileDevice,
};
}
);
}
runParallel: !isMobileDevice,
};
}
);
}
},
};

View File

@ -2,7 +2,6 @@ import ComposerEditor, {
addComposerUploadHandler,
addComposerUploadMarkdownResolver,
addComposerUploadPreProcessor,
addComposerUploadProcessor,
} from "discourse/components/composer-editor";
import {
addButton,
@ -94,8 +93,10 @@ import { CUSTOM_USER_SEARCH_OPTIONS } from "select-kit/components/user-chooser";
import { downloadCalendar } from "discourse/lib/download-calendar";
// If you add any methods to the API ensure you bump up the version number
// based on Semantic Versioning 2.0.0.
const PLUGIN_API_VERSION = "0.14.0";
// based on Semantic Versioning 2.0.0. Please up the changelog at
// docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md whenever you change the version
// using the format described at https://keepachangelog.com/en/1.0.0/.
const PLUGIN_API_VERSION = "1.0.0";
// This helper prevents us from applying the same `modifyClass` over and over in test mode.
function canModify(klass, type, resolverName, changes) {
@ -1021,44 +1022,22 @@ class PluginApi {
}
/**
* Registers a function to handle uploads for specified file types
* Registers a function to handle uploads for specified file types.
* The normal uploading functionality will be bypassed if function returns
* a falsy value.
* This only for uploads of individual files
*
* Example:
*
* api.addComposerUploadHandler(["mp4", "mov"], (file, editor) => {
* console.log("Handling upload for", file.name);
* api.addComposerUploadHandler(["mp4", "mov"], (files, editor) => {
* files.forEach((file) => {
* console.log("Handling upload for", file.name);
* });
* })
*/
addComposerUploadHandler(extensions, method) {
addComposerUploadHandler(extensions, method);
}
/**
* Registers a pre-processor for file uploads
* See https://github.com/blueimp/jQuery-File-Upload/wiki/Options#file-processing-options
*
* Useful for transforming to-be uploaded files client-side
*
* Example:
*
* api.addComposerUploadProcessor({action: 'myFileTransformation'}, {
* myFileTransformation(data, options) {
* let p = new Promise((resolve, reject) => {
* let file = data.files[data.index];
* console.log(`Transforming ${file.name}`);
* // do work...
* resolve(data);
* });
* return p;
* });
*/
addComposerUploadProcessor(queueItem, actionItem) {
addComposerUploadProcessor(queueItem, actionItem);
}
/**
* Registers a pre-processor for file uploads in the form
* of an Uppy preprocessor plugin.

View File

@ -549,4 +549,35 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
showUploadSelector(toolbarEvent) {
this.send("showUploadSelector", toolbarEvent);
},
_bindMobileUploadButton() {
if (this.site.mobileView) {
this.mobileUploadButton = document.getElementById(
this.mobileFileUploaderId
);
this.mobileUploadButtonEventListener = () => {
document.getElementById(this.fileUploadElementId).click();
};
this.mobileUploadButton.addEventListener(
"click",
this.mobileUploadButtonEventListener,
false
);
}
},
_unbindMobileUploadButton() {
this.mobileUploadButton?.removeEventListener(
"click",
this.mobileUploadButtonEventListener
);
},
_filenamePlaceholder(data) {
return data.name.replace(/\u200B-\u200D\uFEFF]/g, "");
},
_resetUploadFilenamePlaceholder() {
this.set("uploadFilenamePlaceholder", null);
},
});

View File

@ -1,367 +0,0 @@
import Mixin from "@ember/object/mixin";
import I18n from "I18n";
import { next, run } from "@ember/runloop";
import getURL from "discourse-common/lib/get-url";
import { clipboardHelpers } from "discourse/lib/utilities";
import discourseComputed, {
observes,
on,
} from "discourse-common/utils/decorators";
import {
displayErrorForUpload,
getUploadMarkdown,
validateUploadedFiles,
} from "discourse/lib/uploads";
import { cacheShortUploadUrl } from "pretty-text/upload-short-url";
import bootbox from "bootbox";
export default Mixin.create({
_xhr: null,
uploadProgress: 0,
uploadFilenamePlaceholder: null,
uploadProcessingFilename: null,
uploadProcessingPlaceholdersAdded: false,
@discourseComputed("uploadFilenamePlaceholder")
uploadPlaceholder(uploadFilenamePlaceholder) {
const clipboard = I18n.t("clipboard");
const filename = uploadFilenamePlaceholder
? uploadFilenamePlaceholder
: clipboard;
let placeholder = `[${I18n.t("uploading_filename", { filename })}]()\n`;
if (!this._cursorIsOnEmptyLine()) {
placeholder = `\n${placeholder}`;
}
return placeholder;
},
@observes("composer.uploadCancelled")
_cancelUpload() {
if (!this.get("composer.uploadCancelled")) {
return;
}
this.set("composer.uploadCancelled", false);
if (this._xhr) {
this._xhr._userCancelled = true;
this._xhr.abort();
}
this._resetUpload(true);
},
_setUploadPlaceholderSend(data) {
const filename = this._filenamePlaceholder(data);
this.set("uploadFilenamePlaceholder", filename);
// when adding two separate files with the same filename search for matching
// placeholder already existing in the editor ie [Uploading: test.png...]
// and add order nr to the next one: [Uploading: test.png(1)...]
const escapedFilename = filename.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const regexString = `\\[${I18n.t("uploading_filename", {
filename: escapedFilename + "(?:\\()?([0-9])?(?:\\))?",
})}\\]\\(\\)`;
const globalRegex = new RegExp(regexString, "g");
const matchingPlaceholder = this.get("composer.reply").match(globalRegex);
if (matchingPlaceholder) {
// get last matching placeholder and its consecutive nr in regex
// capturing group and apply +1 to the placeholder
const lastMatch = matchingPlaceholder[matchingPlaceholder.length - 1];
const regex = new RegExp(regexString);
const orderNr = regex.exec(lastMatch)[1]
? parseInt(regex.exec(lastMatch)[1], 10) + 1
: 1;
data.orderNr = orderNr;
const filenameWithOrderNr = `${filename}(${orderNr})`;
this.set("uploadFilenamePlaceholder", filenameWithOrderNr);
}
},
_setUploadPlaceholderDone(data) {
const filename = this._filenamePlaceholder(data);
if (data.orderNr) {
const filenameWithOrderNr = `${filename}(${data.orderNr})`;
this.set("uploadFilenamePlaceholder", filenameWithOrderNr);
} else {
this.set("uploadFilenamePlaceholder", filename);
}
},
_filenamePlaceholder(data) {
if (data.files) {
return data.files[0].name.replace(/\u200B-\u200D\uFEFF]/g, "");
} else {
return data.name.replace(/\u200B-\u200D\uFEFF]/g, "");
}
},
_resetUploadFilenamePlaceholder() {
this.set("uploadFilenamePlaceholder", null);
},
_resetUpload(removePlaceholder) {
next(() => {
if (this._validUploads > 0) {
this._validUploads--;
}
if (this._validUploads === 0) {
this.setProperties({
uploadProgress: 0,
isUploading: false,
isCancellable: false,
});
}
if (removePlaceholder) {
this.appEvents.trigger(
"composer:replace-text",
this.uploadPlaceholder,
""
);
}
this._resetUploadFilenamePlaceholder();
});
},
_bindUploadTarget() {
this._unbindUploadTarget(); // in case it's still bound, let's clean it up first
this._pasted = false;
const $element = $(this.element);
this.setProperties({
uploadProgress: 0,
isUploading: false,
isProcessingUpload: false,
isCancellable: false,
});
$.blueimp.fileupload.prototype.processActions = this.uploadProcessorActions;
$element.fileupload({
url: getURL(`/uploads.json?client_id=${this.messageBus.clientId}`),
dataType: "json",
pasteZone: $element,
processQueue: this.uploadProcessorQueue,
});
$element
.on("fileuploadprocessstart", () => {
this.setProperties({
uploadProgress: 0,
isUploading: true,
isProcessingUpload: true,
isCancellable: false,
});
})
.on("fileuploadprocess", (e, data) => {
if (!this.uploadProcessingPlaceholdersAdded) {
data.originalFiles
.map((f) => f.name)
.forEach((f) => {
this.appEvents.trigger(
"composer:insert-text",
`[${I18n.t("processing_filename", {
filename: f,
})}]()\n`
);
});
this.uploadProcessingPlaceholdersAdded = true;
}
this.uploadProcessingFilename = data.files[data.index].name;
})
.on("fileuploadprocessstop", () => {
this.setProperties({
uploadProgress: 0,
isUploading: false,
isProcessingUpload: false,
isCancellable: false,
});
this.uploadProcessingPlaceholdersAdded = false;
});
$element.on("fileuploadpaste", (e) => {
this._pasted = true;
if (!$(".d-editor-input").is(":focus")) {
return;
}
const { canUpload, canPasteHtml, types } = clipboardHelpers(e, {
siteSettings: this.siteSettings,
canUpload: true,
});
if (!canUpload || canPasteHtml || types.includes("text/plain")) {
e.preventDefault();
}
});
$element.on("fileuploadsubmit", (e, data) => {
const max = this.siteSettings.simultaneous_uploads;
const fileCount = data.files.length;
// Limit the number of simultaneous uploads
if (max > 0 && fileCount > max) {
bootbox.alert(
I18n.t("post.errors.too_many_dragged_and_dropped_files", {
count: max,
})
);
return false;
}
// Look for a matching file upload handler contributed from a plugin
if (fileCount === 1) {
const file = data.files[0];
const matchingHandler = this._findMatchingUploadHandler(file.name);
if (matchingHandler && !matchingHandler.method(file, this)) {
return false;
}
}
// If no plugin, continue as normal
const isPrivateMessage = this.get("composer.privateMessage");
data.formData = { type: "composer" };
if (isPrivateMessage) {
data.formData.for_private_message = true;
}
if (this._pasted) {
data.formData.pasted = true;
}
const opts = {
user: this.currentUser,
siteSettings: this.siteSettings,
isPrivateMessage,
allowStaffToUploadAnyFileInPm: this.siteSettings
.allow_staff_to_upload_any_file_in_pm,
};
const isUploading = validateUploadedFiles(data.files, opts);
run(() => {
this.setProperties({ uploadProgress: 0, isUploading });
});
return isUploading;
});
$element.on("fileuploadprogressall", (e, data) => {
run(() => {
this.set(
"uploadProgress",
parseInt((data.loaded / data.total) * 100, 10)
);
});
});
$element.on("fileuploadsend", (e, data) => {
run(() => {
this._pasted = false;
this._validUploads++;
this._setUploadPlaceholderSend(data);
if (this.uploadProcessingFilename) {
this.appEvents.trigger(
"composer:replace-text",
`[${I18n.t("processing_filename", {
filename: this.uploadProcessingFilename,
})}]()`,
this.uploadPlaceholder.trim()
);
this.uploadProcessingFilename = null;
} else {
this.appEvents.trigger(
"composer:insert-text",
this.uploadPlaceholder
);
}
if (data.xhr && data.originalFiles.length === 1) {
this.set("isCancellable", true);
this._xhr = data.xhr();
}
});
});
$element.on("fileuploaddone", (e, data) => {
run(() => {
let upload = data.result;
this._setUploadPlaceholderDone(data);
if (!this._xhr || !this._xhr._userCancelled) {
const markdown = this.uploadMarkdownResolvers.reduce(
(md, resolver) => resolver(upload) || md,
getUploadMarkdown(upload)
);
cacheShortUploadUrl(upload.short_url, upload);
this.appEvents.trigger(
"composer:replace-text",
this.uploadPlaceholder.trim(),
markdown
);
this._resetUpload(false);
} else {
this._resetUpload(true);
}
});
});
$element.on("fileuploadfail", (e, data) => {
run(() => {
this._setUploadPlaceholderDone(data);
this._resetUpload(true);
const userCancelled = this._xhr && this._xhr._userCancelled;
this._xhr = null;
if (!userCancelled) {
displayErrorForUpload(data, this.siteSettings, data.files[0].name);
}
});
});
},
_bindMobileUploadButton() {
if (this.site.mobileView) {
this.mobileUploadButton = document.getElementById(
this.mobileFileUploaderId
);
this.mobileUploadButtonEventListener = () => {
document.getElementById(this.fileUploadElementId).click();
};
this.mobileUploadButton.addEventListener(
"click",
this.mobileUploadButtonEventListener,
false
);
}
},
_unbindMobileUploadButton() {
this.mobileUploadButton?.removeEventListener(
"click",
this.mobileUploadButtonEventListener
);
},
@on("willDestroyElement")
_unbindUploadTarget() {
this._validUploads = 0;
const $uploadTarget = $(this.element);
try {
$uploadTarget.fileupload("destroy");
} catch (e) {
/* wasn't initialized yet */
}
$uploadTarget.off();
},
showUploadSelector(toolbarEvent) {
this.send("showUploadSelector", toolbarEvent);
},
});

View File

@ -111,7 +111,7 @@
</div>
{{component composerComponent
{{composer-editor
topic=topic
composer=model
lastValidatedAt=lastValidatedAt

View File

@ -1,326 +0,0 @@
import {
acceptance,
exists,
query,
queryAll,
} from "discourse/tests/helpers/qunit-helpers";
import { withPluginApi } from "discourse/lib/plugin-api";
import { click, fillIn, visit } from "@ember/test-helpers";
import bootbox from "bootbox";
import { test } from "qunit";
function pretender(server, helper) {
server.post("/uploads/lookup-urls", () => {
return helper.response([
{
short_url: "upload://asdsad.png",
url: "/secure-media-uploads/default/3X/1/asjdiasjdiasida.png",
short_path: "/uploads/short-url/asdsad.png",
},
]);
});
}
async function writeInComposer(assert) {
await visit("/t/internationalization-localization/280");
await click("#topic-footer-buttons .btn.create");
await fillIn(".d-editor-input", "[test](upload://abcdefg.png)");
assert.strictEqual(
queryAll(".d-editor-preview:visible").html().trim(),
'<p><a href="/404" tabindex="-1">test</a></p>'
);
await fillIn(".d-editor-input", "[test|attachment](upload://asdsad.png)");
}
acceptance("Composer Attachment - Cooking", function (needs) {
needs.user();
needs.pretender(pretender);
test("attachments are cooked properly", async function (assert) {
await writeInComposer(assert);
assert.strictEqual(
queryAll(".d-editor-preview:visible").html().trim(),
'<p><a class="attachment" href="/uploads/short-url/asdsad.png" tabindex="-1">test</a></p>'
);
});
});
acceptance("Composer Attachment - Secure Media Enabled", function (needs) {
needs.user();
needs.settings({ secure_media: true });
needs.pretender(pretender);
test("attachments are cooked properly when secure media is enabled", async function (assert) {
await writeInComposer(assert);
assert.strictEqual(
queryAll(".d-editor-preview:visible").html().trim(),
'<p><a class="attachment" href="/secure-media-uploads/default/3X/1/asjdiasjdiasida.png" tabindex="-1">test</a></p>'
);
});
});
acceptance("Composer Attachment - Upload Placeholder", function (needs) {
needs.user();
test("should insert a newline before and after an image when pasting into an empty composer", async function (assert) {
await visit("/");
await click("#create-topic");
const image = createImage("avatar.png", "/images/avatar.png?1", 200, 300);
await queryAll(".wmd-controls").trigger("fileuploadsend", image);
assert.strictEqual(
queryAll(".d-editor-input").val(),
"[Uploading: avatar.png...]()\n"
);
await queryAll(".wmd-controls").trigger("fileuploaddone", image);
assert.strictEqual(
queryAll(".d-editor-input").val(),
"![avatar|200x300](/images/avatar.png?1)\n"
);
});
test("should insert a newline after an image when pasting into a blank line", async function (assert) {
await visit("/");
await click("#create-topic");
await fillIn(".d-editor-input", "The image:\n");
const image = createImage("avatar.png", "/images/avatar.png?1", 200, 300);
await queryAll(".wmd-controls").trigger("fileuploadsend", image);
assert.strictEqual(
queryAll(".d-editor-input").val(),
"The image:\n[Uploading: avatar.png...]()\n"
);
await queryAll(".wmd-controls").trigger("fileuploaddone", image);
assert.strictEqual(
queryAll(".d-editor-input").val(),
"The image:\n![avatar|200x300](/images/avatar.png?1)\n"
);
});
test("should insert a newline before and after an image when pasting into a non blank line", async function (assert) {
await visit("/");
await click("#create-topic");
await fillIn(".d-editor-input", "The image:");
const image = createImage("avatar.png", "/images/avatar.png?1", 200, 300);
await queryAll(".wmd-controls").trigger("fileuploadsend", image);
assert.strictEqual(
queryAll(".d-editor-input").val(),
"The image:\n[Uploading: avatar.png...]()\n"
);
await queryAll(".wmd-controls").trigger("fileuploaddone", image);
assert.strictEqual(
queryAll(".d-editor-input").val(),
"The image:\n![avatar|200x300](/images/avatar.png?1)\n"
);
});
test("should insert a newline before and after an image when pasting with cursor in the middle of the line", async function (assert) {
await visit("/");
await click("#create-topic");
await fillIn(".d-editor-input", "The image Text after the image.");
const textArea = query(".d-editor-input");
textArea.selectionStart = 10;
textArea.selectionEnd = 10;
const image = createImage("avatar.png", "/images/avatar.png?1", 200, 300);
await queryAll(".wmd-controls").trigger("fileuploadsend", image);
assert.strictEqual(
queryAll(".d-editor-input").val(),
"The image \n[Uploading: avatar.png...]()\nText after the image."
);
await queryAll(".wmd-controls").trigger("fileuploaddone", image);
assert.strictEqual(
queryAll(".d-editor-input").val(),
"The image \n![avatar|200x300](/images/avatar.png?1)\nText after the image."
);
});
test("should insert a newline before and after an image when pasting with text selected", async function (assert) {
await visit("/");
await click("#create-topic");
const image = createImage("avatar.png", "/images/avatar.png?1", 200, 300);
await fillIn(
".d-editor-input",
"The image [paste here] Text after the image."
);
const textArea = query(".d-editor-input");
textArea.selectionStart = 10;
textArea.selectionEnd = 23;
await queryAll(".wmd-controls").trigger("fileuploadsend", image);
assert.strictEqual(
queryAll(".d-editor-input").val(),
"The image \n[Uploading: avatar.png...]()\n Text after the image."
);
await queryAll(".wmd-controls").trigger("fileuploaddone", image);
assert.strictEqual(
queryAll(".d-editor-input").val(),
"The image \n![avatar|200x300](/images/avatar.png?1)\n Text after the image."
);
});
test("pasting several images", async function (assert) {
await visit("/");
await click("#create-topic");
const image1 = createImage("test.png", "/images/avatar.png?1", 200, 300);
const image2 = createImage("test.png", "/images/avatar.png?2", 100, 200);
const image3 = createImage("image.png", "/images/avatar.png?3", 300, 400);
const image4 = createImage("image.png", "/images/avatar.png?4", 300, 400);
await queryAll(".wmd-controls").trigger("fileuploadsend", image1);
assert.strictEqual(
queryAll(".d-editor-input").val(),
"[Uploading: test.png...]()\n"
);
await queryAll(".wmd-controls").trigger("fileuploadsend", image2);
assert.strictEqual(
queryAll(".d-editor-input").val(),
"[Uploading: test.png...]()\n[Uploading: test.png(1)...]()\n"
);
await queryAll(".wmd-controls").trigger("fileuploadsend", image4);
assert.strictEqual(
queryAll(".d-editor-input").val(),
"[Uploading: test.png...]()\n[Uploading: test.png(1)...]()\n[Uploading: image.png...]()\n"
);
await queryAll(".wmd-controls").trigger("fileuploadsend", image3);
assert.strictEqual(
queryAll(".d-editor-input").val(),
"[Uploading: test.png...]()\n[Uploading: test.png(1)...]()\n[Uploading: image.png...]()\n[Uploading: image.png(1)...]()\n"
);
await queryAll(".wmd-controls").trigger("fileuploaddone", image2);
assert.strictEqual(
queryAll(".d-editor-input").val(),
"[Uploading: test.png...]()\n![test|100x200](/images/avatar.png?2)\n[Uploading: image.png...]()\n[Uploading: image.png(1)...]()\n"
);
await queryAll(".wmd-controls").trigger("fileuploaddone", image3);
assert.strictEqual(
queryAll(".d-editor-input").val(),
"[Uploading: test.png...]()\n![test|100x200](/images/avatar.png?2)\n[Uploading: image.png...]()\n![image|300x400](/images/avatar.png?3)\n"
);
await queryAll(".wmd-controls").trigger("fileuploaddone", image1);
assert.strictEqual(
queryAll(".d-editor-input").val(),
"![test|200x300](/images/avatar.png?1)\n![test|100x200](/images/avatar.png?2)\n[Uploading: image.png...]()\n![image|300x400](/images/avatar.png?3)\n"
);
});
test("should accept files with unescaped characters", async function (assert) {
await visit("/");
await click("#create-topic");
const image = createImage("ima++ge.png", "/images/avatar.png?4", 300, 400);
await queryAll(".wmd-controls").trigger("fileuploadsend", image);
assert.strictEqual(
queryAll(".d-editor-input").val(),
"[Uploading: ima++ge.png...]()\n"
);
await queryAll(".wmd-controls").trigger("fileuploaddone", image);
assert.strictEqual(
queryAll(".d-editor-input").val(),
"![ima++ge|300x400](/images/avatar.png?4)\n"
);
});
});
function createImage(name, url, width, height) {
const file = new Blob([""], { type: "image/png" });
file.name = name;
return {
files: [file],
result: {
original_filename: name,
thumbnail_width: width,
thumbnail_height: height,
url,
},
};
}
acceptance("Composer Attachment - Upload Handler", function (needs) {
needs.user();
needs.hooks.beforeEach(() => {
withPluginApi("0.8.14", (api) => {
api.addComposerUploadHandler(["png"], (file) => {
bootbox.alert(`This is an upload handler test for ${file.name}`);
});
});
});
test("should handle a single file being uploaded with the extension handler", async function (assert) {
await visit("/");
await click("#create-topic");
const image = createImage(
"handlertest.png",
"/images/avatar.png?1",
200,
300
);
await fillIn(".d-editor-input", "This is a handler test.");
await queryAll(".wmd-controls").trigger("fileuploadsubmit", image);
assert.strictEqual(
queryAll(".bootbox .modal-body").html(),
"This is an upload handler test for handlertest.png",
"it should show the bootbox triggered by the upload handler"
);
await click(".modal-footer .btn");
});
});
acceptance("Composer Attachment - File input", function (needs) {
needs.user();
test("shouldn't add to DOM the hidden file input if uploads aren't allowed", async function (assert) {
this.siteSettings.authorized_extensions = "";
await visit("/");
await click("#create-topic");
assert.notOk(exists("input#file-uploader"));
});
test("should fill the accept attribute with allowed file extensions", async function (assert) {
this.siteSettings.authorized_extensions = "jpg|jpeg|png";
await visit("/");
await click("#create-topic");
assert.ok(exists("input#file-uploader"), "An input is rendered");
assert.strictEqual(
query("input#file-uploader").accept,
".jpg,.jpeg,.png",
"Accepted values are correct"
);
});
test("the hidden file input shouldn't have the accept attribute if any file extension is allowed", async function (assert) {
this.siteSettings.authorized_extensions = "jpg|jpeg|png|*";
await visit("/");
await click("#create-topic");
assert.ok(exists("input#file-uploader"), "An input is rendered");
assert.notOk(
query("input#file-uploader").hasAttribute("accept"),
"The input doesn't contain the accept attribute"
);
});
});

View File

@ -59,7 +59,6 @@ acceptance("Uppy Composer Attachment - Upload Placeholder", function (needs) {
needs.user();
needs.pretender(pretender);
needs.settings({
enable_experimental_composer_uploader: true,
simultaneous_uploads: 2,
});
@ -197,7 +196,6 @@ acceptance("Uppy Composer Attachment - Upload Error", function (needs) {
});
});
needs.settings({
enable_experimental_composer_uploader: true,
simultaneous_uploads: 2,
});
@ -229,7 +227,6 @@ acceptance("Uppy Composer Attachment - Upload Handler", function (needs) {
needs.user();
needs.pretender(pretender);
needs.settings({
enable_experimental_composer_uploader: true,
simultaneous_uploads: 2,
});
needs.hooks.beforeEach(() => {

View File

@ -48,7 +48,6 @@ import {
cleanUpComposerUploadHandler,
cleanUpComposerUploadMarkdownResolver,
cleanUpComposerUploadPreProcessor,
cleanUpComposerUploadProcessor,
} from "discourse/components/composer-editor";
import { resetLastEditNotificationClick } from "discourse/models/post-stream";
import { clearAuthMethods } from "discourse/models/login-method";
@ -294,7 +293,6 @@ export function acceptance(name, optionsOrCallback) {
setTopicList(null);
_clearSnapshots();
cleanUpComposerUploadHandler();
cleanUpComposerUploadProcessor();
cleanUpComposerUploadMarkdownResolver();
cleanUpComposerUploadPreProcessor();
clearTopicFooterDropdowns();

View File

@ -267,6 +267,8 @@ basic:
client: true
default: true
hidden: true
# TODO (martin) (2022-02-01) Remove this setting once plugins relying on
# it have been changed.
enable_experimental_composer_uploader:
client: true
default: false

View File

@ -0,0 +1,30 @@
# Changelog
All notable changes to the Discourse JavaScript plugin API located at
app/assets/javascripts/discourse/app/lib/plugin-api.js will be described
in this file..
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.0] - 2021-11-25
### Removed
- Removes the `addComposerUploadProcessor` function, which is no longer used in
favour of `addComposerUploadPreProcessor`. The former was used to add preprocessors
for client side uploads via jQuery file uploader (described at
https://github.com/blueimp/jQuery-File-Upload/wiki/Options#file-processing-options).
The new `addComposerUploadPreProcessor` adds preprocessors for client side
uploads in the form of an Uppy plugin. See https://uppy.io/docs/writing-plugins/
for the Uppy documentation, but other examples of preprocessors in core can be found
in the UppyMediaOptimization and UppyChecksum classes. This has been done because
of the overarching move towards Uppy in the Discourse codebase rather than
jQuery fileupload, which will eventually be removed altogether as a broader effort
to remove jQuery from the codebase.
### Changed
- Changes `addComposerUploadHandler`'s behaviour. Instead of being only usable
for single files at a time, now multiple files are sent to the upload handler
at once. These multiple files are sent based on the groups in which they are
added (e.g. multiple files selected from the system upload dialog, or multiple
files dropped in to the composer). Files will be sent in buckets to the handlers
they match.