Skip to content

Commit e216739

Browse files
authored
Merge pull request #66 from homanp/cursor/fix-dashboard-switch-leak
fix: normalize active dashboard selection to avoid showing widgets from other dashboards
2 parents 8599049 + b945885 commit e216739

File tree

4 files changed

+88
-21
lines changed

4 files changed

+88
-21
lines changed

src/components/dashboard-grid.tsx

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { useMemo, useCallback, useState, useEffect, useRef } from "react";
44
import { LayoutGrid, TrendingUp, Shield, Globe } from "lucide-react";
5-
import { useWidgetStore } from "@/store/widget-store";
5+
import { resolveActiveDashboardId, useWidgetStore } from "@/store/widget-store";
66
import { WidgetCard } from "@/components/widget-card";
77
import { TextBlockItem } from "@/components/text-block-item";
88
import { deleteWidgetFromDb, deleteTextBlockFromDb, scheduleSyncToServer } from "@/lib/sync-db";
@@ -185,31 +185,35 @@ export function DashboardGrid() {
185185
return () => observer.disconnect();
186186
}, []);
187187

188-
const activeDashboard = dashboards.find((d) => d.id === activeDashboardId);
188+
const resolvedActiveDashboardId = useMemo(
189+
() => resolveActiveDashboardId(dashboards, activeDashboardId),
190+
[dashboards, activeDashboardId],
191+
);
192+
const activeDashboard = dashboards.find((d) => d.id === resolvedActiveDashboardId);
189193

190194
const widgets = useMemo(() => {
191-
if (!activeDashboard) return allWidgets;
195+
if (!activeDashboard) return dashboards.length === 0 ? allWidgets : [];
192196
return allWidgets.filter((w) => activeDashboard.widgetIds.includes(w.id));
193-
}, [allWidgets, activeDashboard]);
197+
}, [allWidgets, activeDashboard, dashboards.length]);
194198

195199
const textBlocks = useMemo(() => {
196-
if (!activeDashboard) return allTextBlocks;
200+
if (!activeDashboard) return dashboards.length === 0 ? allTextBlocks : [];
197201
return allTextBlocks.filter((tb) => (activeDashboard.textBlockIds ?? []).includes(tb.id));
198-
}, [allTextBlocks, activeDashboard]);
202+
}, [allTextBlocks, activeDashboard, dashboards.length]);
199203

200204
const DEFAULT_VIEWPORT = { panX: 24, panY: 60, zoom: 1 };
201205

202-
const viewport = activeDashboardId
203-
? viewports[activeDashboardId] ?? DEFAULT_VIEWPORT
206+
const viewport = resolvedActiveDashboardId
207+
? viewports[resolvedActiveDashboardId] ?? DEFAULT_VIEWPORT
204208
: DEFAULT_VIEWPORT;
205209

