FEATURE: Implement new required options in admin user fields UI (#27079)

We're planning to implement a feature that allows adding required fields for existing users. This PR does some preparatory refactoring to make that possible. There should be no changes to existing behaviour. Just a small update to the admin UI.
This commit is contained in:
Ted Johansson 2024-05-23 19:18:25 +08:00 committed by GitHub
parent f5e41f0627
commit 7b437c9401
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 234 additions and 37 deletions

View File

@ -30,29 +30,51 @@
</AdminFormRow> </AdminFormRow>
{{/if}} {{/if}}
<AdminFormRow @wrapLabel="true" @type="checkbox"> <AdminFormRow @label="admin.user_fields.requirement.title">
<Input @type="checkbox" @checked={{this.buffered.editable}} /> <label class="optional">
<span>{{i18n "admin.user_fields.editable.title"}}</span> <RadioButton
@value="optional"
@name="optional"
@selection={{this.buffered.requirement}}
/>
<span>{{i18n "admin.user_fields.requirement.optional.title"}}</span>
</label>
<label class="on_signup">
<RadioButton
@value="on_signup"
@name="on_signup"
@selection={{this.buffered.requirement}}
/>
<div class="label-text">
<span>{{i18n "admin.user_fields.requirement.on_signup.title"}}</span>
<div class="description">{{i18n
"admin.user_fields.requirement.on_signup.description"
}}</div>
</div>
</label>
</AdminFormRow> </AdminFormRow>
<AdminFormRow @wrapLabel="true" @type="checkbox"> <AdminFormRow @label="admin.user_fields.preferences">
<Input @type="checkbox" @checked={{this.buffered.required}} /> <label>
<span>{{i18n "admin.user_fields.required.title"}}</span> <Input @type="checkbox" @checked={{this.buffered.editable}} />
</AdminFormRow> <span>{{i18n "admin.user_fields.editable.title"}}</span>
</label>
<AdminFormRow @wrapLabel="true" @type="checkbox"> <label>
<Input @type="checkbox" @checked={{this.buffered.show_on_profile}} /> <Input @type="checkbox" @checked={{this.buffered.show_on_profile}} />
<span>{{i18n "admin.user_fields.show_on_profile.title"}}</span> <span>{{i18n "admin.user_fields.show_on_profile.title"}}</span>
</AdminFormRow> </label>
<AdminFormRow @wrapLabel="true" @type="checkbox"> <label>
<Input @type="checkbox" @checked={{this.buffered.show_on_user_card}} /> <Input @type="checkbox" @checked={{this.buffered.show_on_user_card}} />
<span>{{i18n "admin.user_fields.show_on_user_card.title"}}</span> <span>{{i18n "admin.user_fields.show_on_user_card.title"}}</span>
</AdminFormRow> </label>
<AdminFormRow @wrapLabel="true" @type="checkbox"> <label>
<Input @type="checkbox" @checked={{this.buffered.searchable}} /> <Input @type="checkbox" @checked={{this.buffered.searchable}} />
<span>{{i18n "admin.user_fields.searchable.title"}}</span> <span>{{i18n "admin.user_fields.searchable.title"}}</span>
</label>
</AdminFormRow> </AdminFormRow>
<PluginOutlet <PluginOutlet

View File

@ -44,16 +44,13 @@ export default Component.extend(bufferedProperty("userField"), {
}, },
@discourseComputed( @discourseComputed(
"userField.{editable,required,show_on_profile,show_on_user_card,searchable}" "userField.{editable,show_on_profile,show_on_user_card,searchable}"
) )
flags(userField) { flags(userField) {
const ret = []; const ret = [];
if (userField.editable) { if (userField.editable) {
ret.push(I18n.t("admin.user_fields.editable.enabled")); ret.push(I18n.t("admin.user_fields.editable.enabled"));
} }
if (userField.required) {
ret.push(I18n.t("admin.user_fields.required.enabled"));
}
if (userField.show_on_profile) { if (userField.show_on_profile) {
ret.push(I18n.t("admin.user_fields.show_on_profile.enabled")); ret.push(I18n.t("admin.user_fields.show_on_profile.enabled"));
} }
@ -74,7 +71,7 @@ export default Component.extend(bufferedProperty("userField"), {
"description", "description",
"field_type", "field_type",
"editable", "editable",
"required", "requirement",
"show_on_profile", "show_on_profile",
"show_on_user_card", "show_on_user_card",
"searchable", "searchable",

View File

@ -994,6 +994,22 @@ table.permalinks {
width: 100%; 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 { &.label-area {
width: 25%; width: 25%;

View File

@ -7,7 +7,7 @@ class Admin::UserFieldsController < Admin::AdminController
field_type field_type
editable editable
description description
required requirement
show_on_profile show_on_profile
show_on_user_card show_on_user_card
position position
@ -20,7 +20,6 @@ class Admin::UserFieldsController < Admin::AdminController
field = UserField.new(params.require(:user_field).permit(*Admin::UserFieldsController.columns)) field = UserField.new(params.require(:user_field).permit(*Admin::UserFieldsController.columns))
field.position = (UserField.maximum(:position) || 0) + 1 field.position = (UserField.maximum(:position) || 0) + 1
field.required = params[:user_field][:required] == "true"
update_options(field) update_options(field)
json_result(field, serializer: UserFieldSerializer) { field.save } json_result(field, serializer: UserFieldSerializer) { field.save }

View File

@ -2,8 +2,11 @@
class UserField < ActiveRecord::Base class UserField < ActiveRecord::Base
include AnonCacheInvalidator include AnonCacheInvalidator
include HasDeprecatedColumns
include HasSanitizableFields include HasSanitizableFields
deprecate_column :required, drop_from: "3.3"
validates_presence_of :description, :field_type validates_presence_of :description, :field_type
validates_presence_of :name, unless: -> { field_type == "confirm" } validates_presence_of :name, unless: -> { field_type == "confirm" }
has_many :user_field_options, dependent: :destroy has_many :user_field_options, dependent: :destroy
@ -15,10 +18,16 @@ class UserField < ActiveRecord::Base
scope :public_fields, -> { where(show_on_profile: true).or(where(show_on_user_card: true)) } scope :public_fields, -> { where(show_on_profile: true).or(where(show_on_user_card: true)) }
enum :requirement, { optional: 0, for_all_users: 1, on_signup: 2 }.freeze
def self.max_length def self.max_length
2048 2048
end end
def required?
!optional?
end
def queue_index_search def queue_index_search
Jobs.enqueue(:index_user_fields_for_search, user_field_id: self.id) Jobs.enqueue(:index_user_fields_for_search, user_field_id: self.id)
end end
@ -50,4 +59,5 @@ end
# external_name :string # external_name :string
# external_type :string # external_type :string
# searchable :boolean default(FALSE), not null # searchable :boolean default(FALSE), not null
# requirement :integer default("optional"), not null
# #

View File

@ -7,12 +7,17 @@ class UserFieldSerializer < ApplicationSerializer
:field_type, :field_type,
:editable, :editable,
:required, :required,
:requirement,
:show_on_profile, :show_on_profile,
:show_on_user_card, :show_on_user_card,
:searchable, :searchable,
:position, :position,
:options :options
def required
object.required?
end
def options def options
object.user_field_options.pluck(:value) object.user_field_options.pluck(:value)
end end

View File

@ -6586,6 +6586,7 @@ en:
name: "Field Name" name: "Field Name"
type: "Field Type" type: "Field Type"
description: "Field Description" description: "Field Description"
preferences: "Preferences"
save: "Save" save: "Save"
edit: "Edit" edit: "Edit"
delete: "Delete" delete: "Delete"
@ -6596,6 +6597,13 @@ en:
title: "Required at signup" title: "Required at signup"
enabled: "required" enabled: "required"
disabled: "not required" disabled: "not required"
requirement:
title: "Field Requirement"
optional:
title: "Optional"
on_signup:
title: "On signup"
description: "When new users sign up, they must fill out this field. Existing users are unaffected."
editable: editable:
title: "Editable after signup" title: "Editable after signup"
enabled: "editable" enabled: "editable"

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class AddRequirementToUserFields < ActiveRecord::Migration[7.0]
def up
add_column :user_fields, :requirement, :integer, null: false, default: 0
execute <<~SQL
UPDATE user_fields
SET requirement =
(CASE WHEN required IS NOT TRUE THEN 0 ELSE 2 END)
SQL
end
def down
remove_column :user_fields, :requirement
end
end

View File

@ -5,5 +5,5 @@ Fabricator(:user_field) do
description "user field description" description "user field description"
field_type "text" field_type "text"
editable true editable true
required true requirement "on_signup"
end end

View File

@ -1,6 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
RSpec.describe UserField do RSpec.describe UserField do
it do
is_expected.to define_enum_for(:requirement).with_values(%w[optional for_all_users on_signup])
end
describe "doesn't validate presence of name if field type is 'confirm'" do describe "doesn't validate presence of name if field type is 'confirm'" do
subject(:confirm_field) { described_class.new(field_type: "confirm") } subject(:confirm_field) { described_class.new(field_type: "confirm") }
@ -40,4 +44,26 @@ RSpec.describe UserField do
job_enqueued?(job: Jobs::IndexUserFieldsForSearch, args: { user_field_id: user_field.id }), job_enqueued?(job: Jobs::IndexUserFieldsForSearch, args: { user_field_id: user_field.id }),
).to eq(true) ).to eq(true)
end end
describe "#required?" do
let(:user_field) { Fabricate(:user_field, requirement:) }
context "when requirement is optional" do
let(:requirement) { "optional" }
it { expect(user_field).not_to be_required }
end
context "when requirement is for all users" do
let(:requirement) { "for_all_users" }
it { expect(user_field).to be_required }
end
context "when requirement is on signup" do
let(:requirement) { "on_signup" }
it { expect(user_field).to be_required }
end
end
end end

View File

@ -17,6 +17,7 @@ RSpec.describe Admin::UserFieldsController do
name: "hello", name: "hello",
description: "hello desc", description: "hello desc",
field_type: "text", field_type: "text",
requirement: "on_signup",
}, },
} }
@ -33,6 +34,7 @@ RSpec.describe Admin::UserFieldsController do
description: "hello desc", description: "hello desc",
field_type: "dropdown", field_type: "dropdown",
options: %w[a b c], options: %w[a b c],
requirement: "on_signup",
}, },
} }
@ -162,13 +164,16 @@ RSpec.describe Admin::UserFieldsController do
name: "fraggle", name: "fraggle",
field_type: "confirm", field_type: "confirm",
description: "muppet", description: "muppet",
requirement: "optional",
}, },
} }
expect(response.status).to eq(200) expect(response.status).to eq(200)
user_field.reload expect(user_field.reload).to have_attributes(
expect(user_field.name).to eq("fraggle") name: "fraggle",
expect(user_field.field_type).to eq("confirm") field_type: "confirm",
required?: false,
)
end end
it "updates the user field options" do it "updates the user field options" do

