@wordpress/core-data 7.50.1-next.v.202607070741.0 → 7.52.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 (139) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/build/actions.cjs +54 -1
  3. package/build/actions.cjs.map +3 -3
  4. package/build/components/entities-saved-states/entity-record-item.cjs +67 -0
  5. package/build/components/entities-saved-states/entity-record-item.cjs.map +7 -0
  6. package/build/components/entities-saved-states/entity-type-list.cjs +135 -0
  7. package/build/components/entities-saved-states/entity-type-list.cjs.map +7 -0
  8. package/build/components/entities-saved-states/hooks/use-is-dirty.cjs +97 -0
  9. package/build/components/entities-saved-states/hooks/use-is-dirty.cjs.map +7 -0
  10. package/build/components/entities-saved-states/index.cjs +215 -0
  11. package/build/components/entities-saved-states/index.cjs.map +7 -0
  12. package/build/entities.cjs +3 -1
  13. package/build/entities.cjs.map +2 -2
  14. package/build/private-actions.cjs +85 -0
  15. package/build/private-actions.cjs.map +3 -3
  16. package/build/private-apis.cjs +19 -0
  17. package/build/private-apis.cjs.map +3 -3
  18. package/build/utils/crdt.cjs +4 -2
  19. package/build/utils/crdt.cjs.map +2 -2
  20. package/build/utils/get-template-info.cjs +53 -0
  21. package/build/utils/get-template-info.cjs.map +7 -0
  22. package/build/utils/get-template-part-icon.cjs +43 -0
  23. package/build/utils/get-template-part-icon.cjs.map +7 -0
  24. package/build-module/actions.mjs +57 -1
  25. package/build-module/actions.mjs.map +2 -2
  26. package/build-module/components/entities-saved-states/entity-record-item.mjs +46 -0
  27. package/build-module/components/entities-saved-states/entity-record-item.mjs.map +7 -0
  28. package/build-module/components/entities-saved-states/entity-type-list.mjs +104 -0
  29. package/build-module/components/entities-saved-states/entity-type-list.mjs.map +7 -0
  30. package/build-module/components/entities-saved-states/hooks/use-is-dirty.mjs +72 -0
  31. package/build-module/components/entities-saved-states/hooks/use-is-dirty.mjs.map +7 -0
  32. package/build-module/components/entities-saved-states/index.mjs +187 -0
  33. package/build-module/components/entities-saved-states/index.mjs.map +7 -0
  34. package/build-module/entities.mjs +3 -1
  35. package/build-module/entities.mjs.map +2 -2
  36. package/build-module/private-actions.mjs +84 -0
  37. package/build-module/private-actions.mjs.map +2 -2
  38. package/build-module/private-apis.mjs +12 -0
  39. package/build-module/private-apis.mjs.map +2 -2
  40. package/build-module/utils/crdt.mjs +2 -1
  41. package/build-module/utils/crdt.mjs.map +2 -2
  42. package/build-module/utils/get-template-info.mjs +28 -0
  43. package/build-module/utils/get-template-info.mjs.map +7 -0
  44. package/build-module/utils/get-template-part-icon.mjs +24 -0
  45. package/build-module/utils/get-template-part-icon.mjs.map +7 -0
  46. package/build-types/actions.d.ts +3 -3
  47. package/build-types/actions.d.ts.map +1 -1
  48. package/build-types/awareness/base-awareness.d.ts +3 -0
  49. package/build-types/awareness/base-awareness.d.ts.map +1 -1
  50. package/build-types/awareness/post-editor-awareness.d.ts +7 -0
  51. package/build-types/awareness/post-editor-awareness.d.ts.map +1 -1
  52. package/build-types/batch/create-batch.d.ts +1 -1
  53. package/build-types/batch/create-batch.d.ts.map +1 -1
  54. package/build-types/components/entities-saved-states/entity-record-item.d.ts +6 -0
  55. package/build-types/components/entities-saved-states/entity-record-item.d.ts.map +1 -0
  56. package/build-types/components/entities-saved-states/entity-type-list.d.ts +6 -0
  57. package/build-types/components/entities-saved-states/entity-type-list.d.ts.map +1 -0
  58. package/build-types/components/entities-saved-states/hooks/use-is-dirty.d.ts +11 -0
  59. package/build-types/components/entities-saved-states/hooks/use-is-dirty.d.ts.map +1 -0
  60. package/build-types/components/entities-saved-states/index.d.ts +49 -0
  61. package/build-types/components/entities-saved-states/index.d.ts.map +1 -0
  62. package/build-types/entities.d.ts +2 -2
  63. package/build-types/entities.d.ts.map +1 -1
  64. package/build-types/entity-provider.d.ts +1 -1
  65. package/build-types/entity-provider.d.ts.map +1 -1
  66. package/build-types/entity-types/attachment.d.ts.map +1 -1
  67. package/build-types/entity-types/base-entity-records.d.ts.map +1 -1
  68. package/build-types/entity-types/base.d.ts.map +1 -1
  69. package/build-types/entity-types/comment.d.ts.map +1 -1
  70. package/build-types/entity-types/font-collection.d.ts.map +1 -1
  71. package/build-types/entity-types/font-family.d.ts.map +1 -1
  72. package/build-types/entity-types/global-styles-revision.d.ts.map +1 -1
  73. package/build-types/entity-types/icon.d.ts.map +1 -1
  74. package/build-types/entity-types/menu-location.d.ts.map +1 -1
  75. package/build-types/entity-types/nav-menu-item.d.ts.map +1 -1
  76. package/build-types/entity-types/nav-menu.d.ts.map +1 -1
  77. package/build-types/entity-types/page.d.ts.map +1 -1
  78. package/build-types/entity-types/plugin.d.ts.map +1 -1
  79. package/build-types/entity-types/post-revision.d.ts.map +1 -1
  80. package/build-types/entity-types/post-status.d.ts.map +1 -1
  81. package/build-types/entity-types/post.d.ts.map +1 -1
  82. package/build-types/entity-types/settings.d.ts.map +1 -1
  83. package/build-types/entity-types/sidebar.d.ts.map +1 -1
  84. package/build-types/entity-types/taxonomy.d.ts.map +1 -1
  85. package/build-types/entity-types/term.d.ts.map +1 -1
  86. package/build-types/entity-types/theme.d.ts.map +1 -1
  87. package/build-types/entity-types/type.d.ts.map +1 -1
  88. package/build-types/entity-types/user.d.ts.map +1 -1
  89. package/build-types/entity-types/widget-type.d.ts.map +1 -1
  90. package/build-types/entity-types/widget.d.ts.map +1 -1
  91. package/build-types/entity-types/wp-template-part.d.ts.map +1 -1
  92. package/build-types/entity-types/wp-template.d.ts.map +1 -1
  93. package/build-types/fetch/__experimental-fetch-url-data.d.ts +3 -0
  94. package/build-types/fetch/__experimental-fetch-url-data.d.ts.map +1 -1
  95. package/build-types/index.d.ts +17 -47
  96. package/build-types/index.d.ts.map +1 -1
  97. package/build-types/locks/actions.d.ts +1 -1
  98. package/build-types/locks/engine.d.ts +1 -1
  99. package/build-types/locks/utils.d.ts +1 -1
  100. package/build-types/locks/utils.d.ts.map +1 -1
  101. package/build-types/private-actions.d.ts +32 -1
  102. package/build-types/private-actions.d.ts.map +1 -1
  103. package/build-types/private-apis.d.ts +9 -0
  104. package/build-types/private-apis.d.ts.map +1 -1
  105. package/build-types/queried-data/actions.d.ts +1 -1
  106. package/build-types/queried-data/actions.d.ts.map +1 -1
  107. package/build-types/queried-data/get-query-parts.d.ts +21 -0
  108. package/build-types/queried-data/get-query-parts.d.ts.map +1 -1
  109. package/build-types/reducer.d.ts +3 -3
  110. package/build-types/reducer.d.ts.map +1 -1
  111. package/build-types/resolvers.d.ts +1 -1
  112. package/build-types/resolvers.d.ts.map +1 -1
  113. package/build-types/utils/crdt.d.ts +8 -0
  114. package/build-types/utils/crdt.d.ts.map +1 -1
  115. package/build-types/utils/get-template-info.d.ts +13 -0
  116. package/build-types/utils/get-template-info.d.ts.map +1 -0
  117. package/build-types/utils/get-template-part-icon.d.ts +9 -0
  118. package/build-types/utils/get-template-part-icon.d.ts.map +1 -0
  119. package/build-types/utils/log-entity-deprecation.d.ts +1 -1
  120. package/build-types/utils/log-entity-deprecation.d.ts.map +1 -1
  121. package/build-types/utils/on-sub-key.d.ts +4 -0
  122. package/build-types/utils/on-sub-key.d.ts.map +1 -0
  123. package/package.json +30 -25
  124. package/src/actions.js +92 -1
  125. package/src/components/entities-saved-states/entity-record-item.js +60 -0
  126. package/src/components/entities-saved-states/entity-type-list.js +130 -0
  127. package/src/components/entities-saved-states/hooks/use-is-dirty.js +101 -0
  128. package/src/components/entities-saved-states/index.js +242 -0
  129. package/src/components/entities-saved-states/style.scss +70 -0
  130. package/src/components/entities-saved-states/test/use-is-dirty.js +88 -0
  131. package/src/entities.js +3 -1
  132. package/src/private-actions.js +127 -0
  133. package/src/private-apis.ts +12 -0
  134. package/src/style.scss +1 -0
  135. package/src/test/actions.js +587 -0
  136. package/src/utils/crdt.ts +1 -1
  137. package/src/utils/get-template-info.js +52 -0
  138. package/src/utils/get-template-part-icon.js +30 -0
  139. package/src/utils/test/get-template-info.js +224 -0
