-
Notifications
You must be signed in to change notification settings - Fork 4.2k
feat(api-service, worker, dashboard): custom sender name in email step on new dashboard fixes NV-6950 #9962
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
Changes from 1 commit
e7054a8
e7f8e74
8805f1e
a401b5a
f7423a4
f865bdd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
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
There are no files selected for viewing
| 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'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className={cn('flex gap-2', className)} {...rest}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid rendering a non-functional button when no edit handler is provided. 💡 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {!minimalHeader && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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'; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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.tsxRepository: 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 -30Repository: novuhq/novu Length of output: 1585 Remove react-icons import; use Figma assets instead. The 🤖 Prompt for AI Agents |
||
| 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> | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
rg -n "EmailPreviewHeader" apps/dashboard -C 3Repository: novuhq/novu
Length of output: 13315
🏁 Script executed:
Repository: novuhq/novu
Length of output: 2629
🏁 Script executed:
Repository: novuhq/novu
Length of output: 1947
🏁 Script executed:
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.tsxRepository: 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.tsxRepository: 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.tsxRepository: 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.tsxRepository: 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.tsxRepository: novuhq/novu
Length of output: 2356
🏁 Script executed:
Repository: novuhq/novu
Length of output: 496
🏁 Script executed:
Repository: novuhq/novu
Length of output: 4915
🏁 Script executed:
Repository: novuhq/novu
Length of output: 37
🏁 Script executed:
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.tsxRepository: novuhq/novu
Length of output: 2400
🏁 Script executed:
Repository: novuhq/novu
Length of output: 37
🏁 Script executed:
Repository: novuhq/novu
Length of output: 37
🏁 Script executed:
Repository: novuhq/novu
Length of output: 4501
🏁 Script executed:
Repository: novuhq/novu
Length of output: 7591
🏁 Script executed:
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.tsxRepository: novuhq/novu
Length of output: 2621
🏁 Script executed:
Repository: novuhq/novu
Length of output: 37
🏁 Script executed:
Repository: novuhq/novu
Length of output: 4065
🏁 Script executed:
Repository: novuhq/novu
Length of output: 37
🏁 Script executed:
Repository: novuhq/novu
Length of output: 8599
🏁 Script executed:
Repository: novuhq/novu
Length of output: 2436
🏁 Script executed:
Repository: novuhq/novu
Length of output: 2442
🏁 Script executed:
Repository: novuhq/novu
Length of output: 1022
🏁 Script executed:
Repository: novuhq/novu
Length of output: 496
🏁 Script executed:
Repository: novuhq/novu
Length of output: 496
🏁 Script executed:
Repository: novuhq/novu
Length of output: 496
🏁 Script executed:
Repository: novuhq/novu
Length of output: 4676
🏁 Script executed:
Repository: novuhq/novu
Length of output: 37
🏁 Script executed:
Repository: novuhq/novu
Length of output: 2293
🏁 Script executed:
Repository: novuhq/novu
Length of output: 3695
🏁 Script executed:
Repository: novuhq/novu
Length of output: 141
🏁 Script executed:
Repository: novuhq/novu
Length of output: 6104
🏁 Script executed:
Repository: novuhq/novu
Length of output: 227
🏁 Script executed:
Repository: novuhq/novu
Length of output: 3906
Guard
useFormContext()or make form data optional props.EmailPreviewHeadercallsuseFormContext()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 standaloneStepPreviewpreview hoverlayout-email-editor.tsx:20– used inLayoutEmailEditorwithout form contextconfigure-email-step-preview.tsx:25– used inConfigureEmailStepPreviewwithout form contextEither wrap all call sites in a FormProvider or refactor
EmailPreviewHeaderto acceptfromdata as optional props with fallback to hook, preventing crashes when no provider exists.🤖 Prompt for AI Agents