mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
fix(frontend): enhance contentType logic
This commit is contained in:
@@ -25,26 +25,27 @@ import {
|
||||
IContentTypeAttributes,
|
||||
} from "@/types/content-type.types";
|
||||
import { generateId } from "@/utils/generateId";
|
||||
import { slugify } from "@/utils/string";
|
||||
|
||||
import { FieldInput } from "./components/FieldInput";
|
||||
import { FIELDS_FORM_DEFAULT_VALUES, READ_ONLY_FIELDS } from "./constants";
|
||||
import { FIELDS_FORM_DEFAULT_VALUES } from "./constants";
|
||||
|
||||
export const ContentTypeForm: FC<ComponentFormProps<IContentType>> = ({
|
||||
data: { defaultValues: contentTypeWithoutId },
|
||||
data: { defaultValues: contentTypeWithoutUuid },
|
||||
Wrapper = Fragment,
|
||||
WrapperProps,
|
||||
...rest
|
||||
}) => {
|
||||
const contentType = useMemo(
|
||||
() =>
|
||||
contentTypeWithoutId && {
|
||||
...contentTypeWithoutId,
|
||||
fields: contentTypeWithoutId?.fields?.map((field) => ({
|
||||
contentTypeWithoutUuid && {
|
||||
...contentTypeWithoutUuid,
|
||||
fields: contentTypeWithoutUuid?.fields?.map((field) => ({
|
||||
...field,
|
||||
uuid: generateId(),
|
||||
})),
|
||||
},
|
||||
[contentTypeWithoutId],
|
||||
[contentTypeWithoutUuid],
|
||||
);
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslate();
|
||||
@@ -62,6 +63,27 @@ export const ContentTypeForm: FC<ComponentFormProps<IContentType>> = ({
|
||||
});
|
||||
const { append, fields, remove } = useFieldArray({
|
||||
name: "fields",
|
||||
rules: {
|
||||
validate: (value) => {
|
||||
const labelCounts = value.reduce((acc, field) => {
|
||||
if (!field.label.trim()) return acc;
|
||||
acc[field.label] = (acc[field.label] || 0) + 1;
|
||||
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
const hasDuplicatedLabels = Object.values(labelCounts).some(
|
||||
(count: number) => count > 1,
|
||||
);
|
||||
|
||||
if (hasDuplicatedLabels) {
|
||||
toast.error(t("message.duplicate_labels_not_allowed"));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
control,
|
||||
});
|
||||
const options = {
|
||||
@@ -83,22 +105,6 @@ export const ContentTypeForm: FC<ComponentFormProps<IContentType>> = ({
|
||||
options,
|
||||
);
|
||||
const onSubmitForm = (params: IContentTypeAttributes) => {
|
||||
const labelCounts = 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 (contentType?.id) {
|
||||
updateContentType({ id: contentType.id, params });
|
||||
} else {
|
||||
@@ -122,23 +128,28 @@ export const ContentTypeForm: FC<ComponentFormProps<IContentType>> = ({
|
||||
autoFocus
|
||||
/>
|
||||
</ContentItem>
|
||||
|
||||
{fields.map((f, index) => (
|
||||
{fields.map((field, idx) => (
|
||||
<ContentItem
|
||||
key={f.id}
|
||||
key={field.id}
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
gap={2}
|
||||
>
|
||||
<FieldInput
|
||||
index={index}
|
||||
remove={remove}
|
||||
idx={idx}
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
disabled={READ_ONLY_FIELDS.includes(f.label as any)}
|
||||
contentTypeField={contentType?.fields?.find(
|
||||
({ uuid }) => uuid === f.uuid,
|
||||
)}
|
||||
onLabelChange={(value) => {
|
||||
const fieldName = contentType?.fields?.find(
|
||||
({ uuid }) => uuid === field.uuid,
|
||||
)?.name;
|
||||
|
||||
if (!fieldName) {
|
||||
setValue(`fields.${idx}.name`, value ? slugify(value) : "");
|
||||
}
|
||||
}}
|
||||
onRemove={() => {
|
||||
remove(idx);
|
||||
}}
|
||||
/>
|
||||
</ContentItem>
|
||||
))}
|
||||
|
||||
@@ -8,37 +8,27 @@
|
||||
|
||||
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
|
||||
import { MenuItem } from "@mui/material";
|
||||
import {
|
||||
Control,
|
||||
Controller,
|
||||
UseFieldArrayRemove,
|
||||
UseFormSetValue,
|
||||
} from "react-hook-form";
|
||||
import { useMemo } from "react";
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
|
||||
import { IconButton } from "@/app-components/buttons/IconButton";
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
import {
|
||||
ContentField,
|
||||
ContentFieldType,
|
||||
IContentType,
|
||||
} from "@/types/content-type.types";
|
||||
import { slugify } from "@/utils/string";
|
||||
import { ContentFieldType, IContentType } from "@/types/content-type.types";
|
||||
|
||||
import { READ_ONLY_FIELDS } from "../constants";
|
||||
|
||||
export const FieldInput = ({
|
||||
setValue,
|
||||
index,
|
||||
contentTypeField,
|
||||
idx,
|
||||
...props
|
||||
}: {
|
||||
index: number;
|
||||
disabled?: boolean;
|
||||
remove: UseFieldArrayRemove;
|
||||
idx: number;
|
||||
control: Control<IContentType>;
|
||||
setValue: UseFormSetValue<IContentType>;
|
||||
contentTypeField?: ContentField;
|
||||
onRemove?: () => void;
|
||||
onLabelChange?: (value: string) => void;
|
||||
}) => {
|
||||
const { t } = useTranslate();
|
||||
const isDisabled = useMemo(() => idx < READ_ONLY_FIELDS.length, [idx]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -46,52 +36,42 @@ export const FieldInput = ({
|
||||
variant="text"
|
||||
color="error"
|
||||
size="medium"
|
||||
onClick={() => props.remove(index)}
|
||||
disabled={props.disabled}
|
||||
onClick={props.onRemove}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<DeleteOutlineIcon strokeWidth={1} fontSize="medium" />
|
||||
</IconButton>
|
||||
|
||||
<Controller
|
||||
control={props.control}
|
||||
name={`fields.${index}.label`}
|
||||
name={`fields.${idx}.label`}
|
||||
rules={{ required: t("message.label_is_required") }}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
disabled={props.disabled}
|
||||
disabled={isDisabled}
|
||||
{...field}
|
||||
label={t("label.label")}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
onChange={(e) => {
|
||||
const currentValue = e.target.value;
|
||||
|
||||
if (!contentTypeField?.label || !contentTypeField?.name) {
|
||||
setValue(
|
||||
`fields.${index}.name`,
|
||||
currentValue ? slugify(currentValue) : "",
|
||||
);
|
||||
}
|
||||
|
||||
props?.onLabelChange?.(e.target.value);
|
||||
field.onChange(e);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name={`fields.${index}.name`}
|
||||
name={`fields.${idx}.name`}
|
||||
render={({ field }) => (
|
||||
<Input disabled {...field} label={t("label.name")} />
|
||||
)}
|
||||
control={props.control}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name={`fields.${index}.type`}
|
||||
name={`fields.${idx}.type`}
|
||||
control={props.control}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
disabled={props.disabled}
|
||||
disabled={isDisabled}
|
||||
label={t("label.type")}
|
||||
{...field}
|
||||
select
|
||||
|
||||
Reference in New Issue
Block a user