mirror of
https://github.com/discourse/discourse.git
synced 2024-11-29 15:55:08 +08:00
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.
This commit is contained in:
parent
66409fa8b4
commit
88af23e1ca
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="container admin-user_fields">
|
||||
{{#if this.fields}}
|
||||
<table class="d-admin-table admin-flags__items">
|
||||
<thead>
|
||||
<th>{{i18n "admin.config_areas.user_fields.field"}}</th>
|
||||
<th>{{i18n "admin.config_areas.user_fields.type"}}</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each this.sortedFields as |field|}}
|
||||
<AdminUserFieldItem
|
||||
@userField={{field}}
|
||||
@fieldTypes={{this.fieldTypes}}
|
||||
@destroyAction={{this.destroyField}}
|
||||
@moveUpAction={{this.moveUp}}
|
||||
@moveDownAction={{this.moveDown}}
|
||||
/>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<AdminConfigAreaEmptyList
|
||||
@ctaLabel="admin.user_fields.add"
|
||||
@ctaRoute="adminUserFields.new"
|
||||
@ctaClass="admin-user_fields__add-emoji"
|
||||
@emptyLabel="admin.user_fields.no_user_fields"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
<template>
|
||||
<tr class="d-admin-row__content admin-user_field-item">
|
||||
<td class="d-admin-row__overview">
|
||||
<div
|
||||
class="d-admin-row__overview-name admin-user_field-item__name"
|
||||
>{{@userField.name}}</div>
|
||||
<div class="d-admin-row__overview-about">{{htmlSafe
|
||||
@userField.description
|
||||
}}</div>
|
||||
<div class="d-admin-row__overview-flags">{{this.flags}}</div>
|
||||
</td>
|
||||
<td class="d-admin-row__detail">
|
||||
{{@userField.fieldTypeName}}
|
||||
</td>
|
||||
<td class="d-admin-row__controls">
|
||||
<div class="d-admin-row__controls-options">
|
||||
<DButton
|
||||
class="btn-small admin-user_field-item__edit"
|
||||
@action={{this.edit}}
|
||||
@label="admin.user_fields.edit"
|
||||
/>
|
||||
|
||||
<DMenu
|
||||
@identifier="user_field-menu"
|
||||
@title={{i18n "admin.config_areas.user_fields.more_options.title"}}
|
||||
@icon="ellipsis-vertical"
|
||||
@onRegisterApi={{this.onRegisterApi}}
|
||||
>
|
||||
<:content>
|
||||
<DropdownMenu as |dropdown|>
|
||||
{{#unless this.cantMoveUp}}
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
@label="admin.config_areas.user_fields.more_options.move_up"
|
||||
@icon="arrow-up"
|
||||
class="btn-transparent admin-user_field-item__move-up"
|
||||
@action={{this.moveUp}}
|
||||
/>
|
||||
</dropdown.item>
|
||||
{{/unless}}
|
||||
{{#unless this.cantMoveDown}}
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
@label="admin.config_areas.user_fields.more_options.move_down"
|
||||
@icon="arrow-down"
|
||||
class="btn-transparent admin-user_field-item__move-down"
|
||||
@action={{this.moveDown}}
|
||||
/>
|
||||
</dropdown.item>
|
||||
{{/unless}}
|
||||
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
@label="admin.config_areas.user_fields.delete"
|
||||
@icon="trash-can"
|
||||
class="btn-transparent admin-user_field-item__delete"
|
||||
@action={{this.destroy}}
|
||||
/>
|
||||
</dropdown.item>
|
||||
</DropdownMenu>
|
||||
</:content>
|
||||
</DMenu>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
}
|
|
@ -1,195 +0,0 @@
|
|||
{{#if (or this.isEditing (not @userField.id))}}
|
||||
<div class="admin-config-area user-field">
|
||||
<div class="admin-config-area__primary-content">
|
||||
<div class="admin-config-area-card">
|
||||
<Form
|
||||
@data={{this.formData}}
|
||||
@onSubmit={{this.save}}
|
||||
{{did-insert this._focusName}}
|
||||
as |form transientData|
|
||||
>
|
||||
<form.Field
|
||||
@name="field_type"
|
||||
@title={{i18n "admin.user_fields.type"}}
|
||||
@format="large"
|
||||
@validation="required"
|
||||
as |field|
|
||||
>
|
||||
<field.Select as |select|>
|
||||
{{#each @fieldTypes as |fieldType|}}
|
||||
<select.Option
|
||||
@value={{fieldType.id}}
|
||||
>{{fieldType.name}}</select.Option>
|
||||
{{/each}}
|
||||
</field.Select>
|
||||
</form.Field>
|
||||
|
||||
<form.Field
|
||||
@name="name"
|
||||
@title={{i18n "admin.user_fields.name"}}
|
||||
@format="large"
|
||||
@validation="required"
|
||||
as |field|
|
||||
>
|
||||
<field.Input class="user-field-name" maxlength="255" />
|
||||
</form.Field>
|
||||
|
||||
<form.Field
|
||||
@name="description"
|
||||
@title={{i18n "admin.user_fields.description"}}
|
||||
@format="large"
|
||||
@validation="required"
|
||||
as |field|
|
||||
>
|
||||
<field.Input class="user-field-desc" maxlength="1000" />
|
||||
</form.Field>
|
||||
|
||||
{{#if
|
||||
(or
|
||||
(eq transientData.field_type "dropdown")
|
||||
(eq transientData.field_type "multiselect")
|
||||
)
|
||||
}}
|
||||
<form.Field
|
||||
@name="options"
|
||||
@title={{i18n "admin.user_fields.options"}}
|
||||
@format="large"
|
||||
@validation="required"
|
||||
as |field|
|
||||
>
|
||||
<field.Custom>
|
||||
<ValueList
|
||||
@values={{transientData.options}}
|
||||
@inputType="array"
|
||||
@onChange={{field.set}}
|
||||
/>
|
||||
</field.Custom>
|
||||
</form.Field>
|
||||
{{/if}}
|
||||
|
||||
<form.Field
|
||||
@name="requirement"
|
||||
@title={{i18n "admin.user_fields.requirement.title"}}
|
||||
@validation="required"
|
||||
@onSet={{this.setRequirement}}
|
||||
@format="full"
|
||||
as |field|
|
||||
>
|
||||
<field.RadioGroup as |radioGroup|>
|
||||
<radioGroup.Radio @value="optional">
|
||||
{{i18n "admin.user_fields.requirement.optional.title"}}
|
||||
</radioGroup.Radio>
|
||||
<radioGroup.Radio @value="for_all_users" as |radio|>
|
||||
{{i18n "admin.user_fields.requirement.for_all_users.title"}}
|
||||
<radio.Description>{{i18n
|
||||
"admin.user_fields.requirement.for_all_users.description"
|
||||
}}</radio.Description>
|
||||
</radioGroup.Radio>
|
||||
<radioGroup.Radio @value="on_signup" as |radio|>
|
||||
{{i18n "admin.user_fields.requirement.on_signup.title"}}
|
||||
<radio.Description>{{i18n
|
||||
"admin.user_fields.requirement.on_signup.description"
|
||||
}}</radio.Description>
|
||||
</radioGroup.Radio>
|
||||
</field.RadioGroup>
|
||||
</form.Field>
|
||||
|
||||
<form.CheckboxGroup
|
||||
class="user-field-preferences"
|
||||
@title={{i18n "admin.user_fields.preferences"}}
|
||||
as |group|
|
||||
>
|
||||
<group.Field
|
||||
@name="editable"
|
||||
@showTitle={{false}}
|
||||
@title={{i18n "admin.user_fields.editable.title"}}
|
||||
as |field|
|
||||
>
|
||||
<field.Checkbox disabled={{this.editableDisabled}} />
|
||||
</group.Field>
|
||||
<group.Field
|
||||
@name="show_on_profile"
|
||||
@showTitle={{false}}
|
||||
@title={{i18n "admin.user_fields.show_on_profile.title"}}
|
||||
as |field|
|
||||
>
|
||||
<field.Checkbox />
|
||||
</group.Field>
|
||||
<group.Field
|
||||
@name="show_on_user_card"
|
||||
@showTitle={{false}}
|
||||
@title={{i18n "admin.user_fields.show_on_user_card.title"}}
|
||||
as |field|
|
||||
>
|
||||
<field.Checkbox />
|
||||
</group.Field>
|
||||
<group.Field
|
||||
@name="searchable"
|
||||
@showTitle={{false}}
|
||||
@title={{i18n "admin.user_fields.searchable.title"}}
|
||||
as |field|
|
||||
>
|
||||
<field.Checkbox />
|
||||
</group.Field>
|
||||
</form.CheckboxGroup>
|
||||
|
||||
<PluginOutlet
|
||||
@name="after-admin-user-fields"
|
||||
@outletArgs={{hash userField=@userField form=form}}
|
||||
/>
|
||||
|
||||
<form.Actions>
|
||||
<form.Submit
|
||||
class="save"
|
||||
@icon="check"
|
||||
@label="admin.user_fields.save"
|
||||
/>
|
||||
<form.Button
|
||||
@action={{this.cancel}}
|
||||
@label="admin.user_fields.cancel"
|
||||
class="btn-default"
|
||||
/>
|
||||
</form.Actions>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="user-field">
|
||||
<div class="row">
|
||||
<div class="form-display">
|
||||
<b class="name">{{@userField.name}}</b>
|
||||
<br />
|
||||
<span class="description">{{html-safe @userField.description}}</span>
|
||||
</div>
|
||||
<div class="form-display field-type">{{@userField.fieldTypeName}}</div>
|
||||
<div class="form-element controls">
|
||||
<DButton
|
||||
@action={{this.edit}}
|
||||
@icon="pencil"
|
||||
@label="admin.user_fields.edit"
|
||||
class="btn-default edit"
|
||||
/>
|
||||
<DButton
|
||||
@action={{fn @destroyAction @userField}}
|
||||
@icon="trash-can"
|
||||
@label="admin.user_fields.delete"
|
||||
class="btn-danger cancel"
|
||||
/>
|
||||
<DButton
|
||||
@action={{fn @moveUpAction @userField}}
|
||||
@icon="arrow-up"
|
||||
@disabled={{this.cantMoveUp}}
|
||||
class="btn-default"
|
||||
/>
|
||||
<DButton
|
||||
@action={{fn @moveDownAction @userField}}
|
||||
@icon="arrow-down"
|
||||
@disabled={{this.cantMoveDown}}
|
||||
class="btn-default"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row user-field-flags">{{this.flags}}</div>
|
||||
</div>
|
||||
{{/if}}
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
<template>
|
||||
<Form
|
||||
@data={{this.formData}}
|
||||
@onSubmit={{this.save}}
|
||||
{{didInsert this._focusName}}
|
||||
as |form transientData|
|
||||
>
|
||||
<form.Field
|
||||
@name="field_type"
|
||||
@title={{i18n "admin.user_fields.type"}}
|
||||
@format="large"
|
||||
@validation="required"
|
||||
as |field|
|
||||
>
|
||||
<field.Select as |select|>
|
||||
{{#each this.fieldTypes as |fieldType|}}
|
||||
<select.Option
|
||||
@value={{fieldType.id}}
|
||||
>{{fieldType.name}}</select.Option>
|
||||
{{/each}}
|
||||
</field.Select>
|
||||
</form.Field>
|
||||
|
||||
<form.Field
|
||||
@name="name"
|
||||
@title={{i18n "admin.user_fields.name"}}
|
||||
@format="large"
|
||||
@validation="required"
|
||||
as |field|
|
||||
>
|
||||
<field.Input class="user-field-name" maxlength="255" />
|
||||
</form.Field>
|
||||
|
||||
<form.Field
|
||||
@name="description"
|
||||
@title={{i18n "admin.user_fields.description"}}
|
||||
@format="large"
|
||||
@validation="required"
|
||||
as |field|
|
||||
>
|
||||
<field.Input class="user-field-desc" maxlength="1000" />
|
||||
</form.Field>
|
||||
|
||||
{{#if
|
||||
(or
|
||||
(eq transientData.field_type "dropdown")
|
||||
(eq transientData.field_type "multiselect")
|
||||
)
|
||||
}}
|
||||
<form.Field
|
||||
@name="options"
|
||||
@title={{i18n "admin.user_fields.options"}}
|
||||
@format="large"
|
||||
@validation="required"
|
||||
as |field|
|
||||
>
|
||||
<field.Custom>
|
||||
<ValueList
|
||||
@values={{transientData.options}}
|
||||
@inputType="array"
|
||||
@onChange={{field.set}}
|
||||
/>
|
||||
</field.Custom>
|
||||
</form.Field>
|
||||
{{/if}}
|
||||
|
||||
<form.Field
|
||||
@name="requirement"
|
||||
@title={{i18n "admin.user_fields.requirement.title"}}
|
||||
@validation="required"
|
||||
@onSet={{this.setRequirement}}
|
||||
@format="full"
|
||||
as |field|
|
||||
>
|
||||
<field.RadioGroup as |radioGroup|>
|
||||
<radioGroup.Radio @value="optional">
|
||||
{{i18n "admin.user_fields.requirement.optional.title"}}
|
||||
</radioGroup.Radio>
|
||||
<radioGroup.Radio @value="for_all_users" as |radio|>
|
||||
{{i18n "admin.user_fields.requirement.for_all_users.title"}}
|
||||
<radio.Description>{{i18n
|
||||
"admin.user_fields.requirement.for_all_users.description"
|
||||
}}</radio.Description>
|
||||
</radioGroup.Radio>
|
||||
<radioGroup.Radio @value="on_signup" as |radio|>
|
||||
{{i18n "admin.user_fields.requirement.on_signup.title"}}
|
||||
<radio.Description>{{i18n
|
||||
"admin.user_fields.requirement.on_signup.description"
|
||||
}}</radio.Description>
|
||||
</radioGroup.Radio>
|
||||
</field.RadioGroup>
|
||||
</form.Field>
|
||||
|
||||
<form.CheckboxGroup
|
||||
class="user-field-preferences"
|
||||
@title={{i18n "admin.user_fields.preferences"}}
|
||||
as |group|
|
||||
>
|
||||
<group.Field
|
||||
@name="editable"
|
||||
@showTitle={{false}}
|
||||
@title={{i18n "admin.user_fields.editable.title"}}
|
||||
as |field|
|
||||
>
|
||||
<field.Checkbox disabled={{this.editableDisabled}} />
|
||||
</group.Field>
|
||||
<group.Field
|
||||
@name="show_on_profile"
|
||||
@showTitle={{false}}
|
||||
@title={{i18n "admin.user_fields.show_on_profile.title"}}
|
||||
as |field|
|
||||
>
|
||||
<field.Checkbox />
|
||||
</group.Field>
|
||||
<group.Field
|
||||
@name="show_on_user_card"
|
||||
@showTitle={{false}}
|
||||
@title={{i18n "admin.user_fields.show_on_user_card.title"}}
|
||||
as |field|
|
||||
>
|
||||
<field.Checkbox />
|
||||
</group.Field>
|
||||
<group.Field
|
||||
@name="searchable"
|
||||
@showTitle={{false}}
|
||||
@title={{i18n "admin.user_fields.searchable.title"}}
|
||||
as |field|
|
||||
>
|
||||
<field.Checkbox />
|
||||
</group.Field>
|
||||
</form.CheckboxGroup>
|
||||
|
||||
<PluginOutlet
|
||||
@name="after-admin-user-fields"
|
||||
@outletArgs={{hash userField=@userField form=form}}
|
||||
/>
|
||||
|
||||
<form.Actions>
|
||||
<form.Submit
|
||||
class="save"
|
||||
@icon="check"
|
||||
@label="admin.user_fields.save"
|
||||
/>
|
||||
<form.Button
|
||||
@action={{this.cancel}}
|
||||
@label="admin.user_fields.cancel"
|
||||
/>
|
||||
</form.Actions>
|
||||
</Form>
|
||||
</template>
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<BackButton @route="adminUserFields.index" @label="admin.user_fields.back" />
|
||||
<div class="admin-config-area user-field">
|
||||
<div class="admin-config-area__primary-content">
|
||||
<div class="admin-config-area-card">
|
||||
<AdminUserFieldsForm @userField={{this.model}} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1 @@
|
|||
<AdminConfigAreas::UserFieldsList @userFields={{this.model}} />
|
|
@ -0,0 +1,8 @@
|
|||
<BackButton @route="adminUserFields.index" @label="admin.user_fields.back" />
|
||||
<div class="admin-config-area user-field">
|
||||
<div class="admin-config-area__primary-content">
|
||||
<div class="admin-config-area-card">
|
||||
<AdminUserFieldsForm @userField={{this.model}} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,29 +1,27 @@
|
|||
<div class="admin-config-page__main-area">
|
||||
<div class="admin-user_fields admin-config-page">
|
||||
<AdminPageHeader
|
||||
@titleLabel="admin.user_fields.title"
|
||||
@descriptionLabel="admin.user_fields.help"
|
||||
@hideTabs={{true}}
|
||||
@learnMoreUrl="https://meta.discourse.org/t/creating-and-configuring-custom-user-fields/113192"
|
||||
>
|
||||
<:breadcrumbs>
|
||||
<DBreadcrumbsItem
|
||||
@path="/admin/customize/user_fields"
|
||||
@label={{i18n "admin.user_fields.title"}}
|
||||
/>
|
||||
</:breadcrumbs>
|
||||
<:actions as |actions|>
|
||||
<actions.Primary
|
||||
@route="adminUserFields.new"
|
||||
@label="admin.user_fields.add"
|
||||
/>
|
||||
</:actions>
|
||||
</AdminPageHeader>
|
||||
|
||||
<div class="admin-config-page__main-area">
|
||||
<div class="user-fields">
|
||||
<h2>{{i18n "admin.user_fields.title"}}</h2>
|
||||
|
||||
<p class="desc">{{i18n "admin.user_fields.help"}}</p>
|
||||
|
||||
{{#if this.model}}
|
||||
{{#each this.sortedFields as |uf|}}
|
||||
<AdminUserFieldItem
|
||||
@userField={{uf}}
|
||||
@fieldTypes={{this.fieldTypes}}
|
||||
@firstField={{this.firstField}}
|
||||
@lastField={{this.lastField}}
|
||||
@destroyAction={{this.destroyField}}
|
||||
@moveUpAction={{this.moveUp}}
|
||||
@moveDownAction={{this.moveDown}}
|
||||
/>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
<DButton
|
||||
@disabled={{this.createDisabled}}
|
||||
@action={{this.createField}}
|
||||
@label="admin.user_fields.create"
|
||||
@icon="plus"
|
||||
class="btn-primary"
|
||||
/>
|
||||
{{outlet}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -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",
|
||||
];
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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 {
|
||||
|
|
10
app/assets/stylesheets/common/admin/user_fields.scss
Normal file
10
app/assets/stylesheets/common/admin/user_fields.scss
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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í"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user