@wordpress/core-data 7.51.0 → 7.53.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 (494) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +1 -0
  3. package/build/actions.cjs +64 -2
  4. package/build/actions.cjs.map +3 -3
  5. package/build/awareness/awareness-state.cjs.map +2 -2
  6. package/build/awareness/base-awareness.cjs.map +2 -2
  7. package/build/awareness/block-lookup.cjs.map +2 -2
  8. package/build/awareness/post-editor-awareness.cjs +7 -0
  9. package/build/awareness/post-editor-awareness.cjs.map +2 -2
  10. package/build/awareness/typed-awareness.cjs.map +2 -2
  11. package/build/awareness/types.cjs.map +1 -1
  12. package/build/awareness/utils.cjs.map +2 -2
  13. package/build/batch/create-batch.cjs.map +2 -2
  14. package/build/batch/default-processor.cjs.map +2 -2
  15. package/build/components/entities-saved-states/entity-record-item.cjs +67 -0
  16. package/build/components/entities-saved-states/entity-record-item.cjs.map +7 -0
  17. package/build/components/entities-saved-states/entity-type-list.cjs +135 -0
  18. package/build/components/entities-saved-states/entity-type-list.cjs.map +7 -0
  19. package/build/components/entities-saved-states/hooks/use-is-dirty.cjs +101 -0
  20. package/build/components/entities-saved-states/hooks/use-is-dirty.cjs.map +7 -0
  21. package/build/components/entities-saved-states/index.cjs +215 -0
  22. package/build/components/entities-saved-states/index.cjs.map +7 -0
  23. package/build/dynamic-entities.cjs.map +2 -2
  24. package/build/entities.cjs +2 -0
  25. package/build/entities.cjs.map +2 -2
  26. package/build/entity-context.cjs.map +2 -2
  27. package/build/entity-provider.cjs.map +2 -2
  28. package/build/entity-types/attachment.cjs.map +1 -1
  29. package/build/entity-types/base.cjs.map +1 -1
  30. package/build/entity-types/comment.cjs.map +1 -1
  31. package/build/entity-types/font-collection.cjs.map +1 -1
  32. package/build/entity-types/font-family.cjs.map +1 -1
  33. package/build/entity-types/global-styles-revision.cjs.map +1 -1
  34. package/build/entity-types/helpers.cjs.map +1 -1
  35. package/build/entity-types/icon.cjs.map +1 -1
  36. package/build/entity-types/index.cjs.map +1 -1
  37. package/build/entity-types/menu-location.cjs.map +1 -1
  38. package/build/entity-types/nav-menu-item.cjs.map +1 -1
  39. package/build/entity-types/nav-menu.cjs.map +1 -1
  40. package/build/entity-types/page.cjs.map +1 -1
  41. package/build/entity-types/plugin.cjs.map +1 -1
  42. package/build/entity-types/post-revision.cjs.map +1 -1
  43. package/build/entity-types/post-status.cjs.map +1 -1
  44. package/build/entity-types/post.cjs.map +1 -1
  45. package/build/entity-types/settings.cjs.map +1 -1
  46. package/build/entity-types/sidebar.cjs.map +1 -1
  47. package/build/entity-types/taxonomy.cjs.map +1 -1
  48. package/build/entity-types/term.cjs.map +1 -1
  49. package/build/entity-types/theme.cjs.map +1 -1
  50. package/build/entity-types/type.cjs.map +1 -1
  51. package/build/entity-types/user.cjs.map +1 -1
  52. package/build/entity-types/widget-type.cjs.map +1 -1
  53. package/build/entity-types/widget.cjs.map +1 -1
  54. package/build/entity-types/wp-template-part.cjs.map +1 -1
  55. package/build/entity-types/wp-template.cjs.map +1 -1
  56. package/build/fetch/__experimental-fetch-link-suggestions.cjs.map +2 -2
  57. package/build/fetch/__experimental-fetch-url-data.cjs.map +2 -2
  58. package/build/fetch/index.cjs.map +2 -2
  59. package/build/footnotes/get-footnotes-order.cjs.map +2 -2
  60. package/build/footnotes/get-rich-text-values-cached.cjs.map +2 -2
  61. package/build/footnotes/index.cjs +12 -1
  62. package/build/footnotes/index.cjs.map +3 -3
  63. package/build/hooks/index.cjs.map +2 -2
  64. package/build/hooks/use-entity-block-editor.cjs.map +2 -2
  65. package/build/hooks/use-entity-id.cjs.map +2 -2
  66. package/build/hooks/use-entity-prop.cjs.map +2 -2
  67. package/build/hooks/use-entity-record.cjs.map +2 -2
  68. package/build/hooks/use-entity-records.cjs +2 -2
  69. package/build/hooks/use-entity-records.cjs.map +2 -2
  70. package/build/hooks/use-post-editor-awareness-state.cjs.map +2 -2
  71. package/build/hooks/use-query-select.cjs.map +2 -2
  72. package/build/hooks/use-resource-permissions.cjs.map +2 -2
  73. package/build/hooks/utils.cjs.map +2 -2
  74. package/build/index.cjs.map +2 -2
  75. package/build/lock-unlock.cjs.map +2 -2
  76. package/build/locks/actions.cjs.map +2 -2
  77. package/build/locks/engine.cjs.map +2 -2
  78. package/build/locks/reducer.cjs.map +2 -2
  79. package/build/locks/selectors.cjs.map +2 -2
  80. package/build/private-actions.cjs +132 -0
  81. package/build/private-actions.cjs.map +3 -3
  82. package/build/private-apis.cjs +22 -0
  83. package/build/private-apis.cjs.map +3 -3
  84. package/build/private-selectors.cjs +5 -0
  85. package/build/private-selectors.cjs.map +2 -2
  86. package/build/queried-data/get-query-parts.cjs.map +2 -2
  87. package/build/queried-data/reducer.cjs.map +2 -2
  88. package/build/queried-data/selectors.cjs.map +2 -2
  89. package/build/reducer.cjs.map +2 -2
  90. package/build/resolvers.cjs +21 -26
  91. package/build/resolvers.cjs.map +2 -2
  92. package/build/selectors.cjs.map +2 -2
  93. package/build/sync.cjs +26 -0
  94. package/build/sync.cjs.map +2 -2
  95. package/build/types.cjs.map +1 -1
  96. package/build/utils/block-selection-history.cjs.map +2 -2
  97. package/build/utils/clear-unchanged-edits.cjs.map +2 -2
  98. package/build/utils/conservative-map-item.cjs.map +2 -2
  99. package/build/utils/crdt-blocks.cjs.map +2 -2
  100. package/build/utils/crdt-selection.cjs.map +2 -2
  101. package/build/utils/crdt-text.cjs.map +2 -2
  102. package/build/utils/crdt-user-selections.cjs +43 -16
  103. package/build/utils/crdt-user-selections.cjs.map +3 -3
  104. package/build/utils/crdt-utils.cjs.map +2 -2
  105. package/build/utils/crdt.cjs +4 -2
  106. package/build/utils/crdt.cjs.map +2 -2
  107. package/build/utils/get-template-info.cjs +53 -0
  108. package/build/utils/get-template-info.cjs.map +7 -0
  109. package/build/utils/get-template-part-icon.cjs +43 -0
  110. package/build/utils/get-template-part-icon.cjs.map +7 -0
  111. package/build/utils/log-entity-deprecation.cjs.map +2 -2
  112. package/build/utils/save-crdt-doc.cjs.map +2 -2
  113. package/build-module/actions.mjs +68 -2
  114. package/build-module/actions.mjs.map +2 -2
  115. package/build-module/awareness/awareness-state.mjs.map +2 -2
  116. package/build-module/awareness/base-awareness.mjs.map +2 -2
  117. package/build-module/awareness/block-lookup.mjs.map +2 -2
  118. package/build-module/awareness/post-editor-awareness.mjs +7 -0
  119. package/build-module/awareness/post-editor-awareness.mjs.map +2 -2
  120. package/build-module/awareness/typed-awareness.mjs.map +2 -2
  121. package/build-module/awareness/utils.mjs.map +2 -2
  122. package/build-module/batch/create-batch.mjs.map +2 -2
  123. package/build-module/batch/default-processor.mjs.map +2 -2
  124. package/build-module/components/entities-saved-states/entity-record-item.mjs +46 -0
  125. package/build-module/components/entities-saved-states/entity-record-item.mjs.map +7 -0
  126. package/build-module/components/entities-saved-states/entity-type-list.mjs +104 -0
  127. package/build-module/components/entities-saved-states/entity-type-list.mjs.map +7 -0
  128. package/build-module/components/entities-saved-states/hooks/use-is-dirty.mjs +76 -0
  129. package/build-module/components/entities-saved-states/hooks/use-is-dirty.mjs.map +7 -0
  130. package/build-module/components/entities-saved-states/index.mjs +187 -0
  131. package/build-module/components/entities-saved-states/index.mjs.map +7 -0
  132. package/build-module/dynamic-entities.mjs.map +2 -2
  133. package/build-module/entities.mjs +2 -0
  134. package/build-module/entities.mjs.map +2 -2
  135. package/build-module/entity-context.mjs.map +2 -2
  136. package/build-module/entity-provider.mjs.map +2 -2
  137. package/build-module/fetch/__experimental-fetch-link-suggestions.mjs.map +2 -2
  138. package/build-module/fetch/__experimental-fetch-url-data.mjs.map +2 -2
  139. package/build-module/fetch/index.mjs.map +2 -2
  140. package/build-module/footnotes/get-footnotes-order.mjs.map +2 -2
  141. package/build-module/footnotes/get-rich-text-values-cached.mjs.map +2 -2
  142. package/build-module/footnotes/index.mjs +12 -1
  143. package/build-module/footnotes/index.mjs.map +2 -2
  144. package/build-module/hooks/index.mjs.map +2 -2
  145. package/build-module/hooks/use-entity-block-editor.mjs.map +2 -2
  146. package/build-module/hooks/use-entity-id.mjs.map +2 -2
  147. package/build-module/hooks/use-entity-prop.mjs.map +2 -2
  148. package/build-module/hooks/use-entity-record.mjs.map +2 -2
  149. package/build-module/hooks/use-entity-records.mjs +2 -2
  150. package/build-module/hooks/use-entity-records.mjs.map +2 -2
  151. package/build-module/hooks/use-post-editor-awareness-state.mjs.map +2 -2
  152. package/build-module/hooks/use-query-select.mjs.map +2 -2
  153. package/build-module/hooks/use-resource-permissions.mjs.map +2 -2
  154. package/build-module/hooks/utils.mjs.map +2 -2
  155. package/build-module/index.mjs.map +2 -2
  156. package/build-module/lock-unlock.mjs.map +2 -2
  157. package/build-module/locks/actions.mjs.map +2 -2
  158. package/build-module/locks/engine.mjs.map +2 -2
  159. package/build-module/locks/reducer.mjs.map +2 -2
  160. package/build-module/locks/selectors.mjs.map +2 -2
  161. package/build-module/private-actions.mjs +131 -0
  162. package/build-module/private-actions.mjs.map +2 -2
  163. package/build-module/private-apis.mjs +21 -1
  164. package/build-module/private-apis.mjs.map +2 -2
  165. package/build-module/private-selectors.mjs +4 -0
  166. package/build-module/private-selectors.mjs.map +2 -2
  167. package/build-module/queried-data/get-query-parts.mjs.map +2 -2
  168. package/build-module/queried-data/reducer.mjs.map +2 -2
  169. package/build-module/queried-data/selectors.mjs.map +2 -2
  170. package/build-module/reducer.mjs.map +2 -2
  171. package/build-module/resolvers.mjs +21 -26
  172. package/build-module/resolvers.mjs.map +2 -2
  173. package/build-module/selectors.mjs.map +2 -2
  174. package/build-module/sync.mjs +23 -0
  175. package/build-module/sync.mjs.map +2 -2
  176. package/build-module/utils/block-selection-history.mjs.map +2 -2
  177. package/build-module/utils/clear-unchanged-edits.mjs.map +2 -2
  178. package/build-module/utils/conservative-map-item.mjs.map +2 -2
  179. package/build-module/utils/crdt-blocks.mjs.map +2 -2
  180. package/build-module/utils/crdt-selection.mjs.map +2 -2
  181. package/build-module/utils/crdt-text.mjs.map +2 -2
  182. package/build-module/utils/crdt-user-selections.mjs +43 -16
  183. package/build-module/utils/crdt-user-selections.mjs.map +3 -3
  184. package/build-module/utils/crdt-utils.mjs.map +2 -2
  185. package/build-module/utils/crdt.mjs +2 -1
  186. package/build-module/utils/crdt.mjs.map +2 -2
  187. package/build-module/utils/get-template-info.mjs +28 -0
  188. package/build-module/utils/get-template-info.mjs.map +7 -0
  189. package/build-module/utils/get-template-part-icon.mjs +24 -0
  190. package/build-module/utils/get-template-part-icon.mjs.map +7 -0
  191. package/build-module/utils/log-entity-deprecation.mjs.map +2 -2
  192. package/build-module/utils/save-crdt-doc.mjs.map +2 -2
  193. package/build-types/actions.d.ts +4 -0
  194. package/build-types/actions.d.ts.map +1 -1
  195. package/build-types/awareness/awareness-state.d.ts.map +1 -1
  196. package/build-types/awareness/base-awareness.d.ts +0 -3
  197. package/build-types/awareness/base-awareness.d.ts.map +1 -1
  198. package/build-types/awareness/block-lookup.d.ts +0 -3
  199. package/build-types/awareness/block-lookup.d.ts.map +1 -1
  200. package/build-types/awareness/post-editor-awareness.d.ts +0 -3
  201. package/build-types/awareness/post-editor-awareness.d.ts.map +1 -1
  202. package/build-types/awareness/typed-awareness.d.ts +0 -3
  203. package/build-types/awareness/typed-awareness.d.ts.map +1 -1
  204. package/build-types/awareness/types.d.ts +0 -6
  205. package/build-types/awareness/types.d.ts.map +1 -1
  206. package/build-types/awareness/utils.d.ts +0 -3
  207. package/build-types/awareness/utils.d.ts.map +1 -1
  208. package/build-types/batch/create-batch.d.ts.map +1 -1
  209. package/build-types/batch/default-processor.d.ts.map +1 -1
  210. package/build-types/components/entities-saved-states/entity-record-item.d.ts +6 -0
  211. package/build-types/components/entities-saved-states/entity-record-item.d.ts.map +1 -0
  212. package/build-types/components/entities-saved-states/entity-type-list.d.ts +6 -0
  213. package/build-types/components/entities-saved-states/entity-type-list.d.ts.map +1 -0
  214. package/build-types/components/entities-saved-states/hooks/use-is-dirty.d.ts +11 -0
  215. package/build-types/components/entities-saved-states/hooks/use-is-dirty.d.ts.map +1 -0
  216. package/build-types/components/entities-saved-states/index.d.ts +49 -0
  217. package/build-types/components/entities-saved-states/index.d.ts.map +1 -0
  218. package/build-types/dynamic-entities.d.ts +0 -3
  219. package/build-types/dynamic-entities.d.ts.map +1 -1
  220. package/build-types/entities.d.ts.map +1 -1
  221. package/build-types/entity-context.d.ts.map +1 -1
  222. package/build-types/entity-provider.d.ts.map +1 -1
  223. package/build-types/entity-types/attachment.d.ts +0 -3
  224. package/build-types/entity-types/attachment.d.ts.map +1 -1
  225. package/build-types/entity-types/base.d.ts +0 -3
  226. package/build-types/entity-types/base.d.ts.map +1 -1
  227. package/build-types/entity-types/comment.d.ts +0 -3
  228. package/build-types/entity-types/comment.d.ts.map +1 -1
  229. package/build-types/entity-types/font-collection.d.ts +0 -3
  230. package/build-types/entity-types/font-collection.d.ts.map +1 -1
  231. package/build-types/entity-types/font-family.d.ts +0 -3
  232. package/build-types/entity-types/font-family.d.ts.map +1 -1
  233. package/build-types/entity-types/global-styles-revision.d.ts +0 -3
  234. package/build-types/entity-types/global-styles-revision.d.ts.map +1 -1
  235. package/build-types/entity-types/helpers.d.ts +0 -3
  236. package/build-types/entity-types/helpers.d.ts.map +1 -1
  237. package/build-types/entity-types/icon.d.ts +0 -3
  238. package/build-types/entity-types/icon.d.ts.map +1 -1
  239. package/build-types/entity-types/index.d.ts +0 -3
  240. package/build-types/entity-types/index.d.ts.map +1 -1
  241. package/build-types/entity-types/menu-location.d.ts +0 -3
  242. package/build-types/entity-types/menu-location.d.ts.map +1 -1
  243. package/build-types/entity-types/nav-menu-item.d.ts +0 -3
  244. package/build-types/entity-types/nav-menu-item.d.ts.map +1 -1
  245. package/build-types/entity-types/nav-menu.d.ts +0 -3
  246. package/build-types/entity-types/nav-menu.d.ts.map +1 -1
  247. package/build-types/entity-types/page.d.ts +0 -3
  248. package/build-types/entity-types/page.d.ts.map +1 -1
  249. package/build-types/entity-types/plugin.d.ts +0 -3
  250. package/build-types/entity-types/plugin.d.ts.map +1 -1
  251. package/build-types/entity-types/post-revision.d.ts +0 -3
  252. package/build-types/entity-types/post-revision.d.ts.map +1 -1
  253. package/build-types/entity-types/post-status.d.ts +0 -3
  254. package/build-types/entity-types/post-status.d.ts.map +1 -1
  255. package/build-types/entity-types/post.d.ts +0 -3
  256. package/build-types/entity-types/post.d.ts.map +1 -1
  257. package/build-types/entity-types/settings.d.ts +0 -3
  258. package/build-types/entity-types/settings.d.ts.map +1 -1
  259. package/build-types/entity-types/sidebar.d.ts +0 -3
  260. package/build-types/entity-types/sidebar.d.ts.map +1 -1
  261. package/build-types/entity-types/taxonomy.d.ts +0 -3
  262. package/build-types/entity-types/taxonomy.d.ts.map +1 -1
  263. package/build-types/entity-types/term.d.ts +0 -3
  264. package/build-types/entity-types/term.d.ts.map +1 -1
  265. package/build-types/entity-types/test/attachment.test.d.ts +0 -8
  266. package/build-types/entity-types/test/attachment.test.d.ts.map +1 -1
  267. package/build-types/entity-types/theme.d.ts +0 -3
  268. package/build-types/entity-types/theme.d.ts.map +1 -1
  269. package/build-types/entity-types/type.d.ts +0 -3
  270. package/build-types/entity-types/type.d.ts.map +1 -1
  271. package/build-types/entity-types/user.d.ts +0 -3
  272. package/build-types/entity-types/user.d.ts.map +1 -1
  273. package/build-types/entity-types/widget-type.d.ts +0 -3
  274. package/build-types/entity-types/widget-type.d.ts.map +1 -1
  275. package/build-types/entity-types/widget.d.ts +0 -3
  276. package/build-types/entity-types/widget.d.ts.map +1 -1
  277. package/build-types/entity-types/wp-template-part.d.ts +0 -3
  278. package/build-types/entity-types/wp-template-part.d.ts.map +1 -1
  279. package/build-types/entity-types/wp-template.d.ts +0 -3
  280. package/build-types/entity-types/wp-template.d.ts.map +1 -1
  281. package/build-types/fetch/__experimental-fetch-link-suggestions.d.ts.map +1 -1
  282. package/build-types/fetch/__experimental-fetch-url-data.d.ts.map +1 -1
  283. package/build-types/fetch/index.d.ts.map +1 -1
  284. package/build-types/footnotes/get-footnotes-order.d.ts.map +1 -1
  285. package/build-types/footnotes/get-rich-text-values-cached.d.ts.map +1 -1
  286. package/build-types/footnotes/index.d.ts.map +1 -1
  287. package/build-types/hooks/index.d.ts +0 -3
  288. package/build-types/hooks/index.d.ts.map +1 -1
  289. package/build-types/hooks/use-entity-block-editor.d.ts.map +1 -1
  290. package/build-types/hooks/use-entity-id.d.ts.map +1 -1
  291. package/build-types/hooks/use-entity-prop.d.ts.map +1 -1
  292. package/build-types/hooks/use-entity-record.d.ts.map +1 -1
  293. package/build-types/hooks/use-entity-records.d.ts.map +1 -1
  294. package/build-types/hooks/use-post-editor-awareness-state.d.ts.map +1 -1
  295. package/build-types/hooks/use-query-select.d.ts.map +1 -1
  296. package/build-types/hooks/use-resource-permissions.d.ts.map +1 -1
  297. package/build-types/hooks/utils.d.ts +0 -6
  298. package/build-types/hooks/utils.d.ts.map +1 -1
  299. package/build-types/index.d.ts +1 -0
  300. package/build-types/index.d.ts.map +1 -1
  301. package/build-types/lock-unlock.d.ts.map +1 -1
  302. package/build-types/locks/actions.d.ts.map +1 -1
  303. package/build-types/locks/engine.d.ts.map +1 -1
  304. package/build-types/locks/reducer.d.ts.map +1 -1
  305. package/build-types/locks/selectors.d.ts.map +1 -1
  306. package/build-types/private-actions.d.ts +19 -0
  307. package/build-types/private-actions.d.ts.map +1 -1
  308. package/build-types/private-apis.d.ts +13 -3
  309. package/build-types/private-apis.d.ts.map +1 -1
  310. package/build-types/private-selectors.d.ts +12 -3
  311. package/build-types/private-selectors.d.ts.map +1 -1
  312. package/build-types/queried-data/get-query-parts.d.ts.map +1 -1
  313. package/build-types/queried-data/reducer.d.ts.map +1 -1
  314. package/build-types/queried-data/selectors.d.ts.map +1 -1
  315. package/build-types/reducer.d.ts.map +1 -1
  316. package/build-types/resolvers.d.ts +1 -2
  317. package/build-types/resolvers.d.ts.map +1 -1
  318. package/build-types/selectors.d.ts.map +1 -1
  319. package/build-types/sync.d.ts +31 -3
  320. package/build-types/sync.d.ts.map +1 -1
  321. package/build-types/types.d.ts +9 -8
  322. package/build-types/types.d.ts.map +1 -1
  323. package/build-types/utils/block-selection-history.d.ts +0 -6
  324. package/build-types/utils/block-selection-history.d.ts.map +1 -1
  325. package/build-types/utils/clear-unchanged-edits.d.ts.map +1 -1
  326. package/build-types/utils/conservative-map-item.d.ts.map +1 -1
  327. package/build-types/utils/crdt-blocks.d.ts +0 -3
  328. package/build-types/utils/crdt-blocks.d.ts.map +1 -1
  329. package/build-types/utils/crdt-selection.d.ts +0 -3
  330. package/build-types/utils/crdt-selection.d.ts.map +1 -1
  331. package/build-types/utils/crdt-text.d.ts +0 -3
  332. package/build-types/utils/crdt-text.d.ts.map +1 -1
  333. package/build-types/utils/crdt-user-selections.d.ts.map +1 -1
  334. package/build-types/utils/crdt-utils.d.ts +0 -6
  335. package/build-types/utils/crdt-utils.d.ts.map +1 -1
  336. package/build-types/utils/crdt.d.ts +8 -0
  337. package/build-types/utils/crdt.d.ts.map +1 -1
  338. package/build-types/utils/get-template-info.d.ts +13 -0
  339. package/build-types/utils/get-template-info.d.ts.map +1 -0
  340. package/build-types/utils/get-template-part-icon.d.ts +9 -0
  341. package/build-types/utils/get-template-part-icon.d.ts.map +1 -0
  342. package/build-types/utils/log-entity-deprecation.d.ts.map +1 -1
  343. package/build-types/utils/save-crdt-doc.d.ts.map +1 -1
  344. package/package.json +28 -22
  345. package/src/actions.js +120 -13
  346. package/src/awareness/awareness-state.ts +0 -3
  347. package/src/awareness/base-awareness.ts +0 -8
  348. package/src/awareness/block-lookup.ts +1 -8
  349. package/src/awareness/post-editor-awareness.ts +13 -10
  350. package/src/awareness/test/awareness-state.ts +0 -11
  351. package/src/awareness/test/base-awareness.ts +0 -7
  352. package/src/awareness/test/block-lookup.ts +0 -8
  353. package/src/awareness/test/post-editor-awareness.ts +0 -7
  354. package/src/awareness/test/typed-awareness.ts +0 -11
  355. package/src/awareness/test/utils.ts +0 -3
  356. package/src/awareness/typed-awareness.ts +0 -7
  357. package/src/awareness/types.ts +0 -7
  358. package/src/awareness/utils.ts +0 -3
  359. package/src/batch/create-batch.js +0 -3
  360. package/src/batch/default-processor.js +0 -4
  361. package/src/batch/test/create-batch.js +0 -3
  362. package/src/batch/test/default-processor.js +0 -7
  363. package/src/components/entities-saved-states/entity-record-item.js +53 -0
  364. package/src/components/entities-saved-states/entity-type-list.js +123 -0
  365. package/src/components/entities-saved-states/hooks/use-is-dirty.js +98 -0
  366. package/src/components/entities-saved-states/index.js +231 -0
  367. package/src/components/entities-saved-states/style.scss +70 -0
  368. package/src/components/entities-saved-states/test/use-is-dirty.js +147 -0
  369. package/src/dynamic-entities.ts +0 -3
  370. package/src/entities.js +2 -11
  371. package/src/entity-context.js +0 -3
  372. package/src/entity-provider.js +0 -7
  373. package/src/entity-types/attachment.ts +0 -4
  374. package/src/entity-types/base.ts +0 -3
  375. package/src/entity-types/comment.ts +0 -3
  376. package/src/entity-types/font-collection.ts +0 -3
  377. package/src/entity-types/font-family.ts +0 -3
  378. package/src/entity-types/global-styles-revision.ts +0 -4
  379. package/src/entity-types/helpers.ts +0 -3
  380. package/src/entity-types/icon.ts +0 -4
  381. package/src/entity-types/index.ts +0 -3
  382. package/src/entity-types/menu-location.ts +0 -4
  383. package/src/entity-types/nav-menu-item.ts +0 -4
  384. package/src/entity-types/nav-menu.ts +0 -4
  385. package/src/entity-types/page.ts +0 -4
  386. package/src/entity-types/plugin.ts +0 -4
  387. package/src/entity-types/post-revision.ts +0 -4
  388. package/src/entity-types/post-status.ts +0 -3
  389. package/src/entity-types/post.ts +0 -4
  390. package/src/entity-types/settings.ts +0 -4
  391. package/src/entity-types/sidebar.ts +0 -4
  392. package/src/entity-types/taxonomy.ts +0 -4
  393. package/src/entity-types/term.ts +0 -4
  394. package/src/entity-types/test/attachment.test.ts +0 -4
  395. package/src/entity-types/theme.ts +0 -4
  396. package/src/entity-types/type.ts +0 -4
  397. package/src/entity-types/user.ts +0 -4
  398. package/src/entity-types/widget-type.ts +0 -4
  399. package/src/entity-types/widget.ts +0 -4
  400. package/src/entity-types/wp-template-part.ts +0 -4
  401. package/src/entity-types/wp-template.ts +0 -4
  402. package/src/fetch/__experimental-fetch-link-suggestions.ts +0 -3
  403. package/src/fetch/__experimental-fetch-url-data.js +0 -3
  404. package/src/fetch/index.js +0 -7
  405. package/src/fetch/test/__experimental-fetch-link-suggestions.js +0 -3
  406. package/src/fetch/test/__experimental-fetch-url-data.js +1 -7
  407. package/src/footnotes/get-footnotes-order.js +0 -3
  408. package/src/footnotes/get-rich-text-values-cached.js +0 -7
  409. package/src/footnotes/index.js +21 -8
  410. package/src/footnotes/test/index.js +57 -0
  411. package/src/hooks/index.ts +0 -3
  412. package/src/hooks/test/use-entity-record.js +0 -12
  413. package/src/hooks/test/use-entity-records.js +0 -12
  414. package/src/hooks/test/use-post-editor-awareness-state.ts +0 -7
  415. package/src/hooks/test/use-query-select.js +0 -11
  416. package/src/hooks/test/use-resource-permissions.js +0 -12
  417. package/src/hooks/use-entity-block-editor.js +0 -7
  418. package/src/hooks/use-entity-id.js +0 -7
  419. package/src/hooks/use-entity-prop.js +0 -7
  420. package/src/hooks/use-entity-record.ts +0 -7
  421. package/src/hooks/use-entity-records.ts +2 -9
  422. package/src/hooks/use-post-editor-awareness-state.ts +0 -7
  423. package/src/hooks/use-query-select.ts +0 -7
  424. package/src/hooks/use-resource-permissions.ts +0 -7
  425. package/src/hooks/utils.ts +0 -7
  426. package/src/index.js +0 -7
  427. package/src/lock-unlock.ts +0 -3
  428. package/src/locks/actions.js +0 -3
  429. package/src/locks/engine.js +0 -3
  430. package/src/locks/reducer.js +0 -3
  431. package/src/locks/selectors.js +0 -3
  432. package/src/locks/test/engine.js +0 -3
  433. package/src/locks/test/reducer.js +0 -7
  434. package/src/locks/test/selectors.js +0 -7
  435. package/src/locks/test/utils.js +0 -7
  436. package/src/private-actions.js +182 -7
  437. package/src/private-apis.ts +21 -4
  438. package/src/private-selectors.ts +22 -7
  439. package/src/queried-data/get-query-parts.js +0 -7
  440. package/src/queried-data/reducer.js +0 -7
  441. package/src/queried-data/selectors.js +0 -7
  442. package/src/queried-data/test/actions.js +0 -3
  443. package/src/queried-data/test/get-query-parts.js +0 -3
  444. package/src/queried-data/test/reducer.js +0 -7
  445. package/src/queried-data/test/selectors.js +0 -3
  446. package/src/reducer.js +0 -11
  447. package/src/resolvers.js +23 -40
  448. package/src/selectors.ts +0 -7
  449. package/src/style.scss +1 -0
  450. package/src/sync.ts +64 -7
  451. package/src/test/actions.js +843 -8
  452. package/src/test/deprecated-entity-logging.js +0 -7
  453. package/src/test/entities.js +0 -8
  454. package/src/test/entity-provider.js +0 -11
  455. package/src/test/private-actions.js +297 -7
  456. package/src/test/private-selectors.js +0 -3
  457. package/src/test/reducer.js +0 -7
  458. package/src/test/resolvers.js +0 -12
  459. package/src/test/rtc-rich-text-offset-space.test.js +0 -12
  460. package/src/test/selectors.js +0 -7
  461. package/src/test/store.js +8 -15
  462. package/src/types.ts +14 -12
  463. package/src/utils/block-selection-history.ts +0 -10
  464. package/src/utils/clear-unchanged-edits.ts +0 -3
  465. package/src/utils/conservative-map-item.js +0 -3
  466. package/src/utils/crdt-blocks.ts +0 -11
  467. package/src/utils/crdt-selection.ts +0 -7
  468. package/src/utils/crdt-text.ts +0 -3
  469. package/src/utils/crdt-user-selections.ts +85 -23
  470. package/src/utils/crdt-utils.ts +0 -7
  471. package/src/utils/crdt.ts +1 -12
  472. package/src/utils/get-template-info.js +46 -0
  473. package/src/utils/get-template-part-icon.js +27 -0
  474. package/src/utils/log-entity-deprecation.ts +0 -7
  475. package/src/utils/save-crdt-doc.js +0 -7
  476. package/src/utils/test/block-selection-history.test.ts +0 -7
  477. package/src/utils/test/clear-unchanged-edits.js +0 -3
  478. package/src/utils/test/conservative-map-item.js +0 -3
  479. package/src/utils/test/crdt-blocks.ts +0 -17
  480. package/src/utils/test/crdt-user-selections.ts +107 -30
  481. package/src/utils/test/crdt-utils.ts +0 -7
  482. package/src/utils/test/crdt.ts +0 -16
  483. package/src/utils/test/get-nested-value.js +0 -3
  484. package/src/utils/test/get-normalized-comma-separable.js +0 -3
  485. package/src/utils/test/get-template-info.js +218 -0
  486. package/src/utils/test/if-matching-action.js +0 -7
  487. package/src/utils/test/is-numeric-id.js +0 -3
  488. package/src/utils/test/log-entity-deprecation.js +0 -7
  489. package/src/utils/test/replace-action.js +0 -3
  490. package/src/utils/test/rtc-rich-text-cursor-scope.test.js +0 -14
  491. package/src/utils/test/rtc-rich-text-offset-space.test.js +0 -14
  492. package/src/utils/test/save-crdt-doc.js +0 -7
  493. package/src/utils/test/set-nested-value.js +0 -3
  494. package/src/utils/test/with-weak-map-cache.js +0 -3
