@wordpress/core-data 7.48.0 → 7.48.1

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.
Files changed (99) hide show
  1. package/CHANGELOG.md +7 -1
  2. package/build/awareness/block-lookup.cjs +14 -26
  3. package/build/awareness/block-lookup.cjs.map +2 -2
  4. package/build/awareness/post-editor-awareness.cjs +4 -3
  5. package/build/awareness/post-editor-awareness.cjs.map +2 -2
  6. package/build/entities.cjs +4 -2
  7. package/build/entities.cjs.map +2 -2
  8. package/build/hooks/use-post-editor-awareness-state.cjs +8 -2
  9. package/build/hooks/use-post-editor-awareness-state.cjs.map +2 -2
  10. package/build/private-actions.cjs +8 -0
  11. package/build/private-actions.cjs.map +2 -2
  12. package/build/private-selectors.cjs.map +2 -2
  13. package/build/reducer.cjs +13 -0
  14. package/build/reducer.cjs.map +2 -2
  15. package/build/resolvers.cjs +13 -8
  16. package/build/resolvers.cjs.map +2 -2
  17. package/build/selectors.cjs +7 -0
  18. package/build/selectors.cjs.map +2 -2
  19. package/build/utils/crdt-blocks.cjs +12 -2
  20. package/build/utils/crdt-blocks.cjs.map +2 -2
  21. package/build/utils/crdt.cjs +2 -1
  22. package/build/utils/crdt.cjs.map +2 -2
  23. package/build/utils/index.cjs +3 -0
  24. package/build/utils/index.cjs.map +2 -2
  25. package/build/utils/save-crdt-doc.cjs +75 -0
  26. package/build/utils/save-crdt-doc.cjs.map +7 -0
  27. package/build-module/awareness/block-lookup.mjs +13 -26
  28. package/build-module/awareness/block-lookup.mjs.map +2 -2
  29. package/build-module/awareness/post-editor-awareness.mjs +4 -3
  30. package/build-module/awareness/post-editor-awareness.mjs.map +2 -2
  31. package/build-module/entities.mjs +4 -2
  32. package/build-module/entities.mjs.map +2 -2
  33. package/build-module/hooks/use-post-editor-awareness-state.mjs +9 -3
  34. package/build-module/hooks/use-post-editor-awareness-state.mjs.map +2 -2
  35. package/build-module/private-actions.mjs +7 -0
  36. package/build-module/private-actions.mjs.map +2 -2
  37. package/build-module/private-selectors.mjs.map +2 -2
  38. package/build-module/reducer.mjs +12 -0
  39. package/build-module/reducer.mjs.map +2 -2
  40. package/build-module/resolvers.mjs +15 -9
  41. package/build-module/resolvers.mjs.map +2 -2
  42. package/build-module/selectors.mjs +7 -0
  43. package/build-module/selectors.mjs.map +2 -2
  44. package/build-module/utils/crdt-blocks.mjs +12 -2
  45. package/build-module/utils/crdt-blocks.mjs.map +2 -2
  46. package/build-module/utils/crdt.mjs +2 -1
  47. package/build-module/utils/crdt.mjs.map +2 -2
  48. package/build-module/utils/index.mjs +2 -0
  49. package/build-module/utils/index.mjs.map +2 -2
  50. package/build-module/utils/save-crdt-doc.mjs +40 -0
  51. package/build-module/utils/save-crdt-doc.mjs.map +7 -0
  52. package/build-types/awareness/block-lookup.d.ts +27 -7
  53. package/build-types/awareness/block-lookup.d.ts.map +1 -1
  54. package/build-types/awareness/post-editor-awareness.d.ts +3 -1
  55. package/build-types/awareness/post-editor-awareness.d.ts.map +1 -1
  56. package/build-types/entities.d.ts.map +1 -1
  57. package/build-types/hooks/use-post-editor-awareness-state.d.ts.map +1 -1
  58. package/build-types/private-actions.d.ts +15 -0
  59. package/build-types/private-actions.d.ts.map +1 -1
  60. package/build-types/private-selectors.d.ts +0 -12
  61. package/build-types/private-selectors.d.ts.map +1 -1
  62. package/build-types/reducer.d.ts +15 -0
  63. package/build-types/reducer.d.ts.map +1 -1
  64. package/build-types/resolvers.d.ts.map +1 -1
  65. package/build-types/selectors.d.ts +4 -0
  66. package/build-types/selectors.d.ts.map +1 -1
  67. package/build-types/utils/crdt-blocks.d.ts +5 -1
  68. package/build-types/utils/crdt-blocks.d.ts.map +1 -1
  69. package/build-types/utils/crdt.d.ts.map +1 -1
  70. package/build-types/utils/index.d.ts +1 -0
  71. package/build-types/utils/index.d.ts.map +1 -1
  72. package/build-types/utils/on-sub-key.d.ts +4 -0
  73. package/build-types/utils/on-sub-key.d.ts.map +1 -0
  74. package/build-types/utils/save-crdt-doc.d.ts +8 -0
  75. package/build-types/utils/save-crdt-doc.d.ts.map +1 -0
  76. package/package.json +22 -20
  77. package/src/awareness/block-lookup.ts +21 -62
  78. package/src/awareness/post-editor-awareness.ts +8 -3
  79. package/src/awareness/test/block-lookup.ts +98 -94
  80. package/src/awareness/test/post-editor-awareness.ts +177 -180
  81. package/src/entities.js +9 -3
  82. package/src/hooks/test/use-post-editor-awareness-state.ts +10 -2
  83. package/src/hooks/use-post-editor-awareness-state.ts +20 -7
  84. package/src/private-actions.js +18 -0
  85. package/src/private-selectors.ts +0 -12
  86. package/src/reducer.js +17 -0
  87. package/src/resolvers.js +20 -13
  88. package/src/selectors.ts +11 -0
  89. package/src/test/private-selectors.js +66 -0
  90. package/src/test/reducer.js +44 -0
  91. package/src/test/resolvers.js +121 -113
  92. package/src/test/selectors.js +48 -0
  93. package/src/utils/crdt-blocks.ts +27 -22
  94. package/src/utils/crdt.ts +2 -1
  95. package/src/utils/index.js +1 -0
  96. package/src/utils/save-crdt-doc.js +64 -0
  97. package/src/utils/test/crdt-blocks.ts +57 -2
  98. package/src/utils/test/rtc-rich-text-cursor-scope.test.js +2 -2
  99. package/src/utils/test/save-crdt-doc.js +185 -0
