@wordpress/core-data 7.41.2-next.v.202603161435.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/build/awareness/post-editor-awareness.cjs +12 -5
- package/build/awareness/post-editor-awareness.cjs.map +2 -2
- package/build/entities.cjs +30 -5
- package/build/entities.cjs.map +2 -2
- package/build/hooks/use-post-editor-awareness-state.cjs +1 -1
- package/build/hooks/use-post-editor-awareness-state.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 +14 -3
- package/build/queried-data/selectors.cjs.map +2 -2
- package/build/reducer.cjs +6 -0
- package/build/reducer.cjs.map +2 -2
- package/build/sync.cjs +3 -0
- package/build/sync.cjs.map +2 -2
- package/build/types.cjs.map +2 -2
- 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 +1 -1
- 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-module/awareness/post-editor-awareness.mjs +12 -5
- package/build-module/awareness/post-editor-awareness.mjs.map +2 -2
- package/build-module/entities.mjs +30 -5
- package/build-module/entities.mjs.map +2 -2
- package/build-module/hooks/use-post-editor-awareness-state.mjs +1 -1
- package/build-module/hooks/use-post-editor-awareness-state.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 +14 -3
- 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/sync.mjs +2 -0
- package/build-module/sync.mjs.map +2 -2
- package/build-module/types.mjs.map +2 -2
- 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 +2 -2
- 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-types/awareness/post-editor-awareness.d.ts +2 -2
- package/build-types/awareness/post-editor-awareness.d.ts.map +1 -1
- package/build-types/entities.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/sync.d.ts +2 -2
- package/build-types/sync.d.ts.map +1 -1
- package/build-types/types.d.ts +4 -2
- 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 +1 -2
- 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/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/awareness/post-editor-awareness.ts +13 -6
- package/src/awareness/test/post-editor-awareness.ts +15 -10
- package/src/entities.js +36 -5
- package/src/hooks/test/use-post-editor-awareness-state.ts +3 -3
- package/src/hooks/use-post-editor-awareness-state.ts +1 -1
- package/src/queried-data/get-query-parts.js +13 -0
- package/src/queried-data/selectors.js +22 -4
- package/src/queried-data/test/get-query-parts.js +34 -0
- package/src/queried-data/test/selectors.js +158 -0
- package/src/reducer.js +11 -0
- package/src/sync.ts +2 -0
- package/src/test/entities.js +185 -1
- package/src/types.ts +8 -2
- 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 +13 -13
- package/src/utils/crdt-utils.ts +99 -0
- package/src/utils/crdt.ts +8 -30
- 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
|
@@ -11,6 +11,7 @@ describe( 'getQueryParts', () => {
|
|
|
11
11
|
context: 'default',
|
|
12
12
|
page: 2,
|
|
13
13
|
perPage: 2,
|
|
14
|
+
offset: undefined,
|
|
14
15
|
stableKey: '',
|
|
15
16
|
fields: null,
|
|
16
17
|
include: null,
|
|
@@ -28,6 +29,7 @@ describe( 'getQueryParts', () => {
|
|
|
28
29
|
context: 'default',
|
|
29
30
|
page: 1,
|
|
30
31
|
perPage: 10,
|
|
32
|
+
offset: undefined,
|
|
31
33
|
stableKey: 'include=1',
|
|
32
34
|
fields: null,
|
|
33
35
|
include: [ 1 ],
|
|
@@ -43,6 +45,7 @@ describe( 'getQueryParts', () => {
|
|
|
43
45
|
context: 'default',
|
|
44
46
|
page: 1,
|
|
45
47
|
perPage: 10,
|
|
48
|
+
offset: undefined,
|
|
46
49
|
stableKey: '%3F=%26&b=2',
|
|
47
50
|
fields: null,
|
|
48
51
|
include: null,
|
|
@@ -56,6 +59,7 @@ describe( 'getQueryParts', () => {
|
|
|
56
59
|
context: 'default',
|
|
57
60
|
page: 1,
|
|
58
61
|
perPage: 10,
|
|
62
|
+
offset: undefined,
|
|
59
63
|
stableKey: 'a%5B0%5D=1&a%5B1%5D=2',
|
|
60
64
|
fields: null,
|
|
61
65
|
include: null,
|
|
@@ -71,6 +75,7 @@ describe( 'getQueryParts', () => {
|
|
|
71
75
|
context: 'default',
|
|
72
76
|
page: 1,
|
|
73
77
|
perPage: 10,
|
|
78
|
+
offset: undefined,
|
|
74
79
|
stableKey: 'b=2',
|
|
75
80
|
fields: null,
|
|
76
81
|
include: null,
|
|
@@ -84,6 +89,7 @@ describe( 'getQueryParts', () => {
|
|
|
84
89
|
context: 'default',
|
|
85
90
|
page: 1,
|
|
86
91
|
perPage: -1,
|
|
92
|
+
offset: undefined,
|
|
87
93
|
stableKey: 'b=2',
|
|
88
94
|
fields: null,
|
|
89
95
|
include: null,
|
|
@@ -97,6 +103,7 @@ describe( 'getQueryParts', () => {
|
|
|
97
103
|
context: 'default',
|
|
98
104
|
page: 1,
|
|
99
105
|
perPage: 10,
|
|
106
|
+
offset: undefined,
|
|
100
107
|
stableKey: '_fields=id%2Ctitle',
|
|
101
108
|
fields: [ 'id', 'title' ],
|
|
102
109
|
include: null,
|
|
@@ -109,10 +116,37 @@ describe( 'getQueryParts', () => {
|
|
|
109
116
|
expect( parts ).toEqual( {
|
|
110
117
|
page: 1,
|
|
111
118
|
perPage: 10,
|
|
119
|
+
offset: undefined,
|
|
112
120
|
stableKey: '',
|
|
113
121
|
include: null,
|
|
114
122
|
fields: null,
|
|
115
123
|
context: 'view',
|
|
116
124
|
} );
|
|
117
125
|
} );
|
|
126
|
+
|
|
127
|
+
it( 'extracts offset and includes it in stableKey', () => {
|
|
128
|
+
const parts = getQueryParts( {
|
|
129
|
+
per_page: 50,
|
|
130
|
+
offset: 100,
|
|
131
|
+
} );
|
|
132
|
+
|
|
133
|
+
expect( parts ).toEqual( {
|
|
134
|
+
context: 'default',
|
|
135
|
+
page: 1,
|
|
136
|
+
perPage: 50,
|
|
137
|
+
offset: 100,
|
|
138
|
+
stableKey: 'offset=100',
|
|
139
|
+
fields: null,
|
|
140
|
+
include: null,
|
|
141
|
+
} );
|
|
142
|
+
} );
|
|
143
|
+
|
|
144
|
+
it( 'ignores non-numeric offset values', () => {
|
|
145
|
+
const parts = getQueryParts( {
|
|
146
|
+
per_page: 10,
|
|
147
|
+
offset: 'abc',
|
|
148
|
+
} );
|
|
149
|
+
|
|
150
|
+
expect( parts.offset ).toBeUndefined();
|
|
151
|
+
} );
|
|
118
152
|
} );
|
|
@@ -264,4 +264,162 @@ describe( 'getQueriedItems', () => {
|
|
|
264
264
|
const result = getQueriedItems( state, { per_page: 3 } );
|
|
265
265
|
expect( result ).toBe( null );
|
|
266
266
|
} );
|
|
267
|
+
|
|
268
|
+
it( 'should return items for offset-based query on the last partial page', () => {
|
|
269
|
+
// Infinite scroll scenario: 103 total items, perPage=50, and the
|
|
270
|
+
// last batch starts at offset=100. The API returns 3 items (items
|
|
271
|
+
// 101-103). X-WP-Total is 103 (the global count). The selector
|
|
272
|
+
// should recognise this as a complete response since
|
|
273
|
+
// 103 - 100 = 3 expected items.
|
|
274
|
+
const state = {
|
|
275
|
+
items: {
|
|
276
|
+
default: {
|
|
277
|
+
101: { id: 101 },
|
|
278
|
+
102: { id: 102 },
|
|
279
|
+
103: { id: 103 },
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
itemIsComplete: {
|
|
283
|
+
default: { 101: true, 102: true, 103: true },
|
|
284
|
+
},
|
|
285
|
+
queries: {
|
|
286
|
+
default: {
|
|
287
|
+
'offset=100': {
|
|
288
|
+
itemIds: [ 101, 102, 103 ],
|
|
289
|
+
meta: { totalItems: 103 },
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const result = getQueriedItems( state, {
|
|
296
|
+
per_page: 50,
|
|
297
|
+
offset: 100,
|
|
298
|
+
} );
|
|
299
|
+
expect( result ).toEqual( [ { id: 101 }, { id: 102 }, { id: 103 } ] );
|
|
300
|
+
} );
|
|
301
|
+
|
|
302
|
+
it( 'should return null for offset-based query when items are still missing', () => {
|
|
303
|
+
// Offset=50, perPage=50, totalItems=200: the API should return
|
|
304
|
+
// 50 items for this batch but only 2 are stored so far.
|
|
305
|
+
const state = {
|
|
306
|
+
items: {
|
|
307
|
+
default: {
|
|
308
|
+
51: { id: 51 },
|
|
309
|
+
52: { id: 52 },
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
itemIsComplete: {
|
|
313
|
+
default: { 51: true, 52: true },
|
|
314
|
+
},
|
|
315
|
+
queries: {
|
|
316
|
+
default: {
|
|
317
|
+
'offset=50': {
|
|
318
|
+
itemIds: [ 51, 52 ],
|
|
319
|
+
meta: { totalItems: 200 },
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const result = getQueriedItems( state, {
|
|
326
|
+
per_page: 50,
|
|
327
|
+
offset: 50,
|
|
328
|
+
} );
|
|
329
|
+
expect( result ).toBe( null );
|
|
330
|
+
} );
|
|
331
|
+
|
|
332
|
+
it( 'should return null for offset query when items are still missing', () => {
|
|
333
|
+
// Query Block scenario: offset=3 with per_page=10. The effective
|
|
334
|
+
// total is totalItems - offset = 47. Only 5 items are stored, so
|
|
335
|
+
// the data is still incomplete.
|
|
336
|
+
const state = {
|
|
337
|
+
items: {
|
|
338
|
+
default: {
|
|
339
|
+
4: { id: 4 },
|
|
340
|
+
5: { id: 5 },
|
|
341
|
+
6: { id: 6 },
|
|
342
|
+
7: { id: 7 },
|
|
343
|
+
8: { id: 8 },
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
itemIsComplete: {
|
|
347
|
+
default: { 4: true, 5: true, 6: true, 7: true, 8: true },
|
|
348
|
+
},
|
|
349
|
+
queries: {
|
|
350
|
+
default: {
|
|
351
|
+
'offset=3': {
|
|
352
|
+
itemIds: [ 4, 5, 6, 7, 8 ],
|
|
353
|
+
meta: { totalItems: 50 },
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const result = getQueriedItems( state, {
|
|
360
|
+
per_page: 10,
|
|
361
|
+
offset: 3,
|
|
362
|
+
} );
|
|
363
|
+
expect( result ).toBe( null );
|
|
364
|
+
} );
|
|
365
|
+
|
|
366
|
+
it( 'should treat offset=0 the same as no offset', () => {
|
|
367
|
+
// The Query Block defaults to offset=0. Since
|
|
368
|
+
// effectiveTotal = totalItems - 0 = totalItems, this should
|
|
369
|
+
// behave identically to a query without offset.
|
|
370
|
+
const state = {
|
|
371
|
+
items: {
|
|
372
|
+
default: {
|
|
373
|
+
1: { id: 1 },
|
|
374
|
+
2: { id: 2 },
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
itemIsComplete: {
|
|
378
|
+
default: { 1: true, 2: true },
|
|
379
|
+
},
|
|
380
|
+
queries: {
|
|
381
|
+
default: {
|
|
382
|
+
'offset=0': {
|
|
383
|
+
itemIds: [ 1, 2 ],
|
|
384
|
+
meta: { totalItems: 5 },
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// 2 items stored, but 5 total exist — should return null.
|
|
391
|
+
const result = getQueriedItems( state, {
|
|
392
|
+
per_page: 3,
|
|
393
|
+
offset: 0,
|
|
394
|
+
} );
|
|
395
|
+
expect( result ).toBe( null );
|
|
396
|
+
} );
|
|
397
|
+
|
|
398
|
+
it( 'should return empty array when offset equals totalItems', () => {
|
|
399
|
+
// Edge case: offset lands exactly at the end (e.g. 84 items,
|
|
400
|
+
// per_page=7, offset=84). The API returns 0 items and that is
|
|
401
|
+
// a complete response — effectiveTotal is 0.
|
|
402
|
+
const state = {
|
|
403
|
+
items: {
|
|
404
|
+
default: {},
|
|
405
|
+
},
|
|
406
|
+
itemIsComplete: {
|
|
407
|
+
default: {},
|
|
408
|
+
},
|
|
409
|
+
queries: {
|
|
410
|
+
default: {
|
|
411
|
+
'offset=84': {
|
|
412
|
+
itemIds: [],
|
|
413
|
+
meta: { totalItems: 84 },
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const result = getQueriedItems( state, {
|
|
420
|
+
per_page: 7,
|
|
421
|
+
offset: 84,
|
|
422
|
+
} );
|
|
423
|
+
expect( result ).toEqual( [] );
|
|
424
|
+
} );
|
|
267
425
|
} );
|
package/src/reducer.js
CHANGED
|
@@ -16,6 +16,7 @@ import { createUndoManager } from '@wordpress/undo-manager';
|
|
|
16
16
|
import { ifMatchingAction, replaceAction } from './utils';
|
|
17
17
|
import { reducer as queriedDataReducer } from './queried-data';
|
|
18
18
|
import { rootEntitiesConfig, DEFAULT_ENTITY_KEY } from './entities';
|
|
19
|
+
import { ConnectionErrorCode } from './sync';
|
|
19
20
|
|
|
20
21
|
/** @typedef {import('./types').AnyFunction} AnyFunction */
|
|
21
22
|
|
|
@@ -706,6 +707,16 @@ export function collaborationSupported( state = true, action ) {
|
|
|
706
707
|
switch ( action.type ) {
|
|
707
708
|
case 'SET_COLLABORATION_SUPPORTED':
|
|
708
709
|
return action.supported;
|
|
710
|
+
|
|
711
|
+
case 'SET_SYNC_CONNECTION_STATUS':
|
|
712
|
+
if (
|
|
713
|
+
ConnectionErrorCode.DOCUMENT_SIZE_LIMIT_EXCEEDED ===
|
|
714
|
+
action.status?.error?.code
|
|
715
|
+
) {
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return state;
|
|
709
720
|
}
|
|
710
721
|
return state;
|
|
711
722
|
}
|
package/src/sync.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { unlock } from './lock-unlock';
|
|
13
13
|
|
|
14
14
|
const {
|
|
15
|
+
ConnectionErrorCode,
|
|
15
16
|
createSyncManager,
|
|
16
17
|
Delta,
|
|
17
18
|
CRDT_DOC_META_PERSISTENCE_KEY,
|
|
@@ -22,6 +23,7 @@ const {
|
|
|
22
23
|
} = unlock( syncPrivateApis );
|
|
23
24
|
|
|
24
25
|
export {
|
|
26
|
+
ConnectionErrorCode,
|
|
25
27
|
Delta,
|
|
26
28
|
CRDT_DOC_META_PERSISTENCE_KEY,
|
|
27
29
|
CRDT_RECORD_MAP_KEY,
|
package/src/test/entities.js
CHANGED
|
@@ -8,6 +8,10 @@ jest.mock( '../sync', () => ( {
|
|
|
8
8
|
...jest.requireActual( '../sync' ),
|
|
9
9
|
getSyncManager: jest.fn(),
|
|
10
10
|
} ) );
|
|
11
|
+
jest.mock( '../utils/crdt', () => ( {
|
|
12
|
+
...jest.requireActual( '../utils/crdt' ),
|
|
13
|
+
applyPostChangesToCRDTDoc: jest.fn(),
|
|
14
|
+
} ) );
|
|
11
15
|
|
|
12
16
|
/**
|
|
13
17
|
* Internal dependencies
|
|
@@ -19,7 +23,10 @@ import {
|
|
|
19
23
|
additionalEntityConfigLoaders,
|
|
20
24
|
} from '../entities';
|
|
21
25
|
import { getSyncManager } from '../sync';
|
|
22
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
applyPostChangesToCRDTDoc,
|
|
28
|
+
POST_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
|
|
29
|
+
} from '../utils/crdt';
|
|
23
30
|
|
|
24
31
|
describe( 'getMethodName', () => {
|
|
25
32
|
it( 'should return the right method name for an entity with the root kind', () => {
|
|
@@ -127,6 +134,183 @@ describe( 'prePersistPostType', () => {
|
|
|
127
134
|
} );
|
|
128
135
|
} );
|
|
129
136
|
|
|
137
|
+
describe( 'loadPostTypeEntities', () => {
|
|
138
|
+
let originalCollaborationEnabled;
|
|
139
|
+
|
|
140
|
+
beforeEach( () => {
|
|
141
|
+
apiFetch.mockReset();
|
|
142
|
+
applyPostChangesToCRDTDoc.mockReset();
|
|
143
|
+
originalCollaborationEnabled = window._wpCollaborationEnabled;
|
|
144
|
+
} );
|
|
145
|
+
|
|
146
|
+
afterEach( () => {
|
|
147
|
+
window._wpCollaborationEnabled = originalCollaborationEnabled;
|
|
148
|
+
} );
|
|
149
|
+
|
|
150
|
+
it( 'should include custom taxonomy rest_bases in synced properties when collaboration is enabled', async () => {
|
|
151
|
+
window._wpCollaborationEnabled = true;
|
|
152
|
+
|
|
153
|
+
const mockPostTypes = {
|
|
154
|
+
book: {
|
|
155
|
+
name: 'Books',
|
|
156
|
+
rest_base: 'books',
|
|
157
|
+
rest_namespace: 'wp/v2',
|
|
158
|
+
taxonomies: [ 'genre', 'audience' ],
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
const mockTaxonomies = {
|
|
162
|
+
genre: {
|
|
163
|
+
name: 'Genres',
|
|
164
|
+
rest_base: 'genres',
|
|
165
|
+
rest_namespace: 'wp/v2',
|
|
166
|
+
},
|
|
167
|
+
audience: {
|
|
168
|
+
name: 'Audiences',
|
|
169
|
+
rest_base: 'audiences',
|
|
170
|
+
rest_namespace: 'wp/v2',
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
apiFetch
|
|
175
|
+
.mockResolvedValueOnce( mockPostTypes )
|
|
176
|
+
.mockResolvedValueOnce( mockTaxonomies );
|
|
177
|
+
|
|
178
|
+
const postTypeLoader = additionalEntityConfigLoaders.find(
|
|
179
|
+
( loader ) => loader.kind === 'postType'
|
|
180
|
+
);
|
|
181
|
+
const entities = await postTypeLoader.loadEntities();
|
|
182
|
+
const bookEntity = entities.find( ( e ) => e.name === 'book' );
|
|
183
|
+
|
|
184
|
+
bookEntity.syncConfig.applyChangesToCRDTDoc( {}, {} );
|
|
185
|
+
|
|
186
|
+
expect( applyPostChangesToCRDTDoc ).toHaveBeenCalledWith(
|
|
187
|
+
{},
|
|
188
|
+
{},
|
|
189
|
+
expect.any( Set )
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const syncedProperties = applyPostChangesToCRDTDoc.mock.calls[ 0 ][ 2 ];
|
|
193
|
+
expect( syncedProperties ).toContain( 'genres' );
|
|
194
|
+
expect( syncedProperties ).toContain( 'audiences' );
|
|
195
|
+
} );
|
|
196
|
+
|
|
197
|
+
it( 'should not fetch taxonomies when collaboration is disabled', async () => {
|
|
198
|
+
window._wpCollaborationEnabled = false;
|
|
199
|
+
|
|
200
|
+
const mockPostTypes = {
|
|
201
|
+
post: {
|
|
202
|
+
name: 'Posts',
|
|
203
|
+
rest_base: 'posts',
|
|
204
|
+
rest_namespace: 'wp/v2',
|
|
205
|
+
taxonomies: [ 'category', 'post_tag' ],
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
apiFetch.mockResolvedValueOnce( mockPostTypes );
|
|
210
|
+
|
|
211
|
+
const postTypeLoader = additionalEntityConfigLoaders.find(
|
|
212
|
+
( loader ) => loader.kind === 'postType'
|
|
213
|
+
);
|
|
214
|
+
const entities = await postTypeLoader.loadEntities();
|
|
215
|
+
const postEntity = entities.find( ( e ) => e.name === 'post' );
|
|
216
|
+
|
|
217
|
+
postEntity.syncConfig.applyChangesToCRDTDoc( {}, {} );
|
|
218
|
+
|
|
219
|
+
// Only one apiFetch call (post types), no taxonomy fetch.
|
|
220
|
+
expect( apiFetch ).toHaveBeenCalledTimes( 1 );
|
|
221
|
+
|
|
222
|
+
const syncedProperties = applyPostChangesToCRDTDoc.mock.calls[ 0 ][ 2 ];
|
|
223
|
+
expect( syncedProperties ).not.toContain( 'categories' );
|
|
224
|
+
expect( syncedProperties ).not.toContain( 'tags' );
|
|
225
|
+
} );
|
|
226
|
+
|
|
227
|
+
it( 'should skip taxonomy rest_base when taxonomy is not found in fetched taxonomies', async () => {
|
|
228
|
+
window._wpCollaborationEnabled = true;
|
|
229
|
+
|
|
230
|
+
const mockPostTypes = {
|
|
231
|
+
book: {
|
|
232
|
+
name: 'Books',
|
|
233
|
+
rest_base: 'books',
|
|
234
|
+
rest_namespace: 'wp/v2',
|
|
235
|
+
taxonomies: [ 'genre', 'missing_taxonomy' ],
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
const mockTaxonomies = {
|
|
239
|
+
genre: {
|
|
240
|
+
name: 'Genres',
|
|
241
|
+
rest_base: 'genres',
|
|
242
|
+
rest_namespace: 'wp/v2',
|
|
243
|
+
},
|
|
244
|
+
// 'missing_taxonomy' is intentionally absent.
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
apiFetch
|
|
248
|
+
.mockResolvedValueOnce( mockPostTypes )
|
|
249
|
+
.mockResolvedValueOnce( mockTaxonomies );
|
|
250
|
+
|
|
251
|
+
const postTypeLoader = additionalEntityConfigLoaders.find(
|
|
252
|
+
( loader ) => loader.kind === 'postType'
|
|
253
|
+
);
|
|
254
|
+
const entities = await postTypeLoader.loadEntities();
|
|
255
|
+
const bookEntity = entities.find( ( e ) => e.name === 'book' );
|
|
256
|
+
|
|
257
|
+
bookEntity.syncConfig.applyChangesToCRDTDoc( {}, {} );
|
|
258
|
+
|
|
259
|
+
const syncedProperties = applyPostChangesToCRDTDoc.mock.calls[ 0 ][ 2 ];
|
|
260
|
+
expect( syncedProperties ).toContain( 'genres' );
|
|
261
|
+
// missing_taxonomy has no rest_base entry, so nothing should be added for it.
|
|
262
|
+
expect( syncedProperties.size ).toBe( 16 ); // 15 base + 1 taxonomy (genres)
|
|
263
|
+
} );
|
|
264
|
+
|
|
265
|
+
it( 'should include base synced properties regardless of taxonomies', async () => {
|
|
266
|
+
window._wpCollaborationEnabled = true;
|
|
267
|
+
|
|
268
|
+
const mockPostTypes = {
|
|
269
|
+
page: {
|
|
270
|
+
name: 'Pages',
|
|
271
|
+
rest_base: 'pages',
|
|
272
|
+
rest_namespace: 'wp/v2',
|
|
273
|
+
taxonomies: [],
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
apiFetch
|
|
278
|
+
.mockResolvedValueOnce( mockPostTypes )
|
|
279
|
+
.mockResolvedValueOnce( {} );
|
|
280
|
+
|
|
281
|
+
const postTypeLoader = additionalEntityConfigLoaders.find(
|
|
282
|
+
( loader ) => loader.kind === 'postType'
|
|
283
|
+
);
|
|
284
|
+
const entities = await postTypeLoader.loadEntities();
|
|
285
|
+
const pageEntity = entities.find( ( e ) => e.name === 'page' );
|
|
286
|
+
|
|
287
|
+
pageEntity.syncConfig.applyChangesToCRDTDoc( {}, {} );
|
|
288
|
+
|
|
289
|
+
const syncedProperties = applyPostChangesToCRDTDoc.mock.calls[ 0 ][ 2 ];
|
|
290
|
+
const expectedBase = [
|
|
291
|
+
'author',
|
|
292
|
+
'blocks',
|
|
293
|
+
'content',
|
|
294
|
+
'comment_status',
|
|
295
|
+
'date',
|
|
296
|
+
'excerpt',
|
|
297
|
+
'featured_media',
|
|
298
|
+
'format',
|
|
299
|
+
'meta',
|
|
300
|
+
'ping_status',
|
|
301
|
+
'slug',
|
|
302
|
+
'status',
|
|
303
|
+
'sticky',
|
|
304
|
+
'template',
|
|
305
|
+
'title',
|
|
306
|
+
];
|
|
307
|
+
for ( const prop of expectedBase ) {
|
|
308
|
+
expect( syncedProperties ).toContain( prop );
|
|
309
|
+
}
|
|
310
|
+
expect( syncedProperties.size ).toBe( 15 );
|
|
311
|
+
} );
|
|
312
|
+
} );
|
|
313
|
+
|
|
130
314
|
describe( 'loadTaxonomyEntities', () => {
|
|
131
315
|
beforeEach( () => {
|
|
132
316
|
apiFetch.mockReset();
|
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
|
}
|
|
@@ -127,6 +133,6 @@ export type SelectionState =
|
|
|
127
133
|
| SelectionWholeBlock;
|
|
128
134
|
|
|
129
135
|
export interface ResolvedSelection {
|
|
130
|
-
|
|
136
|
+
richTextOffset: number | null;
|
|
131
137
|
localClientId: string | null;
|
|
132
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,18 +12,18 @@ 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';
|
|
16
|
-
import type {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
15
|
+
import { getRootMap, richTextOffsetToHtmlIndex } from './crdt-utils';
|
|
16
|
+
import type {
|
|
17
|
+
AbsoluteBlockIndexPath,
|
|
18
|
+
WPBlockSelection,
|
|
19
|
+
SelectionState,
|
|
20
|
+
SelectionNone,
|
|
21
|
+
SelectionCursor,
|
|
22
|
+
SelectionInOneBlock,
|
|
23
|
+
SelectionInMultipleBlocks,
|
|
24
|
+
SelectionWholeBlock,
|
|
25
|
+
SelectionDirection,
|
|
26
|
+
CursorPosition,
|
|
27
27
|
} from '../types';
|
|
28
28
|
|
|
29
29
|
/**
|
|
@@ -178,7 +178,7 @@ function getCursorPosition(
|
|
|
178
178
|
|
|
179
179
|
const relativePosition = Y.createRelativePositionFromTypeIndex(
|
|
180
180
|
currentYText,
|
|
181
|
-
selection.offset
|
|
181
|
+
richTextOffsetToHtmlIndex( currentYText.toString(), selection.offset )
|
|
182
182
|
);
|
|
183
183
|
|
|
184
184
|
return {
|