Fully retrocompatible, factorized Register page 🚀
This commit is contained in:
parent
96f0e6df2a
commit
f0ffb3fc10
@ -1,8 +1,7 @@
|
||||
import { useEffect, Fragment } from "react";
|
||||
import type { ClassKey } from "keycloakify/login/TemplateProps";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { useUserProfileForm, type KcContextLike, type FormAction, type FormFieldError } from "keycloakify/login/lib/useUserProfileForm";
|
||||
import type { Attribute, LegacyAttribute } from "keycloakify/login/kcContext/KcContext";
|
||||
import type { Attribute } from "keycloakify/login/kcContext/KcContext";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { I18n } from "./i18n";
|
||||
|
||||
@ -49,20 +48,9 @@ export default function UserProfileFormFields(props: UserProfileFormFieldsProps)
|
||||
return (
|
||||
<>
|
||||
{formFieldStates.map(({ attribute, displayableErrors, valueOrValues }) => {
|
||||
const formGroupClassName = clsx(
|
||||
getClassName("kcFormGroupClass"),
|
||||
displayableErrors.length !== 0 && getClassName("kcFormGroupErrorClass")
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment key={attribute.name}>
|
||||
<GroupLabel
|
||||
attribute={attribute}
|
||||
getClassName={getClassName}
|
||||
i18n={i18n}
|
||||
groupNameRef={groupNameRef}
|
||||
formGroupClassName={formGroupClassName}
|
||||
/>
|
||||
<GroupLabel attribute={attribute} getClassName={getClassName} i18n={i18n} groupNameRef={groupNameRef} />
|
||||
{BeforeField !== undefined && (
|
||||
<BeforeField
|
||||
attribute={attribute}
|
||||
@ -73,7 +61,7 @@ export default function UserProfileFormFields(props: UserProfileFormFieldsProps)
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={formGroupClassName}
|
||||
className={getClassName("kcFormGroupClass")}
|
||||
style={{ "display": attribute.name === "password-confirm" && !doMakeUserConfirmPassword ? "none" : undefined }}
|
||||
>
|
||||
<div className={getClassName("kcLabelWrapperClass")}>
|
||||
@ -142,41 +130,11 @@ function GroupLabel(props: {
|
||||
groupNameRef: {
|
||||
current: string;
|
||||
};
|
||||
formGroupClassName: string;
|
||||
}) {
|
||||
const { attribute, getClassName, i18n, groupNameRef, formGroupClassName } = props;
|
||||
const { attribute, getClassName, i18n, groupNameRef } = props;
|
||||
|
||||
const { advancedMsg } = i18n;
|
||||
|
||||
keycloak_prior_to_24: {
|
||||
if (attribute.html5DataAnnotations !== undefined) {
|
||||
break keycloak_prior_to_24;
|
||||
}
|
||||
|
||||
const { group = "", groupDisplayHeader = "", groupDisplayDescription = "" } = attribute as any as LegacyAttribute;
|
||||
|
||||
return (
|
||||
<>
|
||||
{group !== groupNameRef.current && (groupNameRef.current = group) !== "" && (
|
||||
<div className={formGroupClassName}>
|
||||
<div className={getClassName("kcContentWrapperClass")}>
|
||||
<label id={`header-${group}`} className={getClassName("kcFormGroupHeader")}>
|
||||
{advancedMsg(groupDisplayHeader) || groupNameRef.current}
|
||||
</label>
|
||||
</div>
|
||||
{groupDisplayDescription !== "" && (
|
||||
<div className={getClassName("kcLabelWrapperClass")}>
|
||||
<label id={`description-${group}`} className={getClassName("kcLabelClass")}>
|
||||
{advancedMsg(groupDisplayDescription)}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (attribute.group?.name !== groupNameRef.current) {
|
||||
groupNameRef.current = attribute.group?.name ?? "";
|
||||
|
||||
|
@ -193,7 +193,7 @@ export declare namespace KcContext {
|
||||
profile: {
|
||||
attributes: Attribute[];
|
||||
attributesByName: Record<string, Attribute>;
|
||||
html5DataAnnotations: Record<string, string>;
|
||||
html5DataAnnotations?: Record<string, string>;
|
||||
};
|
||||
url: {
|
||||
registrationAction: string;
|
||||
@ -450,8 +450,8 @@ export declare namespace KcContext {
|
||||
export type UpdateUserProfile = Common & {
|
||||
pageId: "update-user-profile.ftl";
|
||||
profile: {
|
||||
attributes: LegacyAttribute[];
|
||||
attributesByName: Record<string, LegacyAttribute>;
|
||||
attributes: Attribute[];
|
||||
attributesByName: Record<string, Attribute>;
|
||||
};
|
||||
};
|
||||
|
||||
@ -459,8 +459,8 @@ export declare namespace KcContext {
|
||||
pageId: "idp-review-user-profile.ftl";
|
||||
profile: {
|
||||
context: "IDP_REVIEW";
|
||||
attributes: LegacyAttribute[];
|
||||
attributesByName: Record<string, LegacyAttribute>;
|
||||
attributes: Attribute[];
|
||||
attributesByName: Record<string, Attribute>;
|
||||
};
|
||||
};
|
||||
|
||||
@ -517,7 +517,7 @@ export type Attribute = {
|
||||
name: string;
|
||||
displayDescription?: string;
|
||||
};
|
||||
html5DataAnnotations: {
|
||||
html5DataAnnotations?: {
|
||||
kcNumberFormat?: string;
|
||||
kcNumberUnFormat?: string;
|
||||
};
|
||||
@ -599,13 +599,6 @@ export type Attribute = {
|
||||
| "photo";
|
||||
};
|
||||
|
||||
export type LegacyAttribute = Omit<Attribute, "group" | "html5DataAnnotations"> & {
|
||||
group: string;
|
||||
groupDisplayHeader?: string;
|
||||
groupDisplayDescription?: string;
|
||||
groupAnnotations: Record<string, string>;
|
||||
};
|
||||
|
||||
export type Validators = Partial<{
|
||||
length: Validators.DoIgnoreEmpty & Validators.Range;
|
||||
integer: Validators.DoIgnoreEmpty & Validators.Range;
|
||||
|
182
src/login/kcContext/register.tsx
Normal file
182
src/login/kcContext/register.tsx
Normal file
@ -0,0 +1,182 @@
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function Register(props: PageProps<Extract<KcContext, { pageId: "register.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
|
||||
const { getClassName } = useGetClassName({
|
||||
doUseDefaultCss,
|
||||
classes
|
||||
});
|
||||
|
||||
const { url, messagesPerField, register, realm, passwordRequired, recaptchaRequired, recaptchaSiteKey } = kcContext;
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
return (
|
||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} headerNode={msg("registerTitle")}>
|
||||
<form id="kc-register-form" className={getClassName("kcFormClass")} action={url.registrationAction} method="post">
|
||||
<div
|
||||
className={clsx(
|
||||
getClassName("kcFormGroupClass"),
|
||||
messagesPerField.printIfExists("firstName", getClassName("kcFormGroupErrorClass"))
|
||||
)}
|
||||
>
|
||||
<div className={getClassName("kcLabelWrapperClass")}>
|
||||
<label htmlFor="firstName" className={getClassName("kcLabelClass")}>
|
||||
{msg("firstName")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={getClassName("kcInputWrapperClass")}>
|
||||
<input
|
||||
type="text"
|
||||
id="firstName"
|
||||
className={getClassName("kcInputClass")}
|
||||
name="firstName"
|
||||
defaultValue={register.formData.firstName ?? ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
getClassName("kcFormGroupClass"),
|
||||
messagesPerField.printIfExists("lastName", getClassName("kcFormGroupErrorClass"))
|
||||
)}
|
||||
>
|
||||
<div className={getClassName("kcLabelWrapperClass")}>
|
||||
<label htmlFor="lastName" className={getClassName("kcLabelClass")}>
|
||||
{msg("lastName")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={getClassName("kcInputWrapperClass")}>
|
||||
<input
|
||||
type="text"
|
||||
id="lastName"
|
||||
className={getClassName("kcInputClass")}
|
||||
name="lastName"
|
||||
defaultValue={register.formData.lastName ?? ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={clsx(getClassName("kcFormGroupClass"), messagesPerField.printIfExists("email", getClassName("kcFormGroupErrorClass")))}
|
||||
>
|
||||
<div className={getClassName("kcLabelWrapperClass")}>
|
||||
<label htmlFor="email" className={getClassName("kcLabelClass")}>
|
||||
{msg("email")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={getClassName("kcInputWrapperClass")}>
|
||||
<input
|
||||
type="text"
|
||||
id="email"
|
||||
className={getClassName("kcInputClass")}
|
||||
name="email"
|
||||
defaultValue={register.formData.email ?? ""}
|
||||
autoComplete="email"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!realm.registrationEmailAsUsername && (
|
||||
<div
|
||||
className={clsx(
|
||||
getClassName("kcFormGroupClass"),
|
||||
messagesPerField.printIfExists("username", getClassName("kcFormGroupErrorClass"))
|
||||
)}
|
||||
>
|
||||
<div className={getClassName("kcLabelWrapperClass")}>
|
||||
<label htmlFor="username" className={getClassName("kcLabelClass")}>
|
||||
{msg("username")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={getClassName("kcInputWrapperClass")}>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
className={getClassName("kcInputClass")}
|
||||
name="username"
|
||||
defaultValue={register.formData.username ?? ""}
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{passwordRequired && (
|
||||
<>
|
||||
<div
|
||||
className={clsx(
|
||||
getClassName("kcFormGroupClass"),
|
||||
messagesPerField.printIfExists("password", getClassName("kcFormGroupErrorClass"))
|
||||
)}
|
||||
>
|
||||
<div className={getClassName("kcLabelWrapperClass")}>
|
||||
<label htmlFor="password" className={getClassName("kcLabelClass")}>
|
||||
{msg("password")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={getClassName("kcInputWrapperClass")}>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
className={getClassName("kcInputClass")}
|
||||
name="password"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
getClassName("kcFormGroupClass"),
|
||||
messagesPerField.printIfExists("password-confirm", getClassName("kcFormGroupErrorClass"))
|
||||
)}
|
||||
>
|
||||
<div className={getClassName("kcLabelWrapperClass")}>
|
||||
<label htmlFor="password-confirm" className={getClassName("kcLabelClass")}>
|
||||
{msg("passwordConfirm")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={getClassName("kcInputWrapperClass")}>
|
||||
<input type="password" id="password-confirm" className={getClassName("kcInputClass")} name="password-confirm" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{recaptchaRequired && (
|
||||
<div className="form-group">
|
||||
<div className={getClassName("kcInputWrapperClass")}>
|
||||
<div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey}></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={getClassName("kcFormGroupClass")}>
|
||||
<div id="kc-form-options" className={getClassName("kcFormOptionsClass")}>
|
||||
<div className={getClassName("kcFormOptionsWrapperClass")}>
|
||||
<span>
|
||||
<a href={url.loginUrl}>{msg("backToLogin")}</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="kc-form-buttons" className={getClassName("kcFormButtonsClass")}>
|
||||
<input
|
||||
className={clsx(
|
||||
getClassName("kcButtonClass"),
|
||||
getClassName("kcButtonPrimaryClass"),
|
||||
getClassName("kcButtonBlockClass"),
|
||||
getClassName("kcButtonLargeClass")
|
||||
)}
|
||||
type="submit"
|
||||
value={msgStr("doRegister")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Template>
|
||||
);
|
||||
}
|
@ -68,7 +68,7 @@ export type KcContextLike = {
|
||||
messagesPerField: Pick<KcContext.Common["messagesPerField"], "existsError" | "get">;
|
||||
profile: {
|
||||
attributes: Attribute[];
|
||||
html5DataAnnotations: Record<string, string>;
|
||||
html5DataAnnotations?: Record<string, string>;
|
||||
};
|
||||
passwordRequired: boolean;
|
||||
realm: { registrationEmailAsUsername: boolean };
|
||||
@ -107,8 +107,7 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
|
||||
|
||||
usePrepareTemplate({
|
||||
"styles": [],
|
||||
// NOTE: The ?? {} is for compat with Keycloak version prior to 24
|
||||
"scripts": Object.keys(kcContext.profile.html5DataAnnotations ?? {})
|
||||
"scripts": Object.keys(kcContext.profile?.html5DataAnnotations ?? {})
|
||||
.filter(key => key !== "kcMultivalued" && key !== "kcNumberFormat") // NOTE: Keycloakify handles it.
|
||||
.map(key => ({
|
||||
"isModule": true,
|
||||
@ -126,7 +125,69 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
|
||||
const attributesWithPassword = useMemo(() => {
|
||||
const attributesWithPassword: Attribute[] = [];
|
||||
|
||||
for (const attribute of kcContext.profile.attributes) {
|
||||
const attributes = (() => {
|
||||
retrocompat_patch: {
|
||||
if ("profile" in kcContext && "attributes" in kcContext.profile && kcContext.profile.attributes.length !== 0) {
|
||||
break retrocompat_patch;
|
||||
}
|
||||
|
||||
kcContext.profile = {
|
||||
"attributes": (["firstName", "lastName", "email", "username"] as const)
|
||||
.filter(name => (name !== "username" ? true : !kcContext.realm.registrationEmailAsUsername))
|
||||
.map(name =>
|
||||
id<Attribute>({
|
||||
"name": name,
|
||||
"displayName": id<`\${${MessageKey}}`>(`\${${name}}`),
|
||||
"required": true,
|
||||
"value": (kcContext as any).register.formData[name] ?? "",
|
||||
"html5DataAnnotations": {},
|
||||
"readOnly": false,
|
||||
"validators": {},
|
||||
"annotations": {},
|
||||
"autocomplete": (() => {
|
||||
switch (name) {
|
||||
case "email":
|
||||
return "email";
|
||||
case "username":
|
||||
return "username";
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
})()
|
||||
})
|
||||
),
|
||||
"html5DataAnnotations": {}
|
||||
};
|
||||
}
|
||||
|
||||
return kcContext.profile.attributes;
|
||||
})();
|
||||
|
||||
for (const attribute_pre_group_patch of attributes) {
|
||||
const attribute = (() => {
|
||||
if (typeof attribute_pre_group_patch.group === "string" && attribute_pre_group_patch.group !== "") {
|
||||
const { group, groupDisplayHeader, groupDisplayDescription, groupAnnotations, ...rest } =
|
||||
attribute_pre_group_patch as Attribute & {
|
||||
group: string;
|
||||
groupDisplayHeader?: string;
|
||||
groupDisplayDescription?: string;
|
||||
groupAnnotations: Record<string, string>;
|
||||
};
|
||||
|
||||
return id<Attribute>({
|
||||
...rest,
|
||||
"group": {
|
||||
"name": group,
|
||||
"displayHeader": groupDisplayHeader,
|
||||
"displayDescription": groupDisplayDescription,
|
||||
"html5DataAnnotations": {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return attribute_pre_group_patch;
|
||||
})();
|
||||
|
||||
attributesWithPassword.push(attribute);
|
||||
|
||||
add_password_and_password_confirm: {
|
||||
@ -191,7 +252,6 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
|
||||
apply_formatters: {
|
||||
const { attribute } = formFieldState;
|
||||
|
||||
// NOTE: The `?? {}` is for compat with Keycloak version prior to 24
|
||||
const { kcNumberFormat } = attribute.html5DataAnnotations ?? {};
|
||||
|
||||
if (kcNumberFormat === undefined) {
|
||||
@ -407,7 +467,6 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
|
||||
let { valueOrValues } = formFieldState;
|
||||
|
||||
unFormat_number: {
|
||||
// NOTE: The `?? {}` is for compat with Keycloak version prior to 24
|
||||
const { kcNumberUnFormat } = attribute.html5DataAnnotations ?? {};
|
||||
|
||||
if (kcNumberUnFormat === undefined) {
|
||||
@ -791,7 +850,6 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
|
||||
assert(typeof valueOrValues === "string");
|
||||
|
||||
unFormat_number: {
|
||||
// NOTE: The `?? {}` is for compat with Keycloak version prior to 24
|
||||
const { kcNumberUnFormat } = attribute.html5DataAnnotations ?? {};
|
||||
|
||||
if (kcNumberUnFormat === undefined) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user