Much better support for frontend field validation
This commit is contained in:
parent
92fb3b7529
commit
3aad681538
18
package.json
18
package.json
@ -55,25 +55,25 @@
|
|||||||
],
|
],
|
||||||
"homepage": "https://github.com/garronej/keycloakify",
|
"homepage": "https://github.com/garronej/keycloakify",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
"@emotion/react": "^11.4.1",
|
||||||
|
"powerhooks": "^0.11.0",
|
||||||
"react": "^16.8.0 || ^17.0.0",
|
"react": "^16.8.0 || ^17.0.0",
|
||||||
"tss-react": "^1.1.0",
|
"tss-react": "^1.1.0"
|
||||||
"powerhooks": "^0.9.6",
|
|
||||||
"@emotion/react": "^11.4.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"tss-react": "^1.1.0",
|
|
||||||
"@emotion/react": "^11.4.1",
|
"@emotion/react": "^11.4.1",
|
||||||
"powerhooks": "^0.9.6",
|
|
||||||
"@types/node": "^10.0.0",
|
"@types/node": "^10.0.0",
|
||||||
"@types/react": "^17.0.0",
|
"@types/react": "^17.0.0",
|
||||||
"copyfiles": "^2.4.1",
|
"copyfiles": "^2.4.1",
|
||||||
|
"husky": "^4.3.8",
|
||||||
|
"lint-staged": "^11.0.0",
|
||||||
|
"powerhooks": "^0.11.0",
|
||||||
|
"prettier": "^2.3.0",
|
||||||
"properties-parser": "^0.3.1",
|
"properties-parser": "^0.3.1",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"typescript": "^4.2.3",
|
"tss-react": "^1.1.0",
|
||||||
"husky": "^4.3.8",
|
"typescript": "^4.2.3"
|
||||||
"lint-staged": "^11.0.0",
|
|
||||||
"prettier": "^2.3.0"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cheerio": "^1.0.0-rc.5",
|
"cheerio": "^1.0.0-rc.5",
|
||||||
|
@ -3,7 +3,7 @@ import { Template } from "./Template";
|
|||||||
import type { KcProps } from "./KcProps";
|
import type { KcProps } from "./KcProps";
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||||
import { useKcMessage } from "../i18n/useKcMessage";
|
import { useKcMessage } from "../i18n/useKcMessage";
|
||||||
import { appendHead } from "../tools/appendHead";
|
import { headInsert } from "../tools/headInsert";
|
||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import { useCssAndCx } from "tss-react";
|
import { useCssAndCx } from "tss-react";
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ export const LoginOtp = memo(({ kcContext, ...props }: { kcContext: KcContextBas
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isCleanedUp = false;
|
let isCleanedUp = false;
|
||||||
|
|
||||||
appendHead({
|
headInsert({
|
||||||
"type": "javascript",
|
"type": "javascript",
|
||||||
"src": pathJoin(kcContext.url.resourcesCommonPath, "node_modules/jquery/dist/jquery.min.js"),
|
"src": pathJoin(kcContext.url.resourcesCommonPath, "node_modules/jquery/dist/jquery.min.js"),
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
|
@ -1,17 +1,29 @@
|
|||||||
import { memo, Fragment } from "react";
|
import { useMemo, memo, useEffect, useState, Fragment } from "react";
|
||||||
import { Template } from "./Template";
|
import { Template } from "./Template";
|
||||||
import type { KcProps } from "./KcProps";
|
import type { KcProps } from "./KcProps";
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
import type { KcContextBase, Attribute } from "../getKcContext/KcContextBase";
|
||||||
import { useKcMessage } from "../i18n/useKcMessage";
|
import { useKcMessage } from "../i18n/useKcMessage";
|
||||||
import { useCssAndCx } from "tss-react";
|
import { useCssAndCx } from "tss-react";
|
||||||
import type { ReactComponent } from "../tools/ReactComponent";
|
import type { ReactComponent } from "../tools/ReactComponent";
|
||||||
|
import { useCallbackFactory } from "powerhooks/useCallbackFactory";
|
||||||
|
import { useFormValidationSlice } from "../useFormValidationSlice";
|
||||||
|
|
||||||
export const RegisterUserProfile = memo(({ kcContext, ...props }: { kcContext: KcContextBase.RegisterUserProfile } & KcProps) => {
|
export const RegisterUserProfile = memo(({ kcContext, ...props_ }: { kcContext: KcContextBase.RegisterUserProfile } & KcProps) => {
|
||||||
const { url, messagesPerField, realm, passwordRequired, recaptchaRequired, recaptchaSiteKey } = kcContext;
|
const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext;
|
||||||
|
|
||||||
const { msg, msgStr } = useKcMessage();
|
const { msg, msgStr } = useKcMessage();
|
||||||
|
|
||||||
const { cx } = useCssAndCx();
|
const { cx, css } = useCssAndCx();
|
||||||
|
|
||||||
|
const props = useMemo(
|
||||||
|
() => ({
|
||||||
|
...props_,
|
||||||
|
"kcFormGroupClass": cx(props_.kcFormGroupClass, css({ "marginBottom": 20 })),
|
||||||
|
}),
|
||||||
|
[cx, css],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [isFomSubmittable, setIsFomSubmittable] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Template
|
<Template
|
||||||
@ -22,71 +34,7 @@ export const RegisterUserProfile = memo(({ kcContext, ...props }: { kcContext: K
|
|||||||
headerNode={msg("registerTitle")}
|
headerNode={msg("registerTitle")}
|
||||||
formNode={
|
formNode={
|
||||||
<form id="kc-register-form" className={cx(props.kcFormClass)} action={url.registrationAction} method="post">
|
<form id="kc-register-form" className={cx(props.kcFormClass)} action={url.registrationAction} method="post">
|
||||||
<UserProfileFormFields
|
<UserProfileFormFields kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} {...props} />
|
||||||
kcContext={kcContext}
|
|
||||||
{...props}
|
|
||||||
AfterField={({ attribute }) =>
|
|
||||||
/*render password fields just under the username or email (if used as username)*/
|
|
||||||
(passwordRequired &&
|
|
||||||
(attribute.name == "username" || (attribute.name == "email" && realm.registrationEmailAsUsername)) && (
|
|
||||||
<>
|
|
||||||
<div className={cx(props.kcFormGroupClass)}>
|
|
||||||
<div className={cx(props.kcLabelWrapperClass)}>
|
|
||||||
<label htmlFor="password" className={cx(props.kcLabelClass)}>
|
|
||||||
{msg("password")}
|
|
||||||
</label>{" "}
|
|
||||||
*
|
|
||||||
</div>
|
|
||||||
<div className={cx(props.kcInputWrapperClass)}>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="password"
|
|
||||||
className={cx(props.kcInputClass)}
|
|
||||||
name="password"
|
|
||||||
autoComplete="new-password"
|
|
||||||
aria-invalid={
|
|
||||||
messagesPerField.existsError("password") || messagesPerField.existsError("password-confirm")
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{messagesPerField.existsError("password") && (
|
|
||||||
<span id="input-error-password" className={cx(props.kcInputErrorMessageClass)} aria-live="polite">
|
|
||||||
{messagesPerField.get("password")}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={cx(props.kcFormGroupClass)}>
|
|
||||||
<div className={cx(props.kcLabelWrapperClass)}>
|
|
||||||
<label htmlFor="password-confirm" className={cx(props.kcLabelClass)}>
|
|
||||||
{msg("passwordConfirm")}
|
|
||||||
</label>{" "}
|
|
||||||
*
|
|
||||||
</div>
|
|
||||||
<div className={cx(props.kcInputWrapperClass)}>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="password-confirm"
|
|
||||||
className={cx(props.kcInputClass)}
|
|
||||||
name="password-confirm"
|
|
||||||
autoComplete="new-password"
|
|
||||||
aria-invalid={messagesPerField.existsError("password-confirm")}
|
|
||||||
/>
|
|
||||||
{messagesPerField.existsError("password-confirm") && (
|
|
||||||
<span
|
|
||||||
id="input-error-password-confirm"
|
|
||||||
className={cx(props.kcInputErrorMessageClass)}
|
|
||||||
aria-live="polite"
|
|
||||||
>
|
|
||||||
{messagesPerField.get("password-confirm")}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)) ||
|
|
||||||
null
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{recaptchaRequired && (
|
{recaptchaRequired && (
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<div className={cx(props.kcInputWrapperClass)}>
|
<div className={cx(props.kcInputWrapperClass)}>
|
||||||
@ -108,6 +56,7 @@ export const RegisterUserProfile = memo(({ kcContext, ...props }: { kcContext: K
|
|||||||
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)}
|
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)}
|
||||||
type="submit"
|
type="submit"
|
||||||
value={msgStr("doRegister")}
|
value={msgStr("doRegister")}
|
||||||
|
disabled={!isFomSubmittable}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -117,85 +66,142 @@ export const RegisterUserProfile = memo(({ kcContext, ...props }: { kcContext: K
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const UserProfileFormFields = memo(
|
type UserProfileFormFieldsProps = { kcContext: KcContextBase.RegisterUserProfile } & KcProps &
|
||||||
({
|
Partial<Record<"BeforeField" | "AfterField", ReactComponent<{ attribute: Attribute }>>> & {
|
||||||
|
onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserProfileFormFields = memo(({ kcContext, onIsFormSubmittableValueChange, ...props }: UserProfileFormFieldsProps) => {
|
||||||
|
const { cx, css } = useCssAndCx();
|
||||||
|
|
||||||
|
const { advancedMsg } = useKcMessage();
|
||||||
|
|
||||||
|
const {
|
||||||
|
formValidationState: { fieldStateByAttributeName, isFormSubmittable },
|
||||||
|
formValidationReducer,
|
||||||
|
attributesWithPassword,
|
||||||
|
} = useFormValidationSlice({
|
||||||
kcContext,
|
kcContext,
|
||||||
BeforeField = () => null,
|
});
|
||||||
AfterField = () => null,
|
|
||||||
...props
|
|
||||||
}: { kcContext: KcContextBase.RegisterUserProfile } & KcProps &
|
|
||||||
Partial<
|
|
||||||
Record<
|
|
||||||
"BeforeField" | "AfterField",
|
|
||||||
ReactComponent<{
|
|
||||||
attribute: KcContextBase.RegisterUserProfile["profile"]["attributes"][number];
|
|
||||||
}>
|
|
||||||
>
|
|
||||||
>) => {
|
|
||||||
const { messagesPerField } = kcContext;
|
|
||||||
|
|
||||||
const { cx } = useCssAndCx();
|
useEffect(() => {
|
||||||
|
onIsFormSubmittableValueChange(isFormSubmittable);
|
||||||
|
}, [isFormSubmittable]);
|
||||||
|
|
||||||
const { advancedMsg } = useKcMessage();
|
const onChangeFactory = useCallbackFactory(
|
||||||
|
(
|
||||||
|
[name]: [string],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
target: { value },
|
||||||
|
},
|
||||||
|
]: [React.ChangeEvent<HTMLInputElement>],
|
||||||
|
) =>
|
||||||
|
formValidationReducer({
|
||||||
|
"action": "update value",
|
||||||
|
name,
|
||||||
|
"newValue": value,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
let currentGroup = "";
|
const onBlurFactory = useCallbackFactory(([name]: [string]) =>
|
||||||
|
formValidationReducer({
|
||||||
|
"action": "focus lost",
|
||||||
|
name,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
let currentGroup = "";
|
||||||
<>
|
|
||||||
{kcContext.profile.attributes
|
return (
|
||||||
.map(attribute => [attribute, attribute])
|
<>
|
||||||
.map(([attribute, { group = "", groupDisplayHeader = "", groupDisplayDescription = "" }], i) => (
|
{attributesWithPassword.map((attribute, i) => {
|
||||||
<Fragment key={i}>
|
const { group = "", groupDisplayHeader = "", groupDisplayDescription = "" } = attribute;
|
||||||
{group !== currentGroup && (currentGroup = group) !== "" && (
|
|
||||||
<div className={cx(props.kcFormGroupClass)}>
|
const { value, displayableErrors } = fieldStateByAttributeName[attribute.name];
|
||||||
<div className={cx(props.kcContentWrapperClass)}>
|
|
||||||
<label id={`header-${group}`} className={cx(props.kcFormGroupHeader)}>
|
const formGroupClassName = cx(props.kcFormGroupClass, displayableErrors.length !== 0 && props.kcFormGroupErrorClass);
|
||||||
{(groupDisplayHeader !== "" && advancedMsg(groupDisplayHeader)) || currentGroup}
|
|
||||||
|
return (
|
||||||
|
<Fragment key={i}>
|
||||||
|
{group !== currentGroup && (currentGroup = group) !== "" && (
|
||||||
|
<div className={formGroupClassName}>
|
||||||
|
<div className={cx(props.kcContentWrapperClass)}>
|
||||||
|
<label id={`header-${group}`} className={cx(props.kcFormGroupHeader)}>
|
||||||
|
{advancedMsg(groupDisplayHeader) || currentGroup}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{groupDisplayDescription !== "" && (
|
||||||
|
<div className={cx(props.kcLabelWrapperClass)}>
|
||||||
|
<label id={`description-${group}`} className={`${cx(props.kcLabelClass)}`}>
|
||||||
|
{advancedMsg(groupDisplayDescription)}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{groupDisplayDescription !== "" && (
|
)}
|
||||||
<div className={cx(props.kcLabelWrapperClass)}>
|
|
||||||
<label id={`description-${group}`} className={`${cx(props.kcLabelClass)}`}>
|
|
||||||
{advancedMsg(groupDisplayDescription) ?? ""}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<BeforeField attribute={attribute} />
|
|
||||||
<div className={cx(props.kcFormGroupClass)}>
|
|
||||||
<div className={cx(props.kcLabelWrapperClass)}>
|
|
||||||
<label htmlFor={attribute.name} className={cx(props.kcLabelClass)}>
|
|
||||||
{advancedMsg(attribute.displayName ?? "")}
|
|
||||||
</label>
|
|
||||||
{attribute.required && <>*</>}
|
|
||||||
</div>
|
|
||||||
<div className={cx(props.kcInputWrapperClass)}>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id={attribute.name}
|
|
||||||
name={attribute.name}
|
|
||||||
defaultValue={attribute.value ?? ""}
|
|
||||||
className={cx(props.kcInputClass)}
|
|
||||||
aria-invalid={messagesPerField.existsError(attribute.name)}
|
|
||||||
disabled={attribute.readOnly}
|
|
||||||
{...(attribute.autocomplete === undefined
|
|
||||||
? {}
|
|
||||||
: {
|
|
||||||
"autoComplete": attribute.autocomplete,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
{kcContext.messagesPerField.existsError(attribute.name) && (
|
|
||||||
<span id={`input-error-${attribute.name}`} className={cx(props.kcInputErrorMessageClass)} aria-live="polite">
|
|
||||||
{messagesPerField.get(attribute.name)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<AfterField attribute={attribute} />
|
)}
|
||||||
</Fragment>
|
<div className={formGroupClassName}>
|
||||||
))}
|
<div className={cx(props.kcLabelWrapperClass)}>
|
||||||
</>
|
<label htmlFor={attribute.name} className={cx(props.kcLabelClass)}>
|
||||||
);
|
{advancedMsg(attribute.displayName ?? "")}
|
||||||
},
|
</label>
|
||||||
);
|
{attribute.required && <>*</>}
|
||||||
|
</div>
|
||||||
|
<div className={cx(props.kcInputWrapperClass)}>
|
||||||
|
<input
|
||||||
|
autoComplete={(() => {
|
||||||
|
switch (attribute.name) {
|
||||||
|
case "password-confirm":
|
||||||
|
case "password":
|
||||||
|
return "new-password";
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
})()}
|
||||||
|
type={(() => {
|
||||||
|
switch (attribute.name) {
|
||||||
|
case "password-confirm":
|
||||||
|
return "password-confirm";
|
||||||
|
case "password":
|
||||||
|
return "password";
|
||||||
|
default:
|
||||||
|
return "text";
|
||||||
|
}
|
||||||
|
})()}
|
||||||
|
id={attribute.name}
|
||||||
|
name={attribute.name}
|
||||||
|
value={value}
|
||||||
|
onChange={onChangeFactory(attribute.name)}
|
||||||
|
className={cx(props.kcInputClass)}
|
||||||
|
aria-invalid={displayableErrors.length !== 0}
|
||||||
|
disabled={attribute.readOnly}
|
||||||
|
{...(attribute.autocomplete === undefined
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
"autoComplete": attribute.autocomplete,
|
||||||
|
})}
|
||||||
|
onBlur={onBlurFactory(attribute.name)}
|
||||||
|
/>
|
||||||
|
{displayableErrors.length !== 0 && (
|
||||||
|
<span
|
||||||
|
id={`input-error-${attribute.name}`}
|
||||||
|
className={cx(
|
||||||
|
props.kcInputErrorMessageClass,
|
||||||
|
css({
|
||||||
|
"position": displayableErrors.length === 1 ? "absolute" : undefined,
|
||||||
|
"& > span": { "display": "block" },
|
||||||
|
}),
|
||||||
|
)}
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
{displayableErrors.map(({ errorMessage }) => errorMessage)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
@ -243,6 +243,12 @@ export type Validators = Partial<{
|
|||||||
"person-name-prohibited-characters": Validators.DoIgnoreEmpty & Validators.ErrorMessage;
|
"person-name-prohibited-characters": Validators.DoIgnoreEmpty & Validators.ErrorMessage;
|
||||||
uri: Validators.DoIgnoreEmpty;
|
uri: Validators.DoIgnoreEmpty;
|
||||||
"username-prohibited-characters": Validators.DoIgnoreEmpty & Validators.ErrorMessage;
|
"username-prohibited-characters": Validators.DoIgnoreEmpty & Validators.ErrorMessage;
|
||||||
|
/** Made up validator that only exists in Keycloakify */
|
||||||
|
_compareToOther: Validators.DoIgnoreEmpty &
|
||||||
|
Validators.ErrorMessage & {
|
||||||
|
name: string;
|
||||||
|
shouldBe: "equal" | "different";
|
||||||
|
};
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export declare namespace Validators {
|
export declare namespace Validators {
|
||||||
@ -256,8 +262,8 @@ export declare namespace Validators {
|
|||||||
|
|
||||||
export type Range = {
|
export type Range = {
|
||||||
/** "0", "1", "2"... yeah I know, don't tell me */
|
/** "0", "1", "2"... yeah I know, don't tell me */
|
||||||
min?: string;
|
min?: `${number}`;
|
||||||
max?: string;
|
max?: `${number}`;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,6 +210,7 @@ export const kcContextMocks: KcContextBase[] = [
|
|||||||
"autocomplete": "username",
|
"autocomplete": "username",
|
||||||
"readOnly": false,
|
"readOnly": false,
|
||||||
"name": "username",
|
"name": "username",
|
||||||
|
"value": "xxxx",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"validators": {
|
"validators": {
|
||||||
@ -226,6 +227,10 @@ export const kcContextMocks: KcContextBase[] = [
|
|||||||
"email": {
|
"email": {
|
||||||
"ignore.empty.value": true,
|
"ignore.empty.value": true,
|
||||||
},
|
},
|
||||||
|
"pattern": {
|
||||||
|
"ignore.empty.value": true,
|
||||||
|
"pattern": "gmail\\.com$",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"displayName": "${email}",
|
"displayName": "${email}",
|
||||||
"annotations": {},
|
"annotations": {},
|
||||||
|
@ -1,7 +1,27 @@
|
|||||||
import { kcMessages } from "../generated_kcMessages/15.0.2/login";
|
import { kcMessages as kcMessagesBase } from "../generated_kcMessages/15.0.2/login";
|
||||||
import { Evt } from "evt";
|
import { Evt } from "evt";
|
||||||
import { objectKeys } from "tsafe/objectKeys";
|
import { objectKeys } from "tsafe/objectKeys";
|
||||||
|
|
||||||
|
const kcMessages = {
|
||||||
|
...kcMessagesBase,
|
||||||
|
"en": {
|
||||||
|
...kcMessagesBase["en"],
|
||||||
|
"shouldBeEqual": "{0} should be equal to {1}",
|
||||||
|
"shouldBeDifferent": "{0} should be different to {1}",
|
||||||
|
"shouldMatchPattern": "Pattern should match: `/{0}/`",
|
||||||
|
"mustBeAnInteger": "Must be an integer",
|
||||||
|
},
|
||||||
|
"fr": {
|
||||||
|
...kcMessagesBase["fr"],
|
||||||
|
/* spell-checker: disable */
|
||||||
|
"shouldBeEqual": "{0} doit être egale à {1}",
|
||||||
|
"shouldBeDifferent": "{0} doit être différent de {1}",
|
||||||
|
"shouldMatchPattern": "Dois respecter le schéma: `/{0}/`",
|
||||||
|
"mustBeAnInteger": "Doit être un nombre entiers",
|
||||||
|
/* spell-checker: enable */
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const evtTermsUpdated = Evt.asNonPostable(Evt.create<void>());
|
export const evtTermsUpdated = Evt.asNonPostable(Evt.create<void>());
|
||||||
|
|
||||||
(["termsText", "doAccept", "doDecline", "termsTitle"] as const).forEach(key =>
|
(["termsText", "doAccept", "doDecline", "termsTitle"] as const).forEach(key =>
|
||||||
|
@ -1,21 +1,95 @@
|
|||||||
import { useCallback, useReducer } from "react";
|
import "minimal-polyfills/Object.fromEntries";
|
||||||
|
import { useReducer } from "react";
|
||||||
import { useKcLanguageTag } from "./useKcLanguageTag";
|
import { useKcLanguageTag } from "./useKcLanguageTag";
|
||||||
import { kcMessages, evtTermsUpdated } from "./kcMessages/login";
|
import { kcMessages, evtTermsUpdated } from "./kcMessages/login";
|
||||||
import { useEvt } from "evt/hooks";
|
import { useEvt } from "evt/hooks";
|
||||||
//NOTE for later: https://github.com/remarkjs/react-markdown/blob/236182ecf30bd89c1e5a7652acaf8d0bf81e6170/src/renderers.js#L7-L35
|
//NOTE for later: https://github.com/remarkjs/react-markdown/blob/236182ecf30bd89c1e5a7652acaf8d0bf81e6170/src/renderers.js#L7-L35
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import { id } from "tsafe/id";
|
import { useGuaranteedMemo } from "powerhooks/useGuaranteedMemo";
|
||||||
|
|
||||||
export { kcMessages };
|
export { kcMessages };
|
||||||
|
|
||||||
export type MessageKey = keyof typeof kcMessages["en"];
|
export type MessageKey = keyof typeof kcMessages["en"];
|
||||||
|
|
||||||
|
function resolveMsg<Key extends string, DoRenderMarkdown extends boolean>(props: {
|
||||||
|
key: Key;
|
||||||
|
args: (string | undefined)[];
|
||||||
|
kcLanguageTag: string;
|
||||||
|
doRenderMarkdown: DoRenderMarkdown;
|
||||||
|
}): Key extends MessageKey ? (DoRenderMarkdown extends true ? JSX.Element : string) : undefined {
|
||||||
|
const { key, args, kcLanguageTag, doRenderMarkdown } = props;
|
||||||
|
|
||||||
|
let str = kcMessages[kcLanguageTag as any as "en"][key as MessageKey] ?? kcMessages["en"][key as MessageKey];
|
||||||
|
|
||||||
|
if (str === undefined) {
|
||||||
|
return undefined as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
str = (() => {
|
||||||
|
const startIndex = str
|
||||||
|
.match(/(?<={)[0-9]+(?=})/g)
|
||||||
|
?.map(g => parseInt(g))
|
||||||
|
.sort((a, b) => a - b)[0];
|
||||||
|
|
||||||
|
if (startIndex === undefined) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
args.forEach((arg, i) => {
|
||||||
|
if (arg === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
str = str.replace(new RegExp(`\\{${i + startIndex}\\}`, "g"), arg);
|
||||||
|
});
|
||||||
|
|
||||||
|
return str;
|
||||||
|
})();
|
||||||
|
|
||||||
|
return (
|
||||||
|
doRenderMarkdown ? (
|
||||||
|
<ReactMarkdown allowDangerousHtml renderers={key === "termsText" ? undefined : { "paragraph": "span" }}>
|
||||||
|
{str}
|
||||||
|
</ReactMarkdown>
|
||||||
|
) : (
|
||||||
|
str
|
||||||
|
)
|
||||||
|
) as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveMsgAdvanced<Key extends string, DoRenderMarkdown extends boolean>(props: {
|
||||||
|
key: Key;
|
||||||
|
args: (string | undefined)[];
|
||||||
|
kcLanguageTag: string;
|
||||||
|
doRenderMarkdown: DoRenderMarkdown;
|
||||||
|
}): DoRenderMarkdown extends true ? JSX.Element : string {
|
||||||
|
const { key, args, kcLanguageTag, doRenderMarkdown } = props;
|
||||||
|
|
||||||
|
const match = key.match(/^\$\{([^{]+)\}$/);
|
||||||
|
|
||||||
|
const resolvedKey = match === null ? key : match[1];
|
||||||
|
|
||||||
|
const out = resolveMsg({
|
||||||
|
"key": resolvedKey,
|
||||||
|
args,
|
||||||
|
kcLanguageTag,
|
||||||
|
doRenderMarkdown,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (out !== undefined ? out : match === null ? doRenderMarkdown ? <span>{key}</span> : key : undefined) as any;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When the language is switched the page is reloaded, this may appear
|
* When the language is switched the page is reloaded, this may appear
|
||||||
* as a bug as you might notice that the language successfully switch before
|
* as a bug as you might notice that the language successfully switch before
|
||||||
* reload.
|
* reload.
|
||||||
* However we need to tell Keycloak that the user have changed the language
|
* However we need to tell Keycloak that the user have changed the language
|
||||||
* during login so we can retrieve the "local" field of the JWT encoded accessToken.
|
* during login so we can retrieve the "local" field of the JWT encoded accessToken.
|
||||||
|
* https://user-images.githubusercontent.com/6702424/138096682-351bb61f-f24e-4caf-91b7-cca8cfa2cb58.mov
|
||||||
|
*
|
||||||
|
* advancedMsg("${access-denied}") === advancedMsg("access-denied") === msg("access-denied")
|
||||||
|
* advancedMsg("${not-a-message-key}") === advancedMsg(not-a-message-key") === "not-a-message-key"
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
export function useKcMessage() {
|
export function useKcMessage() {
|
||||||
const { kcLanguageTag } = useKcLanguageTag();
|
const { kcLanguageTag } = useKcLanguageTag();
|
||||||
@ -24,46 +98,17 @@ export function useKcMessage() {
|
|||||||
|
|
||||||
useEvt(ctx => evtTermsUpdated.attach(ctx, forceUpdate), []);
|
useEvt(ctx => evtTermsUpdated.attach(ctx, forceUpdate), []);
|
||||||
|
|
||||||
const msgStr = useCallback(
|
return useGuaranteedMemo(
|
||||||
(key: MessageKey, ...args: (string | undefined)[]): string => {
|
() => ({
|
||||||
let str: string = kcMessages[kcLanguageTag as any as "en"][key] ?? kcMessages["en"][key];
|
"msgStr": (key: MessageKey, ...args: (string | undefined)[]): string =>
|
||||||
|
resolveMsg({ key, args, kcLanguageTag, "doRenderMarkdown": false }),
|
||||||
args.forEach((arg, i) => {
|
"msg": (key: MessageKey, ...args: (string | undefined)[]): JSX.Element =>
|
||||||
if (arg === undefined) {
|
resolveMsg({ key, args, kcLanguageTag, "doRenderMarkdown": true }),
|
||||||
return;
|
"advancedMsg": <Key extends string>(key: Key, ...args: (string | undefined)[]): JSX.Element =>
|
||||||
}
|
resolveMsgAdvanced({ key, args, kcLanguageTag, "doRenderMarkdown": true }),
|
||||||
|
"advancedMsgStr": <Key extends string>(key: Key, ...args: (string | undefined)[]): string =>
|
||||||
str = str.replace(new RegExp(`\\{${i}\\}`, "g"), arg);
|
resolveMsgAdvanced({ key, args, kcLanguageTag, "doRenderMarkdown": false }),
|
||||||
});
|
}),
|
||||||
|
|
||||||
return str;
|
|
||||||
},
|
|
||||||
[kcLanguageTag, trigger],
|
[kcLanguageTag, trigger],
|
||||||
);
|
);
|
||||||
|
|
||||||
const msg = useCallback<(...args: Parameters<typeof msgStr>) => JSX.Element>(
|
|
||||||
(key, ...args) => (
|
|
||||||
<ReactMarkdown allowDangerousHtml renderers={key === "termsText" ? undefined : { "paragraph": "span" }}>
|
|
||||||
{msgStr(key, ...args)}
|
|
||||||
</ReactMarkdown>
|
|
||||||
),
|
|
||||||
[msgStr],
|
|
||||||
);
|
|
||||||
|
|
||||||
const advancedMsg = useCallback(
|
|
||||||
(key: string): string | undefined => {
|
|
||||||
const match = key.match(/^\$\{([^{]+)\}$/);
|
|
||||||
|
|
||||||
const resolvedKey = match === null ? key : match[1];
|
|
||||||
|
|
||||||
const out =
|
|
||||||
id<Record<string, string | undefined>>(kcMessages[kcLanguageTag])[resolvedKey] ??
|
|
||||||
id<Record<string, string | undefined>>(kcMessages["en"])[resolvedKey];
|
|
||||||
|
|
||||||
return out !== undefined ? out : match === null ? key : undefined;
|
|
||||||
},
|
|
||||||
[msgStr],
|
|
||||||
);
|
|
||||||
|
|
||||||
return { msg, msgStr, advancedMsg };
|
|
||||||
}
|
}
|
||||||
|
64
src/lib/tools/Array.prototype.every.ts
Normal file
64
src/lib/tools/Array.prototype.every.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
if (!Array.prototype.every) {
|
||||||
|
Array.prototype.every = function (callbackfn: any, thisArg: any) {
|
||||||
|
"use strict";
|
||||||
|
var T, k;
|
||||||
|
|
||||||
|
if (this == null) {
|
||||||
|
throw new TypeError("this is null or not defined");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Let O be the result of calling ToObject passing the this
|
||||||
|
// value as the argument.
|
||||||
|
var O = Object(this);
|
||||||
|
|
||||||
|
// 2. Let lenValue be the result of calling the Get internal method
|
||||||
|
// of O with the argument "length".
|
||||||
|
// 3. Let len be ToUint32(lenValue).
|
||||||
|
var len = O.length >>> 0;
|
||||||
|
|
||||||
|
// 4. If IsCallable(callbackfn) is false, throw a TypeError exception.
|
||||||
|
if (typeof callbackfn !== "function" && Object.prototype.toString.call(callbackfn) !== "[object Function]") {
|
||||||
|
throw new TypeError();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
|
||||||
|
if (arguments.length > 1) {
|
||||||
|
T = thisArg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Let k be 0.
|
||||||
|
k = 0;
|
||||||
|
|
||||||
|
// 7. Repeat, while k < len
|
||||||
|
while (k < len) {
|
||||||
|
var kValue;
|
||||||
|
|
||||||
|
// a. Let Pk be ToString(k).
|
||||||
|
// This is implicit for LHS operands of the in operator
|
||||||
|
// b. Let kPresent be the result of calling the HasProperty internal
|
||||||
|
// method of O with argument Pk.
|
||||||
|
// This step can be combined with c
|
||||||
|
// c. If kPresent is true, then
|
||||||
|
if (k in O) {
|
||||||
|
var testResult;
|
||||||
|
// i. Let kValue be the result of calling the Get internal method
|
||||||
|
// of O with argument Pk.
|
||||||
|
kValue = O[k];
|
||||||
|
|
||||||
|
// ii. Let testResult be the result of calling the Call internal method
|
||||||
|
// of callbackfn with T as the this value if T is not undefined
|
||||||
|
// else is the result of calling callbackfn
|
||||||
|
// and argument list containing kValue, k, and O.
|
||||||
|
if (T) testResult = callbackfn.call(T, kValue, k, O);
|
||||||
|
else testResult = callbackfn(kValue, k, O);
|
||||||
|
|
||||||
|
// iii. If ToBoolean(testResult) is false, return false.
|
||||||
|
if (!testResult) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
k++;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
2
src/lib/tools/emailRegExp.ts
Normal file
2
src/lib/tools/emailRegExp.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export const emailRegexp =
|
||||||
|
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
428
src/lib/useFormValidationSlice.tsx
Normal file
428
src/lib/useFormValidationSlice.tsx
Normal file
@ -0,0 +1,428 @@
|
|||||||
|
import "./tools/Array.prototype.every";
|
||||||
|
import { useMemo, useReducer, Fragment } from "react";
|
||||||
|
import type { KcContextBase, Validators, Attribute } from "./getKcContext/KcContextBase";
|
||||||
|
import { useKcMessage } from "./i18n/useKcMessage";
|
||||||
|
import { useConstCallback } from "powerhooks/useConstCallback";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
import type { MessageKey } from "./i18n/useKcMessage";
|
||||||
|
import { useConst } from "powerhooks/useConst";
|
||||||
|
import { emailRegexp } from "./tools/emailRegExp";
|
||||||
|
|
||||||
|
export type KcContextLike = {
|
||||||
|
messagesPerField: Pick<KcContextBase.Common["messagesPerField"], "existsError" | "get">;
|
||||||
|
attributes: { name: string; value?: string; validators: Validators }[];
|
||||||
|
passwordRequired: boolean;
|
||||||
|
realm: { registrationEmailAsUsername: boolean };
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useGetErrors(params: {
|
||||||
|
kcContext: {
|
||||||
|
messagesPerField: Pick<KcContextBase.Common["messagesPerField"], "existsError" | "get">;
|
||||||
|
profile: {
|
||||||
|
attributes: { name: string; value?: string; validators: Validators }[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
kcContext: {
|
||||||
|
messagesPerField,
|
||||||
|
profile: { attributes },
|
||||||
|
},
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
const { msg, msgStr, advancedMsg, advancedMsgStr } = useKcMessage();
|
||||||
|
|
||||||
|
const getErrors = useConstCallback((params: { name: string; fieldValueByAttributeName: Record<string, { value: string }> }) => {
|
||||||
|
const { name, fieldValueByAttributeName } = params;
|
||||||
|
|
||||||
|
const { value } = fieldValueByAttributeName[name];
|
||||||
|
|
||||||
|
const { value: defaultValue, validators } = attributes.find(attribute => attribute.name === name)!;
|
||||||
|
|
||||||
|
if (defaultValue === value && messagesPerField.existsError(value)) {
|
||||||
|
const errorMessageStr = messagesPerField.get(value);
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"validatorName": undefined,
|
||||||
|
errorMessageStr,
|
||||||
|
"errorMessage": <span key={0}>{errorMessageStr}</span>,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors: {
|
||||||
|
errorMessage: JSX.Element;
|
||||||
|
errorMessageStr: string;
|
||||||
|
validatorName: keyof Validators | undefined;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
scope: {
|
||||||
|
const validatorName = "length";
|
||||||
|
|
||||||
|
const validator = validators[validatorName];
|
||||||
|
|
||||||
|
if (validator === undefined) {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { "ignore.empty.value": ignoreEmptyValue = false, max, min } = validator;
|
||||||
|
|
||||||
|
if (ignoreEmptyValue && value === "") {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (max !== undefined && value.length > parseInt(max)) {
|
||||||
|
const msgArgs = ["error-invalid-length-too-long", max] as const;
|
||||||
|
|
||||||
|
errors.push({
|
||||||
|
"errorMessage": <Fragment key={errors.length}>{msg(...msgArgs)}</Fragment>,
|
||||||
|
"errorMessageStr": msgStr(...msgArgs),
|
||||||
|
validatorName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (min !== undefined && value.length < parseInt(min)) {
|
||||||
|
const msgArgs = ["error-invalid-length-too-short", min] as const;
|
||||||
|
|
||||||
|
errors.push({
|
||||||
|
"errorMessage": <Fragment key={errors.length}>{msg(...msgArgs)}</Fragment>,
|
||||||
|
"errorMessageStr": msgStr(...msgArgs),
|
||||||
|
validatorName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scope: {
|
||||||
|
const validatorName = "_compareToOther";
|
||||||
|
|
||||||
|
const validator = validators[validatorName];
|
||||||
|
|
||||||
|
if (validator === undefined) {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { "ignore.empty.value": ignoreEmptyValue = false, name: otherName, shouldBe, "error-message": errorMessageKey } = validator;
|
||||||
|
|
||||||
|
if (ignoreEmptyValue && value === "") {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { value: otherValue } = fieldValueByAttributeName[otherName];
|
||||||
|
|
||||||
|
const isValid = (() => {
|
||||||
|
switch (shouldBe) {
|
||||||
|
case "different":
|
||||||
|
return otherValue !== value;
|
||||||
|
case "equal":
|
||||||
|
return otherValue === value;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
const msgArg = [
|
||||||
|
errorMessageKey ??
|
||||||
|
id<MessageKey>(
|
||||||
|
(() => {
|
||||||
|
switch (shouldBe) {
|
||||||
|
case "equal":
|
||||||
|
return "shouldBeEqual";
|
||||||
|
case "different":
|
||||||
|
return "shouldBeDifferent";
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
),
|
||||||
|
otherName,
|
||||||
|
name,
|
||||||
|
shouldBe,
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
errors.push({
|
||||||
|
validatorName,
|
||||||
|
"errorMessage": <Fragment key={errors.length}>{advancedMsg(...msgArg)}</Fragment>,
|
||||||
|
"errorMessageStr": advancedMsgStr(...msgArg),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
scope: {
|
||||||
|
const validatorName = "pattern";
|
||||||
|
|
||||||
|
const validator = validators[validatorName];
|
||||||
|
|
||||||
|
if (validator === undefined) {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { "ignore.empty.value": ignoreEmptyValue = false, pattern, "error-message": errorMessageKey } = validator;
|
||||||
|
|
||||||
|
if (ignoreEmptyValue && value === "") {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new RegExp(pattern).test(value)) {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
const msgArgs = [errorMessageKey ?? id<MessageKey>("shouldMatchPattern"), pattern] as const;
|
||||||
|
|
||||||
|
errors.push({
|
||||||
|
validatorName,
|
||||||
|
"errorMessage": <Fragment key={errors.length}>{advancedMsg(...msgArgs)}</Fragment>,
|
||||||
|
"errorMessageStr": advancedMsgStr(...msgArgs),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
scope: {
|
||||||
|
if ([...errors].reverse()[0]?.validatorName === "pattern") {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validatorName = "email";
|
||||||
|
|
||||||
|
const validator = validators[validatorName];
|
||||||
|
|
||||||
|
if (validator === undefined) {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { "ignore.empty.value": ignoreEmptyValue = false } = validator;
|
||||||
|
|
||||||
|
if (ignoreEmptyValue && value === "") {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emailRegexp.test(value)) {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
const msgArgs = ["invalidEmailMessage"] as const;
|
||||||
|
|
||||||
|
errors.push({
|
||||||
|
validatorName,
|
||||||
|
"errorMessage": <Fragment key={errors.length}>{msg(...msgArgs)}</Fragment>,
|
||||||
|
"errorMessageStr": msgStr(...msgArgs),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
scope: {
|
||||||
|
const validatorName = "integer";
|
||||||
|
|
||||||
|
const validator = validators[validatorName];
|
||||||
|
|
||||||
|
if (validator === undefined) {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { "ignore.empty.value": ignoreEmptyValue = false, max, min } = validator;
|
||||||
|
|
||||||
|
if (ignoreEmptyValue && value === "") {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
const intValue = parseInt(value);
|
||||||
|
|
||||||
|
if (isNaN(intValue)) {
|
||||||
|
const msgArgs = ["mustBeAnInteger"] as const;
|
||||||
|
|
||||||
|
errors.push({
|
||||||
|
validatorName,
|
||||||
|
"errorMessage": <Fragment key={errors.length}>{msg(...msgArgs)}</Fragment>,
|
||||||
|
"errorMessageStr": msgStr(...msgArgs),
|
||||||
|
});
|
||||||
|
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (max !== undefined && intValue > parseInt(max)) {
|
||||||
|
const msgArgs = ["error-number-out-of-range-too-big", max] as const;
|
||||||
|
|
||||||
|
errors.push({
|
||||||
|
validatorName,
|
||||||
|
"errorMessage": <Fragment key={errors.length}>{msg(...msgArgs)}</Fragment>,
|
||||||
|
"errorMessageStr": msgStr(...msgArgs),
|
||||||
|
});
|
||||||
|
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (min !== undefined && intValue < parseInt(min)) {
|
||||||
|
const msgArgs = ["error-number-out-of-range-too-small", min] as const;
|
||||||
|
|
||||||
|
errors.push({
|
||||||
|
validatorName,
|
||||||
|
"errorMessage": <Fragment key={errors.length}>{msg(...msgArgs)}</Fragment>,
|
||||||
|
"errorMessageStr": msgStr(...msgArgs),
|
||||||
|
});
|
||||||
|
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Implement missing validators.
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { getErrors };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFormValidationSlice(params: {
|
||||||
|
kcContext: {
|
||||||
|
messagesPerField: Pick<KcContextBase.Common["messagesPerField"], "existsError" | "get">;
|
||||||
|
profile: {
|
||||||
|
attributes: Attribute[];
|
||||||
|
};
|
||||||
|
passwordRequired: boolean;
|
||||||
|
realm: { registrationEmailAsUsername: boolean };
|
||||||
|
};
|
||||||
|
passwordValidators?: Validators;
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
kcContext,
|
||||||
|
passwordValidators = {
|
||||||
|
"length": {
|
||||||
|
"ignore.empty.value": true,
|
||||||
|
"min": "4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
const attributesWithPassword = useConst(() =>
|
||||||
|
!kcContext.passwordRequired
|
||||||
|
? kcContext.profile.attributes
|
||||||
|
: (() => {
|
||||||
|
const name = kcContext.realm.registrationEmailAsUsername ? "email" : "username";
|
||||||
|
|
||||||
|
return kcContext.profile.attributes.reduce<Attribute[]>(
|
||||||
|
(prev, curr) => [
|
||||||
|
...prev,
|
||||||
|
...(curr.name !== name
|
||||||
|
? [curr]
|
||||||
|
: [
|
||||||
|
curr,
|
||||||
|
id<Attribute>({
|
||||||
|
"name": "password",
|
||||||
|
"displayName": id<`\${${MessageKey}}`>("${password}"),
|
||||||
|
"required": true,
|
||||||
|
"readOnly": false,
|
||||||
|
"validators": passwordValidators,
|
||||||
|
"annotations": {},
|
||||||
|
"groupAnnotations": {},
|
||||||
|
}),
|
||||||
|
id<Attribute>({
|
||||||
|
"name": "password-confirm",
|
||||||
|
"displayName": id<`\${${MessageKey}}`>("${passwordConfirm}"),
|
||||||
|
"required": true,
|
||||||
|
"readOnly": false,
|
||||||
|
"validators": {
|
||||||
|
"_compareToOther": {
|
||||||
|
"name": "password",
|
||||||
|
"ignore.empty.value": true,
|
||||||
|
"shouldBe": "equal",
|
||||||
|
"error-message": id<`\${${MessageKey}}`>("${invalidPasswordConfirmMessage}"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"annotations": {},
|
||||||
|
"groupAnnotations": {},
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { getErrors } = useGetErrors({
|
||||||
|
"kcContext": {
|
||||||
|
"messagesPerField": kcContext.messagesPerField,
|
||||||
|
"profile": {
|
||||||
|
"attributes": attributesWithPassword,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const initialInternalState = useConst(() =>
|
||||||
|
Object.fromEntries(
|
||||||
|
attributesWithPassword
|
||||||
|
.map(attribute => ({
|
||||||
|
attribute,
|
||||||
|
"errors": getErrors({
|
||||||
|
"name": attribute.name,
|
||||||
|
"fieldValueByAttributeName": Object.fromEntries(
|
||||||
|
attributesWithPassword.map(({ name, value }) => [name, { "value": value ?? "" }]),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
.map(({ attribute, errors }) => [
|
||||||
|
attribute.name,
|
||||||
|
{
|
||||||
|
"value": attribute.value ?? "",
|
||||||
|
errors,
|
||||||
|
"doDisplayPotentialErrorMessages": errors.length !== 0,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
type InternalState = typeof initialInternalState;
|
||||||
|
|
||||||
|
const [formValidationInternalState, formValidationReducer] = useReducer(
|
||||||
|
(
|
||||||
|
state: InternalState,
|
||||||
|
params:
|
||||||
|
| {
|
||||||
|
action: "update value";
|
||||||
|
name: string;
|
||||||
|
newValue: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
action: "focus lost";
|
||||||
|
name: string;
|
||||||
|
},
|
||||||
|
): InternalState => ({
|
||||||
|
...state,
|
||||||
|
[params.name]: {
|
||||||
|
...state[params.name],
|
||||||
|
...(() => {
|
||||||
|
switch (params.action) {
|
||||||
|
case "focus lost":
|
||||||
|
return { "doDisplayPotentialErrorMessages": true };
|
||||||
|
case "update value":
|
||||||
|
return {
|
||||||
|
"value": params.newValue,
|
||||||
|
"errors": getErrors({
|
||||||
|
"name": params.name,
|
||||||
|
"fieldValueByAttributeName": {
|
||||||
|
...state,
|
||||||
|
[params.name]: { "value": params.newValue },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
initialInternalState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const formValidationState = useMemo(
|
||||||
|
() => ({
|
||||||
|
"fieldStateByAttributeName": Object.fromEntries(
|
||||||
|
Object.entries(formValidationInternalState).map(([name, { value, errors, doDisplayPotentialErrorMessages }]) => [
|
||||||
|
name,
|
||||||
|
{ value, "displayableErrors": doDisplayPotentialErrorMessages ? errors : [] },
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
"isFormSubmittable": Object.entries(formValidationInternalState).every(
|
||||||
|
([name, { value, errors }]) =>
|
||||||
|
errors.length === 0 && (value !== "" || !attributesWithPassword.find(attribute => attribute.name === name)!.required),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
[formValidationInternalState],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { formValidationState, formValidationReducer, attributesWithPassword };
|
||||||
|
}
|
@ -1221,10 +1221,10 @@ please-upgrade-node@^3.2.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
semver-compare "^1.0.0"
|
semver-compare "^1.0.0"
|
||||||
|
|
||||||
powerhooks@^0.9.6:
|
powerhooks@^0.11.0:
|
||||||
version "0.9.6"
|
version "0.11.0"
|
||||||
resolved "https://registry.yarnpkg.com/powerhooks/-/powerhooks-0.9.6.tgz#45bdd7e7713d0a620b1b099cf2685e5f56cebd8f"
|
resolved "https://registry.yarnpkg.com/powerhooks/-/powerhooks-0.11.0.tgz#923749ed405a1189759cd3f16d36bd5efacfd40c"
|
||||||
integrity sha512-vXGcC5Ty3e5wxnRP37c7rnTE/UY86VVLwAj3tqAMvC9xf1C9wOmu2Q7xTj/4FGK1oGvgqbTiiWuxd+WK4C7kEQ==
|
integrity sha512-I48J5vJqlTRiR3eH6svxiIYLutdedu2YEd3uhCmK+pRg2jcuourVwp1UYORI/EzbKC9vv0X/0Vd/SZ6e07rYtA==
|
||||||
dependencies:
|
dependencies:
|
||||||
evt "2.0.0-beta.38"
|
evt "2.0.0-beta.38"
|
||||||
memoizee "^0.4.15"
|
memoizee "^0.4.15"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user