fontdue-js 2.19.1 → 2.19.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/dist/__tests__/collectionBundleSelection.test.js +453 -0
- package/dist/reducer.js +32 -10
- package/dist/utils.d.ts +3 -0
- package/dist/utils.js +19 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
## 2.19.2
|
|
2
|
+
|
|
3
|
+
- Fixed style packages (bundles within a family) not displaying as selected and not responding to deselection clicks when selected via a parent collection bundle or family
|
|
4
|
+
- Fixed auto-upgrade not selecting collection bundles transitively (e.g., selecting both style packages now upgrades through the family to the collection bundle in a single step)
|
|
5
|
+
|
|
1
6
|
## 2.19.1
|
|
2
7
|
|
|
3
8
|
- Fixed collection bundles not auto-selecting when a family at the same price is chosen — the bundle now correctly takes priority over the family itself
|
|
@@ -910,6 +910,86 @@ function makeThunderState() {
|
|
|
910
910
|
});
|
|
911
911
|
(0, _vitest.expect)(newState.selectedSkuIds['text-bundle-sku']).toBe(false);
|
|
912
912
|
});
|
|
913
|
+
(0, _vitest.it)('deselecting a family bundle deselects the parent collection bundle (transitive)', () => {
|
|
914
|
+
// Collection bundle is selected. A family bundle (style package) within
|
|
915
|
+
// it is shown as selected via transitive isSelected. Clicking to deselect
|
|
916
|
+
// it should find and deselect the collection bundle.
|
|
917
|
+
const state = makeState({
|
|
918
|
+
selectedSkuIds: {
|
|
919
|
+
'coll-bundle-sku': true,
|
|
920
|
+
'family-sku': false,
|
|
921
|
+
'var-sku': false,
|
|
922
|
+
'pkg-upright-sku': false,
|
|
923
|
+
'pkg-italic-sku': false,
|
|
924
|
+
s1: false,
|
|
925
|
+
s2: false,
|
|
926
|
+
si1: false,
|
|
927
|
+
si2: false,
|
|
928
|
+
vs1: false
|
|
929
|
+
},
|
|
930
|
+
collectionStyleSkus: {
|
|
931
|
+
'coll-bundle-sku': {
|
|
932
|
+
fontStyleSkuIds: ['s1', 's2', 'si1', 'si2', 'vs1'],
|
|
933
|
+
fontStyleIds: ['f1', 'f2', 'fi1', 'fi2', 'vf1'],
|
|
934
|
+
childrenSkuIds: ['family-sku', 'var-sku'],
|
|
935
|
+
name: 'Text + Variable'
|
|
936
|
+
},
|
|
937
|
+
'family-sku': {
|
|
938
|
+
fontStyleSkuIds: ['s1', 's2', 'si1', 'si2'],
|
|
939
|
+
fontStyleIds: ['f1', 'f2', 'fi1', 'fi2'],
|
|
940
|
+
childrenSkuIds: ['pkg-upright-sku', 'pkg-italic-sku'],
|
|
941
|
+
name: 'Text Family'
|
|
942
|
+
},
|
|
943
|
+
'var-sku': {
|
|
944
|
+
fontStyleSkuIds: ['vs1'],
|
|
945
|
+
fontStyleIds: ['vf1'],
|
|
946
|
+
childrenSkuIds: [],
|
|
947
|
+
name: 'Variable'
|
|
948
|
+
},
|
|
949
|
+
'pkg-upright-sku': {
|
|
950
|
+
fontStyleSkuIds: ['s1', 's2'],
|
|
951
|
+
fontStyleIds: ['f1', 'f2'],
|
|
952
|
+
childrenSkuIds: [],
|
|
953
|
+
name: 'Package Text'
|
|
954
|
+
},
|
|
955
|
+
'pkg-italic-sku': {
|
|
956
|
+
fontStyleSkuIds: ['si1', 'si2'],
|
|
957
|
+
fontStyleIds: ['fi1', 'fi2'],
|
|
958
|
+
childrenSkuIds: [],
|
|
959
|
+
name: 'Package Italic'
|
|
960
|
+
}
|
|
961
|
+
},
|
|
962
|
+
skuPrices: {
|
|
963
|
+
'coll-bundle-sku': 500,
|
|
964
|
+
'family-sku': 500,
|
|
965
|
+
'var-sku': 300,
|
|
966
|
+
'pkg-upright-sku': 250,
|
|
967
|
+
'pkg-italic-sku': 250,
|
|
968
|
+
s1: 100,
|
|
969
|
+
s2: 100,
|
|
970
|
+
si1: 100,
|
|
971
|
+
si2: 100,
|
|
972
|
+
vs1: 100
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
// Bundle shows as selected via transitive isSelected
|
|
977
|
+
(0, _vitest.expect)((0, _utils.isSelected)('pkg-upright-sku')(state)).toBe(true);
|
|
978
|
+
|
|
979
|
+
// Click to deselect the bundle
|
|
980
|
+
const newState = (0, _reducer.default)(state, {
|
|
981
|
+
type: 'SELECT_SKU_ID',
|
|
982
|
+
skuId: 'pkg-upright-sku',
|
|
983
|
+
selected: false
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
// Collection bundle should be deselected
|
|
987
|
+
(0, _vitest.expect)(newState.selectedSkuIds['coll-bundle-sku']).toBe(false);
|
|
988
|
+
// Nothing should be selected anymore
|
|
989
|
+
(0, _vitest.expect)((0, _utils.isSelected)('pkg-upright-sku')(newState)).toBe(false);
|
|
990
|
+
(0, _vitest.expect)((0, _utils.isSelected)('family-sku')(newState)).toBe(false);
|
|
991
|
+
(0, _vitest.expect)((0, _utils.isSelected)('s1')(newState)).toBe(false);
|
|
992
|
+
});
|
|
913
993
|
(0, _vitest.it)('deselecting a bundle does not affect other bundles', () => {
|
|
914
994
|
const state = makeThunderState({
|
|
915
995
|
selectedSkuIds: {
|
|
@@ -1178,4 +1258,377 @@ function makeThunderState() {
|
|
|
1178
1258
|
});
|
|
1179
1259
|
(0, _vitest.expect)(Object.keys(newState.selectedSkuIds)).toHaveLength(0);
|
|
1180
1260
|
});
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
// =========================================================================
|
|
1264
|
+
// isSelected — family bundles (style packages within a family)
|
|
1265
|
+
// =========================================================================
|
|
1266
|
+
(0, _vitest.describe)('isSelected with family bundles (style packages)', () => {
|
|
1267
|
+
// Helper: a family with two style bundles (upright + italic packages)
|
|
1268
|
+
function makeFamilyWithBundlesState() {
|
|
1269
|
+
let overrides = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
1270
|
+
return makeState({
|
|
1271
|
+
collectionStyleSkus: {
|
|
1272
|
+
'family-sku': {
|
|
1273
|
+
fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5', 'si1', 'si2', 'si3', 'si4', 'si5'],
|
|
1274
|
+
fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5', 'fi1', 'fi2', 'fi3', 'fi4', 'fi5'],
|
|
1275
|
+
childrenSkuIds: ['pkg-upright-sku', 'pkg-italic-sku'],
|
|
1276
|
+
name: 'Thunder Text Family'
|
|
1277
|
+
},
|
|
1278
|
+
'pkg-upright-sku': {
|
|
1279
|
+
fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5'],
|
|
1280
|
+
fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5'],
|
|
1281
|
+
childrenSkuIds: [],
|
|
1282
|
+
name: 'Package Text'
|
|
1283
|
+
},
|
|
1284
|
+
'pkg-italic-sku': {
|
|
1285
|
+
fontStyleSkuIds: ['si1', 'si2', 'si3', 'si4', 'si5'],
|
|
1286
|
+
fontStyleIds: ['fi1', 'fi2', 'fi3', 'fi4', 'fi5'],
|
|
1287
|
+
childrenSkuIds: [],
|
|
1288
|
+
name: 'Package Text Italic'
|
|
1289
|
+
}
|
|
1290
|
+
},
|
|
1291
|
+
skuPrices: {
|
|
1292
|
+
'family-sku': 500,
|
|
1293
|
+
'pkg-upright-sku': 250,
|
|
1294
|
+
'pkg-italic-sku': 250,
|
|
1295
|
+
s1: 100,
|
|
1296
|
+
s2: 100,
|
|
1297
|
+
s3: 100,
|
|
1298
|
+
s4: 100,
|
|
1299
|
+
s5: 100,
|
|
1300
|
+
si1: 100,
|
|
1301
|
+
si2: 100,
|
|
1302
|
+
si3: 100,
|
|
1303
|
+
si4: 100,
|
|
1304
|
+
si5: 100
|
|
1305
|
+
},
|
|
1306
|
+
...overrides
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
(0, _vitest.it)('bundles show selected when family is directly selected', () => {
|
|
1310
|
+
const state = makeFamilyWithBundlesState({
|
|
1311
|
+
selectedSkuIds: {
|
|
1312
|
+
'family-sku': true,
|
|
1313
|
+
'pkg-upright-sku': false,
|
|
1314
|
+
'pkg-italic-sku': false,
|
|
1315
|
+
s1: false,
|
|
1316
|
+
s2: false,
|
|
1317
|
+
s3: false,
|
|
1318
|
+
s4: false,
|
|
1319
|
+
s5: false,
|
|
1320
|
+
si1: false,
|
|
1321
|
+
si2: false,
|
|
1322
|
+
si3: false,
|
|
1323
|
+
si4: false,
|
|
1324
|
+
si5: false
|
|
1325
|
+
}
|
|
1326
|
+
});
|
|
1327
|
+
(0, _vitest.expect)((0, _utils.isSelected)('pkg-upright-sku')(state)).toBe(true);
|
|
1328
|
+
(0, _vitest.expect)((0, _utils.isSelected)('pkg-italic-sku')(state)).toBe(true);
|
|
1329
|
+
(0, _vitest.expect)((0, _utils.isSelected)('s1')(state)).toBe(true);
|
|
1330
|
+
(0, _vitest.expect)((0, _utils.isSelected)('si1')(state)).toBe(true);
|
|
1331
|
+
});
|
|
1332
|
+
(0, _vitest.it)('bundles show selected when collection bundle is selected (transitive)', () => {
|
|
1333
|
+
// Collection bundle is selected → family is false → family bundles should
|
|
1334
|
+
// still show selected because the collection bundle covers all styles
|
|
1335
|
+
const state = makeFamilyWithBundlesState({
|
|
1336
|
+
collectionStyleSkus: {
|
|
1337
|
+
// Collection bundle: groups family + variable family
|
|
1338
|
+
'coll-bundle-sku': {
|
|
1339
|
+
fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5', 'si1', 'si2', 'si3', 'si4', 'si5', 'vs1'],
|
|
1340
|
+
fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5', 'fi1', 'fi2', 'fi3', 'fi4', 'fi5', 'vf1'],
|
|
1341
|
+
childrenSkuIds: ['family-sku', 'var-sku'],
|
|
1342
|
+
name: 'Thunder Text + Variable'
|
|
1343
|
+
},
|
|
1344
|
+
'family-sku': {
|
|
1345
|
+
fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5', 'si1', 'si2', 'si3', 'si4', 'si5'],
|
|
1346
|
+
fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5', 'fi1', 'fi2', 'fi3', 'fi4', 'fi5'],
|
|
1347
|
+
childrenSkuIds: ['pkg-upright-sku', 'pkg-italic-sku'],
|
|
1348
|
+
name: 'Thunder Text Family'
|
|
1349
|
+
},
|
|
1350
|
+
'var-sku': {
|
|
1351
|
+
fontStyleSkuIds: ['vs1'],
|
|
1352
|
+
fontStyleIds: ['vf1'],
|
|
1353
|
+
childrenSkuIds: [],
|
|
1354
|
+
name: 'Thunder Text Variable'
|
|
1355
|
+
},
|
|
1356
|
+
'pkg-upright-sku': {
|
|
1357
|
+
fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5'],
|
|
1358
|
+
fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5'],
|
|
1359
|
+
childrenSkuIds: [],
|
|
1360
|
+
name: 'Package Text'
|
|
1361
|
+
},
|
|
1362
|
+
'pkg-italic-sku': {
|
|
1363
|
+
fontStyleSkuIds: ['si1', 'si2', 'si3', 'si4', 'si5'],
|
|
1364
|
+
fontStyleIds: ['fi1', 'fi2', 'fi3', 'fi4', 'fi5'],
|
|
1365
|
+
childrenSkuIds: [],
|
|
1366
|
+
name: 'Package Text Italic'
|
|
1367
|
+
}
|
|
1368
|
+
},
|
|
1369
|
+
selectedSkuIds: {
|
|
1370
|
+
'coll-bundle-sku': true,
|
|
1371
|
+
'family-sku': false,
|
|
1372
|
+
'var-sku': false,
|
|
1373
|
+
'pkg-upright-sku': false,
|
|
1374
|
+
'pkg-italic-sku': false,
|
|
1375
|
+
s1: false,
|
|
1376
|
+
s2: false,
|
|
1377
|
+
s3: false,
|
|
1378
|
+
s4: false,
|
|
1379
|
+
s5: false,
|
|
1380
|
+
si1: false,
|
|
1381
|
+
si2: false,
|
|
1382
|
+
si3: false,
|
|
1383
|
+
si4: false,
|
|
1384
|
+
si5: false,
|
|
1385
|
+
vs1: false
|
|
1386
|
+
}
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
// Family shows selected (direct child of collection bundle)
|
|
1390
|
+
(0, _vitest.expect)((0, _utils.isSelected)('family-sku')(state)).toBe(true);
|
|
1391
|
+
// Bundles should show selected (grandchildren — family is child of
|
|
1392
|
+
// collection bundle, bundles are children of family)
|
|
1393
|
+
(0, _vitest.expect)((0, _utils.isSelected)('pkg-upright-sku')(state)).toBe(true);
|
|
1394
|
+
(0, _vitest.expect)((0, _utils.isSelected)('pkg-italic-sku')(state)).toBe(true);
|
|
1395
|
+
// Individual styles should also show selected
|
|
1396
|
+
(0, _vitest.expect)((0, _utils.isSelected)('s1')(state)).toBe(true);
|
|
1397
|
+
});
|
|
1398
|
+
(0, _vitest.it)('selecting one bundle then another upgrades to family via reducer', () => {
|
|
1399
|
+
const state = makeFamilyWithBundlesState();
|
|
1400
|
+
|
|
1401
|
+
// Select first bundle
|
|
1402
|
+
let newState = (0, _reducer.default)(state, {
|
|
1403
|
+
type: 'SELECT_SKU_ID',
|
|
1404
|
+
skuId: 'pkg-upright-sku',
|
|
1405
|
+
selected: true
|
|
1406
|
+
});
|
|
1407
|
+
(0, _vitest.expect)(newState.selectedSkuIds['pkg-upright-sku']).toBe(true);
|
|
1408
|
+
|
|
1409
|
+
// Select second bundle — should upgrade to family
|
|
1410
|
+
newState = (0, _reducer.default)(newState, {
|
|
1411
|
+
type: 'SELECT_SKU_ID',
|
|
1412
|
+
skuId: 'pkg-italic-sku',
|
|
1413
|
+
selected: true
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
// Family should be selected since family ($500) = two bundles ($250+$250)
|
|
1417
|
+
(0, _vitest.expect)(newState.selectedSkuIds['family-sku']).toBe(true);
|
|
1418
|
+
// Both bundles should report as selected
|
|
1419
|
+
(0, _vitest.expect)((0, _utils.isSelected)('pkg-upright-sku')(newState)).toBe(true);
|
|
1420
|
+
(0, _vitest.expect)((0, _utils.isSelected)('pkg-italic-sku')(newState)).toBe(true);
|
|
1421
|
+
});
|
|
1422
|
+
(0, _vitest.it)('auto-upgrades transitively: bundle → family → collection bundle', () => {
|
|
1423
|
+
// When a collection bundle exists at the same price as the family,
|
|
1424
|
+
// selecting both bundles should upgrade all the way to the collection
|
|
1425
|
+
// bundle (not stop at the family)
|
|
1426
|
+
const state = makeFamilyWithBundlesState({
|
|
1427
|
+
collectionStyleSkus: {
|
|
1428
|
+
'coll-bundle-sku': {
|
|
1429
|
+
fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5', 'si1', 'si2', 'si3', 'si4', 'si5', 'vs1'],
|
|
1430
|
+
fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5', 'fi1', 'fi2', 'fi3', 'fi4', 'fi5', 'vf1'],
|
|
1431
|
+
childrenSkuIds: ['family-sku', 'var-sku'],
|
|
1432
|
+
name: 'Thunder Text + Variable'
|
|
1433
|
+
},
|
|
1434
|
+
'family-sku': {
|
|
1435
|
+
fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5', 'si1', 'si2', 'si3', 'si4', 'si5'],
|
|
1436
|
+
fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5', 'fi1', 'fi2', 'fi3', 'fi4', 'fi5'],
|
|
1437
|
+
childrenSkuIds: ['pkg-upright-sku', 'pkg-italic-sku'],
|
|
1438
|
+
name: 'Thunder Text Family'
|
|
1439
|
+
},
|
|
1440
|
+
'var-sku': {
|
|
1441
|
+
fontStyleSkuIds: ['vs1'],
|
|
1442
|
+
fontStyleIds: ['vf1'],
|
|
1443
|
+
childrenSkuIds: [],
|
|
1444
|
+
name: 'Thunder Text Variable'
|
|
1445
|
+
},
|
|
1446
|
+
'pkg-upright-sku': {
|
|
1447
|
+
fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5'],
|
|
1448
|
+
fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5'],
|
|
1449
|
+
childrenSkuIds: [],
|
|
1450
|
+
name: 'Package Text'
|
|
1451
|
+
},
|
|
1452
|
+
'pkg-italic-sku': {
|
|
1453
|
+
fontStyleSkuIds: ['si1', 'si2', 'si3', 'si4', 'si5'],
|
|
1454
|
+
fontStyleIds: ['fi1', 'fi2', 'fi3', 'fi4', 'fi5'],
|
|
1455
|
+
childrenSkuIds: [],
|
|
1456
|
+
name: 'Package Text Italic'
|
|
1457
|
+
}
|
|
1458
|
+
},
|
|
1459
|
+
skuPrices: {
|
|
1460
|
+
'coll-bundle-sku': 500,
|
|
1461
|
+
'family-sku': 500,
|
|
1462
|
+
'var-sku': 300,
|
|
1463
|
+
'pkg-upright-sku': 250,
|
|
1464
|
+
'pkg-italic-sku': 250,
|
|
1465
|
+
s1: 100,
|
|
1466
|
+
s2: 100,
|
|
1467
|
+
s3: 100,
|
|
1468
|
+
s4: 100,
|
|
1469
|
+
s5: 100,
|
|
1470
|
+
si1: 100,
|
|
1471
|
+
si2: 100,
|
|
1472
|
+
si3: 100,
|
|
1473
|
+
si4: 100,
|
|
1474
|
+
si5: 100,
|
|
1475
|
+
vs1: 100
|
|
1476
|
+
},
|
|
1477
|
+
selectedSkuIds: {
|
|
1478
|
+
'pkg-upright-sku': true
|
|
1479
|
+
}
|
|
1480
|
+
});
|
|
1481
|
+
const newState = (0, _reducer.default)(state, {
|
|
1482
|
+
type: 'SELECT_SKU_ID',
|
|
1483
|
+
skuId: 'pkg-italic-sku',
|
|
1484
|
+
selected: true
|
|
1485
|
+
});
|
|
1486
|
+
|
|
1487
|
+
// Should transitively upgrade: bundle → family → collection bundle
|
|
1488
|
+
(0, _vitest.expect)(newState.selectedSkuIds['coll-bundle-sku']).toBe(true);
|
|
1489
|
+
(0, _vitest.expect)(newState.selectedSkuIds['family-sku']).toBe(false);
|
|
1490
|
+
(0, _vitest.expect)(newState.selectedSkuIds['pkg-upright-sku']).toBe(false);
|
|
1491
|
+
(0, _vitest.expect)(newState.selectedSkuIds['pkg-italic-sku']).toBe(false);
|
|
1492
|
+
|
|
1493
|
+
// All items should report as selected via isSelected
|
|
1494
|
+
(0, _vitest.expect)((0, _utils.isSelected)('family-sku')(newState)).toBe(true);
|
|
1495
|
+
(0, _vitest.expect)((0, _utils.isSelected)('pkg-upright-sku')(newState)).toBe(true);
|
|
1496
|
+
(0, _vitest.expect)((0, _utils.isSelected)('pkg-italic-sku')(newState)).toBe(true);
|
|
1497
|
+
(0, _vitest.expect)((0, _utils.isSelected)('s1')(newState)).toBe(true);
|
|
1498
|
+
(0, _vitest.expect)((0, _utils.isSelected)('vs1')(newState)).toBe(true);
|
|
1499
|
+
});
|
|
1500
|
+
(0, _vitest.it)('prefers collection bundle over family when family has duplicate fontStyleSkuIds from bundles', () => {
|
|
1501
|
+
// In production data, flattenSkuData produces a family entry whose
|
|
1502
|
+
// fontStyleSkuIds includes BOTH the family's own styles AND the
|
|
1503
|
+
// overlapping styles from its child bundles, creating duplicates.
|
|
1504
|
+
// The tie-breaker must use non-self preference (not raw array length)
|
|
1505
|
+
// to avoid the family's inflated count beating the collection bundle.
|
|
1506
|
+
const state = makeState({
|
|
1507
|
+
collectionStyleSkus: {
|
|
1508
|
+
'family-sku': {
|
|
1509
|
+
// 10 unique styles, but listed 20 times (family's own + bundle overlap)
|
|
1510
|
+
fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5', 'si1', 'si2', 'si3', 'si4', 'si5', 's1', 's2', 's3', 's4', 's5', 'si1', 'si2', 'si3', 'si4', 'si5'],
|
|
1511
|
+
fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5', 'fi1', 'fi2', 'fi3', 'fi4', 'fi5'],
|
|
1512
|
+
childrenSkuIds: ['pkg-upright-sku', 'pkg-italic-sku'],
|
|
1513
|
+
name: 'Thunder Caption Family'
|
|
1514
|
+
},
|
|
1515
|
+
'coll-bundle-sku': {
|
|
1516
|
+
// 12 unique styles (family 10 + variable 2)
|
|
1517
|
+
fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5', 'si1', 'si2', 'si3', 'si4', 'si5', 'vs1', 'vs2'],
|
|
1518
|
+
fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5', 'fi1', 'fi2', 'fi3', 'fi4', 'fi5', 'vf1', 'vf2'],
|
|
1519
|
+
childrenSkuIds: ['family-sku', 'var-sku'],
|
|
1520
|
+
name: 'Thunder Caption Family + Variable'
|
|
1521
|
+
},
|
|
1522
|
+
'var-sku': {
|
|
1523
|
+
fontStyleSkuIds: ['vs1', 'vs2'],
|
|
1524
|
+
fontStyleIds: ['vf1', 'vf2'],
|
|
1525
|
+
childrenSkuIds: [],
|
|
1526
|
+
name: 'Thunder Caption Variable'
|
|
1527
|
+
},
|
|
1528
|
+
'pkg-upright-sku': {
|
|
1529
|
+
fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5'],
|
|
1530
|
+
fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5'],
|
|
1531
|
+
childrenSkuIds: [],
|
|
1532
|
+
name: 'Package Caption'
|
|
1533
|
+
},
|
|
1534
|
+
'pkg-italic-sku': {
|
|
1535
|
+
fontStyleSkuIds: ['si1', 'si2', 'si3', 'si4', 'si5'],
|
|
1536
|
+
fontStyleIds: ['fi1', 'fi2', 'fi3', 'fi4', 'fi5'],
|
|
1537
|
+
childrenSkuIds: [],
|
|
1538
|
+
name: 'Package Caption Italic'
|
|
1539
|
+
}
|
|
1540
|
+
},
|
|
1541
|
+
skuPrices: {
|
|
1542
|
+
'family-sku': 500,
|
|
1543
|
+
'coll-bundle-sku': 500,
|
|
1544
|
+
'var-sku': 500,
|
|
1545
|
+
'pkg-upright-sku': 300,
|
|
1546
|
+
'pkg-italic-sku': 300,
|
|
1547
|
+
s1: 100,
|
|
1548
|
+
s2: 100,
|
|
1549
|
+
s3: 100,
|
|
1550
|
+
s4: 100,
|
|
1551
|
+
s5: 100,
|
|
1552
|
+
si1: 100,
|
|
1553
|
+
si2: 100,
|
|
1554
|
+
si3: 100,
|
|
1555
|
+
si4: 100,
|
|
1556
|
+
si5: 100,
|
|
1557
|
+
vs1: 100,
|
|
1558
|
+
vs2: 100
|
|
1559
|
+
}
|
|
1560
|
+
});
|
|
1561
|
+
|
|
1562
|
+
// family-sku fontStyleSkuIds.length is 20 (duplicates), coll-bundle is 12.
|
|
1563
|
+
// A naive .length comparison would pick the family. The non-self
|
|
1564
|
+
// preference must win so the collection bundle is auto-selected.
|
|
1565
|
+
const [collId] = (0, _reducer.collectionSkuIdWithDiscount)(state, 'family-sku');
|
|
1566
|
+
(0, _vitest.expect)(collId).toBe('coll-bundle-sku');
|
|
1567
|
+
});
|
|
1568
|
+
(0, _vitest.it)('transitive upgrade prefers the collection with the most font styles', () => {
|
|
1569
|
+
// When two collections have the same price difference, prefer the one
|
|
1570
|
+
// covering more font styles
|
|
1571
|
+
const state = makeFamilyWithBundlesState({
|
|
1572
|
+
collectionStyleSkus: {
|
|
1573
|
+
'small-bundle-sku': {
|
|
1574
|
+
fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5', 'si1', 'si2', 'si3', 'si4', 'si5'],
|
|
1575
|
+
fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5', 'fi1', 'fi2', 'fi3', 'fi4', 'fi5'],
|
|
1576
|
+
childrenSkuIds: ['family-sku'],
|
|
1577
|
+
name: 'Small Bundle'
|
|
1578
|
+
},
|
|
1579
|
+
'big-bundle-sku': {
|
|
1580
|
+
fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5', 'si1', 'si2', 'si3', 'si4', 'si5', 'vs1'],
|
|
1581
|
+
fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5', 'fi1', 'fi2', 'fi3', 'fi4', 'fi5', 'vf1'],
|
|
1582
|
+
childrenSkuIds: ['family-sku', 'var-sku'],
|
|
1583
|
+
name: 'Big Bundle'
|
|
1584
|
+
},
|
|
1585
|
+
'family-sku': {
|
|
1586
|
+
fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5', 'si1', 'si2', 'si3', 'si4', 'si5'],
|
|
1587
|
+
fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5', 'fi1', 'fi2', 'fi3', 'fi4', 'fi5'],
|
|
1588
|
+
childrenSkuIds: ['pkg-upright-sku', 'pkg-italic-sku'],
|
|
1589
|
+
name: 'Thunder Text Family'
|
|
1590
|
+
},
|
|
1591
|
+
'var-sku': {
|
|
1592
|
+
fontStyleSkuIds: ['vs1'],
|
|
1593
|
+
fontStyleIds: ['vf1'],
|
|
1594
|
+
childrenSkuIds: [],
|
|
1595
|
+
name: 'Variable'
|
|
1596
|
+
},
|
|
1597
|
+
'pkg-upright-sku': {
|
|
1598
|
+
fontStyleSkuIds: ['s1', 's2', 's3', 's4', 's5'],
|
|
1599
|
+
fontStyleIds: ['f1', 'f2', 'f3', 'f4', 'f5'],
|
|
1600
|
+
childrenSkuIds: [],
|
|
1601
|
+
name: 'Package Text'
|
|
1602
|
+
},
|
|
1603
|
+
'pkg-italic-sku': {
|
|
1604
|
+
fontStyleSkuIds: ['si1', 'si2', 'si3', 'si4', 'si5'],
|
|
1605
|
+
fontStyleIds: ['fi1', 'fi2', 'fi3', 'fi4', 'fi5'],
|
|
1606
|
+
childrenSkuIds: [],
|
|
1607
|
+
name: 'Package Text Italic'
|
|
1608
|
+
}
|
|
1609
|
+
},
|
|
1610
|
+
skuPrices: {
|
|
1611
|
+
'small-bundle-sku': 500,
|
|
1612
|
+
'big-bundle-sku': 500,
|
|
1613
|
+
'family-sku': 500,
|
|
1614
|
+
'var-sku': 300,
|
|
1615
|
+
'pkg-upright-sku': 250,
|
|
1616
|
+
'pkg-italic-sku': 250,
|
|
1617
|
+
s1: 100,
|
|
1618
|
+
s2: 100,
|
|
1619
|
+
s3: 100,
|
|
1620
|
+
s4: 100,
|
|
1621
|
+
s5: 100,
|
|
1622
|
+
si1: 100,
|
|
1623
|
+
si2: 100,
|
|
1624
|
+
si3: 100,
|
|
1625
|
+
si4: 100,
|
|
1626
|
+
si5: 100,
|
|
1627
|
+
vs1: 100
|
|
1628
|
+
}
|
|
1629
|
+
});
|
|
1630
|
+
const [collId] = (0, _reducer.collectionSkuIdWithDiscount)(state, 'family-sku');
|
|
1631
|
+
// Both bundles have the same price, but big-bundle has more styles
|
|
1632
|
+
(0, _vitest.expect)(collId).toBe('big-bundle-sku');
|
|
1633
|
+
});
|
|
1181
1634
|
});
|
package/dist/reducer.js
CHANGED
|
@@ -82,17 +82,27 @@ const collectionSkuIdWithDiscount = (state, skuId) => {
|
|
|
82
82
|
const differences = (0, _utils.collectionSkuIdsDifferences)(state, skuId);
|
|
83
83
|
const collectionId = Object.keys(differences).filter(collSkuId => differences[collSkuId] <= state.skuPrices[skuId]).reduce((smallest, key) => {
|
|
84
84
|
if (!smallest || differences[key] < differences[smallest]) return key;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
if (differences[key] === differences[smallest]) {
|
|
86
|
+
// When differences are equal, prefer a collection that is not the
|
|
87
|
+
// SKU being selected — this ensures collection bundles are preferred
|
|
88
|
+
// over selecting the family itself at the same price
|
|
89
|
+
if (smallest === skuId && key !== skuId) return key;
|
|
90
|
+
// Among non-self candidates, prefer the one covering more unique
|
|
91
|
+
// font styles — this ensures the most comprehensive collection wins
|
|
92
|
+
if (smallest !== skuId && key !== skuId) {
|
|
93
|
+
var _state$collectionStyl, _state$collectionStyl2;
|
|
94
|
+
const smallestStyles = new Set((_state$collectionStyl = state.collectionStyleSkus[smallest]) === null || _state$collectionStyl === void 0 ? void 0 : _state$collectionStyl.fontStyleSkuIds).size;
|
|
95
|
+
const keyStyles = new Set((_state$collectionStyl2 = state.collectionStyleSkus[key]) === null || _state$collectionStyl2 === void 0 ? void 0 : _state$collectionStyl2.fontStyleSkuIds).size;
|
|
96
|
+
if (keyStyles > smallestStyles) return key;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
89
99
|
return smallest;
|
|
90
100
|
}, null);
|
|
91
101
|
if (collectionId) return [collectionId, differences[collectionId]];
|
|
92
102
|
return [null, null];
|
|
93
103
|
};
|
|
94
104
|
exports.collectionSkuIdWithDiscount = collectionSkuIdWithDiscount;
|
|
95
|
-
const selectedCollectionSkuIdContainingSkuId = (state, skuId) => Object.keys(state.selectedSkuIds).filter(id => state.selectedSkuIds[id] === true).find(selectedSkuId => (0, _utils.
|
|
105
|
+
const selectedCollectionSkuIdContainingSkuId = (state, skuId) => Object.keys(state.selectedSkuIds).filter(id => state.selectedSkuIds[id] === true).find(selectedSkuId => (0, _utils.collTransitivelyContainsSkuId)(state.collectionStyleSkus, selectedSkuId, skuId));
|
|
96
106
|
const setKeysFalse = (acc, key) => {
|
|
97
107
|
acc[key] = false;
|
|
98
108
|
return acc;
|
|
@@ -104,12 +114,24 @@ const unselectedMembersSkuIds = (state, action) => {
|
|
|
104
114
|
selected
|
|
105
115
|
} = action;
|
|
106
116
|
if (!selected) return {}; // only do this in case we are selecting something
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
117
|
+
|
|
118
|
+
// Recursively collect all descendant SKU IDs so that transitive
|
|
119
|
+
// selections (e.g., collection bundle → family → bundle) are all
|
|
120
|
+
// marked as false
|
|
121
|
+
const result = {};
|
|
122
|
+
const collectMembers = collSkuId => {
|
|
123
|
+
const members = state.collectionStyleSkus[collSkuId];
|
|
124
|
+
if (!members) return;
|
|
125
|
+
for (const id of members.fontStyleSkuIds) {
|
|
126
|
+
result[id] = false;
|
|
127
|
+
}
|
|
128
|
+
for (const id of members.childrenSkuIds) {
|
|
129
|
+
result[id] = false;
|
|
130
|
+
collectMembers(id);
|
|
131
|
+
}
|
|
112
132
|
};
|
|
133
|
+
collectMembers(skuId);
|
|
134
|
+
return result;
|
|
113
135
|
};
|
|
114
136
|
const selectSkuId = (state, action) => {
|
|
115
137
|
if (action.type !== 'SELECT_SKU_ID') return state;
|
package/dist/utils.d.ts
CHANGED
|
@@ -4,6 +4,9 @@ export declare const pluralize: (count: number, singular: string, plural?: strin
|
|
|
4
4
|
export declare function notEmpty<TValue>(value: TValue | null | undefined): value is TValue;
|
|
5
5
|
export declare function kebabToCamel(str: string): string;
|
|
6
6
|
export declare const collContainsSkuId: (coll: CollectionStyleSkus | undefined, skuIdOrStyleId: string) => boolean;
|
|
7
|
+
export declare const collTransitivelyContainsSkuId: (collectionStyleSkus: {
|
|
8
|
+
[key: string]: CollectionStyleSkus | undefined;
|
|
9
|
+
}, collSkuId: string, skuIdOrStyleId: string) => boolean;
|
|
7
10
|
interface CollectionSkuIdsDifferences {
|
|
8
11
|
[collectionSkuId: string]: number;
|
|
9
12
|
}
|
package/dist/utils.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.compareVariableSettings = exports.collectionSkuIdsDifferences = exports.collContainsSkuId = void 0;
|
|
6
|
+
exports.compareVariableSettings = exports.collectionSkuIdsDifferences = exports.collTransitivelyContainsSkuId = exports.collContainsSkuId = void 0;
|
|
7
7
|
exports.getFeatureGlyphs = getFeatureGlyphs;
|
|
8
8
|
exports.isSelected = exports.inspect = void 0;
|
|
9
9
|
exports.kebabToCamel = kebabToCamel;
|
|
@@ -29,12 +29,25 @@ const collContainsSkuId = (coll, skuIdOrStyleId) => {
|
|
|
29
29
|
if (!coll) return false;
|
|
30
30
|
return coll.childrenSkuIds.indexOf(skuIdOrStyleId) >= 0 || coll.fontStyleSkuIds.indexOf(skuIdOrStyleId) >= 0 || coll.fontStyleIds.indexOf(skuIdOrStyleId) >= 0;
|
|
31
31
|
};
|
|
32
|
+
|
|
33
|
+
// Transitive containment check — walks the collection hierarchy to find
|
|
34
|
+
// if a SKU is contained at any depth (e.g., collection bundle → family → bundle)
|
|
32
35
|
exports.collContainsSkuId = collContainsSkuId;
|
|
36
|
+
const collTransitivelyContainsSkuId = (collectionStyleSkus, collSkuId, skuIdOrStyleId) => {
|
|
37
|
+
const coll = collectionStyleSkus[collSkuId];
|
|
38
|
+
if (!coll) return false;
|
|
39
|
+
if (collContainsSkuId(coll, skuIdOrStyleId)) return true;
|
|
40
|
+
return coll.childrenSkuIds.some(childSkuId => collTransitivelyContainsSkuId(collectionStyleSkus, childSkuId, skuIdOrStyleId));
|
|
41
|
+
};
|
|
42
|
+
exports.collTransitivelyContainsSkuId = collTransitivelyContainsSkuId;
|
|
33
43
|
const isSelected = skuIdOrStyleId => state => {
|
|
34
44
|
if (!skuIdOrStyleId) return false;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
if (state.selectedSkuIds[skuIdOrStyleId] === true) return true;
|
|
46
|
+
// get the collections that contain this skuId as a fontStyle or child
|
|
47
|
+
const parentCollectionSkuIds = Object.keys(state.collectionStyleSkus).filter(collSkuId => collContainsSkuId(state.collectionStyleSkus[collSkuId], skuIdOrStyleId));
|
|
48
|
+
// Recursively check ancestors — handles transitive selection
|
|
49
|
+
// (e.g., collection bundle → family → bundle)
|
|
50
|
+
return parentCollectionSkuIds.some(parentSkuId => isSelected(parentSkuId)(state));
|
|
38
51
|
};
|
|
39
52
|
|
|
40
53
|
// find any collection ancestors that contain this skuId,
|
|
@@ -42,11 +55,9 @@ const isSelected = skuIdOrStyleId => state => {
|
|
|
42
55
|
// and return the delta between the collection price and the sum.
|
|
43
56
|
exports.isSelected = isSelected;
|
|
44
57
|
const collectionSkuIdsDifferences = (state, skuId) => Object.keys(state.collectionStyleSkus).filter(collectionSkuId => {
|
|
45
|
-
|
|
46
|
-
return skuId === collectionSkuId || collContainsSkuId(coll, skuId);
|
|
58
|
+
return skuId === collectionSkuId || collTransitivelyContainsSkuId(state.collectionStyleSkus, collectionSkuId, skuId);
|
|
47
59
|
}).reduce((acc, collectionSkuId) => {
|
|
48
|
-
const
|
|
49
|
-
const selectedSkuIdsInColl = Object.keys(state.selectedSkuIds).filter(selectedSkuId => state.selectedSkuIds[selectedSkuId] === true && collContainsSkuId(coll, selectedSkuId));
|
|
60
|
+
const selectedSkuIdsInColl = Object.keys(state.selectedSkuIds).filter(selectedSkuId => state.selectedSkuIds[selectedSkuId] === true && collTransitivelyContainsSkuId(state.collectionStyleSkus, collectionSkuId, selectedSkuId));
|
|
50
61
|
const sumOfSelectedSkuIdsInColl = selectedSkuIdsInColl.map(selectedSkuId => state.skuPrices[selectedSkuId] ?? 0).reduce((acc, price) => acc + price, 0);
|
|
51
62
|
acc[collectionSkuId] = (state.skuPrices[collectionSkuId] ?? 0) - sumOfSelectedSkuIdsInColl;
|
|
52
63
|
return acc;
|