diff --git a/app/assets/javascripts/admin/addon/components/admin-user-field-item.hbs b/app/assets/javascripts/admin/addon/components/admin-user-field-item.hbs index a2417599637..06835bc1ab1 100644 --- a/app/assets/javascripts/admin/addon/components/admin-user-field-item.hbs +++ b/app/assets/javascripts/admin/addon/components/admin-user-field-item.hbs @@ -1,134 +1,163 @@ -
- {{#if (or this.isEditing (not this.userField.id))}} - - - +{{#if (or this.isEditing (not @userField.id))}} +
+
+
+
+ + + {{#each @fieldTypes as |fieldType|}} + {{fieldType.name}} + {{/each}} + + - - - + + + - - - + + + - {{#if this.bufferedFieldType.hasOptions}} - - - - {{/if}} + {{#if + (or + (eq transientData.field_type "dropdown") + (eq transientData.field_type "multiselect") + ) + }} + + + + + + {{/if}} - - + + + + {{i18n "admin.user_fields.requirement.optional.title"}} + + + {{i18n "admin.user_fields.requirement.for_all_users.title"}} + {{i18n + "admin.user_fields.requirement.for_all_users.description" + }} + + + {{i18n "admin.user_fields.requirement.on_signup.title"}} + {{i18n + "admin.user_fields.requirement.on_signup.description" + }} + + + - + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - {{else}} + + + + + +
+
+
+{{else}} +
- {{this.userField.name}} + {{@userField.name}}
- {{html-safe - this.userField.description - }} + {{html-safe @userField.description}}
-
{{this.fieldName}}
+
{{@userField.fieldTypeName}}
{{this.flags}}
- {{/if}} -
\ No newline at end of file +
+{{/if}} \ No newline at end of file diff --git a/app/assets/javascripts/admin/addon/components/admin-user-field-item.js b/app/assets/javascripts/admin/addon/components/admin-user-field-item.js index c95ac1e3b91..28fb4325c7e 100644 --- a/app/assets/javascripts/admin/addon/components/admin-user-field-item.js +++ b/app/assets/javascripts/admin/addon/components/admin-user-field-item.js @@ -1,4 +1,5 @@ -import Component from "@ember/component"; +import Component from "@glimmer/component"; +import { cached, tracked } from "@glimmer/tracking"; import { action } from "@ember/object"; import { schedule } from "@ember/runloop"; import { service } from "@ember/service"; @@ -6,78 +7,102 @@ import { isEmpty } from "@ember/utils"; import { tagName } from "@ember-decorators/component"; import { Promise } from "rsvp"; import { popupAjaxError } from "discourse/lib/ajax-error"; -import { i18n, propertyEqual } from "discourse/lib/computed"; -import { bufferedProperty } from "discourse/mixins/buffered-content"; -import discourseComputed from "discourse-common/utils/decorators"; import I18n from "discourse-i18n"; import UserField from "admin/models/user-field"; @tagName("") -export default class AdminUserFieldItem extends Component.extend( - bufferedProperty("userField") -) { +export default class AdminUserFieldItem extends Component { @service adminCustomUserFields; @service dialog; - isEditing = false; + @tracked isEditing = false; + @tracked + editableDisabled = this.args.userField.requirement === "for_all_users"; - @propertyEqual("userField", "firstField") cantMoveUp; - @propertyEqual("userField", "lastField") cantMoveDown; - - @i18n("admin.user_fields.description") userFieldsDescription; - - @discourseComputed("buffered.field_type") - bufferedFieldType(fieldType) { - return UserField.fieldTypeById(fieldType); + get fieldName() { + return UserField.fieldTypeById(this.fieldType)?.name; } - didInsertElement() { - super.didInsertElement(...arguments); - - this._focusName(); + get cantMoveUp() { + return this.args.userField.id === this.args.firstField?.id; } - _focusName() { - schedule("afterRender", () => { - document.querySelector(".user-field-name")?.focus(); - }); + get cantMoveDown() { + return this.args.userField.id === this.args.lastField?.id; } - @discourseComputed("userField.field_type") - fieldName(fieldType) { - return UserField.fieldTypeById(fieldType)?.name; + get isNewRecord() { + return isEmpty(this.args.userField?.id); } - @discourseComputed( - "userField.{editable,show_on_profile,show_on_user_card,searchable}" - ) - flags(userField) { - const ret = []; - if (userField.editable) { - ret.push(I18n.t("admin.user_fields.editable.enabled")); - } - if (userField.show_on_profile) { - ret.push(I18n.t("admin.user_fields.show_on_profile.enabled")); - } - if (userField.show_on_user_card) { - ret.push(I18n.t("admin.user_fields.show_on_user_card.enabled")); - } - if (userField.searchable) { - ret.push(I18n.t("admin.user_fields.searchable.enabled")); - } + get flags() { + const flags = [ + "editable", + "show_on_profile", + "show_on_user_card", + "searchable", + ]; - return ret.join(", "); + return flags + .map((flag) => { + if (this.args.userField[flag]) { + return I18n.t(`admin.user_fields.${flag}.enabled`); + } + }) + .filter(Boolean) + .join(", "); } - @discourseComputed("buffered.requirement") - editableDisabled(requirement) { - return requirement === "for_all_users"; + @cached + get formData() { + return this.args.userField.getProperties( + "field_type", + "name", + "description", + "requirement", + "editable", + "show_on_profile", + "show_on_user_card", + "searchable", + "options", + ...this.adminCustomUserFields.additionalProperties + ); } @action - changeRequirementType(requirement) { - this.buffered.set("requirement", requirement); - this.buffered.set("editable", requirement === "for_all_users"); + setRequirement(value, { set }) { + set("requirement", value); + + if (value === "for_all_users") { + this.editableDisabled = true; + set("editable", true); + } else { + this.editableDisabled = false; + } + } + + @action + async save(data) { + let confirm = true; + + if (data.requirement === "for_all_users") { + confirm = await this._confirmChanges(); + } + + if (!confirm) { + return; + } + + return this.args.userField + .save(data) + .then(() => { + if (this.isDestroying || this.isDestroyed) { + return; + } + + this.isEditing = false; + }) + .catch(popupAjaxError); } async _confirmChanges() { @@ -90,57 +115,23 @@ export default class AdminUserFieldItem extends Component.extend( }); } - @action - async save() { - const attrs = this.buffered.getProperties( - "name", - "description", - "field_type", - "editable", - "requirement", - "show_on_profile", - "show_on_user_card", - "searchable", - "options", - ...this.adminCustomUserFields.additionalProperties - ); - - let confirm = true; - - if (attrs.requirement === "for_all_users") { - confirm = await this._confirmChanges(); - } - - if (!confirm) { - return; - } - - return this.userField - .save(attrs) - .then(() => { - if (this.isDestroying || this.isDestroyed) { - return; - } - - this.set("isEditing", false); - this.commitBuffer(); - }) - .catch(popupAjaxError); - } - @action edit() { - this.set("isEditing", true); - this._focusName(); + this.isEditing = true; } @action cancel() { - if (isEmpty(this.userField?.id)) { - this.destroyAction(this.userField); + if (this.isNewRecord) { + this.args.destroyAction(this.args.userField); } else { - this.rollbackBuffer(); - this.set("isEditing", false); + this.isEditing = false; } } + + _focusName() { + schedule("afterRender", () => + document.querySelector(".user-field-name")?.focus() + ); + } } diff --git a/app/assets/javascripts/admin/addon/components/value-list.js b/app/assets/javascripts/admin/addon/components/value-list.js index ceb92ac3056..9c37faabae8 100644 --- a/app/assets/javascripts/admin/addon/components/value-list.js +++ b/app/assets/javascripts/admin/addon/components/value-list.js @@ -14,6 +14,7 @@ export default class ValueList extends Component { newValue = ""; collection = null; values = null; + onChange = null; @reads("addKey") noneKey; @@ -21,7 +22,7 @@ export default class ValueList extends Component { super.didReceiveAttrs(...arguments); if (this.inputType === "array") { - this.set("collection", this.values || []); + this.set("collection", this.values ? [...this.values] : []); return; } @@ -114,6 +115,11 @@ export default class ValueList extends Component { } _saveValues() { + if (this.onChange) { + this.onChange([...this.collection]); + return; + } + if (this.inputType === "array") { this.set("values", this.collection); return; diff --git a/app/assets/javascripts/admin/addon/controllers/admin-user-fields.js b/app/assets/javascripts/admin/addon/controllers/admin-user-fields.js index e760925fd36..6f50ed070ff 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-user-fields.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-user-fields.js @@ -28,6 +28,7 @@ export default class AdminUserFieldsController extends Controller { createField() { const f = this.store.createRecord("user-field", { field_type: "text", + requirement: "optional", position: MAX_FIELDS, }); this.model.pushObject(f); diff --git a/app/assets/javascripts/admin/addon/models/user-field.js b/app/assets/javascripts/admin/addon/models/user-field.js index b27f8b5aff1..da70af5706a 100644 --- a/app/assets/javascripts/admin/addon/models/user-field.js +++ b/app/assets/javascripts/admin/addon/models/user-field.js @@ -1,3 +1,4 @@ +import { tracked } from "@glimmer/tracking"; import EmberObject from "@ember/object"; import { i18n } from "discourse/lib/computed"; import RestModel from "discourse/models/rest"; @@ -19,6 +20,16 @@ export default class UserField extends RestModel { static fieldTypeById(id) { return this.fieldTypes().findBy("id", id); } + + @tracked field_type; + @tracked editable; + @tracked show_on_profile; + @tracked show_on_user_card; + @tracked searchable; + + get fieldTypeName() { + return UserField.fieldTypes().find((ft) => ft.id === this.field_type).name; + } } class UserFieldType extends EmberObject { diff --git a/app/assets/javascripts/admin/addon/templates/user-fields.hbs b/app/assets/javascripts/admin/addon/templates/user-fields.hbs index 79b96ac39f8..f8d6fda47c6 100644 --- a/app/assets/javascripts/admin/addon/templates/user-fields.hbs +++ b/app/assets/javascripts/admin/addon/templates/user-fields.hbs @@ -1,27 +1,29 @@ -
-

{{i18n "admin.user_fields.title"}}

+
+
+

{{i18n "admin.user_fields.title"}}

-

{{i18n "admin.user_fields.help"}}

+

{{i18n "admin.user_fields.help"}}

- {{#if this.model}} - {{#each this.sortedFields as |uf|}} - - {{/each}} - {{/if}} + {{#if this.model}} + {{#each this.sortedFields as |uf|}} + + {{/each}} + {{/if}} - + +
\ No newline at end of file diff --git a/app/assets/javascripts/discourse/tests/integration/components/admin-user-field-item-test.js b/app/assets/javascripts/discourse/tests/integration/components/admin-user-field-item-test.js deleted file mode 100644 index 083e5d7b83d..00000000000 --- a/app/assets/javascripts/discourse/tests/integration/components/admin-user-field-item-test.js +++ /dev/null @@ -1,95 +0,0 @@ -import { click, render } from "@ember/test-helpers"; -import { hbs } from "ember-cli-htmlbars"; -import { module, test } from "qunit"; -import { setupRenderingTest } from "discourse/tests/helpers/component-test"; -import { exists, query } from "discourse/tests/helpers/qunit-helpers"; -import I18n from "discourse-i18n"; - -module("Integration | Component | admin-user-field-item", function (hooks) { - setupRenderingTest(hooks); - - test("user field without an id", async function (assert) { - await render(hbs``); - - assert.ok(exists(".save"), "displays editing mode"); - }); - - test("cancel action", async function (assert) { - this.set("userField", { id: 1, field_type: "text" }); - this.set("isEditing", true); - this.set("destroyAction", () => {}); - this.set("moveUpAction", () => {}); - this.set("moveDownAction", () => {}); - - await render(hbs` - `); - - await click(".cancel"); - assert.ok(exists(".edit")); - }); - - test("edit action", async function (assert) { - this.set("userField", { id: 1, field_type: "text" }); - this.set("destroyAction", () => {}); - this.set("moveUpAction", () => {}); - this.set("moveDownAction", () => {}); - - await render(hbs` - `); - - await click(".edit"); - assert.ok(exists(".save")); - }); - - test("field attributes are rendered correctly", async function (assert) { - this.set("userField", { - id: 1, - field_type: "text", - name: "foo", - description: "what is foo", - show_on_profile: true, - show_on_user_card: true, - searchable: true, - }); - this.set("destroyAction", () => {}); - this.set("moveUpAction", () => {}); - this.set("moveDownAction", () => {}); - - await render(hbs` - `); - - assert.strictEqual(query(".name").innerText, this.userField.name); - assert.strictEqual( - query(".description").innerText, - this.userField.description - ); - assert.strictEqual( - query(".field-type").innerText, - I18n.t("admin.user_fields.field_types.text") - ); - - assert - .dom(".user-field-flags") - .hasText( - `${I18n.t("admin.user_fields.show_on_profile.enabled")}, ${I18n.t( - "admin.user_fields.show_on_user_card.enabled" - )}, ${I18n.t("admin.user_fields.searchable.enabled")}` - ); - }); -}); diff --git a/app/assets/stylesheets/common/admin/customize.scss b/app/assets/stylesheets/common/admin/customize.scss index df2491f61fd..e306399a2d1 100644 --- a/app/assets/stylesheets/common/admin/customize.scss +++ b/app/assets/stylesheets/common/admin/customize.scss @@ -956,64 +956,16 @@ table.permalinks { .user-fields { h2 { - margin-bottom: 10px; + margin-bottom: 1em; } .user-field { - padding: 10px; - margin-bottom: 10px; + padding-block: 0.5em; + margin-bottom: 1em; border-bottom: 1px solid var(--primary-low); .form-display { width: 25%; float: left; } - .form-element, - .form-element-desc { - float: left; - min-height: 30px; - padding: 0.25em 0; - &.input-area { - width: 75%; - .value-list, - .select-kit, - input[type="text"] { - width: 50%; - } - - .value-list { - .select-kit { - width: 100%; - } - } - label { - font-weight: normal; - padding: 0.5em 0; - } - - .label-text { - display: inline-flex; - flex-direction: column; - } - - .description { - margin-top: 0.25em; - color: var(--primary-medium); - font-size: var(--font-down-1); - line-height: var(--line-height-large); - } - } - &.label-area { - width: 25%; - label { - margin: 0.5em 1em 0 0; - text-align: right; - font-weight: bold; - } - } - } - .controls { - float: right; - text-align: right; - } .clearfix { clear: both; } diff --git a/app/assets/stylesheets/common/form-kit/_checkbox-group.scss b/app/assets/stylesheets/common/form-kit/_checkbox-group.scss index 7f7d62194e9..60102f6555c 100644 --- a/app/assets/stylesheets/common/form-kit/_checkbox-group.scss +++ b/app/assets/stylesheets/common/form-kit/_checkbox-group.scss @@ -1,5 +1,4 @@ .form-kit__checkbox-group { display: flex; - flex-direction: column; - gap: 0.75em; + gap: 0em; } diff --git a/app/assets/stylesheets/common/form-kit/_control-custom-value-list.scss b/app/assets/stylesheets/common/form-kit/_control-custom-value-list.scss new file mode 100644 index 00000000000..9328657334e --- /dev/null +++ b/app/assets/stylesheets/common/form-kit/_control-custom-value-list.scss @@ -0,0 +1,7 @@ +.form-kit__control-custom { + .value-list { + .single-select.combobox { + width: 100%; + } + } +} diff --git a/app/assets/stylesheets/common/form-kit/_index.scss b/app/assets/stylesheets/common/form-kit/_index.scss index b2893085faf..82b07caabd0 100644 --- a/app/assets/stylesheets/common/form-kit/_index.scss +++ b/app/assets/stylesheets/common/form-kit/_index.scss @@ -20,6 +20,7 @@ @import "_control-select"; @import "_control-custom"; @import "_control-textarea"; +@import "_control-custom-value-list"; @import "_errors"; @import "_errors-summary"; @import "_field"; diff --git a/spec/system/admin_user_fields_spec.rb b/spec/system/admin_user_fields_spec.rb index a3c2d5b8017..4d8d61612d2 100644 --- a/spec/system/admin_user_fields_spec.rb +++ b/spec/system/admin_user_fields_spec.rb @@ -23,7 +23,7 @@ describe "Admin User Fields", type: :system, js: true do user_fields_page.add_field(name: "Occupation", description: "") - expect(user_fields_page).to have_text(/Description can't be blank/) + expect(user_fields_page.form.field(:description)).to have_errors("Required") end it "makes sure new required fields are editable after signup" do @@ -40,7 +40,7 @@ describe "Admin User Fields", type: :system, js: true do user_fields_page.choose_requirement("optional") - expect(form).to have_field(editable_label, checked: false, disabled: false) + expect(form).to have_field(editable_label, checked: true, disabled: false) end it "requires confirmation when applying required fields retroactively" do diff --git a/spec/system/page_objects/components/form_kit.rb b/spec/system/page_objects/components/form_kit.rb index 24fbd01bc88..93329bc8a00 100644 --- a/spec/system/page_objects/components/form_kit.rb +++ b/spec/system/page_objects/components/form_kit.rb @@ -67,6 +67,16 @@ module PageObjects expect(self.value).to eq(expected_value) end + def has_errors?(*messages) + within component do + messages.all? { |m| find(".form-kit__errors", text: m) } + end + end + + def has_no_errors? + !has_css?(".form-kit__errors") + end + def control_type component["data-control-type"] end diff --git a/spec/system/page_objects/pages/admin_user_fields.rb b/spec/system/page_objects/pages/admin_user_fields.rb index 37692719c3d..664d51c04dd 100644 --- a/spec/system/page_objects/pages/admin_user_fields.rb +++ b/spec/system/page_objects/pages/admin_user_fields.rb @@ -8,6 +8,10 @@ module PageObjects self end + def form + PageObjects::Components::FormKit.new(".user-field .form-kit") + end + def choose_requirement(requirement) form = page.find(".user-field")