@onekeyfe/react-native-native-list 6.7.0-alpha.375 → 6.7.0-alpha.382
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/HybridNativeList.kt +14 -1
- package/android/src/main/java/com/onekey/nativelist/NativeListAccessoryStack.kt +240 -11
- package/android/src/main/java/com/onekey/nativelist/NativeListActivityDetailsView.kt +7 -4
- package/android/src/main/java/com/onekey/nativelist/NativeListActivityRenderer.kt +6 -3
- package/android/src/main/java/com/onekey/nativelist/NativeListButtonGroupRenderer.kt +102 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListContainerSlotAdapter.kt +12 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListDataRowRenderer.kt +87 -7
- package/android/src/main/java/com/onekey/nativelist/NativeListIdentityRenderer.kt +177 -1
- package/android/src/main/java/com/onekey/nativelist/NativeListLeadingVisual.kt +43 -14
- package/android/src/main/java/com/onekey/nativelist/NativeListMessageRenderer.kt +25 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +33 -2
- package/android/src/main/java/com/onekey/nativelist/NativeListPrimitives.kt +22 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRendererRegistry.kt +2 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRendererRowView.kt +2 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListResolvedText.kt +3 -2
- package/android/src/main/java/com/onekey/nativelist/NativeListSectionHeaderRenderer.kt +69 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListTextView.kt +55 -1
- package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +1912 -107
- package/android/src/test/java/com/margelo/nitro/nativelist/NativeListPressPolicyTest.kt +1 -0
- package/docs/ROW_TEMPLATES.md +60 -9
- package/docs/SPEC.md +450 -5
- package/docs/STYLE_SPEC.md +6 -0
- package/ios/HybridNativeList.swift +21 -2
- package/ios/NativeListAccessoryStack.swift +227 -39
- package/ios/NativeListActivityDetailsView.swift +18 -8
- package/ios/NativeListActivityRenderer.swift +8 -2
- package/ios/NativeListButtonGroupRenderer.swift +89 -0
- package/ios/NativeListDataRowRenderer.swift +155 -27
- package/ios/NativeListDesignAssets.swift +3 -0
- package/ios/NativeListIdentityRenderer.swift +166 -30
- package/ios/NativeListLeadingVisual.swift +49 -14
- package/ios/NativeListMessageRenderer.swift +34 -1
- package/ios/NativeListModels.swift +42 -2
- package/ios/NativeListRendererRegistry.swift +1 -0
- package/ios/NativeListResolvedText.swift +2 -1
- package/ios/NativeListRowHost.swift +14 -0
- package/ios/NativeListSectionHeaderRenderer.swift +88 -0
- package/ios/RNCNativeListView.swift +1013 -55
- package/ios/Resources/NativeListIcons.xcassets/onekey_crypto_coin_outline.imageset/Contents.json +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_crypto_coin_outline.imageset/icon.svg +4 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_gas_solid.imageset/Contents.json +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_gas_solid.imageset/icon.svg +3 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_refresh_ccw_outline.imageset/Contents.json +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_refresh_ccw_outline.imageset/icon.svg +3 -0
- package/ios/tests/NativeListActivityPresentationTests.swift +56 -0
- package/ios/tests/NativeListLifecycleTests.swift +2720 -3
- package/lib/module/NativeList.js +175 -28
- package/lib/module/NativeList.web.js +129 -30
- package/lib/module/index.js +1 -1
- package/lib/module/models.js +1 -0
- package/lib/module/selection.js +1 -1
- package/lib/module/validation.js +421 -8
- package/lib/module/web/NativeListWebEngine.js +507 -19
- package/lib/module/web/templates/ActivityRowRenderer.js +8 -7
- package/lib/module/web/templates/ButtonGroupRowRenderer.js +47 -0
- package/lib/module/web/templates/DataRowRenderer.js +40 -5
- package/lib/module/web/templates/IdentityRowRenderer.js +90 -4
- package/lib/module/web/templates/MessageRowRenderer.js +18 -2
- package/lib/module/web/templates/RowContainerStyle.js +1 -0
- package/lib/module/web/templates/RowRendererRegistry.js +3 -0
- package/lib/module/web/templates/SectionHeaderRowRenderer.js +36 -2
- package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +26 -0
- package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +4 -0
- package/lib/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +10 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +20 -0
- package/lib/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +14 -0
- package/lib/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +12 -0
- package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +2 -0
- package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +43 -0
- package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +4 -0
- package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +4 -0
- package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +4 -0
- package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +6 -0
- package/lib/nitrogen/generated/shared/json/NativeListConfig.json +2 -0
- package/lib/typescript/src/NativeList.d.ts +1 -1
- package/lib/typescript/src/NativeList.nitro.d.ts +2 -0
- package/lib/typescript/src/NativeList.types.d.ts +18 -0
- package/lib/typescript/src/index.d.ts +2 -2
- package/lib/typescript/src/models.d.ts +105 -9
- package/lib/typescript/src/validation.d.ts +12 -0
- package/lib/typescript/src/web/NativeListWebEngine.d.ts +32 -0
- package/lib/typescript/src/web/templates/ButtonGroupRowRenderer.d.ts +14 -0
- package/lib/typescript/src/web/templates/IdentityRowRenderer.d.ts +1 -1
- package/lib/typescript/src/web/templates/MessageRowRenderer.d.ts +9 -3
- package/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +26 -0
- package/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +4 -0
- package/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +10 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +20 -0
- package/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +14 -0
- package/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +12 -0
- package/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +2 -0
- package/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +43 -0
- package/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +4 -0
- package/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +4 -0
- package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +4 -0
- package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +6 -0
- package/nitrogen/generated/shared/json/NativeListConfig.json +2 -0
- package/package.json +3 -3
- package/react-native-native-list.podspec +1 -0
- package/src/NativeList.nitro.ts +2 -0
- package/src/NativeList.tsx +313 -44
- package/src/NativeList.types.ts +22 -0
- package/src/NativeList.web.tsx +204 -48
- package/src/index.ts +2 -0
- package/src/models.ts +117 -3
- package/src/selection.ts +1 -0
- package/src/validation.ts +852 -6
- package/src/web/NativeListWebEngine.ts +803 -20
- package/src/web/templates/ActivityRowRenderer.ts +14 -6
- package/src/web/templates/ButtonGroupRowRenderer.ts +58 -0
- package/src/web/templates/DataRowRenderer.ts +93 -20
- package/src/web/templates/IdentityRowRenderer.ts +150 -5
- package/src/web/templates/MessageRowRenderer.ts +23 -7
- package/src/web/templates/RowContainerStyle.ts +2 -0
- package/src/web/templates/RowRendererRegistry.ts +11 -1
- package/src/web/templates/SectionHeaderRowRenderer.ts +47 -4
package/src/validation.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ROW_BOX_STYLE_KEYS_BY_TYPE } from './models';
|
|
2
2
|
import type {
|
|
3
3
|
ActivityRow,
|
|
4
|
+
ButtonGroupRow,
|
|
4
5
|
IdentityRow,
|
|
5
6
|
ImageSource,
|
|
6
7
|
LeadingVisual,
|
|
@@ -13,6 +14,7 @@ import type {
|
|
|
13
14
|
RowBoxStyle,
|
|
14
15
|
RowModel,
|
|
15
16
|
RowPatch,
|
|
17
|
+
RowTextHelp,
|
|
16
18
|
WalletGroupRow,
|
|
17
19
|
WebMarketTableCell,
|
|
18
20
|
WebMarketTablePresentation,
|
|
@@ -220,12 +222,14 @@ const TEXT_STYLE_KEYS_BY_ROW_TYPE: Readonly<
|
|
|
220
222
|
metricCard: ['title', 'value', 'subtitle', 'trend'],
|
|
221
223
|
sectionHeader: ['title', 'subtitle', 'value'],
|
|
222
224
|
action: ['title', 'value'],
|
|
225
|
+
buttonGroup: [],
|
|
223
226
|
system: ['title', 'message', 'actionText'],
|
|
224
227
|
};
|
|
225
228
|
|
|
226
229
|
const EXTRA_STYLE_KEYS_BY_ROW_TYPE: Readonly<
|
|
227
230
|
Partial<Record<RowModel['type'], readonly string[]>>
|
|
228
231
|
> = {
|
|
232
|
+
identity: ['leadingContentMaxWidthRatio'],
|
|
229
233
|
market: [
|
|
230
234
|
'titleBadgeLayout',
|
|
231
235
|
'contentTrailingGap',
|
|
@@ -234,6 +238,7 @@ const EXTRA_STYLE_KEYS_BY_ROW_TYPE: Readonly<
|
|
|
234
238
|
'changeHeight',
|
|
235
239
|
'changeCornerRadius',
|
|
236
240
|
],
|
|
241
|
+
sectionHeader: ['valuePlacement', 'titleDecoration'],
|
|
237
242
|
};
|
|
238
243
|
|
|
239
244
|
function assertTextStyle(
|
|
@@ -492,6 +497,33 @@ function assertRowStyle(row: RowModel, path: string): void {
|
|
|
492
497
|
// Market keeps its own richer assertions, driven from assertMarketRow.
|
|
493
498
|
if (row.type === 'market') return;
|
|
494
499
|
assertBoxStyle(style as RowBoxStyle, `${path}.style`);
|
|
500
|
+
if (row.type === 'identity') {
|
|
501
|
+
const ratio = (style as IdentityRow['style'])?.leadingContentMaxWidthRatio;
|
|
502
|
+
if (
|
|
503
|
+
ratio !== undefined &&
|
|
504
|
+
(!Number.isFinite(ratio) || ratio <= 0 || ratio > 1)
|
|
505
|
+
)
|
|
506
|
+
fail(
|
|
507
|
+
`${path}.style.leadingContentMaxWidthRatio`,
|
|
508
|
+
'must be within (0, 1]'
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
if (row.type === 'sectionHeader') {
|
|
512
|
+
const sectionStyle = style as Extract<
|
|
513
|
+
RowModel,
|
|
514
|
+
{ type: 'sectionHeader' }
|
|
515
|
+
>['style'];
|
|
516
|
+
if (
|
|
517
|
+
sectionStyle?.valuePlacement !== undefined &&
|
|
518
|
+
sectionStyle.valuePlacement !== 'afterTitle'
|
|
519
|
+
)
|
|
520
|
+
fail(`${path}.style.valuePlacement`, 'must be afterTitle');
|
|
521
|
+
if (
|
|
522
|
+
sectionStyle?.titleDecoration !== undefined &&
|
|
523
|
+
!['none', 'dashed'].includes(sectionStyle.titleDecoration)
|
|
524
|
+
)
|
|
525
|
+
fail(`${path}.style.titleDecoration`, 'must be none or dashed');
|
|
526
|
+
}
|
|
495
527
|
const slots = style as Record<string, NativeListTextStyle | undefined>;
|
|
496
528
|
textKeys.forEach((key) =>
|
|
497
529
|
assertTextStyle(slots[key], `${path}.style.${key}`)
|
|
@@ -1049,6 +1081,55 @@ function assertTrailingAccessories(
|
|
|
1049
1081
|
): void {
|
|
1050
1082
|
accessories?.forEach((accessory, index) => {
|
|
1051
1083
|
const accessoryPath = `${path}[${index}]`;
|
|
1084
|
+
if (accessory.kind === 'button') {
|
|
1085
|
+
assertUnknownKeys(
|
|
1086
|
+
accessory as Record<string, unknown>,
|
|
1087
|
+
[
|
|
1088
|
+
'kind',
|
|
1089
|
+
'label',
|
|
1090
|
+
'actionKey',
|
|
1091
|
+
'disabled',
|
|
1092
|
+
'testID',
|
|
1093
|
+
'accessibilityLabel',
|
|
1094
|
+
'textColor',
|
|
1095
|
+
'backgroundColor',
|
|
1096
|
+
'pressedBackgroundColor',
|
|
1097
|
+
'hoverBackgroundColor',
|
|
1098
|
+
'focusRingColor',
|
|
1099
|
+
],
|
|
1100
|
+
accessoryPath,
|
|
1101
|
+
'button key'
|
|
1102
|
+
);
|
|
1103
|
+
if (typeof accessory.label !== 'string' || !accessory.label.trim())
|
|
1104
|
+
fail(`${accessoryPath}.label`, 'must be non-blank');
|
|
1105
|
+
assertKey(accessory.actionKey, `${accessoryPath}.actionKey`);
|
|
1106
|
+
if (
|
|
1107
|
+
accessory.disabled !== undefined &&
|
|
1108
|
+
typeof accessory.disabled !== 'boolean'
|
|
1109
|
+
)
|
|
1110
|
+
fail(`${accessoryPath}.disabled`, 'must be boolean');
|
|
1111
|
+
for (const field of [
|
|
1112
|
+
'textColor',
|
|
1113
|
+
'backgroundColor',
|
|
1114
|
+
'pressedBackgroundColor',
|
|
1115
|
+
'hoverBackgroundColor',
|
|
1116
|
+
'focusRingColor',
|
|
1117
|
+
] as const) {
|
|
1118
|
+
const value = accessory[field];
|
|
1119
|
+
if (
|
|
1120
|
+
value !== undefined &&
|
|
1121
|
+
(typeof value !== 'string' ||
|
|
1122
|
+
!/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(value))
|
|
1123
|
+
)
|
|
1124
|
+
fail(`${accessoryPath}.${field}`, 'must be #RRGGBB or #RRGGBBAA');
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
if (
|
|
1128
|
+
accessory.kind === 'icon' &&
|
|
1129
|
+
accessory.buttonStyle !== undefined &&
|
|
1130
|
+
accessory.buttonStyle !== 'tertiary'
|
|
1131
|
+
)
|
|
1132
|
+
fail(`${accessoryPath}.buttonStyle`, 'must be tertiary');
|
|
1052
1133
|
if (
|
|
1053
1134
|
accessory.kind === 'progress' &&
|
|
1054
1135
|
(accessory.value < 0 || accessory.value > 1)
|
|
@@ -1056,12 +1137,160 @@ function assertTrailingAccessories(
|
|
|
1056
1137
|
fail(`${accessoryPath}.value`, 'must be within 0...1');
|
|
1057
1138
|
}
|
|
1058
1139
|
if (accessory.kind === 'valuePair') {
|
|
1140
|
+
if (
|
|
1141
|
+
accessory.primaryFontWeight !== undefined &&
|
|
1142
|
+
accessory.primaryFontWeight !== 'medium'
|
|
1143
|
+
)
|
|
1144
|
+
fail(
|
|
1145
|
+
`${accessoryPath}.primaryFontWeight`,
|
|
1146
|
+
'must be medium when provided'
|
|
1147
|
+
);
|
|
1148
|
+
assertTextHelp(accessory.primaryHelp, `${accessoryPath}.primaryHelp`);
|
|
1059
1149
|
assertTextTone(accessory.primaryTone, `${accessoryPath}.primaryTone`);
|
|
1060
1150
|
assertTextTone(accessory.secondaryTone, `${accessoryPath}.secondaryTone`);
|
|
1151
|
+
for (const field of [
|
|
1152
|
+
'primaryTextSegments',
|
|
1153
|
+
'secondaryTextSegments',
|
|
1154
|
+
] as const) {
|
|
1155
|
+
const segments = accessory[field];
|
|
1156
|
+
if (segments === undefined) continue;
|
|
1157
|
+
const segmentPath = `${accessoryPath}.${field}`;
|
|
1158
|
+
if (!Array.isArray(segments) || segments.length > 64)
|
|
1159
|
+
fail(segmentPath, 'must be an array of at most 64 segments');
|
|
1160
|
+
let textLength = 0;
|
|
1161
|
+
segments.forEach((segment, segmentIndex) => {
|
|
1162
|
+
const path = `${segmentPath}[${segmentIndex}]`;
|
|
1163
|
+
const entry = protocolObject(segment, path);
|
|
1164
|
+
assertUnknownKeys(entry, ['text', 'style'], path, 'segment key');
|
|
1165
|
+
if (
|
|
1166
|
+
typeof entry.text !== 'string' ||
|
|
1167
|
+
entry.text.length > MAX_TEXT_LENGTH
|
|
1168
|
+
)
|
|
1169
|
+
fail(
|
|
1170
|
+
`${path}.text`,
|
|
1171
|
+
`must be a string no longer than ${MAX_TEXT_LENGTH} characters`
|
|
1172
|
+
);
|
|
1173
|
+
if (entry.style !== undefined && entry.style !== 'subscript')
|
|
1174
|
+
fail(`${path}.style`, 'must be subscript when provided');
|
|
1175
|
+
textLength += entry.text.length;
|
|
1176
|
+
});
|
|
1177
|
+
if (textLength > MAX_TEXT_LENGTH)
|
|
1178
|
+
fail(
|
|
1179
|
+
segmentPath,
|
|
1180
|
+
`total text must be at most ${MAX_TEXT_LENGTH} characters`
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1061
1183
|
}
|
|
1062
1184
|
});
|
|
1063
1185
|
}
|
|
1064
1186
|
|
|
1187
|
+
function assertTextHelp(help: RowTextHelp | undefined, path: string): void {
|
|
1188
|
+
if (help === undefined) return;
|
|
1189
|
+
const object = protocolObject(help, path);
|
|
1190
|
+
assertUnknownKeys(
|
|
1191
|
+
object,
|
|
1192
|
+
['actionKey', 'presentation', 'underline'],
|
|
1193
|
+
path,
|
|
1194
|
+
'help key'
|
|
1195
|
+
);
|
|
1196
|
+
assertKey(help.actionKey, `${path}.actionKey`);
|
|
1197
|
+
if (!['tooltip', 'popover'].includes(help.presentation))
|
|
1198
|
+
fail(`${path}.presentation`, 'must be tooltip or popover');
|
|
1199
|
+
if (help.underline !== undefined && help.underline !== 'dashed')
|
|
1200
|
+
fail(`${path}.underline`, 'must be dashed');
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
function assertButtonGroup(
|
|
1204
|
+
row: Pick<ButtonGroupRow, 'buttons'>,
|
|
1205
|
+
path: string
|
|
1206
|
+
): void {
|
|
1207
|
+
if (!Array.isArray(row.buttons) || row.buttons.length === 0)
|
|
1208
|
+
fail(`${path}.buttons`, 'must contain at least one button');
|
|
1209
|
+
const keys = new Set<string>();
|
|
1210
|
+
row.buttons.forEach((button, index) => {
|
|
1211
|
+
const itemPath = `${path}.buttons[${index}]`;
|
|
1212
|
+
const object = protocolObject(button, itemPath);
|
|
1213
|
+
assertUnknownKeys(
|
|
1214
|
+
object,
|
|
1215
|
+
[
|
|
1216
|
+
'key',
|
|
1217
|
+
'label',
|
|
1218
|
+
'actionKey',
|
|
1219
|
+
'disabled',
|
|
1220
|
+
'loading',
|
|
1221
|
+
'testID',
|
|
1222
|
+
'accessibilityLabel',
|
|
1223
|
+
],
|
|
1224
|
+
itemPath,
|
|
1225
|
+
'button key'
|
|
1226
|
+
);
|
|
1227
|
+
assertKey(button.key, `${itemPath}.key`);
|
|
1228
|
+
if (keys.has(button.key)) fail(`${itemPath}.key`, 'duplicate button key');
|
|
1229
|
+
keys.add(button.key);
|
|
1230
|
+
if (
|
|
1231
|
+
typeof button.label !== 'string' ||
|
|
1232
|
+
!button.label.trim() ||
|
|
1233
|
+
button.label.length > MAX_TEXT_LENGTH
|
|
1234
|
+
)
|
|
1235
|
+
fail(`${itemPath}.label`, 'must be non-blank bounded text');
|
|
1236
|
+
assertKey(button.actionKey, `${itemPath}.actionKey`);
|
|
1237
|
+
for (const field of ['disabled', 'loading'] as const) {
|
|
1238
|
+
if (button[field] !== undefined && typeof button[field] !== 'boolean')
|
|
1239
|
+
fail(`${itemPath}.${field}`, 'must be boolean');
|
|
1240
|
+
}
|
|
1241
|
+
for (const field of ['testID', 'accessibilityLabel'] as const) {
|
|
1242
|
+
if (button[field] !== undefined && typeof button[field] !== 'string')
|
|
1243
|
+
fail(`${itemPath}.${field}`, 'must be a string');
|
|
1244
|
+
}
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function assertIdentityExtras(
|
|
1249
|
+
row: Pick<
|
|
1250
|
+
IdentityRow,
|
|
1251
|
+
'titleIcon' | 'secondaryTitleIcon' | 'trailing' | 'style'
|
|
1252
|
+
>,
|
|
1253
|
+
path: string
|
|
1254
|
+
): void {
|
|
1255
|
+
for (const field of ['titleIcon', 'secondaryTitleIcon'] as const) {
|
|
1256
|
+
const titleIcon = row[field];
|
|
1257
|
+
if (titleIcon === undefined) continue;
|
|
1258
|
+
const iconPath = `${path}.${field}`;
|
|
1259
|
+
const icon = protocolObject(titleIcon, iconPath);
|
|
1260
|
+
assertUnknownKeys(
|
|
1261
|
+
icon,
|
|
1262
|
+
['name', 'tintColor', 'accessibilityLabel', 'hoverActionKey'],
|
|
1263
|
+
iconPath,
|
|
1264
|
+
'title icon key'
|
|
1265
|
+
);
|
|
1266
|
+
if (typeof titleIcon.name !== 'string' || !titleIcon.name.trim())
|
|
1267
|
+
fail(`${iconPath}.name`, 'must be non-blank');
|
|
1268
|
+
if (
|
|
1269
|
+
titleIcon.tintColor !== undefined &&
|
|
1270
|
+
(typeof titleIcon.tintColor !== 'string' ||
|
|
1271
|
+
!/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(titleIcon.tintColor))
|
|
1272
|
+
)
|
|
1273
|
+
fail(`${iconPath}.tintColor`, 'must be #RRGGBB or #RRGGBBAA');
|
|
1274
|
+
if (
|
|
1275
|
+
titleIcon.accessibilityLabel !== undefined &&
|
|
1276
|
+
typeof titleIcon.accessibilityLabel !== 'string'
|
|
1277
|
+
)
|
|
1278
|
+
fail(`${iconPath}.accessibilityLabel`, 'must be a string');
|
|
1279
|
+
if (titleIcon.hoverActionKey !== undefined)
|
|
1280
|
+
assertKey(titleIcon.hoverActionKey, `${iconPath}.hoverActionKey`);
|
|
1281
|
+
}
|
|
1282
|
+
const ratio = row.style?.leadingContentMaxWidthRatio;
|
|
1283
|
+
if (
|
|
1284
|
+
ratio !== undefined &&
|
|
1285
|
+
(typeof ratio !== 'number' ||
|
|
1286
|
+
!Number.isFinite(ratio) ||
|
|
1287
|
+
ratio <= 0 ||
|
|
1288
|
+
ratio > 1)
|
|
1289
|
+
)
|
|
1290
|
+
fail(`${path}.style.leadingContentMaxWidthRatio`, 'must be within (0, 1]');
|
|
1291
|
+
assertTrailingAccessories(row.trailing, `${path}.trailing`);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1065
1294
|
function assertImage(image: ImageSource | undefined, path: string): void {
|
|
1066
1295
|
if (!image) return;
|
|
1067
1296
|
if (!image.uri.trim()) fail(`${path}.uri`, 'must not be empty');
|
|
@@ -1132,6 +1361,24 @@ function assertLeadingVisual(
|
|
|
1132
1361
|
assertText(visual.cornerIcon?.name, `${path}.cornerIcon.name`);
|
|
1133
1362
|
// OneKey patch: validate optional selector overlays at the JSON boundary.
|
|
1134
1363
|
assertText(visual.fallbackIcon?.name, `${path}.fallbackIcon.name`);
|
|
1364
|
+
if (
|
|
1365
|
+
visual.fallbackTextColor !== undefined &&
|
|
1366
|
+
(typeof visual.fallbackTextColor !== 'string' ||
|
|
1367
|
+
!/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(visual.fallbackTextColor))
|
|
1368
|
+
)
|
|
1369
|
+
fail(`${path}.fallbackTextColor`, 'must be a hex color');
|
|
1370
|
+
if (
|
|
1371
|
+
visual.fallbackIcon?.size !== undefined &&
|
|
1372
|
+
(!Number.isFinite(visual.fallbackIcon.size) ||
|
|
1373
|
+
visual.fallbackIcon.size <= 0 ||
|
|
1374
|
+
visual.fallbackIcon.size > 40)
|
|
1375
|
+
)
|
|
1376
|
+
fail(`${path}.fallbackIcon.size`, 'must be within (0, 40]');
|
|
1377
|
+
if (
|
|
1378
|
+
visual.fallbackIcon?.backgroundColor !== undefined &&
|
|
1379
|
+
!/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(visual.fallbackIcon.backgroundColor)
|
|
1380
|
+
)
|
|
1381
|
+
fail(`${path}.fallbackIcon.backgroundColor`, 'must be a hex color');
|
|
1135
1382
|
if ((visual.overlays?.length ?? 0) > 2)
|
|
1136
1383
|
fail(`${path}.overlays`, 'supports at most two overlays');
|
|
1137
1384
|
visual.overlays?.forEach((overlay, index) => {
|
|
@@ -1191,7 +1438,34 @@ function assertLeadingVisual(
|
|
|
1191
1438
|
'must fit inside the overlay'
|
|
1192
1439
|
);
|
|
1193
1440
|
assertImage(overlay.image, `${path}.overlays[${index}].image`);
|
|
1441
|
+
if (overlay.fallbackIcon !== undefined && !overlay.image)
|
|
1442
|
+
fail(`${path}.overlays[${index}].fallbackIcon`, 'requires image');
|
|
1443
|
+
if (
|
|
1444
|
+
overlay.fallbackIcon !== undefined &&
|
|
1445
|
+
(typeof overlay.fallbackIcon.name !== 'string' ||
|
|
1446
|
+
!overlay.fallbackIcon.name.trim())
|
|
1447
|
+
)
|
|
1448
|
+
fail(
|
|
1449
|
+
`${path}.overlays[${index}].fallbackIcon.name`,
|
|
1450
|
+
'must be non-blank'
|
|
1451
|
+
);
|
|
1452
|
+
if (
|
|
1453
|
+
overlay.fallbackIcon?.tintColor !== undefined &&
|
|
1454
|
+
!/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(overlay.fallbackIcon.tintColor)
|
|
1455
|
+
)
|
|
1456
|
+
fail(
|
|
1457
|
+
`${path}.overlays[${index}].fallbackIcon.tintColor`,
|
|
1458
|
+
'must be a hex color'
|
|
1459
|
+
);
|
|
1194
1460
|
assertText(overlay.text, `${path}.overlays[${index}].text`);
|
|
1461
|
+
if (
|
|
1462
|
+
overlay.fontWeight !== undefined &&
|
|
1463
|
+
!['regular', 'medium', 'semibold', 'bold'].includes(overlay.fontWeight)
|
|
1464
|
+
)
|
|
1465
|
+
fail(
|
|
1466
|
+
`${path}.overlays[${index}].fontWeight`,
|
|
1467
|
+
'must be regular, medium, semibold, or bold'
|
|
1468
|
+
);
|
|
1195
1469
|
});
|
|
1196
1470
|
if (visual.kind === 'token') {
|
|
1197
1471
|
assertImage(visual.networkImage, `${path}.networkImage`);
|
|
@@ -1223,6 +1497,8 @@ function assertVisual(row: RowModel, path: string): void {
|
|
|
1223
1497
|
}
|
|
1224
1498
|
|
|
1225
1499
|
function assertActivity(row: Partial<ActivityRow>, path: string): void {
|
|
1500
|
+
if (row.hideLeading !== undefined && typeof row.hideLeading !== 'boolean')
|
|
1501
|
+
fail(`${path}.hideLeading`, 'must be boolean');
|
|
1226
1502
|
assertText(row.title, `${path}.title`);
|
|
1227
1503
|
assertText(row.description, `${path}.description`);
|
|
1228
1504
|
if (
|
|
@@ -1387,6 +1663,18 @@ function assertRow(
|
|
|
1387
1663
|
assertWalletGroup(row, path);
|
|
1388
1664
|
break;
|
|
1389
1665
|
case 'identity':
|
|
1666
|
+
if (row.contentHeight !== undefined && row.contentHeight !== 'auto') {
|
|
1667
|
+
fail(`${path}.contentHeight`, 'must be auto');
|
|
1668
|
+
}
|
|
1669
|
+
if (
|
|
1670
|
+
row.subtitleLines !== undefined &&
|
|
1671
|
+
![0, 1, 2].includes(row.subtitleLines)
|
|
1672
|
+
) {
|
|
1673
|
+
fail(`${path}.subtitleLines`, 'must be 0, 1, or 2');
|
|
1674
|
+
}
|
|
1675
|
+
if (row.longPressActionKey !== undefined) {
|
|
1676
|
+
assertKey(row.longPressActionKey, `${path}.longPressActionKey`);
|
|
1677
|
+
}
|
|
1390
1678
|
if (
|
|
1391
1679
|
row.presentation !== undefined &&
|
|
1392
1680
|
row.presentation !== 'walletSidebar' &&
|
|
@@ -1399,6 +1687,27 @@ function assertRow(
|
|
|
1399
1687
|
);
|
|
1400
1688
|
}
|
|
1401
1689
|
assertText(row.title, `${path}.title`);
|
|
1690
|
+
if ((row.titleSuffixes?.length ?? 0) > 3)
|
|
1691
|
+
fail(`${path}.titleSuffixes`, 'supports at most 3 suffixes');
|
|
1692
|
+
row.titleSuffixes?.forEach((suffix, index) => {
|
|
1693
|
+
assertText(suffix.text, `${path}.titleSuffixes[${index}].text`);
|
|
1694
|
+
if (!['secondary', 'disabled'].includes(suffix.tone))
|
|
1695
|
+
fail(
|
|
1696
|
+
`${path}.titleSuffixes[${index}].tone`,
|
|
1697
|
+
'must be secondary or disabled'
|
|
1698
|
+
);
|
|
1699
|
+
if (
|
|
1700
|
+
suffix.maxWidth !== undefined &&
|
|
1701
|
+
(!Number.isFinite(suffix.maxWidth) ||
|
|
1702
|
+
suffix.maxWidth < 0 ||
|
|
1703
|
+
suffix.maxWidth > 4096)
|
|
1704
|
+
)
|
|
1705
|
+
fail(
|
|
1706
|
+
`${path}.titleSuffixes[${index}].maxWidth`,
|
|
1707
|
+
'must be finite within 0...4096'
|
|
1708
|
+
);
|
|
1709
|
+
});
|
|
1710
|
+
assertIdentityExtras(row, path);
|
|
1402
1711
|
assertText(row.subtitle, `${path}.subtitle`);
|
|
1403
1712
|
// OneKey patch: selector text segments truncate independently.
|
|
1404
1713
|
row.subtitleSegments?.forEach((segment, segmentIndex) => {
|
|
@@ -1466,10 +1775,16 @@ function assertRow(
|
|
|
1466
1775
|
assertActivity(row, path);
|
|
1467
1776
|
break;
|
|
1468
1777
|
case 'message':
|
|
1778
|
+
if (
|
|
1779
|
+
row.presentation !== undefined &&
|
|
1780
|
+
row.presentation !== 'inlineWarning'
|
|
1781
|
+
) {
|
|
1782
|
+
fail(`${path}.presentation`, 'must be inlineWarning');
|
|
1783
|
+
}
|
|
1469
1784
|
assertText(row.title, `${path}.title`);
|
|
1470
1785
|
assertText(row.body, `${path}.body`);
|
|
1471
|
-
if (![1, 2, 3].includes(row.bodyLines ?? 3)) {
|
|
1472
|
-
fail(`${path}.bodyLines`, 'must be 1, 2, or 3');
|
|
1786
|
+
if (![0, 1, 2, 3].includes(row.bodyLines ?? 3)) {
|
|
1787
|
+
fail(`${path}.bodyLines`, 'must be 0, 1, 2, or 3');
|
|
1473
1788
|
}
|
|
1474
1789
|
assertImage(row.thumbnail, `${path}.thumbnail`);
|
|
1475
1790
|
break;
|
|
@@ -1480,6 +1795,23 @@ function assertRow(
|
|
|
1480
1795
|
row.columns.forEach((column, columnIndex) => {
|
|
1481
1796
|
assertKey(column.key, `${path}.columns[${columnIndex}].key`);
|
|
1482
1797
|
assertText(column.text, `${path}.columns[${columnIndex}].text`);
|
|
1798
|
+
assertIdentityExtras(column, `${path}.columns[${columnIndex}]`);
|
|
1799
|
+
for (const field of [
|
|
1800
|
+
'textSegments',
|
|
1801
|
+
'secondaryTextSegments',
|
|
1802
|
+
] as const) {
|
|
1803
|
+
column[field]?.forEach((segment, segmentIndex) => {
|
|
1804
|
+
assertText(
|
|
1805
|
+
segment.text,
|
|
1806
|
+
`${path}.columns[${columnIndex}].${field}[${segmentIndex}].text`
|
|
1807
|
+
);
|
|
1808
|
+
if (segment.style !== undefined && segment.style !== 'subscript')
|
|
1809
|
+
fail(
|
|
1810
|
+
`${path}.columns[${columnIndex}].${field}[${segmentIndex}].style`,
|
|
1811
|
+
'must be subscript'
|
|
1812
|
+
);
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1483
1815
|
assertText(
|
|
1484
1816
|
column.secondaryLeadingText,
|
|
1485
1817
|
`${path}.columns[${columnIndex}].secondaryLeadingText`
|
|
@@ -1496,6 +1828,13 @@ function assertRow(
|
|
|
1496
1828
|
if ((row.badges?.length ?? 0) > MAX_BADGES) {
|
|
1497
1829
|
fail(`${path}.badges`, `supports at most ${MAX_BADGES} badges`);
|
|
1498
1830
|
}
|
|
1831
|
+
if ((row.trailing?.length ?? 0) > MAX_TRAILING_ACCESSORIES) {
|
|
1832
|
+
fail(
|
|
1833
|
+
`${path}.trailing`,
|
|
1834
|
+
`supports at most ${MAX_TRAILING_ACCESSORIES} accessories`
|
|
1835
|
+
);
|
|
1836
|
+
}
|
|
1837
|
+
assertTrailingAccessories(row.trailing, `${path}.trailing`);
|
|
1499
1838
|
break;
|
|
1500
1839
|
case 'market':
|
|
1501
1840
|
assertMarketRow(row, path);
|
|
@@ -1572,6 +1911,11 @@ function assertRow(
|
|
|
1572
1911
|
assertText(row.title, `${path}.title`);
|
|
1573
1912
|
assertText(row.subtitle, `${path}.subtitle`);
|
|
1574
1913
|
assertText(row.value, `${path}.value`);
|
|
1914
|
+
if (row.leadingBadge) {
|
|
1915
|
+
assertKey(row.leadingBadge.key, `${path}.leadingBadge.key`);
|
|
1916
|
+
assertText(row.leadingBadge.text, `${path}.leadingBadge.text`);
|
|
1917
|
+
}
|
|
1918
|
+
assertTextHelp(row.valueHelp, `${path}.valueHelp`);
|
|
1575
1919
|
if (row.valueActionKey !== undefined) {
|
|
1576
1920
|
assertKey(row.valueActionKey, `${path}.valueActionKey`);
|
|
1577
1921
|
}
|
|
@@ -1604,6 +1948,9 @@ function assertRow(
|
|
|
1604
1948
|
}
|
|
1605
1949
|
assertTrailingAccessories(row.trailing, `${path}.trailing`);
|
|
1606
1950
|
break;
|
|
1951
|
+
case 'buttonGroup':
|
|
1952
|
+
assertButtonGroup(row, path);
|
|
1953
|
+
break;
|
|
1607
1954
|
case 'system':
|
|
1608
1955
|
const presentation = 'presentation' in row ? row.presentation : undefined;
|
|
1609
1956
|
if (presentation !== undefined && presentation !== 'market') {
|
|
@@ -1712,6 +2059,36 @@ function assertMediaPreview(value: unknown, path: string) {
|
|
|
1712
2059
|
function assertSnapshotContract(
|
|
1713
2060
|
snapshot: NativeListSnapshot
|
|
1714
2061
|
): NativeListSnapshot {
|
|
2062
|
+
if (snapshot.disclosure !== undefined) {
|
|
2063
|
+
if (typeof snapshot.disclosure.expanded !== 'boolean')
|
|
2064
|
+
fail('snapshot.disclosure.expanded', 'must be a boolean');
|
|
2065
|
+
const inset = snapshot.disclosure.expandedTrailingInset;
|
|
2066
|
+
if (inset !== undefined && (!Number.isFinite(inset) || inset < 0))
|
|
2067
|
+
fail(
|
|
2068
|
+
'snapshot.disclosure.expandedTrailingInset',
|
|
2069
|
+
'must be finite and nonnegative'
|
|
2070
|
+
);
|
|
2071
|
+
}
|
|
2072
|
+
if (snapshot.structuralTransition) {
|
|
2073
|
+
assertKey(
|
|
2074
|
+
snapshot.structuralTransition.id,
|
|
2075
|
+
'snapshot.structuralTransition.id'
|
|
2076
|
+
);
|
|
2077
|
+
if (snapshot.structuralTransition.anchorKey !== undefined)
|
|
2078
|
+
assertKey(
|
|
2079
|
+
snapshot.structuralTransition.anchorKey,
|
|
2080
|
+
'snapshot.structuralTransition.anchorKey'
|
|
2081
|
+
);
|
|
2082
|
+
const duration = snapshot.structuralTransition.durationMs;
|
|
2083
|
+
if (
|
|
2084
|
+
duration !== undefined &&
|
|
2085
|
+
(!Number.isFinite(duration) || duration < 0 || duration > 2000)
|
|
2086
|
+
)
|
|
2087
|
+
fail(
|
|
2088
|
+
'snapshot.structuralTransition.durationMs',
|
|
2089
|
+
'must be finite within 0...2000'
|
|
2090
|
+
);
|
|
2091
|
+
}
|
|
1715
2092
|
if (
|
|
1716
2093
|
snapshot.capabilities?.refreshTriggerDistance !== undefined &&
|
|
1717
2094
|
(!Number.isFinite(snapshot.capabilities.refreshTriggerDistance) ||
|
|
@@ -1727,6 +2104,18 @@ function assertSnapshotContract(
|
|
|
1727
2104
|
snapshot.theme?.imageFallbackBackground,
|
|
1728
2105
|
'theme.imageFallbackBackground'
|
|
1729
2106
|
);
|
|
2107
|
+
if (
|
|
2108
|
+
snapshot.theme?.successBackground !== undefined &&
|
|
2109
|
+
(typeof snapshot.theme.successBackground !== 'string' ||
|
|
2110
|
+
!/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(snapshot.theme.successBackground))
|
|
2111
|
+
)
|
|
2112
|
+
fail('theme.successBackground', 'must be a hex color');
|
|
2113
|
+
if (
|
|
2114
|
+
snapshot.theme?.rowHoverBackground !== undefined &&
|
|
2115
|
+
(typeof snapshot.theme.rowHoverBackground !== 'string' ||
|
|
2116
|
+
!/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(snapshot.theme.rowHoverBackground))
|
|
2117
|
+
)
|
|
2118
|
+
fail('theme.rowHoverBackground', 'must be a hex color');
|
|
1730
2119
|
if (snapshot.schemaVersion !== 1) fail('snapshot.schemaVersion', 'must be 1');
|
|
1731
2120
|
if (!Number.isSafeInteger(snapshot.generation) || snapshot.generation < 0) {
|
|
1732
2121
|
fail('snapshot.generation', 'must be a non-negative safe integer');
|
|
@@ -1874,6 +2263,7 @@ function protocolKey(value: unknown, path: string): void {
|
|
|
1874
2263
|
|
|
1875
2264
|
const PROTOCOL_ARRAY_FIELDS = new Set([
|
|
1876
2265
|
'columns',
|
|
2266
|
+
'titleSuffixes',
|
|
1877
2267
|
'cells',
|
|
1878
2268
|
'points',
|
|
1879
2269
|
'images',
|
|
@@ -1881,6 +2271,7 @@ const PROTOCOL_ARRAY_FIELDS = new Set([
|
|
|
1881
2271
|
'badges',
|
|
1882
2272
|
'trailing',
|
|
1883
2273
|
'footerActions',
|
|
2274
|
+
'buttons',
|
|
1884
2275
|
'amounts',
|
|
1885
2276
|
'metrics',
|
|
1886
2277
|
'children',
|
|
@@ -1899,6 +2290,7 @@ const PROTOCOL_ARRAY_FIELDS = new Set([
|
|
|
1899
2290
|
'primaryTextSegments',
|
|
1900
2291
|
]);
|
|
1901
2292
|
const PROTOCOL_NUMBER_FIELDS = new Set([
|
|
2293
|
+
'leadingContentMaxWidthRatio',
|
|
1902
2294
|
'height',
|
|
1903
2295
|
'width',
|
|
1904
2296
|
'fontSize',
|
|
@@ -1961,6 +2353,32 @@ function assertProtocolFields(
|
|
|
1961
2353
|
return;
|
|
1962
2354
|
}
|
|
1963
2355
|
const object = value as Record<string, unknown>;
|
|
2356
|
+
if (parentField === 'fallbackIcon') {
|
|
2357
|
+
protocolKey(object.name, `${path}.name`);
|
|
2358
|
+
if (
|
|
2359
|
+
object.size !== undefined &&
|
|
2360
|
+
(typeof object.size !== 'number' ||
|
|
2361
|
+
!Number.isFinite(object.size) ||
|
|
2362
|
+
object.size <= 0 ||
|
|
2363
|
+
object.size > 40)
|
|
2364
|
+
)
|
|
2365
|
+
fail(`${path}.size`, 'must be within (0, 40]');
|
|
2366
|
+
for (const field of ['tintColor', 'backgroundColor'] as const) {
|
|
2367
|
+
const color = object[field];
|
|
2368
|
+
if (
|
|
2369
|
+
color !== undefined &&
|
|
2370
|
+
(typeof color !== 'string' ||
|
|
2371
|
+
!/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(color))
|
|
2372
|
+
)
|
|
2373
|
+
fail(`${path}.${field}`, 'must be a hex color');
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
if (
|
|
2377
|
+
parentField === 'overlays' &&
|
|
2378
|
+
object.fallbackIcon !== undefined &&
|
|
2379
|
+
!object.image
|
|
2380
|
+
)
|
|
2381
|
+
fail(`${path}.fallbackIcon`, 'requires image');
|
|
1964
2382
|
if (
|
|
1965
2383
|
!path.includes('.style.') &&
|
|
1966
2384
|
['image', 'networkImage', 'thumbnail', 'images', 'hoverImages'].includes(
|
|
@@ -1987,6 +2405,16 @@ function assertProtocolFields(
|
|
|
1987
2405
|
!['offsetX', 'offsetY'].includes(field)
|
|
1988
2406
|
)
|
|
1989
2407
|
fail(fieldPath, 'must be non-negative');
|
|
2408
|
+
if (
|
|
2409
|
+
parentField === 'overlays' &&
|
|
2410
|
+
field === 'fontWeight' &&
|
|
2411
|
+
!['regular', 'medium', 'semibold', 'bold'].includes(item as string)
|
|
2412
|
+
)
|
|
2413
|
+
fail(fieldPath, 'must be regular, medium, semibold, or bold');
|
|
2414
|
+
if (field === 'buttonStyle' && item !== 'tertiary')
|
|
2415
|
+
fail(fieldPath, 'must be tertiary');
|
|
2416
|
+
if (field === 'hideLeading' && typeof item !== 'boolean')
|
|
2417
|
+
fail(fieldPath, 'must be boolean');
|
|
1990
2418
|
if (
|
|
1991
2419
|
field === 'key' ||
|
|
1992
2420
|
field === 'sectionKey' ||
|
|
@@ -2067,6 +2495,27 @@ function assertRowProtocol(row: RowModel, path: string): void {
|
|
|
2067
2495
|
if (row.height !== undefined && row.height < 0)
|
|
2068
2496
|
fail(`${path}.height`, 'must be non-negative');
|
|
2069
2497
|
if (row.type === 'identity') {
|
|
2498
|
+
assertIdentityExtras(row, path);
|
|
2499
|
+
if (row.titleSuffixes !== undefined && !Array.isArray(row.titleSuffixes))
|
|
2500
|
+
fail(`${path}.titleSuffixes`, 'must be an array');
|
|
2501
|
+
row.titleSuffixes?.forEach((suffix, index) => {
|
|
2502
|
+
protocolObject(suffix, `${path}.titleSuffixes[${index}]`);
|
|
2503
|
+
if (
|
|
2504
|
+
typeof suffix.text !== 'string' ||
|
|
2505
|
+
!['secondary', 'disabled'].includes(suffix.tone)
|
|
2506
|
+
)
|
|
2507
|
+
fail(`${path}.titleSuffixes[${index}]`, 'invalid title suffix');
|
|
2508
|
+
if (
|
|
2509
|
+
suffix.maxWidth !== undefined &&
|
|
2510
|
+
(!Number.isFinite(suffix.maxWidth) ||
|
|
2511
|
+
suffix.maxWidth < 0 ||
|
|
2512
|
+
suffix.maxWidth > 4096)
|
|
2513
|
+
)
|
|
2514
|
+
fail(
|
|
2515
|
+
`${path}.titleSuffixes[${index}].maxWidth`,
|
|
2516
|
+
'must be finite within 0...4096'
|
|
2517
|
+
);
|
|
2518
|
+
});
|
|
2070
2519
|
let end = 0;
|
|
2071
2520
|
row.titleMatch?.forEach((match) => {
|
|
2072
2521
|
if (
|
|
@@ -2080,6 +2529,9 @@ function assertRowProtocol(row: RowModel, path: string): void {
|
|
|
2080
2529
|
end = match.end;
|
|
2081
2530
|
});
|
|
2082
2531
|
}
|
|
2532
|
+
if (row.type === 'buttonGroup') assertButtonGroup(row, path);
|
|
2533
|
+
if (row.type === 'sectionHeader')
|
|
2534
|
+
assertTextHelp(row.valueHelp, `${path}.valueHelp`);
|
|
2083
2535
|
if (row.type === 'mediaTile' && row.media) {
|
|
2084
2536
|
protocolObject(row.media.source, `${path}.media.source`);
|
|
2085
2537
|
if (!Array.isArray(row.media.probeOrder))
|
|
@@ -2100,8 +2552,16 @@ function assertRowProtocol(row: RowModel, path: string): void {
|
|
|
2100
2552
|
keys.add(member.key);
|
|
2101
2553
|
});
|
|
2102
2554
|
}
|
|
2103
|
-
if (row.type === 'dataRow'
|
|
2104
|
-
|
|
2555
|
+
if (row.type === 'dataRow') {
|
|
2556
|
+
if (!Array.isArray(row.columns))
|
|
2557
|
+
fail(`${path}.columns`, 'must be an array');
|
|
2558
|
+
row.columns.forEach((column, index) => {
|
|
2559
|
+
const columnPath = `${path}.columns[${index}]`;
|
|
2560
|
+
protocolObject(column, columnPath);
|
|
2561
|
+
assertIdentityExtras(column, columnPath);
|
|
2562
|
+
});
|
|
2563
|
+
assertTrailingAccessories(row.trailing, `${path}.trailing`);
|
|
2564
|
+
}
|
|
2105
2565
|
if (row.type === 'market') {
|
|
2106
2566
|
protocolObject(row.change, `${path}.change`);
|
|
2107
2567
|
if (row.webTable) {
|
|
@@ -2150,6 +2610,53 @@ function assertRowProtocol(row: RowModel, path: string): void {
|
|
|
2150
2610
|
|
|
2151
2611
|
function assertSnapshotProtocol(snapshot: NativeListSnapshot): void {
|
|
2152
2612
|
protocolObject(snapshot, 'snapshot');
|
|
2613
|
+
if (snapshot.disclosure !== undefined) {
|
|
2614
|
+
protocolObject(snapshot.disclosure, 'snapshot.disclosure');
|
|
2615
|
+
if (typeof snapshot.disclosure.expanded !== 'boolean')
|
|
2616
|
+
fail('snapshot.disclosure.expanded', 'must be a boolean');
|
|
2617
|
+
const inset = snapshot.disclosure.expandedTrailingInset;
|
|
2618
|
+
if (inset !== undefined && (!Number.isFinite(inset) || inset < 0))
|
|
2619
|
+
fail(
|
|
2620
|
+
'snapshot.disclosure.expandedTrailingInset',
|
|
2621
|
+
'must be finite and nonnegative'
|
|
2622
|
+
);
|
|
2623
|
+
}
|
|
2624
|
+
if (snapshot.structuralTransition) {
|
|
2625
|
+
protocolObject(
|
|
2626
|
+
snapshot.structuralTransition,
|
|
2627
|
+
'snapshot.structuralTransition'
|
|
2628
|
+
);
|
|
2629
|
+
protocolKey(
|
|
2630
|
+
snapshot.structuralTransition.id,
|
|
2631
|
+
'snapshot.structuralTransition.id'
|
|
2632
|
+
);
|
|
2633
|
+
if (snapshot.structuralTransition.anchorKey !== undefined)
|
|
2634
|
+
protocolKey(
|
|
2635
|
+
snapshot.structuralTransition.anchorKey,
|
|
2636
|
+
'snapshot.structuralTransition.anchorKey'
|
|
2637
|
+
);
|
|
2638
|
+
const duration = snapshot.structuralTransition.durationMs;
|
|
2639
|
+
if (
|
|
2640
|
+
duration !== undefined &&
|
|
2641
|
+
(!Number.isFinite(duration) || duration < 0 || duration > 2000)
|
|
2642
|
+
)
|
|
2643
|
+
fail(
|
|
2644
|
+
'snapshot.structuralTransition.durationMs',
|
|
2645
|
+
'must be finite within 0...2000'
|
|
2646
|
+
);
|
|
2647
|
+
}
|
|
2648
|
+
if (
|
|
2649
|
+
snapshot.theme?.successBackground !== undefined &&
|
|
2650
|
+
(typeof snapshot.theme.successBackground !== 'string' ||
|
|
2651
|
+
!/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(snapshot.theme.successBackground))
|
|
2652
|
+
)
|
|
2653
|
+
fail('theme.successBackground', 'must be a hex color');
|
|
2654
|
+
if (
|
|
2655
|
+
snapshot.theme?.rowHoverBackground !== undefined &&
|
|
2656
|
+
(typeof snapshot.theme.rowHoverBackground !== 'string' ||
|
|
2657
|
+
!/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(snapshot.theme.rowHoverBackground))
|
|
2658
|
+
)
|
|
2659
|
+
fail('theme.rowHoverBackground', 'must be a hex color');
|
|
2153
2660
|
if (snapshot.schemaVersion !== 1) fail('snapshot.schemaVersion', 'must be 1');
|
|
2154
2661
|
if (!Number.isSafeInteger(snapshot.generation) || snapshot.generation < 0)
|
|
2155
2662
|
fail('snapshot.generation', 'must be a non-negative safe integer');
|
|
@@ -2238,6 +2745,73 @@ export function validateSnapshot(
|
|
|
2238
2745
|
return normalizeSnapshotStyles(normalized);
|
|
2239
2746
|
}
|
|
2240
2747
|
|
|
2748
|
+
const ownedNativeSnapshots = new WeakMap<
|
|
2749
|
+
NativeListSnapshot,
|
|
2750
|
+
ReadonlyMap<string, number>
|
|
2751
|
+
>();
|
|
2752
|
+
|
|
2753
|
+
function capturePlainData<T>(value: T, seen = new Map<object, object>()): T {
|
|
2754
|
+
if (value === null || typeof value !== 'object') return value;
|
|
2755
|
+
const previous = seen.get(value);
|
|
2756
|
+
if (previous) return previous as T;
|
|
2757
|
+
const copy: Record<string, unknown> | unknown[] = Array.isArray(value)
|
|
2758
|
+
? new Array(value.length)
|
|
2759
|
+
: Object.create(Object.getPrototypeOf(value));
|
|
2760
|
+
seen.set(value, copy);
|
|
2761
|
+
const captureKey = (key: string) => {
|
|
2762
|
+
Object.defineProperty(copy, key, {
|
|
2763
|
+
value: capturePlainData((value as Record<string, unknown>)[key], seen),
|
|
2764
|
+
enumerable: true,
|
|
2765
|
+
writable: true,
|
|
2766
|
+
configurable: true,
|
|
2767
|
+
});
|
|
2768
|
+
};
|
|
2769
|
+
if (Array.isArray(value)) {
|
|
2770
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
2771
|
+
if (index in value) captureKey(String(index));
|
|
2772
|
+
}
|
|
2773
|
+
Object.keys(value).forEach((key) => {
|
|
2774
|
+
const index = Number(key);
|
|
2775
|
+
if (
|
|
2776
|
+
Number.isInteger(index) &&
|
|
2777
|
+
index >= 0 &&
|
|
2778
|
+
index < value.length &&
|
|
2779
|
+
String(index) === key
|
|
2780
|
+
)
|
|
2781
|
+
return;
|
|
2782
|
+
captureKey(key);
|
|
2783
|
+
});
|
|
2784
|
+
} else {
|
|
2785
|
+
Object.keys(value).forEach(captureKey);
|
|
2786
|
+
}
|
|
2787
|
+
return copy as T;
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
function deepFreeze<T>(value: T, seen = new WeakSet<object>()): T {
|
|
2791
|
+
if (value === null || typeof value !== 'object' || seen.has(value))
|
|
2792
|
+
return value;
|
|
2793
|
+
seen.add(value);
|
|
2794
|
+
Object.values(value).forEach((item) => deepFreeze(item, seen));
|
|
2795
|
+
return Object.freeze(value);
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
/** Capture caller data once before validation, including accessor values. */
|
|
2799
|
+
export function captureNativeListInput<T>(value: T): T {
|
|
2800
|
+
return capturePlainData(value);
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
/** Private native-wrapper ownership; exported validators keep their public identity. */
|
|
2804
|
+
export function ownValidatedNativeSnapshot(
|
|
2805
|
+
snapshot: NativeListSnapshot
|
|
2806
|
+
): NativeListSnapshot {
|
|
2807
|
+
const owned = deepFreeze(snapshot);
|
|
2808
|
+
ownedNativeSnapshots.set(
|
|
2809
|
+
owned,
|
|
2810
|
+
new Map(owned.rows.map((row, index) => [row.key, index]))
|
|
2811
|
+
);
|
|
2812
|
+
return owned;
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2241
2815
|
/**
|
|
2242
2816
|
* Resolves typography tokens to numbers. Returns the original snapshot when
|
|
2243
2817
|
* nothing needs resolving, so the common no-style path keeps object identity.
|
|
@@ -2294,7 +2868,46 @@ function assertPatchChanges(patch: RowPatch, index: number): void {
|
|
|
2294
2868
|
|
|
2295
2869
|
switch (patch.type) {
|
|
2296
2870
|
case 'identity':
|
|
2871
|
+
if (
|
|
2872
|
+
patch.changes.contentHeight !== undefined &&
|
|
2873
|
+
patch.changes.contentHeight !== 'auto'
|
|
2874
|
+
) {
|
|
2875
|
+
fail(`${path}.contentHeight`, 'must be auto');
|
|
2876
|
+
}
|
|
2877
|
+
if (
|
|
2878
|
+
patch.changes.subtitleLines !== undefined &&
|
|
2879
|
+
![0, 1, 2].includes(patch.changes.subtitleLines)
|
|
2880
|
+
) {
|
|
2881
|
+
fail(`${path}.subtitleLines`, 'must be 0, 1, or 2');
|
|
2882
|
+
}
|
|
2883
|
+
if (patch.changes.longPressActionKey !== undefined) {
|
|
2884
|
+
assertKey(
|
|
2885
|
+
patch.changes.longPressActionKey,
|
|
2886
|
+
`${path}.longPressActionKey`
|
|
2887
|
+
);
|
|
2888
|
+
}
|
|
2297
2889
|
assertText(patch.changes.title, `${path}.title`);
|
|
2890
|
+
if ((patch.changes.titleSuffixes?.length ?? 0) > 3)
|
|
2891
|
+
fail(`${path}.titleSuffixes`, 'supports at most 3 suffixes');
|
|
2892
|
+
patch.changes.titleSuffixes?.forEach((suffix, index) => {
|
|
2893
|
+
assertText(suffix.text, `${path}.titleSuffixes[${index}].text`);
|
|
2894
|
+
if (!['secondary', 'disabled'].includes(suffix.tone))
|
|
2895
|
+
fail(
|
|
2896
|
+
`${path}.titleSuffixes[${index}].tone`,
|
|
2897
|
+
'must be secondary or disabled'
|
|
2898
|
+
);
|
|
2899
|
+
if (
|
|
2900
|
+
suffix.maxWidth !== undefined &&
|
|
2901
|
+
(!Number.isFinite(suffix.maxWidth) ||
|
|
2902
|
+
suffix.maxWidth < 0 ||
|
|
2903
|
+
suffix.maxWidth > 4096)
|
|
2904
|
+
)
|
|
2905
|
+
fail(
|
|
2906
|
+
`${path}.titleSuffixes[${index}].maxWidth`,
|
|
2907
|
+
'must be finite within 0...4096'
|
|
2908
|
+
);
|
|
2909
|
+
});
|
|
2910
|
+
assertIdentityExtras(patch.changes, path);
|
|
2298
2911
|
assertText(patch.changes.subtitle, `${path}.subtitle`);
|
|
2299
2912
|
assertText(patch.changes.tertiary, `${path}.tertiary`);
|
|
2300
2913
|
if (
|
|
@@ -2343,14 +2956,20 @@ function assertPatchChanges(patch: RowPatch, index: number): void {
|
|
|
2343
2956
|
}
|
|
2344
2957
|
break;
|
|
2345
2958
|
case 'message':
|
|
2959
|
+
if (
|
|
2960
|
+
patch.changes.presentation !== undefined &&
|
|
2961
|
+
patch.changes.presentation !== 'inlineWarning'
|
|
2962
|
+
) {
|
|
2963
|
+
fail(`${path}.presentation`, 'must be inlineWarning');
|
|
2964
|
+
}
|
|
2346
2965
|
assertText(patch.changes.title, `${path}.title`);
|
|
2347
2966
|
assertText(patch.changes.body, `${path}.body`);
|
|
2348
2967
|
assertText(patch.changes.time, `${path}.time`);
|
|
2349
2968
|
if (
|
|
2350
2969
|
patch.changes.bodyLines !== undefined &&
|
|
2351
|
-
![1, 2, 3].includes(patch.changes.bodyLines)
|
|
2970
|
+
![0, 1, 2, 3].includes(patch.changes.bodyLines)
|
|
2352
2971
|
) {
|
|
2353
|
-
fail(`${path}.bodyLines`, 'must be 1, 2, or 3');
|
|
2972
|
+
fail(`${path}.bodyLines`, 'must be 0, 1, 2, or 3');
|
|
2354
2973
|
}
|
|
2355
2974
|
assertImage(patch.changes.thumbnail, `${path}.thumbnail`);
|
|
2356
2975
|
break;
|
|
@@ -2364,6 +2983,23 @@ function assertPatchChanges(patch: RowPatch, index: number): void {
|
|
|
2364
2983
|
patch.changes.columns?.forEach((column, columnIndex) => {
|
|
2365
2984
|
assertKey(column.key, `${path}.columns[${columnIndex}].key`);
|
|
2366
2985
|
assertText(column.text, `${path}.columns[${columnIndex}].text`);
|
|
2986
|
+
assertIdentityExtras(column, `${path}.columns[${columnIndex}]`);
|
|
2987
|
+
for (const field of [
|
|
2988
|
+
'textSegments',
|
|
2989
|
+
'secondaryTextSegments',
|
|
2990
|
+
] as const) {
|
|
2991
|
+
column[field]?.forEach((segment, segmentIndex) => {
|
|
2992
|
+
assertText(
|
|
2993
|
+
segment.text,
|
|
2994
|
+
`${path}.columns[${columnIndex}].${field}[${segmentIndex}].text`
|
|
2995
|
+
);
|
|
2996
|
+
if (segment.style !== undefined && segment.style !== 'subscript')
|
|
2997
|
+
fail(
|
|
2998
|
+
`${path}.columns[${columnIndex}].${field}[${segmentIndex}].style`,
|
|
2999
|
+
'must be subscript'
|
|
3000
|
+
);
|
|
3001
|
+
});
|
|
3002
|
+
}
|
|
2367
3003
|
assertText(
|
|
2368
3004
|
column.secondaryText,
|
|
2369
3005
|
`${path}.columns[${columnIndex}].secondaryText`
|
|
@@ -2376,6 +3012,13 @@ function assertPatchChanges(patch: RowPatch, index: number): void {
|
|
|
2376
3012
|
if ((patch.changes.badges?.length ?? 0) > MAX_BADGES) {
|
|
2377
3013
|
fail(`${path}.badges`, `supports at most ${MAX_BADGES} badges`);
|
|
2378
3014
|
}
|
|
3015
|
+
if ((patch.changes.trailing?.length ?? 0) > MAX_TRAILING_ACCESSORIES) {
|
|
3016
|
+
fail(
|
|
3017
|
+
`${path}.trailing`,
|
|
3018
|
+
`supports at most ${MAX_TRAILING_ACCESSORIES} accessories`
|
|
3019
|
+
);
|
|
3020
|
+
}
|
|
3021
|
+
assertTrailingAccessories(patch.changes.trailing, `${path}.trailing`);
|
|
2379
3022
|
break;
|
|
2380
3023
|
case 'market':
|
|
2381
3024
|
assertMarketRow(
|
|
@@ -2450,6 +3093,14 @@ function assertPatchChanges(patch: RowPatch, index: number): void {
|
|
|
2450
3093
|
assertText(patch.changes.title, `${path}.title`);
|
|
2451
3094
|
assertText(patch.changes.subtitle, `${path}.subtitle`);
|
|
2452
3095
|
assertText(patch.changes.value, `${path}.value`);
|
|
3096
|
+
if (patch.changes.leadingBadge) {
|
|
3097
|
+
assertKey(patch.changes.leadingBadge.key, `${path}.leadingBadge.key`);
|
|
3098
|
+
assertText(
|
|
3099
|
+
patch.changes.leadingBadge.text,
|
|
3100
|
+
`${path}.leadingBadge.text`
|
|
3101
|
+
);
|
|
3102
|
+
}
|
|
3103
|
+
assertTextHelp(patch.changes.valueHelp, `${path}.valueHelp`);
|
|
2453
3104
|
if (patch.changes.valueActionKey !== undefined) {
|
|
2454
3105
|
assertKey(patch.changes.valueActionKey, `${path}.valueActionKey`);
|
|
2455
3106
|
}
|
|
@@ -2472,6 +3123,10 @@ function assertPatchChanges(patch: RowPatch, index: number): void {
|
|
|
2472
3123
|
}
|
|
2473
3124
|
assertTrailingAccessories(patch.changes.trailing, `${path}.trailing`);
|
|
2474
3125
|
break;
|
|
3126
|
+
case 'buttonGroup':
|
|
3127
|
+
if (patch.changes.buttons !== undefined)
|
|
3128
|
+
assertButtonGroup({ buttons: patch.changes.buttons }, path);
|
|
3129
|
+
break;
|
|
2475
3130
|
case 'system':
|
|
2476
3131
|
assertText(patch.changes.message, `${path}.message`);
|
|
2477
3132
|
break;
|
|
@@ -2495,6 +3150,8 @@ export function validatePatches(
|
|
|
2495
3150
|
protocolObject(patch.changes, `patches[${index}].changes`);
|
|
2496
3151
|
assertRowType(patch.type, `patches[${index}]`);
|
|
2497
3152
|
assertProtocolFields(patch.changes, `patches[${index}].changes`);
|
|
3153
|
+
if (patch.type === 'identity')
|
|
3154
|
+
assertIdentityExtras(patch.changes, `patches[${index}].changes`);
|
|
2498
3155
|
if (!isDevelopment()) continue;
|
|
2499
3156
|
assertKey(patch.key, `patches[${index}].key`);
|
|
2500
3157
|
if (Object.keys(patch.changes).length === 0) {
|
|
@@ -2529,6 +3186,116 @@ function normalizePatchStyles(
|
|
|
2529
3186
|
return changed ? next : patches;
|
|
2530
3187
|
}
|
|
2531
3188
|
|
|
3189
|
+
const EMPTY_ROW_PATCHES: readonly RowPatch[] = Object.freeze([]);
|
|
3190
|
+
|
|
3191
|
+
function sameJsonValue(left: unknown, right: unknown): boolean {
|
|
3192
|
+
if (left === right) return true;
|
|
3193
|
+
if (left === null || right === null) return false;
|
|
3194
|
+
if (typeof left !== 'object' || typeof right !== 'object') return false;
|
|
3195
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
3196
|
+
return (
|
|
3197
|
+
Array.isArray(left) &&
|
|
3198
|
+
Array.isArray(right) &&
|
|
3199
|
+
left.length === right.length &&
|
|
3200
|
+
left.every((value, index) => sameJsonValue(value, right[index]))
|
|
3201
|
+
);
|
|
3202
|
+
}
|
|
3203
|
+
const leftObject = left as Record<string, unknown>;
|
|
3204
|
+
const rightObject = right as Record<string, unknown>;
|
|
3205
|
+
const leftKeys = Object.keys(leftObject).filter(
|
|
3206
|
+
(key) => leftObject[key] !== undefined
|
|
3207
|
+
);
|
|
3208
|
+
const rightKeys = Object.keys(rightObject).filter(
|
|
3209
|
+
(key) => rightObject[key] !== undefined
|
|
3210
|
+
);
|
|
3211
|
+
return (
|
|
3212
|
+
leftKeys.length === rightKeys.length &&
|
|
3213
|
+
leftKeys.every((key) => sameJsonValue(leftObject[key], rightObject[key]))
|
|
3214
|
+
);
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
function sameOptionalPresence(left: unknown, right: unknown): boolean {
|
|
3218
|
+
return (left === undefined) === (right === undefined);
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
/** Plan value-only patches; callers own valid immutable snapshots and commit state. */
|
|
3222
|
+
export function planNativeListPatches(
|
|
3223
|
+
previous: NativeListSnapshot,
|
|
3224
|
+
next: NativeListSnapshot
|
|
3225
|
+
): readonly RowPatch[] | undefined {
|
|
3226
|
+
if (previous === next) return EMPTY_ROW_PATCHES;
|
|
3227
|
+
const { rows: previousRows, ...previousMetadata } = previous;
|
|
3228
|
+
const { rows: nextRows, ...nextMetadata } = next;
|
|
3229
|
+
if (
|
|
3230
|
+
previousRows.length !== nextRows.length ||
|
|
3231
|
+
!sameJsonValue(previousMetadata, nextMetadata)
|
|
3232
|
+
)
|
|
3233
|
+
return undefined;
|
|
3234
|
+
if (previousRows === nextRows) return EMPTY_ROW_PATCHES;
|
|
3235
|
+
|
|
3236
|
+
const patches: RowPatch[] = [];
|
|
3237
|
+
for (let index = 0; index < nextRows.length; index += 1) {
|
|
3238
|
+
const oldRow = previousRows[index];
|
|
3239
|
+
const row = nextRows[index];
|
|
3240
|
+
if (oldRow.key !== row.key || oldRow.type !== row.type) return undefined;
|
|
3241
|
+
if (oldRow === row) continue;
|
|
3242
|
+
|
|
3243
|
+
if (oldRow.type === 'identity' && row.type === 'identity') {
|
|
3244
|
+
const {
|
|
3245
|
+
subtitle: oldSubtitle,
|
|
3246
|
+
subtitleSegments: oldSubtitleSegments,
|
|
3247
|
+
trailing: oldTrailing,
|
|
3248
|
+
...oldMetadata
|
|
3249
|
+
} = oldRow;
|
|
3250
|
+
const { subtitle, subtitleSegments, trailing, ...metadata } = row;
|
|
3251
|
+
if (
|
|
3252
|
+
!sameOptionalPresence(oldSubtitle, subtitle) ||
|
|
3253
|
+
!sameOptionalPresence(oldSubtitleSegments, subtitleSegments) ||
|
|
3254
|
+
!sameOptionalPresence(oldTrailing, trailing) ||
|
|
3255
|
+
!sameJsonValue(oldMetadata, metadata)
|
|
3256
|
+
)
|
|
3257
|
+
return undefined;
|
|
3258
|
+
if (
|
|
3259
|
+
!sameJsonValue(oldSubtitle, subtitle) ||
|
|
3260
|
+
!sameJsonValue(oldSubtitleSegments, subtitleSegments) ||
|
|
3261
|
+
!sameJsonValue(oldTrailing, trailing)
|
|
3262
|
+
) {
|
|
3263
|
+
patches.push({
|
|
3264
|
+
type: 'identity',
|
|
3265
|
+
key: row.key,
|
|
3266
|
+
changes: { subtitle, subtitleSegments, trailing },
|
|
3267
|
+
});
|
|
3268
|
+
}
|
|
3269
|
+
} else if (oldRow.type === 'dataRow' && row.type === 'dataRow') {
|
|
3270
|
+
const {
|
|
3271
|
+
columns: oldColumns,
|
|
3272
|
+
trailing: oldTrailing,
|
|
3273
|
+
...oldMetadata
|
|
3274
|
+
} = oldRow;
|
|
3275
|
+
const { columns, trailing, ...metadata } = row;
|
|
3276
|
+
if (
|
|
3277
|
+
!sameOptionalPresence(oldTrailing, trailing) ||
|
|
3278
|
+
!sameJsonValue(oldMetadata, metadata) ||
|
|
3279
|
+
!sameJsonValue(oldColumns[0], columns[0])
|
|
3280
|
+
)
|
|
3281
|
+
return undefined;
|
|
3282
|
+
if (
|
|
3283
|
+
!sameJsonValue(oldColumns.slice(1), columns.slice(1)) ||
|
|
3284
|
+
!sameJsonValue(oldTrailing, trailing)
|
|
3285
|
+
) {
|
|
3286
|
+
patches.push({
|
|
3287
|
+
type: 'dataRow',
|
|
3288
|
+
key: row.key,
|
|
3289
|
+
changes: { columns, trailing },
|
|
3290
|
+
});
|
|
3291
|
+
}
|
|
3292
|
+
} else if (!sameJsonValue(oldRow, row)) {
|
|
3293
|
+
return undefined;
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
return patches.length === 0 ? EMPTY_ROW_PATCHES : patches;
|
|
3297
|
+
}
|
|
3298
|
+
|
|
2532
3299
|
export function applyRowPatches(
|
|
2533
3300
|
snapshot: NativeListSnapshot,
|
|
2534
3301
|
patches: readonly RowPatch[]
|
|
@@ -2571,6 +3338,85 @@ export function applyRowPatches(
|
|
|
2571
3338
|
return validateSnapshot({ ...snapshot, rows });
|
|
2572
3339
|
}
|
|
2573
3340
|
|
|
3341
|
+
/** Native wrapper only: preserve full public validation for unsupported patches. */
|
|
3342
|
+
export function applyOwnedNativePatches(
|
|
3343
|
+
snapshot: NativeListSnapshot,
|
|
3344
|
+
patches: readonly RowPatch[]
|
|
3345
|
+
): {
|
|
3346
|
+
patches: readonly RowPatch[];
|
|
3347
|
+
snapshot: NativeListSnapshot;
|
|
3348
|
+
json: string;
|
|
3349
|
+
} {
|
|
3350
|
+
const acceptedPatches = deepFreeze(
|
|
3351
|
+
validatePatches(capturePlainData(patches))
|
|
3352
|
+
);
|
|
3353
|
+
const rowIndexes = ownedNativeSnapshots.get(snapshot);
|
|
3354
|
+
const canPatchRows =
|
|
3355
|
+
rowIndexes !== undefined &&
|
|
3356
|
+
acceptedPatches.every((patch) => {
|
|
3357
|
+
const index = rowIndexes.get(patch.key);
|
|
3358
|
+
if (index === undefined || snapshot.rows[index].type !== patch.type)
|
|
3359
|
+
return false;
|
|
3360
|
+
const allowed =
|
|
3361
|
+
patch.type === 'identity'
|
|
3362
|
+
? ['subtitle', 'subtitleSegments', 'trailing']
|
|
3363
|
+
: patch.type === 'dataRow'
|
|
3364
|
+
? ['columns', 'trailing']
|
|
3365
|
+
: undefined;
|
|
3366
|
+
return (
|
|
3367
|
+
allowed !== undefined &&
|
|
3368
|
+
Object.entries(patch.changes).every(
|
|
3369
|
+
([key, value]) =>
|
|
3370
|
+
allowed.includes(key) &&
|
|
3371
|
+
(value !== undefined ||
|
|
3372
|
+
(snapshot.rows[index] as unknown as Record<string, unknown>)[
|
|
3373
|
+
key
|
|
3374
|
+
] === undefined)
|
|
3375
|
+
)
|
|
3376
|
+
);
|
|
3377
|
+
});
|
|
3378
|
+
if (!canPatchRows) {
|
|
3379
|
+
const baseline = rowIndexes ? snapshot : capturePlainData(snapshot);
|
|
3380
|
+
return {
|
|
3381
|
+
patches: acceptedPatches,
|
|
3382
|
+
snapshot: ownValidatedNativeSnapshot(
|
|
3383
|
+
applyRowPatches(baseline, acceptedPatches)
|
|
3384
|
+
),
|
|
3385
|
+
json: JSON.stringify(acceptedPatches),
|
|
3386
|
+
};
|
|
3387
|
+
}
|
|
3388
|
+
|
|
3389
|
+
const nextRows = snapshot.rows.slice();
|
|
3390
|
+
for (const patch of acceptedPatches) {
|
|
3391
|
+
const index = rowIndexes.get(patch.key)!;
|
|
3392
|
+
const previous = nextRows[index];
|
|
3393
|
+
const merged = {
|
|
3394
|
+
...previous,
|
|
3395
|
+
...patch.changes,
|
|
3396
|
+
key: previous.key,
|
|
3397
|
+
type: previous.type,
|
|
3398
|
+
} as RowModel;
|
|
3399
|
+
const path = `rows[${index}]`;
|
|
3400
|
+
assertPlainSerializable(merged, path, new Set<object>());
|
|
3401
|
+
if (isDevelopment()) assertRow(merged, index);
|
|
3402
|
+
const normalized = isDevelopment()
|
|
3403
|
+
? merged
|
|
3404
|
+
: (normalizeEmptyImages(merged) as RowModel);
|
|
3405
|
+
assertRowProtocol(normalized, path);
|
|
3406
|
+
nextRows[index] = deepFreeze(normalizeRowStyles(normalized));
|
|
3407
|
+
}
|
|
3408
|
+
const nextSnapshot = Object.freeze({
|
|
3409
|
+
...snapshot,
|
|
3410
|
+
rows: Object.freeze(nextRows),
|
|
3411
|
+
});
|
|
3412
|
+
ownedNativeSnapshots.set(nextSnapshot, rowIndexes);
|
|
3413
|
+
return {
|
|
3414
|
+
patches: acceptedPatches,
|
|
3415
|
+
snapshot: nextSnapshot,
|
|
3416
|
+
json: JSON.stringify(acceptedPatches),
|
|
3417
|
+
};
|
|
3418
|
+
}
|
|
3419
|
+
|
|
2574
3420
|
export function serializeSnapshot(snapshot: NativeListSnapshot): string {
|
|
2575
3421
|
return JSON.stringify(validateSnapshot(snapshot));
|
|
2576
3422
|
}
|