@@ -584,6 +584,14 @@ describe( 'PostEditorAwareness', () => {
584
584
  } );
585
585
 
586
586
  describe( 'convertSelectionStateToAbsolute', () => {
587
+ const defaultEditorBlocks = [
588
+ {
589
+ clientId: 'block-1',
590
+ name: 'core/paragraph',
591
+ innerBlocks: [],
592
+ },
593
+ ];
594
+
587
595
  test( 'should return nulls when relative position cannot be resolved', () => {
588
596
  const awareness = new PostEditorAwareness(
589
597
  doc,
@@ -611,8 +619,10 @@ describe( 'PostEditorAwareness', () => {
611
619
  },
612
620
  };
613
621
 
614
- const result =
615
- awareness.convertSelectionStateToAbsolute( selection );
622
+ const result = awareness.convertSelectionStateToAbsolute(
623
+ selection,
624
+ defaultEditorBlocks
625
+ );
616
626
 
617
627
  // Should return nulls when the relative position's type cannot be found
618
628
  expect( result.richTextOffset ).toBeNull();
@@ -651,8 +661,10 @@ describe( 'PostEditorAwareness', () => {
651
661
  },
652
662
  };
653
663
 
654
- const result =
655
- awareness.convertSelectionStateToAbsolute( selection );
664
+ const result = awareness.convertSelectionStateToAbsolute(
665
+ selection,
666
+ defaultEditorBlocks
667
+ );
656
668
 
657
669
  expect( result.richTextOffset ).toBe( 5 );
658
670
  expect( result.localClientId ).toBe( 'block-1' );
@@ -684,8 +696,10 @@ describe( 'PostEditorAwareness', () => {
684
696
  blockPosition,
685
697
  };
686
698
 
687
- const result =
688
- awareness.convertSelectionStateToAbsolute( selection );
699
+ const result = awareness.convertSelectionStateToAbsolute(
700
+ selection,
701
+ defaultEditorBlocks
702
+ );
689
703
 
690
704
  expect( result.richTextOffset ).toBeNull();
691
705
  expect( result.localClientId ).toBe( 'block-1' );
@@ -700,9 +714,10 @@ describe( 'PostEditorAwareness', () => {
700
714
  123
701
715
  );
702
716
 
703
- const result = awareness.convertSelectionStateToAbsolute( {
704
- type: SelectionType.None,
705
- } );
717
+ const result = awareness.convertSelectionStateToAbsolute(
718
+ { type: SelectionType.None },
719
+ []
720
+ );
706
721
 
707
722
  expect( result.attributeKey ).toBeNull();
708
723
  } );
@@ -737,8 +752,10 @@ describe( 'PostEditorAwareness', () => {
737
752
  },
738
753
  };
739
754
 
740
- const result =
741
- awareness.convertSelectionStateToAbsolute( selection );
755
+ const result = awareness.convertSelectionStateToAbsolute(
756
+ selection,
757
+ defaultEditorBlocks
758
+ );
742
759
 
743
760
  expect( result.attributeKey ).toBe( 'body.0.cells.0.content' );
744
761
  } );
@@ -905,13 +922,23 @@ describe( 'PostEditorAwareness', () => {
905
922
  } ),
906
923
  ] );
907
924
 
908
- mockBlockEditorStore( {
909
- blocks: [
910
- { clientId: 'local-0', innerBlocks: [] },
911
- { clientId: 'local-1', innerBlocks: [] },
912
- { clientId: 'local-2', innerBlocks: [] },
913
- ],
914
- } );
925
+ const editorBlocks = [
926
+ {
927
+ clientId: 'local-0',
928
+ name: 'core/paragraph',
929
+ innerBlocks: [],
930
+ },
931
+ {
932
+ clientId: 'local-1',
933
+ name: 'core/paragraph',
934
+ innerBlocks: [],
935
+ },
936
+ {
937
+ clientId: 'local-2',
938
+ name: 'core/paragraph',
939
+ innerBlocks: [],
940
+ },
941
+ ];
915
942
 
916
943
  const awareness = new PostEditorAwareness(
917
944
  nestedDoc,
@@ -942,8 +969,10 @@ describe( 'PostEditorAwareness', () => {
942
969
  },
943
970
  };
944
971
 
945
- const result =
946
- awareness.convertSelectionStateToAbsolute( selection );
972
+ const result = awareness.convertSelectionStateToAbsolute(
973
+ selection,
974
+ editorBlocks
975
+ );
947
976
 
948
977
  expect( result.richTextOffset ).toBe( 2 );
949
978
  expect( result.localClientId ).toBe( 'local-2' );
@@ -968,17 +997,24 @@ describe( 'PostEditorAwareness', () => {
968
997
 
969
998
  const nestedDoc = createTestDocWithBlocks( [ outerColumn ] );
970
999
 
971
- mockBlockEditorStore( {
972
- blocks: [
973
- {
974
- clientId: 'local-outer',
975
- innerBlocks: [
976
- { clientId: 'local-inner-0', innerBlocks: [] },
977
- { clientId: 'local-inner-1', innerBlocks: [] },
978
- ],
979
- },
980
- ],
981
- } );
1000
+ const editorBlocks = [
1001
+ {
1002
+ clientId: 'local-outer',
1003
+ name: 'core/column',
1004
+ innerBlocks: [
1005
+ {
1006
+ clientId: 'local-inner-0',
1007
+ name: 'core/paragraph',
1008
+ innerBlocks: [],
1009
+ },
1010
+ {
1011
+ clientId: 'local-inner-1',
1012
+ name: 'core/paragraph',
1013
+ innerBlocks: [],
1014
+ },
1015
+ ],
1016
+ },
1017
+ ];
982
1018
 
983
1019
  const awareness = new PostEditorAwareness(
984
1020
  nestedDoc,
@@ -1015,8 +1051,10 @@ describe( 'PostEditorAwareness', () => {
1015
1051
  },
1016
1052
  };
1017
1053
 
1018
- const result =
1019
- awareness.convertSelectionStateToAbsolute( selection );
1054
+ const result = awareness.convertSelectionStateToAbsolute(
1055
+ selection,
1056
+ editorBlocks
1057
+ );
1020
1058
 
1021
1059
  expect( result.richTextOffset ).toBe( 5 );
1022
1060
  expect( result.localClientId ).toBe( 'local-inner-1' );
@@ -1032,16 +1070,19 @@ describe( 'PostEditorAwareness', () => {
1032
1070
 
1033
1071
  const nestedDoc = createTestDocWithBlocks( [ outerColumn ] );
1034
1072
 
1035
- mockBlockEditorStore( {
1036
- blocks: [
1037
- {
1038
- clientId: 'local-col',
1039
- innerBlocks: [
1040
- { clientId: 'local-img', innerBlocks: [] },
1041
- ],
1042
- },
1043
- ],
1044
- } );
1073
+ const editorBlocks = [
1074
+ {
1075
+ clientId: 'local-col',
1076
+ name: 'core/column',
1077
+ innerBlocks: [
1078
+ {
1079
+ clientId: 'local-img',
1080
+ name: 'core/image',
1081
+ innerBlocks: [],
1082
+ },
1083
+ ],
1084
+ },
1085
+ ];
1045
1086
 
1046
1087
  const awareness = new PostEditorAwareness(
1047
1088
  nestedDoc,
@@ -1070,8 +1111,10 @@ describe( 'PostEditorAwareness', () => {
1070
1111
  blockPosition,
1071
1112
  };
1072
1113
 
1073
- const result =
1074
- awareness.convertSelectionStateToAbsolute( selection );
1114
+ const result = awareness.convertSelectionStateToAbsolute(
1115
+ selection,
1116
+ editorBlocks
1117
+ );
1075
1118
 
1076
1119
  expect( result.richTextOffset ).toBeNull();
1077
1120
  expect( result.localClientId ).toBe( 'local-img' );
@@ -1103,29 +1146,35 @@ describe( 'PostEditorAwareness', () => {
1103
1146
  outerColumns1,
1104
1147
  ] );
1105
1148
 
1106
- mockBlockEditorStore( {
1107
- blocks: [
1108
- { clientId: 'local-outer-0', innerBlocks: [] },
1109
- {
1110
- clientId: 'local-outer-1',
1111
- innerBlocks: [
1112
- {
1113
- clientId: 'local-mid',
1114
- innerBlocks: [
1115
- {
1116
- clientId: 'local-deep-0',
1117
- innerBlocks: [],
1118
- },
1119
- {
1120
- clientId: 'local-deep-1',
1121
- innerBlocks: [],
1122
- },
1123
- ],
1124
- },
1125
- ],
1126
- },
1127
- ],
1128
- } );
1149
+ const editorBlocks = [
1150
+ {
1151
+ clientId: 'local-outer-0',
1152
+ name: 'core/columns',
1153
+ innerBlocks: [],
1154
+ },
1155
+ {
1156
+ clientId: 'local-outer-1',
1157
+ name: 'core/columns',
1158
+ innerBlocks: [
1159
+ {
1160
+ clientId: 'local-mid',
1161
+ name: 'core/column',
1162
+ innerBlocks: [
1163
+ {
1164
+ clientId: 'local-deep-0',
1165
+ name: 'core/paragraph',
1166
+ innerBlocks: [],
1167
+ },
1168
+ {
1169
+ clientId: 'local-deep-1',
1170
+ name: 'core/paragraph',
1171
+ innerBlocks: [],
1172
+ },
1173
+ ],
1174
+ },
1175
+ ],
1176
+ },
1177
+ ];
1129
1178
 
1130
1179
  const awareness = new PostEditorAwareness(
1131
1180
  nestedDoc,
@@ -1164,8 +1213,10 @@ describe( 'PostEditorAwareness', () => {
1164
1213
  },
1165
1214
  };
1166
1215
 
1167
- const result =
1168
- awareness.convertSelectionStateToAbsolute( selection );
1216
+ const result = awareness.convertSelectionStateToAbsolute(
1217
+ selection,
1218
+ editorBlocks
1219
+ );
1169
1220
 
1170
1221
  expect( result.richTextOffset ).toBe( 7 );
1171
1222
  expect( result.localClientId ).toBe( 'local-deep-1' );
@@ -1175,6 +1226,14 @@ describe( 'PostEditorAwareness', () => {
1175
1226
  } );
1176
1227
 
1177
1228
  describe( 'convertSelectionStateToAbsolute with nested rich-text attributes', () => {
1229
+ const editorBlocks = [
1230
+ {
1231
+ clientId: 'local-nested-attrs',
1232
+ name: 'test/nested-rich-text',
1233
+ innerBlocks: [],
1234
+ },
1235
+ ];
1236
+
1178
1237
  test.each( NESTED_SELECTION_SEEDS )(
1179
1238
  'resolves fuzzed nested rich-text cursor (seed %i)',
1180
1239
  ( seed ) => {
@@ -1185,15 +1244,6 @@ describe( 'PostEditorAwareness', () => {
1185
1244
  );
1186
1245
  const nestedDoc = createTestDocWithBlocks( [ block ] );
1187
1246
 
1188
- mockBlockEditorStore( {
1189
- blocks: [
1190
- {
1191
- clientId: 'local-nested-attrs',
1192
- innerBlocks: [],
1193
- },
1194
- ],
1195
- } );
1196
-
1197
1247
  const target = rng.pick( targets );
1198
1248
  const initialOffset = rng.intBetween(
1199
1249
  1,
@@ -1233,8 +1283,10 @@ describe( 'PostEditorAwareness', () => {
1233
1283
  },
1234
1284
  };
1235
1285
 
1236
- const result =
1237
- awareness.convertSelectionStateToAbsolute( selection );
1286
+ const result = awareness.convertSelectionStateToAbsolute(
1287
+ selection,
1288
+ editorBlocks
1289
+ );
1238
1290
 
1239
1291
  expect( result.richTextOffset ).toBe( expectedOffset );
1240
1292
  expect( result.localClientId ).toBe( 'local-nested-attrs' );
@@ -1244,9 +1296,8 @@ describe( 'PostEditorAwareness', () => {
1244
1296
  );
1245
1297
  } );
1246
1298
 
1247
- describe( 'template mode (core/post-content handling)', () => {
1248
- test( 'should resolve cursor when getBlocks returns template tree with core/post-content', () => {
1249
- // Yjs doc has only the post content blocks (no template wrapper)
1299
+ describe( 'post content blocks resolution', () => {
1300
+ test( 'should resolve cursor with post content blocks', () => {
1250
1301
  const templateDoc = createTestDocWithBlocks( [
1251
1302
  createYBlock( 'yjs-para-0', 'core/paragraph', {
1252
1303
  textContent: 'Post paragraph 1',
@@ -1256,55 +1307,20 @@ describe( 'PostEditorAwareness', () => {
1256
1307
  } ),
1257
1308
  ] );
1258
1309
 
1259
- // In template mode, getBlocks() returns the full template tree.
1260
- // The Yjs paths are relative to post content, so the receiver needs
1261
- // to find core/post-content and navigate from there.
1262
- const postContentClientId = 'local-post-content';
1263
- const mockGetBlocks = jest
1264
- .fn()
1265
- .mockImplementation( ( rootClientId?: string ) => {
1266
- if ( rootClientId === postContentClientId ) {
1267
- // Controlled inner blocks of core/post-content
1268
- return [
1269
- {
1270
- clientId: 'local-para-0',
1271
- name: 'core/paragraph',
1272
- innerBlocks: [],
1273
- },
1274
- {
1275
- clientId: 'local-para-1',
1276
- name: 'core/paragraph',
1277
- innerBlocks: [],
1278
- },
1279
- ];
1280
- }
1281
- // Full template tree
1282
- return [
1283
- {
1284
- clientId: 'local-header',
1285
- name: 'core/template-part',
1286
- innerBlocks: [],
1287
- },
1288
- {
1289
- clientId: 'local-group',
1290
- name: 'core/group',
1291
- innerBlocks: [
1292
- {
1293
- clientId: postContentClientId,
1294
- name: 'core/post-content',
1295
- innerBlocks: [], // empty because they're controlled inner blocks
1296
- },
1297
- ],
1298
- },
1299
- {
1300
- clientId: 'local-footer',
1301
- name: 'core/template-part',
1302
- innerBlocks: [],
1303
- },
1304
- ];
1305
- } );
1306
-
1307
- mockBlockEditorStore( { getBlocks: mockGetBlocks } );
1310
+ // The caller provides post content blocks directly
1311
+ // (template detection is handled by usePostContentBlocks).
1312
+ const postContentBlocks = [
1313
+ {
1314
+ clientId: 'local-para-0',
1315
+ name: 'core/paragraph',
1316
+ innerBlocks: [],
1317
+ },
1318
+ {
1319
+ clientId: 'local-para-1',
1320
+ name: 'core/paragraph',
1321
+ innerBlocks: [],
1322
+ },
1323
+ ];
1308
1324
 
1309
1325
  const awareness = new PostEditorAwareness(
1310
1326
  templateDoc,
@@ -1335,55 +1351,29 @@ describe( 'PostEditorAwareness', () => {
1335
1351
  },
1336
1352
  };
1337
1353
 
1338
- const result =
1339
- awareness.convertSelectionStateToAbsolute( selection );
1354
+ const result = awareness.convertSelectionStateToAbsolute(
1355
+ selection,
1356
+ postContentBlocks
1357
+ );
1340
1358
 
1341
1359
  expect( result.richTextOffset ).toBe( 4 );
1342
- // Should resolve to the post-content inner block, not a template block
1343
1360
  expect( result.localClientId ).toBe( 'local-para-1' );
1344
- // Verify getBlocks was called with the post-content clientId
1345
- expect( mockGetBlocks ).toHaveBeenCalledWith( postContentClientId );
1346
1361
 
1347
1362
  templateDoc.destroy();
1348
1363
  } );
1349
1364
 
1350
- test( 'should resolve WholeBlock in template mode', () => {
1365
+ test( 'should resolve WholeBlock with post content blocks', () => {
1351
1366
  const templateDoc = createTestDocWithBlocks( [
1352
1367
  createYBlock( 'yjs-img', 'core/image' ),
1353
1368
  ] );
1354
1369
 
1355
- const postContentClientId = 'local-post-content';
1356
- const mockGetBlocks = jest
1357
- .fn()
1358
- .mockImplementation( ( rootClientId?: string ) => {
1359
- if ( rootClientId === postContentClientId ) {
1360
- return [
1361
- {
1362
- clientId: 'local-img',
1363
- name: 'core/image',
1364
- innerBlocks: [],
1365
- },
1366
- ];
1367
- }
1368
- return [
1369
- {
1370
- clientId: 'local-group',
1371
- name: 'core/group',
1372
- innerBlocks: [
1373
- {
1374
- clientId: postContentClientId,
1375
- name: 'core/post-content',
1376
- innerBlocks: [],
1377
- },
1378
- ],
1379
- },
1380
- ];
1381
- } );
1382
-
1383
- mockBlockEditorStore( {
1384
- getBlocks: mockGetBlocks,
1385
- getBlockName: 'core/image',
1386
- } );
1370
+ const postContentBlocks = [
1371
+ {
1372
+ clientId: 'local-img',
1373
+ name: 'core/image',
1374
+ innerBlocks: [],
1375
+ },
1376
+ ];
1387
1377
 
1388
1378
  const awareness = new PostEditorAwareness(
1389
1379
  templateDoc,
@@ -1407,8 +1397,10 @@ describe( 'PostEditorAwareness', () => {
1407
1397
  blockPosition,
1408
1398
  };
1409
1399
 
1410
- const result =
1411
- awareness.convertSelectionStateToAbsolute( selection );
1400
+ const result = awareness.convertSelectionStateToAbsolute(
1401
+ selection,
1402
+ postContentBlocks
1403
+ );
1412
1404
 
1413
1405
  expect( result.richTextOffset ).toBeNull();
1414
1406
  expect( result.localClientId ).toBe( 'local-img' );
@@ -1416,17 +1408,20 @@ describe( 'PostEditorAwareness', () => {
1416
1408
  templateDoc.destroy();
1417
1409
  } );
1418
1410
 
1419
- test( 'should fall back to root blocks when no core/post-content exists', () => {
1420
- // Normal mode (no template) — should use root blocks directly
1411
+ test( 'should resolve with root blocks directly', () => {
1421
1412
  const normalDoc = createTestDocWithBlocks( [
1422
1413
  createYBlock( 'yjs-para', 'core/paragraph', {
1423
1414
  textContent: 'Normal mode',
1424
1415
  } ),
1425
1416
  ] );
1426
1417
 
1427
- mockBlockEditorStore( {
1428
- blocks: [ { clientId: 'local-para', innerBlocks: [] } ],
1429
- } );
1418
+ const editorBlocks = [
1419
+ {
1420
+ clientId: 'local-para',
1421
+ name: 'core/paragraph',
1422
+ innerBlocks: [],
1423
+ },
1424
+ ];
1430
1425
 
1431
1426
  const awareness = new PostEditorAwareness(
1432
1427
  normalDoc,
@@ -1456,8 +1451,10 @@ describe( 'PostEditorAwareness', () => {
1456
1451
  },
1457
1452
  };
1458
1453
 
1459
- const result =
1460
- awareness.convertSelectionStateToAbsolute( selection );
1454
+ const result = awareness.convertSelectionStateToAbsolute(
1455
+ selection,
1456
+ editorBlocks
1457
+ );
1461
1458
 
1462
1459
  expect( result.richTextOffset ).toBe( 3 );
1463
1460
  expect( result.localClientId ).toBe( 'local-para' );
package/src/entities.js CHANGED
@@ -44,7 +44,7 @@ export const rootEntitiesConfig = [
44
44
  baseURL: '/',
45
45
  baseURLParams: {
46
46
  // Please also change the preload path when changing this.
47
- // @see lib/compat/wordpress-7.0/preload.php
47
+ // @see lib/compat/wordpress-7.1/preload.php
48
48
  _fields: [
49
49
  'description',
50
50
  'gmt_offset',
@@ -306,11 +306,14 @@ export const prePersistPostType = async (
306
306
  }
307
307
  }
308
308
 
309
- // Add meta for persisted CRDT document.
309
+ // Add meta for the persisted CRDT document during real post saves so the
310
+ // saved post and CRDT snapshot are committed in the same request. We don't
311
+ // want a post save to fail but a CRDT update to succeed or vice versa.
312
+ // CRDT repair uses /wp-sync/v1/save to avoid post-save side effects.
310
313
  if ( persistedRecord ) {
311
314
  const objectType = `postType/${ name }`;
312
315
  const objectId = persistedRecord.id;
313
- const serializedDoc = await getSyncManager()?.createPersistedCRDTDoc(
316
+ const serializedDoc = getSyncManager()?.createPersistedCRDTDoc(
314
317
  objectType,
315
318
  objectId
316
319
  );
@@ -406,6 +409,9 @@ async function loadPostTypeEntities() {
406
409
  * @type {import('@wordpress/sync').SyncConfig}
407
410
  */
408
411
  entity.syncConfig = {
412
+ // Save a CRDT document with this entity
413
+ supportsPersistence: true,
414
+
409
415
  /**
410
416
  * Apply changes from the local editor to the local CRDT document so
411
417
  * that those changes can be synced to other peers (via the provider).
@@ -28,6 +28,14 @@ jest.mock( '../../sync', () => ( {
28
28
  getSyncManager: jest.fn(),
29
29
  } ) );
30
30
 
31
+ const mockPostContentBlocks = [
32
+ { clientId: 'block-1', name: 'core/paragraph', innerBlocks: [] },
33
+ ];
34
+
35
+ jest.mock( '../../awareness/block-lookup', () => ( {
36
+ usePostContentBlocks: jest.fn( () => mockPostContentBlocks ),
37
+ } ) );
38
+
31
39
  const mockAvatarUrls = {
32
40
  '24': 'https://example.com/avatar-24.png',
33
41
  '48': 'https://example.com/avatar-48.png',
@@ -299,7 +307,7 @@ describe( 'use-post-editor-awareness-state hooks', () => {
299
307
  } );
300
308
  } );
301
309
 
302
- test( 'should call awareness.convertSelectionStateToAbsolute with selection', () => {
310
+ test( 'should call awareness.convertSelectionStateToAbsolute with selection and blocks', () => {
303
311
  const mockSelection: SelectionCursor = {
304
312
  type: SelectionType.Cursor,
305
313
  cursorPosition: {
@@ -321,7 +329,7 @@ describe( 'use-post-editor-awareness-state hooks', () => {
321
329
 
322
330
  expect(
323
331
  mockAwareness.convertSelectionStateToAbsolute
324
- ).toHaveBeenCalledWith( mockSelection );
332
+ ).toHaveBeenCalledWith( mockSelection, mockPostContentBlocks );
325
333
  expect( position ).toEqual( {
326
334
  richTextOffset: 10,
327
335
  localClientId: 'block-1',
@@ -2,13 +2,15 @@
2
2
  * External dependencies
3
3
  */
4
4
  import { usePrevious } from '@wordpress/compose';
5
- import { useEffect, useState } from '@wordpress/element';
5
+ import { useEffect, useState, useCallback } from '@wordpress/element';
6
6
  import type { Y } from '@wordpress/sync';
7
7
 
8
8
  /**
9
9
  * Internal dependencies
10
10
  */
11
11
  import { getSyncManager } from '../sync';
12
+ import { usePostContentBlocks } from '../awareness/block-lookup';
13
+ import type { EditorStoreBlock } from '../awareness/block-lookup';
12
14
  import type {
13
15
  PostEditorAwarenessState as ActiveCollaborator,
14
16
  PostSaveEvent,
@@ -19,7 +21,10 @@ import type { PostEditorAwareness } from '../awareness/post-editor-awareness';
19
21
 
20
22
  interface AwarenessState {
21
23
  activeCollaborators: ActiveCollaborator[];
22
- resolveSelection: ( selection: SelectionState ) => ResolvedSelection;
24
+ resolveSelection: (
25
+ selection: SelectionState,
26
+ blocks: EditorStoreBlock[]
27
+ ) => ResolvedSelection;
23
28
  getDebugData: () => YDocDebugData;
24
29
  isCurrentCollaboratorDisconnected: boolean;
25
30
  }
@@ -49,8 +54,10 @@ function getAwarenessState(
49
54
 
50
55
  return {
51
56
  activeCollaborators,
52
- resolveSelection: ( selection: SelectionState ) =>
53
- awareness.convertSelectionStateToAbsolute( selection ),
57
+ resolveSelection: (
58
+ selection: SelectionState,
59
+ blocks: EditorStoreBlock[]
60
+ ) => awareness.convertSelectionStateToAbsolute( selection, blocks ),
54
61
  getDebugData: () => awareness.getDebugData(),
55
62
  isCurrentCollaboratorDisconnected:
56
63
  activeCollaborators.find( ( collaborator ) => collaborator.isMe )
@@ -124,7 +131,13 @@ export function useResolvedSelection(
124
131
  postId: number | null,
125
132
  postType: string | null
126
133
  ): ( selection: SelectionState ) => ResolvedSelection {
127
- return usePostEditorAwarenessState( postId, postType ).resolveSelection;
134
+ const blocks = usePostContentBlocks();
135
+ const awarenessState = usePostEditorAwarenessState( postId, postType );
136
+ return useCallback(
137
+ ( selection: SelectionState ) =>
138
+ awarenessState.resolveSelection( selection, blocks ),
139
+ [ blocks, awarenessState ]
140
+ );
128
141
  }
129
142
 
130
143
  /**
@@ -158,8 +171,8 @@ export function useIsDisconnected(
158
171
 
159
172
  /**
160
173
  * Hook that subscribes to the CRDT state map and returns the most recent
161
- * save event (timestamp + client ID). The state map is updated by
162
- * `markEntityAsSaved` in `@wordpress/sync`
174
+ * user-facing post save event (timestamp + client ID). The state map is
175
+ * updated by `markEntityAsSaved` in `@wordpress/sync`.
163
176
  *
164
177
  * @param postId The ID of the post.
165
178
  * @param postType The type of the post.
@@ -197,6 +197,24 @@ export function receiveViewConfig( kind, name, config ) {
197
197
  };
198
198
  }
199
199
 
200
+ /**
201
+ * Returns an action object used to notify core-data that the sync undo manager
202
+ * state changed outside of the core-data reducer, e.g. The Yjs UndoManager
203
+ * captured an undo level.
204
+ *
205
+ * @param {Object} state The sync undo stack state.
206
+ * @param {boolean} state.hasRedo Whether there are changes to redo.
207
+ * @param {boolean} state.hasUndo Whether there are changes to undo.
208
+ *
209
+ * @return {Object} Action object.
210
+ */
211
+ export function __unstableNotifySyncUndoManagerChange( state ) {
212
+ return {
213
+ type: 'SYNC_UNDO_MANAGER_CHANGE',
214
+ ...state,
215
+ };
216
+ }
217
+
200
218
  /**
201
219
  * Returns an action object used to set the sync connection status for an entity or collection.
202
220
  *