feat(web): add widget config dialog and layout management controls
All checks were successful
ci/woodpecker/push/web Pipeline was successful

Add per-widget configuration dialog (WidgetConfigDialog) with settings
infrastructure ready for future configSchema-based forms. Add layout
management controls: reset to default, edit/done toggle, and widget
gear icon for configuration. Wire onEditWidget through WidgetGrid to
BaseWidget.

Implements TW-WDG-004 and TW-WDG-005.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-23 19:07:07 -06:00
parent f93fa60fff
commit c74d15ee3d
4 changed files with 253 additions and 21 deletions

View File

@@ -5,6 +5,7 @@ import type { ReactElement } from "react";
import type { WidgetPlacement } from "@mosaic/shared";
import { WidgetGrid } from "@/components/widgets/WidgetGrid";
import { WidgetPicker } from "@/components/widgets/WidgetPicker";
import { WidgetConfigDialog } from "@/components/widgets/WidgetConfigDialog";
import { DEFAULT_LAYOUT } from "@/components/widgets/defaultLayout";
import { fetchDefaultLayout, createLayout, updateLayout } from "@/lib/api/layouts";
import { useWorkspaceId } from "@/lib/hooks";
@@ -15,6 +16,7 @@ export default function DashboardPage(): ReactElement {
const [layoutId, setLayoutId] = useState<string | null>(null);
const [isEditing, setIsEditing] = useState(false);
const [isPickerOpen, setIsPickerOpen] = useState(false);
const [configWidgetId, setConfigWidgetId] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
// Debounce timer for auto-saving layout changes
@@ -106,6 +108,15 @@ export default function DashboardPage(): ReactElement {
[layout, saveLayout]
);
const handleResetLayout = useCallback((): void => {
setLayout(DEFAULT_LAYOUT);
saveLayout(DEFAULT_LAYOUT);
}, [saveLayout]);
const handleEditWidget = useCallback((widgetId: string): void => {
setConfigWidgetId(widgetId);
}, []);
if (isLoading) {
return (
<div className="flex items-center justify-center" style={{ minHeight: 400 }}>
@@ -138,24 +149,42 @@ export default function DashboardPage(): ReactElement {
</h1>
<div className="flex items-center gap-2">
{isEditing && (
<button
onClick={(): void => {
setIsPickerOpen(true);
}}
style={{
padding: "6px 14px",
borderRadius: "var(--r)",
border: "1px solid var(--border)",
background: "transparent",
color: "var(--text-2)",
fontSize: "0.83rem",
fontWeight: 500,
cursor: "pointer",
transition: "all 0.15s ease",
}}
>
+ Add Widget
</button>
<>
<button
onClick={handleResetLayout}
style={{
padding: "6px 14px",
borderRadius: "var(--r)",
border: "1px solid var(--border)",
background: "transparent",
color: "var(--muted)",
fontSize: "0.83rem",
fontWeight: 500,
cursor: "pointer",
transition: "all 0.15s ease",
}}
>
Reset
</button>
<button
onClick={(): void => {
setIsPickerOpen(true);
}}
style={{
padding: "6px 14px",
borderRadius: "var(--r)",
border: "1px solid var(--border)",
background: "transparent",
color: "var(--text-2)",
fontSize: "0.83rem",
fontWeight: 500,
cursor: "pointer",
transition: "all 0.15s ease",
}}
>
+ Add Widget
</button>
</>
)}
<button
onClick={(): void => {
@@ -184,9 +213,21 @@ export default function DashboardPage(): ReactElement {
layout={layout}
onLayoutChange={handleLayoutChange}
{...(isEditing && { onRemoveWidget: handleRemoveWidget })}
{...(isEditing && { onEditWidget: handleEditWidget })}
isEditing={isEditing}
/>
{/* Widget config dialog */}
{configWidgetId && (
<WidgetConfigDialog
widgetId={configWidgetId}
open
onClose={(): void => {
setConfigWidgetId(null);
}}
/>
)}
{/* Widget picker drawer */}
<WidgetPicker
open={isPickerOpen}