Files
twenty/front/src/modules/settings/data-model/hooks/useFieldMetadataForm.ts
Thaïs 18dac1a2b6 feat: add Relation field form (#2572)
* feat: add useCreateOneRelationMetadata and useRelationMetadata

Closes #2423

* feat: add Relation field form

Closes #2003

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2023-11-17 23:38:39 +01:00

143 lines
3.9 KiB
TypeScript

import { useState } from 'react';
import { DeepPartial } from 'react-hook-form';
import { z } from 'zod';
import {
FieldMetadataType,
RelationMetadataType,
} from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { SettingsObjectFieldTypeSelectSectionFormValues } from '../components/SettingsObjectFieldTypeSelectSection';
type FormValues = {
description?: string;
icon: string;
label: string;
type: FieldMetadataType;
relation: SettingsObjectFieldTypeSelectSectionFormValues['relation'];
};
const defaultValues: FormValues = {
icon: 'IconUsers',
label: '',
type: FieldMetadataType.Text,
relation: {
type: RelationMetadataType.OneToMany,
},
};
const fieldSchema = z.object({
description: z.string().optional(),
icon: z.string().startsWith('Icon'),
label: z.string().min(1),
});
const relationSchema = fieldSchema.merge(
z.object({
type: z.literal(FieldMetadataType.Relation),
relation: z.object({
field: fieldSchema,
objectMetadataId: z.string().uuid(),
type: z.enum([
RelationMetadataType.OneToMany,
RelationMetadataType.OneToOne,
'MANY_TO_ONE',
]),
}),
}),
);
const { Relation: _, ...otherFieldTypes } = FieldMetadataType;
const otherFieldTypesSchema = fieldSchema.merge(
z.object({
type: z.enum(
Object.values(otherFieldTypes) as [
Exclude<FieldMetadataType, FieldMetadataType.Relation>,
...Exclude<FieldMetadataType, FieldMetadataType.Relation>[],
],
),
}),
);
const schema = z.discriminatedUnion('type', [
relationSchema,
otherFieldTypesSchema,
]);
export const useFieldMetadataForm = () => {
const [isInitialized, setIsInitialized] = useState(false);
const [initialFormValues, setInitialFormValues] =
useState<FormValues>(defaultValues);
const [formValues, setFormValues] = useState<FormValues>(defaultValues);
const [hasFieldFormChanged, setHasFieldFormChanged] = useState(false);
const [hasRelationFormChanged, setHasRelationFormChanged] = useState(false);
const [validationResult, setValidationResult] = useState(
schema.safeParse(formValues),
);
const mergePartialValues = (
previousValues: FormValues,
nextValues: DeepPartial<FormValues>,
) => ({
...previousValues,
...nextValues,
relation: {
...previousValues.relation,
...nextValues.relation,
field: {
...previousValues.relation?.field,
...nextValues.relation?.field,
},
},
});
const initForm = (lazyInitialFormValues: DeepPartial<FormValues>) => {
if (isInitialized) return;
const mergedFormValues = mergePartialValues(
initialFormValues,
lazyInitialFormValues,
);
setInitialFormValues(mergedFormValues);
setFormValues(mergedFormValues);
setValidationResult(schema.safeParse(mergedFormValues));
setIsInitialized(true);
};
const handleFormChange = (values: DeepPartial<FormValues>) => {
const nextFormValues = mergePartialValues(formValues, values);
setFormValues(nextFormValues);
setValidationResult(schema.safeParse(nextFormValues));
const { relation: initialRelationFormValues, ...initialFieldFormValues } =
initialFormValues;
const { relation: nextRelationFormValues, ...nextFieldFormValues } =
nextFormValues;
setHasFieldFormChanged(
!isDeeplyEqual(initialFieldFormValues, nextFieldFormValues),
);
setHasRelationFormChanged(
nextFieldFormValues.type === FieldMetadataType.Relation &&
!isDeeplyEqual(initialRelationFormValues, nextRelationFormValues),
);
};
return {
formValues,
handleFormChange,
hasFieldFormChanged,
hasFormChanged: hasFieldFormChanged || hasRelationFormChanged,
hasRelationFormChanged,
initForm,
isValid: validationResult.success,
validatedFormValues: validationResult.success
? validationResult.data
: undefined,
};
};