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

Skip to content

Conversation

@ianmaccallum
Copy link
Contributor

@ianmaccallum ianmaccallum commented Sep 26, 2025

Summary by CodeRabbit

  • New Features

    • Group-based branding UI: new Group Branding page, application form builder, live previews, editable fields, and modals for adding/editing form fields.
    • Dynamic application form: country field, structured formData, hero, and group-aware application sheet.
  • Improvements

    • Application responses and emails show structured form fields; partner social links auto-fill from submissions.
    • Dashboard group selector and updated group-centric navigation; safer analytics init; rewards hidden when none.
  • Chores

    • Exported application CSV no longer includes legacy proposal/comments fields.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 26, 2025

Walkthrough

Group-scoped application form and lander support added across DB, schemas, APIs, actions, UI and migrations; program-level branding/form surfaces moved to group scope and many form/editor UI components, type/schema changes, and export/email shapes updated.

Changes

Cohort / File(s) Summary
API: Groups
apps/web/app/(ee)/api/groups/[groupIdOrSlug]/route.ts, apps/web/app/(ee)/api/groups/route.ts, apps/web/lib/api/groups/get-group-or-throw.ts
GET now serializes with GroupWithProgramSchema; PATCH accepts/saves applicationFormData and landerData; group creation uses a transaction, copies default links and optional form/lander data, and enforces slug conflicts.
Fetchers & Hooks
apps/web/lib/fetchers/get-program.ts, apps/web/lib/swr/use-group.ts
getProgram returns groups enriched with application/lander fields; useGroup made generic, accepts a query object and builds dynamic query string, returns null when not fetching.
Types & Zod Schemas
apps/web/lib/types.ts, apps/web/lib/zod/schemas/groups.ts, apps/web/lib/zod/schemas/group-with-program.ts, apps/web/lib/zod/schemas/program-application-form.ts, apps/web/lib/zod/schemas/programs.ts, apps/web/lib/zod/schemas/program-lander.ts
Added comprehensive program application form schemas and GroupWithFormData/GroupWithProgram schemas/types; createProgramApplicationSchema now expects country and structured formData; removed rewards from programLanderSchema.
Prisma schema
packages/prisma/schema/group.prisma, packages/prisma/schema/program.prisma, packages/prisma/schema/campaign.prisma, packages/prisma/schema/domain.prisma
PartnerGroup gains applicationFormData/applicationFormPublishedAt and landerData/landerPublishedAt and relations; ProgramApplication gains country, social fields, and formData; Campaign/Domain additions and formatting tweaks.
Actions & Server logic
apps/web/lib/actions/partners/create-program-application.ts, apps/web/lib/actions/partners/online-presence-providers.ts, apps/web/lib/actions/send-otp.ts, apps/web/lib/actions/partners/update-group-branding.ts, apps/web/lib/actions/partners/update-discount.ts
createProgramApplication sanitizes website/socials and returns partnerData; new updateGroupBrandingAction updates group/program branding, uploads assets, publishes timestamps, revalidates paths, and can propagate defaults; update-discount includes program in includes and conditional revalidation; small refactors/typo fixes.
Branding → Group-centric migration
apps/web/ui/partners/groups/design/branding-form.tsx, apps/web/ui/partners/groups/design/branding-settings-form.tsx, apps/web/ui/partners/groups/design/previews/*, apps/web/app/app.dub.co/.../branding/page.tsx, apps/web/ui/layout/sidebar/app-sidebar-nav.tsx
Branding and editor surfaces moved to group scope: forms, previews, preview-window and editor modals now use group.program; new Group Branding page added; program-level branding page removed; navigation updated to default-group routes.
Application form UI (new)
apps/web/ui/partners/groups/design/application-form/*
New ApplicationFormHero, ProgramApplicationForm, ProgramApplicationFormField dispatcher, field components (short-text/long-text/select/multiple-choice/website-and-socials), FormControl, MaxCharacterCount, required-fields preview, add/edit field modals and supporting components.
ProgramApplicationSheet & Apply pages
apps/web/ui/partners/program-application-sheet.tsx, apps/web/app/(ee)/partners.dub.co/(apply)/[programSlug]/apply/page.tsx, apps/web/app/(ee)/partners.dub.co/(apply)/[programSlug]/page.tsx, apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/apply/program-sidebar.tsx
ProgramApplicationSheet is group-aware, uses FormProvider and dynamic fields, accepts programEnrollment?; apply pages now guard for group form/lander presence and may redirect; sidebar passes programEnrollment to sheet hook.
Migrations
apps/web/scripts/migrations/migrate-application-form-data.ts, apps/web/scripts/migrations/migrate-lander-data.ts
Scripts to seed group applicationFormData/landerData from programs and to map existing application fields (website/proposal/comments) into structured formData.
Partner data & propagation
apps/web/lib/partners/complete-program-applications.ts, apps/web/lib/partners/format-application-form-data.ts, apps/web/lib/api/partners/notify-partner-application.ts, packages/email/src/templates/partner-application-received.tsx
On application completion, missing social fields on Partner updated; new formatter flattens application.formData to {title,value} array; emails/templates now consume applicationFormData (proposal/comments removed).
Exports & CSV
apps/web/app/(ee)/api/programs/[programId]/applications/export/route.ts, apps/web/lib/zod/schemas/partners.ts
Removed proposal and comments from exported application shape and export columns.
UI removals/replacements
apps/web/ui/partners/lander/program-application-form.tsx, apps/web/ui/partners/design/previews/application-preview.tsx, apps/web/ui/partners/groups/design/previews/application-preview.tsx
Removed legacy program-level ProgramApplicationForm and preview; introduced new group-level ApplicationPreview under groups/design.
Partner UX & small UI tweaks
apps/web/app/(ee)/partners.dub.co/(onboarding)/onboarding-form.tsx, .../online-presence/page.tsx, apps/web/ui/partners/country-combobox.tsx, apps/web/ui/partners/lander/lander-rewards.tsx, apps/web/ui/partners/partner-application-details.tsx
Prefill partnerData from localStorage; propagate socials from application to partner when missing; CountryCombobox accepts className; LanderRewards early-return if empty; PartnerApplicationDetails renders formatted application form data and includes loading skeleton.
Design editor & helpers
apps/web/ui/partners/groups/design/*
Editor improvements: EditListItem/ExpandableEditListItem, many add/edit field modals (short/select/multiple/multi-site), GenerateLanderModal, edit-hero modal, preview controls and path adjustments.
Combobox & providers
packages/ui/src/combobox/index.tsx, apps/web/app/providers.tsx
Combobox accepts labelProps, iconProps, popoverProps for styling; PostHog init guarded by presence of env key and window.
Misc & formatting
various Prisma schema files and small refactors
Whitespace/formatting updates across Prisma schemas, one legacy redirect removed, and small renames/typo fixes (send-otp).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Form as ProgramApplicationForm (group)
  participant Action as createProgramApplicationAction
  participant DB as Prisma
  participant Notify as notifyPartnerApplication
  participant PartnerUpdater as completeProgramApplications

  User->>Form: complete dynamic fields + submit
  Form->>Action: { programId, groupId, country, formData, contact }
  Action->>DB: create ProgramApplication (sanitized website/socials)
  alt enrollment created
    Action->>DB: create ProgramEnrollment
  end
  par post-create
    Action->>PartnerUpdater: update partner missing socials
    Action->>Notify: send email with formatted applicationFormData
  end
  Action-->>Form: { applicationId, enrollmentId?, partnerData }
  Form-->>User: navigate to success
Loading
sequenceDiagram
  autonumber
  actor Admin
  participant UI as Branding Form (group)
  participant Action as updateGroupBrandingAction
  participant Storage as Asset Storage
  participant DB as Prisma
  participant RV as Revalidate Service

  Admin->>UI: edit logo/wordmark, brandColor, applicationFormData, landerData
  UI->>Action: submit payload
  alt new assets provided
    Action->>Storage: upload logo/wordmark
  end
  Action->>DB: update PartnerGroup (applicationFormData/landerData + publishedAt)
  Action->>DB: update Program branding fields
  par cleanup & cache
    Action->>Storage: delete old assets
    Action->>RV: revalidate affected routes
  end
  Action-->>UI: return updated parsed group/program
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • devkiran

Poem

I hopped through forms and fields with care,
Planted socials, countries, and a flair.
Groups now hold the brand and form,
Little rabbit tested through the storm.
Hooray—new fields sprout everywhere! 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ 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 “Program application form” succinctly captures the primary focus of the changeset, which introduces and refactors support for a structured application form across APIs, schemas, and UI components, and it does so without extraneous details or vague wording.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch program-application-form

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.

@vercel
Copy link
Contributor

vercel bot commented Sep 26, 2025

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

Project Deployment Preview Updated (UTC)
dub Ready Ready Preview Oct 1, 2025 2:14am

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

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/update-discount.ts (1)

47-89: Expire cache when coupon identifiers change.

shouldExpireCache ignores coupon/code changes, so downstream caches never refresh when you swap coupon IDs. Fold couponId/couponTestId into the comparison (or otherwise include them) before skipping invalidation.

         const shouldExpireCache = !deepEqual(
           {
             amount: discount.amount,
             type: discount.type,
             maxDuration: discount.maxDuration,
+            couponId: discount.couponId,
+            couponTestId: discount.couponTestId,
           },
           {
             amount: updatedDiscount.amount,
             type: updatedDiscount.type,
             maxDuration: updatedDiscount.maxDuration,
+            couponId: updatedDiscount.couponId,
+            couponTestId: updatedDiscount.couponTestId,
           },
         );
♻️ Duplicate comments (5)
apps/web/ui/partners/program-application-sheet.tsx (1)

87-97: Provide user feedback when preconditions fail.

The early return at line 88 still exits silently when group, program, partner.email, or partner.country are missing, leaving users without feedback about why submission did nothing.

Surface an error message to guide users:

  const onSubmit = async (data: FormData) => {
-   if (!group || !program || !partner?.email || !partner.country) return;
+   if (!group || !program) {
+     toast.error("Program or group data not loaded. Please try again.");
+     return;
+   }
+   if (!partner?.email || !partner.country) {
+     toast.error("Please complete your profile (email and country) before applying.");
+     setError("root.serverError", {
+       message: "Profile incomplete. Add email and country in your profile.",
+     });
+     return;
+   }

    const result = await executeAsync({

Note: This matches a previously flagged issue that remains unresolved.

apps/web/lib/actions/partners/update-group-branding.ts (4)

63-77: Restore workspace scoping on group updates.

Updating by bare id still lets anyone with a UUID mutate groups across workspaces/programs. Re-fetch the group scoped to the current programId (and bail if not found) before issuing the update.

-    const updatedGroup = await prisma.partnerGroup.update({
-      where: {
-        id: groupId,
-      },
+    const group = await prisma.partnerGroup.findFirst({
+      where: {
+        id: groupId,
+        programId,
+      },
+      select: {
+        id: true,
+        slug: true,
+      },
+    });
+    if (!group) {
+      throw new Error("Group not found in this workspace.");
+    }
+
+    const updatedGroup = await prisma.partnerGroup.update({
+      where: {
+        id: group.id,
+      },

90-99: Guard deletes to R2-backed assets only.

storage.delete will throw if program.logo|wordmark point off-R2. Add isStored to avoid firing deletes against third-party URLs.

-        ...(logoUpdated && program.logo
+        ...(logoUpdated && program.logo && isStored(program.logo)
           ? [storage.delete(program.logo.replace(`${R2_URL}/`, ""))]
           : []),
-        ...(wordmarkUpdated && program.wordmark
+        ...(wordmarkUpdated && program.wordmark && isStored(program.wordmark)
           ? [storage.delete(program.wordmark.replace(`${R2_URL}/`, ""))]
           : []),

108-117: Revalidate group pages whenever lander/form data change.

Lander updates aren’t triggering any revalidation, and group-scoped routes stay stale. Include landerDataInput and the slugged paths so both the default and group pages refresh.

-        ...(logoUpdated ||
-        wordmarkUpdated ||
-        brandColorUpdated ||
-        applicationFormDataInput
+        ...(logoUpdated ||
+          wordmarkUpdated ||
+          brandColorUpdated ||
+          applicationFormDataInput ||
+          landerDataInput
           ? [
               revalidatePath(`/partners.dub.co/${program.slug}`),
               revalidatePath(`/partners.dub.co/${program.slug}/apply`),
               revalidatePath(`/partners.dub.co/${program.slug}/apply/success`),
+              revalidatePath(
+                `/partners.dub.co/${program.slug}/${updatedGroup.slug}`,
+              ),
+              revalidatePath(
+                `/partners.dub.co/${program.slug}/${updatedGroup.slug}/apply`,
+              ),
+              revalidatePath(
+                `/partners.dub.co/${program.slug}/${updatedGroup.slug}/apply/success`,
+              ),
             ]
           : []),

136-142: Avoid throwing when form/lander data are null.

Calling .parse on null blows up whenever these payloads are absent. Switch to safeParse (or allow nullish in schema) and return null when parsing fails.

-    return {
-      success: true,
-      applicationFormData: programApplicationFormSchema.parse(
-        updatedGroup.applicationFormData,
-      ),
-      landerData: programLanderSchema.parse(updatedGroup.landerData),
+    const parsedApplicationForm = programApplicationFormSchema.safeParse(
+      updatedGroup.applicationFormData,
+    );
+    const parsedLander = programLanderSchema.safeParse(
+      updatedGroup.landerData,
+    );
+
+    return {
+      success: true,
+      applicationFormData: parsedApplicationForm.success
+        ? parsedApplicationForm.data
+        : null,
+      landerData: parsedLander.success ? parsedLander.data : null,
       program: ProgramWithLanderDataSchema.parse(updatedProgram),
     };
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bd255c2 and 3973c37.

📒 Files selected for processing (5)
  • apps/web/lib/actions/partners/update-discount.ts (3 hunks)
  • apps/web/lib/actions/partners/update-group-branding.ts (1 hunks)
  • apps/web/ui/layout/sidebar/app-sidebar-nav.tsx (2 hunks)
  • apps/web/ui/partners/groups/design/branding-form.tsx (11 hunks)
  • apps/web/ui/partners/program-application-sheet.tsx (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/ui/layout/sidebar/app-sidebar-nav.tsx
🧰 Additional context used
🧬 Code graph analysis (4)
apps/web/ui/partners/program-application-sheet.tsx (5)
apps/web/lib/types.ts (3)
  • ProgramProps (450-450)
  • ProgramEnrollmentProps (470-470)
  • GroupWithProgramProps (559-559)
apps/web/lib/zod/schemas/groups.ts (1)
  • DEFAULT_PARTNER_GROUP (15-19)
apps/web/lib/swr/use-group.ts (1)
  • useGroup (7-45)
packages/utils/src/constants/misc.ts (1)
  • OG_AVATAR_URL (29-29)
apps/web/ui/partners/groups/design/application-form/fields/index.tsx (1)
  • ProgramApplicationFormField (20-32)
apps/web/lib/actions/partners/update-discount.ts (2)
packages/utils/src/constants/main.ts (1)
  • APP_DOMAIN_WITH_NGROK (20-25)
apps/web/lib/zod/schemas/groups.ts (1)
  • DEFAULT_PARTNER_GROUP (15-19)
apps/web/ui/partners/groups/design/branding-form.tsx (5)
apps/web/lib/types.ts (4)
  • ProgramApplicationFormData (458-458)
  • ProgramLanderData (452-452)
  • ProgramProps (450-450)
  • GroupWithProgramProps (559-559)
apps/web/lib/swr/use-group.ts (1)
  • useGroup (7-45)
apps/web/ui/partners/groups/design/branding-context-provider.tsx (1)
  • BrandingContextProvider (26-42)
apps/web/ui/partners/groups/design/previews/application-preview.tsx (1)
  • ApplicationPreview (41-354)
apps/web/lib/actions/partners/update-group-branding.ts (1)
  • updateGroupBrandingAction (27-144)
apps/web/lib/actions/partners/update-group-branding.ts (7)
apps/web/lib/zod/schemas/program-application-form.ts (1)
  • programApplicationFormSchema (134-139)
apps/web/lib/zod/schemas/program-lander.ts (1)
  • programLanderSchema (84-89)
apps/web/lib/actions/safe-action.ts (1)
  • authActionClient (33-82)
apps/web/lib/api/programs/get-program-or-throw.ts (1)
  • getProgramOrThrow (9-40)
packages/utils/src/constants/main.ts (1)
  • R2_URL (81-81)
apps/web/lib/api/audit-logs/record-audit-log.ts (1)
  • recordAuditLog (47-73)
apps/web/lib/zod/schemas/programs.ts (1)
  • ProgramWithLanderDataSchema (50-53)
⏰ 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)
apps/web/ui/partners/program-application-sheet.tsx (1)

58-65: Incorrect assumption on loading destructuring

The review comment assumes a loading value is destructured from useGroup, but the code only destructures { group }, so there is no unused loading variable to address.

Likely an incorrect or invalid review comment.

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

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/update-discount.ts (1)

49-60: Cache revalidation misses coupon updates

shouldExpireCache ignores the couponId / couponTestId fields we just updated. If those change while amount, type, and maxDuration stay the same, we skip both the QStash invalidation and the partner page revalidatePath calls, leaving stale coupon info visible. Please include the coupon fields in the deep comparison so cache busting runs whenever any persisted discount attribute changes.

Apply this diff to cover the missing fields:

-        const shouldExpireCache = !deepEqual(
-          {
-            amount: discount.amount,
-            type: discount.type,
-            maxDuration: discount.maxDuration,
-          },
-          {
-            amount: updatedDiscount.amount,
-            type: updatedDiscount.type,
-            maxDuration: updatedDiscount.maxDuration,
-          },
-        );
+        const shouldExpireCache = !deepEqual(
+          {
+            amount: discount.amount,
+            type: discount.type,
+            maxDuration: discount.maxDuration,
+            couponId: discount.couponId,
+            couponTestId: discount.couponTestId,
+          },
+          {
+            amount: updatedDiscount.amount,
+            type: updatedDiscount.type,
+            maxDuration: updatedDiscount.maxDuration,
+            couponId: updatedDiscount.couponId,
+            couponTestId: updatedDiscount.couponTestId,
+          },
+        );
♻️ Duplicate comments (10)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/group-header-selector.tsx (2)

35-42: Fix the infinite toggle loop.

This effect will cause an endless flip-flop: when groups.length >= GROUPS_MAX_PAGE_SIZE, it sets useAsync to true, but on the next render the condition evaluates to false (because !useAsync is now false), toggling it back. This creates continuous re-renders and fetch churn.

Apply this diff to only promote to true and never demote:

-  useEffect(
-    () =>
-      setUseAsync(
-        Boolean(groups && !useAsync && groups.length >= GROUPS_MAX_PAGE_SIZE),
-      ),
-    [groups, useAsync],
-  );
+  useEffect(() => {
+    if (groups && !useAsync && groups.length >= GROUPS_MAX_PAGE_SIZE) {
+      setUseAsync(true);
+    }
+  }, [groups, useAsync]);

92-107: Add null to the parameter type.

The Combobox component can pass null to setSelected when deselecting (see line 125 in the Combobox implementation), but the onChange callback currently only types the parameter as { value: string }. This will cause a TypeScript error.

Apply this diff to fix the type:

   const onChange = useCallback(
-    (option: { value: string }) => {
+    (option: { value: string } | null) => {
       if (!option) {
         return;
       }
apps/web/ui/partners/groups/design/branding-settings-form.tsx (1)

7-7: Stop importing from @dub/utils/src

The deep import keeps bypassing the package entry point and can pull non-transpiled sources into the bundle. Please switch to the public entry point.

-import { cn } from "@dub/utils/src";
+import { cn } from "@dub/utils";
apps/web/ui/partners/groups/design/branding-form.tsx (4)

184-202: Reset form with persisted asset URLs after successful publish

After publish, the form still holds pre-upload base64 strings for brand assets (logo, wordmark, brandColor) instead of the canonical URLs returned from the server. This keeps the UI out of sync and forces redundant uploads on the next submit.

Update the reset to include the persisted asset URLs:

     async onSuccess({ data }) {
       await mutateGroup();
       toast.success("Group updated successfully.");

       const currentValues = getValues();
-
-      // Still reset form state to clear isSubmitSuccessful
-      reset({
-        ...currentValues,
-        applicationFormData:
-          data?.applicationFormData ?? currentValues.applicationFormData,
-        landerData: data?.landerData ?? currentValues.landerData,
-      });
+      const program = data?.program;
+
+      reset({
+        ...currentValues,
+        logo: program?.logo ?? currentValues.logo,
+        wordmark: program?.wordmark ?? currentValues.wordmark,
+        brandColor: program?.brandColor ?? currentValues.brandColor,
+        applicationFormData:
+          data?.applicationFormData ?? currentValues.applicationFormData,
+        landerData: data?.landerData ?? currentValues.landerData,
+      });

229-241: Avoid spreading entire form data to prevent unintentional republishing

Spreading ...data sends all form fields to the server on every submit, which may republish unchanged landerData or applicationFormData and update their publishedAt timestamps unnecessarily.

Build a minimal payload containing only changed fields:

       onSubmit={handleSubmit(async (data) => {
-        const result = await executeAsync({
-          workspaceId: workspaceId!,
-          groupId: group.id,
-          ...data,
-        });
+        const payload: any = { workspaceId: workspaceId!, groupId: group.id };
+        
+        // Only include fields that changed
+        if (JSON.stringify(data.applicationFormData) !== JSON.stringify(group.applicationFormData)) {
+          payload.applicationFormData = data.applicationFormData;
+        }
+        if (JSON.stringify(data.landerData) !== JSON.stringify(group.landerData)) {
+          payload.landerData = data.landerData;
+        }
+        if (data.brandColor !== group.program?.brandColor) {
+          payload.brandColor = data.brandColor;
+        }
+        if (data.logo !== group.program?.logo) {
+          payload.logo = data.logo;
+        }
+        if (data.wordmark !== group.program?.wordmark) {
+          payload.wordmark = data.wordmark;
+        }
+        
+        const result = await executeAsync(payload);

For nested object comparison, prefer a deep-equal utility over JSON.stringify.


70-73: Update error message to reference "group" instead of "program"

The error message still references "program" but this component is now group-centric.

-      <div className="text-content-muted text-sm">Failed to load program</div>
+      <div className="text-content-muted text-sm">Failed to load group</div>

110-144: UUID generation on every invocation causes form data instability

Each time defaultApplicationFormData is called, uuid() generates new field IDs. If this function is invoked multiple times (e.g., during re-renders or when resetting defaults), the IDs will differ even though the structure is identical, causing unnecessary form state changes and potential draft mismatches.

Generate stable IDs by memoizing at module level or deriving deterministic IDs:

+const DEFAULT_FIELD_IDS = {
+  website: "default-field-website",
+  promotion: "default-field-promotion",
+  additional: "default-field-additional",
+};
+
 const defaultApplicationFormData = (
   program: ProgramProps,
 ): ProgramApplicationFormData => {
   return {
     fields: [
       {
-        id: uuid(),
+        id: DEFAULT_FIELD_IDS.website,
         type: "short-text",
apps/web/lib/swr/use-group.ts (1)

7-14: Default query to {} and constrain its type to primitives

Two issues flagged in past reviews remain unaddressed:

  1. Runtime error risk: query is destructured without a default, so { ...query } on line 28 will throw when query is undefined.
  2. Type safety: Record<string, any> allows objects that stringify to "[object Object]" in URLSearchParams.

Apply the fixes from the previous review:

 export default function useGroup<T = GroupProps>(
   {
     groupIdOrSlug: groupIdOrSlugProp,
-    query,
+    query = {},
   }: {
     groupIdOrSlug?: string;
-    query?: Record<string, any>;
+    query?: Record<string, string | number | boolean | null | undefined>;
   } = {},

Then optionally normalize values before building URLSearchParams:

const params = new URLSearchParams({ workspaceId });
Object.entries(query).forEach(([k, v]) => {
  if (v != null) params.set(k, String(v));
});
apps/web/scripts/migrations/migrate-application-form-data.ts (2)

99-108: Keep the migration idempotent; only seed groups missing form data.

updateMany currently overwrites every partner group’s applicationFormData on each run, clobbering any form the team may have published later. Add a guard (e.g., require both applicationFormData and applicationFormPublishedAt to be null) so the seeding happens only for groups that haven’t been initialized yet. Re-running the migration should be a no-op for groups that already have data.

-  const updatedGroups = await prisma.partnerGroup.updateMany({
-    where: {
-      id: {
-        in: groupIds,
-      },
-    },
-    data: {
-      applicationFormData,
-    },
-  });
+  const updatedGroups = await prisma.partnerGroup.updateMany({
+    where: {
+      id: { in: groupIds },
+      applicationFormData: null,
+      applicationFormPublishedAt: null,
+    },
+    data: {
+      applicationFormData,
+    },
+  });

113-127: Skip applications that already have formData to avoid wiping responses.

Re-running this script replaces every ProgramApplication.formData, even records that already contain structured submissions captured after the initial run. Filter for formData: null (or bail out per-record) so we only backfill legacy rows lacking the structured payload.

-  const applications = await prisma.programApplication.findMany({
-    where: {
-      programId: program.id,
-    },
-  });
+  const applications = await prisma.programApplication.findMany({
+    where: {
+      programId: program.id,
+      formData: null,
+    },
+    select: {
+      id: true,
+      website: true,
+      proposal: true,
+      comments: true,
+    },
+  });-    await prisma.programApplication.update({
-      where: { id: application.id },
-      data: {
-        formData: defaultApplicationFormDataWithValues(program, application),
-      },
-    });
+    await prisma.programApplication.update({
+      where: { id: application.id },
+      data: {
+        formData: defaultApplicationFormDataWithValues(program, application as ProgramApplication),
+      },
+    });
🧹 Nitpick comments (1)
apps/web/ui/partners/groups/design/branding-form.tsx (1)

50-50: Remove unused groupSlug from useParams

The groupSlug variable is extracted but never used in this component.

-  const { groupSlug } = useParams<{ groupSlug: string }>();
-
   const { group, mutateGroup, loading } = useGroup<GroupWithProgramProps>(
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3973c37 and e2abacb.

📒 Files selected for processing (8)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/group-header-selector.tsx (1 hunks)
  • apps/web/lib/actions/partners/update-discount.ts (3 hunks)
  • apps/web/lib/swr/use-group.ts (2 hunks)
  • apps/web/lib/zod/schemas/group-with-program.ts (1 hunks)
  • apps/web/scripts/migrations/migrate-application-form-data.ts (1 hunks)
  • apps/web/scripts/migrations/migrate-lander-data.ts (1 hunks)
  • apps/web/ui/partners/groups/design/branding-form.tsx (11 hunks)
  • apps/web/ui/partners/groups/design/branding-settings-form.tsx (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/lib/zod/schemas/group-with-program.ts
  • apps/web/scripts/migrations/migrate-lander-data.ts
🧰 Additional context used
🧬 Code graph analysis (6)
apps/web/lib/actions/partners/update-discount.ts (2)
packages/utils/src/constants/main.ts (1)
  • APP_DOMAIN_WITH_NGROK (20-25)
apps/web/lib/zod/schemas/groups.ts (1)
  • DEFAULT_PARTNER_GROUP (15-19)
apps/web/ui/partners/groups/design/branding-settings-form.tsx (1)
apps/web/ui/partners/groups/design/branding-form.tsx (1)
  • useBrandingFormContext (45-47)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/group-header-selector.tsx (5)
apps/web/lib/types.ts (1)
  • GroupProps (555-555)
apps/web/lib/swr/use-groups.ts (1)
  • useGroups (10-38)
apps/web/lib/zod/schemas/groups.ts (1)
  • GROUPS_MAX_PAGE_SIZE (24-24)
apps/web/ui/partners/groups/group-color-circle.tsx (1)
  • GroupColorCircle (5-24)
packages/ui/src/combobox/index.tsx (1)
  • Combobox (85-367)
apps/web/lib/swr/use-group.ts (1)
apps/web/lib/types.ts (1)
  • GroupProps (555-555)
apps/web/ui/partners/groups/design/branding-form.tsx (4)
apps/web/lib/types.ts (4)
  • ProgramApplicationFormData (458-458)
  • ProgramLanderData (452-452)
  • ProgramProps (450-450)
  • GroupWithProgramProps (559-559)
apps/web/lib/swr/use-group.ts (1)
  • useGroup (7-43)
apps/web/ui/partners/groups/design/previews/application-preview.tsx (1)
  • ApplicationPreview (41-354)
apps/web/lib/actions/partners/update-group-branding.ts (1)
  • updateGroupBrandingAction (27-144)
apps/web/scripts/migrations/migrate-application-form-data.ts (1)
apps/web/lib/types.ts (1)
  • ProgramProps (450-450)
⏰ 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/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/group-header-selector.tsx (4)

9-15: Clean type definitions.

The Group type appropriately picks the minimal required fields, and the component props interface is well-defined with clear contracts.


27-33: Proper conditional data fetching.

Both useGroups calls correctly gate their queries—the first enables async search when needed, and the second only fetches when a group is selected.


54-70: Smart fallback for initial options.

Returning the selected group as a fallback option while loading ensures the Combobox displays correctly even before the full group list is fetched.


109-141: Well-integrated Combobox usage.

The component correctly:

  • Toggles client-side filtering based on async mode (shouldFilter={!useAsync})
  • Controls popover state for external coordination
  • Passes proper styling and behavior props
  • Handles loading states gracefully
apps/web/lib/swr/use-group.ts (1)

26-29: Generic type parameter works correctly

The generic T with GroupProps default enables callers to specify extended types like GroupWithProgramProps, which aligns with the PR's goal of supporting richer group data shapes.

apps/web/ui/partners/groups/design/branding-form.tsx (5)

40-43: Type definition correctly expanded for group-scoped form/lander data

The BrandingFormData type now includes applicationFormData and landerData, aligning with the PR's migration to group-centric branding storage.


61-64: Verify localStorage key stability across group lifecycle

The draft key now uses group?.id instead of the previously suggested defaultProgramId-${groupSlug}. While group.id is stable, it won't be available until group loads, which means the key evaluates to "branding-form-undefined" during the initial loading phase. This could cause draft loss or unexpected behavior.

Confirm that:

  1. The draft is only read/written after group is loaded (line 66 guards render until loaded).
  2. No race condition exists between draft read and group load.

If drafts must persist before group load completes, consider scoping by a stable identifier available earlier (e.g., route param or workspace ID + group slug).


165-174: Default values correctly seeded from group and program

The form initialization properly pulls brand assets from group.program and form/lander data from group, with sensible fallbacks for missing data.


223-225: Publish button logic correctly scoped to group's applicationFormPublishedAt

The disable logic now checks group.applicationFormPublishedAt instead of the previous program-level field, aligning with the group-centric branding model.


372-392: Draft loading correctly includes applicationFormData

The draft restoration now includes applicationFormData alongside the other form fields, ensuring unsaved application form changes are preserved.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/ui/partners/partner-application-details.tsx (1)

37-56: Stop showing the loading skeleton for unanswered fields.

When a field value is null/undefined, the current condition drops into the animated placeholder even though we’re done loading, leaving partners staring at a fake spinner. Treat those as empty responses and show the “No response provided” copy instead.

-            {field.value || field.value === "" ? (
+            {field.value !== null &&
+            field.value !== undefined &&
+            field.value !== "" ? (
               <Linkify
                 as="p"
                 options={{
                   target: "_blank",
                   rel: "noopener noreferrer nofollow",
                   className:
                     "underline underline-offset-4 text-sm max-w-prose text-neutral-400 hover:text-neutral-700",
                 }}
               >
-                {field.value || (
-                  <span className="text-content-muted italic">
-                    No response provided
-                  </span>
-                )}
+                {field.value}
               </Linkify>
             ) : (
-              <div className="h-4 w-28 min-w-0 animate-pulse rounded-md bg-neutral-200" />
+              <p className="text-sm text-content-muted italic">
+                No response provided
+              </p>
             )}
♻️ Duplicate comments (3)
apps/web/ui/partners/partner-application-details.tsx (2)

70-75: Replace the empty heading with a visible skeleton bar.

The self-closing <h4> renders nothing, so the title placeholder is still invisible. Drop the empty heading and keep the animated bar.

-        <div key={idx}>
-          <h4 className="text-content-emphasis font-semibold" />
-          <div className="h-5 w-32 animate-pulse rounded-md bg-neutral-200" />
+        <div key={idx}>
+          <div
+            aria-hidden="true"
+            className="h-5 w-32 animate-pulse rounded-md bg-neutral-200"
+          />

17-30: Handle the SWR error state instead of looping the skeleton.

useSWRImmutable returns an error when the fetch fails; because we ignore it, application stays undefined and we render the skeleton forever. Surface the error (same copy we discussed previously) so ops teams see a failure instead of a perpetual loader.

Apply this diff to cover the basics:

-  const { data: application, isLoading } = useSWRImmutable<ProgramApplication>(
+  const {
+    data: application,
+    isLoading,
+    error,
+  } = useSWRImmutable<ProgramApplication>(
@@
-  if (isLoading || !application) {
+  if (error) {
+    return (
+      <div className="text-sm text-red-600">
+        Failed to load application details.
+      </div>
+    );
+  }
+
+  if (isLoading || !application) {
     return <PartnerApplicationDetailsSkeleton />;
   }
apps/web/ui/partners/applications/utils/format-application-form-data.ts (1)

7-24: Parse formData with the Zod schema before iterating.

application.formData is still a raw Prisma.JsonValue; if the payload is malformed we’ll throw while dereferencing field.data/field.label. Safe-parse with programApplicationFormDataWithValuesSchema and bail early on failure instead of trusting the cast.

-import { ProgramApplicationFormDataWithValues } from "@/lib/types";
+import { ProgramApplicationFormDataWithValues } from "@/lib/types";
+import { programApplicationFormDataWithValuesSchema } from "@/lib/zod/schemas/program-application-form";
@@
-export const formatApplicationFormData = (
-  application: ProgramApplication,
-): { title: string; value: string }[] => {
-  const formData =
-    application?.formData as ProgramApplicationFormDataWithValues;
+export const formatApplicationFormData = (
+  application: ProgramApplication,
+): { title: string; value: string }[] => {
+  const parsed =
+    programApplicationFormDataWithValuesSchema.safeParse(application?.formData);
+
+  if (!parsed.success) {
+    return [];
+  }
+
+  const formData: ProgramApplicationFormDataWithValues = parsed.data;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e2abacb and f585b7d.

📒 Files selected for processing (4)
  • apps/web/lib/api/partners/notify-partner-application.ts (0 hunks)
  • apps/web/ui/partners/applications/utils/format-application-form-data.ts (1 hunks)
  • apps/web/ui/partners/partner-application-details.tsx (3 hunks)
  • packages/email/src/templates/partner-application-received.tsx (0 hunks)
💤 Files with no reviewable changes (2)
  • apps/web/lib/api/partners/notify-partner-application.ts
  • packages/email/src/templates/partner-application-received.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
apps/web/ui/partners/partner-application-details.tsx (1)
apps/web/ui/partners/applications/utils/format-application-form-data.ts (1)
  • formatApplicationFormData (4-48)
apps/web/ui/partners/applications/utils/format-application-form-data.ts (1)
apps/web/lib/types.ts (1)
  • ProgramApplicationFormDataWithValues (460-460)
🪛 Biome (2.1.2)
apps/web/ui/partners/applications/utils/format-application-form-data.ts

[error] 29-29: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

⏰ 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

Removes 'proposal' and 'comments' fields from partner application exports and schemas, replacing them with a generic 'applicationFormData' structure. Updates notification and email templates to use the new format, and moves the format utility to a shared location. Also adds logic to propagate default group branding changes to other groups without custom data.
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: 4

♻️ Duplicate comments (2)
apps/web/lib/partners/format-application-form-data.ts (1)

28-42: Wrap the multiple-choice case in a block.

The let value declaration at line 29 needs block scope to prevent leaking to other switch cases, as previously noted.

Apply this diff to fix:

-        case "multiple-choice":
-          let value;
-
-          if (field.data.multiple) {
-            value = Array.isArray(field.value)
-              ? field.value.join(", ")
-              : field.value;
-          } else {
-            value = field.value;
-          }
-
-          return {
-            title: field.label,
-            value,
-          };
+        case "multiple-choice": {
+          let value;
+
+          if (field.data.multiple) {
+            value = Array.isArray(field.value)
+              ? field.value.join(", ")
+              : field.value;
+          } else {
+            value = field.value;
+          }
+
+          return {
+            title: field.label,
+            value,
+          };
+        }
apps/web/lib/actions/partners/update-group-branding.ts (1)

178-185: Avoid runtime throws when form data is null; use safeParse or allow nullish.

Lines 180-184 use .parse() on updatedGroup.applicationFormData and updatedGroup.landerData. If either field is null (which can happen when the user doesn't provide these fields), .parse() will throw a Zod validation error at runtime, causing the action to fail after successful database updates.

Apply this diff to use safeParse() and return null when data is absent:

+  const appForm = programApplicationFormSchema.safeParse(
+    updatedGroup.applicationFormData,
+  );
+  const lander = programLanderSchema.safeParse(updatedGroup.landerData);
+
   return {
     success: true,
-    applicationFormData: programApplicationFormSchema.parse(
-      updatedGroup.applicationFormData,
-    ),
-    landerData: programLanderSchema.parse(updatedGroup.landerData),
+    applicationFormData: appForm.success ? appForm.data : null,
+    landerData: lander.success ? lander.data : null,
     program: ProgramWithLanderDataSchema.parse(updatedProgram),
   };
🧹 Nitpick comments (1)
apps/web/lib/partners/format-application-form-data.ts (1)

10-27: Consider consolidating the simple field type cases.

The short-text, long-text, and select cases are identical. You could simplify the switch statement by handling them in a single case.

Apply this diff to consolidate:

     .map((field) => {
       switch (field.type) {
-        case "short-text":
-          return {
-            title: field.label,
-            value: field.value,
-          };
-        case "long-text":
-          return {
-            title: field.label,
-            value: field.value,
-          };
-        case "select":
+        case "short-text":
+        case "long-text":
+        case "select":
           return {
             title: field.label,
             value: field.value,
           };
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f585b7d and da7d371.

📒 Files selected for processing (7)
  • apps/web/app/(ee)/api/programs/[programId]/applications/export/route.ts (0 hunks)
  • apps/web/lib/actions/partners/update-group-branding.ts (1 hunks)
  • apps/web/lib/api/partners/notify-partner-application.ts (2 hunks)
  • apps/web/lib/partners/format-application-form-data.ts (1 hunks)
  • apps/web/lib/zod/schemas/partners.ts (0 hunks)
  • apps/web/ui/partners/partner-application-details.tsx (3 hunks)
  • packages/email/src/templates/partner-application-received.tsx (3 hunks)
💤 Files with no reviewable changes (2)
  • apps/web/lib/zod/schemas/partners.ts
  • apps/web/app/(ee)/api/programs/[programId]/applications/export/route.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/ui/partners/partner-application-details.tsx
🧰 Additional context used
🧬 Code graph analysis (3)
apps/web/lib/actions/partners/update-group-branding.ts (8)
apps/web/lib/zod/schemas/program-application-form.ts (1)
  • programApplicationFormSchema (134-139)
apps/web/lib/zod/schemas/program-lander.ts (1)
  • programLanderSchema (84-89)
apps/web/lib/actions/safe-action.ts (1)
  • authActionClient (33-82)
apps/web/lib/api/programs/get-program-or-throw.ts (1)
  • getProgramOrThrow (9-40)
packages/utils/src/constants/main.ts (1)
  • R2_URL (81-81)
apps/web/lib/api/audit-logs/record-audit-log.ts (1)
  • recordAuditLog (47-73)
apps/web/lib/zod/schemas/groups.ts (1)
  • DEFAULT_PARTNER_GROUP (15-19)
apps/web/lib/zod/schemas/programs.ts (1)
  • ProgramWithLanderDataSchema (50-53)
apps/web/lib/partners/format-application-form-data.ts (1)
apps/web/lib/types.ts (1)
  • ProgramApplicationFormDataWithValues (460-460)
apps/web/lib/api/partners/notify-partner-application.ts (1)
apps/web/lib/partners/format-application-form-data.ts (1)
  • formatApplicationFormData (4-48)
🪛 Biome (2.1.2)
apps/web/lib/partners/format-application-form-data.ts

[error] 29-29: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

packages/email/src/templates/partner-application-received.tsx

[error] 144-145: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)

⏰ 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/lib/partners/format-application-form-data.ts (1)

1-9: LGTM!

The function signature and initial type casting are well-structured. The optional chaining on application?.formData safely handles potential null/undefined values.

apps/web/lib/api/partners/notify-partner-application.ts (1)

1-1: LGTM!

The integration of formatApplicationFormData is correct and aligns with the email template changes. The formatted application data properly replaces the previous proposal/comments fields.

Also applies to: 58-58

packages/email/src/templates/partner-application-received.tsx (1)

26-37: LGTM!

The transition from proposal/comments to applicationFormData with structured title/value pairs is clean and consistent with the formatter utility.

Also applies to: 54-54

apps/web/lib/actions/partners/update-group-branding.ts (3)

1-26: LGTM: Imports and schema definition are well-structured.

The imports include the necessary utilities (isStored for storage checks, getProgramOrThrow for authorization), and the schema appropriately defines all required and optional fields with nullish types.


47-62: LGTM: Asset upload logic correctly uses isStored() check.

The code appropriately uses isStored() to determine whether new assets need to be uploaded, avoiding unnecessary uploads of already-stored assets.


64-89: Program update logic is correct; group update requires authorization fix.

The program update at lines 80-89 is safe because getProgramOrThrow already validated the program belongs to the workspace. However, the group update at lines 64-78 still requires the authorization fix mentioned in the previous comment.

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

♻️ Duplicate comments (1)
apps/web/lib/actions/partners/update-group-branding.ts (1)

187-194: Runtime error when form data is null.

Using .parse() on lines 189-192 will throw at runtime if applicationFormData or landerData are null (when not provided in the input). This critical issue remains unresolved from previous reviews.

Apply this diff to safely handle null values:

     return {
       success: true,
-      applicationFormData: programApplicationFormSchema.parse(
-        updatedGroup.applicationFormData,
-      ),
-      landerData: programLanderSchema.parse(updatedGroup.landerData),
+      applicationFormData: updatedGroup.applicationFormData
+        ? programApplicationFormSchema.parse(updatedGroup.applicationFormData)
+        : null,
+      landerData: updatedGroup.landerData
+        ? programLanderSchema.parse(updatedGroup.landerData)
+        : null,
       program: ProgramWithLanderDataSchema.parse(updatedProgram),
     };
🧹 Nitpick comments (3)
packages/email/src/templates/partner-application-received.tsx (2)

143-144: Consider using index as fallback for the key prop.

Using field.title as the key assumes all form field titles are unique. If the application form contains duplicate titles (e.g., multiple "Additional comments" fields), React will encounter reconciliation issues.

Apply this diff to add index as a fallback:

-                  {partner.applicationFormData.map((field) => (
-                    <Section key={field.title} className="mb-6">
+                  {partner.applicationFormData.map((field, index) => (
+                    <Section key={`${field.title}-${index}`} className="mb-6">

Alternatively, if a stable unique identifier (like field.id) will be added to the data structure in the future, prefer that over title.


54-54: Consider adding a unique identifier to applicationFormData items.

The current type definition lacks a unique identifier field. Adding an id or fieldId property would provide a more stable key for React rendering and improve data traceability.

Consider updating the type to:

-    applicationFormData: { title: string; value: string }[];
+    applicationFormData: { id: string; title: string; value: string }[];

This change would require updates to the data formatting logic elsewhere in the codebase (e.g., formatApplicationFormData utility mentioned in the AI summary).

apps/web/lib/actions/partners/update-group-branding.ts (1)

72-79: Simplify conditional assignments.

The ternary pattern value ? value : undefined can be simplified using nullish coalescing or by relying on the conditional spread pattern.

Apply this diff to simplify:

       data: {
-        applicationFormData: applicationFormDataInput
-          ? applicationFormDataInput
-          : undefined,
+        applicationFormData: applicationFormDataInput ?? undefined,
         applicationFormPublishedAt: applicationFormDataInput
           ? new Date()
           : undefined,
-        landerData: landerDataInput ? landerDataInput : undefined,
+        landerData: landerDataInput ?? undefined,
         landerPublishedAt: landerDataInput ? new Date() : undefined,
       },
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b4ea6cc and bcf4e55.

📒 Files selected for processing (2)
  • apps/web/lib/actions/partners/update-group-branding.ts (1 hunks)
  • packages/email/src/templates/partner-application-received.tsx (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/lib/actions/partners/update-group-branding.ts (8)
apps/web/lib/zod/schemas/program-application-form.ts (1)
  • programApplicationFormSchema (134-139)
apps/web/lib/zod/schemas/program-lander.ts (1)
  • programLanderSchema (84-89)
apps/web/lib/actions/safe-action.ts (1)
  • authActionClient (33-82)
apps/web/lib/api/groups/get-group-or-throw.ts (1)
  • getGroupOrThrow (4-52)
packages/utils/src/constants/main.ts (1)
  • R2_URL (81-81)
apps/web/lib/api/audit-logs/record-audit-log.ts (1)
  • recordAuditLog (47-73)
apps/web/lib/zod/schemas/groups.ts (1)
  • DEFAULT_PARTNER_GROUP (15-19)
apps/web/lib/zod/schemas/programs.ts (1)
  • ProgramWithLanderDataSchema (50-53)
⏰ 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)
packages/email/src/templates/partner-application-received.tsx (1)

26-37: LGTM! Clean refactoring to structured form data.

The refactoring from separate proposal and comments fields to a structured applicationFormData array provides better flexibility for dynamic form fields.

apps/web/lib/actions/partners/update-group-branding.ts (3)

41-46: Authorization properly validated.

The call to getGroupOrThrow ensures the group belongs to the specified program (see lines 38-44 in get-group-or-throw.ts), addressing the authorization concern from previous reviews.


96-103: Asset deletion properly guarded.

The isStored() checks on lines 98 and 101 correctly prevent deletion attempts on external URLs, addressing the critical issue from previous reviews.


113-125: Revalidation condition complete.

The revalidation logic now correctly includes landerDataInput (line 116), ensuring public pages are updated when lander data changes. This addresses the issue from previous reviews.

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