@wordpress/core-data 7.48.0 → 7.48.2-next.v.202606191442.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 (148) hide show
  1. package/CHANGELOG.md +7 -1
  2. package/build/actions.cjs +1 -7
  3. package/build/actions.cjs.map +3 -3
  4. package/build/awareness/block-lookup.cjs +14 -26
  5. package/build/awareness/block-lookup.cjs.map +2 -2
  6. package/build/awareness/post-editor-awareness.cjs +4 -3
  7. package/build/awareness/post-editor-awareness.cjs.map +2 -2
  8. package/build/entities.cjs +6 -3
  9. package/build/entities.cjs.map +2 -2
  10. package/build/entity-types/helpers.cjs.map +1 -1
  11. package/build/hooks/use-entity-record.cjs +21 -19
  12. package/build/hooks/use-entity-record.cjs.map +3 -3
  13. package/build/hooks/use-entity-records.cjs +22 -20
  14. package/build/hooks/use-entity-records.cjs.map +3 -3
  15. package/build/hooks/use-post-editor-awareness-state.cjs +8 -2
  16. package/build/hooks/use-post-editor-awareness-state.cjs.map +2 -2
  17. package/build/hooks/use-query-select.cjs +2 -20
  18. package/build/hooks/use-query-select.cjs.map +2 -2
  19. package/build/hooks/utils.cjs +53 -0
  20. package/build/hooks/utils.cjs.map +7 -0
  21. package/build/private-actions.cjs +8 -0
  22. package/build/private-actions.cjs.map +2 -2
  23. package/build/private-selectors.cjs.map +2 -2
  24. package/build/reducer.cjs +23 -7
  25. package/build/reducer.cjs.map +2 -2
  26. package/build/resolvers.cjs +13 -8
  27. package/build/resolvers.cjs.map +2 -2
  28. package/build/selectors.cjs +7 -0
  29. package/build/selectors.cjs.map +2 -2
  30. package/build/types.cjs.map +1 -1
  31. package/build/utils/clear-unchanged-edits.cjs +51 -0
  32. package/build/utils/clear-unchanged-edits.cjs.map +7 -0
  33. package/build/utils/crdt-blocks.cjs +12 -2
  34. package/build/utils/crdt-blocks.cjs.map +2 -2
  35. package/build/utils/crdt-user-selections.cjs.map +1 -1
  36. package/build/utils/crdt-utils.cjs.map +1 -1
  37. package/build/utils/crdt.cjs +2 -1
  38. package/build/utils/crdt.cjs.map +2 -2
  39. package/build/utils/index.cjs +6 -0
  40. package/build/utils/index.cjs.map +2 -2
  41. package/build/utils/save-crdt-doc.cjs +75 -0
  42. package/build/utils/save-crdt-doc.cjs.map +7 -0
  43. package/build/utils/set-nested-value.cjs.map +1 -1
  44. package/build-module/actions.mjs +2 -8
  45. package/build-module/actions.mjs.map +2 -2
  46. package/build-module/awareness/block-lookup.mjs +13 -26
  47. package/build-module/awareness/block-lookup.mjs.map +2 -2
  48. package/build-module/awareness/post-editor-awareness.mjs +4 -3
  49. package/build-module/awareness/post-editor-awareness.mjs.map +2 -2
  50. package/build-module/entities.mjs +6 -3
  51. package/build-module/entities.mjs.map +2 -2
  52. package/build-module/hooks/use-entity-record.mjs +21 -19
  53. package/build-module/hooks/use-entity-record.mjs.map +2 -2
  54. package/build-module/hooks/use-entity-records.mjs +20 -18
  55. package/build-module/hooks/use-entity-records.mjs.map +2 -2
  56. package/build-module/hooks/use-post-editor-awareness-state.mjs +9 -3
  57. package/build-module/hooks/use-post-editor-awareness-state.mjs.map +2 -2
  58. package/build-module/hooks/use-query-select.mjs +2 -20
  59. package/build-module/hooks/use-query-select.mjs.map +2 -2
  60. package/build-module/hooks/utils.mjs +28 -0
  61. package/build-module/hooks/utils.mjs.map +7 -0
  62. package/build-module/private-actions.mjs +7 -0
  63. package/build-module/private-actions.mjs.map +2 -2
  64. package/build-module/private-selectors.mjs.map +2 -2
  65. package/build-module/reducer.mjs +23 -8
  66. package/build-module/reducer.mjs.map +2 -2
  67. package/build-module/resolvers.mjs +15 -9
  68. package/build-module/resolvers.mjs.map +2 -2
  69. package/build-module/selectors.mjs +7 -0
  70. package/build-module/selectors.mjs.map +2 -2
  71. package/build-module/utils/clear-unchanged-edits.mjs +20 -0
  72. package/build-module/utils/clear-unchanged-edits.mjs.map +7 -0
  73. package/build-module/utils/crdt-blocks.mjs +12 -2
  74. package/build-module/utils/crdt-blocks.mjs.map +2 -2
  75. package/build-module/utils/crdt-user-selections.mjs.map +1 -1
  76. package/build-module/utils/crdt-utils.mjs.map +1 -1
  77. package/build-module/utils/crdt.mjs +2 -1
  78. package/build-module/utils/crdt.mjs.map +2 -2
  79. package/build-module/utils/index.mjs +24 -20
  80. package/build-module/utils/index.mjs.map +2 -2
  81. package/build-module/utils/save-crdt-doc.mjs +40 -0
  82. package/build-module/utils/save-crdt-doc.mjs.map +7 -0
  83. package/build-module/utils/set-nested-value.mjs.map +1 -1
  84. package/build-types/actions.d.ts.map +1 -1
  85. package/build-types/awareness/block-lookup.d.ts +27 -7
  86. package/build-types/awareness/block-lookup.d.ts.map +1 -1
  87. package/build-types/awareness/post-editor-awareness.d.ts +3 -1
  88. package/build-types/awareness/post-editor-awareness.d.ts.map +1 -1
  89. package/build-types/entities.d.ts.map +1 -1
  90. package/build-types/hooks/use-entity-record.d.ts +4 -0
  91. package/build-types/hooks/use-entity-record.d.ts.map +1 -1
  92. package/build-types/hooks/use-entity-records.d.ts +5 -1
  93. package/build-types/hooks/use-entity-records.d.ts.map +1 -1
  94. package/build-types/hooks/use-post-editor-awareness-state.d.ts.map +1 -1
  95. package/build-types/hooks/utils.d.ts +22 -0
  96. package/build-types/hooks/utils.d.ts.map +1 -0
  97. package/build-types/index.d.ts +8 -8
  98. package/build-types/private-actions.d.ts +15 -0
  99. package/build-types/private-actions.d.ts.map +1 -1
  100. package/build-types/private-selectors.d.ts +0 -12
  101. package/build-types/private-selectors.d.ts.map +1 -1
  102. package/build-types/reducer.d.ts +15 -0
  103. package/build-types/reducer.d.ts.map +1 -1
  104. package/build-types/resolvers.d.ts.map +1 -1
  105. package/build-types/selectors.d.ts +12 -8
  106. package/build-types/selectors.d.ts.map +1 -1
  107. package/build-types/utils/clear-unchanged-edits.d.ts +12 -0
  108. package/build-types/utils/clear-unchanged-edits.d.ts.map +1 -0
  109. package/build-types/utils/crdt-blocks.d.ts +5 -1
  110. package/build-types/utils/crdt-blocks.d.ts.map +1 -1
  111. package/build-types/utils/crdt.d.ts.map +1 -1
  112. package/build-types/utils/index.d.ts +2 -0
  113. package/build-types/utils/index.d.ts.map +1 -1
  114. package/build-types/utils/save-crdt-doc.d.ts +8 -0
  115. package/build-types/utils/save-crdt-doc.d.ts.map +1 -0
  116. package/package.json +27 -20
  117. package/src/actions.js +2 -10
  118. package/src/awareness/block-lookup.ts +21 -62
  119. package/src/awareness/post-editor-awareness.ts +8 -3
  120. package/src/awareness/test/block-lookup.ts +98 -94
  121. package/src/awareness/test/post-editor-awareness.ts +177 -180
  122. package/src/entities.js +14 -3
  123. package/src/hooks/test/use-entity-record.js +5 -1
  124. package/src/hooks/test/use-post-editor-awareness-state.ts +10 -2
  125. package/src/hooks/use-entity-record.ts +26 -19
  126. package/src/hooks/use-entity-records.ts +26 -18
  127. package/src/hooks/use-post-editor-awareness-state.ts +20 -7
  128. package/src/hooks/use-query-select.ts +2 -23
  129. package/src/hooks/utils.ts +40 -0
  130. package/src/private-actions.js +18 -0
  131. package/src/private-selectors.ts +0 -12
  132. package/src/reducer.js +30 -9
  133. package/src/resolvers.js +20 -13
  134. package/src/selectors.ts +11 -0
  135. package/src/test/entities.js +51 -0
  136. package/src/test/private-selectors.js +66 -0
  137. package/src/test/reducer.js +44 -0
  138. package/src/test/resolvers.js +121 -113
  139. package/src/test/selectors.js +48 -0
  140. package/src/utils/clear-unchanged-edits.ts +34 -0
  141. package/src/utils/crdt-blocks.ts +27 -22
  142. package/src/utils/crdt.ts +2 -1
  143. package/src/utils/index.js +2 -0
  144. package/src/utils/save-crdt-doc.js +64 -0
  145. package/src/utils/test/clear-unchanged-edits.js +42 -0
  146. package/src/utils/test/crdt-blocks.ts +57 -2
  147. package/src/utils/test/rtc-rich-text-cursor-scope.test.js +2 -2
  148. package/src/utils/test/save-crdt-doc.js +185 -0
