@wordpress/core-data 7.41.2-next.v.202603102151.0 → 7.42.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 +1 -1
- package/README.md +19 -0
- package/build/actions.cjs +17 -25
- package/build/actions.cjs.map +2 -2
- package/build/awareness/post-editor-awareness.cjs +46 -6
- package/build/awareness/post-editor-awareness.cjs.map +2 -2
- package/build/awareness/types.cjs.map +1 -1
- package/build/entities.cjs +33 -7
- package/build/entities.cjs.map +2 -2
- package/build/hooks/use-entity-prop.cjs +2 -1
- package/build/hooks/use-entity-prop.cjs.map +2 -2
- package/build/hooks/use-post-editor-awareness-state.cjs +84 -3
- package/build/hooks/use-post-editor-awareness-state.cjs.map +2 -2
- package/build/index.cjs +3 -0
- package/build/index.cjs.map +2 -2
- package/build/private-apis.cjs +3 -1
- package/build/private-apis.cjs.map +2 -2
- package/build/queried-data/get-query-parts.cjs +7 -0
- package/build/queried-data/get-query-parts.cjs.map +2 -2
- package/build/queried-data/selectors.cjs +19 -5
- package/build/queried-data/selectors.cjs.map +2 -2
- package/build/reducer.cjs +6 -0
- package/build/reducer.cjs.map +2 -2
- package/build/resolvers.cjs +110 -74
- package/build/resolvers.cjs.map +2 -2
- package/build/selectors.cjs +29 -0
- package/build/selectors.cjs.map +2 -2
- package/build/sync.cjs +3 -0
- package/build/sync.cjs.map +2 -2
- package/build/types.cjs +16 -0
- package/build/types.cjs.map +3 -3
- package/build/utils/block-selection-history.cjs +1 -1
- package/build/utils/block-selection-history.cjs.map +2 -2
- package/build/utils/crdt-blocks.cjs +17 -3
- package/build/utils/crdt-blocks.cjs.map +2 -2
- package/build/utils/crdt-selection.cjs +4 -1
- package/build/utils/crdt-selection.cjs.map +2 -2
- package/build/utils/crdt-user-selections.cjs +9 -6
- package/build/utils/crdt-user-selections.cjs.map +2 -2
- package/build/utils/crdt-utils.cjs +54 -2
- package/build/utils/crdt-utils.cjs.map +2 -2
- package/build/utils/crdt.cjs +4 -23
- package/build/utils/crdt.cjs.map +2 -2
- package/build/utils/index.cjs +3 -0
- package/build/utils/index.cjs.map +2 -2
- package/build/utils/normalize-query-for-resolution.cjs +35 -0
- package/build/utils/normalize-query-for-resolution.cjs.map +7 -0
- package/build-module/actions.mjs +17 -25
- package/build-module/actions.mjs.map +2 -2
- package/build-module/awareness/post-editor-awareness.mjs +46 -6
- package/build-module/awareness/post-editor-awareness.mjs.map +2 -2
- package/build-module/entities.mjs +33 -7
- package/build-module/entities.mjs.map +2 -2
- package/build-module/hooks/use-entity-prop.mjs +2 -1
- package/build-module/hooks/use-entity-prop.mjs.map +2 -2
- package/build-module/hooks/use-post-editor-awareness-state.mjs +81 -2
- package/build-module/hooks/use-post-editor-awareness-state.mjs.map +2 -2
- package/build-module/index.mjs +2 -0
- package/build-module/index.mjs.map +2 -2
- package/build-module/private-apis.mjs +6 -2
- package/build-module/private-apis.mjs.map +2 -2
- package/build-module/queried-data/get-query-parts.mjs +7 -0
- package/build-module/queried-data/get-query-parts.mjs.map +2 -2
- package/build-module/queried-data/selectors.mjs +19 -5
- package/build-module/queried-data/selectors.mjs.map +2 -2
- package/build-module/reducer.mjs +6 -0
- package/build-module/reducer.mjs.map +2 -2
- package/build-module/resolvers.mjs +112 -75
- package/build-module/resolvers.mjs.map +2 -2
- package/build-module/selectors.mjs +28 -0
- package/build-module/selectors.mjs.map +2 -2
- package/build-module/sync.mjs +2 -0
- package/build-module/sync.mjs.map +2 -2
- package/build-module/types.mjs +9 -0
- package/build-module/types.mjs.map +4 -4
- package/build-module/utils/block-selection-history.mjs +5 -2
- package/build-module/utils/block-selection-history.mjs.map +2 -2
- package/build-module/utils/crdt-blocks.mjs +17 -3
- package/build-module/utils/crdt-blocks.mjs.map +2 -2
- package/build-module/utils/crdt-selection.mjs +8 -2
- package/build-module/utils/crdt-selection.mjs.map +2 -2
- package/build-module/utils/crdt-user-selections.mjs +10 -7
- package/build-module/utils/crdt-user-selections.mjs.map +2 -2
- package/build-module/utils/crdt-utils.mjs +51 -1
- package/build-module/utils/crdt-utils.mjs.map +2 -2
- package/build-module/utils/crdt.mjs +4 -23
- package/build-module/utils/crdt.mjs.map +2 -2
- package/build-module/utils/index.mjs +2 -0
- package/build-module/utils/index.mjs.map +2 -2
- package/build-module/utils/normalize-query-for-resolution.mjs +14 -0
- package/build-module/utils/normalize-query-for-resolution.mjs.map +7 -0
- package/build-types/actions.d.ts.map +1 -1
- package/build-types/awareness/post-editor-awareness.d.ts +2 -2
- package/build-types/awareness/post-editor-awareness.d.ts.map +1 -1
- package/build-types/awareness/types.d.ts +1 -1
- package/build-types/awareness/types.d.ts.map +1 -1
- package/build-types/entities.d.ts +1 -1
- package/build-types/entities.d.ts.map +1 -1
- package/build-types/hooks/use-entity-prop.d.ts.map +1 -1
- package/build-types/hooks/use-post-editor-awareness-state.d.ts +34 -10
- package/build-types/hooks/use-post-editor-awareness-state.d.ts.map +1 -1
- package/build-types/index.d.ts +2 -0
- package/build-types/index.d.ts.map +1 -1
- package/build-types/private-apis.d.ts.map +1 -1
- package/build-types/queried-data/get-query-parts.d.ts +7 -0
- package/build-types/queried-data/get-query-parts.d.ts.map +1 -1
- package/build-types/queried-data/selectors.d.ts.map +1 -1
- package/build-types/reducer.d.ts.map +1 -1
- package/build-types/resolvers.d.ts +2 -1
- package/build-types/resolvers.d.ts.map +1 -1
- package/build-types/selectors.d.ts +17 -0
- package/build-types/selectors.d.ts.map +1 -1
- package/build-types/sync.d.ts +2 -2
- package/build-types/sync.d.ts.map +1 -1
- package/build-types/types.d.ts +18 -1
- package/build-types/types.d.ts.map +1 -1
- package/build-types/utils/block-selection-history.d.ts.map +1 -1
- package/build-types/utils/crdt-blocks.d.ts.map +1 -1
- package/build-types/utils/crdt-selection.d.ts.map +1 -1
- package/build-types/utils/crdt-user-selections.d.ts +9 -5
- package/build-types/utils/crdt-user-selections.d.ts.map +1 -1
- package/build-types/utils/crdt-utils.d.ts +20 -0
- package/build-types/utils/crdt-utils.d.ts.map +1 -1
- package/build-types/utils/crdt.d.ts +6 -7
- package/build-types/utils/crdt.d.ts.map +1 -1
- package/build-types/utils/index.d.ts +1 -0
- package/build-types/utils/normalize-query-for-resolution.d.ts +12 -0
- package/build-types/utils/normalize-query-for-resolution.d.ts.map +1 -0
- package/build-types/utils/test/crdt-utils.d.ts +2 -0
- package/build-types/utils/test/crdt-utils.d.ts.map +1 -0
- package/package.json +18 -18
- package/src/actions.js +25 -40
- package/src/awareness/post-editor-awareness.ts +106 -7
- package/src/awareness/test/post-editor-awareness.ts +50 -10
- package/src/awareness/types.ts +1 -1
- package/src/entities.js +38 -6
- package/src/hooks/test/use-post-editor-awareness-state.ts +446 -3
- package/src/hooks/use-entity-prop.js +2 -0
- package/src/hooks/use-post-editor-awareness-state.ts +160 -8
- package/src/index.js +1 -0
- package/src/private-apis.js +6 -2
- package/src/queried-data/get-query-parts.js +13 -0
- package/src/queried-data/selectors.js +33 -8
- package/src/queried-data/test/get-query-parts.js +34 -0
- package/src/queried-data/test/selectors.js +183 -0
- package/src/reducer.js +11 -0
- package/src/resolvers.js +136 -88
- package/src/selectors.ts +56 -0
- package/src/sync.ts +2 -0
- package/src/test/entities.js +185 -1
- package/src/test/resolvers.js +64 -11
- package/src/test/selectors.js +150 -0
- package/src/test/store.js +66 -0
- package/src/types.ts +26 -1
- package/src/utils/block-selection-history.ts +5 -2
- package/src/utils/crdt-blocks.ts +32 -3
- package/src/utils/crdt-selection.ts +8 -2
- package/src/utils/crdt-user-selections.ts +20 -8
- package/src/utils/crdt-utils.ts +99 -0
- package/src/utils/crdt.ts +8 -32
- package/src/utils/index.js +1 -0
- package/src/utils/normalize-query-for-resolution.js +23 -0
- package/src/utils/test/crdt-blocks.ts +146 -0
- package/src/utils/test/crdt-user-selections.ts +5 -0
- package/src/utils/test/crdt-utils.ts +387 -0
- package/src/utils/test/crdt.ts +120 -53
package/src/test/resolvers.js
CHANGED
|
@@ -172,16 +172,69 @@ describe( 'getEntityRecord', () => {
|
|
|
172
172
|
editRecord: expect.any( Function ),
|
|
173
173
|
getEditedRecord: expect.any( Function ),
|
|
174
174
|
onStatusChange: expect.any( Function ),
|
|
175
|
+
persistCRDTDoc: expect.any( Function ),
|
|
175
176
|
refetchRecord: expect.any( Function ),
|
|
176
177
|
restoreUndoMeta: expect.any( Function ),
|
|
177
|
-
saveRecord: expect.any( Function ),
|
|
178
178
|
}
|
|
179
179
|
);
|
|
180
180
|
} );
|
|
181
181
|
|
|
182
|
-
it( '
|
|
183
|
-
const
|
|
184
|
-
const EDITED_RECORD = { id: 1, title: 'Edited
|
|
182
|
+
it( 'persistCRDTDoc fetches edited record and does not save full entity record when the entity does not support meta', async () => {
|
|
183
|
+
const ENTITY_RECORD = { id: 1, title: 'Test Record' };
|
|
184
|
+
const EDITED_RECORD = { id: 1, title: 'Edited Record' };
|
|
185
|
+
const ENTITY_RESPONSE = {
|
|
186
|
+
json: () => Promise.resolve( ENTITY_RECORD ),
|
|
187
|
+
};
|
|
188
|
+
const ENTITIES_WITH_SYNC = [
|
|
189
|
+
{
|
|
190
|
+
name: 'bar',
|
|
191
|
+
kind: 'foo',
|
|
192
|
+
baseURL: '/wp/v2/foo',
|
|
193
|
+
baseURLParams: { context: 'edit' },
|
|
194
|
+
syncConfig: {},
|
|
195
|
+
},
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
dispatch.saveEntityRecord = jest.fn();
|
|
199
|
+
|
|
200
|
+
const resolveSelectWithSync = {
|
|
201
|
+
getEntitiesConfig: jest.fn( () => ENTITIES_WITH_SYNC ),
|
|
202
|
+
getEditedEntityRecord: jest.fn( () =>
|
|
203
|
+
Promise.resolve( EDITED_RECORD )
|
|
204
|
+
),
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
triggerFetch.mockImplementation( () => ENTITY_RESPONSE );
|
|
208
|
+
|
|
209
|
+
await getEntityRecord(
|
|
210
|
+
'foo',
|
|
211
|
+
'bar',
|
|
212
|
+
1
|
|
213
|
+
)( {
|
|
214
|
+
dispatch,
|
|
215
|
+
registry,
|
|
216
|
+
resolveSelect: resolveSelectWithSync,
|
|
217
|
+
} );
|
|
218
|
+
|
|
219
|
+
// Extract the handlers passed to syncManager.load.
|
|
220
|
+
const handlers = syncManager.load.mock.calls[ 0 ][ 4 ];
|
|
221
|
+
|
|
222
|
+
// Call persistCRDTDoc and wait for the internal promise chain.
|
|
223
|
+
handlers.persistCRDTDoc();
|
|
224
|
+
await resolveSelectWithSync.getEditedEntityRecord();
|
|
225
|
+
|
|
226
|
+
// Should have fetched the full edited entity record.
|
|
227
|
+
expect(
|
|
228
|
+
resolveSelectWithSync.getEditedEntityRecord
|
|
229
|
+
).toHaveBeenCalledWith( 'foo', 'bar', 1 );
|
|
230
|
+
|
|
231
|
+
// Should not have called saveEntityRecord.
|
|
232
|
+
expect( dispatch.saveEntityRecord ).not.toHaveBeenCalled();
|
|
233
|
+
} );
|
|
234
|
+
|
|
235
|
+
it( 'persistCRDTDoc fetches edited record and saves full entity record', async () => {
|
|
236
|
+
const POST_RECORD = { id: 1, title: 'Test Post', meta: {} };
|
|
237
|
+
const EDITED_RECORD = { id: 1, title: 'Edited Post', meta: {} };
|
|
185
238
|
const POST_RESPONSE = {
|
|
186
239
|
json: () => Promise.resolve( POST_RECORD ),
|
|
187
240
|
};
|
|
@@ -219,8 +272,8 @@ describe( 'getEntityRecord', () => {
|
|
|
219
272
|
// Extract the handlers passed to syncManager.load.
|
|
220
273
|
const handlers = syncManager.load.mock.calls[ 0 ][ 4 ];
|
|
221
274
|
|
|
222
|
-
// Call
|
|
223
|
-
handlers.
|
|
275
|
+
// Call persistCRDTDoc and wait for the internal promise chain.
|
|
276
|
+
handlers.persistCRDTDoc();
|
|
224
277
|
await resolveSelectWithSync.getEditedEntityRecord();
|
|
225
278
|
|
|
226
279
|
// Should have fetched the full edited entity record.
|
|
@@ -236,8 +289,8 @@ describe( 'getEntityRecord', () => {
|
|
|
236
289
|
);
|
|
237
290
|
} );
|
|
238
291
|
|
|
239
|
-
it( '
|
|
240
|
-
const POST_RECORD = { id: 1, title: 'Test Post' };
|
|
292
|
+
it( 'persistCRDTDoc saves even when there are no unsaved edits', async () => {
|
|
293
|
+
const POST_RECORD = { id: 1, title: 'Test Post', meta: {} };
|
|
241
294
|
const POST_RESPONSE = {
|
|
242
295
|
json: () => Promise.resolve( POST_RECORD ),
|
|
243
296
|
};
|
|
@@ -275,8 +328,8 @@ describe( 'getEntityRecord', () => {
|
|
|
275
328
|
|
|
276
329
|
const handlers = syncManager.load.mock.calls[ 0 ][ 4 ];
|
|
277
330
|
|
|
278
|
-
// Call
|
|
279
|
-
handlers.
|
|
331
|
+
// Call persistCRDTDoc and wait for the internal promise chain.
|
|
332
|
+
handlers.persistCRDTDoc();
|
|
280
333
|
await resolveSelectWithSync.getEditedEntityRecord();
|
|
281
334
|
|
|
282
335
|
// Should save the record even with no edits (the whole point of the fix).
|
|
@@ -336,9 +389,9 @@ describe( 'getEntityRecord', () => {
|
|
|
336
389
|
editRecord: expect.any( Function ),
|
|
337
390
|
getEditedRecord: expect.any( Function ),
|
|
338
391
|
onStatusChange: expect.any( Function ),
|
|
392
|
+
persistCRDTDoc: expect.any( Function ),
|
|
339
393
|
refetchRecord: expect.any( Function ),
|
|
340
394
|
restoreUndoMeta: expect.any( Function ),
|
|
341
|
-
saveRecord: expect.any( Function ),
|
|
342
395
|
}
|
|
343
396
|
);
|
|
344
397
|
} );
|
package/src/test/selectors.js
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
getCurrentUser,
|
|
24
24
|
getRevisions,
|
|
25
25
|
getRevision,
|
|
26
|
+
hasRevision,
|
|
26
27
|
} from '../selectors';
|
|
27
28
|
|
|
28
29
|
describe( 'getEntityRecord', () => {
|
|
@@ -1215,3 +1216,152 @@ describe( 'getRevision', () => {
|
|
|
1215
1216
|
} );
|
|
1216
1217
|
} );
|
|
1217
1218
|
} );
|
|
1219
|
+
|
|
1220
|
+
describe( 'hasRevision', () => {
|
|
1221
|
+
it( 'returns false if revision has not been received', () => {
|
|
1222
|
+
const state = deepFreeze( {
|
|
1223
|
+
entities: {
|
|
1224
|
+
records: {
|
|
1225
|
+
postType: {
|
|
1226
|
+
post: {
|
|
1227
|
+
revisions: {
|
|
1228
|
+
1: {
|
|
1229
|
+
items: {},
|
|
1230
|
+
itemIsComplete: {},
|
|
1231
|
+
queries: {},
|
|
1232
|
+
},
|
|
1233
|
+
},
|
|
1234
|
+
},
|
|
1235
|
+
},
|
|
1236
|
+
},
|
|
1237
|
+
},
|
|
1238
|
+
} );
|
|
1239
|
+
expect( hasRevision( state, 'postType', 'post', 1, 10 ) ).toBe( false );
|
|
1240
|
+
} );
|
|
1241
|
+
|
|
1242
|
+
it( 'returns false if parent record does not exist', () => {
|
|
1243
|
+
const state = deepFreeze( {
|
|
1244
|
+
entities: {
|
|
1245
|
+
records: {},
|
|
1246
|
+
},
|
|
1247
|
+
} );
|
|
1248
|
+
expect( hasRevision( state, 'postType', 'post', 1, 10 ) ).toBe( false );
|
|
1249
|
+
} );
|
|
1250
|
+
|
|
1251
|
+
it( 'returns true when full revision exists and no fields query', () => {
|
|
1252
|
+
const state = deepFreeze( {
|
|
1253
|
+
entities: {
|
|
1254
|
+
records: {
|
|
1255
|
+
postType: {
|
|
1256
|
+
post: {
|
|
1257
|
+
revisions: {
|
|
1258
|
+
1: {
|
|
1259
|
+
items: {
|
|
1260
|
+
default: {
|
|
1261
|
+
10: {
|
|
1262
|
+
id: 10,
|
|
1263
|
+
content: 'chicken',
|
|
1264
|
+
parent: 1,
|
|
1265
|
+
},
|
|
1266
|
+
},
|
|
1267
|
+
},
|
|
1268
|
+
itemIsComplete: {
|
|
1269
|
+
default: {
|
|
1270
|
+
10: true,
|
|
1271
|
+
},
|
|
1272
|
+
},
|
|
1273
|
+
queries: {},
|
|
1274
|
+
},
|
|
1275
|
+
},
|
|
1276
|
+
},
|
|
1277
|
+
},
|
|
1278
|
+
},
|
|
1279
|
+
},
|
|
1280
|
+
} );
|
|
1281
|
+
expect( hasRevision( state, 'postType', 'post', 1, 10 ) ).toBe( true );
|
|
1282
|
+
} );
|
|
1283
|
+
|
|
1284
|
+
it( 'returns true when requested fields exist on the revision', () => {
|
|
1285
|
+
const state = deepFreeze( {
|
|
1286
|
+
entities: {
|
|
1287
|
+
records: {
|
|
1288
|
+
postType: {
|
|
1289
|
+
post: {
|
|
1290
|
+
revisions: {
|
|
1291
|
+
1: {
|
|
1292
|
+
items: {
|
|
1293
|
+
default: {
|
|
1294
|
+
10: {
|
|
1295
|
+
id: 10,
|
|
1296
|
+
content: 'chicken',
|
|
1297
|
+
title: { raw: 'egg' },
|
|
1298
|
+
parent: 1,
|
|
1299
|
+
},
|
|
1300
|
+
},
|
|
1301
|
+
},
|
|
1302
|
+
itemIsComplete: {
|
|
1303
|
+
default: {
|
|
1304
|
+
10: true,
|
|
1305
|
+
},
|
|
1306
|
+
},
|
|
1307
|
+
queries: {},
|
|
1308
|
+
},
|
|
1309
|
+
},
|
|
1310
|
+
},
|
|
1311
|
+
},
|
|
1312
|
+
},
|
|
1313
|
+
},
|
|
1314
|
+
} );
|
|
1315
|
+
expect(
|
|
1316
|
+
hasRevision( state, 'postType', 'post', 1, 10, {
|
|
1317
|
+
_fields: [ 'id', 'content' ],
|
|
1318
|
+
} )
|
|
1319
|
+
).toBe( true );
|
|
1320
|
+
expect(
|
|
1321
|
+
hasRevision( state, 'postType', 'post', 1, 10, {
|
|
1322
|
+
_fields: [ 'id', 'title.raw' ],
|
|
1323
|
+
} )
|
|
1324
|
+
).toBe( true );
|
|
1325
|
+
} );
|
|
1326
|
+
|
|
1327
|
+
it( 'returns false when requested fields are missing', () => {
|
|
1328
|
+
const state = deepFreeze( {
|
|
1329
|
+
entities: {
|
|
1330
|
+
records: {
|
|
1331
|
+
postType: {
|
|
1332
|
+
post: {
|
|
1333
|
+
revisions: {
|
|
1334
|
+
1: {
|
|
1335
|
+
items: {
|
|
1336
|
+
default: {
|
|
1337
|
+
10: {
|
|
1338
|
+
id: 10,
|
|
1339
|
+
parent: 1,
|
|
1340
|
+
},
|
|
1341
|
+
},
|
|
1342
|
+
},
|
|
1343
|
+
itemIsComplete: {
|
|
1344
|
+
default: {
|
|
1345
|
+
10: true,
|
|
1346
|
+
},
|
|
1347
|
+
},
|
|
1348
|
+
queries: {},
|
|
1349
|
+
},
|
|
1350
|
+
},
|
|
1351
|
+
},
|
|
1352
|
+
},
|
|
1353
|
+
},
|
|
1354
|
+
},
|
|
1355
|
+
} );
|
|
1356
|
+
expect(
|
|
1357
|
+
hasRevision( state, 'postType', 'post', 1, 10, {
|
|
1358
|
+
_fields: [ 'id', 'content' ],
|
|
1359
|
+
} )
|
|
1360
|
+
).toBe( false );
|
|
1361
|
+
expect(
|
|
1362
|
+
hasRevision( state, 'postType', 'post', 1, 10, {
|
|
1363
|
+
_fields: [ 'id', 'title.raw' ],
|
|
1364
|
+
} )
|
|
1365
|
+
).toBe( false );
|
|
1366
|
+
} );
|
|
1367
|
+
} );
|
package/src/test/store.js
CHANGED
|
@@ -36,6 +36,10 @@ function createTestRegistry() {
|
|
|
36
36
|
__unstable_rest_base: 'posts',
|
|
37
37
|
supportsPagination: true,
|
|
38
38
|
revisionKey: 'id',
|
|
39
|
+
getRevisionsUrl: ( parentId, revisionId ) =>
|
|
40
|
+
`/wp/v2/posts/${ parentId }/revisions${
|
|
41
|
+
revisionId ? '/' + revisionId : ''
|
|
42
|
+
}`,
|
|
39
43
|
};
|
|
40
44
|
|
|
41
45
|
// Add the post entity to the store
|
|
@@ -258,3 +262,65 @@ describe( 'clearEntityRecordEdits', () => {
|
|
|
258
262
|
).toEqual( select.getRawEntityRecord( 'postType', 'post', post.id ) );
|
|
259
263
|
} );
|
|
260
264
|
} );
|
|
265
|
+
|
|
266
|
+
describe( 'getRevisions', () => {
|
|
267
|
+
const KIND = 'postType';
|
|
268
|
+
const NAME = 'post';
|
|
269
|
+
const RECORD_KEY = 1;
|
|
270
|
+
const REVISIONS = [ { id: 2 }, { id: 3 }, { id: 4 } ];
|
|
271
|
+
|
|
272
|
+
let registry;
|
|
273
|
+
|
|
274
|
+
beforeEach( () => {
|
|
275
|
+
registry = createTestRegistry();
|
|
276
|
+
triggerFetch.mockReset();
|
|
277
|
+
} );
|
|
278
|
+
|
|
279
|
+
it( 'preserves all revisions when getRevision resolves after getRevisions', async () => {
|
|
280
|
+
let resolveSlowFetch;
|
|
281
|
+
const slowFetchPromise = new Promise( ( resolve ) => {
|
|
282
|
+
resolveSlowFetch = resolve;
|
|
283
|
+
} );
|
|
284
|
+
|
|
285
|
+
triggerFetch.mockImplementation( ( { path } ) => {
|
|
286
|
+
if ( path && path.includes( 'revisions' ) ) {
|
|
287
|
+
// Single revision fetch: return slow promise.
|
|
288
|
+
if ( /revisions\/\d+/.test( path ) ) {
|
|
289
|
+
return slowFetchPromise;
|
|
290
|
+
}
|
|
291
|
+
// Collection fetch: return immediately.
|
|
292
|
+
return Promise.resolve( {
|
|
293
|
+
json: () => Promise.resolve( REVISIONS ),
|
|
294
|
+
headers: { get: () => String( REVISIONS.length ) },
|
|
295
|
+
} );
|
|
296
|
+
}
|
|
297
|
+
return Promise.resolve( {} );
|
|
298
|
+
} );
|
|
299
|
+
|
|
300
|
+
const resolveSelectStore = registry.resolveSelect( coreDataStore );
|
|
301
|
+
|
|
302
|
+
// Start getRevision first (slow), then getRevisions (fast).
|
|
303
|
+
const revisionPromise = resolveSelectStore.getRevision(
|
|
304
|
+
KIND,
|
|
305
|
+
NAME,
|
|
306
|
+
RECORD_KEY,
|
|
307
|
+
1,
|
|
308
|
+
{ context: 'edit' }
|
|
309
|
+
);
|
|
310
|
+
await resolveSelectStore.getRevisions( KIND, NAME, RECORD_KEY, {
|
|
311
|
+
context: 'edit',
|
|
312
|
+
} );
|
|
313
|
+
|
|
314
|
+
// Now resolve the slow single-revision fetch.
|
|
315
|
+
resolveSlowFetch( REVISIONS[ 0 ] );
|
|
316
|
+
await revisionPromise;
|
|
317
|
+
|
|
318
|
+
// Wait for all pending thunks (receiveRevisions) to settle.
|
|
319
|
+
await new Promise( ( resolve ) => setTimeout( resolve, 0 ) );
|
|
320
|
+
|
|
321
|
+
const allRevisions = registry
|
|
322
|
+
.select( coreDataStore )
|
|
323
|
+
.getRevisions( KIND, NAME, RECORD_KEY, { context: 'edit' } );
|
|
324
|
+
expect( allRevisions.map( ( r ) => r.id ) ).toEqual( [ 2, 3, 4 ] );
|
|
325
|
+
} );
|
|
326
|
+
} );
|
package/src/types.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WordPress dependencies
|
|
3
3
|
*/
|
|
4
|
-
import type { Y } from '@wordpress/sync';
|
|
4
|
+
import type { ConnectionStatusDisconnected, Y } from '@wordpress/sync';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Internal dependencies
|
|
8
8
|
*/
|
|
9
9
|
import type { SelectionType } from './utils/crdt-user-selections';
|
|
10
10
|
|
|
11
|
+
export type { ConnectionStatus } from '@wordpress/sync';
|
|
12
|
+
|
|
13
|
+
export type ConnectionError = NonNullable<
|
|
14
|
+
ConnectionStatusDisconnected[ 'error' ]
|
|
15
|
+
>;
|
|
16
|
+
|
|
11
17
|
export interface AnyFunction {
|
|
12
18
|
( ...args: any[] ): any;
|
|
13
19
|
}
|
|
@@ -66,6 +72,16 @@ export type CursorPosition = {
|
|
|
66
72
|
absoluteOffset: number;
|
|
67
73
|
};
|
|
68
74
|
|
|
75
|
+
/**
|
|
76
|
+
* The direction of a text selection, indicating where the caret sits.
|
|
77
|
+
*/
|
|
78
|
+
export enum SelectionDirection {
|
|
79
|
+
/** The caret is at the end of the selection (default / left-to-right). */
|
|
80
|
+
Forward = 'f',
|
|
81
|
+
/** The caret is at the start of the selection (right-to-left). */
|
|
82
|
+
Backward = 'b',
|
|
83
|
+
}
|
|
84
|
+
|
|
69
85
|
export type SelectionNone = {
|
|
70
86
|
// The user has not made a selection.
|
|
71
87
|
type: SelectionType.None;
|
|
@@ -86,6 +102,8 @@ export type SelectionInOneBlock = {
|
|
|
86
102
|
type: SelectionType.SelectionInOneBlock;
|
|
87
103
|
cursorStartPosition: CursorPosition;
|
|
88
104
|
cursorEndPosition: CursorPosition;
|
|
105
|
+
// The direction of the selection, indicating where the caret sits.
|
|
106
|
+
selectionDirection?: SelectionDirection;
|
|
89
107
|
};
|
|
90
108
|
|
|
91
109
|
export type SelectionInMultipleBlocks = {
|
|
@@ -95,6 +113,8 @@ export type SelectionInMultipleBlocks = {
|
|
|
95
113
|
type: SelectionType.SelectionInMultipleBlocks;
|
|
96
114
|
cursorStartPosition: CursorPosition;
|
|
97
115
|
cursorEndPosition: CursorPosition;
|
|
116
|
+
// The direction of the selection, indicating where the caret sits.
|
|
117
|
+
selectionDirection?: SelectionDirection;
|
|
98
118
|
};
|
|
99
119
|
|
|
100
120
|
export type SelectionWholeBlock = {
|
|
@@ -111,3 +131,8 @@ export type SelectionState =
|
|
|
111
131
|
| SelectionInOneBlock
|
|
112
132
|
| SelectionInMultipleBlocks
|
|
113
133
|
| SelectionWholeBlock;
|
|
134
|
+
|
|
135
|
+
export interface ResolvedSelection {
|
|
136
|
+
richTextOffset: number | null;
|
|
137
|
+
localClientId: string | null;
|
|
138
|
+
}
|
|
@@ -9,7 +9,10 @@ import { Y } from '@wordpress/sync';
|
|
|
9
9
|
/**
|
|
10
10
|
* Internal dependencies
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
findBlockByClientIdInDoc,
|
|
14
|
+
richTextOffsetToHtmlIndex,
|
|
15
|
+
} from './crdt-utils';
|
|
13
16
|
import type { WPBlockSelection, WPSelection } from '../types';
|
|
14
17
|
|
|
15
18
|
// Default size for selection history (not including current selection)
|
|
@@ -163,7 +166,7 @@ function convertWPBlockSelectionToSelection(
|
|
|
163
166
|
const offset = selection.offset ?? 0;
|
|
164
167
|
const relativePosition = Y.createRelativePositionFromTypeIndex(
|
|
165
168
|
changedYText,
|
|
166
|
-
offset
|
|
169
|
+
richTextOffsetToHtmlIndex( changedYText.toString(), offset )
|
|
167
170
|
);
|
|
168
171
|
|
|
169
172
|
return {
|
package/src/utils/crdt-blocks.ts
CHANGED
|
@@ -62,6 +62,37 @@ export type YBlockAttributes = Y.Map< Y.Text | unknown >;
|
|
|
62
62
|
|
|
63
63
|
const serializableBlocksCache = new WeakMap< WeakKey, Block[] >();
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Recursively walk an attribute value and convert any RichTextData instances
|
|
67
|
+
* to their string (HTML) representation. This is necessary for array-type and
|
|
68
|
+
* object-type attributes, which can contain nested RichTextData.
|
|
69
|
+
*
|
|
70
|
+
* @param value The attribute value to serialize.
|
|
71
|
+
* @return The value with all RichTextData instances replaced by strings.
|
|
72
|
+
*/
|
|
73
|
+
function serializeAttributeValue( value: unknown ): unknown {
|
|
74
|
+
if ( value instanceof RichTextData ) {
|
|
75
|
+
return value.valueOf();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// e.g. core/table `body`: [ { cells: [ { content: RichTextData } ] } ]
|
|
79
|
+
if ( Array.isArray( value ) ) {
|
|
80
|
+
return value.map( serializeAttributeValue );
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// e.g. a single row inside core/table `body`: { cells: [ ... ] }
|
|
84
|
+
if ( value && typeof value === 'object' ) {
|
|
85
|
+
const result: Record< string, unknown > = {};
|
|
86
|
+
|
|
87
|
+
for ( const [ k, v ] of Object.entries( value ) ) {
|
|
88
|
+
result[ k ] = serializeAttributeValue( v );
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
|
|
65
96
|
function makeBlockAttributesSerializable(
|
|
66
97
|
blockName: string,
|
|
67
98
|
attributes: BlockAttributes
|
|
@@ -73,9 +104,7 @@ function makeBlockAttributesSerializable(
|
|
|
73
104
|
continue;
|
|
74
105
|
}
|
|
75
106
|
|
|
76
|
-
|
|
77
|
-
newAttributes[ key ] = value.valueOf();
|
|
78
|
-
}
|
|
107
|
+
newAttributes[ key ] = serializeAttributeValue( value );
|
|
79
108
|
}
|
|
80
109
|
return newAttributes;
|
|
81
110
|
}
|
|
@@ -18,7 +18,10 @@ import {
|
|
|
18
18
|
type YFullSelection,
|
|
19
19
|
type YSelection,
|
|
20
20
|
} from './block-selection-history';
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
findBlockByClientIdInDoc,
|
|
23
|
+
htmlIndexToRichTextOffset,
|
|
24
|
+
} from './crdt-utils';
|
|
22
25
|
import type { WPBlockSelection, WPSelection } from '../types';
|
|
23
26
|
|
|
24
27
|
// WeakMap to store BlockSelectionHistory instances per Y.Doc
|
|
@@ -74,7 +77,10 @@ function convertYSelectionToBlockSelection(
|
|
|
74
77
|
return {
|
|
75
78
|
clientId,
|
|
76
79
|
attributeKey,
|
|
77
|
-
offset:
|
|
80
|
+
offset: htmlIndexToRichTextOffset(
|
|
81
|
+
absolutePosition.type.toString(),
|
|
82
|
+
absolutePosition.index
|
|
83
|
+
),
|
|
78
84
|
};
|
|
79
85
|
}
|
|
80
86
|
} else if ( ySelection.type === YSelectionType.BlockSelection ) {
|
|
@@ -12,7 +12,7 @@ import { store as blockEditorStore } from '@wordpress/block-editor';
|
|
|
12
12
|
import { CRDT_RECORD_MAP_KEY } from '../sync';
|
|
13
13
|
import type { YPostRecord } from './crdt';
|
|
14
14
|
import type { YBlock, YBlocks } from './crdt-blocks';
|
|
15
|
-
import { getRootMap } from './crdt-utils';
|
|
15
|
+
import { getRootMap, richTextOffsetToHtmlIndex } from './crdt-utils';
|
|
16
16
|
import type {
|
|
17
17
|
AbsoluteBlockIndexPath,
|
|
18
18
|
WPBlockSelection,
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
SelectionInOneBlock,
|
|
23
23
|
SelectionInMultipleBlocks,
|
|
24
24
|
SelectionWholeBlock,
|
|
25
|
+
SelectionDirection,
|
|
25
26
|
CursorPosition,
|
|
26
27
|
} from '../types';
|
|
27
28
|
|
|
@@ -44,16 +45,20 @@ export enum SelectionType {
|
|
|
44
45
|
* differ between the block-editor store and the Yjs document (e.g. in "Show
|
|
45
46
|
* Template" mode).
|
|
46
47
|
*
|
|
47
|
-
* @param selectionStart
|
|
48
|
-
* @param selectionEnd
|
|
49
|
-
* @param yDoc
|
|
48
|
+
* @param selectionStart - The start position of the selection
|
|
49
|
+
* @param selectionEnd - The end position of the selection
|
|
50
|
+
* @param yDoc - The Yjs document
|
|
51
|
+
* @param options - Optional parameters
|
|
52
|
+
* @param options.selectionDirection - The direction of the selection (forward or backward)
|
|
50
53
|
* @return The SelectionState
|
|
51
54
|
*/
|
|
52
55
|
export function getSelectionState(
|
|
53
56
|
selectionStart: WPBlockSelection,
|
|
54
57
|
selectionEnd: WPBlockSelection,
|
|
55
|
-
yDoc: Y.Doc
|
|
58
|
+
yDoc: Y.Doc,
|
|
59
|
+
options?: { selectionDirection?: SelectionDirection }
|
|
56
60
|
): SelectionState {
|
|
61
|
+
const { selectionDirection } = options ?? {};
|
|
57
62
|
const ymap = getRootMap< YPostRecord >( yDoc, CRDT_RECORD_MAP_KEY );
|
|
58
63
|
const yBlocks = ymap.get( 'blocks' );
|
|
59
64
|
|
|
@@ -122,6 +127,7 @@ export function getSelectionState(
|
|
|
122
127
|
type: SelectionType.SelectionInOneBlock,
|
|
123
128
|
cursorStartPosition,
|
|
124
129
|
cursorEndPosition,
|
|
130
|
+
selectionDirection,
|
|
125
131
|
};
|
|
126
132
|
}
|
|
127
133
|
|
|
@@ -137,6 +143,7 @@ export function getSelectionState(
|
|
|
137
143
|
type: SelectionType.SelectionInMultipleBlocks,
|
|
138
144
|
cursorStartPosition,
|
|
139
145
|
cursorEndPosition,
|
|
146
|
+
selectionDirection,
|
|
140
147
|
};
|
|
141
148
|
}
|
|
142
149
|
|
|
@@ -171,7 +178,7 @@ function getCursorPosition(
|
|
|
171
178
|
|
|
172
179
|
const relativePosition = Y.createRelativePositionFromTypeIndex(
|
|
173
180
|
currentYText,
|
|
174
|
-
selection.offset
|
|
181
|
+
richTextOffsetToHtmlIndex( currentYText.toString(), selection.offset )
|
|
175
182
|
);
|
|
176
183
|
|
|
177
184
|
return {
|
|
@@ -315,7 +322,9 @@ export function areSelectionsStatesEqual(
|
|
|
315
322
|
areCursorPositionsEqual(
|
|
316
323
|
selection1.cursorEndPosition,
|
|
317
324
|
( selection2 as SelectionInOneBlock ).cursorEndPosition
|
|
318
|
-
)
|
|
325
|
+
) &&
|
|
326
|
+
selection1.selectionDirection ===
|
|
327
|
+
( selection2 as SelectionInOneBlock ).selectionDirection
|
|
319
328
|
);
|
|
320
329
|
|
|
321
330
|
case SelectionType.SelectionInMultipleBlocks:
|
|
@@ -329,7 +338,10 @@ export function areSelectionsStatesEqual(
|
|
|
329
338
|
selection1.cursorEndPosition,
|
|
330
339
|
( selection2 as SelectionInMultipleBlocks )
|
|
331
340
|
.cursorEndPosition
|
|
332
|
-
)
|
|
341
|
+
) &&
|
|
342
|
+
selection1.selectionDirection ===
|
|
343
|
+
( selection2 as SelectionInMultipleBlocks )
|
|
344
|
+
.selectionDirection
|
|
333
345
|
);
|
|
334
346
|
case SelectionType.WholeBlock:
|
|
335
347
|
return Y.compareRelativePositions(
|