@wordpress/core-data 7.38.1-next.v.0 → 7.39.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 (85) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/build/actions.cjs +17 -6
  3. package/build/actions.cjs.map +2 -2
  4. package/build/awareness/base-awareness.cjs +62 -0
  5. package/build/awareness/base-awareness.cjs.map +7 -0
  6. package/build/awareness/post-editor-awareness.cjs +4 -18
  7. package/build/awareness/post-editor-awareness.cjs.map +3 -3
  8. package/build/entities.cjs +14 -2
  9. package/build/entities.cjs.map +2 -2
  10. package/build/resolvers.cjs +40 -0
  11. package/build/resolvers.cjs.map +2 -2
  12. package/build/types.cjs.map +1 -1
  13. package/build/utils/block-selection-history.cjs +101 -0
  14. package/build/utils/block-selection-history.cjs.map +7 -0
  15. package/build/utils/crdt-selection.cjs +139 -0
  16. package/build/utils/crdt-selection.cjs.map +7 -0
  17. package/build/utils/crdt-user-selections.cjs +2 -2
  18. package/build/utils/crdt-user-selections.cjs.map +2 -2
  19. package/build/utils/crdt-utils.cjs +29 -0
  20. package/build/utils/crdt-utils.cjs.map +3 -3
  21. package/build/utils/crdt.cjs +16 -4
  22. package/build/utils/crdt.cjs.map +2 -2
  23. package/build-module/actions.mjs +17 -6
  24. package/build-module/actions.mjs.map +2 -2
  25. package/build-module/awareness/base-awareness.mjs +35 -0
  26. package/build-module/awareness/base-awareness.mjs.map +7 -0
  27. package/build-module/awareness/post-editor-awareness.mjs +4 -18
  28. package/build-module/awareness/post-editor-awareness.mjs.map +2 -2
  29. package/build-module/entities.mjs +15 -2
  30. package/build-module/entities.mjs.map +2 -2
  31. package/build-module/resolvers.mjs +40 -0
  32. package/build-module/resolvers.mjs.map +2 -2
  33. package/build-module/utils/block-selection-history.mjs +75 -0
  34. package/build-module/utils/block-selection-history.mjs.map +7 -0
  35. package/build-module/utils/crdt-selection.mjs +115 -0
  36. package/build-module/utils/crdt-selection.mjs.map +7 -0
  37. package/build-module/utils/crdt-user-selections.mjs +2 -2
  38. package/build-module/utils/crdt-user-selections.mjs.map +2 -2
  39. package/build-module/utils/crdt-utils.mjs +28 -0
  40. package/build-module/utils/crdt-utils.mjs.map +2 -2
  41. package/build-module/utils/crdt.mjs +18 -3
  42. package/build-module/utils/crdt.mjs.map +2 -2
  43. package/build-types/actions.d.ts.map +1 -1
  44. package/build-types/awareness/base-awareness.d.ts +19 -0
  45. package/build-types/awareness/base-awareness.d.ts.map +1 -0
  46. package/build-types/awareness/post-editor-awareness.d.ts +7 -8
  47. package/build-types/awareness/post-editor-awareness.d.ts.map +1 -1
  48. package/build-types/entities.d.ts.map +1 -1
  49. package/build-types/entity-types/test/attachment.test.d.ts +10 -0
  50. package/build-types/entity-types/test/attachment.test.d.ts.map +1 -0
  51. package/build-types/index.d.ts.map +1 -1
  52. package/build-types/resolvers.d.ts.map +1 -1
  53. package/build-types/types.d.ts +12 -2
  54. package/build-types/types.d.ts.map +1 -1
  55. package/build-types/utils/block-selection-history.d.ts +47 -0
  56. package/build-types/utils/block-selection-history.d.ts.map +1 -0
  57. package/build-types/utils/crdt-selection.d.ts +16 -0
  58. package/build-types/utils/crdt-selection.d.ts.map +1 -0
  59. package/build-types/utils/crdt-user-selections.d.ts.map +1 -1
  60. package/build-types/utils/crdt-utils.d.ts +12 -0
  61. package/build-types/utils/crdt-utils.d.ts.map +1 -1
  62. package/build-types/utils/crdt.d.ts +8 -14
  63. package/build-types/utils/crdt.d.ts.map +1 -1
  64. package/build-types/utils/test/block-selection-history.test.d.ts +2 -0
  65. package/build-types/utils/test/block-selection-history.test.d.ts.map +1 -0
  66. package/build-types/utils/test/crdt-blocks.d.ts +2 -0
  67. package/build-types/utils/test/crdt-blocks.d.ts.map +1 -0
  68. package/build-types/utils/test/crdt.d.ts +2 -0
  69. package/build-types/utils/test/crdt.d.ts.map +1 -0
  70. package/package.json +21 -19
  71. package/src/actions.js +40 -7
  72. package/src/awareness/base-awareness.ts +50 -0
  73. package/src/awareness/post-editor-awareness.ts +4 -24
  74. package/src/entities.js +18 -2
  75. package/src/entity-types/test/attachment.test.ts +4 -4
  76. package/src/resolvers.js +51 -0
  77. package/src/test/actions.js +402 -0
  78. package/src/test/resolvers.js +4 -0
  79. package/src/types.ts +12 -3
  80. package/src/utils/block-selection-history.ts +176 -0
  81. package/src/utils/crdt-selection.ts +205 -0
  82. package/src/utils/crdt-user-selections.ts +7 -3
  83. package/src/utils/crdt-utils.ts +54 -0
  84. package/src/utils/crdt.ts +36 -3
  85. package/src/utils/test/block-selection-history.test.ts +764 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wordpress/core-data",
