Skip to content

feat: implement maintainer dashboard actions for multi-winner milestone bounties#265

Open
Jopsan-gm wants to merge 7 commits into
boundlessfi:mainfrom
Jopsan-gm:issue-205-maintainer-dashboard-actions
Open

feat: implement maintainer dashboard actions for multi-winner milestone bounties#265
Jopsan-gm wants to merge 7 commits into
boundlessfi:mainfrom
Jopsan-gm:issue-205-maintainer-dashboard-actions

Conversation

@Jopsan-gm
Copy link
Copy Markdown
Contributor

@Jopsan-gm Jopsan-gm commented May 30, 2026

Title feat: implement maintainer dashboard actions for multi-winner milestone bounties

Closes

Closes #205

Description

This PR implements the requested actions for the Model 4 (Multi-Winner Milestone) Maintainer Dashboard, transitioning it from a static UI to a fully functional interface with optimistic updates.

Changes Made:

  • Replaced the generic handleAction stub with dedicated mutation hooks using @tanstack/react-query (releasePayment, advanceContributor, removeContributor, sendMessage).
  • Integrated sonner toasts to provide immediate success feedback after an action is executed.
  • Cleaned up the UI by removing the [Coming soon] suffix from action buttons.
  • Implemented real Dialog components for the View Submissions and Message actions.
  • Passed bountyId appropriately to ensure the mutations target the correct bounty scope.
  • Fixed strict TypeScript and ESLint type inference errors related to the extended/mocked contributorProgress fields.

Screenshots

image

Summary by CodeRabbit

  • New Features
    • Maintainer dashboard now provides full contributor management functionality, replacing placeholder actions with operational features for messaging contributors, viewing submissions, releasing milestone payments, advancing contributors, and removing them from projects
    • Added dialog interfaces for messaging contributors and reviewing their submissions
    • Operations display loading states and confirmation feedback

Review Change Stack

@vercel
Copy link
Copy Markdown

vercel Bot commented May 30, 2026

@Jopsan-gm is attempting to deploy a commit to the Threadflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 30, 2026

Warning

Review limit reached

@Jopsan-gm, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 13 minutes and 36 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 06e38ac1-9ef8-4f89-a31b-099861261832

📥 Commits

Reviewing files that changed from the base of the PR and between 5e53aa8 and 5ef6ef3.

📒 Files selected for processing (4)
  • components/bounty-detail/bounty-detail-client.tsx
  • components/bounty-detail/model4-maintainer-dashboard.tsx
  • hooks/use-bounty-application.ts
  • lib/services/escrow.ts
📝 Walkthrough

Walkthrough

This PR implements the maintainer dashboard actions for Model 4 Multi-Winner Milestone bounties. It introduces a new React Query hook with four mutations, updates the dashboard component to accept a bountyId prop and use those mutations, wires action buttons and dialogs to trigger state changes, and passes bountyId from the parent component.

Changes

Model 4 Maintainer Dashboard Actions Implementation

Layer / File(s) Summary
Bounty application mutations hook
hooks/use-bounty-application.ts
New hook exports four React Query mutations: releasePayment (invalidates bounty detail query), advanceContributor and removeContributor (optimistic cache updates with rollback), and sendMessage (simulated delay). All operations target the bounty cache keyed by bountyId.
Dashboard component wiring and state setup
components/bounty-detail/bounty-detail-client.tsx, components/bounty-detail/model4-maintainer-dashboard.tsx
BountyDetailClient passes bountyId to Model4MaintainerDashboard. Dashboard component accepts bountyId prop, initializes useBountyApplication hook, manages selectedContributor and dialog/message state, and defines handlers (releasePayment, advance, remove, openMessage, openSubmissions, sendMessage) that trigger mutations with sonner toast notifications and contributor-specific loading states.
Dashboard action buttons and dialogs
components/bounty-detail/model4-maintainer-dashboard.tsx
Five action buttons (Message, View Submissions, Release Payment, Advance, Remove) call their respective handlers and display contributor-specific Loader2 spinners while mutations are pending. Two controlled Dialog modals: submissions dialog (static empty-state content) and message dialog with controlled Textarea. Send button is disabled when message is empty/whitespace or mutation is pending.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • #205: Directly implements all acceptance criteria—splitting handleAction into named handlers backed by mutations, removing "Coming soon" suffixes, adding loading spinners and success toasts, and opening dialogs for submissions and messaging.

