@wordpress/core-data 7.41.0 → 7.41.2-next.v.202603161435.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.
Files changed (161) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +19 -0
  3. package/build/actions.cjs +25 -31
  4. package/build/actions.cjs.map +2 -2
  5. package/build/awareness/post-editor-awareness.cjs +34 -1
  6. package/build/awareness/post-editor-awareness.cjs.map +2 -2
  7. package/build/awareness/types.cjs.map +1 -1
  8. package/build/entities.cjs +3 -2
  9. package/build/entities.cjs.map +2 -2
  10. package/build/entity-provider.cjs +15 -6
  11. package/build/entity-provider.cjs.map +2 -2
  12. package/build/hooks/use-entity-prop.cjs +33 -2
  13. package/build/hooks/use-entity-prop.cjs.map +2 -2
  14. package/build/hooks/use-post-editor-awareness-state.cjs +83 -2
  15. package/build/hooks/use-post-editor-awareness-state.cjs.map +2 -2
  16. package/build/index.cjs +3 -0
  17. package/build/index.cjs.map +2 -2
  18. package/build/private-actions.cjs +1 -1
  19. package/build/private-actions.cjs.map +2 -2
  20. package/build/private-apis.cjs +3 -1
  21. package/build/private-apis.cjs.map +2 -2
  22. package/build/queried-data/actions.cjs +1 -1
  23. package/build/queried-data/actions.cjs.map +2 -2
  24. package/build/queried-data/reducer.cjs +19 -13
  25. package/build/queried-data/reducer.cjs.map +2 -2
  26. package/build/queried-data/selectors.cjs +7 -4
  27. package/build/queried-data/selectors.cjs.map +2 -2
  28. package/build/reducer.cjs +2 -1
  29. package/build/reducer.cjs.map +2 -2
  30. package/build/resolvers.cjs +114 -76
  31. package/build/resolvers.cjs.map +2 -2
  32. package/build/selectors.cjs +29 -0
  33. package/build/selectors.cjs.map +2 -2
  34. package/build/sync.cjs +3 -0
  35. package/build/sync.cjs.map +2 -2
  36. package/build/types.cjs +16 -0
  37. package/build/types.cjs.map +3 -3
  38. package/build/utils/crdt-blocks.cjs +22 -26
  39. package/build/utils/crdt-blocks.cjs.map +2 -2
  40. package/build/utils/crdt-user-selections.cjs +8 -5
  41. package/build/utils/crdt-user-selections.cjs.map +2 -2
  42. package/build/utils/crdt.cjs +1 -3
  43. package/build/utils/crdt.cjs.map +2 -2
  44. package/build/utils/index.cjs +3 -0
  45. package/build/utils/index.cjs.map +2 -2
  46. package/build/utils/normalize-query-for-resolution.cjs +35 -0
  47. package/build/utils/normalize-query-for-resolution.cjs.map +7 -0
  48. package/build/utils/user-permissions.cjs +1 -4
  49. package/build/utils/user-permissions.cjs.map +2 -2
  50. package/build-module/actions.mjs +30 -32
  51. package/build-module/actions.mjs.map +2 -2
  52. package/build-module/awareness/post-editor-awareness.mjs +34 -1
  53. package/build-module/awareness/post-editor-awareness.mjs.map +2 -2
  54. package/build-module/entities.mjs +3 -2
  55. package/build-module/entities.mjs.map +2 -2
  56. package/build-module/entity-provider.mjs +15 -6
  57. package/build-module/entity-provider.mjs.map +2 -2
  58. package/build-module/hooks/use-entity-prop.mjs +34 -3
  59. package/build-module/hooks/use-entity-prop.mjs.map +2 -2
  60. package/build-module/hooks/use-post-editor-awareness-state.mjs +80 -1
  61. package/build-module/hooks/use-post-editor-awareness-state.mjs.map +2 -2
  62. package/build-module/index.mjs +2 -0
  63. package/build-module/index.mjs.map +2 -2
  64. package/build-module/private-actions.mjs +1 -1
  65. package/build-module/private-actions.mjs.map +2 -2
  66. package/build-module/private-apis.mjs +6 -2
  67. package/build-module/private-apis.mjs.map +2 -2
  68. package/build-module/queried-data/actions.mjs +1 -1
  69. package/build-module/queried-data/actions.mjs.map +2 -2
  70. package/build-module/queried-data/reducer.mjs +19 -13
  71. package/build-module/queried-data/reducer.mjs.map +2 -2
  72. package/build-module/queried-data/selectors.mjs +7 -4
  73. package/build-module/queried-data/selectors.mjs.map +2 -2
  74. package/build-module/reducer.mjs +2 -1
  75. package/build-module/reducer.mjs.map +2 -2
  76. package/build-module/resolvers.mjs +116 -77
  77. package/build-module/resolvers.mjs.map +2 -2
  78. package/build-module/selectors.mjs +28 -0
  79. package/build-module/selectors.mjs.map +2 -2
  80. package/build-module/sync.mjs +2 -0
  81. package/build-module/sync.mjs.map +2 -2
  82. package/build-module/types.mjs +9 -0
  83. package/build-module/types.mjs.map +4 -4
  84. package/build-module/utils/crdt-blocks.mjs +22 -26
  85. package/build-module/utils/crdt-blocks.mjs.map +2 -2
  86. package/build-module/utils/crdt-user-selections.mjs +8 -5
  87. package/build-module/utils/crdt-user-selections.mjs.map +2 -2
  88. package/build-module/utils/crdt.mjs +1 -3
  89. package/build-module/utils/crdt.mjs.map +2 -2
  90. package/build-module/utils/index.mjs +2 -0
  91. package/build-module/utils/index.mjs.map +2 -2
  92. package/build-module/utils/normalize-query-for-resolution.mjs +14 -0
  93. package/build-module/utils/normalize-query-for-resolution.mjs.map +7 -0
  94. package/build-module/utils/user-permissions.mjs +1 -4
  95. package/build-module/utils/user-permissions.mjs.map +2 -2
  96. package/build-types/actions.d.ts.map +1 -1
  97. package/build-types/awareness/post-editor-awareness.d.ts.map +1 -1
  98. package/build-types/awareness/types.d.ts +1 -1
  99. package/build-types/awareness/types.d.ts.map +1 -1
  100. package/build-types/entities.d.ts +1 -1
  101. package/build-types/entities.d.ts.map +1 -1
  102. package/build-types/entity-provider.d.ts +11 -6
  103. package/build-types/entity-provider.d.ts.map +1 -1
  104. package/build-types/hooks/use-entity-prop.d.ts.map +1 -1
  105. package/build-types/hooks/use-post-editor-awareness-state.d.ts +34 -10
  106. package/build-types/hooks/use-post-editor-awareness-state.d.ts.map +1 -1
  107. package/build-types/index.d.ts +2 -0
  108. package/build-types/index.d.ts.map +1 -1
  109. package/build-types/private-apis.d.ts.map +1 -1
  110. package/build-types/queried-data/reducer.d.ts.map +1 -1
  111. package/build-types/queried-data/selectors.d.ts.map +1 -1
  112. package/build-types/reducer.d.ts.map +1 -1
  113. package/build-types/resolvers.d.ts +2 -1
  114. package/build-types/resolvers.d.ts.map +1 -1
  115. package/build-types/selectors.d.ts +17 -0
  116. package/build-types/selectors.d.ts.map +1 -1
  117. package/build-types/sync.d.ts +2 -2
  118. package/build-types/sync.d.ts.map +1 -1
  119. package/build-types/types.d.ts +15 -0
  120. package/build-types/types.d.ts.map +1 -1
  121. package/build-types/utils/crdt-blocks.d.ts.map +1 -1
  122. package/build-types/utils/crdt-user-selections.d.ts +10 -5
  123. package/build-types/utils/crdt-user-selections.d.ts.map +1 -1
  124. package/build-types/utils/crdt.d.ts.map +1 -1
  125. package/build-types/utils/index.d.ts +1 -0
  126. package/build-types/utils/normalize-query-for-resolution.d.ts +12 -0
  127. package/build-types/utils/normalize-query-for-resolution.d.ts.map +1 -0
  128. package/build-types/utils/user-permissions.d.ts.map +1 -1
  129. package/package.json +18 -18
  130. package/src/actions.js +49 -50
  131. package/src/awareness/post-editor-awareness.ts +93 -1
  132. package/src/awareness/test/post-editor-awareness.ts +35 -0
  133. package/src/awareness/types.ts +1 -1
  134. package/src/entities.js +2 -1
  135. package/src/entity-provider.js +24 -11
  136. package/src/hooks/test/use-post-editor-awareness-state.ts +443 -0
  137. package/src/hooks/use-entity-prop.js +43 -3
  138. package/src/hooks/use-post-editor-awareness-state.ts +159 -7
  139. package/src/index.js +1 -0
  140. package/src/private-actions.js +1 -1
  141. package/src/private-apis.js +6 -2
  142. package/src/queried-data/actions.js +1 -1
  143. package/src/queried-data/reducer.js +26 -14
  144. package/src/queried-data/selectors.js +12 -5
  145. package/src/queried-data/test/selectors.js +25 -0
  146. package/src/reducer.js +4 -1
  147. package/src/resolvers.js +141 -91
  148. package/src/selectors.ts +56 -0
  149. package/src/sync.ts +2 -0
  150. package/src/test/private-actions.js +1 -1
  151. package/src/test/resolvers.js +88 -14
  152. package/src/test/selectors.js +150 -0
  153. package/src/test/store.js +182 -0
  154. package/src/types.ts +19 -0
  155. package/src/utils/crdt-blocks.ts +47 -54
  156. package/src/utils/crdt-user-selections.ts +28 -16
  157. package/src/utils/crdt.ts +2 -7
  158. package/src/utils/index.js +1 -0
  159. package/src/utils/normalize-query-for-resolution.js +23 -0
  160. package/src/utils/test/crdt-blocks.ts +42 -24
  161. package/src/utils/user-permissions.js +4 -5
