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(wip): add custom step #5323

Merged
merged 11 commits into from May 5, 2024
2 changes: 1 addition & 1 deletion .idea/.name

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .source
29 changes: 29 additions & 0 deletions apps/api/e2e/echo.server.ts
@@ -0,0 +1,29 @@
import * as http from 'http';
import * as express from 'express';
import { serve } from '@novu/echo/express';
import { Echo } from '@novu/echo';

class EchoServer {
private server: express.Express;
private app: http.Server;
private port = 9999;
public echo = new Echo({ devModeBypassAuthentication: true });

get serverPath() {
return `http://localhost:${this.port}`;
}

async start() {
this.server = express();
this.server.use(express.json());
this.server.use(serve({ client: this.echo }));

this.app = await this.server.listen(this.port);
}

async stop() {
await this.app.close();
}
}

export const echoServer = new EchoServer();
4 changes: 4 additions & 0 deletions apps/api/e2e/setup.ts
Expand Up @@ -4,6 +4,7 @@ import * as sinon from 'sinon';
import * as chai from 'chai';

import { bootstrap } from '../src/bootstrap';
import { echoServer } from './echo.server';

const dalService = new DalService();

Expand All @@ -13,11 +14,14 @@ before(async () => {
*/
chai.config.truncateThreshold = 0;
await testServer.create(await bootstrap());
await echoServer.start();

await dalService.connect(process.env.MONGO_URL);
});

after(async () => {
await testServer.teardown();
await echoServer.stop();

try {
await dalService.destroy();
Expand Down
4 changes: 3 additions & 1 deletion apps/api/package.json
Expand Up @@ -47,6 +47,7 @@
"@novu/shared": "^0.24.1",
"@novu/stateless": "^0.24.1",
"@novu/testing": "^0.24.1",
"@novu/echo": "^0.0.1-alpha.25",
"@sendgrid/mail": "^8.1.0",
"@sentry/hub": "^7.40.0",
"@sentry/node": "^7.40.0",
Expand Down Expand Up @@ -114,7 +115,8 @@
"ts-loader": "~9.4.0",
"ts-node": "~10.9.1",
"tsconfig-paths": "~4.1.0",
"typescript": "4.9.5"
"typescript": "4.9.5",
"express": "^4.17.1"
},
"optionalDependencies": {
"@novu/ee-echo-api": "^0.24.1",
Expand Down
102 changes: 102 additions & 0 deletions apps/api/src/app/events/e2e/echo-trigger.e2e-ee.ts
@@ -0,0 +1,102 @@
import axios from 'axios';
import { expect } from 'chai';
import { UserSession, SubscribersService } from '@novu/testing';
import { MessageRepository, SubscriberEntity, NotificationTemplateRepository } from '@novu/dal';
import { StepTypeEnum } from '@novu/shared';
import { echoServer } from '../../../../e2e/echo.server';

const eventTriggerPath = '/v1/events/trigger';

describe('Echo Trigger ', async () => {
let session: UserSession;
const messageRepository = new MessageRepository();
const workflowsRepository = new NotificationTemplateRepository();
let subscriber: SubscriberEntity;
let subscriberService: SubscribersService;

beforeEach(async () => {
session = new UserSession();
await session.initialize();
subscriberService = new SubscribersService(session.organization._id, session.environment._id);
subscriber = await subscriberService.createSubscriber();
});

it('should trigger the echo workflow', async () => {
const workflowId = 'hello-world';
await echoServer.echo.workflow(
workflowId,
async ({ step, payload }) => {
await step.email(
'send-email',
async (inputs) => {
return {
subject: 'This is an email subject ' + inputs.name,
body: 'Body result ' + payload.name,
};
},
{
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', default: 'TEST' },
},
} as const,
}
);
},
{
payloadSchema: {
type: 'object',
properties: {
name: { type: 'string', default: 'default_name' },
},
required: [],
additionalProperties: false,
} as const,
}
);

const resultDiscover = await axios.get(echoServer.serverPath + '/echo?action=discover');

await session.testAgent.post(`/v1/echo/sync`).send({
chimeraUrl: echoServer.serverPath + '/echo',
workflows: resultDiscover.data.workflows,
});

const workflow = await workflowsRepository.findByTriggerIdentifier(session.environment._id, workflowId);
expect(workflow).to.be.ok;

if (!workflow) {
throw new Error('Workflow not found');
}

await axios.post(
`${session.serverUrl}${eventTriggerPath}`,
{
name: workflowId,
to: {
subscriberId: subscriber.subscriberId,
email: '[email protected]',
},
payload: {
name: 'test_name',
},
},
{
headers: {
authorization: `ApiKey ${session.apiKey}`,
},
}
);
await session.awaitRunningJobs(workflow._id);

const messagesAfter = await messageRepository.find({
_environmentId: session.environment._id,
_subscriberId: subscriber._id,
channel: StepTypeEnum.EMAIL,
});

expect(messagesAfter.length).to.be.eq(1);
expect(messagesAfter[0].subject).to.include('This is an email subject TEST');
});
});
Expand Up @@ -116,7 +116,10 @@ export class GetActiveIntegrationsStatus {
for (const step of uniqueSteps) {
const stepType = step.template?.type;
const skipStep =
stepType === StepTypeEnum.DELAY || stepType === StepTypeEnum.DIGEST || stepType === StepTypeEnum.TRIGGER;
stepType === StepTypeEnum.DELAY ||
stepType === StepTypeEnum.DIGEST ||
stepType === StepTypeEnum.TRIGGER ||
stepType === StepTypeEnum.CUSTOM;
Comment on lines +119 to +122
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create ActionStepTypeEnum and skip by it

Suggested change
stepType === StepTypeEnum.DELAY ||
stepType === StepTypeEnum.DIGEST ||
stepType === StepTypeEnum.TRIGGER ||
stepType === StepTypeEnum.CUSTOM;
const skipStep = ActionStepTypeEnum.values.includes((action) => action === stepType)

const isStepWithPrimaryIntegration = stepType === StepTypeEnum.EMAIL || stepType === StepTypeEnum.SMS;
if (stepType && !skipStep) {
const hasActiveIntegrations = activeChannelsStatus[stepType].hasActiveIntegrations;
Expand Down
Expand Up @@ -5,6 +5,7 @@ import { ChatPreview, EmailPreview, InAppPreview, PushPreview, SmsPreview } from
import { useNavigateFromEditor } from '../hooks/useNavigateFromEditor';
import { useStepIndex } from '../hooks/useStepIndex';
import { ChannelPreviewSidebar } from './ChannelPreviewSidebar';
import { TemplateCustomEditor } from './custom-editor/TemplateCustomEditor';

export const PreviewComponent = ({ channel }: { channel: StepTypeEnum }) => {
switch (channel) {
Expand Down Expand Up @@ -32,8 +33,11 @@ export const PreviewComponent = ({ channel }: { channel: StepTypeEnum }) => {
case StepTypeEnum.DIGEST:
return <>DIGEST</>;

case StepTypeEnum.CUSTOM:
return <TemplateCustomEditor />;

default:
return <>dummy</>;
return <>Unknown Step</>;
}
};

Expand Down
21 changes: 20 additions & 1 deletion apps/web/src/pages/templates/components/ChannelStepEditor.tsx
Expand Up @@ -11,12 +11,15 @@ import { DigestMetadata } from '../workflow/DigestMetadata';
import { DelayMetadata } from '../workflow/DelayMetadata';
import { useStepIndex } from '../hooks/useStepIndex';
import { useNavigateFromEditor } from '../hooks/useNavigateFromEditor';
import { TemplateCustomEditor } from './custom-editor/TemplateCustomEditor';
import { useTemplateEditorForm } from './TemplateEditorFormProvider';

export const ChannelStepEditor = () => {
const { channel } = useParams<{
channel: StepTypeEnum | undefined;
}>();
const { stepIndex } = useStepIndex();
const { stepIndex, step } = useStepIndex();
const { template } = useTemplateEditorForm();

useNavigateFromEditor();

Expand All @@ -40,6 +43,22 @@ export const ChannelStepEditor = () => {
);
}

if (channel === StepTypeEnum.CUSTOM) {
return (
<StepEditorSidebar>
<TemplateCustomEditor />
</StepEditorSidebar>
);
}

if (template?.type === 'ECHO' && (channel === StepTypeEnum.DIGEST || channel === StepTypeEnum.DELAY)) {
return (
<StepEditorSidebar>
<TemplateCustomEditor />
</StepEditorSidebar>
);
}

return (
<>
<StepEditorSidebar>
Expand Down
@@ -0,0 +1,37 @@
import { Controller, useFormContext } from 'react-hook-form';
import { StepSettings } from '../../workflow/SideBar/StepSettings';
import { useStepFormPath } from '../../hooks/useStepFormPath';
import { useState } from 'react';
import { Grid, Stack } from '@mantine/core';
import { InputVariablesForm } from '../InputVariablesForm';

export function TemplateCustomEditor() {
const stepFormPath = useStepFormPath();
const { control } = useFormContext();

const [inputVariables, setInputVariables] = useState();

return (
<>
<StepSettings />
<Grid gutter={24}>
<Grid.Col span={'auto'}>
<Controller
name={`${stepFormPath}.template.content` as any}
defaultValue=""
control={control}
render={({ field }) => (
<Stack spacing={8}>
<InputVariablesForm
onChange={(values) => {
setInputVariables(values);
}}
/>
</Stack>
)}
/>
</Grid.Col>
</Grid>
</>
);
}
2 changes: 1 addition & 1 deletion apps/web/src/pages/templates/constants.tsx
Expand Up @@ -47,7 +47,7 @@ export const ordinalNumbers = {
10: 'tenth',
};

export const stepNames: Record<StepTypeEnum | ChannelTypeEnum, string> = {
export const stepNames: Record<StepTypeEnum, string> = {
email: 'Email',
chat: 'Chat',
in_app: 'In-App',
Expand Down
Expand Up @@ -5,6 +5,7 @@ import { colors, DragButton, Tooltip } from '@novu/design-system';
import { useEnvController } from '../../../../hooks';
import { channels, NodeTypeEnum } from '../../../../utils/channels';
import { TOP_ROW_HEIGHT } from '../WorkflowEditor';
import { StepTypeEnum } from '@novu/shared';

export function AddStepMenu({
setDragging,
Expand All @@ -26,6 +27,7 @@ export function AddStepMenu({
<Stack spacing={18} mb={16}>
{channels
.filter((channel) => channel.type === NodeTypeEnum.ACTION)
.filter((channel) => channel.channelType !== StepTypeEnum.CUSTOM)
.map((channel, index) => (
<DraggableNode key={index} channel={channel} setDragging={setDragging} onDragStart={onDragStart} />
))}
Expand Down
22 changes: 16 additions & 6 deletions apps/web/src/utils/channels.ts
Expand Up @@ -8,6 +8,7 @@ import {
InAppFilled,
PushFilled,
SmsFilled,
Bolt,
} from '@novu/design-system';

export enum NodeTypeEnum {
Expand All @@ -16,7 +17,7 @@ export enum NodeTypeEnum {
}

interface IChannelDefinition {
tabKey: StepTypeEnum | ChannelTypeEnum;
tabKey: StepTypeEnum;
label: string;
description: string;
Icon: React.FC<any>;
Expand All @@ -35,7 +36,7 @@ export const CHANNEL_TYPE_TO_STRING: Record<ChannelTypeEnum, string> = {

export const channels: IChannelDefinition[] = [
{
tabKey: ChannelTypeEnum.IN_APP,
tabKey: StepTypeEnum.IN_APP,
label: 'In-App',
description: 'Send notifications to the in-app notification center',
Icon: InAppFilled,
Expand All @@ -44,7 +45,7 @@ export const channels: IChannelDefinition[] = [
type: NodeTypeEnum.CHANNEL,
},
{
tabKey: ChannelTypeEnum.EMAIL,
tabKey: StepTypeEnum.EMAIL,
label: 'Email',
description: 'Send using one of our email integrations',
Icon: EmailFilled,
Expand All @@ -53,7 +54,7 @@ export const channels: IChannelDefinition[] = [
type: NodeTypeEnum.CHANNEL,
},
{
tabKey: ChannelTypeEnum.SMS,
tabKey: StepTypeEnum.SMS,
label: 'SMS',
description: "Send an SMS directly to the user's phone",
Icon: SmsFilled,
Expand All @@ -80,7 +81,16 @@ export const channels: IChannelDefinition[] = [
type: NodeTypeEnum.ACTION,
},
{
tabKey: ChannelTypeEnum.CHAT,
tabKey: StepTypeEnum.CUSTOM,
label: 'Custom',
description: 'Run custom code',
Icon: Bolt,
testId: 'customSelector',
channelType: StepTypeEnum.CUSTOM,
type: NodeTypeEnum.ACTION,
},
{
tabKey: StepTypeEnum.CHAT,
label: 'Chat',
description: 'Send a chat message',
Icon: ChatFilled,
Expand All @@ -89,7 +99,7 @@ export const channels: IChannelDefinition[] = [
type: NodeTypeEnum.CHANNEL,
},
{
tabKey: ChannelTypeEnum.PUSH,
tabKey: StepTypeEnum.PUSH,
label: 'Push',
description: "Send an Push Notification to a user's device",
Icon: PushFilled,
Expand Down