3
- "version": "7.38.1-next.v.0+b8934fcf9",
3
+ "version": "7.39.0",
4
4
  "description": "Access to and manipulation of core WordPress entities.",
5
5
  "author": "The WordPress Contributors",
6
6
  "license": "GPL-2.0-or-later",
@@ -49,22 +49,22 @@
49
49
  "build-module/index.mjs"
50
50
  ],
51
51
  "dependencies": {
52
- "@wordpress/api-fetch": "^7.38.1-next.v.0+b8934fcf9",
53
- "@wordpress/block-editor": "^15.11.1-next.v.0+b8934fcf9",
54
- "@wordpress/blocks": "^15.11.1-next.v.0+b8934fcf9",
55
- "@wordpress/compose": "^7.38.1-next.v.0+b8934fcf9",
56
- "@wordpress/data": "^10.38.1-next.v.0+b8934fcf9",
57
- "@wordpress/deprecated": "^4.38.1-next.v.0+b8934fcf9",
58
- "@wordpress/element": "^6.38.1-next.v.0+b8934fcf9",
59
- "@wordpress/html-entities": "^4.38.1-next.v.0+b8934fcf9",
60
- "@wordpress/i18n": "^6.11.1-next.v.0+b8934fcf9",
61
- "@wordpress/is-shallow-equal": "^5.38.1-next.v.0+b8934fcf9",
62
- "@wordpress/private-apis": "^1.38.1-next.v.0+b8934fcf9",
63
- "@wordpress/rich-text": "^7.38.1-next.v.0+b8934fcf9",
64
- "@wordpress/sync": "^1.38.1-next.v.0+b8934fcf9",
65
- "@wordpress/undo-manager": "^1.38.1-next.v.0+b8934fcf9",
66
- "@wordpress/url": "^4.38.1-next.v.0+b8934fcf9",
67
- "@wordpress/warning": "^3.38.1-next.v.0+b8934fcf9",
52
+ "@wordpress/api-fetch": "^7.39.0",
53
+ "@wordpress/block-editor": "^15.12.0",
54
+ "@wordpress/blocks": "^15.12.0",
55
+ "@wordpress/compose": "^7.39.0",
56
+ "@wordpress/data": "^10.39.0",
57
+ "@wordpress/deprecated": "^4.39.0",
58
+ "@wordpress/element": "^6.39.0",
59
+ "@wordpress/html-entities": "^4.39.0",
60
+ "@wordpress/i18n": "^6.12.0",
61
+ "@wordpress/is-shallow-equal": "^5.39.0",
62
+ "@wordpress/private-apis": "^1.39.0",
63
+ "@wordpress/rich-text": "^7.39.0",
64
+ "@wordpress/sync": "^1.39.0",
65
+ "@wordpress/undo-manager": "^1.39.0",
66
+ "@wordpress/url": "^4.39.0",
67
+ "@wordpress/warning": "^3.39.0",
68
68
  "change-case": "^4.1.2",
69
69
  "equivalent-key-map": "^0.2.2",
70
70
  "fast-deep-equal": "^3.1.3",
@@ -72,7 +72,9 @@
72
72
  "uuid": "^9.0.1"
73
73
  },
