From 407581a27eeaeac2117b54b8350b9882c38240ba Mon Sep 17 00:00:00 2001 From: Mohamed Marrouchi Date: Fri, 31 Jan 2025 12:38:52 +0100 Subject: [PATCH 01/23] 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/23] 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/23] 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/23] 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 2ba5c6660a9e195389d74aa7ac9eec7d8aab104e Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Mon, 3 Feb 2025 11:21:05 +0100 Subject: [PATCH 05/23] 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 06/23] 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 89f0811116101e61ae183bf3eb645c60a1983d61 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Mon, 3 Feb 2025 14:52:24 +0100 Subject: [PATCH 07/23] 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 7ef81e7999df1d8979367bc0184e1b4fc84c9d9c Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Mon, 3 Feb 2025 15:02:24 +0100 Subject: [PATCH 08/23] 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 fce63203820c68f49c30b036fc9d2037bcc52744 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Mon, 3 Feb 2025 15:28:05 +0100 Subject: [PATCH 09/23] 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 10/23] 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: Tue, 4 Feb 2025 06:10:11 +0100 Subject: [PATCH 11/23] 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 12/23] 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 13/23] 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 14/23] 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 15/23] 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 16/23] 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 17/23] 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 18/23] 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 19/23] 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 20/23] 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}