mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
DEV: Add validation message to string fields in theme object editor (#26257)
Why this change? In our schema, we support the `min_length` and `max_length` validation rules like so: ``` some_objects_setting type: objects schema: name: some_object properties: title: type: string validations: min_length: 1 max_length: 10 ``` While the validations used to validate the objects on the server side, we should also add client side validation for better UX.
This commit is contained in:
parent
70f7c0ee6f
commit
8de869630f
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="schema-field__input-description">
|
||||
{{@description}}
|
||||
</div>
|
||||
</template>
|
@ -36,30 +36,28 @@ export default class SchemaThemeSettingField extends Component {
|
||||
|
||||
@cached
|
||||
get description() {
|
||||
return this.args.description.trim().replace(/\n/g, "<br>");
|
||||
}
|
||||
if (!this.args.description) {
|
||||
return;
|
||||
}
|
||||
|
||||
get hasDescription() {
|
||||
return this.args.description?.length > 0;
|
||||
return htmlSafe(this.args.description.trim().replace(/\n/g, "<br>"));
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="schema-field" data-name={{@name}}>
|
||||
<label class="schema-field__label">{{@name}}</label>
|
||||
<label class="schema-field__label">{{@name}}{{if
|
||||
@spec.required
|
||||
"*"
|
||||
}}</label>
|
||||
|
||||
<div class="schema-field__input">
|
||||
<this.component
|
||||
@value={{@value}}
|
||||
@spec={{@spec}}
|
||||
@onChange={{@onValueChange}}
|
||||
@description={{this.description}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{{#if this.hasDescription}}
|
||||
<div class="schema-field__description">
|
||||
{{htmlSafe this.description}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import Component from "@glimmer/component";
|
||||
import { Input } from "@ember/component";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import FieldInputDescription from "admin/components/schema-theme-setting/field-input-description";
|
||||
|
||||
export default class SchemaThemeSettingTypeBoolean extends Component {
|
||||
@action
|
||||
@ -11,5 +12,6 @@ export default class SchemaThemeSettingTypeBoolean extends Component {
|
||||
|
||||
<template>
|
||||
<Input @checked={{@value}} {{on "input" this.onInput}} @type="checkbox" />
|
||||
<FieldInputDescription @description={{@description}} />
|
||||
</template>
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { hash } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import FieldInputDescription from "admin/components/schema-theme-setting/field-input-description";
|
||||
import CategoryChooser from "select-kit/components/category-chooser";
|
||||
|
||||
export default class SchemaThemeSettingTypeCategory extends Component {
|
||||
@ -19,5 +20,6 @@ export default class SchemaThemeSettingTypeCategory extends Component {
|
||||
@onChange={{this.onInput}}
|
||||
@options={{hash allowUncategorized=false}}
|
||||
/>
|
||||
<FieldInputDescription @description={{@description}} />
|
||||
</template>
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import FieldInputDescription from "admin/components/schema-theme-setting/field-input-description";
|
||||
import ComboBox from "select-kit/components/combo-box";
|
||||
|
||||
export default class SchemaThemeSettingTypeEnum extends Component {
|
||||
@ -27,5 +28,6 @@ export default class SchemaThemeSettingTypeEnum extends Component {
|
||||
@value={{this.value}}
|
||||
@onChange={{this.onInput}}
|
||||
/>
|
||||
<FieldInputDescription @description={{@description}} />
|
||||
</template>
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import Component from "@glimmer/component";
|
||||
import { Input } from "@ember/component";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import FieldInputDescription from "admin/components/schema-theme-setting/field-input-description";
|
||||
|
||||
export default class SchemaThemeSettingTypeFloat extends Component {
|
||||
@action
|
||||
@ -16,5 +17,7 @@ export default class SchemaThemeSettingTypeFloat extends Component {
|
||||
@type="number"
|
||||
step="0.1"
|
||||
/>
|
||||
|
||||
<FieldInputDescription @description={{@description}} />
|
||||
</template>
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { tracked } from "@glimmer/tracking";
|
||||
import { hash } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import Group from "discourse/models/group";
|
||||
import FieldInputDescription from "admin/components/schema-theme-setting/field-input-description";
|
||||
import GroupChooser from "select-kit/components/group-chooser";
|
||||
|
||||
export default class SchemaThemeSettingTypeGroup extends Component {
|
||||
@ -24,5 +25,7 @@ export default class SchemaThemeSettingTypeGroup extends Component {
|
||||
@onChange={{this.onInput}}
|
||||
@options={{hash maximum=1}}
|
||||
/>
|
||||
|
||||
<FieldInputDescription @description={{@description}} />
|
||||
</template>
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import Component from "@glimmer/component";
|
||||
import { Input } from "@ember/component";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import FieldInputDescription from "admin/components/schema-theme-setting/field-input-description";
|
||||
|
||||
export default class SchemaThemeSettingTypeInteger extends Component {
|
||||
@action
|
||||
@ -11,5 +12,7 @@ export default class SchemaThemeSettingTypeInteger extends Component {
|
||||
|
||||
<template>
|
||||
<Input @value={{@value}} {{on "input" this.onInput}} @type="number" />
|
||||
|
||||
<FieldInputDescription @description={{@description}} />
|
||||
</template>
|
||||
}
|
||||
|
@ -1,15 +1,81 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { Input } from "@ember/component";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import { and, not } from "truth-helpers";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import I18n from "discourse-i18n";
|
||||
import FieldInputDescription from "admin/components/schema-theme-setting/field-input-description";
|
||||
|
||||
export default class SchemaThemeSettingTypeString extends Component {
|
||||
@tracked touched = false;
|
||||
@tracked value = this.args.value || "";
|
||||
minLength = this.args.spec.validations?.min_length;
|
||||
maxLength = this.args.spec.validations?.max_length;
|
||||
required = this.args.spec.required;
|
||||
|
||||
@action
|
||||
onInput(event) {
|
||||
this.args.onChange(event.currentTarget.value);
|
||||
this.touched = true;
|
||||
const newValue = event.currentTarget.value;
|
||||
this.args.onChange(newValue);
|
||||
this.value = newValue;
|
||||
}
|
||||
|
||||
get validationErrorMessage() {
|
||||
if (!this.touched) {
|
||||
return;
|
||||
}
|
||||
|
||||
const valueLength = this.value.length;
|
||||
|
||||
if (valueLength === 0) {
|
||||
if (this.required) {
|
||||
return I18n.t("admin.customize.theme.schema.fields.required");
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.minLength && valueLength < this.minLength) {
|
||||
return I18n.t("admin.customize.theme.schema.fields.string.too_short", {
|
||||
count: this.minLength,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<Input @value={{@value}} {{on "input" this.onInput}} />
|
||||
<Input
|
||||
class="--string"
|
||||
@value={{this.value}}
|
||||
{{on "input" this.onInput}}
|
||||
required={{this.required}}
|
||||
minLength={{this.minLength}}
|
||||
maxLength={{this.maxLength}}
|
||||
/>
|
||||
|
||||
<div class="schema-field__input-supporting-text">
|
||||
{{#if (and @description (not this.validationErrorMessage))}}
|
||||
<FieldInputDescription @description={{@description}} />
|
||||
{{/if}}
|
||||
|
||||
{{#if this.validationErrorMessage}}
|
||||
<div class="schema-field__input-error">
|
||||
{{this.validationErrorMessage}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.maxLength}}
|
||||
<div
|
||||
class={{concatClass
|
||||
"schema-field__input-count"
|
||||
(if this.validationErrorMessage " --error")
|
||||
}}
|
||||
>
|
||||
{{this.value.length}}/{{this.maxLength}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { hash } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import FieldInputDescription from "admin/components/schema-theme-setting/field-input-description";
|
||||
import TagChooser from "select-kit/components/tag-chooser";
|
||||
|
||||
export default class SchemaThemeSettingTypeTag extends Component {
|
||||
@ -19,5 +20,7 @@ export default class SchemaThemeSettingTypeTag extends Component {
|
||||
@onChange={{this.onInput}}
|
||||
@options={{hash allowAny=false}}
|
||||
/>
|
||||
|
||||
<FieldInputDescription @description={{@description}} />
|
||||
</template>
|
||||
}
|
||||
|
@ -421,6 +421,56 @@ module(
|
||||
assert.dom(inputFields.fields.icon.inputElement).hasValue("phone");
|
||||
});
|
||||
|
||||
test("input fields of type string", async function (assert) {
|
||||
const setting = ThemeSettings.create({
|
||||
setting: "objects_setting",
|
||||
objects_schema: {
|
||||
name: "something",
|
||||
identifier: "id",
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
required: true,
|
||||
validations: {
|
||||
max_length: 5,
|
||||
min_length: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
value: [
|
||||
{
|
||||
id: "bu1",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await render(<template>
|
||||
<AdminSchemaThemeSettingEditor @themeId="1" @setting={{setting}} />
|
||||
</template>);
|
||||
|
||||
const fieldSelector =
|
||||
".schema-field[data-name='id'] .schema-field__input";
|
||||
|
||||
assert.dom(`${fieldSelector} .schema-field__input-count`).hasText("3/5");
|
||||
|
||||
await fillIn(`${fieldSelector} input`, "1");
|
||||
|
||||
assert.dom(`${fieldSelector} .schema-field__input-error`).hasText(
|
||||
I18n.t("admin.customize.theme.schema.fields.string.too_short", {
|
||||
count: 2,
|
||||
})
|
||||
);
|
||||
|
||||
await fillIn(`${fieldSelector} input`, "");
|
||||
|
||||
assert.dom(`${fieldSelector} .schema-field__input-count`).hasText("0/5");
|
||||
|
||||
assert
|
||||
.dom(`${fieldSelector} .schema-field__input-error`)
|
||||
.hasText(I18n.t("admin.customize.theme.schema.fields.required"));
|
||||
});
|
||||
|
||||
test("input fields of type integer", async function (assert) {
|
||||
const setting = schemaAndData(3);
|
||||
|
||||
|
@ -1,17 +1,41 @@
|
||||
.schema-field {
|
||||
margin-bottom: 1em;
|
||||
width: 50%;
|
||||
min-width: 200px;
|
||||
|
||||
.schema-field__input {
|
||||
input {
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
margin-bottom: 0.3em;
|
||||
}
|
||||
.select-kit {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.schema-field__description {
|
||||
font-size: var(--font-down-1);
|
||||
color: var(--primary-medium);
|
||||
.schema-field__input-description {
|
||||
font-size: var(--font-down-1);
|
||||
color: var(--primary-medium);
|
||||
}
|
||||
}
|
||||
.schema-field__input-supporting-text {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 0.2em;
|
||||
|
||||
.schema-field__input-count {
|
||||
margin-left: auto;
|
||||
font-size: var(--font-down-1);
|
||||
|
||||
&.--error {
|
||||
color: var(--danger);
|
||||
}
|
||||
}
|
||||
|
||||
.schema-field__input-error {
|
||||
font-size: var(--font-down-1);
|
||||
color: var(--danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5647,6 +5647,11 @@ en:
|
||||
schema:
|
||||
title: "Edit %{name} setting"
|
||||
back_button: "Back to %{name}"
|
||||
fields:
|
||||
required: "*required"
|
||||
string:
|
||||
too_short: "must be at least %{count} characters"
|
||||
|
||||
colors:
|
||||
select_base:
|
||||
title: "Select base color palette"
|
||||
|
@ -42,7 +42,7 @@ module PageObjects
|
||||
end
|
||||
|
||||
def input_field_description(field_name)
|
||||
page.find(".schema-field[data-name=\"#{field_name}\"] .schema-field__description")
|
||||
page.find(".schema-field[data-name=\"#{field_name}\"] .schema-field__input-description")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user