Feature update-user-profile.ftl #164
This commit is contained in:
@ -27,7 +27,8 @@ export const pageIds = [
|
|||||||
"login-idp-link-email.ftl",
|
"login-idp-link-email.ftl",
|
||||||
"login-page-expired.ftl",
|
"login-page-expired.ftl",
|
||||||
"login-config-totp.ftl",
|
"login-config-totp.ftl",
|
||||||
"logout-confirm.ftl"
|
"logout-confirm.ftl",
|
||||||
|
"update-user-profile.ftl"
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
||||||
|
@ -20,6 +20,7 @@ const LoginPageExpired = lazy(() => import("./LoginPageExpired"));
|
|||||||
const LoginIdpLinkEmail = lazy(() => import("./LoginIdpLinkEmail"));
|
const LoginIdpLinkEmail = lazy(() => import("./LoginIdpLinkEmail"));
|
||||||
const LoginConfigTotp = lazy(() => import("./LoginConfigTotp"));
|
const LoginConfigTotp = lazy(() => import("./LoginConfigTotp"));
|
||||||
const LogoutConfirm = lazy(() => import("./LogoutConfirm"));
|
const LogoutConfirm = lazy(() => import("./LogoutConfirm"));
|
||||||
|
const UpdateUserProfile = lazy(() => import("./UpdateUserProfile"));
|
||||||
|
|
||||||
const KcApp = memo(({ kcContext, i18n: userProvidedI18n, ...kcProps }: { kcContext: KcContextBase; i18n?: I18n } & KcProps) => {
|
const KcApp = memo(({ kcContext, i18n: userProvidedI18n, ...kcProps }: { kcContext: KcContextBase; i18n?: I18n } & KcProps) => {
|
||||||
const i18n = (function useClosure() {
|
const i18n = (function useClosure() {
|
||||||
@ -74,6 +75,8 @@ const KcApp = memo(({ kcContext, i18n: userProvidedI18n, ...kcProps }: { kcConte
|
|||||||
return <LoginConfigTotp {...{ kcContext, ...props }} />;
|
return <LoginConfigTotp {...{ kcContext, ...props }} />;
|
||||||
case "logout-confirm.ftl":
|
case "logout-confirm.ftl":
|
||||||
return <LogoutConfirm {...{ kcContext, ...props }} />;
|
return <LogoutConfirm {...{ kcContext, ...props }} />;
|
||||||
|
case "update-user-profile.ftl":
|
||||||
|
return <UpdateUserProfile {...{ kcContext, ...props }} />;
|
||||||
}
|
}
|
||||||
})()}
|
})()}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import React, { useMemo, memo, useEffect, useState, Fragment } from "react";
|
import React, { useMemo, memo, useState } from "react";
|
||||||
import Template from "./Template";
|
import Template from "./Template";
|
||||||
import type { KcProps } from "./KcProps";
|
import type { KcProps } from "./KcProps";
|
||||||
import type { KcContextBase, Attribute } from "../getKcContext/KcContextBase";
|
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||||
import { useCssAndCx } from "../tools/useCssAndCx";
|
import { useCssAndCx } from "../tools/useCssAndCx";
|
||||||
import type { ReactComponent } from "../tools/ReactComponent";
|
|
||||||
import { useCallbackFactory } from "powerhooks/useCallbackFactory";
|
|
||||||
import { useFormValidationSlice } from "../useFormValidationSlice";
|
|
||||||
import type { I18n } from "../i18n";
|
import type { I18n } from "../i18n";
|
||||||
|
import { UserProfileFormFields } from "./shared/UserProfileCommons";
|
||||||
|
|
||||||
const RegisterUserProfile = memo(({ kcContext, i18n, ...props_ }: { kcContext: KcContextBase.RegisterUserProfile; i18n: I18n } & KcProps) => {
|
const RegisterUserProfile = memo(({ kcContext, i18n, ...props_ }: { kcContext: KcContextBase.RegisterUserProfile; i18n: I18n } & KcProps) => {
|
||||||
const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext;
|
const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext;
|
||||||
@ -34,7 +32,13 @@ const RegisterUserProfile = memo(({ kcContext, i18n, ...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 kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} i18n={i18n} {...props} />
|
<UserProfileFormFields
|
||||||
|
kcContext={kcContext}
|
||||||
|
doInsertPasswordFields={true}
|
||||||
|
onIsFormSubmittableValueChange={setIsFomSubmittable}
|
||||||
|
i18n={i18n}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
{recaptchaRequired && (
|
{recaptchaRequired && (
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<div className={cx(props.kcInputWrapperClass)}>
|
<div className={cx(props.kcInputWrapperClass)}>
|
||||||
@ -66,155 +70,4 @@ const RegisterUserProfile = memo(({ kcContext, i18n, ...props_ }: { kcContext: K
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
type UserProfileFormFieldsProps = { kcContext: KcContextBase.RegisterUserProfile; i18n: I18n } & KcProps &
|
|
||||||
Partial<Record<"BeforeField" | "AfterField", ReactComponent<{ attribute: Attribute }>>> & {
|
|
||||||
onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const UserProfileFormFields = memo(({ kcContext, onIsFormSubmittableValueChange, i18n, ...props }: UserProfileFormFieldsProps) => {
|
|
||||||
const { cx, css } = useCssAndCx();
|
|
||||||
|
|
||||||
const { advancedMsg } = i18n;
|
|
||||||
|
|
||||||
const {
|
|
||||||
formValidationState: { fieldStateByAttributeName, isFormSubmittable },
|
|
||||||
formValidationReducer,
|
|
||||||
attributesWithPassword
|
|
||||||
} = useFormValidationSlice({
|
|
||||||
kcContext,
|
|
||||||
i18n
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onIsFormSubmittableValueChange(isFormSubmittable);
|
|
||||||
}, [isFormSubmittable]);
|
|
||||||
|
|
||||||
const onChangeFactory = useCallbackFactory(
|
|
||||||
(
|
|
||||||
[name]: [string],
|
|
||||||
[
|
|
||||||
{
|
|
||||||
target: { value }
|
|
||||||
}
|
|
||||||
]: [React.ChangeEvent<HTMLInputElement | HTMLSelectElement>]
|
|
||||||
) =>
|
|
||||||
formValidationReducer({
|
|
||||||
"action": "update value",
|
|
||||||
name,
|
|
||||||
"newValue": value
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const onBlurFactory = useCallbackFactory(([name]: [string]) =>
|
|
||||||
formValidationReducer({
|
|
||||||
"action": "focus lost",
|
|
||||||
name
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
let currentGroup = "";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{attributesWithPassword.map((attribute, i) => {
|
|
||||||
const { group = "", groupDisplayHeader = "", groupDisplayDescription = "" } = attribute;
|
|
||||||
|
|
||||||
const { value, displayableErrors } = fieldStateByAttributeName[attribute.name];
|
|
||||||
|
|
||||||
const formGroupClassName = cx(props.kcFormGroupClass, displayableErrors.length !== 0 && props.kcFormGroupErrorClass);
|
|
||||||
|
|
||||||
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>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<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)}>
|
|
||||||
{(() => {
|
|
||||||
const { options } = attribute.validators;
|
|
||||||
|
|
||||||
if (options !== undefined) {
|
|
||||||
return (
|
|
||||||
<select
|
|
||||||
id={attribute.name}
|
|
||||||
name={attribute.name}
|
|
||||||
onChange={onChangeFactory(attribute.name)}
|
|
||||||
onBlur={onBlurFactory(attribute.name)}
|
|
||||||
value={value}
|
|
||||||
>
|
|
||||||
{options.options.map(option => (
|
|
||||||
<option key={option} value={option}>
|
|
||||||
{option}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
type={(() => {
|
|
||||||
switch (attribute.name) {
|
|
||||||
case "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}
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default RegisterUserProfile;
|
export default RegisterUserProfile;
|
||||||
|
77
src/lib/components/UpdateUserProfile.tsx
Normal file
77
src/lib/components/UpdateUserProfile.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import React, { useState, memo } from "react";
|
||||||
|
import Template from "./Template";
|
||||||
|
import type { KcProps } from "./KcProps";
|
||||||
|
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||||
|
import { useCssAndCx } from "../tools/useCssAndCx";
|
||||||
|
import type { I18n } from "../i18n";
|
||||||
|
import { UserProfileFormFields } from "./shared/UserProfileCommons";
|
||||||
|
|
||||||
|
const LoginUpdateProfile = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.UpdateUserProfile; i18n: I18n } & KcProps) => {
|
||||||
|
const { cx } = useCssAndCx();
|
||||||
|
|
||||||
|
const { msg, msgStr } = i18n;
|
||||||
|
|
||||||
|
const { url, isAppInitiatedAction } = kcContext;
|
||||||
|
|
||||||
|
const [isFomSubmittable, setIsFomSubmittable] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template
|
||||||
|
{...{ kcContext, i18n, ...props }}
|
||||||
|
doFetchDefaultThemeResources={true}
|
||||||
|
headerNode={msg("loginProfileTitle")}
|
||||||
|
formNode={
|
||||||
|
<form id="kc-update-profile-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post">
|
||||||
|
<UserProfileFormFields
|
||||||
|
kcContext={kcContext}
|
||||||
|
doInsertPasswordFields={true}
|
||||||
|
onIsFormSubmittableValueChange={setIsFomSubmittable}
|
||||||
|
i18n={i18n}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={cx(props.kcFormGroupClass)}>
|
||||||
|
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
|
||||||
|
<div className={cx(props.kcFormOptionsWrapperClass)}></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
|
||||||
|
{isAppInitiatedAction ? (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonLargeClass)}
|
||||||
|
type="submit"
|
||||||
|
value={msgStr("doSubmit")}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonLargeClass)}
|
||||||
|
type="submit"
|
||||||
|
name="cancel-aia"
|
||||||
|
value="true"
|
||||||
|
formNoValidate
|
||||||
|
>
|
||||||
|
{msg("doCancel")}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
className={cx(
|
||||||
|
props.kcButtonClass,
|
||||||
|
props.kcButtonPrimaryClass,
|
||||||
|
props.kcButtonBlockClass,
|
||||||
|
props.kcButtonLargeClass
|
||||||
|
)}
|
||||||
|
type="submit"
|
||||||
|
defaultValue={msgStr("doSubmit")}
|
||||||
|
disabled={!isFomSubmittable}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default LoginUpdateProfile;
|
172
src/lib/components/shared/UserProfileCommons.tsx
Normal file
172
src/lib/components/shared/UserProfileCommons.tsx
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import React, { memo, useEffect, Fragment } from "react";
|
||||||
|
import type { KcProps } from "../KcProps";
|
||||||
|
import type { Attribute } from "../../getKcContext/KcContextBase";
|
||||||
|
import { useCssAndCx } from "../../tools/useCssAndCx";
|
||||||
|
import type { ReactComponent } from "../../tools/ReactComponent";
|
||||||
|
import { useCallbackFactory } from "powerhooks/useCallbackFactory";
|
||||||
|
import { useFormValidationSlice } from "../../useFormValidationSlice";
|
||||||
|
import type { I18n } from "../../i18n";
|
||||||
|
import type { Param0 } from "tsafe/Param0";
|
||||||
|
|
||||||
|
export type UserProfileFormFieldsProps = {
|
||||||
|
//kcContext: KcContextBase.RegisterUserProfile;
|
||||||
|
kcContext: Param0<typeof useFormValidationSlice>["kcContext"];
|
||||||
|
doInsertPasswordFields: boolean;
|
||||||
|
i18n: I18n;
|
||||||
|
} & KcProps &
|
||||||
|
Partial<Record<"BeforeField" | "AfterField", ReactComponent<{ attribute: Attribute }>>> & {
|
||||||
|
onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UserProfileFormFields = memo(
|
||||||
|
({ kcContext, doInsertPasswordFields, onIsFormSubmittableValueChange, i18n, BeforeField, AfterField, ...props }: UserProfileFormFieldsProps) => {
|
||||||
|
const { cx, css } = useCssAndCx();
|
||||||
|
|
||||||
|
const { advancedMsg } = i18n;
|
||||||
|
|
||||||
|
const {
|
||||||
|
formValidationState: { fieldStateByAttributeName, isFormSubmittable },
|
||||||
|
formValidationReducer,
|
||||||
|
attributesWithPassword
|
||||||
|
} = useFormValidationSlice({
|
||||||
|
kcContext,
|
||||||
|
i18n
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onIsFormSubmittableValueChange(isFormSubmittable);
|
||||||
|
}, [isFormSubmittable]);
|
||||||
|
|
||||||
|
const onChangeFactory = useCallbackFactory(
|
||||||
|
(
|
||||||
|
[name]: [string],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
target: { value }
|
||||||
|
}
|
||||||
|
]: [React.ChangeEvent<HTMLInputElement | HTMLSelectElement>]
|
||||||
|
) =>
|
||||||
|
formValidationReducer({
|
||||||
|
"action": "update value",
|
||||||
|
name,
|
||||||
|
"newValue": value
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const onBlurFactory = useCallbackFactory(([name]: [string]) =>
|
||||||
|
formValidationReducer({
|
||||||
|
"action": "focus lost",
|
||||||
|
name
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let currentGroup = "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{(doInsertPasswordFields ? attributesWithPassword : kcContext.profile.attributes).map((attribute, i) => {
|
||||||
|
const { group = "", groupDisplayHeader = "", groupDisplayDescription = "" } = attribute;
|
||||||
|
|
||||||
|
const { value, displayableErrors } = fieldStateByAttributeName[attribute.name];
|
||||||
|
|
||||||
|
const formGroupClassName = cx(props.kcFormGroupClass, displayableErrors.length !== 0 && props.kcFormGroupErrorClass);
|
||||||
|
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{BeforeField && <BeforeField attribute={attribute} />}
|
||||||
|
|
||||||
|
<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)}>
|
||||||
|
{(() => {
|
||||||
|
const { options } = attribute.validators;
|
||||||
|
|
||||||
|
if (options !== undefined) {
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
id={attribute.name}
|
||||||
|
name={attribute.name}
|
||||||
|
onChange={onChangeFactory(attribute.name)}
|
||||||
|
onBlur={onBlurFactory(attribute.name)}
|
||||||
|
value={value}
|
||||||
|
>
|
||||||
|
{options.options.map(option => (
|
||||||
|
<option key={option} value={option}>
|
||||||
|
{option}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={(() => {
|
||||||
|
switch (attribute.name) {
|
||||||
|
case "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}
|
||||||
|
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>
|
||||||
|
|
||||||
|
{AfterField && <AfterField attribute={attribute} />}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
@ -25,7 +25,8 @@ export type KcContextBase =
|
|||||||
| KcContextBase.LoginIdpLinkEmail
|
| KcContextBase.LoginIdpLinkEmail
|
||||||
| KcContextBase.LoginPageExpired
|
| KcContextBase.LoginPageExpired
|
||||||
| KcContextBase.LoginConfigTotp
|
| KcContextBase.LoginConfigTotp
|
||||||
| KcContextBase.LogoutConfirm;
|
| KcContextBase.LogoutConfirm
|
||||||
|
| KcContextBase.UpdateUserProfile;
|
||||||
|
|
||||||
export declare namespace KcContextBase {
|
export declare namespace KcContextBase {
|
||||||
export type Common = {
|
export type Common = {
|
||||||
@ -270,6 +271,15 @@ export declare namespace KcContextBase {
|
|||||||
skipLink?: boolean;
|
skipLink?: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UpdateUserProfile = Common & {
|
||||||
|
pageId: "update-user-profile.ftl";
|
||||||
|
profile: {
|
||||||
|
context: "REGISTRATION_PROFILE";
|
||||||
|
attributes: Attribute[];
|
||||||
|
attributesByName: Record<string, Attribute>;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Attribute = {
|
export type Attribute = {
|
||||||
|
@ -7,6 +7,100 @@ import { pathJoin } from "../../../bin/tools/pathJoin";
|
|||||||
|
|
||||||
const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/";
|
const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/";
|
||||||
|
|
||||||
|
const attributes: Attribute[] = [
|
||||||
|
{
|
||||||
|
"validators": {
|
||||||
|
"username-prohibited-characters": {
|
||||||
|
"ignore.empty.value": true
|
||||||
|
},
|
||||||
|
"up-username-has-value": {},
|
||||||
|
"length": {
|
||||||
|
"ignore.empty.value": true,
|
||||||
|
"min": "3",
|
||||||
|
"max": "255"
|
||||||
|
},
|
||||||
|
"up-duplicate-username": {},
|
||||||
|
"up-username-mutation": {}
|
||||||
|
},
|
||||||
|
"displayName": "${username}",
|
||||||
|
"annotations": {},
|
||||||
|
"required": true,
|
||||||
|
"groupAnnotations": {},
|
||||||
|
"autocomplete": "username",
|
||||||
|
"readOnly": false,
|
||||||
|
"name": "username",
|
||||||
|
"value": "xxxx"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"validators": {
|
||||||
|
"up-email-exists-as-username": {},
|
||||||
|
"length": {
|
||||||
|
"max": "255",
|
||||||
|
"ignore.empty.value": true
|
||||||
|
},
|
||||||
|
"up-blank-attribute-value": {
|
||||||
|
"error-message": "missingEmailMessage",
|
||||||
|
"fail-on-null": false
|
||||||
|
},
|
||||||
|
"up-duplicate-email": {},
|
||||||
|
"email": {
|
||||||
|
"ignore.empty.value": true
|
||||||
|
},
|
||||||
|
"pattern": {
|
||||||
|
"ignore.empty.value": true,
|
||||||
|
"pattern": "gmail\\.com$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"displayName": "${email}",
|
||||||
|
"annotations": {},
|
||||||
|
"required": true,
|
||||||
|
"groupAnnotations": {},
|
||||||
|
"autocomplete": "email",
|
||||||
|
"readOnly": false,
|
||||||
|
"name": "email"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"validators": {
|
||||||
|
"length": {
|
||||||
|
"max": "255",
|
||||||
|
"ignore.empty.value": true
|
||||||
|
},
|
||||||
|
"person-name-prohibited-characters": {
|
||||||
|
"ignore.empty.value": true
|
||||||
|
},
|
||||||
|
"up-immutable-attribute": {},
|
||||||
|
"up-attribute-required-by-metadata-value": {}
|
||||||
|
},
|
||||||
|
"displayName": "${firstName}",
|
||||||
|
"annotations": {},
|
||||||
|
"required": true,
|
||||||
|
"groupAnnotations": {},
|
||||||
|
"readOnly": false,
|
||||||
|
"name": "firstName"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"validators": {
|
||||||
|
"length": {
|
||||||
|
"max": "255",
|
||||||
|
"ignore.empty.value": true
|
||||||
|
},
|
||||||
|
"person-name-prohibited-characters": {
|
||||||
|
"ignore.empty.value": true
|
||||||
|
},
|
||||||
|
"up-immutable-attribute": {},
|
||||||
|
"up-attribute-required-by-metadata-value": {}
|
||||||
|
},
|
||||||
|
"displayName": "${lastName}",
|
||||||
|
"annotations": {},
|
||||||
|
"required": true,
|
||||||
|
"groupAnnotations": {},
|
||||||
|
"readOnly": false,
|
||||||
|
"name": "lastName"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const attributesByName = Object.fromEntries(attributes.map(attribute => [attribute.name, attribute])) as any;
|
||||||
|
|
||||||
export const kcContextCommonMock: KcContextBase.Common = {
|
export const kcContextCommonMock: KcContextBase.Common = {
|
||||||
"url": {
|
"url": {
|
||||||
"loginAction": "#",
|
"loginAction": "#",
|
||||||
@ -200,104 +294,8 @@ export const kcContextMocks: KcContextBase[] = [
|
|||||||
...registerCommon,
|
...registerCommon,
|
||||||
"profile": {
|
"profile": {
|
||||||
"context": "REGISTRATION_PROFILE" as const,
|
"context": "REGISTRATION_PROFILE" as const,
|
||||||
...(() => {
|
attributes,
|
||||||
const attributes: Attribute[] = [
|
attributesByName
|
||||||
{
|
|
||||||
"validators": {
|
|
||||||
"username-prohibited-characters": {
|
|
||||||
"ignore.empty.value": true
|
|
||||||
},
|
|
||||||
"up-username-has-value": {},
|
|
||||||
"length": {
|
|
||||||
"ignore.empty.value": true,
|
|
||||||
"min": "3",
|
|
||||||
"max": "255"
|
|
||||||
},
|
|
||||||
"up-duplicate-username": {},
|
|
||||||
"up-username-mutation": {}
|
|
||||||
},
|
|
||||||
"displayName": "${username}",
|
|
||||||
"annotations": {},
|
|
||||||
"required": true,
|
|
||||||
"groupAnnotations": {},
|
|
||||||
"autocomplete": "username",
|
|
||||||
"readOnly": false,
|
|
||||||
"name": "username",
|
|
||||||
"value": "xxxx"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"validators": {
|
|
||||||
"up-email-exists-as-username": {},
|
|
||||||
"length": {
|
|
||||||
"max": "255",
|
|
||||||
"ignore.empty.value": true
|
|
||||||
},
|
|
||||||
"up-blank-attribute-value": {
|
|
||||||
"error-message": "missingEmailMessage",
|
|
||||||
"fail-on-null": false
|
|
||||||
},
|
|
||||||
"up-duplicate-email": {},
|
|
||||||
"email": {
|
|
||||||
"ignore.empty.value": true
|
|
||||||
},
|
|
||||||
"pattern": {
|
|
||||||
"ignore.empty.value": true,
|
|
||||||
"pattern": "gmail\\.com$"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"displayName": "${email}",
|
|
||||||
"annotations": {},
|
|
||||||
"required": true,
|
|
||||||
"groupAnnotations": {},
|
|
||||||
"autocomplete": "email",
|
|
||||||
"readOnly": false,
|
|
||||||
"name": "email"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"validators": {
|
|
||||||
"length": {
|
|
||||||
"max": "255",
|
|
||||||
"ignore.empty.value": true
|
|
||||||
},
|
|
||||||
"person-name-prohibited-characters": {
|
|
||||||
"ignore.empty.value": true
|
|
||||||
},
|
|
||||||
"up-immutable-attribute": {},
|
|
||||||
"up-attribute-required-by-metadata-value": {}
|
|
||||||
},
|
|
||||||
"displayName": "${firstName}",
|
|
||||||
"annotations": {},
|
|
||||||
"required": true,
|
|
||||||
"groupAnnotations": {},
|
|
||||||
"readOnly": false,
|
|
||||||
"name": "firstName"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"validators": {
|
|
||||||
"length": {
|
|
||||||
"max": "255",
|
|
||||||
"ignore.empty.value": true
|
|
||||||
},
|
|
||||||
"person-name-prohibited-characters": {
|
|
||||||
"ignore.empty.value": true
|
|
||||||
},
|
|
||||||
"up-immutable-attribute": {},
|
|
||||||
"up-attribute-required-by-metadata-value": {}
|
|
||||||
},
|
|
||||||
"displayName": "${lastName}",
|
|
||||||
"annotations": {},
|
|
||||||
"required": true,
|
|
||||||
"groupAnnotations": {},
|
|
||||||
"readOnly": false,
|
|
||||||
"name": "lastName"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return {
|
|
||||||
attributes,
|
|
||||||
"attributesByName": Object.fromEntries(attributes.map(attribute => [attribute.name, attribute])) as any
|
|
||||||
} as any;
|
|
||||||
})()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
@ -423,5 +421,14 @@ export const kcContextMocks: KcContextBase[] = [
|
|||||||
"baseUrl": "#"
|
"baseUrl": "#"
|
||||||
},
|
},
|
||||||
"logoutConfirm": { "code": "123", skipLink: false }
|
"logoutConfirm": { "code": "123", skipLink: false }
|
||||||
|
}),
|
||||||
|
id<KcContextBase.UpdateUserProfile>({
|
||||||
|
...kcContextCommonMock,
|
||||||
|
"pageId": "update-user-profile.ftl",
|
||||||
|
"profile": {
|
||||||
|
"context": "REGISTRATION_PROFILE" as const,
|
||||||
|
attributes,
|
||||||
|
attributesByName
|
||||||
|
}
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
@ -310,7 +310,7 @@ export function useFormValidationSlice(params: {
|
|||||||
profile: {
|
profile: {
|
||||||
attributes: Attribute[];
|
attributes: Attribute[];
|
||||||
};
|
};
|
||||||
passwordRequired: boolean;
|
passwordRequired?: boolean;
|
||||||
realm: { registrationEmailAsUsername: boolean };
|
realm: { registrationEmailAsUsername: boolean };
|
||||||
};
|
};
|
||||||
/** NOTE: Try to avoid passing a new ref every render for better performances. */
|
/** NOTE: Try to avoid passing a new ref every render for better performances. */
|
||||||
|
Reference in New Issue
Block a user