74
74
  "devDependencies": {
75
- "@types/node": "^20.17.10",
75
+ "@jest/globals": "^30.2.0",
76
+ "@types/jest": "^29.5.14",
77
+ "@types/node": "^20.19.0",
76
78
  "deep-freeze": "0.0.1"
77
79
  },
78
80
  "peerDependencies": {
@@ -82,5 +84,5 @@
82
84
  "publishConfig": {
83
85
  "access": "public"
84
86
  },
85
- "gitHead": "17529010285784fd2e735348a28391c79de87c88"
87
+ "gitHead": "eee1cfb1472f11183e40fb77465a5f13145df7ad"
86
88
  }
package/src/actions.js CHANGED
@@ -339,6 +339,15 @@ export const deleteEntityRecord =
339
339
  } );
340
340
 
341
341
  await dispatch( removeItems( kind, name, recordId, true ) );
342
+
343
+ if ( globalThis.IS_GUTENBERG_PLUGIN ) {
344
+ if ( entityConfig.syncConfig ) {
345
+ const objectType = `${ kind }/${ name }`;
346
+ const objectId = recordId;
347
+
348
+ getSyncManager()?.unload( objectType, objectId );
349
+ }
350
+ }
342
351
  } catch ( _error ) {
343
352
  hasError = true;
344
353
  error = _error;
@@ -393,6 +402,16 @@ export const editEntityRecord =
393
402
  recordId
394
403
  );
395
404
 
405
+ // Some fields are merged with the existing value instead of replaced.
406
+ // See `mergedEdits` definition on the entity config.
407
+ const editsWithMerges = Object.keys( edits ).reduce( ( acc, key ) => {
408
+ acc[ key ] = mergedEdits[ key ]
409
+ ? { ...editedRecord[ key ], ...edits[ key ] }
410
+ : edits[ key ];
411
+
412
+ return acc;
413
+ }, {} );
414
+
396
415
  const edit = {
397
416
  kind,
398
417
  name,
@@ -401,10 +420,7 @@ export const editEntityRecord =
401
420
  // so that the property is not considered dirty.
402
421
  edits: Object.keys( edits ).reduce( ( acc, key ) => {
403
422
  const recordValue = record[ key ];
404
- const editedRecordValue = editedRecord[ key ];
405
- const value = mergedEdits[ key ]
406
- ? { ...editedRecordValue, ...edits[ key ] }
407
- : edits[ key ];
423
+ const value = editsWithMerges[ key ];
408
424
  acc[ key ] = fastDeepEqual( recordValue, value )
409
425
  ? undefined
410
426
  : value;
@@ -416,11 +432,28 @@ export const editEntityRecord =
416
432
  const objectType = `${ kind }/${ name }`;
417
433
  const objectId = recordId;
418
434
 
435
+ // Determine whether this edit should create a new undo level.
436
+ //
437
+ // In Gutenberg, block changes flow through two callbacks:
438
+ // - `onInput`: For transient/in-progress changes (e.g., typing each
439
+ // character). These use `isCached: true` and get merged into
440
+ // the current undo item.
441
+ // - `onChange`: For persistent/completed changes (e.g., formatting
442
+ // transforms, block insertions). These use `isCached: false` and
443
+ // should create a new undo level.
444
+ //
445
+ // Additionally, `undoIgnore: true` means the change should not
446
+ // affect the undo history at all (e.g., selection-only changes).
447
+ const isNewUndoLevel = options.undoIgnore
448
+ ? false
449
+ : ! options.isCached;
450
+
419
451
  getSyncManager()?.update(
420
452
  objectType,
421
453
  objectId,
422
- edit.edits,
423
- LOCAL_EDITOR_ORIGIN
454
+ editsWithMerges,
455
+ LOCAL_EDITOR_ORIGIN,
456
+ { isNewUndoLevel }
424
457
  );
425
458
  }
426
459
  }
@@ -721,7 +754,7 @@ export const saveEntityRecord =
721
754
  recordId,
722
755
  updatedRecord,
723
756
  LOCAL_EDITOR_ORIGIN,
724
- true // isSave
757
+ { isSave: true }
725
758
  );
