diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/services/workspace-entity-migration-builder.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/services/workspace-entity-migration-builder.service.ts index fe3c167847..4d1c16da83 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/services/workspace-entity-migration-builder.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/services/workspace-entity-migration-builder.service.ts @@ -208,6 +208,7 @@ export abstract class WorkspaceEntityMigrationBuilderService< buildOptions, additionalCacheDataMaps, universalIdentifier: flatEntityToUpdateUniversalIdentifier, + remainingFlatEntityMapsToValidate: createdFlatEntityMaps, }); if (validationResult.status === 'fail') { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type.ts index d1cf1a5560..005da4ecd0 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type.ts @@ -5,7 +5,7 @@ import { type UniversalFlatEntityValidationArgs } from 'src/engine/workspace-man export type FlatEntityUpdateValidationArgs = Omit< UniversalFlatEntityValidationArgs, - 'flatEntityToValidate' | 'remainingFlatEntityMapsToValidate' + 'flatEntityToValidate' > & { flatEntityUpdate: UniversalFlatEntityUpdate; universalIdentifier: string; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-navigation-menu-item-validator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-navigation-menu-item-validator.service.ts index 5972799eae..135119e99f 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-navigation-menu-item-validator.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-navigation-menu-item-validator.service.ts @@ -284,6 +284,7 @@ export class FlatNavigationMenuItemValidatorService { optimisticFlatEntityMapsAndRelatedFlatEntityMaps: { flatNavigationMenuItemMaps: optimisticFlatNavigationMenuItemMaps, }, + remainingFlatEntityMapsToValidate, }: FlatEntityUpdateValidationArgs< typeof ALL_METADATA_NAME.navigationMenuItem >): FailedFlatEntityValidation<'navigationMenuItem', 'update'> { @@ -352,12 +353,21 @@ export class FlatNavigationMenuItemValidatorService { const newFolderUniversalIdentifier = folderUniversalIdentifierUpdate; + const combinedFlatNavigationMenuItemMaps: MetadataUniversalFlatEntityMaps< + typeof ALL_METADATA_NAME.navigationMenuItem + > = { + byUniversalIdentifier: { + ...remainingFlatEntityMapsToValidate.byUniversalIdentifier, + ...optimisticFlatNavigationMenuItemMaps.byUniversalIdentifier, + }, + }; + const circularDependencyErrors = this.getCircularDependencyValidationErrors( { navigationMenuItemUniversalIdentifier: fromFlatNavigationMenuItem.universalIdentifier, folderUniversalIdentifier: newFolderUniversalIdentifier, - flatNavigationMenuItemMaps: optimisticFlatNavigationMenuItemMaps, + flatNavigationMenuItemMaps: combinedFlatNavigationMenuItemMaps, }, ); @@ -368,7 +378,7 @@ export class FlatNavigationMenuItemValidatorService { const referencedParentNavigationMenuItem = findFlatEntityByUniversalIdentifier({ universalIdentifier: newFolderUniversalIdentifier, - flatEntityMaps: optimisticFlatNavigationMenuItemMaps, + flatEntityMaps: combinedFlatNavigationMenuItemMaps, }); if (!isDefined(referencedParentNavigationMenuItem)) { diff --git a/packages/twenty-server/test/integration/metadata/suites/application/successful-manifest-update-navigation-menu-item.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/application/successful-manifest-update-navigation-menu-item.integration-spec.ts index 51e32946c0..2a4eff55bd 100644 --- a/packages/twenty-server/test/integration/metadata/suites/application/successful-manifest-update-navigation-menu-item.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/application/successful-manifest-update-navigation-menu-item.integration-spec.ts @@ -167,6 +167,76 @@ describe('Manifest update - navigation menu items', () => { }); }, 60000); + it('should move existing menu items into a folder created in the same sync', async () => { + await syncApplication({ + manifest: buildManifest({ + navigationMenuItems: [ + { + universalIdentifier: TEST_CHILD_ID, + type: NavigationMenuItemType.LINK, + name: 'Child Link', + icon: 'IconLink', + position: 0, + link: 'https://example.com', + }, + ], + }), + expectToFail: false, + }); + + const itemsAfterFirstSync = await findAppNavigationMenuItems(); + + expect(itemsAfterFirstSync).toHaveLength(1); + expect(itemsAfterFirstSync[0]).toMatchObject({ + type: NavigationMenuItemType.LINK, + name: 'Child Link', + folderId: null, + }); + + await syncApplication({ + manifest: buildManifest({ + navigationMenuItems: [ + { + universalIdentifier: TEST_FOLDER_ID, + type: NavigationMenuItemType.FOLDER, + name: 'Test Folder', + icon: 'IconFolder', + position: 0, + }, + { + universalIdentifier: TEST_CHILD_ID, + type: NavigationMenuItemType.LINK, + name: 'Child Link', + icon: 'IconLink', + position: 0, + link: 'https://example.com', + folderUniversalIdentifier: TEST_FOLDER_ID, + }, + ], + }), + expectToFail: false, + }); + + const itemsAfterSecondSync = await findAppNavigationMenuItems(); + + expect(itemsAfterSecondSync).toHaveLength(2); + + const folder = itemsAfterSecondSync.find( + (item) => item.type === NavigationMenuItemType.FOLDER, + ); + const child = itemsAfterSecondSync.find( + (item) => item.type === NavigationMenuItemType.LINK, + ); + + expect(folder).toBeDefined(); + expect(child).toBeDefined(); + expect(child).toMatchObject({ + type: NavigationMenuItemType.LINK, + name: 'Child Link', + folderId: folder!.id, + }); + }, 60000); + it('should delete navigation menu items when removed from manifest on second sync', async () => { await syncApplication({ manifest: buildManifest({