From 07b5ead981c96c2fc337a09de920fcffedb6b04f Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Wed, 18 Mar 2026 21:24:45 -0700 Subject: [PATCH 1/2] fix(document-api): add mutation-ready cell addresses to tables.getCells --- .../reference/_generated-manifest.json | 2 +- .../reference/tables/get-cells.mdx | 9 +++++++++ packages/document-api/src/contract/schemas.ts | 3 ++- packages/document-api/src/index.test.ts | 11 ++++++++++- .../src/types/table-operations.types.ts | 6 ++++++ .../contract-conformance.test.ts | 10 +++++++++- .../tables-adapter.ref-handoff.test.ts | 18 ++++++++++++++++++ .../document-api-adapters/tables-adapter.ts | 4 +++- .../tests/tables/all-commands.ts | 2 ++ 9 files changed, 60 insertions(+), 5 deletions(-) diff --git a/apps/docs/document-api/reference/_generated-manifest.json b/apps/docs/document-api/reference/_generated-manifest.json index 70d324cee9..709b1c4f18 100644 --- a/apps/docs/document-api/reference/_generated-manifest.json +++ b/apps/docs/document-api/reference/_generated-manifest.json @@ -976,5 +976,5 @@ } ], "marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}", - "sourceHash": "3bca09dc183fa828d25a2349a4f780e5ef7bfcad2e9ddf21e552a88c97b9fa97" + "sourceHash": "0ac9ab9c8f464a719722f89f32d725cc3c2079d126fa09d487a90eb7170d474d" } diff --git a/apps/docs/document-api/reference/tables/get-cells.mdx b/apps/docs/document-api/reference/tables/get-cells.mdx index 34d16aa208..73da376e62 100644 --- a/apps/docs/document-api/reference/tables/get-cells.mdx +++ b/apps/docs/document-api/reference/tables/get-cells.mdx @@ -81,6 +81,11 @@ Returns a TablesGetCellsOutput with cell information for the requested rows and }, "cells": [ { + "address": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "tableCell" + }, "colspan": 1, "columnIndex": 1, "nodeId": "node-def456", @@ -151,6 +156,9 @@ Returns a TablesGetCellsOutput with cell information for the requested rows and "items": { "additionalProperties": false, "properties": { + "address": { + "$ref": "#/$defs/TableCellAddress" + }, "colspan": { "minimum": 1, "type": "integer" @@ -173,6 +181,7 @@ Returns a TablesGetCellsOutput with cell information for the requested rows and }, "required": [ "nodeId", + "address", "rowIndex", "columnIndex", "colspan", diff --git a/packages/document-api/src/contract/schemas.ts b/packages/document-api/src/contract/schemas.ts index 16e1b213b6..20ab4bcab0 100644 --- a/packages/document-api/src/contract/schemas.ts +++ b/packages/document-api/src/contract/schemas.ts @@ -5212,12 +5212,13 @@ const operationSchemas: Record = { items: objectSchema( { nodeId: { type: 'string' }, + address: tableCellAddressSchema, rowIndex: { type: 'integer', minimum: 0 }, columnIndex: { type: 'integer', minimum: 0 }, colspan: { type: 'integer', minimum: 1 }, rowspan: { type: 'integer', minimum: 1 }, }, - ['nodeId', 'rowIndex', 'columnIndex', 'colspan', 'rowspan'], + ['nodeId', 'address', 'rowIndex', 'columnIndex', 'colspan', 'rowspan'], ), }, }, diff --git a/packages/document-api/src/index.test.ts b/packages/document-api/src/index.test.ts index defa4fc10e..8580e29db2 100644 --- a/packages/document-api/src/index.test.ts +++ b/packages/document-api/src/index.test.ts @@ -310,7 +310,16 @@ function makeTablesAdapter(): TablesAdapter { getCells: vi.fn(() => ({ nodeId: 't1', address: { kind: 'block' as const, nodeType: 'table' as const, nodeId: 't1' }, - cells: [{ nodeId: 'c1', rowIndex: 0, columnIndex: 0, colspan: 1, rowspan: 1 }], + cells: [ + { + nodeId: 'c1', + address: { kind: 'block' as const, nodeType: 'tableCell' as const, nodeId: 'c1' }, + rowIndex: 0, + columnIndex: 0, + colspan: 1, + rowspan: 1, + }, + ], })), getProperties: vi.fn(() => ({ nodeId: 't1', diff --git a/packages/document-api/src/types/table-operations.types.ts b/packages/document-api/src/types/table-operations.types.ts index f82e08ca75..e20eb264f7 100644 --- a/packages/document-api/src/types/table-operations.types.ts +++ b/packages/document-api/src/types/table-operations.types.ts @@ -6,6 +6,9 @@ import type { TableOrRowAddress, TableRowAddress, } from './base.js'; + +// Re-export for downstream callers that reference cell addresses via this module. +export type { TableCellAddress } from './base.js'; import type { ReceiptFailure, ReceiptInsert } from './receipt.js'; // --------------------------------------------------------------------------- @@ -471,7 +474,10 @@ export interface TablesGetCellsInput extends TableLocator { /** Per-cell info with stable ref for write handoff. */ export interface TableCellInfo { + /** Shorthand cell identifier — convenient for logging, Map keys, and display. */ nodeId: string; + /** Mutation-ready address — pass directly as `target` in follow-up cell operations. */ + address: TableCellAddress; rowIndex: number; columnIndex: number; colspan: number; diff --git a/packages/super-editor/src/document-api-adapters/__conformance__/contract-conformance.test.ts b/packages/super-editor/src/document-api-adapters/__conformance__/contract-conformance.test.ts index 930ce7ba1c..af12e583a5 100644 --- a/packages/super-editor/src/document-api-adapters/__conformance__/contract-conformance.test.ts +++ b/packages/super-editor/src/document-api-adapters/__conformance__/contract-conformance.test.ts @@ -10467,7 +10467,7 @@ describe('document-api adapter conformance', () => { ).toThrow(/rowIndex must not be provided when target is a row node/); }); - it('returns stable cell ids from tables.getCells using table-map resolved absolute positions', () => { + it('returns stable cell ids and mutation-ready addresses from tables.getCells', () => { const editor = makeTableEditor(); const result = tablesGetCellsAdapter(editor, { nodeId: 'table-1' }); @@ -10478,6 +10478,14 @@ describe('document-api adapter conformance', () => { const topLeft = result.cells.find((cell) => cell.rowIndex === 0 && cell.columnIndex === 0); expect(topLeft?.nodeId).toBe('cell-1'); + + // Each cell address mirrors nodeId and is ready for mutation handoff. + expect(topLeft?.address).toEqual({ kind: 'block', nodeType: 'tableCell', nodeId: 'cell-1' }); + + // All cells carry a well-formed address. + for (const cell of result.cells) { + expect(cell.address).toEqual({ kind: 'block', nodeType: 'tableCell', nodeId: cell.nodeId }); + } }); it('reads tables.getProperties from nested tableProperties', () => { diff --git a/packages/super-editor/src/document-api-adapters/tables-adapter.ref-handoff.test.ts b/packages/super-editor/src/document-api-adapters/tables-adapter.ref-handoff.test.ts index ef3fefb690..22e8a0fd09 100644 --- a/packages/super-editor/src/document-api-adapters/tables-adapter.ref-handoff.test.ts +++ b/packages/super-editor/src/document-api-adapters/tables-adapter.ref-handoff.test.ts @@ -138,6 +138,24 @@ describe('SD-2126: post-mutation table ref handoff', () => { expect(table?.nodeType).toBe('table'); }); + it('cell address from getCells is accepted as target in a follow-up mutation', () => { + const ed = createEditor(); + + const createResult = createTableAdapter(ed, { rows: 2, columns: 2, at: { kind: 'documentEnd' } }, DIRECT); + const tableNodeId = requireTableNodeId(createResult, 'create.table'); + const cellsResult = tablesGetCellsAdapter(ed, { nodeId: tableNodeId }); + const firstCell = cellsResult.cells[0]!; + + // Use the cell's address (not its flat nodeId) as the mutation target. + const borderResult = tablesSetBorderAdapter( + ed, + { target: firstCell.address, edge: 'top', lineStyle: 'single', lineWeightPt: 1, color: '000000' }, + DIRECT, + ); + + expect(borderResult.success).toBe(true); + }); + it('tables.move returns a chainable ref after relocating the table', () => { const ed = createEditor(); diff --git a/packages/super-editor/src/document-api-adapters/tables-adapter.ts b/packages/super-editor/src/document-api-adapters/tables-adapter.ts index a9bb87d525..3c00d91699 100644 --- a/packages/super-editor/src/document-api-adapters/tables-adapter.ts +++ b/packages/super-editor/src/document-api-adapters/tables-adapter.ts @@ -3632,8 +3632,10 @@ export function tablesGetCellsAdapter(editor: Editor, input: TablesGetCellsInput if (input.columnIndex != null && col !== input.columnIndex) continue; const attrs = candidate.node.attrs as Record; + const cellNodeId = candidate.nodeId || resolveCellNodeId(attrs); cells.push({ - nodeId: candidate.nodeId || resolveCellNodeId(attrs), + nodeId: cellNodeId, + address: { kind: 'block', nodeType: 'tableCell', nodeId: cellNodeId }, rowIndex: row, columnIndex: col, colspan: typeof attrs.colspan === 'number' ? attrs.colspan : 1, diff --git a/tests/doc-api-stories/tests/tables/all-commands.ts b/tests/doc-api-stories/tests/tables/all-commands.ts index 10a36bebdf..55975b4a88 100644 --- a/tests/doc-api-stories/tests/tables/all-commands.ts +++ b/tests/doc-api-stories/tests/tables/all-commands.ts @@ -142,6 +142,8 @@ describe('document-api story: all table commands', () => { expect(Array.isArray(result?.cells)).toBe(true); expect(result.cells.length).toBeGreaterThan(0); expect(typeof result.cells[0]?.nodeId).toBe('string'); + expect(result.cells[0]?.address?.nodeId).toBe(result.cells[0]?.nodeId); + expect(result.cells[0]?.address?.nodeType).toBe('tableCell'); return; } From 627beabfb2266206b6cf8f58d193eb2093fbf8f6 Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Wed, 18 Mar 2026 21:37:59 -0700 Subject: [PATCH 2/2] chore: fix lint --- packages/document-api/src/types/table-operations.types.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/document-api/src/types/table-operations.types.ts b/packages/document-api/src/types/table-operations.types.ts index e20eb264f7..299c250c5d 100644 --- a/packages/document-api/src/types/table-operations.types.ts +++ b/packages/document-api/src/types/table-operations.types.ts @@ -6,9 +6,6 @@ import type { TableOrRowAddress, TableRowAddress, } from './base.js'; - -// Re-export for downstream callers that reference cell addresses via this module. -export type { TableCellAddress } from './base.js'; import type { ReceiptFailure, ReceiptInsert } from './receipt.js'; // ---------------------------------------------------------------------------