Skip to content

Commit c41e2d2

Browse files
authored
Implement user id context (#99)
* Implement user id context * Add Context util tests * Add jsdoc * Add sdk dependency to relevant packages * Remove sdk from model * fix lint * Add ui reference to collboration package * Add more references * Add more references * fix * Use fake timers for collaboration manager tests * Fix workflow * kill mutants * fix typo * Fix workflow * reuse func
1 parent 2e064d8 commit c41e2d2

35 files changed

+400
-110
lines changed

.github/workflows/collaboration-manager.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
- name: Build the package
1515
uses: ./.github/actions/build
1616
with:
17-
package-name: '@editorjs/model'
17+
package-name: '@editorjs/collaboration-manager'
1818

1919
- name: Run ESLint check
2020
uses: ./.github/actions/lint
@@ -31,7 +31,7 @@ jobs:
3131
- name: Build the package
3232
uses: ./.github/actions/build
3333
with:
34-
package-name: '@editorjs/model'
34+
package-name: '@editorjs/collaboration-manager'
3535

3636
- name: Run unit tests
3737
uses: ./.github/actions/unit-tests

packages/collaboration-manager/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
"clear": "rm -rf ./dist && rm -rf ./tsconfig.build.tsbuildinfo"
1818
},
1919
"dependencies": {
20-
"@editorjs/model": "workspace:^"
20+
"@editorjs/model": "workspace:^",
21+
"@editorjs/sdk": "workspace:^"
2122
},
2223
"devDependencies": {
2324
"@jest/globals": "^29.7.0",

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

Lines changed: 64 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
/* eslint-disable @typescript-eslint/no-magic-numbers */
22
import { createDataKey, IndexBuilder } from '@editorjs/model';
33
import { EditorJSModel } from '@editorjs/model';
4+
import type { CoreConfig } from '@editorjs/sdk';
45
import { beforeAll, jest } from '@jest/globals';
56
import { CollaborationManager } from './CollaborationManager.js';
67
import { Operation, OperationType } from './Operation.js';
78

9+
const config: Partial<CoreConfig> = { userId: 'user' };
10+
811
describe('CollaborationManager', () => {
12+
beforeAll(() => {
13+
jest.useFakeTimers();
14+
});
15+
916
describe('applyOperation', () => {
1017
it('should throw an error on unknown operation type', () => {
1118
const model = new EditorJSModel();
1219

13-
const collaborationManager = new CollaborationManager(model);
20+
const collaborationManager = new CollaborationManager(config, model);
1421

1522
// @ts-expect-error - for test purposes
1623
expect(() => collaborationManager.applyOperation(new Operation('unknown', new IndexBuilder().build(), 'hello'))).toThrow('Unknown operation type');
@@ -30,7 +37,7 @@ describe('CollaborationManager', () => {
3037
},
3138
} ],
3239
});
33-
const collaborationManager = new CollaborationManager(model);
40+
const collaborationManager = new CollaborationManager(config, model);
3441
const index = new IndexBuilder().addBlockIndex(0)
3542
.addDataKey(createDataKey('text'))
3643
.addTextRange([0, 4])
@@ -70,7 +77,7 @@ describe('CollaborationManager', () => {
7077
},
7178
} ],
7279
});
73-
const collaborationManager = new CollaborationManager(model);
80+
const collaborationManager = new CollaborationManager(config, model);
7481
const index = new IndexBuilder().addBlockIndex(0)
7582
.addDataKey(createDataKey('text'))
7683
.addTextRange([
@@ -103,7 +110,7 @@ describe('CollaborationManager', () => {
103110
model.initializeDocument({
104111
blocks: [],
105112
});
106-
const collaborationManager = new CollaborationManager(model);
113+
const collaborationManager = new CollaborationManager(config, model);
107114
const index = new IndexBuilder().addBlockIndex(0)
108115
.build();
109116
const operation = new Operation(OperationType.Insert, index, {
@@ -150,7 +157,7 @@ describe('CollaborationManager', () => {
150157
model.initializeDocument({
151158
blocks: [ block ],
152159
});
153-
const collaborationManager = new CollaborationManager(model);
160+
const collaborationManager = new CollaborationManager(config, model);
154161
const index = new IndexBuilder().addBlockIndex(0)
155162
.build();
156163
const operation = new Operation(OperationType.Delete, index, {
@@ -178,7 +185,7 @@ describe('CollaborationManager', () => {
178185
},
179186
} ],
180187
});
181-
const collaborationManager = new CollaborationManager(model);
188+
const collaborationManager = new CollaborationManager(config, model);
182189
const index = new IndexBuilder().addBlockIndex(0)
183190
.addDataKey(createDataKey('text'))
184191
.addTextRange([0, 5])
@@ -228,7 +235,7 @@ describe('CollaborationManager', () => {
228235
},
229236
} ],
230237
});
231-
const collaborationManager = new CollaborationManager(model);
238+
const collaborationManager = new CollaborationManager(config, model);
232239
const index = new IndexBuilder().addBlockIndex(0)
233240
.addDataKey(createDataKey('text'))
234241
.addTextRange([0, 3])
@@ -280,7 +287,7 @@ describe('CollaborationManager', () => {
280287
},
281288
} ],
282289
});
283-
const collaborationManager = new CollaborationManager(model);
290+
const collaborationManager = new CollaborationManager(config, model);
284291
const index = new IndexBuilder().addBlockIndex(0)
285292
.addDataKey(createDataKey('text'))
286293
.addTextRange([0, 4])
@@ -321,7 +328,7 @@ describe('CollaborationManager', () => {
321328
},
322329
} ],
323330
});
324-
const collaborationManager = new CollaborationManager(model);
331+
const collaborationManager = new CollaborationManager(config, model);
325332
const index = new IndexBuilder().addBlockIndex(0)
326333
.addDataKey(createDataKey('text'))
327334
.addTextRange([
@@ -363,7 +370,7 @@ describe('CollaborationManager', () => {
363370
},
364371
} ],
365372
});
366-
const collaborationManager = new CollaborationManager(model);
373+
const collaborationManager = new CollaborationManager(config, model);
367374
const index = new IndexBuilder().addBlockIndex(0)
368375
.addDataKey(createDataKey('text'))
369376
.addTextRange([0, 4])
@@ -405,7 +412,7 @@ describe('CollaborationManager', () => {
405412
},
406413
} ],
407414
});
408-
const collaborationManager = new CollaborationManager(model);
415+
const collaborationManager = new CollaborationManager(config, model);
409416
const index = new IndexBuilder().addBlockIndex(0)
410417
.addDataKey(createDataKey('text'))
411418
.addTextRange([0, 4])
@@ -440,7 +447,7 @@ describe('CollaborationManager', () => {
440447
model.initializeDocument({
441448
blocks: [],
442449
});
443-
const collaborationManager = new CollaborationManager(model);
450+
const collaborationManager = new CollaborationManager(config, model);
444451
const index = new IndexBuilder().addBlockIndex(0)
445452
.build();
446453
const operation = new Operation(OperationType.Insert, index, {
@@ -479,7 +486,7 @@ describe('CollaborationManager', () => {
479486
},
480487
} ],
481488
});
482-
const collaborationManager = new CollaborationManager(model);
489+
const collaborationManager = new CollaborationManager(config, model);
483490
const index = new IndexBuilder().addBlockIndex(0)
484491
.addDataKey(createDataKey('text'))
485492
.addTextRange([0, 5])
@@ -528,7 +535,7 @@ describe('CollaborationManager', () => {
528535
},
529536
} ],
530537
});
531-
const collaborationManager = new CollaborationManager(model);
538+
const collaborationManager = new CollaborationManager(config, model);
532539
const index = new IndexBuilder().addBlockIndex(0)
533540
.addDataKey(createDataKey('text'))
534541
.addTextRange([0, 3])
@@ -580,7 +587,7 @@ describe('CollaborationManager', () => {
580587
},
581588
} ],
582589
});
583-
const collaborationManager = new CollaborationManager(model);
590+
const collaborationManager = new CollaborationManager(config, model);
584591
const index = new IndexBuilder().addBlockIndex(0)
585592
.addDataKey(createDataKey('text'))
586593
.addTextRange([0, 3])
@@ -632,7 +639,7 @@ describe('CollaborationManager', () => {
632639
model.initializeDocument({
633640
blocks: [ block ],
634641
});
635-
const collaborationManager = new CollaborationManager(model);
642+
const collaborationManager = new CollaborationManager(config, model);
636643
const index = new IndexBuilder().addBlockIndex(0)
637644
.build();
638645
const operation = new Operation(OperationType.Delete, index, {
@@ -667,7 +674,7 @@ describe('CollaborationManager', () => {
667674
model.initializeDocument({
668675
blocks: [ block ],
669676
});
670-
const collaborationManager = new CollaborationManager(model);
677+
const collaborationManager = new CollaborationManager(config, model);
671678
const index = new IndexBuilder().addBlockIndex(0)
672679
.build();
673680
const operation = new Operation(OperationType.Delete, index, {
@@ -705,7 +712,7 @@ describe('CollaborationManager', () => {
705712
model.initializeDocument({
706713
blocks: [ block ],
707714
});
708-
const collaborationManager = new CollaborationManager(model);
715+
const collaborationManager = new CollaborationManager(config, model);
709716
const index = new IndexBuilder().addBlockIndex(0)
710717
.build();
711718
const operation = new Operation(OperationType.Delete, index, {
@@ -747,7 +754,7 @@ describe('CollaborationManager', () => {
747754
model.initializeDocument({
748755
blocks: [ block ],
749756
});
750-
const collaborationManager = new CollaborationManager(model);
757+
const collaborationManager = new CollaborationManager(config, model);
751758
const index = new IndexBuilder().addBlockIndex(0)
752759
.build();
753760
const operation = new Operation(OperationType.Delete, index, {
@@ -787,7 +794,7 @@ describe('CollaborationManager', () => {
787794
},
788795
} ],
789796
});
790-
const collaborationManager = new CollaborationManager(model);
797+
const collaborationManager = new CollaborationManager(config, model);
791798
const index1 = new IndexBuilder().addBlockIndex(0)
792799
.addDataKey(createDataKey('text'))
793800
.addTextRange([0, 0])
@@ -838,7 +845,7 @@ describe('CollaborationManager', () => {
838845
},
839846
} ],
840847
});
841-
const collaborationManager = new CollaborationManager(model);
848+
const collaborationManager = new CollaborationManager(config, model);
842849
const index1 = new IndexBuilder().addBlockIndex(0)
843850
.addDataKey(createDataKey('text'))
844851
.addTextRange([0, 0])
@@ -875,4 +882,40 @@ describe('CollaborationManager', () => {
875882
properties: {},
876883
});
877884
});
885+
886+
it('should not undo operations from not a current user', () => {
887+
const model = new EditorJSModel();
888+
889+
model.initializeDocument({
890+
blocks: [ {
891+
name: 'paragraph',
892+
data: {
893+
text: {
894+
value: '',
895+
$t: 't',
896+
},
897+
},
898+
} ],
899+
});
900+
const collaborationManager = new CollaborationManager(config, model);
901+
902+
model.insertText('another-user', 0, createDataKey('text'), 'hello', 0);
903+
904+
collaborationManager.undo();
905+
906+
expect(model.serialized).toStrictEqual({
907+
blocks: [ {
908+
name: 'paragraph',
909+
tunes: {},
910+
data: {
911+
text: {
912+
$t: 't',
913+
value: 'hello',
914+
fragments: [],
915+
},
916+
},
917+
} ],
918+
properties: {},
919+
});
920+
});
878921
});

packages/collaboration-manager/src/CollaborationManager.ts

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
TextFormattedEvent, TextRemovedEvent,
99
TextUnformattedEvent
1010
} from '@editorjs/model';
11+
import type { CoreConfig } from '@editorjs/sdk';
1112
import { OperationsBatch } from './OperationsBatch.js';
1213
import { type ModifyOperationData, Operation, OperationType } from './Operation.js';
1314
import { UndoRedoManager } from './UndoRedoManager.js';
@@ -31,14 +32,24 @@ export class CollaborationManager {
3132
*/
3233
#shouldHandleEvents = true;
3334

35+
/**
36+
* Current operations batch
37+
*/
3438
#currentBatch: OperationsBatch | null = null;
3539

40+
/**
41+
* Editor's config
42+
*/
43+
#config: CoreConfig;
44+
3645
/**
3746
* Creates an instance of CollaborationManager
3847
*
48+
* @param config - Editor's config
3949
* @param model - EditorJSModel instance to listen to and apply operations
4050
*/
41-
constructor(model: EditorJSModel) {
51+
constructor(config: CoreConfig, model: EditorJSModel) {
52+
this.#config = config;
4253
this.#model = model;
4354
this.#undoRedoManager = new UndoRedoManager();
4455
model.addEventListener(EventType.Changed, this.#handleEvent.bind(this));
@@ -94,13 +105,13 @@ export class CollaborationManager {
94105
public applyOperation(operation: Operation): void {
95106
switch (operation.type) {
96107
case OperationType.Insert:
97-
this.#model.insertData(operation.index, operation.data.payload as string | BlockNodeSerialized[]);
108+
this.#model.insertData(this.#config.userId, operation.index, operation.data.payload as string | BlockNodeSerialized[]);
98109
break;
99110
case OperationType.Delete:
100-
this.#model.removeData(operation.index, operation.data.payload as string | BlockNodeSerialized[]);
111+
this.#model.removeData(this.#config.userId, operation.index, operation.data.payload as string | BlockNodeSerialized[]);
101112
break;
102113
case OperationType.Modify:
103-
this.#model.modifyData(operation.index, {
114+
this.#model.modifyData(this.#config.userId, operation.index, {
104115
value: operation.data.payload,
105116
previous: (operation.data as ModifyOperationData).prevPayload,
106117
});
@@ -119,43 +130,45 @@ export class CollaborationManager {
119130
if (!this.#shouldHandleEvents) {
120131
return;
121132
}
133+
122134
let operation: Operation | null = null;
123135

136+
124137
/**
125138
* @todo add all model events
126139
*/
127140
switch (true) {
128141
case (e instanceof TextAddedEvent):
129142
operation = new Operation(OperationType.Insert, e.detail.index, {
130143
payload: e.detail.data,
131-
});
144+
}, e.detail.userId);
132145
break;
133146
case (e instanceof TextRemovedEvent):
134147
operation = new Operation(OperationType.Delete, e.detail.index, {
135148
payload: e.detail.data,
136-
});
149+
}, e.detail.userId);
137150
break;
138151
case (e instanceof TextFormattedEvent):
139152
operation = new Operation(OperationType.Modify, e.detail.index, {
140153
payload: e.detail.data,
141154
prevPayload: null,
142-
});
155+
}, e.detail.userId);
143156
break;
144157
case (e instanceof TextUnformattedEvent):
145158
operation = new Operation(OperationType.Modify, e.detail.index, {
146159
prevPayload: e.detail.data,
147160
payload: null,
148-
});
161+
}, e.detail.userId);
149162
break;
150163
case (e instanceof BlockAddedEvent):
151164
operation = new Operation(OperationType.Insert, e.detail.index, {
152165
payload: [ e.detail.data ],
153-
});
166+
}, e.detail.userId);
154167
break;
155168
case (e instanceof BlockRemovedEvent):
156169
operation = new Operation(OperationType.Delete, e.detail.index, {
157170
payload: [ e.detail.data ],
158-
});
171+
}, e.detail.userId);
159172
break;
160173
// Stryker disable next-line ConditionalExpression
161174
default:
@@ -167,6 +180,11 @@ export class CollaborationManager {
167180
return;
168181
}
169182

183+
if (e.detail.userId !== this.#config.userId) {
184+
return;
185+
}
186+
187+
170188
const onBatchTermination = (batch: OperationsBatch, lastOp?: Operation): void => {
171189
const effectiveOp = batch.getEffectiveOperation();
172190

0 commit comments

Comments
 (0)