Skip to content

Commit 5eca65b

Browse files
authored
feat(document-api): accept table coordinates in unmergeCells (#2462)
* feat(document-api): accept table coordinates in unmergeCells * chore: type fixes * fix(document-api): treat null coordinates as absent in unmerge cell locator checks
1 parent e84f695 commit 5eca65b

14 files changed

Lines changed: 910 additions & 36 deletions

File tree

apps/docs/document-api/reference/_generated-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -976,5 +976,5 @@
976976
}
977977
],
978978
"marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}",
979-
"sourceHash": "3b3a367f04f06a39426291c6d41ab669a58f346f6520fde2f67e1c8e84abfad5"
979+
"sourceHash": "5eb339719530fd6ff1e69c9c90c36637fedce9fc426b3aba84f973c73facf3e0"
980980
}

apps/docs/document-api/reference/tables/unmerge-cells.mdx

Lines changed: 80 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,30 @@ Returns a TableMutationResult receipt; reports NO_OP if the cell is not merged.
3030

3131
| Field | Type | Required | Description |
3232
| --- | --- | --- | --- |
33-
| `target` | TableCellAddress | yes | TableCellAddress |
33+
| `nodeId` | string | no | |
34+
| `target` | TableCellAddress | no | TableCellAddress |
35+
| `target.kind` | `"block"` | no | Constant: `"block"` |
36+
| `target.nodeId` | string | no | |
37+
| `target.nodeType` | `"tableCell"` | no | Constant: `"tableCell"` |
38+
39+
### Variant 2 (target.nodeType="table")
40+
41+
| Field | Type | Required | Description |
42+
| --- | --- | --- | --- |
43+
| `columnIndex` | integer | yes | |
44+
| `rowIndex` | integer | yes | |
45+
| `target` | TableAddress | yes | TableAddress |
3446
| `target.kind` | `"block"` | yes | Constant: `"block"` |
3547
| `target.nodeId` | string | yes | |
36-
| `target.nodeType` | `"tableCell"` | yes | Constant: `"tableCell"` |
48+
| `target.nodeType` | `"table"` | yes | Constant: `"table"` |
3749

38-
### Variant 2 (nodeId)
50+
### Variant 3 (nodeId, rowIndex, columnIndex)
3951

4052
| Field | Type | Required | Description |
4153
| --- | --- | --- | --- |
54+
| `columnIndex` | integer | yes | |
4255
| `nodeId` | string | yes | |
56+
| `rowIndex` | integer | yes | |
4357

4458
### Example request
4559

@@ -116,28 +130,76 @@ When present, `result.table` is the follow-up address to reuse after this call.
116130
<Accordion title="Raw input schema">
117131
```json
118132
{
119-
"additionalProperties": false,
120133
"oneOf": [
121134
{
122-
"required": [
123-
"target"
124-
]
135+
"additionalProperties": false,
136+
"oneOf": [
137+
{
138+
"required": [
139+
"target"
140+
]
141+
},
142+
{
143+
"required": [
144+
"nodeId"
145+
]
146+
}
147+
],
148+
"properties": {
149+
"nodeId": {
150+
"type": "string"
151+
},
152+
"target": {
153+
"$ref": "#/$defs/TableCellAddress"
154+
}
155+
},
156+
"type": "object"
125157
},
126158
{
159+
"additionalProperties": false,
160+
"properties": {
161+
"columnIndex": {
162+
"minimum": 0,
163+
"type": "integer"
164+
},
165+
"rowIndex": {
166+
"minimum": 0,
167+
"type": "integer"
168+
},
169+
"target": {
170+
"$ref": "#/$defs/TableAddress"
171+
}
172+
},
127173
"required": [
128-
"nodeId"
129-
]
130-
}
131-
],
132-
"properties": {
133-
"nodeId": {
134-
"type": "string"
174+
"target",
175+
"rowIndex",
176+
"columnIndex"
177+
],
178+
"type": "object"
135179
},
136-
"target": {
137-
"$ref": "#/$defs/TableCellAddress"
180+
{
181+
"additionalProperties": false,
182+
"properties": {
183+
"columnIndex": {
184+
"minimum": 0,
185+
"type": "integer"
186+
},
187+
"nodeId": {
188+
"type": "string"
189+
},
190+
"rowIndex": {
191+
"minimum": 0,
192+
"type": "integer"
193+
}
194+
},
195+
"required": [
196+
"nodeId",
197+
"rowIndex",
198+
"columnIndex"
199+
],
200+
"type": "object"
138201
}
139-
},
140-
"type": "object"
202+
]
141203
}
142204
```
143205
</Accordion>

