Skip to content

Fix(webapp) onboarding fixes#3189

Open
samejr wants to merge 9 commits intomainfrom
fix(webapp)-onboarding-fixes
Open

Fix(webapp) onboarding fixes#3189
samejr wants to merge 9 commits intomainfrom
fix(webapp)-onboarding-fixes

Conversation

@samejr
Copy link
Member

@samejr samejr commented Mar 6, 2026

Fixes and improvements to the onboarding questions:

This change is worth double checking @matt-aitken

  • Update to the Button.tsx file: it now takes isLoading that shows a spinner in the middle of the button (replacing the button text and any icons) and sets it to disabled. It does this nicely by keeping the button width the same so there's no layout shift.

Other fixes

  • Fixes an issue where if you type a custom option in the "What technologies do you use" question, it doesn't check the list to see if it matches. Now it checks the box if you've typed an option from that list.
  • When we randomize the list of onboarding question options, we now store the position they appeared in the list

@changeset-bot
Copy link

changeset-bot bot commented Mar 6, 2026

⚠️ No Changeset found

Latest commit: f4a5b0b

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 6, 2026

Walkthrough

Adds an isLoading prop and spinner-overlay behavior to Button/ButtonContent, makes loading automatically disable interactions and affect shortcut handling, and updates multiple call sites to use isLoading instead of previous custom trailing-icon spinners. TechnologyPicker now rejects empty custom input, matches inputs case-insensitively against existing TECHNOLOGY_OPTIONS and uses existing options when matched, and only adds truly new custom values. Onboarding routes and confirm-basic-details now capture positional indices (e.g., referralSourcePosition, rolePosition, workingOnPositions, goalsPositions), include hidden inputs for those indices, add safeParseNumberArray, and persist these position fields alongside selections.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the changes and highlights key improvements, but is missing required template sections including issue reference, checklist completion, explicit testing steps, and changelog format. Complete the PR description template by adding the issue number, filling the checklist, documenting specific testing steps, and providing a formal changelog entry.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Fix(webapp) onboarding fixes' is generic and uses vague language ('fixes') without specifying the main changes. It mentions the area but lacks clarity on the primary changes. Consider a more specific title such as 'Add isLoading prop to Button and fix onboarding form handling' to clearly convey the main changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix(webapp)-onboarding-fixes

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.

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.

🧹 Nitpick comments (1)
apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx (1)

62-69: Consider extracting shared utilities.

The shuffleArray function is duplicated in confirm-basic-details.tsx and here. Similarly, safeParseStringArray and safeParseNumberArray could be shared. Consider extracting these to a shared utility file for better maintainability.

Also applies to: 195-214

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/routes/_app.orgs`.$organizationSlug_.projects.new/route.tsx
around lines 62 - 69, Extract the duplicated utility functions shuffleArray,
safeParseStringArray, and safeParseNumberArray into a single shared module
(e.g., a utils file) and export them; then replace the local definitions in both
confirm-basic-details.tsx and route.tsx with imports from that shared utility to
eliminate duplication and keep behavior identical (preserve generics and return
types), update any imports/usages accordingly, and run type checks to ensure
nothing breaks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/webapp/app/routes/_app.orgs`.$organizationSlug_.projects.new/route.tsx:
- Around line 62-69: Extract the duplicated utility functions shuffleArray,
safeParseStringArray, and safeParseNumberArray into a single shared module
(e.g., a utils file) and export them; then replace the local definitions in both
confirm-basic-details.tsx and route.tsx with imports from that shared utility to
eliminate duplication and keep behavior identical (preserve generics and return
types), update any imports/usages accordingly, and run type checks to ensure
nothing breaks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 405e81ce-fc18-4889-972b-eade0063caaf

📥 Commits

Reviewing files that changed from the base of the PR and between e64b101 and e67a202.

📒 Files selected for processing (5)
  • apps/webapp/app/components/onboarding/TechnologyPicker.tsx
  • apps/webapp/app/components/primitives/Buttons.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx
  • apps/webapp/app/routes/_app.orgs.new/route.tsx
  • apps/webapp/app/routes/confirm-basic-details.tsx
