feat(webui): open contextual configured handbooks
This commit is contained in:
@@ -26,7 +26,7 @@
|
|||||||
"test:dialog-focus": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/dialog-focus.test.js && node scripts/test-dialog-focus-structure.mjs",
|
"test:dialog-focus": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/dialog-focus.test.js && node scripts/test-dialog-focus-structure.mjs",
|
||||||
"test:explorer-tree": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/explorer-tree.test.js",
|
"test:explorer-tree": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/explorer-tree.test.js",
|
||||||
"test:icon-button": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/icon-button.test.js",
|
"test:icon-button": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/icon-button.test.js",
|
||||||
"test:module-capabilities": "rm -rf .module-test-build && mkdir -p .module-test-build && printf '{\"type\":\"commonjs\"}\n' > .module-test-build/package.json && tsc -p tsconfig.module-tests.json && node .module-test-build/tests/module-capabilities.test.js && node .module-test-build/tests/privacy-policy.test.js",
|
"test:module-capabilities": "rm -rf .module-test-build && mkdir -p .module-test-build && printf '{\"type\":\"commonjs\"}\n' > .module-test-build/package.json && tsc -p tsconfig.module-tests.json && node .module-test-build/tests/module-capabilities.test.js && node .module-test-build/tests/privacy-policy.test.js && node .module-test-build/tests/help-context.test.js",
|
||||||
"test:module-permutations": "node scripts/test-module-permutations.mjs",
|
"test:module-permutations": "node scripts/test-module-permutations.mjs",
|
||||||
"test:mail-components": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/mail-components.test.js",
|
"test:mail-components": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/mail-components.test.js",
|
||||||
"test:resource-access": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/resource-access-explanation.test.js",
|
"test:resource-access": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/resource-access-explanation.test.js",
|
||||||
|
|||||||
@@ -9,20 +9,23 @@ import { usePlatformModules } from "../platform/ModuleContext";
|
|||||||
import type { PlatformWebModule } from "../types";
|
import type { PlatformWebModule } from "../types";
|
||||||
import { helpContextForPathname, helpQueryForContext, type HelpContext } from "../utils/helpContext";
|
import { helpContextForPathname, helpQueryForContext, type HelpContext } from "../utils/helpContext";
|
||||||
import { usePlatformLanguage } from "../i18n/LanguageContext";
|
import { usePlatformLanguage } from "../i18n/LanguageContext";
|
||||||
|
import type { AuthInfo } from "../types";
|
||||||
|
import { hasAnyScope } from "../utils/permissions";
|
||||||
|
|
||||||
const EXTERNAL_DOCS_BASE_URL = "https://govoplan.add-ideas.de";
|
const EXTERNAL_DOCS_BASE_URL = "https://govoplan.add-ideas.de";
|
||||||
|
|
||||||
export default function HelpMenu() {
|
export default function HelpMenu({ auth }: { auth: AuthInfo | null }) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [aboutOpen, setAboutOpen] = useState(false);
|
const [aboutOpen, setAboutOpen] = useState(false);
|
||||||
const [contextOpen, setContextOpen] = useState(false);
|
const [contextOpen, setContextOpen] = useState(false);
|
||||||
const wrapRef = useRef<HTMLDivElement>(null);
|
const wrapRef = useRef<HTMLDivElement>(null);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useGuardedNavigate();
|
const navigate = useGuardedNavigate();
|
||||||
const helpContext = helpContextForPathname(location.pathname);
|
const helpContext = helpContextForPathname(location.pathname, location.search);
|
||||||
const modules = usePlatformModules();
|
const modules = usePlatformModules();
|
||||||
const { translateText } = usePlatformLanguage();
|
const { translateText } = usePlatformLanguage();
|
||||||
const docsAvailable = modules.some((module) => module.id === "docs" && module.routes?.some((route) => route.path === "/docs"));
|
const docsAvailable = modules.some((module) => module.id === "docs" && module.routes?.some((route) => route.path === "/docs"));
|
||||||
|
const adminDocsAvailable = hasAnyScope(auth, ["docs:documentation:admin", "system:settings:read", "admin:settings:read"]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function openContextHelp(event: KeyboardEvent) {
|
function openContextHelp(event: KeyboardEvent) {
|
||||||
@@ -53,6 +56,7 @@ export default function HelpMenu() {
|
|||||||
|
|
||||||
function openDocs(type: "user" | "admin") {
|
function openDocs(type: "user" | "admin") {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
setContextOpen(false);
|
||||||
if (!docsAvailable) {
|
if (!docsAvailable) {
|
||||||
window.open(externalDocsUrl(type, helpContext), "_blank", "noopener,noreferrer");
|
window.open(externalDocsUrl(type, helpContext), "_blank", "noopener,noreferrer");
|
||||||
return;
|
return;
|
||||||
@@ -87,15 +91,17 @@ export default function HelpMenu() {
|
|||||||
<button className="dropdown-item" onClick={() => openDocs("user")} title={docsAvailable ? translateText("i18n:govoplan-core.open_user_documentation.084af515") : "Open hosted user documentation"}>
|
<button className="dropdown-item" onClick={() => openDocs("user")} title={docsAvailable ? translateText("i18n:govoplan-core.open_user_documentation.084af515") : "Open hosted user documentation"}>
|
||||||
<BookOpen size={16} /> {translateText("i18n:govoplan-core.user_docs.1e38e8d3")}
|
<BookOpen size={16} /> {translateText("i18n:govoplan-core.user_docs.1e38e8d3")}
|
||||||
</button>
|
</button>
|
||||||
|
{adminDocsAvailable &&
|
||||||
<button className="dropdown-item" onClick={() => openDocs("admin")} title={docsAvailable ? translateText("i18n:govoplan-core.open_admin_documentation.6adbdae3") : "Open hosted admin documentation"}>
|
<button className="dropdown-item" onClick={() => openDocs("admin")} title={docsAvailable ? translateText("i18n:govoplan-core.open_admin_documentation.6adbdae3") : "Open hosted admin documentation"}>
|
||||||
<BookOpen size={16} /> {translateText("i18n:govoplan-core.admin_docs.bf504a56")}
|
<BookOpen size={16} /> {translateText("i18n:govoplan-core.admin_docs.bf504a56")}
|
||||||
</button>
|
</button>
|
||||||
|
}
|
||||||
<hr />
|
<hr />
|
||||||
<a className="dropdown-item" href="https://git.add-ideas.de/add-ideas" target="_blank" rel="noreferrer"><GitBranch size={16} /> i18n:govoplan-core.gitlab.9a9b09d9</a>
|
<a className="dropdown-item" href="https://git.add-ideas.de/add-ideas" target="_blank" rel="noreferrer"><GitBranch size={16} /> i18n:govoplan-core.gitlab.9a9b09d9</a>
|
||||||
<button className="dropdown-item" onClick={() => {setAboutOpen(true);setOpen(false);}}><Info size={16} /> {translateText("i18n:govoplan-core.about.6b21fb79")}</button>
|
<button className="dropdown-item" onClick={() => {setAboutOpen(true);setOpen(false);}}><Info size={16} /> {translateText("i18n:govoplan-core.about.6b21fb79")}</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{contextOpen && <ContextHelpModal context={helpContext} onClose={() => setContextOpen(false)} />}
|
{contextOpen && <ContextHelpModal context={helpContext} onOpenDocs={() => openDocs("user")} onClose={() => setContextOpen(false)} />}
|
||||||
{aboutOpen && <AboutModal modules={modules} onClose={() => setAboutOpen(false)} />}
|
{aboutOpen && <AboutModal modules={modules} onClose={() => setAboutOpen(false)} />}
|
||||||
</div>);
|
</div>);
|
||||||
|
|
||||||
@@ -106,7 +112,7 @@ function externalDocsUrl(type: "user" | "admin", context: HelpContext): string {
|
|||||||
return `${EXTERNAL_DOCS_BASE_URL}/?${params.toString()}`;
|
return `${EXTERNAL_DOCS_BASE_URL}/?${params.toString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContextHelpModal({ context, onClose }: {context: HelpContext;onClose: () => void;}) {
|
function ContextHelpModal({ context, onOpenDocs, onClose }: {context: HelpContext;onOpenDocs: () => void;onClose: () => void;}) {
|
||||||
const { translateText } = usePlatformLanguage();
|
const { translateText } = usePlatformLanguage();
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -123,6 +129,7 @@ function ContextHelpModal({ context, onClose }: {context: HelpContext;onClose: (
|
|||||||
<div className="help-panel-section">
|
<div className="help-panel-section">
|
||||||
<h3>{translateText("i18n:govoplan-core.next_actions.7b09055a")}</h3>
|
<h3>{translateText("i18n:govoplan-core.next_actions.7b09055a")}</h3>
|
||||||
<p className="muted">{translateText("i18n:govoplan-core.the_first_guided_help_content_can_cover_campaign.14a6bd8a")}</p>
|
<p className="muted">{translateText("i18n:govoplan-core.the_first_guided_help_content_can_cover_campaign.14a6bd8a")}</p>
|
||||||
|
<Button onClick={onOpenDocs}><BookOpen size={16} /> {translateText("i18n:govoplan-core.open_user_documentation.084af515")}</Button>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>);
|
</Dialog>);
|
||||||
|
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ export default function Titlebar({ settings, auth, onAuthChange, maintenanceMode
|
|||||||
<div className="titlebar-spacer" />
|
<div className="titlebar-spacer" />
|
||||||
|
|
||||||
<LanguageMenu />
|
<LanguageMenu />
|
||||||
<HelpMenu />
|
<HelpMenu auth={auth} />
|
||||||
|
|
||||||
{auth && notificationsAvailable &&
|
{auth && notificationsAvailable &&
|
||||||
<button className="titlebar-icon-link titlebar-notification-button" onClick={openNotificationCenter} title={translateText("i18n:govoplan-core.notifications.753a22b2")} aria-label={translateText("i18n:govoplan-core.notifications.753a22b2")}>
|
<button className="titlebar-icon-link titlebar-notification-button" onClick={openNotificationCenter} title={translateText("i18n:govoplan-core.notifications.753a22b2")} aria-label={translateText("i18n:govoplan-core.notifications.753a22b2")}>
|
||||||
|
|||||||
@@ -31,16 +31,24 @@ const topLevelContexts: Record<string, Omit<HelpContext, "route">> = {
|
|||||||
campaigns: { id: "campaigns.list", title: "i18n:govoplan-core.campaigns.01a23a28" },
|
campaigns: { id: "campaigns.list", title: "i18n:govoplan-core.campaigns.01a23a28" },
|
||||||
templates: { id: "templates.list", title: "i18n:govoplan-core.templates.f25b700e" },
|
templates: { id: "templates.list", title: "i18n:govoplan-core.templates.f25b700e" },
|
||||||
files: { id: "files.list", title: "i18n:govoplan-core.files.6ce6c512" },
|
files: { id: "files.list", title: "i18n:govoplan-core.files.6ce6c512" },
|
||||||
|
mail: { id: "mail.list", title: "Mail" },
|
||||||
"address-book": { id: "address-book.list", title: "i18n:govoplan-core.address_book.f6327f59" },
|
"address-book": { id: "address-book.list", title: "i18n:govoplan-core.address_book.f6327f59" },
|
||||||
reports: { id: "reports.list", title: "i18n:govoplan-core.reports.88bc3fe3" },
|
reports: { id: "reports.list", title: "i18n:govoplan-core.reports.88bc3fe3" },
|
||||||
settings: { id: "app.settings", title: "i18n:govoplan-core.settings.c7f73bb5" },
|
settings: { id: "app.settings", title: "i18n:govoplan-core.settings.c7f73bb5" },
|
||||||
admin: { id: "app.admin", title: "i18n:govoplan-core.admin.4e7afebc" }
|
admin: { id: "app.admin", title: "i18n:govoplan-core.admin.4e7afebc" }
|
||||||
};
|
};
|
||||||
|
|
||||||
export function helpContextForPathname(pathname: string): HelpContext {
|
export function helpContextForPathname(pathname: string, search = ""): HelpContext {
|
||||||
const route = pathname || "/";
|
const route = pathname || "/";
|
||||||
const segments = route.split("/").filter(Boolean);
|
const segments = route.split("/").filter(Boolean);
|
||||||
|
|
||||||
|
if (segments[0] === "settings") {
|
||||||
|
const section = new URLSearchParams(search).get("section") || "";
|
||||||
|
if (section === "mail-profiles") {
|
||||||
|
return { id: "mail.profiles", title: "Mail profiles", route: `${route}${search}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (segments[0] === "campaigns" && segments[1]) {
|
if (segments[0] === "campaigns" && segments[1]) {
|
||||||
if (!segments[2]) return { id: "campaign.overview", title: "i18n:govoplan-core.campaign_overview.43c3d159", route };
|
if (!segments[2]) return { id: "campaign.overview", title: "i18n:govoplan-core.campaign_overview.43c3d159", route };
|
||||||
if (segments[2] === "wizard") {
|
if (segments[2] === "wizard") {
|
||||||
|
|||||||
18
webui/tests/help-context.test.ts
Normal file
18
webui/tests/help-context.test.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { helpContextForPathname } from "../src/utils/helpContext";
|
||||||
|
|
||||||
|
function assertEqual(actual: string, expected: string, message: string): void {
|
||||||
|
if (actual !== expected) throw new Error(`${message}: expected ${expected}, got ${actual}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEqual(helpContextForPathname("/files").id, "files.list", "Files context");
|
||||||
|
assertEqual(helpContextForPathname("/mail").id, "mail.list", "Mail context");
|
||||||
|
assertEqual(
|
||||||
|
helpContextForPathname("/settings", "?section=mail-profiles").id,
|
||||||
|
"mail.profiles",
|
||||||
|
"Mail profile settings context"
|
||||||
|
);
|
||||||
|
assertEqual(
|
||||||
|
helpContextForPathname("/campaigns/campaign-1/attachments").id,
|
||||||
|
"campaign.attachments",
|
||||||
|
"Campaign attachment context"
|
||||||
|
);
|
||||||
@@ -17,7 +17,9 @@
|
|||||||
"include": [
|
"include": [
|
||||||
"tests/module-capabilities.test.ts",
|
"tests/module-capabilities.test.ts",
|
||||||
"tests/privacy-policy.test.ts",
|
"tests/privacy-policy.test.ts",
|
||||||
|
"tests/help-context.test.ts",
|
||||||
"src/platform/moduleLogic.ts",
|
"src/platform/moduleLogic.ts",
|
||||||
|
"src/utils/helpContext.ts",
|
||||||
"src/features/privacy/policyLogic.ts",
|
"src/features/privacy/policyLogic.ts",
|
||||||
"src/types.ts"
|
"src/types.ts"
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user