From 88af23e1caf7c1d83c415bdd664159294e30842b Mon Sep 17 00:00:00 2001 From: Ted Johansson Date: Mon, 25 Nov 2024 11:54:43 +0800 Subject: [PATCH] DEV: Modernize admin user fields (#29843) This PR modernizes the user fields area of the admin UI. It is largely based on the work on the emoji section. --- .../admin-config-areas/user-fields-list.gjs | 105 +++++++ .../components/admin-user-field-item.gjs | 134 +++++++++ .../components/admin-user-field-item.hbs | 195 ------------- .../addon/components/admin-user-field-item.js | 143 ---------- .../components/admin-user-fields-form.gjs | 269 ++++++++++++++++++ .../addon/controllers/admin-user-fields.js | 82 ------ .../admin/addon/models/user-field.js | 1 + .../admin/addon/routes/admin-route-map.js | 13 +- .../addon/routes/admin-user-fields-edit.js | 12 + .../addon/routes/admin-user-fields-new.js | 20 ++ .../admin/addon/routes/admin-user-fields.js | 10 +- .../admin/addon/services/admin-user-fields.js | 36 +++ .../addon/templates/user-fields-edit.hbs | 8 + .../addon/templates/user-fields-index.hbs | 1 + .../admin/addon/templates/user-fields-new.hbs | 8 + .../admin/addon/templates/user-fields.hbs | 50 ++-- .../discourse/app/lib/constants.js | 7 + .../stylesheets/common/admin/admin_base.scss | 1 + .../stylesheets/common/admin/admin_table.scss | 10 + .../stylesheets/common/admin/user_fields.scss | 10 + .../admin/user_fields_controller.rb | 8 + app/models/user_field.rb | 2 + config/locales/client.bs_BA.yml | 2 +- config/locales/client.cs.yml | 2 +- config/locales/client.en.yml | 36 ++- config/routes.rb | 3 + lib/tasks/javascript.rake | 2 + spec/system/admin_user_fields_spec.rb | 9 +- .../page_objects/pages/admin_user_fields.rb | 10 +- 29 files changed, 712 insertions(+), 477 deletions(-) create mode 100644 app/assets/javascripts/admin/addon/components/admin-config-areas/user-fields-list.gjs create mode 100644 app/assets/javascripts/admin/addon/components/admin-user-field-item.gjs delete mode 100644 app/assets/javascripts/admin/addon/components/admin-user-field-item.hbs delete mode 100644 app/assets/javascripts/admin/addon/components/admin-user-field-item.js create mode 100644 app/assets/javascripts/admin/addon/components/admin-user-fields-form.gjs delete mode 100644 app/assets/javascripts/admin/addon/controllers/admin-user-fields.js create mode 100644 app/assets/javascripts/admin/addon/routes/admin-user-fields-edit.js create mode 100644 app/assets/javascripts/admin/addon/routes/admin-user-fields-new.js create mode 100644 app/assets/javascripts/admin/addon/services/admin-user-fields.js create mode 100644 app/assets/javascripts/admin/addon/templates/user-fields-edit.hbs create mode 100644 app/assets/javascripts/admin/addon/templates/user-fields-index.hbs create mode 100644 app/assets/javascripts/admin/addon/templates/user-fields-new.hbs create mode 100644 app/assets/stylesheets/common/admin/user_fields.scss diff --git a/app/assets/javascripts/admin/addon/components/admin-config-areas/user-fields-list.gjs b/app/assets/javascripts/admin/addon/components/admin-config-areas/user-fields-list.gjs new file mode 100644 index 00000000000..643f822f95b --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/admin-config-areas/user-fields-list.gjs @@ -0,0 +1,105 @@ +import Component from "@glimmer/component"; +import { action } from "@ember/object"; +import { service } from "@ember/service"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import { i18n } from "discourse-i18n"; +import AdminConfigAreaEmptyList from "admin/components/admin-config-area-empty-list"; +import AdminUserFieldItem from "admin/components/admin-user-field-item"; +import UserField from "admin/models/user-field"; + +export default class AdminConfigAreasUserFieldsList extends Component { + @service dialog; + @service store; + @service toasts; + @service adminUserFields; + + fieldTypes = UserField.fieldTypes(); + + get fields() { + return this.adminUserFields.userFields; + } + + get sortedFields() { + return this.adminUserFields.sortedUserFields; + } + + @action + moveUp(field) { + const idx = this.sortedFields.indexOf(field); + if (idx) { + const prev = this.sortedFields.objectAt(idx - 1); + const prevPos = prev.get("position"); + + prev.update({ position: field.get("position") }); + field.update({ position: prevPos }); + } + } + + @action + moveDown(field) { + const idx = this.sortedFields.indexOf(field); + if (idx > -1) { + const next = this.sortedFields.objectAt(idx + 1); + const nextPos = next.get("position"); + + next.update({ position: field.get("position") }); + field.update({ position: nextPos }); + } + } + + @action + destroyField(field) { + this.dialog.yesNoConfirm({ + message: i18n("admin.user_fields.delete_confirm"), + didConfirm: () => { + this.#deleteField(field); + }, + }); + } + + async #deleteField(field) { + try { + await field.destroyRecord(); + this.fields.removeObject(field); + this.toasts.success({ + duration: 3000, + data: { + message: i18n("admin.config_areas.user_fields.delete_successful"), + }, + }); + } catch (error) { + popupAjaxError(error); + } + } + + +} diff --git a/app/assets/javascripts/admin/addon/components/admin-user-field-item.gjs b/app/assets/javascripts/admin/addon/components/admin-user-field-item.gjs new file mode 100644 index 00000000000..9097ffc4551 --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/admin-user-field-item.gjs @@ -0,0 +1,134 @@ +import Component from "@glimmer/component"; +import { action } from "@ember/object"; +import { service } from "@ember/service"; +import { htmlSafe } from "@ember/template"; +import DButton from "discourse/components/d-button"; +import DropdownMenu from "discourse/components/dropdown-menu"; +import { USER_FIELD_FLAGS } from "discourse/lib/constants"; +import { i18n } from "discourse-i18n"; +import UserField from "admin/models/user-field"; +import DMenu from "float-kit/components/d-menu"; + +export default class AdminUserFieldItem extends Component { + @service adminUserFields; + @service adminCustomUserFields; + @service dialog; + @service router; + + get fieldName() { + return UserField.fieldTypeById(this.fieldType)?.name; + } + + get cantMoveUp() { + return this.args.userField.id === this.adminUserFields.firstField?.id; + } + + get cantMoveDown() { + return this.args.userField.id === this.adminUserFields.lastField?.id; + } + + get flags() { + return USER_FIELD_FLAGS.map((flag) => { + if (this.args.userField[flag]) { + return i18n(`admin.user_fields.${flag}.enabled`); + } + }) + .filter(Boolean) + .join(", "); + } + + @action + moveUp() { + this.args.moveUpAction(this.args.userField); + this.dMenu.close(); + } + + @action + moveDown() { + this.args.moveDownAction(this.args.userField); + this.dMenu.close(); + } + + @action + destroy() { + this.args.destroyAction(this.args.userField); + this.dMenu.close(); + } + + @action + onRegisterApi(api) { + this.dMenu = api; + } + + @action + edit() { + this.router.transitionTo("adminUserFields.edit", this.args.userField); + } + + +} 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 deleted file mode 100644 index 314d0e2a56e..00000000000 --- a/app/assets/javascripts/admin/addon/components/admin-user-field-item.hbs +++ /dev/null @@ -1,195 +0,0 @@ -{{#if (or this.isEditing (not @userField.id))}} -
-
-
-
- - - {{#each @fieldTypes as |fieldType|}} - {{fieldType.name}} - {{/each}} - - - - - - - - - - - - {{#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}} -
-
-
- {{@userField.name}} -
- {{html-safe @userField.description}} -
-
{{@userField.fieldTypeName}}
-
- - - - -
-
-
{{this.flags}}
-
-{{/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 deleted file mode 100644 index 91bb9d6a79a..00000000000 --- a/app/assets/javascripts/admin/addon/components/admin-user-field-item.js +++ /dev/null @@ -1,143 +0,0 @@ -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"; -import { isEmpty } from "@ember/utils"; -import { tagName } from "@ember-decorators/component"; -import { Promise } from "rsvp"; -import { popupAjaxError } from "discourse/lib/ajax-error"; -import { i18n } from "discourse-i18n"; -import UserField from "admin/models/user-field"; - -@tagName("") -export default class AdminUserFieldItem extends Component { - @service adminCustomUserFields; - @service dialog; - - @tracked isEditing = false; - @tracked - editableDisabled = this.args.userField.requirement === "for_all_users"; - - originalRequirement = this.args.userField.requirement; - - get fieldName() { - return UserField.fieldTypeById(this.fieldType)?.name; - } - - get cantMoveUp() { - return this.args.userField.id === this.args.firstField?.id; - } - - get cantMoveDown() { - return this.args.userField.id === this.args.lastField?.id; - } - - get isNewRecord() { - return isEmpty(this.args.userField?.id); - } - - get flags() { - const flags = [ - "editable", - "show_on_profile", - "show_on_user_card", - "searchable", - ]; - - return flags - .map((flag) => { - if (this.args.userField[flag]) { - return i18n(`admin.user_fields.${flag}.enabled`); - } - }) - .filter(Boolean) - .join(", "); - } - - @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 - 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" && - this.originalRequirement !== "for_all_users" - ) { - confirm = await this._confirmChanges(); - } - - if (!confirm) { - return; - } - - return this.args.userField - .save(data) - .then(() => { - if (this.isDestroying || this.isDestroyed) { - return; - } - - this.originalRequirement = data.requirement; - this.isEditing = false; - }) - .catch(popupAjaxError); - } - - async _confirmChanges() { - return new Promise((resolve) => { - this.dialog.yesNoConfirm({ - message: i18n("admin.user_fields.requirement.confirmation"), - didCancel: () => resolve(false), - didConfirm: () => resolve(true), - }); - }); - } - - @action - edit() { - this.isEditing = true; - } - - @action - cancel() { - if (this.isNewRecord) { - this.args.destroyAction(this.args.userField); - } else { - this.isEditing = false; - } - } - - _focusName() { - schedule("afterRender", () => - document.querySelector(".user-field-name")?.focus() - ); - } -} diff --git a/app/assets/javascripts/admin/addon/components/admin-user-fields-form.gjs b/app/assets/javascripts/admin/addon/components/admin-user-fields-form.gjs new file mode 100644 index 00000000000..ec97f6af81d --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/admin-user-fields-form.gjs @@ -0,0 +1,269 @@ +import Component from "@glimmer/component"; +import { cached, tracked } from "@glimmer/tracking"; +import { hash } from "@ember/helper"; +import { action } from "@ember/object"; +import didInsert from "@ember/render-modifiers/modifiers/did-insert"; +import { schedule } from "@ember/runloop"; +import { service } from "@ember/service"; +import { eq, or } from "truth-helpers"; +import Form from "discourse/components/form"; +import PluginOutlet from "discourse/components/plugin-outlet"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import { i18n } from "discourse-i18n"; +import ValueList from "admin/components/value-list"; +import UserField from "admin/models/user-field"; + +export default class AdminUserFieldsForm extends Component { + @service dialog; + @service router; + @service adminUserFields; + @service adminCustomUserFields; + @service toasts; + + @tracked + editableDisabled = this.args.userField.requirement === "for_all_users"; + originalRequirement = this.args.userField.requirement; + userField; + + get fieldTypes() { + return UserField.fieldTypes(); + } + + @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 + 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" && + this.originalRequirement !== "for_all_users" + ) { + confirm = await this._confirmChanges(); + } + + if (!confirm) { + return; + } + + try { + const isNew = this.args.userField.isNew; + + await this.args.userField.save(data); + + this.originalRequirement = data.requirement; + + if (isNew) { + this.adminUserFields.userFields.pushObject(this.args.userField); + } + + this.router.transitionTo("adminUserFields.index"); + this.toasts.success({ + duration: 3000, + data: { + message: i18n("admin.config_areas.user_fields.save_successful"), + }, + }); + } catch (error) { + popupAjaxError(error); + } + } + + @action + cancel() { + this.router.transitionTo("adminUserFields.index"); + } + + _focusName() { + schedule("afterRender", () => + document.querySelector(".user-field-name")?.focus() + ); + } + + async _confirmChanges() { + return new Promise((resolve) => { + this.dialog.yesNoConfirm({ + message: i18n("admin.user_fields.requirement.confirmation"), + didCancel: () => resolve(false), + didConfirm: () => resolve(true), + }); + }); + } + + +} diff --git a/app/assets/javascripts/admin/addon/controllers/admin-user-fields.js b/app/assets/javascripts/admin/addon/controllers/admin-user-fields.js deleted file mode 100644 index 46c367ed587..00000000000 --- a/app/assets/javascripts/admin/addon/controllers/admin-user-fields.js +++ /dev/null @@ -1,82 +0,0 @@ -import Controller from "@ember/controller"; -import { action } from "@ember/object"; -import { gte, sort } from "@ember/object/computed"; -import { service } from "@ember/service"; -import { popupAjaxError } from "discourse/lib/ajax-error"; -import { i18n } from "discourse-i18n"; - -const MAX_FIELDS = 30; - -export default class AdminUserFieldsController extends Controller { - @service dialog; - - fieldTypes = null; - fieldSortOrder = ["position"]; - - @gte("model.length", MAX_FIELDS) createDisabled; - @sort("model", "fieldSortOrder") sortedFields; - - get firstField() { - return this.sortedFields[0]; - } - - get lastField() { - return this.sortedFields[this.sortedFields.length - 1]; - } - - @action - createField() { - const f = this.store.createRecord("user-field", { - field_type: "text", - requirement: "optional", - position: MAX_FIELDS, - }); - this.model.pushObject(f); - } - - @action - moveUp(f) { - const idx = this.sortedFields.indexOf(f); - if (idx) { - const prev = this.sortedFields.objectAt(idx - 1); - const prevPos = prev.get("position"); - - prev.update({ position: f.get("position") }); - f.update({ position: prevPos }); - } - } - - @action - moveDown(f) { - const idx = this.sortedFields.indexOf(f); - if (idx > -1) { - const next = this.sortedFields.objectAt(idx + 1); - const nextPos = next.get("position"); - - next.update({ position: f.get("position") }); - f.update({ position: nextPos }); - } - } - - @action - destroyField(f) { - const model = this.model; - - // Only confirm if we already been saved - if (f.get("id")) { - this.dialog.yesNoConfirm({ - message: i18n("admin.user_fields.delete_confirm"), - didConfirm: () => { - return f - .destroyRecord() - .then(function () { - model.removeObject(f); - }) - .catch(popupAjaxError); - }, - }); - } else { - model.removeObject(f); - } - } -} diff --git a/app/assets/javascripts/admin/addon/models/user-field.js b/app/assets/javascripts/admin/addon/models/user-field.js index da70af5706a..210b1e4cc3f 100644 --- a/app/assets/javascripts/admin/addon/models/user-field.js +++ b/app/assets/javascripts/admin/addon/models/user-field.js @@ -26,6 +26,7 @@ export default class UserField extends RestModel { @tracked show_on_profile; @tracked show_on_user_card; @tracked searchable; + @tracked requirement; get fieldTypeName() { return UserField.fieldTypes().find((ft) => ft.id === this.field_type).name; diff --git a/app/assets/javascripts/admin/addon/routes/admin-route-map.js b/app/assets/javascripts/admin/addon/routes/admin-route-map.js index c68f82ae59f..30af0d6a217 100644 --- a/app/assets/javascripts/admin/addon/routes/admin-route-map.js +++ b/app/assets/javascripts/admin/addon/routes/admin-route-map.js @@ -68,10 +68,15 @@ export default function () { } ); - this.route("adminUserFields", { - path: "/user_fields", - resetNamespace: true, - }); + this.route( + "adminUserFields", + { path: "/user_fields", resetNamespace: true }, + function () { + this.route("new"); + this.route("edit", { path: "/:id/edit" }); + this.route("index", { path: "/" }); + } + ); this.route( "adminEmojis", { path: "/emojis", resetNamespace: true }, diff --git a/app/assets/javascripts/admin/addon/routes/admin-user-fields-edit.js b/app/assets/javascripts/admin/addon/routes/admin-user-fields-edit.js new file mode 100644 index 00000000000..6877bbc495a --- /dev/null +++ b/app/assets/javascripts/admin/addon/routes/admin-user-fields-edit.js @@ -0,0 +1,12 @@ +import DiscourseRoute from "discourse/routes/discourse"; +import { i18n } from "discourse-i18n"; + +export default class AdminUserFieldsEditRoute extends DiscourseRoute { + model(params) { + return this.store.find("user-field", params.id); + } + + titleToken() { + return i18n("admin.user_fields.edit_header"); + } +} diff --git a/app/assets/javascripts/admin/addon/routes/admin-user-fields-new.js b/app/assets/javascripts/admin/addon/routes/admin-user-fields-new.js new file mode 100644 index 00000000000..022689e019f --- /dev/null +++ b/app/assets/javascripts/admin/addon/routes/admin-user-fields-new.js @@ -0,0 +1,20 @@ +import { service } from "@ember/service"; +import DiscourseRoute from "discourse/routes/discourse"; +import { i18n } from "discourse-i18n"; + +const DEFAULT_VALUES = { + field_type: "text", + requirement: "optional", +}; + +export default class AdminUserFieldsNewRoute extends DiscourseRoute { + @service store; + + async model() { + return this.store.createRecord("user-field", { ...DEFAULT_VALUES }); + } + + titleToken() { + return i18n("admin.user_fields.new_header"); + } +} diff --git a/app/assets/javascripts/admin/addon/routes/admin-user-fields.js b/app/assets/javascripts/admin/addon/routes/admin-user-fields.js index bc3d4666c83..2263bc21ac9 100644 --- a/app/assets/javascripts/admin/addon/routes/admin-user-fields.js +++ b/app/assets/javascripts/admin/addon/routes/admin-user-fields.js @@ -1,12 +1,8 @@ import DiscourseRoute from "discourse/routes/discourse"; -import UserField from "admin/models/user-field"; +import { i18n } from "discourse-i18n"; export default class AdminUserFieldsRoute extends DiscourseRoute { - model() { - return this.store.findAll("user-field"); - } - - setupController(controller, model) { - controller.setProperties({ model, fieldTypes: UserField.fieldTypes() }); + titleToken() { + return i18n("admin.user_fields.title"); } } diff --git a/app/assets/javascripts/admin/addon/services/admin-user-fields.js b/app/assets/javascripts/admin/addon/services/admin-user-fields.js new file mode 100644 index 00000000000..534275f8d67 --- /dev/null +++ b/app/assets/javascripts/admin/addon/services/admin-user-fields.js @@ -0,0 +1,36 @@ +import { tracked } from "@glimmer/tracking"; +import { sort } from "@ember/object/computed"; +import Service, { service } from "@ember/service"; +import { popupAjaxError } from "discourse/lib/ajax-error"; + +export default class AdminUserFields extends Service { + @service store; + + @tracked userFields = []; + + @sort("userFields", "fieldSortOrder") sortedUserFields; + + fieldSortOrder = ["position"]; + + constructor() { + super(...arguments); + + this.#fetchUserFields(); + } + + async #fetchUserFields() { + try { + this.userFields = await this.store.findAll("user-field"); + } catch (err) { + popupAjaxError(err); + } + } + + get firstField() { + return this.sortedUserFields[0]; + } + + get lastField() { + return this.sortedUserFields[this.sortedUserFields.length - 1]; + } +} diff --git a/app/assets/javascripts/admin/addon/templates/user-fields-edit.hbs b/app/assets/javascripts/admin/addon/templates/user-fields-edit.hbs new file mode 100644 index 00000000000..c8bee5f6e12 --- /dev/null +++ b/app/assets/javascripts/admin/addon/templates/user-fields-edit.hbs @@ -0,0 +1,8 @@ + +
+
+
+ +
+
+
\ No newline at end of file diff --git a/app/assets/javascripts/admin/addon/templates/user-fields-index.hbs b/app/assets/javascripts/admin/addon/templates/user-fields-index.hbs new file mode 100644 index 00000000000..e9c497001f6 --- /dev/null +++ b/app/assets/javascripts/admin/addon/templates/user-fields-index.hbs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/javascripts/admin/addon/templates/user-fields-new.hbs b/app/assets/javascripts/admin/addon/templates/user-fields-new.hbs new file mode 100644 index 00000000000..c8bee5f6e12 --- /dev/null +++ b/app/assets/javascripts/admin/addon/templates/user-fields-new.hbs @@ -0,0 +1,8 @@ + +
+
+
+ +
+
+
\ No newline at end of file diff --git a/app/assets/javascripts/admin/addon/templates/user-fields.hbs b/app/assets/javascripts/admin/addon/templates/user-fields.hbs index f8d6fda47c6..1007dafad2e 100644 --- a/app/assets/javascripts/admin/addon/templates/user-fields.hbs +++ b/app/assets/javascripts/admin/addon/templates/user-fields.hbs @@ -1,29 +1,27 @@ -
-
-

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

+
+ + <:breadcrumbs> + + + <:actions as |actions|> + + + -

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

- - {{#if this.model}} - {{#each this.sortedFields as |uf|}} - - {{/each}} - {{/if}} - - +
+
+ {{outlet}} +
\ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/lib/constants.js b/app/assets/javascripts/discourse/app/lib/constants.js index 97fd6f78452..e08fcd52947 100644 --- a/app/assets/javascripts/discourse/app/lib/constants.js +++ b/app/assets/javascripts/discourse/app/lib/constants.js @@ -99,3 +99,10 @@ export const SITE_SETTING_REQUIRES_CONFIRMATION_TYPES = { }; export const MAX_UNOPTIMIZED_CATEGORIES = 1000; + +export const USER_FIELD_FLAGS = [ + "editable", + "show_on_profile", + "show_on_user_card", + "searchable", +]; diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 5a16b2f3388..cf92963f4f9 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -1078,6 +1078,7 @@ a.inline-editable-field { @import "common/admin/badges"; @import "common/admin/emails"; @import "common/admin/flags"; +@import "common/admin/user_fields"; @import "common/admin/json_schema_editor"; @import "common/admin/schema_field"; @import "common/admin/staff_logs"; diff --git a/app/assets/stylesheets/common/admin/admin_table.scss b/app/assets/stylesheets/common/admin/admin_table.scss index 1d18d2c1a37..0516d7e9914 100644 --- a/app/assets/stylesheets/common/admin/admin_table.scss +++ b/app/assets/stylesheets/common/admin/admin_table.scss @@ -94,6 +94,16 @@ margin-bottom: 0.1em; } } + + &-flags { + color: var(--primary-high); + font-size: var(--font-down-1); + text-transform: lowercase; + + &::first-letter { + text-transform: uppercase; + } + } } .d-admin-row__controls { diff --git a/app/assets/stylesheets/common/admin/user_fields.scss b/app/assets/stylesheets/common/admin/user_fields.scss new file mode 100644 index 00000000000..bb5da5f08d3 --- /dev/null +++ b/app/assets/stylesheets/common/admin/user_fields.scss @@ -0,0 +1,10 @@ +.admin-user_field-item { + &__delete.btn, + &__delete.btn:hover { + border-top: 1px solid var(--primary-low); + color: var(--danger); + svg { + color: var(--danger); + } + } +} diff --git a/app/controllers/admin/user_fields_controller.rb b/app/controllers/admin/user_fields_controller.rb index 14066232810..95bcf4a551f 100644 --- a/app/controllers/admin/user_fields_controller.rb +++ b/app/controllers/admin/user_fields_controller.rb @@ -30,6 +30,14 @@ class Admin::UserFieldsController < Admin::AdminController render_serialized(user_fields, UserFieldSerializer, root: "user_fields") end + def show + user_field = UserField.find(params[:id]) + render_serialized(user_field, UserFieldSerializer) + end + + def edit + end + def update field_params = params[:user_field] field = UserField.where(id: params.require(:id)).first diff --git a/app/models/user_field.rb b/app/models/user_field.rb index 212f948d286..7607ff8b0dd 100644 --- a/app/models/user_field.rb +++ b/app/models/user_field.rb @@ -5,6 +5,8 @@ class UserField < ActiveRecord::Base include HasDeprecatedColumns include HasSanitizableFields + FLAG_ATTRIBUTES = %w[editable show_on_profile show_on_user_card searchable].freeze + deprecate_column :required, drop_from: "3.3" self.ignored_columns += %i[field_type] diff --git a/config/locales/client.bs_BA.yml b/config/locales/client.bs_BA.yml index 5653cca6573..770caf63ac7 100644 --- a/config/locales/client.bs_BA.yml +++ b/config/locales/client.bs_BA.yml @@ -4275,7 +4275,7 @@ bs_BA: enabled: "prikazano na korisničkoj kartici" disabled: "nije prikazano na korisničkoj kartici" field_types: - text: "Text Field" + text: "Text" confirm: "Confirmation" dropdown: "Ispustiti" site_text: diff --git a/config/locales/client.cs.yml b/config/locales/client.cs.yml index e42c445c937..e221f71ba5b 100644 --- a/config/locales/client.cs.yml +++ b/config/locales/client.cs.yml @@ -6412,7 +6412,7 @@ cs: enabled: "zobrazeno na kartě uživatele" disabled: "Nezobrazeno na kartě uživatele" field_types: - text: "Text Field" + text: "Text" confirm: "Potvrzení" dropdown: "Menu" multiselect: "Více možností" diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index ce9419bf53a..b76fece785b 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -5737,6 +5737,16 @@ en: themes_description: "Themes are expansive customizations that change multiple elements of the style of your forum design, and often also include additional front-end features." new_theme: "New theme" user_selectable: "User selectable" + user_fields: + field: "Field" + type: "Type" + more_options: + title: "More options" + move_up: "Move up" + move_down: "Move down" + delete: "Delete" + delete_successful: "User field deleted." + save_successful: "User field saved." plugins: title: "Plugins" installed: "Installed plugins" @@ -6946,8 +6956,12 @@ en: user_fields: title: "User Fields" - help: "Add fields that your users can fill out." - create: "Create User Field" + help: "Create custom user fields to collect extra details about your community members. You can choose what information is required during sign-up, what shows on profiles, and what users can update." + no_user_fields: "You don't have any custom user fields yet." + add: "Add user field" + back: "Back to user fields" + edit_header: "Edit User Field" + new_header: "Add User Field" untitled: "Untitled" name: "Field Name" type: "Field Type" @@ -6976,23 +6990,23 @@ en: confirmation: "This will prompt existing users to fill in this field and will not allow them to do anything else on your site until the field is filled. Proceed?" editable: title: "Editable after signup" - enabled: "editable" - disabled: "not editable" + enabled: "Editable" + disabled: "Not editable" show_on_profile: title: "Show on public profile" - enabled: "shown on profile" - disabled: "not shown on profile" + enabled: "Shown on profile" + disabled: "Not shown on profile" show_on_user_card: title: "Show on user card" - enabled: "shown on user card" - disabled: "not shown on user card" + enabled: "Shown on user card" + disabled: "Not shown on user card" searchable: title: "Searchable" - enabled: "searchable" - disabled: "not searchable" + enabled: "Searchable" + disabled: "Not searchable" field_types: - text: "Text Field" + text: "Text" confirm: "Confirmation" dropdown: "Dropdown" multiselect: "Multiselect" diff --git a/config/routes.rb b/config/routes.rb index 3d9e2a8b3af..df71ef73285 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -245,6 +245,9 @@ Discourse::Application.routes.draw do resources :user_fields, only: %i[index create update destroy], constraints: AdminConstraint.new + get "user_fields/new" => "user_fields#index" + get "user_fields/:id" => "user_fields#show" + get "user_fields/:id/edit" => "user_fields#edit" resources :emojis, only: %i[index create destroy], constraints: AdminConstraint.new get "emojis/new" => "emojis#index" get "emojis/settings" => "emojis#index" diff --git a/lib/tasks/javascript.rake b/lib/tasks/javascript.rake index bd7e8a6dbf3..a794bef55c5 100644 --- a/lib/tasks/javascript.rake +++ b/lib/tasks/javascript.rake @@ -165,6 +165,8 @@ task "javascript:update_constants" => :environment do export const SITE_SETTING_REQUIRES_CONFIRMATION_TYPES = #{SiteSettings::TypeSupervisor::REQUIRES_CONFIRMATION_TYPES.to_json}; export const MAX_UNOPTIMIZED_CATEGORIES = #{CategoryList::MAX_UNOPTIMIZED_CATEGORIES}; + + export const USER_FIELD_FLAGS = #{UserField::FLAG_ATTRIBUTES}; JS pretty_notifications = Notification.types.map { |n| " #{n[0]}: #{n[1]}," }.join("\n") diff --git a/spec/system/admin_user_fields_spec.rb b/spec/system/admin_user_fields_spec.rb index ad0a423caf4..8c77facff95 100644 --- a/spec/system/admin_user_fields_spec.rb +++ b/spec/system/admin_user_fields_spec.rb @@ -28,8 +28,7 @@ describe "Admin User Fields", type: :system do it "makes sure new required fields are editable after signup" do user_fields_page.visit - - page.find(".user-fields .btn-primary").click + user_fields_page.click_add_field form = page.find(".user-field") editable_label = I18n.t("admin_js.admin.user_fields.editable.title") @@ -45,8 +44,7 @@ describe "Admin User Fields", type: :system do it "requires confirmation when applying required fields retroactively" do user_fields_page.visit - - page.find(".user-fields .btn-primary").click + user_fields_page.click_add_field form = page.find(".user-field") @@ -65,8 +63,7 @@ describe "Admin User Fields", type: :system do it "does not require confirmation if the field already applies to all users" do user_fields_page.visit - - page.find(".user-field .edit").click + user_fields_page.click_edit form = page.find(".user-field") diff --git a/spec/system/page_objects/pages/admin_user_fields.rb b/spec/system/page_objects/pages/admin_user_fields.rb index 664d51c04dd..297d1e66881 100644 --- a/spec/system/page_objects/pages/admin_user_fields.rb +++ b/spec/system/page_objects/pages/admin_user_fields.rb @@ -18,8 +18,16 @@ module PageObjects form.choose(I18n.t("admin_js.admin.user_fields.requirement.#{requirement}.title")) end + def click_add_field + page.find(".admin-page-header__actions .btn-primary").click + end + + def click_edit + page.find(".admin-user_field-item__edit").click + end + def add_field(name: nil, description: nil, requirement: nil, preferences: []) - page.find(".user-fields .btn-primary").click + click_add_field form = page.find(".user-field")