feat: implement request revisions flow (Fixes #202) - #257
Conversation
- Moved all mock data to lib/mock/ - Exported factories for building fixtures - Cleaned up ProjectCard and app/discover types - Deleted old lib/mock-*.ts files
… search - Move mock data into `lib/mock` directory using factory patterns - Extract `SidebarCTA` and `MobileCTA` into isolated components via `useBountyCTAState` hook - Enhance `search-command` to query cached bounties and fuzzy-search projects and pages - Verified no type or lint errors remaining
…unties - Add useAdvanceMilestone, useRemoveFromSlot, useReleaseMilestonePayment hooks - Wire Advance, Remove, Release Payment, Message, View Submissions actions - Optimistic cache updates for advance and remove operations - Toast feedback for all actions (success/error/info) - Add aria-labels to icon-only buttons for accessibility - Remove all [Coming soon] stubs from button text and tooltips - Pass bountyId prop from bounty-detail-client to dashboard Closes #205
… panel - Add useRequestRevisions hook with optimistic status update - Replace Coming Soon placeholder with interactive revision form - Textarea for reviewer feedback with cancel/submit controls - Toast notifications for success/error states - Surface latest revision feedback on contributor submit work panel - Show submit panel for REVISION_REQUESTED status - Button text adapts: 'Submit' vs 'Resubmit' based on revision state Closes #202
- Extended user session type with role: 'sponsor' | 'contributor' - Added 'Switch to Sponsor' toggle on /settings - Gated Create Bounty link in navbar to only show for sponsors - Created /bounty/create page that redirects non-sponsors Closes #207
- Added useRaiseDispute mutation - Created DisputeDialog with validation for reason and description - Wired dialog into mobile and sidebar CTAs - Replaced 'Coming Soon' placeholder with fully functional redirect Closes #203
- Added 'latestRevisionFeedback' to Bounty type in types/bounty.ts - Replaced scattered 'as any' and verbose casts in hooks/use-bounty-application.ts with typed BountyQuery intersection - Cleaned up ad-hoc cast for latestRevisionFeedback in bounty-detail-client.tsx Closes #211
… dev tools button
…mponents (#182) - Decompose monolithic notification-center.tsx into notification-bell, notification-list, and notification-item components - Add per-type Lucide icons with distinct color coding per notification type - Add resourceUrl to NotificationItem for click-to-navigate behavior - Implement date grouping (Today / Yesterday / Earlier) with sticky headers - Add clearAll function to reset notifications and localStorage - Extend NotificationType with dispute-raised and payment-received - Gate NotificationCenter on authenticated session in global navbar - Replace notification-center.tsx with thin backward-compatible re-export Closes #182
|
@Ishant5436 is attempting to deploy a commit to the Threadflow Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughThis PR implements major platform features including a 3-step bounty creation wizard, comprehensive bounty action workflows (apply for slot, decline applicants, request revisions), a notifications system with grouping and digest preview, and refactors mock data infrastructure. It also adds sponsor role support with access control and consolidates skeleton loading components into a shared module. ChangesCore Feature Implementation
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
✨ Finishing Touches🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
|
There was a problem hiding this comment.
Actionable comments posted: 15
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
lib/graphql/generated.ts (1)
1958-1997:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winRevisit
BountyFieldsFragmentvstypes/bounty.tsformaxParticipants/claimCount
lib/graphql/generated.ts’sBountyFieldsFragmentdoes not includemaxParticipantsorclaimCount(it only includes_count?.submissionsfor counts).types/bounty.tsstill declaresclaimCount?: number | nullandmaxParticipants?: number | null(lines ~132-133), but components already treat these as pending/optional:
bounty-card.tsxhard-codesmaxParticipants = nullsidebar-cta.tsxderivesclaimCountfrombounty._count?.submissions || 0, andcompetition-status.tsxonly renders/maxParticipantswhen it’s non-null.If
maxParticipants/claimCountare now present in the backend schema, update the GraphQL fragment/queries to select them (to keep generated types and UI expectations in sync). If not, consider aligning/removing the optional fields intypes/bounty.tsto match the current fragment shape.🤖 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 `@lib/graphql/generated.ts` around lines 1958 - 1997, The BountyFieldsFragment in lib/graphql/generated.ts is missing maxParticipants and claimCount whereas types/bounty.ts and UI components (bounty-card.tsx, sidebar-cta.tsx, competition-status.tsx) expect those fields; either add maxParticipants and claimCount to the GraphQL fragment(s) that produce BountyFieldsFragment (and any related queries/mutations) and re-run the codegen to regenerate lib/graphql/generated.ts, or remove/align the optional claimCount/maxParticipants properties from types/bounty.ts and update the components to derive counts from _count?.submissions consistently; reference the fragment name BountyFieldsFragment and the types file types/bounty.ts when making the change and ensure generated types are regenerated.
🧹 Nitpick comments (9)
lib/mock/bounties.ts (1)
279-282: ⚡ Quick winAvoid shared nested references in mock factory output.
This factory only clones the top level, so nested fields still point to
mockBounties[0]internals and can leak mutations across tests/stories.Proposed fix
export const makeMockBounty = (overrides?: Partial<Bounty>): Bounty => ({ - ...mockBounties[0], - ...overrides, -}); + ...structuredClone(mockBounties[0]), + ...structuredClone(overrides ?? {}), +});🤖 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 `@lib/mock/bounties.ts` around lines 279 - 282, makeMockBounty currently shallow-copies mockBounties[0] so nested objects/arrays remain shared; change makeMockBounty to return a deep-cloned base (e.g., use structuredClone(mockBounties[0]) or a deep-clone utility like lodash/cloneDeep) and then apply overrides so the returned Bounty has no shared nested references (update the makeMockBounty function and keep using overrides?: Partial<Bounty> spread afterward).components/cards/project-card.tsx (1)
94-97: 💤 Low valueConsider a more fitting icon for the creator line.
The
Clockicon next to "Created by {creatorName}" reads as time-related rather than authorship. AUser(or similar) icon would communicate intent more clearly.♻️ Proposed tweak
-import { Calendar, Clock, CheckCircle2, Pause, Activity } from "lucide-react"; +import { Calendar, User, CheckCircle2, Pause, Activity } from "lucide-react";<div className="flex items-center gap-1"> - <Clock className="h-3 w-3" /> + <User className="h-3 w-3" /> <span>Created by {project.creatorName}</span> </div>🤖 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/cards/project-card.tsx` around lines 94 - 97, The creator line currently uses the Clock icon (Clock) which implies time rather than authorship; replace Clock with a more appropriate User-type icon (e.g., User or UserIcon) in the JSX for the ProjectCard component (the div showing "Created by {project.creatorName}"), and update the import statements at the top of the file to import the chosen User icon instead of Clock (or keep Clock if used elsewhere and add the User import). Ensure the className/h sizing (e.g., "h-3 w-3") is applied to the new icon so visual alignment remains consistent.components/leaderboard/leaderboard-table.tsx (1)
41-49: 💤 Low valueRename the observer callback parameter to avoid shadowing the
entriesprop.The callback parameter
entriesshadows the component'sentries: LeaderboardEntry[]prop. It works today sinceentries[0].isIntersectingcorrectly reads the observer entries, but it's a footgun for future edits that intend to reference the prop inside the callback.♻️ Proposed rename
- const observer = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) { + const observer = new IntersectionObserver( + (observerEntries) => { + if (observerEntries[0].isIntersecting && hasNextPage && !isFetchingNextPage) { onLoadMore(); } }, { threshold: 0.1 }, );🤖 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/leaderboard/leaderboard-table.tsx` around lines 41 - 49, The IntersectionObserver callback in useEffect is shadowing the component prop entries: LeaderboardEntry[]; rename the callback parameter (e.g., to observerEntries or ioEntries) so it no longer hides the prop, update the check from entries[0].isIntersecting to the new name, and keep the existing guards (hasNextPage, isFetchingNextPage) and call to onLoadMore() unchanged; this affects the IntersectionObserver instantiation inside useEffect in leaderboard-table.tsx.app/bounty/create/page.tsx (1)
15-17: Ensure the sponsor check is also enforced server-side on the mutation.This page-level redirect protects the UI, but the actual
createBountyGraphQL mutation must independently authorize the sponsor role on the backend. A client-routed page guard is insufficient to prevent non-sponsors from invoking the mutation directly.🤖 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 15 - 17, The page-level redirect only protects the UI; enforce the sponsor role on the backend by adding an authorization check in the createBounty GraphQL mutation resolver (or its middleware) to verify the authenticated user's role is "sponsor" before creating a bounty. Locate the createBounty resolver/handler and: (1) fetch the current user from the request/context, (2) if user is missing or user.role !== "sponsor" throw an authorization error (e.g., Forbidden/401), and (3) only proceed to validate input and persist the bounty when the role check passes; ensure tests or error messages clearly indicate the missing sponsor permission.components/bounty/bounty-create-wizard.tsx (2)
252-291: ⚡ Quick winRemove the redundant outer
FormFieldwrapper fortype.The outer
FormField name="type"render callback simply returns an identical innerFormField name="type". The wrapper adds nothing and can be deleted.♻️ Proposed fix
<FormField control={form.control} - name="type" // Map correctly to UI label but field is "type" - render={() => ( - <FormField - control={form.control} - name="type" - render={({ field }) => ( + name="type" + render={({ field }) => ( <FormItem> <FormLabel>Bounty Type</FormLabel> <Select onValueChange={field.onChange} value={field.value} > <FormControl> <SelectTrigger className="bg-background/50"> <SelectValue placeholder="Select type" /> </SelectTrigger> </FormControl> <SelectContent> <SelectItem value={BountyType.FixedPrice}> Fixed Price </SelectItem> <SelectItem value={BountyType.Competition}> Competition </SelectItem> <SelectItem value={BountyType.MilestoneBased}> Milestone Based </SelectItem> <SelectItem value={BountyType.MultiWinnerMilestone}> Multi-winner Milestone </SelectItem> </SelectContent> </Select> <FormMessage /> </FormItem> - )} - /> )} />🤖 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-wizard.tsx` around lines 252 - 291, Remove the redundant outer FormField wrapper for the "type" field: delete the outer FormField that has name="type" and its render wrapper, and keep the inner FormField (the one that actually provides render={({ field }) => (...)}) which contains FormItem, FormLabel, Select, SelectTrigger/SelectValue, SelectContent with SelectItem values using BountyType, and FormMessage; ensure the retained inner FormField continues to use control={form.control}, name="type", field.onChange for Select.onValueChange and field.value for Select.value so form wiring is unchanged.
47-47: 💤 Low valueReplace deprecated
z.nativeEnumwithz.enumforBountyType
z.nativeEnumis deprecated in Zod 4. SinceBountyTypeis a string TypeScript enum (lib/graphql/generated.ts), switch toz.enum(BountyType)instead ofz.nativeEnum(BountyType)incomponents/bounty/bounty-create-wizard.tsx(line ~47).type: z.nativeEnum(BountyType),🤖 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-wizard.tsx` at line 47, Replace the deprecated z.nativeEnum usage for the BountyType field: update the schema line using BountyType in components/bounty/bounty-create-wizard.tsx from "type: z.nativeEnum(BountyType)" to use z.enum with the BountyType values (e.g. type: z.enum(BountyType) or z.enum(Object.values(BountyType) as [string, ...string[]]) if needed), and ensure any import of z is unchanged; reference symbol: BountyType from lib/graphql/generated.ts and the schema field in bounty-create-wizard.tsx.hooks/use-user-mutations.ts (1)
13-13: ⚡ Quick winNarrow
roleto the actual allowed union.
role?: stringis too permissive and can leak invalid values intoupdateUser. Align this type with the UI/schema contract.Suggested fix
export interface UpdateUserParams { @@ - role?: string; + role?: "sponsor" | "contributor"; }🤖 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/use-user-mutations.ts` at line 13, The optional property role is declared as role?: string which is too permissive; replace it with the exact union or enum used by your app (e.g. the UserRole union/enum from your schema or UI types) so only valid roles flow into updateUser; locate the role declaration in hooks/use-user-mutations.ts and change its type to the canonical type (or import and use the existing UserRole/Role type) so updateUser receives only allowed values.components/bounty-detail/model4-maintainer-dashboard.tsx (1)
60-75: 💤 Low valueHandle edge case when
currentMilestoneIdis not found in milestones array.If
milestones.findIndex()returns-1(milestone not found), the code proceeds to advance tomilestones[0], which may not be the intended behavior. Consider adding a guard for this unlikely but possible edge case.Proposed fix
if (action === "Advance") { if (!currentMilestoneId) throw new Error("No current milestone"); const currentIndex = milestones.findIndex( (m) => m.id === currentMilestoneId, ); + if (currentIndex === -1) { + throw new Error("Current milestone not found"); + } if (currentIndex < milestones.length - 1) { const nextMilestone = milestones[currentIndex + 1];🤖 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-detail/model4-maintainer-dashboard.tsx` around lines 60 - 75, The Advance branch does not handle the case where milestones.findIndex(...) returns -1, causing it to treat index -1 as valid; update the action === "Advance" flow to check currentIndex for -1 after computing currentIndex (from milestones.findIndex using currentMilestoneId) and handle it explicitly (e.g., throw or toast an error and return) instead of proceeding to pick nextMilestone; ensure you only call advanceMutation.mutateAsync with nextMilestone when currentIndex >= 0 and currentIndex < milestones.length - 1, and keep the existing success toasts (toast.success) unchanged for the normal paths.components/projects/project-bounties.tsx (1)
98-98: ⚡ Quick winRemove the unnecessary cast in type filter click handler.
At Line 98,
type.valuealready matches the state type (BountyType | "all"), soas BountyTypeis an unsafe narrowing and should be removed.Proposed fix
- onClick={() => setSelectedType(type.value as BountyType)} + onClick={() => setSelectedType(type.value)}🤖 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/projects/project-bounties.tsx` at line 98, The onClick handler is using an unnecessary and unsafe cast "as BountyType" when calling setSelectedType; remove the cast so it becomes setSelectedType(type.value) and ensure the type of type.value already matches the state union (BountyType | "all") so TypeScript accepts it; update any local typings for the variable named type (or its source array) if needed so type.value is correctly inferred as BountyType | "all" for the setSelectedType call (refer to setSelectedType and the type.value usage in the click handler).
🤖 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 `@app/api/leaderboard/route.ts`:
- Around line 7-9: The page and limit query params (variables page and limit
derived from searchParams in route.ts) are parsed without validation and can be
NaN, negative, zero, or unreasonably large; update the parsing logic to validate
and clamp them: parse to integer, fall back to safe defaults when isNaN, enforce
page >= 1, enforce limit between 1 and a reasonable max (e.g., 100) and ensure
both are integers (Math.floor behavior), then use these sanitized values in the
ranking/pagination logic; keep tier parsing as-is but validate it against the
ReputationTier enum if necessary.
In `@app/bounty/create/page.tsx`:
- Around line 28-32: The JSX contains a duplicate nested CardContent that
applies pt-6 twice; remove the inner (or outer) redundant CardContent so
BountyCreateWizard is contained by a single CardContent with the intended
padding. Locate the CardContent wrappers around <BountyCreateWizard /> and
delete the extra CardContent element to avoid double padding and preserve the
remaining CardContent className="pt-6".
In `@components/bounty-detail/bounty-detail-client.tsx`:
- Around line 153-155: The current assigned-user check includes a too-broad
fallback `(!isCreator && bounty.status === "IN_PROGRESS")` which misidentifies
any non-creator as assigned; update the logic in the component where you compute
assignment (referencing bounty?.assignedContributorId, session?.user?.id,
bounty?.submissions, isCreator, bounty.status) to remove that fallback and
instead rely only on the explicit assignedContributorId match or a definitive
submission-based check (e.g., verify bounty?.submissions.some(s => s.submittedBy
=== session?.user?.id && s.status === 'ACCEPTED') if your domain uses an
accepted submission flag) so only the actual assigned/accepted user is
considered assigned.
- Around line 241-242: The UI and hook use an undocumented status
"REVISION_REQUESTED" causing schema/type mismatch; either remove/replace that
value with a valid enum member (e.g., use "UNDER_REVIEW" or "IN_PROGRESS"
depending on intended behavior) or add "REVISION_REQUESTED" to
lib/graphql/schema.graphql and re-generate types in lib/graphql/generated.ts and
backend handling. Concretely, update the conditional in
components/bounty-detail/bounty-detail-client.tsx to check only valid
BountyStatus values (replace (bounty.status as string) === "REVISION_REQUESTED"
with the chosen valid enum), and update hooks/use-bounty-application.ts where
ReviewSubmissionDocument input and optimistic updates set status:
"REVISION_REQUESTED" to use the same valid enum; if you choose to add the new
status, modify schema.graphql, regenerate the generated.ts types, and ensure the
backend accepts the new enum.
In `@components/bounty-detail/milestone-submission-card.tsx`:
- Around line 50-66: The handler handleSubmitWork currently validates using
workCid.trim() but passes the original workCid to submitWork; compute a trimmed
value (e.g., const trimmed = workCid.trim()) after preventing default, use that
for the early-return check, pass trimmed to submitWork in the payload (bountyId,
contributorAddress, workCid: trimmed), and then clear the input state via
setWorkCid("") (or setWorkCid(trimmed) before clearing if needed) so no
leading/trailing whitespace is sent to the backend; update references to workCid
in this function accordingly.
In `@components/bounty-detail/mobile-cta.tsx`:
- Around line 148-176: The primary CTA's onClick opens bounty.githubIssueUrl
without ensuring it exists; update the Button in mobile-cta.tsx (the Button that
uses label(), controlled by canAct) to guard against undefined URLs the same way
SidebarCTA does: disable the button or no-op the onClick when
bounty.githubIssueUrl is falsy, and only call window.open(bounty.githubIssueUrl,
"_blank", "noopener,noreferrer") when bounty.githubIssueUrl is a non-empty
string; ensure the canAct/disabled logic and label() behavior reflect this guard
so the button cannot attempt to open an invalid URL.
In `@components/bounty-detail/sidebar-cta.tsx`:
- Around line 174-190: The default Button click handler should guard against an
undefined or empty bounty.githubIssueUrl: update the onClick for the Button (the
branch rendering Button with className "w-full h-11..." and using label()) to
either disable the button when bounty.githubIssueUrl is falsy or check the URL
before calling window.open; specifically use the existing canAct and
bounty.githubIssueUrl together (e.g., require canAct && bounty.githubIssueUrl)
and avoid calling window.open with an invalid URL, and if desired provide a
fallback action or keep the button disabled when bounty.githubIssueUrl is
missing.
In `@components/bounty/bounty-create-wizard.tsx`:
- Around line 153-166: The form handler onSubmit in bounty-create-wizard.tsx
currently drops milestones, deadline and startDate when calling createBounty
(CreateBountyInput), so MilestoneBased and Competition bounties appear to accept
config that will never be saved; either prevent selection of those types or show
a blocking validation/error before submit. Update the UI in the wizard to detect
when data.type === "MilestoneBased" or "Competition" (or the enum/constant names
used) and: 1) disable/hide those type options in the type selector OR 2) surface
a hard, non-dismissible warning modal/validation error on submit that clearly
states milestones/deadline/startDate are not supported by CreateBountyInput and
abort the createBounty call. Ensure the logic lives alongside the onSubmit
handler and the type selector so users cannot proceed to call createBounty with
unsupported fields, and reference createBounty and CreateBountyInput in your
changes.
In `@components/bounty/submission-approval-panel.tsx`:
- Around line 78-82: The call to requestRevisions is passing bounty.id as
submissionId; update the payload so submissionId uses the actual submission's id
(e.g., obtain it from bounty.submissions?.[0]?.id or the specific submission
being reviewed) instead of bounty.id, and add a guard that handles a missing
submission id (early return or show error) before calling requestRevisions;
ensure this aligns with ReviewSubmissionInput.submissionId and the
reviewSubmission flow.
In `@components/settings/notifications-tab.tsx`:
- Around line 84-86: The mentions filter is dead because NotificationType has no
"mentions" member and the cast hides the bug; either add a real mentions variant
to the NotificationType (and ensure producers push notifications with type
"mentions") so the const mentions = notifications.filter((n) => n.type ===
"mentions").slice(0,3) becomes meaningful, or remove the mentions-related code
path entirely: delete the mentions constant and the Mentions UI block that
checks mentions.length > 0 and update the fallback logic that references
mentions.length === 0; locate the mentions variable and the Mentions rendering
block in notifications-tab.tsx (search for mentions, NotificationType, and the
Mentions section) and apply the chosen fix consistently.
In `@components/settings/profile-tab.tsx`:
- Around line 192-211: The role toggle inside FormFieldWrapper (name="role") is
missing an accessible name; update the render for the role field so the checkbox
input has a programmatic label — e.g. provide a non-empty label prop on
FormFieldWrapper or add a visible or visually-hidden element (e.g. span) with an
id and wire it to the input via aria-labelledby, or add an explicit
aria-label/aria-labelledby on the input itself; ensure the input remains linked
to the label/rendered text (use the same unique id or label text) so screen
readers can identify "Contributor/Sponsor" for the toggle (refer to the render
function, field, and the checkbox input).
In `@hooks/use-bounty-search.ts`:
- Around line 140-175: The cached merge is reading the wrong shape: update the
CachedQueryData type and extraction in the block that builds cachedBountyResults
so it reads queryData.data and queryData.pages[].data (not bounties.bounties);
modify CachedQueryData to have data?: BountyFieldsFragment[] and pages?: Array<{
data?: BountyFieldsFragment[] }>, and change the items assignment to use
queryData.pages.flatMap(p => p.data || []) or queryData.data as appropriate
before de-duplicating into allCachedBounties (affecting cachedBountyResults,
CachedQueryData, and the loop that processes cachedQueries from
queryClient.getQueriesData).
In `@lib/server-auth.ts`:
- Around line 94-104: The E2E test-token bypass in getCurrentUser (the
sessionCookie checks for "fake-sponsor-token" / "fake-e2e-token") must be
disabled in production: restrict this branch behind a non-production/E2E-only
guard (e.g., require NODE_ENV !== "production" and an explicit ENABLE_E2E_TESTS
or IS_E2E flag) so the synthetic user return only executes when the environment
flag is present and not in production; update the conditional around
sessionCookie and ensure any configuration flag is clearly named
(ENABLE_E2E_TESTS or IS_E2E) and false by default.
In `@lib/server-graphql.ts`:
- Around line 22-23: Remove the dead variable unused_query and ensure the
request body uses a normalized query string; replace the current direct use of
query with a single normalized value (e.g., compute const normalizedQuery =
typeof query === "string" ? query : query.toString() and use normalizedQuery
when serializing the request body) so there are no unused variables and
TypedDocumentString instances are converted to plain strings before JSON
serialization (check lib/graphql/typed-document-string.ts if special toJSON
behavior is required).
---
Outside diff comments:
In `@lib/graphql/generated.ts`:
- Around line 1958-1997: The BountyFieldsFragment in lib/graphql/generated.ts is
missing maxParticipants and claimCount whereas types/bounty.ts and UI components
(bounty-card.tsx, sidebar-cta.tsx, competition-status.tsx) expect those fields;
either add maxParticipants and claimCount to the GraphQL fragment(s) that
produce BountyFieldsFragment (and any related queries/mutations) and re-run the
codegen to regenerate lib/graphql/generated.ts, or remove/align the optional
claimCount/maxParticipants properties from types/bounty.ts and update the
components to derive counts from _count?.submissions consistently; reference the
fragment name BountyFieldsFragment and the types file types/bounty.ts when
making the change and ensure generated types are regenerated.
---
Nitpick comments:
In `@app/bounty/create/page.tsx`:
- Around line 15-17: The page-level redirect only protects the UI; enforce the
sponsor role on the backend by adding an authorization check in the createBounty
GraphQL mutation resolver (or its middleware) to verify the authenticated user's
role is "sponsor" before creating a bounty. Locate the createBounty
resolver/handler and: (1) fetch the current user from the request/context, (2)
if user is missing or user.role !== "sponsor" throw an authorization error
(e.g., Forbidden/401), and (3) only proceed to validate input and persist the
bounty when the role check passes; ensure tests or error messages clearly
indicate the missing sponsor permission.
In `@components/bounty-detail/model4-maintainer-dashboard.tsx`:
- Around line 60-75: The Advance branch does not handle the case where
milestones.findIndex(...) returns -1, causing it to treat index -1 as valid;
update the action === "Advance" flow to check currentIndex for -1 after
computing currentIndex (from milestones.findIndex using currentMilestoneId) and
handle it explicitly (e.g., throw or toast an error and return) instead of
proceeding to pick nextMilestone; ensure you only call
advanceMutation.mutateAsync with nextMilestone when currentIndex >= 0 and
currentIndex < milestones.length - 1, and keep the existing success toasts
(toast.success) unchanged for the normal paths.
In `@components/bounty/bounty-create-wizard.tsx`:
- Around line 252-291: Remove the redundant outer FormField wrapper for the
"type" field: delete the outer FormField that has name="type" and its render
wrapper, and keep the inner FormField (the one that actually provides render={({
field }) => (...)}) which contains FormItem, FormLabel, Select,
SelectTrigger/SelectValue, SelectContent with SelectItem values using
BountyType, and FormMessage; ensure the retained inner FormField continues to
use control={form.control}, name="type", field.onChange for Select.onValueChange
and field.value for Select.value so form wiring is unchanged.
- Line 47: Replace the deprecated z.nativeEnum usage for the BountyType field:
update the schema line using BountyType in
components/bounty/bounty-create-wizard.tsx from "type: z.nativeEnum(BountyType)"
to use z.enum with the BountyType values (e.g. type: z.enum(BountyType) or
z.enum(Object.values(BountyType) as [string, ...string[]]) if needed), and
ensure any import of z is unchanged; reference symbol: BountyType from
lib/graphql/generated.ts and the schema field in bounty-create-wizard.tsx.
In `@components/cards/project-card.tsx`:
- Around line 94-97: The creator line currently uses the Clock icon (Clock)
which implies time rather than authorship; replace Clock with a more appropriate
User-type icon (e.g., User or UserIcon) in the JSX for the ProjectCard component
(the div showing "Created by {project.creatorName}"), and update the import
statements at the top of the file to import the chosen User icon instead of
Clock (or keep Clock if used elsewhere and add the User import). Ensure the
className/h sizing (e.g., "h-3 w-3") is applied to the new icon so visual
alignment remains consistent.
In `@components/leaderboard/leaderboard-table.tsx`:
- Around line 41-49: The IntersectionObserver callback in useEffect is shadowing
the component prop entries: LeaderboardEntry[]; rename the callback parameter
(e.g., to observerEntries or ioEntries) so it no longer hides the prop, update
the check from entries[0].isIntersecting to the new name, and keep the existing
guards (hasNextPage, isFetchingNextPage) and call to onLoadMore() unchanged;
this affects the IntersectionObserver instantiation inside useEffect in
leaderboard-table.tsx.
In `@components/projects/project-bounties.tsx`:
- Line 98: The onClick handler is using an unnecessary and unsafe cast "as
BountyType" when calling setSelectedType; remove the cast so it becomes
setSelectedType(type.value) and ensure the type of type.value already matches
the state union (BountyType | "all") so TypeScript accepts it; update any local
typings for the variable named type (or its source array) if needed so
type.value is correctly inferred as BountyType | "all" for the setSelectedType
call (refer to setSelectedType and the type.value usage in the click handler).
In `@hooks/use-user-mutations.ts`:
- Line 13: The optional property role is declared as role?: string which is too
permissive; replace it with the exact union or enum used by your app (e.g. the
UserRole union/enum from your schema or UI types) so only valid roles flow into
updateUser; locate the role declaration in hooks/use-user-mutations.ts and
change its type to the canonical type (or import and use the existing
UserRole/Role type) so updateUser receives only allowed values.
In `@lib/mock/bounties.ts`:
- Around line 279-282: makeMockBounty currently shallow-copies mockBounties[0]
so nested objects/arrays remain shared; change makeMockBounty to return a
deep-cloned base (e.g., use structuredClone(mockBounties[0]) or a deep-clone
utility like lodash/cloneDeep) and then apply overrides so the returned Bounty
has no shared nested references (update the makeMockBounty function and keep
using overrides?: Partial<Bounty> spread afterward).
🪄 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: 6f53e958-c0e7-405f-bb2e-ce5adcf2f7b6
📒 Files selected for processing (64)
app/api/leaderboard/route.tsapp/api/leaderboard/top/route.tsapp/api/leaderboard/user/[userId]/route.tsapp/bounty/create/page.tsxapp/bounty/page.tsxapp/discover/page.tsxapp/projects/[id]/page.tsxapp/projects/page.tsxapp/saved/saved-client.tsxapp/wallet/page.tsxcomponents/bounty-detail/bounty-detail-bounty-detail-skeleton.tsxcomponents/bounty-detail/bounty-detail-client.tsxcomponents/bounty-detail/bounty-detail-sidebar-cta.tsxcomponents/bounty-detail/dispute-dialog.tsxcomponents/bounty-detail/milestone-submission-card.tsxcomponents/bounty-detail/mobile-cta.tsxcomponents/bounty-detail/model4-maintainer-dashboard.tsxcomponents/bounty-detail/sidebar-cta.tsxcomponents/bounty-detail/types.tscomponents/bounty-detail/use-bounty-cta-state.tscomponents/bounty/application-review-dashboard.tsxcomponents/bounty/application-submit-work-panel.tsxcomponents/bounty/bounty-card-skeleton.tsxcomponents/bounty/bounty-create-wizard.tsxcomponents/bounty/bounty-grid.tsxcomponents/bounty/bounty-list.tsxcomponents/bounty/submission-approval-panel.tsxcomponents/cards/project-card.tsxcomponents/global-navbar.tsxcomponents/leaderboard/leaderboard-table.tsxcomponents/mode-toggle.tsxcomponents/notifications/notification-bell.tsxcomponents/notifications/notification-center.tsxcomponents/notifications/notification-item.tsxcomponents/notifications/notification-list.tsxcomponents/projects/project-bounties.tsxcomponents/search-command.tsxcomponents/settings/notifications-tab.tsxcomponents/settings/profile-tab.tsxcomponents/ui/skeleton-loaders.tsxe2e/bounty-creation.spec.tshooks/use-bounty-application.tshooks/use-bounty-search.tshooks/use-competition-join-state.tshooks/use-create-bounty.tshooks/use-notifications.tshooks/use-user-mutations.tslib/auth-client.tslib/graphql/generated.tslib/graphql/schema.graphqllib/mock-data.tslib/mock-wallet.tslib/mock/bounties.tslib/mock/index.tslib/mock/leaderboard.tslib/mock/model4.tslib/mock/projects.tslib/mock/wallet.tslib/server-auth.tslib/server-graphql.tslib/services/withdrawal.tslib/store.tsscripts/refactor_mocks.pytypes/bounty.ts
💤 Files with no reviewable changes (5)
- components/bounty-detail/bounty-detail-bounty-detail-skeleton.tsx
- lib/mock-wallet.ts
- components/bounty/bounty-card-skeleton.tsx
- lib/mock-data.ts
- components/bounty-detail/bounty-detail-sidebar-cta.tsx
| const page = parseInt(searchParams.get("page") || "1"); | ||
| const limit = parseInt(searchParams.get("limit") || "10"); | ||
| const tier = searchParams.get("tier") as ReputationTier | null; |
There was a problem hiding this comment.
Validate and clamp pagination query params before use.
page/limit currently accept invalid or extreme values, which can lead to bad ranking math and inconsistent pagination behavior.
Proposed fix
- const page = parseInt(searchParams.get("page") || "1");
- const limit = parseInt(searchParams.get("limit") || "10");
+ const rawPage = Number.parseInt(searchParams.get("page") ?? "1", 10);
+ const rawLimit = Number.parseInt(searchParams.get("limit") ?? "10", 10);
+ const page = Number.isFinite(rawPage) && rawPage > 0 ? rawPage : 1;
+ const limit =
+ Number.isFinite(rawLimit) && rawLimit > 0
+ ? Math.min(rawLimit, 100)
+ : 10;📝 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.
| const page = parseInt(searchParams.get("page") || "1"); | |
| const limit = parseInt(searchParams.get("limit") || "10"); | |
| const tier = searchParams.get("tier") as ReputationTier | null; | |
| const rawPage = Number.parseInt(searchParams.get("page") ?? "1", 10); | |
| const rawLimit = Number.parseInt(searchParams.get("limit") ?? "10", 10); | |
| const page = Number.isFinite(rawPage) && rawPage > 0 ? rawPage : 1; | |
| const limit = | |
| Number.isFinite(rawLimit) && rawLimit > 0 | |
| ? Math.min(rawLimit, 100) | |
| : 10; | |
| const tier = searchParams.get("tier") as ReputationTier | null; |
🤖 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/api/leaderboard/route.ts` around lines 7 - 9, The page and limit query
params (variables page and limit derived from searchParams in route.ts) are
parsed without validation and can be NaN, negative, zero, or unreasonably large;
update the parsing logic to validate and clamp them: parse to integer, fall back
to safe defaults when isNaN, enforce page >= 1, enforce limit between 1 and a
reasonable max (e.g., 100) and ensure both are integers (Math.floor behavior),
then use these sanitized values in the ranking/pagination logic; keep tier
parsing as-is but validate it against the ReputationTier enum if necessary.
| <CardContent className="pt-6"> | ||
| <CardContent className="pt-6"> | ||
| <BountyCreateWizard /> | ||
| </CardContent> | ||
| </CardContent> |
There was a problem hiding this comment.
Remove the duplicate nested CardContent.
CardContent is wrapped inside another CardContent, applying pt-6 padding twice. Likely a copy/paste artifact.
🧹 Proposed fix
<CardContent className="pt-6">
- <CardContent className="pt-6">
- <BountyCreateWizard />
- </CardContent>
+ <BountyCreateWizard />
</CardContent>📝 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.
| <CardContent className="pt-6"> | |
| <CardContent className="pt-6"> | |
| <BountyCreateWizard /> | |
| </CardContent> | |
| </CardContent> | |
| <CardContent className="pt-6"> | |
| <BountyCreateWizard /> | |
| </CardContent> |
🤖 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 28 - 32, The JSX contains a
duplicate nested CardContent that applies pt-6 twice; remove the inner (or
outer) redundant CardContent so BountyCreateWizard is contained by a single
CardContent with the intended padding. Locate the CardContent wrappers around
<BountyCreateWizard /> and delete the extra CardContent element to avoid double
padding and preserve the remaining CardContent className="pt-6".
| bounty?.assignedContributorId === session?.user?.id || | ||
| bounty?.submissions?.some((s) => s.submittedBy === session?.user?.id) || | ||
| (!isCreator && bounty.status === "IN_PROGRESS"); |
There was a problem hiding this comment.
Overly broad assignment check may incorrectly identify users.
The third condition (!isCreator && bounty.status === "IN_PROGRESS") assumes that any non-creator viewing an IN_PROGRESS bounty is the assigned applicant. This logic is too broad—multiple users could view the same bounty, and they would all be incorrectly identified as assigned.
Consider relying solely on the explicit assignedContributorId check or verifying against submission records rather than this catch-all fallback.
🤖 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-detail/bounty-detail-client.tsx` around lines 153 - 155,
The current assigned-user check includes a too-broad fallback `(!isCreator &&
bounty.status === "IN_PROGRESS")` which misidentifies any non-creator as
assigned; update the logic in the component where you compute assignment
(referencing bounty?.assignedContributorId, session?.user?.id,
bounty?.submissions, isCreator, bounty.status) to remove that fallback and
instead rely only on the explicit assignedContributorId match or a definitive
submission-based check (e.g., verify bounty?.submissions.some(s => s.submittedBy
=== session?.user?.id && s.status === 'ACCEPTED') if your domain uses an
accepted submission flag) so only the actual assigned/accepted user is
considered assigned.
| (bounty.status === "IN_PROGRESS" || | ||
| (bounty.status as string) === "REVISION_REQUESTED") && ( |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify BountyStatus enum values and search for REVISION_REQUESTED usage
echo "=== BountyStatus enum from schema ==="
rg -A 10 'enum BountyStatus' lib/graphql/schema.graphql
echo -e "\n=== All REVISION_REQUESTED references ==="
rg 'REVISION_REQUESTED' --type=ts --type=tsxRepository: boundlessfi/bounties
Length of output: 305
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== BountyStatus enums in repo ==="
rg -n --no-heading 'enum\s+BountyStatus\b' .
echo -e "\n=== All REVISION_REQUESTED references (all files) ==="
rg -n --no-heading 'REVISION_REQUESTED' .
echo -e "\n=== REVISION_REQUESTED references near bounty.status checks ==="
rg -n --no-heading 'bounty\.status.*REVISION_REQUESTED|REVISION_REQUESTED.*bounty\.status' .Repository: boundlessfi/bounties
Length of output: 738
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== components/bounty-detail/bounty-detail-client.tsx (around 241-242) ==="
nl -ba components/bounty-detail/bounty-detail-client.tsx | sed -n '220,270p'
echo -e "\n=== hooks/use-bounty-application.ts (around 540-580) ==="
nl -ba hooks/use-bounty-application.ts | sed -n '510,610p'
echo -e "\n=== lib/graphql/generated.ts: BountyStatus enum (around 720-760) ==="
nl -ba lib/graphql/generated.ts | sed -n '700,770p'Repository: boundlessfi/bounties
Length of output: 200
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== components/bounty-detail/bounty-detail-client.tsx (220-270) ==="
awk 'NR>=220 && NR<=270 {printf "%d:%s\n", NR, $0}' components/bounty-detail/bounty-detail-client.tsx
echo -e "\n=== hooks/use-bounty-application.ts (510-610) ==="
awk 'NR>=510 && NR<=610 {printf "%d:%s\n", NR, $0}' hooks/use-bounty-application.ts
echo -e "\n=== lib/graphql/generated.ts (700-770) ==="
awk 'NR>=700 && NR<=770 {printf "%d:%s\n", NR, $0}' lib/graphql/generated.tsRepository: boundlessfi/bounties
Length of output: 8290
Fix GraphQL BountyStatus mismatch: REVISION_REQUESTED not in schema enum
components/bounty-detail/bounty-detail-client.tsx checks (bounty.status as string) === "REVISION_REQUESTED" (around lines 241-242), but lib/graphql/schema.graphql / lib/graphql/generated.ts defines enum BountyStatus without REVISION_REQUESTED (only CANCELLED, COMPLETED, DISPUTED, DRAFT, IN_PROGRESS, OPEN, SUBMITTED, UNDER_REVIEW). That means this UI path can only work if the backend returns an undocumented status.
(bounty.status === "IN_PROGRESS" ||
(bounty.status as string) === "REVISION_REQUESTED") && (Also, hooks/use-bounty-application.ts (around lines 547 and 562) sends/optimistically sets status: "REVISION_REQUESTED" in the ReviewSubmissionDocument input, so this status is likely rejected by the backend / inconsistent with the schema.
🤖 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-detail/bounty-detail-client.tsx` around lines 241 - 242,
The UI and hook use an undocumented status "REVISION_REQUESTED" causing
schema/type mismatch; either remove/replace that value with a valid enum member
(e.g., use "UNDER_REVIEW" or "IN_PROGRESS" depending on intended behavior) or
add "REVISION_REQUESTED" to lib/graphql/schema.graphql and re-generate types in
lib/graphql/generated.ts and backend handling. Concretely, update the
conditional in components/bounty-detail/bounty-detail-client.tsx to check only
valid BountyStatus values (replace (bounty.status as string) ===
"REVISION_REQUESTED" with the chosen valid enum), and update
hooks/use-bounty-application.ts where ReviewSubmissionDocument input and
optimistic updates set status: "REVISION_REQUESTED" to use the same valid enum;
if you choose to add the new status, modify schema.graphql, regenerate the
generated.ts types, and ensure the backend accepts the new enum.
| function handleSubmitWork(e: React.FormEvent) { | ||
| e.preventDefault(); | ||
| if (!workCid.trim()) return; | ||
|
|
||
| submitWork( | ||
| { | ||
| bountyId, | ||
| contributorAddress, | ||
| workCid, | ||
| }, | ||
| { | ||
| onSuccess: () => { | ||
| setWorkCid(""); | ||
| }, | ||
| }, | ||
| ); | ||
| } |
There was a problem hiding this comment.
Trim workCid before submission for consistency.
Line 52 validates with workCid.trim(), but line 58 submits the untrimmed value. This could send leading/trailing whitespace to the backend.
Proposed fix
function handleSubmitWork(e: React.FormEvent) {
e.preventDefault();
- if (!workCid.trim()) return;
+ const trimmedCid = workCid.trim();
+ if (!trimmedCid) return;
submitWork(
{
bountyId,
contributorAddress,
- workCid,
+ workCid: trimmedCid,
},
{
onSuccess: () => {
setWorkCid("");
},
},
);
}🤖 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-detail/milestone-submission-card.tsx` around lines 50 - 66,
The handler handleSubmitWork currently validates using workCid.trim() but passes
the original workCid to submitWork; compute a trimmed value (e.g., const trimmed
= workCid.trim()) after preventing default, use that for the early-return check,
pass trimmed to submitWork in the payload (bountyId, contributorAddress,
workCid: trimmed), and then clear the input state via setWorkCid("") (or
setWorkCid(trimmed) before clearing if needed) so no leading/trailing whitespace
is sent to the backend; update references to workCid in this function
accordingly.
| <FormFieldWrapper | ||
| control={form.control} | ||
| name="role" | ||
| label="" | ||
| render={({ field }) => ( | ||
| <div className="flex items-center space-x-2"> | ||
| <span className="text-xs font-medium text-muted-foreground"> | ||
| Contributor | ||
| </span> | ||
| <label className="relative inline-flex items-center cursor-pointer"> | ||
| <input | ||
| type="checkbox" | ||
| className="sr-only peer" | ||
| checked={field.value === "sponsor"} | ||
| onChange={(e) => | ||
| field.onChange( | ||
| e.target.checked ? "sponsor" : "contributor", | ||
| ) | ||
| } | ||
| /> |
There was a problem hiding this comment.
Add an explicit accessible name to the role toggle.
The checkbox toggle lacks a reliable programmatic label (empty label prop + unlabeled input), which makes this control unclear for screen readers.
Suggested fix
<FormFieldWrapper
control={form.control}
name="role"
- label=""
+ label="Account Role"
render={({ field }) => (
<div className="flex items-center space-x-2">
@@
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
+ aria-label="Toggle account role between contributor and sponsor"
className="sr-only peer"
checked={field.value === "sponsor"}
onChange={(e) =>📝 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.
| <FormFieldWrapper | |
| control={form.control} | |
| name="role" | |
| label="" | |
| render={({ field }) => ( | |
| <div className="flex items-center space-x-2"> | |
| <span className="text-xs font-medium text-muted-foreground"> | |
| Contributor | |
| </span> | |
| <label className="relative inline-flex items-center cursor-pointer"> | |
| <input | |
| type="checkbox" | |
| className="sr-only peer" | |
| checked={field.value === "sponsor"} | |
| onChange={(e) => | |
| field.onChange( | |
| e.target.checked ? "sponsor" : "contributor", | |
| ) | |
| } | |
| /> | |
| <FormFieldWrapper | |
| control={form.control} | |
| name="role" | |
| label="Account Role" | |
| render={({ field }) => ( | |
| <div className="flex items-center space-x-2"> | |
| <span className="text-xs font-medium text-muted-foreground"> | |
| Contributor | |
| </span> | |
| <label className="relative inline-flex items-center cursor-pointer"> | |
| <input | |
| type="checkbox" | |
| aria-label="Toggle account role between contributor and sponsor" | |
| className="sr-only peer" | |
| checked={field.value === "sponsor"} | |
| onChange={(e) => | |
| field.onChange( | |
| e.target.checked ? "sponsor" : "contributor", | |
| ) | |
| } | |
| /> |
🤖 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/settings/profile-tab.tsx` around lines 192 - 211, The role toggle
inside FormFieldWrapper (name="role") is missing an accessible name; update the
render for the role field so the checkbox input has a programmatic label — e.g.
provide a non-empty label prop on FormFieldWrapper or add a visible or
visually-hidden element (e.g. span) with an id and wire it to the input via
aria-labelledby, or add an explicit aria-label/aria-labelledby on the input
itself; ensure the input remains linked to the label/rendered text (use the same
unique id or label text) so screen readers can identify "Contributor/Sponsor"
for the toggle (refer to the render function, field, and the checkbox input).
| let cachedBountyResults: BountyFieldsFragment[] = []; | ||
| if (searchLower) { | ||
| interface CachedQueryData { | ||
| bounties?: { bounties?: BountyFieldsFragment[] }; | ||
| pages?: Array<{ bounties?: { bounties?: BountyFieldsFragment[] } }>; | ||
| } | ||
| const cachedQueries = queryClient.getQueriesData<CachedQueryData>({ | ||
| queryKey: ["Bounties"], | ||
| }); | ||
| const allCachedBounties = new Map<string, BountyFieldsFragment>(); | ||
|
|
||
| cachedQueries.forEach(([_, queryData]) => { | ||
| // Data might be paginated (pages) or just single list (bounties.bounties) | ||
| // or from the specific search queries | ||
| let items: BountyFieldsFragment[] = []; | ||
| if (queryData?.pages) { | ||
| items = queryData.pages.flatMap((p) => p.bounties?.bounties || []); | ||
| } else if (queryData?.bounties?.bounties) { | ||
| items = queryData.bounties.bounties; | ||
| } | ||
|
|
||
| if (Array.isArray(items)) { | ||
| items.forEach((b) => { | ||
| if (b && b.id) { | ||
| allCachedBounties.set(b.id, b); | ||
| } | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| cachedBountyResults = Array.from(allCachedBounties.values()).filter( | ||
| (b) => | ||
| b.title.toLowerCase().includes(searchLower) || | ||
| (b.description && b.description.toLowerCase().includes(searchLower)), | ||
| ); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 1) Inspect bountyKeys to confirm the literal prefix of bounty query keys
fd -t f 'query-keys' -e ts | xargs rg -nP -C2 'bountyKeys'
# 2) Find any query keys literally starting with "Bounties"
rg -nP --type=ts "queryKey:\s*\[\s*[\"']Bounties[\"']"
# 3) Find queries that store data in the assumed shapes used by the parser
rg -nP --type=ts "bounties\s*\.\s*bounties|pages\?\.\s*flatMap|\.pages\b"Repository: boundlessfi/bounties
Length of output: 1743
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the queryFn return shapes for the actual "Bounties" list/infinite queries
fd -t f 'bounty-queries.ts' | xargs -I{} sh -c 'echo "===== {} ====="; rg -n "bountyKeys\.list|bountyKeys\.infinite|getKey\(\)|queryFn" {} && rg -n "return \{|pages|data:|bounties" {} | head -n 80'
# Inspect hooks that likely read the cached data shapes
for f in hooks/use-bounties.ts hooks/use-infinite-bounties.ts hooks/use-bounty-search.ts; do
if [ -f "$f" ]; then
echo "===== $f ====="
# Print around the lines referenced in the earlier grep output
case "$f" in
hooks/use-bounty-search.ts) start=80; end=240;;
hooks/use-bounties.ts) start=1; end=120;;
hooks/use-infinite-bounties.ts) start=1; end=120;;
*) start=1; end=200;;
esac
sed -n "${start},${end}p" "$f" | nl -ba | sed -n '1,220p'
fi
done
# Find where infinite/page results are shaped (pages[].data vs pages[].bounties)
fd -t f 'pagination.ts' lib | xargs -I{} sh -c 'echo "===== {} ====="; rg -n "response\.bounties\.bounties|pages|flatMap" {} && sed -n "1,120p" {} | nl -ba'
# Find any getQueriesData usage for "Bounties"
rg -n --type=ts "getQueriesData<|getQueriesData\(\{[^}]*queryKey:\s*\[\"Bounties\"" hooks lib appRepository: boundlessfi/bounties
Length of output: 794
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Locate the actual file(s) defining bountyKeys and show the key format (especially the array prefix)
fd -t f 'bountyKeys' -e ts | head -n 20
fd -t f 'query-keys' -e ts | head -n 20
# Print the file that contains "export const bountyKeys"
file="$(rg -l --type=ts "export const bountyKeys" . | head -n 1 || true)"
echo "bountyKeys file: $file"
if [ -n "${file:-}" ]; then
sed -n '1,120p' "$file" | cat -n
echo "---- getKey examples (if present) ----"
rg -n --type=ts "getKey\(|\\[\"Bounties\"|\"Bounties\\\"" "$file" | head -n 80
fi
# 2) Inspect list/infinite queryFn return shapes in lib/query/bounty-queries.ts
qb="lib/query/bounty-queries.ts"
if [ -f "$qb" ]; then
sed -n '1,160p' "$qb" | cat -n
sed -n '160,260p' "$qb" | cat -n
fi
# 3) Inspect how other hooks read the queryData (so we know the cached shape they store)
for f in hooks/use-bounties.ts hooks/use-infinite-bounties.ts hooks/use-bounty-search.ts; do
if [ -f "$f" ]; then
echo "===== $f ====="
sed -n '1,260p' "$f" | cat -n | head -n 260
fi
done
# 4) Show any getQueriesData usage for ["Bounties", ...] elsewhere (to see established conventions)
rg -n --type=ts "getQueriesData\(\s*<|getQueriesData\(\s*\{[^}]*queryKey:\s*\[\"Bounties\"" -S hooks lib app || trueRepository: boundlessfi/bounties
Length of output: 1962
Fix cached bounty merge: current parser reads the wrong cached data shape.
- The query key prefix is fine:
bountyKeysuses keys starting with"Bounties"(e.g.["Bounties", "lists"]), sogetQueriesData({ queryKey: ["Bounties"] })will match. - The cached data shape is the real problem: the actual list/infinite query
queryFnreturns{ data: ... }(not{ bounties: { bounties } }), and infinite pagination pages flatten viapage.data. So the parsing here (queryData.bounties.bounties/pages[].bounties.bounties) will not pick up the cached results, makingcachedBountyResultseffectively empty. - Update
CachedQueryData+ the extraction logic to readqueryData.data/queryData.pages[].datainstead.
🤖 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/use-bounty-search.ts` around lines 140 - 175, The cached merge is
reading the wrong shape: update the CachedQueryData type and extraction in the
block that builds cachedBountyResults so it reads queryData.data and
queryData.pages[].data (not bounties.bounties); modify CachedQueryData to have
data?: BountyFieldsFragment[] and pages?: Array<{ data?: BountyFieldsFragment[]
}>, and change the items assignment to use queryData.pages.flatMap(p => p.data
|| []) or queryData.data as appropriate before de-duplicating into
allCachedBounties (affecting cachedBountyResults, CachedQueryData, and the loop
that processes cachedQueries from queryClient.getQueriesData).
| // E2E Test Bypass for Server Components | ||
| if ( | ||
| sessionCookie === "fake-sponsor-token" || | ||
| sessionCookie === "fake-e2e-token" | ||
| ) { | ||
| return { | ||
| id: "e2e-tester", | ||
| name: "E2E Tester", | ||
| role: sessionCookie === "fake-sponsor-token" ? "sponsor" : "contributor", | ||
| }; | ||
| } |
There was a problem hiding this comment.
Critical: hardcoded test-token auth bypass is active in production.
getCurrentUser grants a synthetic sponsor/contributor identity whenever the session cookie equals "fake-sponsor-token" or "fake-e2e-token", with no environment guard. In production, anyone can set boundless_auth.session_token=fake-sponsor-token and obtain sponsor-level access — a full authentication/authorization bypass.
Gate this strictly behind a non-production E2E flag.
🔒 Proposed fix
- // E2E Test Bypass for Server Components
- if (
- sessionCookie === "fake-sponsor-token" ||
- sessionCookie === "fake-e2e-token"
- ) {
+ // E2E Test Bypass for Server Components — never enable in production
+ if (
+ process.env.NODE_ENV !== "production" &&
+ process.env.E2E_TEST === "true" &&
+ (sessionCookie === "fake-sponsor-token" ||
+ sessionCookie === "fake-e2e-token")
+ ) {
return {
id: "e2e-tester",
name: "E2E Tester",
role: sessionCookie === "fake-sponsor-token" ? "sponsor" : "contributor",
};
}📝 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.
| // E2E Test Bypass for Server Components | |
| if ( | |
| sessionCookie === "fake-sponsor-token" || | |
| sessionCookie === "fake-e2e-token" | |
| ) { | |
| return { | |
| id: "e2e-tester", | |
| name: "E2E Tester", | |
| role: sessionCookie === "fake-sponsor-token" ? "sponsor" : "contributor", | |
| }; | |
| } | |
| // E2E Test Bypass for Server Components — never enable in production | |
| if ( | |
| process.env.NODE_ENV !== "production" && | |
| process.env.E2E_TEST === "true" && | |
| (sessionCookie === "fake-sponsor-token" || | |
| sessionCookie === "fake-e2e-token") | |
| ) { | |
| return { | |
| id: "e2e-tester", | |
| name: "E2E Tester", | |
| role: sessionCookie === "fake-sponsor-token" ? "sponsor" : "contributor", | |
| }; | |
| } |
🤖 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 `@lib/server-auth.ts` around lines 94 - 104, The E2E test-token bypass in
getCurrentUser (the sessionCookie checks for "fake-sponsor-token" /
"fake-e2e-token") must be disabled in production: restrict this branch behind a
non-production/E2E-only guard (e.g., require NODE_ENV !== "production" and an
explicit ENABLE_E2E_TESTS or IS_E2E flag) so the synthetic user return only
executes when the environment flag is present and not in production; update the
conditional around sessionCookie and ensure any configuration flag is clearly
named (ENABLE_E2E_TESTS or IS_E2E) and false by default.
| // Normalize: if TypedDocumentString, convert to string | ||
| const queryString = typeof query === "string" ? query : query.toString(); | ||
| const unused_query = typeof query === "string" ? query : query.toString(); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check whether TypedDocumentString serializes to a string (extends String / toJSON)
ast-grep --pattern 'class TypedDocumentString$_ { $$$ }'
rg -nP --type=ts -C3 'class TypedDocumentString'Repository: boundlessfi/bounties
Length of output: 624
Remove dead unused_query variable (and keep request body using the correct query value).
unused_query is computed but not used; the request body still serializes query directly, so this is dead code and can trip no-unused-vars. Also, TypedDocumentString is a String subclass (extends String), so the string value is already available for JSON serialization unless the class defines custom toJSON/serialization (needs checking in lib/graphql/typed-document-string.ts).
🤖 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 `@lib/server-graphql.ts` around lines 22 - 23, Remove the dead variable
unused_query and ensure the request body uses a normalized query string; replace
the current direct use of query with a single normalized value (e.g., compute
const normalizedQuery = typeof query === "string" ? query : query.toString() and
use normalizedQuery when serializing the request body) so there are no unused
variables and TypedDocumentString instances are converted to plain strings
before JSON serialization (check lib/graphql/typed-document-string.ts if special
toJSON behavior is required).
| process_file("lib/mock-bounty.ts", "lib/mock/bounties.ts", "Bounty", "mockBounties", "makeMockBounty") | ||
| process_file("lib/mock-project.ts", "lib/mock/projects.ts", "Project", "mockProjects", "makeMockProject") | ||
| process_file("lib/mock-leaderboard.ts", "lib/mock/leaderboard.ts", "any", "mockLeaderboard", "makeMockLeaderboardEntry") # need to check types | ||
| process_file("lib/mock-wallet.ts", "lib/mock/wallet.ts", "any", "mockWalletWithAssets", "makeMockWallet") | ||
| process_file("lib/mock-model4.ts", "lib/mock/model4.ts", "any", "mockModel4", "makeMockModel4") |
There was a problem hiding this comment.
Factory generation targets mismatched symbols and will emit broken code.
Several process_file(...) calls reference identifiers that don’t exist in the generated modules, so rerunning this migration script can produce invalid output.
Proposed fix
-process_file("lib/mock-leaderboard.ts", "lib/mock/leaderboard.ts", "any", "mockLeaderboard", "makeMockLeaderboardEntry") # need to check types
-process_file("lib/mock-wallet.ts", "lib/mock/wallet.ts", "any", "mockWalletWithAssets", "makeMockWallet")
-process_file("lib/mock-model4.ts", "lib/mock/model4.ts", "any", "mockModel4", "makeMockModel4")
+process_file("lib/mock-leaderboard.ts", "lib/mock/leaderboard.ts", "LeaderboardContributor", "mockLeaderboardData", "makeMockLeaderboardEntry")
+# wallet/model4 need custom handlers (non-array bases / different structures)
+# process_file(...) should only be used for array-backed fixtures.📝 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.
| process_file("lib/mock-bounty.ts", "lib/mock/bounties.ts", "Bounty", "mockBounties", "makeMockBounty") | |
| process_file("lib/mock-project.ts", "lib/mock/projects.ts", "Project", "mockProjects", "makeMockProject") | |
| process_file("lib/mock-leaderboard.ts", "lib/mock/leaderboard.ts", "any", "mockLeaderboard", "makeMockLeaderboardEntry") # need to check types | |
| process_file("lib/mock-wallet.ts", "lib/mock/wallet.ts", "any", "mockWalletWithAssets", "makeMockWallet") | |
| process_file("lib/mock-model4.ts", "lib/mock/model4.ts", "any", "mockModel4", "makeMockModel4") | |
| process_file("lib/mock-bounty.ts", "lib/mock/bounties.ts", "Bounty", "mockBounties", "makeMockBounty") | |
| process_file("lib/mock-project.ts", "lib/mock/projects.ts", "Project", "mockProjects", "makeMockProject") | |
| process_file("lib/mock-leaderboard.ts", "lib/mock/leaderboard.ts", "LeaderboardContributor", "mockLeaderboardData", "makeMockLeaderboardEntry") | |
| # wallet/model4 need custom handlers (non-array bases / different structures) | |
| # process_file(...) should only be used for array-backed fixtures. |
This PR completes the Request Revisions flow by wiring up the UI to a real GraphQL mutation and optimistically transitioning the bounty status.
Summary by CodeRabbit
New Features
Tests