Skip to content

Commit

Permalink
feat(dashboard): Implement email step editor & mini preview (#7129)
Browse files Browse the repository at this point in the history
  • Loading branch information
desiprisg authored Dec 5, 2024
1 parent 5680bd6 commit 2bc56b1
Show file tree
Hide file tree
Showing 20 changed files with 1,429 additions and 255 deletions.
4 changes: 3 additions & 1 deletion apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@codemirror/autocomplete": "^6.18.3",
"@hookform/resolvers": "^3.9.0",
"@lezer/highlight": "^1.2.1",
"@maily-to/core": "^0.0.16",
"@novu/framework": "workspace:*",
"@novu/js": "workspace:*",
"@novu/react": "workspace:*",
Expand Down Expand Up @@ -65,13 +66,13 @@
"cmdk": "1.0.0",
"date-fns": "^4.1.0",
"flat": "^6.0.1",
"motion": "^11.12.0",
"js-cookie": "^3.0.5",
"launchdarkly-react-client-sdk": "^3.3.2",
"lodash.debounce": "^4.0.8",
"lodash.merge": "^4.6.2",
"lucide-react": "^0.439.0",
"mixpanel-browser": "^2.52.0",
"motion": "^11.12.0",
"next-themes": "^0.3.0",
"react": "^18.3.1",
"react-colorful": "^5.6.1",
Expand All @@ -94,6 +95,7 @@
"@hookform/devtools": "^4.3.0",
"@playwright/test": "^1.44.0",
"@sentry/vite-plugin": "^2.22.6",
"@tiptap/core": "^2.10.3",
"@types/lodash.debounce": "^4.0.9",
"@types/lodash.merge": "^4.6.6",
"@types/mixpanel-browser": "^2.49.0",
Expand Down
1 change: 1 addition & 0 deletions apps/dashboard/src/components/primitives/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const editorVariants = cva('h-full w-full flex-1 [&_.cm-focused]:outline-none',
variants: {
size: {
default: 'text-xs [&_.cm-editor]:py-1',
lg: 'text-base [&_.cm-editor]:py-1',
},
},
defaultVariants: {
Expand Down
16 changes: 15 additions & 1 deletion apps/dashboard/src/components/workflow-editor/add-step-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { Badge } from '../primitives/badge';
import { cn } from '@/utils/ui';
import { StepTypeEnum } from '@/utils/enums';
import { STEP_TYPE_TO_COLOR } from '@/utils/color';
import { useFeatureFlag } from '@/hooks/use-feature-flag';
import { FeatureFlagsKeysEnum } from '@novu/shared';

const MenuGroup = ({ children }: { children: ReactNode }) => {
return <div className="flex flex-col">{children}</div>;
Expand Down Expand Up @@ -73,6 +75,7 @@ export const AddStepMenu = ({
onMenuItemClick: (stepType: StepTypeEnum) => void;
}) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const areNewStepsEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_ND_DELAY_DIGEST_EMAIL_ENABLED);

const handleMenuItemClick = (stepType: StepTypeEnum) => {
onMenuItemClick(stepType);
Expand Down Expand Up @@ -104,7 +107,18 @@ export const AddStepMenu = ({
<MenuGroup>
<MenuTitle>Channels</MenuTitle>
<MenuItemsGroup>
<MenuItem stepType={StepTypeEnum.EMAIL}>Email</MenuItem>
<MenuItem
stepType={StepTypeEnum.EMAIL}
disabled={!areNewStepsEnabled}
onClick={() => {
if (!areNewStepsEnabled) {
return;
}
handleMenuItemClick(StepTypeEnum.EMAIL);
}}
>
Email
</MenuItem>
<MenuItem
stepType={StepTypeEnum.IN_APP}
disabled={false}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { InAppSubject } from '@/components/workflow-editor/steps/in-app/in-app-s
import { InAppBody } from '@/components/workflow-editor/steps/in-app/in-app-body';
import { InAppAvatar } from '@/components/workflow-editor/steps/in-app/in-app-avatar';
import { InAppRedirect } from '@/components/workflow-editor/steps/in-app/in-app-redirect';
import { Maily } from '@/components/workflow-editor/steps/email/maily';
import { EmailSubject } from '@/components/workflow-editor/steps/email/email-subject';

export const getComponentByType = ({ component }: { component?: UiComponentEnum }) => {
switch (component) {
Expand All @@ -23,6 +25,12 @@ export const getComponentByType = ({ component }: { component?: UiComponentEnum
case UiComponentEnum.URL_TEXT_BOX: {
return <InAppRedirect />;
}
case UiComponentEnum.MAILY: {
return <Maily />;
}
case UiComponentEnum.TEXT_INLINE_LABEL: {
return <EmailSubject />;
}
default: {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { zodResolver } from '@hookform/resolvers/zod';
import {
FeatureFlagsKeysEnum,
IEnvironment,
StepDataDto,
StepTypeEnum,
Expand All @@ -8,7 +9,7 @@ import {
WorkflowResponseDto,
} from '@novu/shared';
import { motion } from 'motion/react';
import { useMemo, useState } from 'react';
import { HTMLAttributes, ReactNode, useMemo, useState } from 'react';
import { useForm } from 'react-hook-form';
import { RiArrowLeftSLine, RiArrowRightSLine, RiCloseFill, RiDeleteBin2Line, RiPencilRuler2Fill } from 'react-icons/ri';
import { Link, useNavigate } from 'react-router-dom';
Expand All @@ -29,14 +30,20 @@ import {
getFirstControlsErrorMessage,
updateStepInWorkflow,
} from '@/components/workflow-editor/step-utils';
import { ConfigureInAppStepTemplateCta } from '@/components/workflow-editor/steps/in-app/configure-in-app-step-template-cta';
import { SdkBanner } from '@/components/workflow-editor/steps/sdk-banner';
import { buildRoute, ROUTES } from '@/utils/routes';
import { EXCLUDED_EDITOR_TYPES } from '@/utils/constants';
import { STEP_NAME_BY_TYPE } from './step-provider';
import { useFormAutosave } from '@/hooks/use-form-autosave';

const SUPPORTED_STEP_TYPES = [StepTypeEnum.IN_APP];
import { ConfigureStepTemplateCta } from '@/components/workflow-editor/steps/configure-step-template-cta';
import { ConfigureInAppStepPreview } from '@/components/workflow-editor/steps/in-app/configure-in-app-step-preview';
import { useFeatureFlag } from '@/hooks/use-feature-flag';
import { ConfigureEmailStepPreview } from '@/components/workflow-editor/steps/email/configure-email-step-preview';

const stepTypeToPreview: Record<string, ((props: HTMLAttributes<HTMLDivElement>) => ReactNode) | undefined> = {
[StepTypeEnum.IN_APP]: ConfigureInAppStepPreview,
[StepTypeEnum.EMAIL]: ConfigureEmailStepPreview,
};

type ConfigureStepFormProps = {
workflow: WorkflowResponseDto;
Expand All @@ -50,6 +57,13 @@ export const ConfigureStepForm = (props: ConfigureStepFormProps) => {
const { step, workflow, update, updateStepCache, environment } = props;
const navigate = useNavigate();
const isCodeCreatedWorkflow = workflow.origin === WorkflowOriginEnum.EXTERNAL;
const areNewStepsEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_ND_DELAY_DIGEST_EMAIL_ENABLED);

const supportedStepTypes = [StepTypeEnum.IN_APP];
if (areNewStepsEnabled) {
supportedStepTypes.push(StepTypeEnum.EMAIL);
}
const Preview = stepTypeToPreview[step.type] || (() => null);

const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);

Expand Down Expand Up @@ -84,7 +98,7 @@ export const ConfigureStepForm = (props: ConfigureStepFormProps) => {
[step]
);

const isDashboardStepThatSupportsEditor = !isCodeCreatedWorkflow && SUPPORTED_STEP_TYPES.includes(step.type);
const isDashboardStepThatSupportsEditor = !isCodeCreatedWorkflow && supportedStepTypes.includes(step.type);
const isCodeStepThatSupportsEditor = isCodeCreatedWorkflow && !EXCLUDED_EDITOR_TYPES.includes(step.type);
const isStepSupportsEditor = isDashboardStepThatSupportsEditor || isCodeStepThatSupportsEditor;

Expand Down Expand Up @@ -184,9 +198,13 @@ export const ConfigureStepForm = (props: ConfigureStepFormProps) => {
</>
)}

{step.type === StepTypeEnum.IN_APP && <ConfigureInAppStepTemplateCta step={step} issue={firstError} />}
{supportedStepTypes.includes(step.type) && (
<ConfigureStepTemplateCta step={step} issue={firstError}>
<Preview />
</ConfigureStepTemplateCta>
)}

{!isCodeCreatedWorkflow && !SUPPORTED_STEP_TYPES.includes(step.type) && (
{!isCodeCreatedWorkflow && !supportedStepTypes.includes(step.type) && (
<>
<SidebarContent>
<SdkBanner />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { Button } from '@/components/primitives/button';
import { Separator } from '@/components/primitives/separator';
import { SidebarContent } from '@/components/side-navigation/sidebar';
import { ConfigureInAppStepPreview } from '@/components/workflow-editor/steps/in-app/configure-in-app-step-preview';
import { StepDataDto } from '@novu/shared';
import { PropsWithChildren } from 'react';
import { RiArrowRightUpLine } from 'react-icons/ri';
import { Link } from 'react-router-dom';

type ConfigureInAppStepTemplateCtaProps = {
type ConfigureStepTemplateCtaProps = PropsWithChildren & {
step: StepDataDto;
issue?: string;
};
export const ConfigureInAppStepTemplateCta = (props: ConfigureInAppStepTemplateCtaProps) => {
const { step, issue } = props;
export const ConfigureStepTemplateCta = (props: ConfigureStepTemplateCtaProps) => {
const { step, children, issue } = props;

if (issue) {
return (
Expand Down Expand Up @@ -44,9 +44,7 @@ export const ConfigureInAppStepTemplateCta = (props: ConfigureInAppStepTemplateC

return (
<>
<SidebarContent>
<ConfigureInAppStepPreview />
</SidebarContent>
<SidebarContent>{children}</SidebarContent>
<Separator />
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ import { OtherStepTabs } from './other-steps-tabs';
import { Form } from '@/components/primitives/form/form';
import { useFormAutosave } from '@/hooks/use-form-autosave';
import { SaveFormContext } from '@/components/workflow-editor/steps/save-form-context';
import { EmailTabs } from '@/components/workflow-editor/steps/email/email-tabs';

const STEP_TYPE_TO_EDITOR: Record<StepTypeEnum, (args: StepEditorProps) => React.JSX.Element | null> = {
[StepTypeEnum.EMAIL]: OtherStepTabs,
[StepTypeEnum.EMAIL]: EmailTabs,
[StepTypeEnum.CHAT]: OtherStepTabs,
[StepTypeEnum.IN_APP]: InAppTabs,
[StepTypeEnum.SMS]: OtherStepTabs,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,14 @@ import { PageMeta } from '@/components/page-meta';
import { useWorkflow } from '@/components/workflow-editor/workflow-provider';
import { useStep } from '@/components/workflow-editor/steps/step-provider';
import { getEncodedId, STEP_DIVIDER } from '@/utils/step';
import { StepTypeEnum } from '@novu/shared';
import { cn } from '@/utils/ui';

const transitionSetting = { ease: [0.29, 0.83, 0.57, 0.99], duration: 0.4 };
const stepTypeToClassname: Record<string, string | undefined> = {
[StepTypeEnum.IN_APP]: 'sm:max-w-[600px]',
[StepTypeEnum.EMAIL]: 'sm:max-w-[800px]',
};

export const ConfigureStepTemplate = () => {
const { stepSlug = '' } = useParams<{
Expand Down Expand Up @@ -75,9 +81,10 @@ export const ConfigureStepTemplate = () => {
x: '100%',
}}
transition={transitionSetting}
className={
'bg-background fixed inset-y-0 right-0 z-50 flex h-full w-3/4 flex-col border-l shadow-lg outline-none sm:max-w-[600px]'
}
className={cn(
'bg-background fixed inset-y-0 right-0 z-50 flex h-full w-3/4 flex-col border-l shadow-lg outline-none sm:max-w-[600px]',
stepTypeToClassname[step.type]
)}
>
<VisuallyHidden>
<SheetTitle />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import * as Sentry from '@sentry/react';
import { HTMLAttributes, useEffect } from 'react';
import { useParams } from 'react-router-dom';

import { useStep } from '@/components/workflow-editor/steps/step-provider';
import { usePreviewStep } from '@/hooks';
import { EmailPreviewHeader } from '@/components/workflow-editor/steps/email/email-preview';
import { Separator } from '@/components/primitives/separator';
import { Skeleton } from '@/components/primitives/skeleton';
import { ChannelTypeEnum } from '@novu/shared';
import { cn } from '@/utils/ui';

type MiniEmailPreviewProps = HTMLAttributes<HTMLDivElement>;
const MiniEmailPreview = (props: MiniEmailPreviewProps) => {
const { className, children, ...rest } = props;
return (
<div
className={cn(
'border-neutral-alpha-200 before:to-background relative isolate mb-4 rounded-lg border border-dashed before:pointer-events-none before:absolute before:inset-0 before:-m-px before:rounded-lg before:bg-gradient-to-b before:from-transparent before:bg-clip-padding',
className
)}
{...rest}
>
<div className="flex flex-col gap-1 py-1">
<EmailPreviewHeader className="px-2 text-sm" />
<Separator className="bg-neutral-alpha-100" />
<div className="relative z-10 space-y-1 px-2">{children}</div>
</div>
</div>
);
};

type ConfigureEmailStepPreviewProps = HTMLAttributes<HTMLDivElement>;
export function ConfigureEmailStepPreview(props: ConfigureEmailStepPreviewProps) {
const {
previewStep,
data: previewData,
isPending: isPreviewPending,
} = usePreviewStep({
onError: (error) => {
Sentry.captureException(error);
},
});
const { step, isPending } = useStep();

const { workflowSlug, stepSlug } = useParams<{
workflowSlug: string;
stepSlug: string;
}>();

useEffect(() => {
if (!workflowSlug || !stepSlug || !step || isPending) return;

previewStep({
workflowSlug,
stepSlug,
data: { controlValues: step.controls.values, previewPayload: {} },
});
}, [workflowSlug, stepSlug, previewStep, step, isPending]);

if (isPreviewPending) {
return (
<MiniEmailPreview>
<Skeleton className="h-5 w-full max-w-[25ch]" />
<Skeleton className="h-5 w-full max-w-[15ch]" />
</MiniEmailPreview>
);
}

if (previewData?.result?.type !== ChannelTypeEnum.EMAIL) {
return <MiniEmailPreview>No preview available</MiniEmailPreview>;
}

return (
<MiniEmailPreview {...props}>
<div className="text-foreground-400 line-clamp-2 text-xs">{previewData.result.preview.subject}</div>
</MiniEmailPreview>
);
}
Loading

0 comments on commit 2bc56b1

Please sign in to comment.