feat(dashboard): add bulk trigger UI for approved submissions

- Add checkbox column for selecting approved submissions (admin only)
- Add bulk actions toolbar with "Trigger All" button
- Integrate useBulkTriggerWorkflow hook in dashboard page
- Column is conditionally rendered only for admin users
This commit is contained in:
Thomas Camlong
2025-12-29 11:08:10 +01:00
parent f2fb70025f
commit bfbc1245d5
2 changed files with 104 additions and 3 deletions

View File

@@ -10,7 +10,7 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D
import { Label } from "@/components/ui/label"
import { Skeleton } from "@/components/ui/skeleton"
import { Textarea } from "@/components/ui/textarea"
import { useApproveSubmission, useAuth, useRejectSubmission, useSubmissions, useTriggerWorkflow } from "@/hooks/use-submissions"
import { useApproveSubmission, useAuth, useBulkTriggerWorkflow, useRejectSubmission, useSubmissions, useTriggerWorkflow } from "@/hooks/use-submissions"
export default function DashboardPage() {
// Fetch auth status
@@ -23,6 +23,7 @@ export default function DashboardPage() {
const approveMutation = useApproveSubmission()
const rejectMutation = useRejectSubmission()
const workflowMutation = useTriggerWorkflow()
const bulkWorkflowMutation = useBulkTriggerWorkflow()
// Track workflow URL for showing link after trigger
const [workflowUrl, setWorkflowUrl] = React.useState<string | undefined>()
@@ -58,6 +59,17 @@ export default function DashboardPage() {
)
}
const handleBulkTriggerWorkflow = (submissionIds: string[]) => {
bulkWorkflowMutation.mutate(
{ submissionIds },
{
onSuccess: (data) => {
setWorkflowUrl(data.workflowUrl)
},
},
)
}
const handleRejectSubmit = () => {
if (rejectingSubmissionId) {
rejectMutation.mutate(
@@ -172,9 +184,11 @@ export default function DashboardPage() {
onApprove={handleApprove}
onReject={handleReject}
onTriggerWorkflow={handleTriggerWorkflow}
onBulkTriggerWorkflow={handleBulkTriggerWorkflow}
isApproving={approveMutation.isPending}
isRejecting={rejectMutation.isPending}
isTriggeringWorkflow={workflowMutation.isPending}
isBulkTriggeringWorkflow={bulkWorkflowMutation.isPending}
workflowUrl={workflowUrl}
/>
</CardContent>

View File

@@ -4,6 +4,7 @@ import {
type ColumnDef,
type ColumnFiltersState,
type ExpandedState,
type RowSelectionState,
flexRender,
getCoreRowModel,
getExpandedRowModel,
@@ -14,11 +15,12 @@ import {
} from "@tanstack/react-table"
import dayjs from "dayjs"
import relativeTime from "dayjs/plugin/relativeTime"
import { ChevronDown, ChevronRight, Filter, ImageIcon, Search, SortDesc, X } from "lucide-react"
import { ChevronDown, ChevronRight, Filter, Github, ImageIcon, Search, SortDesc, X } from "lucide-react"
import * as React from "react"
import { SubmissionDetails } from "@/components/submission-details"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { Input } from "@/components/ui/input"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { UserDisplay } from "@/components/user-display"
@@ -54,9 +56,11 @@ interface SubmissionsDataTableProps {
onApprove: (id: string) => void
onReject: (id: string) => void
onTriggerWorkflow?: (id: string) => void
onBulkTriggerWorkflow?: (ids: string[]) => void
isApproving?: boolean
isRejecting?: boolean
isTriggeringWorkflow?: boolean
isBulkTriggeringWorkflow?: boolean
workflowUrl?: string
}
@@ -111,9 +115,11 @@ export function SubmissionsDataTable({
onApprove,
onReject,
onTriggerWorkflow,
onBulkTriggerWorkflow,
isApproving,
isRejecting,
isTriggeringWorkflow,
isBulkTriggeringWorkflow,
workflowUrl,
}: SubmissionsDataTableProps) {
const [sorting, setSorting] = React.useState<SortingState>([])
@@ -121,6 +127,7 @@ export function SubmissionsDataTable({
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
const [globalFilter, setGlobalFilter] = React.useState("")
const [userFilter, setUserFilter] = React.useState<{ userId: string; displayName: string } | null>(null)
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({})
// Handle row expansion - only one row can be expanded at a time
const handleRowToggle = React.useCallback((rowId: string, isExpanded: boolean) => {
@@ -148,6 +155,43 @@ export function SubmissionsDataTable({
const columns: ColumnDef<Submission>[] = React.useMemo(
() => [
...(isAdmin
? [
{
id: "select",
header: ({ table }: { table: any }) => {
const approvedRows = table.getRowModel().rows.filter((row: any) => row.original.status === "approved")
const selectedApprovedCount = approvedRows.filter((row: any) => row.getIsSelected()).length
const allApprovedSelected = approvedRows.length > 0 && selectedApprovedCount === approvedRows.length
return approvedRows.length > 0 ? (
<Checkbox
checked={allApprovedSelected}
onCheckedChange={(value: boolean) => {
approvedRows.forEach((row: any) => row.toggleSelected(!!value))
}}
aria-label="Select all approved"
className="translate-y-[2px]"
/>
) : null
},
cell: ({ row }: { row: any }) => {
const isApproved = row.original.status === "approved"
return isApproved ? (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value: boolean) => row.toggleSelected(!!value)}
onClick={(e: React.MouseEvent) => e.stopPropagation()}
aria-label="Select row"
className="translate-y-[2px]"
/>
) : null
},
enableSorting: false,
enableHiding: false,
} as ColumnDef<Submission>,
]
: []),
{
id: "expander",
header: () => null,
@@ -291,7 +335,7 @@ export function SubmissionsDataTable({
},
},
],
[handleRowToggle, handleUserFilter, userFilter],
[handleRowToggle, handleUserFilter, userFilter, isAdmin],
)
const table = useReactTable({
@@ -305,11 +349,15 @@ export function SubmissionsDataTable({
onExpandedChange: setExpanded,
onColumnFiltersChange: setColumnFilters,
onGlobalFilterChange: setGlobalFilter,
onRowSelectionChange: setRowSelection,
enableRowSelection: (row) => row.original.status === "approved",
getRowId: (row) => row.id,
state: {
sorting,
expanded,
columnFilters,
globalFilter,
rowSelection,
},
getRowCanExpand: () => true,
globalFilterFn: (row, _columnId, value) => {
@@ -328,6 +376,17 @@ export function SubmissionsDataTable({
},
})
const selectedSubmissionIds = React.useMemo(() => {
return Object.keys(rowSelection).filter((id) => rowSelection[id])
}, [rowSelection])
const handleBulkTrigger = () => {
if (onBulkTriggerWorkflow && selectedSubmissionIds.length > 0) {
onBulkTriggerWorkflow(selectedSubmissionIds)
setRowSelection({})
}
}
return (
<div className="space-y-4">
{/* Search and Filters */}
@@ -364,6 +423,34 @@ export function SubmissionsDataTable({
)}
</div>
{/* Bulk Actions Toolbar */}
{isAdmin && selectedSubmissionIds.length > 0 && (
<div className="flex items-center justify-between p-3 bg-blue-500/10 border border-blue-500/20 rounded-lg">
<div className="flex items-center gap-2">
<Badge variant="secondary" className="bg-blue-500/20 text-blue-400">
{selectedSubmissionIds.length} selected
</Badge>
<span className="text-sm text-muted-foreground">
approved submission{selectedSubmissionIds.length > 1 ? "s" : ""} ready to process
</span>
</div>
<div className="flex items-center gap-2">
<Button variant="ghost" size="sm" onClick={() => setRowSelection({})}>
Clear selection
</Button>
<Button
size="sm"
onClick={handleBulkTrigger}
disabled={isBulkTriggeringWorkflow}
className="bg-blue-600 hover:bg-blue-700"
>
<Github className="w-4 h-4 mr-2" />
{isBulkTriggeringWorkflow ? "Triggering..." : `Trigger All (${selectedSubmissionIds.length})`}
</Button>
</div>
</div>
)}
{/* Table */}
<div className="rounded-md border">
<Table>