Possibly related PRs

  • boundlessfi/bounties#200: Both PRs add/alter React Query mutation hooks in hooks/use-bounty-application.ts for bounty lifecycle management, so they overlap at the mutations module level.

Suggested reviewers

  • Benjtalkshow

Poem

🐰 A dashboard unfolds with actions so bright,
No more "Coming soon"—mutations take flight!
Spinners spin, toasts appear, dialogs gleam,
Contributors advance through each milestone dream.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
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.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: implementing maintainer dashboard actions for multi-winner milestone bounties, which directly corresponds to the core functionality added across all modified files.
Linked Issues check ✅ Passed All five required actions (Release Payment, Advance, Remove, View Submissions, Message) are implemented with mutations, dialogs, loading states, and toast notifications as specified in issue #205.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the maintainer dashboard actions specified in issue #205; no unrelated modifications or scope creep detected.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@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: 5

🧹 Nitpick comments (2)
hooks/use-bounty-application.ts (2)

32-88: ⚡ Quick win

Missing onSettled invalidation for optimistic mutations.

advanceContributor and removeContributor use optimistic updates but never reconcile with the server after the mutation completes. If the server state differs from the optimistic update (e.g., another maintainer made changes), the cache will remain stale until the user manually refreshes.

Add onSettled to invalidate and refetch
   const advanceContributor = useMutation({
     // ... existing code ...
     onError: (_err, _vars, context) => {
       if (context?.previous) {
         queryClient.setQueryData(bountyKeys.detail(bountyId), context.previous);
       }
     },
+    onSettled: () => {
+      queryClient.invalidateQueries({ queryKey: bountyKeys.detail(bountyId) });
+    },
   });

   const removeContributor = useMutation({
     // ... existing code ...
     onError: (_err, _vars, context) => {
       if (context?.previous) {
         queryClient.setQueryData(bountyKeys.detail(bountyId), context.previous);
       }
     },
+    onSettled: () => {
+      queryClient.invalidateQueries({ queryKey: bountyKeys.detail(bountyId) });
+    },
   });
🤖 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-application.ts` around lines 32 - 88, The optimistic update
in advanceContributor (and similarly in removeContributor) never reconciles with
the server; add an onSettled handler to the useMutation config for
advanceContributor that calls
queryClient.invalidateQueries(bountyKeys.detail(bountyId)) (and optionally
queryClient.refetchQueries) to ensure the bounty detail cache is refetched after
success or error; locate the useMutation for advanceContributor (and the
removeContributor mutation) and append an onSettled: (_data, _error, _vars,
_context) => { queryClient.invalidateQueries(bountyKeys.detail(bountyId)); } to
properly refresh server state.

113-117: TODO comment indicates incomplete implementation for totalSlotsOccupied.

The comment acknowledges that totalSlotsOccupied should be decremented but isn't implemented. Per issue #205, removing a contributor should "decrement totalSlotsOccupied (visible removal and slot count update)."

Would you like me to help implement the totalSlotsOccupied decrement, or should this be tracked as a follow-up issue?

🤖 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-application.ts` around lines 113 - 117, The TODO notes that
totalSlotsOccupied isn't decremented when removing a contributor; update the
same update patch that builds the new state (the object containing
contributorProgress: contributorProgress.filter((c)=>c.userId!==contributorId))
to also adjust totalSlotsOccupied: read the current totalSlotsOccupied value
and, if it's a number, set totalSlotsOccupied to Math.max(0, totalSlotsOccupied
- 1) (preserving immutability), otherwise leave it undefined or omit it; ensure
you reference the same update object where contributorProgress is modified so
the removal and slot decrement happen atomically.
🤖 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-detail/model4-maintainer-dashboard.tsx`:
- Around line 60-71: Add onError handlers to the mutation calls so failures
surface to users; for each mutate invocation (e.g., releasePayment.mutate in
handleReleasePayment and the other mutate calls referenced around the same area
like approveContribution.mutate and rejectContribution.mutate), pass an onError
callback that calls toast.error with a clear message that includes the error
message or a fallback (e.g., `toast.error(\`Failed to release payment:
${error?.message ?? 'unknown error'}\`)`). Ensure the onError is paired with the
existing onSuccess handlers and uses the mutation-specific context
(contributor.userName or relevant IDs) to make the toast informative.
- Around line 104-115: handleSendMessage currently calls sendMessage.mutate
without an onError handler; update handleSendMessage to pass an onError callback
to sendMessage.mutate (alongside the existing onSuccess) that shows an error
toast (e.g., toast.error with a descriptive message and optional error.message)
and keeps the message UI open or resets state appropriately. Locate the
sendMessage.mutate call inside the handleSendMessage function and add the
onError option to mirror other mutation handlers in this file.
- Around line 268-276: The Advance button currently renders only Loader2 when
advanceContributor.isPending for the contributor, removing the "Advance" text
and ArrowRight icon; update the JSX in the Advance button (the block using
advanceContributor, contributor.userId, Loader2 and ArrowRight) to always render
the "Advance" label and ArrowRight icon and conditionally render Loader2
alongside them when advanceContributor.isPending (e.g., show Loader2
before/after the label and keep the label+icon visible), preserving the existing
conditional check on advanceContributor.variables?.contributorId to target the
correct contributor.

In `@hooks/use-bounty-application.ts`:
- Around line 83-88: The optimistic mutation handlers (advanceContributor and
removeContributor) currently only rollback via queryClient.setQueryData in their
onError callbacks, leaving users uninformed; update both mutation definitions to
add an onError that (1) rolls back as now using context.previous (keep
queryClient.setQueryData(bountyKeys.detail(bountyId), context.previous)), and
(2) triggers a user-facing error toast/message with the error details (use the
mutation onError params to extract the error) so the UI both reverts and shows a
clear failure message to the user; reference the existing onError blocks for
these mutations and reuse bountyId, bountyKeys.detail, and the context.previous
logic.
- Around line 60-78: The cache update path assumes there is a next milestone but
doesn't handle the case when milestoneIndex === milestones.length - 1, so the
mutation reports success with no state change; update the mutation to guard
against already-at-final-milestone by checking milestoneIndex against
milestones.length - 1 (using the same variables: milestoneIndex, milestones,
contributorIndex, contributorProgress) and either return early or throw an error
from the mutationFn so the success path (which calls queryClient.setQueryData on
bountyKeys.detail(bountyId)) and the success toast are not executed; ensure the
guard is placed before the code that constructs newProgress and calls
queryClient.setQueryData to prevent misleading UX.

---

Nitpick comments:
In `@hooks/use-bounty-application.ts`:
- Around line 32-88: The optimistic update in advanceContributor (and similarly
in removeContributor) never reconciles with the server; add an onSettled handler
to the useMutation config for advanceContributor that calls
queryClient.invalidateQueries(bountyKeys.detail(bountyId)) (and optionally
queryClient.refetchQueries) to ensure the bounty detail cache is refetched after
success or error; locate the useMutation for advanceContributor (and the
removeContributor mutation) and append an onSettled: (_data, _error, _vars,
_context) => { queryClient.invalidateQueries(bountyKeys.detail(bountyId)); } to
properly refresh server state.
- Around line 113-117: The TODO notes that totalSlotsOccupied isn't decremented
when removing a contributor; update the same update patch that builds the new
state (the object containing contributorProgress:
contributorProgress.filter((c)=>c.userId!==contributorId)) to also adjust
totalSlotsOccupied: read the current totalSlotsOccupied value and, if it's a
number, set totalSlotsOccupied to Math.max(0, totalSlotsOccupied - 1)
(preserving immutability), otherwise leave it undefined or omit it; ensure you
reference the same update object where contributorProgress is modified so the
removal and slot decrement happen atomically.
🪄 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: d68301e8-8fd6-473d-99a3-57712b9a2bfb

📥 Commits

Reviewing files that changed from the base of the PR and between 227aa7a and 5e53aa8.

📒 Files selected for processing (3)
  • components/bounty-detail/bounty-detail-client.tsx
  • components/bounty-detail/model4-maintainer-dashboard.tsx
  • hooks/use-bounty-application.ts

