Link contextual guidance to Docs

This commit is contained in:
2026-08-03 07:33:57 +02:00
parent 70fc6da811
commit b823a22b9b
13 changed files with 191 additions and 4 deletions
+6
View File
@@ -21,6 +21,8 @@ import { UnsavedChangesProvider } from "./components/UnsavedChangesGuard";
import { PlatformLanguageProvider, type PlatformLanguage } from "./i18n/LanguageContext";
import ViewSurfaceRouteBoundary from "./components/ViewSurfaceRouteBoundary";
import ModuleLoadBoundary from "./components/ModuleLoadBoundary";
import { DocumentationHelpProvider } from "./components/help/DocumentationHelpLink";
import { hasAnyScope } from "./utils/permissions";
const DashboardPage = lazy(() => import("./features/dashboard/DashboardPage"));
const SettingsPage = lazy(() => import("./features/settings/SettingsPage"));
@@ -494,6 +496,8 @@ export default function App() {
}
const defaultRoute = firstAccessibleRoute(auth, webModules, viewProjection);
const localDocsAvailable = hasAnyScope(auth, ["docs:documentation:read", "docs:documentation:admin", "system:settings:read", "admin:settings:read"]) &&
webModules.some((module) => module.id === "docs" && module.routes?.some((route) => route.path === "/docs"));
const authAvailableLanguages = auth.available_languages?.map((item) => ({
code: item.code,
label: item.label,
@@ -514,6 +518,7 @@ export default function App() {
onLanguageChange={persistLanguagePreference}
moduleTranslations={moduleTranslations}>
<PlatformModulesProvider modules={webModules}>
<DocumentationHelpProvider localDocsAvailable={localDocsAvailable}>
<PlatformViewProvider modules={webModules} projection={viewProjection}>
<UnsavedChangesProvider>
<AppShell settings={settings} auth={auth} onSettingsChange={updateSettings} onAuthChange={updateAuth} navItems={navItems} maintenanceMode={maintenanceMode} backendReachable={backendReachable}>
@@ -561,6 +566,7 @@ export default function App() {
</AppShell>
</UnsavedChangesProvider>
</PlatformViewProvider>
</DocumentationHelpProvider>
</PlatformModulesProvider>
</PlatformLanguageProvider>);
+6 -1
View File
@@ -1,6 +1,8 @@
import { AlertTriangle, Info } from "lucide-react";
import type { ReactNode } from "react";
import AdvancedOptionsPanel from "./AdvancedOptionsPanel";
import DocumentationHelpLink from "./help/DocumentationHelpLink";
import type { DocumentationHelpReference } from "./help/documentationHelp";
export type ActionBlockerReason = {
summary: ReactNode;
@@ -23,6 +25,7 @@ type ActionBlockerHintProps = {
tone?: "info" | "warning" | "danger";
className?: string;
labels?: ActionBlockerLabels;
documentation?: DocumentationHelpReference;
};
function joinClasses(...classes: Array<string | undefined | false>) {
@@ -33,7 +36,8 @@ export default function ActionBlockerHint({
reason,
tone = "warning",
className = "",
labels = {}
labels = {},
documentation
}: ActionBlockerHintProps) {
const Icon = tone === "info" ? Info : AlertTriangle;
const hasActionRows = Boolean(reason.requiredAction || reason.actor || reason.target);
@@ -71,6 +75,7 @@ export default function ActionBlockerHint({
<div>{reason.technicalDetails}</div>
</AdvancedOptionsPanel>
)}
{documentation && <DocumentationHelpLink reference={documentation} />}
</div>
</section>
);
+3 -2
View File
@@ -1,14 +1,15 @@
import type { ReactNode } from "react";
import FieldLabel from "./help/FieldLabel";
import type { DocumentationHelpReference } from "./help/documentationHelp";
import { helpForFieldLabel } from "../utils/fieldHelp";
import { usePlatformLanguage } from "../i18n/LanguageContext";
export default function FormField({ label, help, children }: { label: ReactNode; help?: ReactNode; children: ReactNode }) {
export default function FormField({ label, help, documentation, children }: { label: ReactNode; help?: ReactNode; documentation?: DocumentationHelpReference; children: ReactNode }) {
const { translateText } = usePlatformLanguage();
const renderedLabel = typeof label === "string" ? translateText(label) : label;
return (
<label className="form-field">
<FieldLabel className="form-label" help={help ?? helpForFieldLabel(label)}>{renderedLabel}</FieldLabel>
<FieldLabel className="form-label" help={help ?? helpForFieldLabel(label)} documentation={documentation}>{renderedLabel}</FieldLabel>
{children}
</label>
);
@@ -0,0 +1,63 @@
import { BookOpen } from "lucide-react";
import { createContext, useContext, type MouseEvent, type ReactNode } from "react";
import { usePlatformLanguage } from "../../i18n/LanguageContext";
import {
HOSTED_DOCUMENTATION_URL,
documentationHelpHref,
type DocumentationHelpReference
} from "./documentationHelp";
export { documentationHelpHref } from "./documentationHelp";
export type { DocumentationHelpReference } from "./documentationHelp";
const DocumentationHelpAvailabilityContext = createContext(false);
export function DocumentationHelpProvider({
localDocsAvailable,
children
}: {
localDocsAvailable: boolean;
children: ReactNode;
}) {
return (
<DocumentationHelpAvailabilityContext.Provider value={localDocsAvailable}>
{children}
</DocumentationHelpAvailabilityContext.Provider>
);
}
export default function DocumentationHelpLink({
reference,
label = "i18n:govoplan-core.open_user_documentation.084af515",
className = ""
}: {
reference: DocumentationHelpReference;
label?: string;
className?: string;
}) {
const { translateText } = usePlatformLanguage();
const localDocsAvailable = useContext(DocumentationHelpAvailabilityContext);
const href = documentationHelpHref(
reference,
localDocsAvailable ? "/docs" : HOSTED_DOCUMENTATION_URL
);
if (!href) return null;
const translatedLabel = translateText(label);
const stopLabelActivation = (event: MouseEvent<HTMLAnchorElement>) => event.stopPropagation();
return (
<a
className={["documentation-help-link", className].filter(Boolean).join(" ")}
href={href}
aria-label={translatedLabel}
title={translatedLabel}
target={localDocsAvailable ? undefined : "_blank"}
rel={localDocsAvailable ? undefined : "noreferrer"}
onClick={stopLabelActivation}
onMouseDown={stopLabelActivation}
>
<BookOpen size={13} aria-hidden="true" />
</a>
);
}
+5 -1
View File
@@ -1,17 +1,21 @@
import type { ReactNode } from "react";
import DocumentationHelpLink from "./DocumentationHelpLink";
import type { DocumentationHelpReference } from "./documentationHelp";
import InlineHelp from "./InlineHelp";
type FieldLabelProps = {
children: ReactNode;
help?: ReactNode;
documentation?: DocumentationHelpReference;
className?: string;
};
export default function FieldLabel({ children, help, className = "" }: FieldLabelProps) {
export default function FieldLabel({ children, help, documentation, className = "" }: FieldLabelProps) {
return (
<span className={`field-label ${className}`.trim()}>
<span className="field-label-text">{children}</span>
{help && <InlineHelp>{help}</InlineHelp>}
{documentation && <DocumentationHelpLink reference={documentation} />}
</span>
);
}
@@ -0,0 +1,25 @@
export const HOSTED_DOCUMENTATION_URL = "https://govoplan.add-ideas.de/";
export type DocumentationHelpReference = {
topicId?: string;
contextId?: string;
documentationType?: "user" | "admin";
anchorId?: string;
};
export function documentationHelpHref(
reference: DocumentationHelpReference,
baseUrl = "/docs"
): string | null {
const topicId = reference.topicId?.trim();
const contextId = reference.contextId?.trim();
if (!topicId && !contextId) return null;
const params = new URLSearchParams({
type: reference.documentationType === "admin" ? "admin" : "user"
});
if (topicId) params.set("topic", topicId);
else if (contextId) params.set("context", contextId);
const anchorId = reference.anchorId?.trim();
return `${baseUrl}?${params.toString()}${anchorId ? `#${encodeURIComponent(anchorId)}` : ""}`;
}
+3
View File
@@ -174,6 +174,9 @@ export { default as EmailAddressInput } from "./components/email/EmailAddressInp
export { default as MailServerSettingsPanel, MailServerActionResult, MailServerFolderLookupResultView, defaultImapPort, defaultSmtpPort, hasMailImapSettings, mailImapSettingsPayload, mailNumberOrDefault, mailNumberOrNull, mailServerSecurityOptions, mailSmtpSettingsPayload, mailTextOrNull, mailTransportCredentialsPayload, mailTransportCredentialsPayloadFromRecords, normalizeMailServerSecurity } from "./components/mail/MailServerSettingsPanel";
export type { MailServerConnectionTestResult, MailServerCredentialSettings, MailServerFolderLookupResult, MailServerImapSettings, MailServerSecurity, MailServerSecurityOption, MailServerSettingsMode, MailServerSettingsPanelProps, MailServerSettingsSection, MailServerSmtpSettings } from "./components/mail/MailServerSettingsPanel";
export { default as FieldLabel } from "./components/help/FieldLabel";
export { default as DocumentationHelpLink, DocumentationHelpProvider } from "./components/help/DocumentationHelpLink";
export { documentationHelpHref } from "./components/help/documentationHelp";
export type { DocumentationHelpReference } from "./components/help/documentationHelp";
export { default as InlineHelp } from "./components/help/InlineHelp";
export { default as DataGrid, DataGridEmptyAction, DataGridPaginationBar, DataGridRowActions } from "./components/table/DataGrid";
export type { DataGridClientPagination, DataGridColumn, DataGridListOption, DataGridPagination, DataGridPaginationBarProps, DataGridProps, DataGridQueryState, DataGridServerPagination, DataGridSortDirection } from "./components/table/DataGrid";
+21
View File
@@ -1951,6 +1951,27 @@
.inline-help:focus-visible .inline-help-mark {
box-shadow: var(--focus-ring);
}
.documentation-help-link {
display: inline-grid;
place-items: center;
flex: 0 0 auto;
width: 18px;
height: 18px;
border-radius: 3px;
color: var(--text-subtle);
text-decoration: none;
}
.documentation-help-link:hover {
color: var(--accent);
background: var(--hover-bg);
}
.documentation-help-link:focus-visible {
box-shadow: var(--focus-ring);
outline: none;
}
.ui-hide-help-hints .documentation-help-link {
display: none;
}
.policy-path-help {
display: grid;
gap: 3px;