@wordpress/core-data 7.38.0 → 7.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -0
- package/build/actions.cjs +17 -6
- package/build/actions.cjs.map +2 -2
- package/build/awareness/base-awareness.cjs +62 -0
- package/build/awareness/base-awareness.cjs.map +7 -0
- package/build/awareness/config.cjs +34 -0
- package/build/awareness/config.cjs.map +7 -0
- package/build/awareness/post-editor-awareness.cjs +130 -0
- package/build/awareness/post-editor-awareness.cjs.map +7 -0
- package/build/awareness/types.cjs +19 -0
- package/build/awareness/types.cjs.map +7 -0
- package/build/awareness/utils.cjs +116 -0
- package/build/awareness/utils.cjs.map +7 -0
- package/build/entities.cjs +27 -2
- package/build/entities.cjs.map +2 -2
- package/build/resolvers.cjs +43 -2
- package/build/resolvers.cjs.map +2 -2
- package/build/types.cjs.map +1 -1
- package/build/utils/block-selection-history.cjs +101 -0
- package/build/utils/block-selection-history.cjs.map +7 -0
- package/build/utils/crdt-selection.cjs +139 -0
- package/build/utils/crdt-selection.cjs.map +7 -0
- package/build/utils/crdt-user-selections.cjs +171 -0
- package/build/utils/crdt-user-selections.cjs.map +7 -0
- package/build/utils/crdt-utils.cjs +29 -0
- package/build/utils/crdt-utils.cjs.map +3 -3
- package/build/utils/crdt.cjs +16 -4
- package/build/utils/crdt.cjs.map +2 -2
- package/build-module/actions.mjs +17 -6
- package/build-module/actions.mjs.map +2 -2
- package/build-module/awareness/base-awareness.mjs +35 -0
- package/build-module/awareness/base-awareness.mjs.map +7 -0
- package/build-module/awareness/config.mjs +8 -0
- package/build-module/awareness/config.mjs.map +7 -0
- package/build-module/awareness/post-editor-awareness.mjs +111 -0
- package/build-module/awareness/post-editor-awareness.mjs.map +7 -0
- package/build-module/awareness/types.mjs +1 -0
- package/build-module/awareness/types.mjs.map +7 -0
- package/build-module/awareness/utils.mjs +90 -0
- package/build-module/awareness/utils.mjs.map +7 -0
- package/build-module/entities.mjs +28 -2
- package/build-module/entities.mjs.map +2 -2
- package/build-module/resolvers.mjs +43 -2
- package/build-module/resolvers.mjs.map +2 -2
- package/build-module/utils/block-selection-history.mjs +75 -0
- package/build-module/utils/block-selection-history.mjs.map +7 -0
- package/build-module/utils/crdt-selection.mjs +115 -0
- package/build-module/utils/crdt-selection.mjs.map +7 -0
- package/build-module/utils/crdt-user-selections.mjs +144 -0
- package/build-module/utils/crdt-user-selections.mjs.map +7 -0
- package/build-module/utils/crdt-utils.mjs +28 -0
- package/build-module/utils/crdt-utils.mjs.map +2 -2
- package/build-module/utils/crdt.mjs +18 -3
- package/build-module/utils/crdt.mjs.map +2 -2
- package/build-types/actions.d.ts.map +1 -1
- package/build-types/awareness/base-awareness.d.ts +19 -0
- package/build-types/awareness/base-awareness.d.ts.map +1 -0
- package/build-types/awareness/config.d.ts +9 -0
- package/build-types/awareness/config.d.ts.map +1 -0
- package/build-types/awareness/post-editor-awareness.d.ts +38 -0
- package/build-types/awareness/post-editor-awareness.d.ts.map +1 -0
- package/build-types/awareness/types.d.ts +32 -0
- package/build-types/awareness/types.d.ts.map +1 -0
- package/build-types/awareness/utils.d.ts +22 -0
- package/build-types/awareness/utils.d.ts.map +1 -0
- package/build-types/entities.d.ts.map +1 -1
- package/build-types/entity-types/test/attachment.test.d.ts +10 -0
- package/build-types/entity-types/test/attachment.test.d.ts.map +1 -0
- package/build-types/index.d.ts.map +1 -1
- package/build-types/resolvers.d.ts.map +1 -1
- package/build-types/types.d.ts +12 -2
- package/build-types/types.d.ts.map +1 -1
- package/build-types/utils/block-selection-history.d.ts +47 -0
- package/build-types/utils/block-selection-history.d.ts.map +1 -0
- package/build-types/utils/crdt-selection.d.ts +16 -0
- package/build-types/utils/crdt-selection.d.ts.map +1 -0
- package/build-types/utils/crdt-user-selections.d.ts +66 -0
- package/build-types/utils/crdt-user-selections.d.ts.map +1 -0
- package/build-types/utils/crdt-utils.d.ts +12 -0
- package/build-types/utils/crdt-utils.d.ts.map +1 -1
- package/build-types/utils/crdt.d.ts +8 -14
- package/build-types/utils/crdt.d.ts.map +1 -1
- package/build-types/utils/test/block-selection-history.test.d.ts +2 -0
- package/build-types/utils/test/block-selection-history.test.d.ts.map +1 -0
- package/build-types/utils/test/crdt-blocks.d.ts +2 -0
- package/build-types/utils/test/crdt-blocks.d.ts.map +1 -0
- package/build-types/utils/test/crdt.d.ts +2 -0
- package/build-types/utils/test/crdt.d.ts.map +1 -0
- package/package.json +21 -18
- package/src/actions.js +40 -7
- package/src/awareness/base-awareness.ts +50 -0
- package/src/awareness/config.ts +9 -0
- package/src/awareness/post-editor-awareness.ts +167 -0
- package/src/awareness/types.ts +38 -0
- package/src/awareness/utils.ts +159 -0
- package/src/entities.js +32 -2
- package/src/entity-types/test/attachment.test.ts +4 -4
- package/src/resolvers.js +53 -1
- package/src/test/actions.js +402 -0
- package/src/test/entity-provider.js +2 -0
- package/src/test/resolvers.js +4 -0
- package/src/types.ts +12 -3
- package/src/utils/block-selection-history.ts +176 -0
- package/src/utils/crdt-selection.ts +205 -0
- package/src/utils/crdt-user-selections.ts +336 -0
- package/src/utils/crdt-utils.ts +54 -0
- package/src/utils/crdt.ts +36 -3
- package/src/utils/test/block-selection-history.test.ts +764 -0
- package/src/utils/test/crdt-blocks.ts +11 -4
package/src/test/actions.js
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
receiveCurrentUser,
|
|
19
19
|
__experimentalBatch,
|
|
20
20
|
} from '../actions';
|
|
21
|
+
import { getSyncManager } from '../sync';
|
|
21
22
|
|
|
22
23
|
jest.mock( '../batch', () => {
|
|
23
24
|
const { createBatch } = jest.requireActual( '../batch' );
|
|
@@ -28,6 +29,11 @@ jest.mock( '../batch', () => {
|
|
|
28
29
|
};
|
|
29
30
|
} );
|
|
30
31
|
|
|
32
|
+
jest.mock( '../sync', () => ( {
|
|
33
|
+
getSyncManager: jest.fn(),
|
|
34
|
+
LOCAL_EDITOR_ORIGIN: 'local-editor',
|
|
35
|
+
} ) );
|
|
36
|
+
|
|
31
37
|
describe( 'editEntityRecord', () => {
|
|
32
38
|
it( 'throws when the edited entity does not have a loaded config.', async () => {
|
|
33
39
|
const entityConfig = {
|
|
@@ -50,6 +56,402 @@ describe( 'editEntityRecord', () => {
|
|
|
50
56
|
);
|
|
51
57
|
expect( select.getEntityConfig ).toHaveBeenCalledTimes( 1 );
|
|
52
58
|
} );
|
|
59
|
+
|
|
60
|
+
it( 'dispatches the correct action for non-merged edits', () => {
|
|
61
|
+
const dispatch = jest.fn();
|
|
62
|
+
const select = {
|
|
63
|
+
getEntityConfig: () => ( {
|
|
64
|
+
kind: 'postType',
|
|
65
|
+
name: 'post',
|
|
66
|
+
mergedEdits: {},
|
|
67
|
+
} ),
|
|
68
|
+
getRawEntityRecord: () => ( {
|
|
69
|
+
id: 1,
|
|
70
|
+
title: 'Original Title',
|
|
71
|
+
content: 'Original Content',
|
|
72
|
+
} ),
|
|
73
|
+
getEditedEntityRecord: () => ( {
|
|
74
|
+
id: 1,
|
|
75
|
+
title: 'Original Title',
|
|
76
|
+
content: 'Original Content',
|
|
77
|
+
} ),
|
|
78
|
+
getUndoManager: () => ( {
|
|
79
|
+
addRecord: jest.fn(),
|
|
80
|
+
} ),
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
editEntityRecord( 'postType', 'post', 1, { title: 'New Title' } )( {
|
|
84
|
+
select,
|
|
85
|
+
dispatch,
|
|
86
|
+
} );
|
|
87
|
+
|
|
88
|
+
expect( dispatch ).toHaveBeenCalledWith( {
|
|
89
|
+
type: 'EDIT_ENTITY_RECORD',
|
|
90
|
+
kind: 'postType',
|
|
91
|
+
name: 'post',
|
|
92
|
+
recordId: 1,
|
|
93
|
+
edits: { title: 'New Title' },
|
|
94
|
+
} );
|
|
95
|
+
} );
|
|
96
|
+
|
|
97
|
+
it( 'merges edits for fields defined in mergedEdits config', () => {
|
|
98
|
+
const dispatch = jest.fn();
|
|
99
|
+
const select = {
|
|
100
|
+
getEntityConfig: () => ( {
|
|
101
|
+
kind: 'postType',
|
|
102
|
+
name: 'post',
|
|
103
|
+
mergedEdits: { meta: true },
|
|
104
|
+
} ),
|
|
105
|
+
getRawEntityRecord: () => ( {
|
|
106
|
+
id: 1,
|
|
107
|
+
meta: { existingKey: 'existingValue' },
|
|
108
|
+
} ),
|
|
109
|
+
getEditedEntityRecord: () => ( {
|
|
110
|
+
id: 1,
|
|
111
|
+
meta: {
|
|
112
|
+
existingKey: 'existingValue',
|
|
113
|
+
editedKey: 'editedValue',
|
|
114
|
+
},
|
|
115
|
+
} ),
|
|
116
|
+
getUndoManager: () => ( {
|
|
117
|
+
addRecord: jest.fn(),
|
|
118
|
+
} ),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
editEntityRecord( 'postType', 'post', 1, {
|
|
122
|
+
meta: { newKey: 'newValue' },
|
|
123
|
+
} )( {
|
|
124
|
+
select,
|
|
125
|
+
dispatch,
|
|
126
|
+
} );
|
|
127
|
+
|
|
128
|
+
expect( dispatch ).toHaveBeenCalledWith( {
|
|
129
|
+
type: 'EDIT_ENTITY_RECORD',
|
|
130
|
+
kind: 'postType',
|
|
131
|
+
name: 'post',
|
|
132
|
+
recordId: 1,
|
|
133
|
+
edits: {
|
|
134
|
+
meta: {
|
|
135
|
+
existingKey: 'existingValue',
|
|
136
|
+
editedKey: 'editedValue',
|
|
137
|
+
newKey: 'newValue',
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
} );
|
|
141
|
+
} );
|
|
142
|
+
|
|
143
|
+
it( 'handles both merged and non-merged edits together', () => {
|
|
144
|
+
const dispatch = jest.fn();
|
|
145
|
+
const select = {
|
|
146
|
+
getEntityConfig: () => ( {
|
|
147
|
+
kind: 'postType',
|
|
148
|
+
name: 'post',
|
|
149
|
+
mergedEdits: { meta: true },
|
|
150
|
+
} ),
|
|
151
|
+
getRawEntityRecord: () => ( {
|
|
152
|
+
id: 1,
|
|
153
|
+
title: 'Original Title',
|
|
154
|
+
meta: { existingKey: 'existingValue' },
|
|
155
|
+
} ),
|
|
156
|
+
getEditedEntityRecord: () => ( {
|
|
157
|
+
id: 1,
|
|
158
|
+
title: 'Original Title',
|
|
159
|
+
meta: { existingKey: 'existingValue' },
|
|
160
|
+
} ),
|
|
161
|
+
getUndoManager: () => ( {
|
|
162
|
+
addRecord: jest.fn(),
|
|
163
|
+
} ),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
editEntityRecord( 'postType', 'post', 1, {
|
|
167
|
+
title: 'New Title',
|
|
168
|
+
meta: { newKey: 'newValue' },
|
|
169
|
+
} )( { select, dispatch } );
|
|
170
|
+
|
|
171
|
+
expect( dispatch ).toHaveBeenCalledWith( {
|
|
172
|
+
type: 'EDIT_ENTITY_RECORD',
|
|
173
|
+
kind: 'postType',
|
|
174
|
+
name: 'post',
|
|
175
|
+
recordId: 1,
|
|
176
|
+
edits: {
|
|
177
|
+
title: 'New Title',
|
|
178
|
+
meta: {
|
|
179
|
+
existingKey: 'existingValue',
|
|
180
|
+
newKey: 'newValue',
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
} );
|
|
184
|
+
} );
|
|
185
|
+
|
|
186
|
+
it( 'clears edit when merged value equals persisted record', () => {
|
|
187
|
+
const dispatch = jest.fn();
|
|
188
|
+
const select = {
|
|
189
|
+
getEntityConfig: () => ( {
|
|
190
|
+
kind: 'postType',
|
|
191
|
+
name: 'post',
|
|
192
|
+
mergedEdits: { meta: true },
|
|
193
|
+
} ),
|
|
194
|
+
getRawEntityRecord: () => ( {
|
|
195
|
+
id: 1,
|
|
196
|
+
meta: { key1: 'value1', key2: 'value2' },
|
|
197
|
+
} ),
|
|
198
|
+
getEditedEntityRecord: () => ( {
|
|
199
|
+
id: 1,
|
|
200
|
+
meta: { key1: 'value1' },
|
|
201
|
+
} ),
|
|
202
|
+
getUndoManager: () => ( {
|
|
203
|
+
addRecord: jest.fn(),
|
|
204
|
+
} ),
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Editing meta to add key2 back should result in a value equal to the persisted record
|
|
208
|
+
editEntityRecord( 'postType', 'post', 1, {
|
|
209
|
+
meta: { key2: 'value2' },
|
|
210
|
+
} )( { select, dispatch } );
|
|
211
|
+
|
|
212
|
+
expect( dispatch ).toHaveBeenCalledWith( {
|
|
213
|
+
type: 'EDIT_ENTITY_RECORD',
|
|
214
|
+
kind: 'postType',
|
|
215
|
+
name: 'post',
|
|
216
|
+
recordId: 1,
|
|
217
|
+
edits: {
|
|
218
|
+
// meta should be undefined because merged value equals persisted record
|
|
219
|
+
meta: undefined,
|
|
220
|
+
},
|
|
221
|
+
} );
|
|
222
|
+
} );
|
|
223
|
+
|
|
224
|
+
it( 'clears non-merged edit when value equals persisted record', () => {
|
|
225
|
+
const dispatch = jest.fn();
|
|
226
|
+
const select = {
|
|
227
|
+
getEntityConfig: () => ( {
|
|
228
|
+
kind: 'postType',
|
|
229
|
+
name: 'post',
|
|
230
|
+
mergedEdits: {},
|
|
231
|
+
} ),
|
|
232
|
+
getRawEntityRecord: () => ( {
|
|
233
|
+
id: 1,
|
|
234
|
+
title: 'Original Title',
|
|
235
|
+
} ),
|
|
236
|
+
getEditedEntityRecord: () => ( {
|
|
237
|
+
id: 1,
|
|
238
|
+
title: 'Edited Title',
|
|
239
|
+
} ),
|
|
240
|
+
getUndoManager: () => ( {
|
|
241
|
+
addRecord: jest.fn(),
|
|
242
|
+
} ),
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Editing title back to original should clear the edit
|
|
246
|
+
editEntityRecord( 'postType', 'post', 1, {
|
|
247
|
+
title: 'Original Title',
|
|
248
|
+
} )( { select, dispatch } );
|
|
249
|
+
|
|
250
|
+
expect( dispatch ).toHaveBeenCalledWith( {
|
|
251
|
+
type: 'EDIT_ENTITY_RECORD',
|
|
252
|
+
kind: 'postType',
|
|
253
|
+
name: 'post',
|
|
254
|
+
recordId: 1,
|
|
255
|
+
edits: {
|
|
256
|
+
title: undefined,
|
|
257
|
+
},
|
|
258
|
+
} );
|
|
259
|
+
} );
|
|
260
|
+
|
|
261
|
+
describe( 'with SyncManager', () => {
|
|
262
|
+
let syncManager;
|
|
263
|
+
|
|
264
|
+
beforeEach( () => {
|
|
265
|
+
// Create a mock sync manager
|
|
266
|
+
syncManager = {
|
|
267
|
+
update: jest.fn(),
|
|
268
|
+
};
|
|
269
|
+
getSyncManager.mockReturnValue( syncManager );
|
|
270
|
+
} );
|
|
271
|
+
|
|
272
|
+
afterEach( () => {
|
|
273
|
+
getSyncManager.mockReset();
|
|
274
|
+
} );
|
|
275
|
+
|
|
276
|
+
it( 'passes merged edits to SyncManager#update for merged fields', () => {
|
|
277
|
+
const dispatch = jest.fn();
|
|
278
|
+
const select = {
|
|
279
|
+
getEntityConfig: () => ( {
|
|
280
|
+
kind: 'postType',
|
|
281
|
+
name: 'post',
|
|
282
|
+
mergedEdits: { meta: true },
|
|
283
|
+
syncConfig: {},
|
|
284
|
+
} ),
|
|
285
|
+
getRawEntityRecord: () => ( {
|
|
286
|
+
id: 1,
|
|
287
|
+
meta: { existingKey: 'existingValue' },
|
|
288
|
+
} ),
|
|
289
|
+
getEditedEntityRecord: () => ( {
|
|
290
|
+
id: 1,
|
|
291
|
+
meta: {
|
|
292
|
+
existingKey: 'existingValue',
|
|
293
|
+
editedKey: 'editedValue',
|
|
294
|
+
},
|
|
295
|
+
} ),
|
|
296
|
+
getUndoManager: () => ( {
|
|
297
|
+
addRecord: jest.fn(),
|
|
298
|
+
} ),
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
editEntityRecord( 'postType', 'post', 1, {
|
|
302
|
+
meta: { newKey: 'newValue' },
|
|
303
|
+
} )( {
|
|
304
|
+
select,
|
|
305
|
+
dispatch,
|
|
306
|
+
} );
|
|
307
|
+
|
|
308
|
+
// Verify SyncManager#update was called with merged edits
|
|
309
|
+
expect( syncManager.update ).toHaveBeenCalledWith(
|
|
310
|
+
'postType/post',
|
|
311
|
+
1,
|
|
312
|
+
{
|
|
313
|
+
meta: {
|
|
314
|
+
existingKey: 'existingValue',
|
|
315
|
+
editedKey: 'editedValue',
|
|
316
|
+
newKey: 'newValue',
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
'local-editor',
|
|
320
|
+
{ isNewUndoLevel: true }
|
|
321
|
+
);
|
|
322
|
+
} );
|
|
323
|
+
|
|
324
|
+
it( 'passes merged edits to SyncManager#update even when value equals persisted record', () => {
|
|
325
|
+
const dispatch = jest.fn();
|
|
326
|
+
const select = {
|
|
327
|
+
getEntityConfig: () => ( {
|
|
328
|
+
kind: 'postType',
|
|
329
|
+
name: 'post',
|
|
330
|
+
mergedEdits: { meta: true },
|
|
331
|
+
syncConfig: {},
|
|
332
|
+
} ),
|
|
333
|
+
getRawEntityRecord: () => ( {
|
|
334
|
+
id: 1,
|
|
335
|
+
meta: { key1: 'value1', key2: 'value2' },
|
|
336
|
+
} ),
|
|
337
|
+
getEditedEntityRecord: () => ( {
|
|
338
|
+
id: 1,
|
|
339
|
+
meta: { key1: 'value1' },
|
|
340
|
+
} ),
|
|
341
|
+
getUndoManager: () => ( {
|
|
342
|
+
addRecord: jest.fn(),
|
|
343
|
+
} ),
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
// Editing meta to add key2 back results in a value equal to the persisted record
|
|
347
|
+
editEntityRecord( 'postType', 'post', 1, {
|
|
348
|
+
meta: { key2: 'value2' },
|
|
349
|
+
} )( { select, dispatch } );
|
|
350
|
+
|
|
351
|
+
// Verify SyncManager#update was called with merged edits (not cleaned/undefined)
|
|
352
|
+
expect( syncManager.update ).toHaveBeenCalledWith(
|
|
353
|
+
'postType/post',
|
|
354
|
+
1,
|
|
355
|
+
{
|
|
356
|
+
meta: {
|
|
357
|
+
key1: 'value1',
|
|
358
|
+
key2: 'value2',
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
'local-editor',
|
|
362
|
+
{ isNewUndoLevel: true }
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
// But the local store dispatch should still receive undefined for the cleaned edit
|
|
366
|
+
expect( dispatch ).toHaveBeenCalledWith( {
|
|
367
|
+
type: 'EDIT_ENTITY_RECORD',
|
|
368
|
+
kind: 'postType',
|
|
369
|
+
name: 'post',
|
|
370
|
+
recordId: 1,
|
|
371
|
+
edits: {
|
|
372
|
+
meta: undefined,
|
|
373
|
+
},
|
|
374
|
+
} );
|
|
375
|
+
} );
|
|
376
|
+
|
|
377
|
+
it( 'passes merged and non-merged edits correctly to SyncManager#update', () => {
|
|
378
|
+
const dispatch = jest.fn();
|
|
379
|
+
const select = {
|
|
380
|
+
getEntityConfig: () => ( {
|
|
381
|
+
kind: 'postType',
|
|
382
|
+
name: 'post',
|
|
383
|
+
mergedEdits: { meta: true },
|
|
384
|
+
syncConfig: {},
|
|
385
|
+
} ),
|
|
386
|
+
getRawEntityRecord: () => ( {
|
|
387
|
+
id: 1,
|
|
388
|
+
title: 'Original Title',
|
|
389
|
+
meta: { existingKey: 'existingValue' },
|
|
390
|
+
} ),
|
|
391
|
+
getEditedEntityRecord: () => ( {
|
|
392
|
+
id: 1,
|
|
393
|
+
title: 'Original Title',
|
|
394
|
+
meta: { existingKey: 'existingValue' },
|
|
395
|
+
} ),
|
|
396
|
+
getUndoManager: () => ( {
|
|
397
|
+
addRecord: jest.fn(),
|
|
398
|
+
} ),
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
editEntityRecord( 'postType', 'post', 1, {
|
|
402
|
+
title: 'New Title',
|
|
403
|
+
meta: { newKey: 'newValue' },
|
|
404
|
+
} )( { select, dispatch } );
|
|
405
|
+
|
|
406
|
+
// Verify SyncManager#update was called with merged meta but non-merged title
|
|
407
|
+
expect( syncManager.update ).toHaveBeenCalledWith(
|
|
408
|
+
'postType/post',
|
|
409
|
+
1,
|
|
410
|
+
{
|
|
411
|
+
title: 'New Title',
|
|
412
|
+
meta: {
|
|
413
|
+
existingKey: 'existingValue',
|
|
414
|
+
newKey: 'newValue',
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
'local-editor',
|
|
418
|
+
{ isNewUndoLevel: true }
|
|
419
|
+
);
|
|
420
|
+
} );
|
|
421
|
+
|
|
422
|
+
it( 'does not call SyncManager#update when syncConfig is not defined', () => {
|
|
423
|
+
const dispatch = jest.fn();
|
|
424
|
+
const select = {
|
|
425
|
+
getEntityConfig: () => ( {
|
|
426
|
+
kind: 'postType',
|
|
427
|
+
name: 'post',
|
|
428
|
+
mergedEdits: { meta: true },
|
|
429
|
+
// No syncConfig
|
|
430
|
+
} ),
|
|
431
|
+
getRawEntityRecord: () => ( {
|
|
432
|
+
id: 1,
|
|
433
|
+
meta: { existingKey: 'existingValue' },
|
|
434
|
+
} ),
|
|
435
|
+
getEditedEntityRecord: () => ( {
|
|
436
|
+
id: 1,
|
|
437
|
+
meta: { existingKey: 'existingValue' },
|
|
438
|
+
} ),
|
|
439
|
+
getUndoManager: () => ( {
|
|
440
|
+
addRecord: jest.fn(),
|
|
441
|
+
} ),
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
editEntityRecord( 'postType', 'post', 1, {
|
|
445
|
+
meta: { newKey: 'newValue' },
|
|
446
|
+
} )( {
|
|
447
|
+
select,
|
|
448
|
+
dispatch,
|
|
449
|
+
} );
|
|
450
|
+
|
|
451
|
+
// Verify SyncManager#update was NOT called
|
|
452
|
+
expect( syncManager.update ).not.toHaveBeenCalled();
|
|
453
|
+
} );
|
|
454
|
+
} );
|
|
53
455
|
} );
|
|
54
456
|
|
|
55
457
|
describe( 'deleteEntityRecord', () => {
|
|
@@ -86,6 +86,7 @@ describe( 'useEntityBlockEditor', () => {
|
|
|
86
86
|
const edit = ( { children } ) => <>{ children }</>;
|
|
87
87
|
|
|
88
88
|
registerBlockType( 'core/test-block', {
|
|
89
|
+
apiVersion: 3,
|
|
89
90
|
supports: {
|
|
90
91
|
className: false,
|
|
91
92
|
},
|
|
@@ -112,6 +113,7 @@ describe( 'useEntityBlockEditor', () => {
|
|
|
112
113
|
} );
|
|
113
114
|
|
|
114
115
|
registerBlockType( 'core/test-block-with-array-of-strings', {
|
|
116
|
+
apiVersion: 3,
|
|
115
117
|
supports: {
|
|
116
118
|
className: false,
|
|
117
119
|
},
|
package/src/test/resolvers.js
CHANGED
|
@@ -168,9 +168,11 @@ describe( 'getEntityRecord', () => {
|
|
|
168
168
|
1,
|
|
169
169
|
POST_RECORD,
|
|
170
170
|
{
|
|
171
|
+
addUndoMeta: expect.any( Function ),
|
|
171
172
|
editRecord: expect.any( Function ),
|
|
172
173
|
getEditedRecord: expect.any( Function ),
|
|
173
174
|
refetchRecord: expect.any( Function ),
|
|
175
|
+
restoreUndoMeta: expect.any( Function ),
|
|
174
176
|
saveRecord: expect.any( Function ),
|
|
175
177
|
}
|
|
176
178
|
);
|
|
@@ -221,9 +223,11 @@ describe( 'getEntityRecord', () => {
|
|
|
221
223
|
1,
|
|
222
224
|
{ ...POST_RECORD, foo: 'bar' },
|
|
223
225
|
{
|
|
226
|
+
addUndoMeta: expect.any( Function ),
|
|
224
227
|
editRecord: expect.any( Function ),
|
|
225
228
|
getEditedRecord: expect.any( Function ),
|
|
226
229
|
refetchRecord: expect.any( Function ),
|
|
230
|
+
restoreUndoMeta: expect.any( Function ),
|
|
227
231
|
saveRecord: expect.any( Function ),
|
|
228
232
|
}
|
|
229
233
|
);
|
package/src/types.ts
CHANGED
|
@@ -2,11 +2,20 @@ export interface AnyFunction {
|
|
|
2
2
|
( ...args: any[] ): any;
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Avoid a circular dependency with @wordpress/editor
|
|
7
|
+
*
|
|
8
|
+
* Additionaly, this type marks `attributeKey` and `offset` as possibly
|
|
9
|
+
* `undefined`, which can happen in two known scenarios:
|
|
10
|
+
*
|
|
11
|
+
* 1. If a user has an entire block highlighted (e.g., a `core/image` block).
|
|
12
|
+
* 2. If there's an intermediate selection state while inserting a block, those
|
|
13
|
+
* properties will be temporarily`undefined`.
|
|
14
|
+
*/
|
|
6
15
|
export interface WPBlockSelection {
|
|
7
16
|
clientId: string;
|
|
8
|
-
attributeKey
|
|
9
|
-
offset
|
|
17
|
+
attributeKey?: string;
|
|
18
|
+
offset?: number;
|
|
10
19
|
}
|
|
11
20
|
|
|
12
21
|
export interface WPSelection {
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* WordPress dependencies
|
|
6
|
+
*/
|
|
7
|
+
import { Y } from '@wordpress/sync';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Internal dependencies
|
|
11
|
+
*/
|
|
12
|
+
import { findBlockByClientIdInDoc } from './crdt-utils';
|
|
13
|
+
import type { WPBlockSelection, WPSelection } from '../types';
|
|
14
|
+
|
|
15
|
+
// Default size for selection history (not including current selection)
|
|
16
|
+
const SELECTION_HISTORY_DEFAULT_SIZE = 5;
|
|
17
|
+
|
|
18
|
+
export enum YSelectionType {
|
|
19
|
+
RelativeSelection = 'RelativeSelection',
|
|
20
|
+
BlockSelection = 'BlockSelection',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface YRelativeSelection {
|
|
24
|
+
type: YSelectionType.RelativeSelection;
|
|
25
|
+
attributeKey: string;
|
|
26
|
+
relativePosition: Y.RelativePosition;
|
|
27
|
+
clientId: string;
|
|
28
|
+
offset: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface YBlockSelection {
|
|
32
|
+
type: YSelectionType.BlockSelection;
|
|
33
|
+
clientId: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type YSelection = YRelativeSelection | YBlockSelection;
|
|
37
|
+
|
|
38
|
+
export type YFullSelection = {
|
|
39
|
+
start: YSelection;
|
|
40
|
+
end: YSelection;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export interface YSelectionHistory {
|
|
44
|
+
selection: YFullSelection;
|
|
45
|
+
backupSelections?: YFullSelection[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface BlockSelectionHistory {
|
|
49
|
+
getSelectionHistory: () => YFullSelection[];
|
|
50
|
+
updateSelection: ( newSelection: WPSelection ) => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* This function is used to track recent block selections to help in restoring
|
|
55
|
+
* a user's selection after an undo or redo operation.
|
|
56
|
+
*
|
|
57
|
+
* Maintains a history array for previous selections, which can be used for
|
|
58
|
+
* backup restoration locations.
|
|
59
|
+
* @param ydoc
|
|
60
|
+
* @param historySize
|
|
61
|
+
*/
|
|
62
|
+
export function createBlockSelectionHistory(
|
|
63
|
+
ydoc: Y.Doc,
|
|
64
|
+
historySize: number = SELECTION_HISTORY_DEFAULT_SIZE
|
|
65
|
+
): BlockSelectionHistory {
|
|
66
|
+
let history: YFullSelection[] = [];
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get the block history including current selection.
|
|
70
|
+
*/
|
|
71
|
+
const getSelectionHistory = (): YFullSelection[] => {
|
|
72
|
+
return history.slice( 0 );
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Update the selection history with a new selection.
|
|
77
|
+
* @param newSelection
|
|
78
|
+
*/
|
|
79
|
+
const updateSelection = ( newSelection: WPSelection ): void => {
|
|
80
|
+
if (
|
|
81
|
+
! newSelection?.selectionStart?.clientId ||
|
|
82
|
+
! newSelection?.selectionEnd?.clientId
|
|
83
|
+
) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const { selectionStart, selectionEnd } = newSelection;
|
|
88
|
+
const start = convertWPBlockSelectionToSelection(
|
|
89
|
+
selectionStart,
|
|
90
|
+
ydoc
|
|
91
|
+
);
|
|
92
|
+
const end = convertWPBlockSelectionToSelection( selectionEnd, ydoc );
|
|
93
|
+
|
|
94
|
+
addToHistory( { start, end } );
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Add a selection to the history, maintaining only the last `historySize` unique selections.
|
|
99
|
+
* New selections are added to the front.
|
|
100
|
+
* Removes any existing entries with the same start and end block combination.
|
|
101
|
+
* @param yFullSelection
|
|
102
|
+
*/
|
|
103
|
+
const addToHistory = ( yFullSelection: YFullSelection ): void => {
|
|
104
|
+
// Remove any existing entries with the same start and end block combination
|
|
105
|
+
const startClientId = yFullSelection.start.clientId;
|
|
106
|
+
const endClientId = yFullSelection.end.clientId;
|
|
107
|
+
|
|
108
|
+
history = history.filter( ( entry ) => {
|
|
109
|
+
const isSameBlockCombination =
|
|
110
|
+
entry.start.clientId === startClientId &&
|
|
111
|
+
entry.end.clientId === endClientId;
|
|
112
|
+
|
|
113
|
+
return ! isSameBlockCombination;
|
|
114
|
+
} );
|
|
115
|
+
|
|
116
|
+
// Add the new selection to the front
|
|
117
|
+
history.unshift( yFullSelection );
|
|
118
|
+
|
|
119
|
+
// Trim to max size (remove oldest entries from the back)
|
|
120
|
+
if ( history.length > historySize + 1 ) {
|
|
121
|
+
history = history.slice( 0, historySize + 1 );
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
getSelectionHistory,
|
|
127
|
+
updateSelection,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Convert a WPBlockSelection to a YSelection.
|
|
133
|
+
* @param selection
|
|
134
|
+
* @param ydoc
|
|
135
|
+
* @return A YSelection object.
|
|
136
|
+
*/
|
|
137
|
+
function convertWPBlockSelectionToSelection(
|
|
138
|
+
selection: WPBlockSelection,
|
|
139
|
+
ydoc: Y.Doc
|
|
140
|
+
): YSelection {
|
|
141
|
+
const clientId = selection.clientId;
|
|
142
|
+
const block = findBlockByClientIdInDoc( clientId, ydoc );
|
|
143
|
+
const attributes = block?.get( 'attributes' );
|
|
144
|
+
const attributeKey = selection.attributeKey;
|
|
145
|
+
|
|
146
|
+
const changedYText = attributeKey
|
|
147
|
+
? attributes?.get( attributeKey )
|
|
148
|
+
: undefined;
|
|
149
|
+
|
|
150
|
+
const isYText = changedYText instanceof Y.Text;
|
|
151
|
+
const isFullyDefinedSelection = attributeKey && clientId;
|
|
152
|
+
|
|
153
|
+
if ( ! isYText || ! isFullyDefinedSelection ) {
|
|
154
|
+
// We either don't have a valid YText (it's been deleted) or we've
|
|
155
|
+
// been passed a selection that's just a block clientId.
|
|
156
|
+
// Store as BlockSelection.
|
|
157
|
+
return {
|
|
158
|
+
type: YSelectionType.BlockSelection,
|
|
159
|
+
clientId,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const offset = selection.offset ?? 0;
|
|
164
|
+
const relativePosition = Y.createRelativePositionFromTypeIndex(
|
|
165
|
+
changedYText,
|
|
166
|
+
offset
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
type: YSelectionType.RelativeSelection,
|
|
171
|
+
attributeKey,
|
|
172
|
+
relativePosition,
|
|
173
|
+
clientId,
|
|
174
|
+
offset,
|
|
175
|
+
};
|
|
176
|
+
}
|