mirror of
https://github.com/discourse/discourse.git
synced 2024-11-22 11:23:25 +08:00
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:
parent
433f9a4dc9
commit
f70e6c302f
|
@ -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",
|
||||
});
|
|
@ -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,
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
});
|
|
@ -111,7 +111,7 @@
|
|||
|
||||
</div>
|
||||
|
||||
{{component composerComponent
|
||||
{{composer-editor
|
||||
topic=topic
|
||||
composer=model
|
||||
lastValidatedAt=lastValidatedAt
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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(() => {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
30
docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md
Normal file
30
docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md
Normal 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.
|
Loading…
Reference in New Issue
Block a user