@akinon/pz-theme 2.0.152-rc.0 → 2.0.152-rc.2
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 +18 -0
- package/package.json +4 -4
- package/src/__tests__/row-scope.test.tsx +118 -0
- package/src/blocks/__tests__/order-list-rows.test.tsx +112 -0
- package/src/blocks/item-template.tsx +25 -19
- package/src/blocks/order-list-block.tsx +25 -2
- package/src/blocks/row-scope.tsx +35 -0
- package/src/chrome/__tests__/page-context-builders.test.ts +14 -1
- package/src/chrome/page-context.ts +4 -0
- package/src/custom/README.md +20 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @akinon/pz-theme
|
|
2
2
|
|
|
3
|
+
## 2.0.152-rc.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [219164a3]
|
|
8
|
+
- Updated dependencies [9c778307]
|
|
9
|
+
- @akinon/next@2.0.152-rc.2
|
|
10
|
+
- @akinon/theme-contract@2.0.152-rc.2
|
|
11
|
+
|
|
12
|
+
## 2.0.152-rc.1
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- ad5240c4: ZERO-5254: add custom render and theme editor support to pz-rate-review
|
|
17
|
+
- Updated dependencies [97d29893]
|
|
18
|
+
- @akinon/next@2.0.152-rc.1
|
|
19
|
+
- @akinon/theme-contract@2.0.152-rc.1
|
|
20
|
+
|
|
3
21
|
## 2.0.152-rc.0
|
|
4
22
|
|
|
5
23
|
### 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.152-rc.
|
|
4
|
+
"version": "2.0.152-rc.2",
|
|
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.152-rc.
|
|
11
|
+
"@akinon/next": "2.0.152-rc.2",
|
|
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.152-rc.
|
|
16
|
+
"@akinon/theme-contract": "2.0.152-rc.2",
|
|
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.152-rc.
|
|
24
|
+
"@akinon/next": "2.0.152-rc.2",
|
|
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,118 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
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
|
+
});
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
resolveItemProduct
|
|
16
16
|
} from './item-template-utils';
|
|
17
17
|
import { ThemeItemScopeProvider } from './product-block-utils';
|
|
18
|
+
import { ThemeRowScopeProvider } from './row-scope';
|
|
18
19
|
|
|
19
20
|
export {
|
|
20
21
|
findItemTemplate,
|
|
@@ -120,20 +121,15 @@ export const ItemTemplateList = ({
|
|
|
120
121
|
const scopesProducts = publishesProductItems(block.type);
|
|
121
122
|
// The same slice the iterator applied, so clone N is handed the row it
|
|
122
123
|
// was built from rather than row N.
|
|
123
|
-
const
|
|
124
|
-
? sliceIteratorItems(block, rows).items
|
|
125
|
-
: rows;
|
|
124
|
+
const slicedRows = sliceIteratorItems(block, rows).items;
|
|
126
125
|
|
|
127
126
|
return (
|
|
128
127
|
<>
|
|
129
128
|
{clones.map((clone, index) => {
|
|
130
129
|
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}
|
|
137
133
|
block={clone}
|
|
138
134
|
placeholderId={placeholderId}
|
|
139
135
|
sectionId={sectionId}
|
|
@@ -143,22 +139,32 @@ export const ItemTemplateList = ({
|
|
|
143
139
|
currentBreakpoint={currentBreakpoint}
|
|
144
140
|
/>
|
|
145
141
|
);
|
|
142
|
+
const scoped = scopesProducts
|
|
143
|
+
? resolveItemProduct(block.type, rowData)
|
|
144
|
+
: null;
|
|
146
145
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
// Context only — the provider renders no element, so the markup a
|
|
152
|
-
// published row produces is unchanged.
|
|
146
|
+
// Context only — the providers render no element, so the markup a
|
|
147
|
+
// published row produces is unchanged. Every list opens the row
|
|
148
|
+
// scope; only product lists open the product scope on top of it.
|
|
153
149
|
return (
|
|
154
|
-
<
|
|
150
|
+
<ThemeRowScopeProvider
|
|
155
151
|
key={key}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
152
|
+
listType={block.type}
|
|
153
|
+
row={rowData}
|
|
154
|
+
index={index}
|
|
159
155
|
>
|
|
160
|
-
{
|
|
161
|
-
|
|
156
|
+
{scopesProducts ? (
|
|
157
|
+
<ThemeItemScopeProvider
|
|
158
|
+
product={
|
|
159
|
+
scoped ?? (isDesigner ? DESIGNER_SAMPLE_PRODUCT : null)
|
|
160
|
+
}
|
|
161
|
+
>
|
|
162
|
+
{row}
|
|
163
|
+
</ThemeItemScopeProvider>
|
|
164
|
+
) : (
|
|
165
|
+
row
|
|
166
|
+
)}
|
|
167
|
+
</ThemeRowScopeProvider>
|
|
162
168
|
);
|
|
163
169
|
})}
|
|
164
170
|
</>
|
|
@@ -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);
|
|
@@ -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
|
});
|
package/src/custom/README.md
CHANGED
|
@@ -303,6 +303,26 @@ 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
|
+
|
|
306
326
|
## Reporting what happened
|
|
307
327
|
|
|
308
328
|
A custom unit can report a moment — a strip was seen, a lookbook was opened, a
|