📜 Review details
⏰ 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). (28)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (1, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (7, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (6, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (2, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (8, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (7, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (3, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (5, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (5, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (3, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (4, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (8, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (6, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (1, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (2, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (4, 8)
  • GitHub Check: units / packages / 🧪 Unit Tests: Packages (1, 1)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - npm)
  • GitHub Check: sdk-compat / Deno Runtime
  • GitHub Check: sdk-compat / Bun Runtime
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - npm)
  • GitHub Check: sdk-compat / Node.js 20.20 (ubuntu-latest)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - pnpm)
  • GitHub Check: sdk-compat / Cloudflare Workers
  • GitHub Check: typecheck / typecheck
  • GitHub Check: sdk-compat / Node.js 22.12 (ubuntu-latest)
  • GitHub Check: Analyze (javascript-typescript)
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead

**/*.{ts,tsx}: In TypeScript SDK usage, always import from @trigger.dev/sdk, never from @trigger.dev/sdk/v3 or use deprecated client.defineJob
Import from @trigger.dev/core subpaths only, never from the root
Use the Run Engine 2.0 (@internal/run-engine) and redis-worker for all new work, not legacy V1 MarQS queue or deprecated V1 functions

Files:

  • apps/webapp/app/components/onboarding/TechnologyPicker.tsx
  • apps/webapp/app/components/primitives/Buttons.tsx
  • apps/webapp/app/routes/_app.orgs.new/route.tsx
  • apps/webapp/app/routes/confirm-basic-details.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx
{packages/core,apps/webapp}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use zod for validation in packages/core and apps/webapp

Files:

  • apps/webapp/app/components/onboarding/TechnologyPicker.tsx
  • apps/webapp/app/components/primitives/Buttons.tsx
  • apps/webapp/app/routes/_app.orgs.new/route.tsx
  • apps/webapp/app/routes/confirm-basic-details.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use function declarations instead of default exports

Files:

  • apps/webapp/app/components/onboarding/TechnologyPicker.tsx
  • apps/webapp/app/components/primitives/Buttons.tsx
  • apps/webapp/app/routes/_app.orgs.new/route.tsx
  • apps/webapp/app/routes/confirm-basic-details.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx
apps/webapp/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

Access all environment variables through the env export of env.server.ts instead of directly accessing process.env in the Trigger.dev webapp

Files:

  • apps/webapp/app/components/onboarding/TechnologyPicker.tsx
  • apps/webapp/app/components/primitives/Buttons.tsx
  • apps/webapp/app/routes/_app.orgs.new/route.tsx
  • apps/webapp/app/routes/confirm-basic-details.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx
apps/webapp/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

apps/webapp/**/*.{ts,tsx}: When importing from @trigger.dev/core in the webapp, use subpath exports from the package.json instead of importing from the root path
Follow the Remix 2.1.0 and Express server conventions when updating the main trigger.dev webapp

Files:

  • apps/webapp/app/components/onboarding/TechnologyPicker.tsx
  • apps/webapp/app/components/primitives/Buttons.tsx
  • apps/webapp/app/routes/_app.orgs.new/route.tsx
  • apps/webapp/app/routes/confirm-basic-details.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx
**/*.{js,ts,jsx,tsx,json,md,yaml,yml}

📄 CodeRabbit inference engine (AGENTS.md)

Format code using Prettier before committing

Files:

  • apps/webapp/app/components/onboarding/TechnologyPicker.tsx
  • apps/webapp/app/components/primitives/Buttons.tsx
  • apps/webapp/app/routes/_app.orgs.new/route.tsx
  • apps/webapp/app/routes/confirm-basic-details.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx
apps/**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (CLAUDE.md)

When modifying only server components (apps/webapp/, apps/supervisor/, etc.) with no package changes, add a .server-changes/ file instead of a changeset

Files:

  • apps/webapp/app/components/onboarding/TechnologyPicker.tsx
  • apps/webapp/app/components/primitives/Buttons.tsx
  • apps/webapp/app/routes/_app.orgs.new/route.tsx
  • apps/webapp/app/routes/confirm-basic-details.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx
🧠 Learnings (10)
📓 Common learnings
Learnt from: samejr
Repo: triggerdotdev/trigger.dev PR: 2964
File: apps/webapp/app/components/AskAI.tsx:121-141
Timestamp: 2026-01-28T16:57:47.620Z
Learning: In the trigger.dev webapp codebase, the Button component (apps/webapp/app/components/primitives/Buttons) does not spread unknown props to the underlying <button> element—it only passes specific props (type, disabled, onClick, name, value, ref, form, autoFocus). When using Radix UI's TooltipTrigger with asChild, a span wrapper around the Button is necessary to receive Radix props (aria-describedby, onPointerEnter, onPointerLeave, data-state) while the Button handles its own behavior. Directly making the Button the child of TooltipTrigger with asChild will break accessibility and tooltip functionality.
📚 Learning: 2026-02-11T16:37:32.429Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 3019
File: apps/webapp/app/components/primitives/charts/Card.tsx:26-30
Timestamp: 2026-02-11T16:37:32.429Z
Learning: In projects using react-grid-layout, avoid relying on drag-handle class to imply draggability. Ensure drag-handle elements only affect dragging when the parent grid item is configured draggable in the layout; conditionally apply cursor styles based on the draggable prop. This improves correctness and accessibility.

Applied to files:

  • apps/webapp/app/components/onboarding/TechnologyPicker.tsx
  • apps/webapp/app/components/primitives/Buttons.tsx
  • apps/webapp/app/routes/_app.orgs.new/route.tsx
  • apps/webapp/app/routes/confirm-basic-details.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx
📚 Learning: 2026-01-28T16:57:47.620Z
Learnt from: samejr
Repo: triggerdotdev/trigger.dev PR: 2964
File: apps/webapp/app/components/AskAI.tsx:121-141
Timestamp: 2026-01-28T16:57:47.620Z
Learning: In the trigger.dev webapp codebase, the Button component (apps/webapp/app/components/primitives/Buttons) does not spread unknown props to the underlying <button> element—it only passes specific props (type, disabled, onClick, name, value, ref, form, autoFocus). When using Radix UI's TooltipTrigger with asChild, a span wrapper around the Button is necessary to receive Radix props (aria-describedby, onPointerEnter, onPointerLeave, data-state) while the Button handles its own behavior. Directly making the Button the child of TooltipTrigger with asChild will break accessibility and tooltip functionality.

Applied to files:

  • apps/webapp/app/components/primitives/Buttons.tsx
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Use `useRun`, `useRealtimeRun` and other SWR/realtime hooks from `trigger.dev/react-hooks` for data fetching

Applied to files:

  • apps/webapp/app/components/primitives/Buttons.tsx
📚 Learning: 2026-02-03T18:27:40.429Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2994
File: apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx:553-555
Timestamp: 2026-02-03T18:27:40.429Z
Learning: In apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx, the menu buttons (e.g., Edit with PencilSquareIcon) in the TableCellMenu are intentionally icon-only with no text labels as a compact UI pattern. This is a deliberate design choice for this route; preserve the icon-only behavior for consistency in this file.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.new/route.tsx
  • apps/webapp/app/routes/confirm-basic-details.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx
📚 Learning: 2026-02-04T16:34:48.876Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2994
File: apps/webapp/app/routes/vercel.connect.tsx:13-27
Timestamp: 2026-02-04T16:34:48.876Z
Learning: In apps/webapp/app/routes/vercel.connect.tsx, configurationId may be absent for "dashboard" flows but must be present for "marketplace" flows. Enforce this with a Zod superRefine and pass installationId to repository methods only when configurationId is defined (omit the field otherwise).

Applied to files:

  • apps/webapp/app/routes/confirm-basic-details.tsx
📚 Learning: 2025-11-27T16:26:58.661Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/webapp.mdc:0-0
Timestamp: 2025-11-27T16:26:58.661Z
Learning: Applies to apps/webapp/**/*.{ts,tsx} : Follow the Remix 2.1.0 and Express server conventions when updating the main trigger.dev webapp

Applied to files:

  • apps/webapp/app/routes/confirm-basic-details.tsx
📚 Learning: 2025-12-08T15:19:56.823Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2760
File: apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx:278-281
Timestamp: 2025-12-08T15:19:56.823Z
Learning: In apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx, the tableState search parameter uses intentional double-encoding: the parameter value contains a URL-encoded URLSearchParams string, so decodeURIComponent(value("tableState") ?? "") is required to fully decode it before parsing with new URLSearchParams(). This pattern allows bundling multiple filter/pagination params as a single search parameter.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx
📚 Learning: 2025-09-03T14:34:41.781Z
Learnt from: myftija
Repo: triggerdotdev/trigger.dev PR: 2464
File: apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings/route.tsx:357-371
Timestamp: 2025-09-03T14:34:41.781Z
Learning: When using Zod's safeParse, the .data property is undefined when parsing fails, but TypeScript may still complain about accessing .data without checking .success first. The suggested approach of checking .success before accessing .data improves type safety and code clarity.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx
📚 Learning: 2025-08-14T10:09:02.528Z
Learnt from: nicktrn
Repo: triggerdotdev/trigger.dev PR: 2390
File: internal-packages/run-engine/src/engine/index.ts:466-467
Timestamp: 2025-08-14T10:09:02.528Z
Learning: In the triggerdotdev/trigger.dev codebase, it's acceptable to pass `string | undefined` types directly to Prisma operations (both create and update). The codebase consistently uses this pattern and the team is comfortable with how Prisma handles undefined values.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx
🔇 Additional comments (13)
apps/webapp/app/components/onboarding/TechnologyPicker.tsx (1)

211-228: LGTM! Well-implemented case-insensitive matching logic.

The addCustomValue function correctly:

  1. Rejects empty input early
  2. Matches typed values case-insensitively against existing TECHNOLOGY_OPTIONS
  3. Adds matched options to value (avoiding duplicates)
  4. Falls back to adding unmatched values as custom entries (avoiding duplicates in both customValues and value)

This addresses the PR objective of checking the corresponding list item when a typed custom option matches an existing option.

apps/webapp/app/components/primitives/Buttons.tsx (3)

210-219: LGTM! Good spinner delay implementation to prevent UI flicker.

The 200ms delay before showing the spinner is a solid UX pattern that avoids flashing spinners for fast operations. The effect correctly:

  • Resets showSpinner immediately when isLoading becomes false
  • Cleans up the timeout on unmount or when isLoading changes

241-298: LGTM! Clever approach to preserve button dimensions during loading.

Using invisible class (instead of removing content) ensures the button maintains its width while the spinner overlays. The absolute positioning of the spinner centers it correctly within the button bounds.


330-348: LGTM! Proper disabled state handling during loading.

The isDisabled derivation correctly combines disabled and isLoading, ensuring both the button element and keyboard shortcuts are disabled during loading. This prevents accidental double-submissions.

apps/webapp/app/routes/confirm-basic-details.tsx (4)

146-158: LGTM! Position data correctly nested within value checks.

The logic ensures referralSourcePosition and rolePosition are only stored when their corresponding main values (referralSource, role) exist, preventing orphaned position data.


330-338: LGTM! Position tracking hidden inputs correctly implemented.

The 1-based position calculation (indexOf + 1) properly captures where each option appeared in the randomized list, fulfilling the PR objective to persist randomized option positions.

Also applies to: 370-374


411-418: LGTM! Correct usage of the new isLoading prop.

The button properly consumes isSubmitting via the new isLoading prop, providing consistent loading feedback across the onboarding flow.


102-103: The current implementation already handles empty strings correctly via the falsy check.

When no selection is made, the hidden input value is an empty string "", which z.coerce.number() coerces to 0 (not NaN). The subsequent if (submission.value.referralSourcePosition) and if (submission.value.rolePosition) checks on lines 146 and 156 prevent storing falsy values (including 0), so the position is only stored when a valid selection exists. The code works as intended without additional preprocessing.

			> Likely an incorrect or invalid review comment.
apps/webapp/app/routes/_app.orgs.new/route.tsx (1)

223-225: LGTM! Clean adoption of the isLoading prop.

The button correctly uses the existing isLoading state that accounts for both submission and navigation loading states.

apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx (4)

205-214: LGTM! Consistent parsing helper implementation.

The safeParseNumberArray function follows the same pattern as safeParseStringArray, providing consistent error handling with graceful fallback to undefined.


219-223: LGTM! Position data correctly associated with parent values.

The positions are only stored when their corresponding arrays (workingOn, goals) have values, ensuring data integrity.

Also applies to: 236-240


400-406: LGTM! Hidden inputs correctly serialize position arrays.

The position arrays are computed using 1-based indexing and JSON-serialized for form submission. The server-side safeParseNumberArray correctly returns undefined for empty arrays (due to the length > 0 check), so empty selections won't create unnecessary data.

Also applies to: 452-458


483-485: LGTM! Consistent isLoading prop usage.

Aligns with the Button component changes and other routes updated in this PR.

@samejr samejr marked this pull request as ready for review March 6, 2026 16:19
devin-ai-integration[bot]

This comment was marked as resolved.

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

🧹 Nitpick comments (1)
apps/webapp/app/routes/confirm-basic-details.tsx (1)

142-157: Keep onboarding positions numeric in JSON.

The new String(...) conversions on Lines 147 and 157 make these fields inconsistent with the other onboarding position metadata, which is already stored as numbers / number arrays elsewhere. Persisting numeric indices as numbers here avoids mixed JSON shapes and simpler downstream reads.

Suggested consistency fix
-    const onboardingData: Record<string, string | undefined> = {};
+    const onboardingData: Record<string, string | number | undefined> = {};
@@
-      if (submission.value.referralSourcePosition) {
-        onboardingData.referralSourcePosition = String(submission.value.referralSourcePosition);
+      if (submission.value.referralSourcePosition && submission.value.referralSourcePosition > 0) {
+        onboardingData.referralSourcePosition = submission.value.referralSourcePosition;
       }
@@
-      if (submission.value.rolePosition) {
-        onboardingData.rolePosition = String(submission.value.rolePosition);
+      if (submission.value.rolePosition && submission.value.rolePosition > 0) {
+        onboardingData.rolePosition = submission.value.rolePosition;
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/routes/confirm-basic-details.tsx` around lines 142 - 157, The
onboarding position fields are being converted to strings (String(...)) which
creates mixed JSON shapes; instead preserve them as numbers and update the
onboardingData type so it can hold numbers. Replace the String(...) conversions
for submission.value.referralSourcePosition and submission.value.rolePosition
with the raw numeric values (e.g., submission.value.referralSourcePosition and
submission.value.rolePosition) and adjust the onboardingData type (the
onboardingData declaration and any related type annotations) to allow number
values (e.g., Record<string, string | number | undefined> or more specific
property types) so downstream code sees consistent numeric indices.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/webapp/app/routes/confirm-basic-details.tsx`:
- Around line 102-103: The schema currently uses z.coerce.number().optional()
for referralSourcePosition and rolePosition which turns empty string "" into 0;
change each to use z.preprocess(val => val === "" ? undefined : val,
z.coerce.number().optional()) so blank hidden inputs become undefined instead of
0, then add bounds checks (e.g., .refine(n => n === undefined || (n >= 1 && n <=
<optionsLength>), { message: "position must be between 1 and the number of
options" }) to enforce 1-based indices within the length of the corresponding
shuffled option arrays (validate referralSourcePosition against the referral
source options length and rolePosition against the role options length).

---

Nitpick comments:
In `@apps/webapp/app/routes/confirm-basic-details.tsx`:
- Around line 142-157: The onboarding position fields are being converted to
strings (String(...)) which creates mixed JSON shapes; instead preserve them as
numbers and update the onboardingData type so it can hold numbers. Replace the
String(...) conversions for submission.value.referralSourcePosition and
submission.value.rolePosition with the raw numeric values (e.g.,
submission.value.referralSourcePosition and submission.value.rolePosition) and
adjust the onboardingData type (the onboardingData declaration and any related
type annotations) to allow number values (e.g., Record<string, string | number |
undefined> or more specific property types) so downstream code sees consistent
numeric indices.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 88f4d448-b045-4b4a-8180-712d8d0fca9c

📥 Commits

Reviewing files that changed from the base of the PR and between e67a202 and f4a5b0b.

📒 Files selected for processing (1)
  • apps/webapp/app/routes/confirm-basic-details.tsx
📜 Review details
⏰ 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). (26)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (5, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (6, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (5, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (2, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (4, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (1, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (1, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (6, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (7, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (7, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (3, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (3, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (8, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (8, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (4, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (2, 8)
  • GitHub Check: sdk-compat / Bun Runtime
  • GitHub Check: units / packages / 🧪 Unit Tests: Packages (1, 1)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
  • GitHub Check: sdk-compat / Node.js 22.12 (ubuntu-latest)
  • GitHub Check: typecheck / typecheck
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - npm)
  • GitHub Check: sdk-compat / Node.js 20.20 (ubuntu-latest)
  • GitHub Check: sdk-compat / Cloudflare Workers
  • GitHub Check: sdk-compat / Deno Runtime
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - pnpm)
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead

**/*.{ts,tsx}: In TypeScript SDK usage, always import from @trigger.dev/sdk, never from @trigger.dev/sdk/v3 or use deprecated client.defineJob
Import from @trigger.dev/core subpaths only, never from the root
Use the Run Engine 2.0 (@internal/run-engine) and redis-worker for all new work, not legacy V1 MarQS queue or deprecated V1 functions

Files:

  • apps/webapp/app/routes/confirm-basic-details.tsx
{packages/core,apps/webapp}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use zod for validation in packages/core and apps/webapp

Files:

  • apps/webapp/app/routes/confirm-basic-details.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use function declarations instead of default exports

Files:

  • apps/webapp/app/routes/confirm-basic-details.tsx
apps/webapp/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

Access all environment variables through the env export of env.server.ts instead of directly accessing process.env in the Trigger.dev webapp

Files:

  • apps/webapp/app/routes/confirm-basic-details.tsx
apps/webapp/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

apps/webapp/**/*.{ts,tsx}: When importing from @trigger.dev/core in the webapp, use subpath exports from the package.json instead of importing from the root path
Follow the Remix 2.1.0 and Express server conventions when updating the main trigger.dev webapp

Files:

  • apps/webapp/app/routes/confirm-basic-details.tsx
**/*.{js,ts,jsx,tsx,json,md,yaml,yml}

📄 CodeRabbit inference engine (AGENTS.md)

Format code using Prettier before committing

Files:

  • apps/webapp/app/routes/confirm-basic-details.tsx
apps/**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (CLAUDE.md)

When modifying only server components (apps/webapp/, apps/supervisor/, etc.) with no package changes, add a .server-changes/ file instead of a changeset

Files:

  • apps/webapp/app/routes/confirm-basic-details.tsx
🧠 Learnings (5)
📓 Common learnings
Learnt from: samejr
Repo: triggerdotdev/trigger.dev PR: 2964
File: apps/webapp/app/components/AskAI.tsx:121-141
Timestamp: 2026-01-28T16:57:47.620Z
Learning: In the trigger.dev webapp codebase, the Button component (apps/webapp/app/components/primitives/Buttons) does not spread unknown props to the underlying <button> element—it only passes specific props (type, disabled, onClick, name, value, ref, form, autoFocus). When using Radix UI's TooltipTrigger with asChild, a span wrapper around the Button is necessary to receive Radix props (aria-describedby, onPointerEnter, onPointerLeave, data-state) while the Button handles its own behavior. Directly making the Button the child of TooltipTrigger with asChild will break accessibility and tooltip functionality.
📚 Learning: 2026-02-04T16:34:48.876Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2994
File: apps/webapp/app/routes/vercel.connect.tsx:13-27
Timestamp: 2026-02-04T16:34:48.876Z
Learning: In apps/webapp/app/routes/vercel.connect.tsx, configurationId may be absent for "dashboard" flows but must be present for "marketplace" flows. Enforce this with a Zod superRefine and pass installationId to repository methods only when configurationId is defined (omit the field otherwise).

Applied to files:

  • apps/webapp/app/routes/confirm-basic-details.tsx
📚 Learning: 2025-11-27T16:26:58.661Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/webapp.mdc:0-0
Timestamp: 2025-11-27T16:26:58.661Z
Learning: Applies to apps/webapp/**/*.{ts,tsx} : Follow the Remix 2.1.0 and Express server conventions when updating the main trigger.dev webapp

Applied to files:

  • apps/webapp/app/routes/confirm-basic-details.tsx
📚 Learning: 2026-02-03T18:27:40.429Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2994
File: apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx:553-555
Timestamp: 2026-02-03T18:27:40.429Z
Learning: In apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx, the menu buttons (e.g., Edit with PencilSquareIcon) in the TableCellMenu are intentionally icon-only with no text labels as a compact UI pattern. This is a deliberate design choice for this route; preserve the icon-only behavior for consistency in this file.

Applied to files:

  • apps/webapp/app/routes/confirm-basic-details.tsx
📚 Learning: 2026-02-11T16:37:32.429Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 3019
File: apps/webapp/app/components/primitives/charts/Card.tsx:26-30
Timestamp: 2026-02-11T16:37:32.429Z
Learning: In projects using react-grid-layout, avoid relying on drag-handle class to imply draggability. Ensure drag-handle elements only affect dragging when the parent grid item is configured draggable in the layout; conditionally apply cursor styles based on the draggable prop. This improves correctness and accessibility.

Applied to files:

  • apps/webapp/app/routes/confirm-basic-details.tsx
🔇 Additional comments (3)
apps/webapp/app/routes/confirm-basic-details.tsx (3)

7-7: LGTM: navigation-driven loading state.

This keeps the submit button busy through the submit → redirect path without introducing another route-local request flag.

Also applies to: 212-213


330-338: LGTM: positional metadata follows the rendered order.

Computing the hidden indices from shuffledReferralSources and shuffledRoles keeps the recorded position aligned with what the user actually saw.

Also applies to: 370-374


411-416: LGTM: primary CTA is using the shared loading API.

Switching this route to Button.isLoading centralizes spinner/disabled behavior and matches the rest of the rollout.

Comment on lines +102 to +103
referralSourcePosition: z.coerce.number().optional(),
rolePosition: z.coerce.number().optional(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Zod v3.25 z.coerce.number().optional() empty string behavior and best practices

💡 Result:

What happens in Zod v3.25 with z.coerce.number().optional() and "" (empty string)

  • z.coerce.number() coerces using JavaScript’s Number(input). (zod.dev)
  • In JavaScript, Number("") === 0, so parsing "" becomes 0 (not undefined), and then passes number validation. (zod.dev)
  • .optional() only makes the schema accept undefined (i.e., “missing”), not "". This is consistent with Zod’s “optional means undefined” design. (github.com)

So:

z.coerce.number().optional().parse("") // => 0

This is often surprising in HTML form scenarios because an “empty” input can incorrectly validate as 0 (especially bad when the field is “required”). (github.com)


Best-practice patterns (pick based on what “empty” should mean)

1) If empty should mean “missing” → convert "" to undefined before number parsing (recommended)

const optionalNumberFromForm = z.preprocess(
  (v) => (v === "" ? undefined : v),
  z.coerce.number().optional()
);

Now "" becomes undefined, and real numbers still coerce.

If you also want to treat whitespace as empty:

const optionalNumberFromForm = z.preprocess(
  (v) => (typeof v === "string" && v.trim() === "" ? undefined : v),
  z.coerce.number().optional()
);

Related ecosystem tooling: Conform’s coerceFormValue applies a similar rule by default (“empty string => undefined”). (conform.guide)


2) If the field is required, and empty should fail as “required” (not silently become 0)

Same preprocessing, but don’t add .optional():

const requiredNumberFromForm = z.preprocess(
  (v) => (v === "" ? undefined : v),
  z.coerce.number({ required_error: "Required" })
);

Because the empty value becomes undefined, Zod can raise your required_error instead of accepting 0. (zod.dev)


3) If you want to forbid coercing "" to 0 even for optional fields, but keep string inputs

Use a string-first approach and enforce non-empty before transforming:

const num = z
  .union([
    z.coerce.string().min(1).transform((s) => Number(s)),
    z.undefined(),
  ]);

This avoids the Number("") === 0 trap by never calling Number() on empty input. (This general approach is discussed as a workaround in the Zod community.) (github.com)


Rule of thumb

  • Avoid z.coerce.number().optional() directly on raw form values if the form can produce "".
  • Do preprocess ""undefined (or use a form helper that does it) when "" semantically means “not provided”. (conform.guide)

Citations:


🏁 Script executed:

# First, find and examine the file to see the actual implementation
cd /tmp && git -C /tmp/repo rev-parse --show-toplevel 2>/dev/null || echo "no git repo"

# Get the actual file content around the relevant lines
find . -name "confirm-basic-details.tsx" -type f 2>/dev/null | head -5

Repository: triggerdotdev/trigger.dev

Length of output: 80


🏁 Script executed:

# Find the repo and inspect the actual file
find / -type d -name "trigger.dev" 2>/dev/null | head -1 | xargs -I {} cat {}/apps/webapp/app/routes/confirm-basic-details.tsx 2>/dev/null | head -200

Repository: triggerdotdev/trigger.dev

Length of output: 51


🏁 Script executed:

# Alternative: search for the file in common locations and examine it
if [ -d "apps/webapp/app/routes" ]; then
  echo "=== File found in current context ===" 
  head -150 apps/webapp/app/routes/confirm-basic-details.tsx
  echo ""
  echo "=== Lines 100-110 context ==="
  sed -n '95,115p' apps/webapp/app/routes/confirm-basic-details.tsx
  echo ""
  echo "=== Lines 325-375 context (hidden inputs) ==="
  sed -n '325,375p' apps/webapp/app/routes/confirm-basic-details.tsx
else
  find . -name "confirm-basic-details.tsx" 2>/dev/null
fi

Repository: triggerdotdev/trigger.dev

Length of output: 8047


Treat blank positional inputs as undefined, not 0.

The hidden inputs submit "" when no value is chosen. Since z.coerce.number() coerces empty strings to 0 (per JavaScript's Number("") === 0), the current schema parses "" to 0 instead of undefined. While the code happens to work—because 0 is falsy and the position is not stored—this is semantically incorrect and relies on implicit falsy behavior. Additionally, there is no validation to ensure submitted position indices are within valid bounds (1-based, within the length of the shuffled options).

Use z.preprocess() to convert empty strings to undefined before coercion, and add bounds validation for the index range.

Suggested validation tightening
-      referralSourcePosition: z.coerce.number().optional(),
-      rolePosition: z.coerce.number().optional(),
+      referralSourcePosition: z.preprocess(
+        (value) => (value === "" ? undefined : value),
+        z.coerce.number().int().min(1).max(referralSourceOptions.length).optional()
+      ),
+      rolePosition: z.preprocess(
+        (value) => (value === "" ? undefined : value),
+        z.coerce.number().int().min(1).max(roleOptions.length).optional()
+      ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/routes/confirm-basic-details.tsx` around lines 102 - 103, The
schema currently uses z.coerce.number().optional() for referralSourcePosition
and rolePosition which turns empty string "" into 0; change each to use
z.preprocess(val => val === "" ? undefined : val, z.coerce.number().optional())
so blank hidden inputs become undefined instead of 0, then add bounds checks
(e.g., .refine(n => n === undefined || (n >= 1 && n <= <optionsLength>), {
message: "position must be between 1 and the number of options" }) to enforce
1-based indices within the length of the corresponding shuffled option arrays
(validate referralSourcePosition against the referral source options length and
rolePosition against the role options length).

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.

1 participant