diff --git a/apps/cli/scripts/export-sdk-contract.ts b/apps/cli/scripts/export-sdk-contract.ts index ced815bcea..77d82420a4 100644 --- a/apps/cli/scripts/export-sdk-contract.ts +++ b/apps/cli/scripts/export-sdk-contract.ts @@ -186,6 +186,9 @@ const INTENT_NAMES = { 'doc.tables.get': 'get_table', 'doc.tables.getCells': 'get_table_cells', 'doc.tables.getProperties': 'get_table_properties', + 'doc.tables.getStyles': 'get_table_styles', + 'doc.tables.setDefaultStyle': 'set_table_default_style', + 'doc.tables.clearDefaultStyle': 'clear_table_default_style', 'doc.history.get': 'get_history', 'doc.history.undo': 'undo', 'doc.history.redo': 'redo', diff --git a/apps/cli/src/__tests__/conformance/scenarios.ts b/apps/cli/src/__tests__/conformance/scenarios.ts index bce69c75f4..50a6ec0445 100644 --- a/apps/cli/src/__tests__/conformance/scenarios.ts +++ b/apps/cli/src/__tests__/conformance/scenarios.ts @@ -1908,6 +1908,57 @@ export const SUCCESS_SCENARIOS = { 'doc.tables.get': tableReadScenario('tables.get'), 'doc.tables.getCells': tableReadScenario('tables.getCells'), 'doc.tables.getProperties': tableReadScenario('tables.getProperties'), + 'doc.tables.getStyles': async (harness: ConformanceHarness): Promise => { + const stateDir = await harness.createStateDir('table-getStyles-success'); + const { sessionId } = await harness.createTableFixture(stateDir, 'table-getStyles'); + return { + stateDir, + args: [...commandTokens('doc.tables.getStyles'), '--session', sessionId], + }; + }, + 'doc.tables.setDefaultStyle': async (harness: ConformanceHarness): Promise => { + const stateDir = await harness.createStateDir('table-setDefaultStyle-success'); + const { sessionId } = await harness.createTableFixture(stateDir, 'table-setDefaultStyle'); + return { + stateDir, + args: [ + ...commandTokens('doc.tables.setDefaultStyle'), + '--session', + sessionId, + '--style-id', + 'TableGrid', + '--out', + harness.createOutputPath('table-setDefaultStyle-out'), + ], + }; + }, + 'doc.tables.clearDefaultStyle': async (harness: ConformanceHarness): Promise => { + const stateDir = await harness.createStateDir('table-clearDefaultStyle-success'); + const { sessionId } = await harness.createTableFixture(stateDir, 'table-clearDefaultStyle'); + // First set a default so the clear actually has something to remove + await harness.runCli( + [ + ...commandTokens('doc.tables.setDefaultStyle'), + '--session', + sessionId, + '--style-id', + 'TableGrid', + '--out', + harness.createOutputPath('table-clearDefaultStyle-setup-out'), + ], + stateDir, + ); + return { + stateDir, + args: [ + ...commandTokens('doc.tables.clearDefaultStyle'), + '--session', + sessionId, + '--out', + harness.createOutputPath('table-clearDefaultStyle-out'), + ], + }; + }, // --------------------------------------------------------------------------- // History operations @@ -1935,6 +1986,10 @@ const RUNTIME_CONFORMANCE_SKIP = new Set([ 'doc.toc.unmarkEntry', 'doc.toc.getEntry', 'doc.toc.editEntry', + // OOB table-style mutations require translatedLinkedStyles from the style-engine, + // which the CLI test harness fixture does not populate. + 'doc.tables.setDefaultStyle', + 'doc.tables.clearDefaultStyle', ]); export const OPERATION_SCENARIOS = (Object.keys(SUCCESS_SCENARIOS) as CliOperationId[]).map((operationId) => { diff --git a/apps/cli/src/cli/operation-hints.ts b/apps/cli/src/cli/operation-hints.ts index af4160e54a..76c6ce45ef 100644 --- a/apps/cli/src/cli/operation-hints.ts +++ b/apps/cli/src/cli/operation-hints.ts @@ -168,6 +168,9 @@ export const SUCCESS_VERB: Record = { 'tables.get': 'resolved table', 'tables.getCells': 'listed cells', 'tables.getProperties': 'resolved table properties', + 'tables.getStyles': 'listed table styles', + 'tables.setDefaultStyle': 'set default table style', + 'tables.clearDefaultStyle': 'cleared default table style', 'history.get': 'retrieved history state', 'history.undo': 'undid last change', 'history.redo': 'redid last change', @@ -298,6 +301,9 @@ export const OUTPUT_FORMAT: Record = { 'tables.get': 'tableInfo', 'tables.getCells': 'tableCellList', 'tables.getProperties': 'tablePropertiesInfo', + 'tables.getStyles': 'plain', + 'tables.setDefaultStyle': 'plain', + 'tables.clearDefaultStyle': 'plain', 'history.get': 'plain', 'history.undo': 'plain', 'history.redo': 'plain', @@ -412,6 +418,9 @@ export const RESPONSE_ENVELOPE_KEY: Record 'tables.get': 'result', 'tables.getCells': 'result', 'tables.getProperties': 'result', + 'tables.getStyles': 'result', + 'tables.setDefaultStyle': 'result', + 'tables.clearDefaultStyle': 'result', 'history.get': 'result', 'history.undo': 'result', 'history.redo': 'result', @@ -554,6 +563,9 @@ export const OPERATION_FAMILY: Record = 'tables.get': 'tables', 'tables.getCells': 'tables', 'tables.getProperties': 'tables', + 'tables.getStyles': 'tables', + 'tables.setDefaultStyle': 'tables', + 'tables.clearDefaultStyle': 'tables', 'history.get': 'query', 'history.undo': 'general', 'history.redo': 'general', diff --git a/apps/docs/document-api/available-operations.mdx b/apps/docs/document-api/available-operations.mdx index 2340c9180f..f596f3dd7d 100644 --- a/apps/docs/document-api/available-operations.mdx +++ b/apps/docs/document-api/available-operations.mdx @@ -29,7 +29,7 @@ Use the tables below to see what operations are available and where each one is | Sections | 18 | 0 | 18 | [Reference](/document-api/reference/sections/index) | | Styles | 1 | 0 | 1 | [Reference](/document-api/reference/styles/index) | | Table of Contents | 10 | 0 | 10 | [Reference](/document-api/reference/toc/index) | -| Tables | 39 | 0 | 39 | [Reference](/document-api/reference/tables/index) | +| Tables | 42 | 0 | 42 | [Reference](/document-api/reference/tables/index) | | Track Changes | 3 | 0 | 3 | [Reference](/document-api/reference/track-changes/index) | | Editor method | Operation | @@ -209,6 +209,9 @@ Use the tables below to see what operations are available and where each one is | editor.doc.tables.get(...) | [`tables.get`](/document-api/reference/tables/get) | | editor.doc.tables.getCells(...) | [`tables.getCells`](/document-api/reference/tables/get-cells) | | editor.doc.tables.getProperties(...) | [`tables.getProperties`](/document-api/reference/tables/get-properties) | +| editor.doc.tables.getStyles(...) | [`tables.getStyles`](/document-api/reference/tables/get-styles) | +| editor.doc.tables.setDefaultStyle(...) | [`tables.setDefaultStyle`](/document-api/reference/tables/set-default-style) | +| editor.doc.tables.clearDefaultStyle(...) | [`tables.clearDefaultStyle`](/document-api/reference/tables/clear-default-style) | | editor.doc.trackChanges.list(...) | [`trackChanges.list`](/document-api/reference/track-changes/list) | | editor.doc.trackChanges.get(...) | [`trackChanges.get`](/document-api/reference/track-changes/get) | | editor.doc.trackChanges.decide(...) | [`trackChanges.decide`](/document-api/reference/track-changes/decide) | diff --git a/apps/docs/document-api/reference/_generated-manifest.json b/apps/docs/document-api/reference/_generated-manifest.json index be3a7835ee..93adadeeca 100644 --- a/apps/docs/document-api/reference/_generated-manifest.json +++ b/apps/docs/document-api/reference/_generated-manifest.json @@ -145,6 +145,7 @@ "apps/docs/document-api/reference/tables/clear-border.mdx", "apps/docs/document-api/reference/tables/clear-cell-spacing.mdx", "apps/docs/document-api/reference/tables/clear-contents.mdx", + "apps/docs/document-api/reference/tables/clear-default-style.mdx", "apps/docs/document-api/reference/tables/clear-shading.mdx", "apps/docs/document-api/reference/tables/clear-style.mdx", "apps/docs/document-api/reference/tables/convert-from-text.mdx", @@ -157,6 +158,7 @@ "apps/docs/document-api/reference/tables/distribute-rows.mdx", "apps/docs/document-api/reference/tables/get-cells.mdx", "apps/docs/document-api/reference/tables/get-properties.mdx", + "apps/docs/document-api/reference/tables/get-styles.mdx", "apps/docs/document-api/reference/tables/get.mdx", "apps/docs/document-api/reference/tables/index.mdx", "apps/docs/document-api/reference/tables/insert-cell.mdx", @@ -170,6 +172,7 @@ "apps/docs/document-api/reference/tables/set-cell-properties.mdx", "apps/docs/document-api/reference/tables/set-cell-spacing.mdx", "apps/docs/document-api/reference/tables/set-column-width.mdx", + "apps/docs/document-api/reference/tables/set-default-style.mdx", "apps/docs/document-api/reference/tables/set-layout.mdx", "apps/docs/document-api/reference/tables/set-row-height.mdx", "apps/docs/document-api/reference/tables/set-row-options.mdx", @@ -445,7 +448,10 @@ "tables.clearCellSpacing", "tables.get", "tables.getCells", - "tables.getProperties" + "tables.getProperties", + "tables.getStyles", + "tables.setDefaultStyle", + "tables.clearDefaultStyle" ], "pagePath": "apps/docs/document-api/reference/tables/index.mdx", "title": "Tables" @@ -477,5 +483,5 @@ } ], "marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}", - "sourceHash": "f7276958e365ad87346f051a9dc2d6b1a6313353302531b6f669409cf97b3dcc" + "sourceHash": "f6a9d24400077e26e602241bee1ab5f4c0ea8e0d112b2c7425182458977c1885" } diff --git a/apps/docs/document-api/reference/capabilities/get.mdx b/apps/docs/document-api/reference/capabilities/get.mdx index f2b1e39f2d..ce26328619 100644 --- a/apps/docs/document-api/reference/capabilities/get.mdx +++ b/apps/docs/document-api/reference/capabilities/get.mdx @@ -917,6 +917,11 @@ _No fields._ | `operations.tables.clearContents.dryRun` | boolean | yes | | | `operations.tables.clearContents.reasons` | enum[] | no | | | `operations.tables.clearContents.tracked` | boolean | yes | | +| `operations.tables.clearDefaultStyle` | object | yes | | +| `operations.tables.clearDefaultStyle.available` | boolean | yes | | +| `operations.tables.clearDefaultStyle.dryRun` | boolean | yes | | +| `operations.tables.clearDefaultStyle.reasons` | enum[] | no | | +| `operations.tables.clearDefaultStyle.tracked` | boolean | yes | | | `operations.tables.clearShading` | object | yes | | | `operations.tables.clearShading.available` | boolean | yes | | | `operations.tables.clearShading.dryRun` | boolean | yes | | @@ -982,6 +987,11 @@ _No fields._ | `operations.tables.getProperties.dryRun` | boolean | yes | | | `operations.tables.getProperties.reasons` | enum[] | no | | | `operations.tables.getProperties.tracked` | boolean | yes | | +| `operations.tables.getStyles` | object | yes | | +| `operations.tables.getStyles.available` | boolean | yes | | +| `operations.tables.getStyles.dryRun` | boolean | yes | | +| `operations.tables.getStyles.reasons` | enum[] | no | | +| `operations.tables.getStyles.tracked` | boolean | yes | | | `operations.tables.insertCell` | object | yes | | | `operations.tables.insertCell.available` | boolean | yes | | | `operations.tables.insertCell.dryRun` | boolean | yes | | @@ -1037,6 +1047,11 @@ _No fields._ | `operations.tables.setColumnWidth.dryRun` | boolean | yes | | | `operations.tables.setColumnWidth.reasons` | enum[] | no | | | `operations.tables.setColumnWidth.tracked` | boolean | yes | | +| `operations.tables.setDefaultStyle` | object | yes | | +| `operations.tables.setDefaultStyle.available` | boolean | yes | | +| `operations.tables.setDefaultStyle.dryRun` | boolean | yes | | +| `operations.tables.setDefaultStyle.reasons` | enum[] | no | | +| `operations.tables.setDefaultStyle.tracked` | boolean | yes | | | `operations.tables.setLayout` | object | yes | | | `operations.tables.setLayout.available` | boolean | yes | | | `operations.tables.setLayout.dryRun` | boolean | yes | | @@ -2495,6 +2510,14 @@ _No fields._ ], "tracked": true }, + "tables.clearDefaultStyle": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, "tables.clearShading": { "available": true, "dryRun": true, @@ -2599,6 +2622,14 @@ _No fields._ ], "tracked": true }, + "tables.getStyles": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, "tables.insertCell": { "available": true, "dryRun": true, @@ -2687,6 +2718,14 @@ _No fields._ ], "tracked": true }, + "tables.setDefaultStyle": { + "available": true, + "dryRun": true, + "reasons": [ + "COMMAND_UNAVAILABLE" + ], + "tracked": true + }, "tables.setLayout": { "available": true, "dryRun": true, @@ -9067,6 +9106,41 @@ _No fields._ ], "type": "object" }, + "tables.clearDefaultStyle": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, "tables.clearShading": { "additionalProperties": false, "properties": { @@ -9522,6 +9596,41 @@ _No fields._ ], "type": "object" }, + "tables.getStyles": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, "tables.insertCell": { "additionalProperties": false, "properties": { @@ -9907,6 +10016,41 @@ _No fields._ ], "type": "object" }, + "tables.setDefaultStyle": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, "tables.setLayout": { "additionalProperties": false, "properties": { @@ -10912,6 +11056,9 @@ _No fields._ "tables.get", "tables.getCells", "tables.getProperties", + "tables.getStyles", + "tables.setDefaultStyle", + "tables.clearDefaultStyle", "create.tableOfContents", "toc.list", "toc.get", diff --git a/apps/docs/document-api/reference/index.mdx b/apps/docs/document-api/reference/index.mdx index 04da8bee4a..3e69d116a5 100644 --- a/apps/docs/document-api/reference/index.mdx +++ b/apps/docs/document-api/reference/index.mdx @@ -34,7 +34,7 @@ Document API is currently alpha and subject to breaking changes. | Mutations | 2 | 0 | 2 | [Open](/document-api/reference/mutations/index) | | Paragraph Formatting | 17 | 0 | 17 | [Open](/document-api/reference/format/paragraph/index) | | Paragraph Styles | 2 | 0 | 2 | [Open](/document-api/reference/styles/paragraph/index) | -| Tables | 39 | 0 | 39 | [Open](/document-api/reference/tables/index) | +| Tables | 42 | 0 | 42 | [Open](/document-api/reference/tables/index) | | History | 3 | 0 | 3 | [Open](/document-api/reference/history/index) | | Table of Contents | 10 | 0 | 10 | [Open](/document-api/reference/toc/index) | @@ -281,6 +281,9 @@ The tables below are grouped by namespace. | tables.get | editor.doc.tables.get(...) | Retrieve table structure and dimensions by locator. | | tables.getCells | editor.doc.tables.getCells(...) | Retrieve cell information for a table, optionally filtered by row or column. | | tables.getProperties | editor.doc.tables.getProperties(...) | Retrieve layout and style properties of a table. | +| tables.getStyles | editor.doc.tables.getStyles(...) | List all table styles and the document-level default table style setting. | +| tables.setDefaultStyle | editor.doc.tables.setDefaultStyle(...) | Set the document-level default table style (w:defaultTableStyle in settings.xml). | +| tables.clearDefaultStyle | editor.doc.tables.clearDefaultStyle(...) | Remove the document-level default table style setting. | #### History diff --git a/apps/docs/document-api/reference/tables/clear-default-style.mdx b/apps/docs/document-api/reference/tables/clear-default-style.mdx new file mode 100644 index 0000000000..3373bb0a30 --- /dev/null +++ b/apps/docs/document-api/reference/tables/clear-default-style.mdx @@ -0,0 +1,188 @@ +--- +title: tables.clearDefaultStyle +sidebarTitle: tables.clearDefaultStyle +description: Remove the document-level default table style setting. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Remove the document-level default table style setting. + +- Operation ID: `tables.clearDefaultStyle` +- API member path: `editor.doc.tables.clearDefaultStyle(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a DocumentMutationResult; reports NO_OP if no default is set. + +## Input fields + +_No fields._ + +### Example request + +```json +{} +``` + +## Output fields + +### Variant 1 (success=true) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `success` | `true` | yes | Constant: `true` | + +### Variant 2 (success=false) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `failure` | object | yes | | +| `failure.code` | enum | yes | `"NO_OP"` | +| `failure.details` | any | no | | +| `failure.message` | string | yes | | +| `success` | `false` | yes | Constant: `false` | + +### Example response + +```json +{ + "success": true +} +``` + +## Pre-apply throws + +- `CAPABILITY_UNAVAILABLE` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": {}, + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "success": { + "const": true + } + }, + "required": [ + "success" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "success": { + "const": true + } + }, + "required": [ + "success" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/tables/get-styles.mdx b/apps/docs/document-api/reference/tables/get-styles.mdx new file mode 100644 index 0000000000..78004ec94f --- /dev/null +++ b/apps/docs/document-api/reference/tables/get-styles.mdx @@ -0,0 +1,180 @@ +--- +title: tables.getStyles +sidebarTitle: tables.getStyles +description: List all table styles and the document-level default table style setting. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +List all table styles and the document-level default table style setting. + +- Operation ID: `tables.getStyles` +- API member path: `editor.doc.tables.getStyles(...)` +- Mutates document: `no` +- Idempotency: `idempotent` +- Supports tracked mode: `no` +- Supports dry run: `no` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a TablesGetStylesOutput with the style catalog, explicit default, and effective default. + +## Input fields + +_No fields._ + +### Example request + +```json +{} +``` + +## Output fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `effectiveDefaultSource` | string | yes | | +| `effectiveDefaultStyleId` | any | yes | | +| `explicitDefaultStyleId` | any | yes | | +| `styles` | object[] | yes | | + +### Example response + +```json +{ + "effectiveDefaultSource": "example", + "effectiveDefaultStyleId": {}, + "explicitDefaultStyleId": {}, + "styles": [ + { + "basedOn": {}, + "conditionalRegions": [ + "example" + ], + "hidden": true, + "id": "id-001", + "isCustom": true, + "isDefault": true, + "name": {}, + "quickFormat": true, + "uiPriority": {} + } + ] +} +``` + +## Pre-apply throws + +- None + +## Non-applied failure codes + +- None + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": {}, + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "effectiveDefaultSource": { + "type": "string" + }, + "effectiveDefaultStyleId": { + "type": [ + "string", + "null" + ] + }, + "explicitDefaultStyleId": { + "type": [ + "string", + "null" + ] + }, + "styles": { + "items": { + "additionalProperties": false, + "properties": { + "basedOn": { + "type": [ + "string", + "null" + ] + }, + "conditionalRegions": { + "items": { + "type": "string" + }, + "type": "array" + }, + "hidden": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "isCustom": { + "type": "boolean" + }, + "isDefault": { + "type": "boolean" + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "quickFormat": { + "type": "boolean" + }, + "uiPriority": { + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "id", + "name", + "basedOn", + "isDefault", + "isCustom", + "uiPriority", + "hidden", + "quickFormat", + "conditionalRegions" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "explicitDefaultStyleId", + "effectiveDefaultStyleId", + "effectiveDefaultSource", + "styles" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/tables/index.mdx b/apps/docs/document-api/reference/tables/index.mdx index 5e99ca5fdb..16aa3a02b7 100644 --- a/apps/docs/document-api/reference/tables/index.mdx +++ b/apps/docs/document-api/reference/tables/index.mdx @@ -53,4 +53,7 @@ Table structure, layout, styling, and cell operations. | tables.get | `tables.get` | No | `idempotent` | No | No | | tables.getCells | `tables.getCells` | No | `idempotent` | No | No | | tables.getProperties | `tables.getProperties` | No | `idempotent` | No | No | +| tables.getStyles | `tables.getStyles` | No | `idempotent` | No | No | +| tables.setDefaultStyle | `tables.setDefaultStyle` | Yes | `idempotent` | No | Yes | +| tables.clearDefaultStyle | `tables.clearDefaultStyle` | Yes | `conditional` | No | Yes | diff --git a/apps/docs/document-api/reference/tables/set-default-style.mdx b/apps/docs/document-api/reference/tables/set-default-style.mdx new file mode 100644 index 0000000000..50ebac600d --- /dev/null +++ b/apps/docs/document-api/reference/tables/set-default-style.mdx @@ -0,0 +1,203 @@ +--- +title: tables.setDefaultStyle +sidebarTitle: tables.setDefaultStyle +description: "Set the document-level default table style (w:defaultTableStyle in settings.xml)." +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Set the document-level default table style (w:defaultTableStyle in settings.xml). + +- Operation ID: `tables.setDefaultStyle` +- API member path: `editor.doc.tables.setDefaultStyle(...)` +- Mutates document: `yes` +- Idempotency: `idempotent` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a DocumentMutationResult; reports NO_OP if the default already matches. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `styleId` | string | yes | | + +### Example request + +```json +{ + "styleId": "style-001" +} +``` + +## Output fields + +### Variant 1 (success=true) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `success` | `true` | yes | Constant: `true` | + +### Variant 2 (success=false) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `failure` | object | yes | | +| `failure.code` | enum | yes | `"NO_OP"`, `"INVALID_INPUT"` | +| `failure.details` | any | no | | +| `failure.message` | string | yes | | +| `success` | `false` | yes | Constant: `false` | + +### Example response + +```json +{ + "success": true +} +``` + +## Pre-apply throws + +- `CAPABILITY_UNAVAILABLE` +- `INVALID_INPUT` + +## Non-applied failure codes + +- `NO_OP` +- `INVALID_INPUT` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "styleId": { + "type": "string" + } + }, + "required": [ + "styleId" + ], + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "success": { + "const": true + } + }, + "required": [ + "success" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_INPUT" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "success": { + "const": true + } + }, + "required": [ + "success" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_INPUT" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/packages/document-api/src/contract/contract.test.ts b/packages/document-api/src/contract/contract.test.ts index cde528e653..3d4c3fa36a 100644 --- a/packages/document-api/src/contract/contract.test.ts +++ b/packages/document-api/src/contract/contract.test.ts @@ -200,7 +200,13 @@ describe('document-api contract catalog', () => { // styles.apply + all sections.set* / sections.clear* mutations expect(historyUnsafeOps).toContain('styles.apply'); for (const id of historyUnsafeOps) { - expect(id.startsWith('sections.') || id === 'styles.apply', `unexpected historyUnsafe: ${id}`).toBe(true); + expect( + id.startsWith('sections.') || + id === 'styles.apply' || + id === 'tables.setDefaultStyle' || + id === 'tables.clearDefaultStyle', + `unexpected historyUnsafe: ${id}`, + ).toBe(true); } // All section mutations (set*/clear*) should be marked diff --git a/packages/document-api/src/contract/operation-definitions.ts b/packages/document-api/src/contract/operation-definitions.ts index 9823d43da7..180eea90b8 100644 --- a/packages/document-api/src/contract/operation-definitions.ts +++ b/packages/document-api/src/contract/operation-definitions.ts @@ -2063,6 +2063,47 @@ export const OPERATION_DEFINITIONS = { referenceDocPath: 'tables/get-properties.mdx', referenceGroup: 'tables', }, + 'tables.getStyles': { + memberPath: 'tables.getStyles', + description: 'List all table styles and the document-level default table style setting.', + expectedResult: 'Returns a TablesGetStylesOutput with the style catalog, explicit default, and effective default.', + requiresDocumentContext: true, + metadata: readOperation({ idempotency: 'idempotent' }), + referenceDocPath: 'tables/get-styles.mdx', + referenceGroup: 'tables', + }, + 'tables.setDefaultStyle': { + memberPath: 'tables.setDefaultStyle', + description: 'Set the document-level default table style (w:defaultTableStyle in settings.xml).', + expectedResult: 'Returns a DocumentMutationResult; reports NO_OP if the default already matches.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'idempotent', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP', 'INVALID_INPUT'], + throws: ['CAPABILITY_UNAVAILABLE', 'INVALID_INPUT'], + historyUnsafe: true, + }), + referenceDocPath: 'tables/set-default-style.mdx', + referenceGroup: 'tables', + }, + 'tables.clearDefaultStyle': { + memberPath: 'tables.clearDefaultStyle', + description: 'Remove the document-level default table style setting.', + expectedResult: 'Returns a DocumentMutationResult; reports NO_OP if no default is set.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP'], + throws: ['CAPABILITY_UNAVAILABLE'], + historyUnsafe: true, + }), + referenceDocPath: 'tables/clear-default-style.mdx', + referenceGroup: 'tables', + }, // ------------------------------------------------------------------------- // Create: table of contents // ------------------------------------------------------------------------- diff --git a/packages/document-api/src/contract/operation-registry.ts b/packages/document-api/src/contract/operation-registry.ts index d6c1c2b105..325755bcfd 100644 --- a/packages/document-api/src/contract/operation-registry.ts +++ b/packages/document-api/src/contract/operation-registry.ts @@ -190,6 +190,10 @@ import type { TablesGetCellsOutput, TablesGetPropertiesInput, TablesGetPropertiesOutput, + TablesGetStylesInput, + TablesGetStylesOutput, + TablesSetDefaultStyleInput, + TablesClearDefaultStyleInput, } from '../types/table-operations.types.js'; type FormatInlineAliasOperationRegistry = { @@ -532,6 +536,17 @@ export interface OperationRegistry extends FormatInlineAliasOperationRegistry { 'tables.get': { input: TablesGetInput; options: never; output: TablesGetOutput }; 'tables.getCells': { input: TablesGetCellsInput; options: never; output: TablesGetCellsOutput }; 'tables.getProperties': { input: TablesGetPropertiesInput; options: never; output: TablesGetPropertiesOutput }; + 'tables.getStyles': { input: TablesGetStylesInput | undefined; options: never; output: TablesGetStylesOutput }; + 'tables.setDefaultStyle': { + input: TablesSetDefaultStyleInput; + options: MutationOptions; + output: DocumentMutationResult; + }; + 'tables.clearDefaultStyle': { + input: TablesClearDefaultStyleInput | undefined; + options: MutationOptions; + output: DocumentMutationResult; + }; // --- create.tableOfContents --- 'create.tableOfContents': { diff --git a/packages/document-api/src/contract/schemas.ts b/packages/document-api/src/contract/schemas.ts index 8e5dd60873..df86da6020 100644 --- a/packages/document-api/src/contract/schemas.ts +++ b/packages/document-api/src/contract/schemas.ts @@ -3409,6 +3409,55 @@ const operationSchemas: Record = { ['nodeId'], ), }, + 'tables.getStyles': { + input: strictEmptyObjectSchema, + output: objectSchema( + { + explicitDefaultStyleId: { type: ['string', 'null'] }, + effectiveDefaultStyleId: { type: ['string', 'null'] }, + effectiveDefaultSource: { type: 'string' }, + styles: arraySchema( + objectSchema( + { + id: { type: 'string' }, + name: { type: ['string', 'null'] }, + basedOn: { type: ['string', 'null'] }, + isDefault: { type: 'boolean' }, + isCustom: { type: 'boolean' }, + uiPriority: { type: ['integer', 'null'] }, + hidden: { type: 'boolean' }, + quickFormat: { type: 'boolean' }, + conditionalRegions: arraySchema({ type: 'string' }), + }, + [ + 'id', + 'name', + 'basedOn', + 'isDefault', + 'isCustom', + 'uiPriority', + 'hidden', + 'quickFormat', + 'conditionalRegions', + ], + ), + ), + }, + ['explicitDefaultStyleId', 'effectiveDefaultStyleId', 'effectiveDefaultSource', 'styles'], + ), + }, + 'tables.setDefaultStyle': { + input: objectSchema({ styleId: { type: 'string' } }, ['styleId']), + output: documentMutationResultSchemaFor('tables.setDefaultStyle'), + success: documentMutationSuccessSchema, + failure: sectionMutationFailureSchemaFor('tables.setDefaultStyle'), + }, + 'tables.clearDefaultStyle': { + input: strictEmptyObjectSchema, + output: documentMutationResultSchemaFor('tables.clearDefaultStyle'), + success: documentMutationSuccessSchema, + failure: sectionMutationFailureSchemaFor('tables.clearDefaultStyle'), + }, // --- history.* --- 'history.get': { diff --git a/packages/document-api/src/index.ts b/packages/document-api/src/index.ts index c1782a87df..fbd50c6061 100644 --- a/packages/document-api/src/index.ts +++ b/packages/document-api/src/index.ts @@ -179,6 +179,10 @@ import type { TablesGetCellsOutput, TablesGetPropertiesInput, TablesGetPropertiesOutput, + TablesGetStylesInput, + TablesGetStylesOutput, + TablesSetDefaultStyleInput, + TablesClearDefaultStyleInput, } from './types/table-operations.types.js'; import type { TrackChangesAdapter, @@ -642,6 +646,9 @@ export interface TablesApi { get(input: TablesGetInput): TablesGetOutput; getCells(input: TablesGetCellsInput): TablesGetCellsOutput; getProperties(input: TablesGetPropertiesInput): TablesGetPropertiesOutput; + getStyles(input?: TablesGetStylesInput): TablesGetStylesOutput; + setDefaultStyle(input: TablesSetDefaultStyleInput, options?: MutationOptions): DocumentMutationResult; + clearDefaultStyle(input?: TablesClearDefaultStyleInput, options?: MutationOptions): DocumentMutationResult; } export type TablesAdapter = TablesApi; @@ -1395,6 +1402,15 @@ export function createDocumentApi(adapters: DocumentApiAdapters): DocumentApi { getProperties(input) { return adapters.tables.getProperties(input); }, + getStyles(input?) { + return adapters.tables.getStyles(input); + }, + setDefaultStyle(input: TablesSetDefaultStyleInput, options?: MutationOptions) { + return adapters.tables.setDefaultStyle(input, options); + }, + clearDefaultStyle(input?: TablesClearDefaultStyleInput, options?: MutationOptions) { + return adapters.tables.clearDefaultStyle(input, options); + }, }, toc: { list(query?: TocListQuery): TocListResult { diff --git a/packages/document-api/src/invoke/invoke.ts b/packages/document-api/src/invoke/invoke.ts index 9b18d133bd..d01ca9c602 100644 --- a/packages/document-api/src/invoke/invoke.ts +++ b/packages/document-api/src/invoke/invoke.ts @@ -219,6 +219,9 @@ export function buildDispatchTable(api: DocumentApi): TypedDispatchTable { 'tables.get': (input) => api.tables.get(input), 'tables.getCells': (input) => api.tables.getCells(input), 'tables.getProperties': (input) => api.tables.getProperties(input), + 'tables.getStyles': (input) => api.tables.getStyles(input), + 'tables.setDefaultStyle': (input, options) => api.tables.setDefaultStyle(input, options), + 'tables.clearDefaultStyle': (input, options) => api.tables.clearDefaultStyle(input, options), // --- create.tableOfContents --- 'create.tableOfContents': (input, options) => api.create.tableOfContents(input, options), diff --git a/packages/document-api/src/types/table-operations.types.ts b/packages/document-api/src/types/table-operations.types.ts index 0cb4de30fb..1f88fb6174 100644 --- a/packages/document-api/src/types/table-operations.types.ts +++ b/packages/document-api/src/types/table-operations.types.ts @@ -388,6 +388,42 @@ export interface TablesSetCellSpacingInput extends TableLocator { export type TablesClearCellSpacingInput = TableLocator; +// --------------------------------------------------------------------------- +// Document-level style queries & mutations +// --------------------------------------------------------------------------- + +/** Input for `tables.getStyles` — document-level query, no locator needed. */ +export type TablesGetStylesInput = Record; + +/** Per-style metadata returned by `tables.getStyles`. */ +export interface TableStyleInfo { + id: string; + name: string | null; + basedOn: string | null; + isDefault: boolean; + isCustom: boolean; + uiPriority: number | null; + hidden: boolean; + quickFormat: boolean; + conditionalRegions: string[]; +} + +/** Output for `tables.getStyles`. */ +export interface TablesGetStylesOutput { + explicitDefaultStyleId: string | null; + effectiveDefaultStyleId: string | null; + effectiveDefaultSource: string; + styles: TableStyleInfo[]; +} + +/** Input for `tables.setDefaultStyle`. */ +export interface TablesSetDefaultStyleInput { + styleId: string; +} + +/** Input for `tables.clearDefaultStyle`. */ +export type TablesClearDefaultStyleInput = Record; + // --------------------------------------------------------------------------- // Read operations (B4: ref handoff) // --------------------------------------------------------------------------- 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 188cca8700..a3b29f7d51 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 @@ -117,7 +117,13 @@ import { getRevision, initRevision } from '../plan-engine/revision-tracker.js'; import { executePlan } from '../plan-engine/executor.js'; import { toCanonicalTrackedChangeId } from '../helpers/tracked-change-resolver.js'; import { writeAdapter } from '../write-adapter.js'; -import { tablesGetCellsAdapter, tablesGetPropertiesAdapter } from '../tables-adapter.js'; +import { + tablesGetCellsAdapter, + tablesGetPropertiesAdapter, + tablesGetStylesAdapter, + tablesSetDefaultStyleAdapter, + tablesClearDefaultStyleAdapter, +} from '../tables-adapter.js'; import { createSectionBreakAdapter, sectionsSetBreakTypeAdapter, @@ -1192,6 +1198,9 @@ const IMPLEMENTED_TABLE_OPS: ReadonlySet = new Set([ 'tables.setCellPadding', 'tables.setCellSpacing', 'tables.clearCellSpacing', + 'tables.getStyles', + 'tables.setDefaultStyle', + 'tables.clearDefaultStyle', ] as OperationId[]); /** Table stub ops that always throw CAPABILITY_UNAVAILABLE. */ @@ -3730,6 +3739,83 @@ const mutationVectors: Partial> = { return tablesClearCellSpacingWrapper(editor, { nodeId: 'table-1' }, { changeMode: 'direct' }); }, }, + 'tables.setDefaultStyle': { + throwCase: () => { + // No converter → CAPABILITY_UNAVAILABLE + const editor = makeSectionsEditor({ includeConverter: false }); + return tablesSetDefaultStyleAdapter(editor, { styleId: 'TableGrid' }, { changeMode: 'direct' }); + }, + failureCase: () => { + // Style already set → NO_OP + const editor = makeSectionsEditor(); + const converter = (editor as unknown as { converter: Record }).converter; + converter.translatedLinkedStyles = { + styles: { TableGrid: { type: 'table', name: 'Table Grid' } }, + docDefaults: {}, + latentStyles: {}, + }; + // Pre-set the default so the adapter sees it's already the same + const settingsRoot = (converter.convertedXml as Record }>)[ + 'word/settings.xml' + ]; + const wSettings = settingsRoot?.elements?.find( + (el: { name?: string }) => (el as { name?: string }).name === 'w:settings', + ) as { elements?: unknown[] } | undefined; + if (wSettings) { + if (!wSettings.elements) wSettings.elements = []; + wSettings.elements.push({ + type: 'element', + name: 'w:defaultTableStyle', + attributes: { 'w:val': 'TableGrid' }, + elements: [], + }); + } + return tablesSetDefaultStyleAdapter(editor, { styleId: 'TableGrid' }, { changeMode: 'direct' }); + }, + applyCase: () => { + const editor = makeSectionsEditor(); + const converter = (editor as unknown as { converter: Record }).converter; + converter.translatedLinkedStyles = { + styles: { TableGrid: { type: 'table', name: 'Table Grid' } }, + docDefaults: {}, + latentStyles: {}, + }; + return tablesSetDefaultStyleAdapter(editor, { styleId: 'TableGrid' }, { changeMode: 'direct' }); + }, + }, + 'tables.clearDefaultStyle': { + throwCase: () => { + // No converter → CAPABILITY_UNAVAILABLE + const editor = makeSectionsEditor({ includeConverter: false }); + return tablesClearDefaultStyleAdapter(editor, {}, { changeMode: 'direct' }); + }, + failureCase: () => { + // No default set → NO_OP + const editor = makeSectionsEditor(); + return tablesClearDefaultStyleAdapter(editor, {}, { changeMode: 'direct' }); + }, + applyCase: () => { + const editor = makeSectionsEditor(); + const converter = (editor as unknown as { converter: Record }).converter; + // Pre-set a default so clear actually has something to remove + const settingsRoot = (converter.convertedXml as Record }>)[ + 'word/settings.xml' + ]; + const wSettings = settingsRoot?.elements?.find( + (el: { name?: string }) => (el as { name?: string }).name === 'w:settings', + ) as { elements?: unknown[] } | undefined; + if (wSettings) { + if (!wSettings.elements) wSettings.elements = []; + wSettings.elements.push({ + type: 'element', + name: 'w:defaultTableStyle', + attributes: { 'w:val': 'TableGrid' }, + elements: [], + }); + } + return tablesClearDefaultStyleAdapter(editor, {}, { changeMode: 'direct' }); + }, + }, 'styles.apply': { throwCase: () => { const editor = makeStylesEditor({ hasConverter: false }); @@ -4761,6 +4847,46 @@ const dryRunVectors: Partial unknown>> = { expect(dispatch).not.toHaveBeenCalled(); return result; }, + 'tables.setDefaultStyle': () => { + const editor = makeSectionsEditor(); + const converter = (editor as unknown as { converter: Record }).converter; + converter.translatedLinkedStyles = { + styles: { TableGrid: { type: 'table', name: 'Table Grid' } }, + docDefaults: {}, + latentStyles: {}, + }; + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = tablesSetDefaultStyleAdapter( + editor, + { styleId: 'TableGrid' }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'tables.clearDefaultStyle': () => { + const editor = makeSectionsEditor(); + const converter = (editor as unknown as { converter: Record }).converter; + const settingsRoot = (converter.convertedXml as Record }>)[ + 'word/settings.xml' + ]; + const wSettings = settingsRoot?.elements?.find( + (el: { name?: string }) => (el as { name?: string }).name === 'w:settings', + ) as { elements?: unknown[] } | undefined; + if (wSettings) { + if (!wSettings.elements) wSettings.elements = []; + wSettings.elements.push({ + type: 'element', + name: 'w:defaultTableStyle', + attributes: { 'w:val': 'TableGrid' }, + elements: [], + }); + } + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = tablesClearDefaultStyleAdapter(editor, {}, { changeMode: 'direct', dryRun: true }); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, // ------------------------------------------------------------------------- // TOC operations — dryRun vectors @@ -5155,6 +5281,8 @@ describe('document-api adapter conformance', () => { 'tables.clearCellSpacing', 'tables.insertCell', 'tables.deleteCell', + 'tables.setDefaultStyle', + 'tables.clearDefaultStyle', ] as OperationId[]; for (const opId of nonTrackedTableOps) { @@ -5219,6 +5347,40 @@ describe('document-api adapter conformance', () => { expect(deleteColResult.success).toBe(true); }); + // --------------------------------------------------------------------------- + // tables.getStyles: returns graceful empty result without converter + // --------------------------------------------------------------------------- + it('returns empty styles payload when no converter is available (tables.getStyles)', () => { + const editor = makeSectionsEditor({ includeConverter: false }); + const result = tablesGetStylesAdapter(editor); + expect(result).toEqual({ + explicitDefaultStyleId: null, + effectiveDefaultStyleId: null, + effectiveDefaultSource: 'none', + styles: [], + }); + }); + + // --------------------------------------------------------------------------- + // tables.setDefaultStyle: throws INVALID_INPUT for unknown style id + // --------------------------------------------------------------------------- + it('throws INVALID_INPUT when styleId is not a known table style (tables.setDefaultStyle)', () => { + const editor = makeSectionsEditor(); + const converter = (editor as unknown as { converter: Record }).converter; + converter.translatedLinkedStyles = { + styles: { TableGrid: { type: 'table', name: 'Table Grid' } }, + docDefaults: {}, + latentStyles: {}, + }; + let capturedCode: string | null = null; + try { + tablesSetDefaultStyleAdapter(editor, { styleId: 'NonExistentStyle' }, { changeMode: 'direct' }); + } catch (error) { + capturedCode = (error as { code?: string }).code ?? null; + } + expect(capturedCode).toBe('INVALID_INPUT'); + }); + // --------------------------------------------------------------------------- // Layer A gap: Wrapper parity — doc.tables. vs mutations.apply // These tests verify that table wrappers route through executeCompiledPlan diff --git a/packages/super-editor/src/document-api-adapters/assemble-adapters.ts b/packages/super-editor/src/document-api-adapters/assemble-adapters.ts index c5af34a919..495bbd7dd1 100644 --- a/packages/super-editor/src/document-api-adapters/assemble-adapters.ts +++ b/packages/super-editor/src/document-api-adapters/assemble-adapters.ts @@ -123,7 +123,14 @@ import { tablesSetCellSpacingWrapper, tablesClearCellSpacingWrapper, } from './plan-engine/tables-wrappers.js'; -import { tablesGetAdapter, tablesGetCellsAdapter, tablesGetPropertiesAdapter } from './tables-adapter.js'; +import { + tablesGetAdapter, + tablesGetCellsAdapter, + tablesGetPropertiesAdapter, + tablesGetStylesAdapter, + tablesSetDefaultStyleAdapter, + tablesClearDefaultStyleAdapter, +} from './tables-adapter.js'; import { createHistoryAdapter } from './history-adapter.js'; import { tocListWrapper, @@ -298,6 +305,9 @@ export function assembleDocumentApiAdapters(editor: Editor): DocumentApiAdapters get: (input) => tablesGetAdapter(editor, input), getCells: (input) => tablesGetCellsAdapter(editor, input), getProperties: (input) => tablesGetPropertiesAdapter(editor, input), + getStyles: (input) => tablesGetStylesAdapter(editor, input), + setDefaultStyle: (input, options) => tablesSetDefaultStyleAdapter(editor, input, options), + clearDefaultStyle: (input, options) => tablesClearDefaultStyleAdapter(editor, input, options), }, toc: { list: (query) => tocListWrapper(editor, query), diff --git a/packages/super-editor/src/document-api-adapters/capabilities-adapter.ts b/packages/super-editor/src/document-api-adapters/capabilities-adapter.ts index c52904c253..3c5751e979 100644 --- a/packages/super-editor/src/document-api-adapters/capabilities-adapter.ts +++ b/packages/super-editor/src/document-api-adapters/capabilities-adapter.ts @@ -128,6 +128,8 @@ const REQUIRED_HELPERS: Partial boolean> typeof (editor as unknown as EditorWithBlockNodeHelper).helpers?.blockNode?.getBlockNodeById === 'function', 'sections.setOddEvenHeadersFooters': (editor) => Boolean((editor as unknown as { converter?: unknown }).converter), 'sections.setHeaderFooterRef': (editor) => Boolean((editor as unknown as { converter?: unknown }).converter), + 'tables.setDefaultStyle': (editor) => Boolean((editor as unknown as { converter?: unknown }).converter), + 'tables.clearDefaultStyle': (editor) => Boolean((editor as unknown as { converter?: unknown }).converter), }; function hasRequiredHelpers(editor: Editor, operationId: OperationId): boolean { diff --git a/packages/super-editor/src/document-api-adapters/errors.ts b/packages/super-editor/src/document-api-adapters/errors.ts index ede3aad479..b8166625bc 100644 --- a/packages/super-editor/src/document-api-adapters/errors.ts +++ b/packages/super-editor/src/document-api-adapters/errors.ts @@ -4,6 +4,7 @@ export type DocumentApiAdapterErrorCode = | 'INVALID_TARGET' | 'AMBIGUOUS_TARGET' | 'CAPABILITY_UNAVAILABLE' + | 'INVALID_INPUT' | 'INTERNAL_ERROR'; /** 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 27f9e1ea59..b1e48bbed2 100644 --- a/packages/super-editor/src/document-api-adapters/tables-adapter.ts +++ b/packages/super-editor/src/document-api-adapters/tables-adapter.ts @@ -48,6 +48,12 @@ import type { TableCellInfo, TablesGetPropertiesInput, TablesGetPropertiesOutput, + TablesGetStylesInput, + TablesGetStylesOutput, + TableStyleInfo, + TablesSetDefaultStyleInput, + TablesClearDefaultStyleInput, + DocumentMutationResult, } from '@superdoc/document-api'; import type { Transaction } from 'prosemirror-state'; import { TableMap } from 'prosemirror-tables'; @@ -68,6 +74,20 @@ import { applyDirectMutationMeta, applyTrackedMutationMeta } from './helpers/tra import { DocumentApiAdapterError } from './errors.js'; import { toBlockAddress, findBlockById, findBlockByNodeIdOnly } from './helpers/node-address-resolver.js'; import { twipsToPixels } from '../core/super-converter/helpers.js'; +import { + resolvePreferredNewTableStyleId, + isKnownTableStyleId, + type StylesDocumentProperties, +} from '@superdoc/style-engine/ooxml'; +import { + readSettingsRoot, + ensureSettingsRoot, + readDefaultTableStyle, + setDefaultTableStyle, + removeDefaultTableStyle, + type ConverterWithDocumentSettings, +} from './document-settings.js'; +import { executeOutOfBandMutation } from './out-of-band-mutation.js'; // --------------------------------------------------------------------------- // Helpers @@ -3618,3 +3638,179 @@ export function tablesGetPropertiesAdapter(editor: Editor, input: TablesGetPrope return result; } + +// --------------------------------------------------------------------------- +// Document-level table style operations +// --------------------------------------------------------------------------- + +interface ConverterForTableStyles extends ConverterWithDocumentSettings { + translatedLinkedStyles?: StylesDocumentProperties | null; +} + +function getConverterForStyles(editor: Editor): ConverterForTableStyles | undefined { + return (editor as unknown as { converter?: ConverterForTableStyles }).converter; +} + +function toDocumentMutationFailure(code: 'NO_OP' | 'INVALID_INPUT', message: string): DocumentMutationResult { + return { + success: false, + failure: { code, message }, + }; +} + +function toDocumentMutationSuccess(): DocumentMutationResult { + return { success: true }; +} + +export function tablesGetStylesAdapter(editor: Editor, _input?: TablesGetStylesInput): TablesGetStylesOutput { + const converter = getConverterForStyles(editor); + if (!converter) { + return { + explicitDefaultStyleId: null, + effectiveDefaultStyleId: null, + effectiveDefaultSource: 'none', + styles: [], + }; + } + + const translatedLinkedStyles = converter.translatedLinkedStyles ?? null; + const allStyles = translatedLinkedStyles?.styles ?? {}; + + // Collect table styles + const styles: TableStyleInfo[] = []; + for (const [id, def] of Object.entries(allStyles)) { + if (def.type !== 'table') continue; + styles.push({ + id, + name: def.name ?? null, + basedOn: def.basedOn ?? null, + isDefault: def.default === true, + isCustom: def.customStyle === true, + uiPriority: def.uiPriority ?? null, + hidden: def.hidden === true || def.semiHidden === true, + quickFormat: def.qFormat === true, + conditionalRegions: def.tableStyleProperties ? Object.keys(def.tableStyleProperties) : [], + }); + } + + // Read explicit default from settings.xml + let explicitDefaultStyleId: string | null = null; + const settingsRoot = readSettingsRoot(converter); + if (settingsRoot) { + explicitDefaultStyleId = readDefaultTableStyle(settingsRoot); + } + + // Resolve effective default + const resolved = resolvePreferredNewTableStyleId(explicitDefaultStyleId, translatedLinkedStyles); + + return { + explicitDefaultStyleId, + effectiveDefaultStyleId: resolved.styleId, + effectiveDefaultSource: resolved.source, + styles, + }; +} + +export function tablesSetDefaultStyleAdapter( + editor: Editor, + input: TablesSetDefaultStyleInput, + options?: MutationOptions, +): DocumentMutationResult { + rejectTrackedMode('tables.setDefaultStyle', options); + + const converter = getConverterForStyles(editor); + if (!converter) { + throw new DocumentApiAdapterError( + 'CAPABILITY_UNAVAILABLE', + 'tables.setDefaultStyle requires an active document converter.', + ); + } + + // Validate styleId + if (!isKnownTableStyleId(input.styleId, converter.translatedLinkedStyles)) { + throw new DocumentApiAdapterError( + 'INVALID_INPUT', + `tables.setDefaultStyle: "${input.styleId}" is not a known table style.`, + ); + } + + return executeOutOfBandMutation( + editor, + (dryRun) => { + const existingRoot = readSettingsRoot(converter); + const current = existingRoot ? readDefaultTableStyle(existingRoot) : null; + + if (current === input.styleId) { + return { + changed: false, + payload: toDocumentMutationFailure( + 'NO_OP', + 'tables.setDefaultStyle did not produce a document settings change.', + ), + }; + } + + if (!dryRun) { + const settingsRoot = ensureSettingsRoot(converter); + setDefaultTableStyle(settingsRoot, input.styleId); + } + + return { + changed: true, + payload: toDocumentMutationSuccess(), + }; + }, + { + dryRun: options?.dryRun === true, + expectedRevision: options?.expectedRevision, + }, + ); +} + +export function tablesClearDefaultStyleAdapter( + editor: Editor, + _input?: TablesClearDefaultStyleInput, + options?: MutationOptions, +): DocumentMutationResult { + rejectTrackedMode('tables.clearDefaultStyle', options); + + const converter = getConverterForStyles(editor); + if (!converter) { + throw new DocumentApiAdapterError( + 'CAPABILITY_UNAVAILABLE', + 'tables.clearDefaultStyle requires an active document converter.', + ); + } + + return executeOutOfBandMutation( + editor, + (dryRun) => { + const existingRoot = readSettingsRoot(converter); + const current = existingRoot ? readDefaultTableStyle(existingRoot) : null; + + if (current === null) { + return { + changed: false, + payload: toDocumentMutationFailure( + 'NO_OP', + 'tables.clearDefaultStyle did not produce a document settings change.', + ), + }; + } + + if (!dryRun) { + const settingsRoot = ensureSettingsRoot(converter); + removeDefaultTableStyle(settingsRoot); + } + + return { + changed: true, + payload: toDocumentMutationSuccess(), + }; + }, + { + dryRun: options?.dryRun === true, + expectedRevision: options?.expectedRevision, + }, + ); +}