feat: implement definition-aware forms runtime
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
DismissibleAlert,
|
||||
LoadingIndicator,
|
||||
PageScrollViewport,
|
||||
StatusBadge,
|
||||
useGuardedNavigate,
|
||||
type PlatformRouteContext
|
||||
} from "@govoplan/core-webui";
|
||||
import { listFormInstances, type FormInstance } from "../../api/formsRuntime";
|
||||
|
||||
|
||||
const OPEN_STATUSES = ["started", "draft", "submitted", "validated", "needs_review"];
|
||||
|
||||
export default function FormsRuntimePage({ settings }: PlatformRouteContext) {
|
||||
const navigate = useGuardedNavigate();
|
||||
const [items, setItems] = useState<FormInstance[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [status, setStatus] = useState("open");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const load = useCallback((signal?: AbortSignal) => {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
return listFormInstances(settings, {
|
||||
statuses: status === "open" ? OPEN_STATUSES : status ? [status] : undefined,
|
||||
limit: 200
|
||||
}, signal).
|
||||
then((result) => {
|
||||
setItems(result.instances);
|
||||
setTotal(result.total);
|
||||
}).
|
||||
finally(() => setLoading(false));
|
||||
}, [settings, status]);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
load(controller.signal).catch((reason) => {
|
||||
if ((reason as Error).name !== "AbortError") {
|
||||
setError(reason instanceof Error ? reason.message : "Forms could not be loaded.");
|
||||
}
|
||||
});
|
||||
return () => controller.abort();
|
||||
}, [load]);
|
||||
|
||||
return (
|
||||
<main className="forms-runtime-page">
|
||||
<div className="forms-runtime-shell">
|
||||
<div className="forms-runtime-toolbar">
|
||||
<Button onClick={() => void load()} disabled={loading}>
|
||||
<RefreshCw size={16} aria-hidden="true" />
|
||||
Refresh
|
||||
</Button>
|
||||
<label>
|
||||
<span>Status</span>
|
||||
<select value={status} onChange={(event) => setStatus(event.target.value)}>
|
||||
<option value="open">Open</option>
|
||||
<option value="">All</option>
|
||||
<option value="draft">Draft</option>
|
||||
<option value="submitted">Submitted</option>
|
||||
<option value="needs_review">Needs review</option>
|
||||
<option value="accepted">Accepted</option>
|
||||
<option value="rejected">Rejected</option>
|
||||
<option value="handed_off">Handed off</option>
|
||||
<option value="archived">Archived</option>
|
||||
</select>
|
||||
</label>
|
||||
<span className="forms-runtime-count">{total} forms</span>
|
||||
</div>
|
||||
<PageScrollViewport className="forms-runtime-list-viewport">
|
||||
{error &&
|
||||
<DismissibleAlert tone="danger" resetKey={error}>
|
||||
{error}
|
||||
</DismissibleAlert>
|
||||
}
|
||||
{loading && <LoadingIndicator label="Loading forms" />}
|
||||
{!loading && !error && items.length === 0 &&
|
||||
<div className="forms-runtime-empty">No matching Forms.</div>
|
||||
}
|
||||
{!loading && items.length > 0 &&
|
||||
<div className="forms-runtime-list" role="list">
|
||||
{items.map((item) =>
|
||||
<button
|
||||
type="button"
|
||||
role="listitem"
|
||||
className="forms-runtime-row"
|
||||
key={item.instance_id}
|
||||
onClick={() => navigate(`/forms-runtime/${encodeURIComponent(item.instance_id)}`)}>
|
||||
<span className="forms-runtime-row-main">
|
||||
<strong>{item.definition_ref.label ?? humanize(item.definition_ref.object_id)}</strong>
|
||||
<span>Revision {item.definition_ref.version ?? "-"}</span>
|
||||
</span>
|
||||
<span>{formatDateTime(item.recorded_at)}</span>
|
||||
<StatusBadge status={isOpen(item.status) ? "active" : "inactive"} label={humanize(item.status)} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
</PageScrollViewport>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function isOpen(status: string): boolean {
|
||||
return OPEN_STATUSES.includes(status);
|
||||
}
|
||||
|
||||
function formatDateTime(value: string): string {
|
||||
return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(new Date(value));
|
||||
}
|
||||
|
||||
function humanize(value: string): string {
|
||||
return value.replace(/[_:.-]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
|
||||
}
|
||||
Reference in New Issue
Block a user