fix(web): board layout, column config, and 501 error handling

- Forward route handle to React Router so wide:true works on /runs
- Switch board view to CSS grid for full-width columns
- Remove Verify column, rename Merge to Complete
- Use apiJsonOrNull in workflows/workflow-detail loaders to handle 501

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-04-15 07:48:50 -04:00
parent d42d175408
commit 549d85aaa7
No known key found for this signature in database
7 changed files with 105 additions and 93 deletions

View file

@ -35,6 +35,7 @@ type RouteModule = {
default: React.ComponentType<any>;
loader?: RouteObject["loader"];
action?: RouteObject["action"];
handle?: RouteObject["handle"];
ErrorBoundary?: React.ComponentType<any>;
};
@ -56,6 +57,7 @@ function route(
path,
loader: module.loader,
action: module.action,
handle: module.handle,
Component: withRouteModule(module),
ErrorBoundary: module.ErrorBoundary,
...extra,
@ -67,6 +69,7 @@ function indexRoute(module: RouteModule): RouteObject {
index: true,
loader: module.loader,
action: module.action,
handle: module.handle,
Component: withRouteModule(module),
ErrorBoundary: module.ErrorBoundary,
};

View file

@ -37,8 +37,7 @@ const columnConfig: {
}[] = [
{ id: "working", name: "Working", accent: "bg-teal-500", iconColor: "text-teal-500", iconType: "branch", actions: ["Watch", "Steer"] },
{ id: "pending", name: "Pending", accent: "bg-amber", iconColor: "text-amber", iconType: "branch", actions: ["Answer Question"] },
{ id: "review", name: "Verify", accent: "bg-mint", iconColor: "text-mint", iconType: "pr", actions: ["Resolve"] },
{ id: "merge", name: "Merge", accent: "bg-teal-300", iconColor: "text-teal-300", iconType: "pr", actions: ["Merge"] },
{ id: "merge", name: "Complete", accent: "bg-teal-300", iconColor: "text-teal-300", iconType: "pr", actions: ["Merge"] },
];
export async function loader({ request }: any) {
@ -395,7 +394,7 @@ type Column = {
function BoardColumn({ column }: { column: Column }) {
const Icon = iconMap[column.iconType];
return (
<div className="flex min-w-[280px] flex-1 flex-col">
<div className="flex min-w-0 flex-col">
<div className="mb-4 flex items-center gap-3">
<div className={`h-2.5 w-2.5 rounded-full ${column.accent}`} />
<h3 className="text-sm font-semibold tracking-wide text-fg-2">
@ -593,7 +592,7 @@ export default function Runs({ loaderData }: any) {
</div>
{view === "columns" ? (
<div className="flex gap-5 overflow-x-auto pb-4">
<div className="grid grid-cols-3 gap-5 pb-4">
{filteredColumns.map((col) => (
<BoardColumn key={col.id} column={col} />
))}

View file

@ -1,6 +1,6 @@
import { ChevronRightIcon } from "@heroicons/react/20/solid";
import { Link, Outlet, useLocation, useParams } from "react-router";
import { apiJson } from "../api";
import { apiJsonOrNull } from "../api";
import type { RunSettings, WorkflowDetailResponse as ApiWorkflowDetail } from "../lib/workflow-api";
export interface WorkflowEntry {
@ -222,15 +222,24 @@ const tabs = [
export const handle = { hideHeader: true };
export async function loader({ request, params }: any) {
const apiWorkflow = await apiJson<ApiWorkflowDetail>(`/workflows/${params.name}`, { request });
const workflow: WorkflowEntry = {
name: apiWorkflow.name,
slug: apiWorkflow.slug,
description: apiWorkflow.description,
filename: apiWorkflow.filename,
settings: apiWorkflow.settings,
graph: apiWorkflow.graph,
};
const apiWorkflow = await apiJsonOrNull<ApiWorkflowDetail>(`/workflows/${params.name}`, { request });
const workflow: WorkflowEntry = apiWorkflow
? {
name: apiWorkflow.name,
slug: apiWorkflow.slug,
description: apiWorkflow.description,
filename: apiWorkflow.filename,
settings: apiWorkflow.settings,
graph: apiWorkflow.graph,
}
: workflowData[params.name] ?? {
name: params.name,
slug: params.name,
description: "",
filename: `${params.name}.fabro`,
settings: {},
graph: "",
};
return { workflow };
}

View file

@ -13,7 +13,7 @@ import {
WrenchIcon,
} from "@heroicons/react/24/outline";
import { Link } from "react-router";
import { apiJson } from "../api";
import { apiJsonOrNull } from "../api";
import { timeAgo, timeUntil } from "../lib/time";
import type { PaginatedWorkflowListResponse } from "../lib/workflow-api";
@ -105,7 +105,8 @@ interface WorkflowData {
}
export async function loader({ request }: any) {
const { data: apiWorkflows } = await apiJson<PaginatedWorkflowListResponse>("/workflows", { request });
const result = await apiJsonOrNull<PaginatedWorkflowListResponse>("/workflows", { request });
const apiWorkflows = result?.data ?? [];
const workflows: WorkflowData[] = apiWorkflows.map((w) => ({
name: w.name,
slug: w.slug,

File diff suppressed because one or more lines are too long

View file

@ -61,7 +61,7 @@
<script type="module" src="/assets/chunk-sadshphz.js"></script>
<script type="module" src="/assets/chunk-pmthkscp.js"></script>
<script type="module" src="/assets/chunk-v61ks9f7.js"></script>
<script type="module" src="/assets/entry-d9p0rp2v.js"></script>
<script type="module" src="/assets/entry-arhd9tkh.js"></script>
<script type="module" src="/assets/chunk-n1k68xa8.js"></script>
<script type="module" src="/assets/chunk-rsph5pvm.js"></script>
<script type="module" src="/assets/chunk-9t57pdty.js"></script>