-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Program application form #2893
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
Program application form #2893
Conversation
WalkthroughGroup-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
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
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
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 |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
d2639fa to
63b5382
Compare
2d1158d to
8508356
Compare
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
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.
shouldExpireCacheignores coupon/code changes, so downstream caches never refresh when you swap coupon IDs. FoldcouponId/couponTestIdinto 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, orpartner.countryare 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
idstill lets anyone with a UUID mutate groups across workspaces/programs. Re-fetch the group scoped to the currentprogramId(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.deletewill throw ifprogram.logo|wordmarkpoint off-R2. AddisStoredto 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
landerDataInputand 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
.parseonnullblows up whenever these payloads are absent. Switch tosafeParse(or allow nullish in schema) and returnnullwhen 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
📒 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 destructuringThe review comment assumes a
loadingvalue is destructured fromuseGroup, but the code only destructures{ group }, so there is no unusedloadingvariable to address.Likely an incorrect or invalid review comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 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
shouldExpireCacheignores thecouponId/couponTestIdfields we just updated. If those change whileamount,type, andmaxDurationstay the same, we skip both the QStash invalidation and the partner pagerevalidatePathcalls, 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 setsuseAsynctotrue, but on the next render the condition evaluates tofalse(because!useAsyncis now false), toggling it back. This creates continuous re-renders and fetch churn.Apply this diff to only promote to
trueand 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
Comboboxcomponent can passnulltosetSelectedwhen deselecting (see line 125 in the Combobox implementation), but theonChangecallback 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/srcThe 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 publishAfter 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 republishingSpreading
...datasends all form fields to the server on every submit, which may republish unchangedlanderDataorapplicationFormDataand update theirpublishedAttimestamps 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 instabilityEach time
defaultApplicationFormDatais 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: Defaultqueryto{}and constrain its type to primitivesTwo issues flagged in past reviews remain unaddressed:
- Runtime error risk:
queryis destructured without a default, so{ ...query }on line 28 will throw whenqueryisundefined.- Type safety:
Record<string, any>allows objects that stringify to"[object Object]"inURLSearchParams.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.
updateManycurrently overwrites every partner group’sapplicationFormDataon each run, clobbering any form the team may have published later. Add a guard (e.g., require bothapplicationFormDataandapplicationFormPublishedAtto benull) 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 haveformDatato avoid wiping responses.Re-running this script replaces every
ProgramApplication.formData, even records that already contain structured submissions captured after the initial run. Filter forformData: 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 unusedgroupSlugfromuseParamsThe
groupSlugvariable 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
📒 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
Grouptype appropriately picks the minimal required fields, and the component props interface is well-defined with clear contracts.
27-33: Proper conditional data fetching.Both
useGroupscalls 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 correctlyThe generic
TwithGroupPropsdefault enables callers to specify extended types likeGroupWithProgramProps, 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 dataThe
BrandingFormDatatype now includesapplicationFormDataandlanderData, aligning with the PR's migration to group-centric branding storage.
61-64: Verify localStorage key stability across group lifecycleThe draft key now uses
group?.idinstead of the previously suggesteddefaultProgramId-${groupSlug}. Whilegroup.idis stable, it won't be available untilgrouploads, which means the key evaluates to"branding-form-undefined"during the initial loading phase. This could cause draft loss or unexpected behavior.Confirm that:
- The draft is only read/written after
groupis loaded (line 66 guards render until loaded).- 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 programThe form initialization properly pulls brand assets from
group.programand form/lander data fromgroup, with sensible fallbacks for missing data.
223-225: Publish button logic correctly scoped to group's applicationFormPublishedAtThe disable logic now checks
group.applicationFormPublishedAtinstead of the previous program-level field, aligning with the group-centric branding model.
372-392: Draft loading correctly includes applicationFormDataThe draft restoration now includes
applicationFormDataalongside the other form fields, ensuring unsaved application form changes are preserved.
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
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.
useSWRImmutablereturns anerrorwhen the fetch fails; because we ignore it,applicationstaysundefinedand 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.formDatais still a rawPrisma.JsonValue; if the payload is malformed we’ll throw while dereferencingfield.data/field.label. Safe-parse withprogramApplicationFormDataWithValuesSchemaand 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
📒 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.
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: 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 valuedeclaration 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()onupdatedGroup.applicationFormDataandupdatedGroup.landerData. If either field isnull(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 returnnullwhen 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, andselectcases 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
📒 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?.formDatasafely handles potential null/undefined values.apps/web/lib/api/partners/notify-partner-application.ts (1)
1-1: LGTM!The integration of
formatApplicationFormDatais 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/commentstoapplicationFormDatawith structuredtitle/valuepairs 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 (
isStoredfor storage checks,getProgramOrThrowfor authorization), and the schema appropriately defines all required and optional fields with nullish types.
47-62: LGTM: Asset upload logic correctly usesisStored()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
getProgramOrThrowalready 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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
apps/web/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 ifapplicationFormDataorlanderDataare 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.titleas 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
idorfieldIdproperty 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.,
formatApplicationFormDatautility 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 : undefinedcan 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
📒 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
proposalandcommentsfields to a structuredapplicationFormDataarray 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
getGroupOrThrowensures the group belongs to the specified program (see lines 38-44 inget-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.
Summary by CodeRabbit
New Features
Improvements
Chores