diff --git a/apps/api/src/app/environments-v1/novu-bridge.module.ts b/apps/api/src/app/environments-v1/novu-bridge.module.ts index 6bdc538fefd..9339893706e 100644 --- a/apps/api/src/app/environments-v1/novu-bridge.module.ts +++ b/apps/api/src/app/environments-v1/novu-bridge.module.ts @@ -12,7 +12,7 @@ import { HydrateEmailSchemaUseCase, InAppOutputRendererUsecase, PushOutputRendererUsecase, - RenderEmailOutputUsecase, + EmailOutputRendererUsecase, SmsOutputRendererUsecase, } from './usecases/output-renderers'; import { DelayOutputRendererUsecase } from './usecases/output-renderers/delay-output-renderer.usecase'; @@ -31,11 +31,11 @@ import { DigestOutputRendererUsecase } from './usecases/output-renderers/digest- ConstructFrameworkWorkflow, GetDecryptedSecretKey, InAppOutputRendererUsecase, - RenderEmailOutputUsecase, + EmailOutputRendererUsecase, SmsOutputRendererUsecase, ChatOutputRendererUsecase, PushOutputRendererUsecase, - RenderEmailOutputUsecase, + EmailOutputRendererUsecase, ExpandEmailEditorSchemaUsecase, HydrateEmailSchemaUseCase, DelayOutputRendererUsecase, diff --git a/apps/api/src/app/environments-v1/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 index e803139424c..4af07018d61 100644 --- a/apps/api/src/app/environments-v1/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 @@ -12,7 +12,7 @@ import { FullPayloadForRender, InAppOutputRendererUsecase, PushOutputRendererUsecase, - RenderEmailOutputUsecase, + EmailOutputRendererUsecase, SmsOutputRendererUsecase, } from '../output-renderers'; import { DelayOutputRendererUsecase } from '../output-renderers/delay-output-renderer.usecase'; @@ -28,7 +28,7 @@ export class ConstructFrameworkWorkflow { private logger: PinoLogger, private workflowsRepository: NotificationTemplateRepository, private inAppOutputRendererUseCase: InAppOutputRendererUsecase, - private emailOutputRendererUseCase: RenderEmailOutputUsecase, + private emailOutputRendererUseCase: EmailOutputRendererUsecase, private smsOutputRendererUseCase: SmsOutputRendererUsecase, private chatOutputRendererUseCase: ChatOutputRendererUsecase, private pushOutputRendererUseCase: PushOutputRendererUsecase, diff --git a/apps/api/src/app/environments-v1/usecases/output-renderers/render-email-output.usecase.ts b/apps/api/src/app/environments-v1/usecases/output-renderers/email-output-renderer.usecase.ts similarity index 91% rename from apps/api/src/app/environments-v1/usecases/output-renderers/render-email-output.usecase.ts rename to apps/api/src/app/environments-v1/usecases/output-renderers/email-output-renderer.usecase.ts index 44c1b3b2137..1388d640f87 100644 --- a/apps/api/src/app/environments-v1/usecases/output-renderers/render-email-output.usecase.ts +++ b/apps/api/src/app/environments-v1/usecases/output-renderers/email-output-renderer.usecase.ts @@ -6,15 +6,14 @@ import { Instrument, InstrumentUsecase } from '@novu/application-generic'; import { FullPayloadForRender, RenderCommand } from './render-command'; import { ExpandEmailEditorSchemaUsecase } from './expand-email-editor-schema.usecase'; -export class RenderEmailOutputCommand extends RenderCommand {} +export class EmailOutputRendererCommand extends RenderCommand {} @Injectable() -// todo rename to EmailOutputRenderer -export class RenderEmailOutputUsecase { +export class EmailOutputRendererUsecase { constructor(private expandEmailEditorSchemaUseCase: ExpandEmailEditorSchemaUsecase) {} @InstrumentUsecase() - async execute(renderCommand: RenderEmailOutputCommand): Promise { + async execute(renderCommand: EmailOutputRendererCommand): Promise { const { body, subject } = renderCommand.controlValues; if (!body || typeof body !== 'string') { @@ -43,7 +42,7 @@ export class RenderEmailOutputUsecase { private async parseTipTapNodeByLiquid( tiptapNode: TipTapNode, - renderCommand: RenderEmailOutputCommand + renderCommand: EmailOutputRendererCommand ): Promise { const client = new Liquid({ outputEscape: (output) => { diff --git a/apps/api/src/app/environments-v1/usecases/output-renderers/hydrate-email-schema.usecase.ts b/apps/api/src/app/environments-v1/usecases/output-renderers/hydrate-email-schema.usecase.ts index 60b6aec001a..2aa99e2b1ca 100644 --- a/apps/api/src/app/environments-v1/usecases/output-renderers/hydrate-email-schema.usecase.ts +++ b/apps/api/src/app/environments-v1/usecases/output-renderers/hydrate-email-schema.usecase.ts @@ -1,8 +1,10 @@ /* eslint-disable no-param-reassign */ import { Injectable } from '@nestjs/common'; -import { PreviewPayload, TipTapNode } from '@novu/shared'; import { z } from 'zod'; -import { processNodeAttrs } from '@novu/application-generic'; + +import { PreviewPayload, TipTapNode } from '@novu/shared'; +import { processNodeAttrs, processNodeMarks } from '@novu/application-generic'; + import { HydrateEmailSchemaCommand } from './hydrate-email-schema.command'; import { PlaceholderAggregation } from '../../../workflows-v2/usecases'; @@ -18,13 +20,13 @@ export class HydrateEmailSchemaUseCase { }; // TODO: Aligned Zod inferred type and TipTapNode to remove the need of a type assertion - const emailEditorSchema: TipTapNode = TipTapSchema.parse(JSON.parse(command.emailEditor)) as TipTapNode; - if (emailEditorSchema.content) { - this.transformContentInPlace(emailEditorSchema.content, command.fullPayloadForRender, placeholderAggregation); + const emailBody: TipTapNode = TipTapSchema.parse(JSON.parse(command.emailEditor)) as TipTapNode; + if (emailBody) { + this.transformContentInPlace([emailBody], command.fullPayloadForRender, placeholderAggregation); } return { - hydratedEmailSchema: emailEditorSchema, + hydratedEmailSchema: emailBody, placeholderAggregation, }; } @@ -89,6 +91,7 @@ export class HydrateEmailSchemaUseCase { ) { content.forEach((node, index) => { processNodeAttrs(node); + processNodeMarks(node); if (this.isVariableNode(node)) { this.variableLogic(masterPayload, node, content, index, placeholderAggregation); diff --git a/apps/api/src/app/environments-v1/usecases/output-renderers/index.ts b/apps/api/src/app/environments-v1/usecases/output-renderers/index.ts index 8f220cfd907..64cdbeeed83 100644 --- a/apps/api/src/app/environments-v1/usecases/output-renderers/index.ts +++ b/apps/api/src/app/environments-v1/usecases/output-renderers/index.ts @@ -3,7 +3,7 @@ export * from './render-command'; export * from './push-output-renderer.usecase'; export * from './sms-output-renderer.usecase'; export * from './in-app-output-renderer.usecase'; -export * from './render-email-output.usecase'; +export * from './email-output-renderer.usecase'; export * from './hydrate-email-schema.usecase'; export * from './hydrate-email-schema.command'; export * from './expand-email-editor-schema.usecase'; diff --git a/apps/api/src/app/workflows-v2/usecases/generate-preview/generate-preview.usecase.ts b/apps/api/src/app/workflows-v2/usecases/generate-preview/generate-preview.usecase.ts index c3215401736..619db828071 100644 --- a/apps/api/src/app/workflows-v2/usecases/generate-preview/generate-preview.usecase.ts +++ b/apps/api/src/app/workflows-v2/usecases/generate-preview/generate-preview.usecase.ts @@ -2,6 +2,8 @@ import { Injectable, InternalServerErrorException } from '@nestjs/common'; import _ from 'lodash'; import Ajv, { ErrorObject } from 'ajv'; import addFormats from 'ajv-formats'; +import { captureException } from '@sentry/node'; + import { ChannelTypeEnum, createMockObjectFromSchema, @@ -22,8 +24,8 @@ import { PinoLogger, dashboardSanitizeControlValues, } from '@novu/application-generic'; -import { captureException } from '@sentry/node'; import { channelStepSchemas, actionStepSchemas } from '@novu/framework/internal'; + import { PreviewStep, PreviewStepCommand } from '../../../bridge/usecases/preview-step'; import { FrameworkPreviousStepsOutputState } from '../../../bridge/usecases/preview-step/preview-step.command'; import { BuildStepDataUsecase } from '../build-step-data'; @@ -70,7 +72,7 @@ export class GeneratePreviewUsecase { if (!sanitizedValidatedControls && workflow.origin === WorkflowOriginEnum.NOVU_CLOUD) { throw new Error( // eslint-disable-next-line max-len - 'Control values normalization failed: The normalizeControlValues function requires maintenance to sanitize the provided type or data structure correctly' + 'Control values normalization failed, normalizeControlValues function requires maintenance to sanitize the provided type or data structure correctly' ); } diff --git a/apps/api/src/app/workflows-v2/usecases/generate-preview/transform-maily-content-to-liquid.ts b/apps/api/src/app/workflows-v2/usecases/generate-preview/transform-maily-content-to-liquid.ts index f91df8dc0f3..ee3a3d8ba9b 100644 --- a/apps/api/src/app/workflows-v2/usecases/generate-preview/transform-maily-content-to-liquid.ts +++ b/apps/api/src/app/workflows-v2/usecases/generate-preview/transform-maily-content-to-liquid.ts @@ -1,6 +1,6 @@ import { JSONContent } from '@maily-to/render'; import _ from 'lodash'; -import { processNodeAttrs, MailyContentTypeEnum } from '@novu/application-generic'; +import { processNodeAttrs, MailyContentTypeEnum, processNodeMarks } from '@novu/application-generic'; /** * Processes raw Maily JSON editor state by converting variables to Liquid.js output syntax @@ -101,7 +101,8 @@ function processForLoopNode(node: JSONContent): JSONContent { function processNode(node: JSONContent): JSONContent { if (!node) return node; - const processedNode = processNodeAttrs(node); + let processedNode = processNodeAttrs(node); + processedNode = processNodeMarks(processedNode); switch (processedNode.type) { case MailyContentTypeEnum.VARIABLE: diff --git a/libs/application-generic/src/utils/process-node-attrs.ts b/libs/application-generic/src/utils/process-node-attrs.ts index 175850dc73e..737ea214b86 100644 --- a/libs/application-generic/src/utils/process-node-attrs.ts +++ b/libs/application-generic/src/utils/process-node-attrs.ts @@ -5,10 +5,11 @@ export enum MailyContentTypeEnum { FOR = 'for', BUTTON = 'button', IMAGE = 'image', + LINK = 'link', } export const variableAttributeConfig = (type: MailyContentTypeEnum) => { - //todo add variable type + // todo add variable type if (type === MailyContentTypeEnum.BUTTON) { return [ { attr: 'text', flag: 'isTextVariable' }, @@ -25,18 +26,43 @@ export const variableAttributeConfig = (type: MailyContentTypeEnum) => { ]; } + if (type === MailyContentTypeEnum.LINK) { + return [{ attr: 'href', flag: 'isUrlVariable' }]; + } + return [{ attr: 'showIfKey', flag: 'showIfKey' }]; }; -export function processNodeAttrs(node: JSONContent): JSONContent { - if (!node.attrs) return node; +function processAttributes( + attrs: Record, + type: MailyContentTypeEnum, +): void { + if (!attrs) return; - const typeConfig = variableAttributeConfig(node.type as MailyContentTypeEnum); + const typeConfig = variableAttributeConfig(type); for (const { attr, flag } of typeConfig) { - if (node.attrs[flag] && node.attrs[attr]) { + if (attrs[flag] && attrs[attr]) { // eslint-disable-next-line no-param-reassign - node.attrs[attr] = wrapInLiquidOutput(node.attrs[attr] as string); + attrs[attr] = wrapInLiquidOutput(attrs[attr] as string); + } + } +} + +export function processNodeAttrs(node: JSONContent): JSONContent { + if (!node.attrs) return node; + + processAttributes(node.attrs, node.type as MailyContentTypeEnum); + + return node; +} + +export function processNodeMarks(node: JSONContent): JSONContent { + if (!node.marks) return node; + + for (const mark of node.marks) { + if (mark.attrs) { + processAttributes(mark.attrs, mark.type as MailyContentTypeEnum); } }