Skip to content
Merged
10 changes: 6 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ on:
jobs:
build-and-lint:
runs-on: ubuntu-latest
timeout-minutes: 15

strategy:
matrix:
node-version: [24.x]
node-version: [22.x]

steps:
- name: Checkout repository
Expand Down Expand Up @@ -55,15 +56,16 @@ jobs:
test-e2e:
runs-on: ubuntu-latest
needs: build-and-lint
timeout-minutes: 15

steps:
- name: Checkout repository
uses: actions/checkout@v4
Comment on lines 62 to 63

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and print relevant section of the workflow
FILE=".github/workflows/ci.yml"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE" >&2
  exit 1
fi

# Show around the referenced lines
nl -ba "$FILE" | sed -n '45,95p'

Repository: boundlessfi/bounties

Length of output: 108


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/ci.yml"
test -f "$FILE"

echo "== Preview lines 55-80 =="
cat -n "$FILE" | sed -n '55,80p'

echo
echo "== Find all actions/checkout steps =="
rg -n "uses:\s*actions/checkout@|actions/checkout@" "$FILE" || true

echo
echo "== Find persist-credentials settings =="
rg -n "persist-credentials" "$FILE" || true

Repository: boundlessfi/bounties

Length of output: 1026


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/ci.yml"

echo "== Preview lines 1-40 =="
cat -n "$FILE" | sed -n '1,40p'

echo
echo "== Find all persist-credentials settings =="
rg -n "persist-credentials" "$FILE" || true

Repository: boundlessfi/bounties

Length of output: 1286


Disable persisted checkout credentials in CI (persist-credentials: false).

Both actions/checkout@v4 steps (lines 20 and 63) lack persist-credentials: false, which leaves the default token configured for later steps.

🛡️ Suggested fix
       - name: Checkout repository
         uses: actions/checkout@v4
+        with:
+          persist-credentials: false
📝 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
- name: Checkout repository
uses: actions/checkout@v4
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 62-63: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 63-63: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 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 @.github/workflows/ci.yml around lines 62 - 63, Add persist-credentials:
false to both checkout steps that use actions/checkout@v4 to avoid leaking the
default GITHUB_TOKEN to later steps; locate the steps that include "uses:
actions/checkout@v4" and add the key-value pair persist-credentials: false under
each step so credentials are not persisted for subsequent workflow actions.

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/ci.yml"
echo "== File: $FILE =="
ls -l "$FILE"
echo

echo "== Relevant uses lines (with numbers) =="
nl -ba "$FILE" | sed -n '40,140p'

echo
echo "== rg for uses: actions/ =="
rg -n "uses:\s*actions/|uses:\s*pnpm/" "$FILE" || true

Repository: boundlessfi/bounties

Length of output: 279


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/ci.yml"
echo "== File: $FILE =="
ls -l "$FILE"
echo

echo "== Relevant uses lines (with numbers) =="
nl -ba "$FILE" | sed -n '40,140p'

echo
echo "== rg for uses: actions/ =="
rg -n "uses:\s*actions/|uses:\s*pnpm/" "$FILE" || true

Repository: boundlessfi/bounties

Length of output: 279


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/ci.yml"
echo "== File: $FILE =="
ls -l "$FILE"
echo

echo "== Relevant uses lines (with numbers) =="
nl -ba "$FILE" | sed -n '40,140p'

echo
echo "== rg for uses: actions/ =="
rg -n "uses:\s*actions/|uses:\s*pnpm/" "$FILE" || true

Repository: boundlessfi/bounties

Length of output: 279


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/ci.yml"
echo "== File: $FILE =="
nl -ba "$FILE" | sed -n '1,200p'

Repository: boundlessfi/bounties

Length of output: 162


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/ci.yml"
echo "== File: $FILE =="
ls -l "$FILE"
echo

echo "== Line-numbered excerpt (40-140) =="
awk '{printf "%d\t%s\n", NR, $0}' "$FILE" | sed -n '40,140p'

echo
echo "== rg uses: actions/ or pnpm/ =="
rg -n "uses:\s*actions/|uses:\s*pnpm/" "$FILE" || true

