-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Email campaigns #2717
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
Email campaigns #2717
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughAdds gated Email Campaigns: DB/schema and type updates, new campaign APIs and server actions, TipTap-based rich text editor and renderers, many frontend campaign components/hooks/pages/modals, renamed workflow export Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant FE as Frontend (React + TipTap)
participant API as Next API (withWorkspace)
participant DB as Prisma
participant R as Renderer (renderCampaignEmail*)
participant M as Mailer/Storage
User->>FE: Edit campaign content
FE->>API: PATCH /api/campaigns/{id}
API->>DB: getCampaignOrThrow -> update (transaction)
DB-->>API: updated campaign
API-->>FE: CampaignSchema response
User->>FE: Request email preview (addresses)
FE->>API: action sendCampaignPreviewEmail
API->>DB: fetch Program & Campaign
API->>R: renderCampaignEmailMarkdown/HTML + interpolateEmailTemplate
R-->>API: rendered HTML/text
API->>M: send CampaignEmail (mailer)
M-->>API: success
API-->>FE: success
User->>FE: Create draft
FE->>API: POST /api/campaigns
API->>DB: create campaign (+ optional workflow)
DB-->>API: new campaign id
API-->>FE: { id }
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 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: 0
♻️ Duplicate comments (2)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor.tsx (2)
323-323: Remove non-null assertion or add guard.The non-null assertion
workspaceId!is unsafe ifworkspaceIdcan beundefined. Consider adding a guard or early return.Apply this diff to add a defensive check:
uploadImage={async (file) => { try { + if (!workspaceId) { + toast.error("Workspace not found"); + return null; + } + const result = await executeImageUpload({ - workspaceId: workspaceId!, + workspaceId, });
337-337: Remove forbidden Content-Length header in browser PUT.Browsers disallow setting
Content-Length; it's ignored or can cause issues with some signed URLs. Let the browser set it automatically.Apply this diff:
const uploadResponse = await fetch(signedUrl, { method: "PUT", body: file, headers: { "Content-Type": file.type, - "Content-Length": file.size.toString(), }, });
🧹 Nitpick comments (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor.tsx (1)
148-156: Consider removing unused dependencies from the callback.The
saveCampaigndependency array includesisSavingCampaignandwatch, but these values are not used within the callback body. Removing them will prevent unnecessary callback recreation.Apply this diff:
[ - isSavingCampaign, getValues, dirtyFields, - watch, makeRequest, campaign.id, reset, ],
📜 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/campaigns/[campaignId]/campaign-editor.tsx(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor.tsx (9)
apps/web/lib/types.ts (2)
Campaign(661-661)UpdateCampaignFormData(663-663)apps/web/lib/swr/use-program.ts (1)
useProgram(6-40)apps/web/lib/swr/use-api-mutation.ts (1)
useApiMutation(33-108)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/utils.ts (1)
isValidTriggerCondition(4-36)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-groups-selector.tsx (1)
CampaignGroupsSelector(17-125)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/transactional-campaign-logic.tsx (1)
TransactionalCampaignLogic(14-88)packages/ui/src/rich-text-area/index.tsx (1)
RichTextArea(35-198)apps/web/lib/zod/schemas/campaigns.ts (1)
EMAIL_TEMPLATE_VARIABLES(16-19)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-action-bar.tsx (1)
CampaignActionBar(15-78)
⏰ 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
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 (3)
apps/web/app/(ee)/api/campaigns/[campaignId]/route.ts (2)
36-43: Guard bodyJson to never be null before parsingAvoid 500s when DB bodyJson is null by defaulting it (same in PATCH response).
+import { DEFAULT_CAMPAIGN_BODY } from "@/lib/api/campaigns/constants"; @@ -const parsedCampaign = CampaignSchema.parse({ +const parsedCampaign = CampaignSchema.parse({ ...campaign, + bodyJson: campaign.bodyJson ?? DEFAULT_CAMPAIGN_BODY, groups: campaign.groups.map(({ groupId }) => ({ id: groupId })), triggerCondition: campaign.workflow?.triggerConditions?.[0], });And later:
-const response = CampaignSchema.parse({ +const response = CampaignSchema.parse({ ...updatedCampaign, + bodyJson: updatedCampaign.bodyJson ?? DEFAULT_CAMPAIGN_BODY, groups: updatedCampaign.groups.map(({ groupId }) => ({ id: groupId })), triggerCondition: updatedCampaign.workflow?.triggerConditions?.[0], });
215-226: Delete route: gate by trigger schedule and handle QStash errorsSkip deletion when trigger has no schedule, not by attribute, and catch errors.
- const { condition } = parseWorkflowConfig(campaign.workflow); - - if (condition.attribute === "partnerJoined") { - return; - } - - await qstash.schedules.delete(campaign.workflow.id); + const hasSchedule = Boolean( + WORKFLOW_SCHEDULES[campaign.workflow.trigger], + ); + if (!hasSchedule) return; + try { + await qstash.schedules.delete(campaign.workflow.id); + } catch (error) { + console.warn("Failed to delete schedule:", error); + }apps/web/lib/api/workflows/execute-send-campaign-workflow.ts (1)
311-332: Operator from condition is ignored in SQL filtering.
buildEnrollmentWherehardcodes thegteoperator for numeric attributes (lines 319-322), ignoringcondition.operator. This means if a workflow condition uses a different operator (e.g.,lte,eq,neq), the SQL-level filtering will still usegte, producing incorrect results.For example, if a condition specifies "totalClicks ≤ 10", the generated query would still use
totalClicks >= 10.Apply this diff to respect the condition operator:
+function getOperatorCondition(operator: string, value: number) { + switch (operator) { + case 'gte': + return { gte: value }; + case 'gt': + return { gt: value }; + case 'lte': + return { lte: value }; + case 'lt': + return { lt: value }; + case 'eq': + return { equals: value }; + case 'neq': + return { not: value }; + default: + throw new Error(`Unsupported operator: ${operator}`); + } +} + function buildEnrollmentWhere(condition: WorkflowCondition) { switch (condition.attribute) { case "totalClicks": case "totalLeads": case "totalConversions": case "totalSales": case "totalSaleAmount": case "totalCommissions": return { - [condition.attribute]: { - gte: condition.value, - }, + [condition.attribute]: getOperatorCondition(condition.operator, condition.value), }; case "partnerEnrolledDays": case "partnerJoined": + // For date-based conditions, we need to handle operators appropriately + const targetDate = subDays(new Date(), condition.value); + switch (condition.operator) { + case 'gte': + return { createdAt: { lte: targetDate } }; // More than X days ago + case 'lte': + return { createdAt: { gte: targetDate } }; // Less than X days ago + case 'eq': + // For equality, use a date range (same day) + const nextDay = new Date(targetDate); + nextDay.setDate(nextDay.getDate() + 1); + return { + createdAt: { + gte: targetDate, + lt: nextDay, + }, + }; + default: + throw new Error(`Unsupported date operator: ${condition.operator}`); + } - return { - createdAt: { - lte: subDays(new Date(), condition.value), - }, - }; } }
♻️ Duplicate comments (5)
apps/web/app/(ee)/api/campaigns/[campaignId]/route.ts (2)
86-103: Fix: create workflow on add, delete/detach on clear, and link campaign to new workflowCurrently only updates existing workflows. Adding a trigger to a campaign without a workflow does nothing; clearing a trigger leaves the old workflow firing. Handle both cases and update campaign.workflowId accordingly.
const updatedCampaign = await prisma.$transaction(async (tx) => { - if (campaign.workflowId) { + let newWorkflowId: string | null = null; + + if (campaign.workflowId && triggerCondition === null) { + await tx.workflow.delete({ where: { id: campaign.workflowId } }); + } else if (campaign.workflowId) { await tx.workflow.update({ where: { id: campaign.workflowId, }, data: { - ...(triggerCondition && { + ...(triggerCondition && { triggerConditions: [triggerCondition], trigger: WORKFLOW_ATTRIBUTE_TRIGGER[triggerCondition.attribute], }), ...(status && { disabledAt: status === "paused" ? new Date() : null, }), }, }); + } else if (triggerCondition) { + const created = await tx.workflow.create({ + data: { + programId, + trigger: WORKFLOW_ATTRIBUTE_TRIGGER[triggerCondition.attribute], + triggerConditions: [triggerCondition], + actions: [{ type: "sendCampaign", data: { campaignId } }], + }, + }); + newWorkflowId = created.id; } return await tx.campaign.update({ @@ data: { ...(name && { name }), ...(subject && { subject }), ...(status && { status }), ...(bodyJson && { bodyJson }), + ...(triggerCondition === null && campaign.workflowId + ? { workflowId: null } + : {}), + ...(newWorkflowId && { workflowId: newWorkflowId }), ...(shouldUpdateGroups && { groups: { deleteMany: {}, ...(updatedPartnerGroups && updatedPartnerGroups.length > 0 && { create: updatedPartnerGroups.map((group) => ({ groupId: group.id, })), }), }, }), },Also applies to: 104-131
159-167: Add error handling for QStash schedule create/deleteSchedule ops fail when schedule exists/absent; currently unhandled in waitUntil.
- if (shouldSchedule) { - await qstash.schedules.create({ + if (shouldSchedule) { + try { + await qstash.schedules.create({ destination: `${APP_DOMAIN_WITH_NGROK}/api/cron/workflows/${updatedCampaign.workflow.id}`, cron: cronSchedule, scheduleId: updatedCampaign.workflow.id, - }); + }); + } catch (error) { + console.warn("Failed to create schedule:", error); + } } else if (shouldDeleteSchedule) { - await qstash.schedules.delete(updatedCampaign.workflow.id); + try { + await qstash.schedules.delete(updatedCampaign.workflow.id); + } catch (error) { + console.warn("Failed to delete schedule:", error); + } }apps/web/lib/zod/schemas/workflows.ts (1)
76-81: Allow null value in workflowConditionSchemaUI assigns null to value; z.number() rejects it, causing runtime parse errors.
export const workflowConditionSchema = z.object({ attribute: z.enum(WORKFLOW_ATTRIBUTES), operator: z.enum(WORKFLOW_COMPARISON_OPERATORS).default("gte"), - value: z.number(), + value: z.number().nullable(), });apps/web/lib/api/workflows/execute-send-campaign-workflow.ts (2)
139-145: Add error handling for markdown rendering.Past reviews flagged that
renderCampaignEmailMarkdowncan throw ifcampaign.bodyJsonis malformed. Without error handling, one invalid campaign body aborts message creation for the entire chunk (up to 100 partners), losing all workflow progress for that batch.Additionally, the
as unknown as TiptapNodecast bypasses type validation, increasing the risk of runtime failures.Wrap the rendering in error handling to isolate failures to individual partners:
const messages = await prisma.message.createMany({ - data: programEnrollmentChunk.map((programEnrollment) => ({ + data: programEnrollmentChunk + .map((programEnrollment) => { + try { + return { + id: createId({ prefix: "msg_" }), + programId: programEnrollment.programId, + partnerId: programEnrollment.partnerId, + senderUserId: campaign.userId, + type: "campaign", + subject: campaign.subject, + text: renderCampaignEmailMarkdown({ + content: campaign.bodyJson as unknown as TiptapNode, + variables: { + PartnerName: programEnrollment.partner.name, + PartnerEmail: programEnrollment.partner.email, + }, + }), + }; + } catch (error) { + console.error( + `Failed to render markdown for partner ${programEnrollment.partnerId}:`, + error + ); + return null; + } + }) + .filter((data): data is NonNullable<typeof data> => data !== null), - id: createId({ prefix: "msg_" }), - programId: programEnrollment.programId, - partnerId: programEnrollment.partnerId, - senderUserId: campaign.userId, - type: "campaign", - subject: campaign.subject, - text: renderCampaignEmailMarkdown({ - content: campaign.bodyJson as unknown as TiptapNode, - variables: { - PartnerName: programEnrollment.partner.name, - PartnerEmail: programEnrollment.partner.email, - }, - }), - })), });
171-177: Add error handling for HTML generation.
renderCampaignEmailHTMLcan throw ifcampaign.bodyJsonis malformed. Since messages are already created at line 131, a failure here leaves the system in an inconsistent state: messages exist in the database but emails were never sent to partners.The
as unknown as TiptapNodecast bypasses type safety, increasing the likelihood of runtime errors.Wrap HTML generation in error handling to prevent inconsistent state:
+function safeRenderCampaignHTML( + bodyJson: unknown, + variables: { PartnerName: string; PartnerEmail: string } +): string { + try { + return renderCampaignEmailHTML({ + content: bodyJson as unknown as TiptapNode, + variables, + }); + } catch (error) { + console.error('Failed to render campaign HTML:', error); + return '<p>Unable to render campaign content</p>'; + } +} + const { data } = await sendBatchEmail( partnerUsers.map((partnerUser) => ({ variant: "notifications", to: partnerUser.email!, subject: campaign.subject, react: CampaignEmail({ program: { name: program.name, slug: program.slug, logo: program.logo, messagingEnabledAt: program.messagingEnabledAt, }, campaign: { type: campaign.type, subject: campaign.subject, - body: renderCampaignEmailHTML({ - content: campaign.bodyJson as unknown as TiptapNode, - variables: { - PartnerName: partnerUser.partner.name, - PartnerEmail: partnerUser.partner.email, - }, - }), + body: safeRenderCampaignHTML( + campaign.bodyJson, + { + PartnerName: partnerUser.partner.name, + PartnerEmail: partnerUser.partner.email, + } + ), }, }), tags: [{ name: "type", value: "notification-email" }], headers: { "Idempotency-Key": `${campaign.id}-${partnerUser.id}`, }, })), );
🧹 Nitpick comments (1)
apps/web/lib/zod/schemas/campaigns.ts (1)
21-58: Unify labels with central workflow labels to avoid driftLabels here are lowercase and can diverge from WORKFLOW_ATTRIBUTE_LABELS (Title Case). Source labels from the single mapping to keep UI consistent.
Apply:
+import { WORKFLOW_ATTRIBUTE_LABELS } from "./workflows"; @@ totalClicks: { - label: "clicks", + label: WORKFLOW_ATTRIBUTE_LABELS.totalClicks, inputType: "number", }, totalLeads: { - label: "leads", + label: WORKFLOW_ATTRIBUTE_LABELS.totalLeads, inputType: "number", }, totalConversions: { - label: "conversions", + label: WORKFLOW_ATTRIBUTE_LABELS.totalConversions, inputType: "number", }, totalSales: { - label: "sales", + label: WORKFLOW_ATTRIBUTE_LABELS.totalSales, inputType: "number", }, totalSaleAmount: { - label: "revenue", + label: WORKFLOW_ATTRIBUTE_LABELS.totalSaleAmount, inputType: "currency", }, totalCommissions: { - label: "commissions", + label: WORKFLOW_ATTRIBUTE_LABELS.totalCommissions, inputType: "currency", }, partnerEnrolledDays: { - label: "enrollment duration", + label: WORKFLOW_ATTRIBUTE_LABELS.partnerEnrolledDays, inputType: "dropdown", dropdownValues: [1, 3, 7, 14, 30], }, partnerJoined: { - label: "joins the program", + label: WORKFLOW_ATTRIBUTE_LABELS.partnerJoined, inputType: "none", },
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
apps/web/app/(ee)/api/campaigns/[campaignId]/route.ts(6 hunks)apps/web/lib/api/workflows/execute-send-campaign-workflow.ts(5 hunks)apps/web/lib/api/workflows/execute-workflows.ts(4 hunks)apps/web/lib/api/workflows/utils.ts(1 hunks)apps/web/lib/zod/schemas/campaigns.ts(2 hunks)apps/web/lib/zod/schemas/workflows.ts(2 hunks)packages/prisma/schema/workflow.prisma(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-25T17:33:45.072Z
Learnt from: devkiran
PR: dubinc/dub#2736
File: apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts:12-12
Timestamp: 2025-08-25T17:33:45.072Z
Learning: The WorkflowTrigger enum in packages/prisma/schema/workflow.prisma contains three values: leadRecorded, saleRecorded, and commissionEarned. All three are properly used throughout the codebase.
Applied to files:
packages/prisma/schema/workflow.prisma
🧬 Code graph analysis (6)
apps/web/lib/api/workflows/utils.ts (2)
apps/web/lib/types.ts (1)
WorkflowConditionAttribute(625-625)apps/web/lib/api/workflows/parse-workflow-config.ts (1)
parseWorkflowConfig(8-30)
apps/web/lib/zod/schemas/workflows.ts (2)
apps/web/lib/types.ts (1)
WorkflowConditionAttribute(625-625)packages/prisma/client.ts (1)
WorkflowTrigger(32-32)
apps/web/lib/api/workflows/execute-send-campaign-workflow.ts (6)
apps/web/lib/api/workflows/render-campaign-email-markdown.ts (1)
renderCampaignEmailMarkdown(9-55)apps/web/lib/types.ts (3)
TiptapNode(674-680)WorkflowCondition(623-623)WorkflowConditionAttribute(625-625)packages/email/src/index.ts (1)
sendBatchEmail(31-67)packages/email/src/templates/campaign-email.tsx (1)
CampaignEmail(15-111)apps/web/lib/api/workflows/render-campaign-email-html.ts (1)
renderCampaignEmailHTML(10-69)apps/web/lib/api/workflows/execute-workflows.ts (1)
evaluateWorkflowCondition(101-130)
apps/web/lib/api/workflows/execute-workflows.ts (3)
apps/web/lib/api/workflows/execute-complete-bounty-workflow.ts (1)
executeCompleteBountyWorkflow(13-234)apps/web/lib/api/workflows/utils.ts (1)
isScheduledWorkflow(11-21)apps/web/lib/api/workflows/execute-send-campaign-workflow.ts (1)
executeSendCampaignWorkflow(21-209)
apps/web/app/(ee)/api/campaigns/[campaignId]/route.ts (6)
apps/web/lib/api/campaigns/get-campaign-or-throw.ts (1)
getCampaignOrThrow(4-40)apps/web/lib/zod/schemas/campaigns.ts (2)
updateCampaignSchema(92-108)CampaignSchema(60-71)apps/web/lib/api/utils.ts (1)
parseRequestBody(9-20)apps/web/lib/api/groups/throw-if-invalid-group-ids.ts (1)
throwIfInvalidGroupIds(5-37)apps/web/lib/zod/schemas/workflows.ts (2)
WORKFLOW_ATTRIBUTE_TRIGGER(34-46)WORKFLOW_SCHEDULES(50-53)apps/web/lib/api/workflows/utils.ts (1)
isScheduledWorkflow(11-21)
apps/web/lib/zod/schemas/campaigns.ts (5)
apps/web/lib/types.ts (2)
WorkflowAttribute(688-688)CampaignWorkflowAttributeConfig(682-686)apps/web/lib/zod/schemas/groups.ts (1)
GroupSchema(51-64)apps/web/lib/zod/schemas/workflows.ts (1)
workflowConditionSchema(77-81)apps/web/lib/zod/schemas/misc.ts (1)
getPaginationQuerySchema(32-55)apps/web/lib/zod/schemas/partners.ts (1)
EnrolledPartnerSchema(337-402)
⏰ 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 (11)
packages/prisma/schema/workflow.prisma (1)
2-4: Verified: new enum values are properly integrated throughout the codebase.The script output confirms that both
partnerEnrolledandclickRecorded:
- Are mapped in
WORKFLOW_ATTRIBUTE_TRIGGER(lines 44–45 in workflows.ts)- Have schedules defined in
WORKFLOW_SCHEDULES(lines 51–52)- Are referenced in webhook handlers and workflow execution routes
- Are fully integrated with no missing mappings or validators
The additions are sound and ready for use.
apps/web/lib/api/workflows/execute-workflows.ts (2)
31-32: Filtering disabled workflows LGTM
87-96: SendCampaign: skip scheduled workflows LGTMDeferring scheduled workflows via isScheduledWorkflow avoids double execution.
apps/web/lib/api/workflows/execute-send-campaign-workflow.ts (8)
1-19: LGTM! Imports are comprehensive.All imports are used appropriately throughout the workflow execution logic.
21-27: LGTM! Clean parameter destructuring.The function signature properly accepts workflow and optional context parameters with appropriate types.
58-61: LGTM! Good safeguard added.The status check prevents inactive campaigns from being sent, which is an appropriate business rule.
63-73: LGTM! Enrollment fetching refactored.The extraction of enrollment logic into a helper function improves code organization and reusability.
75-109: LGTM! Robust deduplication logic.The implementation correctly prevents duplicate campaign emails by querying previously sent emails and filtering them out using a Set for efficient lookups.
111-128: LGTM! Efficient batch processing setup.The chunking strategy (100 enrollments per chunk) and flattening of partner users with email filtering is implemented correctly.
186-208: LGTM! Notification tracking implemented correctly.The conditional creation of notification emails only when
dataexists ensures tracking records are only created for successfully sent emails.
211-309: Clarify the enrollment limit and consider fetching strategy.The
take: 50limit at line 307 restricts bulk enrollment queries to 50 partners per workflow execution. This may be intentional for rate limiting, but it means larger partner lists require multiple workflow runs.Additionally, there's an asymmetry between the single-partner and bulk paths:
- Single partner: Fetches full link stats, evaluates condition client-side with
evaluateWorkflowCondition- Bulk: Uses SQL-level filtering via
buildEnrollmentWhere, no client-side condition evaluationThis could lead to different behavior depending on whether a specific
partnerIdis provided in the context.Consider whether the 50-partner limit is sufficient for your use case. If campaigns need to reach more partners, you may need:
- Pagination or cursor-based fetching
- Multiple workflow executions scheduled over time
- A higher limit (with careful consideration of email sending rate limits)
Also verify that
buildEnrollmentWherecorrectly handles all workflow condition types thatevaluateWorkflowConditionsupports.
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
♻️ Duplicate comments (3)
apps/web/lib/zod/schemas/workflows.ts (1)
84-88: Schema still doesn't accept null values forvaluefield.The
valuefield remainsz.number()without.nullable(), but the codebase assignsnull as any(referenced in past reviews). This type mismatch bypasses TypeScript's type safety and can cause runtime errors.Apply this diff to allow null values:
export const workflowConditionSchema = z.object({ attribute: z.enum(WORKFLOW_ATTRIBUTES), operator: z.enum(WORKFLOW_COMPARISON_OPERATORS).default("gte"), - value: z.number(), + value: z.number().nullable(), });apps/web/lib/api/workflows/execute-send-campaign-workflow.ts (2)
142-148: Critical: Add error handling for markdown rendering failures.The
renderCampaignEmailMarkdowncall lacks error handling. Ifcampaign.bodyJsoncontains invalid structure, rendering will throw, causing message creation to fail for all partners in the chunk (up to 100). Additionally, theas unknown as TiptapNodecast bypasses type validation entirely.Wrap the rendering in error handling:
+function safeRenderMarkdown( + bodyJson: any, + variables: { PartnerName: string; PartnerEmail: string } +): string { + try { + return renderCampaignEmailMarkdown({ + content: bodyJson as unknown as TiptapNode, + variables, + }); + } catch (error) { + console.error('Failed to render campaign markdown:', error); + return `Campaign: ${variables.PartnerName}`; + } +} + const messages = await prisma.message.createMany({ data: programEnrollmentChunk.map((programEnrollment) => ({ id: createId({ prefix: "msg_" }), programId: programEnrollment.programId, partnerId: programEnrollment.partnerId, senderUserId: campaign.userId, type: "campaign", subject: campaign.subject, - text: renderCampaignEmailMarkdown({ - content: campaign.bodyJson as unknown as TiptapNode, - variables: { - PartnerName: programEnrollment.partner.name, - PartnerEmail: programEnrollment.partner.email, - }, - }), + text: safeRenderMarkdown(campaign.bodyJson, { + PartnerName: programEnrollment.partner.name, + PartnerEmail: programEnrollment.partner.email, + }), })), });
174-180: Critical: Add error handling for HTML generation failures.The
renderCampaignEmailHTMLcall lacks error handling. Ifcampaign.bodyJsoncontains invalid structure, HTML generation will throw, aborting email delivery for all partners in the chunk. Since messages are already created at line 134, this leaves the system in an inconsistent state—messages exist but emails were never sent. Theas unknown as TiptapNodecast bypasses type safety.Wrap the rendering in error handling:
+function safeRenderHTML( + bodyJson: any, + variables: { PartnerName: string; PartnerEmail: string } +): string { + try { + return renderCampaignEmailHTML({ + content: bodyJson as unknown as TiptapNode, + variables, + }); + } catch (error) { + console.error('Failed to render campaign HTML:', error); + return `<p>Campaign content for ${variables.PartnerName}</p>`; + } +} + const { data } = await sendBatchEmail( partnerUsers.map((partnerUser) => ({ variant: "notifications", to: partnerUser.email!, subject: campaign.subject, react: CampaignEmail({ program: { name: program.name, slug: program.slug, logo: program.logo, messagingEnabledAt: program.messagingEnabledAt, }, campaign: { type: campaign.type, subject: campaign.subject, - body: renderCampaignEmailHTML({ - content: campaign.bodyJson as unknown as TiptapNode, - variables: { - PartnerName: partnerUser.partner.name, - PartnerEmail: partnerUser.partner.email, - }, - }), + body: safeRenderHTML(campaign.bodyJson, { + PartnerName: partnerUser.partner.name, + PartnerEmail: partnerUser.partner.email, + }), }, }), tags: [{ name: "type", value: "notification-email" }], headers: { "Idempotency-Key": `${campaign.id}-${partnerUser.id}`, }, })), );
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
apps/web/lib/api/workflows/execute-send-campaign-workflow.ts(5 hunks)apps/web/lib/api/workflows/utils.ts(1 hunks)apps/web/lib/zod/schemas/workflows.ts(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
apps/web/lib/api/workflows/utils.ts (3)
apps/web/lib/types.ts (1)
WorkflowConditionAttribute(625-625)apps/web/lib/api/workflows/parse-workflow-config.ts (1)
parseWorkflowConfig(8-30)apps/web/lib/zod/schemas/workflows.ts (1)
SCHEDULED_WORKFLOW_TRIGGERS(50-54)
apps/web/lib/zod/schemas/workflows.ts (1)
apps/web/lib/types.ts (1)
WorkflowConditionAttribute(625-625)
apps/web/lib/api/workflows/execute-send-campaign-workflow.ts (6)
apps/web/lib/api/workflows/render-campaign-email-markdown.ts (1)
renderCampaignEmailMarkdown(9-55)apps/web/lib/types.ts (3)
TiptapNode(674-680)WorkflowCondition(623-623)WorkflowConditionAttribute(625-625)packages/email/src/index.ts (1)
sendBatchEmail(31-67)packages/email/src/templates/campaign-email.tsx (1)
CampaignEmail(15-111)apps/web/lib/api/workflows/render-campaign-email-html.ts (1)
renderCampaignEmailHTML(10-69)apps/web/lib/api/workflows/execute-workflows.ts (1)
evaluateWorkflowCondition(101-130)
⏰ 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 (5)
apps/web/lib/api/workflows/utils.ts (1)
12-20: Past critical issue has been resolved.The implementation now correctly derives scheduled status from
SCHEDULED_WORKFLOW_TRIGGERSby checking the workflow's trigger, which aligns withWORKFLOW_SCHEDULES. The special case forpartnerJoined(event-based) vspartnerEnrolledDays(scheduled) is appropriate, as both use thepartnerEnrolledtrigger but have different execution semantics.apps/web/lib/zod/schemas/workflows.ts (1)
56-60: LGTM! Schedules are now aligned.The
WORKFLOW_SCHEDULESmap now includescommissionEarned, resolving the past mismatch with scheduled detection logic in utils.apps/web/lib/api/workflows/execute-send-campaign-workflow.ts (3)
58-61: Good defensive check for campaign status.The early return for inactive campaigns prevents unnecessary processing and is a sensible safeguard.
214-313: Well-structured helper function with proper flow control.The
getProgramEnrollmentsfunction handles both single-partner and batch scenarios correctly, includes proper evaluation logic, and has appropriate early returns.
315-336: Clean mapping of conditions to database filters.The
buildEnrollmentWherefunction correctly maps numeric attributes togtefilters and date-based attributes tocreatedAtfilters usingsubDays.
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
♻️ Duplicate comments (2)
packages/prisma/schema/workflow.prisma (1)
2-4: Don't reorder existing Postgres enumsPostgres cannot reorder enum labels; Prisma migrations will only append. Moving
commissionEarnedand inserting new labels at the top will leave the database in the old order, causing drift/noisy diffs. Append new triggers after the existing ones instead of reordering them.enum WorkflowTrigger { - partnerEnrolled // scheduled - clickRecorded // scheduled - commissionEarned - leadRecorded - saleRecorded + leadRecorded + saleRecorded + commissionEarned + partnerEnrolled // scheduled + clickRecorded // scheduled }Based on learnings.
apps/web/lib/zod/schemas/workflows.ts (1)
67-69: Allow workflow condition values to be nullable
partnerJoinedis configured withinputType: "none", so clients won't send a numeric threshold. Keepingvalue: z.number()forces callers to coerce or castnull, which will either fail validation or rely onas any. Please permit null (and optionally default to it) so non-quantitative triggers can be represented safely.export const workflowConditionSchema = z.object({ attribute: z.enum(WORKFLOW_ATTRIBUTES), operator: z.enum(WORKFLOW_COMPARISON_OPERATORS).default("gte"), - value: z.number(), + value: z.number().nullable(), });If downstream logic assumes numbers, guard against
nullbefore comparing.
🧹 Nitpick comments (1)
apps/web/lib/api/bounties/performance-bounty-scope-attributes.ts (1)
1-9: Consider removing redundantas constassertion.The
as constassertion on line 9 is redundant when combined with the explicitRecord<..., string>type annotation. The explicit type takes precedence, so the values remain typed asstringrather than literal types like"Leads".If literal types are desired for stricter type safety, consider removing the explicit type annotation:
-export const PERFORMANCE_BOUNTY_SCOPE_ATTRIBUTES: Record< - "totalLeads" | "totalConversions" | "totalSaleAmount" | "totalCommissions", - string -> = { +export const PERFORMANCE_BOUNTY_SCOPE_ATTRIBUTES = { totalLeads: "Leads", totalConversions: "Conversions", totalSaleAmount: "Revenue", totalCommissions: "Commissions", } as const;If the current
stringtyping is intentional, remove theas constto avoid confusion:export const PERFORMANCE_BOUNTY_SCOPE_ATTRIBUTES: Record< "totalLeads" | "totalConversions" | "totalSaleAmount" | "totalCommissions", string > = { totalLeads: "Leads", totalConversions: "Conversions", totalSaleAmount: "Revenue", totalCommissions: "Commissions", -} as const; +};
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
apps/web/app/(ee)/api/bounties/[bountyId]/route.ts(2 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-submissions-table.tsx(2 hunks)apps/web/lib/api/bounties/generate-performance-bounty-name.ts(2 hunks)apps/web/lib/api/bounties/performance-bounty-scope-attributes.ts(1 hunks)apps/web/lib/api/workflows/execute-send-campaign-workflow.ts(5 hunks)apps/web/lib/zod/schemas/campaigns.ts(2 hunks)apps/web/lib/zod/schemas/workflows.ts(2 hunks)apps/web/ui/partners/bounties/bounty-logic.tsx(2 hunks)apps/web/ui/partners/bounties/bounty-performance.tsx(2 hunks)packages/prisma/schema/workflow.prisma(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/web/ui/partners/bounties/bounty-logic.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-25T17:33:45.072Z
Learnt from: devkiran
PR: dubinc/dub#2736
File: apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts:12-12
Timestamp: 2025-08-25T17:33:45.072Z
Learning: The WorkflowTrigger enum in packages/prisma/schema/workflow.prisma contains three values: leadRecorded, saleRecorded, and commissionEarned. All three are properly used throughout the codebase.
Applied to files:
packages/prisma/schema/workflow.prisma
🧬 Code graph analysis (7)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-submissions-table.tsx (1)
apps/web/lib/api/bounties/performance-bounty-scope-attributes.ts (1)
PERFORMANCE_BOUNTY_SCOPE_ATTRIBUTES(1-9)
apps/web/app/(ee)/api/bounties/[bountyId]/route.ts (1)
apps/web/lib/api/bounties/performance-bounty-scope-attributes.ts (1)
PERFORMANCE_BOUNTY_SCOPE_ATTRIBUTES(1-9)
apps/web/lib/api/bounties/generate-performance-bounty-name.ts (1)
apps/web/lib/api/bounties/performance-bounty-scope-attributes.ts (1)
PERFORMANCE_BOUNTY_SCOPE_ATTRIBUTES(1-9)
apps/web/lib/api/workflows/execute-send-campaign-workflow.ts (4)
apps/web/lib/api/workflows/render-campaign-email-markdown.ts (1)
renderCampaignEmailMarkdown(9-55)apps/web/lib/types.ts (3)
TiptapNode(674-680)WorkflowCondition(623-623)WorkflowConditionAttribute(625-625)apps/web/lib/api/workflows/render-campaign-email-html.ts (1)
renderCampaignEmailHTML(10-69)apps/web/lib/api/workflows/execute-workflows.ts (1)
evaluateWorkflowCondition(101-130)
apps/web/ui/partners/bounties/bounty-performance.tsx (1)
apps/web/lib/api/bounties/performance-bounty-scope-attributes.ts (1)
PERFORMANCE_BOUNTY_SCOPE_ATTRIBUTES(1-9)
apps/web/lib/zod/schemas/workflows.ts (2)
apps/web/lib/types.ts (1)
WorkflowConditionAttribute(625-625)packages/prisma/client.ts (1)
WorkflowTrigger(32-32)
apps/web/lib/zod/schemas/campaigns.ts (5)
apps/web/lib/types.ts (2)
WorkflowAttribute(688-688)CampaignWorkflowAttributeConfig(682-686)apps/web/lib/zod/schemas/groups.ts (1)
GroupSchema(51-64)apps/web/lib/zod/schemas/workflows.ts (1)
workflowConditionSchema(66-70)apps/web/lib/zod/schemas/misc.ts (1)
getPaginationQuerySchema(32-55)apps/web/lib/zod/schemas/partners.ts (1)
EnrolledPartnerSchema(337-402)
⏰ 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/ui/partners/bounties/bounty-performance.tsx (1)
1-1: LGTM!The import and usage of
PERFORMANCE_BOUNTY_SCOPE_ATTRIBUTESis correct, providing a more specific and type-safe mapping for bounty performance attributes.Also applies to: 28-31
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-submissions-table.tsx (1)
3-3: LGTM!The replacement of
WORKFLOW_ATTRIBUTE_LABELSwithPERFORMANCE_BOUNTY_SCOPE_ATTRIBUTESensures consistent, bounty-specific labeling across the UI.Also applies to: 79-81
apps/web/app/(ee)/api/bounties/[bountyId]/route.ts (1)
4-4: LGTM!The error message now correctly references the bounty-specific attribute labels, improving clarity for users encountering this validation error.
Also applies to: 127-127
apps/web/lib/api/bounties/generate-performance-bounty-name.ts (1)
4-4: LGTM!The attribute label lookup is now correctly sourced from the bounty-specific constant, ensuring consistency across the application.
Also applies to: 14-15
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
🧹 Nitpick comments (2)
apps/web/lib/api/partners/bulk-delete-partners.ts (2)
26-26: Fix typo: "finalParterIds" should be "finalPartnerIds".The variable name contains a typo ("Parter" instead of "Partner"). While this is pre-existing code, fixing it now improves maintainability, especially since the new deletion logic uses this variable.
Apply this diff to fix the typo:
- const finalParterIds = partners.map((partner) => partner.id); + const finalPartnerIds = partners.map((partner) => partner.id);Then update all references (lines 55, 63, 71, 79):
await prisma.commission.deleteMany({ where: { partnerId: { - in: finalParterIds, + in: finalPartnerIds, }, }, }); await prisma.payout.deleteMany({ where: { partnerId: { - in: finalParterIds, + in: finalPartnerIds, }, }, }); await prisma.notificationEmail.deleteMany({ where: { partnerId: { - in: finalParterIds, + in: finalPartnerIds, }, }, }); await prisma.partner.deleteMany({ where: { id: { - in: finalParterIds, + in: finalPartnerIds, }, }, });Also applies to: 70-71
4-4: Update comment to include notificationEmail.The comment lists entities deleted during bulk partner deletion but doesn't mention
notificationEmail. Update it to reflect the current implementation.Apply this diff:
-// bulk delete multiple partners and all associated links, customers, payouts, and commissions +// bulk delete multiple partners and all associated links, customers, payouts, commissions, and notification emails
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
apps/web/lib/api/partners/bulk-delete-partners.ts(1 hunks)apps/web/lib/api/workflows/execute-send-campaign-workflow.ts(5 hunks)apps/web/lib/api/workflows/utils.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/web/lib/api/workflows/utils.ts
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/lib/api/workflows/execute-send-campaign-workflow.ts (4)
apps/web/lib/api/workflows/render-campaign-email-markdown.ts (1)
renderCampaignEmailMarkdown(9-55)apps/web/lib/types.ts (3)
TiptapNode(674-680)WorkflowCondition(623-623)WorkflowConditionAttribute(625-625)apps/web/lib/api/workflows/render-campaign-email-html.ts (1)
renderCampaignEmailHTML(10-69)apps/web/lib/api/workflows/execute-workflows.ts (1)
evaluateWorkflowCondition(101-130)
⏰ 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/lib/api/partners/bulk-delete-partners.ts (1)
68-74: Verification passed: notificationEmail schema correctly includes partnerId as a foreign key.The NotificationEmail model (packages/prisma/schema/notification.prisma, line 15) defines
partnerIdas a String field with a proper foreign key relation to Partner with cascading delete enabled (line 25). The deletion logic correctly uses this field and maintains referential integrity by deleting related records before the parent.apps/web/lib/api/workflows/execute-send-campaign-workflow.ts (3)
58-61: LGTM! Good defensive programming.The campaign status check prevents workflow execution for inactive campaigns, avoiding unnecessary processing and potential confusion.
217-227: LGTM! Clean code organization.The
includePartnerUsershelper improves reusability and keeps the enrollment queries DRY.
229-339: Good refactoring, but past critical issues remain.The
getProgramEnrollmentshelper is a solid improvement that consolidates enrollment retrieval logic. However, past reviews identified critical issues in this function that must be addressed:
- Lines 249-266: The
findUniquecall withstatusandgroupIdfilters will throw a Prisma error at runtime. Switch tofindFirstas recommended in previous reviews.- Lines 272-301: The
partnerEnrolledDaysattribute is not populated in the context map, causing condition evaluation to fail for workflows using this attribute. Add it alongsidepartnerJoinedas suggested in previous reviews.Please address these issues before merging.
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
♻️ Duplicate comments (5)
apps/web/ui/layout/page-content/page-content-with-side-panel.tsx (1)
80-82: Restore visible scrollbars when enabling individual scrolling.
scrollbar-hidestrips the visual scrollbar, leaving keyboard/mouse users without any cue that the pane scrolls—exactly the accessibility problem raised in the earlier review. Swap it for a styled scrollbar (or only hide it on coarse pointers) so the scroll affordance remains visible.- individualScrolling && "scrollbar-hide min-h-0 overflow-y-auto", + individualScrolling && + "scrollbar-thin scrollbar-thumb-neutral-300 scrollbar-track-transparent min-h-0 overflow-y-auto",apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-controls.tsx (2)
139-141: Edge case: EmptygroupIdsarray passes validation.The current validation
groupIds === undefinedallows an empty array[]to bypass validation. If sending a campaign to zero groups is invalid, strengthen the check to also reject empty arrays.Apply this diff:
- if (groupIds === undefined) { + if (!groupIds || groupIds.length === 0) { return "Please select the groups you want to send this campaign to."; }
271-280: Missing popover close before duplication.Unlike other menu items (lines 247, 264, 288), the Duplicate action doesn't call
setOpenPopover(false)before executing, leaving the popover visually open during navigation to the duplicated campaign.Apply this diff:
<MenuItem as={Command.Item} icon={isDuplicatingCampaign ? LoadingCircle : Duplicate} disabled={isUpdatingCampaign || isDuplicatingCampaign} onSelect={() => { + setOpenPopover(false); handleCampaignDuplication(); }} >apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor.tsx (2)
323-329: Guard against missingworkspaceIdbefore upload
workspaceId!will throw if the hook hasn’t resolved yet (e.g., first render), so image uploads can crash instead of showing a friendly error. Add a defensive check and return early.uploadImage={async (file) => { try { + if (!workspaceId) { + toast.error("Workspace not found"); + return null; + } + const result = await executeImageUpload({ - workspaceId: workspaceId!, + workspaceId, });
333-339: Remove forbiddenContent-LengthheaderBrowsers ignore or reject manual
Content-Lengthheaders, and signed PUT URLs can fail when extra headers are sent. Let the browser compute it.const uploadResponse = await fetch(signedUrl, { method: "PUT", body: file, headers: { "Content-Type": file.type, - "Content-Length": file.size.toString(), }, });
📜 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/campaigns/[campaignId]/campaign-controls.tsx(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor-skeleton.tsx(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor.tsx(1 hunks)apps/web/ui/layout/page-content/page-content-with-side-panel.tsx(3 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-controls.tsx (8)
apps/web/lib/types.ts (2)
Campaign(661-661)UpdateCampaignFormData(663-663)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-form-context.tsx (1)
useCampaignFormContext(4-5)apps/web/lib/swr/use-api-mutation.ts (1)
useApiMutation(33-108)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/delete-campaign-modal.tsx (1)
useDeleteCampaignModal(88-110)apps/web/ui/modals/confirm-modal.tsx (1)
useConfirmModal(107-120)packages/prisma/client.ts (1)
CampaignStatus(8-8)packages/ui/src/icons/nucleo/media-pause.tsx (1)
MediaPause(3-29)packages/ui/src/icons/nucleo/media-play.tsx (1)
MediaPlay(3-22)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor-skeleton.tsx (1)
apps/web/ui/layout/page-content/index.tsx (1)
PageContent(10-39)
apps/web/ui/layout/page-content/page-content-with-side-panel.tsx (1)
apps/web/ui/layout/page-content/page-content-header.tsx (1)
PageContentHeaderProps(9-15)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor.tsx (5)
apps/web/lib/types.ts (2)
Campaign(661-661)UpdateCampaignFormData(663-663)apps/web/lib/swr/use-api-mutation.ts (1)
useApiMutation(33-108)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/utils.ts (1)
isValidTriggerCondition(4-36)packages/ui/src/rich-text-area/index.tsx (1)
RichTextArea(35-198)apps/web/lib/zod/schemas/campaigns.ts (1)
EMAIL_TEMPLATE_VARIABLES(12-15)
⏰ 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)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-controls.tsx (5)
1-56: LGTM: Component setup is well-structured.The imports, interface definition, and hook initialization follow React best practices. The use of custom hooks for modals and API mutations keeps the component clean and maintainable.
57-122: LGTM: Confirmation modals correctly capture latest form state.The modals properly use
getValues()within theironConfirmcallbacks, ensuring that the latest form data (includingbodyJson) is captured at invocation time rather than closing over stale values. This correctly addresses the closure concern from the previous review.
154-171: LGTM: Callback dependencies are correct.The
updateCampaigncallback doesn't close overbodyJsondirectly, andgetValuesis stable from React Hook Form, so the dependency array[makeRequest, campaign.id]is appropriate. Form values are properly captured viagetValues()at the call sites.
173-181: LGTM: Duplication logic is correct.The duplication handler properly creates a copy via the API, navigates to the new campaign, and invalidates the cache. The implementation is clean and follows expected patterns.
183-215: LGTM: Action button logic handles campaign states correctly.The status-based button configuration is clean and comprehensive, correctly mapping
draft→ Publish,active→ Pause, andpaused→ Resume. The default case safely returnsnullfor unexpected states.apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor-skeleton.tsx (1)
1-6: LGTM: Clean imports and constant definition.The imports are appropriate and the
labelClassNameconstant is a good practice for maintaining consistent styling across multiple elements.
Summary by CodeRabbit
New Features
Enhancements
Bug Fixes
Chores
Style