DEV: Refactor uppy component mixins into standalone classes (#28710)
This commit replaces all uppy-related mixins with standalone classes. The main entrypoint is now lib/uppy/uppy-upload.js, which has a list of its config options listed at the top of the file. Functionality & logic is completely unchanged. The uppy-upload mixin is replaced with a backwards-compatibility shim, which will allow us to migrate to the new pattern incrementally.
This commit is contained in:
parent
6a7bac7694
commit
06d32a8a89
|
@ -20,6 +20,7 @@ import {
|
||||||
import { loadOneboxes } from "discourse/lib/load-oneboxes";
|
import { loadOneboxes } from "discourse/lib/load-oneboxes";
|
||||||
import putCursorAtEnd from "discourse/lib/put-cursor-at-end";
|
import putCursorAtEnd from "discourse/lib/put-cursor-at-end";
|
||||||
import { authorizesOneOrMoreImageExtensions } from "discourse/lib/uploads";
|
import { authorizesOneOrMoreImageExtensions } from "discourse/lib/uploads";
|
||||||
|
import UppyComposerUpload from "discourse/lib/uppy/composer-upload";
|
||||||
import userSearch from "discourse/lib/user-search";
|
import userSearch from "discourse/lib/user-search";
|
||||||
import {
|
import {
|
||||||
destroyUserStatuses,
|
destroyUserStatuses,
|
||||||
|
@ -31,7 +32,6 @@ import {
|
||||||
formatUsername,
|
formatUsername,
|
||||||
inCodeBlock,
|
inCodeBlock,
|
||||||
} from "discourse/lib/utilities";
|
} from "discourse/lib/utilities";
|
||||||
import ComposerUploadUppy from "discourse/mixins/composer-upload-uppy";
|
|
||||||
import Composer from "discourse/models/composer";
|
import Composer from "discourse/models/composer";
|
||||||
import { isTesting } from "discourse-common/config/environment";
|
import { isTesting } from "discourse-common/config/environment";
|
||||||
import { tinyAvatar } from "discourse-common/lib/avatar-utils";
|
import { tinyAvatar } from "discourse-common/lib/avatar-utils";
|
||||||
|
@ -110,23 +110,11 @@ const DEBOUNCE_FETCH_MS = 450;
|
||||||
const DEBOUNCE_JIT_MS = 2000;
|
const DEBOUNCE_JIT_MS = 2000;
|
||||||
|
|
||||||
@classNameBindings("showToolbar:toolbar-visible", ":wmd-controls")
|
@classNameBindings("showToolbar:toolbar-visible", ":wmd-controls")
|
||||||
export default class ComposerEditor extends Component.extend(
|
export default class ComposerEditor extends Component {
|
||||||
ComposerUploadUppy
|
|
||||||
) {
|
|
||||||
editorClass = ".d-editor";
|
|
||||||
fileUploadElementId = "file-uploader";
|
|
||||||
mobileFileUploaderId = "mobile-file-upload";
|
|
||||||
composerEventPrefix = "composer";
|
composerEventPrefix = "composer";
|
||||||
uploadType = "composer";
|
|
||||||
uppyId = "composer-editor-uppy";
|
|
||||||
composerModelContentKey = "reply";
|
|
||||||
editorInputClass = ".d-editor-input";
|
|
||||||
shouldBuildScrollMap = true;
|
shouldBuildScrollMap = true;
|
||||||
scrollMap = null;
|
scrollMap = null;
|
||||||
processPreview = true;
|
processPreview = true;
|
||||||
uploadMarkdownResolvers = uploadMarkdownResolvers;
|
|
||||||
uploadPreProcessors = uploadPreProcessors;
|
|
||||||
uploadHandlers = uploadHandlers;
|
|
||||||
|
|
||||||
@alias("composer") composerModel;
|
@alias("composer") composerModel;
|
||||||
|
|
||||||
|
@ -134,6 +122,14 @@ export default class ComposerEditor extends Component.extend(
|
||||||
super.init(...arguments);
|
super.init(...arguments);
|
||||||
this.warnedCannotSeeMentions = [];
|
this.warnedCannotSeeMentions = [];
|
||||||
this.warnedGroupMentions = [];
|
this.warnedGroupMentions = [];
|
||||||
|
|
||||||
|
this.uppyComposerUpload = new UppyComposerUpload(getOwner(this), {
|
||||||
|
composerEventPrefix: this.composerEventPrefix,
|
||||||
|
composerModel: this.composerModel,
|
||||||
|
uploadMarkdownResolvers,
|
||||||
|
uploadPreProcessors,
|
||||||
|
uploadHandlers,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@discourseComputed("composer.requiredCategoryMissing")
|
@discourseComputed("composer.requiredCategoryMissing")
|
||||||
|
@ -261,8 +257,7 @@ export default class ComposerEditor extends Component.extend(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.allowUpload) {
|
if (this.allowUpload) {
|
||||||
this._bindUploadTarget();
|
this.uppyComposerUpload.setup(this.element);
|
||||||
this._bindMobileUploadButton();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.appEvents.trigger(`${this.composerEventPrefix}:will-open`);
|
this.appEvents.trigger(`${this.composerEventPrefix}:will-open`);
|
||||||
|
@ -840,8 +835,7 @@ export default class ComposerEditor extends Component.extend(
|
||||||
const preview = this.element.querySelector(".d-editor-preview-wrapper");
|
const preview = this.element.querySelector(".d-editor-preview-wrapper");
|
||||||
|
|
||||||
if (this.allowUpload) {
|
if (this.allowUpload) {
|
||||||
this._unbindUploadTarget();
|
this.uppyComposerUpload.teardown();
|
||||||
this._unbindMobileUploadButton();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.appEvents.trigger(`${this.composerEventPrefix}:will-close`);
|
this.appEvents.trigger(`${this.composerEventPrefix}:will-close`);
|
||||||
|
@ -907,26 +901,6 @@ export default class ComposerEditor extends Component.extend(
|
||||||
return element.tagName === "ASIDE" && element.classList.contains("quote");
|
return element.tagName === "ASIDE" && element.classList.contains("quote");
|
||||||
}
|
}
|
||||||
|
|
||||||
_cursorIsOnEmptyLine() {
|
|
||||||
const textArea = this.element.querySelector(".d-editor-input");
|
|
||||||
const selectionStart = textArea.selectionStart;
|
|
||||||
if (selectionStart === 0) {
|
|
||||||
return true;
|
|
||||||
} else if (textArea.value.charAt(selectionStart - 1) === "\n") {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_findMatchingUploadHandler(fileName) {
|
|
||||||
return this.uploadHandlers.find((handler) => {
|
|
||||||
const ext = handler.extensions.join("|");
|
|
||||||
const regex = new RegExp(`\\.(${ext})$`, "i");
|
|
||||||
return regex.test(fileName);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
extraButtons(toolbar) {
|
extraButtons(toolbar) {
|
||||||
toolbar.addButton({
|
toolbar.addButton({
|
||||||
|
|
|
@ -85,12 +85,6 @@ export default class UppyImageUploader extends Component.extend(
|
||||||
return { imagesOnly: true };
|
return { imagesOnly: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
_uppyReady() {
|
|
||||||
this._onPreProcessComplete(() => {
|
|
||||||
this.set("processing", false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadDone(upload) {
|
uploadDone(upload) {
|
||||||
this.setProperties({
|
this.setProperties({
|
||||||
imageFilesize: upload.human_filesize,
|
imageFilesize: upload.human_filesize,
|
||||||
|
@ -139,9 +133,6 @@ export default class UppyImageUploader extends Component.extend(
|
||||||
|
|
||||||
@action
|
@action
|
||||||
trash() {
|
trash() {
|
||||||
// uppy needs to be reset to allow for more uploads
|
|
||||||
this._reset();
|
|
||||||
|
|
||||||
// the value of the property used for imageUrl should be cleared
|
// the value of the property used for imageUrl should be cleared
|
||||||
// in this callback. this should be done in cases where imageUrl
|
// in this callback. this should be done in cases where imageUrl
|
||||||
// is bound to a computed property of the parent component.
|
// is bound to a computed property of the parent component.
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { warn } from "@ember/debug";
|
import { warn } from "@ember/debug";
|
||||||
import EmberObject from "@ember/object";
|
import EmberObject from "@ember/object";
|
||||||
import Mixin from "@ember/object/mixin";
|
import { getOwner, setOwner } from "@ember/owner";
|
||||||
import { getOwner } from "@ember/owner";
|
|
||||||
import { run } from "@ember/runloop";
|
import { run } from "@ember/runloop";
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
import Uppy from "@uppy/core";
|
import Uppy from "@uppy/core";
|
||||||
|
@ -16,130 +15,156 @@ import {
|
||||||
getUploadMarkdown,
|
getUploadMarkdown,
|
||||||
validateUploadedFile,
|
validateUploadedFile,
|
||||||
} from "discourse/lib/uploads";
|
} from "discourse/lib/uploads";
|
||||||
|
import UppyS3Multipart from "discourse/lib/uppy/s3-multipart";
|
||||||
|
import UppyWrapper from "discourse/lib/uppy/wrapper";
|
||||||
import UppyChecksum from "discourse/lib/uppy-checksum-plugin";
|
import UppyChecksum from "discourse/lib/uppy-checksum-plugin";
|
||||||
import { clipboardHelpers } from "discourse/lib/utilities";
|
import { clipboardHelpers } from "discourse/lib/utilities";
|
||||||
import ComposerVideoThumbnailUppy from "discourse/mixins/composer-video-thumbnail-uppy";
|
import ComposerVideoThumbnailUppy from "discourse/mixins/composer-video-thumbnail-uppy";
|
||||||
import ExtendableUploader from "discourse/mixins/extendable-uploader";
|
|
||||||
import UppyS3Multipart from "discourse/mixins/uppy-s3-multipart";
|
|
||||||
import getURL from "discourse-common/lib/get-url";
|
import getURL from "discourse-common/lib/get-url";
|
||||||
import { deepMerge } from "discourse-common/lib/object";
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
import { bind, observes, on } from "discourse-common/utils/decorators";
|
|
||||||
import escapeRegExp from "discourse-common/utils/escape-regexp";
|
import escapeRegExp from "discourse-common/utils/escape-regexp";
|
||||||
import I18n from "discourse-i18n";
|
import I18n from "discourse-i18n";
|
||||||
|
|
||||||
// Note: This mixin is used _in addition_ to the ComposerUpload mixin
|
export default class UppyComposerUpload {
|
||||||
// on the composer-editor component. It overrides some, but not all,
|
@service dialog;
|
||||||
// functions created by ComposerUpload. Eventually this will supplant
|
@service session;
|
||||||
// ComposerUpload, but until then only the functions that need to be
|
@service siteSettings;
|
||||||
// overridden to use uppy will be overridden, so as to not go out of
|
@service appEvents;
|
||||||
// sync with the main ComposerUpload functionality by copying unchanging
|
@service currentUser;
|
||||||
// functions.
|
@service site;
|
||||||
//
|
@service capabilities;
|
||||||
// Some examples are uploadPlaceholder, the main properties e.g. uploadProgress,
|
@service messageBus;
|
||||||
// and the most important _bindUploadTarget which handles all the main upload
|
@service composer;
|
||||||
// functionality and event binding.
|
|
||||||
//
|
|
||||||
export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
|
|
||||||
dialog: service(),
|
|
||||||
session: service(),
|
|
||||||
|
|
||||||
uploadRootPath: "/uploads",
|
uppyWrapper;
|
||||||
uploadTargetBound: false,
|
|
||||||
useUploadPlaceholders: true,
|
uploadRootPath = "/uploads";
|
||||||
|
uppyId = "composer-editor-uppy";
|
||||||
|
uploadType = "composer";
|
||||||
|
editorInputClass = ".d-editor-input";
|
||||||
|
mobileFileUploaderId = "mobile-file-upload";
|
||||||
|
fileUploadElementId = "file-uploader";
|
||||||
|
editorClass = ".d-editor";
|
||||||
|
|
||||||
|
composerEventPrefix;
|
||||||
|
composerModel;
|
||||||
|
uploadMarkdownResolvers;
|
||||||
|
uploadPreProcessors;
|
||||||
|
uploadHandlers;
|
||||||
|
|
||||||
|
#inProgressUploads = [];
|
||||||
|
#bufferedUploadErrors = [];
|
||||||
|
#placeholders = {};
|
||||||
|
|
||||||
|
#useUploadPlaceholders = true;
|
||||||
|
#uploadTargetBound = false;
|
||||||
|
#userCancelled = false;
|
||||||
|
|
||||||
|
#fileInputEl;
|
||||||
|
#editorEl;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
owner,
|
||||||
|
{
|
||||||
|
composerEventPrefix,
|
||||||
|
composerModel,
|
||||||
|
uploadMarkdownResolvers,
|
||||||
|
uploadPreProcessors,
|
||||||
|
uploadHandlers,
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
setOwner(this, owner);
|
||||||
|
this.uppyWrapper = new UppyWrapper(owner);
|
||||||
|
this.composerEventPrefix = composerEventPrefix;
|
||||||
|
this.composerModel = composerModel;
|
||||||
|
this.uploadMarkdownResolvers = uploadMarkdownResolvers;
|
||||||
|
this.uploadPreProcessors = uploadPreProcessors;
|
||||||
|
this.uploadHandlers = uploadHandlers;
|
||||||
|
}
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
_cancelSingleUpload(data) {
|
_cancelUpload(data) {
|
||||||
this._uppyInstance.removeFile(data.fileId);
|
if (data) {
|
||||||
},
|
// Single file
|
||||||
|
this.uppyWrapper.uppyInstance.removeFile(data.fileId);
|
||||||
@observes("composerModel.uploadCancelled")
|
} else {
|
||||||
_cancelUpload() {
|
// All files
|
||||||
if (!this.get("composerModel.uploadCancelled")) {
|
this.#userCancelled = true;
|
||||||
return;
|
this.uppyWrapper.uppyInstance.cancelAll();
|
||||||
}
|
}
|
||||||
this.set("composerModel.uploadCancelled", false);
|
}
|
||||||
this.set("userCancelled", true);
|
|
||||||
|
|
||||||
this._uppyInstance.cancelAll();
|
teardown() {
|
||||||
},
|
if (!this.#uploadTargetBound) {
|
||||||
|
|
||||||
@on("willDestroyElement")
|
|
||||||
_unbindUploadTarget() {
|
|
||||||
if (!this.uploadTargetBound) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fileInputEl?.removeEventListener(
|
this.#fileInputEl?.removeEventListener(
|
||||||
"change",
|
"change",
|
||||||
this.fileInputEventListener
|
this.fileInputEventListener
|
||||||
);
|
);
|
||||||
|
|
||||||
this.editorEl?.removeEventListener("paste", this.pasteEventListener);
|
this.#editorEl?.removeEventListener("paste", this._pasteEventListener);
|
||||||
|
|
||||||
this.appEvents.off(`${this.composerEventPrefix}:add-files`, this._addFiles);
|
this.appEvents.off(`${this.composerEventPrefix}:add-files`, this._addFiles);
|
||||||
this.appEvents.off(
|
this.appEvents.off(
|
||||||
`${this.composerEventPrefix}:cancel-upload`,
|
`${this.composerEventPrefix}:cancel-upload`,
|
||||||
this._cancelSingleUpload
|
this._cancelUpload
|
||||||
);
|
);
|
||||||
|
|
||||||
this._reset();
|
this.#reset();
|
||||||
|
|
||||||
if (this._uppyInstance) {
|
if (this.uppyWrapper.uppyInstance) {
|
||||||
this._uppyInstance.close();
|
this.uppyWrapper.uppyInstance.close();
|
||||||
this._uppyInstance = null;
|
this.uppyWrapper.uppyInstance = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.uploadTargetBound = false;
|
this.#unbindMobileUploadButton();
|
||||||
},
|
this.#uploadTargetBound = false;
|
||||||
|
}
|
||||||
|
|
||||||
_abortAndReset() {
|
#abortAndReset() {
|
||||||
this.appEvents.trigger(`${this.composerEventPrefix}:uploads-aborted`);
|
this.appEvents.trigger(`${this.composerEventPrefix}:uploads-aborted`);
|
||||||
this._reset();
|
this.#reset();
|
||||||
return false;
|
return false;
|
||||||
},
|
}
|
||||||
|
|
||||||
_bindUploadTarget() {
|
setup(element) {
|
||||||
this.set("inProgressUploads", []);
|
this.#editorEl = element.querySelector(this.editorClass);
|
||||||
this.set("bufferedUploadErrors", []);
|
this.#fileInputEl = document.getElementById(this.fileUploadElementId);
|
||||||
this.placeholders = {};
|
|
||||||
this._preProcessorStatus = {};
|
|
||||||
this.editorEl = this.element.querySelector(this.editorClass);
|
|
||||||
this.fileInputEl = document.getElementById(this.fileUploadElementId);
|
|
||||||
const isPrivateMessage = this.get("composerModel.privateMessage");
|
|
||||||
|
|
||||||
this.appEvents.on(`${this.composerEventPrefix}:add-files`, this._addFiles);
|
this.appEvents.on(`${this.composerEventPrefix}:add-files`, this._addFiles);
|
||||||
this.appEvents.on(
|
this.appEvents.on(
|
||||||
`${this.composerEventPrefix}:cancel-upload`,
|
`${this.composerEventPrefix}:cancel-upload`,
|
||||||
this._cancelSingleUpload
|
this._cancelUpload
|
||||||
);
|
);
|
||||||
|
|
||||||
this._unbindUploadTarget();
|
|
||||||
this.fileInputEventListener = bindFileInputChangeListener(
|
this.fileInputEventListener = bindFileInputChangeListener(
|
||||||
this.fileInputEl,
|
this.#fileInputEl,
|
||||||
this._addFiles
|
this._addFiles
|
||||||
);
|
);
|
||||||
this.editorEl.addEventListener("paste", this.pasteEventListener);
|
this.#editorEl.addEventListener("paste", this._pasteEventListener);
|
||||||
|
|
||||||
this._uppyInstance = new Uppy({
|
this.uppyWrapper.uppyInstance = new Uppy({
|
||||||
id: this.uppyId,
|
id: this.uppyId,
|
||||||
autoProceed: true,
|
autoProceed: true,
|
||||||
|
|
||||||
// need to use upload_type because uppy overrides type with the
|
// need to use upload_type because uppy overrides type with the
|
||||||
// actual file type
|
// actual file type
|
||||||
meta: deepMerge({ upload_type: this.uploadType }, this.data || {}),
|
meta: { upload_type: this.uploadType },
|
||||||
|
|
||||||
onBeforeFileAdded: (currentFile) => {
|
onBeforeFileAdded: (currentFile) => {
|
||||||
const validationOpts = {
|
const validationOpts = {
|
||||||
user: this.currentUser,
|
user: this.currentUser,
|
||||||
siteSettings: this.siteSettings,
|
siteSettings: this.siteSettings,
|
||||||
isPrivateMessage,
|
isPrivateMessage: this.composerModel.privateMessage,
|
||||||
allowStaffToUploadAnyFileInPm:
|
allowStaffToUploadAnyFileInPm:
|
||||||
this.siteSettings.allow_staff_to_upload_any_file_in_pm,
|
this.siteSettings.allow_staff_to_upload_any_file_in_pm,
|
||||||
};
|
};
|
||||||
|
|
||||||
const isUploading = validateUploadedFile(currentFile, validationOpts);
|
const isUploading = validateUploadedFile(currentFile, validationOpts);
|
||||||
|
|
||||||
this.setProperties({
|
this.composer.setProperties({
|
||||||
uploadProgress: 0,
|
uploadProgress: 0,
|
||||||
isUploading,
|
isUploading,
|
||||||
isCancellable: isUploading,
|
isCancellable: isUploading,
|
||||||
|
@ -162,7 +187,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
|
||||||
const handlerBuckets = {};
|
const handlerBuckets = {};
|
||||||
|
|
||||||
for (const [fileId, file] of Object.entries(files)) {
|
for (const [fileId, file] of Object.entries(files)) {
|
||||||
const matchingHandler = this._findMatchingUploadHandler(file.name);
|
const matchingHandler = this.#findMatchingUploadHandler(file.name);
|
||||||
if (matchingHandler) {
|
if (matchingHandler) {
|
||||||
// the function signature will be converted to a string for the
|
// the function signature will be converted to a string for the
|
||||||
// object key, so we can send multiple files at once to each handler
|
// object key, so we can send multiple files at once to each handler
|
||||||
|
@ -186,7 +211,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
|
||||||
// a single file at a time through to the handler.
|
// a single file at a time through to the handler.
|
||||||
for (const bucket of Object.values(handlerBuckets)) {
|
for (const bucket of Object.values(handlerBuckets)) {
|
||||||
if (!bucket.fn(bucket.files, this)) {
|
if (!bucket.fn(bucket.files, this)) {
|
||||||
return this._abortAndReset();
|
return this.#abortAndReset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,7 +224,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
|
||||||
count: maxFiles,
|
count: maxFiles,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return this._abortAndReset();
|
return this.#abortAndReset();
|
||||||
}
|
}
|
||||||
|
|
||||||
// uppy uses this new object to track progress of remaining files
|
// uppy uses this new object to track progress of remaining files
|
||||||
|
@ -208,34 +233,40 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.siteSettings.enable_upload_debug_mode) {
|
if (this.siteSettings.enable_upload_debug_mode) {
|
||||||
this._instrumentUploadTimings();
|
this.uppyWrapper.debug.instrumentUploadTimings(
|
||||||
|
this.uppyWrapper.uppyInstance
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.siteSettings.enable_direct_s3_uploads) {
|
if (this.siteSettings.enable_direct_s3_uploads) {
|
||||||
this._useS3MultipartUploads();
|
new UppyS3Multipart(getOwner(this), {
|
||||||
|
uploadRootPath: this.uploadRootPath,
|
||||||
|
uppyWrapper: this.uppyWrapper,
|
||||||
|
errorHandler: this._handleUploadError,
|
||||||
|
}).apply(this.uppyWrapper.uppyInstance);
|
||||||
} else {
|
} else {
|
||||||
this._useXHRUploads();
|
this.#useXHRUploads();
|
||||||
}
|
}
|
||||||
|
|
||||||
this._uppyInstance.on("file-added", (file) => {
|
this.uppyWrapper.uppyInstance.on("file-added", (file) => {
|
||||||
run(() => {
|
run(() => {
|
||||||
if (isPrivateMessage) {
|
if (this.composerModel.privateMessage) {
|
||||||
file.meta.for_private_message = true;
|
file.meta.for_private_message = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this._uppyInstance.on("progress", (progress) => {
|
this.uppyWrapper.uppyInstance.on("progress", (progress) => {
|
||||||
run(() => {
|
run(() => {
|
||||||
if (this.isDestroying || this.isDestroyed) {
|
if (this.isDestroying || this.isDestroyed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.set("uploadProgress", progress);
|
this.composer.set("uploadProgress", progress);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this._uppyInstance.on("file-removed", (file, reason) => {
|
this.uppyWrapper.uppyInstance.on("file-removed", (file, reason) => {
|
||||||
run(() => {
|
run(() => {
|
||||||
// we handle the cancel-all event specifically, so no need
|
// we handle the cancel-all event specifically, so no need
|
||||||
// to do anything here. this event is also fired when some files
|
// to do anything here. this event is also fired when some files
|
||||||
|
@ -248,22 +279,24 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
|
||||||
file.id
|
file.id
|
||||||
);
|
);
|
||||||
file.meta.cancelled = true;
|
file.meta.cancelled = true;
|
||||||
this._removeInProgressUpload(file.id);
|
this.#removeInProgressUpload(file.id);
|
||||||
this._resetUpload(file, { removePlaceholder: true });
|
this.#resetUpload(file, { removePlaceholder: true });
|
||||||
if (this.inProgressUploads.length === 0) {
|
if (this.#inProgressUploads.length === 0) {
|
||||||
this.set("userCancelled", true);
|
this.#userCancelled = true;
|
||||||
this._uppyInstance.cancelAll();
|
this.uppyWrapper.uppyInstance.cancelAll();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this._uppyInstance.on("upload-progress", (file, progress) => {
|
this.uppyWrapper.uppyInstance.on("upload-progress", (file, progress) => {
|
||||||
run(() => {
|
run(() => {
|
||||||
if (this.isDestroying || this.isDestroyed) {
|
if (this.isDestroying || this.isDestroyed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const upload = this.inProgressUploads.find((upl) => upl.id === file.id);
|
const upload = this.#inProgressUploads.find(
|
||||||
|
(upl) => upl.id === file.id
|
||||||
|
);
|
||||||
if (upload) {
|
if (upload) {
|
||||||
const percentage = Math.round(
|
const percentage = Math.round(
|
||||||
(progress.bytesUploaded / progress.bytesTotal) * 100
|
(progress.bytesUploaded / progress.bytesTotal) * 100
|
||||||
|
@ -273,15 +306,15 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this._uppyInstance.on("upload", (data) => {
|
this.uppyWrapper.uppyInstance.on("upload", (data) => {
|
||||||
run(() => {
|
run(() => {
|
||||||
this._addNeedProcessing(data.fileIDs.length);
|
this.uppyWrapper.addNeedProcessing(data.fileIDs.length);
|
||||||
|
|
||||||
const files = data.fileIDs.map((fileId) =>
|
const files = data.fileIDs.map((fileId) =>
|
||||||
this._uppyInstance.getFile(fileId)
|
this.uppyWrapper.uppyInstance.getFile(fileId)
|
||||||
);
|
);
|
||||||
|
|
||||||
this.setProperties({
|
this.composer.setProperties({
|
||||||
isProcessingUpload: true,
|
isProcessingUpload: true,
|
||||||
isCancellable: false,
|
isCancellable: false,
|
||||||
});
|
});
|
||||||
|
@ -290,7 +323,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
|
||||||
// The inProgressUploads is meant to be used to display these uploads
|
// The inProgressUploads is meant to be used to display these uploads
|
||||||
// in a UI, and Ember will only update the array in the UI if pushObject
|
// in a UI, and Ember will only update the array in the UI if pushObject
|
||||||
// is used to notify it.
|
// is used to notify it.
|
||||||
this.inProgressUploads.pushObject(
|
this.#inProgressUploads.pushObject(
|
||||||
EmberObject.create({
|
EmberObject.create({
|
||||||
fileName: file.name,
|
fileName: file.name,
|
||||||
id: file.id,
|
id: file.id,
|
||||||
|
@ -298,12 +331,12 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
|
||||||
extension: file.extension,
|
extension: file.extension,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
const placeholder = this._uploadPlaceholder(file);
|
const placeholder = this.#uploadPlaceholder(file);
|
||||||
this.placeholders[file.id] = {
|
this.#placeholders[file.id] = {
|
||||||
uploadPlaceholder: placeholder,
|
uploadPlaceholder: placeholder,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.useUploadPlaceholders) {
|
if (this.#useUploadPlaceholders) {
|
||||||
this.appEvents.trigger(
|
this.appEvents.trigger(
|
||||||
`${this.composerEventPrefix}:insert-text`,
|
`${this.composerEventPrefix}:insert-text`,
|
||||||
placeholder
|
placeholder
|
||||||
|
@ -318,12 +351,12 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this._uppyInstance.on("upload-success", (file, response) => {
|
this.uppyWrapper.uppyInstance.on("upload-success", (file, response) => {
|
||||||
run(async () => {
|
run(async () => {
|
||||||
if (!this._uppyInstance) {
|
if (!this.uppyWrapper.uppyInstance) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._removeInProgressUpload(file.id);
|
this.#removeInProgressUpload(file.id);
|
||||||
let upload = response.body;
|
let upload = response.body;
|
||||||
const markdown = await this.uploadMarkdownResolvers.reduce(
|
const markdown = await this.uploadMarkdownResolvers.reduce(
|
||||||
(md, resolver) => resolver(upload) || md,
|
(md, resolver) => resolver(upload) || md,
|
||||||
|
@ -336,40 +369,40 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
|
||||||
file,
|
file,
|
||||||
upload.url,
|
upload.url,
|
||||||
() => {
|
() => {
|
||||||
if (this.useUploadPlaceholders) {
|
if (this.#useUploadPlaceholders) {
|
||||||
this.appEvents.trigger(
|
this.appEvents.trigger(
|
||||||
`${this.composerEventPrefix}:replace-text`,
|
`${this.composerEventPrefix}:replace-text`,
|
||||||
this.placeholders[file.id].uploadPlaceholder.trim(),
|
this.#placeholders[file.id].uploadPlaceholder.trim(),
|
||||||
markdown
|
markdown
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this._resetUpload(file, { removePlaceholder: false });
|
this.#resetUpload(file, { removePlaceholder: false });
|
||||||
this.appEvents.trigger(
|
this.appEvents.trigger(
|
||||||
`${this.composerEventPrefix}:upload-success`,
|
`${this.composerEventPrefix}:upload-success`,
|
||||||
file.name,
|
file.name,
|
||||||
upload
|
upload
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.inProgressUploads.length === 0) {
|
if (this.#inProgressUploads.length === 0) {
|
||||||
this.appEvents.trigger(
|
this.appEvents.trigger(
|
||||||
`${this.composerEventPrefix}:all-uploads-complete`
|
`${this.composerEventPrefix}:all-uploads-complete`
|
||||||
);
|
);
|
||||||
this._displayBufferedErrors();
|
this.#displayBufferedErrors();
|
||||||
this._reset();
|
this.#reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this._uppyInstance.on("upload-error", this._handleUploadError);
|
this.uppyWrapper.uppyInstance.on("upload-error", this._handleUploadError);
|
||||||
|
|
||||||
this._uppyInstance.on("cancel-all", () => {
|
this.uppyWrapper.uppyInstance.on("cancel-all", () => {
|
||||||
// Do the manual cancelling work only if the user clicked cancel
|
// Do the manual cancelling work only if the user clicked cancel
|
||||||
if (this.userCancelled) {
|
if (this.#userCancelled) {
|
||||||
Object.values(this.placeholders).forEach((data) => {
|
Object.values(this.#placeholders).forEach((data) => {
|
||||||
run(() => {
|
run(() => {
|
||||||
if (this.useUploadPlaceholders) {
|
if (this.#useUploadPlaceholders) {
|
||||||
this.appEvents.trigger(
|
this.appEvents.trigger(
|
||||||
`${this.composerEventPrefix}:replace-text`,
|
`${this.composerEventPrefix}:replace-text`,
|
||||||
data.uploadPlaceholder,
|
data.uploadPlaceholder,
|
||||||
|
@ -379,68 +412,63 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.set("userCancelled", false);
|
this.#userCancelled = false;
|
||||||
this._reset();
|
this.#reset();
|
||||||
|
|
||||||
this.appEvents.trigger(`${this.composerEventPrefix}:uploads-cancelled`);
|
this.appEvents.trigger(`${this.composerEventPrefix}:uploads-cancelled`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this._setupPreProcessors();
|
this.#setupPreProcessors();
|
||||||
this._setupUIPlugins();
|
|
||||||
|
|
||||||
this.uploadTargetBound = true;
|
this.uppyWrapper.uppyInstance.use(DropTarget, { target: element });
|
||||||
this._uppyReady();
|
|
||||||
},
|
|
||||||
|
|
||||||
// This should be overridden in a child component if you need to
|
this.#uploadTargetBound = true;
|
||||||
// hook into uppy events and be sure that everything is already
|
this.#bindMobileUploadButton();
|
||||||
// set up for _uppyInstance.
|
}
|
||||||
_uppyReady() {},
|
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
_handleUploadError(file, error, response) {
|
_handleUploadError(file, error, response) {
|
||||||
this._removeInProgressUpload(file.id);
|
this.#removeInProgressUpload(file.id);
|
||||||
this._resetUpload(file, { removePlaceholder: true });
|
this.#resetUpload(file, { removePlaceholder: true });
|
||||||
|
|
||||||
file.meta.error = error;
|
file.meta.error = error;
|
||||||
|
|
||||||
if (!this.userCancelled) {
|
if (!this.#userCancelled) {
|
||||||
this._bufferUploadError(response || error, file.name);
|
this.#bufferUploadError(response || error, file.name);
|
||||||
this.appEvents.trigger(`${this.composerEventPrefix}:upload-error`, file);
|
this.appEvents.trigger(`${this.composerEventPrefix}:upload-error`, file);
|
||||||
}
|
}
|
||||||
if (this.inProgressUploads.length === 0) {
|
if (this.#inProgressUploads.length === 0) {
|
||||||
this._displayBufferedErrors();
|
this.#displayBufferedErrors();
|
||||||
this._reset();
|
this.#reset();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
_removeInProgressUpload(fileId) {
|
#removeInProgressUpload(fileId) {
|
||||||
this.set(
|
this.#inProgressUploads = this.#inProgressUploads.filter(
|
||||||
"inProgressUploads",
|
(upl) => upl.id !== fileId
|
||||||
this.inProgressUploads.filter((upl) => upl.id !== fileId)
|
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
|
|
||||||
_displayBufferedErrors() {
|
#displayBufferedErrors() {
|
||||||
if (this.bufferedUploadErrors.length === 0) {
|
if (this.#bufferedUploadErrors.length === 0) {
|
||||||
return;
|
return;
|
||||||
} else if (this.bufferedUploadErrors.length === 1) {
|
} else if (this.#bufferedUploadErrors.length === 1) {
|
||||||
displayErrorForUpload(
|
displayErrorForUpload(
|
||||||
this.bufferedUploadErrors[0].data,
|
this.#bufferedUploadErrors[0].data,
|
||||||
this.siteSettings,
|
this.siteSettings,
|
||||||
this.bufferedUploadErrors[0].fileName
|
this.#bufferedUploadErrors[0].fileName
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
displayErrorForBulkUpload(this.bufferedUploadErrors);
|
displayErrorForBulkUpload(this.#bufferedUploadErrors);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
_bufferUploadError(data, fileName) {
|
#bufferUploadError(data, fileName) {
|
||||||
this.bufferedUploadErrors.push({ data, fileName });
|
this.#bufferedUploadErrors.push({ data, fileName });
|
||||||
},
|
}
|
||||||
|
|
||||||
_setupPreProcessors() {
|
#setupPreProcessors() {
|
||||||
const checksumPreProcessor = {
|
const checksumPreProcessor = {
|
||||||
pluginClass: UppyChecksum,
|
pluginClass: UppyChecksum,
|
||||||
optionsResolverFn: ({ capabilities }) => {
|
optionsResolverFn: ({ capabilities }) => {
|
||||||
|
@ -457,19 +485,18 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
|
||||||
[this.uploadPreProcessors, checksumPreProcessor]
|
[this.uploadPreProcessors, checksumPreProcessor]
|
||||||
.flat()
|
.flat()
|
||||||
.forEach(({ pluginClass, optionsResolverFn }) => {
|
.forEach(({ pluginClass, optionsResolverFn }) => {
|
||||||
this._useUploadPlugin(
|
this.uppyWrapper.useUploadPlugin(
|
||||||
pluginClass,
|
pluginClass,
|
||||||
optionsResolverFn({
|
optionsResolverFn({
|
||||||
composerModel: this.composerModel,
|
composerModel: this.composerModel,
|
||||||
composerElement: this.composerElement,
|
|
||||||
capabilities: this.capabilities,
|
capabilities: this.capabilities,
|
||||||
isMobileDevice: this.site.isMobileDevice,
|
isMobileDevice: this.site.isMobileDevice,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
this._onPreProcessProgress((file) => {
|
this.uppyWrapper.onPreProcessProgress((file) => {
|
||||||
let placeholderData = this.placeholders[file.id];
|
let placeholderData = this.#placeholders[file.id];
|
||||||
placeholderData.processingPlaceholder = `[${I18n.t(
|
placeholderData.processingPlaceholder = `[${I18n.t(
|
||||||
"processing_filename",
|
"processing_filename",
|
||||||
{
|
{
|
||||||
|
@ -493,10 +520,10 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
this._onPreProcessComplete(
|
this.uppyWrapper.onPreProcessComplete(
|
||||||
(file) => {
|
(file) => {
|
||||||
run(() => {
|
run(() => {
|
||||||
let placeholderData = this.placeholders[file.id];
|
let placeholderData = this.#placeholders[file.id];
|
||||||
this.appEvents.trigger(
|
this.appEvents.trigger(
|
||||||
`${this.composerEventPrefix}:replace-text`,
|
`${this.composerEventPrefix}:replace-text`,
|
||||||
placeholderData.processingPlaceholder,
|
placeholderData.processingPlaceholder,
|
||||||
|
@ -506,7 +533,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
run(() => {
|
run(() => {
|
||||||
this.setProperties({
|
this.composer.setProperties({
|
||||||
isProcessingUpload: false,
|
isProcessingUpload: false,
|
||||||
isCancellable: true,
|
isCancellable: true,
|
||||||
});
|
});
|
||||||
|
@ -516,14 +543,10 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
|
|
||||||
_setupUIPlugins() {
|
#uploadFilenamePlaceholder(file) {
|
||||||
this._uppyInstance.use(DropTarget, this._uploadDropTargetOptions());
|
const filename = this.#filenamePlaceholder(file);
|
||||||
},
|
|
||||||
|
|
||||||
_uploadFilenamePlaceholder(file) {
|
|
||||||
const filename = this._filenamePlaceholder(file);
|
|
||||||
|
|
||||||
// when adding two separate files with the same filename search for matching
|
// when adding two separate files with the same filename search for matching
|
||||||
// placeholder already existing in the editor ie [Uploading: test.png…]
|
// placeholder already existing in the editor ie [Uploading: test.png…]
|
||||||
|
@ -533,9 +556,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
|
||||||
filename: escapedFilename + "(?:\\()?([0-9])?(?:\\))?",
|
filename: escapedFilename + "(?:\\()?([0-9])?(?:\\))?",
|
||||||
})}\\]\\(\\)`;
|
})}\\]\\(\\)`;
|
||||||
const globalRegex = new RegExp(regexString, "g");
|
const globalRegex = new RegExp(regexString, "g");
|
||||||
const matchingPlaceholder = this.get(
|
const matchingPlaceholder = this.composerModel.reply.match(globalRegex);
|
||||||
`composerModel.${this.composerModelContentKey}`
|
|
||||||
).match(globalRegex);
|
|
||||||
if (matchingPlaceholder) {
|
if (matchingPlaceholder) {
|
||||||
// get last matching placeholder and its consecutive nr in regex
|
// get last matching placeholder and its consecutive nr in regex
|
||||||
// capturing group and apply +1 to the placeholder
|
// capturing group and apply +1 to the placeholder
|
||||||
|
@ -548,58 +569,58 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
|
||||||
}
|
}
|
||||||
|
|
||||||
return filename;
|
return filename;
|
||||||
},
|
}
|
||||||
|
|
||||||
_uploadPlaceholder(file) {
|
#uploadPlaceholder(file) {
|
||||||
const clipboard = I18n.t("clipboard");
|
const clipboard = I18n.t("clipboard");
|
||||||
const uploadFilenamePlaceholder = this._uploadFilenamePlaceholder(file);
|
const uploadFilenamePlaceholder = this.#uploadFilenamePlaceholder(file);
|
||||||
const filename = uploadFilenamePlaceholder
|
const filename = uploadFilenamePlaceholder
|
||||||
? uploadFilenamePlaceholder
|
? uploadFilenamePlaceholder
|
||||||
: clipboard;
|
: clipboard;
|
||||||
|
|
||||||
let placeholder = `[${I18n.t("uploading_filename", { filename })}]()\n`;
|
let placeholder = `[${I18n.t("uploading_filename", { filename })}]()\n`;
|
||||||
if (!this._cursorIsOnEmptyLine()) {
|
if (!this.#cursorIsOnEmptyLine()) {
|
||||||
placeholder = `\n${placeholder}`;
|
placeholder = `\n${placeholder}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return placeholder;
|
return placeholder;
|
||||||
},
|
}
|
||||||
|
|
||||||
_useXHRUploads() {
|
#useXHRUploads() {
|
||||||
this._uppyInstance.use(XHRUpload, {
|
this.uppyWrapper.uppyInstance.use(XHRUpload, {
|
||||||
endpoint: getURL(`/uploads.json?client_id=${this.messageBus.clientId}`),
|
endpoint: getURL(`/uploads.json?client_id=${this.messageBus.clientId}`),
|
||||||
headers: () => ({
|
headers: () => ({
|
||||||
"X-CSRF-Token": this.session.csrfToken,
|
"X-CSRF-Token": this.session.csrfToken,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
_reset() {
|
#reset() {
|
||||||
this._uppyInstance?.cancelAll();
|
this.uppyWrapper.uppyInstance?.cancelAll();
|
||||||
this.setProperties({
|
this.composer.setProperties({
|
||||||
uploadProgress: 0,
|
uploadProgress: 0,
|
||||||
isUploading: false,
|
isUploading: false,
|
||||||
isProcessingUpload: false,
|
isProcessingUpload: false,
|
||||||
isCancellable: false,
|
isCancellable: false,
|
||||||
inProgressUploads: [],
|
|
||||||
bufferedUploadErrors: [],
|
|
||||||
});
|
});
|
||||||
this._resetPreProcessors();
|
this.#inProgressUploads = [];
|
||||||
this.fileInputEl.value = "";
|
this.#bufferedUploadErrors = [];
|
||||||
},
|
this.uppyWrapper.resetPreProcessors();
|
||||||
|
this.#fileInputEl.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
_resetUpload(file, opts) {
|
#resetUpload(file, opts) {
|
||||||
if (opts.removePlaceholder) {
|
if (opts.removePlaceholder) {
|
||||||
this.appEvents.trigger(
|
this.appEvents.trigger(
|
||||||
`${this.composerEventPrefix}:replace-text`,
|
`${this.composerEventPrefix}:replace-text`,
|
||||||
this.placeholders[file.id].uploadPlaceholder,
|
this.#placeholders[file.id].uploadPlaceholder,
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
pasteEventListener(event) {
|
_pasteEventListener(event) {
|
||||||
if (
|
if (
|
||||||
document.activeElement !== document.querySelector(this.editorInputClass)
|
document.activeElement !== document.querySelector(this.editorInputClass)
|
||||||
) {
|
) {
|
||||||
|
@ -618,7 +639,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
|
||||||
if (event && event.clipboardData && event.clipboardData.files) {
|
if (event && event.clipboardData && event.clipboardData.files) {
|
||||||
this._addFiles([...event.clipboardData.files], { pasted: true });
|
this._addFiles([...event.clipboardData.files], { pasted: true });
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
async _addFiles(files, opts = {}) {
|
async _addFiles(files, opts = {}) {
|
||||||
|
@ -629,7 +650,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
|
||||||
files = Array.isArray(files) ? files : [files];
|
files = Array.isArray(files) ? files : [files];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this._uppyInstance.addFiles(
|
this.uppyWrapper.uppyInstance.addFiles(
|
||||||
files.map((file) => {
|
files.map((file) => {
|
||||||
return {
|
return {
|
||||||
source: this.uppyId,
|
source: this.uppyId,
|
||||||
|
@ -645,13 +666,9 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
|
||||||
id: "discourse.upload.uppy-add-files-error",
|
id: "discourse.upload.uppy-add-files-error",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
showUploadSelector(toolbarEvent) {
|
#bindMobileUploadButton() {
|
||||||
this.send("showUploadSelector", toolbarEvent);
|
|
||||||
},
|
|
||||||
|
|
||||||
_bindMobileUploadButton() {
|
|
||||||
if (this.site.mobileView) {
|
if (this.site.mobileView) {
|
||||||
this.mobileUploadButton = document.getElementById(
|
this.mobileUploadButton = document.getElementById(
|
||||||
this.mobileFileUploaderId
|
this.mobileFileUploaderId
|
||||||
|
@ -662,35 +679,37 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
_mobileUploadButtonEventListener() {
|
_mobileUploadButtonEventListener() {
|
||||||
document.getElementById(this.fileUploadElementId).click();
|
this.#fileInputEl.click();
|
||||||
},
|
}
|
||||||
|
|
||||||
_unbindMobileUploadButton() {
|
#unbindMobileUploadButton() {
|
||||||
this.mobileUploadButton?.removeEventListener(
|
this.mobileUploadButton?.removeEventListener(
|
||||||
"click",
|
"click",
|
||||||
this._mobileUploadButtonEventListener
|
this._mobileUploadButtonEventListener
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
|
|
||||||
_filenamePlaceholder(data) {
|
#filenamePlaceholder(data) {
|
||||||
return data.name.replace(/\u200B-\u200D\uFEFF]/g, "");
|
return data.name.replace(/\u200B-\u200D\uFEFF]/g, "");
|
||||||
},
|
}
|
||||||
|
|
||||||
_resetUploadFilenamePlaceholder() {
|
#findMatchingUploadHandler(fileName) {
|
||||||
this.set("uploadFilenamePlaceholder", null);
|
return this.uploadHandlers.find((handler) => {
|
||||||
},
|
const ext = handler.extensions.join("|");
|
||||||
|
const regex = new RegExp(`\\.(${ext})$`, "i");
|
||||||
|
return regex.test(fileName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// target must be provided as a DOM element, however the
|
#cursorIsOnEmptyLine() {
|
||||||
// onDragOver and onDragLeave callbacks can also be provided.
|
const textArea = this.#editorEl.querySelector(this.editorInputClass);
|
||||||
// it is advisable to debounce/add a setTimeout timer when
|
const selectionStart = textArea.selectionStart;
|
||||||
// doing anything in these callbacks to avoid jumping. uppy
|
return (
|
||||||
// also adds a .uppy-is-drag-over class to the target element by
|
selectionStart === 0 || textArea.value.charAt(selectionStart - 1) === "\n"
|
||||||
// default onDragOver and removes it onDragLeave
|
);
|
||||||
_uploadDropTargetOptions() {
|
}
|
||||||
return { target: this.element };
|
}
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,17 +1,26 @@
|
||||||
import Mixin from "@ember/object/mixin";
|
import { setOwner } from "@ember/owner";
|
||||||
|
import { service } from "@ember/service";
|
||||||
import AwsS3Multipart from "@uppy/aws-s3-multipart";
|
import AwsS3Multipart from "@uppy/aws-s3-multipart";
|
||||||
import { Promise } from "rsvp";
|
import { Promise } from "rsvp";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
|
||||||
|
|
||||||
const RETRY_DELAYS = [0, 1000, 3000, 5000];
|
const RETRY_DELAYS = [0, 1000, 3000, 5000];
|
||||||
const MB = 1024 * 1024;
|
const MB = 1024 * 1024;
|
||||||
|
|
||||||
export default Mixin.create({
|
export default class UppyS3Multipart {
|
||||||
_useS3MultipartUploads() {
|
@service siteSettings;
|
||||||
this.set("usingS3MultipartUploads", true);
|
|
||||||
|
|
||||||
this._uppyInstance.use(AwsS3Multipart, {
|
constructor(owner, { uploadRootPath, errorHandler, uppyWrapper }) {
|
||||||
|
setOwner(this, owner);
|
||||||
|
this.uploadRootPath = uploadRootPath;
|
||||||
|
this.uppyWrapper = uppyWrapper;
|
||||||
|
this.errorHandler = errorHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(uppyInstance) {
|
||||||
|
this.uppyInstance = uppyInstance;
|
||||||
|
|
||||||
|
this.uppyInstance.use(AwsS3Multipart, {
|
||||||
// controls how many simultaneous _chunks_ are uploaded, not files,
|
// controls how many simultaneous _chunks_ are uploaded, not files,
|
||||||
// which in turn controls the minimum number of chunks presigned
|
// which in turn controls the minimum number of chunks presigned
|
||||||
// in each batch (limit / 2)
|
// in each batch (limit / 2)
|
||||||
|
@ -36,20 +45,19 @@ export default Mixin.create({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
createMultipartUpload: this._createMultipartUpload,
|
createMultipartUpload: this.#createMultipartUpload.bind(this),
|
||||||
prepareUploadParts: this._prepareUploadParts,
|
prepareUploadParts: this.#prepareUploadParts.bind(this),
|
||||||
completeMultipartUpload: this._completeMultipartUpload,
|
completeMultipartUpload: this.#completeMultipartUpload.bind(this),
|
||||||
abortMultipartUpload: this._abortMultipartUpload,
|
abortMultipartUpload: this.#abortMultipartUpload.bind(this),
|
||||||
|
|
||||||
// we will need a listParts function at some point when we want to
|
// we will need a listParts function at some point when we want to
|
||||||
// resume multipart uploads; this is used by uppy to figure out
|
// resume multipart uploads; this is used by uppy to figure out
|
||||||
// what parts are uploaded and which still need to be
|
// what parts are uploaded and which still need to be
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
@bind
|
#createMultipartUpload(file) {
|
||||||
_createMultipartUpload(file) {
|
this.uppyInstance.emit("create-multipart", file.id);
|
||||||
this._uppyInstance.emit("create-multipart", file.id);
|
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
file_name: file.name,
|
file_name: file.name,
|
||||||
|
@ -71,7 +79,7 @@ export default Mixin.create({
|
||||||
data,
|
data,
|
||||||
// uppy is inconsistent, an error here fires the upload-error event
|
// uppy is inconsistent, an error here fires the upload-error event
|
||||||
}).then((responseData) => {
|
}).then((responseData) => {
|
||||||
this._uppyInstance.emit("create-multipart-success", file.id);
|
this.uppyInstance.emit("create-multipart-success", file.id);
|
||||||
|
|
||||||
file.meta.unique_identifier = responseData.unique_identifier;
|
file.meta.unique_identifier = responseData.unique_identifier;
|
||||||
return {
|
return {
|
||||||
|
@ -79,10 +87,9 @@ export default Mixin.create({
|
||||||
key: responseData.key,
|
key: responseData.key,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
@bind
|
#prepareUploadParts(file, partData) {
|
||||||
_prepareUploadParts(file, partData) {
|
|
||||||
if (file.preparePartsRetryAttempts === undefined) {
|
if (file.preparePartsRetryAttempts === undefined) {
|
||||||
file.preparePartsRetryAttempts = 0;
|
file.preparePartsRetryAttempts = 0;
|
||||||
}
|
}
|
||||||
|
@ -96,7 +103,7 @@ export default Mixin.create({
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (file.preparePartsRetryAttempts) {
|
if (file.preparePartsRetryAttempts) {
|
||||||
delete file.preparePartsRetryAttempts;
|
delete file.preparePartsRetryAttempts;
|
||||||
this._consoleDebug(
|
this.uppyWrapper.debug.log(
|
||||||
`[uppy] Retrying batch fetch for ${file.id} was successful, continuing.`
|
`[uppy] Retrying batch fetch for ${file.id} was successful, continuing.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -118,27 +125,26 @@ export default Mixin.create({
|
||||||
file.preparePartsRetryAttempts += 1;
|
file.preparePartsRetryAttempts += 1;
|
||||||
const attemptsLeft =
|
const attemptsLeft =
|
||||||
RETRY_DELAYS.length - file.preparePartsRetryAttempts + 1;
|
RETRY_DELAYS.length - file.preparePartsRetryAttempts + 1;
|
||||||
this._consoleDebug(
|
this.uppyWrapper.debug.log(
|
||||||
`[uppy] Fetching a batch of upload part URLs for ${file.id} failed with status ${status}, retrying ${attemptsLeft} more times...`
|
`[uppy] Fetching a batch of upload part URLs for ${file.id} failed with status ${status}, retrying ${attemptsLeft} more times...`
|
||||||
);
|
);
|
||||||
return Promise.reject({ source: { status } });
|
return Promise.reject({ source: { status } });
|
||||||
} else {
|
} else {
|
||||||
this._consoleDebug(
|
this.uppyWrapper.debug.log(
|
||||||
`[uppy] Fetching a batch of upload part URLs for ${file.id} failed too many times, throwing error.`
|
`[uppy] Fetching a batch of upload part URLs for ${file.id} failed too many times, throwing error.`
|
||||||
);
|
);
|
||||||
// uppy is inconsistent, an error here does not fire the upload-error event
|
// uppy is inconsistent, an error here does not fire the upload-error event
|
||||||
this._handleUploadError(file, err);
|
this.handleUploadError(file, err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
@bind
|
#completeMultipartUpload(file, data) {
|
||||||
_completeMultipartUpload(file, data) {
|
|
||||||
if (file.meta.cancelled) {
|
if (file.meta.cancelled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._uppyInstance.emit("complete-multipart", file.id);
|
this.uppyInstance.emit("complete-multipart", file.id);
|
||||||
const parts = data.parts.map((part) => {
|
const parts = data.parts.map((part) => {
|
||||||
return { part_number: part.PartNumber, etag: part.ETag };
|
return { part_number: part.PartNumber, etag: part.ETag };
|
||||||
});
|
});
|
||||||
|
@ -153,13 +159,12 @@ export default Mixin.create({
|
||||||
}),
|
}),
|
||||||
// uppy is inconsistent, an error here fires the upload-error event
|
// uppy is inconsistent, an error here fires the upload-error event
|
||||||
}).then((responseData) => {
|
}).then((responseData) => {
|
||||||
this._uppyInstance.emit("complete-multipart-success", file.id);
|
this.uppyInstance.emit("complete-multipart-success", file.id);
|
||||||
return responseData;
|
return responseData;
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
@bind
|
#abortMultipartUpload(file, { key, uploadId }) {
|
||||||
_abortMultipartUpload(file, { key, uploadId }) {
|
|
||||||
// if the user cancels the upload before the key and uploadId
|
// if the user cancels the upload before the key and uploadId
|
||||||
// are stored from the createMultipartUpload response then they
|
// are stored from the createMultipartUpload response then they
|
||||||
// will not be set, and we don't have to abort the upload because
|
// will not be set, and we don't have to abort the upload because
|
||||||
|
@ -184,7 +189,7 @@ export default Mixin.create({
|
||||||
},
|
},
|
||||||
// uppy is inconsistent, an error here does not fire the upload-error event
|
// uppy is inconsistent, an error here does not fire the upload-error event
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
this._handleUploadError(file, err);
|
this.errorHandler(file, err);
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
});
|
}
|
|
@ -1,15 +1,22 @@
|
||||||
import { warn } from "@ember/debug";
|
import { warn } from "@ember/debug";
|
||||||
import Mixin from "@ember/object/mixin";
|
import { setOwner } from "@ember/owner";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
|
||||||
export default Mixin.create({
|
export default class UppyUploadDebugging {
|
||||||
_consoleDebug(msg) {
|
@service siteSettings;
|
||||||
|
|
||||||
|
constructor(owner) {
|
||||||
|
setOwner(this, owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
log(msg) {
|
||||||
if (this.siteSettings.enable_upload_debug_mode) {
|
if (this.siteSettings.enable_upload_debug_mode) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(msg);
|
console.log(msg);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
_consolePerformanceTiming(timing) {
|
#consolePerformanceTiming(timing) {
|
||||||
// Sometimes performance.measure can fail to return a PerformanceMeasure
|
// Sometimes performance.measure can fail to return a PerformanceMeasure
|
||||||
// object, in this case we can't log anything so return to prevent errors.
|
// object, in this case we can't log anything so return to prevent errors.
|
||||||
if (!timing) {
|
if (!timing) {
|
||||||
|
@ -19,27 +26,25 @@ export default Mixin.create({
|
||||||
const minutes = Math.floor(timing.duration / 60000);
|
const minutes = Math.floor(timing.duration / 60000);
|
||||||
const seconds = ((timing.duration % 60000) / 1000).toFixed(0);
|
const seconds = ((timing.duration % 60000) / 1000).toFixed(0);
|
||||||
const duration = minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
|
const duration = minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
|
||||||
this._consoleDebug(
|
this.log(`${timing.name}:\n duration: ${duration} (${timing.duration}ms)`);
|
||||||
`${timing.name}:\n duration: ${duration} (${timing.duration}ms)`
|
}
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
_performanceApiSupport() {
|
#performanceApiSupport() {
|
||||||
this._performanceMark("testing support 1");
|
this.#performanceMark("testing support 1");
|
||||||
this._performanceMark("testing support 2");
|
this.#performanceMark("testing support 2");
|
||||||
const perfMeasure = this._performanceMeasure(
|
const perfMeasure = this.#performanceMeasure(
|
||||||
"performance api support",
|
"performance api support",
|
||||||
"testing support 1",
|
"testing support 1",
|
||||||
"testing support 2"
|
"testing support 2"
|
||||||
);
|
);
|
||||||
return perfMeasure;
|
return perfMeasure;
|
||||||
},
|
}
|
||||||
|
|
||||||
_performanceMark(markName) {
|
#performanceMark(markName) {
|
||||||
return performance.mark(markName);
|
return performance.mark(markName);
|
||||||
},
|
}
|
||||||
|
|
||||||
_performanceMeasure(measureName, startMark, endMark) {
|
#performanceMeasure(measureName, startMark, endMark) {
|
||||||
let measureResult;
|
let measureResult;
|
||||||
try {
|
try {
|
||||||
measureResult = performance.measure(measureName, startMark, endMark);
|
measureResult = performance.measure(measureName, startMark, endMark);
|
||||||
|
@ -54,36 +59,36 @@ export default Mixin.create({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return measureResult;
|
return measureResult;
|
||||||
},
|
}
|
||||||
|
|
||||||
_instrumentUploadTimings() {
|
instrumentUploadTimings(uppy) {
|
||||||
if (!this._performanceApiSupport()) {
|
if (!this.#performanceApiSupport()) {
|
||||||
warn(
|
warn(
|
||||||
"Some browsers do not return a PerformanceMeasure when calling this._performanceMark, disabling instrumentation. See https://developer.mozilla.org/en-US/docs/Web/API/this._performanceMeasure#return_value and https://bugzilla.mozilla.org/show_bug.cgi?id=1724645",
|
"Some browsers do not return a PerformanceMeasure when calling this.#performanceMark, disabling instrumentation. See https://developer.mozilla.org/en-US/docs/Web/API/this.#performanceMeasure#return_value and https://bugzilla.mozilla.org/show_bug.cgi?id=1724645",
|
||||||
{ id: "discourse.upload-debugging" }
|
{ id: "discourse.upload-debugging" }
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._uppyInstance.on("upload", (data) => {
|
uppy.on("upload", (data) => {
|
||||||
data.fileIDs.forEach((fileId) =>
|
data.fileIDs.forEach((fileId) =>
|
||||||
this._performanceMark(`upload-${fileId}-start`)
|
this.#performanceMark(`upload-${fileId}-start`)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
this._uppyInstance.on("create-multipart", (fileId) => {
|
uppy.on("create-multipart", (fileId) => {
|
||||||
this._performanceMark(`upload-${fileId}-create-multipart`);
|
this.#performanceMark(`upload-${fileId}-create-multipart`);
|
||||||
});
|
});
|
||||||
|
|
||||||
this._uppyInstance.on("create-multipart-success", (fileId) => {
|
uppy.on("create-multipart-success", (fileId) => {
|
||||||
this._performanceMark(`upload-${fileId}-create-multipart-success`);
|
this.#performanceMark(`upload-${fileId}-create-multipart-success`);
|
||||||
});
|
});
|
||||||
|
|
||||||
this._uppyInstance.on("complete-multipart", (fileId) => {
|
uppy.on("complete-multipart", (fileId) => {
|
||||||
this._performanceMark(`upload-${fileId}-complete-multipart`);
|
this.#performanceMark(`upload-${fileId}-complete-multipart`);
|
||||||
|
|
||||||
this._consolePerformanceTiming(
|
this.#consolePerformanceTiming(
|
||||||
this._performanceMeasure(
|
this.#performanceMeasure(
|
||||||
`upload-${fileId}-multipart-all-parts-complete`,
|
`upload-${fileId}-multipart-all-parts-complete`,
|
||||||
`upload-${fileId}-create-multipart-success`,
|
`upload-${fileId}-create-multipart-success`,
|
||||||
`upload-${fileId}-complete-multipart`
|
`upload-${fileId}-complete-multipart`
|
||||||
|
@ -91,27 +96,27 @@ export default Mixin.create({
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
this._uppyInstance.on("complete-multipart-success", (fileId) => {
|
uppy.on("complete-multipart-success", (fileId) => {
|
||||||
this._performanceMark(`upload-${fileId}-complete-multipart-success`);
|
this.#performanceMark(`upload-${fileId}-complete-multipart-success`);
|
||||||
|
|
||||||
this._consolePerformanceTiming(
|
this.#consolePerformanceTiming(
|
||||||
this._performanceMeasure(
|
this.#performanceMeasure(
|
||||||
`upload-${fileId}-multipart-total-network-exclusive-complete-multipart`,
|
`upload-${fileId}-multipart-total-network-exclusive-complete-multipart`,
|
||||||
`upload-${fileId}-create-multipart`,
|
`upload-${fileId}-create-multipart`,
|
||||||
`upload-${fileId}-complete-multipart`
|
`upload-${fileId}-complete-multipart`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
this._consolePerformanceTiming(
|
this.#consolePerformanceTiming(
|
||||||
this._performanceMeasure(
|
this.#performanceMeasure(
|
||||||
`upload-${fileId}-multipart-total-network-inclusive-complete-multipart`,
|
`upload-${fileId}-multipart-total-network-inclusive-complete-multipart`,
|
||||||
`upload-${fileId}-create-multipart`,
|
`upload-${fileId}-create-multipart`,
|
||||||
`upload-${fileId}-complete-multipart-success`
|
`upload-${fileId}-complete-multipart-success`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
this._consolePerformanceTiming(
|
this.#consolePerformanceTiming(
|
||||||
this._performanceMeasure(
|
this.#performanceMeasure(
|
||||||
`upload-${fileId}-multipart-complete-convert-to-upload`,
|
`upload-${fileId}-multipart-complete-convert-to-upload`,
|
||||||
`upload-${fileId}-complete-multipart`,
|
`upload-${fileId}-complete-multipart`,
|
||||||
`upload-${fileId}-complete-multipart-success`
|
`upload-${fileId}-complete-multipart-success`
|
||||||
|
@ -119,15 +124,15 @@ export default Mixin.create({
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
this._uppyInstance.on("upload-success", (file) => {
|
uppy.on("upload-success", (file) => {
|
||||||
this._performanceMark(`upload-${file.id}-end`);
|
this.#performanceMark(`upload-${file.id}-end`);
|
||||||
this._consolePerformanceTiming(
|
this.#consolePerformanceTiming(
|
||||||
this._performanceMeasure(
|
this.#performanceMeasure(
|
||||||
`upload-${file.id}-multipart-total-inclusive-preprocessing`,
|
`upload-${file.id}-multipart-total-inclusive-preprocessing`,
|
||||||
`upload-${file.id}-start`,
|
`upload-${file.id}-start`,
|
||||||
`upload-${file.id}-end`
|
`upload-${file.id}-end`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
});
|
}
|
|
@ -0,0 +1,549 @@
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import { warn } from "@ember/debug";
|
||||||
|
import EmberObject from "@ember/object";
|
||||||
|
import { getOwner, setOwner } from "@ember/owner";
|
||||||
|
import { run } from "@ember/runloop";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
import { TrackedArray } from "@ember-compat/tracked-built-ins";
|
||||||
|
import AwsS3 from "@uppy/aws-s3";
|
||||||
|
import Uppy from "@uppy/core";
|
||||||
|
import DropTarget from "@uppy/drop-target";
|
||||||
|
import XHRUpload from "@uppy/xhr-upload";
|
||||||
|
import { ajax, updateCsrfToken } from "discourse/lib/ajax";
|
||||||
|
import {
|
||||||
|
bindFileInputChangeListener,
|
||||||
|
displayErrorForUpload,
|
||||||
|
validateUploadedFile,
|
||||||
|
} from "discourse/lib/uploads";
|
||||||
|
import UppyS3Multipart from "discourse/lib/uppy/s3-multipart";
|
||||||
|
import UppyWrapper from "discourse/lib/uppy/wrapper";
|
||||||
|
import UppyChecksum from "discourse/lib/uppy-checksum-plugin";
|
||||||
|
import UppyChunkedUploader from "discourse/lib/uppy-chunked-uploader-plugin";
|
||||||
|
import getUrl from "discourse-common/lib/get-url";
|
||||||
|
import { deepMerge } from "discourse-common/lib/object";
|
||||||
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
|
import I18n from "discourse-i18n";
|
||||||
|
|
||||||
|
export const HUGE_FILE_THRESHOLD_BYTES = 104_857_600; // 100MB
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG = {
|
||||||
|
uploadDone: null,
|
||||||
|
uploadError: null,
|
||||||
|
autoStartUploads: true,
|
||||||
|
uploadUrl: null,
|
||||||
|
uploadRootPath: "/uploads",
|
||||||
|
validateUploadedFilesOptions: {},
|
||||||
|
additionalParams: {},
|
||||||
|
maxFiles: null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overridable for custom file validations, executed before uploading.
|
||||||
|
*
|
||||||
|
* @param {object} file
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isUploadedFileAllowed: () => true,
|
||||||
|
|
||||||
|
/** set file data on a per-file basis */
|
||||||
|
perFileData: null,
|
||||||
|
|
||||||
|
uploadDropTargetOptions: null,
|
||||||
|
preventDirectS3Uploads: false,
|
||||||
|
useChunkedUploads: false,
|
||||||
|
useMultipartUploadsIfAvailable: false,
|
||||||
|
uppyReady: null,
|
||||||
|
onProgressUploadsChanged: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Merges incoming config with defaults, without actually evaluating
|
||||||
|
// any getters on the incoming config.
|
||||||
|
function lazyMergeConfig(config) {
|
||||||
|
const mergedConfig = {};
|
||||||
|
|
||||||
|
const incomingDescriptors = Object.getOwnPropertyDescriptors(config);
|
||||||
|
const defaultDescriptors = Object.getOwnPropertyDescriptors(DEFAULT_CONFIG);
|
||||||
|
|
||||||
|
Object.defineProperties(mergedConfig, {
|
||||||
|
...defaultDescriptors,
|
||||||
|
...incomingDescriptors,
|
||||||
|
});
|
||||||
|
|
||||||
|
return mergedConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
const REQUIRED_CONFIG_KEYS = ["id", "uploadDone"];
|
||||||
|
function validateConfig(config) {
|
||||||
|
for (const key of REQUIRED_CONFIG_KEYS) {
|
||||||
|
if (!config[key]) {
|
||||||
|
throw new Error(`Missing required UppyUpload config: ${key}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class UppyUpload {
|
||||||
|
@service dialog;
|
||||||
|
@service messageBus;
|
||||||
|
@service appEvents;
|
||||||
|
@service siteSettings;
|
||||||
|
@service capabilities;
|
||||||
|
@service session;
|
||||||
|
|
||||||
|
@tracked uploading = false;
|
||||||
|
@tracked processing = false;
|
||||||
|
@tracked uploadProgress = 0;
|
||||||
|
@tracked allowMultipleFiles;
|
||||||
|
@tracked filesAwaitingUpload = false;
|
||||||
|
@tracked cancellable = false;
|
||||||
|
|
||||||
|
inProgressUploads = new TrackedArray();
|
||||||
|
|
||||||
|
uppyWrapper;
|
||||||
|
|
||||||
|
#fileInputEventListener;
|
||||||
|
#usingS3Uploads;
|
||||||
|
|
||||||
|
_fileInputEl;
|
||||||
|
|
||||||
|
constructor(owner, config) {
|
||||||
|
setOwner(this, owner);
|
||||||
|
this.uppyWrapper = new UppyWrapper(owner);
|
||||||
|
this.config = lazyMergeConfig(config);
|
||||||
|
validateConfig(this.config);
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown() {
|
||||||
|
this.messageBus.unsubscribe(`/uploads/${this.config.type}`);
|
||||||
|
|
||||||
|
this._fileInputEl?.removeEventListener(
|
||||||
|
"change",
|
||||||
|
this.#fileInputEventListener
|
||||||
|
);
|
||||||
|
this.appEvents.off(
|
||||||
|
`upload-mixin:${this.config.id}:add-files`,
|
||||||
|
this.addFiles
|
||||||
|
);
|
||||||
|
this.appEvents.off(
|
||||||
|
`upload-mixin:${this.config.id}:cancel-upload`,
|
||||||
|
this._cancelSingleUpload
|
||||||
|
);
|
||||||
|
this.uppyWrapper.uppyInstance?.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
setup(fileInputEl) {
|
||||||
|
this._fileInputEl = fileInputEl;
|
||||||
|
|
||||||
|
this.allowMultipleFiles = this._fileInputEl.multiple;
|
||||||
|
|
||||||
|
this.#bindFileInputChange();
|
||||||
|
|
||||||
|
this.uppyWrapper.uppyInstance = new Uppy({
|
||||||
|
id: this.config.id,
|
||||||
|
autoProceed: this.config.autoStartUploads,
|
||||||
|
|
||||||
|
// need to use upload_type because uppy overrides type with the
|
||||||
|
// actual file type
|
||||||
|
meta: deepMerge(
|
||||||
|
{ upload_type: this.config.type },
|
||||||
|
this.config.additionalParams
|
||||||
|
),
|
||||||
|
|
||||||
|
onBeforeFileAdded: (currentFile) => {
|
||||||
|
const validationOpts = deepMerge(
|
||||||
|
{
|
||||||
|
bypassNewUserRestriction: true,
|
||||||
|
user: this.currentUser,
|
||||||
|
siteSettings: this.siteSettings,
|
||||||
|
validateSize: true,
|
||||||
|
},
|
||||||
|
this.config.validateUploadedFilesOptions
|
||||||
|
);
|
||||||
|
const isValid =
|
||||||
|
validateUploadedFile(currentFile, validationOpts) &&
|
||||||
|
this.config.isUploadedFileAllowed(currentFile);
|
||||||
|
Object.assign(this, {
|
||||||
|
uploadProgress: 0,
|
||||||
|
uploading: isValid && this.config.autoStartUploads,
|
||||||
|
filesAwaitingUpload: !this.config.autoStartUploads,
|
||||||
|
cancellable: isValid && this.config.autoStartUploads,
|
||||||
|
});
|
||||||
|
return isValid;
|
||||||
|
},
|
||||||
|
|
||||||
|
onBeforeUpload: (files) => {
|
||||||
|
let tooMany = false;
|
||||||
|
const fileCount = Object.keys(files).length;
|
||||||
|
const maxFiles =
|
||||||
|
this.config.maxFiles || this.siteSettings.simultaneous_uploads;
|
||||||
|
|
||||||
|
if (this.allowMultipleFiles) {
|
||||||
|
tooMany = maxFiles > 0 && fileCount > maxFiles;
|
||||||
|
} else {
|
||||||
|
tooMany = fileCount > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tooMany) {
|
||||||
|
this.dialog.alert(
|
||||||
|
I18n.t("post.errors.too_many_dragged_and_dropped_files", {
|
||||||
|
count: this.allowMultipleFiles ? maxFiles : 1,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.#reset();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.values(files).forEach((file) => {
|
||||||
|
deepMerge(file.meta, this.config.perFileData?.(file));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.config.uploadDropTargetOptions) {
|
||||||
|
// DropTarget is a UI plugin, only preprocessors must call _useUploadPlugin
|
||||||
|
this.uppyWrapper.uppyInstance.use(
|
||||||
|
DropTarget,
|
||||||
|
this.config.uploadDropTargetOptions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.uppyWrapper.uppyInstance.on("progress", (progress) => {
|
||||||
|
this.uploadProgress = progress;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.uppyWrapper.uppyInstance.on("upload", (data) => {
|
||||||
|
this.uppyWrapper.addNeedProcessing(data.fileIDs.length);
|
||||||
|
const files = data.fileIDs.map((fileId) =>
|
||||||
|
this.uppyWrapper.uppyInstance.getFile(fileId)
|
||||||
|
);
|
||||||
|
this.processing = true;
|
||||||
|
this.cancellable = false;
|
||||||
|
files.forEach((file) => {
|
||||||
|
this.inProgressUploads.push(
|
||||||
|
EmberObject.create({
|
||||||
|
fileName: file.name,
|
||||||
|
id: file.id,
|
||||||
|
progress: 0,
|
||||||
|
extension: file.extension,
|
||||||
|
processing: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.#triggerInProgressUploadsEvent();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.uppyWrapper.uppyInstance.on("upload-progress", (file, progress) => {
|
||||||
|
run(() => {
|
||||||
|
const upload = this.inProgressUploads.find((upl) => upl.id === file.id);
|
||||||
|
if (upload) {
|
||||||
|
const percentage = Math.round(
|
||||||
|
(progress.bytesUploaded / progress.bytesTotal) * 100
|
||||||
|
);
|
||||||
|
upload.set("progress", percentage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.uppyWrapper.uppyInstance.on("upload-success", (file, response) => {
|
||||||
|
if (this.#usingS3Uploads) {
|
||||||
|
Object.assign(this, { uploading: false, processing: true });
|
||||||
|
this.#completeExternalUpload(file)
|
||||||
|
.then((completeResponse) => {
|
||||||
|
this.#removeInProgressUpload(file.id);
|
||||||
|
this.appEvents.trigger(
|
||||||
|
`upload-mixin:${this.config.id}:upload-success`,
|
||||||
|
file.name,
|
||||||
|
completeResponse
|
||||||
|
);
|
||||||
|
this.config.uploadDone(
|
||||||
|
deepMerge(completeResponse, { file_name: file.name })
|
||||||
|
);
|
||||||
|
|
||||||
|
this.#triggerInProgressUploadsEvent();
|
||||||
|
if (this.inProgressUploads.length === 0) {
|
||||||
|
this.#allUploadsComplete();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((errResponse) => {
|
||||||
|
displayErrorForUpload(errResponse, this.siteSettings, file.name);
|
||||||
|
this.#triggerInProgressUploadsEvent();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.#removeInProgressUpload(file.id);
|
||||||
|
const upload = response?.body || {};
|
||||||
|
this.appEvents.trigger(
|
||||||
|
`upload-mixin:${this.config.id}:upload-success`,
|
||||||
|
file.name,
|
||||||
|
upload
|
||||||
|
);
|
||||||
|
this.config.uploadDone(deepMerge(upload, { file_name: file.name }));
|
||||||
|
|
||||||
|
this.#triggerInProgressUploadsEvent();
|
||||||
|
if (this.inProgressUploads.length === 0) {
|
||||||
|
this.#allUploadsComplete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.uppyWrapper.uppyInstance.on(
|
||||||
|
"upload-error",
|
||||||
|
(file, error, response) => {
|
||||||
|
this.#removeInProgressUpload(file.id);
|
||||||
|
displayErrorForUpload(response || error, this.siteSettings, file.name);
|
||||||
|
this.#reset();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.uppyWrapper.uppyInstance.on("file-removed", (file, reason) => {
|
||||||
|
run(() => {
|
||||||
|
// we handle the cancel-all event specifically, so no need
|
||||||
|
// to do anything here. this event is also fired when some files
|
||||||
|
// are handled by an upload handler
|
||||||
|
if (reason === "cancel-all") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.appEvents.trigger(
|
||||||
|
`upload-mixin:${this.config.id}:upload-cancelled`,
|
||||||
|
file.id
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.siteSettings.enable_upload_debug_mode) {
|
||||||
|
this.uppyWrapper.debug.instrumentUploadTimings(
|
||||||
|
this.uppyWrapper.uppyInstance
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO (martin) preventDirectS3Uploads is necessary because some of
|
||||||
|
// the current upload mixin components, for example the emoji uploader,
|
||||||
|
// send the upload to custom endpoints that do fancy things in the rails
|
||||||
|
// controller with the upload or create additional data or records. we
|
||||||
|
// need a nice way to do this on complete-external-upload before we can
|
||||||
|
// allow these other uploaders to go direct to S3.
|
||||||
|
if (
|
||||||
|
this.siteSettings.enable_direct_s3_uploads &&
|
||||||
|
!this.config.preventDirectS3Uploads &&
|
||||||
|
!this.config.useChunkedUploads
|
||||||
|
) {
|
||||||
|
if (this.config.useMultipartUploadsIfAvailable) {
|
||||||
|
new UppyS3Multipart(getOwner(this), {
|
||||||
|
uploadRootPath: this.config.uploadRootPath,
|
||||||
|
uppyWrapper: this.uppyWrapper,
|
||||||
|
errorHandler: this.config.uploadError,
|
||||||
|
}).apply(this.uppyWrapper.uppyInstance);
|
||||||
|
} else {
|
||||||
|
this.#useS3Uploads();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.config.useChunkedUploads) {
|
||||||
|
this.#useChunkedUploads();
|
||||||
|
} else {
|
||||||
|
this.#useXHRUploads();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.uppyWrapper.uppyInstance.on("cancel-all", () => {
|
||||||
|
this.appEvents.trigger(
|
||||||
|
`upload-mixin:${this.config.id}:uploads-cancelled`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.inProgressUploads.length) {
|
||||||
|
this.inProgressUploads.length = 0; // Clear array in-place
|
||||||
|
this.#triggerInProgressUploadsEvent();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.appEvents.on(
|
||||||
|
`upload-mixin:${this.config.id}:add-files`,
|
||||||
|
this.addFiles
|
||||||
|
);
|
||||||
|
this.appEvents.on(
|
||||||
|
`upload-mixin:${this.config.id}:cancel-upload`,
|
||||||
|
this._cancelSingleUpload
|
||||||
|
);
|
||||||
|
this.config.uppyReady?.();
|
||||||
|
|
||||||
|
// It is important that the UppyChecksum preprocessor is the last one to
|
||||||
|
// be added; the preprocessors are run in order and since other preprocessors
|
||||||
|
// may modify the file (e.g. the UppyMediaOptimization one), we need to
|
||||||
|
// checksum once we are sure the file data has "settled".
|
||||||
|
this.uppyWrapper.useUploadPlugin(UppyChecksum, {
|
||||||
|
capabilities: this.capabilities,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#triggerInProgressUploadsEvent() {
|
||||||
|
this.config.onProgressUploadsChanged?.(this.inProgressUploads);
|
||||||
|
this.appEvents.trigger(
|
||||||
|
`upload-mixin:${this.config.id}:in-progress-uploads`,
|
||||||
|
this.inProgressUploads
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If auto upload is disabled, use this function to start the upload process.
|
||||||
|
*/
|
||||||
|
startUpload() {
|
||||||
|
if (!this.filesAwaitingUpload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.uppyWrapper.uppyInstance?.getFiles().length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.uploading = true;
|
||||||
|
return this.uppyWrapper.uppyInstance?.upload();
|
||||||
|
}
|
||||||
|
|
||||||
|
#useXHRUploads() {
|
||||||
|
this.uppyWrapper.uppyInstance.use(XHRUpload, {
|
||||||
|
endpoint: this.#xhrUploadUrl(),
|
||||||
|
headers: () => ({
|
||||||
|
"X-CSRF-Token": this.session.csrfToken,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#useChunkedUploads() {
|
||||||
|
this.uppyWrapper.uppyInstance.use(UppyChunkedUploader, {
|
||||||
|
url: this.#xhrUploadUrl(),
|
||||||
|
headers: {
|
||||||
|
"X-CSRF-Token": this.session.csrfToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#useS3Uploads() {
|
||||||
|
this.#usingS3Uploads = true;
|
||||||
|
this.uppyWrapper.uppyInstance.use(AwsS3, {
|
||||||
|
getUploadParameters: (file) => {
|
||||||
|
const data = {
|
||||||
|
file_name: file.name,
|
||||||
|
file_size: file.size,
|
||||||
|
type: this.config.type,
|
||||||
|
};
|
||||||
|
|
||||||
|
// the sha1 checksum is set by the UppyChecksum plugin, except
|
||||||
|
// for in cases where the browser does not support the required
|
||||||
|
// crypto mechanisms or an error occurs. it is an additional layer
|
||||||
|
// of security, and not required.
|
||||||
|
if (file.meta.sha1_checksum) {
|
||||||
|
data.metadata = { "sha1-checksum": file.meta.sha1_checksum };
|
||||||
|
}
|
||||||
|
|
||||||
|
return ajax(`${this.config.uploadRootPath}/generate-presigned-put`, {
|
||||||
|
type: "POST",
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
this.uppyWrapper.uppyInstance.setFileMeta(file.id, {
|
||||||
|
uniqueUploadIdentifier: response.unique_identifier,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
method: "put",
|
||||||
|
url: response.url,
|
||||||
|
headers: {
|
||||||
|
...response.signed_headers,
|
||||||
|
"Content-Type": file.type,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch((errResponse) => {
|
||||||
|
displayErrorForUpload(errResponse, this.siteSettings, file.name);
|
||||||
|
this.#reset();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#xhrUploadUrl() {
|
||||||
|
const uploadUrl = this.config.uploadUrl || this.config.uploadRootPath;
|
||||||
|
return getUrl(uploadUrl) + ".json?client_id=" + this.messageBus?.clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bindFileInputChange() {
|
||||||
|
this.#fileInputEventListener = bindFileInputChangeListener(
|
||||||
|
this._fileInputEl,
|
||||||
|
this.addFiles
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
_cancelSingleUpload(data) {
|
||||||
|
this.uppyWrapper.uppyInstance.removeFile(data.fileId);
|
||||||
|
this.#removeInProgressUpload(data.fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
async addFiles(files, opts = {}) {
|
||||||
|
if (!this.session.csrfToken) {
|
||||||
|
await updateCsrfToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
files = Array.isArray(files) ? files : [files];
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.uppyWrapper.uppyInstance.addFiles(
|
||||||
|
files.map((file) => {
|
||||||
|
return {
|
||||||
|
source: this.config.id,
|
||||||
|
name: file.name,
|
||||||
|
type: file.type,
|
||||||
|
data: file,
|
||||||
|
meta: { pasted: opts.pasted },
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
warn(`error adding files to uppy: ${err}`, {
|
||||||
|
id: "discourse.upload.uppy-add-files-error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#completeExternalUpload(file) {
|
||||||
|
return ajax(`${this.config.uploadRootPath}/complete-external-upload`, {
|
||||||
|
type: "POST",
|
||||||
|
data: deepMerge(
|
||||||
|
{ unique_identifier: file.meta.uniqueUploadIdentifier },
|
||||||
|
this.config.additionalParams
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#reset() {
|
||||||
|
this.uppyWrapper.uppyInstance?.cancelAll();
|
||||||
|
Object.assign(this, {
|
||||||
|
uploading: false,
|
||||||
|
processing: false,
|
||||||
|
cancellable: false,
|
||||||
|
uploadProgress: 0,
|
||||||
|
filesAwaitingUpload: false,
|
||||||
|
});
|
||||||
|
this._fileInputEl.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
#removeInProgressUpload(fileId) {
|
||||||
|
if (this.isDestroyed || this.isDestroying) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = this.inProgressUploads.findIndex((upl) => upl.id === fileId);
|
||||||
|
if (index === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.inProgressUploads.splice(index, 1);
|
||||||
|
this.#triggerInProgressUploadsEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
#allUploadsComplete() {
|
||||||
|
if (this.isDestroying || this.isDestroyed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.appEvents.trigger(
|
||||||
|
`upload-mixin:${this.config.id}:all-uploads-complete`
|
||||||
|
);
|
||||||
|
this.#reset();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,16 +1,16 @@
|
||||||
import Mixin from "@ember/object/mixin";
|
import { setOwner } from "@ember/owner";
|
||||||
import UploadDebugging from "discourse/mixins/upload-debugging";
|
import UppyUploadDebugging from "./upload-debugging";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use this mixin with any component that needs to upload files or images
|
* Use this class whenever you need to upload files or images
|
||||||
* with Uppy. This mixin makes it easier to tell Uppy to use certain uppy plugins
|
* with Uppy. The class makes it easier to tell Uppy to use certain uppy plugins
|
||||||
* as well as tracking all of the state of preprocessor plugins. For example,
|
* as well as tracking all of the state of preprocessor plugins. For example,
|
||||||
* you may have multiple preprocessors:
|
* you may have multiple preprocessors:
|
||||||
*
|
*
|
||||||
* - UppyMediaOptimization
|
* - UppyMediaOptimization
|
||||||
* - UppyChecksum
|
* - UppyChecksum
|
||||||
*
|
*
|
||||||
* Once installed with _useUploadPlugin(PluginClass, opts), we track the following
|
* Once installed with useUploadPlugin(PluginClass, opts), we track the following
|
||||||
* status for every preprocessor plugin:
|
* status for every preprocessor plugin:
|
||||||
*
|
*
|
||||||
* - needProcessing - The total number of files that have been added to uppy that
|
* - needProcessing - The total number of files that have been added to uppy that
|
||||||
|
@ -21,28 +21,37 @@ import UploadDebugging from "discourse/mixins/upload-debugging";
|
||||||
* which is determined by the preprocess-complete event.
|
* which is determined by the preprocess-complete event.
|
||||||
* - allComplete - Whether all files have completed the preprocessing for the plugin.
|
* - allComplete - Whether all files have completed the preprocessing for the plugin.
|
||||||
*
|
*
|
||||||
* There is a caveat - you must call _addNeedProcessing(data.fileIDs.length) when
|
* There is a caveat - you must call addNeedProcessing(data.fileIDs.length) when
|
||||||
* handling the "upload" event with uppy, otherwise this mixin does not know how
|
* handling the "upload" event with uppy, otherwise this mixin does not know how
|
||||||
* many files need to be processed.
|
* many files need to be processed.
|
||||||
*
|
*
|
||||||
* If you need to do something else on progress or completion of preprocessors,
|
* If you need to do something else on progress or completion of preprocessors,
|
||||||
* hook into the _onPreProcessProgress(callback) or _onPreProcessComplete(callback, allCompleteCallback)
|
* hook into the onPreProcessProgress(callback) or onPreProcessComplete(callback, allCompleteCallback)
|
||||||
* functions. Note the _onPreProcessComplete function takes a second callback
|
* functions. Note the onPreProcessComplete function takes a second callback
|
||||||
* that is fired only when _all_ of the files have been preprocessed for all
|
* that is fired only when _all_ of the files have been preprocessed for all
|
||||||
* preprocessor plugins.
|
* preprocessor plugins.
|
||||||
*
|
*
|
||||||
* A preprocessor is considered complete if the completeProcessing count is
|
* A preprocessor is considered complete if the completeProcessing count is
|
||||||
* equal to needProcessing, at which point the allComplete prop is set to true.
|
* equal to needProcessing, at which point the allComplete prop is set to true.
|
||||||
* If all preprocessor plugins have allComplete set to true, then the allCompleteCallback
|
* If all preprocessor plugins have allComplete set to true, then the allCompleteCallback
|
||||||
* is called for _onPreProcessComplete.
|
* is called for onPreProcessComplete.
|
||||||
*
|
*
|
||||||
* To completely reset the preprocessor state for all plugins, call _resetPreProcessors.
|
* To completely reset the preprocessor state for all plugins, call resetPreProcessors.
|
||||||
*
|
*
|
||||||
* See ComposerUploadUppy for an example of a component using this mixin.
|
* See ComposerUploadUppy for an example of a component using this class.
|
||||||
*/
|
*/
|
||||||
export default Mixin.create(UploadDebugging, {
|
export default class UppyWrapper {
|
||||||
_useUploadPlugin(pluginClass, opts = {}) {
|
debug;
|
||||||
if (!this._uppyInstance) {
|
uppyInstance;
|
||||||
|
#preProcessorStatus = {};
|
||||||
|
|
||||||
|
constructor(owner) {
|
||||||
|
setOwner(this, owner);
|
||||||
|
this.debug = new UppyUploadDebugging(owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
useUploadPlugin(pluginClass, opts = {}) {
|
||||||
|
if (!this.uppyInstance) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +70,7 @@ export default Mixin.create(UploadDebugging, {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._uppyInstance.use(
|
this.uppyInstance.use(
|
||||||
pluginClass,
|
pluginClass,
|
||||||
Object.assign(opts, {
|
Object.assign(opts, {
|
||||||
id: pluginClass.pluginId,
|
id: pluginClass.pluginId,
|
||||||
|
@ -70,9 +79,9 @@ export default Mixin.create(UploadDebugging, {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (pluginClass.pluginType === "preprocessor") {
|
if (pluginClass.pluginType === "preprocessor") {
|
||||||
this._trackPreProcessorStatus(pluginClass.pluginId);
|
this.#trackPreProcessorStatus(pluginClass.pluginId);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
// NOTE: This and _onPreProcessComplete will need to be tweaked
|
// NOTE: This and _onPreProcessComplete will need to be tweaked
|
||||||
// if we ever add support for "determinate" preprocessors for uppy, which
|
// if we ever add support for "determinate" preprocessors for uppy, which
|
||||||
|
@ -80,21 +89,19 @@ export default Mixin.create(UploadDebugging, {
|
||||||
// state ("indeterminate").
|
// state ("indeterminate").
|
||||||
//
|
//
|
||||||
// See: https://uppy.io/docs/writing-plugins/#Progress-events
|
// See: https://uppy.io/docs/writing-plugins/#Progress-events
|
||||||
_onPreProcessProgress(callback) {
|
onPreProcessProgress(callback) {
|
||||||
this._uppyInstance.on("preprocess-progress", (file, progress, pluginId) => {
|
this.uppyInstance.on("preprocess-progress", (file, progress, pluginId) => {
|
||||||
this._consoleDebug(
|
this.debug.log(`[${pluginId}] processing file ${file.name} (${file.id})`);
|
||||||
`[${pluginId}] processing file ${file.name} (${file.id})`
|
|
||||||
);
|
|
||||||
|
|
||||||
this._preProcessorStatus[pluginId].activeProcessing++;
|
this.#preProcessorStatus[pluginId].activeProcessing++;
|
||||||
|
|
||||||
callback(file);
|
callback(file);
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
_onPreProcessComplete(callback, allCompleteCallback = null) {
|
onPreProcessComplete(callback, allCompleteCallback = null) {
|
||||||
this._uppyInstance.on("preprocess-complete", (file, skipped, pluginId) => {
|
this.uppyInstance.on("preprocess-complete", (file, skipped, pluginId) => {
|
||||||
this._consoleDebug(
|
this.debug.log(
|
||||||
`[${pluginId}] ${skipped ? "skipped" : "completed"} processing file ${
|
`[${pluginId}] ${skipped ? "skipped" : "completed"} processing file ${
|
||||||
file.name
|
file.name
|
||||||
} (${file.id})`
|
} (${file.id})`
|
||||||
|
@ -102,63 +109,60 @@ export default Mixin.create(UploadDebugging, {
|
||||||
|
|
||||||
callback(file);
|
callback(file);
|
||||||
|
|
||||||
this._completePreProcessing(pluginId, (allComplete) => {
|
this.#completePreProcessing(pluginId, (allComplete) => {
|
||||||
if (allComplete) {
|
if (allComplete) {
|
||||||
this._consoleDebug("[uppy] All upload preprocessors complete!");
|
this.debug.log("[uppy] All upload preprocessors complete!");
|
||||||
if (allCompleteCallback) {
|
if (allCompleteCallback) {
|
||||||
allCompleteCallback();
|
allCompleteCallback();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
_resetPreProcessors() {
|
resetPreProcessors() {
|
||||||
this._eachPreProcessor((pluginId) => {
|
this.#eachPreProcessor((pluginId) => {
|
||||||
this._preProcessorStatus[pluginId] = {
|
this.#preProcessorStatus[pluginId] = {
|
||||||
needProcessing: 0,
|
needProcessing: 0,
|
||||||
activeProcessing: 0,
|
activeProcessing: 0,
|
||||||
completeProcessing: 0,
|
completeProcessing: 0,
|
||||||
allComplete: false,
|
allComplete: false,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
_trackPreProcessorStatus(pluginId) {
|
#trackPreProcessorStatus(pluginId) {
|
||||||
if (!this._preProcessorStatus) {
|
this.#preProcessorStatus[pluginId] = {
|
||||||
this._preProcessorStatus = {};
|
|
||||||
}
|
|
||||||
this._preProcessorStatus[pluginId] = {
|
|
||||||
needProcessing: 0,
|
needProcessing: 0,
|
||||||
activeProcessing: 0,
|
activeProcessing: 0,
|
||||||
completeProcessing: 0,
|
completeProcessing: 0,
|
||||||
allComplete: false,
|
allComplete: false,
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
|
|
||||||
_addNeedProcessing(fileCount) {
|
addNeedProcessing(fileCount) {
|
||||||
this._eachPreProcessor((pluginName, status) => {
|
this.#eachPreProcessor((pluginName, status) => {
|
||||||
status.needProcessing += fileCount;
|
status.needProcessing += fileCount;
|
||||||
status.allComplete = false;
|
status.allComplete = false;
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
_eachPreProcessor(cb) {
|
#eachPreProcessor(cb) {
|
||||||
for (const [pluginId, status] of Object.entries(this._preProcessorStatus)) {
|
for (const [pluginId, status] of Object.entries(this.#preProcessorStatus)) {
|
||||||
cb(pluginId, status);
|
cb(pluginId, status);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
_allPreprocessorsComplete() {
|
#allPreprocessorsComplete() {
|
||||||
let completed = [];
|
let completed = [];
|
||||||
this._eachPreProcessor((pluginId, status) => {
|
this.#eachPreProcessor((pluginId, status) => {
|
||||||
completed.push(status.allComplete);
|
completed.push(status.allComplete);
|
||||||
});
|
});
|
||||||
return completed.every(Boolean);
|
return completed.every(Boolean);
|
||||||
},
|
}
|
||||||
|
|
||||||
_completePreProcessing(pluginId, callback) {
|
#completePreProcessing(pluginId, callback) {
|
||||||
const preProcessorStatus = this._preProcessorStatus[pluginId];
|
const preProcessorStatus = this.#preProcessorStatus[pluginId];
|
||||||
preProcessorStatus.activeProcessing--;
|
preProcessorStatus.activeProcessing--;
|
||||||
preProcessorStatus.completeProcessing++;
|
preProcessorStatus.completeProcessing++;
|
||||||
|
|
||||||
|
@ -170,11 +174,11 @@ export default Mixin.create(UploadDebugging, {
|
||||||
preProcessorStatus.needProcessing = 0;
|
preProcessorStatus.needProcessing = 0;
|
||||||
preProcessorStatus.completeProcessing = 0;
|
preProcessorStatus.completeProcessing = 0;
|
||||||
|
|
||||||
if (this._allPreprocessorsComplete()) {
|
if (this.#allPreprocessorsComplete()) {
|
||||||
callback(true);
|
callback(true);
|
||||||
} else {
|
} else {
|
||||||
callback(false);
|
callback(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
});
|
}
|
|
@ -1,11 +1,14 @@
|
||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
import { warn } from "@ember/debug";
|
import { warn } from "@ember/debug";
|
||||||
import EmberObject from "@ember/object";
|
import EmberObject from "@ember/object";
|
||||||
import { setOwner } from "@ember/owner";
|
import { getOwner, setOwner } from "@ember/owner";
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
import Uppy from "@uppy/core";
|
import Uppy from "@uppy/core";
|
||||||
|
import XHRUpload from "@uppy/xhr-upload";
|
||||||
import { isVideo } from "discourse/lib/uploads";
|
import { isVideo } from "discourse/lib/uploads";
|
||||||
|
import UppyS3Multipart from "discourse/lib/uppy/s3-multipart";
|
||||||
import UppyUploadMixin from "discourse/mixins/uppy-upload";
|
import UppyUploadMixin from "discourse/mixins/uppy-upload";
|
||||||
|
import getUrl from "discourse-common/helpers/get-url";
|
||||||
import I18n from "discourse-i18n";
|
import I18n from "discourse-i18n";
|
||||||
|
|
||||||
// It is not ideal that this is a class extending a mixin, but in the case
|
// It is not ideal that this is a class extending a mixin, but in the case
|
||||||
|
@ -32,11 +35,14 @@ export default class ComposerVideoThumbnailUppy extends EmberObject.extend(
|
||||||
uploadTargetBound = false;
|
uploadTargetBound = false;
|
||||||
useUploadPlaceholders = true;
|
useUploadPlaceholders = true;
|
||||||
capabilities = null;
|
capabilities = null;
|
||||||
|
id = "composer-video";
|
||||||
|
uploadDone = () => {};
|
||||||
|
|
||||||
constructor(owner) {
|
constructor(owner) {
|
||||||
super(...arguments);
|
super(...arguments);
|
||||||
this.capabilities = owner.lookup("service:capabilities");
|
this.capabilities = owner.lookup("service:capabilities");
|
||||||
setOwner(this, owner);
|
setOwner(this, owner);
|
||||||
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
generateVideoThumbnail(videoFile, uploadUrl, callback) {
|
generateVideoThumbnail(videoFile, uploadUrl, callback) {
|
||||||
|
@ -113,13 +119,27 @@ export default class ComposerVideoThumbnailUppy extends EmberObject.extend(
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.siteSettings.enable_upload_debug_mode) {
|
if (this.siteSettings.enable_upload_debug_mode) {
|
||||||
this._instrumentUploadTimings();
|
this.uppyUpload.uppyWrapper.debug.instrumentUploadTimings(
|
||||||
|
this._uppyInstance
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.siteSettings.enable_direct_s3_uploads) {
|
if (this.siteSettings.enable_direct_s3_uploads) {
|
||||||
this._useS3MultipartUploads();
|
new UppyS3Multipart(getOwner(this), {
|
||||||
|
uploadRootPath: this.uploadRootPath,
|
||||||
|
uppyWrapper: this.uppyUpload.uppyWrapper,
|
||||||
|
errorHandler: this._handleUploadError,
|
||||||
|
}).apply(this._uppyInstance);
|
||||||
} else {
|
} else {
|
||||||
this._useXHRUploads();
|
this._uppyInstance.use(XHRUpload, {
|
||||||
|
endpoint:
|
||||||
|
getUrl("/uploads") +
|
||||||
|
".json?client_id=" +
|
||||||
|
this.messageBus?.clientId,
|
||||||
|
headers: () => ({
|
||||||
|
"X-CSRF-Token": this.session.csrfToken,
|
||||||
|
}),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this._uppyInstance.on("upload", () => {
|
this._uppyInstance.on("upload", () => {
|
||||||
|
|
|
@ -1,523 +1,131 @@
|
||||||
import { warn } from "@ember/debug";
|
import { alias, or } from "@ember/object/computed";
|
||||||
import EmberObject from "@ember/object";
|
import { readOnly } from "@ember/object/lib/computed/computed_macros";
|
||||||
import { or } from "@ember/object/computed";
|
|
||||||
import Mixin from "@ember/object/mixin";
|
import Mixin from "@ember/object/mixin";
|
||||||
import { run } from "@ember/runloop";
|
import { getOwner } from "@ember/owner";
|
||||||
import { service } from "@ember/service";
|
import UppyUpload from "discourse/lib/uppy/uppy-upload";
|
||||||
import AwsS3 from "@uppy/aws-s3";
|
|
||||||
import Uppy from "@uppy/core";
|
|
||||||
import DropTarget from "@uppy/drop-target";
|
|
||||||
import XHRUpload from "@uppy/xhr-upload";
|
|
||||||
import { ajax, updateCsrfToken } from "discourse/lib/ajax";
|
|
||||||
import {
|
|
||||||
bindFileInputChangeListener,
|
|
||||||
displayErrorForUpload,
|
|
||||||
validateUploadedFile,
|
|
||||||
} from "discourse/lib/uploads";
|
|
||||||
import UppyChecksum from "discourse/lib/uppy-checksum-plugin";
|
|
||||||
import UppyChunkedUploader from "discourse/lib/uppy-chunked-uploader-plugin";
|
|
||||||
import ExtendableUploader from "discourse/mixins/extendable-uploader";
|
|
||||||
import UppyS3Multipart from "discourse/mixins/uppy-s3-multipart";
|
|
||||||
import getUrl from "discourse-common/lib/get-url";
|
|
||||||
import { deepMerge } from "discourse-common/lib/object";
|
import { deepMerge } from "discourse-common/lib/object";
|
||||||
import { bind, on } from "discourse-common/utils/decorators";
|
|
||||||
import I18n from "discourse-i18n";
|
|
||||||
|
|
||||||
export const HUGE_FILE_THRESHOLD_BYTES = 104_857_600; // 100MB
|
export { HUGE_FILE_THRESHOLD_BYTES } from "discourse/lib/uppy/uppy-upload";
|
||||||
|
|
||||||
export default Mixin.create(UppyS3Multipart, ExtendableUploader, {
|
/**
|
||||||
dialog: service(),
|
* @deprecated
|
||||||
|
*
|
||||||
|
* This mixin exists only for backwards-compatibility.
|
||||||
|
*
|
||||||
|
* New implementations should use `lib/uppy/uppy-upload` directly.
|
||||||
|
*/
|
||||||
|
export default Mixin.create({
|
||||||
|
uppyUpload: null,
|
||||||
|
|
||||||
|
_uppyInstance: alias("uppyUpload.uppyWrapper.uppyInstance"),
|
||||||
|
uploadProgress: readOnly("uppyUpload.uploadProgress"),
|
||||||
|
inProgressUploads: readOnly("uppyUpload.inProgressUploads"),
|
||||||
|
filesAwaitingUpload: readOnly("uppyUpload.filesAwaitingUpload"),
|
||||||
|
cancellable: readOnly("uppyUpload.cancellable"),
|
||||||
|
uploadingOrProcessing: or("uppyUpload.uploading", "uppyUpload.processing"),
|
||||||
|
fileInputEl: alias("uppyUpload._fileInputEl"),
|
||||||
|
allowMultipleFiles: readOnly("uppyUpload.allowMultipleFiles"),
|
||||||
|
|
||||||
|
_addFiles: readOnly("uppyUpload.addFiles"),
|
||||||
|
_startUpload: readOnly("uppyUpload.startUpload"),
|
||||||
|
|
||||||
|
// Some places are two-way-binding these properties into parent components
|
||||||
|
// so we can't use computed properties as aliases.
|
||||||
|
// Instead, we have simple properties, with observers that update them when the underlying properties change.
|
||||||
uploading: false,
|
uploading: false,
|
||||||
uploadProgress: 0,
|
processing: false,
|
||||||
_uppyInstance: null,
|
|
||||||
autoStartUploads: true,
|
|
||||||
inProgressUploads: null,
|
|
||||||
id: null,
|
|
||||||
uploadRootPath: "/uploads",
|
|
||||||
fileInputSelector: ".hidden-upload-field",
|
|
||||||
autoFindInput: true,
|
|
||||||
|
|
||||||
uploadDone() {
|
init() {
|
||||||
warn("You should implement `uploadDone`", {
|
this.uppyUpload = new UppyUpload(getOwner(this), configShim(this));
|
||||||
id: "discourse.upload.missing-upload-done",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
validateUploadedFilesOptions() {
|
this.addObserver("uppyUpload.uploading", () =>
|
||||||
return {};
|
this.set("uploading", this.uppyUpload.uploading)
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Overridable for custom file validations, executed before uploading.
|
|
||||||
*
|
|
||||||
* @param {object} file
|
|
||||||
*
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
isUploadedFileAllowed() {
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
uploadingOrProcessing: or("uploading", "processing"),
|
|
||||||
|
|
||||||
@on("willDestroyElement")
|
|
||||||
_destroy() {
|
|
||||||
if (this.messageBus) {
|
|
||||||
this.messageBus.unsubscribe(`/uploads/${this.type}`);
|
|
||||||
}
|
|
||||||
this.fileInputEl?.removeEventListener(
|
|
||||||
"change",
|
|
||||||
this.fileInputEventListener
|
|
||||||
);
|
);
|
||||||
this.appEvents.off(`upload-mixin:${this.id}:add-files`, this._addFiles);
|
this.addObserver("uppyUpload.processing", () =>
|
||||||
this.appEvents.off(
|
this.set("processing", this.uppyUpload.processing)
|
||||||
`upload-mixin:${this.id}:cancel-upload`,
|
|
||||||
this._cancelSingleUpload
|
|
||||||
);
|
);
|
||||||
this._uppyInstance?.close();
|
|
||||||
this._uppyInstance = null;
|
this._super();
|
||||||
},
|
},
|
||||||
|
|
||||||
@on("didInsertElement")
|
didInsertElement() {
|
||||||
_initialize() {
|
if (this.autoFindInput ?? true) {
|
||||||
if (this.autoFindInput) {
|
this._fileInputEl = this.element.querySelector(
|
||||||
this.setProperties({
|
this.fileInputSelector || ".hidden-upload-field"
|
||||||
fileInputEl: this.element.querySelector(this.fileInputSelector),
|
|
||||||
});
|
|
||||||
} else if (!this.fileInputEl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.set("allowMultipleFiles", this.fileInputEl.multiple);
|
|
||||||
this.set("inProgressUploads", []);
|
|
||||||
|
|
||||||
this._bindFileInputChange();
|
|
||||||
|
|
||||||
if (!this.id) {
|
|
||||||
warn(
|
|
||||||
"uppy needs a unique id, pass one in to the component implementing this mixin",
|
|
||||||
{
|
|
||||||
id: "discourse.upload.missing-id",
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}
|
} else if (!this._fileInputEl) {
|
||||||
|
|
||||||
this._uppyInstance = new Uppy({
|
|
||||||
id: this.id,
|
|
||||||
autoProceed: this.autoStartUploads,
|
|
||||||
|
|
||||||
// need to use upload_type because uppy overrides type with the
|
|
||||||
// actual file type
|
|
||||||
meta: deepMerge(
|
|
||||||
{ upload_type: this.type },
|
|
||||||
this.additionalParams || {},
|
|
||||||
this.data || {}
|
|
||||||
),
|
|
||||||
|
|
||||||
onBeforeFileAdded: (currentFile) => {
|
|
||||||
const validationOpts = deepMerge(
|
|
||||||
{
|
|
||||||
bypassNewUserRestriction: true,
|
|
||||||
user: this.currentUser,
|
|
||||||
siteSettings: this.siteSettings,
|
|
||||||
validateSize: true,
|
|
||||||
},
|
|
||||||
this.validateUploadedFilesOptions()
|
|
||||||
);
|
|
||||||
const isValid =
|
|
||||||
validateUploadedFile(currentFile, validationOpts) &&
|
|
||||||
this.isUploadedFileAllowed(currentFile);
|
|
||||||
this.setProperties({
|
|
||||||
uploadProgress: 0,
|
|
||||||
uploading: isValid && this.autoStartUploads,
|
|
||||||
filesAwaitingUpload: !this.autoStartUploads,
|
|
||||||
cancellable: isValid && this.autoStartUploads,
|
|
||||||
});
|
|
||||||
return isValid;
|
|
||||||
},
|
|
||||||
|
|
||||||
onBeforeUpload: (files) => {
|
|
||||||
let tooMany = false;
|
|
||||||
const fileCount = Object.keys(files).length;
|
|
||||||
const maxFiles =
|
|
||||||
this.maxFiles || this.siteSettings.simultaneous_uploads;
|
|
||||||
|
|
||||||
if (this.allowMultipleFiles) {
|
|
||||||
tooMany = maxFiles > 0 && fileCount > maxFiles;
|
|
||||||
} else {
|
|
||||||
tooMany = fileCount > 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tooMany) {
|
|
||||||
this.dialog.alert(
|
|
||||||
I18n.t("post.errors.too_many_dragged_and_dropped_files", {
|
|
||||||
count: this.allowMultipleFiles ? maxFiles : 1,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
this._reset();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._perFileData) {
|
|
||||||
Object.values(files).forEach((file) => {
|
|
||||||
deepMerge(file.meta, this._perFileData());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// DropTarget is a UI plugin, only preprocessors must call _useUploadPlugin
|
|
||||||
this._uppyInstance.use(DropTarget, this._uploadDropTargetOptions());
|
|
||||||
|
|
||||||
this._uppyInstance.on("progress", (progress) => {
|
|
||||||
if (this.isDestroying || this.isDestroyed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.set("uploadProgress", progress);
|
|
||||||
});
|
|
||||||
|
|
||||||
this._uppyInstance.on("upload", (data) => {
|
|
||||||
if (this.isDestroying || this.isDestroyed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._addNeedProcessing(data.fileIDs.length);
|
|
||||||
const files = data.fileIDs.map((fileId) =>
|
|
||||||
this._uppyInstance.getFile(fileId)
|
|
||||||
);
|
|
||||||
this.setProperties({
|
|
||||||
processing: true,
|
|
||||||
cancellable: false,
|
|
||||||
});
|
|
||||||
files.forEach((file) => {
|
|
||||||
// The inProgressUploads is meant to be used to display these uploads
|
|
||||||
// in a UI, and Ember will only update the array in the UI if pushObject
|
|
||||||
// is used to notify it.
|
|
||||||
this.inProgressUploads.pushObject(
|
|
||||||
EmberObject.create({
|
|
||||||
fileName: file.name,
|
|
||||||
id: file.id,
|
|
||||||
progress: 0,
|
|
||||||
extension: file.extension,
|
|
||||||
processing: false,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
this._triggerInProgressUploadsEvent();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this._uppyInstance.on("upload-progress", (file, progress) => {
|
|
||||||
run(() => {
|
|
||||||
if (this.isDestroying || this.isDestroyed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const upload = this.inProgressUploads.find((upl) => upl.id === file.id);
|
|
||||||
if (upload) {
|
|
||||||
const percentage = Math.round(
|
|
||||||
(progress.bytesUploaded / progress.bytesTotal) * 100
|
|
||||||
);
|
|
||||||
upload.set("progress", percentage);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this._uppyInstance.on("upload-success", (file, response) => {
|
|
||||||
if (this.usingS3Uploads) {
|
|
||||||
this.setProperties({ uploading: false, processing: true });
|
|
||||||
this._completeExternalUpload(file)
|
|
||||||
.then((completeResponse) => {
|
|
||||||
this._removeInProgressUpload(file.id);
|
|
||||||
this.appEvents.trigger(
|
|
||||||
`upload-mixin:${this.id}:upload-success`,
|
|
||||||
file.name,
|
|
||||||
completeResponse
|
|
||||||
);
|
|
||||||
this.uploadDone(
|
|
||||||
deepMerge(completeResponse, { file_name: file.name })
|
|
||||||
);
|
|
||||||
|
|
||||||
this._triggerInProgressUploadsEvent();
|
|
||||||
if (this.inProgressUploads.length === 0) {
|
|
||||||
this._allUploadsComplete();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((errResponse) => {
|
|
||||||
displayErrorForUpload(errResponse, this.siteSettings, file.name);
|
|
||||||
this._triggerInProgressUploadsEvent();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this._removeInProgressUpload(file.id);
|
|
||||||
const upload = response?.body || {};
|
|
||||||
this.appEvents.trigger(
|
|
||||||
`upload-mixin:${this.id}:upload-success`,
|
|
||||||
file.name,
|
|
||||||
upload
|
|
||||||
);
|
|
||||||
this.uploadDone(deepMerge(upload, { file_name: file.name }));
|
|
||||||
|
|
||||||
this._triggerInProgressUploadsEvent();
|
|
||||||
if (this.inProgressUploads.length === 0) {
|
|
||||||
this._allUploadsComplete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this._uppyInstance.on("upload-error", (file, error, response) => {
|
|
||||||
this._removeInProgressUpload(file.id);
|
|
||||||
displayErrorForUpload(response || error, this.siteSettings, file.name);
|
|
||||||
this._reset();
|
|
||||||
});
|
|
||||||
|
|
||||||
this._uppyInstance.on("file-removed", (file, reason) => {
|
|
||||||
run(() => {
|
|
||||||
// we handle the cancel-all event specifically, so no need
|
|
||||||
// to do anything here. this event is also fired when some files
|
|
||||||
// are handled by an upload handler
|
|
||||||
if (reason === "cancel-all") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.appEvents.trigger(
|
|
||||||
`upload-mixin:${this.id}:upload-cancelled`,
|
|
||||||
file.id
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.siteSettings.enable_upload_debug_mode) {
|
|
||||||
this._instrumentUploadTimings();
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO (martin) preventDirectS3Uploads is necessary because some of
|
|
||||||
// the current upload mixin components, for example the emoji uploader,
|
|
||||||
// send the upload to custom endpoints that do fancy things in the rails
|
|
||||||
// controller with the upload or create additional data or records. we
|
|
||||||
// need a nice way to do this on complete-external-upload before we can
|
|
||||||
// allow these other uploaders to go direct to S3.
|
|
||||||
if (
|
|
||||||
this.siteSettings.enable_direct_s3_uploads &&
|
|
||||||
!this.preventDirectS3Uploads &&
|
|
||||||
!this.useChunkedUploads
|
|
||||||
) {
|
|
||||||
if (this.useMultipartUploadsIfAvailable) {
|
|
||||||
this._useS3MultipartUploads();
|
|
||||||
} else {
|
|
||||||
this._useS3Uploads();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (this.useChunkedUploads) {
|
|
||||||
this._useChunkedUploads();
|
|
||||||
} else {
|
|
||||||
this._useXHRUploads();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this._uppyInstance.on("cancel-all", () => {
|
|
||||||
this.appEvents.trigger(`upload-mixin:${this.id}:uploads-cancelled`);
|
|
||||||
if (!this.isDestroyed && !this.isDestroying) {
|
|
||||||
if (this.inProgressUploads.length) {
|
|
||||||
this.set("inProgressUploads", []);
|
|
||||||
this._triggerInProgressUploadsEvent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.appEvents.on(`upload-mixin:${this.id}:add-files`, this._addFiles);
|
|
||||||
this.appEvents.on(
|
|
||||||
`upload-mixin:${this.id}:cancel-upload`,
|
|
||||||
this._cancelSingleUpload
|
|
||||||
);
|
|
||||||
this._uppyReady();
|
|
||||||
|
|
||||||
// It is important that the UppyChecksum preprocessor is the last one to
|
|
||||||
// be added; the preprocessors are run in order and since other preprocessors
|
|
||||||
// may modify the file (e.g. the UppyMediaOptimization one), we need to
|
|
||||||
// checksum once we are sure the file data has "settled".
|
|
||||||
this._useUploadPlugin(UppyChecksum, { capabilities: this.capabilities });
|
|
||||||
},
|
|
||||||
|
|
||||||
_triggerInProgressUploadsEvent() {
|
|
||||||
this.onProgressUploadsChanged?.(this.inProgressUploads);
|
|
||||||
this.appEvents.trigger(
|
|
||||||
`upload-mixin:${this.id}:in-progress-uploads`,
|
|
||||||
this.inProgressUploads
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
// This should be overridden in a child component if you need to
|
|
||||||
// hook into uppy events and be sure that everything is already
|
|
||||||
// set up for _uppyInstance.
|
|
||||||
_uppyReady() {},
|
|
||||||
|
|
||||||
_startUpload() {
|
|
||||||
if (!this.filesAwaitingUpload) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this._uppyInstance?.getFiles().length) {
|
this.uppyUpload.setup(this._fileInputEl);
|
||||||
return;
|
this._super();
|
||||||
}
|
|
||||||
this.set("uploading", true);
|
|
||||||
return this._uppyInstance?.upload();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_useXHRUploads() {
|
willDestroyElement() {
|
||||||
this._uppyInstance.use(XHRUpload, {
|
this.uppyUpload.teardown();
|
||||||
endpoint: this._xhrUploadUrl(),
|
this._super();
|
||||||
headers: () => ({
|
|
||||||
"X-CSRF-Token": this.session.csrfToken,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_useChunkedUploads() {
|
|
||||||
this.set("usingChunkedUploads", true);
|
|
||||||
this._uppyInstance.use(UppyChunkedUploader, {
|
|
||||||
url: this._xhrUploadUrl(),
|
|
||||||
headers: {
|
|
||||||
"X-CSRF-Token": this.session.csrfToken,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_useS3Uploads() {
|
|
||||||
this.set("usingS3Uploads", true);
|
|
||||||
this._uppyInstance.use(AwsS3, {
|
|
||||||
getUploadParameters: (file) => {
|
|
||||||
const data = {
|
|
||||||
file_name: file.name,
|
|
||||||
file_size: file.size,
|
|
||||||
type: this.type,
|
|
||||||
};
|
|
||||||
|
|
||||||
// the sha1 checksum is set by the UppyChecksum plugin, except
|
|
||||||
// for in cases where the browser does not support the required
|
|
||||||
// crypto mechanisms or an error occurs. it is an additional layer
|
|
||||||
// of security, and not required.
|
|
||||||
if (file.meta.sha1_checksum) {
|
|
||||||
data.metadata = { "sha1-checksum": file.meta.sha1_checksum };
|
|
||||||
}
|
|
||||||
|
|
||||||
return ajax(`${this.uploadRootPath}/generate-presigned-put`, {
|
|
||||||
type: "POST",
|
|
||||||
data,
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
this._uppyInstance.setFileMeta(file.id, {
|
|
||||||
uniqueUploadIdentifier: response.unique_identifier,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
method: "put",
|
|
||||||
url: response.url,
|
|
||||||
headers: {
|
|
||||||
...response.signed_headers,
|
|
||||||
"Content-Type": file.type,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.catch((errResponse) => {
|
|
||||||
displayErrorForUpload(errResponse, this.siteSettings, file.name);
|
|
||||||
this._reset();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_xhrUploadUrl() {
|
|
||||||
const uploadUrl = this.uploadUrl || this.uploadRootPath;
|
|
||||||
return getUrl(uploadUrl) + ".json?client_id=" + this.messageBus?.clientId;
|
|
||||||
},
|
|
||||||
|
|
||||||
_bindFileInputChange() {
|
|
||||||
this.fileInputEventListener = bindFileInputChangeListener(
|
|
||||||
this.fileInputEl,
|
|
||||||
this._addFiles
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
@bind
|
|
||||||
_cancelSingleUpload(data) {
|
|
||||||
this._uppyInstance.removeFile(data.fileId);
|
|
||||||
this._removeInProgressUpload(data.fileId);
|
|
||||||
},
|
|
||||||
|
|
||||||
@bind
|
|
||||||
async _addFiles(files, opts = {}) {
|
|
||||||
if (!this.session.csrfToken) {
|
|
||||||
await updateCsrfToken();
|
|
||||||
}
|
|
||||||
|
|
||||||
files = Array.isArray(files) ? files : [files];
|
|
||||||
|
|
||||||
try {
|
|
||||||
this._uppyInstance.addFiles(
|
|
||||||
files.map((file) => {
|
|
||||||
return {
|
|
||||||
source: this.id,
|
|
||||||
name: file.name,
|
|
||||||
type: file.type,
|
|
||||||
data: file,
|
|
||||||
meta: { pasted: opts.pasted },
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
warn(`error adding files to uppy: ${err}`, {
|
|
||||||
id: "discourse.upload.uppy-add-files-error",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_completeExternalUpload(file) {
|
|
||||||
return ajax(`${this.uploadRootPath}/complete-external-upload`, {
|
|
||||||
type: "POST",
|
|
||||||
data: deepMerge(
|
|
||||||
{ unique_identifier: file.meta.uniqueUploadIdentifier },
|
|
||||||
this.additionalParams || {}
|
|
||||||
),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_reset() {
|
|
||||||
this._uppyInstance?.cancelAll();
|
|
||||||
this.setProperties({
|
|
||||||
uploading: false,
|
|
||||||
processing: false,
|
|
||||||
cancellable: false,
|
|
||||||
uploadProgress: 0,
|
|
||||||
filesAwaitingUpload: false,
|
|
||||||
});
|
|
||||||
this.fileInputEl.value = "";
|
|
||||||
},
|
|
||||||
|
|
||||||
_removeInProgressUpload(fileId) {
|
|
||||||
if (this.isDestroyed || this.isDestroying) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.set(
|
|
||||||
"inProgressUploads",
|
|
||||||
this.inProgressUploads.filter((upl) => upl.id !== fileId)
|
|
||||||
);
|
|
||||||
this._triggerInProgressUploadsEvent();
|
|
||||||
},
|
|
||||||
|
|
||||||
// target must be provided as a DOM element, however the
|
|
||||||
// onDragOver and onDragLeave callbacks can also be provided.
|
|
||||||
// it is advisable to debounce/add a setTimeout timer when
|
|
||||||
// doing anything in these callbacks to avoid jumping. uppy
|
|
||||||
// also adds a .uppy-is-drag-over class to the target element by
|
|
||||||
// default onDragOver and removes it onDragLeave
|
|
||||||
_uploadDropTargetOptions() {
|
|
||||||
return { target: this.element };
|
|
||||||
},
|
|
||||||
|
|
||||||
_allUploadsComplete() {
|
|
||||||
if (this.isDestroying || this.isDestroyed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.appEvents.trigger(`upload-mixin:${this.id}:all-uploads-complete`);
|
|
||||||
this._reset();
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a component which was written for the old mixin interface,
|
||||||
|
* this function will generate a config object which is compatible
|
||||||
|
* with the new `lib/uppy/uppy-upload` class.
|
||||||
|
*/
|
||||||
|
function configShim(component) {
|
||||||
|
return {
|
||||||
|
get autoStartUploads() {
|
||||||
|
return component.autoStartUploads || true;
|
||||||
|
},
|
||||||
|
get id() {
|
||||||
|
return component.id;
|
||||||
|
},
|
||||||
|
get type() {
|
||||||
|
return component.type;
|
||||||
|
},
|
||||||
|
get uploadRootPath() {
|
||||||
|
return component.uploadRootPath || "/uploads";
|
||||||
|
},
|
||||||
|
get uploadDone() {
|
||||||
|
return component.uploadDone.bind(component);
|
||||||
|
},
|
||||||
|
get validateUploadedFilesOptions() {
|
||||||
|
return component.validateUploadedFilesOptions?.() || {};
|
||||||
|
},
|
||||||
|
get additionalParams() {
|
||||||
|
return deepMerge({}, component.additionalParams, component.data);
|
||||||
|
},
|
||||||
|
get maxFiles() {
|
||||||
|
return component.maxFiles;
|
||||||
|
},
|
||||||
|
get uploadDropTargetOptions() {
|
||||||
|
return (
|
||||||
|
component.uploadDropTargetOptions?.() || { target: component.element }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
get preventDirectS3Uploads() {
|
||||||
|
return component.preventDirectS3Uploads ?? false;
|
||||||
|
},
|
||||||
|
get useChunkedUploads() {
|
||||||
|
return component.useChunkedUploads ?? false;
|
||||||
|
},
|
||||||
|
get useMultipartUploadsIfAvailable() {
|
||||||
|
return component.useMultipartUploadsIfAvailable ?? false;
|
||||||
|
},
|
||||||
|
get uploadError() {
|
||||||
|
return component._handleUploadError?.bind(component);
|
||||||
|
},
|
||||||
|
get uppyReady() {
|
||||||
|
return component._uppyReady?.bind(component);
|
||||||
|
},
|
||||||
|
onProgressUploadsChanged() {
|
||||||
|
component.notifyPropertyChange("inProgressUploads"); // because TrackedArray isn't perfectly compatible with legacy computed properties
|
||||||
|
return component.onProgressUploadsChanged?.call(component, ...arguments);
|
||||||
|
},
|
||||||
|
get uploadUrl() {
|
||||||
|
return component.uploadUrl;
|
||||||
|
},
|
||||||
|
get perFileData() {
|
||||||
|
return component._perFileData?.bind(component);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -121,6 +121,8 @@ export default class ComposerService extends Service {
|
||||||
lastValidatedAt = null;
|
lastValidatedAt = null;
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
isProcessingUpload = false;
|
isProcessingUpload = false;
|
||||||
|
isCancellable;
|
||||||
|
uploadProgress;
|
||||||
topic = null;
|
topic = null;
|
||||||
linkLookup = null;
|
linkLookup = null;
|
||||||
showPreview = true;
|
showPreview = true;
|
||||||
|
@ -639,7 +641,7 @@ export default class ComposerService extends Service {
|
||||||
@action
|
@action
|
||||||
cancelUpload(event) {
|
cancelUpload(event) {
|
||||||
event?.preventDefault();
|
event?.preventDefault();
|
||||||
this.set("model.uploadCancelled", true);
|
this.appEvents.trigger("composer:cancel-upload");
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
|
|
@ -88,21 +88,21 @@ export default class ChatComposerUploads extends Component.extend(
|
||||||
|
|
||||||
_uppyReady() {
|
_uppyReady() {
|
||||||
if (this.siteSettings.composer_media_optimization_image_enabled) {
|
if (this.siteSettings.composer_media_optimization_image_enabled) {
|
||||||
this._useUploadPlugin(UppyMediaOptimization, {
|
this.uppyUpload.uppyWrapper.useUploadPlugin(UppyMediaOptimization, {
|
||||||
optimizeFn: (data, opts) =>
|
optimizeFn: (data, opts) =>
|
||||||
this.mediaOptimizationWorker.optimizeImage(data, opts),
|
this.mediaOptimizationWorker.optimizeImage(data, opts),
|
||||||
runParallel: !this.site.isMobileDevice,
|
runParallel: !this.site.isMobileDevice,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this._onPreProcessProgress((file) => {
|
this.uppyUpload.uppyWrapper.onPreProcessProgress((file) => {
|
||||||
const inProgressUpload = this.inProgressUploads.findBy("id", file.id);
|
const inProgressUpload = this.inProgressUploads.findBy("id", file.id);
|
||||||
if (!inProgressUpload?.processing) {
|
if (!inProgressUpload?.processing) {
|
||||||
inProgressUpload?.set("processing", true);
|
inProgressUpload?.set("processing", true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this._onPreProcessComplete((file) => {
|
this.uppyUpload.uppyWrapper.onPreProcessComplete((file) => {
|
||||||
const inProgressUpload = this.inProgressUploads.findBy("id", file.id);
|
const inProgressUpload = this.inProgressUploads.findBy("id", file.id);
|
||||||
inProgressUpload?.set("processing", false);
|
inProgressUpload?.set("processing", false);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue