mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
feat: wrap up translation logic
This commit is contained in:
@@ -17,6 +17,7 @@ import AttachmentInput from "@/app-components/attachment/AttachmentInput";
|
||||
import { DialogTitle } from "@/app-components/dialogs/DialogTitle";
|
||||
import { ContentContainer } from "@/app-components/dialogs/layouts/ContentContainer";
|
||||
import { ContentItem } from "@/app-components/dialogs/layouts/ContentItem";
|
||||
import { isSameEntity } from "@/hooks/crud/helpers";
|
||||
import { useApiClient } from "@/hooks/useApiClient";
|
||||
import { DialogControlProps } from "@/hooks/useDialog";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
@@ -40,12 +41,19 @@ export const NlpImportDialog: FC<NlpImportDialogProps> = ({
|
||||
attachmentId && (await apiClient.importNlpSamples(attachmentId));
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.removeQueries([
|
||||
QueryType.collection,
|
||||
EntityType.NLP_SAMPLE,
|
||||
]);
|
||||
queryClient.removeQueries([QueryType.count, EntityType.NLP_SAMPLE]);
|
||||
queryClient.removeQueries({
|
||||
predicate: ({ queryKey }) => {
|
||||
const [qType, qEntity] = queryKey;
|
||||
|
||||
return (
|
||||
((qType === QueryType.count || qType === QueryType.collection) &&
|
||||
isSameEntity(qEntity, EntityType.NLP_SAMPLE)) ||
|
||||
isSameEntity(qEntity, EntityType.NLP_SAMPLE_ENTITY) ||
|
||||
isSameEntity(qEntity, EntityType.NLP_ENTITY) ||
|
||||
isSameEntity(qEntity, EntityType.NLP_VALUE)
|
||||
);
|
||||
},
|
||||
});
|
||||
handleCloseDialog();
|
||||
toast.success(t("message.success_save"));
|
||||
},
|
||||
|
||||
@@ -149,6 +149,7 @@ export default function NlpSample() {
|
||||
renderCell: ({ row }) =>
|
||||
row.entities
|
||||
.map((e) => getSampleEntityFromCache(e) as INlpSampleEntity)
|
||||
.filter((e) => !!e)
|
||||
.map((entity) => (
|
||||
<ChipEntity
|
||||
id={entity.entity}
|
||||
|
||||
@@ -7,8 +7,14 @@
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { Dialog, DialogActions, DialogContent } from "@mui/material";
|
||||
import { useEffect, FC, useMemo } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
FormLabel,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { FC, useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -16,15 +22,15 @@ import DialogButtons from "@/app-components/buttons/DialogButtons";
|
||||
import { DialogTitle } from "@/app-components/dialogs/DialogTitle";
|
||||
import { ContentContainer } from "@/app-components/dialogs/layouts/ContentContainer";
|
||||
import { ContentItem } from "@/app-components/dialogs/layouts/ContentItem";
|
||||
import { useFind } from "@/hooks/crud/useFind";
|
||||
import { useUpdate } from "@/hooks/crud/useUpdate";
|
||||
import { DialogControlProps } from "@/hooks/useDialog";
|
||||
import { useSetting } from "@/hooks/useSetting";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
import { EntityType } from "@/services/types";
|
||||
import {
|
||||
ITranslation,
|
||||
ITranslationAttributes,
|
||||
ITranslations,
|
||||
ITranslation,
|
||||
} from "@/types/translation.types";
|
||||
|
||||
import TranslationInput from "./TranslationInput";
|
||||
@@ -36,10 +42,14 @@ export const EditTranslationDialog: FC<EditTranslationDialogProps> = ({
|
||||
closeDialog,
|
||||
...rest
|
||||
}) => {
|
||||
const { data: languages } = useFind(
|
||||
{ entity: EntityType.LANGUAGE },
|
||||
{
|
||||
hasCount: false,
|
||||
},
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const availableLanguages = useSetting("nlp_settings", "languages");
|
||||
const defaultLanguage = useSetting("nlp_settings", "default_lang");
|
||||
const { mutateAsync: updateTranslation } = useUpdate(EntityType.TRANSLATION, {
|
||||
onError: () => {
|
||||
toast.error(t("message.internal_server_error"));
|
||||
@@ -49,29 +59,16 @@ export const EditTranslationDialog: FC<EditTranslationDialogProps> = ({
|
||||
toast.success(t("message.success_save"));
|
||||
},
|
||||
});
|
||||
const defaultValues: ITranslation | undefined = useMemo(
|
||||
() =>
|
||||
data
|
||||
? {
|
||||
...data,
|
||||
translations: {
|
||||
...data?.translations,
|
||||
[defaultLanguage]: data?.str,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
[defaultLanguage, data],
|
||||
);
|
||||
const { reset, control, handleSubmit } = useForm<ITranslationAttributes>({
|
||||
defaultValues,
|
||||
defaultValues: data,
|
||||
});
|
||||
const onSubmitForm = async (params: ITranslationAttributes) => {
|
||||
if (data?.id) updateTranslation({ id: data.id, params });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) reset(defaultValues);
|
||||
}, [open, reset, defaultValues]);
|
||||
if (open) reset(data);
|
||||
}, [open, reset, data]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} fullWidth onClose={closeDialog} {...rest}>
|
||||
@@ -80,21 +77,26 @@ export const EditTranslationDialog: FC<EditTranslationDialogProps> = ({
|
||||
{t("title.update_translation")}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<ContentItem>
|
||||
<FormLabel>{t("label.original_text")}</FormLabel>
|
||||
<Typography component="p">{data?.str}</Typography>
|
||||
</ContentItem>
|
||||
<ContentContainer>
|
||||
{availableLanguages?.map((language: string) => (
|
||||
<ContentItem key={language}>
|
||||
<Controller
|
||||
name={`translations.${language as keyof ITranslations}`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TranslationInput
|
||||
field={field}
|
||||
language={language as keyof ITranslations}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</ContentItem>
|
||||
))}
|
||||
{languages
|
||||
.filter(({ isDefault }) => !isDefault)
|
||||
.map((language) => (
|
||||
<ContentItem key={language.code}>
|
||||
<Controller
|
||||
name={`translations.${
|
||||
language.code as keyof ITranslations
|
||||
}`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TranslationInput field={field} language={language} />
|
||||
)}
|
||||
/>
|
||||
</ContentItem>
|
||||
))}
|
||||
</ContentContainer>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
|
||||
@@ -7,24 +7,16 @@
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import CheckIcon from "@mui/icons-material/Check";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import { Grid } from "@mui/material";
|
||||
import React from "react";
|
||||
import { ControllerRenderProps } from "react-hook-form";
|
||||
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
import {
|
||||
ITranslationAttributes,
|
||||
ITranslations,
|
||||
} from "@/types/translation.types";
|
||||
|
||||
const isRTL = (language: string) => {
|
||||
return ["AR"].includes(language.toUpperCase());
|
||||
};
|
||||
import { ILanguage } from "@/types/language.types";
|
||||
import { ITranslationAttributes } from "@/types/translation.types";
|
||||
|
||||
interface RenderTranslationInputProps {
|
||||
language: keyof ITranslations;
|
||||
language: ILanguage;
|
||||
field: ControllerRenderProps<ITranslationAttributes, any>;
|
||||
}
|
||||
|
||||
@@ -34,14 +26,14 @@ const TranslationInput: React.FC<RenderTranslationInputProps> = ({
|
||||
}) => (
|
||||
<Input
|
||||
inputRef={field.ref}
|
||||
dir={isRTL(language) ? "rtl" : "ltr"}
|
||||
dir={language.isRTL ? "rtl" : "ltr"}
|
||||
label={
|
||||
<Grid container dir="ltr">
|
||||
<Grid>{language.toUpperCase()}</Grid>
|
||||
<Grid>{field.value ? <CheckIcon /> : <CloseIcon />}</Grid>
|
||||
<Grid>{language.title}</Grid>
|
||||
</Grid>
|
||||
}
|
||||
multiline={true}
|
||||
minRows={3}
|
||||
{...field}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
import { faLanguage } from "@fortawesome/free-solid-svg-icons";
|
||||
import AutorenewIcon from "@mui/icons-material/Autorenew";
|
||||
import { Button, Chip, Grid, Paper } from "@mui/material";
|
||||
import { Button, Chip, Grid, Paper, Stack } from "@mui/material";
|
||||
import { GridColDef } from "@mui/x-data-grid";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -25,10 +25,10 @@ import { useFind } from "@/hooks/crud/useFind";
|
||||
import { useRefreshTranslations } from "@/hooks/entities/translation-hooks";
|
||||
import { getDisplayDialogs, useDialog } from "@/hooks/useDialog";
|
||||
import { useSearch } from "@/hooks/useSearch";
|
||||
import { useSetting } from "@/hooks/useSetting";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
import { PageHeader } from "@/layout/content/PageHeader";
|
||||
import { EntityType } from "@/services/types";
|
||||
import { ILanguage } from "@/types/language.types";
|
||||
import { PermissionAction } from "@/types/permission.types";
|
||||
import { ITranslation } from "@/types/translation.types";
|
||||
import { getDateTimeFormatter } from "@/utils/date";
|
||||
@@ -38,7 +38,12 @@ import { EditTranslationDialog } from "./EditTranslationDialog";
|
||||
export const Translations = () => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const availableLanguages = useSetting("nlp_settings", "languages");
|
||||
const { data: languages } = useFind(
|
||||
{ entity: EntityType.LANGUAGE },
|
||||
{
|
||||
hasCount: false,
|
||||
},
|
||||
);
|
||||
const editDialogCtl = useDialog<ITranslation>(false);
|
||||
const deleteDialogCtl = useDialog<string>(false);
|
||||
const { onSearch, searchPayload } = useSearch<ITranslation>({
|
||||
@@ -92,22 +97,23 @@ export const Translations = () => {
|
||||
field: "translations",
|
||||
headerName: t("label.translations"),
|
||||
sortable: false,
|
||||
renderCell: (params) =>
|
||||
availableLanguages.map((language: string) => (
|
||||
<Chip
|
||||
key={language}
|
||||
variant={
|
||||
params.row.translations[language] ? "available" : "unavailable"
|
||||
}
|
||||
label={language.toUpperCase()}
|
||||
/>
|
||||
)),
|
||||
},
|
||||
{
|
||||
maxWidth: 127,
|
||||
field: "translated",
|
||||
resizable: false,
|
||||
headerName: t("label.translated"),
|
||||
renderCell: (params) => (
|
||||
<Stack direction="row" my={1} spacing={1}>
|
||||
{languages
|
||||
.filter(({ isDefault }) => !isDefault)
|
||||
.map((language: ILanguage) => (
|
||||
<Chip
|
||||
key={language.code}
|
||||
variant={
|
||||
params.row.translations[language.code]
|
||||
? "available"
|
||||
: "unavailable"
|
||||
}
|
||||
label={language.title}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{
|
||||
maxWidth: 140,
|
||||
@@ -167,7 +173,6 @@ export const Translations = () => {
|
||||
deleteTranslation(deleteDialogCtl.data);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Grid item width="100%">
|
||||
<DataGrid {...dataGridProps} columns={columns} />
|
||||
</Grid>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useTranslation } from "react-i18next";
|
||||
import AutoCompleteEntitySelect from "@/app-components/inputs/AutoCompleteEntitySelect";
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
import { RegexInput } from "@/app-components/inputs/RegexInput";
|
||||
import { useGetFromCache } from "@/hooks/crud/useGet";
|
||||
import { EntityType, Format } from "@/services/types";
|
||||
import {
|
||||
IBlockAttributes,
|
||||
@@ -25,7 +26,8 @@ import {
|
||||
PayloadPattern,
|
||||
} from "@/types/block.types";
|
||||
import { IMenuItem } from "@/types/menu.types";
|
||||
import { INlpValueFull } from "@/types/nlp-value.types";
|
||||
import { INlpEntity } from "@/types/nlp-entity.types";
|
||||
import { INlpValue } from "@/types/nlp-value.types";
|
||||
|
||||
import { ContentPostbackInput } from "./ContentPostbackInput";
|
||||
import { PostbackInput } from "./PostbackInput";
|
||||
@@ -64,6 +66,7 @@ const PatternInput: FC<PatternInputProps> = ({ value, onChange, idx }) => {
|
||||
register,
|
||||
formState: { errors },
|
||||
} = useFormContext<IBlockAttributes>();
|
||||
const getNlpEntityFromCache = useGetFromCache(EntityType.NLP_ENTITY);
|
||||
const [pattern, setPattern] = useState<Pattern>(value);
|
||||
const [patternType, setPatternType] = useState<PatternType>(getType(value));
|
||||
const types = [
|
||||
@@ -140,7 +143,7 @@ const PatternInput: FC<PatternInputProps> = ({ value, onChange, idx }) => {
|
||||
</Grid>
|
||||
<Grid item xs={9}>
|
||||
{patternType === "nlp" ? (
|
||||
<AutoCompleteEntitySelect<INlpValueFull, "value">
|
||||
<AutoCompleteEntitySelect<INlpValue, "value">
|
||||
value={(pattern as NlpPattern[]).map((v) =>
|
||||
"value" in v && v.value ? v.value : v.entity,
|
||||
)}
|
||||
@@ -153,25 +156,31 @@ const PatternInput: FC<PatternInputProps> = ({ value, onChange, idx }) => {
|
||||
multiple={true}
|
||||
onChange={(_e, data) => {
|
||||
setPattern(
|
||||
data.map((d) =>
|
||||
d.value === "any"
|
||||
data.map((d) => {
|
||||
const entity = getNlpEntityFromCache(d.entity) as INlpEntity;
|
||||
|
||||
return d.value === "any"
|
||||
? {
|
||||
match: "entity",
|
||||
entity: d.entity.name,
|
||||
entity: entity.name,
|
||||
}
|
||||
: {
|
||||
match: "value",
|
||||
entity: d.entity.name,
|
||||
entity: entity.name,
|
||||
value: d.value,
|
||||
},
|
||||
),
|
||||
};
|
||||
}),
|
||||
);
|
||||
}}
|
||||
getOptionLabel={(option) => {
|
||||
return `${option.entity.name}=${option.value}`;
|
||||
const entity = getNlpEntityFromCache(option.entity) as INlpEntity;
|
||||
|
||||
return `${entity.name}=${option.value}`;
|
||||
}}
|
||||
groupBy={(option) => {
|
||||
return option.entity.name;
|
||||
const entity = getNlpEntityFromCache(option.entity) as INlpEntity;
|
||||
|
||||
return entity.name;
|
||||
}}
|
||||
renderGroup={(params) => (
|
||||
<li key={params.key}>
|
||||
@@ -188,23 +197,25 @@ const PatternInput: FC<PatternInputProps> = ({ value, onChange, idx }) => {
|
||||
)}
|
||||
preprocess={(options) => {
|
||||
return options.reduce((acc, curr) => {
|
||||
if (curr.entity.lookups.includes("keywords")) {
|
||||
const entity = getNlpEntityFromCache(curr.entity) as INlpEntity;
|
||||
|
||||
if (entity.lookups.includes("keywords")) {
|
||||
const exists = acc.find(
|
||||
({ value, id }) => value === "any" && id === curr.entity.id,
|
||||
({ value, id }) => value === "any" && id === entity.id,
|
||||
);
|
||||
|
||||
if (!exists) {
|
||||
acc.push({
|
||||
entity: curr.entity,
|
||||
id: curr.entity.id,
|
||||
entity: entity.id,
|
||||
id: entity.id,
|
||||
value: "any",
|
||||
} as INlpValueFull);
|
||||
} as INlpValue);
|
||||
}
|
||||
}
|
||||
acc.push(curr);
|
||||
|
||||
return acc;
|
||||
}, [] as INlpValueFull[]);
|
||||
}, [] as INlpValue[]);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -550,7 +550,8 @@
|
||||
"no_data": "No data",
|
||||
"code": "Code",
|
||||
"is_default": "Default",
|
||||
"is_rtl": "RTL"
|
||||
"is_rtl": "RTL",
|
||||
"original_text": "Original Text"
|
||||
},
|
||||
"placeholder": {
|
||||
"your_username": "Your username",
|
||||
|
||||
@@ -550,7 +550,8 @@
|
||||
"no_data": "Pas de données",
|
||||
"code": "Code",
|
||||
"is_default": "Par Défaut",
|
||||
"is_rtl": "RTL"
|
||||
"is_rtl": "RTL",
|
||||
"original_text": "Texte par défaut"
|
||||
},
|
||||
"placeholder": {
|
||||
"your_username": "Votre nom d'utilisateur",
|
||||
|
||||
Reference in New Issue
Block a user