@wordpress/core-data 7.42.0 → 7.43.2-next.v.202604091042.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 +9 -1
- package/build/entities.cjs +28 -20
- package/build/entities.cjs.map +2 -2
- package/build/hooks/index.cjs +3 -3
- package/build/hooks/index.cjs.map +1 -1
- package/build/hooks/use-entity-record.cjs +4 -4
- package/build/hooks/use-entity-record.cjs.map +2 -2
- package/build/hooks/use-entity-records.cjs +3 -3
- package/build/hooks/use-entity-records.cjs.map +2 -2
- package/build/hooks/use-query-select.cjs +2 -2
- package/build/hooks/use-query-select.cjs.map +2 -2
- package/build/hooks/use-resource-permissions.cjs +4 -4
- package/build/hooks/use-resource-permissions.cjs.map +2 -2
- package/build/private-actions.cjs +10 -0
- package/build/private-actions.cjs.map +2 -2
- package/build/private-selectors.cjs +20 -3
- package/build/private-selectors.cjs.map +2 -2
- package/build/queried-data/get-query-parts.cjs +8 -7
- package/build/queried-data/get-query-parts.cjs.map +2 -2
- package/build/queried-data/reducer.cjs +15 -9
- package/build/queried-data/reducer.cjs.map +2 -2
- package/build/queried-data/selectors.cjs +23 -22
- package/build/queried-data/selectors.cjs.map +2 -2
- package/build/reducer.cjs +16 -3
- package/build/reducer.cjs.map +2 -2
- package/build/resolvers.cjs +22 -14
- package/build/resolvers.cjs.map +2 -2
- package/build/selectors.cjs +20 -10
- package/build/selectors.cjs.map +2 -2
- package/build/utils/crdt-blocks.cjs +170 -31
- package/build/utils/crdt-blocks.cjs.map +2 -2
- package/build/utils/crdt-text.cjs +52 -0
- package/build/utils/crdt-text.cjs.map +7 -0
- package/build/utils/crdt.cjs +13 -0
- package/build/utils/crdt.cjs.map +2 -2
- package/build/utils/index.cjs +0 -3
- package/build/utils/index.cjs.map +2 -2
- package/build-module/entities.mjs +29 -20
- package/build-module/entities.mjs.map +2 -2
- package/build-module/hooks/index.mjs +6 -6
- package/build-module/hooks/index.mjs.map +2 -2
- package/build-module/hooks/use-entity-record.mjs +3 -3
- package/build-module/hooks/use-entity-record.mjs.map +2 -2
- package/build-module/hooks/use-entity-records.mjs +2 -2
- package/build-module/hooks/use-entity-records.mjs.map +2 -2
- package/build-module/hooks/use-query-select.mjs +1 -1
- package/build-module/hooks/use-query-select.mjs.map +1 -1
- package/build-module/hooks/use-resource-permissions.mjs +3 -3
- package/build-module/hooks/use-resource-permissions.mjs.map +2 -2
- package/build-module/private-actions.mjs +9 -0
- package/build-module/private-actions.mjs.map +2 -2
- package/build-module/private-selectors.mjs +19 -3
- package/build-module/private-selectors.mjs.map +2 -2
- package/build-module/queried-data/get-query-parts.mjs +8 -7
- package/build-module/queried-data/get-query-parts.mjs.map +2 -2
- package/build-module/queried-data/reducer.mjs +15 -9
- package/build-module/queried-data/reducer.mjs.map +2 -2
- package/build-module/queried-data/selectors.mjs +23 -22
- package/build-module/queried-data/selectors.mjs.map +2 -2
- package/build-module/reducer.mjs +14 -2
- package/build-module/reducer.mjs.map +2 -2
- package/build-module/resolvers.mjs +20 -13
- package/build-module/resolvers.mjs.map +2 -2
- package/build-module/selectors.mjs +20 -11
- package/build-module/selectors.mjs.map +2 -2
- package/build-module/utils/crdt-blocks.mjs +169 -31
- package/build-module/utils/crdt-blocks.mjs.map +2 -2
- package/build-module/utils/crdt-text.mjs +26 -0
- package/build-module/utils/crdt-text.mjs.map +7 -0
- package/build-module/utils/crdt.mjs +13 -0
- package/build-module/utils/crdt.mjs.map +2 -2
- package/build-module/utils/index.mjs +8 -10
- package/build-module/utils/index.mjs.map +2 -2
- package/build-types/entities.d.ts +51 -32
- package/build-types/entities.d.ts.map +1 -1
- package/build-types/hooks/index.d.ts +3 -3
- package/build-types/hooks/index.d.ts.map +1 -1
- package/build-types/hooks/use-entity-record.d.ts +1 -1
- package/build-types/hooks/use-entity-record.d.ts.map +1 -1
- package/build-types/hooks/use-entity-records.d.ts +1 -1
- package/build-types/hooks/use-entity-records.d.ts.map +1 -1
- package/build-types/hooks/use-resource-permissions.d.ts +1 -1
- package/build-types/hooks/use-resource-permissions.d.ts.map +1 -1
- package/build-types/index.d.ts.map +1 -1
- package/build-types/private-actions.d.ts +10 -0
- package/build-types/private-actions.d.ts.map +1 -1
- package/build-types/private-selectors.d.ts +11 -4
- package/build-types/private-selectors.d.ts.map +1 -1
- package/build-types/queried-data/get-query-parts.d.ts +11 -19
- package/build-types/queried-data/get-query-parts.d.ts.map +1 -1
- package/build-types/queried-data/reducer.d.ts +11 -5
- package/build-types/queried-data/reducer.d.ts.map +1 -1
- package/build-types/queried-data/selectors.d.ts +5 -3
- package/build-types/queried-data/selectors.d.ts.map +1 -1
- package/build-types/reducer.d.ts +11 -0
- package/build-types/reducer.d.ts.map +1 -1
- package/build-types/resolvers.d.ts +3 -0
- package/build-types/resolvers.d.ts.map +1 -1
- package/build-types/selectors.d.ts +1 -0
- package/build-types/selectors.d.ts.map +1 -1
- package/build-types/utils/crdt-blocks.d.ts +11 -0
- package/build-types/utils/crdt-blocks.d.ts.map +1 -1
- package/build-types/utils/crdt-text.d.ts +16 -0
- package/build-types/utils/crdt-text.d.ts.map +1 -0
- package/build-types/utils/crdt.d.ts +7 -0
- package/build-types/utils/crdt.d.ts.map +1 -1
- package/build-types/utils/index.d.ts +0 -1
- package/package.json +18 -18
- package/src/entities.js +16 -8
- package/src/hooks/index.ts +3 -3
- package/src/hooks/test/use-entity-records.js +1 -1
- package/src/hooks/use-entity-record.ts +1 -1
- package/src/hooks/use-entity-records.ts +1 -1
- package/src/hooks/use-query-select.ts +1 -1
- package/src/hooks/use-resource-permissions.ts +1 -1
- package/src/private-actions.js +18 -0
- package/src/private-selectors.ts +37 -4
- package/src/queried-data/get-query-parts.js +14 -20
- package/src/queried-data/reducer.js +28 -15
- package/src/queried-data/selectors.js +34 -38
- package/src/queried-data/test/get-query-parts.js +11 -11
- package/src/queried-data/test/reducer.js +78 -8
- package/src/queried-data/test/selectors.js +52 -30
- package/src/reducer.js +20 -0
- package/src/resolvers.js +29 -13
- package/src/selectors.ts +28 -21
- package/src/utils/crdt-blocks.ts +348 -60
- package/src/utils/crdt-text.ts +43 -0
- package/src/utils/crdt.ts +25 -0
- package/src/utils/index.js +0 -1
- package/src/utils/test/crdt-blocks.ts +838 -6
- package/src/utils/test/crdt.ts +147 -1
- package/build/hooks/memoize.cjs +0 -38
- package/build/hooks/memoize.cjs.map +0 -7
- package/build/utils/is-raw-attribute.cjs +0 -29
- package/build/utils/is-raw-attribute.cjs.map +0 -7
- package/build-module/hooks/memoize.mjs +0 -7
- package/build-module/hooks/memoize.mjs.map +0 -7
- package/build-module/utils/is-raw-attribute.mjs +0 -8
- package/build-module/utils/is-raw-attribute.mjs.map +0 -7
- package/build-types/hooks/memoize.d.ts +0 -3
- package/build-types/hooks/memoize.d.ts.map +0 -1
- package/build-types/utils/is-raw-attribute.d.ts +0 -10
- package/build-types/utils/is-raw-attribute.d.ts.map +0 -1
- package/src/hooks/memoize.js +0 -7
- package/src/utils/is-raw-attribute.js +0 -11
- package/src/utils/test/is-raw-attribute.js +0 -22
package/src/selectors.ts
CHANGED
|
@@ -20,7 +20,6 @@ import { DEFAULT_ENTITY_KEY } from './entities';
|
|
|
20
20
|
import { getUndoManager } from './private-selectors';
|
|
21
21
|
import {
|
|
22
22
|
getNormalizedCommaSeparable,
|
|
23
|
-
isRawAttribute,
|
|
24
23
|
setNestedValue,
|
|
25
24
|
isNumericID,
|
|
26
25
|
getUserPermissionCacheKey,
|
|
@@ -55,6 +54,7 @@ export interface State {
|
|
|
55
54
|
editorAssets: Record< string, any > | null;
|
|
56
55
|
syncConnectionStatuses?: Record< string, ConnectionStatus >;
|
|
57
56
|
collaborationSupported: boolean;
|
|
57
|
+
viewConfigs: Record< string, Record< string, any > >;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
type EntityRecordKey = string | number;
|
|
@@ -523,25 +523,26 @@ export const getRawEntityRecord = createSelector(
|
|
|
523
523
|
name,
|
|
524
524
|
key
|
|
525
525
|
);
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
526
|
+
const config = getEntityConfig( state, kind, name );
|
|
527
|
+
if ( ! record || ! config?.rawAttributes?.length ) {
|
|
528
|
+
return record;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Because edits are the "raw" attribute values,
|
|
532
|
+
// we return those from record selectors to make rendering,
|
|
533
|
+
// comparisons, and joins with edits easier.
|
|
534
|
+
return Object.fromEntries(
|
|
535
|
+
Object.keys( record ).map( ( _key ) => {
|
|
536
|
+
if ( config.rawAttributes.includes( _key ) ) {
|
|
537
|
+
const rawValue = record[ _key ]?.raw;
|
|
538
|
+
return [
|
|
539
|
+
_key,
|
|
540
|
+
rawValue !== undefined ? rawValue : record[ _key ],
|
|
541
|
+
];
|
|
541
542
|
}
|
|
542
|
-
return
|
|
543
|
-
}
|
|
544
|
-
);
|
|
543
|
+
return [ _key, record[ _key ] ];
|
|
544
|
+
} )
|
|
545
|
+
) as EntityRecord;
|
|
545
546
|
},
|
|
546
547
|
(
|
|
547
548
|
state: State,
|
|
@@ -654,7 +655,10 @@ export const getEntityRecords = ( <
|
|
|
654
655
|
if ( ! queriedState ) {
|
|
655
656
|
return null;
|
|
656
657
|
}
|
|
657
|
-
return getQueriedItems( queriedState, query
|
|
658
|
+
return getQueriedItems( queriedState, query, {
|
|
659
|
+
supportsPagination: !! getEntityConfig( state, kind, name )
|
|
660
|
+
?.supportsPagination,
|
|
661
|
+
} );
|
|
658
662
|
} ) as GetEntityRecords;
|
|
659
663
|
|
|
660
664
|
/**
|
|
@@ -712,7 +716,10 @@ export const getEntityRecordsTotalPages = (
|
|
|
712
716
|
if ( ! queriedState ) {
|
|
713
717
|
return null;
|
|
714
718
|
}
|
|
715
|
-
if (
|
|
719
|
+
if (
|
|
720
|
+
! getEntityConfig( state, kind, name )?.supportsPagination ||
|
|
721
|
+
query?.per_page === -1
|
|
722
|
+
) {
|
|
716
723
|
return 1;
|
|
717
724
|
}
|
|
718
725
|
const totalItems = getQueriedTotalItems( queriedState, query );
|
package/src/utils/crdt-blocks.ts
CHANGED
|
@@ -16,19 +16,21 @@ import { Y } from '@wordpress/sync';
|
|
|
16
16
|
* Internal dependencies
|
|
17
17
|
*/
|
|
18
18
|
import { createYMap, type YMapRecord, type YMapWrap } from './crdt-utils';
|
|
19
|
+
import { getCachedRichTextData } from './crdt-text';
|
|
19
20
|
import { Delta } from '../sync';
|
|
20
21
|
|
|
21
22
|
interface BlockAttributes {
|
|
22
23
|
[ key: string ]: unknown;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
interface
|
|
26
|
+
interface BlockAttributeSchema {
|
|
26
27
|
role?: string;
|
|
27
28
|
type?: string;
|
|
29
|
+
query?: Record< string, BlockAttributeSchema >;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
interface BlockType {
|
|
31
|
-
attributes?: Record< string,
|
|
33
|
+
attributes?: Record< string, BlockAttributeSchema >;
|
|
32
34
|
name: string;
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -122,6 +124,85 @@ function makeBlocksSerializable( blocks: Block[] ): Block[] {
|
|
|
122
124
|
} );
|
|
123
125
|
}
|
|
124
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Recursively walk an attribute value and convert any strings that correspond
|
|
129
|
+
* to rich-text schema nodes into RichTextData instances. This is the inverse
|
|
130
|
+
* of serializeAttributeValue and handles nested structures like table cells.
|
|
131
|
+
*
|
|
132
|
+
* @param schema The attribute type definition for this value.
|
|
133
|
+
* @param value The attribute value from CRDT (toJSON).
|
|
134
|
+
* @return The value with rich-text strings replaced by RichTextData.
|
|
135
|
+
*/
|
|
136
|
+
function deserializeAttributeValue(
|
|
137
|
+
schema: BlockAttributeSchema | undefined,
|
|
138
|
+
value: unknown
|
|
139
|
+
): unknown {
|
|
140
|
+
if ( schema?.type === 'rich-text' && typeof value === 'string' ) {
|
|
141
|
+
return getCachedRichTextData( value );
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// e.g. core/table `body`: [ { cells: [ { content: RichTextData } ] } ]
|
|
145
|
+
if ( Array.isArray( value ) ) {
|
|
146
|
+
return value.map( ( item ) =>
|
|
147
|
+
deserializeAttributeValue( schema, item )
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// e.g. a single row inside core/table `body`: { cells: [ ... ] }
|
|
152
|
+
if ( value && typeof value === 'object' ) {
|
|
153
|
+
const result: Record< string, unknown > = {};
|
|
154
|
+
|
|
155
|
+
for ( const [ key, innerValue ] of Object.entries(
|
|
156
|
+
value as Record< string, unknown >
|
|
157
|
+
) ) {
|
|
158
|
+
result[ key ] = deserializeAttributeValue(
|
|
159
|
+
schema?.query?.[ key ],
|
|
160
|
+
innerValue
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return value;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Convert blocks from their CRDT-serialized form back to the runtime form
|
|
172
|
+
* expected by the block editor. Rich-text attributes are stored as Y.Text in
|
|
173
|
+
* the CRDT document, which serializes to plain strings via toJSON(). This
|
|
174
|
+
* function restores them to RichTextData instances so that block edit
|
|
175
|
+
* components that rely on RichTextData methods (e.g. `.text`) work correctly.
|
|
176
|
+
*
|
|
177
|
+
* @param blocks Blocks as extracted from the CRDT document via toJSON().
|
|
178
|
+
* @return Blocks with rich-text attributes restored to RichTextData.
|
|
179
|
+
*/
|
|
180
|
+
export function deserializeBlockAttributes( blocks: Block[] ): Block[] {
|
|
181
|
+
return blocks.map( ( block: Block ) => {
|
|
182
|
+
const { name, innerBlocks, attributes, ...rest } = block;
|
|
183
|
+
|
|
184
|
+
const newAttributes = { ...attributes };
|
|
185
|
+
|
|
186
|
+
for ( const [ key, value ] of Object.entries( attributes ) ) {
|
|
187
|
+
const schema = getBlockAttributeSchema( name, key );
|
|
188
|
+
|
|
189
|
+
if ( schema ) {
|
|
190
|
+
newAttributes[ key ] = deserializeAttributeValue(
|
|
191
|
+
schema,
|
|
192
|
+
value
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
...rest,
|
|
199
|
+
name,
|
|
200
|
+
attributes: newAttributes,
|
|
201
|
+
innerBlocks: deserializeBlockAttributes( innerBlocks ?? [] ),
|
|
202
|
+
};
|
|
203
|
+
} );
|
|
204
|
+
}
|
|
205
|
+
|
|
125
206
|
/**
|
|
126
207
|
* @param {any} gblock
|
|
127
208
|
* @param {Y.Map} yblock
|
|
@@ -174,14 +255,89 @@ function createNewYAttributeValue(
|
|
|
174
255
|
blockName: string,
|
|
175
256
|
attributeName: string,
|
|
176
257
|
attributeValue: unknown
|
|
177
|
-
): Y.Text | unknown {
|
|
178
|
-
const
|
|
258
|
+
): Y.Text | Y.Array< unknown > | Y.Map< unknown > | unknown {
|
|
259
|
+
const schema = getBlockAttributeSchema( blockName, attributeName );
|
|
260
|
+
return createYValueFromSchema( schema, attributeValue );
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Recursively create the appropriate Y.js type for a value based on its
|
|
265
|
+
* block-attribute schema.
|
|
266
|
+
*
|
|
267
|
+
* - `rich-text` -> Y.Text
|
|
268
|
+
* - `array` with query -> Y.Array of Y.Maps
|
|
269
|
+
* - `object` with query -> Y.Map
|
|
270
|
+
* - anything else -> plain value (unchanged)
|
|
271
|
+
*
|
|
272
|
+
* @param schema The attribute type definition.
|
|
273
|
+
* @param value The plain JS value to convert.
|
|
274
|
+
* @return A Y.js type or the original value.
|
|
275
|
+
*/
|
|
276
|
+
function createYValueFromSchema(
|
|
277
|
+
schema: BlockAttributeSchema | undefined,
|
|
278
|
+
value: unknown
|
|
279
|
+
): Y.Text | Y.Array< unknown > | Y.Map< unknown > | unknown {
|
|
280
|
+
if ( ! schema ) {
|
|
281
|
+
return value;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if ( schema.type === 'rich-text' ) {
|
|
285
|
+
return new Y.Text( value?.toString() ?? '' );
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if ( schema.type === 'array' && schema.query && Array.isArray( value ) ) {
|
|
289
|
+
const query = schema.query;
|
|
290
|
+
const yArray = new Y.Array< Y.Map< unknown > >();
|
|
291
|
+
|
|
292
|
+
yArray.insert(
|
|
293
|
+
0,
|
|
294
|
+
value.map( ( item ) => createYMapFromQuery( query, item ) )
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
return yArray;
|
|
298
|
+
}
|
|
179
299
|
|
|
180
|
-
if (
|
|
181
|
-
return
|
|
300
|
+
if ( schema.type === 'object' && schema.query && isRecord( value ) ) {
|
|
301
|
+
return createYMapFromQuery( schema.query, value );
|
|
182
302
|
}
|
|
183
303
|
|
|
184
|
-
return
|
|
304
|
+
return value;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Type guard that narrows `unknown` to `Record< string, unknown >`.
|
|
309
|
+
*
|
|
310
|
+
* @param value Value to check.
|
|
311
|
+
* @return True if `value` is a non-null, non-array object.
|
|
312
|
+
*/
|
|
313
|
+
function isRecord( value: unknown ): value is Record< string, unknown > {
|
|
314
|
+
return !! value && typeof value === 'object' && ! Array.isArray( value );
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Create a Y.Map from a plain object, using a query schema to decide which
|
|
319
|
+
* properties should become nested Y.js types (Y.Text, Y.Array, Y.Map).
|
|
320
|
+
*
|
|
321
|
+
* @param query The query schema defining the properties.
|
|
322
|
+
* @param obj The plain object to convert.
|
|
323
|
+
* @return A Y.Map with typed values.
|
|
324
|
+
*/
|
|
325
|
+
function createYMapFromQuery(
|
|
326
|
+
query: Record< string, BlockAttributeSchema >,
|
|
327
|
+
obj: unknown
|
|
328
|
+
): Y.Map< unknown > {
|
|
329
|
+
if ( ! isRecord( obj ) ) {
|
|
330
|
+
return new Y.Map();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const entries: [ string, unknown ][] = Object.entries( obj ).map(
|
|
334
|
+
( [ key, val ] ): [ string, unknown ] => {
|
|
335
|
+
const subSchema = query[ key ];
|
|
336
|
+
return [ key, createYValueFromSchema( subSchema, val ) ];
|
|
337
|
+
}
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
return new Y.Map( entries );
|
|
185
341
|
}
|
|
186
342
|
|
|
187
343
|
function createNewYBlock( block: Block ): YBlock {
|
|
@@ -327,8 +483,16 @@ export function mergeCrdtBlocks(
|
|
|
327
483
|
currentAttribute
|
|
328
484
|
);
|
|
329
485
|
|
|
486
|
+
// Y types (Y.Text, Y.Array, Y.Map) cannot be
|
|
487
|
+
// compared with fastDeepEqual against plain values.
|
|
488
|
+
// Delegate to mergeYValue which handles no-op
|
|
489
|
+
// detection at the edges.
|
|
490
|
+
const isYType =
|
|
491
|
+
currentAttribute instanceof Y.AbstractType;
|
|
492
|
+
|
|
330
493
|
const isAttributeChanged =
|
|
331
494
|
! isExpectedType ||
|
|
495
|
+
isYType ||
|
|
332
496
|
! fastDeepEqual(
|
|
333
497
|
currentAttribute,
|
|
334
498
|
attributeValue
|
|
@@ -418,11 +582,151 @@ export function mergeCrdtBlocks(
|
|
|
418
582
|
}
|
|
419
583
|
|
|
420
584
|
/**
|
|
421
|
-
*
|
|
585
|
+
* Merge an incoming plain array into an existing Y.Array in-place.
|
|
586
|
+
*
|
|
587
|
+
* When the array length is unchanged (stable structure), each element is
|
|
588
|
+
* merged individually via `mergeYMapValues`, preserving concurrent edits to
|
|
589
|
+
* different elements. When the length changes (structural edit such as row
|
|
590
|
+
* insertion/deletion), the Y.Array is rebuilt from scratch.
|
|
591
|
+
*
|
|
592
|
+
* @param yArray The existing Y.Array to update.
|
|
593
|
+
* @param newValue The new plain array to merge into the Y.Array.
|
|
594
|
+
* @param schema The attribute schema (must have `query`).
|
|
595
|
+
* @param cursorPosition The local cursor position for rich-text delta merges.
|
|
596
|
+
*/
|
|
597
|
+
function mergeYArray(
|
|
598
|
+
yArray: Y.Array< unknown >,
|
|
599
|
+
newValue: unknown[],
|
|
600
|
+
schema: BlockAttributeSchema,
|
|
601
|
+
cursorPosition: number | null
|
|
602
|
+
): void {
|
|
603
|
+
if ( ! schema.query ) {
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const query = schema.query;
|
|
608
|
+
|
|
609
|
+
if ( yArray.length === newValue.length ) {
|
|
610
|
+
// Same length: update each element in-place.
|
|
611
|
+
for ( let i = 0; i < newValue.length; i++ ) {
|
|
612
|
+
const currentElement = yArray.get( i );
|
|
613
|
+
const newElement = newValue[ i ];
|
|
614
|
+
|
|
615
|
+
if ( currentElement instanceof Y.Map && isRecord( newElement ) ) {
|
|
616
|
+
mergeYMapValues(
|
|
617
|
+
currentElement,
|
|
618
|
+
newElement,
|
|
619
|
+
query,
|
|
620
|
+
cursorPosition
|
|
621
|
+
);
|
|
622
|
+
} else {
|
|
623
|
+
// Element is the wrong type (e.g. partial migration) or the
|
|
624
|
+
// incoming value is not an object. Rebuild the entire array.
|
|
625
|
+
yArray.delete( 0, yArray.length );
|
|
626
|
+
yArray.insert(
|
|
627
|
+
0,
|
|
628
|
+
newValue.map( ( item ) =>
|
|
629
|
+
createYMapFromQuery( query, item )
|
|
630
|
+
)
|
|
631
|
+
);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
} else {
|
|
636
|
+
// Structure changed: rebuild the Y.Array.
|
|
637
|
+
yArray.delete( 0, yArray.length );
|
|
638
|
+
yArray.insert(
|
|
639
|
+
0,
|
|
640
|
+
newValue.map( ( item ) => createYMapFromQuery( query, item ) )
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Merge a single value into a Y.Map entry, using the attribute schema to
|
|
647
|
+
* decide how to merge.
|
|
648
|
+
*
|
|
649
|
+
* If the current value is already a matching Y.js type (Y.Text, Y.Array,
|
|
650
|
+
* Y.Map), the update is merged in-place so concurrent edits are preserved.
|
|
651
|
+
* Otherwise the value is replaced wholesale.
|
|
652
|
+
*
|
|
653
|
+
* @param schema The attribute type definition for this value.
|
|
654
|
+
* @param newVal The new value to merge into the Y.Map entry.
|
|
655
|
+
* @param yMap The Y.Map that owns this entry.
|
|
656
|
+
* @param key The key of this entry in the Y.Map.
|
|
657
|
+
* @param cursorPosition The local cursor position for rich-text delta merges.
|
|
658
|
+
*/
|
|
659
|
+
function mergeYValue(
|
|
660
|
+
schema: BlockAttributeSchema | undefined,
|
|
661
|
+
newVal: unknown,
|
|
662
|
+
yMap: Y.Map< unknown >,
|
|
663
|
+
key: string,
|
|
664
|
+
cursorPosition: number | null
|
|
665
|
+
): void {
|
|
666
|
+
const currentVal = yMap.get( key );
|
|
667
|
+
if (
|
|
668
|
+
schema?.type === 'rich-text' &&
|
|
669
|
+
typeof newVal === 'string' &&
|
|
670
|
+
currentVal instanceof Y.Text
|
|
671
|
+
) {
|
|
672
|
+
mergeRichTextUpdate( currentVal, newVal, cursorPosition );
|
|
673
|
+
} else if (
|
|
674
|
+
schema?.type === 'array' &&
|
|
675
|
+
schema.query &&
|
|
676
|
+
Array.isArray( newVal ) &&
|
|
677
|
+
currentVal instanceof Y.Array
|
|
678
|
+
) {
|
|
679
|
+
mergeYArray( currentVal, newVal, schema, cursorPosition );
|
|
680
|
+
} else if (
|
|
681
|
+
schema?.type === 'object' &&
|
|
682
|
+
schema.query &&
|
|
683
|
+
isRecord( newVal ) &&
|
|
684
|
+
currentVal instanceof Y.Map
|
|
685
|
+
) {
|
|
686
|
+
mergeYMapValues( currentVal, newVal, schema.query, cursorPosition );
|
|
687
|
+
} else {
|
|
688
|
+
const newYValue = createYValueFromSchema( schema, newVal );
|
|
689
|
+
|
|
690
|
+
// If createYValueFromSchema wrapped the value into a Y type, the
|
|
691
|
+
// current value is the wrong type and needs upgrading. Otherwise,
|
|
692
|
+
// only replace if the raw value actually changed.
|
|
693
|
+
if ( newYValue !== newVal || ! fastDeepEqual( currentVal, newVal ) ) {
|
|
694
|
+
yMap.set( key, newYValue );
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Merge an incoming plain object into an existing Y.Map in-place, using
|
|
701
|
+
* the query schema to decide how each property should be merged.
|
|
422
702
|
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
703
|
+
* Properties present in the Y.Map but absent from `newObj` are deleted.
|
|
704
|
+
*
|
|
705
|
+
* @param yMap The existing Y.Map to update.
|
|
706
|
+
* @param newObj The new plain object to merge into the Y.Map.
|
|
707
|
+
* @param query The query schema defining property types.
|
|
708
|
+
* @param cursorPosition The local cursor position for rich-text delta merges.
|
|
709
|
+
*/
|
|
710
|
+
function mergeYMapValues(
|
|
711
|
+
yMap: Y.Map< unknown >,
|
|
712
|
+
newObj: Record< string, unknown >,
|
|
713
|
+
query: Record< string, BlockAttributeSchema >,
|
|
714
|
+
cursorPosition: number | null
|
|
715
|
+
): void {
|
|
716
|
+
for ( const [ key, newVal ] of Object.entries( newObj ) ) {
|
|
717
|
+
mergeYValue( query[ key ], newVal, yMap, key, cursorPosition );
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Delete properties absent from the incoming object.
|
|
721
|
+
for ( const key of yMap.keys() ) {
|
|
722
|
+
if ( ! Object.hasOwn( newObj, key ) ) {
|
|
723
|
+
yMap.delete( key );
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Update a single attribute on a Yjs block attributes map (currentAttributes).
|
|
426
730
|
*
|
|
427
731
|
* @param blockName The block type name, e.g. 'core/paragraph'.
|
|
428
732
|
* @param attributeName The name of the attribute to update, e.g. 'content'.
|
|
@@ -437,28 +741,22 @@ function updateYBlockAttribute(
|
|
|
437
741
|
currentAttributes: YBlockAttributes,
|
|
438
742
|
cursorPosition: number | null
|
|
439
743
|
): void {
|
|
440
|
-
const
|
|
441
|
-
const currentAttribute = currentAttributes.get( attributeName );
|
|
744
|
+
const schema = getBlockAttributeSchema( blockName, attributeName );
|
|
442
745
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
currentAttributes
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
// Update the value with a delta in place.
|
|
451
|
-
mergeRichTextUpdate( currentAttribute, attributeValue, cursorPosition );
|
|
452
|
-
} else {
|
|
453
|
-
currentAttributes.set(
|
|
454
|
-
attributeName,
|
|
455
|
-
createNewYAttributeValue( blockName, attributeName, attributeValue )
|
|
456
|
-
);
|
|
457
|
-
}
|
|
746
|
+
mergeYValue(
|
|
747
|
+
schema,
|
|
748
|
+
attributeValue,
|
|
749
|
+
currentAttributes,
|
|
750
|
+
attributeName,
|
|
751
|
+
cursorPosition
|
|
752
|
+
);
|
|
458
753
|
}
|
|
459
754
|
|
|
460
755
|
// Cached block attribute types, populated once from getBlockTypes().
|
|
461
|
-
let
|
|
756
|
+
let cachedBlockAttributeSchemas: Map<
|
|
757
|
+
string,
|
|
758
|
+
Map< string, BlockAttributeSchema >
|
|
759
|
+
>;
|
|
462
760
|
|
|
463
761
|
/**
|
|
464
762
|
* Get the attribute type definition for a block attribute.
|
|
@@ -467,22 +765,22 @@ let cachedBlockAttributeTypes: Map< string, Map< string, BlockAttributeType > >;
|
|
|
467
765
|
* @param attributeName The name of the attribute, e.g. 'content'.
|
|
468
766
|
* @return The type definition of the attribute.
|
|
469
767
|
*/
|
|
470
|
-
function
|
|
768
|
+
function getBlockAttributeSchema(
|
|
471
769
|
blockName: string,
|
|
472
770
|
attributeName: string
|
|
473
|
-
):
|
|
474
|
-
if ( !
|
|
771
|
+
): BlockAttributeSchema | undefined {
|
|
772
|
+
if ( ! cachedBlockAttributeSchemas ) {
|
|
475
773
|
// Parse the attributes for all blocks once.
|
|
476
|
-
|
|
774
|
+
cachedBlockAttributeSchemas = new Map();
|
|
477
775
|
|
|
478
776
|
for ( const blockType of getBlockTypes() as BlockType[] ) {
|
|
479
|
-
|
|
777
|
+
cachedBlockAttributeSchemas.set(
|
|
480
778
|
blockType.name,
|
|
481
|
-
new Map< string,
|
|
779
|
+
new Map< string, BlockAttributeSchema >(
|
|
482
780
|
Object.entries( blockType.attributes ?? {} ).map(
|
|
483
781
|
( [ name, definition ] ) => {
|
|
484
|
-
const { role, type } = definition;
|
|
485
|
-
return [ name, { role, type } ];
|
|
782
|
+
const { role, type, query } = definition;
|
|
783
|
+
return [ name, { role, type, query } ];
|
|
486
784
|
}
|
|
487
785
|
)
|
|
488
786
|
)
|
|
@@ -490,7 +788,7 @@ function getBlockAttributeType(
|
|
|
490
788
|
}
|
|
491
789
|
}
|
|
492
790
|
|
|
493
|
-
return
|
|
791
|
+
return cachedBlockAttributeSchemas.get( blockName )?.get( attributeName );
|
|
494
792
|
}
|
|
495
793
|
|
|
496
794
|
/**
|
|
@@ -506,20 +804,24 @@ function isExpectedAttributeType(
|
|
|
506
804
|
attributeName: string,
|
|
507
805
|
attributeValue: unknown
|
|
508
806
|
): boolean {
|
|
509
|
-
const
|
|
510
|
-
blockName,
|
|
511
|
-
attributeName
|
|
512
|
-
)?.type;
|
|
807
|
+
const schema = getBlockAttributeSchema( blockName, attributeName );
|
|
513
808
|
|
|
514
|
-
if (
|
|
809
|
+
if ( schema?.type === 'rich-text' ) {
|
|
515
810
|
return attributeValue instanceof Y.Text;
|
|
516
811
|
}
|
|
517
812
|
|
|
518
|
-
if (
|
|
813
|
+
if ( schema?.type === 'string' ) {
|
|
519
814
|
return typeof attributeValue === 'string';
|
|
520
815
|
}
|
|
521
816
|
|
|
522
|
-
|
|
817
|
+
if ( schema?.type === 'array' && schema.query ) {
|
|
818
|
+
return attributeValue instanceof Y.Array;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if ( schema?.type === 'object' && schema.query ) {
|
|
822
|
+
return attributeValue instanceof Y.Map;
|
|
823
|
+
}
|
|
824
|
+
|
|
523
825
|
return true;
|
|
524
826
|
}
|
|
525
827
|
|
|
@@ -532,22 +834,8 @@ function isExpectedAttributeType(
|
|
|
532
834
|
* @return True if the attribute is local, false otherwise.
|
|
533
835
|
*/
|
|
534
836
|
function isLocalAttribute( blockName: string, attributeName: string ): boolean {
|
|
535
|
-
return 'local' === getBlockAttributeType( blockName, attributeName )?.role;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
/**
|
|
539
|
-
* Given a block name and attribute key, return true if the attribute is rich-text typed.
|
|
540
|
-
*
|
|
541
|
-
* @param blockName The name of the block, e.g. 'core/paragraph'.
|
|
542
|
-
* @param attributeName The name of the attribute to check, e.g. 'content'.
|
|
543
|
-
* @return True if the attribute is rich-text typed, false otherwise.
|
|
544
|
-
*/
|
|
545
|
-
function isRichTextAttribute(
|
|
546
|
-
blockName: string,
|
|
547
|
-
attributeName: string
|
|
548
|
-
): boolean {
|
|
549
837
|
return (
|
|
550
|
-
'
|
|
838
|
+
'local' === getBlockAttributeSchema( blockName, attributeName )?.role
|
|
551
839
|
);
|
|
552
840
|
}
|
|
553
841
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { RichTextData } from '@wordpress/rich-text';
|
|
5
|
+
|
|
6
|
+
const RICH_TEXT_CACHE_MAX_SIZE = 500;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Returns a function that converts HTML strings to RichTextData instances,
|
|
10
|
+
* using a FIFO cache bounded by `maxSize` to avoid re-parsing identical
|
|
11
|
+
* strings. Repeated calls with the same string return the cached instance
|
|
12
|
+
* without re-running the HTML parser and DOM traversal.
|
|
13
|
+
*
|
|
14
|
+
* @param maxSize Maximum number of entries to hold in the cache.
|
|
15
|
+
* @return A cached version of RichTextData.fromHTMLString.
|
|
16
|
+
*/
|
|
17
|
+
export function createRichTextDataCache(
|
|
18
|
+
maxSize: number
|
|
19
|
+
): ( value: string ) => RichTextData {
|
|
20
|
+
const cache = new Map< string, RichTextData >();
|
|
21
|
+
|
|
22
|
+
return function ( value: string ): RichTextData {
|
|
23
|
+
const cached = cache.get( value );
|
|
24
|
+
|
|
25
|
+
if ( cached ) {
|
|
26
|
+
return cached;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const result = RichTextData.fromHTMLString( value );
|
|
30
|
+
|
|
31
|
+
if ( cache.size >= maxSize ) {
|
|
32
|
+
// Evict the oldest entry (Map preserves insertion order).
|
|
33
|
+
cache.delete( cache.keys().next().value! );
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
cache.set( value, result );
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const getCachedRichTextData = createRichTextDataCache(
|
|
42
|
+
RICH_TEXT_CACHE_MAX_SIZE
|
|
43
|
+
);
|
package/src/utils/crdt.ts
CHANGED
|
@@ -11,6 +11,8 @@ import { __unstableSerializeAndClean } from '@wordpress/blocks';
|
|
|
11
11
|
import {
|
|
12
12
|
type CRDTDoc,
|
|
13
13
|
type ObjectData,
|
|
14
|
+
type ObjectID,
|
|
15
|
+
type ObjectType,
|
|
14
16
|
type SyncConfig,
|
|
15
17
|
Y,
|
|
16
18
|
} from '@wordpress/sync';
|
|
@@ -20,6 +22,7 @@ import {
|
|
|
20
22
|
*/
|
|
21
23
|
import { BaseAwareness } from '../awareness/base-awareness';
|
|
22
24
|
import {
|
|
25
|
+
deserializeBlockAttributes,
|
|
23
26
|
mergeCrdtBlocks,
|
|
24
27
|
mergeRichTextUpdate,
|
|
25
28
|
type Block,
|
|
@@ -382,6 +385,15 @@ export function getPostChangesFromCRDTDoc(
|
|
|
382
385
|
} )
|
|
383
386
|
);
|
|
384
387
|
|
|
388
|
+
// Blocks extracted from the CRDT document have rich-text attributes as
|
|
389
|
+
// plain strings (from Y.Text.toJSON()). Convert them back to RichTextData
|
|
390
|
+
// so block edit components receive the same types as locally-created blocks.
|
|
391
|
+
if ( changes.blocks ) {
|
|
392
|
+
changes.blocks = deserializeBlockAttributes(
|
|
393
|
+
changes.blocks as Block[]
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
385
397
|
// Meta changes must be merged with the edited record since not all meta
|
|
386
398
|
// properties are synced.
|
|
387
399
|
if ( 'object' === typeof changes.meta ) {
|
|
@@ -418,6 +430,19 @@ export const defaultSyncConfig: SyncConfig = {
|
|
|
418
430
|
getChangesFromCRDTDoc: defaultGetChangesFromCRDTDoc,
|
|
419
431
|
};
|
|
420
432
|
|
|
433
|
+
/**
|
|
434
|
+
* This default collection sync config can be used to sync entity collections
|
|
435
|
+
* (e.g., block comments) where we are not interested in merging changes at the
|
|
436
|
+
* individual record level, but instead want to replace the entire collection
|
|
437
|
+
* when changes are detected.
|
|
438
|
+
*/
|
|
439
|
+
export const defaultCollectionSyncConfig: SyncConfig = {
|
|
440
|
+
applyChangesToCRDTDoc: () => {},
|
|
441
|
+
getChangesFromCRDTDoc: () => ( {} ),
|
|
442
|
+
shouldSync: ( _: ObjectType, objectId: ObjectID | null ) =>
|
|
443
|
+
null === objectId,
|
|
444
|
+
};
|
|
445
|
+
|
|
421
446
|
/**
|
|
422
447
|
* Extract the raw string value from a property that may be a string or an object
|
|
423
448
|
* with a `raw` property (`RenderedText`).
|
package/src/utils/index.js
CHANGED
|
@@ -5,7 +5,6 @@ export { default as forwardResolver } from './forward-resolver';
|
|
|
5
5
|
export { default as onSubKey } from './on-sub-key';
|
|
6
6
|
export { default as replaceAction } from './replace-action';
|
|
7
7
|
export { default as withWeakMapCache } from './with-weak-map-cache';
|
|
8
|
-
export { default as isRawAttribute } from './is-raw-attribute';
|
|
9
8
|
export { default as setNestedValue } from './set-nested-value';
|
|
10
9
|
export { default as getNestedValue } from './get-nested-value';
|
|
11
10
|
export { default as isNumericID } from './is-numeric-id';
|