From 58c96417b59a88e4003fe6e66f95098a3cbbbb4d Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 7 Feb 2025 05:03:00 +0100 Subject: [PATCH 01/10] refactor(frontend): update permissions dialogs --- .../src/components/roles/PermissionsBody.tsx | 251 ++++++++++++++++ .../roles/PermissionsBodyDialog.tsx | 23 ++ .../components/roles/PermissionsDialog.tsx | 278 ------------------ 3 files changed, 274 insertions(+), 278 deletions(-) create mode 100644 frontend/src/components/roles/PermissionsBody.tsx create mode 100644 frontend/src/components/roles/PermissionsBodyDialog.tsx delete mode 100644 frontend/src/components/roles/PermissionsDialog.tsx diff --git a/frontend/src/components/roles/PermissionsBody.tsx b/frontend/src/components/roles/PermissionsBody.tsx new file mode 100644 index 00000000..abf70ebb --- /dev/null +++ b/frontend/src/components/roles/PermissionsBody.tsx @@ -0,0 +1,251 @@ +/* + * 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 AddIcon from "@mui/icons-material/Add"; +import DeleteOutlinedIcon from "@mui/icons-material/DeleteOutlined"; +import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; +import { + Accordion, + AccordionDetails, + AccordionSummary, + Divider, + Grid, + MenuItem, + Paper, + Typography, +} from "@mui/material"; +import { FC, Fragment, useEffect, useState } from "react"; + +import { IconButton } from "@/app-components/buttons/IconButton"; +import { Input } from "@/app-components/inputs/Input"; +import { useCreate } from "@/hooks/crud/useCreate"; +import { useDelete } from "@/hooks/crud/useDelete"; +import { useFind } from "@/hooks/crud/useFind"; +import { useGetFromCache } from "@/hooks/crud/useGet"; +import { useToast } from "@/hooks/useToast"; +import { useTranslate } from "@/hooks/useTranslate"; +import { EntityType, Format } from "@/services/types"; +import { ComponentFormProps } from "@/types/common/dialogs.types"; +import { IPermission, IPermissionAttributes } from "@/types/permission.types"; +import { IRole } from "@/types/role.types"; + +const DEFAULT_PAYLOAD: IPermissionAttributes = { + action: "", + model: "", + relation: "", + role: "", +}; +const AccordionModelHead = () => ( + + + + + Action + + + + + Relation + + + +); + +export const PermissionsBody: FC> = ({ + data: role, + Wrapper = Fragment, + WrapperProps, + ...rest +}) => { + const { t } = useTranslate(); + const { toast } = useToast(); + const { data: models, refetch: modelRefetch } = useFind( + { entity: EntityType.MODEL, format: Format.FULL }, + { + hasCount: false, + }, + ); + const getPermissionFromCache = useGetFromCache(EntityType.PERMISSION); + const options = { + onError: (error: Error) => { + toast.error(error.message || t("message.internal_server_error")); + }, + onSuccess: () => { + modelRefetch(); + toast.success(t("message.item_delete_success")); + }, + }; + const { mutate: createPermission } = useCreate(EntityType.PERMISSION, { + ...options, + onError: (error: Error & { statusCode?: number }) => { + rest.onError?.(); + if (error.statusCode === 409) { + toast.error(t("message.permission_already_exists")); + } else { + toast.error(t("message.internal_server_error")); + } + }, + }); + const { mutate: deletePermission } = useDelete( + EntityType.PERMISSION, + options, + ); + const [expanded, setExpanded] = useState(false); + const [payload, setPayload] = + useState(DEFAULT_PAYLOAD); + const reset = () => setPayload(DEFAULT_PAYLOAD); + const handleChange = + (panel: string) => (event: React.SyntheticEvent, isExpanded: boolean) => { + setExpanded(isExpanded ? panel : false); + }; + + useEffect(() => { + if (expanded === false && models?.[0]?.id) setExpanded(models[0].id); + }, [models]); + + return ( + {}} {...WrapperProps}> + + {role?.name} + + {models?.map((model) => ( + + } + sx={{ + backgroundColor: "background.default", + borderRadius: 1, + fontFamily: "inherit", + }} + > + {model.name} + + + + + {model.permissions + ?.map((p) => getPermissionFromCache(p)) + ?.filter( + (permission) => permission && permission.role === role?.id, + ) + .map((p) => p as IPermission) + .map(({ id, action, relation }, index) => { + return ( + <> + {index > 0 && } + + + deletePermission(id)} + size="small" + > + + + + + {action} + + + {relation} + + + + ); + })} + + + { + if (role?.id) + createPermission({ + ...payload, + role: role.id, + model: model.id, + }); + reset(); + }} + > + + + + + { + if (e.target.value) + setPayload((currentPayload) => ({ + ...currentPayload, + action: e.target.value, + })); + }} + > + {t("label.create")} + {t("label.read")} + {t("label.update")} + {t("label.delete")} + + + + { + if (e.target.value) + setPayload((currentPayload) => ({ + ...currentPayload, + relation: e.target.value, + })); + }} + > + {t("label.role")} + + + + + + + ))} + + ); +}; diff --git a/frontend/src/components/roles/PermissionsBodyDialog.tsx b/frontend/src/components/roles/PermissionsBodyDialog.tsx new file mode 100644 index 00000000..3ee11651 --- /dev/null +++ b/frontend/src/components/roles/PermissionsBodyDialog.tsx @@ -0,0 +1,23 @@ +/* + * Copyright © 2025 Hexastack. All rights reserved. + * + * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: + * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. + * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). + */ + +import { GenericFormDialog } from "@/app-components/dialogs"; +import { ComponentFormDialogProps } from "@/types/common/dialogs.types"; +import { IRole } from "@/types/role.types"; + +import { PermissionsBody } from "./PermissionsBody"; + +export const PermissionBodyDialog = ( + props: ComponentFormDialogProps, +) => ( + + Form={PermissionsBody} + editText="title.manage_permissions" + {...props} + /> +); diff --git a/frontend/src/components/roles/PermissionsDialog.tsx b/frontend/src/components/roles/PermissionsDialog.tsx deleted file mode 100644 index dca38329..00000000 --- a/frontend/src/components/roles/PermissionsDialog.tsx +++ /dev/null @@ -1,278 +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 AddIcon from "@mui/icons-material/Add"; -import DeleteOutlinedIcon from "@mui/icons-material/DeleteOutlined"; -import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; -import { - Accordion, - AccordionDetails, - AccordionSummary, - Dialog, - Grid, - MenuItem, - Paper, - Typography, - DialogContent, - DialogActions, - Button, - Divider, -} from "@mui/material"; -import { useState, FC, useEffect } from "react"; - -import { IconButton } from "@/app-components/buttons/IconButton"; -import { DialogTitle } from "@/app-components/dialogs/DialogTitle"; -import { Input } from "@/app-components/inputs/Input"; -import { useCreate } from "@/hooks/crud/useCreate"; -import { useDelete } from "@/hooks/crud/useDelete"; -import { useFind } from "@/hooks/crud/useFind"; -import { useGetFromCache } from "@/hooks/crud/useGet"; -import { DialogControlProps } from "@/hooks/useDialog"; -import { useToast } from "@/hooks/useToast"; -import { useTranslate } from "@/hooks/useTranslate"; -import { EntityType, Format } from "@/services/types"; -import { IPermission, IPermissionAttributes } from "@/types/permission.types"; -import { IRole } from "@/types/role.types"; - -export type PermissionsDialogProps = DialogControlProps<{ - role: IRole; -}>; - -const DEFAULT_PAYLOAD: IPermissionAttributes = { - action: "", - model: "", - relation: "", - role: "", -}; -const AccordionModelHead = () => ( - - - - - Action - - - - - Relation - - - -); - -export const PermissionsDialog: FC = ({ - open, - data, - closeDialog: closeFunction, -}) => { - const { t } = useTranslate(); - const { toast } = useToast(); - const { data: models, refetch: modelRefetch } = useFind( - { entity: EntityType.MODEL, format: Format.FULL }, - { - hasCount: false, - }, - ); - const getPermisionFromCache = useGetFromCache(EntityType.PERMISSION); - const { mutateAsync: createPermission } = useCreate(EntityType.PERMISSION, { - onError: (error: Error & { statusCode?: number }) => { - if (error.statusCode === 409) { - toast.error(t("message.permission_already_exists")); - } else { - toast.error(t("message.internal_server_error")); - } - }, - onSuccess: () => { - modelRefetch(); - toast.success(t("message.success_save")); - }, - }); - const { mutateAsync: deletePermission } = useDelete(EntityType.PERMISSION, { - onError: () => { - toast.error(t("message.internal_server_error")); - }, - onSuccess: () => { - modelRefetch(); - toast.success(t("message.item_delete_success")); - }, - }); - const [expanded, setExpanded] = useState(false); - const [payload, setPayload] = - useState(DEFAULT_PAYLOAD); - const reset = () => setPayload(DEFAULT_PAYLOAD); - const handleChange = - (panel: string) => (event: React.SyntheticEvent, isExpanded: boolean) => { - setExpanded(isExpanded ? panel : false); - }; - - useEffect(() => { - if (expanded === false && models?.[0]?.id) setExpanded(models[0].id); - }, [models]); - - return ( - - - {t("title.manage_permissions")} - - - - {data?.role.name} - - {models?.map((model) => { - return ( - - } - sx={{ - backgroundColor: "background.default", - borderRadius: 1, - fontFamily: "inherit", - }} - > - {model.name} - - - - - {model.permissions - ?.map((p) => getPermisionFromCache(p)) - ?.filter( - (permission) => - permission && permission.role === data?.role.id, - ) - .map((p) => p as IPermission) - .map(({ id, action, relation }, index) => { - return ( - <> - {index > 0 && } - - - { - deletePermission(id); - }} - size="small" - > - - - - - {action} - - - {relation} - - - - ); - })} - - - { - if (data?.role.id) - createPermission({ - ...payload, - model: model.id, - role: data.role.id, - }); - reset(); - }} - > - - - - - { - if (e.target.value) - setPayload((currentPayload) => ({ - ...currentPayload, - action: e.target.value, - })); - }} - > - {t("label.create")} - {t("label.read")} - {t("label.update")} - {t("label.delete")} - - - - { - if (e.target.value) - setPayload((currentPayload) => ({ - ...currentPayload, - relation: e.target.value, - })); - }} - > - {t("label.role")} - - - - - - - ); - })} - - - - - - ); -}; From a7f4dc0d8e111dec241dbd7fa8befdc5cdd1b293 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 7 Feb 2025 05:03:18 +0100 Subject: [PATCH 02/10] refactor(frontend): update roles dialogs --- frontend/src/components/roles/RoleDialog.tsx | 114 ------------------ frontend/src/components/roles/RoleForm.tsx | 94 +++++++++++++++ .../src/components/roles/RoleFormDialog.tsx | 24 ++++ frontend/src/components/roles/index.tsx | 48 +++----- 4 files changed, 135 insertions(+), 145 deletions(-) delete mode 100644 frontend/src/components/roles/RoleDialog.tsx create mode 100644 frontend/src/components/roles/RoleForm.tsx create mode 100644 frontend/src/components/roles/RoleFormDialog.tsx diff --git a/frontend/src/components/roles/RoleDialog.tsx b/frontend/src/components/roles/RoleDialog.tsx deleted file mode 100644 index 144c1387..00000000 --- a/frontend/src/components/roles/RoleDialog.tsx +++ /dev/null @@ -1,114 +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 { IRole, IRoleAttributes } from "@/types/role.types"; - -export type RoleDialogProps = DialogControlProps; -export const RoleDialog: FC = ({ - open, - data, - closeDialog, - ...rest -}) => { - const { t } = useTranslate(); - const { toast } = useToast(); - const { mutateAsync: createRole } = useCreate(EntityType.ROLE, { - onError: (error) => { - toast.error(error); - }, - onSuccess() { - closeDialog(); - toast.success(t("message.success_save")); - }, - }); - const { mutateAsync: updateRole } = useUpdate(EntityType.ROLE, { - onError: () => { - toast.error(t("message.internal_server_error")); - }, - onSuccess() { - closeDialog(); - toast.success(t("message.success_save")); - }, - }); - const { - handleSubmit, - reset, - register, - formState: { errors }, - } = useForm({ - defaultValues: { name: "" }, - }); - const validationRules = { - name: { - required: t("message.name_is_required"), - }, - }; - const onSubmitForm = async (params: IRoleAttributes) => { - if (data) { - updateRole({ id: data.id, params }); - } else { - createRole(params); - } - }; - - useEffect(() => { - if (open) reset(); - }, [open, reset]); - - useEffect(() => { - if (data) { - reset({ - name: data.name, - }); - } else { - reset(); - } - }, [data, reset]); - - return ( - -
- - {data ? t("title.edit_role") : t("title.new_role")} - - - - - - - - - - - -
-
- ); -}; diff --git a/frontend/src/components/roles/RoleForm.tsx b/frontend/src/components/roles/RoleForm.tsx new file mode 100644 index 00000000..5cc39de2 --- /dev/null +++ b/frontend/src/components/roles/RoleForm.tsx @@ -0,0 +1,94 @@ +/* + * Copyright © 2025 Hexastack. All rights reserved. + * + * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: + * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. + * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). + */ + +import { FC, Fragment, useEffect } from "react"; +import { useForm } from "react-hook-form"; + +import { ContentContainer, ContentItem } from "@/app-components/dialogs/"; +import { Input } from "@/app-components/inputs/Input"; +import { useCreate } from "@/hooks/crud/useCreate"; +import { useUpdate } from "@/hooks/crud/useUpdate"; +import { useToast } from "@/hooks/useToast"; +import { useTranslate } from "@/hooks/useTranslate"; +import { EntityType } from "@/services/types"; +import { ComponentFormProps } from "@/types/common/dialogs.types"; +import { IRole, IRoleAttributes } from "@/types/role.types"; + +export const RoleForm: FC> = ({ + data, + Wrapper = Fragment, + WrapperProps, + ...rest +}) => { + const { t } = useTranslate(); + const { toast } = useToast(); + const options = { + onError: (error: Error) => { + toast.error(error); + }, + onSuccess() { + rest.onSuccess?.(); + toast.success(t("message.success_save")); + }, + }; + const { mutate: createRole } = useCreate(EntityType.ROLE, options); + const { mutate: updateRole } = useUpdate(EntityType.ROLE, options); + const { + handleSubmit, + reset, + register, + formState: { errors }, + } = useForm({ + defaultValues: { name: "" }, + }); + const validationRules = { + name: { + required: t("message.name_is_required"), + }, + }; + const onSubmitForm = (params: IRoleAttributes) => { + if (data) { + updateRole({ id: data.id, params }); + } else { + createRole(params); + } + }; + + useEffect(() => { + if (data) { + reset({ + name: data.name, + }); + } else { + reset(); + } + }, [data, reset]); + + return ( + +
+ + + + + +
+
+ ); +}; diff --git a/frontend/src/components/roles/RoleFormDialog.tsx b/frontend/src/components/roles/RoleFormDialog.tsx new file mode 100644 index 00000000..92590398 --- /dev/null +++ b/frontend/src/components/roles/RoleFormDialog.tsx @@ -0,0 +1,24 @@ +/* + * Copyright © 2025 Hexastack. All rights reserved. + * + * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: + * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. + * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). + */ + +import { GenericFormDialog } from "@/app-components/dialogs"; +import { ComponentFormDialogProps } from "@/types/common/dialogs.types"; +import { IRole } from "@/types/role.types"; + +import { RoleForm } from "./RoleForm"; + +export const RoleFormDialog = ( + props: ComponentFormDialogProps, +) => ( + + Form={RoleForm} + addText="title.new_role" + editText="title.edit_role" + {...props} + /> +); diff --git a/frontend/src/components/roles/index.tsx b/frontend/src/components/roles/index.tsx index f23655e4..531818da 100644 --- a/frontend/src/components/roles/index.tsx +++ b/frontend/src/components/roles/index.tsx @@ -10,9 +10,8 @@ import { faUniversalAccess } from "@fortawesome/free-solid-svg-icons"; import AddIcon from "@mui/icons-material/Add"; import { Button, Grid, Paper } from "@mui/material"; import { GridColDef } from "@mui/x-data-grid"; -import React from "react"; -import { DeleteDialog } from "@/app-components/dialogs/DeleteDialog"; +import { ConfirmDialogBody } from "@/app-components/dialogs"; import { FilterTextfield } from "@/app-components/inputs/FilterTextfield"; import { ActionColumnLabel, @@ -22,7 +21,7 @@ import { renderHeader } from "@/app-components/tables/columns/renderHeader"; import { DataGrid } from "@/app-components/tables/DataGrid"; import { useDelete } from "@/hooks/crud/useDelete"; import { useFind } from "@/hooks/crud/useFind"; -import { getDisplayDialogs, useDialog } from "@/hooks/useDialog"; +import { useDialogs } from "@/hooks/useDialogs"; import { useHasPermission } from "@/hooks/useHasPermission"; import { useSearch } from "@/hooks/useSearch"; import { useToast } from "@/hooks/useToast"; @@ -33,18 +32,13 @@ import { PermissionAction } from "@/types/permission.types"; import { IRole } from "@/types/role.types"; import { getDateTimeFormatter } from "@/utils/date"; -import { PermissionsDialog } from "./PermissionsDialog"; -import { RoleDialog } from "./RoleDialog"; +import { PermissionBodyDialog } from "./PermissionsBodyDialog"; +import { RoleFormDialog } from "./RoleFormDialog"; export const Roles = () => { const { t } = useTranslate(); const { toast } = useToast(); - const addDialogCtl = useDialog(false); - const editDialogCtl = useDialog(false); - const deleteDialogCtl = useDialog(false); - const permissionDialogCtl = useDialog<{ - role: IRole; - }>(false); + const dialogs = useDialogs(); const hasPermission = useHasPermission(); const { onSearch, searchPayload } = useSearch({ $iLike: ["name"], @@ -55,12 +49,11 @@ export const Roles = () => { params: searchPayload, }, ); - const { mutateAsync: deleteRole } = useDelete(EntityType.ROLE, { + const { mutate: deleteRole } = useDelete(EntityType.ROLE, { onError: (error) => { toast.error(error); }, onSuccess() { - deleteDialogCtl.closeDialog(); toast.success(t("message.item_delete_success")); }, }); @@ -70,19 +63,25 @@ export const Roles = () => { { label: ActionColumnLabel.Permissions, action: (row) => - permissionDialogCtl.openDialog({ - role: row, + dialogs.open(PermissionBodyDialog, row, { + hasButtons: false, }), }, { label: ActionColumnLabel.Edit, - action: (row) => editDialogCtl.openDialog(row), + action: (row) => dialogs.open(RoleFormDialog, row), requires: [PermissionAction.UPDATE], }, { label: ActionColumnLabel.Delete, - action: (row) => deleteDialogCtl.openDialog(row.id), + action: async ({ id }) => { + const isConfirmed = await dialogs.confirm(ConfirmDialogBody); + + if (isConfirmed) { + deleteRole(id); + } + }, requires: [PermissionAction.DELETE], }, ], @@ -125,17 +124,6 @@ export const Roles = () => { return ( - {permissionDialogCtl.open ? ( - - ) : null} - - - { - if (deleteDialogCtl.data) deleteRole(deleteDialogCtl.data); - }} - /> { sx={{ float: "right", }} - onClick={() => { - addDialogCtl.openDialog(); - }} + onClick={() => dialogs.open(RoleFormDialog, null)} > {t("button.add")} From 584eb83d6860421d85b1b56664d85d8437e2e04b Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 7 Feb 2025 15:52:44 +0100 Subject: [PATCH 03/10] fix(frontend): add dialog hasButtons option --- frontend/src/app-components/dialogs/FormDialog.tsx | 14 +++++++++----- frontend/src/types/common/dialogs.types.ts | 3 ++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/frontend/src/app-components/dialogs/FormDialog.tsx b/frontend/src/app-components/dialogs/FormDialog.tsx index 97fac8e9..20469895 100644 --- a/frontend/src/app-components/dialogs/FormDialog.tsx +++ b/frontend/src/app-components/dialogs/FormDialog.tsx @@ -22,16 +22,20 @@ export const FormDialog = ({ ...rest }: FormDialogProps) => { const onCancel = () => rest.onClose?.({}, "backdropClick"); - - return ( - - {title} - {children} + const dialogActions = + rest.hasButtons === false ? null : ( + ); + + return ( + + {title} + {children} + {dialogActions} ); }; diff --git a/frontend/src/types/common/dialogs.types.ts b/frontend/src/types/common/dialogs.types.ts index 64191fc1..d5b010b2 100644 --- a/frontend/src/types/common/dialogs.types.ts +++ b/frontend/src/types/common/dialogs.types.ts @@ -150,7 +150,8 @@ export interface DialogProviderProps { // form dialog export interface FormDialogProps extends FormButtonsProps, - Omit { + Omit, + DialogExtraOptions { title?: string; children?: React.ReactNode; } From 16430db79bcb32d7abe9e54cd6b0303a28a8b847 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 7 Feb 2025 22:55:26 +0100 Subject: [PATCH 04/10] fix(frontend): move dialogs provider level --- frontend/src/pages/_app.tsx | 63 ++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx index fcff228c..ca640433 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.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 { CssBaseline } from "@mui/material"; import { StyledEngineProvider, ThemeProvider } from "@mui/material/styles"; import type { NextPage } from "next"; @@ -74,37 +75,35 @@ const App = ({ Component, pageProps }: TAppPropsWithLayout) => {
- - ( - - )} - > - - - - - - - - - - - {getLayout()} - - - - - - - - - - - - + ( + + )} + > + + + + + + + + + + + {getLayout()} + + + + + + + + + + +
From f35b8abbc4f9d048a2917a41207d27a871ed68f3 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 7 Feb 2025 22:55:58 +0100 Subject: [PATCH 05/10] fix(frontend): update theme --- frontend/src/layout/themes/theme.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/layout/themes/theme.ts b/frontend/src/layout/themes/theme.ts index aa52d8af..84e9e0ef 100644 --- a/frontend/src/layout/themes/theme.ts +++ b/frontend/src/layout/themes/theme.ts @@ -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 { Color, SimplePaletteColorOptions } from "@mui/material"; import { grey, teal } from "@mui/material/colors"; import { createTheme } from "@mui/material/styles"; @@ -129,7 +130,7 @@ export const theme = createTheme({ MuiDialogActions: { styleOverrides: { root: { - paddingRight: "15px", + padding: "0.5rem", borderTop: borderLine, backgroundColor: COLOR_PALETTE.lighterGray, }, @@ -159,10 +160,9 @@ export const theme = createTheme({ }, MuiDialogContent: { styleOverrides: { - root: { marginTop: "20px" }, + root: { paddingTop: "15px!important" }, }, }, - MuiTextField: { styleOverrides: { root: { From f1270940be52e276666af9a84dd6b529a73ac3b0 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 7 Feb 2025 22:56:26 +0100 Subject: [PATCH 06/10] fix(frontend): update formButtons --- frontend/src/app-components/buttons/FormButtons.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/src/app-components/buttons/FormButtons.tsx b/frontend/src/app-components/buttons/FormButtons.tsx index 9b277ddb..a4c56429 100644 --- a/frontend/src/app-components/buttons/FormButtons.tsx +++ b/frontend/src/app-components/buttons/FormButtons.tsx @@ -11,6 +11,7 @@ import CloseIcon from "@mui/icons-material/Close"; import { Button, Grid } from "@mui/material"; import { useTranslate } from "@/hooks/useTranslate"; +import { TTranslationKeys } from "@/i18n/i18n.types"; import { FormButtonsProps } from "@/types/common/dialogs.types"; export const DialogFormButtons = ({ @@ -20,6 +21,10 @@ export const DialogFormButtons = ({ confirmButtonProps, }: FormButtonsProps) => { const { t } = useTranslate(); + const cancelButtonTitle = (cancelButtonProps?.value || + "button.cancel") as TTranslationKeys; + const confirmButtonTitle = (confirmButtonProps?.value || + "button.submit") as TTranslationKeys; return ( } {...cancelButtonProps} > - {t("button.cancel")} + {t(cancelButtonTitle)} ); From 725ca3b29ea6e0e6944b47d4631f9e9e8244fbb5 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 7 Feb 2025 22:56:55 +0100 Subject: [PATCH 07/10] fix(frontend): update generic components --- frontend/src/app-components/dialogs/FormDialog.tsx | 2 +- .../src/app-components/dialogs/GenericFormDialog.tsx | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/frontend/src/app-components/dialogs/FormDialog.tsx b/frontend/src/app-components/dialogs/FormDialog.tsx index 97fac8e9..180ba58f 100644 --- a/frontend/src/app-components/dialogs/FormDialog.tsx +++ b/frontend/src/app-components/dialogs/FormDialog.tsx @@ -27,7 +27,7 @@ export const FormDialog = ({ {title} {children} - + diff --git a/frontend/src/app-components/dialogs/GenericFormDialog.tsx b/frontend/src/app-components/dialogs/GenericFormDialog.tsx index 3f4772e0..f73e0e57 100644 --- a/frontend/src/app-components/dialogs/GenericFormDialog.tsx +++ b/frontend/src/app-components/dialogs/GenericFormDialog.tsx @@ -8,29 +8,30 @@ import React from "react"; -import { FormDialog } from "@/app-components/dialogs"; +import { FormDialog as Wrapper } from "@/app-components/dialogs"; import { useTranslate } from "@/hooks/useTranslate"; import { TTranslationKeys } from "@/i18n/i18n.types"; import { ComponentFormDialogProps } from "@/types/common/dialogs.types"; type GenericFormDialogProps = ComponentFormDialogProps & { Form: React.ElementType; + rowKey?: keyof T; addText?: TTranslationKeys; editText?: TTranslationKeys; }; export const GenericFormDialog = ({ Form, - payload, + rowKey, + payload: data, ...rest }: GenericFormDialogProps) => { const { t } = useTranslate(); - const translationKey = payload ? rest.editText : rest.addText; + const hasRow = rowKey ? data?.[rowKey] : data; + const translationKey = hasRow ? rest.editText : rest.addText; return (
{ rest.onClose(true); }} @@ -38,6 +39,7 @@ export const GenericFormDialog = ({ title: translationKey && t(translationKey), ...rest, }} + {...{ data, Wrapper }} /> ); }; From 8e9383a0bdf4cb03904e1c3f4f8794223a0c12fc Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 7 Feb 2025 22:57:56 +0100 Subject: [PATCH 08/10] refactor(frontend): update content and contentImport dialogs --- .../{ContentDialog.tsx => ContentForm.tsx} | 141 +++++++----------- .../components/contents/ContentFormDialog.tsx | 24 +++ ...ImportDialog.tsx => ContentImportForm.tsx} | 80 ++++------ .../contents/ContentImportFormDialog.tsx | 26 ++++ frontend/src/components/contents/index.tsx | 81 ++++------ 5 files changed, 167 insertions(+), 185 deletions(-) rename frontend/src/components/contents/{ContentDialog.tsx => ContentForm.tsx} (66%) create mode 100644 frontend/src/components/contents/ContentFormDialog.tsx rename frontend/src/components/contents/{ContentImportDialog.tsx => ContentImportForm.tsx} (54%) create mode 100644 frontend/src/components/contents/ContentImportFormDialog.tsx diff --git a/frontend/src/components/contents/ContentDialog.tsx b/frontend/src/components/contents/ContentForm.tsx similarity index 66% rename from frontend/src/components/contents/ContentDialog.tsx rename to frontend/src/components/contents/ContentForm.tsx index 4bd93b07..57891dab 100644 --- a/frontend/src/components/contents/ContentDialog.tsx +++ b/frontend/src/components/contents/ContentForm.tsx @@ -7,16 +7,8 @@ */ import LinkIcon from "@mui/icons-material/Link"; -import { - Dialog, - DialogActions, - DialogContent, - FormControl, - FormControlLabel, - Switch, -} from "@mui/material"; -import { isAbsoluteUrl } from "next/dist/shared/lib/utils"; -import { FC, useEffect } from "react"; +import { FormControl, FormControlLabel, Switch } from "@mui/material"; +import { FC, Fragment, useEffect } from "react"; import { Controller, ControllerRenderProps, @@ -25,19 +17,16 @@ import { } from "react-hook-form"; import AttachmentInput from "@/app-components/attachment/AttachmentInput"; -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 { ContentContainer, ContentItem } from "@/app-components/dialogs/"; import { Adornment } from "@/app-components/inputs/Adornment"; 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 { AttachmentResourceRef } from "@/types/attachment.types"; +import { ComponentFormProps } from "@/types/common/dialogs.types"; import { ContentField, ContentFieldType, @@ -45,6 +34,7 @@ import { } from "@/types/content-type.types"; import { IContent, IContentAttributes } from "@/types/content.types"; import { MIME_TYPES } from "@/utils/attachment"; +import { isAbsoluteUrl } from "@/utils/URL"; interface ContentFieldInput { contentField: ContentField; @@ -56,7 +46,6 @@ interface ContentFieldInput { >; idx: number; } - const ContentFieldInput: React.FC = ({ contentField: contentField, field, @@ -139,14 +128,14 @@ const buildDynamicFields = ( }, }); -export type ContentDialogProps = DialogControlProps<{ +export type ContentFormData = { content?: IContent; contentType?: IContentType; -}>; -export const ContentDialog: FC = ({ - open, +}; +export const ContentForm: FC> = ({ data, - closeDialog, + Wrapper = Fragment, + WrapperProps, ...rest }) => { const { content, contentType } = data || { @@ -179,42 +168,32 @@ export const ContentDialog: FC = ({ }; const { mutateAsync: createContent } = useCreate(EntityType.CONTENT); const { mutateAsync: updateContent } = useUpdate(EntityType.CONTENT); + const options = { + onError: (error: Error) => { + rest.onError?.(); + toast.error(error.message || t("message.internal_server_error")); + }, + onSuccess: () => { + rest.onSuccess?.(); + toast.success(t("message.success_save")); + }, + }; const onSubmitForm = async (params: IContentAttributes) => { if (content) { updateContent( { id: content.id, params: buildDynamicFields(params, contentType) }, - { - onError: () => { - toast.error(t("message.internal_server_error")); - }, - onSuccess: () => { - closeDialog(); - toast.success(t("message.success_save")); - }, - }, + options, ); } else if (contentType) { createContent( { ...buildDynamicFields(params, contentType), entity: contentType.id }, - { - onError: (error) => { - toast.error(error); - }, - onSuccess: () => { - closeDialog(); - toast.success(t("message.success_save")); - }, - }, + options, ); } else { throw new Error("Content Type must be passed to the dialog form."); } }; - useEffect(() => { - if (open) reset(); - }, [open, reset]); - useEffect(() => { if (content) { reset(content); @@ -224,47 +203,43 @@ export const ContentDialog: FC = ({ }, [content, reset]); return ( - + - - {content ? t("title.edit_node") : t("title.new_content")} - - - - {(contentType?.fields || []).map((contentField, index) => ( - - ( - - - - )} - /> - - ))} - - - - - + + {(contentType?.fields || []).map((contentField, index) => ( + + ( + + + + )} + /> + + ))} + - + ); }; diff --git a/frontend/src/components/contents/ContentFormDialog.tsx b/frontend/src/components/contents/ContentFormDialog.tsx new file mode 100644 index 00000000..9b82959d --- /dev/null +++ b/frontend/src/components/contents/ContentFormDialog.tsx @@ -0,0 +1,24 @@ +/* + * Copyright © 2025 Hexastack. All rights reserved. + * + * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: + * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. + * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). + */ + +import { GenericFormDialog } from "@/app-components/dialogs"; +import { ComponentFormDialogProps } from "@/types/common/dialogs.types"; + +import { ContentForm, ContentFormData } from "./ContentForm"; + +export const ContentFormDialog = ( + props: ComponentFormDialogProps, +) => ( + + Form={ContentForm} + rowKey="content" + addText="title.new_content" + editText="title.edit_node" + {...props} + /> +); diff --git a/frontend/src/components/contents/ContentImportDialog.tsx b/frontend/src/components/contents/ContentImportForm.tsx similarity index 54% rename from frontend/src/components/contents/ContentImportDialog.tsx rename to frontend/src/components/contents/ContentImportForm.tsx index ddf613bb..52fe9dbb 100644 --- a/frontend/src/components/contents/ContentImportDialog.tsx +++ b/frontend/src/components/contents/ContentImportForm.tsx @@ -6,72 +6,62 @@ * 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 { Button, Dialog, DialogActions, DialogContent } from "@mui/material"; -import { FC, useState } from "react"; +import { FC, Fragment, useState } from "react"; import { useQuery } from "react-query"; import AttachmentInput from "@/app-components/attachment/AttachmentInput"; -import { DialogTitle } from "@/app-components/dialogs/DialogTitle"; -import { ContentContainer } from "@/app-components/dialogs/layouts/ContentContainer"; -import { ContentItem } from "@/app-components/dialogs/layouts/ContentItem"; +import { ContentContainer, ContentItem } from "@/app-components/dialogs/"; import { useApiClient } from "@/hooks/useApiClient"; -import { DialogControlProps } from "@/hooks/useDialog"; import { useToast } from "@/hooks/useToast"; import { useTranslate } from "@/hooks/useTranslate"; import { AttachmentResourceRef } from "@/types/attachment.types"; +import { ComponentFormProps } from "@/types/common/dialogs.types"; import { IContentType } from "@/types/content-type.types"; -export type ContentImportDialogProps = DialogControlProps<{ - contentType?: IContentType; -}>; - -export const ContentImportDialog: FC = ({ - open, - data, - closeDialog, - ...rest -}) => { +export type ContentImportFormData = { row: null; contentType: IContentType }; +export const ContentImportForm: FC< + ComponentFormProps +> = ({ data, Wrapper = Fragment, WrapperProps, ...rest }) => { const [attachmentId, setAttachmentId] = useState(null); const { t } = useTranslate(); const { toast } = useToast(); const { apiClient } = useApiClient(); const { refetch, isFetching } = useQuery( - ["importContent", data?.contentType?.id, attachmentId], + ["importContent", data?.contentType.id, attachmentId], async () => { - if (data?.contentType?.id && attachmentId) { + if (data?.contentType.id && attachmentId) { await apiClient.importContent(data.contentType.id, attachmentId); } }, { enabled: false, - onSuccess: () => { - handleCloseDialog(); - toast.success(t("message.success_save")); - if (rest.callback) { - rest.callback(); - } - }, onError: () => { + rest.onError?.(); toast.error(t("message.internal_server_error")); }, + onSuccess: () => { + rest.onSuccess?.(); + toast.success(t("message.success_save")); + }, }, ); - const handleCloseDialog = () => { - closeDialog(); - setAttachmentId(null); - }; const handleImportClick = () => { - if (attachmentId && data?.contentType?.id) { + if (attachmentId && data?.contentType.id) { refetch(); } }; return ( - - {t("title.import")} - + +
= ({ /> - - - - - -
+ + ); }; diff --git a/frontend/src/components/contents/ContentImportFormDialog.tsx b/frontend/src/components/contents/ContentImportFormDialog.tsx new file mode 100644 index 00000000..046896ea --- /dev/null +++ b/frontend/src/components/contents/ContentImportFormDialog.tsx @@ -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 { GenericFormDialog } from "@/app-components/dialogs"; +import { ComponentFormDialogProps } from "@/types/common/dialogs.types"; + +import { ContentImportForm, ContentImportFormData } from "./ContentImportForm"; + +export const ContentImportFormDialog = < + T extends ContentImportFormData = ContentImportFormData, +>( + props: ComponentFormDialogProps, +) => ( + + Form={ContentImportForm} + rowKey="row" + addText="button.import" + confirmButtonProps={{ value: "button.import" }} + {...props} + /> +); diff --git a/frontend/src/components/contents/index.tsx b/frontend/src/components/contents/index.tsx index 646455d7..ee639e4b 100644 --- a/frontend/src/components/contents/index.tsx +++ b/frontend/src/components/contents/index.tsx @@ -14,7 +14,7 @@ import { Button, Chip, Grid, Paper, Switch, Typography } from "@mui/material"; import Link from "next/link"; import { useRouter } from "next/router"; -import { DeleteDialog } from "@/app-components/dialogs"; +import { ConfirmDialogBody } from "@/app-components/dialogs"; import { FilterTextfield } from "@/app-components/inputs/FilterTextfield"; import { ActionColumnLabel, @@ -26,54 +26,38 @@ import { useDelete } from "@/hooks/crud/useDelete"; import { useFind } from "@/hooks/crud/useFind"; import { useGet, useGetFromCache } from "@/hooks/crud/useGet"; import { useUpdate } from "@/hooks/crud/useUpdate"; -import { getDisplayDialogs, useDialog } from "@/hooks/useDialog"; +import { useDialogs } from "@/hooks/useDialogs"; import { useHasPermission } from "@/hooks/useHasPermission"; import { useSearch } from "@/hooks/useSearch"; import { useToast } from "@/hooks/useToast"; import { useTranslate } from "@/hooks/useTranslate"; import { PageHeader } from "@/layout/content/PageHeader"; import { EntityType, Format } from "@/services/types"; -import { IContentType } from "@/types/content-type.types"; import { IContent } from "@/types/content.types"; import { PermissionAction } from "@/types/permission.types"; import { getDateTimeFormatter } from "@/utils/date"; -import { ContentDialog } from "./ContentDialog"; -import { ContentImportDialog } from "./ContentImportDialog"; +import { ContentFormDialog } from "./ContentFormDialog"; +import { ContentImportFormDialog } from "./ContentImportFormDialog"; export const Contents = () => { const { t } = useTranslate(); const { toast } = useToast(); const { query } = useRouter(); - // Dialog Controls - const addDialogCtl = useDialog<{ - content?: IContent; - contentType?: IContentType; - }>(false); - const editDialogCtl = useDialog<{ - content?: IContent; - contentType?: IContentType; - }>(false); - const deleteDialogCtl = useDialog(false); + const dialogs = useDialogs(); // data fetching const { onSearch, searchPayload } = useSearch({ $eq: [{ entity: String(query.id) }], $iLike: ["title"], }); - const importDialogCtl = useDialog<{ - contentType?: IContentType; - }>(false); const hasPermission = useHasPermission(); - const { data: contentType } = useGet(String(query.id), { - entity: EntityType.CONTENT_TYPE, - }); const { dataGridProps, refetch } = useFind( { entity: EntityType.CONTENT, format: Format.FULL }, { params: searchPayload, }, ); - const { mutateAsync: updateContent } = useUpdate(EntityType.CONTENT, { + const { mutate: updateContent } = useUpdate(EntityType.CONTENT, { onError: (error) => { toast.error(error.message || t("message.internal_server_error")); }, @@ -81,9 +65,8 @@ export const Contents = () => { toast.success(t("message.success_save")); }, }); - const { mutateAsync: deleteContent } = useDelete(EntityType.CONTENT, { + const { mutate: deleteContent } = useDelete(EntityType.CONTENT, { onSuccess: () => { - deleteDialogCtl.closeDialog(); toast.success(t("message.item_delete_success")); }, }); @@ -93,22 +76,25 @@ export const Contents = () => { [ { label: ActionColumnLabel.Edit, - action: (content) => - editDialogCtl.openDialog({ - contentType, - content, - }), + action: (row) => + dialogs.open(ContentFormDialog, { content: row, contentType }), requires: [PermissionAction.UPDATE], }, { label: ActionColumnLabel.Delete, - action: (content) => deleteDialogCtl.openDialog(content.id), + action: async ({ id }) => { + const isConfirmed = await dialogs.confirm(ConfirmDialogBody); + + if (isConfirmed) { + deleteContent(id); + } + }, requires: [PermissionAction.DELETE], }, ], t("label.operations"), ); - const { data } = useGet(String(query.id), { + const { data: contentType } = useGet(String(query.id), { entity: EntityType.CONTENT_TYPE, }); @@ -123,7 +109,9 @@ export const Contents = () => { } + chip={ + + } title={t("title.content")} > @@ -135,7 +123,9 @@ export const Contents = () => {