@wordpress/core-data 7.40.2-next.v.202602271551.0 → 7.40.2

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 (182) hide show
  1. package/README.md +0 -27
  2. package/build/actions.cjs +9 -26
  3. package/build/actions.cjs.map +2 -2
  4. package/build/awareness/post-editor-awareness.cjs +45 -6
  5. package/build/awareness/post-editor-awareness.cjs.map +2 -2
  6. package/build/awareness/types.cjs.map +1 -1
  7. package/build/entities.cjs +36 -16
  8. package/build/entities.cjs.map +2 -2
  9. package/build/entity-provider.cjs +15 -6
  10. package/build/entity-provider.cjs.map +2 -2
  11. package/build/hooks/use-entity-prop.cjs +33 -2
  12. package/build/hooks/use-entity-prop.cjs.map +2 -2
  13. package/build/hooks/use-post-editor-awareness-state.cjs +120 -1
  14. package/build/hooks/use-post-editor-awareness-state.cjs.map +2 -2
  15. package/build/index.cjs +0 -3
  16. package/build/index.cjs.map +2 -2
  17. package/build/private-actions.cjs +27 -3
  18. package/build/private-actions.cjs.map +2 -2
  19. package/build/private-apis.cjs +14 -5
  20. package/build/private-apis.cjs.map +3 -3
  21. package/build/private-selectors.cjs +22 -2
  22. package/build/private-selectors.cjs.map +2 -2
  23. package/build/queried-data/actions.cjs +1 -1
  24. package/build/queried-data/actions.cjs.map +2 -2
  25. package/build/queried-data/reducer.cjs +19 -13
  26. package/build/queried-data/reducer.cjs.map +2 -2
  27. package/build/reducer.cjs +19 -2
  28. package/build/reducer.cjs.map +2 -2
  29. package/build/resolvers.cjs +24 -17
  30. package/build/resolvers.cjs.map +2 -2
  31. package/build/selectors.cjs +0 -15
  32. package/build/selectors.cjs.map +2 -2
  33. package/build/sync.cjs +12 -3
  34. package/build/sync.cjs.map +2 -2
  35. package/build/types.cjs.map +1 -1
  36. package/build/utils/block-selection-history.cjs +1 -1
  37. package/build/utils/block-selection-history.cjs.map +2 -2
  38. package/build/utils/crdt-blocks.cjs +85 -28
  39. package/build/utils/crdt-blocks.cjs.map +2 -2
  40. package/build/utils/crdt-selection.cjs +4 -1
  41. package/build/utils/crdt-selection.cjs.map +2 -2
  42. package/build/utils/crdt-text.cjs +52 -0
  43. package/build/utils/crdt-text.cjs.map +7 -0
  44. package/build/utils/crdt-user-selections.cjs +16 -6
  45. package/build/utils/crdt-user-selections.cjs.map +3 -3
  46. package/build/utils/crdt-utils.cjs +54 -2
  47. package/build/utils/crdt-utils.cjs.map +2 -2
  48. package/build/utils/crdt.cjs +26 -28
  49. package/build/utils/crdt.cjs.map +2 -2
  50. package/build-module/actions.mjs +14 -26
  51. package/build-module/actions.mjs.map +2 -2
  52. package/build-module/awareness/post-editor-awareness.mjs +47 -7
  53. package/build-module/awareness/post-editor-awareness.mjs.map +2 -2
  54. package/build-module/entities.mjs +37 -16
  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 +117 -1
  61. package/build-module/hooks/use-post-editor-awareness-state.mjs.map +2 -2
  62. package/build-module/index.mjs +0 -2
  63. package/build-module/index.mjs.map +2 -2
  64. package/build-module/private-actions.mjs +24 -2
  65. package/build-module/private-actions.mjs.map +2 -2
  66. package/build-module/private-apis.mjs +21 -6
  67. package/build-module/private-apis.mjs.map +3 -3
  68. package/build-module/private-selectors.mjs +19 -1
  69. package/build-module/private-selectors.mjs.map +2 -2
  70. package/build-module/queried-data/actions.mjs +1 -1
  71. package/build-module/queried-data/actions.mjs.map +2 -2
  72. package/build-module/queried-data/reducer.mjs +19 -13
  73. package/build-module/queried-data/reducer.mjs.map +2 -2
  74. package/build-module/reducer.mjs +18 -2
  75. package/build-module/reducer.mjs.map +2 -2
  76. package/build-module/resolvers.mjs +24 -17
  77. package/build-module/resolvers.mjs.map +2 -2
  78. package/build-module/selectors.mjs +0 -14
  79. package/build-module/selectors.mjs.map +2 -2
  80. package/build-module/sync.mjs +8 -2
  81. package/build-module/sync.mjs.map +2 -2
  82. package/build-module/utils/block-selection-history.mjs +5 -2
  83. package/build-module/utils/block-selection-history.mjs.map +2 -2
  84. package/build-module/utils/crdt-blocks.mjs +84 -28
  85. package/build-module/utils/crdt-blocks.mjs.map +2 -2
  86. package/build-module/utils/crdt-selection.mjs +8 -2
  87. package/build-module/utils/crdt-selection.mjs.map +2 -2
  88. package/build-module/utils/crdt-text.mjs +26 -0
  89. package/build-module/utils/crdt-text.mjs.map +7 -0
  90. package/build-module/utils/crdt-user-selections.mjs +16 -7
  91. package/build-module/utils/crdt-user-selections.mjs.map +3 -3
  92. package/build-module/utils/crdt-utils.mjs +51 -1
  93. package/build-module/utils/crdt-utils.mjs.map +2 -2
  94. package/build-module/utils/crdt.mjs +26 -28
  95. package/build-module/utils/crdt.mjs.map +2 -2
  96. package/build-types/actions.d.ts +0 -11
  97. package/build-types/actions.d.ts.map +1 -1
  98. package/build-types/awareness/post-editor-awareness.d.ts +2 -2
  99. package/build-types/awareness/post-editor-awareness.d.ts.map +1 -1
  100. package/build-types/awareness/types.d.ts +6 -1
  101. package/build-types/awareness/types.d.ts.map +1 -1
  102. package/build-types/entities.d.ts +20 -1
  103. package/build-types/entities.d.ts.map +1 -1
  104. package/build-types/entity-provider.d.ts +11 -6
  105. package/build-types/entity-provider.d.ts.map +1 -1
  106. package/build-types/hooks/use-entity-prop.d.ts.map +1 -1
  107. package/build-types/hooks/use-post-editor-awareness-state.d.ts +40 -7
  108. package/build-types/hooks/use-post-editor-awareness-state.d.ts.map +1 -1
  109. package/build-types/index.d.ts +0 -3
  110. package/build-types/index.d.ts.map +1 -1
  111. package/build-types/parsed-blocks-cache.d.ts +10 -0
  112. package/build-types/parsed-blocks-cache.d.ts.map +1 -0
  113. package/build-types/private-actions.d.ts +12 -0
  114. package/build-types/private-actions.d.ts.map +1 -1
  115. package/build-types/private-apis.d.ts +21 -1
  116. package/build-types/private-apis.d.ts.map +1 -1
  117. package/build-types/private-selectors.d.ts +17 -0
  118. package/build-types/private-selectors.d.ts.map +1 -1
  119. package/build-types/queried-data/reducer.d.ts.map +1 -1
  120. package/build-types/reducer.d.ts +15 -0
  121. package/build-types/reducer.d.ts.map +1 -1
  122. package/build-types/resolvers.d.ts.map +1 -1
  123. package/build-types/selectors.d.ts +1 -9
  124. package/build-types/selectors.d.ts.map +1 -1
  125. package/build-types/sync.d.ts +2 -2
  126. package/build-types/sync.d.ts.map +1 -1
  127. package/build-types/types.d.ts +10 -2
  128. package/build-types/types.d.ts.map +1 -1
  129. package/build-types/utils/block-selection-history.d.ts.map +1 -1
  130. package/build-types/utils/crdt-blocks.d.ts +11 -0
  131. package/build-types/utils/crdt-blocks.d.ts.map +1 -1
  132. package/build-types/utils/crdt-selection.d.ts.map +1 -1
  133. package/build-types/utils/crdt-text.d.ts +16 -0
  134. package/build-types/utils/crdt-text.d.ts.map +1 -0
  135. package/build-types/utils/crdt-user-selections.d.ts +17 -4
  136. package/build-types/utils/crdt-user-selections.d.ts.map +1 -1
  137. package/build-types/utils/crdt-utils.d.ts +20 -0
  138. package/build-types/utils/crdt-utils.d.ts.map +1 -1
  139. package/build-types/utils/crdt.d.ts +13 -7
  140. package/build-types/utils/crdt.d.ts.map +1 -1
  141. package/build-types/utils/normalize-query-for-resolution.d.ts +12 -0
  142. package/build-types/utils/normalize-query-for-resolution.d.ts.map +1 -0
  143. package/build-types/utils/save-crdt-doc.d.ts +8 -0
  144. package/build-types/utils/save-crdt-doc.d.ts.map +1 -0
  145. package/build-types/utils/test/crdt-utils.d.ts +2 -0
  146. package/build-types/utils/test/crdt-utils.d.ts.map +1 -0
  147. package/package.json +18 -18
  148. package/src/actions.js +26 -41
  149. package/src/awareness/post-editor-awareness.ts +106 -7
  150. package/src/awareness/test/post-editor-awareness.ts +50 -10
  151. package/src/awareness/types.ts +7 -1
  152. package/src/entities.js +43 -17
  153. package/src/entity-provider.js +24 -11
  154. package/src/hooks/test/use-post-editor-awareness-state.ts +446 -3
  155. package/src/hooks/use-entity-prop.js +43 -3
  156. package/src/hooks/use-post-editor-awareness-state.ts +229 -7
  157. package/src/index.js +0 -6
  158. package/src/private-actions.js +43 -1
  159. package/src/{private-apis.js → private-apis.ts} +21 -3
  160. package/src/private-selectors.ts +43 -0
  161. package/src/queried-data/actions.js +1 -1
  162. package/src/queried-data/reducer.js +26 -14
  163. package/src/reducer.js +36 -1
  164. package/src/resolvers.js +33 -23
  165. package/src/selectors.ts +1 -32
  166. package/src/sync.ts +6 -0
  167. package/src/test/entities.js +204 -16
  168. package/src/test/private-actions.js +1 -1
  169. package/src/test/resolvers.js +144 -57
  170. package/src/test/store.js +116 -0
  171. package/src/types.ts +20 -2
  172. package/src/utils/block-selection-history.ts +5 -2
  173. package/src/utils/crdt-blocks.ts +159 -56
  174. package/src/utils/crdt-selection.ts +8 -2
  175. package/src/utils/crdt-text.ts +43 -0
  176. package/src/utils/crdt-user-selections.ts +29 -8
  177. package/src/utils/crdt-utils.ts +99 -0
  178. package/src/utils/crdt.ts +50 -40
  179. package/src/utils/test/crdt-blocks.ts +588 -24
  180. package/src/utils/test/crdt-user-selections.ts +5 -0
  181. package/src/utils/test/crdt-utils.ts +387 -0
  182. package/src/utils/test/crdt.ts +291 -54
