Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: move in app preview render to backend #4936

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions apps/api/src/app/content-templates/content-templates.controller.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
import { ApiExcludeController } from '@nestjs/swagger';
import { IEmailBlock, IJwtPayload, MessageTemplateContentType } from '@novu/shared';
import { CompileEmailTemplate, CompileEmailTemplateCommand, JwtAuthGuard } from '@novu/application-generic';
import { IEmailBlock, IJwtPayload, IMessageCTA, MessageTemplateContentType } from '@novu/shared';
import {
CompileEmailTemplate,
CompileEmailTemplateCommand,
CompileInAppTemplate,
CompileInAppTemplateCommand,
JwtAuthGuard,
} from '@novu/application-generic';

import { UserSession } from '../shared/framework/user.decorator';

@Controller('/content-templates')
@UseGuards(JwtAuthGuard)
@ApiExcludeController()
export class ContentTemplatesController {
constructor(private compileEmailTemplateUsecase: CompileEmailTemplate) {}
constructor(
private compileEmailTemplateUsecase: CompileEmailTemplate,
private compileInAppTemplate: CompileInAppTemplate
) {}

@Post('/preview/email')
public previewEmail(
Expand All @@ -33,4 +42,23 @@ export class ContentTemplatesController {
})
);
}

@Post('/preview/in-app')
public previewInApp(
@UserSession() user: IJwtPayload,
@Body('content') content: string,
@Body('payload') payload: any,
@Body('cta') cta: IMessageCTA
) {
return this.compileInAppTemplate.execute(
CompileInAppTemplateCommand.create({
userId: user._id,
organizationId: user.organizationId,
environmentId: user.environmentId,
content,
payload,
cta,
})
);
}
}
4 changes: 2 additions & 2 deletions apps/api/src/app/content-templates/usecases/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { CompileTemplate, CompileEmailTemplate } from '@novu/application-generic';
import { CompileTemplate, CompileEmailTemplate, CompileInAppTemplate } from '@novu/application-generic';

export const USE_CASES = [CompileTemplate, CompileEmailTemplate];
export const USE_CASES = [CompileTemplate, CompileEmailTemplate, CompileInAppTemplate];
4 changes: 4 additions & 0 deletions apps/web/src/api/content-templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ export async function previewEmail({
}) {
return api.post('/v1/content-templates/preview/email', { content, contentType, payload, subject, layoutId });
}

