@wordpress/core-data 7.40.2-next.v.202602241322.0 → 7.41.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 (100) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/build/actions.cjs +1 -1
  3. package/build/actions.cjs.map +2 -2
  4. package/build/awareness/types.cjs.map +1 -1
  5. package/build/entities.cjs +17 -10
  6. package/build/entities.cjs.map +2 -2
  7. package/build/hooks/use-post-editor-awareness-state.cjs +38 -0
  8. package/build/hooks/use-post-editor-awareness-state.cjs.map +2 -2
  9. package/build/private-actions.cjs +7 -2
  10. package/build/private-actions.cjs.map +2 -2
  11. package/build/private-apis.cjs +4 -1
  12. package/build/private-apis.cjs.map +2 -2
  13. package/build/private-selectors.cjs +7 -2
  14. package/build/private-selectors.cjs.map +2 -2
  15. package/build/reducer.cjs +11 -1
  16. package/build/reducer.cjs.map +2 -2
  17. package/build/resolvers.cjs +15 -12
  18. package/build/resolvers.cjs.map +2 -2
  19. package/build/selectors.cjs.map +2 -2
  20. package/build/sync.cjs +5 -5
  21. package/build/sync.cjs.map +1 -1
  22. package/build/types.cjs.map +1 -1
  23. package/build/utils/crdt-blocks.cjs +50 -31
  24. package/build/utils/crdt-blocks.cjs.map +2 -2
  25. package/build/utils/crdt-selection.cjs +46 -18
  26. package/build/utils/crdt-selection.cjs.map +2 -2
  27. package/build/utils/crdt.cjs +12 -1
  28. package/build/utils/crdt.cjs.map +2 -2
  29. package/build-module/actions.mjs +1 -1
  30. package/build-module/actions.mjs.map +2 -2
  31. package/build-module/entities.mjs +19 -11
  32. package/build-module/entities.mjs.map +2 -2
  33. package/build-module/hooks/use-post-editor-awareness-state.mjs +37 -0
  34. package/build-module/hooks/use-post-editor-awareness-state.mjs.map +2 -2
  35. package/build-module/private-actions.mjs +5 -1
  36. package/build-module/private-actions.mjs.map +2 -2
  37. package/build-module/private-apis.mjs +6 -2
  38. package/build-module/private-apis.mjs.map +2 -2
  39. package/build-module/private-selectors.mjs +5 -1
  40. package/build-module/private-selectors.mjs.map +2 -2
  41. package/build-module/reducer.mjs +10 -1
  42. package/build-module/reducer.mjs.map +2 -2
  43. package/build-module/resolvers.mjs +15 -12
  44. package/build-module/resolvers.mjs.map +2 -2
  45. package/build-module/selectors.mjs.map +2 -2
  46. package/build-module/sync.mjs +3 -3
  47. package/build-module/sync.mjs.map +1 -1
  48. package/build-module/utils/crdt-blocks.mjs +50 -31
  49. package/build-module/utils/crdt-blocks.mjs.map +2 -2
  50. package/build-module/utils/crdt-selection.mjs +45 -18
  51. package/build-module/utils/crdt-selection.mjs.map +2 -2
  52. package/build-module/utils/crdt.mjs +16 -6
  53. package/build-module/utils/crdt.mjs.map +2 -2
  54. package/build-types/awareness/types.d.ts +5 -0
  55. package/build-types/awareness/types.d.ts.map +1 -1
  56. package/build-types/entities.d.ts +1 -1
  57. package/build-types/entities.d.ts.map +1 -1
  58. package/build-types/hooks/use-post-editor-awareness-state.d.ts +10 -1
  59. package/build-types/hooks/use-post-editor-awareness-state.d.ts.map +1 -1
  60. package/build-types/index.d.ts.map +1 -1
  61. package/build-types/private-actions.d.ts +1 -0
  62. package/build-types/private-actions.d.ts.map +1 -1
  63. package/build-types/private-apis.d.ts.map +1 -1
  64. package/build-types/private-selectors.d.ts +7 -0
  65. package/build-types/private-selectors.d.ts.map +1 -1
  66. package/build-types/reducer.d.ts +15 -0
  67. package/build-types/reducer.d.ts.map +1 -1
  68. package/build-types/resolvers.d.ts.map +1 -1
  69. package/build-types/selectors.d.ts +1 -0
  70. package/build-types/selectors.d.ts.map +1 -1
  71. package/build-types/sync.d.ts +2 -2
  72. package/build-types/sync.d.ts.map +1 -1
  73. package/build-types/types.d.ts +1 -0
  74. package/build-types/types.d.ts.map +1 -1
  75. package/build-types/utils/crdt-blocks.d.ts +1 -1
  76. package/build-types/utils/crdt-blocks.d.ts.map +1 -1
  77. package/build-types/utils/crdt-selection.d.ts +10 -0
  78. package/build-types/utils/crdt-selection.d.ts.map +1 -1
  79. package/build-types/utils/crdt.d.ts +1 -0
  80. package/build-types/utils/crdt.d.ts.map +1 -1
  81. package/package.json +18 -18
  82. package/src/actions.js +2 -2
  83. package/src/awareness/types.ts +6 -0
  84. package/src/entities.js +23 -11
  85. package/src/hooks/use-post-editor-awareness-state.ts +70 -0
  86. package/src/private-actions.js +13 -0
  87. package/src/private-apis.js +4 -0
  88. package/src/private-selectors.ts +10 -0
  89. package/src/reducer.js +21 -0
  90. package/src/resolvers.js +21 -15
  91. package/src/selectors.ts +1 -0
  92. package/src/sync.ts +2 -2
  93. package/src/test/entities.js +47 -14
  94. package/src/test/resolvers.js +46 -80
  95. package/src/types.ts +1 -0
  96. package/src/utils/crdt-blocks.ts +113 -47
  97. package/src/utils/crdt-selection.ts +84 -24
  98. package/src/utils/crdt.ts +23 -7
  99. package/src/utils/test/crdt-blocks.ts +938 -0
  100. package/src/utils/test/crdt.ts +136 -10
