diff --git a/app/assets/javascripts/discourse/app/components/composer-editor-uppy.js b/app/assets/javascripts/discourse/app/components/composer-editor-uppy.js deleted file mode 100644 index 1e230a37814..00000000000 --- a/app/assets/javascripts/discourse/app/components/composer-editor-uppy.js +++ /dev/null @@ -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", -}); diff --git a/app/assets/javascripts/discourse/app/components/composer-editor.js b/app/assets/javascripts/discourse/app/components/composer-editor.js index a5287c8eef4..80b94812074 100644 --- a/app/assets/javascripts/discourse/app/components/composer-editor.js +++ b/app/assets/javascripts/discourse/app/components/composer-editor.js @@ -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, diff --git a/app/assets/javascripts/discourse/app/controllers/composer.js b/app/assets/javascripts/discourse/app/controllers/composer.js index 016ed8d3bdf..9d85a476ba0 100644 --- a/app/assets/javascripts/discourse/app/controllers/composer.js +++ b/app/assets/javascripts/discourse/app/controllers/composer.js @@ -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; diff --git a/app/assets/javascripts/discourse/app/initializers/register-media-optimization-upload-processor.js b/app/assets/javascripts/discourse/app/initializers/register-media-optimization-upload-processor.js index 7b6bfbff653..3f1bfed382e 100644 --- a/app/assets/javascripts/discourse/app/initializers/register-media-optimization-upload-processor.js +++ b/app/assets/javascripts/discourse/app/initializers/register-media-optimization-upload-processor.js @@ -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, + }; + } + ); } }, }; diff --git a/app/assets/javascripts/discourse/app/lib/plugin-api.js b/app/assets/javascripts/discourse/app/lib/plugin-api.js index d11adef5f16..12f1b900c20 100644 --- a/app/assets/javascripts/discourse/app/lib/plugin-api.js +++ b/app/assets/javascripts/discourse/app/lib/plugin-api.js @@ -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. 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 c623a92388c..dfd8efd160f 100644 --- a/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js +++ b/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js @@ -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); + }, }); diff --git a/app/assets/javascripts/discourse/app/mixins/composer-upload.js b/app/assets/javascripts/discourse/app/mixins/composer-upload.js deleted file mode 100644 index cee453f6d02..00000000000 --- a/app/assets/javascripts/discourse/app/mixins/composer-upload.js +++ /dev/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); - }, -}); diff --git a/app/assets/javascripts/discourse/app/templates/composer.hbs b/app/assets/javascripts/discourse/app/templates/composer.hbs index 26f8f21b327..e1c50bc71b6 100644 --- a/app/assets/javascripts/discourse/app/templates/composer.hbs +++ b/app/assets/javascripts/discourse/app/templates/composer.hbs @@ -111,7 +111,7 @@ - {{component composerComponent + {{composer-editor topic=topic composer=model lastValidatedAt=lastValidatedAt diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-attachment-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-attachment-test.js deleted file mode 100644 index 37ed65aca3e..00000000000 --- a/app/assets/javascripts/discourse/tests/acceptance/composer-attachment-test.js +++ /dev/null @@ -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(), - '
' - ); - - 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(), - '' - ); - }); -}); - -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(), - '' - ); - }); -}); - -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" - ); - }); -}); 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 6671bc8a925..622fe159ede 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 @@ -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(() => { diff --git a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js index 956deeb0de7..58410713f66 100644 --- a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js +++ b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js @@ -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(); diff --git a/config/site_settings.yml b/config/site_settings.yml index d82c83144a6..f2019ac4356 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -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 diff --git a/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md b/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md new file mode 100644 index 00000000000..7ded201f976 --- /dev/null +++ b/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md @@ -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.