@@ -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
@@ -113,6 +117,122 @@ describe( 'getEntityRecord', () => {
113
117
  } );
114
118
  } );
115
119
 
120
+ describe( 'getEntityRecords', () => {
121
+ const POSTS = [
122
+ createTestPost( 1 ),
123
+ createTestPost( 2 ),
124
+ createTestPost( 3 ),
125
+ ];
126
+
127
+ let registry;
128
+
129
+ beforeEach( () => {
130
+ registry = createTestRegistry();
131
+ triggerFetch.mockReset();
132
+ } );
133
+
134
+ it( 'preserves collection when getEntityRecord resolves after getEntityRecords', async () => {
135
+ let resolveSlowFetch;
136
+ const slowFetchPromise = new Promise( ( resolve ) => {
137
+ resolveSlowFetch = resolve;
138
+ } );
139
+
140
+ triggerFetch.mockImplementation( ( { path } ) => {
141
+ // Single post fetch (e.g. /wp/v2/posts/1): return slow promise.
142
+ if ( /\/wp\/v2\/posts\/\d+/.test( path ) ) {
143
+ return slowFetchPromise;
144
+ }
145
+ // Collection fetch: return immediately.
146
+ return Promise.resolve( {
147
+ json: () => Promise.resolve( POSTS ),
148
+ headers: {
149
+ get: ( header ) => {
150
+ if ( header === 'X-WP-Total' ) {
151
+ return String( POSTS.length );
152
+ }
153
+ if ( header === 'X-WP-TotalPages' ) {
154
+ return '1';
155
+ }
156
+ return null;
157
+ },
158
+ },
159
+ } );
160
+ } );
161
+
162
+ const resolveSelectStore = registry.resolveSelect( coreDataStore );
163
+
164
+ // Start getEntityRecord first (slow), then getEntityRecords (fast).
165
+ const singlePromise = resolveSelectStore.getEntityRecord(
166
+ 'postType',
167
+ 'post',
168
+ 1,
169
+ { context: 'edit' }
170
+ );
171
+ await resolveSelectStore.getEntityRecords( 'postType', 'post', {
172
+ context: 'edit',
173
+ } );
174
+
175
+ // Now resolve the slow single-record fetch.
176
+ resolveSlowFetch( {
177
+ json: () => Promise.resolve( POSTS[ 0 ] ),
178
+ headers: { get: () => null },
179
+ } );
180
+ await singlePromise;
181
+
182
+ // Wait for all pending thunks to settle.
183
+ await new Promise( ( resolve ) => setTimeout( resolve, 0 ) );
184
+
185
+ const allPosts = registry
186
+ .select( coreDataStore )
187
+ .getEntityRecords( 'postType', 'post', { context: 'edit' } );
188
+ expect( allPosts.map( ( p ) => p.id ) ).toEqual( [ 1, 2, 3 ] );
189
+ } );
190
+
191
+ it( 'preserves collection when getEntityRecord is called after getEntityRecords', async () => {
192
+ triggerFetch.mockImplementation( ( { path } ) => {
193
+ // Collection fetch.
194
+ if ( ! /\/wp\/v2\/posts\/\d+/.test( path ) ) {
195
+ return Promise.resolve( {
196
+ json: () => Promise.resolve( POSTS ),
197
+ headers: {
198
+ get: ( header ) => {
199
+ if ( header === 'X-WP-Total' ) {
200
+ return String( POSTS.length );
201
+ }
202
+ if ( header === 'X-WP-TotalPages' ) {
203
+ return '1';
204
+ }
205
+ return null;
206
+ },
207
+ },
208
+ } );
209
+ }
210
+ // Single post fetch.
211
+ return Promise.resolve( {
212
+ json: () => Promise.resolve( POSTS[ 0 ] ),
213
+ headers: { get: () => null },
214
+ } );
215
+ } );
216
+
217
+ const resolveSelectStore = registry.resolveSelect( coreDataStore );
218
+
219
+ // First resolve the collection.
220
+ await resolveSelectStore.getEntityRecords( 'postType', 'post', {
221
+ context: 'edit',
222
+ } );
223
+
224
+ // Then resolve a single record.
225
+ await resolveSelectStore.getEntityRecord( 'postType', 'post', 1, {
226
+ context: 'edit',
227
+ } );
228
+
229
+ const allPosts = registry
230
+ .select( coreDataStore )
231
+ .getEntityRecords( 'postType', 'post', { context: 'edit' } );
232
+ expect( allPosts.map( ( p ) => p.id ) ).toEqual( [ 1, 2, 3 ] );
233
+ } );
234
+ } );
235
+
116
236
  describe( 'clearEntityRecordEdits', () => {
117
237
  let registry;
118
238
 
@@ -142,3 +262,65 @@ describe( 'clearEntityRecordEdits', () => {
142
262
  ).toEqual( select.getRawEntityRecord( 'postType', 'post', post.id ) );
143
263
  } );
144
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
@@ -66,6 +66,16 @@ export type CursorPosition = {
66
66
  absoluteOffset: number;
67
67
  };
68
68
 
69
+ /**
70
+ * The direction of a text selection, indicating where the caret sits.
71
+ */
72
+ export enum SelectionDirection {
73
+ /** The caret is at the end of the selection (default / left-to-right). */
74
+ Forward = 'f',
75
+ /** The caret is at the start of the selection (right-to-left). */
76
+ Backward = 'b',
77
+ }
78
+
69
79
  export type SelectionNone = {
70
80
  // The user has not made a selection.
71
81
  type: SelectionType.None;
@@ -86,6 +96,8 @@ export type SelectionInOneBlock = {
86
96
  type: SelectionType.SelectionInOneBlock;
87
97
  cursorStartPosition: CursorPosition;
88
98
  cursorEndPosition: CursorPosition;
99
+ // The direction of the selection, indicating where the caret sits.
100
+ selectionDirection?: SelectionDirection;
89
101
  };
90
102
 
91
103
  export type SelectionInMultipleBlocks = {
@@ -95,6 +107,8 @@ export type SelectionInMultipleBlocks = {
95
107
  type: SelectionType.SelectionInMultipleBlocks;
96
108
  cursorStartPosition: CursorPosition;
97
109
  cursorEndPosition: CursorPosition;
110
+ // The direction of the selection, indicating where the caret sits.
111
+ selectionDirection?: SelectionDirection;
98
112
  };
99
113
 
100
114
  export type SelectionWholeBlock = {
@@ -111,3 +125,8 @@ export type SelectionState =
111
125
  | SelectionInOneBlock
112
126
  | SelectionInMultipleBlocks
113
127
  | SelectionWholeBlock;
128
+
129
+ export interface ResolvedSelection {
130
+ textIndex: number | null;
131
+ localClientId: string | null;
132
+ }
@@ -22,9 +22,14 @@ interface BlockAttributes {
22
22
  [ key: string ]: unknown;
23
23
  }
24
24
 
25
+ interface BlockAttributeType {
26
+ role?: string;
27
+ type?: string;
28
+ }
29
+
25
30
  interface BlockType {
31
+ attributes?: Record< string, BlockAttributeType >;
26
32
  name: string;
27
- attributes?: Record< string, { type?: string } >;
28
33
  }
29
34
 
30
35
  // A block as represented in Gutenberg's data store.
@@ -58,10 +63,16 @@ export type YBlockAttributes = Y.Map< Y.Text | unknown >;
58
63
  const serializableBlocksCache = new WeakMap< WeakKey, Block[] >();
59
64
 
60
65
  function makeBlockAttributesSerializable(
66
+ blockName: string,
61
67
  attributes: BlockAttributes
62
68
  ): BlockAttributes {
63
69
  const newAttributes = { ...attributes };
64
70
  for ( const [ key, value ] of Object.entries( attributes ) ) {
71
+ if ( isLocalAttribute( blockName, key ) ) {
72
+ delete newAttributes[ key ];
73
+ continue;
74
+ }
75
+
65
76
  if ( value instanceof RichTextData ) {
66
77
  newAttributes[ key ] = value.valueOf();
67
78
  }
@@ -76,7 +87,7 @@ function makeBlocksSerializable( blocks: Block[] ): Block[] {
76
87
  return {
77
88
  ...rest,
78
89
  name,
79
- attributes: makeBlockAttributesSerializable( attributes ),
90
+ attributes: makeBlockAttributesSerializable( name, attributes ),
80
91
  innerBlocks: makeBlocksSerializable( innerBlocks ),
81
92
  };
82
93
  } );
@@ -202,12 +213,7 @@ export function mergeCrdtBlocks(
202
213
  makeBlocksSerializable( incomingBlocks )
203
214
  );
204
215
  }
205
- const allBlocks = serializableBlocksCache.get( incomingBlocks ) ?? [];
206
-
207
- // Ensure we skip blocks that we don't want to sync at the moment
208
- const blocksToSync = allBlocks.filter( ( block ) =>
209
- shouldBlockBeSynced( block )
210
- );
216
+ const blocksToSync = serializableBlocksCache.get( incomingBlocks ) ?? [];
211
217
 
212
218
  // This is a rudimentary diff implementation similar to the y-prosemirror diffing
213
219
  // approach.
@@ -382,32 +388,6 @@ export function mergeCrdtBlocks(
382
388
  }
383
389
  }
384
390
 
385
- /**
386
- * Determine if a block should be synced.
387
- *
388
- * Ex: A gallery block should not be synced until the images have been
389
- * uploaded to WordPress, and their url is available. Before that,
390
- * it's not possible to access the blobs on a client as those are
391
- * local.
392
- *
393
- * @param block The block to check.
394
- * @return True if the block should be synced, false otherwise.
395
- */
396
- function shouldBlockBeSynced( block: Block ): boolean {
397
- // Verify that the gallery block is ready to be synced.
398
- // This means that, all images have had their blobs converted to full URLs.
399
- // Checking for only the blobs ensures that blocks that have just been inserted work as well.
400
- if ( 'core/gallery' === block.name ) {
401
- return ! block.innerBlocks.some(
402
- ( innerBlock ) =>
403
- innerBlock.attributes && innerBlock.attributes.blob
404
- );
405
- }
406
-
407
- // Allow all other blocks to be synced.
408
- return true;
409
- }
410
-
411
391
  /**
412
392
  * Update a single attribute on a Yjs block attributes map (currentAttributes).
413
393
  *
@@ -448,38 +428,35 @@ function updateYBlockAttribute(
448
428
  }
449
429
  }
450
430
 
451
- // Cached types for block attributes.
452
- let cachedBlockAttributeTypes: Map< string, Map< string, string > >;
431
+ // Cached block attribute types, populated once from getBlockTypes().
432
+ let cachedBlockAttributeTypes: Map< string, Map< string, BlockAttributeType > >;
453
433
 
454
434
  /**
455
- * Get the defined attribute type for a block attribute.
435
+ * Get the attribute type definition for a block attribute.
456
436
  *
457
437
  * @param blockName The name of the block, e.g. 'core/paragraph'.
458
438
  * @param attributeName The name of the attribute, e.g. 'content'.
459
- * @return The type of the attribute, e.g. 'rich-text' or 'string'.
439
+ * @return The type definition of the attribute.
460
440
  */
461
441
  function getBlockAttributeType(
462
442
  blockName: string,
463
443
  attributeName: string
464
- ): string | undefined {
444
+ ): BlockAttributeType | undefined {
465
445
  if ( ! cachedBlockAttributeTypes ) {
466
446
  // Parse the attributes for all blocks once.
467
- cachedBlockAttributeTypes = new Map< string, Map< string, string > >();
447
+ cachedBlockAttributeTypes = new Map();
468
448
 
469
449
  for ( const blockType of getBlockTypes() as BlockType[] ) {
470
- const blockAttributeTypeMap = new Map< string, string >();
471
-
472
- for ( const [ name, definition ] of Object.entries(
473
- blockType.attributes ?? {}
474
- ) ) {
475
- if ( definition.type ) {
476
- blockAttributeTypeMap.set( name, definition.type );
477
- }
478
- }
479
-
480
450
  cachedBlockAttributeTypes.set(
481
451
  blockType.name,
482
- blockAttributeTypeMap
452
+ new Map< string, BlockAttributeType >(
453
+ Object.entries( blockType.attributes ?? {} ).map(
454
+ ( [ name, definition ] ) => {
455
+ const { role, type } = definition;
456
+ return [ name, { role, type } ];
457
+ }
458
+ )
459
+ )
483
460
  );
484
461
  }
485
462
  }
@@ -503,11 +480,13 @@ function isExpectedAttributeType(
503
480
  const expectedAttributeType = getBlockAttributeType(
504
481
  blockName,
505
482
  attributeName
506
- );
483
+ )?.type;
507
484
 
508
485
  if ( expectedAttributeType === 'rich-text' ) {
509
486
  return attributeValue instanceof Y.Text;
510
- } else if ( expectedAttributeType === 'string' ) {
487
+ }
488
+
489
+ if ( expectedAttributeType === 'string' ) {
511
490
  return typeof attributeValue === 'string';
512
491
  }
513
492
 
@@ -515,6 +494,18 @@ function isExpectedAttributeType(
515
494
  return true;
516
495
  }
517
496
 
497
+ /**
498
+ * Given a block name and attribute key, return true if the attribute is local
499
+ * and should not be synced.
500
+ *
501
+ * @param blockName The name of the block, e.g. 'core/image'.
502
+ * @param attributeName The name of the attribute to check, e.g. 'blob'.
503
+ * @return True if the attribute is local, false otherwise.
504
+ */
505
+ function isLocalAttribute( blockName: string, attributeName: string ): boolean {
506
+ return 'local' === getBlockAttributeType( blockName, attributeName )?.role;
507
+ }
508
+
518
509
  /**
519
510
  * Given a block name and attribute key, return true if the attribute is rich-text typed.
520
511
  *
@@ -526,7 +517,9 @@ function isRichTextAttribute(
526
517
  blockName: string,
527
518
  attributeName: string
528
519
  ): boolean {
529
- return 'rich-text' === getBlockAttributeType( blockName, attributeName );
520
+ return (
521
+ 'rich-text' === getBlockAttributeType( blockName, attributeName )?.type
522
+ );
530
523
  }
531
524
 
532
525
  let localDoc: Y.Doc;
@@ -13,16 +13,17 @@ import { CRDT_RECORD_MAP_KEY } from '../sync';
13
13
  import type { YPostRecord } from './crdt';
14
14
  import type { YBlock, YBlocks } from './crdt-blocks';
15
15
  import { getRootMap } from './crdt-utils';
16
- import type {
17
- AbsoluteBlockIndexPath,
18
- WPBlockSelection,
19
- SelectionState,
20
- SelectionNone,
21
- SelectionCursor,
22
- SelectionInOneBlock,
23
- SelectionInMultipleBlocks,
24
- SelectionWholeBlock,
25
- CursorPosition,
16
+ import type { SelectionDirection } from '../types';
17
+ import {
18
+ type AbsoluteBlockIndexPath,
19
+ type WPBlockSelection,
20
+ type SelectionState,
21
+ type SelectionNone,
22
+ type SelectionCursor,
23
+ type SelectionInOneBlock,
24
+ type SelectionInMultipleBlocks,
25
+ type SelectionWholeBlock,
26
+ type CursorPosition,
26
27
  } from '../types';
27
28
 
28
29
  /**
@@ -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 - The start position of the selection
48
- * @param selectionEnd - The end position of the selection
49
- * @param yDoc - The Yjs document
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
 
@@ -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(
package/src/utils/crdt.ts CHANGED
@@ -342,8 +342,6 @@ export function getPostChangesFromCRDTDoc(
342
342
  );
343
343
  }
344
344
 
345
- // The consumers of blocks have memoization that renders optimization
346
- // here unnecessary.
347
345
  return true;
348
346
  }
349
347
 
@@ -351,11 +349,8 @@ export function getPostChangesFromCRDTDoc(
351
349
  // Do not overwrite a "floating" date. Borrowing logic from the
352
350
  // isEditedPostDateFloating selector.
353
351
  const currentDateIsFloating =
354
- [ 'draft', 'auto-draft', 'pending' ].includes(
355
- ymap.get( 'status' ) as string
356
- ) &&
357
- ( null === currentValue ||
358
- editedRecord.modified === currentValue );
352
+ null === currentValue ||
353
+ editedRecord.modified === currentValue;
359
354
 
360
355
  if ( currentDateIsFloating ) {
361
356
  return false;
@@ -15,3 +15,4 @@ export {
15
15
  ALLOWED_RESOURCE_ACTIONS,
16
16
  } from './user-permissions';
17
17
  export { RECEIVE_INTERMEDIATE_RESULTS } from './receive-intermediate-results';
18
+ export { default as normalizeQueryForResolution } from './normalize-query-for-resolution';