Skip to content

Commit

Permalink
refactor: use react-hook-form to validate Settings/DataModel/Field (#…
Browse files Browse the repository at this point in the history
…4916)

Closes #4295
  • Loading branch information
thaisguigon committed May 7, 2024
1 parent 9c25c1b commit d0759ad
Show file tree
Hide file tree
Showing 18 changed files with 13,185 additions and 13,320 deletions.
3 changes: 2 additions & 1 deletion packages/twenty-front/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@
"commands": [
"npx concurrently --kill-others --success=first -n SB,TEST 'nx storybook:static {projectName} --port={args.port}' 'npx wait-on tcp:{args.port} && nx storybook:test {projectName} --port={args.port} --configuration={args.scope}'"
],
"port": 6006
"port": 6006,
"env": { "NODE_OPTIONS": "--max-old-space-size=5000" }
},
"configurations": {
"docs": { "scope": "ui-docs" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import { act, renderHook } from '@testing-library/react';
import { RecoilRoot } from 'recoil';

import { useCreateOneRelationMetadataItem } from '@/object-metadata/hooks/useCreateOneRelationMetadataItem';
import { FieldMetadataType } from '~/generated/graphql';
import { RelationMetadataType } from '~/generated-metadata/graphql';
import { RelationMetadataType } from '~/generated/graphql';

import {
query,
Expand Down Expand Up @@ -46,7 +45,6 @@ describe('useCreateOneRelationMetadataItem', () => {
relationType: RelationMetadataType.OneToOne,
field: {
label: 'label',
type: FieldMetadataType.Relation,
},
objectMetadataId: 'objectMetadataId',
connect: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { formatFieldMetadataItemInput } from './formatFieldMetadataItemInput';

export type FormatRelationMetadataInputParams = {
relationType: RelationType;
field: Pick<Field, 'label' | 'icon' | 'description' | 'type'>;
field: Pick<Field, 'label' | 'icon' | 'description'>;
objectMetadataId: string;
connect: {
field: Pick<Field, 'label' | 'icon'>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import { SafeParseSuccess } from 'zod';

import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { mockedCompanyObjectMetadataItem } from '~/testing/mock-data/metadata';

import { objectMetadataItemSchema } from '../objectMetadataItemSchema';
Expand All @@ -11,13 +8,10 @@ describe('objectMetadataItemSchema', () => {
const validObjectMetadataItem = mockedCompanyObjectMetadataItem;

// When
const result = objectMetadataItemSchema.safeParse(validObjectMetadataItem);
const result = objectMetadataItemSchema.parse(validObjectMetadataItem);

// Then
expect(result.success).toBe(true);
expect((result as SafeParseSuccess<ObjectMetadataItem>).data).toEqual(
validObjectMetadataItem,
);
expect(result).toEqual(validObjectMetadataItem);
});

it('fails for an invalid object metadata item', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,104 @@
import { z } from 'zod';

import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { metadataLabelSchema } from '@/object-metadata/validation-schemas/metadataLabelSchema';
import { themeColorSchema } from '@/ui/theme/utils/themeColorSchema';
import {
FieldMetadataType,
RelationDefinitionType,
RelationMetadataType,
} from '~/generated-metadata/graphql';
import { camelCaseStringSchema } from '~/utils/validation-schemas/camelCaseStringSchema';

// TODO: implement fieldMetadataItemSchema
export const fieldMetadataItemSchema: z.ZodType<FieldMetadataItem> = z.any();
export const fieldMetadataItemSchema = z.object({
__typename: z.literal('field').optional(),
createdAt: z.string().datetime(),
defaultValue: z.any().optional(),
description: z.string().trim().nullable().optional(),
fromRelationMetadata: z
.object({
__typename: z.literal('relation').optional(),
id: z.string().uuid(),
relationType: z.nativeEnum(RelationMetadataType),
toFieldMetadataId: z.string().uuid(),
toObjectMetadata: z.object({
__typename: z.literal('object').optional(),
dataSourceId: z.string().uuid(),
id: z.string().uuid(),
isRemote: z.boolean(),
isSystem: z.boolean(),
namePlural: z.string().trim().min(1),
nameSingular: z.string().trim().min(1),
}),
})
.nullable()
.optional(),
icon: z.string().startsWith('Icon').trim().nullable(),
id: z.string().uuid(),
isActive: z.boolean(),
isCustom: z.boolean(),
isNullable: z.boolean(),
isSystem: z.boolean(),
label: metadataLabelSchema,
name: camelCaseStringSchema,
options: z
.array(
z.object({
color: themeColorSchema,
id: z.string().uuid(),
label: z.string().trim().min(1),
position: z.number(),
value: z.string().trim().min(1),
}),
)
.optional(),
relationDefinition: z
.object({
__typename: z.literal('RelationDefinition').optional(),
direction: z.nativeEnum(RelationDefinitionType),
sourceFieldMetadata: z.object({
__typename: z.literal('field').optional(),
id: z.string().uuid(),
name: z.string().trim().min(1),
}),
sourceObjectMetadata: z.object({
__typename: z.literal('object').optional(),
id: z.string().uuid(),
namePlural: z.string().trim().min(1),
nameSingular: z.string().trim().min(1),
}),
targetFieldMetadata: z.object({
__typename: z.literal('field').optional(),
id: z.string().uuid(),
name: z.string().trim().min(1),
}),
targetObjectMetadata: z.object({
__typename: z.literal('object').optional(),
id: z.string().uuid(),
namePlural: z.string().trim().min(1),
nameSingular: z.string().trim().min(1),
}),
})
.nullable()
.optional(),
toRelationMetadata: z
.object({
__typename: z.literal('relation').optional(),
id: z.string().uuid(),
relationType: z.nativeEnum(RelationMetadataType),
fromFieldMetadataId: z.string().uuid(),
fromObjectMetadata: z.object({
__typename: z.literal('object').optional(),
id: z.string().uuid(),
dataSourceId: z.string().uuid(),
isRemote: z.boolean(),
isSystem: z.boolean(),
namePlural: z.string().trim().min(1),
nameSingular: z.string().trim().min(1),
}),
})
.nullable()
.optional(),
type: z.nativeEnum(FieldMetadataType),
updatedAt: z.string().datetime(),
}) satisfies z.ZodType<FieldMetadataItem>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { z } from 'zod';

export const metadataLabelSchema = z
.string()
.trim()
.min(1)
.regex(/^[a-zA-Z][a-zA-Z0-9 ()]*$/);
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { z } from 'zod';

import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { fieldMetadataItemSchema } from '@/object-metadata/validation-schemas/fieldMetadataItemSchema';
import { metadataLabelSchema } from '@/object-metadata/validation-schemas/metadataLabelSchema';
import { camelCaseStringSchema } from '~/utils/validation-schemas/camelCaseStringSchema';

export const objectMetadataItemSchema = z.object({
Expand All @@ -18,8 +19,8 @@ export const objectMetadataItemSchema = z.object({
isRemote: z.boolean(),
isSystem: z.boolean(),
labelIdentifierFieldMetadataId: z.string().uuid().nullable(),
labelPlural: z.string().trim().min(1),
labelSingular: z.string().trim().min(1),
labelPlural: metadataLabelSchema,
labelSingular: metadataLabelSchema,
namePlural: camelCaseStringSchema,
nameSingular: camelCaseStringSchema,
updatedAt: z.string().datetime(),
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Controller, useFormContext } from 'react-hook-form';
import styled from '@emotion/styled';
import { z } from 'zod';

import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { fieldMetadataItemSchema } from '@/object-metadata/validation-schemas/fieldMetadataItemSchema';
import { IconPicker } from '@/ui/input/components/IconPicker';
import { TextArea } from '@/ui/input/components/TextArea';
import { TextInput } from '@/ui/input/components/TextInput';

export const settingsDataModelFieldAboutFormSchema =
fieldMetadataItemSchema.pick({
description: true,
icon: true,
label: true,
});

type SettingsDataModelFieldAboutFormValues = z.infer<
typeof settingsDataModelFieldAboutFormSchema
>;

type SettingsDataModelFieldAboutFormProps = {
disabled?: boolean;
fieldMetadataItem?: FieldMetadataItem;
};

const StyledInputsContainer = styled.div`
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
margin-bottom: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;

export const SettingsDataModelFieldAboutForm = ({
disabled,
fieldMetadataItem,
}: SettingsDataModelFieldAboutFormProps) => {
const { control } = useFormContext<SettingsDataModelFieldAboutFormValues>();

return (
<>
<StyledInputsContainer>
<Controller
name="icon"
control={control}
defaultValue={fieldMetadataItem?.icon ?? 'IconUsers'}
render={({ field: { onChange, value } }) => (
<IconPicker
disabled={disabled}
selectedIconKey={value ?? ''}
onChange={({ iconKey }) => onChange(iconKey)}
variant="primary"
/>
)}
/>
<Controller
name="label"
control={control}
defaultValue={fieldMetadataItem?.label}
render={({ field: { onChange, value } }) => (
<TextInput
placeholder="Employees"
value={value}
onChange={onChange}
disabled={disabled}
fullWidth
/>
)}
/>
</StyledInputsContainer>
<Controller
name="description"
control={control}
defaultValue={fieldMetadataItem?.description}
render={({ field: { onChange, value } }) => (
<TextArea
placeholder="Write a description"
minRows={4}
value={value ?? undefined}
onChange={onChange}
disabled={disabled}
/>
)}
/>
</>
);
};

0 comments on commit d0759ad

Please sign in to comment.