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,168 +130,137 @@ 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 = (() => { if (
const syntheticAttributes: Attribute[] = []; "profile" in kcContext &&
"attributesByName" in kcContext.profile &&
Object.keys(kcContext.profile.attributesByName).length !== 0
) {
break mock_user_profile_attributes_for_older_keycloak_versions;
}
const attributes = (() => { if ("register" in kcContext && kcContext.register instanceof Object && "formData" in kcContext.register) {
retrocompat_patch: { //NOTE: Handle legacy register.ftl page
if ( return (["firstName", "lastName", "email", "username"] as const)
"profile" in kcContext && .filter(name => (name !== "username" ? true : !kcContext.realm.registrationEmailAsUsername))
"attributesByName" in kcContext.profile && .map(name =>
Object.keys(kcContext.profile.attributesByName).length !== 0
) {
break retrocompat_patch;
}
if ("register" in kcContext && kcContext.register instanceof Object && "formData" in kcContext.register) {
//NOTE: Handle legacy register.ftl page
return (["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.register as any).formData[name] ?? "",
html5DataAnnotations: {},
readOnly: false,
validators: {},
annotations: {},
autocomplete: (() => {
switch (name) {
case "email":
return "email";
case "username":
return "username";
default:
return undefined;
}
})()
})
);
}
if ("user" in kcContext && kcContext.user instanceof Object) {
//NOTE: Handle legacy login-update-profile.ftl
return (["username", "email", "firstName", "lastName"] as const)
.filter(name => (name !== "username" ? true : (kcContext.user as any).editUsernameAllowed))
.map(name =>
id<Attribute>({
name: name,
displayName: id<`\${${MessageKey}}`>(`\${${name}}`),
required: true,
value: (kcContext as any).user[name] ?? "",
html5DataAnnotations: {},
readOnly: false,
validators: {},
annotations: {},
autocomplete: (() => {
switch (name) {
case "email":
return "email";
case "username":
return "username";
default:
return undefined;
}
})()
})
);
}
if ("email" in kcContext && kcContext.email instanceof Object) {
//NOTE: Handle legacy update-email.ftl
return [
id<Attribute>({ id<Attribute>({
name: "email", name: name,
displayName: id<`\${${MessageKey}}`>(`\${email}`), displayName: id<`\${${MessageKey}}`>(`\${${name}}`),
required: true, required: true,
value: (kcContext.email as any).value ?? "", value: (kcContext.register as any).formData[name] ?? "",
html5DataAnnotations: {}, html5DataAnnotations: {},
readOnly: false, readOnly: false,
validators: {}, validators: {},
annotations: {}, annotations: {},
autocomplete: "email" autocomplete: (() => {
switch (name) {
case "email":
return "email";
case "username":
return "username";
default:
return undefined;
}
})()
}) })
]; );
}
assert(false, "Unable to mock user profile from the current kcContext");
} }
return Object.values(kcContext.profile.attributesByName).map(attribute_pre_group_patch => { if ("user" in kcContext && kcContext.user instanceof Object) {
if (typeof attribute_pre_group_patch.group === "string" && attribute_pre_group_patch.group !== "") { //NOTE: Handle legacy login-update-profile.ftl
const { group, groupDisplayHeader, groupDisplayDescription, groupAnnotations, ...rest } = return (["username", "email", "firstName", "lastName"] as const)
attribute_pre_group_patch as Attribute & { .filter(name => (name !== "username" ? true : (kcContext.user as any).editUsernameAllowed))
group: string; .map(name =>
groupDisplayHeader?: string; id<Attribute>({
groupDisplayDescription?: string; name: name,
groupAnnotations: Record<string, string>; displayName: id<`\${${MessageKey}}`>(`\${${name}}`),
}; required: true,
value: (kcContext as any).user[name] ?? "",
html5DataAnnotations: {},
readOnly: false,
validators: {},
annotations: {},
autocomplete: (() => {
switch (name) {
case "email":
return "email";
case "username":
return "username";
default:
return undefined;
}
})()
})
);
}
return id<Attribute>({ if ("email" in kcContext && kcContext.email instanceof Object) {
...rest, //NOTE: Handle legacy update-email.ftl
group: { return [
name: group, id<Attribute>({
displayHeader: groupDisplayHeader, name: "email",
displayDescription: groupDisplayDescription, displayName: id<`\${${MessageKey}}`>(`\${email}`),
html5DataAnnotations: {}
}
});
}
return attribute_pre_group_patch;
});
})();
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")) {
// 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, required: true,
value: (kcContext.email as any).value ?? "",
html5DataAnnotations: {},
readOnly: false, readOnly: false,
validators: {}, validators: {},
annotations: {}, annotations: {},
autocomplete: "new-password", autocomplete: "email"
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 {})
}
);
} }
assert(false, "Unable to mock user profile from the current kcContext");
} }
// NOTE: Consistency patch return Object.values(kcContext.profile.attributesByName).map(structuredCloneButFunctions);
syntheticAttributes.forEach(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;
groupDisplayHeader?: string;
groupDisplayDescription?: string;
groupAnnotations: Record<string, string>;
};
delete attribute.group;
// @ts-expect-error
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,
displayHeader: groupDisplayHeader,
displayDescription: groupDisplayDescription,
html5DataAnnotations: {}
};
}
// Attributes with options rendered by default as select inputs
if (attribute.validators.options !== undefined && attribute.annotations.inputType === undefined) {
attribute.annotations.inputType = "select";
}
// Consistency patch on values/value property
{
if (getIsMultivaluedSingleField({ attribute })) { if (getIsMultivaluedSingleField({ attribute })) {
attribute.multivalued = true; attribute.multivalued = true;
} }
@ -303,65 +272,98 @@ 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 = (() => {
const out: {
attribute: Attribute;
valueOrValues: string | string[];
}[] = [];
for (const attribute of syntheticAttributes) {
handle_multi_valued_attribute: {
if (!attribute.multivalued) {
break handle_multi_valued_attribute;
}
const values = attribute.values?.length ? attribute.values : [""];
apply_validator_min_range: {
if (getIsMultivaluedSingleField({ attribute })) {
break apply_validator_min_range;
}
const validator = attribute.validators.multivalued;
if (validator === undefined) {
break apply_validator_min_range;
}
const { min: minStr } = validator;
if (!minStr) {
break apply_validator_min_range;
}
const min = parseInt(`${minStr}`);
for (let index = values.length; index < min; index++) {
values.push("");
}
}
out.push({
attribute,
valueOrValues: values
});
continue;
}
out.push({
attribute,
valueOrValues: attribute.value ?? ""
});
} }
return out; attributes.forEach((attribute, i) => {
})(); 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;
valueOrValues: string | string[];
}[] = [];
for (const attribute of attributes) {
handle_multi_valued_attribute: {
if (!attribute.multivalued) {
break handle_multi_valued_attribute;
}
const values = attribute.values?.length ? attribute.values : [""];
apply_validator_min_range: {
if (getIsMultivaluedSingleField({ attribute })) {
break apply_validator_min_range;
}
const validator = attribute.validators.multivalued;
if (validator === undefined) {
break apply_validator_min_range;
}
const { min: minStr } = validator;
if (!minStr) {
break apply_validator_min_range;
}
const min = parseInt(`${minStr}`);
for (let index = values.length; index < min; index++) {
values.push("");
}
}
initialFormFieldState.push({
attribute,
valueOrValues: values
});
continue;
}
initialFormFieldState.push({
attribute,
valueOrValues: attribute.value ?? ""
});
}
const initialState: internal.State = { const initialState: internal.State = {
formFieldStates: initialFormFieldState.map(({ attribute, valueOrValues }) => ({ formFieldStates: initialFormFieldState.map(({ attribute, valueOrValues }) => ({