diff --git a/src/workspaces/officerdev/src/apps/Dashboards/DashboardPreview.tsx b/src/workspaces/officerdev/src/apps/Dashboards/DashboardPreview.tsx index 0feaa44f..7e4d9d96 100644 --- a/src/workspaces/officerdev/src/apps/Dashboards/DashboardPreview.tsx +++ b/src/workspaces/officerdev/src/apps/Dashboards/DashboardPreview.tsx @@ -308,20 +308,40 @@ const CreatePanel = () => { ), ); - const wsLayout = templates[templateIdx]?.layout() ?? createDefaultLayout(); const currentState = queryClient.getQueryData>(['DASHBOARD_STATE']) ?? {}; const newLayoutKey = `ws-layout-${newId}`; + // Rebuild from the template only when the user actually picked a different one. This ran + // unconditionally, so renaming a dashboard — or fixing a typo in its description — threw away + // however its panels had been arranged and whichever apps were in them. A template is a seed + // chosen once at creation; it is not a description of the dashboard as it now stands. + const kept = currentState[`ws-layout-${editingId}`]; + const templateChanged = dashboards.find((w) => w.id === editingId)?.templateIdx !== templateIdx; + const keepable = !!kept && typeof kept === 'object' && !Array.isArray(kept); + const wsLayout = + !templateChanged && keepable ? kept : (templates[templateIdx]?.layout() ?? createDefaultLayout()); + if (idChanged) { + // A rename is the same dashboard under a new key, so everything keyed on the old id moves with it. + // Dropping the terminal maps without carrying them over abandoned every shell the dashboard held: + // the panels came back empty and the processes stayed alive with nothing pointing at them. + const carried: Record = { [newLayoutKey]: wsLayout }; + for (const prefix of ['ws-terminals', 'ws-host-terminals']) { + const value = currentState[`${prefix}-${editingId}`]; + if (value !== undefined) carried[`${prefix}-${newId}`] = value; + } const oldKeys = [`ws-layout-${editingId}`, `ws-terminals-${editingId}`, `ws-host-terminals-${editingId}`]; const cleaned = { ...currentState }; for (const k of oldKeys) delete cleaned[k]; - queryClient.setQueryData(['DASHBOARD_STATE'], { ...cleaned, [newLayoutKey]: wsLayout }); + queryClient.setQueryData(['DASHBOARD_STATE'], { ...cleaned, ...carried }); - const patch: Record = { [newLayoutKey]: wsLayout }; + // Key order is load-bearing: the server walks this object in insertion order, `ws-layout-` + // upserts the row and `ws-terminals-` only *updates* one, so the new key must be written + // before the old `ws-layout-: null` deletes the row it was copied from. + const patch: Record = { ...carried }; for (const k of oldKeys) patch[k] = null; persistDashboardState(client, queryClient, patch); - } else { + } else if (wsLayout !== kept) { queryClient.setQueryData(['DASHBOARD_STATE'], { ...currentState, [newLayoutKey]: wsLayout }); persistDashboardState(client, queryClient, { [newLayoutKey]: wsLayout }); }