From 1ec7970f0aa296387f13076a20bec4124e5f4340 Mon Sep 17 00:00:00 2001 From: Sokratis Vidros Date: Mon, 1 Jul 2024 12:40:23 +0300 Subject: [PATCH 01/18] fix(web): Minor fix for BridgeAPI client Support localtunnel.it in Studio if developers prefer to use it instead of the Novu tunnel. --- apps/web/src/bridgeApi/bridgeApi.client.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/web/src/bridgeApi/bridgeApi.client.ts b/apps/web/src/bridgeApi/bridgeApi.client.ts index 03177dada2bd..4fdefb2f25e4 100644 --- a/apps/web/src/bridgeApi/bridgeApi.client.ts +++ b/apps/web/src/bridgeApi/bridgeApi.client.ts @@ -27,6 +27,8 @@ export function buildBridgeHTTPClient(baseURL: string) { baseURL, headers: { 'Content-Type': 'application/json', + // Required if a custom tunnel is used by developers such as localtunnel.it + 'Bypass-Tunnel-Reminder': true, }, }); From 7fa6ee467501574851ee284a117f3c4a1b9739d5 Mon Sep 17 00:00:00 2001 From: George Desipris <73396808+desiprisg@users.noreply.github.com> Date: Mon, 1 Jul 2024 12:59:31 +0300 Subject: [PATCH 02/18] fix(js): Use key prefix instead of id for alpha shades (#5890) --- packages/js/src/ui/helpers/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/js/src/ui/helpers/utils.ts b/packages/js/src/ui/helpers/utils.ts index 47898df77295..8968c00d4cee 100644 --- a/packages/js/src/ui/helpers/utils.ts +++ b/packages/js/src/ui/helpers/utils.ts @@ -68,7 +68,7 @@ export function generatesAlphaShadesFromColor(props: { color: string; key: strin const rules = []; for (let i = 0; i < shades.length; i++) { const shade = shades[i]; - const cssVariableAlphaRule = `.${props.id} { --nv-${props.id}-${shade}: oklch(from ${props.color} l c h / ${ + const cssVariableAlphaRule = `.${props.id} { --nv-${props.key}-${shade}: oklch(from ${props.color} l c h / ${ shade / 1000 }); }`; rules.push(cssVariableAlphaRule); From 46ea4ec16e6fa9881e4aa390f615e5366f61a98c Mon Sep 17 00:00:00 2001 From: Richard Fontein <32132657+rifont@users.noreply.github.com> Date: Mon, 1 Jul 2024 11:13:51 +0100 Subject: [PATCH 03/18] chore(novu): bump version to 0.24.3-alpha.8 (#5891) * chore(cli): bump version to 0.24.3-alpha.6 * chore(cli): update version and fix studio spinner emoji --- packages/cli/package.json | 2 +- packages/cli/src/commands/dev.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 196eec750b25..cd111f9d1e29 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "novu", - "version": "0.24.3-alpha.5", + "version": "0.24.3-alpha.8", "description": "Novu CLI. Used to sync with Novu Cloud, and run Novu Studio.", "main": "index.js", "scripts": { diff --git a/packages/cli/src/commands/dev.ts b/packages/cli/src/commands/dev.ts index b03bf3e07b83..961f78da50ec 100644 --- a/packages/cli/src/commands/dev.ts +++ b/packages/cli/src/commands/dev.ts @@ -55,8 +55,8 @@ export async function devCommand(options: DevCommandOptions) { const studioSpinner = ora('Starting local studio server').start(); await httpServer.listen(); - studioSpinner.succeed(`🧑‍💻 Studio → ${httpServer.getStudioAddress()}`); dashboardSpinner.succeed(`🖥️ Dashboard → ${parsedOptions.dashboardUrl}`); + studioSpinner.succeed(`🎨 Studio → ${httpServer.getStudioAddress()}`); if (process.env.NODE_ENV !== 'dev') { await open(httpServer.getStudioAddress()); } From 37b0d6440bf5d07aa5fa52085790f7bee7f6d2b3 Mon Sep 17 00:00:00 2001 From: Sokratis Vidros Date: Mon, 1 Jul 2024 14:33:47 +0300 Subject: [PATCH 04/18] fix(web): Render legacy Echo trigger step This is a regression --- .../components/workflows/test-workflow/WorkflowsTestPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/studio/components/workflows/test-workflow/WorkflowsTestPage.tsx b/apps/web/src/studio/components/workflows/test-workflow/WorkflowsTestPage.tsx index ce6318694d8c..a7cfb90b1a5b 100644 --- a/apps/web/src/studio/components/workflows/test-workflow/WorkflowsTestPage.tsx +++ b/apps/web/src/studio/components/workflows/test-workflow/WorkflowsTestPage.tsx @@ -154,8 +154,8 @@ export const WorkflowsTestPage = () => { payloadSchema={ workflow?.payload?.schema || workflow?.data?.schema || - (template as any)?.rawData?.payload.schema || - (template as any)?.rawData?.data.schema + (template as any)?.rawData?.payload?.schema || + (template as any)?.rawData?.data?.schema } to={{ subscriberId: testUser?.id || '', From 38dfbec778b2f2fee1d632e813520baa8a2952d2 Mon Sep 17 00:00:00 2001 From: Sokratis Vidros Date: Mon, 1 Jul 2024 15:02:57 +0300 Subject: [PATCH 05/18] fix(create-novu-app): Update copywriting It's React Email according to https://react.email/docs/introduction --- packages/create-novu-app/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-novu-app/index.ts b/packages/create-novu-app/index.ts index 1a7563442435..013bb0fa04d9 100644 --- a/packages/create-novu-app/index.ts +++ b/packages/create-novu-app/index.ts @@ -166,7 +166,7 @@ async function run(): Promise { if (ciInfo.isCI) { program.tailwind = getPrefOrDefault('tailwind'); } else { - const tw = blue('React E-mail'); + const tw = blue('React Email'); const { reactEmail } = await prompts({ onState: onPromptState, type: 'toggle', From 59c8d3ddc551fedf1b22b7ae714259be4ecc8ce6 Mon Sep 17 00:00:00 2001 From: Sokratis Vidros Date: Mon, 1 Jul 2024 15:36:44 +0300 Subject: [PATCH 06/18] fix(worker): Revert I18n logic on SMS step This caused a production regression. --- .../workflow/usecases/send-message/send-message-sms.usecase.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/worker/src/app/workflow/usecases/send-message/send-message-sms.usecase.ts b/apps/worker/src/app/workflow/usecases/send-message/send-message-sms.usecase.ts index e906d246128f..39ca63316a46 100644 --- a/apps/worker/src/app/workflow/usecases/send-message/send-message-sms.usecase.ts +++ b/apps/worker/src/app/workflow/usecases/send-message/send-message-sms.usecase.ts @@ -99,7 +99,7 @@ export class SendMessageSms extends SendMessageBase { CompileTemplateCommand.create({ template: step.template.content as string, data: this.getCompilePayload(command.compileContext), - i18next: i18nextInstance, + // i18next: i18nextInstance, }) ); From d9814af178ce4466329c1293575593e8e40d7928 Mon Sep 17 00:00:00 2001 From: Gali Ainouz Baum Date: Mon, 1 Jul 2024 16:29:46 +0300 Subject: [PATCH 07/18] fix(web): set cookie as secure (#5892) * fix(web): set cookie as secure * fix(web): set cookie as secure --- apps/web/src/utils/cookies.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/utils/cookies.ts b/apps/web/src/utils/cookies.ts index 116b4fe12927..c7010fb44b72 100644 --- a/apps/web/src/utils/cookies.ts +++ b/apps/web/src/utils/cookies.ts @@ -42,7 +42,7 @@ const ONBOARDING_COOKIE_EXPIRY_DAYS = 10 * 365; export function setNovuOnboardingStepCookie() { return novuOnboardedCookie.set('1', { expires: ONBOARDING_COOKIE_EXPIRY_DAYS, - sameSite: 'none', - secure: window.location.protocol === 'https', + sameSite: 'None', + secure: window.location.protocol === 'https:', }); } From 5d59c16ce1c546991897e942f1602cc79ff96ad8 Mon Sep 17 00:00:00 2001 From: George Djabarov <39195835+djabarovgeorge@users.noreply.github.com> Date: Mon, 1 Jul 2024 16:51:03 +0300 Subject: [PATCH 08/18] feat(api, worker): add support for stateless step controls (#5889) * feat(bridge): add support for stateless step controls * fix: remove only flag * feat: update submodule hash --- .source | 2 +- .../events/dtos/trigger-event-request.dto.ts | 3 + .../app/events/e2e/bridge-trigger.e2e-ee.ts | 79 +++++++++++++++++-- .../src/app/events/e2e/echo-trigger.e2e-ee.ts | 0 apps/api/src/app/events/events.controller.ts | 1 + .../parse-event-request.command.ts | 3 + .../usecases/add-job/add-job.command.ts | 3 + .../store-subscriber-jobs.command.ts | 2 - .../store-subscriber-jobs.usecase.ts | 3 +- .../subscriber-job-bound.command.ts | 10 ++- .../subscriber-job-bound.usecase.ts | 22 +++--- .../packages/echo/echo-worker/tsconfig.json | 1 - .../src/dtos/process-subscriber-job.dto.ts | 2 + .../src/dtos/workflow-job.dto.ts | 2 + .../create-notification-jobs.command.ts | 3 + .../create-notification-jobs.usecase.ts | 2 + .../trigger-broadcast.usecase.ts | 1 + .../trigger-event/trigger-event.command.ts | 3 + .../trigger-event/trigger-event.usecase.ts | 16 +--- .../trigger-multicast.usecase.ts | 9 +-- .../notification-template.entity.ts | 3 + .../notification/notification.entity.ts | 4 +- .../notification/notification.schema.ts | 3 + libs/shared/src/dto/controls/controls.dto.ts | 4 + libs/shared/src/dto/controls/index.ts | 1 + libs/shared/src/dto/index.ts | 1 + .../notification-template.interface.ts | 6 ++ .../controls/control-variables-level.enum.ts | 4 + libs/shared/src/types/controls/index.ts | 1 + libs/shared/src/types/index.ts | 1 + 30 files changed, 147 insertions(+), 48 deletions(-) delete mode 100644 apps/api/src/app/events/e2e/echo-trigger.e2e-ee.ts create mode 100644 libs/shared/src/dto/controls/controls.dto.ts create mode 100644 libs/shared/src/dto/controls/index.ts create mode 100644 libs/shared/src/types/controls/control-variables-level.enum.ts create mode 100644 libs/shared/src/types/controls/index.ts diff --git a/.source b/.source index a574177bd567..450a664d71c2 160000 --- a/.source +++ b/.source @@ -1 +1 @@ -Subproject commit a574177bd56717ca5c1a3ca7944a521d3ffdbbfc +Subproject commit 450a664d71c2548cb08a593d9e5e3727d7e83d87 diff --git a/apps/api/src/app/events/dtos/trigger-event-request.dto.ts b/apps/api/src/app/events/dtos/trigger-event-request.dto.ts index 41ce7ce67ee1..50c4da4c6375 100644 --- a/apps/api/src/app/events/dtos/trigger-event-request.dto.ts +++ b/apps/api/src/app/events/dtos/trigger-event-request.dto.ts @@ -12,6 +12,7 @@ import { import { Type } from 'class-transformer'; import { ApiExtraModels, ApiProperty, ApiPropertyOptional, getSchemaPath } from '@nestjs/swagger'; import { + ControlsDto, TopicKey, TriggerRecipients, TriggerRecipientsTypeEnum, @@ -137,6 +138,8 @@ export class TriggerEventRequestDto { @ValidateNested() @Type(() => TenantPayloadDto) tenant?: TriggerTenantContext; + + controls?: ControlsDto; } export class BulkTriggerEventDto { diff --git a/apps/api/src/app/events/e2e/bridge-trigger.e2e-ee.ts b/apps/api/src/app/events/e2e/bridge-trigger.e2e-ee.ts index 35481cbd639a..cbc3c558cb3e 100644 --- a/apps/api/src/app/events/e2e/bridge-trigger.e2e-ee.ts +++ b/apps/api/src/app/events/e2e/bridge-trigger.e2e-ee.ts @@ -2,13 +2,13 @@ import axios from 'axios'; import { expect } from 'chai'; import { v4 as uuidv4 } from 'uuid'; -import { UserSession, SubscribersService } from '@novu/testing'; +import { SubscribersService, UserSession } from '@novu/testing'; import { + ExecutionDetailsRepository, + JobRepository, MessageRepository, - SubscriberEntity, NotificationTemplateRepository, - JobRepository, - ExecutionDetailsRepository, + SubscriberEntity, } from '@novu/dal'; import { ChannelTypeEnum, @@ -566,8 +566,8 @@ contexts.forEach((context: Context) => { expect(messagesAfter[0].content).to.match(/people waited for \d+ seconds/); }); - it(`should trigger the echo workflow with control variables [${context.name}]`, async () => { - const workflowId = `control-variables-workflow-${context.name + '-' + uuidv4()}`; + it(`should trigger the echo workflow with control default and payload data [${context.name}]`, async () => { + const workflowId = `default-payload-params-workflow-${context.name + '-' + uuidv4()}`; const newWorkflow = workflow( workflowId, async ({ step, payload }) => { @@ -622,6 +622,64 @@ contexts.forEach((context: Context) => { expect(sentMessage[1].subject).to.include('prefix Hello default_name'); expect(sentMessage[0].subject).to.include('prefix Hello payload_name'); }); + + it(`should trigger the echo workflow with control variables [${context.name}]`, async () => { + const workflowId = `control-variables-workflow-${context.name + '-' + uuidv4()}`; + const stepId = 'send-email'; + const newWorkflow = workflow( + workflowId, + async ({ step, payload }) => { + await step.email( + stepId, + async (controls) => { + return { + subject: 'email subject ' + controls.name, + body: 'Body result', + }; + }, + { + controlSchema: { + type: 'object', + properties: { + name: { type: 'string', default: 'control default' }, + }, + } as const, + } + ); + }, + { + // todo delete + payloadSchema: { + type: 'object', + properties: { + name: { type: 'string', default: 'default_name' }, + }, + required: [], + additionalProperties: false, + } as const, + } + ); + + await echoServer.start({ workflows: [newWorkflow] }); + + if (context.isStateful) { + await discoverAndSyncEcho(session, workflowsRepository, workflowId, echoServer); + await saveControlVariables(session, workflowId, stepId, { variables: { name: 'stored_control_name' } }); + } + + const controls = { controls: { step: [{ stepId: stepId, name: 'stored_control_name' }] } }; + await triggerEvent(session, workflowId, subscriber, controls, bridge); + await session.awaitRunningJobs(); + + const sentMessage = await messageRepository.find({ + _environmentId: session.environment._id, + _subscriberId: subscriber._id, + channel: StepTypeEnum.EMAIL, + }); + + expect(sentMessage.length).to.be.eq(1); + expect(sentMessage[0].subject).to.equal('email subject stored_control_name'); + }); }); }); @@ -689,6 +747,15 @@ async function discoverAndSyncEcho( return discoverResponse; } +async function saveControlVariables( + session: UserSession, + workflowIdentifier?: string, + stepIdentifier?: string, + payloadBody?: any +) { + return await session.testAgent.put(`/v1/bridge/controls/${workflowIdentifier}/${stepIdentifier}`).send(payloadBody); +} + async function markAllSubscriberMessagesAs(session: UserSession, subscriberId: string, markAs: MessagesStatusEnum) { const response = await axios.post( `${session.serverUrl}/v1/subscribers/${subscriberId}/messages/mark-all`, diff --git a/apps/api/src/app/events/e2e/echo-trigger.e2e-ee.ts b/apps/api/src/app/events/e2e/echo-trigger.e2e-ee.ts deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/apps/api/src/app/events/events.controller.ts b/apps/api/src/app/events/events.controller.ts index 6c77a532c6d2..3746816c882b 100644 --- a/apps/api/src/app/events/events.controller.ts +++ b/apps/api/src/app/events/events.controller.ts @@ -83,6 +83,7 @@ export class EventsController { addressingType: AddressingTypeEnum.MULTICAST, requestCategory: TriggerRequestCategoryEnum.SINGLE, bridgeUrl: body.bridgeUrl, + controls: body.controls, }) ); diff --git a/apps/api/src/app/events/usecases/parse-event-request/parse-event-request.command.ts b/apps/api/src/app/events/usecases/parse-event-request/parse-event-request.command.ts index 37727362ebee..ecc3aa687308 100644 --- a/apps/api/src/app/events/usecases/parse-event-request/parse-event-request.command.ts +++ b/apps/api/src/app/events/usecases/parse-event-request/parse-event-request.command.ts @@ -1,6 +1,7 @@ import { IsDefined, IsString, IsOptional, ValidateNested, ValidateIf, IsEnum, IsObject } from 'class-validator'; import { AddressingTypeEnum, + ControlsDto, TriggerRecipients, TriggerRecipientSubscriber, TriggerRequestCategoryEnum, @@ -41,6 +42,8 @@ export class ParseEventRequestBaseCommand extends EnvironmentWithUserCommand { @IsString() @IsOptional() bridgeUrl?: string; + + controls?: ControlsDto; } export class ParseEventRequestMulticastCommand extends ParseEventRequestBaseCommand { diff --git a/apps/worker/src/app/workflow/usecases/add-job/add-job.command.ts b/apps/worker/src/app/workflow/usecases/add-job/add-job.command.ts index b21e6cf7de05..b81555b61877 100644 --- a/apps/worker/src/app/workflow/usecases/add-job/add-job.command.ts +++ b/apps/worker/src/app/workflow/usecases/add-job/add-job.command.ts @@ -1,6 +1,7 @@ import { IsDefined } from 'class-validator'; import { JobEntity } from '@novu/dal'; import { EnvironmentWithUserCommand } from '@novu/application-generic'; +import { ControlsDto } from '@novu/shared'; export class AddJobCommand extends EnvironmentWithUserCommand { @IsDefined() @@ -8,4 +9,6 @@ export class AddJobCommand extends EnvironmentWithUserCommand { @IsDefined() job: JobEntity; + + controls?: ControlsDto; } diff --git a/apps/worker/src/app/workflow/usecases/store-subscriber-jobs/store-subscriber-jobs.command.ts b/apps/worker/src/app/workflow/usecases/store-subscriber-jobs/store-subscriber-jobs.command.ts index a7dbd3aeaf9e..919009fc257e 100644 --- a/apps/worker/src/app/workflow/usecases/store-subscriber-jobs/store-subscriber-jobs.command.ts +++ b/apps/worker/src/app/workflow/usecases/store-subscriber-jobs/store-subscriber-jobs.command.ts @@ -6,6 +6,4 @@ import { EnvironmentCommand } from '@novu/application-generic'; export class StoreSubscriberJobsCommand extends EnvironmentCommand { @IsDefined() jobs: Omit[]; - - bridge?: any; } diff --git a/apps/worker/src/app/workflow/usecases/store-subscriber-jobs/store-subscriber-jobs.usecase.ts b/apps/worker/src/app/workflow/usecases/store-subscriber-jobs/store-subscriber-jobs.usecase.ts index 4f1b21d66f44..e90d2f6a1d77 100644 --- a/apps/worker/src/app/workflow/usecases/store-subscriber-jobs/store-subscriber-jobs.usecase.ts +++ b/apps/worker/src/app/workflow/usecases/store-subscriber-jobs/store-subscriber-jobs.usecase.ts @@ -45,7 +45,8 @@ export class StoreSubscriberJobs { organizationId: firstJob._organizationId, jobId: firstJob._id, job: firstJob, - bridge: command.bridge, + bridge: firstJob.bridge, + controlVariables: firstJob.controlVariables, }; await this.addJob.execute(addJobCommand); diff --git a/apps/worker/src/app/workflow/usecases/subscriber-job-bound/subscriber-job-bound.command.ts b/apps/worker/src/app/workflow/usecases/subscriber-job-bound/subscriber-job-bound.command.ts index bd5e0ee162cf..84417019ab89 100644 --- a/apps/worker/src/app/workflow/usecases/subscriber-job-bound/subscriber-job-bound.command.ts +++ b/apps/worker/src/app/workflow/usecases/subscriber-job-bound/subscriber-job-bound.command.ts @@ -1,6 +1,12 @@ import { IsDefined, IsString, IsOptional, ValidateNested, IsMongoId, IsEnum } from 'class-validator'; -import { ISubscribersDefine, ITenantDefine, SubscriberSourceEnum, TriggerRequestCategoryEnum } from '@novu/shared'; +import { + ControlsDto, + ISubscribersDefine, + ITenantDefine, + SubscriberSourceEnum, + TriggerRequestCategoryEnum, +} from '@novu/shared'; import { SubscriberEntity } from '@novu/dal'; import { EnvironmentWithUserCommand } from '@novu/application-generic'; import { DiscoverWorkflowOutput } from '@novu/framework'; @@ -43,4 +49,6 @@ export class SubscriberJobBoundCommand extends EnvironmentWithUserCommand { requestCategory?: TriggerRequestCategoryEnum; bridge?: { url: string; workflow: DiscoverWorkflowOutput }; + + controls?: ControlsDto; } diff --git a/apps/worker/src/app/workflow/usecases/subscriber-job-bound/subscriber-job-bound.usecase.ts b/apps/worker/src/app/workflow/usecases/subscriber-job-bound/subscriber-job-bound.usecase.ts index a1c1f083522d..d360db6fee18 100644 --- a/apps/worker/src/app/workflow/usecases/subscriber-job-bound/subscriber-job-bound.usecase.ts +++ b/apps/worker/src/app/workflow/usecases/subscriber-job-bound/subscriber-job-bound.usecase.ts @@ -1,13 +1,9 @@ import { Injectable, Logger } from '@nestjs/common'; -import { - JobRepository, - NotificationTemplateEntity, - NotificationTemplateRepository, - IntegrationRepository, -} from '@novu/dal'; +import { NotificationTemplateEntity, NotificationTemplateRepository, IntegrationRepository } from '@novu/dal'; import { ChannelTypeEnum, + ControlVariablesLevelEnum, InAppProviderIdEnum, ISubscribersDefine, ProvidersIdEnum, @@ -26,7 +22,6 @@ import { PinoLogger, ProcessSubscriber, ProcessSubscriberCommand, - ProcessTenant, } from '@novu/application-generic'; import { SubscriberJobBoundCommand } from './subscriber-job-bound.command'; @@ -95,6 +90,7 @@ export class SubscriberJobBound { source: command.payload.__source || 'api', subscriberSource: _subscriberSource || null, requestCategory: requestCategory || null, + statelessWorkflow: !!command.bridge?.url, }); const subscriberProcessed = await this.processSubscriber.execute( @@ -108,11 +104,6 @@ export class SubscriberJobBound { // If no subscriber makes no sense to try to create notification if (!subscriberProcessed) { - /** - * TODO: Potentially add a CreateExecutionDetails entry. Right now we - * have the limitation we need a job to be created for that. Here there - * is no job at this point. - */ Logger.warn( `Subscriber ${JSON.stringify(subscriber.subscriberId)} of organization ${ command.organizationId @@ -152,7 +143,6 @@ export class SubscriberJobBound { environmentId: command.environmentId, jobs: notificationJobs, organizationId: command.organizationId, - bridge: command.bridge, }) ); } @@ -172,8 +162,14 @@ export class SubscriberJobBound { ...bridgeWorkflow, type: 'ECHO', steps: bridgeWorkflow.steps.map((step) => { + const stepControlVariables = command.payload.controls?.[ControlVariablesLevelEnum.STEP_CONTROLS].find( + (control) => control.stepId === step.stepId + ); + return { ...step, + bridgeUrl: command.bridge?.url, + controlVariables: stepControlVariables, active: true, template: { type: step.type, diff --git a/enterprise/packages/echo/echo-worker/tsconfig.json b/enterprise/packages/echo/echo-worker/tsconfig.json index 15a26cfbb8fb..4781a4fb473d 100644 --- a/enterprise/packages/echo/echo-worker/tsconfig.json +++ b/enterprise/packages/echo/echo-worker/tsconfig.json @@ -9,7 +9,6 @@ "rootDir": "src", "sourceMap": true, "strict": true, - "sourceMap": true, "types": ["node", "mocha", "chai", "sinon"], "skipLibCheck": true, "typeRoots": ["./node_modules/@types", "../../node_modules/@types"] diff --git a/libs/application-generic/src/dtos/process-subscriber-job.dto.ts b/libs/application-generic/src/dtos/process-subscriber-job.dto.ts index 1da7c23bbaa5..3c79dcf9ec17 100644 --- a/libs/application-generic/src/dtos/process-subscriber-job.dto.ts +++ b/libs/application-generic/src/dtos/process-subscriber-job.dto.ts @@ -1,4 +1,5 @@ import { + ControlsDto, ISubscribersDefine, ITenantDefine, SubscriberSourceEnum, @@ -27,6 +28,7 @@ export interface IProcessSubscriberDataDto { _subscriberSource: SubscriberSourceEnum; requestCategory?: TriggerRequestCategoryEnum; bridge?: { url: string; workflow: DiscoverWorkflowOutput }; + controls?: ControlsDto; } export interface IProcessSubscriberJobDto extends IJobParams { diff --git a/libs/application-generic/src/dtos/workflow-job.dto.ts b/libs/application-generic/src/dtos/workflow-job.dto.ts index 81e074d4e7d0..864ec666d79b 100644 --- a/libs/application-generic/src/dtos/workflow-job.dto.ts +++ b/libs/application-generic/src/dtos/workflow-job.dto.ts @@ -1,5 +1,6 @@ import { AddressingTypeEnum, + ControlsDto, TriggerRecipientsPayload, TriggerRecipientSubscriber, TriggerRequestCategoryEnum, @@ -35,6 +36,7 @@ export type IWorkflowDataDto = { requestCategory?: TriggerRequestCategoryEnum; bridgeUrl?: string; bridgeWorkflow?: DiscoverWorkflowOutput; + controls?: ControlsDto; } & Addressing; export interface IWorkflowJobDto extends IJobParams { diff --git a/libs/application-generic/src/usecases/create-notification-jobs/create-notification-jobs.command.ts b/libs/application-generic/src/usecases/create-notification-jobs/create-notification-jobs.command.ts index 48fcddb4419f..0abefc7246f0 100644 --- a/libs/application-generic/src/usecases/create-notification-jobs/create-notification-jobs.command.ts +++ b/libs/application-generic/src/usecases/create-notification-jobs/create-notification-jobs.command.ts @@ -3,6 +3,7 @@ import { IsDefined, IsString, IsOptional } from 'class-validator'; import { NotificationTemplateEntity, SubscriberEntity } from '@novu/dal'; import { ChannelTypeEnum, + ControlsDto, ISubscribersDefine, ITenantDefine, ProvidersIdEnum, @@ -44,4 +45,6 @@ export class CreateNotificationJobsCommand extends EnvironmentWithUserCommand { tenant?: ITenantDefine; bridge?: any; + + controls?: ControlsDto; } diff --git a/libs/application-generic/src/usecases/create-notification-jobs/create-notification-jobs.usecase.ts b/libs/application-generic/src/usecases/create-notification-jobs/create-notification-jobs.usecase.ts index 1877891ade36..7c66001f1027 100644 --- a/libs/application-generic/src/usecases/create-notification-jobs/create-notification-jobs.usecase.ts +++ b/libs/application-generic/src/usecases/create-notification-jobs/create-notification-jobs.usecase.ts @@ -63,6 +63,7 @@ export class CreateNotificationJobs { expireAt: this.calculateExpireAt(command), channels, bridge: command.bridge, + controls: command.controls, }); if (!notification) { @@ -144,6 +145,7 @@ export class CreateNotificationJobs { overrides: command.overrides, tenant: command.tenant, step: { + bridgeUrl: command.bridge?.url, template: { _environmentId: command.environmentId, _organizationId: command.organizationId, diff --git a/libs/application-generic/src/usecases/trigger-broadcast/trigger-broadcast.usecase.ts b/libs/application-generic/src/usecases/trigger-broadcast/trigger-broadcast.usecase.ts index b57f479f5386..cbc2a4f6499b 100644 --- a/libs/application-generic/src/usecases/trigger-broadcast/trigger-broadcast.usecase.ts +++ b/libs/application-generic/src/usecases/trigger-broadcast/trigger-broadcast.usecase.ts @@ -149,6 +149,7 @@ export class TriggerBroadcast { subscriber, templateId: command.template._id, _subscriberSource: SubscriberSourceEnum.BROADCAST, + controls: command.controls, requestCategory: command.requestCategory, bridge: { url: command.bridgeUrl, diff --git a/libs/application-generic/src/usecases/trigger-event/trigger-event.command.ts b/libs/application-generic/src/usecases/trigger-event/trigger-event.command.ts index 7049dca30eb1..df039af52b6b 100644 --- a/libs/application-generic/src/usecases/trigger-event/trigger-event.command.ts +++ b/libs/application-generic/src/usecases/trigger-event/trigger-event.command.ts @@ -9,6 +9,7 @@ import { import { AddressingTypeEnum, + ControlsDto, TriggerRecipientsPayload, TriggerRecipientSubscriber, TriggerRequestCategoryEnum, @@ -53,6 +54,8 @@ export class TriggerEventBaseCommand extends EnvironmentWithUserCommand { @IsOptional() bridgeWorkflow?: DiscoverWorkflowOutput; + + controls?: ControlsDto; } export class TriggerEventMulticastCommand extends TriggerEventBaseCommand { diff --git a/libs/application-generic/src/usecases/trigger-event/trigger-event.usecase.ts b/libs/application-generic/src/usecases/trigger-event/trigger-event.usecase.ts index c13de5567857..3ebefa13af38 100644 --- a/libs/application-generic/src/usecases/trigger-event/trigger-event.usecase.ts +++ b/libs/application-generic/src/usecases/trigger-event/trigger-event.usecase.ts @@ -1,9 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import * as Sentry from '@sentry/node'; -import { ModuleRef } from '@nestjs/core'; import { - EnvironmentRepository, IntegrationRepository, JobEntity, JobRepository, @@ -20,13 +18,8 @@ import { TriggerRecipientSubscriber, TriggerTenantContext, } from '@novu/shared'; -import { DiscoverOutput, DiscoverWorkflowOutput } from '@novu/framework'; -import { - TriggerEventBroadcastCommand, - TriggerEventCommand, - TriggerEventMulticastCommand, -} from './trigger-event.command'; +import { TriggerEventCommand } from './trigger-event.command'; import { ProcessSubscriber, ProcessSubscriberCommand, @@ -45,7 +38,6 @@ import { TriggerMulticast, TriggerMulticastCommand, } from '../trigger-multicast'; -import { IUseCaseInterface, requireInject } from '../../utils/require-inject'; const LOG_CONTEXT = 'TriggerEventUseCase'; @@ -157,8 +149,6 @@ export class TriggerEvent { await this.triggerMulticast.execute( TriggerMulticastCommand.create({ ...mappedCommand, - bridgeUrl: command.bridgeUrl, - bridgeWorkflow: command.bridgeWorkflow, actor: actorProcessed, template: template || @@ -171,8 +161,6 @@ export class TriggerEvent { await this.triggerBroadcast.execute( TriggerBroadcastCommand.create({ ...mappedCommand, - bridgeUrl: command.bridgeUrl, - bridgeWorkflow: command.bridgeWorkflow, actor: actorProcessed, template: template || @@ -186,8 +174,6 @@ export class TriggerEvent { TriggerMulticastCommand.create({ addressingType: AddressingTypeEnum.MULTICAST, ...(mappedCommand as TriggerMulticastCommand), - bridgeUrl: command.bridgeUrl, - bridgeWorkflow: command.bridgeWorkflow, actor: actorProcessed, template: template || diff --git a/libs/application-generic/src/usecases/trigger-multicast/trigger-multicast.usecase.ts b/libs/application-generic/src/usecases/trigger-multicast/trigger-multicast.usecase.ts index da007bc3b043..e1158039eef2 100644 --- a/libs/application-generic/src/usecases/trigger-multicast/trigger-multicast.usecase.ts +++ b/libs/application-generic/src/usecases/trigger-multicast/trigger-multicast.usecase.ts @@ -45,13 +45,7 @@ export class TriggerMulticast { @InstrumentUsecase() async execute(command: TriggerMulticastCommand) { { - const { - environmentId, - organizationId, - to: recipients, - actor, - userId, - } = command; + const { environmentId, organizationId, to: recipients, actor } = command; const mappedRecipients = Array.isArray(recipients) ? recipients @@ -266,6 +260,7 @@ export const mapSubscribersToJobs = ( templateId: command.template._id, _subscriberSource: _subscriberSource, requestCategory: command.requestCategory, + controls: command.controls, bridge: { url: command.bridgeUrl, workflow: command.bridgeWorkflow, diff --git a/libs/dal/src/repositories/notification-template/notification-template.entity.ts b/libs/dal/src/repositories/notification-template/notification-template.entity.ts index 9814f3fc5ab5..5da3f6126a76 100644 --- a/libs/dal/src/repositories/notification-template/notification-template.entity.ts +++ b/libs/dal/src/repositories/notification-template/notification-template.entity.ts @@ -16,6 +16,7 @@ import { INotificationTemplateStep, IMessageTemplate, NotificationTemplateTypeEnum, + ControlsDto, } from '@novu/shared'; import { NotificationGroupEntity } from '../notification-group'; @@ -127,6 +128,8 @@ export class StepVariantEntity implements IStepVariant { shouldStopOnFail?: boolean; bridgeUrl?: string; + + controlVariables?: ControlsDto; } export class NotificationStepEntity extends StepVariantEntity implements INotificationTemplateStep { diff --git a/libs/dal/src/repositories/notification/notification.entity.ts b/libs/dal/src/repositories/notification/notification.entity.ts index cde46f0f91a1..f21692510093 100644 --- a/libs/dal/src/repositories/notification/notification.entity.ts +++ b/libs/dal/src/repositories/notification/notification.entity.ts @@ -1,4 +1,4 @@ -import { ISubscribersDefine, StepTypeEnum } from '@novu/shared'; +import { ControlsDto, ISubscribersDefine, StepTypeEnum } from '@novu/shared'; import { NotificationTemplateEntity } from '../notification-template'; import type { OrganizationId } from '../organization'; @@ -39,6 +39,8 @@ export class NotificationEntity { expireAt?: string; bridge?: any; + + controls?: ControlsDto; } export type NotificationDBModel = ChangePropsValueType< diff --git a/libs/dal/src/repositories/notification/notification.schema.ts b/libs/dal/src/repositories/notification/notification.schema.ts index 27fd1724c101..2bf9bf5bd6e2 100644 --- a/libs/dal/src/repositories/notification/notification.schema.ts +++ b/libs/dal/src/repositories/notification/notification.schema.ts @@ -44,6 +44,9 @@ const notificationSchema = new Schema( bridge: { type: Schema.Types.Mixed, }, + controls: { + type: Schema.Types.Mixed, + }, }, schemaOptions ); diff --git a/libs/shared/src/dto/controls/controls.dto.ts b/libs/shared/src/dto/controls/controls.dto.ts new file mode 100644 index 000000000000..2d3047661341 --- /dev/null +++ b/libs/shared/src/dto/controls/controls.dto.ts @@ -0,0 +1,4 @@ +import { ControlVariablesLevelEnum } from '../../types'; + +export type ControlsDto = Record; +type Control = { stepId: string } & Record; diff --git a/libs/shared/src/dto/controls/index.ts b/libs/shared/src/dto/controls/index.ts new file mode 100644 index 000000000000..ea4825a6c32f --- /dev/null +++ b/libs/shared/src/dto/controls/index.ts @@ -0,0 +1 @@ +export * from './controls.dto'; diff --git a/libs/shared/src/dto/index.ts b/libs/shared/src/dto/index.ts index 162ee1dc40ed..4228accab980 100644 --- a/libs/shared/src/dto/index.ts +++ b/libs/shared/src/dto/index.ts @@ -12,3 +12,4 @@ export * from './tenant'; export * from './workflow-override'; export * from './widget'; export * from './session'; +export * from './controls'; diff --git a/libs/shared/src/entities/notification-template/notification-template.interface.ts b/libs/shared/src/entities/notification-template/notification-template.interface.ts index c9b441d820f8..1e1d0c6748bf 100644 --- a/libs/shared/src/entities/notification-template/notification-template.interface.ts +++ b/libs/shared/src/entities/notification-template/notification-template.interface.ts @@ -5,6 +5,7 @@ import { IMessageTemplate } from '../message-template'; import { IPreferenceChannels } from '../subscriber-preference'; import { IWorkflowStepMetadata } from '../step'; import { INotificationGroup } from '../notification-group'; +import { ControlsDto } from '../../dto'; export enum NotificationTemplateTypeEnum { REGULAR = 'REGULAR', @@ -96,6 +97,11 @@ export interface IStepVariant { controls?: { schema: JSONSchema7; }; + /* + * controlVariables exists + * only on none production environment in order to provide stateless control variables on fly + */ + controlVariables?: ControlsDto; bridgeUrl?: string; } diff --git a/libs/shared/src/types/controls/control-variables-level.enum.ts b/libs/shared/src/types/controls/control-variables-level.enum.ts new file mode 100644 index 000000000000..861163c8a36e --- /dev/null +++ b/libs/shared/src/types/controls/control-variables-level.enum.ts @@ -0,0 +1,4 @@ +export enum ControlVariablesLevelEnum { + WORKFLOW_CONTROLS = 'workflow', + STEP_CONTROLS = 'step', +} diff --git a/libs/shared/src/types/controls/index.ts b/libs/shared/src/types/controls/index.ts new file mode 100644 index 000000000000..384a81b755da --- /dev/null +++ b/libs/shared/src/types/controls/index.ts @@ -0,0 +1 @@ +export * from './control-variables-level.enum'; diff --git a/libs/shared/src/types/index.ts b/libs/shared/src/types/index.ts index 0bb8c1fe699e..cbbc948b106a 100644 --- a/libs/shared/src/types/index.ts +++ b/libs/shared/src/types/index.ts @@ -26,3 +26,4 @@ export * from './product-features'; export * from './resource-limiting'; export * from './files'; export * from './storage'; +export * from './controls'; From 30df5d4e9459f11a818dd6e2ef8aeca5d4f7bf00 Mon Sep 17 00:00:00 2001 From: Richard Fontein <32132657+rifont@users.noreply.github.com> Date: Mon, 1 Jul 2024 16:55:49 +0100 Subject: [PATCH 09/18] refactor(SyncInfoModal): replace useApiKeysPage with useStudioState (#5898) --- .../layout/components/v2/SyncInfoModal.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/apps/web/src/components/layout/components/v2/SyncInfoModal.tsx b/apps/web/src/components/layout/components/v2/SyncInfoModal.tsx index ed052ec5cc7f..99f620fa9a45 100644 --- a/apps/web/src/components/layout/components/v2/SyncInfoModal.tsx +++ b/apps/web/src/components/layout/components/v2/SyncInfoModal.tsx @@ -4,9 +4,9 @@ import { Prism } from '@mantine/prism'; import { Modal } from '@novu/design-system'; import { Tabs, Text, Title } from '@novu/novui'; import { FC } from 'react'; -import { useApiKeysPage } from '../../../../pages/settings/ApiKeysPage/useApiKeysPage'; import { useBridgeURL } from '../../../../studio/hooks/useBridgeURL'; import { API_ROOT, ENV } from '../../../../config'; +import { useStudioState } from '../../../../studio/StudioStateProvider'; export type SyncInfoModalProps = { isOpen: boolean; @@ -16,7 +16,7 @@ export type SyncInfoModalProps = { const BRIDGE_ENDPOINT_PLACEHOLDER = ''; export const SyncInfoModal: FC = ({ isOpen, toggleOpen }) => { - const { secretKey } = useApiKeysPage(); + const { devSecretKey } = useStudioState(); const bridgeUrl = useBridgeURL(true); const tabs = [ @@ -25,7 +25,7 @@ export const SyncInfoModal: FC = ({ isOpen, toggleOpen }) => label: 'CLI', content: ( - {getOtherCodeContent({ secretKey, bridgeUrl })} + {getOtherCodeContent({ secretKey: devSecretKey || '', bridgeUrl })} ), }, @@ -34,7 +34,7 @@ export const SyncInfoModal: FC = ({ isOpen, toggleOpen }) => label: 'GitHub Actions', content: ( - {getGithubYamlContent({ secretKey, bridgeUrl })} + {getGithubYamlContent({ bridgeUrl })} ), }, @@ -56,8 +56,9 @@ export const SyncInfoModal: FC = ({ isOpen, toggleOpen }) => ); }; -function getGithubYamlContent({ secretKey, bridgeUrl }: { secretKey: string; bridgeUrl: string }) { - return `name: Deploy Workflow State to Novu +function getGithubYamlContent({ bridgeUrl }: { bridgeUrl: string }) { + return `# .github/workflows/novu.yml +name: Novu Sync on: workflow_dispatch: @@ -67,10 +68,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Sync State to Novu - uses: novuhq/actions-novu-sync@v0.0.4 + uses: novuhq/actions-novu-sync@v2 with: secret-key: $\{{ secrets.NOVU_SECRET_KEY }} bridge-url: ${bridgeUrl || BRIDGE_ENDPOINT_PLACEHOLDER}`; From 3e776cdd394b525ce9392b2a48b6fe8ec5c2bb34 Mon Sep 17 00:00:00 2001 From: George Djabarov <39195835+djabarovgeorge@users.noreply.github.com> Date: Mon, 1 Jul 2024 19:13:53 +0300 Subject: [PATCH 10/18] feat(api): change control dto structure (#5900) --- apps/api/src/app/events/e2e/bridge-trigger.e2e-ee.ts | 2 +- .../subscriber-job-bound/subscriber-job-bound.usecase.ts | 4 +--- libs/shared/src/dto/controls/controls.dto.ts | 8 ++++++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/api/src/app/events/e2e/bridge-trigger.e2e-ee.ts b/apps/api/src/app/events/e2e/bridge-trigger.e2e-ee.ts index cbc3c558cb3e..4300881cc35b 100644 --- a/apps/api/src/app/events/e2e/bridge-trigger.e2e-ee.ts +++ b/apps/api/src/app/events/e2e/bridge-trigger.e2e-ee.ts @@ -667,7 +667,7 @@ contexts.forEach((context: Context) => { await saveControlVariables(session, workflowId, stepId, { variables: { name: 'stored_control_name' } }); } - const controls = { controls: { step: [{ stepId: stepId, name: 'stored_control_name' }] } }; + const controls = { controls: { step: { [stepId]: { name: 'stored_control_name' } } } }; await triggerEvent(session, workflowId, subscriber, controls, bridge); await session.awaitRunningJobs(); diff --git a/apps/worker/src/app/workflow/usecases/subscriber-job-bound/subscriber-job-bound.usecase.ts b/apps/worker/src/app/workflow/usecases/subscriber-job-bound/subscriber-job-bound.usecase.ts index d360db6fee18..0804d3b6d0e0 100644 --- a/apps/worker/src/app/workflow/usecases/subscriber-job-bound/subscriber-job-bound.usecase.ts +++ b/apps/worker/src/app/workflow/usecases/subscriber-job-bound/subscriber-job-bound.usecase.ts @@ -162,9 +162,7 @@ export class SubscriberJobBound { ...bridgeWorkflow, type: 'ECHO', steps: bridgeWorkflow.steps.map((step) => { - const stepControlVariables = command.payload.controls?.[ControlVariablesLevelEnum.STEP_CONTROLS].find( - (control) => control.stepId === step.stepId - ); + const stepControlVariables = command.payload.controls?.[ControlVariablesLevelEnum.STEP_CONTROLS]?.[step.stepId]; return { ...step, diff --git a/libs/shared/src/dto/controls/controls.dto.ts b/libs/shared/src/dto/controls/controls.dto.ts index 2d3047661341..8a068feef14d 100644 --- a/libs/shared/src/dto/controls/controls.dto.ts +++ b/libs/shared/src/dto/controls/controls.dto.ts @@ -1,4 +1,8 @@ import { ControlVariablesLevelEnum } from '../../types'; -export type ControlsDto = Record; -type Control = { stepId: string } & Record; +export type ControlsDto = { + [K in ControlVariablesLevelEnum]?: StepControl; +}; +type StepControl = Record; +type stepId = string; +type Data = Record; From 12c2c58334c41546ee862106b9dfbfbb51589f16 Mon Sep 17 00:00:00 2001 From: Richard Fontein <32132657+rifont@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:30:06 +0100 Subject: [PATCH 11/18] fix(web): Make "Edit endpoint" button look more clickable (#5901) * refactor(BridgeUpdateModalTrigger): replace button with Button component * refactor(studio): rename local to isLocalStudio * Update apps/web/src/components/layout/components/v2/BridgeUpdateModalTrigger.tsx Co-authored-by: Gali Ainouz Baum --------- Co-authored-by: Gali Ainouz Baum --- .../layout/components/v2/BridgeMenuItems.tsx | 8 +++++++- .../layout/components/v2/BridgeUpdateModal.tsx | 4 ++-- .../components/v2/BridgeUpdateModalTrigger.tsx | 15 ++++----------- apps/web/src/studio/LocalStudioAuthenticator.tsx | 2 +- apps/web/src/studio/StudioPageLayout.tsx | 2 +- apps/web/src/studio/StudioStateProvider.tsx | 10 +++++----- .../step-editor/WorkflowTestStepButton.tsx | 2 +- .../workflows/test-workflow/WorkflowsTestPage.tsx | 2 +- apps/web/src/studio/hooks/useBridgeAPI.ts | 2 +- apps/web/src/studio/hooks/useBridgeURL.ts | 2 +- apps/web/src/studio/types.ts | 4 ++-- 11 files changed, 26 insertions(+), 27 deletions(-) diff --git a/apps/web/src/components/layout/components/v2/BridgeMenuItems.tsx b/apps/web/src/components/layout/components/v2/BridgeMenuItems.tsx index 8c5bae765d51..eb06bdce9ed8 100644 --- a/apps/web/src/components/layout/components/v2/BridgeMenuItems.tsx +++ b/apps/web/src/components/layout/components/v2/BridgeMenuItems.tsx @@ -1,11 +1,17 @@ +import { useStudioState } from '../../../../studio/StudioStateProvider'; +import { When } from '../../../utils/When'; import { BridgeUpdateModalTrigger } from './BridgeUpdateModalTrigger'; import { SyncInfoModalTrigger } from './SyncInfoModalTrigger'; export function BridgeMenuItems() { + const { isLocalStudio } = useStudioState(); + return ( <> - + + + ); } diff --git a/apps/web/src/components/layout/components/v2/BridgeUpdateModal.tsx b/apps/web/src/components/layout/components/v2/BridgeUpdateModal.tsx index e43f11c247b9..25cadfa9ca4d 100644 --- a/apps/web/src/components/layout/components/v2/BridgeUpdateModal.tsx +++ b/apps/web/src/components/layout/components/v2/BridgeUpdateModal.tsx @@ -20,7 +20,7 @@ export type BridgeUpdateModalProps = { export const BridgeUpdateModal: FC = ({ isOpen, toggleOpen }) => { const segment = useSegment(); - const { local, bridgeURL, setBridgeURL } = useStudioState(); + const { isLocalStudio, bridgeURL, setBridgeURL } = useStudioState(); const [urlError, setUrlError] = useState(''); const [url, setUrl] = useState(bridgeURL); const [isUpdating, setIsUpdating] = useState(false); @@ -60,7 +60,7 @@ export const BridgeUpdateModal: FC = ({ isOpen, toggleOp const storeInProperLocation = async (newUrl: string) => { setBridgeURL(newUrl); - if (!local) { + if (!isLocalStudio) { await updateBridgeUrl({ url: newUrl }, environment?._id ?? ''); } }; diff --git a/apps/web/src/components/layout/components/v2/BridgeUpdateModalTrigger.tsx b/apps/web/src/components/layout/components/v2/BridgeUpdateModalTrigger.tsx index 516a49c984bc..be2377a91d2b 100644 --- a/apps/web/src/components/layout/components/v2/BridgeUpdateModalTrigger.tsx +++ b/apps/web/src/components/layout/components/v2/BridgeUpdateModalTrigger.tsx @@ -1,6 +1,6 @@ import { FC, useState } from 'react'; import { Tooltip } from '@novu/design-system'; -import { Text } from '@novu/novui'; +import { Text, Button } from '@novu/novui'; import { css } from '@novu/novui/css'; import { IconEdit, IconLink, IconLinkOff } from '@novu/novui/icons'; import { HStack } from '@novu/novui/jsx'; @@ -32,16 +32,9 @@ function BridgeUpdateModalTriggerControl({ onClick }: { onClick: () => void }) { const { status, bridgeURL } = useBridgeConnectionStatus(); const trigger = isHovered ? ( - + ) : ( ); diff --git a/apps/web/src/studio/LocalStudioAuthenticator.tsx b/apps/web/src/studio/LocalStudioAuthenticator.tsx index 8e0069f060c0..c26bd85623f8 100644 --- a/apps/web/src/studio/LocalStudioAuthenticator.tsx +++ b/apps/web/src/studio/LocalStudioAuthenticator.tsx @@ -111,7 +111,7 @@ export function LocalStudioAuthenticator() { const tunnelBridgeURL = buildBridgeURL(tunnelOrigin, tunnelPath); const state: StudioState = { - local: true, + isLocalStudio: true, devSecretKey: apiKey, testUser: { id: currentUser._id, diff --git a/apps/web/src/studio/StudioPageLayout.tsx b/apps/web/src/studio/StudioPageLayout.tsx index d0c3060404a1..3a141ec3e3c0 100644 --- a/apps/web/src/studio/StudioPageLayout.tsx +++ b/apps/web/src/studio/StudioPageLayout.tsx @@ -12,7 +12,7 @@ export function StudioPageLayout() { return ; } - if (state?.local) { + if (state?.isLocalStudio) { return ; } diff --git a/apps/web/src/studio/StudioStateProvider.tsx b/apps/web/src/studio/StudioStateProvider.tsx index a79954860401..63f830c898ee 100644 --- a/apps/web/src/studio/StudioStateProvider.tsx +++ b/apps/web/src/studio/StudioStateProvider.tsx @@ -11,7 +11,7 @@ type BridgeURLGetterSetter = { bridgeURL: string; setBridgeURL: (url: string) => const StudioStateContext = React.createContext<(StudioState & BridgeURLGetterSetter) | undefined>(undefined); function computeBridgeURL(state: StudioState) { - return state.local ? state.localBridgeURL || state.tunnelBridgeURL : state.storedBridgeURL; + return state.isLocalStudio ? state.localBridgeURL || state.tunnelBridgeURL : state.storedBridgeURL; } function convertToTestUser(currentUser?: IUserEntity) { @@ -36,7 +36,7 @@ export const StudioStateProvider = ({ children }: { children: React.ReactNode }) } return { - local: false, + isLocalStudio: false, storedBridgeURL: environment?.echo?.url || '', testUser: convertToTestUser(currentUser), organizationName: currentOrganization?.name || '', @@ -46,15 +46,15 @@ export const StudioStateProvider = ({ children }: { children: React.ReactNode }) const [bridgeURL, setBridgeURL] = useState(computeBridgeURL(state)); useEffect(() => { - if (!state.local) { + if (!state.isLocalStudio) { setState({ - local: false, + isLocalStudio: false, storedBridgeURL: environment?.echo?.url || '', testUser: convertToTestUser(currentUser), organizationName: currentOrganization?.name || '', }); } - }, [environment, state?.local, currentUser, currentOrganization]); + }, [environment, state?.isLocalStudio, currentUser, currentOrganization]); useEffect(() => { setBridgeURL(computeBridgeURL(state)); diff --git a/apps/web/src/studio/components/workflows/step-editor/WorkflowTestStepButton.tsx b/apps/web/src/studio/components/workflows/step-editor/WorkflowTestStepButton.tsx index 87cfba2d7637..1a363b3ffd38 100644 --- a/apps/web/src/studio/components/workflows/step-editor/WorkflowTestStepButton.tsx +++ b/apps/web/src/studio/components/workflows/step-editor/WorkflowTestStepButton.tsx @@ -21,7 +21,7 @@ export const WorkflowTestStepButton = ({ stepType: ChannelTypeEnum; }) => { const segment = useSegment(); - const { local, testUser } = useStudioState(); + const { isLocalStudio: local, testUser } = useStudioState(); const { mutateAsync: testSendEmailEvent, isLoading: isTestingEmail } = useMutation(testSendEmailMessage); const handleTestClick = async () => { diff --git a/apps/web/src/studio/components/workflows/test-workflow/WorkflowsTestPage.tsx b/apps/web/src/studio/components/workflows/test-workflow/WorkflowsTestPage.tsx index a7cfb90b1a5b..ee722b7d23f1 100644 --- a/apps/web/src/studio/components/workflows/test-workflow/WorkflowsTestPage.tsx +++ b/apps/web/src/studio/components/workflows/test-workflow/WorkflowsTestPage.tsx @@ -22,7 +22,7 @@ import { useApiKeys } from '../../../../hooks/useNovuAPI'; export const WorkflowsTestPage = () => { const segment = useSegment(); - const { local, testUser } = useStudioState() || {}; + const { isLocalStudio: local, testUser } = useStudioState() || {}; const { templateId = '' } = useParams<{ templateId: string }>(); const [payload, setPayload] = useState>({}); const [to, setTo] = useState({ diff --git a/apps/web/src/studio/hooks/useBridgeAPI.ts b/apps/web/src/studio/hooks/useBridgeAPI.ts index 410745705a7f..6f002c2e84e7 100644 --- a/apps/web/src/studio/hooks/useBridgeAPI.ts +++ b/apps/web/src/studio/hooks/useBridgeAPI.ts @@ -89,7 +89,7 @@ export const useWorkflowTrigger = () => { const { mutateAsync, ...rest } = useMutation(api.trigger); - const bridgeUrl = state.local ? state.tunnelBridgeURL : state.storedBridgeURL; + const bridgeUrl = state.isLocalStudio ? state.tunnelBridgeURL : state.storedBridgeURL; async function trigger(params: TriggerParams): Promise<{ data: { transactionId: string } }> { return mutateAsync({ ...params, bridgeUrl }); diff --git a/apps/web/src/studio/hooks/useBridgeURL.ts b/apps/web/src/studio/hooks/useBridgeURL.ts index 8fa5a770544b..0d1364d9edbc 100644 --- a/apps/web/src/studio/hooks/useBridgeURL.ts +++ b/apps/web/src/studio/hooks/useBridgeURL.ts @@ -5,7 +5,7 @@ export function useBridgeURL(tunnel = false) { let bridgeURL; - if (studioState.local) { + if (studioState.isLocalStudio) { /* * Local studio mode. * Prefer local host for bridge discovery as it's faster diff --git a/apps/web/src/studio/types.ts b/apps/web/src/studio/types.ts index 0afb9b8c9204..a39e6c10d67e 100644 --- a/apps/web/src/studio/types.ts +++ b/apps/web/src/studio/types.ts @@ -32,12 +32,12 @@ type BaseStudioState = { }; type CloudStudioState = BaseStudioState & { - local: false; + isLocalStudio: false; storedBridgeURL: string; }; type LocalStudioState = BaseStudioState & { - local: true; + isLocalStudio: true; localBridgeURL: string; tunnelBridgeURL: string; }; From f4fa6793554176c09318390e658e189ff17cbec5 Mon Sep 17 00:00:00 2001 From: Richard Fontein <32132657+rifont@users.noreply.github.com> Date: Mon, 1 Jul 2024 19:30:25 +0100 Subject: [PATCH 12/18] fix(web): Remove faulty cross-iframe link for Discord invite (#5899) * style(LocalStudioHeader): Remove commented-out code * refactor(LocalStudioHeader): remove unused import --- .../components/LocalStudioHeader/LocalStudioHeader.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/apps/web/src/components/layout/components/LocalStudioHeader/LocalStudioHeader.tsx b/apps/web/src/components/layout/components/LocalStudioHeader/LocalStudioHeader.tsx index 8a8e3ea95621..576c4569477c 100644 --- a/apps/web/src/components/layout/components/LocalStudioHeader/LocalStudioHeader.tsx +++ b/apps/web/src/components/layout/components/LocalStudioHeader/LocalStudioHeader.tsx @@ -1,10 +1,9 @@ import { Header } from '@mantine/core'; import { IconButton } from '@novu/novui'; import { css } from '@novu/novui/css'; -import { IconHelpOutline, IconOutlineMenuBook } from '@novu/novui/icons'; +import { IconOutlineMenuBook } from '@novu/novui/icons'; import { HStack } from '@novu/novui/jsx'; import { FC } from 'react'; -import { discordInviteUrl } from '../../../../pages/quick-start/consts'; import { useStudioWorkflowsNavigation } from '../../../../studio/hooks'; import { HEADER_NAV_HEIGHT } from '../../constants'; import { BridgeMenuItems } from '../v2/BridgeMenuItems'; @@ -35,10 +34,6 @@ export const LocalStudioHeader: FC = () => { target="_blank" rel="noopener noreferrer" /> - - {/* This doesn't work because of Discord's popup blocker via the response header: - Cross-Origin-Opener-Policy: same-origin-allow-popups. We will likely need a Javascript workaround for Discord's popup blocker. */} - From ae75d4452ba2d835beff5ccc5c7739124bce9f00 Mon Sep 17 00:00:00 2001 From: Denis Kralj <168424106+denis-kralj-novu@users.noreply.github.com> Date: Mon, 1 Jul 2024 20:52:52 +0200 Subject: [PATCH 13/18] chore(create-novu-app): align code with latest state (#5896) * chore(create-novu-app): align code with latest state Update to fit with example repository code Move .env to .env.local Add github action * chore(create-novu-app): align code with latest state Add new words to cspell * chore(create-novu-app): align code with latest state PR comment adjustments * chore(create-novu-app): align code with latest state Make button clickable --- .cspell.json | 3 + packages/create-novu-app/create-app.ts | 1 - .../app-react-email/ts/app/api/novu/route.ts | 7 +- .../app/novu/emails/novu-onboarding-email.tsx | 181 ++++++++++++++++++ .../ts/app/novu/emails/vercel.tsx | 172 ----------------- .../app-react-email/ts/app/novu/workflows.ts | 57 ------ .../ts/app/novu/workflows/index.ts | 1 + .../welcome-onboarding-email/index.ts | 3 + .../welcome-onboarding-email/schemas.ts | 103 ++++++++++ .../welcome-onboarding-email/types.ts | 12 ++ .../welcome-onboarding-email/workflow.ts | 24 +++ .../templates/github/workflows/novu.yml | 17 ++ packages/create-novu-app/templates/index.ts | 17 +- 13 files changed, 363 insertions(+), 235 deletions(-) create mode 100644 packages/create-novu-app/templates/app-react-email/ts/app/novu/emails/novu-onboarding-email.tsx delete mode 100644 packages/create-novu-app/templates/app-react-email/ts/app/novu/emails/vercel.tsx delete mode 100644 packages/create-novu-app/templates/app-react-email/ts/app/novu/workflows.ts create mode 100644 packages/create-novu-app/templates/app-react-email/ts/app/novu/workflows/index.ts create mode 100644 packages/create-novu-app/templates/app-react-email/ts/app/novu/workflows/welcome-onboarding-email/index.ts create mode 100644 packages/create-novu-app/templates/app-react-email/ts/app/novu/workflows/welcome-onboarding-email/schemas.ts create mode 100644 packages/create-novu-app/templates/app-react-email/ts/app/novu/workflows/welcome-onboarding-email/types.ts create mode 100644 packages/create-novu-app/templates/app-react-email/ts/app/novu/workflows/welcome-onboarding-email/workflow.ts create mode 100644 packages/create-novu-app/templates/github/workflows/novu.yml diff --git a/.cspell.json b/.cspell.json index 2fbcd7789d13..5a48c6b144d7 100644 --- a/.cspell.json +++ b/.cspell.json @@ -140,6 +140,7 @@ "domainname", "domainsuffix", "donefunc", + "Dotan", "dotenv", "doublecolon", "dtos", @@ -151,6 +152,7 @@ "elif", "emailjs", "Embeddable", + "Emek", "EMSA", "endgroup", "enroute", @@ -308,6 +310,7 @@ "mkdocs", "mlen", "moby", + "Modiin", "modlen", "mongod", "mongosh", diff --git a/packages/create-novu-app/create-app.ts b/packages/create-novu-app/create-app.ts index b53271e32fa9..1662f0a03a04 100644 --- a/packages/create-novu-app/create-app.ts +++ b/packages/create-novu-app/create-app.ts @@ -61,7 +61,6 @@ export async function createApp({ process.chdir(root); - const packageJsonPath = path.join(root, 'package.json'); let hasPackageJson = false; /** diff --git a/packages/create-novu-app/templates/app-react-email/ts/app/api/novu/route.ts b/packages/create-novu-app/templates/app-react-email/ts/app/api/novu/route.ts index 1d917a15e6ea..916b0029f895 100644 --- a/packages/create-novu-app/templates/app-react-email/ts/app/api/novu/route.ts +++ b/packages/create-novu-app/templates/app-react-email/ts/app/api/novu/route.ts @@ -1,4 +1,7 @@ import { serve } from "@novu/framework/next"; -import { myWorkflow } from "../../novu/workflows"; +import { welcomeOnboardingEmail } from "../../novu/workflows"; -export const { GET, POST, OPTIONS } = serve({ workflows: [myWorkflow] }); +// the workflows collection can hold as many workflow definitions as you need +export const { GET, POST, OPTIONS } = serve({ + workflows: [welcomeOnboardingEmail], +}); diff --git a/packages/create-novu-app/templates/app-react-email/ts/app/novu/emails/novu-onboarding-email.tsx b/packages/create-novu-app/templates/app-react-email/ts/app/novu/emails/novu-onboarding-email.tsx new file mode 100644 index 000000000000..db8e1d9dde06 --- /dev/null +++ b/packages/create-novu-app/templates/app-react-email/ts/app/novu/emails/novu-onboarding-email.tsx @@ -0,0 +1,181 @@ +import { + Body, + Button, + Column, + Container, + Head, + Html, + Img, + Preview, + Row, + Section, + Text, + Hr, + Tailwind, + render, +} from "@react-email/components"; +import * as React from "react"; +import { ControlSchema, PayloadSchema } from "../workflows/"; + +type NovuWelcomeEmailProps = ControlSchema & PayloadSchema; + +export const NovuWelcomeEmail = ({ + components, + userImage, + teamImage, + arrowImage, +}: NovuWelcomeEmailProps) => { + return ( + + + Novu Welcome + + + Netlify + + {components?.map((component, componentIndex) => { + return ( +
+ {component.componentType === "heading" ? ( + +

+ {component.componentText} +

+
+ ) : null} + + {component.componentType === "list" ? ( + +
    + {component.componentListItems?.map( + (listItem, listItemIndex) => ( +
  • + {listItem.title} {listItem.body} +
  • + ), + )} +
+
+ ) : null} + + {component.componentType === "button" ? ( + + + + ) : null} + + {component.componentType === "image" ? ( + + first image + + ) : null} + + {component.componentType === "text" ? ( +
+ + {component.componentText} + +
+ ) : null} + {component.componentType === "divider" ? ( + + {" "} +
+
+ ) : null} + {component.componentType === "users" ? ( + +
+ + + + + + invited you to + + + + + +
+
+ ) : null} +
+ ); + })} +
+ + + + Novu, Emek Dotan 109, Apt 2, Modiin, Israel + + + +
+ + ); +}; + +export default NovuWelcomeEmail; + +const heading = { + fontSize: "30px", + lineHeight: "1.1", + fontWeight: "700", + color: "#000000", +}; + +export function renderEmail(controls: ControlSchema, payload: PayloadSchema) { + return render(); +} diff --git a/packages/create-novu-app/templates/app-react-email/ts/app/novu/emails/vercel.tsx b/packages/create-novu-app/templates/app-react-email/ts/app/novu/emails/vercel.tsx deleted file mode 100644 index be193aa48115..000000000000 --- a/packages/create-novu-app/templates/app-react-email/ts/app/novu/emails/vercel.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import { - Body, - Button, - Container, - Column, - Head, - Heading, - Hr, - Html, - Img, - Link, - Preview, - Row, - Section, - Text, - render, -} from "@react-email/components"; -import { Tailwind } from "@react-email/tailwind"; -import * as React from "react"; - -const baseUrl = process.env.VERCEL_URL - ? `https://react-email-demo-bdj5iju9r-resend.vercel.app` - : "https://react-email-demo-bdj5iju9r-resend.vercel.app"; - -export const VercelInviteUserEmail = ({ - username, - showButton, - userImage, - invitedByUsername, - invitedByEmail, - teamName, - teamImage, - inviteLink, - inviteFromIp, - inviteFromLocation, - listItems, -}: any) => { - const previewText = `Join ${invitedByUsername} on Vercel`; - - return ( - - - {previewText} - - - -
- Vercel -
- - Joined {teamName} on Vercel - - - Hello {username}, - - - {invitedByUsername} ( - - {invitedByEmail} - - ) has invited you to the {teamName} team on{" "} - Vercel. - -
- - - - - - invited you to - - - - - -
- - {listItems?.map((item: string) => { - return ( - - {item === "component1" ? ( - - - - ) : null} - {item === "component2" ? ( - - - {item} - - - ) : null} - {item === "component3" ? ( - - {" "} -
-
- ) : null} - {item === "component4" ? IMAGE : null} -
- ); - })} - {showButton && ( -
- -
- )} - -
- - or copy and paste this URL into your browser:{" "} - - {inviteLink} - - -
- - This invitation was intended for{" "} - {username}. This invite was - sent from {inviteFromIp}{" "} - located in{" "} - {inviteFromLocation}. If you - were not expecting this invitation, you can ignore this email. If - you are concerned about your account's safety, please reply - to this email to get in touch with us. - -
- -
- - ); -}; - -export default VercelInviteUserEmail; - -export function renderReactEmail(controls: any) { - return render(); -} diff --git a/packages/create-novu-app/templates/app-react-email/ts/app/novu/workflows.ts b/packages/create-novu-app/templates/app-react-email/ts/app/novu/workflows.ts deleted file mode 100644 index 91ebc50b2a2d..000000000000 --- a/packages/create-novu-app/templates/app-react-email/ts/app/novu/workflows.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { workflow } from "@novu/framework"; -import { renderReactEmail } from "./emails/vercel"; - -export const myWorkflow = workflow( - "hello-world", - async ({ step }) => { - await step.email( - "send-email", - async (controls) => { - return { - subject: "This is an email subject", - body: renderReactEmail(controls), - }; - }, - { - controlSchema: { - type: "object", - - properties: { - showButton: { type: "boolean", default: true }, - username: { type: "string", default: "alanturing" }, - userImage: { - type: "string", - default: - "https://react-email-demo-bdj5iju9r-resend.vercel.app/static/vercel-user.png", - format: "uri", - }, - invitedByUsername: { type: "string", default: "Alan" }, - invitedByEmail: { - type: "string", - default: "alan.turing@example.com", - format: "email", - }, - teamName: { type: "string", default: "Team Awesome" }, - teamImage: { - type: "string", - default: - "https://react-email-demo-bdj5iju9r-resend.vercel.app/static/vercel-team.png", - format: "uri", - }, - inviteLink: { - type: "string", - default: "https://vercel.com/teams/invite/foo", - format: "uri", - }, - inviteFromIp: { type: "string", default: "204.13.186.218" }, - inviteFromLocation: { - type: "string", - default: "São Paulo, Brazil", - }, - }, - } as const, - }, - ); - }, - { payloadSchema: { type: "object", properties: {} } }, -); diff --git a/packages/create-novu-app/templates/app-react-email/ts/app/novu/workflows/index.ts b/packages/create-novu-app/templates/app-react-email/ts/app/novu/workflows/index.ts new file mode 100644 index 000000000000..93135cea4505 --- /dev/null +++ b/packages/create-novu-app/templates/app-react-email/ts/app/novu/workflows/index.ts @@ -0,0 +1 @@ +export * from "./welcome-onboarding-email"; diff --git a/packages/create-novu-app/templates/app-react-email/ts/app/novu/workflows/welcome-onboarding-email/index.ts b/packages/create-novu-app/templates/app-react-email/ts/app/novu/workflows/welcome-onboarding-email/index.ts new file mode 100644 index 000000000000..bdb49360032d --- /dev/null +++ b/packages/create-novu-app/templates/app-react-email/ts/app/novu/workflows/welcome-onboarding-email/index.ts @@ -0,0 +1,3 @@ +export * from "./schemas"; +export * from "./types"; +export * from "./workflow"; diff --git a/packages/create-novu-app/templates/app-react-email/ts/app/novu/workflows/welcome-onboarding-email/schemas.ts b/packages/create-novu-app/templates/app-react-email/ts/app/novu/workflows/welcome-onboarding-email/schemas.ts new file mode 100644 index 000000000000..a427f944f104 --- /dev/null +++ b/packages/create-novu-app/templates/app-react-email/ts/app/novu/workflows/welcome-onboarding-email/schemas.ts @@ -0,0 +1,103 @@ +import { z } from "zod"; + +// Learn more about zod at the official website: https://zod.dev/ +export const emailListElementTypeSchema = z.object({ + title: z.string().default(""), + body: z.string().default(""), +}); + +export const componentTypeSchema = z.discriminatedUnion("componentType", [ + z.object({ + componentType: z.literal("text"), + componentText: z.string().default(""), + align: z.enum(["left", "center", "right"]).default("center"), + }), + z.object({ + componentType: z.literal("list"), + componentText: z.string().default(""), + componentListItems: z.array(emailListElementTypeSchema), + }), + z.object({ + componentType: z.literal("image"), + componentText: z.string().default(""), + src: z.string().default(""), + }), + z.object({ + componentType: z.literal("heading"), + componentText: z.string().default(""), + }), + z.object({ + componentType: z.literal("button"), + componentText: z.string().default(""), + href: z.string().url().default(""), + }), + z.object({ componentType: z.literal("divider") }), + z.object({ componentType: z.literal("users") }), +]); + +export const zodControlSchema = z.object({ + components: z.array(componentTypeSchema).default([ + { + componentType: "heading", + componentText: "Welcome to Novu", + }, + { + componentType: "text", + componentText: + "Congratulations on receiving your first notification email from Novu! Join the hundreds of thousands of developers worldwide who use Novu to build notification platforms for their products.", + align: "left", + }, + { + componentType: "users", + }, + { + componentType: "list", + componentListItems: [ + { + title: "Send Multi-channel notifications", + body: "You can send notifications to your users via multiple channels (Email, SMS, Push, and In-App) in a heartbeat.", + }, + { + title: "Delay notifying your users", + body: "You can use the delay action whenever you need to pause the execution of your workflow for a period of time.", + }, + { + title: "Digest multiple notifications into one", + body: "You can streamline notifications by accumulating multiple trigger events into one coherent message before delivery.", + }, + ], + }, + { + componentType: "text", + componentText: + "Ready to get started? Click on the button below, and you will see first-hand how easily you can edit this email content.", + align: "left", + }, + { + componentType: "button", + componentText: "Edit Email", + href: "http://localhost:2022/studio", + }, + ]), +}); + +export const zodPayloadSchema = z.object({ + teamImage: z + .string() + .url() + .default( + "https://images.spr.so/cdn-cgi/imagedelivery/j42No7y-dcokJuNgXeA0ig/dca73b36-cf39-4e28-9bc7-8a0d0cd8ac70/standalone-gradient2x_2/w=128,quality=90,fit=scale-down", + ), + userImage: z + .string() + .url() + .default( + "https://react-email-demo-48zvx380u-resend.vercel.app/static/vercel-user.png", + ), + arrowImage: z + .string() + .url() + .default( + "https://react-email-demo-bdj5iju9r-resend.vercel.app/static/vercel-arrow.png", + ), +}); diff --git a/packages/create-novu-app/templates/app-react-email/ts/app/novu/workflows/welcome-onboarding-email/types.ts b/packages/create-novu-app/templates/app-react-email/ts/app/novu/workflows/welcome-onboarding-email/types.ts new file mode 100644 index 000000000000..3ab750373f5e --- /dev/null +++ b/packages/create-novu-app/templates/app-react-email/ts/app/novu/workflows/welcome-onboarding-email/types.ts @@ -0,0 +1,12 @@ +import { z } from "zod"; +import { + zodControlSchema, + zodPayloadSchema, + componentTypeSchema, + emailListElementTypeSchema, +} from "./schemas"; + +export type ControlSchema = z.infer; +export type PayloadSchema = z.infer; +export type EmailComponent = z.infer; +export type ListElementComponent = z.infer; diff --git a/packages/create-novu-app/templates/app-react-email/ts/app/novu/workflows/welcome-onboarding-email/workflow.ts b/packages/create-novu-app/templates/app-react-email/ts/app/novu/workflows/welcome-onboarding-email/workflow.ts new file mode 100644 index 000000000000..b3095ba8b28a --- /dev/null +++ b/packages/create-novu-app/templates/app-react-email/ts/app/novu/workflows/welcome-onboarding-email/workflow.ts @@ -0,0 +1,24 @@ +import { workflow } from "@novu/framework"; +import { renderEmail } from "../../emails/novu-onboarding-email"; +import { zodControlSchema, zodPayloadSchema } from "./schemas"; + +export const welcomeOnboardingEmail = workflow( + "welcome-onboarding-email", + async ({ step, payload }) => { + await step.email( + "send-email", + async (controls) => { + return { + subject: "A Successful Test on Novu!", + body: renderEmail(controls, payload), + }; + }, + { + controlSchema: zodControlSchema, + }, + ); + }, + { + payloadSchema: zodPayloadSchema, + }, +); diff --git a/packages/create-novu-app/templates/github/workflows/novu.yml b/packages/create-novu-app/templates/github/workflows/novu.yml new file mode 100644 index 000000000000..920b9335c78c --- /dev/null +++ b/packages/create-novu-app/templates/github/workflows/novu.yml @@ -0,0 +1,17 @@ +name: Novu Sync + +on: + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Sync State to Novu + uses: novuhq/actions-novu-sync@v2 + with: + secret-key: ${{ secrets.NOVU_SECRET_KEY }} + bridge-url: ${{ secrets.NOVU_BRIDGE_URL }} diff --git a/packages/create-novu-app/templates/index.ts b/packages/create-novu-app/templates/index.ts index 5eff0d33d7e3..bbbed54474e2 100644 --- a/packages/create-novu-app/templates/index.ts +++ b/packages/create-novu-app/templates/index.ts @@ -2,7 +2,6 @@ import { install } from "../helpers/install"; import { copy } from "../helpers/copy"; import { async as glob } from "fast-glob"; -import { createHash } from "crypto"; import os from "os"; import fs from "fs/promises"; import path from "path"; @@ -176,7 +175,13 @@ export const installTemplate = async ({ return `${acc}${key}=${value}${os.EOL}`; }, ""); - await fs.writeFile(path.join(root, ".env"), val); + await fs.writeFile(path.join(root, ".env.local"), val); + + /* write github action */ + await copy(copySource, `${root}/.github`, { + parents: true, + cwd: path.join(__dirname, `./github`), + }); /** Copy the version from package.json or override for tests. */ const version = "14.2.3"; @@ -211,7 +216,6 @@ export const installTemplate = async ({ packageJson.devDependencies = { ...packageJson.devDependencies, typescript: "^5", - tsx: "^4.15.1", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", @@ -232,6 +236,13 @@ export const installTemplate = async ({ "@react-email/tailwind": "0.0.16", "react-email": "2.1.2", }; + + /* Zod dependencies used in react email example */ + packageJson.dependencies = { + ...packageJson.dependencies, + zod: "^3.23.8", + "zod-to-json-schema": "^3.23.1", + }; } /* Default ESLint dependencies. */ From e2b9a58758dc4b79e2212b6e986eaacbc2868b8b Mon Sep 17 00:00:00 2001 From: Dima Grossman Date: Mon, 1 Jul 2024 22:21:08 +0300 Subject: [PATCH 14/18] fix: sync --- .source | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.source b/.source index 450a664d71c2..270a112fc01e 160000 --- a/.source +++ b/.source @@ -1 +1 @@ -Subproject commit 450a664d71c2548cb08a593d9e5e3727d7e83d87 +Subproject commit 270a112fc01ebb23e9dc7cdfe79899c692f8386e From 30492259d87b86d587c77d46cf7c56399900be84 Mon Sep 17 00:00:00 2001 From: Dima Grossman Date: Mon, 1 Jul 2024 22:37:52 +0300 Subject: [PATCH 15/18] fix(root): no more eslint on pre-commit (#5905) * fix: no more eslint * fix: stop only for e2e tests --- apps/api/package.json | 5 ----- apps/web/package.json | 5 ----- apps/widget/package.json | 5 ----- apps/worker/package.json | 5 ----- enterprise/packages/libs/dal/package.json | 5 ----- libs/dal/package.json | 5 ----- libs/embed/package.json | 3 --- libs/testing/package.json | 5 ----- package.json | 15 ++++++--------- 9 files changed, 6 insertions(+), 47 deletions(-) diff --git a/apps/api/package.json b/apps/api/package.json index 54cbc40766a4..2c10d08383f2 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -125,10 +125,5 @@ "@novu/ee-echo-api": "workspace:*", "@novu/ee-shared-services": "workspace:*", "@novu/ee-translation": "workspace:*" - }, - "lint-staged": { - "*.{js,jsx,ts,tsx}": [ - "eslint" - ] } } diff --git a/apps/web/package.json b/apps/web/package.json index a7705a7f701b..62a9c2fccc0f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -184,11 +184,6 @@ "last 1 safari version" ] }, - "lint-staged": { - "*.{js,jsx,ts,tsx}": [ - "eslint" - ] - }, "eslintConfig": { "overrides": [ { diff --git a/apps/widget/package.json b/apps/widget/package.json index ad333bfd91ad..e46e0070156f 100644 --- a/apps/widget/package.json +++ b/apps/widget/package.json @@ -121,10 +121,5 @@ "**/@babel", "**/@babel/**" ] - }, - "lint-staged": { - "*.{js,jsx,ts,tsx}": [ - "eslint" - ] } } diff --git a/apps/worker/package.json b/apps/worker/package.json index a831e2de751e..c42172ea0e37 100644 --- a/apps/worker/package.json +++ b/apps/worker/package.json @@ -91,10 +91,5 @@ "@novu/ee-echo-worker": "workspace:*", "@novu/ee-shared-services": "workspace:*", "@novu/ee-translation": "workspace:*" - }, - "lint-staged": { - "*.{js,jsx,ts,tsx}": [ - "eslint" - ] } } diff --git a/enterprise/packages/libs/dal/package.json b/enterprise/packages/libs/dal/package.json index bc0e678f572b..9c39ab524e97 100644 --- a/enterprise/packages/libs/dal/package.json +++ b/enterprise/packages/libs/dal/package.json @@ -34,10 +34,5 @@ }, "peerDependencies": { "@nestjs/common": "10.2.2" - }, - "lint-staged": { - "*.{js,jsx,ts,tsx}": [ - "eslint" - ] } } diff --git a/libs/dal/package.json b/libs/dal/package.json index daeba20491da..58647a45ec98 100644 --- a/libs/dal/package.json +++ b/libs/dal/package.json @@ -50,10 +50,5 @@ "typescript": "4.9.5", "rimraf": "^3.0.2", "supertest": "^5.0.0" - }, - "lint-staged": { - "*.{js,jsx,ts,tsx}": [ - "eslint" - ] } } diff --git a/libs/embed/package.json b/libs/embed/package.json index d4b359b87cb1..f1ab4e96d4c4 100644 --- a/libs/embed/package.json +++ b/libs/embed/package.json @@ -39,9 +39,6 @@ "precommit": "lint-staged" }, "lint-staged": { - "*.{js,jsx,ts,tsx}": [ - "eslint" - ], "{*.json,.{babelrc,eslintrc,prettierrc,stylelintrc}}": [ "prettier --ignore-path .eslintignore --parser json --write" ], diff --git a/libs/testing/package.json b/libs/testing/package.json index 1375205a9aa1..0ed3018a80e0 100644 --- a/libs/testing/package.json +++ b/libs/testing/package.json @@ -53,10 +53,5 @@ "ts-node": "~10.9.1", "tsconfig-paths": "~4.1.0", "typescript": "4.9.5" - }, - "lint-staged": { - "*.{js,jsx,ts,tsx}": [ - "eslint" - ] } } diff --git a/package.json b/package.json index 149921758432..9fadbbed4a1c 100644 --- a/package.json +++ b/package.json @@ -183,20 +183,17 @@ } }, "lint-staged": { - "apps/**/*.{ts,tsx,json}": [ - "prettier --ignore-path ./.prettierignore --write", - "eslint", + "*.{e2e,e2e-ee,spec}.{js,ts}": [ "stop-only --file" ], + "apps/**/*.{ts,tsx,json}": [ + "prettier --ignore-path ./.prettierignore --write" + ], "packages/**/*.{ts,tsx,json}": [ - "prettier --ignore-path ./.prettierignore --write", - "eslint", - "stop-only --file" + "prettier --ignore-path ./.prettierignore --write" ], "libs/**/*.{ts,js,json}": [ - "prettier --ignore-path ./.prettierignore --write", - "eslint", - "stop-only --file" + "prettier --ignore-path ./.prettierignore --write" ] }, "engines": { From 95eb32c36fe086a4d1207a586fcf140c9322ba9e Mon Sep 17 00:00:00 2001 From: Denis Kralj <168424106+denis-kralj-novu@users.noreply.github.com> Date: Mon, 1 Jul 2024 21:48:50 +0200 Subject: [PATCH 16/18] fix(web): store localhost url (#5906) NV-3969 --- .../components/v2/BridgeUpdateModal.tsx | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/layout/components/v2/BridgeUpdateModal.tsx b/apps/web/src/components/layout/components/v2/BridgeUpdateModal.tsx index 25cadfa9ca4d..3d0707ee81ae 100644 --- a/apps/web/src/components/layout/components/v2/BridgeUpdateModal.tsx +++ b/apps/web/src/components/layout/components/v2/BridgeUpdateModal.tsx @@ -12,6 +12,7 @@ import { hstack } from '@novu/novui/patterns'; import { useSegment } from '../../../providers/SegmentProvider'; import { validateURL } from '../../../../utils/url'; import { useStudioState } from '../../../../studio/StudioStateProvider'; +import { buildBridgeHTTPClient } from '../../../../bridgeApi/bridgeApi.client'; export type BridgeUpdateModalProps = { isOpen: boolean; @@ -33,6 +34,23 @@ export const BridgeUpdateModal: FC = ({ isOpen, toggleOp setUrl(event.target.value); }; + const validateFromLocal = async (bridgeUrl: string): Promise<{ isValid: boolean }> => { + try { + const client = buildBridgeHTTPClient(bridgeUrl); + const response = await client.healthCheck(); + + const result = { isValid: response.status === 'ok' }; + + return result; + } catch {} + + return { isValid: false }; + }; + const localDomains = ['localhost', '127.0.0.1']; + const isLocalAddress = () => { + return localDomains.includes(location.hostname); + }; + const onUpdateClick = async () => { setUrlError(''); setIsUpdating(true); @@ -40,7 +58,14 @@ export const BridgeUpdateModal: FC = ({ isOpen, toggleOp if (url) { validateURL(url); - const result = await validateBridgeUrl({ bridgeUrl: url }); + let result = + isLocalStudio && isLocalAddress() + ? await validateFromLocal(url) + : await validateBridgeUrl({ bridgeUrl: url }); + + if (!result.isValid && isLocalStudio) { + result = await validateBridgeUrl({ bridgeUrl: url }); + } if (!result.isValid) { throw new Error('The provided URL is not the Novu Endpoint URL'); } From d3cd7601547bb7e2f87466ed28ee5dc4142bf020 Mon Sep 17 00:00:00 2001 From: Dima Grossman Date: Mon, 1 Jul 2024 23:05:01 +0300 Subject: [PATCH 17/18] feat(web): fix cloud bridge status (#5904) * feat: fix cloud bridge status * fix: bridge api --- apps/web/src/studio/hooks/useBridgeAPI.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/apps/web/src/studio/hooks/useBridgeAPI.ts b/apps/web/src/studio/hooks/useBridgeAPI.ts index 6f002c2e84e7..ce1122f50d59 100644 --- a/apps/web/src/studio/hooks/useBridgeAPI.ts +++ b/apps/web/src/studio/hooks/useBridgeAPI.ts @@ -7,6 +7,7 @@ import { type BridgeStatus, } from '../../bridgeApi/bridgeApi.client'; import { useStudioState } from '../StudioStateProvider'; +import { api as cloudApi } from '../../api'; function useBridgeAPI() { const { bridgeURL } = useStudioState(); @@ -14,7 +15,7 @@ function useBridgeAPI() { return useMemo(() => buildBridgeHTTPClient(bridgeURL), [bridgeURL]); } -const BRIDGE_STATUS_REFRESH_INTERVAL_IN_MS = 3 * 1000; +const BRIDGE_STATUS_REFRESH_INTERVAL_IN_MS = 5 * 1000; export const useDiscover = (options?: any) => { const api = useBridgeAPI(); @@ -29,14 +30,18 @@ export const useDiscover = (options?: any) => { }; export const useHealthCheck = (options?: any) => { - const api = useBridgeAPI(); - const { bridgeURL } = useStudioState(); + const bridgeAPI = useBridgeAPI(); + const { bridgeURL, isLocalStudio } = useStudioState(); const res = useQuery( ['bridge-health-check', bridgeURL], async () => { try { - return await api.healthCheck(); + if (isLocalStudio) { + return await bridgeAPI.healthCheck(); + } else { + return await cloudApi.get('/v1/bridge/status'); + } } catch (error) { throw error; } From 752b3a2e292dfadbe867eb41b5e6d9886ce1a754 Mon Sep 17 00:00:00 2001 From: Dima Grossman Date: Mon, 1 Jul 2024 23:51:31 +0300 Subject: [PATCH 18/18] feat(web): add controls to the preview (#5884) * feat: add controls to the preview * fix: sync to latest enterprise * fix: steps * fix: test * fix: page colorr * feat: add debounce * fix: schema * fix: pr comments * fix: pointer --- .../app/events/e2e/bridge-trigger.e2e-ee.ts | 14 +- apps/web/src/bridgeApi/bridgeApi.client.ts | 12 +- .../src/pages/studio-onboarding/preview.tsx | 255 +++++------------- .../components/TemplateEditorFormProvider.tsx | 2 +- .../WorkflowStepEditorControlsPanel.tsx | 15 +- .../subscriber-job-bound.usecase.ts | 2 +- libs/novui/package.json | 2 +- libs/novui/src/hooks/index.ts | 1 + libs/novui/src/index.ts | 1 + .../json-schema-components/JsonSchemaForm.tsx | 1 + libs/shared/src/dto/controls/controls.dto.ts | 4 +- packages/framework/src/handler.ts | 1 + .../framework/src/types/execution.types.ts | 1 + 13 files changed, 103 insertions(+), 208 deletions(-) create mode 100644 libs/novui/src/hooks/index.ts diff --git a/apps/api/src/app/events/e2e/bridge-trigger.e2e-ee.ts b/apps/api/src/app/events/e2e/bridge-trigger.e2e-ee.ts index 4300881cc35b..cfa75d075ec5 100644 --- a/apps/api/src/app/events/e2e/bridge-trigger.e2e-ee.ts +++ b/apps/api/src/app/events/e2e/bridge-trigger.e2e-ee.ts @@ -667,8 +667,8 @@ contexts.forEach((context: Context) => { await saveControlVariables(session, workflowId, stepId, { variables: { name: 'stored_control_name' } }); } - const controls = { controls: { step: { [stepId]: { name: 'stored_control_name' } } } }; - await triggerEvent(session, workflowId, subscriber, controls, bridge); + const controls = { steps: { [stepId]: { name: 'stored_control_name' } } }; + await triggerEvent(session, workflowId, subscriber, undefined, bridge, controls); await session.awaitRunningJobs(); const sentMessage = await messageRepository.find({ @@ -699,7 +699,14 @@ async function syncWorkflow( if (!foundWorkflow) throw new Error('Workflow not found'); } -async function triggerEvent(session, workflowId: string, subscriber, payload?: any, bridge?: { url: string }) { +async function triggerEvent( + session, + workflowId: string, + subscriber, + payload?: any, + bridge?: { url: string }, + controls?: Record +) { const defaultPayload = { name: 'test_name', }; @@ -713,6 +720,7 @@ async function triggerEvent(session, workflowId: string, subscriber, payload?: a email: 'test@subscriber.com', }, payload: payload ?? defaultPayload, + controls: controls ?? undefined, bridgeUrl: bridge?.url ?? undefined, }, { diff --git a/apps/web/src/bridgeApi/bridgeApi.client.ts b/apps/web/src/bridgeApi/bridgeApi.client.ts index 4fdefb2f25e4..441e460c9671 100644 --- a/apps/web/src/bridgeApi/bridgeApi.client.ts +++ b/apps/web/src/bridgeApi/bridgeApi.client.ts @@ -12,6 +12,9 @@ export type TriggerParams = { bridgeUrl?: string; to: { subscriberId: string; email: string }; payload: Record; + controls?: { + steps?: Record; + }; }; export type BridgeStatus = { @@ -85,17 +88,15 @@ export function buildBridgeHTTPClient(baseURL: string) { */ async getStepPreview({ workflowId, stepId, controls, payload }: StepPreviewParams): Promise { return post(`${baseURL}?action=preview&workflowId=${workflowId}&stepId=${stepId}`, { - // TODO: Rename to controls - inputs: controls || {}, - // TODO: Rename to payload - data: payload || {}, + controls: controls || {}, + payload: payload || {}, }); }, /** * TODO: Use framework shared types */ - async trigger({ workflowId, bridgeUrl, to, payload }: TriggerParams): Promise { + async trigger({ workflowId, bridgeUrl, to, payload, controls }: TriggerParams): Promise { payload = payload || {}; payload.__source = 'studio-test-workflow'; @@ -103,6 +104,7 @@ export function buildBridgeHTTPClient(baseURL: string) { bridgeUrl, to, payload, + controls, }); }, }; diff --git a/apps/web/src/pages/studio-onboarding/preview.tsx b/apps/web/src/pages/studio-onboarding/preview.tsx index 2aa6620d4bdf..a1b5e2a89225 100644 --- a/apps/web/src/pages/studio-onboarding/preview.tsx +++ b/apps/web/src/pages/studio-onboarding/preview.tsx @@ -19,8 +19,14 @@ import { useStudioState } from '../../studio/StudioStateProvider'; import { Text, Title } from '@novu/novui'; import { SetupTimeline } from './components/SetupTimeline'; import { BridgeStatus } from '../../bridgeApi/bridgeApi.client'; +import { WorkflowsPanelLayout } from '../../studio/components/workflows/layout'; +import { WorkflowStepEditorContentPanel } from '../../studio/components/workflows/step-editor/WorkflowStepEditorContentPanel'; +import { WorkflowStepEditorControlsPanel } from '../../studio/components/workflows/step-editor/WorkflowStepEditorControlsPanel'; +import { PageContainer } from '../../studio/layout'; export const StudioOnboardingPreview = () => { + const [controls, setStepControls] = useState({}); + const [payload, setPayload] = useState({}); const { testUser } = useStudioState(); const [tab, setTab] = useState('Preview'); const segment = useSegment(); @@ -28,7 +34,7 @@ export const StudioOnboardingPreview = () => { const { data: bridgeResponse, isLoading: isLoadingList } = useDiscover(); const { trigger, isLoading } = useWorkflowTrigger(); - const template = useMemo(() => { + const workflow = useMemo(() => { if (!bridgeResponse?.workflows?.length) { return null; } @@ -36,15 +42,23 @@ export const StudioOnboardingPreview = () => { return bridgeResponse.workflows[0]; }, [bridgeResponse]); + const step = useMemo(() => { + if (!workflow) { + return null; + } + + return workflow?.steps?.[0]; + }, [workflow]); + const { data: preview, isLoading: previewLoading } = useWorkflowPreview( { - workflowId: template?.workflowId, - stepId: template?.steps[0]?.stepId, - payload: {}, - controls: {}, + workflowId: workflow?.workflowId, + stepId: step?.stepId, + payload: payload, + controls: controls, }, { - enabled: !!(template && template?.workflowId && template?.steps[0]?.stepId), + enabled: !!(workflow?.workflowId && step?.stepId), refetchOnWindowFocus: 'always', refetchInterval: 1000, } @@ -55,14 +69,33 @@ export const StudioOnboardingPreview = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + function onControlsChange(type: string, form: any) { + switch (type) { + case 'step': + setStepControls(form.formData); + break; + case 'payload': + setPayload(form.formData); + break; + } + } + const onTrigger = async () => { const to = { subscriberId: testUser.id, email: testUser.emailAddress, }; - const payload = { __source: 'studio-onboarding-test-workflow' }; - const response = await trigger({ workflowId: template?.workflowId, to, payload }); + const response = await trigger({ + workflowId: workflow?.workflowId, + to, + payload: { ...payload, __source: 'studio-onboarding-test-workflow' }, + controls: { + steps: { + [step?.stepId]: controls, + }, + }, + }); navigate({ pathname: ROUTES.STUDIO_ONBOARDING_SUCCESS, @@ -76,26 +109,23 @@ export const StudioOnboardingPreview = () => {
-
Preview your workflow @@ -111,179 +141,22 @@ export const StudioOnboardingPreview = () => { This is a preview of your sample workflow located in the app/novu/workflows directory. You can edit this file in your IDE and see the email changes reflected here. - { - setTab(value as string); - }} - menuTabs={[ - { - icon: , - value: 'Preview', - content: ( - {}} - locales={[]} - bridge={true} - loading={previewLoading || isLoadingList} - classNames={{ - contentContainer: css({ - height: 'calc(60vh - 28px) !important', - }), - }} - /> - ), - }, - { - icon: , - value: 'Code', - content: ( - - {`workflow("welcome-onboarding-email", async ({ step, payload }) => { - await step.email( - "send-email", - async (controls) => { - return { - subject: "A Successful Test on Novu!", - body: renderEmail(controls, payload), - }; - }, - { - controlSchema: { - type: "object", - properties: { - components: { - title: "Add Custom Fields:", - type: "array", - default: [{ - "componentType": "heading", - "componentText": "Welcome to Novu" - }, { - "componentType": "text", - "componentText": "Congratulations on receiving your first notification email from Novu! Join the hundreds of thousands of developers worldwide who use Novu to build notification platforms for their products." - }, { - "componentType": "list", - "componentListItems": [ - { - title: "Send Multi-channel notifications", - body: "You can send notifications to your users via multiple channels (Email, SMS, Push, and In-App) in a heartbeat." - }, - { - title: "Send Multi-channel notifications", - body: "You can send notifications to your users via multiple channels (Email, SMS, Push, and In-App) in a heartbeat." - } - ] - }, { - "componentType": "text", - "componentText": "Ready to get started? Click on the button below, and you will see first-hand how easily you can edit this email content." - }, { - "componentType": "button", - "componentText": "Edit Email" - }], - items: { - type: "object", - properties: { - componentType: { - type: "string", - enum: [ - "text", "divider", "button", "button-link", "image", "image-2", "image-3", "heading", "users", "list" - ], - default: "text", - }, - componentText: { - type: "string", - default: "", - }, - componentLink: { - type: "string", - default: "https://enterlink.com", - format: "uri", - }, - align: { - type: "string", - enum: ["left", "center", "right"], - default: "center", - }, - componentListItems: { - type: "array", - default: [], - items: { - type: "object", - properties: { - title: { - type: "string" - }, - body: { - type: "string" - } - } - } - } - }, - }, - }, - welcomeHeaderText: { - type: "string", - default: "Welcome to Novu {{helloWorld}}" - }, - belowHeaderText: { - title: "Text Under The Welcome Header", - type: "string", - default: "Congratulations on receiving your first notification email from Novu! Join the hundreds of thousands of developers worldwide who use Novu to build notification platforms for their products." - }, - }, - }, - }, - ); - }, - { payloadSchema: { - type: "object", - properties: { - teamImage: { - title: "Team Image", - type: "string", - default: - "https://images.spr.so/cdn-cgi/imagedelivery/j42No7y-dcokJuNgXeA0ig/dca73b36-cf39-4e28-9bc7-8a0d0cd8ac70/standalone-gradient2x_2/w=128,quality=90,fit=scale-down", - format: "uri", - }, - userImage: { - title: "User Image", - type: "string", - default: - "https://react-email-demo-48zvx380u-resend.vercel.app/static/vercel-user.png", - format: "uri", - }, - arrowImage: { - title: "Arrow", - type: "string", - default: - "https://react-email-demo-bdj5iju9r-resend.vercel.app/static/vercel-arrow.png", - format: "uri", - }, - editEmailLink: { - title: "Email Link Button Text", - type: "string", - default: "https://web.novu.co", - format: "uri", - }, - helloWorld: { - type: "string", - default: "Hello World" - }, - } - } - }, -); -`} - - ), - }, - ]} - /> -
+ + + + + +