diff --git a/.github/workflows/check-schema.yml b/.github/workflows/check-schema.yml index b1458a85..a47bc0c2 100644 --- a/.github/workflows/check-schema.yml +++ b/.github/workflows/check-schema.yml @@ -10,28 +10,38 @@ jobs: check-schema: name: Check schema is in sync runs-on: ubuntu-latest + env: + BOUNDLESS_NESTJS_TOKEN: ${{ secrets.BOUNDLESS_NESTJS_TOKEN }} + steps: - name: Checkout repository uses: actions/checkout@v4 - name: Checkout canonical schema repo (private) - if: ${{ secrets.BOUNDLESS_NESTJS_TOKEN != '' }} + if: ${{ env.BOUNDLESS_NESTJS_TOKEN != '' }} uses: actions/checkout@v4 with: repository: boundlessfi/boundless-nestjs path: boundless-nestjs - token: ${{ secrets.BOUNDLESS_NESTJS_TOKEN }} + token: ${{ env.BOUNDLESS_NESTJS_TOKEN }} - name: Note about private repo - if: ${{ secrets.BOUNDLESS_NESTJS_TOKEN == '' }} + if: ${{ env.BOUNDLESS_NESTJS_TOKEN == '' }} run: | echo "No secret BOUNDLESS_NESTJS_TOKEN provided; skipping checkout of private repo." - echo "If the canonical schema is in the private repo, add a repository secret named BOUNDLESS_NESTJS_TOKEN with a PAT that has access to boundlessfi/boundless-nestjs." + echo "This is expected for forked pull requests where repository secrets are not exposed." - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: "20" - name: Run schema check + if: ${{ env.BOUNDLESS_NESTJS_TOKEN != '' }} run: node ./scripts/sync-schema.js --check + + - name: Skip schema check without private repo token + if: ${{ env.BOUNDLESS_NESTJS_TOKEN == '' }} + run: | + echo "Skipping schema sync check because BOUNDLESS_NESTJS_TOKEN is not available." + echo "Maintainers can run the full schema check from a trusted branch with the secret available." \ No newline at end of file diff --git a/components/bounty/application-review-dashboard.tsx b/components/bounty/application-review-dashboard.tsx index 2fdd0d16..0cafb9dc 100644 --- a/components/bounty/application-review-dashboard.tsx +++ b/components/bounty/application-review-dashboard.tsx @@ -1,6 +1,5 @@ "use client"; - -import { useState } from "react"; +import { useMemo, useState } from "react"; import { Users, CheckCircle, @@ -8,12 +7,27 @@ import { Star, Trophy, ArrowRight, + XCircle, } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; - -import { useSelectApplicant } from "@/hooks/use-bounty-application"; +import { Textarea } from "@/components/ui/textarea"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { toast } from "sonner"; +import { + useDeclineApplicant, + useSelectApplicant, +} from "@/hooks/use-bounty-application"; export interface Application { id: string; @@ -45,9 +59,14 @@ export function ApplicationReviewDashboard({ applications, }: ApplicationReviewDashboardProps) { const [selectedForCompare, setSelectedForCompare] = useState([]); + const [reviewApplications, setReviewApplications] = useState(applications); + const [declineTarget, setDeclineTarget] = useState(null); + const [declineReason, setDeclineReason] = useState(""); const { mutate: selectApplicant, isPending: isSelecting } = useSelectApplicant(); - + useEffect(() => { + setReviewApplications(applications); + }, [applications]); const handleSelectApplicant = (applicantAddress: string) => { selectApplicant({ bountyId, @@ -56,6 +75,44 @@ export function ApplicationReviewDashboard({ }); }; + const { mutate: declineApplicant, isPending: isDeclining } = + useDeclineApplicant(); + + const handleDeclineApplicant = () => { + if (!declineTarget) return; + + const previousApplications = reviewApplications; + const applicantAddress = declineTarget.applicantAddress; + const reason = declineReason.trim(); + + setReviewApplications((current) => + current.filter((app) => app.applicantAddress !== applicantAddress), + ); + + setSelectedForCompare((current) => + current.filter((id) => id !== declineTarget.id), + ); + + declineApplicant( + { + bountyId, + applicantAddress, + reason, + }, + { + onSuccess: () => { + toast.success("Applicant declined"); + setDeclineTarget(null); + setDeclineReason(""); + }, + onError: () => { + setReviewApplications(previousApplications); + toast.error("Failed to decline applicant"); + }, + }, + ); + }; + const visibleApplications = reviewApplications; const toggleCompare = (id: string) => { if (selectedForCompare.includes(id)) { setSelectedForCompare(selectedForCompare.filter((i) => i !== id)); @@ -113,6 +170,19 @@ export function ApplicationReviewDashboard({ {selectedForCompare.includes(app.id) ? "Comparing" : "Compare"} )} +
- {applications + {visibleApplications .filter((app) => selectedForCompare.includes(app.id)) .map((app) => renderApplicationCard(app, false))}
) : (
- {applications.map((app) => renderApplicationCard(app, false))} + {visibleApplications.map((app) => renderApplicationCard(app, false))}
)} + { + if (!open) { + setDeclineTarget(null); + setDeclineReason(""); + } + }} + > + + + Decline applicant? + + This will remove the applicant from the review queue. You can + include an optional reason for internal records. + + + +