Skip to content

Commit

Permalink
Merge branch 'next' into add-env-enable-notify-topic
Browse files Browse the repository at this point in the history
  • Loading branch information
doanthai authored Dec 25, 2024
2 parents 600ec27 + 5428505 commit e129143
Show file tree
Hide file tree
Showing 52 changed files with 537 additions and 201 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { Injectable } from '@nestjs/common';
import { PreviewPayload, TipTapNode } from '@novu/shared';
import { z } from 'zod';
import { processNodeAttrs } from '@novu/application-generic';
import { HydrateEmailSchemaCommand } from './hydrate-email-schema.command';
import { PlaceholderAggregation } from '../../../workflows-v2/usecases';

Expand Down Expand Up @@ -87,6 +88,8 @@ export class HydrateEmailSchemaUseCase {
placeholderAggregation: PlaceholderAggregation
) {
content.forEach((node, index) => {
processNodeAttrs(node);

if (this.isVariableNode(node)) {
this.variableLogic(masterPayload, node, content, index, placeholderAggregation);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { emailControlZodSchema } from '../../../workflows-v2/shared/schemas/emai
export class RenderEmailOutputCommand extends RenderCommand {}

@Injectable()
// todo rename to EmailOutputRenderer
export class RenderEmailOutputUsecase {
constructor(private expandEmailEditorSchemaUseCase: ExpandEmailEditorSchemaUsecase) {}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { EnvironmentWithUserCommand } from '@novu/application-generic';
import { IsString, IsObject, IsNotEmpty, IsOptional } from 'class-validator';
import { IsString, IsObject, IsOptional } from 'class-validator';

export class BuildPayloadSchemaCommand extends EnvironmentWithUserCommand {
@IsString()
@IsNotEmpty()
workflowId: string;
@IsOptional()
workflowId?: string;

/**
* Control values used for preview purposes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class BuildPayloadSchema {
private async getControlValues(command: BuildPayloadSchemaCommand) {
let controlValues = command.controlValues ? [command.controlValues] : [];

if (!controlValues.length) {
if (!controlValues.length && command.workflowId) {
controlValues = (
await this.controlValuesRepository.find(
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,15 @@ export class BuildAvailableVariableSchemaUsecase {
workflow: NotificationTemplateEntity | undefined,
command: BuildAvailableVariableSchemaCommand
): Promise<JSONSchemaDto> {
if (!workflow) {
if (workflow && workflow.steps.length === 0) {
return {
type: 'object',
properties: {},
additionalProperties: true,
};
}

if (workflow.payloadSchema) {
if (workflow?.payloadSchema) {
return parsePayloadSchema(workflow.payloadSchema, { safe: true }) || emptyJsonSchema();
}

Expand All @@ -79,7 +79,7 @@ export class BuildAvailableVariableSchemaUsecase {
environmentId: command.environmentId,
organizationId: command.organizationId,
userId: command.userId,
workflowId: workflow._id,
workflowId: workflow?._id,
...(command.optimisticControlValues ? { controlValues: command.optimisticControlValues } : {}),
})
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { JSONContent } from '@maily-to/render';
import _ from 'lodash';
import { processNodeAttrs, MailyContentTypeEnum } from '@novu/application-generic';

/**
* Processes raw Maily JSON editor state by converting variables to Liquid.js output syntax
Expand Down Expand Up @@ -100,16 +101,12 @@ function processForLoopNode(node: JSONContent): JSONContent {
function processNode(node: JSONContent): JSONContent {
if (!node) return node;

const processedNode: JSONContent = { ...node };

if (processedNode.attrs) {
processedNode.attrs = processAttributes(processedNode.attrs);
}
const processedNode = processNodeAttrs(node);

switch (processedNode.type) {
case 'variable':
case MailyContentTypeEnum.VARIABLE:
return processVariableNode(processedNode);
case 'for':
case MailyContentTypeEnum.FOR:
return processForLoopNode(processedNode);
default:
if (Array.isArray(processedNode.content)) {
Expand All @@ -120,44 +117,6 @@ function processNode(node: JSONContent): JSONContent {
}
}

const LIQUID_WRAPPED_KEYS = ['showIfKey'] as const;
type LiquidWrappedKey = (typeof LIQUID_WRAPPED_KEYS)[number];

/**
* Processes node attributes by converting specific keys to Liquid.js syntax
* * Please update LIQUID_WRAPPED_KEYS if you want to wrap more attributes
* @example
* // Input
* {
* showIfKey: "user.isActive",
* title: "Hello",
* color: "blue"
* }
* // Output
* {
* showIfKey: "{{user.isActive}}",
* title: "Hello",
* color: "blue"
* }
*/
export function processAttributes(attrs: Record<string, unknown>): Record<string, unknown> {
return Object.entries(attrs).reduce(
(acc, [key, value]) => ({
...acc,
[key]: shouldWrapInLiquid(key) && isString(value) ? wrapInLiquidOutput(value) : value,
}),
{} as Record<string, unknown>
);
}

function shouldWrapInLiquid(key: string): key is LiquidWrappedKey {
return LIQUID_WRAPPED_KEYS.includes(key as LiquidWrappedKey);
}

function isString(value: unknown): value is string {
return typeof value === 'string';
}

function wrapInLiquidOutput(value: string): string {
return `{{${value}}}`;
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import {
WorkflowStatusEnum,
StepIssues,
ControlSchemas,
DigestUnitEnum,
} from '@novu/shared';
import {
CreateWorkflow as CreateWorkflowGeneric,
Expand Down
33 changes: 33 additions & 0 deletions apps/api/src/app/workflows-v2/workflow.controller.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,39 @@ describe('Workflow Controller E2E API Testing', () => {
await assertValuesInSteps(workflowCreated);
}
});

it('should generate a payload schema if only control values are provided during workflow creation', async () => {
const steps = [
{
...buildEmailStep(),
controlValues: {
body: 'Welcome {{payload.name}}',
subject: 'Hello {{payload.name}}',
},
},
];

const nameSuffix = `Test Workflow${new Date().toISOString()}`;

const createWorkflowDto: CreateWorkflowDto = buildCreateWorkflowDto(`${nameSuffix}`, { steps });
const res = await workflowsClient.createWorkflow(createWorkflowDto);
expect(res.isSuccessResult()).to.be.true;

const workflow = res.value as WorkflowResponseDto;
expect(workflow).to.be.ok;

expect(workflow.steps[0].variables).to.be.ok;

const stepData = await getStepData(workflow._id, workflow.steps[0]._id);
expect(stepData.variables).to.be.ok;

const { properties } = stepData.variables as JSONSchemaDto;
expect(properties).to.be.ok;

const payloadProperties = properties?.payload as JSONSchemaDto;
expect(payloadProperties).to.be.ok;
expect(payloadProperties.properties?.name).to.be.ok;
});
});

describe('Update Workflow Permutations', () => {
Expand Down
6 changes: 3 additions & 3 deletions apps/dashboard/src/components/auth/inbox-playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ const defaultFormValues: InboxPlaygroundFormData = {
label: 'Add to your app',
redirect: {
target: '_self',
url: '/',
url: '/onboarding/inbox/embed',
},
},
secondaryAction: null,
Expand Down Expand Up @@ -270,14 +270,14 @@ async function createDemoWorkflow({ environment }: { environment: IEnvironment }
label: '{{payload.primaryActionLabel}}',
redirect: {
target: '_self',
url: '',
url: '/onboarding/inbox/embed',
},
},
secondaryAction: {
label: '{{payload.secondaryActionLabel}}',
redirect: {
target: '_self',
url: '',
url: '/onboarding/inbox/embed',
},
},
},
Expand Down
11 changes: 10 additions & 1 deletion apps/dashboard/src/components/auth/inbox-preview-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useFetchEnvironments } from '../../context/environment/hooks';
import { useUser } from '@clerk/clerk-react';
import { useAuth } from '../../context/auth/hooks';
import { API_HOSTNAME, WEBSOCKET_HOSTNAME } from '../../config';
import { useNavigate } from 'react-router-dom';

interface InboxPreviewContentProps {
selectedStyle: string;
Expand All @@ -19,6 +20,7 @@ export function InboxPreviewContent({
primaryColor,
foregroundColor,
}: InboxPreviewContentProps) {
const navigate = useNavigate();
const auth = useAuth();
const { user } = useUser();
const { environments } = useFetchEnvironments({ organizationId: auth?.currentOrganization?._id });
Expand Down Expand Up @@ -66,7 +68,14 @@ export function InboxPreviewContent({
{selectedStyle === 'popover' && (
<div className="relative flex h-full w-full flex-col items-center">
<div className="mt-10 flex w-full max-w-[440px] items-center justify-end">
<Inbox {...configuration} placement="bottom-end" open />
<Inbox
{...configuration}
routerPush={(path: string) => {
return navigate(path);
}}
placement="bottom-end"
open
/>
</div>
<div className="absolute bottom-[-10px] left-2 flex flex-col items-start">
<SendNotificationArrow className="mt-2 h-[73px] w-[86px]" />
Expand Down
14 changes: 5 additions & 9 deletions apps/dashboard/src/components/create-workflow-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { ComponentProps, useState } from 'react';
import { useForm } from 'react-hook-form';
import { RiExternalLinkLine } from 'react-icons/ri';
import { Link, useNavigate } from 'react-router-dom';
import { RiArrowRightSLine } from 'react-icons/ri';
import { ExternalLink } from '@/components/shared/external-link';
import { useNavigate } from 'react-router-dom';
import { z } from 'zod';
import { type CreateWorkflowDto, WorkflowCreationSourceEnum, slugify } from '@novu/shared';
import { createWorkflow } from '@/api/workflows';
Expand Down Expand Up @@ -73,13 +74,7 @@ export const CreateWorkflowButton = (props: CreateWorkflowButtonProps) => {
<div>
<SheetDescription>
Define the steps to notify subscribers using channels like in-app, email, and more.{' '}
<Link
target="_blank"
to="https://docs.novu.co/concepts/workflows"
className="text-foreground-400 inline-flex items-center text-xs underline"
>
Learn more <RiExternalLinkLine className="inline size-4" />
</Link>
<ExternalLink href="https://docs.novu.co/concepts/workflows">Learn more</ExternalLink>
</SheetDescription>
</div>
</SheetHeader>
Expand Down Expand Up @@ -195,6 +190,7 @@ export const CreateWorkflowButton = (props: CreateWorkflowButtonProps) => {
<SheetFooter>
<Button isLoading={isPending} variant="default" type="submit" form="create-workflow">
Create workflow
<RiArrowRightSLine className="size-4" />
</Button>
</SheetFooter>
</SheetContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const EditBridgeUrlButton = () => {
className={cn(
'relative size-1.5 animate-[pulse-shadow_1s_ease-in-out_infinite] rounded-full',
status === ConnectionStatus.DISCONNECTED || status === ConnectionStatus.LOADING
? 'bg-destructive [--pulse-color:var(--destructive)]'
? 'bg-destructive'
: 'bg-success [--pulse-color:var(--success)]'
)}
/>
Expand Down
8 changes: 4 additions & 4 deletions apps/dashboard/src/components/primitives/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { cn } from '@/utils/ui';
import { RiLoader4Line } from 'react-icons/ri';

export const buttonVariants = cva(
`relative isolate inline-flex items-center justify-center whitespace-nowrap rounded-lg gap-1 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50`,
`relative isolate inline-flex items-center justify-center whitespace-nowrap rounded-lg gap-1 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50`,
{
variants: {
variant: {
Expand All @@ -26,10 +26,10 @@ export const buttonVariants = cva(
},
size: {
default: 'h-9 p-2.5',
xs: 'h-6 px-1.5 rounded-md text-xs',
sm: 'h-8 px-1.5 rounded-md text-xs',
xs: 'h-6 px-2 rounded-md',
sm: 'h-8 px-2 rounded-md',
lg: 'h-10 rounded-md px-8',
'input-right': 'rounded-none border-b-0 h-full text-xs border-r-0 border-t-0 px-2 py-0',
'input-right': 'rounded-none border-b-0 h-full border-r-0 border-t-0 px-2 py-0',
icon: 'size-8',
},
},
Expand Down
12 changes: 7 additions & 5 deletions apps/dashboard/src/components/shared/external-link.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RiBookMarkedLine, RiExternalLinkLine, RiQuestionLine } from 'react-icons/ri';
import { RiBookMarkedLine, RiArrowRightUpLine, RiQuestionLine } from 'react-icons/ri';
import { cn } from '@/utils/ui';
import { useTelemetry } from '@/hooks/use-telemetry';
import { TelemetryEvent } from '@/utils/telemetry';
Expand Down Expand Up @@ -26,19 +26,21 @@ export function ExternalLink({
});
};

const finalIconClassName = cn('inline size-3 mb-1', iconClassName);

return (
<a
target="_blank"
rel="noopener noreferrer"
className={cn('text-foreground-600 inline-flex items-center gap-1 hover:underline', className)}
className={cn('text-foreground-400 inline-flex items-center text-xs underline', className)}
href={href}
onClick={handleClick}
{...props}
>
{variant === 'documentation' && <RiBookMarkedLine className={cn('size-4', iconClassName)} aria-hidden="true" />}
{variant === 'default' && <RiExternalLinkLine className={cn('size-4', iconClassName)} aria-hidden="true" />}
{variant === 'tip' && <RiQuestionLine className={cn('size-4', iconClassName)} aria-hidden="true" />}
{children}
{variant === 'documentation' && <RiBookMarkedLine className={finalIconClassName} aria-hidden="true" />}
{variant === 'default' && <RiArrowRightUpLine className={finalIconClassName} aria-hidden="true" />}
{variant === 'tip' && <RiQuestionLine className={finalIconClassName} aria-hidden="true" />}
</a>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const CardContent = ({
</Tooltip>
</div>
<span className="text-foreground-600 text-xs">
Experience Novu without any limits for free for the next {pluralizedDays}.
Enjoy unlimited access to Novu for free for the next {pluralizedDays}.
</span>
<div className={`max-h-3 overflow-hidden opacity-100 ${transition} group-hover:max-h-0 group-hover:opacity-0`}>
<Progress value={daysTotal - daysLeft} max={daysTotal} />
Expand Down
19 changes: 11 additions & 8 deletions apps/dashboard/src/components/user-profile.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { UserButton } from '@clerk/clerk-react';
import { UserButton, useOrganization } from '@clerk/clerk-react';
import { useNewDashboardOptIn } from '@/hooks/use-new-dashboard-opt-in';
import { RiSignpostFill } from 'react-icons/ri';
import { ROUTES } from '../utils/routes';

export function UserProfile() {
const { organization } = useOrganization();
const { optOut } = useNewDashboardOptIn();

return (
Expand All @@ -16,13 +17,15 @@ export function UserProfile() {
},
}}
>
<UserButton.MenuItems>
<UserButton.Action
label="Go back to the legacy Dashboard"
labelIcon={<RiSignpostFill size="16" color="var(--nv-colors-typography-text-main)" />}
onClick={optOut}
/>
</UserButton.MenuItems>
{organization && organization.createdAt < new Date('2024-12-24') && (
<UserButton.MenuItems>
<UserButton.Action
label="Go back to the legacy Dashboard"
labelIcon={<RiSignpostFill size="16" color="var(--nv-colors-typography-text-main)" />}
onClick={optOut}
/>
</UserButton.MenuItems>
)}
</UserButton>
);
}
Loading

0 comments on commit e129143

Please sign in to comment.