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 - /> - -