Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@TWilson023
Copy link
Collaborator

@TWilson023 TWilson023 commented Nov 4, 2025

  • Fixes image max widths that were previously ignored by some email clients
  • Hides the Dub logo from the top of campaign emails, since they already have program logos
  • Allows customizing email preview text

Summary by CodeRabbit

  • New Features

    • Added campaign preview field across UI, API, editor, and email sending so you can set/clear custom preview text.
    • Campaign editor gets a toggleable Preview input with focus/blur behavior.
    • Email templates now render preview text when present; image rendering improved for responsive sizing.
  • Tests / Chores

    • Persisted preview field in schema and updated tests to expect preview (nullable).

@vercel
Copy link
Contributor

vercel bot commented Nov 4, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Updated (UTC)
dub Ready Ready Preview Nov 5, 2025 10:27pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 4, 2025

Walkthrough

This PR adds a new optional preview field to campaigns and wires it through the database schema, Zod validation, API update and broadcast routes, editor UI, preview email actions/workflows, and email templates; also adjusts HTML sanitization and image styling for rendered campaign HTML.

Changes

Cohort / File(s) Summary
Database Schema
packages/prisma/schema/campaign.prisma
Added optional preview String? field to Campaign model
Validation Schemas
apps/web/lib/zod/schemas/campaigns.ts
Added preview to CampaignSchema (z.string().nullable().default(null)) and updateCampaignSchema (z.string().nullish())
API Update Route
apps/web/app/(ee)/api/campaigns/[campaignId]/route.ts
PATCH handler now accepts preview and conditionally includes it in the update payload (allows explicit set/clear)
Broadcast / Cron Route & Validation
apps/web/app/(ee)/api/cron/campaigns/broadcast/route.ts, apps/web/lib/api/campaigns/validate-campaign.ts
Broadcast data now passes campaign.preview into email payload; validation editability check includes input.preview
Editor UI & Preview Modal
apps/web/app/.../campaign-editor.tsx, apps/web/app/.../send-email-preview-modal.tsx
Editor: added preview input, toggle, auto-focus, animated reveal/hide, wired preview into form defaultValues and useWatch; preview modal: useWatch now includes preview and preview passed to sendPreviewEmail
Send Preview Action & Workflow
apps/web/lib/actions/campaigns/send-campaign-preview-email.ts, apps/web/lib/api/workflows/execute-send-campaign-workflow.ts
Validation/schema for send preview accepts optional preview; payloads and message creation now include campaign.preview (used by template) instead of campaign.subject
Email Template & HTML Rendering
packages/email/src/templates/campaign-email.tsx, apps/web/lib/api/workflows/render-campaign-email-html.ts
Template now uses campaign.preview (conditionally rendered) and removed header wordmark; render pipeline adds Image HTMLAttributes (inline max-width/height/margin) and allows style on img in sanitizeHtml
Tests
apps/web/tests/campaigns/index.test.ts
Updated expected campaign shape to include preview: null
Scripts / Misc
apps/web/scripts/perplexity/ban-partners.ts
Adjusted import path, excluded status: "banned" in filter, increased paginator take to 200, and added debug logging

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Editor as Campaign Editor UI
    participant API as PATCH /api/campaigns/[id]
    participant DB as Database
    participant PreviewSvc as Send Preview Action
    participant Template as Campaign Email Template

    User->>Editor: Edit subject and/or toggle/set preview
    Editor->>API: PATCH with { subject?, preview? }
    API->>DB: Update campaign (preview included when provided)
    DB-->>API: Update result

    User->>Editor: Click "Send preview"
    Editor->>PreviewSvc: sendPreviewEmail({ subject, preview, body, from })
    PreviewSvc->>Template: render with campaign.preview (if present)
    Template-->>User: Preview email rendered/sent
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Areas requiring extra attention:

  • campaign-editor.tsx: preview state, focus/blur behavior, ConditionalColumn animation and form integration
  • packages/email/src/templates/campaign-email.tsx: template restructuring and conditional preview rendering; removal of wordmark
  • Email rendering pipeline: sanitizeHtml style allowance and Image extension changes affecting output

Possibly related PRs

Suggested reviewers

  • devkiran

Poem

πŸ‡
A little preview hops in line,
A snippet bright, a tiny sign.
Editors toggle, templates singβ€”
A rabbit sends the preview spring! βœ¨πŸ“§

Pre-merge checks and finishing touches

βœ… Passed checks (2 passed)
Check name Status Explanation
Description Check βœ… Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check βœ… Passed The title 'Email campaign updates' directly relates to the main changes in this PR, which involve updating email campaign functionality including preview text customization, image rendering fixes, and logo removal.
✨ Finishing touches
  • πŸ“ Generate docstrings
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch email-campaign-updates

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.

