From 0419fa7c2c5f29e578e76e9ed319a29d125cb46a Mon Sep 17 00:00:00 2001 From: Reversean Date: Sun, 24 May 2026 15:22:47 +0300 Subject: [PATCH 1/2] feat: implement block conversion (export side) Add exportTextContent() to BlockToolFacade: supports function, string keypath, and dot-notation keypath via conversionConfig.export. Add convertBlock() to BlocksManager: exports text from the source block, imports it into the target type, and replaces the block at the same index. --- packages/core/src/api/BlocksAPI.ts | 17 ++++ .../core/src/components/BlockManager.spec.ts | 93 +++++++++++++++++++ packages/core/src/components/BlockManager.ts | 28 ++++++ packages/sdk/src/api/BlocksAPI.ts | 19 +++- .../src/tools/facades/BaseToolFacade.spec.ts | 60 ++++++++++++ .../sdk/src/tools/facades/BlockToolFacade.ts | 25 ++++- 6 files changed, 237 insertions(+), 5 deletions(-) diff --git a/packages/core/src/api/BlocksAPI.ts b/packages/core/src/api/BlocksAPI.ts index b61d8893..0ad51329 100644 --- a/packages/core/src/api/BlocksAPI.ts +++ b/packages/core/src/api/BlocksAPI.ts @@ -208,4 +208,21 @@ export class BlocksAPI implements BlocksApiInterface { public split({ block, key, offset, userId }: Parameters[0]): void { this.#blocksManager.splitBlock(block as BlockIndexOrId, createDataKey(key), offset, userId); } + + /** + * Converts a block to a new type + * @param params - conversion parameters + * @param params.block - index or id of the block to convert + * @param params.newType - block tool name to convert to + * @param [params.dataOverrides] - optional data overrides for the new block + * @param [params.userId] - user id to attribute the change to + */ + public convert({ + block, + newType, + dataOverrides, + userId = this.#config.userId, + }: Parameters[0]): void { + this.#blocksManager.convertBlock(block, newType, userId, dataOverrides); + } } diff --git a/packages/core/src/components/BlockManager.spec.ts b/packages/core/src/components/BlockManager.spec.ts index 8102c397..7aefea52 100644 --- a/packages/core/src/components/BlockManager.spec.ts +++ b/packages/core/src/components/BlockManager.spec.ts @@ -328,6 +328,99 @@ describe('BlocksManager (unit, mocked deps)', () => { }); }); + describe('.convertBlock()', () => { + beforeEach(() => { + model.resolveBlockIndex = jest.fn(() => 0); + model.getBlockSerialized = jest.fn(() => ({ + name: 'header', + id: 'b1', + data: { text: { value: 'Hello', + fragments: [] } }, + })); + }); + + it('exports text from source, imports into target, and replaces block at the same index', () => { + const sourceTool = { exportTextContent: jest.fn(() => 'Hello') }; + const targetTool = { importTextContent: jest.fn(() => ({ text: { value: 'Hello', + fragments: [] } })) }; + + // @ts-expect-error — mock + toolsManager.blockTools.get = jest.fn((name: string) => + name === 'header' ? sourceTool : targetTool + ); + + blocksManager.convertBlock('b1', 'paragraph'); + + expect(sourceTool.exportTextContent).toHaveBeenCalledWith({ text: { value: 'Hello', + fragments: [] } }); + expect(targetTool.importTextContent).toHaveBeenCalledWith('Hello', []); + expect(model.removeBlock).toHaveBeenCalledWith(USER_ID, 0); + expect(model.addBlock).toHaveBeenCalledWith( + USER_ID, + { name: 'paragraph', + data: { text: { value: 'Hello', + fragments: [] } } }, + 0 + ); + }); + + it('merges dataOverrides on top of the imported data', () => { + const sourceTool = { exportTextContent: jest.fn(() => 'Hello') }; + const targetTool = { importTextContent: jest.fn(() => ({ text: { value: 'Hello', + fragments: [] } })) }; + + // @ts-expect-error — mock + toolsManager.blockTools.get = jest.fn((name: string) => + name === 'header' ? sourceTool : targetTool + ); + + blocksManager.convertBlock('b1', 'paragraph', USER_ID, { level: 2 }); + + expect(model.addBlock).toHaveBeenCalledWith( + USER_ID, + { name: 'paragraph', + data: { text: { value: 'Hello', + fragments: [] }, + level: 2 } }, + 0 + ); + }); + + it('throws if source tool has no export config', () => { + const sourceTool = { + exportTextContent: jest.fn(() => { + throw new Error('Tool header does not have export configuration for text content'); + }), + }; + const targetTool = { importTextContent: jest.fn() }; + + // @ts-expect-error — mock + toolsManager.blockTools.get = jest.fn((name: string) => + name === 'header' ? sourceTool : targetTool + ); + + expect(() => blocksManager.convertBlock('b1', 'paragraph')) + .toThrow('does not have export configuration'); + }); + + it('throws if target tool has no import config', () => { + const sourceTool = { exportTextContent: jest.fn(() => 'Hello') }; + const targetTool = { + importTextContent: jest.fn(() => { + throw new Error('Tool paragraph does not have import configuration for text content'); + }), + }; + + // @ts-expect-error — mock + toolsManager.blockTools.get = jest.fn((name: string) => + name === 'header' ? sourceTool : targetTool + ); + + expect(() => blocksManager.convertBlock('b1', 'paragraph')) + .toThrow('does not have import configuration'); + }); + }); + describe('.splitBlock()', () => { /** * Restore split-specific mock implementations that jest.resetAllMocks() clears. diff --git a/packages/core/src/components/BlockManager.ts b/packages/core/src/components/BlockManager.ts index 3e5abbde..9fdb51c7 100644 --- a/packages/core/src/components/BlockManager.ts +++ b/packages/core/src/components/BlockManager.ts @@ -370,6 +370,34 @@ export class BlocksManager { }, blockIndex + 1); } + /** + * Converts a block to a new type by exporting its text content and importing it into the new tool. + * Both the source and target tools must define conversionConfig. + * @param blockId - id or index of the block to convert + * @param newType - block tool name to convert to + * @param [userId] - user id to attribute the change to + * @param [dataOverrides] - optional data fields to merge on top of the converted data + */ + public convertBlock(blockId: string | number, newType: string, userId: string | number = this.#config.userId, dataOverrides?: BlockToolData): void { + const blockIndex = this.#model.resolveBlockIndex(blockId as BlockId); + + const block = this.#model.getBlockSerialized(blockIndex); + + const sourceTool = this.#toolsManager.blockTools.get(block.name)!; + const targetTool = this.#toolsManager.blockTools.get(newType)!; + + const text = sourceTool.exportTextContent(block.data); + const newData = targetTool.importTextContent(text, []); + const finalData = dataOverrides !== undefined + ? { ...newData, + ...dataOverrides } + : newData; + + this.#model.removeBlock(userId, blockIndex); + this.#model.addBlock(userId, { name: newType, + data: finalData }, blockIndex); + } + /** * Returns block index where user caret is placed */ diff --git a/packages/sdk/src/api/BlocksAPI.ts b/packages/sdk/src/api/BlocksAPI.ts index 565498e5..7422ec43 100644 --- a/packages/sdk/src/api/BlocksAPI.ts +++ b/packages/sdk/src/api/BlocksAPI.ts @@ -249,10 +249,21 @@ export interface BlocksAPI { /** * Converts block to another type. Both blocks should provide the conversionConfig. - * @param id - id of the existed block to convert. Should provide 'conversionConfig.export' method - * @param newType - new block type. Should provide 'conversionConfig.import' method - * @param dataOverrides - optional data overrides for the new block + * @param params.block - index or id of the block to convert. Should provide 'conversionConfig.export' method + * @param params.newType - new block type. Should provide 'conversionConfig.import' method + * @param [params.dataOverrides] - optional data overrides for the new block + * @param [params.userId] - user id to attribute the change to * @throws Error if conversion is not possible */ - // convert(id: string, newType: string, dataOverrides?: BlockToolData): Promise; + // @todo return BlockAPI when it is implemented + convert(params: { + /** Index or id of the block to convert */ + block: number | string; + /** Block tool name to convert to */ + newType: string; + /** Optional data overrides for the new block */ + dataOverrides?: BlockToolData; + /** User id. Defaults to the current user id from the config */ + userId?: string | number; + }): void; } diff --git a/packages/sdk/src/tools/facades/BaseToolFacade.spec.ts b/packages/sdk/src/tools/facades/BaseToolFacade.spec.ts index 76d4207c..2994ef1c 100644 --- a/packages/sdk/src/tools/facades/BaseToolFacade.spec.ts +++ b/packages/sdk/src/tools/facades/BaseToolFacade.spec.ts @@ -165,6 +165,66 @@ describe('BaseToolFacade (via BlockToolFacade)', () => { }); }); + describe('exportTextContent', () => { + it('throws when the tool has no conversionConfig.export', () => { + const facade = createBlockFacade({}, {} as ToolOptions); + + expect(() => facade.exportTextContent({})).toThrow( + /does not have export configuration/ + ); + }); + + it('calls the export function when conversionConfig.export is a function', () => { + const exportFn = (data: BlockToolData): string => (data.text as { value: string }).value; + const facade = createBlockFacade( + { [BlockToolOptionKey.ConversionConfig]: { export: exportFn } }, + {} as ToolOptions + ); + + const result = facade.exportTextContent({ text: { value: 'hello' } }); + + expect(result).toBe('hello'); + }); + + it('reads a top-level string key from data', () => { + const facade = createBlockFacade( + { [BlockToolOptionKey.ConversionConfig]: { export: 'text' } }, + {} as ToolOptions + ); + + const result = facade.exportTextContent({ + text: { + value: 'hello', + fragments: [], + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + }, + }); + + expect(result).toBe('hello'); + }); + + it('reads a dot-notation keypath from data', () => { + const facade = createBlockFacade( + { [BlockToolOptionKey.ConversionConfig]: { export: 'items.0.text' } }, + {} as ToolOptions + ); + + const result = facade.exportTextContent({ + items: [ + { + text: { + value: 'hello', + fragments: [], + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + }, + }, + ], + }); + + expect(result).toBe('hello'); + }); + }); + describe('importTextContent', () => { it('throws when the tool has no conversionConfig.import', () => { const facade = createBlockFacade({}, {} as ToolOptions); diff --git a/packages/sdk/src/tools/facades/BlockToolFacade.ts b/packages/sdk/src/tools/facades/BlockToolFacade.ts index 34a34295..879f5c64 100644 --- a/packages/sdk/src/tools/facades/BlockToolFacade.ts +++ b/packages/sdk/src/tools/facades/BlockToolFacade.ts @@ -19,7 +19,7 @@ import { ToolsCollection } from '../ToolsCollection.js'; import type { BlockToolConstructor, BlockToolConstructorOptions, BlockTool, BlockToolData } from '../../entities'; import { ToolType } from '../../entities'; import { BlockChildType, NODE_TYPE_HIDDEN_PROP, keypath } from '@editorjs/model'; -import type { InlineFragment } from '@editorjs/model'; +import type { InlineFragment, TextNodeSerialized } from '@editorjs/model'; /** * Class to work with Block tools constructables @@ -187,6 +187,29 @@ export class BlockToolFacade extends BaseToolFacade { return result; } + /** + * Returns block data serialized to a plain-text string using the tool's conversion config export function. + * If the export config is a function, it is called with the block data. + * If the export config is a string keypath, the value at that path is read from the data. + * @param data - serialized block data to convert to plain text + */ + public exportTextContent(data: BlockToolData): string { + const conversionConfig = this.options[BlockToolOptionKey.ConversionConfig]; + const exportFnOrProp = conversionConfig?.export; + + if (exportFnOrProp === undefined) { + throw new Error(`Tool ${this.name} does not have export configuration for text content`); + } + + if (typeof exportFnOrProp === 'function') { + return exportFnOrProp(data); + } + + const node = keypath.get(data, exportFnOrProp); + + return node?.value ?? ''; + } + /** * Returns enabled inline tools for Tool */ From 161783cb44295c5618db40c7a0d7052582f22338 Mon Sep 17 00:00:00 2001 From: Reversean Date: Mon, 1 Jun 2026 19:20:32 +0300 Subject: [PATCH 2/2] fix: review fixes --- packages/core/src/api/BlocksAPI.ts | 7 +- .../core/src/components/BlockManager.spec.ts | 261 +++++++++++------- packages/core/src/components/BlockManager.ts | 53 +++- packages/sdk/src/api/BlocksAPI.ts | 8 +- 4 files changed, 221 insertions(+), 108 deletions(-) diff --git a/packages/core/src/api/BlocksAPI.ts b/packages/core/src/api/BlocksAPI.ts index 0ad51329..8d09afce 100644 --- a/packages/core/src/api/BlocksAPI.ts +++ b/packages/core/src/api/BlocksAPI.ts @@ -213,16 +213,19 @@ export class BlocksAPI implements BlocksApiInterface { * Converts a block to a new type * @param params - conversion parameters * @param params.block - index or id of the block to convert + * @param params.key - data key of the text input at which to convert * @param params.newType - block tool name to convert to - * @param [params.dataOverrides] - optional data overrides for the new block + * @param [params.dataOverrides] - optional data overrides for the new block. Merged shallowly: + * object-valued keys are replaced wholesale, not deep-merged. * @param [params.userId] - user id to attribute the change to */ public convert({ block, + key, newType, dataOverrides, userId = this.#config.userId, }: Parameters[0]): void { - this.#blocksManager.convertBlock(block, newType, userId, dataOverrides); + this.#blocksManager.convertBlock(block as BlockIndexOrId, createDataKey(key), newType, userId, dataOverrides); } } diff --git a/packages/core/src/components/BlockManager.spec.ts b/packages/core/src/components/BlockManager.spec.ts index 7aefea52..6f7ebaaa 100644 --- a/packages/core/src/components/BlockManager.spec.ts +++ b/packages/core/src/components/BlockManager.spec.ts @@ -328,99 +328,6 @@ describe('BlocksManager (unit, mocked deps)', () => { }); }); - describe('.convertBlock()', () => { - beforeEach(() => { - model.resolveBlockIndex = jest.fn(() => 0); - model.getBlockSerialized = jest.fn(() => ({ - name: 'header', - id: 'b1', - data: { text: { value: 'Hello', - fragments: [] } }, - })); - }); - - it('exports text from source, imports into target, and replaces block at the same index', () => { - const sourceTool = { exportTextContent: jest.fn(() => 'Hello') }; - const targetTool = { importTextContent: jest.fn(() => ({ text: { value: 'Hello', - fragments: [] } })) }; - - // @ts-expect-error — mock - toolsManager.blockTools.get = jest.fn((name: string) => - name === 'header' ? sourceTool : targetTool - ); - - blocksManager.convertBlock('b1', 'paragraph'); - - expect(sourceTool.exportTextContent).toHaveBeenCalledWith({ text: { value: 'Hello', - fragments: [] } }); - expect(targetTool.importTextContent).toHaveBeenCalledWith('Hello', []); - expect(model.removeBlock).toHaveBeenCalledWith(USER_ID, 0); - expect(model.addBlock).toHaveBeenCalledWith( - USER_ID, - { name: 'paragraph', - data: { text: { value: 'Hello', - fragments: [] } } }, - 0 - ); - }); - - it('merges dataOverrides on top of the imported data', () => { - const sourceTool = { exportTextContent: jest.fn(() => 'Hello') }; - const targetTool = { importTextContent: jest.fn(() => ({ text: { value: 'Hello', - fragments: [] } })) }; - - // @ts-expect-error — mock - toolsManager.blockTools.get = jest.fn((name: string) => - name === 'header' ? sourceTool : targetTool - ); - - blocksManager.convertBlock('b1', 'paragraph', USER_ID, { level: 2 }); - - expect(model.addBlock).toHaveBeenCalledWith( - USER_ID, - { name: 'paragraph', - data: { text: { value: 'Hello', - fragments: [] }, - level: 2 } }, - 0 - ); - }); - - it('throws if source tool has no export config', () => { - const sourceTool = { - exportTextContent: jest.fn(() => { - throw new Error('Tool header does not have export configuration for text content'); - }), - }; - const targetTool = { importTextContent: jest.fn() }; - - // @ts-expect-error — mock - toolsManager.blockTools.get = jest.fn((name: string) => - name === 'header' ? sourceTool : targetTool - ); - - expect(() => blocksManager.convertBlock('b1', 'paragraph')) - .toThrow('does not have export configuration'); - }); - - it('throws if target tool has no import config', () => { - const sourceTool = { exportTextContent: jest.fn(() => 'Hello') }; - const targetTool = { - importTextContent: jest.fn(() => { - throw new Error('Tool paragraph does not have import configuration for text content'); - }), - }; - - // @ts-expect-error — mock - toolsManager.blockTools.get = jest.fn((name: string) => - name === 'header' ? sourceTool : targetTool - ); - - expect(() => blocksManager.convertBlock('b1', 'paragraph')) - .toThrow('does not have import configuration'); - }); - }); - describe('.splitBlock()', () => { /** * Restore split-specific mock implementations that jest.resetAllMocks() clears. @@ -773,4 +680,172 @@ describe('BlocksManager (unit, mocked deps)', () => { ); }); }); + + describe('.convertBlock()', () => { + beforeEach(() => { + model.resolveBlockIndex = jest.fn(() => 0); + model.getBlockSerialized = jest.fn(() => ({ + name: 'header', + id: 'b1', + data: { + text: { + value: 'Hello', + fragments: [] + } + } + })); + model.getBlockTextContent = jest.fn(() => ({ + text: { + value: 'Hello', + fragments: [] + } + })); + }); + + it('should export text from source, imports into target, and replaces block at the same index', () => { + const sourceTool = { exportTextContent: jest.fn(() => 'Hello') }; + const targetTool = { + importTextContent: jest.fn(() => ({ + text: { + value: 'Hello', + fragments: [] + } + })) + }; + + // @ts-expect-error — mock + toolsManager.blockTools.get = jest.fn((name: string) => + name === 'header' ? sourceTool : targetTool + ); + + blocksManager.convertBlock(0, 'text' as DataKey, 'paragraph'); + + expect(sourceTool.exportTextContent).toHaveBeenCalledWith({ + text: { + value: 'Hello', + fragments: [] + } + }); + expect(targetTool.importTextContent).toHaveBeenCalledWith('Hello', []); + expect(model.removeBlock).toHaveBeenCalledWith(USER_ID, 0); + expect(model.addBlock).toHaveBeenCalledWith( + USER_ID, + { + name: 'paragraph', + data: { + text: { + value: 'Hello', + fragments: [] + } + } + }, + 0 + ); + }); + + it('should merge dataOverrides on top of the imported data', () => { + const sourceTool = { exportTextContent: jest.fn(() => 'Hello') }; + const targetTool = { + importTextContent: jest.fn(() => ({ + text: { + value: 'Hello', + fragments: [] + } + })) + }; + + // @ts-expect-error — mock + toolsManager.blockTools.get = jest.fn((name: string) => + name === 'header' ? sourceTool : targetTool + ); + + blocksManager.convertBlock(0, 'text' as DataKey, 'paragraph', USER_ID, { level: 2 }); + + expect(model.addBlock).toHaveBeenCalledWith( + USER_ID, + { + name: 'paragraph', + data: { + text: { + value: 'Hello', + fragments: [] + }, + level: 2 + } + }, + 0 + ); + }); + + it('should throw if dataKey is not found in block text content', () => { + // @ts-expect-error — mock + toolsManager.blockTools.get = jest.fn(() => ({ + exportTextContent: jest.fn(() => 'Hello'), + importTextContent: jest.fn() + })); + + expect(() => blocksManager.convertBlock(0, 'nonexistent' as DataKey, 'paragraph')) + .toThrow('Data key "nonexistent" not found in block content'); + }); + + it('should pass fragments from getBlockTextContent to importTextContent', () => { + const fragments = [{ + type: 'bold', + range: [0, 5] + }]; + + model.getBlockTextContent = jest.fn(() => ({ + text: { + value: 'Hello', + fragments + } + })); + + const sourceTool = { exportTextContent: jest.fn(() => 'Hello') }; + const targetTool = { importTextContent: jest.fn(() => ({})) }; + + // @ts-expect-error — mock + toolsManager.blockTools.get = jest.fn((name: string) => + name === 'header' ? sourceTool : targetTool + ); + + blocksManager.convertBlock(0, 'text' as DataKey, 'paragraph'); + + expect(targetTool.importTextContent).toHaveBeenCalledWith('Hello', fragments); + }); + + it('should throw if source tool has no export config', () => { + const sourceTool = { + exportTextContent: jest.fn(() => { + throw new Error('Tool header does not have export configuration for text content'); + }) + }; + const targetTool = { importTextContent: jest.fn() }; + + // @ts-expect-error — mock + toolsManager.blockTools.get = jest.fn((name: string) => + name === 'header' ? sourceTool : targetTool + ); + + expect(() => blocksManager.convertBlock(0, 'text' as DataKey, 'paragraph')) + .toThrow('does not have export configuration'); + }); + + it('should throw if target tool has no import config', () => { + const sourceTool = { exportTextContent: jest.fn(() => 'Hello') }; + const targetTool = { + importTextContent: jest.fn(() => { + throw new Error('Tool paragraph does not have import configuration for text content'); + }) + }; + + // @ts-expect-error — mock + toolsManager.blockTools.get = jest.fn((name: string) => + name === 'header' ? sourceTool : targetTool + ); + + expect(() => blocksManager.convertBlock(0, 'text' as DataKey, 'paragraph')) + .toThrow('does not have import configuration'); + }); + }); }); diff --git a/packages/core/src/components/BlockManager.ts b/packages/core/src/components/BlockManager.ts index 9fdb51c7..df3e68db 100644 --- a/packages/core/src/components/BlockManager.ts +++ b/packages/core/src/components/BlockManager.ts @@ -373,29 +373,60 @@ export class BlocksManager { /** * Converts a block to a new type by exporting its text content and importing it into the new tool. * Both the source and target tools must define conversionConfig. - * @param blockId - id or index of the block to convert + * @param blockIndexOrId - numeric position or named identifier that locates the block + * @param dataKey - the data key at which the conversion is performed * @param newType - block tool name to convert to * @param [userId] - user id to attribute the change to - * @param [dataOverrides] - optional data fields to merge on top of the converted data + * @param [dataOverrides] - optional data fields to merge on top of the converted data. Merged shallowly: + * object-valued keys are replaced wholesale, not deep-merged. */ - public convertBlock(blockId: string | number, newType: string, userId: string | number = this.#config.userId, dataOverrides?: BlockToolData): void { - const blockIndex = this.#model.resolveBlockIndex(blockId as BlockId); + public convertBlock( + blockIndexOrId: number | BlockId, + dataKey: DataKey, + newType: string, + userId: string | number = this.#config.userId, + dataOverrides?: BlockToolData + ): void { + const blockIndex = this.#model.resolveBlockIndex(blockIndexOrId); const block = this.#model.getBlockSerialized(blockIndex); - const sourceTool = this.#toolsManager.blockTools.get(block.name)!; - const targetTool = this.#toolsManager.blockTools.get(newType)!; + const sourceTool = this.#toolsManager.blockTools.get(block.name); + const targetTool = this.#toolsManager.blockTools.get(newType); + + if (sourceTool === undefined) { + throw new Error(`Cannot convert block: source tool "${block.name}" is not registered`); + } + if (targetTool === undefined) { + throw new Error(`Cannot convert block: target tool "${newType}" is not registered`); + } const text = sourceTool.exportTextContent(block.data); - const newData = targetTool.importTextContent(text, []); + + const blockInputs = Object.entries( + this.#model.getBlockTextContent(blockIndex) + ); + const convertIndex = blockInputs.findIndex(([key]) => key === dataKey); + + if (convertIndex === -1) { + throw new Error(`Data key "${dataKey}" not found in block content`); + } + + const [, convertInput] = blockInputs[convertIndex]; + + const newData = targetTool.importTextContent(text, convertInput.fragments); const finalData = dataOverrides !== undefined - ? { ...newData, - ...dataOverrides } + ? { + ...newData, + ...dataOverrides, + } : newData; this.#model.removeBlock(userId, blockIndex); - this.#model.addBlock(userId, { name: newType, - data: finalData }, blockIndex); + this.#model.addBlock(userId, { + name: newType, + data: finalData, + }, blockIndex); } /** diff --git a/packages/sdk/src/api/BlocksAPI.ts b/packages/sdk/src/api/BlocksAPI.ts index 7422ec43..932125d3 100644 --- a/packages/sdk/src/api/BlocksAPI.ts +++ b/packages/sdk/src/api/BlocksAPI.ts @@ -250,8 +250,10 @@ export interface BlocksAPI { /** * Converts block to another type. Both blocks should provide the conversionConfig. * @param params.block - index or id of the block to convert. Should provide 'conversionConfig.export' method + * @param params.key - data key of the text input at which to split * @param params.newType - new block type. Should provide 'conversionConfig.import' method - * @param [params.dataOverrides] - optional data overrides for the new block + * @param [params.dataOverrides] - optional data overrides for the new block. Merged shallowly: + * object-valued keys are replaced wholesale, not deep-merged. * @param [params.userId] - user id to attribute the change to * @throws Error if conversion is not possible */ @@ -259,9 +261,11 @@ export interface BlocksAPI { convert(params: { /** Index or id of the block to convert */ block: number | string; + /** Data key of the text input to split */ + key: string; /** Block tool name to convert to */ newType: string; - /** Optional data overrides for the new block */ + /** Optional data overrides for the new block. Merged shallowly: object-valued keys are replaced wholesale, not deep-merged. */ dataOverrides?: BlockToolData; /** User id. Defaults to the current user id from the config */ userId?: string | number;