@wordpress/core-data 7.41.0 → 7.41.2-next.v.202603161435.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 (161) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +19 -0
  3. package/build/actions.cjs +25 -31
  4. package/build/actions.cjs.map +2 -2
  5. package/build/awareness/post-editor-awareness.cjs +34 -1
  6. package/build/awareness/post-editor-awareness.cjs.map +2 -2
  7. package/build/awareness/types.cjs.map +1 -1
  8. package/build/entities.cjs +3 -2
  9. package/build/entities.cjs.map +2 -2
  10. package/build/entity-provider.cjs +15 -6
  11. package/build/entity-provider.cjs.map +2 -2
  12. package/build/hooks/use-entity-prop.cjs +33 -2
  13. package/build/hooks/use-entity-prop.cjs.map +2 -2
  14. package/build/hooks/use-post-editor-awareness-state.cjs +83 -2
  15. package/build/hooks/use-post-editor-awareness-state.cjs.map +2 -2
  16. package/build/index.cjs +3 -0
  17. package/build/index.cjs.map +2 -2
  18. package/build/private-actions.cjs +1 -1
  19. package/build/private-actions.cjs.map +2 -2
  20. package/build/private-apis.cjs +3 -1
  21. package/build/private-apis.cjs.map +2 -2
  22. package/build/queried-data/actions.cjs +1 -1
  23. package/build/queried-data/actions.cjs.map +2 -2
  24. package/build/queried-data/reducer.cjs +19 -13
  25. package/build/queried-data/reducer.cjs.map +2 -2
  26. package/build/queried-data/selectors.cjs +7 -4
  27. package/build/queried-data/selectors.cjs.map +2 -2
  28. package/build/reducer.cjs +2 -1
  29. package/build/reducer.cjs.map +2 -2
  30. package/build/resolvers.cjs +114 -76
  31. package/build/resolvers.cjs.map +2 -2
  32. package/build/selectors.cjs +29 -0
  33. package/build/selectors.cjs.map +2 -2
  34. package/build/sync.cjs +3 -0
  35. package/build/sync.cjs.map +2 -2
  36. package/build/types.cjs +16 -0
  37. package/build/types.cjs.map +3 -3
  38. package/build/utils/crdt-blocks.cjs +22 -26
  39. package/build/utils/crdt-blocks.cjs.map +2 -2
  40. package/build/utils/crdt-user-selections.cjs +8 -5
  41. package/build/utils/crdt-user-selections.cjs.map +2 -2
  42. package/build/utils/crdt.cjs +1 -3
  43. package/build/utils/crdt.cjs.map +2 -2
  44. package/build/utils/index.cjs +3 -0
  45. package/build/utils/index.cjs.map +2 -2
  46. package/build/utils/normalize-query-for-resolution.cjs +35 -0
  47. package/build/utils/normalize-query-for-resolution.cjs.map +7 -0
  48. package/build/utils/user-permissions.cjs +1 -4
  49. package/build/utils/user-permissions.cjs.map +2 -2
  50. package/build-module/actions.mjs +30 -32
  51. package/build-module/actions.mjs.map +2 -2
  52. package/build-module/awareness/post-editor-awareness.mjs +34 -1
  53. package/build-module/awareness/post-editor-awareness.mjs.map +2 -2
  54. package/build-module/entities.mjs +3 -2
  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 +80 -1
  61. package/build-module/hooks/use-post-editor-awareness-state.mjs.map +2 -2
  62. package/build-module/index.mjs +2 -0
  63. package/build-module/index.mjs.map +2 -2
  64. package/build-module/private-actions.mjs +1 -1
  65. package/build-module/private-actions.mjs.map +2 -2
  66. package/build-module/private-apis.mjs +6 -2
  67. package/build-module/private-apis.mjs.map +2 -2
  68. package/build-module/queried-data/actions.mjs +1 -1
  69. package/build-module/queried-data/actions.mjs.map +2 -2
  70. package/build-module/queried-data/reducer.mjs +19 -13
  71. package/build-module/queried-data/reducer.mjs.map +2 -2
  72. package/build-module/queried-data/selectors.mjs +7 -4
  73. package/build-module/queried-data/selectors.mjs.map +2 -2
  74. package/build-module/reducer.mjs +2 -1
  75. package/build-module/reducer.mjs.map +2 -2
  76. package/build-module/resolvers.mjs +116 -77
  77. package/build-module/resolvers.mjs.map +2 -2
  78. package/build-module/selectors.mjs +28 -0
  79. package/build-module/selectors.mjs.map +2 -2
  80. package/build-module/sync.mjs +2 -0
  81. package/build-module/sync.mjs.map +2 -2
  82. package/build-module/types.mjs +9 -0
  83. package/build-module/types.mjs.map +4 -4
  84. package/build-module/utils/crdt-blocks.mjs +22 -26
  85. package/build-module/utils/crdt-blocks.mjs.map +2 -2
  86. package/build-module/utils/crdt-user-selections.mjs +8 -5
  87. package/build-module/utils/crdt-user-selections.mjs.map +2 -2
  88. package/build-module/utils/crdt.mjs +1 -3
  89. package/build-module/utils/crdt.mjs.map +2 -2
  90. package/build-module/utils/index.mjs +2 -0
  91. package/build-module/utils/index.mjs.map +2 -2
  92. package/build-module/utils/normalize-query-for-resolution.mjs +14 -0
  93. package/build-module/utils/normalize-query-for-resolution.mjs.map +7 -0
  94. package/build-module/utils/user-permissions.mjs +1 -4
  95. package/build-module/utils/user-permissions.mjs.map +2 -2
  96. package/build-types/actions.d.ts.map +1 -1
  97. package/build-types/awareness/post-editor-awareness.d.ts.map +1 -1
  98. package/build-types/awareness/types.d.ts +1 -1
  99. package/build-types/awareness/types.d.ts.map +1 -1
  100. package/build-types/entities.d.ts +1 -1
  101. package/build-types/entities.d.ts.map +1 -1
  102. package/build-types/entity-provider.d.ts +11 -6
  103. package/build-types/entity-provider.d.ts.map +1 -1
  104. package/build-types/hooks/use-entity-prop.d.ts.map +1 -1
  105. package/build-types/hooks/use-post-editor-awareness-state.d.ts +34 -10
  106. package/build-types/hooks/use-post-editor-awareness-state.d.ts.map +1 -1
  107. package/build-types/index.d.ts +2 -0
  108. package/build-types/index.d.ts.map +1 -1
  109. package/build-types/private-apis.d.ts.map +1 -1
  110. package/build-types/queried-data/reducer.d.ts.map +1 -1
  111. package/build-types/queried-data/selectors.d.ts.map +1 -1
  112. package/build-types/reducer.d.ts.map +1 -1
  113. package/build-types/resolvers.d.ts +2 -1
  114. package/build-types/resolvers.d.ts.map +1 -1
  115. package/build-types/selectors.d.ts +17 -0
  116. package/build-types/selectors.d.ts.map +1 -1
  117. package/build-types/sync.d.ts +2 -2
  118. package/build-types/sync.d.ts.map +1 -1
  119. package/build-types/types.d.ts +15 -0
  120. package/build-types/types.d.ts.map +1 -1
  121. package/build-types/utils/crdt-blocks.d.ts.map +1 -1
  122. package/build-types/utils/crdt-user-selections.d.ts +10 -5
  123. package/build-types/utils/crdt-user-selections.d.ts.map +1 -1
  124. package/build-types/utils/crdt.d.ts.map +1 -1
  125. package/build-types/utils/index.d.ts +1 -0
  126. package/build-types/utils/normalize-query-for-resolution.d.ts +12 -0
  127. package/build-types/utils/normalize-query-for-resolution.d.ts.map +1 -0
  128. package/build-types/utils/user-permissions.d.ts.map +1 -1
  129. package/package.json +18 -18
  130. package/src/actions.js +49 -50
  131. package/src/awareness/post-editor-awareness.ts +93 -1
  132. package/src/awareness/test/post-editor-awareness.ts +35 -0
  133. package/src/awareness/types.ts +1 -1
  134. package/src/entities.js +2 -1
  135. package/src/entity-provider.js +24 -11
  136. package/src/hooks/test/use-post-editor-awareness-state.ts +443 -0
  137. package/src/hooks/use-entity-prop.js +43 -3
  138. package/src/hooks/use-post-editor-awareness-state.ts +159 -7
  139. package/src/index.js +1 -0
  140. package/src/private-actions.js +1 -1
  141. package/src/private-apis.js +6 -2
  142. package/src/queried-data/actions.js +1 -1
  143. package/src/queried-data/reducer.js +26 -14
  144. package/src/queried-data/selectors.js +12 -5
  145. package/src/queried-data/test/selectors.js +25 -0
  146. package/src/reducer.js +4 -1
  147. package/src/resolvers.js +141 -91
  148. package/src/selectors.ts +56 -0
  149. package/src/sync.ts +2 -0
  150. package/src/test/private-actions.js +1 -1
  151. package/src/test/resolvers.js +88 -14
  152. package/src/test/selectors.js +150 -0
  153. package/src/test/store.js +182 -0
  154. package/src/types.ts +19 -0
  155. package/src/utils/crdt-blocks.ts +47 -54
  156. package/src/utils/crdt-user-selections.ts +28 -16
  157. package/src/utils/crdt.ts +2 -7
  158. package/src/utils/index.js +1 -0
  159. package/src/utils/normalize-query-for-resolution.js +23 -0
  160. package/src/utils/test/crdt-blocks.ts +42 -24
  161. package/src/utils/user-permissions.js +4 -5
