import { Archive, BookOpen, FilePlus2, History, Link2, MessageSquarePlus, Pencil, Plus, Search, Send, Settings2, Upload } from "lucide-react"; import { useEffect, useMemo, useState, type FormEvent } from "react"; import { Button, ConfirmDialog, Dialog, DocumentationHelpLink, DismissibleAlert, FieldLabel, FilterBar, FormLayout, LoadingIndicator, PageScrollViewport, SelectionList, SelectionListItem, SelectionListItemContent, StatePanel, StatusBadge, WorkspaceActionBar, WorkspaceFrame, hasScope, type PlatformRouteContext } from "@govoplan/core-webui"; import { addWikiComment, archiveWikiPage, archiveWikiSpace, compareWikiRevisions, createWikiPage, createWikiSpace, getWikiAvailability, getWikiPage, listWikiComments, listWikiPages, listWikiRevisions, listWikiSpaces, publishWikiPage, updateWikiPage, updateWikiSpace, type WikiAvailability, type WikiComment, type WikiLink, type WikiPageRecord, type WikiRevision, type WikiSpace } from "../../api/wiki"; type ArchiveTarget = { kind: "page"; record: WikiPageRecord } | { kind: "space"; record: WikiSpace } | null; export default function WikiPage({ settings, auth }: PlatformRouteContext) { const [spaces, setSpaces] = useState([]); const [pages, setPages] = useState([]); const [selectedSpaceId, setSelectedSpaceId] = useState(""); const [selectedPageId, setSelectedPageId] = useState(""); const [query, setQuery] = useState(""); const [submittedQuery, setSubmittedQuery] = useState(""); const [revisions, setRevisions] = useState([]); const [comments, setComments] = useState([]); const [availability, setAvailability] = useState(null); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [error, setError] = useState(""); const [dialogError, setDialogError] = useState(""); const [spaceDialog, setSpaceDialog] = useState(null); const [pageDialog, setPageDialog] = useState(null); const [linkOpen, setLinkOpen] = useState(false); const [historyOpen, setHistoryOpen] = useState(false); const [compareText, setCompareText] = useState(""); const [archiveTarget, setArchiveTarget] = useState(null); const canWrite = hasScope(auth, "wiki:page:write"); const canComment = canWrite || hasScope(auth, "wiki:page:comment"); const canPublish = hasScope(auth, "wiki:page:publish") || hasScope(auth, "wiki:space:admin"); const canAdmin = hasScope(auth, "wiki:space:admin"); async function reload(signal?: AbortSignal, preferredSpaceId?: string) { setLoading(true); setError(""); try { const [spaceResult, integrationState] = await Promise.all([listWikiSpaces(settings, signal), getWikiAvailability(settings, signal)]); const activeSpace = preferredSpaceId && spaceResult.spaces.some((item) => item.space_id === preferredSpaceId) ? preferredSpaceId : selectedSpaceId && spaceResult.spaces.some((item) => item.space_id === selectedSpaceId) ? selectedSpaceId : spaceResult.spaces[0]?.space_id ?? ""; const pageResult = activeSpace ? await listWikiPages(settings, { spaceId: activeSpace, query: submittedQuery }, signal) : { pages: [], total: 0 }; setSpaces(spaceResult.spaces); setAvailability(integrationState); setSelectedSpaceId(activeSpace); setPages(pageResult.pages); setSelectedPageId((current) => pageResult.pages.some((item) => item.page_id === current) ? current : pageResult.pages[0]?.page_id ?? ""); } catch (reason) { if ((reason as Error).name !== "AbortError") setError(message(reason, "Wiki content could not be loaded.")); } finally { setLoading(false); } } useEffect(() => { const controller = new AbortController(); void reload(controller.signal); return () => controller.abort(); }, [settings, submittedQuery]); useEffect(() => { if (!selectedSpaceId) return; const controller = new AbortController(); setLoading(true); void listWikiPages(settings, { spaceId: selectedSpaceId, query: submittedQuery }, controller.signal).then((result) => { setPages(result.pages); setSelectedPageId((current) => result.pages.some((item) => item.page_id === current) ? current : result.pages[0]?.page_id ?? ""); }).catch((reason) => { if ((reason as Error).name !== "AbortError") setError(message(reason, "Wiki pages could not be loaded.")); }).finally(() => setLoading(false)); return () => controller.abort(); }, [settings, selectedSpaceId]); const selectedSpace = useMemo(() => spaces.find((item) => item.space_id === selectedSpaceId) ?? null, [spaces, selectedSpaceId]); const selectedPage = useMemo(() => pages.find((item) => item.page_id === selectedPageId) ?? null, [pages, selectedPageId]); useEffect(() => { if (!selectedPageId) { setRevisions([]); setComments([]); return; } const controller = new AbortController(); const requests: Promise[] = [getWikiPage(settings, selectedPageId, controller.signal).then((record) => setPages((current) => current.map((item) => item.page_id === record.page_id ? record : item))), listWikiComments(settings, selectedPageId, controller.signal).then((result) => setComments(result.comments))]; if (canWrite || canPublish) requests.push(listWikiRevisions(settings, selectedPageId, controller.signal).then((result) => setRevisions(result.revisions))); void Promise.all(requests).catch((reason) => { if ((reason as Error).name !== "AbortError") setError(message(reason, "Wiki page details could not be loaded.")); }); return () => controller.abort(); }, [settings, selectedPageId, canWrite, canPublish]); function submitSearch(event: FormEvent) { event.preventDefault(); setSubmittedQuery(query.trim()); } async function runAction(action: () => Promise, close?: () => void) { setSaving(true); setDialogError(""); try { const saved = await action(); close?.(); await reload(undefined, saved.space_id); setSelectedPageId(saved.page_id); } catch (reason) { setDialogError(message(reason, "The Wiki action could not be completed.")); } finally { setSaving(false); } } async function saveSpace(values: SpaceValues) { setSaving(true); setDialogError(""); try { const saved = spaceDialog !== "new" && spaceDialog ? await updateWikiSpace(settings, spaceDialog, { title: values.title, description: values.description, visibility: values.visibility, acl_tokens: tokens(values.aclTokens), publish_mode: values.publishMode }, values.changeReason) : await createWikiSpace(settings, { space_id: crypto.randomUUID(), space_key: values.spaceKey, title: values.title, visibility: values.visibility, acl_tokens: tokens(values.aclTokens), publish_mode: values.publishMode }, values.description); setSpaceDialog(null); await reload(undefined, saved.space_id); } catch (reason) { setDialogError(message(reason, "The Wiki space could not be saved.")); } finally { setSaving(false); } } async function savePage(values: PageValues) { if (!selectedSpace) return; if (pageDialog !== "new" && pageDialog) { await runAction(() => updateWikiPage(settings, pageDialog, { title: values.title, body: values.body, summary: values.summary, slug: values.slug, parent_page_id: values.parentPageId || null, inherits_access: values.inheritsAccess, visibility: values.visibility, acl_tokens: tokens(values.aclTokens), labels: tokens(values.labels), redirect_page_id: values.redirectPageId || null }, values.changeReason), () => setPageDialog(null)); return; } await runAction(() => createWikiPage(settings, { page_id: crypto.randomUUID(), space_id: selectedSpace.space_id, parent_page_id: values.parentPageId || null, slug: values.slug, title: values.title, body: values.body, summary: values.summary, inherits_access: values.inheritsAccess, visibility: values.inheritsAccess ? "inherit" : values.visibility, acl_tokens: values.inheritsAccess ? [] : tokens(values.aclTokens), labels: tokens(values.labels), links: [], redirect_page_id: values.redirectPageId || null }, values.changeReason), () => setPageDialog(null)); } async function addReference(link: WikiLink, reason: string) { if (!selectedPage) return; await runAction(() => updateWikiPage(settings, selectedPage, { links: [...selectedPage.links, link] }, reason), () => setLinkOpen(false)); } async function removeReference(linkId: string) { if (!selectedPage) return; await runAction(() => updateWikiPage(settings, selectedPage, { links: selectedPage.links.filter((item) => item.link_id !== linkId) }, "Removed a stale Wiki reference.")); } async function addComment(body: string) { if (!selectedPage) return; setSaving(true); setDialogError(""); try { await addWikiComment(settings, selectedPage, body); setComments((await listWikiComments(settings, selectedPage.page_id)).comments); } catch (reason) { setDialogError(message(reason, "The comment could not be added.")); } finally { setSaving(false); } } async function showComparison(from: number, to: number) { if (!selectedPage) return; setCompareText("Loading comparison..."); try { setCompareText((await compareWikiRevisions(settings, selectedPage.page_id, from, to)).diff || "The selected revisions have identical text."); } catch (reason) { setCompareText(message(reason, "Revisions could not be compared.")); } } async function confirmArchive() { const target = archiveTarget; if (!target) return; setSaving(true); setDialogError(""); try { if (target.kind === "page") { const saved = await archiveWikiPage(settings, target.record); setSelectedPageId(saved.page_id); } else await archiveWikiSpace(settings, target.record); setArchiveTarget(null); await reload(); } catch (reason) { setDialogError(message(reason, "Wiki content could not be archived.")); } finally { setSaving(false); } } return
void reload(), loading }} contextActions={<> {pages.length} pages } helpAction={} createAction={
{canAdmin && } {canWrite && selectedSpace && }
} /> {error && setError("")}>{error}} {dialogError && setDialogError("")}>{dialogError}} {availability && (!availability.search || !availability.files && !availability.dms) &&
{!availability.search && {availability.consequences.search}} {!availability.files && !availability.dms && {availability.consequences.files}}
}
{selectedSpace &&
Space

{selectedSpace.title}

{selectedSpace.description}

{canAdmin && }
} {loading && } {!loading && pages.length === 0 && } {pages.map((item) => setSelectedPageId(item.page_id)}>)} {canAdmin && selectedSpace &&
}
{selectedPage ? setPageDialog(selectedPage)} onPublish={() => void runAction(() => publishWikiPage(settings, selectedPage))} onAddLink={() => setLinkOpen(true)} onRemoveLink={removeReference} onHistory={() => { setCompareText(""); setHistoryOpen(true); }} onComment={addComment} onArchive={() => setArchiveTarget({ kind: "page", record: selectedPage })} /> : }
setSpaceDialog(null)} onSave={saveSpace} /> setPageDialog(null)} onSave={savePage} /> setLinkOpen(false)} onSave={addReference} /> setHistoryOpen(false)} onCompare={showComparison} /> setArchiveTarget(null)} onConfirm={() => void confirmArchive()} />
; } function WikiDetail({ record, revisions, comments, canWrite, canPublish, canComment, saving, onEdit, onPublish, onAddLink, onRemoveLink, onHistory, onComment, onArchive }: { record: WikiPageRecord; revisions: WikiRevision[]; comments: WikiComment[]; canWrite: boolean; canPublish: boolean; canComment: boolean; saving: boolean; onEdit: () => void; onPublish: () => void; onAddLink: () => void; onRemoveLink: (id: string) => Promise; onHistory: () => void; onComment: (body: string) => Promise; onArchive: () => void }) { const [comment, setComment] = useState(""); return
{record.path} · revision {record.revision}

{record.title}

{record.summary}

{canWrite && record.state !== "archived" && } {canWrite && record.state !== "archived" && } {(canWrite || canPublish) && } {canPublish && record.state === "draft" && }
{record.published_revision && record.state === "draft" &&
Readers and Search still receive published revision {record.published_revision} until this draft is published.
}
{record.body || This page has no body text yet.}
{record.labels.length > 0 &&
    {record.labels.map((label) =>
  • {label}
  • )}
}

References and attachments {record.links.length}

{record.links.length === 0 ?

No typed references are attached.

:
    {record.links.map((link) =>
  • {link.url ? {link.label || link.resource_id} : {link.label || link.resource_id}}{humanize(link.kind)} · {link.owner_module} · {link.resource_type}
    {canWrite && }
  • )}
}

Comments {comments.length}

{comments.length === 0 ?

No comments have been recorded.

:
    {comments.map((item) =>
  1. {item.body}

    {item.created_by || "System actor"} · revision {item.page_revision} · {formatDate(item.recorded_at)}
  2. )}
}{canComment && record.state !== "archived" &&
{ event.preventDefault(); const body = comment.trim(); if (!body) return; void onComment(body).then(() => setComment("")); }}>