From 9e37ad1df90bb9b7bfaacf7e637e83a97380fdac Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Tue, 8 Apr 2025 20:44:57 +0200 Subject: [PATCH 01/16] Implement user id context --- .../src/CollaborationManager.ts | 26 +++++- packages/core/src/components/BlockManager.ts | 3 +- packages/core/src/index.ts | 11 ++- packages/core/src/utils/uid.ts | 7 ++ .../src/BlockToolAdapter/index.ts | 46 ++++++----- .../dom-adapters/src/CaretAdapter/index.ts | 9 ++- .../src/FormattingAdapter/index.ts | 12 ++- .../model/src/CaretManagement/Caret/types.ts | 5 ++ packages/model/src/EditorJSModel.ts | 79 ++++++++++++++----- .../model/src/EventBus/events/BaseEvent.ts | 9 ++- .../src/EventBus/events/BlockAddedEvent.ts | 5 +- .../src/EventBus/events/BlockRemovedEvent.ts | 5 +- .../events/CaretManagerCaretAddedEvent.ts | 8 +- .../events/CaretManagerCaretRemovedEvent.ts | 8 +- .../events/CaretManagerCaretUpdatedEvent.ts | 8 +- .../EventBus/events/PropertyModifiedEvent.ts | 5 +- .../src/EventBus/events/TextAddedEvent.ts | 5 +- .../src/EventBus/events/TextFormattedEvent.ts | 5 +- .../src/EventBus/events/TextRemovedEvent.ts | 5 +- .../EventBus/events/TextUnformattedEvent.ts | 5 +- .../src/EventBus/events/TuneModifiedEvent.ts | 5 +- .../src/EventBus/events/ValueModifiedEvent.ts | 5 +- packages/model/src/utils/Context.ts | 47 +++++++++++ packages/sdk/src/entities/Config.ts | 2 + 24 files changed, 249 insertions(+), 76 deletions(-) create mode 100644 packages/core/src/utils/uid.ts create mode 100644 packages/model/src/utils/Context.ts diff --git a/packages/collaboration-manager/src/CollaborationManager.ts b/packages/collaboration-manager/src/CollaborationManager.ts index 3ab79ead..184500b7 100644 --- a/packages/collaboration-manager/src/CollaborationManager.ts +++ b/packages/collaboration-manager/src/CollaborationManager.ts @@ -8,6 +8,7 @@ import { TextFormattedEvent, TextRemovedEvent, TextUnformattedEvent } from '@editorjs/model'; +import type { CoreConfig } from '@editorjs/sdk'; import { OperationsBatch } from './OperationsBatch.js'; import { type ModifyOperationData, Operation, OperationType } from './Operation.js'; import { UndoRedoManager } from './UndoRedoManager.js'; @@ -31,14 +32,24 @@ export class CollaborationManager { */ #shouldHandleEvents = true; + /** + * Current operations batch + */ #currentBatch: OperationsBatch | null = null; + /** + * Editor's config + */ + #config: CoreConfig; + /** * Creates an instance of CollaborationManager * + * @param config - Editor's config * @param model - EditorJSModel instance to listen to and apply operations */ - constructor(model: EditorJSModel) { + constructor(config: CoreConfig, model: EditorJSModel) { + this.#config = config; this.#model = model; this.#undoRedoManager = new UndoRedoManager(); model.addEventListener(EventType.Changed, this.#handleEvent.bind(this)); @@ -94,13 +105,13 @@ export class CollaborationManager { public applyOperation(operation: Operation): void { switch (operation.type) { case OperationType.Insert: - this.#model.insertData(operation.index, operation.data.payload as string | BlockNodeSerialized[]); + this.#model.insertData(this.#config.userId, operation.index, operation.data.payload as string | BlockNodeSerialized[]); break; case OperationType.Delete: - this.#model.removeData(operation.index, operation.data.payload as string | BlockNodeSerialized[]); + this.#model.removeData(this.#config.userId, operation.index, operation.data.payload as string | BlockNodeSerialized[]); break; case OperationType.Modify: - this.#model.modifyData(operation.index, { + this.#model.modifyData(this.#config.userId, operation.index, { value: operation.data.payload, previous: (operation.data as ModifyOperationData).prevPayload, }); @@ -119,8 +130,10 @@ export class CollaborationManager { if (!this.#shouldHandleEvents) { return; } + let operation: Operation | null = null; + /** * @todo add all model events */ @@ -167,6 +180,11 @@ export class CollaborationManager { return; } + if (e.detail.userId !== this.#config.userId) { + return; + } + + const onBatchTermination = (batch: OperationsBatch, lastOp?: Operation): void => { const effectiveOp = batch.getEffectiveOperation(); diff --git a/packages/core/src/components/BlockManager.ts b/packages/core/src/components/BlockManager.ts index 260972ac..316dfd71 100644 --- a/packages/core/src/components/BlockManager.ts +++ b/packages/core/src/components/BlockManager.ts @@ -122,7 +122,7 @@ export class BlocksManager { } // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - this.#model.addBlock({ + this.#model.addBlock(this.#config.userId, { ...data, name: type, }, index); @@ -162,6 +162,7 @@ export class BlocksManager { const toolName = event.detail.data.name; const blockToolAdapter = new BlockToolAdapter( + this.#config, this.#model, this.#caretAdapter, index.blockIndex, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ed2e5c61..e7ca6040 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -13,6 +13,7 @@ import { SelectionManager } from './components/SelectionManager.js'; import type { EditorjsPluginConstructor } from './entities/EditorjsPlugin.js'; import { EditorAPI } from './api/index.js'; import { UiComponentType } from './entities/Ui.js'; +import { generateId } from './utils/uid.js'; /** * If no holder is provided via config, the editor will be appended to the element with this id @@ -72,6 +73,10 @@ export default class Core { this.#config = config as CoreConfigValidated; + if (this.#config.userId === undefined) { + this.#config.userId = generateId(); + } + this.#iocContainer.set('EditorConfig', this.#config); this.#model = new EditorJSModel(); @@ -79,14 +84,14 @@ export default class Core { this.#toolsManager = this.#iocContainer.get(ToolsManager); - this.#caretAdapter = new CaretAdapter(this.#config.holder, this.#model); + this.#caretAdapter = new CaretAdapter(this.#config, this.#config.holder, this.#model); this.#iocContainer.set(CaretAdapter, this.#caretAdapter); - this.#collaborationManager = new CollaborationManager(this.#model); + this.#collaborationManager = new CollaborationManager(this.#config, this.#model); this.#iocContainer.set(CollaborationManager, this.#collaborationManager); - this.#formattingAdapter = new FormattingAdapter(this.#model, this.#caretAdapter); + this.#formattingAdapter = new FormattingAdapter(this.#config, this.#model, this.#caretAdapter); this.#iocContainer.set(FormattingAdapter, this.#formattingAdapter); this.#iocContainer.get(SelectionManager); diff --git a/packages/core/src/utils/uid.ts b/packages/core/src/utils/uid.ts new file mode 100644 index 00000000..a08bc371 --- /dev/null +++ b/packages/core/src/utils/uid.ts @@ -0,0 +1,7 @@ +/** + * Generates unique UUID + */ +export function generateId(): string { + // eslint-disable-next-line n/no-unsupported-features/node-builtins + return crypto.randomUUID(); +} diff --git a/packages/dom-adapters/src/BlockToolAdapter/index.ts b/packages/dom-adapters/src/BlockToolAdapter/index.ts index 79d7dd5d..995274aa 100644 --- a/packages/dom-adapters/src/BlockToolAdapter/index.ts +++ b/packages/dom-adapters/src/BlockToolAdapter/index.ts @@ -20,7 +20,7 @@ import { isNonTextInput } from '../utils/index.js'; import { InputType } from './types/InputType.js'; -import type { BlockToolAdapter as BlockToolAdapterInterface } from '@editorjs/sdk'; +import type { BlockToolAdapter as BlockToolAdapterInterface, CoreConfig } from '@editorjs/sdk'; import type { FormattingAdapter } from '../FormattingAdapter/index.js'; /** @@ -54,16 +54,20 @@ export class BlockToolAdapter implements BlockToolAdapterInterface { */ #toolName: string; + #config: CoreConfig; + /** * BlockToolAdapter constructor * + * @param config - Editor's config * @param model - EditorJSModel instance * @param caretAdapter - CaretAdapter instance * @param blockIndex - index of the block that this adapter is connected to * @param formattingAdapter - needed to render formatted text * @param toolName - tool name of the block */ - constructor(model: EditorJSModel, caretAdapter: CaretAdapter, blockIndex: number, formattingAdapter: FormattingAdapter, toolName: string) { + constructor(config: CoreConfig, model: EditorJSModel, caretAdapter: CaretAdapter, blockIndex: number, formattingAdapter: FormattingAdapter, toolName: string) { + this.#config = config; this.#model = model; this.#blockIndex = blockIndex; this.#caretAdapter = caretAdapter; @@ -134,7 +138,7 @@ export class BlockToolAdapter implements BlockToolAdapterInterface { * If selection is not collapsed, just remove selected text */ if (start !== end) { - this.#model.removeText(this.#blockIndex, key, start, end); + this.#model.removeText(this.#config.userId, this.#blockIndex, key, start, end); return; } @@ -196,7 +200,7 @@ export class BlockToolAdapter implements BlockToolAdapterInterface { */ } - this.#model.removeText(this.#blockIndex, key, start, end); + this.#model.removeText(this.#config.userId, this.#blockIndex, key, start, end); }; /** @@ -213,7 +217,7 @@ export class BlockToolAdapter implements BlockToolAdapterInterface { const start: number = getAbsoluteRangeOffset(input, range.startContainer, range.startOffset); const end: number = getAbsoluteRangeOffset(input, range.endContainer, range.endOffset); - this.#model.removeText(this.#blockIndex, key, start, end); + this.#model.removeText(this.#config.userId, this.#blockIndex, key, start, end); }; /** @@ -254,7 +258,7 @@ export class BlockToolAdapter implements BlockToolAdapterInterface { case InputType.InsertFromDrop: case InputType.InsertFromPaste: { if (start !== end) { - this.#model.removeText(this.#blockIndex, key, start, end); + this.#model.removeText(this.#config.userId, this.#blockIndex, key, start, end); } let data: string; @@ -271,7 +275,7 @@ export class BlockToolAdapter implements BlockToolAdapterInterface { data = event.dataTransfer!.getData('text/plain'); } - this.#model.insertText(this.#blockIndex, key, data, start); + this.#model.insertText(this.#config.userId, this.#blockIndex, key, data, start); break; } @@ -285,12 +289,12 @@ export class BlockToolAdapter implements BlockToolAdapterInterface { * it means that user selected some text and replaced it with new one */ if (start !== end) { - this.#model.removeText(this.#blockIndex, key, start, end); + this.#model.removeText(this.#config.userId, this.#blockIndex, key, start, end); } const data = event.data as string; - this.#model.insertText(this.#blockIndex, key, data, start); + this.#model.insertText(this.#config.userId, this.#blockIndex, key, data, start); break; } @@ -322,7 +326,7 @@ export class BlockToolAdapter implements BlockToolAdapterInterface { * @todo Think if we need to keep that or not */ if (isInputNative === true) { - this.#model.insertText(this.#blockIndex, key, '\n', start); + this.#model.insertText(this.#config.userId, this.#blockIndex, key, '\n', start); } break; default: @@ -342,17 +346,21 @@ export class BlockToolAdapter implements BlockToolAdapterInterface { const currentValue = this.#model.getText(this.#blockIndex, key); const newValueAfter = currentValue.slice(end); - this.#model.removeText(this.#blockIndex, key, start, currentValue.length); - this.#model.addBlock({ - name: this.#toolName, - data : { - [key]: { - $t: 't', - value: newValueAfter, - fragments: [], + this.#model.removeText(this.#config.userId, this.#blockIndex, key, start, currentValue.length); + this.#model.addBlock( + this.#config.userId, + { + name: this.#toolName, + data : { + [key]: { + $t: 't', + value: newValueAfter, + fragments: [], + }, }, }, - }, this.#blockIndex + 1); + this.#blockIndex + 1 + ); /** * Raf is needed to ensure that the new block is added so caret can be moved to it diff --git a/packages/dom-adapters/src/CaretAdapter/index.ts b/packages/dom-adapters/src/CaretAdapter/index.ts index 327694e6..fa1f0a2a 100644 --- a/packages/dom-adapters/src/CaretAdapter/index.ts +++ b/packages/dom-adapters/src/CaretAdapter/index.ts @@ -1,4 +1,5 @@ import type { Caret, EditorJSModel, CaretManagerEvents } from '@editorjs/model'; +import type { CoreConfig } from '@editorjs/sdk'; import { getAbsoluteRangeOffset, getBoundaryPointByAbsoluteOffset, useSelectionChange } from '../utils/index.js'; import type { TextRange } from '@editorjs/model'; import { EventType, IndexBuilder, Index } from '@editorjs/model'; @@ -38,17 +39,21 @@ export class CaretAdapter extends EventTarget { */ #userCaret: Caret; + #config: CoreConfig; + /** * @class + * @param config - Editor's config * @param container - Editor.js DOM container * @param model - Editor.js model */ - constructor(container: HTMLElement, model: EditorJSModel) { + constructor(config: CoreConfig, container: HTMLElement, model: EditorJSModel) { super(); + this.#config = config; this.#model = model; this.#container = container; - this.#userCaret = this.#model.createCaret(); + this.#userCaret = this.#model.createCaret(this.#config.userId); const { on } = useSelectionChange(); diff --git a/packages/dom-adapters/src/FormattingAdapter/index.ts b/packages/dom-adapters/src/FormattingAdapter/index.ts index b2e92985..fe7bbf4e 100644 --- a/packages/dom-adapters/src/FormattingAdapter/index.ts +++ b/packages/dom-adapters/src/FormattingAdapter/index.ts @@ -12,7 +12,7 @@ import { } from '@editorjs/model'; import type { CaretAdapter } from '../CaretAdapter/index.js'; import { FormattingAction } from '@editorjs/model'; -import type { InlineTool } from '@editorjs/sdk'; +import type { CoreConfig, InlineTool } from '@editorjs/sdk'; import { surround } from '../utils/surround.js'; /** @@ -35,12 +35,16 @@ export class FormattingAdapter { */ #caretAdapter: CaretAdapter; + #config: CoreConfig; + /** * @class + * @param config - Editor's config * @param model - editor model instance * @param caretAdapter - caret adapter instance */ - constructor(model: EditorJSModel, caretAdapter: CaretAdapter) { + constructor(config: CoreConfig, model: EditorJSModel, caretAdapter: CaretAdapter) { + this.#config = config; this.#model = model; this.#caretAdapter = caretAdapter; @@ -138,11 +142,11 @@ export class FormattingAdapter { switch (action) { case FormattingAction.Format: - this.#model.format(blockIndex, dataKey, toolName, ...range, data); + this.#model.format(this.#config.userId, blockIndex, dataKey, toolName, ...range, data); break; case FormattingAction.Unformat: - this.#model.unformat(blockIndex, dataKey, toolName, ...range); + this.#model.unformat(this.#config.userId, blockIndex, dataKey, toolName, ...range); break; } diff --git a/packages/model/src/CaretManagement/Caret/types.ts b/packages/model/src/CaretManagement/Caret/types.ts index d462fae4..cfa50776 100644 --- a/packages/model/src/CaretManagement/Caret/types.ts +++ b/packages/model/src/CaretManagement/Caret/types.ts @@ -14,6 +14,11 @@ export interface CaretSerialized { * Caret index */ readonly index: string | null; + + /** + * User identifier + */ + userId?: string | number } /** diff --git a/packages/model/src/EditorJSModel.ts b/packages/model/src/EditorJSModel.ts index c45b4e02..ee911d73 100644 --- a/packages/model/src/EditorJSModel.ts +++ b/packages/model/src/EditorJSModel.ts @@ -5,6 +5,7 @@ import { type BlockNodeSerialized, EditorDocument } from './entities/index.js'; import { EventBus, EventType } from './EventBus/index.js'; import type { ModelEvents, CaretManagerCaretUpdatedEvent, CaretManagerEvents } from './EventBus/index.js'; import { BaseDocumentEvent, type ModifiedEventData } from './EventBus/events/BaseEvent.js'; +import { getContext, WithContext } from './utils/Context.js'; import type { Constructor } from './utils/types.js'; import { CaretManager } from './CaretManagement/index.js'; @@ -79,8 +80,10 @@ export class EditorJSModel extends EventBus { this.#caretManager.addEventListener( EventType.CaretManagerUpdated, (event: CustomEvent) => { + const userId = getContext(); + this.dispatchEvent( - new (event.constructor as Constructor)(event.detail) + new (event.constructor as Constructor)(event.detail, userId) ); } ); @@ -101,20 +104,26 @@ export class EditorJSModel extends EventBus { /** * Creates a new Caret instance in the model * + * @param _userId - user identifier which is being set to the context * @param parameters - createCaret method parameters * @param [parameters.index] - initial caret index */ - public createCaret(...parameters: Parameters): ReturnType { + @WithContext + public createCaret(_userId?: string | number, ...parameters: Parameters): ReturnType { return this.#caretManager.createCaret(...parameters); } /** * Updates caret instance in the model * + * @param _userId - user identifier which is being set to the context * @param parameters - updateCaret method parameters * @param parameters.caret - Caret instance to update */ - public updateCaret(...parameters: Parameters): ReturnType { + @WithContext + public updateCaret(_userId?: string | number, ...parameters: Parameters): ReturnType { + console.trace(); + return this.#caretManager.updateCaret(...parameters); } @@ -122,10 +131,12 @@ export class EditorJSModel extends EventBus { /** * Removes caret instance from the model * + * @param _userId - user identifier which is being set to the context * @param parameters - removeCaret method parameters * @param parameters.caret - Caret instance to remove */ - public removeCaret(...parameters: Parameters): ReturnType { + @WithContext + public removeCaret(_userId?: string | number, ...parameters: Parameters): ReturnType { return this.#caretManager.removeCaret(...parameters); } @@ -144,11 +155,13 @@ export class EditorJSModel extends EventBus { * Updates a property of the EditorDocument. * Adds the property if it does not exist. * + * @param _userId - user identifier which is being set to the context * @param parameters - setProperty method parameters * @param parameters.name - The name of the property to update * @param parameters.value - The value to update the property with */ - public setProperty(...parameters: Parameters): ReturnType { + @WithContext + public setProperty(_userId?: string | number, ...parameters: Parameters): ReturnType { return this.#document.setProperty(...parameters); } @@ -156,24 +169,28 @@ export class EditorJSModel extends EventBus { * Adds a BlockNode to the EditorDocument at the specified index. * If no index is provided, the BlockNode will be added to the end of the array. * + * @param _userId - user identifier which is being set to the context * @param parameters - addBlock method parameters * @param parameters.blockNodeData - The data to create the BlockNode with * @param parameters.index - The index at which to add the BlockNode * @throws Error if the index is out of bounds */ - public addBlock(...parameters: Parameters): ReturnType { + @WithContext + public addBlock(_userId?: string | number, ...parameters: Parameters): ReturnType { return this.#document.addBlock(...parameters); } /** * Moves a BlockNode from one index to another * + * @param _userId - user identifier which is being set to the context * @param parameters = moveBlock method parameters * @param parameters.from - The index of the BlockNode to move * @param parameters.to - The index to move the BlockNode to * @throws Error if the index is out of bounds */ - public moveBlock(...parameters: Parameters): ReturnType { + @WithContext + public moveBlock(_userId?: string | number, ...parameters: Parameters): ReturnType { return this.#document.moveBlock(...parameters); } @@ -181,67 +198,79 @@ export class EditorJSModel extends EventBus { /** * Removes a BlockNode from the EditorDocument at the specified index. * + * @param _userId - user identifier which is being set to the context * @param parameters - removeBlock method parameters * @param parameters.index - The index of the BlockNode to remove * @throws Error if the index is out of bounds */ - public removeBlock(...parameters: Parameters): ReturnType { + @WithContext + public removeBlock(_userId?: string | number, ...parameters: Parameters): ReturnType { return this.#document.removeBlock(...parameters); } /** * Inserts data to the specified index * + * @param _userId - user identifier which is being set to the context * @param index - index to insert data * @param data - data to insert (text or blocks) */ - public insertData(index: Index, data: string | BlockNodeSerialized[]): void { + @WithContext + public insertData(_userId: string | number | undefined, index: Index, data: string | BlockNodeSerialized[]): void { this.#document.insertData(index, data); } /** * Removes data from the specified index * + * @param _userId - user identifier which is being set to the context * @param index - index to remove data from * @param data - text or blocks to remove */ - public removeData(index: Index, data: string | BlockNodeSerialized[]): void { + @WithContext + public removeData(_userId: string | number | undefined, index: Index, data: string | BlockNodeSerialized[]): void { this.#document.removeData(index, data); } /** * Modifies data for the specific index * + * @param _userId - user identifier which is being set to the context * @param index - index of data to modify * @param data - data to modify (includes current and previous values) */ - public modifyData(index: Index, data: ModifiedEventData): void { + @WithContext + public modifyData(_userId: string | number | undefined, index: Index, data: ModifiedEventData): void { this.#document.modifyData(index, data); } /** * Updates the ValueNode data associated with the BlockNode at the specified index. * + * @param _userId - user identifier which is being set to the context * @param parameters - updateValue method parameters * @param parameters.blockIndex - The index of the BlockNode to update * @param parameters.dataKey - The key of the ValueNode to update * @param parameters.value - The new value of the ValueNode * @throws Error if the index is out of bounds */ - public updateValue(...parameters: Parameters): ReturnType { + @WithContext + public updateValue(_userId?: string | number, ...parameters: Parameters): ReturnType { return this.#document.updateValue(...parameters); } /** * Updates BlockTune data associated with the BlockNode at the specified index. * + * @param _userId - user identifier which is being set to the context * @param parameters - updateTuneData method parameters * @param parameters.blockIndex - The index of the BlockNode to update * @param parameters.tuneName - The name of the BlockTune to update * @param parameters.data - The data to update the BlockTune with * @throws Error if the index is out of bounds */ - public updateTuneData(...parameters: Parameters): ReturnType { + @WithContext + public updateTuneData(_userId?: string | number, ...parameters: Parameters): ReturnType { return this.#document.updateTuneData(...parameters); } @@ -259,32 +288,37 @@ export class EditorJSModel extends EventBus { /** * Inserts text to the specified block * + * @param _userId - user identifier which is being set to the context * @param parameters - insertText method parameters * @param parameters.blockIndex - index of the block * @param parameters.dataKey - key of the data * @param parameters.text - text to insert * @param [parameters.start] - char index where to insert text */ - public insertText(...parameters: Parameters): ReturnType { + @WithContext + public insertText(_userId?: string | number, ...parameters: Parameters): ReturnType { return this.#document.insertText(...parameters); } /** * Removes text from specified block * + * @param _userId - user identifier which is being set to the context * @param parameters - removeText method parameters * @param parameters.blockIndex - index of the block * @param parameters.dataKey - key of the data * @param [parameters.start] - start char index of the range * @param [parameters.end] - end char index of the range */ - public removeText(...parameters: Parameters): ReturnType { + @WithContext + public removeText(_userId?: string | number, ...parameters: Parameters): ReturnType { return this.#document.removeText(...parameters); } /** * Formats text in the specified block * + * @param _userId - user identifier which is being set to the context * @param parameters - format method parameters * @param parameters.blockIndex - index of the block * @param parameters.dataKey - key of the data @@ -293,13 +327,15 @@ export class EditorJSModel extends EventBus { * @param parameters.end - end char index of the range * @param [parameters.data] - Inline Tool data if applicable */ - public format(...parameters: Parameters): ReturnType { + @WithContext + public format(_userId?: string | number, ...parameters: Parameters): ReturnType { return this.#document.format(...parameters); } /** * Removes formatting from the specified block * + * @param _userId - user identifier which is being set to the context * @param parameters - unformat method parameters * @param parameters.blockIndex - index of the block * @param parameters.key - key of the data @@ -307,7 +343,8 @@ export class EditorJSModel extends EventBus { * @param parameters.start - start char index of the range * @param parameters.end - end char index of the range */ - public unformat(...parameters: Parameters): ReturnType { + @WithContext + public unformat(_userId?: string | number, ...parameters: Parameters): ReturnType { return this.#document.unformat(...parameters); } @@ -349,12 +386,18 @@ export class EditorJSModel extends EventBus { return; } + const userId = getContext(); + /** * Here could be any logic to filter EditorDocument events; */ this.dispatchEvent( - new (event.constructor as Constructor)(event.detail.index, event.detail.data) + new (event.constructor as Constructor)( + event.detail.index, + event.detail.data, + userId + ) ); } ); diff --git a/packages/model/src/EventBus/events/BaseEvent.ts b/packages/model/src/EventBus/events/BaseEvent.ts index 989faf87..52fbeaf8 100644 --- a/packages/model/src/EventBus/events/BaseEvent.ts +++ b/packages/model/src/EventBus/events/BaseEvent.ts @@ -20,6 +20,11 @@ export interface EventPayloadBase { * The data of the changed information */ data: Data; + + /** + * User identifier + */ + userId?: number | string; } /** @@ -40,13 +45,15 @@ export class BaseDocumentEvent exten * @param index - index of the modified value in the document * @param action - event action * @param data - event data + * @param userId - user identifier */ - constructor(index: Index, action: Action, data: Data) { + constructor(index: Index, action: Action, data: Data, userId?: string | number) { super(EventType.Changed, { detail: { index, action, data, + userId, }, }); } diff --git a/packages/model/src/EventBus/events/BlockAddedEvent.ts b/packages/model/src/EventBus/events/BlockAddedEvent.ts index 446bd41b..716e22a2 100644 --- a/packages/model/src/EventBus/events/BlockAddedEvent.ts +++ b/packages/model/src/EventBus/events/BlockAddedEvent.ts @@ -13,9 +13,10 @@ export class BlockAddedEvent extends BaseDocumentEvent { * CaretManagerCaretAddedEvent class constructor * * @param payload - event payload + * @param userId - user identifier */ - constructor(payload: CaretSerialized) { + constructor(payload: CaretSerialized, userId?: string | number) { // Stryker disable next-line ObjectLiteral super(EventType.CaretManagerUpdated, { - detail: payload, + detail: { + ...payload, + userId, + }, }); } } diff --git a/packages/model/src/EventBus/events/CaretManagerCaretRemovedEvent.ts b/packages/model/src/EventBus/events/CaretManagerCaretRemovedEvent.ts index 9ee6ef17..b0596a0b 100644 --- a/packages/model/src/EventBus/events/CaretManagerCaretRemovedEvent.ts +++ b/packages/model/src/EventBus/events/CaretManagerCaretRemovedEvent.ts @@ -9,11 +9,15 @@ export class CaretManagerCaretRemovedEvent extends CustomEvent * CaretManagerCaretRemovedEvent class constructor * * @param payload - event payload + * @param userId - user identifier */ - constructor(payload: CaretSerialized) { + constructor(payload: CaretSerialized, userId?: string | number) { // Stryker disable next-line ObjectLiteral super(EventType.CaretManagerUpdated, { - detail: payload, + detail: { + ...payload, + userId, + }, }); } } diff --git a/packages/model/src/EventBus/events/CaretManagerCaretUpdatedEvent.ts b/packages/model/src/EventBus/events/CaretManagerCaretUpdatedEvent.ts index de3996ed..e2ebb942 100644 --- a/packages/model/src/EventBus/events/CaretManagerCaretUpdatedEvent.ts +++ b/packages/model/src/EventBus/events/CaretManagerCaretUpdatedEvent.ts @@ -9,11 +9,15 @@ export class CaretManagerCaretUpdatedEvent extends CustomEvent * CaretManagerCaretUpdatedEvent class constructor * * @param payload - event payload + * @param userId - user identifier */ - constructor(payload: CaretSerialized) { + constructor(payload: CaretSerialized, userId?: string | number) { // Stryker disable next-line ObjectLiteral super(EventType.CaretManagerUpdated, { - detail: payload, + detail: { + ...payload, + userId, + }, }); } } diff --git a/packages/model/src/EventBus/events/PropertyModifiedEvent.ts b/packages/model/src/EventBus/events/PropertyModifiedEvent.ts index bcd7a821..a520839c 100644 --- a/packages/model/src/EventBus/events/PropertyModifiedEvent.ts +++ b/packages/model/src/EventBus/events/PropertyModifiedEvent.ts @@ -12,8 +12,9 @@ export class PropertyModifiedEvent

extends BaseDocumentEvent) { - super(index, EventAction.Modified, data); + constructor(index: Index, data: ModifiedEventData

, userId?: string | number) { + super(index, EventAction.Modified, data, userId); } } diff --git a/packages/model/src/EventBus/events/TextAddedEvent.ts b/packages/model/src/EventBus/events/TextAddedEvent.ts index 295e3666..f3f4e1c7 100644 --- a/packages/model/src/EventBus/events/TextAddedEvent.ts +++ b/packages/model/src/EventBus/events/TextAddedEvent.ts @@ -12,8 +12,9 @@ export class TextAddedEvent extends BaseDocumentEvent * * @param index - index of the added text in the document * @param text - added text + * @param userId - user identifier */ - constructor(index: Index, text: string) { - super(index, EventAction.Added, text); + constructor(index: Index, text: string, userId?: string | number) { + super(index, EventAction.Added, text, userId); } } diff --git a/packages/model/src/EventBus/events/TextFormattedEvent.ts b/packages/model/src/EventBus/events/TextFormattedEvent.ts index 775bb406..4dd49180 100644 --- a/packages/model/src/EventBus/events/TextFormattedEvent.ts +++ b/packages/model/src/EventBus/events/TextFormattedEvent.ts @@ -17,8 +17,9 @@ export class TextFormattedEvent extends BaseDocumentEvent extends BaseDocumentEvent) { - super(index, EventAction.Modified, data); + constructor(index: Index, data: ModifiedEventData, userId?: string | number) { + super(index, EventAction.Modified, data, userId); } } diff --git a/packages/model/src/EventBus/events/ValueModifiedEvent.ts b/packages/model/src/EventBus/events/ValueModifiedEvent.ts index 9601fe82..8b3e91e5 100644 --- a/packages/model/src/EventBus/events/ValueModifiedEvent.ts +++ b/packages/model/src/EventBus/events/ValueModifiedEvent.ts @@ -12,8 +12,9 @@ export class ValueModifiedEvent extends BaseDocumentEvent) { - super(index, EventAction.Modified, data); + constructor(index: Index, data: ModifiedEventData, userId?: string | number) { + super(index, EventAction.Modified, data, userId); } } diff --git a/packages/model/src/utils/Context.ts b/packages/model/src/utils/Context.ts new file mode 100644 index 00000000..29923721 --- /dev/null +++ b/packages/model/src/utils/Context.ts @@ -0,0 +1,47 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const ContextStack: any[] = []; + +/** + * Puts context value to the context stack so function is able to retrieve it + * + * @param context - context value + * @param fn - function to call + */ +export function runWithContext(context: C, fn: () => T): T { + ContextStack.push(context); + + try { + return fn(); + } finally { + ContextStack.pop(); + } +} + +/** + * Returns current context value + */ +export function getContext(): C | undefined { + return ContextStack[ContextStack.length - 1]; +} + + +/** + * Decorator to run a class method inside a context + * + * @param _target - target class + * @param _propertyKey - method name + * @param descriptor - method descriptor + */ +export function WithContext(_target: unknown, _propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor { + const originalMethod = descriptor.value; + + descriptor.value = function (...args: unknown[]) { + // The first argument should be the context (context payload) + const context = args[0]; // context will be the first argument passed to the method + + // Execute the method within the provided context + return runWithContext(context, () => originalMethod.apply(this, args)); + }; + + return descriptor; +} diff --git a/packages/sdk/src/entities/Config.ts b/packages/sdk/src/entities/Config.ts index ff88890c..111d2463 100644 --- a/packages/sdk/src/entities/Config.ts +++ b/packages/sdk/src/entities/Config.ts @@ -17,4 +17,6 @@ export interface CoreConfig extends EditorConfig { * @param model - EditorJSModel instance */ onModelUpdate?: (model: EditorJSModel) => void; + + userId?: string | number; } From 3da144749edda9e65e4b3b24a4961684ce2b82e4 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Tue, 8 Apr 2025 21:22:49 +0200 Subject: [PATCH 02/16] Add Context util tests --- packages/collaboration-manager/package.json | 3 +- packages/model/src/utils/Context.spec.ts | 38 +++++++++++++++++++++ yarn.lock | 1 + 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 packages/model/src/utils/Context.spec.ts diff --git a/packages/collaboration-manager/package.json b/packages/collaboration-manager/package.json index 09e375d0..87ab2757 100644 --- a/packages/collaboration-manager/package.json +++ b/packages/collaboration-manager/package.json @@ -17,7 +17,8 @@ "clear": "rm -rf ./dist && rm -rf ./tsconfig.build.tsbuildinfo" }, "dependencies": { - "@editorjs/model": "workspace:^" + "@editorjs/model": "workspace:^", + "@editorjs/sdk": "workspace:^" }, "devDependencies": { "@jest/globals": "^29.7.0", diff --git a/packages/model/src/utils/Context.spec.ts b/packages/model/src/utils/Context.spec.ts new file mode 100644 index 00000000..b1e6fa9a --- /dev/null +++ b/packages/model/src/utils/Context.spec.ts @@ -0,0 +1,38 @@ +import { getContext, runWithContext, WithContext } from './Context.js'; + +describe('Context util', () => { + it('should run function in context', () => { + const func = (): string | undefined => { + return getContext(); + }; + + expect(runWithContext('context', func)).toEqual('context'); + }); + + it('should run several functions in context respectively', () => { + const func = (): string | undefined => { + return getContext(); + }; + + const result1 = runWithContext('context1', func); + const result2 = runWithContext('context2', func); + + expect(result1).toEqual('context1'); + expect(result2).toEqual('context2'); + }); + + it('should run method in context', () => { + // eslint-disable-next-line jsdoc/require-jsdoc + class Test { + @WithContext + // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars,jsdoc/require-jsdoc + public method(_context: string): string | undefined { + return getContext(); + } + } + + const instance = new Test(); + + expect(instance.method('context')).toEqual('context'); + }); +}); diff --git a/yarn.lock b/yarn.lock index b7ff2cd4..dd95ec5a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -645,6 +645,7 @@ __metadata: resolution: "@editorjs/collaboration-manager@workspace:packages/collaboration-manager" dependencies: "@editorjs/model": "workspace:^" + "@editorjs/sdk": "workspace:^" "@jest/globals": "npm:^29.7.0" "@stryker-mutator/core": "npm:^7.0.2" "@stryker-mutator/jest-runner": "npm:^7.0.2" From d5415341af958b82d9bffec2632b6f5bccc10f30 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Tue, 8 Apr 2025 21:53:39 +0200 Subject: [PATCH 03/16] Add jsdoc --- .github/workflows/collaboration-manager.yml | 7 +- .../src/CollaborationManager.spec.ts | 125 ++++++++++++------ .../src/CollaborationManager.ts | 12 +- .../collaboration-manager/src/Operation.ts | 9 +- .../src/BlockToolAdapter/index.ts | 3 + .../dom-adapters/src/CaretAdapter/index.ts | 3 + .../src/FormattingAdapter/index.ts | 5 +- .../src/EditorJSModel.integration.spec.ts | 2 +- packages/sdk/src/entities/Config.ts | 3 + 9 files changed, 116 insertions(+), 53 deletions(-) diff --git a/.github/workflows/collaboration-manager.yml b/.github/workflows/collaboration-manager.yml index 7fec28ae..8529e2e2 100644 --- a/.github/workflows/collaboration-manager.yml +++ b/.github/workflows/collaboration-manager.yml @@ -11,11 +11,16 @@ jobs: - run: yarn - - name: Build the package + - name: Build the model package uses: ./.github/actions/build with: package-name: '@editorjs/model' + - name: Build the sdk package + uses: ./.github/actions/build + with: + package-name: '@editorjs/sdk' + - name: Run ESLint check uses: ./.github/actions/lint with: diff --git a/packages/collaboration-manager/src/CollaborationManager.spec.ts b/packages/collaboration-manager/src/CollaborationManager.spec.ts index 25b8974b..79a64e73 100644 --- a/packages/collaboration-manager/src/CollaborationManager.spec.ts +++ b/packages/collaboration-manager/src/CollaborationManager.spec.ts @@ -1,16 +1,19 @@ /* eslint-disable @typescript-eslint/no-magic-numbers */ import { createDataKey, IndexBuilder } from '@editorjs/model'; import { EditorJSModel } from '@editorjs/model'; +import type { CoreConfig } from '@editorjs/sdk'; import { beforeAll, jest } from '@jest/globals'; import { CollaborationManager } from './CollaborationManager.js'; import { Operation, OperationType } from './Operation.js'; +const config: Partial = { userId: 'user' }; + describe('CollaborationManager', () => { describe('applyOperation', () => { it('should throw an error on unknown operation type', () => { const model = new EditorJSModel(); - const collaborationManager = new CollaborationManager(model); + const collaborationManager = new CollaborationManager(config, model); // @ts-expect-error - for test purposes expect(() => collaborationManager.applyOperation(new Operation('unknown', new IndexBuilder().build(), 'hello'))).toThrow('Unknown operation type'); @@ -30,10 +33,10 @@ describe('CollaborationManager', () => { }, } ], }); - const collaborationManager = new CollaborationManager(model); + const collaborationManager = new CollaborationManager(config, model); const index = new IndexBuilder().addBlockIndex(0) .addDataKey(createDataKey('text')) - .addTextRange([0, 4]) + .addTextRange([ 0, 4 ]) .build(); const operation = new Operation(OperationType.Insert, index, { payload: 'test', @@ -70,11 +73,11 @@ describe('CollaborationManager', () => { }, } ], }); - const collaborationManager = new CollaborationManager(model); + const collaborationManager = new CollaborationManager(config, model); const index = new IndexBuilder().addBlockIndex(0) .addDataKey(createDataKey('text')) .addTextRange([ - 3, 5]) + 3, 5 ]) .build(); const operation = new Operation(OperationType.Delete, index, { payload: '11', @@ -103,7 +106,7 @@ describe('CollaborationManager', () => { model.initializeDocument({ blocks: [], }); - const collaborationManager = new CollaborationManager(model); + const collaborationManager = new CollaborationManager(config, model); const index = new IndexBuilder().addBlockIndex(0) .build(); const operation = new Operation(OperationType.Insert, index, { @@ -150,7 +153,7 @@ describe('CollaborationManager', () => { model.initializeDocument({ blocks: [ block ], }); - const collaborationManager = new CollaborationManager(model); + const collaborationManager = new CollaborationManager(config, model); const index = new IndexBuilder().addBlockIndex(0) .build(); const operation = new Operation(OperationType.Delete, index, { @@ -178,10 +181,10 @@ describe('CollaborationManager', () => { }, } ], }); - const collaborationManager = new CollaborationManager(model); + const collaborationManager = new CollaborationManager(config, model); const index = new IndexBuilder().addBlockIndex(0) .addDataKey(createDataKey('text')) - .addTextRange([0, 5]) + .addTextRange([ 0, 5 ]) .build(); const operation = new Operation(OperationType.Modify, index, { payload: { @@ -201,7 +204,7 @@ describe('CollaborationManager', () => { value: 'Hello world', fragments: [ { tool: 'bold', - range: [0, 5], + range: [ 0, 5 ], } ], }, }, @@ -222,16 +225,16 @@ describe('CollaborationManager', () => { $t: 't', fragments: [ { tool: 'bold', - range: [0, 5], + range: [ 0, 5 ], } ], }, }, } ], }); - const collaborationManager = new CollaborationManager(model); + const collaborationManager = new CollaborationManager(config, model); const index = new IndexBuilder().addBlockIndex(0) .addDataKey(createDataKey('text')) - .addTextRange([0, 3]) + .addTextRange([ 0, 3 ]) .build(); const operation = new Operation(OperationType.Modify, index, { payload: null, @@ -251,7 +254,7 @@ describe('CollaborationManager', () => { value: 'Hello world', fragments: [ { tool: 'bold', - range: [3, 5], + range: [ 3, 5 ], } ], }, }, @@ -280,10 +283,10 @@ describe('CollaborationManager', () => { }, } ], }); - const collaborationManager = new CollaborationManager(model); + const collaborationManager = new CollaborationManager(config, model); const index = new IndexBuilder().addBlockIndex(0) .addDataKey(createDataKey('text')) - .addTextRange([0, 4]) + .addTextRange([ 0, 4 ]) .build(); const operation = new Operation(OperationType.Insert, index, { payload: 'test', @@ -321,11 +324,11 @@ describe('CollaborationManager', () => { }, } ], }); - const collaborationManager = new CollaborationManager(model); + const collaborationManager = new CollaborationManager(config, model); const index = new IndexBuilder().addBlockIndex(0) .addDataKey(createDataKey('text')) .addTextRange([ - 3, 5]) + 3, 5 ]) .build(); const operation = new Operation(OperationType.Delete, index, { payload: '11', @@ -363,10 +366,10 @@ describe('CollaborationManager', () => { }, } ], }); - const collaborationManager = new CollaborationManager(model); + const collaborationManager = new CollaborationManager(config, model); const index = new IndexBuilder().addBlockIndex(0) .addDataKey(createDataKey('text')) - .addTextRange([0, 4]) + .addTextRange([ 0, 4 ]) .build(); const operation = new Operation(OperationType.Insert, index, { payload: 'test', @@ -405,10 +408,10 @@ describe('CollaborationManager', () => { }, } ], }); - const collaborationManager = new CollaborationManager(model); + const collaborationManager = new CollaborationManager(config, model); const index = new IndexBuilder().addBlockIndex(0) .addDataKey(createDataKey('text')) - .addTextRange([0, 4]) + .addTextRange([ 0, 4 ]) .build(); const operation = new Operation(OperationType.Insert, index, { payload: 'test', @@ -440,7 +443,7 @@ describe('CollaborationManager', () => { model.initializeDocument({ blocks: [], }); - const collaborationManager = new CollaborationManager(model); + const collaborationManager = new CollaborationManager(config, model); const index = new IndexBuilder().addBlockIndex(0) .build(); const operation = new Operation(OperationType.Insert, index, { @@ -479,10 +482,10 @@ describe('CollaborationManager', () => { }, } ], }); - const collaborationManager = new CollaborationManager(model); + const collaborationManager = new CollaborationManager(config, model); const index = new IndexBuilder().addBlockIndex(0) .addDataKey(createDataKey('text')) - .addTextRange([0, 5]) + .addTextRange([ 0, 5 ]) .build(); const operation = new Operation(OperationType.Modify, index, { payload: { @@ -522,16 +525,16 @@ describe('CollaborationManager', () => { $t: 't', fragments: [ { tool: 'bold', - range: [0, 5], + range: [ 0, 5 ], } ], }, }, } ], }); - const collaborationManager = new CollaborationManager(model); + const collaborationManager = new CollaborationManager(config, model); const index = new IndexBuilder().addBlockIndex(0) .addDataKey(createDataKey('text')) - .addTextRange([0, 3]) + .addTextRange([ 0, 3 ]) .build(); const operation = new Operation(OperationType.Modify, index, { payload: null, @@ -553,7 +556,7 @@ describe('CollaborationManager', () => { value: 'Hello world', fragments: [ { tool: 'bold', - range: [0, 5], + range: [ 0, 5 ], } ], }, }, @@ -574,16 +577,16 @@ describe('CollaborationManager', () => { $t: 't', fragments: [ { tool: 'bold', - range: [0, 5], + range: [ 0, 5 ], } ], }, }, } ], }); - const collaborationManager = new CollaborationManager(model); + const collaborationManager = new CollaborationManager(config, model); const index = new IndexBuilder().addBlockIndex(0) .addDataKey(createDataKey('text')) - .addTextRange([0, 3]) + .addTextRange([ 0, 3 ]) .build(); const operation = new Operation(OperationType.Modify, index, { payload: null, @@ -606,7 +609,7 @@ describe('CollaborationManager', () => { value: 'Hello world', fragments: [ { tool: 'bold', - range: [3, 5], + range: [ 3, 5 ], } ], }, }, @@ -632,7 +635,7 @@ describe('CollaborationManager', () => { model.initializeDocument({ blocks: [ block ], }); - const collaborationManager = new CollaborationManager(model); + const collaborationManager = new CollaborationManager(config, model); const index = new IndexBuilder().addBlockIndex(0) .build(); const operation = new Operation(OperationType.Delete, index, { @@ -667,7 +670,7 @@ describe('CollaborationManager', () => { model.initializeDocument({ blocks: [ block ], }); - const collaborationManager = new CollaborationManager(model); + const collaborationManager = new CollaborationManager(config, model); const index = new IndexBuilder().addBlockIndex(0) .build(); const operation = new Operation(OperationType.Delete, index, { @@ -705,7 +708,7 @@ describe('CollaborationManager', () => { model.initializeDocument({ blocks: [ block ], }); - const collaborationManager = new CollaborationManager(model); + const collaborationManager = new CollaborationManager(config, model); const index = new IndexBuilder().addBlockIndex(0) .build(); const operation = new Operation(OperationType.Delete, index, { @@ -747,7 +750,7 @@ describe('CollaborationManager', () => { model.initializeDocument({ blocks: [ block ], }); - const collaborationManager = new CollaborationManager(model); + const collaborationManager = new CollaborationManager(config, model); const index = new IndexBuilder().addBlockIndex(0) .build(); const operation = new Operation(OperationType.Delete, index, { @@ -787,17 +790,17 @@ describe('CollaborationManager', () => { }, } ], }); - const collaborationManager = new CollaborationManager(model); + const collaborationManager = new CollaborationManager(config, model); const index1 = new IndexBuilder().addBlockIndex(0) .addDataKey(createDataKey('text')) - .addTextRange([0, 0]) + .addTextRange([ 0, 0 ]) .build(); const operation1 = new Operation(OperationType.Insert, index1, { payload: 'te', }); const index2 = new IndexBuilder().from(index1) - .addTextRange([1, 1]) + .addTextRange([ 1, 1 ]) .build(); const operation2 = new Operation(OperationType.Insert, index2, { payload: 'st', @@ -838,17 +841,17 @@ describe('CollaborationManager', () => { }, } ], }); - const collaborationManager = new CollaborationManager(model); + const collaborationManager = new CollaborationManager(config, model); const index1 = new IndexBuilder().addBlockIndex(0) .addDataKey(createDataKey('text')) - .addTextRange([0, 0]) + .addTextRange([ 0, 0 ]) .build(); const operation1 = new Operation(OperationType.Insert, index1, { payload: 'te', }); const index2 = new IndexBuilder().from(index1) - .addTextRange([1, 1]) + .addTextRange([ 1, 1 ]) .build(); const operation2 = new Operation(OperationType.Insert, index2, { payload: 'st', @@ -875,4 +878,40 @@ describe('CollaborationManager', () => { properties: {}, }); }); + + it('should not undo operations from not a current user', () => { + const model = new EditorJSModel(); + + model.initializeDocument({ + blocks: [ { + name: 'paragraph', + data: { + text: { + value: '', + $t: 't', + }, + }, + } ], + }); + const collaborationManager = new CollaborationManager(config, model); + + model.insertText('another-user', 0, createDataKey('text'), 'hello', 0); + + collaborationManager.undo(); + + expect(model.serialized).toStrictEqual({ + blocks: [ { + name: 'paragraph', + tunes: {}, + data: { + text: { + $t: 't', + value: 'hello', + fragments: [], + }, + }, + } ], + properties: {}, + }); + }) }); diff --git a/packages/collaboration-manager/src/CollaborationManager.ts b/packages/collaboration-manager/src/CollaborationManager.ts index 184500b7..6b7d9874 100644 --- a/packages/collaboration-manager/src/CollaborationManager.ts +++ b/packages/collaboration-manager/src/CollaborationManager.ts @@ -141,34 +141,34 @@ export class CollaborationManager { case (e instanceof TextAddedEvent): operation = new Operation(OperationType.Insert, e.detail.index, { payload: e.detail.data, - }); + }, e.detail.userId); break; case (e instanceof TextRemovedEvent): operation = new Operation(OperationType.Delete, e.detail.index, { payload: e.detail.data, - }); + }, e.detail.userId); break; case (e instanceof TextFormattedEvent): operation = new Operation(OperationType.Modify, e.detail.index, { payload: e.detail.data, prevPayload: null, - }); + }, e.detail.userId); break; case (e instanceof TextUnformattedEvent): operation = new Operation(OperationType.Modify, e.detail.index, { prevPayload: e.detail.data, payload: null, - }); + }, e.detail.userId); break; case (e instanceof BlockAddedEvent): operation = new Operation(OperationType.Insert, e.detail.index, { payload: [ e.detail.data ], - }); + }, e.detail.userId); break; case (e instanceof BlockRemovedEvent): operation = new Operation(OperationType.Delete, e.detail.index, { payload: [ e.detail.data ], - }); + }, e.detail.userId); break; // Stryker disable next-line ConditionalExpression default: diff --git a/packages/collaboration-manager/src/Operation.ts b/packages/collaboration-manager/src/Operation.ts index 8cb66731..69b12cf9 100644 --- a/packages/collaboration-manager/src/Operation.ts +++ b/packages/collaboration-manager/src/Operation.ts @@ -68,17 +68,24 @@ export class Operation { */ public data: OperationTypeToData; + /** + * Identifier of a user who created an operation; + */ + public userId?: string | number; + /** * Creates an instance of Operation * * @param type - operation type * @param index - index in the document model tree * @param data - operation data + * @param userId - user identifier */ - constructor(type: T, index: Index, data: OperationTypeToData) { + constructor(type: T, index: Index, data: OperationTypeToData, userId?: string | number) { this.type = type; this.index = index; this.data = data; + this.userId = userId; } /** diff --git a/packages/dom-adapters/src/BlockToolAdapter/index.ts b/packages/dom-adapters/src/BlockToolAdapter/index.ts index 995274aa..dc96b238 100644 --- a/packages/dom-adapters/src/BlockToolAdapter/index.ts +++ b/packages/dom-adapters/src/BlockToolAdapter/index.ts @@ -54,6 +54,9 @@ export class BlockToolAdapter implements BlockToolAdapterInterface { */ #toolName: string; + /** + * Editor's config + */ #config: CoreConfig; /** diff --git a/packages/dom-adapters/src/CaretAdapter/index.ts b/packages/dom-adapters/src/CaretAdapter/index.ts index fa1f0a2a..29fec389 100644 --- a/packages/dom-adapters/src/CaretAdapter/index.ts +++ b/packages/dom-adapters/src/CaretAdapter/index.ts @@ -39,6 +39,9 @@ export class CaretAdapter extends EventTarget { */ #userCaret: Caret; + /** + * Editor's config + */ #config: CoreConfig; /** diff --git a/packages/dom-adapters/src/FormattingAdapter/index.ts b/packages/dom-adapters/src/FormattingAdapter/index.ts index fe7bbf4e..8d194279 100644 --- a/packages/dom-adapters/src/FormattingAdapter/index.ts +++ b/packages/dom-adapters/src/FormattingAdapter/index.ts @@ -35,6 +35,9 @@ export class FormattingAdapter { */ #caretAdapter: CaretAdapter; + /** + * Editor's config + */ #config: CoreConfig; /** @@ -59,7 +62,7 @@ export class FormattingAdapter { * * @param input - input element to apply format to * @param inlineFragment - instance that contains index, toolName and toolData - * @param inlineFragment.index - text range inside of the input element + * @param inlineFragment.index - text range inside the input element * @param inlineFragment.toolName - name of the tool, which format to apply * @param inlineFragment.toolData - additional data for the tool */ diff --git a/packages/model/src/EditorJSModel.integration.spec.ts b/packages/model/src/EditorJSModel.integration.spec.ts index b8b94e96..69dfb75d 100644 --- a/packages/model/src/EditorJSModel.integration.spec.ts +++ b/packages/model/src/EditorJSModel.integration.spec.ts @@ -19,7 +19,7 @@ describe('[Integration tests] EditorJSModel', () => { model.addEventListener(EventType.Changed, handler); - model.addBlock({ + model.addBlock('user', { name: 'paragraph', data: { text: { diff --git a/packages/sdk/src/entities/Config.ts b/packages/sdk/src/entities/Config.ts index 111d2463..58413242 100644 --- a/packages/sdk/src/entities/Config.ts +++ b/packages/sdk/src/entities/Config.ts @@ -18,5 +18,8 @@ export interface CoreConfig extends EditorConfig { */ onModelUpdate?: (model: EditorJSModel) => void; + /** + * Current user's identifier + */ userId?: string | number; } From cb20e540a468957e9d5ff9e0cd83096dfe7fc3d9 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Tue, 8 Apr 2025 21:57:12 +0200 Subject: [PATCH 04/16] Add sdk dependency to relevant packages --- .github/workflows/collaboration-manager.yml | 5 ----- packages/model/package.json | 3 +++ yarn.lock | 1 + 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/collaboration-manager.yml b/.github/workflows/collaboration-manager.yml index 8529e2e2..6478d576 100644 --- a/.github/workflows/collaboration-manager.yml +++ b/.github/workflows/collaboration-manager.yml @@ -16,11 +16,6 @@ jobs: with: package-name: '@editorjs/model' - - name: Build the sdk package - uses: ./.github/actions/build - with: - package-name: '@editorjs/sdk' - - name: Run ESLint check uses: ./.github/actions/lint with: diff --git a/packages/model/package.json b/packages/model/package.json index 559ab89c..15b5e2c6 100644 --- a/packages/model/package.json +++ b/packages/model/package.json @@ -31,5 +31,8 @@ "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "typescript": "^5.5.4" + }, + "dependencies": { + "@editorjs/sdk": "workspace:^" } } diff --git a/yarn.lock b/yarn.lock index dd95ec5a..53c8be64 100644 --- a/yarn.lock +++ b/yarn.lock @@ -761,6 +761,7 @@ __metadata: version: 0.0.0-use.local resolution: "@editorjs/model@workspace:packages/model" dependencies: + "@editorjs/sdk": "workspace:^" "@jest/globals": "npm:^29.7.0" "@stryker-mutator/core": "npm:^7.0.2" "@stryker-mutator/jest-runner": "npm:^7.0.2" From d8b9b8a4d4feee7d5abbffd765a8bd52795d3b32 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Tue, 8 Apr 2025 21:59:28 +0200 Subject: [PATCH 05/16] Remove sdk from model --- packages/model/package.json | 3 --- packages/sdk/src/entities/InlineTool.ts | 2 +- yarn.lock | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/model/package.json b/packages/model/package.json index 15b5e2c6..559ab89c 100644 --- a/packages/model/package.json +++ b/packages/model/package.json @@ -31,8 +31,5 @@ "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "typescript": "^5.5.4" - }, - "dependencies": { - "@editorjs/sdk": "workspace:^" } } diff --git a/packages/sdk/src/entities/InlineTool.ts b/packages/sdk/src/entities/InlineTool.ts index 3cd6fede..5dfb1add 100644 --- a/packages/sdk/src/entities/InlineTool.ts +++ b/packages/sdk/src/entities/InlineTool.ts @@ -1,4 +1,4 @@ -import type { TextRange, InlineFragment, FormattingAction, IntersectType, InlineToolName } from '@editorjs/model'; +import type { TextRange, InlineFragment, FormattingAction, IntersectType } from '@editorjs/model'; import type { InlineTool as InlineToolVersion2 } from '@editorjs/editorjs'; import type { InlineToolConstructable as InlineToolConstructableV2 } from '@editorjs/editorjs'; import type { InlineToolConstructorOptions as InlineToolConstructorOptionsVersion2 } from '@editorjs/editorjs'; diff --git a/yarn.lock b/yarn.lock index 53c8be64..dd95ec5a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -761,7 +761,6 @@ __metadata: version: 0.0.0-use.local resolution: "@editorjs/model@workspace:packages/model" dependencies: - "@editorjs/sdk": "workspace:^" "@jest/globals": "npm:^29.7.0" "@stryker-mutator/core": "npm:^7.0.2" "@stryker-mutator/jest-runner": "npm:^7.0.2" From dec4dea9d6622eced9a7006611e7448019a007d0 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Tue, 8 Apr 2025 22:13:53 +0200 Subject: [PATCH 06/16] fix lint --- .../src/CollaborationManager.spec.ts | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/collaboration-manager/src/CollaborationManager.spec.ts b/packages/collaboration-manager/src/CollaborationManager.spec.ts index 79a64e73..953f4b68 100644 --- a/packages/collaboration-manager/src/CollaborationManager.spec.ts +++ b/packages/collaboration-manager/src/CollaborationManager.spec.ts @@ -36,7 +36,7 @@ describe('CollaborationManager', () => { const collaborationManager = new CollaborationManager(config, model); const index = new IndexBuilder().addBlockIndex(0) .addDataKey(createDataKey('text')) - .addTextRange([ 0, 4 ]) + .addTextRange([0, 4]) .build(); const operation = new Operation(OperationType.Insert, index, { payload: 'test', @@ -77,7 +77,7 @@ describe('CollaborationManager', () => { const index = new IndexBuilder().addBlockIndex(0) .addDataKey(createDataKey('text')) .addTextRange([ - 3, 5 ]) + 3, 5]) .build(); const operation = new Operation(OperationType.Delete, index, { payload: '11', @@ -184,7 +184,7 @@ describe('CollaborationManager', () => { const collaborationManager = new CollaborationManager(config, model); const index = new IndexBuilder().addBlockIndex(0) .addDataKey(createDataKey('text')) - .addTextRange([ 0, 5 ]) + .addTextRange([0, 5]) .build(); const operation = new Operation(OperationType.Modify, index, { payload: { @@ -204,7 +204,7 @@ describe('CollaborationManager', () => { value: 'Hello world', fragments: [ { tool: 'bold', - range: [ 0, 5 ], + range: [0, 5], } ], }, }, @@ -225,7 +225,7 @@ describe('CollaborationManager', () => { $t: 't', fragments: [ { tool: 'bold', - range: [ 0, 5 ], + range: [0, 5], } ], }, }, @@ -234,7 +234,7 @@ describe('CollaborationManager', () => { const collaborationManager = new CollaborationManager(config, model); const index = new IndexBuilder().addBlockIndex(0) .addDataKey(createDataKey('text')) - .addTextRange([ 0, 3 ]) + .addTextRange([0, 3]) .build(); const operation = new Operation(OperationType.Modify, index, { payload: null, @@ -254,7 +254,7 @@ describe('CollaborationManager', () => { value: 'Hello world', fragments: [ { tool: 'bold', - range: [ 3, 5 ], + range: [3, 5], } ], }, }, @@ -286,7 +286,7 @@ describe('CollaborationManager', () => { const collaborationManager = new CollaborationManager(config, model); const index = new IndexBuilder().addBlockIndex(0) .addDataKey(createDataKey('text')) - .addTextRange([ 0, 4 ]) + .addTextRange([0, 4]) .build(); const operation = new Operation(OperationType.Insert, index, { payload: 'test', @@ -328,7 +328,7 @@ describe('CollaborationManager', () => { const index = new IndexBuilder().addBlockIndex(0) .addDataKey(createDataKey('text')) .addTextRange([ - 3, 5 ]) + 3, 5]) .build(); const operation = new Operation(OperationType.Delete, index, { payload: '11', @@ -369,7 +369,7 @@ describe('CollaborationManager', () => { const collaborationManager = new CollaborationManager(config, model); const index = new IndexBuilder().addBlockIndex(0) .addDataKey(createDataKey('text')) - .addTextRange([ 0, 4 ]) + .addTextRange([0, 4]) .build(); const operation = new Operation(OperationType.Insert, index, { payload: 'test', @@ -411,7 +411,7 @@ describe('CollaborationManager', () => { const collaborationManager = new CollaborationManager(config, model); const index = new IndexBuilder().addBlockIndex(0) .addDataKey(createDataKey('text')) - .addTextRange([ 0, 4 ]) + .addTextRange([0, 4]) .build(); const operation = new Operation(OperationType.Insert, index, { payload: 'test', @@ -485,7 +485,7 @@ describe('CollaborationManager', () => { const collaborationManager = new CollaborationManager(config, model); const index = new IndexBuilder().addBlockIndex(0) .addDataKey(createDataKey('text')) - .addTextRange([ 0, 5 ]) + .addTextRange([0, 5]) .build(); const operation = new Operation(OperationType.Modify, index, { payload: { @@ -525,7 +525,7 @@ describe('CollaborationManager', () => { $t: 't', fragments: [ { tool: 'bold', - range: [ 0, 5 ], + range: [0, 5], } ], }, }, @@ -534,7 +534,7 @@ describe('CollaborationManager', () => { const collaborationManager = new CollaborationManager(config, model); const index = new IndexBuilder().addBlockIndex(0) .addDataKey(createDataKey('text')) - .addTextRange([ 0, 3 ]) + .addTextRange([0, 3]) .build(); const operation = new Operation(OperationType.Modify, index, { payload: null, @@ -556,7 +556,7 @@ describe('CollaborationManager', () => { value: 'Hello world', fragments: [ { tool: 'bold', - range: [ 0, 5 ], + range: [0, 5], } ], }, }, @@ -577,7 +577,7 @@ describe('CollaborationManager', () => { $t: 't', fragments: [ { tool: 'bold', - range: [ 0, 5 ], + range: [0, 5], } ], }, }, @@ -586,7 +586,7 @@ describe('CollaborationManager', () => { const collaborationManager = new CollaborationManager(config, model); const index = new IndexBuilder().addBlockIndex(0) .addDataKey(createDataKey('text')) - .addTextRange([ 0, 3 ]) + .addTextRange([0, 3]) .build(); const operation = new Operation(OperationType.Modify, index, { payload: null, @@ -609,7 +609,7 @@ describe('CollaborationManager', () => { value: 'Hello world', fragments: [ { tool: 'bold', - range: [ 3, 5 ], + range: [3, 5], } ], }, }, @@ -793,14 +793,14 @@ describe('CollaborationManager', () => { const collaborationManager = new CollaborationManager(config, model); const index1 = new IndexBuilder().addBlockIndex(0) .addDataKey(createDataKey('text')) - .addTextRange([ 0, 0 ]) + .addTextRange([0, 0]) .build(); const operation1 = new Operation(OperationType.Insert, index1, { payload: 'te', }); const index2 = new IndexBuilder().from(index1) - .addTextRange([ 1, 1 ]) + .addTextRange([1, 1]) .build(); const operation2 = new Operation(OperationType.Insert, index2, { payload: 'st', @@ -844,14 +844,14 @@ describe('CollaborationManager', () => { const collaborationManager = new CollaborationManager(config, model); const index1 = new IndexBuilder().addBlockIndex(0) .addDataKey(createDataKey('text')) - .addTextRange([ 0, 0 ]) + .addTextRange([0, 0]) .build(); const operation1 = new Operation(OperationType.Insert, index1, { payload: 'te', }); const index2 = new IndexBuilder().from(index1) - .addTextRange([ 1, 1 ]) + .addTextRange([1, 1]) .build(); const operation2 = new Operation(OperationType.Insert, index2, { payload: 'st', @@ -913,5 +913,5 @@ describe('CollaborationManager', () => { } ], properties: {}, }); - }) + }); }); From 0eee78adeaf43e3c5243c945e9e2cc5bcf01dfac Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Tue, 8 Apr 2025 22:16:11 +0200 Subject: [PATCH 07/16] Add ui reference to collboration package --- packages/collaboration-manager/tsconfig.build.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/collaboration-manager/tsconfig.build.json b/packages/collaboration-manager/tsconfig.build.json index d59c527a..2f3c7713 100644 --- a/packages/collaboration-manager/tsconfig.build.json +++ b/packages/collaboration-manager/tsconfig.build.json @@ -13,6 +13,9 @@ "references": [ { "path": "../model/tsconfig.build.json" + }, + { + "path": "../ui/tsconfig.build.json" } ] } From 5ba98ae6390ca81f4b2194c5802626699df14dfa Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Tue, 8 Apr 2025 22:21:03 +0200 Subject: [PATCH 08/16] Add more references --- packages/collaboration-manager/tsconfig.build.json | 3 +++ packages/playground/tsconfig.json | 3 +++ 2 files changed, 6 insertions(+) diff --git a/packages/collaboration-manager/tsconfig.build.json b/packages/collaboration-manager/tsconfig.build.json index 2f3c7713..72227cf5 100644 --- a/packages/collaboration-manager/tsconfig.build.json +++ b/packages/collaboration-manager/tsconfig.build.json @@ -16,6 +16,9 @@ }, { "path": "../ui/tsconfig.build.json" + }, + { + "path": "../sdk/tsconfig.build.json" } ] } diff --git a/packages/playground/tsconfig.json b/packages/playground/tsconfig.json index ec8071bb..31f9f624 100644 --- a/packages/playground/tsconfig.json +++ b/packages/playground/tsconfig.json @@ -34,6 +34,9 @@ }, { "path": "../core/tsconfig.json" + }, + { + "path": "../collaboration-manager/tsconfig.json", } ] } From 402277334c9ac6a1da9414f98b938ea938c9f370 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Tue, 8 Apr 2025 22:22:53 +0200 Subject: [PATCH 09/16] Add more references --- packages/core/tsconfig.build.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/core/tsconfig.build.json b/packages/core/tsconfig.build.json index 9bcd789e..2fc407f5 100644 --- a/packages/core/tsconfig.build.json +++ b/packages/core/tsconfig.build.json @@ -12,6 +12,9 @@ }, { "path": "../dom-adapters/tsconfig.build.json" + }, + { + "path": "../collaboration-manager/tsconfig.build.json" } ] } From aa1c5b7b0efebe82ffcdbb104841c62020842c44 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Tue, 8 Apr 2025 22:45:37 +0200 Subject: [PATCH 10/16] fix --- packages/collaboration-manager/tsconfig.build.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/collaboration-manager/tsconfig.build.json b/packages/collaboration-manager/tsconfig.build.json index 72227cf5..c53706de 100644 --- a/packages/collaboration-manager/tsconfig.build.json +++ b/packages/collaboration-manager/tsconfig.build.json @@ -14,9 +14,6 @@ { "path": "../model/tsconfig.build.json" }, - { - "path": "../ui/tsconfig.build.json" - }, { "path": "../sdk/tsconfig.build.json" } From 784acf7a20290528831cef9f787a501095246056 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Tue, 8 Apr 2025 22:51:30 +0200 Subject: [PATCH 11/16] Use fake timers for collaboration manager tests --- .../collaboration-manager/src/CollaborationManager.spec.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/collaboration-manager/src/CollaborationManager.spec.ts b/packages/collaboration-manager/src/CollaborationManager.spec.ts index 953f4b68..18ea9fb6 100644 --- a/packages/collaboration-manager/src/CollaborationManager.spec.ts +++ b/packages/collaboration-manager/src/CollaborationManager.spec.ts @@ -9,6 +9,10 @@ import { Operation, OperationType } from './Operation.js'; const config: Partial = { userId: 'user' }; describe('CollaborationManager', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + describe('applyOperation', () => { it('should throw an error on unknown operation type', () => { const model = new EditorJSModel(); From 8b50326eba92fcf51029491910c4493bbb820ceb Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Tue, 8 Apr 2025 22:55:01 +0200 Subject: [PATCH 12/16] Fix workflow --- .github/workflows/collaboration-manager.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/collaboration-manager.yml b/.github/workflows/collaboration-manager.yml index 6478d576..61189bc4 100644 --- a/.github/workflows/collaboration-manager.yml +++ b/.github/workflows/collaboration-manager.yml @@ -31,7 +31,7 @@ jobs: - name: Build the package uses: ./.github/actions/build with: - package-name: '@editorjs/model' + package-name: '@editorjs/collaboration-manager' - name: Run unit tests uses: ./.github/actions/unit-tests From b7475091f38bf593c0cdbabae51b0958554fbcb3 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Tue, 8 Apr 2025 23:00:33 +0200 Subject: [PATCH 13/16] kill mutants --- packages/model/src/utils/Context.spec.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/model/src/utils/Context.spec.ts b/packages/model/src/utils/Context.spec.ts index b1e6fa9a..cf11576d 100644 --- a/packages/model/src/utils/Context.spec.ts +++ b/packages/model/src/utils/Context.spec.ts @@ -35,4 +35,18 @@ describe('Context util', () => { expect(instance.method('context')).toEqual('context'); }); + + it('should return undefined as a context outside of run', () => { + expect(getContext()).toBeUndefined(); + }); + + it('should return udnefined as a context after a function call in the context', () => { + const func = (): string | undefined => { + return getContext(); + }; + + runWithContext('context', func); + + expect(getContext()).toBeUndefined(); + }); }); From efd27ce5b93396b85b9199fbd8f8f88c3e85f3db Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Tue, 8 Apr 2025 23:00:58 +0200 Subject: [PATCH 14/16] fix typo --- packages/model/src/utils/Context.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/model/src/utils/Context.spec.ts b/packages/model/src/utils/Context.spec.ts index cf11576d..c18558cd 100644 --- a/packages/model/src/utils/Context.spec.ts +++ b/packages/model/src/utils/Context.spec.ts @@ -40,7 +40,7 @@ describe('Context util', () => { expect(getContext()).toBeUndefined(); }); - it('should return udnefined as a context after a function call in the context', () => { + it('should return undefined as a context after a function call in the context', () => { const func = (): string | undefined => { return getContext(); }; From 8a19807b76b5280d00d3d02e3adff6d8a1540fb7 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Tue, 8 Apr 2025 23:03:01 +0200 Subject: [PATCH 15/16] Fix workflow --- .github/workflows/collaboration-manager.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/collaboration-manager.yml b/.github/workflows/collaboration-manager.yml index 61189bc4..82af2c4a 100644 --- a/.github/workflows/collaboration-manager.yml +++ b/.github/workflows/collaboration-manager.yml @@ -11,10 +11,10 @@ jobs: - run: yarn - - name: Build the model package + - name: Build the package uses: ./.github/actions/build with: - package-name: '@editorjs/model' + package-name: '@editorjs/collaboration-manager' - name: Run ESLint check uses: ./.github/actions/lint From 012130f1826606d4cafd83509bbb7b0c723732a8 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Tue, 8 Apr 2025 23:21:47 +0200 Subject: [PATCH 16/16] reuse func --- packages/model/src/utils/Context.spec.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/packages/model/src/utils/Context.spec.ts b/packages/model/src/utils/Context.spec.ts index c18558cd..454624da 100644 --- a/packages/model/src/utils/Context.spec.ts +++ b/packages/model/src/utils/Context.spec.ts @@ -1,19 +1,15 @@ import { getContext, runWithContext, WithContext } from './Context.js'; +const func = (): string | undefined => { + return getContext(); +}; + describe('Context util', () => { it('should run function in context', () => { - const func = (): string | undefined => { - return getContext(); - }; - expect(runWithContext('context', func)).toEqual('context'); }); it('should run several functions in context respectively', () => { - const func = (): string | undefined => { - return getContext(); - }; - const result1 = runWithContext('context1', func); const result2 = runWithContext('context2', func); @@ -41,10 +37,6 @@ describe('Context util', () => { }); it('should return undefined as a context after a function call in the context', () => { - const func = (): string | undefined => { - return getContext(); - }; - runWithContext('context', func); expect(getContext()).toBeUndefined();