Add command menu item entity (#17181)

This commit is contained in:
Abdul Rahman
2026-01-16 19:02:07 +00:00
committed by GitHub
parent b7fbe1c49e
commit 5a617e4718
66 changed files with 2960 additions and 15 deletions
@@ -609,6 +609,26 @@ export type ClientAiModelConfig = {
provider: ModelProvider;
};
export type CommandMenuItem = {
__typename?: 'CommandMenuItem';
applicationId?: Maybe<Scalars['UUID']>;
availabilityObjectMetadataId?: Maybe<Scalars['UUID']>;
availabilityType: CommandMenuItemAvailabilityType;
createdAt: Scalars['DateTime'];
icon?: Maybe<Scalars['String']>;
id: Scalars['UUID'];
isPinned: Scalars['Boolean'];
label: Scalars['String'];
updatedAt: Scalars['DateTime'];
workflowVersionId: Scalars['UUID'];
};
export enum CommandMenuItemAvailabilityType {
BULK_RECORDS = 'BULK_RECORDS',
GLOBAL = 'GLOBAL',
SINGLE_RECORD = 'SINGLE_RECORD'
}
export type ComputeStepOutputSchemaInput = {
/** Step JSON format */
step: Scalars['JSON'];
@@ -836,6 +856,15 @@ export type CreateApprovedAccessDomainInput = {
email: Scalars['String'];
};
export type CreateCommandMenuItemInput = {
availabilityObjectMetadataId?: InputMaybe<Scalars['UUID']>;
availabilityType?: InputMaybe<CommandMenuItemAvailabilityType>;
icon?: InputMaybe<Scalars['String']>;
isPinned?: InputMaybe<Scalars['Boolean']>;
label: Scalars['String'];
workflowVersionId: Scalars['UUID'];
};
export type CreateCronTriggerInput = {
serverlessFunctionId: Scalars['String'];
settings: Scalars['JSON'];
@@ -1373,6 +1402,7 @@ export type FeatureFlagDto = {
export enum FeatureFlagKey {
IS_AI_ENABLED = 'IS_AI_ENABLED',
IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED',
IS_COMMAND_MENU_ITEM_ENABLED = 'IS_COMMAND_MENU_ITEM_ENABLED',
IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED',
IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED',
IS_IF_ELSE_ENABLED = 'IS_IF_ELSE_ENABLED',
@@ -1878,6 +1908,7 @@ export type Mutation = {
createApiKey: ApiKey;
createApprovedAccessDomain: ApprovedAccessDomain;
createChatThread: AgentChatThread;
createCommandMenuItem: CommandMenuItem;
createCoreView: CoreView;
createCoreViewField: CoreViewField;
createCoreViewFilter: CoreViewFilter;
@@ -1917,6 +1948,7 @@ export type Mutation = {
deactivateSkill: Skill;
deactivateWorkflowVersion: Scalars['Boolean'];
deleteApprovedAccessDomain: Scalars['Boolean'];
deleteCommandMenuItem: CommandMenuItem;
deleteCoreView: Scalars['Boolean'];
deleteCoreViewField: CoreViewField;
deleteCoreViewFilter: CoreViewFilter;
@@ -2007,6 +2039,7 @@ export type Mutation = {
trackAnalytics: Analytics;
uninstallApplication: Scalars['Boolean'];
updateApiKey?: Maybe<ApiKey>;
updateCommandMenuItem: CommandMenuItem;
updateCoreView: CoreView;
updateCoreViewField: CoreViewField;
updateCoreViewFilter: CoreViewFilter;
@@ -2125,6 +2158,11 @@ export type MutationCreateApprovedAccessDomainArgs = {
};
export type MutationCreateCommandMenuItemArgs = {
input: CreateCommandMenuItemInput;
};
export type MutationCreateCoreViewArgs = {
input: CreateViewInput;
};
@@ -2326,6 +2364,11 @@ export type MutationDeleteApprovedAccessDomainArgs = {
};
export type MutationDeleteCommandMenuItemArgs = {
id: Scalars['UUID'];
};
export type MutationDeleteCoreViewArgs = {
id: Scalars['String'];
};
@@ -2756,6 +2799,11 @@ export type MutationUpdateApiKeyArgs = {
};
export type MutationUpdateCommandMenuItemArgs = {
input: UpdateCommandMenuItemInput;
};
export type MutationUpdateCoreViewArgs = {
id: Scalars['String'];
input: UpdateViewInput;
@@ -3391,6 +3439,8 @@ export type Query = {
chatThreads: Array<AgentChatThread>;
checkUserExists: CheckUserExistOutput;
checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValidOutput;
commandMenuItem?: Maybe<CommandMenuItem>;
commandMenuItems: Array<CommandMenuItem>;
currentUser: User;
currentWorkspace: Workspace;
field: Field;
@@ -3509,6 +3559,11 @@ export type QueryCheckWorkspaceInviteHashIsValidArgs = {
};
export type QueryCommandMenuItemArgs = {
id: Scalars['UUID'];
};
export type QueryFieldArgs = {
id: Scalars['UUID'];
};
@@ -4494,6 +4549,15 @@ export type UpdateApiKeyInput = {
revokedAt?: InputMaybe<Scalars['String']>;
};
export type UpdateCommandMenuItemInput = {
availabilityObjectMetadataId?: InputMaybe<Scalars['UUID']>;
availabilityType?: InputMaybe<CommandMenuItemAvailabilityType>;
icon?: InputMaybe<Scalars['String']>;
id: Scalars['UUID'];
isPinned?: InputMaybe<Scalars['Boolean']>;
label?: InputMaybe<Scalars['String']>;
};
export type UpdateCronTriggerInput = {
/** The id of the cron trigger to update */
id: Scalars['String'];
@@ -609,6 +609,26 @@ export type ClientAiModelConfig = {
provider: ModelProvider;
};
export type CommandMenuItem = {
__typename?: 'CommandMenuItem';
applicationId?: Maybe<Scalars['UUID']>;
availabilityObjectMetadataId?: Maybe<Scalars['UUID']>;
availabilityType: CommandMenuItemAvailabilityType;
createdAt: Scalars['DateTime'];
icon?: Maybe<Scalars['String']>;
id: Scalars['UUID'];
isPinned: Scalars['Boolean'];
label: Scalars['String'];
updatedAt: Scalars['DateTime'];
workflowVersionId: Scalars['UUID'];
};
export enum CommandMenuItemAvailabilityType {
BULK_RECORDS = 'BULK_RECORDS',
GLOBAL = 'GLOBAL',
SINGLE_RECORD = 'SINGLE_RECORD'
}
export type ComputeStepOutputSchemaInput = {
/** Step JSON format */
step: Scalars['JSON'];
@@ -832,6 +852,15 @@ export type CreateApprovedAccessDomainInput = {
email: Scalars['String'];
};
export type CreateCommandMenuItemInput = {
availabilityObjectMetadataId?: InputMaybe<Scalars['UUID']>;
availabilityType?: InputMaybe<CommandMenuItemAvailabilityType>;
icon?: InputMaybe<Scalars['String']>;
isPinned?: InputMaybe<Scalars['Boolean']>;
label: Scalars['String'];
workflowVersionId: Scalars['UUID'];
};
export type CreateCronTriggerInput = {
serverlessFunctionId: Scalars['String'];
settings: Scalars['JSON'];
@@ -1350,6 +1379,7 @@ export type FeatureFlagDto = {
export enum FeatureFlagKey {
IS_AI_ENABLED = 'IS_AI_ENABLED',
IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED',
IS_COMMAND_MENU_ITEM_ENABLED = 'IS_COMMAND_MENU_ITEM_ENABLED',
IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED',
IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED',
IS_IF_ELSE_ENABLED = 'IS_IF_ELSE_ENABLED',
@@ -1853,6 +1883,7 @@ export type Mutation = {
computeStepOutputSchema: Scalars['JSON'];
createApiKey: ApiKey;
createApprovedAccessDomain: ApprovedAccessDomain;
createCommandMenuItem: CommandMenuItem;
createCoreView: CoreView;
createCoreViewField: CoreViewField;
createCoreViewFilter: CoreViewFilter;
@@ -1889,6 +1920,7 @@ export type Mutation = {
createWorkflowVersionStep: WorkflowVersionStepChanges;
deactivateWorkflowVersion: Scalars['Boolean'];
deleteApprovedAccessDomain: Scalars['Boolean'];
deleteCommandMenuItem: CommandMenuItem;
deleteCoreView: Scalars['Boolean'];
deleteCoreViewField: CoreViewField;
deleteCoreViewFilter: CoreViewFilter;
@@ -1975,6 +2007,7 @@ export type Mutation = {
trackAnalytics: Analytics;
uninstallApplication: Scalars['Boolean'];
updateApiKey?: Maybe<ApiKey>;
updateCommandMenuItem: CommandMenuItem;
updateCoreView: CoreView;
updateCoreViewField: CoreViewField;
updateCoreViewFilter: CoreViewFilter;
@@ -2086,6 +2119,11 @@ export type MutationCreateApprovedAccessDomainArgs = {
};
export type MutationCreateCommandMenuItemArgs = {
input: CreateCommandMenuItemInput;
};
export type MutationCreateCoreViewArgs = {
input: CreateViewInput;
};
@@ -2267,6 +2305,11 @@ export type MutationDeleteApprovedAccessDomainArgs = {
};
export type MutationDeleteCommandMenuItemArgs = {
id: Scalars['UUID'];
};
export type MutationDeleteCoreViewArgs = {
id: Scalars['String'];
};
@@ -2676,6 +2719,11 @@ export type MutationUpdateApiKeyArgs = {
};
export type MutationUpdateCommandMenuItemArgs = {
input: UpdateCommandMenuItemInput;
};
export type MutationUpdateCoreViewArgs = {
id: Scalars['String'];
input: UpdateViewInput;
@@ -3297,6 +3345,8 @@ export type Query = {
billingPortalSession: BillingSessionOutput;
checkUserExists: CheckUserExistOutput;
checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValidOutput;
commandMenuItem?: Maybe<CommandMenuItem>;
commandMenuItems: Array<CommandMenuItem>;
currentUser: User;
currentWorkspace: Workspace;
field: Field;
@@ -3396,6 +3446,11 @@ export type QueryCheckWorkspaceInviteHashIsValidArgs = {
};
export type QueryCommandMenuItemArgs = {
id: Scalars['UUID'];
};
export type QueryFindOneAgentArgs = {
input: AgentIdInput;
};
@@ -4338,6 +4393,15 @@ export type UpdateApiKeyInput = {
revokedAt?: InputMaybe<Scalars['String']>;
};
export type UpdateCommandMenuItemInput = {
availabilityObjectMetadataId?: InputMaybe<Scalars['UUID']>;
availabilityType?: InputMaybe<CommandMenuItemAvailabilityType>;
icon?: InputMaybe<Scalars['String']>;
id: Scalars['UUID'];
isPinned?: InputMaybe<Scalars['Boolean']>;
label?: InputMaybe<Scalars['String']>;
};
export type UpdateCronTriggerInput = {
/** The id of the cron trigger to update */
id: Scalars['String'];
@@ -42,6 +42,7 @@ export const useMetadataErrorHandler = () => {
rowLevelPermissionPredicate: t`row level permission predicate`,
rowLevelPermissionPredicateGroup: t`row level permission predicate group`,
viewFilterGroup: t`view filter group`,
commandMenuItem: t`command menu item`,
frontComponent: t`front component`,
} as const satisfies Record<AllMetadataName, string>;
@@ -0,0 +1,53 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
export class AddCommandMenuItemEntity1768503887441
implements MigrationInterface
{
name = 'AddCommandMenuItemEntity1768503887441';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TYPE "core"."commandMenuItem_availabilitytype_enum" AS ENUM('GLOBAL', 'SINGLE_RECORD', 'BULK_RECORDS')`,
);
await queryRunner.query(
`CREATE TABLE "core"."commandMenuItem" ("workspaceId" uuid NOT NULL, "universalIdentifier" uuid NOT NULL, "applicationId" uuid NOT NULL, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "workflowVersionId" uuid NOT NULL, "label" character varying NOT NULL, "icon" character varying, "isPinned" boolean NOT NULL DEFAULT false, "availabilityType" "core"."commandMenuItem_availabilitytype_enum" NOT NULL DEFAULT 'GLOBAL', "availabilityObjectMetadataId" uuid, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_fd076dc869e721593133fe8a007" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE UNIQUE INDEX "IDX_a3a5976e1b580ba1086c595802" ON "core"."commandMenuItem" ("workspaceId", "universalIdentifier")`,
);
await queryRunner.query(
`CREATE INDEX "IDX_COMMAND_MENU_ITEM_WORKFLOW_VERSION_ID_WORKSPACE_ID" ON "core"."commandMenuItem" ("workflowVersionId", "workspaceId")`,
);
await queryRunner.query(
`ALTER TABLE "core"."commandMenuItem" ADD CONSTRAINT "FK_94947770f00413f134a1ec01dd7" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "core"."commandMenuItem" ADD CONSTRAINT "FK_ad42dd64b117491a38120466d65" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "core"."commandMenuItem" ADD CONSTRAINT "FK_6e050fb56a8385718123a4f8bc6" FOREIGN KEY ("availabilityObjectMetadataId") REFERENCES "core"."objectMetadata"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."commandMenuItem" DROP CONSTRAINT "FK_6e050fb56a8385718123a4f8bc6"`,
);
await queryRunner.query(
`ALTER TABLE "core"."commandMenuItem" DROP CONSTRAINT "FK_ad42dd64b117491a38120466d65"`,
);
await queryRunner.query(
`ALTER TABLE "core"."commandMenuItem" DROP CONSTRAINT "FK_94947770f00413f134a1ec01dd7"`,
);
await queryRunner.query(
`DROP INDEX "core"."IDX_COMMAND_MENU_ITEM_WORKFLOW_VERSION_ID_WORKSPACE_ID"`,
);
await queryRunner.query(
`DROP INDEX "core"."IDX_a3a5976e1b580ba1086c595802"`,
);
await queryRunner.query(`DROP TABLE "core"."commandMenuItem"`);
await queryRunner.query(
`DROP TYPE "core"."commandMenuItem_availabilitytype_enum"`,
);
}
}
@@ -12,4 +12,5 @@ export enum FeatureFlagKey {
IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED = 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED',
IS_IF_ELSE_ENABLED = 'IS_IF_ELSE_ENABLED',
IS_SSE_DB_EVENTS_ENABLED = 'IS_SSE_DB_EVENTS_ENABLED',
IS_COMMAND_MENU_ITEM_ENABLED = 'IS_COMMAND_MENU_ITEM_ENABLED',
}
@@ -0,0 +1,37 @@
import { type MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { assertUnreachable } from 'twenty-shared/utils';
import { CustomException } from 'src/utils/custom-exception';
export enum CommandMenuItemExceptionCode {
COMMAND_MENU_ITEM_NOT_FOUND = 'COMMAND_MENU_ITEM_NOT_FOUND',
INVALID_COMMAND_MENU_ITEM_INPUT = 'INVALID_COMMAND_MENU_ITEM_INPUT',
}
const getCommandMenuItemExceptionUserFriendlyMessage = (
code: CommandMenuItemExceptionCode,
) => {
switch (code) {
case CommandMenuItemExceptionCode.COMMAND_MENU_ITEM_NOT_FOUND:
return msg`Command menu item not found.`;
case CommandMenuItemExceptionCode.INVALID_COMMAND_MENU_ITEM_INPUT:
return msg`Invalid command menu item input.`;
default:
assertUnreachable(code);
}
};
export class CommandMenuItemException extends CustomException<CommandMenuItemExceptionCode> {
constructor(
message: string,
code: CommandMenuItemExceptionCode,
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
) {
super(message, code, {
userFriendlyMessage:
userFriendlyMessage ??
getCommandMenuItemExceptionUserFriendlyMessage(code),
});
}
}
@@ -0,0 +1,29 @@
import { Module } from '@nestjs/common';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { CommandMenuItemResolver } from 'src/engine/metadata-modules/command-menu-item/command-menu-item.resolver';
import { CommandMenuItemService } from 'src/engine/metadata-modules/command-menu-item/command-menu-item.service';
import { CommandMenuItemGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/command-menu-item/interceptors/command-menu-item-graphql-api-exception.interceptor';
import { FlatCommandMenuItemModule } from 'src/engine/metadata-modules/flat-command-menu-item/flat-command-menu-item.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
@Module({
imports: [
WorkspaceManyOrAllFlatEntityMapsCacheModule,
WorkspaceMigrationModule,
ApplicationModule,
FlatCommandMenuItemModule,
FeatureFlagModule,
],
providers: [
CommandMenuItemService,
CommandMenuItemResolver,
CommandMenuItemGraphqlApiExceptionInterceptor,
WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor,
],
exports: [CommandMenuItemService],
})
export class CommandMenuItemModule {}
@@ -0,0 +1,80 @@
import { UseGuards, UseInterceptors } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import {
FeatureFlagGuard,
RequireFeatureFlag,
} from 'src/engine/guards/feature-flag.guard';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { CommandMenuItemService } from 'src/engine/metadata-modules/command-menu-item/command-menu-item.service';
import { CommandMenuItemDTO } from 'src/engine/metadata-modules/command-menu-item/dtos/command-menu-item.dto';
import { CreateCommandMenuItemInput } from 'src/engine/metadata-modules/command-menu-item/dtos/create-command-menu-item.input';
import { UpdateCommandMenuItemInput } from 'src/engine/metadata-modules/command-menu-item/dtos/update-command-menu-item.input';
import { CommandMenuItemGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/command-menu-item/interceptors/command-menu-item-graphql-api-exception.interceptor';
import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor';
@UseGuards(WorkspaceAuthGuard, FeatureFlagGuard)
@UseInterceptors(
WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor,
CommandMenuItemGraphqlApiExceptionInterceptor,
)
@Resolver(() => CommandMenuItemDTO)
export class CommandMenuItemResolver {
constructor(
private readonly commandMenuItemService: CommandMenuItemService,
) {}
@Query(() => [CommandMenuItemDTO])
@UseGuards(NoPermissionGuard)
@RequireFeatureFlag(FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED)
async commandMenuItems(
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<CommandMenuItemDTO[]> {
return await this.commandMenuItemService.findAll(workspace.id);
}
@Query(() => CommandMenuItemDTO, { nullable: true })
@UseGuards(NoPermissionGuard)
@RequireFeatureFlag(FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED)
async commandMenuItem(
@Args('id', { type: () => UUIDScalarType }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<CommandMenuItemDTO | null> {
return await this.commandMenuItemService.findById(id, workspace.id);
}
@Mutation(() => CommandMenuItemDTO)
@UseGuards(NoPermissionGuard)
@RequireFeatureFlag(FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED)
async createCommandMenuItem(
@Args('input') input: CreateCommandMenuItemInput,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<CommandMenuItemDTO> {
return await this.commandMenuItemService.create(input, workspace.id);
}
@Mutation(() => CommandMenuItemDTO)
@UseGuards(NoPermissionGuard)
@RequireFeatureFlag(FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED)
async updateCommandMenuItem(
@Args('input') input: UpdateCommandMenuItemInput,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<CommandMenuItemDTO> {
return await this.commandMenuItemService.update(input, workspace.id);
}
@Mutation(() => CommandMenuItemDTO)
@UseGuards(NoPermissionGuard)
@RequireFeatureFlag(FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED)
async deleteCommandMenuItem(
@Args('id', { type: () => UUIDScalarType }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<CommandMenuItemDTO> {
return await this.commandMenuItemService.delete(id, workspace.id);
}
}
@@ -0,0 +1,279 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import {
CommandMenuItemException,
CommandMenuItemExceptionCode,
} from 'src/engine/metadata-modules/command-menu-item/command-menu-item.exception';
import { type CommandMenuItemDTO } from 'src/engine/metadata-modules/command-menu-item/dtos/command-menu-item.dto';
import { type CreateCommandMenuItemInput } from 'src/engine/metadata-modules/command-menu-item/dtos/create-command-menu-item.input';
import { type UpdateCommandMenuItemInput } from 'src/engine/metadata-modules/command-menu-item/dtos/update-command-menu-item.input';
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
import { fromCreateCommandMenuItemInputToFlatCommandMenuItemToCreate } from 'src/engine/metadata-modules/flat-command-menu-item/utils/from-create-command-menu-item-input-to-flat-command-menu-item-to-create.util';
import { fromDeleteCommandMenuItemInputToFlatCommandMenuItemOrThrow } from 'src/engine/metadata-modules/flat-command-menu-item/utils/from-delete-command-menu-item-input-to-flat-command-menu-item-or-throw.util';
import { fromFlatCommandMenuItemToCommandMenuItemDto } from 'src/engine/metadata-modules/flat-command-menu-item/utils/from-flat-command-menu-item-to-command-menu-item-dto.util';
import { fromUpdateCommandMenuItemInputToFlatCommandMenuItemToUpdateOrThrow } from 'src/engine/metadata-modules/flat-command-menu-item/utils/from-update-command-menu-item-input-to-flat-command-menu-item-to-update-or-throw.util';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
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';
@Injectable()
export class CommandMenuItemService {
constructor(
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly applicationService: ApplicationService,
) {}
async findAll(workspaceId: string): Promise<CommandMenuItemDTO[]> {
const { flatCommandMenuItemMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatCommandMenuItemMaps'],
},
);
return Object.values(flatCommandMenuItemMaps.byId)
.filter(isDefined)
.sort((a, b) => a.label.localeCompare(b.label))
.map(fromFlatCommandMenuItemToCommandMenuItemDto);
}
async findById(
id: string,
workspaceId: string,
): Promise<CommandMenuItemDTO | null> {
const { flatCommandMenuItemMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatCommandMenuItemMaps'],
},
);
const flatCommandMenuItem = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
flatEntityMaps: flatCommandMenuItemMaps,
});
if (!isDefined(flatCommandMenuItem)) {
return null;
}
return fromFlatCommandMenuItemToCommandMenuItemDto(flatCommandMenuItem);
}
async findByIdOrThrow(
id: string,
workspaceId: string,
): Promise<CommandMenuItemDTO> {
const commandMenuItem = await this.findById(id, workspaceId);
if (!isDefined(commandMenuItem)) {
throw new CommandMenuItemException(
'Command menu item not found',
CommandMenuItemExceptionCode.COMMAND_MENU_ITEM_NOT_FOUND,
);
}
return commandMenuItem;
}
async create(
input: CreateCommandMenuItemInput,
workspaceId: string,
): Promise<CommandMenuItemDTO> {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const flatCommandMenuItemToCreate =
fromCreateCommandMenuItemInputToFlatCommandMenuItemToCreate({
createCommandMenuItemInput: input,
workspaceId,
applicationId: workspaceCustomFlatApplication.id,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
commandMenuItem: {
flatEntityToCreate: [flatCommandMenuItemToCreate],
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
isSystemBuild: false,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while creating command menu item',
);
}
const { flatCommandMenuItemMaps: recomputedFlatCommandMenuItemMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatCommandMenuItemMaps'],
},
);
return fromFlatCommandMenuItemToCommandMenuItemDto(
findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatCommandMenuItemToCreate.id,
flatEntityMaps: recomputedFlatCommandMenuItemMaps,
}),
);
}
async update(
input: UpdateCommandMenuItemInput,
workspaceId: string,
): Promise<CommandMenuItemDTO> {
const { flatCommandMenuItemMaps: existingFlatCommandMenuItemMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatCommandMenuItemMaps'],
},
);
const flatCommandMenuItemToUpdate =
fromUpdateCommandMenuItemInputToFlatCommandMenuItemToUpdateOrThrow({
flatCommandMenuItemMaps: existingFlatCommandMenuItemMaps,
updateCommandMenuItemInput: input,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
commandMenuItem: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [flatCommandMenuItemToUpdate],
},
},
workspaceId,
isSystemBuild: false,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating command menu item',
);
}
const { flatCommandMenuItemMaps: recomputedFlatCommandMenuItemMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatCommandMenuItemMaps'],
},
);
return fromFlatCommandMenuItemToCommandMenuItemDto(
findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: input.id,
flatEntityMaps: recomputedFlatCommandMenuItemMaps,
}),
);
}
async delete(id: string, workspaceId: string): Promise<CommandMenuItemDTO> {
const { flatCommandMenuItemMaps: existingFlatCommandMenuItemMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatCommandMenuItemMaps'],
},
);
const flatCommandMenuItemToDelete =
fromDeleteCommandMenuItemInputToFlatCommandMenuItemOrThrow({
flatCommandMenuItemMaps: existingFlatCommandMenuItemMaps,
commandMenuItemId: id,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
commandMenuItem: {
flatEntityToCreate: [],
flatEntityToDelete: [flatCommandMenuItemToDelete],
flatEntityToUpdate: [],
},
},
workspaceId,
isSystemBuild: false,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while deleting command menu item',
);
}
return fromFlatCommandMenuItemToCommandMenuItemDto(
flatCommandMenuItemToDelete,
);
}
async findAllFlatCommandMenuItems(
workspaceId: string,
): Promise<FlatCommandMenuItem[]> {
const { flatCommandMenuItemMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatCommandMenuItemMaps'],
},
);
return Object.values(flatCommandMenuItemMaps.byId)
.filter(isDefined)
.sort((a, b) => a.label.localeCompare(b.label));
}
async findByWorkflowVersionId(
workflowVersionId: string,
workspaceId: string,
): Promise<CommandMenuItemDTO | null> {
const { flatCommandMenuItemMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatCommandMenuItemMaps'],
},
);
const flatCommandMenuItem = Object.values(
flatCommandMenuItemMaps.byId,
).find(
(item) => isDefined(item) && item.workflowVersionId === workflowVersionId,
);
if (!isDefined(flatCommandMenuItem)) {
return null;
}
return fromFlatCommandMenuItemToCommandMenuItemDto(flatCommandMenuItem);
}
}
@@ -0,0 +1,73 @@
import {
Field,
HideField,
ObjectType,
registerEnumType,
} from '@nestjs/graphql';
import {
IsBoolean,
IsDateString,
IsEnum,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
} from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
registerEnumType(CommandMenuItemAvailabilityType, {
name: 'CommandMenuItemAvailabilityType',
});
@ObjectType('CommandMenuItem')
export class CommandMenuItemDTO {
@IsUUID()
@IsNotEmpty()
@Field(() => UUIDScalarType)
id: string;
@IsUUID()
@IsNotEmpty()
@Field(() => UUIDScalarType)
workflowVersionId: string;
@IsString()
@IsNotEmpty()
@Field()
label: string;
@IsString()
@IsOptional()
@Field({ nullable: true })
icon?: string;
@IsBoolean()
@Field()
isPinned: boolean;
@IsEnum(CommandMenuItemAvailabilityType)
@Field(() => CommandMenuItemAvailabilityType)
availabilityType: CommandMenuItemAvailabilityType;
@IsUUID()
@IsOptional()
@Field(() => UUIDScalarType, { nullable: true })
availabilityObjectMetadataId?: string;
@HideField()
workspaceId: string;
@Field(() => UUIDScalarType, { nullable: true })
applicationId?: string;
@IsDateString()
@Field()
createdAt: Date;
@IsDateString()
@Field()
updatedAt: Date;
}
@@ -0,0 +1,46 @@
import { Field, InputType } from '@nestjs/graphql';
import {
IsBoolean,
IsEnum,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
} from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
@InputType()
export class CreateCommandMenuItemInput {
@IsUUID()
@IsNotEmpty()
@Field(() => UUIDScalarType)
workflowVersionId: string;
@IsString()
@IsNotEmpty()
@Field()
label: string;
@IsString()
@IsOptional()
@Field({ nullable: true })
icon?: string;
@IsBoolean()
@IsOptional()
@Field({ nullable: true })
isPinned?: boolean;
@IsEnum(CommandMenuItemAvailabilityType)
@IsOptional()
@Field(() => CommandMenuItemAvailabilityType, { nullable: true })
availabilityType?: CommandMenuItemAvailabilityType;
@IsUUID()
@IsOptional()
@Field(() => UUIDScalarType, { nullable: true })
availabilityObjectMetadataId?: string;
}
@@ -0,0 +1,46 @@
import { Field, InputType } from '@nestjs/graphql';
import {
IsBoolean,
IsEnum,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
} from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
@InputType()
export class UpdateCommandMenuItemInput {
@IsUUID()
@IsNotEmpty()
@Field(() => UUIDScalarType)
id: string;
@IsString()
@IsOptional()
@Field({ nullable: true })
label?: string;
@IsString()
@IsOptional()
@Field({ nullable: true })
icon?: string;
@IsBoolean()
@IsOptional()
@Field({ nullable: true })
isPinned?: boolean;
@IsEnum(CommandMenuItemAvailabilityType)
@IsOptional()
@Field(() => CommandMenuItemAvailabilityType, { nullable: true })
availabilityType?: CommandMenuItemAvailabilityType;
@IsUUID()
@IsOptional()
@Field(() => UUIDScalarType, { nullable: true })
availabilityObjectMetadataId?: string;
}
@@ -0,0 +1,69 @@
import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
type Relation,
UpdateDateColumn,
} from 'typeorm';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { SyncableEntityRequired } from 'src/engine/workspace-manager/types/syncable-entity-required.interface';
export enum CommandMenuItemAvailabilityType {
GLOBAL = 'GLOBAL',
SINGLE_RECORD = 'SINGLE_RECORD',
BULK_RECORDS = 'BULK_RECORDS',
}
@Entity({ name: 'commandMenuItem', schema: 'core' })
@Index('IDX_COMMAND_MENU_ITEM_WORKFLOW_VERSION_ID_WORKSPACE_ID', [
'workflowVersionId',
'workspaceId',
])
export class CommandMenuItemEntity
extends SyncableEntityRequired
implements Required<CommandMenuItemEntity>
{
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: false, type: 'uuid' })
workflowVersionId: string;
@Column({ nullable: false })
label: string;
@Column({ nullable: true, type: 'varchar' })
icon: string | null;
@Column({ default: false })
isPinned: boolean;
@Column({
type: 'enum',
enum: CommandMenuItemAvailabilityType,
nullable: false,
default: CommandMenuItemAvailabilityType.GLOBAL,
})
availabilityType: CommandMenuItemAvailabilityType;
@Column({ nullable: true, type: 'uuid' })
availabilityObjectMetadataId: string | null;
@ManyToOne(() => ObjectMetadataEntity, {
onDelete: 'CASCADE',
nullable: true,
})
@JoinColumn({ name: 'availabilityObjectMetadataId' })
availabilityObjectMetadata: Relation<ObjectMetadataEntity> | null;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
}
@@ -0,0 +1,24 @@
import {
type CallHandler,
type ExecutionContext,
Injectable,
type NestInterceptor,
} from '@nestjs/common';
import { type Observable, catchError } from 'rxjs';
import { commandMenuItemGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/command-menu-item/utils/command-menu-item-graphql-api-exception-handler.util';
@Injectable()
export class CommandMenuItemGraphqlApiExceptionInterceptor
implements NestInterceptor
{
intercept(
_context: ExecutionContext,
next: CallHandler,
): Observable<unknown> {
return next
.handle()
.pipe(catchError(commandMenuItemGraphqlApiExceptionHandler));
}
}
@@ -0,0 +1,26 @@
import { assertUnreachable } from 'twenty-shared/utils';
import {
NotFoundError,
UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import {
CommandMenuItemException,
CommandMenuItemExceptionCode,
} from 'src/engine/metadata-modules/command-menu-item/command-menu-item.exception';
export const commandMenuItemGraphqlApiExceptionHandler = (error: Error) => {
if (error instanceof CommandMenuItemException) {
switch (error.code) {
case CommandMenuItemExceptionCode.COMMAND_MENU_ITEM_NOT_FOUND:
throw new NotFoundError(error);
case CommandMenuItemExceptionCode.INVALID_COMMAND_MENU_ITEM_INPUT:
throw new UserInputError(error);
default: {
return assertUnreachable(error.code);
}
}
}
throw error;
};
@@ -0,0 +1,9 @@
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
export const FLAT_COMMAND_MENU_ITEM_EDITABLE_PROPERTIES = [
'label',
'icon',
'isPinned',
'availabilityType',
'availabilityObjectMetadataId',
] as const satisfies (keyof FlatCommandMenuItem)[];
@@ -0,0 +1,16 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CommandMenuItemEntity } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
import { WorkspaceFlatCommandMenuItemMapCacheService } from 'src/engine/metadata-modules/flat-command-menu-item/services/workspace-flat-command-menu-item-map-cache.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
@Module({
imports: [
TypeOrmModule.forFeature([CommandMenuItemEntity]),
WorkspaceManyOrAllFlatEntityMapsCacheModule,
],
providers: [WorkspaceFlatCommandMenuItemMapCacheService],
exports: [WorkspaceFlatCommandMenuItemMapCacheService],
})
export class FlatCommandMenuItemModule {}
@@ -0,0 +1,45 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
import { CommandMenuItemEntity } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
import { type FlatCommandMenuItemMaps } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item-maps.type';
import { fromCommandMenuItemEntityToFlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/utils/from-command-menu-item-entity-to-flat-command-menu-item.util';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@Injectable()
@WorkspaceCache('flatCommandMenuItemMaps')
export class WorkspaceFlatCommandMenuItemMapCacheService extends WorkspaceCacheProvider<FlatCommandMenuItemMaps> {
constructor(
@InjectRepository(CommandMenuItemEntity)
private readonly commandMenuItemRepository: Repository<CommandMenuItemEntity>,
) {
super();
}
async computeForCache(workspaceId: string): Promise<FlatCommandMenuItemMaps> {
const commandMenuItems = await this.commandMenuItemRepository.find({
where: { workspaceId },
withDeleted: true,
});
const flatCommandMenuItemMaps = createEmptyFlatEntityMaps();
for (const commandMenuItemEntity of commandMenuItems) {
const flatCommandMenuItem =
fromCommandMenuItemEntityToFlatCommandMenuItem(commandMenuItemEntity);
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatCommandMenuItem,
flatEntityMapsToMutate: flatCommandMenuItemMaps,
});
}
return flatCommandMenuItemMaps;
}
}
@@ -0,0 +1,4 @@
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
export type FlatCommandMenuItemMaps = FlatEntityMaps<FlatCommandMenuItem>;
@@ -0,0 +1,4 @@
import { type CommandMenuItemEntity } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
export type FlatCommandMenuItem = FlatEntityFrom<CommandMenuItemEntity>;
@@ -0,0 +1,22 @@
import { type CommandMenuItemEntity } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
export const fromCommandMenuItemEntityToFlatCommandMenuItem = (
commandMenuItemEntity: CommandMenuItemEntity,
): FlatCommandMenuItem => {
return {
id: commandMenuItemEntity.id,
workflowVersionId: commandMenuItemEntity.workflowVersionId,
label: commandMenuItemEntity.label,
icon: commandMenuItemEntity.icon,
isPinned: commandMenuItemEntity.isPinned,
availabilityType: commandMenuItemEntity.availabilityType,
availabilityObjectMetadataId:
commandMenuItemEntity.availabilityObjectMetadataId,
workspaceId: commandMenuItemEntity.workspaceId,
universalIdentifier: commandMenuItemEntity.universalIdentifier,
applicationId: commandMenuItemEntity.applicationId,
createdAt: commandMenuItemEntity.createdAt.toISOString(),
updatedAt: commandMenuItemEntity.updatedAt.toISOString(),
};
};
@@ -0,0 +1,36 @@
import { v4 as uuidv4 } from 'uuid';
import { type CreateCommandMenuItemInput } from 'src/engine/metadata-modules/command-menu-item/dtos/create-command-menu-item.input';
import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
export const fromCreateCommandMenuItemInputToFlatCommandMenuItemToCreate = ({
createCommandMenuItemInput,
workspaceId,
applicationId,
}: {
createCommandMenuItemInput: CreateCommandMenuItemInput;
workspaceId: string;
applicationId: string;
}): FlatCommandMenuItem => {
const id = uuidv4();
const now = new Date().toISOString();
return {
id,
universalIdentifier: id,
workflowVersionId: createCommandMenuItemInput.workflowVersionId,
label: createCommandMenuItemInput.label,
icon: createCommandMenuItemInput.icon ?? null,
isPinned: createCommandMenuItemInput.isPinned ?? false,
availabilityType:
createCommandMenuItemInput.availabilityType ??
CommandMenuItemAvailabilityType.GLOBAL,
availabilityObjectMetadataId:
createCommandMenuItemInput.availabilityObjectMetadataId ?? null,
workspaceId,
applicationId,
createdAt: now,
updatedAt: now,
};
};
@@ -0,0 +1,31 @@
import { isDefined } from 'twenty-shared/utils';
import {
CommandMenuItemException,
CommandMenuItemExceptionCode,
} from 'src/engine/metadata-modules/command-menu-item/command-menu-item.exception';
import { type FlatCommandMenuItemMaps } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item-maps.type';
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
export const fromDeleteCommandMenuItemInputToFlatCommandMenuItemOrThrow = ({
flatCommandMenuItemMaps,
commandMenuItemId,
}: {
flatCommandMenuItemMaps: FlatCommandMenuItemMaps;
commandMenuItemId: string;
}): FlatCommandMenuItem => {
const existingFlatCommandMenuItem = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: commandMenuItemId,
flatEntityMaps: flatCommandMenuItemMaps,
});
if (!isDefined(existingFlatCommandMenuItem)) {
throw new CommandMenuItemException(
'Command menu item not found',
CommandMenuItemExceptionCode.COMMAND_MENU_ITEM_NOT_FOUND,
);
}
return existingFlatCommandMenuItem;
};
@@ -0,0 +1,19 @@
import { type CommandMenuItemDTO } from 'src/engine/metadata-modules/command-menu-item/dtos/command-menu-item.dto';
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
export const fromFlatCommandMenuItemToCommandMenuItemDto = (
flatCommandMenuItem: FlatCommandMenuItem,
): CommandMenuItemDTO => ({
id: flatCommandMenuItem.id,
workflowVersionId: flatCommandMenuItem.workflowVersionId,
label: flatCommandMenuItem.label,
icon: flatCommandMenuItem.icon ?? undefined,
isPinned: flatCommandMenuItem.isPinned,
availabilityType: flatCommandMenuItem.availabilityType,
availabilityObjectMetadataId:
flatCommandMenuItem.availabilityObjectMetadataId ?? undefined,
workspaceId: flatCommandMenuItem.workspaceId,
applicationId: flatCommandMenuItem.applicationId ?? undefined,
createdAt: new Date(flatCommandMenuItem.createdAt),
updatedAt: new Date(flatCommandMenuItem.updatedAt),
});
@@ -0,0 +1,44 @@
import { isDefined } from 'twenty-shared/utils';
import {
CommandMenuItemException,
CommandMenuItemExceptionCode,
} from 'src/engine/metadata-modules/command-menu-item/command-menu-item.exception';
import { type UpdateCommandMenuItemInput } from 'src/engine/metadata-modules/command-menu-item/dtos/update-command-menu-item.input';
import { FLAT_COMMAND_MENU_ITEM_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-command-menu-item/constants/flat-command-menu-item-editable-properties.constant';
import { type FlatCommandMenuItemMaps } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item-maps.type';
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
export const fromUpdateCommandMenuItemInputToFlatCommandMenuItemToUpdateOrThrow =
({
flatCommandMenuItemMaps,
updateCommandMenuItemInput,
}: {
flatCommandMenuItemMaps: FlatCommandMenuItemMaps;
updateCommandMenuItemInput: UpdateCommandMenuItemInput;
}): FlatCommandMenuItem => {
const existingFlatCommandMenuItem = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: updateCommandMenuItemInput.id,
flatEntityMaps: flatCommandMenuItemMaps,
});
if (!isDefined(existingFlatCommandMenuItem)) {
throw new CommandMenuItemException(
'Command menu item not found',
CommandMenuItemExceptionCode.COMMAND_MENU_ITEM_NOT_FOUND,
);
}
const { id: _id, ...updates } = updateCommandMenuItemInput;
return {
...mergeUpdateInExistingRecord({
existing: existingFlatCommandMenuItem,
properties: [...FLAT_COMMAND_MENU_ITEM_EDITABLE_PROPERTIES],
update: updates,
}),
updatedAt: new Date().toISOString(),
};
};
@@ -14,6 +14,7 @@ import { FLAT_ROLE_TARGET_EDITABLE_PROPERTIES } from 'src/engine/metadata-module
import { FLAT_ROLE_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-role/constants/flat-role-editable-properties.constant';
import { FLAT_ROW_LEVEL_PERMISSION_PREDICATE_GROUP_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-row-level-permission-predicate-group/constants/flat-row-level-permission-predicate-group-editable-properties.constant';
import { FLAT_ROW_LEVEL_PERMISSION_PREDICATE_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-row-level-permission-predicate/constants/flat-row-level-permission-predicate-editable-properties.constant';
import { FLAT_COMMAND_MENU_ITEM_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-command-menu-item/constants/flat-command-menu-item-editable-properties.constant';
import { FLAT_SKILL_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-skill/constants/flat-skill-editable-properties.constant';
import { FLAT_VIEW_FIELD_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-field/constants/flat-view-field-editable-properties.constant';
import { FLAT_VIEW_FILTER_GROUP_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-filter-group/constants/flat-view-filter-group-editable-properties.constant';
@@ -141,6 +142,10 @@ export const ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY = {
propertiesToCompare: [...FLAT_SKILL_EDITABLE_PROPERTIES],
propertiesToStringify: [],
},
commandMenuItem: {
propertiesToCompare: [...FLAT_COMMAND_MENU_ITEM_EDITABLE_PROPERTIES],
propertiesToStringify: [],
},
rowLevelPermissionPredicate: {
propertiesToCompare: [
...FLAT_ROW_LEVEL_PERMISSION_PREDICATE_EDITABLE_PROPERTIES,
@@ -79,6 +79,18 @@ export const ALL_METADATA_RELATIONS = {
},
oneToMany: {},
},
commandMenuItem: {
manyToOne: {
workspace: null,
application: null,
availabilityObjectMetadata: {
metadataName: 'objectMetadata',
flatEntityForeignKeyAggregator: null,
foreignKey: 'availabilityObjectMetadataId',
},
},
oneToMany: {},
},
fieldMetadata: {
manyToOne: {
object: {
@@ -62,6 +62,9 @@ export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = {
role: true,
},
skill: {},
commandMenuItem: {
objectMetadata: true,
},
pageLayout: {
objectMetadata: true,
},
@@ -1,13 +1,14 @@
import { type AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component.type';
import { type FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
import { type CommandMenuItemEntity } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
import { type CronTriggerEntity } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity';
import { type FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type';
import { type DatabaseEventTriggerEntity } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity';
import { type FlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger.type';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component.type';
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
@@ -21,6 +22,7 @@ import { type FlatViewFilterGroup } from 'src/engine/metadata-modules/flat-view-
import { type FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type';
import { type FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type';
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
import { type FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
import { type IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { type PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity';
@@ -47,6 +49,11 @@ import {
type DeleteAgentAction,
type UpdateAgentAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/agent/types/workspace-migration-agent-action-builder.service';
import {
type CreateCommandMenuItemAction,
type DeleteCommandMenuItemAction,
type UpdateCommandMenuItemAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/command-menu-item/types/workspace-migration-command-menu-item-action.type';
import {
type CreateCronTriggerAction,
type DeleteCronTriggerAction,
@@ -62,6 +69,11 @@ import {
type DeleteFieldAction,
type UpdateFieldAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action';
import {
type CreateFrontComponentAction,
type DeleteFrontComponentAction,
type UpdateFrontComponentAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/types/workspace-migration-front-component-action.type';
import {
type CreateIndexAction,
type DeleteIndexAction,
@@ -147,11 +159,6 @@ import {
type DeleteViewAction,
type UpdateViewAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view/types/workspace-migration-view-action.type';
import {
type CreateFrontComponentAction,
type DeleteFrontComponentAction,
type UpdateFrontComponentAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/types/workspace-migration-front-component-action.type';
export type AllFlatEntityTypesByMetadataName = {
fieldMetadata: {
@@ -316,6 +323,15 @@ export type AllFlatEntityTypesByMetadataName = {
flatEntity: FlatSkill;
entity: SkillEntity;
};
commandMenuItem: {
actions: {
create: CreateCommandMenuItemAction;
update: UpdateCommandMenuItemAction;
delete: DeleteCommandMenuItemAction;
};
flatEntity: FlatCommandMenuItem;
entity: CommandMenuItemEntity;
};
pageLayout: {
actions: {
create: CreatePageLayoutAction;
@@ -2,6 +2,12 @@
exports[`getMetadataRelatedMetadataNames should return related metadata names for agent 1`] = `[]`;
exports[`getMetadataRelatedMetadataNames should return related metadata names for commandMenuItem 1`] = `
[
"objectMetadata",
]
`;
exports[`getMetadataRelatedMetadataNames should return related metadata names for cronTrigger 1`] = `
[
"serverlessFunction",
@@ -3,6 +3,7 @@ import { Module } from '@nestjs/common';
import { AiAgentMonitorModule } from 'src/engine/metadata-modules/ai/ai-agent-monitor/ai-agent-monitor.module';
import { AiAgentModule } from 'src/engine/metadata-modules/ai/ai-agent/ai-agent.module';
import { AiChatModule } from 'src/engine/metadata-modules/ai/ai-chat/ai-chat.module';
import { CommandMenuItemModule } from 'src/engine/metadata-modules/command-menu-item/command-menu-item.module';
import { CronTriggerModule } from 'src/engine/metadata-modules/cron-trigger/cron-trigger.module';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { DatabaseEventTriggerModule } from 'src/engine/metadata-modules/database-event-trigger/database-event-trigger.module';
@@ -29,6 +30,7 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work
ServerlessFunctionModule,
ServerlessFunctionLayerModule,
SkillModule,
CommandMenuItemModule,
AiAgentModule,
AiAgentMonitorModule,
AiChatModule,
@@ -49,6 +51,7 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work
SearchFieldMetadataModule,
ServerlessFunctionModule,
SkillModule,
CommandMenuItemModule,
AiAgentModule,
AiChatModule,
ViewModule,
@@ -222,6 +222,7 @@ describe('WorkspaceEntityManager', () => {
IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED: false,
IS_IF_ELSE_ENABLED: false,
IS_SSE_DB_EVENTS_ENABLED: false,
IS_COMMAND_MENU_ITEM_ENABLED: false,
},
userWorkspaceRoleMap: {},
eventEmitterService: {
@@ -32,6 +32,7 @@ export const WORKSPACE_CACHE_KEYS_V2 = {
ORMEntityMetadatas: 'orm:entity-metadatas',
flatAgentMaps: 'flat-maps:agent',
flatSkillMaps: 'flat-maps:skill',
flatCommandMenuItemMaps: 'flat-maps:command-menu-item',
flatRoleTargetByAgentIdMaps: 'flat-maps:flatRoleTargetByAgentId',
flatPageLayoutMaps: 'flat-maps:page-layout',
flatPageLayoutWidgetMaps: 'flat-maps:page-layout-widget',
@@ -15,6 +15,7 @@ import {
} from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type';
import { aggregateOrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration/utils/aggregate-orchestrator-actions-report.util';
import { WorkspaceMigrationAgentActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/agent/workspace-migration-agent-actions-builder.service';
import { WorkspaceMigrationCommandMenuItemActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/command-menu-item/workspace-migration-command-menu-item-actions-builder.service';
import { WorkspaceMigrationCronTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/cron-trigger/workspace-migration-cron-trigger-action-builder.service';
import { WorkspaceMigrationDatabaseEventTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/database-event-trigger/workspace-migration-database-event-trigger-actions-builder.service';
import { WorkspaceMigrationFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/workspace-migration-field-actions-builder.service';
@@ -56,6 +57,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
private readonly workspaceMigrationRoleActionsBuilderService: WorkspaceMigrationRoleActionsBuilderService,
private readonly workspaceMigrationAgentActionsBuilderService: WorkspaceMigrationAgentActionsBuilderService,
private readonly workspaceMigrationSkillActionsBuilderService: WorkspaceMigrationSkillActionsBuilderService,
private readonly workspaceMigrationCommandMenuItemActionsBuilderService: WorkspaceMigrationCommandMenuItemActionsBuilderService,
private readonly workspaceMigrationPageLayoutActionsBuilderService: WorkspaceMigrationPageLayoutActionsBuilderService,
private readonly workspaceMigrationPageLayoutWidgetActionsBuilderService: WorkspaceMigrationPageLayoutWidgetActionsBuilderService,
private readonly workspaceMigrationPageLayoutTabActionsBuilderService: WorkspaceMigrationPageLayoutTabActionsBuilderService,
@@ -158,6 +160,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
flatRoleTargetMaps,
flatAgentMaps,
flatSkillMaps,
flatCommandMenuItemMaps,
flatPageLayoutMaps,
flatPageLayoutWidgetMaps,
flatPageLayoutTabMaps,
@@ -817,6 +820,45 @@ export class WorkspaceMigrationBuildOrchestratorService {
}
}
if (isDefined(flatCommandMenuItemMaps)) {
const {
from: fromFlatCommandMenuItemMaps,
to: toFlatCommandMenuItemMaps,
} = flatCommandMenuItemMaps;
const commandMenuItemResult =
await this.workspaceMigrationCommandMenuItemActionsBuilderService.validateAndBuild(
{
additionalCacheDataMaps,
from: fromFlatCommandMenuItemMaps,
to: toFlatCommandMenuItemMaps,
buildOptions,
dependencyOptimisticFlatEntityMaps: {
flatObjectMetadataMaps:
optimisticAllFlatEntityMaps.flatObjectMetadataMaps,
},
workspaceId,
},
);
this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation(
{
allFlatEntityMaps: optimisticAllFlatEntityMaps,
flatEntityMapsAndRelatedFlatEntityMaps:
commandMenuItemResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps,
},
);
if (commandMenuItemResult.status === 'fail') {
orchestratorFailureReport.commandMenuItem.push(
...commandMenuItemResult.errors,
);
} else {
orchestratorActionsReport.commandMenuItem =
commandMenuItemResult.actions;
}
}
if (isDefined(flatPageLayoutMaps)) {
const { from: fromFlatPageLayoutMaps, to: toFlatPageLayoutMaps } =
flatPageLayoutMaps;
@@ -1070,6 +1112,12 @@ export class WorkspaceMigrationBuildOrchestratorService {
...aggregatedOrchestratorActionsReport.skill.update,
///
// Command Menu Items
...aggregatedOrchestratorActionsReport.commandMenuItem.delete,
...aggregatedOrchestratorActionsReport.commandMenuItem.create,
...aggregatedOrchestratorActionsReport.commandMenuItem.update,
///
// Page layouts
...aggregatedOrchestratorActionsReport.pageLayout.delete,
...aggregatedOrchestratorActionsReport.pageLayout.create,
@@ -0,0 +1,12 @@
import { type BaseCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-create-workspace-migration-action.type';
import { type BaseDeleteWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-delete-workspace-migration-action.type';
import { type BaseUpdateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-update-workspace-migration-action.type';
export type CreateCommandMenuItemAction =
BaseCreateWorkspaceMigrationAction<'commandMenuItem'>;
export type UpdateCommandMenuItemAction =
BaseUpdateWorkspaceMigrationAction<'commandMenuItem'>;
export type DeleteCommandMenuItemAction =
BaseDeleteWorkspaceMigrationAction<'commandMenuItem'>;
@@ -0,0 +1,116 @@
import { Injectable } from '@nestjs/common';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { UpdateCommandMenuItemAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/command-menu-item/types/workspace-migration-command-menu-item-action.type';
import { WorkspaceEntityMigrationBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/services/workspace-entity-migration-builder.service';
import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/flat-entity-update-validation-args.type';
import { FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/flat-entity-validation-args.type';
import { FlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/flat-entity-validation-result.type';
import { FlatCommandMenuItemValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-command-menu-item-validator.service';
@Injectable()
export class WorkspaceMigrationCommandMenuItemActionsBuilderService extends WorkspaceEntityMigrationBuilderService<
typeof ALL_METADATA_NAME.commandMenuItem
> {
constructor(
private readonly flatCommandMenuItemValidatorService: FlatCommandMenuItemValidatorService,
) {
super(ALL_METADATA_NAME.commandMenuItem);
}
protected validateFlatEntityCreation(
args: FlatEntityValidationArgs<typeof ALL_METADATA_NAME.commandMenuItem>,
): FlatEntityValidationReturnType<
typeof ALL_METADATA_NAME.commandMenuItem,
'create'
> {
const validationResult =
this.flatCommandMenuItemValidatorService.validateFlatCommandMenuItemCreation(
args,
);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityToValidate: flatCommandMenuItemToValidate } = args;
return {
status: 'success',
action: {
type: 'create',
metadataName: 'commandMenuItem',
flatEntity: flatCommandMenuItemToValidate,
},
};
}
protected validateFlatEntityDeletion(
args: FlatEntityValidationArgs<typeof ALL_METADATA_NAME.commandMenuItem>,
): FlatEntityValidationReturnType<
typeof ALL_METADATA_NAME.commandMenuItem,
'delete'
> {
const validationResult =
this.flatCommandMenuItemValidatorService.validateFlatCommandMenuItemDeletion(
args,
);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityToValidate: flatCommandMenuItemToValidate } = args;
return {
status: 'success',
action: {
type: 'delete',
metadataName: 'commandMenuItem',
entityId: flatCommandMenuItemToValidate.id,
},
};
}
protected validateFlatEntityUpdate(
args: FlatEntityUpdateValidationArgs<
typeof ALL_METADATA_NAME.commandMenuItem
>,
): FlatEntityValidationReturnType<
typeof ALL_METADATA_NAME.commandMenuItem,
'update'
> {
const validationResult =
this.flatCommandMenuItemValidatorService.validateFlatCommandMenuItemUpdate(
args,
);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityId, flatEntityUpdates } = args;
const updateCommandMenuItemAction: UpdateCommandMenuItemAction = {
type: 'update',
metadataName: 'commandMenuItem',
entityId: flatEntityId,
updates: flatEntityUpdates,
};
return {
status: 'success',
action: updateCommandMenuItemAction,
};
}
}
@@ -0,0 +1,124 @@
import { Injectable } from '@nestjs/common';
import { msg, t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { CommandMenuItemExceptionCode } from 'src/engine/metadata-modules/command-menu-item/command-menu-item.exception';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { findFlatEntityPropertyUpdate } from 'src/engine/workspace-manager/workspace-migration/utils/find-flat-entity-property-update.util';
import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type';
import { getEmptyFlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/utils/get-flat-entity-validation-error.util';
import { type FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/flat-entity-update-validation-args.type';
import { type FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/flat-entity-validation-args.type';
@Injectable()
export class FlatCommandMenuItemValidatorService {
public validateFlatCommandMenuItemCreation({
flatEntityToValidate: flatCommandMenuItem,
}: FlatEntityValidationArgs<
typeof ALL_METADATA_NAME.commandMenuItem
>): FailedFlatEntityValidation<'commandMenuItem', 'create'> {
const validationResult = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
id: flatCommandMenuItem.id,
universalIdentifier: flatCommandMenuItem.universalIdentifier,
},
metadataName: 'commandMenuItem',
type: 'create',
});
if (!isNonEmptyString(flatCommandMenuItem.label)) {
validationResult.errors.push({
code: CommandMenuItemExceptionCode.INVALID_COMMAND_MENU_ITEM_INPUT,
message: t`Label is required`,
userFriendlyMessage: msg`Label is required`,
});
}
return validationResult;
}
public validateFlatCommandMenuItemDeletion({
flatEntityToValidate,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatCommandMenuItemMaps: optimisticFlatCommandMenuItemMaps,
},
}: FlatEntityValidationArgs<
typeof ALL_METADATA_NAME.commandMenuItem
>): FailedFlatEntityValidation<'commandMenuItem', 'delete'> {
const validationResult = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
id: flatEntityToValidate.id,
universalIdentifier: flatEntityToValidate.universalIdentifier,
},
metadataName: 'commandMenuItem',
type: 'delete',
});
const existingCommandMenuItem = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: flatEntityToValidate.id,
flatEntityMaps: optimisticFlatCommandMenuItemMaps,
});
if (!isDefined(existingCommandMenuItem)) {
validationResult.errors.push({
code: CommandMenuItemExceptionCode.COMMAND_MENU_ITEM_NOT_FOUND,
message: t`Command menu item not found`,
userFriendlyMessage: msg`Command menu item not found`,
});
}
return validationResult;
}
public validateFlatCommandMenuItemUpdate({
flatEntityId,
flatEntityUpdates,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatCommandMenuItemMaps: optimisticFlatCommandMenuItemMaps,
},
}: FlatEntityUpdateValidationArgs<
typeof ALL_METADATA_NAME.commandMenuItem
>): FailedFlatEntityValidation<'commandMenuItem', 'update'> {
const fromFlatCommandMenuItem = findFlatEntityByIdInFlatEntityMaps({
flatEntityId,
flatEntityMaps: optimisticFlatCommandMenuItemMaps,
});
const validationResult = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
id: flatEntityId,
universalIdentifier: fromFlatCommandMenuItem?.universalIdentifier,
},
metadataName: 'commandMenuItem',
type: 'update',
});
if (!isDefined(fromFlatCommandMenuItem)) {
validationResult.errors.push({
code: CommandMenuItemExceptionCode.COMMAND_MENU_ITEM_NOT_FOUND,
message: t`Command menu item not found`,
userFriendlyMessage: msg`Command menu item not found`,
});
return validationResult;
}
const labelUpdate = findFlatEntityPropertyUpdate({
flatEntityUpdates,
property: 'label',
});
if (isDefined(labelUpdate) && !isNonEmptyString(labelUpdate.to)) {
validationResult.errors.push({
code: CommandMenuItemExceptionCode.INVALID_COMMAND_MENU_ITEM_INPUT,
message: t`Label is required`,
userFriendlyMessage: msg`Label is required`,
});
}
return validationResult;
}
}
@@ -18,6 +18,7 @@ import { FlatRouteTriggerValidatorService } from 'src/engine/workspace-manager/w
import { FlatRowLevelPermissionPredicateGroupValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-row-level-permission-predicate-group-validator.service';
import { FlatRowLevelPermissionPredicateValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-row-level-permission-predicate-validator.service';
import { FlatServerlessFunctionValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-serverless-function-validator.service';
import { FlatCommandMenuItemValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-command-menu-item-validator.service';
import { FlatSkillValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-skill-validator.service';
import { FlatFrontComponentValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-front-component-validator.service';
import { FlatViewFieldValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-field-validator.service';
@@ -47,6 +48,7 @@ import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace
FlatRoleTargetValidatorService,
FlatAgentValidatorService,
FlatSkillValidatorService,
FlatCommandMenuItemValidatorService,
FlatPageLayoutValidatorService,
FlatPageLayoutWidgetValidatorService,
FlatPageLayoutTabValidatorService,
@@ -72,6 +74,7 @@ import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace
FlatRoleTargetValidatorService,
FlatAgentValidatorService,
FlatSkillValidatorService,
FlatCommandMenuItemValidatorService,
FlatPageLayoutValidatorService,
FlatPageLayoutWidgetValidatorService,
FlatPageLayoutTabValidatorService,
@@ -3,6 +3,7 @@ import { Module } from '@nestjs/common';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FlatFieldMetadataTypeValidatorService } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service';
import { WorkspaceMigrationAgentActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/agent/workspace-migration-agent-actions-builder.service';
import { WorkspaceMigrationCommandMenuItemActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/command-menu-item/workspace-migration-command-menu-item-actions-builder.service';
import { WorkspaceMigrationCronTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/cron-trigger/workspace-migration-cron-trigger-action-builder.service';
import { WorkspaceMigrationDatabaseEventTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/database-event-trigger/workspace-migration-database-event-trigger-actions-builder.service';
import { WorkspaceMigrationFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/workspace-migration-field-actions-builder.service';
@@ -46,6 +47,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
WorkspaceMigrationRoleTargetActionsBuilderService,
WorkspaceMigrationAgentActionsBuilderService,
WorkspaceMigrationSkillActionsBuilderService,
WorkspaceMigrationCommandMenuItemActionsBuilderService,
WorkspaceMigrationPageLayoutActionsBuilderService,
WorkspaceMigrationPageLayoutWidgetActionsBuilderService,
WorkspaceMigrationPageLayoutTabActionsBuilderService,
@@ -70,6 +72,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
WorkspaceMigrationRoleTargetActionsBuilderService,
WorkspaceMigrationAgentActionsBuilderService,
WorkspaceMigrationSkillActionsBuilderService,
WorkspaceMigrationCommandMenuItemActionsBuilderService,
WorkspaceMigrationPageLayoutActionsBuilderService,
WorkspaceMigrationPageLayoutWidgetActionsBuilderService,
WorkspaceMigrationPageLayoutTabActionsBuilderService,
@@ -0,0 +1,40 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { CommandMenuItemEntity } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
import { CreateCommandMenuItemAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/command-menu-item/types/workspace-migration-command-menu-item-action.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
@Injectable()
export class CreateCommandMenuItemActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'create',
'commandMenuItem',
) {
constructor() {
super();
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<CreateCommandMenuItemAction>,
): Promise<void> {
const { action, queryRunner, workspaceId } = context;
const { flatEntity } = action;
const commandMenuItemRepository =
queryRunner.manager.getRepository<CommandMenuItemEntity>(
CommandMenuItemEntity,
);
await commandMenuItemRepository.insert({
...flatEntity,
workspaceId,
});
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<CreateCommandMenuItemAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,37 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { CommandMenuItemEntity } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
import { DeleteCommandMenuItemAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/command-menu-item/types/workspace-migration-command-menu-item-action.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
@Injectable()
export class DeleteCommandMenuItemActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'delete',
'commandMenuItem',
) {
constructor() {
super();
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<DeleteCommandMenuItemAction>,
): Promise<void> {
const { action, queryRunner, workspaceId } = context;
const { entityId } = action;
const commandMenuItemRepository =
queryRunner.manager.getRepository<CommandMenuItemEntity>(
CommandMenuItemEntity,
);
await commandMenuItemRepository.delete({ id: entityId, workspaceId });
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<DeleteCommandMenuItemAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,39 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { CommandMenuItemEntity } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
import { UpdateCommandMenuItemAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/command-menu-item/types/workspace-migration-command-menu-item-action.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/from-flat-entity-properties-updates-to-partial-flat-entity';
@Injectable()
export class UpdateCommandMenuItemActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'update',
'commandMenuItem',
) {
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<UpdateCommandMenuItemAction>,
): Promise<void> {
const { action, queryRunner, workspaceId } = context;
const { entityId, updates } = action;
const commandMenuItemRepository =
queryRunner.manager.getRepository<CommandMenuItemEntity>(
CommandMenuItemEntity,
);
await commandMenuItemRepository.update(
{ id: entityId, workspaceId },
fromFlatEntityPropertiesUpdatesToPartialFlatEntity({
updates,
}),
);
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<UpdateCommandMenuItemAction>,
): Promise<void> {
return;
}
}
@@ -4,6 +4,9 @@ import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-sc
import { CreateAgentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/agent/services/create-agent-action-handler.service';
import { DeleteAgentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/agent/services/delete-agent-action-handler.service';
import { UpdateAgentActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/agent/services/update-agent-action-handler.service';
import { CreateCommandMenuItemActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/command-menu-item/services/create-command-menu-item-action-handler.service';
import { DeleteCommandMenuItemActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/command-menu-item/services/delete-command-menu-item-action-handler.service';
import { UpdateCommandMenuItemActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/command-menu-item/services/update-command-menu-item-action-handler.service';
import { CreateCronTriggerActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/cron-trigger/services/create-cron-trigger-action-handler.service';
import { DeleteCronTriggerActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/cron-trigger/services/delete-cron-trigger-action-handler.service';
import { UpdateCronTriggerActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/cron-trigger/services/update-cron-trigger-action-handler.service';
@@ -135,6 +138,10 @@ import { UpdateViewActionHandlerService } from 'src/engine/workspace-manager/wor
UpdateSkillActionHandlerService,
DeleteSkillActionHandlerService,
CreateCommandMenuItemActionHandlerService,
UpdateCommandMenuItemActionHandlerService,
DeleteCommandMenuItemActionHandlerService,
CreatePageLayoutActionHandlerService,
UpdatePageLayoutActionHandlerService,
DeletePageLayoutActionHandlerService,
@@ -68,8 +68,9 @@ export const optimisticallyApplyCreateActionOnAllFlatEntityMaps = <
case 'skill':
case 'pageLayout':
case 'pageLayoutWidget':
case 'frontComponent':
case 'pageLayoutTab': {
case 'pageLayoutTab':
case 'commandMenuItem':
case 'frontComponent': {
addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow({
flatEntity: action.flatEntity,
flatEntityAndRelatedMapsToMutate: allFlatEntityMaps,
@@ -45,8 +45,9 @@ export const optimisticallyApplyDeleteActionOnAllFlatEntityMaps = <
case 'skill':
case 'pageLayout':
case 'pageLayoutWidget':
case 'frontComponent':
case 'pageLayoutTab': {
case 'pageLayoutTab':
case 'commandMenuItem':
case 'frontComponent': {
const flatEntityToDelete = findFlatEntityByIdInFlatEntityMapsOrThrow<
MetadataFlatEntity<typeof action.metadataName>
>({
@@ -68,6 +68,7 @@ export const optimisticallyApplyUpdateActionOnAllFlatEntityMaps = <
case 'pageLayout':
case 'pageLayoutWidget':
case 'pageLayoutTab':
case 'commandMenuItem':
case 'frontComponent': {
const flatEntityMapsKey = getMetadataFlatEntityMapsKey(
action.metadataName,
@@ -2,6 +2,8 @@ import { Module } from '@nestjs/common';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { CommandMenuItemModule } from 'src/engine/metadata-modules/command-menu-item/command-menu-item.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
import { WorkflowRunnerModule } from 'src/modules/workflow/workflow-runner/workflow-runner.module';
@@ -14,6 +16,8 @@ import { WorkflowTriggerWorkspaceService } from 'src/modules/workflow/workflow-t
WorkflowCommonModule,
WorkflowRunnerModule,
AutomatedTriggerModule,
CommandMenuItemModule,
FeatureFlagModule,
NestjsQueryTypeOrmModule.forFeature([ObjectMetadataEntity]),
],
providers: [WorkflowTriggerWorkspaceService, WorkflowTriggerJob],
@@ -3,6 +3,10 @@ import { Injectable } from '@nestjs/common';
import { msg } from '@lingui/core/macro';
import { type ActorMetadata } from 'twenty-shared/types';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { CommandMenuItemService } from 'src/engine/metadata-modules/command-menu-item/command-menu-item.service';
import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
@@ -24,7 +28,10 @@ import {
WorkflowTriggerException,
WorkflowTriggerExceptionCode,
} from 'src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception';
import { WorkflowTriggerType } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
import {
type WorkflowManualTrigger,
WorkflowTriggerType,
} from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
import { assertVersionCanBeActivated } from 'src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util';
import { computeCronPatternFromSchedule } from 'src/modules/workflow/workflow-trigger/utils/compute-cron-pattern-from-schedule';
import { assertNever } from 'src/utils/assert';
@@ -37,6 +44,8 @@ export class WorkflowTriggerWorkspaceService {
private readonly workflowRunnerWorkspaceService: WorkflowRunnerWorkspaceService,
private readonly automatedTriggerWorkspaceService: AutomatedTriggerWorkspaceService,
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
private readonly commandMenuItemService: CommandMenuItemService,
private readonly featureFlagService: FeatureFlagService,
) {}
async runWorkflowVersion({
@@ -191,7 +200,7 @@ export class WorkflowTriggerWorkspaceService {
workspaceId,
);
await this.enableTrigger(workflowVersion, workspaceId);
await this.enableTrigger(workflow, workflowVersion, workspaceId);
}
private async performDeactivationSteps(
@@ -306,13 +315,77 @@ export class WorkflowTriggerWorkspaceService {
}
private async enableTrigger(
workflow: WorkflowWorkspaceEntity,
workflowVersion: WorkflowVersionWorkspaceEntity,
workspaceId: string,
) {
assertWorkflowVersionTriggerIsDefined(workflowVersion);
switch (workflowVersion.trigger.type) {
case WorkflowTriggerType.MANUAL:
case WorkflowTriggerType.MANUAL: {
const isCommandMenuItemEnabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
workspaceId,
);
if (!isCommandMenuItemEnabled) {
return;
}
const trigger = workflowVersion.trigger as WorkflowManualTrigger;
const availability = trigger.settings.availability;
let availabilityType = CommandMenuItemAvailabilityType.GLOBAL;
let availabilityObjectMetadataId: string | undefined;
if (availability) {
switch (availability.type) {
case 'GLOBAL':
availabilityType = CommandMenuItemAvailabilityType.GLOBAL;
break;
case 'SINGLE_RECORD':
case 'BULK_RECORDS': {
availabilityType =
availability.type === 'SINGLE_RECORD'
? CommandMenuItemAvailabilityType.SINGLE_RECORD
: CommandMenuItemAvailabilityType.BULK_RECORDS;
const { objectIdByNameSingular } =
await this.workflowCommonWorkspaceService.getFlatEntityMaps(
workspaceId,
);
const objectId =
objectIdByNameSingular[availability.objectNameSingular];
if (!objectId) {
throw new WorkflowTriggerException(
`Object metadata not found for object: ${availability.objectNameSingular}`,
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION,
);
}
availabilityObjectMetadataId = objectId;
break;
}
}
}
await this.commandMenuItemService.create(
{
workflowVersionId: workflowVersion.id,
label: workflow.name ?? 'Manual Trigger',
icon: trigger.settings.icon,
availabilityType,
availabilityObjectMetadataId,
},
workspaceId,
);
return;
}
case WorkflowTriggerType.WEBHOOK:
return;
case WorkflowTriggerType.DATABASE_EVENT: {
@@ -361,7 +434,32 @@ export class WorkflowTriggerWorkspaceService {
});
return;
case WorkflowTriggerType.MANUAL:
case WorkflowTriggerType.MANUAL: {
const isCommandMenuItemEnabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
workspaceId,
);
if (!isCommandMenuItemEnabled) {
return;
}
const existingCommandMenuItem =
await this.commandMenuItemService.findByWorkflowVersionId(
workflowVersion.id,
workspaceId,
);
if (existingCommandMenuItem) {
await this.commandMenuItemService.delete(
existingCommandMenuItem.id,
workspaceId,
);
}
return;
}
case WorkflowTriggerType.WEBHOOK:
return;
default:
@@ -0,0 +1,95 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`CommandMenuItem creation should fail when creating with empty label 1`] = `
{
"extensions": {
"code": "METADATA_VALIDATION_FAILED",
"errors": {
"commandMenuItem": [
{
"errors": [
{
"code": "INVALID_COMMAND_MENU_ITEM_INPUT",
"message": "Label is required",
"userFriendlyMessage": "Label is required",
},
],
"flatEntityMinimalInformation": {
"id": Any<String>,
"universalIdentifier": Any<String>,
},
"metadataName": "commandMenuItem",
"status": "fail",
"type": "create",
},
],
},
"message": "Validation failed for 1 commandMenuItem",
"summary": {
"commandMenuItem": 1,
"totalErrors": 1,
},
"userFriendlyMessage": "Metadata validation failed",
},
"message": "Multiple validation errors occurred while creating command menu item",
"name": "GraphQLError",
}
`;
exports[`CommandMenuItem creation should fail when creating with missing label 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"http": {
"status": 400,
},
"userFriendlyMessage": "An error occurred.",
},
"message": "Field "label" of required type "String!" was not provided.",
"name": "GraphQLError",
}
`;
exports[`CommandMenuItem creation should fail when creating with missing workflowVersionId 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"http": {
"status": 400,
},
"userFriendlyMessage": "An error occurred.",
},
"message": "Field "workflowVersionId" of required type "UUID!" was not provided.",
"name": "GraphQLError",
}
`;
exports[`CommandMenuItem creation should fail when creating with empty workflowVersionId 1`] = `
{
"extensions": {
"code": "GRAPHQL_VALIDATION_FAILED",
"http": {
"status": 400,
},
"userFriendlyMessage": "An error occurred.",
"value": "",
},
"message": "Invalid UUID",
"name": "ValidationError",
}
`;
exports[`CommandMenuItem creation should fail when creating with invalid workflowVersionId (not a UUID) 1`] = `
{
"extensions": {
"code": "GRAPHQL_VALIDATION_FAILED",
"http": {
"status": 400,
},
"userFriendlyMessage": "An error occurred.",
"value": "not-a-valid-uuid",
},
"message": "Invalid UUID",
"name": "ValidationError",
}
`;
@@ -0,0 +1,57 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`CommandMenuItem deletion should fail when deleting a non-existent command menu item 1`] = `
{
"extensions": {
"code": "NOT_FOUND",
"subCode": "COMMAND_MENU_ITEM_NOT_FOUND",
"userFriendlyMessage": "Command menu item not found.",
},
"message": "Command menu item not found",
"name": "NotFoundError",
}
`;
exports[`CommandMenuItem deletion should fail when deleting with missing id 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"http": {
"status": 400,
},
"userFriendlyMessage": "An error occurred.",
},
"message": "Variable "$id" of required type "UUID!" was not provided.",
"name": "GraphQLError",
}
`;
exports[`CommandMenuItem deletion should fail when deleting with empty id 1`] = `
{
"extensions": {
"code": "GRAPHQL_VALIDATION_FAILED",
"http": {
"status": 400,
},
"userFriendlyMessage": "An error occurred.",
"value": "",
},
"message": "Invalid UUID",
"name": "ValidationError",
}
`;
exports[`CommandMenuItem deletion should fail when deleting with invalid id (not a UUID) 1`] = `
{
"extensions": {
"code": "GRAPHQL_VALIDATION_FAILED",
"http": {
"status": 400,
},
"userFriendlyMessage": "An error occurred.",
"value": "not-a-valid-uuid",
},
"message": "Invalid UUID",
"name": "ValidationError",
}
`;
@@ -0,0 +1,93 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`CommandMenuItem update should fail when updating with empty label 1`] = `
{
"extensions": {
"code": "METADATA_VALIDATION_FAILED",
"errors": {
"commandMenuItem": [
{
"errors": [
{
"code": "INVALID_COMMAND_MENU_ITEM_INPUT",
"message": "Label is required",
"userFriendlyMessage": "Label is required",
},
],
"flatEntityMinimalInformation": {
"id": Any<String>,
"universalIdentifier": Any<String>,
},
"metadataName": "commandMenuItem",
"status": "fail",
"type": "update",
},
],
},
"message": "Validation failed for 1 commandMenuItem",
"summary": {
"commandMenuItem": 1,
"totalErrors": 1,
},
"userFriendlyMessage": "Metadata validation failed",
},
"message": "Multiple validation errors occurred while updating command menu item",
"name": "GraphQLError",
}
`;
exports[`CommandMenuItem update should fail when updating with missing id 1`] = `
{
"extensions": {
"code": "BAD_USER_INPUT",
"http": {
"status": 400,
},
"userFriendlyMessage": "An error occurred.",
},
"message": "Field "id" of required type "UUID!" was not provided.",
"name": "GraphQLError",
}
`;
exports[`CommandMenuItem update should fail when updating with empty id 1`] = `
{
"extensions": {
"code": "GRAPHQL_VALIDATION_FAILED",
"http": {
"status": 400,
},
"userFriendlyMessage": "An error occurred.",
"value": "",
},
"message": "Invalid UUID",
"name": "ValidationError",
}
`;
exports[`CommandMenuItem update should fail when updating with invalid id (not a UUID) 1`] = `
{
"extensions": {
"code": "GRAPHQL_VALIDATION_FAILED",
"http": {
"status": 400,
},
"userFriendlyMessage": "An error occurred.",
"value": "not-a-valid-uuid",
},
"message": "Invalid UUID",
"name": "ValidationError",
}
`;
exports[`CommandMenuItem update should fail when updating a non-existent command menu item 1`] = `
{
"extensions": {
"code": "NOT_FOUND",
"subCode": "COMMAND_MENU_ITEM_NOT_FOUND",
"userFriendlyMessage": "Command menu item not found.",
},
"message": "Command menu item not found",
"name": "NotFoundError",
}
`;
@@ -0,0 +1,94 @@
import { faker } from '@faker-js/faker';
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
import { createCommandMenuItem } from 'test/integration/metadata/suites/command-menu-item/utils/create-command-menu-item.util';
import {
eachTestingContextFilter,
type EachTestingContext,
} from 'twenty-shared/testing';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { type CreateCommandMenuItemInput } from 'src/engine/metadata-modules/command-menu-item/dtos/create-command-menu-item.input';
type TestContext = {
input: CreateCommandMenuItemInput;
};
const failingCommandMenuItemCreationTestCases: EachTestingContext<TestContext>[] =
[
{
title: 'when creating with empty label',
context: {
input: {
workflowVersionId: faker.string.uuid(),
label: '',
},
},
},
{
title: 'when creating with missing label',
context: {
input: {
workflowVersionId: faker.string.uuid(),
} as CreateCommandMenuItemInput,
},
},
{
title: 'when creating with missing workflowVersionId',
context: {
input: {
label: 'Test Label',
} as CreateCommandMenuItemInput,
},
},
{
title: 'when creating with empty workflowVersionId',
context: {
input: {
workflowVersionId: '',
label: 'Test Label',
},
},
},
{
title: 'when creating with invalid workflowVersionId (not a UUID)',
context: {
input: {
workflowVersionId: 'not-a-valid-uuid',
label: 'Test Label',
},
},
},
];
describe('CommandMenuItem creation should fail', () => {
beforeAll(async () => {
await updateFeatureFlag({
featureFlag: FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
value: true,
expectToFail: false,
});
});
afterAll(async () => {
await updateFeatureFlag({
featureFlag: FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
value: false,
expectToFail: false,
});
});
it.each(eachTestingContextFilter(failingCommandMenuItemCreationTestCases))(
'$title',
async ({ context }) => {
const { errors } = await createCommandMenuItem({
expectToFail: true,
input: context.input,
});
expectOneNotInternalServerErrorSnapshot({
errors,
});
},
);
});
@@ -0,0 +1,81 @@
import { faker } from '@faker-js/faker';
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
import { type DeleteCommandMenuItemFactoryInput } from 'test/integration/metadata/suites/command-menu-item/utils/delete-command-menu-item-query-factory.util';
import { deleteCommandMenuItem } from 'test/integration/metadata/suites/command-menu-item/utils/delete-command-menu-item.util';
import {
eachTestingContextFilter,
type EachTestingContext,
} from 'twenty-shared/testing';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
type TestContext = {
input: DeleteCommandMenuItemFactoryInput;
};
const failingCommandMenuItemDeletionTestCases: EachTestingContext<TestContext>[] =
[
{
title: 'when deleting a non-existent command menu item',
context: {
input: {
id: faker.string.uuid(),
},
},
},
{
title: 'when deleting with missing id',
context: {
input: {} as DeleteCommandMenuItemFactoryInput,
},
},
{
title: 'when deleting with empty id',
context: {
input: {
id: '',
},
},
},
{
title: 'when deleting with invalid id (not a UUID)',
context: {
input: {
id: 'not-a-valid-uuid',
},
},
},
];
describe('CommandMenuItem deletion should fail', () => {
beforeAll(async () => {
await updateFeatureFlag({
featureFlag: FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
value: true,
expectToFail: false,
});
});
afterAll(async () => {
await updateFeatureFlag({
featureFlag: FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
value: false,
expectToFail: false,
});
});
it.each(eachTestingContextFilter(failingCommandMenuItemDeletionTestCases))(
'$title',
async ({ context }) => {
const { errors } = await deleteCommandMenuItem({
expectToFail: true,
input: context.input,
});
expectOneNotInternalServerErrorSnapshot({
errors,
});
},
);
});
@@ -0,0 +1,133 @@
import { faker } from '@faker-js/faker';
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
import { createCommandMenuItem } from 'test/integration/metadata/suites/command-menu-item/utils/create-command-menu-item.util';
import { deleteCommandMenuItem } from 'test/integration/metadata/suites/command-menu-item/utils/delete-command-menu-item.util';
import { updateCommandMenuItem } from 'test/integration/metadata/suites/command-menu-item/utils/update-command-menu-item.util';
import {
eachTestingContextFilter,
type EachTestingContext,
} from 'twenty-shared/testing';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { type UpdateCommandMenuItemInput } from 'src/engine/metadata-modules/command-menu-item/dtos/update-command-menu-item.input';
type TestContext = {
input: (testSetup: TestSetup) => UpdateCommandMenuItemInput;
};
type TestSetup = {
testCommandMenuItemId: string;
};
describe('CommandMenuItem update should fail', () => {
let testCommandMenuItemId: string;
beforeAll(async () => {
await updateFeatureFlag({
featureFlag: FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
value: true,
expectToFail: false,
});
});
afterAll(async () => {
await updateFeatureFlag({
featureFlag: FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
value: false,
expectToFail: false,
});
});
beforeEach(async () => {
const { data } = await createCommandMenuItem({
expectToFail: false,
input: {
workflowVersionId: faker.string.uuid(),
label: 'Test Command Menu Item To Update',
icon: 'IconOriginal',
isPinned: false,
},
});
testCommandMenuItemId = data.createCommandMenuItem.id;
});
afterEach(async () => {
if (testCommandMenuItemId) {
await deleteCommandMenuItem({
expectToFail: false,
input: { id: testCommandMenuItemId },
});
}
});
const failingCommandMenuItemUpdateTestCases: EachTestingContext<TestContext>[] =
[
{
title: 'when updating with empty label',
context: {
input: (testSetup) => ({
id: testSetup.testCommandMenuItemId,
label: '',
}),
},
},
{
title: 'when updating with missing id',
context: {
input: () =>
({
label: 'Updated Label',
}) as UpdateCommandMenuItemInput,
},
},
{
title: 'when updating with empty id',
context: {
input: () => ({
id: '',
label: 'Updated Label',
}),
},
},
{
title: 'when updating with invalid id (not a UUID)',
context: {
input: () => ({
id: 'not-a-valid-uuid',
label: 'Updated Label',
}),
},
},
{
title: 'when updating a non-existent command menu item',
context: {
input: () => ({
id: faker.string.uuid(),
label: 'Updated Label',
}),
},
},
];
it.each(eachTestingContextFilter(failingCommandMenuItemUpdateTestCases))(
'$title',
async ({ context }) => {
const testSetup: TestSetup = {
testCommandMenuItemId,
};
const input = context.input(testSetup);
const { errors } = await updateCommandMenuItem({
expectToFail: true,
input,
});
expectOneNotInternalServerErrorSnapshot({
errors,
});
},
);
});
@@ -0,0 +1,165 @@
import { faker } from '@faker-js/faker';
import { createCommandMenuItem } from 'test/integration/metadata/suites/command-menu-item/utils/create-command-menu-item.util';
import { deleteCommandMenuItem } from 'test/integration/metadata/suites/command-menu-item/utils/delete-command-menu-item.util';
import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util';
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
describe('CommandMenuItem creation should succeed', () => {
let createdCommandMenuItemId: string;
let companyObjectMetadataId: string;
let personObjectMetadataId: string;
beforeAll(async () => {
await updateFeatureFlag({
featureFlag: FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
value: true,
expectToFail: false,
});
const { objects } = await findManyObjectMetadata({
expectToFail: false,
input: {
filter: {},
paging: { first: 1000 },
},
gqlFields: `
id
nameSingular
`,
});
jestExpectToBeDefined(objects);
const companyObjectMetadata = objects.find(
(object: { nameSingular: string }) => object.nameSingular === 'company',
);
const personObjectMetadata = objects.find(
(object: { nameSingular: string }) => object.nameSingular === 'person',
);
jestExpectToBeDefined(companyObjectMetadata);
jestExpectToBeDefined(personObjectMetadata);
companyObjectMetadataId = companyObjectMetadata.id;
personObjectMetadataId = personObjectMetadata.id;
});
afterAll(async () => {
await updateFeatureFlag({
featureFlag: FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
value: false,
expectToFail: false,
});
});
afterEach(async () => {
if (createdCommandMenuItemId) {
await deleteCommandMenuItem({
expectToFail: false,
input: { id: createdCommandMenuItemId },
});
createdCommandMenuItemId = undefined as unknown as string;
}
});
it('should create a basic command menu item with minimal input', async () => {
const workflowVersionId = faker.string.uuid();
const { data } = await createCommandMenuItem({
expectToFail: false,
input: {
workflowVersionId,
label: 'Test Command Menu Item',
},
});
createdCommandMenuItemId = data?.createCommandMenuItem?.id;
expect(data.createCommandMenuItem).toMatchObject({
id: expect.any(String),
workflowVersionId,
label: 'Test Command Menu Item',
icon: null,
isPinned: false,
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
availabilityObjectMetadataId: null,
});
});
it('should create command menu item with all optional fields', async () => {
const workflowVersionId = faker.string.uuid();
const { data } = await createCommandMenuItem({
expectToFail: false,
input: {
workflowVersionId,
label: 'Full Command Menu Item',
icon: 'IconSparkles',
isPinned: true,
availabilityType: CommandMenuItemAvailabilityType.SINGLE_RECORD,
availabilityObjectMetadataId: companyObjectMetadataId,
},
});
createdCommandMenuItemId = data?.createCommandMenuItem?.id;
expect(data.createCommandMenuItem).toMatchObject({
id: expect.any(String),
workflowVersionId,
label: 'Full Command Menu Item',
icon: 'IconSparkles',
isPinned: true,
availabilityType: CommandMenuItemAvailabilityType.SINGLE_RECORD,
availabilityObjectMetadataId: companyObjectMetadataId,
});
});
it('should create command menu item with BULK_RECORDS availability', async () => {
const workflowVersionId = faker.string.uuid();
const { data } = await createCommandMenuItem({
expectToFail: false,
input: {
workflowVersionId,
label: 'Bulk Records Command',
availabilityType: CommandMenuItemAvailabilityType.BULK_RECORDS,
availabilityObjectMetadataId: personObjectMetadataId,
},
});
createdCommandMenuItemId = data?.createCommandMenuItem?.id;
expect(data.createCommandMenuItem).toMatchObject({
id: expect.any(String),
workflowVersionId,
label: 'Bulk Records Command',
availabilityType: CommandMenuItemAvailabilityType.BULK_RECORDS,
availabilityObjectMetadataId: personObjectMetadataId,
});
});
it('should create command menu item with GLOBAL availability (default)', async () => {
const workflowVersionId = faker.string.uuid();
const { data } = await createCommandMenuItem({
expectToFail: false,
input: {
workflowVersionId,
label: 'Global Command',
},
});
createdCommandMenuItemId = data?.createCommandMenuItem?.id;
expect(data.createCommandMenuItem).toMatchObject({
id: expect.any(String),
workflowVersionId,
label: 'Global Command',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
});
});
});
@@ -0,0 +1,60 @@
import { faker } from '@faker-js/faker';
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
import { createCommandMenuItem } from 'test/integration/metadata/suites/command-menu-item/utils/create-command-menu-item.util';
import { deleteCommandMenuItem } from 'test/integration/metadata/suites/command-menu-item/utils/delete-command-menu-item.util';
import { findCommandMenuItems } from 'test/integration/metadata/suites/command-menu-item/utils/find-command-menu-items.util';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
describe('CommandMenuItem deletion should succeed', () => {
beforeAll(async () => {
await updateFeatureFlag({
featureFlag: FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
value: true,
expectToFail: false,
});
});
afterAll(async () => {
await updateFeatureFlag({
featureFlag: FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
value: false,
expectToFail: false,
});
});
it('should delete an existing command menu item', async () => {
const workflowVersionId = faker.string.uuid();
const { data: createData } = await createCommandMenuItem({
expectToFail: false,
input: {
workflowVersionId,
label: 'Command Menu Item To Delete',
},
});
const createdId = createData.createCommandMenuItem.id;
const { data: deleteData } = await deleteCommandMenuItem({
expectToFail: false,
input: { id: createdId },
});
expect(deleteData.deleteCommandMenuItem).toMatchObject({
id: createdId,
label: 'Command Menu Item To Delete',
});
const { data: findData } = await findCommandMenuItems({
expectToFail: false,
input: undefined,
});
const deletedItem = findData.commandMenuItems.find(
(item) => item.id === createdId,
);
expect(deletedItem).toBeUndefined();
});
});
@@ -0,0 +1,171 @@
import { faker } from '@faker-js/faker';
import { createCommandMenuItem } from 'test/integration/metadata/suites/command-menu-item/utils/create-command-menu-item.util';
import { deleteCommandMenuItem } from 'test/integration/metadata/suites/command-menu-item/utils/delete-command-menu-item.util';
import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util';
import { updateCommandMenuItem } from 'test/integration/metadata/suites/command-menu-item/utils/update-command-menu-item.util';
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
describe('CommandMenuItem update should succeed', () => {
let createdCommandMenuItemId: string;
let companyObjectMetadataId: string;
let personObjectMetadataId: string;
beforeAll(async () => {
await updateFeatureFlag({
featureFlag: FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
value: true,
expectToFail: false,
});
const { objects } = await findManyObjectMetadata({
expectToFail: false,
input: {
filter: {},
paging: { first: 1000 },
},
gqlFields: `
id
nameSingular
`,
});
jestExpectToBeDefined(objects);
const companyObjectMetadata = objects.find(
(object: { nameSingular: string }) => object.nameSingular === 'company',
);
const personObjectMetadata = objects.find(
(object: { nameSingular: string }) => object.nameSingular === 'person',
);
jestExpectToBeDefined(companyObjectMetadata);
jestExpectToBeDefined(personObjectMetadata);
companyObjectMetadataId = companyObjectMetadata.id;
personObjectMetadataId = personObjectMetadata.id;
});
afterAll(async () => {
await updateFeatureFlag({
featureFlag: FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
value: false,
expectToFail: false,
});
});
beforeEach(async () => {
const workflowVersionId = faker.string.uuid();
const { data } = await createCommandMenuItem({
expectToFail: false,
input: {
workflowVersionId,
label: 'Original Label',
icon: 'IconOriginal',
isPinned: false,
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
},
});
createdCommandMenuItemId = data.createCommandMenuItem.id;
});
afterEach(async () => {
if (createdCommandMenuItemId) {
await deleteCommandMenuItem({
expectToFail: false,
input: { id: createdCommandMenuItemId },
});
createdCommandMenuItemId = undefined as unknown as string;
}
});
it('should update the label', async () => {
const { data } = await updateCommandMenuItem({
expectToFail: false,
input: {
id: createdCommandMenuItemId,
label: 'Updated Label',
},
});
expect(data.updateCommandMenuItem).toMatchObject({
id: createdCommandMenuItemId,
label: 'Updated Label',
});
});
it('should update the icon', async () => {
const { data } = await updateCommandMenuItem({
expectToFail: false,
input: {
id: createdCommandMenuItemId,
icon: 'IconUpdated',
},
});
expect(data.updateCommandMenuItem).toMatchObject({
id: createdCommandMenuItemId,
icon: 'IconUpdated',
});
});
it('should update isPinned', async () => {
const { data } = await updateCommandMenuItem({
expectToFail: false,
input: {
id: createdCommandMenuItemId,
isPinned: true,
},
});
expect(data.updateCommandMenuItem).toMatchObject({
id: createdCommandMenuItemId,
isPinned: true,
});
});
it('should update availabilityType and availabilityObjectMetadataId', async () => {
const { data } = await updateCommandMenuItem({
expectToFail: false,
input: {
id: createdCommandMenuItemId,
availabilityType: CommandMenuItemAvailabilityType.SINGLE_RECORD,
availabilityObjectMetadataId: companyObjectMetadataId,
},
});
expect(data.updateCommandMenuItem).toMatchObject({
id: createdCommandMenuItemId,
availabilityType: CommandMenuItemAvailabilityType.SINGLE_RECORD,
availabilityObjectMetadataId: companyObjectMetadataId,
});
});
it('should update multiple fields at once', async () => {
const { data } = await updateCommandMenuItem({
expectToFail: false,
input: {
id: createdCommandMenuItemId,
label: 'Fully Updated Label',
icon: 'IconNew',
isPinned: true,
availabilityType: CommandMenuItemAvailabilityType.BULK_RECORDS,
availabilityObjectMetadataId: personObjectMetadataId,
},
});
expect(data.updateCommandMenuItem).toMatchObject({
id: createdCommandMenuItemId,
label: 'Fully Updated Label',
icon: 'IconNew',
isPinned: true,
availabilityType: CommandMenuItemAvailabilityType.BULK_RECORDS,
availabilityObjectMetadataId: personObjectMetadataId,
});
});
});
@@ -0,0 +1,35 @@
import gql from 'graphql-tag';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { type CreateCommandMenuItemInput } from 'src/engine/metadata-modules/command-menu-item/dtos/create-command-menu-item.input';
export type CreateCommandMenuItemFactoryInput = CreateCommandMenuItemInput;
const DEFAULT_COMMAND_MENU_ITEM_GQL_FIELDS = `
id
workflowVersionId
label
icon
isPinned
availabilityType
availabilityObjectMetadataId
applicationId
createdAt
updatedAt
`;
export const createCommandMenuItemQueryFactory = ({
input,
gqlFields = DEFAULT_COMMAND_MENU_ITEM_GQL_FIELDS,
}: PerformMetadataQueryParams<CreateCommandMenuItemFactoryInput>) => ({
query: gql`
mutation CreateCommandMenuItem($input: CreateCommandMenuItemInput!) {
createCommandMenuItem(input: $input) {
${gqlFields}
}
}
`,
variables: {
input,
},
});
@@ -0,0 +1,43 @@
import {
type CreateCommandMenuItemFactoryInput,
createCommandMenuItemQueryFactory,
} from 'test/integration/metadata/suites/command-menu-item/utils/create-command-menu-item-query-factory.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type CommandMenuItemDTO } from 'src/engine/metadata-modules/command-menu-item/dtos/command-menu-item.dto';
export const createCommandMenuItem = async ({
input,
gqlFields,
expectToFail = false,
token,
}: PerformMetadataQueryParams<CreateCommandMenuItemFactoryInput>): CommonResponseBody<{
createCommandMenuItem: CommandMenuItemDTO;
}> => {
const graphqlOperation = createCommandMenuItemQueryFactory({
input,
gqlFields,
});
const response = await makeMetadataAPIRequest(graphqlOperation, token);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'CommandMenuItem creation should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'CommandMenuItem creation has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};
@@ -0,0 +1,27 @@
import gql from 'graphql-tag';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
export type DeleteCommandMenuItemFactoryInput = {
id: string;
};
const DEFAULT_COMMAND_MENU_ITEM_GQL_FIELDS = `
id
label
`;
export const deleteCommandMenuItemQueryFactory = ({
input,
gqlFields = DEFAULT_COMMAND_MENU_ITEM_GQL_FIELDS,
}: PerformMetadataQueryParams<DeleteCommandMenuItemFactoryInput>) => ({
query: gql`
mutation DeleteCommandMenuItem($id: UUID!) {
deleteCommandMenuItem(id: $id) {
${gqlFields}
}
}
`,
variables: {
id: input.id,
},
});
@@ -0,0 +1,43 @@
import {
type DeleteCommandMenuItemFactoryInput,
deleteCommandMenuItemQueryFactory,
} from 'test/integration/metadata/suites/command-menu-item/utils/delete-command-menu-item-query-factory.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type CommandMenuItemDTO } from 'src/engine/metadata-modules/command-menu-item/dtos/command-menu-item.dto';
export const deleteCommandMenuItem = async ({
input,
gqlFields,
expectToFail = false,
token,
}: PerformMetadataQueryParams<DeleteCommandMenuItemFactoryInput>): CommonResponseBody<{
deleteCommandMenuItem: CommandMenuItemDTO;
}> => {
const graphqlOperation = deleteCommandMenuItemQueryFactory({
input,
gqlFields,
});
const response = await makeMetadataAPIRequest(graphqlOperation, token);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'CommandMenuItem deletion should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'CommandMenuItem deletion has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};
@@ -0,0 +1,27 @@
import gql from 'graphql-tag';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
const DEFAULT_COMMAND_MENU_ITEM_GQL_FIELDS = `
id
workflowVersionId
label
icon
isPinned
availabilityType
availabilityObjectMetadataId
applicationId
createdAt
updatedAt
`;
export const findCommandMenuItemsQueryFactory = ({
gqlFields = DEFAULT_COMMAND_MENU_ITEM_GQL_FIELDS,
}: PerformMetadataQueryParams<void>) => ({
query: gql`
query CommandMenuItems {
commandMenuItems {
${gqlFields}
}
}
`,
});
@@ -0,0 +1,39 @@
import { findCommandMenuItemsQueryFactory } from 'test/integration/metadata/suites/command-menu-item/utils/find-command-menu-items-query-factory.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type CommandMenuItemDTO } from 'src/engine/metadata-modules/command-menu-item/dtos/command-menu-item.dto';
export const findCommandMenuItems = async ({
gqlFields,
expectToFail = false,
token,
}: PerformMetadataQueryParams<void>): CommonResponseBody<{
commandMenuItems: CommandMenuItemDTO[];
}> => {
const graphqlOperation = findCommandMenuItemsQueryFactory({
input: undefined,
gqlFields,
});
const response = await makeMetadataAPIRequest(graphqlOperation, token);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'CommandMenuItems query should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'CommandMenuItems query has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};
@@ -0,0 +1,35 @@
import gql from 'graphql-tag';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { type UpdateCommandMenuItemInput } from 'src/engine/metadata-modules/command-menu-item/dtos/update-command-menu-item.input';
export type UpdateCommandMenuItemFactoryInput = UpdateCommandMenuItemInput;
const DEFAULT_COMMAND_MENU_ITEM_GQL_FIELDS = `
id
workflowVersionId
label
icon
isPinned
availabilityType
availabilityObjectMetadataId
applicationId
createdAt
updatedAt
`;
export const updateCommandMenuItemQueryFactory = ({
input,
gqlFields = DEFAULT_COMMAND_MENU_ITEM_GQL_FIELDS,
}: PerformMetadataQueryParams<UpdateCommandMenuItemFactoryInput>) => ({
query: gql`
mutation UpdateCommandMenuItem($input: UpdateCommandMenuItemInput!) {
updateCommandMenuItem(input: $input) {
${gqlFields}
}
}
`,
variables: {
input,
},
});
@@ -0,0 +1,43 @@
import {
type UpdateCommandMenuItemFactoryInput,
updateCommandMenuItemQueryFactory,
} from 'test/integration/metadata/suites/command-menu-item/utils/update-command-menu-item-query-factory.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type CommandMenuItemDTO } from 'src/engine/metadata-modules/command-menu-item/dtos/command-menu-item.dto';
export const updateCommandMenuItem = async ({
input,
gqlFields,
expectToFail = false,
token,
}: PerformMetadataQueryParams<UpdateCommandMenuItemFactoryInput>): CommonResponseBody<{
updateCommandMenuItem: CommandMenuItemDTO;
}> => {
const graphqlOperation = updateCommandMenuItemQueryFactory({
input,
gqlFields,
});
const response = await makeMetadataAPIRequest(graphqlOperation, token);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'CommandMenuItem update should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'CommandMenuItem update has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};
@@ -20,5 +20,6 @@ export const ALL_METADATA_NAME = {
pageLayout: 'pageLayout',
pageLayoutWidget: 'pageLayoutWidget',
pageLayoutTab: 'pageLayoutTab',
commandMenuItem: 'commandMenuItem',
frontComponent: 'frontComponent',
} as const;