Add version-aware documentation resolution
This commit is contained in:
@@ -20,6 +20,11 @@ from govoplan_core.core.modules import (
|
|||||||
user_workflow_scope_condition_issues,
|
user_workflow_scope_condition_issues,
|
||||||
)
|
)
|
||||||
from govoplan_core.core.registry import PlatformRegistry
|
from govoplan_core.core.registry import PlatformRegistry
|
||||||
|
from govoplan_core.core.versioning import (
|
||||||
|
format_version_range,
|
||||||
|
version_satisfies_range,
|
||||||
|
version_tuple,
|
||||||
|
)
|
||||||
from govoplan_core.db.session import get_database
|
from govoplan_core.db.session import get_database
|
||||||
|
|
||||||
from govoplan_docs.backend.manifest import DOCS_ADMIN_READ_SCOPES, DOCS_READ_SCOPES
|
from govoplan_docs.backend.manifest import DOCS_ADMIN_READ_SCOPES, DOCS_READ_SCOPES
|
||||||
@@ -42,6 +47,7 @@ def docs_context(
|
|||||||
request: Request,
|
request: Request,
|
||||||
documentation_type: DocumentationType = Query(default="user", alias="type", pattern="^(admin|user)$"),
|
documentation_type: DocumentationType = Query(default="user", alias="type", pattern="^(admin|user)$"),
|
||||||
locale: str | None = Query(default=None, min_length=2, max_length=20),
|
locale: str | None = Query(default=None, min_length=2, max_length=20),
|
||||||
|
version: str | None = Query(default=None, min_length=1, max_length=40),
|
||||||
principal: ApiPrincipal = Depends(require_any_scope(*DOCS_READ_SCOPES)),
|
principal: ApiPrincipal = Depends(require_any_scope(*DOCS_READ_SCOPES)),
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
can_read_admin_documentation = _has_any_scope(principal, DOCS_ADMIN_READ_SCOPES)
|
can_read_admin_documentation = _has_any_scope(principal, DOCS_ADMIN_READ_SCOPES)
|
||||||
@@ -51,10 +57,18 @@ def docs_context(
|
|||||||
detail="Administrative documentation requires documentation-administrator authority",
|
detail="Administrative documentation requires documentation-administrator authority",
|
||||||
)
|
)
|
||||||
registry = _registry(request)
|
registry = _registry(request)
|
||||||
|
target_version = version if isinstance(version, str) else None
|
||||||
resolved_locale = _preferred_locale(request, locale)
|
resolved_locale = _preferred_locale(request, locale)
|
||||||
route_items = _route_items(registry.manifests(), principal)
|
route_items = _route_items(registry.manifests(), principal)
|
||||||
visible_route_items = [item for item in route_items if item["visible"]]
|
visible_route_items = [item for item in route_items if item["visible"]]
|
||||||
documentation_layers = _documentation_layers(request, registry, principal, documentation_type=documentation_type, locale=resolved_locale)
|
documentation_layers = _documentation_layers(
|
||||||
|
request,
|
||||||
|
registry,
|
||||||
|
principal,
|
||||||
|
documentation_type=documentation_type,
|
||||||
|
locale=resolved_locale,
|
||||||
|
target_version=target_version,
|
||||||
|
)
|
||||||
evidence_sources = (
|
evidence_sources = (
|
||||||
_documentation_source_summaries(
|
_documentation_source_summaries(
|
||||||
request,
|
request,
|
||||||
@@ -81,6 +95,10 @@ def docs_context(
|
|||||||
)
|
)
|
||||||
topic_groups = _documentation_topic_groups(documentation_layers)
|
topic_groups = _documentation_topic_groups(documentation_layers)
|
||||||
return {
|
return {
|
||||||
|
"versions": _documentation_version_context(
|
||||||
|
registry,
|
||||||
|
target_version=target_version,
|
||||||
|
),
|
||||||
"actor": _documentation_actor(
|
"actor": _documentation_actor(
|
||||||
principal,
|
principal,
|
||||||
documentation_type=documentation_type,
|
documentation_type=documentation_type,
|
||||||
@@ -243,6 +261,50 @@ def _documentation_actor(
|
|||||||
return actor
|
return actor
|
||||||
|
|
||||||
|
|
||||||
|
def _documentation_version_context(
|
||||||
|
registry: PlatformRegistry,
|
||||||
|
*,
|
||||||
|
target_version: str | None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
manifests = registry.manifests()
|
||||||
|
installed_versions = {manifest.id: manifest.version for manifest in manifests}
|
||||||
|
supported_versions = sorted(
|
||||||
|
{
|
||||||
|
*(manifest.version for manifest in manifests),
|
||||||
|
*(
|
||||||
|
topic.version_min
|
||||||
|
for manifest in manifests
|
||||||
|
for topic in manifest.documentation
|
||||||
|
if topic.version_min
|
||||||
|
),
|
||||||
|
},
|
||||||
|
key=version_tuple,
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
latest = supported_versions[0] if supported_versions else None
|
||||||
|
if target_version is None:
|
||||||
|
status_name = "installed"
|
||||||
|
elif target_version == latest:
|
||||||
|
status_name = "stable"
|
||||||
|
elif target_version in supported_versions:
|
||||||
|
status_name = "older_supported"
|
||||||
|
else:
|
||||||
|
status_name = "unsupported"
|
||||||
|
return {
|
||||||
|
"mode": "selected" if target_version else "installed",
|
||||||
|
"selected_version": target_version,
|
||||||
|
"status": status_name,
|
||||||
|
"latest_version": latest,
|
||||||
|
"stable_version": latest,
|
||||||
|
"supported_versions": supported_versions,
|
||||||
|
"installed_versions": installed_versions,
|
||||||
|
"fallback_policy": (
|
||||||
|
"Topics without bounds apply to every version. Bounded topics are "
|
||||||
|
"hidden outside their declared half-open version range."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _documentation_summary(
|
def _documentation_summary(
|
||||||
catalog: Mapping[str, list[dict[str, Any]]],
|
catalog: Mapping[str, list[dict[str, Any]]],
|
||||||
*,
|
*,
|
||||||
@@ -471,13 +533,14 @@ def _documentation_layers(
|
|||||||
*,
|
*,
|
||||||
documentation_type: DocumentationType,
|
documentation_type: DocumentationType,
|
||||||
locale: str,
|
locale: str,
|
||||||
|
target_version: str | None = None,
|
||||||
) -> dict[str, list[dict[str, Any]]]:
|
) -> dict[str, list[dict[str, Any]]]:
|
||||||
settings = _settings(request)
|
settings = _settings(request)
|
||||||
try:
|
try:
|
||||||
with get_database().SessionLocal() as session:
|
with get_database().SessionLocal() as session:
|
||||||
return _classify_documentation(registry, principal, settings=settings, session=session, documentation_type=documentation_type, locale=locale)
|
return _classify_documentation(registry, principal, settings=settings, session=session, documentation_type=documentation_type, locale=locale, target_version=target_version)
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
return _classify_documentation(registry, principal, settings=settings, session=None, documentation_type=documentation_type, locale=locale)
|
return _classify_documentation(registry, principal, settings=settings, session=None, documentation_type=documentation_type, locale=locale, target_version=target_version)
|
||||||
|
|
||||||
|
|
||||||
def _settings(request: Request) -> object | None:
|
def _settings(request: Request) -> object | None:
|
||||||
@@ -628,6 +691,7 @@ def _classify_documentation(
|
|||||||
session: object | None,
|
session: object | None,
|
||||||
documentation_type: DocumentationType,
|
documentation_type: DocumentationType,
|
||||||
locale: str,
|
locale: str,
|
||||||
|
target_version: str | None = None,
|
||||||
) -> dict[str, list[dict[str, Any]]]:
|
) -> dict[str, list[dict[str, Any]]]:
|
||||||
layers: dict[str, list[dict[str, Any]]] = {"always": [], "configured": [], "available": [], "evidence": []}
|
layers: dict[str, list[dict[str, Any]]] = {"always": [], "configured": [], "available": [], "evidence": []}
|
||||||
installed = {manifest.id for manifest in registry.manifests()}
|
installed = {manifest.id for manifest in registry.manifests()}
|
||||||
@@ -646,11 +710,34 @@ def _classify_documentation(
|
|||||||
session=session,
|
session=session,
|
||||||
documentation_type=documentation_type,
|
documentation_type=documentation_type,
|
||||||
locale=locale,
|
locale=locale,
|
||||||
|
data={
|
||||||
|
"target_version": target_version,
|
||||||
|
"installed_versions": {
|
||||||
|
manifest.id: manifest.version for manifest in registry.manifests()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
topics = _collect_documentation_topics(
|
||||||
|
registry,
|
||||||
|
principal,
|
||||||
|
settings=settings,
|
||||||
|
session=session,
|
||||||
|
documentation_type=documentation_type,
|
||||||
|
locale=locale,
|
||||||
|
target_version=target_version,
|
||||||
)
|
)
|
||||||
topics = _collect_documentation_topics(registry, principal, settings=settings, session=session, documentation_type=documentation_type, locale=locale)
|
|
||||||
for source_module_id, topic in sorted(topics, key=lambda item: (item[1].order, item[0], item[1].id)):
|
for source_module_id, topic in sorted(topics, key=lambda item: (item[1].order, item[0], item[1].id)):
|
||||||
if not _topic_matches_documentation_type(topic, documentation_type):
|
if not _topic_matches_documentation_type(topic, documentation_type):
|
||||||
continue
|
continue
|
||||||
|
module_id = topic.source_module_id or source_module_id
|
||||||
|
manifest = registry.get(module_id)
|
||||||
|
resolved_version = target_version or (manifest.version if manifest else "0")
|
||||||
|
if not version_satisfies_range(
|
||||||
|
resolved_version,
|
||||||
|
version_min=topic.version_min,
|
||||||
|
version_max_exclusive=topic.version_max_exclusive,
|
||||||
|
):
|
||||||
|
continue
|
||||||
configuration_keys = _documentation_configuration_keys(topic)
|
configuration_keys = _documentation_configuration_keys(topic)
|
||||||
configuration = _resolve_documentation_configuration(
|
configuration = _resolve_documentation_configuration(
|
||||||
registry,
|
registry,
|
||||||
@@ -686,6 +773,7 @@ def _classify_documentation(
|
|||||||
documentation_type=documentation_type,
|
documentation_type=documentation_type,
|
||||||
visible_runtime_paths=visible_runtime_paths,
|
visible_runtime_paths=visible_runtime_paths,
|
||||||
configuration=configuration,
|
configuration=configuration,
|
||||||
|
resolved_version=resolved_version,
|
||||||
))
|
))
|
||||||
return layers
|
return layers
|
||||||
|
|
||||||
@@ -698,6 +786,7 @@ def _collect_documentation_topics(
|
|||||||
session: object | None,
|
session: object | None,
|
||||||
documentation_type: DocumentationType,
|
documentation_type: DocumentationType,
|
||||||
locale: str,
|
locale: str,
|
||||||
|
target_version: str | None = None,
|
||||||
) -> list[tuple[str, DocumentationTopic]]:
|
) -> list[tuple[str, DocumentationTopic]]:
|
||||||
topics: list[tuple[str, DocumentationTopic]] = []
|
topics: list[tuple[str, DocumentationTopic]] = []
|
||||||
for manifest in registry.manifests():
|
for manifest in registry.manifests():
|
||||||
@@ -713,7 +802,13 @@ def _collect_documentation_topics(
|
|||||||
session=session,
|
session=session,
|
||||||
documentation_type=documentation_type,
|
documentation_type=documentation_type,
|
||||||
locale=locale,
|
locale=locale,
|
||||||
data={"source_module_id": manifest.id},
|
data={
|
||||||
|
"source_module_id": manifest.id,
|
||||||
|
"target_version": target_version,
|
||||||
|
"installed_versions": {
|
||||||
|
item.id: item.version for item in registry.manifests()
|
||||||
|
},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
for provider in manifest.documentation_providers:
|
for provider in manifest.documentation_providers:
|
||||||
try:
|
try:
|
||||||
@@ -1060,6 +1155,7 @@ def _documentation_topic_payload(
|
|||||||
documentation_type: DocumentationType,
|
documentation_type: DocumentationType,
|
||||||
visible_runtime_paths: frozenset[str],
|
visible_runtime_paths: frozenset[str],
|
||||||
configuration: Mapping[str, DocumentationConfigurationDecision],
|
configuration: Mapping[str, DocumentationConfigurationDecision],
|
||||||
|
resolved_version: str,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
module_id = topic.source_module_id or source_module_id
|
module_id = topic.source_module_id or source_module_id
|
||||||
translation_locale, translation = _translation_for_locale(topic, locale)
|
translation_locale, translation = _translation_for_locale(topic, locale)
|
||||||
@@ -1090,6 +1186,21 @@ def _documentation_topic_payload(
|
|||||||
"i18n_key": topic.i18n_key or topic.id,
|
"i18n_key": topic.i18n_key or topic.id,
|
||||||
"locale": locale,
|
"locale": locale,
|
||||||
"translation_locale": translation_locale,
|
"translation_locale": translation_locale,
|
||||||
|
"version": {
|
||||||
|
"resolved": resolved_version,
|
||||||
|
"minimum": topic.version_min,
|
||||||
|
"maximum_exclusive": topic.version_max_exclusive,
|
||||||
|
"range": format_version_range(
|
||||||
|
version_min=topic.version_min,
|
||||||
|
version_max_exclusive=topic.version_max_exclusive,
|
||||||
|
),
|
||||||
|
"fallback": (
|
||||||
|
"unversioned"
|
||||||
|
if topic.version_min is None
|
||||||
|
and topic.version_max_exclusive is None
|
||||||
|
else "matching_range"
|
||||||
|
),
|
||||||
|
},
|
||||||
"conditions": [_documentation_condition_payload(condition) for condition in topic.conditions],
|
"conditions": [_documentation_condition_payload(condition) for condition in topic.conditions],
|
||||||
"links": [_documentation_link_payload(link) for link in topic.links],
|
"links": [_documentation_link_payload(link) for link in topic.links],
|
||||||
"related_modules": list(topic.related_modules),
|
"related_modules": list(topic.related_modules),
|
||||||
|
|||||||
@@ -46,6 +46,66 @@ class FakePrincipal:
|
|||||||
|
|
||||||
|
|
||||||
class DocsContextTests(unittest.TestCase):
|
class DocsContextTests(unittest.TestCase):
|
||||||
|
def test_topics_are_filtered_by_installed_or_selected_version(self) -> None:
|
||||||
|
registry = PlatformRegistry()
|
||||||
|
registry.register(ModuleManifest(
|
||||||
|
id="example",
|
||||||
|
name="Example",
|
||||||
|
version="2.1.0",
|
||||||
|
documentation=(
|
||||||
|
DocumentationTopic(
|
||||||
|
id="example.legacy",
|
||||||
|
title="Legacy",
|
||||||
|
summary="Legacy behavior",
|
||||||
|
version_max_exclusive="2.0.0",
|
||||||
|
),
|
||||||
|
DocumentationTopic(
|
||||||
|
id="example.current",
|
||||||
|
title="Current",
|
||||||
|
summary="Current behavior",
|
||||||
|
version_min="2.0.0",
|
||||||
|
),
|
||||||
|
DocumentationTopic(
|
||||||
|
id="example.universal",
|
||||||
|
title="Universal",
|
||||||
|
summary="All versions",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
principal = FakePrincipal({"docs:documentation:read"})
|
||||||
|
|
||||||
|
installed = _classify_documentation(
|
||||||
|
registry,
|
||||||
|
principal,
|
||||||
|
settings=None,
|
||||||
|
session=None,
|
||||||
|
documentation_type="admin",
|
||||||
|
locale="en",
|
||||||
|
)
|
||||||
|
selected = _classify_documentation(
|
||||||
|
registry,
|
||||||
|
principal,
|
||||||
|
settings=None,
|
||||||
|
session=None,
|
||||||
|
documentation_type="admin",
|
||||||
|
locale="en",
|
||||||
|
target_version="1.9.0",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
{topic["id"] for topic in installed["configured"]},
|
||||||
|
{"example.current", "example.universal"},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
{topic["id"] for topic in selected["configured"]},
|
||||||
|
{"example.legacy", "example.universal"},
|
||||||
|
)
|
||||||
|
universal = next(
|
||||||
|
topic for topic in selected["configured"]
|
||||||
|
if topic["id"] == "example.universal"
|
||||||
|
)
|
||||||
|
self.assertEqual(universal["version"]["fallback"], "unversioned")
|
||||||
|
|
||||||
def test_docs_reader_is_the_managed_authenticated_tenant_default(self) -> None:
|
def test_docs_reader_is_the_managed_authenticated_tenant_default(self) -> None:
|
||||||
manifest = get_docs_manifest()
|
manifest = get_docs_manifest()
|
||||||
roles = {template.slug: template for template in manifest.role_templates}
|
roles = {template.slug: template for template in manifest.role_templates}
|
||||||
|
|||||||
+19
-1
@@ -126,6 +126,13 @@ export type DocsDocumentationTopic = {
|
|||||||
i18n_key: string;
|
i18n_key: string;
|
||||||
locale: string;
|
locale: string;
|
||||||
translation_locale: string;
|
translation_locale: string;
|
||||||
|
version: {
|
||||||
|
resolved: string;
|
||||||
|
minimum?: string | null;
|
||||||
|
maximum_exclusive?: string | null;
|
||||||
|
range: string;
|
||||||
|
fallback: "unversioned" | "matching_range" | string;
|
||||||
|
};
|
||||||
conditions: DocsDocumentationCondition[];
|
conditions: DocsDocumentationCondition[];
|
||||||
links: DocsDocumentationLink[];
|
links: DocsDocumentationLink[];
|
||||||
related_modules: string[];
|
related_modules: string[];
|
||||||
@@ -141,6 +148,16 @@ export type DocsDocumentationTopic = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type DocsContext = {
|
export type DocsContext = {
|
||||||
|
versions: {
|
||||||
|
mode: "installed" | "selected";
|
||||||
|
selected_version?: string | null;
|
||||||
|
status: "installed" | "stable" | "older_supported" | "unsupported" | string;
|
||||||
|
latest_version?: string | null;
|
||||||
|
stable_version?: string | null;
|
||||||
|
supported_versions: string[];
|
||||||
|
installed_versions: Record<string, string>;
|
||||||
|
fallback_policy: string;
|
||||||
|
};
|
||||||
actor: {
|
actor: {
|
||||||
tenant_id?: string;
|
tenant_id?: string;
|
||||||
user_id?: string;
|
user_id?: string;
|
||||||
@@ -193,10 +210,11 @@ export type DocsContext = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function fetchDocsContext(settings: ApiSettings, options: { documentationType?: "admin" | "user"; locale?: string } = {}): Promise<DocsContext> {
|
export function fetchDocsContext(settings: ApiSettings, options: { documentationType?: "admin" | "user"; locale?: string; version?: string | null } = {}): Promise<DocsContext> {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (options.documentationType) params.set("type", options.documentationType);
|
if (options.documentationType) params.set("type", options.documentationType);
|
||||||
if (options.locale) params.set("locale", options.locale);
|
if (options.locale) params.set("locale", options.locale);
|
||||||
|
if (options.version) params.set("version", options.version);
|
||||||
const query = params.toString();
|
const query = params.toString();
|
||||||
return apiFetch(settings, `/api/v1/docs/context${query ? `?${query}` : ""}`);
|
return apiFetch(settings, `/api/v1/docs/context${query ? `?${query}` : ""}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export default function DocsPage({ settings }: { settings: ApiSettings }) {
|
|||||||
const [documentationType, setDocumentationType] = useState<DocumentationType>(() => documentationTypeFromSearch(location.search));
|
const [documentationType, setDocumentationType] = useState<DocumentationType>(() => documentationTypeFromSearch(location.search));
|
||||||
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(() => new Set());
|
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(() => new Set());
|
||||||
const locale = localeFromSearch(location.search) ?? language;
|
const locale = localeFromSearch(location.search) ?? language;
|
||||||
|
const selectedVersion = versionFromSearch(location.search);
|
||||||
const adminDocs = documentationType === "admin";
|
const adminDocs = documentationType === "admin";
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
@@ -82,7 +83,7 @@ export default function DocsPage({ settings }: { settings: ApiSettings }) {
|
|||||||
setError("");
|
setError("");
|
||||||
setContext(null);
|
setContext(null);
|
||||||
try {
|
try {
|
||||||
const nextContext = await fetchDocsContext(settings, { documentationType, locale });
|
const nextContext = await fetchDocsContext(settings, { documentationType, locale, version: selectedVersion });
|
||||||
if (sequence !== loadSequence.current) return;
|
if (sequence !== loadSequence.current) return;
|
||||||
if (nextContext.actor.documentation_type !== documentationType) {
|
if (nextContext.actor.documentation_type !== documentationType) {
|
||||||
throw new Error("Documentation response type did not match the requested projection.");
|
throw new Error("Documentation response type did not match the requested projection.");
|
||||||
@@ -101,7 +102,7 @@ export default function DocsPage({ settings }: { settings: ApiSettings }) {
|
|||||||
setDocumentationType((current) => current === nextType ? current : nextType);
|
setDocumentationType((current) => current === nextType ? current : nextType);
|
||||||
}, [location.search]);
|
}, [location.search]);
|
||||||
|
|
||||||
useEffect(() => { void load(); }, [settings.apiBaseUrl, settings.apiKey, settings.accessToken, documentationType, locale]);
|
useEffect(() => { void load(); }, [settings.apiBaseUrl, settings.apiKey, settings.accessToken, documentationType, locale, selectedVersion]);
|
||||||
|
|
||||||
const treeNodes = useMemo(() => docsTreeNodes(context, adminDocs), [context, adminDocs]);
|
const treeNodes = useMemo(() => docsTreeNodes(context, adminDocs), [context, adminDocs]);
|
||||||
const pages = useMemo(() => flattenTreePages(treeNodes), [treeNodes]);
|
const pages = useMemo(() => flattenTreePages(treeNodes), [treeNodes]);
|
||||||
@@ -140,6 +141,21 @@ export default function DocsPage({ settings }: { settings: ApiSettings }) {
|
|||||||
onSelect={selectDocumentationType}
|
onSelect={selectDocumentationType}
|
||||||
canViewAdmin={context?.actor.available_documentation_types.includes("admin") ?? documentationType === "admin"}
|
canViewAdmin={context?.actor.available_documentation_types.includes("admin") ?? documentationType === "admin"}
|
||||||
/>
|
/>
|
||||||
|
<label className="docs-version-selector" title={context?.versions.fallback_policy}>
|
||||||
|
<span>Version</span>
|
||||||
|
<select
|
||||||
|
value={selectedVersion ?? ""}
|
||||||
|
onChange={(event) => selectVersion(event.target.value || null)}
|
||||||
|
>
|
||||||
|
<option value="">Installed versions</option>
|
||||||
|
{selectedVersion && !context?.versions.supported_versions.includes(selectedVersion) &&
|
||||||
|
<option value={selectedVersion}>{selectedVersion} (unsupported)</option>
|
||||||
|
}
|
||||||
|
{(context?.versions.supported_versions ?? []).map((version) => (
|
||||||
|
<option key={version} value={version}>{version}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<nav className="docs-tree" aria-label="i18n:govoplan-docs.documentation_outline.6f836b99">
|
<nav className="docs-tree" aria-label="i18n:govoplan-docs.documentation_outline.6f836b99">
|
||||||
<ExplorerTree
|
<ExplorerTree
|
||||||
@@ -210,6 +226,13 @@ export default function DocsPage({ settings }: { settings: ApiSettings }) {
|
|||||||
navigate(`${location.pathname}?${params.toString()}`, { replace: true });
|
navigate(`${location.pathname}?${params.toString()}`, { replace: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectVersion(version: string | null) {
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
if (version) params.set("version", version);
|
||||||
|
else params.delete("version");
|
||||||
|
navigate(`${location.pathname}?${params.toString()}`, { replace: true });
|
||||||
|
}
|
||||||
|
|
||||||
function selectPage(page: DocsPageNode) {
|
function selectPage(page: DocsPageNode) {
|
||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(location.search);
|
||||||
params.set("topic", page.id);
|
params.set("topic", page.id);
|
||||||
@@ -895,6 +918,11 @@ function documentationTypeFromSearch(search: string): DocumentationType {
|
|||||||
return new URLSearchParams(search).get("type") === "admin" ? "admin" : "user";
|
return new URLSearchParams(search).get("type") === "admin" ? "admin" : "user";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function versionFromSearch(search: string): string | null {
|
||||||
|
const value = new URLSearchParams(search).get("version")?.trim();
|
||||||
|
return value || null;
|
||||||
|
}
|
||||||
|
|
||||||
function localeFromSearch(search: string): string | null {
|
function localeFromSearch(search: string): string | null {
|
||||||
const value = new URLSearchParams(search).get("locale");
|
const value = new URLSearchParams(search).get("locale");
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user