diff --git a/frontend/src/app-components/buttons/FormButtons.tsx b/frontend/src/app-components/buttons/FormButtons.tsx index 469b9cf5..9b277ddb 100644 --- a/frontend/src/app-components/buttons/FormButtons.tsx +++ b/frontend/src/app-components/buttons/FormButtons.tsx @@ -13,7 +13,12 @@ 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 = ({ + onSubmit, + onCancel, + cancelButtonProps, + confirmButtonProps, +}: FormButtonsProps) => { const { t } = useTranslate(); return ( @@ -28,6 +33,7 @@ export const DialogFormButtons = ({ onCancel, onSubmit }: FormButtonsProps) => { variant="outlined" onClick={onCancel} startIcon={} + {...cancelButtonProps} > {t("button.cancel")} @@ -36,6 +42,7 @@ export const DialogFormButtons = ({ onCancel, onSubmit }: FormButtonsProps) => { variant="contained" onClick={onSubmit} startIcon={} + {...confirmButtonProps} > {t("button.submit")} diff --git a/frontend/src/app-components/dialogs/FormDialog.tsx b/frontend/src/app-components/dialogs/FormDialog.tsx index 8a5bfe24..97fac8e9 100644 --- a/frontend/src/app-components/dialogs/FormDialog.tsx +++ b/frontend/src/app-components/dialogs/FormDialog.tsx @@ -17,16 +17,20 @@ export const FormDialog = ({ title, children, onSubmit, + cancelButtonProps, + confirmButtonProps, ...rest }: FormDialogProps) => { - const handleClose = () => rest.onClose?.({}, "backdropClick"); + const onCancel = () => rest.onClose?.({}, "backdropClick"); return ( - {title} + {title} {children} - + ); diff --git a/frontend/src/components/users/InvitationDialog.tsx b/frontend/src/components/users/InvitationDialog.tsx deleted file mode 100644 index 77458710..00000000 --- a/frontend/src/components/users/InvitationDialog.tsx +++ /dev/null @@ -1,144 +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 CloseIcon from "@mui/icons-material/Close"; -import SendIcon from "@mui/icons-material/Send"; -import { Button, Dialog, DialogActions, DialogContent } from "@mui/material"; -import { FC, useEffect } from "react"; -import { Controller, useForm } from "react-hook-form"; - -import { DialogTitle } from "@/app-components/dialogs/DialogTitle"; -import { ContentContainer } from "@/app-components/dialogs/layouts/ContentContainer"; -import { ContentItem } from "@/app-components/dialogs/layouts/ContentItem"; -import AutoCompleteEntitySelect from "@/app-components/inputs/AutoCompleteEntitySelect"; -import { Input } from "@/app-components/inputs/Input"; -import { useSendInvitation } from "@/hooks/entities/invitation-hooks"; -import { DialogControlProps } from "@/hooks/useDialog"; -import { useToast } from "@/hooks/useToast"; -import { useTranslate } from "@/hooks/useTranslate"; -import { useValidationRules } from "@/hooks/useValidationRules"; -import { EntityType, Format } from "@/services/types"; -import { IInvitationAttributes } from "@/types/invitation.types"; -import { IRole } from "@/types/role.types"; - -const DEFAULT_VALUES: IInvitationAttributes = { email: "", roles: [] }; - -export type InvitationDialogProps = DialogControlProps; -export const InvitationDialog: FC = ({ - open, - closeDialog, - ...rest -}) => { - const { t } = useTranslate(); - const { toast } = useToast(); - const { mutateAsync: sendInvitation } = useSendInvitation({ - onSuccess: () => { - closeDialog(); - toast.success(t("message.success_invitation_sent")); - }, - onError: () => { - toast.error(t("message.internal_server_error")); - }, - }); - const { - handleSubmit, - reset, - register, - control, - formState: { errors }, - } = useForm({ - defaultValues: DEFAULT_VALUES, - }); - const rules = useValidationRules(); - const validationRules = { - email: { - ...rules.email, - required: t("message.email_is_required"), - }, - roles: { - required: t("message.roles_is_required"), - }, - }; - const onSubmitForm = async (params: IInvitationAttributes) => { - sendInvitation(params); - }; - - useEffect(() => { - if (open) reset(DEFAULT_VALUES); - }, [open, reset]); - - return ( - -
- - {t("title.invite_new_user")} - - - - - - - - { - const { onChange, ...rest } = field; - - return ( - - autoFocus - searchFields={["name"]} - entity={EntityType.ROLE} - format={Format.BASIC} - labelKey="name" - label={t("label.roles")} - multiple={true} - {...field} - error={!!errors.roles} - helperText={errors.roles ? errors.roles.message : null} - onChange={(_e, selected) => - onChange(selected.map(({ id }) => id)) - } - {...rest} - /> - ); - }} - /> - - - - - - - -
-
- ); -}; diff --git a/frontend/src/components/users/InviteUserForm.tsx b/frontend/src/components/users/InviteUserForm.tsx new file mode 100644 index 00000000..d4517382 --- /dev/null +++ b/frontend/src/components/users/InviteUserForm.tsx @@ -0,0 +1,116 @@ +/* + * 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 } from "react"; +import { Controller, useForm } from "react-hook-form"; + +import { ContentContainer, ContentItem } from "@/app-components/dialogs/"; +import AutoCompleteEntitySelect from "@/app-components/inputs/AutoCompleteEntitySelect"; +import { Input } from "@/app-components/inputs/Input"; +import { useSendInvitation } from "@/hooks/entities/invitation-hooks"; +import { useToast } from "@/hooks/useToast"; +import { useTranslate } from "@/hooks/useTranslate"; +import { useValidationRules } from "@/hooks/useValidationRules"; +import { EntityType, Format } from "@/services/types"; +import { ComponentFormProps } from "@/types/common/dialogs.types"; +import { IInvitationAttributes } from "@/types/invitation.types"; +import { IRole } from "@/types/role.types"; + +const DEFAULT_VALUES: IInvitationAttributes = { email: "", roles: [] }; + +export const InviteUserForm: FC> = ({ + Wrapper = Fragment, + WrapperProps, + ...rest +}) => { + const { t } = useTranslate(); + const { toast } = useToast(); + const { mutateAsync: sendInvitation } = useSendInvitation({ + onSuccess: () => { + rest.onSuccess?.(); + toast.success(t("message.success_invitation_sent")); + }, + onError: () => { + rest.onError?.(); + toast.error(t("message.internal_server_error")); + }, + }); + const { + control, + register, + formState: { errors }, + handleSubmit, + } = useForm({ + defaultValues: DEFAULT_VALUES, + }); + const rules = useValidationRules(); + const validationRules = { + email: { + ...rules.email, + required: t("message.email_is_required"), + }, + roles: { + required: t("message.roles_is_required"), + }, + }; + const onSubmitForm = async (params: IInvitationAttributes) => { + sendInvitation(params); + }; + + return ( + +
+ + + + + + { + const { onChange, ...rest } = field; + + return ( + + autoFocus + searchFields={["name"]} + entity={EntityType.ROLE} + format={Format.BASIC} + labelKey="name" + label={t("label.roles")} + multiple={true} + {...field} + error={!!errors.roles} + helperText={errors.roles ? errors.roles.message : null} + onChange={(_e, selected) => + onChange(selected.map(({ id }) => id)) + } + {...rest} + /> + ); + }} + /> + + +
+
+ ); +}; diff --git a/frontend/src/components/users/InviteUserFormDialog.tsx b/frontend/src/components/users/InviteUserFormDialog.tsx new file mode 100644 index 00000000..2ceae59b --- /dev/null +++ b/frontend/src/components/users/InviteUserFormDialog.tsx @@ -0,0 +1,25 @@ +/* + * 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 SendIcon from "@mui/icons-material/Send"; + +import { GenericFormDialog } from "@/app-components/dialogs"; +import { ComponentFormDialogProps } from "@/types/common/dialogs.types"; + +import { InviteUserForm } from "./InviteUserForm"; + +export const InviteUserFormFormDialog = ( + props: ComponentFormDialogProps, +) => ( + + Form={InviteUserForm} + addText="title.invite_new_user" + confirmButtonProps={{ startIcon: }} + {...props} + /> +); diff --git a/frontend/src/components/users/index.tsx b/frontend/src/components/users/index.tsx index fd16366e..caad6fa7 100644 --- a/frontend/src/components/users/index.tsx +++ b/frontend/src/components/users/index.tsx @@ -24,7 +24,7 @@ import { useFind } from "@/hooks/crud/useFind"; import { useUpdate } from "@/hooks/crud/useUpdate"; import { useAuth } from "@/hooks/useAuth"; import { useConfig } from "@/hooks/useConfig"; -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"; @@ -32,19 +32,19 @@ import { useTranslate } from "@/hooks/useTranslate"; import { PageHeader } from "@/layout/content/PageHeader"; import { EntityType, Format } from "@/services/types"; import { PermissionAction } from "@/types/permission.types"; -import { IRole } from "@/types/role.types"; import { IUser } from "@/types/user.types"; import { getDateTimeFormatter } from "@/utils/date"; -import { EditUserDialog } from "./EditUserDialog"; -import { InvitationDialog } from "./InvitationDialog"; +import { CategoryFormDialog } from "./EditUserFormDialog"; +import { InviteUserFormFormDialog } from "./InviteUserFormDialog"; export const Users = () => { const { ssoEnabled } = useConfig(); const { t } = useTranslate(); const { toast } = useToast(); + const dialogs = useDialogs(); const { user } = useAuth(); - const { mutateAsync: updateUser } = useUpdate(EntityType.USER, { + const { mutate: updateUser } = useUpdate(EntityType.USER, { onError: (error) => { toast.error(error.message || t("message.internal_server_error")); }, @@ -52,8 +52,6 @@ export const Users = () => { toast.success(t("message.success_save")); }, }); - const invitationDialogCtl = useDialog(false); - const editDialogCtl = useDialog<{ user: IUser; roles: IRole[] }>(false); const hasPermission = useHasPermission(); const { onSearch, searchPayload } = useSearch({ $or: ["first_name", "last_name", "email"], @@ -76,9 +74,9 @@ export const Users = () => { { label: ActionColumnLabel.Manage_Roles, action: (row) => - editDialogCtl.openDialog({ - roles: roles || [], + dialogs.open(CategoryFormDialog, { user: row, + roles: roles || [], }), requires: [PermissionAction.CREATE], }, @@ -151,14 +149,14 @@ export const Users = () => { ssoEnabled || !hasPermission(EntityType.USER, PermissionAction.UPDATE) } - onChange={() => { + onChange={() => updateUser({ id: params.row.id, params: { state: !params.row.state, }, - }); - }} + }) + } /> ), }, @@ -189,8 +187,6 @@ export const Users = () => { return ( - - { sx={{ float: "right", }} - onClick={() => { - invitationDialogCtl.openDialog(roles); - }} + onClick={() => dialogs.open(InviteUserFormFormDialog)} > {t("button.invite")} diff --git a/frontend/src/contexts/dialogs.context.tsx b/frontend/src/contexts/dialogs.context.tsx index 7daa9027..f849f5ff 100644 --- a/frontend/src/contexts/dialogs.context.tsx +++ b/frontend/src/contexts/dialogs.context.tsx @@ -137,6 +137,7 @@ function DialogsProvider(props: DialogProviderProps) { onClose={async (result) => { await closeDialog(promise, result); }} + onSubmit={() => {}} {...msgProps} /> ))} diff --git a/frontend/src/types/common/dialogs.types.ts b/frontend/src/types/common/dialogs.types.ts index 9c884f15..6c756542 100644 --- a/frontend/src/types/common/dialogs.types.ts +++ b/frontend/src/types/common/dialogs.types.ts @@ -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 { DialogProps as MuiDialogProps } from "@mui/material"; +import { ButtonProps, DialogProps as MuiDialogProps } from "@mui/material"; import { BaseSyntheticEvent } from "react"; interface ConfirmDialogExtraOptions { @@ -24,6 +24,8 @@ export interface OpenDialogOptions extends ConfirmDialogExtraOptions { * @returns A promise that resolves when the dialog can be closed. */ onClose?: (result: R) => Promise; + + onSubmit?: (e: BaseSyntheticEvent) => void; } /** @@ -47,6 +49,8 @@ export interface DialogProps

{ * @returns A promise that resolves when the dialog can be fully closed. */ onClose: (result: R) => Promise; + + onSubmit: (e: BaseSyntheticEvent) => void; } export type DialogComponent = React.ComponentType>; @@ -142,24 +146,28 @@ export interface DialogProviderProps { } // form dialog -export interface FormDialogProps extends MuiDialogProps { +export interface FormDialogProps + extends FormButtonsProps, + Omit { title?: string; children?: React.ReactNode; - onSubmit: (e: BaseSyntheticEvent) => void; } // form -export type ComponentFormProps = { +export interface FormButtonsProps { + onSubmit: (e: BaseSyntheticEvent) => void; + onCancel?: () => void; + cancelButtonProps?: ButtonProps; + confirmButtonProps?: ButtonProps; +} + +export type ComponentFormProps = FormButtonsProps & { data: T | null; onError?: () => void; onSuccess?: () => void; Wrapper?: React.FC; - WrapperProps?: Partial; + WrapperProps?: Partial & Partial; }; -export interface FormButtonsProps { - onCancel?: () => void; - onSubmit: (e: BaseSyntheticEvent) => void; -} - -export type ComponentFormDialogProps = DialogProps; +export type ComponentFormDialogProps = FormButtonsProps & + DialogProps;