From 407581a27eeaeac2117b54b8350b9882c38240ba Mon Sep 17 00:00:00 2001 From: Mohamed Marrouchi Date: Fri, 31 Jan 2025 12:38:52 +0100 Subject: [PATCH 01/53] refactor: dialog system (partial) --- .../app-components/dialogs/DialogTitle.tsx | 20 +- .../src/app-components/dialogs/FormDialog.tsx | 40 ++++ .../components/categories/CategoryForm.tsx | 90 ++++++++ .../categories/CategoryFormDialog.tsx | 37 +++ frontend/src/components/categories/index.tsx | 17 +- frontend/src/contexts/dialogs.context.tsx | 217 ++++++++++++++++++ frontend/src/hooks/useDialogs.ts | 126 ++++++++++ frontend/src/pages/_app.tsx | 53 +++-- 8 files changed, 560 insertions(+), 40 deletions(-) create mode 100644 frontend/src/app-components/dialogs/FormDialog.tsx create mode 100644 frontend/src/components/categories/CategoryForm.tsx create mode 100644 frontend/src/components/categories/CategoryFormDialog.tsx create mode 100644 frontend/src/contexts/dialogs.context.tsx create mode 100644 frontend/src/hooks/useDialogs.ts diff --git a/frontend/src/app-components/dialogs/DialogTitle.tsx b/frontend/src/app-components/dialogs/DialogTitle.tsx index 5bc70726..ce6db7ce 100644 --- a/frontend/src/app-components/dialogs/DialogTitle.tsx +++ b/frontend/src/app-components/dialogs/DialogTitle.tsx @@ -8,10 +8,10 @@ import CloseIcon from "@mui/icons-material/Close"; import { + IconButton, + DialogTitle as MuiDialogTitle, Typography, styled, - DialogTitle as MuiDialogTitle, - IconButton, } from "@mui/material"; const StyledDialogTitle = styled(Typography)(() => ({ @@ -24,12 +24,20 @@ export const DialogTitle = ({ onClose, }: { children: React.ReactNode; - onClose?: () => void; + onClose?: + | ((event: {}, reason: "backdropClick" | "escapeKeyDown") => void) + | undefined; }) => ( {children} - - - + {onClose && ( + onClose(e, "backdropClick")} + > + + + )} ); diff --git a/frontend/src/app-components/dialogs/FormDialog.tsx b/frontend/src/app-components/dialogs/FormDialog.tsx new file mode 100644 index 00000000..262c83cf --- /dev/null +++ b/frontend/src/app-components/dialogs/FormDialog.tsx @@ -0,0 +1,40 @@ +/* + * 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, +} from "@mui/material"; +import { FC, ReactNode } from "react"; + +import { DialogTitle } from "@/app-components/dialogs/DialogTitle"; + +export interface FormDialogProps extends DialogProps { + title: string; + children: ReactNode; +} + +export const FormDialog: FC = ({ + title, + children, + open, + onClose, + ...rest +}) => { + return ( + + {title} + {children} + + {/* */} + + + ); +}; diff --git a/frontend/src/components/categories/CategoryForm.tsx b/frontend/src/components/categories/CategoryForm.tsx new file mode 100644 index 00000000..59fb1ed2 --- /dev/null +++ b/frontend/src/components/categories/CategoryForm.tsx @@ -0,0 +1,90 @@ +/* + * 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 { FC, useEffect } from "react"; +import { useForm } from "react-hook-form"; + +import { ContentContainer } from "@/app-components/dialogs/layouts/ContentContainer"; +import { ContentItem } from "@/app-components/dialogs/layouts/ContentItem"; +import { Input } from "@/app-components/inputs/Input"; +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 { ICategory, ICategoryAttributes } from "@/types/category.types"; + +export type CategoryFormProps = { + data: ICategory | null; +}; + +export const CategoryForm: FC = ({ data }) => { + const { t } = useTranslate(); + const { toast } = useToast(); + const { mutateAsync: createCategory } = useCreate(EntityType.CATEGORY, { + onError: (error) => { + toast.error(error); + }, + onSuccess: () => { + toast.success(t("message.success_save")); + }, + }); + const { mutateAsync: updateCategory } = useUpdate(EntityType.CATEGORY, { + onError: () => { + toast.error(t("message.internal_server_error")); + }, + onSuccess: () => { + toast.success(t("message.success_save")); + }, + }); + const { + reset, + register, + formState: { errors }, + handleSubmit, + } = useForm({ + defaultValues: { label: data?.label || "" }, + }); + const validationRules = { + label: { + required: t("message.label_is_required"), + }, + }; + const onSubmitForm = async (params: ICategoryAttributes) => { + if (data) { + updateCategory({ id: data.id, params }); + } else { + createCategory(params); + } + }; + + useEffect(() => { + if (data) { + reset({ + label: data.label, + }); + } else { + reset(); + } + }, [data, reset]); + + return ( +
+ + + + + +
+ ); +}; diff --git a/frontend/src/components/categories/CategoryFormDialog.tsx b/frontend/src/components/categories/CategoryFormDialog.tsx new file mode 100644 index 00000000..f95f0c35 --- /dev/null +++ b/frontend/src/components/categories/CategoryFormDialog.tsx @@ -0,0 +1,37 @@ +/* + * 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 { FC } from "react"; + +import { FormDialog } from "@/app-components/dialogs/FormDialog"; +import { DialogProps } from "@/contexts/dialogs.context"; +import { useTranslate } from "@/hooks/useTranslate"; +import { ICategory } from "@/types/category.types"; + +import { CategoryForm } from "./CategoryForm"; + +export type CategoryFormProps = DialogProps; + +export const CategoryFormDialog: FC = ({ + onClose, + payload, + ...rest +}) => { + const { t } = useTranslate(); + + return ( + await onClose(false)} + > + + + ); +}; diff --git a/frontend/src/components/categories/index.tsx b/frontend/src/components/categories/index.tsx index d9976536..bdb8bff1 100644 --- a/frontend/src/components/categories/index.tsx +++ b/frontend/src/components/categories/index.tsx @@ -13,7 +13,6 @@ import { Button, Grid, Paper } from "@mui/material"; import { GridColDef, GridRowSelectionModel } from "@mui/x-data-grid"; import { useState } from "react"; -import { DeleteDialog } from "@/app-components/dialogs/DeleteDialog"; import { FilterTextfield } from "@/app-components/inputs/FilterTextfield"; import { ActionColumnLabel, @@ -24,7 +23,8 @@ import { DataGrid } from "@/app-components/tables/DataGrid"; import { useDelete } from "@/hooks/crud/useDelete"; import { useDeleteMany } from "@/hooks/crud/useDeleteMany"; import { useFind } from "@/hooks/crud/useFind"; -import { getDisplayDialogs, useDialog } from "@/hooks/useDialog"; +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"; @@ -36,13 +36,11 @@ import { getDateTimeFormatter } from "@/utils/date"; import { ICategory } from "../../types/category.types"; -import { CategoryDialog } from "./CategoryDialog"; +import { CategoryFormDialog } from "./CategoryFormDialog"; export const Categories = () => { const { t } = useTranslate(); const { toast } = useToast(); - const addDialogCtl = useDialog(false); - const editDialogCtl = useDialog(false); const deleteDialogCtl = useDialog(false); const hasPermission = useHasPermission(); const { onSearch, searchPayload } = useSearch({ @@ -80,7 +78,7 @@ export const Categories = () => { [ { label: ActionColumnLabel.Edit, - action: (row) => editDialogCtl.openDialog(row), + action: (row) => dialogs.open(CategoryFormDialog, row), requires: [PermissionAction.UPDATE], }, { @@ -128,10 +126,11 @@ export const Categories = () => { const handleSelectionChange = (selection: GridRowSelectionModel) => { setSelectedCategories(selection as string[]); }; + const dialogs = useDialogs(); return ( - + {/* { } } }} - /> + /> */} { startIcon={} variant="contained" sx={{ float: "right" }} - onClick={() => addDialogCtl.openDialog()} + onClick={() => dialogs.open(CategoryFormDialog, null)} > {t("button.add")} diff --git a/frontend/src/contexts/dialogs.context.tsx b/frontend/src/contexts/dialogs.context.tsx new file mode 100644 index 00000000..c893e6ca --- /dev/null +++ b/frontend/src/contexts/dialogs.context.tsx @@ -0,0 +1,217 @@ +/* + * 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 * as React from "react"; + +export interface OpenDialogOptions { + /** + * A function that is called before closing the dialog closes. The dialog + * stays open as long as the returned promise is not resolved. Use this if + * you want to perform an async action on close and show a loading state. + * + * @param result The result that the dialog will return after closing. + * @returns A promise that resolves when the dialog can be closed. + */ + onClose?: (result: R) => Promise; +} + +/** + * The props that are passed to a dialog component. + */ +export interface DialogProps

{ + /** + * The payload that was passed when the dialog was opened. + */ + payload: P; + /** + * Whether the dialog is open. + */ + open: boolean; + /** + * A function to call when the dialog should be closed. If the dialog has a return + * value, it should be passed as an argument to this function. You should use the promise + * that is returned to show a loading state while the dialog is performing async actions + * on close. + * @param result The result to return from the dialog. + * @returns A promise that resolves when the dialog can be fully closed. + */ + onClose: (result: R) => Promise; +} + +export type DialogComponent = React.ComponentType>; + +export interface OpenDialog { + /** + * Open a dialog without payload. + * @param Component The dialog component to open. + * @param options Additional options for the dialog. + */ +

( + Component: DialogComponent, + payload?: P, + options?: OpenDialogOptions, + ): Promise; + /** + * Open a dialog and pass a payload. + * @param Component The dialog component to open. + * @param payload The payload to pass to the dialog. + * @param options Additional options for the dialog. + */ + ( + Component: DialogComponent, + payload: P, + options?: OpenDialogOptions, + ): Promise; +} + +export interface CloseDialog { + /** + * Close a dialog and return a result. + * @param dialog The dialog to close. The promise returned by `open`. + * @param result The result to return from the dialog. + * @returns A promise that resolves when the dialog is fully closed. + */ + (dialog: Promise, result: R): Promise; +} + +export interface DialogHook { + // alert: OpenAlertDialog; + // confirm: OpenConfirmDialog; + // prompt: OpenPromptDialog; + open: OpenDialog; + close: CloseDialog; +} + +export const DialogsContext = React.createContext<{ + open: OpenDialog; + close: CloseDialog; +} | null>(null); + +interface DialogStackEntry { + key: string; + open: boolean; + promise: Promise; + Component: DialogComponent; + payload: P; + onClose: (result: R) => Promise; + resolve: (result: R) => void; +} + +export interface DialogProviderProps { + children?: React.ReactNode; + unmountAfter?: number; +} + +/** + * Provider for Dialog stacks. The subtree of this component can use the `useDialogs` hook to + * access the dialogs API. The dialogs are rendered in the order they are requested. + * + * Demos: + * + * - [useDialogs](https://mui.com/toolpad/core/react-use-dialogs/) + * + * API: + * + * - [DialogsProvider API](https://mui.com/toolpad/core/api/dialogs-provider) + */ +function DialogsProvider(props: DialogProviderProps) { + const { children, unmountAfter = 1000 } = props; + const [stack, setStack] = React.useState[]>([]); + const keyPrefix = React.useId(); + const nextId = React.useRef(0); + const requestDialog = React.useCallback( + function open( + Component: DialogComponent, + payload: P, + options: OpenDialogOptions = {}, + ) { + const { onClose = async () => {} } = options; + let resolve: ((result: R) => void) | undefined; + const promise = new Promise((resolveImpl) => { + resolve = resolveImpl; + }); + + if (!resolve) { + throw new Error("resolve not set"); + } + + const key = `${keyPrefix}-${nextId.current}`; + + nextId.current += 1; + + const newEntry: DialogStackEntry = { + key, + open: true, + promise, + Component, + payload, + onClose, + resolve, + }; + + setStack((prevStack) => [...prevStack, newEntry]); + + return promise; + }, + [keyPrefix], + ); + const closeDialogUi = React.useCallback( + function closeDialogUi(dialog: Promise) { + setStack((prevStack) => + prevStack.map((entry) => + entry.promise === dialog ? { ...entry, open: false } : entry, + ), + ); + setTimeout(() => { + // wait for closing animation + setStack((prevStack) => + prevStack.filter((entry) => entry.promise !== dialog), + ); + }, unmountAfter); + }, + [unmountAfter], + ); + const closeDialog = React.useCallback( + async function closeDialog(dialog: Promise, result: R) { + const entryToClose = stack.find((entry) => entry.promise === dialog); + + if (!entryToClose) { + throw new Error("dialog not found"); + } + + await entryToClose.onClose(result); + entryToClose.resolve(result); + closeDialogUi(dialog); + + return dialog; + }, + [stack, closeDialogUi], + ); + const contextValue = React.useMemo( + () => ({ open: requestDialog, close: closeDialog }), + [requestDialog, closeDialog], + ); + + return ( + + {children} + {stack.map(({ key, open, Component, payload, promise }) => ( + { + await closeDialog(promise, result); + }} + /> + ))} + + ); +} + +export { DialogsProvider }; diff --git a/frontend/src/hooks/useDialogs.ts b/frontend/src/hooks/useDialogs.ts new file mode 100644 index 00000000..7a481e48 --- /dev/null +++ b/frontend/src/hooks/useDialogs.ts @@ -0,0 +1,126 @@ +/* + * 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 { useContext, useMemo } from "react"; + +import { + CloseDialog, + DialogsContext, + OpenDialog, +} from "@/contexts/dialogs.context"; + +/* + * 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). + */ + +/* + * 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). + */ + +/* + * 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). + */ + +/* + * 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). + */ + +/* + * 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). + */ + +/* + * 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). + */ + +/* + * 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). + */ + +/* + * 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). + */ + +/* + * 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). + */ + +export interface DialogHook { + // alert: OpenAlertDialog; + // confirm: OpenConfirmDialog; + // prompt: OpenPromptDialog; + open: OpenDialog; + close: CloseDialog; +} + +export function useDialogs(): DialogHook { + const { open, close } = useContext(DialogsContext); + // const alert = React.useCallback( + // async (msg, { onClose, ...options } = {}) => + // open(AlertDialog, { ...options, msg }, { onClose }), + // [open], + // ); + // const confirm = React.useCallback( + // async (msg, { onClose, ...options } = {}) => + // open(ConfirmDialog, { ...options, msg }, { onClose }), + // [open], + // ); + // const prompt = React.useCallback( + // async (msg, { onClose, ...options } = {}) => + // open(PromptDialog, { ...options, msg }, { onClose }), + // [open], + // ); + + return useMemo( + () => ({ + // alert, + // confirm, + // prompt, + open, + close, + }), + // [alert, close, confirm, open, prompt], + [close, open], + ); +} diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx index a0b2067d..d41af17f 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.tsx @@ -20,6 +20,7 @@ import { SnackbarCloseButton } from "@/app-components/displays/Toast/CloseButton import { ApiClientProvider } from "@/contexts/apiClient.context"; import { AuthProvider } from "@/contexts/auth.context"; import { ConfigProvider } from "@/contexts/config.context"; +import { DialogsProvider } from "@/contexts/dialogs.context"; import { PermissionProvider } from "@/contexts/permission.context"; import { SettingsProvider } from "@/contexts/setting.context"; import { ToastProvider } from "@/hooks/useToast"; @@ -72,31 +73,33 @@ const App = ({ Component, pageProps }: TAppPropsWithLayout) => {

- ( - - )} - > - - - - - - - - - {getLayout()} - - - - - - - - - + + ( + + )} + > + + + + + + + + + {getLayout()} + + + + + + + + + +
From 735e783864b298ff92364f38341e2fbcfc5ad192 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Sat, 1 Feb 2025 10:01:00 +0100 Subject: [PATCH 02/53] feat(frontend): add useDialog --- frontend/src/contexts/dialogs.context.tsx | 144 +++++------------ frontend/src/hooks/useDialogs.ts | 117 ++++---------- frontend/src/pages/_app.tsx | 24 +-- frontend/src/types/common/dialogs.types.ts | 173 +++++++++++++++++++++ 4 files changed, 250 insertions(+), 208 deletions(-) create mode 100644 frontend/src/types/common/dialogs.types.ts diff --git a/frontend/src/contexts/dialogs.context.tsx b/frontend/src/contexts/dialogs.context.tsx index c893e6ca..0d0ae032 100644 --- a/frontend/src/contexts/dialogs.context.tsx +++ b/frontend/src/contexts/dialogs.context.tsx @@ -6,106 +6,34 @@ * 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 * as React from "react"; +import { + createContext, + FC, + useCallback, + useId, + useMemo, + useRef, + useState, +} from "react"; -export interface OpenDialogOptions { - /** - * A function that is called before closing the dialog closes. The dialog - * stays open as long as the returned promise is not resolved. Use this if - * you want to perform an async action on close and show a loading state. - * - * @param result The result that the dialog will return after closing. - * @returns A promise that resolves when the dialog can be closed. - */ - onClose?: (result: R) => Promise; -} +import { ConfirmDialog, ConfirmDialogProps } from "@/app-components/dialogs"; +import { + CloseDialog, + DialogComponent, + DialogProviderProps, + DialogStackEntry, + OpenDialog, + OpenDialogOptions, +} from "@/types/common/dialogs.types"; -/** - * The props that are passed to a dialog component. - */ -export interface DialogProps

