import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation"; import type { UserProfileAttribute, UserProfileConfig, } from "@keycloak/keycloak-admin-client/lib/defs/userProfileMetadata"; import { AlertVariant, Button, Form, PageSection, } from "@patternfly/react-core"; import { flatten } from "flat"; import { useMemo, useState } from "react"; import { FormProvider, useForm, useFormContext } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { Link, useNavigate } from "react-router-dom"; import { ScrollForm } from "@keycloak/keycloak-ui-shared"; import { useAdminClient } from "../admin-client"; import { useAlerts } from "../components/alert/Alerts"; import { FixedButtonsGroup } from "../components/form/FixedButtonGroup"; import { ViewHeader } from "../components/view-header/ViewHeader"; import { convertToFormValues } from "../util"; import { useFetch } from "../utils/useFetch"; import { useParams } from "../utils/useParams"; import type { AttributeParams } from "./routes/Attribute"; import { toUserProfile } from "./routes/UserProfile"; import { UserProfileProvider } from "./user-profile/UserProfileContext"; import { AttributeAnnotations } from "./user-profile/attribute/AttributeAnnotations"; import { AttributeGeneralSettings } from "./user-profile/attribute/AttributeGeneralSettings"; import { AttributePermission } from "./user-profile/attribute/AttributePermission"; import { AttributeValidations } from "./user-profile/attribute/AttributeValidations"; import { DEFAULT_LOCALE } from "../i18n/i18n"; import "./realm-settings-section.css"; type TranslationForm = { locale: string; value: string; }; type Translations = { key: string; translations: TranslationForm[]; }; type IndexedAnnotations = { key: string; value?: Record; }; export type IndexedValidations = { key: string; value?: Record; }; type UserProfileAttributeFormFields = Omit< UserProfileAttribute, "validations" | "annotations" > & Attribute & Permission & { validations: IndexedValidations[]; annotations: IndexedAnnotations[]; hasSelector: boolean; hasRequiredScopes: boolean; }; type Attribute = { roles: string[]; scopes: string[]; isRequired: boolean; }; type Permission = { view: PermissionView[]; edit: PermissionEdit[]; }; type PermissionView = [ { adminView: boolean; userView: boolean; }, ]; type PermissionEdit = [ { adminEdit: boolean; userEdit: boolean; }, ]; export const USERNAME_EMAIL = ["username", "email"]; const CreateAttributeFormContent = ({ onHandlingTranslationsData, onHandlingGeneratedDisplayName, save, }: { save: (profileConfig: UserProfileConfig) => void; onHandlingTranslationsData: (translationsData: Translations) => void; onHandlingGeneratedDisplayName: (generatedDisplayName: string) => void; }) => { const { t } = useTranslation(); const form = useFormContext(); const { realm, attributeName } = useParams(); const editMode = attributeName ? true : false; const handleTranslationsData = (translationsData: Translations) => { onHandlingTranslationsData(translationsData); }; const handleGeneratedDisplayName = (generatedDisplayName: string) => { onHandlingGeneratedDisplayName(generatedDisplayName); }; return ( ), }, { title: t("permission"), panel: }, { title: t("validations"), panel: }, { title: t("annotations"), panel: }, ]} />
{t("cancel")}
); }; export default function NewAttributeSettings() { const { adminClient } = useAdminClient(); const { realm: realmName, attributeName } = useParams(); const form = useForm(); const { t } = useTranslation(); const navigate = useNavigate(); const { addAlert, addError } = useAlerts(); const [config, setConfig] = useState(null); const editMode = attributeName ? true : false; const [translationsData, setTranslationsData] = useState({ key: "", translations: [], }); const [generatedDisplayName, setGeneratedDisplayName] = useState(""); const [realm, setRealm] = useState(); const defaultSupportedLocales = useMemo(() => { return realm?.supportedLocales?.length ? realm.supportedLocales : [DEFAULT_LOCALE]; }, [realm]); const defaultLocales = useMemo(() => { return realm?.defaultLocale?.length ? [realm.defaultLocale] : []; }, [realm]); const combinedLocales = useMemo(() => { return Array.from(new Set([...defaultLocales, ...defaultSupportedLocales])); }, [defaultLocales, defaultSupportedLocales]); useFetch( () => adminClient.realms.findOne({ realm: realmName }), (realm) => { if (!realm) { throw new Error(t("notFound")); } setRealm(realm); }, [], ); useFetch( async () => { const translationsToSave: any[] = []; await Promise.all( combinedLocales.map(async (selectedLocale) => { try { const translations = await adminClient.realms.getRealmLocalizationTexts({ realm: realmName, selectedLocale, }); const formData = form.getValues(); const formattedKey = formData.displayName?.substring( 2, formData.displayName.length - 1, ); const filteredTranslations: Array<{ locale: string; value: string; }> = []; const allTranslations = Object.entries(translations).map( ([key, value]) => ({ key, value, }), ); allTranslations.forEach((translation) => { if (translation.key === formattedKey) { filteredTranslations.push({ locale: selectedLocale, value: translation.value, }); } }); const translationToSave: any = { key: formattedKey, translations: filteredTranslations, }; translationsToSave.push(translationToSave); } catch (error) { console.error( `Error fetching translations for ${selectedLocale}:`, error, ); } }), ); return translationsToSave; }, (translationsToSaveData) => { setTranslationsData(() => ({ key: translationsToSaveData[0].key, translations: translationsToSaveData.flatMap( (translationData) => translationData.translations, ), })); }, [combinedLocales], ); useFetch( () => adminClient.users.getProfile(), (config) => { setConfig(config); const { annotations, validations, permissions, selector, required, multivalued, ...values } = config.attributes!.find( (attribute) => attribute.name === attributeName, ) || { permissions: { edit: ["admin"] } }; convertToFormValues( { ...values, hasSelector: typeof selector !== "undefined", hasRequiredScopes: typeof required?.scopes !== "undefined", }, form.setValue, ); Object.entries( flatten({ permissions, selector, required }, { safe: true }), ).map(([key, value]) => form.setValue(key as any, value)); form.setValue( "annotations", Object.entries(annotations || {}).map(([key, value]) => ({ key, value: value as Record, })), ); form.setValue( "validations", Object.entries(validations || {}).map(([key, value]) => ({ key, value: value as Record, })), ); form.setValue("isRequired", required !== undefined); form.setValue("multivalued", multivalued === true); }, [], ); const saveTranslations = async () => { try { const nonEmptyTranslations = translationsData.translations.map( async (translation) => { try { await adminClient.realms.addLocalization( { realm: realmName, selectedLocale: translation.locale, key: translationsData.key, }, translation.value, ); } catch (error) { console.error(`Error saving translation for ${translation.locale}`); } }, ); await Promise.all(nonEmptyTranslations); } catch (error) { console.error(`Error saving translations: ${error}`); } }; const save = async ({ hasSelector, hasRequiredScopes, ...formFields }: UserProfileAttributeFormFields) => { if (!hasSelector) { delete formFields.selector; } if (!hasRequiredScopes) { delete formFields.required?.scopes; } const validations = formFields.validations.reduce( (prevValidations, currentValidations) => { prevValidations[currentValidations.key] = currentValidations.value || {}; return prevValidations; }, {} as Record, ); const annotations = formFields.annotations.reduce( (obj, item) => Object.assign(obj, { [item.key]: item.value }), {}, ); const patchAttributes = () => (config?.attributes || []).map((attribute) => { if (attribute.name !== attributeName) { return attribute; } delete attribute.required; return Object.assign( { ...attribute, name: attributeName, displayName: formFields.displayName!, selector: formFields.selector, permissions: formFields.permissions!, multivalued: formFields.multivalued, annotations, validations, }, formFields.isRequired ? { required: formFields.required } : undefined, formFields.group ? { group: formFields.group } : { group: null }, ); }); const addAttribute = () => (config?.attributes || []).concat([ Object.assign( { name: formFields.name, displayName: formFields.displayName! || generatedDisplayName, required: formFields.isRequired ? formFields.required : undefined, selector: formFields.selector, permissions: formFields.permissions!, multivalued: formFields.multivalued, annotations, validations, }, formFields.isRequired ? { required: formFields.required } : undefined, formFields.group ? { group: formFields.group } : undefined, ), ] as UserProfileAttribute); if (realm?.internationalizationEnabled) { const hasNonEmptyTranslations = translationsData.translations.some( (translation) => translation.value.trim() !== "", ); if (!hasNonEmptyTranslations) { addError("createAttributeError", t("translationError")); return; } } try { const updatedAttributes = editMode ? patchAttributes() : addAttribute(); await adminClient.users.updateProfile({ ...config, attributes: updatedAttributes as UserProfileAttribute[], realm: realmName, }); await saveTranslations(); navigate(toUserProfile({ realm: realmName, tab: "attributes" })); addAlert(t("createAttributeSuccess"), AlertVariant.success); } catch (error) { addError("createAttributeError", error); } }; return ( form.handleSubmit(save)()} onHandlingTranslationsData={setTranslationsData} onHandlingGeneratedDisplayName={setGeneratedDisplayName} /> ); }