package/src/actions.js CHANGED
@@ -19,9 +19,17 @@ import { receiveItems, removeItems, receiveQueriedItems } from './queried-data';
19
19
  import { DEFAULT_ENTITY_KEY } from './entities';
20
20
  import { createBatch } from './batch';
21
21
  import { STORE_NAME } from './name';
22
- import { LOCAL_EDITOR_ORIGIN, getSyncManager } from './sync';
22
+ import {
23
+ LOCAL_EDITOR_ORIGIN,
24
+ LOCAL_UNDO_IGNORED_ORIGIN,
25
+ getSyncManager,
26
+ } from './sync';
23
27
  import logEntityDeprecation from './utils/log-entity-deprecation';
24
28
 
29
+ function addTitleToAutoDraft( record ) {
30
+ return record.status === 'auto-draft' ? { ...record, title: '' } : record;
31
+ }
32
+
25
33
  /**
26
34
  * Returns an action object used in signalling that authors have been received.
27
35
  * Ignored from documentation as it's internal to the data store.
@@ -96,12 +104,9 @@ export function receiveEntityRecords(
96
104
  // Auto drafts should not have titles, but some plugins rely on them so we can't filter this
97
105
  // on the server.
98
106
  if ( kind === 'postType' ) {
99
- records = ( Array.isArray( records ) ? records : [ records ] ).map(
100
- ( record ) =>
101
- record.status === 'auto-draft'
102
- ? { ...record, title: '' }
103
- : record
104
- );
107
+ records = Array.isArray( records )
108
+ ? records.map( addTitleToAutoDraft )
109
+ : addTitleToAutoDraft( records );
105
110
  }
106
111
  let action;
107
112
  if ( query ) {
@@ -445,11 +450,18 @@ export const editEntityRecord =
445
450
  ? false
446
451
  : ! options.isCached;
447
452
 
453
+ // Use an untracked origin for undoIgnore changes so the Yjs
454
+ // UndoManager does not capture them as undo levels, while
455
+ // still syncing them to the CRDT document and other peers.
456
+ const origin = options.undoIgnore
457
+ ? LOCAL_UNDO_IGNORED_ORIGIN
458
+ : LOCAL_EDITOR_ORIGIN;
459
+
448
460
  getSyncManager()?.update(
449
461
  objectType,
450
462
  objectId,
451
463
  editsWithMerges,
452
- LOCAL_EDITOR_ORIGIN,
464
+ origin,
453
465
  { isNewUndoLevel }
454
466
  );
455
467
  }
@@ -773,10 +785,10 @@ export const saveEntityRecord =
773
785
  if ( entityConfig.__unstablePrePersist ) {
774
786
  edits = {
775
787
  ...edits,
776
- ...entityConfig.__unstablePrePersist(
788
+ ...( await entityConfig.__unstablePrePersist(
777
789
  persistedRecord,
778
790
  edits
779
- ),
791
+ ) ),
780
792
  };
781
793
  }
782
794
  updatedRecord = await __unstableFetch( {
@@ -793,11 +805,13 @@ export const saveEntityRecord =
793
805
  edits
794
806
  );
795
807
  if ( entityConfig.syncConfig ) {
808
+ // Use an untracked origin so that the save
809
+ // response does not create undo levels.
796
810
  getSyncManager()?.update(
797
811
  `${ kind }/${ name }`,
798
812
  recordId,
799
813
  updatedRecord,
800
- LOCAL_EDITOR_ORIGIN,
814
+ LOCAL_UNDO_IGNORED_ORIGIN,
801
815
  { isSave: true }
802
816
  );
803
817
  }
@@ -1108,7 +1122,7 @@ export const receiveRevisions =
1108
1122
  dispatch( {
1109
1123
  type: 'RECEIVE_ITEM_REVISIONS',
1110
1124
  key,
1111
- items: Array.isArray( records ) ? records : [ records ],
1125
+ items: records,
1112
1126
  recordKey,
1113
1127
  meta,
1114
1128
  query,
@@ -1117,32 +1131,3 @@ export const receiveRevisions =
1117
1131
  invalidateCache,
1118
1132
  } );
1119
1133
  };
1120
-
1121
- /**
1122
- * Returns an action object used to set the sync connection status for an entity or collection.
1123
- *
1124
- * @param {string} kind Kind of the entity.
1125
- * @param {string} name Name of the entity.
1126
- * @param {number|string|null} key The entity key, or null for collections.
1127
- * @param {Object|null} status The connection state object or null on unload.
1128
- *
1129
- * @return {Object} Action object.
1130
- */
1131
- export function setSyncConnectionStatus( kind, name, key, status ) {
1132
- if ( ! status ) {
1133
- return {
1134
- type: 'CLEAR_SYNC_CONNECTION_STATUS',
1135
- kind,
1136
- name,
1137
- key,
1138
- };
1139
- }
1140
-
1141
- return {
1142
- type: 'SET_SYNC_CONNECTION_STATUS',
1143
- kind,
1144
- name,
1145
- key,
1146
- status,
1147
- };
1148
- }
@@ -16,10 +16,12 @@ import {
16
16
  LOCAL_CURSOR_UPDATE_DEBOUNCE_IN_MS,
17
17
  } from './config';
18
18
  import { STORE_NAME as coreStore } from '../name';
19
+ import { htmlIndexToRichTextOffset } from '../utils/crdt-utils';
19
20
  import {
20
21
  areSelectionsStatesEqual,
21
22
  getSelectionState,
22
23
  SelectionType,
24
+ SelectionDirection,
23
25
  } from '../utils/crdt-user-selections';
24
26
 
25
27
  import type { SelectionState, WPBlockSelection } from '../types';
@@ -69,6 +71,18 @@ export class PostEditorAwareness extends BaseAwarenessState< PostEditorState > {
69
71
  let selectionEnd = getSelectionEnd();
70
72
  let localCursorTimeout: NodeJS.Timeout | null = null;
71
73
 
74
+ // During rapid selection changes (e.g. undo restoring content and
75
+ // selection), the debounce discards intermediate events. If we use the
76
+ // last intermediate state instead of the overall change it can produce
77
+ // the wrong direction.
78
+ // Use selectionBeforeDebounce to capture the selection state from
79
+ // before the debounce window so that direction is computed across the
80
+ // full window when it fires.
81
+ let selectionBeforeDebounce: {
82
+ start: WPBlockSelection;
83
+ end: WPBlockSelection;
84
+ } | null = null;
85
+
72
86
  subscribe( () => {
73
87
  const newSelectionStart = getSelectionStart();
74
88
  const newSelectionEnd = getSelectionEnd();
@@ -80,6 +94,15 @@ export class PostEditorAwareness extends BaseAwarenessState< PostEditorState > {
80
94
  return;
81
95
  }
82
96
 
97
+ // On the first change of a debounce window, snapshot the state
98
+ // we're moving away from.
99
+ if ( ! selectionBeforeDebounce ) {
100
+ selectionBeforeDebounce = {
101
+ start: selectionStart,
102
+ end: selectionEnd,
103
+ };
104
+ }
105
+
83
106
  selectionStart = newSelectionStart;
84
107
  selectionEnd = newSelectionEnd;
85
108
 
@@ -103,10 +126,29 @@ export class PostEditorAwareness extends BaseAwarenessState< PostEditorState > {
103
126
  }
104
127
 
105
128
  localCursorTimeout = setTimeout( () => {
129
+ // Compute direction across the full debounce window.
130
+ const selectionStateOptions: {
131
+ selectionDirection?: SelectionDirection;
132
+ } = {};
133
+
134
+ if ( selectionBeforeDebounce ) {
135
+ selectionStateOptions.selectionDirection =
136
+ detectSelectionDirection(
137
+ selectionBeforeDebounce.start,
138
+ selectionBeforeDebounce.end,
139
+ selectionStart,
140
+ selectionEnd
141
+ );
142
+
143
+ // Reset debounced selection state.
144
+ selectionBeforeDebounce = null;
145
+ }
146
+
106
147
  const selectionState = getSelectionState(
107
148
  selectionStart,
108
149
  selectionEnd,
109
- this.doc
150
+ this.doc,
151
+ selectionStateOptions
110
152
  );
111
153
 
112
154
  this.setThrottledLocalStateField(
@@ -171,6 +213,10 @@ export class PostEditorAwareness extends BaseAwarenessState< PostEditorState > {
171
213
  return state1 === state2;
172
214
  }
173
215
 
216
+ if ( ! state1.selection || ! state2.selection ) {
217
+ return state1.selection === state2.selection;
218
+ }
219
+
174
220
  return areSelectionsStatesEqual( state1.selection, state2.selection );
175
221
  }
176
222
 
@@ -188,14 +234,14 @@ export class PostEditorAwareness extends BaseAwarenessState< PostEditorState > {
188
234
  * clientIds (e.g. in "Show Template" mode where blocks are cloned).
189
235
  *
190
236
  * @param selection - The selection state.
191
- * @return The text index and block client ID, or nulls if not resolvable.
237
+ * @return The rich-text offset and block client ID, or nulls if not resolvable.
192
238
  */
193
239
  public convertSelectionStateToAbsolute( selection: SelectionState ): {
194
- textIndex: number | null;
240
+ richTextOffset: number | null;
195
241
  localClientId: string | null;
196
242
  } {
197
243
  if ( selection.type === SelectionType.None ) {
198
- return { textIndex: null, localClientId: null };
244
+ return { richTextOffset: null, localClientId: null };
199
245
  }
200
246
 
201
247
  if ( selection.type === SelectionType.WholeBlock ) {
@@ -218,7 +264,7 @@ export class PostEditorAwareness extends BaseAwarenessState< PostEditorState > {
218
264
  }
219
265
  }
220
266
 
221
- return { textIndex: null, localClientId };
267
+ return { richTextOffset: null, localClientId };
222
268
  }
223
269
 
224
270
  // Text-based selections: resolve cursor position and navigate up.
@@ -233,7 +279,7 @@ export class PostEditorAwareness extends BaseAwarenessState< PostEditorState > {
233
279
  );
234
280
 
235
281
  if ( ! absolutePosition ) {
236
- return { textIndex: null, localClientId: null };
282
+ return { richTextOffset: null, localClientId: null };
237
283
  }
238
284
 
239
285
  // Navigate up: Y.Text -> attributes Y.Map -> block Y.Map
@@ -242,7 +288,13 @@ export class PostEditorAwareness extends BaseAwarenessState< PostEditorState > {
242
288
  yType instanceof Y.Map ? getBlockPathInYdoc( yType ) : null;
243
289
  const localClientId = path ? resolveBlockClientIdByPath( path ) : null;
244
290
 
245
- return { textIndex: absolutePosition.index, localClientId };
291
+ return {
292
+ richTextOffset: htmlIndexToRichTextOffset(
293
+ absolutePosition.type.toString(),
294
+ absolutePosition.index
295
+ ),
296
+ localClientId,
297
+ };
246
298
  }
247
299
 
248
300
  /**
@@ -325,3 +377,50 @@ export class PostEditorAwareness extends BaseAwarenessState< PostEditorState > {
325
377
  };
326
378
  }
327
379
  }
380
+
381
+ /**
382
+ * Detect the direction of a selection change by comparing old and new edges.
383
+ *
384
+ * When the user extends a selection backward (e.g. Shift+Left), the
385
+ * selectionStart edge moves while selectionEnd stays fixed, so the caret
386
+ * is at the start. The reverse is true for forward extension.
387
+ *
388
+ * @param prevStart - The previous selectionStart.
389
+ * @param prevEnd - The previous selectionEnd.
390
+ * @param newStart - The new selectionStart.
391
+ * @param newEnd - The new selectionEnd.
392
+ * @return The detected direction, defaulting to Forward when indeterminate.
393
+ */
394
+ function detectSelectionDirection(
395
+ prevStart: WPBlockSelection,
396
+ prevEnd: WPBlockSelection,
397
+ newStart: WPBlockSelection,
398
+ newEnd: WPBlockSelection
399
+ ): SelectionDirection {
400
+ const startMoved = ! areBlockSelectionsEqual( prevStart, newStart );
401
+ const endMoved = ! areBlockSelectionsEqual( prevEnd, newEnd );
402
+
403
+ if ( startMoved && ! endMoved ) {
404
+ return SelectionDirection.Backward;
405
+ }
406
+
407
+ return SelectionDirection.Forward;
408
+ }
409
+
410
+ /**
411
+ * Compare two WPBlockSelection objects by value.
412
+ *
413
+ * @param a - First selection.
414
+ * @param b - Second selection.
415
+ * @return True if all fields are equal.
416
+ */
417
+ function areBlockSelectionsEqual(
418
+ a: WPBlockSelection,
419
+ b: WPBlockSelection
420
+ ): boolean {
421
+ return (
422
+ a.clientId === b.clientId &&
423
+ a.attributeKey === b.attributeKey &&
424
+ a.offset === b.offset
425
+ );
426
+ }
@@ -23,6 +23,11 @@ jest.mock( '@wordpress/data', () => ( {
23
23
  select: jest.fn(),
24
24
  subscribe: jest.fn(),
25
25
  resolveSelect: jest.fn(),
26
+ // Needed because @wordpress/rich-text initialises its store at import time.
27
+ combineReducers: jest.fn( () => jest.fn( () => ( {} ) ) ),
28
+ createReduxStore: jest.fn( () => ( {} ) ),
29
+ register: jest.fn(),
30
+ createSelector: ( selector: Function ) => selector,
26
31
  } ) );
27
32
 
28
33
  jest.mock( '@wordpress/block-editor', () => ( {
@@ -422,6 +427,41 @@ describe( 'PostEditorAwareness', () => {
422
427
  // Callback should not be called for equal editor states
423
428
  expect( callback ).not.toHaveBeenCalled();
424
429
  } );
430
+
431
+ test( 'should not notify when editorState without selection is unchanged', () => {
432
+ const awareness = new PostEditorAwareness(
433
+ doc,
434
+ 'postType',
435
+ 'post',
436
+ 123
437
+ );
438
+ awareness.setUp();
439
+
440
+ awareness.setLocalStateField( 'editorState', {} );
441
+
442
+ const callback = jest.fn();
443
+ awareness.onStateChange( callback );
444
+
445
+ awareness.emit( 'change', [
446
+ {
447
+ added: [],
448
+ updated: [ awareness.clientID ],
449
+ removed: [],
450
+ },
451
+ ] );
452
+ callback.mockClear();
453
+
454
+ awareness.setLocalStateField( 'editorState', {} );
455
+ awareness.emit( 'change', [
456
+ {
457
+ added: [],
458
+ updated: [ awareness.clientID ],
459
+ removed: [],
460
+ },
461
+ ] );
462
+
463
+ expect( callback ).not.toHaveBeenCalled();
464
+ } );
425
465
  } );
426
466
 
427
467
  describe( 'convertSelectionStateToAbsolute', () => {
@@ -456,7 +496,7 @@ describe( 'PostEditorAwareness', () => {
456
496
  awareness.convertSelectionStateToAbsolute( selection );
457
497
 
458
498
  // Should return nulls when the relative position's type cannot be found
459
- expect( result.textIndex ).toBeNull();
499
+ expect( result.richTextOffset ).toBeNull();
460
500
  expect( result.localClientId ).toBeNull();
461
501
  } );
462
502
 
@@ -494,7 +534,7 @@ describe( 'PostEditorAwareness', () => {
494
534
  const result =
495
535
  awareness.convertSelectionStateToAbsolute( selection );
496
536
 
497
- expect( result.textIndex ).toBe( 5 );
537
+ expect( result.richTextOffset ).toBe( 5 );
498
538
  expect( result.localClientId ).toBe( 'block-1' );
499
539
  } );
500
540
 
@@ -526,7 +566,7 @@ describe( 'PostEditorAwareness', () => {
526
566
  const result =
527
567
  awareness.convertSelectionStateToAbsolute( selection );
528
568
 
529
- expect( result.textIndex ).toBeNull();
569
+ expect( result.richTextOffset ).toBeNull();
530
570
  expect( result.localClientId ).toBe( 'block-1' );
531
571
  } );
532
572
  } );
@@ -732,7 +772,7 @@ describe( 'PostEditorAwareness', () => {
732
772
  const result =
733
773
  awareness.convertSelectionStateToAbsolute( selection );
734
774
 
735
- expect( result.textIndex ).toBe( 2 );
775
+ expect( result.richTextOffset ).toBe( 2 );
736
776
  expect( result.localClientId ).toBe( 'local-2' );
737
777
 
738
778
  nestedDoc.destroy();
@@ -805,7 +845,7 @@ describe( 'PostEditorAwareness', () => {
805
845
  const result =
806
846
  awareness.convertSelectionStateToAbsolute( selection );
807
847
 
808
- expect( result.textIndex ).toBe( 5 );
848
+ expect( result.richTextOffset ).toBe( 5 );
809
849
  expect( result.localClientId ).toBe( 'local-inner-1' );
810
850
 
811
851
  nestedDoc.destroy();
@@ -860,7 +900,7 @@ describe( 'PostEditorAwareness', () => {
860
900
  const result =
861
901
  awareness.convertSelectionStateToAbsolute( selection );
862
902
 
863
- expect( result.textIndex ).toBeNull();
903
+ expect( result.richTextOffset ).toBeNull();
864
904
  expect( result.localClientId ).toBe( 'local-img' );
865
905
 
866
906
  nestedDoc.destroy();
@@ -954,7 +994,7 @@ describe( 'PostEditorAwareness', () => {
954
994
  const result =
955
995
  awareness.convertSelectionStateToAbsolute( selection );
956
996
 
957
- expect( result.textIndex ).toBe( 7 );
997
+ expect( result.richTextOffset ).toBe( 7 );
958
998
  expect( result.localClientId ).toBe( 'local-deep-1' );
959
999
 
960
1000
  nestedDoc.destroy();
@@ -1055,7 +1095,7 @@ describe( 'PostEditorAwareness', () => {
1055
1095
  const result =
1056
1096
  awareness.convertSelectionStateToAbsolute( selection );
1057
1097
 
1058
- expect( result.textIndex ).toBe( 4 );
1098
+ expect( result.richTextOffset ).toBe( 4 );
1059
1099
  // Should resolve to the post-content inner block, not a template block
1060
1100
  expect( result.localClientId ).toBe( 'local-para-1' );
1061
1101
  // Verify getBlocks was called with the post-content clientId
@@ -1127,7 +1167,7 @@ describe( 'PostEditorAwareness', () => {
1127
1167
  const result =
1128
1168
  awareness.convertSelectionStateToAbsolute( selection );
1129
1169
 
1130
- expect( result.textIndex ).toBeNull();
1170
+ expect( result.richTextOffset ).toBeNull();
1131
1171
  expect( result.localClientId ).toBe( 'local-img' );
1132
1172
 
1133
1173
  templateDoc.destroy();
@@ -1176,7 +1216,7 @@ describe( 'PostEditorAwareness', () => {
1176
1216
  const result =
1177
1217
  awareness.convertSelectionStateToAbsolute( selection );
1178
1218
 
1179
- expect( result.textIndex ).toBe( 3 );
1219
+ expect( result.richTextOffset ).toBe( 3 );
1180
1220
  expect( result.localClientId ).toBe( 'local-para' );
1181
1221
 
1182
1222
  normalDoc.destroy();
@@ -30,7 +30,7 @@ export interface BaseState {
30
30
  * The editor state includes information about the collaborator's current selection.
31
31
  */
32
32
  export interface EditorState {
33
- selection: SelectionState;
33
+ selection?: SelectionState;
34
34
  }
35
35
 
36
36
  /**
@@ -94,3 +94,9 @@ export type EqualityFieldCheck< State, FieldName extends keyof State > = (
94
94
  value1?: State[ FieldName ],
95
95
  value2?: State[ FieldName ]
96
96
  ) => boolean;
97
+
98
+ export interface PostSaveEvent {
99
+ savedAt: number;
100
+ savedByClientId: number;
101
+ postStatus: string | undefined;
102
+ }
package/src/entities.js CHANGED
@@ -17,6 +17,7 @@ import { PostEditorAwareness } from './awareness/post-editor-awareness';
17
17
  import { getSyncManager } from './sync';
18
18
  import {
19
19
  applyPostChangesToCRDTDoc,
20
+ defaultCollectionSyncConfig,
20
21
  defaultSyncConfig,
21
22
  getPostChangesFromCRDTDoc,
22
23
  POST_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
@@ -141,6 +142,7 @@ export const rootEntitiesConfig = [
141
142
  plural: 'comments',
142
143
  label: __( 'Comment' ),
143
144
  supportsPagination: true,
145
+ syncConfig: defaultCollectionSyncConfig,
144
146
  },
145
147
  {
146
148
  name: 'menu',
@@ -236,14 +238,7 @@ export const rootEntitiesConfig = [
236
238
  plural: 'icons',
237
239
  key: 'name',
238
240
  },
239
- ].map( ( entity ) => {
240
- const syncEnabledRootEntities = new Set( [ 'comment' ] );
241
-
242
- if ( syncEnabledRootEntities.has( entity.name ) ) {
243
- entity.syncConfig = defaultSyncConfig;
244
- }
245
- return entity;
246
- } );
241
+ ];
247
242
 
248
243
  export const deprecatedEntities = {
249
244
  root: {
@@ -275,9 +270,9 @@ export const additionalEntityConfigLoaders = [
275
270
  * @param {Object} edits Edits.
276
271
  * @param {string} name Post type name.
277
272
  * @param {boolean} isTemplate Whether the post type is a template.
278
- * @return {Object} Updated edits.
273
+ * @return {Promise< Object >} Updated edits.
279
274
  */
280
- export const prePersistPostType = (
275
+ export const prePersistPostType = async (
281
276
  persistedRecord,
282
277
  edits,
283
278
  name,
@@ -306,7 +301,7 @@ export const prePersistPostType = (
306
301
  if ( persistedRecord ) {
307
302
  const objectType = `postType/${ name }`;
308
303
  const objectId = persistedRecord.id;
309
- const serializedDoc = getSyncManager()?.createPersistedCRDTDoc(
304
+ const serializedDoc = await getSyncManager()?.createPersistedCRDTDoc(
310
305
  objectType,
311
306
  objectId
312
307
  );
@@ -328,15 +323,42 @@ export const prePersistPostType = (
328
323
  * @return {Promise} Entities promise
329
324
  */
330
325
  async function loadPostTypeEntities() {
331
- const postTypes = await apiFetch( {
332
- path: '/wp/v2/types?context=view',
333
- } );
326
+ const postTypesPromise = apiFetch( { path: '/wp/v2/types?context=view' } );
327
+ const taxonomiesPromise = window._wpCollaborationEnabled
328
+ ? apiFetch( { path: '/wp/v2/taxonomies?context=view' } )
329
+ : Promise.resolve( {} );
330
+ const [ postTypes, taxonomies ] = await Promise.all( [
331
+ postTypesPromise,
332
+ taxonomiesPromise,
333
+ ] );
334
+
334
335
  return Object.entries( postTypes ?? {} ).map( ( [ name, postType ] ) => {
335
336
  const isTemplate = [ 'wp_template', 'wp_template_part' ].includes(
336
337
  name
337
338
  );
338
339
  const namespace = postType?.rest_namespace ?? 'wp/v2';
339
340
 
341
+ const syncedProperties = new Set( [
342
+ 'author',
343
+ 'blocks',
344
+ 'content',
345
+ 'comment_status',
346
+ 'date',
347
+ 'excerpt',
348
+ 'featured_media',
349
+ 'format',
350
+ 'meta',
351
+ 'ping_status',
352
+ 'slug',
353
+ 'status',
354
+ 'sticky',
355
+ 'template',
356
+ 'title',
357
+ ...( postType.taxonomies
358
+ ?.map( ( taxonomy ) => taxonomies?.[ taxonomy ]?.rest_base )
359
+ ?.filter( Boolean ) ?? [] ),
360
+ ] );
361
+
340
362
  const entity = {
341
363
  kind: 'postType',
342
364
  baseURL: `/${ namespace }/${ postType.rest_base }`,
@@ -384,7 +406,7 @@ async function loadPostTypeEntities() {
384
406
  * @return {void}
385
407
  */
386
408
  applyChangesToCRDTDoc: ( crdtDoc, changes ) =>
387
- applyPostChangesToCRDTDoc( crdtDoc, changes, postType ),
409
+ applyPostChangesToCRDTDoc( crdtDoc, changes, syncedProperties ),
388
410
 
389
411
  /**
390
412
  * Create the awareness instance for the entity's CRDT document.
@@ -408,7 +430,11 @@ async function loadPostTypeEntities() {
408
430
  * @return {Partial< import('@wordpress/sync').ObjectData >} Changes to record
409
431
  */
410
432
  getChangesFromCRDTDoc: ( crdtDoc, editedRecord ) =>
411
- getPostChangesFromCRDTDoc( crdtDoc, editedRecord, postType ),
433
+ getPostChangesFromCRDTDoc(
434
+ crdtDoc,
435
+ editedRecord,
436
+ syncedProperties
437
+ ),
412
438
 
413
439
  /**
414
440
  * Extract changes from a CRDT document that can be used to update the
@@ -419,7 +445,7 @@ async function loadPostTypeEntities() {
419
445
  */
420
446
  getPersistedCRDTDoc: ( record ) => {
421
447
  return (
422
- record?.meta[ POST_META_KEY_FOR_CRDT_DOC_PERSISTENCE ] ||
448
+ record?.meta?.[ POST_META_KEY_FOR_CRDT_DOC_PERSISTENCE ] ||
423
449
  null
424
450
  );
425
451
  },
@@ -12,26 +12,39 @@ import { EntityContext } from './entity-context';
12
12
  * Context provider component for providing
13
13
  * an entity for a specific entity.
14
14
  *
15
- * @param {Object} props The component's props.
16
- * @param {string} props.kind The entity kind.
17
- * @param {string} props.type The entity name.
18
- * @param {number} props.id The entity ID.
19
- * @param {*} props.children The children to wrap.
15
+ * @param {Object} props The component's props.
16
+ * @param {string} props.kind The entity kind.
17
+ * @param {string} props.type The entity name.
18
+ * @param {number} props.id The entity ID.
19
+ * @param {number} [props.revisionId] Optional revision ID. When set,
20
+ * `useEntityProp` reads from the
21
+ * revision record instead of the
22
+ * current entity.
23
+ * @param {*} props.children The children to wrap.
20
24
  *
21
25
  * @return {Object} The provided children, wrapped with
22
26
  * the entity's context provider.
23
27
  */
24
- export default function EntityProvider( { kind, type: name, id, children } ) {
28
+ export default function EntityProvider( {
29
+ kind,
30
+ type: name,
31
+ id,
32
+ revisionId,
33
+ children,
34
+ } ) {
25
35
  const parent = useContext( EntityContext );
26
36
  const childContext = useMemo(
27
37
  () => ( {
28
38
  ...parent,
29
- [ kind ]: {
30
- ...parent?.[ kind ],
31
- [ name ]: id,
32
- },
39
+ ...( kind && {
40
+ [ kind ]: {
41
+ ...parent?.[ kind ],
42
+ [ name ]: id,
43
+ },
44
+ } ),
45
+ ...( revisionId !== undefined && { revisionId } ),
33
46
  } ),
34
- [ parent, kind, name, id ]
47
+ [ parent, kind, name, id, revisionId ]
35
48
  );
36
49
  return (
37
50
  <EntityContext.Provider value={ childContext }>