Skip to content

Commit

Permalink
feat(framework): Add new Inbox properties to step.inApp schema (#6075)
Browse files Browse the repository at this point in the history
Co-authored-by: Joel Anton <[email protected]>
  • Loading branch information
rifont and Joel Anton authored Aug 5, 2024
1 parent 55ebfb5 commit d98ee52
Show file tree
Hide file tree
Showing 12 changed files with 149 additions and 27 deletions.
3 changes: 2 additions & 1 deletion apps/api/src/app/inbox/utils/notification-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,12 @@ const mapSingleItem = ({
avatar,
primaryAction: primaryCta && {
label: primaryCta.content,
url: cta?.data.url,
url: primaryCta.url,
isCompleted: actionType === ButtonTypeEnum.PRIMARY && actionStatus === MessageActionStatusEnum.DONE,
},
secondaryAction: secondaryCta && {
label: secondaryCta.content,
url: secondaryCta.url,
isCompleted: actionType === ButtonTypeEnum.SECONDARY && actionStatus === MessageActionStatusEnum.DONE,
},
channelType: channel,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ export const NotificationTextStyled = styled.div<{ isExampleNotification: boolea
}}
`;

export const SubjectTextStyled = styled.p`
font-size: 16px;
font-style: normal;
font-weight: 600;
line-height: 20px;
margin: 0;
`;

export const TimeTextStyled = styled(NotificationTextStyled)`
color: ${colors.B40};
${({ isExampleNotification }) => {
Expand Down
11 changes: 9 additions & 2 deletions apps/web/src/components/workflow/preview/in-app/Content.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { Group, Stack } from '@mantine/core';
import { colors, Text } from '@novu/design-system';
import { MessageActionStatusEnum } from '@novu/shared';
import { ActorTypeEnum, MessageActionStatusEnum } from '@novu/shared';

import { useHover } from '../../../../hooks';
import { ActionBlockContainer } from '../../../../pages/templates/components/in-app-editor/preview/ActionBlockContainer';
import AvatarContainer from '../../../../pages/templates/components/in-app-editor/preview/AvatarContainer';
import AvatarContainer, {
RenderAvatar,
} from '../../../../pages/templates/components/in-app-editor/preview/AvatarContainer';
import { ParsedPreviewStateType } from '../../../../pages/templates/hooks/usePreviewInAppTemplate';
import { PreviewEditOverlay } from '../common';
import {
ContentAndOverlayWrapperStyled,
ContentStyled,
NotificationTextStyled,
SkeletonStyled,
SubjectTextStyled,
TimeTextStyled,
} from './Content.styles';

Expand Down Expand Up @@ -48,7 +51,11 @@ export default function Content({
<div>
<Group spacing={10} align="flex-start">
{enableAvatar && <AvatarContainer opened={false} setOpened={() => {}} readonly={true} />}
{parsedPreviewState.avatar && (
<RenderAvatar actor={{ type: ActorTypeEnum.SYSTEM_CUSTOM, data: parsedPreviewState.avatar }} />
)}
<Stack spacing={24}>
{parsedPreviewState.subject && <SubjectTextStyled>{parsedPreviewState.subject}</SubjectTextStyled>}
<NotificationTextStyled
isExampleNotification={false}
dangerouslySetInnerHTML={{
Expand Down
26 changes: 18 additions & 8 deletions apps/web/src/components/workflow/preview/in-app/InAppPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useMutation } from '@tanstack/react-query';
import { useTemplateEditorForm } from '../../../../pages/templates/components/TemplateEditorFormProvider';
import { ControlVariablesForm } from '../../../../pages/templates/components/ControlVariablesForm';
import { InAppBasePreview } from './InAppBasePreview';
import { IMessageButton, inAppMessageFromBridgeOutputs } from '@novu/shared';

export function InAppPreview({ showVariables = true }: { showVariables?: boolean }) {
const theme = useMantineTheme();
Expand All @@ -28,19 +29,28 @@ export function InAppPreview({ showVariables = true }: { showVariables?: boolean
const processedVariables = useProcessVariables(variables);

const stepId = watch(`${path}.uuid`);
const [bridgeContent, setBridgeContent] = useState({ content: '', ctaButtons: [] });
const [bridgeContent, setBridgeContent] = useState<{
content: string;
ctaButtons: Array<IMessageButton>;
subject?: string;
avatar?: string;
}>({
content: '',
ctaButtons: [],
subject: '',
avatar: '',
});

const {
mutateAsync,
isLoading: isBridgeLoading,
error: previewError,
} = useMutation(
const { mutateAsync, isLoading: isBridgeLoading } = useMutation(
(data) => api.post('/v1/bridge/preview/' + formState?.defaultValues?.identifier + '/' + stepId, data),
{
onSuccess(data) {
const inAppMessage = inAppMessageFromBridgeOutputs(data.outputs);
setBridgeContent({
content: data.outputs.body,
ctaButtons: [],
subject: inAppMessage.subject,
content: inAppMessage.content,
avatar: inAppMessage.avatar,
ctaButtons: inAppMessage.cta.action.buttons,
});
},
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ const AvatarContainer = ({
);
};

function RenderAvatar({ actor }: { actor: IActor }) {
export function RenderAvatar({ actor }: { actor: IActor }) {
if (!actor.type || actor.type === ActorTypeEnum.NONE) {
return <Camera />;
}
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/pages/templates/hooks/usePreviewInAppTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { useStepFormPath } from './useStepFormPath';
import { parsePayload } from '../../../utils';

export type ParsedPreviewStateType = {
subject?: string;
avatar?: string;
ctaButtons: IMessageButton[];
content: string;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Prism } from '@mantine/prism';
import { Tabs } from '@novu/novui';
import { IconOutlineCode, IconVisibility } from '@novu/novui/icons';
import { VStack } from '@novu/novui/jsx';
import { StepTypeEnum } from '@novu/shared';
import { ButtonTypeEnum, inAppMessageFromBridgeOutputs, StepTypeEnum } from '@novu/shared';
import { PreviewWeb } from '../../../../components/workflow/preview/email/PreviewWeb';
import { useActiveIntegrations } from '../../../../hooks';
import {
Expand Down Expand Up @@ -133,7 +133,19 @@ export const PreviewStep = ({
return <SmsBasePreview content={preview?.outputs?.body} {...props} />;

case StepTypeEnum.IN_APP:
return <InAppBasePreview content={{ content: preview?.outputs?.body, ctaButtons: [] }} {...props} />;
const inAppMessage = inAppMessageFromBridgeOutputs(preview?.outputs);

return (
<InAppBasePreview
content={{
subject: inAppMessage.subject,
content: inAppMessage.content,
avatar: inAppMessage.avatar,
ctaButtons: inAppMessage.cta.action.buttons,
}}
{...props}
/>
);

case StepTypeEnum.CHAT:
return <ChatBasePreview content={preview?.outputs?.body} {...props} />;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { addBreadcrumb } from '@sentry/node';
import { ModuleRef } from '@nestjs/core';

import {
MessageRepository,
NotificationStepEntity,
SubscriberRepository,
MessageEntity,
OrganizationEntity,
OrganizationRepository,
} from '@novu/dal';
import { MessageRepository, NotificationStepEntity, SubscriberRepository, MessageEntity } from '@novu/dal';
import {
ChannelTypeEnum,
ExecutionDetailsSourceEnum,
Expand All @@ -32,11 +25,13 @@ import {
ExecutionLogRoute,
ExecutionLogRouteCommand,
} from '@novu/application-generic';
import { InAppOutput } from '@novu/framework';

import { CreateLog } from '../../../shared/logs';
import { SendMessageCommand } from './send-message.command';
import { SendMessageBase } from './send-message.base';
import { PlatformException } from '../../../shared/utils';
import { inAppMessageFromBridgeOutputs } from '@novu/shared';

@Injectable()
export class SendMessageInApp extends SendMessageBase {
Expand Down Expand Up @@ -180,7 +175,9 @@ export class SendMessageInApp extends SendMessageBase {
}),
});

const bridgeBody = command.bridgeData?.outputs.body;
// V2 data
const bridgeOutputs = command.bridgeData?.outputs as InAppOutput;
const inAppMessage = inAppMessageFromBridgeOutputs(bridgeOutputs);

if (!oldMessage) {
message = await this.messageRepository.create({
Expand All @@ -191,10 +188,12 @@ export class SendMessageInApp extends SendMessageBase {
_templateId: command._templateId,
_messageTemplateId: step.template._id,
channel: ChannelTypeEnum.IN_APP,
cta: step.template.cta,
cta: bridgeOutputs ? inAppMessage.cta : step.template.cta,
_feedId: step.template._feedId,
transactionId: command.transactionId,
content: this.storeContent() ? bridgeBody || content : null,
content: this.storeContent() ? inAppMessage.content || content : null,
subject: inAppMessage.subject,
avatar: inAppMessage.avatar,
payload: messagePayload,
providerId: integration.providerId,
templateIdentifier: command.identifier,
Expand Down
1 change: 1 addition & 0 deletions libs/dal/src/repositories/message/message.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const messageSchema = new Schema<MessageDBModel>(
},
content: Schema.Types.String,
resultContent: Schema.Types.String,
url: Schema.Types.String,
},
],
result: {
Expand Down
3 changes: 3 additions & 0 deletions libs/shared/src/entities/messages/messages.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export interface IMessage {
_layoutId?: string;
payload: Record<string, unknown>;
actor?: IActor;
avatar?: string;
subject?: string;
}

export interface IMessageCTA {
Expand All @@ -47,6 +49,7 @@ export interface IMessageButton {
type: ButtonTypeEnum;
content: string;
resultContent?: string;
url?: string;
}

export enum MessageActionStatusEnum {
Expand Down
67 changes: 66 additions & 1 deletion libs/shared/src/utils/bridge.utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,70 @@
import { WorkflowTypeEnum } from '../types';
import { ButtonTypeEnum, IMessage, IMessageCTA } from '../entities/messages';
import { ChannelCTATypeEnum, WorkflowTypeEnum } from '../types';

export const isBridgeWorkflow = (workflowType?: WorkflowTypeEnum): boolean => {
return workflowType === WorkflowTypeEnum.BRIDGE || workflowType === WorkflowTypeEnum.ECHO;
};

/**
* This typing already lives in @novu/framework, but due to a circular dependency, we currently
* need to duplicate it here.
*
* TODO: reconsider the dependency tree between @novu/shared and @novu/framework and move this
* function to be shared across all apps. We will likely want to create a separate package for
* schemas and their inferred type definitions.
*/
type InAppOutput = {
subject?: string;
body: string;
avatar?: string;
primaryAction?: {
label: string;
url?: string;
};
secondaryAction?: {
label: string;
url?: string;
};
};

type InAppMessage = Pick<IMessage, 'subject' | 'content' | 'cta' | 'avatar'>;

/**
* This function maps the V2 InAppOutput to the V1 MessageEntity.
*/
export const inAppMessageFromBridgeOutputs = (outputs?: InAppOutput) => {
const cta = {
type: ChannelCTATypeEnum.REDIRECT,
data: {},
action: {
result: {},
buttons: [
...(outputs?.primaryAction
? [
{
type: ButtonTypeEnum.PRIMARY,
content: outputs.primaryAction.label,
url: outputs.primaryAction.url,
},
]
: []),
...(outputs?.secondaryAction
? [
{
type: ButtonTypeEnum.SECONDARY,
content: outputs.secondaryAction.label,
url: outputs.secondaryAction.url,
},
]
: []),
],
},
} satisfies IMessageCTA;

return {
subject: outputs?.subject,
content: outputs?.body || '',
cta,
avatar: outputs?.avatar,
} satisfies InAppMessage;
};
14 changes: 14 additions & 0 deletions packages/framework/src/schemas/steps/channels/in-app.schema.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
import { Schema } from '../../../types/schema.types';

const actionSchema = {
type: 'object',
properties: {
label: { type: 'string' },
url: { type: 'string' },
},
required: ['label'],
additionalProperties: false,
} as const satisfies Schema;

const inAppOutputSchema = {
type: 'object',
properties: {
subject: { type: 'string' },
body: { type: 'string' },
avatar: { type: 'string', format: 'uri' },
primaryAction: actionSchema,
secondaryAction: actionSchema,
},
required: ['body'],
additionalProperties: false,
Expand Down

0 comments on commit d98ee52

Please sign in to comment.