import { useEffect, useMemo, useState } from "react"; import { useSearchParams } from "react-router"; import { Archive, Check, Plus } from "lucide-react"; import { Button, Card, ContentGrid, DismissibleAlert, FormField, FormGrid, PageActionBar, PageLayout, StatePanel, StatusBadge, WorkspaceLayout, adminErrorMessage, usePlatformLanguage, useUnsavedDraftGuard, type ApiSettings } from "@govoplan/core-webui"; import { createSemanticEntry, fetchSemanticEntries, fetchSemanticSubjects, transitionSemanticEntry, updateSemanticEntry, type SemanticContent, type SemanticEntry, type SemanticSubjectDescriptor } from "../../api/docs"; const emptyContent = (): SemanticContent => ({ title: "", summary: "", body: "", meaning: "", intended_use: "", non_intended_use: "", examples: [], owner_account_id: null, steward_account_id: null, audience: [], classification: "internal", links: [] }); export default function SemanticDocumentationPage({ settings }: { settings: ApiSettings }) { const { language } = usePlatformLanguage(); const [searchParams, setSearchParams] = useSearchParams(); const [subjects, setSubjects] = useState([]); const [entries, setEntries] = useState([]); const [selectedSubject, setSelectedSubject] = useState(null); const [selectedEntry, setSelectedEntry] = useState(null); const [content, setContent] = useState(emptyContent); const [locale, setLocale] = useState(searchParams.get("locale") || language || "de"); const [reason, setReason] = useState("Document configured meaning"); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [error, setError] = useState(""); const [success, setSuccess] = useState(""); const baseline = selectedEntry?.content ?? emptyContent(); const dirty = Boolean(selectedSubject) && ( JSON.stringify(content) !== JSON.stringify(baseline) || (!selectedEntry && Boolean(content.title || content.body || content.meaning)) ); const valid = Boolean(selectedSubject && content.title.trim() && reason.trim()); useUnsavedDraftGuard({ dirty, onSave: save, onDiscard: resetDraft }); async function load() { setLoading(true); setError(""); try { const [nextSubjects, nextEntries] = await Promise.all([ fetchSemanticSubjects(settings), fetchSemanticEntries(settings, locale, true) ]); setSubjects(nextSubjects); setEntries(nextEntries); const requestedEntry = searchParams.get("entryId"); const entry = nextEntries.find((item) => item.id === requestedEntry) ?? selectedEntry; if (entry) { selectExisting(entry, nextSubjects); } else { const requestedSubject = nextSubjects.find((item) => ( item.reference.module_id === searchParams.get("module") && item.reference.subject_kind === searchParams.get("subjectKind") && item.reference.subject_id === searchParams.get("subjectId") && (item.route_anchor ?? "") === (searchParams.get("routeAnchor") ?? "") )); if (requestedSubject) selectNew(requestedSubject, nextEntries); } } catch (reason) { setError(adminErrorMessage(reason)); } finally { setLoading(false); } } useEffect(() => { void load(); }, [settings.apiBaseUrl, settings.apiKey, settings.accessToken]); async function save(): Promise { if (!selectedSubject || !valid) return false; setSaving(true); setError(""); try { const next = selectedEntry ? await updateSemanticEntry(settings, selectedEntry.id, { expected_revision: selectedEntry.current_revision, content, change_reason: reason }) : await createSemanticEntry(settings, { subject: selectedSubject.reference, locale, content, change_reason: reason }); setSelectedEntry(next); setContent(next.content); setEntries((current) => [next, ...current.filter((item) => item.id !== next.id)]); setSearchParams({ entryId: next.id, locale: next.locale }, { replace: true }); setSuccess(`Saved immutable revision ${next.current_revision}.`); return true; } catch (reason) { setError(adminErrorMessage(reason)); return false; } finally { setSaving(false); } } async function transition(kind: "publish" | "retire") { if (!selectedEntry || dirty) return; setSaving(true); setError(""); try { const next = await transitionSemanticEntry( settings, selectedEntry.id, kind, selectedEntry.current_revision, reason ); setSelectedEntry(next); setEntries((current) => [next, ...current.filter((item) => item.id !== next.id)]); setSuccess(kind === "publish" ? "Semantic documentation published." : "Semantic documentation retired."); } catch (reason) { setError(adminErrorMessage(reason)); } finally { setSaving(false); } } function selectExisting(entry: SemanticEntry, availableSubjects = subjects) { const subject = availableSubjects.find((item) => sameSubject(item.reference, entry.subject)); setSelectedEntry(entry); setSelectedSubject(subject ?? descriptorFromEntry(entry)); setContent(entry.content_redacted ? emptyContent() : entry.content); setLocale(entry.locale); setSearchParams({ entryId: entry.id, locale: entry.locale }, { replace: true }); } function selectNew(subject: SemanticSubjectDescriptor, availableEntries = entries) { const existing = availableEntries.find((entry) => entry.locale === locale && sameSubject(entry.subject, subject.reference)); if (existing) { selectExisting(existing); return; } setSelectedSubject(subject); setSelectedEntry(null); setContent({ ...emptyContent(), title: localized(subject.labels, locale) }); setSearchParams({ locale }, { replace: true }); } function resetDraft() { setContent(selectedEntry?.content ?? emptyContent()); } const sourceStatus = selectedEntry?.subject_resolution.availability ?? "available"; const sortedEntries = useMemo( () => [...entries].sort((left, right) => left.content.title.localeCompare(right.content.title)), [entries] ); return (
Documented subjects
Available configured subjects
)} > void load(), loading }} primaryActions={selectedEntry?.lifecycle_state === "draft" && !dirty ? ( ) : null} destructiveActions={selectedEntry && !["retired", "superseded"].includes(selectedEntry.lifecycle_state) && !dirty ? ( ) : null} discardAction={{ label: "Discard", onClick: resetDraft }} saveAction={{ label: selectedEntry ? "Save revision" : "Create entry", onClick: () => void save() }} /> )} > {error ? {error} : null} {!selectedSubject ? ( ) : ( } > setLocale(event.target.value)} maxLength={20} /> setContent({ ...content, title: event.target.value })} />