From dcc5f0c7f3e46e6acfd88b4b5ff660c663646efe Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Fri, 7 Feb 2025 17:39:23 +0100 Subject: [PATCH] refactor(frontend): update contentType dialogs --- .../content-types/ContentTypeDialog.tsx | 178 ------------------ .../content-types/ContentTypeForm.tsx | 160 ++++++++++++++++ .../content-types/ContentTypeFormDialog.tsx | 24 +++ .../src/components/content-types/index.tsx | 51 ++--- 4 files changed, 204 insertions(+), 209 deletions(-) delete mode 100644 frontend/src/components/content-types/ContentTypeDialog.tsx create mode 100644 frontend/src/components/content-types/ContentTypeForm.tsx create mode 100644 frontend/src/components/content-types/ContentTypeFormDialog.tsx diff --git a/frontend/src/components/content-types/ContentTypeDialog.tsx b/frontend/src/components/content-types/ContentTypeDialog.tsx deleted file mode 100644 index 735496da..00000000 --- a/frontend/src/components/content-types/ContentTypeDialog.tsx +++ /dev/null @@ -1,178 +0,0 @@ -/* - * 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 { Button, Dialog, DialogActions, DialogContent } from "@mui/material"; -import { FC, useEffect } from "react"; -import { useFieldArray, 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 { ContentFieldType, IContentType } from "@/types/content-type.types"; - -import { FieldInput } from "./components/FieldInput"; -import { FIELDS_FORM_DEFAULT_VALUES, READ_ONLY_FIELDS } from "./constants"; - -export type ContentTypeDialogProps = DialogControlProps; -export const ContentTypeDialog: FC = ({ - open, - data, - closeDialog, -}) => { - const { toast } = useToast(); - const { t } = useTranslate(); - const { - handleSubmit, - register, - control, - reset, - setValue, - formState: { errors }, - } = useForm>({ - defaultValues: { - name: data?.name || "", - fields: data?.fields || FIELDS_FORM_DEFAULT_VALUES, - }, - }); - const { append, fields, remove } = useFieldArray({ - name: "fields", - control, - }); - const closeAndReset = () => { - closeDialog(); - reset({ - name: "", - fields: FIELDS_FORM_DEFAULT_VALUES, - }); - }; - const { mutate: createContentType } = useCreate(EntityType.CONTENT_TYPE, { - onError: (error) => { - toast.error(error.message || t("message.internal_server_error")); - }, - onSuccess: () => { - closeDialog(); - toast.success(t("message.success_save")); - }, - }); - const { mutate: updateContentType } = useUpdate(EntityType.CONTENT_TYPE, { - onError: (error) => { - toast.error(error.message || t("message.internal_server_error")); - }, - onSuccess: () => { - closeDialog(); - toast.success(t("message.success_save")); - }, - }); - const onSubmitForm = async (params) => { - const labelCounts: Record = params.fields.reduce( - (acc, field) => { - if (!field.label.trim()) return acc; - acc[field.label] = (acc[field.label] || 0) + 1; - - return acc; - }, - {} as Record, - ); - const hasDuplicates = Object.values(labelCounts).some( - (count: number) => count > 1, - ); - - if (hasDuplicates) { - toast.error(t("message.duplicate_labels_not_allowed")); - - return; - } - - if (data) { - updateContentType({ id: data.id, params }); - } else { - createContentType(params); - } - }; - - useEffect(() => { - if (open) reset(); - }, [open, reset]); - - useEffect(() => { - if (data) { - reset({ - name: data.name, - fields: data.fields || FIELDS_FORM_DEFAULT_VALUES, - }); - } else { - reset({ name: "", fields: FIELDS_FORM_DEFAULT_VALUES }); - } - }, [data, reset]); - - return ( - -
- - {data ? t("title.edit_content_type") : t("title.new_content_type")} - - - - - - - - {fields.map((f, index) => ( - - - - ))} - - - - - - - - -
-
- ); -}; diff --git a/frontend/src/components/content-types/ContentTypeForm.tsx b/frontend/src/components/content-types/ContentTypeForm.tsx new file mode 100644 index 00000000..de23f7db --- /dev/null +++ b/frontend/src/components/content-types/ContentTypeForm.tsx @@ -0,0 +1,160 @@ +/* + * 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 { Button } from "@mui/material"; +import { FC, Fragment, useEffect } from "react"; +import { useFieldArray, 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 { ContentFieldType, IContentType } from "@/types/content-type.types"; + +import { FieldInput } from "./components/FieldInput"; +import { FIELDS_FORM_DEFAULT_VALUES, READ_ONLY_FIELDS } from "./constants"; + +export const ContentTypeForm: FC> = ({ + data, + Wrapper = Fragment, + WrapperProps, + ...rest +}) => { + const { toast } = useToast(); + const { t } = useTranslate(); + const { + reset, + control, + register, + setValue, + formState: { errors }, + handleSubmit, + } = useForm>({ + defaultValues: { + name: data?.name || "", + fields: data?.fields || FIELDS_FORM_DEFAULT_VALUES, + }, + }); + const { append, fields, remove } = useFieldArray({ + name: "fields", + control, + }); + 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 { mutate: createContentType } = useCreate( + EntityType.CONTENT_TYPE, + options, + ); + const { mutate: updateContentType } = useUpdate( + EntityType.CONTENT_TYPE, + options, + ); + const onSubmitForm = (params) => { + const labelCounts: Record = params.fields.reduce( + (acc, field) => { + if (!field.label.trim()) return acc; + acc[field.label] = (acc[field.label] || 0) + 1; + + return acc; + }, + {} as Record, + ); + const hasDuplicates = Object.values(labelCounts).some( + (count: number) => count > 1, + ); + + if (hasDuplicates) { + toast.error(t("message.duplicate_labels_not_allowed")); + + return; + } + + if (data) { + updateContentType({ id: data.id, params }); + } else { + createContentType(params); + } + }; + + useEffect(() => { + if (data) { + reset({ + name: data.name, + fields: data.fields || FIELDS_FORM_DEFAULT_VALUES, + }); + } else { + reset({ name: "", fields: FIELDS_FORM_DEFAULT_VALUES }); + } + }, [data, reset]); + + return ( + +
+ + + + + + {fields.map((f, index) => ( + + + + ))} + + + + +
+
+ ); +}; diff --git a/frontend/src/components/content-types/ContentTypeFormDialog.tsx b/frontend/src/components/content-types/ContentTypeFormDialog.tsx new file mode 100644 index 00000000..7675715f --- /dev/null +++ b/frontend/src/components/content-types/ContentTypeFormDialog.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 { IContentType } from "@/types/content-type.types"; + +import { ContentTypeForm } from "./ContentTypeForm"; + +export const ContentTypeFormDialog = ( + props: ComponentFormDialogProps, +) => ( + + Form={ContentTypeForm} + addText="title.new_content_type" + editText="title.edit_content_type" + {...props} + /> +); diff --git a/frontend/src/components/content-types/index.tsx b/frontend/src/components/content-types/index.tsx index 46ef5638..7cd13d74 100644 --- a/frontend/src/components/content-types/index.tsx +++ b/frontend/src/components/content-types/index.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 { faAlignLeft } from "@fortawesome/free-solid-svg-icons"; import AddIcon from "@mui/icons-material/Add"; import { Button, Grid, Paper } from "@mui/material"; 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, @@ -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,16 +32,13 @@ import { IContentType } from "@/types/content-type.types"; import { PermissionAction } from "@/types/permission.types"; import { getDateTimeFormatter } from "@/utils/date"; -import { ContentTypeDialog } from "./ContentTypeDialog"; +import { ContentTypeFormDialog } from "./ContentTypeFormDialog"; export const ContentTypes = () => { const { t } = useTranslate(); const { toast } = useToast(); const router = useRouter(); - // Dialog Controls - const addDialogCtl = useDialog(false); - const deleteDialogCtl = useDialog(false); - const fieldsDialogCtl = useDialog(false); + const dialogs = useDialogs(); // data fetching const { onSearch, searchPayload } = useSearch({ $iLike: ["name"], @@ -53,18 +49,14 @@ export const ContentTypes = () => { params: searchPayload, }, ); - const { mutateAsync: deleteContentType } = useDelete( - EntityType.CONTENT_TYPE, - { - onSuccess: () => { - deleteDialogCtl.closeDialog(); - toast.success(t("message.item_delete_success")); - }, - onError: (error) => { - toast.error(error.message || t("message.internal_server_error")); - }, + const { mutate: deleteContentType } = useDelete(EntityType.CONTENT_TYPE, { + onSuccess: () => { + toast.success(t("message.item_delete_success")); }, - ); + onError: (error) => { + toast.error(error.message || t("message.internal_server_error")); + }, + }); const hasPermission = useHasPermission(); const actionColumns = useActionColumns( EntityType.CONTENT_TYPE, @@ -75,12 +67,18 @@ export const ContentTypes = () => { }, { label: ActionColumnLabel.Edit, - action: (row) => fieldsDialogCtl.openDialog(row), + action: (row) => dialogs.open(ContentTypeFormDialog, row), requires: [PermissionAction.UPDATE], }, { label: ActionColumnLabel.Delete, - action: (row) => deleteDialogCtl.openDialog(row.id), + action: async ({ id }) => { + const isConfirmed = await dialogs.confirm(ConfirmDialogBody); + + if (isConfirmed) { + deleteContentType(id); + } + }, requires: [PermissionAction.DELETE], }, ], @@ -106,7 +104,7 @@ export const ContentTypes = () => {