refactor: use react-hook-form for Field type config forms (#5326)
Closes #4295 Note: for the sake of an easier code review, I did not rename/move some files and added "todo" comments instead so Github is able to match those files with their previous version.
This commit is contained in:
@ -1,20 +1,25 @@
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import styled from '@emotion/styled';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { SettingsDataModelDefaultValueForm } from '@/settings/data-model/components/SettingsDataModelDefaultValue';
|
||||
import {
|
||||
SettingsDataModelFieldBooleanForm,
|
||||
settingsDataModelFieldBooleanFormSchema,
|
||||
} from '@/settings/data-model/components/SettingsDataModelDefaultValue';
|
||||
import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard';
|
||||
import {
|
||||
SettingsObjectFieldCurrencyForm,
|
||||
SettingsObjectFieldCurrencyFormValues,
|
||||
SettingsDataModelFieldCurrencyForm,
|
||||
settingsDataModelFieldCurrencyFormSchema,
|
||||
} from '@/settings/data-model/components/SettingsObjectFieldCurrencyForm';
|
||||
import {
|
||||
SettingsObjectFieldRelationForm,
|
||||
SettingsObjectFieldRelationFormValues,
|
||||
SettingsDataModelFieldRelationForm,
|
||||
settingsDataModelFieldRelationFormSchema,
|
||||
} from '@/settings/data-model/components/SettingsObjectFieldRelationForm';
|
||||
import {
|
||||
SettingsObjectFieldSelectForm,
|
||||
SettingsObjectFieldSelectFormValues,
|
||||
SettingsDataModelFieldSelectForm,
|
||||
settingsDataModelFieldSelectFormSchema,
|
||||
} from '@/settings/data-model/components/SettingsObjectFieldSelectForm';
|
||||
import { RELATION_TYPES } from '@/settings/data-model/constants/RelationTypes';
|
||||
import {
|
||||
@ -23,26 +28,44 @@ import {
|
||||
} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export type SettingsDataModelFieldSettingsFormValues = {
|
||||
currency: SettingsObjectFieldCurrencyFormValues;
|
||||
relation: SettingsObjectFieldRelationFormValues;
|
||||
select: SettingsObjectFieldSelectFormValues;
|
||||
multiSelect: SettingsObjectFieldSelectFormValues;
|
||||
defaultValue: any;
|
||||
};
|
||||
const booleanFieldFormSchema = z
|
||||
.object({ type: z.literal(FieldMetadataType.Boolean) })
|
||||
.merge(settingsDataModelFieldBooleanFormSchema);
|
||||
|
||||
const currencyFieldFormSchema = z
|
||||
.object({ type: z.literal(FieldMetadataType.Currency) })
|
||||
.merge(settingsDataModelFieldCurrencyFormSchema);
|
||||
|
||||
const relationFieldFormSchema = z
|
||||
.object({ type: z.literal(FieldMetadataType.Relation) })
|
||||
.merge(settingsDataModelFieldRelationFormSchema);
|
||||
|
||||
const selectFieldFormSchema = z
|
||||
.object({
|
||||
type: z.enum([FieldMetadataType.Select, FieldMetadataType.MultiSelect]),
|
||||
})
|
||||
.merge(settingsDataModelFieldSelectFormSchema);
|
||||
|
||||
export const settingsDataModelFieldSettingsFormSchema = z.discriminatedUnion(
|
||||
'type',
|
||||
[
|
||||
booleanFieldFormSchema,
|
||||
currencyFieldFormSchema,
|
||||
relationFieldFormSchema,
|
||||
selectFieldFormSchema,
|
||||
],
|
||||
);
|
||||
|
||||
type SettingsDataModelFieldSettingsFormValues = z.infer<
|
||||
typeof settingsDataModelFieldSettingsFormSchema
|
||||
>;
|
||||
|
||||
type SettingsDataModelFieldSettingsFormCardProps = {
|
||||
disableCurrencyForm?: boolean;
|
||||
onChange: (values: Partial<SettingsDataModelFieldSettingsFormValues>) => void;
|
||||
relationFieldMetadataItem?: Pick<
|
||||
FieldMetadataItem,
|
||||
'id' | 'isCustom' | 'name'
|
||||
>;
|
||||
values: SettingsDataModelFieldSettingsFormValues;
|
||||
} & Pick<
|
||||
SettingsDataModelFieldPreviewCardProps,
|
||||
'fieldMetadataItem' | 'objectMetadataItem'
|
||||
>;
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'icon' | 'label' | 'type'> &
|
||||
Partial<Omit<FieldMetadataItem, 'icon' | 'label' | 'type'>>;
|
||||
relationFieldMetadataItem?: FieldMetadataItem;
|
||||
} & Pick<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>;
|
||||
|
||||
const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
|
||||
display: grid;
|
||||
@ -81,18 +104,23 @@ export const SettingsDataModelFieldSettingsFormCard = ({
|
||||
disableCurrencyForm,
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
onChange,
|
||||
relationFieldMetadataItem,
|
||||
values,
|
||||
}: SettingsDataModelFieldSettingsFormCardProps) => {
|
||||
const { watch: watchFormValue } =
|
||||
useFormContext<SettingsDataModelFieldSettingsFormValues>();
|
||||
const { findObjectMetadataItemById } = useFilteredObjectMetadataItems();
|
||||
|
||||
if (!previewableTypes.includes(fieldMetadataItem.type)) return null;
|
||||
|
||||
const relationObjectMetadataItem = findObjectMetadataItemById(
|
||||
values.relation.objectMetadataId,
|
||||
);
|
||||
const relationTypeConfig = RELATION_TYPES[values.relation.type];
|
||||
const relationObjectMetadataId = watchFormValue('relation.objectMetadataId');
|
||||
const relationObjectMetadataItem = relationObjectMetadataId
|
||||
? findObjectMetadataItemById(relationObjectMetadataId)
|
||||
: undefined;
|
||||
|
||||
const relationType = watchFormValue('relation.type');
|
||||
const relationTypeConfig = relationType
|
||||
? RELATION_TYPES[relationType]
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<SettingsDataModelPreviewFormCard
|
||||
@ -103,14 +131,11 @@ export const SettingsDataModelFieldSettingsFormCard = ({
|
||||
shrink={fieldMetadataItem.type === FieldMetadataType.Relation}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
relationObjectMetadataItem={relationObjectMetadataItem}
|
||||
selectOptions={
|
||||
fieldMetadataItem.type === FieldMetadataType.MultiSelect
|
||||
? values.multiSelect
|
||||
: values.select
|
||||
}
|
||||
selectOptions={watchFormValue('options')}
|
||||
/>
|
||||
{fieldMetadataItem.type === FieldMetadataType.Relation &&
|
||||
!!relationObjectMetadataItem && (
|
||||
!!relationObjectMetadataItem &&
|
||||
!!relationTypeConfig && (
|
||||
<>
|
||||
<StyledRelationImage
|
||||
src={relationTypeConfig.imageSrc}
|
||||
@ -119,11 +144,11 @@ export const SettingsDataModelFieldSettingsFormCard = ({
|
||||
/>
|
||||
<StyledFieldPreviewCard
|
||||
fieldMetadataItem={{
|
||||
icon: values.relation.field.icon,
|
||||
label: values.relation.field.label || 'Field name',
|
||||
...relationFieldMetadataItem,
|
||||
icon: watchFormValue('relation.field.icon'),
|
||||
label:
|
||||
watchFormValue('relation.field.label') || 'Field name',
|
||||
type: FieldMetadataType.Relation,
|
||||
name: relationFieldMetadataItem?.name,
|
||||
id: relationFieldMetadataItem?.id,
|
||||
}}
|
||||
shrink
|
||||
objectMetadataItem={relationObjectMetadataItem}
|
||||
@ -134,49 +159,25 @@ export const SettingsDataModelFieldSettingsFormCard = ({
|
||||
</StyledPreviewContent>
|
||||
}
|
||||
form={
|
||||
fieldMetadataItem.type === FieldMetadataType.Currency ? (
|
||||
<SettingsObjectFieldCurrencyForm
|
||||
fieldMetadataItem.type === FieldMetadataType.Boolean ? (
|
||||
<SettingsDataModelFieldBooleanForm
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
/>
|
||||
) : fieldMetadataItem.type === FieldMetadataType.Currency ? (
|
||||
<SettingsDataModelFieldCurrencyForm
|
||||
disabled={disableCurrencyForm}
|
||||
values={values.currency}
|
||||
onChange={(nextCurrencyValues) =>
|
||||
onChange({
|
||||
currency: { ...values.currency, ...nextCurrencyValues },
|
||||
})
|
||||
}
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
/>
|
||||
) : fieldMetadataItem.type === FieldMetadataType.Relation ? (
|
||||
<SettingsObjectFieldRelationForm
|
||||
disableFieldEdition={
|
||||
relationFieldMetadataItem && !relationFieldMetadataItem.isCustom
|
||||
}
|
||||
disableRelationEdition={!!relationFieldMetadataItem}
|
||||
values={values.relation}
|
||||
onChange={(nextRelationValues) =>
|
||||
onChange({
|
||||
relation: { ...values.relation, ...nextRelationValues },
|
||||
})
|
||||
}
|
||||
<SettingsDataModelFieldRelationForm
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
/>
|
||||
) : fieldMetadataItem.type === FieldMetadataType.Select ? (
|
||||
<SettingsObjectFieldSelectForm
|
||||
values={values.select}
|
||||
onChange={(nextSelectValues) =>
|
||||
onChange({ select: nextSelectValues })
|
||||
}
|
||||
/>
|
||||
) : fieldMetadataItem.type === FieldMetadataType.MultiSelect ? (
|
||||
<SettingsObjectFieldSelectForm
|
||||
values={values.multiSelect}
|
||||
onChange={(nextMultiSelectValues) =>
|
||||
onChange({ multiSelect: nextMultiSelectValues })
|
||||
}
|
||||
isMultiSelect={true}
|
||||
/>
|
||||
) : fieldMetadataItem.type === FieldMetadataType.Boolean ? (
|
||||
<SettingsDataModelDefaultValueForm
|
||||
value={values.defaultValue}
|
||||
onChange={(nextValueDefaultValue) =>
|
||||
onChange({ defaultValue: nextValueDefaultValue })
|
||||
) : fieldMetadataItem.type === FieldMetadataType.Select ||
|
||||
fieldMetadataItem.type === FieldMetadataType.MultiSelect ? (
|
||||
<SettingsDataModelFieldSelectForm
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
isMultiSelect={
|
||||
fieldMetadataItem.type === FieldMetadataType.MultiSelect
|
||||
}
|
||||
/>
|
||||
) : undefined
|
||||
|
||||
@ -1,38 +1,47 @@
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import omit from 'lodash.omit';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import {
|
||||
SETTINGS_FIELD_TYPE_CONFIGS,
|
||||
SettingsFieldTypeConfig,
|
||||
} from '@/settings/data-model/constants/SettingsFieldTypeConfigs';
|
||||
import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType';
|
||||
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const settingsDataModelFieldTypeFormSchema = z.object({
|
||||
type: z.enum(
|
||||
Object.keys(SETTINGS_FIELD_TYPE_CONFIGS) as [
|
||||
SettingsSupportedFieldType,
|
||||
...SettingsSupportedFieldType[],
|
||||
],
|
||||
),
|
||||
});
|
||||
|
||||
type SettingsDataModelFieldTypeFormValues = z.infer<
|
||||
typeof settingsDataModelFieldTypeFormSchema
|
||||
>;
|
||||
|
||||
type SettingsDataModelFieldTypeSelectProps = {
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
excludedFieldTypes?: SettingsSupportedFieldType[];
|
||||
onChange?: ({
|
||||
type,
|
||||
defaultValue,
|
||||
}: {
|
||||
type: SettingsSupportedFieldType;
|
||||
defaultValue: any;
|
||||
}) => void;
|
||||
value?: SettingsSupportedFieldType;
|
||||
fieldMetadataItem?: FieldMetadataItem;
|
||||
};
|
||||
|
||||
export const SettingsDataModelFieldTypeSelect = ({
|
||||
className,
|
||||
disabled,
|
||||
excludedFieldTypes = [],
|
||||
onChange,
|
||||
value,
|
||||
fieldMetadataItem,
|
||||
}: SettingsDataModelFieldTypeSelectProps) => {
|
||||
const fieldTypeConfigs = omit(
|
||||
SETTINGS_FIELD_TYPE_CONFIGS,
|
||||
excludedFieldTypes,
|
||||
);
|
||||
const { control } = useFormContext<SettingsDataModelFieldTypeFormValues>();
|
||||
|
||||
const fieldTypeConfigs: Partial<
|
||||
Record<SettingsSupportedFieldType, SettingsFieldTypeConfig>
|
||||
> = omit(SETTINGS_FIELD_TYPE_CONFIGS, excludedFieldTypes);
|
||||
|
||||
const fieldTypeOptions = Object.entries<SettingsFieldTypeConfig>(
|
||||
fieldTypeConfigs,
|
||||
).map<SelectOption<SettingsSupportedFieldType>>(([key, dataTypeConfig]) => ({
|
||||
@ -42,19 +51,25 @@ export const SettingsDataModelFieldTypeSelect = ({
|
||||
}));
|
||||
|
||||
return (
|
||||
<Select
|
||||
className={className}
|
||||
fullWidth
|
||||
disabled={disabled}
|
||||
dropdownId="object-field-type-select"
|
||||
value={value}
|
||||
onChange={(value) =>
|
||||
onChange?.({
|
||||
type: value,
|
||||
defaultValue: value === FieldMetadataType.Boolean ? false : undefined,
|
||||
})
|
||||
<Controller
|
||||
name="type"
|
||||
control={control}
|
||||
defaultValue={
|
||||
fieldMetadataItem && fieldMetadataItem.type in fieldTypeConfigs
|
||||
? (fieldMetadataItem.type as SettingsSupportedFieldType)
|
||||
: undefined
|
||||
}
|
||||
options={fieldTypeOptions}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Select
|
||||
className={className}
|
||||
fullWidth
|
||||
disabled={disabled}
|
||||
dropdownId="object-field-type-select"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={fieldTypeOptions}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,12 +1,8 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { fn } from '@storybook/test';
|
||||
import { ComponentDecorator } from 'twenty-ui';
|
||||
|
||||
import { fieldMetadataFormDefaultValues } from '@/settings/data-model/fields/forms/hooks/useFieldMetadataForm';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
RelationMetadataType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { FormProviderDecorator } from '~/testing/decorators/FormProviderDecorator';
|
||||
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
||||
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||
@ -22,14 +18,6 @@ const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find(
|
||||
({ type }) => type === FieldMetadataType.Text,
|
||||
)!;
|
||||
|
||||
const defaultValues = {
|
||||
currency: fieldMetadataFormDefaultValues.currency,
|
||||
relation: fieldMetadataFormDefaultValues.relation,
|
||||
select: fieldMetadataFormDefaultValues.select,
|
||||
multiSelect: fieldMetadataFormDefaultValues.multiSelect,
|
||||
defaultValue: fieldMetadataFormDefaultValues.defaultValue,
|
||||
};
|
||||
|
||||
const meta: Meta<typeof SettingsDataModelFieldSettingsFormCard> = {
|
||||
title:
|
||||
'Modules/Settings/DataModel/Fields/Forms/SettingsDataModelFieldSettingsFormCard',
|
||||
@ -39,12 +27,11 @@ const meta: Meta<typeof SettingsDataModelFieldSettingsFormCard> = {
|
||||
ComponentDecorator,
|
||||
ObjectMetadataItemsDecorator,
|
||||
SnackBarDecorator,
|
||||
FormProviderDecorator,
|
||||
],
|
||||
args: {
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem: mockedCompanyObjectMetadataItem,
|
||||
onChange: fn(),
|
||||
values: defaultValues,
|
||||
},
|
||||
parameters: {
|
||||
container: { width: 512 },
|
||||
@ -57,24 +44,14 @@ type Story = StoryObj<typeof SettingsDataModelFieldSettingsFormCard>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
const relationFieldMetadataItem = mockedPersonObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'company',
|
||||
)!;
|
||||
|
||||
export const WithRelationForm: Story = {
|
||||
args: {
|
||||
fieldMetadataItem: mockedCompanyObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'people',
|
||||
),
|
||||
relationFieldMetadataItem,
|
||||
values: {
|
||||
...defaultValues,
|
||||
relation: {
|
||||
field: relationFieldMetadataItem,
|
||||
objectMetadataId: mockedPersonObjectMetadataItem.id,
|
||||
type: RelationMetadataType.OneToMany,
|
||||
},
|
||||
},
|
||||
relationFieldMetadataItem: mockedPersonObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'company',
|
||||
)!,
|
||||
},
|
||||
};
|
||||
|
||||
@ -85,34 +62,5 @@ export const WithSelectForm: Story = {
|
||||
icon: 'IconBuildingFactory2',
|
||||
type: FieldMetadataType.Select,
|
||||
},
|
||||
values: {
|
||||
...defaultValues,
|
||||
select: [
|
||||
{
|
||||
color: 'pink',
|
||||
isDefault: true,
|
||||
label: '💊 Health',
|
||||
value: 'HEALTH',
|
||||
},
|
||||
{
|
||||
color: 'purple',
|
||||
label: '🏭 Industry',
|
||||
value: 'INDUSTRY',
|
||||
},
|
||||
{ color: 'sky', label: '🤖 SaaS', value: 'SAAS' },
|
||||
{
|
||||
color: 'turquoise',
|
||||
label: '🌿 Green tech',
|
||||
value: 'GREEN_TECH',
|
||||
},
|
||||
{
|
||||
color: 'yellow',
|
||||
label: '🚲 Mobility',
|
||||
value: 'MOBILITY',
|
||||
},
|
||||
{ color: 'green', label: '🌏 NGO', value: 'NGO' },
|
||||
],
|
||||
defaultValue: undefined,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { expect, fn, userEvent, within } from '@storybook/test';
|
||||
import { expect, userEvent, within } from '@storybook/test';
|
||||
import { ComponentDecorator } from 'twenty-ui';
|
||||
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
@ -12,10 +12,6 @@ const meta: Meta<typeof SettingsDataModelFieldTypeSelect> = {
|
||||
'Modules/Settings/DataModel/Fields/Forms/SettingsDataModelFieldTypeSelect',
|
||||
component: SettingsDataModelFieldTypeSelect,
|
||||
decorators: [ComponentDecorator],
|
||||
args: {
|
||||
onChange: fn(),
|
||||
value: FieldMetadataType.Text,
|
||||
},
|
||||
parameters: {
|
||||
container: { width: 512 },
|
||||
msw: graphqlMocks,
|
||||
|
||||
@ -1,55 +0,0 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
|
||||
import { FieldMetadataType } from '~/generated/graphql';
|
||||
|
||||
import { useFieldMetadataForm } from '../useFieldMetadataForm';
|
||||
|
||||
describe('useFieldMetadataForm', () => {
|
||||
it('should initialize with default values', () => {
|
||||
const { result } = renderHook(() => useFieldMetadataForm());
|
||||
|
||||
expect(result.current.isInitialized).toBe(false);
|
||||
|
||||
act(() => {
|
||||
result.current.initForm({});
|
||||
});
|
||||
|
||||
expect(result.current.isInitialized).toBe(true);
|
||||
expect(result.current.formValues).toEqual({
|
||||
type: 'TEXT',
|
||||
currency: { currencyCode: 'USD' },
|
||||
relation: {
|
||||
type: 'ONE_TO_MANY',
|
||||
objectMetadataId: '',
|
||||
field: { label: '' },
|
||||
},
|
||||
defaultValue: null,
|
||||
select: [
|
||||
{ color: 'green', label: 'Option 1', value: expect.any(String) },
|
||||
],
|
||||
multiSelect: [
|
||||
{ color: 'green', label: 'Option 1', value: expect.any(String) },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle form changes', () => {
|
||||
const { result } = renderHook(() => useFieldMetadataForm());
|
||||
|
||||
act(() => {
|
||||
result.current.initForm({});
|
||||
});
|
||||
|
||||
expect(result.current.hasFieldFormChanged).toBe(false);
|
||||
expect(result.current.hasRelationFormChanged).toBe(false);
|
||||
expect(result.current.hasSelectFormChanged).toBe(false);
|
||||
|
||||
act(() => {
|
||||
result.current.handleFormChange({ type: FieldMetadataType.Number });
|
||||
});
|
||||
|
||||
expect(result.current.hasFieldFormChanged).toBe(true);
|
||||
expect(result.current.hasRelationFormChanged).toBe(false);
|
||||
expect(result.current.hasSelectFormChanged).toBe(false);
|
||||
});
|
||||
});
|
||||
@ -1,262 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { DeepPartial } from 'react-hook-form';
|
||||
import { v4 } from 'uuid';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode';
|
||||
import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType';
|
||||
import { themeColorSchema } from '@/ui/theme/utils/themeColorSchema';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
RelationMetadataType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
import { SettingsDataModelFieldSettingsFormValues } from '../components/SettingsDataModelFieldSettingsFormCard';
|
||||
|
||||
type FormValues = {
|
||||
defaultValue: any;
|
||||
type: SettingsSupportedFieldType;
|
||||
} & SettingsDataModelFieldSettingsFormValues;
|
||||
|
||||
export const fieldMetadataFormDefaultValues: FormValues = {
|
||||
type: FieldMetadataType.Text,
|
||||
currency: { currencyCode: CurrencyCode.USD },
|
||||
relation: {
|
||||
type: RelationMetadataType.OneToMany,
|
||||
objectMetadataId: '',
|
||||
field: { label: '' },
|
||||
},
|
||||
defaultValue: null,
|
||||
select: [{ color: 'green', label: 'Option 1', value: v4() }],
|
||||
multiSelect: [{ color: 'green', label: 'Option 1', value: v4() }],
|
||||
};
|
||||
const relationTargetFieldSchema = z.object({
|
||||
description: z.string().optional(),
|
||||
icon: z.string().startsWith('Icon'),
|
||||
label: z.string().min(1),
|
||||
defaultValue: z.any(),
|
||||
});
|
||||
const fieldSchema = z.object({
|
||||
defaultValue: z.any(),
|
||||
type: z.enum(
|
||||
Object.values(FieldMetadataType) as [
|
||||
FieldMetadataType,
|
||||
...FieldMetadataType[],
|
||||
],
|
||||
),
|
||||
});
|
||||
|
||||
const currencySchema = fieldSchema.merge(
|
||||
z.object({
|
||||
type: z.literal(FieldMetadataType.Currency),
|
||||
currency: z.object({
|
||||
currencyCode: z.nativeEnum(CurrencyCode),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const relationSchema = fieldSchema.merge(
|
||||
z.object({
|
||||
type: z.literal(FieldMetadataType.Relation),
|
||||
relation: z.object({
|
||||
field: relationTargetFieldSchema,
|
||||
objectMetadataId: z.string().uuid(),
|
||||
type: z.enum([
|
||||
RelationMetadataType.OneToMany,
|
||||
RelationMetadataType.OneToOne,
|
||||
'MANY_TO_ONE',
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const selectSchema = fieldSchema.merge(
|
||||
z.object({
|
||||
type: z.literal(FieldMetadataType.Select),
|
||||
select: z
|
||||
.array(
|
||||
z.object({
|
||||
color: themeColorSchema,
|
||||
id: z.string().optional(),
|
||||
isDefault: z.boolean().optional(),
|
||||
label: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.nonempty(),
|
||||
}),
|
||||
);
|
||||
|
||||
const multiSelectSchema = fieldSchema.merge(
|
||||
z.object({
|
||||
type: z.literal(FieldMetadataType.MultiSelect),
|
||||
multiSelect: z
|
||||
.array(
|
||||
z.object({
|
||||
color: themeColorSchema,
|
||||
id: z.string().optional(),
|
||||
isDefault: z.boolean().optional(),
|
||||
label: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.nonempty(),
|
||||
}),
|
||||
);
|
||||
|
||||
const {
|
||||
Currency: _Currency,
|
||||
Relation: _Relation,
|
||||
Select: _Select,
|
||||
MultiSelect: _MultiSelect,
|
||||
...otherFieldTypes
|
||||
} = FieldMetadataType;
|
||||
|
||||
type OtherFieldType = Exclude<
|
||||
FieldMetadataType,
|
||||
| FieldMetadataType.Currency
|
||||
| FieldMetadataType.Relation
|
||||
| FieldMetadataType.Select
|
||||
| FieldMetadataType.MultiSelect
|
||||
>;
|
||||
|
||||
const otherFieldTypesSchema = fieldSchema.merge(
|
||||
z.object({
|
||||
type: z.enum(
|
||||
Object.values(otherFieldTypes) as [OtherFieldType, ...OtherFieldType[]],
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
const schema = z.discriminatedUnion('type', [
|
||||
currencySchema,
|
||||
relationSchema,
|
||||
selectSchema,
|
||||
multiSelectSchema,
|
||||
otherFieldTypesSchema,
|
||||
]);
|
||||
|
||||
type PartialFormValues = Partial<Omit<FormValues, 'relation'>> &
|
||||
DeepPartial<Pick<FormValues, 'relation'>>;
|
||||
|
||||
export const useFieldMetadataForm = () => {
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const [initialFormValues, setInitialFormValues] = useState<FormValues>(
|
||||
fieldMetadataFormDefaultValues,
|
||||
);
|
||||
const [formValues, setFormValues] = useState<FormValues>(
|
||||
fieldMetadataFormDefaultValues,
|
||||
);
|
||||
const [hasFieldFormChanged, setHasFieldFormChanged] = useState(false);
|
||||
const [hasCurrencyFormChanged, setHasCurrencyFormChanged] = useState(false);
|
||||
const [hasRelationFormChanged, setHasRelationFormChanged] = useState(false);
|
||||
const [hasSelectFormChanged, setHasSelectFormChanged] = useState(false);
|
||||
const [hasMultiSelectFormChanged, setHasMultiSelectFormChanged] =
|
||||
useState(false);
|
||||
const [hasDefaultValueChanged, setHasDefaultValueFormChanged] =
|
||||
useState(false);
|
||||
const [validationResult, setValidationResult] = useState(
|
||||
schema.safeParse(formValues),
|
||||
);
|
||||
|
||||
const mergePartialValues = (
|
||||
previousValues: FormValues,
|
||||
nextValues: PartialFormValues,
|
||||
): FormValues => ({
|
||||
...previousValues,
|
||||
...nextValues,
|
||||
currency: { ...previousValues.currency, ...nextValues.currency },
|
||||
relation: {
|
||||
...previousValues.relation,
|
||||
...nextValues.relation,
|
||||
field: {
|
||||
...previousValues.relation?.field,
|
||||
...nextValues.relation?.field,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const initForm = (lazyInitialFormValues: PartialFormValues) => {
|
||||
if (isInitialized) return;
|
||||
|
||||
const mergedFormValues = mergePartialValues(
|
||||
initialFormValues,
|
||||
lazyInitialFormValues,
|
||||
);
|
||||
|
||||
setInitialFormValues(mergedFormValues);
|
||||
setFormValues(mergedFormValues);
|
||||
setValidationResult(schema.safeParse(mergedFormValues));
|
||||
setIsInitialized(true);
|
||||
};
|
||||
|
||||
const handleFormChange = (values: PartialFormValues) => {
|
||||
const nextFormValues = mergePartialValues(formValues, values);
|
||||
|
||||
setFormValues(nextFormValues);
|
||||
setValidationResult(schema.safeParse(nextFormValues));
|
||||
|
||||
const {
|
||||
currency: initialCurrencyFormValues,
|
||||
relation: initialRelationFormValues,
|
||||
select: initialSelectFormValues,
|
||||
multiSelect: initialMultiSelectFormValues,
|
||||
defaultValue: initialDefaultValue,
|
||||
...initialFieldFormValues
|
||||
} = initialFormValues;
|
||||
const {
|
||||
currency: nextCurrencyFormValues,
|
||||
relation: nextRelationFormValues,
|
||||
select: nextSelectFormValues,
|
||||
multiSelect: nextMultiSelectFormValues,
|
||||
defaultValue: nextDefaultValue,
|
||||
...nextFieldFormValues
|
||||
} = nextFormValues;
|
||||
|
||||
setHasFieldFormChanged(
|
||||
!isDeeplyEqual(initialFieldFormValues, nextFieldFormValues),
|
||||
);
|
||||
setHasCurrencyFormChanged(
|
||||
nextFieldFormValues.type === FieldMetadataType.Currency &&
|
||||
!isDeeplyEqual(initialCurrencyFormValues, nextCurrencyFormValues),
|
||||
);
|
||||
setHasRelationFormChanged(
|
||||
nextFieldFormValues.type === FieldMetadataType.Relation &&
|
||||
!isDeeplyEqual(initialRelationFormValues, nextRelationFormValues),
|
||||
);
|
||||
setHasSelectFormChanged(
|
||||
nextFieldFormValues.type === FieldMetadataType.Select &&
|
||||
!isDeeplyEqual(initialSelectFormValues, nextSelectFormValues),
|
||||
);
|
||||
setHasMultiSelectFormChanged(
|
||||
nextFieldFormValues.type === FieldMetadataType.MultiSelect &&
|
||||
!isDeeplyEqual(initialMultiSelectFormValues, nextMultiSelectFormValues),
|
||||
);
|
||||
setHasDefaultValueFormChanged(
|
||||
nextFieldFormValues.type === FieldMetadataType.Boolean &&
|
||||
!isDeeplyEqual(initialDefaultValue, nextDefaultValue),
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
formValues,
|
||||
handleFormChange,
|
||||
hasFieldFormChanged,
|
||||
hasFormChanged:
|
||||
hasFieldFormChanged ||
|
||||
hasCurrencyFormChanged ||
|
||||
hasRelationFormChanged ||
|
||||
hasSelectFormChanged ||
|
||||
hasMultiSelectFormChanged ||
|
||||
hasDefaultValueChanged,
|
||||
hasRelationFormChanged,
|
||||
hasSelectFormChanged,
|
||||
hasMultiSelectFormChanged,
|
||||
hasDefaultValueChanged,
|
||||
initForm,
|
||||
isInitialized,
|
||||
isValid: validationResult.success,
|
||||
validatedFormValues: validationResult.success
|
||||
? validationResult.data
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
@ -1,3 +1,11 @@
|
||||
import { settingsDataModelFieldAboutFormSchema } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldAboutForm';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const settingsFieldFormSchema = settingsDataModelFieldAboutFormSchema;
|
||||
import { settingsDataModelFieldAboutFormSchema } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldAboutForm';
|
||||
import { settingsDataModelFieldSettingsFormSchema } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard';
|
||||
import { settingsDataModelFieldTypeFormSchema } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldTypeSelect';
|
||||
|
||||
export const settingsFieldFormSchema = z
|
||||
.object({})
|
||||
.merge(settingsDataModelFieldAboutFormSchema)
|
||||
.merge(settingsDataModelFieldTypeFormSchema)
|
||||
.and(settingsDataModelFieldSettingsFormSchema);
|
||||
|
||||
@ -8,7 +8,7 @@ import { FieldDisplay } from '@/object-record/record-field/components/FieldDispl
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { BooleanFieldInput } from '@/object-record/record-field/meta-types/input/components/BooleanFieldInput';
|
||||
import { RatingFieldInput } from '@/object-record/record-field/meta-types/input/components/RatingFieldInput';
|
||||
import { SettingsObjectFieldSelectFormValues } from '@/settings/data-model/components/SettingsObjectFieldSelectForm';
|
||||
import { SettingsDataModelFieldSelectFormValues } from '@/settings/data-model/components/SettingsObjectFieldSelectForm';
|
||||
import { SettingsDataModelSetFieldValueEffect } from '@/settings/data-model/fields/preview/components/SettingsDataModelSetFieldValueEffect';
|
||||
import { SettingsDataModelSetRecordEffect } from '@/settings/data-model/fields/preview/components/SettingsDataModelSetRecordEffect';
|
||||
import { useFieldPreview } from '@/settings/data-model/fields/preview/hooks/useFieldPreview';
|
||||
@ -24,7 +24,7 @@ export type SettingsDataModelFieldPreviewProps = {
|
||||
};
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
relationObjectMetadataItem?: ObjectMetadataItem;
|
||||
selectOptions?: SettingsObjectFieldSelectFormValues;
|
||||
selectOptions?: SettingsDataModelFieldSelectFormValues['options'];
|
||||
shrink?: boolean;
|
||||
withFieldLabel?: boolean;
|
||||
};
|
||||
|
||||
@ -4,7 +4,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { isFieldValueEmpty } from '@/object-record/record-field/utils/isFieldValueEmpty';
|
||||
import { SettingsObjectFieldSelectFormValues } from '@/settings/data-model/components/SettingsObjectFieldSelectForm';
|
||||
import { SettingsDataModelFieldSelectFormValues } from '@/settings/data-model/components/SettingsObjectFieldSelectForm';
|
||||
import { getFieldDefaultPreviewValue } from '@/settings/data-model/utils/getFieldDefaultPreviewValue';
|
||||
import { getFieldPreviewValueFromRecord } from '@/settings/data-model/utils/getFieldPreviewValueFromRecord';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
@ -16,7 +16,7 @@ type UseFieldPreviewParams = {
|
||||
};
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
relationObjectMetadataItem?: ObjectMetadataItem;
|
||||
selectOptions?: SettingsObjectFieldSelectFormValues;
|
||||
selectOptions?: SettingsDataModelFieldSelectFormValues['options'];
|
||||
};
|
||||
|
||||
export const useFieldPreview = ({
|
||||
|
||||
Reference in New Issue
Block a user