echo
echo "== rg for persist-credentials in checkout steps =="
rg -n "persist-credentials" "$FILE" || true

Repository: boundlessfi/bounties

Length of output: 3938


🌐 Web query:

GitHub Actions actions/checkout persist-credentials default value true and description

💡 Result:

For actions/checkout, the input persist-credentials has default value true, and it controls whether the auth token / SSH key is configured in the local git config. Default value: true [1] Description: “Whether to configure the token or SSH key with the local git config” [1] Additional context from the action README: the auth token is persisted in the local git config so scripts can run authenticated git commands, and the token is removed during post-job cleanup; you can opt out with persist-credentials: false. [2]

Citations:


🌐 Web query:

actions/checkout persist-credentials defaults to true

💡 Result:

For actions/checkout, the persist-credentials input defaults to true (i.e., the token is persisted locally) unless you explicitly set persist-credentials: false.[1][2] The action documentation states that “The auth token is persisted in the local git config… Set persist-credentials: false to opt-out.”[1] Security discussions in the actions/checkout issue tracker also describe the parameter’s default as true and that you must explicitly pass persist-credentials: false to prevent persistence.[2]

Citations:


🌐 Web query:

"persist-credentials" actions/checkout defaults to true

💡 Result:

Yes, the persist-credentials input for the actions/checkout GitHub Action defaults to true [1][2][3]. When this setting is enabled (the default), the action configures the local Git environment with the authentication token (such as the GITHUB_TOKEN) used to fetch the repository [1][2][3]. This allows subsequent steps in the same job to execute authenticated Git commands—such as git push or git pull—without requiring further manual configuration [1][3]. The token is removed from the local Git configuration during the post-job cleanup phase [1][2][3]. Security practitioners often recommend setting persist-credentials to false in workflows that do not require Git write access (e.g., build or test jobs) to adhere to the principle of least privilege [4][5][6]. This prevents the authentication token from being available to other steps in the job, which can reduce the impact if a subsequent step is compromised [4][6]. To disable this behavior, use the following syntax in your workflow file [5]: - uses: actions/checkout@v4 with: persist-credentials: false

Citations:


🌐 Web query:

actions/checkout@v4 persist-credentials documentation default

💡 Result:

In the actions/checkout@v4 action, the default value for the persist-credentials input is true [1][2][3]. When this setting is enabled (the default), the action configures the repository's local git configuration to include the authentication token or SSH key used for the checkout [1][4]. This allows subsequent steps in your workflow to run authenticated git commands (such as git push) without needing to manually re-configure credentials [1][5]. The token is automatically removed during the post-job cleanup phase [1][2][4]. If you wish to prevent this behavior—for example, to enhance security by ensuring the credentials are not available to subsequent steps in the workflow—you must explicitly set persist-credentials to false in your workflow YAML [1][5]: steps: - uses: actions/checkout@v4 with: persist-credentials: false

Citations:


Pin GitHub Actions to commit SHAs and set checkout token persistence explicitly.

In .github/workflows/ci.yml, the following steps still use major tags instead of commit SHAs: actions/checkout@v4 (line 63), actions/setup-node@v4 (line 66), pnpm/action-setup@v4 (line 71), actions/cache@v4 (line 76), and actions/upload-artifact@v4 (line 108). This weakens supply-chain guarantees.

Also, actions/checkout@v4 (line 63) does not set with: persist-credentials: false; persist-credentials defaults to true, so credentials are persisted locally in git config unless opted out.

🧰 Tools
🪛 zizmor (1.25.2)

[warning] 62-63: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 63-63: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 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 @.github/workflows/ci.yml at line 63, The workflow currently uses floating
major tags and leaves checkout credentials persisted; update the specified
actions to pinned commit SHAs and explicitly disable persisted credentials for
checkout: replace uses: actions/checkout@v4 with a pinned commit SHA and add
with: persist-credentials: false to the checkout step, and similarly pin
actions/setup-node@v4, pnpm/action-setup@v4, actions/cache@v4, and
actions/upload-artifact@v4 to their corresponding commit SHAs (use the official
repository commit SHAs for each action) so the CI references immutable versions
and credentials are not stored in git config.


