1 Commits
Author SHA1 Message Date
zemion 944401d53b fix(ui): keep dashboard configuration exit available and align heading help
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:03:36 +02:00
6 changed files with 54 additions and 13 deletions
+14 -4
View File
@@ -273,6 +273,9 @@ manifest = ModuleManifest(
title="Configurable user dashboard", title="Configurable user dashboard",
summary="The dashboard module owns the configurable home surface. Feature modules expose widgets through a narrow dashboard.widgets capability.", summary="The dashboard module owns the configurable home surface. Feature modules expose widgets through a narrow dashboard.widgets capability.",
body=( 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. " "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. " "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 " "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." "Das Dashboard-Modul führt die konfigurierbare Startoberfläche; Fachmodule stellen Widgets über die enge Fähigkeit dashboard.widgets bereit."
), ),
"body": ( "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 " "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. " "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 " "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 " "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 " "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 " "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 " "positions. Cancel exits configuration even when nothing has changed; Save is disabled until "
"dirty invokes the shared save-or-discard guard. Widget removal removes only the placement, " "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 " "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 " "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 " "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 " "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 " "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, " "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 " "ungespeichert oder in Speicherung ist; Speichern und Abbrechen bleiben an stabilen Positionen. Abbrechen beendet "
"Erklärung deaktiviert, wenn keine Änderung vorliegt. Beim Verlassen eines geänderten Entwurfs greift die gemeinsame " "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 " "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 " "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 " "Speichern umkehrbar. Ein Widget wird nur angeboten, wenn Modul, fokussierte View-Oberfläche und Berechtigungsvertrag "
+18 -2
View File
@@ -92,12 +92,28 @@ class DashboardInterfaceDocumentationContractTests(unittest.TestCase):
REPO_ROOT / "webui/src/features/dashboard/WidgetLibrary.tsx" REPO_ROOT / "webui/src/features/dashboard/WidgetLibrary.tsx"
).read_text(encoding="utf-8") ).read_text(encoding="utf-8")
self.assertIn("DocumentationHelpLink", page) self.assertEqual(
1,
page.count("titleHelp={<DocumentationHelpLink reference={DASHBOARD_DOCUMENTATION} />}"),
)
self.assertNotIn("helpAction=", page)
self.assertIn("useUnsavedDraftGuard", page) self.assertIn("useUnsavedDraftGuard", page)
self.assertIn("useUnsavedDraftGuard", dialog) self.assertIn("useUnsavedDraftGuard", dialog)
self.assertIn("DASHBOARD_LAYOUT_DOCUMENTATION", dialog) self.assertIn(
"titleHelp={<DocumentationHelpLink reference={DASHBOARD_LAYOUT_DOCUMENTATION} />}",
dialog,
)
self.assertIn("disabled={atCapacity}", library) 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 && <DocumentationHelpLink reference={widget.documentation} />}"),
"Regular cards and height-preserving drag cards retain provider documentation beside their existing title.",
)
self.assertNotIn("helpAction=", grid)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
@@ -15,6 +15,7 @@ import {
} from "lucide-react"; } from "lucide-react";
import { import {
Card, Card,
DocumentationHelpLink,
IconButton, IconButton,
type ApiSettings, type ApiSettings,
type AuthInfo, type AuthInfo,
@@ -154,7 +155,7 @@ export default function DashboardGrid({
onDragOver={(event) => event.preventDefault()} onDragOver={(event) => event.preventDefault()}
onDrop={onDropPreview} onDrop={onDropPreview}
> >
<Card title={widget.title}> <Card title={widget.title} titleHelp={widget.documentation && <DocumentationHelpLink reference={widget.documentation} />}>
<DashboardWidgetContent <DashboardWidgetContent
widget={widget} widget={widget}
placement={placement} placement={placement}
@@ -186,6 +187,7 @@ export default function DashboardGrid({
> >
<Card <Card
title={widget.title} title={widget.title}
titleHelp={widget.documentation && <DocumentationHelpLink reference={widget.documentation} />}
collapsible={!configuring} collapsible={!configuring}
collapseKey={`dashboard-widget:${placement.instanceId}`} collapseKey={`dashboard-widget:${placement.instanceId}`}
actions={ actions={
+13 -4
View File
@@ -6,7 +6,6 @@ import {
type DragEvent as ReactDragEvent type DragEvent as ReactDragEvent
} from "react"; } from "react";
import { import {
RefreshCw,
Save, Save,
SlidersHorizontal, SlidersHorizontal,
X X
@@ -24,6 +23,7 @@ import {
isApiError, isApiError,
useEffectiveView, useEffectiveView,
usePlatformModules, usePlatformModules,
useUnsavedChanges,
useUnsavedDraftGuard, useUnsavedDraftGuard,
type ApiSettings, type ApiSettings,
type AuthInfo, type AuthInfo,
@@ -185,6 +185,7 @@ export default function DashboardPage({
]); ]);
const dirty = configuring && !layoutsEqual(savedLayout, draftLayout); const dirty = configuring && !layoutsEqual(savedLayout, draftLayout);
const { requestDiscard } = useUnsavedChanges();
useUnsavedDraftGuard({ useUnsavedDraftGuard({
dirty, dirty,
onSave: persistLayout, onSave: persistLayout,
@@ -253,12 +254,21 @@ export default function DashboardPage({
function discardChanges() { function discardChanges() {
setDraftLayout(savedLayout); setDraftLayout(savedLayout);
exitConfiguration();
}
function exitConfiguration() {
setConfiguring(false); setConfiguring(false);
setEditingInstanceId(null); setEditingInstanceId(null);
setDragItem(null); setDragItem(null);
setDropTarget(null); setDropTarget(null);
} }
function cancelConfiguration() {
if (dirty) requestDiscard(exitConfiguration);
else exitConfiguration();
}
function beginConfiguration() { function beginConfiguration() {
setDraftLayout(savedLayout); setDraftLayout(savedLayout);
setConfiguring(true); setConfiguring(true);
@@ -447,6 +457,7 @@ export default function DashboardPage({
archetype={configuring ? "editor" : "overview"} archetype={configuring ? "editor" : "overview"}
className="dashboard-page" className="dashboard-page"
title="i18n:govoplan-dashboard.dashboard.3f8b4df2" title="i18n:govoplan-dashboard.dashboard.3f8b4df2"
titleHelp={<DocumentationHelpLink reference={DASHBOARD_DOCUMENTATION} />}
description="Personal workspace assembled from installed module widgets." description="Personal workspace assembled from installed module widgets."
error={error} error={error}
success={error ? "" : notice} success={error ? "" : notice}
@@ -454,8 +465,7 @@ export default function DashboardPage({
<PageActionBar <PageActionBar
variant="editor" variant="editor"
state={saving ? "saving" : dirty ? "dirty" : "clean"} state={saving ? "saving" : dirty ? "dirty" : "clean"}
helpAction={<DocumentationHelpLink reference={DASHBOARD_DOCUMENTATION} />} discardAction={{ label: <><X size={16} /> Cancel</>, behavior: "exit", onClick: cancelConfiguration }}
discardAction={{ label: <><X size={16} /> Cancel</>, onClick: discardChanges }}
saveAction={{ label: <><Save size={16} /> Save layout</>, onClick: () => void persistLayout() }} saveAction={{ label: <><Save size={16} /> Save layout</>, onClick: () => void persistLayout() }}
/> />
) : ( ) : (
@@ -467,7 +477,6 @@ export default function DashboardPage({
loading, loading,
disabledReason: loading ? DASHBOARD_I18N.loading : undefined disabledReason: loading ? DASHBOARD_I18N.loading : undefined
}} }}
helpAction={<DocumentationHelpLink reference={DASHBOARD_DOCUMENTATION} />}
primaryActions={( primaryActions={(
<Button <Button
onClick={beginConfiguration} onClick={beginConfiguration}
@@ -118,6 +118,7 @@ export default function WidgetConfigurationDialog({
} }
function reset() { function reset() {
if (!widget) return;
setSize(defaultWidgetSize(widget)); setSize(defaultWidgetSize(widget));
setConfiguration({ ...(widget.defaultConfiguration ?? {}) }); setConfiguration({ ...(widget.defaultConfiguration ?? {}) });
} }
@@ -126,6 +127,7 @@ export default function WidgetConfigurationDialog({
<Dialog <Dialog
open={open} open={open}
title={`Configure ${widget.title}`} title={`Configure ${widget.title}`}
titleHelp={<DocumentationHelpLink reference={DASHBOARD_LAYOUT_DOCUMENTATION} />}
onClose={close} onClose={close}
className="dashboard-widget-config-dialog" className="dashboard-widget-config-dialog"
footer={ footer={
@@ -145,7 +147,6 @@ export default function WidgetConfigurationDialog({
</> </>
} }
> >
<DocumentationHelpLink reference={DASHBOARD_LAYOUT_DOCUMENTATION} />
{supportedSizes.length > 1 && ( {supportedSizes.length > 1 && (
<FormField label="Widget size" documentation={DASHBOARD_LAYOUT_DOCUMENTATION}> <FormField label="Widget size" documentation={DASHBOARD_LAYOUT_DOCUMENTATION}>
<SegmentedControl <SegmentedControl
+4 -1
View File
@@ -186,10 +186,13 @@
padding: 12px 16px; padding: 12px 16px;
} }
.dashboard-widget-grid.is-configuring .card-header > h2 { .dashboard-widget-grid.is-configuring .card-header > .card-title-with-help {
order: 2; order: 2;
flex: 1 1 auto; flex: 1 1 auto;
min-width: 0; min-width: 0;
}
.dashboard-widget-grid.is-configuring .card-title-with-help > h2 {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;