Validation improve pattern for derived data
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>pull/21833/head
parent
8ec7e7c714
commit
ed0e188b4f
|
@ -40,11 +40,7 @@ interface IProps {
|
||||||
onValidate(result: IValidationResult);
|
onValidate(result: IValidationResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
class PassphraseField extends PureComponent<IProps> {
|
||||||
complexity: zxcvbn.ZXCVBNResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
class PassphraseField extends PureComponent<IProps, IState> {
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
label: _td("Password"),
|
label: _td("Password"),
|
||||||
labelEnterPassword: _td("Enter password"),
|
labelEnterPassword: _td("Enter password"),
|
||||||
|
@ -52,14 +48,16 @@ class PassphraseField extends PureComponent<IProps, IState> {
|
||||||
labelAllowedButUnsafe: _td("Password is allowed, but unsafe"),
|
labelAllowedButUnsafe: _td("Password is allowed, but unsafe"),
|
||||||
};
|
};
|
||||||
|
|
||||||
state = { complexity: null };
|
public readonly validate = withValidation<this, zxcvbn.ZXCVBNResult>({
|
||||||
|
description: function(complexity) {
|
||||||
public readonly validate = withValidation<this>({
|
|
||||||
description: function() {
|
|
||||||
const complexity = this.state.complexity;
|
|
||||||
const score = complexity ? complexity.score : 0;
|
const score = complexity ? complexity.score : 0;
|
||||||
return <progress className="mx_PassphraseField_progress" max={4} value={score} />;
|
return <progress className="mx_PassphraseField_progress" max={4} value={score} />;
|
||||||
},
|
},
|
||||||
|
deriveData: async ({ value }) => {
|
||||||
|
if (!value) return null;
|
||||||
|
const { scorePassword } = await import('../../../utils/PasswordScorer');
|
||||||
|
return scorePassword(value);
|
||||||
|
},
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
key: "required",
|
key: "required",
|
||||||
|
@ -68,28 +66,24 @@ class PassphraseField extends PureComponent<IProps, IState> {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "complexity",
|
key: "complexity",
|
||||||
test: async function({ value }) {
|
test: async function({ value }, complexity) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const { scorePassword } = await import('../../../utils/PasswordScorer');
|
|
||||||
const complexity = scorePassword(value);
|
|
||||||
this.setState({ complexity });
|
|
||||||
const safe = complexity.score >= this.props.minScore;
|
const safe = complexity.score >= this.props.minScore;
|
||||||
const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"];
|
const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"];
|
||||||
return allowUnsafe || safe;
|
return allowUnsafe || safe;
|
||||||
},
|
},
|
||||||
valid: function() {
|
valid: function(complexity) {
|
||||||
// Unsafe passwords that are valid are only possible through a
|
// Unsafe passwords that are valid are only possible through a
|
||||||
// configuration flag. We'll print some helper text to signal
|
// configuration flag. We'll print some helper text to signal
|
||||||
// to the user that their password is allowed, but unsafe.
|
// to the user that their password is allowed, but unsafe.
|
||||||
if (this.state.complexity.score >= this.props.minScore) {
|
if (complexity.score >= this.props.minScore) {
|
||||||
return _t(this.props.labelStrongPassword);
|
return _t(this.props.labelStrongPassword);
|
||||||
}
|
}
|
||||||
return _t(this.props.labelAllowedButUnsafe);
|
return _t(this.props.labelAllowedButUnsafe);
|
||||||
},
|
},
|
||||||
invalid: function() {
|
invalid: function(complexity) {
|
||||||
const complexity = this.state.complexity;
|
|
||||||
if (!complexity) {
|
if (!complexity) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,18 +21,19 @@ import classNames from "classnames";
|
||||||
|
|
||||||
type Data = Pick<IFieldState, "value" | "allowEmpty">;
|
type Data = Pick<IFieldState, "value" | "allowEmpty">;
|
||||||
|
|
||||||
interface IRule<T> {
|
interface IRule<T, D = void> {
|
||||||
key: string;
|
key: string;
|
||||||
final?: boolean;
|
final?: boolean;
|
||||||
skip?(this: T, data: Data): boolean;
|
skip?(this: T, data: Data, derivedData: D): boolean;
|
||||||
test(this: T, data: Data): boolean | Promise<boolean>;
|
test(this: T, data: Data, derivedData: D): boolean | Promise<boolean>;
|
||||||
valid?(this: T): string;
|
valid?(this: T, derivedData: D): string;
|
||||||
invalid?(this: T): string;
|
invalid?(this: T, derivedData: D): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IArgs<T> {
|
interface IArgs<T, D = void> {
|
||||||
rules: IRule<T>[];
|
rules: IRule<T, D>[];
|
||||||
description(this: T): React.ReactChild;
|
description(this: T, derivedData: D): React.ReactChild;
|
||||||
|
deriveData?(data: Data): Promise<D>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFieldState {
|
export interface IFieldState {
|
||||||
|
@ -53,6 +54,10 @@ export interface IValidationResult {
|
||||||
* @param {Function} description
|
* @param {Function} description
|
||||||
* Function that returns a string summary of the kind of value that will
|
* Function that returns a string summary of the kind of value that will
|
||||||
* meet the validation rules. Shown at the top of the validation feedback.
|
* meet the validation rules. Shown at the top of the validation feedback.
|
||||||
|
* @param {Function} deriveData
|
||||||
|
* Optional function that returns a Promise to an object of generic type D.
|
||||||
|
* The result of this Promise is passed to rule methods `skip`, `test`, `valid`, and `invalid`.
|
||||||
|
* Useful for doing calculations per-value update once rather than in each of the above rule methods.
|
||||||
* @param {Object} rules
|
* @param {Object} rules
|
||||||
* An array of rules describing how to check to input value. Each rule in an object
|
* An array of rules describing how to check to input value. Each rule in an object
|
||||||
* and may have the following properties:
|
* and may have the following properties:
|
||||||
|
@ -66,7 +71,7 @@ export interface IValidationResult {
|
||||||
* A validation function that takes in the current input value and returns
|
* A validation function that takes in the current input value and returns
|
||||||
* the overall validity and a feedback UI that can be rendered for more detail.
|
* the overall validity and a feedback UI that can be rendered for more detail.
|
||||||
*/
|
*/
|
||||||
export default function withValidation<T = undefined>({ description, rules }: IArgs<T>) {
|
export default function withValidation<T = undefined, D = void>({ description, deriveData, rules }: IArgs<T, D>) {
|
||||||
return async function onValidate({ value, focused, allowEmpty = true }: IFieldState): Promise<IValidationResult> {
|
return async function onValidate({ value, focused, allowEmpty = true }: IFieldState): Promise<IValidationResult> {
|
||||||
if (!value && allowEmpty) {
|
if (!value && allowEmpty) {
|
||||||
return {
|
return {
|
||||||
|
@ -75,6 +80,9 @@ export default function withValidation<T = undefined>({ description, rules }: IA
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = { value, allowEmpty };
|
||||||
|
const derivedData = deriveData ? await deriveData(data) : undefined;
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
let valid = true;
|
let valid = true;
|
||||||
if (rules && rules.length) {
|
if (rules && rules.length) {
|
||||||
|
@ -87,20 +95,18 @@ export default function withValidation<T = undefined>({ description, rules }: IA
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = { value, allowEmpty };
|
if (rule.skip && rule.skip.call(this, data, derivedData)) {
|
||||||
|
|
||||||
if (rule.skip && rule.skip.call(this, data)) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We're setting `this` to whichever component holds the validation
|
// We're setting `this` to whichever component holds the validation
|
||||||
// function. That allows rules to access the state of the component.
|
// function. That allows rules to access the state of the component.
|
||||||
const ruleValid = await rule.test.call(this, data);
|
const ruleValid = await rule.test.call(this, data, derivedData);
|
||||||
valid = valid && ruleValid;
|
valid = valid && ruleValid;
|
||||||
if (ruleValid && rule.valid) {
|
if (ruleValid && rule.valid) {
|
||||||
// If the rule's result is valid and has text to show for
|
// If the rule's result is valid and has text to show for
|
||||||
// the valid state, show it.
|
// the valid state, show it.
|
||||||
const text = rule.valid.call(this);
|
const text = rule.valid.call(this, derivedData);
|
||||||
if (!text) {
|
if (!text) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -112,7 +118,7 @@ export default function withValidation<T = undefined>({ description, rules }: IA
|
||||||
} else if (!ruleValid && rule.invalid) {
|
} else if (!ruleValid && rule.invalid) {
|
||||||
// If the rule's result is invalid and has text to show for
|
// If the rule's result is invalid and has text to show for
|
||||||
// the invalid state, show it.
|
// the invalid state, show it.
|
||||||
const text = rule.invalid.call(this);
|
const text = rule.invalid.call(this, derivedData);
|
||||||
if (!text) {
|
if (!text) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -153,7 +159,7 @@ export default function withValidation<T = undefined>({ description, rules }: IA
|
||||||
if (description) {
|
if (description) {
|
||||||
// We're setting `this` to whichever component holds the validation
|
// We're setting `this` to whichever component holds the validation
|
||||||
// function. That allows rules to access the state of the component.
|
// function. That allows rules to access the state of the component.
|
||||||
const content = description.call(this);
|
const content = description.call(this, derivedData);
|
||||||
summary = <div className="mx_Validation_description">{content}</div>;
|
summary = <div className="mx_Validation_description">{content}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue