984 lines
44 KiB
TypeScript
984 lines
44 KiB
TypeScript
import { useEffect, useMemo, useState } from "react";
|
|
import { Link, useLocation } from "react-router-dom";
|
|
import { ChevronDown, ChevronRight, RefreshCw } from "lucide-react";
|
|
import {
|
|
Button,
|
|
DataGrid,
|
|
DismissibleAlert,
|
|
ExplorerTree,
|
|
LoadingFrame,
|
|
PageTitle,
|
|
SegmentedControl,
|
|
StatusBadge,
|
|
adminErrorMessage,
|
|
useGuardedNavigate,
|
|
usePlatformLanguage,
|
|
type ApiSettings,
|
|
type DataGridColumn
|
|
} from "@govoplan/core-webui";
|
|
import {
|
|
fetchDocsContext,
|
|
type DocsContext,
|
|
type DocsDocumentationTopic,
|
|
type DocsModule,
|
|
type DocsOptionalModuleEvidence,
|
|
type DocsRoute,
|
|
type DocsSource
|
|
} from "../../api/docs";
|
|
|
|
type DocumentationType = "admin" | "user";
|
|
|
|
type DocsPageNode =
|
|
| {
|
|
id: string;
|
|
title: string;
|
|
kind: "topic";
|
|
topic: DocsDocumentationTopic;
|
|
}
|
|
| {
|
|
id: string;
|
|
title: string;
|
|
kind: "topic-list";
|
|
topics: DocsDocumentationTopic[];
|
|
emptyText: string;
|
|
}
|
|
| {
|
|
id: string;
|
|
title: string;
|
|
kind: "admin-reference";
|
|
};
|
|
|
|
type DocsTreeNode = {
|
|
id: string;
|
|
title: string;
|
|
page: DocsPageNode;
|
|
children: DocsTreeNode[];
|
|
};
|
|
|
|
type OutlineItem = {
|
|
id: string;
|
|
label: string;
|
|
};
|
|
|
|
export default function DocsPage({ settings }: { settings: ApiSettings }) {
|
|
const location = useLocation();
|
|
const navigate = useGuardedNavigate();
|
|
const { language } = usePlatformLanguage();
|
|
const [context, setContext] = useState<DocsContext | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState("");
|
|
const [documentationType, setDocumentationType] = useState<DocumentationType>(() => documentationTypeFromSearch(location.search));
|
|
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(() => new Set());
|
|
const locale = localeFromSearch(location.search) ?? language;
|
|
const adminDocs = documentationType === "admin";
|
|
|
|
async function load() {
|
|
setLoading(true);
|
|
setError("");
|
|
try {
|
|
setContext(await fetchDocsContext(settings, { documentationType, locale }));
|
|
} catch (err) {
|
|
setError(adminErrorMessage(err));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
const nextType = documentationTypeFromSearch(location.search);
|
|
setDocumentationType((current) => current === nextType ? current : nextType);
|
|
}, [location.search]);
|
|
|
|
useEffect(() => { void load(); }, [settings.apiBaseUrl, settings.apiKey, settings.accessToken, documentationType, locale]);
|
|
|
|
const treeNodes = useMemo(() => docsTreeNodes(context, adminDocs), [context, adminDocs]);
|
|
const pages = useMemo(() => flattenTreePages(treeNodes), [treeNodes]);
|
|
const selectedPage = selectedPageFromSearch(location.search, pages);
|
|
const topicById = useMemo(() => topicIndex(context), [context]);
|
|
const visibleRoutes = context?.layers.configured.routes ?? [];
|
|
const availableRoutes = context?.layers.available.routes ?? [];
|
|
const grantedPermissions = context?.layers.configured.permissions.filter((item) => item.granted) ?? [];
|
|
const moduleRows = useMemo(() => context?.layers.configured.modules ?? [], [context]);
|
|
const outlineItems = outlineForPage(selectedPage, adminDocs);
|
|
|
|
useEffect(() => {
|
|
setExpandedNodes(new Set(defaultExpandedNodeIds(treeNodes)));
|
|
}, [treeNodes]);
|
|
|
|
useEffect(() => {
|
|
if (!selectedPage) return;
|
|
const ancestorIds = ancestorNodeIdsForPage(treeNodes, selectedPage.id);
|
|
if (!ancestorIds.length) return;
|
|
setExpandedNodes((current) => {
|
|
const next = new Set(current);
|
|
for (const nodeId of ancestorIds) {
|
|
next.add(nodeId);
|
|
}
|
|
return next;
|
|
});
|
|
}, [treeNodes, selectedPage?.id]);
|
|
|
|
return (
|
|
<div className="workspace module-workspace docs-workspace">
|
|
<aside className="section-sidebar docs-outline" aria-label="i18n:govoplan-docs.documentation_outline.6f836b99">
|
|
<div className="docs-sidebar-header">
|
|
<div className="section-title">i18n:govoplan-docs.help_center.f3f3a34b</div>
|
|
<AudienceToggle
|
|
selected={documentationType}
|
|
onSelect={selectDocumentationType}
|
|
canViewAdmin={context?.actor.available_documentation_types.includes("admin") ?? documentationType === "admin"}
|
|
/>
|
|
</div>
|
|
<nav className="docs-tree" aria-label="i18n:govoplan-docs.documentation_outline.6f836b99">
|
|
<ExplorerTree
|
|
nodes={treeNodes}
|
|
getNodeId={(node) => node.id}
|
|
getNodeLabel={(node) => node.title}
|
|
getNodeChildren={(node) => node.children}
|
|
activeId={activeNodeIdForPage(treeNodes, selectedPage?.id ?? "")}
|
|
expandedIds={expandedNodes}
|
|
depth={0}
|
|
childrenBaseClassName="docs-tree-children"
|
|
nodeContainerClassName="docs-tree-node"
|
|
nodeWrapBaseClassName="docs-tree-row"
|
|
toggleBaseClassName="docs-tree-toggle"
|
|
nodeButtonBaseClassName="docs-tree-page"
|
|
getNodeWrapStyle={(_node, context) => ({ paddingLeft: `${context.depth * 14}px` })}
|
|
getNodeButtonClassName={(node) => selectedPage?.id === node.page.id ? "is-active" : ""}
|
|
renderToggleIcon={(_node, context) => context.hasChildren ? context.expanded ? <ChevronDown size={15} /> : <ChevronRight size={15} /> : <span className="docs-tree-toggle-placeholder" />}
|
|
renderNodeContent={(node) => node.title}
|
|
onToggle={(node) => toggleNode(node.id)}
|
|
onOpen={(node) => selectPage(node.page)}
|
|
/>
|
|
</nav>
|
|
</aside>
|
|
<section className="workspace-content docs-workspace-content">
|
|
<div className="content-pad workspace-data-page docs-page">
|
|
<div className="page-heading split workspace-heading">
|
|
<div>
|
|
<PageTitle loading={loading}>i18n:govoplan-docs.help_center.f3f3a34b</PageTitle>
|
|
<p>{adminDocs ? "i18n:govoplan-docs.technical_documentation_for_the_modules_and_conf.267e739e" : "i18n:govoplan-docs.guidance_for_the_functions_available_in_this_ins.723b7cad"}</p>
|
|
</div>
|
|
<div className="button-row compact-actions">
|
|
<Button onClick={() => void load()} disabled={loading}><RefreshCw size={16} /> i18n:govoplan-docs.reload.cce71553</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
|
|
|
|
<LoadingFrame loading={loading} label="i18n:govoplan-docs.loading_documentation_context.1c091645">
|
|
<div className="docs-content">
|
|
<main className="docs-page-main">
|
|
<SelectedPageContent
|
|
page={selectedPage}
|
|
adminDocs={adminDocs}
|
|
documentationType={documentationType}
|
|
topicById={topicById}
|
|
moduleRows={moduleRows}
|
|
visibleRoutes={visibleRoutes}
|
|
availableRoutes={availableRoutes}
|
|
grantedPermissions={grantedPermissions}
|
|
evidenceModules={context?.layers.evidence.optional_modules ?? []}
|
|
evidenceSources={context?.layers.evidence.sources ?? []}
|
|
/>
|
|
</main>
|
|
<PageOutline items={outlineItems} />
|
|
</div>
|
|
</LoadingFrame>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
|
|
function selectDocumentationType(type: DocumentationType) {
|
|
const params = new URLSearchParams(location.search);
|
|
params.set("type", type);
|
|
navigate(`${location.pathname}?${params.toString()}`, { replace: true });
|
|
}
|
|
|
|
function selectPage(page: DocsPageNode) {
|
|
const params = new URLSearchParams(location.search);
|
|
params.set("topic", page.id);
|
|
navigate(`${location.pathname}?${params.toString()}`, { replace: true });
|
|
}
|
|
|
|
function toggleNode(nodeId: string) {
|
|
setExpandedNodes((current) => {
|
|
const next = new Set(current);
|
|
if (next.has(nodeId)) {
|
|
next.delete(nodeId);
|
|
} else {
|
|
next.add(nodeId);
|
|
}
|
|
return next;
|
|
});
|
|
}
|
|
}
|
|
|
|
function AudienceToggle({ selected, onSelect, canViewAdmin }: { selected: DocumentationType; onSelect: (type: DocumentationType) => void; canViewAdmin: boolean }) {
|
|
const options: Array<{ id: DocumentationType; label: string }> = [
|
|
{ id: "user", label: "i18n:govoplan-docs.user_docs.1e38e8d3" }
|
|
];
|
|
if (canViewAdmin) {
|
|
options.unshift({ id: "admin", label: "i18n:govoplan-docs.admin_docs.bf504a56" });
|
|
}
|
|
return (
|
|
<SegmentedControl
|
|
className="docs-audience-toggle"
|
|
role="group"
|
|
size="equal"
|
|
width="fill"
|
|
ariaLabel="i18n:govoplan-docs.documentation_type.5a66690c"
|
|
value={selected}
|
|
onChange={onSelect}
|
|
options={options}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function PageOutline({ items }: { items: OutlineItem[] }) {
|
|
return (
|
|
<aside className="docs-page-outline-box" aria-label="i18n:govoplan-docs.page_outline.e5940949">
|
|
<h2>i18n:govoplan-docs.page_outline.e5940949</h2>
|
|
{items.length ?
|
|
<nav>
|
|
{items.map((item) => <a key={item.id} href={`#${item.id}`}>{item.label}</a>)}
|
|
</nav> :
|
|
<p className="muted">i18n:govoplan-docs.no_outline_items.3ea2842e</p>
|
|
}
|
|
</aside>
|
|
);
|
|
}
|
|
|
|
function SelectedPageContent({
|
|
page,
|
|
adminDocs,
|
|
documentationType,
|
|
topicById,
|
|
moduleRows,
|
|
visibleRoutes,
|
|
availableRoutes,
|
|
grantedPermissions,
|
|
evidenceModules,
|
|
evidenceSources
|
|
}: {
|
|
page: DocsPageNode | null;
|
|
adminDocs: boolean;
|
|
documentationType: DocumentationType;
|
|
topicById: Map<string, DocsDocumentationTopic>;
|
|
moduleRows: DocsModule[];
|
|
visibleRoutes: DocsRoute[];
|
|
availableRoutes: DocsRoute[];
|
|
grantedPermissions: Array<{ scope: string; label: string; category: string }>;
|
|
evidenceModules: DocsOptionalModuleEvidence[];
|
|
evidenceSources: DocsSource[];
|
|
}) {
|
|
if (!page) {
|
|
return (
|
|
<section className="docs-section">
|
|
<h2>i18n:govoplan-docs.help_center.f3f3a34b</h2>
|
|
<p className="muted">i18n:govoplan-docs.no_system_topics_found.d01a8068</p>
|
|
</section>
|
|
);
|
|
}
|
|
if (page.kind === "topic") {
|
|
return <TopicPage topic={page.topic} showTechnical={adminDocs} documentationType={documentationType} topicById={topicById} />;
|
|
}
|
|
if (page.kind === "admin-reference") {
|
|
return (
|
|
<section className="docs-section" id={page.id}>
|
|
<h2>{page.title}</h2>
|
|
<section id="docs-admin-modules" className="docs-reference-block">
|
|
<h3>i18n:govoplan-docs.modules.04e9462c</h3>
|
|
<ModuleTable modules={moduleRows} />
|
|
</section>
|
|
<section id="docs-admin-routes" className="docs-reference-block">
|
|
<h3>i18n:govoplan-docs.visible_routes.6c9b1605</h3>
|
|
<RouteTable routes={visibleRoutes} emptyText="i18n:govoplan-docs.no_visible_routes_found_for_this_actor.3feeaf0b" />
|
|
<h3>i18n:govoplan-docs.available_routes.c2635868</h3>
|
|
<RouteTable routes={availableRoutes} emptyText="i18n:govoplan-docs.no_hidden_installed_routes_found.22f62845" />
|
|
</section>
|
|
<section id="docs-admin-permissions" className="docs-reference-block">
|
|
<h3>i18n:govoplan-docs.granted_permissions.0a232e78</h3>
|
|
<PermissionList permissions={grantedPermissions} />
|
|
<EvidenceList modules={evidenceModules} sources={evidenceSources} />
|
|
</section>
|
|
</section>
|
|
);
|
|
}
|
|
return (
|
|
<section className="docs-section" id={page.id}>
|
|
<h2>{page.title}</h2>
|
|
<DocumentationTopicList topics={page.topics} emptyText={page.emptyText} showTechnical={adminDocs} documentationType={documentationType} topicById={topicById} />
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function TopicPage({ topic, showTechnical, documentationType, topicById }: { topic: DocsDocumentationTopic; showTechnical: boolean; documentationType: DocumentationType; topicById: Map<string, DocsDocumentationTopic> }) {
|
|
return (
|
|
<article className={`docs-topic docs-topic-page docs-topic-${topic.kind}`} id={topicAnchorId(topic)}>
|
|
<div className="docs-topic-meta">
|
|
<StatusBadge status={topic.active ? "success" : "warning"} label={topic.active ? topic.kind : topic.target_layer} />
|
|
<span className="muted">{topic.source_module_id}</span>
|
|
{showTechnical && <span className="muted">{topic.i18n_key} · {topic.translation_locale}</span>}
|
|
</div>
|
|
<h2>{topic.title}</h2>
|
|
{topic.summary && <p id={`${topicAnchorId(topic)}-summary`} className="docs-topic-summary">{topic.summary}</p>}
|
|
<div id={`${topicAnchorId(topic)}-details`}>
|
|
<BodyText value={topic.body} />
|
|
<TopicMetadata topic={topic} showTechnical={showTechnical} documentationType={documentationType} topicById={topicById} />
|
|
{!topic.active && <UnavailableDocumentationReason topic={topic} />}
|
|
</div>
|
|
{!!topic.unlocks.length && <p className="muted">i18n:govoplan-docs.unlocks.67d569db {topic.unlocks.join(" ")}</p>}
|
|
{!!topic.links.length && <TopicLinks topic={topic} />}
|
|
</article>
|
|
);
|
|
}
|
|
|
|
function DocumentationTopicList({ topics, emptyText, showTechnical = false, documentationType, topicById }: { topics: DocsDocumentationTopic[]; emptyText: string; showTechnical?: boolean; documentationType: DocumentationType; topicById: Map<string, DocsDocumentationTopic> }) {
|
|
if (!topics.length) return <p className="muted">{emptyText}</p>;
|
|
return (
|
|
<div className="docs-topic-list">
|
|
{topics.map((topic) =>
|
|
<article className={`docs-topic docs-topic-${topic.kind}`} id={topicAnchorId(topic)} key={`${topic.source_module_id}-${topic.id}`}>
|
|
<div className="docs-topic-meta">
|
|
<StatusBadge status={topic.active ? "success" : "warning"} label={topic.active ? topic.kind : topic.target_layer} />
|
|
<span className="muted">{topic.source_module_id}</span>
|
|
{showTechnical && <span className="muted">{topic.i18n_key} · {topic.translation_locale}</span>}
|
|
</div>
|
|
<h3>{topic.title}</h3>
|
|
{topic.summary && <p className="docs-topic-summary">{topic.summary}</p>}
|
|
<BodyText value={topic.body} />
|
|
<TopicMetadata topic={topic} showTechnical={showTechnical} documentationType={documentationType} topicById={topicById} />
|
|
{!topic.active && <UnavailableDocumentationReason topic={topic} />}
|
|
{!!topic.unlocks.length && <p className="muted">i18n:govoplan-docs.unlocks.67d569db {topic.unlocks.join(" ")}</p>}
|
|
{!!topic.links.length && <TopicLinks topic={topic} />}
|
|
</article>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function TopicMetadata({ topic, showTechnical, documentationType, topicById }: { topic: DocsDocumentationTopic; showTechnical: boolean; documentationType: DocumentationType; topicById: Map<string, DocsDocumentationTopic> }) {
|
|
const configuredDetails = <ConfiguredDetails topic={topic} />;
|
|
if (topic.kind === "workflow") return <>{configuredDetails}<WorkflowDetails topic={topic} documentationType={documentationType} topicById={topicById} /></>;
|
|
if (topic.kind === "reference") return <>{configuredDetails}<ReferenceDetails topic={topic} showTechnical={showTechnical} documentationType={documentationType} topicById={topicById} /></>;
|
|
if (topic.kind === "pattern") return <>{configuredDetails}<PatternDetails topic={topic} showTechnical={showTechnical} documentationType={documentationType} topicById={topicById} /></>;
|
|
return <>{configuredDetails}<RelatedTopics topic={topic} documentationType={documentationType} topicById={topicById} /></>;
|
|
}
|
|
|
|
function ConfiguredDetails({ topic }: { topic: DocsDocumentationTopic }) {
|
|
const currentConfiguration = metadataList(topic.metadata, "current_configuration");
|
|
const limitations = metadataList(topic.metadata, "limitations");
|
|
const constraints = metadataRecords(topic.metadata, "constraints");
|
|
const prefix = topicAnchorId(topic);
|
|
if (!currentConfiguration.length && !limitations.length && !constraints.length) return null;
|
|
return (
|
|
<div className="docs-topic-details">
|
|
{!!currentConfiguration.length && <DetailList id={`${prefix}-current-configuration`} title="i18n:govoplan-docs.this_system.b13a51ad" items={currentConfiguration} />}
|
|
{!!constraints.length && <ConstraintDetails id={`${prefix}-constraints`} constraints={constraints} />}
|
|
{!!limitations.length && <DetailList id={`${prefix}-limitations`} title="i18n:govoplan-docs.details.a6b3c45f" items={limitations} />}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function WorkflowDetails({ topic, documentationType, topicById }: { topic: DocsDocumentationTopic; documentationType: DocumentationType; topicById: Map<string, DocsDocumentationTopic> }) {
|
|
const prerequisites = metadataList(topic.metadata, "prerequisites");
|
|
const steps = metadataList(topic.metadata, "steps");
|
|
const outcome = metadataString(topic.metadata, "outcome");
|
|
const result = metadataString(topic.metadata, "result");
|
|
const verification = metadataString(topic.metadata, "verification");
|
|
const prefix = topicAnchorId(topic);
|
|
return (
|
|
<div className="docs-topic-details">
|
|
{outcome && <DetailBlock id={`${prefix}-outcome`} title="i18n:govoplan-docs.outcome.10172bd3" value={outcome} />}
|
|
{!!prerequisites.length && <DetailList id={`${prefix}-prerequisites`} title="i18n:govoplan-docs.prerequisites.fdf2407f" items={prerequisites} />}
|
|
{!!steps.length &&
|
|
<div className="docs-detail-block" id={`${prefix}-steps`}>
|
|
<h4>i18n:govoplan-docs.steps.6041435e</h4>
|
|
<ol>
|
|
{steps.map((step) => <li key={step}>{step}</li>)}
|
|
</ol>
|
|
</div>
|
|
}
|
|
{result && <DetailBlock id={`${prefix}-result`} title="i18n:govoplan-docs.result.1f4fbf43" value={result} />}
|
|
{verification && <DetailBlock id={`${prefix}-verification`} title="i18n:govoplan-docs.verification.4b5f10dd" value={verification} />}
|
|
<RelatedTopics topic={topic} documentationType={documentationType} topicById={topicById} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ConstraintDetails({ id, constraints }: { id: string; constraints: Record<string, unknown>[] }) {
|
|
return (
|
|
<div className="docs-detail-block" id={id}>
|
|
<h4>i18n:govoplan-docs.requirements.09a428f9</h4>
|
|
<dl className="detail-list compact">
|
|
{constraints.map((constraint, index) => {
|
|
const label = metadataString(constraint, "label");
|
|
const description = metadataString(constraint, "description");
|
|
const values = metadataList(constraint, "values");
|
|
return (
|
|
<div key={metadataString(constraint, "id") || `${label}-${index}`}>
|
|
<dt>{label}</dt>
|
|
<dd>
|
|
{description}
|
|
{!!values.length && <span className="muted block">{values.join(", ")}</span>}
|
|
</dd>
|
|
</div>
|
|
);
|
|
})}
|
|
</dl>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ReferenceDetails({ topic, showTechnical, documentationType, topicById }: { topic: DocsDocumentationTopic; showTechnical: boolean; documentationType: DocumentationType; topicById: Map<string, DocsDocumentationTopic> }) {
|
|
const route = metadataString(topic.metadata, "route");
|
|
const screen = metadataString(topic.metadata, "screen");
|
|
const section = metadataString(topic.metadata, "section");
|
|
const fields = metadataRecords(topic.metadata, "fields");
|
|
return (
|
|
<div className="docs-topic-details">
|
|
{(route || screen || section) &&
|
|
<dl className="detail-list compact">
|
|
{screen && <div><dt>i18n:govoplan-docs.screen.c4878ec4</dt><dd>{screen}</dd></div>}
|
|
{section && <div><dt>i18n:govoplan-docs.section.5e498158</dt><dd>{section}</dd></div>}
|
|
{showTechnical && route && <div><dt>i18n:govoplan-docs.route.4999528e</dt><dd>{route}</dd></div>}
|
|
</dl>
|
|
}
|
|
{!!fields.length && <ReferenceFieldTable id={`${topicAnchorId(topic)}-fields`} fields={fields} showTechnical={showTechnical} />}
|
|
<RelatedTopics topic={topic} documentationType={documentationType} topicById={topicById} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ReferenceFieldTable({ id, fields, showTechnical }: { id: string; fields: Record<string, unknown>[]; showTechnical: boolean }) {
|
|
const columns: DataGridColumn<Record<string, unknown>>[] = [
|
|
{ id: "field", header: "i18n:govoplan-docs.field.7558c082", width: "minmax(180px, .8fr)", minWidth: 160, resizable: true, sortable: true, filterable: true, value: (field) => metadataString(field, "label"), render: (field) => <strong>{metadataString(field, "label")}</strong> },
|
|
{ id: "meaning", header: "i18n:govoplan-docs.meaning.584d8aa0", width: "minmax(260px, 1.3fr)", minWidth: 220, resizable: true, filterable: true, value: (field) => [metadataString(field, "user_description"), metadataString(field, "admin_description"), metadataString(field, "provenance")].join(" "), render: (field) => <div>{metadataString(field, "user_description")}{showTechnical && metadataString(field, "admin_description") && <span className="muted block">{metadataString(field, "admin_description")}</span>}{showTechnical && metadataString(field, "provenance") && <span className="muted block">{metadataString(field, "provenance")}</span>}</div> },
|
|
...(showTechnical ? [
|
|
{ id: "api", header: "i18n:govoplan-docs.api_mapping.e969f6f7", width: "minmax(190px, .8fr)", minWidth: 170, resizable: true, filterable: true, value: (field: Record<string, unknown>) => `${metadataString(field, "api_path")} ${metadataString(field, "api_field")}`, render: (field: Record<string, unknown>) => <div>{metadataString(field, "api_path")}<span className="muted block">{metadataString(field, "api_field")}</span></div> },
|
|
{ id: "permission", header: "i18n:govoplan-docs.permission.d71c9448", width: 190, filterable: true, value: (field: Record<string, unknown>) => metadataString(field, "permission_scope") || "-", render: (field: Record<string, unknown>) => metadataString(field, "permission_scope") || "-" }
|
|
] satisfies DataGridColumn<Record<string, unknown>>[] : []),
|
|
{ id: "validation", header: "i18n:govoplan-docs.validation.ef7f6d9c", width: "minmax(180px, .7fr)", minWidth: 160, resizable: true, filterable: true, value: (field) => metadataString(field, "validation") || "-", render: (field) => metadataString(field, "validation") || "-" }
|
|
];
|
|
return (
|
|
<div id={id}>
|
|
<DataGrid id={`${id}-grid`} rows={fields} columns={columns} getRowKey={(field) => metadataString(field, "field_id") || metadataString(field, "label")} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function PatternDetails({ topic, showTechnical, documentationType, topicById }: { topic: DocsDocumentationTopic; showTechnical: boolean; documentationType: DocumentationType; topicById: Map<string, DocsDocumentationTopic> }) {
|
|
const purpose = metadataString(topic.metadata, "purpose");
|
|
const whenUsed = metadataString(topic.metadata, "when_used");
|
|
const explanation = showTechnical ? metadataString(topic.metadata, "admin_explanation") : metadataString(topic.metadata, "user_explanation");
|
|
const componentRefs = metadataList(topic.metadata, "component_refs");
|
|
const prefix = topicAnchorId(topic);
|
|
return (
|
|
<div className="docs-topic-details">
|
|
{purpose && <DetailBlock id={`${prefix}-purpose`} title="i18n:govoplan-docs.purpose.4af40581" value={purpose} />}
|
|
{whenUsed && <DetailBlock id={`${prefix}-when-used`} title="i18n:govoplan-docs.when_used.b870b361" value={whenUsed} />}
|
|
{explanation && <DetailBlock id={`${prefix}-details`} title="i18n:govoplan-docs.details.a6b3c45f" value={explanation} />}
|
|
{showTechnical && !!componentRefs.length && <DetailList id={`${prefix}-components`} title="i18n:govoplan-docs.components.e81e57d8" items={componentRefs} />}
|
|
<RelatedTopics topic={topic} documentationType={documentationType} topicById={topicById} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function DetailBlock({ id, title, value }: { id: string; title: string; value: string }) {
|
|
return (
|
|
<div className="docs-detail-block" id={id}>
|
|
<h4>{title}</h4>
|
|
<p>{value}</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function DetailList({ id, title, items }: { id: string; title: string; items: string[] }) {
|
|
return (
|
|
<div className="docs-detail-block" id={id}>
|
|
<h4>{title}</h4>
|
|
<ul>
|
|
{items.map((item) => <li key={item}>{item}</li>)}
|
|
</ul>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function RelatedTopics({ topic, documentationType, topicById }: { topic: DocsDocumentationTopic; documentationType: DocumentationType; topicById: Map<string, DocsDocumentationTopic> }) {
|
|
const related = metadataList(topic.metadata, "related_topic_ids")
|
|
.map((id) => topicById.get(id))
|
|
.filter((item): item is DocsDocumentationTopic => Boolean(item));
|
|
if (!related.length) return null;
|
|
return (
|
|
<p className="docs-topic-links">
|
|
<span>i18n:govoplan-docs.related_topics.d5eb8b74 </span>
|
|
{related.map((item, index) =>
|
|
<span key={item.id}>
|
|
{index > 0 && " | "}
|
|
<Link to={topicPageHref(item, documentationType)}>{item.title}</Link>
|
|
</span>
|
|
)}
|
|
</p>
|
|
);
|
|
}
|
|
|
|
function TopicLinks({ topic }: { topic: DocsDocumentationTopic }) {
|
|
return (
|
|
<p className="docs-topic-links">
|
|
{topic.links.map((link, index) =>
|
|
<span key={link.href}>
|
|
{index > 0 && " | "}
|
|
<a href={link.href} target={link.kind === "public" ? "_blank" : undefined} rel={link.kind === "public" ? "noreferrer" : undefined}>{link.label}</a>
|
|
</span>
|
|
)}
|
|
</p>
|
|
);
|
|
}
|
|
|
|
function BodyText({ value }: { value: string }) {
|
|
if (!value) return null;
|
|
return (
|
|
<>
|
|
{value.split("\n").filter(Boolean).map((line) => <p className="docs-topic-body" key={line}>{line}</p>)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function UnavailableDocumentationReason({ topic }: { topic: DocsDocumentationTopic }) {
|
|
const rows = [
|
|
["i18n:govoplan-docs.modules.04e9462c", topic.blockers.modules],
|
|
["i18n:govoplan-docs.capabilities.ca09c54b", topic.blockers.capabilities],
|
|
["i18n:govoplan-docs.permissions.d06d5557", topic.blockers.scopes]
|
|
].filter(([, values]) => Array.isArray(values) && values.length);
|
|
return (
|
|
<div className="docs-unavailable-reason">
|
|
<p className="muted">{topic.reason}</p>
|
|
{!!rows.length &&
|
|
<dl className="detail-list compact">
|
|
{rows.map(([label, values]) =>
|
|
<div key={String(label)}>
|
|
<dt>{label}</dt>
|
|
<dd>{(values as string[]).join(", ")}</dd>
|
|
</div>
|
|
)}
|
|
</dl>
|
|
}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function docsTreeNodes(context: DocsContext | null, adminDocs: boolean): DocsTreeNode[] {
|
|
const allTopics = allDocumentationTopics(context);
|
|
const systemTopics = context?.topic_groups.system ?? [];
|
|
const workflowTopics = context?.topic_groups.workflow ?? [];
|
|
const referenceTopics = context?.topic_groups.reference ?? [];
|
|
const patternTopics = context?.topic_groups.pattern ?? [];
|
|
const systemTopicIds = new Set(systemTopics.map((topic) => topic.id));
|
|
const moduleTopics = allTopics.filter((topic) => !systemTopicIds.has(topic.id));
|
|
const workingTopics = uniqueTopics([...patternTopics, ...workflowTopics]);
|
|
const troubleshootingTopics = allTopics.filter((topic) => !topic.active || topic.layer === "evidence");
|
|
const basicsNode = categoryTreeNode(
|
|
"basics",
|
|
"i18n:govoplan-docs.basics.5fcebeef",
|
|
patternTopics,
|
|
"i18n:govoplan-docs.no_basic_topics_found.ee5ec3c6"
|
|
);
|
|
const commonTasksNode = categoryTreeNode(
|
|
"common-tasks",
|
|
"i18n:govoplan-docs.common_tasks.9f825c48",
|
|
workflowTopics,
|
|
"i18n:govoplan-docs.no_common_task_topics_found.c17814cf"
|
|
);
|
|
const nodes: DocsTreeNode[] = [
|
|
categoryTreeNode(
|
|
"about-govoplan",
|
|
"i18n:govoplan-docs.about_govoplan.6b2d7127",
|
|
systemTopics,
|
|
"i18n:govoplan-docs.no_about_topics_found.7cd8af6d"
|
|
),
|
|
{
|
|
id: "tree:working-with-govoplan",
|
|
title: "i18n:govoplan-docs.working_with_govoplan.81908ef4",
|
|
page: {
|
|
id: "working-with-govoplan:overview",
|
|
title: "i18n:govoplan-docs.working_with_govoplan.81908ef4",
|
|
kind: "topic-list",
|
|
topics: workingTopics,
|
|
emptyText: "i18n:govoplan-docs.no_working_with_govoplan_topics_found.b6db8384"
|
|
},
|
|
children: [basicsNode, commonTasksNode]
|
|
},
|
|
categoryTreeNode(
|
|
"advanced",
|
|
"i18n:govoplan-docs.advanced.203a8d6b",
|
|
referenceTopics,
|
|
"i18n:govoplan-docs.no_advanced_topics_found.271166c9"
|
|
),
|
|
{
|
|
id: "tree:modules",
|
|
title: "i18n:govoplan-docs.modules.04e9462c",
|
|
page: {
|
|
id: "modules:overview",
|
|
title: "i18n:govoplan-docs.modules.04e9462c",
|
|
kind: "topic-list",
|
|
topics: moduleTopics,
|
|
emptyText: "i18n:govoplan-docs.no_module_topics_found.0cbbdc9b"
|
|
},
|
|
children: moduleTreeNodes(moduleTopics, context)
|
|
}
|
|
];
|
|
if (adminDocs) {
|
|
nodes.push({
|
|
id: "tree:administration",
|
|
title: "i18n:govoplan-docs.administration.101aedaf",
|
|
page: {
|
|
id: "administration:overview",
|
|
title: "i18n:govoplan-docs.administration.101aedaf",
|
|
kind: "topic-list",
|
|
topics: referenceTopics,
|
|
emptyText: "i18n:govoplan-docs.no_administration_topics_found.6213dff2"
|
|
},
|
|
children: [
|
|
...referenceTopics.map(topicTreeNode),
|
|
{
|
|
id: "tree:administration:technical-reference",
|
|
title: "i18n:govoplan-docs.technical_reference.f271430d",
|
|
page: {
|
|
id: "system:technical-reference",
|
|
title: "i18n:govoplan-docs.technical_reference.f271430d",
|
|
kind: "admin-reference"
|
|
},
|
|
children: []
|
|
}
|
|
]
|
|
});
|
|
}
|
|
if (troubleshootingTopics.length) {
|
|
nodes.push(categoryTreeNode(
|
|
"troubleshooting",
|
|
"i18n:govoplan-docs.troubleshooting_and_support.3d42f434",
|
|
troubleshootingTopics,
|
|
"i18n:govoplan-docs.no_troubleshooting_topics_found.8c275468"
|
|
));
|
|
}
|
|
return nodes;
|
|
}
|
|
|
|
function categoryTreeNode(id: string, title: string, topics: DocsDocumentationTopic[], emptyText: string): DocsTreeNode {
|
|
const sortedTopics = [...topics].sort((left, right) => left.order - right.order || left.title.localeCompare(right.title));
|
|
return {
|
|
id: `tree:${id}`,
|
|
title,
|
|
page: {
|
|
id: `${id}:overview`,
|
|
title,
|
|
kind: "topic-list",
|
|
topics: sortedTopics,
|
|
emptyText
|
|
},
|
|
children: nestedTopicNodes(sortedTopics)
|
|
};
|
|
}
|
|
|
|
function topicPage(topic: DocsDocumentationTopic): DocsPageNode {
|
|
return {
|
|
id: topic.id,
|
|
title: topic.title,
|
|
kind: "topic",
|
|
topic
|
|
};
|
|
}
|
|
|
|
function topicTreeNode(topic: DocsDocumentationTopic): DocsTreeNode {
|
|
return {
|
|
id: `tree:topic:${topic.id}`,
|
|
title: topic.title,
|
|
page: topicPage(topic),
|
|
children: []
|
|
};
|
|
}
|
|
|
|
function moduleTreeNodes(topics: DocsDocumentationTopic[], context: DocsContext | null): DocsTreeNode[] {
|
|
const topicsByModule = new Map<string, DocsDocumentationTopic[]>();
|
|
for (const topic of topics) {
|
|
const moduleTopics = topicsByModule.get(topic.source_module_id) ?? [];
|
|
moduleTopics.push(topic);
|
|
topicsByModule.set(topic.source_module_id, moduleTopics);
|
|
}
|
|
const configuredModules = context?.layers.configured.modules ?? [];
|
|
const moduleOrder = new Map(configuredModules.map((module, index) => [module.id, index]));
|
|
return Array.from(topicsByModule.entries())
|
|
.sort(([left], [right]) => (moduleOrder.get(left) ?? 999) - (moduleOrder.get(right) ?? 999) || moduleTitle(left, context).localeCompare(moduleTitle(right, context)))
|
|
.map(([moduleId, moduleTopics]) => {
|
|
const sortedTopics = [...moduleTopics].sort((left, right) => left.order - right.order || left.title.localeCompare(right.title));
|
|
const title = moduleTitle(moduleId, context);
|
|
return {
|
|
id: `tree:module:${moduleId}`,
|
|
title,
|
|
page: {
|
|
id: `module:${moduleId}`,
|
|
title,
|
|
kind: "topic-list",
|
|
topics: sortedTopics,
|
|
emptyText: "i18n:govoplan-docs.no_system_topics_found.d01a8068"
|
|
},
|
|
children: nestedTopicNodes(sortedTopics)
|
|
};
|
|
});
|
|
}
|
|
|
|
function nestedTopicNodes(topics: DocsDocumentationTopic[]): DocsTreeNode[] {
|
|
const nodes = new Map(topics.map((topic) => [topic.id, topicTreeNode(topic)]));
|
|
const roots: DocsTreeNode[] = [];
|
|
for (const topic of topics) {
|
|
const node = nodes.get(topic.id);
|
|
if (!node) continue;
|
|
const parentId = topicParentId(topic);
|
|
const parent = parentId ? nodes.get(parentId) : undefined;
|
|
if (parent && parentId !== topic.id) {
|
|
parent.children.push(node);
|
|
} else {
|
|
roots.push(node);
|
|
}
|
|
}
|
|
return roots;
|
|
}
|
|
|
|
function topicParentId(topic: DocsDocumentationTopic): string {
|
|
return metadataString(topic.metadata, "tree_parent_id") || metadataString(topic.metadata, "parent_topic_id") || metadataString(topic.metadata, "parent_id");
|
|
}
|
|
|
|
function moduleTitle(moduleId: string, context: DocsContext | null): string {
|
|
const module = context?.layers.configured.modules.find((item) => item.id === moduleId);
|
|
if (module) return module.name;
|
|
return moduleId
|
|
.replace(/^govoplan[-_]/, "")
|
|
.split(/[-_]+/)
|
|
.filter(Boolean)
|
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
.join(" ") || moduleId;
|
|
}
|
|
|
|
function flattenTreePages(nodes: DocsTreeNode[]): DocsPageNode[] {
|
|
return nodes.flatMap((node) => [node.page, ...flattenTreePages(node.children)]);
|
|
}
|
|
|
|
function defaultExpandedNodeIds(nodes: DocsTreeNode[]): string[] {
|
|
return nodes.filter((node) => node.children.length).map((node) => node.id);
|
|
}
|
|
|
|
function ancestorNodeIdsForPage(nodes: DocsTreeNode[], pageId: string, ancestors: string[] = []): string[] {
|
|
for (const node of nodes) {
|
|
if (node.page.id === pageId) return ancestors;
|
|
const match = ancestorNodeIdsForPage(node.children, pageId, [...ancestors, node.id]);
|
|
if (match.length) return match;
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function activeNodeIdForPage(nodes: DocsTreeNode[], pageId: string): string {
|
|
if (!pageId) return "";
|
|
for (const node of nodes) {
|
|
if (node.page.id === pageId) return node.id;
|
|
const childId = activeNodeIdForPage(node.children, pageId);
|
|
if (childId) return childId;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function allDocumentationTopics(context: DocsContext | null): DocsDocumentationTopic[] {
|
|
const topics = Object.values(context?.topic_groups ?? {}).flat();
|
|
return uniqueTopics(topics).sort((left, right) => left.order - right.order || left.title.localeCompare(right.title));
|
|
}
|
|
|
|
function uniqueTopics(topics: DocsDocumentationTopic[]): DocsDocumentationTopic[] {
|
|
const seen = new Set<string>();
|
|
return topics.filter((topic) => {
|
|
if (seen.has(topic.id)) return false;
|
|
seen.add(topic.id);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
function selectedPageFromSearch(search: string, pages: DocsPageNode[]): DocsPageNode | null {
|
|
if (!pages.length) return null;
|
|
const params = new URLSearchParams(search);
|
|
const requested = params.get("topic") || "";
|
|
if (requested) return pages.find((page) => page.id === requested) ?? pages[0];
|
|
const helpContext = params.get("context") || "";
|
|
if (helpContext) {
|
|
const exact = pages.find((page) => page.kind === "topic" && metadataList(page.topic.metadata, "help_contexts").includes(helpContext));
|
|
if (exact) return exact;
|
|
const moduleId = helpContextModuleId(helpContext);
|
|
const moduleTopic = pages.find((page) => page.kind === "topic" && page.topic.source_module_id === moduleId);
|
|
if (moduleTopic) return moduleTopic;
|
|
}
|
|
return pages[0];
|
|
}
|
|
|
|
function helpContextModuleId(value: string): string {
|
|
const prefix = value.split(".", 1)[0];
|
|
if (prefix === "campaign") return "campaigns";
|
|
if (prefix === "address-book") return "addresses";
|
|
return prefix;
|
|
}
|
|
|
|
function outlineForPage(page: DocsPageNode | null, showTechnical: boolean): OutlineItem[] {
|
|
if (!page) return [];
|
|
if (page.kind === "admin-reference") {
|
|
return [
|
|
{ id: page.id, label: page.title },
|
|
{ id: "docs-admin-modules", label: "i18n:govoplan-docs.modules.04e9462c" },
|
|
{ id: "docs-admin-routes", label: "i18n:govoplan-docs.routes.03730e58" },
|
|
{ id: "docs-admin-permissions", label: "i18n:govoplan-docs.permissions.d06d5557" }
|
|
];
|
|
}
|
|
if (page.kind === "topic-list") {
|
|
return [
|
|
{ id: page.id, label: page.title },
|
|
...page.topics.map((topic) => ({ id: topicAnchorId(topic), label: topic.title }))
|
|
];
|
|
}
|
|
const topic = page.topic;
|
|
const prefix = topicAnchorId(topic);
|
|
const items: OutlineItem[] = [{ id: prefix, label: topic.title }];
|
|
if (topic.summary) items.push({ id: `${prefix}-summary`, label: "i18n:govoplan-docs.summary.d6b9936d" });
|
|
if (metadataList(topic.metadata, "current_configuration").length) items.push({ id: `${prefix}-current-configuration`, label: "i18n:govoplan-docs.this_system.b13a51ad" });
|
|
if (metadataRecords(topic.metadata, "constraints").length) items.push({ id: `${prefix}-constraints`, label: "i18n:govoplan-docs.requirements.09a428f9" });
|
|
if (metadataList(topic.metadata, "limitations").length) items.push({ id: `${prefix}-limitations`, label: "i18n:govoplan-docs.details.a6b3c45f" });
|
|
if (topic.kind === "workflow") {
|
|
if (metadataString(topic.metadata, "outcome")) items.push({ id: `${prefix}-outcome`, label: "i18n:govoplan-docs.outcome.10172bd3" });
|
|
if (metadataList(topic.metadata, "prerequisites").length) items.push({ id: `${prefix}-prerequisites`, label: "i18n:govoplan-docs.prerequisites.fdf2407f" });
|
|
if (metadataList(topic.metadata, "steps").length) items.push({ id: `${prefix}-steps`, label: "i18n:govoplan-docs.steps.6041435e" });
|
|
if (metadataString(topic.metadata, "result")) items.push({ id: `${prefix}-result`, label: "i18n:govoplan-docs.result.1f4fbf43" });
|
|
if (metadataString(topic.metadata, "verification")) items.push({ id: `${prefix}-verification`, label: "i18n:govoplan-docs.verification.4b5f10dd" });
|
|
}
|
|
if (topic.kind === "reference" && metadataRecords(topic.metadata, "fields").length) {
|
|
items.push({ id: `${prefix}-fields`, label: "i18n:govoplan-docs.field.7558c082" });
|
|
}
|
|
if (topic.kind === "pattern") {
|
|
if (metadataString(topic.metadata, "purpose")) items.push({ id: `${prefix}-purpose`, label: "i18n:govoplan-docs.purpose.4af40581" });
|
|
if (metadataString(topic.metadata, "when_used")) items.push({ id: `${prefix}-when-used`, label: "i18n:govoplan-docs.when_used.b870b361" });
|
|
if (showTechnical && metadataList(topic.metadata, "component_refs").length) items.push({ id: `${prefix}-components`, label: "i18n:govoplan-docs.components.e81e57d8" });
|
|
}
|
|
return items;
|
|
}
|
|
|
|
function documentationTypeFromSearch(search: string): DocumentationType {
|
|
return new URLSearchParams(search).get("type") === "admin" ? "admin" : "user";
|
|
}
|
|
|
|
function localeFromSearch(search: string): string | null {
|
|
const value = new URLSearchParams(search).get("locale");
|
|
if (!value) return null;
|
|
return value.toLowerCase().startsWith("de") ? "de" : "en";
|
|
}
|
|
|
|
function topicAnchorId(topic: DocsDocumentationTopic): string {
|
|
return topic.anchor_id || `docs-topic-${topic.source_module_id}-${topic.id}`.replace(/[^a-z0-9_-]+/gi, "-").toLowerCase();
|
|
}
|
|
|
|
function topicPageHref(topic: DocsDocumentationTopic, documentationType: DocumentationType): string {
|
|
const params = new URLSearchParams();
|
|
params.set("type", documentationType);
|
|
params.set("topic", topic.id);
|
|
return `?${params.toString()}#${topicAnchorId(topic)}`;
|
|
}
|
|
|
|
function topicIndex(context: DocsContext | null): Map<string, DocsDocumentationTopic> {
|
|
const index = new Map<string, DocsDocumentationTopic>();
|
|
for (const topic of allDocumentationTopics(context)) {
|
|
index.set(topic.id, topic);
|
|
}
|
|
return index;
|
|
}
|
|
|
|
function metadataString(metadata: Record<string, unknown>, key: string): string {
|
|
const value = metadata[key];
|
|
return typeof value === "string" ? value : "";
|
|
}
|
|
|
|
function metadataList(metadata: Record<string, unknown>, key: string): string[] {
|
|
const value = metadata[key];
|
|
if (!Array.isArray(value)) return [];
|
|
return value.filter((item): item is string => typeof item === "string");
|
|
}
|
|
|
|
function metadataRecords(metadata: Record<string, unknown>, key: string): Record<string, unknown>[] {
|
|
const value = metadata[key];
|
|
if (!Array.isArray(value)) return [];
|
|
return value.filter((item): item is Record<string, unknown> => Boolean(item) && typeof item === "object" && !Array.isArray(item));
|
|
}
|
|
|
|
function ModuleTable({ modules }: { modules: DocsModule[] }) {
|
|
if (!modules.length) return <p className="muted">i18n:govoplan-docs.no_configured_modules_found.f6f9ce24</p>;
|
|
const columns: DataGridColumn<DocsModule>[] = [
|
|
{ id: "module", header: "i18n:govoplan-docs.module.b8ff0289", width: "minmax(220px, 1fr)", minWidth: 190, resizable: true, sortable: true, filterable: true, value: (module) => `${module.name} ${module.id} ${module.version}`, render: (module) => <div><strong>{module.name}</strong><span className="muted block">{module.id} {module.version}</span></div> },
|
|
{ id: "routes", header: "i18n:govoplan-docs.routes.03730e58", width: 170, sortable: true, value: (module) => module.route_count, render: (module) => <>{module.route_count} i18n:govoplan-docs.route.200e2a66 {module.nav_count} nav</> },
|
|
{ id: "permissions", header: "i18n:govoplan-docs.permissions.d06d5557", width: 130, sortable: true, value: (module) => module.permission_count },
|
|
{ id: "frontend", header: "i18n:govoplan-docs.frontend.152d1cf2", width: "minmax(180px, .8fr)", minWidth: 160, resizable: true, sortable: true, filterable: true, value: (module) => module.frontend_package || "-", render: (module) => module.frontend_package || "-" },
|
|
{ id: "capabilities", header: "i18n:govoplan-docs.capabilities.ca09c54b", width: "minmax(260px, 1.2fr)", minWidth: 220, resizable: true, filterable: true, value: (module) => module.capabilities.join(" "), render: (module) => <div>{module.capabilities.length ? module.capabilities.join(", ") : "-"}{module.documentation_count || module.documentation_provider_count ? <span className="muted block">{module.documentation_count} i18n:govoplan-docs.docs.5dc1e9b8 {module.documentation_provider_count} provider</span> : null}</div> }
|
|
];
|
|
return (
|
|
<DataGrid id="docs-configured-modules" rows={modules} columns={columns} getRowKey={(module) => module.id} />
|
|
);
|
|
}
|
|
|
|
function RouteTable({ routes, emptyText }: { routes: DocsRoute[]; emptyText: string }) {
|
|
if (!routes.length) return <p className="muted">{emptyText}</p>;
|
|
const columns: DataGridColumn<DocsRoute>[] = [
|
|
{ id: "route", header: "i18n:govoplan-docs.route.4999528e", width: "minmax(240px, 1fr)", minWidth: 200, resizable: true, sortable: true, filterable: true, value: (route) => `${route.label} ${route.path}`, render: (route) => <div><strong>{route.label}</strong><span className="muted block">{route.path}</span></div> },
|
|
{ id: "module", header: "i18n:govoplan-docs.module.b8ff0289", width: 160, sortable: true, filterable: true, value: (route) => route.module_id },
|
|
{ id: "source", header: "i18n:govoplan-docs.source.6da13add", width: 160, sortable: true, filterable: true, value: (route) => route.source },
|
|
{ id: "requirements", header: "i18n:govoplan-docs.requirements.09a428f9", width: "minmax(220px, 1fr)", minWidth: 180, resizable: true, filterable: true, value: (route) => routeRequirements(route) },
|
|
{ id: "status", header: "i18n:govoplan-docs.status.bae7d5be", width: 160, sortable: true, filterable: true, value: (route) => route.visible ? "visible" : route.reason, render: (route) => <StatusBadge status={route.visible ? "success" : "warning"} label={route.visible ? "visible" : route.reason} /> }
|
|
];
|
|
return (
|
|
<DataGrid id={`docs-routes-${emptyText}`} rows={routes} columns={columns} getRowKey={(route) => `${route.source}-${route.module_id}-${route.path}`} />
|
|
);
|
|
}
|
|
|
|
function PermissionList({ permissions }: { permissions: Array<{ scope: string; label: string; category: string }> }) {
|
|
if (!permissions.length) return <p className="muted">i18n:govoplan-docs.no_granted_platform_permissions_found.36010898</p>;
|
|
return (
|
|
<dl className="detail-list">
|
|
{permissions.slice(0, 24).map((permission) =>
|
|
<div key={permission.scope}>
|
|
<dt>{permission.category}</dt>
|
|
<dd><strong>{permission.label}</strong><span className="muted"> · {permission.scope}</span></dd>
|
|
</div>
|
|
)}
|
|
{permissions.length > 24 && <div><dt>i18n:govoplan-docs.more.4bab2d8f</dt><dd>{permissions.length - 24} i18n:govoplan-docs.additional_permissions.8042cb01</dd></div>}
|
|
</dl>
|
|
);
|
|
}
|
|
|
|
function EvidenceList({ modules, sources }: { modules: DocsOptionalModuleEvidence[]; sources: DocsSource[] }) {
|
|
if (!modules.length && !sources.length) return <p className="muted">i18n:govoplan-docs.no_evidence_sources_found.be3bb2f6</p>;
|
|
return (
|
|
<dl className="detail-list">
|
|
{modules.map((item) =>
|
|
<div key={`${item.source_module_id}-${item.module_id}`}>
|
|
<dt><StatusBadge status={item.status === "installed" ? "success" : "inactive"} label={item.status} /></dt>
|
|
<dd><strong>{item.module_id}</strong><span className="muted"> · {item.reason}</span></dd>
|
|
</div>
|
|
)}
|
|
{sources.map((item) =>
|
|
<div key={item.source}>
|
|
<dt>{item.layer}</dt>
|
|
<dd><strong>{item.label}</strong><span className="muted"> · {item.source}</span></dd>
|
|
</div>
|
|
)}
|
|
</dl>
|
|
);
|
|
}
|
|
|
|
function routeRequirements(route: DocsRoute): string {
|
|
const parts = [];
|
|
if (route.required_all.length) parts.push(`all: ${route.required_all.join(", ")}`);
|
|
if (route.required_any.length) parts.push(`any: ${route.required_any.join(", ")}`);
|
|
return parts.join(" | ") || "-";
|
|
}
|