diff --git a/app/assets/javascripts/discourse/app/components/composer-editor.js b/app/assets/javascripts/discourse/app/components/composer-editor.js
index 71312232091..f836b7ff784 100644
--- a/app/assets/javascripts/discourse/app/components/composer-editor.js
+++ b/app/assets/javascripts/discourse/app/components/composer-editor.js
@@ -1,8 +1,4 @@
-import {
- authorizedExtensions,
- authorizesAllExtensions,
- authorizesOneOrMoreImageExtensions,
-} from "discourse/lib/uploads";
+import { authorizesOneOrMoreImageExtensions } from "discourse/lib/uploads";
import { alias } from "@ember/object/computed";
import { BasePlugin } from "@uppy/core";
import { resolveAllShortUrls } from "pretty-text/upload-short-url";
@@ -201,24 +197,6 @@ export default Component.extend(ComposerUploadUppy, {
});
},
- @discourseComputed()
- acceptsAllFormats() {
- return (
- this.capabilities.isIOS ||
- authorizesAllExtensions(this.currentUser.staff, this.siteSettings)
- );
- },
-
- @discourseComputed()
- acceptedFormats() {
- const extensions = authorizedExtensions(
- this.currentUser.staff,
- this.siteSettings
- );
-
- return extensions.map((ext) => `.${ext}`).join();
- },
-
@bind
_afterMentionComplete(value) {
this.composer.set("reply", value);
diff --git a/app/assets/javascripts/discourse/app/components/pick-files-button.js b/app/assets/javascripts/discourse/app/components/pick-files-button.js
index 9ab6465bcd8..56aae7ed42c 100644
--- a/app/assets/javascripts/discourse/app/components/pick-files-button.js
+++ b/app/assets/javascripts/discourse/app/components/pick-files-button.js
@@ -1,25 +1,46 @@
import Component from "@ember/component";
-import { action } from "@ember/object";
-import { empty } from "@ember/object/computed";
-import computed, { bind } from "discourse-common/utils/decorators";
-import I18n from "I18n";
import bootbox from "bootbox";
+import { isBlank } from "@ember/utils";
+import {
+ authorizedExtensions,
+ authorizesAllExtensions,
+} from "discourse/lib/uploads";
+import { action } from "@ember/object";
+import discourseComputed, { bind } from "discourse-common/utils/decorators";
+import I18n from "I18n";
+// This picker is intended to be used with UppyUploadMixin or with
+// ComposerUploadUppy, which is why there are no change events registered
+// for the input. They are handled by the uppy mixins directly.
+//
+// However, if you provide an onFilesPicked action to this component, the change
+// binding will still be added, and the file type will be validated here. This
+// is sometimes useful if you need to do something outside the uppy upload with
+// the file, such as directly using JSON or CSV data from a file in JS.
export default Component.extend({
+ fileInputId: null,
+ fileInputClass: null,
classNames: ["pick-files-button"],
- acceptedFileTypes: null,
- acceptAnyFile: empty("acceptedFileTypes"),
+ acceptedFormatsOverride: null,
+ allowMultiple: false,
+ showButton: false,
didInsertElement() {
this._super(...arguments);
- const fileInput = this.element.querySelector("input");
- this.set("fileInput", fileInput);
- fileInput.addEventListener("change", this.onChange, false);
+
+ if (this.onFilesPicked) {
+ const fileInput = this.element.querySelector("input");
+ this.set("fileInput", fileInput);
+ fileInput.addEventListener("change", this.onChange, false);
+ }
},
willDestroyElement() {
this._super(...arguments);
- this.fileInput.removeEventListener("change", this.onChange);
+
+ if (this.onFilesPicked) {
+ this.fileInput.removeEventListener("change", this.onChange);
+ }
},
@bind
@@ -28,33 +49,27 @@ export default Component.extend({
this._filesPicked(files);
},
- @computed
- acceptedFileTypesString() {
- if (!this.acceptedFileTypes) {
- return null;
- }
-
- return this.acceptedFileTypes.join(",");
+ @discourseComputed()
+ acceptsAllFormats() {
+ return (
+ this.capabilities.isIOS ||
+ authorizesAllExtensions(this.currentUser.staff, this.siteSettings)
+ );
},
- @computed
- acceptedExtensions() {
- if (!this.acceptedFileTypes) {
- return null;
+ @discourseComputed()
+ acceptedFormats() {
+ // the acceptedFormatsOverride can be a list of extensions or mime types
+ if (!isBlank(this.acceptedFormatsOverride)) {
+ return this.acceptedFormatsOverride;
}
- return this.acceptedFileTypes
- .filter((type) => type.startsWith("."))
- .map((type) => type.substring(1));
- },
+ const extensions = authorizedExtensions(
+ this.currentUser.staff,
+ this.siteSettings
+ );
- @computed
- acceptedMimeTypes() {
- if (!this.acceptedFileTypes) {
- return null;
- }
-
- return this.acceptedFileTypes.filter((type) => !type.startsWith("."));
+ return extensions.map((ext) => `.${ext}`).join();
},
@action
@@ -79,25 +94,18 @@ export default Component.extend({
_haveAcceptedTypes(files) {
for (const file of files) {
- if (
- !(this._hasAcceptedExtension(file) || this._hasAcceptedMimeType(file))
- ) {
+ if (!this._hasAcceptedExtensionOrType(file)) {
return false;
}
}
return true;
},
- _hasAcceptedExtension(file) {
+ _hasAcceptedExtensionOrType(file) {
const extension = this._fileExtension(file.name);
return (
- !this.acceptedExtensions || this.acceptedExtensions.includes(extension)
- );
- },
-
- _hasAcceptedMimeType(file) {
- return (
- !this.acceptedMimeTypes || this.acceptedMimeTypes.includes(file.type)
+ this.acceptedFormats.includes(`.${extension}`) ||
+ this.acceptedFormats.includes(file.type)
);
},
diff --git a/app/assets/javascripts/discourse/app/templates/components/composer-editor.hbs b/app/assets/javascripts/discourse/app/templates/components/composer-editor.hbs
index 77aac380bcb..767b0a854ae 100644
--- a/app/assets/javascripts/discourse/app/templates/components/composer-editor.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/composer-editor.hbs
@@ -23,9 +23,5 @@
{{/d-editor}}
{{#if allowUpload}}
- {{#if acceptsAllFormats}}
-
- {{else}}
-
- {{/if}}
+ {{pick-files-button fileInputId="file-uploader" allowMultiple=true}}
{{/if}}
diff --git a/app/assets/javascripts/discourse/app/templates/components/pick-files-button.hbs b/app/assets/javascripts/discourse/app/templates/components/pick-files-button.hbs
index 409a3becbd7..dc6bc33f6ab 100644
--- a/app/assets/javascripts/discourse/app/templates/components/pick-files-button.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/pick-files-button.hbs
@@ -1,6 +1,8 @@
-{{d-button action=(action "openSystemFilePicker") label=label icon=icon}}
-{{#if acceptAnyFile}}
-
-{{else}}
-
+{{#if showButton}}
+ {{d-button action=(action "openSystemFilePicker") label=label icon=icon}}
+{{/if}}
+{{#if acceptsAllFormats}}
+
+{{else}}
+
{{/if}}
diff --git a/app/assets/javascripts/discourse/tests/integration/components/pick-files-button-tests.js b/app/assets/javascripts/discourse/tests/integration/components/pick-files-button-tests.js
deleted file mode 100644
index db622c99b43..00000000000
--- a/app/assets/javascripts/discourse/tests/integration/components/pick-files-button-tests.js
+++ /dev/null
@@ -1,92 +0,0 @@
-import componentTest, {
- setupRenderingTest,
-} from "discourse/tests/helpers/component-test";
-import { discourseModule } from "discourse/tests/helpers/qunit-helpers";
-import hbs from "htmlbars-inline-precompile";
-import { triggerEvent } from "@ember/test-helpers";
-import sinon from "sinon";
-import bootbox from "bootbox";
-
-function createBlob(mimeType, extension) {
- const blob = new Blob(["content"], {
- type: mimeType,
- });
- blob.name = `filename${extension}`;
- return blob;
-}
-
-discourseModule(
- "Integration | Component | pick-files-button",
- function (hooks) {
- const expectedExtension = ".json";
- const expectedMimeType = "text/json";
-
- setupRenderingTest(hooks);
-
- hooks.beforeEach(function () {
- this.set("acceptedFileTypes", [expectedExtension, expectedMimeType]);
- this.set("onFilesPicked", () => {});
- });
-
- componentTest("it doesn't show alert if a file has a supported MIME type", {
- skip: true,
- template: hbs`
- {{pick-files-button
- acceptedFileTypes=this.acceptedFileTypes
- onFilesPicked=this.onFilesPicked}}`,
-
- async test(assert) {
- sinon.stub(bootbox, "alert");
-
- const wrongExtension = ".txt";
- const file = createBlob(expectedMimeType, wrongExtension);
-
- await triggerEvent("input[type='file']", "change", { files: [file] });
-
- assert.ok(bootbox.alert.notCalled);
- },
- });
-
- componentTest("it doesn't show alert if a file has a supported extension", {
- skip: true,
- template: hbs`
- {{pick-files-button
- acceptedFileTypes=this.acceptedFileTypes
- onFilesPicked=this.onFilesPicked}}`,
-
- async test(assert) {
- sinon.stub(bootbox, "alert");
-
- const wrongMimeType = "text/plain";
- const file = createBlob(wrongMimeType, expectedExtension);
-
- await triggerEvent("input[type='file']", "change", { files: [file] });
-
- assert.ok(bootbox.alert.notCalled);
- },
- });
-
- componentTest(
- "it shows alert if a file has an unsupported extension and unsupported MIME type",
- {
- skip: true,
- template: hbs`
- {{pick-files-button
- acceptedFileTypes=this.acceptedFileTypes
- onFilesPicked=this.onFilesPicked}}`,
-
- async test(assert) {
- sinon.stub(bootbox, "alert");
-
- const wrongExtension = ".txt";
- const wrongMimeType = "text/plain";
- const file = createBlob(wrongMimeType, wrongExtension);
-
- await triggerEvent("input[type='file']", "change", { files: [file] });
-
- assert.ok(bootbox.alert.calledOnce);
- },
- }
- );
- }
-);