@@ -8,9 +8,9 @@ import { useMemo } from '@wordpress/element';
8
8
  /**
9
9
  * Internal dependencies
10
10
  */
11
- import useQuerySelect from './use-query-select';
12
11
  import { store as coreStore } from '../';
13
12
  import type { Status } from './constants';
13
+ import { getResolutionStatus } from './utils';
14
14
 
15
15
  export interface EntityRecordResolution< RecordType > {
16
16
  /** The requested entity record */
@@ -38,6 +38,11 @@ export interface EntityRecordResolution< RecordType > {
38
38
  */
39
39
  hasEdits: boolean;
40
40
 
41
+ /**
42
+ * Has the resolution started?
43
+ */
44
+ hasStarted: boolean;
45
+
41
46
  /**
42
47
  * Is the record resolved by now?
43
48
  */
@@ -169,57 +174,59 @@ export default function useEntityRecord< RecordType >(
169
174
  [ editEntityRecord, kind, name, recordId, saveEditedEntityRecord ]
170
175
  );
171
176
 
172
- const { editedRecord, hasEdits, edits } = useSelect(
177
+ const { record, editedRecord, hasEdits, edits, ...resolution } = useSelect(
173
178
  ( select ) => {
174
179
  if ( ! options.enabled ) {
175
180
  return {
181
+ record: null,
176
182
  editedRecord: EMPTY_OBJECT,
177
183
  hasEdits: false,
178
184
  edits: EMPTY_OBJECT,
185
+ ...getResolutionStatus(),
179
186
  };
180
187
  }
181
188
 
189
+ const storeSelectors = select( coreStore );
190
+ const resolutionStatus = storeSelectors.getResolutionState(
191
+ 'getEntityRecord',
192
+ [ kind, name, recordId ]
193
+ )?.status;
194
+
182
195
  return {
183
- editedRecord: select( coreStore ).getEditedEntityRecord(
196
+ record: ( storeSelectors.getEntityRecord(
197
+ kind,
198
+ name,
199
+ recordId
200
+ ) ?? null ) as RecordType | null,
201
+ editedRecord: storeSelectors.getEditedEntityRecord(
184
202
  kind,
185
203
  name,
186
204
  recordId
187
205
  ),
188
- hasEdits: select( coreStore ).hasEditsForEntityRecord(
206
+ hasEdits: storeSelectors.hasEditsForEntityRecord(
189
207
  kind,
190
208
  name,
191
209
  recordId
192
210
  ),
193
- edits: select( coreStore ).getEntityRecordNonTransientEdits(
211
+ edits: storeSelectors.getEntityRecordNonTransientEdits(
194
212
  kind,
195
213
  name,
196
214
  recordId
197
215
  ),
216
+ ...getResolutionStatus( resolutionStatus ),
198
217
  };
199
218
  },
200
219
  [ kind, name, recordId, options.enabled ]
201
220
  );
202
221
 
203
- const { data: record, ...querySelectRest } = useQuerySelect(
204
- ( query ) => {
205
- if ( ! options.enabled ) {
206
- return {
207
- data: null,
208
- };
209
- }
210
- return query( coreStore ).getEntityRecord( kind, name, recordId );
211
- },
212
- [ kind, name, recordId, options.enabled ]
213
- );
214
-
215
222
  return {
216
223
  record,
217
224
  editedRecord,
218
225
  hasEdits,
219
226
  edits,
220
- ...querySelectRest,
227
+ ...resolution,
221
228
  ...mutations,
222
- };
229
+ } as EntityRecordResolution< RecordType >;
223
230
  }
224
231
 
225
232
  export function useDeprecatedEntityRecord(
@@ -9,14 +9,14 @@ import { useMemo } from '@wordpress/element';
9
9
  /**
10
10
  * Internal dependencies
11
11
  */
12
- import useQuerySelect from './use-query-select';
13
12
  import { store as coreStore } from '../';
14
13
  import type { Options } from './use-entity-record';
15
14
  import type { Status } from './constants';
15
+ import { getResolutionStatus } from './utils';
16
16
  import { unlock } from '../lock-unlock';
17
17
  import { getNormalizedCommaSeparable } from '../utils';
18
18
 
19
- interface EntityRecordsResolution< RecordType > {
19
+ export interface EntityRecordsResolution< RecordType > {
20
20
  /** The requested entity records */
21
21
  records: RecordType[] | null;
22
22
 
@@ -25,6 +25,11 @@ interface EntityRecordsResolution< RecordType > {
25
25
  */
26
26
  isResolving: boolean;
27
27
 
28
+ /**
29
+ * Has the resolution started?
30
+ */
31
+ hasStarted: boolean;
32
+
28
33
  /**
29
34
  * Is the record resolved by now?
30
35
  */
@@ -108,38 +113,41 @@ export default function useEntityRecords< RecordType >(
108
113
  // if the values remain the same.
109
114
  const queryAsString = addQueryArgs( '', queryArgs );
110
115
 
111
- const { data: records, ...rest } = useQuerySelect(
112
- ( query ) => {
113
- if ( ! options.enabled ) {
114
- return {
115
- // Avoiding returning a new reference on every execution.
116
- data: EMPTY_ARRAY,
117
- };
118
- }
119
- return query( coreStore ).getEntityRecords( kind, name, queryArgs );
120
- },
121
- [ kind, name, queryAsString, options.enabled ]
122
- );
123
-
124
- const { totalItems, totalPages } = useSelect(
116
+ const { records, totalItems, totalPages, ...rest } = useSelect(
125
117
  ( select ) => {
126
118
  if ( ! options.enabled ) {
127
119
  return {
120
+ // Avoiding returning a new reference on every execution.
121
+ records: EMPTY_ARRAY,
128
122
  totalItems: null,
129
123
  totalPages: null,
124
+ ...getResolutionStatus(),
130
125
  };
131
126
  }
127
+
128
+ const storeSelectors = select( coreStore );
129
+ const resolutionStatus = storeSelectors.getResolutionState(
130
+ 'getEntityRecords',
131
+ [ kind, name, queryArgs ]
132
+ )?.status;
133
+
132
134
  return {
133
- totalItems: select( coreStore ).getEntityRecordsTotalItems(
135
+ records: storeSelectors.getEntityRecords(
136
+ kind,
137
+ name,
138
+ queryArgs
139
+ ) as RecordType[] | null,
140
+ totalItems: storeSelectors.getEntityRecordsTotalItems(
134
141
  kind,
135
142
  name,
136
143
  queryArgs
137
144
  ),
138
- totalPages: select( coreStore ).getEntityRecordsTotalPages(
145
+ totalPages: storeSelectors.getEntityRecordsTotalPages(
139
146
  kind,
140
147
  name,
141
148
  queryArgs
142
149
  ),
150
+ ...getResolutionStatus( resolutionStatus ),
143
151
  };
144
152
  },
145
153
  [ kind, name, queryAsString, options.enabled ]
@@ -2,13 +2,15 @@
2
2
  * External dependencies
3
3
  */
4
4
  import { usePrevious } from '@wordpress/compose';
5
- import { useEffect, useState } from '@wordpress/element';
5
+ import { useEffect, useState, useCallback } from '@wordpress/element';
6
6
  import type { Y } from '@wordpress/sync';
7
7
 
8
8
  /**
9
9
  * Internal dependencies
10
10
  */
11
11
  import { getSyncManager } from '../sync';
12
+ import { usePostContentBlocks } from '../awareness/block-lookup';
13
+ import type { EditorStoreBlock } from '../awareness/block-lookup';
12
14
  import type {
13
15
  PostEditorAwarenessState as ActiveCollaborator,
14
16
  PostSaveEvent,
@@ -19,7 +21,10 @@ import type { PostEditorAwareness } from '../awareness/post-editor-awareness';
19
21
 
20
22
  interface AwarenessState {
21
23
  activeCollaborators: ActiveCollaborator[];
22
- resolveSelection: ( selection: SelectionState ) => ResolvedSelection;
24
+ resolveSelection: (
25
+ selection: SelectionState,
26
+ blocks: EditorStoreBlock[]
27
+ ) => ResolvedSelection;
23
28
  getDebugData: () => YDocDebugData;
24
29
  isCurrentCollaboratorDisconnected: boolean;
25
30
  }
@@ -49,8 +54,10 @@ function getAwarenessState(
49
54
 
50
55
  return {
51
56
  activeCollaborators,
52
- resolveSelection: ( selection: SelectionState ) =>
53
- awareness.convertSelectionStateToAbsolute( selection ),
57
+ resolveSelection: (
58
+ selection: SelectionState,
59
+ blocks: EditorStoreBlock[]
60
+ ) => awareness.convertSelectionStateToAbsolute( selection, blocks ),
54
61
  getDebugData: () => awareness.getDebugData(),
55
62
  isCurrentCollaboratorDisconnected:
56
63
  activeCollaborators.find( ( collaborator ) => collaborator.isMe )
@@ -124,7 +131,13 @@ export function useResolvedSelection(
124
131
  postId: number | null,
125
132
  postType: string | null
126
133
  ): ( selection: SelectionState ) => ResolvedSelection {
127
- return usePostEditorAwarenessState( postId, postType ).resolveSelection;
134
+ const blocks = usePostContentBlocks();
135
+ const awarenessState = usePostEditorAwarenessState( postId, postType );
136
+ return useCallback(
137
+ ( selection: SelectionState ) =>
138
+ awarenessState.resolveSelection( selection, blocks ),
139
+ [ blocks, awarenessState ]
140
+ );
128
141
  }
129
142
 
130
143
  /**
@@ -158,8 +171,8 @@ export function useIsDisconnected(
158
171
 
159
172
  /**
160
173
  * Hook that subscribes to the CRDT state map and returns the most recent
161
- * save event (timestamp + client ID). The state map is updated by
162
- * `markEntityAsSaved` in `@wordpress/sync`
174
+ * user-facing post save event (timestamp + client ID). The state map is
175
+ * updated by `markEntityAsSaved` in `@wordpress/sync`.
163
176
  *
164
177
  * @param postId The ID of the post.
165
178
  * @param postType The type of the post.
@@ -7,7 +7,7 @@ import { useSelect } from '@wordpress/data';
7
7
  * Internal dependencies
8
8
  */
9
9
  import memoize from 'memize';
10
- import { Status } from './constants';
10
+ import { getResolutionStatus } from './utils';
11
11
 
12
12
  export const META_SELECTORS = [
13
13
  'getIsResolving',
@@ -114,30 +114,9 @@ const enrichSelectors = memoize( ( ( selectors ) => {
114
114
  args
115
115
  )?.status;
116
116
 
117
- let status;
118
- switch ( resolutionStatus ) {
119
- case 'resolving':
120
- status = Status.Resolving;
121
- break;
122
- case 'finished':
123
- status = Status.Success;
124
- break;
125
- case 'error':
126
- status = Status.Error;
127
- break;
128
- case undefined:
129
- status = Status.Idle;
130
- break;
131
- }
132
-
133
117
  return {
134
118
  data,
135
- status,
136
- isResolving: status === Status.Resolving,
137
- hasStarted: status !== Status.Idle,
138
- hasResolved:
139
- status === Status.Success ||
140
- status === Status.Error,
119
+ ...getResolutionStatus( resolutionStatus ),
141
120
  };
142
121
  },
143
122
  } );
@@ -0,0 +1,40 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import type { ResolutionStatus } from '@wordpress/data';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import { Status } from './constants';
10
+
11
+ /**
12
+ * Normalizes a resolution status from the store into the resolution info
13
+ * shared by the entity record hooks and `useQuerySelect`.
14
+ *
15
+ * @param resolutionStatus Status returned by the `getResolutionState` selector.
16
+ * @return Resolution info object.
17
+ */
18
+ export function getResolutionStatus( resolutionStatus?: ResolutionStatus ) {
19
+ let status: Status;
20
+ switch ( resolutionStatus ) {
21
+ case 'resolving':
22
+ status = Status.Resolving;
23
+ break;
24
+ case 'finished':
25
+ status = Status.Success;
26
+ break;
27
+ case 'error':
28
+ status = Status.Error;
29
+ break;
30
+ default:
31
+ status = Status.Idle;
32
+ }
33
+
34
+ return {
35
+ status,
36
+ isResolving: status === Status.Resolving,
37
+ hasStarted: status !== Status.Idle,
38
+ hasResolved: status === Status.Success || status === Status.Error,
39
+ };
40
+ }
@@ -197,6 +197,24 @@ export function receiveViewConfig( kind, name, config ) {
197
197
  };
198
198
  }
199
199
 
200
+ /**
201
+ * Returns an action object used to notify core-data that the sync undo manager
202
+ * state changed outside of the core-data reducer, e.g. The Yjs UndoManager
203
+ * captured an undo level.
204
+ *
205
+ * @param {Object} state The sync undo stack state.
206
+ * @param {boolean} state.hasRedo Whether there are changes to redo.
207
+ * @param {boolean} state.hasUndo Whether there are changes to undo.
208
+ *
209
+ * @return {Object} Action object.
210
+ */
211
+ export function __unstableNotifySyncUndoManagerChange( state ) {
212
+ return {
213
+ type: 'SYNC_UNDO_MANAGER_CHANGE',
214
+ ...state,
215
+ };
216
+ }
217
+
200
218
  /**
201
219
  * Returns an action object used to set the sync connection status for an entity or collection.
202
220
  *
@@ -21,18 +21,6 @@ const EMPTY_OBJECT = {};
21
21
  * Returns the previous edit from the current undo offset
22
22
  * for the entity records edits history, if any.
23
23
  *
24
- * Known Issue: Every-time state.undoManager changes, the getUndoManager
25
- * private selector is called (if used within useSelect and things like that)
26
- * which ensures the UI is always properly reactive. But, it's not the case with
27
- * the custom "sync" undo manager.
28
- *
29
- * Assumption: When an undo/redo is created, other parts of the core-data state
30
- * are likely changing simultaneously, which will trigger the selectors again.
31
- *
32
- * This issue is acceptable based on the assumption above.
33
- *
34
- * @see https://github.com/WordPress/gutenberg/pull/72407/files#r2580214235 for more details.
35
- *
36
24
  * @param state State tree.
37
25
  *
38
26
  * @return The undo manager.
package/src/reducer.js CHANGED
@@ -13,7 +13,7 @@ import { createUndoManager } from '@wordpress/undo-manager';
13
13
  /**
14
14
  * Internal dependencies
15
15
  */
16
- import { ifMatchingAction, replaceAction } from './utils';
16
+ import { clearUnchangedEdits, ifMatchingAction, replaceAction } from './utils';
17
17
  import { reducer as queriedDataReducer } from './queried-data';
18
18
  import { rootEntitiesConfig, DEFAULT_ENTITY_KEY } from './entities';
19
19
  import { ConnectionErrorCode } from './sync';
@@ -150,19 +150,23 @@ const withMultiEntityRecordEdits = ( reducer ) => ( state, action ) => {
150
150
 
151
151
  let newState = state;
152
152
  record.forEach( ( { id: { kind, name, recordId }, changes } ) => {
153
+ const persistedRecord =
154
+ state?.queriedData?.items?.default?.[ recordId ];
155
+ const edits = Object.fromEntries(
156
+ Object.entries( changes ).map( ( [ key, value ] ) => [
157
+ key,
158
+ action.type === 'UNDO' ? value.from : value.to,
159
+ ] )
160
+ );
161
+
153
162
  newState = reducer( newState, {
154
163
  type: 'EDIT_ENTITY_RECORD',
155
164
  kind,
156
165
  name,
157
166
  recordId,
158
- edits: Object.entries( changes ).reduce(
159
- ( acc, [ key, value ] ) => {
160
- acc[ key ] =
161
- action.type === 'UNDO' ? value.from : value.to;
162
- return acc;
163
- },
164
- {}
165
- ),
167
+ // Clear edits matching the persisted record so the entity is
168
+ // no longer dirty after undoing back to its saved state.
169
+ edits: clearUnchangedEdits( edits, persistedRecord ),
166
170
  } );
167
171
  } );
168
172
  return newState;
@@ -460,6 +464,22 @@ export function undoManager( state = createUndoManager() ) {
460
464
  return state;
461
465
  }
462
466
 
467
+ // Stores a snapshot of the sync undo manager's undo/redo availability so
468
+ // core-data selectors can react to undo stack changes.
469
+ export function syncUndoManagerState(
470
+ state = { hasRedo: false, hasUndo: false },
471
+ action
472
+ ) {
473
+ switch ( action.type ) {
474
+ case 'SYNC_UNDO_MANAGER_CHANGE':
475
+ return {
476
+ hasRedo: action.hasRedo,
477
+ hasUndo: action.hasUndo,
478
+ };
479
+ }
480
+ return state;
481
+ }
482
+
463
483
  export function editsReference( state = {}, action ) {
464
484
  switch ( action.type ) {
465
485
  case 'EDIT_ENTITY_RECORD':
@@ -750,6 +770,7 @@ export default combineReducers( {
750
770
  themeGlobalStyleRevisions,
751
771
  entities,
752
772
  editsReference,
773
+ syncUndoManagerState,
753
774
  undoManager,
754
775
  embedPreviews,
755
776
  userPermissions,
package/src/resolvers.js CHANGED
@@ -25,6 +25,7 @@ import {
25
25
  RECEIVE_INTERMEDIATE_RESULTS,
26
26
  isNumericID,
27
27
  normalizeQueryForResolution,
28
+ saveCRDTDoc,
28
29
  } from './utils';
29
30
  import { fetchBlockPatterns } from './fetch';
30
31
  import { restoreSelection, getSelectionHistory } from './utils/crdt-selection';
@@ -250,9 +251,15 @@ export const getEntityRecord =
250
251
  // persistence. As we add support for syncing additional entity,
251
252
  // we'll need to revisit where persisted CRDT documents are stored.
252
253
  persistCRDTDoc: () => {
253
- resolveSelect
254
+ if (
255
+ ! entityConfig.syncConfig?.supportsPersistence
256
+ ) {
257
+ return;
258
+ }
259
+
260
+ return resolveSelect
254
261
  .getEditedEntityRecord( kind, name, key )
255
- .then( ( editedRecord ) => {
262
+ .then( async ( editedRecord ) => {
256
263
  // Don't persist the CRDT document if the record is still an
257
264
  // auto-draft or if the entity does not support meta.
258
265
  const { meta, status } = editedRecord;
@@ -260,19 +267,14 @@ export const getEntityRecord =
260
267
  return;
261
268
  }
262
269
 
263
- // Trigger a minimal save to persist the CRDT document. The
264
- // entity's pre-persist hooks will create the persisted CRDT
265
- // document and apply it to the record's meta.
266
270
  const entityIdKey =
267
271
  entityConfig.key || DEFAULT_ENTITY_KEY;
268
- dispatch.saveEntityRecord(
269
- kind,
270
- name,
271
- {
272
- [ entityIdKey ]:
273
- editedRecord[ entityIdKey ],
274
- },
275
- { __unstableSkipSyncUpdate: true }
272
+ const entityId =
273
+ editedRecord[ entityIdKey ];
274
+
275
+ await saveCRDTDoc(
276
+ `${ kind }/${ name }`,
277
+ entityId
276
278
  );
277
279
  } );
278
280
  },
@@ -287,6 +289,11 @@ export const getEntityRecord =
287
289
  );
288
290
  }
289
291
  },
292
+ onUndoStackChange: ( undoState ) => {
293
+ dispatch.__unstableNotifySyncUndoManagerChange(
294
+ undoState
295
+ );
296
+ },
290
297
  restoreUndoMeta: ( ydoc, meta ) => {
291
298
  const selectionHistory =
292
299
  meta.get( 'selectionHistory' );
package/src/selectors.ts CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  isNumericID,
25
25
  getUserPermissionCacheKey,
26
26
  } from './utils';
27
+ import { getSyncManager } from './sync';
27
28
  import type * as ET from './entity-types';
28
29
  import logEntityDeprecation from './utils/log-entity-deprecation';
29
30
 
@@ -44,6 +45,10 @@ export interface State {
44
45
  themeGlobalStyleVariations: Record< string, string >;
45
46
  themeGlobalStyleRevisions: Record< number, Object >;
46
47
  undoManager: UndoManager;
48
+ syncUndoManagerState: {
49
+ hasRedo: boolean;
50
+ hasUndo: boolean;
51
+ };
47
52
  userPermissions: Record< string, boolean >;
48
53
  users: UserState;
49
54
  navigationFallbackId: EntityRecordKey;
@@ -1148,6 +1153,9 @@ export function getRedoEdit( state: State ): Optional< any > {
1148
1153
  * @return Whether there is a previous edit or not.
1149
1154
  */
1150
1155
  export function hasUndo( state: State ): boolean {
1156
+ if ( getSyncManager()?.undoManager ) {
1157
+ return state.syncUndoManagerState.hasUndo;
1158
+ }
1151
1159
  return getUndoManager( state ).hasUndo();
1152
1160
  }
1153
1161
 
@@ -1160,6 +1168,9 @@ export function hasUndo( state: State ): boolean {
1160
1168
  * @return Whether there is a next edit or not.
1161
1169
  */
1162
1170
  export function hasRedo( state: State ): boolean {
1171
+ if ( getSyncManager()?.undoManager ) {
1172
+ return state.syncUndoManagerState.hasRedo;
1173
+ }
1163
1174
  return getUndoManager( state ).hasRedo();
1164
1175
  }
1165
1176
 
@@ -136,15 +136,20 @@ describe( 'prePersistPostType', () => {
136
136
 
137
137
  describe( 'loadPostTypeEntities', () => {
138
138
  let originalCollaborationEnabled;
139
+ let originalCollaborationDisabledPostTypes;
139
140
 
140
141
  beforeEach( () => {
141
142
  apiFetch.mockReset();
142
143
  applyPostChangesToCRDTDoc.mockReset();
143
144
  originalCollaborationEnabled = window._wpCollaborationEnabled;
145
+ originalCollaborationDisabledPostTypes =
146
+ window._wpCollaborationDisabledPostTypes;
144
147
  } );
145
148
 
146
149
  afterEach( () => {
147
150
  window._wpCollaborationEnabled = originalCollaborationEnabled;
151
+ window._wpCollaborationDisabledPostTypes =
152
+ originalCollaborationDisabledPostTypes;
148
153
  } );
149
154
 
150
155
  it( 'should include custom taxonomy rest_bases in synced properties when collaboration is enabled', async () => {
@@ -224,6 +229,52 @@ describe( 'loadPostTypeEntities', () => {
224
229
  expect( syncedProperties ).not.toContain( 'tags' );
225
230
  } );
226
231
 
232
+ it( 'should sync post type entities by default', async () => {
233
+ window._wpCollaborationEnabled = false;
234
+ window._wpCollaborationDisabledPostTypes = undefined;
235
+
236
+ const mockPostTypes = {
237
+ post: {
238
+ name: 'Posts',
239
+ rest_base: 'posts',
240
+ rest_namespace: 'wp/v2',
241
+ },
242
+ };
243
+
244
+ apiFetch.mockResolvedValueOnce( mockPostTypes );
245
+
246
+ const postTypeLoader = additionalEntityConfigLoaders.find(
247
+ ( loader ) => loader.kind === 'postType'
248
+ );
249
+ const entities = await postTypeLoader.loadEntities();
250
+ const postEntity = entities.find( ( e ) => e.name === 'post' );
251
+
252
+ expect( postEntity.syncConfig.shouldSync() ).toBe( true );
253
+ } );
254
+
255
+ it( 'should not sync post type entities disabled for collaboration', async () => {
256
+ window._wpCollaborationEnabled = false;
257
+ window._wpCollaborationDisabledPostTypes = [ 'book' ];
258
+
259
+ const mockPostTypes = {
260
+ book: {
261
+ name: 'Books',
262
+ rest_base: 'books',
263
+ rest_namespace: 'wp/v2',
264
+ },
265
+ };
266
+
267
+ apiFetch.mockResolvedValueOnce( mockPostTypes );
268
+
269
+ const postTypeLoader = additionalEntityConfigLoaders.find(
270
+ ( loader ) => loader.kind === 'postType'
271
+ );
272
+ const entities = await postTypeLoader.loadEntities();
273
+ const bookEntity = entities.find( ( e ) => e.name === 'book' );
274
+
275
+ expect( bookEntity.syncConfig.shouldSync() ).toBe( false );
276
+ } );
277
+
227
278
  it( 'should skip taxonomy rest_base when taxonomy is not found in fetched taxonomies', async () => {
228
279
  window._wpCollaborationEnabled = true;
229
280
 
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import { getUndoManager } from '../private-selectors';
5
+ import { getSyncManager } from '../sync';
6
+
7
+ jest.mock( '../sync', () => ( {
8
+ getSyncManager: jest.fn(),
9
+ } ) );
10
+
11
+ describe( 'getUndoManager', () => {
12
+ afterEach( () => {
13
+ getSyncManager.mockReset();
14
+ } );
15
+
16
+ it( 'returns the sync undo manager when one is available', () => {
17
+ const syncUndoManager = {
18
+ addRecord: jest.fn(),
19
+ hasRedo: jest.fn(),
20
+ hasUndo: jest.fn(),
21
+ redo: jest.fn(),
22
+ undo: jest.fn(),
23
+ };
24
+ const fallbackUndoManager = {
25
+ addRecord: jest.fn(),
26
+ hasRedo: jest.fn(),
27
+ hasUndo: jest.fn(),
28
+ redo: jest.fn(),
29
+ undo: jest.fn(),
30
+ };
31
+ getSyncManager.mockReturnValue( {
32
+ undoManager: syncUndoManager,
33
+ } );
34
+
35
+ const state = {
36
+ undoManager: fallbackUndoManager,
37
+ syncUndoManagerState: {
38
+ hasRedo: false,
39
+ hasUndo: false,
40
+ },
41
+ };
42
+
43
+ expect( getUndoManager( state ) ).toBe( syncUndoManager );
44
+ } );
45
+
46
+ it( 'returns the default undo manager when there is no sync undo manager', () => {
47
+ const fallbackUndoManager = {
48
+ addRecord: jest.fn(),
49
+ hasRedo: jest.fn(),
50
+ hasUndo: jest.fn(),
51
+ redo: jest.fn(),
52
+ undo: jest.fn(),
53
+ };
54
+ getSyncManager.mockReturnValue( undefined );
55
+
56
+ expect(
57
+ getUndoManager( {
58
+ undoManager: fallbackUndoManager,
59
+ syncUndoManagerState: {
60
+ hasRedo: false,
61
+ hasUndo: false,
62
+ },
63
+ } )
64
+ ).toBe( fallbackUndoManager );
65
+ } );
66
+ } );