Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add sender config drawer & enable sender edit
Introduce a SenderConfigDrawer component and wire it into the email editor/preview to allow editing the sender name/email per email step. EmailPreviewHeader now shows an edit button and reads values from the form (falling back to integration defaults). EmailEditor toggles the drawer and restricts body editing to DEV environment. sanitize-control-values now includes the from field and tightens typing for email controls. Also remove a Sentry breadcrumb and add a console.log for bridge outputs in the email send usecase for debugging.
  • Loading branch information
scopsy committed Feb 2, 2026
commit e7f8e744cda1d3d7e35e006d9b6ac737e7a1af91
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { EnvironmentTypeEnum, UiComponentEnum, type UiSchema, UiSchemaGroupEnum } from '@novu/shared';
import { useState } from 'react';
import { getComponentByType } from '@/components/workflow-editor/steps/component-utils';
import { EmailPreviewHeader } from '@/components/workflow-editor/steps/email/email-preview';
import { SenderConfigDrawer } from '@/components/workflow-editor/steps/email/sender-config-drawer';
import { useEnvironment } from '@/context/environment/hooks';
import { cn } from '../../../../utils/ui';
import { StepEditorUnavailable } from '../step-editor-unavailable';
Expand All @@ -10,6 +12,7 @@ type EmailEditorProps = { uiSchema: UiSchema; isEditorV2?: boolean };
export const EmailEditor = (props: EmailEditorProps) => {
const { currentEnvironment } = useEnvironment();
const { uiSchema, isEditorV2 = false } = props;
const [senderDrawerOpen, setSenderDrawerOpen] = useState(false);

if (uiSchema.group !== UiSchemaGroupEnum.EMAIL) {
return null;
Expand All @@ -18,28 +21,32 @@ export const EmailEditor = (props: EmailEditorProps) => {
const { body, subject, disableOutputSanitization, editorType, layoutId } = uiSchema.properties ?? {};

return (
<div className="flex h-full flex-col">
<div className={cn('px-4 pb-0 pt-4', isEditorV2 && 'px-0 pt-0')}>
<div className={cn(isEditorV2 && 'border-b border-neutral-200 px-3 py-2')}>
<EmailPreviewHeader minimalHeader={isEditorV2}>
{disableOutputSanitization &&
getComponentByType({
component: disableOutputSanitization.component,
})}
{getComponentByType({ component: editorType?.component ?? UiComponentEnum.EMAIL_EDITOR_SELECT })}
</EmailPreviewHeader>
</div>
<>
<div className="flex h-full flex-col">
<div className={cn('px-4 pb-0 pt-4', isEditorV2 && 'px-0 pt-0')}>
<div className={cn(isEditorV2 && 'border-b border-neutral-200 px-3 py-2')}>
<EmailPreviewHeader minimalHeader={isEditorV2} onEditSenderClick={() => setSenderDrawerOpen(true)}>
{disableOutputSanitization &&
getComponentByType({
component: disableOutputSanitization.component,
})}
{getComponentByType({ component: editorType?.component ?? UiComponentEnum.EMAIL_EDITOR_SELECT })}
</EmailPreviewHeader>
</div>

<div className={cn(isEditorV2 && 'px-3 py-0')}>{getComponentByType({ component: subject.component })}</div>
<div className="flex items-center gap-0.5 border-b border-t border-neutral-100 px-1 py-1">
{getComponentByType({ component: layoutId?.component ?? UiComponentEnum.LAYOUT_SELECT })}
<div className={cn(isEditorV2 && 'px-3 py-0')}>{getComponentByType({ component: subject.component })}</div>
<div className="flex items-center gap-0.5 border-b border-t border-neutral-100 px-1 py-1">
{getComponentByType({ component: layoutId?.component ?? UiComponentEnum.LAYOUT_SELECT })}
</div>
</div>
{currentEnvironment?.type === EnvironmentTypeEnum.DEV ? (
getComponentByType({ component: body.component })
) : (
<StepEditorUnavailable />
)}
</div>
{currentEnvironment?.type === EnvironmentTypeEnum.DEV ? (
getComponentByType({ component: body.component })
) : (
<StepEditorUnavailable />
)}
</div>

<SenderConfigDrawer open={senderDrawerOpen} onOpenChange={setSenderDrawerOpen} />
</>
);
};
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
import { ResourceOriginEnum } from '@novu/shared';
import { HTMLAttributes, useCallback, useEffect, useRef } from 'react';
import { RiArrowDownSFill } from 'react-icons/ri';
import { useFormContext } from 'react-hook-form';
import { RiArrowDownSFill, RiEdit2Line } from 'react-icons/ri';
import { MAILY_EMAIL_WIDTH } from '@/components/maily/maily-config';
import { Avatar, AvatarImage } from '@/components/primitives/avatar';
import { Skeleton } from '@/components/primitives/skeleton';
import { usePrimaryEmailIntegration } from '@/hooks/use-primary-email-integration';
import { cn } from '@/utils/ui';
import { NovuBranding } from './novu-branding';

type EmailPreviewHeaderProps = HTMLAttributes<HTMLDivElement> & { minimalHeader?: boolean };
type EmailPreviewHeaderProps = HTMLAttributes<HTMLDivElement> & {
minimalHeader?: boolean;
onEditSenderClick?: () => void;
};

export const EmailPreviewHeader = (props: EmailPreviewHeaderProps) => {
const { className, children, minimalHeader = false, ...rest } = props;
const { className, children, minimalHeader = false, onEditSenderClick, ...rest } = props;
const { senderEmail, senderName, isLoading } = usePrimaryEmailIntegration();
const formContext = useFormContext();
const fromEmail = formContext?.watch('from.email');
const fromName = formContext?.watch('from.name');

const displaySenderName = senderName || 'Acme Inc.';
const displaySenderEmail = senderEmail || 'noreply@novu.co';
const displaySenderName = fromName || senderName || 'Acme Inc.';
const displaySenderEmail = fromEmail || senderEmail || 'noreply@novu.co';

Comment on lines +20 to 26
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n "EmailPreviewHeader" apps/dashboard -C 3

Repository: novuhq/novu

Length of output: 13315


🏁 Script executed:

# Also search for the component definition to understand its structure
cat -n apps/dashboard/src/components/workflow-editor/steps/email/email-preview.tsx | head -50

Repository: novuhq/novu

Length of output: 2629


🏁 Script executed:

# Search for FormProvider usage around the email preview component
rg -n "FormProvider" apps/dashboard/src/components/workflow-editor -C 2

Repository: novuhq/novu

Length of output: 1947


🏁 Script executed:

# Check step-preview-hover-card.tsx context around EmailPreviewHeader usage
sed -n '1,100p' apps/dashboard/src/components/step-preview-hover-card.tsx

Repository: novuhq/novu

Length of output: 2703


🏁 Script executed:

# Check layout-email-editor.tsx for FormProvider context
cat -n apps/dashboard/src/components/layouts/layout-email-editor.tsx

Repository: novuhq/novu

Length of output: 1246


🏁 Script executed:

# Check email-preview-wrapper.tsx for FormProvider context
head -100 apps/dashboard/src/components/workflow-editor/steps/preview/previews/email-preview-wrapper.tsx

Repository: novuhq/novu

Length of output: 3324


🏁 Script executed:

# Check email-editor-preview.tsx for FormProvider context
head -80 apps/dashboard/src/components/workflow-editor/steps/email/email-editor-preview.tsx

Repository: novuhq/novu

Length of output: 2643


🏁 Script executed:

# Check email-editor.tsx for FormProvider - we know it has onEditSenderClick so likely has form context
head -50 apps/dashboard/src/components/workflow-editor/steps/email/email-editor.tsx

Repository: novuhq/novu

Length of output: 2440


🏁 Script executed:

# Check configure-email-step-preview.tsx
head -60 apps/dashboard/src/components/workflow-editor/steps/email/configure-email-step-preview.tsx

Repository: novuhq/novu

Length of output: 2356


🏁 Script executed:

# Search for FormProvider usage in workflow-editor directory to understand context
rg -n "FormProvider" apps/dashboard/src/components/workflow-editor --include="*.tsx" -B 2 -A 2

Repository: novuhq/novu

Length of output: 496


🏁 Script executed:

# Check email-editor.tsx usage - where is it imported/used
rg -n "EmailEditor\b" apps/dashboard/src/components -A 2 -B 2

Repository: novuhq/novu

Length of output: 4915


🏁 Script executed:

# Check the structure around email-preview-wrapper and how it's used
rg -n "EmailCorePreview\|EmailPreviewWrapper" apps/dashboard/src/components -B 2 -A 2

Repository: novuhq/novu

Length of output: 37


🏁 Script executed:

# Check step-editor-factory.tsx for FormProvider context
rg -n "FormProvider" apps/dashboard/src/components/workflow-editor/steps/editor/step-editor-factory.tsx

Repository: novuhq/novu

Length of output: 37


🏁 Script executed:

# Check email-tabs.tsx which wraps EmailEditor
cat -n apps/dashboard/src/components/workflow-editor/steps/email/email-tabs.tsx

Repository: novuhq/novu

Length of output: 2400


🏁 Script executed:

# Check step-preview-hover-card.tsx more broadly for FormProvider
rg -n "FormProvider" apps/dashboard/src/components/step-preview-hover-card.tsx

Repository: novuhq/novu

Length of output: 37


🏁 Script executed:

# Check if there's a higher-level workflow form context
rg -n "FormProvider|useFormContext" apps/dashboard/src/components/workflow-editor/workflow-provider.tsx -A 5 -B 5

Repository: novuhq/novu

Length of output: 37


🏁 Script executed:

# Check parent component of step-editor-factory
rg -n "StepEditorFactory" apps/dashboard/src/components -B 5 -A 5

Repository: novuhq/novu

Length of output: 4501


🏁 Script executed:

# Check step-editor-layout.tsx to see if FormProvider is provided
cat -n apps/dashboard/src/components/workflow-editor/steps/step-editor-layout.tsx | head -150

Repository: novuhq/novu

Length of output: 7591


🏁 Script executed:

# Check if StepEditorProvider or other providers wrap FormProvider
rg -n "FormProvider" apps/dashboard/src/components/workflow-editor/steps/context/ -B 3 -A 3

Repository: novuhq/novu

Length of output: 37


🏁 Script executed:

# Check StepPreviewFactory to see how preview components are used
head -80 apps/dashboard/src/components/workflow-editor/steps/preview/step-preview-factory.tsx

Repository: novuhq/novu

Length of output: 2621


🏁 Script executed:

# Check if step-preview-hover-card context has FormProvider
rg -n "StepPreview\|FormProvider" apps/dashboard/src/components/step-preview-hover-card.tsx -B 5 -A 5

Repository: novuhq/novu

Length of output: 37


🏁 Script executed:

# Check StepEditorProvider to see if it provides FormProvider
cat -n apps/dashboard/src/components/workflow-editor/steps/context/step-editor-context.tsx | head -100

Repository: novuhq/novu

Length of output: 4065


🏁 Script executed:

# Check what context provides the form for EmailTabs
rg -n "StepEditorProvider\|FormProvider" apps/dashboard/src/components/workflow-editor/steps/context/step-editor-context.tsx -A 10 -B 2

Repository: novuhq/novu

Length of output: 37


🏁 Script executed:

# Search for where EmailTabs is actually used - it should have form context
rg -n "EmailTabs" apps/dashboard/src/components/workflow-editor -B 5 -A 5

Repository: novuhq/novu

Length of output: 8599


🏁 Script executed:

# Find where StepEditorLayout is used to see if FormProvider wraps it
rg -n "StepEditorLayout" apps/dashboard/src/components -B 5 -A 5

Repository: novuhq/novu

Length of output: 2436


🏁 Script executed:

# Check step-preview-hover-card parent usage
rg -n "StepPreview\(" apps/dashboard/src/components -B 3 -A 3 | head -60

Repository: novuhq/novu

Length of output: 2442


🏁 Script executed:

# Check if there's a FormProvider at the page level where these are used
rg -n "FormProvider" apps/dashboard/src --max-count=20

Repository: novuhq/novu

Length of output: 1022


🏁 Script executed:

# Check what page/layout wraps StepEditorLayout - find the workflow editor page
rg -n "StepEditorLayout" apps/dashboard/src --include="*.tsx" -B 10 | head -50

Repository: novuhq/novu

Length of output: 496


🏁 Script executed:

# Look for FormProvider wrapper at the page level
rg -n "FormProvider\|useForm" apps/dashboard/src/pages --include="*.tsx" -A 3 -B 3 | head -80

Repository: novuhq/novu

Length of output: 496


🏁 Script executed:

# Confirm: does step-preview-hover-card StepPreview get used with a form context or standalone?
rg -n "step-preview-hover-card\|StepPreview" apps/dashboard/src --include="*.tsx" -B 3 -A 3 | head -100

Repository: novuhq/novu

Length of output: 496


🏁 Script executed:

# Check what page/layout wraps StepEditorLayout
rg -n "StepEditorLayout" apps/dashboard/src -B 10 | head -80

Repository: novuhq/novu

Length of output: 4676


🏁 Script executed:

# Look for FormProvider wrapper at the page level - search pages directory
rg -n "FormProvider" apps/dashboard/src/pages -B 3 -A 3 | head -100

Repository: novuhq/novu

Length of output: 37


🏁 Script executed:

# Check step-preview-hover-card usage context
rg -n "step-preview-hover-card" apps/dashboard/src -B 5 -A 5

Repository: novuhq/novu

Length of output: 2293


🏁 Script executed:

# Check base-node.tsx usage context - where is it used and is it inside a form?
rg -n "base-node" apps/dashboard/src -B 3 -A 3

Repository: novuhq/novu

Length of output: 3695


🏁 Script executed:

# Check if workflow-editor (where base-node is used) has FormProvider
cat -n apps/dashboard/src/pages/edit-workflow-template-v2.tsx | head -100

Repository: novuhq/novu

Length of output: 141


🏁 Script executed:

# Verify the exact usage of StepPreview in base-node
sed -n '1,150p' apps/dashboard/src/components/workflow-editor/base-node.tsx

Repository: novuhq/novu

Length of output: 6104


🏁 Script executed:

# Find where workflow pages are and check if FormProvider wraps the whole page
fd -type f -name "*.tsx" apps/dashboard/src/pages | xargs grep -l "workflow"

Repository: novuhq/novu

Length of output: 227


🏁 Script executed:

# Check one workflow editor page to see how it's structured
cat -n apps/dashboard/src/pages/workflows.tsx | head -80

Repository: novuhq/novu

Length of output: 3906


Guard useFormContext() or make form data optional props.

EmailPreviewHeader calls useFormContext() at line 20, which throws synchronously if no provider is present. Optional chaining on lines 21-22 doesn't prevent this crash. Multiple call sites render it without a FormProvider:

  • step-preview-hover-card.tsx:53 – used in standalone StepPreview preview hover
  • layout-email-editor.tsx:20 – used in LayoutEmailEditor without form context
  • configure-email-step-preview.tsx:25 – used in ConfigureEmailStepPreview without form context

Either wrap all call sites in a FormProvider or refactor EmailPreviewHeader to accept from data as optional props with fallback to hook, preventing crashes when no provider exists.

🤖 Prompt for AI Agents
In `@apps/dashboard/src/components/workflow-editor/steps/email/email-preview.tsx`
around lines 20 - 26, EmailPreviewHeader currently calls useFormContext()
directly (which throws if no FormProvider exists) and then attempts optional
chaining on values; change EmailPreviewHeader to accept an optional prop (e.g.,
from?: { email?: string; name?: string }) and use that prop as the primary
source for from.email/from.name, falling back to calling useFormContext() only
when the prop is undefined OR, alternatively, guard the useFormContext() call by
wrapping it in a try/catch and treating missing provider as undefined; update
usages (StepPreview, LayoutEmailEditor, ConfigureEmailStepPreview) to pass from
when they can, and in EmailPreviewHeader derive
displaySenderName/displaySenderEmail from prop values first, then from
formContext.watch('from.email') / watch('from.name'), then the existing
senderName/senderEmail/defaults.

return (
<div className={cn('flex gap-2', className)} {...rest}>
Expand All @@ -30,9 +37,16 @@ export const EmailPreviewHeader = (props: EmailPreviewHeaderProps) => {
{isLoading ? (
<Skeleton className="h-4 w-40" />
) : (
<>
<button
type="button"
onClick={onEditSenderClick}
className="group flex items-center gap-1 text-left hover:text-foreground-950 focus:outline-none"
>
{displaySenderName} <span className="text-foreground-600 text-xs">{`<${displaySenderEmail}>`}</span>
</>
{onEditSenderClick && (
<RiEdit2Line className="text-foreground-600 size-3.5 opacity-0 transition-opacity group-hover:opacity-100" />
)}
</button>
Comment on lines +40 to +53
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid rendering a non-functional button when no edit handler is provided.
When onEditSenderClick is undefined, the button is still focusable but does nothing. Consider rendering a non-button element or a disabled/aria-disabled button.

💡 One way to keep behavior explicit
   const displaySenderName = fromName || senderName || 'Acme Inc.';
   const displaySenderEmail = fromEmail || senderEmail || 'noreply@novu.co';
+  const senderContent = onEditSenderClick ? (
+    <button
+      type="button"
+      onClick={onEditSenderClick}
+      className="group flex items-center gap-1 text-left hover:text-foreground-950 focus:outline-none"
+    >
+      {displaySenderName} <span className="text-foreground-600 text-xs">{`<${displaySenderEmail}>`}</span>
+      <RiEdit2Line className="text-foreground-600 size-3.5 opacity-0 transition-opacity group-hover:opacity-100" />
+    </button>
+  ) : (
+    <div className="flex items-center gap-1 text-left">
+      {displaySenderName} <span className="text-foreground-600 text-xs">{`<${displaySenderEmail}>`}</span>
+    </div>
+  );

   return (
     <div className={cn('flex gap-2', className)} {...rest}>
@@
             {isLoading ? (
               <Skeleton className="h-4 w-40" />
             ) : (
-              <button
-                type="button"
-                onClick={onEditSenderClick}
-                className="group flex items-center gap-1 text-left hover:text-foreground-950 focus:outline-none"
-              >
-                {displaySenderName} <span className="text-foreground-600 text-xs">{`<${displaySenderEmail}>`}</span>
-                {onEditSenderClick && (
-                  <RiEdit2Line className="text-foreground-600 size-3.5 opacity-0 transition-opacity group-hover:opacity-100" />
-                )}
-              </button>
+              senderContent
             )}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
type="button"
onClick={onEditSenderClick}
className="group flex items-center gap-1 text-left hover:text-foreground-950 focus:outline-none"
>
{displaySenderName} <span className="text-foreground-600 text-xs">{`<${displaySenderEmail}>`}</span>
</>
{onEditSenderClick && (
<RiEdit2Line className="text-foreground-600 size-3.5 opacity-0 transition-opacity group-hover:opacity-100" />
)}
</button>
const senderContent = onEditSenderClick ? (
<button
type="button"
onClick={onEditSenderClick}
className="group flex items-center gap-1 text-left hover:text-foreground-950 focus:outline-none"
>
{displaySenderName} <span className="text-foreground-600 text-xs">{`<${displaySenderEmail}>`}</span>
<RiEdit2Line className="text-foreground-600 size-3.5 opacity-0 transition-opacity group-hover:opacity-100" />
</button>
) : (
<div className="flex items-center gap-1 text-left">
{displaySenderName} <span className="text-foreground-600 text-xs">{`<${displaySenderEmail}>`}</span>
</div>
);
return (
<div className={cn('flex gap-2', className)} {...rest}>
{isLoading ? (
<Skeleton className="h-4 w-40" />
) : (
senderContent
)}
</div>
);
🤖 Prompt for AI Agents
In `@apps/dashboard/src/components/workflow-editor/steps/email/email-preview.tsx`
around lines 40 - 49, The sender line currently always renders a clickable
<button> with onClick={onEditSenderClick} even when onEditSenderClick is
undefined, producing a focusable but non-functional control; update the
rendering in the email-preview component so that when onEditSenderClick is
truthy you render the interactive <button> as now (keeping className, children:
displaySenderName and displaySenderEmail and the RiEdit2Line icon), and when
onEditSenderClick is falsy render a non-interactive element (e.g., a <div> or a
<span>) or render the button with disabled and aria-disabled attributes so it is
not focusable/interactive; ensure the visual styling and the presence/absence of
the edit icon match the clickability and that the accessible name
(displaySenderName + displaySenderEmail) remains unchanged.

)}
</div>
{!minimalHeader && (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { useEffect, useState } from 'react';
import { useFormContext } from 'react-hook-form';
import { RiInformation2Line } from 'react-icons/ri';
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for react-icons usage in the dashboard
echo "=== Searching for react-icons imports in dashboard ==="
rg "from ['\"]react-icons" apps/dashboard/src -t ts -t tsx -A 1 -B 1 | head -50

echo ""
echo "=== Count of react-icons imports ==="
rg "from ['\"]react-icons" apps/dashboard/src -t ts -t tsx | wc -l

echo ""
echo "=== Checking file structure of sender-config-drawer.tsx ==="
head -20 apps/dashboard/src/components/workflow-editor/steps/email/sender-config-drawer.tsx

Repository: novuhq/novu

Length of output: 1172


🏁 Script executed:

#!/bin/bash
# View the full file to see how RiInformation2Line is used
cat apps/dashboard/src/components/workflow-editor/steps/email/sender-config-drawer.tsx | grep -A 5 -B 5 "RiInformation2Line" | head -30

Repository: novuhq/novu

Length of output: 1585


Remove react-icons import; use Figma assets instead.

The RiInformation2Line import from react-icons violates the coding guideline stating all assets in the dashboard must come from the Figma payload, not external icon packages. Replace the imported icon with an icon from the Figma Dev Mode MCP Server or remove it if the information can be conveyed through the existing Tooltip component.

🤖 Prompt for AI Agents
In
`@apps/dashboard/src/components/workflow-editor/steps/email/sender-config-drawer.tsx`
at line 3, Remove the external RiInformation2Line import and all usages of
RiInformation2Line in sender-config-drawer.tsx; instead either render the
corresponding icon SVG from the Figma payload (use your project's Figma asset
helper/component to load the Dev Mode/MCP Server asset and swap it in where
RiInformation2Line is used) or drop the icon and rely on the existing Tooltip
component to convey the info—search for the RiInformation2Line symbol in the
file and replace each occurrence with the Figma asset loader or remove the
element if redundant.

import { FormControl, FormField, FormItem, FormLabel } from '@/components/primitives/form/form';
import { Input } from '@/components/primitives/input';
import { Separator } from '@/components/primitives/separator';
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/primitives/sheet';
import { Switch } from '@/components/primitives/switch';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/primitives/tooltip';
import { usePrimaryEmailIntegration } from '@/hooks/use-primary-email-integration';

type SenderConfigDrawerProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
};

export function SenderConfigDrawer({ open, onOpenChange }: SenderConfigDrawerProps) {
const { control, watch, setValue, trigger } = useFormContext();
const { senderEmail: integrationEmail, senderName: integrationName } = usePrimaryEmailIntegration();

const fromEmail = watch('from.email');
const fromName = watch('from.name');
const useProviderDefaults = fromEmail === undefined && fromName === undefined;

const [localEmail, setLocalEmail] = useState(fromEmail || '');
const [localName, setLocalName] = useState(fromName || '');

useEffect(() => {
setLocalEmail(fromEmail || '');
setLocalName(fromName || '');
}, [fromEmail, fromName]);

const handleToggleDefaults = async (checked: boolean) => {
if (checked) {
setValue('from.email', undefined, { shouldDirty: true, shouldValidate: true });
setValue('from.name', undefined, { shouldDirty: true, shouldValidate: true });
setLocalEmail('');
setLocalName('');
} else {
setValue('from.email', '', { shouldDirty: true, shouldValidate: true });
setValue('from.name', '', { shouldDirty: true, shouldValidate: true });
setLocalEmail('');
setLocalName('');
}

await trigger();
};

return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent className="flex w-[400px] flex-col p-0 sm:max-w-[400px]">
<SheetHeader className="space-y-1 px-3 py-4">
<SheetTitle className="text-label-lg flex items-center gap-2">Sender configuration</SheetTitle>
<SheetDescription className="text-paragraph-xs mt-0 hidden">
Configure the sender name and email address for this email step.
</SheetDescription>
</SheetHeader>
<Separator />

<div className="flex-1 space-y-4 overflow-auto p-3">
<div className="rounded-4 border flex items-center justify-between border-neutral-100 bg-white p-1.5">
<div className="text-text-strong text-label-xs flex items-center gap-1">
Use provider defaults
<Tooltip>
<TooltipTrigger className="flex cursor-default flex-row items-center gap-1">
<RiInformation2Line className="size-3 text-neutral-400" />
</TooltipTrigger>
<TooltipContent>
<p>
When enabled, the email will use the sender name and email from your configured email integration.
</p>
</TooltipContent>
</Tooltip>
</div>
<Switch checked={useProviderDefaults} onCheckedChange={handleToggleDefaults} />
</div>

<FormField
control={control}
name="from.name"
render={() => (
<FormItem>
<FormLabel>Sender name</FormLabel>
<FormControl>
<Input
placeholder={integrationName || 'e.g. Acme Security'}
disabled={useProviderDefaults}
value={localName}
onChange={(e) => {
setLocalName(e.target.value);
}}
onBlur={() => {
setValue('from.name', localName, { shouldDirty: true, shouldValidate: true });
trigger();
}}
/>
</FormControl>
</FormItem>
)}
/>

<FormField
control={control}
name="from.email"
render={() => (
<FormItem>
<FormLabel>Sender email</FormLabel>
<FormControl>
<Input
type="email"
placeholder={integrationEmail || 'e.g. noreply@acme.com'}
disabled={useProviderDefaults}
value={localEmail}
onChange={(e) => {
setLocalEmail(e.target.value);
}}
onBlur={() => {
setValue('from.email', localEmail, { shouldDirty: true, shouldValidate: true });
trigger();
}}
/>
</FormControl>
</FormItem>
)}
/>
</div>
</SheetContent>
</Sheet>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ import {
WebhookEventEnum,
WebhookObjectTypeEnum,
} from '@novu/shared';
import { addBreadcrumb } from '@sentry/node';
import inlineCss from 'inline-css';

import { PlatformException } from '../../../shared/utils';
Expand Down Expand Up @@ -132,10 +131,6 @@ export class SendMessageEmail extends SendMessageBase {
if (!step) throw new PlatformException('Email channel step not found');
if (!step.template) throw new PlatformException('Email channel template not found');

addBreadcrumb({
message: 'Sending Email',
});

if (!integration) {
await this.createExecutionDetails.execute(
CreateExecutionDetailsCommand.create({
Expand All @@ -162,6 +157,7 @@ export class SendMessageEmail extends SendMessageBase {
}

const bridgeOutputs = command.bridgeData?.outputs;
console.log('bridgeOptions', JSON.stringify(bridgeOutputs, null, 2));
const [template, overrideLayoutId] = await Promise.all([
this.processVariants(command),
this.getOverrideLayoutId(command, !!bridgeOutputs),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,14 @@ function sanitizeEmail(controlValues: EmailControlType) {
content: [{ type: 'paragraph' }],
});

const emailControls: EmailControlType = {
const emailControls: Required<EmailControlType> = {
editorType: controlValues.editorType,
subject: sanitizeEmptyInput(controlValues.subject, ' '),
body: sanitizeEmptyInput(controlValues.body, EMPTY_TIP_TAP),
skip: controlValues.skip,
disableOutputSanitization: controlValues.disableOutputSanitization,
layoutId: controlValues.layoutId,
from: controlValues.from,
};

return filterNullishValues(emailControls);
Expand Down
Loading