From d0759ad7cc10f341a75d926527f658ba18fa82b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tha=C3=AFs?= Date: Tue, 7 May 2024 11:44:46 +0200 Subject: [PATCH] refactor: use react-hook-form to validate Settings/DataModel/Field (#4916) Closes #4295 --- packages/twenty-front/project.json | 3 +- .../useCreateOneRelationMetadataItem.test.tsx | 4 +- .../utils/formatRelationMetadataInput.ts | 2 +- .../objectMetadataItemSchema.test.ts | 10 +- .../fieldMetadataItemSchema.ts | 102 +- .../validation-schemas/metadataLabelSchema.ts | 7 + .../objectMetadataItemSchema.ts | 5 +- .../SettingsObjectFieldFormSection.tsx | 70 - ...SettingsObjectFieldFormSection.stories.tsx | 23 - .../SettingsDataModelFieldAboutForm.tsx | 87 + ...ettingsDataModelFieldAboutForm.stories.tsx | 47 + .../__tests__/useFieldMetadataForm.test.ts | 6 +- .../forms/hooks/useFieldMetadataForm.ts | 8 - .../settingsFieldFormSchema.ts | 3 + .../data-model/SettingsObjectFieldEdit.tsx | 174 +- .../SettingsObjectNewFieldStep2.tsx | 147 +- .../standard-metadata-query-result.ts | 25898 ++++++++-------- .../src/testing/mock-data/metadata.ts | 7 - 18 files changed, 13234 insertions(+), 13369 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-metadata/validation-schemas/metadataLabelSchema.ts delete mode 100644 packages/twenty-front/src/modules/settings/data-model/components/SettingsObjectFieldFormSection.tsx delete mode 100644 packages/twenty-front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldFormSection.stories.tsx create mode 100644 packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldAboutForm.tsx create mode 100644 packages/twenty-front/src/modules/settings/data-model/fields/forms/components/__stories__/SettingsDataModelFieldAboutForm.stories.tsx create mode 100644 packages/twenty-front/src/modules/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema.ts diff --git a/packages/twenty-front/project.json b/packages/twenty-front/project.json index d36b197e6..bf86d4c8c 100644 --- a/packages/twenty-front/project.json +++ b/packages/twenty-front/project.json @@ -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" }, diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useCreateOneRelationMetadataItem.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useCreateOneRelationMetadataItem.test.tsx index c98def504..2d9b7f3e3 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useCreateOneRelationMetadataItem.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useCreateOneRelationMetadataItem.test.tsx @@ -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, @@ -46,7 +45,6 @@ describe('useCreateOneRelationMetadataItem', () => { relationType: RelationMetadataType.OneToOne, field: { label: 'label', - type: FieldMetadataType.Relation, }, objectMetadataId: 'objectMetadataId', connect: { diff --git a/packages/twenty-front/src/modules/object-metadata/utils/formatRelationMetadataInput.ts b/packages/twenty-front/src/modules/object-metadata/utils/formatRelationMetadataInput.ts index a33a21523..94de1d58a 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/formatRelationMetadataInput.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/formatRelationMetadataInput.ts @@ -9,7 +9,7 @@ import { formatFieldMetadataItemInput } from './formatFieldMetadataItemInput'; export type FormatRelationMetadataInputParams = { relationType: RelationType; - field: Pick; + field: Pick; objectMetadataId: string; connect: { field: Pick; diff --git a/packages/twenty-front/src/modules/object-metadata/validation-schemas/__tests__/objectMetadataItemSchema.test.ts b/packages/twenty-front/src/modules/object-metadata/validation-schemas/__tests__/objectMetadataItemSchema.test.ts index 95e1773e6..0e8d60c66 100644 --- a/packages/twenty-front/src/modules/object-metadata/validation-schemas/__tests__/objectMetadataItemSchema.test.ts +++ b/packages/twenty-front/src/modules/object-metadata/validation-schemas/__tests__/objectMetadataItemSchema.test.ts @@ -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'; @@ -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).data).toEqual( - validObjectMetadataItem, - ); + expect(result).toEqual(validObjectMetadataItem); }); it('fails for an invalid object metadata item', () => { diff --git a/packages/twenty-front/src/modules/object-metadata/validation-schemas/fieldMetadataItemSchema.ts b/packages/twenty-front/src/modules/object-metadata/validation-schemas/fieldMetadataItemSchema.ts index 0943a2e83..924beaac2 100644 --- a/packages/twenty-front/src/modules/object-metadata/validation-schemas/fieldMetadataItemSchema.ts +++ b/packages/twenty-front/src/modules/object-metadata/validation-schemas/fieldMetadataItemSchema.ts @@ -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 = 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; diff --git a/packages/twenty-front/src/modules/object-metadata/validation-schemas/metadataLabelSchema.ts b/packages/twenty-front/src/modules/object-metadata/validation-schemas/metadataLabelSchema.ts new file mode 100644 index 000000000..8f23b365f --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/validation-schemas/metadataLabelSchema.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; + +export const metadataLabelSchema = z + .string() + .trim() + .min(1) + .regex(/^[a-zA-Z][a-zA-Z0-9 ()]*$/); diff --git a/packages/twenty-front/src/modules/object-metadata/validation-schemas/objectMetadataItemSchema.ts b/packages/twenty-front/src/modules/object-metadata/validation-schemas/objectMetadataItemSchema.ts index bbd31e87c..231e66ace 100644 --- a/packages/twenty-front/src/modules/object-metadata/validation-schemas/objectMetadataItemSchema.ts +++ b/packages/twenty-front/src/modules/object-metadata/validation-schemas/objectMetadataItemSchema.ts @@ -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({ @@ -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(), diff --git a/packages/twenty-front/src/modules/settings/data-model/components/SettingsObjectFieldFormSection.tsx b/packages/twenty-front/src/modules/settings/data-model/components/SettingsObjectFieldFormSection.tsx deleted file mode 100644 index f452d06b3..000000000 --- a/packages/twenty-front/src/modules/settings/data-model/components/SettingsObjectFieldFormSection.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import styled from '@emotion/styled'; - -import { validateMetadataLabel } from '@/object-metadata/utils/validateMetadataLabel'; -import { H2Title } from '@/ui/display/typography/components/H2Title'; -import { IconPicker } from '@/ui/input/components/IconPicker'; -import { TextArea } from '@/ui/input/components/TextArea'; -import { TextInput } from '@/ui/input/components/TextInput'; -import { Section } from '@/ui/layout/section/components/Section'; - -type SettingsObjectFieldFormSectionProps = { - disabled?: boolean; - name?: string; - description?: string; - iconKey?: string; - onChange?: ( - formValues: Partial<{ - icon: string; - label: string; - description: string; - }>, - ) => void; -}; - -const StyledInputsContainer = styled.div` - display: flex; - gap: ${({ theme }) => theme.spacing(2)}; - margin-bottom: ${({ theme }) => theme.spacing(2)}; - width: 100%; -`; - -export const SettingsObjectFieldFormSection = ({ - disabled, - name = '', - description = '', - iconKey = 'IconUsers', - onChange, -}: SettingsObjectFieldFormSectionProps) => ( -
- - - onChange?.({ icon: value.iconKey })} - variant="primary" - /> - { - if (!value || validateMetadataLabel(value)) { - onChange?.({ label: value }); - } - }} - disabled={disabled} - fullWidth - /> - -