Model Validation Warnings (#19913)
* updates model validations to support warnings and adds alert to form field * adds changelog entry * updates model validations tests
This commit is contained in:
parent
3f0620ce2c
commit
efe31ae32e
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
ui: Adds whitespace warning to secrets engine and auth method path inputs
|
||||
```
|
|
@ -63,6 +63,17 @@ export default class MountBackendForm extends Component {
|
|||
return isValid;
|
||||
}
|
||||
|
||||
checkModelWarnings() {
|
||||
// check for warnings on change
|
||||
// since we only show errors on submit we need to clear those out and only send warning state
|
||||
const { state } = this.args.mountModel.validate();
|
||||
for (const key in state) {
|
||||
state[key].errors = [];
|
||||
}
|
||||
this.modelValidations = state;
|
||||
this.invalidFormAlert = null;
|
||||
}
|
||||
|
||||
async showWarningsForKvv2() {
|
||||
try {
|
||||
const capabilities = await this.store.findRecord('capabilities', `${this.args.mountModel.path}/config`);
|
||||
|
@ -141,6 +152,7 @@ export default class MountBackendForm extends Component {
|
|||
@action
|
||||
onKeyUp(name, value) {
|
||||
this.args.mountModel[name] = value;
|
||||
this.checkModelWarnings();
|
||||
}
|
||||
|
||||
@action
|
||||
|
|
|
@ -11,14 +11,20 @@ import { get } from '@ember/object';
|
|||
* used to validate properties on a class
|
||||
*
|
||||
* decorator expects validations object with the following shape:
|
||||
* { [propertyKeyName]: [{ type, options, message, validator }] }
|
||||
* { [propertyKeyName]: [{ type, options, message, level, validator }] }
|
||||
* each key in the validations object should refer to the property on the class to apply the validation to
|
||||
* type refers to the type of validation to apply -- must be exported from validators util for lookup
|
||||
* options is an optional object for given validator -- min, max, nullable etc. -- see validators in util
|
||||
* message is added to the errors array and returned from the validate method if validation fails
|
||||
* validator may be used in place of type to provide a function that gets executed in the validate method
|
||||
* validator is useful when specific validations are needed (dependent on other class properties etc.)
|
||||
* validator must be passed as function that takes the class context (this) as the only argument and returns true or false
|
||||
*
|
||||
* type - string referring to the type of validation to apply -- must be exported from validators util for lookup
|
||||
*
|
||||
* options - an optional object for given validator -- min, max, nullable etc. -- see validators in util
|
||||
*
|
||||
* message - string added to the errors array and returned from the validate method if validation fails
|
||||
*
|
||||
* level - optional string that defaults to 'error'. Currently the only other accepted value is 'warn'
|
||||
*
|
||||
* validator - function that may be used in place of type that is invoked in the validate method
|
||||
* useful when specific validations are needed (dependent on other class properties etc.)
|
||||
* must be passed as function that takes the class context (this) as the only argument and returns true or false
|
||||
* each property supports multiple validations provided as an array -- for example, presence and length for string
|
||||
*
|
||||
* validations must be invoked using the validate method which is added directly to the decorated class
|
||||
|
@ -59,6 +65,7 @@ import { get } from '@ember/object';
|
|||
* -> state.foo.errors = ['foo is required if bar includes test.'];
|
||||
*
|
||||
* *** example adding class in hbs file
|
||||
*
|
||||
* all form-validations need to have a red border around them. Add this by adding a conditional class 'has-error-border'
|
||||
* class="input field {{if this.errors.name.errors 'has-error-border'}}"
|
||||
*/
|
||||
|
@ -91,10 +98,10 @@ export function withModelValidations(validations) {
|
|||
continue;
|
||||
}
|
||||
|
||||
state[key] = { errors: [] };
|
||||
state[key] = { errors: [], warnings: [] };
|
||||
|
||||
for (const rule of rules) {
|
||||
const { type, options, message, validator: customValidator } = rule;
|
||||
const { type, options, level, message, validator: customValidator } = rule;
|
||||
// check for custom validator or lookup in validators util by type
|
||||
const useCustomValidator = typeof customValidator === 'function';
|
||||
const validator = useCustomValidator ? customValidator : validators[type];
|
||||
|
@ -115,12 +122,16 @@ export function withModelValidations(validations) {
|
|||
if (!passedValidation) {
|
||||
// consider setting a prop like validationErrors directly on the model
|
||||
// for now return an errors object
|
||||
if (level === 'warn') {
|
||||
state[key].warnings.push(message);
|
||||
} else {
|
||||
state[key].errors.push(message);
|
||||
if (isValid) {
|
||||
isValid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
errorCount += state[key].errors.length;
|
||||
state[key].isValid = !state[key].errors.length;
|
||||
}
|
||||
|
|
|
@ -13,7 +13,15 @@ import attachCapabilities from 'vault/lib/attach-capabilities';
|
|||
import { withModelValidations } from 'vault/decorators/model-validations';
|
||||
|
||||
const validations = {
|
||||
path: [{ type: 'presence', message: "Path can't be blank." }],
|
||||
path: [
|
||||
{ type: 'presence', message: "Path can't be blank." },
|
||||
{
|
||||
type: 'containsWhiteSpace',
|
||||
message:
|
||||
"Path contains whitespace. If this is desired, you'll need to encode it with %20 in API requests.",
|
||||
level: 'warn',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// unsure if ember-api-actions will work on native JS class model
|
||||
|
|
|
@ -14,7 +14,15 @@ import { withExpandedAttributes } from 'vault/decorators/model-expanded-attribut
|
|||
const LIST_EXCLUDED_BACKENDS = ['system', 'identity'];
|
||||
|
||||
const validations = {
|
||||
path: [{ type: 'presence', message: "Path can't be blank." }],
|
||||
path: [
|
||||
{ type: 'presence', message: "Path can't be blank." },
|
||||
{
|
||||
type: 'containsWhiteSpace',
|
||||
message:
|
||||
"Path contains whitespace. If this is desired, you'll need to encode it with %20 in API requests.",
|
||||
level: 'warn',
|
||||
},
|
||||
],
|
||||
maxVersions: [
|
||||
{ type: 'number', message: 'Maximum versions must be a number.' },
|
||||
{ type: 'length', options: { min: 1, max: 16 }, message: 'You cannot go over 16 characters.' },
|
||||
|
|
|
@ -318,6 +318,14 @@
|
|||
{{#if this.validationError}}
|
||||
<AlertInline @type="danger" @message={{this.validationError}} @paddingTop={{true}} />
|
||||
{{/if}}
|
||||
{{#if this.validationWarning}}
|
||||
<AlertInline
|
||||
@type="warning"
|
||||
@message={{this.validationWarning}}
|
||||
@paddingTop={{true}}
|
||||
data-test-validation-warning
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{else if (eq @attr.type "boolean")}}
|
||||
|
|
|
@ -117,6 +117,11 @@ export default class FormFieldComponent extends Component {
|
|||
const state = validations[this.valuePath];
|
||||
return state && !state.isValid ? state.errors.join(' ') : null;
|
||||
}
|
||||
get validationWarning() {
|
||||
const validations = this.args.modelValidations || {};
|
||||
const state = validations[this.valuePath];
|
||||
return state?.warnings?.length ? state.warnings.join(' ') : null;
|
||||
}
|
||||
|
||||
onChange() {
|
||||
if (this.args.onChange) {
|
||||
|
|
|
@ -210,4 +210,20 @@ module('Integration | Component | form field', function (hooks) {
|
|||
assert.dom('[data-test-toggle-input="Foo"]').isChecked('Toggle is initially checked when given value');
|
||||
assert.dom('[data-test-ttl-value="Foo"]').hasValue('1', 'Ttl input displays with correct value');
|
||||
});
|
||||
|
||||
test('it should show validation warning', async function (assert) {
|
||||
const model = this.owner.lookup('service:store').createRecord('auth-method');
|
||||
model.path = 'foo bar';
|
||||
this.validations = model.validate().state;
|
||||
this.setProperties({
|
||||
model,
|
||||
attr: createAttr('path', 'string'),
|
||||
onChange: () => {},
|
||||
});
|
||||
|
||||
await render(
|
||||
hbs`<FormField @attr={{this.attr}} @model={{this.model}} @modelValidations={{this.validations}} @onChange={{this.onChange}} />`
|
||||
);
|
||||
assert.dom('[data-test-validation-warning]').exists('Validation warning renders');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -74,7 +74,7 @@ module('Unit | Decorators | ModelValidations', function (hooks) {
|
|||
assert.false(v1.isValid, 'isValid state is correct when errors exist');
|
||||
assert.deepEqual(
|
||||
v1.state,
|
||||
{ foo: { isValid: false, errors: [message] } },
|
||||
{ foo: { isValid: false, errors: [message], warnings: [] } },
|
||||
'Correct state returned when property is invalid'
|
||||
);
|
||||
|
||||
|
@ -83,7 +83,7 @@ module('Unit | Decorators | ModelValidations', function (hooks) {
|
|||
assert.true(v2.isValid, 'isValid state is correct when no errors exist');
|
||||
assert.deepEqual(
|
||||
v2.state,
|
||||
{ foo: { isValid: true, errors: [] } },
|
||||
{ foo: { isValid: true, errors: [], warnings: [] } },
|
||||
'Correct state returned when property is valid'
|
||||
);
|
||||
});
|
||||
|
@ -115,4 +115,22 @@ module('Unit | Decorators | ModelValidations', function (hooks) {
|
|||
const v3 = fooClass.validate();
|
||||
assert.strictEqual(v3.invalidFormMessage, null, 'invalidFormMessage is null when form is valid');
|
||||
});
|
||||
|
||||
test('it should validate warnings', function (assert) {
|
||||
const message = 'Value contains whitespace.';
|
||||
const validations = {
|
||||
foo: [
|
||||
{
|
||||
type: 'containsWhiteSpace',
|
||||
message,
|
||||
level: 'warn',
|
||||
},
|
||||
],
|
||||
};
|
||||
const fooClass = createClass(validations);
|
||||
fooClass.foo = 'foo bar';
|
||||
const { state, isValid } = fooClass.validate();
|
||||
assert.true(isValid, 'Model is considered valid when there are only warnings');
|
||||
assert.strictEqual(state.foo.warnings.join(' '), message, 'Warnings are returned');
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue