[go: up one dir, main page]

Skip to content

Commit

Permalink
Merge branch 'next' into nv-4595-common-preference-use-cases-for-subs…
Browse files Browse the repository at this point in the history
…criber-api-inbox-api
  • Loading branch information
rifont authored Nov 12, 2024
2 parents dcc98e1 + 99c0906 commit b96fc0f
Show file tree
Hide file tree
Showing 9 changed files with 250 additions and 61 deletions.
76 changes: 41 additions & 35 deletions apps/dashboard/src/components/workflow-editor/nodes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,17 +167,19 @@ export const DelayNode = (props: NodeProps<NodeType>) => {
const Icon = STEP_TYPE_TO_ICON[StepTypeEnum.DELAY];

return (
<StepNode data={data}>
<NodeHeader type={StepTypeEnum.DELAY}>
<NodeIcon variant={STEP_TYPE_TO_COLOR[StepTypeEnum.DELAY]}>
<Icon />
</NodeIcon>
<NodeName>{data.name || 'Delay Step'}</NodeName>
</NodeHeader>
<NodeBody>{data.content || 'You have been invited to the Novu party on "commentSnippet"'}</NodeBody>
<Handle isConnectable={false} className={handleClassName} type="target" position={Position.Top} id="a" />
<Handle isConnectable={false} className={handleClassName} type="source" position={Position.Bottom} id="b" />
</StepNode>
<Link to={buildRoute(ROUTES.CONFIGURE_STEP, { stepSlug: data.stepSlug ?? '' })}>
<StepNode data={data}>
<NodeHeader type={StepTypeEnum.DELAY}>
<NodeIcon variant={STEP_TYPE_TO_COLOR[StepTypeEnum.DELAY]}>
<Icon />
</NodeIcon>
<NodeName>{data.name || 'Delay Step'}</NodeName>
</NodeHeader>
<NodeBody>{data.content || 'You have been invited to the Novu party on "commentSnippet"'}</NodeBody>
<Handle isConnectable={false} className={handleClassName} type="target" position={Position.Top} id="a" />
<Handle isConnectable={false} className={handleClassName} type="source" position={Position.Bottom} id="b" />
</StepNode>
</Link>
);
};

Expand All @@ -186,19 +188,21 @@ export const DigestNode = (props: NodeProps<NodeType>) => {
const Icon = STEP_TYPE_TO_ICON[StepTypeEnum.DIGEST];

return (
<StepNode data={data}>
<NodeHeader type={StepTypeEnum.DIGEST}>
<NodeIcon variant={STEP_TYPE_TO_COLOR[StepTypeEnum.DIGEST]}>
<Icon />
</NodeIcon>
<NodeName>{data.name || 'Digest Step'}</NodeName>
</NodeHeader>
<NodeBody>
{data.content || 'Batches events into one coherent message before delivery to the subscriber.'}
</NodeBody>
<Handle isConnectable={false} className={handleClassName} type="target" position={Position.Top} id="a" />
<Handle isConnectable={false} className={handleClassName} type="source" position={Position.Bottom} id="b" />
</StepNode>
<Link to={buildRoute(ROUTES.CONFIGURE_STEP, { stepSlug: data.stepSlug ?? '' })}>
<StepNode data={data}>
<NodeHeader type={StepTypeEnum.DIGEST}>
<NodeIcon variant={STEP_TYPE_TO_COLOR[StepTypeEnum.DIGEST]}>
<Icon />
</NodeIcon>
<NodeName>{data.name || 'Digest Step'}</NodeName>
</NodeHeader>
<NodeBody>
{data.content || 'Batches events into one coherent message before delivery to the subscriber.'}
</NodeBody>
<Handle isConnectable={false} className={handleClassName} type="target" position={Position.Top} id="a" />
<Handle isConnectable={false} className={handleClassName} type="source" position={Position.Bottom} id="b" />
</StepNode>
</Link>
);
};

Expand All @@ -207,17 +211,19 @@ export const CustomNode = (props: NodeProps<NodeType>) => {
const Icon = STEP_TYPE_TO_ICON[StepTypeEnum.CUSTOM];

return (
<StepNode data={data}>
<NodeHeader type={StepTypeEnum.CUSTOM}>
<NodeIcon variant={STEP_TYPE_TO_COLOR[StepTypeEnum.CUSTOM]}>
<Icon />
</NodeIcon>
<NodeName>{data.name || 'Custom Step'}</NodeName>
</NodeHeader>
<NodeBody>Executes the business logic in your bridge application</NodeBody>
<Handle isConnectable={false} className={handleClassName} type="target" position={Position.Top} id="a" />
<Handle isConnectable={false} className={handleClassName} type="source" position={Position.Bottom} id="b" />
</StepNode>
<Link to={buildRoute(ROUTES.CONFIGURE_STEP, { stepSlug: data.stepSlug ?? '' })}>
<StepNode data={data}>
<NodeHeader type={StepTypeEnum.CUSTOM}>
<NodeIcon variant={STEP_TYPE_TO_COLOR[StepTypeEnum.CUSTOM]}>
<Icon />
</NodeIcon>
<NodeName>{data.name || 'Custom Step'}</NodeName>
</NodeHeader>
<NodeBody>Executes the business logic in your bridge application</NodeBody>
<Handle isConnectable={false} className={handleClassName} type="target" position={Position.Top} id="a" />
<Handle isConnectable={false} className={handleClassName} type="source" position={Position.Bottom} id="b" />
</StepNode>
</Link>
);
};

Expand Down
17 changes: 0 additions & 17 deletions apps/dashboard/src/components/workflow-editor/steps/chat.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* This component is used as a placeholder for the other step configuration until the actual configuration is implemented.
*/
import { Separator } from '@/components/primitives/separator';
import { CommonFields } from './common-fields';
import { SdkBanner } from './sdk-banner';
import { Link } from 'react-router-dom';
import { Button } from '@/components/primitives/button';
import { RiArrowRightSLine, RiPencilRuler2Fill } from 'react-icons/ri';

export default function ConfigureOtherSteps({ channelName }: { channelName?: string }) {
return (
<>
<div className="flex flex-col gap-4 p-3">
<CommonFields />
</div>
<Separator />

{channelName && (
<>
<div className="flex flex-col gap-4 p-3">
<Link to={'./edit'} relative="path">
<Button variant="outline" className="flex w-full justify-start gap-1.5 text-xs font-medium" type="button">
<RiPencilRuler2Fill className="h-4 w-4 text-neutral-600" />
Configure {channelName} template <RiArrowRightSLine className="ml-auto h-4 w-4 text-neutral-600" />
</Button>
</Link>
</div>
<Separator />
</>
)}

<div className="px-3 py-4">
<SdkBanner />
</div>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { buildRoute, ROUTES } from '@/utils/routes';
import { motion } from 'framer-motion';
import { ConfigureInApp } from './in-app/configure-in-app';
import { useStep } from './use-step';
import Chat from './chat';
import ConfigureOtherSteps from './configure-other-steps';
import { useState } from 'react';
import { ConfirmationModal } from '@/components/confirmation-modal';

Expand Down Expand Up @@ -104,11 +104,23 @@ const Step = ({ stepType }: { stepType?: StepTypeEnum }) => {
case StepTypeEnum.IN_APP:
return <ConfigureInApp />;

case StepTypeEnum.EMAIL:
return <ConfigureOtherSteps channelName="email" />;

case StepTypeEnum.SMS:
return <ConfigureOtherSteps channelName="sms" />;

case StepTypeEnum.PUSH:
return <ConfigureOtherSteps channelName="push" />;

case StepTypeEnum.CHAT:
return <ConfigureOtherSteps channelName="chat" />;

/**
* TODO: Add other step types here
* For now, it is just a placeholder with the use sdk banner
*/
default:
return <Chat />;
return <ConfigureOtherSteps />;
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,18 @@ import { RiArrowDownSLine, RiArrowUpSLine, RiInputField } from 'react-icons/ri';
import { type ControlsMetadata } from '@novu/shared';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/primitives/collapsible';
import { JsonForm } from './json-form';
import { WorkflowOriginEnum } from '@/utils/enums';

export function CustomStepControls({ dataSchema }: { dataSchema: ControlsMetadata['dataSchema'] }) {
export function CustomStepControls({
dataSchema,
origin,
}: {
dataSchema: ControlsMetadata['dataSchema'];
origin: WorkflowOriginEnum;
}) {
const [isEditorOpen, setIsEditorOpen] = useState(true);

if (!dataSchema) {
if (!dataSchema || origin !== WorkflowOriginEnum.EXTERNAL) {
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const InAppBody = () => {
]}
value={field.value}
onChange={(val) => field.onChange(val)}
height="100%"
/>
</InputField>
</FormControl>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export const InAppTabs = ({ workflow, step }: { workflow: WorkflowResponseDto; s
<Separator />
<TabsContent value="editor" className={tabsContentClassName}>
<InAppEditor uiSchema={uiSchema} />
<CustomStepControls dataSchema={dataSchema} />
<CustomStepControls dataSchema={dataSchema} origin={workflow.origin} />
</TabsContent>
<TabsContent value="preview" className={tabsContentClassName}>
<InAppEditorPreview
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/**
* This component is used as a placeholder for the other step configuration until the actual configuration is implemented.
*/
import { zodResolver } from '@hookform/resolvers/zod';
import { type StepDataDto, type WorkflowResponseDto } from '@novu/shared';
import { Cross2Icon } from '@radix-ui/react-icons';
import { useForm } from 'react-hook-form';
import { RiEdit2Line, RiPencilRuler2Line } from 'react-icons/ri';
import { useNavigate, useParams } from 'react-router-dom';

import { Notification5Fill } from '@/components/icons';
import { Button } from '@/components/primitives/button';
import { Form } from '@/components/primitives/form/form';
import { Separator } from '@/components/primitives/separator';
import { ToastIcon } from '@/components/primitives/sonner';
import { showToast } from '@/components/primitives/sonner-helpers';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/primitives/tabs';
import { useUpdateWorkflow } from '@/hooks/use-update-workflow';
import { buildDefaultValues, buildDynamicZodSchema } from '@/utils/schema';
import { CustomStepControls } from './controls/custom-step-controls';

const tabsContentClassName = 'h-full w-full px-3 py-3.5';

export const OtherStepTabs = ({ workflow, step }: { workflow: WorkflowResponseDto; step: StepDataDto }) => {
const { stepSlug = '' } = useParams<{ workflowSlug: string; stepSlug: string }>();
const { dataSchema, uiSchema } = step.controls;
const navigate = useNavigate();
const schema = buildDynamicZodSchema(dataSchema ?? {});
const form = useForm({
mode: 'onSubmit',
resolver: zodResolver(schema),
resetOptions: { keepDirtyValues: true },
defaultValues: buildDefaultValues(uiSchema ?? {}),
values: step.controls.values,
});

const { reset, formState } = form;

const { updateWorkflow } = useUpdateWorkflow({
onSuccess: () => {
showToast({
children: () => (
<>
<ToastIcon variant="success" />
<span className="text-sm">Saved</span>
</>
),
options: {
position: 'bottom-right',
classNames: {
toast: 'ml-10 mb-4',
},
},
});
},
onError: () => {
showToast({
children: () => (
<>
<ToastIcon variant="error" />
<span className="text-sm">Failed to save</span>
</>
),
options: {
position: 'bottom-right',
classNames: {
toast: 'ml-10 mb-4',
},
},
});
},
});

const onSubmit = async (data: any) => {
await updateWorkflow({
id: workflow._id,
workflow: {
...workflow,
steps: workflow.steps.map((step) => (step.slug === stepSlug ? { ...step, controlValues: { ...data } } : step)),
},
});
reset({ ...data });
};

return (
<Form {...form}>
<form
id="save-step"
className="flex h-full flex-col"
onSubmit={(event) => {
event.preventDefault();
event.stopPropagation();
form.handleSubmit(onSubmit)(event);
}}
>
<Tabs defaultValue="editor" className="flex h-full flex-1 flex-col">
<header className="flex flex-row items-center gap-3 px-3 py-1.5">
<div className="mr-auto flex items-center gap-2.5 text-sm font-medium">
<RiEdit2Line className="size-4" />
<span>Configure Template</span>
</div>
<TabsList className="w-min">
<TabsTrigger value="editor" className="gap-1.5">
<RiPencilRuler2Line className="size-5 p-0.5" />
<span>Editor</span>
</TabsTrigger>
<TabsTrigger value="preview" className="gap-1.5" disabled>
<Notification5Fill className="size-5 p-0.5" />
<span>Preview</span>
</TabsTrigger>
</TabsList>

<Button
variant="ghost"
size="xs"
className="size-6"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
navigate('../', { relative: 'path' });
}}
>
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</Button>
</header>
<Separator />
<TabsContent value="editor" className={tabsContentClassName}>
<CustomStepControls dataSchema={dataSchema} origin={workflow.origin} />
</TabsContent>
<Separator />
<footer className="flex justify-end px-3 py-3.5">
<Button className="ml-auto" variant="default" type="submit" form="save-step" disabled={!formState.isDirty}>
Save step
</Button>
</footer>
</Tabs>
</form>
</Form>
);
};
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { type StepDataDto, StepTypeEnum, type WorkflowResponseDto } from '@novu/shared';
import { InAppTabs } from '@/components/workflow-editor/steps/in-app/in-app-tabs';
import { OtherStepTabs } from './other-steps-tabs';

const STEP_TYPE_TO_EDITOR: Record<
StepTypeEnum,
(args: { workflow: WorkflowResponseDto; step: StepDataDto }) => React.JSX.Element
> = {
[StepTypeEnum.EMAIL]: () => <div>EMAIL Editor</div>,
[StepTypeEnum.CHAT]: () => <div>CHAT Editor</div>,
[StepTypeEnum.EMAIL]: OtherStepTabs,
[StepTypeEnum.CHAT]: OtherStepTabs,
[StepTypeEnum.IN_APP]: InAppTabs,
[StepTypeEnum.SMS]: () => <div>SMS Editor</div>,
[StepTypeEnum.PUSH]: () => <div>PUSH Editor</div>,
[StepTypeEnum.SMS]: OtherStepTabs,
[StepTypeEnum.PUSH]: OtherStepTabs,
[StepTypeEnum.DIGEST]: () => <div>DIGEST Editor</div>,
[StepTypeEnum.DELAY]: () => <div>DELAY Editor</div>,
[StepTypeEnum.TRIGGER]: () => <div>TRIGGER Editor</div>,
Expand Down

0 comments on commit b96fc0f

Please sign in to comment.