Skip to content

Commit 6f42a90

Browse files
committed
MDH: fix stale index/search-index bodies on collection switch + refresh
The Indexes and Search Indexes panels render each index body through a read-only JsonEditor inside IndexCard. JsonEditor created its CodeMirror view once in a mount-only effect and never synced later 'value' prop changes. Since the cards carry no key, Preact reuses the same JsonEditor instances by position across a collection switch or a Refresh click, so the bodies kept showing the previously selected collection's definitions even though names/badges updated. Add a value-sync effect gated on readOnly: read-only editors re-project their value when it changes; editable editors still treat value as a seed only (edits live in the view, read via editorRef) so user input is never clobbered. Covers both IndexPanel and SearchIndexPanel via the shared IndexCard. Add tests/mdh-json-editor.test.js exercising the real (un-mocked) JsonEditor for both the read-only sync and the editable seed-only guarantee.
1 parent a7210b1 commit 6f42a90

2 files changed

Lines changed: 80 additions & 0 deletions

File tree

src/mdh/components/JsonEditor.jsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,21 @@ export default function JsonEditor({ value = '', onChange, onValidChange, mode =
264264
};
265265
}, []);
266266

267+
// Keep read-only editors in sync with their `value` prop. The same instance is
268+
// reused (no key) when an index / search-index card body changes on a
269+
// collection switch or a Refresh click, and the mount-only effect above never
270+
// re-runs — so without this the body would stay stale. Gated on readOnly:
271+
// editable editors treat `value` as a seed only (edits live in the view, read
272+
// via editorRef), so syncing there would clobber user input on a re-render.
273+
useEffect(() => {
274+
if (!readOnly) return;
275+
const view = viewRef.current;
276+
if (!view) return;
277+
const current = view.state.doc.toString();
278+
if (value === current) return;
279+
view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: value } });
280+
}, [value, readOnly]);
281+
267282
useEffect(() => {
268283
if (editorRef) {
269284
editorRef.current = {

tests/mdh-json-editor.test.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// @vitest-environment jsdom
2+
//
3+
// JsonEditor value-prop syncing. A read-only editor is a pure projection of its
4+
// `value` prop — index / search-index card bodies are rendered read-only and the
5+
// SAME JsonEditor instance is reused (no key) when the value changes on a
6+
// collection switch or a Refresh click, so a mount-only seed leaves the body
7+
// stale. Editable editors deliberately treat `value` as a seed only: edits live
8+
// in the view and are read back via editorRef, so a parent re-render must never
9+
// clobber them.
10+
import { describe, it, expect, vi } from 'vitest';
11+
import { h, render, Fragment } from 'preact';
12+
import JsonEditor from '../src/mdh/components/JsonEditor.jsx';
13+
14+
function mount() {
15+
const root = document.createElement('div');
16+
document.body.appendChild(root);
17+
return root;
18+
}
19+
20+
describe('JsonEditor — value prop syncing', () => {
21+
it('read-only editor updates its document when the value prop changes (collection switch / refresh)', async () => {
22+
const root = mount();
23+
const ref = { current: null };
24+
25+
render(h(JsonEditor, { readOnly: true, value: '{"a":1}', editorRef: ref }), root);
26+
await vi.waitFor(() => expect(ref.current).not.toBeNull());
27+
expect(ref.current.getValue()).toBe('{"a":1}');
28+
29+
// Same instance reused with a new value — exactly what IndexCard / the index
30+
// panels do when the selected collection changes or Refresh is clicked.
31+
render(h(JsonEditor, { readOnly: true, value: '{"b":2}', editorRef: ref }), root);
32+
await vi.waitFor(() => expect(ref.current.getValue()).toBe('{"b":2}'));
33+
});
34+
35+
it('editable editor treats value as a seed only — a parent re-render does not clobber edits', async () => {
36+
const root = mount();
37+
const roRef = { current: null }; // read-only sibling: deterministic "effects flushed" signal
38+
const edRef = { current: null }; // editable editor under test
39+
40+
render(
41+
h(Fragment, null,
42+
h(JsonEditor, { readOnly: true, value: '{"seed":1}', editorRef: roRef }),
43+
h(JsonEditor, { value: '{"seed":1}', editorRef: edRef }),
44+
),
45+
root,
46+
);
47+
await vi.waitFor(() => expect(edRef.current).not.toBeNull());
48+
49+
// Simulate a user edit living inside the editable view.
50+
edRef.current.setValue('{"edited":true}');
51+
expect(edRef.current.getValue()).toBe('{"edited":true}');
52+
53+
// Re-render both with a new value prop. The read-only one syncs (positive,
54+
// deterministic signal that the effect cycle ran); the editable one must not.
55+
render(
56+
h(Fragment, null,
57+
h(JsonEditor, { readOnly: true, value: '{"next":2}', editorRef: roRef }),
58+
h(JsonEditor, { value: '{"next":2}', editorRef: edRef }),
59+
),
60+
root,
61+
);
62+
await vi.waitFor(() => expect(roRef.current.getValue()).toBe('{"next":2}'));
63+
expect(edRef.current.getValue()).toBe('{"edited":true}');
64+
});
65+
});

0 commit comments

Comments
 (0)