@wordpress/core-data 7.39.1-next.v.202602111440.0 → 7.40.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 (112) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/README.md +41 -0
  3. package/build/actions.cjs +52 -0
  4. package/build/actions.cjs.map +2 -2
  5. package/build/awareness/base-awareness.cjs +1 -8
  6. package/build/awareness/base-awareness.cjs.map +2 -2
  7. package/build/awareness/types.cjs.map +1 -1
  8. package/build/awareness/utils.cjs +8 -51
  9. package/build/awareness/utils.cjs.map +2 -2
  10. package/build/entities.cjs +7 -1
  11. package/build/entities.cjs.map +2 -2
  12. package/build/hooks/use-entity-block-editor.cjs +13 -19
  13. package/build/hooks/use-entity-block-editor.cjs.map +2 -2
  14. package/build/index.cjs +6 -1
  15. package/build/index.cjs.map +2 -2
  16. package/build/private-actions.cjs +8 -0
  17. package/build/private-actions.cjs.map +2 -2
  18. package/build/private-apis.cjs +2 -1
  19. package/build/private-apis.cjs.map +2 -2
  20. package/build/private-selectors.cjs +5 -0
  21. package/build/private-selectors.cjs.map +2 -2
  22. package/build/reducer.cjs +31 -1
  23. package/build/reducer.cjs.map +2 -2
  24. package/build/resolvers.cjs +25 -0
  25. package/build/resolvers.cjs.map +2 -2
  26. package/build/selectors.cjs +15 -0
  27. package/build/selectors.cjs.map +2 -2
  28. package/build/utils/crdt-blocks.cjs +5 -3
  29. package/build/utils/crdt-blocks.cjs.map +2 -2
  30. package/build/utils/crdt.cjs +23 -19
  31. package/build/utils/crdt.cjs.map +2 -2
  32. package/build-module/actions.mjs +50 -0
  33. package/build-module/actions.mjs.map +2 -2
  34. package/build-module/awareness/base-awareness.mjs +1 -8
  35. package/build-module/awareness/base-awareness.mjs.map +2 -2
  36. package/build-module/awareness/utils.mjs +8 -51
  37. package/build-module/awareness/utils.mjs.map +2 -2
  38. package/build-module/entities.mjs +7 -1
  39. package/build-module/entities.mjs.map +2 -2
  40. package/build-module/hooks/use-entity-block-editor.mjs +13 -19
  41. package/build-module/hooks/use-entity-block-editor.mjs.map +2 -2
  42. package/build-module/index.mjs +3 -0
  43. package/build-module/index.mjs.map +2 -2
  44. package/build-module/private-actions.mjs +7 -0
  45. package/build-module/private-actions.mjs.map +2 -2
  46. package/build-module/private-apis.mjs +6 -2
  47. package/build-module/private-apis.mjs.map +2 -2
  48. package/build-module/private-selectors.mjs +8 -1
  49. package/build-module/private-selectors.mjs.map +2 -2
  50. package/build-module/reducer.mjs +29 -1
  51. package/build-module/reducer.mjs.map +2 -2
  52. package/build-module/resolvers.mjs +24 -0
  53. package/build-module/resolvers.mjs.map +2 -2
  54. package/build-module/selectors.mjs +14 -0
  55. package/build-module/selectors.mjs.map +2 -2
  56. package/build-module/utils/crdt-blocks.mjs +3 -2
  57. package/build-module/utils/crdt-blocks.mjs.map +2 -2
  58. package/build-module/utils/crdt.mjs +25 -20
  59. package/build-module/utils/crdt.mjs.map +2 -2
  60. package/build-types/actions.d.ts +12 -0
  61. package/build-types/actions.d.ts.map +1 -1
  62. package/build-types/awareness/base-awareness.d.ts.map +1 -1
  63. package/build-types/awareness/types.d.ts +0 -1
  64. package/build-types/awareness/types.d.ts.map +1 -1
  65. package/build-types/awareness/utils.d.ts +2 -3
  66. package/build-types/awareness/utils.d.ts.map +1 -1
  67. package/build-types/entities.d.ts.map +1 -1
  68. package/build-types/hooks/use-entity-block-editor.d.ts.map +1 -1
  69. package/build-types/index.d.ts +5 -0
  70. package/build-types/index.d.ts.map +1 -1
  71. package/build-types/private-actions.d.ts +8 -0
  72. package/build-types/private-actions.d.ts.map +1 -1
  73. package/build-types/private-apis.d.ts.map +1 -1
  74. package/build-types/private-selectors.d.ts +8 -1
  75. package/build-types/private-selectors.d.ts.map +1 -1
  76. package/build-types/reducer.d.ts +23 -0
  77. package/build-types/reducer.d.ts.map +1 -1
  78. package/build-types/resolvers.d.ts +3 -0
  79. package/build-types/resolvers.d.ts.map +1 -1
  80. package/build-types/selectors.d.ts +17 -0
  81. package/build-types/selectors.d.ts.map +1 -1
  82. package/build-types/utils/crdt-blocks.d.ts +9 -0
  83. package/build-types/utils/crdt-blocks.d.ts.map +1 -1
  84. package/build-types/utils/crdt.d.ts +6 -4
  85. package/build-types/utils/crdt.d.ts.map +1 -1
  86. package/package.json +18 -18
  87. package/src/actions.js +78 -0
  88. package/src/awareness/base-awareness.ts +1 -14
  89. package/src/awareness/test/awareness-state.ts +5 -1
  90. package/src/awareness/test/base-awareness.ts +2 -6
  91. package/src/awareness/test/post-editor-awareness.ts +0 -1
  92. package/src/awareness/test/typed-awareness.ts +5 -1
  93. package/src/awareness/test/utils.ts +12 -88
  94. package/src/awareness/types.ts +0 -1
  95. package/src/awareness/utils.ts +8 -82
  96. package/src/entities.js +7 -1
  97. package/src/hooks/test/use-post-editor-awareness-state.ts +0 -3
  98. package/src/hooks/use-entity-block-editor.js +15 -21
  99. package/src/index.js +7 -0
  100. package/src/private-actions.js +14 -0
  101. package/src/private-apis.js +5 -1
  102. package/src/private-selectors.ts +16 -1
  103. package/src/reducer.js +45 -0
  104. package/src/resolvers.js +29 -0
  105. package/src/selectors.ts +41 -0
  106. package/src/test/actions.js +79 -0
  107. package/src/test/entity-provider.js +74 -0
  108. package/src/test/resolvers.js +2 -0
  109. package/src/test/store.js +30 -0
  110. package/src/utils/crdt-blocks.ts +2 -2
  111. package/src/utils/crdt.ts +44 -29
  112. package/src/utils/test/crdt.ts +212 -7