@@ -0,0 +1,242 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import clsx from 'clsx';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { Button, Flex, FlexItem } from '@wordpress/components';
10
+ import { __, _n, sprintf } from '@wordpress/i18n';
11
+ import {
12
+ useCallback,
13
+ useRef,
14
+ createInterpolateElement,
15
+ } from '@wordpress/element';
16
+ import {
17
+ __experimentalUseDialog as useDialog,
18
+ useInstanceId,
19
+ } from '@wordpress/compose';
20
+ import { useDispatch } from '@wordpress/data';
21
+
22
+ /**
23
+ * Internal dependencies
24
+ */
25
+ import EntityTypeList from './entity-type-list';
26
+ import { useIsDirty } from './hooks/use-is-dirty';
27
+ import { unlock } from '../../lock-unlock';
28
+ import { STORE_NAME } from '../../name';
29
+
30
+ /**
31
+ * Renders the component for managing saved states of entities.
32
+ *
33
+ * @param {Object} props The component props.
34
+ * @param {Function} props.close The function to close the dialog.
35
+ * @param {boolean=} props.renderDialog Whether to render the component with modal dialog behavior.
36
+ * @param {string} props.variant Changes the layout of the component. When an `inline` value is provided, the action buttons are rendered at the end of the component instead of at the start.
37
+ *
38
+ * @return {React.ReactNode} The rendered component.
39
+ */
40
+ export default function EntitiesSavedStates( {
41
+ close,
42
+ renderDialog,
43
+ variant,
44
+ } ) {
45
+ const isDirtyProps = useIsDirty();
46
+ return (
47
+ <EntitiesSavedStatesExtensible
48
+ close={ close }
49
+ renderDialog={ renderDialog }
50
+ variant={ variant }
51
+ { ...isDirtyProps }
52
+ />
53
+ );
54
+ }
55
+
56
+ /**
57
+ * Renders a panel for saving entities with dirty records.
58
+ *
59
+ * @param {Object} props The component props.
60
+ * @param {string} props.additionalPrompt Additional prompt to display.
61
+ * @param {Function} props.close Function to close the panel.
62
+ * @param {Function} props.onSave Function to call when saving entities.
63
+ * @param {boolean} props.saveEnabled Flag indicating if save is enabled.
64
+ * @param {string} props.saveLabel Label for the save button.
65
+ * @param {boolean} props.renderDialog Whether to render the component with modal dialog behavior.
66
+ * @param {Array} props.dirtyEntityRecords Array of dirty entity records.
67
+ * @param {boolean} props.isDirty Flag indicating if there are dirty entities.
68
+ * @param {Function} props.setUnselectedEntities Function to set unselected entities.
69
+ * @param {Array} props.unselectedEntities Array of unselected entities.
70
+ * @param {string} props.variant Changes the layout of the component. When an `inline` value is provided, the action buttons are rendered at the end of the component instead of at the start.
71
+ * @param {string} props.successNoticeContent Optional custom success notice content.
72
+ *
73
+ * @return {React.ReactNode} The rendered component.
74
+ */
75
+ export function EntitiesSavedStatesExtensible( {
76
+ additionalPrompt = undefined,
77
+ close,
78
+ onSave = undefined,
79
+ saveEnabled: saveEnabledProp = undefined,
80
+ saveLabel = __( 'Save' ),
81
+ renderDialog,
82
+ dirtyEntityRecords,
83
+ isDirty,
84
+ setUnselectedEntities,
85
+ unselectedEntities,
86
+ variant = 'default',
87
+ successNoticeContent,
88
+ } ) {
89
+ const saveButtonRef = useRef();
90
+ const { saveDirtyEntities } = unlock( useDispatch( STORE_NAME ) );
91
+ // To group entities by type.
92
+ const partitionedSavables = dirtyEntityRecords.reduce( ( acc, record ) => {
93
+ const { name } = record;
94
+ if ( ! acc[ name ] ) {
95
+ acc[ name ] = [];
96
+ }
97
+ acc[ name ].push( record );
98
+ return acc;
99
+ }, {} );
100
+
101
+ // Sort entity groups.
102
+ const {
103
+ site: siteSavables,
104
+ wp_template: templateSavables,
105
+ wp_template_part: templatePartSavables,
106
+ ...contentSavables
107
+ } = partitionedSavables;
108
+ const sortedPartitionedSavables = [
109
+ siteSavables,
110
+ templateSavables,
111
+ templatePartSavables,
112
+ ...Object.values( contentSavables ),
113
+ ].filter( Array.isArray );
114
+
115
+ const saveEnabled = saveEnabledProp ?? isDirty;
116
+ // Explicitly define this with no argument passed. Using `close` on
117
+ // its own will use the event object in place of the expected saved entities.
118
+ const dismissPanel = useCallback( () => close(), [ close ] );
119
+
120
+ const [ saveDialogRef, saveDialogProps ] = useDialog( {
121
+ onClose: () => dismissPanel(),
122
+ } );
123
+ const dialogLabelId = useInstanceId(
124
+ EntitiesSavedStatesExtensible,
125
+ 'entities-saved-states__panel-label'
126
+ );
127
+ const dialogDescriptionId = useInstanceId(
128
+ EntitiesSavedStatesExtensible,
129
+ 'entities-saved-states__panel-description'
130
+ );
131
+
132
+ const selectItemsToSaveDescription = !! dirtyEntityRecords.length
133
+ ? __( 'Select the items you want to save.' )
134
+ : undefined;
135
+
136
+ const isInline = variant === 'inline';
137
+
138
+ const actionButtons = (
139
+ <>
140
+ <FlexItem
141
+ isBlock={ isInline ? false : true }
142
+ as={ Button }
143
+ variant={ isInline ? 'tertiary' : 'secondary' }
144
+ size={ isInline ? undefined : 'compact' }
145
+ onClick={ dismissPanel }
146
+ >
147
+ { __( 'Cancel' ) }
148
+ </FlexItem>
149
+ <FlexItem
150
+ isBlock={ isInline ? false : true }
151
+ as={ Button }
152
+ ref={ saveButtonRef }
153
+ variant="primary"
154
+ size={ isInline ? undefined : 'compact' }
155
+ disabled={ ! saveEnabled }
156
+ accessibleWhenDisabled
157
+ onClick={ () =>
158
+ saveDirtyEntities( {
159
+ onSave,
160
+ dirtyEntityRecords,
161
+ entitiesToSkip: unselectedEntities,
162
+ close,
163
+ successNoticeContent,
164
+ } )
165
+ }
166
+ className="entities-saved-states__save-button"
167
+ >
168
+ { saveLabel }
169
+ </FlexItem>
170
+ </>
171
+ );
172
+
173
+ return (
174
+ <div
175
+ ref={ renderDialog ? saveDialogRef : undefined }
176
+ { ...( renderDialog && saveDialogProps ) }
177
+ className={ clsx( 'entities-saved-states__panel', {
178
+ 'is-inline': isInline,
179
+ } ) }
180
+ role={ renderDialog ? 'dialog' : undefined }
181
+ aria-labelledby={ renderDialog ? dialogLabelId : undefined }
182
+ aria-describedby={ renderDialog ? dialogDescriptionId : undefined }
183
+ >
184
+ { ! isInline && (
185
+ <Flex className="entities-saved-states__panel-header" gap={ 2 }>
186
+ { actionButtons }
187
+ </Flex>
188
+ ) }
189
+
190
+ <div className="entities-saved-states__text-prompt">
191
+ <div className="entities-saved-states__text-prompt--header-wrapper">
192
+ <strong
193
+ id={ renderDialog ? dialogLabelId : undefined }
194
+ className="entities-saved-states__text-prompt--header"
195
+ >
196
+ { __( 'Are you ready to save?' ) }
197
+ </strong>
198
+ </div>
199
+ <div id={ renderDialog ? dialogDescriptionId : undefined }>
200
+ { additionalPrompt }
201
+ <p className="entities-saved-states__text-prompt--changes-count">
202
+ { isDirty
203
+ ? createInterpolateElement(
204
+ sprintf(
205
+ /* translators: %d: number of site changes waiting to be saved. */
206
+ _n(
207
+ 'There is <strong>%d site change</strong> waiting to be saved.',
208
+ 'There are <strong>%d site changes</strong> waiting to be saved.',
209
+ dirtyEntityRecords.length
210
+ ),
211
+ dirtyEntityRecords.length
212
+ ),
213
+ { strong: <strong /> }
214
+ )
215
+ : selectItemsToSaveDescription }
216
+ </p>
217
+ </div>
218
+ </div>
219
+
220
+ { sortedPartitionedSavables.map( ( list ) => {
221
+ return (
222
+ <EntityTypeList
223
+ key={ list[ 0 ].name }
224
+ list={ list }
225
+ unselectedEntities={ unselectedEntities }
226
+ setUnselectedEntities={ setUnselectedEntities }
227
+ />
228
+ );
229
+ } ) }
230
+
231
+ { isInline && (
232
+ <Flex
233
+ direction="row"
234
+ justify="flex-end"
235
+ className="entities-saved-states__panel-footer"
236
+ >
237
+ { actionButtons }
238
+ </Flex>
239
+ ) }
240
+ </div>
241
+ );
242
+ }
@@ -0,0 +1,70 @@
1
+ @use "@wordpress/base-styles/colors" as *;
2
+ @use "@wordpress/base-styles/variables" as *;
3
+
4
+ .entities-saved-states__panel-header {
5
+ box-sizing: border-box;
6
+ background: $white;
7
+ padding-left: $grid-unit-20;
8
+ padding-right: $grid-unit-20;
9
+ height: $header-height;
10
+ border-bottom: $border-width solid $gray-300;
11
+ }
12
+
13
+ .entities-saved-states__text-prompt {
14
+ padding: $grid-unit-20;
15
+ padding-bottom: $grid-unit-05;
16
+ .entities-saved-states__text-prompt--header {
17
+ display: block;
18
+ margin-bottom: $grid-unit-15;
19
+ }
20
+ }
21
+
22
+ .entities-saved-states__panel.is-inline {
23
+ .entities-saved-states__text-prompt {
24
+ padding: 0;
25
+ }
26
+
27
+ .entities-saved-states__panel-body {
28
+ padding-left: 0;
29
+ padding-right: 0;
30
+ border: 0;
31
+
32
+ > h2 {
33
+ margin-left: -1 * $grid-unit-20;
34
+ margin-right: -1 * $grid-unit-20;
35
+ margin-bottom: 0;
36
+
37
+ button {
38
+ font-size: $font-size-x-small;
39
+ text-transform: uppercase;
40
+ }
41
+ }
42
+ }
43
+
44
+ .entities-saved-states__text-prompt--header-wrapper {
45
+ display: none;
46
+ }
47
+
48
+ .entities-saved-states__text-prompt--changes-count {
49
+ margin-top: 0;
50
+ margin-bottom: $grid-unit-10;
51
+ }
52
+
53
+ .entities-saved-states__panel-footer {
54
+ margin-top: $grid-unit-20;
55
+ }
56
+ }
57
+
58
+ .entities-saved-states__change-control {
59
+ flex: 1;
60
+ }
61
+
62
+ .entities-saved-states__changes {
63
+ font-size: $default-font-size;
64
+ margin: $grid-unit-05 $grid-unit-20 0 $grid-unit-30;
65
+ list-style: disc;
66
+
67
+ li {
68
+ margin-bottom: $grid-unit-05;
69
+ }
70
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { act, renderHook } from '@testing-library/react';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import { useIsDirty } from '../hooks/use-is-dirty';
10
+
11
+ jest.mock( '@wordpress/data', () => {
12
+ return {
13
+ useSelect: jest.fn().mockImplementation( ( fn ) => {
14
+ const select = () => {
15
+ return {
16
+ __experimentalGetDirtyEntityRecords: jest
17
+ .fn()
18
+ .mockReturnValue( [
19
+ {
20
+ kind: 'root',
21
+ name: 'site',
22
+ title: 'title',
23
+ property: 'property',
24
+ },
25
+ {
26
+ kind: 'postType',
27
+ name: 'post',
28
+ title: 'title',
29
+ property: 'property',
30
+ },
31
+ ] ),
32
+ getEntityRecordEdits: jest.fn().mockReturnValue( {
33
+ title: 'My Site',
34
+ } ),
35
+ getEntityConfig: jest.fn().mockReturnValue( {
36
+ meta: { labels: { title: 'Title' } },
37
+ } ),
38
+ };
39
+ };
40
+ return fn( select );
41
+ } ),
42
+ };
43
+ } );
44
+
45
+ describe( 'useIsDirty', () => {
46
+ it( 'should calculate dirtyEntityRecords', () => {
47
+ const { result } = renderHook( () => useIsDirty() );
48
+ expect( result.current.dirtyEntityRecords ).toEqual( [
49
+ {
50
+ kind: 'postType',
51
+ name: 'post',
52
+ property: 'property',
53
+ title: 'title',
54
+ },
55
+ { kind: 'root', name: 'site', property: 'title', title: 'Title' },
56
+ ] );
57
+ } );
58
+ it( 'should return `isDirty: true` when there are changes', () => {
59
+ const { result } = renderHook( () => useIsDirty() );
60
+ expect( result.current.isDirty ).toBeTruthy();
61
+ } );
62
+ it( 'should return `isDirty: false` when there are NO changes', async () => {
63
+ const { result } = renderHook( () => useIsDirty() );
64
+ act( () => {
65
+ result.current.setUnselectedEntities(
66
+ {
67
+ kind: 'postType',
68
+ name: 'post',
69
+ key: 'key',
70
+ property: 'property',
71
+ },
72
+ false
73
+ );
74
+ } );
75
+ act( () => {
76
+ result.current.setUnselectedEntities(
77
+ {
78
+ kind: 'root',
79
+ name: 'site',
80
+ key: 'key',
81
+ property: 'property',
82
+ },
83
+ false
84
+ );
85
+ } );
86
+ expect( result.current.isDirty ).toBeFalsy();
87
+ } );
88
+ } );
package/src/entities.js CHANGED
@@ -49,8 +49,10 @@ export const rootEntitiesConfig = [
49
49
  'description',
50
50
  'gmt_offset',
51
51
  'home',
52
+ 'image_max_bit_depth',
52
53
  'image_sizes',
53
54
  'image_size_threshold',
55
+ 'image_strip_meta',
54
56
  'name',
55
57
  'site_icon',
56
58
  'site_icon_url',
@@ -323,7 +325,7 @@ export const prePersistPostType = async (
323
325
  if ( persistedRecord ) {
324
326
  const objectType = `postType/${ name }`;
325
327
  const objectId = persistedRecord.id;
326
- const serializedDoc = getSyncManager()?.createPersistedCRDTDoc(
328
+ const serializedDoc = await getSyncManager()?.createPersistedCRDTDoc(
327
329
  objectType,
328
330
  objectId
329
331
  );
@@ -2,6 +2,9 @@
2
2
  * WordPress dependencies
3
3
  */
4
4
  import apiFetch from '@wordpress/api-fetch';
5
+ import { store as noticesStore } from '@wordpress/notices';
6
+ import { store as blockEditorStore } from '@wordpress/block-editor';
7
+ import { __ } from '@wordpress/i18n';
5
8
 
6
9
  /**
7
10
  * Internal dependencies
@@ -247,3 +250,127 @@ export function setSyncConnectionStatus( kind, name, key, status ) {
247
250
  status,
248
251
  };
249
252
  }
253
+
254
+ /**
255
+ * Save entity records marked as dirty.
256
+ *
257
+ * @param {Object} options Options for the action.
258
+ * @param {Function} [options.onSave] Callback when saving happens.
259
+ * @param {object[]} [options.dirtyEntityRecords] Array of dirty entities.
260
+ * @param {object[]} [options.entitiesToSkip] Array of entities to skip saving.
261
+ * @param {Function} [options.close] Callback when the actions is called. It should be consolidated with `onSave`.
262
+ * @param {string} [options.successNoticeContent] Optional custom success notice content. Defaults to 'Site updated.'.
263
+ */
264
+ export const saveDirtyEntities =
265
+ ( {
266
+ onSave,
267
+ dirtyEntityRecords = [],
268
+ entitiesToSkip = [],
269
+ close,
270
+ successNoticeContent,
271
+ } = {} ) =>
272
+ ( { registry } ) => {
273
+ const PUBLISH_ON_SAVE_ENTITIES = [
274
+ { kind: 'postType', name: 'wp_navigation' },
275
+ ];
276
+ const saveNoticeId = 'site-editor-save-success';
277
+ const homeUrl = registry
278
+ .select( STORE_NAME )
279
+ .getEntityRecord( 'root', '__unstableBase' )?.home;
280
+ registry.dispatch( noticesStore ).removeNotice( saveNoticeId );
281
+ const entitiesToSave = dirtyEntityRecords.filter(
282
+ ( { kind, name, key, property } ) => {
283
+ return ! entitiesToSkip.some(
284
+ ( elt ) =>
285
+ elt.kind === kind &&
286
+ elt.name === name &&
287
+ elt.key === key &&
288
+ elt.property === property
289
+ );
290
+ }
291
+ );
292
+ close?.( entitiesToSave );
293
+ const siteItemsToSave = [];
294
+ const pendingSavedRecords = [];
295
+ entitiesToSave.forEach( ( { kind, name, key, property } ) => {
296
+ if ( 'root' === kind && 'site' === name ) {
297
+ siteItemsToSave.push( property );
298
+ } else {
299
+ if (
300
+ PUBLISH_ON_SAVE_ENTITIES.some(
301
+ ( typeToPublish ) =>
302
+ typeToPublish.kind === kind &&
303
+ typeToPublish.name === name
304
+ )
305
+ ) {
306
+ registry
307
+ .dispatch( STORE_NAME )
308
+ .editEntityRecord( kind, name, key, {
309
+ status: 'publish',
310
+ } );
311
+ }
312
+
313
+ pendingSavedRecords.push(
314
+ registry
315
+ .dispatch( STORE_NAME )
316
+ .saveEditedEntityRecord( kind, name, key )
317
+ );
318
+ }
319
+ } );
320
+ if ( siteItemsToSave.length ) {
321
+ pendingSavedRecords.push(
322
+ registry
323
+ .dispatch( STORE_NAME )
324
+ .__experimentalSaveSpecifiedEntityEdits(
325
+ 'root',
326
+ 'site',
327
+ undefined,
328
+ siteItemsToSave
329
+ )
330
+ );
331
+ }
332
+ registry
333
+ .dispatch( blockEditorStore )
334
+ .__unstableMarkLastChangeAsPersistent();
335
+
336
+ Promise.all( pendingSavedRecords )
337
+ .then( async ( values ) => {
338
+ if ( onSave ) {
339
+ await onSave();
340
+ }
341
+ return values;
342
+ } )
343
+ .then( ( values ) => {
344
+ if (
345
+ values.some( ( value ) => typeof value === 'undefined' )
346
+ ) {
347
+ registry
348
+ .dispatch( noticesStore )
349
+ .createErrorNotice( __( 'Saving failed.' ) );
350
+ } else {
351
+ registry
352
+ .dispatch( noticesStore )
353
+ .createSuccessNotice(
354
+ successNoticeContent || __( 'Site updated.' ),
355
+ {
356
+ type: 'snackbar',
357
+ id: saveNoticeId,
358
+ actions: [
359
+ {
360
+ label: __( 'View site' ),
361
+ url: homeUrl,
362
+ openInNewTab: true,
363
+ },
364
+ ],
365
+ }
366
+ );
367
+ }
368
+ } )
369
+ .catch( ( error ) =>
370
+ registry
371
+ .dispatch( noticesStore )
372
+ .createErrorNotice(
373
+ `${ __( 'Saving failed.' ) } ${ error }`
374
+ )
375
+ );
376
+ };
@@ -10,6 +10,13 @@ import {
10
10
  useOnCollaboratorLeave,
11
11
  useOnPostSave,
12
12
  } from './hooks/use-post-editor-awareness-state';
13
+ import { getTemplateInfo } from './utils/get-template-info';
14
+ import { getTemplatePartIcon } from './utils/get-template-part-icon';
15
+ import {
16
+ EntitiesSavedStatesExtensible,
17
+ default as EntitiesSavedStates,
18
+ } from './components/entities-saved-states';
19
+ import { useIsDirty as useEntitiesSavedStatesIsDirty } from './components/entities-saved-states/hooks/use-is-dirty';
13
20
  import { lock } from './lock-unlock';
14
21
  import { retrySyncConnection } from './sync';
15
22
  import {
@@ -18,6 +25,11 @@ import {
18
25
  } from './utils/crdt-user-selections';
19
26
 
20
27
  const lockedApis = {
28
+ EntitiesSavedStates,
29
+ EntitiesSavedStatesExtensible,
30
+ getTemplateInfo,
31
+ getTemplatePartIcon,
32
+ useEntitiesSavedStatesIsDirty,
21
33
  useEntityRecordsWithPermissions,
22
34
  RECEIVE_INTERMEDIATE_RESULTS,
23
35
  retrySyncConnection,
package/src/style.scss ADDED
@@ -0,0 +1 @@
1
+ @use "./components/entities-saved-states/style.scss" as *;