@wordpress/core-data 7.49.1 → 7.50.1-next.v.202607070741.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 (36) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/build/entities.cjs +10 -0
  3. package/build/entities.cjs.map +2 -2
  4. package/build/private-actions.cjs +4 -0
  5. package/build/private-actions.cjs.map +2 -2
  6. package/build/resolvers.cjs +2 -1
  7. package/build/resolvers.cjs.map +2 -2
  8. package/build/utils/crdt-blocks.cjs.map +2 -2
  9. package/build/utils/crdt.cjs +7 -2
  10. package/build/utils/crdt.cjs.map +2 -2
  11. package/build-module/entities.mjs +10 -0
  12. package/build-module/entities.mjs.map +2 -2
  13. package/build-module/private-actions.mjs +4 -0
  14. package/build-module/private-actions.mjs.map +2 -2
  15. package/build-module/resolvers.mjs +2 -1
  16. package/build-module/resolvers.mjs.map +2 -2
  17. package/build-module/utils/crdt-blocks.mjs.map +2 -2
  18. package/build-module/utils/crdt.mjs +7 -2
  19. package/build-module/utils/crdt.mjs.map +2 -2
  20. package/build-types/entities.d.ts +17 -0
  21. package/build-types/entities.d.ts.map +1 -1
  22. package/build-types/private-actions.d.ts.map +1 -1
  23. package/build-types/resolvers.d.ts.map +1 -1
  24. package/build-types/utils/crdt-blocks.d.ts +1 -0
  25. package/build-types/utils/crdt-blocks.d.ts.map +1 -1
  26. package/build-types/utils/crdt.d.ts.map +1 -1
  27. package/package.json +19 -18
  28. package/src/entities.js +10 -0
  29. package/src/private-actions.js +4 -0
  30. package/src/resolvers.js +6 -1
  31. package/src/test/private-actions.js +38 -1
  32. package/src/test/resolvers.js +45 -0
  33. package/src/utils/crdt-blocks.ts +1 -0
  34. package/src/utils/crdt.ts +18 -2
  35. package/src/utils/test/crdt-blocks.ts +58 -0
  36. package/src/utils/test/crdt.ts +47 -0
@@ -6,9 +6,14 @@ import apiFetch from '@wordpress/api-fetch';
6
6
  /**
7
7
  * Internal dependencies
8
8
  */
9
- import { editMediaEntity } from '../private-actions';
9
+ import { editMediaEntity, setCollaborationSupported } from '../private-actions';
10
+ import { getSyncManager, hasSyncManager } from '../sync';
10
11
 
11
12
  jest.mock( '@wordpress/api-fetch' );
13
+ jest.mock( '../sync', () => ( {
14
+ getSyncManager: jest.fn(),
15
+ hasSyncManager: jest.fn(),
16
+ } ) );
12
17
 
13
18
  describe( 'editMediaEntity', () => {
14
19
  let dispatch;
@@ -211,3 +216,35 @@ describe( 'editMediaEntity', () => {
211
216
  expect( result ).toBeUndefined();
212
217
  } );
213
218
  } );
219
+
220
+ describe( 'setCollaborationSupported', () => {
221
+ afterEach( () => {
222
+ getSyncManager.mockReset();
223
+ hasSyncManager.mockReset();
224
+ } );
225
+
226
+ it( 'unloads sync and resets sync undo state when disabling collaboration', () => {
227
+ const syncManager = {
228
+ unloadAll: jest.fn(),
229
+ };
230
+ const dispatch = Object.assign( jest.fn(), {
231
+ __unstableNotifySyncUndoManagerChange: jest.fn(),
232
+ } );
233
+ hasSyncManager.mockReturnValue( true );
234
+ getSyncManager.mockReturnValue( syncManager );
235
+
236
+ setCollaborationSupported( false )( { dispatch } );
237
+
238
+ expect( dispatch ).toHaveBeenCalledWith( {
239
+ type: 'SET_COLLABORATION_SUPPORTED',
240
+ supported: false,
241
+ } );
242
+ expect( syncManager.unloadAll ).toHaveBeenCalledTimes( 1 );
243
+ expect(
244
+ dispatch.__unstableNotifySyncUndoManagerChange
245
+ ).toHaveBeenCalledWith( {
246
+ hasUndo: false,
247
+ hasRedo: false,
248
+ } );
249
+ } );
250
+ } );
@@ -183,6 +183,51 @@ describe( 'getEntityRecord', () => {
183
183
  );
184
184
  } );
185
185
 
186
+ it( 'does not load entity with sync manager when collaboration is unsupported', async () => {
187
+ const POST_RECORD = { id: 1, title: 'Test Post' };
188
+ const POST_RESPONSE = {
189
+ json: () => Promise.resolve( POST_RECORD ),
190
+ };
191
+ const ENTITIES_WITH_SYNC = [
192
+ {
193
+ name: 'post',
194
+ kind: 'postType',
195
+ baseURL: '/wp/v2/posts',
196
+ baseURLParams: { context: 'edit' },
197
+ syncConfig: {},
198
+ },
199
+ ];
200
+
201
+ const resolveSelectWithSync = {
202
+ getEntitiesConfig: jest.fn( () => ENTITIES_WITH_SYNC ),
203
+ };
204
+ const select = {
205
+ isCollaborationSupported: jest.fn( () => false ),
206
+ };
207
+
208
+ triggerFetch.mockImplementation( () => POST_RESPONSE );
209
+
210
+ await getEntityRecord(
211
+ 'postType',
212
+ 'post',
213
+ 1
214
+ )( {
215
+ select,
216
+ dispatch,
217
+ registry,
218
+ resolveSelect: resolveSelectWithSync,
219
+ } );
220
+
221
+ expect( syncManager.load ).not.toHaveBeenCalled();
222
+ expect( select.isCollaborationSupported ).toHaveBeenCalledTimes( 1 );
223
+ expect( dispatch.receiveEntityRecords ).toHaveBeenCalledWith(
224
+ 'postType',
225
+ 'post',
226
+ POST_RECORD,
227
+ undefined
228
+ );
229
+ } );
230
+
186
231
  it( 'notifies core-data when the sync undo manager stack changes', async () => {
187
232
  const POST_RECORD = { id: 1, title: 'Test Post' };
188
233
  const POST_RESPONSE = {
@@ -46,6 +46,7 @@ export interface Block {
46
46
  attributes: BlockAttributes;
47
47
  clientId?: string;
48
48
  innerBlocks: Block[];
49
+ innerContent?: Array< string | null >;
49
50
  isValid?: boolean;
50
51
  name: string;
51
52
  originalContent?: string;
package/src/utils/crdt.ts CHANGED
@@ -345,8 +345,24 @@ function parseCursorSelection( selection?: WPSelection ): MergeCursorPosition {
345
345
  : null;
346
346
  }
347
347
 
348
- function defaultGetChangesFromCRDTDoc( crdtDoc: CRDTDoc ): ObjectData {
349
- return getRootMap( crdtDoc, CRDT_RECORD_MAP_KEY ).toJSON();
348
+ function defaultGetChangesFromCRDTDoc(
349
+ crdtDoc: CRDTDoc,
350
+ editedRecord: ObjectData
351
+ ): ObjectData {
352
+ const docRecord = getRootMap( crdtDoc, CRDT_RECORD_MAP_KEY ).toJSON();
353
+
354
+ /*
355
+ * Only report properties that differ from the edited record. Reporting
356
+ * unchanged properties as edits marks the record dirty: `Y.Map.toJSON()`
357
+ * returns fresh object instances, so without this comparison every synced
358
+ * update (e.g. from another tab) re-dispatches the entire record as edits.
359
+ * See https://github.com/WordPress/gutenberg/issues/79907.
360
+ */
361
+ return Object.fromEntries(
362
+ Object.entries( docRecord ).filter( ( [ key, newValue ] ) =>
363
+ haveValuesChanged( editedRecord?.[ key ], newValue )
364
+ )
365
+ );
350
366
  }
351
367
 
352
368
  /**
@@ -165,6 +165,64 @@ describe( 'crdt-blocks', () => {
165
165
  expect( content.toString() ).toBe( 'Hello World' );
166
166
  } );
167
167
 
168
+ it( 'syncs innerContent of static inner-content blocks into the Y.Doc', () => {
169
+ const incomingBlocks: Block[] = [
170
+ {
171
+ name: 'core/html',
172
+ attributes: {},
173
+ innerBlocks: [
174
+ {
175
+ name: 'core/paragraph',
176
+ attributes: { content: 'Editable' },
177
+ innerBlocks: [],
178
+ clientId: 'inner-1',
179
+ },
180
+ ],
181
+ innerContent: [ '<div class="banner">', null, '</div>' ],
182
+ clientId: 'html-1',
183
+ },
184
+ ];
185
+
186
+ mergeCrdtBlocks( yblocks, incomingBlocks, null );
187
+
188
+ const block = yblocks.get( 0 );
189
+ expect( block.get( 'innerContent' ) ).toEqual( [
190
+ '<div class="banner">',
191
+ null,
192
+ '</div>',
193
+ ] );
194
+ } );
195
+
196
+ it( 'updates innerContent when the static markup changes', () => {
197
+ const initialBlocks: Block[] = [
198
+ {
199
+ name: 'core/html',
200
+ attributes: {},
201
+ innerBlocks: [],
202
+ innerContent: [ '<p>one</p>' ],
203
+ clientId: 'html-1',
204
+ },
205
+ ];
206
+
207
+ mergeCrdtBlocks( yblocks, initialBlocks, null );
208
+
209
+ const updatedBlocks: Block[] = [
210
+ {
211
+ name: 'core/html',
212
+ attributes: {},
213
+ innerBlocks: [],
214
+ innerContent: [ '<p>two</p>' ],
215
+ clientId: 'html-1',
216
+ },
217
+ ];
218
+
219
+ mergeCrdtBlocks( yblocks, updatedBlocks, null );
220
+
221
+ expect( yblocks.get( 0 ).get( 'innerContent' ) ).toEqual( [
222
+ '<p>two</p>',
223
+ ] );
224
+ } );
225
+
168
226
  it( 'updates existing blocks when content changes', () => {
169
227
  const initialBlocks: Block[] = [
170
228
  {
@@ -68,6 +68,7 @@ import { CRDT_RECORD_MAP_KEY } from '../../sync';
68
68
  import {
69
69
  applyPostChangesToCRDTDoc,
70
70
  defaultCollectionSyncConfig,
71
+ defaultSyncConfig,
71
72
  getPostChangesFromCRDTDoc,
72
73
  POST_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
73
74
  type PostChanges,
@@ -93,6 +94,52 @@ const defaultSyncedProperties = new Set< string >( [
93
94
  'title',
94
95
  ] );
95
96
 
97
+ describe( 'defaultSyncConfig', () => {
98
+ // Regression test for https://github.com/WordPress/gutenberg/issues/79907:
99
+ // reporting unchanged properties as changes marks resolved records
100
+ // (e.g. taxonomy terms) as dirty whenever a synced update arrives.
101
+ it( 'getChangesFromCRDTDoc returns no changes when the document matches the edited record', () => {
102
+ const doc = new Y.Doc();
103
+ const record = {
104
+ id: 1,
105
+ name: 'Uncategorized',
106
+ slug: 'uncategorized',
107
+ meta: [],
108
+ _links: { self: [ { href: 'https://example.com' } ] },
109
+ };
110
+ defaultSyncConfig.applyChangesToCRDTDoc( doc, record );
111
+
112
+ // The edited record deep-equals the document contents, but
113
+ // `Y.Map.toJSON()` returns fresh object instances.
114
+ const changes = defaultSyncConfig.getChangesFromCRDTDoc( doc, {
115
+ ...record,
116
+ meta: [],
117
+ _links: { self: [ { href: 'https://example.com' } ] },
118
+ } );
119
+
120
+ expect( changes ).toEqual( {} );
121
+ doc.destroy();
122
+ } );
123
+
124
+ it( 'getChangesFromCRDTDoc returns only the properties that differ from the edited record', () => {
125
+ const doc = new Y.Doc();
126
+ defaultSyncConfig.applyChangesToCRDTDoc( doc, {
127
+ id: 1,
128
+ name: 'Renamed category',
129
+ slug: 'uncategorized',
130
+ } );
131
+
132
+ const changes = defaultSyncConfig.getChangesFromCRDTDoc( doc, {
133
+ id: 1,
134
+ name: 'Uncategorized',
135
+ slug: 'uncategorized',
136
+ } );
137
+
138
+ expect( changes ).toEqual( { name: 'Renamed category' } );
139
+ doc.destroy();
140
+ } );
141
+ } );
142
+
96
143
  describe( 'defaultCollectionSyncConfig', () => {
97
144
  it( 'has no-op applyChangesToCRDTDoc', () => {
98
145
  const doc = new Y.Doc();