Merge pull request #664 from Hexastack/660-bug---content-edit-is-broken

fix: content edit
This commit is contained in:
Med Marrouchi 2025-02-07 08:56:11 +01:00 committed by GitHub
commit 3d0b7fbc77
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 111 additions and 278 deletions

View File

@ -1,5 +1,5 @@
/*
* 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.
@ -43,7 +43,7 @@ export class ContentStub extends BaseSchema {
@Prop({ type: Boolean, default: true })
status: boolean;
@Prop({ type: mongoose.Schema.Types.Mixed })
@Prop({ type: mongoose.Schema.Types.Mixed, default: {} })
dynamicFields: Record<string, any>;
@Prop({ type: String })

View File

@ -112,7 +112,8 @@
"text_is_required": "Text is required",
"invalid_file_type": "Invalid file type. Please select a file in the supported format.",
"select_category": "Select a flow",
"logout_failed": "Something went wrong during logout"
"logout_failed": "Something went wrong during logout",
"duplicate_labels_not_allowed": "Duplicate labels are not allowed"
},
"menu": {
"terms": "Terms of Use",
@ -191,7 +192,6 @@
"entities": "Content Types",
"new_content_type": "New Content Type",
"edit_content_type": "Edit Content Type",
"manage_fields": "Manage Fields",
"nodes": "Content",
"new_node": "New Content",
"edit_node": "Edit Content",

View File

@ -112,7 +112,8 @@
"text_is_required": "Texte requis",
"invalid_file_type": "Type de fichier invalide. Veuillez choisir un fichier dans un format pris en charge.",
"select_category": "Sélectionner une catégorie",
"logout_failed": "Une erreur s'est produite lors de la déconnexion"
"logout_failed": "Une erreur s'est produite lors de la déconnexion",
"duplicate_labels_not_allowed": "Les étiquettes en double ne sont pas autorisées"
},
"menu": {
"terms": "Conditions d'utilisation",
@ -191,7 +192,6 @@
"entities": "Types de contenu",
"new_content_type": "Nouveau type de contenu",
"edit_content_type": "Modifier le type de contenu",
"manage_fields": "Gérer les champs",
"nodes": "Contenu",
"new_node": "Nouveau contenu",
"edit_node": "Modifier le contenu",

View File

@ -1,14 +1,15 @@
/*
* 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 AddIcon from "@mui/icons-material/Add";
import { Button, Dialog, DialogActions, DialogContent } from "@mui/material";
import { FC, useEffect } from "react";
import { useForm } from "react-hook-form";
import { useFieldArray, useForm } from "react-hook-form";
import DialogButtons from "@/app-components/buttons/DialogButtons";
import { DialogTitle } from "@/app-components/dialogs/DialogTitle";
@ -21,10 +22,10 @@ import { DialogControlProps } from "@/hooks/useDialog";
import { useToast } from "@/hooks/useToast";
import { useTranslate } from "@/hooks/useTranslate";
import { EntityType } from "@/services/types";
import {
IContentType,
IContentTypeAttributes,
} from "@/types/content-type.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<IContentType>;
export const ContentTypeDialog: FC<ContentTypeDialogProps> = ({
@ -37,50 +38,67 @@ export const ContentTypeDialog: FC<ContentTypeDialogProps> = ({
const {
handleSubmit,
register,
control,
reset,
setValue,
formState: { errors },
} = useForm<IContentTypeAttributes>({
defaultValues: { name: data?.name || "" },
} = useForm<Partial<IContentType>>({
defaultValues: {
name: data?.name || "",
fields: data?.fields || FIELDS_FORM_DEFAULT_VALUES,
},
});
const CloseAndReset = () => {
const { append, fields, remove } = useFieldArray({
name: "fields",
control,
});
const closeAndReset = () => {
closeDialog();
reset();
reset({
name: "",
fields: FIELDS_FORM_DEFAULT_VALUES,
});
};
const { mutateAsync: createContentType } = useCreate(
EntityType.CONTENT_TYPE,
{
onError: (error) => {
toast.error(error);
},
onSuccess: () => {
closeDialog();
toast.success(t("message.success_save"));
},
const { mutate: createContentType } = useCreate(EntityType.CONTENT_TYPE, {
onError: (error) => {
toast.error(error.message || t("message.internal_server_error"));
},
);
const { mutateAsync: updateContentType } = useUpdate(
EntityType.CONTENT_TYPE,
{
onError: () => {
toast.error(t("message.internal_server_error"));
},
onSuccess: () => {
closeDialog();
toast.success(t("message.success_save"));
},
onSuccess: () => {
closeDialog();
toast.success(t("message.success_save"));
},
);
const validationRules = {
name: {
required: t("message.name_is_required"),
});
const { mutate: updateContentType } = useUpdate(EntityType.CONTENT_TYPE, {
onError: (error) => {
toast.error(error.message || t("message.internal_server_error"));
},
};
const onSubmitForm = async (params: IContentTypeAttributes) => {
onSuccess: () => {
closeDialog();
toast.success(t("message.success_save"));
},
});
const onSubmitForm = async (params) => {
const labelCounts: Record<string, number> = params.fields.reduce(
(acc, field) => {
if (!field.label.trim()) return acc;
acc[field.label] = (acc[field.label] || 0) + 1;
return acc;
},
{} as Record<string, number>,
);
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,
});
updateContentType({ id: data.id, params });
} else {
createContentType(params);
}
@ -94,16 +112,17 @@ export const ContentTypeDialog: FC<ContentTypeDialogProps> = ({
if (data) {
reset({
name: data.name,
fields: data.fields || FIELDS_FORM_DEFAULT_VALUES,
});
} else {
reset();
reset({ name: "", fields: FIELDS_FORM_DEFAULT_VALUES });
}
}, [data, reset]);
return (
<Dialog open={open} fullWidth onClose={CloseAndReset}>
<Dialog open={open} fullWidth onClose={closeAndReset}>
<form onSubmit={handleSubmit(onSubmitForm)}>
<DialogTitle onClose={CloseAndReset}>
<DialogTitle onClose={closeAndReset}>
{data ? t("title.edit_content_type") : t("title.new_content_type")}
</DialogTitle>
<DialogContent>
@ -112,16 +131,46 @@ export const ContentTypeDialog: FC<ContentTypeDialogProps> = ({
<Input
label={t("label.name")}
error={!!errors.name}
{...register("name", validationRules.name)}
{...register("name", {
required: t("message.name_is_required"),
})}
helperText={errors.name ? errors.name.message : null}
required
autoFocus
/>
</ContentItem>
{fields.map((f, index) => (
<ContentItem
key={f.id}
display="flex"
justifyContent="space-between"
gap={2}
>
<FieldInput
setValue={setValue}
control={control}
remove={remove}
index={index}
disabled={READ_ONLY_FIELDS.includes(f.label as any)}
/>
</ContentItem>
))}
<ContentItem>
<Button
startIcon={<AddIcon />}
variant="contained"
onClick={() =>
append({ label: "", name: "", type: ContentFieldType.TEXT })
}
>
{t("button.add")}
</Button>
</ContentItem>
</ContentContainer>
</DialogContent>
<DialogActions>
<DialogButtons closeDialog={closeDialog} />
<DialogButtons closeDialog={closeAndReset} />
</DialogActions>
</form>
</Dialog>

View File

@ -1,219 +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 {
Button,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
Stack,
} from "@mui/material";
import { useEffect } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import DialogButtons from "@/app-components/buttons/DialogButtons";
import {
DialogTitle,
ContentContainer,
ContentItem,
} from "@/app-components/dialogs";
import { Input } from "@/app-components/inputs/Input";
import { useGet } from "@/hooks/crud/useGet";
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 EditContentTypeDialogFieldsProps = DialogControlProps<IContentType>;
export const EditContentTypeFieldsDialog = ({
data: contentType,
closeDialog,
open,
}: EditContentTypeDialogFieldsProps) => {
const { t } = useTranslate();
const { isLoading, data, refetch } = useGet(contentType?.id || "", {
entity: EntityType.CONTENT_TYPE,
});
const { toast } = useToast();
const {
handleSubmit,
control,
reset,
setValue,
register,
formState: { errors },
} = useForm<Partial<IContentType>>({
mode: "onChange",
values: {
fields: data?.fields,
name: data?.name,
},
defaultValues: {
fields: FIELDS_FORM_DEFAULT_VALUES,
name: data?.name,
},
});
const { append, fields, replace, remove } = useFieldArray<
Pick<IContentType, "fields">,
"fields"
>({
name: "fields",
control,
keyName: "id",
rules: {
required: true,
},
});
const validationRules = {
name: {
required: t("message.name_is_required"),
},
};
useEffect(() => {
register("fields");
}, [register]);
useEffect(() => {
if (data?.fields) {
replace(data.fields);
}
}, [data, replace]);
useEffect(() => {
if (!open) {
reset();
}
if (open) {
refetch();
}
}, [open, reset]);
useEffect(() => {
if (data) {
reset({
name: data.name,
fields: data.fields,
});
} else {
reset();
}
}, [data, reset]);
function handleClose() {
closeDialog();
}
const { mutateAsync: updateContentType } = useUpdate(
EntityType.CONTENT_TYPE,
{
onError: (error) => {
toast.error(`${t("message.internal_server_error")}: ${error}`);
},
onSuccess: () => {
toast.success(t("message.success_save"));
},
},
);
return (
<Dialog
open={open}
fullWidth
maxWidth="xl"
sx={{ width: "fit-content", mx: "auto", minWidth: "600px" }}
onClose={handleClose}
>
<DialogTitle onClose={handleClose}>
{t("title.manage_fields")}
</DialogTitle>
<form
onSubmit={handleSubmit(async ({ name, fields }) => {
if (!!contentType)
await updateContentType({
id: contentType.id,
params: {
name,
fields,
},
});
handleClose();
})}
>
<DialogContent>
<ContentContainer>
<ContentItem>
<Input
label={t("label.name")}
error={!!errors.name}
{...register("name", validationRules.name)}
helperText={errors.name ? errors.name.message : null}
required
autoFocus
/>
</ContentItem>
{!isLoading
? fields.map((f, index) => (
<ContentItem
key={f.id}
justifyContent="space-between"
alignItems="center"
gap={2}
display="flex"
>
<FieldInput
setValue={setValue}
control={control}
remove={remove}
index={index}
disabled={READ_ONLY_FIELDS.includes(f.label as any)}
/>
</ContentItem>
))
: null}
{isLoading ? (
<ContentItem>
<Stack sx={{ alignItems: "center", placeContent: "center" }}>
<CircularProgress sx={{ color: "primary.main" }} />
</Stack>
</ContentItem>
) : null}
<ContentItem>
<Button
startIcon={<AddIcon />}
variant="contained"
onClick={() =>
append({
label: "",
name: "",
type: ContentFieldType.TEXT,
})
}
disabled={isLoading}
sx={{ mx: "auto" }}
>
{t("button.add")}
</Button>
</ContentItem>
</ContentContainer>
</DialogContent>
<DialogActions>
<DialogButtons closeDialog={closeDialog} />
</DialogActions>
</form>
</Dialog>
);
};

View File

@ -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 DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
import { MenuItem } from "@mui/material";
import { useEffect } from "react";
@ -59,11 +60,14 @@ export const FieldInput = ({
<Controller
control={props.control}
name={`fields.${index}.label`}
render={({ field }) => (
rules={{ required: t("message.label_is_required") }}
render={({ field, fieldState }) => (
<Input
disabled={props.disabled}
{...field}
label={t("label.label")}
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>

View File

@ -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 { faAlignLeft } from "@fortawesome/free-solid-svg-icons";
import AddIcon from "@mui/icons-material/Add";
import { Button, Grid, Paper } from "@mui/material";
@ -33,7 +34,6 @@ import { PermissionAction } from "@/types/permission.types";
import { getDateTimeFormatter } from "@/utils/date";
import { ContentTypeDialog } from "./ContentTypeDialog";
import { EditContentTypeFieldsDialog } from "./EditContentTypeFieldsDialog";
export const ContentTypes = () => {
const { t } = useTranslate();
@ -125,7 +125,7 @@ export const ContentTypes = () => {
deleteContentType(deleteDialogCtl.data);
}}
/>
<EditContentTypeFieldsDialog {...fieldsDialogCtl} />
<ContentTypeDialog {...getDisplayDialogs(fieldsDialogCtl)} />
<Grid padding={2} container>
<Grid item width="100%">
<DataGrid

View File

@ -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 LinkIcon from "@mui/icons-material/Link";
import {
Dialog,