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 = () => {