diff --git a/apps/docs/document-api/reference/_generated-manifest.json b/apps/docs/document-api/reference/_generated-manifest.json
index 1b8bb06f03..00e41d3267 100644
--- a/apps/docs/document-api/reference/_generated-manifest.json
+++ b/apps/docs/document-api/reference/_generated-manifest.json
@@ -1090,5 +1090,5 @@
}
],
"marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}",
- "sourceHash": "b7a0382edb33e88b28f1af627a89d6a0b441890883e30993390a76c756214e4c"
+ "sourceHash": "b03f74e26212692ef1ae1b4fd5a6f497588c8524a993267c43ea79338cf5733b"
}
diff --git a/apps/docs/document-api/reference/capabilities/get.mdx b/apps/docs/document-api/reference/capabilities/get.mdx
index e0da7062f3..1e8a021e65 100644
--- a/apps/docs/document-api/reference/capabilities/get.mdx
+++ b/apps/docs/document-api/reference/capabilities/get.mdx
@@ -4394,12 +4394,12 @@ _No fields._
"tables.deleteColumn": {
"available": true,
"dryRun": true,
- "tracked": true
+ "tracked": false
},
"tables.deleteRow": {
"available": true,
"dryRun": true,
- "tracked": true
+ "tracked": false
},
"tables.distributeColumns": {
"available": true,
@@ -4439,7 +4439,7 @@ _No fields._
"tables.insertColumn": {
"available": true,
"dryRun": true,
- "tracked": true
+ "tracked": false
},
"tables.insertRow": {
"available": true,
diff --git a/apps/docs/document-api/reference/tables/delete-column.mdx b/apps/docs/document-api/reference/tables/delete-column.mdx
index 4dd01bf0fd..5da1e2133b 100644
--- a/apps/docs/document-api/reference/tables/delete-column.mdx
+++ b/apps/docs/document-api/reference/tables/delete-column.mdx
@@ -14,7 +14,7 @@ Delete a column from the target table.
- API member path: `editor.doc.tables.deleteColumn(...)`
- Mutates document: `yes`
- Idempotency: `conditional`
-- Supports tracked mode: `yes`
+- Supports tracked mode: `no`
- Supports dry run: `yes`
- Deterministic target resolution: `yes`
diff --git a/apps/docs/document-api/reference/tables/delete-row.mdx b/apps/docs/document-api/reference/tables/delete-row.mdx
index 68726016cd..abf2cbb202 100644
--- a/apps/docs/document-api/reference/tables/delete-row.mdx
+++ b/apps/docs/document-api/reference/tables/delete-row.mdx
@@ -14,7 +14,7 @@ Delete a row from the target table.
- API member path: `editor.doc.tables.deleteRow(...)`
- Mutates document: `yes`
- Idempotency: `conditional`
-- Supports tracked mode: `yes`
+- Supports tracked mode: `no`
- Supports dry run: `yes`
- Deterministic target resolution: `yes`
diff --git a/apps/docs/document-api/reference/tables/index.mdx b/apps/docs/document-api/reference/tables/index.mdx
index d0d249ae23..f657149a12 100644
--- a/apps/docs/document-api/reference/tables/index.mdx
+++ b/apps/docs/document-api/reference/tables/index.mdx
@@ -24,12 +24,12 @@ For non-destructive table-targeted mutations, reuse `result.table.nodeId` from t
| tables.convertToText | `tables.convertToText` | Yes | `conditional` | No | Yes |
| tables.setLayout | `tables.setLayout` | Yes | `idempotent` | No | Yes |
| tables.insertRow | `tables.insertRow` | Yes | `non-idempotent` | Yes | Yes |
-| tables.deleteRow | `tables.deleteRow` | Yes | `conditional` | Yes | Yes |
+| tables.deleteRow | `tables.deleteRow` | Yes | `conditional` | No | Yes |
| tables.setRowHeight | `tables.setRowHeight` | Yes | `idempotent` | No | Yes |
| tables.distributeRows | `tables.distributeRows` | Yes | `conditional` | No | Yes |
| tables.setRowOptions | `tables.setRowOptions` | Yes | `idempotent` | No | Yes |
-| tables.insertColumn | `tables.insertColumn` | Yes | `non-idempotent` | Yes | Yes |
-| tables.deleteColumn | `tables.deleteColumn` | Yes | `conditional` | Yes | Yes |
+| tables.insertColumn | `tables.insertColumn` | Yes | `non-idempotent` | No | Yes |
+| tables.deleteColumn | `tables.deleteColumn` | Yes | `conditional` | No | Yes |
| tables.setColumnWidth | `tables.setColumnWidth` | Yes | `idempotent` | No | Yes |
| tables.distributeColumns | `tables.distributeColumns` | Yes | `conditional` | No | Yes |
| tables.insertCell | `tables.insertCell` | Yes | `non-idempotent` | No | Yes |
diff --git a/apps/docs/document-api/reference/tables/insert-column.mdx b/apps/docs/document-api/reference/tables/insert-column.mdx
index eb3a07ed1e..743ed88477 100644
--- a/apps/docs/document-api/reference/tables/insert-column.mdx
+++ b/apps/docs/document-api/reference/tables/insert-column.mdx
@@ -14,7 +14,7 @@ Insert a new column into the target table. The new column is cloned from an adja
- API member path: `editor.doc.tables.insertColumn(...)`
- Mutates document: `yes`
- Idempotency: `non-idempotent`
-- Supports tracked mode: `yes`
+- Supports tracked mode: `no`
- Supports dry run: `yes`
- Deterministic target resolution: `yes`
diff --git a/packages/document-api/src/contract/operation-definitions.ts b/packages/document-api/src/contract/operation-definitions.ts
index d5f5be9de2..0ef95bfd60 100644
--- a/packages/document-api/src/contract/operation-definitions.ts
+++ b/packages/document-api/src/contract/operation-definitions.ts
@@ -2852,7 +2852,13 @@ export const OPERATION_DEFINITIONS = {
metadata: mutationOperation({
idempotency: 'conditional',
supportsDryRun: true,
- supportsTrackedMode: true,
+ // Row-level structural deletion cannot be represented as a decidable
+ // tracked change in the current review model (only whole-table
+ // insert/delete is decidable; see structuralRowChanges.js). Reported as
+ // unsupported so a tracked call is rejected loudly rather than silently
+ // applied as a direct (untracked) deletion, which would be data loss in
+ // suggesting mode.
+ supportsTrackedMode: false,
possibleFailureCodes: ['INVALID_TARGET', 'NO_OP'],
throws: [...T_NOT_FOUND_COMMAND, 'INVALID_TARGET'],
}),
@@ -2922,7 +2928,11 @@ export const OPERATION_DEFINITIONS = {
metadata: mutationOperation({
idempotency: 'non-idempotent',
supportsDryRun: true,
- supportsTrackedMode: true,
+ // Column structure changes have no structural tracked-change
+ // representation (OOXML tracks rows, not columns, via / in
+ // ). Reported as unsupported so a tracked call is rejected loudly
+ // rather than silently applied directly.
+ supportsTrackedMode: false,
possibleFailureCodes: ['INVALID_TARGET'],
throws: [...T_NOT_FOUND_COMMAND, 'INVALID_TARGET'],
}),
@@ -2939,7 +2949,11 @@ export const OPERATION_DEFINITIONS = {
metadata: mutationOperation({
idempotency: 'conditional',
supportsDryRun: true,
- supportsTrackedMode: true,
+ // Column structure changes have no structural tracked-change
+ // representation (OOXML tracks rows, not columns). Reported as
+ // unsupported so a tracked call is rejected loudly rather than silently
+ // applied as a direct (untracked) deletion.
+ supportsTrackedMode: false,
possibleFailureCodes: ['INVALID_TARGET', 'NO_OP'],
throws: [...T_NOT_FOUND_COMMAND, 'INVALID_TARGET'],
}),
diff --git a/packages/super-editor/src/editors/v1/document-api-adapters/__conformance__/contract-conformance.test.ts b/packages/super-editor/src/editors/v1/document-api-adapters/__conformance__/contract-conformance.test.ts
index c7f22ff75f..3350a87092 100644
--- a/packages/super-editor/src/editors/v1/document-api-adapters/__conformance__/contract-conformance.test.ts
+++ b/packages/super-editor/src/editors/v1/document-api-adapters/__conformance__/contract-conformance.test.ts
@@ -12214,6 +12214,12 @@ describe('document-api adapter conformance', () => {
'tables.setRowOptions',
'tables.setColumnWidth',
'tables.distributeColumns',
+ // Row/column structural ops cannot produce a decidable tracked change in
+ // the review model (only whole-table insert/delete is decidable), so they
+ // report supportsTrackedMode: false and reject changeMode: 'tracked'.
+ 'tables.deleteRow',
+ 'tables.insertColumn',
+ 'tables.deleteColumn',
'tables.convertFromText',
'tables.split',
'tables.convertToText',
@@ -12254,10 +12260,9 @@ describe('document-api adapter conformance', () => {
const trackedTableOps: OperationId[] = [
'create.table',
'tables.delete',
+ // tables.insertRow stamps a structural rowInsert revision that exports as
+ // ; the row/column structural ops do not support tracked mode.
'tables.insertRow',
- 'tables.deleteRow',
- 'tables.insertColumn',
- 'tables.deleteColumn',
] as OperationId[];
for (const opId of trackedTableOps) {
@@ -12283,28 +12288,45 @@ describe('document-api adapter conformance', () => {
{ changeMode: 'tracked' },
);
expect(insertRowResult.success).toBe(true);
+ });
- // tables.deleteRow with tracked mode
- const deleteRowResult = tablesDeleteRowWrapper(editor, { nodeId: 'table-1', rowIndex: 0 } as any, {
- changeMode: 'tracked',
- });
- expect(deleteRowResult.success).toBe(true);
+ it('rejects changeMode=tracked with CAPABILITY_UNAVAILABLE for row/column structural ops', () => {
+ // deleteRow/insertColumn/deleteColumn cannot produce a decidable tracked
+ // change in the review model, so they must fail loudly rather than silently
+ // applying directly (which for the deletes is data loss in suggesting mode).
+ const editor = makeTableEditor({ insertTrackedChange: vi.fn(() => true) });
+ (editor as any).options = { user: { name: 'Agent', email: 'agent@test.com' } };
+ initRevision(editor);
- // tables.insertColumn with tracked mode
- const insertColResult = tablesInsertColumnWrapper(
- editor,
- { nodeId: 'table-1', columnIndex: 0, position: 'right' },
- { changeMode: 'tracked' },
- );
- expect(insertColResult.success).toBe(true);
+ const expectCapabilityUnavailable = (fn: () => unknown, opId: string) => {
+ let capturedCode: string | null = null;
+ try {
+ fn();
+ } catch (error) {
+ capturedCode = (error as { code?: string }).code ?? null;
+ }
+ expect(capturedCode, `${opId} should throw CAPABILITY_UNAVAILABLE in tracked mode`).toBe(
+ 'CAPABILITY_UNAVAILABLE',
+ );
+ };
- // tables.deleteColumn with tracked mode
- const deleteColResult = tablesDeleteColumnWrapper(
- editor,
- { nodeId: 'table-1', columnIndex: 0 },
- { changeMode: 'tracked' },
+ expectCapabilityUnavailable(
+ () => tablesDeleteRowWrapper(editor, { nodeId: 'table-1', rowIndex: 0 } as any, { changeMode: 'tracked' }),
+ 'tables.deleteRow',
+ );
+ expectCapabilityUnavailable(
+ () =>
+ tablesInsertColumnWrapper(
+ editor,
+ { nodeId: 'table-1', columnIndex: 0, position: 'right' },
+ { changeMode: 'tracked' },
+ ),
+ 'tables.insertColumn',
+ );
+ expectCapabilityUnavailable(
+ () => tablesDeleteColumnWrapper(editor, { nodeId: 'table-1', columnIndex: 0 }, { changeMode: 'tracked' }),
+ 'tables.deleteColumn',
);
- expect(deleteColResult.success).toBe(true);
});
// ---------------------------------------------------------------------------
diff --git a/packages/super-editor/src/editors/v1/document-api-adapters/tables-adapter.convenience.test.ts b/packages/super-editor/src/editors/v1/document-api-adapters/tables-adapter.convenience.test.ts
index 5406a5b224..1f2c415e5a 100644
--- a/packages/super-editor/src/editors/v1/document-api-adapters/tables-adapter.convenience.test.ts
+++ b/packages/super-editor/src/editors/v1/document-api-adapters/tables-adapter.convenience.test.ts
@@ -2,6 +2,7 @@
import { afterEach, beforeAll, describe, expect, it } from 'vitest';
import { initTestEditor, loadTestDataForEditorTests } from '@tests/helpers/helpers.js';
+import { translator as trTranslator } from '../core/super-converter/v3/handlers/w/tr/tr-translator.js';
import type { Editor } from '../core/Editor.js';
import {
createTableAdapter,
@@ -14,7 +15,9 @@ import {
tablesApplyBorderPresetAdapter,
tablesSetShadingAdapter,
tablesInsertRowAdapter,
+ tablesDeleteRowAdapter,
tablesInsertColumnAdapter,
+ tablesDeleteColumnAdapter,
tablesGetCellsAdapter,
tablesSetCellTextAdapter,
tablesApplyPresetAdapter,
@@ -38,13 +41,14 @@ describe('SD-2129: table convenience operations', () => {
editor = undefined;
});
- function createEditor(): Editor {
+ function createEditor(opts?: { user?: { name: string; email: string } }): Editor {
const result = initTestEditor({
content: docData.docx,
media: docData.media,
mediaFiles: docData.mediaFiles,
fonts: docData.fonts,
useImmediateSetTimeout: false,
+ ...(opts?.user ? { user: opts.user, trackedChanges: {} } : {}),
});
editor = result.editor;
return editor;
@@ -676,6 +680,162 @@ describe('SD-2129: table convenience operations', () => {
});
});
+ // ---------------------------------------------------------------------------
+ // Tracked-mode operations
+ // ---------------------------------------------------------------------------
+
+ describe('tracked mode', () => {
+ const TRACKED = { changeMode: 'tracked' as const };
+ const TEST_USER = { name: 'Tester', email: 'tester@example.com' };
+
+ /** Collect the trackChange attrs of all rowInsert-stamped rows in the doc. */
+ const collectStampedRows = (ed: Editor) => {
+ const out: Array<{ node: any; tc: any }> = [];
+ ed.state.doc.descendants((node: any) => {
+ if (node.type.name !== 'tableRow') return;
+ const tc = node.attrs?.trackChange;
+ if (tc && tc.type === 'rowInsert') out.push({ node, tc });
+ });
+ return out;
+ };
+
+ const expectCapabilityUnavailable = (fn: () => unknown, opId: string) => {
+ let code: string | null = null;
+ try {
+ fn();
+ } catch (error) {
+ code = (error as { code?: string }).code ?? null;
+ }
+ expect(code, `${opId} should throw CAPABILITY_UNAVAILABLE in tracked mode`).toBe('CAPABILITY_UNAVAILABLE');
+ };
+
+ it('tables.insertRow with tracked mode stamps rowInsert revision on the new row', () => {
+ const ed = createEditor({ user: TEST_USER });
+ // Enable tracked changes so the dispatch pipeline routes through
+ // trackedTransaction; the adapter stamps the row revision directly.
+ ed.commands.enableTrackChanges();
+
+ const tableId = createTableAndGetId(ed); // 3 rows × 3 columns
+
+ // Insert a row in tracked mode — below row 0.
+ const result = tablesInsertRowAdapter(ed, { nodeId: tableId, rowIndex: 0, position: 'below' }, TRACKED);
+ expect(result.success).toBe(true);
+
+ // Exactly one row carries a rowInsert revision — the newly inserted row
+ // at index 1. Its shape mirrors the OOXML importer (row-track-change.js).
+ const stamped = collectStampedRows(ed);
+ expect(stamped.length).toBe(1);
+ const { tc } = stamped[0]!;
+ expect(tc.author).toBe(TEST_USER.name);
+ expect(tc.authorEmail).toBe(TEST_USER.email);
+ expect(typeof tc.id).toBe('string');
+ expect(typeof tc.revisionGroupId).toBe('string');
+ expect(typeof tc.date).toBe('string');
+
+ // The table should now have 4 rows.
+ const cells = tablesGetCellsAdapter(ed, { nodeId: requireTableNodeId(result, 'insertRow') });
+ const rowCount = new Set(cells.cells.map((c) => c.rowIndex)).size;
+ expect(rowCount).toBe(4);
+ });
+
+ it('tables.insertRow tracked mode exports the new row as inside ', () => {
+ // This is the user-visible payoff of tracked mode: the inserted row
+ // round-trips to OOXML as a tracked insertion that Word can accept/reject.
+ const ed = createEditor({ user: TEST_USER });
+ ed.commands.enableTrackChanges();
+ const tableId = createTableAndGetId(ed);
+
+ tablesInsertRowAdapter(ed, { nodeId: tableId, rowIndex: 0, position: 'below' }, TRACKED);
+
+ const stamped = collectStampedRows(ed);
+ expect(stamped.length).toBe(1);
+
+ const decoded = (trTranslator as any).decode({ node: stamped[0]!.node.toJSON() }, {});
+ const trPr = decoded.elements?.find((el: any) => el.name === 'w:trPr');
+ expect(trPr).toBeDefined();
+ const ins = trPr.elements?.find((el: any) => el.name === 'w:ins');
+ expect(ins).toBeDefined();
+ expect(ins.attributes['w:author']).toBe(TEST_USER.name);
+ });
+
+ it('tables.insertRow with tracked mode shares one revisionGroupId across a multi-row insert', () => {
+ const ed = createEditor({ user: TEST_USER });
+ ed.commands.enableTrackChanges();
+
+ const tableId = createTableAndGetId(ed); // 3 rows
+
+ const result = tablesInsertRowAdapter(ed, { nodeId: tableId, count: 2, rowIndex: 0, position: 'below' }, TRACKED);
+ expect(result.success).toBe(true);
+
+ const stamped = collectStampedRows(ed);
+ expect(stamped.length).toBe(2);
+ // Word assigns a distinct w:id per row, but rows inserted in one call form
+ // one logical change and therefore share a single revisionGroupId.
+ expect(new Set(stamped.map((r) => r.tc.id)).size).toBe(2);
+ expect(new Set(stamped.map((r) => r.tc.revisionGroupId)).size).toBe(1);
+
+ // Table should now have 5 rows.
+ const cells = tablesGetCellsAdapter(ed, { nodeId: requireTableNodeId(result, 'insertRow') });
+ const rowCount = new Set(cells.cells.map((c) => c.rowIndex)).size;
+ expect(rowCount).toBe(5);
+ });
+
+ it('tables.insertRow with direct mode does NOT stamp trackChange', () => {
+ const ed = createEditor({ user: TEST_USER });
+ ed.commands.enableTrackChanges(); // tracking is ON, but changeMode is direct
+
+ const tableId = createTableAndGetId(ed); // 3 rows
+
+ const result = tablesInsertRowAdapter(ed, { nodeId: tableId, rowIndex: 0, position: 'below' }, DIRECT);
+ expect(result.success).toBe(true);
+
+ // No row should have trackChange — direct mode bypasses stamping.
+ expect(collectStampedRows(ed).length).toBe(0);
+ });
+
+ it('tables.insertRow append-at-end shorthand in tracked mode stamps rowInsert', () => {
+ const ed = createEditor({ user: TEST_USER });
+ ed.commands.enableTrackChanges();
+
+ const tableId = createTableAndGetId(ed); // 3 rows
+
+ // Append-at-end shorthand: table-level target, no rowIndex/position.
+ const result = tablesInsertRowAdapter(ed, { nodeId: tableId }, TRACKED);
+ expect(result.success).toBe(true);
+
+ expect(collectStampedRows(ed).length).toBe(1);
+
+ const cells = tablesGetCellsAdapter(ed, { nodeId: requireTableNodeId(result, 'insertRow') });
+ const rowCount = new Set(cells.cells.map((c) => c.rowIndex)).size;
+ expect(rowCount).toBe(4);
+ });
+
+ it('row/column structural ops reject tracked mode with CAPABILITY_UNAVAILABLE', () => {
+ // These ops cannot produce a decidable tracked change, so they must fail
+ // loudly rather than silently apply directly (data loss on the deletes).
+ const ed = createEditor({ user: TEST_USER });
+ ed.commands.enableTrackChanges();
+ const tableId = createTableAndGetId(ed); // 3 rows × 3 columns
+
+ expectCapabilityUnavailable(
+ () => tablesDeleteRowAdapter(ed, { nodeId: tableId, rowIndex: 1 } as any, TRACKED),
+ 'tables.deleteRow',
+ );
+ expectCapabilityUnavailable(
+ () => tablesInsertColumnAdapter(ed, { nodeId: tableId, columnIndex: 0, position: 'right' } as any, TRACKED),
+ 'tables.insertColumn',
+ );
+ expectCapabilityUnavailable(
+ () => tablesDeleteColumnAdapter(ed, { nodeId: tableId, columnIndex: 1 } as any, TRACKED),
+ 'tables.deleteColumn',
+ );
+
+ // The table is untouched — no partial mutation leaked before the throw.
+ const cells = tablesGetCellsAdapter(ed, { nodeId: tableId });
+ expect(new Set(cells.cells.map((c) => c.rowIndex)).size).toBe(3);
+ });
+ });
+
// ---------------------------------------------------------------------------
// SD-2540 round 3 — set_cell_text + apply_preset + set_style_options
// ---------------------------------------------------------------------------
diff --git a/packages/super-editor/src/editors/v1/document-api-adapters/tables-adapter.ts b/packages/super-editor/src/editors/v1/document-api-adapters/tables-adapter.ts
index 875b76c7f5..9b16775c75 100644
--- a/packages/super-editor/src/editors/v1/document-api-adapters/tables-adapter.ts
+++ b/packages/super-editor/src/editors/v1/document-api-adapters/tables-adapter.ts
@@ -922,6 +922,75 @@ function insertRowInTable(
return true;
}
+/**
+ * Stamp a structural `rowInsert` revision on a single table row that was
+ * inserted via the document API's `tables.insertRow` adapter.
+ *
+ * The tracked-transaction pipeline (inline overlap compiler) cannot represent
+ * raw `tableRow` insert steps, so this structural revision is stamped directly
+ * on the row's `trackChange` attribute — the same shape the importer lands from
+ * `` inside `` (see `super-converter/v3/handlers/w/tr/row-track-change.js`,
+ * and the canonical whole-table authoring path in
+ * `extensions/track-changes/trackChangesHelpers/stampTableRows.js`).
+ *
+ * Scope note: the stamp makes the row export as a tracked insertion (``),
+ * which Word can accept/reject. It is NOT, however, a decidable in-app
+ * structural change: SuperDoc's review model only treats a WHOLE-table
+ * insert/delete as decidable (`structuralRowChanges.js`); a single tracked row
+ * inside an otherwise-untracked table is surfaced as `partial-rows` /
+ * `decidable: false`. All rows of one `insertRow` call share `revision` so they
+ * read as one logical change if/when row-level decidability is added.
+ *
+ * @param tr - The transaction containing the inserted row.
+ * @param tablePos - Absolute position of the table node.
+ * @param rowIndex - Index of the newly inserted row within the table.
+ * @param editor - The editor instance (used for user attribution).
+ * @param revision - Shared revision identity (groupId + ISO date) for the call.
+ */
+function stampInsertedRowTrackChange(
+ tr: Transaction,
+ tablePos: number,
+ rowIndex: number,
+ editor: Editor,
+ revision: { revisionGroupId: string; date: string },
+): void {
+ const user = editor.options?.user;
+ if (!user) return;
+
+ const tableNode = tr.doc.nodeAt(tablePos);
+ if (!tableNode || tableNode.type.name !== 'table') return;
+
+ // Walk the table children to find the absolute position of the target row.
+ let rowPos = tablePos + 1;
+ const maxRow = Math.min(rowIndex, tableNode.childCount - 1);
+ for (let r = 0; r < maxRow; r++) {
+ rowPos += tableNode.child(r).nodeSize;
+ }
+
+ const rowNode = tr.doc.nodeAt(rowPos);
+ if (!rowNode || rowNode.type.name !== 'tableRow') return;
+ // Don't clobber an existing structural revision.
+ if ((rowNode.attrs as Record)?.trackChange) return;
+
+ const trackChange = {
+ type: 'rowInsert' as const,
+ // Word assigns a distinct w:id per row; the shared revisionGroupId groups
+ // them as one logical change.
+ id: uuidv4(),
+ author: user.name || '',
+ authorId: user.id || '',
+ authorEmail: user.email || '',
+ authorImage: user.image || '',
+ date: revision.date,
+ revisionGroupId: revision.revisionGroupId,
+ };
+
+ tr.setNodeMarkup(rowPos, undefined, {
+ ...(rowNode.attrs as Record),
+ trackChange,
+ });
+}
+
function addColumnToTableForSplit(
tr: Transaction,
tablePos: number,
@@ -1368,6 +1437,10 @@ export function tablesInsertRowAdapter(
const position = normalizedAny.position ?? 'below';
const schema = editor.state.schema;
+ // One shared revision identity for all rows inserted by this call, so the
+ // structural enumerator can group them as a single logical change.
+ const revision = mode === 'tracked' ? { revisionGroupId: uuidv4(), date: new Date().toISOString() } : null;
+
for (let i = 0; i < count; i++) {
// Re-read the table from the (possibly modified) transaction
const currentTableNode = tr.doc.nodeAt(tablePos);
@@ -1386,10 +1459,18 @@ export function tablesInsertRowAdapter(
if (!didInsertRow) {
return toTableFailure('INVALID_TARGET', 'Row insertion could not be applied.');
}
+
+ // In tracked mode, stamp the inserted row with a structural revision
+ // (rowInsert) so it exports as a tracked insertion (``). We stamp
+ // directly rather than relying on the trackedTransaction pipeline because
+ // the inline overlap compiler cannot represent tableRow insert steps. See
+ // stampInsertedRowTrackChange for the in-app decidability caveat.
+ if (revision) {
+ stampInsertedRowTrackChange(tr, tablePos, insertIdx, editor, revision);
+ }
}
- if (mode === 'tracked') applyTrackedMutationMeta(tr);
- else applyDirectMutationMeta(tr);
+ applyDirectMutationMeta(tr);
editor.dispatch(tr);
clearIndexCache(editor);
return buildTableSuccess(resolvePostMutationTableAddress(editor, table.candidate.pos, table.address.nodeId, tr));
@@ -1407,10 +1488,10 @@ export function tablesDeleteRowAdapter(
input: TablesDeleteRowInput,
options?: MutationOptions,
): TableMutationResult {
- const mode = options?.changeMode ?? 'direct';
- if (mode === 'tracked') {
- ensureTrackedCapability(editor, { operation: 'tables.deleteRow' });
- }
+ // Row-level structural deletion has no decidable tracked-change
+ // representation in the review model; reject tracked mode rather than
+ // silently deleting the row untracked (data loss in suggesting mode).
+ rejectTrackedMode('tables.deleteRow', options);
const resolved = resolveRowLocator(editor, input as RowLocatorFields, 'tables.deleteRow');
const { table, rowIndex, rowNode, rowPos } = resolved;
@@ -1461,8 +1542,7 @@ export function tablesDeleteRowAdapter(
}
}
- if (mode === 'tracked') applyTrackedMutationMeta(tr);
- else applyDirectMutationMeta(tr);
+ applyDirectMutationMeta(tr);
editor.dispatch(tr);
clearIndexCache(editor);
return buildTableSuccess(resolvePostMutationTableAddress(editor, table.candidate.pos, table.address.nodeId, tr));
@@ -1647,10 +1727,10 @@ export function tablesInsertColumnAdapter(
input: TablesInsertColumnInput,
options?: MutationOptions,
): TableMutationResult {
- const mode = options?.changeMode ?? 'direct';
- if (mode === 'tracked') {
- ensureTrackedCapability(editor, { operation: 'tables.insertColumn' });
- }
+ // Column structure changes have no tracked-change representation (OOXML
+ // tracks rows, not columns); reject tracked mode rather than silently
+ // applying the column insert untracked.
+ rejectTrackedMode('tables.insertColumn', options);
// Resolve shorthand to a concrete columnIndex + left/right before
// delegating to the column locator (which requires columnIndex). Two cases:
@@ -1709,8 +1789,7 @@ export function tablesInsertColumnAdapter(
}
}
- if (mode === 'tracked') applyTrackedMutationMeta(tr);
- else applyDirectMutationMeta(tr);
+ applyDirectMutationMeta(tr);
editor.dispatch(tr);
clearIndexCache(editor);
return buildTableSuccess(resolvePostMutationTableAddress(editor, table.candidate.pos, table.address.nodeId, tr));
@@ -1728,10 +1807,10 @@ export function tablesDeleteColumnAdapter(
input: TablesDeleteColumnInput,
options?: MutationOptions,
): TableMutationResult {
- const mode = options?.changeMode ?? 'direct';
- if (mode === 'tracked') {
- ensureTrackedCapability(editor, { operation: 'tables.deleteColumn' });
- }
+ // Column structure changes have no tracked-change representation (OOXML
+ // tracks rows, not columns); reject tracked mode rather than silently
+ // deleting the column untracked (data loss in suggesting mode).
+ rejectTrackedMode('tables.deleteColumn', options);
const resolved = resolveColumnLocator(editor, input, 'tables.deleteColumn');
const { table, columnIndex, columnCount } = resolved;
@@ -1761,8 +1840,7 @@ export function tablesDeleteColumnAdapter(
}
}
- if (mode === 'tracked') applyTrackedMutationMeta(tr);
- else applyDirectMutationMeta(tr);
+ applyDirectMutationMeta(tr);
editor.dispatch(tr);
clearIndexCache(editor);
return buildTableSuccess(resolvePostMutationTableAddress(editor, table.candidate.pos, table.address.nodeId, tr));