jobs: persist the panel layout across reloads

The page backed WorkspaceLayout with local useState, so onLayoutChange
(fired on every resize) only updated ephemeral state — pane sizes reset
to the default split on reload. Back it with useDashboardState under
screens/jobs, like the other workspace routes, with a structural guard
that falls back to the default when a persisted layout's panel ids no
longer match PANEL_COMPONENTS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 21:39:34 +00:00
co-authored by Claude Opus 4.8
parent fd203138bf
commit aa0cb733a5
@@ -15,6 +15,7 @@ import {
import { WorkspaceLayout } from 'officerdev';
import type { LayoutNode, PanelComponents } from 'officerdev';
import { useClient } from 'hooks/useClient';
import { useDashboardState } from 'state/useDashboardState';
import { Card } from '@/components/Card';
import { ScriptJobDetail } from './ScriptJobDetail';
import { DownloadJobDetail } from './DownloadJobDetail';
@@ -312,12 +313,29 @@ const PANEL_COMPONENTS: PanelComponents = {
'job-detail': JobDetailPanel,
};
// One page for /jobs and /jobs/:id — master (list) + detail, resizable like /chat.
// Guard the persisted layout against a stale shape (e.g. panel ids changed in a later build): the panels
// render from PANEL_COMPONENTS by id, so a layout whose panel ids don't match ours would render blanks.
// If it doesn't line up exactly, fall back to the default rather than trust the saved node.
const collectPanelIds = (node: LayoutNode, acc: Set<string>): Set<string> => {
if (node.type === 'panel') acc.add(node.id);
else for (const child of node.children) collectPanelIds(child.node, acc);
return acc;
};
const matchesPanelSet = (node: LayoutNode): boolean => {
const ids = collectPanelIds(node, new Set<string>());
const expected = Object.keys(PANEL_COMPONENTS);
return ids.size === expected.length && expected.every((id) => ids.has(id));
};
// One page for /jobs and /jobs/:id — master (list) + detail, resizable like /chat. The layout is
// persisted per-user via useDashboardState (screens/ namespace), so pane sizes survive reloads.
export const JobsPage = () => {
const [layout, setLayout] = useState<LayoutNode>(JOBS_LAYOUT);
const { value, setValue } = useDashboardState<LayoutNode>('screens/jobs', JOBS_LAYOUT);
const layout = matchesPanelSet(value) ? value : JOBS_LAYOUT;
return (
<div className="h-full w-full">
<WorkspaceLayout layout={layout} onLayoutChange={setLayout} components={PANEL_COMPONENTS} noHeader />
<WorkspaceLayout layout={layout} onLayoutChange={setValue} components={PANEL_COMPONENTS} noHeader />
</div>
);
};