export async function previewInApp({ content, cta, payload }: { content?: string; cta: any; payload: string }) {
return api.post('/v1/content-templates/preview/in-app', { content, payload, cta });
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { useEffect, useState } from 'react';
import { Control, Controller, useFormContext, useWatch } from 'react-hook-form';
import Handlebars from 'handlebars/dist/handlebars';
import { IMessageAction } from '@novu/shared';

import { Controller, useFormContext, useWatch } from 'react-hook-form';
import { IMessageAction, MessageActionStatusEnum } from '@novu/shared';
import { InAppWidgetPreview } from './preview/InAppWidgetPreview';
import type { IForm } from '../formTypes';
import { EmailCustomCodeEditor } from '../email-editor/EmailCustomCodeEditor';
import { When } from '../../../../components/utils/When';
import { useStepFormPath } from '../../hooks/useStepFormPath';
import { colors, errorMessage } from '@novu/design-system';
import { useMutation } from '@tanstack/react-query';
import { previewInApp } from '../../../../api/content-templates';
import { Center, Loader } from '@mantine/core';

export function InAppEditorBlock({
readonly,
Expand All @@ -25,6 +26,10 @@ export function InAppEditorBlock({
control,
});

if (preview) {
return <ContentRender readonly={readonly} payload={payload} />;
}

return (
<Controller
name={`${path}.template.cta.action`}
Expand All @@ -35,39 +40,85 @@ export function InAppEditorBlock({
const { ref, ...fieldRefs } = field;

return (
<InAppWidgetPreview {...fieldRefs} preview={preview} readonly={readonly} enableAvatar={!!enableAvatar}>
<>
<When truthy={!preview}>
<ContentContainerController />
</When>
<When truthy={preview}>
<ContentRender payload={payload} />
</When>
</>
<InAppWidgetPreview {...fieldRefs} readonly={readonly} enableAvatar={!!enableAvatar}>
<ContentContainerController />
</InAppWidgetPreview>
);
}}
/>
);
}

const ContentRender = ({ payload }: { payload: string }) => {
const ContentRender = ({ payload, readonly }: { payload: string; readonly: boolean }) => {
const { control } = useFormContext<IForm>();
const path = useStepFormPath();
const content = useWatch({
name: `${path}.template.content`,
control,
});
const cta = useWatch({
name: `${path}.template.cta`,
control,
});
const enableAvatar = useWatch({
name: `${path}.template.enableAvatar` as any,
control,
});

const [buttons, setButtons] = useState<any[]>([]);
const { isLoading, mutateAsync } = useMutation(previewInApp);
const [compiledContent, setCompiledContent] = useState('');

const parseContent = (args: { content?: string | any; payload: any; cta: any }) => {
mutateAsync({
...args,
payload: JSON.parse(args.payload),
})
.then((result: { content: string; ctaButtons: any[] }) => {
setCompiledContent(result.content);
setButtons(result.ctaButtons);

return result;
})
.catch((e: any) => {
errorMessage(e?.message || 'Un-expected error occurred');
});
};

useEffect(() => {
try {
const template = Handlebars.compile(content);
setCompiledContent(template(JSON.parse(payload)));
} catch (e) {}
}, [content, payload]);
parseContent({
content,
payload,
cta,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [compiledContent, payload, cta]);

if (isLoading) {
return (
<div>
<Center>
<Loader color={colors.B70} mb={20} mt={20} size={32} />
</Center>
</div>
);
}

return <span data-test-id="in-app-content-preview" dangerouslySetInnerHTML={{ __html: compiledContent }} />;
return (
<InAppWidgetPreview
preview={true}
readonly={readonly}
enableAvatar={!!enableAvatar}
value={{
status: MessageActionStatusEnum.PENDING,
buttons: buttons,
result: {},
}}
onChange={() => {}}
>
<span data-test-id="in-app-content-preview" dangerouslySetInnerHTML={{ __html: compiledContent }} />
</InAppWidgetPreview>
);
};

function ContentContainerController() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,11 @@ import {
SubscriberRepository,
MessageEntity,
OrganizationRepository,
OrganizationEntity,
} from '@novu/dal';
import {
ChannelTypeEnum,
IMessageButton,
ExecutionDetailsSourceEnum,
ExecutionDetailsStatusEnum,
IEmailBlock,
ActorTypeEnum,
WebSocketEventEnum,
} from '@novu/shared';
Expand All @@ -25,14 +22,14 @@ import {
DetailEnum,
CreateExecutionDetailsCommand,
SelectIntegration,
CompileTemplate,
CompileTemplateCommand,
WebSocketsQueueService,
buildFeedKey,
buildMessageCountKey,
GetNovuProviderCredentials,
SelectVariant,
ExecutionLogQueueService,
CompileInAppTemplate,
CompileInAppTemplateCommand,
} from '@novu/application-generic';

import { CreateLog } from '../../../shared/logs';
Expand All @@ -51,12 +48,12 @@ export class SendMessageInApp extends SendMessageBase {
protected createLogUsecase: CreateLog,
protected executionLogQueueService: ExecutionLogQueueService,
protected subscriberRepository: SubscriberRepository,
private compileTemplate: CompileTemplate,
private organizationRepository: OrganizationRepository,
protected selectIntegration: SelectIntegration,
protected getNovuProviderCredentials: GetNovuProviderCredentials,
protected selectVariant: SelectVariant,
protected moduleRef: ModuleRef
protected moduleRef: ModuleRef,
protected compileInAppTemplate: CompileInAppTemplate
) {
super(
messageRepository,
Expand Down Expand Up @@ -126,25 +123,24 @@ export class SendMessageInApp extends SendMessageBase {
}

try {
content = await this.compileInAppTemplate(step.template.content, command.compileContext, organization);
const compiled = await this.compileInAppTemplate.execute(
CompileInAppTemplateCommand.create({
organizationId: command.organizationId,
environmentId: command.environmentId,
payload: this.getCompilePayload(command.compileContext),
content: step.template.content as string,
cta: step.template.cta,
userId: command.userId,
})
);
content = compiled.content;

if (step.template.cta?.data?.url) {
step.template.cta.data.url = await this.compileInAppTemplate(
step.template.cta?.data?.url,
command.compileContext,
organization
);
step.template.cta.data.url = compiled.url;
}

if (step.template.cta?.action?.buttons) {
const ctaButtons: IMessageButton[] = [];

for (const action of step.template.cta.action.buttons) {
const buttonContent = await this.compileInAppTemplate(action.content, command.compileContext, organization);
ctaButtons.push({ type: action.type, content: buttonContent });
}

step.template.cta.action.buttons = ctaButtons;
step.template.cta.action.buttons = compiled.ctaButtons;
}
} catch (e) {
await this.sendErrorHandlebars(command.job, e.message);
Expand Down Expand Up @@ -277,20 +273,4 @@ export class SendMessageInApp extends SendMessageBase {
command.organizationId
);
}

private async compileInAppTemplate(
content: string | IEmailBlock[],
payload: any,
organization: OrganizationEntity | null
): Promise<string> {
return await this.compileTemplate.execute(
CompileTemplateCommand.create({
template: content as string,
data: {
...this.getCompilePayload(payload),
branding: { logo: organization?.branding?.logo, color: organization?.branding?.color || '#f47373' },
},
})
);
}
}
2 changes: 2 additions & 0 deletions apps/worker/src/app/workflow/workflow.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
SubscriberJobBound,
TriggerBroadcast,
TriggerMulticast,
CompileInAppTemplate,
} from '@novu/application-generic';
import { JobRepository } from '@novu/dal';

Expand Down Expand Up @@ -131,6 +132,7 @@ const USE_CASES = [
SubscriberJobBound,
TriggerBroadcast,
TriggerMulticast,
CompileInAppTemplate,
];

const PROVIDERS: Provider[] = [
Expand Down
2 changes: 1 addition & 1 deletion enterprise
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { IsDefined, IsOptional } from 'class-validator';
import { IMessageCTA } from '@novu/shared';

import { EnvironmentWithUserCommand } from '../../commands/project.command';

export class CompileInAppTemplateCommand extends EnvironmentWithUserCommand {
@IsDefined()
content: string;

@IsDefined()
payload: any; // eslint-disable-line @typescript-eslint/no-explicit-any

@IsOptional()
cta?: IMessageCTA;
}
Loading