@wordpress/core-data 7.41.0 → 7.41.2-next.v.202603102151.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 (79) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/build/actions.cjs +8 -6
  3. package/build/actions.cjs.map +2 -2
  4. package/build/entity-provider.cjs +15 -6
  5. package/build/entity-provider.cjs.map +2 -2
  6. package/build/hooks/use-entity-prop.cjs +32 -2
  7. package/build/hooks/use-entity-prop.cjs.map +2 -2
  8. package/build/private-actions.cjs +1 -1
  9. package/build/private-actions.cjs.map +2 -2
  10. package/build/queried-data/actions.cjs +1 -1
  11. package/build/queried-data/actions.cjs.map +2 -2
  12. package/build/queried-data/reducer.cjs +19 -13
  13. package/build/queried-data/reducer.cjs.map +2 -2
  14. package/build/reducer.cjs +2 -1
  15. package/build/reducer.cjs.map +2 -2
  16. package/build/resolvers.cjs +4 -2
  17. package/build/resolvers.cjs.map +2 -2
  18. package/build/sync.cjs +3 -0
  19. package/build/sync.cjs.map +2 -2
  20. package/build/utils/crdt-blocks.cjs +22 -26
  21. package/build/utils/crdt-blocks.cjs.map +2 -2
  22. package/build/utils/crdt.cjs +1 -3
  23. package/build/utils/crdt.cjs.map +2 -2
  24. package/build/utils/user-permissions.cjs +1 -4
  25. package/build/utils/user-permissions.cjs.map +2 -2
  26. package/build-module/actions.mjs +13 -7
  27. package/build-module/actions.mjs.map +2 -2
  28. package/build-module/entity-provider.mjs +15 -6
  29. package/build-module/entity-provider.mjs.map +2 -2
  30. package/build-module/hooks/use-entity-prop.mjs +33 -3
  31. package/build-module/hooks/use-entity-prop.mjs.map +2 -2
  32. package/build-module/private-actions.mjs +1 -1
  33. package/build-module/private-actions.mjs.map +2 -2
  34. package/build-module/queried-data/actions.mjs +1 -1
  35. package/build-module/queried-data/actions.mjs.map +2 -2
  36. package/build-module/queried-data/reducer.mjs +19 -13
  37. package/build-module/queried-data/reducer.mjs.map +2 -2
  38. package/build-module/reducer.mjs +2 -1
  39. package/build-module/reducer.mjs.map +2 -2
  40. package/build-module/resolvers.mjs +4 -2
  41. package/build-module/resolvers.mjs.map +2 -2
  42. package/build-module/sync.mjs +2 -0
  43. package/build-module/sync.mjs.map +2 -2
  44. package/build-module/utils/crdt-blocks.mjs +22 -26
  45. package/build-module/utils/crdt-blocks.mjs.map +2 -2
  46. package/build-module/utils/crdt.mjs +1 -3
  47. package/build-module/utils/crdt.mjs.map +2 -2
  48. package/build-module/utils/user-permissions.mjs +1 -4
  49. package/build-module/utils/user-permissions.mjs.map +2 -2
  50. package/build-types/actions.d.ts.map +1 -1
  51. package/build-types/entity-provider.d.ts +11 -6
  52. package/build-types/entity-provider.d.ts.map +1 -1
  53. package/build-types/hooks/use-entity-prop.d.ts.map +1 -1
  54. package/build-types/index.d.ts.map +1 -1
  55. package/build-types/queried-data/reducer.d.ts.map +1 -1
  56. package/build-types/reducer.d.ts.map +1 -1
  57. package/build-types/resolvers.d.ts.map +1 -1
  58. package/build-types/sync.d.ts +2 -2
  59. package/build-types/sync.d.ts.map +1 -1
  60. package/build-types/utils/crdt-blocks.d.ts.map +1 -1
  61. package/build-types/utils/crdt.d.ts.map +1 -1
  62. package/build-types/utils/user-permissions.d.ts.map +1 -1
  63. package/package.json +18 -18
  64. package/src/actions.js +24 -10
  65. package/src/entity-provider.js +24 -11
  66. package/src/hooks/use-entity-prop.js +41 -3
  67. package/src/private-actions.js +1 -1
  68. package/src/queried-data/actions.js +1 -1
  69. package/src/queried-data/reducer.js +26 -14
  70. package/src/reducer.js +4 -1
  71. package/src/resolvers.js +5 -3
  72. package/src/sync.ts +2 -0
  73. package/src/test/private-actions.js +1 -1
  74. package/src/test/resolvers.js +24 -3
  75. package/src/test/store.js +116 -0
  76. package/src/utils/crdt-blocks.ts +47 -54
  77. package/src/utils/crdt.ts +2 -5
  78. package/src/utils/test/crdt-blocks.ts +42 -24
  79. package/src/utils/user-permissions.js +4 -5
