diff --git a/apps/web/src/app/(app)/gastown/[townId]/merges/NeedsAttention.tsx b/apps/web/src/app/(app)/gastown/[townId]/merges/NeedsAttention.tsx index a7fc670317..cdd73bc7bf 100644 --- a/apps/web/src/app/(app)/gastown/[townId]/merges/NeedsAttention.tsx +++ b/apps/web/src/app/(app)/gastown/[townId]/merges/NeedsAttention.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useMemo, Fragment } from 'react'; +import { useState, useMemo, Fragment, useCallback } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useSession } from 'next-auth/react'; import { useGastownTRPC } from '@/lib/gastown/trpc'; @@ -15,7 +15,9 @@ import { Eye, GitBranch, GitMerge, + Loader2, RefreshCw, + X, XCircle, CheckCircle2, Clock, @@ -113,6 +115,8 @@ export function NeedsAttention({ }) { const session = useSession(); const isAdmin = session?.data?.isAdmin ?? false; + const trpc = useGastownTRPC(); + const queryClient = useQueryClient(); const totalCount = data.openPRs.length + data.failedReviews.length + data.stalePRs.length; // Tag each item with its category for rendering @@ -139,6 +143,39 @@ export function NeedsAttention({ return map; }, [allItems]); + const failedItems = useMemo( + () => allItems.filter(({ category }) => category === 'failed').map(({ item }) => item), + [allItems] + ); + + const [isDismissingAll, setIsDismissingAll] = useState(false); + const updateBeadMutation = useMutation(trpc.gastown.updateBead.mutationOptions({})); + + const dismissAllFailed = useCallback(async () => { + if (failedItems.length === 0) return; + setIsDismissingAll(true); + try { + await Promise.all( + failedItems.map(item => + updateBeadMutation.mutateAsync({ + rigId: item.mrBead.rig_id ?? '', + beadId: item.mrBead.bead_id, + status: 'closed', + }) + ) + ); + void queryClient.invalidateQueries({ + queryKey: trpc.gastown.getMergeQueueData.queryKey({ townId }), + }); + toast.success(`Dismissed ${failedItems.length} failed ${failedItems.length === 1 ? 'bead' : 'beads'}`); + } catch (err: unknown) { + const message = err instanceof Error ? err.message : 'Unknown error'; + toast.error(`Failed to dismiss all: ${message}`); + } finally { + setIsDismissingAll(false); + } + }, [failedItems, updateBeadMutation, queryClient, trpc, townId]); + if (totalCount === 0) { return (
@@ -150,6 +187,24 @@ export function NeedsAttention({ return (
+ {/* Dismiss all failed button */} + {failedItems.length > 0 && ( +
+ +
+ )} + {/* Convoy groups */} {convoyGroups.map(group => ( @@ -335,6 +390,18 @@ function AttentionItemRow({ }) ); + const dismissMutation = useMutation( + trpc.gastown.updateBead.mutationOptions({ + onSuccess: () => { + invalidateMergeQueue(); + toast.success('Bead dismissed'); + }, + onError: (err: { message: string }) => { + toast.error(`Failed to dismiss: ${err.message}`); + }, + }) + ); + // Fail bead mutation: use adminForceFailBead const failMutation = useMutation( trpc.gastown.adminForceFailBead.mutationOptions({ @@ -349,7 +416,7 @@ function AttentionItemRow({ }) ); - const isPending = retryMutation.isPending || failMutation.isPending; + const isPending = retryMutation.isPending || failMutation.isPending || dismissMutation.isPending; const handleConfirm = () => { if (!confirmAction) return; @@ -388,13 +455,12 @@ function AttentionItemRow({ + <> + + + )}