Skip to content
Merged
4 changes: 2 additions & 2 deletions .github/workflows/collaboration-manager.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- 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
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion packages/collaboration-manager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
85 changes: 64 additions & 21 deletions packages/collaboration-manager/src/CollaborationManager.spec.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
/* 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<CoreConfig> = { userId: 'user' };

describe('CollaborationManager', () => {
beforeAll(() => {
jest.useFakeTimers();
});

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');
Expand All @@ -30,7 +37,7 @@ 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])
Expand Down Expand Up @@ -70,7 +77,7 @@ describe('CollaborationManager', () => {
},
} ],
});
const collaborationManager = new CollaborationManager(model);
const collaborationManager = new CollaborationManager(config, model);
const index = new IndexBuilder().addBlockIndex(0)
.addDataKey(createDataKey('text'))
.addTextRange([
Expand Down Expand Up @@ -103,7 +110,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, {
Expand Down Expand Up @@ -150,7 +157,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, {
Expand Down Expand Up @@ -178,7 +185,7 @@ 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])
Expand Down Expand Up @@ -228,7 +235,7 @@ describe('CollaborationManager', () => {
},
} ],
});
const collaborationManager = new CollaborationManager(model);
const collaborationManager = new CollaborationManager(config, model);
const index = new IndexBuilder().addBlockIndex(0)
.addDataKey(createDataKey('text'))
.addTextRange([0, 3])
Expand Down Expand Up @@ -280,7 +287,7 @@ 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])
Expand Down Expand Up @@ -321,7 +328,7 @@ describe('CollaborationManager', () => {
},
} ],
});
const collaborationManager = new CollaborationManager(model);
const collaborationManager = new CollaborationManager(config, model);
const index = new IndexBuilder().addBlockIndex(0)
.addDataKey(createDataKey('text'))
.addTextRange([
Expand Down Expand Up @@ -363,7 +370,7 @@ 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])
Expand Down Expand Up @@ -405,7 +412,7 @@ 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])
Expand Down Expand Up @@ -440,7 +447,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, {
Expand Down Expand Up @@ -479,7 +486,7 @@ 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])
Expand Down Expand Up @@ -528,7 +535,7 @@ describe('CollaborationManager', () => {
},
} ],
});
const collaborationManager = new CollaborationManager(model);
const collaborationManager = new CollaborationManager(config, model);
const index = new IndexBuilder().addBlockIndex(0)
.addDataKey(createDataKey('text'))
.addTextRange([0, 3])
Expand Down Expand Up @@ -580,7 +587,7 @@ describe('CollaborationManager', () => {
},
} ],
});
const collaborationManager = new CollaborationManager(model);
const collaborationManager = new CollaborationManager(config, model);
const index = new IndexBuilder().addBlockIndex(0)
.addDataKey(createDataKey('text'))
.addTextRange([0, 3])
Expand Down Expand Up @@ -632,7 +639,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, {
Expand Down Expand Up @@ -667,7 +674,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, {
Expand Down Expand Up @@ -705,7 +712,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, {
Expand Down Expand Up @@ -747,7 +754,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, {
Expand Down Expand Up @@ -787,7 +794,7 @@ 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])
Expand Down Expand Up @@ -838,7 +845,7 @@ 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])
Expand Down Expand Up @@ -875,4 +882,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: {},
});
});
});
38 changes: 28 additions & 10 deletions packages/collaboration-manager/src/CollaborationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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));
Expand Down Expand Up @@ -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,
});
Expand All @@ -119,43 +130,45 @@ export class CollaborationManager {
if (!this.#shouldHandleEvents) {
return;
}

let operation: Operation | null = null;


/**
* @todo add all model events
*/
switch (true) {
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:
Expand All @@ -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();

Expand Down
Loading
Loading