Feature/sponsor bounty creation - #290
Conversation
- Remove unused useUserRole import and userRole variable from �pp/bounty/create/page.tsx (no-unused-vars) - Remove unused useUserRole import and userRole variable from components/global-navbar.tsx (no-unused-vars) - Add missing ole field to ExtendedUser interface in both files so user.role access is type-safe without runtime cast - Replace error: any in hooks/use-create-bounty.ts onError handler with unknown + instanceof Error guard (@typescript-eslint/no-explicit-any) - Replace four orm as any casts in �ounty-create-form.tsx with typed UseFormReturn<FieldValues> casts (@typescript-eslint/no-explicit-any) - Update currency picker from AQUA → EURC in �udget-input.tsx and schemas.ts to match issue spec (XLM, USDC, EURC) - Add unit tests for use-create-bounty hook covering success/error paths, cache invalidation, redirect, and toast notifications
|
@superman32432432 is attempting to deploy a commit to the Threadflow Team on Vercel. A member of the Team first needs to authorize it. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds a bounty creation page and multi-step form, broadens create access to sponsors or organization members, wires a creation mutation hook with cache invalidation and redirects, and updates the allowed reward asset from AQUA to EURC. ChangesBounty Creation Flow
Sequence Diagram(s)sequenceDiagram
participant User
participant CreateBountyPage
participant BountyCreateForm
participant useCreateBounty
participant useCreateBountyMutation
participant QueryClient
participant Router
participant toast
User->>CreateBountyPage: Open /bounty/create
CreateBountyPage->>BountyCreateForm: Render authorized form
User->>BountyCreateForm: Submit bounty details
BountyCreateForm->>useCreateBounty: createBounty(input)
useCreateBounty->>useCreateBountyMutation: mutate({ input })
alt success
useCreateBountyMutation-->>useCreateBounty: createBounty.id
useCreateBounty->>QueryClient: invalidate bountyKeys.lists()
useCreateBounty->>toast: success message
useCreateBounty->>Router: push("/bounty/" + id) or "/bounty"
else error
useCreateBounty->>toast: error message
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 OpenGrep (1.23.0)components/bounty/bounty-create-form.tsx┌──────────────┐ �[32m✔�[39m �[1mOpengrep OSS�[0m [00.13][ERROR]: unable to find a config; path Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
app/bounty/create/page.tsx (1)
8-15: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicated
ExtendedUserinterface across three files.The same
ExtendedUsershape is declared here, incomponents/global-navbar.tsx(Lines 28-35), and incomponents/bounty/bounty-create-form.tsx(Lines 8-15). The session-user casting (session?.user as ExtendedUser) and theisSponsorOrOrgMemberderivation are also duplicated. Extract the type and a smallisSponsorOrOrgMember(user)helper into a shared module so the gating contract stays consistent ifrole/organizationssemantics change.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/bounty/create/page.tsx` around lines 8 - 15, The `ExtendedUser` type and sponsor/org-member gating logic are duplicated across multiple components, so extract them into one shared module and reuse them from `app/bounty/create/page.tsx`, `components/global-navbar.tsx`, and `components/bounty/bounty-create-form.tsx`. Move the session-user cast (`session?.user as ExtendedUser`) and the `isSponsorOrOrgMember(user)` derivation into that shared helper so the role/organizations contract stays consistent in one place. Update the affected components to import the shared type and helper instead of redefining the interface locally.components/bounty/bounty-create-form.tsx (1)
256-263: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueSpecify a radix in
parseInt.parseInt(match[1])should pass radix10to avoid surprising parsing behavior and satisfy lint rules. The captured group is\d+so it's safe, but being explicit is the idiomatic choice.🔧 Proposed fix
- return match ? parseInt(match[1]) : undefined; + return match ? parseInt(match[1], 10) : undefined;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/bounty/bounty-create-form.tsx` around lines 256 - 263, The Github issue URL parsing helper parseGithubIssueNumber currently calls parseInt without an explicit radix, which can trip lint rules and lead to ambiguous parsing behavior. Update the parseGithubIssueNumber function to pass radix 10 when converting match[1], keeping the existing regex and return behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@components/bounty/bounty-create-form.tsx`:
- Around line 224-226: The default reward amount in the bounty form currently
uses an explicit any cast, which trips linting. Update the initial state in
bounty-create-form.tsx where the reward object is built so amount is typed
without any, either by modeling it as number | undefined or by casting through
unknown in the same spot. Keep the empty default behavior intact while removing
the no-explicit-any violation in the form state setup.
- Line 422: The SelectItem usage with an empty string value will crash at
runtime because the Radix-based Select primitive does not allow empty-string
item values. Update the Select setup in bounty-create-form so the “None” and
“Standard Bounty (No Window)” options use a non-empty sentinel value, and
normalize that sentinel back to unset/empty in the form wiring. Adjust the
relevant Select value/defaultValues handling and the onSubmit path so the
sentinel maps to undefined or "" consistently without ever passing "" into
SelectItem.
- Around line 68-70: The bounty form schema is using Zod 3-style
`required_error` options that are ignored in Zod 4, so update the validation
messages in `bounty-create-form.tsx` to use the `error` callback instead. In the
schema definition for `type`, `amount`, and `percentage`, follow the same
`error` pattern used in `schemas.ts` (for example, `budgetSchema`) and branch on
`issue.input` to return the required-vs-invalid message for each field. Use the
existing symbols `BountyType`, `z.nativeEnum`, and the form schema object to
locate and update the affected validators.
In `@hooks/__tests__/use-create-bounty.test.tsx`:
- Around line 26-41: The `@/lib/graphql/generated` mock is replacing the whole
module, so `BountyType` is missing and `MOCK_INPUT` crashes when
`BountyType.FixedPrice` is evaluated. Update the `jest.mock` factory in
`use-create-bounty.test.tsx` to preserve the module’s वास्तविक exports while
only overriding `useCreateBountyMutation`. Keep `BountyType` available from the
original generated module so the test setup can resolve it at import time.
---
Nitpick comments:
In `@app/bounty/create/page.tsx`:
- Around line 8-15: The `ExtendedUser` type and sponsor/org-member gating logic
are duplicated across multiple components, so extract them into one shared
module and reuse them from `app/bounty/create/page.tsx`,
`components/global-navbar.tsx`, and `components/bounty/bounty-create-form.tsx`.
Move the session-user cast (`session?.user as ExtendedUser`) and the
`isSponsorOrOrgMember(user)` derivation into that shared helper so the
role/organizations contract stays consistent in one place. Update the affected
components to import the shared type and helper instead of redefining the
interface locally.
In `@components/bounty/bounty-create-form.tsx`:
- Around line 256-263: The Github issue URL parsing helper
parseGithubIssueNumber currently calls parseInt without an explicit radix, which
can trip lint rules and lead to ambiguous parsing behavior. Update the
parseGithubIssueNumber function to pass radix 10 when converting match[1],
keeping the existing regex and return behavior unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f152c07f-c9ba-4fcc-bc8b-a7c20df8c0b0
📒 Files selected for processing (7)
app/bounty/create/page.tsxcomponents/bounty/bounty-create-form.tsxcomponents/bounty/forms/budget-input.tsxcomponents/bounty/forms/schemas.tscomponents/global-navbar.tsxhooks/__tests__/use-create-bounty.test.tsxhooks/use-create-bounty.ts
| type: z.nativeEnum(BountyType, { | ||
| required_error: "Please select a bounty type" | ||
| }), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical
🧩 Analysis chain
🌐 Web query:
In Zod 4, does z.object field schema still honor required_error, or must it use the unified error callback? Is z.ZodIssueCode.custom still valid in Zod 4?
💡 Result:
In Zod 4, the required_error and invalid_type_error parameters have been dropped [1]. Zod 4 standardizes error customization under a single, unified error parameter [2][1], which is the recommended way to handle custom error messaging within schema definitions. Regarding z.ZodIssueCode.custom: Yes, it remains a valid issue code in Zod 4 and continues to be used for custom validation issues, such as those generated by refinements [3][4]. While Zod 4 allows for more flexibility with issue codes, z.ZodIssueCode.custom is explicitly defined in the core types and remains supported for custom logic and error handling [3]. Summary of changes: - Required_error/invalid_type_error: Dropped in Zod 4 in favor of the unified error parameter [1]. - Unified error parameter: This now serves as the standard for schema-level error customization [2][1]. - z.ZodIssueCode.custom: Remains valid and active for custom refinements and issue generation [3].
Citations:
- 1: https://zod.dev/v4/changelog
- 2: https://zod.dev/v4
- 3: https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/core/errors.ts
- 4: https://github.com/colinhacks/zod/blob/v4.4.3/packages/docs-v3/ERROR_HANDLING.md
🏁 Script executed:
cat package.json | grep -A 2 -B 2 zodRepository: boundlessfi/bounties
Length of output: 260
🏁 Script executed:
find . -name "schemas.ts" -o -name "schema.ts" -o -name "validation.ts" | head -5Repository: boundlessfi/bounties
Length of output: 484
🏁 Script executed:
cat components/bounty/bounty-create-form.tsx | sed -n '65,100p'Repository: boundlessfi/bounties
Length of output: 1390
🏁 Script executed:
cat components/bounty/forms/schemas.ts | head -100Repository: boundlessfi/bounties
Length of output: 2231
Zod 4 API mismatch: required_error is unsupported; switch to the error callback.
The project uses zod@4.3.5, where required_error and invalid_type_error were removed in favor of the unified error callback. The current schema definitions silently ignore these options, resulting in generic Zod error messages instead of the intended custom ones.
Align with the existing schemas.ts pattern (e.g., budgetSchema) by replacing required_error with an error function:
typefield (Line 68): Replacez.nativeEnum(BountyType, { required_error: ... })withz.nativeEnum(BountyType, { error: (issue) => issue.input === undefined ? "Please select a bounty type" : "Invalid type" }).amountfield (Line 80): Replace.number({ required_error: ... })with.number({ error: (issue) => issue.input === undefined ? "Amount is required" : "Amount must be a number" }).percentagefield (Line 95): Replace.number({ required_error: ... })with.number({ error: (issue) => issue.input === undefined ? "% is required" : "Percentage must be a number" }).
Note: While z.nativeEnum remains available for TypeScript enums in Zod 4, the error configuration syntax has changed to the error callback as shown above.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/bounty/bounty-create-form.tsx` around lines 68 - 70, The bounty
form schema is using Zod 3-style `required_error` options that are ignored in
Zod 4, so update the validation messages in `bounty-create-form.tsx` to use the
`error` callback instead. In the schema definition for `type`, `amount`, and
`percentage`, follow the same `error` pattern used in `schemas.ts` (for example,
`budgetSchema`) and branch on `issue.input` to return the required-vs-invalid
message for each field. Use the existing symbols `BountyType`, `z.nativeEnum`,
and the form schema object to locate and update the affected validators.
| reward: { | ||
| amount: undefined as any, | ||
| asset: "USDC", |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Avoid as any for the reward amount default (ESLint failing). Static analysis flags @typescript-eslint/no-explicit-any at Line 225. Cast through unknown (or model the field as number | undefined) to keep the empty initial state without any.
🔧 Proposed fix
reward: {
- amount: undefined as any,
+ amount: undefined as unknown as number,
asset: "USDC",
},📝 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.
| reward: { | |
| amount: undefined as any, | |
| asset: "USDC", | |
| reward: { | |
| amount: undefined as unknown as number, | |
| asset: "USDC", |
🧰 Tools
🪛 ESLint
[error] 225-225: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/bounty/bounty-create-form.tsx` around lines 224 - 226, The default
reward amount in the bounty form currently uses an explicit any cast, which
trips linting. Update the initial state in bounty-create-form.tsx where the
reward object is built so amount is typed without any, either by modeling it as
number | undefined or by casting through unknown in the same spot. Keep the
empty default behavior intact while removing the no-explicit-any violation in
the form state setup.
Source: Linters/SAST tools
| </SelectTrigger> | ||
| </FormControl> | ||
| <SelectContent> | ||
| <SelectItem value="">None</SelectItem> |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
<SelectItem value=""> will crash at runtime (Radix constraint). Radix UI Select.Item forbids an empty-string value ("A <Select.Item /> must have a value prop that is not an empty string"). Both the projectId "None" option (Line 422) and the "Standard Bounty (No Window)" option (Line 490) use value="" and will throw when the dropdown renders. Use a sentinel value and map it back to undefined/"" in onSubmit.
🐛 Proposed direction
- <SelectItem value="">None</SelectItem>
+ <SelectItem value="none">None</SelectItem>- <SelectItem value="">Standard Bounty (No Window)</SelectItem>
+ <SelectItem value="none">Standard Bounty (No Window)</SelectItem>Then normalize in onSubmit (treat "none" as unset) and in the defaultValues/Select value wiring.
Confirm the Select primitive is Radix-based (shadcn/ui) where the empty-value constraint applies:
#!/bin/bash
fd -t f 'select.tsx' components/ui --exec sed -n '1,40p' {}Also applies to: 490-490
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/bounty/bounty-create-form.tsx` at line 422, The SelectItem usage
with an empty string value will crash at runtime because the Radix-based Select
primitive does not allow empty-string item values. Update the Select setup in
bounty-create-form so the “None” and “Standard Bounty (No Window)” options use a
non-empty sentinel value, and normalize that sentinel back to unset/empty in the
form wiring. Adjust the relevant Select value/defaultValues handling and the
onSubmit path so the sentinel maps to undefined or "" consistently without ever
passing "" into SelectItem.
| jest.mock("@/lib/graphql/generated", () => ({ | ||
| useCreateBountyMutation: (options: { | ||
| onSuccess: (data: unknown) => void; | ||
| onError: (error: unknown) => void; | ||
| }) => { | ||
| capturedOnSuccess = options.onSuccess; | ||
| capturedOnError = options.onError; | ||
| return { | ||
| mutate: mockMutate, | ||
| mutateAsync: mockMutateAsync, | ||
| isPending: false, | ||
| isError: false, | ||
| isSuccess: false, | ||
| }; | ||
| }, | ||
| })); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -nP 'export (enum|const) BountyType|BountyType' lib/graphql/generated.ts | head
fd -t f 'use-create-bounty.test.tsx' --exec sed -n '26,82p' {}Repository: boundlessfi/bounties
Length of output: 2122
Mock omits BountyType, causing suite crash. The factory for @/lib/graphql/generated defines only useCreateBountyMutation but the test imports BountyType from that same module. Since BountyType is dereferenced at module evaluation time in MOCK_INPUT (BountyType.FixedPrice), the missing export resolves to undefined, throwing a TypeError that fails all tests.
🐛 Proposed fix: preserve real exports while overriding the mutation hook
jest.mock("`@/lib/graphql/generated`", () => ({
+ ...jest.requireActual("`@/lib/graphql/generated`"),
useCreateBountyMutation: (options: {
onSuccess: (data: unknown) => void;
onError: (error: unknown) => void;
}) => {📝 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.
| jest.mock("@/lib/graphql/generated", () => ({ | |
| useCreateBountyMutation: (options: { | |
| onSuccess: (data: unknown) => void; | |
| onError: (error: unknown) => void; | |
| }) => { | |
| capturedOnSuccess = options.onSuccess; | |
| capturedOnError = options.onError; | |
| return { | |
| mutate: mockMutate, | |
| mutateAsync: mockMutateAsync, | |
| isPending: false, | |
| isError: false, | |
| isSuccess: false, | |
| }; | |
| }, | |
| })); | |
| jest.mock("`@/lib/graphql/generated`", () => ({ | |
| ...jest.requireActual("`@/lib/graphql/generated`"), | |
| useCreateBountyMutation: (options: { | |
| onSuccess: (data: unknown) => void; | |
| onError: (error: unknown) => void; | |
| }) => { | |
| capturedOnSuccess = options.onSuccess; | |
| capturedOnError = options.onError; | |
| return { | |
| mutate: mockMutate, | |
| mutateAsync: mockMutateAsync, | |
| isPending: false, | |
| isError: false, | |
| isSuccess: false, | |
| }; | |
| }, | |
| })); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@hooks/__tests__/use-create-bounty.test.tsx` around lines 26 - 41, The
`@/lib/graphql/generated` mock is replacing the whole module, so `BountyType` is
missing and `MOCK_INPUT` crashes when `BountyType.FixedPrice` is evaluated.
Update the `jest.mock` factory in `use-create-bounty.test.tsx` to preserve the
module’s वास्तविक exports while only overriding `useCreateBountyMutation`. Keep
`BountyType` available from the original generated module so the test setup can
resolve it at import time.
Benjtalkshow
left a comment
There was a problem hiding this comment.
Thanks for the work on this, but there are a few blockers before merge:
pnpm tsc --noEmit and pnpm lint both fail (Zod v4 uses error: not
required_error:, the as UseFormReturn<FieldValues> casts are rejected, and
form.watch("type") needs useWatch per the lint rule). On the design side
the form drops deadline, startDate, endDate, and milestones at submit
since CreateBountyInput doesn't accept them, useUserRole is bypassed in
the page and navbar (please extend the hook to expose org membership instead),
and mock organizations and mockProjects are showing up in the production
flow. Please also fix the empty-string <SelectItem value=""> (Radix throws
on this), keep AQUA alongside EURC, and split the 805-line component into
per-step files under components/bounty/create-form/. Once those are
addressed, please attach screenshots of step 1, step 2, and the review screen
for each bounty type so I can verify the UI before re-reviewing.
closes #181
Summary by CodeRabbit