diff --git a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/realm_settings/UserProfile.ts b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/realm_settings/UserProfile.ts index eba3825527d..bdfcb1724a1 100644 --- a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/realm_settings/UserProfile.ts +++ b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/realm_settings/UserProfile.ts @@ -14,8 +14,8 @@ export default class UserProfile { #deleteDrpDwnOption = "deleteDropdownAttributeItem"; #editDrpDwnOption = "editDropdownAttributeItem"; #cancelNewAttribute = "attribute-cancel"; - #newAttributeNameInput = "attribute-name"; - #newAttributeDisplayNameInput = "attribute-display-name"; + #newAttributeNameInput = "name"; + #newAttributeDisplayNameInput = "attributes-displayName"; #newAttributeEnabledWhen = 'input[name="enabledWhen"]'; #newAttributeEmptyValidators = ".kc-emptyValidators"; #newAttributeAnnotationBtn = "annotations-add-row"; diff --git a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties index 635733f10b2..59b614fe7e2 100644 --- a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties +++ b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties @@ -458,7 +458,7 @@ noTokens=No initial access tokens addMapper=Add mapper webauthnPolicy=Webauthn Policy userAttributeName=User attribute name to store SAML attribute. Use email, lastName, and firstName to map to those predefined user properties. -displayDescriptionField=Display description +displayDescription=Display description eventTypes.DELETE_ACCOUNT.description=Delete account eventTypes.RESTART_AUTHENTICATION_ERROR.description=Restart authentication error evictionHour=Eviction hour @@ -871,6 +871,7 @@ testAuthentication=Test authentication groupNameLdapAttributeHelp=Name of LDAP attribute, which is used in group objects for name and RDN of group. Usually it will be 'cn'. In this case typical group/role object may have DN like 'cn\=Group1,ouu\=groups,dc\=example,dc\=org'. deleteError=Could not delete the provider {{error}} attributeDisplayName=Display name +displayName=Display name pkceEnabled=Use PKCE userProviderSaveSuccess=User federation provider successfully saved month=Month @@ -1472,7 +1473,7 @@ roleHelpHelp=Role to grant to user. Click 'Select Role' button to browse roles, storedTokensReadable=Stored tokens readable defaultRoleDeleteError=You cannot delete a default role. unknownUser=Anonymous -displayHeaderField=Display name +displayHeader=Display name userVerify.not\ specified=Not specified usermodel.prop.label=Property userFedUnlinkUsersConfirm=Do you want to unlink all the users? Any users without a password in the database will not be able to authenticate anymore. @@ -3121,14 +3122,12 @@ sendIdTokenOnLogoutHelp=If the 'id_token_hint' parameter should be sent in logou sendClientIdOnLogout=Send 'client_id' in logout requests sendClientIdOnLogoutHelp=If the 'client_id' parameter should be sent in logout requests. addAttributeTranslationBtn=Add translation button -addAttributeTranslationInfo=Add translations for this field using the icon next to the "Display name" field. -addAttributeDisplayNameTranslation=Add translation for the display name +addAttributeTranslationInfo=Add translations for this field using the icon next to the "{{fieldName}}" field. +addAttributeTranslation=Add translation for the "{{fieldName}}" field addAttributeDisplayDescriptionTranslation=Add translation for the display description addTranslationsModalTitle=Add translations -addTranslationsModalSubTitle=You are able to translate the "Display name" based on your locale or preferred languages. In addition, you are also able to create or edit the "Display name" translations in the +addTranslationsModalSubTitle=You are able to translate the "{{fieldName}}" based on your locale or preferred languages. In addition, you are also able to create or edit the "{{fieldName}}" translations in the addTranslationsModalSubTitleBolded=Realm settings > Localization > Realm overrides. -addTranslationsModalSubTitleDescription=You are able to translate the "Display description" based on your locale or preferred languages. In addition, you are also able to create or edit the "Display description" translations in the -addAttributesGroupTranslationInfo=Add translations for this field using the icon next to the "Display name" field. translationKey=Key translationsTableHeading=Translations searchForLanguage=Search for language diff --git a/js/apps/admin-ui/src/realm-settings/NewAttributeSettings.tsx b/js/apps/admin-ui/src/realm-settings/NewAttributeSettings.tsx index 7564a5d158d..baa684c3948 100644 --- a/js/apps/admin-ui/src/realm-settings/NewAttributeSettings.tsx +++ b/js/apps/admin-ui/src/realm-settings/NewAttributeSettings.tsx @@ -18,29 +18,22 @@ import { Link, useNavigate } from "react-router-dom"; import { useAdminClient } from "../admin-client"; import { FixedButtonsGroup } from "../components/form/FixedButtonGroup"; import { ViewHeader } from "../components/view-header/ViewHeader"; -import { useRealm } from "../context/realm-context/RealmContext"; -import { i18n } from "../i18n/i18n"; import { convertToFormValues } from "../util"; -import useLocale from "../utils/useLocale"; import { useParams } from "../utils/useParams"; -import "./realm-settings-section.css"; +import { TranslationForm } from "./AddTranslationModal"; import type { AttributeParams } from "./routes/Attribute"; import { toUserProfile } from "./routes/UserProfile"; import { UserProfileProvider } from "./user-profile/UserProfileContext"; +import { + saveTranslations, + Translations, +} from "./user-profile/attribute/TranslatableField"; 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"; -type TranslationForm = { - locale: string; - value: string; -}; - -type Translations = { - key: string; - translations: TranslationForm[]; -}; +import "./realm-settings-section.css"; type IndexedAnnotations = { key: string; @@ -56,6 +49,7 @@ type UserProfileAttributeFormFields = Omit< UserProfileAttribute, "validations" | "annotations" > & + Translations & Attribute & Permission & { validations: IndexedValidations[]; @@ -93,41 +87,21 @@ type PermissionEdit = [ 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("generalSettings"), panel: }, { title: t("permission"), panel: }, { title: t("validations"), panel: }, { title: t("annotations"), panel: }, @@ -158,79 +132,12 @@ const CreateAttributeFormContent = ({ export default function NewAttributeSettings() { const { adminClient } = useAdminClient(); const { realm: realmName, attributeName } = useParams(); - const { realmRepresentation: realm } = useRealm(); const form = useForm(); const { t } = useTranslation(); - const combinedLocales = useLocale(); 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(""); - - useFetch( - async () => { - const translationsToSave: Translations[] = []; - - 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: TranslationForm[] = Object.entries( - translations, - ) - .filter(([key]) => key === formattedKey) - .map(([_, value]) => ({ - locale: selectedLocale, - value, - })); - - if (filteredTranslations.length > 0) { - translationsToSave.push({ - key: formattedKey, - translations: filteredTranslations, - }); - } - } catch (error) { - addError("errorSavingTranslations", error); - } - }), - ); - - return translationsToSave; - }, - (translationsToSave) => { - if (translationsToSave && translationsToSave.length > 0) { - const allTranslations = translationsToSave.flatMap( - (translation) => translation.translations, - ); - - setTranslationsData({ - key: translationsToSave[0].key, - translations: allTranslations, - }); - - form.setValue("translations", allTranslations); - } - }, - [combinedLocales, realmName, form], - ); useFetch( () => adminClient.users.getProfile(), @@ -278,31 +185,6 @@ export default function NewAttributeSettings() { [], ); - const saveTranslations = async () => { - try { - const nonEmptyTranslations = translationsData.translations - .filter((translation) => translation.value.trim() !== "") - .map(async (translation) => { - try { - await adminClient.realms.addLocalization( - { - realm: realmName, - selectedLocale: translation.locale, - key: translationsData.key, - }, - translation.value, - ); - } catch (error) { - addError(t("errorSavingTranslations"), error); - } - }); - - await Promise.all(nonEmptyTranslations); - } catch (error) { - console.error(`Error saving translations: ${error}`); - } - }; - const save = async ({ hasSelector, hasRequiredScopes, @@ -358,7 +240,7 @@ export default function NewAttributeSettings() { Object.assign( { name: formFields.name, - displayName: formFields.displayName! || generatedDisplayName, + displayName: formFields.displayName!, required: formFields.isRequired ? formFields.required : undefined, selector: formFields.selector, permissions: formFields.permissions!, @@ -371,17 +253,6 @@ export default function NewAttributeSettings() { ), ] 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(); @@ -391,8 +262,19 @@ export default function NewAttributeSettings() { realm: realmName, }); - await saveTranslations(); - i18n.reloadResources(); + if (formFields.translation) { + try { + await saveTranslations({ + adminClient, + realmName, + translationsData: { + translation: formFields.translation, + }, + }); + } catch (error) { + addError(t("errorSavingTranslations"), error); + } + } navigate(toUserProfile({ realm: realmName, tab: "attributes" })); addAlert(t("createAttributeSuccess"), AlertVariant.success); @@ -408,11 +290,7 @@ export default function NewAttributeSettings() { subKey={editMode ? "" : t("createAttributeSubTitle")} /> - form.handleSubmit(save)()} - onHandlingTranslationsData={setTranslationsData} - onHandlingGeneratedDisplayName={setGeneratedDisplayName} - /> + form.handleSubmit(save)()} /> ); diff --git a/js/apps/admin-ui/src/realm-settings/user-profile/AttributesGroupForm.tsx b/js/apps/admin-ui/src/realm-settings/user-profile/AttributesGroupForm.tsx index 7642fa6ccb1..da8e82ca0e3 100644 --- a/js/apps/admin-ui/src/realm-settings/user-profile/AttributesGroupForm.tsx +++ b/js/apps/admin-ui/src/realm-settings/user-profile/AttributesGroupForm.tsx @@ -1,50 +1,29 @@ -/* eslint-disable @typescript-eslint/no-empty-function */ import type { UserProfileGroup } from "@keycloak/keycloak-admin-client/lib/defs/userProfileMetadata"; -import { - HelpItem, - TextControl, - useAlerts, - useFetch, -} from "@keycloak/keycloak-ui-shared"; +import { HelpItem, TextControl, useAlerts } from "@keycloak/keycloak-ui-shared"; import { ActionGroup, - Alert, Button, FormGroup, - Grid, - GridItem, PageSection, Text, TextContent, - TextInput, } from "@patternfly/react-core"; -import { useEffect, useMemo, useState } from "react"; -import { - FormProvider, - SubmitHandler, - useForm, - useWatch, -} from "react-hook-form"; +import { useEffect, useMemo } from "react"; +import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { Link, useNavigate, useParams } from "react-router-dom"; +import { useAdminClient } from "../../admin-client"; import { FormAccess } from "../../components/form/FormAccess"; import { KeyValueInput } from "../../components/key-value-form/KeyValueInput"; import type { KeyValueType } from "../../components/key-value-form/key-value-convert"; import { ViewHeader } from "../../components/view-header/ViewHeader"; import { useRealm } from "../../context/realm-context/RealmContext"; -import { i18n } from "../../i18n/i18n"; -import useLocale from "../../utils/useLocale"; -import useToggle from "../../utils/useToggle"; import "../realm-settings-section.css"; import type { EditAttributesGroupParams } from "../routes/EditAttributesGroup"; import { toUserProfile } from "../routes/UserProfile"; import { useUserProfile } from "./UserProfileContext"; -import { - AddTranslationsDialog, - TranslationsType, -} from "./attribute/AddTranslationsDialog"; -import { GlobeRouteIcon } from "@patternfly/react-icons"; -import { useAdminClient } from "../../admin-client"; +import { saveTranslations, Translations } from "./attribute/TranslatableField"; +import { TranslatableField } from "./attribute/TranslatableField"; function parseAnnotations(input: Record): KeyValueType[] { return Object.entries(input).reduce((p, [key, value]) => { @@ -64,25 +43,17 @@ function transformAnnotations(input: KeyValueType[]): Record { ); } -type FormFields = Required> & { - annotations: KeyValueType[]; -}; - -type TranslationForm = { - locale: string; - value: string; -}; - -type Translations = { - key: string; - translations: TranslationForm[]; -}; +type FormFields = Required> & + Translations & { + annotations: KeyValueType[]; + }; const defaultValues: FormFields = { annotations: [], displayDescription: "", displayHeader: "", name: "", + translation: { key: [] }, }; export default function AttributesGroupForm() { @@ -91,34 +62,10 @@ export default function AttributesGroupForm() { const { realm: realmName, realmRepresentation: realm } = useRealm(); const { config, save } = useUserProfile(); const navigate = useNavigate(); - const combinedLocales = useLocale(); const params = useParams(); const form = useForm({ defaultValues }); const { addError } = useAlerts(); const editMode = params.name ? true : false; - const [newAttributesGroupName, setNewAttributesGroupName] = useState(""); - const [ - generatedAttributesGroupDisplayName, - setGeneratedAttributesGroupDisplayName, - ] = useState(""); - const [ - generatedAttributesGroupDisplayDescription, - setGeneratedAttributesGroupDisplayDescription, - ] = useState(""); - const [addTranslationsModalOpen, toggleModal] = useToggle(); - const regexPattern = /\$\{([^}]+)\}/; - const [type, setType] = useState(); - - const [translationsData, setTranslationsData] = useState({ - displayHeader: { - key: "", - translations: [] as TranslationForm[], - }, - displayDescription: { - key: "", - translations: [] as TranslationForm[], - }, - }); const matchingGroup = useMemo( () => config?.groups?.find(({ name }) => name === params.name), @@ -137,156 +84,6 @@ export default function AttributesGroupForm() { form.reset({ ...defaultValues, ...matchingGroup, annotations }); }, [matchingGroup, form]); - useEffect(() => { - form.setValue( - "displayHeader", - matchingGroup?.displayHeader || generatedAttributesGroupDisplayName || "", - ); - form.setValue( - "displayDescription", - matchingGroup?.displayDescription || - generatedAttributesGroupDisplayDescription || - "", - ); - }, [ - generatedAttributesGroupDisplayName, - generatedAttributesGroupDisplayDescription, - matchingGroup, - form, - ]); - - useFetch( - async () => { - const translationsToSaveDisplayHeader: Translations[] = []; - const translationsToSaveDisplayDescription: Translations[] = []; - - await Promise.all( - combinedLocales.map(async (locale: string) => { - try { - const translations = - await adminClient.realms.getRealmLocalizationTexts({ - realm: realmName, - selectedLocale: locale, - }); - - const formData = form.getValues(); - const extractKey = (value: string | undefined) => { - const match = value?.match(/\$\{(.*?)\}/); - return match ? match[1] : ""; - }; - - const displayHeaderKey = extractKey(formData.displayHeader) || ""; - const displayDescriptionKey = - extractKey(formData.displayDescription) || ""; - - const headerTranslation = translations[displayHeaderKey] || ""; - const descriptionTranslation = - translations[displayDescriptionKey] || ""; - - if (headerTranslation) { - translationsToSaveDisplayHeader.push({ - key: displayHeaderKey, - translations: [{ locale, value: headerTranslation }], - }); - } - - if (descriptionTranslation) { - translationsToSaveDisplayDescription.push({ - key: displayDescriptionKey, - translations: [{ locale, value: descriptionTranslation }], - }); - } - } catch (error) { - console.error(`Error fetching translations for ${locale}:`, error); - } - }), - ); - - const translationsDataNew = { - displayHeader: { - key: - translationsToSaveDisplayHeader.length > 0 - ? translationsToSaveDisplayHeader[0].key - : "", - translations: translationsToSaveDisplayHeader.flatMap( - (data) => data.translations, - ), - }, - displayDescription: { - key: - translationsToSaveDisplayDescription.length > 0 - ? translationsToSaveDisplayDescription[0].key - : "", - translations: translationsToSaveDisplayDescription.flatMap( - (data) => data.translations, - ), - }, - }; - - setTranslationsData(translationsDataNew); - }, - () => {}, - [combinedLocales, realmName, form], - ); - - const saveTranslations = async () => { - const addLocalization = async ( - key: string, - locale: string, - value: string, - ) => { - try { - await adminClient.realms.addLocalization( - { - realm: realmName, - selectedLocale: locale, - key: key, - }, - value, - ); - } catch (error) { - console.error( - `Error saving translation for locale ${locale}: ${error}`, - ); - } - }; - - try { - if ( - translationsData && - translationsData.displayHeader.translations.length > 0 - ) { - for (const translation of translationsData.displayHeader.translations) { - if (translation.locale && translation.value) { - await addLocalization( - translationsData.displayHeader.key, - translation.locale, - translation.value, - ); - } - } - } - - if ( - translationsData && - translationsData.displayDescription.translations.length > 0 - ) { - for (const translation of translationsData.displayDescription - .translations) { - if (translation.locale && translation.value) { - await addLocalization( - translationsData.displayDescription.key, - translation.locale, - translation.value, - ); - } - } - } - } catch (error) { - console.error(`Error while processing translations: ${error}`); - } - }; - const onSubmit: SubmitHandler = async (values) => { if (!config) { return; @@ -294,8 +91,9 @@ export default function AttributesGroupForm() { const groups = [...(config.groups ?? [])]; const updateAt = matchingGroup ? groups.indexOf(matchingGroup) : -1; + const { translation, ...groupValues } = values; const updatedGroup: UserProfileGroup = { - ...values, + ...groupValues, annotations: transformAnnotations(values.annotations), }; @@ -305,303 +103,100 @@ export default function AttributesGroupForm() { groups[updateAt] = updatedGroup; } - if (realm?.internationalizationEnabled) { - const hasNonEmptyDisplayHeaderTranslations = - translationsData.displayHeader.translations.some( - (translation) => translation.value.trim() !== "", - ); - const hasNonEmptyDisplayDescriptionTranslations = - translationsData.displayDescription.translations.some( - (translation) => translation.value.trim() !== "", - ); - - if ( - !hasNonEmptyDisplayHeaderTranslations || - !hasNonEmptyDisplayDescriptionTranslations - ) { - addError("createAttributeError", t("translationError")); - return; - } - } - const success = await save({ ...config, groups }); if (success) { - await saveTranslations(); - i18n.reloadResources(); + if (realm?.internationalizationEnabled) { + try { + await saveTranslations({ + adminClient, + realmName, + translationsData: { translation }, + }); + } catch (error) { + addError(t("errorSavingTranslations"), error); + } + } navigate(toUserProfile({ realm: realmName, tab: "attributes-group" })); } }; - const attributesGroupDisplayName = useWatch({ - control: form.control, - name: "displayHeader", - }); - - const attributesGroupDisplayDescription = useWatch({ - control: form.control, - name: "displayDescription", - }); - - const handleAttributesGroupNameChange = ( - event: React.FormEvent, - value: string, - ) => { - const newDisplayName = - value !== "" && realm?.internationalizationEnabled - ? "${profile.attribute-group." + `${value}}` - : ""; - const newDisplayDescription = - value !== "" && realm?.internationalizationEnabled - ? "${profile.attribute-group-description." + `${value}}` - : ""; - setNewAttributesGroupName(value); - setGeneratedAttributesGroupDisplayName(newDisplayName); - setGeneratedAttributesGroupDisplayDescription(newDisplayDescription); - }; - - const attributesGroupDisplayPatternMatch = regexPattern.test( - attributesGroupDisplayName || attributesGroupDisplayDescription, - ); - - const formattedAttributesGroupDisplayName = - attributesGroupDisplayName?.substring( - 2, - attributesGroupDisplayName.length - 1, - ); - const formattedAttributesGroupDisplayDescription = - attributesGroupDisplayDescription?.substring( - 2, - attributesGroupDisplayDescription.length - 1, - ); - - const handleHeaderTranslationsAdded = (headerTranslations: Translations) => { - setTranslationsData((prev) => ({ - ...prev, - displayHeader: headerTranslations, - })); - }; - - const handleDescriptionTranslationsAdded = ( - descriptionTranslations: Translations, - ) => { - setTranslationsData((prev) => ({ - ...prev, - displayDescription: descriptionTranslations, - })); - }; - - const handleToggleDialog = () => { - toggleModal(); - }; - - const groupDisplayNameKey = - type === "displayHeader" - ? formattedAttributesGroupDisplayName - : `profile.attribute-group.${newAttributesGroupName}`; - const groupDisplayDescriptionKey = - type === "displayDescription" - ? formattedAttributesGroupDisplayDescription - : `profile.attribute-group-description.${newAttributesGroupName}`; - return ( - <> - {addTranslationsModalOpen && ( - { - toggleModal(); - }} - /> - )} + - - { - handleAttributesGroupNameChange(event, event.target.value); - }, - }} + + + } + fieldId="kc-attributes-group-display-header" + > + - {!!matchingGroup && ( - - )} - + + } + fieldId="kc-attributes-group-display-description" + > + + + + {t("annotationsText")} + + + + + + + - - - + {t("cancel")} + + - + ); } diff --git a/js/apps/admin-ui/src/realm-settings/user-profile/attribute/AddTranslationsDialog.tsx b/js/apps/admin-ui/src/realm-settings/user-profile/attribute/AddTranslationsDialog.tsx index a7eae8be8ec..c8e6da350aa 100644 --- a/js/apps/admin-ui/src/realm-settings/user-profile/attribute/AddTranslationsDialog.tsx +++ b/js/apps/admin-ui/src/realm-settings/user-profile/attribute/AddTranslationsDialog.tsx @@ -1,6 +1,6 @@ import { + ListEmptyState, PaginatingTableToolbar, - TextControl, useFetch, } from "@keycloak/keycloak-ui-shared"; import { @@ -14,51 +14,31 @@ import { ModalVariant, Text, TextContent, + TextInput, TextVariants, } from "@patternfly/react-core"; import { SearchIcon } from "@patternfly/react-icons"; import { Table, Tbody, Td, Th, Thead, Tr } from "@patternfly/react-table"; -import { useEffect, useMemo, useState } from "react"; -import { FormProvider, useForm, useWatch } from "react-hook-form"; +import { useState } from "react"; +import { useFormContext } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { useAdminClient } from "../../../admin-client"; -import { ListEmptyState } from "@keycloak/keycloak-ui-shared"; import { useRealm } from "../../../context/realm-context/RealmContext"; import { useWhoAmI } from "../../../context/whoami/WhoAmI"; -import { localeToDisplayName } from "../../../util"; +import { beerify, localeToDisplayName } from "../../../util"; import useLocale from "../../../utils/useLocale"; +import { Translation, TranslationForm } from "./TranslatableField"; -export type TranslationsType = - | "displayName" - | "displayHeader" - | "displayDescription"; - -type TranslationForm = { - locale: string; - value: string; -}; - -type Translations = { - key: string; - translations: TranslationForm[]; -}; - -export type AddTranslationsDialogProps = { +type AddTranslationsDialogProps = { translationKey: string; - translations: Translations; - type: TranslationsType; - onCancel: () => void; + fieldName: string; toggleDialog: () => void; - onTranslationsAdded: (translations: Translations) => void; }; export const AddTranslationsDialog = ({ translationKey, - translations, - type, - onCancel, + fieldName, toggleDialog, - onTranslationsAdded, }: AddTranslationsDialogProps) => { const { adminClient } = useAdminClient(); const { t } = useTranslation(); @@ -68,39 +48,33 @@ export const AddTranslationsDialog = ({ const [max, setMax] = useState(10); const [first, setFirst] = useState(0); const [filter, setFilter] = useState(""); - const [defaultTranslations, setDefaultTranslations] = useState<{ - [key: string]: string; - }>({}); - - const form = useForm<{ - key: string; - translations: TranslationForm[]; - }>({ - mode: "onChange", - }); + const [translations, setTranslations] = useState([]); + const prefix = `translation.${beerify(translationKey)}`; const { - getValues, - handleSubmit, + register, setValue, + getValues, formState: { isValid }, - } = form; + } = useFormContext(); - const defaultLocales = useMemo(() => { - return realm?.defaultLocale!.length ? [realm.defaultLocale] : []; - }, [realm]); - - const filteredLocales = useMemo(() => { - return combinedLocales.filter((locale) => - localeToDisplayName(locale, whoAmI.getLocale())! - .toLowerCase() - .includes(filter.toLowerCase()), - ); - }, [combinedLocales, filter, whoAmI]); + const setupForm = (translation: Translation) => { + translation[translationKey].forEach((translation, rowIndex) => { + const valueKey = `${prefix}.${rowIndex}.value`; + setValue(`${prefix}.${rowIndex}.locale`, translation.locale || ""); + setValue(valueKey, getValues(valueKey) || translation.value); + }); + }; useFetch( async () => { - const selectedLocales = combinedLocales.map((locale) => locale); + const selectedLocales = combinedLocales + .filter((l) => + localeToDisplayName(l, whoAmI.getLocale()) + ?.toLocaleLowerCase(realm?.defaultLocale) + ?.includes(filter.toLocaleLowerCase(realm?.defaultLocale)), + ) + .slice(first, first + max + 1); const results = await Promise.all( selectedLocales.map((selectedLocale) => @@ -111,83 +85,18 @@ export const AddTranslationsDialog = ({ ), ); - const translations = results.map((result, index) => { - const locale = selectedLocales[index]; - const value = result[translationKey]; - return { - key: translationKey, - translations: [{ locale, value }], - }; - }); - - const defaultValuesMap = translations.reduce((acc, translation) => { - const locale = translation.translations[0].locale; - const value = translation.translations[0].value; - return { ...acc, [locale]: value }; - }, {}); - - return defaultValuesMap; + return results.map((result, index) => ({ + locale: selectedLocales[index], + value: result[translationKey], + })); }, (fetchedData) => { - setDefaultTranslations((prevTranslations) => { - if (prevTranslations !== fetchedData) { - return fetchedData; - } - return prevTranslations; - }); + setTranslations(fetchedData); + setupForm({ [translationKey]: fetchedData }); }, - [combinedLocales, translationKey, realmName], + [combinedLocales, first, max, filter], ); - useEffect(() => { - combinedLocales.forEach((locale, rowIndex) => { - setValue(`translations.${rowIndex}.locale`, locale); - - const translationExists = - translations.translations[rowIndex] !== undefined; - setValue( - `translations.${rowIndex}.value`, - translationExists - ? translations.translations[rowIndex]?.value - : defaultTranslations[locale] || "", - ); - }); - setValue("key", translationKey); - }, [ - combinedLocales, - defaultTranslations, - translationKey, - setValue, - translations, - ]); - - const handleOk = () => { - const formData = getValues(); - - const updatedTranslations = formData.translations.map((translation) => { - if (translation.locale === filter) { - return { - ...translation, - value: - formData.translations.find((t) => t.locale === filter)?.value ?? "", - }; - } - return translation; - }); - - onTranslationsAdded({ - key: formData.key, - translations: updatedTranslations, - }); - - toggleDialog(); - }; - - const isRequiredTranslation = useWatch({ - control: form.control, - name: "translations.0.value", - }); - return ( {t("addTranslationDialogOkBtn")} , @@ -209,7 +118,10 @@ export const AddTranslationsDialog = ({ key="cancel" data-testid="cancelTranslationBtn" variant="link" - onClick={onCancel} + onClick={() => { + setupForm({ [translationKey]: translations }); + toggleDialog(); + }} > {t("cancel")} , @@ -222,141 +134,108 @@ export const AddTranslationsDialog = ({ - {type !== "displayHeader" - ? t("addTranslationsModalSubTitleDescription") - : t("addTranslationsModalSubTitle")}{" "} + {t("addTranslationsModalSubTitle", { fieldName })} {t("addTranslationsModalSubTitleBolded")} - -
- + + - - - - {t("translationsTableHeading")} - - - { - setFirst(first); - setMax(max); - }} - inputGroupName={"search"} - inputGroupOnEnter={(search) => { - setFilter(search); - setFirst(0); - setMax(10); - }} - inputGroupPlaceholder={t("searchForLanguage")} + + + + - {filteredLocales.length === 0 && !filter && ( - - )} - {filteredLocales.length === 0 && filter && ( - - )} - {filteredLocales.length !== 0 && ( - - - - - -
- {t("supportedLanguagesTableColumnName")} - - {t("translationTableColumnName")} -
+ )} + +
+ diff --git a/js/apps/admin-ui/src/realm-settings/user-profile/attribute/AttributeGeneralSettings.tsx b/js/apps/admin-ui/src/realm-settings/user-profile/attribute/AttributeGeneralSettings.tsx index 81eefc77057..ed6dc752d8c 100644 --- a/js/apps/admin-ui/src/realm-settings/user-profile/attribute/AttributeGeneralSettings.tsx +++ b/js/apps/admin-ui/src/realm-settings/user-profile/attribute/AttributeGeneralSettings.tsx @@ -1,28 +1,23 @@ import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientScopeRepresentation"; import type { UserProfileConfig } from "@keycloak/keycloak-admin-client/lib/defs/userProfileMetadata"; import { - FormErrorText, HelpItem, KeycloakSelect, + KeycloakSpinner, SelectControl, SelectVariant, + TextControl, useFetch, } from "@keycloak/keycloak-ui-shared"; import { - Alert, - Button, Divider, FormGroup, - Grid, - GridItem, Radio, SelectOption, Switch, - TextInput, } from "@patternfly/react-core"; -import { GlobeRouteIcon } from "@patternfly/react-icons"; import { isEqual } from "lodash-es"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { Controller, FormProvider, @@ -32,17 +27,11 @@ import { import { useTranslation } from "react-i18next"; import { useAdminClient } from "../../../admin-client"; import { FormAccess } from "../../../components/form/FormAccess"; -import { KeycloakSpinner } from "@keycloak/keycloak-ui-shared"; import { DefaultSwitchControl } from "../../../components/SwitchControl"; -import { useRealm } from "../../../context/realm-context/RealmContext"; import { useParams } from "../../../utils/useParams"; -import useToggle from "../../../utils/useToggle"; import { USERNAME_EMAIL } from "../../NewAttributeSettings"; import { AttributeParams } from "../../routes/Attribute"; -import { - AddTranslationsDialog, - TranslationsType, -} from "./AddTranslationsDialog"; +import { TranslatableField } from "./TranslatableField"; import "../../realm-settings-section.css"; @@ -52,57 +41,17 @@ const REQUIRED_FOR = [ { label: "requiredForLabel.admins", value: ["admin"] }, ] as const; -type TranslationForm = { - locale: string; - value: string; -}; - -type Translations = { - key: string; - translations: TranslationForm[]; -}; - -export type AttributeGeneralSettingsProps = { - onHandlingTranslationData: (data: Translations) => void; - onHandlingGeneratedDisplayName: (displayName: string) => void; -}; - -export const AttributeGeneralSettings = ({ - onHandlingTranslationData, - onHandlingGeneratedDisplayName, -}: AttributeGeneralSettingsProps) => { +export const AttributeGeneralSettings = () => { const { adminClient } = useAdminClient(); const { t } = useTranslation(); - const { realmRepresentation: realm } = useRealm(); const form = useFormContext(); const [clientScopes, setClientScopes] = useState(); const [config, setConfig] = useState(); const [selectEnabledWhenOpen, setSelectEnabledWhenOpen] = useState(false); const [selectRequiredForOpen, setSelectRequiredForOpen] = useState(false); - const [addTranslationsModalOpen, toggleModal] = useToggle(); const { attributeName } = useParams(); const editMode = attributeName ? true : false; - const [newAttributeName, setNewAttributeName] = useState(""); - const [generatedDisplayName, setGeneratedDisplayName] = useState(""); - const [type, setType] = useState(); - const [translationsData, setTranslationsData] = useState({ - key: "", - translations: [], - }); - const displayNameRegex = /\$\{([^}]+)\}/; - - const handleAttributeNameChange = ( - _event: React.FormEvent, - value: string, - ) => { - setNewAttributeName(value); - const newDisplayName = - value !== "" && realm?.internationalizationEnabled - ? "${profile.attributes." + `${value}}` - : ""; - setGeneratedDisplayName(newDisplayName); - }; const hasSelector = useWatch({ control: form.control, @@ -120,29 +69,9 @@ export const AttributeGeneralSettings = ({ defaultValue: false, }); - const attributeDisplayName = useWatch({ - control: form.control, - name: "displayName", - }); - - const displayNamePatternMatch = displayNameRegex.test(attributeDisplayName); - useFetch(() => adminClient.clientScopes.find(), setClientScopes, []); useFetch(() => adminClient.users.getProfile(), setConfig, []); - const handleTranslationsData = (translationsData: Translations) => { - onHandlingTranslationData(translationsData); - }; - - const handleGeneratedDisplayName = (displayName: string) => { - onHandlingGeneratedDisplayName(displayName); - }; - - useEffect(() => { - handleTranslationsData(translationsData); - handleGeneratedDisplayName(generatedDisplayName); - }, [translationsData, generatedDisplayName]); - if (!clientScopes) { return ; } @@ -155,379 +84,286 @@ export const AttributeGeneralSettings = ({ form.setValue("hasRequiredScopes", hasRequiredScopes); } - const handleTranslationsAdded = (translationsData: Translations) => { - setTranslationsData(translationsData); - }; - - const handleToggleDialog = () => { - toggleModal(); - handleTranslationsData(translationsData); - handleGeneratedDisplayName(generatedDisplayName); - }; - - const formattedAttributeDisplayName = attributeDisplayName?.substring( - 2, - attributeDisplayName.length - 1, - ); - return ( - <> - {addTranslationsModalOpen && ( - { - toggleModal(); + + + - )} - - - - } - fieldId="kc-attribute-name" - isRequired - > - - {form.formState.errors.name && ( - - )} - - + + + + ({ + key: g.name!, + value: g.name!, + })) || []), + ]} + /> + {!USERNAME_EMAIL.includes(attributeName) && ( + <> + + + } + fieldId="enabledWhen" + hasNoPaddingTop + > + setHasSelector(false)} + className="pf-v5-u-mb-md" /> - } - fieldId="kc-attribute-display-name" - > - - - - {generatedDisplayName && ( - - )} - - {realm?.internationalizationEnabled && ( - -