dashboards: drive list selection from a ?selected= url param, not a global

the /dashboards list row was a <div onClick> that set SELECTED_DASHBOARD_KEY (a
global) with no url change on-page, so selection wasn't deep-linkable and the row
wasn't a real link. selection now lives in the url as ?selected=<id>, read by the
list, the screen (mobile panel), and the preview. rows are real <Link>s (edit/delete
kept as sibling buttons, not nested in the anchor); the preview + its "open" link are
unchanged. edit-save and delete update/clear the param.

NOT runtime-tested yet (server not restarted) — verify the /dashboards preview,
create/edit/delete, and mobile panel flows on next restart.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 05:32:10 +00:00
co-authored by Claude Opus 4.8
parent 04073dc6a0
commit 1b6b980af0
3 changed files with 62 additions and 44 deletions
@@ -1,14 +1,16 @@
import type { LayoutNode } from 'officerdev';
import { WorkspaceView, SELECTED_DASHBOARD_KEY } from 'officerdev';
import { useSearchParams } from 'react-router';
import { WorkspaceView } from 'officerdev';
import { useIsMobile } from 'hooks/useIsMobile';
import { useGlobal } from 'hooks/useGlobal';
import { useDashboardState } from 'state/useDashboardState';
import { defaultLayout } from './defaultLayout';
export const DashboardsScreen = () => {
const workspace = useDashboardState<LayoutNode>('screens/dashboards', defaultLayout);
const isMobile = useIsMobile();
const [selected, setSelected] = useGlobal<string | null>(SELECTED_DASHBOARD_KEY, null);
// Selection lives in the URL (?selected=<id>); on mobile it drives which panel is shown.
const [searchParams, setSearchParams] = useSearchParams();
const selected = searchParams.get('selected');
const mobilePanelId = isMobile && selected ? 'ws-home-right' : undefined;
return (
@@ -18,7 +20,14 @@ export const DashboardsScreen = () => {
locked
mobilePanelId={mobilePanelId}
onMobilePanelChange={(id) => {
if (!id) setSelected(null);
if (!id)
setSearchParams(
(p) => {
p.delete('selected');
return p;
},
{ replace: true },
);
}}
/>
</div>
@@ -1,5 +1,5 @@
import { useState } from 'react';
import { Link, useLocation, useNavigate } from 'react-router';
import { Link, useLocation, useNavigate, useSearchParams } from 'react-router';
import { LayoutGrid, Plus, Pencil, Trash2, Search } from 'lucide-react';
import { useQueryClient } from '@tanstack/react-query';
import { useGlobal } from 'hooks/useGlobal';
@@ -19,7 +19,6 @@ import {
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import {
SELECTED_DASHBOARD_KEY,
CREATING_DASHBOARD_KEY,
EDITING_DASHBOARD_KEY,
NEW_DASH_NAME_KEY,
@@ -33,7 +32,17 @@ export const DashboardListApp = () => {
const client = useClient();
const queryClient = useQueryClient();
const { value: dashboards, setValue: setDashboards } = useDashboardState<DashboardDefinition[]>('workspaces', []);
const [selected, setSelected] = useGlobal<string | null>(SELECTED_DASHBOARD_KEY, null);
// Selection is URL state now (?selected=<id>), not a global — so it's deep-linkable and the rows are real links.
const [searchParams, setSearchParams] = useSearchParams();
const selected = searchParams.get('selected');
const clearSelected = () =>
setSearchParams(
(p) => {
p.delete('selected');
return p;
},
{ replace: true },
);
const [, setCreating] = useGlobal<boolean>(CREATING_DASHBOARD_KEY, false);
const [, setEditing] = useGlobal<string | null>(EDITING_DASHBOARD_KEY, null);
const [, setName] = useGlobal<string>(NEW_DASH_NAME_KEY, '');
@@ -53,7 +62,7 @@ export const DashboardListApp = () => {
const handleEdit = (ev: React.MouseEvent, ws: DashboardDefinition) => {
ev.stopPropagation();
setSelected(null);
clearSelected();
setCreating(false);
setEditing(ws.id);
setName(ws.name);
@@ -71,7 +80,7 @@ export const DashboardListApp = () => {
const confirmDelete = () => {
if (!deleting) return;
setDashboards((prev) => prev.filter((w) => w.id !== deleting.id));
if (selected === deleting.id) setSelected(null);
if (selected === deleting.id) clearSelected();
const layoutKey = `ws-layout-${deleting.id}`;
const currentState = queryClient.getQueryData<Record<string, unknown>>(['DASHBOARD_STATE']) ?? {};
@@ -81,16 +90,6 @@ export const DashboardListApp = () => {
setDeleting(null);
};
const handleClick = (ws: DashboardDefinition) => {
if (isDashboardsPage) {
setSelected(ws.id);
setCreating(false);
setEditing(null);
} else {
navigate(`/dashboards/${ws.id}`);
}
};
return (
<div className="flex flex-col h-full overflow-y-auto">
<div className="p-3 pb-0 flex flex-col gap-2">
@@ -104,7 +103,7 @@ export const DashboardListApp = () => {
<button
type="button"
onClick={() => {
setSelected(null);
clearSelected();
setEditing(null);
setName(generateSlug());
setDescription('');
@@ -133,17 +132,21 @@ export const DashboardListApp = () => {
{filtered.map((ws) => (
<div
key={ws.id}
className={`flex items-center gap-2.5 py-2 px-3 rounded-lg text-sm font-medium cursor-pointer group ${
className={`flex items-center rounded-lg text-sm font-medium group ${
isDashboardsPage && selected === ws.id
? 'bg-duck-teal/10 text-duck-teal'
: 'text-duck-dark/80 dark:text-white/80 hover:bg-duck-dark/5'
}`}
onClick={() => handleClick(ws)}
>
<LayoutGrid className="h-4 w-4 shrink-0" />
<span className="flex-1 text-left truncate">{ws.name}</span>
<Link
to={isDashboardsPage ? `/dashboards?selected=${ws.id}` : `/dashboards/${ws.id}`}
className="flex flex-1 items-center gap-2.5 py-2 px-3 min-w-0 cursor-pointer"
>
<LayoutGrid className="h-4 w-4 shrink-0" />
<span className="flex-1 text-left truncate">{ws.name}</span>
</Link>
{isDashboardsPage && (
<>
<div className="flex shrink-0 items-center pr-2">
<button
type="button"
onClick={(ev) => handleEdit(ev, ws)}
@@ -158,18 +161,21 @@ export const DashboardListApp = () => {
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</>
</div>
)}
</div>
))}
{filtered.length === 0 && (
<p className="text-xs text-gray-500 px-3 py-4 text-center">
{search ? 'No matches' : 'No dashboards yet'}
</p>
<p className="text-xs text-gray-500 px-3 py-4 text-center">{search ? 'No matches' : 'No dashboards yet'}</p>
)}
</div>
<AlertDialog open={deleting !== null} onOpenChange={(open) => { if (!open) setDeleting(null); }}>
<AlertDialog
open={deleting !== null}
onOpenChange={(open) => {
if (!open) setDeleting(null);
}}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete dashboard</AlertDialogTitle>
@@ -1,5 +1,5 @@
import { useState } from 'react';
import { Link, useNavigate } from 'react-router';
import { Link, useNavigate, useSearchParams } from 'react-router';
import { FolderOpen, LayoutGrid, Plus, ArrowRight, Type, Rocket, FileText, Layout } from 'lucide-react';
import { useQueryClient } from '@tanstack/react-query';
import { useGlobal } from 'hooks/useGlobal';
@@ -13,7 +13,6 @@ import { generateSlug, slugify } from 'helpers/slug';
import { FileBrowserApp } from '../FileBrowser';
import { useAppRegistry } from '../../AppRegistry';
import {
SELECTED_DASHBOARD_KEY,
CREATING_DASHBOARD_KEY,
EDITING_DASHBOARD_KEY,
NEW_DASH_NAME_KEY,
@@ -221,7 +220,10 @@ const tplPanelLayout: LayoutNode = {
};
const tplRegistry = Object.fromEntries(
templates.map((tpl, i) => [`tpl-${i}`, { name: tpl.name, icon: Layout, component: () => <TemplateCell index={i} /> }]),
templates.map((tpl, i) => [
`tpl-${i}`,
{ name: tpl.name, icon: Layout, component: () => <TemplateCell index={i} /> },
]),
);
const TemplatePanel = () => (
@@ -284,7 +286,7 @@ const CreatePanel = () => {
const [templateIdx, setTemplateIdx] = useGlobal<number>(NEW_DASH_TEMPLATE_KEY, 0);
const [editingId, setEditingId] = useGlobal<string | null>(EDITING_DASHBOARD_KEY, null);
const [, setCreating] = useGlobal<boolean>(CREATING_DASHBOARD_KEY, false);
const [, setSelected] = useGlobal<string | null>(SELECTED_DASHBOARD_KEY, null);
const [, setSearchParams] = useSearchParams();
const isEditing = !!editingId;
@@ -302,9 +304,7 @@ const CreatePanel = () => {
setDashboards((prev) =>
prev.map((ws) =>
ws.id === editingId
? { ...ws, id: newId, name: trimmed, description: desc || undefined, templateIdx }
: ws,
ws.id === editingId ? { ...ws, id: newId, name: trimmed, description: desc || undefined, templateIdx } : ws,
),
);
@@ -327,7 +327,13 @@ const CreatePanel = () => {
}
setEditingId(null);
setSelected(newId);
setSearchParams(
(p) => {
p.set('selected', newId);
return p;
},
{ replace: true },
);
} else {
const existingIds = new Set(dashboards.map((w) => w.id));
let id = slugify(trimmed) || generateSlug();
@@ -373,11 +379,7 @@ const CreatePanel = () => {
{isEditing ? 'Update Dashboard' : 'Create Dashboard'}
</Button>
{isEditing && (
<Button
onClick={() => navigate(`/dashboards/${editingId}`)}
variant="outline"
className="cursor-pointer"
>
<Button onClick={() => navigate(`/dashboards/${editingId}`)} variant="outline" className="cursor-pointer">
<ArrowRight className="h-4 w-4 mr-1" />
Open Dashboard
</Button>
@@ -487,7 +489,8 @@ const DashboardPreviewInner = ({ dashboard }: { dashboard: DashboardDefinition }
// --- Main Export ---
export const DashboardPreview = () => {
const [selectedId] = useGlobal<string | null>(SELECTED_DASHBOARD_KEY, null);
const [searchParams] = useSearchParams();
const selectedId = searchParams.get('selected');
const { value: dashboards } = useDashboardState<DashboardDefinition[]>('workspaces', []);
const dashboard = selectedId ? dashboards.find((ws) => ws.id === selectedId) : null;