@akinon/pz-theme 2.0.157-beta.0 → 2.0.157-rc.12
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 +42 -7
- package/package.json +4 -4
- package/src/__tests__/editor-locale-bridge.test.tsx +32 -1
- package/src/__tests__/row-scope.test.tsx +118 -0
- package/src/blocks/__tests__/order-line-actions.test.tsx +54 -0
- package/src/blocks/__tests__/order-list-rows.test.tsx +112 -0
- package/src/blocks/__tests__/text-block.test.tsx +34 -6
- package/src/blocks/item-template.tsx +25 -19
- package/src/blocks/order-detail-block.tsx +116 -96
- package/src/blocks/order-line-actions.ts +45 -0
- package/src/blocks/order-list-block.tsx +25 -2
- package/src/blocks/row-scope.tsx +35 -0
- package/src/blocks/text-block.tsx +6 -2
- package/src/chrome/__tests__/page-context-builders.test.ts +14 -1
- package/src/chrome/page-context.ts +4 -0
- package/src/custom/README.md +120 -219
- package/src/custom/__tests__/use-theme-setting.test.tsx +97 -0
- package/src/custom/collect.ts +4 -9
- package/src/custom/property-types.ts +15 -15
- package/src/custom/use-theme-setting.ts +21 -5
- package/src/editor-locale-bridge.tsx +39 -11
- package/src/theme-placeholder-wrapper.tsx +3 -0
- package/src/utils/__tests__/sanitize-rich-text.test.ts +80 -0
- package/src/utils/sanitize-rich-text.ts +215 -0
|
@@ -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
|
-
|
|
20
|
+
type OrderItemLike = {
|
|
20
21
|
id?: number;
|
|
21
22
|
price?: string;
|
|
22
23
|
quantity?: string | number;
|
|
@@ -35,7 +36,7 @@ interface OrderItemLike {
|
|
|
35
36
|
absolute_url?: string;
|
|
36
37
|
attributes?: Record<string, unknown>;
|
|
37
38
|
} | null;
|
|
38
|
-
}
|
|
39
|
+
};
|
|
39
40
|
|
|
40
41
|
interface AddressLike {
|
|
41
42
|
first_name?: string;
|
|
@@ -372,7 +373,9 @@ const formatAddress = (address: AddressLike | null | undefined): string[] => {
|
|
|
372
373
|
* order key is read from the URL; the editor owns which sections are exposed,
|
|
373
374
|
* every caption and the styling.
|
|
374
375
|
* A designer can mark one child as the row layout for the order lines; without
|
|
375
|
-
* one the built-in line renders.
|
|
376
|
+
* one the built-in line renders. A brand may hang per-line actions on the
|
|
377
|
+
* built-in line through `registerOrderLineActions`; they render inside the
|
|
378
|
+
* line's `item-actions` part.
|
|
376
379
|
* Properties: numberLabel, numberTag, dateLabel, headerTotalLabel,
|
|
377
380
|
* showInvoiceLink, invoiceLinkLabel, showCancelLink, cancelLinkLabel,
|
|
378
381
|
* showReturnLink, returnLinkLabel, cancelHrefBase, showShipmentStatus, showItems, showImage, imageWidth,
|
|
@@ -390,7 +393,7 @@ const formatAddress = (address: AddressLike | null | undefined): string[] => {
|
|
|
390
393
|
* currencyText, currencyPosition.
|
|
391
394
|
* Parts: detail, header, number, date, header-total, status,
|
|
392
395
|
* invoice-link, cancel-link, return-link, list, item, image, name, quantity,
|
|
393
|
-
* color, size, item-status, item-tracking, item-return-code,
|
|
396
|
+
* color, size, item-status, item-tracking, item-return-code, item-actions,
|
|
394
397
|
* item-return-method, item-return-carrier, item-pickup-date, price, shipment,
|
|
395
398
|
* delivery, tracking, addresses, address, address-label, totals, total-row,
|
|
396
399
|
* total-label, total-value, grand-total, payment-info, payment-info-label.
|
|
@@ -930,6 +933,21 @@ const OrderDetailBlock = ({
|
|
|
930
933
|
showPickupDate && returnDetails.pickupDate
|
|
931
934
|
? formatPickupDate(returnDetails.pickupDate, locale)
|
|
932
935
|
: '';
|
|
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
|
+
);
|
|
933
951
|
|
|
934
952
|
return (
|
|
935
953
|
<li
|
|
@@ -1045,98 +1063,100 @@ const OrderDetailBlock = ({
|
|
|
1045
1063
|
</div>
|
|
1046
1064
|
</div>
|
|
1047
1065
|
|
|
1048
|
-
{
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
</span>
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
</span>
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
</span>
|
|
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
|
-
|
|
1066
|
+
{(showLineMeta || lineActions) && (
|
|
1067
|
+
<div
|
|
1068
|
+
style={{
|
|
1069
|
+
display: 'flex',
|
|
1070
|
+
flexDirection: 'column',
|
|
1071
|
+
alignItems: isMobile ? 'flex-start' : 'flex-end',
|
|
1072
|
+
textAlign: isMobile ? 'left' : 'right',
|
|
1073
|
+
gap: '4px',
|
|
1074
|
+
flex: isMobile ? '1 1 100%' : '0 0 auto'
|
|
1075
|
+
}}
|
|
1076
|
+
>
|
|
1077
|
+
{lineActions && (
|
|
1078
|
+
<div
|
|
1079
|
+
data-order-detail-item-actions=""
|
|
1080
|
+
{...partAttrs('item-actions')}
|
|
1081
|
+
>
|
|
1082
|
+
{lineActions}
|
|
1083
|
+
</div>
|
|
1084
|
+
)}
|
|
1085
|
+
{showLineMeta && item.status?.label && (
|
|
1086
|
+
<span
|
|
1087
|
+
data-order-detail-item-status=""
|
|
1088
|
+
data-order-status-value={item.status.value}
|
|
1089
|
+
{...partAttrs('item-status')}
|
|
1090
|
+
style={{ display: 'inline-block' }}
|
|
1091
|
+
>
|
|
1092
|
+
{item.status.label}
|
|
1093
|
+
</span>
|
|
1094
|
+
)}
|
|
1095
|
+
{/* Label and value in their own spans, so a
|
|
1096
|
+
composition can colour them apart. */}
|
|
1097
|
+
{showLineMeta && returnMethodValue && (
|
|
1098
|
+
<span
|
|
1099
|
+
data-order-detail-item-return-method=""
|
|
1100
|
+
{...partAttrs('item-return-method')}
|
|
1101
|
+
style={{ opacity: 0.8 }}
|
|
1102
|
+
>
|
|
1103
|
+
<span data-label="">{returnMethodLabel}</span>{' '}
|
|
1104
|
+
<span data-value="">{returnMethodValue}</span>
|
|
1105
|
+
</span>
|
|
1106
|
+
)}
|
|
1107
|
+
{showLineMeta && returnCarrierValue && (
|
|
1108
|
+
<span
|
|
1109
|
+
data-order-detail-item-return-carrier=""
|
|
1110
|
+
{...partAttrs('item-return-carrier')}
|
|
1111
|
+
style={{ opacity: 0.8 }}
|
|
1112
|
+
>
|
|
1113
|
+
<span data-label="">{returnCarrierLabel}</span>{' '}
|
|
1114
|
+
<span data-value="">{returnCarrierValue}</span>
|
|
1115
|
+
</span>
|
|
1116
|
+
)}
|
|
1117
|
+
{showLineMeta && pickupDateValue && (
|
|
1118
|
+
<span
|
|
1119
|
+
data-order-detail-item-pickup-date=""
|
|
1120
|
+
{...partAttrs('item-pickup-date')}
|
|
1121
|
+
style={{ opacity: 0.8 }}
|
|
1122
|
+
>
|
|
1123
|
+
<span data-label="">{pickupDateLabel}</span>{' '}
|
|
1124
|
+
<span data-value="">{pickupDateValue}</span>
|
|
1125
|
+
</span>
|
|
1126
|
+
)}
|
|
1127
|
+
{showLineMeta && trackingValue && (
|
|
1128
|
+
<span
|
|
1129
|
+
data-order-detail-item-tracking=""
|
|
1130
|
+
{...partAttrs('item-tracking')}
|
|
1131
|
+
style={{ opacity: 0.8 }}
|
|
1132
|
+
>
|
|
1133
|
+
{lineTrackingLabel}{' '}
|
|
1134
|
+
{item.tracking_url ? (
|
|
1135
|
+
<a
|
|
1136
|
+
href={isDesigner ? '#' : item.tracking_url}
|
|
1137
|
+
onClick={guard}
|
|
1138
|
+
target={isDesigner ? undefined : '_blank'}
|
|
1139
|
+
rel="noreferrer"
|
|
1140
|
+
style={{ color: 'inherit' }}
|
|
1141
|
+
>
|
|
1142
|
+
{item.tracking_number || item.tracking_url}
|
|
1143
|
+
</a>
|
|
1144
|
+
) : (
|
|
1145
|
+
trackingValue
|
|
1146
|
+
)}
|
|
1147
|
+
</span>
|
|
1148
|
+
)}
|
|
1149
|
+
{showLineMeta && returnCodeValue && (
|
|
1150
|
+
<span
|
|
1151
|
+
data-order-detail-item-return-code=""
|
|
1152
|
+
{...partAttrs('item-return-code')}
|
|
1153
|
+
style={{ opacity: 0.8 }}
|
|
1154
|
+
>
|
|
1155
|
+
{returnCodeLabel} {returnCodeValue}
|
|
1156
|
+
</span>
|
|
1157
|
+
)}
|
|
1158
|
+
</div>
|
|
1159
|
+
)}
|
|
1140
1160
|
</div>
|
|
1141
1161
|
</li>
|
|
1142
1162
|
);
|
|
@@ -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
|
-
|
|
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 =
|
|
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] ||
|
|
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
|
});
|