From 7eeb2f15bcab4cfbb33ba1138449b74d01ff5953 Mon Sep 17 00:00:00 2001 From: ginnoir Date: Sat, 4 Jul 2026 18:10:55 -0500 Subject: [PATCH] fix: dashboard edit mode renders live widgets --- src/app/d/[slug]/page.tsx | 16 ++++++++++- src/components/dashboard-editor.tsx | 15 +++++++--- src/components/dashboard-widget-content.tsx | 32 +++++++++++++++++++++ src/lib/dashboard.ts | 4 +++ tests/e2e/dashboard.spec.ts | 31 ++++++++++++++++++-- 5 files changed, 91 insertions(+), 7 deletions(-) create mode 100644 src/components/dashboard-widget-content.tsx diff --git a/src/app/d/[slug]/page.tsx b/src/app/d/[slug]/page.tsx index 95318d1..c9e3739 100644 --- a/src/app/d/[slug]/page.tsx +++ b/src/app/d/[slug]/page.tsx @@ -1,11 +1,12 @@ import { notFound } from "next/navigation"; import { Suspense } from "react"; import { getCurrentSession } from "@/lib/session"; -import { parseDashboardLayout } from "@/lib/dashboard"; +import { parseDashboardLayout, widgetContentKey } from "@/lib/dashboard"; import { computeDefaultLayout } from "@/lib/dashboard.server"; import { getWidget, getWidgetMetas } from "@/modules/_core"; import { getDashboardBySlug } from "@/app/d/actions"; import { DashboardEditor } from "@/components/dashboard-editor"; +import { DashboardWidgetContent } from "@/components/dashboard-widget-content"; import { EditDashboardButton } from "@/components/edit-dashboard-button"; import { DashboardSwitcher } from "@/components/dashboard-switcher"; import { DashboardTab } from "@/components/dashboard-tab"; @@ -52,11 +53,24 @@ export default async function DashboardPage({ const widgetMetas = getWidgetMetas(); if (isEditing) { + const ctx = { userId: user.id, householdId: household.id }; + const widgetContents = Object.fromEntries( + layout.widgets.map((placement) => [ + widgetContentKey(placement), + , + ]), + ); + return ( ); } diff --git a/src/components/dashboard-editor.tsx b/src/components/dashboard-editor.tsx index 450691b..9f5151b 100644 --- a/src/components/dashboard-editor.tsx +++ b/src/components/dashboard-editor.tsx @@ -3,13 +3,13 @@ import "react-grid-layout/css/styles.css"; import "react-resizable/css/styles.css"; -import { useEffect, useState, useTransition } from "react"; +import { useEffect, useState, useTransition, type ReactNode } from "react"; import { useRouter } from "next/navigation"; import { GridLayout } from "react-grid-layout"; import type { Layout } from "react-grid-layout"; import { GripVertical, Settings2, Trash2, RotateCcw, Plus, LayoutGrid } from "lucide-react"; import type { DashboardLayout, WidgetPlacement, PresetId } from "@/lib/dashboard"; -import { computePresetLayoutFromMetas } from "@/lib/dashboard"; +import { computePresetLayoutFromMetas, widgetContentKey } from "@/lib/dashboard"; import type { SerializedWidgetMeta } from "@/modules/_core/registry"; import { saveDashboardLayout, resetDashboardLayout } from "@/app/d/actions"; import { Button } from "@/components/ui/button"; @@ -23,10 +23,12 @@ export function DashboardEditor({ dashboard, layout: initialLayout, widgetMetas, + widgetContents, }: { dashboard: { id: string; name: string; slug: string }; layout: DashboardLayout; widgetMetas: SerializedWidgetMeta[]; + widgetContents: Record; }) { const router = useRouter(); const [isPending, startTransition] = useTransition(); @@ -213,6 +215,7 @@ export function DashboardEditor({ > {placements.map((placement, i) => { const meta = widgetMetas.find((m) => m.id === placement.widgetId); + const content = widgetContents[widgetContentKey(placement)]; return (
-
-

{meta?.description}

+
+ {content ?? ( +

+ {meta?.description ?? "Preview available after save"} +

+ )}
); diff --git a/src/components/dashboard-widget-content.tsx b/src/components/dashboard-widget-content.tsx new file mode 100644 index 0000000..d1007ef --- /dev/null +++ b/src/components/dashboard-widget-content.tsx @@ -0,0 +1,32 @@ +import { Suspense } from "react"; +import type { WidgetPlacement } from "@/lib/dashboard"; +import { getWidget } from "@/modules/_core"; +import type { WidgetContext } from "@/modules/_core/module"; +import { Skeleton } from "@/components/ui/skeleton"; + +export function DashboardWidgetContent({ + placement, + ctx, +}: { + placement: WidgetPlacement; + ctx: WidgetContext; +}) { + const widget = getWidget(placement.widgetId); + if (!widget) { + return

Unknown widget

; + } + + return ( + + + + + + } + > + {widget.render({ config: placement.config, ctx })} + + ); +} diff --git a/src/lib/dashboard.ts b/src/lib/dashboard.ts index 371faba..e3cb42f 100644 --- a/src/lib/dashboard.ts +++ b/src/lib/dashboard.ts @@ -10,6 +10,10 @@ export type WidgetPlacement = { h: number; }; +export function widgetContentKey(placement: Pick): string { + return `${placement.widgetId}::${JSON.stringify(placement.config)}`; +} + export type DashboardLayout = { version: 1; widgets: WidgetPlacement[]; diff --git a/tests/e2e/dashboard.spec.ts b/tests/e2e/dashboard.spec.ts index b4551b3..297888c 100644 --- a/tests/e2e/dashboard.spec.ts +++ b/tests/e2e/dashboard.spec.ts @@ -1,7 +1,17 @@ -import { expect, test } from "@playwright/test"; +import { expect, test, type Page } from "@playwright/test"; + +async function ensureSignedIn(page: Page) { + await page.goto("/"); + const devLogin = page.getByRole("button", { name: "Dev login" }); + if (await devLogin.isVisible().catch(() => false)) { + await devLogin.click(); + await page.waitForURL((url) => !url.pathname.startsWith("/login")); + } + await expect(page.getByText("Upcoming events", { exact: true })).toBeVisible(); +} test("dashboard happy path", async ({ page }) => { - await page.goto("/"); + await ensureSignedIn(page); // Page title is present await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible(); @@ -24,3 +34,20 @@ test("dashboard happy path", async ({ page }) => { const viewportWidth = await page.evaluate(() => window.innerWidth); expect(bodyWidth).toBeLessThanOrEqual(viewportWidth + 2); // 2px tolerance for sub-pixel rendering }); + +test("dashboard edit mode shows live widget content", async ({ page }) => { + await ensureSignedIn(page); + await page.goto("/d/home?edit=1"); + + await expect(page.getByText("Drag to reorder")).toBeVisible(); + await expect(page.getByRole("button", { name: "Save" })).toBeVisible(); + + const main = page.getByRole("main"); + await expect(main.getByText("Recent activity", { exact: true })).toBeVisible(); + + // Server-rendered widget body (empty-state copy), not the meta description placeholder + await expect( + main.getByText(/No recent activity|No events in the next|No pinned notes|No notes/i).first(), + ).toBeVisible(); + await expect(main.getByText("Latest changes across your household.")).not.toBeVisible(); +});