@wordpress/core-data 7.51.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.
- package/CHANGELOG.md +6 -0
- package/build/actions.cjs +54 -1
- package/build/actions.cjs.map +3 -3
- package/build/components/entities-saved-states/entity-record-item.cjs +67 -0
- package/build/components/entities-saved-states/entity-record-item.cjs.map +7 -0
- package/build/components/entities-saved-states/entity-type-list.cjs +135 -0
- package/build/components/entities-saved-states/entity-type-list.cjs.map +7 -0
- package/build/components/entities-saved-states/hooks/use-is-dirty.cjs +97 -0
- package/build/components/entities-saved-states/hooks/use-is-dirty.cjs.map +7 -0
- package/build/components/entities-saved-states/index.cjs +215 -0
- package/build/components/entities-saved-states/index.cjs.map +7 -0
- package/build/entities.cjs +2 -0
- package/build/entities.cjs.map +2 -2
- package/build/private-actions.cjs +85 -0
- package/build/private-actions.cjs.map +3 -3
- package/build/private-apis.cjs +19 -0
- package/build/private-apis.cjs.map +3 -3
- package/build/utils/crdt.cjs +4 -2
- package/build/utils/crdt.cjs.map +2 -2
- package/build/utils/get-template-info.cjs +53 -0
- package/build/utils/get-template-info.cjs.map +7 -0
- package/build/utils/get-template-part-icon.cjs +43 -0
- package/build/utils/get-template-part-icon.cjs.map +7 -0
- package/build-module/actions.mjs +57 -1
- package/build-module/actions.mjs.map +2 -2
- package/build-module/components/entities-saved-states/entity-record-item.mjs +46 -0
- package/build-module/components/entities-saved-states/entity-record-item.mjs.map +7 -0
- package/build-module/components/entities-saved-states/entity-type-list.mjs +104 -0
- package/build-module/components/entities-saved-states/entity-type-list.mjs.map +7 -0
- package/build-module/components/entities-saved-states/hooks/use-is-dirty.mjs +72 -0
- package/build-module/components/entities-saved-states/hooks/use-is-dirty.mjs.map +7 -0
- package/build-module/components/entities-saved-states/index.mjs +187 -0
- package/build-module/components/entities-saved-states/index.mjs.map +7 -0
- package/build-module/entities.mjs +2 -0
- package/build-module/entities.mjs.map +2 -2
- package/build-module/private-actions.mjs +84 -0
- package/build-module/private-actions.mjs.map +2 -2
- package/build-module/private-apis.mjs +12 -0
- package/build-module/private-apis.mjs.map +2 -2
- package/build-module/utils/crdt.mjs +2 -1
- package/build-module/utils/crdt.mjs.map +2 -2
- package/build-module/utils/get-template-info.mjs +28 -0
- package/build-module/utils/get-template-info.mjs.map +7 -0
- package/build-module/utils/get-template-part-icon.mjs +24 -0
- package/build-module/utils/get-template-part-icon.mjs.map +7 -0
- package/build-types/actions.d.ts.map +1 -1
- package/build-types/components/entities-saved-states/entity-record-item.d.ts +6 -0
- package/build-types/components/entities-saved-states/entity-record-item.d.ts.map +1 -0
- package/build-types/components/entities-saved-states/entity-type-list.d.ts +6 -0
- package/build-types/components/entities-saved-states/entity-type-list.d.ts.map +1 -0
- package/build-types/components/entities-saved-states/hooks/use-is-dirty.d.ts +11 -0
- package/build-types/components/entities-saved-states/hooks/use-is-dirty.d.ts.map +1 -0
- package/build-types/components/entities-saved-states/index.d.ts +49 -0
- package/build-types/components/entities-saved-states/index.d.ts.map +1 -0
- package/build-types/entities.d.ts.map +1 -1
- package/build-types/private-actions.d.ts +19 -0
- package/build-types/private-actions.d.ts.map +1 -1
- package/build-types/private-apis.d.ts +9 -0
- package/build-types/private-apis.d.ts.map +1 -1
- package/build-types/utils/crdt.d.ts +8 -0
- package/build-types/utils/crdt.d.ts.map +1 -1
- package/build-types/utils/get-template-info.d.ts +13 -0
- package/build-types/utils/get-template-info.d.ts.map +1 -0
- package/build-types/utils/get-template-part-icon.d.ts +9 -0
- package/build-types/utils/get-template-part-icon.d.ts.map +1 -0
- package/build-types/utils/on-sub-key.d.ts +4 -0
- package/build-types/utils/on-sub-key.d.ts.map +1 -0
- package/package.json +27 -22
- package/src/actions.js +92 -1
- package/src/components/entities-saved-states/entity-record-item.js +60 -0
- package/src/components/entities-saved-states/entity-type-list.js +130 -0
- package/src/components/entities-saved-states/hooks/use-is-dirty.js +101 -0
- package/src/components/entities-saved-states/index.js +242 -0
- package/src/components/entities-saved-states/style.scss +70 -0
- package/src/components/entities-saved-states/test/use-is-dirty.js +88 -0
- package/src/entities.js +2 -0
- package/src/private-actions.js +127 -0
- package/src/private-apis.ts +12 -0
- package/src/style.scss +1 -0
- package/src/test/actions.js +587 -0
- package/src/utils/crdt.ts +1 -1
- package/src/utils/get-template-info.js +52 -0
- package/src/utils/get-template-part-icon.js +30 -0
- package/src/utils/test/get-template-info.js +224 -0
package/src/actions.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* External dependencies
|
|
3
3
|
*/
|
|
4
|
+
import fastDeepEqual from 'fast-deep-equal/es6/index.js';
|
|
4
5
|
import { v4 as uuid } from 'uuid';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -24,11 +25,71 @@ import {
|
|
|
24
25
|
getSyncManager,
|
|
25
26
|
} from './sync';
|
|
26
27
|
import logEntityDeprecation from './utils/log-entity-deprecation';
|
|
28
|
+
import {
|
|
29
|
+
getRawValue,
|
|
30
|
+
POST_META_KEY_FOR_CRDT_DOC_PERSISTENCE,
|
|
31
|
+
} from './utils/crdt';
|
|
27
32
|
|
|
28
33
|
function addTitleToAutoDraft( record ) {
|
|
29
34
|
return record.status === 'auto-draft' ? { ...record, title: '' } : record;
|
|
30
35
|
}
|
|
31
36
|
|
|
37
|
+
// Post meta is applied to the CRDT one subkey at a time, so compare the save
|
|
38
|
+
// response at the same granularity to avoid carrying stale sibling values.
|
|
39
|
+
function getServerMutatedMetaFields( updatedMeta, persistedMeta, syncedMeta ) {
|
|
40
|
+
const baseline = { ...persistedMeta, ...syncedMeta };
|
|
41
|
+
|
|
42
|
+
return Object.fromEntries(
|
|
43
|
+
Object.entries( updatedMeta ?? {} ).filter( ( [ key, value ] ) => {
|
|
44
|
+
if ( key === POST_META_KEY_FOR_CRDT_DOC_PERSISTENCE ) {
|
|
45
|
+
// The persisted CRDT snapshot may change on save and is
|
|
46
|
+
// intentionally excluded from CRDT meta synchronization, so it
|
|
47
|
+
// is not a server mutation.
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return ! fastDeepEqual( value, baseline[ key ] );
|
|
52
|
+
} )
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getServerMutatedFields(
|
|
57
|
+
updatedRecord,
|
|
58
|
+
persistedRecord,
|
|
59
|
+
syncedChanges
|
|
60
|
+
) {
|
|
61
|
+
return Object.fromEntries(
|
|
62
|
+
Object.entries( updatedRecord ).flatMap( ( [ key, value ] ) => {
|
|
63
|
+
if ( key === 'meta' ) {
|
|
64
|
+
const serverMutatedMeta = getServerMutatedMetaFields(
|
|
65
|
+
value,
|
|
66
|
+
persistedRecord.meta,
|
|
67
|
+
syncedChanges.meta
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
return Object.keys( serverMutatedMeta ).length
|
|
71
|
+
? [ [ key, serverMutatedMeta ] ]
|
|
72
|
+
: [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const baseline =
|
|
76
|
+
key in syncedChanges
|
|
77
|
+
? syncedChanges[ key ]
|
|
78
|
+
: persistedRecord[ key ];
|
|
79
|
+
|
|
80
|
+
// The save response nests raw attributes as `{ raw, rendered }`
|
|
81
|
+
// while the baseline holds raw strings; compare raw values so the
|
|
82
|
+
// shape difference does not read as a server mutation.
|
|
83
|
+
const wasServerMutated = ! fastDeepEqual(
|
|
84
|
+
getRawValue( value ) ?? value,
|
|
85
|
+
getRawValue( baseline ) ?? baseline
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return wasServerMutated ? [ [ key, value ] ] : [];
|
|
89
|
+
} )
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
32
93
|
/**
|
|
33
94
|
* Returns an action object used in signalling that authors have been received.
|
|
34
95
|
* Ignored from documentation as it's internal to the data store.
|
|
@@ -759,6 +820,23 @@ export const saveEntityRecord =
|
|
|
759
820
|
);
|
|
760
821
|
}
|
|
761
822
|
} else {
|
|
823
|
+
// `saveEntityRecord` can be called directly, bypassing
|
|
824
|
+
// `editEntityRecord`, so make sure its changes enter the
|
|
825
|
+
// CRDT before the persisted document is created below.
|
|
826
|
+
if (
|
|
827
|
+
entityConfig.syncConfig &&
|
|
828
|
+
! __unstableSkipSyncUpdate &&
|
|
829
|
+
! isNewRecord &&
|
|
830
|
+
persistedRecord
|
|
831
|
+
) {
|
|
832
|
+
getSyncManager()?.update(
|
|
833
|
+
`${ kind }/${ name }`,
|
|
834
|
+
recordId,
|
|
835
|
+
record,
|
|
836
|
+
LOCAL_UNDO_IGNORED_ORIGIN
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
|
|
762
840
|
let edits = record;
|
|
763
841
|
if ( entityConfig.__unstablePrePersist ) {
|
|
764
842
|
edits = {
|
|
@@ -783,12 +861,25 @@ export const saveEntityRecord =
|
|
|
783
861
|
edits
|
|
784
862
|
);
|
|
785
863
|
if ( entityConfig.syncConfig ) {
|
|
864
|
+
let syncChanges;
|
|
865
|
+
if ( __unstableSkipSyncUpdate ) {
|
|
866
|
+
syncChanges = {};
|
|
867
|
+
} else if ( isNewRecord || ! persistedRecord ) {
|
|
868
|
+
syncChanges = updatedRecord;
|
|
869
|
+
} else {
|
|
870
|
+
syncChanges = getServerMutatedFields(
|
|
871
|
+
updatedRecord,
|
|
872
|
+
persistedRecord,
|
|
873
|
+
record
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
|
|
786
877
|
// Use an untracked origin so that the save
|
|
787
878
|
// response does not create undo levels.
|
|
788
879
|
getSyncManager()?.update(
|
|
789
880
|
`${ kind }/${ name }`,
|
|
790
881
|
recordId,
|
|
791
|
-
|
|
882
|
+
syncChanges,
|
|
792
883
|
LOCAL_UNDO_IGNORED_ORIGIN,
|
|
793
884
|
{ isSave: true }
|
|
794
885
|
);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { CheckboxControl, PanelRow } from '@wordpress/components';
|
|
5
|
+
import { __ } from '@wordpress/i18n';
|
|
6
|
+
import { useSelect } from '@wordpress/data';
|
|
7
|
+
import { decodeEntities } from '@wordpress/html-entities';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Internal dependencies
|
|
11
|
+
*/
|
|
12
|
+
import { STORE_NAME } from '../../name';
|
|
13
|
+
import { getTemplateInfo } from '../../utils/get-template-info';
|
|
14
|
+
|
|
15
|
+
export default function EntityRecordItem( { record, checked, onChange } ) {
|
|
16
|
+
const { name, kind, title, key } = record;
|
|
17
|
+
|
|
18
|
+
// Handle templates that might use default descriptive titles.
|
|
19
|
+
const { entityRecordTitle } = useSelect(
|
|
20
|
+
( select ) => {
|
|
21
|
+
if ( 'postType' !== kind || 'wp_template' !== name ) {
|
|
22
|
+
return {
|
|
23
|
+
entityRecordTitle: title,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const template = select( STORE_NAME ).getEditedEntityRecord(
|
|
28
|
+
kind,
|
|
29
|
+
name,
|
|
30
|
+
key
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const { default_template_types: templateTypes = [] } =
|
|
34
|
+
select( STORE_NAME ).getCurrentTheme() ?? {};
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
entityRecordTitle: getTemplateInfo( {
|
|
38
|
+
template,
|
|
39
|
+
templateTypes,
|
|
40
|
+
} ).title,
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
[ name, kind, title, key ]
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<>
|
|
48
|
+
<PanelRow>
|
|
49
|
+
<CheckboxControl
|
|
50
|
+
label={
|
|
51
|
+
decodeEntities( entityRecordTitle ) || __( 'Untitled' )
|
|
52
|
+
}
|
|
53
|
+
checked={ checked }
|
|
54
|
+
onChange={ onChange }
|
|
55
|
+
className="entities-saved-states__change-control"
|
|
56
|
+
/>
|
|
57
|
+
</PanelRow>
|
|
58
|
+
</>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { __ } from '@wordpress/i18n';
|
|
5
|
+
import { useSelect } from '@wordpress/data';
|
|
6
|
+
import { PanelBody, PanelRow } from '@wordpress/components';
|
|
7
|
+
import { getGlobalStylesChanges } from '@wordpress/global-styles-engine';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Internal dependencies
|
|
11
|
+
*/
|
|
12
|
+
import EntityRecordItem from './entity-record-item';
|
|
13
|
+
import { STORE_NAME } from '../../name';
|
|
14
|
+
|
|
15
|
+
function getEntityDescription( entity, count ) {
|
|
16
|
+
switch ( entity ) {
|
|
17
|
+
case 'site':
|
|
18
|
+
return 1 === count
|
|
19
|
+
? __( 'This change will affect your whole site.' )
|
|
20
|
+
: __( 'These changes will affect your whole site.' );
|
|
21
|
+
case 'wp_template':
|
|
22
|
+
return __(
|
|
23
|
+
'This change will affect other parts of your site that use this template.'
|
|
24
|
+
);
|
|
25
|
+
case 'page':
|
|
26
|
+
case 'post':
|
|
27
|
+
return __( 'The following has been modified.' );
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function GlobalStylesDescription( { record } ) {
|
|
32
|
+
const { editedRecord, savedRecord } = useSelect(
|
|
33
|
+
( select ) => {
|
|
34
|
+
const { getEditedEntityRecord, getEntityRecord } =
|
|
35
|
+
select( STORE_NAME );
|
|
36
|
+
return {
|
|
37
|
+
editedRecord: getEditedEntityRecord(
|
|
38
|
+
record.kind,
|
|
39
|
+
record.name,
|
|
40
|
+
record.key
|
|
41
|
+
),
|
|
42
|
+
savedRecord: getEntityRecord(
|
|
43
|
+
record.kind,
|
|
44
|
+
record.name,
|
|
45
|
+
record.key
|
|
46
|
+
),
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
[ record.kind, record.name, record.key ]
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const globalStylesChanges = getGlobalStylesChanges(
|
|
53
|
+
editedRecord,
|
|
54
|
+
savedRecord,
|
|
55
|
+
{
|
|
56
|
+
maxResults: 10,
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
return globalStylesChanges.length ? (
|
|
60
|
+
<ul className="entities-saved-states__changes">
|
|
61
|
+
{ globalStylesChanges.map( ( change ) => (
|
|
62
|
+
<li key={ change }>{ change }</li>
|
|
63
|
+
) ) }
|
|
64
|
+
</ul>
|
|
65
|
+
) : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function EntityDescription( { record, count } ) {
|
|
69
|
+
if ( 'globalStyles' === record?.name ) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const description = getEntityDescription( record?.name, count );
|
|
73
|
+
return description ? <PanelRow>{ description }</PanelRow> : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export default function EntityTypeList( {
|
|
77
|
+
list,
|
|
78
|
+
unselectedEntities,
|
|
79
|
+
setUnselectedEntities,
|
|
80
|
+
} ) {
|
|
81
|
+
const count = list.length;
|
|
82
|
+
const firstRecord = list[ 0 ];
|
|
83
|
+
const entityConfig = useSelect(
|
|
84
|
+
( select ) =>
|
|
85
|
+
select( STORE_NAME ).getEntityConfig(
|
|
86
|
+
firstRecord.kind,
|
|
87
|
+
firstRecord.name
|
|
88
|
+
),
|
|
89
|
+
[ firstRecord.kind, firstRecord.name ]
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
let entityLabel = entityConfig.label;
|
|
93
|
+
if ( firstRecord?.name === 'wp_template_part' ) {
|
|
94
|
+
entityLabel =
|
|
95
|
+
1 === count ? __( 'Template Part' ) : __( 'Template Parts' );
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<PanelBody
|
|
100
|
+
title={ entityLabel }
|
|
101
|
+
initialOpen
|
|
102
|
+
className="entities-saved-states__panel-body"
|
|
103
|
+
>
|
|
104
|
+
<EntityDescription record={ firstRecord } count={ count } />
|
|
105
|
+
{ list.map( ( record ) => {
|
|
106
|
+
return (
|
|
107
|
+
<EntityRecordItem
|
|
108
|
+
key={ record.key || record.property }
|
|
109
|
+
record={ record }
|
|
110
|
+
checked={
|
|
111
|
+
! unselectedEntities.some(
|
|
112
|
+
( elt ) =>
|
|
113
|
+
elt.kind === record.kind &&
|
|
114
|
+
elt.name === record.name &&
|
|
115
|
+
elt.key === record.key &&
|
|
116
|
+
elt.property === record.property
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
onChange={ ( value ) =>
|
|
120
|
+
setUnselectedEntities( record, value )
|
|
121
|
+
}
|
|
122
|
+
/>
|
|
123
|
+
);
|
|
124
|
+
} ) }
|
|
125
|
+
{ 'globalStyles' === firstRecord?.name && (
|
|
126
|
+
<GlobalStylesDescription record={ firstRecord } />
|
|
127
|
+
) }
|
|
128
|
+
</PanelBody>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { useSelect } from '@wordpress/data';
|
|
5
|
+
import { useMemo, useState } from '@wordpress/element';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Internal dependencies
|
|
9
|
+
*/
|
|
10
|
+
import { STORE_NAME } from '../../../name';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Custom hook that determines if any entities are dirty (edited) and provides a way to manage selected/unselected entities.
|
|
14
|
+
*
|
|
15
|
+
* @return {Object} An object containing the following properties:
|
|
16
|
+
* - dirtyEntityRecords: An array of dirty entity records.
|
|
17
|
+
* - isDirty: A boolean indicating if there are any dirty entity records.
|
|
18
|
+
* - setUnselectedEntities: A function to set the unselected entities.
|
|
19
|
+
* - unselectedEntities: An array of unselected entities.
|
|
20
|
+
*/
|
|
21
|
+
export const useIsDirty = () => {
|
|
22
|
+
const { editedEntities, siteEdits, siteEntityConfig } = useSelect(
|
|
23
|
+
( select ) => {
|
|
24
|
+
const {
|
|
25
|
+
__experimentalGetDirtyEntityRecords,
|
|
26
|
+
getEntityRecordEdits,
|
|
27
|
+
getEntityConfig,
|
|
28
|
+
} = select( STORE_NAME );
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
editedEntities: __experimentalGetDirtyEntityRecords(),
|
|
32
|
+
siteEdits: getEntityRecordEdits( 'root', 'site' ),
|
|
33
|
+
siteEntityConfig: getEntityConfig( 'root', 'site' ),
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
[]
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const dirtyEntityRecords = useMemo( () => {
|
|
40
|
+
// Remove site object and decouple into its edited pieces.
|
|
41
|
+
const editedEntitiesWithoutSite = editedEntities.filter(
|
|
42
|
+
( record ) => ! ( record.kind === 'root' && record.name === 'site' )
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const siteEntityLabels = siteEntityConfig?.meta?.labels ?? {};
|
|
46
|
+
const {
|
|
47
|
+
site_logo: siteLogoEdit,
|
|
48
|
+
site_icon: siteIconEdit,
|
|
49
|
+
...otherSiteEdits
|
|
50
|
+
} = siteEdits ?? {};
|
|
51
|
+
const orderedSiteProperties = [
|
|
52
|
+
siteLogoEdit !== undefined && 'site_logo',
|
|
53
|
+
siteIconEdit !== undefined && 'site_icon',
|
|
54
|
+
...Object.keys( otherSiteEdits ),
|
|
55
|
+
].filter( Boolean );
|
|
56
|
+
const editedSiteEntities = orderedSiteProperties.map(
|
|
57
|
+
( property ) => ( {
|
|
58
|
+
kind: 'root',
|
|
59
|
+
name: 'site',
|
|
60
|
+
title: siteEntityLabels[ property ] || property,
|
|
61
|
+
property,
|
|
62
|
+
} )
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return [ ...editedEntitiesWithoutSite, ...editedSiteEntities ];
|
|
66
|
+
}, [ editedEntities, siteEdits, siteEntityConfig ] );
|
|
67
|
+
|
|
68
|
+
// Unchecked entities to be ignored by save function.
|
|
69
|
+
const [ unselectedEntities, _setUnselectedEntities ] = useState( [] );
|
|
70
|
+
|
|
71
|
+
const setUnselectedEntities = (
|
|
72
|
+
{ kind, name, key, property },
|
|
73
|
+
checked
|
|
74
|
+
) => {
|
|
75
|
+
if ( checked ) {
|
|
76
|
+
_setUnselectedEntities(
|
|
77
|
+
unselectedEntities.filter(
|
|
78
|
+
( elt ) =>
|
|
79
|
+
elt.kind !== kind ||
|
|
80
|
+
elt.name !== name ||
|
|
81
|
+
elt.key !== key ||
|
|
82
|
+
elt.property !== property
|
|
83
|
+
)
|
|
84
|
+
);
|
|
85
|
+
} else {
|
|
86
|
+
_setUnselectedEntities( [
|
|
87
|
+
...unselectedEntities,
|
|
88
|
+
{ kind, name, key, property },
|
|
89
|
+
] );
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const isDirty = dirtyEntityRecords.length - unselectedEntities.length > 0;
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
dirtyEntityRecords,
|
|
97
|
+
isDirty,
|
|
98
|
+
setUnselectedEntities,
|
|
99
|
+
unselectedEntities,
|
|
100
|
+
};
|
|
101
|
+
};
|
|
@@ -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
|
+
}
|