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:
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user