@@ -6,7 +6,12 @@ import { createSelector, createRegistrySelector } from '@wordpress/data';
6
6
  /**
7
7
  * Internal dependencies
8
8
  */
9
- import { getDefaultTemplateId, getEntityRecord, type State } from './selectors';
9
+ import {
10
+ getDefaultTemplateId,
11
+ getEntityRecord,
12
+ type State,
13
+ type Icon,
14
+ } from './selectors';
10
15
  import { STORE_NAME } from './name';
11
16
  import { unlock } from './lock-unlock';
12
17
  import { getSyncManager } from './sync';
@@ -308,3 +313,13 @@ export function getEditorSettings(
308
313
  export function getEditorAssets( state: State ): Record< string, any > | null {
309
314
  return state.editorAssets;
310
315
  }
316
+
317
+ /**
318
+ * Returns the list of available icons.
319
+ *
320
+ * @param state Data state.
321
+ * @return The list of icons or empty array if not loaded.
322
+ */
323
+ export function getIcons( state: State ): Icon[] {
324
+ return state.icons ?? [];
325
+ }
package/src/reducer.js CHANGED
@@ -660,6 +660,49 @@ export function editorAssets( state = null, action ) {
660
660
  return state;
661
661
  }
662
662
 
663
+ /**
664
+ * Reducer managing icons.
665
+ *
666
+ * @param {Array} state Current state.
667
+ * @param {Object} action Action object.
668
+ *
669
+ * @return {Array} Updated state.
670
+ */
671
+ export function icons( state = [], action ) {
672
+ switch ( action.type ) {
673
+ case 'RECEIVE_ICONS':
674
+ return action.icons;
675
+ }
676
+ return state;
677
+ }
678
+
679
+ /**
680
+ * Reducer managing sync connection states for entities.
681
+ * Keyed by "kind/name:id" (e.g., "postType/post:123").
682
+ *
683
+ * @param {Object} state Current state.
684
+ * @param {Object} action Dispatched action.
685
+ *
686
+ * @return {Object} Updated state.
687
+ */
688
+ export function syncConnectionStatuses( state = {}, action ) {
689
+ switch ( action.type ) {
690
+ case 'SET_SYNC_CONNECTION_STATUS': {
691
+ const key = `${ action.kind }/${ action.name }:${ action.key }`;
692
+ return {
693
+ ...state,
694
+ [ key ]: action.status,
695
+ };
696
+ }
697
+ case 'CLEAR_SYNC_CONNECTION_STATUS': {
698
+ const key = `${ action.kind }/${ action.name }:${ action.key }`;
699
+ const { [ key ]: _, ...rest } = state;
700
+ return rest;
701
+ }
702
+ }
703
+ return state;
704
+ }
705
+
663
706
  export default combineReducers( {
664
707
  users,
665
708
  currentTheme,
@@ -682,4 +725,6 @@ export default combineReducers( {
682
725
  registeredPostMeta,
683
726
  editorSettings,
684
727
  editorAssets,
728
+ icons,
729
+ syncConnectionStatuses,
685
730
  } );
package/src/resolvers.js CHANGED
@@ -217,6 +217,15 @@ export const getEntityRecord =
217
217
  name,
218
218
  key
219
219
  ),
220
+ // Handle sync connection status changes.
221
+ onStatusChange: ( status ) => {
222
+ dispatch.setSyncConnectionStatus(
223
+ kind,
224
+ name,
225
+ key,
226
+ status
227
+ );
228
+ },
220
229
  // Refetch the current entity record from the database.
221
230
  refetchRecord: async () => {
222
231
  dispatch.receiveEntityRecords(
@@ -469,6 +478,14 @@ export const getEntityRecords =
469
478
  entityConfig.syncConfig,
470
479
  objectType,
471
480
  {
481
+ onStatusChange: ( status ) => {
482
+ dispatch.setSyncConnectionStatus(
483
+ kind,
484
+ name,
485
+ null,
486
+ status
487
+ );
488
+ },
472
489
  refetchRecords: async () => {
473
490
  dispatch.receiveEntityRecords(
474
491
  kind,
@@ -1254,3 +1271,15 @@ export const getEditorAssets =
1254
1271
  } );
1255
1272
  dispatch.receiveEditorAssets( assets );
1256
1273
  };
1274
+
1275
+ /**
1276
+ * Requests icons from the REST API.
1277
+ */
1278
+ export const getIcons =
1279
+ () =>
1280
+ async ( { dispatch } ) => {
1281
+ const icons = await apiFetch( {
1282
+ path: '/wp/v2/icons',
1283
+ } );
1284
+ dispatch.receiveIcons( icons );
1285
+ };
package/src/selectors.ts CHANGED
@@ -5,6 +5,7 @@ import { createSelector, createRegistrySelector } from '@wordpress/data';
5
5
  import { addQueryArgs } from '@wordpress/url';
6
6
  import type { UndoManager } from '@wordpress/undo-manager';
7
7
  import deprecated from '@wordpress/deprecated';
8
+ import type { ConnectionStatus } from '@wordpress/sync';
8
9
 
9
10
  /**
10
11
  * Internal dependencies
@@ -52,10 +53,18 @@ export interface State {
52
53
  registeredPostMeta: Record< string, Object >;
53
54
  editorSettings: Record< string, any > | null;
54
55
  editorAssets: Record< string, any > | null;
56
+ icons: Icon[];
57
+ syncConnectionStatuses?: Record< string, ConnectionStatus >;
55
58
  }
56
59
 
57
60
  type EntityRecordKey = string | number;
58
61
 
62
+ export interface Icon {
63
+ name: string;
64
+ content: string;
65
+ label: string;
66
+ }
67
+
59
68
  interface EntitiesState {
60
69
  config: EntityConfig[];
61
70
  records: Record< string, Record< string, EntityState< ET.EntityRecord > > >;
@@ -1595,3 +1604,35 @@ export const getRevision = createSelector(
1595
1604
  ];
1596
1605
  }
1597
1606
  );
1607
+
1608
+ /**
1609
+ * Returns the current sync connection status across all entities. Prioritizes
1610
+ * disconnected states, then connecting, then connected.
1611
+ *
1612
+ * @param state Data state.
1613
+ *
1614
+ * @return The current sync connection state, prioritized by importance.
1615
+ */
1616
+ export function getSyncConnectionStatus(
1617
+ state: State
1618
+ ): ConnectionStatus | undefined {
1619
+ if ( ! state.syncConnectionStatuses ) {
1620
+ return undefined;
1621
+ }
1622
+
1623
+ const PRIORITIZED_STATUSES = [ 'disconnected', 'connecting', 'connected' ];
1624
+
1625
+ let coalesced: ConnectionStatus | undefined;
1626
+
1627
+ for ( const status of Object.values( state.syncConnectionStatuses ) ) {
1628
+ if (
1629
+ ! coalesced ||
1630
+ PRIORITIZED_STATUSES.indexOf( status.status ) <
1631
+ PRIORITIZED_STATUSES.indexOf( coalesced.status )
1632
+ ) {
1633
+ coalesced = status;
1634
+ }
1635
+ }
1636
+
1637
+ return coalesced;
1638
+ }
@@ -10,6 +10,7 @@ jest.mock( '@wordpress/api-fetch' );
10
10
  */
11
11
  import {
12
12
  editEntityRecord,
13
+ clearEntityRecordEdits,
13
14
  saveEntityRecord,
14
15
  saveEditedEntityRecord,
15
16
  deleteEntityRecord,
@@ -454,6 +455,84 @@ describe( 'editEntityRecord', () => {
454
455
  } );
455
456
  } );
456
457
 
458
+ describe( 'clearEntityRecordEdits', () => {
459
+ it( 'throws when the entity does not have a loaded config.', async () => {
460
+ const select = {
461
+ getEntityConfig: jest.fn(),
462
+ };
463
+ const fulfillment = async () =>
464
+ clearEntityRecordEdits(
465
+ 'someKind',
466
+ 'someName',
467
+ 'someId'
468
+ )( { select } );
469
+ await expect( fulfillment ).rejects.toThrow(
470
+ `The entity being edited (someKind, someName) does not have a loaded config.`
471
+ );
472
+ } );
473
+
474
+ it( 'does nothing when there are no edits', () => {
475
+ const dispatch = jest.fn();
476
+ const select = {
477
+ getEntityConfig: () => ( {
478
+ kind: 'postType',
479
+ name: 'post',
480
+ } ),
481
+ getEntityRecordEdits: () => undefined,
482
+ };
483
+
484
+ clearEntityRecordEdits(
485
+ 'postType',
486
+ 'post',
487
+ 1
488
+ )( {
489
+ select,
490
+ dispatch,
491
+ } );
492
+
493
+ expect( dispatch ).not.toHaveBeenCalled();
494
+ } );
495
+
496
+ it( 'clears all edits for an entity record', () => {
497
+ const dispatch = jest.fn();
498
+ const select = {
499
+ getEntityConfig: () => ( {
500
+ kind: 'postType',
501
+ name: 'post',
502
+ } ),
503
+ getEntityRecordEdits: () => ( {
504
+ title: 'New Title',
505
+ content: 'New Content',
506
+ } ),
507
+ getEditedEntityRecord: () => ( {
508
+ id: 1,
509
+ title: 'New Title',
510
+ content: 'New Content',
511
+ } ),
512
+ };
513
+
514
+ clearEntityRecordEdits(
515
+ 'postType',
516
+ 'post',
517
+ 1
518
+ )( {
519
+ select,
520
+ dispatch,
521
+ } );
522
+
523
+ expect( dispatch ).toHaveBeenCalledWith( {
524
+ type: 'EDIT_ENTITY_RECORD',
525
+ kind: 'postType',
526
+ name: 'post',
527
+ recordId: 1,
528
+ edits: {
529
+ title: undefined,
530
+ content: undefined,
531
+ },
532
+ } );
533
+ } );
534
+ } );
535
+
457
536
  describe( 'deleteEntityRecord', () => {
458
537
  beforeEach( async () => {
459
538
  apiFetch.mockReset();
@@ -276,4 +276,78 @@ describe( 'useEntityBlockEditor', () => {
276
276
  'A paragraph<sup data-fn="abcd" class="fn"><a href="#abcd" id="abcd-link">2</a></sup>'
277
277
  );
278
278
  } );
279
+
280
+ it( 'preserves block clientIds across unmount and remount when content is unchanged', () => {
281
+ let blocks;
282
+ const TestComponent = () => {
283
+ [ blocks ] = useEntityBlockEditor( 'postType', 'post', {
284
+ id: 1,
285
+ } );
286
+ return <div />;
287
+ };
288
+
289
+ const { unmount } = render(
290
+ <RegistryProvider value={ registry }>
291
+ <TestComponent />
292
+ </RegistryProvider>
293
+ );
294
+
295
+ const firstClientIds = blocks.map( ( b ) => b.clientId );
296
+ expect( firstClientIds ).toHaveLength( 2 );
297
+
298
+ // Simulate navigating away.
299
+ unmount();
300
+
301
+ // Simulate navigating back — same entity, same content.
302
+ render(
303
+ <RegistryProvider value={ registry }>
304
+ <TestComponent />
305
+ </RegistryProvider>
306
+ );
307
+
308
+ // The cache should return the same block objects with the same clientIds.
309
+ expect( blocks.map( ( b ) => b.clientId ) ).toEqual( firstClientIds );
310
+ } );
311
+
312
+ it( 'returns new blocks when content changes', () => {
313
+ let blocks;
314
+ const TestComponent = () => {
315
+ [ blocks ] = useEntityBlockEditor( 'postType', 'post', {
316
+ id: 1,
317
+ } );
318
+ return <div />;
319
+ };
320
+
321
+ render(
322
+ <RegistryProvider value={ registry }>
323
+ <TestComponent />
324
+ </RegistryProvider>
325
+ );
326
+
327
+ const firstClientIds = blocks.map( ( b ) => b.clientId );
328
+
329
+ // Receive a new entity record with different content.
330
+ act( () => {
331
+ registry
332
+ .dispatch( coreDataStore )
333
+ .receiveEntityRecords( 'postType', 'post', [
334
+ {
335
+ id: 1,
336
+ type: 'post',
337
+ content: {
338
+ raw: '<!-- wp:test-block --><p>Different content</p><!-- /wp:test-block -->',
339
+ rendered: '<p>Different content</p>',
340
+ },
341
+ meta: { footnotes: '[]' },
342
+ },
343
+ ] );
344
+ } );
345
+
346
+ // Blocks should be new objects with new clientIds.
347
+ expect( blocks ).toHaveLength( 1 );
348
+ expect( blocks[ 0 ].attributes.content ).toEqual( 'Different content' );
349
+ expect( blocks.map( ( b ) => b.clientId ) ).not.toEqual(
350
+ firstClientIds
351
+ );
352
+ } );
279
353
  } );
@@ -171,6 +171,7 @@ describe( 'getEntityRecord', () => {
171
171
  addUndoMeta: expect.any( Function ),
172
172
  editRecord: expect.any( Function ),
173
173
  getEditedRecord: expect.any( Function ),
174
+ onStatusChange: expect.any( Function ),
174
175
  refetchRecord: expect.any( Function ),
175
176
  restoreUndoMeta: expect.any( Function ),
176
177
  saveRecord: expect.any( Function ),
@@ -226,6 +227,7 @@ describe( 'getEntityRecord', () => {
226
227
  addUndoMeta: expect.any( Function ),
227
228
  editRecord: expect.any( Function ),
228
229
  getEditedRecord: expect.any( Function ),
230
+ onStatusChange: expect.any( Function ),
229
231
  refetchRecord: expect.any( Function ),
230
232
  restoreUndoMeta: expect.any( Function ),
231
233
  saveRecord: expect.any( Function ),
package/src/test/store.js CHANGED
@@ -112,3 +112,33 @@ describe( 'getEntityRecord', () => {
112
112
  expect( triggerFetch ).not.toHaveBeenCalled();
113
113
  } );
114
114
  } );
115
+
116
+ describe( 'clearEntityRecordEdits', () => {
117
+ let registry;
118
+
119
+ beforeEach( () => {
120
+ registry = createTestRegistry();
121
+ triggerFetch.mockReset();
122
+ } );
123
+
124
+ it( 'should return the persisted record after clearing edits', () => {
125
+ const post = createTestPost( 1 );
126
+ const dispatch = registry.dispatch( coreDataStore );
127
+ const select = registry.select( coreDataStore );
128
+
129
+ dispatch.receiveEntityRecords( 'postType', 'post', post );
130
+ dispatch.editEntityRecord( 'postType', 'post', post.id, {
131
+ slug: 'updated-slug',
132
+ } );
133
+
134
+ expect(
135
+ select.getEditedEntityRecord( 'postType', 'post', post.id ).slug
136
+ ).toBe( 'updated-slug' );
137
+
138
+ dispatch.clearEntityRecordEdits( 'postType', 'post', post.id );
139
+
140
+ expect(
141
+ select.getEditedEntityRecord( 'postType', 'post', post.id )
142
+ ).toEqual( select.getRawEntityRecord( 'postType', 'post', post.id ) );
143
+ } );
144
+ } );
@@ -473,10 +473,10 @@ let localDoc: Y.Doc;
473
473
  * @param updatedValue The updated value.
474
474
  * @param cursorPosition The position of the cursor after the change occurs.
475
475
  */
476
- function mergeRichTextUpdate(
476
+ export function mergeRichTextUpdate(
477
477
  blockYText: Y.Text,
478
478
  updatedValue: string,
479
- cursorPosition: number | null
479
+ cursorPosition: number | null = null
480
480
  ): void {
481
481
  // Gutenberg does not use Yjs shared types natively, so we can only subscribe
482
482
  // to changes from store and apply them to Yjs types that we create and
package/src/utils/crdt.ts CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  import { BaseAwareness } from '../awareness/base-awareness';
22
22
  import {
23
23
  mergeCrdtBlocks,
24
+ mergeRichTextUpdate,
24
25
  type Block,
25
26
  type YBlock,
26
27
  type YBlocks,
@@ -45,6 +46,7 @@ import {
45
46
  // Changes that can be applied to a post entity record.
46
47
  export type PostChanges = Partial< Post > & {
47
48
  blocks?: Block[];
49
+ content?: Post[ 'content' ] | string;
48
50
  excerpt?: Post[ 'excerpt' ] | string;
49
51
  selection?: WPSelection;
50
52
  title?: Post[ 'title' ] | string;
@@ -53,11 +55,13 @@ export type PostChanges = Partial< Post > & {
53
55
  // A post record as represented in the CRDT document (Y.Map).
54
56
  export interface YPostRecord extends YMapRecord {
55
57
  author: number;
56
- blocks: YBlocks;
58
+ // Blocks are undefined when they need to be re-parsed from content.
59
+ blocks: YBlocks | undefined;
60
+ content: Y.Text;
57
61
  categories: number[];
58
62
  comment_status: string;
59
63
  date: string | null;
60
- excerpt: string;
64
+ excerpt: Y.Text;
61
65
  featured_media: number;
62
66
  format: string;
63
67
  meta: YMapWrap< YMapRecord >;
@@ -67,13 +71,14 @@ export interface YPostRecord extends YMapRecord {
67
71
  sticky: boolean;
68
72
  tags: number[];
69
73
  template: string;
70
- title: string;
74
+ title: Y.Text;
71
75
  }
72
76
 
73
77
  // Properties that are allowed to be synced for a post.
74
78
  const allowedPostProperties = new Set< string >( [
75
79
  'author',
76
80
  'blocks',
81
+ 'content',
77
82
  'categories',
78
83
  'comment_status',
79
84
  'date',
@@ -156,6 +161,14 @@ export function applyPostChangesToCRDTDoc(
156
161
 
157
162
  switch ( key ) {
158
163
  case 'blocks': {
164
+ // Blocks are undefined when they need to be re-parsed from content.
165
+ if ( ! newValue ) {
166
+ // Set to undefined instead of deleting the key. This is important
167
+ // since we iterate over the Y.Map keys in getPostChangesFromCRDTDoc.
168
+ ymap.set( key, undefined );
169
+ break;
170
+ }
171
+
159
172
  let currentBlocks = ymap.get( key );
160
173
 
161
174
  // Initialize.
@@ -164,9 +177,6 @@ export function applyPostChangesToCRDTDoc(
164
177
  ymap.set( key, currentBlocks );
165
178
  }
166
179
 
167
- // Block[] from local changes.
168
- const newBlocks = ( newValue as PostChanges[ 'blocks' ] ) ?? [];
169
-
170
180
  // Block changes from typing are bundled with a 'selection' update.
171
181
  // Pass the resulting cursor position to the mergeCrdtBlocks function.
172
182
  const cursorPosition =
@@ -174,15 +184,33 @@ export function applyPostChangesToCRDTDoc(
174
184
 
175
185
  // Merge blocks does not need `setValue` because it is operating on a
176
186
  // Yjs type that is already in the Y.Doc.
177
- mergeCrdtBlocks( currentBlocks, newBlocks, cursorPosition );
187
+ mergeCrdtBlocks( currentBlocks, newValue, cursorPosition );
178
188
  break;
179
189
  }
180
190
 
181
- case 'excerpt': {
182
- const currentValue = ymap.get( 'excerpt' );
183
- const rawNewValue = getRawValue( newValue );
191
+ case 'content':
192
+ case 'excerpt':
193
+ case 'title': {
194
+ const currentValue = ymap.get( key );
195
+ let rawValue = getRawValue( newValue );
196
+
197
+ // Copy logic from prePersistPostType to ensure that the "Auto
198
+ // Draft" template title is not synced.
199
+ if (
200
+ key === 'title' &&
201
+ ! currentValue?.toString() &&
202
+ 'Auto Draft' === rawValue
203
+ ) {
204
+ rawValue = '';
205
+ }
206
+
207
+ if ( currentValue instanceof Y.Text ) {
208
+ mergeRichTextUpdate( currentValue, rawValue ?? '' );
209
+ } else {
210
+ const newYText = new Y.Text( rawValue ?? '' );
211
+ ymap.set( key, newYText );
212
+ }
184
213
 
185
- updateMapValue( ymap, key, currentValue, rawNewValue );
186
214
  break;
187
215
  }
188
216
 
@@ -227,20 +255,6 @@ export function applyPostChangesToCRDTDoc(
227
255
  break;
228
256
  }
229
257
 
230
- case 'title': {
231
- const currentValue = ymap.get( key );
232
-
233
- // Copy logic from prePersistPostType to ensure that the "Auto
234
- // Draft" template title is not synced.
235
- let rawNewValue = getRawValue( newValue );
236
- if ( ! currentValue && 'Auto Draft' === rawNewValue ) {
237
- rawNewValue = '';
238
- }
239
-
240
- updateMapValue( ymap, key, currentValue, rawNewValue );
241
- break;
242
- }
243
-
244
258
  // Add support for additional properties here.
245
259
 
246
260
  default: {
@@ -318,11 +332,11 @@ export function getPostChangesFromCRDTDoc(
318
332
  ydoc.meta?.get( CRDT_DOC_META_PERSISTENCE_KEY ) &&
319
333
  editedRecord.content
320
334
  ) {
321
- const blocks = ymap.get( 'blocks' ) as YBlocks;
335
+ const blocksJson = ymap.get( 'blocks' )?.toJSON() ?? [];
336
+
322
337
  return (
323
- __unstableSerializeAndClean(
324
- blocks.toJSON()
325
- ).trim() !== editedRecord.content.raw.trim()
338
+ __unstableSerializeAndClean( blocksJson ).trim() !==
339
+ getRawValue( editedRecord.content )
326
340
  );
327
341
  }
328
342
 
@@ -375,6 +389,7 @@ export function getPostChangesFromCRDTDoc(
375
389
  return haveValuesChanged( currentValue, newValue );
376
390
  }
377
391
 
392
+ case 'content':
378
393
  case 'excerpt':
379
394
  case 'title': {
380
395
  return haveValuesChanged(