@akinon/pz-theme 2.0.103-rc.0 → 2.0.103
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 +8 -33
- package/package.json +4 -4
- package/src/__tests__/collection-row-scope.test.tsx +187 -0
- package/src/__tests__/group-row-styles.test.tsx +110 -0
- package/src/__tests__/theme-typography-vars.test.tsx +125 -0
- package/src/blocks/__tests__/listing-row-swatches.test.ts +111 -0
- package/src/blocks/group-block.tsx +35 -1
- package/src/blocks/item-template-utils.ts +34 -0
- package/src/blocks/item-template.tsx +6 -1
- package/src/blocks/listing-context.ts +17 -622
- package/src/blocks/listing-row.ts +692 -0
- package/src/components/with-designer-features.tsx +6 -0
- package/src/sections/__tests__/posts-slider-row-visibility.test.tsx +147 -0
- package/src/sections/posts-slider-section.tsx +23 -2
- package/src/theme-block.tsx +63 -2
- package/src/theme-placeholder-client.tsx +4 -3
- package/src/theme-placeholder-wrapper.tsx +61 -5
- package/src/utils/__tests__/property-switches.test.tsx +250 -0
- package/src/utils/index.ts +55 -3
- package/src/utils/iterator-price-shapes.test.ts +465 -0
- package/src/utils/iterator-utils.ts +371 -35
- package/src/utils/property-switches.ts +230 -0
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
// Type-only: the CSS generator imports the binding-token guard from here, and
|
|
2
2
|
// a value import of the block module would close that loop at runtime.
|
|
3
3
|
import type { Block } from '../theme-block';
|
|
4
|
+
import {
|
|
5
|
+
publishesProductRows,
|
|
6
|
+
resolveCollectionRowProduct
|
|
7
|
+
} from '../blocks/item-template-utils';
|
|
8
|
+
import type {
|
|
9
|
+
MoneyCurrencyDisplay,
|
|
10
|
+
MoneyCurrencyPosition,
|
|
11
|
+
MoneyDecimals,
|
|
12
|
+
MoneyStyleOptions
|
|
13
|
+
} from '../blocks/commerce-format';
|
|
14
|
+
import {
|
|
15
|
+
formatListingPrice,
|
|
16
|
+
normalizeListingProduct,
|
|
17
|
+
parseListingLookups,
|
|
18
|
+
type ListingValueLookups
|
|
19
|
+
} from '../blocks/listing-row';
|
|
4
20
|
import { devWarn } from './dev-warn';
|
|
5
21
|
import { isMoneyPath } from './field-semantics';
|
|
6
22
|
import { isVisibleForItem, readRowVisibility } from './row-visibility';
|
|
@@ -520,8 +536,58 @@ export const getPriceWithCurrency = (
|
|
|
520
536
|
return `${amount} ${currencyLabel}`;
|
|
521
537
|
};
|
|
522
538
|
|
|
539
|
+
/**
|
|
540
|
+
* Which part of the row's price a text bound to a price path prints.
|
|
541
|
+
*
|
|
542
|
+
* `full` is what every published text prints: the list price struck through
|
|
543
|
+
* when there is a discount, then the current price, as one value. A card that
|
|
544
|
+
* wants to colour the struck price on its own — the ask that named this — has
|
|
545
|
+
* to be able to put the two in two texts, so `current` and `retail` print one
|
|
546
|
+
* each and nothing when that figure is absent or zero.
|
|
547
|
+
*/
|
|
548
|
+
export type PricePart = 'full' | 'current' | 'retail';
|
|
549
|
+
|
|
550
|
+
const PRICE_PARTS: PricePart[] = ['full', 'current', 'retail'];
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* How a row's money is written. `money` is the brand's format as the product
|
|
554
|
+
* grid reads it off its own properties; absent, a bound price prints the way
|
|
555
|
+
* it always has (`getPriceWithCurrency`, "3.499 TL").
|
|
556
|
+
*/
|
|
557
|
+
export interface RowMoneyFormat {
|
|
558
|
+
money?: MoneyStyleOptions;
|
|
559
|
+
locale?: string;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** A property as the editor stores it: a scalar or a per-breakpoint object. */
|
|
563
|
+
const readDesktopProperty = (value: unknown): unknown => {
|
|
564
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
565
|
+
return value;
|
|
566
|
+
}
|
|
567
|
+
const record = value as Record<string, unknown>;
|
|
568
|
+
return record.desktop ?? record.mobile;
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
export const readPricePart = (block: Block): PricePart => {
|
|
572
|
+
const raw = String(readDesktopProperty(block.properties?.pricePart) ?? '')
|
|
573
|
+
.trim()
|
|
574
|
+
.toLowerCase();
|
|
575
|
+
return (PRICE_PARTS as string[]).includes(raw) ? (raw as PricePart) : 'full';
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
const formatRowPrice = (
|
|
579
|
+
raw: unknown,
|
|
580
|
+
currency: unknown,
|
|
581
|
+
format?: RowMoneyFormat
|
|
582
|
+
): string =>
|
|
583
|
+
format?.money
|
|
584
|
+
? formatListingPrice(raw, String(currency ?? ''), format.locale, format.money)
|
|
585
|
+
: getPriceWithCurrency(raw, currency);
|
|
586
|
+
|
|
523
587
|
const buildProductPriceHtml = (
|
|
524
|
-
productData: Record<string, unknown
|
|
588
|
+
productData: Record<string, unknown>,
|
|
589
|
+
part: PricePart = 'full',
|
|
590
|
+
format?: RowMoneyFormat
|
|
525
591
|
): string => {
|
|
526
592
|
const activePrice =
|
|
527
593
|
((productData as any)?.active_price as
|
|
@@ -538,8 +604,19 @@ const buildProductPriceHtml = (
|
|
|
538
604
|
const currentNumeric = parsePriceValue(currentRaw);
|
|
539
605
|
const retailNumeric = parsePriceValue(retailRaw);
|
|
540
606
|
|
|
541
|
-
|
|
542
|
-
|
|
607
|
+
if (part === 'current') {
|
|
608
|
+
return currentNumeric > 0
|
|
609
|
+
? escapeHtml(formatRowPrice(currentRaw, currency, format))
|
|
610
|
+
: '';
|
|
611
|
+
}
|
|
612
|
+
if (part === 'retail') {
|
|
613
|
+
return retailNumeric > 0
|
|
614
|
+
? escapeHtml(formatRowPrice(retailRaw, currency, format))
|
|
615
|
+
: '';
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const currentText = escapeHtml(formatRowPrice(currentRaw, currency, format));
|
|
619
|
+
const retailText = escapeHtml(formatRowPrice(retailRaw, currency, format));
|
|
543
620
|
|
|
544
621
|
if (retailNumeric > 0 && retailNumeric > currentNumeric) {
|
|
545
622
|
return `<span style="display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap;"><span style='color:#6b7280;text-decoration:line-through;font-weight:400;'>${retailText}</span><span style='font-weight:600;'>${currentText}</span></span>`;
|
|
@@ -605,7 +682,8 @@ const applyResolvedBinding = (
|
|
|
605
682
|
block: Block,
|
|
606
683
|
resolvedValue: unknown,
|
|
607
684
|
bindingPath: string,
|
|
608
|
-
moneyCurrency?: string
|
|
685
|
+
moneyCurrency?: string,
|
|
686
|
+
rowFormat?: RowMoneyFormat
|
|
609
687
|
): Block => {
|
|
610
688
|
const target = getItemBindingTarget(block);
|
|
611
689
|
const nextBlock: Block = {
|
|
@@ -620,7 +698,11 @@ const applyResolvedBinding = (
|
|
|
620
698
|
isPriceBinding(bindingPath) &&
|
|
621
699
|
isRecord(resolvedValue)
|
|
622
700
|
) {
|
|
623
|
-
nextBlock.value = buildProductPriceHtml(
|
|
701
|
+
nextBlock.value = buildProductPriceHtml(
|
|
702
|
+
resolvedValue,
|
|
703
|
+
readPricePart(block),
|
|
704
|
+
rowFormat
|
|
705
|
+
);
|
|
624
706
|
return nextBlock;
|
|
625
707
|
}
|
|
626
708
|
|
|
@@ -1129,7 +1211,9 @@ const replaceBlockValues = (
|
|
|
1129
1211
|
* swatch. A visibility rule is worse still: `hidden` is only ever set, never
|
|
1130
1212
|
* taken back, so one outer row decides every entry at once.
|
|
1131
1213
|
*/
|
|
1132
|
-
insideNestedRepeat = false
|
|
1214
|
+
insideNestedRepeat = false,
|
|
1215
|
+
/** The row's money format, from the template this row was built from. */
|
|
1216
|
+
rowFormat?: RowMoneyFormat
|
|
1133
1217
|
): Block => {
|
|
1134
1218
|
const nextBlock: Block = {
|
|
1135
1219
|
...blockToReplace,
|
|
@@ -1241,13 +1325,20 @@ const replaceBlockValues = (
|
|
|
1241
1325
|
|
|
1242
1326
|
if (interpolated !== undefined) {
|
|
1243
1327
|
return {
|
|
1244
|
-
...applyResolvedBinding(
|
|
1328
|
+
...applyResolvedBinding(
|
|
1329
|
+
nextBlock,
|
|
1330
|
+
interpolated,
|
|
1331
|
+
bindingPath,
|
|
1332
|
+
undefined,
|
|
1333
|
+
rowFormat
|
|
1334
|
+
),
|
|
1245
1335
|
blocks: nextBlock.blocks?.map((childBlock) =>
|
|
1246
1336
|
replaceBlockValues(
|
|
1247
1337
|
childBlock,
|
|
1248
1338
|
itemData,
|
|
1249
1339
|
itemIndex,
|
|
1250
|
-
childInsideNestedRepeat
|
|
1340
|
+
childInsideNestedRepeat,
|
|
1341
|
+
rowFormat
|
|
1251
1342
|
)
|
|
1252
1343
|
)
|
|
1253
1344
|
};
|
|
@@ -1256,29 +1347,46 @@ const replaceBlockValues = (
|
|
|
1256
1347
|
const itemBindingPath = stripItemPrefix(bindingPath);
|
|
1257
1348
|
const resolvedValue = getValueAtPath(itemData, itemBindingPath);
|
|
1258
1349
|
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1350
|
+
// A price is read off the whole row (list price, currency, discount), so
|
|
1351
|
+
// a row that is a plain value cannot answer one — and the row is asked
|
|
1352
|
+
// even when the bound path itself is absent. Omnitron's collection rows
|
|
1353
|
+
// (what the editor previews) carry the money under `active_price`, the
|
|
1354
|
+
// commerce rows the storefront fetches carry it flat; `readProductPricePair`
|
|
1355
|
+
// reads both, but it used to be reached only when `item.price` resolved
|
|
1356
|
+
// on its own, so a card bound to `item.price` previewed without prices.
|
|
1357
|
+
const answersPrice =
|
|
1358
|
+
nextBlock.type === 'text' &&
|
|
1359
|
+
bindingPath.startsWith('item.') &&
|
|
1360
|
+
isPriceBinding(bindingPath) &&
|
|
1361
|
+
isRecord(itemData) &&
|
|
1362
|
+
(resolvedValue !== undefined ||
|
|
1363
|
+
readProductPricePair(itemData).current > 0);
|
|
1364
|
+
|
|
1365
|
+
if (answersPrice) {
|
|
1366
|
+
nextBlock.value = buildProductPriceHtml(
|
|
1367
|
+
itemData,
|
|
1368
|
+
readPricePart(nextBlock),
|
|
1369
|
+
rowFormat
|
|
1370
|
+
);
|
|
1371
|
+
} else if (resolvedValue !== undefined) {
|
|
1372
|
+
return {
|
|
1373
|
+
...applyResolvedBinding(
|
|
1374
|
+
nextBlock,
|
|
1375
|
+
resolvedValue,
|
|
1376
|
+
bindingPath,
|
|
1377
|
+
undefined,
|
|
1378
|
+
rowFormat
|
|
1379
|
+
),
|
|
1380
|
+
blocks: nextBlock.blocks?.map((childBlock) =>
|
|
1381
|
+
replaceBlockValues(
|
|
1382
|
+
childBlock,
|
|
1383
|
+
itemData,
|
|
1384
|
+
itemIndex,
|
|
1385
|
+
childInsideNestedRepeat,
|
|
1386
|
+
rowFormat
|
|
1279
1387
|
)
|
|
1280
|
-
|
|
1281
|
-
}
|
|
1388
|
+
)
|
|
1389
|
+
};
|
|
1282
1390
|
}
|
|
1283
1391
|
}
|
|
1284
1392
|
} else if (isPlaceholderMode && nextBlock.type === 'image') {
|
|
@@ -1306,7 +1414,8 @@ const replaceBlockValues = (
|
|
|
1306
1414
|
childBlock,
|
|
1307
1415
|
itemData,
|
|
1308
1416
|
itemIndex,
|
|
1309
|
-
childInsideNestedRepeat
|
|
1417
|
+
childInsideNestedRepeat,
|
|
1418
|
+
rowFormat
|
|
1310
1419
|
)
|
|
1311
1420
|
);
|
|
1312
1421
|
}
|
|
@@ -1314,6 +1423,191 @@ const replaceBlockValues = (
|
|
|
1314
1423
|
return nextBlock;
|
|
1315
1424
|
};
|
|
1316
1425
|
|
|
1426
|
+
const MONEY_DECIMALS: MoneyDecimals[] = ['always', 'auto', 'never'];
|
|
1427
|
+
const MONEY_DISPLAYS: MoneyCurrencyDisplay[] = [
|
|
1428
|
+
'',
|
|
1429
|
+
'symbol',
|
|
1430
|
+
'code',
|
|
1431
|
+
'custom',
|
|
1432
|
+
'none'
|
|
1433
|
+
];
|
|
1434
|
+
const MONEY_POSITIONS: MoneyCurrencyPosition[] = ['', 'before', 'after'];
|
|
1435
|
+
|
|
1436
|
+
const readTemplateText = (template: Block, key: string): string => {
|
|
1437
|
+
const raw = readDesktopProperty(template.properties?.[key]);
|
|
1438
|
+
return typeof raw === 'string' ? raw : '';
|
|
1439
|
+
};
|
|
1440
|
+
|
|
1441
|
+
const pickOption = <T extends string>(raw: string, allowed: T[]): T | undefined =>
|
|
1442
|
+
(allowed as string[]).includes(raw) ? (raw as T) : undefined;
|
|
1443
|
+
|
|
1444
|
+
/**
|
|
1445
|
+
* The money format authored on a repeated row's template — the same four
|
|
1446
|
+
* properties the product grid and the PDP price block carry. Nothing authored
|
|
1447
|
+
* means no `money`, and every price on the row prints as it always has.
|
|
1448
|
+
*/
|
|
1449
|
+
export const readRowMoneyFormat = (template: Block): RowMoneyFormat => {
|
|
1450
|
+
const decimals = pickOption(
|
|
1451
|
+
readTemplateText(template, 'decimals').trim(),
|
|
1452
|
+
MONEY_DECIMALS
|
|
1453
|
+
);
|
|
1454
|
+
const currencyDisplay = pickOption(
|
|
1455
|
+
readTemplateText(template, 'currencyDisplay').trim(),
|
|
1456
|
+
MONEY_DISPLAYS
|
|
1457
|
+
);
|
|
1458
|
+
const currencyPosition = pickOption(
|
|
1459
|
+
readTemplateText(template, 'currencyPosition').trim(),
|
|
1460
|
+
MONEY_POSITIONS
|
|
1461
|
+
);
|
|
1462
|
+
const currencyText = readTemplateText(template, 'currencyText').trim();
|
|
1463
|
+
|
|
1464
|
+
const authored =
|
|
1465
|
+
decimals !== undefined ||
|
|
1466
|
+
(currencyDisplay !== undefined && currencyDisplay !== '') ||
|
|
1467
|
+
(currencyPosition !== undefined && currencyPosition !== '') ||
|
|
1468
|
+
currencyText !== '';
|
|
1469
|
+
|
|
1470
|
+
return {
|
|
1471
|
+
locale: bindingLocale,
|
|
1472
|
+
...(authored
|
|
1473
|
+
? {
|
|
1474
|
+
money: {
|
|
1475
|
+
...(decimals ? { decimals } : {}),
|
|
1476
|
+
...(currencyDisplay ? { currencyDisplay } : {}),
|
|
1477
|
+
...(currencyText ? { currencyText } : {}),
|
|
1478
|
+
...(currencyPosition ? { currencyPosition } : {})
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
: {})
|
|
1482
|
+
};
|
|
1483
|
+
};
|
|
1484
|
+
|
|
1485
|
+
/** The badge and swatch tables authored on the template, as the grid's. */
|
|
1486
|
+
export const readRowLookups = (
|
|
1487
|
+
template: Block
|
|
1488
|
+
): ListingValueLookups | undefined =>
|
|
1489
|
+
parseListingLookups(
|
|
1490
|
+
readTemplateText(template, 'badgeAttribute').trim(),
|
|
1491
|
+
readTemplateText(template, 'badgeMap'),
|
|
1492
|
+
readTemplateText(template, 'swatchMap')
|
|
1493
|
+
);
|
|
1494
|
+
|
|
1495
|
+
/**
|
|
1496
|
+
* A collection row published in the listing row's shape as well as its own.
|
|
1497
|
+
*
|
|
1498
|
+
* The editor previews a collection with Omnitron item rows (money under
|
|
1499
|
+
* `active_price`, no `absolute_url`); the storefront fetches commerce rows
|
|
1500
|
+
* (money flat). Neither carries what a card designed against a product-grid
|
|
1501
|
+
* row binds: `url`, `image`, `retailPrice`, `hasDiscount`, `discountPercent`,
|
|
1502
|
+
* `inStock`, `attributes` keyed, `colors[]`, `badge`. The listing normaliser
|
|
1503
|
+
* derives all of those from either shape once the money is flat, so the row is
|
|
1504
|
+
* flattened, normalised, and the normalised fields are added where the row had
|
|
1505
|
+
* none. Nothing the row already carried is replaced — `item.price` still reads
|
|
1506
|
+
* the raw figure the price pair is built from, and every binding a published
|
|
1507
|
+
* collection section has resolves exactly as before.
|
|
1508
|
+
*
|
|
1509
|
+
* Without an authored money format the normaliser would print the list price
|
|
1510
|
+
* in the locale's own form ("₺3.999,00") beside a current price the pair
|
|
1511
|
+
* prints as "3.499 TL"; the list price is therefore written the way the pair
|
|
1512
|
+
* writes it, so the two halves of one card agree.
|
|
1513
|
+
*/
|
|
1514
|
+
export const normalizeCollectionRow = (
|
|
1515
|
+
item: Record<string, unknown>,
|
|
1516
|
+
index: number,
|
|
1517
|
+
options: { lookups?: ListingValueLookups; format?: RowMoneyFormat } = {}
|
|
1518
|
+
): Record<string, unknown> => {
|
|
1519
|
+
if (!isRecord(item) || Object.keys(item).length === 0) return item;
|
|
1520
|
+
|
|
1521
|
+
const activePrice = isRecord(item.active_price) ? item.active_price : null;
|
|
1522
|
+
const flat: Record<string, unknown> = { ...item };
|
|
1523
|
+
if (activePrice) {
|
|
1524
|
+
if (flat.price === undefined) flat.price = activePrice.price;
|
|
1525
|
+
if (flat.retail_price === undefined) {
|
|
1526
|
+
flat.retail_price = activePrice.retail_price;
|
|
1527
|
+
}
|
|
1528
|
+
if (flat.currency_type === undefined && flat.currency === undefined) {
|
|
1529
|
+
flat.currency_type = activePrice.currency_type;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
const currency = readProductCurrency(flat);
|
|
1534
|
+
const normalized = normalizeListingProduct(flat, index, {
|
|
1535
|
+
currency: typeof currency === 'string' ? currency : undefined,
|
|
1536
|
+
locale: options.format?.locale,
|
|
1537
|
+
money: options.format?.money,
|
|
1538
|
+
lookups: options.lookups
|
|
1539
|
+
});
|
|
1540
|
+
if (!normalized) return flat;
|
|
1541
|
+
|
|
1542
|
+
// Only fields the row lacks, and only fields that say something: the
|
|
1543
|
+
// normaliser answers a missing figure with '' (a formatted nothing), and
|
|
1544
|
+
// writing that onto the row would turn "no price" into a price of nothing —
|
|
1545
|
+
// the pair reader prints "0 TL" for an empty string it is handed.
|
|
1546
|
+
const row: Record<string, unknown> = { ...flat };
|
|
1547
|
+
for (const [key, value] of Object.entries(normalized)) {
|
|
1548
|
+
if (value !== undefined && value !== '' && row[key] === undefined) {
|
|
1549
|
+
row[key] = value;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// The listing normaliser reads `image` off the page-context row, which has
|
|
1554
|
+
// already been flattened; a raw row keeps its pictures in `productimage_set`,
|
|
1555
|
+
// which the normaliser publishes as `images` — so the first of those is the
|
|
1556
|
+
// picture, the way the page context would have made it.
|
|
1557
|
+
if (
|
|
1558
|
+
(row.image === undefined || row.image === '') &&
|
|
1559
|
+
Array.isArray(normalized.images) &&
|
|
1560
|
+
typeof normalized.images[0] === 'string'
|
|
1561
|
+
) {
|
|
1562
|
+
row.image = normalized.images[0];
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
if (!options.format?.money) {
|
|
1566
|
+
row.retailPrice =
|
|
1567
|
+
parsePriceValue(flat.retail_price) > 0
|
|
1568
|
+
? getPriceWithCurrency(flat.retail_price, currency)
|
|
1569
|
+
: '';
|
|
1570
|
+
if (normalized.previousPrice !== undefined) {
|
|
1571
|
+
const previous = isRecord(flat.price_extra_field)
|
|
1572
|
+
? flat.price_extra_field.previous_price
|
|
1573
|
+
: undefined;
|
|
1574
|
+
row.previousPrice = getPriceWithCurrency(previous, currency);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
return row;
|
|
1579
|
+
};
|
|
1580
|
+
|
|
1581
|
+
/**
|
|
1582
|
+
* Expands the repeats INSIDE an already-built row.
|
|
1583
|
+
*
|
|
1584
|
+
* A functional list (the product grid's ItemTemplateList) expands its row
|
|
1585
|
+
* template itself and renders the clones; the section renderer's walk, which
|
|
1586
|
+
* expands a nested repeat after the repeat around it, never sees them. So a
|
|
1587
|
+
* colour-swatch strip inside a grid card was handed its colours
|
|
1588
|
+
* (`iteratorScopedItems`) and then drawn unexpanded. Each clone is walked here
|
|
1589
|
+
* the way the section walks its own: a nested repeat expands against the list
|
|
1590
|
+
* its row handed it, and its children are walked in turn.
|
|
1591
|
+
*/
|
|
1592
|
+
export const expandNestedIterators = (
|
|
1593
|
+
block: Block,
|
|
1594
|
+
options: { isDesigner?: boolean } = {}
|
|
1595
|
+
): Block => {
|
|
1596
|
+
if (!block.blocks || block.blocks.length === 0) return block;
|
|
1597
|
+
|
|
1598
|
+
let changed = false;
|
|
1599
|
+
const blocks = block.blocks.map((child) => {
|
|
1600
|
+
const expanded = child.isIterator
|
|
1601
|
+
? buildIteratorBlock({ iteratorBlock: child, isDesigner: options.isDesigner })
|
|
1602
|
+
: child;
|
|
1603
|
+
const deep = expandNestedIterators(expanded, options);
|
|
1604
|
+
if (deep !== child) changed = true;
|
|
1605
|
+
return deep;
|
|
1606
|
+
});
|
|
1607
|
+
|
|
1608
|
+
return changed ? { ...block, blocks } : block;
|
|
1609
|
+
};
|
|
1610
|
+
|
|
1317
1611
|
export const buildIteratorBlock = ({
|
|
1318
1612
|
iteratorBlock,
|
|
1319
1613
|
sectionDataSource,
|
|
@@ -1363,18 +1657,51 @@ export const buildIteratorBlock = ({
|
|
|
1363
1657
|
// its row's list is neither: the row has already answered, and an empty
|
|
1364
1658
|
// answer means this product has no colours — one empty swatch next to
|
|
1365
1659
|
// products that do have colours reads as a colour the shopper can pick.
|
|
1660
|
+
//
|
|
1661
|
+
// Except on the canvas. The editor previews a collection with Omnitron's
|
|
1662
|
+
// item rows, which carry no colour variants at all, so a swatch strip in
|
|
1663
|
+
// that card answered "no colours" for every product and the designer had
|
|
1664
|
+
// nothing to arrange or hover — the strip looked broken rather than empty.
|
|
1665
|
+
// The canvas draws the count floor there too; the storefront still draws
|
|
1666
|
+
// nothing for a product without colours.
|
|
1366
1667
|
const actualCount =
|
|
1367
1668
|
displayItems.length > 0
|
|
1368
1669
|
? displayItems.length
|
|
1369
|
-
: scopedItems
|
|
1670
|
+
: scopedItems && !isDesigner
|
|
1370
1671
|
? 0
|
|
1371
1672
|
: Math.max(iteratorCount, 1);
|
|
1372
1673
|
|
|
1674
|
+
// Only a repeat that read its rows off a collection publishes them as
|
|
1675
|
+
// products; a caller that brought its own items (a product grid's
|
|
1676
|
+
// ItemTemplateList) opens the scope itself and must not get a second one.
|
|
1677
|
+
// …and not a repeat INSIDE a row: a swatch strip handed its product's
|
|
1678
|
+
// colours draws colours, whatever the section's source publishes.
|
|
1679
|
+
const scopesProducts =
|
|
1680
|
+
providedItems === undefined &&
|
|
1681
|
+
scopedItems === null &&
|
|
1682
|
+
publishesProductRows(sectionDataSource);
|
|
1683
|
+
// The card's own money format and lookup tables, read off the template the
|
|
1684
|
+
// way the product grid reads them off itself — so a card that repeats over
|
|
1685
|
+
// a collection prints "3.499,00 TL" and fills `item.badge` / `item.colors[]`
|
|
1686
|
+
// exactly as the same card does inside a grid.
|
|
1687
|
+
const rowFormat = scopesProducts ? readRowMoneyFormat(template) : undefined;
|
|
1688
|
+
const rowLookups = scopesProducts ? readRowLookups(template) : undefined;
|
|
1689
|
+
|
|
1373
1690
|
const clonedBlocks = Array.from({ length: actualCount }, (_, index) => {
|
|
1374
1691
|
// `??`, not `||`: an entry of a list of plain values can be falsy (0, '')
|
|
1375
1692
|
// and is still the row, while a missing entry is the placeholder padding
|
|
1376
1693
|
// the count floor asked for.
|
|
1377
|
-
const
|
|
1694
|
+
const rawItem = displayItems[index] ?? {};
|
|
1695
|
+
// A collection row is a commerce product: it is published in the listing
|
|
1696
|
+
// row's shape too (`item.url`, `item.retailPrice`, `item.hasDiscount`,
|
|
1697
|
+
// `item.colors`…), on top of its own fields, so one card design binds over
|
|
1698
|
+
// a grid row and a collection row alike. See normalizeCollectionRow.
|
|
1699
|
+
const item = scopesProducts
|
|
1700
|
+
? normalizeCollectionRow(rawItem as Record<string, unknown>, index, {
|
|
1701
|
+
lookups: rowLookups,
|
|
1702
|
+
format: rowFormat
|
|
1703
|
+
})
|
|
1704
|
+
: rawItem;
|
|
1378
1705
|
|
|
1379
1706
|
const templateCopy: Block = {
|
|
1380
1707
|
...template,
|
|
@@ -1393,10 +1720,19 @@ export const buildIteratorBlock = ({
|
|
|
1393
1720
|
: template.properties,
|
|
1394
1721
|
blocks: template.blocks
|
|
1395
1722
|
? template.blocks.map((childBlock) => ({ ...childBlock }))
|
|
1396
|
-
: undefined
|
|
1723
|
+
: undefined,
|
|
1724
|
+
// Carried on the clone, read by ThemeBlock, which opens the product
|
|
1725
|
+
// scope around it — see `Block.itemScopeProduct`.
|
|
1726
|
+
...(scopesProducts
|
|
1727
|
+
? {
|
|
1728
|
+
itemScopeProduct: resolveCollectionRowProduct(
|
|
1729
|
+
rawItem as Record<string, unknown>
|
|
1730
|
+
)
|
|
1731
|
+
}
|
|
1732
|
+
: {})
|
|
1397
1733
|
};
|
|
1398
1734
|
|
|
1399
|
-
return replaceBlockValues(templateCopy, item, index);
|
|
1735
|
+
return replaceBlockValues(templateCopy, item, index, false, rowFormat);
|
|
1400
1736
|
});
|
|
1401
1737
|
|
|
1402
1738
|
const nextProperties = forgetScopedItems(iteratorBlock.properties);
|