DEV: Add validation message to float fields in theme object editor (#26285)

Why this change?

This is a continuation of a30d73f255879f3e4cc0edbb9de358c03f3aed94

In our schema, we support the `min` and `max` validation
rules like so:

```
some_objects_setting
  type: objects
  schema:
    name: some_object
    properties:
      id:
        type: float
        validations:
          min: 5
          max: 10
```

While the validations used to validate the objects on the server side,
we should also add client side validation for better UX.

What does this change do?

Since the integer and float input fields share very very similar logic
in the component. This commit pulls the common logic into
`admin/components/schema-theme-setting/number-field.gjs` which
`admin/components/schema-theme-setting/types/integer.gjs` and `admin/components/schema-theme-setting/types/float.gjs`
will inherit from.
This commit is contained in:
Alan Guo Xiang Tan 2024-03-21 15:33:38 +08:00 committed by GitHub
parent a30d73f255
commit 2d867aa8e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 150 additions and 109 deletions

View File

@ -0,0 +1,90 @@
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 I18n from "discourse-i18n";
import FieldInputDescription from "admin/components/schema-theme-setting/field-input-description";
export default class SchemaThemeSettingNumberField extends Component {
@tracked touched = false;
@tracked value = this.args.value;
min = this.args.spec.validations?.min;
max = this.args.spec.validations?.max;
required = this.args.spec.required;
@action
onInput(event) {
this.touched = true;
let inputValue = event.currentTarget.value;
if (isNaN(inputValue)) {
this.value = null;
} else {
this.value = this.parseValue(inputValue);
}
this.args.onChange(this.value);
}
/**
* @param {string} value - The value of the input field to parse into a number
* @returns {number}
*/
parseFunc() {
throw "Not implemented";
}
get validationErrorMessage() {
if (!this.touched) {
return;
}
if (!this.value) {
if (this.required) {
return I18n.t("admin.customize.theme.schema.fields.required");
} else {
return;
}
}
if (this.min && this.value < this.min) {
return I18n.t("admin.customize.theme.schema.fields.number.too_small", {
count: this.min,
});
}
if (this.max && this.value > this.max) {
return I18n.t("admin.customize.theme.schema.fields.number.too_large", {
count: this.max,
});
}
}
<template>
<Input
@value={{this.value}}
{{on "input" this.onInput}}
@type="number"
inputmode={{this.inputmode}}
pattern={{this.pattern}}
step={{this.step}}
max={{this.max}}
min={{this.min}}
required={{this.required}}
/>
<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}}
</div>
</template>
}

View File

@ -1,23 +1,9 @@
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";
import SchemaThemeSettingNumberField from "admin/components/schema-theme-setting/number-field";
export default class SchemaThemeSettingTypeFloat extends Component {
@action
onInput(event) {
this.args.onChange(parseFloat(event.currentTarget.value));
export default class SchemaThemeSettingTypeFloat extends SchemaThemeSettingNumberField {
step = 0.1;
parseValue(value) {
return parseFloat(value);
}
<template>
<Input
@value={{@value}}
{{on "input" this.onInput}}
@type="number"
step="0.1"
/>
<FieldInputDescription @description={{@description}} />
</template>
}

View File

@ -1,80 +1,10 @@
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 I18n from "discourse-i18n";
import FieldInputDescription from "admin/components/schema-theme-setting/field-input-description";
import SchemaThemeSettingNumberField from "admin/components/schema-theme-setting/number-field";
export default class SchemaThemeSettingTypeInteger extends Component {
@tracked touched = false;
@tracked value = this.args.value;
min = this.args.spec.validations?.min;
max = this.args.spec.validations?.max;
required = this.args.spec.required;
export default class SchemaThemeSettingTypeInteger extends SchemaThemeSettingNumberField {
inputMode = "numeric";
pattern = "[0-9]*";
@action
onInput(event) {
this.touched = true;
let newValue = parseInt(event.currentTarget.value, 10);
if (isNaN(newValue)) {
newValue = null;
}
this.value = newValue;
this.args.onChange(newValue);
parseValue(value) {
return parseInt(value, 10);
}
get validationErrorMessage() {
if (!this.touched) {
return;
}
if (!this.value) {
if (this.required) {
return I18n.t("admin.customize.theme.schema.fields.required");
} else {
return;
}
}
if (this.min && this.value < this.min) {
return I18n.t("admin.customize.theme.schema.fields.number.too_small", {
count: this.min,
});
}
if (this.max && this.value > this.max) {
return I18n.t("admin.customize.theme.schema.fields.number.too_large", {
count: this.max,
});
}
}
<template>
<Input
@value={{this.value}}
{{on "input" this.onInput}}
@type="number"
inputmode="numeric"
pattern="[0-9]*"
max={{this.max}}
min={{this.min}}
required={{this.required}}
/>
<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}}
</div>
</template>
}

View File

@ -546,34 +546,69 @@ module(
});
test("input fields of type float", async function (assert) {
const setting = schemaAndData(3);
const setting = ThemeSettings.create({
setting: "objects_setting",
objects_schema: {
name: "something",
identifier: "id",
properties: {
id: {
type: "float",
required: true,
validations: {
max: 10.5,
min: 5.5,
},
},
},
},
value: [
{
id: 6.5,
},
],
});
await render(<template>
<AdminSchemaThemeSettingEditor @themeId="1" @setting={{setting}} />
</template>);
const inputFields = new InputFieldsFromDOM();
assert.dom(inputFields.fields.id.labelElement).hasText("id*");
assert.dom(inputFields.fields.id.inputElement).hasValue("6.5");
assert
.dom(inputFields.fields.float_field.labelElement)
.hasText("float_field");
assert.dom(inputFields.fields.float_field.inputElement).hasValue("");
.dom(inputFields.fields.id.inputElement)
.hasAttribute("type", "number");
await fillIn(inputFields.fields.float_field.inputElement, "6934.24");
const tree = new TreeFromDOM();
await click(tree.nodes[1].element);
await fillIn(inputFields.fields.id.inputElement, "100.0");
inputFields.refresh();
assert.dom(inputFields.fields.float_field.inputElement).hasValue("");
assert.dom(inputFields.fields.id.errorElement).hasText(
I18n.t("admin.customize.theme.schema.fields.number.too_large", {
count: 10.5,
})
);
await fillIn(inputFields.fields.id.inputElement, "0.2");
inputFields.refresh();
assert.dom(inputFields.fields.id.errorElement).hasText(
I18n.t("admin.customize.theme.schema.fields.number.too_small", {
count: 5.5,
})
);
await fillIn(inputFields.fields.id.inputElement, "");
tree.refresh();
await click(tree.nodes[0].element);
inputFields.refresh();
assert
.dom(inputFields.fields.float_field.inputElement)
.hasValue("6934.24");
.dom(inputFields.fields.id.errorElement)
.hasText(I18n.t("admin.customize.theme.schema.fields.required"));
});
test("input fields of type boolean", async function (assert) {