@@ -1,13 +1,15 @@
1
1
  /**
2
2
  * WordPress dependencies
3
3
  */
4
- import { useCallback } from '@wordpress/element';
4
+ import { useCallback, useContext } from '@wordpress/element';
5
5
  import { useDispatch, useSelect } from '@wordpress/data';
6
6
 
7
7
  /**
8
8
  * Internal dependencies
9
9
  */
10
10
  import { STORE_NAME } from '../name';
11
+ import { DEFAULT_ENTITY_KEY } from '../entities';
12
+ import { EntityContext } from '../entity-context';
11
13
  import useEntityId from './use-entity-id';
12
14
 
13
15
  /**
@@ -30,9 +32,42 @@ import useEntityId from './use-entity-id';
30
32
  export default function useEntityProp( kind, name, prop, _id ) {
31
33
  const providerId = useEntityId( kind, name );
32
34
  const id = _id ?? providerId;
35
+ const context = useContext( EntityContext );
36
+ const revisionId = context?.revisionId;
33
37
 
34
38
  const { value, fullValue } = useSelect(
35
39
  ( select ) => {
40
+ if ( revisionId ) {
41
+ // Use getRevisions (not getRevision) to read from the
42
+ // already-cached collection. Using getRevision would
43
+ // trigger a redundant single-revision API fetch that
44
+ // can wipe the collection due to a race condition.
45
+ // See https://github.com/WordPress/gutenberg/pull/76043.
46
+ const revisions = select( STORE_NAME ).getRevisions(
47
+ kind,
48
+ name,
49
+ id,
50
+ {
51
+ per_page: -1,
52
+ context: 'edit',
53
+ }
54
+ );
55
+ const entityConfig = select( STORE_NAME ).getEntityConfig(
56
+ kind,
57
+ name
58
+ );
59
+ const revKey = entityConfig?.revisionKey || DEFAULT_ENTITY_KEY;
60
+ const revision = revisions?.find(
61
+ ( r ) => r[ revKey ] === revisionId
62
+ );
63
+ return revision
64
+ ? {
65
+ value: revision[ prop ],
66
+ fullValue: revision[ prop ],
67
+ }
68
+ : {};
69
+ }
70
+
36
71
  const { getEntityRecord, getEditedEntityRecord } =
37
72
  select( STORE_NAME );
38
73
  const record = getEntityRecord( kind, name, id ); // Trigger resolver.
@@ -44,16 +79,19 @@ export default function useEntityProp( kind, name, prop, _id ) {
44
79
  }
45
80
  : {};
46
81
  },
47
- [ kind, name, id, prop ]
82
+ [ kind, name, id, prop, revisionId ]
48
83
  );
49
84
  const { editEntityRecord } = useDispatch( STORE_NAME );
50
85
  const setValue = useCallback(
51
86
  ( newValue ) => {
87
+ if ( revisionId ) {
88
+ return;
89
+ }
52
90
  editEntityRecord( kind, name, id, {
53
91
  [ prop ]: newValue,
54
92
  } );
55
93
  },
56
- [ editEntityRecord, kind, name, id, prop ]
94
+ [ editEntityRecord, kind, name, id, prop, revisionId ]
57
95
  );
58
96
 
59
97
  return [ value, setValue, fullValue ];
@@ -103,7 +103,7 @@ export const editMediaEntity =
103
103
  dispatch.receiveEntityRecords(
104
104
  kind,
105
105
  name,
106
- [ newRecord ],
106
+ newRecord,
107
107
  undefined,
108
108
  true,
109
109
  undefined,
@@ -10,7 +10,7 @@
10
10
  export function receiveItems( items, edits, meta ) {
11
11
  return {
12
12
  type: 'RECEIVE_ITEMS',
13
- items: Array.isArray( items ) ? items : [ items ],
13
+ items,
14
14
  persistedEdits: edits,
15
15
  meta,
16
16
  };
@@ -105,19 +105,22 @@ export function items( state = {}, action ) {
105
105
  case 'RECEIVE_ITEMS': {
106
106
  const context = getContextFromAction( action );
107
107
  const key = action.key || DEFAULT_ENTITY_KEY;
108
+ const itemsList = Array.isArray( action.items )
109
+ ? action.items
110
+ : [ action.items ];
108
111
  return {
109
112
  ...state,
110
113
  [ context ]: {
111
114
  ...state[ context ],
112
- ...action.items.reduce( ( accumulator, value ) => {
113
- const itemId = value?.[ key ];
114
-
115
- accumulator[ itemId ] = conservativeMapItem(
116
- state?.[ context ]?.[ itemId ],
117
- value
118
- );
119
- return accumulator;
120
- }, {} ),
115
+ ...Object.fromEntries(
116
+ itemsList.map( ( item ) => [
117
+ item?.[ key ],
118
+ conservativeMapItem(
119
+ state?.[ context ]?.[ item?.[ key ] ],
120
+ item
121
+ ),
122
+ ] )
123
+ ),
121
124
  },
122
125
  };
123
126
  }
@@ -149,6 +152,9 @@ export function itemIsComplete( state = {}, action ) {
149
152
  case 'RECEIVE_ITEMS': {
150
153
  const context = getContextFromAction( action );
151
154
  const { query, key = DEFAULT_ENTITY_KEY } = action;
155
+ const itemsList = Array.isArray( action.items )
156
+ ? action.items
157
+ : [ action.items ];
152
158
 
153
159
  // An item is considered complete if it is received without an associated
154
160
  // fields query. Ideally, this would be implemented in such a way where the
@@ -164,7 +170,7 @@ export function itemIsComplete( state = {}, action ) {
164
170
  ...state,
165
171
  [ context ]: {
166
172
  ...state[ context ],
167
- ...action.items.reduce( ( result, item ) => {
173
+ ...itemsList.reduce( ( result, item ) => {
168
174
  const itemId = item?.[ key ];
169
175
 
170
176
  // Defer to completeness if already assigned. Technically the
@@ -224,18 +230,24 @@ const receiveQueries = compose( [
224
230
  // reducer tracks only a single query object.
225
231
  onSubKey( 'stableKey' ),
226
232
  ] )( ( state = {}, action ) => {
227
- const { type, page, perPage, key = DEFAULT_ENTITY_KEY } = action;
233
+ if ( action.type !== 'RECEIVE_ITEMS' ) {
234
+ return state;
235
+ }
228
236
 
229
- if ( type !== 'RECEIVE_ITEMS' ) {
237
+ // Single items don't have page or total count metadata
238
+ // (only collection query responses do), so skip updating itemIds.
239
+ if ( ! Array.isArray( action.items ) ) {
230
240
  return state;
231
241
  }
232
242
 
243
+ const key = action.key ?? DEFAULT_ENTITY_KEY;
244
+
233
245
  return {
234
246
  itemIds: getMergedItemIds(
235
247
  state?.itemIds || [],
236
248
  action.items.map( ( item ) => item?.[ key ] ).filter( Boolean ),
237
- page,
238
- perPage
249
+ action.page,
250
+ action.perPage
239
251
  ),
240
252
  meta: action.meta,
241
253
  };
package/src/reducer.js CHANGED
@@ -214,8 +214,11 @@ function entity( entityConfig ) {
214
214
  }
215
215
 
216
216
  const nextState = { ...state };
217
+ const itemsList = Array.isArray( action.items )
218
+ ? action.items
219
+ : [ action.items ];
217
220
 
218
- for ( const record of action.items ) {
221
+ for ( const record of itemsList ) {
219
222
  const recordId = record?.[ action.key ];
220
223
  const edits = nextState[ recordId ];
221
224
  if ( ! edits ) {
package/src/resolvers.js CHANGED
@@ -991,9 +991,11 @@ export const getDefaultTemplateId =
991
991
  template.id = id;
992
992
  registry.batch( () => {
993
993
  dispatch.receiveDefaultTemplateId( query, id );
994
- dispatch.receiveEntityRecords( 'postType', template.type, [
995
- template,
996
- ] );
994
+ dispatch.receiveEntityRecords(
995
+ 'postType',
996
+ template.type,
997
+ template
998
+ );
997
999
  // Avoid further network requests.
998
1000
  dispatch.finishResolution( 'getEntityRecord', [
999
1001
  'postType',
package/src/sync.ts CHANGED
@@ -17,6 +17,7 @@ const {
17
17
  CRDT_DOC_META_PERSISTENCE_KEY,
18
18
  CRDT_RECORD_MAP_KEY,
19
19
  LOCAL_EDITOR_ORIGIN,
20
+ LOCAL_UNDO_IGNORED_ORIGIN,
20
21
  retrySyncConnection,
21
22
  } = unlock( syncPrivateApis );
22
23
 
@@ -25,6 +26,7 @@ export {
25
26
  CRDT_DOC_META_PERSISTENCE_KEY,
26
27
  CRDT_RECORD_MAP_KEY,
27
28
  LOCAL_EDITOR_ORIGIN,
29
+ LOCAL_UNDO_IGNORED_ORIGIN,
28
30
  retrySyncConnection,
29
31
  };
30
32
 
@@ -102,7 +102,7 @@ describe( 'editMediaEntity', () => {
102
102
  expect( dispatch.receiveEntityRecords ).toHaveBeenCalledWith(
103
103
  'postType',
104
104
  'attachment',
105
- [ updatedRecord ],
105
+ updatedRecord,
106
106
  undefined,
107
107
  true,
108
108
  undefined,
@@ -862,6 +862,24 @@ describe( 'canUser', () => {
862
862
  expect( dispatch.receiveUserPermissions ).not.toHaveBeenCalled();
863
863
  } );
864
864
 
865
+ it( 'receives false when the allow header is missing', async () => {
866
+ triggerFetch.mockImplementation( () => ( {
867
+ headers: new Map(),
868
+ } ) );
869
+
870
+ await canUser(
871
+ 'create',
872
+ 'media'
873
+ )( { dispatch, registry, resolveSelect } );
874
+
875
+ expect( dispatch.receiveUserPermissions ).toHaveBeenCalledWith( {
876
+ 'create/media': false,
877
+ 'read/media': false,
878
+ 'update/media': false,
879
+ 'delete/media': false,
880
+ } );
881
+ } );
882
+
865
883
  it( 'throws an error when an entity resource object is malformed', async () => {
866
884
  await expect(
867
885
  canUser( 'create', { name: 'wp_block' } )( {
@@ -888,9 +906,12 @@ describe( 'canUser', () => {
888
906
  parse: false,
889
907
  } );
890
908
 
891
- expect( dispatch.receiveUserPermissions ).toHaveBeenCalledWith(
892
- expect.objectContaining( { 'create/media': false } )
893
- );
909
+ expect( dispatch.receiveUserPermissions ).toHaveBeenCalledWith( {
910
+ 'create/media': false,
911
+ 'read/media': true,
912
+ 'update/media': false,
913
+ 'delete/media': false,
914
+ } );
894
915
  } );
895
916
 
896
917
  it( 'receives false when the user is not allowed to perform an action on entities', async () => {
package/src/test/store.js CHANGED
@@ -113,6 +113,122 @@ describe( 'getEntityRecord', () => {
113
113
  } );
114
114
  } );
115
115
 
116
+ describe( 'getEntityRecords', () => {
117
+ const POSTS = [
118
+ createTestPost( 1 ),
119
+ createTestPost( 2 ),
120
+ createTestPost( 3 ),
121
+ ];
122
+
123
+ let registry;
124
+
125
+ beforeEach( () => {
126
+ registry = createTestRegistry();
127
+ triggerFetch.mockReset();
128
+ } );
129
+
130
+ it( 'preserves collection when getEntityRecord resolves after getEntityRecords', async () => {
131
+ let resolveSlowFetch;
132
+ const slowFetchPromise = new Promise( ( resolve ) => {
133
+ resolveSlowFetch = resolve;
134
+ } );
135
+
136
+ triggerFetch.mockImplementation( ( { path } ) => {
137
+ // Single post fetch (e.g. /wp/v2/posts/1): return slow promise.
138
+ if ( /\/wp\/v2\/posts\/\d+/.test( path ) ) {
139
+ return slowFetchPromise;
140
+ }
141
+ // Collection fetch: return immediately.
142
+ return Promise.resolve( {
143
+ json: () => Promise.resolve( POSTS ),
144
+ headers: {
145
+ get: ( header ) => {
146
+ if ( header === 'X-WP-Total' ) {
147
+ return String( POSTS.length );
148
+ }
149
+ if ( header === 'X-WP-TotalPages' ) {
150
+ return '1';
151
+ }
152
+ return null;
153
+ },
154
+ },
155
+ } );
156
+ } );
157
+
158
+ const resolveSelectStore = registry.resolveSelect( coreDataStore );
159
+
160
+ // Start getEntityRecord first (slow), then getEntityRecords (fast).
161
+ const singlePromise = resolveSelectStore.getEntityRecord(
162
+ 'postType',
163
+ 'post',
164
+ 1,
165
+ { context: 'edit' }
166
+ );
167
+ await resolveSelectStore.getEntityRecords( 'postType', 'post', {
168
+ context: 'edit',
169
+ } );
170
+
171
+ // Now resolve the slow single-record fetch.
172
+ resolveSlowFetch( {
173
+ json: () => Promise.resolve( POSTS[ 0 ] ),
174
+ headers: { get: () => null },
175
+ } );
176
+ await singlePromise;
177
+
178
+ // Wait for all pending thunks to settle.
179
+ await new Promise( ( resolve ) => setTimeout( resolve, 0 ) );
180
+
181
+ const allPosts = registry
182
+ .select( coreDataStore )
183
+ .getEntityRecords( 'postType', 'post', { context: 'edit' } );
184
+ expect( allPosts.map( ( p ) => p.id ) ).toEqual( [ 1, 2, 3 ] );
185
+ } );
186
+
187
+ it( 'preserves collection when getEntityRecord is called after getEntityRecords', async () => {
188
+ triggerFetch.mockImplementation( ( { path } ) => {
189
+ // Collection fetch.
190
+ if ( ! /\/wp\/v2\/posts\/\d+/.test( path ) ) {
191
+ return Promise.resolve( {
192
+ json: () => Promise.resolve( POSTS ),
193
+ headers: {
194
+ get: ( header ) => {
195
+ if ( header === 'X-WP-Total' ) {
196
+ return String( POSTS.length );
197
+ }
198
+ if ( header === 'X-WP-TotalPages' ) {
199
+ return '1';
200
+ }
201
+ return null;
202
+ },
203
+ },
204
+ } );
205
+ }
206
+ // Single post fetch.
207
+ return Promise.resolve( {
208
+ json: () => Promise.resolve( POSTS[ 0 ] ),
209
+ headers: { get: () => null },
210
+ } );
211
+ } );
212
+
213
+ const resolveSelectStore = registry.resolveSelect( coreDataStore );
214
+
215
+ // First resolve the collection.
216
+ await resolveSelectStore.getEntityRecords( 'postType', 'post', {
217
+ context: 'edit',
218
+ } );
219
+
220
+ // Then resolve a single record.
221
+ await resolveSelectStore.getEntityRecord( 'postType', 'post', 1, {
222
+ context: 'edit',
223
+ } );
224
+
225
+ const allPosts = registry
226
+ .select( coreDataStore )
227
+ .getEntityRecords( 'postType', 'post', { context: 'edit' } );
228
+ expect( allPosts.map( ( p ) => p.id ) ).toEqual( [ 1, 2, 3 ] );
229
+ } );
230
+ } );
231
+
116
232
  describe( 'clearEntityRecordEdits', () => {
117
233
  let registry;
118
234
 
@@ -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;
package/src/utils/crdt.ts CHANGED
@@ -351,11 +351,8 @@ export function getPostChangesFromCRDTDoc(
351
351
  // Do not overwrite a "floating" date. Borrowing logic from the
352
352
  // isEditedPostDateFloating selector.
353
353
  const currentDateIsFloating =
354
- [ 'draft', 'auto-draft', 'pending' ].includes(
355
- ymap.get( 'status' ) as string
356
- ) &&
357
- ( null === currentValue ||
358
- editedRecord.modified === currentValue );
354
+ null === currentValue ||
355
+ editedRecord.modified === currentValue;
359
356
 
360
357
  if ( currentDateIsFloating ) {
361
358
  return false;