@akinon/pz-theme 2.0.154-rc.5 → 2.0.154-rc.6

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # @akinon/pz-theme
2
2
 
3
+ ## 2.0.154-rc.6
4
+
5
+ ### Patch Changes
6
+
7
+ - 8e040731: ZERO-5235: let brands hang actions on order detail lines
8
+ - @akinon/next@2.0.154-rc.6
9
+ - @akinon/theme-contract@2.0.154-rc.6
10
+
3
11
  ## 2.0.154-rc.5
4
12
 
5
13
  ### Patch Changes
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "@akinon/pz-theme",
3
3
  "description": "Theme package for Project Zero Next — ThemePlaceholder system and theme editor infrastructure",
4
- "version": "2.0.154-rc.5",
4
+ "version": "2.0.154-rc.6",
5
5
  "license": "MIT",
6
6
  "main": "src/index.ts",
7
7
  "scripts": {
8
8
  "test": "jest"
9
9
  },
10
10
  "peerDependencies": {
11
- "@akinon/next": "2.0.154-rc.5",
11
+ "@akinon/next": "2.0.154-rc.6",
12
12
  "react": "^18.0.0 || ^19.0.0",
13
13
  "react-dom": "^18.0.0 || ^19.0.0"
14
14
  },
15
15
  "dependencies": {
16
- "@akinon/theme-contract": "2.0.154-rc.5",
16
+ "@akinon/theme-contract": "2.0.154-rc.6",
17
17
  "clsx": "2.1.1",
18
18
  "lottie-web": "^5.13.0",
19
19
  "react-multi-carousel": "2.8.4",
@@ -21,7 +21,7 @@
21
21
  "zod": "4.4.1"
22
22
  },
