@akinon/pz-theme 2.0.157 → 2.0.158-rc.14

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.
@@ -9,14 +9,23 @@ import { getResponsiveValue, partAttrs } from '../utils';
9
9
  import { formatDate, formatMoney } from './commerce-format';
10
10
  import { readMoneyStyle } from './money-style';
11
11
  import { hasItemTemplate, ItemTemplateList } from './item-template';
12
+ import { renderOrderLineActions } from './order-line-actions';
12
13
  import { BlockRendererProps } from './block-renderer-registry';
13
14
  import {
14
15
  DesignerPlaceholder,
15
16
  groupOrderItems,
16
17
  readToggle
17
18
  } from './product-block-utils';
18
-
19
- interface OrderItemLike {
19
+ import {
20
+ allowsLineReturn,
21
+ allowsOrderCancel,
22
+ type OrderActionLine,
23
+ type OrderActionOrder,
24
+ type OrderActionRules,
25
+ useOrderActionRules
26
+ } from '../utils/order-action-rules';
27
+
28
+ type OrderItemLike = {
20
29
  id?: number;
21
30
  price?: string;
22
31
  quantity?: string | number;
@@ -35,7 +44,7 @@ interface OrderItemLike {
35
44
  absolute_url?: string;
36
45
  attributes?: Record<string, unknown>;
37
46
  } | null;
38
- }
47
+ };
39
48
 
