159 lines
6.3 KiB
TypeScript
159 lines
6.3 KiB
TypeScript
import { Link2, RefreshCw, UserRoundPlus } from "lucide-react";
|
|
import { useCallback, useEffect, useState } from "react";
|
|
import { ActionToolbar,
|
|
Button,
|
|
DocumentationHelpLink,
|
|
DismissibleAlert,
|
|
LoadingIndicator,
|
|
PageScrollViewport,
|
|
SelectionList,
|
|
SelectionListItem,
|
|
SelectionListItemContent,
|
|
StatePanel,
|
|
StatusBadge,
|
|
hasScope,
|
|
i18nMessage,
|
|
useGuardedNavigate,
|
|
usePlatformLanguage,
|
|
WorkspaceFrame,
|
|
type PlatformRouteContext
|
|
} from "@govoplan/core-webui";
|
|
import { listFormInstances, type FormInstance } from "../../api/formsRuntime";
|
|
import { FORMS_RUNTIME_DOCUMENTATION, FORMS_RUNTIME_I18N } from "./interfacePatterns";
|
|
import IntakeProfilesDialog from "./IntakeProfilesDialog";
|
|
import AssistedIntakeDialog from "./AssistedIntakeDialog";
|
|
|
|
|
|
const OPEN_STATUSES = ["started", "draft", "submitted", "validated", "needs_review"];
|
|
|
|
export default function FormsRuntimePage({ settings, auth }: PlatformRouteContext) {
|
|
const navigate = useGuardedNavigate();
|
|
const { language } = usePlatformLanguage();
|
|
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 [intakeOpen, setIntakeOpen] = useState(false);
|
|
const [assistedOpen, setAssistedOpen] = useState(false);
|
|
const canAdmin = hasScope(auth, "forms_runtime:workspace:admin");
|
|
const canAssist = hasScope(auth, "forms_runtime:submission:assist")
|
|
|| hasScope(auth, "forms_runtime:workspace:write");
|
|
|
|
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">
|
|
<WorkspaceFrame className="forms-runtime-shell" label="Forms runtime" interfaceId="forms-runtime.workspace" helpContextId="forms-runtime.page.workspace" helpModuleId="forms-runtime">
|
|
<ActionToolbar surface="panel-header" className="forms-runtime-toolbar">
|
|
<Button onClick={() => void load()} disabled={loading} disabledReason={loading ? FORMS_RUNTIME_I18N.loading : undefined}>
|
|
<RefreshCw size={16} aria-hidden="true" />
|
|
Refresh
|
|
</Button>
|
|
{canAdmin &&
|
|
<Button onClick={() => setIntakeOpen(true)}>
|
|
<Link2 size={16} aria-hidden="true" />
|
|
Public intake
|
|
</Button>
|
|
}
|
|
{canAssist &&
|
|
<Button onClick={() => setAssistedOpen(true)}>
|
|
<UserRoundPlus size={16} aria-hidden="true" />
|
|
Assisted intake
|
|
</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">{i18nMessage("i18n:govoplan-forms-runtime.form_count", { total })}</span>
|
|
<DocumentationHelpLink reference={FORMS_RUNTIME_DOCUMENTATION} />
|
|
</ActionToolbar>
|
|
<PageScrollViewport className="forms-runtime-list-viewport">
|
|
{error &&
|
|
<DismissibleAlert tone="danger" resetKey={error}>
|
|
{error}
|
|
</DismissibleAlert>
|
|
}
|
|
{loading && <LoadingIndicator label="Loading forms" />}
|
|
{!loading && !error && items.length === 0 &&
|
|
<StatePanel size="compact" description="No matching Forms." />
|
|
}
|
|
{!loading && items.length > 0 &&
|
|
<SelectionList variant="navigation" label="Forms">
|
|
{items.map((item) =>
|
|
<SelectionListItem
|
|
key={item.instance_id}
|
|
selected={false}
|
|
onClick={() => navigate(`/forms-runtime/${encodeURIComponent(item.instance_id)}`)}>
|
|
<SelectionListItemContent title={item.definition_ref.label ?? humanize(item.definition_ref.object_id)} description={`Revision ${item.definition_ref.version ?? "-"} · ${formatDateTime(item.recorded_at, language)}`} />
|
|
<StatusBadge status={isOpen(item.status) ? "active" : "inactive"} label={stateLabel(item.status)} />
|
|
</SelectionListItem>
|
|
)}
|
|
</SelectionList>
|
|
}
|
|
</PageScrollViewport>
|
|
</WorkspaceFrame>
|
|
<IntakeProfilesDialog open={intakeOpen} settings={settings} onClose={() => setIntakeOpen(false)} />
|
|
<AssistedIntakeDialog
|
|
open={assistedOpen}
|
|
settings={settings}
|
|
language={language}
|
|
onClose={() => setAssistedOpen(false)}
|
|
onStarted={(instanceId) => {
|
|
setAssistedOpen(false);
|
|
navigate(`/forms-runtime/${encodeURIComponent(instanceId)}`);
|
|
}}
|
|
/>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
function isOpen(status: string): boolean {
|
|
return OPEN_STATUSES.includes(status);
|
|
}
|
|
|
|
function formatDateTime(value: string, locale?: string): string {
|
|
return new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeStyle: "short" }).format(new Date(value));
|
|
}
|
|
|
|
function humanize(value: string): string {
|
|
return value.replace(/[_:.-]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
}
|
|
|
|
function stateLabel(value: string): string {
|
|
return `i18n:govoplan-forms-runtime.state_${value}`;
|
|
}
|