Skip to content

Commit 82aed33

Browse files
authored
Block ID implementation (#133)
* Fix inputs and values attachments to be data-first * Load adapters as plugin * Fix tests * Review comments * (try to) fix test run for core package * Run base branch coverage manually * Fix unit-tests action: use ArtiomTr with pre-generated coverage files * Try just PR branch tests * Try clear before build * Try no cache * Use latest node * Review comments resolved * Resolve review comments * Add destoryBlockToolAdapter method to Adapter plugin * Block ID implementation * Fix lint * Fix tests * Fix build * Remove userId from getBlockSerialized method * Fix call in the block manager * Review comments * Cover uncovered lines
1 parent 7478236 commit 82aed33

30 files changed

Lines changed: 970 additions & 307 deletions

packages/collaboration-manager/src/CollaborationManager.spec.ts

Lines changed: 45 additions & 45 deletions
Large diffs are not rendered by default.

packages/core/src/api/BlocksAPI.integration.spec.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -271,8 +271,11 @@ describe('BlocksAPI integration (real model, mocked DOM adapters)', () => {
271271
blocksAPI.render({
272272
identifier: 'new-doc',
273273
blocks: [
274-
{ name: 'header',
275-
data: {} },
274+
{
275+
id: 'mock',
276+
name: 'header',
277+
data: {},
278+
},
276279
],
277280
properties: {},
278281
});
@@ -375,10 +378,16 @@ describe('BlocksAPI integration (real model, mocked DOM adapters)', () => {
375378
blocksAPI.render({
376379
identifier: 'doc-2',
377380
blocks: [
378-
{ name: 'header',
379-
data: {} },
380-
{ name: 'list',
381-
data: {} },
381+
{
382+
id: 'mock-header',
383+
name: 'header',
384+
data: {},
385+
},
386+
{
387+
id: 'mock-list',
388+
name: 'list',
389+
data: {},
390+
},
382391
],
383392
properties: {},
384393
});

packages/core/src/api/BlocksAPI.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { BlocksManager } from '../components/BlockManager.js';
55
import { BlockToolData } from '@editorjs/editorjs';
66
import { CoreConfigValidated } from '@editorjs/sdk';
77
import { BlocksAPI as BlocksApiInterface } from '@editorjs/sdk';
8-
import { type BlockNodeSerialized, EditorDocumentSerialized } from '@editorjs/model';
8+
import { type BlockNodeInit, type EditorDocumentSerialized } from '@editorjs/model';
99

1010
/**
1111
* Blocks API
@@ -80,7 +80,7 @@ export class BlocksAPI implements BlocksApiInterface {
8080
* @param blocks - array of blocks to insert
8181
* @param [index] - index to insert blocks at. If undefined, inserts at the end
8282
*/
83-
public insertMany(blocks: BlockNodeSerialized[], index?: number): void {
83+
public insertMany(blocks: BlockNodeInit[], index?: number): void {
8484
return this.#blocksManager.insertMany(blocks, index);
8585
}
8686

packages/core/src/components/BlockManager.spec.ts

Lines changed: 16 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ jest.unstable_mockModule('@editorjs/model', () => {
1919
initializeDocument: jest.fn(),
2020
clearBlocks: jest.fn(),
2121
getCaret: jest.fn(),
22+
getBlockSerialized: jest.fn(),
2223
get length() {
2324
return BLOCKS_COUNT;
2425
},
@@ -222,6 +223,7 @@ describe('BlocksManager (unit, mocked deps)', () => {
222223
identifier: 'doc',
223224
blocks: [
224225
{
226+
id: 'mock',
225227
name: 'x',
226228
data: {}
227229
}
@@ -255,20 +257,24 @@ describe('BlocksManager (unit, mocked deps)', () => {
255257

256258
expect(model.removeBlock).toHaveBeenCalledWith(USER_ID, 0);
257259
});
260+
261+
it('should call model.getCaret with the configured userId to resolve current block', () => {
262+
// @ts-expect-error - mock return value does not need full Caret shape
263+
model.getCaret = jest.fn(() => ({ index: { blockIndex: 2 } }));
264+
265+
blocksManager.deleteBlock();
266+
267+
expect(model.getCaret).toHaveBeenCalledWith(USER_ID);
268+
expect(model.removeBlock).toHaveBeenCalledWith(USER_ID, 2);
269+
});
258270
});
259271

260272
describe('.move()', () => {
261273
it('should call removeBlock and addBlock when moving current block forward', () => {
262-
// @ts-expect-error - need to assign read only property to mock it
263-
model.serialized = {
264-
blocks: [
265-
{ name: 'a' },
266-
{ name: 'b' },
267-
{ name: 'c' }
268-
]
269-
};
270274
// @ts-expect-error - mock return value does not need full Caret shape
271275
model.getCaret = jest.fn(() => ({ index: { blockIndex: 0 } }));
276+
// @ts-expect-error - mock return value does not need full BlockNodeSerialized shape
277+
model.getBlockSerialized = jest.fn(() => ({ name: 'a' }));
272278

273279
blocksManager.move(2);
274280

@@ -283,14 +289,8 @@ describe('BlocksManager (unit, mocked deps)', () => {
283289
});
284290

285291
it('should pass toIndex directly when toIndex is less than fromIndex', () => {
286-
// @ts-expect-error - need to assign read only property to mock it
287-
model.serialized = {
288-
blocks: [
289-
{ name: 'a' },
290-
{ name: 'b' },
291-
{ name: 'c' }
292-
]
293-
};
292+
// @ts-expect-error - mock return value does not need full BlockNodeSerialized shape
293+
model.getBlockSerialized = jest.fn(() => ({ name: 'c' }));
294294

295295
blocksManager.move(0, 2);
296296

@@ -299,15 +299,6 @@ describe('BlocksManager (unit, mocked deps)', () => {
299299
});
300300

301301
it('should do nothing when toIndex equals fromIndex', () => {
302-
// @ts-expect-error - need to assign read only property to mock it
303-
model.serialized = {
304-
blocks: [
305-
{ name: 'a' },
306-
{ name: 'b' },
307-
{ name: 'c' }
308-
]
309-
};
310-
311302
blocksManager.move(1, 1);
312303

313304
expect(model.removeBlock).not.toHaveBeenCalled();

packages/core/src/components/BlockManager.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {
2-
type BlockNodeSerialized,
2+
type BlockNodeInit,
33
type EditorDocumentSerialized,
44
EditorJSModel
55
} from '@editorjs/model';
@@ -17,7 +17,10 @@ import {
1717
* Parameters for the BlocksManager.insert() method
1818
*/
1919
interface InsertBlockParameters {
20-
// id?: string;
20+
/**
21+
* Block ID
22+
*/
23+
id?: string;
2124
/**
2225
* Block tool name to insert
2326
*/
@@ -111,7 +114,7 @@ export class BlocksManager {
111114
* @param parameters.replace - flag indicates if block at index should be replaced
112115
*/
113116
public insert({
114-
// id = undefined,
117+
id = undefined,
115118
type = this.#config.defaultBlock,
116119
data = {},
117120
index,
@@ -131,6 +134,7 @@ export class BlocksManager {
131134

132135
this.#model.addBlock(this.#config.userId, {
133136
...data,
137+
id,
134138
name: type,
135139
}, newIndex);
136140

@@ -146,7 +150,7 @@ export class BlocksManager {
146150
* @param blocks - array of blocks to insert
147151
* @param [index] - index to insert blocks at. If undefined, inserts at the end
148152
*/
149-
public insertMany(blocks: BlockNodeSerialized[], index: number = this.#model.length): void {
153+
public insertMany(blocks: BlockNodeInit[], index: number = this.#model.length): void {
150154
blocks.forEach((block, i) => this.#model.addBlock(this.#config.userId, block, index + i));
151155
}
152156

@@ -197,7 +201,7 @@ export class BlocksManager {
197201
return;
198202
}
199203

200-
const block = this.#model.serialized.blocks[fromIndex];
204+
const block = this.#model.getBlockSerialized(fromIndex);
201205

202206
this.#model.removeBlock(this.#config.userId, fromIndex);
203207
this.#model.addBlock(this.#config.userId, block, toIndex);

packages/core/src/components/BlockRenderer.spec.ts

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable jsdoc/require-jsdoc, @stylistic/comma-dangle,@typescript-eslint/naming-convention */
22
import { beforeEach, jest } from '@jest/globals';
33
import type { BlockToolFacade, EditorJSAdapterPlugin } from '@editorjs/sdk';
4-
import type { Index } from '@editorjs/model';
4+
import type { BlockId, Index } from '@editorjs/model';
55

66
const USER_ID = 'user';
77

@@ -45,6 +45,7 @@ jest.unstable_mockModule('@editorjs/model', () => {
4545
BlockAddedEvent,
4646
BlockRemovedEvent,
4747
EventType,
48+
createBlockId: (str: string) => str,
4849
};
4950
});
5051

@@ -114,8 +115,11 @@ describe('BlockRenderer (unit, mocked deps)', () => {
114115

115116
const event = new BlockAddedEvent(
116117
{ blockIndex: 0 } as Index,
117-
{ name: 'tool',
118-
data: {} },
118+
{
119+
name: 'tool',
120+
id: 'test-block-id' as BlockId,
121+
data: {}
122+
},
119123
USER_ID,
120124
);
121125

@@ -132,8 +136,11 @@ describe('BlockRenderer (unit, mocked deps)', () => {
132136
it('should throw when blockIndex is undefined', async () => {
133137
const event = new BlockAddedEvent(
134138
{} as Index,
135-
{ name: 'tool',
136-
data: {} },
139+
{
140+
name: 'tool',
141+
id: 'test-block-id' as BlockId,
142+
data: {}
143+
},
137144
USER_ID,
138145
);
139146

@@ -149,8 +156,11 @@ describe('BlockRenderer (unit, mocked deps)', () => {
149156

150157
const event = new BlockAddedEvent(
151158
{ blockIndex: 0 } as Index,
152-
{ name: 'missing-tool',
153-
data: {} },
159+
{
160+
name: 'missing-tool',
161+
id: 'test-block-id' as BlockId,
162+
data: {}
163+
},
154164
USER_ID,
155165
);
156166

@@ -174,8 +184,11 @@ describe('BlockRenderer (unit, mocked deps)', () => {
174184

175185
const event = new BlockAddedEvent(
176186
{ blockIndex: 0 } as Index,
177-
{ name: 'tool',
178-
data: {} },
187+
{
188+
name: 'tool',
189+
id: 'test-block-id' as BlockId,
190+
data: {}
191+
},
179192
USER_ID,
180193
);
181194

@@ -194,8 +207,11 @@ describe('BlockRenderer (unit, mocked deps)', () => {
194207
it('should dispatch BlockRemovedCoreEvent via EventBus', () => {
195208
const event = new BlockRemovedEvent(
196209
{ blockIndex: 1 } as Index,
197-
{ name: 'tool',
198-
data: {} },
210+
{
211+
name: 'tool',
212+
id: 'test-block-id' as BlockId,
213+
data: {}
214+
},
199215
USER_ID,
200216
);
201217

@@ -204,24 +220,31 @@ describe('BlockRenderer (unit, mocked deps)', () => {
204220
expect(eventBus.dispatchEvent).toHaveBeenCalled();
205221
});
206222

207-
it('should call destroyBlockToolAdapter with the block index', () => {
223+
it('should call destroyBlockToolAdapter with the block id', () => {
224+
const blockId = 'test-block-id' as unknown as BlockId;
208225
const event = new BlockRemovedEvent(
209-
{ blockIndex: 2 } as Index,
210-
{ name: 'tool',
211-
data: {} },
226+
{ blockIndex: 1 } as Index,
227+
{
228+
name: 'tool',
229+
id: blockId,
230+
data: {}
231+
},
212232
USER_ID,
213233
);
214234

215235
void changedListener(event);
216236

217-
expect(adapter.destroyBlockToolAdapter).toHaveBeenCalledWith(2);
237+
expect(adapter.destroyBlockToolAdapter).toHaveBeenCalledWith(blockId);
218238
});
219239

220240
it('should throw when blockIndex is undefined', () => {
221241
const event = new BlockRemovedEvent(
222242
{} as Index,
223-
{ name: 'tool',
224-
data: {} },
243+
{
244+
name: 'tool',
245+
id: 'test-block-id' as BlockId,
246+
data: {}
247+
},
225248
USER_ID,
226249
);
227250

packages/core/src/components/BlockRenderer.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
BlockAddedEvent,
33
BlockRemovedEvent,
4+
createBlockId,
45
EditorJSModel,
56
EventType,
67
ModelEvents
@@ -110,7 +111,7 @@ export class BlockRenderer {
110111
throw new Error(`[BlockRenderer] Block Tool ${data.name} not found`);
111112
}
112113

113-
const blockToolAdapter = this.#adapter.createBlockToolAdapter(index.blockIndex, tool.name);
114+
const blockToolAdapter = this.#adapter.createBlockToolAdapter(createBlockId(data.id), tool.name);
114115

115116
const block = tool.create({
116117
adapter: blockToolAdapter,
@@ -146,7 +147,7 @@ export class BlockRenderer {
146147
throw new Error('[BlockRenderer] Block index should be defined. Probably something wrong with the Editor Model. Please, report this issue');
147148
}
148149

149-
this.#adapter.destroyBlockToolAdapter(index.blockIndex);
150+
this.#adapter.destroyBlockToolAdapter(createBlockId(data.id));
150151

151152
this.#eventBus.dispatchEvent(new BlockRemovedCoreEvent({
152153
tool: data.name,

packages/core/src/components/SelectionManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
import { inject, injectable } from 'inversify';
1616
import { TOKENS } from '../tokens.js';
1717
import { InlineToolFormatData } from '@editorjs/sdk';
18-
import ToolsManager from '../tools/ToolsManager';
18+
import ToolsManager from '../tools/ToolsManager.js';
1919

2020
/**
2121
* SelectionManager responsible for handling selection changes and applying inline tools formatting

packages/core/src/utils/composeDataFromVersion2.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { OutputData } from '@editorjs/editorjs';
22
import type { InlineFragment } from '@editorjs/model';
3-
import { createInlineToolData, createInlineToolName, TextNode, ValueNode, type BlockNodeSerialized } from '@editorjs/model';
3+
import { createInlineToolData, createInlineToolName, TextNode, ValueNode, type BlockNodeInit } from '@editorjs/model';
44

55
/**
66
* Removes HTML tags from the input string
@@ -92,7 +92,7 @@ export function composeDataFromVersion2(data: OutputData): {
9292
/**
9393
* The child BlockNodes of the EditorDocument
9494
*/
95-
blocks: BlockNodeSerialized[];
95+
blocks: BlockNodeInit[];
9696
} {
9797
return {
9898
blocks: data.blocks.map((block) => {

0 commit comments

Comments
 (0)