@akinon/pz-theme 2.0.154-rc.7 → 2.0.154
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 +3 -50
- package/package.json +4 -4
- package/src/blocks/item-template.tsx +19 -25
- package/src/blocks/order-detail-block.tsx +97 -117
- package/src/blocks/order-list-block.tsx +2 -25
- package/src/chrome/__tests__/page-context-builders.test.ts +1 -14
- package/src/chrome/page-context.ts +0 -4
- package/src/custom/README.md +0 -20
- package/src/__tests__/row-scope.test.tsx +0 -118
- package/src/blocks/__tests__/order-line-actions.test.tsx +0 -54
- package/src/blocks/__tests__/order-list-rows.test.tsx +0 -112
- package/src/blocks/order-line-actions.ts +0 -45
- package/src/blocks/row-scope.tsx +0 -35
package/CHANGELOG.md
CHANGED
|
@@ -1,59 +1,12 @@
|
|
|
1
1
|
# @akinon/pz-theme
|
|
2
2
|
|
|
3
|
-
## 2.0.154
|
|
3
|
+
## 2.0.154
|
|
4
4
|
|
|
5
5
|
### Patch Changes
|
|
6
6
|
|
|
7
7
|
- 51a88b89: ZERO-5183: Give the order cancellation screen its own placeholder (`account-order-cancellation-body`) so it can be composed in the theme editor. The `order-detail` block gains an "Easy return" link for orders that can be returned and, per returned line, the return method (scheduled pickup or standard), the carrier and the pickup date, each behind its own switch. Order rows now keep units of one product together only while they share a status and cancellation/return request, so returning one of two units no longer reads as both returned.
|
|
8
|
-
- @akinon/next@2.0.154
|
|
9
|
-
- @akinon/theme-contract@2.0.154
|
|
10
|
-
|
|
11
|
-
## 2.0.154-rc.6
|
|
12
|
-
|
|
13
|
-
### Patch Changes
|
|
14
|
-
|
|
15
|
-
- 8e040731: ZERO-5235: let brands hang actions on order detail lines
|
|
16
|
-
- @akinon/next@2.0.154-rc.6
|
|
17
|
-
- @akinon/theme-contract@2.0.154-rc.6
|
|
18
|
-
|
|
19
|
-
## 2.0.154-rc.5
|
|
20
|
-
|
|
21
|
-
### Patch Changes
|
|
22
|
-
|
|
23
|
-
- ad5240c4: ZERO-5254: add custom render and theme editor support to pz-rate-review
|
|
24
|
-
- Updated dependencies [219164a3]
|
|
25
|
-
- Updated dependencies [86187b1c2]
|
|
26
|
-
- Updated dependencies [0cf9ea239]
|
|
27
|
-
- Updated dependencies [97d29893]
|
|
28
|
-
- Updated dependencies [324f97d55]
|
|
29
|
-
- Updated dependencies [51ea06888]
|
|
30
|
-
- Updated dependencies
|
|
31
|
-
- Updated dependencies [2fea41430]
|
|
32
|
-
- Updated dependencies [b55acb768]
|
|
33
|
-
- Updated dependencies [760258c1c]
|
|
34
|
-
- Updated dependencies [143be2b9d]
|
|
35
|
-
- Updated dependencies [7889b08fe]
|
|
36
|
-
- Updated dependencies [9f8cd3bc5]
|
|
37
|
-
- Updated dependencies [bfafa3f49]
|
|
38
|
-
- Updated dependencies [57d7eb305]
|
|
39
|
-
- Updated dependencies [d99a6a7d5]
|
|
40
|
-
- Updated dependencies [9db81a714]
|
|
41
|
-
- Updated dependencies [591e345e1]
|
|
42
|
-
- Updated dependencies [4de5303c5]
|
|
43
|
-
- Updated dependencies [95b139dc1]
|
|
44
|
-
- Updated dependencies [1d00f2d06]
|
|
45
|
-
- Updated dependencies [4ac7b2a1e]
|
|
46
|
-
- Updated dependencies [4998a9631]
|
|
47
|
-
- Updated dependencies [804d2bd6c]
|
|
48
|
-
- Updated dependencies [3909d3224]
|
|
49
|
-
- Updated dependencies [6a3d8a631]
|
|
50
|
-
- Updated dependencies [e18836b20]
|
|
51
|
-
- Updated dependencies [b1111d7b1]
|
|
52
|
-
- Updated dependencies [1595d458e]
|
|
53
|
-
- Updated dependencies [dca128112]
|
|
54
|
-
- Updated dependencies [9c778307]
|
|
55
|
-
- @akinon/next@2.0.154-rc.5
|
|
56
|
-
- @akinon/theme-contract@2.0.154-rc.5
|
|
8
|
+
- @akinon/next@2.0.154
|
|
9
|
+
- @akinon/theme-contract@2.0.154
|
|
57
10
|
|
|
58
11
|
## 2.0.153
|
|
59
12
|
|
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
|
|
4
|
+
"version": "2.0.154",
|
|
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
|
|
11
|
+
"@akinon/next": "2.0.154",
|
|
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
|
|
16
|
+
"@akinon/theme-contract": "2.0.154",
|
|
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
|
|
24
|
+
"@akinon/next": "2.0.154",
|
|
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",
|
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
resolveItemProduct
|
|
16
16
|
} from './item-template-utils';
|
|
17
17
|
import { ThemeItemScopeProvider } from './product-block-utils';
|
|
18
|
-
import { ThemeRowScopeProvider } from './row-scope';
|
|
19
18
|
|
|
20
19
|
export {
|
|
21
20
|
findItemTemplate,
|
|
@@ -121,15 +120,20 @@ export const ItemTemplateList = ({
|
|
|
121
120
|
const scopesProducts = publishesProductItems(block.type);
|
|
122
121
|
// The same slice the iterator applied, so clone N is handed the row it
|
|
123
122
|
// was built from rather than row N.
|
|
124
|
-
const
|
|
123
|
+
const scopedRows = scopesProducts
|
|
124
|
+
? sliceIteratorItems(block, rows).items
|
|
125
|
+
: rows;
|
|
125
126
|
|
|
126
127
|
return (
|
|
127
128
|
<>
|
|
128
129
|
{clones.map((clone, index) => {
|
|
129
130
|
const key = clone.id || `${template.id}-item-${index}`;
|
|
130
|
-
const rowData = slicedRows[index] ?? {};
|
|
131
131
|
const row = (
|
|
132
132
|
<ThemeBlock
|
|
133
|
+
// Keyed here so the non-scoped branch is the element it always
|
|
134
|
+
// was: wrapping it in anything, even a fragment, would change the
|
|
135
|
+
// element type at this position and remount every existing row.
|
|
136
|
+
key={key}
|
|
133
137
|
block={clone}
|
|
134
138
|
placeholderId={placeholderId}
|
|
135
139
|
sectionId={sectionId}
|
|
@@ -139,32 +143,22 @@ export const ItemTemplateList = ({
|
|
|
139
143
|
currentBreakpoint={currentBreakpoint}
|
|
140
144
|
/>
|
|
141
145
|
);
|
|
142
|
-
const scoped = scopesProducts
|
|
143
|
-
? resolveItemProduct(block.type, rowData)
|
|
144
|
-
: null;
|
|
145
146
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
if (!scopesProducts) return row;
|
|
148
|
+
|
|
149
|
+
const scoped = resolveItemProduct(block.type, scopedRows[index]);
|
|
150
|
+
|
|
151
|
+
// Context only — the provider renders no element, so the markup a
|
|
152
|
+
// published row produces is unchanged.
|
|
149
153
|
return (
|
|
150
|
-
<
|
|
154
|
+
<ThemeItemScopeProvider
|
|
151
155
|
key={key}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
156
|
+
product={
|
|
157
|
+
scoped ?? (isDesigner ? DESIGNER_SAMPLE_PRODUCT : null)
|
|
158
|
+
}
|
|
155
159
|
>
|
|
156
|
-
{
|
|
157
|
-
|
|
158
|
-
product={
|
|
159
|
-
scoped ?? (isDesigner ? DESIGNER_SAMPLE_PRODUCT : null)
|
|
160
|
-
}
|
|
161
|
-
>
|
|
162
|
-
{row}
|
|
163
|
-
</ThemeItemScopeProvider>
|
|
164
|
-
) : (
|
|
165
|
-
row
|
|
166
|
-
)}
|
|
167
|
-
</ThemeRowScopeProvider>
|
|
160
|
+
{row}
|
|
161
|
+
</ThemeItemScopeProvider>
|
|
168
162
|
);
|
|
169
163
|
})}
|
|
170
164
|
</>
|
|
@@ -9,7 +9,6 @@ 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';
|
|
13
12
|
import { BlockRendererProps } from './block-renderer-registry';
|
|
14
13
|
import {
|
|
15
14
|
DesignerPlaceholder,
|
|
@@ -17,7 +16,7 @@ import {
|
|
|
17
16
|
readToggle
|
|
18
17
|
} from './product-block-utils';
|
|
19
18
|
|
|
20
|
-
|
|
19
|
+
interface OrderItemLike {
|
|
21
20
|
id?: number;
|
|
22
21
|
price?: string;
|
|
23
22
|
quantity?: string | number;
|
|
@@ -36,7 +35,7 @@ type OrderItemLike = {
|
|
|
36
35
|
absolute_url?: string;
|
|
37
36
|
attributes?: Record<string, unknown>;
|
|
38
37
|
} | null;
|
|
39
|
-
}
|
|
38
|
+
}
|
|
40
39
|
|
|
41
40
|
interface AddressLike {
|
|
42
41
|
first_name?: string;
|
|
@@ -373,9 +372,7 @@ const formatAddress = (address: AddressLike | null | undefined): string[] => {
|
|
|
373
372
|
* order key is read from the URL; the editor owns which sections are exposed,
|
|
374
373
|
* every caption and the styling.
|
|
375
374
|
* A designer can mark one child as the row layout for the order lines; without
|
|
376
|
-
* one the built-in line renders.
|
|
377
|
-
* built-in line through `registerOrderLineActions`; they render inside the
|
|
378
|
-
* line's `item-actions` part.
|
|
375
|
+
* one the built-in line renders.
|
|
379
376
|
* Properties: numberLabel, numberTag, dateLabel, headerTotalLabel,
|
|
380
377
|
* showInvoiceLink, invoiceLinkLabel, showCancelLink, cancelLinkLabel,
|
|
381
378
|
* showReturnLink, returnLinkLabel, cancelHrefBase, showShipmentStatus, showItems, showImage, imageWidth,
|
|
@@ -394,8 +391,8 @@ const formatAddress = (address: AddressLike | null | undefined): string[] => {
|
|
|
394
391
|
* Parts: detail, header, number, date, header-total, status,
|
|
395
392
|
* invoice-link, cancel-link, return-link, list, item, image, name, quantity,
|
|
396
393
|
* color, size, item-status, item-tracking, item-return-code,
|
|
397
|
-
* item-return-method, item-return-carrier, item-pickup-date,
|
|
398
|
-
*
|
|
394
|
+
* item-return-method, item-return-carrier, item-pickup-date, price, shipment,
|
|
395
|
+
* delivery, tracking, addresses, address, address-label, totals, total-row,
|
|
399
396
|
* total-label, total-value, grand-total, payment-info, payment-info-label.
|
|
400
397
|
*/
|
|
401
398
|
const OrderDetailBlock = ({
|
|
@@ -933,21 +930,6 @@ const OrderDetailBlock = ({
|
|
|
933
930
|
showPickupDate && returnDetails.pickupDate
|
|
934
931
|
? formatPickupDate(returnDetails.pickupDate, locale)
|
|
935
932
|
: '';
|
|
936
|
-
const lineActions = renderOrderLineActions({
|
|
937
|
-
order,
|
|
938
|
-
line,
|
|
939
|
-
isDesigner
|
|
940
|
-
});
|
|
941
|
-
const showLineMeta =
|
|
942
|
-
showShipmentStatus &&
|
|
943
|
-
Boolean(
|
|
944
|
-
item.status?.label ||
|
|
945
|
-
trackingValue ||
|
|
946
|
-
returnCodeValue ||
|
|
947
|
-
returnMethodValue ||
|
|
948
|
-
returnCarrierValue ||
|
|
949
|
-
pickupDateValue
|
|
950
|
-
);
|
|
951
933
|
|
|
952
934
|
return (
|
|
953
935
|
<li
|
|
@@ -1063,100 +1045,98 @@ const OrderDetailBlock = ({
|
|
|
1063
1045
|
</div>
|
|
1064
1046
|
</div>
|
|
1065
1047
|
|
|
1066
|
-
{
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
</div>
|
|
1159
|
-
)}
|
|
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
|
+
)}
|
|
1160
1140
|
</div>
|
|
1161
1141
|
</li>
|
|
1162
1142
|
);
|
|
@@ -13,14 +13,7 @@ import { readToggle } from './product-block-utils';
|
|
|
13
13
|
import { BlockRendererProps } from './block-renderer-registry';
|
|
14
14
|
|
|
15
15
|
interface OrderItemLike {
|
|
16
|
-
|
|
17
|
-
status?: { value?: string | number; label?: string };
|
|
18
|
-
product?: {
|
|
19
|
-
image?: string;
|
|
20
|
-
name?: string;
|
|
21
|
-
sku?: string;
|
|
22
|
-
base_code?: string;
|
|
23
|
-
} | null;
|
|
16
|
+
product?: { image?: string; name?: string } | null;
|
|
24
17
|
}
|
|
25
18
|
|
|
26
19
|
interface OrderLike {
|
|
@@ -255,23 +248,7 @@ const OrderListBlock = ({
|
|
|
255
248
|
maxImages
|
|
256
249
|
),
|
|
257
250
|
url: 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
|
-
)
|
|
251
|
+
href: hrefFor(order)
|
|
275
252
|
}));
|
|
276
253
|
|
|
277
254
|
// A designer who marked a child as the row layout gets that layout repeated
|
|
@@ -41,7 +41,7 @@ const orderPayload = {
|
|
|
41
41
|
orderitem_set: [
|
|
42
42
|
{
|
|
43
43
|
price: '649.95',
|
|
44
|
-
status: {
|
|
44
|
+
status: { label: 'Shipped' },
|
|
45
45
|
product: {
|
|
46
46
|
name: 'Spring Sneaker',
|
|
47
47
|
image: '/i.jpg',
|
|
@@ -63,19 +63,6 @@ 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');
|
|
79
66
|
});
|
|
80
67
|
|
|
81
68
|
it('does not publish the identity fields the raw payload carries', () => {
|
|
@@ -59,9 +59,6 @@ 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
|
-
|
|
65
62
|
const humanizeSlug = (value: string) =>
|
|
66
63
|
value
|
|
67
64
|
.split('-')
|
|
@@ -482,7 +479,6 @@ export const buildOrderPageContext = ({
|
|
|
482
479
|
unitPrice: toStringValue(item.price),
|
|
483
480
|
quantity: 1,
|
|
484
481
|
status: toStringValue(toRecord(item.status).label),
|
|
485
|
-
statusValue: toCodeValue(toRecord(item.status).value),
|
|
486
482
|
currency
|
|
487
483
|
};
|
|
488
484
|
});
|
package/src/custom/README.md
CHANGED
|
@@ -303,26 +303,6 @@ attached". The editor's **Use preview** toggle turns it off while the editor
|
|
|
303
303
|
stays open, so the canvas behaves like the live site: whatever you held open
|
|
304
304
|
closes and links navigate again.
|
|
305
305
|
|
|
306
|
-
## Reading the row a block is placed in
|
|
307
|
-
|
|
308
|
-
A block dropped into a list's row layout (the child a designer marked as the
|
|
309
|
-
row template of an order list, a product grid, an address list, …) renders once
|
|
310
|
-
per row. The product lists open the product scope `useThemeProduct()` reads;
|
|
311
|
-
every list also opens a row scope that carries the row itself:
|
|
312
|
-
|
|
313
|
-
```ts
|
|
314
|
-
import { useThemeRowScope } from '@akinon/pz-theme/src/blocks/row-scope';
|
|
315
|
-
|
|
316
|
-
const scope = useThemeRowScope();
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
Inside a row it returns `{ listType, row, index }`, for example
|
|
320
|
-
`listType: 'order-list'` with the order's `number` and `items` on `row`; anywhere
|
|
321
|
-
else it returns `null`. `row` is the same record the row's bindings resolve
|
|
322
|
-
against, so whatever a text block can bind to (`item.number`), code can read.
|
|
323
|
-
Import it from that deep path for the same reason as `useThemePageContext`: the
|
|
324
|
-
package root is not importable from a client module.
|
|
325
|
-
|
|
326
306
|
## Reporting what happened
|
|
327
307
|
|
|
328
308
|
A custom unit can report a moment — a strip was seen, a lookbook was opened, a
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { render, screen } from '@testing-library/react';
|
|
3
|
-
import { ItemTemplateList } from '../blocks/item-template';
|
|
4
|
-
import { useThemeProduct } from '../blocks/product-block-utils';
|
|
5
|
-
import { useThemeRowScope } from '../blocks/row-scope';
|
|
6
|
-
import blockRendererRegistry from '../blocks/block-renderer-registry';
|
|
7
|
-
import { resetCustomCatalog } from '../custom/catalog';
|
|
8
|
-
import { resetCustomCollection } from '../custom/collect';
|
|
9
|
-
import { isCustomThemeType, registerCustomBlocks } from '../custom';
|
|
10
|
-
import type { Block } from '../theme-block';
|
|
11
|
-
|
|
12
|
-
const READER = 'custom:acme:row-reader';
|
|
13
|
-
|
|
14
|
-
function RowReader() {
|
|
15
|
-
const scope = useThemeRowScope();
|
|
16
|
-
const product = useThemeProduct();
|
|
17
|
-
|
|
18
|
-
return (
|
|
19
|
-
<span
|
|
20
|
-
data-testid="reader"
|
|
21
|
-
data-list={scope?.listType ?? ''}
|
|
22
|
-
data-index={scope?.index ?? ''}
|
|
23
|
-
data-number={String(scope?.row.number ?? '')}
|
|
24
|
-
data-product={String(product?.pk ?? '')}
|
|
25
|
-
/>
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const template = (): Block =>
|
|
30
|
-
({
|
|
31
|
-
id: 'tpl',
|
|
32
|
-
type: 'group',
|
|
33
|
-
label: 'Row',
|
|
34
|
-
order: 0,
|
|
35
|
-
hidden: false,
|
|
36
|
-
value: '',
|
|
37
|
-
properties: { role: { desktop: 'item-template' } },
|
|
38
|
-
styles: {},
|
|
39
|
-
blocks: [
|
|
40
|
-
{
|
|
41
|
-
id: 'tpl-reader',
|
|
42
|
-
type: READER,
|
|
43
|
-
label: 'Reader',
|
|
44
|
-
order: 0,
|
|
45
|
-
hidden: false,
|
|
46
|
-
value: '',
|
|
47
|
-
properties: {},
|
|
48
|
-
styles: {},
|
|
49
|
-
blocks: []
|
|
50
|
-
}
|
|
51
|
-
]
|
|
52
|
-
} as Block);
|
|
53
|
-
|
|
54
|
-
const list = (type: string): Block =>
|
|
55
|
-
({
|
|
56
|
-
id: 'list',
|
|
57
|
-
type,
|
|
58
|
-
label: 'List',
|
|
59
|
-
order: 0,
|
|
60
|
-
hidden: false,
|
|
61
|
-
value: '',
|
|
62
|
-
properties: {},
|
|
63
|
-
styles: {},
|
|
64
|
-
blocks: [template()]
|
|
65
|
-
} as Block);
|
|
66
|
-
|
|
67
|
-
const renderRows = (type: string, items: Record<string, unknown>[]) =>
|
|
68
|
-
render(
|
|
69
|
-
<ItemTemplateList
|
|
70
|
-
block={list(type)}
|
|
71
|
-
items={items}
|
|
72
|
-
placeholderId="p1"
|
|
73
|
-
sectionId="s1"
|
|
74
|
-
/>
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
beforeAll(() => {
|
|
78
|
-
registerCustomBlocks('acme', { 'row-reader': RowReader });
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
afterAll(() => {
|
|
82
|
-
const map = (
|
|
83
|
-
blockRendererRegistry as unknown as { renderers: Map<string, unknown> }
|
|
84
|
-
).renderers;
|
|
85
|
-
Array.from(map.keys())
|
|
86
|
-
.filter(isCustomThemeType)
|
|
87
|
-
.forEach((key) => map.delete(key));
|
|
88
|
-
resetCustomCatalog();
|
|
89
|
-
resetCustomCollection();
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
describe('the row scope', () => {
|
|
93
|
-
it('hands every repeated row to the blocks inside it, on any list', () => {
|
|
94
|
-
renderRows('order-list', [{ number: 'ORD-1' }, { number: 'ORD-2' }]);
|
|
95
|
-
|
|
96
|
-
const readers = screen.getAllByTestId('reader');
|
|
97
|
-
expect(readers).toHaveLength(2);
|
|
98
|
-
expect(readers[0]).toHaveAttribute('data-list', 'order-list');
|
|
99
|
-
expect(readers[0]).toHaveAttribute('data-index', '0');
|
|
100
|
-
expect(readers[0]).toHaveAttribute('data-number', 'ORD-1');
|
|
101
|
-
expect(readers[1]).toHaveAttribute('data-number', 'ORD-2');
|
|
102
|
-
expect(readers[0]).toHaveAttribute('data-product', '');
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('keeps the product scope on product lists next to the row scope', () => {
|
|
106
|
-
renderRows('product-grid', [{ pk: 7, name: 'Sneaker' }]);
|
|
107
|
-
|
|
108
|
-
const reader = screen.getByTestId('reader');
|
|
109
|
-
expect(reader).toHaveAttribute('data-list', 'product-grid');
|
|
110
|
-
expect(reader).toHaveAttribute('data-product', '7');
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('is closed outside a repeated row', () => {
|
|
114
|
-
render(<RowReader />);
|
|
115
|
-
|
|
116
|
-
expect(screen.getByTestId('reader')).toHaveAttribute('data-list', '');
|
|
117
|
-
});
|
|
118
|
-
});
|
|
@@ -1,54 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { render } from '@testing-library/react';
|
|
3
|
-
import OrderListBlock from '../order-list-block';
|
|
4
|
-
|
|
5
|
-
const captured: { items: Record<string, unknown>[] } = { items: [] };
|
|
6
|
-
|
|
7
|
-
jest.mock('../item-template', () => ({
|
|
8
|
-
__esModule: true,
|
|
9
|
-
hasItemTemplate: () => true,
|
|
10
|
-
ItemTemplateList: ({ items }: { items: Record<string, unknown>[] }) => {
|
|
11
|
-
captured.items = items;
|
|
12
|
-
return <div data-testid="item-template-list" />;
|
|
13
|
-
}
|
|
14
|
-
}));
|
|
15
|
-
|
|
16
|
-
jest.mock('next-auth/react', () => ({
|
|
17
|
-
useSession: () => ({ data: null, status: 'authenticated' })
|
|
18
|
-
}));
|
|
19
|
-
|
|
20
|
-
jest.mock('@akinon/next/hooks', () => ({
|
|
21
|
-
__esModule: true,
|
|
22
|
-
useLocalization: () => ({ locale: 'tr', currency: 'try' })
|
|
23
|
-
}));
|
|
24
|
-
|
|
25
|
-
jest.mock('@akinon/next/redux/hooks', () => ({
|
|
26
|
-
__esModule: true,
|
|
27
|
-
useAppSelector: () => undefined,
|
|
28
|
-
useAppDispatch: () => jest.fn()
|
|
29
|
-
}));
|
|
30
|
-
|
|
31
|
-
jest.mock('@akinon/next/data/client/account', () => ({
|
|
32
|
-
__esModule: true,
|
|
33
|
-
useGetOrdersQuery: () => ({
|
|
34
|
-
data: {
|
|
35
|
-
count: 1,
|
|
36
|
-
results: [
|
|
37
|
-
{
|
|
38
|
-
id: 42,
|
|
39
|
-
number: 'ORD-42',
|
|
40
|
-
amount: '100.00',
|
|
41
|
-
created_date: '2026-09-01T10:00:00Z',
|
|
42
|
-
status: { value: '550', label: 'Delivered' },
|
|
43
|
-
orderitem_set: [
|
|
44
|
-
{
|
|
45
|
-
id: 7,
|
|
46
|
-
status: { value: 550, label: 'Delivered' },
|
|
47
|
-
product: {
|
|
48
|
-
sku: 'SKU-1',
|
|
49
|
-
base_code: 'BASE-1',
|
|
50
|
-
name: 'Sneaker',
|
|
51
|
-
image: '/s.jpg'
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
status: { value: '400', label: 'Approved' },
|
|
56
|
-
product: { name: 'Boot' }
|
|
57
|
-
}
|
|
58
|
-
]
|
|
59
|
-
}
|
|
60
|
-
]
|
|
61
|
-
},
|
|
62
|
-
isLoading: false
|
|
63
|
-
})
|
|
64
|
-
}));
|
|
65
|
-
|
|
66
|
-
describe('order list rows', () => {
|
|
67
|
-
it('publishes the order items with their product codes and status code', () => {
|
|
68
|
-
render(
|
|
69
|
-
<OrderListBlock
|
|
70
|
-
block={{
|
|
71
|
-
id: 'order-list-1',
|
|
72
|
-
type: 'order-list',
|
|
73
|
-
properties: {},
|
|
74
|
-
styles: {},
|
|
75
|
-
blocks: []
|
|
76
|
-
}}
|
|
77
|
-
placeholderId="p1"
|
|
78
|
-
sectionId="s1"
|
|
79
|
-
/>
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
expect(captured.items).toHaveLength(1);
|
|
83
|
-
expect(captured.items[0]).toMatchObject({
|
|
84
|
-
id: 42,
|
|
85
|
-
number: 'ORD-42',
|
|
86
|
-
createdDate: '2026-09-01T10:00:00Z'
|
|
87
|
-
});
|
|
88
|
-
expect(captured.items[0].items).toEqual([
|
|
89
|
-
{
|
|
90
|
-
id: 7,
|
|
91
|
-
name: 'Sneaker',
|
|
92
|
-
image: '/s.jpg',
|
|
93
|
-
status: 'Delivered',
|
|
94
|
-
statusValue: '550',
|
|
95
|
-
product: {
|
|
96
|
-
sku: 'SKU-1',
|
|
97
|
-
base_code: 'BASE-1',
|
|
98
|
-
name: 'Sneaker',
|
|
99
|
-
image: '/s.jpg'
|
|
100
|
-
}
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
id: 1,
|
|
104
|
-
name: 'Boot',
|
|
105
|
-
image: '',
|
|
106
|
-
status: 'Approved',
|
|
107
|
-
statusValue: '400',
|
|
108
|
-
product: { sku: '', base_code: '', name: 'Boot', image: '' }
|
|
109
|
-
}
|
|
110
|
-
]);
|
|
111
|
-
});
|
|
112
|
-
});
|
|
@@ -1,45 +0,0 @@
|
|
|
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
|
-
};
|
package/src/blocks/row-scope.tsx
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
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);
|