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, FormFieldState } from "keycloakify/login/lib/useUserProfileForm"; import type { Attribute, LegacyAttribute } from "keycloakify/login/kcContext/KcContext"; import type { I18n } from "../../i18n"; import { assert } from "tsafe/assert"; export type UserProfileFormFieldsProps = { kcContext: KcContextLike; i18n: I18n; getClassName: (classKey: ClassKey) => string; onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void; BeforeField?: (props: BeforeAfterFieldProps) => JSX.Element | null; AfterField?: (props: BeforeAfterFieldProps) => JSX.Element | null; }; type BeforeAfterFieldProps = { attribute: Attribute; index: number; value: string; dispatchFormAction: React.Dispatch; formFieldErrors: FormFieldError[]; i18n: I18n; }; // NOTE: Enabled by default but it's a UX best practice to set it to false. const doMakeUserConfirmPassword = true; export function UserProfileFormFields(props: UserProfileFormFieldsProps) { const { kcContext, onIsFormSubmittableValueChange, i18n, getClassName, BeforeField, AfterField } = props; const { advancedMsg } = i18n; const { formState: { formFieldStates, isFormSubmittable }, dispatchFormAction } = useUserProfileForm({ kcContext, i18n, doMakeUserConfirmPassword }); useEffect(() => { onIsFormSubmittableValueChange(isFormSubmittable); }, [isFormSubmittable]); const groupNameRef = { "current": "" }; return ( <> {formFieldStates.map(({ index, value, attribute, displayableErrors }) => { const formGroupClassName = clsx( getClassName("kcFormGroupClass"), displayableErrors.length !== 0 && getClassName("kcFormGroupErrorClass") ); return ( {BeforeField !== undefined && ( )}
{attribute.required && <>*}
{attribute.annotations.inputHelperTextBefore !== undefined && index === 0 && (
{advancedMsg(attribute.annotations.inputHelperTextBefore)}
)} {attribute.multivalued && ( )} {displayableErrors.length !== 0 && ( )} {attribute.annotations.inputHelperTextAfter !== undefined && index === 0 && (
{advancedMsg(attribute.annotations.inputHelperTextAfter)}
)} {AfterField !== undefined && ( )} {/* TODO: <#list profile.html5DataAnnotations?keys as key> */}
); })} ); } function GroupLabel(props: { attribute: Attribute; getClassName: UserProfileFormFieldsProps["getClassName"]; i18n: I18n; groupNameRef: { current: string; }; formGroupClassName: string; }) { const { attribute, getClassName, i18n, groupNameRef, formGroupClassName } = 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) !== "" && (
{groupDisplayDescription !== "" && (
)}
)} ); } if (attribute.group?.name !== groupNameRef.current) { groupNameRef.current = attribute.group?.name ?? ""; if (groupNameRef.current !== "") { assert(attribute.group !== undefined); return (
[`data-${key}`, value]))} > {(() => { const groupDisplayHeader = attribute.group.displayHeader ?? ""; const groupHeaderText = groupDisplayHeader !== "" ? advancedMsg(groupDisplayHeader) : attribute.group.name; return (
); })()} {(() => { const groupDisplayDescription = attribute.group.displayDescription ?? ""; if (groupDisplayDescription !== "") { const groupDescriptionText = advancedMsg(groupDisplayDescription); return (
); } return null; })()}
); } } return null; } function FieldErrors(props: { attribute: Attribute; index: number; getClassName: UserProfileFormFieldsProps["getClassName"]; displayableErrors: FormFieldError[]; }) { const { attribute, index, getClassName, displayableErrors } = props; return ( {displayableErrors.map(({ errorMessage }, i, arr) => ( <> {errorMessage} {arr.length - 1 !== i &&
} ))}
); } function AddRemoveButtonsMultiValuedAttribute(props: { formFieldStates: FormFieldState[]; attribute: Attribute; index: number; dispatchFormAction: React.Dispatch< Extract >; i18n: I18n; }) { const { formFieldStates, attribute, index, dispatchFormAction, i18n } = props; const { msg } = i18n; const currentCount = formFieldStates.filter(({ attribute: attribute_i }) => attribute_i.name === attribute.name).length; const hasRemove = (() => { if (currentCount === 1) { return false; } const minCount = (() => { const { multivalued } = attribute.validators; if (multivalued === undefined) { return undefined; } const minStr = multivalued.min; if (minStr === undefined) { return undefined; } return parseInt(minStr); })(); if (minCount === undefined) { return true; } if (currentCount === minCount) { return false; } return true; })(); const hasAdd = (() => { if (index + 1 !== currentCount) { return false; } const maxCount = (() => { const { multivalued } = attribute.validators; if (multivalued === undefined) { return undefined; } const maxStr = multivalued.max; if (maxStr === undefined) { return undefined; } return parseInt(maxStr); })(); if (maxCount === undefined) { return false; } if (currentCount === maxCount) { return false; } return true; })(); return ( <> {hasRemove && ( )} {hasAdd && ( )} ); } type PropsOfInputFiledByType = { attribute: Attribute; index: number; value: string; displayableErrors: FormFieldError[]; formValidationDispatch: React.Dispatch; getClassName: UserProfileFormFieldsProps["getClassName"]; i18n: I18n; }; function InputFiledByType(props: PropsOfInputFiledByType) { const { attribute } = props; /* <#macro inputFieldByType attribute> <#switch attribute.annotations.inputType!''> <#case 'textarea'> <@textareaTag attribute=attribute/> <#break> <#case 'select'> <#case 'multiselect'> <@selectTag attribute=attribute/> <#break> <#case 'select-radiobuttons'> <#case 'multiselect-checkboxes'> <@inputTagSelects attribute=attribute/> <#break> <#default> <#if attribute.multivalued && attribute.values?has_content> <#list attribute.values as value> <@inputTag attribute=attribute value=value!''/> <#else> <@inputTag attribute=attribute value=attribute.value!''/> */ switch (attribute.annotations.inputType) { case "textarea": return ; case "select": case "multiselect": return ; case "select-radiobuttons": case "multiselect-checkboxes": return ; default: return ; } } function TextareaTag(props: PropsOfInputFiledByType) { const { attribute, index, value, formValidationDispatch, getClassName, displayableErrors } = props; return (