- name: Setup Node.js 24.x
- name: Setup Node.js 22.x
uses: actions/setup-node@v4
with:
node-version: 24.x
node-version: 22.x

- name: Setup pnpm
uses: pnpm/action-setup@v4
Expand Down Expand Up @@ -92,7 +94,7 @@ jobs:
NEXT_PUBLIC_NATIVE_TOKEN_CONTRACT: ${{ secrets.NEXT_PUBLIC_NATIVE_TOKEN_CONTRACT || 'CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC' }}

- name: Install Playwright Chromium
run: pnpm exec playwright install chromium --with-deps
run: pnpm exec playwright install chromium

- name: Run E2E tests
run: pnpm run test:e2e
Expand Down
1 change: 1 addition & 0 deletions components/bounty-detail/bounty-detail-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ export function BountyDetailClient({ bountyId }: { bountyId: string }) {
getFullMilestoneData(bounty);
return (
<Model4MaintainerDashboard
bountyId={bountyId}
milestones={milestones}
contributors={contributorProgress}
/>
Expand Down
218 changes: 172 additions & 46 deletions components/bounty-detail/model4-maintainer-dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,22 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Textarea } from "@/components/ui/textarea";
import { toast } from "sonner";
import {
useReleasePayment,
useAdvanceContributor,
useRemoveContributor,
useSendMessage,
} from "@/hooks/use-bounty-application";
import {
ChevronRight,
UserMinus,
Expand All @@ -23,25 +39,86 @@ import {
} from "lucide-react";

interface Model4MaintainerDashboardProps {
bountyId: string;
milestones: Milestone[];
contributors: ContributorProgress[];
maxSlots?: number;
className?: string;
}

export function Model4MaintainerDashboard({
bountyId,
milestones,
contributors: initialContributors,
maxSlots = 5,
className,
}: Model4MaintainerDashboardProps) {
const [loadingAction, setLoadingAction] = React.useState<string | null>(null);
const releasePayment = useReleasePayment(bountyId);
const advanceContributor = useAdvanceContributor(bountyId);
const removeContributor = useRemoveContributor(bountyId);
const sendMessage = useSendMessage(bountyId);

const [selectedContributor, setSelectedContributor] =
React.useState<ContributorProgress | null>(null);
const [isSubmissionsOpen, setIsSubmissionsOpen] = React.useState(false);
const [isMessageOpen, setIsMessageOpen] = React.useState(false);
const [messageText, setMessageText] = React.useState("");

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

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.


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

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

const handleOpenSubmissions = (contributor: ContributorProgress) => {
setSelectedContributor(contributor);
setIsSubmissionsOpen(true);
};

const handleOpenMessage = (contributor: ContributorProgress) => {
setSelectedContributor(contributor);
setMessageText("");
setIsMessageOpen(true);
};

const handleAction = async (action: string, userName: string) => {
setLoadingAction(`${action}-${userName}`);
console.log(`[Coming soon] ${action} for ${userName}`);
await new Promise((r) => setTimeout(r, 1000));
setLoadingAction(null);
const handleSendMessage = () => {
if (!selectedContributor || !messageText.trim()) return;
sendMessage.mutate(
{ contributorId: selectedContributor.userId, message: messageText },
{
onSuccess: () => {
toast.success(`Message sent to ${selectedContributor.userName}`);
setIsMessageOpen(false);
},
},
);
};
Comment on lines +111 to 122

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.


return (
Expand Down Expand Up @@ -135,18 +212,12 @@ export function Model4MaintainerDashboard({
variant="ghost"
size="icon-sm"
className="text-gray-400 hover:text-white"
aria-label="Send message"
onClick={() =>
handleAction("Message", contributor.userName)
}
disabled={loadingAction !== null}
onClick={() => handleOpenMessage(contributor)}
>
<MessageSquare className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
Send Message [Coming soon]
</TooltipContent>
<TooltipContent>Send Message</TooltipContent>
</Tooltip>

<Tooltip>
Expand All @@ -155,15 +226,9 @@ export function Model4MaintainerDashboard({
variant="outline"
size="sm"
className="h-8 text-xs border-gray-700 hover:bg-gray-800"
onClick={() =>
handleAction(
"View Submissions",
contributor.userName,
)
}
disabled={loadingAction !== null}
onClick={() => handleOpenSubmissions(contributor)}
>
View Submissions [Coming soon]
View Submissions
</Button>
</TooltipTrigger>
<TooltipContent>Review work</TooltipContent>
Expand All @@ -174,21 +239,21 @@ export function Model4MaintainerDashboard({
<Button
size="sm"
className="h-8 text-xs bg-emerald-500/10 text-emerald-400 hover:bg-emerald-500/20 border border-emerald-500/20 font-bold"
onClick={() =>
handleAction(
"Release Payment",
contributor.userName,
)
onClick={() => handleReleasePayment(contributor)}
disabled={
releasePayment.isPending &&
releasePayment.variables?.contributorId ===
contributor.userId
}
disabled={loadingAction !== null}
>
{loadingAction ===
`Release Payment-${contributor.userName}` ? (
{releasePayment.isPending &&
releasePayment.variables?.contributorId ===
contributor.userId ? (
<Loader2 className="size-3 mr-1.5 animate-spin" />
) : (
<Coins className="size-3 mr-1.5" />
)}
Release Payment [Coming soon]
Release Payment
</Button>
</TooltipTrigger>
<TooltipContent>Pay for milestone</TooltipContent>
Expand All @@ -200,18 +265,20 @@ export function Model4MaintainerDashboard({
size="sm"
variant="secondary"
className="h-8 text-xs font-bold"
onClick={() =>
handleAction("Advance", contributor.userName)
onClick={() => handleAdvance(contributor)}
disabled={
advanceContributor.isPending &&
advanceContributor.variables?.contributorId ===
contributor.userId
}
disabled={loadingAction !== null}
>
{loadingAction ===
`Advance-${contributor.userName}` ? (
{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" />
</>
)}
Comment on lines +275 to 283

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.

</Button>
Expand All @@ -225,18 +292,23 @@ export function Model4MaintainerDashboard({
variant="ghost"
size="icon-sm"
className="text-red-400/50 hover:text-red-400 hover:bg-red-400/10"
aria-label="Remove from slot"
onClick={() =>
handleAction("Remove", contributor.userName)
onClick={() => handleRemove(contributor)}
disabled={
removeContributor.isPending &&
removeContributor.variables?.contributorId ===
contributor.userId
}
disabled={loadingAction !== null}
>
<UserMinus className="size-4" />
{removeContributor.isPending &&
removeContributor.variables?.contributorId ===
contributor.userId ? (
<Loader2 className="size-4 animate-spin" />
) : (
<UserMinus className="size-4" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>
Remove from slot [Coming soon]
</TooltipContent>
<TooltipContent>Remove from slot</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
Expand Down Expand Up @@ -273,6 +345,60 @@ export function Model4MaintainerDashboard({
</Tooltip>
</TooltipProvider>
</div>

{/* Modals */}
<Dialog open={isSubmissionsOpen} onOpenChange={setIsSubmissionsOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>
Submissions for {selectedContributor?.userName}
</DialogTitle>
<DialogDescription>
Review the submitted work from this contributor.
</DialogDescription>
</DialogHeader>
<div className="py-6 text-center text-gray-400">
No submissions found for this contributor.
</div>
<DialogFooter>
<Button onClick={() => setIsSubmissionsOpen(false)}>Close</Button>
</DialogFooter>
</DialogContent>
</Dialog>

<Dialog open={isMessageOpen} onOpenChange={setIsMessageOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Message {selectedContributor?.userName}</DialogTitle>
<DialogDescription>
Send a message directly to this contributor regarding their
application.
</DialogDescription>
</DialogHeader>
<div className="py-4">
<Textarea
placeholder="Type your message here..."
value={messageText}
onChange={(e) => setMessageText(e.target.value)}
className="min-h-[100px]"
/>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsMessageOpen(false)}>
Cancel
</Button>
<Button
onClick={handleSendMessage}
disabled={sendMessage.isPending || !messageText.trim()}
>
{sendMessage.isPending && (
<Loader2 className="size-3 mr-1.5 animate-spin" />
)}
Send Message
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</CardContent>
</Card>
);
Expand Down
Loading
Loading