@akinon/pz-theme 2.0.166-rc.1 → 2.0.166

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.
@@ -8,9 +8,13 @@ import { useLocalization } from '@akinon/next/hooks';
8
8
  import { getResponsiveValue, partAttrs } from '../utils';
9
9
  import { formatDate, formatMoney } from './commerce-format';
10
10
  import { readMoneyStyle } from './money-style';
11
- import { hasItemTemplate, ItemTemplateList } from './item-template';
12
- import { renderOrderLineActions } from './order-line-actions';
11
+ import {
12
+ hasItemTemplate,
13
+ ITEM_TEMPLATE_ROLE,
14
+ ItemTemplateList
15
+ } from './item-template';
13
16
  import { BlockRendererProps } from './block-renderer-registry';
17
+ import type { Block } from '../theme-block';
14
18
  import {
15
19
  DesignerPlaceholder,
16
20
  groupOrderItems,
@@ -25,7 +29,7 @@ import {
25
29
  useOrderActionRules
26
30
  } from '../utils/order-action-rules';
27
31
 
28
- type OrderItemLike = {
32
+ interface OrderItemLike {
29
33
  id?: number;
30
34
  price?: string;
31
35
  quantity?: string | number;
@@ -33,18 +37,21 @@ type OrderItemLike = {
33
37
  status?: { value?: string; label?: string };
34
38
  tracking_url?: string;
35
39
  tracking_number?: string;
40
+ shipping_company?: { value?: string; label?: string } | null;
36
41
  estimated_delivery_date?: string;
37
42
  price_currency?: { value?: string; label?: string };
38
43
  is_refundable?: boolean;
39
44
  active_cancellation_request?: unknown;
40
45
  product?: {
41
46
  pk?: number;
47
+ sku?: string;
48
+ base_code?: string;
42
49
  name?: string;
43
50
  image?: string;
44
51
  absolute_url?: string;
45
52
  attributes?: Record<string, unknown>;
46
53
  } | null;
47
- };
54
+ }
48
55
 
49
56
  interface AddressLike {
50
57
  first_name?: string;
@@ -70,6 +77,7 @@ interface OrderLike {
70
77
  shipping_amount?: string;
71
78
  tracking_url?: string;
72
79
  tracking_number?: string;
80
+ shipping_company?: { value?: string; label?: string } | null;
73
81
  status?: { value?: string; label?: string };
74
82
  currency?: { value?: string; label?: string };
75
83
  orderitem_set?: OrderItemLike[];
@@ -108,6 +116,19 @@ const ORDER_PARAM_KEYS = [
108
116
  // opts into h1/h2/h3 when the order number is the page's main heading.
109
117
  const NUMBER_TAGS = ['span', 'div', 'h1', 'h2', 'h3'];
110
118
 
119
+ /**
120
+ * A child group marked with this role is the whole order page, drawn in place
121
+ * of everything the block renders itself (ZERO-5509). It is repeated once, over
122
+ * the order: its blocks bind `item.*` to the order (number, date, totals,
123
+ * addresses, payment), and a repeat inside it over `item.lines` draws one row
124
+ * per order line, so the header, the line rows, the totals and every caption
125
+ * are blocks the panel arranges instead of fixed markup styled with CSS.
126
+ */
127
+ export const ORDER_LAYOUT_ROLE = 'order-detail-layout';
128
+
129
+ const readChildRole = (child: Block, breakpoint: string): string =>
130
+ String(getResponsiveValue(child.properties?.role, breakpoint, '') ?? '');
131
+
111
132
  const firstString = (
112
133
  read: (key: string) => unknown,
113
134
  keys: string[]
@@ -409,7 +430,7 @@ const formatAddress = (address: AddressLike | null | undefined): string[] => {
409
430
  * currencyText, currencyPosition.
410
431
  * Parts: detail, header, number, date, header-total, status,
411
432
  * invoice-link, cancel-link, return-link, list, item, image, name, quantity,
412
- * color, size, item-status, item-tracking, item-return-code, item-actions,
433
+ * color, size, item-status, item-tracking, item-return-code,
413
434
  * item-return-method, item-return-carrier, item-pickup-date, price, shipment,
414
435
  * delivery, tracking, addresses, address, address-label, totals, total-row,
415
436
  * total-label, total-value, grand-total, payment-info, payment-info-label.
@@ -673,8 +694,16 @@ const OrderDetailBlock = ({
673
694
  ? {}
674
695
  : { width: '50%', marginLeft: '50%' };
675
696
 
697
+ // A row publishes every field the built-in line can show, whatever its own
698
+ // show/hide toggles say: those govern the built-in line, and a designer's
699
+ // own layout decides for itself what to draw (ZERO-5509).
676
700
  const lineRows = groupedItems.map((line) => {
677
701
  const lineCurrency = line.item.price_currency?.value || currency;
702
+ const attributes = line.item.product?.attributes;
703
+ const returnDetails = resolveReturnDetails(
704
+ line.item.active_cancellation_request
705
+ );
706
+ const lineStatusValue = String(line.item.status?.value ?? '');
678
707
 
679
708
  return {
680
709
  key: line.key,
@@ -694,7 +723,43 @@ const OrderDetailBlock = ({
694
723
  ...moneyStyle
695
724
  }),
696
725
  status: line.item.status?.label || '',
697
- statusValue: line.item.status?.value || ''
726
+ statusValue: lineStatusValue,
727
+ color: resolveAttribute(attributes, colorAttributeKey),
728
+ size: resolveAttribute(attributes, sizeAttributeKey),
729
+ trackingNumber: line.item.tracking_number || '',
730
+ trackingUrl: isDesigner ? '#' : line.item.tracking_url || '',
731
+ shippingCompany: line.item.shipping_company?.label || '',
732
+ returnCode: resolveReturnCode(line.item.active_cancellation_request),
733
+ returnMethod:
734
+ returnDetails.method === 'appointment'
735
+ ? appointmentReturnLabel
736
+ : returnDetails.method === 'standard'
737
+ ? standardReturnLabel
738
+ : '',
739
+ returnCarrier: returnDetails.carrier,
740
+ pickupDate: returnDetails.pickupDate
741
+ ? formatPickupDate(returnDetails.pickupDate, locale)
742
+ : '',
743
+ // The line seen as a one-line order, for blocks that act on an order
744
+ // from inside a row — the rate & review "Rate" action reads `id`,
745
+ // `number` and `items` off its row, so inside a line row it offers a
746
+ // review for that line's product only.
747
+ id: order.id ?? '',
748
+ number: order.number || orderKey,
749
+ createdDate: order.created_date || '',
750
+ items: [
751
+ {
752
+ id: line.item.id ?? line.key,
753
+ name: line.name,
754
+ image: line.image,
755
+ statusValue: lineStatusValue,
756
+ product: {
757
+ sku: line.item.product?.sku || '',
758
+ base_code: line.item.product?.base_code || '',
759
+ name: line.name
760
+ }
761
+ }
762
+ ]
698
763
  };
699
764
  });
700
765
 
@@ -778,6 +843,116 @@ const OrderDetailBlock = ({
778
843
  );
779
844
  };
780
845
 
846
+ // The designer's whole-order layout, when there is one: drawn once over
847
+ // the order instead of everything below. Hidden on the storefront when the
848
+ // author hid it, still drawn on the canvas so it can be designed.
849
+ const layoutChild =
850
+ (block.blocks || []).find(
851
+ (child) =>
852
+ readChildRole(child, currentBreakpoint) === ORDER_LAYOUT_ROLE &&
853
+ (isDesigner || !child.hidden)
854
+ ) ?? null;
855
+
856
+ if (layoutChild) {
857
+ const shippingLines = formatAddress(order.shipping_address);
858
+ const billingLines = formatAddress(order.billing_address);
859
+ const pieceCount = groupedItems.reduce(
860
+ (sum, line) => sum + Number(line.quantity || 0),
861
+ 0
862
+ );
863
+ const shippingAmount = Number(order.shipping_amount || 0);
864
+ const signedMoney = (value: unknown) => {
865
+ const amount = Math.abs(Number(value || 0));
866
+ return amount > 0 ? `-${money(String(amount))}` : money('0');
867
+ };
868
+ const deliveryLine = items.find((item) => item.estimated_delivery_date);
869
+ const linkOrInert = (href: string) => (isDesigner && href ? '#' : href);
870
+
871
+ // One row: the order. Every value is already formatted the way the block
872
+ // formats it (money style, locale date), so a text block bound to
873
+ // `item.total` prints "15.499 TL" exactly as the built-in header does.
874
+ const orderRows = [
875
+ {
876
+ key: String(order.id ?? order.number ?? orderKey),
877
+ id: order.id ?? '',
878
+ number: order.number || orderKey,
879
+ date: formatDate(order.created_date, locale),
880
+ createdDate: order.created_date || '',
881
+ status: order.status?.label || '',
882
+ statusValue: String(order.status?.value ?? ''),
883
+ total: money(order.amount),
884
+ pieceCount,
885
+ pieceSubtotal,
886
+ shipping: money(order.shipping_amount),
887
+ isFreeShipping: !(shippingAmount > 0),
888
+ discountTotal: signedMoney(order.discount_amount),
889
+ hasDiscount: Number(order.discount_amount || 0) !== 0,
890
+ discounts: (order.discountitem_set || []).map((discount, index) => ({
891
+ key: String(index),
892
+ name: discount.name || discountLabel,
893
+ amount: signedMoney(discount.amount)
894
+ })),
895
+ hasInstallments: installmentCount > 1,
896
+ installmentCount,
897
+ installmentAmount: perInstallment,
898
+ invoiceUrl: linkOrInert(order.e_archive_url || ''),
899
+ hasInvoice: Boolean(order.e_archive_url),
900
+ canCancel: offersCancel && Boolean(cancelHref),
901
+ canReturn: offersReturn && Boolean(cancelHref),
902
+ cancelUrl: linkOrInert(cancelHref),
903
+ trackingNumber: order.tracking_number || '',
904
+ trackingUrl: linkOrInert(order.tracking_url || ''),
905
+ hasTracking: Boolean(order.tracking_url || order.tracking_number),
906
+ shippingCompany: order.shipping_company?.label || '',
907
+ estimatedDelivery: deliveryLine
908
+ ? formatDate(deliveryLine.estimated_delivery_date, locale)
909
+ : '',
910
+ shippingName: shippingLines.length > 1 ? shippingLines[0] : '',
911
+ shippingAddress: shippingLines[shippingLines.length - 1] || '',
912
+ billingName: billingLines.length > 1 ? billingLines[0] : '',
913
+ billingAddress: billingLines[billingLines.length - 1] || '',
914
+ hasBillingAddress: billingLines.length > 0,
915
+ bankName: order.bank?.name || '',
916
+ paymentMethod: order.payment_option?.name || '',
917
+ lineCount: lineRows.length,
918
+ lines: lineRows,
919
+ // The order's delivered lines, for an order-wide rate & review block
920
+ // placed outside the line repeat.
921
+ items: lineRows.flatMap((row) => row.items)
922
+ }
923
+ ];
924
+
925
+ return (
926
+ <div
927
+ data-order-detail=""
928
+ data-order-detail-layout=""
929
+ style={{ width: '100%' }}
930
+ >
931
+ <ItemTemplateList
932
+ block={{
933
+ ...block,
934
+ blocks: [
935
+ {
936
+ ...layoutChild,
937
+ properties: {
938
+ ...(layoutChild.properties || {}),
939
+ role: ITEM_TEMPLATE_ROLE
940
+ }
941
+ }
942
+ ]
943
+ }}
944
+ items={orderRows}
945
+ placeholderId={placeholderId}
946
+ sectionId={sectionId}
947
+ isDesigner={isDesigner}
948
+ selectedBlockId={selectedBlockId}
949
+ currentBreakpoint={currentBreakpoint}
950
+ designerPlaceholderCount={1}
951
+ />
952
+ </div>
953
+ );
954
+ }
955
+
781
956
  return (
782
957
  <div
783
958
  data-order-detail=""
@@ -956,21 +1131,6 @@ const OrderDetailBlock = ({
956
1131
  showPickupDate && returnDetails.pickupDate
957
1132
  ? formatPickupDate(returnDetails.pickupDate, locale)
958
1133
  : '';
959
- const lineActions = renderOrderLineActions({
960
- order,
961
- line,
962
- isDesigner
963
- });
964
- const showLineMeta =
965
- showShipmentStatus &&
966
- Boolean(
967
- item.status?.label ||
968
- trackingValue ||
969
- returnCodeValue ||
970
- returnMethodValue ||
971
- returnCarrierValue ||
972
- pickupDateValue
973
- );
974
1134
 
975
1135
  return (
976
1136
  <li
@@ -1086,100 +1246,98 @@ const OrderDetailBlock = ({
1086
1246
  </div>
1087
1247
  </div>
1088
1248
 
1089
- {(showLineMeta || lineActions) && (
1090
- <div
1091
- style={{
1092
- display: 'flex',
1093
- flexDirection: 'column',
1094
- alignItems: isMobile ? 'flex-start' : 'flex-end',
1095
- textAlign: isMobile ? 'left' : 'right',
1096
- gap: '4px',
1097
- flex: isMobile ? '1 1 100%' : '0 0 auto'
1098
- }}
1099
- >
1100
- {lineActions && (
1101
- <div
1102
- data-order-detail-item-actions=""
1103
- {...partAttrs('item-actions')}
1104
- >
1105
- {lineActions}
1106
- </div>
1107
- )}
1108
- {showLineMeta && item.status?.label && (
1109
- <span
1110
- data-order-detail-item-status=""
1111
- data-order-status-value={item.status.value}
1112
- {...partAttrs('item-status')}
1113
- style={{ display: 'inline-block' }}
1114
- >
1115
- {item.status.label}
1116
- </span>
1117
- )}
1118
- {/* Label and value in their own spans, so a
1119
- composition can colour them apart. */}
1120
- {showLineMeta && returnMethodValue && (
1121
- <span
1122
- data-order-detail-item-return-method=""
1123
- {...partAttrs('item-return-method')}
1124
- style={{ opacity: 0.8 }}
1125
- >
1126
- <span data-label="">{returnMethodLabel}</span>{' '}
1127
- <span data-value="">{returnMethodValue}</span>
1128
- </span>
1129
- )}
1130
- {showLineMeta && returnCarrierValue && (
1131
- <span
1132
- data-order-detail-item-return-carrier=""
1133
- {...partAttrs('item-return-carrier')}
1134
- style={{ opacity: 0.8 }}
1135
- >
1136
- <span data-label="">{returnCarrierLabel}</span>{' '}
1137
- <span data-value="">{returnCarrierValue}</span>
1138
- </span>
1139
- )}
1140
- {showLineMeta && pickupDateValue && (
1141
- <span
1142
- data-order-detail-item-pickup-date=""
1143
- {...partAttrs('item-pickup-date')}
1144
- style={{ opacity: 0.8 }}
1145
- >
1146
- <span data-label="">{pickupDateLabel}</span>{' '}
1147
- <span data-value="">{pickupDateValue}</span>
1148
- </span>
1149
- )}
1150
- {showLineMeta && trackingValue && (
1151
- <span
1152
- data-order-detail-item-tracking=""
1153
- {...partAttrs('item-tracking')}
1154
- style={{ opacity: 0.8 }}
1155
- >
1156
- {lineTrackingLabel}{' '}
1157
- {item.tracking_url ? (
1158
- <a
1159
- href={isDesigner ? '#' : item.tracking_url}
1160
- onClick={guard}
1161
- target={isDesigner ? undefined : '_blank'}
1162
- rel="noreferrer"
1163
- style={{ color: 'inherit' }}
1164
- >
1165
- {item.tracking_number || item.tracking_url}
1166
- </a>
1167
- ) : (
1168
- trackingValue
1169
- )}
1170
- </span>
1171
- )}
1172
- {showLineMeta && returnCodeValue && (
1173
- <span
1174
- data-order-detail-item-return-code=""
1175
- {...partAttrs('item-return-code')}
1176
- style={{ opacity: 0.8 }}
1177
- >
1178
- {returnCodeLabel} {returnCodeValue}
1179
- </span>
1180
- )}
1181
- </div>
1182
- )}
1249
+ {showShipmentStatus &&
1250
+ (item.status?.label ||
1251
+ trackingValue ||
1252
+ returnCodeValue ||
1253
+ returnMethodValue ||
1254
+ returnCarrierValue ||
1255
+ pickupDateValue) && (
1256
+ <div
1257
+ style={{
1258
+ display: 'flex',
1259
+ flexDirection: 'column',
1260
+ alignItems: isMobile ? 'flex-start' : 'flex-end',
1261
+ textAlign: isMobile ? 'left' : 'right',
1262
+ gap: '4px',
1263
+ flex: isMobile ? '1 1 100%' : '0 0 auto'
1264
+ }}
1265
+ >
1266
+ {item.status?.label && (
1267
+ <span
1268
+ data-order-detail-item-status=""
1269
+ data-order-status-value={item.status.value}
1270
+ {...partAttrs('item-status')}
1271
+ style={{ display: 'inline-block' }}
1272
+ >
1273
+ {item.status.label}
1274
+ </span>
1275
+ )}
1276
+ {/* Label and value in their own spans, so a
1277
+ composition can colour them apart. */}
1278
+ {returnMethodValue && (
1279
+ <span
1280
+ data-order-detail-item-return-method=""
1281
+ {...partAttrs('item-return-method')}
1282
+ style={{ opacity: 0.8 }}
1283
+ >
1284
+ <span data-label="">{returnMethodLabel}</span>{' '}
1285
+ <span data-value="">{returnMethodValue}</span>
1286
+ </span>
1287
+ )}
1288
+ {returnCarrierValue && (
1289
+ <span
1290
+ data-order-detail-item-return-carrier=""
1291
+ {...partAttrs('item-return-carrier')}
1292
+ style={{ opacity: 0.8 }}
1293
+ >
1294
+ <span data-label="">{returnCarrierLabel}</span>{' '}
1295
+ <span data-value="">{returnCarrierValue}</span>
1296
+ </span>
1297
+ )}
1298
+ {pickupDateValue && (
1299
+ <span
1300
+ data-order-detail-item-pickup-date=""
1301
+ {...partAttrs('item-pickup-date')}
1302
+ style={{ opacity: 0.8 }}
1303
+ >
1304
+ <span data-label="">{pickupDateLabel}</span>{' '}
1305
+ <span data-value="">{pickupDateValue}</span>
1306
+ </span>
1307
+ )}
1308
+ {trackingValue && (
1309
+ <span
1310
+ data-order-detail-item-tracking=""
1311
+ {...partAttrs('item-tracking')}
1312
+ style={{ opacity: 0.8 }}
1313
+ >
1314
+ {lineTrackingLabel}{' '}
1315
+ {item.tracking_url ? (
1316
+ <a
1317
+ href={isDesigner ? '#' : item.tracking_url}
1318
+ onClick={guard}
1319
+ target={isDesigner ? undefined : '_blank'}
1320
+ rel="noreferrer"
1321
+ style={{ color: 'inherit' }}
1322
+ >
1323
+ {item.tracking_number || item.tracking_url}
1324
+ </a>
1325
+ ) : (
1326
+ trackingValue
1327
+ )}
1328
+ </span>
1329
+ )}
1330
+ {returnCodeValue && (
1331
+ <span
1332
+ data-order-detail-item-return-code=""
1333
+ {...partAttrs('item-return-code')}
1334
+ style={{ opacity: 0.8 }}
1335
+ >
1336
+ {returnCodeLabel} {returnCodeValue}
1337
+ </span>
1338
+ )}
1339
+ </div>
1340
+ )}
1183
1341
  </div>
1184
1342
  </li>
1185
1343
  );
@@ -7,7 +7,6 @@ import { getInnerRootInlineStyles, partAttrs } from '../utils';
7
7
  import { useThemeSettingsContext } from '../theme-settings-context';
8
8
  import { useLocalizeHref } from '../hooks/use-localize-href';
9
9
  import { localizeHtmlLinks } from '../utils/localize-href';
10
- import { useValueLocale } from '../utils/localized-value';
11
10
  // Rich-text element defaults from the theme settings.
12
11
  import {
13
12
  THEME_RICH_TEXT_ATTRIBUTE,
@@ -20,7 +19,7 @@ const TextBlock = ({
20
19
  }: BlockRendererProps) => {
21
20
  const { locale } = useLocalization();
22
21
  const localize = useLocalizeHref();
23
- const { defaultLocale } = useValueLocale();
22
+ const defaultLocale = process.env.NEXT_PUBLIC_DEFAULT_LOCALE || 'en';
24
23
  const themeSettings = useThemeSettingsContext();
25
24
 
26
25
  const getLocalizedContent = (): string => {
@@ -36,10 +35,7 @@ const TextBlock = ({
36
35
 
37
36
  if (typeof value === 'object' && value !== null) {
38
37
  return (
39
- value[locale] ||
40
- (defaultLocale && value[defaultLocale]) ||
41
- Object.values(value)[0] ||
42
- ''
38
+ value[locale] || value[defaultLocale] || Object.values(value)[0] || ''
43
39
  );
44
40
  }
45
41