{ - /** - * The payload that was passed when the dialog was opened. - */ - payload: P; - /** - * Whether the dialog is open. - */ - open: boolean; - /** - * A function to call when the dialog should be closed. If the dialog has a return - * value, it should be passed as an argument to this function. You should use the promise - * that is returned to show a loading state while the dialog is performing async actions - * on close. - * @param result The result to return from the dialog. - * @returns A promise that resolves when the dialog can be fully closed. - */ - onClose: (result: R) => Promise; -} - -export type DialogComponent = React.ComponentType>; - -export interface OpenDialog { - /** - * Open a dialog without payload. - * @param Component The dialog component to open. - * @param options Additional options for the dialog. - */ -

( - Component: DialogComponent, - payload?: P, - options?: OpenDialogOptions, - ): Promise; - /** - * Open a dialog and pass a payload. - * @param Component The dialog component to open. - * @param payload The payload to pass to the dialog. - * @param options Additional options for the dialog. - */ - ( - Component: DialogComponent, - payload: P, - options?: OpenDialogOptions, - ): Promise; -} - -export interface CloseDialog { - /** - * Close a dialog and return a result. - * @param dialog The dialog to close. The promise returned by `open`. - * @param result The result to return from the dialog. - * @returns A promise that resolves when the dialog is fully closed. - */ - (dialog: Promise, result: R): Promise; -} - -export interface DialogHook { - // alert: OpenAlertDialog; - // confirm: OpenConfirmDialog; - // prompt: OpenPromptDialog; - open: OpenDialog; - close: CloseDialog; -} - -export const DialogsContext = React.createContext<{ - open: OpenDialog; - close: CloseDialog; -} | null>(null); - -interface DialogStackEntry { - key: string; - open: boolean; - promise: Promise; - Component: DialogComponent; - payload: P; - onClose: (result: R) => Promise; - resolve: (result: R) => void; -} - -export interface DialogProviderProps { - children?: React.ReactNode; - unmountAfter?: number; -} +export const DialogsContext = createContext< + | { + open: OpenDialog; + close: CloseDialog; + confirm: FC; + } + | undefined +>(undefined); /** * Provider for Dialog stacks. The subtree of this component can use the `useDialogs` hook to @@ -121,10 +49,10 @@ export interface DialogProviderProps { */ function DialogsProvider(props: DialogProviderProps) { const { children, unmountAfter = 1000 } = props; - const [stack, setStack] = React.useState[]>([]); - const keyPrefix = React.useId(); - const nextId = React.useRef(0); - const requestDialog = React.useCallback( + const [stack, setStack] = useState[]>([]); + const keyPrefix = useId(); + const nextId = useRef(0); + const requestDialog = useCallback( function open( Component: DialogComponent, payload: P, @@ -160,7 +88,7 @@ function DialogsProvider(props: DialogProviderProps) { }, [keyPrefix], ); - const closeDialogUi = React.useCallback( + const closeDialogUi = useCallback( function closeDialogUi(dialog: Promise) { setStack((prevStack) => prevStack.map((entry) => @@ -176,7 +104,7 @@ function DialogsProvider(props: DialogProviderProps) { }, [unmountAfter], ); - const closeDialog = React.useCallback( + const closeDialog = useCallback( async function closeDialog(dialog: Promise, result: R) { const entryToClose = stack.find((entry) => entry.promise === dialog); @@ -192,8 +120,12 @@ function DialogsProvider(props: DialogProviderProps) { }, [stack, closeDialogUi], ); - const contextValue = React.useMemo( - () => ({ open: requestDialog, close: closeDialog }), + const contextValue = useMemo( + () => ({ + open: requestDialog, + close: closeDialog, + confirm: ConfirmDialog, + }), [requestDialog, closeDialog], ); diff --git a/frontend/src/hooks/useDialogs.ts b/frontend/src/hooks/useDialogs.ts index 7a481e48..c6fd8879 100644 --- a/frontend/src/hooks/useDialogs.ts +++ b/frontend/src/hooks/useDialogs.ts @@ -6,121 +6,56 @@ * 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 { useContext, useMemo } from "react"; +import React, { useContext, useMemo } from "react"; +import { ConfirmDialog } from "@/app-components/dialogs"; +import { DialogsContext } from "@/contexts/dialogs.context"; import { CloseDialog, - DialogsContext, + OpenConfirmDialog, OpenDialog, -} from "@/contexts/dialogs.context"; - -/* - * 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). - */ - -/* - * 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). - */ - -/* - * 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). - */ - -/* - * 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). - */ - -/* - * 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). - */ - -/* - * 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). - */ - -/* - * 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). - */ - -/* - * 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). - */ - -/* - * 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). - */ +} from "@/types/common/dialogs.types"; export interface DialogHook { - // alert: OpenAlertDialog; - // confirm: OpenConfirmDialog; - // prompt: OpenPromptDialog; open: OpenDialog; close: CloseDialog; + // alert: OpenAlertDialog; + // prompt: OpenPromptDialog; + confirm: OpenConfirmDialog; } -export function useDialogs(): DialogHook { - const { open, close } = useContext(DialogsContext); +export const useDialogs = (): DialogHook => { + const context = useContext(DialogsContext); + + if (!context) { + throw new Error("useDialogs must be used within a DialogsProvider"); + } + + const { open, close } = context; // const alert = React.useCallback( // async (msg, { onClose, ...options } = {}) => // open(AlertDialog, { ...options, msg }, { onClose }), // [open], // ); - // const confirm = React.useCallback( - // async (msg, { onClose, ...options } = {}) => - // open(ConfirmDialog, { ...options, msg }, { onClose }), - // [open], - // ); // const prompt = React.useCallback( // async (msg, { onClose, ...options } = {}) => // open(PromptDialog, { ...options, msg }, { onClose }), // [open], // ); + const confirm = React.useCallback( + async (msg, { onClose, ...options } = {}) => + open(ConfirmDialog, { ...options, msg }, { onClose }), + [open], + ); return useMemo( () => ({ - // alert, - // confirm, - // prompt, open, close, + // alert, + // prompt, + confirm, }), - // [alert, close, confirm, open, prompt], - [close, open], + [close, open, confirm], ); -} +}; diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx index bfbca108..fcff228c 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.tsx @@ -86,17 +86,19 @@ const App = ({ Component, pageProps }: TAppPropsWithLayout) => { - - - - - - {getLayout()} - - - - - + + + + + + + {getLayout()} + + + + + + diff --git a/frontend/src/types/common/dialogs.types.ts b/frontend/src/types/common/dialogs.types.ts new file mode 100644 index 00000000..568218ea --- /dev/null +++ b/frontend/src/types/common/dialogs.types.ts @@ -0,0 +1,173 @@ +/* + * 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 { DialogProps as MuiDialogProps } from "@mui/material"; +import { BaseSyntheticEvent, ReactNode } from "react"; + +// context + +export interface OpenDialogOptions { + /** + * A function that is called before closing the dialog closes. The dialog + * stays open as long as the returned promise is not resolved. Use this if + * you want to perform an async action on close and show a loading state. + * + * @param result The result that the dialog will return after closing. + * @returns A promise that resolves when the dialog can be closed. + */ + onClose?: (result: R) => Promise; +} + +/** + * The props that are passed to a dialog component. + */ +export interface DialogProps

{ + /** + * The payload that was passed when the dialog was opened. + */ + payload: P; + /** + * Whether the dialog is open. + */ + open: boolean; + /** + * A function to call when the dialog should be closed. If the dialog has a return + * value, it should be passed as an argument to this function. You should use the promise + * that is returned to show a loading state while the dialog is performing async actions + * on close. + * @param result The result to return from the dialog. + * @returns A promise that resolves when the dialog can be fully closed. + */ + onClose: (result: R) => Promise; +} + +export type DialogComponent = React.ComponentType>; + +export interface OpenDialog { + /** + * Open a dialog without payload. + * @param Component The dialog component to open. + * @param options Additional options for the dialog. + */ +

( + Component: DialogComponent, + payload?: P, + options?: OpenDialogOptions, + ): Promise; + /** + * Open a dialog and pass a payload. + * @param Component The dialog component to open. + * @param payload The payload to pass to the dialog. + * @param options Additional options for the dialog. + */ + ( + Component: DialogComponent, + payload: P, + options?: OpenDialogOptions, + ): Promise; +} + +export interface CloseDialog { + /** + * Close a dialog and return a result. + * @param dialog The dialog to close. The promise returned by `open`. + * @param result The result to return from the dialog. + * @returns A promise that resolves when the dialog is fully closed. + */ + (dialog: Promise, result: R): Promise; +} + +export interface ConfirmOptions extends OpenDialogOptions { + /** + * A title for the dialog. Defaults to `'Confirm'`. + */ + title?: React.ReactNode; + /** + * The text to show in the "Ok" button. Defaults to `'Ok'`. + */ + okText?: React.ReactNode; + /** + * Denotes the purpose of the dialog. This will affect the color of the + * "Ok" button. Defaults to `undefined`. + */ + severity?: "error" | "info" | "success" | "warning"; + /** + * The text to show in the "Cancel" button. Defaults to `'Cancel'`. + */ + cancelText?: React.ReactNode; +} + +export interface OpenConfirmDialog { + /** + * Open a confirmation dialog. Returns a promise that resolves to true if + * the user confirms, false if the user cancels. + * + * @param msg The message to show in the dialog. + * @param options Additional options for the dialog. + * @returns A promise that resolves to true if the user confirms, false if the user cancels. + */ + (msg: React.ReactNode, options?: ConfirmOptions): Promise; +} + +export interface DialogHook { + // alert: OpenAlertDialog; + confirm: OpenConfirmDialog; + // prompt: OpenPromptDialog; + open: OpenDialog; + close: CloseDialog; +} + +export interface DialogStackEntry { + key: string; + open: boolean; + promise: Promise; + Component: DialogComponent; + payload: P; + onClose: (result: R) => Promise; + resolve: (result: R) => void; +} + +export interface DialogProviderProps { + children?: React.ReactNode; + unmountAfter?: number; +} + +// form dialog +export interface FormDialogProps extends MuiDialogProps { + title: string; + children: ReactNode; + onConfirm: ( + e: BaseSyntheticEvent, + ) => Promise; +} + +// form +export type ComponentFormProps = { + data: T | null; + onError?: () => void; + onSuccess?: () => void; +}; + +export interface FormButtonsProps { + onCancel?: () => void; + onConfirm?: ( + e: BaseSyntheticEvent, + ) => Promise; +} + +export type HTMLFormElementExtra = { + submitAsync: ( + e: BaseSyntheticEvent, + ) => Promise; +}; + +export type HTMLFormElementExtension = + | HTMLFormElement + | HTMLFormElementExtra; + +export type ComponentFormDialogProps = DialogProps; From c68939945f9850607e863614c5bcf78c561896f1 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Sun, 2 Feb 2025 13:52:00 +0100 Subject: [PATCH 03/53] refactor(frontend): update categories pages --- .../app-components/buttons/FormButtons.tsx | 42 +++++++ .../src/app-components/dialogs/FormDialog.tsx | 34 +++--- .../dialogs/confirm/ConfirmDialog.tsx | 56 +++++++++ .../dialogs/confirm/ConfirmDialogBody.tsx | 27 +++++ .../confirm/hooks/useDialogLoadingButton.ts | 26 ++++ frontend/src/app-components/dialogs/index.ts | 3 + .../components/categories/CategoryDialog.tsx | 113 ------------------ .../components/categories/CategoryForm.tsx | 71 +++++++---- .../categories/CategoryFormDialog.tsx | 34 ++++-- frontend/src/components/categories/index.tsx | 48 +++++--- 10 files changed, 269 insertions(+), 185 deletions(-) create mode 100644 frontend/src/app-components/buttons/FormButtons.tsx create mode 100644 frontend/src/app-components/dialogs/confirm/ConfirmDialog.tsx create mode 100644 frontend/src/app-components/dialogs/confirm/ConfirmDialogBody.tsx create mode 100644 frontend/src/app-components/dialogs/confirm/hooks/useDialogLoadingButton.ts delete mode 100644 frontend/src/components/categories/CategoryDialog.tsx diff --git a/frontend/src/app-components/buttons/FormButtons.tsx b/frontend/src/app-components/buttons/FormButtons.tsx new file mode 100644 index 00000000..181c9c51 --- /dev/null +++ b/frontend/src/app-components/buttons/FormButtons.tsx @@ -0,0 +1,42 @@ +/* + * 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 CheckIcon from "@mui/icons-material/Check"; +import CloseIcon from "@mui/icons-material/Close"; +import { Button } from "@mui/material"; + +import { useTranslate } from "@/hooks/useTranslate"; +import { FormButtonsProps } from "@/types/common/dialogs.types"; + +export const FormButtons = ({ + onCancel, + onConfirm, +}: FormButtonsProps) => { + const { t } = useTranslate(); + + return ( + <> + + + + ); +}; diff --git a/frontend/src/app-components/dialogs/FormDialog.tsx b/frontend/src/app-components/dialogs/FormDialog.tsx index 262c83cf..62341ce2 100644 --- a/frontend/src/app-components/dialogs/FormDialog.tsx +++ b/frontend/src/app-components/dialogs/FormDialog.tsx @@ -6,34 +6,30 @@ * 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, -} from "@mui/material"; -import { FC, ReactNode } from "react"; +import { Dialog, DialogActions, DialogContent } from "@mui/material"; -import { DialogTitle } from "@/app-components/dialogs/DialogTitle"; +import { DialogTitle } from "@/app-components/dialogs"; +import { FormDialogProps } from "@/types/common/dialogs.types"; -export interface FormDialogProps extends DialogProps { - title: string; - children: ReactNode; -} +import { FormButtons } from "../buttons/FormButtons"; -export const FormDialog: FC = ({ +export const FormDialog = ({ title, children, - open, - onClose, + onConfirm, ...rest -}) => { +}: FormDialogProps) => { return ( -

- {title} + + rest.onClose?.({}, "backdropClick")}> + {title} + {children} - {/* */} + + onCancel={() => rest.onClose?.({}, "backdropClick")} + onConfirm={onConfirm} + /> ); diff --git a/frontend/src/app-components/dialogs/confirm/ConfirmDialog.tsx b/frontend/src/app-components/dialogs/confirm/ConfirmDialog.tsx new file mode 100644 index 00000000..512adeb6 --- /dev/null +++ b/frontend/src/app-components/dialogs/confirm/ConfirmDialog.tsx @@ -0,0 +1,56 @@ +/* + * 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 { Button, Dialog, DialogActions, DialogContent } from "@mui/material"; +import { FC, ReactNode } from "react"; + +import { ConfirmOptions, DialogProps } from "@/types/common/dialogs.types"; + +import { DialogTitle } from "../DialogTitle"; + +import { useDialogLoadingButton } from "./hooks/useDialogLoadingButton"; + +export interface ConfirmDialogPayload extends ConfirmOptions { + msg: ReactNode; +} + +export interface ConfirmDialogProps + extends DialogProps {} + +export const ConfirmDialog: FC = ({ payload, ...rest }) => { + const cancelButtonProps = useDialogLoadingButton(() => rest.onClose(false)); + const okButtonProps = useDialogLoadingButton(() => rest.onClose(true)); + + return ( + rest.onClose(false)} + > + rest.onClose(false)}> + {payload.title} + + {payload.msg} + + + + + + ); +}; diff --git a/frontend/src/app-components/dialogs/confirm/ConfirmDialogBody.tsx b/frontend/src/app-components/dialogs/confirm/ConfirmDialogBody.tsx new file mode 100644 index 00000000..f54f954a --- /dev/null +++ b/frontend/src/app-components/dialogs/confirm/ConfirmDialogBody.tsx @@ -0,0 +1,27 @@ +/* + * 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 ErrorIcon from "@mui/icons-material/Error"; +import { Grid, Typography } from "@mui/material"; + +import { useTranslate } from "@/hooks/useTranslate"; + +export const ConfirmDialogBody = () => { + const { t } = useTranslate(); + + return ( + + + + + + {t("message.item_delete_confirm")} + + + ); +}; diff --git a/frontend/src/app-components/dialogs/confirm/hooks/useDialogLoadingButton.ts b/frontend/src/app-components/dialogs/confirm/hooks/useDialogLoadingButton.ts new file mode 100644 index 00000000..915f9d9c --- /dev/null +++ b/frontend/src/app-components/dialogs/confirm/hooks/useDialogLoadingButton.ts @@ -0,0 +1,26 @@ +/* + * 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 { useState } from "react"; + +export const useDialogLoadingButton = (onClose: () => Promise) => { + const [loading, setLoading] = useState(false); + const handleClick = async () => { + try { + setLoading(true); + await onClose(); + } finally { + setLoading(false); + } + }; + + return { + onClick: handleClick, + loading, + }; +}; diff --git a/frontend/src/app-components/dialogs/index.ts b/frontend/src/app-components/dialogs/index.ts index 6948629d..f56c6c22 100644 --- a/frontend/src/app-components/dialogs/index.ts +++ b/frontend/src/app-components/dialogs/index.ts @@ -6,7 +6,10 @@ * 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). */ +export * from "./confirm/ConfirmDialog"; +export * from "./confirm/ConfirmDialogBody"; export * from "./DeleteDialog"; export * from "./DialogTitle"; +export * from "./FormDialog"; export * from "./layouts/ContentContainer"; export * from "./layouts/ContentItem"; diff --git a/frontend/src/components/categories/CategoryDialog.tsx b/frontend/src/components/categories/CategoryDialog.tsx deleted file mode 100644 index 8a2b69ec..00000000 --- a/frontend/src/components/categories/CategoryDialog.tsx +++ /dev/null @@ -1,113 +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 { FC, useEffect } from "react"; -import { 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 { useCreate } from "@/hooks/crud/useCreate"; -import { useUpdate } from "@/hooks/crud/useUpdate"; -import { DialogControlProps } from "@/hooks/useDialog"; -import { useToast } from "@/hooks/useToast"; -import { useTranslate } from "@/hooks/useTranslate"; -import { EntityType } from "@/services/types"; -import { ICategory, ICategoryAttributes } from "@/types/category.types"; - -export type CategoryDialogProps = DialogControlProps; - -export const CategoryDialog: FC = ({ - open, - data, - closeDialog, - ...rest -}) => { - const { t } = useTranslate(); - const { toast } = useToast(); - const { mutateAsync: createCategory } = useCreate(EntityType.CATEGORY, { - onError: (error) => { - toast.error(error); - }, - onSuccess: () => { - closeDialog(); - toast.success(t("message.success_save")); - }, - }); - const { mutateAsync: updateCategory } = useUpdate(EntityType.CATEGORY, { - onError: () => { - toast.error(t("message.internal_server_error")); - }, - onSuccess: () => { - closeDialog(); - toast.success(t("message.success_save")); - }, - }); - const { - reset, - register, - formState: { errors }, - handleSubmit, - } = useForm({ - defaultValues: { label: data?.label || "" }, - }); - const validationRules = { - label: { - required: t("message.label_is_required"), - }, - }; - const onSubmitForm = async (params: ICategoryAttributes) => { - if (data) { - updateCategory({ id: data.id, params }); - } else { - createCategory(params); - } - }; - - useEffect(() => { - if (open) reset(); - }, [open, reset]); - - useEffect(() => { - if (data) { - reset({ - label: data.label, - }); - } else { - reset(); - } - }, [data, reset]); - - return ( - -
- - {data ? t("title.edit_category") : t("title.new_category")} - - - - - - - - - - - -
-
- ); -}; diff --git a/frontend/src/components/categories/CategoryForm.tsx b/frontend/src/components/categories/CategoryForm.tsx index 59fb1ed2..9294a214 100644 --- a/frontend/src/components/categories/CategoryForm.tsx +++ b/frontend/src/components/categories/CategoryForm.tsx @@ -6,7 +6,12 @@ * 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 { FC, useEffect } from "react"; +import { + BaseSyntheticEvent, + forwardRef, + useEffect, + useImperativeHandle, +} from "react"; import { useForm } from "react-hook-form"; import { ContentContainer } from "@/app-components/dialogs/layouts/ContentContainer"; @@ -18,30 +23,36 @@ import { useToast } from "@/hooks/useToast"; import { useTranslate } from "@/hooks/useTranslate"; import { EntityType } from "@/services/types"; import { ICategory, ICategoryAttributes } from "@/types/category.types"; +import { + ComponentFormProps, + HTMLFormElementExtension, + HTMLFormElementExtra, +} from "@/types/common/dialogs.types"; -export type CategoryFormProps = { - data: ICategory | null; -}; - -export const CategoryForm: FC = ({ data }) => { +export const CategoryForm = forwardRef< + HTMLFormElement, + ComponentFormProps +>(({ data, ...rest }, ref) => { const { t } = useTranslate(); const { toast } = useToast(); - const { mutateAsync: createCategory } = useCreate(EntityType.CATEGORY, { - onError: (error) => { - toast.error(error); + 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 { mutateAsync: updateCategory } = useUpdate(EntityType.CATEGORY, { - onError: () => { - toast.error(t("message.internal_server_error")); - }, - onSuccess: () => { - toast.success(t("message.success_save")); - }, - }); + }; + const { mutateAsync: createCategory } = useCreate( + EntityType.CATEGORY, + options, + ); + const { mutateAsync: updateCategory } = useUpdate( + EntityType.CATEGORY, + options, + ); const { reset, register, @@ -57,11 +68,25 @@ export const CategoryForm: FC = ({ data }) => { }; const onSubmitForm = async (params: ICategoryAttributes) => { if (data) { - updateCategory({ id: data.id, params }); + return await updateCategory({ id: data.id, params }); } else { - createCategory(params); + return await createCategory(params); } }; + const submitAsync = async (e: BaseSyntheticEvent) => { + return await new Promise((resolve) => { + handleSubmit((params) => { + resolve(onSubmitForm(params)); + })(e); + }); + }; + + useImperativeHandle< + HTMLFormElementExtension, + HTMLFormElementExtra + >(ref, () => ({ + submitAsync, + })); useEffect(() => { if (data) { @@ -74,7 +99,7 @@ export const CategoryForm: FC = ({ data }) => { }, [data, reset]); return ( -
+ = ({ data }) => {
); -}; +}); + +CategoryForm.displayName = "CategoryForm"; diff --git a/frontend/src/components/categories/CategoryFormDialog.tsx b/frontend/src/components/categories/CategoryFormDialog.tsx index f95f0c35..e174a222 100644 --- a/frontend/src/components/categories/CategoryFormDialog.tsx +++ b/frontend/src/components/categories/CategoryFormDialog.tsx @@ -6,32 +6,42 @@ * 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 { FC } from "react"; +import { useRef } from "react"; -import { FormDialog } from "@/app-components/dialogs/FormDialog"; -import { DialogProps } from "@/contexts/dialogs.context"; +import { FormDialog } from "@/app-components/dialogs"; import { useTranslate } from "@/hooks/useTranslate"; import { ICategory } from "@/types/category.types"; +import { + ComponentFormDialogProps, + HTMLFormElementExtra, +} from "@/types/common/dialogs.types"; import { CategoryForm } from "./CategoryForm"; -export type CategoryFormProps = DialogProps; - -export const CategoryFormDialog: FC = ({ - onClose, +export const CategoryFormDialog = ({ payload, ...rest -}) => { +}: ComponentFormDialogProps) => { const { t } = useTranslate(); + const formRef = useRef<(HTMLFormElement & HTMLFormElementExtra) | null>( + null, + ); return ( - title={payload ? t("title.edit_category") : t("title.new_category")} + onConfirm={async (e) => { + return await formRef.current?.submitAsync(e); + }} {...rest} - // @TODO: fix typing - onClose={async () => await onClose(false)} > - + { + rest.onClose(true); + }} + /> ); }; diff --git a/frontend/src/components/categories/index.tsx b/frontend/src/components/categories/index.tsx index bdb8bff1..6c974e08 100644 --- a/frontend/src/components/categories/index.tsx +++ b/frontend/src/components/categories/index.tsx @@ -13,6 +13,7 @@ import { Button, Grid, Paper } from "@mui/material"; import { GridColDef, GridRowSelectionModel } from "@mui/x-data-grid"; import { useState } from "react"; +import { ConfirmDialogBody } from "@/app-components/dialogs"; import { FilterTextfield } from "@/app-components/inputs/FilterTextfield"; import { ActionColumnLabel, @@ -78,12 +79,24 @@ export const Categories = () => { [ { label: ActionColumnLabel.Edit, - action: (row) => dialogs.open(CategoryFormDialog, row), + action: async (row) => { + await dialogs.open(CategoryFormDialog, row); + }, requires: [PermissionAction.UPDATE], }, { label: ActionColumnLabel.Delete, - action: (row) => deleteDialogCtl.openDialog(row.id), + action: async ({ id }) => { + const isConfirmed = await dialogs.confirm(, { + title: t("title.warning"), + okText: t("label.yes"), + cancelText: t("label.no"), + }); + + if (isConfirmed) { + await deleteCategory(id); + } + }, requires: [PermissionAction.DELETE], }, ], @@ -130,22 +143,6 @@ export const Categories = () => { return ( - {/* - - { - if (selectedCategories.length > 0) { - deleteCategories(selectedCategories), setSelectedCategories([]); - deleteDialogCtl.closeDialog(); - } else if (deleteDialogCtl?.data) { - { - deleteCategory(deleteDialogCtl.data); - deleteDialogCtl.closeDialog(); - } - } - }} - /> */} { startIcon={} variant="contained" color="error" - onClick={() => deleteDialogCtl.openDialog(undefined)} + onClick={async () => { + const isConfirmed = await dialogs.confirm( + , + { + title: t("title.warning"), + okText: t("label.yes"), + cancelText: t("label.no"), + }, + ); + + if (isConfirmed) { + await deleteCategories(selectedCategories); + } + }} > {t("button.delete")} From 51c3a88d0113c95ab0df21d21af332518cd61fbf Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Mon, 3 Feb 2025 01:12:51 +0100 Subject: [PATCH 04/53] refactor(frontend): update visual editor categories pages --- frontend/src/components/visual-editor/v2/Diagrams.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/visual-editor/v2/Diagrams.tsx b/frontend/src/components/visual-editor/v2/Diagrams.tsx index 4fa0a934..7e6237b0 100644 --- a/frontend/src/components/visual-editor/v2/Diagrams.tsx +++ b/frontend/src/components/visual-editor/v2/Diagrams.tsx @@ -41,7 +41,7 @@ import { useQueryClient } from "react-query"; import { DeleteDialog } from "@/app-components/dialogs"; import { MoveDialog } from "@/app-components/dialogs/MoveDialog"; -import { CategoryDialog } from "@/components/categories/CategoryDialog"; +import { CategoryFormDialog } from "@/components/categories/CategoryFormDialog"; import { isSameEntity } from "@/hooks/crud/helpers"; import { useDeleteFromCache } from "@/hooks/crud/useDelete"; import { useDeleteMany } from "@/hooks/crud/useDeleteMany"; @@ -51,11 +51,11 @@ import { useUpdate, useUpdateCache } from "@/hooks/crud/useUpdate"; import { useUpdateMany } from "@/hooks/crud/useUpdateMany"; import useDebouncedUpdate from "@/hooks/useDebouncedUpdate"; import { getDisplayDialogs, useDialog } from "@/hooks/useDialog"; +import { useDialogs } from "@/hooks/useDialogs"; import { useSearch } from "@/hooks/useSearch"; import { useTranslate } from "@/hooks/useTranslate"; import { EntityType, Format, QueryType, RouterType } from "@/services/types"; import { IBlock } from "@/types/block.types"; -import { ICategory } from "@/types/category.types"; import { BlockPorts } from "@/types/visual-editor.types"; import BlockDialog from "../BlockDialog"; @@ -74,9 +74,9 @@ const Diagrams = () => { const [engine, setEngine] = useState(); const [canvas, setCanvas] = useState(); const [selectedBlockId, setSelectedBlockId] = useState(); + const dialogs = useDialogs(); const deleteDialogCtl = useDialog(false); const moveDialogCtl = useDialog(false); - const addCategoryDialogCtl = useDialog(false); const { mutateAsync: updateBlocks } = useUpdateMany(EntityType.BLOCK); const { buildDiagram, @@ -528,7 +528,6 @@ const Diagrams = () => { }} > - {...deleteDialogCtl} callback={onDelete} /> { width: "42px", minWidth: "42px", }} - onClick={(e) => { - addCategoryDialogCtl.openDialog(); + onClick={async (e) => { + await dialogs.open(CategoryFormDialog, null); e.preventDefault(); }} > From 8ca12e3a3d04a98910a48d5de6415c893b9894ac Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Mon, 3 Feb 2025 10:40:17 +0100 Subject: [PATCH 05/53] feat: zod attachment --- api/package-lock.json | 11 +++++++- api/package.json | 7 ++--- api/src/chat/schemas/types/attachment.ts | 34 +++++++++++++++--------- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/api/package-lock.json b/api/package-lock.json index c9982881..404f3eaf 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -57,7 +57,8 @@ "sanitize-filename": "^1.6.3", "slug": "^8.2.2", "ts-migrate-mongoose": "^3.8.4", - "uuid": "^9.0.1" + "uuid": "^9.0.1", + "zod": "^3.24.1" }, "devDependencies": { "@compodoc/compodoc": "^1.1.24", @@ -20280,6 +20281,14 @@ "resolved": "https://registry.npmjs.org/zepto/-/zepto-1.2.0.tgz", "integrity": "sha512-C1x6lfvBICFTQIMgbt3JqMOno3VOtkWat/xEakLTOurskYIHPmzJrzd1e8BnmtdDVJlGuk5D+FxyCA8MPmkIyA==", "dev": true + }, + "node_modules/zod": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/api/package.json b/api/package.json index cb1d9a61..2cd82dbe 100644 --- a/api/package.json +++ b/api/package.json @@ -92,7 +92,8 @@ "sanitize-filename": "^1.6.3", "slug": "^8.2.2", "ts-migrate-mongoose": "^3.8.4", - "uuid": "^9.0.1" + "uuid": "^9.0.1", + "zod": "^3.24.1" }, "devDependencies": { "@compodoc/compodoc": "^1.1.24", @@ -144,8 +145,8 @@ }, "optionalDependencies": { "@css-inline/css-inline-linux-arm64-musl": "^0.14.1", - "@resvg/resvg-js-linux-arm64-musl": "^2.6.2", - "@resvg/resvg-js-darwin-arm64": "^2.6.2" + "@resvg/resvg-js-darwin-arm64": "^2.6.2", + "@resvg/resvg-js-linux-arm64-musl": "^2.6.2" }, "overrides": { "mjml": "5.0.0-alpha.4" diff --git a/api/src/chat/schemas/types/attachment.ts b/api/src/chat/schemas/types/attachment.ts index bb52b303..d4bac205 100644 --- a/api/src/chat/schemas/types/attachment.ts +++ b/api/src/chat/schemas/types/attachment.ts @@ -6,6 +6,8 @@ * 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 { z } from 'zod'; + export enum FileType { image = 'image', video = 'video', @@ -14,6 +16,8 @@ export enum FileType { unknown = 'unknown', } +export const fileTypeSchema = z.nativeEnum(FileType); + /** * The `AttachmentRef` type defines two possible ways to reference an attachment: * 1. By `id`: This is used when the attachment is uploaded and stored in the Hexabot system. @@ -22,20 +26,24 @@ export enum FileType { * the content is generated or retrieved by a plugin that consumes a third-party API. * In this case, the `url` field contains the direct link to the external resource. */ -export type AttachmentRef = - | { - id: string | null; - } - | { - /** @deprecated To be used only for external URLs (plugins), for stored attachments use "id" instead */ - url: string; - }; -/** IMPORTANT: No need to use generic type here */ -export interface AttachmentPayload { - type: FileType; - payload: T; -} +export const attachmentRefSchema = z.union([ + z.object({ + id: z.string().nullable(), + }), + z.object({ + url: z.string(), + }), +]); + +export type AttachmentRef = z.infer; + +export const attachmentPayloadSchema = z.object({ + type: fileTypeSchema, + payload: attachmentRefSchema, +}); + +export type AttachmentPayload = z.infer; /** @deprecated */ export type WithUrl = A & { url?: string }; From 87e068b2fe695d234563319f2a5f902f142e6a84 Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Mon, 3 Feb 2025 10:44:56 +0100 Subject: [PATCH 06/53] feat: zod capture-var --- api/src/chat/schemas/types/capture-var.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/api/src/chat/schemas/types/capture-var.ts b/api/src/chat/schemas/types/capture-var.ts index 002340ab..f6044cce 100644 --- a/api/src/chat/schemas/types/capture-var.ts +++ b/api/src/chat/schemas/types/capture-var.ts @@ -1,15 +1,19 @@ /* - * 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). */ -export interface CaptureVar { - // entity=`-1` to match text message - // entity=`-2` for postback payload - // entity is `String` for NLP entities - entity: number | string; - context_var: string; -} +import { z } from 'zod'; + +// entity=`-1` to match text message +// entity=`-2` for postback payload +// entity is `String` for NLP entities +export const captureVarSchema = z.object({ + entity: z.union([z.number().min(-2).max(-1), z.string()]), + context_var: z.string(), +}); + +export type CaptureVar = z.infer; From 8084623b061f7aca60978211bf133a99970abe14 Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Mon, 3 Feb 2025 10:48:40 +0100 Subject: [PATCH 07/53] feat: zod position --- api/src/chat/schemas/types/position.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/api/src/chat/schemas/types/position.ts b/api/src/chat/schemas/types/position.ts index b8440f0a..7ff3545a 100644 --- a/api/src/chat/schemas/types/position.ts +++ b/api/src/chat/schemas/types/position.ts @@ -1,12 +1,16 @@ /* - * 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). */ -export type Position = { - x: number; - y: number; -}; +import { z } from 'zod'; + +export const positionSchema = z.object({ + x: z.number(), + y: z.number(), +}); + +export type Position = z.infer; From b09f198da359ae604929ca2c79baee5fe0663c02 Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Mon, 3 Feb 2025 10:52:43 +0100 Subject: [PATCH 08/53] feat: zod subscriber-context --- api/src/chat/schemas/types/subscriberContext.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/api/src/chat/schemas/types/subscriberContext.ts b/api/src/chat/schemas/types/subscriberContext.ts index 9eebff43..100c054d 100644 --- a/api/src/chat/schemas/types/subscriberContext.ts +++ b/api/src/chat/schemas/types/subscriberContext.ts @@ -1,11 +1,15 @@ /* - * 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). */ -export interface SubscriberContext { - vars?: { [key: string]: any }; -} +import { z } from 'zod'; + +export const subscriberContextSchema = z.object({ + vars: z.record(z.any()).optional(), +}); + +export type SubscriberContext = z.infer; From 22046d57e97ad45cda9c1e1f2c83821619a483d8 Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Mon, 3 Feb 2025 10:58:53 +0100 Subject: [PATCH 09/53] feat: zod quick reply --- api/src/chat/schemas/types/quick-reply.ts | 47 ++++++++++++++--------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/api/src/chat/schemas/types/quick-reply.ts b/api/src/chat/schemas/types/quick-reply.ts index 29ef17cd..e58cea15 100644 --- a/api/src/chat/schemas/types/quick-reply.ts +++ b/api/src/chat/schemas/types/quick-reply.ts @@ -6,21 +6,10 @@ * 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 { AttachmentPayload } from './attachment'; -import { PayloadType } from './message'; +import { z } from 'zod'; -export type Payload = - | { - type: PayloadType.location; - coordinates: { - lat: number; - lon: number; - }; - } - | { - type: PayloadType.attachments; - attachment: AttachmentPayload; - }; +import { attachmentPayloadSchema } from './attachment'; +import { PayloadType } from './message'; export enum QuickReplyType { text = 'text', @@ -29,8 +18,28 @@ export enum QuickReplyType { user_email = 'user_email', } -export interface StdQuickReply { - content_type: QuickReplyType; - title: string; - payload: string; -} +export const cordinatesSchema = z.object({ + lat: z.number(), + lon: z.number(), +}); + +export const payloadSchema = z.discriminatedUnion('type', [ + z.object({ + type: z.literal(PayloadType.location), + coordinates: cordinatesSchema, + }), + z.object({ + type: z.literal(PayloadType.attachments), + attachment: attachmentPayloadSchema, + }), +]); + +export const stdQuickReplySchema = z.object({ + content_type: z.nativeEnum(QuickReplyType), + title: z.string(), + payload: z.string(), +}); + +export type Payload = z.infer; + +export type StdQuickReply = z.infer; From aa8c54c511463b5afa98a6bb5a40d90e455f97e4 Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Mon, 3 Feb 2025 11:09:23 +0100 Subject: [PATCH 10/53] feat: zod pattern --- api/src/chat/schemas/types/pattern.ts | 49 +++++++++++++++++---------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/api/src/chat/schemas/types/pattern.ts b/api/src/chat/schemas/types/pattern.ts index 8b0ceab1..9116a9ec 100644 --- a/api/src/chat/schemas/types/pattern.ts +++ b/api/src/chat/schemas/types/pattern.ts @@ -1,29 +1,42 @@ /* - * 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 { z } from 'zod'; + import { PayloadType } from './message'; -export interface PayloadPattern { - label: string; - value: string; - // @todo : rename 'attachment' to 'attachments' - type?: PayloadType; -} +export const PayloadPatternSchema = z.object({ + label: z.string(), + value: z.string(), + type: z.nativeEnum(PayloadType).optional(), +}); -export type NlpPattern = - | { - entity: string; - match: 'entity'; - } - | { - entity: string; - match: 'value'; - value: string; - }; +export type PayloadPattern = z.infer; -export type Pattern = string | RegExp | PayloadPattern | NlpPattern[]; +export const nlpPatternSchema = z.discriminatedUnion('match', [ + z.object({ + entity: z.string(), + match: z.literal('entity'), + }), + z.object({ + entity: z.string(), + match: z.literal('value'), + value: z.string(), + }), +]); + +export type NlpPattern = z.infer; + +export const PatternSchema = z.union([ + z.string(), + z.instanceof(RegExp), + PayloadPatternSchema, + z.array(nlpPatternSchema), +]); + +export type Pattern = z.infer; From 2ba5c6660a9e195389d74aa7ac9eec7d8aab104e Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Mon, 3 Feb 2025 11:21:05 +0100 Subject: [PATCH 11/53] fix(frontend): apply feedback updates --- .../src/app-components/dialogs/FormDialog.tsx | 9 +-- .../components/categories/CategoryForm.tsx | 65 +++++++++---------- .../categories/CategoryFormDialog.tsx | 35 ++++------ frontend/src/types/common/dialogs.types.ts | 26 ++------ 4 files changed, 53 insertions(+), 82 deletions(-) diff --git a/frontend/src/app-components/dialogs/FormDialog.tsx b/frontend/src/app-components/dialogs/FormDialog.tsx index 62341ce2..c397420c 100644 --- a/frontend/src/app-components/dialogs/FormDialog.tsx +++ b/frontend/src/app-components/dialogs/FormDialog.tsx @@ -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 { Dialog, DialogActions, DialogContent } from "@mui/material"; import { DialogTitle } from "@/app-components/dialogs"; @@ -16,7 +17,7 @@ import { FormButtons } from "../buttons/FormButtons"; export const FormDialog = ({ title, children, - onConfirm, + onSubmit, ...rest }: FormDialogProps) => { return ( @@ -26,9 +27,9 @@ export const FormDialog = ({ {children} - + rest.onClose?.({}, "backdropClick")} - onConfirm={onConfirm} + onConfirm={onSubmit} />
diff --git a/frontend/src/components/categories/CategoryForm.tsx b/frontend/src/components/categories/CategoryForm.tsx index 9294a214..170e78da 100644 --- a/frontend/src/components/categories/CategoryForm.tsx +++ b/frontend/src/components/categories/CategoryForm.tsx @@ -1,17 +1,13 @@ /* - * 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 { - BaseSyntheticEvent, - forwardRef, - useEffect, - useImperativeHandle, -} from "react"; + +import React, { BaseSyntheticEvent, FC, useEffect } from "react"; import { useForm } from "react-hook-form"; import { ContentContainer } from "@/app-components/dialogs/layouts/ContentContainer"; @@ -23,16 +19,14 @@ import { useToast } from "@/hooks/useToast"; import { useTranslate } from "@/hooks/useTranslate"; import { EntityType } from "@/services/types"; import { ICategory, ICategoryAttributes } from "@/types/category.types"; -import { - ComponentFormProps, - HTMLFormElementExtension, - HTMLFormElementExtra, -} from "@/types/common/dialogs.types"; +import { ComponentFormProps } from "@/types/common/dialogs.types"; -export const CategoryForm = forwardRef< - HTMLFormElement, - ComponentFormProps ->(({ data, ...rest }, ref) => { +export const CategoryForm: FC> = ({ + data, + FormWrapper = React.Fragment, + FormWrapperProps, + ...rest +}) => { const { t } = useTranslate(); const { toast } = useToast(); const options = { @@ -81,13 +75,6 @@ export const CategoryForm = forwardRef< }); }; - useImperativeHandle< - HTMLFormElementExtension, - HTMLFormElementExtra - >(ref, () => ({ - submitAsync, - })); - useEffect(() => { if (data) { reset({ @@ -99,19 +86,25 @@ export const CategoryForm = forwardRef< }, [data, reset]); return ( -
- - - - - -
+ +
+ + + + + +
+
); -}); +}; CategoryForm.displayName = "CategoryForm"; diff --git a/frontend/src/components/categories/CategoryFormDialog.tsx b/frontend/src/components/categories/CategoryFormDialog.tsx index e174a222..a116be00 100644 --- a/frontend/src/components/categories/CategoryFormDialog.tsx +++ b/frontend/src/components/categories/CategoryFormDialog.tsx @@ -1,20 +1,16 @@ /* - * 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 { useRef } from "react"; import { FormDialog } from "@/app-components/dialogs"; import { useTranslate } from "@/hooks/useTranslate"; import { ICategory } from "@/types/category.types"; -import { - ComponentFormDialogProps, - HTMLFormElementExtra, -} from "@/types/common/dialogs.types"; +import { ComponentFormDialogProps } from "@/types/common/dialogs.types"; import { CategoryForm } from "./CategoryForm"; @@ -23,25 +19,18 @@ export const CategoryFormDialog = ({ ...rest }: ComponentFormDialogProps) => { const { t } = useTranslate(); - const formRef = useRef<(HTMLFormElement & HTMLFormElementExtra) | null>( - null, - ); return ( - - title={payload ? t("title.edit_category") : t("title.new_category")} - onConfirm={async (e) => { - return await formRef.current?.submitAsync(e); + { + rest.onClose(true); }} - {...rest} - > - { - rest.onClose(true); - }} - /> - + FormWrapper={FormDialog} + FormWrapperProps={{ + title: payload ? t("title.edit_category") : t("title.new_category"), + ...rest, + }} + /> ); }; diff --git a/frontend/src/types/common/dialogs.types.ts b/frontend/src/types/common/dialogs.types.ts index 568218ea..9ecdbbf4 100644 --- a/frontend/src/types/common/dialogs.types.ts +++ b/frontend/src/types/common/dialogs.types.ts @@ -7,7 +7,7 @@ */ import { DialogProps as MuiDialogProps } from "@mui/material"; -import { BaseSyntheticEvent, ReactNode } from "react"; +import { BaseSyntheticEvent } from "react"; // context @@ -139,11 +139,9 @@ export interface DialogProviderProps { // form dialog export interface FormDialogProps extends MuiDialogProps { - title: string; - children: ReactNode; - onConfirm: ( - e: BaseSyntheticEvent, - ) => Promise; + title?: string; + children?: React.ReactNode; + onSubmit: (e: BaseSyntheticEvent) => Promise; } // form @@ -151,23 +149,13 @@ export type ComponentFormProps = { data: T | null; onError?: () => void; onSuccess?: () => void; + FormWrapper?: React.FC>; + FormWrapperProps?: Partial>; }; export interface FormButtonsProps { onCancel?: () => void; - onConfirm?: ( - e: BaseSyntheticEvent, - ) => Promise; + onConfirm: (e: BaseSyntheticEvent) => Promise; } -export type HTMLFormElementExtra = { - submitAsync: ( - e: BaseSyntheticEvent, - ) => Promise; -}; - -export type HTMLFormElementExtension = - | HTMLFormElement - | HTMLFormElementExtra; - export type ComponentFormDialogProps = DialogProps; From d3c8ff54899fa885ff1277807627393490398613 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Mon, 3 Feb 2025 11:29:52 +0100 Subject: [PATCH 12/53] fix(frontend): remove unused comments --- frontend/src/hooks/useDialogs.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/frontend/src/hooks/useDialogs.ts b/frontend/src/hooks/useDialogs.ts index c6fd8879..f3afc818 100644 --- a/frontend/src/hooks/useDialogs.ts +++ b/frontend/src/hooks/useDialogs.ts @@ -19,8 +19,6 @@ import { export interface DialogHook { open: OpenDialog; close: CloseDialog; - // alert: OpenAlertDialog; - // prompt: OpenPromptDialog; confirm: OpenConfirmDialog; } @@ -32,16 +30,6 @@ export const useDialogs = (): DialogHook => { } const { open, close } = context; - // const alert = React.useCallback( - // async (msg, { onClose, ...options } = {}) => - // open(AlertDialog, { ...options, msg }, { onClose }), - // [open], - // ); - // const prompt = React.useCallback( - // async (msg, { onClose, ...options } = {}) => - // open(PromptDialog, { ...options, msg }, { onClose }), - // [open], - // ); const confirm = React.useCallback( async (msg, { onClose, ...options } = {}) => open(ConfirmDialog, { ...options, msg }, { onClose }), @@ -52,8 +40,6 @@ export const useDialogs = (): DialogHook => { () => ({ open, close, - // alert, - // prompt, confirm, }), [close, open, confirm], From e33b483ff32e1637d70006a352acc7ce5034b579 Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Mon, 3 Feb 2025 12:06:52 +0100 Subject: [PATCH 13/53] fix: broadcast channel provider missing for widget --- widget/src/ChatWidget.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/widget/src/ChatWidget.tsx b/widget/src/ChatWidget.tsx index 9c7966c8..ac8853f8 100644 --- a/widget/src/ChatWidget.tsx +++ b/widget/src/ChatWidget.tsx @@ -1,15 +1,17 @@ /* - * 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 "normalize.css"; import "./ChatWidget.css"; import Launcher from "./components/Launcher"; import UserSubscription from "./components/UserSubscription"; +import BroadcastChannelProvider from "./providers/BroadcastChannelProvider"; import ChatProvider from "./providers/ChatProvider"; import { ColorProvider } from "./providers/ColorProvider"; import { ConfigProvider } from "./providers/ConfigProvider"; @@ -29,9 +31,11 @@ function ChatWidget(props: Partial) { - - - + + + + + From 22007e451aec49e1020aca45c01bc94faf89efca Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Mon, 3 Feb 2025 12:36:32 +0100 Subject: [PATCH 14/53] fix: update pattern schema camel case --- api/src/chat/schemas/types/pattern.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/chat/schemas/types/pattern.ts b/api/src/chat/schemas/types/pattern.ts index 9116a9ec..80f191d9 100644 --- a/api/src/chat/schemas/types/pattern.ts +++ b/api/src/chat/schemas/types/pattern.ts @@ -32,11 +32,11 @@ export const nlpPatternSchema = z.discriminatedUnion('match', [ export type NlpPattern = z.infer; -export const PatternSchema = z.union([ +export const patternSchema = z.union([ z.string(), z.instanceof(RegExp), PayloadPatternSchema, z.array(nlpPatternSchema), ]); -export type Pattern = z.infer; +export type Pattern = z.infer; From a6d4f78c39b3c690dbb5ae81a6e6d0a0b1ce6192 Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Mon, 3 Feb 2025 13:38:03 +0100 Subject: [PATCH 15/53] fix: change payloadPatternSchema to camel case --- api/src/chat/schemas/types/pattern.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/src/chat/schemas/types/pattern.ts b/api/src/chat/schemas/types/pattern.ts index 80f191d9..7c43cf11 100644 --- a/api/src/chat/schemas/types/pattern.ts +++ b/api/src/chat/schemas/types/pattern.ts @@ -10,13 +10,13 @@ import { z } from 'zod'; import { PayloadType } from './message'; -export const PayloadPatternSchema = z.object({ +export const payloadPatternSchema = z.object({ label: z.string(), value: z.string(), type: z.nativeEnum(PayloadType).optional(), }); -export type PayloadPattern = z.infer; +export type PayloadPattern = z.infer; export const nlpPatternSchema = z.discriminatedUnion('match', [ z.object({ @@ -35,7 +35,7 @@ export type NlpPattern = z.infer; export const patternSchema = z.union([ z.string(), z.instanceof(RegExp), - PayloadPatternSchema, + payloadPatternSchema, z.array(nlpPatternSchema), ]); From 7a8545e646ac142972a10417885901a936371c5a Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Mon, 3 Feb 2025 13:52:49 +0100 Subject: [PATCH 16/53] feat: replace joi with zod is-pattern-list --- .../chat/validation-rules/is-pattern-list.ts | 63 +++---------------- 1 file changed, 9 insertions(+), 54 deletions(-) diff --git a/api/src/chat/validation-rules/is-pattern-list.ts b/api/src/chat/validation-rules/is-pattern-list.ts index 528144ee..c6f85852 100644 --- a/api/src/chat/validation-rules/is-pattern-list.ts +++ b/api/src/chat/validation-rules/is-pattern-list.ts @@ -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. @@ -12,62 +12,17 @@ import { ValidatorConstraint, ValidatorConstraintInterface, } from 'class-validator'; -import Joi from 'joi'; -import { Pattern } from '../schemas/types/pattern'; +import { Pattern, patternSchema } from '../schemas/types/pattern'; export function isPatternList(patterns: Pattern[]) { - return ( - Array.isArray(patterns) && - patterns.every((pattern) => { - if (typeof pattern === 'string') { - // Check if valid regex - if (pattern.endsWith('/') && pattern.startsWith('/')) { - try { - new RegExp(pattern.slice(1, -1), 'gi'); - } catch (err) { - return false; - } - return true; - } - // Check if valid string (Equals/Like) - return pattern !== ''; - } else if (Array.isArray(pattern)) { - // Check if valid NLP pattern - const nlpSchema = Joi.array() - .items( - Joi.object().keys({ - entity: Joi.string().required(), - match: Joi.string().valid('entity', 'value').required(), - value: Joi.string().required(), - }), - ) - .min(1); - const nlpCheck = nlpSchema.validate(pattern); - if (nlpCheck.error) { - // console.log('Message validation failed! ', nlpCheck); - } - return !nlpCheck.error; - } else if (typeof pattern === 'object') { - // Invalid structure? - const payloadSchema = Joi.object().keys({ - label: Joi.string().required(), - value: Joi.any().required(), - type: Joi.string(), - }); - const payloadCheck = payloadSchema.validate(pattern); - if (payloadCheck.error) { - // console.log( - // 'Message validation failed! ', - // payloadCheck, - // ); - } - return !payloadCheck.error; - } else { - return false; - } - }) - ); + patterns.every((pattern) => { + const result = patternSchema.safeParse(pattern); + if (!result.success) { + return false; + } + }); + return true; } @ValidatorConstraint({ async: false }) From d4a6c26919a4a2467cf443747aec41a3af6582e7 Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Mon, 3 Feb 2025 14:00:32 +0100 Subject: [PATCH 17/53] fix: ensure the input being passed is an array --- api/src/chat/validation-rules/is-pattern-list.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/src/chat/validation-rules/is-pattern-list.ts b/api/src/chat/validation-rules/is-pattern-list.ts index c6f85852..77ad43f5 100644 --- a/api/src/chat/validation-rules/is-pattern-list.ts +++ b/api/src/chat/validation-rules/is-pattern-list.ts @@ -16,6 +16,9 @@ import { import { Pattern, patternSchema } from '../schemas/types/pattern'; export function isPatternList(patterns: Pattern[]) { + if (!Array.isArray(patterns)) { + return false; + } patterns.every((pattern) => { const result = patternSchema.safeParse(pattern); if (!result.success) { From 37486150df99a52b57035f71ddbbdc804ad25a89 Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Mon, 3 Feb 2025 14:18:47 +0100 Subject: [PATCH 18/53] fix: apply feedback --- api/src/chat/validation-rules/is-pattern-list.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/api/src/chat/validation-rules/is-pattern-list.ts b/api/src/chat/validation-rules/is-pattern-list.ts index 77ad43f5..c64b2d08 100644 --- a/api/src/chat/validation-rules/is-pattern-list.ts +++ b/api/src/chat/validation-rules/is-pattern-list.ts @@ -19,13 +19,8 @@ export function isPatternList(patterns: Pattern[]) { if (!Array.isArray(patterns)) { return false; } - patterns.every((pattern) => { - const result = patternSchema.safeParse(pattern); - if (!result.success) { - return false; - } - }); - return true; + + return patterns.every((pattern) => patternSchema.safeParse(pattern).success); } @ValidatorConstraint({ async: false }) From a3cecf097734bab40e7fe0da209f21db0ef9941f Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Mon, 3 Feb 2025 14:21:33 +0100 Subject: [PATCH 19/53] feat: replace join with zod is-position --- api/src/chat/validation-rules/is-position.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/api/src/chat/validation-rules/is-position.ts b/api/src/chat/validation-rules/is-position.ts index c00ba57b..bbf6e95c 100644 --- a/api/src/chat/validation-rules/is-position.ts +++ b/api/src/chat/validation-rules/is-position.ts @@ -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. @@ -13,18 +13,10 @@ import { ValidatorConstraintInterface, } from 'class-validator'; -import { Position } from '../schemas/types/position'; +import { Position, positionSchema } from '../schemas/types/position'; export function isPosition(position: Position) { - return ( - typeof position === 'object' && - !isNaN(position.x) && - !isNaN(position.y) && - position.x !== Infinity && - position.x !== -Infinity && - position.y !== Infinity && - position.y !== -Infinity - ); + return positionSchema.safeParse(position).success; } @ValidatorConstraint({ async: false }) From e341f2d3da2adcb39820da4fdac3d0afc34917f7 Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Mon, 3 Feb 2025 14:37:55 +0100 Subject: [PATCH 20/53] feat: replace joi with zod captureVar --- .../chat/validation-rules/is-valid-capture.ts | 42 ++++--------------- api/src/utils/test/mocks/block.ts | 2 +- 2 files changed, 9 insertions(+), 35 deletions(-) diff --git a/api/src/chat/validation-rules/is-valid-capture.ts b/api/src/chat/validation-rules/is-valid-capture.ts index 664280ea..3956af1e 100644 --- a/api/src/chat/validation-rules/is-valid-capture.ts +++ b/api/src/chat/validation-rules/is-valid-capture.ts @@ -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. @@ -12,43 +12,17 @@ import { ValidatorConstraint, ValidatorConstraintInterface, } from 'class-validator'; -import Joi from 'joi'; -type Tentity = -1 | -2; - -export interface CaptureVar { - // entity=`-1` to match text message - // entity=`-2` for postback payload - // entity is `String` for NLP entities - entity: Tentity | string; - context_var: string; -} - -const allowedEntityValues: Tentity[] = [-1, -2]; +import { CaptureVar, captureVarSchema } from '../schemas/types/capture-var'; export function isValidVarCapture(vars: CaptureVar[]) { - const captureSchema = Joi.array().items( - Joi.object().keys({ - entity: Joi.alternatives().try( - // `-1` to match text message & `-2` for postback payload - Joi.number() - .valid(...allowedEntityValues) - .required(), - // String for NLP entities - Joi.string().required(), - ), - context_var: Joi.string() - .regex(/^[a-z][a-z_0-9]*$/) - .required(), - }), - ); - - const captureCheck = captureSchema.validate(vars); - if (captureCheck.error) { - // eslint-disable-next-line - console.log('Capture vars validation failed!', captureCheck.error); + if (!Array.isArray(vars)) { + return false; } - return !captureCheck.error; + + return vars.every( + (captureVar) => captureVarSchema.safeParse(captureVar).success, + ); } @ValidatorConstraint({ async: false }) diff --git a/api/src/utils/test/mocks/block.ts b/api/src/utils/test/mocks/block.ts index 556d0399..7562e327 100644 --- a/api/src/utils/test/mocks/block.ts +++ b/api/src/utils/test/mocks/block.ts @@ -13,6 +13,7 @@ import { import { BlockFull } from '@/chat/schemas/block.schema'; import { FileType } from '@/chat/schemas/types/attachment'; import { ButtonType } from '@/chat/schemas/types/button'; +import { CaptureVar } from '@/chat/schemas/types/capture-var'; import { OutgoingMessageFormat, PayloadType, @@ -20,7 +21,6 @@ import { import { BlockOptions, ContentOptions } from '@/chat/schemas/types/options'; import { Pattern } from '@/chat/schemas/types/pattern'; import { QuickReplyType } from '@/chat/schemas/types/quick-reply'; -import { CaptureVar } from '@/chat/validation-rules/is-valid-capture'; import { modelInstance } from './misc'; From 89f0811116101e61ae183bf3eb645c60a1983d61 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Mon, 3 Feb 2025 14:52:24 +0100 Subject: [PATCH 21/53] fix(frontend): add retrocompatibility for dialog close button --- .../src/app-components/dialogs/DialogTitle.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/frontend/src/app-components/dialogs/DialogTitle.tsx b/frontend/src/app-components/dialogs/DialogTitle.tsx index ce6db7ce..b005cd5c 100644 --- a/frontend/src/app-components/dialogs/DialogTitle.tsx +++ b/frontend/src/app-components/dialogs/DialogTitle.tsx @@ -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 CloseIcon from "@mui/icons-material/Close"; import { IconButton, @@ -22,9 +23,11 @@ const StyledDialogTitle = styled(Typography)(() => ({ export const DialogTitle = ({ children, onClose, + onCloseV2, }: { children: React.ReactNode; - onClose?: + onClose?: () => void; + onCloseV2?: | ((event: {}, reason: "backdropClick" | "escapeKeyDown") => void) | undefined; }) => ( @@ -34,7 +37,14 @@ export const DialogTitle = ({ onClose(e, "backdropClick")} + onClick={(e) => { + if (onCloseV2) { + onCloseV2(e, "backdropClick"); + } else { + //TODO: the old onClose prop can be replaced by the new one after the full implementation of the useDialogs hook + onClose(); + } + }} > From 011bc69db917f200c5d3e0531407508c07c47458 Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Mon, 3 Feb 2025 14:58:57 +0100 Subject: [PATCH 22/53] feat: channelSchema validation with zod --- .../chat/validation-rules/is-channel-data.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/api/src/chat/validation-rules/is-channel-data.ts b/api/src/chat/validation-rules/is-channel-data.ts index 3dbd4c2d..57f67128 100644 --- a/api/src/chat/validation-rules/is-channel-data.ts +++ b/api/src/chat/validation-rules/is-channel-data.ts @@ -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. @@ -12,18 +12,21 @@ import { ValidatorConstraint, ValidatorConstraintInterface, } from 'class-validator'; +import { z } from 'zod'; -export function isChannelData(channel: any) { - return ( - typeof channel === 'object' && - channel.name && - typeof channel.name === 'string' - ); +export const channelSchema = z.object({ + name: z.string(), +}); + +export type Channel = z.infer; + +export function isChannelData(channel: Channel) { + return channelSchema.safeParse(channel).success; } @ValidatorConstraint({ async: false }) export class ChannelDataValidator implements ValidatorConstraintInterface { - validate(channel: any) { + validate(channel: Channel) { return isChannelData(channel); } } From 7ef81e7999df1d8979367bc0184e1b4fc84c9d9c Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Mon, 3 Feb 2025 15:02:24 +0100 Subject: [PATCH 23/53] fix(frontend): update dialog close button --- .../app-components/dialogs/DialogTitle.tsx | 22 +++---------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/frontend/src/app-components/dialogs/DialogTitle.tsx b/frontend/src/app-components/dialogs/DialogTitle.tsx index b005cd5c..ba816660 100644 --- a/frontend/src/app-components/dialogs/DialogTitle.tsx +++ b/frontend/src/app-components/dialogs/DialogTitle.tsx @@ -6,7 +6,6 @@ * 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 CloseIcon from "@mui/icons-material/Close"; import { IconButton, @@ -23,31 +22,16 @@ const StyledDialogTitle = styled(Typography)(() => ({ export const DialogTitle = ({ children, onClose, - onCloseV2, }: { children: React.ReactNode; onClose?: () => void; - onCloseV2?: - | ((event: {}, reason: "backdropClick" | "escapeKeyDown") => void) - | undefined; }) => ( {children} - {onClose && ( - { - if (onCloseV2) { - onCloseV2(e, "backdropClick"); - } else { - //TODO: the old onClose prop can be replaced by the new one after the full implementation of the useDialogs hook - onClose(); - } - }} - > + {onClose ? ( + - )} + ) : null} ); From 5108169174d5081e79c96cf68353d53569228d7d Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Mon, 3 Feb 2025 15:14:22 +0100 Subject: [PATCH 24/53] fix: add regex for context_var --- api/src/chat/schemas/types/capture-var.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/chat/schemas/types/capture-var.ts b/api/src/chat/schemas/types/capture-var.ts index f6044cce..54a821e6 100644 --- a/api/src/chat/schemas/types/capture-var.ts +++ b/api/src/chat/schemas/types/capture-var.ts @@ -13,7 +13,7 @@ import { z } from 'zod'; // entity is `String` for NLP entities export const captureVarSchema = z.object({ entity: z.union([z.number().min(-2).max(-1), z.string()]), - context_var: z.string(), + context_var: z.string().regex(/^[a-z][a-z_0-9]*$/), }); export type CaptureVar = z.infer; From fce63203820c68f49c30b036fc9d2037bcc52744 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Mon, 3 Feb 2025 15:28:05 +0100 Subject: [PATCH 25/53] fix(frontend): include minor updates --- frontend/src/components/categories/CategoryForm.tsx | 4 +--- frontend/src/components/categories/CategoryFormDialog.tsx | 5 +++-- frontend/src/types/common/dialogs.types.ts | 1 - 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/categories/CategoryForm.tsx b/frontend/src/components/categories/CategoryForm.tsx index 170e78da..714b06ad 100644 --- a/frontend/src/components/categories/CategoryForm.tsx +++ b/frontend/src/components/categories/CategoryForm.tsx @@ -6,12 +6,10 @@ * 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 React, { BaseSyntheticEvent, FC, useEffect } from "react"; import { useForm } from "react-hook-form"; -import { ContentContainer } from "@/app-components/dialogs/layouts/ContentContainer"; -import { ContentItem } from "@/app-components/dialogs/layouts/ContentItem"; +import { ContentContainer, ContentItem } from "@/app-components/dialogs/"; import { Input } from "@/app-components/inputs/Input"; import { useCreate } from "@/hooks/crud/useCreate"; import { useUpdate } from "@/hooks/crud/useUpdate"; diff --git a/frontend/src/components/categories/CategoryFormDialog.tsx b/frontend/src/components/categories/CategoryFormDialog.tsx index a116be00..b90aba6d 100644 --- a/frontend/src/components/categories/CategoryFormDialog.tsx +++ b/frontend/src/components/categories/CategoryFormDialog.tsx @@ -6,6 +6,7 @@ * 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 { FC } from "react"; import { FormDialog } from "@/app-components/dialogs"; import { useTranslate } from "@/hooks/useTranslate"; @@ -14,10 +15,10 @@ import { ComponentFormDialogProps } from "@/types/common/dialogs.types"; import { CategoryForm } from "./CategoryForm"; -export const CategoryFormDialog = ({ +export const CategoryFormDialog: FC> = ({ payload, ...rest -}: ComponentFormDialogProps) => { +}) => { const { t } = useTranslate(); return ( diff --git a/frontend/src/types/common/dialogs.types.ts b/frontend/src/types/common/dialogs.types.ts index 9ecdbbf4..6548f8f8 100644 --- a/frontend/src/types/common/dialogs.types.ts +++ b/frontend/src/types/common/dialogs.types.ts @@ -10,7 +10,6 @@ import { DialogProps as MuiDialogProps } from "@mui/material"; import { BaseSyntheticEvent } from "react"; // context - export interface OpenDialogOptions { /** * A function that is called before closing the dialog closes. The dialog From 0681893b56864b0bfc7a876c3645dc19010d7a71 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Mon, 3 Feb 2025 16:36:00 +0100 Subject: [PATCH 26/53] fix(frontend): add input error attribute --- frontend/src/components/categories/CategoryForm.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/categories/CategoryForm.tsx b/frontend/src/components/categories/CategoryForm.tsx index 714b06ad..8e054abc 100644 --- a/frontend/src/components/categories/CategoryForm.tsx +++ b/frontend/src/components/categories/CategoryForm.tsx @@ -94,6 +94,7 @@ export const CategoryForm: FC> = ({ Date: Mon, 3 Feb 2025 17:44:55 +0100 Subject: [PATCH 27/53] fix: apply feedback --- api/src/chat/schemas/types/channel.ts | 10 +++++++++- api/src/chat/validation-rules/is-channel-data.ts | 7 +------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/api/src/chat/schemas/types/channel.ts b/api/src/chat/schemas/types/channel.ts index 06359493..bdb1ffdb 100644 --- a/api/src/chat/schemas/types/channel.ts +++ b/api/src/chat/schemas/types/channel.ts @@ -1,11 +1,13 @@ /* - * 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 { z } from 'zod'; + import { ChannelName } from '@/channel/types'; export type SubscriberChannelData = @@ -17,3 +19,9 @@ export type SubscriberChannelData = // Channel's specific attributes [P in keyof SubscriberChannelDict[C]]: SubscriberChannelDict[C][P]; }; + +export const channelSchema = z.object({ + name: z.string().regex(/-channel$/) as z.ZodType, +}); + +export type Channel = z.infer; diff --git a/api/src/chat/validation-rules/is-channel-data.ts b/api/src/chat/validation-rules/is-channel-data.ts index 57f67128..ef1590ff 100644 --- a/api/src/chat/validation-rules/is-channel-data.ts +++ b/api/src/chat/validation-rules/is-channel-data.ts @@ -12,13 +12,8 @@ import { ValidatorConstraint, ValidatorConstraintInterface, } from 'class-validator'; -import { z } from 'zod'; -export const channelSchema = z.object({ - name: z.string(), -}); - -export type Channel = z.infer; +import { Channel, channelSchema } from '../schemas/types/channel'; export function isChannelData(channel: Channel) { return channelSchema.safeParse(channel).success; From ac01cc72697a6bef8115609036b0a45b814f84aa Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Mon, 3 Feb 2025 18:02:42 +0100 Subject: [PATCH 28/53] fix: apply feedback naming --- api/src/chat/schemas/types/channel.ts | 4 ++-- api/src/chat/validation-rules/is-channel-data.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/src/chat/schemas/types/channel.ts b/api/src/chat/schemas/types/channel.ts index bdb1ffdb..13de7204 100644 --- a/api/src/chat/schemas/types/channel.ts +++ b/api/src/chat/schemas/types/channel.ts @@ -20,8 +20,8 @@ export type SubscriberChannelData = [P in keyof SubscriberChannelDict[C]]: SubscriberChannelDict[C][P]; }; -export const channelSchema = z.object({ +export const channelDataSchema = z.object({ name: z.string().regex(/-channel$/) as z.ZodType, }); -export type Channel = z.infer; +export type Channel = z.infer; diff --git a/api/src/chat/validation-rules/is-channel-data.ts b/api/src/chat/validation-rules/is-channel-data.ts index ef1590ff..5bb6082e 100644 --- a/api/src/chat/validation-rules/is-channel-data.ts +++ b/api/src/chat/validation-rules/is-channel-data.ts @@ -13,10 +13,10 @@ import { ValidatorConstraintInterface, } from 'class-validator'; -import { Channel, channelSchema } from '../schemas/types/channel'; +import { Channel, channelDataSchema } from '../schemas/types/channel'; export function isChannelData(channel: Channel) { - return channelSchema.safeParse(channel).success; + return channelDataSchema.safeParse(channel).success; } @ValidatorConstraint({ async: false }) From a648fec828960d20c909f2e496c9a70916b5f946 Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Mon, 3 Feb 2025 18:30:14 +0100 Subject: [PATCH 29/53] fix: accept other key,values in channelDataSchema --- api/src/chat/schemas/types/channel.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/api/src/chat/schemas/types/channel.ts b/api/src/chat/schemas/types/channel.ts index 13de7204..dee41f1c 100644 --- a/api/src/chat/schemas/types/channel.ts +++ b/api/src/chat/schemas/types/channel.ts @@ -20,8 +20,10 @@ export type SubscriberChannelData = [P in keyof SubscriberChannelDict[C]]: SubscriberChannelDict[C][P]; }; -export const channelDataSchema = z.object({ - name: z.string().regex(/-channel$/) as z.ZodType, -}); +export const channelDataSchema = z + .object({ + name: z.string().regex(/-channel$/) as z.ZodType, + }) + .passthrough(); export type Channel = z.infer; From 42c7d110a2630b1c90c9c676b5bc8f087396dcec Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Mon, 3 Feb 2025 15:04:38 +0100 Subject: [PATCH 30/53] feat: button zod validation --- api/src/chat/schemas/types/button.ts | 36 +++++++++++++++++----------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/api/src/chat/schemas/types/button.ts b/api/src/chat/schemas/types/button.ts index c38cfe7b..37bf74ee 100644 --- a/api/src/chat/schemas/types/button.ts +++ b/api/src/chat/schemas/types/button.ts @@ -1,28 +1,36 @@ /* - * 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 { z } from 'zod'; + export enum ButtonType { postback = 'postback', web_url = 'web_url', } -export type PostBackButton = { - type: ButtonType.postback; - title: string; - payload: string; -}; +const postBackButtonSchema = z.object({ + type: z.literal(ButtonType.postback), + title: z.string(), + payload: z.string(), +}); -export type WebUrlButton = { - type: ButtonType.web_url; - title: string; - url: string; - messenger_extensions?: boolean; - webview_height_ratio?: 'compact' | 'tall' | 'full'; -}; +const webUrlButtonSchema = z.object({ + type: z.literal(ButtonType.web_url), + title: z.string(), + url: z.string().url(), + messenger_extensions: z.boolean().optional(), + webview_height_ratio: z.enum(['compact', 'tall', 'full']).optional(), +}); -export type Button = PostBackButton | WebUrlButton; +export const buttonSchema = z.union([postBackButtonSchema, webUrlButtonSchema]); + +export type PostBackButton = z.infer; + +export type WebUrlButton = z.infer; + +export type Button = z.infer; From 0b19f45b59b52e40dbf3f82b6c02528a64b48bc9 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Tue, 4 Feb 2025 06:10:11 +0100 Subject: [PATCH 31/53] fix(frontend): apply feedback updates --- .../app-components/buttons/FormButtons.tsx | 34 +++++++++++-------- .../src/app-components/dialogs/FormDialog.tsx | 9 +++-- .../components/categories/CategoryForm.tsx | 4 +-- frontend/src/types/common/dialogs.types.ts | 2 +- 4 files changed, 27 insertions(+), 22 deletions(-) diff --git a/frontend/src/app-components/buttons/FormButtons.tsx b/frontend/src/app-components/buttons/FormButtons.tsx index 181c9c51..4a86be72 100644 --- a/frontend/src/app-components/buttons/FormButtons.tsx +++ b/frontend/src/app-components/buttons/FormButtons.tsx @@ -1,34 +1,32 @@ /* - * 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 CheckIcon from "@mui/icons-material/Check"; import CloseIcon from "@mui/icons-material/Close"; -import { Button } from "@mui/material"; +import { Button, Grid } from "@mui/material"; import { useTranslate } from "@/hooks/useTranslate"; import { FormButtonsProps } from "@/types/common/dialogs.types"; -export const FormButtons = ({ +export const DialogFormButtons = ({ onCancel, - onConfirm, + onSubmit, }: FormButtonsProps) => { const { t } = useTranslate(); return ( - <> - + - + + ); }; diff --git a/frontend/src/app-components/dialogs/FormDialog.tsx b/frontend/src/app-components/dialogs/FormDialog.tsx index c397420c..d2f22249 100644 --- a/frontend/src/app-components/dialogs/FormDialog.tsx +++ b/frontend/src/app-components/dialogs/FormDialog.tsx @@ -6,13 +6,12 @@ * 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 { DialogTitle } from "@/app-components/dialogs"; import { FormDialogProps } from "@/types/common/dialogs.types"; -import { FormButtons } from "../buttons/FormButtons"; +import { DialogFormButtons } from "../buttons/FormButtons"; export const FormDialog = ({ title, @@ -26,10 +25,10 @@ export const FormDialog = ({ {title} {children} - - + rest.onClose?.({}, "backdropClick")} - onConfirm={onSubmit} + onSubmit={onSubmit} /> diff --git a/frontend/src/components/categories/CategoryForm.tsx b/frontend/src/components/categories/CategoryForm.tsx index 8e054abc..a2f17f69 100644 --- a/frontend/src/components/categories/CategoryForm.tsx +++ b/frontend/src/components/categories/CategoryForm.tsx @@ -85,9 +85,9 @@ export const CategoryForm: FC> = ({ return (
diff --git a/frontend/src/types/common/dialogs.types.ts b/frontend/src/types/common/dialogs.types.ts index 6548f8f8..5f2a13c8 100644 --- a/frontend/src/types/common/dialogs.types.ts +++ b/frontend/src/types/common/dialogs.types.ts @@ -154,7 +154,7 @@ export type ComponentFormProps = { export interface FormButtonsProps { onCancel?: () => void; - onConfirm: (e: BaseSyntheticEvent) => Promise; + onSubmit: (e: BaseSyntheticEvent) => Promise; } export type ComponentFormDialogProps = DialogProps; From 658bfbc924cc333845393aa78612df4271c8f3b2 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Tue, 4 Feb 2025 11:30:44 +0100 Subject: [PATCH 32/53] fix(frontend): apply feedback updates --- frontend/public/locales/en/translation.json | 4 +- frontend/public/locales/fr/translation.json | 3 +- .../app-components/buttons/FormButtons.tsx | 3 +- .../src/app-components/dialogs/FormDialog.tsx | 13 ++--- .../dialogs/confirm/ConfirmDialog.tsx | 38 +++++++++------ .../dialogs/confirm/ConfirmDialogBody.tsx | 22 +++++++-- .../components/categories/CategoryForm.tsx | 12 ++--- .../categories/CategoryFormDialog.tsx | 4 +- frontend/src/components/categories/index.tsx | 47 +++++++------------ frontend/src/types/common/dialogs.types.ts | 4 +- 10 files changed, 80 insertions(+), 70 deletions(-) diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index dd9bad2a..f090d8af 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -43,7 +43,9 @@ "account_update_success": "Account has been updated successfully", "account_disabled": "Your account has been either disabled or is pending confirmation.", "success_invitation_sent": "Invitation to join has been successfully sent.", - "item_delete_confirm": "Are you sure you want to delete this item?", + "item_delete_confirm": "Are you sure you want to delete this {{0}} item?", + "items_delete_confirm": "Are you sure you want to delete those {{0}} selected items?", + "selected": "selected", "item_delete_success": "Item has been deleted successfully", "success_save": "Changes has been saved!", "no_result_found": "No result found", diff --git a/frontend/public/locales/fr/translation.json b/frontend/public/locales/fr/translation.json index 7ce2c0dd..7378a7f1 100644 --- a/frontend/public/locales/fr/translation.json +++ b/frontend/public/locales/fr/translation.json @@ -43,7 +43,8 @@ "account_update_success": "Le compte a été mis à jour avec succès", "account_disabled": "Votre compte a été désactivé ou est en attente de confirmation.", "success_invitation_sent": "L'invitation a été envoyée avec succès.", - "item_delete_confirm": "Êtes-vous sûr de bien vouloir supprimer cet élément?", + "item_delete_confirm": "Êtes-vous sûr de bien vouloir supprimer cet élément {{0}}?", + "items_delete_confirm": "Êtes-vous sûr de bien vouloir supprimer ces {{0}} éléments sélectionnés?", "item_delete_success": "L'élément a été supprimé avec succès", "success_save": "Les modifications ont été enregistrées!", "no_result_found": "Aucun résultat trouvé", diff --git a/frontend/src/app-components/buttons/FormButtons.tsx b/frontend/src/app-components/buttons/FormButtons.tsx index 4a86be72..04a9f520 100644 --- a/frontend/src/app-components/buttons/FormButtons.tsx +++ b/frontend/src/app-components/buttons/FormButtons.tsx @@ -6,7 +6,6 @@ * 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 CheckIcon from "@mui/icons-material/Check"; import CloseIcon from "@mui/icons-material/Close"; import { Button, Grid } from "@mui/material"; @@ -22,7 +21,7 @@ export const DialogFormButtons = ({ return ( ({ onSubmit, ...rest }: FormDialogProps) => { + const handleClose = () => rest.onClose?.({}, "backdropClick"); + return ( - rest.onClose?.({}, "backdropClick")}> - {title} - + {title} {children} - - rest.onClose?.({}, "backdropClick")} - onSubmit={onSubmit} - /> + + ); diff --git a/frontend/src/app-components/dialogs/confirm/ConfirmDialog.tsx b/frontend/src/app-components/dialogs/confirm/ConfirmDialog.tsx index 512adeb6..37985cee 100644 --- a/frontend/src/app-components/dialogs/confirm/ConfirmDialog.tsx +++ b/frontend/src/app-components/dialogs/confirm/ConfirmDialog.tsx @@ -6,9 +6,16 @@ * 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 { Button, Dialog, DialogActions, DialogContent } from "@mui/material"; +import { + Button, + Dialog, + DialogActions, + DialogContent, + Grid, +} from "@mui/material"; import { FC, ReactNode } from "react"; +import { useTranslate } from "@/hooks/useTranslate"; import { ConfirmOptions, DialogProps } from "@/types/common/dialogs.types"; import { DialogTitle } from "../DialogTitle"; @@ -23,6 +30,7 @@ export interface ConfirmDialogProps extends DialogProps {} export const ConfirmDialog: FC = ({ payload, ...rest }) => { + const { t } = useTranslate(); const cancelButtonProps = useDialogLoadingButton(() => rest.onClose(false)); const okButtonProps = useDialogLoadingButton(() => rest.onClose(true)); @@ -34,22 +42,24 @@ export const ConfirmDialog: FC = ({ payload, ...rest }) => { onClose={() => rest.onClose(false)} > rest.onClose(false)}> - {payload.title} + {payload.title || t("title.warning")} {payload.msg} - - + + + + ); diff --git a/frontend/src/app-components/dialogs/confirm/ConfirmDialogBody.tsx b/frontend/src/app-components/dialogs/confirm/ConfirmDialogBody.tsx index f54f954a..3ac5c9fb 100644 --- a/frontend/src/app-components/dialogs/confirm/ConfirmDialogBody.tsx +++ b/frontend/src/app-components/dialogs/confirm/ConfirmDialogBody.tsx @@ -11,16 +11,30 @@ import { Grid, Typography } from "@mui/material"; import { useTranslate } from "@/hooks/useTranslate"; -export const ConfirmDialogBody = () => { +export const ConfirmDialogBody = ({ + mode = "click", + itemsNumber = 1, +}: { + mode?: "selection" | "click"; + itemsNumber?: number; +}) => { const { t } = useTranslate(); + const DialogBodyText = + itemsNumber === 1 + ? t("message.item_delete_confirm", { + "0": mode === "click" ? "" : t("message.selected"), + }) + : t("message.items_delete_confirm", { + "0": itemsNumber.toString(), + }); return ( - - + + - {t("message.item_delete_confirm")} + {DialogBodyText} ); diff --git a/frontend/src/components/categories/CategoryForm.tsx b/frontend/src/components/categories/CategoryForm.tsx index a2f17f69..47bb9dcb 100644 --- a/frontend/src/components/categories/CategoryForm.tsx +++ b/frontend/src/components/categories/CategoryForm.tsx @@ -21,8 +21,8 @@ import { ComponentFormProps } from "@/types/common/dialogs.types"; export const CategoryForm: FC> = ({ data, - FormWrapper = React.Fragment, - FormWrapperProps, + Wrapper = React.Fragment, + WrapperProps, ...rest }) => { const { t } = useTranslate(); @@ -84,10 +84,10 @@ export const CategoryForm: FC> = ({ }, [data, reset]); return ( - @@ -102,7 +102,7 @@ export const CategoryForm: FC> = ({ - + ); }; diff --git a/frontend/src/components/categories/CategoryFormDialog.tsx b/frontend/src/components/categories/CategoryFormDialog.tsx index b90aba6d..74112ea3 100644 --- a/frontend/src/components/categories/CategoryFormDialog.tsx +++ b/frontend/src/components/categories/CategoryFormDialog.tsx @@ -27,8 +27,8 @@ export const CategoryFormDialog: FC> = ({ onSuccess={() => { rest.onClose(true); }} - FormWrapper={FormDialog} - FormWrapperProps={{ + Wrapper={FormDialog} + WrapperProps={{ title: payload ? t("title.edit_category") : t("title.new_category"), ...rest, }} diff --git a/frontend/src/components/categories/index.tsx b/frontend/src/components/categories/index.tsx index 6c974e08..ea32b1ff 100644 --- a/frontend/src/components/categories/index.tsx +++ b/frontend/src/components/categories/index.tsx @@ -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 AddIcon from "@mui/icons-material/Add"; import DeleteIcon from "@mui/icons-material/Delete"; import FolderIcon from "@mui/icons-material/Folder"; @@ -24,7 +25,6 @@ import { DataGrid } from "@/app-components/tables/DataGrid"; import { useDelete } from "@/hooks/crud/useDelete"; import { useDeleteMany } from "@/hooks/crud/useDeleteMany"; import { useFind } from "@/hooks/crud/useFind"; -import { useDialog } from "@/hooks/useDialog"; import { useDialogs } from "@/hooks/useDialogs"; import { useHasPermission } from "@/hooks/useHasPermission"; import { useSearch } from "@/hooks/useSearch"; @@ -42,7 +42,6 @@ import { CategoryFormDialog } from "./CategoryFormDialog"; export const Categories = () => { const { t } = useTranslate(); const { toast } = useToast(); - const deleteDialogCtl = useDialog(false); const hasPermission = useHasPermission(); const { onSearch, searchPayload } = useSearch({ $iLike: ["label"], @@ -53,26 +52,20 @@ export const Categories = () => { params: searchPayload, }, ); - const { mutateAsync: deleteCategory } = useDelete(EntityType.CATEGORY, { - onError: (error) => { + const options = { + onError: (error: Error) => { toast.error(error.message || t("message.internal_server_error")); }, onSuccess: () => { - deleteDialogCtl.closeDialog(); setSelectedCategories([]); toast.success(t("message.item_delete_success")); }, - }); - const { mutateAsync: deleteCategories } = useDeleteMany(EntityType.CATEGORY, { - onError: (error) => { - toast.error(error.message || t("message.internal_server_error")); - }, - onSuccess: () => { - deleteDialogCtl.closeDialog(); - setSelectedCategories([]); - toast.success(t("message.item_delete_success")); - }, - }); + }; + const { mutate: deleteCategory } = useDelete(EntityType.CATEGORY, options); + const { mutate: deleteCategories } = useDeleteMany( + EntityType.CATEGORY, + options, + ); const [selectedCategories, setSelectedCategories] = useState([]); const actionColumns = useActionColumns( EntityType.CATEGORY, @@ -87,14 +80,10 @@ export const Categories = () => { { label: ActionColumnLabel.Delete, action: async ({ id }) => { - const isConfirmed = await dialogs.confirm(, { - title: t("title.warning"), - okText: t("label.yes"), - cancelText: t("label.no"), - }); + const isConfirmed = await dialogs.confirm(); if (isConfirmed) { - await deleteCategory(id); + deleteCategory(id); } }, requires: [PermissionAction.DELETE], @@ -176,16 +165,14 @@ export const Categories = () => { color="error" onClick={async () => { const isConfirmed = await dialogs.confirm( - , - { - title: t("title.warning"), - okText: t("label.yes"), - cancelText: t("label.no"), - }, + , ); if (isConfirmed) { - await deleteCategories(selectedCategories); + deleteCategories(selectedCategories); } }} > diff --git a/frontend/src/types/common/dialogs.types.ts b/frontend/src/types/common/dialogs.types.ts index 5f2a13c8..e4e9b8b2 100644 --- a/frontend/src/types/common/dialogs.types.ts +++ b/frontend/src/types/common/dialogs.types.ts @@ -148,8 +148,8 @@ export type ComponentFormProps = { data: T | null; onError?: () => void; onSuccess?: () => void; - FormWrapper?: React.FC>; - FormWrapperProps?: Partial>; + Wrapper?: React.FC>; + WrapperProps?: Partial>; }; export interface FormButtonsProps { From dfff8cab10e130f6c2b8e846b7b11f015cf6c6d4 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Tue, 4 Feb 2025 11:37:21 +0100 Subject: [PATCH 33/53] fix(frontend): update categoryForm to use mutate --- .../src/app-components/buttons/FormButtons.tsx | 5 +---- .../src/app-components/dialogs/FormDialog.tsx | 4 ++-- .../src/components/categories/CategoryForm.tsx | 16 +++++----------- frontend/src/types/common/dialogs.types.ts | 12 ++++++------ 4 files changed, 14 insertions(+), 23 deletions(-) diff --git a/frontend/src/app-components/buttons/FormButtons.tsx b/frontend/src/app-components/buttons/FormButtons.tsx index 04a9f520..469b9cf5 100644 --- a/frontend/src/app-components/buttons/FormButtons.tsx +++ b/frontend/src/app-components/buttons/FormButtons.tsx @@ -13,10 +13,7 @@ import { Button, Grid } from "@mui/material"; import { useTranslate } from "@/hooks/useTranslate"; import { FormButtonsProps } from "@/types/common/dialogs.types"; -export const DialogFormButtons = ({ - onCancel, - onSubmit, -}: FormButtonsProps) => { +export const DialogFormButtons = ({ onCancel, onSubmit }: FormButtonsProps) => { const { t } = useTranslate(); return ( diff --git a/frontend/src/app-components/dialogs/FormDialog.tsx b/frontend/src/app-components/dialogs/FormDialog.tsx index 874d507a..8a5bfe24 100644 --- a/frontend/src/app-components/dialogs/FormDialog.tsx +++ b/frontend/src/app-components/dialogs/FormDialog.tsx @@ -13,12 +13,12 @@ import { FormDialogProps } from "@/types/common/dialogs.types"; import { DialogFormButtons } from "../buttons/FormButtons"; -export const FormDialog = ({ +export const FormDialog = ({ title, children, onSubmit, ...rest -}: FormDialogProps) => { +}: FormDialogProps) => { const handleClose = () => rest.onClose?.({}, "backdropClick"); return ( diff --git a/frontend/src/components/categories/CategoryForm.tsx b/frontend/src/components/categories/CategoryForm.tsx index 47bb9dcb..77d1d52a 100644 --- a/frontend/src/components/categories/CategoryForm.tsx +++ b/frontend/src/components/categories/CategoryForm.tsx @@ -37,14 +37,8 @@ export const CategoryForm: FC> = ({ toast.success(t("message.success_save")); }, }; - const { mutateAsync: createCategory } = useCreate( - EntityType.CATEGORY, - options, - ); - const { mutateAsync: updateCategory } = useUpdate( - EntityType.CATEGORY, - options, - ); + const { mutate: createCategory } = useCreate(EntityType.CATEGORY, options); + const { mutate: updateCategory } = useUpdate(EntityType.CATEGORY, options); const { reset, register, @@ -60,13 +54,13 @@ export const CategoryForm: FC> = ({ }; const onSubmitForm = async (params: ICategoryAttributes) => { if (data) { - return await updateCategory({ id: data.id, params }); + updateCategory({ id: data.id, params }); } else { - return await createCategory(params); + createCategory(params); } }; const submitAsync = async (e: BaseSyntheticEvent) => { - return await new Promise((resolve) => { + return await new Promise((resolve) => { handleSubmit((params) => { resolve(onSubmitForm(params)); })(e); diff --git a/frontend/src/types/common/dialogs.types.ts b/frontend/src/types/common/dialogs.types.ts index e4e9b8b2..52b88c07 100644 --- a/frontend/src/types/common/dialogs.types.ts +++ b/frontend/src/types/common/dialogs.types.ts @@ -137,10 +137,10 @@ export interface DialogProviderProps { } // form dialog -export interface FormDialogProps extends MuiDialogProps { +export interface FormDialogProps extends MuiDialogProps { title?: string; children?: React.ReactNode; - onSubmit: (e: BaseSyntheticEvent) => Promise; + onSubmit: (e: BaseSyntheticEvent) => void; } // form @@ -148,13 +148,13 @@ export type ComponentFormProps = { data: T | null; onError?: () => void; onSuccess?: () => void; - Wrapper?: React.FC>; - WrapperProps?: Partial>; + Wrapper?: React.FC; + WrapperProps?: Partial; }; -export interface FormButtonsProps { +export interface FormButtonsProps { onCancel?: () => void; - onSubmit: (e: BaseSyntheticEvent) => Promise; + onSubmit: (e: BaseSyntheticEvent) => void; } export type ComponentFormDialogProps = DialogProps; From b7351e532fc58bc6660b672092fd7e87f98913bb Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Tue, 4 Feb 2025 11:40:44 +0100 Subject: [PATCH 34/53] fix(frontend): remove submitAsync function --- frontend/src/components/categories/CategoryForm.tsx | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/categories/CategoryForm.tsx b/frontend/src/components/categories/CategoryForm.tsx index 77d1d52a..c83b6034 100644 --- a/frontend/src/components/categories/CategoryForm.tsx +++ b/frontend/src/components/categories/CategoryForm.tsx @@ -6,7 +6,7 @@ * 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 React, { BaseSyntheticEvent, FC, useEffect } from "react"; +import React, { FC, useEffect } from "react"; import { useForm } from "react-hook-form"; import { ContentContainer, ContentItem } from "@/app-components/dialogs/"; @@ -59,13 +59,6 @@ export const CategoryForm: FC> = ({ createCategory(params); } }; - const submitAsync = async (e: BaseSyntheticEvent) => { - return await new Promise((resolve) => { - handleSubmit((params) => { - resolve(onSubmitForm(params)); - })(e); - }); - }; useEffect(() => { if (data) { @@ -80,10 +73,10 @@ export const CategoryForm: FC> = ({ return ( -
+ Date: Tue, 4 Feb 2025 11:44:04 +0100 Subject: [PATCH 35/53] fix(frontend): include minor changes --- frontend/src/components/categories/CategoryForm.tsx | 4 ++-- frontend/src/components/categories/index.tsx | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/categories/CategoryForm.tsx b/frontend/src/components/categories/CategoryForm.tsx index c83b6034..866e5cff 100644 --- a/frontend/src/components/categories/CategoryForm.tsx +++ b/frontend/src/components/categories/CategoryForm.tsx @@ -6,7 +6,7 @@ * 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 React, { FC, useEffect } from "react"; +import { FC, Fragment, useEffect } from "react"; import { useForm } from "react-hook-form"; import { ContentContainer, ContentItem } from "@/app-components/dialogs/"; @@ -21,7 +21,7 @@ import { ComponentFormProps } from "@/types/common/dialogs.types"; export const CategoryForm: FC> = ({ data, - Wrapper = React.Fragment, + Wrapper = Fragment, WrapperProps, ...rest }) => { diff --git a/frontend/src/components/categories/index.tsx b/frontend/src/components/categories/index.tsx index ea32b1ff..a8d6248a 100644 --- a/frontend/src/components/categories/index.tsx +++ b/frontend/src/components/categories/index.tsx @@ -6,7 +6,6 @@ * 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 AddIcon from "@mui/icons-material/Add"; import DeleteIcon from "@mui/icons-material/Delete"; import FolderIcon from "@mui/icons-material/Folder"; @@ -42,6 +41,7 @@ import { CategoryFormDialog } from "./CategoryFormDialog"; export const Categories = () => { const { t } = useTranslate(); const { toast } = useToast(); + const dialogs = useDialogs(); const hasPermission = useHasPermission(); const { onSearch, searchPayload } = useSearch({ $iLike: ["label"], @@ -128,7 +128,6 @@ export const Categories = () => { const handleSelectionChange = (selection: GridRowSelectionModel) => { setSelectedCategories(selection as string[]); }; - const dialogs = useDialogs(); return ( From 2962540124c9f78fa526c958436d348ec6dc1552 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Tue, 4 Feb 2025 11:56:31 +0100 Subject: [PATCH 36/53] fix(frontend): update variable name --- .../src/app-components/dialogs/confirm/ConfirmDialogBody.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app-components/dialogs/confirm/ConfirmDialogBody.tsx b/frontend/src/app-components/dialogs/confirm/ConfirmDialogBody.tsx index 3ac5c9fb..cd1a6442 100644 --- a/frontend/src/app-components/dialogs/confirm/ConfirmDialogBody.tsx +++ b/frontend/src/app-components/dialogs/confirm/ConfirmDialogBody.tsx @@ -19,7 +19,7 @@ export const ConfirmDialogBody = ({ itemsNumber?: number; }) => { const { t } = useTranslate(); - const DialogBodyText = + const dialogBodyText = itemsNumber === 1 ? t("message.item_delete_confirm", { "0": mode === "click" ? "" : t("message.selected"), @@ -34,7 +34,7 @@ export const ConfirmDialogBody = ({ - {DialogBodyText} + {dialogBodyText} ); From 1398250e581451181d643c3912a0bca374cf231a Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Tue, 4 Feb 2025 14:46:53 +0100 Subject: [PATCH 37/53] fix(frontend): include minor changes --- frontend/public/locales/en/translation.json | 6 +++--- frontend/public/locales/fr/translation.json | 5 +++-- .../dialogs/confirm/ConfirmDialogBody.tsx | 14 +++++++------- .../src/components/categories/CategoryForm.tsx | 1 + 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index f090d8af..5d2a42a6 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -43,9 +43,9 @@ "account_update_success": "Account has been updated successfully", "account_disabled": "Your account has been either disabled or is pending confirmation.", "success_invitation_sent": "Invitation to join has been successfully sent.", - "item_delete_confirm": "Are you sure you want to delete this {{0}} item?", - "items_delete_confirm": "Are you sure you want to delete those {{0}} selected items?", - "selected": "selected", + "item_delete_confirm": "Are you sure you want to delete this item?", + "item_selected_delete_confirm": "Are you sure you want to delete this selected item?", + "items_selected_delete_confirm": "Are you sure you want to delete those {{0}} selected items?", "item_delete_success": "Item has been deleted successfully", "success_save": "Changes has been saved!", "no_result_found": "No result found", diff --git a/frontend/public/locales/fr/translation.json b/frontend/public/locales/fr/translation.json index 7378a7f1..741e1168 100644 --- a/frontend/public/locales/fr/translation.json +++ b/frontend/public/locales/fr/translation.json @@ -43,8 +43,9 @@ "account_update_success": "Le compte a été mis à jour avec succès", "account_disabled": "Votre compte a été désactivé ou est en attente de confirmation.", "success_invitation_sent": "L'invitation a été envoyée avec succès.", - "item_delete_confirm": "Êtes-vous sûr de bien vouloir supprimer cet élément {{0}}?", - "items_delete_confirm": "Êtes-vous sûr de bien vouloir supprimer ces {{0}} éléments sélectionnés?", + "item_delete_confirm": "Êtes-vous sûr de bien vouloir supprimer cet élément?", + "item_selected_delete_confirm": "Êtes-vous sûr de bien vouloir supprimer cet élément sélectionné?", + "items_selected_delete_confirm": "Êtes-vous sûr de bien vouloir supprimer ces {{0}} éléments sélectionnés?", "item_delete_success": "L'élément a été supprimé avec succès", "success_save": "Les modifications ont été enregistrées!", "no_result_found": "Aucun résultat trouvé", diff --git a/frontend/src/app-components/dialogs/confirm/ConfirmDialogBody.tsx b/frontend/src/app-components/dialogs/confirm/ConfirmDialogBody.tsx index cd1a6442..fb7a353b 100644 --- a/frontend/src/app-components/dialogs/confirm/ConfirmDialogBody.tsx +++ b/frontend/src/app-components/dialogs/confirm/ConfirmDialogBody.tsx @@ -20,13 +20,13 @@ export const ConfirmDialogBody = ({ }) => { const { t } = useTranslate(); const dialogBodyText = - itemsNumber === 1 - ? t("message.item_delete_confirm", { - "0": mode === "click" ? "" : t("message.selected"), - }) - : t("message.items_delete_confirm", { - "0": itemsNumber.toString(), - }); + mode === "selection" + ? itemsNumber === 1 + ? t("message.item_selected_delete_confirm") + : t("message.items_selected_delete_confirm", { + "0": itemsNumber.toString(), + }) + : t("message.item_delete_confirm"); return ( diff --git a/frontend/src/components/categories/CategoryForm.tsx b/frontend/src/components/categories/CategoryForm.tsx index 866e5cff..108f6d4f 100644 --- a/frontend/src/components/categories/CategoryForm.tsx +++ b/frontend/src/components/categories/CategoryForm.tsx @@ -83,6 +83,7 @@ export const CategoryForm: FC> = ({ label={t("placeholder.label")} error={!!errors.label} {...register("label", validationRules.label)} + required autoFocus helperText={errors.label ? errors.label.message : null} /> From 7a19ba67e55e58fcb0769f0a400453250983fef7 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Tue, 4 Feb 2025 14:58:24 +0100 Subject: [PATCH 38/53] fix(frontend): add disabled attribute for category delete button --- frontend/src/components/categories/index.tsx | 41 +++++++++----------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/frontend/src/components/categories/index.tsx b/frontend/src/components/categories/index.tsx index a8d6248a..02da4c0b 100644 --- a/frontend/src/components/categories/index.tsx +++ b/frontend/src/components/categories/index.tsx @@ -156,29 +156,26 @@ export const Categories = () => { ) : null} - {selectedCategories.length > 0 && ( - - - - )} + if (isConfirmed) { + deleteCategories(selectedCategories); + } + }} + disabled={!selectedCategories.length} + startIcon={} + > + {t("button.delete")} + From 64707c5cf5c8940f3d556e0dda1a8afb02ab8d2d Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Tue, 4 Feb 2025 15:42:37 +0100 Subject: [PATCH 39/53] fix(frontend): add feedback updates --- .../app-components/dialogs/confirm/ConfirmDialogBody.tsx | 8 ++++---- frontend/src/components/categories/CategoryForm.tsx | 2 +- frontend/src/components/categories/index.tsx | 2 +- frontend/src/contexts/dialogs.context.tsx | 4 ---- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/frontend/src/app-components/dialogs/confirm/ConfirmDialogBody.tsx b/frontend/src/app-components/dialogs/confirm/ConfirmDialogBody.tsx index fb7a353b..8396a397 100644 --- a/frontend/src/app-components/dialogs/confirm/ConfirmDialogBody.tsx +++ b/frontend/src/app-components/dialogs/confirm/ConfirmDialogBody.tsx @@ -13,18 +13,18 @@ import { useTranslate } from "@/hooks/useTranslate"; export const ConfirmDialogBody = ({ mode = "click", - itemsNumber = 1, + count = 1, }: { mode?: "selection" | "click"; - itemsNumber?: number; + count?: number; }) => { const { t } = useTranslate(); const dialogBodyText = mode === "selection" - ? itemsNumber === 1 + ? count === 1 ? t("message.item_selected_delete_confirm") : t("message.items_selected_delete_confirm", { - "0": itemsNumber.toString(), + "0": count.toString(), }) : t("message.item_delete_confirm"); diff --git a/frontend/src/components/categories/CategoryForm.tsx b/frontend/src/components/categories/CategoryForm.tsx index 108f6d4f..abeb522c 100644 --- a/frontend/src/components/categories/CategoryForm.tsx +++ b/frontend/src/components/categories/CategoryForm.tsx @@ -52,7 +52,7 @@ export const CategoryForm: FC> = ({ required: t("message.label_is_required"), }, }; - const onSubmitForm = async (params: ICategoryAttributes) => { + const onSubmitForm = (params: ICategoryAttributes) => { if (data) { updateCategory({ id: data.id, params }); } else { diff --git a/frontend/src/components/categories/index.tsx b/frontend/src/components/categories/index.tsx index 02da4c0b..cb968b69 100644 --- a/frontend/src/components/categories/index.tsx +++ b/frontend/src/components/categories/index.tsx @@ -163,7 +163,7 @@ export const Categories = () => { const isConfirmed = await dialogs.confirm( , ); diff --git a/frontend/src/contexts/dialogs.context.tsx b/frontend/src/contexts/dialogs.context.tsx index 0d0ae032..303b0763 100644 --- a/frontend/src/contexts/dialogs.context.tsx +++ b/frontend/src/contexts/dialogs.context.tsx @@ -8,7 +8,6 @@ import { createContext, - FC, useCallback, useId, useMemo, @@ -16,7 +15,6 @@ import { useState, } from "react"; -import { ConfirmDialog, ConfirmDialogProps } from "@/app-components/dialogs"; import { CloseDialog, DialogComponent, @@ -30,7 +28,6 @@ export const DialogsContext = createContext< | { open: OpenDialog; close: CloseDialog; - confirm: FC; } | undefined >(undefined); @@ -124,7 +121,6 @@ function DialogsProvider(props: DialogProviderProps) { () => ({ open: requestDialog, close: closeDialog, - confirm: ConfirmDialog, }), [requestDialog, closeDialog], ); From 268dd6812b1976f2041a708fcbfef18442af8901 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Tue, 4 Feb 2025 16:54:28 +0100 Subject: [PATCH 40/53] fix(frontend): add dialog extra props --- .../dialogs/confirm/ConfirmDialog.tsx | 14 +++++++++++--- frontend/src/components/categories/index.tsx | 6 ++---- frontend/src/contexts/dialogs.context.tsx | 4 +++- frontend/src/hooks/useDialogs.ts | 18 ++++++++++++++++-- frontend/src/types/common/dialogs.types.ts | 7 ++++++- 5 files changed, 38 insertions(+), 11 deletions(-) diff --git a/frontend/src/app-components/dialogs/confirm/ConfirmDialog.tsx b/frontend/src/app-components/dialogs/confirm/ConfirmDialog.tsx index 37985cee..e83d32b7 100644 --- a/frontend/src/app-components/dialogs/confirm/ConfirmDialog.tsx +++ b/frontend/src/app-components/dialogs/confirm/ConfirmDialog.tsx @@ -13,7 +13,7 @@ import { DialogContent, Grid, } from "@mui/material"; -import { FC, ReactNode } from "react"; +import { cloneElement, FC, ReactNode } from "react"; import { useTranslate } from "@/hooks/useTranslate"; import { ConfirmOptions, DialogProps } from "@/types/common/dialogs.types"; @@ -27,12 +27,20 @@ export interface ConfirmDialogPayload extends ConfirmOptions { } export interface ConfirmDialogProps - extends DialogProps {} + extends DialogProps { + mode?: "selection" | "click"; + count?: number; +} export const ConfirmDialog: FC = ({ payload, ...rest }) => { const { t } = useTranslate(); const cancelButtonProps = useDialogLoadingButton(() => rest.onClose(false)); const okButtonProps = useDialogLoadingButton(() => rest.onClose(true)); + // @ts-ignore + const messageReactNode = cloneElement(payload.msg, { + mode: rest.mode, + count: rest.count, + }); return ( = ({ payload, ...rest }) => { rest.onClose(false)}> {payload.title || t("title.warning")} - {payload.msg} + {messageReactNode} ) : null} - {selectedContextVars.length > 0 && ( - - - - )} + + +
From e630d1e2ac660e016425a074e195dcf95ae4c821 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Wed, 5 Feb 2025 11:35:59 +0100 Subject: [PATCH 49/53] fix(frontend): centrelize delete options in one variable --- .../src/components/context-vars/index.tsx | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/context-vars/index.tsx b/frontend/src/components/context-vars/index.tsx index a16a38b1..fdee1011 100644 --- a/frontend/src/components/context-vars/index.tsx +++ b/frontend/src/components/context-vars/index.tsx @@ -60,24 +60,23 @@ export const ContextVars = () => { toast.success(t("message.success_save")); }, }); - const { mutate: deleteContextVar } = useDelete(EntityType.CONTEXT_VAR, { - onError: (error) => { + const options = { + onError: (error: Error) => { toast.error(error); }, onSuccess() { setSelectedContextVars([]); toast.success(t("message.item_delete_success")); }, - }); - const { mutate: deleteContextVars } = useDeleteMany(EntityType.CONTEXT_VAR, { - onError: (error) => { - toast.error(error); - }, - onSuccess: () => { - setSelectedContextVars([]); - toast.success(t("message.item_delete_success")); - }, - }); + }; + const { mutate: deleteContextVar } = useDelete( + EntityType.CONTEXT_VAR, + options, + ); + const { mutate: deleteContextVars } = useDeleteMany( + EntityType.CONTEXT_VAR, + options, + ); const [selectedContextVars, setSelectedContextVars] = useState([]); const actionColumns = useActionColumns( EntityType.CONTEXT_VAR, From 6fe846d36065cd6fb0b6e323319a62df0e051918 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Wed, 5 Feb 2025 11:58:30 +0100 Subject: [PATCH 50/53] refactor(frontend): update label dialogs --- .../src/components/labels/LabelDialog.tsx | 150 ------------------ frontend/src/components/labels/LabelForm.tsx | 131 +++++++++++++++ .../src/components/labels/LabelFormDialog.tsx | 37 +++++ frontend/src/components/labels/index.tsx | 37 ++--- 4 files changed, 184 insertions(+), 171 deletions(-) delete mode 100644 frontend/src/components/labels/LabelDialog.tsx create mode 100644 frontend/src/components/labels/LabelForm.tsx create mode 100644 frontend/src/components/labels/LabelFormDialog.tsx diff --git a/frontend/src/components/labels/LabelDialog.tsx b/frontend/src/components/labels/LabelDialog.tsx deleted file mode 100644 index 3c9b50a5..00000000 --- a/frontend/src/components/labels/LabelDialog.tsx +++ /dev/null @@ -1,150 +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 { FC, useEffect } from "react"; -import { 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 { useCreate } from "@/hooks/crud/useCreate"; -import { useUpdate } from "@/hooks/crud/useUpdate"; -import { DialogControlProps } from "@/hooks/useDialog"; -import { useToast } from "@/hooks/useToast"; -import { useTranslate } from "@/hooks/useTranslate"; -import { EntityType } from "@/services/types"; -import { ILabel, ILabelAttributes } from "@/types/label.types"; -import { slugify } from "@/utils/string"; - -export type LabelDialogProps = DialogControlProps; -export const LabelDialog: FC = ({ - open, - data, - closeDialog, - ...rest -}) => { - const { t } = useTranslate(); - const { toast } = useToast(); - const { mutateAsync: createLabel } = useCreate(EntityType.LABEL, { - onError: () => { - toast.error(t("message.internal_server_error")); - }, - onSuccess() { - closeDialog(); - toast.success(t("message.success_save")); - }, - }); - const { mutateAsync: updateLabel } = useUpdate(EntityType.LABEL, { - onError: () => { - toast.error(t("message.internal_server_error")); - }, - onSuccess() { - closeDialog(); - toast.success(t("message.success_save")); - }, - }); - const { - reset, - register, - setValue, - formState: { errors }, - handleSubmit, - } = useForm({ - defaultValues: { - name: data?.name || "", - title: data?.title || "", - description: data?.description || "", - }, - }); - const validationRules = { - title: { - required: t("message.title_is_required"), - }, - name: {}, - description: {}, - }; - const onSubmitForm = async (params: ILabelAttributes) => { - if (data) { - updateLabel({ id: data.id, params }); - } else { - createLabel(params); - } - }; - - useEffect(() => { - if (open) reset(); - }, [open, reset]); - - useEffect(() => { - if (data) { - reset({ - name: data.name, - title: data.title, - description: data.description, - }); - } else { - reset(); - } - }, [data, reset]); - - return ( - -
- - {data ? t("title.edit_label") : t("title.new_label")} - - - - - { - setValue("title", value); - setValue("name", slugify(value).toUpperCase()); - }, - }} - helperText={errors.title ? errors.title.message : null} - /> - - - - - - - - - - - - -
-
- ); -}; diff --git a/frontend/src/components/labels/LabelForm.tsx b/frontend/src/components/labels/LabelForm.tsx new file mode 100644 index 00000000..d4b4cbf7 --- /dev/null +++ b/frontend/src/components/labels/LabelForm.tsx @@ -0,0 +1,131 @@ +/* + * 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 { FC, Fragment, useEffect } from "react"; +import { useForm } from "react-hook-form"; + +import { ContentContainer, ContentItem } from "@/app-components/dialogs/"; +import { Input } from "@/app-components/inputs/Input"; +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 { ILabel, ILabelAttributes } from "@/types/label.types"; +import { slugify } from "@/utils/string"; + +export const LabelForm: 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: createLabel } = useCreate(EntityType.LABEL, options); + const { mutate: updateLabel } = useUpdate(EntityType.LABEL, options); + const { + reset, + register, + setValue, + formState: { errors }, + handleSubmit, + } = useForm({ + defaultValues: { + name: data?.name || "", + title: data?.title || "", + description: data?.description || "", + }, + }); + const validationRules = { + title: { + required: t("message.title_is_required"), + }, + name: {}, + description: {}, + }; + const onSubmitForm = (params: ILabelAttributes) => { + if (data) { + updateLabel({ id: data.id, params }); + } else { + createLabel(params); + } + }; + + useEffect(() => { + if (data) { + reset({ + name: data.name, + title: data.title, + description: data.description, + }); + } else { + reset(); + } + }, [data, reset]); + + return ( + +
+ + + { + setValue("title", value); + setValue("name", slugify(value).toUpperCase()); + }, + }} + helperText={errors.title ? errors.title.message : null} + /> + + + + + + + + +
+
+ ); +}; diff --git a/frontend/src/components/labels/LabelFormDialog.tsx b/frontend/src/components/labels/LabelFormDialog.tsx new file mode 100644 index 00000000..16f692b7 --- /dev/null +++ b/frontend/src/components/labels/LabelFormDialog.tsx @@ -0,0 +1,37 @@ +/* + * 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 { FC } from "react"; + +import { FormDialog } from "@/app-components/dialogs"; +import { useTranslate } from "@/hooks/useTranslate"; +import { ComponentFormDialogProps } from "@/types/common/dialogs.types"; +import { ILabel } from "@/types/label.types"; + +import { LabelForm } from "./LabelForm"; + +export const LabelFormDialog: FC> = ({ + payload, + ...rest +}) => { + const { t } = useTranslate(); + + return ( + { + rest.onClose(true); + }} + Wrapper={FormDialog} + WrapperProps={{ + title: payload ? t("title.edit_label") : t("title.new_label"), + ...rest, + }} + /> + ); +}; diff --git a/frontend/src/components/labels/index.tsx b/frontend/src/components/labels/index.tsx index 2f529d96..152f12cb 100644 --- a/frontend/src/components/labels/index.tsx +++ b/frontend/src/components/labels/index.tsx @@ -1,18 +1,18 @@ /* - * 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 { faTags } from "@fortawesome/free-solid-svg-icons"; import AddIcon from "@mui/icons-material/Add"; import { Button, Grid, Paper } from "@mui/material"; import { GridColDef } from "@mui/x-data-grid"; -import React from "react"; -import { DeleteDialog } from "@/app-components/dialogs/DeleteDialog"; +import { ConfirmDialogBody } from "@/app-components/dialogs"; import { FilterTextfield } from "@/app-components/inputs/FilterTextfield"; import { ActionColumnLabel, @@ -22,7 +22,7 @@ import { renderHeader } from "@/app-components/tables/columns/renderHeader"; import { DataGrid } from "@/app-components/tables/DataGrid"; import { useDelete } from "@/hooks/crud/useDelete"; import { useFind } from "@/hooks/crud/useFind"; -import { getDisplayDialogs, useDialog } from "@/hooks/useDialog"; +import { useDialogs } from "@/hooks/useDialogs"; import { useHasPermission } from "@/hooks/useHasPermission"; import { useSearch } from "@/hooks/useSearch"; import { useToast } from "@/hooks/useToast"; @@ -33,14 +33,12 @@ import { ILabel } from "@/types/label.types"; import { PermissionAction } from "@/types/permission.types"; import { getDateTimeFormatter } from "@/utils/date"; -import { LabelDialog } from "./LabelDialog"; +import { LabelFormDialog } from "./LabelFormDialog"; export const Labels = () => { const { t } = useTranslate(); const { toast } = useToast(); - const addDialogCtl = useDialog(false); - const editDialogCtl = useDialog(false); - const deleteDialogCtl = useDialog(false); + const dialogs = useDialogs(); const hasPermission = useHasPermission(); const { onSearch, searchPayload } = useSearch({ $or: ["name", "title"], @@ -51,12 +49,11 @@ export const Labels = () => { params: searchPayload, }, ); - const { mutateAsync: deleteLabel } = useDelete(EntityType.LABEL, { + const { mutate: deleteLabel } = useDelete(EntityType.LABEL, { onError: () => { toast.error(t("message.internal_server_error")); }, onSuccess() { - deleteDialogCtl.closeDialog(); toast.success(t("message.item_delete_success")); }, }); @@ -65,12 +62,18 @@ export const Labels = () => { [ { label: ActionColumnLabel.Edit, - action: (row) => editDialogCtl.openDialog(row), + action: (row) => dialogs.open(LabelFormDialog, row), requires: [PermissionAction.UPDATE], }, { label: ActionColumnLabel.Delete, - action: (row) => deleteDialogCtl.openDialog(row.id), + action: async ({ id }) => { + const isConfirmed = await dialogs.confirm(ConfirmDialogBody); + + if (isConfirmed) { + deleteLabel(id); + } + }, requires: [PermissionAction.DELETE], }, ], @@ -149,14 +152,6 @@ export const Labels = () => { return ( - - - { - if (deleteDialogCtl?.data) deleteLabel(deleteDialogCtl.data); - }} - /> { startIcon={} variant="contained" sx={{ float: "right" }} - onClick={() => addDialogCtl.openDialog()} + onClick={() => dialogs.open(LabelFormDialog, null)} > {t("button.add")} From 3e8d5a1e1cd4653ce3784c79e280407df06610b0 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Wed, 5 Feb 2025 15:02:41 +0100 Subject: [PATCH 51/53] fix(frontend): update onError toast error message --- frontend/src/components/labels/LabelForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/labels/LabelForm.tsx b/frontend/src/components/labels/LabelForm.tsx index d4b4cbf7..f4f713c0 100644 --- a/frontend/src/components/labels/LabelForm.tsx +++ b/frontend/src/components/labels/LabelForm.tsx @@ -31,7 +31,7 @@ export const LabelForm: FC> = ({ const options = { onError: (error: Error) => { rest.onError?.(); - toast.error(error || t("message.internal_server_error")); + toast.error(error.message || t("message.internal_server_error")); }, onSuccess: () => { rest.onSuccess?.(); From ea64e2960ec3c5b89ea0be7cbea310e94e33d65d Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Wed, 5 Feb 2025 15:07:02 +0100 Subject: [PATCH 52/53] fix(frontend): update onError toast error message --- frontend/src/components/context-vars/ContextVarForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/context-vars/ContextVarForm.tsx b/frontend/src/components/context-vars/ContextVarForm.tsx index ba8d9ae2..6250daea 100644 --- a/frontend/src/components/context-vars/ContextVarForm.tsx +++ b/frontend/src/components/context-vars/ContextVarForm.tsx @@ -32,7 +32,7 @@ export const ContextVarForm: FC> = ({ const options = { onError: (error: Error) => { rest.onError?.(); - toast.error(error || t("message.internal_server_error")); + toast.error(error.message || t("message.internal_server_error")); }, onSuccess: () => { rest.onSuccess?.(); From c520fd8f36f536e975a7f337f6c95e99056f5e6e Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Wed, 5 Feb 2025 15:07:49 +0100 Subject: [PATCH 53/53] fix(frontend): update onError toast error message --- frontend/src/components/translations/TranslationForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/translations/TranslationForm.tsx b/frontend/src/components/translations/TranslationForm.tsx index c4fb1dcc..a796c94b 100644 --- a/frontend/src/components/translations/TranslationForm.tsx +++ b/frontend/src/components/translations/TranslationForm.tsx @@ -65,7 +65,7 @@ export const TranslationForm: FC> = ({ const { mutate: updateTranslation } = useUpdate(EntityType.TRANSLATION, { onError: (error: Error) => { rest.onError?.(); - toast.error(error || t("message.internal_server_error")); + toast.error(error.message || t("message.internal_server_error")); }, onSuccess() { rest.onSuccess?.();