View File

@ -1432,7 +1432,7 @@ RSpec.describe UsersController do
context "with custom fields" do context "with custom fields" do
fab!(:user_field) fab!(:user_field)
fab!(:another_field) { Fabricate(:user_field) } fab!(:another_field) { Fabricate(:user_field) }
fab!(:optional_field) { Fabricate(:user_field, required: false) } fab!(:optional_field) { Fabricate(:user_field, requirement: "optional") }
context "without a value for the fields" do context "without a value for the fields" do
let(:create_params) do let(:create_params) do
@ -1492,7 +1492,7 @@ RSpec.describe UsersController do
it "value can't be nil or empty if the field is required" do it "value can't be nil or empty if the field is required" do
put update_user_url, params: { user_fields: { field_id => valid_options } } put update_user_url, params: { user_fields: { field_id => valid_options } }
user_field.update!(required: true) user_field.for_all_users!
expect do expect do
put update_user_url, params: { user_fields: { field_id => nil } } put update_user_url, params: { user_fields: { field_id => nil } }
@ -1506,7 +1506,7 @@ RSpec.describe UsersController do
it "value can nil or empty if the field is not required" do it "value can nil or empty if the field is not required" do
put update_user_url, params: { user_fields: { field_id => valid_options } } put update_user_url, params: { user_fields: { field_id => valid_options } }
user_field.update!(required: false) user_field.optional!
expect do expect do
put update_user_url, params: { user_fields: { field_id => nil } } put update_user_url, params: { user_fields: { field_id => nil } }
@ -1547,7 +1547,7 @@ RSpec.describe UsersController do
it "value can't be nil if the field is required" do it "value can't be nil if the field is required" do
put update_user_url, params: { user_fields: { field_id => valid_options.first } } put update_user_url, params: { user_fields: { field_id => valid_options.first } }
user_field.update!(required: true) user_field.for_all_users!
expect do expect do
put update_user_url, params: { user_fields: { field_id => nil } } put update_user_url, params: { user_fields: { field_id => nil } }
@ -1557,7 +1557,7 @@ RSpec.describe UsersController do
it "value can be set to nil if the field is not required" do it "value can be set to nil if the field is not required" do
put update_user_url, params: { user_fields: { field_id => valid_options.last } } put update_user_url, params: { user_fields: { field_id => valid_options.last } }
user_field.update!(required: false) user_field.optional!
expect do expect do
put update_user_url, params: { user_fields: { field_id => nil } } put update_user_url, params: { user_fields: { field_id => nil } }
@ -1614,7 +1614,7 @@ RSpec.describe UsersController do
end end
context "with only optional custom fields" do context "with only optional custom fields" do
fab!(:user_field) { Fabricate(:user_field, required: false) } fab!(:user_field) { Fabricate(:user_field, requirement: "optional") }
context "without values for the fields" do context "without values for the fields" do
let(:create_params) do let(:create_params) do
@ -2378,8 +2378,8 @@ RSpec.describe UsersController do
context "with user fields" do context "with user fields" do
context "with an editable field" do context "with an editable field" do
fab!(:user_field) fab!(:user_field) { Fabricate(:user_field, requirement: "for_all_users") }
fab!(:optional_field) { Fabricate(:user_field, required: false) } fab!(:optional_field) { Fabricate(:user_field, requirement: "optional") }
it "should update the user field" do it "should update the user field" do
put "/u/#{user.username}.json", put "/u/#{user.username}.json",

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
describe "Admin User Fields", type: :system, js: true do
fab!(:current_user) { Fabricate(:admin) }
before { sign_in(current_user) }
let(:user_fields_page) { PageObjects::Pages::AdminUserFields.new }
it "correctly saves user fields" do
user_fields_page.visit
user_fields_page.add_field(name: "Occupation", description: "What you do for work")
expect(user_fields_page).to have_user_field("Occupation")
user_fields_page.refresh
expect(user_fields_page).to have_user_field("Occupation")
end
it "displays an error when missing required fields" do
user_fields_page.visit
user_fields_page.add_field(name: "Occupation", description: "")
expect(user_fields_page).to have_text(/Description can't be blank/)
end
end

View File

@ -55,6 +55,10 @@ module PageObjects
find("#inviteCode").fill_in(with: code) find("#inviteCode").fill_in(with: code)
end end
def fill_custom_field(name, value)
find(".user-field-#{name.downcase} input").fill_in(with: value)
end
def has_valid_email? def has_valid_email?
find(".create-account-email").has_css?("#account-email-validation.good") find(".create-account-email").has_css?("#account-email-validation.good")
end end

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
module PageObjects
module Pages
class AdminUserFields < PageObjects::Pages::Base
def visit
page.visit "admin/customize/user_fields"
self
end
def add_field(name: nil, description: nil, requirement: nil, preferences: [])
page.find(".user-fields .btn-primary").click
form = page.find(".user-field")
form.find(".user-field-name").fill_in(with: name)
form.find(".user-field-desc").fill_in(with: description)
form.find(".save").click
end
def has_user_field?(name)
page.has_text?(name)
end
end
end
end

View File

@ -47,6 +47,40 @@ describe "Signup", type: :system do
end end
end end
context "when there are required user fields" do
before do
Fabricate(
:user_field,
name: "Occupation",
requirement: "on_signup",
description: "What you do for work",
)
end
it "can signup when filling the custom field" do
signup_modal.open
signup_modal.fill_email("johndoe@example.com")
signup_modal.fill_username("john")
signup_modal.fill_password("supersecurepassword")
signup_modal.fill_custom_field("Occupation", "Jedi")
expect(signup_modal).to have_valid_fields
signup_modal.click_create_account
expect(page).to have_css(".account-created")
end
it "cannot signup without filling the custom field" do
signup_modal.open
signup_modal.fill_email("johndoe@example.com")
signup_modal.fill_username("john")
signup_modal.fill_password("supersecurepassword")
signup_modal.click_create_account
expect(signup_modal).to have_content(I18n.t("js.user_fields.required", name: "Occupation"))
expect(signup_modal).to have_no_css(".account-created")
end
end
context "when user requires approval" do context "when user requires approval" do
before do before do
SiteSetting.must_approve_users = true SiteSetting.must_approve_users = true