diff --git a/app/assets/javascripts/admin/components/simple-list.js b/app/assets/javascripts/admin/components/simple-list.js
new file mode 100644
index 00000000000..3f43885d77d
--- /dev/null
+++ b/app/assets/javascripts/admin/components/simple-list.js
@@ -0,0 +1,57 @@
+import { empty } from "@ember/object/computed";
+import Component from "@ember/component";
+import { action } from "@ember/object";
+import { on } from "discourse-common/utils/decorators";
+
+export default Component.extend({
+ classNameBindings: [":simple-list", ":value-list"],
+ inputEmpty: empty("newValue"),
+ inputDelimiter: null,
+ newValue: "",
+ collection: null,
+ values: null,
+
+ @on("didReceiveAttrs")
+ _setupCollection() {
+ this.set("collection", this._splitValues(this.values, this.inputDelimiter));
+ },
+
+ keyDown(event) {
+ if (event.which === 13) {
+ this.addValue(this.newValue);
+ return;
+ }
+ },
+
+ @action
+ changeValue(index, newValue) {
+ this.collection.replace(index, 1, [newValue]);
+ this.collection.arrayContentDidChange(index);
+ this._onChange();
+ },
+
+ @action
+ addValue(newValue) {
+ if (this.inputEmpty) return;
+
+ this.set("newValue", null);
+ this.collection.addObject(newValue);
+ this._onChange();
+ },
+
+ @action
+ removeValue(value) {
+ this.collection.removeObject(value);
+ this._onChange();
+ },
+
+ _onChange() {
+ this.attrs.onChange && this.attrs.onChange(this.collection);
+ },
+
+ _splitValues(values, delimiter) {
+ return values && values.length
+ ? values.split(delimiter || "\n").filter(Boolean)
+ : [];
+ }
+});
diff --git a/app/assets/javascripts/admin/components/site-settings/simple-list.js b/app/assets/javascripts/admin/components/site-settings/simple-list.js
new file mode 100644
index 00000000000..aab078bbe3c
--- /dev/null
+++ b/app/assets/javascripts/admin/components/site-settings/simple-list.js
@@ -0,0 +1,11 @@
+import Component from "@ember/component";
+import { action } from "@ember/object";
+
+export default Component.extend({
+ inputDelimiter: "|",
+
+ @action
+ onChange(value) {
+ this.set("value", value.join(this.inputDelimiter || "\n"));
+ }
+});
diff --git a/app/assets/javascripts/admin/mixins/setting-component.js b/app/assets/javascripts/admin/mixins/setting-component.js
index 83dcc31e65a..699cecd32eb 100644
--- a/app/assets/javascripts/admin/mixins/setting-component.js
+++ b/app/assets/javascripts/admin/mixins/setting-component.js
@@ -25,7 +25,8 @@ const CUSTOM_TYPES = [
"upload",
"group_list",
"tag_list",
- "color"
+ "color",
+ "simple_list"
];
const AUTO_REFRESH_ON_SAVE = ["logo", "logo_small", "large_icon"];
diff --git a/app/assets/javascripts/admin/templates/components/simple-list.hbs b/app/assets/javascripts/admin/templates/components/simple-list.hbs
new file mode 100644
index 00000000000..3e78f77b059
--- /dev/null
+++ b/app/assets/javascripts/admin/templates/components/simple-list.hbs
@@ -0,0 +1,40 @@
+{{#if collection}}
+
+ {{#each collection as |value index|}}
+
+ {{d-button
+ action=(action "removeValue")
+ actionParam=value
+ icon="times"
+ class="remove-value-btn btn-small"
+ }}
+
+ {{input
+ title=value
+ value=value
+ class="value-input"
+ focus-out=(action "changeValue" index)
+ }}
+
+ {{/each}}
+
+{{/if}}
+
+
+ {{input
+ type="text"
+ value=newValue
+ placeholderKey="admin.site_settings.simple_list.add_item"
+ class="add-value-input"
+ autocomplete="discourse"
+ autocorrect="off"
+ autocapitalize="off"}}
+
+ {{d-button
+ action=(action "addValue")
+ actionParam=newValue
+ disabled=inputEmpty
+ icon="plus"
+ class="add-value-btn btn-small"
+ }}
+
diff --git a/app/assets/javascripts/admin/templates/components/site-settings/simple-list.hbs b/app/assets/javascripts/admin/templates/components/site-settings/simple-list.hbs
new file mode 100644
index 00000000000..3eca57db83a
--- /dev/null
+++ b/app/assets/javascripts/admin/templates/components/site-settings/simple-list.hbs
@@ -0,0 +1,3 @@
+{{simple-list values=value inputDelimiter=inputDelimiter onChange=(action "onChange")}}
+{{setting-validation-message message=validationMessage}}
+{{html-safe setting.description}}
diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss
index afdc9a7e3be..208e9127b36 100644
--- a/app/assets/stylesheets/common/admin/admin_base.scss
+++ b/app/assets/stylesheets/common/admin/admin_base.scss
@@ -931,6 +931,20 @@ table#user-badges {
}
}
+.simple-list-input {
+ display: flex;
+
+ .add-value-input {
+ margin: 0;
+ box-sizing: border-box;
+ flex: 1 0 0px;
+ }
+
+ .add-value-btn {
+ margin-left: 0.25em;
+ }
+}
+
// Mobile view text-inputs need some padding
.mobile-view .admin-contents {
input[type="text"] {
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index b5db1f5059a..308944b3b28 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -4588,6 +4588,8 @@ en:
modal_description: "Would you like to apply this change historically? This will change preferences for %{count} existing users."
modal_yes: "Yes"
modal_no: "No, only apply change going forward"
+ simple_list:
+ add_item: "Add item..."
badges:
title: Badges
diff --git a/config/site_settings.yml b/config/site_settings.yml
index 5901d6d3388..c658ec727c1 100644
--- a/config/site_settings.yml
+++ b/config/site_settings.yml
@@ -1445,7 +1445,7 @@ security:
content_security_policy_collect_reports:
default: false
content_security_policy_script_src:
- type: list
+ type: simple_list
default: ""
invalidate_inactive_admin_email_after_days:
default: 365
diff --git a/lib/site_settings/type_supervisor.rb b/lib/site_settings/type_supervisor.rb
index 26cc20b84e5..ae78d5dd1ff 100644
--- a/lib/site_settings/type_supervisor.rb
+++ b/lib/site_settings/type_supervisor.rb
@@ -34,7 +34,8 @@ class SiteSettings::TypeSupervisor
group: 19,
group_list: 20,
tag_list: 21,
- color: 22
+ color: 22,
+ simple_list: 23
)
end
diff --git a/spec/components/site_settings/type_supervisor_spec.rb b/spec/components/site_settings/type_supervisor_spec.rb
index caea601178d..cc697c01d5c 100644
--- a/spec/components/site_settings/type_supervisor_spec.rb
+++ b/spec/components/site_settings/type_supervisor_spec.rb
@@ -88,6 +88,9 @@ describe SiteSettings::TypeSupervisor do
it "'color' should be at the right position" do
expect(SiteSettings::TypeSupervisor.types[:color]).to eq(22)
end
+ it "'simple_list' should be at the right position" do
+ expect(SiteSettings::TypeSupervisor.types[:simple_list]).to eq(23)
+ end
end
end
diff --git a/test/javascripts/components/simple-list-test.js b/test/javascripts/components/simple-list-test.js
new file mode 100644
index 00000000000..5c8d26dd51c
--- /dev/null
+++ b/test/javascripts/components/simple-list-test.js
@@ -0,0 +1,84 @@
+import componentTest from "helpers/component-test";
+moduleForComponent("simple-list", { integration: true });
+
+componentTest("adding a value", {
+ template: "{{simple-list values=values}}",
+
+ beforeEach() {
+ this.set("values", "vinkas\nosama");
+ },
+
+ async test(assert) {
+ assert.ok(
+ find(".add-value-btn[disabled]").length,
+ "while loading the + button is disabled"
+ );
+
+ await fillIn(".add-value-input", "penar");
+ await click(".add-value-btn");
+
+ assert.ok(
+ find(".values .value").length === 3,
+ "it adds the value to the list of values"
+ );
+
+ assert.ok(
+ find(".values .value[data-index='2'] .value-input")[0].value === "penar",
+ "it sets the correct value for added item"
+ );
+
+ await fillIn(".add-value-input", "eviltrout");
+ await keyEvent(".add-value-input", "keydown", 13); // enter
+
+ assert.ok(
+ find(".values .value").length === 4,
+ "it adds the value when keying Enter"
+ );
+ }
+});
+
+componentTest("removing a value", {
+ template: "{{simple-list values=values}}",
+
+ beforeEach() {
+ this.set("values", "vinkas\nosama");
+ },
+
+ async test(assert) {
+ await click(".values .value[data-index='0'] .remove-value-btn");
+
+ assert.ok(
+ find(".values .value").length === 1,
+ "it removes the value from the list of values"
+ );
+
+ assert.ok(
+ find(".values .value[data-index='0'] .value-input")[0].value === "osama",
+ "it removes the correct value"
+ );
+ }
+});
+
+componentTest("delimiter support", {
+ template: "{{simple-list values=values inputDelimiter='|'}}",
+
+ beforeEach() {
+ this.set("values", "vinkas|osama");
+ },
+
+ async test(assert) {
+ await fillIn(".add-value-input", "eviltrout");
+ await click(".add-value-btn");
+
+ assert.ok(
+ find(".values .value").length === 3,
+ "it adds the value to the list of values"
+ );
+
+ assert.ok(
+ find(".values .value[data-index='2'] .value-input")[0].value ===
+ "eviltrout",
+ "it adds the correct value"
+ );
+ }
+});