Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 25 additions & 9 deletions components/bounty-detail/bounty-detail-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,19 @@ export function BountyDetailClient({ bountyId }: { bountyId: string }) {
(bounty as { submissions?: CompetitionSubmissionEntry[] | null })
.submissions ?? [];

const mySubmission = bounty.submissions?.find(
(s) => s.submittedBy === session?.user?.id,
);
const hasRevision =
mySubmission?.status === "REVISION_REQUESTED" &&
!!mySubmission?.reviewComments;
Comment on lines +174 to +176

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

Potential stuck state if revision requested without feedback.

The check for non-empty reviewComments on line 176 prevents showSubmitPanel from being true when bounty.status === "UNDER_REVIEW" unless feedback is present. If a revision is somehow requested with empty or missing reviewComments, the contributor would be unable to see the submission panel to resubmit their work, creating a stuck state.

While the UI validation should prevent this (per the PR description, "Send is disabled when textarea is empty"), relying solely on client-side validation is fragile if the backend doesn't enforce the same constraint.

Consider removing the && !!mySubmission?.reviewComments check and allowing the ApplicationSubmitWorkPanel component to handle empty feedback gracefully (e.g., showing the form without a feedback banner).

🛡️ Proposed fix to prevent stuck state
 const hasRevision =
-  mySubmission?.status === "REVISION_REQUESTED" &&
-  !!mySubmission?.reviewComments;
+  mySubmission?.status === "REVISION_REQUESTED";

With this change, revisionFeedback at line 260-263 would be undefined when comments are missing, and the panel would still allow resubmission.

📝 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 hasRevision =
mySubmission?.status === "REVISION_REQUESTED" &&
!!mySubmission?.reviewComments;
const hasRevision =
mySubmission?.status === "REVISION_REQUESTED";
🤖 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 174 - 176,
The current hasRevision boolean (used to compute showSubmitPanel) includes a
check for mySubmission?.reviewComments which can block showing
ApplicationSubmitWorkPanel when a revision was requested but reviewComments is
empty; remove the "&& !!mySubmission?.reviewComments" condition so hasRevision
is simply mySubmission?.status === "REVISION_REQUESTED", and let
ApplicationSubmitWorkPanel (and the revisionFeedback prop/logic) handle
undefined/empty feedback gracefully; update any references that compute
revisionFeedback to tolerate undefined reviewComments.

const showSubmitPanel =
bounty.type === "MILESTONE_BASED" &&
isAssignedApplicant &&
!!walletAddress &&
(bounty.status === "IN_PROGRESS" ||
(bounty.status === "UNDER_REVIEW" && hasRevision));

return (
<div className="flex flex-col lg:flex-row gap-10">
{/* Main content */}
Expand Down Expand Up @@ -240,22 +253,25 @@ export function BountyDetailClient({ bountyId }: { bountyId: string }) {
/>
)}

{bounty.type === "MILESTONE_BASED" &&
isAssignedApplicant &&
walletAddress &&
bounty.status === "IN_PROGRESS" && (
<ApplicationSubmitWorkPanel
bountyId={bountyId}
contributorAddress={walletAddress}
/>
)}
{showSubmitPanel && (
<ApplicationSubmitWorkPanel
bountyId={bountyId}
contributorAddress={walletAddress}
revisionFeedback={
hasRevision
? (mySubmission?.reviewComments ?? undefined)
: undefined
}
/>
)}

{bounty.type === "MILESTONE_BASED" &&
isCreator &&
bounty.status === "UNDER_REVIEW" && (
<SubmissionApprovalPanel
bounty={bounty}
creatorAddress={walletAddress}
submissionId={bounty.submissions?.[0]?.id}
submittedWorkCid={
bounty.submissions?.[0]?.githubPullRequestUrl || undefined
}
Expand Down
24 changes: 23 additions & 1 deletion components/bounty/application-submit-work-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
"use client";

import { useState } from "react";
import { Upload, Link as LinkIcon, Send } from "lucide-react";
import {
Upload,
Link as LinkIcon,
Send,
MessageSquareWarning,
} from "lucide-react";
import {
Card,
CardContent,
Expand All @@ -17,11 +22,13 @@ import { useSubmitApplicationWork } from "@/hooks/use-bounty-application";
interface ApplicationSubmitWorkPanelProps {
bountyId: string;
contributorAddress: string;
revisionFeedback?: string;
}

export function ApplicationSubmitWorkPanel({
bountyId,
contributorAddress,
revisionFeedback,
}: ApplicationSubmitWorkPanelProps) {
const [workCid, setWorkCid] = useState("");

Expand Down Expand Up @@ -51,6 +58,21 @@ export function ApplicationSubmitWorkPanel({
</CardDescription>
</CardHeader>
<CardContent className="pt-6">
{revisionFeedback && (
<div className="mb-6 rounded-lg border border-amber-500/30 bg-amber-500/10 p-4">
<div className="flex items-start gap-3">
<MessageSquareWarning className="size-5 text-amber-400 mt-0.5 shrink-0" />
<div>
<p className="text-sm font-semibold text-amber-300 mb-1">
Revision Requested
</p>
<p className="text-sm text-amber-200/80 whitespace-pre-wrap leading-relaxed">
{revisionFeedback}
</p>
</div>
</div>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-2">
<Label htmlFor="work-cid">
Expand Down
120 changes: 101 additions & 19 deletions components/bounty/submission-approval-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
AlertTriangle,
ExternalLink,
ShieldCheck,
RotateCcw,
} from "lucide-react";
import {
Card,
Expand All @@ -18,7 +19,12 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { useApproveApplicationSubmission } from "@/hooks/use-bounty-application";
import { Textarea } from "@/components/ui/textarea";
import { toast } from "sonner";
import {
useApproveApplicationSubmission,
useRequestRevisions,
} from "@/hooks/use-bounty-application";
import type { BountyFieldsFragment } from "@/lib/graphql/generated";
import type { Bounty } from "@/types/bounty";

Expand All @@ -29,19 +35,26 @@ interface SubmissionApprovalPanelProps {
creatorAddress: string;
submittedWorkCid?: string;
submissionDescription?: string;
submissionId?: string;
}

export function SubmissionApprovalPanel({
bounty,
creatorAddress,
submittedWorkCid,
submissionDescription,
submissionId,
}: SubmissionApprovalPanelProps) {
const [points, setPoints] = useState<number>(5);
const [showRevisionForm, setShowRevisionForm] = useState(false);
const [revisionFeedback, setRevisionFeedback] = useState("");

const { mutate: approveSubmission, isPending: isApproving } =
useApproveApplicationSubmission();

const { mutate: requestRevisions, isPending: isRequestingRevisions } =
useRequestRevisions();

const handleApprove = () => {
const clampedPoints = Math.max(1, Math.min(100, points || 0));
approveSubmission({
Expand All @@ -51,6 +64,27 @@ export function SubmissionApprovalPanel({
});
};

const handleRequestRevisions = () => {
if (!submissionId || !revisionFeedback.trim()) return;
requestRevisions(
{
bountyId: bounty.id,
submissionId,
feedback: revisionFeedback.trim(),
},
{
onSuccess: () => {
toast.success("Revision request sent to contributor.");
setShowRevisionForm(false);
setRevisionFeedback("");
},
onError: () => {
toast.error("Failed to request revisions. Please try again.");
},
},
);
};

return (
<Card className="border-emerald-500/20 bg-emerald-500/5 backdrop-blur-sm overflow-hidden">
<CardHeader className="border-b border-emerald-500/10 pb-4">
Expand Down Expand Up @@ -131,25 +165,73 @@ export function SubmissionApprovalPanel({
</Button>
</div>

{/* Revision Section - Coming Soon */}
{/* Revision Section */}
<div className="space-y-4 border-l border-gray-800/50 pl-6">
<div className="h-full flex flex-col justify-center items-center text-center p-4 border border-dashed border-gray-800 rounded-lg opacity-60">
<AlertTriangle className="size-6 text-gray-500 mb-2" />
<h4 className="text-sm font-medium text-gray-400 mb-1">
Needs Changes?
</h4>
<p className="text-xs text-gray-500 mb-4">
Request revisions before releasing the escrow.
</p>
<Button
variant="outline"
size="sm"
className="border-gray-700 text-gray-500 cursor-not-allowed"
disabled
>
Coming Soon
</Button>
</div>
{!showRevisionForm ? (
<div className="h-full flex flex-col justify-center items-center text-center p-4 border border-dashed border-gray-800 rounded-lg">
<AlertTriangle className="size-6 text-amber-500 mb-2" />
<h4 className="text-sm font-medium text-gray-300 mb-1">
Needs Changes?
</h4>
<p className="text-xs text-gray-500 mb-4">
Request revisions before releasing the escrow.
</p>
<Button
variant="outline"
size="sm"
className="border-amber-700/50 text-amber-400 hover:bg-amber-500/10 hover:border-amber-600"
onClick={() => setShowRevisionForm(true)}
disabled={!submissionId}
>
<RotateCcw className="size-3 mr-1.5" />
Request Revisions
</Button>
</div>
) : (
<div className="space-y-3">
<div>
<Label
htmlFor="revision-feedback"
className="text-sm font-medium text-gray-300"
>
Revision Feedback
</Label>
<p className="text-xs text-gray-500 mt-1 mb-2">
Describe what needs to change before you can approve.
</p>
<Textarea
id="revision-feedback"
placeholder="Please update the API documentation to include error codes, and add unit tests for the authentication flow..."
value={revisionFeedback}
onChange={(e) => setRevisionFeedback(e.target.value)}
className="bg-gray-900/50 border-gray-700 resize-none min-h-[100px]"
/>
</div>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
className="flex-1 border-gray-700 text-gray-400"
onClick={() => {
setShowRevisionForm(false);
setRevisionFeedback("");
}}
disabled={isRequestingRevisions}
>
Cancel
</Button>
<Button
size="sm"
className="flex-1 bg-amber-500 hover:bg-amber-600 text-white"
onClick={handleRequestRevisions}
disabled={!revisionFeedback.trim() || isRequestingRevisions}
>
<RotateCcw className="size-3 mr-1.5" />
{isRequestingRevisions ? "Sending..." : "Send Request"}
</Button>
</div>
</div>
)}
</div>
</div>
</CardContent>
Expand Down
Loading