feat: implement maintainer dashboard actions for multi-winner milestone bounties#265
feat: implement maintainer dashboard actions for multi-winner milestone bounties#265Jopsan-gm wants to merge 7 commits into
Conversation
|
@Jopsan-gm is attempting to deploy a commit to the Threadflow Team on Vercel. A member of the Team first needs to authorize it. |
|
Warning Review limit reached
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 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 configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughThis 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. ChangesModel 4 Maintainer Dashboard Actions Implementation
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (2)
hooks/use-bounty-application.ts (2)
32-88: ⚡ Quick winMissing
onSettledinvalidation for optimistic mutations.
advanceContributorandremoveContributoruse 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 fortotalSlotsOccupied.The comment acknowledges that
totalSlotsOccupiedshould be decremented but isn't implemented. Per issue#205, removing a contributor should "decrementtotalSlotsOccupied(visible removal and slot count update)."Would you like me to help implement the
totalSlotsOccupieddecrement, 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
📒 Files selected for processing (3)
components/bounty-detail/bounty-detail-client.tsxcomponents/bounty-detail/model4-maintainer-dashboard.tsxhooks/use-bounty-application.ts
| const handleReleasePayment = (contributor: ContributorProgress) => { | ||
| releasePayment.mutate( | ||
| { | ||
| contributorId: contributor.userId, | ||
| milestoneId: contributor.currentMilestoneId, | ||
| }, | ||
| { | ||
| onSuccess: () => | ||
| toast.success(`Payment released for ${contributor.userName}`), | ||
| }, | ||
| ); | ||
| }; |
There was a problem hiding this comment.
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.
| const handleSendMessage = () => { | ||
| if (!selectedContributor || !messageText.trim()) return; | ||
| sendMessage.mutate( | ||
| { contributorId: selectedContributor.userId, message: messageText }, | ||
| { | ||
| onSuccess: () => { | ||
| toast.success(`Message sent to ${selectedContributor.userName}`); | ||
| setIsMessageOpen(false); | ||
| }, | ||
| }, | ||
| ); | ||
| }; |
There was a problem hiding this comment.
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.
| 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.
| {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" /> | ||
| </> | ||
| )} |
There was a problem hiding this comment.
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.
| {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.
| 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, | ||
| }, | ||
| }, | ||
| ); | ||
| } |
There was a problem hiding this comment.
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.
| onError: (_err, _vars, context) => { | ||
| if (context?.previous) { | ||
| queryClient.setQueryData(bountyKeys.detail(bountyId), context.previous); | ||
| } | ||
| }, | ||
| }); |
There was a problem hiding this comment.
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.
|
@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! 🚀 |
|
Hello @Benjtalkshow PR ready! ;) |
A few things to fix before this can merge. This PR duplicates PR #263's work for issue #211. The Three actions don't fully meet the acceptance criteria. Release Payment only invalidates without an escrow update. Remove filters Two code quality items: Last thing: there's significant Prettier reformatting noise across 10+ unrelated files ( |
…eaning up PR noise
|
Hi @Jopsan-gm One last thing: Once that's reverted, this is ready. |
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:
handleActionstub with dedicated mutation hooks using@tanstack/react-query(releasePayment,advanceContributor,removeContributor,sendMessage).sonnertoasts to provide immediate success feedback after an action is executed.[Coming soon]suffix from action buttons.Dialogcomponents for the View Submissions and Message actions.bountyIdappropriately to ensure the mutations target the correct bounty scope.contributorProgressfields.Screenshots
Summary by CodeRabbit