@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.
- package/CHANGELOG.md +12 -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 +3 -1
- 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 +3 -1
- 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 +3 -3
- package/build-types/actions.d.ts.map +1 -1
- package/build-types/awareness/base-awareness.d.ts +3 -0
- package/build-types/awareness/base-awareness.d.ts.map +1 -1
- package/build-types/awareness/post-editor-awareness.d.ts +7 -0
- package/build-types/awareness/post-editor-awareness.d.ts.map +1 -1
- package/build-types/batch/create-batch.d.ts +1 -1
- package/build-types/batch/create-batch.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 +2 -2
- package/build-types/entities.d.ts.map +1 -1
- package/build-types/entity-provider.d.ts +1 -1
- package/build-types/entity-provider.d.ts.map +1 -1
- package/build-types/entity-types/attachment.d.ts.map +1 -1
- package/build-types/entity-types/base-entity-records.d.ts.map +1 -1
- package/build-types/entity-types/base.d.ts.map +1 -1
- package/build-types/entity-types/comment.d.ts.map +1 -1
- package/build-types/entity-types/font-collection.d.ts.map +1 -1
- package/build-types/entity-types/font-family.d.ts.map +1 -1
- package/build-types/entity-types/global-styles-revision.d.ts.map +1 -1
- package/build-types/entity-types/icon.d.ts.map +1 -1
- package/build-types/entity-types/menu-location.d.ts.map +1 -1
- package/build-types/entity-types/nav-menu-item.d.ts.map +1 -1
- package/build-types/entity-types/nav-menu.d.ts.map +1 -1
- package/build-types/entity-types/page.d.ts.map +1 -1
- package/build-types/entity-types/plugin.d.ts.map +1 -1
- package/build-types/entity-types/post-revision.d.ts.map +1 -1
- package/build-types/entity-types/post-status.d.ts.map +1 -1
- package/build-types/entity-types/post.d.ts.map +1 -1
- package/build-types/entity-types/settings.d.ts.map +1 -1
- package/build-types/entity-types/sidebar.d.ts.map +1 -1
- package/build-types/entity-types/taxonomy.d.ts.map +1 -1
- package/build-types/entity-types/term.d.ts.map +1 -1
- package/build-types/entity-types/theme.d.ts.map +1 -1
- package/build-types/entity-types/type.d.ts.map +1 -1
- package/build-types/entity-types/user.d.ts.map +1 -1
- package/build-types/entity-types/widget-type.d.ts.map +1 -1
- package/build-types/entity-types/widget.d.ts.map +1 -1
- package/build-types/entity-types/wp-template-part.d.ts.map +1 -1
- package/build-types/entity-types/wp-template.d.ts.map +1 -1
- package/build-types/fetch/__experimental-fetch-url-data.d.ts +3 -0
- package/build-types/fetch/__experimental-fetch-url-data.d.ts.map +1 -1
- package/build-types/index.d.ts +17 -47
- package/build-types/index.d.ts.map +1 -1
- package/build-types/locks/actions.d.ts +1 -1
- package/build-types/locks/engine.d.ts +1 -1
- package/build-types/locks/utils.d.ts +1 -1
- package/build-types/locks/utils.d.ts.map +1 -1
- package/build-types/private-actions.d.ts +32 -1
- 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/queried-data/actions.d.ts +1 -1
- package/build-types/queried-data/actions.d.ts.map +1 -1
- package/build-types/queried-data/get-query-parts.d.ts +21 -0
- package/build-types/queried-data/get-query-parts.d.ts.map +1 -1
- package/build-types/reducer.d.ts +3 -3
- package/build-types/reducer.d.ts.map +1 -1
- package/build-types/resolvers.d.ts +1 -1
- package/build-types/resolvers.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/log-entity-deprecation.d.ts +1 -1
- package/build-types/utils/log-entity-deprecation.d.ts.map +1 -1
- 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 +30 -25
- 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 +3 -1
- 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/test/actions.js
CHANGED
|
@@ -973,6 +973,7 @@ describe( 'saveEntityRecord', () => {
|
|
|
973
973
|
'local-undo-ignored',
|
|
974
974
|
{ isSave: true }
|
|
975
975
|
);
|
|
976
|
+
expect( syncManager.update ).toHaveBeenCalledTimes( 1 );
|
|
976
977
|
expect( liveSyncState ).toEqual( {
|
|
977
978
|
isSaved: true,
|
|
978
979
|
title: 'synced title',
|
|
@@ -980,6 +981,592 @@ describe( 'saveEntityRecord', () => {
|
|
|
980
981
|
expect( result ).toBe( staleSaveResponse );
|
|
981
982
|
} );
|
|
982
983
|
|
|
984
|
+
it( 'only passes server-mutated fields to SyncManager#update after saving', async () => {
|
|
985
|
+
const persistedRecord = {
|
|
986
|
+
id: 10,
|
|
987
|
+
title: 'Initial title',
|
|
988
|
+
content: 'Initial content',
|
|
989
|
+
template: 'single',
|
|
990
|
+
modified: '2026-07-01T00:00:00',
|
|
991
|
+
};
|
|
992
|
+
const edits = {
|
|
993
|
+
id: 10,
|
|
994
|
+
content: 'Updated content',
|
|
995
|
+
};
|
|
996
|
+
const configs = [
|
|
997
|
+
{
|
|
998
|
+
name: 'post',
|
|
999
|
+
kind: 'postType',
|
|
1000
|
+
baseURL: '/wp/v2/posts',
|
|
1001
|
+
syncConfig: {},
|
|
1002
|
+
},
|
|
1003
|
+
];
|
|
1004
|
+
const syncManager = {
|
|
1005
|
+
update: jest.fn(),
|
|
1006
|
+
};
|
|
1007
|
+
const select = {
|
|
1008
|
+
getRawEntityRecord: () => persistedRecord,
|
|
1009
|
+
};
|
|
1010
|
+
const resolveSelect = { getEntitiesConfig: jest.fn( () => configs ) };
|
|
1011
|
+
const updatedRecord = {
|
|
1012
|
+
...persistedRecord,
|
|
1013
|
+
content: edits.content,
|
|
1014
|
+
modified: '2026-07-02T00:00:00',
|
|
1015
|
+
};
|
|
1016
|
+
apiFetch.mockImplementation( () => updatedRecord );
|
|
1017
|
+
getSyncManager.mockReturnValue( syncManager );
|
|
1018
|
+
|
|
1019
|
+
const result = await saveEntityRecord(
|
|
1020
|
+
'postType',
|
|
1021
|
+
'post',
|
|
1022
|
+
edits
|
|
1023
|
+
)( { select, dispatch, resolveSelect } );
|
|
1024
|
+
|
|
1025
|
+
expect( syncManager.update ).toHaveBeenNthCalledWith(
|
|
1026
|
+
1,
|
|
1027
|
+
'postType/post',
|
|
1028
|
+
10,
|
|
1029
|
+
edits,
|
|
1030
|
+
'local-undo-ignored'
|
|
1031
|
+
);
|
|
1032
|
+
expect( syncManager.update ).toHaveBeenCalledWith(
|
|
1033
|
+
'postType/post',
|
|
1034
|
+
10,
|
|
1035
|
+
{
|
|
1036
|
+
modified: '2026-07-02T00:00:00',
|
|
1037
|
+
},
|
|
1038
|
+
'local-undo-ignored',
|
|
1039
|
+
{ isSave: true }
|
|
1040
|
+
);
|
|
1041
|
+
expect( syncManager.update ).toHaveBeenCalledTimes( 2 );
|
|
1042
|
+
expect( result ).toBe( updatedRecord );
|
|
1043
|
+
} );
|
|
1044
|
+
|
|
1045
|
+
it( 'does not pass unchanged meta fields to SyncManager#update after saving', async () => {
|
|
1046
|
+
const persistedRecord = {
|
|
1047
|
+
id: 10,
|
|
1048
|
+
content: 'Initial content',
|
|
1049
|
+
meta: {
|
|
1050
|
+
plugin_value: 'persisted',
|
|
1051
|
+
peer_value: 'persisted',
|
|
1052
|
+
_crdt_document: 'old-doc',
|
|
1053
|
+
},
|
|
1054
|
+
};
|
|
1055
|
+
const edits = {
|
|
1056
|
+
id: 10,
|
|
1057
|
+
content: 'Updated content',
|
|
1058
|
+
};
|
|
1059
|
+
const configs = [
|
|
1060
|
+
{
|
|
1061
|
+
name: 'post',
|
|
1062
|
+
kind: 'postType',
|
|
1063
|
+
baseURL: '/wp/v2/posts',
|
|
1064
|
+
syncConfig: {},
|
|
1065
|
+
__unstablePrePersist: async () => ( {
|
|
1066
|
+
meta: { _crdt_document: 'new-doc' },
|
|
1067
|
+
} ),
|
|
1068
|
+
},
|
|
1069
|
+
];
|
|
1070
|
+
const syncManager = {
|
|
1071
|
+
update: jest.fn(),
|
|
1072
|
+
};
|
|
1073
|
+
const select = {
|
|
1074
|
+
getRawEntityRecord: () => persistedRecord,
|
|
1075
|
+
};
|
|
1076
|
+
const resolveSelect = { getEntitiesConfig: jest.fn( () => configs ) };
|
|
1077
|
+
const updatedRecord = {
|
|
1078
|
+
...persistedRecord,
|
|
1079
|
+
content: edits.content,
|
|
1080
|
+
meta: {
|
|
1081
|
+
plugin_value: 'persisted',
|
|
1082
|
+
peer_value: 'persisted',
|
|
1083
|
+
_crdt_document: 'new-doc',
|
|
1084
|
+
},
|
|
1085
|
+
};
|
|
1086
|
+
apiFetch.mockImplementation( () => updatedRecord );
|
|
1087
|
+
getSyncManager.mockReturnValue( syncManager );
|
|
1088
|
+
|
|
1089
|
+
const result = await saveEntityRecord(
|
|
1090
|
+
'postType',
|
|
1091
|
+
'post',
|
|
1092
|
+
edits
|
|
1093
|
+
)( { select, dispatch, resolveSelect } );
|
|
1094
|
+
|
|
1095
|
+
expect( syncManager.update ).toHaveBeenNthCalledWith(
|
|
1096
|
+
2,
|
|
1097
|
+
'postType/post',
|
|
1098
|
+
10,
|
|
1099
|
+
{},
|
|
1100
|
+
'local-undo-ignored',
|
|
1101
|
+
{ isSave: true }
|
|
1102
|
+
);
|
|
1103
|
+
expect( syncManager.update ).toHaveBeenCalledTimes( 2 );
|
|
1104
|
+
expect( result ).toBe( updatedRecord );
|
|
1105
|
+
} );
|
|
1106
|
+
|
|
1107
|
+
it( 'passes only server-mutated meta fields to SyncManager#update after saving', async () => {
|
|
1108
|
+
const persistedRecord = {
|
|
1109
|
+
id: 10,
|
|
1110
|
+
meta: {
|
|
1111
|
+
unchanged: 'persisted',
|
|
1112
|
+
peer_value: 'persisted',
|
|
1113
|
+
edited: 'initial',
|
|
1114
|
+
server_mutated: 'before',
|
|
1115
|
+
settings: { color: 'blue', size: 'small' },
|
|
1116
|
+
},
|
|
1117
|
+
};
|
|
1118
|
+
const edits = {
|
|
1119
|
+
id: 10,
|
|
1120
|
+
meta: {
|
|
1121
|
+
edited: 'local',
|
|
1122
|
+
server_mutated: 'Needs Normalizing',
|
|
1123
|
+
},
|
|
1124
|
+
};
|
|
1125
|
+
const configs = [
|
|
1126
|
+
{
|
|
1127
|
+
name: 'post',
|
|
1128
|
+
kind: 'postType',
|
|
1129
|
+
baseURL: '/wp/v2/posts',
|
|
1130
|
+
syncConfig: {},
|
|
1131
|
+
__unstablePrePersist: async () => ( {
|
|
1132
|
+
meta: {
|
|
1133
|
+
...edits.meta,
|
|
1134
|
+
_crdt_document: 'new-doc',
|
|
1135
|
+
},
|
|
1136
|
+
} ),
|
|
1137
|
+
},
|
|
1138
|
+
];
|
|
1139
|
+
const syncManager = {
|
|
1140
|
+
update: jest.fn(),
|
|
1141
|
+
};
|
|
1142
|
+
const select = {
|
|
1143
|
+
getRawEntityRecord: () => persistedRecord,
|
|
1144
|
+
};
|
|
1145
|
+
const resolveSelect = { getEntitiesConfig: jest.fn( () => configs ) };
|
|
1146
|
+
const updatedRecord = {
|
|
1147
|
+
id: 10,
|
|
1148
|
+
meta: {
|
|
1149
|
+
unchanged: 'persisted',
|
|
1150
|
+
peer_value: 'persisted',
|
|
1151
|
+
edited: 'local',
|
|
1152
|
+
server_mutated: 'needs-normalizing',
|
|
1153
|
+
settings: { color: 'red', size: 'small' },
|
|
1154
|
+
_crdt_document: 'new-doc',
|
|
1155
|
+
},
|
|
1156
|
+
};
|
|
1157
|
+
apiFetch.mockImplementation( () => updatedRecord );
|
|
1158
|
+
getSyncManager.mockReturnValue( syncManager );
|
|
1159
|
+
|
|
1160
|
+
const result = await saveEntityRecord(
|
|
1161
|
+
'postType',
|
|
1162
|
+
'post',
|
|
1163
|
+
edits
|
|
1164
|
+
)( { select, dispatch, resolveSelect } );
|
|
1165
|
+
|
|
1166
|
+
expect( syncManager.update ).toHaveBeenNthCalledWith(
|
|
1167
|
+
1,
|
|
1168
|
+
'postType/post',
|
|
1169
|
+
10,
|
|
1170
|
+
edits,
|
|
1171
|
+
'local-undo-ignored'
|
|
1172
|
+
);
|
|
1173
|
+
expect( syncManager.update ).toHaveBeenNthCalledWith(
|
|
1174
|
+
2,
|
|
1175
|
+
'postType/post',
|
|
1176
|
+
10,
|
|
1177
|
+
{
|
|
1178
|
+
meta: {
|
|
1179
|
+
server_mutated: 'needs-normalizing',
|
|
1180
|
+
settings: { color: 'red', size: 'small' },
|
|
1181
|
+
},
|
|
1182
|
+
},
|
|
1183
|
+
'local-undo-ignored',
|
|
1184
|
+
{ isSave: true }
|
|
1185
|
+
);
|
|
1186
|
+
expect( syncManager.update ).toHaveBeenCalledTimes( 2 );
|
|
1187
|
+
expect( result ).toBe( updatedRecord );
|
|
1188
|
+
} );
|
|
1189
|
+
|
|
1190
|
+
it( 'syncs direct save changes before pre-persisting the record', async () => {
|
|
1191
|
+
const persistedRecord = {
|
|
1192
|
+
id: 10,
|
|
1193
|
+
status: 'auto-draft',
|
|
1194
|
+
template: '',
|
|
1195
|
+
};
|
|
1196
|
+
const edits = {
|
|
1197
|
+
id: 10,
|
|
1198
|
+
template: 'page-no-title',
|
|
1199
|
+
};
|
|
1200
|
+
const syncManager = {
|
|
1201
|
+
update: jest.fn(),
|
|
1202
|
+
};
|
|
1203
|
+
const prePersist = jest.fn( async () => {
|
|
1204
|
+
expect( syncManager.update ).toHaveBeenCalledTimes( 1 );
|
|
1205
|
+
expect( syncManager.update ).toHaveBeenLastCalledWith(
|
|
1206
|
+
'postType/page',
|
|
1207
|
+
10,
|
|
1208
|
+
edits,
|
|
1209
|
+
'local-undo-ignored'
|
|
1210
|
+
);
|
|
1211
|
+
|
|
1212
|
+
return { status: 'draft' };
|
|
1213
|
+
} );
|
|
1214
|
+
const configs = [
|
|
1215
|
+
{
|
|
1216
|
+
name: 'page',
|
|
1217
|
+
kind: 'postType',
|
|
1218
|
+
baseURL: '/wp/v2/pages',
|
|
1219
|
+
syncConfig: {},
|
|
1220
|
+
__unstablePrePersist: prePersist,
|
|
1221
|
+
},
|
|
1222
|
+
];
|
|
1223
|
+
const select = {
|
|
1224
|
+
getRawEntityRecord: () => persistedRecord,
|
|
1225
|
+
};
|
|
1226
|
+
const resolveSelect = { getEntitiesConfig: jest.fn( () => configs ) };
|
|
1227
|
+
const updatedRecord = {
|
|
1228
|
+
...persistedRecord,
|
|
1229
|
+
...edits,
|
|
1230
|
+
status: 'draft',
|
|
1231
|
+
};
|
|
1232
|
+
apiFetch.mockImplementation( () => updatedRecord );
|
|
1233
|
+
getSyncManager.mockReturnValue( syncManager );
|
|
1234
|
+
|
|
1235
|
+
const result = await saveEntityRecord(
|
|
1236
|
+
'postType',
|
|
1237
|
+
'page',
|
|
1238
|
+
edits
|
|
1239
|
+
)( { select, dispatch, resolveSelect } );
|
|
1240
|
+
|
|
1241
|
+
expect( prePersist ).toHaveBeenCalledWith( persistedRecord, edits );
|
|
1242
|
+
expect( apiFetch ).toHaveBeenCalledWith( {
|
|
1243
|
+
path: '/wp/v2/pages/10',
|
|
1244
|
+
method: 'PUT',
|
|
1245
|
+
data: { ...edits, status: 'draft' },
|
|
1246
|
+
} );
|
|
1247
|
+
expect( syncManager.update ).toHaveBeenNthCalledWith(
|
|
1248
|
+
2,
|
|
1249
|
+
'postType/page',
|
|
1250
|
+
10,
|
|
1251
|
+
{ status: 'draft' },
|
|
1252
|
+
'local-undo-ignored',
|
|
1253
|
+
{ isSave: true }
|
|
1254
|
+
);
|
|
1255
|
+
expect( syncManager.update ).toHaveBeenCalledTimes( 2 );
|
|
1256
|
+
expect( result ).toBe( updatedRecord );
|
|
1257
|
+
} );
|
|
1258
|
+
|
|
1259
|
+
it( 'does not mark pre-synced direct save changes as saved when the request fails', async () => {
|
|
1260
|
+
const persistedRecord = {
|
|
1261
|
+
id: 10,
|
|
1262
|
+
template: '',
|
|
1263
|
+
};
|
|
1264
|
+
const edits = {
|
|
1265
|
+
id: 10,
|
|
1266
|
+
template: 'page-no-title',
|
|
1267
|
+
};
|
|
1268
|
+
const configs = [
|
|
1269
|
+
{
|
|
1270
|
+
name: 'page',
|
|
1271
|
+
kind: 'postType',
|
|
1272
|
+
baseURL: '/wp/v2/pages',
|
|
1273
|
+
syncConfig: {},
|
|
1274
|
+
},
|
|
1275
|
+
];
|
|
1276
|
+
const syncManager = {
|
|
1277
|
+
update: jest.fn(),
|
|
1278
|
+
};
|
|
1279
|
+
const select = {
|
|
1280
|
+
getRawEntityRecord: () => persistedRecord,
|
|
1281
|
+
};
|
|
1282
|
+
const resolveSelect = { getEntitiesConfig: jest.fn( () => configs ) };
|
|
1283
|
+
const error = new Error( 'API error' );
|
|
1284
|
+
apiFetch.mockRejectedValue( error );
|
|
1285
|
+
getSyncManager.mockReturnValue( syncManager );
|
|
1286
|
+
|
|
1287
|
+
await expect(
|
|
1288
|
+
saveEntityRecord( 'postType', 'page', edits, {
|
|
1289
|
+
throwOnError: true,
|
|
1290
|
+
} )( { select, dispatch, resolveSelect } )
|
|
1291
|
+
).rejects.toBe( error );
|
|
1292
|
+
|
|
1293
|
+
expect( syncManager.update ).toHaveBeenCalledTimes( 1 );
|
|
1294
|
+
expect( syncManager.update ).toHaveBeenCalledWith(
|
|
1295
|
+
'postType/page',
|
|
1296
|
+
10,
|
|
1297
|
+
edits,
|
|
1298
|
+
'local-undo-ignored'
|
|
1299
|
+
);
|
|
1300
|
+
} );
|
|
1301
|
+
|
|
1302
|
+
it( 'passes server-normalized edited fields to SyncManager#update after saving', async () => {
|
|
1303
|
+
const persistedRecord = {
|
|
1304
|
+
id: 10,
|
|
1305
|
+
slug: 'initial-slug',
|
|
1306
|
+
};
|
|
1307
|
+
const edits = {
|
|
1308
|
+
id: 10,
|
|
1309
|
+
slug: 'Needs Normalizing',
|
|
1310
|
+
};
|
|
1311
|
+
const configs = [
|
|
1312
|
+
{
|
|
1313
|
+
name: 'post',
|
|
1314
|
+
kind: 'postType',
|
|
1315
|
+
baseURL: '/wp/v2/posts',
|
|
1316
|
+
syncConfig: {},
|
|
1317
|
+
},
|
|
1318
|
+
];
|
|
1319
|
+
const syncManager = {
|
|
1320
|
+
update: jest.fn(),
|
|
1321
|
+
};
|
|
1322
|
+
const select = {
|
|
1323
|
+
getRawEntityRecord: () => persistedRecord,
|
|
1324
|
+
};
|
|
1325
|
+
const resolveSelect = { getEntitiesConfig: jest.fn( () => configs ) };
|
|
1326
|
+
const updatedRecord = {
|
|
1327
|
+
id: 10,
|
|
1328
|
+
slug: 'needs-normalizing',
|
|
1329
|
+
};
|
|
1330
|
+
apiFetch.mockImplementation( () => updatedRecord );
|
|
1331
|
+
getSyncManager.mockReturnValue( syncManager );
|
|
1332
|
+
|
|
1333
|
+
const result = await saveEntityRecord(
|
|
1334
|
+
'postType',
|
|
1335
|
+
'post',
|
|
1336
|
+
edits
|
|
1337
|
+
)( { select, dispatch, resolveSelect } );
|
|
1338
|
+
|
|
1339
|
+
expect( syncManager.update ).toHaveBeenCalledWith(
|
|
1340
|
+
'postType/post',
|
|
1341
|
+
10,
|
|
1342
|
+
{
|
|
1343
|
+
slug: 'needs-normalizing',
|
|
1344
|
+
},
|
|
1345
|
+
'local-undo-ignored',
|
|
1346
|
+
{ isSave: true }
|
|
1347
|
+
);
|
|
1348
|
+
expect( syncManager.update ).toHaveBeenCalledTimes( 2 );
|
|
1349
|
+
expect( result ).toBe( updatedRecord );
|
|
1350
|
+
} );
|
|
1351
|
+
|
|
1352
|
+
it( 'does not pass unchanged raw-attribute fields to SyncManager#update after saving', async () => {
|
|
1353
|
+
// The raw entity record holds raw strings while the save response
|
|
1354
|
+
// nests them as `{ raw, rendered }`; the comparison must not treat
|
|
1355
|
+
// that shape difference as a server mutation.
|
|
1356
|
+
const persistedRecord = {
|
|
1357
|
+
id: 10,
|
|
1358
|
+
title: 'Initial title',
|
|
1359
|
+
content: '<p>Initial content</p>',
|
|
1360
|
+
excerpt: 'Initial excerpt',
|
|
1361
|
+
slug: 'initial-slug',
|
|
1362
|
+
modified: '2026-07-01T00:00:00',
|
|
1363
|
+
};
|
|
1364
|
+
const edits = {
|
|
1365
|
+
id: 10,
|
|
1366
|
+
slug: 'updated-slug',
|
|
1367
|
+
};
|
|
1368
|
+
const configs = [
|
|
1369
|
+
{
|
|
1370
|
+
name: 'post',
|
|
1371
|
+
kind: 'postType',
|
|
1372
|
+
baseURL: '/wp/v2/posts',
|
|
1373
|
+
syncConfig: {},
|
|
1374
|
+
},
|
|
1375
|
+
];
|
|
1376
|
+
const syncManager = {
|
|
1377
|
+
update: jest.fn(),
|
|
1378
|
+
};
|
|
1379
|
+
const select = {
|
|
1380
|
+
getRawEntityRecord: () => persistedRecord,
|
|
1381
|
+
};
|
|
1382
|
+
const resolveSelect = { getEntitiesConfig: jest.fn( () => configs ) };
|
|
1383
|
+
const updatedRecord = {
|
|
1384
|
+
id: 10,
|
|
1385
|
+
title: { raw: 'Initial title', rendered: 'Initial title' },
|
|
1386
|
+
content: {
|
|
1387
|
+
raw: '<p>Initial content</p>',
|
|
1388
|
+
rendered: '<p>Initial content</p>',
|
|
1389
|
+
},
|
|
1390
|
+
excerpt: { raw: 'Initial excerpt', rendered: 'Initial excerpt' },
|
|
1391
|
+
slug: 'updated-slug',
|
|
1392
|
+
modified: '2026-07-02T00:00:00',
|
|
1393
|
+
};
|
|
1394
|
+
apiFetch.mockImplementation( () => updatedRecord );
|
|
1395
|
+
getSyncManager.mockReturnValue( syncManager );
|
|
1396
|
+
|
|
1397
|
+
const result = await saveEntityRecord(
|
|
1398
|
+
'postType',
|
|
1399
|
+
'post',
|
|
1400
|
+
edits
|
|
1401
|
+
)( { select, dispatch, resolveSelect } );
|
|
1402
|
+
|
|
1403
|
+
expect( syncManager.update ).toHaveBeenCalledWith(
|
|
1404
|
+
'postType/post',
|
|
1405
|
+
10,
|
|
1406
|
+
{
|
|
1407
|
+
modified: '2026-07-02T00:00:00',
|
|
1408
|
+
},
|
|
1409
|
+
'local-undo-ignored',
|
|
1410
|
+
{ isSave: true }
|
|
1411
|
+
);
|
|
1412
|
+
expect( syncManager.update ).toHaveBeenCalledTimes( 2 );
|
|
1413
|
+
expect( result ).toBe( updatedRecord );
|
|
1414
|
+
} );
|
|
1415
|
+
|
|
1416
|
+
it( 'does not pass edited raw-attribute fields to SyncManager#update when the server echoes them unchanged', async () => {
|
|
1417
|
+
const persistedRecord = {
|
|
1418
|
+
id: 10,
|
|
1419
|
+
content: '<p>Initial content</p>',
|
|
1420
|
+
modified: '2026-07-01T00:00:00',
|
|
1421
|
+
};
|
|
1422
|
+
const edits = {
|
|
1423
|
+
id: 10,
|
|
1424
|
+
content: '<p>Updated content</p>',
|
|
1425
|
+
};
|
|
1426
|
+
const configs = [
|
|
1427
|
+
{
|
|
1428
|
+
name: 'post',
|
|
1429
|
+
kind: 'postType',
|
|
1430
|
+
baseURL: '/wp/v2/posts',
|
|
1431
|
+
syncConfig: {},
|
|
1432
|
+
},
|
|
1433
|
+
];
|
|
1434
|
+
const syncManager = {
|
|
1435
|
+
update: jest.fn(),
|
|
1436
|
+
};
|
|
1437
|
+
const select = {
|
|
1438
|
+
getRawEntityRecord: () => persistedRecord,
|
|
1439
|
+
};
|
|
1440
|
+
const resolveSelect = { getEntitiesConfig: jest.fn( () => configs ) };
|
|
1441
|
+
const updatedRecord = {
|
|
1442
|
+
id: 10,
|
|
1443
|
+
content: {
|
|
1444
|
+
raw: edits.content,
|
|
1445
|
+
// The rendered value differs from the raw baseline, but only
|
|
1446
|
+
// raw values are compared.
|
|
1447
|
+
rendered: '<p class="rendered">Updated content</p>\n',
|
|
1448
|
+
},
|
|
1449
|
+
modified: '2026-07-02T00:00:00',
|
|
1450
|
+
};
|
|
1451
|
+
apiFetch.mockImplementation( () => updatedRecord );
|
|
1452
|
+
getSyncManager.mockReturnValue( syncManager );
|
|
1453
|
+
|
|
1454
|
+
const result = await saveEntityRecord(
|
|
1455
|
+
'postType',
|
|
1456
|
+
'post',
|
|
1457
|
+
edits
|
|
1458
|
+
)( { select, dispatch, resolveSelect } );
|
|
1459
|
+
|
|
1460
|
+
expect( syncManager.update ).toHaveBeenCalledWith(
|
|
1461
|
+
'postType/post',
|
|
1462
|
+
10,
|
|
1463
|
+
{
|
|
1464
|
+
modified: '2026-07-02T00:00:00',
|
|
1465
|
+
},
|
|
1466
|
+
'local-undo-ignored',
|
|
1467
|
+
{ isSave: true }
|
|
1468
|
+
);
|
|
1469
|
+
expect( result ).toBe( updatedRecord );
|
|
1470
|
+
} );
|
|
1471
|
+
|
|
1472
|
+
it( 'passes raw-attribute fields whose raw value the server mutated to SyncManager#update', async () => {
|
|
1473
|
+
const persistedRecord = {
|
|
1474
|
+
id: 10,
|
|
1475
|
+
content: '<p>Initial content</p>',
|
|
1476
|
+
};
|
|
1477
|
+
const edits = {
|
|
1478
|
+
id: 10,
|
|
1479
|
+
content: '<p>Content with <script>bad</script> markup</p>',
|
|
1480
|
+
};
|
|
1481
|
+
const configs = [
|
|
1482
|
+
{
|
|
1483
|
+
name: 'post',
|
|
1484
|
+
kind: 'postType',
|
|
1485
|
+
baseURL: '/wp/v2/posts',
|
|
1486
|
+
syncConfig: {},
|
|
1487
|
+
},
|
|
1488
|
+
];
|
|
1489
|
+
const syncManager = {
|
|
1490
|
+
update: jest.fn(),
|
|
1491
|
+
};
|
|
1492
|
+
const select = {
|
|
1493
|
+
getRawEntityRecord: () => persistedRecord,
|
|
1494
|
+
};
|
|
1495
|
+
const resolveSelect = { getEntitiesConfig: jest.fn( () => configs ) };
|
|
1496
|
+
// The server strips disallowed markup from the sent content.
|
|
1497
|
+
const updatedRecord = {
|
|
1498
|
+
id: 10,
|
|
1499
|
+
content: {
|
|
1500
|
+
raw: '<p>Content with bad markup</p>',
|
|
1501
|
+
rendered: '<p>Content with bad markup</p>',
|
|
1502
|
+
},
|
|
1503
|
+
};
|
|
1504
|
+
apiFetch.mockImplementation( () => updatedRecord );
|
|
1505
|
+
getSyncManager.mockReturnValue( syncManager );
|
|
1506
|
+
|
|
1507
|
+
const result = await saveEntityRecord(
|
|
1508
|
+
'postType',
|
|
1509
|
+
'post',
|
|
1510
|
+
edits
|
|
1511
|
+
)( { select, dispatch, resolveSelect } );
|
|
1512
|
+
|
|
1513
|
+
expect( syncManager.update ).toHaveBeenCalledWith(
|
|
1514
|
+
'postType/post',
|
|
1515
|
+
10,
|
|
1516
|
+
{
|
|
1517
|
+
content: updatedRecord.content,
|
|
1518
|
+
},
|
|
1519
|
+
'local-undo-ignored',
|
|
1520
|
+
{ isSave: true }
|
|
1521
|
+
);
|
|
1522
|
+
expect( result ).toBe( updatedRecord );
|
|
1523
|
+
} );
|
|
1524
|
+
|
|
1525
|
+
it( 'passes the full save response to SyncManager#update when the persisted record is missing', async () => {
|
|
1526
|
+
const edits = {
|
|
1527
|
+
id: 10,
|
|
1528
|
+
content: 'Updated content',
|
|
1529
|
+
};
|
|
1530
|
+
const configs = [
|
|
1531
|
+
{
|
|
1532
|
+
name: 'post',
|
|
1533
|
+
kind: 'postType',
|
|
1534
|
+
baseURL: '/wp/v2/posts',
|
|
1535
|
+
syncConfig: {},
|
|
1536
|
+
},
|
|
1537
|
+
];
|
|
1538
|
+
const syncManager = {
|
|
1539
|
+
update: jest.fn(),
|
|
1540
|
+
};
|
|
1541
|
+
const select = {
|
|
1542
|
+
getRawEntityRecord: () => undefined,
|
|
1543
|
+
};
|
|
1544
|
+
const resolveSelect = { getEntitiesConfig: jest.fn( () => configs ) };
|
|
1545
|
+
const updatedRecord = {
|
|
1546
|
+
id: 10,
|
|
1547
|
+
content: 'Updated content',
|
|
1548
|
+
template: 'single',
|
|
1549
|
+
};
|
|
1550
|
+
apiFetch.mockImplementation( () => updatedRecord );
|
|
1551
|
+
getSyncManager.mockReturnValue( syncManager );
|
|
1552
|
+
|
|
1553
|
+
const result = await saveEntityRecord(
|
|
1554
|
+
'postType',
|
|
1555
|
+
'post',
|
|
1556
|
+
edits
|
|
1557
|
+
)( { select, dispatch, resolveSelect } );
|
|
1558
|
+
|
|
1559
|
+
expect( syncManager.update ).toHaveBeenCalledWith(
|
|
1560
|
+
'postType/post',
|
|
1561
|
+
10,
|
|
1562
|
+
updatedRecord,
|
|
1563
|
+
'local-undo-ignored',
|
|
1564
|
+
{ isSave: true }
|
|
1565
|
+
);
|
|
1566
|
+
expect( syncManager.update ).toHaveBeenCalledTimes( 1 );
|
|
1567
|
+
expect( result ).toBe( updatedRecord );
|
|
1568
|
+
} );
|
|
1569
|
+
|
|
983
1570
|
it( 'triggers a PUT request for an existing record with a custom key', async () => {
|
|
984
1571
|
const postType = { slug: 'page', title: 'Pages' };
|
|
985
1572
|
const configs = [
|
package/src/utils/crdt.ts
CHANGED
|
@@ -577,7 +577,7 @@ export const defaultCollectionSyncConfig: SyncConfig = {
|
|
|
577
577
|
* @param {unknown} value The value to extract from.
|
|
578
578
|
* @return {string|undefined} The raw string value, or undefined if it could not be determined.
|
|
579
579
|
*/
|
|
580
|
-
function getRawValue( value?: unknown ): string | undefined {
|
|
580
|
+
export function getRawValue( value?: unknown ): string | undefined {
|
|
581
581
|
// Value may be a string property or a nested object with a `raw` property.
|
|
582
582
|
if ( 'string' === typeof value ) {
|
|
583
583
|
return value;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { layout } from '@wordpress/icons';
|
|
5
|
+
/**
|
|
6
|
+
* Internal dependencies
|
|
7
|
+
*/
|
|
8
|
+
import { getTemplatePartIcon } from './get-template-part-icon';
|
|
9
|
+
const EMPTY_OBJECT = {};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Helper function to retrieve the corresponding template info for a given template.
|
|
13
|
+
* @param {Object} params
|
|
14
|
+
* @param {Array} params.templateTypes
|
|
15
|
+
* @param {Array} [params.templateAreas]
|
|
16
|
+
* @param {Object} params.template
|
|
17
|
+
*/
|
|
18
|
+
export const getTemplateInfo = ( params ) => {
|
|
19
|
+
if ( ! params ) {
|
|
20
|
+
return EMPTY_OBJECT;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { templateTypes, templateAreas, template } = params;
|
|
24
|
+
|
|
25
|
+
const { description, slug, title, area } = template;
|
|
26
|
+
|
|
27
|
+
const { title: defaultTitle, description: defaultDescription } =
|
|
28
|
+
Object.values( templateTypes ).find( ( type ) => type.slug === slug ) ??
|
|
29
|
+
EMPTY_OBJECT;
|
|
30
|
+
|
|
31
|
+
const templateTitle = typeof title === 'string' ? title : title?.rendered;
|
|
32
|
+
const templateDescription =
|
|
33
|
+
typeof description === 'string' ? description : description?.raw;
|
|
34
|
+
|
|
35
|
+
const templateAreasWithIcon = templateAreas?.map( ( item ) => ( {
|
|
36
|
+
...item,
|
|
37
|
+
icon: getTemplatePartIcon( item.icon ),
|
|
38
|
+
} ) );
|
|
39
|
+
|
|
40
|
+
const templateIcon =
|
|
41
|
+
templateAreasWithIcon?.find( ( item ) => area === item.area )?.icon ||
|
|
42
|
+
layout;
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
title:
|
|
46
|
+
templateTitle && templateTitle !== slug
|
|
47
|
+
? templateTitle
|
|
48
|
+
: defaultTitle || slug,
|
|
49
|
+
description: templateDescription || defaultDescription,
|
|
50
|
+
icon: templateIcon,
|
|
51
|
+
};
|
|
52
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
header as headerIcon,
|
|
6
|
+
footer as footerIcon,
|
|
7
|
+
sidebar as sidebarIcon,
|
|
8
|
+
navigationOverlay as navigationOverlayIcon,
|
|
9
|
+
symbolFilled as symbolFilledIcon,
|
|
10
|
+
} from '@wordpress/icons';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Helper function to retrieve the corresponding icon by area name.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} areaOrIconName The area name (e.g., 'header', 'navigation-overlay').
|
|
16
|
+
*
|
|
17
|
+
* @return {Object} The corresponding icon.
|
|
18
|
+
*/
|
|
19
|
+
export function getTemplatePartIcon( areaOrIconName ) {
|
|
20
|
+
if ( 'header' === areaOrIconName ) {
|
|
21
|
+
return headerIcon;
|
|
22
|
+
} else if ( 'footer' === areaOrIconName ) {
|
|
23
|
+
return footerIcon;
|
|
24
|
+
} else if ( 'sidebar' === areaOrIconName ) {
|
|
25
|
+
return sidebarIcon;
|
|
26
|
+
} else if ( 'navigation-overlay' === areaOrIconName ) {
|
|
27
|
+
return navigationOverlayIcon;
|
|
28
|
+
}
|
|
29
|
+
return symbolFilledIcon;
|
|
30
|
+
}
|