Adding translations when a new attribute is created (#27313)
* reimplemented attribute translations Signed-off-by: Agnieszka Gancarczyk <agancarc@redhat.com> * added translations refresh Signed-off-by: Agnieszka Gancarczyk <agancarc@redhat.com> * improvement Signed-off-by: Agnieszka Gancarczyk <agancarc@redhat.com> * feedback Signed-off-by: Agnieszka Gancarczyk <agancarc@redhat.com> * feedback Signed-off-by: Agnieszka Gancarczyk <agancarc@redhat.com> * refactor Signed-off-by: Agnieszka Gancarczyk <agancarc@redhat.com> * refactor Signed-off-by: Agnieszka Gancarczyk <agancarc@redhat.com> * added type Signed-off-by: Agnieszka Gancarczyk <agancarc@redhat.com> --------- Signed-off-by: Agnieszka Gancarczyk <agancarc@redhat.com> Co-authored-by: Agnieszka Gancarczyk <agancarc@redhat.com>
This commit is contained in:
parent
2c750c8ffb
commit
f49efd0a51
11 changed files with 735 additions and 61 deletions
|
@ -1181,7 +1181,7 @@ deleteClientSuccess=Client profile deleted
|
|||
emptyClientScopesPrimaryAction=Add client scopes
|
||||
addStepTo=Add step to {{name}}
|
||||
eventTypes.AUTHREQID_TO_TOKEN_ERROR.description=Authreqid to token error
|
||||
deleteAttributeConfirm=Are you sure you want to permanently delete the attribute {{attributeName}}?
|
||||
deleteAttributeConfirm=Are you sure you want to permanently delete the attribute {{attributeName}} and its corresponding translations?
|
||||
chooseResources=Choose the resources you want to import
|
||||
selectOne=Select an option
|
||||
emailTheme=Email theme
|
||||
|
@ -1592,7 +1592,7 @@ exportFail=Could not export realm\: '{{error}}'
|
|||
flowTypeHelp=What kind of form is it
|
||||
targetHelp=Destination field for the mapper. LOCAL (default) means that the changes are applied to the username stored in local database upon user import. BROKER_ID and BROKER_USERNAME means that the changes are stored into the ID or username used for federation user lookup, respectively.
|
||||
setPasswordConfirm=Set password?
|
||||
attributeDisplayNameHelp=Display name for the attribute. Supports keys for localized values as well. For example\: ${profile.attribute.phoneNumber}.
|
||||
attributeDisplayNameHelp=Display name for the attribute. Supports keys for localized values as well. For example\: ${profile.attribute.phoneNumber}. Hit the 'Globe Route' icon to add translations.
|
||||
assignedType=Assigned type
|
||||
modeHelp=LDAP_ONLY means that all group mappings of users are retrieved from LDAP and saved into LDAP. READ_ONLY is Read-only LDAP mode where group mappings are retrieved from both LDAP and DB and merged together. New group joins are not saved to LDAP but to DB. IMPORT is Read-only LDAP mode where group mappings are retrieved from LDAP just at the time when user is imported from LDAP and then they are saved to local keycloak DB.
|
||||
identityProvider=Identity provider
|
||||
|
@ -3075,4 +3075,25 @@ to the attribute. For that, make sure to use any of the built-in validators to p
|
|||
sendIdTokenOnLogout=Send 'id_token_hint' in logout requests
|
||||
sendIdTokenOnLogoutHelp=If the 'id_token_hint' parameter should be sent in logout requests.
|
||||
sendClientIdOnLogout=Send 'client_id' in logout requests
|
||||
sendClientIdOnLogoutHelp=If the 'client_id' parameter should be sent in logout requests.
|
||||
sendClientIdOnLogoutHelp=If the 'client_id' parameter should be sent in logout requests.
|
||||
searchClientAuthorizationPermission=Search permission
|
||||
addAttributeTranslationBtn=Add translation button
|
||||
addAttributeTranslationTooltip=Add translations for this field value.
|
||||
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
|
||||
addTranslationsModalSubTitleBolded=Realm settings > Localization > Realm overrides.
|
||||
translationKey=Key
|
||||
translationValue=Value
|
||||
translationsTableHeading=Translations
|
||||
searchForLanguage=Search for language
|
||||
supportedLanguagesTableColumnName=Supported languages
|
||||
translationTableColumnName=Translation
|
||||
defaultLanguage=Default
|
||||
translationValue=Translation value
|
||||
noLanguages=No languages
|
||||
noLanguagesInstructions=Add a language in Realm settings > Localization > Locales to get started.
|
||||
addTranslationsDialogRowsTable=Add a translations dialog rows table
|
||||
addTranslationDialogHelperText=The translation based on the default language is required.
|
||||
noLanguagesSearchResultsInstructions=Click on the search bar above to search for languages
|
||||
addTranslationDialogOkBtn=Ok
|
||||
translationError=Please add translations before saving
|
||||
|
|
|
@ -40,7 +40,7 @@ export const AddTranslationModal = ({
|
|||
return (
|
||||
<Modal
|
||||
variant={ModalVariant.small}
|
||||
title={t("AddTranslation")}
|
||||
title={t("addTranslation")}
|
||||
isOpen
|
||||
onClose={handleModalToggle}
|
||||
actions={[
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
|
||||
import type {
|
||||
UserProfileAttribute,
|
||||
UserProfileConfig,
|
||||
|
@ -31,6 +32,16 @@ import { AttributeValidations } from "./user-profile/attribute/AttributeValidati
|
|||
|
||||
import "./realm-settings-section.css";
|
||||
|
||||
type TranslationForm = {
|
||||
locale: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type Translations = {
|
||||
key: string;
|
||||
translations: TranslationForm[];
|
||||
};
|
||||
|
||||
type IndexedAnnotations = {
|
||||
key: string;
|
||||
value?: Record<string, unknown>;
|
||||
|
@ -81,21 +92,41 @@ 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<AttributeParams>();
|
||||
const editMode = attributeName ? true : false;
|
||||
|
||||
const handleTranslationsData = (translationsData: Translations) => {
|
||||
onHandlingTranslationsData(translationsData);
|
||||
};
|
||||
|
||||
const handleGeneratedDisplayName = (generatedDisplayName: string) => {
|
||||
onHandlingGeneratedDisplayName(generatedDisplayName);
|
||||
};
|
||||
|
||||
return (
|
||||
<UserProfileProvider>
|
||||
<ScrollForm
|
||||
label={t("jumpToSection")}
|
||||
sections={[
|
||||
{ title: t("generalSettings"), panel: <AttributeGeneralSettings /> },
|
||||
{
|
||||
title: t("generalSettings"),
|
||||
panel: (
|
||||
<AttributeGeneralSettings
|
||||
onHandlingTranslationData={handleTranslationsData}
|
||||
onHandlingGeneratedDisplayName={handleGeneratedDisplayName}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{ title: t("permission"), panel: <AttributePermission /> },
|
||||
{ title: t("validations"), panel: <AttributeValidations /> },
|
||||
{ title: t("annotations"), panel: <AttributeAnnotations /> },
|
||||
|
@ -124,13 +155,30 @@ const CreateAttributeFormContent = ({
|
|||
};
|
||||
|
||||
export default function NewAttributeSettings() {
|
||||
const { realm, attributeName } = useParams<AttributeParams>();
|
||||
const { realm: realmName, attributeName } = useParams<AttributeParams>();
|
||||
const form = useForm<UserProfileAttributeFormFields>();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { addAlert, addError } = useAlerts();
|
||||
const [config, setConfig] = useState<UserProfileConfig | null>(null);
|
||||
const editMode = attributeName ? true : false;
|
||||
const [translationsData, setTranslationsData] = useState<Translations>({
|
||||
key: "",
|
||||
translations: [],
|
||||
});
|
||||
const [generatedDisplayName, setGeneratedDisplayName] = useState<string>("");
|
||||
const [realm, setRealm] = useState<RealmRepresentation>();
|
||||
|
||||
useFetch(
|
||||
() => adminClient.realms.findOne({ realm: realmName }),
|
||||
(realm) => {
|
||||
if (!realm) {
|
||||
throw new Error(t("notFound"));
|
||||
}
|
||||
setRealm(realm);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useFetch(
|
||||
() => adminClient.users.getProfile(),
|
||||
|
@ -178,6 +226,30 @@ 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) {
|
||||
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,
|
||||
|
@ -206,7 +278,7 @@ export default function NewAttributeSettings() {
|
|||
);
|
||||
|
||||
const patchAttributes = () =>
|
||||
config?.attributes!.map((attribute) => {
|
||||
(config?.attributes || []).map((attribute) => {
|
||||
if (attribute.name !== attributeName) {
|
||||
return attribute;
|
||||
}
|
||||
|
@ -229,11 +301,11 @@ export default function NewAttributeSettings() {
|
|||
});
|
||||
|
||||
const addAttribute = () =>
|
||||
config?.attributes!.concat([
|
||||
(config?.attributes || []).concat([
|
||||
Object.assign(
|
||||
{
|
||||
name: formFields.name,
|
||||
displayName: formFields.displayName!,
|
||||
displayName: formFields.displayName! || generatedDisplayName,
|
||||
required: formFields.isRequired ? formFields.required : undefined,
|
||||
selector: formFields.selector,
|
||||
permissions: formFields.permissions!,
|
||||
|
@ -246,16 +318,28 @@ export default function NewAttributeSettings() {
|
|||
),
|
||||
] as UserProfileAttribute);
|
||||
|
||||
const updatedAttributes = editMode ? patchAttributes() : addAttribute();
|
||||
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,
|
||||
realm: realmName,
|
||||
});
|
||||
|
||||
navigate(toUserProfile({ realm, tab: "attributes" }));
|
||||
await saveTranslations();
|
||||
navigate(toUserProfile({ realm: realmName, tab: "attributes" }));
|
||||
|
||||
addAlert(t("createAttributeSuccess"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
|
@ -270,7 +354,11 @@ export default function NewAttributeSettings() {
|
|||
subKey={editMode ? "" : t("createAttributeSubTitle")}
|
||||
/>
|
||||
<PageSection variant="light">
|
||||
<CreateAttributeFormContent save={() => form.handleSubmit(save)()} />
|
||||
<CreateAttributeFormContent
|
||||
save={() => form.handleSubmit(save)()}
|
||||
onHandlingTranslationsData={setTranslationsData}
|
||||
onHandlingGeneratedDisplayName={setGeneratedDisplayName}
|
||||
/>
|
||||
</PageSection>
|
||||
</FormProvider>
|
||||
);
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
TabTitleText,
|
||||
Tooltip,
|
||||
} from "@patternfly/react-core";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
@ -53,6 +53,7 @@ import { ClientPoliciesTab, toClientPolicies } from "./routes/ClientPolicies";
|
|||
import { RealmSettingsTab, toRealmSettings } from "./routes/RealmSettings";
|
||||
import { SecurityDefenses } from "./security-defences/SecurityDefenses";
|
||||
import { UserProfileTab } from "./user-profile/UserProfileTab";
|
||||
import { DEFAULT_LOCALE } from "../i18n/i18n";
|
||||
|
||||
export interface UIRealmRepresentation extends RealmRepresentation {
|
||||
upConfig?: UserProfileConfig;
|
||||
|
@ -174,6 +175,7 @@ const RealmSettingsHeader = ({
|
|||
type RealmSettingsTabsProps = {
|
||||
realm: UIRealmRepresentation;
|
||||
refresh: () => void;
|
||||
tableData?: Record<string, string>[];
|
||||
};
|
||||
|
||||
export const RealmSettingsTabs = ({
|
||||
|
@ -186,6 +188,9 @@ export const RealmSettingsTabs = ({
|
|||
const { refresh: refreshRealms } = useRealms();
|
||||
const navigate = useNavigate();
|
||||
const isFeatureEnabled = useIsFeatureEnabled();
|
||||
const [tableData, setTableData] = useState<
|
||||
Record<string, string>[] | undefined
|
||||
>(undefined);
|
||||
|
||||
const { control, setValue, getValues } = useForm({
|
||||
mode: "onChange",
|
||||
|
@ -200,7 +205,47 @@ export const RealmSettingsTabs = ({
|
|||
convertToFormValues(r, setValue);
|
||||
};
|
||||
|
||||
useEffect(setupForm, [setValue, realm]);
|
||||
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]);
|
||||
|
||||
useEffect(() => {
|
||||
setupForm();
|
||||
const fetchLocalizationTexts = async () => {
|
||||
try {
|
||||
await Promise.all(
|
||||
combinedLocales.map(async (locale) => {
|
||||
try {
|
||||
const response =
|
||||
await adminClient.realms.getRealmLocalizationTexts({
|
||||
realm: realmName,
|
||||
selectedLocale: locale,
|
||||
});
|
||||
|
||||
if (response) {
|
||||
setTableData([response]);
|
||||
}
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
fetchLocalizationTexts();
|
||||
}, [setValue, realm]);
|
||||
|
||||
const save = async (r: UIRealmRepresentation) => {
|
||||
r = convertFormValuesToObject(r);
|
||||
|
@ -354,7 +399,12 @@ export const RealmSettingsTabs = ({
|
|||
data-testid="rs-localization-tab"
|
||||
{...localizationTab}
|
||||
>
|
||||
<LocalizationTab key={key} save={save} realm={realm} />
|
||||
<LocalizationTab
|
||||
key={key}
|
||||
save={save}
|
||||
realm={realm}
|
||||
tableData={tableData}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
title={<TabTitleText>{t("securityDefences")}</TabTitleText>}
|
||||
|
@ -422,7 +472,7 @@ export const RealmSettingsTabs = ({
|
|||
data-testid="rs-user-profile-tab"
|
||||
{...userProfileTab}
|
||||
>
|
||||
<UserProfileTab />
|
||||
<UserProfileTab setTableData={setTableData as any} />
|
||||
</Tab>
|
||||
<Tab
|
||||
title={<TabTitleText>{t("userRegistration")}</TabTitleText>}
|
||||
|
|
|
@ -26,9 +26,14 @@ import { RealmOverrides } from "./RealmOverrides";
|
|||
type LocalizationTabProps = {
|
||||
save: (realm: RealmRepresentation) => void;
|
||||
realm: RealmRepresentation;
|
||||
tableData: Record<string, string>[] | undefined;
|
||||
};
|
||||
|
||||
export const LocalizationTab = ({ save, realm }: LocalizationTabProps) => {
|
||||
export const LocalizationTab = ({
|
||||
save,
|
||||
realm,
|
||||
tableData,
|
||||
}: LocalizationTabProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { whoAmI } = useWhoAmI();
|
||||
|
||||
|
@ -244,6 +249,7 @@ export const LocalizationTab = ({ save, realm }: LocalizationTabProps) => {
|
|||
internationalizationEnabled={internationalizationEnabled}
|
||||
watchSupportedLocales={watchSupportedLocales}
|
||||
realm={realm}
|
||||
tableData={tableData}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
|
|
|
@ -34,7 +34,7 @@ import {
|
|||
Thead,
|
||||
Tr,
|
||||
} from "@patternfly/react-table";
|
||||
import RealmRepresentation from "libs/keycloak-admin-client/lib/defs/realmRepresentation";
|
||||
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
|
||||
import { cloneDeep, isEqual, uniqWith } from "lodash-es";
|
||||
import { ChangeEvent, useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
@ -56,6 +56,7 @@ type RealmOverridesProps = {
|
|||
internationalizationEnabled: boolean;
|
||||
watchSupportedLocales: string[];
|
||||
realm: RealmRepresentation;
|
||||
tableData: Record<string, string>[] | undefined;
|
||||
};
|
||||
|
||||
type EditStatesType = { [key: number]: boolean };
|
||||
|
@ -77,6 +78,7 @@ export const RealmOverrides = ({
|
|||
internationalizationEnabled,
|
||||
watchSupportedLocales,
|
||||
realm,
|
||||
tableData,
|
||||
}: RealmOverridesProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [addTranslationModalOpen, setAddTranslationModalOpen] = useState(false);
|
||||
|
@ -173,7 +175,7 @@ export const RealmOverrides = ({
|
|||
|
||||
setTableRows(updatedRows);
|
||||
});
|
||||
}, [tableKey, first, max, filter]);
|
||||
}, [tableKey, tableData, first, max, filter]);
|
||||
|
||||
const handleModalToggle = () => {
|
||||
setAddTranslationModalOpen(!addTranslationModalOpen);
|
||||
|
|
|
@ -293,3 +293,7 @@ input#kc-scopes {
|
|||
.kc--attributes-validations--action-cell {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.kc-attribute-display-name-iconBtn:hover {
|
||||
color: var(--pf-c-button--m-primary--active--BackgroundColor);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
|
||||
import type { UserProfileAttribute } from "@keycloak/keycloak-admin-client/lib/defs/userProfileMetadata";
|
||||
import {
|
||||
Button,
|
||||
|
@ -12,7 +13,7 @@ import {
|
|||
} from "@patternfly/react-core";
|
||||
import { FilterIcon } from "@patternfly/react-icons";
|
||||
import { uniqBy } from "lodash-es";
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { DraggableTable } from "../../authentication/components/DraggableTable";
|
||||
|
@ -23,12 +24,21 @@ import useToggle from "../../utils/useToggle";
|
|||
import { toAddAttribute } from "../routes/AddAttribute";
|
||||
import { toAttribute } from "../routes/Attribute";
|
||||
import { useUserProfile } from "./UserProfileContext";
|
||||
import { useFetch } from "../../utils/useFetch";
|
||||
import { adminClient } from "../../admin-client";
|
||||
import { DEFAULT_LOCALE } from "../../i18n/i18n";
|
||||
|
||||
const RESTRICTED_ATTRIBUTES = ["username", "email"];
|
||||
|
||||
type movedAttributeType = UserProfileAttribute;
|
||||
|
||||
export const AttributesTab = () => {
|
||||
type AttributesTabProps = {
|
||||
setTableData: React.Dispatch<
|
||||
React.SetStateAction<Record<string, string>[] | undefined>
|
||||
>;
|
||||
};
|
||||
|
||||
export const AttributesTab = ({ setTableData }: AttributesTabProps) => {
|
||||
const { config, save } = useUserProfile();
|
||||
const { realm: realmName } = useRealm();
|
||||
const { t } = useTranslation();
|
||||
|
@ -38,6 +48,32 @@ export const AttributesTab = () => {
|
|||
useToggle();
|
||||
const [data, setData] = useState(config?.attributes);
|
||||
const [attributeToDelete, setAttributeToDelete] = useState("");
|
||||
const [realm, setRealm] = useState<RealmRepresentation>();
|
||||
|
||||
useFetch(
|
||||
() => adminClient.realms.findOne({ realm: realmName }),
|
||||
(realm) => {
|
||||
if (!realm) {
|
||||
throw new Error(t("notFound"));
|
||||
}
|
||||
setRealm(realm);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
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]);
|
||||
|
||||
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||
titleKey: t("deleteAttributeConfirmTitle"),
|
||||
|
@ -49,18 +85,58 @@ export const AttributesTab = () => {
|
|||
onConfirm: async () => {
|
||||
if (!config?.attributes) return;
|
||||
|
||||
const updatedAttributes = config.attributes.filter(
|
||||
(attribute) => attribute.name !== attributeToDelete,
|
||||
);
|
||||
const translationsToDelete = config.attributes.find(
|
||||
(attribute) => attribute.name === attributeToDelete,
|
||||
)?.displayName;
|
||||
|
||||
save(
|
||||
{ ...config, attributes: updatedAttributes!, groups: config.groups },
|
||||
{
|
||||
successMessageKey: "deleteAttributeSuccess",
|
||||
errorMessageKey: "deleteAttributeError",
|
||||
},
|
||||
);
|
||||
setAttributeToDelete("");
|
||||
try {
|
||||
await Promise.all(
|
||||
combinedLocales.map(async (locale) => {
|
||||
try {
|
||||
const response =
|
||||
await adminClient.realms.getRealmLocalizationTexts({
|
||||
realm: realmName,
|
||||
selectedLocale: locale,
|
||||
});
|
||||
|
||||
if (response) {
|
||||
await adminClient.realms.deleteRealmLocalizationTexts({
|
||||
realm: realmName,
|
||||
selectedLocale: locale,
|
||||
key: translationsToDelete,
|
||||
});
|
||||
|
||||
const updatedData =
|
||||
await adminClient.realms.getRealmLocalizationTexts({
|
||||
realm: realmName,
|
||||
selectedLocale: locale,
|
||||
});
|
||||
setTableData([updatedData]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error removing translations for ${locale}`);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const updatedAttributes = config.attributes.filter(
|
||||
(attribute) => attribute.name !== attributeToDelete,
|
||||
);
|
||||
|
||||
save(
|
||||
{ ...config, attributes: updatedAttributes, groups: config.groups },
|
||||
{
|
||||
successMessageKey: "deleteAttributeSuccess",
|
||||
errorMessageKey: "deleteAttributeError",
|
||||
},
|
||||
);
|
||||
|
||||
setAttributeToDelete("");
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error removing translations or updating attributes: ${error}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -15,7 +15,13 @@ import { AttributesTab } from "./AttributesTab";
|
|||
import { JsonEditorTab } from "./JsonEditorTab";
|
||||
import { UserProfileProvider } from "./UserProfileContext";
|
||||
|
||||
export const UserProfileTab = () => {
|
||||
type UserProfileTabProps = {
|
||||
setTableData: React.Dispatch<
|
||||
React.SetStateAction<Record<string, string>[] | undefined>
|
||||
>;
|
||||
};
|
||||
|
||||
export const UserProfileTab = ({ setTableData }: UserProfileTabProps) => {
|
||||
const { realm } = useRealm();
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
@ -37,7 +43,7 @@ export const UserProfileTab = () => {
|
|||
data-testid="attributesTab"
|
||||
{...attributesTab}
|
||||
>
|
||||
<AttributesTab />
|
||||
<AttributesTab setTableData={setTableData} />
|
||||
</Tab>
|
||||
<Tab
|
||||
title={<TabTitleText>{t("attributesGroup")}</TabTitleText>}
|
||||
|
|
|
@ -0,0 +1,290 @@
|
|||
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
FlexItem,
|
||||
Form,
|
||||
FormGroup,
|
||||
Label,
|
||||
Modal,
|
||||
ModalVariant,
|
||||
Text,
|
||||
TextContent,
|
||||
TextVariants,
|
||||
} from "@patternfly/react-core";
|
||||
import { Table, Tbody, Td, Th, Thead, Tr } from "@patternfly/react-table";
|
||||
import { SearchIcon } from "@patternfly/react-icons";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { useRealm } from "../../../context/realm-context/RealmContext";
|
||||
import { useWhoAmI } from "../../../context/whoami/WhoAmI";
|
||||
import { adminClient } from "../../../admin-client";
|
||||
import { PaginatingTableToolbar } from "../../../components/table-toolbar/PaginatingTableToolbar";
|
||||
import { ListEmptyState } from "../../../components/list-empty-state/ListEmptyState";
|
||||
import { useFetch } from "../../../utils/useFetch";
|
||||
import { localeToDisplayName } from "../../../util";
|
||||
import { DEFAULT_LOCALE } from "../../../i18n/i18n";
|
||||
import { TextControl } from "ui-shared";
|
||||
|
||||
type TranslationForm = {
|
||||
locale: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type Translations = {
|
||||
key: string;
|
||||
translations: TranslationForm[];
|
||||
};
|
||||
|
||||
export type AddTranslationsDialogProps = {
|
||||
translationKey: string;
|
||||
onCancel: () => void;
|
||||
toggleDialog: () => void;
|
||||
onTranslationsAdded: (translations: Translations) => void;
|
||||
};
|
||||
|
||||
export const AddTranslationsDialog = ({
|
||||
translationKey,
|
||||
onCancel,
|
||||
toggleDialog,
|
||||
onTranslationsAdded,
|
||||
}: AddTranslationsDialogProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { realm: realmName } = useRealm();
|
||||
const [realm, setRealm] = useState<RealmRepresentation>();
|
||||
const { whoAmI } = useWhoAmI();
|
||||
const [max, setMax] = useState(10);
|
||||
const [first, setFirst] = useState(0);
|
||||
const [filter, setFilter] = useState("");
|
||||
|
||||
const form = useForm<{
|
||||
key: string;
|
||||
translations: TranslationForm[];
|
||||
}>({
|
||||
mode: "onChange",
|
||||
});
|
||||
|
||||
const {
|
||||
getValues,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { isValid },
|
||||
} = form;
|
||||
|
||||
useFetch(
|
||||
() => adminClient.realms.findOne({ realm: realmName }),
|
||||
(realm) => {
|
||||
if (!realm) {
|
||||
throw new Error(t("notFound"));
|
||||
}
|
||||
setRealm(realm);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
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]);
|
||||
|
||||
const filteredLocales = useMemo(() => {
|
||||
return combinedLocales.filter((locale) =>
|
||||
localeToDisplayName(locale, whoAmI.getLocale())!
|
||||
.toLowerCase()
|
||||
.includes(filter.toLowerCase()),
|
||||
);
|
||||
}, [combinedLocales, filter, whoAmI]);
|
||||
|
||||
useEffect(() => {
|
||||
combinedLocales.forEach((locale, rowIndex) => {
|
||||
setValue(`translations.${rowIndex}`, {
|
||||
locale,
|
||||
value: "",
|
||||
});
|
||||
setValue("key", translationKey);
|
||||
});
|
||||
}, [combinedLocales, translationKey, setValue]);
|
||||
|
||||
const handleOk = () => {
|
||||
const formData = getValues();
|
||||
onTranslationsAdded(formData);
|
||||
toggleDialog();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
variant={ModalVariant.medium}
|
||||
title={t("addTranslationsModalTitle")}
|
||||
isOpen
|
||||
onClose={toggleDialog}
|
||||
actions={[
|
||||
<Button
|
||||
key="ok"
|
||||
data-testid="okTranslationBtn"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
form="add-translation"
|
||||
isDisabled={!isValid}
|
||||
>
|
||||
{t("addTranslationDialogOkBtn")}
|
||||
</Button>,
|
||||
<Button
|
||||
key="cancel"
|
||||
data-testid="cancelTranslationBtn"
|
||||
variant="link"
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Flex
|
||||
direction={{ default: "column" }}
|
||||
spaceItems={{ default: "spaceItemsNone" }}
|
||||
>
|
||||
<FlexItem>
|
||||
<TextContent>
|
||||
<Text component={TextVariants.p}>
|
||||
{t("addTranslationsModalSubTitle")}{" "}
|
||||
<strong>{t("addTranslationsModalSubTitleBolded")}</strong>
|
||||
</Text>
|
||||
</TextContent>
|
||||
</FlexItem>
|
||||
<FlexItem>
|
||||
<FormProvider {...form}>
|
||||
<Form
|
||||
id="add-translation"
|
||||
data-testid="addTranslationForm"
|
||||
onSubmit={handleSubmit(handleOk)}
|
||||
>
|
||||
<TextControl
|
||||
name="key"
|
||||
label={t("translationKey")}
|
||||
className="pf-u-mt-md"
|
||||
data-testid="translation-key"
|
||||
isDisabled
|
||||
/>
|
||||
<FlexItem>
|
||||
<TextContent>
|
||||
<Text
|
||||
className="pf-u-font-size-sm pf-u-font-weight-bold"
|
||||
component={TextVariants.p}
|
||||
>
|
||||
{t("translationsTableHeading")}
|
||||
</Text>
|
||||
</TextContent>
|
||||
<PaginatingTableToolbar
|
||||
count={combinedLocales.length}
|
||||
first={first}
|
||||
max={max}
|
||||
onNextClick={setFirst}
|
||||
onPreviousClick={setFirst}
|
||||
onPerPageSelect={(first, max) => {
|
||||
setFirst(first);
|
||||
setMax(max);
|
||||
}}
|
||||
inputGroupName={"search"}
|
||||
inputGroupOnEnter={(search) => {
|
||||
setFilter(search);
|
||||
setFirst(0);
|
||||
setMax(10);
|
||||
}}
|
||||
inputGroupPlaceholder={t("searchForLanguage")}
|
||||
>
|
||||
{filteredLocales.length === 0 && !filter && (
|
||||
<ListEmptyState
|
||||
hasIcon
|
||||
message={t("noLanguages")}
|
||||
instructions={t("noLanguagesInstructions")}
|
||||
/>
|
||||
)}
|
||||
{filteredLocales.length === 0 && filter && (
|
||||
<ListEmptyState
|
||||
hasIcon
|
||||
icon={SearchIcon}
|
||||
isSearchVariant
|
||||
message={t("noSearchResults")}
|
||||
instructions={t("noLanguagesSearchResultsInstructions")}
|
||||
/>
|
||||
)}
|
||||
{filteredLocales.length !== 0 && (
|
||||
<Table
|
||||
aria-label={t("addTranslationsDialogRowsTable")}
|
||||
data-testid="add-translations-dialog-rows-table"
|
||||
>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th className="pf-u-py-lg">
|
||||
{t("supportedLanguagesTableColumnName")}
|
||||
</Th>
|
||||
<Th className="pf-u-py-lg">
|
||||
{t("translationTableColumnName")}
|
||||
</Th>
|
||||
<Th aria-hidden="true" />
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{filteredLocales.map((locale, rowIndex) => (
|
||||
<Tr key={rowIndex}>
|
||||
<Td
|
||||
className="pf-m-sm pf-u-px-sm"
|
||||
dataLabel={t("supportedLanguage")}
|
||||
>
|
||||
<FormGroup fieldId="kc-supportedLanguage">
|
||||
{localeToDisplayName(
|
||||
locale,
|
||||
whoAmI.getLocale(),
|
||||
)}
|
||||
{locale === defaultLocales.toString() && (
|
||||
<Label className="pf-u-ml-xs" color="blue">
|
||||
{t("defaultLanguage")}
|
||||
</Label>
|
||||
)}
|
||||
</FormGroup>
|
||||
</Td>
|
||||
<Td>
|
||||
{locale === defaultLocales.toString() && (
|
||||
<TextControl
|
||||
name={`translations.${rowIndex}.value`}
|
||||
label={t("translationValue")}
|
||||
data-testid="translation-value"
|
||||
rules={{
|
||||
required: {
|
||||
value: true,
|
||||
message: t("required"),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{locale !== defaultLocales.toString() && (
|
||||
<TextControl
|
||||
name={`translations.${rowIndex}.value`}
|
||||
label={t("translationValue")}
|
||||
data-testid="translation-value"
|
||||
/>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
)}
|
||||
</PaginatingTableToolbar>
|
||||
</FlexItem>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
</Modal>
|
||||
);
|
||||
};
|
|
@ -1,36 +1,38 @@
|
|||
import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientScopeRepresentation";
|
||||
import type { UserProfileConfig } from "@keycloak/keycloak-admin-client/lib/defs/userProfileMetadata";
|
||||
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
FormGroup,
|
||||
Grid,
|
||||
GridItem,
|
||||
Radio,
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
Switch,
|
||||
Tooltip,
|
||||
} from "@patternfly/react-core";
|
||||
import { isEqual } from "lodash-es";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Controller,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
useWatch,
|
||||
} from "react-hook-form";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { HelpItem } from "ui-shared";
|
||||
|
||||
import { adminClient } from "../../../admin-client";
|
||||
import { DefaultSwitchControl } from "../../../components/SwitchControl";
|
||||
import { FormAccess } from "../../../components/form/FormAccess";
|
||||
import { KeycloakSpinner } from "../../../components/keycloak-spinner/KeycloakSpinner";
|
||||
import { KeycloakTextInput } from "../../../components/keycloak-text-input/KeycloakTextInput";
|
||||
import { useFetch } from "../../../utils/useFetch";
|
||||
import { useParams } from "../../../utils/useParams";
|
||||
import { USERNAME_EMAIL } from "../../NewAttributeSettings";
|
||||
import type { AttributeParams } from "../../routes/Attribute";
|
||||
|
||||
import "../../realm-settings-section.css";
|
||||
import { GlobeRouteIcon } from "@patternfly/react-icons";
|
||||
import { AddTranslationsDialog } from "./AddTranslationsDialog";
|
||||
import useToggle from "../../../utils/useToggle";
|
||||
import { AttributeParams } from "../../routes/Attribute";
|
||||
import { useRealm } from "../../../context/realm-context/RealmContext";
|
||||
|
||||
const REQUIRED_FOR = [
|
||||
{ label: "requiredForLabel.both", value: ["admin", "user"] },
|
||||
|
@ -38,9 +40,29 @@ const REQUIRED_FOR = [
|
|||
{ label: "requiredForLabel.admins", value: ["admin"] },
|
||||
] as const;
|
||||
|
||||
export const AttributeGeneralSettings = () => {
|
||||
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) => {
|
||||
const { t } = useTranslation();
|
||||
const { realm: realmName } = useRealm();
|
||||
const form = useFormContext();
|
||||
const tooltipRef = useRef();
|
||||
const [clientScopes, setClientScopes] =
|
||||
useState<ClientScopeRepresentation[]>();
|
||||
const [config, setConfig] = useState<UserProfileConfig>();
|
||||
|
@ -48,8 +70,31 @@ export const AttributeGeneralSettings = () => {
|
|||
const [selectRequiredForOpen, setSelectRequiredForOpen] = useState(false);
|
||||
const [isAttributeGroupDropdownOpen, setIsAttributeGroupDropdownOpen] =
|
||||
useState(false);
|
||||
const [addTranslationsModalOpen, toggleModal] = useToggle();
|
||||
const { attributeName } = useParams<AttributeParams>();
|
||||
const editMode = attributeName ? true : false;
|
||||
const [realm, setRealm] = useState<RealmRepresentation>();
|
||||
const [newAttributeName, setNewAttributeName] = useState("");
|
||||
const [generatedDisplayName, setGeneratedDisplayName] = useState("");
|
||||
const [translationsData, setTranslationsData] = useState<Translations>({
|
||||
key: "",
|
||||
translations: [],
|
||||
});
|
||||
const displayNameRegex = /\$\{([^}]+)\}/;
|
||||
|
||||
const handleAttributeNameChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
const newAttributeName = event.target.value;
|
||||
setNewAttributeName(newAttributeName);
|
||||
|
||||
const newDisplayName =
|
||||
newAttributeName !== "" && realm?.internationalizationEnabled
|
||||
? "${profile.attributes." + `${newAttributeName}}`
|
||||
: "";
|
||||
|
||||
setGeneratedDisplayName(newDisplayName);
|
||||
};
|
||||
|
||||
const hasSelector = useWatch({
|
||||
control: form.control,
|
||||
|
@ -67,9 +112,40 @@ export const AttributeGeneralSettings = () => {
|
|||
defaultValue: false,
|
||||
});
|
||||
|
||||
const attributeDisplayName = useWatch({
|
||||
control: form.control,
|
||||
name: "displayName",
|
||||
});
|
||||
|
||||
const displayNamePatternMatch = displayNameRegex.test(attributeDisplayName);
|
||||
|
||||
useFetch(
|
||||
() => adminClient.realms.findOne({ realm: realmName }),
|
||||
(realm) => {
|
||||
if (!realm) {
|
||||
throw new Error(t("notFound"));
|
||||
}
|
||||
setRealm(realm);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
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 <KeycloakSpinner />;
|
||||
}
|
||||
|
@ -82,9 +158,33 @@ export const AttributeGeneralSettings = () => {
|
|||
form.setValue("hasRequiredScopes", hasRequiredScopes);
|
||||
}
|
||||
|
||||
const handleTranslationsAdded = (translationsData: Translations) => {
|
||||
setTranslationsData(translationsData);
|
||||
};
|
||||
|
||||
const handleToggleDialog = () => {
|
||||
toggleModal();
|
||||
handleTranslationsData(translationsData);
|
||||
handleGeneratedDisplayName(generatedDisplayName);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormAccess role="manage-realm" isHorizontal>
|
||||
<FormProvider {...form}>
|
||||
<>
|
||||
{addTranslationsModalOpen && (
|
||||
<AddTranslationsDialog
|
||||
translationKey={
|
||||
editMode
|
||||
? attributeDisplayName
|
||||
: "${profile.attributes." + `${newAttributeName}}`
|
||||
}
|
||||
onTranslationsAdded={handleTranslationsAdded}
|
||||
toggleDialog={handleToggleDialog}
|
||||
onCancel={() => {
|
||||
toggleModal();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<FormAccess role="manage-realm" isHorizontal>
|
||||
<FormGroup
|
||||
label={t("attributeName")}
|
||||
labelIcon={
|
||||
|
@ -106,6 +206,7 @@ export const AttributeGeneralSettings = () => {
|
|||
isDisabled={editMode}
|
||||
validated={form.formState.errors.name ? "error" : "default"}
|
||||
{...form.register("name", { required: true })}
|
||||
onChange={handleAttributeNameChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
|
@ -118,18 +219,48 @@ export const AttributeGeneralSettings = () => {
|
|||
}
|
||||
fieldId="kc-attribute-display-name"
|
||||
>
|
||||
<KeycloakTextInput
|
||||
id="kc-attribute-display-name"
|
||||
defaultValue=""
|
||||
data-testid="attribute-display-name"
|
||||
{...form.register("displayName")}
|
||||
/>
|
||||
<Grid hasGutter>
|
||||
<GridItem span={realm?.internationalizationEnabled ? 11 : 12}>
|
||||
<KeycloakTextInput
|
||||
id="kc-attribute-display-name"
|
||||
data-testid="attribute-display-name"
|
||||
isDisabled={
|
||||
(realm?.internationalizationEnabled &&
|
||||
newAttributeName !== "") ||
|
||||
(editMode && displayNamePatternMatch)
|
||||
}
|
||||
value={
|
||||
editMode
|
||||
? attributeDisplayName
|
||||
: realm?.internationalizationEnabled
|
||||
? generatedDisplayName
|
||||
: undefined
|
||||
}
|
||||
{...form.register("displayName")}
|
||||
/>
|
||||
</GridItem>
|
||||
{realm?.internationalizationEnabled && (
|
||||
<GridItem span={1}>
|
||||
<Button
|
||||
ref={tooltipRef}
|
||||
variant="link"
|
||||
className="pf-m-plain kc-attribute-display-name-iconBtn"
|
||||
data-testid="addAttributeTranslationBtn"
|
||||
aria-label={t("addAttributeTranslationBtn")}
|
||||
isDisabled={!newAttributeName && !editMode}
|
||||
onClick={() => {
|
||||
toggleModal();
|
||||
}}
|
||||
icon={<GlobeRouteIcon />}
|
||||
/>
|
||||
<Tooltip
|
||||
content={t("addAttributeTranslationTooltip")}
|
||||
reference={tooltipRef}
|
||||
/>
|
||||
</GridItem>
|
||||
)}
|
||||
</Grid>
|
||||
</FormGroup>
|
||||
<DefaultSwitchControl
|
||||
name="multivalued"
|
||||
label={t("multivalued")}
|
||||
labelIcon={t("multivaluedHelp")}
|
||||
/>
|
||||
<FormGroup
|
||||
label={t("attributeGroup")}
|
||||
labelIcon={
|
||||
|
@ -407,7 +538,7 @@ export const AttributeGeneralSettings = () => {
|
|||
)}
|
||||
</>
|
||||
)}
|
||||
</FormProvider>
|
||||
</FormAccess>
|
||||
</FormAccess>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue