Skip to content

fix: filter synthetic tracked-change comments on export#3744

Open
caio-pizzol wants to merge 2 commits into
mainfrom
caio/fix-3741-synthetic-comments
Open

fix: filter synthetic tracked-change comments on export#3744
caio-pizzol wants to merge 2 commits into
mainfrom
caio/fix-3741-synthetic-comments

Conversation

@caio-pizzol

Copy link
Copy Markdown
Contributor

Summary

Fixes DOCX export for comments that overlap tracked changes by separating real authored comments from automatic tracked-change sidebar rows.

Based on #3741. The original PR commit is retained in this branch history.

Why

The first approach had the right goal, but it checked the export record shape after comments were already normalized. That could still write automatic tracked-change rows into Word comment files.

What changed

  • Mark automatic tracked-change projection rows when they are created.
  • Preserve that marker through comment values and sync.
  • Filter marked rows before writing DOCX comment parts.
  • Keep replies attached to real comment parents, including comments anchored to tracked text.
  • Harden comment XML output so empty real comments still get normalized safely.

Validation

  • node --check packages/super-editor/src/editors/v1/core/super-converter/v2/exporter/commentsExporter.js
  • node --check packages/super-editor/src/editors/v1/core/super-converter/v2/exporter/commentsExporter.test.js
  • node --check packages/superdoc/src/stores/comments-store.js
  • node --check packages/superdoc/src/stores/comments-store.test.js
  • node --check packages/superdoc/src/components/CommentsLayer/use-comment.js

Unit tests were not run locally per project guidance.

dheerajiiitv and others added 2 commits June 16, 2026 12:49
…on export

exportToDocx excluded every comment flagged trackedChange:true from comments.xml and dropped its commentRange/commentReference anchors. That flag is set both on SuperDoc's synthetic tracked-change projection rows and on genuine user comments anchored to (or overlapping) a tracked change, so real comments placed on a redline were silently lost on export while plain comments survived.

Narrow the filter to exclude only synthetic projection rows (trackedChange + no authored body + no parentCommentId) via a new isSyntheticTrackedChangeComment, mirroring isSyntheticTrackedChangeProjection. Genuine comments stay in the export list and the body translator re-emits their anchors. Adds unit coverage in commentsExporter.test.js.

Co-authored-by: Cursor <cursoragent@cursor.com>
@caio-pizzol

Copy link
Copy Markdown
Contributor Author

@dheerajiiitv FYI

@caio-pizzol caio-pizzol self-assigned this Jun 16, 2026
@caio-pizzol caio-pizzol marked this pull request as ready for review June 16, 2026 16:52
@caio-pizzol caio-pizzol requested a review from a team as a code owner June 16, 2026 16:52
@github-actions

Copy link
Copy Markdown
Contributor

I attempted to verify against the live ECMA-376 spec tools but permission was denied in this session, so the checks below are against ECMA-376 (§17.13.4 comments) and the documented Microsoft w14/w15/w16cid/w16cex extension schemas from knowledge.

Status: PASS

This PR is almost entirely export/filtering logic (deciding which comment rows reach the comment parts and threading them), not new element/attribute emission. Every OOXML construct it touches checks out:

  • w:comment (commentsExporter.js:224-237) — the normalized element keeps only w:id, w:author, w:date, w:initials, which are exactly the valid attributes for CT_Comment (w:initials plus the CT_TrackChange base of w:id/w:author/w:date). The change at :195-204 and the attribute rebuild actually improve compliance: the non-schema w:done and w15:paraId that getCommentDefinition stamps are now reliably stripped off the <w:comment> element, and w14:paraId is correctly placed on the w:p (where it belongs) rather than the comment. See https://ooxml.dev/spec?q=comment

  • w15:commentEx (:335-339) — uses w15:paraId, w15:done, and w15:paraIdParent, all valid on CT_CommentEx. Dropping the !parentComment.trackedChange gate is purely a SuperDoc data-model decision (which rows are real parents); it doesn't emit anything off-schema. https://ooxml.dev/spec?q=commentEx

  • w16cid:commentId / w16cex:commentExtensible (unchanged, :368-385) — w16cid:paraId/w16cid:durableId and w16cex:durableId/w16cex:dateUtc match their extension types.

  • Empty-paragraph injection (:202) — giving a body-less comment one w:p keeps the comment's content model (block-level content) well-formed rather than emitting a contentless <w:comment>. This is the spec-safer choice.

One pre-existing, non-blocking note (not introduced by this PR): the custom:* attributes bound via xmlns:custom to the WML main namespace (:236) aren't ECMA-376 attributes on CT_Comment — they're SuperDoc round-trip metadata sharing the w: namespace URI under a different prefix. Word tolerates it and the PR doesn't change this behavior, so it's out of scope here, but worth flagging if a future pass tightens the comment part to schema-only attributes.

No non-existent elements/attributes, no missing required attributes, no wrong defaults, no spec violations in the changed code.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 49867e7f31

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +67 to +72
collectCommentTextFragments(record.commentText, fragments);
collectCommentTextFragments(record.commentJSON, fragments);
collectCommentTextFragments(record.elements, fragments);
collectCommentTextFragments(record.text, fragments);

return fragments.join('').trim().length > 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ignore empty rich-text shells when filtering synthetic comments

For pre-marker synthetic tracked-change rows (for example, existing collaboration state created before isSyntheticTrackedChangeProjection was added), commentText can be the editor's empty rich-text value '<p></p>'—the UI explicitly treats that as no content in CommentDialog.vue. Counting that raw HTML string as meaningful here makes isSyntheticTrackedChangeComment keep the row, so DOCX export can still emit the automatic tracked-change sidebar entry into comments.xml even though it has no authored body. Consider stripping/parsing HTML empties (at least '<p></p>') before treating commentText/text as authored content.

Useful? React with 👍 / 👎.

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@dheerajiiitv

Copy link
Copy Markdown

Any updated?

@dheerajiiitv

Copy link
Copy Markdown

@caio-pizzol When are we planning to merge it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants