Migrate Dashboard interface patterns
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
import {
|
||||
Button,
|
||||
DismissibleAlert,
|
||||
DocumentationHelpLink,
|
||||
LoadingFrame,
|
||||
MetricCard,
|
||||
PageScrollViewport,
|
||||
@@ -36,6 +37,10 @@ import {
|
||||
import DashboardGrid from "./DashboardGrid";
|
||||
import WidgetConfigurationDialog from "./WidgetConfigurationDialog";
|
||||
import WidgetLibrary from "./WidgetLibrary";
|
||||
import {
|
||||
DASHBOARD_DOCUMENTATION,
|
||||
DASHBOARD_I18N
|
||||
} from "./interfacePatterns";
|
||||
import {
|
||||
appendPlacement,
|
||||
DASHBOARD_COLUMN_COUNT,
|
||||
@@ -184,8 +189,8 @@ export default function DashboardPage({
|
||||
dirty,
|
||||
onSave: persistLayout,
|
||||
onDiscard: discardChanges,
|
||||
title: "Unsaved Dashboard layout",
|
||||
message: "Save or discard the Dashboard arrangement before leaving this page."
|
||||
title: "i18n:govoplan-dashboard.unsaved_layout_title",
|
||||
message: "i18n:govoplan-dashboard.unsaved_layout_message"
|
||||
});
|
||||
|
||||
const activeLayout = configuring ? draftLayout : savedLayout;
|
||||
@@ -446,25 +451,39 @@ export default function DashboardPage({
|
||||
<p>Personal workspace assembled from installed module widgets.</p>
|
||||
</div>
|
||||
<div className="button-row compact-actions">
|
||||
<DocumentationHelpLink reference={DASHBOARD_DOCUMENTATION} />
|
||||
{!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
|
||||
</Button>
|
||||
<Button onClick={beginConfiguration}>
|
||||
<Button
|
||||
onClick={beginConfiguration}
|
||||
disabled={loading}
|
||||
disabledReason={loading ? DASHBOARD_I18N.loading : undefined}
|
||||
>
|
||||
<SlidersHorizontal size={16} /> Configure
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{configuring && (
|
||||
<>
|
||||
<Button onClick={discardChanges} disabled={saving}>
|
||||
<Button
|
||||
onClick={discardChanges}
|
||||
disabled={saving}
|
||||
disabledReason={saving ? DASHBOARD_I18N.saving : undefined}
|
||||
>
|
||||
<X size={16} /> Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => void persistLayout()}
|
||||
disabled={saving || !dirty}
|
||||
disabledReason={saving ? DASHBOARD_I18N.saving : !dirty ? DASHBOARD_I18N.noChanges : undefined}
|
||||
>
|
||||
<Save size={16} /> {saving ? "Saving..." : "Save layout"}
|
||||
</Button>
|
||||
|
||||
@@ -2,9 +2,12 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DocumentationHelpLink,
|
||||
FormField,
|
||||
SegmentedControl,
|
||||
ToggleSwitch,
|
||||
useUnsavedChanges,
|
||||
useUnsavedDraftGuard,
|
||||
type DashboardWidgetConfiguration,
|
||||
type DashboardWidgetConfigurationField,
|
||||
type DashboardWidgetConfigurationValue,
|
||||
@@ -16,6 +19,10 @@ import {
|
||||
supportedWidgetSizes,
|
||||
type DashboardWidgetPlacement
|
||||
} from "./dashboardLayout";
|
||||
import {
|
||||
DASHBOARD_I18N,
|
||||
DASHBOARD_LAYOUT_DOCUMENTATION
|
||||
} from "./interfacePatterns";
|
||||
|
||||
type WidgetConfigurationDialogProps = {
|
||||
open: boolean;
|
||||
@@ -32,8 +39,25 @@ export default function WidgetConfigurationDialog({
|
||||
onClose,
|
||||
onSave
|
||||
}: WidgetConfigurationDialogProps) {
|
||||
const { requestDiscard } = useUnsavedChanges();
|
||||
const [size, setSize] = useState<DashboardWidgetSize>("medium");
|
||||
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(() => {
|
||||
if (!open || !widget || !placement) return;
|
||||
@@ -58,6 +82,34 @@ export default function WidgetConfigurationDialog({
|
||||
[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;
|
||||
const supportedSizes = supportedWidgetSizes(widget);
|
||||
|
||||
@@ -74,26 +126,28 @@ export default function WidgetConfigurationDialog({
|
||||
<Dialog
|
||||
open={open}
|
||||
title={`Configure ${widget.title}`}
|
||||
onClose={onClose}
|
||||
onClose={close}
|
||||
className="dashboard-widget-config-dialog"
|
||||
footer={
|
||||
<>
|
||||
<Button type="button" variant="ghost" onClick={reset}>Reset</Button>
|
||||
<span className="dialog-footer-spacer" />
|
||||
<Button type="button" onClick={onClose}>Cancel</Button>
|
||||
<Button type="button" onClick={close}>Cancel</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
disabled={invalidConfiguration}
|
||||
onClick={() => onSave({ ...placement, size, configuration })}
|
||||
disabledReason={invalidConfiguration ? DASHBOARD_I18N.invalidWidget : undefined}
|
||||
onClick={applyDraft}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<DocumentationHelpLink reference={DASHBOARD_LAYOUT_DOCUMENTATION} />
|
||||
{supportedSizes.length > 1 && (
|
||||
<FormField label="Widget size">
|
||||
<FormField label="Widget size" documentation={DASHBOARD_LAYOUT_DOCUMENTATION}>
|
||||
<SegmentedControl
|
||||
options={supportedSizes.map((candidate) => ({
|
||||
id: candidate,
|
||||
@@ -142,7 +196,7 @@ function ConfigurationField({
|
||||
|
||||
if (field.kind === "select") {
|
||||
return (
|
||||
<FormField label={field.label} help={field.description}>
|
||||
<FormField label={field.label} help={field.description} documentation={DASHBOARD_LAYOUT_DOCUMENTATION}>
|
||||
<select
|
||||
value={typeof value === "string" ? value : ""}
|
||||
required={field.required}
|
||||
@@ -161,7 +215,7 @@ function ConfigurationField({
|
||||
|
||||
if (field.kind === "number") {
|
||||
return (
|
||||
<FormField label={field.label} help={field.description}>
|
||||
<FormField label={field.label} help={field.description} documentation={DASHBOARD_LAYOUT_DOCUMENTATION}>
|
||||
<input
|
||||
type="number"
|
||||
value={typeof value === "number" ? value : ""}
|
||||
@@ -180,7 +234,7 @@ function ConfigurationField({
|
||||
}
|
||||
|
||||
return (
|
||||
<FormField label={field.label} help={field.description}>
|
||||
<FormField label={field.label} help={field.description} documentation={DASHBOARD_LAYOUT_DOCUMENTATION}>
|
||||
<input
|
||||
type="text"
|
||||
value={typeof value === "string" ? value : ""}
|
||||
|
||||
@@ -66,6 +66,7 @@ export default function WidgetLibrary({
|
||||
label={`Add ${widget.title}`}
|
||||
icon={<Plus size={16} />}
|
||||
variant="ghost"
|
||||
disabled={atCapacity}
|
||||
disabledReason={
|
||||
atCapacity
|
||||
? "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;
|
||||
Reference in New Issue
Block a user