3 Commits
Author SHA1 Message Date
zemion 129f2a4c73 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.
2026-09-09 02:04:19 +02:00
zemion 1d41cdbbe2 fix(packaging): expose immutable WebUI Git package for v0.1.22
Module Package Release / publish-packages (push) Successful in 11s
2026-09-08 02:06:11 +02:00
zemion 2081bd18c3 Release govoplan-scheduling v0.1.22: unify interface contracts and documentation 2026-09-08 01:32:52 +02:00
15 changed files with 140 additions and 104 deletions
+18
View File
@@ -304,3 +304,21 @@ inline help. The only intentional omissions are:
There are no other authorized Scheduling omissions. Inline help remains
controlled by the user's shared interface setting; hiding it does not remove
accessible labels.
## Git-source WebUI package
The repository root exposes `@govoplan/scheduling-webui` for Git-tagged release
dependencies. It mirrors the owning `webui/package.json` version, public
TypeScript/CSS exports and peer requirements, with entry paths under
`webui/src`. Consumers provide the shared Core/React peers; the facade runs no
development or install scripts. The source archive contains `webui/src`, this
README and any repository license file. Run module development checks from `webui/`; Python
installation remains governed by `pyproject.toml`.
Das Repository stellt `@govoplan/scheduling-webui` am Wurzelpfad für versionierte
Git-Abhängigkeiten bereit. Version, öffentliche TypeScript-/CSS-Exporte und
Peer-Anforderungen entsprechen `webui/package.json`; die Einstiegspfade liegen
unter `webui/src`. Gemeinsame Core-/React-Peers stellt die einbindende Anwendung
bereit. Die Fassade führt keine Entwicklungs- oder Installationsskripte aus.
Entwicklungsprüfungen bleiben in `webui/`, die Python-Installation weiterhin in
`pyproject.toml` definiert.
+36
View File
@@ -0,0 +1,36 @@
{
"name": "@govoplan/scheduling-webui",
"version": "0.1.22",
"private": true,
"type": "module",
"main": "webui/src/index.ts",
"module": "webui/src/index.ts",
"types": "webui/src/index.ts",
"exports": {
".": {
"types": "./webui/src/index.ts",
"import": "./webui/src/index.ts"
},
"./styles/scheduling.css": "./webui/src/styles/scheduling.css"
},
"peerDependencies": {
"@govoplan/core-webui": "^0.1.45",
"lucide-react": "^1.23.0",
"react": ">=19.2.7 <20",
"react-dom": ">=19.2.7 <20",
"react-router": ">=8.3.0 <9",
"@vitejs/plugin-react": "^5.2.0",
"typescript": "^5.7.2",
"vite": "^7.3.6"
},
"peerDependenciesMeta": {
"@govoplan/core-webui": {
"optional": true
}
},
"files": [
"webui/src",
"README.md",
"LICENSE"
]
}
+2 -2
View File
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
[project]
name = "govoplan-scheduling"
version = "0.1.21"
version = "0.1.22"
description = "GovOPlaN meeting scheduling and Terminfindung module seed."
readme = "README.md"
requires-python = ">=3.12"
license = { file = "LICENSE" }
authors = [{ name = "GovOPlaN" }]
dependencies = [
"govoplan-core>=0.1.18",
"govoplan-core>=0.1.45",
"govoplan-poll>=0.1.18",
]
+1 -1
View File
@@ -2,4 +2,4 @@
__all__ = ["__version__"]
__version__ = "0.1.21"
__version__ = "0.1.22"
+23 -1
View File
@@ -51,7 +51,7 @@ from govoplan_scheduling.backend.dsar_provider import SCHEDULING_DSAR_CAPABILITY
MODULE_ID = "scheduling"
MODULE_NAME = "Scheduling"
MODULE_VERSION = "0.1.21"
MODULE_VERSION = "0.1.22"
READ_SCOPE = "scheduling:schedule:read"
WRITE_SCOPE = "scheduling:schedule:write"
ADMIN_SCOPE = "scheduling:schedule:admin"
@@ -111,6 +111,28 @@ ROLE_TEMPLATES = (
)
DOCUMENTATION = (
DocumentationTopic(
id="scheduling.workspace-layout",
title="Scheduling workspace layout",
summary="Find workspace actions and read consistently arranged content.",
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",
documentation_types=("user", "admin"),
audience=("user", "module_admin", "operator"),
order=5,
translations={"de": {
"title": "Terminfindung: Aufbau des Arbeitsbereichs",
"summary": "Arbeitsbereichsaktionen finden und einheitlich angeordnete Inhalte lesen.",
"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(
id="scheduling.module-boundary",
title="Scheduling module boundary",
+11
View File
@@ -1,12 +1,23 @@
from __future__ import annotations
import unittest
from pathlib import Path
from govoplan_core.core.modules import ModuleManifest
from govoplan_scheduling.backend.manifest import get_manifest
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:
manifest = get_manifest()
for topic in manifest.documentation:
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@govoplan/scheduling-webui",
"version": "0.1.21",
"version": "0.1.22",
"private": true,
"type": "module",
"main": "src/index.ts",
@@ -18,7 +18,7 @@
"test:ui-structure": "node scripts/test-scheduling-page-structure.mjs"
},
"peerDependencies": {
"@govoplan/core-webui": "^0.1.18",
"@govoplan/core-webui": "^0.1.45",
"lucide-react": "^1.23.0",
"react": ">=19.2.7 <20",
"react-dom": ">=19.2.7 <20",
@@ -26,7 +26,12 @@ assert.match(page, /ActionBlockerHint,[\s\S]*DocumentationHelpLink,[\s\S]*StageR
assert.match(page, /className="scheduling-workspace-layout"/);
assert.match(page, /<aside className="scheduling-request-sidebar">/);
assert.match(page, /<section className="scheduling-main-panel">/);
assert.match(page, /title=\{I18N\.requests\}[\s\S]*<Plus aria-hidden="true" size=\{16\} \/> \{I18N\.add\}/);
const workspaceBar = page.slice(page.indexOf('<section className="scheduling-workspace">'), page.indexOf('<div className="scheduling-workspace-layout">'));
assert.match(workspaceBar, /scope="workspace"[\s\S]*variant="collection"[\s\S]*reloadAction=/);
assert.match(workspaceBar, /createAction=\{<Button[\s\S]*disabled=\{!canCreateOrWrite \|\| saving\}[\s\S]*I18N\.newRequest/);
assert.doesNotMatch(workspaceBar, /createAction=\{canCreateOrWrite \?/);
assert.match(workspaceBar, /!canCreateOrWrite \? I18N\.createPermissionRequired : undefined/);
assert.doesNotMatch(page, /scheduling-sidebar-actions/);
assert.match(page, /title=\{I18N\.myRequests\}/);
assert.match(page, /title=\{I18N\.invitedRequests\}/);
assert.ok(page.indexOf("title={I18N.myRequests}") < page.indexOf("title={I18N.invitedRequests}"));
@@ -196,7 +201,8 @@ assert.match(enrollmentPage, /topicId: "scheduling\.public-self-enrollment"/);
assert.doesNotMatch(enrollmentPage, /window\.(?:alert|confirm)\(/);
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, /label=\{request\.status === "collecting" \? I18N\.open : I18N\.draft\}/);
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 ? (
<form className="scheduling-public-content" onSubmit={submit}>
<Card
title={enrollment.title}
actions={<DocumentationHelpLink reference={{
title={enrollment.title} titleHelp={<DocumentationHelpLink reference={{
topicId: "scheduling.public-self-enrollment",
documentationType: "user"
}} />}>
}} />}
>
{enrollment.description ? <p>{enrollment.description}</p> : null}
<dl className="scheduling-public-summary">
{enrollment.deadline_at ? <><dt>{I18N.deadline}</dt><dd>{formatDateTime(enrollment.deadline_at)}</dd></> : null}
@@ -27,7 +27,7 @@ import { ContentGrid, FormGrid,
DocumentationHelpLink,
FormField,
MetricCard,
PageTitle,
TextWithHelp,
PasswordField,
PeoplePicker,
formatDateTime,
@@ -140,6 +140,7 @@ const I18N = {
actions: "i18n:govoplan-core.actions.c3cd636a",
add: "i18n:govoplan-core.add.61cc55aa",
addRequest: "i18n:govoplan-scheduling.add_scheduling_request.3c71be15",
createPermissionRequired: "i18n:govoplan-scheduling.create_permission_required",
addParticipant: "i18n:govoplan-scheduling.add_participant.6cff957d",
addSlot: "i18n:govoplan-scheduling.add_slot.9fcff3ed",
answered: "i18n:govoplan-scheduling.answered.e0aafffa",
@@ -933,39 +934,19 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
return (
<WorkspaceFrame as="main" height="viewport" surface="plain" className="scheduling-page" label="Scheduling workspace">
<section className="scheduling-workspace">
<div className="scheduling-workspace-layout">
<aside className="scheduling-request-sidebar">
<Card
title={I18N.requests}
actions={(
<WorkspaceActionBar
scope="collection-pane"
scope="workspace"
variant="collection"
refreshable
reloadAction={{
onReload: () => void loadRequests(selected?.id),
loading: loading || saving,
disabledReason: saving ? I18N.saving : undefined,
label: I18N.refresh
}}
className="scheduling-sidebar-actions"
helpAction={<DocumentationHelpLink
reference={{
topicId: "scheduling.find-and-decide-meeting-time",
documentationType: "user"
}} />}
createAction={canCreateOrWrite ? (
<Button
type="button"
variant="primary"
onClick={beginCreate}
disabled={saving}
disabledReason={saving ? I18N.saving : undefined}>
<Plus aria-hidden="true" size={16} /> {I18N.add}
</Button>
) : undefined}
reloadAction={{ onReload: () => void loadRequests(selected?.id), loading: loading || saving, disabledReason: saving ? I18N.saving : undefined, label: I18N.refresh }}
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>}
/>
)}>
<div className="scheduling-workspace-layout">
<aside className="scheduling-request-sidebar">
<Card title={I18N.requests}>
{loading ? <p className="scheduling-note" role="status">{I18N.loading}</p> : null}
<RequestGroup
title={I18N.myRequests}
@@ -1003,19 +984,15 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
scope="editor-pane"
variant="editor"
state={saving ? "saving" : draftDirty && !title.trim() ? "invalid" : draftDirty ? "dirty" : "clean"}
className="scheduling-page-header"
contextActions={<div className="scheduling-page-title">
<CalendarCheck aria-hidden="true" size={20} />
<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>
</div>
</div>}
helpAction={<DocumentationHelpLink
reference={{
topicId: "scheduling.find-and-decide-meeting-time",
documentationType: "user"
}} />}
discardAction={{ label: I18N.discard, type: "button", onClick: discardEditor }}
saveAction={{
label: <><Save aria-hidden="true" size={16} /> {saving ? I18N.saving : I18N.save}</>,
@@ -1193,14 +1170,14 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
statusLabel: stage.current ? requestStatusLabel(selected, actor) : undefined
}))} />
<Card
title={selected.title}
actions={(
<div className="scheduling-actions">
<DocumentationHelpLink
title={selected.title} titleHelp={<DocumentationHelpLink
reference={{
topicId: "scheduling.find-and-decide-meeting-time",
documentationType: "user"
}} />
}} />}
actions={(
<div className="scheduling-actions">
{canEditSelected ? (
<Button
type="button"
@@ -2416,11 +2393,11 @@ function SelfEnrollmentLinksCard({
return (
<Card
title={I18N.selfEnrollmentLinks}
actions={<DocumentationHelpLink reference={{
title={I18N.selfEnrollmentLinks} titleHelp={<DocumentationHelpLink reference={{
topicId: "scheduling.public-self-enrollment",
documentationType: "user"
}} />}>
}} />}
>
<p className="scheduling-capability-note">
{I18N.selfEnrollmentLinksHelp}
</p>
@@ -170,14 +170,12 @@ export default function SchedulingPublicPage({ settings, auth }: SchedulingPubli
{!response && !loading && (
<Card
title={I18N.accessDetails}
actions={(
<DocumentationHelpLink
title={I18N.accessDetails} titleHelp={<DocumentationHelpLink
reference={{
topicId: "scheduling.find-and-decide-meeting-time",
documentationType: "user"
}} />
)}>
}} />}
>
<form className="scheduling-public-access-form" onSubmit={openRequest}>
<p className="muted">{I18N.accessHelp}</p>
{accessAttempted && error && <DismissibleAlert tone="danger">{error}</DismissibleAlert>}
@@ -213,14 +211,12 @@ export default function SchedulingPublicPage({ settings, auth }: SchedulingPubli
{response && (
<form className="scheduling-public-content" onSubmit={saveResponse}>
<Card
title={response.title}
actions={(
<DocumentationHelpLink
title={response.title} titleHelp={<DocumentationHelpLink
reference={{
topicId: "scheduling.find-and-decide-meeting-time",
documentationType: "user"
}} />
)}>
}} />}
>
{response.description && <p className="scheduling-public-description">{response.description}</p>}
<dl className="scheduling-public-summary">
{response.location && <><dt>i18n:govoplan-scheduling.location.d219c681</dt><dd>{response.location}</dd></>}
@@ -4,7 +4,6 @@ import { Link } from "react-router";
import {
DashboardWidgetList,
DismissibleAlert,
DocumentationHelpLink,
LoadingFrame,
StatusBadge,
formatDateTime,
@@ -80,11 +79,6 @@ export default function SchedulingRequestsWidget({
}))}
/>
<div className="dashboard-contribution-footer">
<DocumentationHelpLink
reference={{
topicId: "scheduling.find-and-decide-meeting-time",
documentationType: "user"
}} />
<Link className="btn btn-secondary" to="/scheduling">
{I18N.openScheduling}
</Link>
+4
View File
@@ -1,5 +1,6 @@
export const generatedTranslations = {
en: {
"Scheduling": "Scheduling",
"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_help": "Account binding requires confirmation and lets you manage this response from Scheduling.",
@@ -206,6 +207,7 @@ export const generatedTranslations = {
"i18n:govoplan-scheduling.the_response_replaces_your_previous_availability_choices.74c16d53": "The response replaces your previous availability choices.",
"i18n:govoplan-scheduling.title.768e0c1c": "Title",
"i18n:govoplan-scheduling.unavailable.2c9c1f79": "Unavailable",
"i18n:govoplan-scheduling.create_permission_required": "Creating a scheduling request requires Scheduling write or administration permission. Ask an Access or tenant administrator to grant it.",
"i18n:govoplan-scheduling.unchecked.1b927dec": "Unchecked",
"i18n:govoplan-scheduling.update_response.346233cf": "Update response",
"i18n:govoplan-scheduling.use_a_calendar_for_availability_checks_tentative_holds_a.20ccc1fa": "Use a calendar for availability checks, tentative holds, and the final event.",
@@ -218,6 +220,7 @@ export const generatedTranslations = {
"i18n:govoplan-scheduling.your_response_has_been_recorded.b855088d": "Your response has been recorded."
},
de: {
"Scheduling": "Terminplanung",
"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_help": "Die Kontoverknüpfung muss bestätigt werden und ermöglicht die Verwaltung dieser Antwort in der Terminplanung.",
@@ -424,6 +427,7 @@ export const generatedTranslations = {
"i18n:govoplan-scheduling.the_response_replaces_your_previous_availability_choices.74c16d53": "Die Antwort ersetzt Ihre vorherige Verfügbarkeitsauswahl.",
"i18n:govoplan-scheduling.title.768e0c1c": "Titel",
"i18n:govoplan-scheduling.unavailable.2c9c1f79": "Nicht verfügbar",
"i18n:govoplan-scheduling.create_permission_required": "Zum Anlegen einer Terminanfrage benötigen Sie Schreib- oder Administrationsrechte für Scheduling. Bitten Sie die Access- oder Mandantenadministration um diese Berechtigung.",
"i18n:govoplan-scheduling.unchecked.1b927dec": "Nicht geprüft",
"i18n:govoplan-scheduling.update_response.346233cf": "Antwort aktualisieren",
"i18n:govoplan-scheduling.use_a_calendar_for_availability_checks_tentative_holds_a.20ccc1fa": "Einen Kalender für Verfügbarkeitsprüfungen, vorläufige Reservierungen und den endgültigen Termin verwenden.",
+1
View File
@@ -18,6 +18,7 @@ const schedulingDashboardWidgets: DashboardWidgetsUiCapability = {
id: "scheduling.open-requests",
surfaceId: "scheduling.widget.open-requests",
title: "Scheduling requests",
documentation: { topicId: "scheduling.find-and-decide-meeting-time", documentationType: "user" },
description: "Open scheduling polls and their response progress.",
moduleId: "scheduling",
category: "Planning",
+1 -30
View File
@@ -145,18 +145,6 @@
padding: 10px 12px;
}
.scheduling-sidebar-actions {
display: flex;
align-items: center;
gap: 6px;
}
.scheduling-sidebar-actions .btn {
display: inline-flex;
align-items: center;
gap: 5px;
}
.scheduling-main-panel {
display: flex;
flex-direction: column;
@@ -176,18 +164,6 @@
overflow: hidden;
}
.scheduling-page-header {
min-height: 58px;
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 14px;
border-bottom: var(--border-line);
background: var(--panel-header);
}
.scheduling-page-title,
.scheduling-page-actions,
.scheduling-card-actions,
@@ -206,7 +182,7 @@
min-width: 0;
}
.scheduling-page-title h1 {
.scheduling-page-title h2 {
margin: 0;
color: var(--text-strong);
font-size: 19px;
@@ -468,11 +444,6 @@
border-bottom: var(--border-line);
}
.scheduling-page-header {
align-items: flex-start;
flex-wrap: wrap;
}
.scheduling-page-actions,
.scheduling-card-actions,
.scheduling-actions {