diff --git a/apps/api/migrations/changes-migration.ts b/apps/api/migrations/changes-migration.ts index 781d6fa88de..b9ca7f67465 100644 --- a/apps/api/migrations/changes-migration.ts +++ b/apps/api/migrations/changes-migration.ts @@ -12,8 +12,8 @@ import { OrganizationRepository, } from '@novu/dal'; import { ChangeEntityTypeEnum, MemberRoleEnum } from '@novu/shared'; -import { CreateEnvironment } from '../src/app/environments/usecases/create-environment/create-environment.usecase'; -import { CreateEnvironmentCommand } from '../src/app/environments/usecases/create-environment/create-environment.command'; +import { CreateEnvironment } from '../src/app/environments-v1/usecases/create-environment/create-environment.usecase'; +import { CreateEnvironmentCommand } from '../src/app/environments-v1/usecases/create-environment/create-environment.command'; import { ApplyChange } from '../src/app/change/usecases/apply-change/apply-change.usecase'; import { ApplyChangeCommand } from '../src/app/change/usecases/apply-change/apply-change.command'; import { CreateChange, CreateChangeCommand } from '@novu/application-generic'; diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 322e0eeda4e..081e9d2990a 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -12,7 +12,6 @@ import { AuthModule } from './app/auth/auth.module'; import { TestingModule } from './app/testing/testing.module'; import { HealthModule } from './app/health/health.module'; import { OrganizationModule } from './app/organization/organization.module'; -import { EnvironmentsModule } from './app/environments/environments.module'; import { ExecutionDetailsModule } from './app/execution-details/execution-details.module'; import { EventsModule } from './app/events/events.module'; import { WidgetsModule } from './app/widgets/widgets.module'; @@ -44,6 +43,8 @@ import { PreferencesModule } from './app/preferences'; import { StepSchemasModule } from './app/step-schemas/step-schemas.module'; import { WorkflowModule } from './app/workflows-v2/workflow.module'; import { WorkflowModuleV1 } from './app/workflows-v1/workflow-v1.module'; +import { EnvironmentsModuleV1 } from './app/environments-v1/environments-v1.module'; +import { EnvironmentsModule } from './app/environments-v2/environments.module'; const enterpriseImports = (): Array | ForwardReference> => { const modules: Array | ForwardReference> = []; @@ -76,7 +77,7 @@ const baseModules: Array | Forward InboundParseModule, SharedModule, HealthModule, - EnvironmentsModule, + EnvironmentsModuleV1, ExecutionDetailsModule, WorkflowModuleV1, EventsModule, @@ -106,6 +107,7 @@ const baseModules: Array | Forward BridgeModule, PreferencesModule, WorkflowModule, + EnvironmentsModule, ]; const enterpriseModules = enterpriseImports(); diff --git a/apps/api/src/app/auth/community.auth.module.config.ts b/apps/api/src/app/auth/community.auth.module.config.ts index 51b1390e44d..15156443a45 100644 --- a/apps/api/src/app/auth/community.auth.module.config.ts +++ b/apps/api/src/app/auth/community.auth.module.config.ts @@ -13,7 +13,7 @@ import { USE_CASES } from './usecases'; import { SharedModule } from '../shared/shared.module'; import { GitHubStrategy } from './services/passport/github.strategy'; import { OrganizationModule } from '../organization/organization.module'; -import { EnvironmentsModule } from '../environments/environments.module'; +import { EnvironmentsModuleV1 } from '../environments-v1/environments-v1.module'; import { JwtSubscriberStrategy } from './services/passport/subscriber-jwt.strategy'; import { RootEnvironmentGuard } from './framework/root-environment-guard.service'; import { ApiKeyStrategy } from './services/passport/apikey.strategy'; @@ -39,7 +39,7 @@ export function getCommunityAuthModuleConfig(): ModuleMetadata { expiresIn: 360000, }, }), - EnvironmentsModule, + EnvironmentsModuleV1, ], controllers: [AuthController], providers: [ diff --git a/apps/api/src/app/bridge/usecases/sync/sync.usecase.ts b/apps/api/src/app/bridge/usecases/sync/sync.usecase.ts index b5556063423..9d40f68dff5 100644 --- a/apps/api/src/app/bridge/usecases/sync/sync.usecase.ts +++ b/apps/api/src/app/bridge/usecases/sync/sync.usecase.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException, HttpException, Injectable } from '@nestjs/common'; import { EnvironmentRepository, @@ -56,8 +56,12 @@ export class Sync { retriesLimit: 1, workflowOrigin: WorkflowOriginEnum.EXTERNAL, })) as DiscoverOutput; - } catch (error: any) { - throw new BadRequestException(`Bridge URL is not valid. ${error.message}`); + } catch (error) { + if (error instanceof HttpException) { + throw new BadRequestException(error.message); + } + + throw error; } if (!discover) { @@ -84,7 +88,7 @@ export class Sync { return persistedWorkflowsInBridge; } - private async updateBridgeUrl(command: SyncCommand) { + private async updateBridgeUrl(command: SyncCommand): Promise { await this.environmentRepository.update( { _id: command.environmentId }, { @@ -100,7 +104,10 @@ export class Sync { ); } - private async disposeOldWorkflows(command: SyncCommand, createdWorkflows: NotificationTemplateEntity[]) { + private async disposeOldWorkflows( + command: SyncCommand, + createdWorkflows: NotificationTemplateEntity[] + ): Promise { const persistedWorkflowIdsInBridge = createdWorkflows.map((i) => i._id); const workflowsToDelete = await this.findAllWorkflowsWithOtherIds(command, persistedWorkflowIdsInBridge); @@ -119,7 +126,10 @@ export class Sync { ); } - private async findAllWorkflowsWithOtherIds(command: SyncCommand, persistedWorkflowIdsInBridge: string[]) { + private async findAllWorkflowsWithOtherIds( + command: SyncCommand, + persistedWorkflowIdsInBridge: string[] + ): Promise { return await this.notificationTemplateRepository.find({ _environmentId: command.environmentId, type: { @@ -132,7 +142,10 @@ export class Sync { }); } - private async createWorkflows(command: SyncCommand, workflowsFromBridge: DiscoverWorkflowOutput[]) { + private async createWorkflows( + command: SyncCommand, + workflowsFromBridge: DiscoverWorkflowOutput[] + ): Promise { return Promise.all( workflowsFromBridge.map(async (workflow) => { const workflowExist = await this.notificationTemplateRepository.findByTriggerIdentifier( @@ -183,7 +196,12 @@ export class Sync { ); } - private async createWorkflow(notificationGroupId: string, isWorkflowActive, command: SyncCommand, workflow) { + private async createWorkflow( + notificationGroupId: string, + isWorkflowActive: boolean, + command: SyncCommand, + workflow: DiscoverWorkflowOutput + ): Promise { return await this.createWorkflowUsecase.execute( CreateWorkflowCommand.create({ origin: WorkflowOriginEnum.EXTERNAL, @@ -193,7 +211,8 @@ export class Sync { environmentId: command.environmentId, organizationId: command.organizationId, userId: command.userId, - name: workflow.workflowId, + name: this.getWorkflowName(workflow), + triggerIdentifier: workflow.workflowId, __source: WorkflowCreationSourceEnum.BRIDGE, steps: this.mapSteps(workflow.steps), /** @deprecated */ @@ -209,23 +228,28 @@ export class Sync { /** @deprecated */ (workflow.options?.payloadSchema as Record), active: isWorkflowActive, - description: this.castToAnyNotSupportedParam(workflow.options).description, + description: this.getWorkflowDescription(workflow), data: this.castToAnyNotSupportedParam(workflow).options?.data, - tags: workflow.tags || [], + tags: this.getWorkflowTags(workflow), critical: this.castToAnyNotSupportedParam(workflow.options)?.critical ?? false, preferenceSettings: this.castToAnyNotSupportedParam(workflow.options)?.preferenceSettings, }) ); } - private async updateWorkflow(workflowExist, command: SyncCommand, workflow) { + private async updateWorkflow( + workflowExist: NotificationTemplateEntity, + command: SyncCommand, + workflow: DiscoverWorkflowOutput + ): Promise { return await this.updateWorkflowUsecase.execute( UpdateWorkflowCommand.create({ id: workflowExist._id, environmentId: command.environmentId, organizationId: command.organizationId, userId: command.userId, - name: workflow.workflowId, + name: this.getWorkflowName(workflow), + workflowId: workflow.workflowId, steps: this.mapSteps(workflow.steps, workflowExist), inputs: { schema: workflow.controls?.schema || workflow.inputs.schema, @@ -238,9 +262,9 @@ export class Sync { (workflow.payload?.schema as Record) || (workflow.options?.payloadSchema as Record), type: WorkflowTypeEnum.BRIDGE, - description: this.castToAnyNotSupportedParam(workflow.options).description, + description: this.getWorkflowDescription(workflow), data: this.castToAnyNotSupportedParam(workflow.options)?.data, - tags: workflow.tags, + tags: this.getWorkflowTags(workflow), active: this.castToAnyNotSupportedParam(workflow.options)?.active ?? true, critical: this.castToAnyNotSupportedParam(workflow.options)?.critical ?? false, preferenceSettings: this.castToAnyNotSupportedParam(workflow.options)?.preferenceSettings, @@ -248,7 +272,10 @@ export class Sync { ); } - private mapSteps(commandWorkflowSteps: DiscoverStepOutput[], workflow?: NotificationTemplateEntity | undefined) { + private mapSteps( + commandWorkflowSteps: DiscoverStepOutput[], + workflow?: NotificationTemplateEntity | undefined + ): NotificationStep[] { const steps: NotificationStep[] = commandWorkflowSteps.map((step) => { const foundStep = workflow?.steps?.find((workflowStep) => workflowStep.stepId === step.stepId); @@ -275,7 +302,10 @@ export class Sync { return steps; } - private async getNotificationGroup(notificationGroupIdCommand: string | undefined, environmentId: string) { + private async getNotificationGroup( + notificationGroupIdCommand: string | undefined, + environmentId: string + ): Promise { let notificationGroupId = notificationGroupIdCommand; if (!notificationGroupId) { @@ -293,6 +323,18 @@ export class Sync { return notificationGroupId; } + private getWorkflowName(workflow: DiscoverWorkflowOutput): string { + return workflow.name || workflow.workflowId; + } + + private getWorkflowDescription(workflow: DiscoverWorkflowOutput): string { + return workflow.description || ''; + } + + private getWorkflowTags(workflow: DiscoverWorkflowOutput): string[] { + return workflow.tags || []; + } + private castToAnyNotSupportedParam(param: any): any { return param as any; } diff --git a/apps/api/src/app/environments/dtos/create-environment-request.dto.ts b/apps/api/src/app/environments-v1/dtos/create-environment-request.dto.ts similarity index 100% rename from apps/api/src/app/environments/dtos/create-environment-request.dto.ts rename to apps/api/src/app/environments-v1/dtos/create-environment-request.dto.ts diff --git a/apps/api/src/app/environments/dtos/environment-response.dto.ts b/apps/api/src/app/environments-v1/dtos/environment-response.dto.ts similarity index 100% rename from apps/api/src/app/environments/dtos/environment-response.dto.ts rename to apps/api/src/app/environments-v1/dtos/environment-response.dto.ts diff --git a/apps/api/src/app/environments/dtos/update-environment-request.dto.ts b/apps/api/src/app/environments-v1/dtos/update-environment-request.dto.ts similarity index 100% rename from apps/api/src/app/environments/dtos/update-environment-request.dto.ts rename to apps/api/src/app/environments-v1/dtos/update-environment-request.dto.ts diff --git a/apps/api/src/app/environments/e2e/get-api-keys.e2e.ts b/apps/api/src/app/environments-v1/e2e/get-api-keys.e2e.ts similarity index 100% rename from apps/api/src/app/environments/e2e/get-api-keys.e2e.ts rename to apps/api/src/app/environments-v1/e2e/get-api-keys.e2e.ts diff --git a/apps/api/src/app/environments/e2e/guard-check.e2e.ts b/apps/api/src/app/environments-v1/e2e/guard-check.e2e.ts similarity index 100% rename from apps/api/src/app/environments/e2e/guard-check.e2e.ts rename to apps/api/src/app/environments-v1/e2e/guard-check.e2e.ts diff --git a/apps/api/src/app/environments/e2e/regenerate-api-keys.e2e.ts b/apps/api/src/app/environments-v1/e2e/regenerate-api-keys.e2e.ts similarity index 100% rename from apps/api/src/app/environments/e2e/regenerate-api-keys.e2e.ts rename to apps/api/src/app/environments-v1/e2e/regenerate-api-keys.e2e.ts diff --git a/apps/api/src/app/environments/environments.controller.ts b/apps/api/src/app/environments-v1/environments-v1.controller.ts similarity index 98% rename from apps/api/src/app/environments/environments.controller.ts rename to apps/api/src/app/environments-v1/environments-v1.controller.ts index 41bb9024a31..93fced76adf 100644 --- a/apps/api/src/app/environments/environments.controller.ts +++ b/apps/api/src/app/environments-v1/environments-v1.controller.ts @@ -32,12 +32,15 @@ import { ApiCommonResponses, ApiResponse } from '../shared/framework/response.de import { UserAuthentication } from '../shared/framework/swagger/api.key.security'; import { SdkGroupName } from '../shared/framework/swagger/sdk.decorators'; +/** + * @deprecated use EnvironmentsControllerV2 + */ @ApiCommonResponses() @Controller('/environments') @UseInterceptors(ClassSerializerInterceptor) @UserAuthentication() @ApiTags('Environments') -export class EnvironmentsController { +export class EnvironmentsControllerV1 { constructor( private createEnvironmentUsecase: CreateEnvironment, private updateEnvironmentUsecase: UpdateEnvironment, diff --git a/apps/api/src/app/environments/environments.module.ts b/apps/api/src/app/environments-v1/environments-v1.module.ts similarity index 80% rename from apps/api/src/app/environments/environments.module.ts rename to apps/api/src/app/environments-v1/environments-v1.module.ts index b3f228bcc77..436bd4a14a1 100644 --- a/apps/api/src/app/environments/environments.module.ts +++ b/apps/api/src/app/environments-v1/environments-v1.module.ts @@ -2,7 +2,7 @@ import { forwardRef, Module } from '@nestjs/common'; import { SharedModule } from '../shared/shared.module'; import { USE_CASES } from './usecases'; -import { EnvironmentsController } from './environments.controller'; +import { EnvironmentsControllerV1 } from './environments-v1.controller'; import { NotificationGroupsModule } from '../notification-groups/notification-groups.module'; import { AuthModule } from '../auth/auth.module'; import { LayoutsModule } from '../layouts/layouts.module'; @@ -16,8 +16,8 @@ import { NovuBridgeModule } from './novu-bridge.module'; forwardRef(() => LayoutsModule), NovuBridgeModule, ], - controllers: [EnvironmentsController], + controllers: [EnvironmentsControllerV1], providers: [...USE_CASES], exports: [...USE_CASES], }) -export class EnvironmentsModule {} +export class EnvironmentsModuleV1 {} diff --git a/apps/api/src/app/environments/novu-bridge-client.ts b/apps/api/src/app/environments-v1/novu-bridge-client.ts similarity index 100% rename from apps/api/src/app/environments/novu-bridge-client.ts rename to apps/api/src/app/environments-v1/novu-bridge-client.ts diff --git a/apps/api/src/app/environments/novu-bridge.controller.ts b/apps/api/src/app/environments-v1/novu-bridge.controller.ts similarity index 100% rename from apps/api/src/app/environments/novu-bridge.controller.ts rename to apps/api/src/app/environments-v1/novu-bridge.controller.ts diff --git a/apps/api/src/app/environments/novu-bridge.module.ts b/apps/api/src/app/environments-v1/novu-bridge.module.ts similarity index 100% rename from apps/api/src/app/environments/novu-bridge.module.ts rename to apps/api/src/app/environments-v1/novu-bridge.module.ts diff --git a/apps/api/src/app/environments/usecases/construct-framework-workflow/construct-framework-workflow.command.ts b/apps/api/src/app/environments-v1/usecases/construct-framework-workflow/construct-framework-workflow.command.ts similarity index 100% rename from apps/api/src/app/environments/usecases/construct-framework-workflow/construct-framework-workflow.command.ts rename to apps/api/src/app/environments-v1/usecases/construct-framework-workflow/construct-framework-workflow.command.ts diff --git a/apps/api/src/app/environments/usecases/construct-framework-workflow/construct-framework-workflow.usecase.ts b/apps/api/src/app/environments-v1/usecases/construct-framework-workflow/construct-framework-workflow.usecase.ts similarity index 100% rename from apps/api/src/app/environments/usecases/construct-framework-workflow/construct-framework-workflow.usecase.ts rename to apps/api/src/app/environments-v1/usecases/construct-framework-workflow/construct-framework-workflow.usecase.ts diff --git a/apps/api/src/app/environments/usecases/construct-framework-workflow/index.ts b/apps/api/src/app/environments-v1/usecases/construct-framework-workflow/index.ts similarity index 100% rename from apps/api/src/app/environments/usecases/construct-framework-workflow/index.ts rename to apps/api/src/app/environments-v1/usecases/construct-framework-workflow/index.ts diff --git a/apps/api/src/app/environments/usecases/create-environment/create-environment.command.ts b/apps/api/src/app/environments-v1/usecases/create-environment/create-environment.command.ts similarity index 100% rename from apps/api/src/app/environments/usecases/create-environment/create-environment.command.ts rename to apps/api/src/app/environments-v1/usecases/create-environment/create-environment.command.ts diff --git a/apps/api/src/app/environments/usecases/create-environment/create-environment.e2e.ts b/apps/api/src/app/environments-v1/usecases/create-environment/create-environment.e2e.ts similarity index 100% rename from apps/api/src/app/environments/usecases/create-environment/create-environment.e2e.ts rename to apps/api/src/app/environments-v1/usecases/create-environment/create-environment.e2e.ts diff --git a/apps/api/src/app/environments/usecases/create-environment/create-environment.usecase.ts b/apps/api/src/app/environments-v1/usecases/create-environment/create-environment.usecase.ts similarity index 100% rename from apps/api/src/app/environments/usecases/create-environment/create-environment.usecase.ts rename to apps/api/src/app/environments-v1/usecases/create-environment/create-environment.usecase.ts diff --git a/apps/api/src/app/environments/usecases/generate-unique-api-key/generate-unique-api-key.spec.ts b/apps/api/src/app/environments-v1/usecases/generate-unique-api-key/generate-unique-api-key.spec.ts similarity index 100% rename from apps/api/src/app/environments/usecases/generate-unique-api-key/generate-unique-api-key.spec.ts rename to apps/api/src/app/environments-v1/usecases/generate-unique-api-key/generate-unique-api-key.spec.ts diff --git a/apps/api/src/app/environments/usecases/generate-unique-api-key/generate-unique-api-key.usecase.ts b/apps/api/src/app/environments-v1/usecases/generate-unique-api-key/generate-unique-api-key.usecase.ts similarity index 100% rename from apps/api/src/app/environments/usecases/generate-unique-api-key/generate-unique-api-key.usecase.ts rename to apps/api/src/app/environments-v1/usecases/generate-unique-api-key/generate-unique-api-key.usecase.ts diff --git a/apps/api/src/app/environments/usecases/get-api-keys/get-api-keys.command.ts b/apps/api/src/app/environments-v1/usecases/get-api-keys/get-api-keys.command.ts similarity index 100% rename from apps/api/src/app/environments/usecases/get-api-keys/get-api-keys.command.ts rename to apps/api/src/app/environments-v1/usecases/get-api-keys/get-api-keys.command.ts diff --git a/apps/api/src/app/environments/usecases/get-api-keys/get-api-keys.usecase.ts b/apps/api/src/app/environments-v1/usecases/get-api-keys/get-api-keys.usecase.ts similarity index 100% rename from apps/api/src/app/environments/usecases/get-api-keys/get-api-keys.usecase.ts rename to apps/api/src/app/environments-v1/usecases/get-api-keys/get-api-keys.usecase.ts diff --git a/apps/api/src/app/environments/usecases/get-environment/get-environment.command.ts b/apps/api/src/app/environments-v1/usecases/get-environment/get-environment.command.ts similarity index 100% rename from apps/api/src/app/environments/usecases/get-environment/get-environment.command.ts rename to apps/api/src/app/environments-v1/usecases/get-environment/get-environment.command.ts diff --git a/apps/api/src/app/environments/usecases/get-environment/get-environment.e2e.ts b/apps/api/src/app/environments-v1/usecases/get-environment/get-environment.e2e.ts similarity index 100% rename from apps/api/src/app/environments/usecases/get-environment/get-environment.e2e.ts rename to apps/api/src/app/environments-v1/usecases/get-environment/get-environment.e2e.ts diff --git a/apps/api/src/app/environments/usecases/get-environment/get-environment.usecase.ts b/apps/api/src/app/environments-v1/usecases/get-environment/get-environment.usecase.ts similarity index 100% rename from apps/api/src/app/environments/usecases/get-environment/get-environment.usecase.ts rename to apps/api/src/app/environments-v1/usecases/get-environment/get-environment.usecase.ts diff --git a/apps/api/src/app/environments/usecases/get-environment/index.ts b/apps/api/src/app/environments-v1/usecases/get-environment/index.ts similarity index 100% rename from apps/api/src/app/environments/usecases/get-environment/index.ts rename to apps/api/src/app/environments-v1/usecases/get-environment/index.ts diff --git a/apps/api/src/app/environments/usecases/get-my-environments/get-my-environments.command.ts b/apps/api/src/app/environments-v1/usecases/get-my-environments/get-my-environments.command.ts similarity index 100% rename from apps/api/src/app/environments/usecases/get-my-environments/get-my-environments.command.ts rename to apps/api/src/app/environments-v1/usecases/get-my-environments/get-my-environments.command.ts diff --git a/apps/api/src/app/environments/usecases/get-my-environments/get-my-environments.e2e.ts b/apps/api/src/app/environments-v1/usecases/get-my-environments/get-my-environments.e2e.ts similarity index 100% rename from apps/api/src/app/environments/usecases/get-my-environments/get-my-environments.e2e.ts rename to apps/api/src/app/environments-v1/usecases/get-my-environments/get-my-environments.e2e.ts diff --git a/apps/api/src/app/environments/usecases/get-my-environments/get-my-environments.usecase.ts b/apps/api/src/app/environments-v1/usecases/get-my-environments/get-my-environments.usecase.ts similarity index 100% rename from apps/api/src/app/environments/usecases/get-my-environments/get-my-environments.usecase.ts rename to apps/api/src/app/environments-v1/usecases/get-my-environments/get-my-environments.usecase.ts diff --git a/apps/api/src/app/environments/usecases/index.ts b/apps/api/src/app/environments-v1/usecases/index.ts similarity index 100% rename from apps/api/src/app/environments/usecases/index.ts rename to apps/api/src/app/environments-v1/usecases/index.ts diff --git a/apps/api/src/app/environments/usecases/regenerate-api-keys/regenerate-api-keys.usecase.ts b/apps/api/src/app/environments-v1/usecases/regenerate-api-keys/regenerate-api-keys.usecase.ts similarity index 100% rename from apps/api/src/app/environments/usecases/regenerate-api-keys/regenerate-api-keys.usecase.ts rename to apps/api/src/app/environments-v1/usecases/regenerate-api-keys/regenerate-api-keys.usecase.ts diff --git a/apps/api/src/app/environments/usecases/update-environment/update-environment.command.ts b/apps/api/src/app/environments-v1/usecases/update-environment/update-environment.command.ts similarity index 100% rename from apps/api/src/app/environments/usecases/update-environment/update-environment.command.ts rename to apps/api/src/app/environments-v1/usecases/update-environment/update-environment.command.ts diff --git a/apps/api/src/app/environments/usecases/update-environment/update-environment.e2e-ee.ts b/apps/api/src/app/environments-v1/usecases/update-environment/update-environment.e2e-ee.ts similarity index 100% rename from apps/api/src/app/environments/usecases/update-environment/update-environment.e2e-ee.ts rename to apps/api/src/app/environments-v1/usecases/update-environment/update-environment.e2e-ee.ts diff --git a/apps/api/src/app/environments/usecases/update-environment/update-environment.e2e.ts b/apps/api/src/app/environments-v1/usecases/update-environment/update-environment.e2e.ts similarity index 100% rename from apps/api/src/app/environments/usecases/update-environment/update-environment.e2e.ts rename to apps/api/src/app/environments-v1/usecases/update-environment/update-environment.e2e.ts diff --git a/apps/api/src/app/environments/usecases/update-environment/update-environment.usecase.ts b/apps/api/src/app/environments-v1/usecases/update-environment/update-environment.usecase.ts similarity index 100% rename from apps/api/src/app/environments/usecases/update-environment/update-environment.usecase.ts rename to apps/api/src/app/environments-v1/usecases/update-environment/update-environment.usecase.ts diff --git a/apps/api/src/app/environments-v2/dtos/get-environment-tags.dto.ts b/apps/api/src/app/environments-v2/dtos/get-environment-tags.dto.ts new file mode 100644 index 00000000000..3998a59588f --- /dev/null +++ b/apps/api/src/app/environments-v2/dtos/get-environment-tags.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsDefined, IsString } from 'class-validator'; + +export class GetEnvironmentTagsDto { + @ApiProperty() + @IsDefined() + @IsString() + name: string; +} diff --git a/apps/api/src/app/environments-v2/environments.controller.ts b/apps/api/src/app/environments-v2/environments.controller.ts new file mode 100644 index 00000000000..8ea8f4673d8 --- /dev/null +++ b/apps/api/src/app/environments-v2/environments.controller.ts @@ -0,0 +1,34 @@ +import { ClassSerializerInterceptor, Controller, Get, Param, UseInterceptors } from '@nestjs/common'; +import { UserSessionData } from '@novu/shared'; +import { ApiTags } from '@nestjs/swagger'; +import { UserSession } from '../shared/framework/user.decorator'; +import { GetEnvironmentTags, GetEnvironmentTagsCommand } from './usecases/get-environment-tags'; +import { ExternalApiAccessible } from '../auth/framework/external-api.decorator'; +import { ApiCommonResponses, ApiResponse } from '../shared/framework/response.decorator'; +import { UserAuthentication } from '../shared/framework/swagger/api.key.security'; +import { GetEnvironmentTagsDto } from './dtos/get-environment-tags.dto'; + +@ApiCommonResponses() +@Controller({ path: `/environments`, version: '2' }) +@UseInterceptors(ClassSerializerInterceptor) +@UserAuthentication() +@ApiTags('Environments') +export class EnvironmentsController { + constructor(private getEnvironmentTagsUsecase: GetEnvironmentTags) {} + + @Get('/:environmentId/tags') + @ApiResponse(GetEnvironmentTagsDto) + @ExternalApiAccessible() + async getEnvironmentTags( + @UserSession() user: UserSessionData, + @Param('environmentId') environmentId: string + ): Promise { + return await this.getEnvironmentTagsUsecase.execute( + GetEnvironmentTagsCommand.create({ + environmentId, + userId: user._id, + organizationId: user.organizationId, + }) + ); + } +} diff --git a/apps/api/src/app/environments-v2/environments.module.ts b/apps/api/src/app/environments-v2/environments.module.ts new file mode 100644 index 00000000000..3e31508bbeb --- /dev/null +++ b/apps/api/src/app/environments-v2/environments.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { EnvironmentsController } from './environments.controller'; +import { GetEnvironmentTags } from './usecases/get-environment-tags'; +import { SharedModule } from '../shared/shared.module'; + +@Module({ + imports: [SharedModule], + controllers: [EnvironmentsController], + providers: [GetEnvironmentTags], + exports: [], +}) +export class EnvironmentsModule {} diff --git a/apps/api/src/app/environments-v2/usecases/get-environment-tags/get-environment-tags.command.ts b/apps/api/src/app/environments-v2/usecases/get-environment-tags/get-environment-tags.command.ts new file mode 100644 index 00000000000..e3629364bd9 --- /dev/null +++ b/apps/api/src/app/environments-v2/usecases/get-environment-tags/get-environment-tags.command.ts @@ -0,0 +1,3 @@ +import { EnvironmentWithUserCommand } from '../../../shared/commands/project.command'; + +export class GetEnvironmentTagsCommand extends EnvironmentWithUserCommand {} diff --git a/apps/api/src/app/environments-v2/usecases/get-environment-tags/get-environment-tags.e2e.ts b/apps/api/src/app/environments-v2/usecases/get-environment-tags/get-environment-tags.e2e.ts new file mode 100644 index 00000000000..0e92a305f1d --- /dev/null +++ b/apps/api/src/app/environments-v2/usecases/get-environment-tags/get-environment-tags.e2e.ts @@ -0,0 +1,53 @@ +import { EnvironmentRepository, NotificationTemplateRepository } from '@novu/dal'; +import { UserSession } from '@novu/testing'; +import { expect } from 'chai'; + +describe('Get Environment Tags - /v2/environments/:environmentId/tags (GET)', async () => { + let session: UserSession; + const environmentRepository = new EnvironmentRepository(); + const notificationTemplateRepository = new NotificationTemplateRepository(); + + before(async () => { + session = new UserSession(); + await session.initialize(); + }); + + it('should return correct tags for the environment', async () => { + await notificationTemplateRepository.create({ + _environmentId: session.environment._id, + tags: ['tag1', 'tag2'], + }); + await notificationTemplateRepository.create({ + _environmentId: session.environment._id, + tags: ['tag2', 'tag3', null, '', undefined], + }); + + const { body } = await session.testAgent.get(`/v2/environments/${session.environment._id}/tags`); + + expect(body.data).to.be.an('array'); + expect(body.data).to.have.lengthOf(3); + expect(body.data).to.deep.include({ name: 'tag1' }); + expect(body.data).to.deep.include({ name: 'tag2' }); + expect(body.data).to.deep.include({ name: 'tag3' }); + }); + + it('should return an empty array when no tags exist', async () => { + const newEnvironment = await environmentRepository.create({ + name: 'Test Environment', + _organizationId: session.organization._id, + }); + + const { body } = await session.testAgent.get(`/v2/environments/${newEnvironment._id}/tags`); + + expect(body.data).to.be.an('array'); + expect(body.data).to.have.lengthOf(0); + }); + + it('should throw NotFoundException for non-existent environment', async () => { + const nonExistentId = '60a5f2f2f2f2f2f2f2f2f2f2'; + const { body } = await session.testAgent.get(`/v2/environments/${nonExistentId}/tags`); + + expect(body.statusCode).to.equal(404); + expect(body.message).to.equal(`Environment ${nonExistentId} not found`); + }); +}); diff --git a/apps/api/src/app/environments-v2/usecases/get-environment-tags/get-environment-tags.usecase.ts b/apps/api/src/app/environments-v2/usecases/get-environment-tags/get-environment-tags.usecase.ts new file mode 100644 index 00000000000..4bb3057861f --- /dev/null +++ b/apps/api/src/app/environments-v2/usecases/get-environment-tags/get-environment-tags.usecase.ts @@ -0,0 +1,38 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { EnvironmentEntity, EnvironmentRepository, NotificationTemplateRepository } from '@novu/dal'; +import { GetEnvironmentTagsCommand } from './get-environment-tags.command'; +import { GetEnvironmentTagsDto } from '../../dtos/get-environment-tags.dto'; + +@Injectable() +export class GetEnvironmentTags { + constructor( + private environmentRepository: EnvironmentRepository, + private notificationTemplateRepository: NotificationTemplateRepository + ) {} + + async execute(command: GetEnvironmentTagsCommand): Promise { + const environment: Omit | null = await this.environmentRepository.findOne( + { + _id: command.environmentId, + _organizationId: command.organizationId, + }, + '-apiKeys' + ); + + if (!environment) throw new NotFoundException(`Environment ${command.environmentId} not found`); + + const notificationTemplates = await this.notificationTemplateRepository.find({ + _environmentId: command.environmentId, + tags: { $exists: true, $type: 'array', $ne: [] }, + }); + + const tags = notificationTemplates.flatMap((template) => template.tags); + const uniqueTags = Array.from(new Set(tags)); + + return this.sanitizeTags(uniqueTags); + } + + private sanitizeTags(tags: string[]): GetEnvironmentTagsDto[] { + return tags.filter((tag) => tag != null && tag !== '').map((tag) => ({ name: tag })); + } +} diff --git a/apps/api/src/app/environments-v2/usecases/get-environment-tags/index.ts b/apps/api/src/app/environments-v2/usecases/get-environment-tags/index.ts new file mode 100644 index 00000000000..92bc33e058c --- /dev/null +++ b/apps/api/src/app/environments-v2/usecases/get-environment-tags/index.ts @@ -0,0 +1,2 @@ +export * from './get-environment-tags.command'; +export * from './get-environment-tags.usecase'; diff --git a/apps/api/src/app/events/e2e/bridge-sync.e2e.ts b/apps/api/src/app/events/e2e/bridge-sync.e2e.ts index bd81c734af5..8a6df9d0692 100644 --- a/apps/api/src/app/events/e2e/bridge-sync.e2e.ts +++ b/apps/api/src/app/events/e2e/bridge-sync.e2e.ts @@ -18,7 +18,7 @@ describe('Bridge Sync - /bridge/sync (POST)', async () => { showButton: { type: 'boolean', default: true }, }, }, - }; + } as const; let bridgeServer: BridgeServer; beforeEach(async () => { @@ -164,7 +164,7 @@ describe('Bridge Sync - /bridge/sync (POST)', async () => { }; }, { - controlSchema: inputPostPayload.schema as any, + controlSchema: inputPostPayload.schema, } ); }, @@ -364,4 +364,197 @@ describe('Bridge Sync - /bridge/sync (POST)', async () => { expect(response.status).to.equal(200); }); + + it('should create a workflow with a name', async () => { + const workflowId = 'hello-world-description'; + const newWorkflow = workflow( + workflowId, + async ({ step }) => { + await step.email('send-email', () => ({ + subject: 'Welcome!', + body: 'Hello there', + })); + }, + { + name: 'My Workflow', + } + ); + await bridgeServer.start({ workflows: [newWorkflow] }); + + const result = await session.testAgent.post(`/v1/bridge/sync`).send({ + bridgeUrl: bridgeServer.serverPath, + }); + + const workflows = await workflowsRepository.find({ + _environmentId: session.environment._id, + _id: result.body.data[0]._id, + }); + expect(workflows.length).to.equal(1); + + const workflowData = workflows[0]; + expect(workflowData.name).to.equal('My Workflow'); + }); + + it('should create a workflow with a name that defaults to the workflowId', async () => { + const workflowId = 'hello-world-description'; + const newWorkflow = workflow(workflowId, async ({ step }) => { + await step.email('send-email', () => ({ + subject: 'Welcome!', + body: 'Hello there', + })); + }); + await bridgeServer.start({ workflows: [newWorkflow] }); + + const result = await session.testAgent.post(`/v1/bridge/sync`).send({ + bridgeUrl: bridgeServer.serverPath, + }); + + const workflows = await workflowsRepository.find({ + _environmentId: session.environment._id, + _id: result.body.data[0]._id, + }); + expect(workflows.length).to.equal(1); + + const workflowData = workflows[0]; + expect(workflowData.name).to.equal(workflowId); + }); + + it('should preserve the original workflow resource when syncing a workflow that has added a name', async () => { + const workflowId = 'hello-world-description'; + const newWorkflow = workflow(workflowId, async ({ step }) => { + await step.email('send-email', () => ({ + subject: 'Welcome!', + body: 'Hello there', + })); + }); + await bridgeServer.start({ workflows: [newWorkflow] }); + + const result = await session.testAgent.post(`/v1/bridge/sync`).send({ + bridgeUrl: bridgeServer.serverPath, + }); + const workflowDbId = result.body.data[0]._id; + + const workflows = await workflowsRepository.find({ + _environmentId: session.environment._id, + _id: workflowDbId, + }); + expect(workflows.length).to.equal(1); + + const workflowData = workflows[0]; + expect(workflowData.name).to.equal(workflowId); + + await bridgeServer.stop(); + + bridgeServer = new BridgeServer(); + const newWorkflowWithName = workflow( + workflowId, + async ({ step }) => { + await step.email('send-email', () => ({ + subject: 'Welcome!', + body: 'Hello there', + })); + }, + { + name: 'My Workflow', + } + ); + await bridgeServer.start({ workflows: [newWorkflowWithName] }); + + await session.testAgent.post(`/v1/bridge/sync`).send({ + bridgeUrl: bridgeServer.serverPath, + }); + + const workflowsWithName = await workflowsRepository.find({ + _environmentId: session.environment._id, + _id: workflowDbId, + }); + expect(workflowsWithName.length).to.equal(1); + + const workflowDataWithName = workflowsWithName[0]; + expect(workflowDataWithName.name).to.equal('My Workflow'); + }); + + it('should create a workflow with a description', async () => { + const workflowId = 'hello-world-description'; + const newWorkflow = workflow( + workflowId, + async ({ step }) => { + await step.email('send-email', () => ({ + subject: 'Welcome!', + body: 'Hello there', + })); + }, + { + description: 'This is a description', + } + ); + await bridgeServer.start({ workflows: [newWorkflow] }); + + const result = await session.testAgent.post(`/v1/bridge/sync`).send({ + bridgeUrl: bridgeServer.serverPath, + }); + + const workflows = await workflowsRepository.find({ + _environmentId: session.environment._id, + _id: result.body.data[0]._id, + }); + expect(workflows.length).to.equal(1); + + const workflowData = workflows[0]; + expect(workflowData.description).to.equal('This is a description'); + }); + + it('should unset the workflow description after the description is removed', async () => { + const workflowId = 'hello-world-description'; + const newWorkflow = workflow( + workflowId, + async ({ step }) => { + await step.email('send-email', () => ({ + subject: 'Welcome!', + body: 'Hello there', + })); + }, + { + description: 'This is a description', + } + ); + await bridgeServer.start({ workflows: [newWorkflow] }); + + const result = await session.testAgent.post(`/v1/bridge/sync`).send({ + bridgeUrl: bridgeServer.serverPath, + }); + const workflowDbId = result.body.data[0]._id; + const workflows = await workflowsRepository.find({ + _environmentId: session.environment._id, + _id: workflowDbId, + }); + expect(workflows.length).to.equal(1); + + const workflowData = workflows[0]; + expect(workflowData.description).to.equal('This is a description'); + + await bridgeServer.stop(); + + bridgeServer = new BridgeServer(); + const newWorkflowWithName = workflow(workflowId, async ({ step }) => { + await step.email('send-email', () => ({ + subject: 'Welcome!', + body: 'Hello there', + })); + }); + await bridgeServer.start({ workflows: [newWorkflowWithName] }); + + await session.testAgent.post(`/v1/bridge/sync`).send({ + bridgeUrl: bridgeServer.serverPath, + }); + + const workflowsWithDescription = await workflowsRepository.find({ + _environmentId: session.environment._id, + _id: workflowDbId, + }); + expect(workflowsWithDescription.length).to.equal(1); + + const workflowDataWithName = workflowsWithDescription[0]; + expect(workflowDataWithName.description).to.equal(''); + }); }); diff --git a/apps/api/src/app/organization/organization.module.ts b/apps/api/src/app/organization/organization.module.ts index 9d376f3a657..6e18b25844b 100644 --- a/apps/api/src/app/organization/organization.module.ts +++ b/apps/api/src/app/organization/organization.module.ts @@ -12,7 +12,7 @@ import { import { AuthGuard } from '@nestjs/passport'; import { Type } from '@nestjs/common/interfaces/type.interface'; import { isClerkEnabled } from '@novu/shared'; -import { EnvironmentsModule } from '../environments/environments.module'; +import { EnvironmentsModuleV1 } from '../environments-v1/environments-v1.module'; import { IntegrationModule } from '../integrations/integrations.module'; import { SharedModule } from '../shared/shared.module'; import { UserModule } from '../user/user.module'; @@ -48,7 +48,7 @@ function getControllers() { imports: [ SharedModule, UserModule, - EnvironmentsModule, + EnvironmentsModuleV1, IntegrationModule, forwardRef(() => AuthModule), ...enterpriseImports(), diff --git a/apps/api/src/app/organization/usecases/create-organization/create-organization.usecase.ts b/apps/api/src/app/organization/usecases/create-organization/create-organization.usecase.ts index 635bcda355d..2cfa20421b4 100644 --- a/apps/api/src/app/organization/usecases/create-organization/create-organization.usecase.ts +++ b/apps/api/src/app/organization/usecases/create-organization/create-organization.usecase.ts @@ -5,8 +5,8 @@ import { ApiServiceLevelEnum, JobTitleEnum, MemberRoleEnum } from '@novu/shared' import { AnalyticsService } from '@novu/application-generic'; import { ModuleRef } from '@nestjs/core'; -import { CreateEnvironmentCommand } from '../../../environments/usecases/create-environment/create-environment.command'; -import { CreateEnvironment } from '../../../environments/usecases/create-environment/create-environment.usecase'; +import { CreateEnvironmentCommand } from '../../../environments-v1/usecases/create-environment/create-environment.command'; +import { CreateEnvironment } from '../../../environments-v1/usecases/create-environment/create-environment.usecase'; import { GetOrganizationCommand } from '../get-organization/get-organization.command'; import { GetOrganization } from '../get-organization/get-organization.usecase'; import { AddMemberCommand } from '../membership/add-member/add-member.command'; diff --git a/apps/api/src/app/organization/usecases/create-organization/sync-external-organization/sync-external-organization.usecase.ts b/apps/api/src/app/organization/usecases/create-organization/sync-external-organization/sync-external-organization.usecase.ts index e973751e977..6f10aeb5569 100644 --- a/apps/api/src/app/organization/usecases/create-organization/sync-external-organization/sync-external-organization.usecase.ts +++ b/apps/api/src/app/organization/usecases/create-organization/sync-external-organization/sync-external-organization.usecase.ts @@ -4,8 +4,8 @@ import { OrganizationEntity, OrganizationRepository, UserRepository } from '@nov import { AnalyticsService } from '@novu/application-generic'; import { ModuleRef } from '@nestjs/core'; -import { CreateEnvironmentCommand } from '../../../../environments/usecases/create-environment/create-environment.command'; -import { CreateEnvironment } from '../../../../environments/usecases/create-environment/create-environment.usecase'; +import { CreateEnvironmentCommand } from '../../../../environments-v1/usecases/create-environment/create-environment.command'; +import { CreateEnvironment } from '../../../../environments-v1/usecases/create-environment/create-environment.usecase'; import { GetOrganizationCommand } from '../../get-organization/get-organization.command'; import { GetOrganization } from '../../get-organization/get-organization.usecase'; diff --git a/apps/api/src/app/partner-integrations/partner-integrations.module.ts b/apps/api/src/app/partner-integrations/partner-integrations.module.ts index f5fd1a87e54..6416a2b0051 100644 --- a/apps/api/src/app/partner-integrations/partner-integrations.module.ts +++ b/apps/api/src/app/partner-integrations/partner-integrations.module.ts @@ -4,11 +4,11 @@ import { CommunityUserRepository, CommunityOrganizationRepository } from '@novu/ import { USE_CASES } from './usecases'; import { PartnerIntegrationsController } from './partner-integrations.controller'; import { SharedModule } from '../shared/shared.module'; -import { EnvironmentsModule } from '../environments/environments.module'; +import { EnvironmentsModuleV1 } from '../environments-v1/environments-v1.module'; import { BridgeModule } from '../bridge'; @Module({ - imports: [SharedModule, HttpModule, EnvironmentsModule, BridgeModule], + imports: [SharedModule, HttpModule, EnvironmentsModuleV1, BridgeModule], providers: [...USE_CASES, CommunityUserRepository, CommunityOrganizationRepository], controllers: [PartnerIntegrationsController], }) diff --git a/apps/api/src/app/workflows-v2/mappers/notification-template-mapper.ts b/apps/api/src/app/workflows-v2/mappers/notification-template-mapper.ts index 540451b8342..f92079f8a1f 100644 --- a/apps/api/src/app/workflows-v2/mappers/notification-template-mapper.ts +++ b/apps/api/src/app/workflows-v2/mappers/notification-template-mapper.ts @@ -13,6 +13,7 @@ import { } from '@novu/shared'; import { ControlValuesEntity, NotificationStepEntity, NotificationTemplateEntity } from '@novu/dal'; import { GetPreferencesResponseDto } from '@novu/application-generic'; +import { BadRequestException } from '@nestjs/common'; export function toResponseWorkflowDto( template: NotificationTemplateEntity, @@ -87,7 +88,7 @@ function convertControls(step: NotificationStepEntity): ControlsSchema { if (step.template?.controls) { return { schema: step.template.controls.schema }; } else { - throw new Error('Missing controls'); + throw new BadRequestException('Step controls must be defined.'); } } diff --git a/apps/web/src/components/layout/components/LocalStudioSidebar/LocalStudioSidebarContent.tsx b/apps/web/src/components/layout/components/LocalStudioSidebar/LocalStudioSidebarContent.tsx index 1e5a449a048..2b763c1c1b2 100644 --- a/apps/web/src/components/layout/components/LocalStudioSidebar/LocalStudioSidebarContent.tsx +++ b/apps/web/src/components/layout/components/LocalStudioSidebar/LocalStudioSidebarContent.tsx @@ -5,7 +5,7 @@ import { FC } from 'react'; import { token } from '@novu/novui/tokens'; import { css, cx } from '@novu/novui/css'; import { WithLoadingSkeleton } from '@novu/novui'; -import { IBridgeWorkflow } from '../../../../studio/types'; +import type { DiscoverWorkflowOutput } from '@novu/framework'; import { NavMenu } from '../../../nav/NavMenu'; import { NavMenuSection } from '../../../nav/NavMenuSection'; import { LocalStudioSidebarOrganizationDisplay } from './LocalStudioSidebarOrganizationDisplay'; @@ -14,10 +14,9 @@ import { useStudioState } from '../../../../studio/StudioStateProvider'; import { NavMenuButtonInner, rawButtonBaseStyles } from '../../../nav/NavMenuButton/NavMenuButton.shared'; import { useDocsModal } from '../../../docs/useDocsModal'; import { PATHS } from '../../../docs/docs.const'; -import { ROUTES } from '../../../../constants/routes'; type LocalStudioSidebarContentProps = { - workflows: IBridgeWorkflow[]; + workflows: DiscoverWorkflowOutput[]; isLoading?: boolean; }; diff --git a/apps/web/src/components/layout/components/LocalStudioSidebar/LocalStudioSidebarToggleButton.tsx b/apps/web/src/components/layout/components/LocalStudioSidebar/LocalStudioSidebarToggleButton.tsx index 939c429af8d..11dec10c286 100644 --- a/apps/web/src/components/layout/components/LocalStudioSidebar/LocalStudioSidebarToggleButton.tsx +++ b/apps/web/src/components/layout/components/LocalStudioSidebar/LocalStudioSidebarToggleButton.tsx @@ -1,8 +1,8 @@ import { css } from '@novu/novui/css'; import { IconBolt } from '@novu/novui/icons'; import { FC } from 'react'; +import type { DiscoverWorkflowOutput } from '@novu/framework'; import { WORKFLOW_NODE_STEP_ICON_DICTIONARY } from '../../../../studio/components/workflows/node-view/WorkflowNodes'; -import { IBridgeWorkflow } from '../../../../studio/types'; import { getStudioWorkflowLink, getStudioWorkflowStepLink, @@ -11,16 +11,16 @@ import { import { NavMenuLinkButton, NavMenuToggleButton } from '../../../nav/NavMenuButton'; type LocalStudioSidebarToggleButtonProps = { - workflow: IBridgeWorkflow; + workflow: DiscoverWorkflowOutput; }; const linkButtonClassName = css({ padding: '75', _before: { display: 'none' } }); export const LocalStudioSidebarToggleButton: FC = ({ workflow }) => { - const { workflowId, steps } = workflow; + const { workflowId, name, steps } = workflow; return ( - + void; + steps: Pick[] | null; + onStepClick: (step: Pick) => void; onTriggerClick: () => void; } -export const WORKFLOW_NODE_STEP_ICON_DICTIONARY: Record = { +export const WORKFLOW_NODE_STEP_ICON_DICTIONARY: Record<`${ChannelStepEnum | ActionStepEnum}`, IconType> = { email: IconOutlineEmail, in_app: IconOutlineNotifications, sms: IconOutlineSms, @@ -54,7 +53,7 @@ export function WorkflowNodes({ steps, onStepClick, onTriggerClick }: WorkflowNo return ( } + icon={} title={step.stepId} onClick={handleStepClick} /> diff --git a/apps/web/src/studio/components/workflows/node-view/WorkflowsDetailPage.tsx b/apps/web/src/studio/components/workflows/node-view/WorkflowsDetailPage.tsx index ec5dc841fd9..26956b6f016 100644 --- a/apps/web/src/studio/components/workflows/node-view/WorkflowsDetailPage.tsx +++ b/apps/web/src/studio/components/workflows/node-view/WorkflowsDetailPage.tsx @@ -6,6 +6,7 @@ import { HStack, Stack } from '@novu/novui/jsx'; import { token } from '@novu/novui/tokens'; import { FeatureFlagsKeysEnum } from '@novu/shared'; import { useEffect, useState } from 'react'; +import type { DiscoverWorkflowOutput } from '@novu/framework'; import { useFeatureFlag } from '../../../../hooks/useFeatureFlag'; import { useTelemetry } from '../../../../hooks/useNovuAPI'; import { useWorkflow } from '../../../hooks/useBridgeAPI'; @@ -41,7 +42,10 @@ const BaseWorkflowsDetailPage = () => { return ; } - const title = workflow?.workflowId; + // After loading has completed, we can safely cast the workflow to DiscoverWorkflowOutput + const fetchedWorkflow = workflow as DiscoverWorkflowOutput; + + const title = fetchedWorkflow?.name || fetchedWorkflow.workflowId; return ( { if (workflow) { setValue('general.workflowId', workflow.workflowId); + setValue('general.name', workflow.name || workflow.workflowId); + setValue('general.description', workflow.description || ''); + setValue('general.tags', workflow.tags || []); setValue('preferences', buildWorkflowPreferences(workflow.preferences)); } }, [setValue, workflow]); diff --git a/apps/web/src/studio/components/workflows/preferences/WorkflowDetailFormContextProvider.tsx b/apps/web/src/studio/components/workflows/preferences/WorkflowDetailFormContextProvider.tsx index b64d0372eb9..c4683b27364 100644 --- a/apps/web/src/studio/components/workflows/preferences/WorkflowDetailFormContextProvider.tsx +++ b/apps/web/src/studio/components/workflows/preferences/WorkflowDetailFormContextProvider.tsx @@ -1,4 +1,4 @@ -import { buildWorkflowPreferences, WorkflowPreferences } from '@novu/shared'; +import { WorkflowPreferences } from '@novu/shared'; import { FC, PropsWithChildren } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { WorkflowGeneralSettings } from './types'; diff --git a/apps/web/src/studio/components/workflows/preferences/WorkflowGeneralSettingsForm.tsx b/apps/web/src/studio/components/workflows/preferences/WorkflowGeneralSettingsForm.tsx index 69840d548da..e7b2c0593dc 100644 --- a/apps/web/src/studio/components/workflows/preferences/WorkflowGeneralSettingsForm.tsx +++ b/apps/web/src/studio/components/workflows/preferences/WorkflowGeneralSettingsForm.tsx @@ -1,21 +1,38 @@ import { useClipboard } from '@mantine/hooks'; -import { IconButton, Input } from '@novu/novui'; +import { IconButton, Input, Textarea } from '@novu/novui'; import { IconCheck, IconCopyAll } from '@novu/novui/icons'; -import { Stack } from '@novu/novui/jsx'; +import { Select } from '@novu/design-system'; +import { Stack, Box, styled } from '@novu/novui/jsx'; import { FC } from 'react'; import { Controller, FieldPath, useFormContext } from 'react-hook-form'; +import { token } from '@novu/novui/tokens'; import { WorkflowDetailFormContext } from './WorkflowDetailFormContextProvider'; export type WorkflowGeneralSettingsFieldName = Extract< FieldPath, - 'general.workflowId' | 'general.name' + 'general.workflowId' | 'general.name' | 'general.description' | 'general.tags' >; export type WorkflowGeneralSettingsProps = { - checkShouldDisableField?: (fieldName: WorkflowGeneralSettingsFieldName, fieldValue: string) => boolean; + checkShouldDisableField?: (fieldName: WorkflowGeneralSettingsFieldName) => boolean; checkShouldHideField?: (fieldName: WorkflowGeneralSettingsFieldName) => boolean; }; +const InboxSnippet = () => ( + + {``} + +); + export const WorkflowGeneralSettingsForm: FC = ({ checkShouldDisableField, checkShouldHideField, @@ -29,45 +46,96 @@ export const WorkflowGeneralSettingsForm: FC = ({ return ( + {!checkShouldHideField?.('general.workflowId') && ( + { + return ( + + A unique, lowercase identifier, using only - (dash) separators + + } + rightSection={ copy(field.value)} />} + error={errors?.general?.workflowId?.message} + value={field.value || ''} + disabled={checkShouldDisableField?.(field.name)} + /> + ); + }} + /> + )} {!checkShouldHideField?.('general.name') && ( { return ( + A human-friendly name for the workflow, displayed in the Dashboard and the + + } value={field.value || ''} - disabled={checkShouldDisableField?.(field.name, field.value)} + disabled={checkShouldDisableField?.(field.name)} error={errors?.general?.name?.message} /> ); }} /> )} - {!checkShouldHideField?.('general.workflowId') && ( + {!checkShouldHideField?.('general.description') && ( { return ( - copy(field.value)} />} - error={errors?.general?.workflowId?.message} + label="Description" + description="A brief description of the workflow's purpose for team members" + placeholder="Add a description..." value={field.value || ''} - disabled={checkShouldDisableField?.(field.name, field.value)} + disabled={checkShouldDisableField?.(field.name)} + error={errors?.general?.description?.message} + /> + ); + }} + /> + )} + {!checkShouldHideField?.('general.tags') && ( + { + return ( +