From 1b6b980af0604da5fa5cdc5258d2e1b40fbe8dd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Thu, 30 Jul 2026 05:32:10 +0000 Subject: [PATCH] dashboards: drive list selection from a ?selected= url param, not a global MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit the /dashboards list row was a
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=, read by the list, the screen (mobile panel), and the preview. rows are real 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 --- .../Dashboard/Dashboards/DashboardsScreen.tsx | 17 ++++-- .../src/apps/Dashboards/DashboardListApp.tsx | 58 ++++++++++--------- .../src/apps/Dashboards/DashboardPreview.tsx | 31 +++++----- 3 files changed, 62 insertions(+), 44 deletions(-) diff --git a/src/apps/officer-web/Screens/Dashboard/Dashboards/DashboardsScreen.tsx b/src/apps/officer-web/Screens/Dashboard/Dashboards/DashboardsScreen.tsx index 8df75cdf..6f4e16e6 100644 --- a/src/apps/officer-web/Screens/Dashboard/Dashboards/DashboardsScreen.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Dashboards/DashboardsScreen.tsx @@ -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('screens/dashboards', defaultLayout); const isMobile = useIsMobile(); - const [selected, setSelected] = useGlobal(SELECTED_DASHBOARD_KEY, null); + // Selection lives in the URL (?selected=); 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 }, + ); }} />
diff --git a/src/workspaces/officerdev/src/apps/Dashboards/DashboardListApp.tsx b/src/workspaces/officerdev/src/apps/Dashboards/DashboardListApp.tsx index 7d704176..ffde057a 100644 --- a/src/workspaces/officerdev/src/apps/Dashboards/DashboardListApp.tsx +++ b/src/workspaces/officerdev/src/apps/Dashboards/DashboardListApp.tsx @@ -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('workspaces', []); - const [selected, setSelected] = useGlobal(SELECTED_DASHBOARD_KEY, null); + // Selection is URL state now (?selected=), 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(CREATING_DASHBOARD_KEY, false); const [, setEditing] = useGlobal(EDITING_DASHBOARD_KEY, null); const [, setName] = useGlobal(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>(['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 (
@@ -104,7 +103,7 @@ export const DashboardListApp = () => { - +
)}
))} {filtered.length === 0 && ( -

- {search ? 'No matches' : 'No dashboards yet'} -

+

{search ? 'No matches' : 'No dashboards yet'}

)} - { if (!open) setDeleting(null); }}> + { + if (!open) setDeleting(null); + }} + > Delete dashboard diff --git a/src/workspaces/officerdev/src/apps/Dashboards/DashboardPreview.tsx b/src/workspaces/officerdev/src/apps/Dashboards/DashboardPreview.tsx index 92ae0bed..ca004dc4 100644 --- a/src/workspaces/officerdev/src/apps/Dashboards/DashboardPreview.tsx +++ b/src/workspaces/officerdev/src/apps/Dashboards/DashboardPreview.tsx @@ -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: () => }]), + templates.map((tpl, i) => [ + `tpl-${i}`, + { name: tpl.name, icon: Layout, component: () => }, + ]), ); const TemplatePanel = () => ( @@ -284,7 +286,7 @@ const CreatePanel = () => { const [templateIdx, setTemplateIdx] = useGlobal(NEW_DASH_TEMPLATE_KEY, 0); const [editingId, setEditingId] = useGlobal(EDITING_DASHBOARD_KEY, null); const [, setCreating] = useGlobal(CREATING_DASHBOARD_KEY, false); - const [, setSelected] = useGlobal(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'} {isEditing && ( - @@ -487,7 +489,8 @@ const DashboardPreviewInner = ({ dashboard }: { dashboard: DashboardDefinition } // --- Main Export --- export const DashboardPreview = () => { - const [selectedId] = useGlobal(SELECTED_DASHBOARD_KEY, null); + const [searchParams] = useSearchParams(); + const selectedId = searchParams.get('selected'); const { value: dashboards } = useDashboardState('workspaces', []); const dashboard = selectedId ? dashboards.find((ws) => ws.id === selectedId) : null;