mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
Until v1.4.14 the layout schema in `lib/dashboard-layout.ts` had a single `visible` flag per widget that controlled BOTH the upper-row strip tile AND the lower-row chart for the same metric. Marc wanted them as independent toggles — to keep the chart visible for tracking without the tile cluttering the strip, or vice versa. Schema upgrade (back-compat): - New optional `tileVisible: boolean` on `DashboardWidgetConfig`. When omitted on a saved layout, `resolveDashboardLayout()` mirrors it from `visible` so legacy users see no behavioural change until they flip the new switch. `serializeDashboardLayout()` always writes the field explicitly so a re-read keeps the user's choice. - Zod input schema (`/api/dashboard/widgets`) marks the field optional so older clients (web v1.4.14 cache, native iOS) can keep PUTting their existing payload shape. - Default layout sets `tileVisible: true` for the seven default-on widgets and `tileVisible: false` for the six default-off ones. Settings UI: - The single-column toggle list grows a SECOND switch column. Header row labels them "Tile" / "Kachel" and "Chart" / "Chart". Each widget now paints two switches (`data-slot="widget-tile-switch"` and `widget-chart-switch`) with discrete `aria-label`s `<metric> — Tile` / `<metric> — Chart` so a screen-reader user can tell them apart. Dashboard render (`src/app/page.tsx`): - `isWidgetVisible(id)` split into `isTileVisible(id)` (new flag with mirror fallback) and `isChartVisible(id)` (legacy flag). Each metric now has a `showXxxTile` AND `showXxxChart` gate. Glucose has no separate chart slot today, so its single flag still maps to the tile flag. Tests: - `resolveDashboardLayout()`: mirrors tileVisible from visible for legacy layouts, respects an explicit value, auto-appends new widgets with both flags `false`, defaults gracefully on null input. - `serializeDashboardLayout()`: persists tileVisible explicitly, derives from visible when missing, normalises order to dense 0-based. - `<DashboardLayoutSection>` SSR smoke: column headers paint EN + DE, both switch slots present, exactly one tile-switch + one chart-switch per widget. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
134 lines
4.6 KiB
TypeScript
134 lines
4.6 KiB
TypeScript
/**
|
|
* Dashboard widget layout — persisted in User.dashboardWidgetsJson.
|
|
*
|
|
* Single source of truth for which widgets show on /, what order, and
|
|
* what the default layout is for new users. Null / missing = default.
|
|
*/
|
|
|
|
export type DashboardWidgetId =
|
|
| "weight"
|
|
| "bp"
|
|
| "pulse"
|
|
| "bodyFat"
|
|
| "mood"
|
|
| "medications"
|
|
| "sleep"
|
|
| "steps"
|
|
| "glucose"
|
|
| "totalBodyWater"
|
|
| "boneMass"
|
|
| "bpInTarget"
|
|
| "oxygenSaturation";
|
|
|
|
export interface DashboardWidgetConfig {
|
|
id: DashboardWidgetId;
|
|
/**
|
|
* Whether the widget shows up in the *charts* row (the lower section
|
|
* of the dashboard with the line graphs). The legacy single-toggle
|
|
* field — kept on the wire for back-compat with users who saved a
|
|
* layout pre-v1.4.15.
|
|
*/
|
|
visible: boolean;
|
|
/**
|
|
* v1.4.15 Fix 5 — independent visibility for the *strip tile* (the
|
|
* upper row of trend cards). When omitted, the resolver mirrors
|
|
* `visible` so existing saved layouts keep their previous behaviour
|
|
* (one toggle controls both surfaces). When set explicitly the user
|
|
* can show the chart but hide the tile, or show the tile but hide
|
|
* the chart, which is what Marc asked for in the v1.4.15 follow-up
|
|
* (memory `feedback_dashboard_top_tiles_selectable.md`).
|
|
*/
|
|
tileVisible?: boolean;
|
|
order: number;
|
|
}
|
|
|
|
export interface DashboardLayout {
|
|
version: number;
|
|
widgets: DashboardWidgetConfig[];
|
|
}
|
|
|
|
export const DASHBOARD_LAYOUT_VERSION = 1;
|
|
|
|
/**
|
|
* Default layout mirrors v1.1 behavior: core vitals always on, mood +
|
|
* body-fat show if data exists (see dashboard page for the data-conditional
|
|
* rendering). Ordering matches the visual order on / today.
|
|
*/
|
|
export const DEFAULT_DASHBOARD_LAYOUT: DashboardLayout = {
|
|
version: DASHBOARD_LAYOUT_VERSION,
|
|
widgets: [
|
|
{ id: "weight", visible: true, tileVisible: true, order: 0 },
|
|
{ id: "bp", visible: true, tileVisible: true, order: 1 },
|
|
{ id: "pulse", visible: true, tileVisible: true, order: 2 },
|
|
{ id: "bodyFat", visible: true, tileVisible: true, order: 3 },
|
|
{ id: "mood", visible: true, tileVisible: true, order: 4 },
|
|
{ id: "bpInTarget", visible: true, tileVisible: true, order: 5 },
|
|
{ id: "medications", visible: true, tileVisible: true, order: 6 },
|
|
{ id: "sleep", visible: false, tileVisible: false, order: 7 },
|
|
{ id: "steps", visible: false, tileVisible: false, order: 8 },
|
|
{ id: "glucose", visible: false, tileVisible: false, order: 9 },
|
|
{ id: "totalBodyWater", visible: false, tileVisible: false, order: 10 },
|
|
{ id: "boneMass", visible: false, tileVisible: false, order: 11 },
|
|
{ id: "oxygenSaturation", visible: false, tileVisible: false, order: 12 },
|
|
],
|
|
};
|
|
|
|
export function resolveDashboardLayout(raw: unknown): DashboardLayout {
|
|
if (!raw || typeof raw !== "object") return DEFAULT_DASHBOARD_LAYOUT;
|
|
const candidate = raw as Partial<DashboardLayout>;
|
|
if (
|
|
typeof candidate.version !== "number" ||
|
|
!Array.isArray(candidate.widgets)
|
|
) {
|
|
return DEFAULT_DASHBOARD_LAYOUT;
|
|
}
|
|
|
|
// Merge with defaults so new widgets introduced in later versions show up
|
|
// automatically (invisible by default, users opt-in).
|
|
const savedIds = new Set(candidate.widgets.map((w) => w.id));
|
|
const missing = DEFAULT_DASHBOARD_LAYOUT.widgets.filter(
|
|
(w) => !savedIds.has(w.id),
|
|
);
|
|
const maxOrder = Math.max(0, ...candidate.widgets.map((w) => w.order));
|
|
const appended = missing.map((w, i) => ({
|
|
...w,
|
|
visible: false, // default-invisible on auto-upgrade
|
|
tileVisible: false,
|
|
order: maxOrder + 1 + i,
|
|
}));
|
|
|
|
// Default `tileVisible` to mirror `visible` for legacy entries that
|
|
// never had the field (the v1.4.15 schema upgrade). Users who saved
|
|
// a layout before this release see no behavioural change until they
|
|
// explicitly toggle the new strip switch.
|
|
const normalized = candidate.widgets.map((w) => ({
|
|
...w,
|
|
tileVisible: typeof w.tileVisible === "boolean" ? w.tileVisible : w.visible,
|
|
}));
|
|
|
|
return {
|
|
version: DASHBOARD_LAYOUT_VERSION,
|
|
widgets: [...normalized, ...appended].sort((a, b) => a.order - b.order),
|
|
};
|
|
}
|
|
|
|
export function serializeDashboardLayout(
|
|
layout: DashboardLayout,
|
|
): DashboardLayout {
|
|
return {
|
|
version: DASHBOARD_LAYOUT_VERSION,
|
|
widgets: layout.widgets
|
|
.slice()
|
|
.sort((a, b) => a.order - b.order)
|
|
.map((w, i) => ({
|
|
...w,
|
|
// Persist tileVisible explicitly so a follow-up read of the
|
|
// saved layout doesn't have to re-mirror from `visible` and
|
|
// discard the user's actual choice.
|
|
tileVisible:
|
|
typeof w.tileVisible === "boolean" ? w.tileVisible : w.visible,
|
|
order: i, // normalize to 0-based dense order
|
|
})),
|
|
};
|
|
}
|