@@ -1 +1 @@
1
- {"version":3,"file":"crdt-selection.d.ts","sourceRoot":"","sources":["../../src/utils/crdt-selection.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,KAAK,OAAO,EAAE,CAAC,EAAE,MAAM,iBAAiB,CAAC;AAElD;;GAEG;AACH,OAAO,EAIN,KAAK,cAAc,EAEnB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,KAAK,EAAoB,WAAW,EAAE,MAAM,UAAU,CAAC;AAsB9D,wBAAgB,mBAAmB,CAAE,IAAI,EAAE,OAAO,GAAI,cAAc,EAAE,CAErE;AAED,wBAAgB,sBAAsB,CACrC,IAAI,EAAE,OAAO,EACb,WAAW,EAAE,WAAW,GACtB,IAAI,CAEN;AAkFD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC/B,gBAAgB,EAAE,cAAc,EAAE,EAClC,IAAI,EAAE,CAAC,CAAC,GAAG,GACT,IAAI,CA+DN"}
1
+ {"version":3,"file":"crdt-selection.d.ts","sourceRoot":"","sources":["../../src/utils/crdt-selection.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,KAAK,OAAO,EAAE,CAAC,EAAE,MAAM,iBAAiB,CAAC;AAElD;;GAEG;AACH,OAAO,EAIN,KAAK,cAAc,EAEnB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,KAAK,EAAoB,WAAW,EAAE,MAAM,UAAU,CAAC;AAsB9D,wBAAgB,mBAAmB,CAAE,IAAI,EAAE,OAAO,GAAI,cAAc,EAAE,CAErE;AAED,wBAAgB,sBAAsB,CACrC,IAAI,EAAE,OAAO,EACb,WAAW,EAAE,WAAW,GACtB,IAAI,CAEN;AAiGD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC/B,gBAAgB,EAAE,cAAc,EAAE,EAClC,IAAI,EAAE,CAAC,CAAC,GAAG,GACT,IAAI,CA+DN;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAClC,IAAI,EAAE,CAAC,CAAC,GAAG,EACX,gBAAgB,EAAE,cAAc,EAAE,GAChC,WAAW,GAAG,IAAI,CA+BpB"}
@@ -30,6 +30,7 @@ export interface YPostRecord extends YMapRecord {
30
30
  template: string;
31
31
  title: Y.Text;
32
32
  }
33
+ export declare const POST_META_KEY_FOR_CRDT_DOC_PERSISTENCE = "_crdt_document";
33
34
  /**
34
35
  * Given a set of local changes to a post record, apply those changes to the
35
36
  * local Y.Doc.
@@ -1 +1 @@
1
- {"version":3,"file":"crdt.d.ts","sourceRoot":"","sources":["../../src/utils/crdt.ts"],"names":[],"mappings":"AAUA,OAAO,EACN,KAAK,OAAO,EAEZ,KAAK,UAAU,EACf,CAAC,EACD,MAAM,iBAAiB,CAAC;AAMzB,OAAO,EAGN,KAAK,KAAK,EAEV,KAAK,OAAO,EACZ,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAM5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,OAAO,EAIN,KAAK,UAAU,EACf,KAAK,QAAQ,EACb,MAAM,cAAc,CAAC;AAGtB,MAAM,MAAM,WAAW,GAAG,OAAO,CAAE,IAAI,CAAE,GAAG;IAC3C,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;IACjB,OAAO,CAAC,EAAE,IAAI,CAAE,SAAS,CAAE,GAAG,MAAM,CAAC;IACrC,OAAO,CAAC,EAAE,IAAI,CAAE,SAAS,CAAE,GAAG,MAAM,CAAC;IACrC,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,KAAK,CAAC,EAAE,IAAI,CAAE,OAAO,CAAE,GAAG,MAAM,CAAC;CACjC,CAAC;AAGF,MAAM,WAAW,WAAY,SAAQ,UAAU;IAC9C,MAAM,EAAE,MAAM,CAAC;IAEf,MAAM,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC;IAChB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,QAAQ,CAAE,UAAU,CAAE,CAAC;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC;CACd;AA2DD;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACxC,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,IAAI,GACb,IAAI,CAoIN;AAMD;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACxC,IAAI,EAAE,OAAO,EACb,YAAY,EAAE,IAAI,EAClB,SAAS,EAAE,IAAI,GACb,WAAW,CAyHb;AAED;;;GAGG;AACH,eAAO,MAAM,iBAAiB,EAAE,UAI/B,CAAC"}
1
+ {"version":3,"file":"crdt.d.ts","sourceRoot":"","sources":["../../src/utils/crdt.ts"],"names":[],"mappings":"AAUA,OAAO,EACN,KAAK,OAAO,EAEZ,KAAK,UAAU,EACf,CAAC,EACD,MAAM,iBAAiB,CAAC;AAMzB,OAAO,EAGN,KAAK,KAAK,EAEV,KAAK,OAAO,EACZ,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAE5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAM5C,OAAO,EAIN,KAAK,UAAU,EACf,KAAK,QAAQ,EACb,MAAM,cAAc,CAAC;AAGtB,MAAM,MAAM,WAAW,GAAG,OAAO,CAAE,IAAI,CAAE,GAAG;IAC3C,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;IACjB,OAAO,CAAC,EAAE,IAAI,CAAE,SAAS,CAAE,GAAG,MAAM,CAAC;IACrC,OAAO,CAAC,EAAE,IAAI,CAAE,SAAS,CAAE,GAAG,MAAM,CAAC;IACrC,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,KAAK,CAAC,EAAE,IAAI,CAAE,OAAO,CAAE,GAAG,MAAM,CAAC;CACjC,CAAC;AAGF,MAAM,WAAW,WAAY,SAAQ,UAAU;IAC9C,MAAM,EAAE,MAAM,CAAC;IAEf,MAAM,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC;IAChB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,QAAQ,CAAE,UAAU,CAAE,CAAC;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC;CACd;AAED,eAAO,MAAM,sCAAsC,mBAAmB,CAAC;AA2DvE;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACxC,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,IAAI,GACb,IAAI,CAoIN;AAMD;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACxC,IAAI,EAAE,OAAO,EACb,YAAY,EAAE,IAAI,EAClB,SAAS,EAAE,IAAI,GACb,WAAW,CAuIb;AAED;;;GAGG;AACH,eAAO,MAAM,iBAAiB,EAAE,UAI/B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wordpress/core-data",
3
- "version": "7.40.2-next.v.202602241322.0+bce7cff88",
3
+ "version": "7.41.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.40.1-next.v.202602241322.0+bce7cff88",
53
- "@wordpress/block-editor": "^15.13.2-next.v.202602241322.0+bce7cff88",
54
- "@wordpress/blocks": "^15.13.1-next.v.202602241322.0+bce7cff88",
55
- "@wordpress/compose": "^7.40.1-next.v.202602241322.0+bce7cff88",
56
- "@wordpress/data": "^10.40.1-next.v.202602241322.0+bce7cff88",
57
- "@wordpress/deprecated": "^4.40.1-next.v.202602241322.0+bce7cff88",
58
- "@wordpress/element": "^6.40.1-next.v.202602241322.0+bce7cff88",
59
- "@wordpress/html-entities": "^4.40.1-next.v.202602241322.0+bce7cff88",
60
- "@wordpress/i18n": "^6.13.1-next.v.202602241322.0+bce7cff88",
61
- "@wordpress/is-shallow-equal": "^5.40.1-next.v.202602241322.0+bce7cff88",
62
- "@wordpress/private-apis": "^1.40.1-next.v.202602241322.0+bce7cff88",
63
- "@wordpress/rich-text": "^7.40.1-next.v.202602241322.0+bce7cff88",
64
- "@wordpress/sync": "^1.40.1-next.v.202602241322.0+bce7cff88",
65
- "@wordpress/undo-manager": "^1.40.1-next.v.202602241322.0+bce7cff88",
66
- "@wordpress/url": "^4.40.1-next.v.202602241322.0+bce7cff88",
67
- "@wordpress/warning": "^3.40.1-next.v.202602241322.0+bce7cff88",
52
+ "@wordpress/api-fetch": "^7.41.0",
53
+ "@wordpress/block-editor": "^15.14.0",
54
+ "@wordpress/blocks": "^15.14.0",
55
+ "@wordpress/compose": "^7.41.0",
56
+ "@wordpress/data": "^10.41.0",
57
+ "@wordpress/deprecated": "^4.41.0",
58
+ "@wordpress/element": "^6.41.0",
59
+ "@wordpress/html-entities": "^4.41.0",
60
+ "@wordpress/i18n": "^6.14.0",
61
+ "@wordpress/is-shallow-equal": "^5.41.0",
62
+ "@wordpress/private-apis": "^1.41.0",
63
+ "@wordpress/rich-text": "^7.41.0",
64
+ "@wordpress/sync": "^1.41.0",
65
+ "@wordpress/undo-manager": "^1.41.0",
66
+ "@wordpress/url": "^4.41.0",
67
+ "@wordpress/warning": "^3.41.0",
68
68
  "change-case": "^4.1.2",
69
69
  "equivalent-key-map": "^0.2.2",
70
70
  "fast-deep-equal": "^3.1.3",
@@ -84,5 +84,5 @@
84
84
  "publishConfig": {
85
85
  "access": "public"
86
86
  },
87
- "gitHead": "943dde7f0b600ce238726c36284bc9f70ce0ffa4"
87
+ "gitHead": "8bfc179b9aed74c0a6dd6e8edf7a49e40e4f87cc"
88
88
  }
package/src/actions.js CHANGED
@@ -773,10 +773,10 @@ export const saveEntityRecord =
773
773
  if ( entityConfig.__unstablePrePersist ) {
774
774
  edits = {
775
775
  ...edits,
776
- ...entityConfig.__unstablePrePersist(
776
+ ...( await entityConfig.__unstablePrePersist(
777
777
  persistedRecord,
778
778
  edits
779
- ),
779
+ ) ),
780
780
  };
781
781
  }
782
782
  updatedRecord = await __unstableFetch( {
@@ -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
@@ -19,6 +19,7 @@ import {
19
19
  applyPostChangesToCRDTDoc,
20
20
  defaultSyncConfig,
21
21
  getPostChangesFromCRDTDoc,
22
+ POST_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
22
23
  } from './utils/crdt';
23
24
 
24
25
  export const DEFAULT_ENTITY_KEY = 'id';
@@ -274,9 +275,9 @@ export const additionalEntityConfigLoaders = [
274
275
  * @param {Object} edits Edits.
275
276
  * @param {string} name Post type name.
276
277
  * @param {boolean} isTemplate Whether the post type is a template.
277
- * @return {Object} Updated edits.
278
+ * @return {Promise< Object >} Updated edits.
278
279
  */
279
- export const prePersistPostType = (
280
+ export const prePersistPostType = async (
280
281
  persistedRecord,
281
282
  edits,
282
283
  name,
@@ -305,11 +306,17 @@ export const prePersistPostType = (
305
306
  if ( persistedRecord ) {
306
307
  const objectType = `postType/${ name }`;
307
308
  const objectId = persistedRecord.id;
308
- const meta = getSyncManager()?.createMeta( objectType, objectId );
309
- newEdits.meta = {
310
- ...edits.meta,
311
- ...meta,
312
- };
309
+ const serializedDoc = await getSyncManager()?.createPersistedCRDTDoc(
310
+ objectType,
311
+ objectId
312
+ );
313
+
314
+ if ( serializedDoc ) {
315
+ newEdits.meta = {
316
+ ...edits.meta,
317
+ [ POST_META_KEY_FOR_CRDT_DOC_PERSISTENCE ]: serializedDoc,
318
+ };
319
+ }
313
320
  }
314
321
 
315
322
  return newEdits;
@@ -404,12 +411,17 @@ async function loadPostTypeEntities() {
404
411
  getPostChangesFromCRDTDoc( crdtDoc, editedRecord, postType ),
405
412
 
406
413
  /**
407
- * Sync features supported by the entity.
414
+ * Extract changes from a CRDT document that can be used to update the
415
+ * local editor state.
408
416
  *
409
- * @type {Record< string, boolean >}
417
+ * @param {import('@wordpress/sync').ObjectData} record
418
+ * @return {Partial< import('@wordpress/sync').ObjectData >} Changes to record
410
419
  */
411
- supports: {
412
- crdtPersistence: true,
420
+ getPersistedCRDTDoc: ( record ) => {
421
+ return (
422
+ record?.meta[ POST_META_KEY_FOR_CRDT_DOC_PERSISTENCE ] ||
423
+ null
424
+ );
413
425
  },
414
426
  };
415
427
 
@@ -2,6 +2,7 @@
2
2
  * External dependencies
3
3
  */
4
4
  import { useEffect, useState } from '@wordpress/element';
5
+ import type { Y } from '@wordpress/sync';
5
6
 
6
7
  /**
7
8
  * Internal dependencies
@@ -9,6 +10,7 @@ import { useEffect, useState } from '@wordpress/element';
9
10
  import { getSyncManager } from '../sync';
10
11
  import type {
11
12
  PostEditorAwarenessState as ActiveCollaborator,
13
+ PostSaveEvent,
12
14
  YDocDebugData,
13
15
  } from '../awareness/types';
14
16
  import type { SelectionState } from '../types';
@@ -156,3 +158,71 @@ export function useIsDisconnected(
156
158
  return usePostEditorAwarenessState( postId, postType )
157
159
  .isCurrentCollaboratorDisconnected;
158
160
  }
161
+
162
+ /**
163
+ * Hook that subscribes to the CRDT state map and returns the most recent
164
+ * save event (timestamp + client ID). The state map is updated by
165
+ * `markEntityAsSaved` in `@wordpress/sync`
166
+ *
167
+ * @param postId The ID of the post.
168
+ * @param postType The type of the post.
169
+ */
170
+ export function useLastPostSave(
171
+ postId: number | null,
172
+ postType: string | null
173
+ ): PostSaveEvent | null {
174
+ const [ lastSave, setLastSave ] = useState< PostSaveEvent | null >( null );
175
+
176
+ useEffect( () => {
177
+ if ( null === postId || null === postType ) {
178
+ setLastSave( null );
179
+ return;
180
+ }
181
+
182
+ const awareness = getSyncManager()?.getAwareness< PostEditorAwareness >(
183
+ `postType/${ postType }`,
184
+ postId.toString()
185
+ );
186
+
187
+ if ( ! awareness ) {
188
+ setLastSave( null );
189
+ return;
190
+ }
191
+
192
+ awareness.setUp();
193
+
194
+ const stateMap = awareness.doc.getMap( 'state' );
195
+ const recordMap = awareness.doc.getMap( 'document' );
196
+
197
+ // Only notify for saves that occur after the observer is
198
+ // set up. This prevents false notifications when the Y.Doc
199
+ // syncs historical state on page load or peer reconnect.
200
+ const setupTime = Date.now();
201
+
202
+ const observer = ( event: Y.YMapEvent< unknown > ) => {
203
+ if ( event.keysChanged.has( 'savedAt' ) ) {
204
+ const savedAt = stateMap.get( 'savedAt' ) as number;
205
+ const savedByClientId = stateMap.get( 'savedBy' ) as number;
206
+
207
+ if (
208
+ typeof savedAt === 'number' &&
209
+ typeof savedByClientId === 'number' &&
210
+ savedAt > setupTime
211
+ ) {
212
+ const postStatus = recordMap.get( 'status' ) as
213
+ | string
214
+ | undefined;
215
+ setLastSave( { savedAt, savedByClientId, postStatus } );
216
+ }
217
+ }
218
+ };
219
+
220
+ stateMap.observe( observer );
221
+
222
+ return () => {
223
+ stateMap.unobserve( observer );
224
+ };
225
+ }, [ postId, postType ] );
226
+
227
+ return lastSave;
228
+ }
@@ -160,3 +160,16 @@ export function receiveEditorAssets( assets ) {
160
160
  assets,
161
161
  };
162
162
  }
163
+
164
+ /**
165
+ * Returns an action object used to set whether collaboration is supported.
166
+ *
167
+ * @param {boolean} supported Whether collaboration is supported.
168
+ *
169
+ * @return {Object} Action object.
170
+ */
171
+ export const setCollaborationSupported =
172
+ ( supported ) =>
173
+ ( { dispatch } ) => {
174
+ dispatch( { type: 'SET_COLLABORATION_SUPPORTED', supported } );
175
+ };
@@ -6,13 +6,17 @@ import { RECEIVE_INTERMEDIATE_RESULTS } from './utils';
6
6
  import {
7
7
  useActiveCollaborators,
8
8
  useResolvedSelection,
9
+ useLastPostSave,
9
10
  } from './hooks/use-post-editor-awareness-state';
10
11
  import { lock } from './lock-unlock';
12
+ import { retrySyncConnection } from './sync';
11
13
 
12
14
  export const privateApis = {};
13
15
  lock( privateApis, {
14
16
  useEntityRecordsWithPermissions,
15
17
  RECEIVE_INTERMEDIATE_RESULTS,
18
+ retrySyncConnection,
16
19
  useActiveCollaborators,
17
20
  useResolvedSelection,
21
+ useLastPostSave,
18
22
  } );
@@ -304,3 +304,13 @@ export function getEditorSettings(
304
304
  export function getEditorAssets( state: State ): Record< string, any > | null {
305
305
  return state.editorAssets;
306
306
  }
307
+
308
+ /**
309
+ * Returns whether collaboration is supported.
310
+ *
311
+ * @param state Data state.
312
+ * @return Whether collaboration is supported.
313
+ */
314
+ export function isCollaborationSupported( state: State ): boolean {
315
+ return state.collaborationSupported;
316
+ }
package/src/reducer.js CHANGED
@@ -687,6 +687,26 @@ export function syncConnectionStatuses( state = {}, action ) {
687
687
  return state;
688
688
  }
689
689
 
690
+ /**
691
+ * Reducer managing whether collaboration is supported.
692
+ *
693
+ * Default to true, as collaboration is supported by default
694
+ * unless explicitly disabled due to unsupported conditions
695
+ * such as metaboxes.
696
+ *
697
+ * @param {boolean} state Current state.
698
+ * @param {Object} action Dispatched action.
699
+ *
700
+ * @return {boolean} Updated state.
701
+ */
702
+ export function collaborationSupported( state = true, action ) {
703
+ switch ( action.type ) {
704
+ case 'SET_COLLABORATION_SUPPORTED':
705
+ return action.supported;
706
+ }
707
+ return state;
708
+ }
709
+
690
710
  export default combineReducers( {
691
711
  users,
692
712
  currentTheme,
@@ -710,4 +730,5 @@ export default combineReducers( {
710
730
  editorSettings,
711
731
  editorAssets,
712
732
  syncConnectionStatuses,
733
+ collaborationSupported,
713
734
  } );
package/src/resolvers.js CHANGED
@@ -235,6 +235,12 @@ export const getEntityRecord =
235
235
  resolveSelect
236
236
  .getEditedEntityRecord( kind, name, key )
237
237
  .then( ( editedRecord ) => {
238
+ // Don't trigger a save if the record is still an auto-draft.
239
+ const { status } = editedRecord;
240
+ if ( 'auto-draft' === status ) {
241
+ return;
242
+ }
243
+
238
244
  dispatch.saveEntityRecord(
239
245
  kind,
240
246
  name,
@@ -707,21 +713,21 @@ export const canUser =
707
713
  const permissions = getUserPermissionsFromAllowHeader(
708
714
  response.headers?.get( 'allow' )
709
715
  );
710
- registry.batch( () => {
711
- for ( const action of ALLOWED_RESOURCE_ACTIONS ) {
712
- const key = getUserPermissionCacheKey( action, resource, id );
713
-
714
- dispatch.receiveUserPermission( key, permissions[ action ] );
715
-
716
- // Mark related action resolutions as finished.
717
- if ( action !== requestedAction ) {
718
- dispatch.finishResolution( 'canUser', [
719
- action,
720
- resource,
721
- id,
722
- ] );
723
- }
716
+ const receiveUserPermissionArgs = {};
717
+ const canUserResolutionsArgs = [];
718
+ for ( const action of ALLOWED_RESOURCE_ACTIONS ) {
719
+ receiveUserPermissionArgs[
720
+ getUserPermissionCacheKey( action, resource, id )
721
+ ] = permissions[ action ];
722
+
723
+ // Mark related action resolutions as finished.
724
+ if ( action !== requestedAction ) {
725
+ canUserResolutionsArgs.push( [ action, resource, id ] );
724
726
  }
727
+ }
728
+ registry.batch( () => {
729
+ dispatch.receiveUserPermissions( receiveUserPermissionArgs );
730
+ dispatch.finishResolutions( 'canUser', canUserResolutionsArgs );
725
731
  } );
726
732
  };
727
733
 
@@ -1099,7 +1105,7 @@ export const getRevisions =
1099
1105
  // When requesting all fields, the list of results can be used to
1100
1106
  // resolve the `getRevision` selector in addition to `getRevisions`.
1101
1107
  if ( ! query?._fields && ! query.context ) {
1102
- const key = entityConfig.key || DEFAULT_ENTITY_KEY;
1108
+ const key = entityConfig.revisionKey || DEFAULT_ENTITY_KEY;
1103
1109
  const resolutionsArgs = records
1104
1110
  .filter( ( record ) => record[ key ] )
1105
1111
  .map( ( record ) => [
package/src/selectors.ts CHANGED
@@ -54,6 +54,7 @@ export interface State {
54
54
  editorSettings: Record< string, any > | null;
55
55
  editorAssets: Record< string, any > | null;
56
56
  syncConnectionStatuses?: Record< string, ConnectionStatus >;
57
+ collaborationSupported: boolean;
57
58
  }
58
59
 
59
60
  type EntityRecordKey = string | number;
package/src/sync.ts CHANGED
@@ -17,7 +17,7 @@ const {
17
17
  CRDT_DOC_META_PERSISTENCE_KEY,
18
18
  CRDT_RECORD_MAP_KEY,
19
19
  LOCAL_EDITOR_ORIGIN,
20
- WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
20
+ retrySyncConnection,
21
21
  } = unlock( syncPrivateApis );
22
22
 
23
23
  export {
@@ -25,7 +25,7 @@ export {
25
25
  CRDT_DOC_META_PERSISTENCE_KEY,
26
26
  CRDT_RECORD_MAP_KEY,
27
27
  LOCAL_EDITOR_ORIGIN,
28
- WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
28
+ retrySyncConnection,
29
29
  };
30
30
 
31
31
  let syncManager: SyncManager;
@@ -4,6 +4,10 @@
4
4
  import apiFetch from '@wordpress/api-fetch';
5
5
 
6
6
  jest.mock( '@wordpress/api-fetch' );
7
+ jest.mock( '../sync', () => ( {
8
+ ...jest.requireActual( '../sync' ),
9
+ getSyncManager: jest.fn(),
10
+ } ) );
7
11
 
8
12
  /**
9
13
  * Internal dependencies
@@ -14,6 +18,8 @@ import {
14
18
  prePersistPostType,
15
19
  additionalEntityConfigLoaders,
16
20
  } from '../entities';
21
+ import { getSyncManager } from '../sync';
22
+ import { POST_META_KEY_FOR_CRDT_DOC_PERSISTENCE } from '../utils/crdt';
17
23
 
18
24
  describe( 'getMethodName', () => {
19
25
  it( 'should return the right method name for an entity with the root kind', () => {
@@ -45,52 +51,79 @@ describe( 'getMethodName', () => {
45
51
  } );
46
52
 
47
53
  describe( 'prePersistPostType', () => {
48
- it( 'set the status to draft and empty the title when saving auto-draft posts', () => {
54
+ it( 'set the status to draft and empty the title when saving auto-draft posts', async () => {
49
55
  let record = {
50
56
  status: 'auto-draft',
51
57
  };
52
58
  const edits = {};
53
- expect( prePersistPostType( record, edits, 'post', false ) ).toEqual( {
59
+ expect(
60
+ await prePersistPostType( record, edits, 'post', false )
61
+ ).toEqual( {
54
62
  status: 'draft',
55
63
  title: '',
56
- meta: {},
57
64
  } );
58
65
 
59
66
  record = {
60
67
  status: 'publish',
61
68
  };
62
- expect( prePersistPostType( record, edits, 'post', false ) ).toEqual( {
63
- meta: {},
64
- } );
69
+ expect(
70
+ await prePersistPostType( record, edits, 'post', false )
71
+ ).toEqual( {} );
65
72
 
66
73
  record = {
67
74
  status: 'auto-draft',
68
75
  title: 'Auto Draft',
69
76
  };
70
- expect( prePersistPostType( record, edits, 'post', false ) ).toEqual( {
77
+ expect(
78
+ await prePersistPostType( record, edits, 'post', false )
79
+ ).toEqual( {
71
80
  status: 'draft',
72
81
  title: '',
73
- meta: {},
74
82
  } );
75
83
 
76
84
  record = {
77
85
  status: 'publish',
78
86
  title: 'My Title',
79
87
  };
80
- expect( prePersistPostType( record, edits, 'post', false ) ).toEqual( {
81
- meta: {},
82
- } );
88
+ expect(
89
+ await prePersistPostType( record, edits, 'post', false )
90
+ ).toEqual( {} );
83
91
  } );
84
92
 
85
- it( 'does not set the status to draft and empty the title when saving templates', () => {
93
+ it( 'does not set the status to draft and empty the title when saving templates', async () => {
86
94
  const record = {
87
95
  status: 'auto-draft',
88
96
  title: 'Auto Draft',
89
97
  };
90
98
  const edits = {};
91
- expect( prePersistPostType( record, edits, 'post', true ) ).toEqual( {
92
- meta: {},
99
+ expect(
100
+ await prePersistPostType( record, edits, 'post', true )
101
+ ).toEqual( {} );
102
+ } );
103
+
104
+ it( 'adds meta with serialized CRDT doc when createPersistedCRDTDoc returns a value', async () => {
105
+ const mockSerializedDoc = 'serialized-crdt-doc-data';
106
+ getSyncManager.mockReturnValue( {
107
+ createPersistedCRDTDoc: jest
108
+ .fn()
109
+ .mockReturnValue( mockSerializedDoc ),
93
110
  } );
111
+
112
+ const record = { id: 123, status: 'publish' };
113
+ const edits = {};
114
+ const result = await prePersistPostType( record, edits, 'post', false );
115
+
116
+ expect( result.meta ).toEqual( {
117
+ [ POST_META_KEY_FOR_CRDT_DOC_PERSISTENCE ]: mockSerializedDoc,
118
+ } );
119
+
120
+ expect( getSyncManager ).toHaveBeenCalled();
121
+ expect( getSyncManager().createPersistedCRDTDoc ).toHaveBeenCalledWith(
122
+ 'postType/post',
123
+ 123
124
+ );
125
+
126
+ getSyncManager.mockReset();
94
127
  } );
95
128
  } );
96
129