❀️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@TWilson023 TWilson023 marked this pull request as ready for review November 5, 2025 21:36
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 77a8191 and 972ffc3.

πŸ“’ Files selected for processing (11)
  • apps/web/app/(ee)/api/campaigns/[campaignId]/route.ts (2 hunks)
  • apps/web/app/(ee)/api/cron/campaigns/broadcast/route.ts (1 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor.tsx (9 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/send-email-preview-modal.tsx (2 hunks)
  • apps/web/lib/actions/campaigns/send-campaign-preview-email.ts (3 hunks)
  • apps/web/lib/api/campaigns/validate-campaign.ts (1 hunks)
  • apps/web/lib/api/workflows/execute-send-campaign-workflow.ts (1 hunks)
  • apps/web/lib/api/workflows/render-campaign-email-html.ts (2 hunks)
  • apps/web/lib/zod/schemas/campaigns.ts (2 hunks)
  • packages/email/src/templates/campaign-email.tsx (3 hunks)
  • packages/prisma/schema/campaign.prisma (1 hunks)
🧰 Additional context used
🧠 Learnings (7)
πŸ““ Common learnings
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.
πŸ“š 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:

  • packages/email/src/templates/campaign-email.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor.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/campaigns/[campaignId]/campaign-editor.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/campaigns/[campaignId]/campaign-editor.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/campaigns/[campaignId]/campaign-editor.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/campaigns/[campaignId]/campaign-editor.tsx
πŸ“š Learning: 2025-10-08T21:33:23.553Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2936
File: apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/add-hostname-modal.tsx:28-34
Timestamp: 2025-10-08T21:33:23.553Z
Learning: In the dub/ui Button component, when the `disabledTooltip` prop is set to a non-undefined value (e.g., a string), the button is automatically disabled. Therefore, it's not necessary to also add the same condition to the `disabled` propβ€”setting `disabledTooltip={permissionsError || undefined}` is sufficient to disable the button when there's a permissions error.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor.tsx
🧬 Code graph analysis (1)
packages/email/src/templates/campaign-email.tsx (1)
packages/email/src/react-email.d.ts (3)
  • Html (4-4)
  • Head (5-5)
  • Preview (18-18)
πŸͺ› Biome (2.1.2)
packages/email/src/templates/campaign-email.tsx

[error] 78-78: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

⏰ 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 (12)
apps/web/lib/api/workflows/render-campaign-email-html.ts (2)

23-27: LGTM! Image styling enhancement.

The inline styles ensure responsive image rendering in email clients, which is best practice since many email clients have limited CSS support.


91-91: LGTM! Sanitization updated correctly.

The style attribute is now allowed for img tags, which is necessary for the inline styles added in the Image extension configuration.

apps/web/lib/actions/campaigns/send-campaign-preview-email.ts (2)

19-19: LGTM! Schema validation for preview field.

The nullish() validator correctly allows both null and undefined, matching the optional nature of the preview field in the Prisma schema.


36-37: LGTM! Preview field wired through correctly.

The preview value is extracted from the input and passed to the CampaignEmail template under campaign.preview, aligning with the data shape expected by the email template.

Also applies to: 71-71

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/send-email-preview-modal.tsx (2)

30-33: LGTM! Form field watching updated correctly.

The useWatch hook now tracks the preview field alongside subject, bodyJson, and from, ensuring the modal has access to the latest preview value from the form.


84-84: LGTM! Preview passed to action.

The preview value is correctly passed to the sendEmailPreview action, completing the data flow from form to email sending.

apps/web/app/(ee)/api/cron/campaigns/broadcast/route.ts (1)

229-229: LGTM! Preview field added to email template data.

The preview field is now passed to the CampaignEmail template within the campaign object, enabling email clients to display preview text. Note that the email subject line (line 219) correctly continues to use campaign.subject.

apps/web/app/(ee)/api/campaigns/[campaignId]/route.ts (2)

67-67: LGTM! Preview extracted from validated input.

The preview field is correctly extracted from the validated campaign input alongside other editable fields.


125-125: Review comment is based on a misunderstanding of the schema design.

The update patterns are intentional and correct. The zod updateCampaignSchema explicitly defines:

  • subject as z.string() (not nullable) β€” cannot be set to null
  • preview as z.string().nullable() β€” can be set to null
  • scheduledAt as parseDateSchema.nullish() β€” can be set to null

The update logic correctly reflects these constraints:

  • subject && { subject } only updates for truthy values (correct for non-nullable field)
  • preview !== undefined && { preview } allows explicit null (correct for nullable field)
  • scheduledAt !== undefined && { scheduledAt } allows explicit null (correct for nullish field)

No changes needed.

Likely an incorrect or invalid review comment.

apps/web/lib/api/campaigns/validate-campaign.ts (1)

37-37: LGTM! Validation includes preview field.

The preview field is correctly included in the editability check, ensuring it follows the same status-based editing rules as other campaign content fields like subject and bodyJson.

apps/web/lib/api/workflows/execute-send-campaign-workflow.ts (1)

195-195: LGTM! Preview field added to workflow email template data.

The preview field is now passed to the CampaignEmail template within the campaign object. Note that both the message subject (line 163) and email subject (line 184) correctly continue to use campaign.subject, maintaining consistency with the broadcast route.

packages/prisma/schema/campaign.prisma (1)

30-30: Schema change verified; migration status unclearβ€”requires manual verification.

The preview String? field is correctly added to the Campaign model (line 26) and positioned logically after subject. However, the migrations directory does not exist at the standard Prisma location (packages/prisma/migrations/), making it impossible to verify whether a migration has been generated and applied. Confirm that Prisma migrations have been properly created for this schema change before deployment.

@TWilson023 TWilson023 requested a review from steven-tey November 5, 2025 22:09
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

🧹 Nitpick comments (1)
apps/web/scripts/perplexity/ban-partners.ts (1)

8-8: Maintain import consistency with absolute paths.

The codebase uses absolute imports with the @/ alias prefix (lines 1, 3), but this change introduces a relative path. For consistency and maintainability, consider using the absolute import pattern instead.

Apply this diff to align with the project's import convention:

-import { syncTotalCommissions } from "../../lib/api/partners/sync-total-commissions";
+import { syncTotalCommissions } from "@/lib/api/partners/sync-total-commissions";
πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 972ffc3 and 938af45.

πŸ“’ Files selected for processing (4)
  • apps/web/lib/zod/schemas/campaigns.ts (2 hunks)
  • apps/web/scripts/perplexity/ban-partners.ts (3 hunks)
  • apps/web/tests/campaigns/index.test.ts (1 hunks)
  • packages/prisma/schema/campaign.prisma (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/prisma/schema/campaign.prisma
🧰 Additional context used
🧠 Learnings (3)
πŸ““ Common learnings
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.
πŸ“š Learning: 2025-06-06T07:59:03.120Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2177
File: apps/web/lib/api/links/bulk-create-links.ts:66-84
Timestamp: 2025-06-06T07:59:03.120Z
Learning: In apps/web/lib/api/links/bulk-create-links.ts, the team accepts the risk of potential undefined results from links.find() operations when building invalidLinks arrays, because existing links are fetched from the database based on the input links, so matches are expected to always exist.

Applied to files:

  • apps/web/scripts/perplexity/ban-partners.ts
πŸ“š Learning: 2025-09-24T15:50:16.414Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.

Applied to files:

  • apps/web/lib/zod/schemas/campaigns.ts
⏰ 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/campaigns.ts (2)

53-53: LGTM! Correct use of .default(null) in the main schema.

Using .default(null) here ensures the preview field is always present when parsing campaign objects, which is appropriate for the main CampaignSchema.


88-88: Previous concern addressed! Correct use of .nullish() for PATCH.

The use of .nullish() (equivalent to .nullable().optional()) correctly handles the update schema. This ensures that:

  • Omitted preview in PATCH requests stays undefined (preserves existing value)
  • Explicit null clears the field
  • String value updates the field

This addresses the data loss concern from the previous review.

apps/web/tests/campaigns/index.test.ts (1)

36-36: LGTM! Test correctly reflects the new schema.

Adding preview: null to the expected campaign object correctly aligns with the updated CampaignSchema that includes the new preview field with a default value of null.

apps/web/scripts/perplexity/ban-partners.ts (4)

1-162: Verify that this file belongs in this PR.

This file contains changes to a partner banning script, which appears unrelated to the PR's stated objectives of fixing email campaign image widths, hiding the Dub logo, and adding preview text customization. Please confirm whether these changes were intentionally included or if this file was modified by mistake.


41-43: Good defensive filter to prevent re-banning.

The status filter ensures already-banned partners are excluded from processing, making the script safer to re-run and preventing duplicate ban operations.


133-134: LGTM!

The debug logging is helpful for monitoring script execution and is consistent with the existing logging pattern throughout the file.


49-49: Add pagination or document the 200-partner limit.

The concern is valid. This script processes only the first 200 partners from the CSV without pagination. If the CSV contains more matches, they won't be banned. While this is an admin one-off script, it will silently process a truncated set if the CSV grows beyond 200 entries.

Consider either:

  1. Adding a loop with skip/offset to process all partners in batches
  2. Adding a check after the query: if (programEnrollments.length === 200) { console.warn("WARNING: 200 partners processed; may be more to ban"); }
  3. Documenting that this script intentionally processes max 200 partners per run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants