fix(ui): move widget and page documentation into headings

Verified with the coordinated workspace changes by devkit full run
2026-09-08T225814-186389-0000-3e3ed7cd (all seven phases passed).
This shared UI pass does not mark the individual module reviews complete.
This commit is contained in:
2026-09-09 02:04:19 +02:00
parent 1d41cdbbe2
commit 129f2a4c73
10 changed files with 50 additions and 39 deletions
+9 -2
View File
@@ -115,7 +115,10 @@ DOCUMENTATION = (
id="scheduling.workspace-layout", id="scheduling.workspace-layout",
title="Scheduling workspace layout", title="Scheduling workspace layout",
summary="Find workspace actions and read consistently arranged content.", summary="Find workspace actions and read consistently arranged content.",
body="The workspace always keeps Reload and New scheduling request at the upper right, including empty, detail, and editor states. Reload sits immediately before New. Creation remains visible but disabled while saving or without creation/write permission; it never disappears inside the request-list card. An open editor keeps its own Save and Discard actions and the unsaved-change guard applies before switching requests. Administrators grant existing Scheduling permissions; this layout adds no permission or automatic invitation or Calendar action.", body="Documentation books sit immediately beside the visible heading or contextual label for "
"Scheduling, the selected request and public participation details, not among operational "
"action buttons. Field help remains beside its label. "
"The workspace always keeps Reload and New scheduling request at the upper right, including empty, detail, and editor states. Reload sits immediately before New. Creation remains visible but disabled while saving or without creation/write permission; it never disappears inside the request-list card. An open editor keeps its own Save and Discard actions and the unsaved-change guard applies before switching requests. Administrators grant existing Scheduling permissions; this layout adds no permission or automatic invitation or Calendar action.",
layer="static", layer="static",
documentation_types=("user", "admin"), documentation_types=("user", "admin"),
audience=("user", "module_admin", "operator"), audience=("user", "module_admin", "operator"),
@@ -123,7 +126,11 @@ DOCUMENTATION = (
translations={"de": { translations={"de": {
"title": "Terminfindung: Aufbau des Arbeitsbereichs", "title": "Terminfindung: Aufbau des Arbeitsbereichs",
"summary": "Arbeitsbereichsaktionen finden und einheitlich angeordnete Inhalte lesen.", "summary": "Arbeitsbereichsaktionen finden und einheitlich angeordnete Inhalte lesen.",
"body": "Der Arbeitsbereich zeigt Neu laden und Neue Terminanfrage immer oben rechts, auch in leerem Zustand, Detailansicht und Bearbeitung. Neu laden steht unmittelbar vor Neu. Das Anlegen bleibt beim Speichern oder ohne Schreibrecht sichtbar, aber deaktiviert; es verschwindet nicht innerhalb der Anfragekartenleiste. Ein geöffneter Editor behält Speichern und Verwerfen; beim Wechsel schützt die Rückfrage ungespeicherte Änderungen. Administratoren vergeben bestehende Scheduling-Rechte; dieses Layout fügt weder Rechte noch automatische Einladungen oder Kalenderaktionen hinzu.", "body": "Dokumentationsbücher stehen unmittelbar neben der sichtbaren Überschrift oder "
"Kontextbezeichnung für Terminfindung, die ausgewählte Anfrage und öffentliche "
"Teilnahmedetails, nicht zwischen ausführbaren Aktionsschaltflächen. Feldhilfe bleibt neben "
"der Feldbezeichnung. "
"Der Arbeitsbereich zeigt Neu laden und Neue Terminanfrage immer oben rechts, auch in leerem Zustand, Detailansicht und Bearbeitung. Neu laden steht unmittelbar vor Neu. Das Anlegen bleibt beim Speichern oder ohne Schreibrecht sichtbar, aber deaktiviert; es verschwindet nicht innerhalb der Anfragekartenleiste. Ein geöffneter Editor behält Speichern und Verwerfen; beim Wechsel schützt die Rückfrage ungespeicherte Änderungen. Administratoren vergeben bestehende Scheduling-Rechte; dieses Layout fügt weder Rechte noch automatische Einladungen oder Kalenderaktionen hinzu.",
}}, }},
), ),
DocumentationTopic( DocumentationTopic(
+11
View File
@@ -1,12 +1,23 @@
from __future__ import annotations from __future__ import annotations
import unittest import unittest
from pathlib import Path
from govoplan_core.core.modules import ModuleManifest from govoplan_core.core.modules import ModuleManifest
from govoplan_scheduling.backend.manifest import get_manifest from govoplan_scheduling.backend.manifest import get_manifest
class SchedulingManifestTests(unittest.TestCase): class SchedulingManifestTests(unittest.TestCase):
def test_widget_documentation_is_contributed_for_its_dashboard_title(self) -> None:
root = Path(__file__).resolve().parents[1]
contribution = (root / "webui/src/module.ts").read_text(encoding="utf-8")
widget = (root / "webui/src/features/scheduling/SchedulingRequestsWidget.tsx").read_text(encoding="utf-8")
self.assertIn(
'documentation: { topicId: "scheduling.find-and-decide-meeting-time", documentationType: "user" }',
contribution,
)
self.assertNotIn("DocumentationHelpLink", widget)
def test_all_static_topics_have_complete_german_content(self) -> None: def test_all_static_topics_have_complete_german_content(self) -> None:
manifest = get_manifest() manifest = get_manifest()
for topic in manifest.documentation: for topic in manifest.documentation:
@@ -201,7 +201,8 @@ assert.match(enrollmentPage, /topicId: "scheduling\.public-self-enrollment"/);
assert.doesNotMatch(enrollmentPage, /window\.(?:alert|confirm)\(/); assert.doesNotMatch(enrollmentPage, /window\.(?:alert|confirm)\(/);
assert.doesNotMatch(enrollmentPage, /(?:localStorage|sessionStorage).*(?:token|proof)|(?:token|proof).*(?:localStorage|sessionStorage)/); assert.doesNotMatch(enrollmentPage, /(?:localStorage|sessionStorage).*(?:token|proof)|(?:token|proof).*(?:localStorage|sessionStorage)/);
assert.match(widget, /DocumentationHelpLink/); assert.doesNotMatch(widget, /DocumentationHelpLink/, "widget help belongs to the host card title, not a detached footer");
assert.match(moduleSource, /documentation: \{ topicId: "scheduling\.find-and-decide-meeting-time", documentationType: "user" \}/);
assert.match(widget, /to: `\/scheduling\?request_id=\$\{encodeURIComponent\(request\.id\)\}`/); assert.match(widget, /to: `\/scheduling\?request_id=\$\{encodeURIComponent\(request\.id\)\}`/);
assert.match(widget, /label=\{request\.status === "collecting" \? I18N\.open : I18N\.draft\}/); assert.match(widget, /label=\{request\.status === "collecting" \? I18N\.open : I18N\.draft\}/);
assert.doesNotMatch(widget, /Loading scheduling requests|Open scheduling|No scheduling requests are awaiting responses/); assert.doesNotMatch(widget, /Loading scheduling requests|Open scheduling|No scheduling requests are awaiting responses/);
@@ -202,11 +202,11 @@ export default function SchedulingEnrollmentPage({ settings, auth }: SchedulingE
{enrollment ? ( {enrollment ? (
<form className="scheduling-public-content" onSubmit={submit}> <form className="scheduling-public-content" onSubmit={submit}>
<Card <Card
title={enrollment.title} title={enrollment.title} titleHelp={<DocumentationHelpLink reference={{
actions={<DocumentationHelpLink reference={{
topicId: "scheduling.public-self-enrollment", topicId: "scheduling.public-self-enrollment",
documentationType: "user" documentationType: "user"
}} />}> }} />}
>
{enrollment.description ? <p>{enrollment.description}</p> : null} {enrollment.description ? <p>{enrollment.description}</p> : null}
<dl className="scheduling-public-summary"> <dl className="scheduling-public-summary">
{enrollment.deadline_at ? <><dt>{I18N.deadline}</dt><dd>{formatDateTime(enrollment.deadline_at)}</dd></> : null} {enrollment.deadline_at ? <><dt>{I18N.deadline}</dt><dd>{formatDateTime(enrollment.deadline_at)}</dd></> : null}
@@ -27,7 +27,7 @@ import { ContentGrid, FormGrid,
DocumentationHelpLink, DocumentationHelpLink,
FormField, FormField,
MetricCard, MetricCard,
PageTitle, TextWithHelp,
PasswordField, PasswordField,
PeoplePicker, PeoplePicker,
formatDateTime, formatDateTime,
@@ -939,7 +939,9 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
variant="collection" variant="collection"
refreshable refreshable
reloadAction={{ onReload: () => void loadRequests(selected?.id), loading: loading || saving, disabledReason: saving ? I18N.saving : undefined, label: I18N.refresh }} reloadAction={{ onReload: () => void loadRequests(selected?.id), loading: loading || saving, disabledReason: saving ? I18N.saving : undefined, label: I18N.refresh }}
helpAction={<DocumentationHelpLink reference={{ topicId: "scheduling.find-and-decide-meeting-time", documentationType: "user" }} />} title="Scheduling"
titleLevel={1}
titleHelp={<DocumentationHelpLink reference={{ topicId: "scheduling.find-and-decide-meeting-time", documentationType: "user" }} />}
createAction={<Button type="button" variant="primary" onClick={beginCreate} disabled={!canCreateOrWrite || saving} disabledReason={saving ? I18N.saving : !canCreateOrWrite ? I18N.createPermissionRequired : undefined}><Plus aria-hidden="true" size={16} /> {I18N.newRequest}</Button>} createAction={<Button type="button" variant="primary" onClick={beginCreate} disabled={!canCreateOrWrite || saving} disabledReason={saving ? I18N.saving : !canCreateOrWrite ? I18N.createPermissionRequired : undefined}><Plus aria-hidden="true" size={16} /> {I18N.newRequest}</Button>}
/> />
<div className="scheduling-workspace-layout"> <div className="scheduling-workspace-layout">
@@ -985,15 +987,12 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
contextActions={<div className="scheduling-page-title"> contextActions={<div className="scheduling-page-title">
<CalendarCheck aria-hidden="true" size={20} /> <CalendarCheck aria-hidden="true" size={20} />
<div> <div>
<PageTitle>{editorMode === "create" ? I18N.newRequest : I18N.edit}</PageTitle> <TextWithHelp as="div" help={<DocumentationHelpLink
reference={{ topicId: "scheduling.find-and-decide-meeting-time", documentationType: "user" }}
/>}><h2>{editorMode === "create" ? I18N.newRequest : I18N.edit}</h2></TextWithHelp>
<p>{editorMode === "create" ? I18N.addRequest : title}</p> <p>{editorMode === "create" ? I18N.addRequest : title}</p>
</div> </div>
</div>} </div>}
helpAction={<DocumentationHelpLink
reference={{
topicId: "scheduling.find-and-decide-meeting-time",
documentationType: "user"
}} />}
discardAction={{ label: I18N.discard, type: "button", onClick: discardEditor }} discardAction={{ label: I18N.discard, type: "button", onClick: discardEditor }}
saveAction={{ saveAction={{
label: <><Save aria-hidden="true" size={16} /> {saving ? I18N.saving : I18N.save}</>, label: <><Save aria-hidden="true" size={16} /> {saving ? I18N.saving : I18N.save}</>,
@@ -1171,14 +1170,14 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
statusLabel: stage.current ? requestStatusLabel(selected, actor) : undefined statusLabel: stage.current ? requestStatusLabel(selected, actor) : undefined
}))} /> }))} />
<Card <Card
title={selected.title} title={selected.title} titleHelp={<DocumentationHelpLink
actions={(
<div className="scheduling-actions">
<DocumentationHelpLink
reference={{ reference={{
topicId: "scheduling.find-and-decide-meeting-time", topicId: "scheduling.find-and-decide-meeting-time",
documentationType: "user" documentationType: "user"
}} /> }} />}
actions={(
<div className="scheduling-actions">
{canEditSelected ? ( {canEditSelected ? (
<Button <Button
type="button" type="button"
@@ -2394,11 +2393,11 @@ function SelfEnrollmentLinksCard({
return ( return (
<Card <Card
title={I18N.selfEnrollmentLinks} title={I18N.selfEnrollmentLinks} titleHelp={<DocumentationHelpLink reference={{
actions={<DocumentationHelpLink reference={{
topicId: "scheduling.public-self-enrollment", topicId: "scheduling.public-self-enrollment",
documentationType: "user" documentationType: "user"
}} />}> }} />}
>
<p className="scheduling-capability-note"> <p className="scheduling-capability-note">
{I18N.selfEnrollmentLinksHelp} {I18N.selfEnrollmentLinksHelp}
</p> </p>
@@ -170,14 +170,12 @@ export default function SchedulingPublicPage({ settings, auth }: SchedulingPubli
{!response && !loading && ( {!response && !loading && (
<Card <Card
title={I18N.accessDetails} title={I18N.accessDetails} titleHelp={<DocumentationHelpLink
actions={(
<DocumentationHelpLink
reference={{ reference={{
topicId: "scheduling.find-and-decide-meeting-time", topicId: "scheduling.find-and-decide-meeting-time",
documentationType: "user" documentationType: "user"
}} /> }} />}
)}> >
<form className="scheduling-public-access-form" onSubmit={openRequest}> <form className="scheduling-public-access-form" onSubmit={openRequest}>
<p className="muted">{I18N.accessHelp}</p> <p className="muted">{I18N.accessHelp}</p>
{accessAttempted && error && <DismissibleAlert tone="danger">{error}</DismissibleAlert>} {accessAttempted && error && <DismissibleAlert tone="danger">{error}</DismissibleAlert>}
@@ -213,14 +211,12 @@ export default function SchedulingPublicPage({ settings, auth }: SchedulingPubli
{response && ( {response && (
<form className="scheduling-public-content" onSubmit={saveResponse}> <form className="scheduling-public-content" onSubmit={saveResponse}>
<Card <Card
title={response.title} title={response.title} titleHelp={<DocumentationHelpLink
actions={(
<DocumentationHelpLink
reference={{ reference={{
topicId: "scheduling.find-and-decide-meeting-time", topicId: "scheduling.find-and-decide-meeting-time",
documentationType: "user" documentationType: "user"
}} /> }} />}
)}> >
{response.description && <p className="scheduling-public-description">{response.description}</p>} {response.description && <p className="scheduling-public-description">{response.description}</p>}
<dl className="scheduling-public-summary"> <dl className="scheduling-public-summary">
{response.location && <><dt>i18n:govoplan-scheduling.location.d219c681</dt><dd>{response.location}</dd></>} {response.location && <><dt>i18n:govoplan-scheduling.location.d219c681</dt><dd>{response.location}</dd></>}
@@ -4,7 +4,6 @@ import { Link } from "react-router";
import { import {
DashboardWidgetList, DashboardWidgetList,
DismissibleAlert, DismissibleAlert,
DocumentationHelpLink,
LoadingFrame, LoadingFrame,
StatusBadge, StatusBadge,
formatDateTime, formatDateTime,
@@ -80,11 +79,6 @@ export default function SchedulingRequestsWidget({
}))} }))}
/> />
<div className="dashboard-contribution-footer"> <div className="dashboard-contribution-footer">
<DocumentationHelpLink
reference={{
topicId: "scheduling.find-and-decide-meeting-time",
documentationType: "user"
}} />
<Link className="btn btn-secondary" to="/scheduling"> <Link className="btn btn-secondary" to="/scheduling">
{I18N.openScheduling} {I18N.openScheduling}
</Link> </Link>
+2
View File
@@ -1,5 +1,6 @@
export const generatedTranslations = { export const generatedTranslations = {
en: { en: {
"Scheduling": "Scheduling",
"i18n:govoplan-scheduling.self_enrollment.access": "Open self-enrollment", "i18n:govoplan-scheduling.self_enrollment.access": "Open self-enrollment",
"i18n:govoplan-scheduling.self_enrollment.bind_account": "Bind this enrollment to my signed-in account", "i18n:govoplan-scheduling.self_enrollment.bind_account": "Bind this enrollment to my signed-in account",
"i18n:govoplan-scheduling.self_enrollment.bind_help": "Account binding requires confirmation and lets you manage this response from Scheduling.", "i18n:govoplan-scheduling.self_enrollment.bind_help": "Account binding requires confirmation and lets you manage this response from Scheduling.",
@@ -219,6 +220,7 @@ export const generatedTranslations = {
"i18n:govoplan-scheduling.your_response_has_been_recorded.b855088d": "Your response has been recorded." "i18n:govoplan-scheduling.your_response_has_been_recorded.b855088d": "Your response has been recorded."
}, },
de: { de: {
"Scheduling": "Terminplanung",
"i18n:govoplan-scheduling.self_enrollment.access": "Selbstanmeldung öffnen", "i18n:govoplan-scheduling.self_enrollment.access": "Selbstanmeldung öffnen",
"i18n:govoplan-scheduling.self_enrollment.bind_account": "Diese Anmeldung mit meinem angemeldeten Konto verknüpfen", "i18n:govoplan-scheduling.self_enrollment.bind_account": "Diese Anmeldung mit meinem angemeldeten Konto verknüpfen",
"i18n:govoplan-scheduling.self_enrollment.bind_help": "Die Kontoverknüpfung muss bestätigt werden und ermöglicht die Verwaltung dieser Antwort in der Terminplanung.", "i18n:govoplan-scheduling.self_enrollment.bind_help": "Die Kontoverknüpfung muss bestätigt werden und ermöglicht die Verwaltung dieser Antwort in der Terminplanung.",
+1
View File
@@ -18,6 +18,7 @@ const schedulingDashboardWidgets: DashboardWidgetsUiCapability = {
id: "scheduling.open-requests", id: "scheduling.open-requests",
surfaceId: "scheduling.widget.open-requests", surfaceId: "scheduling.widget.open-requests",
title: "Scheduling requests", title: "Scheduling requests",
documentation: { topicId: "scheduling.find-and-decide-meeting-time", documentationType: "user" },
description: "Open scheduling polls and their response progress.", description: "Open scheduling polls and their response progress.",
moduleId: "scheduling", moduleId: "scheduling",
category: "Planning", category: "Planning",
+1 -1
View File
@@ -182,7 +182,7 @@
min-width: 0; min-width: 0;
} }
.scheduling-page-title h1 { .scheduling-page-title h2 {
margin: 0; margin: 0;
color: var(--text-strong); color: var(--text-strong);
font-size: 19px; font-size: 19px;