726
759
  }
727
760
  }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { select } from '@wordpress/data';
5
+ import { AwarenessState } from '@wordpress/sync';
6
+
7
+ /**
8
+ * Internal dependencies
9
+ */
10
+ import { STORE_NAME as coreStore } from '../name';
11
+ import { generateUserInfo, areUserInfosEqual } from './utils';
12
+
13
+ import type { BaseState } from './types';
14
+
15
+ export abstract class BaseAwarenessState<
16
+ State extends BaseState,
17
+ > extends AwarenessState< State > {
18
+ public setUp(): void {
19
+ super.setUp();
20
+
21
+ this.setCurrentUserInfo();
22
+ }
23
+
24
+ /**
25
+ * Set the current user info in the local state.
26
+ */
27
+ private setCurrentUserInfo(): void {
28
+ const states = this.getStates();
29
+ const otherUserColors = Array.from( states.entries() )
30
+ .filter(
31
+ ( [ clientId, state ] ) =>
32
+ state.userInfo && clientId !== this.clientID
33
+ )
34
+ .map( ( [ , state ] ) => state.userInfo.color )
35
+ .filter( Boolean );
36
+
37
+ // Get current user info and set it in local state.
38
+ const currentUser = select( coreStore ).getCurrentUser();
39
+ const userInfo = generateUserInfo( currentUser, otherUserColors );
40
+ this.setLocalStateField( 'userInfo', userInfo );
41
+ }
42
+ }
43
+
44
+ export const baseEqualityFieldChecks = {
45
+ userInfo: areUserInfosEqual,
46
+ };
47
+
48
+ export class BaseAwareness extends BaseAwarenessState< BaseState > {
49
+ protected equalityFieldChecks = baseEqualityFieldChecks;
50
+ }
@@ -2,19 +2,19 @@
2
2
  * WordPress dependencies
3
3
  */
4
4
  import { dispatch, select, subscribe } from '@wordpress/data';
5
- import { AwarenessState, type Y } from '@wordpress/sync';
5
+ import type { Y } from '@wordpress/sync';
6
6
  // @ts-ignore No exported types for block editor store selectors.
7
7
  import { store as blockEditorStore } from '@wordpress/block-editor';
8
8
 
9
9
  /**
10
10
  * Internal dependencies
11
11
  */
12
+ import { BaseAwarenessState, baseEqualityFieldChecks } from './base-awareness';
12
13
  import {
13
14
  AWARENESS_CURSOR_UPDATE_THROTTLE_IN_MS,
14
15
  LOCAL_CURSOR_UPDATE_DEBOUNCE_IN_MS,
15
16
  } from './config';
16
17
  import { STORE_NAME as coreStore } from '../name';