206210
const handleViewportChange = useCallback(
207211
(panX: number, panY: number, zoom: number) => {
208-
if (activeDashboardId) {
209-
setViewport(activeDashboardId, { panX, panY, zoom });
212+
if (resolvedActiveDashboardId) {
213+
setViewport(resolvedActiveDashboardId, { panX, panY, zoom });
210214
}
211215
},
212-
[activeDashboardId, setViewport]
216+
[resolvedActiveDashboardId, setViewport]
213217
);
214218

215219
const handleRemove = useCallback(

src/components/dashboard-picker.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ChevronDown, Plus, Pencil, Check, Trash2, LayoutDashboard } from "lucid
55
import { Button } from "@/components/ui/button";
66
import { Input } from "@/components/ui/input";
77
import { cn } from "@/lib/utils";
8-
import { useWidgetStore } from "@/store/widget-store";
8+
import { resolveActiveDashboardId, useWidgetStore } from "@/store/widget-store";
99
import { scheduleSyncToServer } from "@/lib/sync-db";
1010

1111
export function DashboardPicker() {
@@ -25,7 +25,8 @@ export function DashboardPicker() {
2525
const editInputRef = useRef<HTMLInputElement>(null);
2626
const newInputRef = useRef<HTMLInputElement>(null);
2727

28-
const activeDashboard = dashboards.find((d) => d.id === activeDashboardId);
28+
const resolvedActiveDashboardId = resolveActiveDashboardId(dashboards, activeDashboardId);
29+
const activeDashboard = dashboards.find((d) => d.id === resolvedActiveDashboardId);
2930

3031
useEffect(() => {
3132
if (!open) return;
@@ -141,7 +142,7 @@ export function DashboardPicker() {
141142
onClick={() => editingId !== d.id && handleSelect(d.id)}
142143
className={cn(
143144
"flex items-center gap-2 px-3 py-1.5 text-xs uppercase tracking-wider cursor-pointer transition-colors",
144-
d.id === activeDashboardId
145+
d.id === resolvedActiveDashboardId
145146
? "text-zinc-100 bg-zinc-700/50"
146147
: "text-zinc-400 hover:text-zinc-200 hover:bg-zinc-700/30"
147148
)}
@@ -168,9 +169,9 @@ export function DashboardPicker() {
168169
<button
169170
onClick={(e) => handleStartEdit(e, d.id, d.title)}
170171
className="opacity-0 group-hover:opacity-100 text-zinc-500 hover:text-zinc-300 shrink-0"
171-
style={{ opacity: d.id === activeDashboardId ? 0.6 : 0 }}
172+
style={{ opacity: d.id === resolvedActiveDashboardId ? 0.6 : 0 }}
172173
onMouseEnter={(e) => { (e.currentTarget as HTMLElement).style.opacity = "1"; }}
173-
onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.opacity = d.id === activeDashboardId ? "0.6" : "0"; }}
174+
onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.opacity = d.id === resolvedActiveDashboardId ? "0.6" : "0"; }}
174175
>
175176
<Pencil className="h-3 w-3" />
176177
</button>

src/store/__tests__/widget-store.test.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { beforeEach, describe, expect, it } from "vitest";
2-
import { getNextWidgetInsertionY, shiftItemsDown, useWidgetStore } from "@/store/widget-store";
2+
import {
3+
getNextWidgetInsertionY,
4+
resolveActiveDashboardId,
5+
shiftItemsDown,
6+
useWidgetStore,
7+
} from "@/store/widget-store";
38
import type { Dashboard, TextBlock, Widget } from "@/store/widget-store";
49

510
function makeWidget(id: string, x: number, y: number, w: number, h: number): Widget {
@@ -111,6 +116,24 @@ describe("getNextWidgetInsertionY", () => {
111116
});
112117
});
113118

119+
describe("resolveActiveDashboardId", () => {
120+
it("keeps the current dashboard when it still exists", () => {
121+
const dashboards = [makeDashboard("dash-1"), makeDashboard("dash-2")];
122+
123+
expect(resolveActiveDashboardId(dashboards, "dash-2")).toBe("dash-2");
124+
});
125+
126+
it("falls back to the first dashboard when the stored id is stale", () => {
127+
const dashboards = [makeDashboard("dash-1"), makeDashboard("dash-2")];
128+
129+
expect(resolveActiveDashboardId(dashboards, "missing")).toBe("dash-1");
130+
});
131+
132+
it("returns null when there are no dashboards", () => {
133+
expect(resolveActiveDashboardId([], "missing")).toBeNull();
134+
});
135+
});
136+
114137
describe("addWidget", () => {
115138
it("places the first widget at the baseline row", () => {
116139
const widgetId = useWidgetStore.getState().addWidget("First");
@@ -165,3 +188,27 @@ describe("addWidget", () => {
165188
});
166189
});
167190

191+
describe("dashboard selection", () => {
192+
it("falls back to the first dashboard when selecting a stale id", () => {
193+
useWidgetStore.setState({
194+
dashboards: [makeDashboard("dash-1"), makeDashboard("dash-2")],
195+
activeDashboardId: "dash-1",
196+
});
197+
198+
useWidgetStore.getState().setActiveDashboard("missing");
199+
200+
expect(useWidgetStore.getState().activeDashboardId).toBe("dash-1");
201+
});
202+
203+
it("moves selection to the next available dashboard when removing the active one", () => {
204+
useWidgetStore.setState({
205+
dashboards: [makeDashboard("dash-1"), makeDashboard("dash-2")],
206+
activeDashboardId: "dash-1",
207+
});
208+
209+
useWidgetStore.getState().removeDashboard("dash-1");
210+
211+
expect(useWidgetStore.getState().activeDashboardId).toBe("dash-2");
212+
});
213+
});
214+

src/store/widget-store.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,17 @@ export function getNextWidgetInsertionY(
161161
return Number.isFinite(topY) ? topY - newHeight : 0;
162162
}
163163

164+
export function resolveActiveDashboardId(
165+
dashboards: Dashboard[],
166+
activeDashboardId: string | null | undefined,
167+
): string | null {
168+
if (dashboards.length === 0) return null;
169+
if (activeDashboardId && dashboards.some((dashboard) => dashboard.id === activeDashboardId)) {
170+
return activeDashboardId;
171+
}
172+
return dashboards[0].id;
173+
}
174+
164175
export const useWidgetStore = create<WidgetStore>()(
165176
persist(
166177
(set, get) => ({
@@ -196,13 +207,17 @@ export const useWidgetStore = create<WidgetStore>()(
196207
const widgetIds = dashboard?.widgetIds ?? [];
197208
const textBlockIds = dashboard?.textBlockIds ?? [];
198209
set((state) => {
210+
const dashboards = state.dashboards.filter((d) => d.id !== id);
199211
const nextActions = { ...state.currentActions };
200212
for (const wid of widgetIds) delete nextActions[wid];
201213
return {
202-
dashboards: state.dashboards.filter((d) => d.id !== id),
214+
dashboards,
203215
widgets: state.widgets.filter((w) => !widgetIds.includes(w.id)),
204216
textBlocks: state.textBlocks.filter((tb) => !textBlockIds.includes(tb.id)),
205-
activeDashboardId: state.activeDashboardId === id ? null : state.activeDashboardId,
217+
activeDashboardId: resolveActiveDashboardId(
218+
dashboards,
219+
state.activeDashboardId === id ? null : state.activeDashboardId,
220+
),
206221
activeWidgetId: widgetIds.includes(state.activeWidgetId ?? "") ? null : state.activeWidgetId,
207222
streamingWidgetIds: state.streamingWidgetIds.filter((wid) => !widgetIds.includes(wid)),
208223
reasoningStreamingIds: state.reasoningStreamingIds.filter((wid) => !widgetIds.includes(wid)),
@@ -212,7 +227,7 @@ export const useWidgetStore = create<WidgetStore>()(
212227
},
213228

214229
setActiveDashboard: (id) => {
215-
set({ activeDashboardId: id });
230+
set({ activeDashboardId: resolveActiveDashboardId(get().dashboards, id) });
216231
},
217232

218233
addWidget: (title = "Untitled Widget", description = "") => {
@@ -600,7 +615,7 @@ export const useWidgetStore = create<WidgetStore>()(
600615
...current,
601616
...stored,
602617
dashboards,
603-
activeDashboardId: stored.activeDashboardId ?? dashboards[0]?.id ?? null,
618+
activeDashboardId: resolveActiveDashboardId(dashboards, stored.activeDashboardId ?? null),
604619
streamingWidgetIds: [],
605620
currentActions: {},
606621
reasoningStreamingIds: [],

0 commit comments

Comments
 (0)