diff --git a/src/govoplan_dashboard/backend/manifest.py b/src/govoplan_dashboard/backend/manifest.py index e13ce26..a668c15 100644 --- a/src/govoplan_dashboard/backend/manifest.py +++ b/src/govoplan_dashboard/backend/manifest.py @@ -273,6 +273,9 @@ manifest = ModuleManifest( title="Configurable user dashboard", summary="The dashboard module owns the configurable home surface. Feature modules expose widgets through a narrow dashboard.widgets capability.", body=( + "The user documentation book sits immediately to the right of Dashboard in both display and " + "configuration mode. Widget configuration help sits beside its dialog title. " + "A widget's contributed documentation book sits beside its existing card title, not in the widget footer. " "Core only provides a minimal fallback home when the dashboard module is absent. " "Dashboard widgets must be contributed through core contracts, not by importing sibling module components directly. " "Personal layouts are stored per tenant, account, and active View. The active interface-module count includes only " @@ -314,6 +317,9 @@ manifest = ModuleManifest( "Das Dashboard-Modul führt die konfigurierbare Startoberfläche; Fachmodule stellen Widgets über die enge Fähigkeit dashboard.widgets bereit." ), "body": ( + "Das Buch für die Benutzerdokumentation steht im Anzeige- und Konfigurationsmodus unmittelbar " + "rechts neben Übersicht (Dashboard). Die Hilfe zur Widget-Konfiguration steht neben ihrem Dialogtitel. " + "Das beigetragene Dokumentationsbuch eines Widgets steht neben seinem vorhandenen Kartentitel, nicht in der Fußzeile. " "Core stellt nur dann eine minimale Ersatzstartseite bereit, wenn das Dashboard-Modul fehlt. Dashboard-Widgets " "müssen über Core-Verträge beigetragen werden und dürfen Komponenten anderer Module nicht direkt importieren. " "Persönliche Layouts werden je Mandant, Konto und aktivem View gespeichert. Die Anzahl aktiver Oberflächenmodule " @@ -348,8 +354,10 @@ manifest = ModuleManifest( "Configuring changes only a local draft until Save layout is selected; Cancel or " "Discard restores the last saved arrangement. The page action bar always reports whether " "the draft is saved, unsaved, or currently saving; Save and Cancel remain visible in stable " - "positions and are disabled with an explanation when no draft change exists. Leaving while " - "dirty invokes the shared save-or-discard guard. Widget removal removes only the placement, " + "positions. Cancel exits configuration even when nothing has changed; Save is disabled until " + "there are changes. Cancel asks before discarding an edited draft. Both actions are temporarily " + "disabled while a save is in progress. Leaving while dirty invokes the shared save-or-discard " + "guard. Widget removal removes only the placement, " "not the module data represented by the widget. Reset restores the defaults announced by " "currently active modules and remains reversible until save. A widget is offered only when " "its module, focused View surface, and permission contract are available. Widgets never grant " @@ -389,8 +397,10 @@ manifest = ModuleManifest( "Ein Dashboard-Layout gehört zum aktiven Mandanten, Konto und fokussierten View. Konfigurationen ändern " "zunächst nur einen lokalen Entwurf; erst Layout speichern übernimmt sie. Abbrechen oder Verwerfen stellt " "die zuletzt gespeicherte Anordnung wieder her. Die Seitenaktionsleiste zeigt stets, ob der Entwurf gespeichert, " - "ungespeichert oder in Speicherung ist; Speichern und Abbrechen bleiben an stabilen Positionen und sind mit " - "Erklärung deaktiviert, wenn keine Änderung vorliegt. Beim Verlassen eines geänderten Entwurfs greift die gemeinsame " + "ungespeichert oder in Speicherung ist; Speichern und Abbrechen bleiben an stabilen Positionen. Abbrechen beendet " + "die Konfiguration auch ohne Änderungen; Speichern wird erst bei Änderungen verfügbar. Bei einem geänderten " + "Entwurf fragt Abbrechen vor dem Verwerfen nach. Nur während einer laufenden Speicherung sind beide Aktionen " + "vorübergehend deaktiviert. Beim Verlassen eines geänderten Entwurfs greift die gemeinsame " "Speichern-oder-Verwerfen-Sicherung. Das Entfernen eines Widgets entfernt nur seine Platzierung, nicht die dargestellten " "Moduldaten. Zurücksetzen übernimmt die von aktuell aktiven Modulen angekündigten Standardwerte und bleibt bis zum " "Speichern umkehrbar. Ein Widget wird nur angeboten, wenn Modul, fokussierte View-Oberfläche und Berechtigungsvertrag " diff --git a/tests/test_interface_documentation_contract.py b/tests/test_interface_documentation_contract.py index 235fb7e..e864964 100644 --- a/tests/test_interface_documentation_contract.py +++ b/tests/test_interface_documentation_contract.py @@ -92,12 +92,28 @@ class DashboardInterfaceDocumentationContractTests(unittest.TestCase): REPO_ROOT / "webui/src/features/dashboard/WidgetLibrary.tsx" ).read_text(encoding="utf-8") - self.assertIn("DocumentationHelpLink", page) + self.assertEqual( + 1, + page.count("titleHelp={}"), + ) + self.assertNotIn("helpAction=", page) self.assertIn("useUnsavedDraftGuard", page) self.assertIn("useUnsavedDraftGuard", dialog) - self.assertIn("DASHBOARD_LAYOUT_DOCUMENTATION", dialog) + self.assertIn( + "titleHelp={}", + dialog, + ) self.assertIn("disabled={atCapacity}", library) + def test_widget_help_uses_provider_reference_at_the_existing_title(self) -> None: + grid = (REPO_ROOT / "webui/src/features/dashboard/DashboardGrid.tsx").read_text(encoding="utf-8") + self.assertEqual( + 2, + grid.count("titleHelp={widget.documentation && }"), + "Regular cards and height-preserving drag cards retain provider documentation beside their existing title.", + ) + self.assertNotIn("helpAction=", grid) + if __name__ == "__main__": unittest.main() diff --git a/webui/src/features/dashboard/DashboardGrid.tsx b/webui/src/features/dashboard/DashboardGrid.tsx index 85b4386..884c6cc 100644 --- a/webui/src/features/dashboard/DashboardGrid.tsx +++ b/webui/src/features/dashboard/DashboardGrid.tsx @@ -15,6 +15,7 @@ import { } from "lucide-react"; import { Card, + DocumentationHelpLink, IconButton, type ApiSettings, type AuthInfo, @@ -154,7 +155,7 @@ export default function DashboardGrid({ onDragOver={(event) => event.preventDefault()} onDrop={onDropPreview} > - + }> } collapsible={!configuring} collapseKey={`dashboard-widget:${placement.instanceId}`} actions={ diff --git a/webui/src/features/dashboard/DashboardPage.tsx b/webui/src/features/dashboard/DashboardPage.tsx index 2b450dc..f4853c6 100644 --- a/webui/src/features/dashboard/DashboardPage.tsx +++ b/webui/src/features/dashboard/DashboardPage.tsx @@ -6,7 +6,6 @@ import { type DragEvent as ReactDragEvent } from "react"; import { - RefreshCw, Save, SlidersHorizontal, X @@ -24,6 +23,7 @@ import { isApiError, useEffectiveView, usePlatformModules, + useUnsavedChanges, useUnsavedDraftGuard, type ApiSettings, type AuthInfo, @@ -185,6 +185,7 @@ export default function DashboardPage({ ]); const dirty = configuring && !layoutsEqual(savedLayout, draftLayout); + const { requestDiscard } = useUnsavedChanges(); useUnsavedDraftGuard({ dirty, onSave: persistLayout, @@ -253,12 +254,21 @@ export default function DashboardPage({ function discardChanges() { setDraftLayout(savedLayout); + exitConfiguration(); + } + + function exitConfiguration() { setConfiguring(false); setEditingInstanceId(null); setDragItem(null); setDropTarget(null); } + function cancelConfiguration() { + if (dirty) requestDiscard(exitConfiguration); + else exitConfiguration(); + } + function beginConfiguration() { setDraftLayout(savedLayout); setConfiguring(true); @@ -447,6 +457,7 @@ export default function DashboardPage({ archetype={configuring ? "editor" : "overview"} className="dashboard-page" title="i18n:govoplan-dashboard.dashboard.3f8b4df2" + titleHelp={} description="Personal workspace assembled from installed module widgets." error={error} success={error ? "" : notice} @@ -454,8 +465,7 @@ export default function DashboardPage({ } - discardAction={{ label: <> Cancel, onClick: discardChanges }} + discardAction={{ label: <> Cancel, behavior: "exit", onClick: cancelConfiguration }} saveAction={{ label: <> Save layout, onClick: () => void persistLayout() }} /> ) : ( @@ -467,7 +477,6 @@ export default function DashboardPage({ loading, disabledReason: loading ? DASHBOARD_I18N.loading : undefined }} - helpAction={} primaryActions={(