@@ -1,13 +1,5 @@
1
- /**
2
- * WordPress dependencies
3
- */
4
1
  import apiFetch from '@wordpress/api-fetch';
5
-
6
2
  jest.mock( '@wordpress/api-fetch' );
7
-
8
- /**
9
- * Internal dependencies
10
- */
11
3
  import {
12
4
  editEntityRecord,
13
5
  clearEntityRecordEdits,
@@ -32,6 +24,7 @@ jest.mock( '../batch', () => {
32
24
 
33
25
  jest.mock( '../sync', () => ( {
34
26
  getSyncManager: jest.fn(),
27
+ CRDT_AUTOSAVE_SNAPSHOT_KEY: 'crdt_snapshot',
35
28
  LOCAL_EDITOR_ORIGIN: 'local-editor',
36
29
  LOCAL_UNDO_IGNORED_ORIGIN: 'local-undo-ignored',
37
30
  } ) );
@@ -973,6 +966,7 @@ describe( 'saveEntityRecord', () => {
973
966
  'local-undo-ignored',
974
967
  { isSave: true }
975
968
  );
969
+ expect( syncManager.update ).toHaveBeenCalledTimes( 1 );
976
970
  expect( liveSyncState ).toEqual( {
977
971
  isSaved: true,
978
972
  title: 'synced title',
@@ -980,6 +974,677 @@ describe( 'saveEntityRecord', () => {
980
974
  expect( result ).toBe( staleSaveResponse );
981
975
  } );
982
976
 
977
+ it( 'only passes server-mutated fields to SyncManager#update after saving', async () => {
978
+ const persistedRecord = {
979
+ id: 10,
980
+ title: 'Initial title',
981
+ content: 'Initial content',
982
+ template: 'single',
983
+ modified: '2026-07-01T00:00:00',
984
+ };
985
+ const edits = {
986
+ id: 10,
987
+ content: 'Updated content',
988
+ };
989
+ const configs = [
990
+ {
991
+ name: 'post',
992
+ kind: 'postType',
993
+ baseURL: '/wp/v2/posts',
994
+ syncConfig: {},
995
+ },
996
+ ];
997
+ const syncManager = {
998
+ update: jest.fn(),
999
+ };
1000
+ const select = {
1001
+ getRawEntityRecord: () => persistedRecord,
1002
+ };
1003
+ const resolveSelect = { getEntitiesConfig: jest.fn( () => configs ) };
1004
+ const updatedRecord = {
1005
+ ...persistedRecord,
1006
+ content: edits.content,
1007
+ modified: '2026-07-02T00:00:00',
1008
+ };
1009
+ apiFetch.mockImplementation( () => updatedRecord );
1010
+ getSyncManager.mockReturnValue( syncManager );
1011
+
1012
+ const result = await saveEntityRecord(
1013
+ 'postType',
1014
+ 'post',
1015
+ edits
1016
+ )( { select, dispatch, resolveSelect } );
1017
+
1018
+ expect( syncManager.update ).toHaveBeenNthCalledWith(
1019
+ 1,
1020
+ 'postType/post',
1021
+ 10,
1022
+ edits,
1023
+ 'local-undo-ignored'
1024
+ );
1025
+ expect( syncManager.update ).toHaveBeenCalledWith(
1026
+ 'postType/post',
1027
+ 10,
1028
+ {
1029
+ modified: '2026-07-02T00:00:00',
1030
+ },
1031
+ 'local-undo-ignored',
1032
+ { isSave: true }
1033
+ );
1034
+ expect( syncManager.update ).toHaveBeenCalledTimes( 2 );
1035
+ expect( result ).toBe( updatedRecord );
1036
+ } );
1037
+
1038
+ it( 'does not pass unchanged meta fields to SyncManager#update after saving', async () => {
1039
+ const persistedRecord = {
1040
+ id: 10,
1041
+ content: 'Initial content',
1042
+ meta: {
1043
+ plugin_value: 'persisted',
1044
+ peer_value: 'persisted',
1045
+ _crdt_document: 'old-doc',
1046
+ },
1047
+ };
1048
+ const edits = {
1049
+ id: 10,
1050
+ content: 'Updated content',
1051
+ };
1052
+ const configs = [
1053
+ {
1054
+ name: 'post',
1055
+ kind: 'postType',
1056
+ baseURL: '/wp/v2/posts',
1057
+ syncConfig: {},
1058
+ __unstablePrePersist: async () => ( {
1059
+ meta: { _crdt_document: 'new-doc' },
1060
+ } ),
1061
+ },
1062
+ ];
1063
+ const syncManager = {
1064
+ update: jest.fn(),
1065
+ };
1066
+ const select = {
1067
+ getRawEntityRecord: () => persistedRecord,
1068
+ };
1069
+ const resolveSelect = { getEntitiesConfig: jest.fn( () => configs ) };
1070
+ const updatedRecord = {
1071
+ ...persistedRecord,
1072
+ content: edits.content,
1073
+ meta: {
1074
+ plugin_value: 'persisted',
1075
+ peer_value: 'persisted',
1076
+ _crdt_document: 'new-doc',
1077
+ },
1078
+ };
1079
+ apiFetch.mockImplementation( () => updatedRecord );
1080
+ getSyncManager.mockReturnValue( syncManager );
1081
+
1082
+ const result = await saveEntityRecord(
1083
+ 'postType',
1084
+ 'post',
1085
+ edits
1086
+ )( { select, dispatch, resolveSelect } );
1087
+
1088
+ expect( syncManager.update ).toHaveBeenNthCalledWith(
1089
+ 2,
1090
+ 'postType/post',
1091
+ 10,
1092
+ {},
1093
+ 'local-undo-ignored',
1094
+ { isSave: true }
1095
+ );
1096
+ expect( syncManager.update ).toHaveBeenCalledTimes( 2 );
1097
+ expect( result ).toBe( updatedRecord );
1098
+ } );
1099
+
1100
+ it( 'passes only server-mutated meta fields to SyncManager#update after saving', async () => {
1101
+ const persistedRecord = {
1102
+ id: 10,
1103
+ meta: {
1104
+ unchanged: 'persisted',
1105
+ peer_value: 'persisted',
1106
+ edited: 'initial',
1107
+ server_mutated: 'before',
1108
+ settings: { color: 'blue', size: 'small' },
1109
+ },
1110
+ };
1111
+ const edits = {
1112
+ id: 10,
1113
+ meta: {
1114
+ edited: 'local',
1115
+ server_mutated: 'Needs Normalizing',
1116
+ },
1117
+ };
1118
+ const configs = [
1119
+ {
1120
+ name: 'post',
1121
+ kind: 'postType',
1122
+ baseURL: '/wp/v2/posts',
1123
+ syncConfig: {},
1124
+ __unstablePrePersist: async () => ( {
1125
+ meta: {
1126
+ ...edits.meta,
1127
+ _crdt_document: 'new-doc',
1128
+ },
1129
+ } ),
1130
+ },
1131
+ ];
1132
+ const syncManager = {
1133
+ update: jest.fn(),
1134
+ };
1135
+ const select = {
1136
+ getRawEntityRecord: () => persistedRecord,
1137
+ };
1138
+ const resolveSelect = { getEntitiesConfig: jest.fn( () => configs ) };
1139
+ const updatedRecord = {
1140
+ id: 10,
1141
+ meta: {
1142
+ unchanged: 'persisted',
1143
+ peer_value: 'persisted',
1144
+ edited: 'local',
1145
+ server_mutated: 'needs-normalizing',
1146
+ settings: { color: 'red', size: 'small' },
1147
+ _crdt_document: 'new-doc',
1148
+ },
1149
+ };
1150
+ apiFetch.mockImplementation( () => updatedRecord );
1151
+ getSyncManager.mockReturnValue( syncManager );
1152
+
1153
+ const result = await saveEntityRecord(
1154
+ 'postType',
1155
+ 'post',
1156
+ edits
1157
+ )( { select, dispatch, resolveSelect } );
1158
+
1159
+ expect( syncManager.update ).toHaveBeenNthCalledWith(
1160
+ 1,
1161
+ 'postType/post',
1162
+ 10,
1163
+ edits,
1164
+ 'local-undo-ignored'
1165
+ );
1166
+ expect( syncManager.update ).toHaveBeenNthCalledWith(
1167
+ 2,
1168
+ 'postType/post',
1169
+ 10,
1170
+ {
1171
+ meta: {
1172
+ server_mutated: 'needs-normalizing',
1173
+ settings: { color: 'red', size: 'small' },
1174
+ },
1175
+ },
1176
+ 'local-undo-ignored',
1177
+ { isSave: true }
1178
+ );
1179
+ expect( syncManager.update ).toHaveBeenCalledTimes( 2 );
1180
+ expect( result ).toBe( updatedRecord );
1181
+ } );
1182
+
1183
+ it( 'resets persisted edits using the pre-prePersist edits so the record is clean after saving', async () => {
1184
+ const persistedRecord = {
1185
+ id: 10,
1186
+ meta: {
1187
+ plugin_value: 'persisted',
1188
+ _crdt_document: 'old-doc',
1189
+ },
1190
+ };
1191
+ // Mirrors a store meta edit: `mergedEdits` snapshots the full edited
1192
+ // meta, including the load-time CRDT document.
1193
+ const edits = {
1194
+ id: 10,
1195
+ meta: {
1196
+ plugin_value: 'edited',
1197
+ _crdt_document: 'old-doc',
1198
+ },
1199
+ };
1200
+ const configs = [
1201
+ {
1202
+ name: 'post',
1203
+ kind: 'postType',
1204
+ baseURL: '/wp/v2/posts',
1205
+ syncConfig: {},
1206
+ // Mirrors prePersistPostType, which injects a freshly
1207
+ // serialized CRDT snapshot into the request meta.
1208
+ __unstablePrePersist: async ( _persisted, saveEdits ) => ( {
1209
+ meta: {
1210
+ ...saveEdits.meta,
1211
+ _crdt_document: 'new-doc',
1212
+ },
1213
+ } ),
1214
+ },
1215
+ ];
1216
+ const syncManager = {
1217
+ update: jest.fn(),
1218
+ };
1219
+ const select = {
1220
+ getRawEntityRecord: () => persistedRecord,
1221
+ };
1222
+ const resolveSelect = { getEntitiesConfig: jest.fn( () => configs ) };
1223
+ const updatedRecord = {
1224
+ id: 10,
1225
+ meta: {
1226
+ plugin_value: 'edited',
1227
+ // The server may also mutate meta on save (e.g. a plugin
1228
+ // normalizing a value in a save hook).
1229
+ server_value: 'server-mutated',
1230
+ _crdt_document: 'new-doc',
1231
+ },
1232
+ };
1233
+ apiFetch.mockImplementation( () => updatedRecord );
1234
+ getSyncManager.mockReturnValue( syncManager );
1235
+
1236
+ await saveEntityRecord(
1237
+ 'postType',
1238
+ 'post',
1239
+ edits
1240
+ )( { select, dispatch, resolveSelect } );
1241
+
1242
+ expect( apiFetch ).toHaveBeenCalledWith( {
1243
+ path: '/wp/v2/posts/10',
1244
+ method: 'PUT',
1245
+ data: {
1246
+ ...edits,
1247
+ meta: {
1248
+ plugin_value: 'edited',
1249
+ _crdt_document: 'new-doc',
1250
+ },
1251
+ },
1252
+ } );
1253
+
1254
+ // The persisted edits passed to the reducer must be the original
1255
+ // edits, not the prePersist-augmented request payload. Otherwise the
1256
+ // injected CRDT snapshot makes the comparison against the state edits
1257
+ // fail and the record stays dirty after a successful save.
1258
+ expect( dispatch.receiveEntityRecords ).toHaveBeenCalledWith(
1259
+ 'postType',
1260
+ 'post',
1261
+ updatedRecord,
1262
+ undefined,
1263
+ true,
1264
+ edits
1265
+ );
1266
+ } );
1267
+
1268
+ it( 'syncs direct save changes before pre-persisting the record', async () => {
1269
+ const persistedRecord = {
1270
+ id: 10,
1271
+ status: 'auto-draft',
1272
+ template: '',
1273
+ };
1274
+ const edits = {
1275
+ id: 10,
1276
+ template: 'page-no-title',
1277
+ };
1278
+ const syncManager = {
1279
+ update: jest.fn(),
1280
+ };
1281
+ const prePersist = jest.fn( async () => {
1282
+ expect( syncManager.update ).toHaveBeenCalledTimes( 1 );
1283
+ expect( syncManager.update ).toHaveBeenLastCalledWith(
1284
+ 'postType/page',
1285
+ 10,
1286
+ edits,
1287
+ 'local-undo-ignored'
1288
+ );
1289
+
1290
+ return { status: 'draft' };
1291
+ } );
1292
+ const configs = [
1293
+ {
1294
+ name: 'page',
1295
+ kind: 'postType',
1296
+ baseURL: '/wp/v2/pages',
1297
+ syncConfig: {},
1298
+ __unstablePrePersist: prePersist,
1299
+ },
1300
+ ];
1301
+ const select = {
1302
+ getRawEntityRecord: () => persistedRecord,
1303
+ };
1304
+ const resolveSelect = { getEntitiesConfig: jest.fn( () => configs ) };
1305
+ const updatedRecord = {
1306
+ ...persistedRecord,
1307
+ ...edits,
1308
+ status: 'draft',
1309
+ };
1310
+ apiFetch.mockImplementation( () => updatedRecord );
1311
+ getSyncManager.mockReturnValue( syncManager );
1312
+
1313
+ const result = await saveEntityRecord(
1314
+ 'postType',
1315
+ 'page',
1316
+ edits
1317
+ )( { select, dispatch, resolveSelect } );
1318
+
1319
+ expect( prePersist ).toHaveBeenCalledWith( persistedRecord, edits );
1320
+ expect( apiFetch ).toHaveBeenCalledWith( {
1321
+ path: '/wp/v2/pages/10',
1322
+ method: 'PUT',
1323
+ data: { ...edits, status: 'draft' },
1324
+ } );
1325
+ expect( syncManager.update ).toHaveBeenNthCalledWith(
1326
+ 2,
1327
+ 'postType/page',
1328
+ 10,
1329
+ { status: 'draft' },
1330
+ 'local-undo-ignored',
1331
+ { isSave: true }
1332
+ );
1333
+ expect( syncManager.update ).toHaveBeenCalledTimes( 2 );
1334
+ expect( result ).toBe( updatedRecord );
1335
+ } );
1336
+
1337
+ it( 'does not mark pre-synced direct save changes as saved when the request fails', async () => {
1338
+ const persistedRecord = {
1339
+ id: 10,
1340
+ template: '',
1341
+ };
1342
+ const edits = {
1343
+ id: 10,
1344
+ template: 'page-no-title',
1345
+ };
1346
+ const configs = [
1347
+ {
1348
+ name: 'page',
1349
+ kind: 'postType',
1350
+ baseURL: '/wp/v2/pages',
1351
+ syncConfig: {},
1352
+ },
1353
+ ];
1354
+ const syncManager = {
1355
+ update: jest.fn(),
1356
+ };
1357
+ const select = {
1358
+ getRawEntityRecord: () => persistedRecord,
1359
+ };
1360
+ const resolveSelect = { getEntitiesConfig: jest.fn( () => configs ) };
1361
+ const error = new Error( 'API error' );
1362
+ apiFetch.mockRejectedValue( error );
1363
+ getSyncManager.mockReturnValue( syncManager );
1364
+
1365
+ await expect(
1366
+ saveEntityRecord( 'postType', 'page', edits, {
1367
+ throwOnError: true,
1368
+ } )( { select, dispatch, resolveSelect } )
1369
+ ).rejects.toBe( error );
1370
+
1371
+ expect( syncManager.update ).toHaveBeenCalledTimes( 1 );
1372
+ expect( syncManager.update ).toHaveBeenCalledWith(
1373
+ 'postType/page',
1374
+ 10,
1375
+ edits,
1376
+ 'local-undo-ignored'
1377
+ );
1378
+ } );
1379
+
1380
+ it( 'passes server-normalized edited fields to SyncManager#update after saving', async () => {
1381
+ const persistedRecord = {
1382
+ id: 10,
1383
+ slug: 'initial-slug',
1384
+ };
1385
+ const edits = {
1386
+ id: 10,
1387
+ slug: 'Needs Normalizing',
1388
+ };
1389
+ const configs = [
1390
+ {
1391
+ name: 'post',
1392
+ kind: 'postType',
1393
+ baseURL: '/wp/v2/posts',
1394
+ syncConfig: {},
1395
+ },
1396
+ ];
1397
+ const syncManager = {
1398
+ update: jest.fn(),
1399
+ };
1400
+ const select = {
1401
+ getRawEntityRecord: () => persistedRecord,
1402
+ };
1403
+ const resolveSelect = { getEntitiesConfig: jest.fn( () => configs ) };
1404
+ const updatedRecord = {
1405
+ id: 10,
1406
+ slug: 'needs-normalizing',
1407
+ };
1408
+ apiFetch.mockImplementation( () => updatedRecord );
1409
+ getSyncManager.mockReturnValue( syncManager );
1410
+
1411
+ const result = await saveEntityRecord(
1412
+ 'postType',
1413
+ 'post',
1414
+ edits
1415
+ )( { select, dispatch, resolveSelect } );
1416
+
1417
+ expect( syncManager.update ).toHaveBeenCalledWith(
1418
+ 'postType/post',
1419
+ 10,
1420
+ {
1421
+ slug: 'needs-normalizing',
1422
+ },
1423
+ 'local-undo-ignored',
1424
+ { isSave: true }
1425
+ );
1426
+ expect( syncManager.update ).toHaveBeenCalledTimes( 2 );
1427
+ expect( result ).toBe( updatedRecord );
1428
+ } );
1429
+
1430
+ it( 'does not pass unchanged raw-attribute fields to SyncManager#update after saving', async () => {
1431
+ // The raw entity record holds raw strings while the save response
1432
+ // nests them as `{ raw, rendered }`; the comparison must not treat
1433
+ // that shape difference as a server mutation.
1434
+ const persistedRecord = {
1435
+ id: 10,
1436
+ title: 'Initial title',
1437
+ content: '<p>Initial content</p>',
1438
+ excerpt: 'Initial excerpt',
1439
+ slug: 'initial-slug',
1440
+ modified: '2026-07-01T00:00:00',
1441
+ };
1442
+ const edits = {
1443
+ id: 10,
1444
+ slug: 'updated-slug',
1445
+ };
1446
+ const configs = [
1447
+ {
1448
+ name: 'post',
1449
+ kind: 'postType',
1450
+ baseURL: '/wp/v2/posts',
1451
+ syncConfig: {},
1452
+ },
1453
+ ];
1454
+ const syncManager = {
1455
+ update: jest.fn(),
1456
+ };
1457
+ const select = {
1458
+ getRawEntityRecord: () => persistedRecord,
1459
+ };
1460
+ const resolveSelect = { getEntitiesConfig: jest.fn( () => configs ) };
1461
+ const updatedRecord = {
1462
+ id: 10,
1463
+ title: { raw: 'Initial title', rendered: 'Initial title' },
1464
+ content: {
1465
+ raw: '<p>Initial content</p>',
1466
+ rendered: '<p>Initial content</p>',
1467
+ },
1468
+ excerpt: { raw: 'Initial excerpt', rendered: 'Initial excerpt' },
1469
+ slug: 'updated-slug',
1470
+ modified: '2026-07-02T00:00:00',
1471
+ };
1472
+ apiFetch.mockImplementation( () => updatedRecord );
1473
+ getSyncManager.mockReturnValue( syncManager );
1474
+
1475
+ const result = await saveEntityRecord(
1476
+ 'postType',
1477
+ 'post',
1478
+ edits
1479
+ )( { select, dispatch, resolveSelect } );
1480
+
1481
+ expect( syncManager.update ).toHaveBeenCalledWith(
1482
+ 'postType/post',
1483
+ 10,
1484
+ {
1485
+ modified: '2026-07-02T00:00:00',
1486
+ },
1487
+ 'local-undo-ignored',
1488
+ { isSave: true }
1489
+ );
1490
+ expect( syncManager.update ).toHaveBeenCalledTimes( 2 );
1491
+ expect( result ).toBe( updatedRecord );
1492
+ } );
1493
+
1494
+ it( 'does not pass edited raw-attribute fields to SyncManager#update when the server echoes them unchanged', async () => {
1495
+ const persistedRecord = {
1496
+ id: 10,
1497
+ content: '<p>Initial content</p>',
1498
+ modified: '2026-07-01T00:00:00',
1499
+ };
1500
+ const edits = {
1501
+ id: 10,
1502
+ content: '<p>Updated content</p>',
1503
+ };
1504
+ const configs = [
1505
+ {
1506
+ name: 'post',
1507
+ kind: 'postType',
1508
+ baseURL: '/wp/v2/posts',
1509
+ syncConfig: {},
1510
+ },
1511
+ ];
1512
+ const syncManager = {
1513
+ update: jest.fn(),
1514
+ };
1515
+ const select = {
1516
+ getRawEntityRecord: () => persistedRecord,
1517
+ };
1518
+ const resolveSelect = { getEntitiesConfig: jest.fn( () => configs ) };
1519
+ const updatedRecord = {
1520
+ id: 10,
1521
+ content: {
1522
+ raw: edits.content,
1523
+ // The rendered value differs from the raw baseline, but only
1524
+ // raw values are compared.
1525
+ rendered: '<p class="rendered">Updated content</p>\n',
1526
+ },
1527
+ modified: '2026-07-02T00:00:00',
1528
+ };
1529
+ apiFetch.mockImplementation( () => updatedRecord );
1530
+ getSyncManager.mockReturnValue( syncManager );
1531
+
1532
+ const result = await saveEntityRecord(
1533
+ 'postType',
1534
+ 'post',
1535
+ edits
1536
+ )( { select, dispatch, resolveSelect } );
1537
+
1538
+ expect( syncManager.update ).toHaveBeenCalledWith(
1539
+ 'postType/post',
1540
+ 10,
1541
+ {
1542
+ modified: '2026-07-02T00:00:00',
1543
+ },
1544
+ 'local-undo-ignored',
1545
+ { isSave: true }
1546
+ );
1547
+ expect( result ).toBe( updatedRecord );
1548
+ } );
1549
+
1550
+ it( 'passes raw-attribute fields whose raw value the server mutated to SyncManager#update', async () => {
1551
+ const persistedRecord = {
1552
+ id: 10,
1553
+ content: '<p>Initial content</p>',
1554
+ };
1555
+ const edits = {
1556
+ id: 10,
1557
+ content: '<p>Content with <script>bad</script> markup</p>',
1558
+ };
1559
+ const configs = [
1560
+ {
1561
+ name: 'post',
1562
+ kind: 'postType',
1563
+ baseURL: '/wp/v2/posts',
1564
+ syncConfig: {},
1565
+ },
1566
+ ];
1567
+ const syncManager = {
1568
+ update: jest.fn(),
1569
+ };
1570
+ const select = {
1571
+ getRawEntityRecord: () => persistedRecord,
1572
+ };
1573
+ const resolveSelect = { getEntitiesConfig: jest.fn( () => configs ) };
1574
+ // The server strips disallowed markup from the sent content.
1575
+ const updatedRecord = {
1576
+ id: 10,
1577
+ content: {
1578
+ raw: '<p>Content with bad markup</p>',
1579
+ rendered: '<p>Content with bad markup</p>',
1580
+ },
1581
+ };
1582
+ apiFetch.mockImplementation( () => updatedRecord );
1583
+ getSyncManager.mockReturnValue( syncManager );
1584
+
1585
+ const result = await saveEntityRecord(
1586
+ 'postType',
1587
+ 'post',
1588
+ edits
1589
+ )( { select, dispatch, resolveSelect } );
1590
+
1591
+ expect( syncManager.update ).toHaveBeenCalledWith(
1592
+ 'postType/post',
1593
+ 10,
1594
+ {
1595
+ content: updatedRecord.content,
1596
+ },
1597
+ 'local-undo-ignored',
1598
+ { isSave: true }
1599
+ );
1600
+ expect( result ).toBe( updatedRecord );
1601
+ } );
1602
+
1603
+ it( 'passes the full save response to SyncManager#update when the persisted record is missing', async () => {
1604
+ const edits = {
1605
+ id: 10,
1606
+ content: 'Updated content',
1607
+ };
1608
+ const configs = [
1609
+ {
1610
+ name: 'post',
1611
+ kind: 'postType',
1612
+ baseURL: '/wp/v2/posts',
1613
+ syncConfig: {},
1614
+ },
1615
+ ];
1616
+ const syncManager = {
1617
+ update: jest.fn(),
1618
+ };
1619
+ const select = {
1620
+ getRawEntityRecord: () => undefined,
1621
+ };
1622
+ const resolveSelect = { getEntitiesConfig: jest.fn( () => configs ) };
1623
+ const updatedRecord = {
1624
+ id: 10,
1625
+ content: 'Updated content',
1626
+ template: 'single',
1627
+ };
1628
+ apiFetch.mockImplementation( () => updatedRecord );
1629
+ getSyncManager.mockReturnValue( syncManager );
1630
+
1631
+ const result = await saveEntityRecord(
1632
+ 'postType',
1633
+ 'post',
1634
+ edits
1635
+ )( { select, dispatch, resolveSelect } );
1636
+
1637
+ expect( syncManager.update ).toHaveBeenCalledWith(
1638
+ 'postType/post',
1639
+ 10,
1640
+ updatedRecord,
1641
+ 'local-undo-ignored',
1642
+ { isSave: true }
1643
+ );
1644
+ expect( syncManager.update ).toHaveBeenCalledTimes( 1 );
1645
+ expect( result ).toBe( updatedRecord );
1646
+ } );
1647
+
983
1648
  it( 'triggers a PUT request for an existing record with a custom key', async () => {
984
1649
  const postType = { slug: 'page', title: 'Pages' };
985
1650
  const configs = [
@@ -1046,6 +1711,176 @@ describe( 'saveEntityRecord', () => {
1046
1711
 
1047
1712
  expect( result ).toBe( postType );
1048
1713
  } );
1714
+
1715
+ describe( 'autosave CRDT snapshots', () => {
1716
+ const persistedRecord = {
1717
+ id: 10,
1718
+ title: 'Test post',
1719
+ content: 'Test content',
1720
+ };
1721
+ let select;
1722
+ let resolveSelect;
1723
+ let syncManager;
1724
+
1725
+ beforeEach( () => {
1726
+ dispatch.receiveAutosaves = jest.fn();
1727
+ select = {
1728
+ getRawEntityRecord: () => persistedRecord,
1729
+ };
1730
+ syncManager = {
1731
+ getEntitySnapshot: jest.fn( () => 'ENCODED_SNAPSHOT' ),
1732
+ update: jest.fn(),
1733
+ };
1734
+ getSyncManager.mockReturnValue( syncManager );
1735
+ apiFetch.mockImplementation( () => ( {
1736
+ id: 20,
1737
+ parent: 10,
1738
+ author: 2,
1739
+ modified_gmt: '2026-07-21T10:00:00',
1740
+ } ) );
1741
+ } );
1742
+
1743
+ afterEach( () => {
1744
+ getSyncManager.mockReset();
1745
+ } );
1746
+
1747
+ function makeResolveSelect( entityConfig ) {
1748
+ return {
1749
+ getEntitiesConfig: jest.fn( () => [ entityConfig ] ),
1750
+ };
1751
+ }
1752
+
1753
+ function getAutosaveRequestData() {
1754
+ return apiFetch.mock.calls[ 0 ][ 0 ].data;
1755
+ }
1756
+
1757
+ it( 'sends the current CRDT snapshot with the autosave request', async () => {
1758
+ resolveSelect = makeResolveSelect( {
1759
+ name: 'post',
1760
+ kind: 'postType',
1761
+ baseURL: '/wp/v2/posts',
1762
+ syncConfig: {},
1763
+ } );
1764
+
1765
+ await saveEntityRecord( 'postType', 'post', persistedRecord, {
1766
+ isAutosave: true,
1767
+ } )( { select, dispatch, resolveSelect } );
1768
+
1769
+ expect( syncManager.getEntitySnapshot ).toHaveBeenCalledWith(
1770
+ 'postType/post',
1771
+ 10
1772
+ );
1773
+ expect( getAutosaveRequestData() ).toEqual(
1774
+ expect.objectContaining( {
1775
+ crdt_snapshot: 'ENCODED_SNAPSHOT',
1776
+ } )
1777
+ );
1778
+ } );
1779
+
1780
+ it( 'omits the snapshot for entities without a sync config', async () => {
1781
+ resolveSelect = makeResolveSelect( {
1782
+ name: 'post',
1783
+ kind: 'postType',
1784
+ baseURL: '/wp/v2/posts',
1785
+ } );
1786
+
1787
+ await saveEntityRecord( 'postType', 'post', persistedRecord, {
1788
+ isAutosave: true,
1789
+ } )( { select, dispatch, resolveSelect } );
1790
+
1791
+ expect( syncManager.getEntitySnapshot ).not.toHaveBeenCalled();
1792
+ expect( getAutosaveRequestData() ).not.toHaveProperty(
1793
+ 'crdt_snapshot'
1794
+ );
1795
+ expect( dispatch.receiveAutosaves ).toHaveBeenCalled();
1796
+ } );
1797
+
1798
+ it( 'omits the snapshot when the entity is not loaded in the sync manager', async () => {
1799
+ resolveSelect = makeResolveSelect( {
1800
+ name: 'post',
1801
+ kind: 'postType',
1802
+ baseURL: '/wp/v2/posts',
1803
+ syncConfig: {},
1804
+ } );
1805
+ syncManager.getEntitySnapshot.mockReturnValue( undefined );
1806
+
1807
+ await saveEntityRecord( 'postType', 'post', persistedRecord, {
1808
+ isAutosave: true,
1809
+ } )( { select, dispatch, resolveSelect } );
1810
+
1811
+ expect( getAutosaveRequestData() ).not.toHaveProperty(
1812
+ 'crdt_snapshot'
1813
+ );
1814
+ } );
1815
+
1816
+ it( 'captures the snapshot before the request is sent', async () => {
1817
+ resolveSelect = makeResolveSelect( {
1818
+ name: 'post',
1819
+ kind: 'postType',
1820
+ baseURL: '/wp/v2/posts',
1821
+ syncConfig: {},
1822
+ } );
1823
+
1824
+ // A snapshot captured after the request would describe content
1825
+ // the autosave did not include, which could wrongly suppress the
1826
+ // notice. Assert the ordering directly.
1827
+ const callOrder = [];
1828
+ syncManager.getEntitySnapshot.mockImplementation( () => {
1829
+ callOrder.push( 'snapshot' );
1830
+ return 'ENCODED_SNAPSHOT';
1831
+ } );
1832
+ apiFetch.mockImplementation( () => {
1833
+ callOrder.push( 'fetch' );
1834
+ return { id: 20, parent: 10 };
1835
+ } );
1836
+
1837
+ await saveEntityRecord( 'postType', 'post', persistedRecord, {
1838
+ isAutosave: true,
1839
+ } )( { select, dispatch, resolveSelect } );
1840
+
1841
+ expect( callOrder ).toEqual( [ 'snapshot', 'fetch' ] );
1842
+ } );
1843
+
1844
+ it( 'applies direct record changes to the CRDT before capturing the snapshot', async () => {
1845
+ resolveSelect = makeResolveSelect( {
1846
+ name: 'post',
1847
+ kind: 'postType',
1848
+ baseURL: '/wp/v2/posts',
1849
+ syncConfig: {},
1850
+ } );
1851
+
1852
+ // A direct caller can pass content that never went through
1853
+ // `editEntityRecord`, so it is not yet in the CRDT. If the
1854
+ // snapshot were captured first, it would describe a state
1855
+ // without this content and wrongly suppress the recovery
1856
+ // notice on reload.
1857
+ const callOrder = [];
1858
+ syncManager.update = jest.fn( () => {
1859
+ callOrder.push( 'update' );
1860
+ } );
1861
+ syncManager.getEntitySnapshot.mockImplementation( () => {
1862
+ callOrder.push( 'snapshot' );
1863
+ return 'ENCODED_SNAPSHOT';
1864
+ } );
1865
+
1866
+ const record = {
1867
+ id: 10,
1868
+ content: 'Directly autosaved content',
1869
+ };
1870
+
1871
+ await saveEntityRecord( 'postType', 'post', record, {
1872
+ isAutosave: true,
1873
+ } )( { select, dispatch, resolveSelect } );
1874
+
1875
+ expect( syncManager.update ).toHaveBeenCalledWith(
1876
+ 'postType/post',
1877
+ 10,
1878
+ record,
1879
+ 'local-undo-ignored'
1880
+ );
1881
+ expect( callOrder ).toEqual( [ 'update', 'snapshot' ] );
1882
+ } );
1883
+ } );
1049
1884
  } );
1050
1885
 
1051
1886
  describe( 'receiveUserPermission', () => {