"use client"; export const dynamic = "force-dynamic"; import { useMemo, useState } from "react"; import Link from "next/link"; import { useAuth } from "@/auth/clerk"; import { type ColumnDef, flexRender, getCoreRowModel, useReactTable, } from "@tanstack/react-table"; import { useQueryClient } from "@tanstack/react-query"; import { ApiError } from "@/api/mutator"; import { type listBoardsApiV1BoardsGetResponse, getListBoardsApiV1BoardsGetQueryKey, useDeleteBoardApiV1BoardsBoardIdDelete, useListBoardsApiV1BoardsGet, } from "@/api/generated/boards/boards"; import { type listBoardGroupsApiV1BoardGroupsGetResponse, useListBoardGroupsApiV1BoardGroupsGet, } from "@/api/generated/board-groups/board-groups"; import { formatTimestamp } from "@/lib/formatters"; import { useOrganizationMembership } from "@/lib/use-organization-membership"; import type { BoardGroupRead, BoardRead } from "@/api/generated/model"; import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout"; import { Button, buttonVariants } from "@/components/ui/button"; import { ConfirmActionDialog } from "@/components/ui/confirm-action-dialog"; import { TableEmptyStateRow, TableLoadingRow } from "@/components/ui/table-state"; const compactId = (value: string) => value.length > 8 ? `${value.slice(0, 8)}…` : value; export default function BoardsPage() { const { isSignedIn } = useAuth(); const queryClient = useQueryClient(); const { isAdmin } = useOrganizationMembership(isSignedIn); const [deleteTarget, setDeleteTarget] = useState(null); const boardsKey = getListBoardsApiV1BoardsGetQueryKey(); const boardsQuery = useListBoardsApiV1BoardsGet< listBoardsApiV1BoardsGetResponse, ApiError >(undefined, { query: { enabled: Boolean(isSignedIn), refetchInterval: 30_000, refetchOnMount: "always", }, }); const groupsQuery = useListBoardGroupsApiV1BoardGroupsGet< listBoardGroupsApiV1BoardGroupsGetResponse, ApiError >( { limit: 200 }, { query: { enabled: Boolean(isSignedIn), refetchInterval: 30_000, refetchOnMount: "always", }, }, ); const boards = useMemo( () => boardsQuery.data?.status === 200 ? (boardsQuery.data.data.items ?? []) : [], [boardsQuery.data], ); const groups = useMemo(() => { if (groupsQuery.data?.status !== 200) return []; return groupsQuery.data.data.items ?? []; }, [groupsQuery.data]); const groupById = useMemo(() => { const map = new Map(); for (const group of groups) { map.set(group.id, group); } return map; }, [groups]); const deleteMutation = useDeleteBoardApiV1BoardsBoardIdDelete< ApiError, { previous?: listBoardsApiV1BoardsGetResponse } >( { mutation: { onMutate: async ({ boardId }) => { await queryClient.cancelQueries({ queryKey: boardsKey }); const previous = queryClient.getQueryData( boardsKey, ); if (previous && previous.status === 200) { const nextItems = previous.data.items.filter( (board) => board.id !== boardId, ); const removedCount = previous.data.items.length - nextItems.length; queryClient.setQueryData( boardsKey, { ...previous, data: { ...previous.data, items: nextItems, total: Math.max(0, previous.data.total - removedCount), }, }, ); } return { previous }; }, onError: (_error, _board, context) => { if (context?.previous) { queryClient.setQueryData(boardsKey, context.previous); } }, onSuccess: () => { setDeleteTarget(null); }, onSettled: () => { queryClient.invalidateQueries({ queryKey: boardsKey }); }, }, }, queryClient, ); const handleDelete = () => { if (!deleteTarget) return; deleteMutation.mutate({ boardId: deleteTarget.id }); }; const columns = useMemo[]>( () => [ { accessorKey: "name", header: "Board", cell: ({ row }) => (

{row.original.name}

), }, { id: "group", header: "Group", cell: ({ row }) => { const groupId = row.original.board_group_id; if (!groupId) { return ; } const group = groupById.get(groupId); const label = group?.name ?? compactId(groupId); const title = group?.name ?? groupId; return ( {label} ); }, }, { accessorKey: "updated_at", header: "Updated", cell: ({ row }) => ( {formatTimestamp(row.original.updated_at)} ), }, { id: "actions", header: "", cell: ({ row }) => (
Edit
), }, ], [groupById], ); // eslint-disable-next-line react-hooks/incompatible-library const table = useReactTable({ data: boards, columns, getCoreRowModel: getCoreRowModel(), }); return ( <> 0 && isAdmin ? ( Create board ) : null } stickyHeader >
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( ))} ))} {boardsQuery.isLoading ? ( ) : table.getRowModel().rows.length ? ( table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => ( ))} )) ) : ( } title="No boards yet" description="Create your first board to start routing tasks and monitoring work across agents." actionHref="/boards/new" actionLabel="Create your first board" /> )}
{header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext(), )}
{flexRender( cell.column.columnDef.cell, cell.getContext(), )}
{boardsQuery.error ? (

{boardsQuery.error.message}

) : null}
{ if (!open) { setDeleteTarget(null); } }} ariaLabel="Delete board" title="Delete board" description={ <> This will remove {deleteTarget?.name}. This action cannot be undone. } errorMessage={deleteMutation.error?.message} onConfirm={handleDelete} isConfirming={deleteMutation.isPending} /> ); }