40
49
  interface AddressLike {
41
50
  first_name?: string;
@@ -117,31 +126,39 @@ const firstString = (
117
126
  const hasOpenRequest = (request: unknown): boolean =>
118
127
  Array.isArray(request) ? request.length > 0 : Boolean(request);
119
128
 
120
- /** Commerce's "Teslim edildi". */
121
- const DELIVERED = '550';
122
-
123
129
  /**
124
- * Whether the order still has a line to return: delivered, refundable, with
125
- * no request open on it. Commerce also marks a shipped line refundable, and
126
- * keeps `is_refundable` true on the order and the line while a return request
127
- * is open, so the order flag alone would offer a return before the parcel
128
- * arrives and again after the shopper has asked. Lines that carry no flag at
129
- * all (an older payload) leave the decision to the order's flag and status.
130
+ * Whether the order still has a line to return: one commerce flags
131
+ * refundable, with no request open on it, that the brand's rules let a return
132
+ * start from (`registerOrderActionRules`: from which statuses, for how long
133
+ * after the order — ZERO-5302). Commerce keeps `is_refundable` true on the
134
+ * order and the line while a return request is open, so the order flag alone
135
+ * would offer a return again after the shopper has asked. Lines that carry no
136
+ * flag at all (an older payload) go by the order's flag, still through the
137
+ * brand's rules.
130
138
  */
131
- const hasReturnableLine = (order: OrderLike): boolean => {
139
+ const hasReturnableLine = (
140
+ order: OrderLike,
141
+ rules: OrderActionRules
142
+ ): boolean => {
132
143
  if (!order.is_refundable) return false;
133
144
 
134
145
  const lines = order.orderitem_set ?? [];
146
+ const allows = (item: OrderItemLike | null | undefined) =>
147
+ allowsLineReturn(
148
+ rules,
149
+ (item ?? {}) as OrderActionLine,
150
+ order as OrderActionOrder
151
+ );
135
152
 
136
153
  if (!lines.some((item) => item && 'is_refundable' in item)) {
137
- return String(order.status?.value ?? '') === DELIVERED;
154
+ return lines.length === 0 ? allows(null) : lines.some(allows);
138
155
  }
139
156
 
140
157
  return lines.some(
141
158
  (item) =>
142
- String(item?.status?.value ?? '') === DELIVERED &&
143
159
  item?.is_refundable &&
144
- !hasOpenRequest(item.active_cancellation_request)
160
+ !hasOpenRequest(item.active_cancellation_request) &&
161
+ allows(item)
145
162
  );
146
163
  };
147
164
 
@@ -373,6 +390,8 @@ const formatAddress = (address: AddressLike | null | undefined): string[] => {
373
390
  * every caption and the styling.
374
391
  * A designer can mark one child as the row layout for the order lines; without
375
392
  * one the built-in line renders.
393
+ * The cancel and return links offer what commerce allows and the brand's
394
+ * `registerOrderActionRules` lets through (ZERO-5302).
376
395
  * Properties: numberLabel, numberTag, dateLabel, headerTotalLabel,
377
396
  * showInvoiceLink, invoiceLinkLabel, showCancelLink, cancelLinkLabel,
378
397
  * showReturnLink, returnLinkLabel, cancelHrefBase, showShipmentStatus, showItems, showImage, imageWidth,
@@ -390,7 +409,7 @@ const formatAddress = (address: AddressLike | null | undefined): string[] => {
390
409
  * currencyText, currencyPosition.
391
410
  * Parts: detail, header, number, date, header-total, status,
392
411
  * invoice-link, cancel-link, return-link, list, item, image, name, quantity,
393
- * color, size, item-status, item-tracking, item-return-code,
412
+ * color, size, item-status, item-tracking, item-return-code, item-actions,
394
413
  * item-return-method, item-return-carrier, item-pickup-date, price, shipment,
395
414
  * delivery, tracking, addresses, address, address-label, totals, total-row,
396
415
  * total-label, total-value, grand-total, payment-info, payment-info-label.
@@ -588,6 +607,8 @@ const OrderDetailBlock = ({
588
607
  // Not gated on the session: brands expose the same endpoint for anonymous
589
608
  // order tracking. Without a key there is nothing to ask for at all.
590
609
  const { data, isLoading } = useGetOrderQuery(orderKey, { skip: !orderKey });
610
+ // The brand's say over the return and cancel links (ZERO-5302).
611
+ const actionRules = useOrderActionRules();
591
612
 
592
613
  const liveOrder = (data ?? null) as OrderLike | null;
593
614
  // See DESIGNER_SAMPLE_ORDER's own comment: the preview route's order id is
@@ -621,11 +642,16 @@ const OrderDetailBlock = ({
621
642
  const money = (value: unknown) =>
622
643
  formatMoney(value, { locale, currency, ...moneyStyle });
623
644
 
624
- // The sample order is not a delivered one; it shows the link for styling.
645
+ // The sample order shows both links for styling, whatever the brand's
646
+ // rules would make of it.
625
647
  const offersReturn =
626
648
  order === DESIGNER_SAMPLE_ORDER
627
649
  ? Boolean(order.is_refundable)
628
- : hasReturnableLine(order);
650
+ : hasReturnableLine(order, actionRules);
651
+ const offersCancel =
652
+ Boolean(order.is_cancellable) &&
653
+ (order === DESIGNER_SAMPLE_ORDER ||
654
+ allowsOrderCancel(actionRules, order as OrderActionOrder));
629
655
 
630
656
  // Navigating inside the designer iframe would drop the author out of the page
631
657
  // they are editing, so every link is inert there.
@@ -830,7 +856,7 @@ const OrderDetailBlock = ({
830
856
  → {invoiceLinkLabel}
831
857
  </a>
832
858
  )}
833
- {showCancelLink && order.is_cancellable && cancelHref && (
859
+ {showCancelLink && offersCancel && cancelHref && (
834
860
  <NextLink
835
861
  href={isDesigner ? '#' : cancelHref}
836
862
  onClick={guard}
@@ -930,6 +956,21 @@ const OrderDetailBlock = ({
930
956
  showPickupDate && returnDetails.pickupDate
931
957
  ? formatPickupDate(returnDetails.pickupDate, locale)
932
958
  : '';
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
+ );
933
974
 
934
975
  return (
935
976
  <li
@@ -1045,98 +1086,100 @@ const OrderDetailBlock = ({
1045
1086
  </div>
1046
1087
  </div>
1047
1088
 
1048
- {showShipmentStatus &&
1049
- (item.status?.label ||
1050
- trackingValue ||
1051
- returnCodeValue ||
1052
- returnMethodValue ||
1053
- returnCarrierValue ||
1054
- pickupDateValue) && (
1055
- <div
1056
- style={{
1057
- display: 'flex',
1058
- flexDirection: 'column',
1059
- alignItems: isMobile ? 'flex-start' : 'flex-end',
1060
- textAlign: isMobile ? 'left' : 'right',
1061
- gap: '4px',
1062
- flex: isMobile ? '1 1 100%' : '0 0 auto'
1063
- }}
1064
- >
1065
- {item.status?.label && (
1066
- <span
1067
- data-order-detail-item-status=""
1068
- data-order-status-value={item.status.value}
1069
- {...partAttrs('item-status')}
1070
- style={{ display: 'inline-block' }}
1071
- >
1072
- {item.status.label}
1073
- </span>
1074
- )}
1075
- {/* Label and value in their own spans, so a
1076
- composition can colour them apart. */}
1077
- {returnMethodValue && (
1078
- <span
1079
- data-order-detail-item-return-method=""
1080
- {...partAttrs('item-return-method')}
1081
- style={{ opacity: 0.8 }}
1082
- >
1083
- <span data-label="">{returnMethodLabel}</span>{' '}
1084
- <span data-value="">{returnMethodValue}</span>
1085
- </span>
1086
- )}
1087
- {returnCarrierValue && (
1088
- <span
1089
- data-order-detail-item-return-carrier=""
1090
- {...partAttrs('item-return-carrier')}
1091
- style={{ opacity: 0.8 }}
1092
- >
1093
- <span data-label="">{returnCarrierLabel}</span>{' '}
1094
- <span data-value="">{returnCarrierValue}</span>
1095
- </span>
1096
- )}
1097
- {pickupDateValue && (
1098
- <span
1099
- data-order-detail-item-pickup-date=""
1100
- {...partAttrs('item-pickup-date')}
1101
- style={{ opacity: 0.8 }}
1102
- >
1103
- <span data-label="">{pickupDateLabel}</span>{' '}
1104
- <span data-value="">{pickupDateValue}</span>
1105
- </span>
1106
- )}
1107
- {trackingValue && (
1108
- <span
1109
- data-order-detail-item-tracking=""
1110
- {...partAttrs('item-tracking')}
1111
- style={{ opacity: 0.8 }}
1112
- >
1113
- {lineTrackingLabel}{' '}
1114
- {item.tracking_url ? (
1115
- <a
1116
- href={isDesigner ? '#' : item.tracking_url}
1117
- onClick={guard}
1118
- target={isDesigner ? undefined : '_blank'}
1119
- rel="noreferrer"
1120
- style={{ color: 'inherit' }}
1121
- >
1122
- {item.tracking_number || item.tracking_url}
1123
- </a>
1124
- ) : (
1125
- trackingValue
1126
- )}
1127
- </span>
1128
- )}
1129
- {returnCodeValue && (
1130
- <span
1131
- data-order-detail-item-return-code=""
1132
- {...partAttrs('item-return-code')}
1133
- style={{ opacity: 0.8 }}
1134
- >
1135
- {returnCodeLabel} {returnCodeValue}
1136
- </span>
1137
- )}
1138
- </div>
1139
- )}
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
+ )}
1140
1183
  </div>
1141
1184
  </li>
1142
1185
  );
@@ -0,0 +1,45 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { GroupedOrderLine, OrderLineLike } from './product-block-utils';
3
+
4
+ export interface OrderLineActionsOrder {
5
+ id?: number;
6
+ number?: string;
7
+ created_date?: string;
8
+ }
9
+
10
+ export type OrderLineActionsItem = OrderLineLike & {
11
+ status?: { value?: string; label?: string };
12
+ [key: string]: unknown;
13
+ };
14
+
15
+ export interface OrderLineActionsContext {
16
+ order: OrderLineActionsOrder;
17
+ line: GroupedOrderLine<OrderLineActionsItem>;
18
+ isDesigner: boolean;
19
+ }
20
+
21
+ export type OrderLineActionsRenderer = (
22
+ context: OrderLineActionsContext
23
+ ) => ReactNode;
24
+
25
+ let renderer: OrderLineActionsRenderer | null = null;
26
+
27
+ export const registerOrderLineActions = (
28
+ fn: OrderLineActionsRenderer
29
+ ): (() => void) => {
30
+ renderer = fn;
31
+ return () => {
32
+ if (renderer === fn) renderer = null;
33
+ };
34
+ };
35
+
36
+ export const renderOrderLineActions = (
37
+ context: OrderLineActionsContext
38
+ ): ReactNode => {
39
+ if (!renderer) return null;
40
+ try {
41
+ return renderer(context) ?? null;
42
+ } catch {
43
+ return null;
44
+ }
45
+ };
@@ -13,7 +13,14 @@ import { readToggle } from './product-block-utils';
13
13
  import { BlockRendererProps } from './block-renderer-registry';
14
14
 
15
15
  interface OrderItemLike {
16
- product?: { image?: string; name?: string } | null;
16
+ id?: number;
17
+ status?: { value?: string | number; label?: string };
18
+ product?: {
19
+ image?: string;
20
+ name?: string;
21
+ sku?: string;
22
+ base_code?: string;
23
+ } | null;
17
24
  }
18
25
 
19
26
  interface OrderLike {
@@ -248,7 +255,23 @@ const OrderListBlock = ({
248
255
  maxImages
249
256
  ),
250
257
  url: hrefFor(order),
251
- href: hrefFor(order)
258
+ href: hrefFor(order),
259
+ createdDate: order.created_date || '',
260
+ items: (Array.isArray(order.orderitem_set) ? order.orderitem_set : []).map(
261
+ (item, itemIndex) => ({
262
+ id: item?.id ?? itemIndex,
263
+ name: item?.product?.name || '',
264
+ image: item?.product?.image || '',
265
+ status: item?.status?.label || '',
266
+ statusValue: String(item?.status?.value ?? ''),
267
+ product: {
268
+ sku: item?.product?.sku || '',
269
+ base_code: item?.product?.base_code || '',
270
+ name: item?.product?.name || '',
271
+ image: item?.product?.image || ''
272
+ }
273
+ })
274
+ )
252
275
  }));
253
276
 
254
277
  // A designer who marked a child as the row layout gets that layout repeated
@@ -0,0 +1,35 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+
5
+ export interface ThemeRowScopeValue {
6
+ listType: string;
7
+ row: Record<string, unknown>;
8
+ index: number;
9
+ }
10
+
11
+ const ThemeRowScopeContext = React.createContext<ThemeRowScopeValue | null>(
12
+ null
13
+ );
14
+
15
+ export const ThemeRowScopeProvider = ({
16
+ listType,
17
+ row,
18
+ index,
19
+ children
20
+ }: ThemeRowScopeValue & { children: React.ReactNode }) => {
21
+ const value = React.useMemo(
22
+ () => ({ listType, row, index }),
23
+ [listType, row, index]
24
+ );
25
+
26
+ return (
27
+ <ThemeRowScopeContext.Provider value={value}>
28
+ {children}
29
+ </ThemeRowScopeContext.Provider>
30
+ );
31
+ };
32
+
33
+ /** Non-null while a repeated row of any list renders; null everywhere else. */
34
+ export const useThemeRowScope = (): ThemeRowScopeValue | null =>
35
+ React.useContext(ThemeRowScopeContext);
@@ -7,6 +7,7 @@ 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';
10
11
 
11
12
  const TextBlock = ({
12
13
  block,
@@ -14,7 +15,7 @@ const TextBlock = ({
14
15
  }: BlockRendererProps) => {
15
16
  const { locale } = useLocalization();
16
17
  const localize = useLocalizeHref();
17
- const defaultLocale = process.env.NEXT_PUBLIC_DEFAULT_LOCALE || 'en';
18
+ const { defaultLocale } = useValueLocale();
18
19
  const themeSettings = useThemeSettingsContext();
19
20
 
20
21
  const getLocalizedContent = (): string => {
@@ -30,7 +31,10 @@ const TextBlock = ({
30
31
 
31
32
  if (typeof value === 'object' && value !== null) {
32
33
  return (
33
- value[locale] || value[defaultLocale] || Object.values(value)[0] || ''
34
+ value[locale] ||
35
+ (defaultLocale && value[defaultLocale]) ||
36
+ Object.values(value)[0] ||
37
+ ''
34
38
  );
35
39
  }
36
40
 
@@ -41,7 +41,7 @@ const orderPayload = {
41
41
  orderitem_set: [
42
42
  {
43
43
  price: '649.95',
44
- status: { label: 'Shipped' },
44
+ status: { value: '500', label: 'Shipped' },
45
45
  product: {
46
46
  name: 'Spring Sneaker',
47
47
  image: '/i.jpg',
@@ -63,6 +63,19 @@ describe('buildOrderPageContext', () => {
63
63
  expect(context.order.currency).toBe('TRY');
64
64
  expect(context.order.items).toHaveLength(1);
65
65
  expect(context.order.items[0].title).toBe('Spring Sneaker');
66
+ expect(context.order.items[0].status).toBe('Shipped');
67
+ expect(context.order.items[0].statusValue).toBe('500');
68
+ });
69
+
70
+ it('stringifies a numeric order item status code', () => {
71
+ const context = buildOrderPageContext({
72
+ order: {
73
+ ...orderPayload,
74
+ orderitem_set: [{ status: { value: 550, label: 'Delivered' } }]
75
+ }
76
+ });
77
+
78
+ expect(context.order.items[0].statusValue).toBe('550');
66
79
  });
67
80
 
68
81
  it('does not publish the identity fields the raw payload carries', () => {
@@ -59,6 +59,9 @@ const toRecord = (value: unknown): UnknownRecord =>
59
59
  const toStringValue = (value: unknown, fallback = ''): string =>
60
60
  typeof value === 'string' ? value : fallback;
61
61
 
62
+ const toCodeValue = (value: unknown): string =>
63
+ typeof value === 'number' ? String(value) : toStringValue(value);
64
+
62
65
  const humanizeSlug = (value: string) =>
63
66
  value
64
67
  .split('-')
@@ -479,6 +482,7 @@ export const buildOrderPageContext = ({
479
482
  unitPrice: toStringValue(item.price),
480
483
  quantity: 1,
481
484
  status: toStringValue(toRecord(item.status).label),
485
+ statusValue: toCodeValue(toRecord(item.status).value),
482
486
  currency
483
487
  };
484
488
  });