Merge pull request #698 from Hexastack/697-refactor-nlu-values-dialogs-add-edit-delete-bulk
Some checks are pending
Build and Push Docker API Image / build-and-push (push) Waiting to run
Build and Push Docker Base Image / build-and-push (push) Waiting to run
Build and Push Docker UI Image / build-and-push (push) Waiting to run

refactor(frontend): update nlpValue dialogs
This commit is contained in:
Med Marrouchi 2025-02-07 10:45:48 +01:00 committed by GitHub
commit 3c19dde936
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 197 additions and 201 deletions

View File

@ -1,145 +0,0 @@
/*
* Copyright © 2024 Hexastack. All rights reserved.
*
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import { Dialog, DialogActions, DialogContent } from "@mui/material";
import { useRouter } from "next/router";
import { FC, useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
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 { Input } from "@/app-components/inputs/Input";
import MultipleInput from "@/app-components/inputs/MultipleInput";
import { useCreate } from "@/hooks/crud/useCreate";
import { useGet } from "@/hooks/crud/useGet";
import { useUpdate } from "@/hooks/crud/useUpdate";
import { DialogControlProps } from "@/hooks/useDialog";
import { useToast } from "@/hooks/useToast";
import { useTranslate } from "@/hooks/useTranslate";
import { EntityType, Format } from "@/services/types";
import { INlpValue, INlpValueAttributes } from "@/types/nlp-value.types";
export type TNlpValueAttributesWithRequiredExpressions = INlpValueAttributes & {
expressions: string[];
};
export type NlpValueDialogProps = DialogControlProps<INlpValue, INlpValue> & {
canHaveSynonyms: boolean;
};
export const NlpValueDialog: FC<NlpValueDialogProps> = ({
open,
closeDialog,
data,
canHaveSynonyms,
callback,
}) => {
const { t } = useTranslate();
const { toast } = useToast();
const { query } = useRouter();
const { refetch: refetchEntity } = useGet(data?.entity || String(query.id), {
entity: EntityType.NLP_ENTITY,
format: Format.FULL,
});
const { mutateAsync: createNlpValue } = useCreate(EntityType.NLP_VALUE, {
onError: () => {
toast.error(t("message.internal_server_error"));
},
onSuccess(data) {
refetchEntity();
closeDialog();
toast.success(t("message.success_save"));
callback?.(data);
},
});
const { mutateAsync: updateNlpValue } = useUpdate(EntityType.NLP_VALUE, {
onError: () => {
toast.error(t("message.internal_server_error"));
},
onSuccess(data) {
closeDialog();
toast.success(t("message.success_save"));
callback?.(data);
},
});
const { reset, register, handleSubmit, control } =
useForm<TNlpValueAttributesWithRequiredExpressions>({
defaultValues: {
value: data?.value || "",
expressions: data?.expressions || [],
},
});
const validationRules = {
value: {
required: t("message.value_is_required"),
},
name: {},
description: {},
};
const onSubmitForm = async (params: INlpValueAttributes) => {
if (data) {
updateNlpValue({ id: data.id, params });
} else {
createNlpValue({ ...params, entity: String(query.id) });
}
};
useEffect(() => {
if (open) reset();
}, [open, reset]);
useEffect(() => {
if (data) {
reset({
value: data.value,
expressions: data.expressions,
});
} else {
reset();
}
}, [data, reset]);
return (
<Dialog open={open} fullWidth onClose={closeDialog}>
<form onSubmit={handleSubmit(onSubmitForm)}>
<DialogTitle onClose={closeDialog}>
{data ? t("title.edit_nlp_value") : t("title.new_nlp_entity_value")}
</DialogTitle>
<DialogContent>
<ContentContainer>
<ContentItem>
<Input
label={t("placeholder.nlp_value")}
required
autoFocus
{...register("value", validationRules.value)}
/>
</ContentItem>
{canHaveSynonyms ? (
<ContentItem>
<Controller
name="expressions"
control={control}
render={({ field }) => (
<MultipleInput label="synonyms" {...field} />
)}
/>
</ContentItem>
) : null}
</ContentContainer>
</DialogContent>
<DialogActions>
<DialogButtons closeDialog={closeDialog} />
</DialogActions>
</form>
</Dialog>
);
};

View File

@ -1,5 +1,5 @@
/*
* Copyright © 2024 Hexastack. All rights reserved.
* Copyright © 2025 Hexastack. All rights reserved.
*
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
@ -15,7 +15,7 @@ import { GridColDef, GridRowSelectionModel } from "@mui/x-data-grid";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { DeleteDialog } from "@/app-components/dialogs";
import { ConfirmDialogBody } from "@/app-components/dialogs";
import { FilterTextfield } from "@/app-components/inputs/FilterTextfield";
import {
ActionColumnLabel,
@ -27,7 +27,7 @@ import { useDelete } from "@/hooks/crud/useDelete";
import { useDeleteMany } from "@/hooks/crud/useDeleteMany";
import { useFind } from "@/hooks/crud/useFind";
import { useGet } from "@/hooks/crud/useGet";
import { useDialog } from "@/hooks/useDialog";
import { useDialogs } from "@/hooks/useDialogs";
import { useHasPermission } from "@/hooks/useHasPermission";
import { useSearch } from "@/hooks/useSearch";
import { useToast } from "@/hooks/useToast";
@ -39,21 +39,20 @@ import { INlpValue } from "@/types/nlp-value.types";
import { PermissionAction } from "@/types/permission.types";
import { getDateTimeFormatter } from "@/utils/date";
import { NlpValueDialog } from "../NlpValueDialog";
import { NlpValueFormDialog } from "./NlpValueFormDialog";
export const NlpValues = ({ entityId }: { entityId: string }) => {
const [direction, setDirection] = useState<"up" | "down">("up");
const deleteEntityDialogCtl = useDialog<string>(false);
const editValueDialogCtl = useDialog<INlpValue>(false);
const addNlpValueDialogCtl = useDialog<INlpValue>(false);
const hasPermission = useHasPermission();
const router = useRouter();
const { t } = useTranslate();
const { toast } = useToast();
const dialogs = useDialogs();
const router = useRouter();
const [direction, setDirection] = useState<"up" | "down">("up");
const hasPermission = useHasPermission();
const { data: nlpEntity, refetch: refetchEntity } = useGet(entityId, {
entity: EntityType.NLP_ENTITY,
format: Format.FULL,
});
const canHaveSynonyms = nlpEntity?.lookups?.[0] === NlpLookups.keywords;
const { onSearch, searchPayload } = useSearch<INlpValue>({
$eq: [{ entity: entityId }],
$iLike: ["value"],
@ -64,23 +63,20 @@ export const NlpValues = ({ entityId }: { entityId: string }) => {
params: searchPayload,
},
);
const { mutateAsync: deleteNlpValue } = useDelete(EntityType.NLP_VALUE, {
onError: () => {
toast.error(t("message.internal_server_error"));
const { mutate: deleteNlpValue } = useDelete(EntityType.NLP_VALUE, {
onError: (error: Error) => {
toast.error(error.message || t("message.internal_server_error"));
},
onSuccess() {
deleteEntityDialogCtl.closeDialog();
toast.success(t("message.item_delete_success"));
refetchEntity();
toast.success(t("message.item_delete_success"));
},
});
const { mutateAsync: deleteNlpValues } = useDeleteMany(EntityType.NLP_VALUE, {
onError: (error) => {
toast.error(error);
const { mutate: deleteNlpValues } = useDeleteMany(EntityType.NLP_VALUE, {
onError: (error: Error) => {
toast.error(error.message || t("message.internal_server_error"));
},
onSuccess: () => {
deleteEntityDialogCtl.closeDialog();
setSelectedNlpValues([]);
onSuccess() {
toast.success(t("message.item_delete_success"));
},
});
@ -90,11 +86,18 @@ export const NlpValues = ({ entityId }: { entityId: string }) => {
[
{
label: ActionColumnLabel.Edit,
action: (row) => editValueDialogCtl.openDialog(row),
action: (row) =>
dialogs.open(NlpValueFormDialog, { data: row, canHaveSynonyms }),
},
{
label: ActionColumnLabel.Delete,
action: (row) => deleteEntityDialogCtl.openDialog(row.id),
action: async ({ id }) => {
const isConfirmed = await dialogs.confirm(ConfirmDialogBody);
if (isConfirmed) {
deleteNlpValue(id);
}
},
},
],
t("label.operations"),
@ -150,10 +153,19 @@ export const NlpValues = ({ entityId }: { entityId: string }) => {
return setDirection("down");
}, []);
const canHaveSynonyms = nlpEntity?.lookups?.[0] === NlpLookups.keywords;
const handleSelectionChange = (selection: GridRowSelectionModel) => {
setSelectedNlpValues(selection as string[]);
};
const handleDeleteNlpValues = async () => {
const isConfirmed = await dialogs.confirm(ConfirmDialogBody, {
mode: "selection",
count: selectedNlpValues.length,
});
if (isConfirmed) {
deleteNlpValues(selectedNlpValues);
}
};
return (
<Grid container gap={2} flexDirection="column">
@ -198,8 +210,8 @@ export const NlpValues = ({ entityId }: { entityId: string }) => {
<Button
startIcon={<AddIcon />}
variant="contained"
onClick={() => addNlpValueDialogCtl.openDialog()}
sx={{ float: "right" }}
onClick={() => dialogs.open(NlpValueFormDialog, null)}
>
{t("button.add")}
</Button>
@ -207,12 +219,10 @@ export const NlpValues = ({ entityId }: { entityId: string }) => {
{selectedNlpValues.length > 0 && (
<Grid item>
<Button
startIcon={<DeleteIcon />}
variant="contained"
color="error"
onClick={() =>
deleteEntityDialogCtl.openDialog(undefined)
}
variant="contained"
onClick={handleDeleteNlpValues}
startIcon={<DeleteIcon />}
>
{t("button.delete")}
</Button>
@ -221,30 +231,6 @@ export const NlpValues = ({ entityId }: { entityId: string }) => {
</ButtonGroup>
</Grid>
</PageHeader>
<NlpValueDialog
{...addNlpValueDialogCtl}
canHaveSynonyms={canHaveSynonyms}
callback={() => {
refetchEntity();
}}
/>
<DeleteDialog
{...deleteEntityDialogCtl}
callback={() => {
if (selectedNlpValues.length > 0) {
deleteNlpValues(selectedNlpValues);
setSelectedNlpValues([]);
deleteEntityDialogCtl.closeDialog();
} else if (deleteEntityDialogCtl.data) {
deleteNlpValue(deleteEntityDialogCtl.data);
}
}}
/>
<NlpValueDialog
{...editValueDialogCtl}
canHaveSynonyms={canHaveSynonyms}
callback={() => {}}
/>
<Grid padding={1} marginTop={2} container>
<DataGrid
columns={columns}

View File

@ -0,0 +1,125 @@
/*
* Copyright © 2025 Hexastack. All rights reserved.
*
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import { useRouter } from "next/router";
import { FC, Fragment, useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { ContentContainer, ContentItem } from "@/app-components/dialogs/";
import { Input } from "@/app-components/inputs/Input";
import MultipleInput from "@/app-components/inputs/MultipleInput";
import { useCreate } from "@/hooks/crud/useCreate";
import { useGet } from "@/hooks/crud/useGet";
import { useUpdate } from "@/hooks/crud/useUpdate";
import { useToast } from "@/hooks/useToast";
import { useTranslate } from "@/hooks/useTranslate";
import { EntityType, Format } from "@/services/types";
import { ComponentFormProps } from "@/types/common/dialogs.types";
import { INlpValue, INlpValueAttributes } from "@/types/nlp-value.types";
export const NlpValueForm: FC<
ComponentFormProps<{ data: INlpValue; canHaveSynonyms: boolean }>
> = ({ data: props, Wrapper = Fragment, WrapperProps, ...rest }) => {
const { data, canHaveSynonyms } = props || {};
const { t } = useTranslate();
const { toast } = useToast();
const { query } = useRouter();
const { refetch: refetchEntity } = useGet(data?.entity || String(query.id), {
entity: EntityType.NLP_ENTITY,
format: Format.FULL,
});
const { mutate: createNlpValue } = useCreate(EntityType.NLP_VALUE, {
onError: () => {
rest.onError?.();
toast.error(t("message.internal_server_error"));
},
onSuccess() {
rest.onSuccess?.();
refetchEntity();
toast.success(t("message.success_save"));
},
});
const { mutate: updateNlpValue } = useUpdate(EntityType.NLP_VALUE, {
onError: () => {
rest.onError?.();
toast.error(t("message.internal_server_error"));
},
onSuccess() {
rest.onSuccess?.();
toast.success(t("message.success_save"));
},
});
const { reset, register, handleSubmit, control } = useForm<
INlpValueAttributes & {
expressions: string[];
}
>({
defaultValues: {
value: data?.value || "",
expressions: data?.expressions || [],
},
});
const validationRules = {
value: {
required: t("message.value_is_required"),
},
name: {},
description: {},
};
const onSubmitForm = async (params: INlpValueAttributes) => {
if (data) {
updateNlpValue({ id: data.id, params });
} else {
createNlpValue({ ...params, entity: String(query.id) });
}
};
useEffect(() => {
if (data) {
reset({
value: data.value,
expressions: data.expressions,
});
} else {
reset();
}
}, [data, reset]);
return (
<Wrapper
open={!!WrapperProps?.open}
onSubmit={handleSubmit(onSubmitForm)}
{...WrapperProps}
>
<form onSubmit={handleSubmit(onSubmitForm)}>
<ContentContainer>
<ContentItem>
<Input
label={t("placeholder.nlp_value")}
required
autoFocus
{...register("value", validationRules.value)}
/>
</ContentItem>
{canHaveSynonyms ? (
<ContentItem>
<Controller
name="expressions"
control={control}
render={({ field }) => (
<MultipleInput label="synonyms" {...field} />
)}
/>
</ContentItem>
) : null}
</ContentContainer>
</form>
</Wrapper>
);
};

View File

@ -0,0 +1,29 @@
/*
* Copyright © 2025 Hexastack. All rights reserved.
*
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import { GenericFormDialog } from "@/app-components/dialogs";
import { ComponentFormDialogProps } from "@/types/common/dialogs.types";
import { INlpValue } from "@/types/nlp-value.types";
import { NlpValueForm } from "./NlpValueForm";
export const NlpValueFormDialog = <
T extends { data: INlpValue; canHaveSynonyms: boolean } = {
data: INlpValue;
canHaveSynonyms: boolean;
},
>(
props: ComponentFormDialogProps<T>,
) => (
<GenericFormDialog<T>
Form={NlpValueForm}
addText="title.new_nlp_entity_value"
editText="title.edit_nlp_value"
{...props}
/>
);

View File

@ -1,11 +1,12 @@
/*
* Copyright © 2024 Hexastack. All rights reserved.
* Copyright © 2025 Hexastack. All rights reserved.
*
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import { faGraduationCap } from "@fortawesome/free-solid-svg-icons";
import { Grid, Paper, Tab, Tabs } from "@mui/material";
import dynamic from "next/dynamic";
@ -29,7 +30,7 @@ import {
import NlpDatasetCounter from "./components/NlpDatasetCounter";
import NlpSample from "./components/NlpSample";
import NlpDatasetSample from "./components/NlpTrainForm";
import { NlpValues } from "./components/NlpValues";
import { NlpValues } from "./components/NlpValue";
const NlpEntity = dynamic(() => import("./components/NlpEntity"));