mirror of
https://github.com/homarr-labs/dashboard-icons.git
synced 2026-01-13 00:27:20 +08:00
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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user