Localization: Realm Overrides Fixes (#26169)
* resolved conflict Signed-off-by: Agnieszka Gancarczyk <agancarc@redhat.com> * improvements Signed-off-by: Agnieszka Gancarczyk <agancarc@redhat.com> * improved tests Signed-off-by: Agnieszka Gancarczyk <agancarc@redhat.com> * feedback Signed-off-by: Agnieszka Gancarczyk <agancarc@redhat.com> * test fix Signed-off-by: Agnieszka Gancarczyk <agancarc@redhat.com> * test fix Signed-off-by: Agnieszka Gancarczyk <agancarc@redhat.com> * resolved conflict Signed-off-by: Agnieszka Gancarczyk <agancarc@redhat.com> * fixed test 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
e83cc12664
commit
33651c396c
7 changed files with 43 additions and 675 deletions
|
@ -206,7 +206,8 @@ describe("Realm settings tabs tests", () => {
|
||||||
.contains("td", "123")
|
.contains("td", "123")
|
||||||
.should("be.visible");
|
.should("be.visible");
|
||||||
|
|
||||||
cy.findByTestId("realmOverrides-deleteKebabToggle").click();
|
cy.get('td.pf-c-table__action button[aria-label="Actions"]').click();
|
||||||
|
cy.contains("button", "Delete").click();
|
||||||
cy.findByTestId("confirm").click();
|
cy.findByTestId("confirm").click();
|
||||||
masthead.checkNotificationMessage(
|
masthead.checkNotificationMessage(
|
||||||
"Successfully removed message(s) from the bundle.",
|
"Successfully removed message(s) from the bundle.",
|
||||||
|
@ -253,7 +254,8 @@ describe("Realm settings tabs tests", () => {
|
||||||
.contains("td", "def")
|
.contains("td", "def")
|
||||||
.should("be.visible");
|
.should("be.visible");
|
||||||
|
|
||||||
cy.findByTestId("realmOverrides-deleteKebabToggle").click();
|
cy.get('td.pf-c-table__action button[aria-label="Actions"]').click();
|
||||||
|
cy.contains("button", "Delete").click();
|
||||||
cy.findByTestId("confirm").click();
|
cy.findByTestId("confirm").click();
|
||||||
|
|
||||||
masthead.checkNotificationMessage(
|
masthead.checkNotificationMessage(
|
||||||
|
@ -343,7 +345,7 @@ describe("Realm settings tabs tests", () => {
|
||||||
it("Check a11y violations on localization realm overrides sub tab/ adding message bundle", () => {
|
it("Check a11y violations on localization realm overrides sub tab/ adding message bundle", () => {
|
||||||
realmSettingsPage.goToLocalizationTab();
|
realmSettingsPage.goToLocalizationTab();
|
||||||
realmSettingsPage.goToLocalizationRealmOverridesSubTab();
|
realmSettingsPage.goToLocalizationRealmOverridesSubTab();
|
||||||
cy.findByTestId("add-bundle-button").click();
|
cy.findByTestId("add-translationBtn").click();
|
||||||
cy.checkA11y();
|
cy.checkA11y();
|
||||||
modalUtils.cancelModal();
|
modalUtils.cancelModal();
|
||||||
});
|
});
|
||||||
|
|
|
@ -95,7 +95,7 @@ export default class RealmSettingsPage extends CommonPage {
|
||||||
testConnectionButton = "test-connection-button";
|
testConnectionButton = "test-connection-button";
|
||||||
modalTestConnectionButton = "modal-test-connection-button";
|
modalTestConnectionButton = "modal-test-connection-button";
|
||||||
emailAddressInput = "email-address-input";
|
emailAddressInput = "email-address-input";
|
||||||
addBundleButton = "add-bundle-button";
|
addBundleButton = "add-translationBtn";
|
||||||
confirmAddBundle = "add-bundle-confirm-button";
|
confirmAddBundle = "add-bundle-confirm-button";
|
||||||
keyInput = "key-input";
|
keyInput = "key-input";
|
||||||
valueInput = "value-input";
|
valueInput = "value-input";
|
||||||
|
|
|
@ -964,7 +964,7 @@ deleteMappingConfirm=Are you sure you want to delete this mapping?
|
||||||
createClientProfileSuccess=New client profile created
|
createClientProfileSuccess=New client profile created
|
||||||
eventTypes.CLIENT_LOGIN_ERROR.description=Client login error
|
eventTypes.CLIENT_LOGIN_ERROR.description=Client login error
|
||||||
explainBearerOnly=This is a special OIDC type. This client only allows bearer token requests and cannot participate in browser logins.
|
explainBearerOnly=This is a special OIDC type. This client only allows bearer token requests and cannot participate in browser logins.
|
||||||
noMessageBundlesInstructions=Add a message bundle to get started.
|
noTranslationsInstructions=Add a translation to get started.
|
||||||
clearFile=Clear this file
|
clearFile=Clear this file
|
||||||
allowCreate=Allow create
|
allowCreate=Allow create
|
||||||
providerUpdatedError=Could not update client policy due to {{error}}
|
providerUpdatedError=Could not update client policy due to {{error}}
|
||||||
|
@ -2632,7 +2632,7 @@ minus=Minus
|
||||||
groupsHelp=Groups where the user has membership. To leave a group, select it and click Leave.
|
groupsHelp=Groups where the user has membership. To leave a group, select it and click Leave.
|
||||||
includeGroupsAndRoles=Include groups and roles
|
includeGroupsAndRoles=Include groups and roles
|
||||||
groupsPermissionsHint=Determines if fine grained permissions are enabled for managing this role. Disabling will delete all current permissions that have been set up.
|
groupsPermissionsHint=Determines if fine grained permissions are enabled for managing this role. Disabling will delete all current permissions that have been set up.
|
||||||
searchForMessageBundle=Search for message bundle
|
searchForTranslation=Search for translation
|
||||||
offlineSessionMaxHelp=Max time before an offline session is expired regardless of activity.
|
offlineSessionMaxHelp=Max time before an offline session is expired regardless of activity.
|
||||||
resourceSaveError=Could not persist resource due to {{error}}
|
resourceSaveError=Could not persist resource due to {{error}}
|
||||||
clientsClientScopesHelp=The scopes associated with this resource.
|
clientsClientScopesHelp=The scopes associated with this resource.
|
||||||
|
@ -2673,7 +2673,7 @@ policyType.totp=Time based
|
||||||
addAttribute=Add {{label}}
|
addAttribute=Add {{label}}
|
||||||
clientScopeSearch.protocol=Protocol
|
clientScopeSearch.protocol=Protocol
|
||||||
initialAccessTokenDetails=Initial access token details
|
initialAccessTokenDetails=Initial access token details
|
||||||
noMessageBundles=No message bundles
|
noTranslations=No translations
|
||||||
deleteProvider=Delete provider?
|
deleteProvider=Delete provider?
|
||||||
inputTypeSize=Input size
|
inputTypeSize=Input size
|
||||||
createAttributeSubTitle=Create a new attribute
|
createAttributeSubTitle=Create a new attribute
|
||||||
|
@ -2984,3 +2984,5 @@ joinCommunity=Join community
|
||||||
readBlog=Read blog
|
readBlog=Read blog
|
||||||
customValue=Custom value
|
customValue=Custom value
|
||||||
termsAndConditionsUserAttribute=Terms and conditions accepted timestamp
|
termsAndConditionsUserAttribute=Terms and conditions accepted timestamp
|
||||||
|
realmOverridesDescription= Realm overrides allow you to specify translations that will take effect for the entire realm. These translations will override any translation specified by a theme.
|
||||||
|
addTranslation=Add translation
|
|
@ -951,7 +951,7 @@ deleteMappingConfirm=Czy na pewno chcesz usunąć to odwzorowanie?
|
||||||
createClientProfileSuccess=Utworzono nowy profil klienta
|
createClientProfileSuccess=Utworzono nowy profil klienta
|
||||||
eventTypes.CLIENT_LOGIN_ERROR.description=Błąd logowania klienta
|
eventTypes.CLIENT_LOGIN_ERROR.description=Błąd logowania klienta
|
||||||
explainBearerOnly=To jest specjalny typ OIDC. Ten klient pozwala tylko na żądania tokenów dostępu i nie może uczestniczyć w logowaniach przeglądarki.
|
explainBearerOnly=To jest specjalny typ OIDC. Ten klient pozwala tylko na żądania tokenów dostępu i nie może uczestniczyć w logowaniach przeglądarki.
|
||||||
noMessageBundlesInstructions=Dodaj pakiet wiadomości, aby rozpocząć.
|
noTranslationsInstructions=Dodaj tłumaczenie, aby rozpocząć.
|
||||||
clearFile=Wyczyść ten plik
|
clearFile=Wyczyść ten plik
|
||||||
allowCreate=Zezwól na tworzenie
|
allowCreate=Zezwól na tworzenie
|
||||||
providerUpdatedError=Nie można zaktualizować polityki klienta z powodu błędu: {{error}}
|
providerUpdatedError=Nie można zaktualizować polityki klienta z powodu błędu: {{error}}
|
||||||
|
@ -2644,7 +2644,7 @@ policyType.totp=Oparte na czasie
|
||||||
addAttribute=Dodaj atrybut
|
addAttribute=Dodaj atrybut
|
||||||
clientScopeSearch.protocol=Protokół
|
clientScopeSearch.protocol=Protokół
|
||||||
initialAccessTokenDetails=Informacje o tokenie początkowym dostępu
|
initialAccessTokenDetails=Informacje o tokenie początkowym dostępu
|
||||||
noMessageBundles=Brak pakietów wiadomości
|
noTranslations=Brak tłuamczeń
|
||||||
deleteProvider=Usunąć dostawcę?
|
deleteProvider=Usunąć dostawcę?
|
||||||
inputTypeSize=Rozmiar pola wejściowego
|
inputTypeSize=Rozmiar pola wejściowego
|
||||||
createAttributeSubTitle=Utwórz nowy atrybut
|
createAttributeSubTitle=Utwórz nowy atrybut
|
||||||
|
@ -2916,3 +2916,4 @@ invalidEmailMessage='{{0}}': Nieprawidłowy adres e-mail.
|
||||||
missingLastNameMessage='{{0}}': Proszę podać nazwisko.
|
missingLastNameMessage='{{0}}': Proszę podać nazwisko.
|
||||||
missingEmailMessage='{{0}}': Proszę podać adres e-mail.
|
missingEmailMessage='{{0}}': Proszę podać adres e-mail.
|
||||||
missingPasswordMessage='{{0}}': Proszę podać hasło.
|
missingPasswordMessage='{{0}}': Proszę podać hasło.
|
||||||
|
addTranslation=Dodaj tłumaczenie
|
||||||
|
|
|
@ -1,625 +0,0 @@
|
||||||
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
|
|
||||||
import {
|
|
||||||
ActionGroup,
|
|
||||||
AlertVariant,
|
|
||||||
Button,
|
|
||||||
Divider,
|
|
||||||
FormGroup,
|
|
||||||
PageSection,
|
|
||||||
Select,
|
|
||||||
SelectGroup,
|
|
||||||
SelectOption,
|
|
||||||
SelectVariant,
|
|
||||||
Switch,
|
|
||||||
TextContent,
|
|
||||||
ToolbarItem,
|
|
||||||
} from "@patternfly/react-core";
|
|
||||||
import { SearchIcon } from "@patternfly/react-icons";
|
|
||||||
import {
|
|
||||||
EditableTextCell,
|
|
||||||
IEditableTextCell,
|
|
||||||
IRow,
|
|
||||||
IRowCell,
|
|
||||||
RowEditType,
|
|
||||||
RowErrors,
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableHeader,
|
|
||||||
TableVariant,
|
|
||||||
applyCellEdits,
|
|
||||||
cancelCellEdits,
|
|
||||||
validateCellEdits,
|
|
||||||
} from "@patternfly/react-table";
|
|
||||||
import { cloneDeep, isEqual, uniqWith } from "lodash-es";
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
|
||||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { FormPanel, HelpItem } from "ui-shared";
|
|
||||||
import { adminClient } from "../admin-client";
|
|
||||||
import { useAlerts } from "../components/alert/Alerts";
|
|
||||||
import { FormAccess } from "../components/form/FormAccess";
|
|
||||||
import type { KeyValueType } from "../components/key-value-form/key-value-convert";
|
|
||||||
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
|
||||||
import { PaginatingTableToolbar } from "../components/table-toolbar/PaginatingTableToolbar";
|
|
||||||
import { useRealm } from "../context/realm-context/RealmContext";
|
|
||||||
import { useServerInfo } from "../context/server-info/ServerInfoProvider";
|
|
||||||
import { useWhoAmI } from "../context/whoami/WhoAmI";
|
|
||||||
import { DEFAULT_LOCALE } from "../i18n/i18n";
|
|
||||||
import { convertToFormValues, localeToDisplayName } from "../util";
|
|
||||||
import { useFetch } from "../utils/useFetch";
|
|
||||||
import useLocaleSort, { mapByKey } from "../utils/useLocaleSort";
|
|
||||||
import { AddMessageBundleModal } from "./AddMessageBundleModal";
|
|
||||||
|
|
||||||
type LocalizationTabProps = {
|
|
||||||
save: (realm: RealmRepresentation) => void;
|
|
||||||
refresh: () => void;
|
|
||||||
realm: RealmRepresentation;
|
|
||||||
};
|
|
||||||
|
|
||||||
type LocaleSpecificEntry = {
|
|
||||||
key: string;
|
|
||||||
value: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum RowEditAction {
|
|
||||||
Save = "save",
|
|
||||||
Cancel = "cancel",
|
|
||||||
Edit = "edit",
|
|
||||||
Delete = "delete",
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BundleForm = {
|
|
||||||
key: string;
|
|
||||||
value: string;
|
|
||||||
messageBundle: KeyValueType;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LocalizationTab = ({ save, realm }: LocalizationTabProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [addMessageBundleModalOpen, setAddMessageBundleModalOpen] =
|
|
||||||
useState(false);
|
|
||||||
|
|
||||||
const [supportedLocalesOpen, setSupportedLocalesOpen] = useState(false);
|
|
||||||
const [defaultLocaleOpen, setDefaultLocaleOpen] = useState(false);
|
|
||||||
const [filterDropdownOpen, setFilterDropdownOpen] = useState(false);
|
|
||||||
const [selectMenuLocale, setSelectMenuLocale] = useState(DEFAULT_LOCALE);
|
|
||||||
|
|
||||||
const { setValue, getValues, control, handleSubmit, formState } = useForm();
|
|
||||||
const [selectMenuValueSelected, setSelectMenuValueSelected] = useState(false);
|
|
||||||
const [messageBundles, setMessageBundles] = useState<LocaleSpecificEntry[]>(
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
const [tableRows, setTableRows] = useState<IRow[]>([]);
|
|
||||||
|
|
||||||
const themeTypes = useServerInfo().themes!;
|
|
||||||
const allLocales = useMemo(() => {
|
|
||||||
const locales = Object.values(themeTypes).flatMap((theme) =>
|
|
||||||
theme.flatMap(({ locales }) => (locales ? locales : [])),
|
|
||||||
);
|
|
||||||
return Array.from(new Set(locales));
|
|
||||||
}, [themeTypes]);
|
|
||||||
const bundleForm = useForm<BundleForm>({ mode: "onChange" });
|
|
||||||
const { addAlert, addError } = useAlerts();
|
|
||||||
const { realm: currentRealm } = useRealm();
|
|
||||||
const { whoAmI } = useWhoAmI();
|
|
||||||
const localeSort = useLocaleSort();
|
|
||||||
|
|
||||||
const defaultSupportedLocales = realm.supportedLocales?.length
|
|
||||||
? realm.supportedLocales
|
|
||||||
: [DEFAULT_LOCALE];
|
|
||||||
|
|
||||||
const setupForm = () => {
|
|
||||||
convertToFormValues(realm, setValue);
|
|
||||||
setValue("supportedLocales", defaultSupportedLocales);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(setupForm, []);
|
|
||||||
|
|
||||||
const watchSupportedLocales: string[] = useWatch({
|
|
||||||
control,
|
|
||||||
name: "supportedLocales",
|
|
||||||
defaultValue: defaultSupportedLocales,
|
|
||||||
});
|
|
||||||
const internationalizationEnabled = useWatch({
|
|
||||||
control,
|
|
||||||
name: "internationalizationEnabled",
|
|
||||||
defaultValue: realm.internationalizationEnabled,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [tableKey, setTableKey] = useState(0);
|
|
||||||
const [max, setMax] = useState(10);
|
|
||||||
const [first, setFirst] = useState(0);
|
|
||||||
const [filter, setFilter] = useState("");
|
|
||||||
|
|
||||||
const refreshTable = () => {
|
|
||||||
setTableKey(tableKey + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
useFetch(
|
|
||||||
async () => {
|
|
||||||
let result = await adminClient.realms
|
|
||||||
.getRealmLocalizationTexts({
|
|
||||||
first,
|
|
||||||
max,
|
|
||||||
realm: realm.realm!,
|
|
||||||
selectedLocale:
|
|
||||||
selectMenuLocale ||
|
|
||||||
getValues("defaultLocale") ||
|
|
||||||
whoAmI.getLocale(),
|
|
||||||
})
|
|
||||||
// prevents server error in dev mode due to snowpack
|
|
||||||
.catch(() => []);
|
|
||||||
|
|
||||||
const searchInBundles = (idx: number) => {
|
|
||||||
return Object.entries(result).filter((i) => i[idx].includes(filter));
|
|
||||||
};
|
|
||||||
|
|
||||||
if (filter) {
|
|
||||||
const filtered = uniqWith(
|
|
||||||
searchInBundles(0).concat(searchInBundles(1)),
|
|
||||||
isEqual,
|
|
||||||
);
|
|
||||||
|
|
||||||
result = Object.fromEntries(filtered);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { result };
|
|
||||||
},
|
|
||||||
({ result }) => {
|
|
||||||
const bundles = localeSort(
|
|
||||||
Object.entries(result).map<LocaleSpecificEntry>(([key, value]) => ({
|
|
||||||
key,
|
|
||||||
value,
|
|
||||||
})),
|
|
||||||
mapByKey("key"),
|
|
||||||
).slice(first, first + max + 1);
|
|
||||||
|
|
||||||
setMessageBundles(bundles);
|
|
||||||
|
|
||||||
const updatedRows = bundles.map<IRow>((messageBundle) => ({
|
|
||||||
rowEditBtnAriaLabel: () =>
|
|
||||||
t("rowEditBtnAriaLabel", {
|
|
||||||
messageBundle: messageBundle.value,
|
|
||||||
}),
|
|
||||||
rowSaveBtnAriaLabel: () =>
|
|
||||||
t("rowSaveBtnAriaLabel", {
|
|
||||||
messageBundle: messageBundle.value,
|
|
||||||
}),
|
|
||||||
rowCancelBtnAriaLabel: () =>
|
|
||||||
t("rowCancelBtnAriaLabel", {
|
|
||||||
messageBundle: messageBundle.value,
|
|
||||||
}),
|
|
||||||
cells: [
|
|
||||||
{
|
|
||||||
title: (value, rowIndex, cellIndex, props) => (
|
|
||||||
<EditableTextCell
|
|
||||||
value={value!}
|
|
||||||
rowIndex={rowIndex!}
|
|
||||||
cellIndex={cellIndex!}
|
|
||||||
props={props}
|
|
||||||
isDisabled
|
|
||||||
handleTextInputChange={handleTextInputChange}
|
|
||||||
inputAriaLabel={messageBundle.key}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
props: {
|
|
||||||
value: messageBundle.key,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: (value, rowIndex, cellIndex, props) => (
|
|
||||||
<EditableTextCell
|
|
||||||
value={value!}
|
|
||||||
rowIndex={rowIndex!}
|
|
||||||
cellIndex={cellIndex!}
|
|
||||||
props={props}
|
|
||||||
handleTextInputChange={handleTextInputChange}
|
|
||||||
inputAriaLabel={messageBundle.value}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
props: {
|
|
||||||
value: messageBundle.value,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
setTableRows(updatedRows);
|
|
||||||
|
|
||||||
return bundles;
|
|
||||||
},
|
|
||||||
[tableKey, filter, first, max],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleTextInputChange = (
|
|
||||||
newValue: string,
|
|
||||||
evt: any,
|
|
||||||
rowIndex: number,
|
|
||||||
cellIndex: number,
|
|
||||||
) => {
|
|
||||||
setTableRows((prev) => {
|
|
||||||
const newRows = cloneDeep(prev);
|
|
||||||
const textCell = newRows[rowIndex]?.cells?.[
|
|
||||||
cellIndex
|
|
||||||
] as IEditableTextCell;
|
|
||||||
textCell.props.editableValue = newValue;
|
|
||||||
return newRows;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateEditableRows = async (
|
|
||||||
type: RowEditType,
|
|
||||||
rowIndex?: number,
|
|
||||||
validationErrors?: RowErrors,
|
|
||||||
) => {
|
|
||||||
if (rowIndex === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newRows = cloneDeep(tableRows);
|
|
||||||
let newRow: IRow;
|
|
||||||
const invalid =
|
|
||||||
!!validationErrors && Object.keys(validationErrors).length > 0;
|
|
||||||
|
|
||||||
if (invalid) {
|
|
||||||
newRow = validateCellEdits(newRows[rowIndex], type, validationErrors);
|
|
||||||
} else if (type === RowEditAction.Cancel) {
|
|
||||||
newRow = cancelCellEdits(newRows[rowIndex]);
|
|
||||||
} else {
|
|
||||||
newRow = applyCellEdits(newRows[rowIndex], type);
|
|
||||||
}
|
|
||||||
newRows[rowIndex] = newRow;
|
|
||||||
|
|
||||||
// Update the copy of the retrieved data set so we can save it when the user saves changes
|
|
||||||
|
|
||||||
if (!invalid && type === RowEditAction.Save) {
|
|
||||||
const key = (newRow.cells?.[0] as IRowCell).props.value;
|
|
||||||
const value = (newRow.cells?.[1] as IRowCell).props.value;
|
|
||||||
|
|
||||||
// We only have one editable value, otherwise we'd need to save each
|
|
||||||
try {
|
|
||||||
await adminClient.realms.addLocalization(
|
|
||||||
{
|
|
||||||
realm: realm.realm!,
|
|
||||||
selectedLocale:
|
|
||||||
selectMenuLocale || getValues("defaultLocale") || DEFAULT_LOCALE,
|
|
||||||
key,
|
|
||||||
},
|
|
||||||
value,
|
|
||||||
);
|
|
||||||
addAlert(t("updateMessageBundleSuccess"), AlertVariant.success);
|
|
||||||
} catch (error) {
|
|
||||||
addAlert(t("updateMessageBundleError"), AlertVariant.danger);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setTableRows(newRows);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleModalToggle = () => {
|
|
||||||
setAddMessageBundleModalOpen(!addMessageBundleModalOpen);
|
|
||||||
};
|
|
||||||
|
|
||||||
const options = [
|
|
||||||
<SelectGroup label={t("defaultLocale")} key="group1">
|
|
||||||
<SelectOption key={DEFAULT_LOCALE} value={DEFAULT_LOCALE}>
|
|
||||||
{localeToDisplayName(DEFAULT_LOCALE, whoAmI.getLocale())}
|
|
||||||
</SelectOption>
|
|
||||||
</SelectGroup>,
|
|
||||||
<Divider key="divider" />,
|
|
||||||
<SelectGroup label={t("supportedLocales")} key="group2">
|
|
||||||
{watchSupportedLocales.map((locale) => (
|
|
||||||
<SelectOption key={locale} value={locale}>
|
|
||||||
{localeToDisplayName(locale, whoAmI.getLocale())}
|
|
||||||
</SelectOption>
|
|
||||||
))}
|
|
||||||
</SelectGroup>,
|
|
||||||
];
|
|
||||||
|
|
||||||
const addKeyValue = async (pair: KeyValueType): Promise<void> => {
|
|
||||||
try {
|
|
||||||
await adminClient.realms.addLocalization(
|
|
||||||
{
|
|
||||||
realm: currentRealm!,
|
|
||||||
selectedLocale:
|
|
||||||
selectMenuLocale || getValues("defaultLocale") || DEFAULT_LOCALE,
|
|
||||||
key: pair.key,
|
|
||||||
},
|
|
||||||
pair.value,
|
|
||||||
);
|
|
||||||
|
|
||||||
adminClient.setConfig({
|
|
||||||
realmName: currentRealm!,
|
|
||||||
});
|
|
||||||
refreshTable();
|
|
||||||
addAlert(t("addMessageBundleSuccess"), AlertVariant.success);
|
|
||||||
} catch (error) {
|
|
||||||
addError(t("addMessageBundleError"), error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteKey = async (key: string) => {
|
|
||||||
try {
|
|
||||||
await adminClient.realms.deleteRealmLocalizationTexts({
|
|
||||||
realm: currentRealm!,
|
|
||||||
selectedLocale: selectMenuLocale,
|
|
||||||
key,
|
|
||||||
});
|
|
||||||
refreshTable();
|
|
||||||
addAlert(t("deleteMessageBundleSuccess"));
|
|
||||||
} catch (error) {
|
|
||||||
addError("deleteMessageBundleError", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{addMessageBundleModalOpen && (
|
|
||||||
<AddMessageBundleModal
|
|
||||||
handleModalToggle={handleModalToggle}
|
|
||||||
save={(pair: any) => {
|
|
||||||
addKeyValue(pair);
|
|
||||||
handleModalToggle();
|
|
||||||
}}
|
|
||||||
form={bundleForm}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<PageSection variant="light">
|
|
||||||
<FormAccess
|
|
||||||
isHorizontal
|
|
||||||
role="manage-realm"
|
|
||||||
className="pf-u-mt-lg"
|
|
||||||
onSubmit={handleSubmit(save)}
|
|
||||||
>
|
|
||||||
<FormGroup
|
|
||||||
label={t("internationalization")}
|
|
||||||
fieldId="kc-internationalization"
|
|
||||||
labelIcon={
|
|
||||||
<HelpItem
|
|
||||||
helpText={t("internationalizationHelp")}
|
|
||||||
fieldLabelId="internationalization"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
name="internationalizationEnabled"
|
|
||||||
control={control}
|
|
||||||
defaultValue={realm.internationalizationEnabled}
|
|
||||||
render={({ field }) => (
|
|
||||||
<Switch
|
|
||||||
id="kc-l-internationalization"
|
|
||||||
label={t("enabled")}
|
|
||||||
labelOff={t("disabled")}
|
|
||||||
isChecked={field.value}
|
|
||||||
data-testid={
|
|
||||||
field.value
|
|
||||||
? "internationalization-enabled"
|
|
||||||
: "internationalization-disabled"
|
|
||||||
}
|
|
||||||
onChange={field.onChange}
|
|
||||||
aria-label={t("internationalization")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
{internationalizationEnabled && (
|
|
||||||
<>
|
|
||||||
<FormGroup
|
|
||||||
label={t("supportedLocales")}
|
|
||||||
fieldId="kc-l-supported-locales"
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
name="supportedLocales"
|
|
||||||
control={control}
|
|
||||||
defaultValue={defaultSupportedLocales}
|
|
||||||
render={({ field }) => (
|
|
||||||
<Select
|
|
||||||
toggleId="kc-l-supported-locales"
|
|
||||||
onToggle={(open) => {
|
|
||||||
setSupportedLocalesOpen(open);
|
|
||||||
}}
|
|
||||||
onSelect={(_, v) => {
|
|
||||||
const option = v as string;
|
|
||||||
if (field.value.includes(option)) {
|
|
||||||
field.onChange(
|
|
||||||
field.value.filter(
|
|
||||||
(item: string) => item !== option,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
field.onChange([...field.value, option]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onClear={() => {
|
|
||||||
field.onChange([]);
|
|
||||||
}}
|
|
||||||
selections={field.value}
|
|
||||||
variant={SelectVariant.typeaheadMulti}
|
|
||||||
aria-label={t("supportedLocales")}
|
|
||||||
isOpen={supportedLocalesOpen}
|
|
||||||
placeholderText={t("selectLocales")}
|
|
||||||
>
|
|
||||||
{allLocales.map((locale) => (
|
|
||||||
<SelectOption
|
|
||||||
selected={field.value.includes(locale)}
|
|
||||||
key={locale}
|
|
||||||
value={locale}
|
|
||||||
>
|
|
||||||
{localeToDisplayName(locale, whoAmI.getLocale())}
|
|
||||||
</SelectOption>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
<FormGroup
|
|
||||||
label={t("defaultLocale")}
|
|
||||||
fieldId="kc-l-default-locale"
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
name="defaultLocale"
|
|
||||||
control={control}
|
|
||||||
defaultValue={DEFAULT_LOCALE}
|
|
||||||
render={({ field }) => (
|
|
||||||
<Select
|
|
||||||
toggleId="kc-default-locale"
|
|
||||||
onToggle={() => setDefaultLocaleOpen(!defaultLocaleOpen)}
|
|
||||||
onSelect={(_, value) => {
|
|
||||||
field.onChange(value as string);
|
|
||||||
setDefaultLocaleOpen(false);
|
|
||||||
}}
|
|
||||||
selections={
|
|
||||||
field.value
|
|
||||||
? localeToDisplayName(field.value, whoAmI.getLocale())
|
|
||||||
: realm.defaultLocale !== ""
|
|
||||||
? localeToDisplayName(
|
|
||||||
realm.defaultLocale || DEFAULT_LOCALE,
|
|
||||||
whoAmI.getLocale(),
|
|
||||||
)
|
|
||||||
: t("placeholderText")
|
|
||||||
}
|
|
||||||
variant={SelectVariant.single}
|
|
||||||
aria-label={t("defaultLocale")}
|
|
||||||
isOpen={defaultLocaleOpen}
|
|
||||||
placeholderText={t("placeholderText")}
|
|
||||||
data-testid="select-default-locale"
|
|
||||||
>
|
|
||||||
{watchSupportedLocales.map((locale, idx) => (
|
|
||||||
<SelectOption
|
|
||||||
key={`default-locale-${idx}`}
|
|
||||||
value={locale}
|
|
||||||
>
|
|
||||||
{localeToDisplayName(locale, whoAmI.getLocale())}
|
|
||||||
</SelectOption>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<ActionGroup>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
isDisabled={!formState.isDirty}
|
|
||||||
type="submit"
|
|
||||||
data-testid="localization-tab-save"
|
|
||||||
>
|
|
||||||
{t("save")}
|
|
||||||
</Button>
|
|
||||||
<Button variant="link" onClick={setupForm}>
|
|
||||||
{t("revert")}
|
|
||||||
</Button>
|
|
||||||
</ActionGroup>
|
|
||||||
</FormAccess>
|
|
||||||
|
|
||||||
<FormPanel className="kc-message-bundles" title="Edit message bundles">
|
|
||||||
<TextContent className="messageBundleDescription">
|
|
||||||
{t("messageBundleDescription")}
|
|
||||||
</TextContent>
|
|
||||||
<div className="tableBorder">
|
|
||||||
<PaginatingTableToolbar
|
|
||||||
count={messageBundles.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("searchForMessageBundle")}
|
|
||||||
toolbarItem={
|
|
||||||
<Button
|
|
||||||
data-testid="add-bundle-button"
|
|
||||||
onClick={() => setAddMessageBundleModalOpen(true)}
|
|
||||||
>
|
|
||||||
{t("addMessageBundle")}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
searchTypeComponent={
|
|
||||||
<ToolbarItem>
|
|
||||||
<Select
|
|
||||||
width={180}
|
|
||||||
data-testid="filter-by-locale-select"
|
|
||||||
isOpen={filterDropdownOpen}
|
|
||||||
className="kc-filter-by-locale-select"
|
|
||||||
variant={SelectVariant.single}
|
|
||||||
isDisabled={!internationalizationEnabled}
|
|
||||||
onToggle={(isExpanded) => setFilterDropdownOpen(isExpanded)}
|
|
||||||
onSelect={(_, value) => {
|
|
||||||
setSelectMenuLocale(value.toString());
|
|
||||||
setSelectMenuValueSelected(true);
|
|
||||||
refreshTable();
|
|
||||||
setFilterDropdownOpen(false);
|
|
||||||
}}
|
|
||||||
selections={
|
|
||||||
selectMenuValueSelected
|
|
||||||
? localeToDisplayName(
|
|
||||||
selectMenuLocale,
|
|
||||||
whoAmI.getLocale(),
|
|
||||||
)
|
|
||||||
: realm.defaultLocale !== ""
|
|
||||||
? localeToDisplayName(
|
|
||||||
DEFAULT_LOCALE,
|
|
||||||
whoAmI.getLocale(),
|
|
||||||
)
|
|
||||||
: t("placeholderText")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{options}
|
|
||||||
</Select>
|
|
||||||
</ToolbarItem>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{messageBundles.length === 0 && !filter && (
|
|
||||||
<ListEmptyState
|
|
||||||
hasIcon
|
|
||||||
message={t("noMessageBundles")}
|
|
||||||
instructions={t("noMessageBundlesInstructions")}
|
|
||||||
onPrimaryAction={handleModalToggle}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{messageBundles.length === 0 && filter && (
|
|
||||||
<ListEmptyState
|
|
||||||
hasIcon
|
|
||||||
icon={SearchIcon}
|
|
||||||
isSearchVariant
|
|
||||||
message={t("noSearchResults")}
|
|
||||||
instructions={t("noSearchResultsInstructions")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{messageBundles.length !== 0 && (
|
|
||||||
<Table
|
|
||||||
aria-label={t("editableRowsTable")}
|
|
||||||
data-testid="editable-rows-table"
|
|
||||||
variant={TableVariant.compact}
|
|
||||||
cells={[t("key"), t("value")]}
|
|
||||||
rows={tableRows}
|
|
||||||
onRowEdit={(_, type, _b, rowIndex, validation) =>
|
|
||||||
updateEditableRows(type, rowIndex, validation)
|
|
||||||
}
|
|
||||||
actions={[
|
|
||||||
{
|
|
||||||
title: t("delete"),
|
|
||||||
onClick: (_, row) =>
|
|
||||||
deleteKey(
|
|
||||||
(tableRows[row].cells?.[0] as IRowCell).props.value,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<TableHeader />
|
|
||||||
<TableBody />
|
|
||||||
</Table>
|
|
||||||
)}
|
|
||||||
</PaginatingTableToolbar>
|
|
||||||
</div>
|
|
||||||
</FormPanel>
|
|
||||||
</PageSection>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -231,17 +231,7 @@ export const LocalizationTab = ({ save, realm }: LocalizationTabProps) => {
|
||||||
<Tab
|
<Tab
|
||||||
id="realm-overrides"
|
id="realm-overrides"
|
||||||
eventKey={1}
|
eventKey={1}
|
||||||
title={
|
title={<TabTitleText>{t("realmOverrides")} </TabTitleText>}
|
||||||
<TabTitleText>
|
|
||||||
{t("realmOverrides")}{" "}
|
|
||||||
<HelpItem
|
|
||||||
fieldLabelId="realm-overrides"
|
|
||||||
helpText={t("realmOverridesHelp")}
|
|
||||||
noVerticalAlign={false}
|
|
||||||
unWrap
|
|
||||||
/>
|
|
||||||
</TabTitleText>
|
|
||||||
}
|
|
||||||
data-testid="rs-localization-realm-overrides-tab"
|
data-testid="rs-localization-realm-overrides-tab"
|
||||||
>
|
>
|
||||||
<RealmOverrides
|
<RealmOverrides
|
||||||
|
@ -253,17 +243,7 @@ export const LocalizationTab = ({ save, realm }: LocalizationTabProps) => {
|
||||||
<Tab
|
<Tab
|
||||||
id="effective-message-bundles"
|
id="effective-message-bundles"
|
||||||
eventKey={2}
|
eventKey={2}
|
||||||
title={
|
title={<TabTitleText>{t("effectiveMessageBundles")}</TabTitleText>}
|
||||||
<TabTitleText>
|
|
||||||
{t("effectiveMessageBundles")}
|
|
||||||
<HelpItem
|
|
||||||
fieldLabelId="effective-message-bundles"
|
|
||||||
helpText={t("effectiveMessageBundlesHelp")}
|
|
||||||
noVerticalAlign={false}
|
|
||||||
unWrap
|
|
||||||
/>
|
|
||||||
</TabTitleText>
|
|
||||||
}
|
|
||||||
data-testid="rs-localization-effective-message-bundles-tab"
|
data-testid="rs-localization-effective-message-bundles-tab"
|
||||||
>
|
>
|
||||||
<EffectiveMessageBundles
|
<EffectiveMessageBundles
|
||||||
|
|
|
@ -12,7 +12,10 @@ import {
|
||||||
SelectGroup,
|
SelectGroup,
|
||||||
SelectOption,
|
SelectOption,
|
||||||
SelectVariant,
|
SelectVariant,
|
||||||
|
TextContent,
|
||||||
|
Text,
|
||||||
ToolbarItem,
|
ToolbarItem,
|
||||||
|
TextVariants,
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
import {
|
import {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
|
@ -21,6 +24,7 @@ import {
|
||||||
TimesIcon,
|
TimesIcon,
|
||||||
} from "@patternfly/react-icons";
|
} from "@patternfly/react-icons";
|
||||||
import {
|
import {
|
||||||
|
ActionsColumn,
|
||||||
IRow,
|
IRow,
|
||||||
IRowCell,
|
IRowCell,
|
||||||
Table,
|
Table,
|
||||||
|
@ -323,6 +327,11 @@ export const RealmOverrides = ({
|
||||||
form={bundleForm}
|
form={bundleForm}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<TextContent>
|
||||||
|
<Text className="pf-u-mt-lg pf-u-ml-md" component={TextVariants.p}>
|
||||||
|
{t("realmOverridesDescription")}
|
||||||
|
</Text>
|
||||||
|
</TextContent>
|
||||||
<PaginatingTableToolbar
|
<PaginatingTableToolbar
|
||||||
count={messageBundles.length}
|
count={messageBundles.length}
|
||||||
first={first}
|
first={first}
|
||||||
|
@ -339,14 +348,14 @@ export const RealmOverrides = ({
|
||||||
setFirst(0);
|
setFirst(0);
|
||||||
setMax(10);
|
setMax(10);
|
||||||
}}
|
}}
|
||||||
inputGroupPlaceholder={t("searchForMessageBundle")}
|
inputGroupPlaceholder={t("searchForTranslation")}
|
||||||
toolbarItem={
|
toolbarItem={
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
data-testid="add-bundle-button"
|
data-testid="add-translationBtn"
|
||||||
onClick={() => setAddMessageBundleModalOpen(true)}
|
onClick={() => setAddMessageBundleModalOpen(true)}
|
||||||
>
|
>
|
||||||
{t("addMessageBundle")}
|
{t("addTranslation")}
|
||||||
</Button>
|
</Button>
|
||||||
<ToolbarItem>
|
<ToolbarItem>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
|
@ -408,8 +417,8 @@ export const RealmOverrides = ({
|
||||||
{messageBundles.length === 0 && !filter && (
|
{messageBundles.length === 0 && !filter && (
|
||||||
<ListEmptyState
|
<ListEmptyState
|
||||||
hasIcon
|
hasIcon
|
||||||
message={t("noMessageBundles")}
|
message={t("noTranslations")}
|
||||||
instructions={t("noMessageBundlesInstructions")}
|
instructions={t("noTranslationsInstructions")}
|
||||||
onPrimaryAction={handleModalToggle}
|
onPrimaryAction={handleModalToggle}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -544,20 +553,19 @@ export const RealmOverrides = ({
|
||||||
</Form>
|
</Form>
|
||||||
</Td>
|
</Td>
|
||||||
<Td isActionCell>
|
<Td isActionCell>
|
||||||
<Dropdown
|
<ActionsColumn
|
||||||
toggle={
|
items={[
|
||||||
<KebabToggle
|
{
|
||||||
className="pf-m-plain"
|
title: t("delete"),
|
||||||
data-testid="realmOverrides-deleteKebabToggle"
|
onClick: () => {
|
||||||
/>
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedRowKeys([
|
setSelectedRowKeys([
|
||||||
(row.cells?.[0] as IRowCell).props.value,
|
(row.cells?.[0] as IRowCell).props.value,
|
||||||
]);
|
]);
|
||||||
toggleDeleteDialog();
|
toggleDeleteDialog();
|
||||||
setKebabOpen(false);
|
setKebabOpen(false);
|
||||||
}}
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
|
|
Loading…
Reference in a new issue