Skip to content

Commit 5a6c10e

Browse files
tupizzsuperdoc-oss-port[bot]
authored andcommitted
fix(v1): preserve nested content-control parent topology (#1105)
* fix(v1): preserve nested content-control parent topology * test(v1): strengthen nested SDT round-trip coverage Ported-From-Source-Repo: superdoc/orbit Ported-From-Source-Commit: 5a7401384e0d2dc0a6bdfd1375bb45614b2eb91b Ported-Public-Prefix: superdoc/public
1 parent 49d8c9c commit 5a6c10e

2 files changed

Lines changed: 97 additions & 1 deletion

File tree

packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/content-controls-wrappers.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,10 @@ function getParentWrapper(editor: Editor, input: ContentControlsGetParentInput):
470470
const sdt = resolveSdtByTarget(editor.state.doc, input.target);
471471
const $pos = editor.state.doc.resolve(sdt.pos);
472472

473-
for (let depth = $pos.depth - 1; depth >= 0; depth--) {
473+
// `sdt.pos` points immediately before the target node. At that boundary,
474+
// `$pos.depth` is already the depth of the containing node, so skipping it
475+
// misses the direct parent for nested SDTs.
476+
for (let depth = $pos.depth; depth >= 0; depth--) {
474477
const ancestor = $pos.node(depth);
475478
if (isSdtNode(ancestor)) {
476479
return buildContentControlInfoFromNode({
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/* @vitest-environment jsdom */
2+
3+
import { describe, expect, it } from 'vitest';
4+
import { Editor } from '../../core/Editor.js';
5+
import { initTestEditor, loadTestDataForEditorTests } from '../../tests/helpers/helpers.js';
6+
7+
type LoadedDocData = Awaited<ReturnType<typeof loadTestDataForEditorTests>>;
8+
9+
function openEditor(docData: LoadedDocData) {
10+
return initTestEditor({
11+
content: docData.docx,
12+
media: docData.media,
13+
mediaFiles: docData.mediaFiles,
14+
fonts: docData.fonts,
15+
useImmediateSetTimeout: false,
16+
isHeadless: true,
17+
user: { name: 'Test', email: 'test@example.com' },
18+
}).editor;
19+
}
20+
21+
async function reopenEditor(editor: Editor) {
22+
const exported = await editor.exportDocx();
23+
const bytes = exported instanceof Uint8Array ? exported : new Uint8Array(exported);
24+
const [docx, media, mediaFiles, fonts] = await Editor.loadXmlData(bytes, true);
25+
return openEditor({ docx, media, mediaFiles, fonts });
26+
}
27+
28+
describe('SD-3617 nested content-control wrap persistence', () => {
29+
it('contentControls.wrap persists a real parent around an existing block SDT after save and reopen', async () => {
30+
const source = await loadTestDataForEditorTests('sdt-nested-block.docx');
31+
const editor = openEditor(source);
32+
let reopened: Editor | undefined;
33+
try {
34+
const child = editor.doc.contentControls.selectByTag({ tag: 'outer-block' }).items[0]!;
35+
const result = editor.doc.contentControls.wrap({
36+
target: child.target,
37+
kind: 'block',
38+
tag: 'sdk-parent',
39+
alias: 'SDK parent',
40+
});
41+
42+
expect(result.success).toBe(true);
43+
if (!result.success || !result.updatedRef) {
44+
throw new Error('Expected contentControls.wrap to return the persisted parent reference');
45+
}
46+
expect(editor.doc.contentControls.get({ target: result.updatedRef }).properties.tag).toBe('sdk-parent');
47+
reopened = await reopenEditor(editor);
48+
49+
const reopenedChild = reopened.doc.contentControls.selectByTag({ tag: 'outer-block' }).items[0]!;
50+
const reopenedParent = reopened.doc.contentControls.get({ target: result.updatedRef });
51+
52+
expect(reopenedChild.id).toBe(child.id);
53+
expect(reopenedParent.properties.tag).toBe('sdk-parent');
54+
expect(reopened.doc.contentControls.getParent({ target: reopenedChild.target })?.id).toBe(reopenedParent.id);
55+
expect(
56+
reopened.doc.contentControls.listChildren({ target: reopenedParent.target }).items.map(({ id }) => id),
57+
).toEqual([reopenedChild.id]);
58+
} finally {
59+
reopened?.destroy();
60+
editor.destroy();
61+
}
62+
});
63+
64+
it('contentControls.group.wrap persists a group parent around an existing block SDT after save and reopen', async () => {
65+
const source = await loadTestDataForEditorTests('sdt-nested-block.docx');
66+
const editor = openEditor(source);
67+
let reopened: Editor | undefined;
68+
try {
69+
const child = editor.doc.contentControls.selectByTag({ tag: 'outer-block' }).items[0]!;
70+
const result = editor.doc.contentControls.group.wrap({ target: child.target });
71+
72+
expect(result.success).toBe(true);
73+
if (!result.success || !result.updatedRef) {
74+
throw new Error('Expected contentControls.group.wrap to return the persisted parent reference');
75+
}
76+
expect(editor.doc.contentControls.get({ target: result.updatedRef }).controlType).toBe('group');
77+
reopened = await reopenEditor(editor);
78+
79+
const reopenedParent = reopened.doc.contentControls.get({ target: result.updatedRef });
80+
const reopenedChild = reopened.doc.contentControls.selectByTag({ tag: 'outer-block' }).items[0]!;
81+
expect(reopenedChild.id).toBe(child.id);
82+
expect(reopenedParent.controlType).toBe('group');
83+
expect(reopenedParent.id).toBe(result.updatedRef.nodeId);
84+
expect(reopened.doc.contentControls.getParent({ target: reopenedChild.target })?.id).toBe(reopenedParent.id);
85+
expect(
86+
reopened.doc.contentControls.listChildren({ target: reopenedParent.target }).items.map(({ id }) => id),
87+
).toEqual([reopenedChild.id]);
88+
} finally {
89+
reopened?.destroy();
90+
editor.destroy();
91+
}
92+
});
93+
});

0 commit comments

Comments
 (0)