@wordpress/core-data 7.41.0 → 7.41.2-next.v.202603102151.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 +8 -6
- package/build/actions.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 +32 -2
- package/build/hooks/use-entity-prop.cjs.map +2 -2
- package/build/private-actions.cjs +1 -1
- package/build/private-actions.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/reducer.cjs +2 -1
- package/build/reducer.cjs.map +2 -2
- package/build/resolvers.cjs +4 -2
- package/build/resolvers.cjs.map +2 -2
- package/build/sync.cjs +3 -0
- package/build/sync.cjs.map +2 -2
- package/build/utils/crdt-blocks.cjs +22 -26
- package/build/utils/crdt-blocks.cjs.map +2 -2
- package/build/utils/crdt.cjs +1 -3
- package/build/utils/crdt.cjs.map +2 -2
- package/build/utils/user-permissions.cjs +1 -4
- package/build/utils/user-permissions.cjs.map +2 -2
- package/build-module/actions.mjs +13 -7
- package/build-module/actions.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 +33 -3
- package/build-module/hooks/use-entity-prop.mjs.map +2 -2
- package/build-module/private-actions.mjs +1 -1
- package/build-module/private-actions.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/reducer.mjs +2 -1
- package/build-module/reducer.mjs.map +2 -2
- package/build-module/resolvers.mjs +4 -2
- package/build-module/resolvers.mjs.map +2 -2
- package/build-module/sync.mjs +2 -0
- package/build-module/sync.mjs.map +2 -2
- package/build-module/utils/crdt-blocks.mjs +22 -26
- package/build-module/utils/crdt-blocks.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/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/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/index.d.ts.map +1 -1
- package/build-types/queried-data/reducer.d.ts.map +1 -1
- package/build-types/reducer.d.ts.map +1 -1
- package/build-types/resolvers.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/utils/crdt-blocks.d.ts.map +1 -1
- package/build-types/utils/crdt.d.ts.map +1 -1
- package/build-types/utils/user-permissions.d.ts.map +1 -1
- package/package.json +18 -18
- package/src/actions.js +24 -10
- package/src/entity-provider.js +24 -11
- package/src/hooks/use-entity-prop.js +41 -3
- package/src/private-actions.js +1 -1
- package/src/queried-data/actions.js +1 -1
- package/src/queried-data/reducer.js +26 -14
- package/src/reducer.js +4 -1
- package/src/resolvers.js +5 -3
- package/src/sync.ts +2 -0
- package/src/test/private-actions.js +1 -1
- package/src/test/resolvers.js +24 -3
- package/src/test/store.js +116 -0
- package/src/utils/crdt-blocks.ts +47 -54
- package/src/utils/crdt.ts +2 -5
- package/src/utils/test/crdt-blocks.ts +42 -24
- package/src/utils/user-permissions.js +4 -5
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WordPress dependencies
|
|
3
3
|
*/
|
|
4
|
-
import { useCallback } from '@wordpress/element';
|
|
4
|
+
import { useCallback, useContext } from '@wordpress/element';
|
|
5
5
|
import { useDispatch, useSelect } from '@wordpress/data';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Internal dependencies
|
|
9
9
|
*/
|
|
10
10
|
import { STORE_NAME } from '../name';
|
|
11
|
+
import { DEFAULT_ENTITY_KEY } from '../entities';
|
|
12
|
+
import { EntityContext } from '../entity-context';
|
|
11
13
|
import useEntityId from './use-entity-id';
|
|
12
14
|
|
|
13
15
|
/**
|
|
@@ -30,9 +32,42 @@ import useEntityId from './use-entity-id';
|
|
|
30
32
|
export default function useEntityProp( kind, name, prop, _id ) {
|
|
31
33
|
const providerId = useEntityId( kind, name );
|
|
32
34
|
const id = _id ?? providerId;
|
|
35
|
+
const context = useContext( EntityContext );
|
|
36
|
+
const revisionId = context?.revisionId;
|
|
33
37
|
|
|
34
38
|
const { value, fullValue } = useSelect(
|
|
35
39
|
( select ) => {
|
|
40
|
+
if ( revisionId ) {
|
|
41
|
+
// Use getRevisions (not getRevision) to read from the
|
|
42
|
+
// already-cached collection. Using getRevision would
|
|
43
|
+
// trigger a redundant single-revision API fetch that
|
|
44
|
+
// can wipe the collection due to a race condition.
|
|
45
|
+
// See https://github.com/WordPress/gutenberg/pull/76043.
|
|
46
|
+
const revisions = select( STORE_NAME ).getRevisions(
|
|
47
|
+
kind,
|
|
48
|
+
name,
|
|
49
|
+
id,
|
|
50
|
+
{
|
|
51
|
+
per_page: -1,
|
|
52
|
+
context: 'edit',
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
const entityConfig = select( STORE_NAME ).getEntityConfig(
|
|
56
|
+
kind,
|
|
57
|
+
name
|
|
58
|
+
);
|
|
59
|
+
const revKey = entityConfig?.revisionKey || DEFAULT_ENTITY_KEY;
|
|
60
|
+
const revision = revisions?.find(
|
|
61
|
+
( r ) => r[ revKey ] === revisionId
|
|
62
|
+
);
|
|
63
|
+
return revision
|
|
64
|
+
? {
|
|
65
|
+
value: revision[ prop ],
|
|
66
|
+
fullValue: revision[ prop ],
|
|
67
|
+
}
|
|
68
|
+
: {};
|
|
69
|
+
}
|
|
70
|
+
|
|
36
71
|
const { getEntityRecord, getEditedEntityRecord } =
|
|
37
72
|
select( STORE_NAME );
|
|
38
73
|
const record = getEntityRecord( kind, name, id ); // Trigger resolver.
|
|
@@ -44,16 +79,19 @@ export default function useEntityProp( kind, name, prop, _id ) {
|
|
|
44
79
|
}
|
|
45
80
|
: {};
|
|
46
81
|
},
|
|
47
|
-
[ kind, name, id, prop ]
|
|
82
|
+
[ kind, name, id, prop, revisionId ]
|
|
48
83
|
);
|
|
49
84
|
const { editEntityRecord } = useDispatch( STORE_NAME );
|
|
50
85
|
const setValue = useCallback(
|
|
51
86
|
( newValue ) => {
|
|
87
|
+
if ( revisionId ) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
52
90
|
editEntityRecord( kind, name, id, {
|
|
53
91
|
[ prop ]: newValue,
|
|
54
92
|
} );
|
|
55
93
|
},
|
|
56
|
-
[ editEntityRecord, kind, name, id, prop ]
|
|
94
|
+
[ editEntityRecord, kind, name, id, prop, revisionId ]
|
|
57
95
|
);
|
|
58
96
|
|
|
59
97
|
return [ value, setValue, fullValue ];
|
package/src/private-actions.js
CHANGED
|
@@ -105,19 +105,22 @@ export function items( state = {}, action ) {
|
|
|
105
105
|
case 'RECEIVE_ITEMS': {
|
|
106
106
|
const context = getContextFromAction( action );
|
|
107
107
|
const key = action.key || DEFAULT_ENTITY_KEY;
|
|
108
|
+
const itemsList = Array.isArray( action.items )
|
|
109
|
+
? action.items
|
|
110
|
+
: [ action.items ];
|
|
108
111
|
return {
|
|
109
112
|
...state,
|
|
110
113
|
[ context ]: {
|
|
111
114
|
...state[ context ],
|
|
112
|
-
...
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
115
|
+
...Object.fromEntries(
|
|
116
|
+
itemsList.map( ( item ) => [
|
|
117
|
+
item?.[ key ],
|
|
118
|
+
conservativeMapItem(
|
|
119
|
+
state?.[ context ]?.[ item?.[ key ] ],
|
|
120
|
+
item
|
|
121
|
+
),
|
|
122
|
+
] )
|
|
123
|
+
),
|
|
121
124
|
},
|
|
122
125
|
};
|
|
123
126
|
}
|
|
@@ -149,6 +152,9 @@ export function itemIsComplete( state = {}, action ) {
|
|
|
149
152
|
case 'RECEIVE_ITEMS': {
|
|
150
153
|
const context = getContextFromAction( action );
|
|
151
154
|
const { query, key = DEFAULT_ENTITY_KEY } = action;
|
|
155
|
+
const itemsList = Array.isArray( action.items )
|
|
156
|
+
? action.items
|
|
157
|
+
: [ action.items ];
|
|
152
158
|
|
|
153
159
|
// An item is considered complete if it is received without an associated
|
|
154
160
|
// fields query. Ideally, this would be implemented in such a way where the
|
|
@@ -164,7 +170,7 @@ export function itemIsComplete( state = {}, action ) {
|
|
|
164
170
|
...state,
|
|
165
171
|
[ context ]: {
|
|
166
172
|
...state[ context ],
|
|
167
|
-
...
|
|
173
|
+
...itemsList.reduce( ( result, item ) => {
|
|
168
174
|
const itemId = item?.[ key ];
|
|
169
175
|
|
|
170
176
|
// Defer to completeness if already assigned. Technically the
|
|
@@ -224,18 +230,24 @@ const receiveQueries = compose( [
|
|
|
224
230
|
// reducer tracks only a single query object.
|
|
225
231
|
onSubKey( 'stableKey' ),
|
|
226
232
|
] )( ( state = {}, action ) => {
|
|
227
|
-
|
|
233
|
+
if ( action.type !== 'RECEIVE_ITEMS' ) {
|
|
234
|
+
return state;
|
|
235
|
+
}
|
|
228
236
|
|
|
229
|
-
|
|
237
|
+
// Single items don't have page or total count metadata
|
|
238
|
+
// (only collection query responses do), so skip updating itemIds.
|
|
239
|
+
if ( ! Array.isArray( action.items ) ) {
|
|
230
240
|
return state;
|
|
231
241
|
}
|
|
232
242
|
|
|
243
|
+
const key = action.key ?? DEFAULT_ENTITY_KEY;
|
|
244
|
+
|
|
233
245
|
return {
|
|
234
246
|
itemIds: getMergedItemIds(
|
|
235
247
|
state?.itemIds || [],
|
|
236
248
|
action.items.map( ( item ) => item?.[ key ] ).filter( Boolean ),
|
|
237
|
-
page,
|
|
238
|
-
perPage
|
|
249
|
+
action.page,
|
|
250
|
+
action.perPage
|
|
239
251
|
),
|
|
240
252
|
meta: action.meta,
|
|
241
253
|
};
|
package/src/reducer.js
CHANGED
|
@@ -214,8 +214,11 @@ function entity( entityConfig ) {
|
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
const nextState = { ...state };
|
|
217
|
+
const itemsList = Array.isArray( action.items )
|
|
218
|
+
? action.items
|
|
219
|
+
: [ action.items ];
|
|
217
220
|
|
|
218
|
-
for ( const record of
|
|
221
|
+
for ( const record of itemsList ) {
|
|
219
222
|
const recordId = record?.[ action.key ];
|
|
220
223
|
const edits = nextState[ recordId ];
|
|
221
224
|
if ( ! edits ) {
|
package/src/resolvers.js
CHANGED
|
@@ -991,9 +991,11 @@ export const getDefaultTemplateId =
|
|
|
991
991
|
template.id = id;
|
|
992
992
|
registry.batch( () => {
|
|
993
993
|
dispatch.receiveDefaultTemplateId( query, id );
|
|
994
|
-
dispatch.receiveEntityRecords(
|
|
995
|
-
|
|
996
|
-
|
|
994
|
+
dispatch.receiveEntityRecords(
|
|
995
|
+
'postType',
|
|
996
|
+
template.type,
|
|
997
|
+
template
|
|
998
|
+
);
|
|
997
999
|
// Avoid further network requests.
|
|
998
1000
|
dispatch.finishResolution( 'getEntityRecord', [
|
|
999
1001
|
'postType',
|
package/src/sync.ts
CHANGED
|
@@ -17,6 +17,7 @@ const {
|
|
|
17
17
|
CRDT_DOC_META_PERSISTENCE_KEY,
|
|
18
18
|
CRDT_RECORD_MAP_KEY,
|
|
19
19
|
LOCAL_EDITOR_ORIGIN,
|
|
20
|
+
LOCAL_UNDO_IGNORED_ORIGIN,
|
|
20
21
|
retrySyncConnection,
|
|
21
22
|
} = unlock( syncPrivateApis );
|
|
22
23
|
|
|
@@ -25,6 +26,7 @@ export {
|
|
|
25
26
|
CRDT_DOC_META_PERSISTENCE_KEY,
|
|
26
27
|
CRDT_RECORD_MAP_KEY,
|
|
27
28
|
LOCAL_EDITOR_ORIGIN,
|
|
29
|
+
LOCAL_UNDO_IGNORED_ORIGIN,
|
|
28
30
|
retrySyncConnection,
|
|
29
31
|
};
|
|
30
32
|
|
package/src/test/resolvers.js
CHANGED
|
@@ -862,6 +862,24 @@ describe( 'canUser', () => {
|
|
|
862
862
|
expect( dispatch.receiveUserPermissions ).not.toHaveBeenCalled();
|
|
863
863
|
} );
|
|
864
864
|
|
|
865
|
+
it( 'receives false when the allow header is missing', async () => {
|
|
866
|
+
triggerFetch.mockImplementation( () => ( {
|
|
867
|
+
headers: new Map(),
|
|
868
|
+
} ) );
|
|
869
|
+
|
|
870
|
+
await canUser(
|
|
871
|
+
'create',
|
|
872
|
+
'media'
|
|
873
|
+
)( { dispatch, registry, resolveSelect } );
|
|
874
|
+
|
|
875
|
+
expect( dispatch.receiveUserPermissions ).toHaveBeenCalledWith( {
|
|
876
|
+
'create/media': false,
|
|
877
|
+
'read/media': false,
|
|
878
|
+
'update/media': false,
|
|
879
|
+
'delete/media': false,
|
|
880
|
+
} );
|
|
881
|
+
} );
|
|
882
|
+
|
|
865
883
|
it( 'throws an error when an entity resource object is malformed', async () => {
|
|
866
884
|
await expect(
|
|
867
885
|
canUser( 'create', { name: 'wp_block' } )( {
|
|
@@ -888,9 +906,12 @@ describe( 'canUser', () => {
|
|
|
888
906
|
parse: false,
|
|
889
907
|
} );
|
|
890
908
|
|
|
891
|
-
expect( dispatch.receiveUserPermissions ).toHaveBeenCalledWith(
|
|
892
|
-
|
|
893
|
-
|
|
909
|
+
expect( dispatch.receiveUserPermissions ).toHaveBeenCalledWith( {
|
|
910
|
+
'create/media': false,
|
|
911
|
+
'read/media': true,
|
|
912
|
+
'update/media': false,
|
|
913
|
+
'delete/media': false,
|
|
914
|
+
} );
|
|
894
915
|
} );
|
|
895
916
|
|
|
896
917
|
it( 'receives false when the user is not allowed to perform an action on entities', async () => {
|
package/src/test/store.js
CHANGED
|
@@ -113,6 +113,122 @@ describe( 'getEntityRecord', () => {
|
|
|
113
113
|
} );
|
|
114
114
|
} );
|
|
115
115
|
|
|
116
|
+
describe( 'getEntityRecords', () => {
|
|
117
|
+
const POSTS = [
|
|
118
|
+
createTestPost( 1 ),
|
|
119
|
+
createTestPost( 2 ),
|
|
120
|
+
createTestPost( 3 ),
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
let registry;
|
|
124
|
+
|
|
125
|
+
beforeEach( () => {
|
|
126
|
+
registry = createTestRegistry();
|
|
127
|
+
triggerFetch.mockReset();
|
|
128
|
+
} );
|
|
129
|
+
|
|
130
|
+
it( 'preserves collection when getEntityRecord resolves after getEntityRecords', async () => {
|
|
131
|
+
let resolveSlowFetch;
|
|
132
|
+
const slowFetchPromise = new Promise( ( resolve ) => {
|
|
133
|
+
resolveSlowFetch = resolve;
|
|
134
|
+
} );
|
|
135
|
+
|
|
136
|
+
triggerFetch.mockImplementation( ( { path } ) => {
|
|
137
|
+
// Single post fetch (e.g. /wp/v2/posts/1): return slow promise.
|
|
138
|
+
if ( /\/wp\/v2\/posts\/\d+/.test( path ) ) {
|
|
139
|
+
return slowFetchPromise;
|
|
140
|
+
}
|
|
141
|
+
// Collection fetch: return immediately.
|
|
142
|
+
return Promise.resolve( {
|
|
143
|
+
json: () => Promise.resolve( POSTS ),
|
|
144
|
+
headers: {
|
|
145
|
+
get: ( header ) => {
|
|
146
|
+
if ( header === 'X-WP-Total' ) {
|
|
147
|
+
return String( POSTS.length );
|
|
148
|
+
}
|
|
149
|
+
if ( header === 'X-WP-TotalPages' ) {
|
|
150
|
+
return '1';
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
} );
|
|
156
|
+
} );
|
|
157
|
+
|
|
158
|
+
const resolveSelectStore = registry.resolveSelect( coreDataStore );
|
|
159
|
+
|
|
160
|
+
// Start getEntityRecord first (slow), then getEntityRecords (fast).
|
|
161
|
+
const singlePromise = resolveSelectStore.getEntityRecord(
|
|
162
|
+
'postType',
|
|
163
|
+
'post',
|
|
164
|
+
1,
|
|
165
|
+
{ context: 'edit' }
|
|
166
|
+
);
|
|
167
|
+
await resolveSelectStore.getEntityRecords( 'postType', 'post', {
|
|
168
|
+
context: 'edit',
|
|
169
|
+
} );
|
|
170
|
+
|
|
171
|
+
// Now resolve the slow single-record fetch.
|
|
172
|
+
resolveSlowFetch( {
|
|
173
|
+
json: () => Promise.resolve( POSTS[ 0 ] ),
|
|
174
|
+
headers: { get: () => null },
|
|
175
|
+
} );
|
|
176
|
+
await singlePromise;
|
|
177
|
+
|
|
178
|
+
// Wait for all pending thunks to settle.
|
|
179
|
+
await new Promise( ( resolve ) => setTimeout( resolve, 0 ) );
|
|
180
|
+
|
|
181
|
+
const allPosts = registry
|
|
182
|
+
.select( coreDataStore )
|
|
183
|
+
.getEntityRecords( 'postType', 'post', { context: 'edit' } );
|
|
184
|
+
expect( allPosts.map( ( p ) => p.id ) ).toEqual( [ 1, 2, 3 ] );
|
|
185
|
+
} );
|
|
186
|
+
|
|
187
|
+
it( 'preserves collection when getEntityRecord is called after getEntityRecords', async () => {
|
|
188
|
+
triggerFetch.mockImplementation( ( { path } ) => {
|
|
189
|
+
// Collection fetch.
|
|
190
|
+
if ( ! /\/wp\/v2\/posts\/\d+/.test( path ) ) {
|
|
191
|
+
return Promise.resolve( {
|
|
192
|
+
json: () => Promise.resolve( POSTS ),
|
|
193
|
+
headers: {
|
|
194
|
+
get: ( header ) => {
|
|
195
|
+
if ( header === 'X-WP-Total' ) {
|
|
196
|
+
return String( POSTS.length );
|
|
197
|
+
}
|
|
198
|
+
if ( header === 'X-WP-TotalPages' ) {
|
|
199
|
+
return '1';
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
} );
|
|
205
|
+
}
|
|
206
|
+
// Single post fetch.
|
|
207
|
+
return Promise.resolve( {
|
|
208
|
+
json: () => Promise.resolve( POSTS[ 0 ] ),
|
|
209
|
+
headers: { get: () => null },
|
|
210
|
+
} );
|
|
211
|
+
} );
|
|
212
|
+
|
|
213
|
+
const resolveSelectStore = registry.resolveSelect( coreDataStore );
|
|
214
|
+
|
|
215
|
+
// First resolve the collection.
|
|
216
|
+
await resolveSelectStore.getEntityRecords( 'postType', 'post', {
|
|
217
|
+
context: 'edit',
|
|
218
|
+
} );
|
|
219
|
+
|
|
220
|
+
// Then resolve a single record.
|
|
221
|
+
await resolveSelectStore.getEntityRecord( 'postType', 'post', 1, {
|
|
222
|
+
context: 'edit',
|
|
223
|
+
} );
|
|
224
|
+
|
|
225
|
+
const allPosts = registry
|
|
226
|
+
.select( coreDataStore )
|
|
227
|
+
.getEntityRecords( 'postType', 'post', { context: 'edit' } );
|
|
228
|
+
expect( allPosts.map( ( p ) => p.id ) ).toEqual( [ 1, 2, 3 ] );
|
|
229
|
+
} );
|
|
230
|
+
} );
|
|
231
|
+
|
|
116
232
|
describe( 'clearEntityRecordEdits', () => {
|
|
117
233
|
let registry;
|
|
118
234
|
|
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;
|
package/src/utils/crdt.ts
CHANGED
|
@@ -351,11 +351,8 @@ export function getPostChangesFromCRDTDoc(
|
|
|
351
351
|
// Do not overwrite a "floating" date. Borrowing logic from the
|
|
352
352
|
// isEditedPostDateFloating selector.
|
|
353
353
|
const currentDateIsFloating =
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
) &&
|
|
357
|
-
( null === currentValue ||
|
|
358
|
-
editedRecord.modified === currentValue );
|
|
354
|
+
null === currentValue ||
|
|
355
|
+
editedRecord.modified === currentValue;
|
|
359
356
|
|
|
360
357
|
if ( currentDateIsFloating ) {
|
|
361
358
|
return false;
|