package/src/resolvers.js CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  ALLOWED_RESOURCE_ACTIONS,
25
25
  RECEIVE_INTERMEDIATE_RESULTS,
26
26
  isNumericID,
27
+ normalizeQueryForResolution,
27
28
  } from './utils';
28
29
  import { fetchBlockPatterns } from './fetch';
29
30
  import { restoreSelection, getSelectionHistory } from './utils/crdt-selection';
@@ -229,18 +230,26 @@ export const getEntityRecord =
229
230
  query
230
231
  );
231
232
  },
232
- // Save the current entity record, whether or not it has unsaved
233
- // edits. This is used to trigger a persisted CRDT document.
234
- saveRecord: () => {
233
+ // Persist the CRDT document.
234
+ //
235
+ // TODO: Currently, persisted CRDT documents are stored in post meta.
236
+ // This effectively means that only post entities support CRDT
237
+ // persistence. As we add support for syncing additional entity,
238
+ // we'll need to revisit where persisted CRDT documents are stored.
239
+ persistCRDTDoc: () => {
235
240
  resolveSelect
236
241
  .getEditedEntityRecord( kind, name, key )
237
242
  .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 ) {
243
+ // Don't persist the CRDT document if the record is still an
244
+ // auto-draft or if the entity does not support meta.
245
+ const { meta, status } = editedRecord;
246
+ if ( 'auto-draft' === status || ! meta ) {
241
247
  return;
242
248
  }
243
249
 
250
+ // Trigger a save to persist the CRDT document. The entity's
251
+ // pre-persist hooks will create the persisted CRDT document
252
+ // and apply it to the record's meta.
244
253
  dispatch.saveEntityRecord(
245
254
  kind,
246
255
  name,
@@ -350,18 +359,14 @@ export const getEntityRecords =
350
359
  const key = entityConfig.key || DEFAULT_ENTITY_KEY;
351
360
 
352
361
  function getResolutionsArgs( records, recordsQuery ) {
353
- const queryArgs = Object.fromEntries(
354
- Object.entries( recordsQuery ).filter( ( [ k, v ] ) => {
355
- return [ 'context', '_fields' ].includes( k ) && !! v;
356
- } )
357
- );
362
+ const normalizedQuery = normalizeQueryForResolution( recordsQuery );
358
363
  return records
359
364
  .filter( ( record ) => record?.[ key ] )
360
365
  .map( ( record ) => [
361
366
  kind,
362
367
  name,
363
368
  record[ key ],
364
- Object.keys( queryArgs ).length > 0 ? queryArgs : undefined,
369
+ normalizedQuery,
365
370
  ] );
366
371
  }
367
372
 
@@ -991,9 +996,11 @@ export const getDefaultTemplateId =
991
996
  template.id = id;
992
997
  registry.batch( () => {
993
998
  dispatch.receiveDefaultTemplateId( query, id );
994
- dispatch.receiveEntityRecords( 'postType', template.type, [
995
- template,
996
- ] );
999
+ dispatch.receiveEntityRecords(
1000
+ 'postType',
1001
+ template.type,
1002
+ template
1003
+ );
997
1004
  // Avoid further network requests.
998
1005
  dispatch.finishResolution( 'getEntityRecord', [
999
1006
  'postType',
@@ -1034,78 +1041,87 @@ export const getRevisions =
1034
1041
  return;
1035
1042
  }
1036
1043
 
1037
- if ( query._fields ) {
1038
- // If requesting specific fields, items and query association to said
1039
- // records are stored by ID reference. Thus, fields must always include
1040
- // the ID.
1041
- query = {
1042
- ...query,
1043
- _fields: [
1044
- ...new Set( [
1045
- ...( getNormalizedCommaSeparable( query._fields ) ||
1046
- [] ),
1047
- entityConfig.revisionKey || DEFAULT_ENTITY_KEY,
1048
- ] ),
1049
- ].join(),
1050
- };
1051
- }
1052
-
1053
- const path = addQueryArgs(
1054
- entityConfig.getRevisionsUrl( recordKey ),
1055
- query
1044
+ const rawQuery = { ...query };
1045
+ const lock = await dispatch.__unstableAcquireStoreLock(
1046
+ STORE_NAME,
1047
+ [ 'entities', 'records', kind, name, recordKey, 'revisions' ],
1048
+ { exclusive: false }
1056
1049
  );
1057
1050
 
1058
- let records, response;
1059
- const meta = {};
1060
- const isPaginated =
1061
- entityConfig.supportsPagination && query.per_page !== -1;
1062
1051
  try {
1063
- response = await apiFetch( { path, parse: ! isPaginated } );
1064
- } catch ( error ) {
1065
- // Do nothing if our request comes back with an API error.
1066
- return;
1067
- }
1068
-
1069
- if ( response ) {
1070
- if ( isPaginated ) {
1071
- records = Object.values( await response.json() );
1072
- meta.totalItems = parseInt(
1073
- response.headers.get( 'X-WP-Total' )
1074
- );
1075
- } else {
1076
- records = Object.values( response );
1052
+ if ( query._fields ) {
1053
+ // If requesting specific fields, items and query association to said
1054
+ // records are stored by ID reference. Thus, fields must always include
1055
+ // the ID.
1056
+ query = {
1057
+ ...query,
1058
+ _fields: [
1059
+ ...new Set( [
1060
+ ...( getNormalizedCommaSeparable( query._fields ) ||
1061
+ [] ),
1062
+ entityConfig.revisionKey || DEFAULT_ENTITY_KEY,
1063
+ ] ),
1064
+ ].join(),
1065
+ };
1077
1066
  }
1078
1067
 
1079
- // If we request fields but the result doesn't contain the fields,
1080
- // explicitly set these fields as "undefined"
1081
- // that way we consider the query "fulfilled".
1082
- if ( query._fields ) {
1083
- records = records.map( ( record ) => {
1084
- query._fields.split( ',' ).forEach( ( field ) => {
1085
- if ( ! record.hasOwnProperty( field ) ) {
1086
- record[ field ] = undefined;
1087
- }
1088
- } );
1068
+ const path = addQueryArgs(
1069
+ entityConfig.getRevisionsUrl( recordKey ),
1070
+ query
1071
+ );
1089
1072
 
1090
- return record;
1091
- } );
1073
+ let records, response;
1074
+ const meta = {};
1075
+ const isPaginated =
1076
+ entityConfig.supportsPagination && query.per_page !== -1;
1077
+ try {
1078
+ response = await apiFetch( { path, parse: ! isPaginated } );
1079
+ } catch ( error ) {
1080
+ // Do nothing if our request comes back with an API error.
1081
+ return;
1092
1082
  }
1093
1083
 
1094
- registry.batch( () => {
1095
- dispatch.receiveRevisions(
1096
- kind,
1097
- name,
1098
- recordKey,
1099
- records,
1100
- query,
1101
- false,
1102
- meta
1103
- );
1084
+ if ( response ) {
1085
+ if ( isPaginated ) {
1086
+ records = Object.values( await response.json() );
1087
+ meta.totalItems = parseInt(
1088
+ response.headers.get( 'X-WP-Total' )
1089
+ );
1090
+ } else {
1091
+ records = Object.values( response );
1092
+ }
1104
1093
 
1105
- // When requesting all fields, the list of results can be used to
1106
- // resolve the `getRevision` selector in addition to `getRevisions`.
1107
- if ( ! query?._fields && ! query.context ) {
1094
+ // If we request fields but the result doesn't contain the fields,
1095
+ // explicitly set these fields as "undefined"
1096
+ // that way we consider the query "fulfilled".
1097
+ if ( query._fields ) {
1098
+ records = records.map( ( record ) => {
1099
+ query._fields.split( ',' ).forEach( ( field ) => {
1100
+ if ( ! record.hasOwnProperty( field ) ) {
1101
+ record[ field ] = undefined;
1102
+ }
1103
+ } );
1104
+
1105
+ return record;
1106
+ } );
1107
+ }
1108
+
1109
+ registry.batch( () => {
1110
+ dispatch.receiveRevisions(
1111
+ kind,
1112
+ name,
1113
+ recordKey,
1114
+ records,
1115
+ query,
1116
+ false,
1117
+ meta
1118
+ );
1119
+
1120
+ // Mark individual getRevision resolutions as done so that
1121
+ // subsequent getRevision calls skip redundant API fetches.
1108
1122
  const key = entityConfig.revisionKey || DEFAULT_ENTITY_KEY;
1123
+ const normalizedQuery =
1124
+ normalizeQueryForResolution( rawQuery );
1109
1125
  const resolutionsArgs = records
1110
1126
  .filter( ( record ) => record[ key ] )
1111
1127
  .map( ( record ) => [
@@ -1113,14 +1129,17 @@ export const getRevisions =
1113
1129
  name,
1114
1130
  recordKey,
1115
1131
  record[ key ],
1132
+ normalizedQuery,
1116
1133
  ] );
1117
1134
 
1118
1135
  dispatch.finishResolutions(
1119
1136
  'getRevision',
1120
1137
  resolutionsArgs
1121
1138
  );
1122
- }
1123
- } );
1139
+ } );
1140
+ }
1141
+ } finally {
1142
+ dispatch.__unstableReleaseStoreLock( lock );
1124
1143
  }
1125
1144
  };
1126
1145
 
@@ -1145,7 +1164,7 @@ getRevisions.shouldInvalidate = ( action, kind, name, recordKey ) =>
1145
1164
  */
1146
1165
  export const getRevision =
1147
1166
  ( kind, name, recordKey, revisionKey, query ) =>
1148
- async ( { dispatch, resolveSelect } ) => {
1167
+ async ( { select, dispatch, resolveSelect } ) => {
1149
1168
  const configs = await resolveSelect.getEntitiesConfig( kind );
1150
1169
  const entityConfig = configs.find(
1151
1170
  ( config ) => config.name === name && config.kind === kind
@@ -1170,21 +1189,52 @@ export const getRevision =
1170
1189
  ].join(),
1171
1190
  };
1172
1191
  }
1173
- const path = addQueryArgs(
1174
- entityConfig.getRevisionsUrl( recordKey, revisionKey ),
1175
- query
1192
+
1193
+ const lock = await dispatch.__unstableAcquireStoreLock(
1194
+ STORE_NAME,
1195
+ [
1196
+ 'entities',
1197
+ 'records',
1198
+ kind,
1199
+ name,
1200
+ recordKey,
1201
+ 'revisions',
1202
+ revisionKey,
1203
+ ],
1204
+ { exclusive: false }
1176
1205
  );
1177
1206
 
1178
- let record;
1179
1207
  try {
1180
- record = await apiFetch( { path } );
1181
- } catch ( error ) {
1182
- // Do nothing if our request comes back with an API error.
1183
- return;
1184
- }
1208
+ if (
1209
+ select.hasRevision( kind, name, recordKey, revisionKey, query )
1210
+ ) {
1211
+ return;
1212
+ }
1185
1213
 
1186
- if ( record ) {
1187
- dispatch.receiveRevisions( kind, name, recordKey, record, query );
1214
+ const path = addQueryArgs(
1215
+ entityConfig.getRevisionsUrl( recordKey, revisionKey ),
1216
+ query
1217
+ );
1218
+
1219
+ let record;
1220
+ try {
1221
+ record = await apiFetch( { path } );
1222
+ } catch ( error ) {
1223
+ // Do nothing if our request comes back with an API error.
1224
+ return;
1225
+ }
1226
+
1227
+ if ( record ) {
1228
+ dispatch.receiveRevisions(
1229
+ kind,
1230
+ name,
1231
+ recordKey,
1232
+ record,
1233
+ query
1234
+ );
1235
+ }
1236
+ } finally {
1237
+ dispatch.__unstableReleaseStoreLock( lock );
1188
1238
  }
1189
1239
  };
1190
1240
 
package/src/selectors.ts CHANGED
@@ -1524,6 +1524,62 @@ export const getRevisions = (
1524
1524
  return getQueriedItems( queriedStateRevisions, query );
1525
1525
  };
1526
1526
 
1527
+ /**
1528
+ * Returns true if a revision has been received for the given set of parameters,
1529
+ * or false otherwise.
1530
+ *
1531
+ * Note: This does not trigger a request for the revision from the API
1532
+ * if it's not available in the local state.
1533
+ *
1534
+ * @param state State tree
1535
+ * @param kind Entity kind.
1536
+ * @param name Entity name.
1537
+ * @param recordKey The key of the entity record whose revision you want to check.
1538
+ * @param revisionKey The revision's key.
1539
+ * @param query Optional query.
1540
+ *
1541
+ * @return Whether a revision has been received.
1542
+ */
1543
+ export function hasRevision(
1544
+ state: State,
1545
+ kind: string,
1546
+ name: string,
1547
+ recordKey: EntityRecordKey,
1548
+ revisionKey: EntityRecordKey,
1549
+ query?: GetRecordsHttpQuery
1550
+ ): boolean {
1551
+ const queriedState =
1552
+ state.entities.records?.[ kind ]?.[ name ]?.revisions?.[ recordKey ];
1553
+ if ( ! queriedState ) {
1554
+ return false;
1555
+ }
1556
+ const context = query?.context ?? 'default';
1557
+
1558
+ if ( ! query || ! query._fields ) {
1559
+ return !! queriedState.itemIsComplete[ context ]?.[ revisionKey ];
1560
+ }
1561
+
1562
+ const item = queriedState.items[ context ]?.[ revisionKey ];
1563
+ if ( ! item ) {
1564
+ return false;
1565
+ }
1566
+
1567
+ const fields = getNormalizedCommaSeparable( query._fields ) ?? [];
1568
+ for ( let i = 0; i < fields.length; i++ ) {
1569
+ const path = fields[ i ].split( '.' );
1570
+ let value = item;
1571
+ for ( let p = 0; p < path.length; p++ ) {
1572
+ const part = path[ p ];
1573
+ if ( ! value || ! Object.hasOwn( value, part ) ) {
1574
+ return false;
1575
+ }
1576
+ value = value[ part ];
1577
+ }
1578
+ }
1579
+
1580
+ return true;
1581
+ }
1582
+
1527
1583
  /**
1528
1584
  * Returns a single, specific revision of a parent entity.
1529
1585
  *
package/src/sync.ts CHANGED
@@ -17,6 +17,7 @@ const {
17
17
  CRDT_DOC_META_PERSISTENCE_KEY,
18
18
  CRDT_RECORD_MAP_KEY,
19
19
  LOCAL_EDITOR_ORIGIN,
20
+ LOCAL_UNDO_IGNORED_ORIGIN,
20
21
  retrySyncConnection,
21
22
  } = unlock( syncPrivateApis );
22
23
 
@@ -25,6 +26,7 @@ export {
25
26
  CRDT_DOC_META_PERSISTENCE_KEY,
26
27
  CRDT_RECORD_MAP_KEY,
27
28
  LOCAL_EDITOR_ORIGIN,
29
+ LOCAL_UNDO_IGNORED_ORIGIN,
28
30
  retrySyncConnection,
29
31
  };
30
32
 
@@ -102,7 +102,7 @@ describe( 'editMediaEntity', () => {
102
102
  expect( dispatch.receiveEntityRecords ).toHaveBeenCalledWith(
103
103
  'postType',
104
104
  'attachment',
105
- [ updatedRecord ],
105
+ updatedRecord,
106
106
  undefined,
107
107
  true,
108
108
  undefined,
@@ -172,16 +172,69 @@ describe( 'getEntityRecord', () => {
172
172
  editRecord: expect.any( Function ),
173
173
  getEditedRecord: expect.any( Function ),
174
174
  onStatusChange: expect.any( Function ),
175
+ persistCRDTDoc: expect.any( Function ),
175
176
  refetchRecord: expect.any( Function ),
176
177
  restoreUndoMeta: expect.any( Function ),
177
- saveRecord: expect.any( Function ),
178
178
  }
179
179
  );
180
180
  } );
181
181
 
182
- it( 'saveRecord fetches edited record and saves full entity record', async () => {
183
- const POST_RECORD = { id: 1, title: 'Test Post' };
184
- const EDITED_RECORD = { id: 1, title: 'Edited Post' };
182
+ it( 'persistCRDTDoc fetches edited record and does not save full entity record when the entity does not support meta', async () => {
183
+ const ENTITY_RECORD = { id: 1, title: 'Test Record' };
184
+ const EDITED_RECORD = { id: 1, title: 'Edited Record' };
185
+ const ENTITY_RESPONSE = {
186
+ json: () => Promise.resolve( ENTITY_RECORD ),
187
+ };
188
+ const ENTITIES_WITH_SYNC = [
189
+ {
190
+ name: 'bar',
191
+ kind: 'foo',
192
+ baseURL: '/wp/v2/foo',
193
+ baseURLParams: { context: 'edit' },
194
+ syncConfig: {},
195
+ },
196
+ ];
197
+
198
+ dispatch.saveEntityRecord = jest.fn();
199
+
200
+ const resolveSelectWithSync = {
201
+ getEntitiesConfig: jest.fn( () => ENTITIES_WITH_SYNC ),
202
+ getEditedEntityRecord: jest.fn( () =>
203
+ Promise.resolve( EDITED_RECORD )
204
+ ),
205
+ };
206
+
207
+ triggerFetch.mockImplementation( () => ENTITY_RESPONSE );
208
+
209
+ await getEntityRecord(
210
+ 'foo',
211
+ 'bar',
212
+ 1
213
+ )( {
214
+ dispatch,
215
+ registry,
216
+ resolveSelect: resolveSelectWithSync,
217
+ } );
218
+
219
+ // Extract the handlers passed to syncManager.load.
220
+ const handlers = syncManager.load.mock.calls[ 0 ][ 4 ];
221
+
222
+ // Call persistCRDTDoc and wait for the internal promise chain.
223
+ handlers.persistCRDTDoc();
224
+ await resolveSelectWithSync.getEditedEntityRecord();
225
+
226
+ // Should have fetched the full edited entity record.
227
+ expect(
228
+ resolveSelectWithSync.getEditedEntityRecord
229
+ ).toHaveBeenCalledWith( 'foo', 'bar', 1 );
230
+
231
+ // Should not have called saveEntityRecord.
232
+ expect( dispatch.saveEntityRecord ).not.toHaveBeenCalled();
233
+ } );
234
+
235
+ it( 'persistCRDTDoc fetches edited record and saves full entity record', async () => {
236
+ const POST_RECORD = { id: 1, title: 'Test Post', meta: {} };
237
+ const EDITED_RECORD = { id: 1, title: 'Edited Post', meta: {} };
185
238
  const POST_RESPONSE = {
186
239
  json: () => Promise.resolve( POST_RECORD ),
187
240
  };
@@ -219,8 +272,8 @@ describe( 'getEntityRecord', () => {
219
272
  // Extract the handlers passed to syncManager.load.
220
273
  const handlers = syncManager.load.mock.calls[ 0 ][ 4 ];
221
274
 
222
- // Call saveRecord and wait for the internal promise chain.
223
- handlers.saveRecord();
275
+ // Call persistCRDTDoc and wait for the internal promise chain.
276
+ handlers.persistCRDTDoc();
224
277
  await resolveSelectWithSync.getEditedEntityRecord();
225
278
 
226
279
  // Should have fetched the full edited entity record.
@@ -236,8 +289,8 @@ describe( 'getEntityRecord', () => {
236
289
  );
237
290
  } );
238
291
 
239
- it( 'saveRecord saves even when there are no unsaved edits', async () => {
240
- const POST_RECORD = { id: 1, title: 'Test Post' };
292
+ it( 'persistCRDTDoc saves even when there are no unsaved edits', async () => {
293
+ const POST_RECORD = { id: 1, title: 'Test Post', meta: {} };
241
294
  const POST_RESPONSE = {
242
295
  json: () => Promise.resolve( POST_RECORD ),
243
296
  };
@@ -275,8 +328,8 @@ describe( 'getEntityRecord', () => {
275
328
 
276
329
  const handlers = syncManager.load.mock.calls[ 0 ][ 4 ];
277
330
 
278
- // Call saveRecord and wait for the internal promise chain.
279
- handlers.saveRecord();
331
+ // Call persistCRDTDoc and wait for the internal promise chain.
332
+ handlers.persistCRDTDoc();
280
333
  await resolveSelectWithSync.getEditedEntityRecord();
281
334
 
282
335
  // Should save the record even with no edits (the whole point of the fix).
@@ -336,9 +389,9 @@ describe( 'getEntityRecord', () => {
336
389
  editRecord: expect.any( Function ),
337
390
  getEditedRecord: expect.any( Function ),
338
391
  onStatusChange: expect.any( Function ),
392
+ persistCRDTDoc: expect.any( Function ),
339
393
  refetchRecord: expect.any( Function ),
340
394
  restoreUndoMeta: expect.any( Function ),
341
- saveRecord: expect.any( Function ),
342
395
  }
343
396
  );
344
397
  } );
@@ -862,6 +915,24 @@ describe( 'canUser', () => {
862
915
  expect( dispatch.receiveUserPermissions ).not.toHaveBeenCalled();
863
916
  } );
864
917
 
918
+ it( 'receives false when the allow header is missing', async () => {
919
+ triggerFetch.mockImplementation( () => ( {
920
+ headers: new Map(),
921
+ } ) );
922
+
923
+ await canUser(
924
+ 'create',
925
+ 'media'
926
+ )( { dispatch, registry, resolveSelect } );
927
+
928
+ expect( dispatch.receiveUserPermissions ).toHaveBeenCalledWith( {
929
+ 'create/media': false,
930
+ 'read/media': false,
931
+ 'update/media': false,
932
+ 'delete/media': false,
933
+ } );
934
+ } );
935
+
865
936
  it( 'throws an error when an entity resource object is malformed', async () => {
866
937
  await expect(
867
938
  canUser( 'create', { name: 'wp_block' } )( {
@@ -888,9 +959,12 @@ describe( 'canUser', () => {
888
959
  parse: false,
889
960
  } );
890
961
 
891
- expect( dispatch.receiveUserPermissions ).toHaveBeenCalledWith(
892
- expect.objectContaining( { 'create/media': false } )
893
- );
962
+ expect( dispatch.receiveUserPermissions ).toHaveBeenCalledWith( {
963
+ 'create/media': false,
964
+ 'read/media': true,
965
+ 'update/media': false,
966
+ 'delete/media': false,
967
+ } );
894
968
  } );
895
969
 
896
970
  it( 'receives false when the user is not allowed to perform an action on entities', async () => {