Skip to content

Commit 2aa3bdc

Browse files
fix(schema): clear the selection ring on every pane-close path (review #65)
Address the medium /code-review findings on this PR: - Esc-closing the detail pane (the schema tab + the in-app overlay handlers in explain-graph.js) did `pane.remove()` directly, bypassing the ✕ button's cleanup — leaving the accent ring + .eg-card--selected class orphaned on the card with no pane open. Export `clearSchemaSelection(doc)` and call it from both Esc paths so every close route clears the selection. (the real bug) - Simplify clearSchemaSelection to a single scan: the ring is always a child of the selected card, so strip the class and remove its ring together. - Fix two now-stale comments (schemaDetailClick + openDetailPane) that still referenced the removed "Insert SHOW CREATE" button / old close behavior. - Drop the dead `insertCreate: vi.fn()` stubs from two tests (and the now-unused `vi` import) — the detail pane no longer calls insertCreate. Extends the overlay Esc test to assert the ring is cleared. 1023 tests pass; schema-detail.js stays at 100% coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019kE9qbgBNBrfNgwg9fRsMJ
1 parent 38537ae commit 2aa3bdc

4 files changed

Lines changed: 37 additions & 20 deletions

File tree

src/ui/explain-graph.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { qualifyIdent } from '../core/format.js';
1414
import { fitBox, fitWidthBox, zoomBox, panBox, viewBoxStr } from '../core/panzoom.js';
1515
import { straightEdgePoints, incidentEdges, dragDeltaToSvg, applyPositions, recordPosition, createMoveHistory } from '../core/graph-layout.js';
1616
import { flashToast } from './toast.js';
17+
import { clearSchemaSelection } from './schema-detail.js';
1718

1819
const ZOOM_STEP = 1.2; // per zoom-button press
1920
const WHEEL_ZOOM_STEP = 1.04; // per ⌘/Ctrl+wheel notch — gentle, so trackpad/wheel zoom isn't jumpy
@@ -344,9 +345,9 @@ const schemaClick = (app) => (n) => {
344345
};
345346

346347
// In the full schema view, clicking an object opens the detail pane (full columns
347-
// / keys / partitions / DDL) instead of inserting SHOW CREATE — the pane carries
348-
// its own "Insert SHOW CREATE" button. External (ext:) leaves have no detail; a
349-
// ⌘/Ctrl-click is reserved for dragging the node, so it doesn't open the pane.
348+
// / keys / partitions / DDL) instead of inserting SHOW CREATE, and rings the
349+
// clicked card. External (ext:) leaves have no detail; a ⌘/Ctrl-click is reserved
350+
// for dragging the node, so it doesn't open the pane.
350351
// `targetDoc` is this view's own document (the tab or the overlay's host), threaded
351352
// so a node click always opens the pane in the view it came from — even when
352353
// several full views are open at once (no shared single-slot document).
@@ -601,7 +602,7 @@ function openInTab(app, win, childDoc, mainDoc) {
601602
childDoc.addEventListener('keydown', (e) => {
602603
if (e.key !== 'Escape') return;
603604
const pane = childDoc.querySelector('.schema-detail');
604-
if (pane) { e.stopPropagation(); pane.remove(); }
605+
if (pane) { e.stopPropagation(); pane.remove(); clearSchemaSelection(childDoc); }
605606
}, true);
606607
return makeController(app, childDoc, mainDoc, canvas, bar, null);
607608
});
@@ -617,7 +618,7 @@ function openInOverlay(app, mainDoc) {
617618
if (e.key !== 'Escape') return;
618619
e.stopPropagation();
619620
const pane = mainDoc.querySelector('.schema-detail');
620-
if (pane) pane.remove(); else close();
621+
if (pane) { pane.remove(); clearSchemaSelection(mainDoc); } else close();
621622
};
622623
let backdrop;
623624
// close() also tears down the interaction listeners attached to the main

