diff --git a/frontend/src/components/Menu/MenuDialog.tsx b/frontend/src/components/Menu/MenuDialog.tsx deleted file mode 100644 index d5cd288b3..000000000 --- a/frontend/src/components/Menu/MenuDialog.tsx +++ /dev/null @@ -1,188 +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, - DialogProps, - MenuItem, -} from "@mui/material"; -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 { ToggleableInput } from "@/app-components/inputs/ToggleableInput"; -import { useTranslate } from "@/hooks/useTranslate"; -import { IMenuItem, IMenuItemAttributes, MenuType } from "@/types/menu.types"; -import { isAbsoluteUrl } from "@/utils/URL"; - -export type MenuDialogProps = DialogProps & { - open: boolean; - closeFunction?: () => void; - createFunction?: (_menu: IMenuItemAttributes) => void; - editFunction?: (_menu: IMenuItemAttributes) => void; - row?: IMenuItem; - parentId?: string; -}; -const DEFAULT_VALUES = { title: "", type: MenuType.web_url, url: undefined }; - -export const MenuDialog: FC = ({ - open, - closeFunction, - createFunction, - editFunction, - row, - parentId, - ...rest -}) => { - const { t } = useTranslate(); - const { - reset, - resetField, - control, - formState: { errors }, - handleSubmit, - register, - watch, - } = useForm({ - defaultValues: DEFAULT_VALUES, - }); - const validationRules = { - type: { - required: t("message.type_is_required"), - }, - title: { required: t("message.title_is_required") }, - url: { - required: t("message.url_is_invalid"), - validate: (value: string = "") => - isAbsoluteUrl(value) || t("message.url_is_invalid"), - }, - payload: {}, - }; - const onSubmitForm = async (data: IMenuItemAttributes) => { - if (createFunction) { - createFunction({ ...data, parent: parentId }); - } else if (editFunction) { - editFunction({ ...data, parent: parentId }); - } - }; - - useEffect(() => { - if (open) { - if (row) { - reset(row); - } else { - reset(DEFAULT_VALUES); - } - } - }, [open, reset, row]); - - const typeValue = watch("type"); - const titleValue = watch("title"); - - return ( - -
- - {createFunction - ? t("title.add_menu_item") - : t("title.edit_menu_item")} - - - - - - { - const { onChange, ...rest } = field; - - return ( - { - onChange(value); - resetField("url"); - }} - helperText={errors.type ? errors.type.message : null} - {...rest} - > - {Object.keys(MenuType).map((value, key) => ( - - {t(`label.${value}`)} - - ))} - - ); - }} - /> - - - - - - - - - {typeValue === MenuType.web_url ? ( - - ) : typeValue === MenuType.postback ? ( - { - return ( - - ); - }} - /> - ) : null} - - - - - - -
-
- ); -}; diff --git a/frontend/src/components/Menu/MenuForm.tsx b/frontend/src/components/Menu/MenuForm.tsx new file mode 100644 index 000000000..cc8ae1131 --- /dev/null +++ b/frontend/src/components/Menu/MenuForm.tsx @@ -0,0 +1,187 @@ +/* + * 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 { MenuItem } from "@mui/material"; +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 { ToggleableInput } from "@/app-components/inputs/ToggleableInput"; +import { useCreate } from "@/hooks/crud/useCreate"; +import { useUpdate } from "@/hooks/crud/useUpdate"; +import { useToast } from "@/hooks/useToast"; +import { useTranslate } from "@/hooks/useTranslate"; +import { EntityType } from "@/services/types"; +import { ComponentFormProps } from "@/types/common/dialogs.types"; +import { IMenuItem, IMenuItemAttributes, MenuType } from "@/types/menu.types"; +import { isAbsoluteUrl } from "@/utils/URL"; + +const DEFAULT_VALUES = { title: "", type: MenuType.web_url, url: undefined }; + +export type MenuFormData = { + row?: IMenuItem; + rowId?: string; + parentId?: string; +}; + +export const MenuForm: FC> = ({ + data, + Wrapper = Fragment, + WrapperProps, + ...rest +}) => { + const { t } = useTranslate(); + const { toast } = useToast(); + const options = { + onError: (error: Error) => { + rest.onError?.(); + toast.error(error || t("message.internal_server_error")); + }, + onSuccess: () => { + rest.onSuccess?.(); + toast.success(t("message.success_save")); + }, + }; + const { mutate: createMenu } = useCreate(EntityType.MENU, options); + const { mutate: updateMenu } = useUpdate(EntityType.MENU, options); + const { + watch, + reset, + control, + register, + formState: { errors }, + resetField, + handleSubmit, + } = useForm({ + defaultValues: DEFAULT_VALUES, + }); + const validationRules = { + type: { + required: t("message.type_is_required"), + }, + title: { required: t("message.title_is_required") }, + url: { + required: t("message.url_is_invalid"), + validate: (value: string = "") => + isAbsoluteUrl(value) || t("message.url_is_invalid"), + }, + payload: {}, + }; + const typeValue = watch("type"); + const titleValue = watch("title"); + const onSubmitForm = (params: IMenuItemAttributes) => { + const { url, ...rest } = params; + const payload = typeValue === "web_url" ? { ...rest, url } : rest; + + if (data?.row?.id) { + updateMenu({ + id: data.row.id, + params: payload, + }); + } else { + createMenu({ ...payload, parent: data?.parentId }); + } + }; + + useEffect(() => { + if (data?.row) { + reset(data.row); + } else { + reset(DEFAULT_VALUES); + } + }, [reset, data?.row]); + + return ( + +
+ + + + { + const { onChange, ...rest } = field; + + return ( + { + onChange(value); + resetField("url"); + }} + helperText={errors.type ? errors.type.message : null} + {...rest} + > + {Object.keys(MenuType).map((value, key) => ( + + {t(`label.${value}`)} + + ))} + + ); + }} + /> + + + + + + + {typeValue === MenuType.web_url ? ( + + ) : typeValue === MenuType.postback ? ( + { + return ( + + ); + }} + /> + ) : null} + + +
+
+ ); +}; diff --git a/frontend/src/components/Menu/MenuFormDialog.tsx b/frontend/src/components/Menu/MenuFormDialog.tsx new file mode 100644 index 000000000..dc5a95f6b --- /dev/null +++ b/frontend/src/components/Menu/MenuFormDialog.tsx @@ -0,0 +1,23 @@ +/* + * 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 { MenuForm, MenuFormData } from "./MenuForm"; + +export const MenuFormDialog = ( + props: ComponentFormDialogProps, +) => ( + + Form={MenuForm} + addText="title.add_menu_item" + editText="title.edit_menu_item" + {...props} + /> +); diff --git a/frontend/src/components/Menu/index.tsx b/frontend/src/components/Menu/index.tsx index bab6d447f..49685f26b 100644 --- a/frontend/src/components/Menu/index.tsx +++ b/frontend/src/components/Menu/index.tsx @@ -8,35 +8,26 @@ import { faBars } from "@fortawesome/free-solid-svg-icons"; import AddIcon from "@mui/icons-material/Add"; -import { Grid, Paper, Button, Box, debounce } from "@mui/material"; -import React, { useRef, useState } from "react"; +import { Box, Button, debounce, Grid, Paper } from "@mui/material"; +import { useRef, useState } from "react"; -import { DeleteDialog } from "@/app-components/dialogs/DeleteDialog"; +import { ConfirmDialogBody } from "@/app-components/dialogs"; import { NoDataOverlay } from "@/app-components/tables/NoDataOverlay"; -import { useCreate } from "@/hooks/crud/useCreate"; import { useDelete } from "@/hooks/crud/useDelete"; import { useFind } from "@/hooks/crud/useFind"; -import { useUpdate } from "@/hooks/crud/useUpdate"; +import { useDialogs } from "@/hooks/useDialogs"; import { useHasPermission } from "@/hooks/useHasPermission"; import { useTranslate } from "@/hooks/useTranslate"; import { PageHeader } from "@/layout/content/PageHeader"; import { EntityType } from "@/services/types"; -import { IMenuItem } from "@/types/menu.types"; import { PermissionAction } from "@/types/permission.types"; import MenuAccordion from "./MenuAccordion"; -import { MenuDialog } from "./MenuDialog"; +import { MenuFormDialog } from "./MenuFormDialog"; export const Menu = () => { const { t } = useTranslate(); - const [addDialogOpened, setAddDialogOpened] = useState(false); - const [editDialogOpened, setEditDialogOpened] = useState(false); - const [selectedMenuId, setSelectedMenuId] = useState( - undefined, - ); - const [editRow, setEditRow] = useState(null); - const [deleteDialogOpened, setDeleteDialogOpened] = useState(false); - const [deleteRowId, setDeleteRowId] = useState(); + const dialogs = useDialogs(); const hasPermission = useHasPermission(); const { data: menus, refetch } = useFind( { entity: EntityType.MENUTREE }, @@ -44,38 +35,11 @@ export const Menu = () => { hasCount: false, }, ); - const { mutateAsync: createMenu } = useCreate(EntityType.MENU, { - onSuccess: () => { - setAddDialogOpened(false); - refetch(); - }, - }); - const { mutateAsync: updateMenu } = useUpdate(EntityType.MENU, { - onSuccess: () => { - setEditDialogOpened(false); - setEditRow(null); - refetch(); - }, - }); - const { mutateAsync: deleteMenu } = useDelete(EntityType.MENU, { + const { mutate: deleteMenu } = useDelete(EntityType.MENU, { onSuccess: () => { - setDeleteDialogOpened(false); refetch(); }, }); - const handleAppend = (menuId: string) => { - setSelectedMenuId(menuId); - setAddDialogOpened(true); - refetch(); - }; - const handleUpdate = (menu: IMenuItem) => { - setEditRow(menu); - setEditDialogOpened(true); - }; - const handleDelete = (menu: IMenuItem) => { - setDeleteRowId(menu.id); - setDeleteDialogOpened(true); - }; const [position, setPosition] = useState(0); const ref = useRef(null); const [shadowVisible, setShadowVisible] = useState(false); @@ -95,10 +59,7 @@ export const Menu = () => { {hasPermission(EntityType.MENU, PermissionAction.CREATE) ? (