17
- import { generateUserInfo, areUserInfosEqual } from './utils';
18
18
  import {
19
19
  areSelectionsStatesEqual,
20
20
  getSelectionState,
@@ -23,10 +23,10 @@ import {
23
23
  import type { WPBlockSelection } from '../types';
24
24
  import type { EditorState, PostEditorState } from './types';
25
25
 
26
- export class PostEditorAwareness extends AwarenessState< PostEditorState > {
26
+ export class PostEditorAwareness extends BaseAwarenessState< PostEditorState > {
27
27
  protected equalityFieldChecks = {
28
+ ...baseEqualityFieldChecks,
28
29
  editorState: this.areEditorStatesEqual,
29
- userInfo: areUserInfosEqual,
30
30
  };
31
31
 
32
32
  public constructor(
@@ -41,29 +41,9 @@ export class PostEditorAwareness extends AwarenessState< PostEditorState > {
41
41
  public setUp(): void {
42
42
  super.setUp();
43
43
 
44
- this.setCurrentUserInfo();
45
44
  this.subscribeToUserSelectionChanges();
46
45
  }
47
46
 
48
- /**
49
- * Set the current user info in the local state.
50
- */
51
- private setCurrentUserInfo(): void {
52
- const states = this.getStates();
53
- const otherUserColors = Array.from( states.entries() )
54
- .filter(
55
- ( [ clientId, state ] ) =>
56
- state.userInfo && clientId !== this.clientID
57
- )
58
- .map( ( [ , state ] ) => state.userInfo.color )
59
- .filter( Boolean );
60
-
61
- // Get current user info and set it in local state.
62
- const currentUser = select( coreStore ).getCurrentUser();
63
- const userInfo = generateUserInfo( currentUser, otherUserColors );
64
- this.setLocalStateField( 'userInfo', userInfo );
65
- }
66
-
67
47
  /**
68
48
  * Subscribe to user selection changes and update the selection state.
69
49
  */
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
+ defaultSyncConfig,
20
21
  getPostChangesFromCRDTDoc,
21
22
  } from './utils/crdt';
22
23
 
@@ -219,7 +220,16 @@ export const rootEntitiesConfig = [
219
220
  plural: 'fontCollections',
220
221
  key: 'slug',
221
222
  },
222
- ];
223
+ ].map( ( entity ) => {
224
+ const syncEnabledRootEntities = new Set( [ 'comment' ] );
225
+
226
+ if ( globalThis.IS_GUTENBERG_PLUGIN ) {
227
+ if ( syncEnabledRootEntities.has( entity.name ) ) {
228
+ entity.syncConfig = defaultSyncConfig;
229
+ }
230
+ }
231
+ return entity;
232
+ } );
223
233
 
224
234
  export const deprecatedEntities = {
225
235
  root: {
@@ -413,7 +423,7 @@ async function loadTaxonomyEntities() {
413
423
  } );
414
424
  return Object.entries( taxonomies ?? {} ).map( ( [ name, taxonomy ] ) => {
415
425
  const namespace = taxonomy?.rest_namespace ?? 'wp/v2';
416
- return {
426
+ const entity = {
417
427
  kind: 'taxonomy',
418
428
  baseURL: `/${ namespace }/${ taxonomy.rest_base }`,
419
429
  baseURLParams: { context: 'edit' },
@@ -422,6 +432,12 @@ async function loadTaxonomyEntities() {
422
432
  getTitle: ( record ) => record?.name,
423
433
  supportsPagination: true,
424
434
  };
435
+
436
+ if ( globalThis.IS_GUTENBERG_PLUGIN ) {
437
+ entity.syncConfig = defaultSyncConfig;
438
+ }
439
+
440
+ return entity;
425
441
  } );
426
442
  }
427
443
 
@@ -20,7 +20,7 @@ describe( 'Attachment type', () => {
20
20
  describe( 'Image attachment', () => {
21
21
  it( 'should validate against real image attachment from REST API', () => {
22
22
  const attachment: Attachment< 'edit' > =
23
- imageAttachmentFixture as Attachment< 'edit' >;
23
+ imageAttachmentFixture as unknown as Attachment< 'edit' >;
24
24
 
25
25
  // Edit-context fields
26
26
  expect( attachment.permalink_template ).toBeDefined();
@@ -38,7 +38,7 @@ describe( 'Attachment type', () => {
38
38
  describe( 'Zip file attachment', () => {
39
39
  it( 'should validate against real zip file attachment from REST API', () => {
40
40
  const attachment: Attachment< 'edit' > =
41
- zipAttachmentFixture as Attachment< 'edit' >;
41
+ zipAttachmentFixture as unknown as Attachment< 'edit' >;
42
42
 
43
43
  // Edit-context fields
44
44
  expect( attachment.permalink_template ).toBeDefined();
@@ -58,7 +58,7 @@ describe( 'Attachment type', () => {
58
58
  describe( 'Audio file attachment', () => {
59
59
  it( 'should validate against real audio attachment from REST API', () => {
60
60
  const attachment: Attachment< 'edit' > =
61
- audioAttachmentFixture as Attachment< 'edit' >;
61
+ audioAttachmentFixture as unknown as Attachment< 'edit' >;
62
62
 
63
63
  // Edit-context fields
64
64
  expect( attachment.permalink_template ).toBeDefined();
@@ -79,7 +79,7 @@ describe( 'Attachment type', () => {
79
79
  describe( 'Video file attachment', () => {
80
80
  it( 'should validate against real video attachment from REST API', () => {
81
81
  const attachment: Attachment< 'edit' > =
82
- videoAttachmentFixture as Attachment< 'edit' >;
82
+ videoAttachmentFixture as unknown as Attachment< 'edit' >;
83
83
 
84
84
  // Edit-context fields
85
85
  expect( attachment.permalink_template ).toBeDefined();
package/src/resolvers.js CHANGED
@@ -26,6 +26,7 @@ import {
26
26
  isNumericID,
27
27
  } from './utils';
28
28
  import { fetchBlockPatterns } from './fetch';
29
+ import { restoreSelection, getSelectionHistory } from './utils/crdt-selection';
29
30
 
30
31
  /**
31
32
  * Requests authors from the REST API.
@@ -233,6 +234,36 @@ export const getEntityRecord =
233
234
  key
234
235
  );
235
236
  },
237
+ addUndoMeta: ( ydoc, meta ) => {
238
+ const selectionHistory =
239
+ getSelectionHistory( ydoc );
240
+
241
+ if ( selectionHistory ) {
242
+ meta.set(
243
+ 'selectionHistory',
244
+ selectionHistory
245
+ );
246
+ }
247
+ },
248
+ restoreUndoMeta: ( ydoc, meta ) => {
249
+ const selectionHistory =
250
+ meta.get( 'selectionHistory' );
251
+
252
+ if ( selectionHistory ) {
253
+ // Because Yjs initiates an undo, we need to
254
+ // wait until the content is restored before
255
+ // we can update the selection.
256
+ // Use setTimeout() to wait until content is
257
+ // finished updating, and then set the correct
258
+ // selection.
259
+ setTimeout( () => {
260
+ restoreSelection(
261
+ selectionHistory,
262
+ ydoc
263
+ );
264
+ }, 0 );
265
+ }
266
+ },
236
267
  }
237
268
  );
238
269
  }
@@ -431,6 +462,26 @@ export const getEntityRecords =
431
462
  };
432
463
  }
433
464
 
465
+ if ( globalThis.IS_GUTENBERG_PLUGIN ) {
466
+ if ( entityConfig.syncConfig && -1 === query.per_page ) {
467
+ const objectType = `${ kind }/${ name }`;
468
+ getSyncManager()?.loadCollection(
469
+ entityConfig.syncConfig,
470
+ objectType,
471
+ {
472
+ refetchRecords: async () => {
473
+ dispatch.receiveEntityRecords(
474
+ kind,
475
+ name,
476
+ await apiFetch( { path, parse: true } ),
477
+ query
478
+ );
479
+ },
480
+ }
481
+ );
482
+ }
483
+ }
484
+
434
485
  // If we request fields but the result doesn't contain the fields,
435
486
  // explicitly set these fields as "undefined"
436
487
  // that way we consider the query "fulfilled".