Refactor + attributes with options rendered by default as select inputs

This commit is contained in:
Joseph Garrone
2024-06-11 09:22:50 +02:00
parent 9a92054c1a
commit 287dd9bd31

View File

@ -130,20 +130,15 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
const initialState = useMemo((): internal.State => { const initialState = useMemo((): internal.State => {
// NOTE: We don't use te kcContext.profile.attributes directly because // NOTE: We don't use te kcContext.profile.attributes directly because
// they don't includes the password and password confirm fields and we want to add them. // they don't includes the password and password confirm fields and we want to add them.
// Also, we want to polyfill the attributes for older Keycloak version before User Profile was introduced. // We also want to apply some retro-compatibility and consistency patches.
// Finally we want to patch the changes made by Keycloak on the attributes format so we have an homogeneous const attributes: Attribute[] = (() => {
// attributes format to work with. mock_user_profile_attributes_for_older_keycloak_versions: {
const syntheticAttributes = (() => {
const syntheticAttributes: Attribute[] = [];
const attributes = (() => {
retrocompat_patch: {
if ( if (
"profile" in kcContext && "profile" in kcContext &&
"attributesByName" in kcContext.profile && "attributesByName" in kcContext.profile &&
Object.keys(kcContext.profile.attributesByName).length !== 0 Object.keys(kcContext.profile.attributesByName).length !== 0
) { ) {
break retrocompat_patch; break mock_user_profile_attributes_for_older_keycloak_versions;
} }
if ("register" in kcContext && kcContext.register instanceof Object && "formData" in kcContext.register) { if ("register" in kcContext && kcContext.register instanceof Object && "formData" in kcContext.register) {
@ -222,76 +217,50 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
assert(false, "Unable to mock user profile from the current kcContext"); assert(false, "Unable to mock user profile from the current kcContext");
} }
return Object.values(kcContext.profile.attributesByName).map(attribute_pre_group_patch => { return Object.values(kcContext.profile.attributesByName).map(structuredCloneButFunctions);
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 & { // Retro-compatibility and consistency patches
attributes.forEach(attribute => {
patch_legacy_group: {
if (typeof attribute.group !== "string") {
break patch_legacy_group;
}
const { group, groupDisplayHeader, groupDisplayDescription /*, groupAnnotations*/ } = attribute as Attribute & {
group: string; group: string;
groupDisplayHeader?: string; groupDisplayHeader?: string;
groupDisplayDescription?: string; groupDisplayDescription?: string;
groupAnnotations: Record<string, string>; groupAnnotations: Record<string, string>;
}; };
return id<Attribute>({ delete attribute.group;
...rest, // @ts-expect-error
group: { delete attribute.groupDisplayHeader;
// @ts-expect-error
delete attribute.groupDisplayDescription;
// @ts-expect-error
delete attribute.groupAnnotations;
if (group === "") {
break patch_legacy_group;
}
attribute.group = {
name: group, name: group,
displayHeader: groupDisplayHeader, displayHeader: groupDisplayHeader,
displayDescription: groupDisplayDescription, displayDescription: groupDisplayDescription,
html5DataAnnotations: {} html5DataAnnotations: {}
} };
});
} }
return attribute_pre_group_patch; // Attributes with options rendered by default as select inputs
}); if (attribute.validators.options !== undefined && attribute.annotations.inputType === undefined) {
})(); attribute.annotations.inputType = "select";
for (const attribute of attributes) {
syntheticAttributes.push(structuredCloneButFunctions(attribute));
add_password_and_password_confirm: {
if (!kcContext.passwordRequired) {
break add_password_and_password_confirm;
} }
if (attribute.name !== (kcContext.realm.registrationEmailAsUsername ? "email" : "username")) { // Consistency patch on values/value property
// NOTE: We want to add password and password-confirm after the field that identifies the user.
// It's either email or username.
break add_password_and_password_confirm;
}
syntheticAttributes.push(
{ {
name: "password",
displayName: id<`\${${MessageKey}}`>("${password}"),
required: true,
readOnly: false,
validators: {},
annotations: {},
autocomplete: "new-password",
html5DataAnnotations: {},
// NOTE: Compat with Keycloak version prior to 24
...({ groupAnnotations: {} } as {})
},
{
name: "password-confirm",
displayName: id<`\${${MessageKey}}`>("${passwordConfirm}"),
required: true,
readOnly: false,
validators: {},
annotations: {},
html5DataAnnotations: {},
autocomplete: "new-password",
// NOTE: Compat with Keycloak version prior to 24
...({ groupAnnotations: {} } as {})
}
);
}
}
// NOTE: Consistency patch
syntheticAttributes.forEach(attribute => {
if (getIsMultivaluedSingleField({ attribute })) { if (getIsMultivaluedSingleField({ attribute })) {
attribute.multivalued = true; attribute.multivalued = true;
} }
@ -303,18 +272,54 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
attribute.value ??= attribute.values?.[0]; attribute.value ??= attribute.values?.[0];
delete attribute.values; delete attribute.values;
} }
}
}); });
return syntheticAttributes; add_password_and_password_confirm: {
})(); if (!kcContext.passwordRequired) {
break add_password_and_password_confirm;
}
const initialFormFieldState = (() => { attributes.forEach((attribute, i) => {
const out: { if (attribute.name !== (kcContext.realm.registrationEmailAsUsername ? "email" : "username")) {
// NOTE: We want to add password and password-confirm after the field that identifies the user.
// It's either email or username.
return;
}
attributes.splice(
i + 1,
0,
{
name: "password",
displayName: id<`\${${MessageKey}}`>("${password}"),
required: true,
readOnly: false,
validators: {},
annotations: {},
autocomplete: "new-password",
html5DataAnnotations: {}
},
{
name: "password-confirm",
displayName: id<`\${${MessageKey}}`>("${passwordConfirm}"),
required: true,
readOnly: false,
validators: {},
annotations: {},
html5DataAnnotations: {},
autocomplete: "new-password"
}
);
});
}
const initialFormFieldState: {
attribute: Attribute; attribute: Attribute;
valueOrValues: string | string[]; valueOrValues: string | string[];
}[] = []; }[] = [];
for (const attribute of syntheticAttributes) { for (const attribute of attributes) {
handle_multi_valued_attribute: { handle_multi_valued_attribute: {
if (!attribute.multivalued) { if (!attribute.multivalued) {
break handle_multi_valued_attribute; break handle_multi_valued_attribute;
@ -346,7 +351,7 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
} }
} }
out.push({ initialFormFieldState.push({
attribute, attribute,
valueOrValues: values valueOrValues: values
}); });
@ -354,15 +359,12 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
continue; continue;
} }
out.push({ initialFormFieldState.push({
attribute, attribute,
valueOrValues: attribute.value ?? "" valueOrValues: attribute.value ?? ""
}); });
} }
return out;
})();
const initialState: internal.State = { const initialState: internal.State = {
formFieldStates: initialFormFieldState.map(({ attribute, valueOrValues }) => ({ formFieldStates: initialFormFieldState.map(({ attribute, valueOrValues }) => ({
attribute, attribute,