Add version-aware documentation resolution

This commit is contained in:
2026-07-31 22:48:07 +02:00
parent c1ea7bb8f1
commit 0d8a49c8af
4 changed files with 225 additions and 8 deletions
+19 -1
View File
@@ -126,6 +126,13 @@ export type DocsDocumentationTopic = {
i18n_key: string;
locale: string;
translation_locale: string;
version: {
resolved: string;
minimum?: string | null;
maximum_exclusive?: string | null;
range: string;
fallback: "unversioned" | "matching_range" | string;
};
conditions: DocsDocumentationCondition[];
links: DocsDocumentationLink[];
related_modules: string[];
@@ -141,6 +148,16 @@ export type DocsDocumentationTopic = {
};
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: {
tenant_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();
if (options.documentationType) params.set("type", options.documentationType);
if (options.locale) params.set("locale", options.locale);
if (options.version) params.set("version", options.version);
const query = params.toString();
return apiFetch(settings, `/api/v1/docs/context${query ? `?${query}` : ""}`);
}
+30 -2
View File
@@ -74,6 +74,7 @@ export default function DocsPage({ settings }: { settings: ApiSettings }) {
const [documentationType, setDocumentationType] = useState<DocumentationType>(() => documentationTypeFromSearch(location.search));
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(() => new Set());
const locale = localeFromSearch(location.search) ?? language;
const selectedVersion = versionFromSearch(location.search);
const adminDocs = documentationType === "admin";
async function load() {
@@ -82,7 +83,7 @@ export default function DocsPage({ settings }: { settings: ApiSettings }) {
setError("");
setContext(null);
try {
const nextContext = await fetchDocsContext(settings, { documentationType, locale });
const nextContext = await fetchDocsContext(settings, { documentationType, locale, version: selectedVersion });
if (sequence !== loadSequence.current) return;
if (nextContext.actor.documentation_type !== documentationType) {
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);
}, [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 pages = useMemo(() => flattenTreePages(treeNodes), [treeNodes]);
@@ -140,6 +141,21 @@ export default function DocsPage({ settings }: { settings: ApiSettings }) {
onSelect={selectDocumentationType}
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>
<nav className="docs-tree" aria-label="i18n:govoplan-docs.documentation_outline.6f836b99">
<ExplorerTree
@@ -210,6 +226,13 @@ export default function DocsPage({ settings }: { settings: ApiSettings }) {
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) {
const params = new URLSearchParams(location.search);
params.set("topic", page.id);
@@ -895,6 +918,11 @@ function documentationTypeFromSearch(search: string): DocumentationType {
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 {
const value = new URLSearchParams(search).get("locale");
if (!value) return null;