feat: implement raise dispute flow with redirect to dispute review page#256
feat: implement raise dispute flow with redirect to dispute review page#256od-hunter wants to merge 1 commit into
Conversation
- Add useRaiseDispute mutation hook in hooks/use-bounty-application.ts
that POSTs to /api/disputes with bountyId, reason, and description,
then invalidates the bounty detail and list queries on success
- Create app/api/disputes/route.ts Next.js API route handler that
authenticates the user, validates input, and proxies the request
to the backend REST endpoint with the auth token
- Enable the Raise Dispute button in bounty-detail-sidebar-cta.tsx
(removes disabled + Coming Soon label), wires up an AlertDialog
with a reason Select (DisputeReasonEnum) and description Textarea,
inline validation that keeps the dialog open on empty fields, a
loading spinner during submission, and on success closes the dialog,
shows a toast, and redirects to /dispute/{newDisputeId}
- Document the RaiseDispute operation in admin-dispute.graphql
|
@od-hunter 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 a complete "Raise a Dispute" feature. A new ChangesRaise Dispute Flow
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 |
|
@od-hunter 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! 🚀 |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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/disputes/route.ts`:
- Around line 66-77: The fetch call that posts to `${backendUrl}/disputes` in
route.ts needs an AbortController-based timeout to avoid hanging; create an
AbortController, pass controller.signal into the fetch options for the
backendResponse call, start a setTimeout (e.g. 5–10s) that calls
controller.abort(), and clear the timeout after fetch completes (use
try/finally). Ensure you handle the abort/AbortError (from fetch of
backendResponse) and return an appropriate 504/timeout response instead of
leaving the request open.
- Around line 41-46: Validate the incoming reason against the DisputeReasonEnum
instead of accepting any string: in the POST handler in route.ts (where reason
is checked) replace the loose typeof check with a membership check against
DisputeReasonEnum (or a helper like isValidDisputeReason) and return
NextResponse.json({ error: "invalid reason", valid:
Object.values(DisputeReasonEnum) }, { status: 400 }) when it does not match so
invalid dispute reasons are rejected early before forwarding downstream.
🪄 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: ee8e4a10-c726-408b-a207-2423c2ca8a8e
📒 Files selected for processing (4)
app/api/disputes/route.tscomponents/bounty-detail/bounty-detail-sidebar-cta.tsxhooks/use-bounty-application.tslib/graphql/operations/admin-dispute.graphql
| if (!reason || typeof reason !== "string") { | ||
| return NextResponse.json( | ||
| { error: "reason is required" }, | ||
| { status: 400 }, | ||
| ); | ||
| } |
There was a problem hiding this comment.
Reject values outside DisputeReasonEnum.
This currently accepts any string and forwards it downstream, so invalid dispute reasons bypass server-side validation and fail later with a generic backend error.
Suggested fix
-import type { AdminDisputeDto, DisputeReasonEnum } from "`@/lib/graphql/generated`";
+import { DisputeReasonEnum } from "`@/lib/graphql/generated`";
+import type { AdminDisputeDto } from "`@/lib/graphql/generated`";
...
- if (!reason || typeof reason !== "string") {
+ if (
+ !reason ||
+ typeof reason !== "string" ||
+ !Object.values(DisputeReasonEnum).includes(reason as DisputeReasonEnum)
+ ) {
return NextResponse.json(
- { error: "reason is required" },
+ { error: "reason must be a valid dispute reason" },
{ status: 400 },
);
}🤖 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/disputes/route.ts` around lines 41 - 46, Validate the incoming reason
against the DisputeReasonEnum instead of accepting any string: in the POST
handler in route.ts (where reason is checked) replace the loose typeof check
with a membership check against DisputeReasonEnum (or a helper like
isValidDisputeReason) and return NextResponse.json({ error: "invalid reason",
valid: Object.values(DisputeReasonEnum) }, { status: 400 }) when it does not
match so invalid dispute reasons are rejected early before forwarding
downstream.
| const backendResponse = await fetch(`${backendUrl}/disputes`, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| ...(token ? { Authorization: `Bearer ${token}` } : {}), | ||
| }, | ||
| body: JSON.stringify({ | ||
| campaignId, | ||
| reason, | ||
| description, | ||
| }), | ||
| }); |
There was a problem hiding this comment.
Add a timeout to the backend proxy call.
The route waits on fetch() with no abort/timeout, so a slow or wedged backend can pin this request until the platform times it out.
Suggested fix
+ const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), 10_000);
+
- const backendResponse = await fetch(`${backendUrl}/disputes`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- ...(token ? { Authorization: `Bearer ${token}` } : {}),
- },
- body: JSON.stringify({
- campaignId,
- reason,
- description,
- }),
- });
+ let backendResponse: Response;
+ try {
+ backendResponse = await fetch(`${backendUrl}/disputes`, {
+ method: "POST",
+ signal: controller.signal,
+ headers: {
+ "Content-Type": "application/json",
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
+ },
+ body: JSON.stringify({
+ campaignId,
+ reason,
+ description,
+ }),
+ });
+ } finally {
+ clearTimeout(timeout);
+ }🤖 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/disputes/route.ts` around lines 66 - 77, The fetch call that posts to
`${backendUrl}/disputes` in route.ts needs an AbortController-based timeout to
avoid hanging; create an AbortController, pass controller.signal into the fetch
options for the backendResponse call, start a setTimeout (e.g. 5–10s) that calls
controller.abort(), and clear the timeout after fetch completes (use
try/finally). Ensure you handle the abort/AbortError (from fetch of
backendResponse) and return an appropriate 504/timeout response instead of
leaving the request open.
Benjtalkshow
left a comment
There was a problem hiding this comment.
The dispute flow itself is wired correctly: useRaiseDispute hook, REST proxy with auth check and validation, dialog with reason select and description textarea, inline validation, success path that closes the dialog and redirects to /dispute/{id}. All four acceptance criteria pass functionally.
Three things to fix before merge.
app/api/disputes/route.ts:3 imports graphqlRequest but never uses it. The PR uses REST since the GraphQL mutation isn't available yet. Remove the dead import.
Same file, line 63: const { getAccessToken } = await import("@/lib/auth-utils"); is a dynamic import inside the handler. Move it to a regular top-of-file import.
The biggest issue: this adds 176 lines inline to bounty-detail-sidebar-cta.tsx, growing it from around 387 to 563 lines. Issue #209 has a separate open PR (Biokes) trying to split this file because it's already too big. Please extract a RaiseDisputeDialog component into its own file. The sidebar only needs the trigger button and the disputeDialogOpen state. Move the dialog markup, DISPUTE_REASON_LABELS, validation, mutation wiring, and reset-on-close logic into the new component.
Once those are in this is ready.
Closes #203
#203
Summary by CodeRabbit