@onekeyfe/react-native-native-list 6.7.0-alpha.397 → 6.7.0-alpha.399
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/android/src/main/java/com/onekey/nativelist/NativeListActivityRenderer.kt +23 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListIdentityRenderer.kt +43 -15
- package/android/src/main/java/com/onekey/nativelist/NativeListLeadingVisual.kt +88 -19
- package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +94 -1
- package/android/src/main/java/com/onekey/nativelist/NativeListPrimitives.kt +29 -0
- package/android/src/test/java/com/margelo/nitro/nativelist/NativeListActivityHostClipPolicyTest.kt +29 -0
- package/android/src/test/java/com/margelo/nitro/nativelist/NativeListIdentityTrailingAxisTest.kt +36 -0
- package/android/src/test/java/com/margelo/nitro/nativelist/NativeListLeadingSlotFrameTest.kt +68 -0
- package/docs/ROW_TEMPLATES.md +4 -0
- package/docs/SPEC.md +180 -37
- package/ios/NativeListDesignAssets.swift +6 -0
- package/ios/NativeListIdentityRenderer.swift +76 -4
- package/ios/NativeListLeadingVisual.swift +70 -8
- package/ios/RNCNativeListView.swift +6 -1
- package/ios/Resources/NativeListIcons.xcassets/onekey_document2.imageset/Contents.json +1 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_document2.imageset/icon.svg +1 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_dot_ver_solid.imageset/Contents.json +1 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_dot_ver_solid.imageset/icon.svg +1 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_plus_large.imageset/Contents.json +1 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_plus_large.imageset/icon.svg +1 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_ticket.imageset/Contents.json +1 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_ticket.imageset/icon.svg +1 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_wallet.imageset/Contents.json +1 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_wallet.imageset/icon.svg +1 -0
- package/ios/tests/NativeListIdentityTitleIconLayoutTests.swift +81 -0
- package/ios/tests/NativeListLeadingVisualPairBackdropTests.swift +50 -0
- package/ios/tests/NativeListLifecycleTests.swift +60 -0
- package/lib/module/NativeList.web.js +9 -12
- package/lib/module/models.js +2 -0
- package/lib/module/validation.js +627 -114
- package/lib/module/web/NativeListWebEngine.js +49 -9
- package/lib/module/web/templates/IdentityRowRenderer.js +58 -4
- package/lib/typescript/src/models.d.ts +25 -14
- package/lib/typescript/src/validation.d.ts +7 -2
- package/package.json +3 -3
- package/src/NativeList.web.tsx +10 -14
- package/src/models.ts +144 -153
- package/src/validation.ts +1617 -167
- package/src/web/NativeListWebEngine.ts +62 -10
- package/src/web/templates/IdentityRowRenderer.ts +70 -4
package/src/validation.ts
CHANGED
|
@@ -74,13 +74,15 @@ export function emptyNativeListSnapshot(): NativeListSnapshot {
|
|
|
74
74
|
function assertPlainSerializable(
|
|
75
75
|
value: unknown,
|
|
76
76
|
path: string,
|
|
77
|
-
seen: Set<object
|
|
77
|
+
seen: Set<object>,
|
|
78
|
+
requireFinite = true
|
|
78
79
|
): void {
|
|
79
80
|
if (value === null || value === undefined) return;
|
|
80
81
|
const valueType = typeof value;
|
|
81
82
|
if (valueType === 'string' || valueType === 'boolean') return;
|
|
82
83
|
if (valueType === 'number') {
|
|
83
|
-
if (!Number.isFinite(value))
|
|
84
|
+
if (requireFinite && !Number.isFinite(value))
|
|
85
|
+
fail(path, 'numbers must be finite');
|
|
84
86
|
return;
|
|
85
87
|
}
|
|
86
88
|
if (valueType !== 'object')
|
|
@@ -91,16 +93,21 @@ function assertPlainSerializable(
|
|
|
91
93
|
seen.add(objectValue);
|
|
92
94
|
if (Array.isArray(value)) {
|
|
93
95
|
value.forEach((item, index) =>
|
|
94
|
-
assertPlainSerializable(item, `${path}[${index}]`, seen)
|
|
96
|
+
assertPlainSerializable(item, `${path}[${index}]`, seen, requireFinite)
|
|
95
97
|
);
|
|
96
98
|
} else {
|
|
97
99
|
const prototype = Object.getPrototypeOf(value);
|
|
98
100
|
if (prototype !== Object.prototype && prototype !== null) {
|
|
99
101
|
fail(path, 'must contain plain objects only');
|
|
100
102
|
}
|
|
101
|
-
Object.entries(value as Record<string, unknown>).forEach(([key, item]) =>
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
Object.entries(value as Record<string, unknown>).forEach(([key, item]) => {
|
|
104
|
+
if (
|
|
105
|
+
key === '__proto__' ||
|
|
106
|
+
(key === 'constructor' && typeof item === 'object')
|
|
107
|
+
)
|
|
108
|
+
fail(path, 'unsafe object key');
|
|
109
|
+
assertPlainSerializable(item, `${path}.${key}`, seen, requireFinite);
|
|
110
|
+
});
|
|
104
111
|
}
|
|
105
112
|
seen.delete(objectValue);
|
|
106
113
|
}
|
|
@@ -1248,7 +1255,7 @@ function assertButtonGroup(
|
|
|
1248
1255
|
function assertIdentityExtras(
|
|
1249
1256
|
row: Pick<
|
|
1250
1257
|
IdentityRow,
|
|
1251
|
-
'titleIcon' | 'secondaryTitleIcon' | 'trailing' | 'style'
|
|
1258
|
+
'titleIcon' | 'secondaryTitleIcon' | 'titleImage' | 'trailing' | 'style'
|
|
1252
1259
|
>,
|
|
1253
1260
|
path: string
|
|
1254
1261
|
): void {
|
|
@@ -1279,6 +1286,30 @@ function assertIdentityExtras(
|
|
|
1279
1286
|
if (titleIcon.hoverActionKey !== undefined)
|
|
1280
1287
|
assertKey(titleIcon.hoverActionKey, `${iconPath}.hoverActionKey`);
|
|
1281
1288
|
}
|
|
1289
|
+
if (row.titleImage !== undefined) {
|
|
1290
|
+
const imagePath = `${path}.titleImage`;
|
|
1291
|
+
const descriptor = protocolObject(row.titleImage, imagePath);
|
|
1292
|
+
assertUnknownKeys(
|
|
1293
|
+
descriptor,
|
|
1294
|
+
['image', 'accessibilityLabel', 'hoverActionKey'],
|
|
1295
|
+
imagePath,
|
|
1296
|
+
'title image key'
|
|
1297
|
+
);
|
|
1298
|
+
protocolObject(row.titleImage.image, `${imagePath}.image`);
|
|
1299
|
+
if (typeof row.titleImage.image.uri !== 'string')
|
|
1300
|
+
fail(`${imagePath}.image.uri`, 'must be a string');
|
|
1301
|
+
assertImage(row.titleImage.image, `${imagePath}.image`);
|
|
1302
|
+
const { width, height } = row.titleImage.image;
|
|
1303
|
+
if (width !== height || width < 1 || width > 40)
|
|
1304
|
+
fail(`${imagePath}.image`, 'must be square within 1...40');
|
|
1305
|
+
if (
|
|
1306
|
+
row.titleImage.accessibilityLabel !== undefined &&
|
|
1307
|
+
typeof row.titleImage.accessibilityLabel !== 'string'
|
|
1308
|
+
)
|
|
1309
|
+
fail(`${imagePath}.accessibilityLabel`, 'must be a string');
|
|
1310
|
+
if (row.titleImage.hoverActionKey !== undefined)
|
|
1311
|
+
assertKey(row.titleImage.hoverActionKey, `${imagePath}.hoverActionKey`);
|
|
1312
|
+
}
|
|
1282
1313
|
const ratio = row.style?.leadingContentMaxWidthRatio;
|
|
1283
1314
|
if (
|
|
1284
1315
|
ratio !== undefined &&
|
|
@@ -1293,6 +1324,8 @@ function assertIdentityExtras(
|
|
|
1293
1324
|
|
|
1294
1325
|
function assertImage(image: ImageSource | undefined, path: string): void {
|
|
1295
1326
|
if (!image) return;
|
|
1327
|
+
if (!isAllowedImageUri(image.uri))
|
|
1328
|
+
fail(`${path}.uri`, 'unsupported image URI');
|
|
1296
1329
|
if (!image.uri.trim()) fail(`${path}.uri`, 'must not be empty');
|
|
1297
1330
|
if (image.fallbackUris !== undefined) {
|
|
1298
1331
|
if (!Array.isArray(image.fallbackUris))
|
|
@@ -1634,6 +1667,7 @@ function assertRow(
|
|
|
1634
1667
|
path = `rows[${index}]`
|
|
1635
1668
|
): void {
|
|
1636
1669
|
assertRowType(row.type, path);
|
|
1670
|
+
assertUnknownKeys(row, ROW_FIELDS[row.type], path, 'row key');
|
|
1637
1671
|
assertKey(row.key, `${path}.key`);
|
|
1638
1672
|
// OneKey patch: explicit dimensions and opacity cannot corrupt list layout.
|
|
1639
1673
|
if (row.height !== undefined && (row.height < 0 || row.height > 4096))
|
|
@@ -2250,6 +2284,338 @@ function assertSnapshotContract(
|
|
|
2250
2284
|
return snapshot;
|
|
2251
2285
|
}
|
|
2252
2286
|
|
|
2287
|
+
const COMMON_ROW_FIELDS = [
|
|
2288
|
+
'height',
|
|
2289
|
+
'heightRounding',
|
|
2290
|
+
'testID',
|
|
2291
|
+
'opacity',
|
|
2292
|
+
'backgroundColor',
|
|
2293
|
+
'backgroundFullWidth',
|
|
2294
|
+
'pressDisabled',
|
|
2295
|
+
'key',
|
|
2296
|
+
'revision',
|
|
2297
|
+
'sectionKey',
|
|
2298
|
+
'groupId',
|
|
2299
|
+
'groupPosition',
|
|
2300
|
+
'size',
|
|
2301
|
+
'density',
|
|
2302
|
+
'disabled',
|
|
2303
|
+
'selected',
|
|
2304
|
+
'accessibilityLabel',
|
|
2305
|
+
'separator',
|
|
2306
|
+
'type',
|
|
2307
|
+
'style',
|
|
2308
|
+
];
|
|
2309
|
+
|
|
2310
|
+
const ROW_FIELDS: Readonly<Record<string, readonly string[]>> = {
|
|
2311
|
+
identity: [
|
|
2312
|
+
...COMMON_ROW_FIELDS,
|
|
2313
|
+
'contentHeight',
|
|
2314
|
+
'longPressActionKey',
|
|
2315
|
+
'titleMatch',
|
|
2316
|
+
'titleActionKey',
|
|
2317
|
+
'titleActionOnHover',
|
|
2318
|
+
'subtitleSegments',
|
|
2319
|
+
'presentation',
|
|
2320
|
+
'leading',
|
|
2321
|
+
'leadingAction',
|
|
2322
|
+
'title',
|
|
2323
|
+
'titleSuffixes',
|
|
2324
|
+
'titleIcon',
|
|
2325
|
+
'secondaryTitleIcon',
|
|
2326
|
+
'titleImage',
|
|
2327
|
+
'subtitle',
|
|
2328
|
+
'tertiary',
|
|
2329
|
+
'tertiaryTone',
|
|
2330
|
+
'titleLines',
|
|
2331
|
+
'subtitleLines',
|
|
2332
|
+
'badges',
|
|
2333
|
+
'trailing',
|
|
2334
|
+
'draggable',
|
|
2335
|
+
],
|
|
2336
|
+
walletGroup: [...COMMON_ROW_FIELDS, 'parent', 'children', 'draggable'],
|
|
2337
|
+
rail: [
|
|
2338
|
+
...COMMON_ROW_FIELDS,
|
|
2339
|
+
'visual',
|
|
2340
|
+
'title',
|
|
2341
|
+
'status',
|
|
2342
|
+
'badge',
|
|
2343
|
+
'draggable',
|
|
2344
|
+
],
|
|
2345
|
+
activity: [
|
|
2346
|
+
...COMMON_ROW_FIELDS,
|
|
2347
|
+
'leading',
|
|
2348
|
+
'hideLeading',
|
|
2349
|
+
'secondaryLeading',
|
|
2350
|
+
'title',
|
|
2351
|
+
'description',
|
|
2352
|
+
'status',
|
|
2353
|
+
'primaryAmount',
|
|
2354
|
+
'secondaryAmount',
|
|
2355
|
+
'amounts',
|
|
2356
|
+
'badges',
|
|
2357
|
+
'presentation',
|
|
2358
|
+
'fee',
|
|
2359
|
+
'descriptionActionKey',
|
|
2360
|
+
'footerActions',
|
|
2361
|
+
],
|
|
2362
|
+
message: [
|
|
2363
|
+
...COMMON_ROW_FIELDS,
|
|
2364
|
+
'presentation',
|
|
2365
|
+
'unread',
|
|
2366
|
+
'leading',
|
|
2367
|
+
'title',
|
|
2368
|
+
'body',
|
|
2369
|
+
'bodyLines',
|
|
2370
|
+
'time',
|
|
2371
|
+
'thumbnail',
|
|
2372
|
+
],
|
|
2373
|
+
dataRow: [
|
|
2374
|
+
...COMMON_ROW_FIELDS,
|
|
2375
|
+
'leading',
|
|
2376
|
+
'columns',
|
|
2377
|
+
'trailing',
|
|
2378
|
+
'checkbox',
|
|
2379
|
+
'index',
|
|
2380
|
+
'badge',
|
|
2381
|
+
'badges',
|
|
2382
|
+
'favorite',
|
|
2383
|
+
'favoriteActive',
|
|
2384
|
+
],
|
|
2385
|
+
market: [
|
|
2386
|
+
...COMMON_ROW_FIELDS,
|
|
2387
|
+
'variant',
|
|
2388
|
+
'webTable',
|
|
2389
|
+
'leadingAction',
|
|
2390
|
+
'leading',
|
|
2391
|
+
'title',
|
|
2392
|
+
'subtitle',
|
|
2393
|
+
'subtitlePrefix',
|
|
2394
|
+
'subtitleSegments',
|
|
2395
|
+
'price',
|
|
2396
|
+
'priceSegments',
|
|
2397
|
+
'change',
|
|
2398
|
+
'badges',
|
|
2399
|
+
'pressActionKey',
|
|
2400
|
+
'pressInActionKey',
|
|
2401
|
+
'longPressActionKey',
|
|
2402
|
+
'diagnostics',
|
|
2403
|
+
],
|
|
2404
|
+
mediaTile: [
|
|
2405
|
+
...COMMON_ROW_FIELDS,
|
|
2406
|
+
'variant',
|
|
2407
|
+
'media',
|
|
2408
|
+
'image',
|
|
2409
|
+
'imageState',
|
|
2410
|
+
'networkImage',
|
|
2411
|
+
'title',
|
|
2412
|
+
'subtitle',
|
|
2413
|
+
'badge',
|
|
2414
|
+
'closeActionKey',
|
|
2415
|
+
],
|
|
2416
|
+
metricCard: [
|
|
2417
|
+
...COMMON_ROW_FIELDS,
|
|
2418
|
+
'variant',
|
|
2419
|
+
'title',
|
|
2420
|
+
'value',
|
|
2421
|
+
'subtitle',
|
|
2422
|
+
'trend',
|
|
2423
|
+
'trendTone',
|
|
2424
|
+
'visual',
|
|
2425
|
+
'badge',
|
|
2426
|
+
'metrics',
|
|
2427
|
+
'progress',
|
|
2428
|
+
],
|
|
2429
|
+
sectionHeader: [
|
|
2430
|
+
...COMMON_ROW_FIELDS,
|
|
2431
|
+
'sticky',
|
|
2432
|
+
'valueActionTestID',
|
|
2433
|
+
'valueSegments',
|
|
2434
|
+
'titleActionKey',
|
|
2435
|
+
'titleActionOnHover',
|
|
2436
|
+
'presentation',
|
|
2437
|
+
'indexTitle',
|
|
2438
|
+
'variant',
|
|
2439
|
+
'title',
|
|
2440
|
+
'leadingBadge',
|
|
2441
|
+
'titleLoading',
|
|
2442
|
+
'subtitle',
|
|
2443
|
+
'value',
|
|
2444
|
+
'valueHelp',
|
|
2445
|
+
'valueActionKey',
|
|
2446
|
+
'titleIcon',
|
|
2447
|
+
'valueIcon',
|
|
2448
|
+
'checkbox',
|
|
2449
|
+
],
|
|
2450
|
+
action: [
|
|
2451
|
+
...COMMON_ROW_FIELDS,
|
|
2452
|
+
'presentation',
|
|
2453
|
+
'title',
|
|
2454
|
+
'actionKey',
|
|
2455
|
+
'tone',
|
|
2456
|
+
'icon',
|
|
2457
|
+
'checkbox',
|
|
2458
|
+
'trailing',
|
|
2459
|
+
],
|
|
2460
|
+
buttonGroup: [...COMMON_ROW_FIELDS, 'buttons'],
|
|
2461
|
+
system: [
|
|
2462
|
+
...COMMON_ROW_FIELDS,
|
|
2463
|
+
'variant',
|
|
2464
|
+
'presentation',
|
|
2465
|
+
'loadingStyle',
|
|
2466
|
+
'message',
|
|
2467
|
+
'actionKey',
|
|
2468
|
+
'actionText',
|
|
2469
|
+
'title',
|
|
2470
|
+
'borderColor',
|
|
2471
|
+
],
|
|
2472
|
+
};
|
|
2473
|
+
|
|
2474
|
+
const COMMON_PATCH_FIELDS = [
|
|
2475
|
+
'height',
|
|
2476
|
+
'heightRounding',
|
|
2477
|
+
'opacity',
|
|
2478
|
+
'pressDisabled',
|
|
2479
|
+
'revision',
|
|
2480
|
+
'disabled',
|
|
2481
|
+
'selected',
|
|
2482
|
+
'accessibilityLabel',
|
|
2483
|
+
'separator',
|
|
2484
|
+
'style',
|
|
2485
|
+
];
|
|
2486
|
+
|
|
2487
|
+
const PATCH_FIELDS: Readonly<Record<string, readonly string[]>> = {
|
|
2488
|
+
identity: [
|
|
2489
|
+
...COMMON_PATCH_FIELDS,
|
|
2490
|
+
'contentHeight',
|
|
2491
|
+
'longPressActionKey',
|
|
2492
|
+
'titleMatch',
|
|
2493
|
+
'titleActionKey',
|
|
2494
|
+
'titleActionOnHover',
|
|
2495
|
+
'subtitleSegments',
|
|
2496
|
+
'leading',
|
|
2497
|
+
'leadingAction',
|
|
2498
|
+
'title',
|
|
2499
|
+
'titleSuffixes',
|
|
2500
|
+
'titleIcon',
|
|
2501
|
+
'secondaryTitleIcon',
|
|
2502
|
+
'titleImage',
|
|
2503
|
+
'subtitle',
|
|
2504
|
+
'tertiary',
|
|
2505
|
+
'tertiaryTone',
|
|
2506
|
+
'subtitleLines',
|
|
2507
|
+
'badges',
|
|
2508
|
+
'trailing',
|
|
2509
|
+
],
|
|
2510
|
+
rail: [...COMMON_PATCH_FIELDS, 'title', 'visual', 'status', 'badge'],
|
|
2511
|
+
activity: [
|
|
2512
|
+
...COMMON_PATCH_FIELDS,
|
|
2513
|
+
'presentation',
|
|
2514
|
+
'leading',
|
|
2515
|
+
'title',
|
|
2516
|
+
'badges',
|
|
2517
|
+
'status',
|
|
2518
|
+
'hideLeading',
|
|
2519
|
+
'secondaryLeading',
|
|
2520
|
+
'description',
|
|
2521
|
+
'primaryAmount',
|
|
2522
|
+
'secondaryAmount',
|
|
2523
|
+
'amounts',
|
|
2524
|
+
'fee',
|
|
2525
|
+
'descriptionActionKey',
|
|
2526
|
+
'footerActions',
|
|
2527
|
+
],
|
|
2528
|
+
message: [
|
|
2529
|
+
...COMMON_PATCH_FIELDS,
|
|
2530
|
+
'presentation',
|
|
2531
|
+
'title',
|
|
2532
|
+
'unread',
|
|
2533
|
+
'body',
|
|
2534
|
+
'bodyLines',
|
|
2535
|
+
'time',
|
|
2536
|
+
'thumbnail',
|
|
2537
|
+
],
|
|
2538
|
+
dataRow: [
|
|
2539
|
+
...COMMON_PATCH_FIELDS,
|
|
2540
|
+
'leading',
|
|
2541
|
+
'badges',
|
|
2542
|
+
'trailing',
|
|
2543
|
+
'badge',
|
|
2544
|
+
'columns',
|
|
2545
|
+
'checkbox',
|
|
2546
|
+
'index',
|
|
2547
|
+
'favorite',
|
|
2548
|
+
'favoriteActive',
|
|
2549
|
+
],
|
|
2550
|
+
market: [
|
|
2551
|
+
...COMMON_PATCH_FIELDS,
|
|
2552
|
+
'longPressActionKey',
|
|
2553
|
+
'subtitleSegments',
|
|
2554
|
+
'leading',
|
|
2555
|
+
'leadingAction',
|
|
2556
|
+
'title',
|
|
2557
|
+
'subtitle',
|
|
2558
|
+
'badges',
|
|
2559
|
+
'webTable',
|
|
2560
|
+
'price',
|
|
2561
|
+
'priceSegments',
|
|
2562
|
+
'change',
|
|
2563
|
+
'pressActionKey',
|
|
2564
|
+
'pressInActionKey',
|
|
2565
|
+
'diagnostics',
|
|
2566
|
+
],
|
|
2567
|
+
mediaTile: [
|
|
2568
|
+
...COMMON_PATCH_FIELDS,
|
|
2569
|
+
'title',
|
|
2570
|
+
'subtitle',
|
|
2571
|
+
'badge',
|
|
2572
|
+
'media',
|
|
2573
|
+
'image',
|
|
2574
|
+
'imageState',
|
|
2575
|
+
'networkImage',
|
|
2576
|
+
'closeActionKey',
|
|
2577
|
+
],
|
|
2578
|
+
metricCard: [
|
|
2579
|
+
...COMMON_PATCH_FIELDS,
|
|
2580
|
+
'title',
|
|
2581
|
+
'subtitle',
|
|
2582
|
+
'visual',
|
|
2583
|
+
'badge',
|
|
2584
|
+
'variant',
|
|
2585
|
+
'value',
|
|
2586
|
+
'trend',
|
|
2587
|
+
'trendTone',
|
|
2588
|
+
'metrics',
|
|
2589
|
+
'progress',
|
|
2590
|
+
],
|
|
2591
|
+
sectionHeader: [
|
|
2592
|
+
...COMMON_PATCH_FIELDS,
|
|
2593
|
+
'titleActionKey',
|
|
2594
|
+
'titleActionOnHover',
|
|
2595
|
+
'title',
|
|
2596
|
+
'titleIcon',
|
|
2597
|
+
'subtitle',
|
|
2598
|
+
'checkbox',
|
|
2599
|
+
'variant',
|
|
2600
|
+
'value',
|
|
2601
|
+
'leadingBadge',
|
|
2602
|
+
'titleLoading',
|
|
2603
|
+
'valueHelp',
|
|
2604
|
+
'valueActionKey',
|
|
2605
|
+
'valueIcon',
|
|
2606
|
+
],
|
|
2607
|
+
action: [
|
|
2608
|
+
...COMMON_PATCH_FIELDS,
|
|
2609
|
+
'title',
|
|
2610
|
+
'trailing',
|
|
2611
|
+
'checkbox',
|
|
2612
|
+
'tone',
|
|
2613
|
+
'icon',
|
|
2614
|
+
],
|
|
2615
|
+
buttonGroup: [...COMMON_PATCH_FIELDS, 'buttons'],
|
|
2616
|
+
system: ['revision', 'disabled', 'message'],
|
|
2617
|
+
};
|
|
2618
|
+
|
|
2253
2619
|
function protocolObject(value: unknown, path: string): Record<string, unknown> {
|
|
2254
2620
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
2255
2621
|
fail(path, 'must be an object');
|
|
@@ -2389,6 +2755,14 @@ function assertProtocolFields(
|
|
|
2389
2755
|
fail(`${path}.uri`, 'must be a string');
|
|
2390
2756
|
Object.entries(value).forEach(([field, item]) => {
|
|
2391
2757
|
if (item === undefined) return;
|
|
2758
|
+
if (field === 'headers') {
|
|
2759
|
+
const headers = protocolObject(item, `${path}.headers`);
|
|
2760
|
+
for (const [name, headerValue] of Object.entries(headers)) {
|
|
2761
|
+
if (!name.trim() || typeof headerValue !== 'string')
|
|
2762
|
+
fail(`${path}.headers`, 'must contain string headers');
|
|
2763
|
+
}
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
2392
2766
|
const fieldPath = `${path}.${field}`;
|
|
2393
2767
|
if (
|
|
2394
2768
|
!path.endsWith('.style') &&
|
|
@@ -2418,6 +2792,7 @@ function assertProtocolFields(
|
|
|
2418
2792
|
if (
|
|
2419
2793
|
field === 'key' ||
|
|
2420
2794
|
field === 'sectionKey' ||
|
|
2795
|
+
field === 'groupId' ||
|
|
2421
2796
|
field === 'actionKey' ||
|
|
2422
2797
|
field.endsWith('ActionKey')
|
|
2423
2798
|
)
|
|
@@ -2466,6 +2841,7 @@ function assertRowProtocol(row: RowModel, path: string): void {
|
|
|
2466
2841
|
protocolObject(row, path);
|
|
2467
2842
|
assertRowType(row.type, path);
|
|
2468
2843
|
protocolKey(row.key, `${path}.key`);
|
|
2844
|
+
assertCoreRow(row, path);
|
|
2469
2845
|
assertProtocolFields(row, path);
|
|
2470
2846
|
for (const field of ['leading', 'visual', 'secondaryLeading'] as const) {
|
|
2471
2847
|
const visual = (row as unknown as Record<string, unknown>)[field];
|
|
@@ -2495,7 +2871,7 @@ function assertRowProtocol(row: RowModel, path: string): void {
|
|
|
2495
2871
|
if (row.height !== undefined && row.height < 0)
|
|
2496
2872
|
fail(`${path}.height`, 'must be non-negative');
|
|
2497
2873
|
if (row.type === 'identity') {
|
|
2498
|
-
assertIdentityExtras(row, path);
|
|
2874
|
+
if (isDevelopment()) assertIdentityExtras(row, path);
|
|
2499
2875
|
if (row.titleSuffixes !== undefined && !Array.isArray(row.titleSuffixes))
|
|
2500
2876
|
fail(`${path}.titleSuffixes`, 'must be an array');
|
|
2501
2877
|
row.titleSuffixes?.forEach((suffix, index) => {
|
|
@@ -2529,8 +2905,9 @@ function assertRowProtocol(row: RowModel, path: string): void {
|
|
|
2529
2905
|
end = match.end;
|
|
2530
2906
|
});
|
|
2531
2907
|
}
|
|
2532
|
-
if (row.type === 'buttonGroup'
|
|
2533
|
-
|
|
2908
|
+
if (row.type === 'buttonGroup' && isDevelopment())
|
|
2909
|
+
assertButtonGroup(row, path);
|
|
2910
|
+
if (row.type === 'sectionHeader' && isDevelopment())
|
|
2534
2911
|
assertTextHelp(row.valueHelp, `${path}.valueHelp`);
|
|
2535
2912
|
if (row.type === 'mediaTile' && row.media) {
|
|
2536
2913
|
protocolObject(row.media.source, `${path}.media.source`);
|
|
@@ -2558,9 +2935,10 @@ function assertRowProtocol(row: RowModel, path: string): void {
|
|
|
2558
2935
|
row.columns.forEach((column, index) => {
|
|
2559
2936
|
const columnPath = `${path}.columns[${index}]`;
|
|
2560
2937
|
protocolObject(column, columnPath);
|
|
2561
|
-
assertIdentityExtras(column, columnPath);
|
|
2938
|
+
if (isDevelopment()) assertIdentityExtras(column, columnPath);
|
|
2562
2939
|
});
|
|
2563
|
-
|
|
2940
|
+
if (isDevelopment())
|
|
2941
|
+
assertTrailingAccessories(row.trailing, `${path}.trailing`);
|
|
2564
2942
|
}
|
|
2565
2943
|
if (row.type === 'market') {
|
|
2566
2944
|
protocolObject(row.change, `${path}.change`);
|
|
@@ -2696,19 +3074,780 @@ function assertSnapshotProtocol(snapshot: NativeListSnapshot): void {
|
|
|
2696
3074
|
}
|
|
2697
3075
|
}
|
|
2698
3076
|
|
|
2699
|
-
|
|
3077
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
3078
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
function isAllowedImageUri(value: unknown): value is string {
|
|
3082
|
+
return (
|
|
3083
|
+
typeof value === 'string' &&
|
|
3084
|
+
value.trim().length > 0 &&
|
|
3085
|
+
(!/^[a-z][a-z\d+.-]*:/i.test(value) ||
|
|
3086
|
+
/^(https?|file|content|ph|assets-library|asset|res|blob):/i.test(value) ||
|
|
3087
|
+
/^data:image\//i.test(value))
|
|
3088
|
+
);
|
|
3089
|
+
}
|
|
3090
|
+
|
|
3091
|
+
const IMAGE_FIELDS = [
|
|
3092
|
+
'uri',
|
|
3093
|
+
'width',
|
|
3094
|
+
'height',
|
|
3095
|
+
'headers',
|
|
3096
|
+
'contentFit',
|
|
3097
|
+
'cachePolicy',
|
|
3098
|
+
'autoplay',
|
|
3099
|
+
'optimizeTos',
|
|
3100
|
+
'overscan',
|
|
3101
|
+
'loadingStrategy',
|
|
3102
|
+
'fallbackUri',
|
|
3103
|
+
'fallbackUris',
|
|
3104
|
+
'retryTimes',
|
|
3105
|
+
];
|
|
3106
|
+
const ICON_FIELDS = [
|
|
3107
|
+
'name',
|
|
3108
|
+
'tintColor',
|
|
3109
|
+
'backgroundColor',
|
|
3110
|
+
'size',
|
|
3111
|
+
'accessibilityLabel',
|
|
3112
|
+
'hoverActionKey',
|
|
3113
|
+
];
|
|
3114
|
+
const VISUAL_FIELDS = [
|
|
3115
|
+
'kind',
|
|
3116
|
+
'name',
|
|
3117
|
+
'image',
|
|
3118
|
+
'images',
|
|
3119
|
+
'networkImage',
|
|
3120
|
+
'shape',
|
|
3121
|
+
'tintColor',
|
|
3122
|
+
'backgroundColor',
|
|
3123
|
+
'fallbackIcon',
|
|
3124
|
+
'fallbackText',
|
|
3125
|
+
'fallbackTextColor',
|
|
3126
|
+
'overlays',
|
|
3127
|
+
'borderStyle',
|
|
3128
|
+
'borderColor',
|
|
3129
|
+
'cornerIcon',
|
|
3130
|
+
];
|
|
3131
|
+
const TEXT_STYLE_FIELDS = [
|
|
3132
|
+
'token',
|
|
3133
|
+
'fontSize',
|
|
3134
|
+
'fontWeight',
|
|
3135
|
+
'color',
|
|
3136
|
+
'lineHeight',
|
|
3137
|
+
'lines',
|
|
3138
|
+
'truncate',
|
|
3139
|
+
'alignment',
|
|
3140
|
+
'verticalAlignment',
|
|
3141
|
+
'offsetY',
|
|
3142
|
+
];
|
|
3143
|
+
const ACCESSORY_FIELDS: Readonly<Record<string, readonly string[]>> = {
|
|
3144
|
+
value: ['kind', 'text', 'secondary', 'textSegments'],
|
|
3145
|
+
valuePair: [
|
|
3146
|
+
'kind',
|
|
3147
|
+
'primary',
|
|
3148
|
+
'secondary',
|
|
3149
|
+
'primaryFontWeight',
|
|
3150
|
+
'primaryHelp',
|
|
3151
|
+
'primaryTextSegments',
|
|
3152
|
+
'secondaryTextSegments',
|
|
3153
|
+
'primaryTone',
|
|
3154
|
+
'secondaryTone',
|
|
3155
|
+
],
|
|
3156
|
+
button: [
|
|
3157
|
+
'kind',
|
|
3158
|
+
'label',
|
|
3159
|
+
'actionKey',
|
|
3160
|
+
'disabled',
|
|
3161
|
+
'testID',
|
|
3162
|
+
'accessibilityLabel',
|
|
3163
|
+
'textColor',
|
|
3164
|
+
'backgroundColor',
|
|
3165
|
+
'pressedBackgroundColor',
|
|
3166
|
+
'hoverBackgroundColor',
|
|
3167
|
+
'focusRingColor',
|
|
3168
|
+
],
|
|
3169
|
+
icon: [
|
|
3170
|
+
'kind',
|
|
3171
|
+
'name',
|
|
3172
|
+
'tintColor',
|
|
3173
|
+
'buttonStyle',
|
|
3174
|
+
'disabled',
|
|
3175
|
+
'actionKey',
|
|
3176
|
+
'testID',
|
|
3177
|
+
'hoverActionKey',
|
|
3178
|
+
'accessibilityLabel',
|
|
3179
|
+
],
|
|
3180
|
+
checkbox: ['kind', 'state', 'disabled', 'loading', 'target', 'actionKey'],
|
|
3181
|
+
radio: ['kind', 'checked', 'disabled', 'actionKey'],
|
|
3182
|
+
switch: ['kind', 'value', 'disabled', 'actionKey'],
|
|
3183
|
+
chevron: ['kind', 'actionKey'],
|
|
3184
|
+
menu: ['kind', 'actionKey'],
|
|
3185
|
+
drag: ['kind', 'actionKey'],
|
|
3186
|
+
spinner: ['kind'],
|
|
3187
|
+
progress: ['kind', 'value'],
|
|
3188
|
+
};
|
|
3189
|
+
|
|
3190
|
+
const REQUIRED_ROW_FIELDS: Readonly<Record<string, readonly string[]>> = {
|
|
3191
|
+
identity: ['title', 'leading'],
|
|
3192
|
+
walletGroup: ['parent', 'children'],
|
|
3193
|
+
rail: ['title', 'visual'],
|
|
3194
|
+
activity: ['title', 'leading'],
|
|
3195
|
+
message: ['title', 'body', 'time'],
|
|
3196
|
+
dataRow: ['columns'],
|
|
3197
|
+
market: ['title', 'leading', 'variant', 'price', 'change'],
|
|
3198
|
+
mediaTile: ['title', 'variant'],
|
|
3199
|
+
metricCard: ['title', 'value'],
|
|
3200
|
+
sectionHeader: ['title', 'sectionKey'],
|
|
3201
|
+
action: ['title', 'actionKey'],
|
|
3202
|
+
buttonGroup: ['buttons'],
|
|
3203
|
+
system: ['variant'],
|
|
3204
|
+
};
|
|
3205
|
+
|
|
3206
|
+
function assertCoreRow(row: RowModel, path: string): void {
|
|
3207
|
+
const object = row as unknown as Record<string, unknown>;
|
|
3208
|
+
for (const key of REQUIRED_ROW_FIELDS[row.type]) {
|
|
3209
|
+
const value = object[key];
|
|
3210
|
+
if (value === undefined || value === null)
|
|
3211
|
+
fail(`${path}.${key}`, 'required field');
|
|
3212
|
+
if (
|
|
3213
|
+
['title', 'body', 'time', 'price', 'value', 'variant'].includes(key) &&
|
|
3214
|
+
typeof value !== 'string'
|
|
3215
|
+
)
|
|
3216
|
+
fail(`${path}.${key}`, 'must be a string');
|
|
3217
|
+
}
|
|
3218
|
+
if (row.type === 'market') {
|
|
3219
|
+
if (!row.price.trim()) fail(`${path}.price`, 'must be nonblank');
|
|
3220
|
+
if (typeof row.change?.text !== 'string')
|
|
3221
|
+
fail(`${path}.change.text`, 'must be a string');
|
|
3222
|
+
}
|
|
3223
|
+
if (row.type === 'activity') {
|
|
3224
|
+
row.amounts?.forEach((amount, index) => {
|
|
3225
|
+
protocolKey(amount.key, `${path}.amounts[${index}].key`);
|
|
3226
|
+
if (typeof amount.text !== 'string')
|
|
3227
|
+
fail(`${path}.amounts[${index}].text`, 'required financial text');
|
|
3228
|
+
});
|
|
3229
|
+
if (row.fee && typeof row.fee.primary !== 'string')
|
|
3230
|
+
fail(`${path}.fee.primary`, 'required financial text');
|
|
3231
|
+
row.footerActions?.forEach((action, index) => {
|
|
3232
|
+
protocolKey(action.key, `${path}.footerActions[${index}].key`);
|
|
3233
|
+
protocolKey(action.label, `${path}.footerActions[${index}].label`);
|
|
3234
|
+
});
|
|
3235
|
+
}
|
|
3236
|
+
if (row.type === 'action') protocolKey(row.actionKey, `${path}.actionKey`);
|
|
3237
|
+
if (row.type === 'sectionHeader')
|
|
3238
|
+
protocolKey(row.sectionKey, `${path}.sectionKey`);
|
|
3239
|
+
if (
|
|
3240
|
+
row.type === 'system' &&
|
|
3241
|
+
['retry', 'warning', 'noMatch'].includes(row.variant)
|
|
3242
|
+
) {
|
|
3243
|
+
if (typeof object.message !== 'string')
|
|
3244
|
+
fail(`${path}.message`, 'must be a string');
|
|
3245
|
+
if (row.variant === 'retry')
|
|
3246
|
+
protocolKey(row.actionKey, `${path}.actionKey`);
|
|
3247
|
+
}
|
|
3248
|
+
if (row.type === 'dataRow') {
|
|
3249
|
+
if (!Array.isArray(row.columns) || row.columns.length === 0)
|
|
3250
|
+
fail(`${path}.columns`, 'required columns');
|
|
3251
|
+
row.columns.forEach((column, index) => {
|
|
3252
|
+
protocolObject(column, `${path}.columns[${index}]`);
|
|
3253
|
+
protocolKey(column.key, `${path}.columns[${index}].key`);
|
|
3254
|
+
if (typeof column.text !== 'string')
|
|
3255
|
+
fail(`${path}.columns[${index}].text`, 'must be a string');
|
|
3256
|
+
});
|
|
3257
|
+
}
|
|
3258
|
+
if (row.type === 'buttonGroup') {
|
|
3259
|
+
if (!Array.isArray(row.buttons) || row.buttons.length === 0)
|
|
3260
|
+
fail(`${path}.buttons`, 'required buttons');
|
|
3261
|
+
const keys = new Set<string>();
|
|
3262
|
+
row.buttons.forEach((button, index) => {
|
|
3263
|
+
const field = `${path}.buttons[${index}]`;
|
|
3264
|
+
protocolObject(button, field);
|
|
3265
|
+
protocolKey(button.key, `${field}.key`);
|
|
3266
|
+
protocolKey(button.actionKey, `${field}.actionKey`);
|
|
3267
|
+
protocolKey(button.label, `${field}.label`);
|
|
3268
|
+
if (keys.has(button.key)) fail(field, 'duplicate button identity');
|
|
3269
|
+
keys.add(button.key);
|
|
3270
|
+
});
|
|
3271
|
+
}
|
|
3272
|
+
for (const field of [
|
|
3273
|
+
'trailing',
|
|
3274
|
+
'leadingAction',
|
|
3275
|
+
'titleIcon',
|
|
3276
|
+
'valueIcon',
|
|
3277
|
+
] as const) {
|
|
3278
|
+
const value = object[field];
|
|
3279
|
+
if (value === undefined) continue;
|
|
3280
|
+
// Identity title icons are decorative descriptors, not trailing controls.
|
|
3281
|
+
if (field === 'titleIcon' && row.type !== 'sectionHeader') continue;
|
|
3282
|
+
const accessories = field === 'trailing' ? value : [value];
|
|
3283
|
+
if (!Array.isArray(accessories))
|
|
3284
|
+
fail(`${path}.${field}`, 'must be an array');
|
|
3285
|
+
accessories.forEach((accessory, index) => {
|
|
3286
|
+
const entry = protocolObject(accessory, `${path}.${field}[${index}]`);
|
|
3287
|
+
for (const key of entry.kind === 'value'
|
|
3288
|
+
? ['text']
|
|
3289
|
+
: entry.kind === 'valuePair'
|
|
3290
|
+
? ['primary', 'secondary']
|
|
3291
|
+
: []) {
|
|
3292
|
+
if (typeof entry[key] !== 'string')
|
|
3293
|
+
fail(`${path}.${field}[${index}].${key}`, 'required financial text');
|
|
3294
|
+
}
|
|
3295
|
+
if (
|
|
3296
|
+
['button', 'menu', 'switch'].includes(entry.kind as string) ||
|
|
3297
|
+
'actionKey' in entry
|
|
3298
|
+
) {
|
|
3299
|
+
protocolKey(entry.actionKey, `${path}.${field}[${index}].actionKey`);
|
|
3300
|
+
if (entry.kind === 'button')
|
|
3301
|
+
protocolKey(entry.label, `${path}.${field}[${index}].label`);
|
|
3302
|
+
}
|
|
3303
|
+
if (entry.kind === 'icon')
|
|
3304
|
+
protocolKey(entry.name, `${path}.${field}[${index}].name`);
|
|
3305
|
+
});
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
function pickFields(
|
|
3310
|
+
object: Record<string, unknown>,
|
|
3311
|
+
fields: readonly string[]
|
|
3312
|
+
): Record<string, unknown> {
|
|
3313
|
+
return Object.fromEntries(
|
|
3314
|
+
Object.entries(object).filter(([key]) => fields.includes(key))
|
|
3315
|
+
);
|
|
3316
|
+
}
|
|
3317
|
+
|
|
3318
|
+
function sanitizeImage(value: unknown): Record<string, unknown> | undefined {
|
|
3319
|
+
if (
|
|
3320
|
+
!isRecord(value) ||
|
|
3321
|
+
!isAllowedImageUri(value.uri) ||
|
|
3322
|
+
typeof value.width !== 'number' ||
|
|
3323
|
+
!Number.isFinite(value.width) ||
|
|
3324
|
+
value.width <= 0 ||
|
|
3325
|
+
typeof value.height !== 'number' ||
|
|
3326
|
+
!Number.isFinite(value.height) ||
|
|
3327
|
+
value.height <= 0
|
|
3328
|
+
)
|
|
3329
|
+
return undefined;
|
|
3330
|
+
const next = pickFields(value, IMAGE_FIELDS);
|
|
3331
|
+
next.width = Math.min(MAX_IMAGE_EDGE, value.width);
|
|
3332
|
+
next.height = Math.min(MAX_IMAGE_EDGE, value.height);
|
|
3333
|
+
if (next.headers !== undefined) {
|
|
3334
|
+
if (!isRecord(next.headers)) delete next.headers;
|
|
3335
|
+
else
|
|
3336
|
+
next.headers = Object.fromEntries(
|
|
3337
|
+
Object.entries(next.headers)
|
|
3338
|
+
.filter(
|
|
3339
|
+
([key, item]) =>
|
|
3340
|
+
key.trim() &&
|
|
3341
|
+
key.length <= MAX_HEADER_LENGTH &&
|
|
3342
|
+
typeof item === 'string' &&
|
|
3343
|
+
item.length <= MAX_HEADER_LENGTH
|
|
3344
|
+
)
|
|
3345
|
+
.slice(0, MAX_IMAGE_HEADERS)
|
|
3346
|
+
);
|
|
3347
|
+
}
|
|
3348
|
+
if (next.fallbackUris !== undefined) {
|
|
3349
|
+
if (Array.isArray(next.fallbackUris))
|
|
3350
|
+
next.fallbackUris = next.fallbackUris.filter(isAllowedImageUri);
|
|
3351
|
+
else delete next.fallbackUris;
|
|
3352
|
+
}
|
|
3353
|
+
if (next.fallbackUri !== undefined && !isAllowedImageUri(next.fallbackUri))
|
|
3354
|
+
delete next.fallbackUri;
|
|
3355
|
+
if (
|
|
3356
|
+
next.overscan !== undefined &&
|
|
3357
|
+
(typeof next.overscan !== 'number' ||
|
|
3358
|
+
!Number.isFinite(next.overscan) ||
|
|
3359
|
+
next.overscan < 1 ||
|
|
3360
|
+
next.overscan > 4)
|
|
3361
|
+
)
|
|
3362
|
+
delete next.overscan;
|
|
3363
|
+
if (
|
|
3364
|
+
next.retryTimes !== undefined &&
|
|
3365
|
+
(typeof next.retryTimes !== 'number' ||
|
|
3366
|
+
!Number.isFinite(next.retryTimes) ||
|
|
3367
|
+
next.retryTimes < 0)
|
|
3368
|
+
)
|
|
3369
|
+
delete next.retryTimes;
|
|
3370
|
+
const options: Readonly<Record<string, readonly string[]>> = {
|
|
3371
|
+
contentFit: ['cover', 'contain', 'fill', 'center'],
|
|
3372
|
+
cachePolicy: ['memory-disk', 'memory', 'disk', 'none'],
|
|
3373
|
+
loadingStrategy: ['static', 'skeleton', 'none'],
|
|
3374
|
+
};
|
|
3375
|
+
for (const [key, allowed] of Object.entries(options))
|
|
3376
|
+
if (next[key] !== undefined && !allowed.includes(next[key] as string))
|
|
3377
|
+
delete next[key];
|
|
3378
|
+
for (const key of ['autoplay', 'optimizeTos'])
|
|
3379
|
+
if (next[key] !== undefined && typeof next[key] !== 'boolean')
|
|
3380
|
+
delete next[key];
|
|
3381
|
+
return next;
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3384
|
+
const NESTED_DECORATION_FIELDS: Readonly<Record<string, readonly string[]>> = {
|
|
3385
|
+
webTable: ['columns', 'cells', 'testID', 'draggable', 'contextMenuActionKey'],
|
|
3386
|
+
columns: [
|
|
3387
|
+
'key',
|
|
3388
|
+
'text',
|
|
3389
|
+
'textSegments',
|
|
3390
|
+
'titleIcon',
|
|
3391
|
+
'secondaryTitleIcon',
|
|
3392
|
+
'secondaryLeadingText',
|
|
3393
|
+
'secondaryText',
|
|
3394
|
+
'secondaryTextSegments',
|
|
3395
|
+
'weight',
|
|
3396
|
+
'alignment',
|
|
3397
|
+
'tone',
|
|
3398
|
+
'secondaryTone',
|
|
3399
|
+
'width',
|
|
3400
|
+
'minWidth',
|
|
3401
|
+
'flex',
|
|
3402
|
+
'paddingStart',
|
|
3403
|
+
'paddingEnd',
|
|
3404
|
+
],
|
|
3405
|
+
cells: [
|
|
3406
|
+
'key',
|
|
3407
|
+
'kind',
|
|
3408
|
+
'active',
|
|
3409
|
+
'disabled',
|
|
3410
|
+
'testID',
|
|
3411
|
+
'hoverActionKey',
|
|
3412
|
+
'actionKey',
|
|
3413
|
+
'accessibilityLabel',
|
|
3414
|
+
'leading',
|
|
3415
|
+
'title',
|
|
3416
|
+
'secondary',
|
|
3417
|
+
'lineGap',
|
|
3418
|
+
'secondaryBadges',
|
|
3419
|
+
'hoverImages',
|
|
3420
|
+
'hoverSecondary',
|
|
3421
|
+
'hoverActionLabel',
|
|
3422
|
+
'badges',
|
|
3423
|
+
'primary',
|
|
3424
|
+
'partGap',
|
|
3425
|
+
'points',
|
|
3426
|
+
'width',
|
|
3427
|
+
'height',
|
|
3428
|
+
'strokeColor',
|
|
3429
|
+
'fillColor',
|
|
3430
|
+
],
|
|
3431
|
+
badges: [
|
|
3432
|
+
'key',
|
|
3433
|
+
'text',
|
|
3434
|
+
'tone',
|
|
3435
|
+
'iconName',
|
|
3436
|
+
'icon',
|
|
3437
|
+
'textColor',
|
|
3438
|
+
'backgroundColor',
|
|
3439
|
+
'actionKey',
|
|
3440
|
+
'accessibilityLabel',
|
|
3441
|
+
'style',
|
|
3442
|
+
],
|
|
3443
|
+
overlays: [
|
|
3444
|
+
'position',
|
|
3445
|
+
'size',
|
|
3446
|
+
'width',
|
|
3447
|
+
'height',
|
|
3448
|
+
'padding',
|
|
3449
|
+
'offset',
|
|
3450
|
+
'offsetX',
|
|
3451
|
+
'offsetY',
|
|
3452
|
+
'image',
|
|
3453
|
+
'fallbackIcon',
|
|
3454
|
+
'name',
|
|
3455
|
+
'text',
|
|
3456
|
+
'fontWeight',
|
|
3457
|
+
'tintColor',
|
|
3458
|
+
'backgroundColor',
|
|
3459
|
+
],
|
|
3460
|
+
primaryHelp: ['actionKey', 'presentation', 'underline'],
|
|
3461
|
+
valueHelp: ['actionKey', 'presentation', 'underline'],
|
|
3462
|
+
};
|
|
3463
|
+
|
|
3464
|
+
function sanitizeDecoration(value: unknown, field: string): unknown {
|
|
3465
|
+
if (value === undefined || value === null) return undefined;
|
|
3466
|
+
if (
|
|
3467
|
+
!Array.isArray(value) &&
|
|
3468
|
+
[
|
|
3469
|
+
'image',
|
|
3470
|
+
'networkImage',
|
|
3471
|
+
'thumbnail',
|
|
3472
|
+
'source',
|
|
3473
|
+
'images',
|
|
3474
|
+
'hoverImages',
|
|
3475
|
+
].includes(field)
|
|
3476
|
+
)
|
|
3477
|
+
return sanitizeImage(value);
|
|
3478
|
+
if (
|
|
3479
|
+
[
|
|
3480
|
+
'titleImage',
|
|
3481
|
+
'titleIcon',
|
|
3482
|
+
'secondaryTitleIcon',
|
|
3483
|
+
'fallbackIcon',
|
|
3484
|
+
'cornerIcon',
|
|
3485
|
+
'media',
|
|
3486
|
+
].includes(field) &&
|
|
3487
|
+
!isRecord(value)
|
|
3488
|
+
)
|
|
3489
|
+
return undefined;
|
|
3490
|
+
if (field === 'style' && typeof value !== 'string' && !isRecord(value))
|
|
3491
|
+
return undefined;
|
|
3492
|
+
if (
|
|
3493
|
+
['badges', 'overlays'].includes(field) &&
|
|
3494
|
+
!Array.isArray(value) &&
|
|
3495
|
+
!isRecord(value)
|
|
3496
|
+
)
|
|
3497
|
+
return undefined;
|
|
3498
|
+
if (field === 'style' && typeof value === 'string')
|
|
3499
|
+
return value === 'subscript' ? value : undefined;
|
|
3500
|
+
if (Array.isArray(value)) {
|
|
3501
|
+
const items = value.map((item) => sanitizeDecoration(item, field));
|
|
3502
|
+
return field === 'hoverImages'
|
|
3503
|
+
? items.map((item) => item ?? null)
|
|
3504
|
+
: items.filter((item) => item !== undefined);
|
|
3505
|
+
}
|
|
3506
|
+
if (!isRecord(value)) return value;
|
|
3507
|
+
let object = value;
|
|
3508
|
+
if (field === 'titleImage') {
|
|
3509
|
+
const image = sanitizeImage(value.image);
|
|
3510
|
+
if (!image) return undefined;
|
|
3511
|
+
const edge = Math.min(
|
|
3512
|
+
40,
|
|
3513
|
+
Math.max(
|
|
3514
|
+
1,
|
|
3515
|
+
Math.round(Math.min(image.width as number, image.height as number))
|
|
3516
|
+
)
|
|
3517
|
+
);
|
|
3518
|
+
object = {
|
|
3519
|
+
...pickFields(value, ['image', 'accessibilityLabel', 'hoverActionKey']),
|
|
3520
|
+
image: { ...image, width: edge, height: edge },
|
|
3521
|
+
};
|
|
3522
|
+
} else if (
|
|
3523
|
+
['titleIcon', 'secondaryTitleIcon', 'fallbackIcon', 'cornerIcon'].includes(
|
|
3524
|
+
field
|
|
3525
|
+
) &&
|
|
3526
|
+
value.kind === undefined
|
|
3527
|
+
) {
|
|
3528
|
+
if (typeof value.name !== 'string' || !value.name.trim()) return undefined;
|
|
3529
|
+
object = pickFields(value, ICON_FIELDS);
|
|
3530
|
+
} else if (['leading', 'visual', 'secondaryLeading'].includes(field)) {
|
|
3531
|
+
object = pickFields(value, VISUAL_FIELDS);
|
|
3532
|
+
} else if (field === 'media') {
|
|
3533
|
+
object = pickFields(value, ['source', 'probeOrder']);
|
|
3534
|
+
}
|
|
3535
|
+
if (
|
|
3536
|
+
field === 'trailing' ||
|
|
3537
|
+
(['leadingAction', 'titleIcon', 'valueIcon'].includes(field) &&
|
|
3538
|
+
object.kind !== undefined)
|
|
3539
|
+
) {
|
|
3540
|
+
const fields = ACCESSORY_FIELDS[object.kind as string];
|
|
3541
|
+
if (fields) object = pickFields(object, fields);
|
|
3542
|
+
} else if (field === 'buttons')
|
|
3543
|
+
object = pickFields(object, [
|
|
3544
|
+
'key',
|
|
3545
|
+
'label',
|
|
3546
|
+
'actionKey',
|
|
3547
|
+
'disabled',
|
|
3548
|
+
'loading',
|
|
3549
|
+
'testID',
|
|
3550
|
+
'accessibilityLabel',
|
|
3551
|
+
]);
|
|
3552
|
+
else if (field === 'container')
|
|
3553
|
+
object = pickFields(object, [
|
|
3554
|
+
'height',
|
|
3555
|
+
'backgroundColor',
|
|
3556
|
+
'opacity',
|
|
3557
|
+
'cornerRadius',
|
|
3558
|
+
'borderWidth',
|
|
3559
|
+
'borderColor',
|
|
3560
|
+
'contentVerticalAlignment',
|
|
3561
|
+
]);
|
|
3562
|
+
else if (field === 'imageStyle')
|
|
3563
|
+
object = pickFields(object, [
|
|
3564
|
+
'width',
|
|
3565
|
+
'height',
|
|
3566
|
+
'shape',
|
|
3567
|
+
'cornerRadius',
|
|
3568
|
+
'contentFit',
|
|
3569
|
+
]);
|
|
3570
|
+
else if (field === 'style')
|
|
3571
|
+
object = pickFields(object, [
|
|
3572
|
+
...TEXT_STYLE_FIELDS,
|
|
3573
|
+
'container',
|
|
3574
|
+
...new Set(Object.values(ROW_BOX_STYLE_KEYS_BY_TYPE).flat()),
|
|
3575
|
+
...new Set(Object.values(TEXT_STYLE_KEYS_BY_ROW_TYPE).flat()),
|
|
3576
|
+
...new Set(Object.values(EXTRA_STYLE_KEYS_BY_ROW_TYPE).flat()),
|
|
3577
|
+
'height',
|
|
3578
|
+
]);
|
|
3579
|
+
else if (
|
|
3580
|
+
[
|
|
3581
|
+
'title',
|
|
3582
|
+
'subtitle',
|
|
3583
|
+
'tertiary',
|
|
3584
|
+
'body',
|
|
3585
|
+
'time',
|
|
3586
|
+
'price',
|
|
3587
|
+
'change',
|
|
3588
|
+
'value',
|
|
3589
|
+
'valueSecondary',
|
|
3590
|
+
'columnSecondary',
|
|
3591
|
+
'primaryAmount',
|
|
3592
|
+
'secondaryAmount',
|
|
3593
|
+
'description',
|
|
3594
|
+
'status',
|
|
3595
|
+
'trend',
|
|
3596
|
+
'message',
|
|
3597
|
+
'actionText',
|
|
3598
|
+
].includes(field) &&
|
|
3599
|
+
!('text' in object)
|
|
3600
|
+
) {
|
|
3601
|
+
object = pickFields(object, TEXT_STYLE_FIELDS);
|
|
3602
|
+
}
|
|
3603
|
+
if (NESTED_DECORATION_FIELDS[field])
|
|
3604
|
+
object = pickFields(object, NESTED_DECORATION_FIELDS[field]);
|
|
3605
|
+
if (
|
|
3606
|
+
[
|
|
3607
|
+
'textSegments',
|
|
3608
|
+
'primaryTextSegments',
|
|
3609
|
+
'secondaryTextSegments',
|
|
3610
|
+
'priceSegments',
|
|
3611
|
+
'segments',
|
|
3612
|
+
].includes(field)
|
|
3613
|
+
)
|
|
3614
|
+
object = pickFields(object, ['text', 'style']);
|
|
3615
|
+
if (field === 'subtitleSegments')
|
|
3616
|
+
object = pickFields(object, [
|
|
3617
|
+
'text',
|
|
3618
|
+
'style',
|
|
3619
|
+
'textSegments',
|
|
3620
|
+
'tone',
|
|
3621
|
+
'separatorBefore',
|
|
3622
|
+
]);
|
|
3623
|
+
const styleObject =
|
|
3624
|
+
['style', 'imageStyle', 'container'].includes(field) ||
|
|
3625
|
+
TEXT_STYLE_FIELDS.some((key) => key in object);
|
|
3626
|
+
const next: Record<string, unknown> = {};
|
|
3627
|
+
for (const [key, item] of Object.entries(object)) {
|
|
3628
|
+
if (item === undefined || item === null) continue;
|
|
3629
|
+
if (
|
|
3630
|
+
(/color$/i.test(key) || /background$/i.test(key)) &&
|
|
3631
|
+
(typeof item !== 'string' || !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(item))
|
|
3632
|
+
)
|
|
3633
|
+
continue;
|
|
3634
|
+
if (
|
|
3635
|
+
/TextSegments$/.test(key) ||
|
|
3636
|
+
[
|
|
3637
|
+
'textSegments',
|
|
3638
|
+
'priceSegments',
|
|
3639
|
+
'subtitleSegments',
|
|
3640
|
+
'segments',
|
|
3641
|
+
].includes(key)
|
|
3642
|
+
) {
|
|
3643
|
+
if (
|
|
3644
|
+
!Array.isArray(item) ||
|
|
3645
|
+
item.some(
|
|
3646
|
+
(segment) => !isRecord(segment) || typeof segment.text !== 'string'
|
|
3647
|
+
)
|
|
3648
|
+
)
|
|
3649
|
+
continue;
|
|
3650
|
+
}
|
|
3651
|
+
if (
|
|
3652
|
+
styleObject &&
|
|
3653
|
+
PROTOCOL_NUMBER_FIELDS.has(key) &&
|
|
3654
|
+
typeof item !== 'number'
|
|
3655
|
+
)
|
|
3656
|
+
continue;
|
|
3657
|
+
if (
|
|
3658
|
+
key === 'token' &&
|
|
3659
|
+
(typeof item !== 'string' || !Object.hasOwn(TYPOGRAPHY_TOKENS, item))
|
|
3660
|
+
)
|
|
3661
|
+
continue;
|
|
3662
|
+
if (
|
|
3663
|
+
key === 'fontWeight' &&
|
|
3664
|
+
!['regular', 'medium', 'semibold', 'bold'].includes(item as string)
|
|
3665
|
+
)
|
|
3666
|
+
continue;
|
|
3667
|
+
if (
|
|
3668
|
+
[
|
|
3669
|
+
'disabled',
|
|
3670
|
+
'loading',
|
|
3671
|
+
'checked',
|
|
3672
|
+
'selected',
|
|
3673
|
+
'separator',
|
|
3674
|
+
'hideLeading',
|
|
3675
|
+
'pressDisabled',
|
|
3676
|
+
'draggable',
|
|
3677
|
+
'active',
|
|
3678
|
+
'autoplay',
|
|
3679
|
+
].includes(key) &&
|
|
3680
|
+
typeof item !== 'boolean'
|
|
3681
|
+
)
|
|
3682
|
+
continue;
|
|
3683
|
+
if (key === 'primaryFontWeight' && item !== 'medium') continue;
|
|
3684
|
+
if (key === 'buttonStyle' && item !== 'tertiary') continue;
|
|
3685
|
+
if (key === 'hoverActionKey' && (typeof item !== 'string' || !item.trim()))
|
|
3686
|
+
continue;
|
|
3687
|
+
if (key === 'accessibilityLabel' && typeof item !== 'string') continue;
|
|
3688
|
+
if (
|
|
3689
|
+
(field === 'style' || PROTOCOL_NUMBER_FIELDS.has(key)) &&
|
|
3690
|
+
typeof item !== 'object' &&
|
|
3691
|
+
typeof item === 'number' &&
|
|
3692
|
+
(!Number.isFinite(item) ||
|
|
3693
|
+
(item < 0 && !['offsetX', 'offsetY'].includes(key)))
|
|
3694
|
+
)
|
|
3695
|
+
continue;
|
|
3696
|
+
if (
|
|
3697
|
+
field === 'style' &&
|
|
3698
|
+
PROTOCOL_NUMBER_FIELDS.has(key) &&
|
|
3699
|
+
typeof item !== 'number'
|
|
3700
|
+
)
|
|
3701
|
+
continue;
|
|
3702
|
+
if (
|
|
3703
|
+
key === 'leadingContentMaxWidthRatio' &&
|
|
3704
|
+
(typeof item !== 'number' || item <= 0 || item > 1)
|
|
3705
|
+
)
|
|
3706
|
+
continue;
|
|
3707
|
+
const normalized = sanitizeDecoration(
|
|
3708
|
+
item,
|
|
3709
|
+
field === 'style' && key === 'image' ? 'imageStyle' : key
|
|
3710
|
+
);
|
|
3711
|
+
if (normalized !== undefined) next[key] = normalized;
|
|
3712
|
+
}
|
|
3713
|
+
if (field === 'titleImage') {
|
|
3714
|
+
// The nested image was already normalized with title-specific geometry.
|
|
3715
|
+
if (!next.image) return undefined;
|
|
3716
|
+
}
|
|
3717
|
+
if (field === 'media' && (!next.source || !Array.isArray(next.probeOrder)))
|
|
3718
|
+
return undefined;
|
|
3719
|
+
if (['leading', 'visual', 'secondaryLeading'].includes(field)) {
|
|
3720
|
+
if (next.kind === 'image' && !next.image)
|
|
3721
|
+
return {
|
|
3722
|
+
kind: 'token',
|
|
3723
|
+
...pickFields(next, [
|
|
3724
|
+
'fallbackIcon',
|
|
3725
|
+
'fallbackText',
|
|
3726
|
+
'fallbackTextColor',
|
|
3727
|
+
]),
|
|
3728
|
+
};
|
|
3729
|
+
if (
|
|
3730
|
+
next.kind === 'stackedImages' &&
|
|
3731
|
+
(!Array.isArray(next.images) || next.images.length < 2)
|
|
3732
|
+
)
|
|
3733
|
+
return { kind: 'token' };
|
|
3734
|
+
if (
|
|
3735
|
+
next.kind === 'icon' &&
|
|
3736
|
+
(typeof next.name !== 'string' || !next.name.trim())
|
|
3737
|
+
)
|
|
3738
|
+
return { kind: 'token' };
|
|
3739
|
+
if (
|
|
3740
|
+
![
|
|
3741
|
+
'token',
|
|
3742
|
+
'account',
|
|
3743
|
+
'wallet',
|
|
3744
|
+
'network',
|
|
3745
|
+
'image',
|
|
3746
|
+
'icon',
|
|
3747
|
+
'stackedImages',
|
|
3748
|
+
].includes(next.kind as string)
|
|
3749
|
+
)
|
|
3750
|
+
return { kind: 'token' };
|
|
3751
|
+
}
|
|
3752
|
+
if (
|
|
3753
|
+
field === 'overlays' &&
|
|
3754
|
+
(!['topLeft', 'bottomRight'].includes(next.position as string) ||
|
|
3755
|
+
(!next.image && !next.name && !next.text))
|
|
3756
|
+
)
|
|
3757
|
+
return undefined;
|
|
3758
|
+
if (
|
|
3759
|
+
field === 'trailing' &&
|
|
3760
|
+
next.kind === 'icon' &&
|
|
3761
|
+
!('actionKey' in value) &&
|
|
3762
|
+
(typeof next.name !== 'string' || !next.name.trim())
|
|
3763
|
+
)
|
|
3764
|
+
return undefined;
|
|
3765
|
+
if (
|
|
3766
|
+
field === 'trailing' &&
|
|
3767
|
+
![
|
|
3768
|
+
'value',
|
|
3769
|
+
'valuePair',
|
|
3770
|
+
'button',
|
|
3771
|
+
'checkbox',
|
|
3772
|
+
'radio',
|
|
3773
|
+
'switch',
|
|
3774
|
+
'chevron',
|
|
3775
|
+
'menu',
|
|
3776
|
+
'drag',
|
|
3777
|
+
'icon',
|
|
3778
|
+
'spinner',
|
|
3779
|
+
'progress',
|
|
3780
|
+
].includes(next.kind as string) &&
|
|
3781
|
+
!('actionKey' in value)
|
|
3782
|
+
)
|
|
3783
|
+
return undefined;
|
|
3784
|
+
if (
|
|
3785
|
+
field === 'badges' &&
|
|
3786
|
+
(typeof next.key !== 'string' ||
|
|
3787
|
+
(!next.text && !next.iconName && !next.icon))
|
|
3788
|
+
)
|
|
3789
|
+
return undefined;
|
|
3790
|
+
if (next.kind === 'sparkline') {
|
|
3791
|
+
const points = Array.isArray(value.points)
|
|
3792
|
+
? value.points
|
|
3793
|
+
.filter(
|
|
3794
|
+
(point) => typeof point === 'number' && Number.isFinite(point)
|
|
3795
|
+
)
|
|
3796
|
+
.slice(0, 256)
|
|
3797
|
+
: [];
|
|
3798
|
+
// Preserve the table's column identity even when its optional drawing fails.
|
|
3799
|
+
return points.length < 2
|
|
3800
|
+
? { key: next.key, kind: 'text', primary: [] }
|
|
3801
|
+
: { ...next, points };
|
|
3802
|
+
}
|
|
3803
|
+
return next;
|
|
3804
|
+
}
|
|
3805
|
+
|
|
3806
|
+
function sanitizeRowForProduction(row: RowModel): RowModel {
|
|
3807
|
+
const object = protocolObject(row, 'row');
|
|
3808
|
+
const fields = ROW_FIELDS[row.type];
|
|
3809
|
+
if (!fields) return row;
|
|
3810
|
+
const next = pickFields(object, fields);
|
|
3811
|
+
for (const [field, value] of Object.entries(next)) {
|
|
3812
|
+
if (
|
|
3813
|
+
(/color$/i.test(field) || /background$/i.test(field)) &&
|
|
3814
|
+
(typeof value !== 'string' ||
|
|
3815
|
+
!/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(value))
|
|
3816
|
+
) {
|
|
3817
|
+
delete next[field];
|
|
3818
|
+
continue;
|
|
3819
|
+
}
|
|
3820
|
+
if (field === 'parent' && isRecord(value))
|
|
3821
|
+
next[field] = sanitizeRowForProduction(value as unknown as RowModel);
|
|
3822
|
+
else if (field === 'children' && Array.isArray(value))
|
|
3823
|
+
next[field] = value.map((child) =>
|
|
3824
|
+
sanitizeRowForProduction(child as RowModel)
|
|
3825
|
+
);
|
|
3826
|
+
else {
|
|
3827
|
+
const normalized = sanitizeDecoration(value, field);
|
|
3828
|
+
if (normalized === undefined) delete next[field];
|
|
3829
|
+
else next[field] = normalized;
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
if (isRecord(next.style))
|
|
3833
|
+
next.style = pickFields(next.style, [
|
|
3834
|
+
'container',
|
|
3835
|
+
...ROW_BOX_STYLE_KEYS_BY_TYPE[row.type],
|
|
3836
|
+
...(TEXT_STYLE_KEYS_BY_ROW_TYPE[row.type] ?? []),
|
|
3837
|
+
...(EXTRA_STYLE_KEYS_BY_ROW_TYPE[row.type] ?? []),
|
|
3838
|
+
]);
|
|
3839
|
+
return sameJsonValue(row, next) ? row : (next as unknown as RowModel);
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3842
|
+
/** Empty images and optional nulls share one meaning before strict assertions. */
|
|
2700
3843
|
function normalizeEmptyImages(value: unknown, field = ''): unknown {
|
|
2701
|
-
if (
|
|
3844
|
+
if (value === null || value === undefined || typeof value !== 'object')
|
|
3845
|
+
return value;
|
|
2702
3846
|
if (Array.isArray(value)) {
|
|
2703
|
-
const next = value.map((item) => normalizeEmptyImages(item));
|
|
2704
|
-
|
|
2705
|
-
field === 'images'
|
|
2706
|
-
? next.filter((item) => item !== undefined)
|
|
2707
|
-
: next.map((item) => (item === undefined ? null : item));
|
|
2708
|
-
return filtered.length === value.length &&
|
|
2709
|
-
filtered.every((item, index) => item === value[index])
|
|
3847
|
+
const next = value.map((item) => normalizeEmptyImages(item, field));
|
|
3848
|
+
return next.every((item, index) => item === value[index])
|
|
2710
3849
|
? value
|
|
2711
|
-
:
|
|
3850
|
+
: next.filter((item) => item !== undefined);
|
|
2712
3851
|
}
|
|
2713
3852
|
const object = value as Record<string, unknown>;
|
|
2714
3853
|
if (
|
|
@@ -2719,30 +3858,146 @@ function normalizeEmptyImages(value: unknown, field = ''): unknown {
|
|
|
2719
3858
|
)
|
|
2720
3859
|
return undefined;
|
|
2721
3860
|
let changed = false;
|
|
2722
|
-
const next = {
|
|
2723
|
-
|
|
2724
|
-
const
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
3861
|
+
const next: Record<string, unknown> = {};
|
|
3862
|
+
for (const [key, item] of Object.entries(object)) {
|
|
3863
|
+
const optional =
|
|
3864
|
+
object.type &&
|
|
3865
|
+
![
|
|
3866
|
+
'key',
|
|
3867
|
+
'type',
|
|
3868
|
+
...(REQUIRED_ROW_FIELDS[object.type as string] ?? []),
|
|
3869
|
+
].includes(key);
|
|
3870
|
+
if (
|
|
3871
|
+
item === undefined ||
|
|
3872
|
+
(item === null && optional) ||
|
|
3873
|
+
(['priceSegments', 'subtitleSegments'].includes(key) &&
|
|
3874
|
+
Array.isArray(item) &&
|
|
3875
|
+
item.length === 0)
|
|
3876
|
+
) {
|
|
2729
3877
|
changed = true;
|
|
3878
|
+
continue;
|
|
2730
3879
|
}
|
|
2731
|
-
|
|
2732
|
-
|
|
3880
|
+
const normalized =
|
|
3881
|
+
field === 'headers' ? item : normalizeEmptyImages(item, key);
|
|
3882
|
+
if (normalized !== undefined) next[key] = normalized;
|
|
3883
|
+
if (normalized !== item) changed = true;
|
|
3884
|
+
}
|
|
3885
|
+
if (
|
|
3886
|
+
(field === 'media' &&
|
|
3887
|
+
object.source !== undefined &&
|
|
3888
|
+
next.source === undefined) ||
|
|
3889
|
+
(field === 'titleImage' &&
|
|
3890
|
+
object.image !== undefined &&
|
|
3891
|
+
next.image === undefined)
|
|
3892
|
+
)
|
|
3893
|
+
return undefined;
|
|
3894
|
+
if (
|
|
3895
|
+
['leading', 'visual', 'secondaryLeading'].includes(field) &&
|
|
3896
|
+
next.kind === 'image' &&
|
|
3897
|
+
!next.image
|
|
3898
|
+
)
|
|
3899
|
+
return { kind: 'token' };
|
|
3900
|
+
if (field === 'overlays' && object.image !== undefined && !next.image)
|
|
3901
|
+
return undefined;
|
|
2733
3902
|
return changed ? next : value;
|
|
2734
3903
|
}
|
|
2735
3904
|
|
|
3905
|
+
function normalizeSnapshotForProduction(
|
|
3906
|
+
snapshot: NativeListSnapshot
|
|
3907
|
+
): NativeListSnapshot {
|
|
3908
|
+
if (!Array.isArray(snapshot.rows)) return snapshot;
|
|
3909
|
+
let rows = snapshot.rows.map(sanitizeRowForProduction);
|
|
3910
|
+
rows = rows.map((row, index) => {
|
|
3911
|
+
if (!row.groupId) return row;
|
|
3912
|
+
const previous = rows[index - 1]?.groupId === row.groupId;
|
|
3913
|
+
const following = rows[index + 1]?.groupId === row.groupId;
|
|
3914
|
+
return {
|
|
3915
|
+
...row,
|
|
3916
|
+
groupPosition: previous
|
|
3917
|
+
? following
|
|
3918
|
+
? 'middle'
|
|
3919
|
+
: 'last'
|
|
3920
|
+
: following
|
|
3921
|
+
? 'first'
|
|
3922
|
+
: 'single',
|
|
3923
|
+
};
|
|
3924
|
+
});
|
|
3925
|
+
const next = {
|
|
3926
|
+
...snapshot,
|
|
3927
|
+
rows,
|
|
3928
|
+
...(snapshot.emptyState
|
|
3929
|
+
? {
|
|
3930
|
+
emptyState: sanitizeRowForProduction(
|
|
3931
|
+
snapshot.emptyState
|
|
3932
|
+
) as NativeListSnapshot['emptyState'],
|
|
3933
|
+
}
|
|
3934
|
+
: {}),
|
|
3935
|
+
...(snapshot.fixedFooter
|
|
3936
|
+
? {
|
|
3937
|
+
fixedFooter: sanitizeRowForProduction(
|
|
3938
|
+
snapshot.fixedFooter
|
|
3939
|
+
) as NativeListSnapshot['fixedFooter'],
|
|
3940
|
+
}
|
|
3941
|
+
: {}),
|
|
3942
|
+
};
|
|
3943
|
+
if (snapshot.theme)
|
|
3944
|
+
next.theme = sanitizeDecoration(
|
|
3945
|
+
snapshot.theme,
|
|
3946
|
+
'theme'
|
|
3947
|
+
) as NativeListSnapshot['theme'];
|
|
3948
|
+
if (snapshot.listStyle)
|
|
3949
|
+
next.listStyle = sanitizeDecoration(
|
|
3950
|
+
snapshot.listStyle,
|
|
3951
|
+
'style'
|
|
3952
|
+
) as NativeListSnapshot['listStyle'];
|
|
3953
|
+
if (snapshot.selection && Array.isArray(snapshot.selection.selectedKeys)) {
|
|
3954
|
+
const selectable = new Set(
|
|
3955
|
+
rows
|
|
3956
|
+
.filter(
|
|
3957
|
+
(row) =>
|
|
3958
|
+
!row.disabled &&
|
|
3959
|
+
![
|
|
3960
|
+
'sectionHeader',
|
|
3961
|
+
'action',
|
|
3962
|
+
'system',
|
|
3963
|
+
'buttonGroup',
|
|
3964
|
+
'market',
|
|
3965
|
+
'walletGroup',
|
|
3966
|
+
].includes(row.type)
|
|
3967
|
+
)
|
|
3968
|
+
.map((row) => row.key)
|
|
3969
|
+
);
|
|
3970
|
+
const selectedKeys = [
|
|
3971
|
+
...new Set(
|
|
3972
|
+
snapshot.selection.selectedKeys.filter((key) => selectable.has(key))
|
|
3973
|
+
),
|
|
3974
|
+
];
|
|
3975
|
+
next.selection = {
|
|
3976
|
+
...snapshot.selection,
|
|
3977
|
+
selectedKeys:
|
|
3978
|
+
snapshot.selection.mode === 'none'
|
|
3979
|
+
? []
|
|
3980
|
+
: snapshot.selection.mode === 'single'
|
|
3981
|
+
? selectedKeys.slice(0, 1)
|
|
3982
|
+
: selectedKeys,
|
|
3983
|
+
};
|
|
3984
|
+
}
|
|
3985
|
+
return sameJsonValue(snapshot, next) ? snapshot : next;
|
|
3986
|
+
}
|
|
3987
|
+
|
|
2736
3988
|
export function validateSnapshot(
|
|
2737
3989
|
snapshot: NativeListSnapshot
|
|
2738
3990
|
): NativeListSnapshot {
|
|
2739
|
-
assertPlainSerializable(snapshot, 'snapshot', new Set<object>());
|
|
2740
|
-
|
|
2741
|
-
const normalized =
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
3991
|
+
assertPlainSerializable(snapshot, 'snapshot', new Set<object>(), false);
|
|
3992
|
+
protocolObject(snapshot, 'snapshot');
|
|
3993
|
+
const normalized = normalizeEmptyImages(snapshot) as NativeListSnapshot;
|
|
3994
|
+
const canonical = isDevelopment()
|
|
3995
|
+
? normalized
|
|
3996
|
+
: normalizeSnapshotForProduction(normalized);
|
|
3997
|
+
assertPlainSerializable(canonical, 'snapshot', new Set<object>());
|
|
3998
|
+
if (isDevelopment()) assertSnapshotContract(canonical);
|
|
3999
|
+
assertSnapshotProtocol(canonical);
|
|
4000
|
+
return normalizeSnapshotStyles(canonical);
|
|
2746
4001
|
}
|
|
2747
4002
|
|
|
2748
4003
|
const ownedNativeSnapshots = new WeakMap<
|
|
@@ -2838,10 +4093,23 @@ function normalizeSnapshotStyles(
|
|
|
2838
4093
|
) {
|
|
2839
4094
|
return snapshot;
|
|
2840
4095
|
}
|
|
2841
|
-
return {
|
|
4096
|
+
return {
|
|
4097
|
+
...snapshot,
|
|
4098
|
+
rows,
|
|
4099
|
+
...(emptyState !== undefined ? { emptyState } : {}),
|
|
4100
|
+
...(fixedFooter !== undefined ? { fixedFooter } : {}),
|
|
4101
|
+
};
|
|
2842
4102
|
}
|
|
2843
4103
|
|
|
2844
|
-
|
|
4104
|
+
type PatchValues<Patch = RowPatch> = Patch extends RowPatch
|
|
4105
|
+
? Omit<Patch, 'changes'> & {
|
|
4106
|
+
changes: {
|
|
4107
|
+
[Key in keyof Patch['changes']]: Exclude<Patch['changes'][Key], null>;
|
|
4108
|
+
};
|
|
4109
|
+
}
|
|
4110
|
+
: never;
|
|
4111
|
+
|
|
4112
|
+
function assertPatchChanges(patch: PatchValues, index: number): void {
|
|
2845
4113
|
const path = `patches[${index}].changes`;
|
|
2846
4114
|
const patchedStyle = (patch.changes as { style?: unknown }).style;
|
|
2847
4115
|
if (patchedStyle !== undefined) {
|
|
@@ -3135,36 +4403,150 @@ function assertPatchChanges(patch: RowPatch, index: number): void {
|
|
|
3135
4403
|
}
|
|
3136
4404
|
}
|
|
3137
4405
|
|
|
4406
|
+
function normalizePatch(patch: RowPatch, index: number): RowPatch | undefined {
|
|
4407
|
+
const path = `patches[${index}]`;
|
|
4408
|
+
protocolObject(patch, path);
|
|
4409
|
+
protocolKey(patch.key, `${path}.key`);
|
|
4410
|
+
if (isDevelopment()) assertKey(patch.key, `${path}.key`);
|
|
4411
|
+
assertRowType(patch.type, path);
|
|
4412
|
+
const raw = protocolObject(patch.changes, `${path}.changes`);
|
|
4413
|
+
assertPlainSerializable(raw, `${path}.changes`, new Set<object>(), false);
|
|
4414
|
+
const fields = PATCH_FIELDS[patch.type];
|
|
4415
|
+
const changes: Record<string, unknown> = {};
|
|
4416
|
+
for (const [field, value] of Object.entries(raw)) {
|
|
4417
|
+
if (value === undefined) continue;
|
|
4418
|
+
if (!fields?.includes(field)) {
|
|
4419
|
+
if (isDevelopment())
|
|
4420
|
+
fail(`${path}.changes.${field}`, 'unsupported patch field');
|
|
4421
|
+
continue;
|
|
4422
|
+
}
|
|
4423
|
+
if (value === null) {
|
|
4424
|
+
if (
|
|
4425
|
+
REQUIRED_ROW_FIELDS[patch.type].includes(field) ||
|
|
4426
|
+
(patch.type === 'system' && field === 'message')
|
|
4427
|
+
) {
|
|
4428
|
+
// The concrete system variant is checked after merging with its baseline.
|
|
4429
|
+
if (patch.type !== 'system')
|
|
4430
|
+
fail(`${path}.changes.${field}`, 'required field cannot be cleared');
|
|
4431
|
+
}
|
|
4432
|
+
changes[field] = null;
|
|
4433
|
+
continue;
|
|
4434
|
+
}
|
|
4435
|
+
if (
|
|
4436
|
+
['priceSegments', 'subtitleSegments'].includes(field) &&
|
|
4437
|
+
Array.isArray(value) &&
|
|
4438
|
+
value.length === 0
|
|
4439
|
+
) {
|
|
4440
|
+
changes[field] = null;
|
|
4441
|
+
continue;
|
|
4442
|
+
}
|
|
4443
|
+
const normalized = normalizeEmptyImages(value, field);
|
|
4444
|
+
if (normalized === undefined) {
|
|
4445
|
+
if (!REQUIRED_ROW_FIELDS[patch.type].includes(field))
|
|
4446
|
+
changes[field] = null;
|
|
4447
|
+
continue;
|
|
4448
|
+
}
|
|
4449
|
+
const canonical = isDevelopment()
|
|
4450
|
+
? normalized
|
|
4451
|
+
: sanitizeDecoration(normalized, field);
|
|
4452
|
+
if (canonical !== undefined) changes[field] = canonical;
|
|
4453
|
+
else if (field === 'titleImage') changes[field] = null;
|
|
4454
|
+
}
|
|
4455
|
+
if (
|
|
4456
|
+
patch.type === 'market' &&
|
|
4457
|
+
Array.isArray(changes.priceSegments) &&
|
|
4458
|
+
changes.priceSegments.length > 0 &&
|
|
4459
|
+
(typeof changes.price !== 'string' || !changes.price.trim())
|
|
4460
|
+
)
|
|
4461
|
+
fail(`${path}.changes.price`, 'is required when priceSegments is updated');
|
|
4462
|
+
if (!isDevelopment()) {
|
|
4463
|
+
const sanitized = sanitizeRowForProduction({
|
|
4464
|
+
type: patch.type,
|
|
4465
|
+
...changes,
|
|
4466
|
+
} as unknown as RowModel) as unknown as Record<string, unknown>;
|
|
4467
|
+
for (const [field, value] of Object.entries(changes)) {
|
|
4468
|
+
if (value === null) continue;
|
|
4469
|
+
if (sanitized[field] === undefined) delete changes[field];
|
|
4470
|
+
else changes[field] = sanitized[field];
|
|
4471
|
+
}
|
|
4472
|
+
}
|
|
4473
|
+
const values = Object.fromEntries(
|
|
4474
|
+
Object.entries(changes).filter(([, value]) => value !== null)
|
|
4475
|
+
);
|
|
4476
|
+
assertPlainSerializable(values, `${path}.changes`, new Set<object>());
|
|
4477
|
+
assertProtocolFields(values, `${path}.changes`);
|
|
4478
|
+
if (isDevelopment())
|
|
4479
|
+
assertPatchChanges({ ...patch, changes: values } as PatchValues, index);
|
|
4480
|
+
const normalizedPatch = {
|
|
4481
|
+
type: patch.type,
|
|
4482
|
+
key: patch.key,
|
|
4483
|
+
changes,
|
|
4484
|
+
} as RowPatch;
|
|
4485
|
+
return Object.keys(changes).length > 0
|
|
4486
|
+
? normalizePatchStyles([normalizedPatch])[0]
|
|
4487
|
+
: undefined;
|
|
4488
|
+
}
|
|
4489
|
+
|
|
4490
|
+
function reportRejectedPatch(index: number): void {
|
|
4491
|
+
console.warn(`NativeList rejected invalid patch unit at patches[${index}]`);
|
|
4492
|
+
}
|
|
4493
|
+
|
|
3138
4494
|
export function validatePatches(
|
|
3139
4495
|
patches: readonly RowPatch[]
|
|
3140
4496
|
): readonly RowPatch[] {
|
|
3141
|
-
assertPlainSerializable(patches, 'patches', new Set<object>());
|
|
3142
4497
|
if (!Array.isArray(patches)) fail('patches', 'must be an array');
|
|
3143
|
-
const
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
4498
|
+
const units = new Map<string, { patch: RowPatch; index: number }>();
|
|
4499
|
+
const rejectedKeys = new Set<string>();
|
|
4500
|
+
const reject = (key: string, index: number, error: unknown) => {
|
|
4501
|
+
if (isDevelopment()) throw error;
|
|
4502
|
+
rejectedKeys.add(key);
|
|
4503
|
+
reportRejectedPatch(index);
|
|
4504
|
+
};
|
|
4505
|
+
patches.forEach((input, index) => {
|
|
4506
|
+
const key =
|
|
4507
|
+
isRecord(input) && typeof input.key === 'string' ? input.key : '';
|
|
4508
|
+
try {
|
|
4509
|
+
protocolObject(input, `patches[${index}]`);
|
|
4510
|
+
protocolKey(input.key, `patches[${index}].key`);
|
|
4511
|
+
assertRowType(input.type, `patches[${index}]`);
|
|
4512
|
+
const changes = capturePlainData(
|
|
4513
|
+
protocolObject(input.changes, `patches[${index}].changes`)
|
|
4514
|
+
);
|
|
4515
|
+
assertPlainSerializable(
|
|
4516
|
+
changes,
|
|
4517
|
+
`patches[${index}].changes`,
|
|
4518
|
+
new Set<object>(),
|
|
4519
|
+
false
|
|
4520
|
+
);
|
|
4521
|
+
const previous = units.get(key);
|
|
4522
|
+
if (previous && previous.patch.type !== input.type)
|
|
4523
|
+
fail(`patches[${index}].type`, 'conflicting row types');
|
|
4524
|
+
const defined = Object.fromEntries(
|
|
4525
|
+
Object.entries(changes).filter(([, value]) => value !== undefined)
|
|
4526
|
+
);
|
|
4527
|
+
units.set(key, {
|
|
4528
|
+
index,
|
|
4529
|
+
patch: {
|
|
4530
|
+
key,
|
|
4531
|
+
type: input.type,
|
|
4532
|
+
changes: { ...previous?.patch.changes, ...defined },
|
|
4533
|
+
} as RowPatch,
|
|
4534
|
+
});
|
|
4535
|
+
} catch (error) {
|
|
4536
|
+
reject(key, index, error);
|
|
4537
|
+
}
|
|
4538
|
+
});
|
|
4539
|
+
const accepted: RowPatch[] = [];
|
|
4540
|
+
for (const [key, unit] of units) {
|
|
4541
|
+
if (rejectedKeys.has(key)) continue;
|
|
4542
|
+
try {
|
|
4543
|
+
const patch = normalizePatch(unit.patch, unit.index);
|
|
4544
|
+
if (patch) accepted.push(patch);
|
|
4545
|
+
} catch (error) {
|
|
4546
|
+
reject(key, unit.index, error);
|
|
3159
4547
|
}
|
|
3160
|
-
assertPatchChanges(patch, index);
|
|
3161
4548
|
}
|
|
3162
|
-
|
|
3163
|
-
? patches
|
|
3164
|
-
: (normalizeEmptyImages(patches) as readonly RowPatch[]);
|
|
3165
|
-
return normalizePatchStyles(
|
|
3166
|
-
normalized.filter((patch) => Object.keys(patch.changes).length > 0)
|
|
3167
|
-
);
|
|
4549
|
+
return accepted;
|
|
3168
4550
|
}
|
|
3169
4551
|
|
|
3170
4552
|
/** Patches reach the native side without passing through a snapshot. */
|
|
@@ -3296,50 +4678,65 @@ export function planNativeListPatches(
|
|
|
3296
4678
|
return patches.length === 0 ? EMPTY_ROW_PATCHES : patches;
|
|
3297
4679
|
}
|
|
3298
4680
|
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
)
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
fail('patches', `unknown row key "${patch.key}"`);
|
|
3311
|
-
});
|
|
3312
|
-
const rows = snapshot.rows.map((row): RowModel => {
|
|
3313
|
-
const patch = patchByKey.get(row.key);
|
|
3314
|
-
if (!patch) return row;
|
|
3315
|
-
if (patch.type !== row.type) {
|
|
3316
|
-
fail('patches', `row "${row.key}" is ${row.type}, not ${patch.type}`);
|
|
3317
|
-
}
|
|
4681
|
+
function mergeAndValidateSingleRow(
|
|
4682
|
+
previous: RowModel,
|
|
4683
|
+
patch: RowPatch,
|
|
4684
|
+
index: number
|
|
4685
|
+
): { row: RowModel; patch: RowPatch } {
|
|
4686
|
+
if (previous.key !== patch.key)
|
|
4687
|
+
fail(`patches[${index}].key`, 'does not match row identity');
|
|
4688
|
+
if (previous.type !== patch.type)
|
|
4689
|
+
fail(`patches[${index}].type`, 'does not match row type');
|
|
4690
|
+
const changes = { ...patch.changes } as Record<string, unknown>;
|
|
4691
|
+
if (previous.type === 'market') {
|
|
3318
4692
|
if (
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
4693
|
+
typeof changes.price === 'string' &&
|
|
4694
|
+
!('priceSegments' in changes) &&
|
|
4695
|
+
previous.priceSegments !== undefined
|
|
4696
|
+
)
|
|
4697
|
+
changes.priceSegments = null;
|
|
4698
|
+
if (
|
|
4699
|
+
(previous.webTable !== undefined || 'webTable' in changes) &&
|
|
4700
|
+
['price', 'priceSegments', 'change'].some((field) => field in changes) &&
|
|
4701
|
+
changes.webTable === undefined
|
|
3326
4702
|
)
|
|
3327
4703
|
fail(
|
|
3328
|
-
|
|
3329
|
-
'table quote changes
|
|
4704
|
+
`patches[${index}].changes.webTable`,
|
|
4705
|
+
'table quote changes require visible table cells'
|
|
3330
4706
|
);
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
4707
|
+
}
|
|
4708
|
+
if (previous.type === 'identity' || previous.type === 'market') {
|
|
4709
|
+
if (
|
|
4710
|
+
typeof changes.subtitle === 'string' &&
|
|
4711
|
+
!('subtitleSegments' in changes) &&
|
|
4712
|
+
previous.subtitleSegments !== undefined
|
|
4713
|
+
)
|
|
4714
|
+
changes.subtitleSegments = null;
|
|
4715
|
+
if (
|
|
4716
|
+
Array.isArray(changes.subtitleSegments) &&
|
|
4717
|
+
changes.subtitleSegments.length > 0 &&
|
|
4718
|
+
!('subtitle' in changes) &&
|
|
4719
|
+
previous.subtitle !== undefined
|
|
4720
|
+
)
|
|
4721
|
+
changes.subtitle = null;
|
|
4722
|
+
}
|
|
4723
|
+
const merged = { ...previous } as unknown as Record<string, unknown>;
|
|
4724
|
+
for (const [field, value] of Object.entries(changes)) {
|
|
4725
|
+
if (value === null) delete merged[field];
|
|
4726
|
+
else if (value !== undefined) merged[field] = value;
|
|
4727
|
+
}
|
|
4728
|
+
const next = normalizeRowStyles(merged as unknown as RowModel);
|
|
4729
|
+
assertPlainSerializable(next, `rows[${index}]`, new Set<object>());
|
|
4730
|
+
if (isDevelopment()) assertRow(next, index);
|
|
4731
|
+
assertRowProtocol(next, `rows[${index}]`);
|
|
4732
|
+
return {
|
|
4733
|
+
row: next,
|
|
4734
|
+
patch: { type: patch.type, key: patch.key, changes } as RowPatch,
|
|
4735
|
+
};
|
|
3339
4736
|
}
|
|
3340
4737
|
|
|
3341
|
-
/**
|
|
3342
|
-
export function
|
|
4738
|
+
/** One canonical batch owns both accepted rows and the native wire payload. */
|
|
4739
|
+
export function applyValidatedRowPatches(
|
|
3343
4740
|
snapshot: NativeListSnapshot,
|
|
3344
4741
|
patches: readonly RowPatch[]
|
|
3345
4742
|
): {
|
|
@@ -3347,74 +4744,127 @@ export function applyOwnedNativePatches(
|
|
|
3347
4744
|
snapshot: NativeListSnapshot;
|
|
3348
4745
|
json: string;
|
|
3349
4746
|
} {
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
4747
|
+
if (!Array.isArray(patches)) fail('patches', 'must be an array');
|
|
4748
|
+
const owned = ownedNativeSnapshots.has(snapshot);
|
|
4749
|
+
const rowIndexes =
|
|
4750
|
+
ownedNativeSnapshots.get(snapshot) ??
|
|
4751
|
+
validatedRowIndexes.get(snapshot) ??
|
|
4752
|
+
new Map(snapshot.rows.map((row, index) => [row.key, index]));
|
|
4753
|
+
validatedRowIndexes.set(snapshot, rowIndexes);
|
|
4754
|
+
const units = new Map<
|
|
4755
|
+
string,
|
|
4756
|
+
{ index: number; changes: Record<string, unknown>; type: RowPatch['type'] }
|
|
4757
|
+
>();
|
|
4758
|
+
const rejected = new Set<string>();
|
|
4759
|
+
const rejectedGroups = new Set<string>();
|
|
4760
|
+
const reject = (key: string, index: number, error: unknown) => {
|
|
4761
|
+
if (isDevelopment()) throw error;
|
|
4762
|
+
rejected.add(key);
|
|
4763
|
+
const rowIndex = rowIndexes.get(key);
|
|
4764
|
+
const group =
|
|
4765
|
+
rowIndex === undefined ? undefined : snapshot.rows[rowIndex].groupId;
|
|
4766
|
+
if (group) rejectedGroups.add(group);
|
|
4767
|
+
reportRejectedPatch(index);
|
|
4768
|
+
};
|
|
4769
|
+
patches.forEach((input, index) => {
|
|
4770
|
+
const key =
|
|
4771
|
+
isRecord(input) && typeof input.key === 'string' ? input.key : '';
|
|
4772
|
+
try {
|
|
4773
|
+
protocolObject(input, `patches[${index}]`);
|
|
4774
|
+
protocolKey(input.key, `patches[${index}].key`);
|
|
4775
|
+
const rowIndex = rowIndexes.get(input.key);
|
|
4776
|
+
if (rowIndex === undefined)
|
|
4777
|
+
fail(`patches[${index}].key`, 'unknown row identity');
|
|
4778
|
+
if (snapshot.rows[rowIndex].type !== input.type)
|
|
4779
|
+
fail(`patches[${index}].type`, 'does not match row type');
|
|
4780
|
+
const changes = protocolObject(
|
|
4781
|
+
input.changes,
|
|
4782
|
+
`patches[${index}].changes`
|
|
3376
4783
|
);
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
)
|
|
3385
|
-
|
|
3386
|
-
|
|
4784
|
+
const captured = capturePlainData(changes);
|
|
4785
|
+
assertPlainSerializable(
|
|
4786
|
+
captured,
|
|
4787
|
+
`patches[${index}].changes`,
|
|
4788
|
+
new Set<object>(),
|
|
4789
|
+
false
|
|
4790
|
+
);
|
|
4791
|
+
const previous = units.get(input.key);
|
|
4792
|
+
// Undefined is omitted at every merge, so it cannot overwrite an earlier change.
|
|
4793
|
+
const defined = Object.fromEntries(
|
|
4794
|
+
Object.entries(captured).filter(([, value]) => value !== undefined)
|
|
4795
|
+
);
|
|
4796
|
+
units.set(input.key, {
|
|
4797
|
+
index,
|
|
4798
|
+
type: input.type,
|
|
4799
|
+
changes: { ...previous?.changes, ...defined },
|
|
4800
|
+
});
|
|
4801
|
+
} catch (error) {
|
|
4802
|
+
reject(key, index, error);
|
|
4803
|
+
}
|
|
4804
|
+
});
|
|
4805
|
+
const pending = new Map<string, { row: RowModel; patch: RowPatch }>();
|
|
4806
|
+
for (const [key, unit] of units) {
|
|
4807
|
+
if (rejected.has(key)) continue;
|
|
4808
|
+
try {
|
|
4809
|
+
const patch = normalizePatch(
|
|
4810
|
+
{ key, type: unit.type, changes: unit.changes } as RowPatch,
|
|
4811
|
+
unit.index
|
|
4812
|
+
);
|
|
4813
|
+
if (!patch) continue;
|
|
4814
|
+
const merged = mergeAndValidateSingleRow(
|
|
4815
|
+
snapshot.rows[rowIndexes.get(key)!],
|
|
4816
|
+
patch,
|
|
4817
|
+
rowIndexes.get(key)!
|
|
4818
|
+
);
|
|
4819
|
+
if (merged.row.disabled && snapshot.selection?.selectedKeys.includes(key))
|
|
4820
|
+
fail(`patches[${unit.index}]`, 'cannot disable a selected row');
|
|
4821
|
+
pending.set(key, merged);
|
|
4822
|
+
} catch (error) {
|
|
4823
|
+
reject(key, unit.index, error);
|
|
4824
|
+
}
|
|
4825
|
+
}
|
|
4826
|
+
const accepted: RowPatch[] = [];
|
|
4827
|
+
let rows: RowModel[] | undefined;
|
|
4828
|
+
for (const [key, value] of pending) {
|
|
4829
|
+
const index = rowIndexes.get(key)!;
|
|
4830
|
+
const group = snapshot.rows[index].groupId;
|
|
4831
|
+
if (rejected.has(key) || (group && rejectedGroups.has(group))) continue;
|
|
4832
|
+
if (!rows) rows = snapshot.rows.slice();
|
|
4833
|
+
rows[index] = owned ? deepFreeze(value.row) : value.row;
|
|
4834
|
+
accepted.push(owned ? deepFreeze(value.patch) : value.patch);
|
|
3387
4835
|
}
|
|
4836
|
+
if (!rows) return { patches: EMPTY_ROW_PATCHES, snapshot, json: '[]' };
|
|
4837
|
+
const next = owned
|
|
4838
|
+
? Object.freeze({ ...snapshot, rows: Object.freeze(rows) })
|
|
4839
|
+
: { ...snapshot, rows };
|
|
4840
|
+
if (owned) ownedNativeSnapshots.set(next, rowIndexes);
|
|
4841
|
+
else validatedRowIndexes.set(next, rowIndexes);
|
|
4842
|
+
return { patches: accepted, snapshot: next, json: JSON.stringify(accepted) };
|
|
4843
|
+
}
|
|
3388
4844
|
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
const
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
ownedNativeSnapshots.set(nextSnapshot, rowIndexes);
|
|
3413
|
-
return {
|
|
3414
|
-
patches: acceptedPatches,
|
|
3415
|
-
snapshot: nextSnapshot,
|
|
3416
|
-
json: JSON.stringify(acceptedPatches),
|
|
3417
|
-
};
|
|
4845
|
+
const validatedRowIndexes = new WeakMap<
|
|
4846
|
+
NativeListSnapshot,
|
|
4847
|
+
ReadonlyMap<string, number>
|
|
4848
|
+
>();
|
|
4849
|
+
|
|
4850
|
+
export function applyOwnedNativePatches(
|
|
4851
|
+
snapshot: NativeListSnapshot,
|
|
4852
|
+
patches: readonly RowPatch[]
|
|
4853
|
+
) {
|
|
4854
|
+
const owned = ownedNativeSnapshots.has(snapshot)
|
|
4855
|
+
? snapshot
|
|
4856
|
+
: ownValidatedNativeSnapshot(validateSnapshot(capturePlainData(snapshot)));
|
|
4857
|
+
return applyValidatedRowPatches(owned, patches);
|
|
4858
|
+
}
|
|
4859
|
+
|
|
4860
|
+
export function applyRowPatches(
|
|
4861
|
+
snapshot: NativeListSnapshot,
|
|
4862
|
+
patches: readonly RowPatch[]
|
|
4863
|
+
): NativeListSnapshot {
|
|
4864
|
+
const baseline = ownedNativeSnapshots.has(snapshot)
|
|
4865
|
+
? snapshot
|
|
4866
|
+
: validateSnapshot(snapshot);
|
|
4867
|
+
return applyValidatedRowPatches(baseline, patches).snapshot;
|
|
3418
4868
|
}
|
|
3419
4869
|
|
|
3420
4870
|
export function serializeSnapshot(snapshot: NativeListSnapshot): string {
|