From 9f2c5a317c29abea251c75cbe22a19a44a41cf30 Mon Sep 17 00:00:00 2001 From: Adam Chmara Date: Thu, 17 Oct 2024 14:41:51 +0200 Subject: [PATCH 1/6] feat(api): add get environment tags endpoint (#6713) --- apps/api/migrations/changes-migration.ts | 4 +- apps/api/src/app.module.ts | 6 ++- .../app/auth/community.auth.module.config.ts | 4 +- .../dtos/create-environment-request.dto.ts | 0 .../dtos/environment-response.dto.ts | 0 .../dtos/update-environment-request.dto.ts | 0 .../e2e/get-api-keys.e2e.ts | 0 .../e2e/guard-check.e2e.ts | 0 .../e2e/regenerate-api-keys.e2e.ts | 0 .../environments-v1.controller.ts} | 5 +- .../environments-v1.module.ts} | 6 +-- .../novu-bridge-client.ts | 0 .../novu-bridge.controller.ts | 0 .../novu-bridge.module.ts | 0 .../construct-framework-workflow.command.ts | 0 .../construct-framework-workflow.usecase.ts | 0 .../construct-framework-workflow/index.ts | 0 .../create-environment.command.ts | 0 .../create-environment.e2e.ts | 0 .../create-environment.usecase.ts | 0 .../generate-unique-api-key.spec.ts | 0 .../generate-unique-api-key.usecase.ts | 0 .../get-api-keys/get-api-keys.command.ts | 0 .../get-api-keys/get-api-keys.usecase.ts | 0 .../get-environment.command.ts | 0 .../get-environment/get-environment.e2e.ts | 0 .../get-environment.usecase.ts | 0 .../usecases/get-environment/index.ts | 0 .../get-my-environments.command.ts | 0 .../get-my-environments.e2e.ts | 0 .../get-my-environments.usecase.ts | 0 .../usecases/index.ts | 0 .../regenerate-api-keys.usecase.ts | 0 .../update-environment.command.ts | 0 .../update-environment.e2e-ee.ts | 0 .../update-environment.e2e.ts | 0 .../update-environment.usecase.ts | 0 .../dtos/get-environment-tags.dto.ts | 9 ++++ .../environments.controller.ts | 34 ++++++++++++ .../environments-v2/environments.module.ts | 12 +++++ .../get-environment-tags.command.ts | 3 ++ .../get-environment-tags.e2e.ts | 53 +++++++++++++++++++ .../get-environment-tags.usecase.ts | 38 +++++++++++++ .../usecases/get-environment-tags/index.ts | 2 + .../app/organization/organization.module.ts | 4 +- .../create-organization.usecase.ts | 4 +- .../sync-external-organization.usecase.ts | 4 +- .../partner-integrations.module.ts | 4 +- 48 files changed, 174 insertions(+), 18 deletions(-) rename apps/api/src/app/{environments => environments-v1}/dtos/create-environment-request.dto.ts (100%) rename apps/api/src/app/{environments => environments-v1}/dtos/environment-response.dto.ts (100%) rename apps/api/src/app/{environments => environments-v1}/dtos/update-environment-request.dto.ts (100%) rename apps/api/src/app/{environments => environments-v1}/e2e/get-api-keys.e2e.ts (100%) rename apps/api/src/app/{environments => environments-v1}/e2e/guard-check.e2e.ts (100%) rename apps/api/src/app/{environments => environments-v1}/e2e/regenerate-api-keys.e2e.ts (100%) rename apps/api/src/app/{environments/environments.controller.ts => environments-v1/environments-v1.controller.ts} (98%) rename apps/api/src/app/{environments/environments.module.ts => environments-v1/environments-v1.module.ts} (80%) rename apps/api/src/app/{environments => environments-v1}/novu-bridge-client.ts (100%) rename apps/api/src/app/{environments => environments-v1}/novu-bridge.controller.ts (100%) rename apps/api/src/app/{environments => environments-v1}/novu-bridge.module.ts (100%) rename apps/api/src/app/{environments => environments-v1}/usecases/construct-framework-workflow/construct-framework-workflow.command.ts (100%) rename apps/api/src/app/{environments => environments-v1}/usecases/construct-framework-workflow/construct-framework-workflow.usecase.ts (100%) rename apps/api/src/app/{environments => environments-v1}/usecases/construct-framework-workflow/index.ts (100%) rename apps/api/src/app/{environments => environments-v1}/usecases/create-environment/create-environment.command.ts (100%) rename apps/api/src/app/{environments => environments-v1}/usecases/create-environment/create-environment.e2e.ts (100%) rename apps/api/src/app/{environments => environments-v1}/usecases/create-environment/create-environment.usecase.ts (100%) rename apps/api/src/app/{environments => environments-v1}/usecases/generate-unique-api-key/generate-unique-api-key.spec.ts (100%) rename apps/api/src/app/{environments => environments-v1}/usecases/generate-unique-api-key/generate-unique-api-key.usecase.ts (100%) rename apps/api/src/app/{environments => environments-v1}/usecases/get-api-keys/get-api-keys.command.ts (100%) rename apps/api/src/app/{environments => environments-v1}/usecases/get-api-keys/get-api-keys.usecase.ts (100%) rename apps/api/src/app/{environments => environments-v1}/usecases/get-environment/get-environment.command.ts (100%) rename apps/api/src/app/{environments => environments-v1}/usecases/get-environment/get-environment.e2e.ts (100%) rename apps/api/src/app/{environments => environments-v1}/usecases/get-environment/get-environment.usecase.ts (100%) rename apps/api/src/app/{environments => environments-v1}/usecases/get-environment/index.ts (100%) rename apps/api/src/app/{environments => environments-v1}/usecases/get-my-environments/get-my-environments.command.ts (100%) rename apps/api/src/app/{environments => environments-v1}/usecases/get-my-environments/get-my-environments.e2e.ts (100%) rename apps/api/src/app/{environments => environments-v1}/usecases/get-my-environments/get-my-environments.usecase.ts (100%) rename apps/api/src/app/{environments => environments-v1}/usecases/index.ts (100%) rename apps/api/src/app/{environments => environments-v1}/usecases/regenerate-api-keys/regenerate-api-keys.usecase.ts (100%) rename apps/api/src/app/{environments => environments-v1}/usecases/update-environment/update-environment.command.ts (100%) rename apps/api/src/app/{environments => environments-v1}/usecases/update-environment/update-environment.e2e-ee.ts (100%) rename apps/api/src/app/{environments => environments-v1}/usecases/update-environment/update-environment.e2e.ts (100%) rename apps/api/src/app/{environments => environments-v1}/usecases/update-environment/update-environment.usecase.ts (100%) create mode 100644 apps/api/src/app/environments-v2/dtos/get-environment-tags.dto.ts create mode 100644 apps/api/src/app/environments-v2/environments.controller.ts create mode 100644 apps/api/src/app/environments-v2/environments.module.ts create mode 100644 apps/api/src/app/environments-v2/usecases/get-environment-tags/get-environment-tags.command.ts create mode 100644 apps/api/src/app/environments-v2/usecases/get-environment-tags/get-environment-tags.e2e.ts create mode 100644 apps/api/src/app/environments-v2/usecases/get-environment-tags/get-environment-tags.usecase.ts create mode 100644 apps/api/src/app/environments-v2/usecases/get-environment-tags/index.ts 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/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/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], }) From baef2807c0e375e312660feb4ad34c737e48b5db Mon Sep 17 00:00:00 2001 From: Richard Fontein <32132657+rifont@users.noreply.github.com> Date: Thu, 17 Oct 2024 15:50:36 +0200 Subject: [PATCH 2/6] fix(framework): Support json values in LiquidJS templates (#6714) --- packages/framework/src/client.test.ts | 380 ++++++++++++++++++ packages/framework/src/client.ts | 11 +- .../framework/src/utils/string.utils.test.ts | 60 ++- packages/framework/src/utils/string.utils.ts | 18 + 4 files changed, 466 insertions(+), 3 deletions(-) diff --git a/packages/framework/src/client.test.ts b/packages/framework/src/client.test.ts index c9bebaa4205..513c17359d9 100644 --- a/packages/framework/src/client.test.ts +++ b/packages/framework/src/client.test.ts @@ -593,6 +593,386 @@ describe('Novu Client', () => { expect(body).toContain('dog'); }); + it('should compile array control variables to a string with single quotes', async () => { + const newWorkflow = workflow( + 'test-workflow', + async ({ step }) => { + await step.email( + 'send-email', + async (controls) => ({ + body: controls.body, + subject: controls.subject, + }), + { + controlSchema: { + type: 'object', + properties: { + body: { type: 'string' }, + subject: { type: 'string' }, + }, + required: ['body', 'subject'], + additionalProperties: false, + } as const, + } + ); + }, + { + payloadSchema: { + type: 'object', + properties: { + comments: { + type: 'array', + items: { + type: 'object', + properties: { text: { type: 'string' } }, + required: ['text'], + }, + }, + subject: { type: 'string' }, + }, + required: ['comments', 'subject'], + additionalProperties: false, + } as const, + } + ); + + client.addWorkflows([newWorkflow]); + + const event: Event = { + action: PostActionEnum.EXECUTE, + data: {}, + payload: { comments: [{ text: 'cat' }, { text: 'dog' }], subject: 'Hello' }, + workflowId: 'test-workflow', + stepId: 'send-email', + subscriber: {}, + state: [], + inputs: {}, + controls: { + body: '{{payload.comments}}', + subject: '{{payload.subject}}', + }, + }; + + const emailExecutionResult = await client.executeWorkflow(event); + + expect(emailExecutionResult.outputs).toEqual({ + body: "[{'text':'cat'},{'text':'dog'}]", + subject: 'Hello', + }); + }); + + it('should compile array control variables to a string with single quotes when using json filter', async () => { + const newWorkflow = workflow( + 'test-workflow', + async ({ step }) => { + await step.email( + 'send-email', + async (controls) => ({ + body: controls.body, + subject: controls.subject, + }), + { + controlSchema: { + type: 'object', + properties: { + body: { type: 'string' }, + subject: { type: 'string' }, + }, + required: ['body', 'subject'], + additionalProperties: false, + } as const, + } + ); + }, + { + payloadSchema: { + type: 'object', + properties: { + comments: { + type: 'array', + items: { + type: 'object', + properties: { text: { type: 'string' } }, + required: ['text'], + }, + }, + subject: { type: 'string' }, + }, + required: ['comments', 'subject'], + additionalProperties: false, + } as const, + } + ); + + client.addWorkflows([newWorkflow]); + + const event: Event = { + action: PostActionEnum.EXECUTE, + data: {}, + payload: { comments: [{ text: 'cat' }, { text: 'dog' }], subject: 'Hello' }, + workflowId: 'test-workflow', + stepId: 'send-email', + subscriber: {}, + state: [], + inputs: {}, + controls: { + body: '{{payload.comments | json}}', + subject: '{{payload.subject}}', + }, + }; + + const emailExecutionResult = await client.executeWorkflow(event); + + expect(emailExecutionResult.outputs).toEqual({ + body: "[{'text':'cat'},{'text':'dog'}]", + subject: 'Hello', + }); + }); + + it('should compile object control variables to a string with single quotes', async () => { + const newWorkflow = workflow( + 'test-workflow', + async ({ step }) => { + await step.email( + 'send-email', + async (controls) => ({ + body: controls.body, + subject: controls.subject, + }), + { + controlSchema: { + type: 'object', + properties: { + body: { type: 'string' }, + subject: { type: 'string' }, + }, + required: ['body', 'subject'], + additionalProperties: false, + } as const, + } + ); + }, + { + payloadSchema: { + type: 'object', + properties: { + comment: { + type: 'object', + properties: { text: { type: 'string' } }, + required: ['text'], + }, + subject: { type: 'string' }, + }, + required: ['comment', 'subject'], + additionalProperties: false, + } as const, + } + ); + + client.addWorkflows([newWorkflow]); + + const event: Event = { + action: PostActionEnum.EXECUTE, + data: {}, + payload: { comment: { text: 'cat' }, subject: 'Hello' }, + workflowId: 'test-workflow', + stepId: 'send-email', + subscriber: {}, + state: [], + inputs: {}, + controls: { + body: '{{payload.comment}}', + subject: '{{payload.subject}}', + }, + }; + + const emailExecutionResult = await client.executeWorkflow(event); + + expect(emailExecutionResult.outputs).toEqual({ + body: "{'text':'cat'}", + subject: 'Hello', + }); + }); + + it('should compile object control variables to a string with single quotes when using json filter', async () => { + const newWorkflow = workflow( + 'test-workflow', + async ({ step }) => { + await step.email( + 'send-email', + async (controls) => ({ + body: controls.body, + subject: controls.subject, + }), + { + controlSchema: { + type: 'object', + properties: { + body: { type: 'string' }, + subject: { type: 'string' }, + }, + required: ['body', 'subject'], + additionalProperties: false, + } as const, + } + ); + }, + { + payloadSchema: { + type: 'object', + properties: { + comment: { + type: 'object', + properties: { text: { type: 'string' } }, + required: ['text'], + }, + subject: { type: 'string' }, + }, + required: ['comment', 'subject'], + additionalProperties: false, + } as const, + } + ); + + client.addWorkflows([newWorkflow]); + + const event: Event = { + action: PostActionEnum.EXECUTE, + data: {}, + payload: { comment: { text: 'cat' }, subject: 'Hello' }, + workflowId: 'test-workflow', + stepId: 'send-email', + subscriber: {}, + state: [], + inputs: {}, + controls: { + body: '{{payload.comment | json}}', + subject: '{{payload.subject}}', + }, + }; + + const emailExecutionResult = await client.executeWorkflow(event); + + expect(emailExecutionResult.outputs).toEqual({ + body: "{'text':'cat'}", + subject: 'Hello', + }); + }); + + it('should respect the spaces option when using json filter', async () => { + const newWorkflow = workflow( + 'test-workflow', + async ({ step }) => { + await step.email( + 'send-email', + async (controls) => ({ + body: controls.body, + subject: controls.subject, + }), + { + controlSchema: { + type: 'object', + properties: { + body: { type: 'string' }, + subject: { type: 'string' }, + }, + required: ['body', 'subject'], + additionalProperties: false, + } as const, + } + ); + }, + { + payloadSchema: { + type: 'object', + properties: { + comment: { + type: 'object', + properties: { text: { type: 'string' } }, + required: ['text'], + }, + subject: { type: 'string' }, + }, + required: ['comment', 'subject'], + additionalProperties: false, + } as const, + } + ); + + client.addWorkflows([newWorkflow]); + + const event: Event = { + action: PostActionEnum.EXECUTE, + data: {}, + payload: { comment: { text: 'cat' }, subject: 'Hello' }, + workflowId: 'test-workflow', + stepId: 'send-email', + subscriber: {}, + state: [], + inputs: {}, + controls: { + body: '{{payload.comment | json: 2}}', + subject: '{{payload.subject}}', + }, + }; + + const emailExecutionResult = await client.executeWorkflow(event); + + expect(emailExecutionResult.outputs).toEqual({ + body: `{ + 'text': 'cat' +}`, + subject: 'Hello', + }); + }); + + it('should gracefully compile control variables that are not present', async () => { + const newWorkflow = workflow('test-workflow', async ({ step }) => { + await step.email( + 'send-email', + async (controls) => ({ + body: controls.body, + subject: controls.subject, + }), + { + controlSchema: { + type: 'object', + properties: { + body: { type: 'string' }, + subject: { type: 'string' }, + }, + required: ['body', 'subject'], + additionalProperties: false, + } as const, + } + ); + }); + + client.addWorkflows([newWorkflow]); + + const event: Event = { + action: PostActionEnum.EXECUTE, + data: {}, + payload: {}, + workflowId: 'test-workflow', + stepId: 'send-email', + subscriber: {}, + state: [], + inputs: {}, + controls: { + body: 'Hi {{payload.does_not_exist}}', + subject: 'Test subject', + }, + }; + + const emailExecutionResult = await client.executeWorkflow(event); + + expect(emailExecutionResult.outputs).toEqual({ + body: 'Hi undefined', + subject: 'Test subject', + }); + }); + // skipped until we implement support for control variables https://linear.app/novu/issue/NV-4248/support-for-controls-in-autocomplete it.skip('should compile control variables used in other control variables', async () => { const newWorkflow = workflow('test-workflow', async ({ step }) => { diff --git a/packages/framework/src/client.ts b/packages/framework/src/client.ts index 3bb83b2a6b2..d88897da9cc 100644 --- a/packages/framework/src/client.ts +++ b/packages/framework/src/client.ts @@ -35,7 +35,7 @@ import type { Workflow, } from './types'; import { WithPassthrough } from './types/provider.types'; -import { EMOJI, log, sanitizeHtmlInObject } from './utils'; +import { EMOJI, log, sanitizeHtmlInObject, stringifyDataStructureWithSingleQuotes } from './utils'; import { transformSchema, validateData } from './validators'; /** @@ -57,7 +57,11 @@ function isRuntimeInDevelopment() { export class Client { private discoveredWorkflows: Array = []; - private templateEngine = new Liquid(); + private templateEngine = new Liquid({ + outputEscape: (output) => { + return stringifyDataStructureWithSingleQuotes(output); + }, + }); public secretKey?: string; @@ -69,6 +73,9 @@ export class Client { const builtOpts = this.buildOptions(options); this.secretKey = builtOpts.secretKey; this.strictAuthentication = builtOpts.strictAuthentication; + this.templateEngine.registerFilter('json', (value, spaces) => + stringifyDataStructureWithSingleQuotes(value, spaces) + ); } private buildOptions(providedOptions?: ClientOptions) { diff --git a/packages/framework/src/utils/string.utils.test.ts b/packages/framework/src/utils/string.utils.test.ts index 2f24621bda4..f351221a64f 100644 --- a/packages/framework/src/utils/string.utils.test.ts +++ b/packages/framework/src/utils/string.utils.test.ts @@ -1,5 +1,5 @@ import { expect, it, describe } from 'vitest'; -import { toConstantCase } from './string.utils'; +import { stringifyDataStructureWithSingleQuotes, toConstantCase } from './string.utils'; describe('convert to constant case', () => { it('converts properties correctly', () => { @@ -19,3 +19,61 @@ describe('convert to constant case', () => { }, ''); }); }); + +describe('stringifyDataStructureWithSingleQuotes', () => { + it('should convert a simple array to a string with single quotes', () => { + const myTestArray = ['a', 'b', 'c']; + const converted = stringifyDataStructureWithSingleQuotes(myTestArray); + expect(converted).toStrictEqual("['a','b','c']"); + }); + + it('should convert an array with nested objects to a string with single quotes', () => { + const myTestObject = [{ text: 'cat' }, { text: 'dog' }]; + const converted = stringifyDataStructureWithSingleQuotes(myTestObject); + expect(converted).toStrictEqual("[{'text':'cat'},{'text':'dog'}]"); + }); + + it('should convert an object with nested objects to a string with single quotes', () => { + const myTestObject = { comments: [{ text: 'cat' }, { text: 'dog' }] }; + const converted = stringifyDataStructureWithSingleQuotes(myTestObject); + expect(converted).toStrictEqual("{'comments':[{'text':'cat'},{'text':'dog'}]}"); + }); + + it('should convert an object with nested objects to a string with single quotes and spaces', () => { + const myTestObject = { comments: [{ text: 'cat' }, { text: 'dog' }] }; + const converted = stringifyDataStructureWithSingleQuotes(myTestObject, 2); + expect(converted).toStrictEqual( + `{\\n 'comments': [\\n {\\n 'text': 'cat'\\n },\\n {\\n 'text': 'dog'\\n }\\n ]\\n}` + ); + }); + + it('should convert a string to a string without single quotes', () => { + const myTestString = 'hello'; + const converted = stringifyDataStructureWithSingleQuotes(myTestString); + expect(converted).toStrictEqual('hello'); + }); + + it('should convert a number to a string without single quotes', () => { + const myTestNumber = 123; + const converted = stringifyDataStructureWithSingleQuotes(myTestNumber); + expect(converted).toStrictEqual('123'); + }); + + it('should convert a boolean to a string without single quotes', () => { + const myTestBoolean = true; + const converted = stringifyDataStructureWithSingleQuotes(myTestBoolean); + expect(converted).toStrictEqual('true'); + }); + + it('should convert null to a string without single quotes', () => { + const myTestNull = null; + const converted = stringifyDataStructureWithSingleQuotes(myTestNull); + expect(converted).toStrictEqual('null'); + }); + + it('should convert undefined to an empty string', () => { + const myTestUndefined = undefined; + const converted = stringifyDataStructureWithSingleQuotes(myTestUndefined); + expect(converted).toStrictEqual('undefined'); + }); +}); diff --git a/packages/framework/src/utils/string.utils.ts b/packages/framework/src/utils/string.utils.ts index 1e8365877fd..3326d4038a3 100644 --- a/packages/framework/src/utils/string.utils.ts +++ b/packages/framework/src/utils/string.utils.ts @@ -20,3 +20,21 @@ export const enumToPrettyString = (_enum: T): string => export const toPascalCase = (str: string): string => str.replaceAll(/(\w)(\w*)/g, (_, first, rest) => first.toUpperCase() + rest.toLowerCase()).replaceAll(/[\s-]+/g, ''); + +/** + * Converts a data structure to a string with single quotes, + * converting primitives to strings. + * @param value The value to convert + * @returns A string with single quotes around objects and arrays, and the stringified value itself if it's not an object or array + */ +export const stringifyDataStructureWithSingleQuotes = (value: unknown, spaces: number = 0): string => { + if (Array.isArray(value) || (typeof value === 'object' && value !== null)) { + const valueStringified = JSON.stringify(value, null, spaces); + const valueSingleQuotes = valueStringified.replace(/"/g, "'"); + const valueEscapedNewLines = valueSingleQuotes.replace(/\n/g, '\\n'); + + return valueEscapedNewLines; + } else { + return String(value); + } +}; From 64a248d3f30da84b2b158185e8701a94951b0abd Mon Sep 17 00:00:00 2001 From: Richard Fontein <32132657+rifont@users.noreply.github.com> Date: Thu, 17 Oct 2024 16:45:29 +0200 Subject: [PATCH 3/6] fix(api): Throw `BadRequestException` when missing controls (#6716) --- .../app/workflows-v2/mappers/notification-template-mapper.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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.'); } } From ec35a01736b8c1fef4dddc1cf0368c1b37c84970 Mon Sep 17 00:00:00 2001 From: Richard Fontein <32132657+rifont@users.noreply.github.com> Date: Thu, 17 Oct 2024 16:48:04 +0200 Subject: [PATCH 4/6] feat(framework, api, web, application-generic): Add `name` and `description` to Framework workflow options (#6708) --- .../app/bridge/usecases/sync/sync.usecase.ts | 66 ++++-- .../api/src/app/events/e2e/bridge-sync.e2e.ts | 197 +++++++++++++++++- .../LocalStudioSidebarContent.tsx | 5 +- .../LocalStudioSidebarToggleButton.tsx | 8 +- .../editor_v2/useWorkflowDetailPageForm.ts | 2 + .../workflows/node-view/WorkflowNodes.tsx | 11 +- .../node-view/WorkflowsDetailPage.tsx | 6 +- .../StudioWorkflowSettingsSidePanel.tsx | 3 + .../WorkflowDetailFormContextProvider.tsx | 2 +- .../WorkflowGeneralSettingsForm.tsx | 112 ++++++++-- .../WorkflowSettingsSidePanelContent.tsx | 5 +- .../components/workflows/preferences/types.ts | 2 + .../workflows/table/WorkflowsTable.tsx | 46 ---- .../workflows/table/WorkflowsTable.types.ts | 3 - .../table/WorkflowsTableCellRenderers.tsx | 63 ------ .../components/workflows/table/index.ts | 1 - .../test-workflow/WorkflowsTestPage.tsx | 6 +- apps/web/src/studio/hooks/useBridgeAPI.ts | 5 +- .../src/studio/pages/StudioStepEditorPage.tsx | 2 +- apps/web/src/studio/types.ts | 22 -- .../update-workflow.usecase.ts | 2 +- libs/design-system/src/select/Select.tsx | 12 +- libs/design-system/src/select/Select.types.ts | 2 +- packages/framework/src/resources/index.ts | 2 +- .../framework/src/resources/workflow/index.ts | 181 +--------------- .../resources/workflow/workflow.resource.ts | 182 ++++++++++++++++ .../src/resources/workflow/workflow.test.ts | 36 +++- .../framework/src/types/discover.types.ts | 2 + .../framework/src/types/workflow.types.ts | 18 ++ 29 files changed, 623 insertions(+), 381 deletions(-) delete mode 100644 apps/web/src/studio/components/workflows/table/WorkflowsTable.tsx delete mode 100644 apps/web/src/studio/components/workflows/table/WorkflowsTable.types.ts delete mode 100644 apps/web/src/studio/components/workflows/table/WorkflowsTableCellRenderers.tsx delete mode 100644 apps/web/src/studio/components/workflows/table/index.ts create mode 100644 packages/framework/src/resources/workflow/workflow.resource.ts 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..2d93d73b2c5 100644 --- a/apps/api/src/app/bridge/usecases/sync/sync.usecase.ts +++ b/apps/api/src/app/bridge/usecases/sync/sync.usecase.ts @@ -84,7 +84,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 +100,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 +122,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 +138,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 +192,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 +207,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 +224,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 +258,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 +268,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 +298,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 +319,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/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/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 ( +