-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Add customizable email content for partner invites #3066
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
Conversation
Enables editing of subject, title, and body for partner invite emails in the dashboard. Updates the invite action and schema to accept custom email fields, and updates the email template to render user-provided markdown content. This allows program admins to personalize partner invitation emails.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThis PR introduces email template customization for partner invitations. Users can edit invitation email subject, title, and body (with Markdown rendering) through an enhanced UI, save templates to the database, and apply saved content when sending invites. Includes input sanitization, Zod validation, and preview functionality. Changes
Sequence DiagramsequenceDiagram
participant User
participant UI as Edit UI
participant Action as save-invite-email-data
participant DB as Database
participant Partner as Partner Invite Send
rect rgb(200, 220, 255)
note over User, DB: Email Template Customization
User->>UI: Edit subject/title/body
UI->>UI: Track draft changes
User->>UI: Click Save (or Ctrl+Enter)
UI->>Action: saveInviteEmailDataAction(subject, title, body)
Action->>Action: Validate & sanitize body
Action->>DB: Update Program.inviteEmailData
DB-->>Action: Confirm
Action-->>UI: Success
UI->>UI: Update saved state, exit edit mode
UI-->>User: Show confirmation
end
rect rgb(220, 255, 220)
note over User, Partner: Send Partner Invites
User->>UI: Click Send Invite
UI->>Partner: Trigger invite send
Partner->>DB: Fetch Program + inviteEmailData
Partner->>Partner: Apply email template overrides
Partner->>Partner: Send customized email
Partner-->>UI: Invite sent
UI-->>User: Show success
end
Estimated code review effortπ― 3 (Moderate) | β±οΈ ~25 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touchesβ Failed checks (1 warning)
β Passed checks (2 passed)
β¨ Finishing touches
π§ͺ Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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.
Actionable comments posted: 1
π Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
π Files selected for processing (4)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx(5 hunks)apps/web/lib/actions/partners/invite-partner.ts(2 hunks)apps/web/lib/zod/schemas/partners.ts(1 hunks)packages/email/src/templates/program-invite.tsx(4 hunks)
π§° Additional context used
π§ Learnings (9)
π Learning: 2025-07-30T15:25:13.936Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.936Z
Learning: In apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, the form schema uses partial condition objects to allow users to add empty/unconfigured condition fields without type errors, while submission validation uses strict schemas to ensure data integrity. This two-stage validation pattern improves UX by allowing progressive completion of complex forms.
Applied to files:
apps/web/lib/zod/schemas/partners.tsapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
π Learning: 2025-05-29T04:45:18.504Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2448
File: packages/email/src/templates/partner-program-summary.tsx:0-0
Timestamp: 2025-05-29T04:45:18.504Z
Learning: In the PartnerProgramSummary email template (packages/email/src/templates/partner-program-summary.tsx), the stat titles are hardcoded constants ("Clicks", "Leads", "Sales", "Earnings") that will always match the ICONS object keys after toLowerCase() conversion, so icon lookup failures are not possible.
Applied to files:
packages/email/src/templates/program-invite.tsx
π Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
π Learning: 2025-09-24T16:10:37.349Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/ui/partners/partner-about.tsx:11-11
Timestamp: 2025-09-24T16:10:37.349Z
Learning: In the Dub codebase, the team prefers to import Icon as a runtime value from "dub/ui" and uses Icon as both a type and variable name in component props, even when this creates shadowing. This is their established pattern and should not be suggested for refactoring.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
π Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification. The implementation uses: type={onClick ? "button" : type}
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
π Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
π Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically sets type="button" when an onClick prop is passed and defaults to type="submit" otherwise, using the logic: type={props.onClick ? "button" : "submit"}. This prevents accidental form submissions when buttons are used for modal triggers or other non-form actions.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
π Learning: 2025-10-06T15:48:14.205Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2935
File: apps/web/lib/actions/partners/invite-partner-from-network.ts:21-28
Timestamp: 2025-10-06T15:48:14.205Z
Learning: For the network invites limit check in apps/web/lib/actions/partners/invite-partner-from-network.ts, the team accepts that concurrent invites may bypass the limit due to race conditions. Perfect atomicity is not required for this feature.
Applied to files:
apps/web/lib/actions/partners/invite-partner.ts
π Learning: 2025-09-17T17:44:03.965Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/lib/actions/partners/update-program.ts:96-0
Timestamp: 2025-09-17T17:44:03.965Z
Learning: In apps/web/lib/actions/partners/update-program.ts, the team prefers to keep the messagingEnabledAt update logic simple by allowing client-provided timestamps rather than implementing server-controlled timestamp logic to avoid added complexity.
Applied to files:
apps/web/lib/actions/partners/invite-partner.ts
𧬠Code graph analysis (3)
packages/email/src/templates/program-invite.tsx (1)
packages/email/src/react-email.d.ts (3)
Heading(16-16)Text(15-15)Link(14-14)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (3)
apps/web/lib/swr/use-program.ts (1)
useProgram(6-40)apps/web/lib/swr/use-workspace.ts (1)
useWorkspace(6-46)packages/ui/src/hooks/use-enter-submit.ts (1)
useEnterSubmit(3-25)
apps/web/lib/actions/partners/invite-partner.ts (3)
packages/email/src/index.ts (1)
sendEmail(6-29)packages/email/src/templates/program-invite.tsx (1)
ProgramInvite(69-257)apps/web/lib/api/partners/get-partner-invite-rewards-and-bounties.ts (1)
getPartnerInviteRewardsAndBounties(19-97)
β° Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
Replaced custom markdown parsing in the program invite email template with react-markdown and remark-gfm for improved markdown rendering. Updated dependencies to include react-markdown and remark-gfm.
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.
Actionable comments posted: 1
π§Ή Nitpick comments (1)
packages/email/src/templates/program-invite.tsx (1)
21-86: Consider email client compatibility for raw HTML elements.The custom markdown components use raw HTML elements (
<ul>,<ol>,<li>,<blockquote>,<pre>,<hr>) with Tailwind classes. While React Email's Tailwind wrapper converts classes to inline styles, several features may not render consistently across email clients:
- List styling (
list-disc,list-decimal,marker:) has limited support- Overflow handling (
overflow-x-autoon code blocks) often doesn't work in email- Border positioning (
border-l-2on blockquotes) can be inconsistent- Advanced layout properties may degrade in Outlook, Gmail, etc.
You previously discussed using
@react-email/markdownwhich is purpose-built for email rendering and handles these edge cases. If you encounter rendering issues during testing, that component remains a more robust option.Test the rendered emails across major clients (Gmail, Outlook, Apple Mail, Yahoo) to ensure markdown elements display as expected. Pay particular attention to:
- Code blocks with long lines
- Nested lists
- Blockquotes
- List markers
π Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
β Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
π Files selected for processing (2)
packages/email/package.json(1 hunks)packages/email/src/templates/program-invite.tsx(4 hunks)
π§° Additional context used
π§ Learnings (1)
π Learning: 2025-05-29T04:45:18.504Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2448
File: packages/email/src/templates/partner-program-summary.tsx:0-0
Timestamp: 2025-05-29T04:45:18.504Z
Learning: In the PartnerProgramSummary email template (packages/email/src/templates/partner-program-summary.tsx), the stat titles are hardcoded constants ("Clicks", "Leads", "Sales", "Earnings") that will always match the ICONS object keys after toLowerCase() conversion, so icon lookup failures are not possible.
Applied to files:
packages/email/src/templates/program-invite.tsx
𧬠Code graph analysis (1)
packages/email/src/templates/program-invite.tsx (1)
packages/email/src/react-email.d.ts (3)
Text(15-15)Link(14-14)Heading(16-16)
β° Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Socket Security: Pull Request Alerts
- GitHub Check: build
π Additional comments (5)
packages/email/src/templates/program-invite.tsx (4)
17-18: LGTM!The imports are appropriate for adding markdown rendering support to the email template.
116-118: LGTM!The new optional props for customizable email content are correctly typed and align with the PR objectives.
Also applies to: 129-131
133-135: LGTM!The derived email metadata with fallback values is well-structured and provides sensible defaults.
152-153: LGTM!The heading correctly uses the customizable email title.
packages/email/package.json (1)
16-17: No action requiredβboth packages are secure and appropriately versioned.react-markdown v9.0.3 is secure with no known vulnerabilities, and the breaking change in v10 (className prop removal) does not affect this codebase, which uses custom component mapping and remark plugins instead. remark-gfm v4.0.1 includes only type and documentation improvements, making the current v4.0.0 safe and stable. Both packages are production-ready.
Added a sanitizeMarkdown utility to clean and validate markdown content for partner invite emails. Enforced a 3000 character limit for the email body in both the UI and schema, truncating and notifying users if exceeded. Updated the email template to use rehype-sanitize for additional HTML sanitization, and added the rehype-sanitize dependency.
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.
Actionable comments posted: 3
Caution
Some comments are outside the diff and canβt be posted inline due to platform limitations.
β οΈ Outside diff range comments (1)
apps/web/lib/actions/partners/invite-partner.ts (1)
60-103: Email failure leaves orphaned partner enrollment.The partner is created and enrolled (lines 60-71) before the email is sent (lines 76-99). If
sendEmailfails, the partner enrollment persists but no invitation email is delivered. This creates an inconsistent state where the partner appears "invited" in the database but never received the invitation.Consider either:
- Transactional approach: Wrap the enrollment and email send in a transaction-like pattern where enrollment is marked as "pending" until email succeeds, or
- Idempotent retry: Store the email send as a queued task that can be retried, or
- Rollback on failure: Delete the enrollment if email sending fails:
const enrolledPartner = await createAndEnrollPartner({ workspace, program, partner: { email, username, ...(groupId && { groupId }), }, userId: user.id, skipEnrollmentCheck: true, status: "invited", }); const sanitizedEmailBody = emailBody ? sanitizeMarkdown(emailBody) : null; try { await sendEmail({ subject: emailSubject || `${program.name} invited you to join Dub Partners`, variant: "notifications", to: email, replyTo: program.supportEmail || "noreply", react: ProgramInvite({ email, name: enrolledPartner.name, program: { name: program.name, slug: program.slug, logo: program.logo, }, ...(emailSubject && { subject: emailSubject }), ...(emailTitle && { title: emailTitle }), ...(sanitizedEmailBody && { body: sanitizedEmailBody }), ...(await getPartnerInviteRewardsAndBounties({ programId, groupId: enrolledPartner.groupId || program.defaultGroupId, })), }), }); } catch (error) { console.error("Failed to send partner invite email", error); + // Rollback the enrollment since email failed + await prisma.programEnrollment.delete({ + where: { id: enrolledPartner.id }, + }); throw error; }
π§Ή Nitpick comments (2)
apps/web/lib/partners/sanitize-markdown.ts (1)
15-49: Consider renaming to reflect validation/normalization behavior.The function name
sanitizeMarkdownsuggests content sanitization (removing dangerous HTML/XSS), but the implementation primarily validates and normalizes (trims, checks for binary content, normalizes line endings). The actual HTML sanitization happens downstream viarehype-sanitizein the email template.Consider renaming to
validateMarkdownornormalizeMarkdownfor clarity, or add a comment explaining that XSS/HTML sanitization occurs separately.apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (1)
110-113: Consider warning users before they exceed the character limit.Currently, truncation only shows an error toast after the user clicks "Save" (line 112). This could frustrate users who spend time crafting content beyond 3000 characters only to have it silently truncated.
Consider one of these UX improvements:
- Real-time character counter: Show remaining characters as they type (e.g., "2,500 / 3,000 characters")
- Warning threshold: Show a warning toast when they approach the limit (e.g., at 2,800 characters)
- Prevent input beyond limit: The
maxLength={3000}on the textarea (line 399) already prevents typing beyond the limit, but copy-paste could bypass itSince the textarea already has
maxLength={3000}, the truncation logic may be redundant unless there's a path where users can bypass it.Verify if the truncation logic (lines 110-113) is reachable given the
maxLength={3000}attribute on the textarea (line 399).
π Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
β Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
π Files selected for processing (6)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx(5 hunks)apps/web/lib/actions/partners/invite-partner.ts(3 hunks)apps/web/lib/partners/sanitize-markdown.ts(1 hunks)apps/web/lib/zod/schemas/partners.ts(1 hunks)packages/email/package.json(1 hunks)packages/email/src/templates/program-invite.tsx(4 hunks)
π§ Files skipped from review as they are similar to previous changes (1)
- apps/web/lib/zod/schemas/partners.ts
π§° Additional context used
π§ Learnings (7)
π Learning: 2025-05-29T04:45:18.504Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2448
File: packages/email/src/templates/partner-program-summary.tsx:0-0
Timestamp: 2025-05-29T04:45:18.504Z
Learning: In the PartnerProgramSummary email template (packages/email/src/templates/partner-program-summary.tsx), the stat titles are hardcoded constants ("Clicks", "Leads", "Sales", "Earnings") that will always match the ICONS object keys after toLowerCase() conversion, so icon lookup failures are not possible.
Applied to files:
packages/email/src/templates/program-invite.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
π Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
π Learning: 2025-09-24T16:10:37.349Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/ui/partners/partner-about.tsx:11-11
Timestamp: 2025-09-24T16:10:37.349Z
Learning: In the Dub codebase, the team prefers to import Icon as a runtime value from "dub/ui" and uses Icon as both a type and variable name in component props, even when this creates shadowing. This is their established pattern and should not be suggested for refactoring.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
π Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification. The implementation uses: type={onClick ? "button" : type}
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
π Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
π Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically sets type="button" when an onClick prop is passed and defaults to type="submit" otherwise, using the logic: type={props.onClick ? "button" : "submit"}. This prevents accidental form submissions when buttons are used for modal triggers or other non-form actions.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
π Learning: 2025-07-30T15:25:13.936Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.936Z
Learning: In apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, the form schema uses partial condition objects to allow users to add empty/unconfigured condition fields without type errors, while submission validation uses strict schemas to ensure data integrity. This two-stage validation pattern improves UX by allowing progressive completion of complex forms.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
𧬠Code graph analysis (3)
packages/email/src/templates/program-invite.tsx (1)
packages/email/src/react-email.d.ts (3)
Text(15-15)Link(14-14)Heading(16-16)
apps/web/lib/actions/partners/invite-partner.ts (4)
apps/web/lib/partners/sanitize-markdown.ts (1)
sanitizeMarkdown(15-49)packages/email/src/index.ts (1)
sendEmail(6-29)packages/email/src/templates/program-invite.tsx (1)
ProgramInvite(89-254)apps/web/lib/api/partners/get-partner-invite-rewards-and-bounties.ts (1)
getPartnerInviteRewardsAndBounties(19-97)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (4)
apps/web/lib/swr/use-program.ts (1)
useProgram(6-40)apps/web/lib/swr/use-workspace.ts (1)
useWorkspace(6-46)packages/ui/src/hooks/use-enter-submit.ts (1)
useEnterSubmit(3-25)packages/email/src/templates/program-invite.tsx (6)
a(28-38)p(23-27)strong(56-58)ul(39-45)ol(46-52)li(53-55)
β° Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Socket Security: Pull Request Alerts
- GitHub Check: build
π Additional comments (5)
packages/email/package.json (1)
16-18: LGTM! Dependency additions support the Markdown rendering pipeline.The three new dependencies correctly enable Markdown rendering with GitHub Flavored Markdown support and HTML sanitization for email templates.
packages/email/src/templates/program-invite.tsx (1)
22-87: Comprehensive Markdown component mapping with consistent styling.The
markdownComponentsimplementation correctly maps Markdown elements to styled React Email components, handling:
- Paragraphs, links with proper targets
- Lists (ul/ol) with custom markers
- Inline/block code with appropriate styling
- Typography (strong, em, blockquote)
- Horizontal rules
The styling is consistent with the existing email design and uses email-safe Tailwind classes.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (3)
44-129: Well-structured email editing workflow with proper state management.The implementation correctly handles:
- Memoized default content derived from program name
- Separate
emailContent(saved) anddraftEmailContent(editing) state- Edit/save/cancel workflow with proper state transitions
- Fallback to defaults for empty fields
- 3000-character limit enforcement with user feedback
The state management ensures that canceling edits properly reverts to the last saved state, and empty fields fall back to sensible defaults.
451-502: Excellent alignment between preview and email template rendering.The ReactMarkdown implementation in the preview (lines 451-502) uses the same plugins (
remarkGfm) and similar component styling as the email template (packages/email/src/templates/program-invite.tsx), ensuring that what users see in the preview matches what recipients receive. This addresses the past review concern about Markdown parsing alignment.The component overrides correctly:
- Style links, paragraphs, lists consistently
- Use
cn()for class composition- Maintain responsive styling
271-271: Good UX: disabling submit during email editing.Disabling the "Send invite" button while
isEditingEmailis true (line 271) prevents users from accidentally submitting with unsaved draft changes. They must either save or cancel their edits before submitting the form.
Refactors the partner invite action to rollback partner enrollment if sending the invite email fails. Updates markdown sanitization to reject content with excessively long lines instead of truncating. Refactors program invite email template to use inline styles for better compatibility and introduces a pixel-based Tailwind config for consistent styling.
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.
Actionable comments posted: 2
π§Ή Nitpick comments (1)
apps/web/lib/partners/sanitize-markdown.ts (1)
15-43: Consider adding a total length limit.While the per-line length check (1000 chars) prevents individual malformed lines, there's no limit on the total content size. A user could submit tens of thousands of 999-character lines, potentially causing memory or processing issues.
Consider adding a check before line splitting:
// Return null if empty after trimming if (!sanitized) { return null; } + + // Limit total content size to prevent DoS + const maxTotalLength = 10000; // Adjust based on requirements + if (sanitized.length > maxTotalLength) { + return null; + } // Check for binary content - markdown should be valid UTF-8 text
π Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
π Files selected for processing (3)
apps/web/lib/actions/partners/invite-partner.ts(3 hunks)apps/web/lib/partners/sanitize-markdown.ts(1 hunks)packages/email/src/templates/program-invite.tsx(4 hunks)
π§° Additional context used
π§ Learnings (3)
π Learning: 2025-10-06T15:48:14.205Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2935
File: apps/web/lib/actions/partners/invite-partner-from-network.ts:21-28
Timestamp: 2025-10-06T15:48:14.205Z
Learning: For the network invites limit check in apps/web/lib/actions/partners/invite-partner-from-network.ts, the team accepts that concurrent invites may bypass the limit due to race conditions. Perfect atomicity is not required for this feature.
Applied to files:
apps/web/lib/actions/partners/invite-partner.tspackages/email/src/templates/program-invite.tsx
π Learning: 2025-09-17T17:44:03.965Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/lib/actions/partners/update-program.ts:96-0
Timestamp: 2025-09-17T17:44:03.965Z
Learning: In apps/web/lib/actions/partners/update-program.ts, the team prefers to keep the messagingEnabledAt update logic simple by allowing client-provided timestamps rather than implementing server-controlled timestamp logic to avoid added complexity.
Applied to files:
apps/web/lib/actions/partners/invite-partner.ts
π Learning: 2025-05-29T04:45:18.504Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2448
File: packages/email/src/templates/partner-program-summary.tsx:0-0
Timestamp: 2025-05-29T04:45:18.504Z
Learning: In the PartnerProgramSummary email template (packages/email/src/templates/partner-program-summary.tsx), the stat titles are hardcoded constants ("Clicks", "Leads", "Sales", "Earnings") that will always match the ICONS object keys after toLowerCase() conversion, so icon lookup failures are not possible.
Applied to files:
packages/email/src/templates/program-invite.tsx
𧬠Code graph analysis (2)
apps/web/lib/actions/partners/invite-partner.ts (5)
apps/web/lib/partners/sanitize-markdown.ts (1)
sanitizeMarkdown(15-49)apps/web/lib/api/partners/get-partner-invite-rewards-and-bounties.ts (1)
getPartnerInviteRewardsAndBounties(19-97)packages/email/src/index.ts (1)
sendEmail(6-29)packages/email/src/templates/program-invite.tsx (1)
ProgramInvite(191-356)packages/prisma/index.ts (1)
prisma(3-9)
packages/email/src/templates/program-invite.tsx (1)
packages/email/src/react-email.d.ts (8)
Text(15-15)Link(14-14)Html(4-4)Head(5-5)Preview(18-18)Tailwind(19-19)Heading(16-16)Section(8-8)
β° Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
π Additional comments (6)
packages/email/src/templates/program-invite.tsx (4)
22-59: LGTM! Email-safe pixel-based preset.The pixel-based preset correctly avoids rem units and other CSS features that email clients strip. This addresses the email client compatibility concerns from previous reviews.
61-189: LGTM! Email-safe Markdown components.The custom components correctly use inline styles for cross-client compatibility. The list styling with explicit
listStyleTypeandlistStylePositionavoids the::markerpseudo-element issues flagged in previous reviews, and arbitrary Tailwind values have been eliminated.
219-238: LGTM! Clear default values.The optional email customization props with sensible defaults provide the right balance between flexibility and fallback behavior.
259-288: LGTM! Secure Markdown rendering with appropriate fallback.The conditional rendering correctly uses
rehypeSanitizefor XSS protection and provides a sensible fallback. Note that an empty stringbodywill trigger the fallback, which appears intentional given the upstream sanitization returnsnullfor empty content.apps/web/lib/partners/sanitize-markdown.ts (1)
45-49: LGTM! Standard line ending normalization.The line ending normalization is correct and follows standard practices.
apps/web/lib/actions/partners/invite-partner.ts (1)
123-141:Promise.allSettledcorrectly ensures both tasks complete.The use of
Promise.allSettledensures the audit log is recorded even if email sending fails. However, note the critical issue above regarding response timingβthe action returns success before knowing whether the email succeeded.
Adds a check to throw an error if the sanitized email body is invalid, providing a clear message for unsupported content. Simplifies error handling and logging when sending partner invite emails by removing the rollback logic and including more context in the log output.
Improved code formatting in sanitizeMarkdown function and JSX strong element for better readability and consistency. No functional changes were made.
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.
Actionable comments posted: 1
π Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
π Files selected for processing (2)
apps/web/lib/partners/sanitize-markdown.ts(1 hunks)packages/email/src/templates/program-invite.tsx(4 hunks)
π§ Files skipped from review as they are similar to previous changes (1)
- apps/web/lib/partners/sanitize-markdown.ts
π§° Additional context used
π§ Learnings (2)
π Learning: 2025-10-06T15:48:14.205Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2935
File: apps/web/lib/actions/partners/invite-partner-from-network.ts:21-28
Timestamp: 2025-10-06T15:48:14.205Z
Learning: For the network invites limit check in apps/web/lib/actions/partners/invite-partner-from-network.ts, the team accepts that concurrent invites may bypass the limit due to race conditions. Perfect atomicity is not required for this feature.
Applied to files:
packages/email/src/templates/program-invite.tsx
π Learning: 2025-05-29T04:45:18.504Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2448
File: packages/email/src/templates/partner-program-summary.tsx:0-0
Timestamp: 2025-05-29T04:45:18.504Z
Learning: In the PartnerProgramSummary email template (packages/email/src/templates/partner-program-summary.tsx), the stat titles are hardcoded constants ("Clicks", "Leads", "Sales", "Earnings") that will always match the ICONS object keys after toLowerCase() conversion, so icon lookup failures are not possible.
Applied to files:
packages/email/src/templates/program-invite.tsx
𧬠Code graph analysis (1)
packages/email/src/templates/program-invite.tsx (1)
packages/email/src/react-email.d.ts (8)
Text(15-15)Link(14-14)Html(4-4)Head(5-5)Preview(18-18)Tailwind(19-19)Heading(16-16)Section(8-8)
β° Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
Replaces the hardcoded preview text with the dynamic emailSubject variable in the program invite email template for improved customization.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
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.
Actionable comments posted: 2
π§Ή Nitpick comments (4)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (4)
266-273: Consider adding a tooltip to explain disabled submit button.The submit button is correctly disabled during email editing (line 272), but users might not understand why. A tooltip would improve clarity.
<Button type="submit" variant="primary" text="Send invite" className="w-fit" loading={isPending} disabled={isPending || !email || isEditingEmail} + {...(isEditingEmail && { + disabledTooltip: "Save or cancel email changes before sending invite" + })} />
476-486: Dynamic key causes unnecessary remounts on body changes.Using the full email body content in the key (line 477) forces the RichTextProvider to remount every time the body changes, which may cause performance issues and lose editor state unnecessarily.
Consider using a stable key or incrementing counter:
+// At component level +const [previewKey, setPreviewKey] = useState(0); +// In handleSaveEmail, increment the key when content is saved +setPreviewKey(prev => prev + 1); <RichTextProvider - key={`preview-${displayContent.body}`} + key={`preview-${previewKey}`} features={["bold", "italic", "links"]} style="condensed" markdown editable={false} initialValue={displayContent.body}Alternatively, if remounting is intentional to reset editor state on content changes, add a comment explaining this choice.
348-397: Consider extracting a helper to reduce code duplication.The pattern for updating
draftEmailContentis repeated for subject (lines 362-366) and title (lines 387-391) fields.// Helper function const updateDraftField = (field: keyof EmailContent, value: string) => { setDraftEmailContent({ ...draftEmailContent, [field]: value, }); }; // Then use it: <input id="email-subject" type="text" value={draftEmailContent.subject} onChange={(e) => updateDraftField('subject', e.target.value)} // ... /> <input id="email-title" type="text" value={draftEmailContent.title} onChange={(e) => updateDraftField('title', e.target.value)} // ... />
280-499: Consider splitting the large EmailPreview component.At 219 lines, the
EmailPreviewcomponent handles multiple concerns: rendering editing UI, preview UI, and managing RichText editor state. This makes it harder to maintain and test.Consider extracting sub-components:
EmailEditForm- handles the editing UI (lines 348-449)EmailPreviewDisplay- handles the preview UI (lines 450-494)- Keep
EmailPreviewas the coordinatorExample structure:
function EmailPreview({ ... }) { // State and effects remain here return ( <div className="mt-6 rounded-lg border border-neutral-200 bg-neutral-50"> <EmailPreviewHeader isEditingEmail={isEditingEmail} onStartEditing={onStartEditing} onSave={onSave} onCancel={onCancel} /> <div className="..."> {isEditingEmail ? ( <EmailEditForm draftEmailContent={draftEmailContent} setDraftEmailContent={setDraftEmailContent} richTextRef={richTextRef} onSave={onSave} /> ) : ( <EmailPreviewDisplay displayContent={displayContent} program={program} /> )} </div> </div> ); }This would improve readability and make each component easier to test in isolation.
π Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
π Files selected for processing (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx(5 hunks)
π§° Additional context used
π§ Learnings (6)
π Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
π Learning: 2025-05-29T04:45:18.504Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2448
File: packages/email/src/templates/partner-program-summary.tsx:0-0
Timestamp: 2025-05-29T04:45:18.504Z
Learning: In the PartnerProgramSummary email template (packages/email/src/templates/partner-program-summary.tsx), the stat titles are hardcoded constants ("Clicks", "Leads", "Sales", "Earnings") that will always match the ICONS object keys after toLowerCase() conversion, so icon lookup failures are not possible.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
π Learning: 2025-09-24T16:10:37.349Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/ui/partners/partner-about.tsx:11-11
Timestamp: 2025-09-24T16:10:37.349Z
Learning: In the Dub codebase, the team prefers to import Icon as a runtime value from "dub/ui" and uses Icon as both a type and variable name in component props, even when this creates shadowing. This is their established pattern and should not be suggested for refactoring.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
π Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification. The implementation uses: type={onClick ? "button" : type}
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
π Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
π Learning: 2025-07-30T15:25:13.936Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.936Z
Learning: In apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, the form schema uses partial condition objects to allow users to add empty/unconfigured condition fields without type errors, while submission validation uses strict schemas to ensure data integrity. This two-stage validation pattern improves UX by allowing progressive completion of complex forms.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
𧬠Code graph analysis (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (5)
apps/web/lib/swr/use-program.ts (1)
useProgram(6-40)apps/web/lib/swr/use-workspace.ts (1)
useWorkspace(6-46)packages/ui/src/rich-text-area/rich-text-provider.tsx (1)
RichTextProvider(69-242)packages/ui/src/rich-text-area/index.tsx (1)
RichTextArea(9-32)packages/ui/src/rich-text-area/rich-text-toolbar.tsx (1)
RichTextToolbar(16-129)
β° Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
π Additional comments (4)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (4)
12-14: LGTM! Clean imports and type definition.The imports are well-organized and the
EmailContenttype clearly defines the structure for customizable email fields.Also applies to: 18-18, 20-27, 38-42
49-63: Well-structured state management for email editing.The use of separate
emailContent(saved) anddraftEmailContent(working draft) states enables a clean cancel flow. The memoized default content is computed efficiently.
88-102: Form submission correctly includes optional email fields.The conditional spread pattern ensures email customization fields are only sent when the user has edited them, keeping the payload clean for default invites.
415-421: No changes required β the type cast is the correct way to use the official @tiptap/markdown API.The
getMarkdown()method is a documented method added by the @tiptap/markdown extension and works correctly across the codebase. The type cast toanyis necessary because @tiptap adds this method at runtime through its extension system, so it's not exposed in the Editor type definitions. This pattern is used consistently throughout the codebase (message-input.tsx, add-edit-integration-form.tsx, add-edit-app-form.tsx, etc.) and is the standard way to access @tiptap extension methods in TypeScript.Likely an incorrect or invalid review comment.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
Outdated
Show resolved
Hide resolved
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
Show resolved
Hide resolved
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.
Actionable comments posted: 2
π Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
π Files selected for processing (1)
packages/email/src/templates/program-invite.tsx(4 hunks)
π§° Additional context used
π§ Learnings (2)
π Learning: 2025-10-06T15:48:14.205Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2935
File: apps/web/lib/actions/partners/invite-partner-from-network.ts:21-28
Timestamp: 2025-10-06T15:48:14.205Z
Learning: For the network invites limit check in apps/web/lib/actions/partners/invite-partner-from-network.ts, the team accepts that concurrent invites may bypass the limit due to race conditions. Perfect atomicity is not required for this feature.
Applied to files:
packages/email/src/templates/program-invite.tsx
π Learning: 2025-05-29T04:45:18.504Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2448
File: packages/email/src/templates/partner-program-summary.tsx:0-0
Timestamp: 2025-05-29T04:45:18.504Z
Learning: In the PartnerProgramSummary email template (packages/email/src/templates/partner-program-summary.tsx), the stat titles are hardcoded constants ("Clicks", "Leads", "Sales", "Earnings") that will always match the ICONS object keys after toLowerCase() conversion, so icon lookup failures are not possible.
Applied to files:
packages/email/src/templates/program-invite.tsx
𧬠Code graph analysis (1)
packages/email/src/templates/program-invite.tsx (1)
packages/email/src/react-email.d.ts (7)
Html(4-4)Head(5-5)Preview(18-18)Heading(16-16)Text(15-15)Link(14-14)Section(8-8)
β° Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
π Additional comments (1)
packages/email/src/templates/program-invite.tsx (1)
148-151: Dynamic subject/title wiring looks goodUsing
emailTitle/emailSubjectfor the main heading and<Preview>aligns the template with the customizable subject/title behavior and removes the previous unused-variable issue. The smallertext-xson the CTA also matches the more compact layout.No further changes needed here from my side.
Also applies to: 155-155, 168-168, 200-200
|
@coderabbitai full review |
β Actions performedFull review triggered. |
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.
Actionable comments posted: 0
β»οΈ Duplicate comments (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (1)
331-365: Document the intentionaluseEffectdependency suppression inEmailPreviewThe
useEffectthat syncs the editor when entering edit mode:const prevIsEditingEmail = useRef(isEditingEmail); useEffect(() => { if (isEditingEmail && !prevIsEditingEmail.current && richTextRef.current) { richTextRef.current.setContent(draftEmailContent.body); } prevIsEditingEmail.current = isEditingEmail; // eslint-disable-next-line react-hooks/exhaustive-deps }, [isEditingEmail]);intentionally ignores
draftEmailContent.bodyso the content is only injected once when toggling into edit mode (subsequent changes are driven by the editor itself). To make this clearer for future maintainers and justify the ESLint override, consider adding an inline comment above the disable explaining thatdraftEmailContent.bodyis deliberately excluded and thatinitialValue/editor updates handle ongoing changes.The preview-side
RichTextProviderwitheditable={false}and the markdown configuration looks good and closely matches what the email template sends.Also applies to: 462-500, 531-541
π§Ή Nitpick comments (1)
apps/web/lib/actions/partners/invite-partner.ts (1)
72-132: Invite email customization wiring looks correct; consider DRY-ing subject handlingThe new flow correctly:
- Pulls
inviteEmailDatafromprogram,- Uses the custom subject when present (with a sensible default),
- Passes optional subject/title/body into
ProgramInvite,- Sends the email in a background task while logging failures, without rolling back enrollment.
To avoid future drift between the
sendEmailsubject and the templateβs default, consider computing the subject once and reusing it:- const inviteEmailData = program.inviteEmailData; + const inviteEmailData = program.inviteEmailData; + const emailSubject = + inviteEmailData?.subject || + `${program.name} invited you to join Dub Partners`; @@ - await sendEmail({ - subject: - inviteEmailData?.subject || - `${program.name} invited you to join Dub Partners`, + await sendEmail({ + subject: emailSubject, @@ - react: ProgramInvite({ + react: ProgramInvite({ email, name: enrolledPartner.name, program: { name: program.name, slug: program.slug, logo: program.logo, }, - ...(inviteEmailData?.subject && { - subject: inviteEmailData.subject, - }), + subject: emailSubject, ...(inviteEmailData?.title && { title: inviteEmailData.title }), ...(inviteEmailData?.body && { body: inviteEmailData.body }), ...rewardsAndBounties, }),This keeps the subject line consistent between the actual email header and the rendered template, and centralizes the default logic.
π Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
π Files selected for processing (7)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx(6 hunks)apps/web/lib/actions/partners/invite-partner.ts(1 hunks)apps/web/lib/actions/partners/save-invite-email-data.ts(1 hunks)apps/web/lib/zod/schemas/program-invite-email.ts(1 hunks)apps/web/lib/zod/schemas/programs.ts(2 hunks)packages/email/src/templates/program-invite.tsx(4 hunks)packages/prisma/schema/program.prisma(1 hunks)
π§° Additional context used
π§ Learnings (10)
π Learning: 2025-10-06T15:48:14.205Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2935
File: apps/web/lib/actions/partners/invite-partner-from-network.ts:21-28
Timestamp: 2025-10-06T15:48:14.205Z
Learning: For the network invites limit check in apps/web/lib/actions/partners/invite-partner-from-network.ts, the team accepts that concurrent invites may bypass the limit due to race conditions. Perfect atomicity is not required for this feature.
Applied to files:
apps/web/lib/actions/partners/invite-partner.tspackages/email/src/templates/program-invite.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsxapps/web/lib/actions/partners/save-invite-email-data.ts
π Learning: 2025-11-12T22:23:10.390Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 3098
File: apps/web/lib/actions/partners/message-program.ts:49-58
Timestamp: 2025-11-12T22:23:10.390Z
Learning: In apps/web/lib/actions/partners/message-program.ts, when checking if a partner can continue messaging after messaging is disabled, the code intentionally requires `senderPartnerId: null` (program-initiated messages) to prevent partners from continuing to send junk messages. Only conversations started by the program should continue after messaging is disabled, as a spam prevention mechanism.
Applied to files:
apps/web/lib/actions/partners/invite-partner.tsapps/web/lib/actions/partners/save-invite-email-data.ts
π Learning: 2025-09-17T17:44:03.965Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/lib/actions/partners/update-program.ts:96-0
Timestamp: 2025-09-17T17:44:03.965Z
Learning: In apps/web/lib/actions/partners/update-program.ts, the team prefers to keep the messagingEnabledAt update logic simple by allowing client-provided timestamps rather than implementing server-controlled timestamp logic to avoid added complexity.
Applied to files:
apps/web/lib/actions/partners/invite-partner.tsapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsxapps/web/lib/actions/partners/save-invite-email-data.ts
π Learning: 2025-10-17T08:18:19.278Z
Learnt from: devkiran
Repo: dubinc/dub PR: 0
File: :0-0
Timestamp: 2025-10-17T08:18:19.278Z
Learning: In the apps/web codebase, `@/lib/zod` should only be used for places that need OpenAPI extended zod schema. All other places should import from the standard `zod` package directly using `import { z } from "zod"`.
Applied to files:
apps/web/lib/zod/schemas/program-invite-email.ts
π Learning: 2025-05-29T04:45:18.504Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2448
File: packages/email/src/templates/partner-program-summary.tsx:0-0
Timestamp: 2025-05-29T04:45:18.504Z
Learning: In the PartnerProgramSummary email template (packages/email/src/templates/partner-program-summary.tsx), the stat titles are hardcoded constants ("Clicks", "Leads", "Sales", "Earnings") that will always match the ICONS object keys after toLowerCase() conversion, so icon lookup failures are not possible.
Applied to files:
packages/email/src/templates/program-invite.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
π Learning: 2025-07-30T15:25:13.936Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.936Z
Learning: In apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, the form schema uses partial condition objects to allow users to add empty/unconfigured condition fields without type errors, while submission validation uses strict schemas to ensure data integrity. This two-stage validation pattern improves UX by allowing progressive completion of complex forms.
Applied to files:
apps/web/lib/zod/schemas/programs.tsapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
π Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
π Learning: 2025-09-24T16:10:37.349Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/ui/partners/partner-about.tsx:11-11
Timestamp: 2025-09-24T16:10:37.349Z
Learning: In the Dub codebase, the team prefers to import Icon as a runtime value from "dub/ui" and uses Icon as both a type and variable name in component props, even when this creates shadowing. This is their established pattern and should not be suggested for refactoring.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
π Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification. The implementation uses: type={onClick ? "button" : type}
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
π Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
𧬠Code graph analysis (5)
apps/web/lib/actions/partners/invite-partner.ts (3)
apps/web/lib/api/partners/get-partner-invite-rewards-and-bounties.ts (1)
getPartnerInviteRewardsAndBounties(19-97)packages/email/src/index.ts (1)
sendEmail(6-29)packages/email/src/templates/program-invite.tsx (1)
ProgramInvite(103-264)
packages/email/src/templates/program-invite.tsx (1)
packages/email/src/react-email.d.ts (7)
Html(4-4)Head(5-5)Preview(18-18)Heading(16-16)Text(15-15)Link(14-14)Section(8-8)
apps/web/lib/zod/schemas/programs.ts (1)
apps/web/lib/zod/schemas/program-invite-email.ts (1)
programInviteEmailDataSchema(3-9)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (7)
apps/web/lib/swr/use-program.ts (1)
useProgram(6-40)apps/web/lib/swr/use-workspace.ts (1)
useWorkspace(6-46)apps/web/lib/actions/partners/invite-partner.ts (1)
invitePartnerAction(15-133)apps/web/lib/actions/partners/save-invite-email-data.ts (1)
saveInviteEmailDataAction(16-45)packages/ui/src/rich-text-area/rich-text-provider.tsx (1)
RichTextProvider(69-242)packages/ui/src/rich-text-area/index.tsx (1)
RichTextArea(9-32)packages/ui/src/rich-text-area/rich-text-toolbar.tsx (1)
RichTextToolbar(16-129)
apps/web/lib/actions/partners/save-invite-email-data.ts (3)
apps/web/lib/actions/safe-action.ts (1)
authActionClient(39-88)apps/web/lib/partners/sanitize-markdown.ts (1)
sanitizeMarkdown(15-51)packages/prisma/index.ts (1)
prisma(3-9)
β° Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
π Additional comments (7)
apps/web/lib/zod/schemas/programs.ts (1)
21-54: ProgramSchema wiring forinviteEmailDatalooks consistent
inviteEmailData: programInviteEmailDataSchemacleanly exposes the new JSON field on Program while keeping it optional/nullish and aligned with the PrismaJson?column and the dedicated schema file. No issues from a typing or serialization standpoint.packages/prisma/schema/program.prisma (1)
42-44: NewinviteEmailDatacolumn is well-placed and typedAdding
inviteEmailData Json? @db.JsontoProgrammatches how other structured config blobs are stored (embedData,resources) and lines up with the Zod schemaβs nullable shape. Migration-wise this is a safe additive change.apps/web/lib/zod/schemas/program-invite-email.ts (1)
1-9: Invite email data schema matches persisted shape
programInviteEmailDataSchema(subject/title/body as strings, nullish) matches what the server action writes intoProgram.inviteEmailDataand what the email template expects. Using the plainzodimport here is also consistent with the apps/web schema conventions.packages/email/src/templates/program-invite.tsx (2)
131-196: Markdown-powered body, subject/title props, and Preview wiring look solidThe new optional
subject,title, andbodyprops, together withemailTitle/emailSubjectand the conditional{body ? <Markdown> : fallback copy}, cleanly support customized content while preserving the existing default copy. Using the sameemailSubjectfor<Preview>ensures inbox preview text matches the actual subject logic.
21-101: I'll verify the claim about the@react-email/markdownlibrary and the correct key for styling links.Based on my web search of @react-email/markdown documentation which states that markdownCustomStyles provides custom styles for the corresponding html element (p, h1, h2, etc.), I was unable to find definitive documentation confirming whether the key for styling anchors should be
linkorain themarkdownCustomStylesobject.The official React Email documentation and examples consistently show keys like
h1,h2, andcodeInline, but notably no examples demonstrate anchor/link styling. While the review comment's reasoning that anchors render as<a>elements (and thus the key should bea) is logically sound, I cannot confirm from available documentation whether@react-email/markdownindeed expects the keyarather thanlinkfor anchor styling.Verify the correct key for styling links in
@react-email/markdownPlease confirm whether the
markdownCustomStylesobject should uselinkoraas the key for styling anchors by:
- Testing the current implementation with the
linkkey to see if anchor styles apply- Checking the
@react-email/markdownsource code or TypeScript type definitions to see what keys are supported- Consulting the library's repository issues or discussions for examples of link styling
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (1)
51-81: Email editing state and save flow are coherent and aligned with server validationThe combination of:
defaultEmailContentderived fromprogram?.name,savedEmailContentbuilt fromprogram.inviteEmailData,- local
emailContentvsdraftEmailContentstate, andsaveInviteEmailDataActionwith trimming and a 3000βchar limitgives a clear UX: admins can edit a draft, save to persist (with truncation surfaced via toast), and immediately see the updated preview. The main βSend inviteβ button being disabled while editing or while a save is in flight avoids sending with partially edited or unsaved content. This all matches the server schema and sanitization behavior.
Also applies to: 107-183, 292-323
apps/web/lib/actions/partners/save-invite-email-data.ts (1)
9-45: Validation and sanitization for invite email data are robust
saveInviteEmailDataAction:
- Requires
workspaceId, non-emptysubject/title, and a trimmedbodycapped at 3000 characters, and- Runs
sanitizeMarkdown(body)and throws if the content is invalid (binary/null bytes, excessively long lines, etc.),before updating
Program.inviteEmailDatawith trimmed subject/title and the sanitized body. This eliminates the earlier silent-failure risk around markdown sanitization and ensures only safe, bounded content is persisted.
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.
Actionable comments posted: 3
β»οΈ Duplicate comments (3)
packages/email/src/templates/program-invite.tsx (1)
28-34: Fix the markdownCustomStyles key for links.The
linkkey won't match anchor elements rendered by@react-email/markdown. The component expects the keyato style anchor tags.Apply this diff:
- link: { + a: { fontWeight: "600", color: "#000000", textDecoration: "underline", textDecorationStyle: "dotted", textUnderlineOffset: "2px", },apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (2)
358-364: Exhaustive-deps concern persists from previous review.The
useEffectintentionally excludesdraftEmailContent.bodyfrom dependencies, which could cause stale editor content if the body changes while not in edit mode.Refer to the previous review comment for suggested solutions: either add an inline comment explaining the intentional exclusion, or include
draftEmailContent.bodyin the dependency array with appropriate guards.
152-156: Truncation UX concern persists from previous review.As noted in the prior review, automatically truncating and saving content without explicit user confirmation could lead to unintended data loss, especially if the toast notification is missed.
Refer to the previous review comment for suggested alternatives (prevent saving until within limit, or show confirmation dialog).
π§Ή Nitpick comments (3)
apps/web/lib/partners/sanitize-markdown.ts (1)
38-45: Consider more granular validation for excessively long lines.The current implementation rejects the entire input if any single line exceeds 1000 characters. While this prevents broken Markdown from truncation, it may be too strict for legitimate use casesβa single long URL or table row would cause the entire email body to be rejected.
Consider these alternatives:
- Line-by-line rejection: Return an error message indicating which line(s) are too long so users can fix specific issues.
- Smart truncation: Detect Markdown constructs (links, code blocks) and truncate only at safe boundaries.
- Document the limit: If 1000 chars/line is the hard limit, ensure the UI clearly communicates this constraint.
Example of providing more context:
- const hasExcessivelyLongLine = sanitized - .split("\n") - .some((line) => line.length > maxLineLength); - - if (hasExcessivelyLongLine) { - return null; - } + const lines = sanitized.split("\n"); + const longLineIndex = lines.findIndex((line) => line.length > maxLineLength); + + if (longLineIndex !== -1) { + throw new Error( + `Line ${longLineIndex + 1} exceeds maximum length of ${maxLineLength} characters (${lines[longLineIndex].length} chars)` + ); + }This provides actionable feedback to users while maintaining security.
apps/web/lib/actions/partners/save-invite-email-data.ts (1)
37-43: Remove redundant trimming.The schema already trims
subjectandtitleviaz.string().trim(), so the additional.trim()calls at lines 39-40 are redundant.Apply this diff:
data: { inviteEmailData: { - subject: subject.trim(), - title: title.trim(), + subject, + title, body: sanitizedBody, }, },apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (1)
143-177: Simplify the validation logic.The function creates two sets of variables with redundant fallback logic:
- Lines 158-162:
updatedContentwith fallbacks- Lines 165-168:
final*variables with the same fallback patternSince the server schema already validates
min(1)aftertrim(), the client can rely on server-side validation and simplify this code.Apply this diff to streamline the logic:
const handleSaveEmail = async () => { if (!workspaceId) { return; } - const sanitizedSubject = draftEmailContent.subject.trim(); - const sanitizedTitle = draftEmailContent.title.trim(); - let sanitizedBody = draftEmailContent.body.trim(); + let body = draftEmailContent.body.trim(); // Enforce max length validation (matches schema) - if (sanitizedBody.length > 3000) { - sanitizedBody = sanitizedBody.substring(0, 3000); + if (body.length > 3000) { + body = body.substring(0, 3000); toast.error("Email body was truncated to 3000 characters"); } - const updatedContent: EmailContent = { - subject: sanitizedSubject || defaultEmailContent.subject, - title: sanitizedTitle || defaultEmailContent.title, - body: sanitizedBody || defaultEmailContent.body, - }; - - // Ensure all values are non-empty (schema requirement) - const finalSubject = - updatedContent.subject.trim() || defaultEmailContent.subject; - const finalTitle = updatedContent.title.trim() || defaultEmailContent.title; - const finalBody = updatedContent.body.trim() || defaultEmailContent.body; - // Save to server (state updates happen in onSuccess callback) await saveEmailDataAsync({ workspaceId, - subject: finalSubject, - title: finalTitle, - body: finalBody, + subject: draftEmailContent.subject.trim() || defaultEmailContent.subject, + title: draftEmailContent.title.trim() || defaultEmailContent.title, + body: body || defaultEmailContent.body, }); };
π Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
β Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
π Files selected for processing (10)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx(6 hunks)apps/web/lib/actions/partners/invite-partner.ts(1 hunks)apps/web/lib/actions/partners/save-invite-email-data.ts(1 hunks)apps/web/lib/partners/sanitize-markdown.ts(1 hunks)apps/web/lib/zod/schemas/partners.ts(1 hunks)apps/web/lib/zod/schemas/program-invite-email.ts(1 hunks)apps/web/lib/zod/schemas/programs.ts(2 hunks)packages/email/package.json(1 hunks)packages/email/src/templates/program-invite.tsx(4 hunks)packages/prisma/schema/program.prisma(1 hunks)
π§° Additional context used
π§ Learnings (10)
π Learning: 2025-10-17T08:18:19.278Z
Learnt from: devkiran
Repo: dubinc/dub PR: 0
File: :0-0
Timestamp: 2025-10-17T08:18:19.278Z
Learning: In the apps/web codebase, `@/lib/zod` should only be used for places that need OpenAPI extended zod schema. All other places should import from the standard `zod` package directly using `import { z } from "zod"`.
Applied to files:
apps/web/lib/zod/schemas/programs.ts
π Learning: 2025-07-30T15:25:13.936Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.936Z
Learning: In apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, the form schema uses partial condition objects to allow users to add empty/unconfigured condition fields without type errors, while submission validation uses strict schemas to ensure data integrity. This two-stage validation pattern improves UX by allowing progressive completion of complex forms.
Applied to files:
apps/web/lib/zod/schemas/programs.tsapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
π Learning: 2025-09-17T17:44:03.965Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/lib/actions/partners/update-program.ts:96-0
Timestamp: 2025-09-17T17:44:03.965Z
Learning: In apps/web/lib/actions/partners/update-program.ts, the team prefers to keep the messagingEnabledAt update logic simple by allowing client-provided timestamps rather than implementing server-controlled timestamp logic to avoid added complexity.
Applied to files:
apps/web/lib/actions/partners/save-invite-email-data.tsapps/web/lib/actions/partners/invite-partner.ts
π Learning: 2025-10-06T15:48:14.205Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2935
File: apps/web/lib/actions/partners/invite-partner-from-network.ts:21-28
Timestamp: 2025-10-06T15:48:14.205Z
Learning: For the network invites limit check in apps/web/lib/actions/partners/invite-partner-from-network.ts, the team accepts that concurrent invites may bypass the limit due to race conditions. Perfect atomicity is not required for this feature.
Applied to files:
apps/web/lib/actions/partners/save-invite-email-data.tspackages/email/src/templates/program-invite.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsxapps/web/lib/actions/partners/invite-partner.ts
π Learning: 2025-05-29T04:45:18.504Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2448
File: packages/email/src/templates/partner-program-summary.tsx:0-0
Timestamp: 2025-05-29T04:45:18.504Z
Learning: In the PartnerProgramSummary email template (packages/email/src/templates/partner-program-summary.tsx), the stat titles are hardcoded constants ("Clicks", "Leads", "Sales", "Earnings") that will always match the ICONS object keys after toLowerCase() conversion, so icon lookup failures are not possible.
Applied to files:
packages/email/src/templates/program-invite.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
π Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
π Learning: 2025-09-24T16:10:37.349Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/ui/partners/partner-about.tsx:11-11
Timestamp: 2025-09-24T16:10:37.349Z
Learning: In the Dub codebase, the team prefers to import Icon as a runtime value from "dub/ui" and uses Icon as both a type and variable name in component props, even when this creates shadowing. This is their established pattern and should not be suggested for refactoring.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
π Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification. The implementation uses: type={onClick ? "button" : type}
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
π Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
π Learning: 2025-11-12T22:23:10.390Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 3098
File: apps/web/lib/actions/partners/message-program.ts:49-58
Timestamp: 2025-11-12T22:23:10.390Z
Learning: In apps/web/lib/actions/partners/message-program.ts, when checking if a partner can continue messaging after messaging is disabled, the code intentionally requires `senderPartnerId: null` (program-initiated messages) to prevent partners from continuing to send junk messages. Only conversations started by the program should continue after messaging is disabled, as a spam prevention mechanism.
Applied to files:
apps/web/lib/actions/partners/invite-partner.ts
π Additional comments (8)
packages/prisma/schema/program.prisma (1)
42-42: LGTM!The optional JSON field for storing invite email data is well-placed and appropriately typed for flexible email customization.
apps/web/lib/zod/schemas/programs.ts (1)
21-21: LGTM!The import and integration of
programInviteEmailDataSchemaintoProgramSchemais clean and follows the existing pattern.Also applies to: 50-50
packages/email/src/templates/program-invite.tsx (1)
103-196: Good integration of Markdown rendering with appropriate fallback.The conditional rendering logic cleanly separates custom Markdown content from the default template, and the
markdownCustomStylesprovide consistent styling for all common Markdown elements. The use ofemailTitleandemailSubjectderived from props ensures proper defaults.packages/email/package.json (1)
13-13: Version @react-email/markdown@^0.0.17 is current and actively maintained.The package is actively maintained with recent npm publish and active React Email repo and docs. The specified version is the latest stable release. While pre-1.0 versioning historically indicates potential for breaking changes, this package is actively developed within the Resend ecosystem and suitable for the dependency.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (3)
38-80: LGTM! Clean state management pattern.The separation of concerns between
emailContent(persisted state),draftEmailContent(editing state), anddefaultEmailContent(fallback) is well-structured and easy to follow.
320-323: LGTM! Comprehensive disabled state logic.The submit button correctly prevents submission during email editing (
isEditingEmail) and while saving email data (isSavingEmailData), ensuring data integrity.
477-488: Nice UX enhancement with keyboard shortcut!The Ctrl/Cmd+Enter shortcut to save during editing is a thoughtful addition that improves the editing experience.
apps/web/lib/actions/partners/save-invite-email-data.ts (1)
24-31: The review comment's XSS concern is incorrect. Subject and title are rendered safely in React components which auto-escape user input.The verification shows that
subjectandtitleare rendered through React Email components (<Heading>and<Preview>) and in React state assignments, both of which automatically escape potentially dangerous content. React's JSX syntax prevents HTML injection by default. ThesanitizeMarkdownfunction applied tobodyis not an XSS sanitizerβit validates content format (checks for binary data, excessive line length, and normalizes line endings).There is a minor asymmetry worth noting:
subjectandtitlelackmax()length constraints (unlikebodywithmax(3000)), but this is unrelated to the XSS concern and poses no security risk given React's built-in escaping.Likely an incorrect or invalid review comment.
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.
Actionable comments posted: 0
β»οΈ Duplicate comments (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (1)
361-367: Add explanatory comment for intentional dependency exclusion.The
useEffectintentionally excludesdraftEmailContent.bodyfrom the dependency array, but there's no comment explaining why. This was previously flagged in review.Consider adding a comment above the
eslint-disableline:// Note: draftEmailContent.body intentionally excluded - we only sync // editor content when first entering edit mode, not on subsequent updates // eslint-disable-next-line react-hooks/exhaustive-deps
π§Ή Nitpick comments (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (1)
167-171: Remove redundant validation logic.The
updatedContentvalues (lines 161-165) are already trimmed and defaulted. Creatingfinal*variables that trim and default again is redundant.Apply this diff to simplify:
- const updatedContent: EmailContent = { - subject: sanitizedSubject || defaultEmailContent.subject, - title: sanitizedTitle || defaultEmailContent.title, - body: sanitizedBody || defaultEmailContent.body, - }; - - // Ensure all values are non-empty (schema requirement) - const finalSubject = - updatedContent.subject.trim() || defaultEmailContent.subject; - const finalTitle = updatedContent.title.trim() || defaultEmailContent.title; - const finalBody = updatedContent.body.trim() || defaultEmailContent.body; + // Ensure all values are non-empty (schema requirement) + const finalSubject = sanitizedSubject || defaultEmailContent.subject; + const finalTitle = sanitizedTitle || defaultEmailContent.title; + const finalBody = sanitizedBody || defaultEmailContent.body; // Save to server (state updates happen in onSuccess callback) await saveEmailDataAsync({ workspaceId, subject: finalSubject, title: finalTitle, body: finalBody, });
π Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
π Files selected for processing (3)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx(5 hunks)apps/web/lib/api/programs/get-program-or-throw.ts(2 hunks)apps/web/lib/types.ts(2 hunks)
π§° Additional context used
π§ Learnings (4)
π Learning: 2025-10-17T08:18:19.278Z
Learnt from: devkiran
Repo: dubinc/dub PR: 0
File: :0-0
Timestamp: 2025-10-17T08:18:19.278Z
Learning: In the apps/web codebase, `@/lib/zod` should only be used for places that need OpenAPI extended zod schema. All other places should import from the standard `zod` package directly using `import { z } from "zod"`.
Applied to files:
apps/web/lib/types.ts
π Learning: 2025-07-30T15:25:13.936Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.936Z
Learning: In apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, the form schema uses partial condition objects to allow users to add empty/unconfigured condition fields without type errors, while submission validation uses strict schemas to ensure data integrity. This two-stage validation pattern improves UX by allowing progressive completion of complex forms.
Applied to files:
apps/web/lib/types.ts
π Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
π Learning: 2025-10-06T15:48:14.205Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2935
File: apps/web/lib/actions/partners/invite-partner-from-network.ts:21-28
Timestamp: 2025-10-06T15:48:14.205Z
Learning: For the network invites limit check in apps/web/lib/actions/partners/invite-partner-from-network.ts, the team accepts that concurrent invites may bypass the limit due to race conditions. Perfect atomicity is not required for this feature.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
𧬠Code graph analysis (3)
apps/web/lib/types.ts (1)
apps/web/lib/zod/schemas/program-invite-email.ts (1)
programInviteEmailDataSchema(3-9)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (7)
apps/web/lib/swr/use-program.ts (1)
useProgram(6-40)apps/web/lib/types.ts (2)
ProgramProps(467-467)ProgramInviteEmailData(469-471)apps/web/lib/swr/use-workspace.ts (1)
useWorkspace(6-46)apps/web/lib/actions/partners/invite-partner.ts (1)
invitePartnerAction(15-133)apps/web/lib/actions/partners/save-invite-email-data.ts (1)
saveInviteEmailDataAction(16-45)packages/ui/src/rich-text-area/rich-text-provider.tsx (1)
RichTextProvider(69-242)packages/ui/src/rich-text-area/rich-text-toolbar.tsx (1)
RichTextToolbar(16-129)
apps/web/lib/api/programs/get-program-or-throw.ts (2)
apps/web/lib/zod/schemas/programs.ts (1)
ProgramSchema(25-52)apps/web/lib/zod/schemas/program-invite-email.ts (1)
programInviteEmailDataSchema(3-9)
β° Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
π Additional comments (10)
apps/web/lib/types.ts (2)
100-100: LGTM!The import follows the correct pattern for local schema files.
469-471: LGTM!The type definition is clean and follows the established patterns in this file.
apps/web/lib/api/programs/get-program-or-throw.ts (2)
1-1: LGTM!The import is appropriately placed and required for the schema extension below.
26-28: LGTM!The schema extension correctly adds the
inviteEmailDatafield with runtime validation.apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (6)
2-2: LGTM!The new imports are appropriate for the email template editing functionality.
Also applies to: 13-15, 19-19, 21-28
46-83: LGTM!The email content state management is well-structured with appropriate use of
useMemofor derived values and clear separation between draft and committed content.
110-128: LGTM!The save action handler properly synchronizes local state with the server response and manages the editing flow.
370-403: LGTM!The conditional rendering of edit controls provides a clear and intuitive editing experience.
465-503: LGTM!The RichText editor integration is well-implemented with appropriate features and excellent keyboard shortcut support (Cmd/Ctrl+Enter to save).
508-553: LGTM!The preview mode provides a realistic representation of the email with proper markdown rendering.
Enables editing of subject, title, and body for partner invite emails in the dashboard. Updates the invite action and schema to accept custom email fields, and updates the email template to render user-provided markdown content. This allows program admins to personalize partner invitation emails.
CleanShot.2025-11-05.at.22.12.07.mp4
Summary by CodeRabbit