import type { JSX } from "keycloakify/tools/JSX"; import { useEffect, Fragment } from "react"; import { assert } from "keycloakify/tools/assert"; import { useIsPasswordRevealed } from "keycloakify/tools/useIsPasswordRevealed"; import type { KcClsx } from "keycloakify/login/lib/kcClsx"; import { useUserProfileForm, getButtonToDisplayForMultivaluedAttributeField, type FormAction, type FormFieldError } from "keycloakify/login/lib/useUserProfileForm"; import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFieldsProps"; import type { Attribute } from "keycloakify/login/KcContext"; import type { KcContext } from "./KcContext"; import type { I18n } from "./i18n"; export default function UserProfileFormFields(props: UserProfileFormFieldsProps) { const { kcContext, i18n, kcClsx, onIsFormSubmittableValueChange, doMakeUserConfirmPassword, 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(({ attribute, displayableErrors, valueOrValues }) => { return ( {BeforeField !== undefined && ( )}
{attribute.required && <> *}
{attribute.annotations.inputHelperTextBefore !== undefined && (
{advancedMsg(attribute.annotations.inputHelperTextBefore)}
)} {attribute.annotations.inputHelperTextAfter !== undefined && (
{advancedMsg(attribute.annotations.inputHelperTextAfter)}
)} {AfterField !== undefined && ( )} {/* NOTE: Downloading of html5DataAnnotations scripts is done in the useUserProfileForm hook */}
); })} ); } function GroupLabel(props: { attribute: Attribute; groupNameRef: { current: string; }; i18n: I18n; kcClsx: KcClsx; }) { const { attribute, groupNameRef, i18n, kcClsx } = props; const { advancedMsg } = i18n; 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; displayableErrors: FormFieldError[]; fieldIndex: number | undefined; kcClsx: KcClsx }) { const { attribute, fieldIndex, kcClsx } = props; const displayableErrors = props.displayableErrors.filter(error => error.fieldIndex === fieldIndex); if (displayableErrors.length === 0) { return null; } return ( {displayableErrors .filter(error => error.fieldIndex === fieldIndex) .map(({ errorMessage }, i, arr) => ( {errorMessage} {arr.length - 1 !== i &&
}
))}
); } type InputFieldByTypeProps = { attribute: Attribute; valueOrValues: string | string[]; displayableErrors: FormFieldError[]; dispatchFormAction: React.Dispatch; i18n: I18n; kcClsx: KcClsx; }; function InputFieldByType(props: InputFieldByTypeProps) { const { attribute, valueOrValues } = props; switch (attribute.annotations.inputType) { case "textarea": return ; case "select": case "multiselect": return ; case "select-radiobuttons": case "multiselect-checkboxes": return ; default: { if (valueOrValues instanceof Array) { return ( <> {valueOrValues.map((...[, i]) => ( ))} ); } const inputNode = ; if (attribute.name === "password" || attribute.name === "password-confirm") { return ( {inputNode} ); } return inputNode; } } } function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: string; children: JSX.Element }) { const { kcClsx, i18n, passwordInputId, children } = props; const { msgStr } = i18n; const { isPasswordRevealed, toggleIsPasswordRevealed } = useIsPasswordRevealed({ passwordInputId }); return (
{children}
); } function InputTag(props: InputFieldByTypeProps & { fieldIndex: number | undefined }) { const { attribute, fieldIndex, kcClsx, dispatchFormAction, valueOrValues, i18n, displayableErrors } = props; const { advancedMsgStr } = i18n; return ( <> { const { inputType } = attribute.annotations; if (inputType?.startsWith("html5-")) { return inputType.slice(6); } return inputType ?? "text"; })()} id={attribute.name} name={attribute.name} value={(() => { if (fieldIndex !== undefined) { assert(valueOrValues instanceof Array); return valueOrValues[fieldIndex]; } assert(typeof valueOrValues === "string"); return valueOrValues; })()} className={kcClsx("kcInputClass")} aria-invalid={displayableErrors.find(error => error.fieldIndex === fieldIndex) !== undefined} disabled={attribute.readOnly} autoComplete={attribute.autocomplete} placeholder={ attribute.annotations.inputTypePlaceholder === undefined ? undefined : advancedMsgStr(attribute.annotations.inputTypePlaceholder) } pattern={attribute.annotations.inputTypePattern} size={attribute.annotations.inputTypeSize === undefined ? undefined : parseInt(`${attribute.annotations.inputTypeSize}`)} maxLength={ attribute.annotations.inputTypeMaxlength === undefined ? undefined : parseInt(`${attribute.annotations.inputTypeMaxlength}`) } minLength={ attribute.annotations.inputTypeMinlength === undefined ? undefined : parseInt(`${attribute.annotations.inputTypeMinlength}`) } max={attribute.annotations.inputTypeMax} min={attribute.annotations.inputTypeMin} step={attribute.annotations.inputTypeStep} {...Object.fromEntries(Object.entries(attribute.html5DataAnnotations ?? {}).map(([key, value]) => [`data-${key}`, value]))} onChange={event => dispatchFormAction({ action: "update", name: attribute.name, valueOrValues: (() => { if (fieldIndex !== undefined) { assert(valueOrValues instanceof Array); return valueOrValues.map((value, i) => { if (i === fieldIndex) { return event.target.value; } return value; }); } return event.target.value; })() }) } onBlur={() => dispatchFormAction({ action: "focus lost", name: attribute.name, fieldIndex: fieldIndex }) } /> {(() => { if (fieldIndex === undefined) { return null; } assert(valueOrValues instanceof Array); const values = valueOrValues; return ( <> ); })()} ); } function AddRemoveButtonsMultiValuedAttribute(props: { attribute: Attribute; values: string[]; fieldIndex: number; dispatchFormAction: React.Dispatch>; i18n: I18n; }) { const { attribute, values, fieldIndex, dispatchFormAction, i18n } = props; const { msg } = i18n; const { hasAdd, hasRemove } = getButtonToDisplayForMultivaluedAttributeField({ attribute, values, fieldIndex }); const idPostfix = `-${attribute.name}-${fieldIndex + 1}`; return ( <> {hasRemove && ( <> {hasAdd ? <> |  : null} )} {hasAdd && ( )} ); } function InputTagSelects(props: InputFieldByTypeProps) { const { attribute, dispatchFormAction, kcClsx, i18n, valueOrValues } = props; const { classDiv, classInput, classLabel, inputType } = (() => { const { inputType } = attribute.annotations; assert(inputType === "select-radiobuttons" || inputType === "multiselect-checkboxes"); switch (inputType) { case "select-radiobuttons": return { inputType: "radio", classDiv: kcClsx("kcInputClassRadio"), classInput: kcClsx("kcInputClassRadioInput"), classLabel: kcClsx("kcInputClassRadioLabel") }; case "multiselect-checkboxes": return { inputType: "checkbox", classDiv: kcClsx("kcInputClassCheckbox"), classInput: kcClsx("kcInputClassCheckboxInput"), classLabel: kcClsx("kcInputClassCheckboxLabel") }; } })(); const options = (() => { walk: { const { inputOptionsFromValidation } = attribute.annotations; if (inputOptionsFromValidation === undefined) { break walk; } const validator = (attribute.validators as Record)[inputOptionsFromValidation]; if (validator === undefined) { break walk; } if (validator.options === undefined) { break walk; } return validator.options; } return attribute.validators.options?.options ?? []; })(); return ( <> {options.map(option => (
dispatchFormAction({ action: "update", name: attribute.name, valueOrValues: (() => { const isChecked = event.target.checked; if (valueOrValues instanceof Array) { const newValues = [...valueOrValues]; if (isChecked) { newValues.push(option); } else { newValues.splice(newValues.indexOf(option), 1); } return newValues; } return event.target.checked ? option : ""; })() }) } onBlur={() => dispatchFormAction({ action: "focus lost", name: attribute.name, fieldIndex: undefined }) } />
))} ); } function TextareaTag(props: InputFieldByTypeProps) { const { attribute, dispatchFormAction, kcClsx, displayableErrors, valueOrValues } = props; assert(typeof valueOrValues === "string"); const value = valueOrValues; return (