-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Program Link settings UI #2731
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 Link settings UI #2731
Conversation
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
WalkthroughNew partner link settings— Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant LinksSettingsForm
participant Backend (updateProgramAction)
participant Database
User->>LinksSettingsForm: Adjusts "Max links" or "Link restriction"
LinksSettingsForm->>LinksSettingsForm: Updates form state
User->>LinksSettingsForm: Submits form
LinksSettingsForm->>Backend (updateProgramAction): Sends updated fields (including new settings)
Backend (updateProgramAction)->>Database: Updates program with new fields
Database-->>Backend (updateProgramAction): Confirms update
Backend (updateProgramAction)-->>LinksSettingsForm: Returns result
LinksSettingsForm-->>User: Shows confirmation or error
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~15 minutes Suggested reviewers
Poem
✨ 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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Nitpick comments (2)
packages/ui/src/number-stepper.tsx (1)
53-76: Minor optimisation: avoid re-creating handlers every render
handleDecrement/handleIncrementdepend oncanDecrement/canIncrement, which are derived values fromvalue/min/max. This causes new closures on every value change. ComputingcanDecrement/canIncrementinside the callback removes those dependencies and keeps the handlers stable.Not critical, but saves rerenders when the component is nested in lists.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/link-settings/form.tsx (1)
195-232: Repeatedwatch()calls inside map add unnecessary subscriptions
watch("urlValidationMode")is executed for everyURL_VALIDATION_MODES.map, creating multiple watchers each render.
Callwatchonce outside the loop:const urlValidationMode = watch("urlValidationMode"); ... URL_VALIDATION_MODES.map((mode) => { const isSelected = mode.value === urlValidationMode;Minor perf tidy-up.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/link-settings/form.tsx(5 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/link-settings/page.tsx(2 hunks)apps/web/lib/actions/partners/update-program.ts(2 hunks)apps/web/lib/zod/schemas/import-error-log.ts(1 hunks)apps/web/lib/zod/schemas/programs.ts(1 hunks)packages/ui/src/index.tsx(1 hunks)packages/ui/src/number-stepper.tsx(1 hunks)
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
Learnt from: TWilson023
PR: dubinc/dub#2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.936Z
Learning: In apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, the form schema uses partial condition objects to allow users to add empty/unconfigured condition fields without type errors, while submission validation uses strict schemas to ensure data integrity. This two-stage validation pattern improves UX by allowing progressive completion of complex forms.
Learnt from: TWilson023
PR: dubinc/dub#2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.131Z
Learning: In apps/web/ui/partners/rewards/rewards-logic.tsx, when setting the entity field in a reward condition, dependent fields (attribute, operator, value) should be reset rather than preserved because different entities (customer vs sale) have different available attributes. Maintaining existing fields when the entity changes would create invalid state combinations and confusing UX.
📚 Learning: 2025-06-06T07:59:03.120Z
Learnt from: devkiran
PR: dubinc/dub#2177
File: apps/web/lib/api/links/bulk-create-links.ts:66-84
Timestamp: 2025-06-06T07:59:03.120Z
Learning: In apps/web/lib/api/links/bulk-create-links.ts, the team accepts the risk of potential undefined results from links.find() operations when building invalidLinks arrays, because existing links are fetched from the database based on the input links, so matches are expected to always exist.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/link-settings/page.tsxapps/web/lib/actions/partners/update-program.tsapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/link-settings/form.tsx
📚 Learning: 2025-07-17T06:41:45.620Z
Learnt from: devkiran
PR: dubinc/dub#2637
File: apps/web/app/(ee)/api/singular/webhook/route.ts:0-0
Timestamp: 2025-07-17T06:41:45.620Z
Learning: In the Singular integration (apps/web/app/(ee)/api/singular/webhook/route.ts), the event names in the singularToDubEvent object have intentionally different casing: "Copy GAID" and "copy IDFA". This casing difference is valid and should not be changed, as these are the correct event names expected from Singular.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/link-settings/page.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/link-settings/form.tsx
📚 Learning: 2025-07-30T15:29:54.131Z
Learnt from: TWilson023
PR: dubinc/dub#2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.131Z
Learning: In apps/web/ui/partners/rewards/rewards-logic.tsx, when setting the entity field in a reward condition, dependent fields (attribute, operator, value) should be reset rather than preserved because different entities (customer vs sale) have different available attributes. Maintaining existing fields when the entity changes would create invalid state combinations and confusing UX.
Applied to files:
apps/web/lib/actions/partners/update-program.tsapps/web/lib/zod/schemas/programs.tsapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/link-settings/form.tsx
📚 Learning: 2025-07-30T15:25:13.936Z
Learnt from: TWilson023
PR: dubinc/dub#2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.936Z
Learning: In apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, the form schema uses partial condition objects to allow users to add empty/unconfigured condition fields without type errors, while submission validation uses strict schemas to ensure data integrity. This two-stage validation pattern improves UX by allowing progressive completion of complex forms.
Applied to files:
apps/web/lib/zod/schemas/programs.tsapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/link-settings/form.tsx
📚 Learning: 2025-05-29T09:47:33.583Z
Learnt from: devkiran
PR: dubinc/dub#2433
File: apps/web/app/api/workspaces/[idOrSlug]/billing/payment-methods/route.ts:74-76
Timestamp: 2025-05-29T09:47:33.583Z
Learning: When Zod enum validation is used (e.g., `z.enum(PAYMENT_METHOD_TYPES)`), it ensures the validated value is one of the valid enum values, making subsequent type casts to the corresponding TypeScript types safe without additional validation.
Applied to files:
apps/web/lib/zod/schemas/programs.ts
📚 Learning: 2025-05-29T04:45:18.504Z
Learnt from: devkiran
PR: dubinc/dub#2448
File: packages/email/src/templates/partner-program-summary.tsx:0-0
Timestamp: 2025-05-29T04:45:18.504Z
Learning: In the PartnerProgramSummary email template (packages/email/src/templates/partner-program-summary.tsx), the stat titles are hardcoded constants ("Clicks", "Leads", "Sales", "Earnings") that will always match the ICONS object keys after toLowerCase() conversion, so icon lookup failures are not possible.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/link-settings/form.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (5)
apps/web/lib/zod/schemas/import-error-log.ts (1)
18-19: Trailing comma is fine; no behavioral change.Consistent with common formatting rules (e.g., Prettier). Safe, no runtime impact.
packages/ui/src/index.tsx (1)
28-28: NumberStepper export verifiedThe
packages/ui/src/number-stepper.tsxfile exists and declares a named export (export function NumberStepper…at lines 19–20). Consumers import it as a named export (import { NumberStepper } from "@dub/ui"), so the re-export inpackages/ui/src/index.tsxis correct. No changes needed.apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/link-settings/page.tsx (1)
3-3: Import for LinksSettingsForm confirmed
Theform.tsxfile atapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/link-settings/form.tsxexportsLinksSettingsFormas a named function (line 37), so both imports on lines 3 and 18 are valid. No further changes needed.apps/web/lib/actions/partners/update-program.ts (1)
46-48: DB Defaults Confirmed for urlValidationMode and maxPartnerLinks
The Prisma schema defines both fields as non-nullable with defaults (@default(domain) and @default(10)), so the migration will backfill existing rows and guarantee non-null values. No additional backfill or code changes are required.packages/ui/src/number-stepper.tsx (1)
77-88: Disabled control is still keyboard-focusable
tabIndex={0}on therole="spinbutton"element lets users tab into the widget even whenaria-disabled="true". A disabled control should be removed from the tab order.- tabIndex={0} + tabIndex={disabled ? -1 : 0}Likely an incorrect or invalid review comment.
| <NumberStepper | ||
| value={watch("maxPartnerLinks")} | ||
| onChange={(v) => | ||
| setValue("maxPartnerLinks", v, { | ||
| shouldDirty: true, | ||
| shouldValidate: true, | ||
| }) | ||
| } | ||
| min={1} | ||
| max={999} | ||
| step={1} | ||
| className="w-full" | ||
| /> | ||
| </div> |
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.
Guard against undefined initial value
watch("maxPartnerLinks") can return undefined on first render (e.g. program prop missing).
NumberStepper expects a number, and passing undefined will trigger React warnings and sets aria-valuenow="undefined".
- value={watch("maxPartnerLinks")}
+ value={watch("maxPartnerLinks") ?? 1}Also consider adding a required default in useForm’s defaultValues to avoid the fallback altogether.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <NumberStepper | |
| value={watch("maxPartnerLinks")} | |
| onChange={(v) => | |
| setValue("maxPartnerLinks", v, { | |
| shouldDirty: true, | |
| shouldValidate: true, | |
| }) | |
| } | |
| min={1} | |
| max={999} | |
| step={1} | |
| className="w-full" | |
| /> | |
| </div> | |
| <NumberStepper | |
| value={watch("maxPartnerLinks") ?? 1} | |
| onChange={(v) => | |
| setValue("maxPartnerLinks", v, { | |
| shouldDirty: true, | |
| shouldValidate: true, | |
| }) | |
| } | |
| min={1} | |
| max={999} | |
| step={1} | |
| className="w-full" | |
| /> | |
| </div> |
🤖 Prompt for AI Agents
In
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/link-settings/form.tsx
around lines 175 to 188, the value passed to NumberStepper via
watch("maxPartnerLinks") can be undefined initially, causing React warnings and
invalid aria attributes. Fix this by providing a fallback number (e.g., 1) when
watch returns undefined, and also add a required default value for
maxPartnerLinks in the useForm defaultValues configuration to prevent undefined
on first render.
| urlValidationMode: z.nativeEnum(PartnerUrlValidationMode), | ||
| maxPartnerLinks: z.number().min(1).max(999), | ||
| supportEmail: z.string().email().max(255).nullish(), |
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.
💡 Verification agent
🧩 Analysis chain
Tighten numeric validation and ensure safe coercion for form submissions.
- Suggest enforcing integer and coercing from strings to handle form inputs consistently.
- Confirm a DB default/backfill exists so
ProgramSchema(which treats these as required) won’t encounter nulls for existing rows.
Apply:
- urlValidationMode: z.nativeEnum(PartnerUrlValidationMode),
- maxPartnerLinks: z.number().min(1).max(999),
+ urlValidationMode: z.nativeEnum(PartnerUrlValidationMode),
+ maxPartnerLinks: z.coerce.number().int().min(1).max(999),Optionally mirror constraints in ProgramSchema for consistency.
To verify Prisma enum/columns and defaults:
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Locate Prisma schema and check enum + model fields:"
rg -n -A3 -B3 'enum\\s+PartnerUrlValidationMode|model\\s+Program|urlValidationMode|maxPartnerLinks' | sed -n '1,200p'
echo "Search migrations mentioning added fields or backfills:"
rg -n -A3 -B3 'urlValidationMode|maxPartnerLinks' **/migrations/** || trueLength of output: 17511
Enforce integer coercion for maxPartnerLinks; Prisma defaults are in place
-
In apps/web/lib/zod/schemas/programs.ts, tighten maxPartnerLinks to coerce strings and enforce integers:
- maxPartnerLinks: z.number().min(1).max(999), + maxPartnerLinks: z.coerce.number().int().min(1).max(999),
-
No change needed for urlValidationMode — it’s already
z.nativeEnum(PartnerUrlValidationMode). -
Verified in packages/prisma/schema/program.prisma:
urlValidationMode PartnerUrlValidationMode @default(domain)maxPartnerLinks Int @default(10)
so new rows get defaults and no separate backfill is required.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| urlValidationMode: z.nativeEnum(PartnerUrlValidationMode), | |
| maxPartnerLinks: z.number().min(1).max(999), | |
| supportEmail: z.string().email().max(255).nullish(), | |
| urlValidationMode: z.nativeEnum(PartnerUrlValidationMode), | |
| maxPartnerLinks: z.coerce.number().int().min(1).max(999), | |
| supportEmail: z.string().email().max(255).nullish(), |
🤖 Prompt for AI Agents
In apps/web/lib/zod/schemas/programs.ts around lines 72 to 74, the
maxPartnerLinks schema should be updated to coerce input to an integer and
enforce it as an integer. Replace z.number() with z.coerce.number().int() and
keep the min and max constraints to ensure only integer values between 1 and 999
are accepted. No changes are needed for urlValidationMode or supportEmail.
| <div | ||
| role="spinbutton" | ||
| aria-valuenow={value} | ||
| aria-valuemin={min} | ||
| aria-valuemax={max} | ||
| tabIndex={0} | ||
| onKeyDown={(e) => { | ||
| if (disabled) { | ||
| return; | ||
| } | ||
|
|
||
| if (e.key === "ArrowLeft" || e.key === "ArrowDown") { | ||
| e.preventDefault(); | ||
| handleDecrement(); | ||
| } | ||
| if (e.key === "ArrowRight" || e.key === "ArrowUp") { | ||
| e.preventDefault(); | ||
| handleIncrement(); | ||
| } | ||
| }} | ||
| className={cn( | ||
| "flex min-w-0 flex-1 items-center justify-center px-3 text-sm text-neutral-900", | ||
| )} | ||
| prefix="" | ||
| > | ||
| {formatValue ? formatValue(value) : value} | ||
| </div> |
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.
🛠️ Refactor suggestion
Missing accessible label for spinbutton value
The div with role="spinbutton" exposes aria-valuenow but is not labelled (aria-label/aria-labelledby). Screen-reader users will only hear a number with no context.
Add an explicit label:
- role="spinbutton"
+ role="spinbutton"
+ aria-label="Current value"or reference an external <label> via aria-labelledby.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div | |
| role="spinbutton" | |
| aria-valuenow={value} | |
| aria-valuemin={min} | |
| aria-valuemax={max} | |
| tabIndex={0} | |
| onKeyDown={(e) => { | |
| if (disabled) { | |
| return; | |
| } | |
| if (e.key === "ArrowLeft" || e.key === "ArrowDown") { | |
| e.preventDefault(); | |
| handleDecrement(); | |
| } | |
| if (e.key === "ArrowRight" || e.key === "ArrowUp") { | |
| e.preventDefault(); | |
| handleIncrement(); | |
| } | |
| }} | |
| className={cn( | |
| "flex min-w-0 flex-1 items-center justify-center px-3 text-sm text-neutral-900", | |
| )} | |
| prefix="" | |
| > | |
| {formatValue ? formatValue(value) : value} | |
| </div> | |
| <div | |
| role="spinbutton" | |
| aria-label="Current value" | |
| aria-valuenow={value} | |
| aria-valuemin={min} | |
| aria-valuemax={max} | |
| tabIndex={0} | |
| onKeyDown={(e) => { | |
| if (disabled) { | |
| return; | |
| } | |
| if (e.key === "ArrowLeft" || e.key === "ArrowDown") { | |
| e.preventDefault(); | |
| handleDecrement(); | |
| } | |
| if (e.key === "ArrowRight" || e.key === "ArrowUp") { | |
| e.preventDefault(); | |
| handleIncrement(); | |
| } | |
| }} | |
| className={cn( | |
| "flex min-w-0 flex-1 items-center justify-center px-3 text-sm text-neutral-900", | |
| )} | |
| prefix="" | |
| > | |
| {formatValue ? formatValue(value) : value} | |
| </div> |
🤖 Prompt for AI Agents
In packages/ui/src/number-stepper.tsx around lines 102 to 128, the div with
role="spinbutton" lacks an accessible label, causing screen readers to announce
the value without context. Fix this by adding an aria-label attribute with a
descriptive text or by linking to an existing label using aria-labelledby to
provide context for the spinbutton's value.
Summary by CodeRabbit