@wordpress/core-data 7.41.0 → 7.41.2-next.v.202603161435.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +19 -0
- package/build/actions.cjs +25 -31
- package/build/actions.cjs.map +2 -2
- package/build/awareness/post-editor-awareness.cjs +34 -1
- package/build/awareness/post-editor-awareness.cjs.map +2 -2
- package/build/awareness/types.cjs.map +1 -1
- package/build/entities.cjs +3 -2
- package/build/entities.cjs.map +2 -2
- package/build/entity-provider.cjs +15 -6
- package/build/entity-provider.cjs.map +2 -2
- package/build/hooks/use-entity-prop.cjs +33 -2
- package/build/hooks/use-entity-prop.cjs.map +2 -2
- package/build/hooks/use-post-editor-awareness-state.cjs +83 -2
- package/build/hooks/use-post-editor-awareness-state.cjs.map +2 -2
- package/build/index.cjs +3 -0
- package/build/index.cjs.map +2 -2
- package/build/private-actions.cjs +1 -1
- package/build/private-actions.cjs.map +2 -2
- package/build/private-apis.cjs +3 -1
- package/build/private-apis.cjs.map +2 -2
- package/build/queried-data/actions.cjs +1 -1
- package/build/queried-data/actions.cjs.map +2 -2
- package/build/queried-data/reducer.cjs +19 -13
- package/build/queried-data/reducer.cjs.map +2 -2
- package/build/queried-data/selectors.cjs +7 -4
- package/build/queried-data/selectors.cjs.map +2 -2
- package/build/reducer.cjs +2 -1
- package/build/reducer.cjs.map +2 -2
- package/build/resolvers.cjs +114 -76
- package/build/resolvers.cjs.map +2 -2
- package/build/selectors.cjs +29 -0
- package/build/selectors.cjs.map +2 -2
- package/build/sync.cjs +3 -0
- package/build/sync.cjs.map +2 -2
- package/build/types.cjs +16 -0
- package/build/types.cjs.map +3 -3
- package/build/utils/crdt-blocks.cjs +22 -26
- package/build/utils/crdt-blocks.cjs.map +2 -2
- package/build/utils/crdt-user-selections.cjs +8 -5
- package/build/utils/crdt-user-selections.cjs.map +2 -2
- package/build/utils/crdt.cjs +1 -3
- package/build/utils/crdt.cjs.map +2 -2
- package/build/utils/index.cjs +3 -0
- package/build/utils/index.cjs.map +2 -2
- package/build/utils/normalize-query-for-resolution.cjs +35 -0
- package/build/utils/normalize-query-for-resolution.cjs.map +7 -0
- package/build/utils/user-permissions.cjs +1 -4
- package/build/utils/user-permissions.cjs.map +2 -2
- package/build-module/actions.mjs +30 -32
- package/build-module/actions.mjs.map +2 -2
- package/build-module/awareness/post-editor-awareness.mjs +34 -1
- package/build-module/awareness/post-editor-awareness.mjs.map +2 -2
- package/build-module/entities.mjs +3 -2
- package/build-module/entities.mjs.map +2 -2
- package/build-module/entity-provider.mjs +15 -6
- package/build-module/entity-provider.mjs.map +2 -2
- package/build-module/hooks/use-entity-prop.mjs +34 -3
- package/build-module/hooks/use-entity-prop.mjs.map +2 -2
- package/build-module/hooks/use-post-editor-awareness-state.mjs +80 -1
- package/build-module/hooks/use-post-editor-awareness-state.mjs.map +2 -2
- package/build-module/index.mjs +2 -0
- package/build-module/index.mjs.map +2 -2
- package/build-module/private-actions.mjs +1 -1
- package/build-module/private-actions.mjs.map +2 -2
- package/build-module/private-apis.mjs +6 -2
- package/build-module/private-apis.mjs.map +2 -2
- package/build-module/queried-data/actions.mjs +1 -1
- package/build-module/queried-data/actions.mjs.map +2 -2
- package/build-module/queried-data/reducer.mjs +19 -13
- package/build-module/queried-data/reducer.mjs.map +2 -2
- package/build-module/queried-data/selectors.mjs +7 -4
- package/build-module/queried-data/selectors.mjs.map +2 -2
- package/build-module/reducer.mjs +2 -1
- package/build-module/reducer.mjs.map +2 -2
- package/build-module/resolvers.mjs +116 -77
- package/build-module/resolvers.mjs.map +2 -2
- package/build-module/selectors.mjs +28 -0
- package/build-module/selectors.mjs.map +2 -2
- package/build-module/sync.mjs +2 -0
- package/build-module/sync.mjs.map +2 -2
- package/build-module/types.mjs +9 -0
- package/build-module/types.mjs.map +4 -4
- package/build-module/utils/crdt-blocks.mjs +22 -26
- package/build-module/utils/crdt-blocks.mjs.map +2 -2
- package/build-module/utils/crdt-user-selections.mjs +8 -5
- package/build-module/utils/crdt-user-selections.mjs.map +2 -2
- package/build-module/utils/crdt.mjs +1 -3
- package/build-module/utils/crdt.mjs.map +2 -2
- package/build-module/utils/index.mjs +2 -0
- package/build-module/utils/index.mjs.map +2 -2
- package/build-module/utils/normalize-query-for-resolution.mjs +14 -0
- package/build-module/utils/normalize-query-for-resolution.mjs.map +7 -0
- package/build-module/utils/user-permissions.mjs +1 -4
- package/build-module/utils/user-permissions.mjs.map +2 -2
- package/build-types/actions.d.ts.map +1 -1
- package/build-types/awareness/post-editor-awareness.d.ts.map +1 -1
- package/build-types/awareness/types.d.ts +1 -1
- package/build-types/awareness/types.d.ts.map +1 -1
- package/build-types/entities.d.ts +1 -1
- package/build-types/entities.d.ts.map +1 -1
- package/build-types/entity-provider.d.ts +11 -6
- package/build-types/entity-provider.d.ts.map +1 -1
- package/build-types/hooks/use-entity-prop.d.ts.map +1 -1
- package/build-types/hooks/use-post-editor-awareness-state.d.ts +34 -10
- package/build-types/hooks/use-post-editor-awareness-state.d.ts.map +1 -1
- package/build-types/index.d.ts +2 -0
- package/build-types/index.d.ts.map +1 -1
- package/build-types/private-apis.d.ts.map +1 -1
- package/build-types/queried-data/reducer.d.ts.map +1 -1
- package/build-types/queried-data/selectors.d.ts.map +1 -1
- package/build-types/reducer.d.ts.map +1 -1
- package/build-types/resolvers.d.ts +2 -1
- package/build-types/resolvers.d.ts.map +1 -1
- package/build-types/selectors.d.ts +17 -0
- package/build-types/selectors.d.ts.map +1 -1
- package/build-types/sync.d.ts +2 -2
- package/build-types/sync.d.ts.map +1 -1
- package/build-types/types.d.ts +15 -0
- package/build-types/types.d.ts.map +1 -1
- package/build-types/utils/crdt-blocks.d.ts.map +1 -1
- package/build-types/utils/crdt-user-selections.d.ts +10 -5
- package/build-types/utils/crdt-user-selections.d.ts.map +1 -1
- package/build-types/utils/crdt.d.ts.map +1 -1
- package/build-types/utils/index.d.ts +1 -0
- package/build-types/utils/normalize-query-for-resolution.d.ts +12 -0
- package/build-types/utils/normalize-query-for-resolution.d.ts.map +1 -0
- package/build-types/utils/user-permissions.d.ts.map +1 -1
- package/package.json +18 -18
- package/src/actions.js +49 -50
- package/src/awareness/post-editor-awareness.ts +93 -1
- package/src/awareness/test/post-editor-awareness.ts +35 -0
- package/src/awareness/types.ts +1 -1
- package/src/entities.js +2 -1
- package/src/entity-provider.js +24 -11
- package/src/hooks/test/use-post-editor-awareness-state.ts +443 -0
- package/src/hooks/use-entity-prop.js +43 -3
- package/src/hooks/use-post-editor-awareness-state.ts +159 -7
- package/src/index.js +1 -0
- package/src/private-actions.js +1 -1
- package/src/private-apis.js +6 -2
- package/src/queried-data/actions.js +1 -1
- package/src/queried-data/reducer.js +26 -14
- package/src/queried-data/selectors.js +12 -5
- package/src/queried-data/test/selectors.js +25 -0
- package/src/reducer.js +4 -1
- package/src/resolvers.js +141 -91
- package/src/selectors.ts +56 -0
- package/src/sync.ts +2 -0
- package/src/test/private-actions.js +1 -1
- package/src/test/resolvers.js +88 -14
- package/src/test/selectors.js +150 -0
- package/src/test/store.js +182 -0
- package/src/types.ts +19 -0
- package/src/utils/crdt-blocks.ts +47 -54
- package/src/utils/crdt-user-selections.ts +28 -16
- package/src/utils/crdt.ts +2 -7
- package/src/utils/index.js +1 -0
- package/src/utils/normalize-query-for-resolution.js +23 -0
- package/src/utils/test/crdt-blocks.ts +42 -24
- package/src/utils/user-permissions.js +4 -5
package/src/test/selectors.js
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
getCurrentUser,
|
|
24
24
|
getRevisions,
|
|
25
25
|
getRevision,
|
|
26
|
+
hasRevision,
|
|
26
27
|
} from '../selectors';
|
|
27
28
|
|
|
28
29
|
describe( 'getEntityRecord', () => {
|
|
@@ -1215,3 +1216,152 @@ describe( 'getRevision', () => {
|
|
|
1215
1216
|
} );
|
|
1216
1217
|
} );
|
|
1217
1218
|
} );
|
|
1219
|
+
|
|
1220
|
+
describe( 'hasRevision', () => {
|
|
1221
|
+
it( 'returns false if revision has not been received', () => {
|
|
1222
|
+
const state = deepFreeze( {
|
|
1223
|
+
entities: {
|
|
1224
|
+
records: {
|
|
1225
|
+
postType: {
|
|
1226
|
+
post: {
|
|
1227
|
+
revisions: {
|
|
1228
|
+
1: {
|
|
1229
|
+
items: {},
|
|
1230
|
+
itemIsComplete: {},
|
|
1231
|
+
queries: {},
|
|
1232
|
+
},
|
|
1233
|
+
},
|
|
1234
|
+
},
|
|
1235
|
+
},
|
|
1236
|
+
},
|
|
1237
|
+
},
|
|
1238
|
+
} );
|
|
1239
|
+
expect( hasRevision( state, 'postType', 'post', 1, 10 ) ).toBe( false );
|
|
1240
|
+
} );
|
|
1241
|
+
|
|
1242
|
+
it( 'returns false if parent record does not exist', () => {
|
|
1243
|
+
const state = deepFreeze( {
|
|
1244
|
+
entities: {
|
|
1245
|
+
records: {},
|
|
1246
|
+
},
|
|
1247
|
+
} );
|
|
1248
|
+
expect( hasRevision( state, 'postType', 'post', 1, 10 ) ).toBe( false );
|
|
1249
|
+
} );
|
|
1250
|
+
|
|
1251
|
+
it( 'returns true when full revision exists and no fields query', () => {
|
|
1252
|
+
const state = deepFreeze( {
|
|
1253
|
+
entities: {
|
|
1254
|
+
records: {
|
|
1255
|
+
postType: {
|
|
1256
|
+
post: {
|
|
1257
|
+
revisions: {
|
|
1258
|
+
1: {
|
|
1259
|
+
items: {
|
|
1260
|
+
default: {
|
|
1261
|
+
10: {
|
|
1262
|
+
id: 10,
|
|
1263
|
+
content: 'chicken',
|
|
1264
|
+
parent: 1,
|
|
1265
|
+
},
|
|
1266
|
+
},
|
|
1267
|
+
},
|
|
1268
|
+
itemIsComplete: {
|
|
1269
|
+
default: {
|
|
1270
|
+
10: true,
|
|
1271
|
+
},
|
|
1272
|
+
},
|
|
1273
|
+
queries: {},
|
|
1274
|
+
},
|
|
1275
|
+
},
|
|
1276
|
+
},
|
|
1277
|
+
},
|
|
1278
|
+
},
|
|
1279
|
+
},
|
|
1280
|
+
} );
|
|
1281
|
+
expect( hasRevision( state, 'postType', 'post', 1, 10 ) ).toBe( true );
|
|
1282
|
+
} );
|
|
1283
|
+
|
|
1284
|
+
it( 'returns true when requested fields exist on the revision', () => {
|
|
1285
|
+
const state = deepFreeze( {
|
|
1286
|
+
entities: {
|
|
1287
|
+
records: {
|
|
1288
|
+
postType: {
|
|
1289
|
+
post: {
|
|
1290
|
+
revisions: {
|
|
1291
|
+
1: {
|
|
1292
|
+
items: {
|
|
1293
|
+
default: {
|
|
1294
|
+
10: {
|
|
1295
|
+
id: 10,
|
|
1296
|
+
content: 'chicken',
|
|
1297
|
+
title: { raw: 'egg' },
|
|
1298
|
+
parent: 1,
|
|
1299
|
+
},
|
|
1300
|
+
},
|
|
1301
|
+
},
|
|
1302
|
+
itemIsComplete: {
|
|
1303
|
+
default: {
|
|
1304
|
+
10: true,
|
|
1305
|
+
},
|
|
1306
|
+
},
|
|
1307
|
+
queries: {},
|
|
1308
|
+
},
|
|
1309
|
+
},
|
|
1310
|
+
},
|
|
1311
|
+
},
|
|
1312
|
+
},
|
|
1313
|
+
},
|
|
1314
|
+
} );
|
|
1315
|
+
expect(
|
|
1316
|
+
hasRevision( state, 'postType', 'post', 1, 10, {
|
|
1317
|
+
_fields: [ 'id', 'content' ],
|
|
1318
|
+
} )
|
|
1319
|
+
).toBe( true );
|
|
1320
|
+
expect(
|
|
1321
|
+
hasRevision( state, 'postType', 'post', 1, 10, {
|
|
1322
|
+
_fields: [ 'id', 'title.raw' ],
|
|
1323
|
+
} )
|
|
1324
|
+
).toBe( true );
|
|
1325
|
+
} );
|
|
1326
|
+
|
|
1327
|
+
it( 'returns false when requested fields are missing', () => {
|
|
1328
|
+
const state = deepFreeze( {
|
|
1329
|
+
entities: {
|
|
1330
|
+
records: {
|
|
1331
|
+
postType: {
|
|
1332
|
+
post: {
|
|
1333
|
+
revisions: {
|
|
1334
|
+
1: {
|
|
1335
|
+
items: {
|
|
1336
|
+
default: {
|
|
1337
|
+
10: {
|
|
1338
|
+
id: 10,
|
|
1339
|
+
parent: 1,
|
|
1340
|
+
},
|
|
1341
|
+
},
|
|
1342
|
+
},
|
|
1343
|
+
itemIsComplete: {
|
|
1344
|
+
default: {
|
|
1345
|
+
10: true,
|
|
1346
|
+
},
|
|
1347
|
+
},
|
|
1348
|
+
queries: {},
|
|
1349
|
+
},
|
|
1350
|
+
},
|
|
1351
|
+
},
|
|
1352
|
+
},
|
|
1353
|
+
},
|
|
1354
|
+
},
|
|
1355
|
+
} );
|
|
1356
|
+
expect(
|
|
1357
|
+
hasRevision( state, 'postType', 'post', 1, 10, {
|
|
1358
|
+
_fields: [ 'id', 'content' ],
|
|
1359
|
+
} )
|
|
1360
|
+
).toBe( false );
|
|
1361
|
+
expect(
|
|
1362
|
+
hasRevision( state, 'postType', 'post', 1, 10, {
|
|
1363
|
+
_fields: [ 'id', 'title.raw' ],
|
|
1364
|
+
} )
|
|
1365
|
+
).toBe( false );
|
|
1366
|
+
} );
|
|
1367
|
+
} );
|
package/src/test/store.js
CHANGED
|
@@ -36,6 +36,10 @@ function createTestRegistry() {
|
|
|
36
36
|
__unstable_rest_base: 'posts',
|
|
37
37
|
supportsPagination: true,
|
|
38
38
|
revisionKey: 'id',
|
|
39
|
+
getRevisionsUrl: ( parentId, revisionId ) =>
|
|
40
|
+
`/wp/v2/posts/${ parentId }/revisions${
|
|
41
|
+
revisionId ? '/' + revisionId : ''
|
|
42
|
+
}`,
|
|
39
43
|
};
|
|
40
44
|
|
|
41
45
|
// Add the post entity to the store
|
|
@@ -113,6 +117,122 @@ describe( 'getEntityRecord', () => {
|
|
|
113
117
|
} );
|
|
114
118
|
} );
|
|
115
119
|
|
|
120
|
+
describe( 'getEntityRecords', () => {
|
|
121
|
+
const POSTS = [
|
|
122
|
+
createTestPost( 1 ),
|
|
123
|
+
createTestPost( 2 ),
|
|
124
|
+
createTestPost( 3 ),
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
let registry;
|
|
128
|
+
|
|
129
|
+
beforeEach( () => {
|
|
130
|
+
registry = createTestRegistry();
|
|
131
|
+
triggerFetch.mockReset();
|
|
132
|
+
} );
|
|
133
|
+
|
|
134
|
+
it( 'preserves collection when getEntityRecord resolves after getEntityRecords', async () => {
|
|
135
|
+
let resolveSlowFetch;
|
|
136
|
+
const slowFetchPromise = new Promise( ( resolve ) => {
|
|
137
|
+
resolveSlowFetch = resolve;
|
|
138
|
+
} );
|
|
139
|
+
|
|
140
|
+
triggerFetch.mockImplementation( ( { path } ) => {
|
|
141
|
+
// Single post fetch (e.g. /wp/v2/posts/1): return slow promise.
|
|
142
|
+
if ( /\/wp\/v2\/posts\/\d+/.test( path ) ) {
|
|
143
|
+
return slowFetchPromise;
|
|
144
|
+
}
|
|
145
|
+
// Collection fetch: return immediately.
|
|
146
|
+
return Promise.resolve( {
|
|
147
|
+
json: () => Promise.resolve( POSTS ),
|
|
148
|
+
headers: {
|
|
149
|
+
get: ( header ) => {
|
|
150
|
+
if ( header === 'X-WP-Total' ) {
|
|
151
|
+
return String( POSTS.length );
|
|
152
|
+
}
|
|
153
|
+
if ( header === 'X-WP-TotalPages' ) {
|
|
154
|
+
return '1';
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
} );
|
|
160
|
+
} );
|
|
161
|
+
|
|
162
|
+
const resolveSelectStore = registry.resolveSelect( coreDataStore );
|
|
163
|
+
|
|
164
|
+
// Start getEntityRecord first (slow), then getEntityRecords (fast).
|
|
165
|
+
const singlePromise = resolveSelectStore.getEntityRecord(
|
|
166
|
+
'postType',
|
|
167
|
+
'post',
|
|
168
|
+
1,
|
|
169
|
+
{ context: 'edit' }
|
|
170
|
+
);
|
|
171
|
+
await resolveSelectStore.getEntityRecords( 'postType', 'post', {
|
|
172
|
+
context: 'edit',
|
|
173
|
+
} );
|
|
174
|
+
|
|
175
|
+
// Now resolve the slow single-record fetch.
|
|
176
|
+
resolveSlowFetch( {
|
|
177
|
+
json: () => Promise.resolve( POSTS[ 0 ] ),
|
|
178
|
+
headers: { get: () => null },
|
|
179
|
+
} );
|
|
180
|
+
await singlePromise;
|
|
181
|
+
|
|
182
|
+
// Wait for all pending thunks to settle.
|
|
183
|
+
await new Promise( ( resolve ) => setTimeout( resolve, 0 ) );
|
|
184
|
+
|
|
185
|
+
const allPosts = registry
|
|
186
|
+
.select( coreDataStore )
|
|
187
|
+
.getEntityRecords( 'postType', 'post', { context: 'edit' } );
|
|
188
|
+
expect( allPosts.map( ( p ) => p.id ) ).toEqual( [ 1, 2, 3 ] );
|
|
189
|
+
} );
|
|
190
|
+
|
|
191
|
+
it( 'preserves collection when getEntityRecord is called after getEntityRecords', async () => {
|
|
192
|
+
triggerFetch.mockImplementation( ( { path } ) => {
|
|
193
|
+
// Collection fetch.
|
|
194
|
+
if ( ! /\/wp\/v2\/posts\/\d+/.test( path ) ) {
|
|
195
|
+
return Promise.resolve( {
|
|
196
|
+
json: () => Promise.resolve( POSTS ),
|
|
197
|
+
headers: {
|
|
198
|
+
get: ( header ) => {
|
|
199
|
+
if ( header === 'X-WP-Total' ) {
|
|
200
|
+
return String( POSTS.length );
|
|
201
|
+
}
|
|
202
|
+
if ( header === 'X-WP-TotalPages' ) {
|
|
203
|
+
return '1';
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
} );
|
|
209
|
+
}
|
|
210
|
+
// Single post fetch.
|
|
211
|
+
return Promise.resolve( {
|
|
212
|
+
json: () => Promise.resolve( POSTS[ 0 ] ),
|
|
213
|
+
headers: { get: () => null },
|
|
214
|
+
} );
|
|
215
|
+
} );
|
|
216
|
+
|
|
217
|
+
const resolveSelectStore = registry.resolveSelect( coreDataStore );
|
|
218
|
+
|
|
219
|
+
// First resolve the collection.
|
|
220
|
+
await resolveSelectStore.getEntityRecords( 'postType', 'post', {
|
|
221
|
+
context: 'edit',
|
|
222
|
+
} );
|
|
223
|
+
|
|
224
|
+
// Then resolve a single record.
|
|
225
|
+
await resolveSelectStore.getEntityRecord( 'postType', 'post', 1, {
|
|
226
|
+
context: 'edit',
|
|
227
|
+
} );
|
|
228
|
+
|
|
229
|
+
const allPosts = registry
|
|
230
|
+
.select( coreDataStore )
|
|
231
|
+
.getEntityRecords( 'postType', 'post', { context: 'edit' } );
|
|
232
|
+
expect( allPosts.map( ( p ) => p.id ) ).toEqual( [ 1, 2, 3 ] );
|
|
233
|
+
} );
|
|
234
|
+
} );
|
|
235
|
+
|
|
116
236
|
describe( 'clearEntityRecordEdits', () => {
|
|
117
237
|
let registry;
|
|
118
238
|
|
|
@@ -142,3 +262,65 @@ describe( 'clearEntityRecordEdits', () => {
|
|
|
142
262
|
).toEqual( select.getRawEntityRecord( 'postType', 'post', post.id ) );
|
|
143
263
|
} );
|
|
144
264
|
} );
|
|
265
|
+
|
|
266
|
+
describe( 'getRevisions', () => {
|
|
267
|
+
const KIND = 'postType';
|
|
268
|
+
const NAME = 'post';
|
|
269
|
+
const RECORD_KEY = 1;
|
|
270
|
+
const REVISIONS = [ { id: 2 }, { id: 3 }, { id: 4 } ];
|
|
271
|
+
|
|
272
|
+
let registry;
|
|
273
|
+
|
|
274
|
+
beforeEach( () => {
|
|
275
|
+
registry = createTestRegistry();
|
|
276
|
+
triggerFetch.mockReset();
|
|
277
|
+
} );
|
|
278
|
+
|
|
279
|
+
it( 'preserves all revisions when getRevision resolves after getRevisions', async () => {
|
|
280
|
+
let resolveSlowFetch;
|
|
281
|
+
const slowFetchPromise = new Promise( ( resolve ) => {
|
|
282
|
+
resolveSlowFetch = resolve;
|
|
283
|
+
} );
|
|
284
|
+
|
|
285
|
+
triggerFetch.mockImplementation( ( { path } ) => {
|
|
286
|
+
if ( path && path.includes( 'revisions' ) ) {
|
|
287
|
+
// Single revision fetch: return slow promise.
|
|
288
|
+
if ( /revisions\/\d+/.test( path ) ) {
|
|
289
|
+
return slowFetchPromise;
|
|
290
|
+
}
|
|
291
|
+
// Collection fetch: return immediately.
|
|
292
|
+
return Promise.resolve( {
|
|
293
|
+
json: () => Promise.resolve( REVISIONS ),
|
|
294
|
+
headers: { get: () => String( REVISIONS.length ) },
|
|
295
|
+
} );
|
|
296
|
+
}
|
|
297
|
+
return Promise.resolve( {} );
|
|
298
|
+
} );
|
|
299
|
+
|
|
300
|
+
const resolveSelectStore = registry.resolveSelect( coreDataStore );
|
|
301
|
+
|
|
302
|
+
// Start getRevision first (slow), then getRevisions (fast).
|
|
303
|
+
const revisionPromise = resolveSelectStore.getRevision(
|
|
304
|
+
KIND,
|
|
305
|
+
NAME,
|
|
306
|
+
RECORD_KEY,
|
|
307
|
+
1,
|
|
308
|
+
{ context: 'edit' }
|
|
309
|
+
);
|
|
310
|
+
await resolveSelectStore.getRevisions( KIND, NAME, RECORD_KEY, {
|
|
311
|
+
context: 'edit',
|
|
312
|
+
} );
|
|
313
|
+
|
|
314
|
+
// Now resolve the slow single-revision fetch.
|
|
315
|
+
resolveSlowFetch( REVISIONS[ 0 ] );
|
|
316
|
+
await revisionPromise;
|
|
317
|
+
|
|
318
|
+
// Wait for all pending thunks (receiveRevisions) to settle.
|
|
319
|
+
await new Promise( ( resolve ) => setTimeout( resolve, 0 ) );
|
|
320
|
+
|
|
321
|
+
const allRevisions = registry
|
|
322
|
+
.select( coreDataStore )
|
|
323
|
+
.getRevisions( KIND, NAME, RECORD_KEY, { context: 'edit' } );
|
|
324
|
+
expect( allRevisions.map( ( r ) => r.id ) ).toEqual( [ 2, 3, 4 ] );
|
|
325
|
+
} );
|
|
326
|
+
} );
|
package/src/types.ts
CHANGED
|
@@ -66,6 +66,16 @@ export type CursorPosition = {
|
|
|
66
66
|
absoluteOffset: number;
|
|
67
67
|
};
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* The direction of a text selection, indicating where the caret sits.
|
|
71
|
+
*/
|
|
72
|
+
export enum SelectionDirection {
|
|
73
|
+
/** The caret is at the end of the selection (default / left-to-right). */
|
|
74
|
+
Forward = 'f',
|
|
75
|
+
/** The caret is at the start of the selection (right-to-left). */
|
|
76
|
+
Backward = 'b',
|
|
77
|
+
}
|
|
78
|
+
|
|
69
79
|
export type SelectionNone = {
|
|
70
80
|
// The user has not made a selection.
|
|
71
81
|
type: SelectionType.None;
|
|
@@ -86,6 +96,8 @@ export type SelectionInOneBlock = {
|
|
|
86
96
|
type: SelectionType.SelectionInOneBlock;
|
|
87
97
|
cursorStartPosition: CursorPosition;
|
|
88
98
|
cursorEndPosition: CursorPosition;
|
|
99
|
+
// The direction of the selection, indicating where the caret sits.
|
|
100
|
+
selectionDirection?: SelectionDirection;
|
|
89
101
|
};
|
|
90
102
|
|
|
91
103
|
export type SelectionInMultipleBlocks = {
|
|
@@ -95,6 +107,8 @@ export type SelectionInMultipleBlocks = {
|
|
|
95
107
|
type: SelectionType.SelectionInMultipleBlocks;
|
|
96
108
|
cursorStartPosition: CursorPosition;
|
|
97
109
|
cursorEndPosition: CursorPosition;
|
|
110
|
+
// The direction of the selection, indicating where the caret sits.
|
|
111
|
+
selectionDirection?: SelectionDirection;
|
|
98
112
|
};
|
|
99
113
|
|
|
100
114
|
export type SelectionWholeBlock = {
|
|
@@ -111,3 +125,8 @@ export type SelectionState =
|
|
|
111
125
|
| SelectionInOneBlock
|
|
112
126
|
| SelectionInMultipleBlocks
|
|
113
127
|
| SelectionWholeBlock;
|
|
128
|
+
|
|
129
|
+
export interface ResolvedSelection {
|
|
130
|
+
textIndex: number | null;
|
|
131
|
+
localClientId: string | null;
|
|
132
|
+
}
|
package/src/utils/crdt-blocks.ts
CHANGED
|
@@ -22,9 +22,14 @@ interface BlockAttributes {
|
|
|
22
22
|
[ key: string ]: unknown;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
interface BlockAttributeType {
|
|
26
|
+
role?: string;
|
|
27
|
+
type?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
25
30
|
interface BlockType {
|
|
31
|
+
attributes?: Record< string, BlockAttributeType >;
|
|
26
32
|
name: string;
|
|
27
|
-
attributes?: Record< string, { type?: string } >;
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
// A block as represented in Gutenberg's data store.
|
|
@@ -58,10 +63,16 @@ export type YBlockAttributes = Y.Map< Y.Text | unknown >;
|
|
|
58
63
|
const serializableBlocksCache = new WeakMap< WeakKey, Block[] >();
|
|
59
64
|
|
|
60
65
|
function makeBlockAttributesSerializable(
|
|
66
|
+
blockName: string,
|
|
61
67
|
attributes: BlockAttributes
|
|
62
68
|
): BlockAttributes {
|
|
63
69
|
const newAttributes = { ...attributes };
|
|
64
70
|
for ( const [ key, value ] of Object.entries( attributes ) ) {
|
|
71
|
+
if ( isLocalAttribute( blockName, key ) ) {
|
|
72
|
+
delete newAttributes[ key ];
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
65
76
|
if ( value instanceof RichTextData ) {
|
|
66
77
|
newAttributes[ key ] = value.valueOf();
|
|
67
78
|
}
|
|
@@ -76,7 +87,7 @@ function makeBlocksSerializable( blocks: Block[] ): Block[] {
|
|
|
76
87
|
return {
|
|
77
88
|
...rest,
|
|
78
89
|
name,
|
|
79
|
-
attributes: makeBlockAttributesSerializable( attributes ),
|
|
90
|
+
attributes: makeBlockAttributesSerializable( name, attributes ),
|
|
80
91
|
innerBlocks: makeBlocksSerializable( innerBlocks ),
|
|
81
92
|
};
|
|
82
93
|
} );
|
|
@@ -202,12 +213,7 @@ export function mergeCrdtBlocks(
|
|
|
202
213
|
makeBlocksSerializable( incomingBlocks )
|
|
203
214
|
);
|
|
204
215
|
}
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
// Ensure we skip blocks that we don't want to sync at the moment
|
|
208
|
-
const blocksToSync = allBlocks.filter( ( block ) =>
|
|
209
|
-
shouldBlockBeSynced( block )
|
|
210
|
-
);
|
|
216
|
+
const blocksToSync = serializableBlocksCache.get( incomingBlocks ) ?? [];
|
|
211
217
|
|
|
212
218
|
// This is a rudimentary diff implementation similar to the y-prosemirror diffing
|
|
213
219
|
// approach.
|
|
@@ -382,32 +388,6 @@ export function mergeCrdtBlocks(
|
|
|
382
388
|
}
|
|
383
389
|
}
|
|
384
390
|
|
|
385
|
-
/**
|
|
386
|
-
* Determine if a block should be synced.
|
|
387
|
-
*
|
|
388
|
-
* Ex: A gallery block should not be synced until the images have been
|
|
389
|
-
* uploaded to WordPress, and their url is available. Before that,
|
|
390
|
-
* it's not possible to access the blobs on a client as those are
|
|
391
|
-
* local.
|
|
392
|
-
*
|
|
393
|
-
* @param block The block to check.
|
|
394
|
-
* @return True if the block should be synced, false otherwise.
|
|
395
|
-
*/
|
|
396
|
-
function shouldBlockBeSynced( block: Block ): boolean {
|
|
397
|
-
// Verify that the gallery block is ready to be synced.
|
|
398
|
-
// This means that, all images have had their blobs converted to full URLs.
|
|
399
|
-
// Checking for only the blobs ensures that blocks that have just been inserted work as well.
|
|
400
|
-
if ( 'core/gallery' === block.name ) {
|
|
401
|
-
return ! block.innerBlocks.some(
|
|
402
|
-
( innerBlock ) =>
|
|
403
|
-
innerBlock.attributes && innerBlock.attributes.blob
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// Allow all other blocks to be synced.
|
|
408
|
-
return true;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
391
|
/**
|
|
412
392
|
* Update a single attribute on a Yjs block attributes map (currentAttributes).
|
|
413
393
|
*
|
|
@@ -448,38 +428,35 @@ function updateYBlockAttribute(
|
|
|
448
428
|
}
|
|
449
429
|
}
|
|
450
430
|
|
|
451
|
-
// Cached types
|
|
452
|
-
let cachedBlockAttributeTypes: Map< string, Map< string,
|
|
431
|
+
// Cached block attribute types, populated once from getBlockTypes().
|
|
432
|
+
let cachedBlockAttributeTypes: Map< string, Map< string, BlockAttributeType > >;
|
|
453
433
|
|
|
454
434
|
/**
|
|
455
|
-
* Get the
|
|
435
|
+
* Get the attribute type definition for a block attribute.
|
|
456
436
|
*
|
|
457
437
|
* @param blockName The name of the block, e.g. 'core/paragraph'.
|
|
458
438
|
* @param attributeName The name of the attribute, e.g. 'content'.
|
|
459
|
-
* @return The type of the attribute
|
|
439
|
+
* @return The type definition of the attribute.
|
|
460
440
|
*/
|
|
461
441
|
function getBlockAttributeType(
|
|
462
442
|
blockName: string,
|
|
463
443
|
attributeName: string
|
|
464
|
-
):
|
|
444
|
+
): BlockAttributeType | undefined {
|
|
465
445
|
if ( ! cachedBlockAttributeTypes ) {
|
|
466
446
|
// Parse the attributes for all blocks once.
|
|
467
|
-
cachedBlockAttributeTypes = new Map
|
|
447
|
+
cachedBlockAttributeTypes = new Map();
|
|
468
448
|
|
|
469
449
|
for ( const blockType of getBlockTypes() as BlockType[] ) {
|
|
470
|
-
const blockAttributeTypeMap = new Map< string, string >();
|
|
471
|
-
|
|
472
|
-
for ( const [ name, definition ] of Object.entries(
|
|
473
|
-
blockType.attributes ?? {}
|
|
474
|
-
) ) {
|
|
475
|
-
if ( definition.type ) {
|
|
476
|
-
blockAttributeTypeMap.set( name, definition.type );
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
450
|
cachedBlockAttributeTypes.set(
|
|
481
451
|
blockType.name,
|
|
482
|
-
|
|
452
|
+
new Map< string, BlockAttributeType >(
|
|
453
|
+
Object.entries( blockType.attributes ?? {} ).map(
|
|
454
|
+
( [ name, definition ] ) => {
|
|
455
|
+
const { role, type } = definition;
|
|
456
|
+
return [ name, { role, type } ];
|
|
457
|
+
}
|
|
458
|
+
)
|
|
459
|
+
)
|
|
483
460
|
);
|
|
484
461
|
}
|
|
485
462
|
}
|
|
@@ -503,11 +480,13 @@ function isExpectedAttributeType(
|
|
|
503
480
|
const expectedAttributeType = getBlockAttributeType(
|
|
504
481
|
blockName,
|
|
505
482
|
attributeName
|
|
506
|
-
);
|
|
483
|
+
)?.type;
|
|
507
484
|
|
|
508
485
|
if ( expectedAttributeType === 'rich-text' ) {
|
|
509
486
|
return attributeValue instanceof Y.Text;
|
|
510
|
-
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if ( expectedAttributeType === 'string' ) {
|
|
511
490
|
return typeof attributeValue === 'string';
|
|
512
491
|
}
|
|
513
492
|
|
|
@@ -515,6 +494,18 @@ function isExpectedAttributeType(
|
|
|
515
494
|
return true;
|
|
516
495
|
}
|
|
517
496
|
|
|
497
|
+
/**
|
|
498
|
+
* Given a block name and attribute key, return true if the attribute is local
|
|
499
|
+
* and should not be synced.
|
|
500
|
+
*
|
|
501
|
+
* @param blockName The name of the block, e.g. 'core/image'.
|
|
502
|
+
* @param attributeName The name of the attribute to check, e.g. 'blob'.
|
|
503
|
+
* @return True if the attribute is local, false otherwise.
|
|
504
|
+
*/
|
|
505
|
+
function isLocalAttribute( blockName: string, attributeName: string ): boolean {
|
|
506
|
+
return 'local' === getBlockAttributeType( blockName, attributeName )?.role;
|
|
507
|
+
}
|
|
508
|
+
|
|
518
509
|
/**
|
|
519
510
|
* Given a block name and attribute key, return true if the attribute is rich-text typed.
|
|
520
511
|
*
|
|
@@ -526,7 +517,9 @@ function isRichTextAttribute(
|
|
|
526
517
|
blockName: string,
|
|
527
518
|
attributeName: string
|
|
528
519
|
): boolean {
|
|
529
|
-
return
|
|
520
|
+
return (
|
|
521
|
+
'rich-text' === getBlockAttributeType( blockName, attributeName )?.type
|
|
522
|
+
);
|
|
530
523
|
}
|
|
531
524
|
|
|
532
525
|
let localDoc: Y.Doc;
|
|
@@ -13,16 +13,17 @@ import { CRDT_RECORD_MAP_KEY } from '../sync';
|
|
|
13
13
|
import type { YPostRecord } from './crdt';
|
|
14
14
|
import type { YBlock, YBlocks } from './crdt-blocks';
|
|
15
15
|
import { getRootMap } from './crdt-utils';
|
|
16
|
-
import type {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
import type { SelectionDirection } from '../types';
|
|
17
|
+
import {
|
|
18
|
+
type AbsoluteBlockIndexPath,
|
|
19
|
+
type WPBlockSelection,
|
|
20
|
+
type SelectionState,
|
|
21
|
+
type SelectionNone,
|
|
22
|
+
type SelectionCursor,
|
|
23
|
+
type SelectionInOneBlock,
|
|
24
|
+
type SelectionInMultipleBlocks,
|
|
25
|
+
type SelectionWholeBlock,
|
|
26
|
+
type CursorPosition,
|
|
26
27
|
} from '../types';
|
|
27
28
|
|
|
28
29
|
/**
|
|
@@ -44,16 +45,20 @@ export enum SelectionType {
|
|
|
44
45
|
* differ between the block-editor store and the Yjs document (e.g. in "Show
|
|
45
46
|
* Template" mode).
|
|
46
47
|
*
|
|
47
|
-
* @param selectionStart
|
|
48
|
-
* @param selectionEnd
|
|
49
|
-
* @param yDoc
|
|
48
|
+
* @param selectionStart - The start position of the selection
|
|
49
|
+
* @param selectionEnd - The end position of the selection
|
|
50
|
+
* @param yDoc - The Yjs document
|
|
51
|
+
* @param options - Optional parameters
|
|
52
|
+
* @param options.selectionDirection - The direction of the selection (forward or backward)
|
|
50
53
|
* @return The SelectionState
|
|
51
54
|
*/
|
|
52
55
|
export function getSelectionState(
|
|
53
56
|
selectionStart: WPBlockSelection,
|
|
54
57
|
selectionEnd: WPBlockSelection,
|
|
55
|
-
yDoc: Y.Doc
|
|
58
|
+
yDoc: Y.Doc,
|
|
59
|
+
options?: { selectionDirection?: SelectionDirection }
|
|
56
60
|
): SelectionState {
|
|
61
|
+
const { selectionDirection } = options ?? {};
|
|
57
62
|
const ymap = getRootMap< YPostRecord >( yDoc, CRDT_RECORD_MAP_KEY );
|
|
58
63
|
const yBlocks = ymap.get( 'blocks' );
|
|
59
64
|
|
|
@@ -122,6 +127,7 @@ export function getSelectionState(
|
|
|
122
127
|
type: SelectionType.SelectionInOneBlock,
|
|
123
128
|
cursorStartPosition,
|
|
124
129
|
cursorEndPosition,
|
|
130
|
+
selectionDirection,
|
|
125
131
|
};
|
|
126
132
|
}
|
|
127
133
|
|
|
@@ -137,6 +143,7 @@ export function getSelectionState(
|
|
|
137
143
|
type: SelectionType.SelectionInMultipleBlocks,
|
|
138
144
|
cursorStartPosition,
|
|
139
145
|
cursorEndPosition,
|
|
146
|
+
selectionDirection,
|
|
140
147
|
};
|
|
141
148
|
}
|
|
142
149
|
|
|
@@ -315,7 +322,9 @@ export function areSelectionsStatesEqual(
|
|
|
315
322
|
areCursorPositionsEqual(
|
|
316
323
|
selection1.cursorEndPosition,
|
|
317
324
|
( selection2 as SelectionInOneBlock ).cursorEndPosition
|
|
318
|
-
)
|
|
325
|
+
) &&
|
|
326
|
+
selection1.selectionDirection ===
|
|
327
|
+
( selection2 as SelectionInOneBlock ).selectionDirection
|
|
319
328
|
);
|
|
320
329
|
|
|
321
330
|
case SelectionType.SelectionInMultipleBlocks:
|
|
@@ -329,7 +338,10 @@ export function areSelectionsStatesEqual(
|
|
|
329
338
|
selection1.cursorEndPosition,
|
|
330
339
|
( selection2 as SelectionInMultipleBlocks )
|
|
331
340
|
.cursorEndPosition
|
|
332
|
-
)
|
|
341
|
+
) &&
|
|
342
|
+
selection1.selectionDirection ===
|
|
343
|
+
( selection2 as SelectionInMultipleBlocks )
|
|
344
|
+
.selectionDirection
|
|
333
345
|
);
|
|
334
346
|
case SelectionType.WholeBlock:
|
|
335
347
|
return Y.compareRelativePositions(
|
package/src/utils/crdt.ts
CHANGED
|
@@ -342,8 +342,6 @@ export function getPostChangesFromCRDTDoc(
|
|
|
342
342
|
);
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
-
// The consumers of blocks have memoization that renders optimization
|
|
346
|
-
// here unnecessary.
|
|
347
345
|
return true;
|
|
348
346
|
}
|
|
349
347
|
|
|
@@ -351,11 +349,8 @@ export function getPostChangesFromCRDTDoc(
|
|
|
351
349
|
// Do not overwrite a "floating" date. Borrowing logic from the
|
|
352
350
|
// isEditedPostDateFloating selector.
|
|
353
351
|
const currentDateIsFloating =
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
) &&
|
|
357
|
-
( null === currentValue ||
|
|
358
|
-
editedRecord.modified === currentValue );
|
|
352
|
+
null === currentValue ||
|
|
353
|
+
editedRecord.modified === currentValue;
|
|
359
354
|
|
|
360
355
|
if ( currentDateIsFloating ) {
|
|
361
356
|
return false;
|
package/src/utils/index.js
CHANGED