Migrate Dashboard interface patterns
This commit is contained in:
@@ -0,0 +1,34 @@
|
|||||||
|
# Dashboard Interface Pattern Migration
|
||||||
|
|
||||||
|
This migration applies the GovOPlaN interface pattern language to the
|
||||||
|
view-specific summary, widget library, four-column placement grid, and widget
|
||||||
|
configuration dialog.
|
||||||
|
|
||||||
|
## Surface Inventory
|
||||||
|
|
||||||
|
| Surface | Archetype | Consequence class | Contract |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `/dashboard` | Personal workspace | Inspect current task context | Stable loading, fallback, empty, error, and help states |
|
||||||
|
| Summary | Metric overview | Inspect module/widget/layout state | Installed, available, placed, active View, and persistence source remain distinguishable |
|
||||||
|
| Widget library | Capability-filtered catalogue | Add or reset placements | Permission/View filtering, search, capacity reason, drag/click parity, and reversible defaults |
|
||||||
|
| Dashboard grid | Configurable composition | Reorder, resize, configure, or remove placement | Four-column placement contract, visible drag target, guarded draft, and no provider-data mutation |
|
||||||
|
| Widget settings | Nested definition editor | Apply presentation/query settings | Guarded local draft, contextual field help, validation reason, reset, apply, and discard |
|
||||||
|
|
||||||
|
## Consequence And Availability Rules
|
||||||
|
|
||||||
|
- Layouts are scoped by tenant, account, and active focused View. A focused
|
||||||
|
View changes the composition key but never grants access to widget data.
|
||||||
|
- Configure mode edits a local draft. Save persists the complete layout using
|
||||||
|
its revision; stale concurrent saves are rejected rather than overwritten.
|
||||||
|
- Removing a widget affects only its placement. Reset restores the defaults
|
||||||
|
contributed by active modules and remains reversible until Save layout.
|
||||||
|
- Widgets are filtered by installed module, focused View surface, and declared
|
||||||
|
scopes. Every provider still authorizes its own requests.
|
||||||
|
- When the saved layout is unreachable, the browser or module defaults are
|
||||||
|
clearly identified as a fallback. Saving promotes the current draft to the
|
||||||
|
account-backed layout.
|
||||||
|
|
||||||
|
Backend and WebUI manifests publish matching page, summary, library, grid,
|
||||||
|
settings, and built-in-widget surface identifiers. English and German
|
||||||
|
catalogues cover Dashboard-owned vocabulary, contextual help resolves from
|
||||||
|
manifest documentation, and both layout and widget-setting drafts are guarded.
|
||||||
@@ -72,11 +72,51 @@ manifest = ModuleManifest(
|
|||||||
routes=(FrontendRoute(path="/dashboard", component="DashboardPage", order=10),),
|
routes=(FrontendRoute(path="/dashboard", component="DashboardPage", order=10),),
|
||||||
nav_items=(NavItem(path="/dashboard", label="Dashboard", icon="dashboard", order=10),),
|
nav_items=(NavItem(path="/dashboard", label="Dashboard", icon="dashboard", order=10),),
|
||||||
view_surfaces=(
|
view_surfaces=(
|
||||||
|
ViewSurface(
|
||||||
|
id="dashboard.page",
|
||||||
|
module_id="dashboard",
|
||||||
|
kind="route",
|
||||||
|
label="Dashboard",
|
||||||
|
order=10,
|
||||||
|
),
|
||||||
|
ViewSurface(
|
||||||
|
id="dashboard.summary",
|
||||||
|
module_id="dashboard",
|
||||||
|
kind="section",
|
||||||
|
label="Dashboard summary",
|
||||||
|
parent_id="dashboard.page",
|
||||||
|
order=10,
|
||||||
|
),
|
||||||
|
ViewSurface(
|
||||||
|
id="dashboard.library",
|
||||||
|
module_id="dashboard",
|
||||||
|
kind="section",
|
||||||
|
label="Widget library",
|
||||||
|
parent_id="dashboard.page",
|
||||||
|
order=20,
|
||||||
|
),
|
||||||
|
ViewSurface(
|
||||||
|
id="dashboard.grid",
|
||||||
|
module_id="dashboard",
|
||||||
|
kind="section",
|
||||||
|
label="Dashboard grid",
|
||||||
|
parent_id="dashboard.page",
|
||||||
|
order=30,
|
||||||
|
),
|
||||||
|
ViewSurface(
|
||||||
|
id="dashboard.widget-settings",
|
||||||
|
module_id="dashboard",
|
||||||
|
kind="action",
|
||||||
|
label="Widget settings",
|
||||||
|
parent_id="dashboard.grid",
|
||||||
|
order=40,
|
||||||
|
),
|
||||||
ViewSurface(
|
ViewSurface(
|
||||||
id="dashboard.widget.installed-modules",
|
id="dashboard.widget.installed-modules",
|
||||||
module_id="dashboard",
|
module_id="dashboard",
|
||||||
kind="section",
|
kind="section",
|
||||||
label="Installed modules widget",
|
label="Installed modules widget",
|
||||||
|
parent_id="dashboard.grid",
|
||||||
order=10,
|
order=10,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -96,6 +136,53 @@ manifest = ModuleManifest(
|
|||||||
audience=("user", "tenant_admin", "operator"),
|
audience=("user", "tenant_admin", "operator"),
|
||||||
related_modules=("core", "ops"),
|
related_modules=("core", "ops"),
|
||||||
order=20,
|
order=20,
|
||||||
|
metadata={
|
||||||
|
"help_contexts": [
|
||||||
|
"dashboard.page",
|
||||||
|
"dashboard.summary",
|
||||||
|
"dashboard.library",
|
||||||
|
"dashboard.grid",
|
||||||
|
"dashboard.state.browser-fallback",
|
||||||
|
"dashboard.state.view-specific",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
DocumentationTopic(
|
||||||
|
id="dashboard.reference.layout-and-widgets",
|
||||||
|
title="Dashboard layouts and widget consequences",
|
||||||
|
summary="Per-user, per-tenant, and per-View widget placement, sizing, configuration, availability, and fallback semantics.",
|
||||||
|
body=(
|
||||||
|
"A Dashboard layout belongs to the active tenant, account, and focused View. "
|
||||||
|
"Configuring changes only a local draft until Save layout is selected; Cancel or "
|
||||||
|
"Discard restores the last saved arrangement. 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 "
|
||||||
|
"access and providers must authorize every data request. If the server layout is unreachable, "
|
||||||
|
"the browser or module default can be displayed but is identified as a fallback until saved. "
|
||||||
|
"Concurrent saves use the layout revision and reject stale updates instead of overwriting them."
|
||||||
|
),
|
||||||
|
layer="available",
|
||||||
|
documentation_types=("admin", "user"),
|
||||||
|
audience=("user", "tenant_admin", "operator", "module_admin"),
|
||||||
|
related_modules=("core", "views", "access", "ops"),
|
||||||
|
order=21,
|
||||||
|
metadata={
|
||||||
|
"help_contexts": [
|
||||||
|
"dashboard.action.configure",
|
||||||
|
"dashboard.action.save",
|
||||||
|
"dashboard.action.reset",
|
||||||
|
"dashboard.action.remove-widget",
|
||||||
|
"dashboard.field.widget-size",
|
||||||
|
"dashboard.field.widget-configuration",
|
||||||
|
],
|
||||||
|
"consequence_classes": {
|
||||||
|
"save_layout": "Persists the complete layout for the active tenant, account, and View revision context.",
|
||||||
|
"reset_layout": "Replaces the draft with current module defaults and remains reversible until save.",
|
||||||
|
"remove_widget": "Removes only the placement from the draft; provider data is unchanged.",
|
||||||
|
"configure_widget": "Changes presentation and query preferences for one widget placement.",
|
||||||
|
},
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
architecture=declared_module_architecture(
|
architecture=declared_module_architecture(
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from govoplan_dashboard.backend.manifest import get_manifest
|
||||||
|
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
class DashboardInterfaceDocumentationContractTests(unittest.TestCase):
|
||||||
|
def test_surface_hierarchy_remains_declared(self) -> None:
|
||||||
|
frontend = get_manifest().frontend
|
||||||
|
self.assertIsNotNone(frontend)
|
||||||
|
surfaces = {item.id: item for item in frontend.view_surfaces} # type: ignore[union-attr]
|
||||||
|
self.assertEqual(
|
||||||
|
{
|
||||||
|
"dashboard.page",
|
||||||
|
"dashboard.summary",
|
||||||
|
"dashboard.library",
|
||||||
|
"dashboard.grid",
|
||||||
|
"dashboard.widget-settings",
|
||||||
|
"dashboard.widget.installed-modules",
|
||||||
|
},
|
||||||
|
set(surfaces),
|
||||||
|
)
|
||||||
|
self.assertEqual("dashboard.page", surfaces["dashboard.library"].parent_id)
|
||||||
|
self.assertEqual("dashboard.page", surfaces["dashboard.grid"].parent_id)
|
||||||
|
self.assertEqual("dashboard.grid", surfaces["dashboard.widget-settings"].parent_id)
|
||||||
|
|
||||||
|
def test_help_and_consequence_metadata_remain_published(self) -> None:
|
||||||
|
topics = {topic.id: topic for topic in get_manifest().documentation}
|
||||||
|
home = topics["dashboard.configurable-home"]
|
||||||
|
reference = topics["dashboard.reference.layout-and-widgets"]
|
||||||
|
|
||||||
|
self.assertIn("dashboard.state.browser-fallback", home.metadata["help_contexts"])
|
||||||
|
self.assertIn("dashboard.field.widget-size", reference.metadata["help_contexts"])
|
||||||
|
self.assertIn("save_layout", reference.metadata["consequence_classes"])
|
||||||
|
self.assertIn("remove_widget", reference.metadata["consequence_classes"])
|
||||||
|
|
||||||
|
def test_webui_guards_layout_and_nested_widget_drafts(self) -> None:
|
||||||
|
page = (REPO_ROOT / "webui/src/features/dashboard/DashboardPage.tsx").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
dialog = (
|
||||||
|
REPO_ROOT
|
||||||
|
/ "webui/src/features/dashboard/WidgetConfigurationDialog.tsx"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
library = (
|
||||||
|
REPO_ROOT / "webui/src/features/dashboard/WidgetLibrary.tsx"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
self.assertIn("DocumentationHelpLink", page)
|
||||||
|
self.assertIn("useUnsavedDraftGuard", page)
|
||||||
|
self.assertIn("useUnsavedDraftGuard", dialog)
|
||||||
|
self.assertIn("DASHBOARD_LAYOUT_DOCUMENTATION", dialog)
|
||||||
|
self.assertIn("disabled={atCapacity}", library)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
|
DocumentationHelpLink,
|
||||||
LoadingFrame,
|
LoadingFrame,
|
||||||
MetricCard,
|
MetricCard,
|
||||||
PageScrollViewport,
|
PageScrollViewport,
|
||||||
@@ -36,6 +37,10 @@ import {
|
|||||||
import DashboardGrid from "./DashboardGrid";
|
import DashboardGrid from "./DashboardGrid";
|
||||||
import WidgetConfigurationDialog from "./WidgetConfigurationDialog";
|
import WidgetConfigurationDialog from "./WidgetConfigurationDialog";
|
||||||
import WidgetLibrary from "./WidgetLibrary";
|
import WidgetLibrary from "./WidgetLibrary";
|
||||||
|
import {
|
||||||
|
DASHBOARD_DOCUMENTATION,
|
||||||
|
DASHBOARD_I18N
|
||||||
|
} from "./interfacePatterns";
|
||||||
import {
|
import {
|
||||||
appendPlacement,
|
appendPlacement,
|
||||||
DASHBOARD_COLUMN_COUNT,
|
DASHBOARD_COLUMN_COUNT,
|
||||||
@@ -184,8 +189,8 @@ export default function DashboardPage({
|
|||||||
dirty,
|
dirty,
|
||||||
onSave: persistLayout,
|
onSave: persistLayout,
|
||||||
onDiscard: discardChanges,
|
onDiscard: discardChanges,
|
||||||
title: "Unsaved Dashboard layout",
|
title: "i18n:govoplan-dashboard.unsaved_layout_title",
|
||||||
message: "Save or discard the Dashboard arrangement before leaving this page."
|
message: "i18n:govoplan-dashboard.unsaved_layout_message"
|
||||||
});
|
});
|
||||||
|
|
||||||
const activeLayout = configuring ? draftLayout : savedLayout;
|
const activeLayout = configuring ? draftLayout : savedLayout;
|
||||||
@@ -446,25 +451,39 @@ export default function DashboardPage({
|
|||||||
<p>Personal workspace assembled from installed module widgets.</p>
|
<p>Personal workspace assembled from installed module widgets.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="button-row compact-actions">
|
<div className="button-row compact-actions">
|
||||||
|
<DocumentationHelpLink reference={DASHBOARD_DOCUMENTATION} />
|
||||||
{!configuring && (
|
{!configuring && (
|
||||||
<>
|
<>
|
||||||
<Button onClick={() => setRefreshKey((value) => value + 1)}>
|
<Button
|
||||||
|
onClick={() => setRefreshKey((value) => value + 1)}
|
||||||
|
disabled={loading}
|
||||||
|
disabledReason={loading ? DASHBOARD_I18N.loading : undefined}
|
||||||
|
>
|
||||||
<RefreshCw size={16} /> Refresh
|
<RefreshCw size={16} /> Refresh
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={beginConfiguration}>
|
<Button
|
||||||
|
onClick={beginConfiguration}
|
||||||
|
disabled={loading}
|
||||||
|
disabledReason={loading ? DASHBOARD_I18N.loading : undefined}
|
||||||
|
>
|
||||||
<SlidersHorizontal size={16} /> Configure
|
<SlidersHorizontal size={16} /> Configure
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{configuring && (
|
{configuring && (
|
||||||
<>
|
<>
|
||||||
<Button onClick={discardChanges} disabled={saving}>
|
<Button
|
||||||
|
onClick={discardChanges}
|
||||||
|
disabled={saving}
|
||||||
|
disabledReason={saving ? DASHBOARD_I18N.saving : undefined}
|
||||||
|
>
|
||||||
<X size={16} /> Cancel
|
<X size={16} /> Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={() => void persistLayout()}
|
onClick={() => void persistLayout()}
|
||||||
disabled={saving || !dirty}
|
disabled={saving || !dirty}
|
||||||
|
disabledReason={saving ? DASHBOARD_I18N.saving : !dirty ? DASHBOARD_I18N.noChanges : undefined}
|
||||||
>
|
>
|
||||||
<Save size={16} /> {saving ? "Saving..." : "Save layout"}
|
<Save size={16} /> {saving ? "Saving..." : "Save layout"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ import { useEffect, useMemo, useState } from "react";
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
|
DocumentationHelpLink,
|
||||||
FormField,
|
FormField,
|
||||||
SegmentedControl,
|
SegmentedControl,
|
||||||
ToggleSwitch,
|
ToggleSwitch,
|
||||||
|
useUnsavedChanges,
|
||||||
|
useUnsavedDraftGuard,
|
||||||
type DashboardWidgetConfiguration,
|
type DashboardWidgetConfiguration,
|
||||||
type DashboardWidgetConfigurationField,
|
type DashboardWidgetConfigurationField,
|
||||||
type DashboardWidgetConfigurationValue,
|
type DashboardWidgetConfigurationValue,
|
||||||
@@ -16,6 +19,10 @@ import {
|
|||||||
supportedWidgetSizes,
|
supportedWidgetSizes,
|
||||||
type DashboardWidgetPlacement
|
type DashboardWidgetPlacement
|
||||||
} from "./dashboardLayout";
|
} from "./dashboardLayout";
|
||||||
|
import {
|
||||||
|
DASHBOARD_I18N,
|
||||||
|
DASHBOARD_LAYOUT_DOCUMENTATION
|
||||||
|
} from "./interfacePatterns";
|
||||||
|
|
||||||
type WidgetConfigurationDialogProps = {
|
type WidgetConfigurationDialogProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -32,8 +39,25 @@ export default function WidgetConfigurationDialog({
|
|||||||
onClose,
|
onClose,
|
||||||
onSave
|
onSave
|
||||||
}: WidgetConfigurationDialogProps) {
|
}: WidgetConfigurationDialogProps) {
|
||||||
|
const { requestDiscard } = useUnsavedChanges();
|
||||||
const [size, setSize] = useState<DashboardWidgetSize>("medium");
|
const [size, setSize] = useState<DashboardWidgetSize>("medium");
|
||||||
const [configuration, setConfiguration] = useState<DashboardWidgetConfiguration>({});
|
const [configuration, setConfiguration] = useState<DashboardWidgetConfiguration>({});
|
||||||
|
const savedConfiguration = useMemo(
|
||||||
|
() => ({
|
||||||
|
...(widget?.defaultConfiguration ?? {}),
|
||||||
|
...(placement?.configuration ?? {})
|
||||||
|
}),
|
||||||
|
[placement, widget]
|
||||||
|
);
|
||||||
|
const dirty = Boolean(
|
||||||
|
open
|
||||||
|
&& widget
|
||||||
|
&& placement
|
||||||
|
&& (
|
||||||
|
size !== placement.size
|
||||||
|
|| JSON.stringify(configuration) !== JSON.stringify(savedConfiguration)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open || !widget || !placement) return;
|
if (!open || !widget || !placement) return;
|
||||||
@@ -58,6 +82,34 @@ export default function WidgetConfigurationDialog({
|
|||||||
[configuration, widget]
|
[configuration, widget]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function discardDraft() {
|
||||||
|
if (!widget || !placement) return;
|
||||||
|
setSize(placement.size);
|
||||||
|
setConfiguration({
|
||||||
|
...(widget.defaultConfiguration ?? {}),
|
||||||
|
...placement.configuration
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyDraft(): boolean {
|
||||||
|
if (!placement || invalidConfiguration) return false;
|
||||||
|
onSave({ ...placement, size, configuration });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
useUnsavedDraftGuard({
|
||||||
|
dirty,
|
||||||
|
title: "i18n:govoplan-dashboard.unsaved_widget_title",
|
||||||
|
message: "i18n:govoplan-dashboard.unsaved_widget_message",
|
||||||
|
onSave: applyDraft,
|
||||||
|
onDiscard: discardDraft
|
||||||
|
});
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
if (dirty) requestDiscard(onClose);
|
||||||
|
else onClose();
|
||||||
|
}
|
||||||
|
|
||||||
if (!widget || !placement) return null;
|
if (!widget || !placement) return null;
|
||||||
const supportedSizes = supportedWidgetSizes(widget);
|
const supportedSizes = supportedWidgetSizes(widget);
|
||||||
|
|
||||||
@@ -74,26 +126,28 @@ export default function WidgetConfigurationDialog({
|
|||||||
<Dialog
|
<Dialog
|
||||||
open={open}
|
open={open}
|
||||||
title={`Configure ${widget.title}`}
|
title={`Configure ${widget.title}`}
|
||||||
onClose={onClose}
|
onClose={close}
|
||||||
className="dashboard-widget-config-dialog"
|
className="dashboard-widget-config-dialog"
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
<Button type="button" variant="ghost" onClick={reset}>Reset</Button>
|
<Button type="button" variant="ghost" onClick={reset}>Reset</Button>
|
||||||
<span className="dialog-footer-spacer" />
|
<span className="dialog-footer-spacer" />
|
||||||
<Button type="button" onClick={onClose}>Cancel</Button>
|
<Button type="button" onClick={close}>Cancel</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
disabled={invalidConfiguration}
|
disabled={invalidConfiguration}
|
||||||
onClick={() => onSave({ ...placement, size, configuration })}
|
disabledReason={invalidConfiguration ? DASHBOARD_I18N.invalidWidget : undefined}
|
||||||
|
onClick={applyDraft}
|
||||||
>
|
>
|
||||||
Apply
|
Apply
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<DocumentationHelpLink reference={DASHBOARD_LAYOUT_DOCUMENTATION} />
|
||||||
{supportedSizes.length > 1 && (
|
{supportedSizes.length > 1 && (
|
||||||
<FormField label="Widget size">
|
<FormField label="Widget size" documentation={DASHBOARD_LAYOUT_DOCUMENTATION}>
|
||||||
<SegmentedControl
|
<SegmentedControl
|
||||||
options={supportedSizes.map((candidate) => ({
|
options={supportedSizes.map((candidate) => ({
|
||||||
id: candidate,
|
id: candidate,
|
||||||
@@ -142,7 +196,7 @@ function ConfigurationField({
|
|||||||
|
|
||||||
if (field.kind === "select") {
|
if (field.kind === "select") {
|
||||||
return (
|
return (
|
||||||
<FormField label={field.label} help={field.description}>
|
<FormField label={field.label} help={field.description} documentation={DASHBOARD_LAYOUT_DOCUMENTATION}>
|
||||||
<select
|
<select
|
||||||
value={typeof value === "string" ? value : ""}
|
value={typeof value === "string" ? value : ""}
|
||||||
required={field.required}
|
required={field.required}
|
||||||
@@ -161,7 +215,7 @@ function ConfigurationField({
|
|||||||
|
|
||||||
if (field.kind === "number") {
|
if (field.kind === "number") {
|
||||||
return (
|
return (
|
||||||
<FormField label={field.label} help={field.description}>
|
<FormField label={field.label} help={field.description} documentation={DASHBOARD_LAYOUT_DOCUMENTATION}>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={typeof value === "number" ? value : ""}
|
value={typeof value === "number" ? value : ""}
|
||||||
@@ -180,7 +234,7 @@ function ConfigurationField({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormField label={field.label} help={field.description}>
|
<FormField label={field.label} help={field.description} documentation={DASHBOARD_LAYOUT_DOCUMENTATION}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={typeof value === "string" ? value : ""}
|
value={typeof value === "string" ? value : ""}
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export default function WidgetLibrary({
|
|||||||
label={`Add ${widget.title}`}
|
label={`Add ${widget.title}`}
|
||||||
icon={<Plus size={16} />}
|
icon={<Plus size={16} />}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
disabled={atCapacity}
|
||||||
disabledReason={
|
disabledReason={
|
||||||
atCapacity
|
atCapacity
|
||||||
? "A Dashboard can contain at most 100 widgets."
|
? "A Dashboard can contain at most 100 widgets."
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import type { DocumentationHelpReference } from "@govoplan/core-webui";
|
||||||
|
|
||||||
|
export const DASHBOARD_DOCUMENTATION = {
|
||||||
|
topicId: "dashboard.configurable-home",
|
||||||
|
documentationType: "user"
|
||||||
|
} satisfies DocumentationHelpReference;
|
||||||
|
|
||||||
|
export const DASHBOARD_LAYOUT_DOCUMENTATION = {
|
||||||
|
topicId: "dashboard.reference.layout-and-widgets",
|
||||||
|
documentationType: "user"
|
||||||
|
} satisfies DocumentationHelpReference;
|
||||||
|
|
||||||
|
export const DASHBOARD_I18N = {
|
||||||
|
loading: "i18n:govoplan-dashboard.loading_reason",
|
||||||
|
saving: "i18n:govoplan-dashboard.saving_reason",
|
||||||
|
noChanges: "i18n:govoplan-dashboard.no_changes_reason",
|
||||||
|
invalidWidget: "i18n:govoplan-dashboard.invalid_widget_reason"
|
||||||
|
} as const;
|
||||||
@@ -1,9 +1,73 @@
|
|||||||
export const generatedTranslations = {
|
import type { PlatformTranslations } from "@govoplan/core-webui";
|
||||||
en: {
|
|
||||||
"i18n:govoplan-dashboard.dashboard.3f8b4df2": "Dashboard"
|
const en = {
|
||||||
},
|
"i18n:govoplan-dashboard.dashboard.3f8b4df2": "Dashboard",
|
||||||
de: {
|
"i18n:govoplan-dashboard.summary": "Dashboard summary",
|
||||||
"i18n:govoplan-dashboard.dashboard.3f8b4df2": "Dashboard"
|
"i18n:govoplan-dashboard.library": "Widget library",
|
||||||
}
|
"i18n:govoplan-dashboard.grid": "Dashboard grid",
|
||||||
|
"i18n:govoplan-dashboard.widget_settings": "Widget settings",
|
||||||
|
"i18n:govoplan-dashboard.installed_modules_widget": "Installed modules widget",
|
||||||
|
"i18n:govoplan-dashboard.loading_reason": "The saved Dashboard layout is still loading.",
|
||||||
|
"i18n:govoplan-dashboard.saving_reason": "The Dashboard layout is still being saved.",
|
||||||
|
"i18n:govoplan-dashboard.no_changes_reason": "There are no unsaved Dashboard changes.",
|
||||||
|
"i18n:govoplan-dashboard.invalid_widget_reason": "Complete the required widget settings with values inside their allowed ranges.",
|
||||||
|
"i18n:govoplan-dashboard.unsaved_layout_title": "Unsaved Dashboard layout",
|
||||||
|
"i18n:govoplan-dashboard.unsaved_layout_message": "Save or discard the Dashboard arrangement before leaving this page.",
|
||||||
|
"i18n:govoplan-dashboard.unsaved_widget_title": "Unapplied widget settings",
|
||||||
|
"i18n:govoplan-dashboard.unsaved_widget_message": "Apply or discard the widget settings before leaving this dialog.",
|
||||||
|
"Personal workspace assembled from installed module widgets.": "Personal workspace assembled from installed module widgets.",
|
||||||
|
"Refresh": "Refresh",
|
||||||
|
"Configure": "Configure",
|
||||||
|
"Cancel": "Cancel",
|
||||||
|
"Save layout": "Save layout",
|
||||||
|
"Saving...": "Saving...",
|
||||||
|
"Installed modules": "Installed modules",
|
||||||
|
"Available widgets": "Available widgets",
|
||||||
|
"On dashboard": "On dashboard",
|
||||||
|
"Layout": "Layout",
|
||||||
|
"Widget library": "Widget library",
|
||||||
|
"Search widgets": "Search widgets",
|
||||||
|
"Reset to module defaults": "Reset to module defaults",
|
||||||
|
"Widget size": "Widget size",
|
||||||
|
"Reset": "Reset",
|
||||||
|
"Apply": "Apply",
|
||||||
|
"Select an option": "Select an option",
|
||||||
|
"None": "None"
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const de: Record<keyof typeof en, string> = {
|
||||||
|
"i18n:govoplan-dashboard.dashboard.3f8b4df2": "Übersicht",
|
||||||
|
"i18n:govoplan-dashboard.summary": "Übersichtszusammenfassung",
|
||||||
|
"i18n:govoplan-dashboard.library": "Widget-Bibliothek",
|
||||||
|
"i18n:govoplan-dashboard.grid": "Übersichtsraster",
|
||||||
|
"i18n:govoplan-dashboard.widget_settings": "Widget-Einstellungen",
|
||||||
|
"i18n:govoplan-dashboard.installed_modules_widget": "Widget für installierte Module",
|
||||||
|
"i18n:govoplan-dashboard.loading_reason": "Die gespeicherte Übersichtsanordnung wird noch geladen.",
|
||||||
|
"i18n:govoplan-dashboard.saving_reason": "Die Übersichtsanordnung wird noch gespeichert.",
|
||||||
|
"i18n:govoplan-dashboard.no_changes_reason": "Es gibt keine ungespeicherten Änderungen an der Übersicht.",
|
||||||
|
"i18n:govoplan-dashboard.invalid_widget_reason": "Füllen Sie die erforderlichen Widget-Einstellungen mit Werten innerhalb der erlaubten Bereiche aus.",
|
||||||
|
"i18n:govoplan-dashboard.unsaved_layout_title": "Ungespeicherte Übersichtsanordnung",
|
||||||
|
"i18n:govoplan-dashboard.unsaved_layout_message": "Speichern oder verwerfen Sie die Übersichtsanordnung, bevor Sie diese Seite verlassen.",
|
||||||
|
"i18n:govoplan-dashboard.unsaved_widget_title": "Nicht angewendete Widget-Einstellungen",
|
||||||
|
"i18n:govoplan-dashboard.unsaved_widget_message": "Wenden Sie die Widget-Einstellungen an oder verwerfen Sie sie, bevor Sie den Dialog verlassen.",
|
||||||
|
"Personal workspace assembled from installed module widgets.": "Persönlicher Arbeitsbereich aus Widgets der installierten Module.",
|
||||||
|
"Refresh": "Aktualisieren",
|
||||||
|
"Configure": "Konfigurieren",
|
||||||
|
"Cancel": "Abbrechen",
|
||||||
|
"Save layout": "Anordnung speichern",
|
||||||
|
"Saving...": "Speichert...",
|
||||||
|
"Installed modules": "Installierte Module",
|
||||||
|
"Available widgets": "Verfügbare Widgets",
|
||||||
|
"On dashboard": "Auf der Übersicht",
|
||||||
|
"Layout": "Anordnung",
|
||||||
|
"Widget library": "Widget-Bibliothek",
|
||||||
|
"Search widgets": "Widgets suchen",
|
||||||
|
"Reset to module defaults": "Auf Modulvorgaben zurücksetzen",
|
||||||
|
"Widget size": "Widget-Größe",
|
||||||
|
"Reset": "Zurücksetzen",
|
||||||
|
"Apply": "Anwenden",
|
||||||
|
"Select an option": "Option auswählen",
|
||||||
|
"None": "Keine"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const generatedTranslations: PlatformTranslations = { en, de };
|
||||||
|
|||||||
+47
-2
@@ -69,17 +69,62 @@ export const dashboardModule: PlatformWebModule = {
|
|||||||
optionalDependencies: ["ops", "campaigns", "files", "mail", "tasks", "notifications", "reporting"],
|
optionalDependencies: ["ops", "campaigns", "files", "mail", "tasks", "notifications", "reporting"],
|
||||||
translations,
|
translations,
|
||||||
viewSurfaces: [
|
viewSurfaces: [
|
||||||
|
{
|
||||||
|
id: "dashboard.page",
|
||||||
|
moduleId: "dashboard",
|
||||||
|
kind: "route",
|
||||||
|
label: "i18n:govoplan-dashboard.dashboard.3f8b4df2",
|
||||||
|
order: 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "dashboard.summary",
|
||||||
|
moduleId: "dashboard",
|
||||||
|
kind: "section",
|
||||||
|
label: "i18n:govoplan-dashboard.summary",
|
||||||
|
parentId: "dashboard.page",
|
||||||
|
order: 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "dashboard.library",
|
||||||
|
moduleId: "dashboard",
|
||||||
|
kind: "section",
|
||||||
|
label: "i18n:govoplan-dashboard.library",
|
||||||
|
parentId: "dashboard.page",
|
||||||
|
order: 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "dashboard.grid",
|
||||||
|
moduleId: "dashboard",
|
||||||
|
kind: "section",
|
||||||
|
label: "i18n:govoplan-dashboard.grid",
|
||||||
|
parentId: "dashboard.page",
|
||||||
|
order: 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "dashboard.widget-settings",
|
||||||
|
moduleId: "dashboard",
|
||||||
|
kind: "action",
|
||||||
|
label: "i18n:govoplan-dashboard.widget_settings",
|
||||||
|
parentId: "dashboard.grid",
|
||||||
|
order: 40
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "dashboard.widget.installed-modules",
|
id: "dashboard.widget.installed-modules",
|
||||||
moduleId: "dashboard",
|
moduleId: "dashboard",
|
||||||
kind: "section",
|
kind: "section",
|
||||||
label: "Installed modules widget",
|
label: "i18n:govoplan-dashboard.installed_modules_widget",
|
||||||
|
parentId: "dashboard.grid",
|
||||||
order: 10
|
order: 10
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
navItems: [{ to: "/dashboard", label: "i18n:govoplan-dashboard.dashboard.3f8b4df2", iconName: "dashboard", order: 10 }],
|
navItems: [{ to: "/dashboard", label: "i18n:govoplan-dashboard.dashboard.3f8b4df2", iconName: "dashboard", order: 10 }],
|
||||||
routes: [
|
routes: [
|
||||||
{ path: "/dashboard", order: 10, render: ({ settings, auth }) => createElement(DashboardPage, { settings, auth }) }
|
{
|
||||||
|
path: "/dashboard",
|
||||||
|
order: 10,
|
||||||
|
surfaceId: "dashboard.page",
|
||||||
|
render: ({ settings, auth }) => createElement(DashboardPage, { settings, auth })
|
||||||
|
}
|
||||||
],
|
],
|
||||||
uiCapabilities: {
|
uiCapabilities: {
|
||||||
"dashboard.widgets": dashboardWidgets
|
"dashboard.widgets": dashboardWidgets
|
||||||
|
|||||||
Reference in New Issue
Block a user