From 9080180156db4ea0c464ff19ddbda0bb6c8abfc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Thu, 9 Apr 2026 10:18:49 +0200 Subject: [PATCH] [COMMAND MENU ITEMS] Sync object metadata with navigation command menu items (#19456) When an object is created or enabled we create a navigation command menu item to that object. And when the object is disabled or deleted, we remove this command menu item. --- .../useCreateOneObjectMetadataItem.ts | 3 + .../useCreateOneObjectMetadataItem.test.tsx | 16 +- .../useDeleteOneObjectMetadataItem.test.tsx | 12 + .../hooks/useCreateOneObjectMetadataItem.ts | 37 ++- .../hooks/useDeleteOneObjectMetadataItem.ts | 22 +- .../hooks/useUpdateOneObjectMetadataItem.ts | 18 +- .../object-metadata.service.ts | 279 ++++++++++++++++-- ...ect-on-object-metadata.integration-spec.ts | 278 +++++++++++++++++ 8 files changed, 630 insertions(+), 35 deletions(-) create mode 100644 packages/twenty-server/test/integration/metadata/suites/object-metadata/command-menu-item-side-effect-on-object-metadata.integration-spec.ts diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useCreateOneObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useCreateOneObjectMetadataItem.ts index 72fe17ca8c..a616059646 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useCreateOneObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useCreateOneObjectMetadataItem.ts @@ -1,5 +1,6 @@ import { CreateOneObjectMetadataItemDocument, + FindManyCommandMenuItemsDocument, FindManyNavigationMenuItemsDocument, FindManyViewsDocument, } from '~/generated-metadata/graphql'; @@ -11,6 +12,8 @@ export const findManyViewsQuery = FindManyViewsDocument; export const findManyNavigationMenuItemsQuery = FindManyNavigationMenuItemsDocument; +export const findManyCommandMenuItemsQuery = FindManyCommandMenuItemsDocument; + export const variables = { input: { object: { diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useCreateOneObjectMetadataItem.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useCreateOneObjectMetadataItem.test.tsx index 860f9a9041..7bcf75cbff 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useCreateOneObjectMetadataItem.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useCreateOneObjectMetadataItem.test.tsx @@ -4,6 +4,7 @@ import { useCreateOneObjectMetadataItem } from '@/object-metadata/hooks/useCreat import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; import { + findManyCommandMenuItemsQuery, findManyNavigationMenuItemsQuery, findManyViewsQuery, query, @@ -12,7 +13,7 @@ import { } from '@/object-metadata/hooks/__mocks__/useCreateOneObjectMetadataItem'; import { jestExpectSuccessfulMetadataRequestResult } from '@/object-metadata/hooks/__tests__/utils/jest-expect-metadata-request-status.util'; -import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; +import { GetCurrentUserDocument } from '~/generated-metadata/graphql'; import { mockedUserData } from '~/testing/mock-data/users'; import { query as findManyObjectMetadataItemsQuery, @@ -33,7 +34,7 @@ const mocks = [ }, { request: { - query: GET_CURRENT_USER, + query: GetCurrentUserDocument, variables: {}, }, result: jest.fn(() => ({ @@ -75,6 +76,17 @@ const mocks = [ }, })), }, + { + request: { + query: findManyCommandMenuItemsQuery, + variables: {}, + }, + result: jest.fn(() => ({ + data: { + commandMenuItems: [], + }, + })), + }, ]; const Wrapper = getJestMetadataAndApolloMocksWrapper({ diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useDeleteOneObjectMetadataItem.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useDeleteOneObjectMetadataItem.test.tsx index fabca7f835..d176c5deac 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useDeleteOneObjectMetadataItem.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useDeleteOneObjectMetadataItem.test.tsx @@ -11,6 +11,7 @@ import { import { jestExpectSuccessfulMetadataRequestResult } from '@/object-metadata/hooks/__tests__/utils/jest-expect-metadata-request-status.util'; import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; import { FIND_ALL_VIEWS } from '@/views/graphql/queries/findAllViews'; +import { FindManyCommandMenuItemsDocument } from '~/generated-metadata/graphql'; import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; import { mockedUserData } from '~/testing/mock-data/users'; import { mockedViews } from '~/testing/mock-data/generated/metadata/views/mock-views-data'; @@ -62,6 +63,17 @@ const mocks = [ data: findManyObjectMetadataItemsResponseData, })), }, + { + request: { + query: FindManyCommandMenuItemsDocument, + variables: {}, + }, + result: jest.fn(() => ({ + data: { + commandMenuItems: [], + }, + })), + }, ]; const Wrapper = getJestMetadataAndApolloMocksWrapper({ diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts index dd52c7a77b..ace4b5995f 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts @@ -2,6 +2,7 @@ import { useApolloClient, useMutation } from '@apollo/client/react'; import { type CreateObjectInput, CreateOneObjectMetadataItemDocument, + FindManyCommandMenuItemsDocument, FindManyNavigationMenuItemsDocument, FindManyViewsDocument, } from '~/generated-metadata/graphql'; @@ -13,6 +14,7 @@ import { type FlatObjectMetadataItem } from '@/metadata-store/types/FlatObjectMe import { splitViewWithRelated } from '@/metadata-store/utils/splitViewWithRelated'; import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { useLoadCurrentUser } from '@/users/hooks/useLoadCurrentUser'; import { CombinedGraphQLErrors } from '@apollo/client/errors'; import { t } from '@lingui/core/macro'; import { CrudOperationType } from 'twenty-shared/types'; @@ -28,6 +30,7 @@ export const useCreateOneObjectMetadataItem = () => { const { enqueueErrorSnackBar } = useSnackBar(); const { addToDraft, replaceDraft, applyChanges } = useUpdateMetadataStoreDraft(); + const { loadCurrentUser } = useLoadCurrentUser(); const createOneObjectMetadataItem = async ( input: CreateObjectInput, @@ -70,17 +73,22 @@ export const useCreateOneObjectMetadataItem = () => { applyChanges(); - const [viewsResult, navItemsResult] = await Promise.all([ - client.query({ - query: FindManyViewsDocument, - variables: { objectMetadataId: createdObject.id }, - fetchPolicy: 'network-only', - }), - client.query({ - query: FindManyNavigationMenuItemsDocument, - fetchPolicy: 'network-only', - }), - ]); + const [viewsResult, navItemsResult, commandMenuItemsResult] = + await Promise.all([ + client.query({ + query: FindManyViewsDocument, + variables: { objectMetadataId: createdObject.id }, + fetchPolicy: 'network-only', + }), + client.query({ + query: FindManyNavigationMenuItemsDocument, + fetchPolicy: 'network-only', + }), + client.query({ + query: FindManyCommandMenuItemsDocument, + fetchPolicy: 'network-only', + }), + ]); const fetchedViews = viewsResult.data?.getViews ?? []; @@ -107,7 +115,14 @@ export const useCreateOneObjectMetadataItem = () => { navItemsResult.data?.navigationMenuItems ?? [], ); + replaceDraft( + 'commandMenuItems', + commandMenuItemsResult.data?.commandMenuItems ?? [], + ); + applyChanges(); + + await loadCurrentUser(); } return { diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts index b4fb40b715..4e56bb02a7 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts @@ -1,5 +1,8 @@ -import { useMutation } from '@apollo/client/react'; -import { DeleteOneObjectMetadataItemDocument } from '~/generated-metadata/graphql'; +import { useApolloClient, useMutation } from '@apollo/client/react'; +import { + DeleteOneObjectMetadataItemDocument, + FindManyCommandMenuItemsDocument, +} from '~/generated-metadata/graphql'; import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler'; import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft'; @@ -14,9 +17,11 @@ export const useDeleteOneObjectMetadataItem = () => { DeleteOneObjectMetadataItemDocument, ); + const client = useApolloClient(); const { handleMetadataError } = useMetadataErrorHandler(); const { enqueueErrorSnackBar } = useSnackBar(); - const { removeFromDraft, applyChanges } = useUpdateMetadataStoreDraft(); + const { removeFromDraft, replaceDraft, applyChanges } = + useUpdateMetadataStoreDraft(); const deleteOneObjectMetadataItem = async ( idToDelete: string, @@ -35,6 +40,17 @@ export const useDeleteOneObjectMetadataItem = () => { removeFromDraft({ key: 'objectMetadataItems', itemIds: [idToDelete] }); applyChanges(); + const commandMenuItemsResult = await client.query({ + query: FindManyCommandMenuItemsDocument, + fetchPolicy: 'network-only', + }); + + replaceDraft( + 'commandMenuItems', + commandMenuItemsResult.data?.commandMenuItems ?? [], + ); + applyChanges(); + return { status: 'successful', response, diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneObjectMetadataItem.ts index a0d1a295a2..f186b711d1 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneObjectMetadataItem.ts @@ -1,6 +1,7 @@ -import { useMutation } from '@apollo/client/react'; +import { useApolloClient, useMutation } from '@apollo/client/react'; import { type UpdateOneObjectInput, + FindManyCommandMenuItemsDocument, UpdateOneObjectMetadataItemDocument, } from '~/generated-metadata/graphql'; @@ -20,9 +21,11 @@ export const useUpdateOneObjectMetadataItem = () => { UpdateOneObjectMetadataItemDocument, ); + const client = useApolloClient(); const { handleMetadataError } = useMetadataErrorHandler(); const { enqueueErrorSnackBar } = useSnackBar(); - const { updateInDraft, applyChanges } = useUpdateMetadataStoreDraft(); + const { updateInDraft, replaceDraft, applyChanges } = + useUpdateMetadataStoreDraft(); const updateOneObjectMetadataItem = async ({ idToUpdate, @@ -52,6 +55,17 @@ export const useUpdateOneObjectMetadataItem = () => { objectData as FlatObjectMetadataItem, ]); applyChanges(); + + const commandMenuItemsResult = await client.query({ + query: FindManyCommandMenuItemsDocument, + fetchPolicy: 'network-only', + }); + + replaceDraft( + 'commandMenuItems', + commandMenuItemsResult.data?.commandMenuItems ?? [], + ); + applyChanges(); } return { diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts index 930140232c..74d6e9ba6a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts @@ -11,12 +11,15 @@ import { } from 'twenty-shared/types'; import { fromArrayToUniqueKeyRecord, isDefined } from 'twenty-shared/utils'; import { FindManyOptions, FindOneOptions, Repository } from 'typeorm'; -import { v4 as uuidv4, v4 } from 'uuid'; +import { v4, v5 } from 'uuid'; import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; -import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; -import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; +import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type'; +import { + buildNavigationFlatCommandMenuItem, + NAVIGATION_COMMAND_UUID_NAMESPACE, +} from 'src/engine/metadata-modules/flat-command-menu-item/utils/build-navigation-flat-command-menu-item.util'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util'; @@ -43,7 +46,6 @@ import { import { computeFlatDefaultRecordPageLayoutToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-default-record-page-layout-to-create.util'; import { computeFlatRecordPageFieldsViewToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-record-page-fields-view-to-create.util'; import { computeFlatViewFieldsToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util'; -import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception'; import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; @@ -59,10 +61,7 @@ export class ObjectMetadataService extends TypeOrmQueryService { + const { workspaceCustomFlatApplication, twentyStandardFlatApplication } = + await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { workspaceId }, + ); + const resolvedOwnerFlatApplication = - ownerFlatApplication ?? - ( - await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( - { workspaceId }, - ) - ).workspaceCustomFlatApplication; + ownerFlatApplication ?? workspaceCustomFlatApplication; const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps, @@ -90,6 +89,7 @@ export class ObjectMetadataService extends TypeOrmQueryService 0 || + commandMenuItemsToDelete.length > 0; + + if (hasCommandMenuItemChanges) { + const commandMenuItemMigrationResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + commandMenuItem: { + flatEntityToCreate: commandMenuItemsToCreate, + flatEntityToDelete: commandMenuItemsToDelete, + flatEntityToUpdate: [], + }, + }, + workspaceId, + applicationUniversalIdentifier: + twentyStandardFlatApplication.universalIdentifier, + }, + ); + + if (commandMenuItemMigrationResult.status === 'fail') { + throw new WorkspaceMigrationBuilderException( + commandMenuItemMigrationResult, + 'Multiple validation errors occurred while updating command menu items', + ); + } + } + const { flatObjectMetadataMaps: recomputedFlatObjectMetadataMaps } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { @@ -187,10 +246,10 @@ export class ObjectMetadataService extends TypeOrmQueryService + this.findNavigationCommandMenuItemForObject({ + objectUniversalIdentifier: + flatObjectMetadataToDelete.universalIdentifier, + flatCommandMenuItemMaps, + }), + ) + .filter(isDefined); + const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { @@ -358,6 +433,11 @@ export class ObjectMetadataService extends TypeOrmQueryService item.position)) + 1 : 0; - const newId = uuidv4(); + const newId = v4(); const now = new Date().toISOString(); return { @@ -729,6 +849,131 @@ export class ObjectMetadataService extends TypeOrmQueryService; + }; + }): FlatCommandMenuItem { + const existingItems = Object.values( + flatCommandMenuItemMaps.byUniversalIdentifier, + ).filter(isDefined); + + const nextPosition = + existingItems.length > 0 + ? Math.max(...existingItems.map((item) => item.position)) + 1 + : 0; + + return buildNavigationFlatCommandMenuItem({ + objectMetadata, + commandMenuItemId: v4(), + applicationId, + workspaceId, + position: nextPosition, + now: new Date().toISOString(), + }); + } + + private findNavigationCommandMenuItemForObject({ + objectUniversalIdentifier, + flatCommandMenuItemMaps, + }: { + objectUniversalIdentifier: string; + flatCommandMenuItemMaps: { + byUniversalIdentifier: Record; + }; + }): FlatCommandMenuItem | undefined { + const commandMenuItemUniversalIdentifier = v5( + objectUniversalIdentifier, + NAVIGATION_COMMAND_UUID_NAMESPACE, + ); + + return findFlatEntityByUniversalIdentifier({ + flatEntityMaps: flatCommandMenuItemMaps, + universalIdentifier: commandMenuItemUniversalIdentifier, + }); + } + + private computeCommandMenuItemChangesForActiveToggle({ + isBeingEnabled, + isBeingDisabled, + existingFlatObjectMetadata, + flatCommandMenuItemMaps, + workspaceId, + applicationId, + }: { + isBeingEnabled: boolean; + isBeingDisabled: boolean; + existingFlatObjectMetadata: FlatObjectMetadata | undefined; + flatCommandMenuItemMaps: { + byUniversalIdentifier: Record; + }; + workspaceId: string; + applicationId: string; + }): { + commandMenuItemsToCreate: FlatCommandMenuItem[]; + commandMenuItemsToDelete: FlatCommandMenuItem[]; + } { + if (!isDefined(existingFlatObjectMetadata)) { + return { commandMenuItemsToCreate: [], commandMenuItemsToDelete: [] }; + } + + if (isBeingEnabled) { + const existingCommandMenuItem = + this.findNavigationCommandMenuItemForObject({ + objectUniversalIdentifier: + existingFlatObjectMetadata.universalIdentifier, + flatCommandMenuItemMaps, + }); + + if (!isDefined(existingCommandMenuItem)) { + return { + commandMenuItemsToCreate: [ + this.buildFlatNavigationCommandMenuItem({ + objectMetadata: existingFlatObjectMetadata, + workspaceId, + applicationId, + flatCommandMenuItemMaps, + }), + ], + commandMenuItemsToDelete: [], + }; + } + } + + if (isBeingDisabled) { + const commandMenuItemToDelete = + this.findNavigationCommandMenuItemForObject({ + objectUniversalIdentifier: + existingFlatObjectMetadata.universalIdentifier, + flatCommandMenuItemMaps, + }); + + if (isDefined(commandMenuItemToDelete)) { + return { + commandMenuItemsToCreate: [], + commandMenuItemsToDelete: [commandMenuItemToDelete], + }; + } + } + + return { commandMenuItemsToCreate: [], commandMenuItemsToDelete: [] }; + } + public async findOneWithinWorkspace( workspaceId: string, options: FindOneOptions, diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/command-menu-item-side-effect-on-object-metadata.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/command-menu-item-side-effect-on-object-metadata.integration-spec.ts new file mode 100644 index 0000000000..b0900c7398 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/command-menu-item-side-effect-on-object-metadata.integration-spec.ts @@ -0,0 +1,278 @@ +import { findCommandMenuItems } from 'test/integration/metadata/suites/command-menu-item/utils/find-command-menu-items.util'; +import { type CreateOneObjectFactoryInput } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata-query-factory.util'; +import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; +import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; +import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; +import { isDefined } from 'twenty-shared/utils'; + +import { type CommandMenuItemDTO } from 'src/engine/metadata-modules/command-menu-item/dtos/command-menu-item.dto'; +import { type ObjectMetadataCommandMenuItemPayload } from 'src/engine/metadata-modules/command-menu-item/dtos/types/object-metadata-command-menu-item-payload.type'; +import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum'; + +const findNavigationCommandMenuItemForObject = ( + commandMenuItems: CommandMenuItemDTO[], + objectMetadataId: string, +) => + commandMenuItems.find( + (item) => + item.engineComponentKey === EngineComponentKey.NAVIGATION && + (item.payload as ObjectMetadataCommandMenuItemPayload | undefined) + ?.objectMetadataItemId === objectMetadataId, + ); + +const COMMAND_MENU_ITEM_GQL_FIELDS = ` + id + engineComponentKey + label + icon + payload { + ... on PathCommandMenuItemPayload { + path + } + ... on ObjectMetadataCommandMenuItemPayload { + objectMetadataItemId + } + } +`; + +describe('Command menu item side effect on object metadata', () => { + let createdObjectMetadataId: string | undefined = undefined; + + const uniqueSuffix = Date.now().toString().slice(-8); + + const createObjectInput: CreateOneObjectFactoryInput = { + namePlural: `sideEffectItems${uniqueSuffix}`, + nameSingular: `sideEffectItem${uniqueSuffix}`, + labelPlural: `Side Effect Items ${uniqueSuffix}`, + labelSingular: `Side Effect Item ${uniqueSuffix}`, + description: 'Object for command menu item side effect tests', + icon: 'IconBox', + isLabelSyncedWithName: false, + }; + + afterEach(async () => { + if (!isDefined(createdObjectMetadataId)) { + return; + } + + await updateOneObjectMetadata({ + expectToFail: false, + input: { + idToUpdate: createdObjectMetadataId, + updatePayload: { isActive: false }, + }, + }); + + await deleteOneObjectMetadata({ + input: { idToDelete: createdObjectMetadataId }, + expectToFail: false, + }); + + createdObjectMetadataId = undefined; + }); + + it('should create a navigation command menu item when a custom object is created', async () => { + const { + data: { createOneObject }, + } = await createOneObjectMetadata({ + expectToFail: false, + input: createObjectInput, + gqlFields: 'id', + }); + + createdObjectMetadataId = createOneObject.id; + + const { + data: { commandMenuItems }, + } = await findCommandMenuItems({ + expectToFail: false, + input: undefined, + gqlFields: COMMAND_MENU_ITEM_GQL_FIELDS, + }); + + const navigationItem = findNavigationCommandMenuItemForObject( + commandMenuItems, + createdObjectMetadataId, + ); + + expect(navigationItem).toEqual( + expect.objectContaining({ + label: `Go to ${createObjectInput.labelPlural}`, + icon: createObjectInput.icon, + engineComponentKey: EngineComponentKey.NAVIGATION, + }), + ); + }); + + it('should delete the navigation command menu item when a custom object is deleted', async () => { + const { + data: { createOneObject }, + } = await createOneObjectMetadata({ + expectToFail: false, + input: createObjectInput, + gqlFields: 'id', + }); + + createdObjectMetadataId = createOneObject.id; + const deletedObjectId = createdObjectMetadataId; + + const { + data: { commandMenuItems: itemsBeforeDelete }, + } = await findCommandMenuItems({ + expectToFail: false, + input: undefined, + gqlFields: COMMAND_MENU_ITEM_GQL_FIELDS, + }); + + expect( + findNavigationCommandMenuItemForObject( + itemsBeforeDelete, + deletedObjectId, + ), + ).toBeDefined(); + + await updateOneObjectMetadata({ + expectToFail: false, + input: { + idToUpdate: createdObjectMetadataId, + updatePayload: { isActive: false }, + }, + }); + + await deleteOneObjectMetadata({ + input: { idToDelete: createdObjectMetadataId }, + expectToFail: false, + }); + + createdObjectMetadataId = undefined; + + const { + data: { commandMenuItems: itemsAfterDelete }, + } = await findCommandMenuItems({ + expectToFail: false, + input: undefined, + gqlFields: COMMAND_MENU_ITEM_GQL_FIELDS, + }); + + expect( + findNavigationCommandMenuItemForObject( + itemsAfterDelete, + deletedObjectId, + ), + ).toBeUndefined(); + }); + + it('should delete the navigation command menu item when a custom object is disabled', async () => { + const { + data: { createOneObject }, + } = await createOneObjectMetadata({ + expectToFail: false, + input: createObjectInput, + gqlFields: 'id', + }); + + createdObjectMetadataId = createOneObject.id; + + const { + data: { commandMenuItems: itemsBeforeDisable }, + } = await findCommandMenuItems({ + expectToFail: false, + input: undefined, + gqlFields: COMMAND_MENU_ITEM_GQL_FIELDS, + }); + + expect( + findNavigationCommandMenuItemForObject( + itemsBeforeDisable, + createdObjectMetadataId, + ), + ).toBeDefined(); + + await updateOneObjectMetadata({ + expectToFail: false, + input: { + idToUpdate: createdObjectMetadataId, + updatePayload: { isActive: false }, + }, + }); + + const { + data: { commandMenuItems: itemsAfterDisable }, + } = await findCommandMenuItems({ + expectToFail: false, + input: undefined, + gqlFields: COMMAND_MENU_ITEM_GQL_FIELDS, + }); + + expect( + findNavigationCommandMenuItemForObject( + itemsAfterDisable, + createdObjectMetadataId, + ), + ).toBeUndefined(); + }); + + it('should recreate the navigation command menu item when a disabled object is re-enabled', async () => { + const { + data: { createOneObject }, + } = await createOneObjectMetadata({ + expectToFail: false, + input: createObjectInput, + gqlFields: 'id', + }); + + createdObjectMetadataId = createOneObject.id; + + await updateOneObjectMetadata({ + expectToFail: false, + input: { + idToUpdate: createdObjectMetadataId, + updatePayload: { isActive: false }, + }, + }); + + const { + data: { commandMenuItems: itemsWhileDisabled }, + } = await findCommandMenuItems({ + expectToFail: false, + input: undefined, + gqlFields: COMMAND_MENU_ITEM_GQL_FIELDS, + }); + + expect( + findNavigationCommandMenuItemForObject( + itemsWhileDisabled, + createdObjectMetadataId, + ), + ).toBeUndefined(); + + await updateOneObjectMetadata({ + expectToFail: false, + input: { + idToUpdate: createdObjectMetadataId, + updatePayload: { isActive: true }, + }, + }); + + const { + data: { commandMenuItems: itemsAfterReEnable }, + } = await findCommandMenuItems({ + expectToFail: false, + input: undefined, + gqlFields: COMMAND_MENU_ITEM_GQL_FIELDS, + }); + + const navigationItem = findNavigationCommandMenuItemForObject( + itemsAfterReEnable, + createdObjectMetadataId, + ); + + expect(navigationItem).toEqual( + expect.objectContaining({ + label: `Go to ${createObjectInput.labelPlural}`, + icon: createObjectInput.icon, + engineComponentKey: EngineComponentKey.NAVIGATION, + }), + ); + }); +});