23
23
  "devDependencies": {
24
- "@akinon/next": "2.0.154-rc.5",
24
+ "@akinon/next": "2.0.154-rc.6",
25
25
  "@testing-library/jest-dom": "5.16.4",
26
26
  "@testing-library/react": "16.3.2",
27
27
  "@testing-library/user-event": "14.1.1",
@@ -0,0 +1,54 @@
1
+ import React from 'react';
2
+ import {
3
+ registerOrderLineActions,
4
+ renderOrderLineActions
5
+ } from '../order-line-actions';
6
+
7
+ const context = {
8
+ order: { id: 7, number: '20920397829', created_date: '2026-10-01' },
9
+ line: {
10
+ key: '1',
11
+ name: 'Product',
12
+ image: '',
13
+ href: '',
14
+ price: '100',
15
+ total: 100,
16
+ quantity: 1,
17
+ item: { id: 1, status: { value: '550', label: 'Delivered' } }
18
+ },
19
+ isDesigner: false
20
+ };
21
+
22
+ describe('order line actions registry', () => {
23
+ it('renders nothing until a brand registers', () => {
24
+ expect(renderOrderLineActions(context)).toBeNull();
25
+ });
26
+
27
+ it('hands the line and order to the registered renderer', () => {
28
+ const renderer = jest.fn(() => <span>action</span>);
29
+ const unregister = registerOrderLineActions(renderer);
30
+
31
+ expect(renderOrderLineActions(context)).toEqual(<span>action</span>);
32
+ expect(renderer).toHaveBeenCalledWith(context);
33
+
34
+ unregister();
35
+ expect(renderOrderLineActions(context)).toBeNull();
36
+ });
37
+
38
+ it('swallows a throwing renderer so a brand bug cannot break the line', () => {
39
+ const unregister = registerOrderLineActions(() => {
40
+ throw new Error('boom');
41
+ });
42
+
43
+ expect(renderOrderLineActions(context)).toBeNull();
44
+ unregister();
45
+ });
46
+
47
+ it('only forgets the renderer it was asked to forget', () => {
48
+ const first = registerOrderLineActions(() => 'first');
49
+ registerOrderLineActions(() => 'second');
50
+
51
+ first();
52
+ expect(renderOrderLineActions(context)).toBe('second');
53
+ });
54
+ });
@@ -9,6 +9,7 @@ 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,
@@ -16,7 +17,7 @@ import {
16
17
  readToggle
17
18
  } from './product-block-utils';
18
19
 
19
- interface OrderItemLike {
20
+ type OrderItemLike = {
20
21
  id?: number;
21
22
  price?: string;
22
23
  quantity?: string | number;
@@ -34,7 +35,7 @@ interface OrderItemLike {
34
35
  absolute_url?: string;
35
36
  attributes?: Record<string, unknown>;
36
37
  } | null;
37
- }
38
+ };
38
39
 
39
40
  interface AddressLike {
40
41
  first_name?: string;
@@ -272,7 +273,9 @@ const formatAddress = (address: AddressLike | null | undefined): string[] => {
272
273
  * order key is read from the URL; the editor owns which sections are exposed,
273
274
  * every caption and the styling.
274
275
  * A designer can mark one child as the row layout for the order lines; without
275
- * one the built-in line renders.
276
+ * one the built-in line renders. A brand may hang per-line actions on the
277
+ * built-in line through `registerOrderLineActions`; they render inside the
278
+ * line's `item-actions` part.
276
279
  * Properties: numberLabel, numberTag, dateLabel, headerTotalLabel,
277
280
  * showInvoiceLink, invoiceLinkLabel, showCancelLink, cancelLinkLabel,
278
281
  * cancelHrefBase, showShipmentStatus, showItems, showImage, imageWidth,
@@ -288,7 +291,7 @@ const formatAddress = (address: AddressLike | null | undefined): string[] => {
288
291
  * currencyText, currencyPosition.
289
292
  * Parts: detail, header, number, date, header-total, status,
290
293
  * invoice-link, cancel-link, list, item, image, name, quantity, color, size,
291
- * item-status, item-tracking, item-return-code, price, shipment,
294
+ * item-status, item-tracking, item-return-code, item-actions, price, shipment,
292
295
  * delivery, tracking, addresses, address, address-label, totals, total-row,
293
296
  * total-label, total-value, grand-total, payment-info, payment-info-label.
294
297
  */
@@ -758,6 +761,14 @@ const OrderDetailBlock = ({
758
761
  const returnCodeValue = showReturnCode
759
762
  ? resolveReturnCode(item.active_cancellation_request)
760
763
  : '';
764
+ const lineActions = renderOrderLineActions({
765
+ order,
766
+ line,
767
+ isDesigner
768
+ });
769
+ const showLineMeta =
770
+ showShipmentStatus &&
771
+ Boolean(item.status?.label || trackingValue || returnCodeValue);
761
772
 
762
773
  return (
763
774
  <li
@@ -873,63 +884,68 @@ const OrderDetailBlock = ({
873
884
  </div>
874
885
  </div>
875
886
 
876
- {showShipmentStatus &&
877
- (item.status?.label ||
878
- trackingValue ||
879
- returnCodeValue) && (
880
- <div
881
- style={{
882
- display: 'flex',
883
- flexDirection: 'column',
884
- alignItems: isMobile ? 'flex-start' : 'flex-end',
885
- textAlign: isMobile ? 'left' : 'right',
886
- gap: '4px',
887
- flex: isMobile ? '1 1 100%' : '0 0 auto'
888
- }}
889
- >
890
- {item.status?.label && (
891
- <span
892
- data-order-detail-item-status=""
893
- data-order-status-value={item.status.value}
894
- {...partAttrs('item-status')}
895
- style={{ display: 'inline-block' }}
896
- >
897
- {item.status.label}
898
- </span>
899
- )}
900
- {trackingValue && (
901
- <span
902
- data-order-detail-item-tracking=""
903
- {...partAttrs('item-tracking')}
904
- style={{ opacity: 0.8 }}
905
- >
906
- {lineTrackingLabel}{' '}
907
- {item.tracking_url ? (
908
- <a
909
- href={isDesigner ? '#' : item.tracking_url}
910
- onClick={guard}
911
- target={isDesigner ? undefined : '_blank'}
912
- rel="noreferrer"
913
- style={{ color: 'inherit' }}
914
- >
915
- {item.tracking_number || item.tracking_url}
916
- </a>
917
- ) : (
918
- trackingValue
919
- )}
920
- </span>
921
- )}
922
- {returnCodeValue && (
923
- <span
924
- data-order-detail-item-return-code=""
925
- {...partAttrs('item-return-code')}
926
- style={{ opacity: 0.8 }}
927
- >
928
- {returnCodeLabel} {returnCodeValue}
929
- </span>
930
- )}
931
- </div>
932
- )}
887
+ {(showLineMeta || lineActions) && (
888
+ <div
889
+ style={{
890
+ display: 'flex',
891
+ flexDirection: 'column',
892
+ alignItems: isMobile ? 'flex-start' : 'flex-end',
893
+ textAlign: isMobile ? 'left' : 'right',
894
+ gap: '4px',
895
+ flex: isMobile ? '1 1 100%' : '0 0 auto'
896
+ }}
897
+ >
898
+ {lineActions && (
899
+ <div
900
+ data-order-detail-item-actions=""
901
+ {...partAttrs('item-actions')}
902
+ >
903
+ {lineActions}
904
+ </div>
905
+ )}
906
+ {showLineMeta && item.status?.label && (
907
+ <span
908
+ data-order-detail-item-status=""
909
+ data-order-status-value={item.status.value}
910
+ {...partAttrs('item-status')}
911
+ style={{ display: 'inline-block' }}
912
+ >
913
+ {item.status.label}
914
+ </span>
915
+ )}
916
+ {showLineMeta && trackingValue && (
917
+ <span
918
+ data-order-detail-item-tracking=""
919
+ {...partAttrs('item-tracking')}
920
+ style={{ opacity: 0.8 }}
921
+ >
922
+ {lineTrackingLabel}{' '}
923
+ {item.tracking_url ? (
924
+ <a
925
+ href={isDesigner ? '#' : item.tracking_url}
926
+ onClick={guard}
927
+ target={isDesigner ? undefined : '_blank'}
928
+ rel="noreferrer"
929
+ style={{ color: 'inherit' }}
930
+ >
931
+ {item.tracking_number || item.tracking_url}
932
+ </a>
933
+ ) : (
934
+ trackingValue
935
+ )}
936
+ </span>
937
+ )}
938
+ {showLineMeta && returnCodeValue && (
939
+ <span
940
+ data-order-detail-item-return-code=""
941
+ {...partAttrs('item-return-code')}
942
+ style={{ opacity: 0.8 }}
943
+ >
944
+ {returnCodeLabel} {returnCodeValue}
945
+ </span>
946
+ )}
947
+ </div>
948
+ )}
933
949
  </div>
934
950
  </li>
935
951
  );
@@ -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
+ };