From 2e9624858c070c8d94165f506c1275003dfcc5d1 Mon Sep 17 00:00:00 2001 From: martmull Date: Tue, 3 Mar 2026 11:40:17 +0100 Subject: [PATCH] Fix name singular updates in dev mode (#18339) as title --- .../app-dev/expected-manifest.ts | 40 +++++++++---------- .../app-dev/expected-manifest.ts | 10 ++--- ...et-default-fields-in-object-fields.spec.ts | 12 ++++++ .../get-default-relation-object-fields.ts | 3 +- ...manifest-update-object.integration-spec.ts | 15 +++++-- .../build-default-object-manifest.util.ts | 4 +- 6 files changed, 53 insertions(+), 31 deletions(-) diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/expected-manifest.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/expected-manifest.ts index 4aa293b6b3..fb3db6466a 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/expected-manifest.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/expected-manifest.ts @@ -709,7 +709,7 @@ export const EXPECTED_MANIFEST: Manifest = { { description: 'Root notes tied to the RootNote', icon: 'IconBuildingSkyscraper', - isNullable: false, + isNullable: true, label: 'Timeline Activities', name: 'timelineActivities', relationTargetFieldMetadataUniversalIdentifier: @@ -725,7 +725,7 @@ export const EXPECTED_MANIFEST: Manifest = { { description: 'Root notes tied to the RootNote', icon: 'IconBuildingSkyscraper', - isNullable: false, + isNullable: true, label: 'Favorites', name: 'favorites', relationTargetFieldMetadataUniversalIdentifier: @@ -741,7 +741,7 @@ export const EXPECTED_MANIFEST: Manifest = { { description: 'Root notes tied to the RootNote', icon: 'IconBuildingSkyscraper', - isNullable: false, + isNullable: true, label: 'Attachments', name: 'attachments', relationTargetFieldMetadataUniversalIdentifier: @@ -757,7 +757,7 @@ export const EXPECTED_MANIFEST: Manifest = { { description: 'Root notes tied to the RootNote', icon: 'IconBuildingSkyscraper', - isNullable: false, + isNullable: true, label: 'Note Targets', name: 'noteTargets', relationTargetFieldMetadataUniversalIdentifier: @@ -773,7 +773,7 @@ export const EXPECTED_MANIFEST: Manifest = { { description: 'Root notes tied to the RootNote', icon: 'IconBuildingSkyscraper', - isNullable: false, + isNullable: true, label: 'Task Targets', name: 'taskTargets', relationTargetFieldMetadataUniversalIdentifier: @@ -907,7 +907,7 @@ export const EXPECTED_MANIFEST: Manifest = { { description: 'Post Card Recipients tied to the PostCardRecipient', icon: 'IconBuildingSkyscraper', - isNullable: false, + isNullable: true, label: 'Timeline Activities', name: 'timelineActivities', relationTargetFieldMetadataUniversalIdentifier: @@ -923,7 +923,7 @@ export const EXPECTED_MANIFEST: Manifest = { { description: 'Post Card Recipients tied to the PostCardRecipient', icon: 'IconBuildingSkyscraper', - isNullable: false, + isNullable: true, label: 'Favorites', name: 'favorites', relationTargetFieldMetadataUniversalIdentifier: @@ -939,7 +939,7 @@ export const EXPECTED_MANIFEST: Manifest = { { description: 'Post Card Recipients tied to the PostCardRecipient', icon: 'IconBuildingSkyscraper', - isNullable: false, + isNullable: true, label: 'Attachments', name: 'attachments', relationTargetFieldMetadataUniversalIdentifier: @@ -955,7 +955,7 @@ export const EXPECTED_MANIFEST: Manifest = { { description: 'Post Card Recipients tied to the PostCardRecipient', icon: 'IconBuildingSkyscraper', - isNullable: false, + isNullable: true, label: 'Note Targets', name: 'noteTargets', relationTargetFieldMetadataUniversalIdentifier: @@ -971,7 +971,7 @@ export const EXPECTED_MANIFEST: Manifest = { { description: 'Post Card Recipients tied to the PostCardRecipient', icon: 'IconBuildingSkyscraper', - isNullable: false, + isNullable: true, label: 'Task Targets', name: 'taskTargets', relationTargetFieldMetadataUniversalIdentifier: @@ -1165,7 +1165,7 @@ export const EXPECTED_MANIFEST: Manifest = { { description: 'Post cards tied to the PostCard', icon: 'IconBuildingSkyscraper', - isNullable: false, + isNullable: true, label: 'Timeline Activities', name: 'timelineActivities', relationTargetFieldMetadataUniversalIdentifier: @@ -1181,7 +1181,7 @@ export const EXPECTED_MANIFEST: Manifest = { { description: 'Post cards tied to the PostCard', icon: 'IconBuildingSkyscraper', - isNullable: false, + isNullable: true, label: 'Favorites', name: 'favorites', relationTargetFieldMetadataUniversalIdentifier: @@ -1197,7 +1197,7 @@ export const EXPECTED_MANIFEST: Manifest = { { description: 'Post cards tied to the PostCard', icon: 'IconBuildingSkyscraper', - isNullable: false, + isNullable: true, label: 'Attachments', name: 'attachments', relationTargetFieldMetadataUniversalIdentifier: @@ -1213,7 +1213,7 @@ export const EXPECTED_MANIFEST: Manifest = { { description: 'Post cards tied to the PostCard', icon: 'IconBuildingSkyscraper', - isNullable: false, + isNullable: true, label: 'Note Targets', name: 'noteTargets', relationTargetFieldMetadataUniversalIdentifier: @@ -1229,7 +1229,7 @@ export const EXPECTED_MANIFEST: Manifest = { { description: 'Post cards tied to the PostCard', icon: 'IconBuildingSkyscraper', - isNullable: false, + isNullable: true, label: 'Task Targets', name: 'taskTargets', relationTargetFieldMetadataUniversalIdentifier: @@ -1368,7 +1368,7 @@ export const EXPECTED_MANIFEST: Manifest = { { description: 'Recipients tied to the Recipient', icon: 'IconBuildingSkyscraper', - isNullable: false, + isNullable: true, label: 'Timeline Activities', name: 'timelineActivities', relationTargetFieldMetadataUniversalIdentifier: @@ -1384,7 +1384,7 @@ export const EXPECTED_MANIFEST: Manifest = { { description: 'Recipients tied to the Recipient', icon: 'IconBuildingSkyscraper', - isNullable: false, + isNullable: true, label: 'Favorites', name: 'favorites', relationTargetFieldMetadataUniversalIdentifier: @@ -1400,7 +1400,7 @@ export const EXPECTED_MANIFEST: Manifest = { { description: 'Recipients tied to the Recipient', icon: 'IconBuildingSkyscraper', - isNullable: false, + isNullable: true, label: 'Attachments', name: 'attachments', relationTargetFieldMetadataUniversalIdentifier: @@ -1416,7 +1416,7 @@ export const EXPECTED_MANIFEST: Manifest = { { description: 'Recipients tied to the Recipient', icon: 'IconBuildingSkyscraper', - isNullable: false, + isNullable: true, label: 'Note Targets', name: 'noteTargets', relationTargetFieldMetadataUniversalIdentifier: @@ -1432,7 +1432,7 @@ export const EXPECTED_MANIFEST: Manifest = { { description: 'Recipients tied to the Recipient', icon: 'IconBuildingSkyscraper', - isNullable: false, + isNullable: true, label: 'Task Targets', name: 'taskTargets', relationTargetFieldMetadataUniversalIdentifier: diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/expected-manifest.ts b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/expected-manifest.ts index aa9efd3245..8734575fc2 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/expected-manifest.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/root-app/__integration__/app-dev/expected-manifest.ts @@ -239,7 +239,7 @@ export const EXPECTED_MANIFEST: Manifest = { label: 'Timeline Activities', description: 'My notes tied to the MyNote', icon: 'IconBuildingSkyscraper', - isNullable: false, + isNullable: true, type: FieldType.RELATION, universalSettings: { relationType: RelationType.ONE_TO_MANY, @@ -255,7 +255,7 @@ export const EXPECTED_MANIFEST: Manifest = { label: 'Favorites', description: 'My notes tied to the MyNote', icon: 'IconBuildingSkyscraper', - isNullable: false, + isNullable: true, type: FieldType.RELATION, universalSettings: { relationType: RelationType.ONE_TO_MANY, @@ -271,7 +271,7 @@ export const EXPECTED_MANIFEST: Manifest = { label: 'Attachments', description: 'My notes tied to the MyNote', icon: 'IconBuildingSkyscraper', - isNullable: false, + isNullable: true, type: FieldType.RELATION, universalSettings: { relationType: RelationType.ONE_TO_MANY, @@ -287,7 +287,7 @@ export const EXPECTED_MANIFEST: Manifest = { label: 'Note Targets', description: 'My notes tied to the MyNote', icon: 'IconBuildingSkyscraper', - isNullable: false, + isNullable: true, type: FieldType.RELATION, universalSettings: { relationType: RelationType.ONE_TO_MANY, @@ -303,7 +303,7 @@ export const EXPECTED_MANIFEST: Manifest = { label: 'Task Targets', description: 'My notes tied to the MyNote', icon: 'IconBuildingSkyscraper', - isNullable: false, + isNullable: true, type: FieldType.RELATION, universalSettings: { relationType: RelationType.ONE_TO_MANY, diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/__tests__/get-default-fields-in-object-fields.spec.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/__tests__/get-default-fields-in-object-fields.spec.ts index 614eed0baa..5c5d66def3 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/__tests__/get-default-fields-in-object-fields.spec.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/__tests__/get-default-fields-in-object-fields.spec.ts @@ -29,6 +29,18 @@ describe('getDefaultFieldsInObjectFields', () => { ...defaultRelationObjectFields, ]); expect(fields).toEqual(expectedReverseFields); + + for (const field of fields) { + if (field.type === FieldMetadataType.RELATION) { + expect(field.isNullable).toBe(true); + } + } + + for (const objectField of objectFields) { + if (objectField.type === FieldMetadataType.RELATION) { + expect(objectField.isNullable).toBe(true); + } + } }); it('should preserve custom fields and append missing default fields', () => { diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/get-default-relation-object-fields.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/get-default-relation-object-fields.ts index 6bd5fa662e..8ee7627482 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/get-default-relation-object-fields.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/utils/get-default-relation-object-fields.ts @@ -28,6 +28,7 @@ const DEFAULT_DEFAULT_RELATION = { targetLabel: (objectConfig: ObjectConfig) => capitalize(objectConfig.nameSingular), icon: 'IconBuildingSkyscraper', + isNullable: true, }; const DEFAULT_RELATION_CONFIGS = [ @@ -153,7 +154,7 @@ export const getDefaultRelationObjectFields = ( label: config.label, description: `${objectConfig.labelPlural} tied to the ${config.targetLabel(objectConfig)}`, icon: config.icon, - isNullable: false, + isNullable: config.isNullable, type: FieldMetadataType.RELATION, universalSettings: { relationType: RelationType.ONE_TO_MANY }, universalIdentifier: forwardFieldUniversalIdentifier, diff --git a/packages/twenty-server/test/integration/metadata/suites/application/successful-manifest-update-object.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/application/successful-manifest-update-object.integration-spec.ts index 3e2ca156b8..11381ef4d7 100644 --- a/packages/twenty-server/test/integration/metadata/suites/application/successful-manifest-update-object.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/application/successful-manifest-update-object.integration-spec.ts @@ -15,7 +15,7 @@ const buildManifest = ( ) => buildBaseManifest({ appId: TEST_APP_ID, roleId: TEST_ROLE_ID, overrides }); const OBJECT_GQL_FIELDS = - 'id nameSingular namePlural labelSingular labelPlural description icon isCustom isActive'; + 'id nameSingular namePlural labelSingular labelPlural description icon isCustom isActive universalIdentifier'; const findCustomObjects = async () => { const { objects } = await findManyObjectMetadata({ @@ -113,6 +113,7 @@ describe('Manifest update - objects', () => { }, 60000); it('should update object properties when changed in manifest on second sync', async () => { + const universalIdentifier = uuidv4(); const ticketObject = buildDefaultObjectManifest({ nameSingular: 'ticket', namePlural: 'tickets', @@ -120,6 +121,7 @@ describe('Manifest update - objects', () => { labelPlural: 'Tickets', description: 'A support ticket', icon: 'IconTicket', + universalIdentifier, }); await syncApplication({ @@ -129,10 +131,12 @@ describe('Manifest update - objects', () => { const objectsAfterFirstSync = await findCustomObjects(); const ticketBefore = objectsAfterFirstSync.find( - (obj) => obj.nameSingular === 'ticket', + (obj) => obj.universalIdentifier === universalIdentifier, ); + expect(ticketBefore).toBeDefined(); expect(ticketBefore).toMatchObject({ + nameSingular: 'ticket', labelSingular: 'Ticket', description: 'A support ticket', icon: 'IconTicket', @@ -140,6 +144,8 @@ describe('Manifest update - objects', () => { const updatedTicketObject = { ...ticketObject, + nameSingular: 'ticket2', + namePlural: 'tickets2', labelSingular: 'Support Ticket', labelPlural: 'Support Tickets', description: 'A customer support ticket', @@ -153,12 +159,13 @@ describe('Manifest update - objects', () => { const objectsAfterSecondSync = await findCustomObjects(); const ticketAfter = objectsAfterSecondSync.find( - (obj) => obj.nameSingular === 'ticket', + (obj) => obj.universalIdentifier === universalIdentifier, ); expect(ticketAfter).toBeDefined(); expect(ticketAfter).toMatchObject({ - nameSingular: 'ticket', + nameSingular: 'ticket2', + namePlural: 'tickets2', labelSingular: 'Support Ticket', labelPlural: 'Support Tickets', description: 'A customer support ticket', diff --git a/packages/twenty-server/test/integration/metadata/suites/application/utils/build-default-object-manifest.util.ts b/packages/twenty-server/test/integration/metadata/suites/application/utils/build-default-object-manifest.util.ts index 6f928bf40d..d28d74297f 100644 --- a/packages/twenty-server/test/integration/metadata/suites/application/utils/build-default-object-manifest.util.ts +++ b/packages/twenty-server/test/integration/metadata/suites/application/utils/build-default-object-manifest.util.ts @@ -10,6 +10,7 @@ export const buildDefaultObjectManifest = ({ description, icon = 'IconTicket', additionalFields = [], + universalIdentifier, }: { nameSingular: string; namePlural: string; @@ -18,11 +19,12 @@ export const buildDefaultObjectManifest = ({ description?: string; icon?: string; additionalFields?: ObjectManifest['fields']; + universalIdentifier?: string; }): ObjectManifest => { const idFieldUniversalIdentifier = uuidv4(); return { - universalIdentifier: uuidv4(), + universalIdentifier: universalIdentifier ?? uuidv4(), labelIdentifierFieldMetadataUniversalIdentifier: idFieldUniversalIdentifier, nameSingular, namePlural,