Skip to content

Commit b65488d

Browse files
fix(comments): include commentsExtended.xml in export when comments are resolved (#2564)
Resolved comments were appearing as unresolved when the exported DOCX was opened in Word. Word reads the w15:done attribute from commentsExtended.xml to determine resolved status, but that file was omitted when there were no threaded comments.
1 parent 0cbcee4 commit b65488d

3 files changed

Lines changed: 114 additions & 1 deletion

File tree

packages/super-editor/src/core/super-converter/v2/exporter/commentsExporter.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ export const updateCommentsExtendedXml = (comments = [], commentsExtendedXml, th
190190
const exportStrategy = typeof threadingProfile === 'string' ? threadingProfile : 'word';
191191
const profile = typeof threadingProfile === 'string' ? null : threadingProfile;
192192
const hasThreadedComments = comments.some((comment) => comment.threadingParentCommentId || comment.parentCommentId);
193+
const hasResolvedComments = comments.some((comment) => comment.resolvedTime || comment.isDone);
193194

194195
// Always generate commentsExtended.xml when exporting comments (unless Google Docs style)
195196
// This ensures that comments without threading relationships are explicitly marked as
@@ -204,7 +205,9 @@ export const updateCommentsExtendedXml = (comments = [], commentsExtendedXml, th
204205
// If any threaded comments exist, always include commentsExtended.xml so Word can retain threads.
205206
const shouldIncludeForThreads = hasThreadedComments;
206207

207-
if (!shouldGenerateCommentsExtended && !shouldIncludeForThreads) {
208+
// Word reads w15:done from commentsExtended.xml to determine resolved status.
209+
// Without this file, resolved comments appear unresolved when opened in Word.
210+
if (!shouldGenerateCommentsExtended && !shouldIncludeForThreads && !hasResolvedComments) {
208211
return null;
209212
}
210213

packages/super-editor/src/core/super-converter/v2/exporter/commentsExporter.test.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,70 @@ describe('updateCommentsExtendedXml', () => {
513513

514514
expect(childEntry.attributes['w15:paraIdParent']).toBe('PARENT-PARA');
515515
});
516+
517+
it('SD-2306: generates commentsExtended.xml for resolved comments with range-based threading', () => {
518+
const comments = [
519+
{
520+
commentId: 'resolved-comment',
521+
commentParaId: 'RESOLVED-PARA',
522+
resolvedTime: 1711234567890,
523+
},
524+
];
525+
526+
const commentsExtendedXml = {
527+
elements: [{ elements: [] }],
528+
};
529+
530+
const profile = {
531+
defaultStyle: 'range-based',
532+
mixed: false,
533+
fileSet: {
534+
hasCommentsExtended: false,
535+
hasCommentsExtensible: false,
536+
hasCommentsIds: false,
537+
},
538+
};
539+
540+
const result = updateCommentsExtendedXml(comments, commentsExtendedXml, profile);
541+
542+
// Must not return null — Word needs this file to read w15:done
543+
expect(result).not.toBeNull();
544+
const entries = result.elements[0].elements;
545+
expect(entries).toHaveLength(1);
546+
expect(entries[0].attributes['w15:done']).toBe('1');
547+
});
548+
549+
it('SD-2306: generates commentsExtended.xml for isDone comments with range-based threading', () => {
550+
const comments = [
551+
{
552+
commentId: 'done-comment',
553+
commentParaId: 'DONE-PARA',
554+
isDone: true,
555+
resolvedTime: null,
556+
},
557+
];
558+
559+
const commentsExtendedXml = {
560+
elements: [{ elements: [] }],
561+
};
562+
563+
const profile = {
564+
defaultStyle: 'range-based',
565+
mixed: false,
566+
fileSet: {
567+
hasCommentsExtended: false,
568+
hasCommentsExtensible: false,
569+
hasCommentsIds: false,
570+
},
571+
};
572+
573+
const result = updateCommentsExtendedXml(comments, commentsExtendedXml, profile);
574+
575+
expect(result).not.toBeNull();
576+
const entries = result.elements[0].elements;
577+
expect(entries).toHaveLength(1);
578+
expect(entries[0].attributes['w15:done']).toBe('1');
579+
});
516580
});
517581

518582
// =============================================================================
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { test, expect } from '../../fixtures/superdoc.js';
2+
import { assertDocumentApiReady, addCommentByText, resolveComment, listComments } from '../../helpers/document-api.js';
3+
import JSZip from 'jszip';
4+
5+
test.use({ config: { toolbar: 'full', comments: 'on' } });
6+
7+
test('SD-2306 resolved comment is marked as done in exported DOCX', async ({ superdoc }) => {
8+
await assertDocumentApiReady(superdoc.page);
9+
10+
// 1. Type a line of text
11+
await superdoc.type('This text has a resolved comment');
12+
await superdoc.waitForStable();
13+
14+
// 2. Add a comment to "resolved comment"
15+
const commentId = await addCommentByText(superdoc.page, {
16+
pattern: 'resolved comment',
17+
text: 'This is a test comment',
18+
});
19+
await superdoc.waitForStable();
20+
21+
// 3. Resolve the comment
22+
await resolveComment(superdoc.page, { commentId });
23+
await superdoc.waitForStable();
24+
25+
// Verify the comment is resolved before exporting
26+
const comments = await listComments(superdoc.page, { includeResolved: true });
27+
expect(comments.matches.some((entry: any) => entry.status === 'resolved')).toBe(true);
28+
29+
// 4. Export to DOCX
30+
const bytes: number[] = await superdoc.page.evaluate(async () => {
31+
const blob: Blob = await (window as any).editor.exportDocx();
32+
const buffer = await blob.arrayBuffer();
33+
return Array.from(new Uint8Array(buffer));
34+
});
35+
36+
// 5. Parse the exported zip and verify the resolved status
37+
const zip = await JSZip.loadAsync(Buffer.from(bytes));
38+
39+
// commentsExtended.xml must exist — Word reads w15:done from this file
40+
// to determine whether a comment is resolved
41+
const commentsExtendedFile = zip.file('word/commentsExtended.xml');
42+
expect(commentsExtendedFile).not.toBeNull();
43+
44+
const commentsExtendedXml = await commentsExtendedFile!.async('string');
45+
expect(commentsExtendedXml).toContain('w15:done="1"');
46+
});

0 commit comments

Comments
 (0)