659f0f583f
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
320 lines
10 KiB
TypeScript
320 lines
10 KiB
TypeScript
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 { Tbody, Td, Th, Thead, Tr } from "@patternfly/react-table";
|
|
import { Table } from "@patternfly/react-table/deprecated";
|
|
import { SearchIcon } from "@patternfly/react-icons";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { FormProvider, useForm, useWatch } 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 "@keycloak/keycloak-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();
|
|
|
|
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 (
|
|
<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 || !isRequiredTranslation}
|
|
>
|
|
{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-v5-u-mt-md"
|
|
data-testid="translation-key"
|
|
isDisabled
|
|
/>
|
|
<FlexItem>
|
|
<TextContent>
|
|
<Text
|
|
className="pf-v5-u-font-size-sm pf-v5-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-v5-u-py-lg">
|
|
{t("supportedLanguagesTableColumnName")}
|
|
</Th>
|
|
<Th className="pf-v5-u-py-lg">
|
|
{t("translationTableColumnName")}
|
|
</Th>
|
|
<Th aria-hidden="true" />
|
|
</Tr>
|
|
</Thead>
|
|
<Tbody>
|
|
{filteredLocales.map((locale, index) => {
|
|
const rowIndex = combinedLocales.findIndex(
|
|
(combinedLocale) => combinedLocale === locale,
|
|
);
|
|
return (
|
|
<Tr key={index}>
|
|
<Td
|
|
className="pf-m-sm pf-v5-u-px-sm"
|
|
dataLabel={t("supportedLanguage")}
|
|
>
|
|
<FormGroup fieldId="kc-supportedLanguage">
|
|
{localeToDisplayName(
|
|
locale,
|
|
whoAmI.getLocale(),
|
|
)}
|
|
{locale === defaultLocales.toString() && (
|
|
<Label
|
|
className="pf-v5-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-${rowIndex}`}
|
|
rules={{
|
|
required: {
|
|
value: true,
|
|
message: t("required"),
|
|
},
|
|
}}
|
|
/>
|
|
)}
|
|
{locale !== defaultLocales.toString() && (
|
|
<TextControl
|
|
name={`translations.${rowIndex}.value`}
|
|
label={t("translationValue")}
|
|
data-testid={`translation-value-${rowIndex}`}
|
|
/>
|
|
)}
|
|
</Td>
|
|
</Tr>
|
|
);
|
|
})}
|
|
</Tbody>
|
|
</Table>
|
|
)}
|
|
</PaginatingTableToolbar>
|
|
</FlexItem>
|
|
</Form>
|
|
</FormProvider>
|
|
</FlexItem>
|
|
</Flex>
|
|
</Modal>
|
|
);
|
|
};
|