src/ui/schema-detail.js

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ const TOP_MARGIN = 100;
1616
/**
1717
* Mount (or replace) the detail pane for `node` inside the live fullscreen overlay,
1818
* populated from `detail` ({ columns, partitions, ddl }). Returns the pane element,
19-
* or null when no overlay is open. The ✕ button closes just the pane; Esc closes
20-
* the whole overlay (which removes the pane with it).
19+
* or null when no overlay is open. The ✕ button and Esc both close just the pane
20+
* and clear the card's selection ring (Esc is wired in explain-graph.js via the
21+
* exported clearSchemaSelection); a further Esc / backdrop click closes the view.
2122
*/
2223
export function openDetailPane(app, node, detail, targetDoc) {
2324
// `targetDoc` is the view's own document (a schema tab, or the overlay's host);
@@ -41,18 +42,24 @@ function findCard(doc, nodeId) {
4142
return [...doc.querySelectorAll('.eg-card[data-node-id]')].find((g) => g.getAttribute('data-node-id') === nodeId) || null;
4243
}
4344

44-
// Clear any prior selection: drop the marker class and remove the ring rects.
45-
function clearSelection(doc) {
46-
doc.querySelectorAll('.eg-card--selected').forEach((g) => g.classList.remove('eg-card--selected'));
47-
doc.querySelectorAll('.eg-card-ring').forEach((ring) => ring.remove());
45+
// Clear the selection highlight in `doc`: drop the marker class and its ring rect
46+
// from the selected card (the ring is always a child of that card). Exported so the
47+
// graph's other pane-close paths — Esc in the schema tab / in-app overlay, in
48+
// explain-graph.js — clear it too, not only the pane's own ✕ button.
49+
export function clearSchemaSelection(doc) {
50+
doc.querySelectorAll('.eg-card--selected').forEach((g) => {
51+
g.classList.remove('eg-card--selected');
52+
const ring = g.querySelector('.eg-card-ring');
53+
if (ring) ring.remove();
54+
});
4855
}
4956

5057
// Mark `nodeId`'s card as selected: an accent ring drawn just outside its box (a
5158
// "double border" alongside the card's own kind-coloured stroke) plus a class the
5259
// CSS keys off. Replaces any prior selection. No-op when the card isn't drawn
5360
// (e.g. the pane opened over a view without that card, or in a test harness).
5461
function markSelected(doc, nodeId) {
55-
clearSelection(doc);
62+
clearSchemaSelection(doc);
5663
const card = findCard(doc, nodeId);
5764
if (!card) return;
5865
card.classList.add('eg-card--selected');
@@ -98,7 +105,7 @@ function buildDetailPane(node, detail, panel) {
98105
const handle = h('div', { class: 'schema-detail-handle', title: 'Drag to resize' });
99106
const pane = h('div', { class: 'schema-detail' },
100107
handle,
101-
h('button', { class: 'schema-detail-close', title: 'Close', onclick: () => { pane.remove(); clearSelection(doc); } }, Icon.close()),
108+
h('button', { class: 'schema-detail-close', title: 'Close', onclick: () => { pane.remove(); clearSchemaSelection(doc); } }, Icon.close()),
102109
h('div', { class: 'schema-detail-body' },
103110
h('div', { class: 'schema-detail-head' },
104111
h('b', null, ident), h('span', { class: 'schema-detail-kind' }, node.kind || 'table')),

tests/unit/explain-graph.test.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -287,14 +287,23 @@ describe('schema lineage graph', () => {
287287
expect(document.body.contains(overlay)).toBe(false);
288288
});
289289

290-
it('Esc closes the open detail pane first, then the overlay', () => {
290+
it('Esc closes the open detail pane first (clearing its card ring), then the overlay', () => {
291291
openSchemaView(overlayApp({ openNodeDetail: vi.fn() })).render(GRAPH);
292292
const overlay = overlayOf();
293+
const panel = overlay.querySelector('.graph-overlay-panel');
293294
const pane = document.createElement('div'); pane.className = 'schema-detail';
294-
overlay.querySelector('.graph-overlay-panel').appendChild(pane);
295+
panel.appendChild(pane);
296+
// a selected card with a ring, as markSelected would have left it on the canvas
297+
const card = document.createElementNS('http://www.w3.org/2000/svg', 'g');
298+
card.setAttribute('class', 'eg-card eg-card--selected');
299+
card.setAttribute('data-node-id', 'x.y');
300+
card.appendChild(document.createElementNS('http://www.w3.org/2000/svg', 'rect')).setAttribute('class', 'eg-card-ring');
301+
panel.appendChild(card);
295302
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
296-
expect(overlay.querySelector('.schema-detail')).toBeNull(); // pane closed
297-
expect(document.body.contains(overlay)).toBe(true); // overlay stays
303+
expect(overlay.querySelector('.schema-detail')).toBeNull(); // pane closed
304+
expect(overlay.querySelector('.eg-card--selected')).toBeNull(); // selection class cleared
305+
expect(overlay.querySelector('.eg-card-ring')).toBeNull(); // ring removed
306+
expect(document.body.contains(overlay)).toBe(true); // overlay stays
298307
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
299308
expect(document.body.contains(overlay)).toBe(false); // second Esc closes overlay
300309
});

tests/unit/schema-detail.test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, expect, vi, afterEach } from 'vitest';
1+
import { describe, it, expect, afterEach } from 'vitest';
22
import { openDetailPane } from '../../src/ui/schema-detail.js';
33

44
afterEach(() => { document.body.innerHTML = ''; });
@@ -157,15 +157,15 @@ describe('openDetailPane', () => {
157157
const panel = childDoc.createElement('div');
158158
panel.className = 'graph-overlay-panel';
159159
childDoc.body.appendChild(panel);
160-
const pane = openDetailPane({ document, actions: { insertCreate: vi.fn() } }, NODE, DETAIL, childDoc);
160+
const pane = openDetailPane({ document, actions: {} }, NODE, DETAIL, childDoc);
161161
expect(pane.ownerDocument).toBe(childDoc); // built in the child tab's document
162162
expect(childDoc.querySelector('.schema-detail')).not.toBeNull();
163163
expect(document.querySelector('.schema-detail')).toBeNull(); // not in the main document
164164
});
165165

166166
it('falls back to the global document and tolerates missing columns/partitions', () => {
167167
mountPanel();
168-
const pane = openDetailPane({ actions: { insertCreate: vi.fn() } }, NODE, { ddl: '' }); // no document/detailDocument
168+
const pane = openDetailPane({ actions: {} }, NODE, { ddl: '' }); // no document/detailDocument
169169
expect(pane).not.toBeNull();
170170
expect([...pane.querySelectorAll('h4')].map((e) => e.textContent)).toEqual(['Columns (0)']);
171171
});

0 commit comments

Comments
 (0)