@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
|
@@ -38,14 +38,65 @@ jest.mock( '@wordpress/blocks', () => ( {
|
|
|
38
38
|
url: { type: 'string' },
|
|
39
39
|
},
|
|
40
40
|
},
|
|
41
|
+
{
|
|
42
|
+
name: 'core/test-object-query',
|
|
43
|
+
attributes: {
|
|
44
|
+
metadata: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
query: {
|
|
47
|
+
title: { type: 'rich-text' },
|
|
48
|
+
value: { type: 'string' },
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
41
53
|
{
|
|
42
54
|
name: 'core/table',
|
|
43
55
|
attributes: {
|
|
44
56
|
hasFixedLayout: { type: 'boolean' },
|
|
45
57
|
caption: { type: 'rich-text' },
|
|
46
|
-
head: {
|
|
47
|
-
|
|
48
|
-
|
|
58
|
+
head: {
|
|
59
|
+
type: 'array',
|
|
60
|
+
query: {
|
|
61
|
+
cells: {
|
|
62
|
+
type: 'array',
|
|
63
|
+
query: {
|
|
64
|
+
content: { type: 'rich-text' },
|
|
65
|
+
tag: { type: 'string' },
|
|
66
|
+
scope: { type: 'string' },
|
|
67
|
+
align: { type: 'string' },
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
body: {
|
|
73
|
+
type: 'array',
|
|
74
|
+
query: {
|
|
75
|
+
cells: {
|
|
76
|
+
type: 'array',
|
|
77
|
+
query: {
|
|
78
|
+
content: { type: 'rich-text' },
|
|
79
|
+
tag: { type: 'string' },
|
|
80
|
+
scope: { type: 'string' },
|
|
81
|
+
align: { type: 'string' },
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
foot: {
|
|
87
|
+
type: 'array',
|
|
88
|
+
query: {
|
|
89
|
+
cells: {
|
|
90
|
+
type: 'array',
|
|
91
|
+
query: {
|
|
92
|
+
content: { type: 'rich-text' },
|
|
93
|
+
tag: { type: 'string' },
|
|
94
|
+
scope: { type: 'string' },
|
|
95
|
+
align: { type: 'string' },
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
49
100
|
},
|
|
50
101
|
},
|
|
51
102
|
],
|
|
@@ -67,6 +118,7 @@ import {
|
|
|
67
118
|
type YBlocks,
|
|
68
119
|
type YBlockAttributes,
|
|
69
120
|
} from '../crdt-blocks';
|
|
121
|
+
import { getCachedRichTextData, createRichTextDataCache } from '../crdt-text';
|
|
70
122
|
|
|
71
123
|
describe( 'crdt-blocks', () => {
|
|
72
124
|
let doc: Y.Doc;
|
|
@@ -1158,7 +1210,8 @@ describe( 'crdt-blocks', () => {
|
|
|
1158
1210
|
|
|
1159
1211
|
const block = yblocks2.get( 0 );
|
|
1160
1212
|
const attrs = block.get( 'attributes' ) as YBlockAttributes;
|
|
1161
|
-
const
|
|
1213
|
+
const bodyYArray = attrs.get( 'body' ) as Y.Array< unknown >;
|
|
1214
|
+
const body = bodyYArray.toJSON() as {
|
|
1162
1215
|
cells: { content: string; tag: string }[];
|
|
1163
1216
|
}[];
|
|
1164
1217
|
|
|
@@ -1217,14 +1270,16 @@ describe( 'crdt-blocks', () => {
|
|
|
1217
1270
|
const block = yblocks2.get( 0 );
|
|
1218
1271
|
const attrs = block.get( 'attributes' ) as YBlockAttributes;
|
|
1219
1272
|
|
|
1220
|
-
const
|
|
1273
|
+
const headYArray = attrs.get( 'head' ) as Y.Array< unknown >;
|
|
1274
|
+
const head = headYArray.toJSON() as {
|
|
1221
1275
|
cells: { content: string }[];
|
|
1222
1276
|
}[];
|
|
1223
1277
|
expect( head[ 0 ].cells[ 0 ].content ).toBe(
|
|
1224
1278
|
'<strong>Header</strong>'
|
|
1225
1279
|
);
|
|
1226
1280
|
|
|
1227
|
-
const
|
|
1281
|
+
const bodyYArray = attrs.get( 'body' ) as Y.Array< unknown >;
|
|
1282
|
+
const body = bodyYArray.toJSON() as {
|
|
1228
1283
|
cells: { content: string }[];
|
|
1229
1284
|
}[];
|
|
1230
1285
|
expect( body[ 0 ].cells[ 0 ].content ).toBe(
|
|
@@ -1233,6 +1288,731 @@ describe( 'crdt-blocks', () => {
|
|
|
1233
1288
|
|
|
1234
1289
|
doc2.destroy();
|
|
1235
1290
|
} );
|
|
1291
|
+
|
|
1292
|
+
it( 'stores table body as nested Y types (Y.Array of Y.Maps with Y.Text)', () => {
|
|
1293
|
+
const tableBlocks: Block[] = [
|
|
1294
|
+
{
|
|
1295
|
+
name: 'core/table',
|
|
1296
|
+
attributes: {
|
|
1297
|
+
hasFixedLayout: true,
|
|
1298
|
+
body: [
|
|
1299
|
+
{
|
|
1300
|
+
cells: [
|
|
1301
|
+
{
|
|
1302
|
+
content:
|
|
1303
|
+
RichTextData.fromPlainText( 'A1' ),
|
|
1304
|
+
tag: 'td',
|
|
1305
|
+
},
|
|
1306
|
+
{
|
|
1307
|
+
content:
|
|
1308
|
+
RichTextData.fromPlainText( 'B1' ),
|
|
1309
|
+
tag: 'td',
|
|
1310
|
+
},
|
|
1311
|
+
],
|
|
1312
|
+
},
|
|
1313
|
+
],
|
|
1314
|
+
},
|
|
1315
|
+
innerBlocks: [],
|
|
1316
|
+
},
|
|
1317
|
+
];
|
|
1318
|
+
|
|
1319
|
+
mergeCrdtBlocks( yblocks, tableBlocks, null );
|
|
1320
|
+
|
|
1321
|
+
const attrs = yblocks
|
|
1322
|
+
.get( 0 )
|
|
1323
|
+
.get( 'attributes' ) as YBlockAttributes;
|
|
1324
|
+
const body = attrs.get( 'body' );
|
|
1325
|
+
|
|
1326
|
+
// body should be a Y.Array, not a plain array.
|
|
1327
|
+
expect( body ).toBeInstanceOf( Y.Array );
|
|
1328
|
+
|
|
1329
|
+
// Each row should be a Y.Map.
|
|
1330
|
+
const row = ( body as Y.Array< unknown > ).get( 0 );
|
|
1331
|
+
expect( row ).toBeInstanceOf( Y.Map );
|
|
1332
|
+
|
|
1333
|
+
// Each row's cells should be a Y.Array.
|
|
1334
|
+
const cells = ( row as Y.Map< unknown > ).get( 'cells' );
|
|
1335
|
+
expect( cells ).toBeInstanceOf( Y.Array );
|
|
1336
|
+
|
|
1337
|
+
// Each cell should be a Y.Map with Y.Text content.
|
|
1338
|
+
const cell = ( cells as Y.Array< unknown > ).get( 0 );
|
|
1339
|
+
expect( cell ).toBeInstanceOf( Y.Map );
|
|
1340
|
+
|
|
1341
|
+
const content = ( cell as Y.Map< unknown > ).get(
|
|
1342
|
+
'content'
|
|
1343
|
+
) as Y.Text;
|
|
1344
|
+
expect( content ).toBeInstanceOf( Y.Text );
|
|
1345
|
+
expect( content.toString() ).toBe( 'A1' );
|
|
1346
|
+
|
|
1347
|
+
// tag should be a plain string value.
|
|
1348
|
+
expect( ( cell as Y.Map< unknown > ).get( 'tag' ) ).toBe( 'td' );
|
|
1349
|
+
} );
|
|
1350
|
+
|
|
1351
|
+
it( 'merges table cell edits in-place without replacing sibling cells', () => {
|
|
1352
|
+
const tableBlocks: Block[] = [
|
|
1353
|
+
{
|
|
1354
|
+
name: 'core/table',
|
|
1355
|
+
attributes: {
|
|
1356
|
+
body: [
|
|
1357
|
+
{
|
|
1358
|
+
cells: [
|
|
1359
|
+
{ content: 'A1', tag: 'td' },
|
|
1360
|
+
{ content: 'B1', tag: 'td' },
|
|
1361
|
+
],
|
|
1362
|
+
},
|
|
1363
|
+
{
|
|
1364
|
+
cells: [
|
|
1365
|
+
{ content: 'A2', tag: 'td' },
|
|
1366
|
+
{ content: 'B2', tag: 'td' },
|
|
1367
|
+
],
|
|
1368
|
+
},
|
|
1369
|
+
],
|
|
1370
|
+
},
|
|
1371
|
+
innerBlocks: [],
|
|
1372
|
+
},
|
|
1373
|
+
];
|
|
1374
|
+
|
|
1375
|
+
mergeCrdtBlocks( yblocks, tableBlocks, null );
|
|
1376
|
+
|
|
1377
|
+
// Grab the Y.Text for cell B2 before the update.
|
|
1378
|
+
const attrs = yblocks
|
|
1379
|
+
.get( 0 )
|
|
1380
|
+
.get( 'attributes' ) as YBlockAttributes;
|
|
1381
|
+
const body = attrs.get( 'body' ) as Y.Array< unknown >;
|
|
1382
|
+
const row1 = body.get( 1 ) as Y.Map< unknown >;
|
|
1383
|
+
const cells1 = row1.get( 'cells' ) as Y.Array< unknown >;
|
|
1384
|
+
const cellB2 = cells1.get( 1 ) as Y.Map< unknown >;
|
|
1385
|
+
const b2Text = cellB2.get( 'content' ) as Y.Text;
|
|
1386
|
+
|
|
1387
|
+
// Edit only cell A1.
|
|
1388
|
+
const updatedBlocks: Block[] = [
|
|
1389
|
+
{
|
|
1390
|
+
name: 'core/table',
|
|
1391
|
+
attributes: {
|
|
1392
|
+
body: [
|
|
1393
|
+
{
|
|
1394
|
+
cells: [
|
|
1395
|
+
{ content: 'A1-edited', tag: 'td' },
|
|
1396
|
+
{ content: 'B1', tag: 'td' },
|
|
1397
|
+
],
|
|
1398
|
+
},
|
|
1399
|
+
{
|
|
1400
|
+
cells: [
|
|
1401
|
+
{ content: 'A2', tag: 'td' },
|
|
1402
|
+
{ content: 'B2', tag: 'td' },
|
|
1403
|
+
],
|
|
1404
|
+
},
|
|
1405
|
+
],
|
|
1406
|
+
},
|
|
1407
|
+
innerBlocks: [],
|
|
1408
|
+
},
|
|
1409
|
+
];
|
|
1410
|
+
|
|
1411
|
+
mergeCrdtBlocks( yblocks, updatedBlocks, null );
|
|
1412
|
+
|
|
1413
|
+
// The Y.Text for B2 should be the exact same object (identity).
|
|
1414
|
+
const bodyAfter = attrs.get( 'body' ) as Y.Array< unknown >;
|
|
1415
|
+
const row1After = bodyAfter.get( 1 ) as Y.Map< unknown >;
|
|
1416
|
+
const cells1After = row1After.get( 'cells' ) as Y.Array< unknown >;
|
|
1417
|
+
const cellB2After = cells1After.get( 1 ) as Y.Map< unknown >;
|
|
1418
|
+
const b2TextAfter = cellB2After.get( 'content' ) as Y.Text;
|
|
1419
|
+
|
|
1420
|
+
expect( b2TextAfter ).toBe( b2Text );
|
|
1421
|
+
expect( b2TextAfter.toString() ).toBe( 'B2' );
|
|
1422
|
+
|
|
1423
|
+
// Cell A1 should be updated.
|
|
1424
|
+
const row0After = bodyAfter.get( 0 ) as Y.Map< unknown >;
|
|
1425
|
+
const cells0After = row0After.get( 'cells' ) as Y.Array< unknown >;
|
|
1426
|
+
const cellA1After = cells0After.get( 0 ) as Y.Map< unknown >;
|
|
1427
|
+
const a1Content = cellA1After.get( 'content' ) as Y.Text;
|
|
1428
|
+
expect( a1Content.toString() ).toBe( 'A1-edited' );
|
|
1429
|
+
} );
|
|
1430
|
+
|
|
1431
|
+
it( 'rebuilds Y.Array when row count changes (structural edit)', () => {
|
|
1432
|
+
const tableBlocks: Block[] = [
|
|
1433
|
+
{
|
|
1434
|
+
name: 'core/table',
|
|
1435
|
+
attributes: {
|
|
1436
|
+
body: [
|
|
1437
|
+
{
|
|
1438
|
+
cells: [ { content: 'A1', tag: 'td' } ],
|
|
1439
|
+
},
|
|
1440
|
+
],
|
|
1441
|
+
},
|
|
1442
|
+
innerBlocks: [],
|
|
1443
|
+
},
|
|
1444
|
+
];
|
|
1445
|
+
|
|
1446
|
+
mergeCrdtBlocks( yblocks, tableBlocks, null );
|
|
1447
|
+
|
|
1448
|
+
// Add a second row.
|
|
1449
|
+
const updatedBlocks: Block[] = [
|
|
1450
|
+
{
|
|
1451
|
+
name: 'core/table',
|
|
1452
|
+
attributes: {
|
|
1453
|
+
body: [
|
|
1454
|
+
{
|
|
1455
|
+
cells: [ { content: 'A1', tag: 'td' } ],
|
|
1456
|
+
},
|
|
1457
|
+
{
|
|
1458
|
+
cells: [ { content: 'A2', tag: 'td' } ],
|
|
1459
|
+
},
|
|
1460
|
+
],
|
|
1461
|
+
},
|
|
1462
|
+
innerBlocks: [],
|
|
1463
|
+
},
|
|
1464
|
+
];
|
|
1465
|
+
|
|
1466
|
+
mergeCrdtBlocks( yblocks, updatedBlocks, null );
|
|
1467
|
+
|
|
1468
|
+
const attrs = yblocks
|
|
1469
|
+
.get( 0 )
|
|
1470
|
+
.get( 'attributes' ) as YBlockAttributes;
|
|
1471
|
+
const body = attrs.get( 'body' ) as Y.Array< unknown >;
|
|
1472
|
+
|
|
1473
|
+
expect( body.length ).toBe( 2 );
|
|
1474
|
+
|
|
1475
|
+
const row1 = body.get( 1 ) as Y.Map< unknown >;
|
|
1476
|
+
const cells = ( row1.get( 'cells' ) as Y.Array< unknown > ).get(
|
|
1477
|
+
0
|
|
1478
|
+
) as Y.Map< unknown >;
|
|
1479
|
+
const a2Content = cells.get( 'content' ) as Y.Text;
|
|
1480
|
+
expect( a2Content.toString() ).toBe( 'A2' );
|
|
1481
|
+
} );
|
|
1482
|
+
|
|
1483
|
+
it( 'concurrent cell edits on different cells are both preserved', () => {
|
|
1484
|
+
// Simulate two users editing different cells in the same table.
|
|
1485
|
+
const initialBlocks: Block[] = [
|
|
1486
|
+
{
|
|
1487
|
+
name: 'core/table',
|
|
1488
|
+
attributes: {
|
|
1489
|
+
body: [
|
|
1490
|
+
{
|
|
1491
|
+
cells: [
|
|
1492
|
+
{ content: 'A1', tag: 'td' },
|
|
1493
|
+
{ content: 'B1', tag: 'td' },
|
|
1494
|
+
],
|
|
1495
|
+
},
|
|
1496
|
+
],
|
|
1497
|
+
},
|
|
1498
|
+
innerBlocks: [],
|
|
1499
|
+
},
|
|
1500
|
+
];
|
|
1501
|
+
|
|
1502
|
+
// Set up doc1 (User A).
|
|
1503
|
+
mergeCrdtBlocks( yblocks, initialBlocks, null );
|
|
1504
|
+
|
|
1505
|
+
// Set up doc2 (User B) by syncing initial state.
|
|
1506
|
+
const doc2 = new Y.Doc();
|
|
1507
|
+
const yblocks2 = doc2.getArray< YBlock >();
|
|
1508
|
+
Y.applyUpdate( doc2, Y.encodeStateAsUpdate( doc ) );
|
|
1509
|
+
|
|
1510
|
+
// User A edits cell A1.
|
|
1511
|
+
const userABlocks: Block[] = [
|
|
1512
|
+
{
|
|
1513
|
+
name: 'core/table',
|
|
1514
|
+
attributes: {
|
|
1515
|
+
body: [
|
|
1516
|
+
{
|
|
1517
|
+
cells: [
|
|
1518
|
+
{ content: 'A1-userA', tag: 'td' },
|
|
1519
|
+
{ content: 'B1', tag: 'td' },
|
|
1520
|
+
],
|
|
1521
|
+
},
|
|
1522
|
+
],
|
|
1523
|
+
},
|
|
1524
|
+
innerBlocks: [],
|
|
1525
|
+
},
|
|
1526
|
+
];
|
|
1527
|
+
mergeCrdtBlocks( yblocks, userABlocks, null );
|
|
1528
|
+
|
|
1529
|
+
// User B edits cell B1 (concurrently, before syncing A's change).
|
|
1530
|
+
const userBBlocks: Block[] = [
|
|
1531
|
+
{
|
|
1532
|
+
name: 'core/table',
|
|
1533
|
+
attributes: {
|
|
1534
|
+
body: [
|
|
1535
|
+
{
|
|
1536
|
+
cells: [
|
|
1537
|
+
{ content: 'A1', tag: 'td' },
|
|
1538
|
+
{ content: 'B1-userB', tag: 'td' },
|
|
1539
|
+
],
|
|
1540
|
+
},
|
|
1541
|
+
],
|
|
1542
|
+
},
|
|
1543
|
+
innerBlocks: [],
|
|
1544
|
+
},
|
|
1545
|
+
];
|
|
1546
|
+
mergeCrdtBlocks( yblocks2, userBBlocks, null );
|
|
1547
|
+
|
|
1548
|
+
// Sync: apply each other's changes.
|
|
1549
|
+
const updateA = Y.encodeStateAsUpdate( doc );
|
|
1550
|
+
const updateB = Y.encodeStateAsUpdate( doc2 );
|
|
1551
|
+
Y.applyUpdate( doc2, updateA );
|
|
1552
|
+
Y.applyUpdate( doc, updateB );
|
|
1553
|
+
|
|
1554
|
+
// Both docs should have both edits preserved.
|
|
1555
|
+
for ( const checkBlocks of [ yblocks, yblocks2 ] ) {
|
|
1556
|
+
const attrs = checkBlocks
|
|
1557
|
+
.get( 0 )
|
|
1558
|
+
.get( 'attributes' ) as YBlockAttributes;
|
|
1559
|
+
const body = (
|
|
1560
|
+
attrs.get( 'body' ) as Y.Array< unknown >
|
|
1561
|
+
).toJSON() as { cells: { content: string }[] }[];
|
|
1562
|
+
|
|
1563
|
+
expect( body[ 0 ].cells[ 0 ].content ).toBe( 'A1-userA' );
|
|
1564
|
+
expect( body[ 0 ].cells[ 1 ].content ).toBe( 'B1-userB' );
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
doc2.destroy();
|
|
1568
|
+
} );
|
|
1569
|
+
|
|
1570
|
+
it( 'migrates plain array to Y.Array on first update', () => {
|
|
1571
|
+
// Manually set up a block with a plain array body (old format).
|
|
1572
|
+
const block = new Y.Map() as unknown as YBlock;
|
|
1573
|
+
block.set( 'name' as any, 'core/table' );
|
|
1574
|
+
block.set( 'clientId' as any, 'table-migration' );
|
|
1575
|
+
block.set( 'innerBlocks' as any, new Y.Array() );
|
|
1576
|
+
|
|
1577
|
+
const attrs = new Y.Map();
|
|
1578
|
+
attrs.set( 'hasFixedLayout', true );
|
|
1579
|
+
// Store body as a plain array (pre-migration format).
|
|
1580
|
+
attrs.set( 'body', [
|
|
1581
|
+
{ cells: [ { content: 'old', tag: 'td' } ] },
|
|
1582
|
+
] );
|
|
1583
|
+
block.set( 'attributes' as any, attrs );
|
|
1584
|
+
|
|
1585
|
+
doc.transact( () => {
|
|
1586
|
+
yblocks.push( [ block ] );
|
|
1587
|
+
} );
|
|
1588
|
+
|
|
1589
|
+
// The body is currently a plain array.
|
|
1590
|
+
expect( attrs.get( 'body' ) ).not.toBeInstanceOf( Y.Array );
|
|
1591
|
+
|
|
1592
|
+
// Now merge blocks, which should trigger migration.
|
|
1593
|
+
const updatedBlocks: Block[] = [
|
|
1594
|
+
{
|
|
1595
|
+
name: 'core/table',
|
|
1596
|
+
attributes: {
|
|
1597
|
+
hasFixedLayout: true,
|
|
1598
|
+
body: [
|
|
1599
|
+
{
|
|
1600
|
+
cells: [ { content: 'migrated', tag: 'td' } ],
|
|
1601
|
+
},
|
|
1602
|
+
],
|
|
1603
|
+
},
|
|
1604
|
+
innerBlocks: [],
|
|
1605
|
+
},
|
|
1606
|
+
];
|
|
1607
|
+
|
|
1608
|
+
mergeCrdtBlocks( yblocks, updatedBlocks, null );
|
|
1609
|
+
|
|
1610
|
+
// After migration, body should be a Y.Array.
|
|
1611
|
+
const bodyAfter = attrs.get( 'body' );
|
|
1612
|
+
expect( bodyAfter ).toBeInstanceOf( Y.Array );
|
|
1613
|
+
|
|
1614
|
+
const bodyJson = ( bodyAfter as Y.Array< unknown > ).toJSON() as {
|
|
1615
|
+
cells: { content: string }[];
|
|
1616
|
+
}[];
|
|
1617
|
+
expect( bodyJson[ 0 ].cells[ 0 ].content ).toBe( 'migrated' );
|
|
1618
|
+
} );
|
|
1619
|
+
|
|
1620
|
+
it( 'preserves non-rich-text cell properties alongside Y.Text content', () => {
|
|
1621
|
+
const tableBlocks: Block[] = [
|
|
1622
|
+
{
|
|
1623
|
+
name: 'core/table',
|
|
1624
|
+
attributes: {
|
|
1625
|
+
body: [
|
|
1626
|
+
{
|
|
1627
|
+
cells: [
|
|
1628
|
+
{
|
|
1629
|
+
content: 'Header',
|
|
1630
|
+
tag: 'th',
|
|
1631
|
+
scope: 'col',
|
|
1632
|
+
align: 'center',
|
|
1633
|
+
},
|
|
1634
|
+
],
|
|
1635
|
+
},
|
|
1636
|
+
],
|
|
1637
|
+
},
|
|
1638
|
+
innerBlocks: [],
|
|
1639
|
+
},
|
|
1640
|
+
];
|
|
1641
|
+
|
|
1642
|
+
mergeCrdtBlocks( yblocks, tableBlocks, null );
|
|
1643
|
+
|
|
1644
|
+
const attrs = yblocks
|
|
1645
|
+
.get( 0 )
|
|
1646
|
+
.get( 'attributes' ) as YBlockAttributes;
|
|
1647
|
+
const body = attrs.get( 'body' ) as Y.Array< unknown >;
|
|
1648
|
+
const row = body.get( 0 ) as Y.Map< unknown >;
|
|
1649
|
+
const cells = row.get( 'cells' ) as Y.Array< unknown >;
|
|
1650
|
+
const cell = cells.get( 0 ) as Y.Map< unknown >;
|
|
1651
|
+
|
|
1652
|
+
// Rich-text content should be Y.Text.
|
|
1653
|
+
const content = cell.get( 'content' ) as Y.Text;
|
|
1654
|
+
expect( content ).toBeInstanceOf( Y.Text );
|
|
1655
|
+
expect( content.toString() ).toBe( 'Header' );
|
|
1656
|
+
|
|
1657
|
+
// Plain string properties should be stored as-is.
|
|
1658
|
+
expect( cell.get( 'tag' ) ).toBe( 'th' );
|
|
1659
|
+
expect( cell.get( 'scope' ) ).toBe( 'col' );
|
|
1660
|
+
expect( cell.get( 'align' ) ).toBe( 'center' );
|
|
1661
|
+
|
|
1662
|
+
// Update only the content, verify other properties remain.
|
|
1663
|
+
const updatedBlocks: Block[] = [
|
|
1664
|
+
{
|
|
1665
|
+
name: 'core/table',
|
|
1666
|
+
attributes: {
|
|
1667
|
+
body: [
|
|
1668
|
+
{
|
|
1669
|
+
cells: [
|
|
1670
|
+
{
|
|
1671
|
+
content: 'Updated Header',
|
|
1672
|
+
tag: 'th',
|
|
1673
|
+
scope: 'col',
|
|
1674
|
+
align: 'center',
|
|
1675
|
+
},
|
|
1676
|
+
],
|
|
1677
|
+
},
|
|
1678
|
+
],
|
|
1679
|
+
},
|
|
1680
|
+
innerBlocks: [],
|
|
1681
|
+
},
|
|
1682
|
+
];
|
|
1683
|
+
|
|
1684
|
+
mergeCrdtBlocks( yblocks, updatedBlocks, null );
|
|
1685
|
+
|
|
1686
|
+
const cellAfter = (
|
|
1687
|
+
(
|
|
1688
|
+
( attrs.get( 'body' ) as Y.Array< unknown > ).get(
|
|
1689
|
+
0
|
|
1690
|
+
) as Y.Map< unknown >
|
|
1691
|
+
).get( 'cells' ) as Y.Array< unknown >
|
|
1692
|
+
).get( 0 ) as Y.Map< unknown >;
|
|
1693
|
+
|
|
1694
|
+
expect( ( cellAfter.get( 'content' ) as Y.Text ).toString() ).toBe(
|
|
1695
|
+
'Updated Header'
|
|
1696
|
+
);
|
|
1697
|
+
expect( cellAfter.get( 'tag' ) ).toBe( 'th' );
|
|
1698
|
+
expect( cellAfter.get( 'scope' ) ).toBe( 'col' );
|
|
1699
|
+
expect( cellAfter.get( 'align' ) ).toBe( 'center' );
|
|
1700
|
+
} );
|
|
1701
|
+
|
|
1702
|
+
it( 'deletes removed properties from Y.Map cells', () => {
|
|
1703
|
+
const tableBlocks: Block[] = [
|
|
1704
|
+
{
|
|
1705
|
+
name: 'core/table',
|
|
1706
|
+
attributes: {
|
|
1707
|
+
body: [
|
|
1708
|
+
{
|
|
1709
|
+
cells: [
|
|
1710
|
+
{
|
|
1711
|
+
content: 'Header',
|
|
1712
|
+
tag: 'th',
|
|
1713
|
+
scope: 'col',
|
|
1714
|
+
},
|
|
1715
|
+
],
|
|
1716
|
+
},
|
|
1717
|
+
],
|
|
1718
|
+
},
|
|
1719
|
+
innerBlocks: [],
|
|
1720
|
+
},
|
|
1721
|
+
];
|
|
1722
|
+
|
|
1723
|
+
mergeCrdtBlocks( yblocks, tableBlocks, null );
|
|
1724
|
+
|
|
1725
|
+
const attrs = yblocks
|
|
1726
|
+
.get( 0 )
|
|
1727
|
+
.get( 'attributes' ) as YBlockAttributes;
|
|
1728
|
+
const body = attrs.get( 'body' ) as Y.Array< unknown >;
|
|
1729
|
+
const row = body.get( 0 ) as Y.Map< unknown >;
|
|
1730
|
+
const cells = row.get( 'cells' ) as Y.Array< unknown >;
|
|
1731
|
+
const cell = cells.get( 0 ) as Y.Map< unknown >;
|
|
1732
|
+
|
|
1733
|
+
// Scope should exist initially.
|
|
1734
|
+
expect( cell.get( 'scope' ) ).toBe( 'col' );
|
|
1735
|
+
|
|
1736
|
+
// Update without the scope property.
|
|
1737
|
+
const updatedBlocks: Block[] = [
|
|
1738
|
+
{
|
|
1739
|
+
name: 'core/table',
|
|
1740
|
+
attributes: {
|
|
1741
|
+
body: [
|
|
1742
|
+
{
|
|
1743
|
+
cells: [
|
|
1744
|
+
{
|
|
1745
|
+
content: 'Header',
|
|
1746
|
+
tag: 'th',
|
|
1747
|
+
},
|
|
1748
|
+
],
|
|
1749
|
+
},
|
|
1750
|
+
],
|
|
1751
|
+
},
|
|
1752
|
+
innerBlocks: [],
|
|
1753
|
+
},
|
|
1754
|
+
];
|
|
1755
|
+
|
|
1756
|
+
mergeCrdtBlocks( yblocks, updatedBlocks, null );
|
|
1757
|
+
|
|
1758
|
+
const cellAfter = (
|
|
1759
|
+
(
|
|
1760
|
+
( attrs.get( 'body' ) as Y.Array< unknown > ).get(
|
|
1761
|
+
0
|
|
1762
|
+
) as Y.Map< unknown >
|
|
1763
|
+
).get( 'cells' ) as Y.Array< unknown >
|
|
1764
|
+
).get( 0 ) as Y.Map< unknown >;
|
|
1765
|
+
|
|
1766
|
+
// Scope should be deleted.
|
|
1767
|
+
expect( cellAfter.get( 'scope' ) ).toBeUndefined();
|
|
1768
|
+
// Other properties should remain.
|
|
1769
|
+
expect( cellAfter.get( 'tag' ) ).toBe( 'th' );
|
|
1770
|
+
expect( ( cellAfter.get( 'content' ) as Y.Text ).toString() ).toBe(
|
|
1771
|
+
'Header'
|
|
1772
|
+
);
|
|
1773
|
+
} );
|
|
1774
|
+
|
|
1775
|
+
it( 'rebuilds Y.Array when element is wrong type (partial migration)', () => {
|
|
1776
|
+
// Manually set up a block with a Y.Array whose elements are
|
|
1777
|
+
// plain values instead of Y.Maps (simulating a partial migration).
|
|
1778
|
+
const block = new Y.Map() as unknown as YBlock;
|
|
1779
|
+
block.set( 'name' as any, 'core/table' );
|
|
1780
|
+
block.set( 'clientId' as any, 'table-partial' );
|
|
1781
|
+
block.set( 'innerBlocks' as any, new Y.Array() );
|
|
1782
|
+
|
|
1783
|
+
const attrs = new Y.Map();
|
|
1784
|
+
// Create a Y.Array with a plain object element (not a Y.Map).
|
|
1785
|
+
const bodyArray = new Y.Array();
|
|
1786
|
+
bodyArray.insert( 0, [
|
|
1787
|
+
{ cells: [ { content: 'plain', tag: 'td' } ] },
|
|
1788
|
+
] );
|
|
1789
|
+
attrs.set( 'body', bodyArray );
|
|
1790
|
+
block.set( 'attributes' as any, attrs );
|
|
1791
|
+
|
|
1792
|
+
doc.transact( () => {
|
|
1793
|
+
yblocks.push( [ block ] );
|
|
1794
|
+
} );
|
|
1795
|
+
|
|
1796
|
+
// The element should be a plain object, not a Y.Map.
|
|
1797
|
+
expect( bodyArray.get( 0 ) ).not.toBeInstanceOf( Y.Map );
|
|
1798
|
+
|
|
1799
|
+
// Merge, which should detect the wrong type and rebuild.
|
|
1800
|
+
const updatedBlocks: Block[] = [
|
|
1801
|
+
{
|
|
1802
|
+
name: 'core/table',
|
|
1803
|
+
attributes: {
|
|
1804
|
+
body: [
|
|
1805
|
+
{
|
|
1806
|
+
cells: [ { content: 'rebuilt', tag: 'td' } ],
|
|
1807
|
+
},
|
|
1808
|
+
],
|
|
1809
|
+
},
|
|
1810
|
+
innerBlocks: [],
|
|
1811
|
+
},
|
|
1812
|
+
];
|
|
1813
|
+
|
|
1814
|
+
mergeCrdtBlocks( yblocks, updatedBlocks, null );
|
|
1815
|
+
|
|
1816
|
+
const bodyAfter = attrs.get( 'body' ) as Y.Array< unknown >;
|
|
1817
|
+
expect( bodyAfter ).toBeInstanceOf( Y.Array );
|
|
1818
|
+
|
|
1819
|
+
// After rebuild, elements should be proper Y.Maps.
|
|
1820
|
+
const row = bodyAfter.get( 0 );
|
|
1821
|
+
expect( row ).toBeInstanceOf( Y.Map );
|
|
1822
|
+
|
|
1823
|
+
const cells = ( row as Y.Map< unknown > ).get(
|
|
1824
|
+
'cells'
|
|
1825
|
+
) as Y.Array< unknown >;
|
|
1826
|
+
const cell = cells.get( 0 ) as Y.Map< unknown >;
|
|
1827
|
+
expect( cell ).toBeInstanceOf( Y.Map );
|
|
1828
|
+
expect( ( cell.get( 'content' ) as Y.Text ).toString() ).toBe(
|
|
1829
|
+
'rebuilt'
|
|
1830
|
+
);
|
|
1831
|
+
} );
|
|
1832
|
+
} );
|
|
1833
|
+
|
|
1834
|
+
describe( 'object+query attributes', () => {
|
|
1835
|
+
it( 'creates Y.Map for object+query attributes with Y.Text sub-values', () => {
|
|
1836
|
+
const blocks: Block[] = [
|
|
1837
|
+
{
|
|
1838
|
+
name: 'core/test-object-query',
|
|
1839
|
+
attributes: {
|
|
1840
|
+
metadata: {
|
|
1841
|
+
title: 'Hello',
|
|
1842
|
+
value: 'world',
|
|
1843
|
+
},
|
|
1844
|
+
},
|
|
1845
|
+
innerBlocks: [],
|
|
1846
|
+
},
|
|
1847
|
+
];
|
|
1848
|
+
|
|
1849
|
+
mergeCrdtBlocks( yblocks, blocks, null );
|
|
1850
|
+
|
|
1851
|
+
const attrs = yblocks
|
|
1852
|
+
.get( 0 )
|
|
1853
|
+
.get( 'attributes' ) as YBlockAttributes;
|
|
1854
|
+
const metadata = attrs.get( 'metadata' );
|
|
1855
|
+
|
|
1856
|
+
// Should be a Y.Map, not a plain object.
|
|
1857
|
+
expect( metadata ).toBeInstanceOf( Y.Map );
|
|
1858
|
+
|
|
1859
|
+
const metadataMap = metadata as Y.Map< unknown >;
|
|
1860
|
+
|
|
1861
|
+
// title is rich-text, so it should be Y.Text.
|
|
1862
|
+
expect( metadataMap.get( 'title' ) ).toBeInstanceOf( Y.Text );
|
|
1863
|
+
expect( ( metadataMap.get( 'title' ) as Y.Text ).toString() ).toBe(
|
|
1864
|
+
'Hello'
|
|
1865
|
+
);
|
|
1866
|
+
|
|
1867
|
+
// value is a plain string, so it should remain a string.
|
|
1868
|
+
expect( metadataMap.get( 'value' ) ).toBe( 'world' );
|
|
1869
|
+
} );
|
|
1870
|
+
|
|
1871
|
+
it( 'merges object+query attribute in-place preserving Y.Map identity', () => {
|
|
1872
|
+
const blocks: Block[] = [
|
|
1873
|
+
{
|
|
1874
|
+
name: 'core/test-object-query',
|
|
1875
|
+
attributes: {
|
|
1876
|
+
metadata: {
|
|
1877
|
+
title: 'Original',
|
|
1878
|
+
value: 'v1',
|
|
1879
|
+
},
|
|
1880
|
+
},
|
|
1881
|
+
innerBlocks: [],
|
|
1882
|
+
clientId: 'obj-query-1',
|
|
1883
|
+
},
|
|
1884
|
+
];
|
|
1885
|
+
|
|
1886
|
+
mergeCrdtBlocks( yblocks, blocks, null );
|
|
1887
|
+
|
|
1888
|
+
const attrs = yblocks
|
|
1889
|
+
.get( 0 )
|
|
1890
|
+
.get( 'attributes' ) as YBlockAttributes;
|
|
1891
|
+
const metadataBefore = attrs.get( 'metadata' ) as Y.Map< unknown >;
|
|
1892
|
+
const titleBefore = metadataBefore.get( 'title' ) as Y.Text;
|
|
1893
|
+
|
|
1894
|
+
// Update the metadata.
|
|
1895
|
+
const updatedBlocks: Block[] = [
|
|
1896
|
+
{
|
|
1897
|
+
name: 'core/test-object-query',
|
|
1898
|
+
attributes: {
|
|
1899
|
+
metadata: {
|
|
1900
|
+
title: 'Updated',
|
|
1901
|
+
value: 'v2',
|
|
1902
|
+
},
|
|
1903
|
+
},
|
|
1904
|
+
innerBlocks: [],
|
|
1905
|
+
clientId: 'obj-query-1',
|
|
1906
|
+
},
|
|
1907
|
+
];
|
|
1908
|
+
|
|
1909
|
+
mergeCrdtBlocks( yblocks, updatedBlocks, null );
|
|
1910
|
+
|
|
1911
|
+
const metadataAfter = attrs.get( 'metadata' ) as Y.Map< unknown >;
|
|
1912
|
+
|
|
1913
|
+
// The Y.Map should be the same object (in-place merge).
|
|
1914
|
+
expect( metadataAfter ).toBe( metadataBefore );
|
|
1915
|
+
|
|
1916
|
+
// The Y.Text for title should be the same object (merged in-place).
|
|
1917
|
+
const titleAfter = metadataAfter.get( 'title' ) as Y.Text;
|
|
1918
|
+
expect( titleAfter ).toBe( titleBefore );
|
|
1919
|
+
expect( titleAfter.toString() ).toBe( 'Updated' );
|
|
1920
|
+
|
|
1921
|
+
// Plain value should be updated.
|
|
1922
|
+
expect( metadataAfter.get( 'value' ) ).toBe( 'v2' );
|
|
1923
|
+
} );
|
|
1924
|
+
|
|
1925
|
+
it( 'deletes removed properties from object+query Y.Map', () => {
|
|
1926
|
+
const blocks: Block[] = [
|
|
1927
|
+
{
|
|
1928
|
+
name: 'core/test-object-query',
|
|
1929
|
+
attributes: {
|
|
1930
|
+
metadata: {
|
|
1931
|
+
title: 'Keep',
|
|
1932
|
+
value: 'remove-me',
|
|
1933
|
+
},
|
|
1934
|
+
},
|
|
1935
|
+
innerBlocks: [],
|
|
1936
|
+
clientId: 'obj-query-2',
|
|
1937
|
+
},
|
|
1938
|
+
];
|
|
1939
|
+
|
|
1940
|
+
mergeCrdtBlocks( yblocks, blocks, null );
|
|
1941
|
+
|
|
1942
|
+
const attrs = yblocks
|
|
1943
|
+
.get( 0 )
|
|
1944
|
+
.get( 'attributes' ) as YBlockAttributes;
|
|
1945
|
+
const metadata = attrs.get( 'metadata' ) as Y.Map< unknown >;
|
|
1946
|
+
expect( metadata.get( 'value' ) ).toBe( 'remove-me' );
|
|
1947
|
+
|
|
1948
|
+
// Update without the value property.
|
|
1949
|
+
const updatedBlocks: Block[] = [
|
|
1950
|
+
{
|
|
1951
|
+
name: 'core/test-object-query',
|
|
1952
|
+
attributes: {
|
|
1953
|
+
metadata: {
|
|
1954
|
+
title: 'Keep',
|
|
1955
|
+
},
|
|
1956
|
+
},
|
|
1957
|
+
innerBlocks: [],
|
|
1958
|
+
clientId: 'obj-query-2',
|
|
1959
|
+
},
|
|
1960
|
+
];
|
|
1961
|
+
|
|
1962
|
+
mergeCrdtBlocks( yblocks, updatedBlocks, null );
|
|
1963
|
+
|
|
1964
|
+
expect( metadata.get( 'value' ) ).toBeUndefined();
|
|
1965
|
+
expect( ( metadata.get( 'title' ) as Y.Text ).toString() ).toBe(
|
|
1966
|
+
'Keep'
|
|
1967
|
+
);
|
|
1968
|
+
} );
|
|
1969
|
+
|
|
1970
|
+
it( 'upgrades plain value to Y.Map when schema requires it', () => {
|
|
1971
|
+
// Manually set up a block with a plain object attribute
|
|
1972
|
+
// where the schema expects object+query (Y.Map).
|
|
1973
|
+
const block = new Y.Map() as unknown as YBlock;
|
|
1974
|
+
block.set( 'name' as any, 'core/test-object-query' );
|
|
1975
|
+
block.set( 'clientId' as any, 'obj-upgrade' );
|
|
1976
|
+
block.set( 'innerBlocks' as any, new Y.Array() );
|
|
1977
|
+
|
|
1978
|
+
const attrs = new Y.Map();
|
|
1979
|
+
// Store metadata as a plain object (pre-migration).
|
|
1980
|
+
attrs.set( 'metadata', { title: 'plain', value: 'old' } );
|
|
1981
|
+
block.set( 'attributes' as any, attrs );
|
|
1982
|
+
|
|
1983
|
+
doc.transact( () => {
|
|
1984
|
+
yblocks.push( [ block ] );
|
|
1985
|
+
} );
|
|
1986
|
+
|
|
1987
|
+
// metadata should be a plain object currently.
|
|
1988
|
+
expect( attrs.get( 'metadata' ) ).not.toBeInstanceOf( Y.Map );
|
|
1989
|
+
|
|
1990
|
+
// Merge, which should upgrade to Y.Map.
|
|
1991
|
+
const updatedBlocks: Block[] = [
|
|
1992
|
+
{
|
|
1993
|
+
name: 'core/test-object-query',
|
|
1994
|
+
attributes: {
|
|
1995
|
+
metadata: {
|
|
1996
|
+
title: 'upgraded',
|
|
1997
|
+
value: 'new',
|
|
1998
|
+
},
|
|
1999
|
+
},
|
|
2000
|
+
innerBlocks: [],
|
|
2001
|
+
},
|
|
2002
|
+
];
|
|
2003
|
+
|
|
2004
|
+
mergeCrdtBlocks( yblocks, updatedBlocks, null );
|
|
2005
|
+
|
|
2006
|
+
const metadataAfter = attrs.get( 'metadata' );
|
|
2007
|
+
expect( metadataAfter ).toBeInstanceOf( Y.Map );
|
|
2008
|
+
|
|
2009
|
+
const metadataMap = metadataAfter as Y.Map< unknown >;
|
|
2010
|
+
expect( metadataMap.get( 'title' ) ).toBeInstanceOf( Y.Text );
|
|
2011
|
+
expect( ( metadataMap.get( 'title' ) as Y.Text ).toString() ).toBe(
|
|
2012
|
+
'upgraded'
|
|
2013
|
+
);
|
|
2014
|
+
expect( metadataMap.get( 'value' ) ).toBe( 'new' );
|
|
2015
|
+
} );
|
|
1236
2016
|
} );
|
|
1237
2017
|
|
|
1238
2018
|
describe( 'emoji handling', () => {
|
|
@@ -1581,3 +2361,55 @@ describe( 'crdt-blocks', () => {
|
|
|
1581
2361
|
} );
|
|
1582
2362
|
} );
|
|
1583
2363
|
} );
|
|
2364
|
+
|
|
2365
|
+
describe( 'getCachedRichTextData', () => {
|
|
2366
|
+
let spy: ReturnType< typeof jest.spyOn >;
|
|
2367
|
+
|
|
2368
|
+
beforeEach( () => {
|
|
2369
|
+
spy = jest.spyOn( RichTextData, 'fromHTMLString' );
|
|
2370
|
+
} );
|
|
2371
|
+
|
|
2372
|
+
afterEach( () => {
|
|
2373
|
+
spy.mockRestore();
|
|
2374
|
+
} );
|
|
2375
|
+
|
|
2376
|
+
it( 'does not call fromHTMLString again for the same HTML string', () => {
|
|
2377
|
+
getCachedRichTextData( '<strong>cached-hit</strong>' );
|
|
2378
|
+
getCachedRichTextData( '<strong>cached-hit</strong>' );
|
|
2379
|
+
|
|
2380
|
+
expect( spy ).toHaveBeenCalledTimes( 1 );
|
|
2381
|
+
} );
|
|
2382
|
+
|
|
2383
|
+
it( 'calls fromHTMLString for each unique HTML string', () => {
|
|
2384
|
+
getCachedRichTextData( '<strong>cached-miss-a</strong>' );
|
|
2385
|
+
getCachedRichTextData( '<em>cached-miss-b</em>' );
|
|
2386
|
+
|
|
2387
|
+
expect( spy ).toHaveBeenCalledTimes( 2 );
|
|
2388
|
+
} );
|
|
2389
|
+
|
|
2390
|
+
it( 'calls fromHTMLString again for an evicted entry', () => {
|
|
2391
|
+
const cacheSize = 10;
|
|
2392
|
+
const getCachedValue = createRichTextDataCache( cacheSize );
|
|
2393
|
+
|
|
2394
|
+
const firstString = 'eviction-test-first';
|
|
2395
|
+
|
|
2396
|
+
getCachedValue( firstString );
|
|
2397
|
+
|
|
2398
|
+
for ( let i = 1; i < cacheSize; i++ ) {
|
|
2399
|
+
getCachedValue( `eviction-test-${ i }` );
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
// This should push firstString out of the cache.
|
|
2403
|
+
getCachedValue( 'eviction-test-overflow' );
|
|
2404
|
+
|
|
2405
|
+
spy.mockClear();
|
|
2406
|
+
|
|
2407
|
+
// firstString was evicted, so fromHTMLString should be called again.
|
|
2408
|
+
getCachedValue( firstString );
|
|
2409
|
+
expect( spy ).toHaveBeenCalledTimes( 1 );
|
|
2410
|
+
|
|
2411
|
+
// The overflow entry is still cached, so fromHTMLString should not be called.
|
|
2412
|
+
getCachedValue( 'eviction-test-overflow' );
|
|
2413
|
+
expect( spy ).toHaveBeenCalledTimes( 1 );
|
|
2414
|
+
} );
|
|
2415
|
+
} );
|