refactor: use react-hook-form to validate Settings/DataModel/Field (#4916)

Closes #4295
This commit is contained in:
Thaïs
2024-05-07 11:44:46 +02:00
committed by GitHub
parent 9c25c1beae
commit d0759ad7cc
18 changed files with 13234 additions and 13369 deletions

View File

@ -1,8 +1,11 @@
import { useEffect, useMemo } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { useNavigate, useParams } from 'react-router-dom';
import styled from '@emotion/styled';
import { zodResolver } from '@hookform/resolvers/zod';
import { isNonEmptyString } from '@sniptt/guards';
import { IconArchive, IconSettings } from 'twenty-ui';
import { z } from 'zod';
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
@ -14,10 +17,11 @@ import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsObjectFieldCurrencyFormValues } from '@/settings/data-model/components/SettingsObjectFieldCurrencyForm';
import { SettingsObjectFieldFormSection } from '@/settings/data-model/components/SettingsObjectFieldFormSection';
import { SettingsDataModelFieldAboutForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldAboutForm';
import { SettingsDataModelFieldSettingsFormCard } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard';
import { SettingsDataModelFieldTypeSelect } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldTypeSelect';
import { useFieldMetadataForm } from '@/settings/data-model/fields/forms/hooks/useFieldMetadataForm';
import { settingsFieldFormSchema } from '@/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema';
import { isFieldTypeSupportedInSettings } from '@/settings/data-model/utils/isFieldTypeSupportedInSettings';
import { AppPath } from '@/types/AppPath';
import { H2Title } from '@/ui/display/typography/components/H2Title';
@ -31,6 +35,10 @@ import {
RelationMetadataType,
} from '~/generated-metadata/graphql';
type SettingsDataModelFieldEditFormValues = z.infer<
typeof settingsFieldFormSchema
>;
const StyledSettingsObjectFieldTypeSelect = styled(
SettingsDataModelFieldTypeSelect,
)`
@ -80,6 +88,11 @@ export const SettingsObjectFieldEdit = () => {
[activeMetadataField, getRelationMetadata],
) ?? {};
const formConfig = useForm<SettingsDataModelFieldEditFormValues>({
mode: 'onTouched',
resolver: zodResolver(settingsFieldFormSchema),
});
const {
formValues,
handleFormChange,
@ -130,9 +143,6 @@ export const SettingsObjectFieldEdit = () => {
if (!isFieldTypeSupported) return;
initForm({
icon: activeMetadataField.icon ?? undefined,
label: activeMetadataField.label,
description: activeMetadataField.description ?? undefined,
type: fieldType,
...(currencyDefaultValue ? { currency: currencyDefaultValue } : {}),
relation: {
@ -163,7 +173,10 @@ export const SettingsObjectFieldEdit = () => {
if (!isInitialized || !activeObjectMetadataItem || !activeMetadataField)
return null;
const canSave = isValid && hasFormChanged;
const canSave =
formConfig.formState.isValid &&
isValid &&
(formConfig.formState.isDirty || hasFormChanged);
const isLabelIdentifier = isLabelIdentifierField({
fieldMetadataItem: activeMetadataField,
@ -173,6 +186,9 @@ export const SettingsObjectFieldEdit = () => {
const handleSave = async () => {
if (!validatedFormValues) return;
const formValues = formConfig.getValues();
const { dirtyFields } = formConfig.formState;
try {
if (
validatedFormValues.type === FieldMetadataType.Relation &&
@ -186,17 +202,17 @@ export const SettingsObjectFieldEdit = () => {
type: validatedFormValues.type,
});
}
if (
Object.keys(dirtyFields).length > 0 ||
hasFieldFormChanged ||
hasSelectFormChanged ||
hasMultiSelectFormChanged ||
hasDefaultValueChanged
) {
await editMetadataField({
description: validatedFormValues.description,
icon: validatedFormValues.icon,
...formValues,
id: activeMetadataField.id,
label: validatedFormValues.label,
defaultValue: validatedFormValues.defaultValue,
type: validatedFormValues.type,
options:
@ -225,77 +241,83 @@ export const SettingsObjectFieldEdit = () => {
canPersistFieldMetadataItemUpdate(activeMetadataField);
return (
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
<SettingsPageContainer>
<SettingsHeaderContainer>
<Breadcrumb
links={[
{ children: 'Objects', href: '/settings/objects' },
{
children: activeObjectMetadataItem.labelPlural,
href: `/settings/objects/${objectSlug}`,
},
{ children: activeMetadataField.label },
]}
/>
{shouldDisplaySaveAndCancel && (
<SaveAndCancelButtons
isSaveDisabled={!canSave}
onCancel={() => navigate(`/settings/objects/${objectSlug}`)}
onSave={handleSave}
// eslint-disable-next-line react/jsx-props-no-spreading
<FormProvider {...formConfig}>
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
<SettingsPageContainer>
<SettingsHeaderContainer>
<Breadcrumb
links={[
{ children: 'Objects', href: '/settings/objects' },
{
children: activeObjectMetadataItem.labelPlural,
href: `/settings/objects/${objectSlug}`,
},
{ children: activeMetadataField.label },
]}
/>
)}
</SettingsHeaderContainer>
<SettingsObjectFieldFormSection
disabled={!activeMetadataField.isCustom}
name={formValues.label}
description={formValues.description}
iconKey={formValues.icon}
onChange={handleFormChange}
/>
<Section>
<H2Title
title="Type and values"
description="The field's type and values."
/>
<StyledSettingsObjectFieldTypeSelect
disabled
onChange={handleFormChange}
value={formValues.type}
/>
<SettingsDataModelFieldSettingsFormCard
disableCurrencyForm
fieldMetadataItem={{
icon: formValues.icon,
id: activeMetadataField.id,
label: formValues.label,
name: activeMetadataField.name,
type: formValues.type,
}}
objectMetadataItem={activeObjectMetadataItem}
onChange={handleFormChange}
relationFieldMetadataItem={relationFieldMetadataItem}
values={{
currency: formValues.currency,
relation: formValues.relation,
select: formValues.select,
multiSelect: formValues.multiSelect,
defaultValue: formValues.defaultValue,
}}
/>
</Section>
{!isLabelIdentifier && (
{shouldDisplaySaveAndCancel && (
<SaveAndCancelButtons
isSaveDisabled={!canSave}
onCancel={() => navigate(`/settings/objects/${objectSlug}`)}
onSave={handleSave}
/>
)}
</SettingsHeaderContainer>
<Section>
<H2Title title="Danger zone" description="Disable this field" />
<Button
Icon={IconArchive}
title="Disable"
size="small"
onClick={handleDisable}
<H2Title
title="Name and description"
description="The name and description of this field"
/>
<SettingsDataModelFieldAboutForm
disabled={!activeMetadataField.isCustom}
fieldMetadataItem={activeMetadataField}
/>
</Section>
)}
</SettingsPageContainer>
</SubMenuTopBarContainer>
<Section>
<H2Title
title="Type and values"
description="The field's type and values."
/>
<StyledSettingsObjectFieldTypeSelect
disabled
onChange={handleFormChange}
value={formValues.type}
/>
<SettingsDataModelFieldSettingsFormCard
disableCurrencyForm
fieldMetadataItem={{
icon: formConfig.watch('icon'),
id: activeMetadataField.id,
label: formConfig.watch('label'),
name: activeMetadataField.name,
type: formValues.type,
}}
objectMetadataItem={activeObjectMetadataItem}
onChange={handleFormChange}
relationFieldMetadataItem={relationFieldMetadataItem}
values={{
currency: formValues.currency,
relation: formValues.relation,
select: formValues.select,
multiSelect: formValues.multiSelect,
defaultValue: formValues.defaultValue,
}}
/>
</Section>
{!isLabelIdentifier && (
<Section>
<H2Title title="Danger zone" description="Disable this field" />
<Button
Icon={IconArchive}
title="Disable"
size="small"
onClick={handleDisable}
/>
</Section>
)}
</SettingsPageContainer>
</SubMenuTopBarContainer>
</FormProvider>
);
};

View File

@ -1,8 +1,12 @@
import { useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { useNavigate, useParams } from 'react-router-dom';
import { Reference, useApolloClient } from '@apollo/client';
import styled from '@emotion/styled';
import { zodResolver } from '@hookform/resolvers/zod';
import pick from 'lodash.pick';
import { IconSettings } from 'twenty-ui';
import { z } from 'zod';
import { useCreateOneRelationMetadataItem } from '@/object-metadata/hooks/useCreateOneRelationMetadataItem';
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
@ -15,10 +19,11 @@ import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsObjectFieldFormSection } from '@/settings/data-model/components/SettingsObjectFieldFormSection';
import { SettingsDataModelFieldAboutForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldAboutForm';
import { SettingsDataModelFieldSettingsFormCard } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard';
import { SettingsDataModelFieldTypeSelect } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldTypeSelect';
import { useFieldMetadataForm } from '@/settings/data-model/fields/forms/hooks/useFieldMetadataForm';
import { settingsFieldFormSchema } from '@/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema';
import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType';
import { AppPath } from '@/types/AppPath';
import { H2Title } from '@/ui/display/typography/components/H2Title';
@ -31,6 +36,10 @@ import { ViewType } from '@/views/types/ViewType';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
type SettingsDataModelNewFieldFormValues = z.infer<
typeof settingsFieldFormSchema
>;
const StyledSettingsObjectFieldTypeSelect = styled(
SettingsDataModelFieldTypeSelect,
)`
@ -57,10 +66,15 @@ export const SettingsObjectNewFieldStep2 = () => {
formValues,
handleFormChange,
initForm,
isValid: canSave,
isValid,
validatedFormValues,
} = useFieldMetadataForm();
const formConfig = useForm<SettingsDataModelNewFieldFormValues>({
mode: 'onTouched',
resolver: zodResolver(settingsFieldFormSchema),
});
useEffect(() => {
if (!activeObjectMetadataItem) {
navigate(AppPath.NotFound);
@ -121,19 +135,18 @@ export const SettingsObjectNewFieldStep2 = () => {
if (!activeObjectMetadataItem) return null;
const canSave = formConfig.formState.isValid && isValid;
const handleSave = async () => {
if (!validatedFormValues) return;
const formValues = formConfig.getValues();
try {
if (validatedFormValues.type === FieldMetadataType.Relation) {
const createdRelation = await createOneRelationMetadata({
relationType: validatedFormValues.relation.type,
field: {
description: validatedFormValues.description,
icon: validatedFormValues.icon,
label: validatedFormValues.label,
type: validatedFormValues.type,
},
field: pick(formValues, ['icon', 'label', 'description']),
objectMetadataId: activeObjectMetadataItem.id,
connect: {
field: {
@ -223,9 +236,7 @@ export const SettingsObjectNewFieldStep2 = () => {
currencyCode: validatedFormValues.currency.currencyCode,
}
: validatedFormValues.defaultValue,
description: validatedFormValues.description,
icon: validatedFormValues.icon,
label: validatedFormValues.label ?? '',
...formValues,
objectMetadataId: activeObjectMetadataItem.id,
type: validatedFormValues.type,
options:
@ -287,61 +298,65 @@ export const SettingsObjectNewFieldStep2 = () => {
];
return (
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
<SettingsPageContainer>
<SettingsHeaderContainer>
<Breadcrumb
links={[
{ children: 'Objects', href: '/settings/objects' },
{
children: activeObjectMetadataItem.labelPlural,
href: `/settings/objects/${objectSlug}`,
},
{ children: 'New Field' },
]}
/>
{!activeObjectMetadataItem.isRemote && (
<SaveAndCancelButtons
isSaveDisabled={!canSave}
onCancel={() => navigate(`/settings/objects/${objectSlug}`)}
onSave={handleSave}
// eslint-disable-next-line react/jsx-props-no-spreading
<FormProvider {...formConfig}>
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
<SettingsPageContainer>
<SettingsHeaderContainer>
<Breadcrumb
links={[
{ children: 'Objects', href: '/settings/objects' },
{
children: activeObjectMetadataItem.labelPlural,
href: `/settings/objects/${objectSlug}`,
},
{ children: 'New Field' },
]}
/>
)}
</SettingsHeaderContainer>
<SettingsObjectFieldFormSection
iconKey={formValues.icon}
name={formValues.label}
description={formValues.description}
onChange={handleFormChange}
/>
<Section>
<H2Title
title="Type and values"
description="The field's type and values."
/>
<StyledSettingsObjectFieldTypeSelect
excludedFieldTypes={excludedFieldTypes}
onChange={handleFormChange}
value={formValues.type}
/>
<SettingsDataModelFieldSettingsFormCard
fieldMetadataItem={{
icon: formValues.icon,
label: formValues.label || 'Employees',
type: formValues.type,
}}
objectMetadataItem={activeObjectMetadataItem}
onChange={handleFormChange}
values={{
currency: formValues.currency,
relation: formValues.relation,
select: formValues.select,
multiSelect: formValues.multiSelect,
defaultValue: formValues.defaultValue,
}}
/>
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
{!activeObjectMetadataItem.isRemote && (
<SaveAndCancelButtons
isSaveDisabled={!canSave}
onCancel={() => navigate(`/settings/objects/${objectSlug}`)}
onSave={handleSave}
/>
)}
</SettingsHeaderContainer>
<Section>
<H2Title
title="Name and description"
description="The name and description of this field"
/>
<SettingsDataModelFieldAboutForm />
</Section>
<Section>
<H2Title
title="Type and values"
description="The field's type and values."
/>
<StyledSettingsObjectFieldTypeSelect
excludedFieldTypes={excludedFieldTypes}
onChange={handleFormChange}
value={formValues.type}
/>
<SettingsDataModelFieldSettingsFormCard
fieldMetadataItem={{
icon: formConfig.watch('icon'),
label: formConfig.watch('label') || 'Employees',
type: formValues.type,
}}
objectMetadataItem={activeObjectMetadataItem}
onChange={handleFormChange}
values={{
currency: formValues.currency,
relation: formValues.relation,
select: formValues.select,
multiSelect: formValues.multiSelect,
defaultValue: formValues.defaultValue,
}}
/>
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
</FormProvider>
);
};