packages/document-api/src/contract/contract.test.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ describe('document-api contract catalog', () => {
194194
properties?: { address?: { $ref?: string } };
195195
};
196196
const unmergeInput = schemas.operations['tables.unmergeCells'].input as {
197-
properties?: { target?: { $ref?: string } };
197+
oneOf?: Array<Record<string, unknown>>;
198198
};
199199
const setBorderInput = schemas.operations['tables.setBorder'].input as {
200200
properties?: { target?: { $ref?: string } };
@@ -205,7 +205,25 @@ describe('document-api contract catalog', () => {
205205

206206
expect(tablesGetInput.properties?.target?.$ref).toBe('#/$defs/TableAddress');
207207
expect(tablesGetOutput.properties?.address?.$ref).toBe('#/$defs/TableAddress');
208-
expect(unmergeInput.properties?.target?.$ref).toBe('#/$defs/TableCellAddress');
208+
209+
// unmergeCells input is a oneOf: [cellLocator, tableScopedCellLocator (target), tableScopedCellLocator (nodeId)]
210+
expect(unmergeInput.oneOf).toHaveLength(3);
211+
const [cellBranch, tableTargetBranch, tableNodeIdBranch] = unmergeInput.oneOf as Array<{
212+
properties?: { target?: { $ref?: string }; nodeId?: unknown; rowIndex?: unknown; columnIndex?: unknown };
213+
required?: string[];
214+
}>;
215+
// First branch: direct cell locator (target.$ref → TableCellAddress)
216+
expect(cellBranch.properties?.target?.$ref).toBe('#/$defs/TableCellAddress');
217+
// Second branch: table-scoped with target (target.$ref → TableAddress + coordinates)
218+
expect(tableTargetBranch.properties?.target?.$ref).toBe('#/$defs/TableAddress');
219+
expect(tableTargetBranch.required).toContain('rowIndex');
220+
expect(tableTargetBranch.required).toContain('columnIndex');
221+
// Third branch: table-scoped with nodeId + coordinates
222+
expect(tableNodeIdBranch.properties?.nodeId).toBeDefined();
223+
expect(tableNodeIdBranch.required).toContain('nodeId');
224+
expect(tableNodeIdBranch.required).toContain('rowIndex');
225+
expect(tableNodeIdBranch.required).toContain('columnIndex');
226+
209227
expect(setBorderInput.properties?.target?.$ref).toBe('#/$defs/TableOrCellAddress');
210228
expect(insertRowSuccess.properties?.table?.$ref).toBe('#/$defs/TableAddress');
211229
});

packages/document-api/src/contract/schemas.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1507,6 +1507,32 @@ const cellLocatorSchema: JsonSchema = {
15071507
oneOf: [{ required: ['target'] }, { required: ['nodeId'] }],
15081508
};
15091509

1510+
/**
1511+
* Accepts either a direct cell locator (target/nodeId pointing at a cell)
1512+
* or a table-scoped cell locator (target/nodeId pointing at a table + rowIndex + columnIndex).
1513+
*/
1514+
const cellOrTableScopedCellLocatorSchema: JsonSchema = {
1515+
oneOf: [
1516+
cellLocatorSchema,
1517+
objectSchema(
1518+
{
1519+
target: tableAddressSchema,
1520+
rowIndex: { type: 'integer', minimum: 0 },
1521+
columnIndex: { type: 'integer', minimum: 0 },
1522+
},
1523+
['target', 'rowIndex', 'columnIndex'],
1524+
),
1525+
objectSchema(
1526+
{
1527+
nodeId: { type: 'string' },
1528+
rowIndex: { type: 'integer', minimum: 0 },
1529+
columnIndex: { type: 'integer', minimum: 0 },
1530+
},
1531+
['nodeId', 'rowIndex', 'columnIndex'],
1532+
),
1533+
],
1534+
};
1535+
15101536
const tableOrCellLocatorSchema: JsonSchema = {
15111537
...objectSchema({
15121538
target: tableOrCellAddressSchema,
@@ -4925,7 +4951,7 @@ const operationSchemas: Record<OperationId, OperationSchemaSet> = {
49254951
failure: tableMutationFailureSchema,
49264952
},
49274953
'tables.unmergeCells': {
4928-
input: cellLocatorSchema,
4954+
input: cellOrTableScopedCellLocatorSchema,
49294955
output: tableMutationResultSchema,
49304956
success: tableMutationSuccessSchema,
49314957
failure: tableMutationFailureSchema,

packages/document-api/src/index.test.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2742,6 +2742,94 @@ describe('createDocumentApi', () => {
27422742
).not.toThrow();
27432743
});
27442744

2745+
// -- unmergeCells mixed cell/table-scoped locator validation --
2746+
2747+
it('accepts direct cell nodeId for unmergeCells', () => {
2748+
const api = makeApi();
2749+
expect(() => api.tables.unmergeCells({ nodeId: 'cell-1' })).not.toThrow();
2750+
});
2751+
2752+
it('accepts direct cell target for unmergeCells', () => {
2753+
const api = makeApi();
2754+
const target = { kind: 'block' as const, nodeType: 'tableCell' as const, nodeId: 'c1' };
2755+
expect(() => api.tables.unmergeCells({ target })).not.toThrow();
2756+
});
2757+
2758+
it('treats explicit null coordinates as absent for direct cell target on unmergeCells', () => {
2759+
const api = makeApi();
2760+
const target = { kind: 'block' as const, nodeType: 'tableCell' as const, nodeId: 'c1' };
2761+
expect(() => api.tables.unmergeCells({ target, rowIndex: null, columnIndex: null } as any)).not.toThrow();
2762+
});
2763+
2764+
it('accepts table-scoped locator (nodeId + rowIndex + columnIndex) for unmergeCells', () => {
2765+
const api = makeApi();
2766+
expect(() => api.tables.unmergeCells({ nodeId: 'table-1', rowIndex: 0, columnIndex: 0 })).not.toThrow();
2767+
});
2768+
2769+
it('accepts table-scoped locator (target + rowIndex + columnIndex) for unmergeCells', () => {
2770+
const api = makeApi();
2771+
const target = { kind: 'block' as const, nodeType: 'table' as const, nodeId: 't1' };
2772+
expect(() => api.tables.unmergeCells({ target, rowIndex: 0, columnIndex: 0 })).not.toThrow();
2773+
});
2774+
2775+
it('treats explicit undefined coordinates as a direct cell call for unmergeCells', () => {
2776+
const api = makeApi();
2777+
// { nodeId, rowIndex: undefined, columnIndex: undefined } must pass validation
2778+
// as a direct-cell call — the keys exist but the values are absent.
2779+
expect(() =>
2780+
api.tables.unmergeCells({ nodeId: 'cell-1', rowIndex: undefined, columnIndex: undefined } as any),
2781+
).not.toThrow();
2782+
});
2783+
2784+
it('rejects unmergeCells with only rowIndex (missing columnIndex)', () => {
2785+
const api = makeApi();
2786+
expect(() => api.tables.unmergeCells({ nodeId: 'table-1', rowIndex: 0 } as any)).toThrow(
2787+
/both rowIndex and columnIndex/,
2788+
);
2789+
});
2790+
2791+
it('rejects unmergeCells with only columnIndex (missing rowIndex)', () => {
2792+
const api = makeApi();
2793+
expect(() => api.tables.unmergeCells({ nodeId: 'table-1', columnIndex: 0 } as any)).toThrow(
2794+
/both rowIndex and columnIndex/,
2795+
);
2796+
});
2797+
2798+
it('rejects unmergeCells with cell target plus coordinates', () => {
2799+
const api = makeApi();
2800+
const target = { kind: 'block' as const, nodeType: 'tableCell' as const, nodeId: 'c1' };
2801+
expect(() => api.tables.unmergeCells({ target, rowIndex: 0, columnIndex: 0 } as any)).toThrow(
2802+
/must not be provided when target is a cell node/,
2803+
);
2804+
});
2805+
2806+
it('rejects unmergeCells with table target without coordinates', () => {
2807+
const api = makeApi();
2808+
const target = { kind: 'block' as const, nodeType: 'table' as const, nodeId: 't1' };
2809+
expect(() => api.tables.unmergeCells({ target } as any)).toThrow(
2810+
/rowIndex and columnIndex are required when target is a table/,
2811+
);
2812+
});
2813+
2814+
it('rejects unmergeCells with table target and null coordinates', () => {
2815+
const api = makeApi();
2816+
const target = { kind: 'block' as const, nodeType: 'table' as const, nodeId: 't1' };
2817+
expect(() => api.tables.unmergeCells({ target, rowIndex: null, columnIndex: null } as any)).toThrow(
2818+
/rowIndex and columnIndex are required when target is a table/,
2819+
);
2820+
});
2821+
2822+
it('rejects unmergeCells with table target and mixed null coordinates', () => {
2823+
const api = makeApi();
2824+
const target = { kind: 'block' as const, nodeType: 'table' as const, nodeId: 't1' };
2825+
expect(() => api.tables.unmergeCells({ target, rowIndex: null, columnIndex: 0 } as any)).toThrow(
2826+
/both rowIndex and columnIndex/,
2827+
);
2828+
expect(() => api.tables.unmergeCells({ target, rowIndex: 0, columnIndex: null } as any)).toThrow(
2829+
/both rowIndex and columnIndex/,
2830+
);
2831+
});
2832+
27452833
// -- create.table locator validation --
27462834

27472835
it('rejects ambiguous create.table at locator (both target + nodeId)', () => {

packages/document-api/src/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,12 @@ import type {
311311
DiffApplyInput,
312312
DiffApplyOptions,
313313
} from './diff/diff.types.js';
314-
import { executeTableLocatorOp, executeRowLocatorOp, executeDocumentLevelTableOp } from './tables/tables.js';
314+
import {
315+
executeTableLocatorOp,
316+
executeRowLocatorOp,
317+
executeCellOrTableScopedCellLocatorOp,
318+
executeDocumentLevelTableOp,
319+
} from './tables/tables.js';
315320
import type {
316321
ParagraphsAdapter,
317322
ParagraphFormatApi,
@@ -2321,7 +2326,7 @@ export function createDocumentApi(adapters: DocumentApiAdapters): DocumentApi {
23212326
);
23222327
},
23232328
unmergeCells(input, options?) {
2324-
return executeTableLocatorOp(
2329+
return executeCellOrTableScopedCellLocatorOp(
23252330
'tables.unmergeCells',
23262331
adapters.tables.unmergeCells.bind(adapters.tables),
23272332
input,

0 commit comments

Comments
 (0)