Comment on lines +60 to +71
const handleReleasePayment = (contributor: ContributorProgress) => {
releasePayment.mutate(
{
contributorId: contributor.userId,
milestoneId: contributor.currentMilestoneId,
},
{
onSuccess: () =>
toast.success(`Payment released for ${contributor.userName}`),
},
);
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing onError callbacks on mutation calls.

The mutate calls only handle onSuccess but don't handle onError. If the hook-level onError doesn't show a toast (which it currently doesn't), users won't know their action failed.

Add onError callbacks to show error toasts
   const handleReleasePayment = (contributor: ContributorProgress) => {
     releasePayment.mutate(
       {
         contributorId: contributor.userId,
         milestoneId: contributor.currentMilestoneId,
       },
       {
         onSuccess: () =>
           toast.success(`Payment released for ${contributor.userName}`),
+        onError: () =>
+          toast.error(`Failed to release payment for ${contributor.userName}`),
       },
     );
   };

   const handleAdvance = (contributor: ContributorProgress) => {
     advanceContributor.mutate(
       { contributorId: contributor.userId },
       {
         onSuccess: () =>
           toast.success(`${contributor.userName} advanced to next milestone`),
+        onError: () =>
+          toast.error(`Failed to advance ${contributor.userName}`),
       },
     );
   };

   const handleRemove = (contributor: ContributorProgress) => {
     removeContributor.mutate(
       { contributorId: contributor.userId },
       {
         onSuccess: () =>
           toast.success(`${contributor.userName} removed from bounty`),
+        onError: () =>
+          toast.error(`Failed to remove ${contributor.userName}`),
       },
     );
   };

Also applies to: 73-81, 83-91

🤖 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 -
71, Add onError handlers to the mutation calls so failures surface to users; for
each mutate invocation (e.g., releasePayment.mutate in handleReleasePayment and
the other mutate calls referenced around the same area like
approveContribution.mutate and rejectContribution.mutate), pass an onError
callback that calls toast.error with a clear message that includes the error
message or a fallback (e.g., `toast.error(\`Failed to release payment:
${error?.message ?? 'unknown error'}\`)`). Ensure the onError is paired with the
existing onSuccess handlers and uses the mutation-specific context
(contributor.userName or relevant IDs) to make the toast informative.

Comment on lines +104 to 115
const handleSendMessage = () => {
if (!selectedContributor || !messageText.trim()) return;
sendMessage.mutate(
{ contributorId: selectedContributor.userId, message: messageText },
{
onSuccess: () => {
toast.success(`Message sent to ${selectedContributor.userName}`);
setIsMessageOpen(false);
},
},
);
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing onError handler for sendMessage mutation.

Similar to other handlers, handleSendMessage should handle errors so users know if message delivery failed.

Add onError callback
   const handleSendMessage = () => {
     if (!selectedContributor || !messageText.trim()) return;
     sendMessage.mutate(
       { contributorId: selectedContributor.userId, message: messageText },
       {
         onSuccess: () => {
           toast.success(`Message sent to ${selectedContributor.userName}`);
           setIsMessageOpen(false);
         },
+        onError: () => {
+          toast.error(`Failed to send message to ${selectedContributor.userName}`);
+        },
       },
     );
   };
📝 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.

Suggested change
const handleSendMessage = () => {
if (!selectedContributor || !messageText.trim()) return;
sendMessage.mutate(
{ contributorId: selectedContributor.userId, message: messageText },
{
onSuccess: () => {
toast.success(`Message sent to ${selectedContributor.userName}`);
setIsMessageOpen(false);
},
},
);
};
const handleSendMessage = () => {
if (!selectedContributor || !messageText.trim()) return;
sendMessage.mutate(
{ contributorId: selectedContributor.userId, message: messageText },
{
onSuccess: () => {
toast.success(`Message sent to ${selectedContributor.userName}`);
setIsMessageOpen(false);
},
onError: () => {
toast.error(`Failed to send message to ${selectedContributor.userName}`);
},
},
);
};
🤖 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 104 -
115, handleSendMessage currently calls sendMessage.mutate without an onError
handler; update handleSendMessage to pass an onError callback to
sendMessage.mutate (alongside the existing onSuccess) that shows an error toast
(e.g., toast.error with a descriptive message and optional error.message) and
keeps the message UI open or resets state appropriately. Locate the
sendMessage.mutate call inside the handleSendMessage function and add the
onError option to mirror other mutation handlers in this file.

Comment on lines +268 to 276
{advanceContributor.isPending &&
advanceContributor.variables?.contributorId ===
contributor.userId ? (
<Loader2 className="size-3 mr-1.5 animate-spin" />
) : (
<>
Advance [Coming soon]{" "}
<ArrowRight className="size-3 ml-1.5" />
Advance <ArrowRight className="size-3 ml-1.5" />
</>
)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Advance button label missing during loading state.

When advanceContributor.isPending is true for this contributor, only the Loader2 spinner is rendered. The "Advance" text and arrow icon are lost, unlike the other buttons that keep their labels during loading.

Keep label visible during loading
                           >
-                            {advanceContributor.isPending &&
-                            advanceContributor.variables?.contributorId ===
-                              contributor.userId ? (
+                            {advanceContributor.isPending &&
+                            advanceContributor.variables?.contributorId ===
+                              contributor.userId && (
                               <Loader2 className="size-3 mr-1.5 animate-spin" />
-                            ) : (
-                              <>
-                                Advance <ArrowRight className="size-3 ml-1.5" />
-                              </>
                             )}
+                            Advance <ArrowRight className="size-3 ml-1.5" />
                           </Button>
📝 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.

Suggested change
{advanceContributor.isPending &&
advanceContributor.variables?.contributorId ===
contributor.userId ? (
<Loader2 className="size-3 mr-1.5 animate-spin" />
) : (
<>
Advance [Coming soon]{" "}
<ArrowRight className="size-3 ml-1.5" />
Advance <ArrowRight className="size-3 ml-1.5" />
</>
)}
{advanceContributor.isPending &&
advanceContributor.variables?.contributorId ===
contributor.userId && (
<Loader2 className="size-3 mr-1.5 animate-spin" />
)}
Advance <ArrowRight className="size-3 ml-1.5" />
🤖 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 268 -
276, The Advance button currently renders only Loader2 when
advanceContributor.isPending for the contributor, removing the "Advance" text
and ArrowRight icon; update the JSX in the Advance button (the block using
advanceContributor, contributor.userId, Loader2 and ArrowRight) to always render
the "Advance" label and ArrowRight icon and conditionally render Loader2
alongside them when advanceContributor.isPending (e.g., show Loader2
before/after the label and keep the label+icon visible), preserving the existing
conditional check on advanceContributor.variables?.contributorId to target the
correct contributor.

Comment on lines +60 to +78
if (milestoneIndex >= 0 && milestoneIndex < milestones.length - 1) {
const nextMilestone = milestones[milestoneIndex + 1];
const newProgress = [...contributorProgress];
newProgress[contributorIndex] = {
...newProgress[contributorIndex],
currentMilestoneId: nextMilestone.id,
};

queryClient.setQueryData<ExtendedBountyQuery>(
bountyKeys.detail(bountyId),
{
...previous,
bounty: {
...previous.bounty,
contributorProgress: newProgress,
},
},
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

No guard when contributor is already at the last milestone.

If a contributor is on the final milestone (milestoneIndex === milestones.length - 1), the condition at line 60 prevents the cache update, but the mutation still "succeeds" with a toast saying the contributor advanced. This creates a confusing UX where the user sees success but nothing changes.

Consider throwing or returning early in mutationFn
   const advanceContributor = useMutation({
-    mutationFn: async ({ contributorId }: { contributorId: string }) => {
+    mutationFn: async ({ contributorId }: { contributorId: string }) => {
+      // Check if already at last milestone before making the call
+      const current = queryClient.getQueryData<ExtendedBountyQuery>(
+        bountyKeys.detail(bountyId),
+      );
+      if (current?.bounty) {
+        const progress = current.bounty.contributorProgress || [];
+        const milestones = current.bounty.milestones || [];
+        const contributor = progress.find((c) => c.userId === contributorId);
+        if (contributor) {
+          const idx = milestones.findIndex((m) => m.id === contributor.currentMilestoneId);
+          if (idx >= milestones.length - 1) {
+            throw new Error("Contributor is already at the final milestone");
+          }
+        }
+      }
       await delay(1000);
       return { contributorId };
     },
🤖 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-application.ts` around lines 60 - 78, The cache update path
assumes there is a next milestone but doesn't handle the case when
milestoneIndex === milestones.length - 1, so the mutation reports success with
no state change; update the mutation to guard against already-at-final-milestone
by checking milestoneIndex against milestones.length - 1 (using the same
variables: milestoneIndex, milestones, contributorIndex, contributorProgress)
and either return early or throw an error from the mutationFn so the success
path (which calls queryClient.setQueryData on bountyKeys.detail(bountyId)) and
the success toast are not executed; ensure the guard is placed before the code
that constructs newProgress and calls queryClient.setQueryData to prevent
misleading UX.

Comment on lines +83 to +88
onError: (_err, _vars, context) => {
if (context?.previous) {
queryClient.setQueryData(bountyKeys.detail(bountyId), context.previous);
}
},
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing onError callback for user feedback on mutation failures.

Both advanceContributor and removeContributor silently rollback on error without notifying the user. When optimistic updates fail, the UI reverts but the user has no indication that their action didn't succeed on the server.

Proposed fix to add error toasts
+import { toast } from "sonner";
+
 // In advanceContributor mutation:
     onError: (_err, _vars, context) => {
       if (context?.previous) {
         queryClient.setQueryData(bountyKeys.detail(bountyId), context.previous);
       }
+      toast.error("Failed to advance contributor. Please try again.");
     },

 // In removeContributor mutation:
     onError: (_err, _vars, context) => {
       if (context?.previous) {
         queryClient.setQueryData(bountyKeys.detail(bountyId), context.previous);
       }
+      toast.error("Failed to remove contributor. Please try again.");
     },

Also applies to: 123-128

🤖 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-application.ts` around lines 83 - 88, The optimistic
mutation handlers (advanceContributor and removeContributor) currently only
rollback via queryClient.setQueryData in their onError callbacks, leaving users
uninformed; update both mutation definitions to add an onError that (1) rolls
back as now using context.previous (keep
queryClient.setQueryData(bountyKeys.detail(bountyId), context.previous)), and
(2) triggers a user-facing error toast/message with the error details (use the
mutation onError params to extract the error) so the UI both reverts and shows a
clear failure message to the user; reference the existing onError blocks for
these mutations and reuse bountyId, bountyKeys.detail, and the context.previous
logic.

@drips-wave
Copy link
Copy Markdown

drips-wave Bot commented May 30, 2026

@Jopsan-gm Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Jopsan-gm
Copy link
Copy Markdown
Contributor Author

Hello @Benjtalkshow PR ready! ;)

@Benjtalkshow
Copy link
Copy Markdown
Contributor

Hello @Benjtalkshow PR ready! ;)

A few things to fix before this can merge.

This PR duplicates PR #263's work for issue #211. The BountyApplication interface and the new fields you added to Bounty in types/bounty.ts belong to #211, not #205. Please remove them.

Three actions don't fully meet the acceptance criteria. Release Payment only invalidates without an escrow update. Remove filters contributorProgress but doesn't decrement totalSlotsOccupied (the code comment admits this). Send Message just await delay(1000) and returns without recording anything against the bounty. Please address each.

Two code quality items: useBountyApplication(bountyId) returns four mutations as one object, but the rest of hooks/use-bounty-application.ts exports one mutation per hook. Split into useReleasePayment, useAdvanceContributor, useRemoveContributor, useSendMessage. Also move type ExtendedBountyQuery and const delay from mid-file (lines 327, 331) to the top.

Last thing: there's significant Prettier reformatting noise across 10+ unrelated files (mode-toggle.tsx, dispute page, saved page, both bounty cards, document-upload, submission-draft test, use-bounty-cta-state.ts, generated.ts, types/participation.ts). Revert those to upstream so the diff only shows the actual #205 work.

@Benjtalkshow
Copy link
Copy Markdown
Contributor

Hi @Jopsan-gm
Most of the previous feedback is addressed. Hook split, top-of-file types, real escrow update on Release Payment, slot decrement on Remove, message recording on Send Message, and the Prettier noise is gone.

One last thing: types/bounty.ts still has the 23-line addition for BountyApplication and the new applications / claimCount / maxParticipants / assignedContributorId fields. That's PR #263's work for issue #211 and will collide with it. Please revert types/bounty.ts to upstream. Your dashboard code doesn't depend on those fields directly, so the revert should be clean.

Once that's reverted, this is ready.

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.

Implement the maintainer dashboard actions for Multi-Winner Milestone bounties

2 participants