Skip to content

feat(track-changes): add pairReplacements mode matching Word/ECMA-376 (SD-2607)#2849

Open
caio-pizzol wants to merge 2 commits intocaio/sd-2608-consolidate-track-changes-config-underfrom
caio/sd-2607-support-separate-un-paired-tracked-change-acceptreject-as-a
Open

feat(track-changes): add pairReplacements mode matching Word/ECMA-376 (SD-2607)#2849
caio-pizzol wants to merge 2 commits intocaio/sd-2608-consolidate-track-changes-config-underfrom
caio/sd-2607-support-separate-un-paired-tracked-change-acceptreject-as-a

Conversation

@caio-pizzol
Copy link
Copy Markdown
Contributor

when track changes are on and a user replaces text, SuperDoc groups the insertion and deletion under one shared id so accepting or rejecting takes one click (Google-Docs-like). Microsoft Word and ECMA-376 §17.13.5 treat every <w:ins> and <w:del> as an independent revision with its own required w:id. a consumer asked us to match the Word model.

this adds modules.trackChanges.pairReplacements (default true preserves current behavior). when false, each insertion and each deletion is an independent change, addressable and resolvable on its own.

  • importer: buildTrackedChangeIdMap accepts { pairReplacements }; when false, skips the adjacent w:del+w:ins pairing so each Word w:id maps to its own UUID
  • insertTrackedChange: no shared id on replacements in unpaired mode
  • getChangesByIdToResolve: returns just the single matching mark in unpaired mode (no neighbor walk)
  • wiring: SuperDoc.vue → editor.options.trackedChangesEditor.tsSuperConverter.trackedChangesOptionsdocxImporter

Must stay the same: paired-mode behavior is the default; existing consumers see no change.
Rejected: making unpaired mode the global default — that would turn one-click resolve into two for every existing consumer, regressing UX for the Google-Docs-like crowd.
Review: check the two new code paths in track-changes.js (pairReplacements read at insert time and at accept/reject time), and the mapper's conditional pairing.
Verified: pnpm --filter @superdoc/super-editor test → 11358 pass (13 skipped), pnpm --filter superdoc test → 877 pass (same pre-existing collab-server.test.ts module-resolution failure as main).

no exporter change needed — <w:ins>/<w:del> are already written per-mark with their own w:id in both modes. public API (trackChanges.list/get/decide) keeps the same shape — decide({ id }) just resolves one mark instead of two in unpaired mode.

closes SD-2607, unblocks IT-935. stacked on #2847 — the base will auto-retarget to main once that merges.

… (SD-2607)

when track changes are on and a user replaces text, SuperDoc groups the
insertion and deletion under one shared id so accepting or rejecting takes
one click (Google-Docs-like). Microsoft Word and ECMA-376 §17.13.5 treat
every <w:ins> and <w:del> as an independent revision with its own required
w:id. a consumer wants their UI to match Word.

this adds modules.trackChanges.pairReplacements (default true preserves
current behaviour). when false, each insertion and each deletion is an
independent change, addressable and resolvable on its own.

- importer: buildTrackedChangeIdMap accepts { pairReplacements }; when
  false, skips the adjacent w:del+w:ins pairing so each Word w:id maps to
  its own UUID
- insertTrackedChange: no shared id on replacements in unpaired mode
- getChangesByIdToResolve: returns just the single matching mark in
  unpaired mode (no neighbor walk)
- wiring: SuperDoc.vue -> editor.options.trackedChanges -> Editor.ts ->
  SuperConverter.trackedChangesOptions -> docxImporter

no exporter change needed — <w:ins>/<w:del> are already written per-mark
with their own w:id in both modes. no public API shape change.
@linear
Copy link
Copy Markdown

linear bot commented Apr 16, 2026

@github-actions
Copy link
Copy Markdown
Contributor

Status: PASS

The changed files handle w:ins and w:del correctly per the spec. Here's what I checked:

Elements and attributes (trackedChangeIdMapper.js)

All three attributes read from w:ins / w:del are valid (§17.13.5.18, §17.13.5.14):

  • w:id (ST_DecimalNumber) — required; document is non-conformant if omitted. The code's if (!wordId) return guard at line 57 handles the non-conformant case gracefully.
  • w:author (ST_String) — optional; the ?? '' fallback is correct.
  • w:date (ST_DateTime) — optional; the ?? '' fallback is correct. String equality for the pairing check is fine because both halves of a real replacement come from Word with the exact same ISO 8601 string.

PAIRING_TRANSPARENT_NAMES set

All twelve elements are real OOXML cross-structure annotation markers: bookmark start/end (Annex L §L.1.14.8), permission start/end (§L.1.14.9), w:proofErr (§17.13.8.1), comment range start/end, and move range markers — none carry document content, so treating them as transparent to pairing is correct.

pairReplacements semantics

The spec (§17.13.5) does treat each w:ins / w:del as an independent revision with its own w:id, so the pairReplacements: false path is the strictly conformant model. The default pairReplacements: true is an application-level interpretation layered on top of the import — it doesn't misread any OOXML attribute, it just assigns the same internal UUID to two separate revisions. That's a product decision, not a spec violation.

docxImporter.js — the only change is passing the options object through to buildTrackedChangeIdMap; no new XML is read.

Nothing here misuses an OOXML element or attribute.

Copy link
Copy Markdown

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

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: d2e8af3a32

ℹ️ 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 thread packages/super-editor/src/editors/v1/extensions/track-changes/track-changes.js Outdated
…607)

- always walk adjacent same-id marks in getChangesByIdToResolve so a single
  logical revision split across multiple segments resolves as one unit; the
  unpaired case is handled implicitly because ins/del now have distinct ids
- align changeId with the insertion mark's id so comment threads and the
  optional comment reply attach to the same thread in unpaired mode
- simplify id-minting: one primary id anchors the operation; the deletion
  mints its own fresh id only when unpaired AND it's a replacement. the
  Document API write adapter now gets unpaired revisions when the flag is
  off without any adapter-level change
- add trackedChanges?: {...} to EditorOptions so consumers don't need casts
- add an unpaired-mode example snippet to the docs
- extension test now covers the headline guarantee: in unpaired mode,
  acceptTrackedChangeById(insertionId) resolves only the insertion, and the
  deletion is still independently rejectable by its own id
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.

1 participant