import { Download, Pencil, Plus, RefreshCw, Search, Upload } from "lucide-react"; import { useCallback, useEffect, useRef, useState, type FormEvent } from "react"; import { ActionBlockerHint, Button, Dialog, DocumentationHelpLink, DismissibleAlert, IconButton, FormField, LoadingIndicator, PageScrollViewport, StatusBadge, hasScope, useUnsavedChanges, useUnsavedDraftGuard, type PlatformRouteContext } from "@govoplan/core-webui"; import { assessFormDefinitionPackage, exportFormDefinitionPackage, importFormDefinitionPackage, listFormDefinitions, type FormDefinition, type FormPackageFragment } from "../../api/forms"; import FormDefinitionDialog from "./FormDefinitionDialog"; import { FORMS_DOCUMENTATION, FORMS_FIELD_DOCUMENTATION, FORMS_I18N } from "./interfacePatterns"; export default function FormsPage({ settings, auth }: PlatformRouteContext) { const [query, setQuery] = useState(""); const [submittedQuery, setSubmittedQuery] = useState(""); const [state, setState] = useState(""); const [items, setItems] = useState([]); const [total, setTotal] = useState(0); const [editing, setEditing] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [importing, setImporting] = useState(null); const [importReason, setImportReason] = useState(""); const [importAssessment, setImportAssessment] = useState(""); const [importBusy, setImportBusy] = useState(false); const { requestDiscard } = useUnsavedChanges(); const importInput = useRef(null); const canWrite = hasScope(auth, "forms:definition:write"); const canAdmin = hasScope(auth, "forms:definition:admin"); const tenantId = auth.active_tenant?.id ?? auth.tenant.id; const load = useCallback((signal?: AbortSignal) => { setLoading(true); setError(""); return listFormDefinitions(settings, { query: submittedQuery, states: state ? [state] : undefined, limit: 200 }, signal). then((result) => { setItems(result.definitions); setTotal(result.total); }). finally(() => setLoading(false)); }, [settings, state, submittedQuery]); useEffect(() => { const controller = new AbortController(); load(controller.signal).catch((reason) => { if ((reason as Error).name !== "AbortError") { setError(reason instanceof Error ? reason.message : "Form definitions could not be loaded."); } }); return () => controller.abort(); }, [load]); function search(event: FormEvent) { event.preventDefault(); setSubmittedQuery(query.trim()); } async function choosePackage(file?: File) { if (!file) return; setError(""); try { const fragment = JSON.parse(await file.text()) as FormPackageFragment; const assessment = await assessFormDefinitionPackage(settings, fragment); setImporting(fragment); setImportAssessment(assessment.outcome); setImportReason(""); } catch (reason) { setError(reason instanceof Error ? reason.message : "The Form package could not be read."); } finally { if (importInput.current) importInput.current.value = ""; } } async function importPackage(): Promise { if (!importing || !importReason.trim()) return false; setImportBusy(true); setError(""); try { await importFormDefinitionPackage(settings, importing, { changeReason: importReason.trim() }); setImporting(null); await load(); return true; } catch (reason) { setError(reason instanceof Error ? reason.message : "The Form package could not be imported."); return false; } finally { setImportBusy(false); } } useUnsavedDraftGuard({ dirty: Boolean(importing && importReason), onSave: importPackage, onDiscard: () => { setImporting(null); setImportReason(""); }, title: "i18n:govoplan-forms.unsaved_title", message: "i18n:govoplan-forms.unsaved_message" }); function closeImport() { if (importBusy) return; if (importing && importReason) requestDiscard(() => setImporting(null)); else setImporting(null); } async function downloadPackage(item: FormDefinition) { setError(""); try { const fragment = await exportFormDefinitionPackage(settings, item.reference.object_id, item.reference.version); const href = URL.createObjectURL(new Blob([JSON.stringify(fragment, null, 2)], { type: "application/json" })); const anchor = document.createElement("a"); anchor.href = href; anchor.download = `${item.key}-${item.reference.version}.govoplan-form.json`; anchor.click(); URL.revokeObjectURL(href); } catch (reason) { setError(reason instanceof Error ? reason.message : "The Form package could not be exported."); } } return (
{error && {error}} {!canWrite && } {loading && } {!loading && !error && items.length === 0 &&
No matching definitions.
} {!loading && items.length > 0 &&
{items.map((item) => { const mayRevise = canWrite && (item.publication_state === "draft" || canAdmin) && item.publication_state !== "retired"; return (
{item.title}{item.key} {item.fields.length} fields Revision {item.reference.version} } onClick={() => void downloadPackage(item)} /> } disabled={!mayRevise} disabledReason={!canWrite ? FORMS_I18N.writeReason : item.publication_state === "retired" || (item.publication_state === "published" && !canAdmin) ? FORMS_I18N.lifecycleReason : undefined} onClick={() => setEditing(item)} />
); })}
}
{editing && setEditing(null)} onSaved={() => { setEditing(null); void load().catch((reason) => setError(reason instanceof Error ? reason.message : "Definitions could not be reloaded.")); }} /> } }>

Assessment: {humanize(importAssessment)}. The source revision is retained as provenance and imported as a new local draft.

setImportReason(event.target.value)} />
); } function humanize(value: string): string { return value.replace(/[_:.-]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()); }