@akinon/pz-theme 2.0.166-rc.0 → 2.0.166-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
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @akinon/pz-theme
|
|
2
2
|
|
|
3
|
+
## 2.0.166-rc.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f6871947: ZERO-5509: The order-detail block can hand its whole page to a child group with role `order-detail-layout`. The group is drawn once over the order in place of the built-in markup: its blocks bind `item.*` to the order (number, date, status, totals, discounts, installments, invoice / cancel / tracking links, addresses, payment), already formatted with the block's money style, and a repeat over `item.lines` draws the line rows. Line rows (also in the existing row layout) now carry color, size, tracking, carrier, return code / method / carrier / pickup date, and are scoped as a one-line order so the rate & review action placed in a line rates that line only. Clones of a repeat inside a row now open a row scope for their entry (`Block.itemScopeRow`). Without a layout the block renders exactly as before.
|
|
8
|
+
- @akinon/next@2.0.166-rc.2
|
|
9
|
+
- @akinon/theme-contract@2.0.166-rc.2
|
|
10
|
+
|
|
11
|
+
## 2.0.166-rc.1
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- e3c2bab2: ZERO-5521: restore the text block's useValueLocale import and the /api/auth no-store header rule that the automatic RC sync dropped
|
|
16
|
+
- Updated dependencies [e3c2bab2]
|
|
17
|
+
- @akinon/next@2.0.166-rc.1
|
|
18
|
+
- @akinon/theme-contract@2.0.166-rc.1
|
|
19
|
+
|
|
3
20
|
## 2.0.166-rc.0
|
|
4
21
|
|
|
5
22
|
### 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.166-rc.
|
|
4
|
+
"version": "2.0.166-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.166-rc.
|
|
11
|
+
"@akinon/next": "2.0.166-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.166-rc.
|
|
16
|
+
"@akinon/theme-contract": "2.0.166-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.166-rc.
|
|
24
|
+
"@akinon/next": "2.0.166-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,311 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import OrderDetailBlock from '../order-detail-block';
|
|
4
|
+
import blockRendererRegistry from '../block-renderer-registry';
|
|
5
|
+
import { useThemeRowScope } from '../row-scope';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ZERO-5509 — the order detail page as blocks. A child group with role
|
|
9
|
+
* `order-detail-layout` is drawn once over the order instead of the built-in
|
|
10
|
+
* markup: its blocks bind `item.*` to the order, a repeat over `item.lines`
|
|
11
|
+
* draws a row per line, and each line row is a row of its own, so a block that
|
|
12
|
+
* acts on "its row" (the rate & review action) answers for that line only.
|
|
13
|
+
*/
|
|
14
|
+
let order: Record<string, unknown> | null = null;
|
|
15
|
+
let isLoading = false;
|
|
16
|
+
|
|
17
|
+
jest.mock('next/navigation', () => ({
|
|
18
|
+
useParams: () => ({ id: '42' }),
|
|
19
|
+
useSearchParams: () => null
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
jest.mock('next-auth/react', () => ({
|
|
23
|
+
useSession: () => ({ data: null, status: 'authenticated' })
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
jest.mock('@akinon/next/hooks', () => ({
|
|
27
|
+
__esModule: true,
|
|
28
|
+
useLocalization: () => ({ locale: 'tr', currency: 'try' })
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
jest.mock('@akinon/next/data/client/account', () => ({
|
|
32
|
+
__esModule: true,
|
|
33
|
+
useGetOrderQuery: () => ({ data: order, isLoading })
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
// Stands in for the rate & review action: shows which row it was handed.
|
|
37
|
+
const RowProbe = () => {
|
|
38
|
+
const scope = useThemeRowScope();
|
|
39
|
+
const row = (scope?.row ?? {}) as {
|
|
40
|
+
number?: string;
|
|
41
|
+
items?: Array<{ product?: { sku?: string } }>;
|
|
42
|
+
};
|
|
43
|
+
return (
|
|
44
|
+
<span data-testid="row-probe">
|
|
45
|
+
{`${row.number}:${(row.items ?? [])
|
|
46
|
+
.map((item) => item.product?.sku)
|
|
47
|
+
.join(',')}`}
|
|
48
|
+
</span>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
blockRendererRegistry.register('test-row-probe', RowProbe as never);
|
|
52
|
+
|
|
53
|
+
const line = (id: number, sku: string, color: string, size: string) => ({
|
|
54
|
+
id,
|
|
55
|
+
price: '5499.00',
|
|
56
|
+
quantity: 1,
|
|
57
|
+
status: { value: '550', label: 'Teslim edildi' },
|
|
58
|
+
tracking_number: `TRK${id}`,
|
|
59
|
+
tracking_url: `https://carrier.example/${id}`,
|
|
60
|
+
product: {
|
|
61
|
+
pk: id,
|
|
62
|
+
sku,
|
|
63
|
+
base_code: `BASE-${id}`,
|
|
64
|
+
name: `Gömlek ${id}`,
|
|
65
|
+
image: `/img-${id}.jpg`,
|
|
66
|
+
absolute_url: `/p-${id}/`,
|
|
67
|
+
attributes: { integration_renk_detay: color, filtrenebilir_beden: size }
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const ORDER = {
|
|
72
|
+
id: 42,
|
|
73
|
+
number: '20920397829',
|
|
74
|
+
created_date: '2026-06-10T10:00:00Z',
|
|
75
|
+
amount: '15499.00',
|
|
76
|
+
amount_without_discount: '15499.00',
|
|
77
|
+
discount_amount: '2499.00',
|
|
78
|
+
shipping_amount: '0.00',
|
|
79
|
+
status: { value: '400', label: 'Onaylandı' },
|
|
80
|
+
currency: { value: 'try', label: 'TRY' },
|
|
81
|
+
e_archive_url: 'https://invoice.example/42',
|
|
82
|
+
tracking_number: 'TRK1',
|
|
83
|
+
tracking_url: 'https://carrier.example/1',
|
|
84
|
+
installment_count: 3,
|
|
85
|
+
orderitem_set: [
|
|
86
|
+
line(1, 'SKU-1', 'Beyaz', 'S'),
|
|
87
|
+
line(2, 'SKU-2', 'Lacivert', 'M')
|
|
88
|
+
],
|
|
89
|
+
discountitem_set: [{ name: 'Sepet indirimi', amount: '2499.00' }],
|
|
90
|
+
shipping_address: {
|
|
91
|
+
first_name: 'İlker',
|
|
92
|
+
last_name: 'Gökçil',
|
|
93
|
+
line: 'Atatürk Cad. No:2/5',
|
|
94
|
+
district: { name: 'Atatürk Mah.' },
|
|
95
|
+
city: { name: 'İstanbul' }
|
|
96
|
+
},
|
|
97
|
+
billing_address: null,
|
|
98
|
+
bank: { name: 'İş Bankası' },
|
|
99
|
+
payment_option: { name: 'Kredi Kartı' }
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const text = (id: string, binding: string, extra = {}) => ({
|
|
103
|
+
id,
|
|
104
|
+
type: 'text',
|
|
105
|
+
label: id,
|
|
106
|
+
value: '',
|
|
107
|
+
properties: { tag: 'span', dataBinding: binding, ...extra },
|
|
108
|
+
styles: {},
|
|
109
|
+
blocks: []
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const layout = (overrides: Record<string, unknown> = {}) => ({
|
|
113
|
+
id: 'order-layout',
|
|
114
|
+
type: 'group',
|
|
115
|
+
label: 'Order Layout',
|
|
116
|
+
properties: { tag: 'div', role: 'order-detail-layout' },
|
|
117
|
+
styles: {},
|
|
118
|
+
blocks: [
|
|
119
|
+
text('number', 'item.number'),
|
|
120
|
+
text('date', 'item.date'),
|
|
121
|
+
text('total', 'item.total'),
|
|
122
|
+
{
|
|
123
|
+
...text('shipping-free', '', {
|
|
124
|
+
visibleWhenField: 'item.isFreeShipping',
|
|
125
|
+
visibleWhenMode: 'truthy'
|
|
126
|
+
}),
|
|
127
|
+
value: 'Ücretsiz'
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
...text('shipping-paid', 'item.shipping', {
|
|
131
|
+
visibleWhenField: 'item.isFreeShipping',
|
|
132
|
+
visibleWhenMode: 'falsy'
|
|
133
|
+
})
|
|
134
|
+
},
|
|
135
|
+
text(
|
|
136
|
+
'installments',
|
|
137
|
+
'{{item.installmentCount}} x {{item.installmentAmount}}'
|
|
138
|
+
),
|
|
139
|
+
text('address', 'item.shippingAddress'),
|
|
140
|
+
text('billing', 'item.billingAddress', {
|
|
141
|
+
visibleWhenField: 'item.hasBillingAddress',
|
|
142
|
+
visibleWhenMode: 'truthy'
|
|
143
|
+
}),
|
|
144
|
+
{
|
|
145
|
+
id: 'discounts',
|
|
146
|
+
type: 'group',
|
|
147
|
+
label: 'Discounts',
|
|
148
|
+
isIterator: true,
|
|
149
|
+
iteratorDataPath: 'item.discounts',
|
|
150
|
+
properties: { tag: 'div' },
|
|
151
|
+
styles: {},
|
|
152
|
+
blocks: [text('discount', '{{item.name}} {{item.amount}}')]
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: 'lines',
|
|
156
|
+
type: 'group',
|
|
157
|
+
label: 'Lines',
|
|
158
|
+
isIterator: true,
|
|
159
|
+
iteratorDataPath: 'item.lines',
|
|
160
|
+
properties: { tag: 'div' },
|
|
161
|
+
styles: {},
|
|
162
|
+
blocks: [
|
|
163
|
+
{
|
|
164
|
+
id: 'line',
|
|
165
|
+
type: 'group',
|
|
166
|
+
label: 'Line',
|
|
167
|
+
properties: { tag: 'div' },
|
|
168
|
+
styles: {},
|
|
169
|
+
blocks: [
|
|
170
|
+
text('line-name', 'item.name'),
|
|
171
|
+
text('line-color', 'Renk: {{item.color}}'),
|
|
172
|
+
text('line-size', 'Beden: {{item.size}}'),
|
|
173
|
+
text('line-tracking', 'item.trackingNumber'),
|
|
174
|
+
{
|
|
175
|
+
id: 'probe',
|
|
176
|
+
type: 'test-row-probe',
|
|
177
|
+
label: 'Probe',
|
|
178
|
+
properties: {},
|
|
179
|
+
styles: {},
|
|
180
|
+
blocks: []
|
|
181
|
+
}
|
|
182
|
+
]
|
|
183
|
+
}
|
|
184
|
+
]
|
|
185
|
+
}
|
|
186
|
+
],
|
|
187
|
+
...overrides
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const block = (blocks: unknown[] = [layout()]) =>
|
|
191
|
+
({
|
|
192
|
+
id: 'order-detail-1',
|
|
193
|
+
type: 'order-detail',
|
|
194
|
+
properties: {
|
|
195
|
+
colorAttributeKey: { desktop: 'integration_renk_detay' },
|
|
196
|
+
sizeAttributeKey: { desktop: 'filtrenebilir_beden' },
|
|
197
|
+
currencyDisplay: { desktop: 'custom' },
|
|
198
|
+
currencyText: { desktop: 'TL' },
|
|
199
|
+
currencyPosition: { desktop: 'after' },
|
|
200
|
+
decimals: { desktop: 'auto' }
|
|
201
|
+
},
|
|
202
|
+
styles: {},
|
|
203
|
+
blocks
|
|
204
|
+
} as never);
|
|
205
|
+
|
|
206
|
+
const renderBlock = (blocks?: unknown[], isDesigner = false) =>
|
|
207
|
+
render(
|
|
208
|
+
<OrderDetailBlock
|
|
209
|
+
block={block(blocks)}
|
|
210
|
+
placeholderId="p1"
|
|
211
|
+
sectionId="s1"
|
|
212
|
+
isDesigner={isDesigner}
|
|
213
|
+
/>
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
beforeEach(() => {
|
|
217
|
+
order = ORDER;
|
|
218
|
+
isLoading = false;
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe('order detail layout', () => {
|
|
222
|
+
it('draws the authored layout in place of the built-in markup', () => {
|
|
223
|
+
const { container } = renderBlock();
|
|
224
|
+
|
|
225
|
+
expect(
|
|
226
|
+
container.querySelector('[data-order-detail-layout]')
|
|
227
|
+
).not.toBeNull();
|
|
228
|
+
expect(container.querySelector('[data-order-detail-header]')).toBeNull();
|
|
229
|
+
expect(container.querySelector('[data-order-detail-items]')).toBeNull();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('binds the order-level values, formatted the way the block formats money', () => {
|
|
233
|
+
const { container } = renderBlock();
|
|
234
|
+
const textOf = (selector: string) =>
|
|
235
|
+
container.querySelector(`[data-block-id^="${selector}"]`)?.textContent;
|
|
236
|
+
|
|
237
|
+
expect(textOf('number')).toBe('20920397829');
|
|
238
|
+
expect(textOf('date')).toBe('10.06.2026');
|
|
239
|
+
expect(textOf('total')).toMatch(/^15\.499(,00)? TL$/);
|
|
240
|
+
expect(textOf('installments')).toMatch(/^3 x 5\.166,33 TL$/);
|
|
241
|
+
expect(textOf('address')).toBe(
|
|
242
|
+
'Atatürk Cad. No:2/5 Atatürk Mah. / İstanbul'
|
|
243
|
+
);
|
|
244
|
+
expect(textOf('discount')).toMatch(/^Sepet indirimi -2\.499(,00)? TL$/);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('shows and hides per the row conditions on the storefront', () => {
|
|
248
|
+
const { container } = renderBlock();
|
|
249
|
+
|
|
250
|
+
expect(container.textContent).toContain('Ücretsiz');
|
|
251
|
+
expect(
|
|
252
|
+
container.querySelector('[data-block-id^="shipping-paid"]')
|
|
253
|
+
).toBeNull();
|
|
254
|
+
// No billing address on this order: the bound block is hidden.
|
|
255
|
+
expect(container.querySelector('[data-block-id^="billing"]')).toBeNull();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('repeats a row per line with its color, size and tracking', () => {
|
|
259
|
+
const { container } = renderBlock();
|
|
260
|
+
const lines = container.querySelectorAll('[data-block-id^="line-color"]');
|
|
261
|
+
|
|
262
|
+
expect(lines).toHaveLength(2);
|
|
263
|
+
expect(lines[0].textContent).toBe('Renk: Beyaz');
|
|
264
|
+
expect(lines[1].textContent).toBe('Renk: Lacivert');
|
|
265
|
+
expect(
|
|
266
|
+
container.querySelectorAll('[data-block-id^="line-size"]')[1].textContent
|
|
267
|
+
).toBe('Beden: M');
|
|
268
|
+
expect(
|
|
269
|
+
container.querySelectorAll('[data-block-id^="line-tracking"]')[0]
|
|
270
|
+
.textContent
|
|
271
|
+
).toBe('TRK1');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('gives every line row its own row scope, as a one-line order', () => {
|
|
275
|
+
renderBlock();
|
|
276
|
+
|
|
277
|
+
expect(
|
|
278
|
+
screen.getAllByTestId('row-probe').map((el) => el.textContent)
|
|
279
|
+
).toEqual(['20920397829:SKU-1', '20920397829:SKU-2']);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('leaves a hidden layout off the storefront and draws the built-in order', () => {
|
|
283
|
+
const { container } = renderBlock([layout({ hidden: true })]);
|
|
284
|
+
|
|
285
|
+
expect(container.querySelector('[data-order-detail-layout]')).toBeNull();
|
|
286
|
+
expect(
|
|
287
|
+
container.querySelector('[data-order-detail-header]')
|
|
288
|
+
).not.toBeNull();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('keeps drawing the built-in order when there is no layout', () => {
|
|
292
|
+
const { container } = renderBlock([]);
|
|
293
|
+
|
|
294
|
+
expect(container.querySelector('[data-order-detail-layout]')).toBeNull();
|
|
295
|
+
expect(
|
|
296
|
+
container.querySelector('[data-order-detail-header]')
|
|
297
|
+
).not.toBeNull();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('draws the layout over the sample order on the canvas', () => {
|
|
301
|
+
order = null;
|
|
302
|
+
const { container } = renderBlock(undefined, true);
|
|
303
|
+
|
|
304
|
+
expect(
|
|
305
|
+
container.querySelector('[data-order-detail-layout]')
|
|
306
|
+
).not.toBeNull();
|
|
307
|
+
expect(
|
|
308
|
+
container.querySelectorAll('[data-block-id^="line-color"]').length
|
|
309
|
+
).toBeGreaterThan(0);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
@@ -8,9 +8,14 @@ import { useLocalization } from '@akinon/next/hooks';
|
|
|
8
8
|
import { getResponsiveValue, partAttrs } from '../utils';
|
|
9
9
|
import { formatDate, formatMoney } from './commerce-format';
|
|
10
10
|
import { readMoneyStyle } from './money-style';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
hasItemTemplate,
|
|
13
|
+
ITEM_TEMPLATE_ROLE,
|
|
14
|
+
ItemTemplateList
|
|
15
|
+
} from './item-template';
|
|
12
16
|
import { renderOrderLineActions } from './order-line-actions';
|
|
13
17
|
import { BlockRendererProps } from './block-renderer-registry';
|
|
18
|
+
import type { Block } from '../theme-block';
|
|
14
19
|
import {
|
|
15
20
|
DesignerPlaceholder,
|
|
16
21
|
groupOrderItems,
|
|
@@ -33,12 +38,15 @@ type OrderItemLike = {
|
|
|
33
38
|
status?: { value?: string; label?: string };
|
|
34
39
|
tracking_url?: string;
|
|
35
40
|
tracking_number?: string;
|
|
41
|
+
shipping_company?: { value?: string; label?: string } | null;
|
|
36
42
|
estimated_delivery_date?: string;
|
|
37
43
|
price_currency?: { value?: string; label?: string };
|
|
38
44
|
is_refundable?: boolean;
|
|
39
45
|
active_cancellation_request?: unknown;
|
|
40
46
|
product?: {
|
|
41
47
|
pk?: number;
|
|
48
|
+
sku?: string;
|
|
49
|
+
base_code?: string;
|
|
42
50
|
name?: string;
|
|
43
51
|
image?: string;
|
|
44
52
|
absolute_url?: string;
|
|
@@ -70,6 +78,7 @@ interface OrderLike {
|
|
|
70
78
|
shipping_amount?: string;
|
|
71
79
|
tracking_url?: string;
|
|
72
80
|
tracking_number?: string;
|
|
81
|
+
shipping_company?: { value?: string; label?: string } | null;
|
|
73
82
|
status?: { value?: string; label?: string };
|
|
74
83
|
currency?: { value?: string; label?: string };
|
|
75
84
|
orderitem_set?: OrderItemLike[];
|
|
@@ -108,6 +117,19 @@ const ORDER_PARAM_KEYS = [
|
|
|
108
117
|
// opts into h1/h2/h3 when the order number is the page's main heading.
|
|
109
118
|
const NUMBER_TAGS = ['span', 'div', 'h1', 'h2', 'h3'];
|
|
110
119
|
|
|
120
|
+
/**
|
|
121
|
+
* A child group marked with this role is the whole order page, drawn in place
|
|
122
|
+
* of everything the block renders itself (ZERO-5509). It is repeated once, over
|
|
123
|
+
* the order: its blocks bind `item.*` to the order (number, date, totals,
|
|
124
|
+
* addresses, payment), and a repeat inside it over `item.lines` draws one row
|
|
125
|
+
* per order line, so the header, the line rows, the totals and every caption
|
|
126
|
+
* are blocks the panel arranges instead of fixed markup styled with CSS.
|
|
127
|
+
*/
|
|
128
|
+
export const ORDER_LAYOUT_ROLE = 'order-detail-layout';
|
|
129
|
+
|
|
130
|
+
const readChildRole = (child: Block, breakpoint: string): string =>
|
|
131
|
+
String(getResponsiveValue(child.properties?.role, breakpoint, '') ?? '');
|
|
132
|
+
|
|
111
133
|
const firstString = (
|
|
112
134
|
read: (key: string) => unknown,
|
|
113
135
|
keys: string[]
|
|
@@ -673,8 +695,16 @@ const OrderDetailBlock = ({
|
|
|
673
695
|
? {}
|
|
674
696
|
: { width: '50%', marginLeft: '50%' };
|
|
675
697
|
|
|
698
|
+
// A row publishes every field the built-in line can show, whatever its own
|
|
699
|
+
// show/hide toggles say: those govern the built-in line, and a designer's
|
|
700
|
+
// own layout decides for itself what to draw (ZERO-5509).
|
|
676
701
|
const lineRows = groupedItems.map((line) => {
|
|
677
702
|
const lineCurrency = line.item.price_currency?.value || currency;
|
|
703
|
+
const attributes = line.item.product?.attributes;
|
|
704
|
+
const returnDetails = resolveReturnDetails(
|
|
705
|
+
line.item.active_cancellation_request
|
|
706
|
+
);
|
|
707
|
+
const lineStatusValue = String(line.item.status?.value ?? '');
|
|
678
708
|
|
|
679
709
|
return {
|
|
680
710
|
key: line.key,
|
|
@@ -694,7 +724,43 @@ const OrderDetailBlock = ({
|
|
|
694
724
|
...moneyStyle
|
|
695
725
|
}),
|
|
696
726
|
status: line.item.status?.label || '',
|
|
697
|
-
statusValue:
|
|
727
|
+
statusValue: lineStatusValue,
|
|
728
|
+
color: resolveAttribute(attributes, colorAttributeKey),
|
|
729
|
+
size: resolveAttribute(attributes, sizeAttributeKey),
|
|
730
|
+
trackingNumber: line.item.tracking_number || '',
|
|
731
|
+
trackingUrl: isDesigner ? '#' : line.item.tracking_url || '',
|
|
732
|
+
shippingCompany: line.item.shipping_company?.label || '',
|
|
733
|
+
returnCode: resolveReturnCode(line.item.active_cancellation_request),
|
|
734
|
+
returnMethod:
|
|
735
|
+
returnDetails.method === 'appointment'
|
|
736
|
+
? appointmentReturnLabel
|
|
737
|
+
: returnDetails.method === 'standard'
|
|
738
|
+
? standardReturnLabel
|
|
739
|
+
: '',
|
|
740
|
+
returnCarrier: returnDetails.carrier,
|
|
741
|
+
pickupDate: returnDetails.pickupDate
|
|
742
|
+
? formatPickupDate(returnDetails.pickupDate, locale)
|
|
743
|
+
: '',
|
|
744
|
+
// The line seen as a one-line order, for blocks that act on an order
|
|
745
|
+
// from inside a row — the rate & review "Rate" action reads `id`,
|
|
746
|
+
// `number` and `items` off its row, so inside a line row it offers a
|
|
747
|
+
// review for that line's product only.
|
|
748
|
+
id: order.id ?? '',
|
|
749
|
+
number: order.number || orderKey,
|
|
750
|
+
createdDate: order.created_date || '',
|
|
751
|
+
items: [
|
|
752
|
+
{
|
|
753
|
+
id: line.item.id ?? line.key,
|
|
754
|
+
name: line.name,
|
|
755
|
+
image: line.image,
|
|
756
|
+
statusValue: lineStatusValue,
|
|
757
|
+
product: {
|
|
758
|
+
sku: line.item.product?.sku || '',
|
|
759
|
+
base_code: line.item.product?.base_code || '',
|
|
760
|
+
name: line.name
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
]
|
|
698
764
|
};
|
|
699
765
|
});
|
|
700
766
|
|
|
@@ -778,6 +844,116 @@ const OrderDetailBlock = ({
|
|
|
778
844
|
);
|
|
779
845
|
};
|
|
780
846
|
|
|
847
|
+
// The designer's whole-order layout, when there is one: drawn once over
|
|
848
|
+
// the order instead of everything below. Hidden on the storefront when the
|
|
849
|
+
// author hid it, still drawn on the canvas so it can be designed.
|
|
850
|
+
const layoutChild =
|
|
851
|
+
(block.blocks || []).find(
|
|
852
|
+
(child) =>
|
|
853
|
+
readChildRole(child, currentBreakpoint) === ORDER_LAYOUT_ROLE &&
|
|
854
|
+
(isDesigner || !child.hidden)
|
|
855
|
+
) ?? null;
|
|
856
|
+
|
|
857
|
+
if (layoutChild) {
|
|
858
|
+
const shippingLines = formatAddress(order.shipping_address);
|
|
859
|
+
const billingLines = formatAddress(order.billing_address);
|
|
860
|
+
const pieceCount = groupedItems.reduce(
|
|
861
|
+
(sum, line) => sum + Number(line.quantity || 0),
|
|
862
|
+
0
|
|
863
|
+
);
|
|
864
|
+
const shippingAmount = Number(order.shipping_amount || 0);
|
|
865
|
+
const signedMoney = (value: unknown) => {
|
|
866
|
+
const amount = Math.abs(Number(value || 0));
|
|
867
|
+
return amount > 0 ? `-${money(String(amount))}` : money('0');
|
|
868
|
+
};
|
|
869
|
+
const deliveryLine = items.find((item) => item.estimated_delivery_date);
|
|
870
|
+
const linkOrInert = (href: string) => (isDesigner && href ? '#' : href);
|
|
871
|
+
|
|
872
|
+
// One row: the order. Every value is already formatted the way the block
|
|
873
|
+
// formats it (money style, locale date), so a text block bound to
|
|
874
|
+
// `item.total` prints "15.499 TL" exactly as the built-in header does.
|
|
875
|
+
const orderRows = [
|
|
876
|
+
{
|
|
877
|
+
key: String(order.id ?? order.number ?? orderKey),
|
|
878
|
+
id: order.id ?? '',
|
|
879
|
+
number: order.number || orderKey,
|
|
880
|
+
date: formatDate(order.created_date, locale),
|
|
881
|
+
createdDate: order.created_date || '',
|
|
882
|
+
status: order.status?.label || '',
|
|
883
|
+
statusValue: String(order.status?.value ?? ''),
|
|
884
|
+
total: money(order.amount),
|
|
885
|
+
pieceCount,
|
|
886
|
+
pieceSubtotal,
|
|
887
|
+
shipping: money(order.shipping_amount),
|
|
888
|
+
isFreeShipping: !(shippingAmount > 0),
|
|
889
|
+
discountTotal: signedMoney(order.discount_amount),
|
|
890
|
+
hasDiscount: Number(order.discount_amount || 0) !== 0,
|
|
891
|
+
discounts: (order.discountitem_set || []).map((discount, index) => ({
|
|
892
|
+
key: String(index),
|
|
893
|
+
name: discount.name || discountLabel,
|
|
894
|
+
amount: signedMoney(discount.amount)
|
|
895
|
+
})),
|
|
896
|
+
hasInstallments: installmentCount > 1,
|
|
897
|
+
installmentCount,
|
|
898
|
+
installmentAmount: perInstallment,
|
|
899
|
+
invoiceUrl: linkOrInert(order.e_archive_url || ''),
|
|
900
|
+
hasInvoice: Boolean(order.e_archive_url),
|
|
901
|
+
canCancel: offersCancel && Boolean(cancelHref),
|
|
902
|
+
canReturn: offersReturn && Boolean(cancelHref),
|
|
903
|
+
cancelUrl: linkOrInert(cancelHref),
|
|
904
|
+
trackingNumber: order.tracking_number || '',
|
|
905
|
+
trackingUrl: linkOrInert(order.tracking_url || ''),
|
|
906
|
+
hasTracking: Boolean(order.tracking_url || order.tracking_number),
|
|
907
|
+
shippingCompany: order.shipping_company?.label || '',
|
|
908
|
+
estimatedDelivery: deliveryLine
|
|
909
|
+
? formatDate(deliveryLine.estimated_delivery_date, locale)
|
|
910
|
+
: '',
|
|
911
|
+
shippingName: shippingLines.length > 1 ? shippingLines[0] : '',
|
|
912
|
+
shippingAddress: shippingLines[shippingLines.length - 1] || '',
|
|
913
|
+
billingName: billingLines.length > 1 ? billingLines[0] : '',
|
|
914
|
+
billingAddress: billingLines[billingLines.length - 1] || '',
|
|
915
|
+
hasBillingAddress: billingLines.length > 0,
|
|
916
|
+
bankName: order.bank?.name || '',
|
|
917
|
+
paymentMethod: order.payment_option?.name || '',
|
|
918
|
+
lineCount: lineRows.length,
|
|
919
|
+
lines: lineRows,
|
|
920
|
+
// The order's delivered lines, for an order-wide rate & review block
|
|
921
|
+
// placed outside the line repeat.
|
|
922
|
+
items: lineRows.flatMap((row) => row.items)
|
|
923
|
+
}
|
|
924
|
+
];
|
|
925
|
+
|
|
926
|
+
return (
|
|
927
|
+
<div
|
|
928
|
+
data-order-detail=""
|
|
929
|
+
data-order-detail-layout=""
|
|
930
|
+
style={{ width: '100%' }}
|
|
931
|
+
>
|
|
932
|
+
<ItemTemplateList
|
|
933
|
+
block={{
|
|
934
|
+
...block,
|
|
935
|
+
blocks: [
|
|
936
|
+
{
|
|
937
|
+
...layoutChild,
|
|
938
|
+
properties: {
|
|
939
|
+
...(layoutChild.properties || {}),
|
|
940
|
+
role: ITEM_TEMPLATE_ROLE
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
]
|
|
944
|
+
}}
|
|
945
|
+
items={orderRows}
|
|
946
|
+
placeholderId={placeholderId}
|
|
947
|
+
sectionId={sectionId}
|
|
948
|
+
isDesigner={isDesigner}
|
|
949
|
+
selectedBlockId={selectedBlockId}
|
|
950
|
+
currentBreakpoint={currentBreakpoint}
|
|
951
|
+
designerPlaceholderCount={1}
|
|
952
|
+
/>
|
|
953
|
+
</div>
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
|
|
781
957
|
return (
|
|
782
958
|
<div
|
|
783
959
|
data-order-detail=""
|
|
@@ -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
|
// Rich-text element defaults from the theme settings.
|
|
11
12
|
import {
|
|
12
13
|
THEME_RICH_TEXT_ATTRIBUTE,
|
package/src/theme-block.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type ThemeProduct
|
|
9
9
|
} from './blocks/product-block-utils';
|
|
10
10
|
import { DESIGNER_SAMPLE_PRODUCT } from './blocks/item-template-utils';
|
|
11
|
+
import { ThemeRowScopeProvider } from './blocks/row-scope';
|
|
11
12
|
import {
|
|
12
13
|
getInnerRootInlineStyles,
|
|
13
14
|
getResponsiveValue,
|
|
@@ -67,6 +68,14 @@ export interface Block {
|
|
|
67
68
|
* position a product card's click reports (ZERO-5305). Never authored.
|
|
68
69
|
*/
|
|
69
70
|
itemScopeIndex?: number;
|
|
71
|
+
/**
|
|
72
|
+
* Set on the clones of a repeat INSIDE a row (an order's lines inside the
|
|
73
|
+
* order): the entry the clone draws, opened here as its row scope so a block
|
|
74
|
+
* reading `useThemeRowScope()` answers for that entry. Never authored —
|
|
75
|
+
* written by `buildIteratorBlock` (ZERO-5509).
|
|
76
|
+
*/
|
|
77
|
+
itemScopeRow?: Record<string, unknown>;
|
|
78
|
+
itemScopeRowIndex?: number;
|
|
70
79
|
iteratorDataPath?: string;
|
|
71
80
|
/**
|
|
72
81
|
* Written by the property-switch pass (utils/property-switches.ts), never
|
|
@@ -240,7 +249,22 @@ export default function ThemeBlock({
|
|
|
240
249
|
</WithDesignerFeatures>
|
|
241
250
|
);
|
|
242
251
|
|
|
243
|
-
|
|
252
|
+
// A clone of a repeat inside a row: its entry is the row for every block
|
|
253
|
+
// inside it (see `Block.itemScopeRow`). Context only, no element.
|
|
254
|
+
const scopedRendered =
|
|
255
|
+
block.itemScopeRow === undefined ? (
|
|
256
|
+
rendered
|
|
257
|
+
) : (
|
|
258
|
+
<ThemeRowScopeProvider
|
|
259
|
+
listType="nested"
|
|
260
|
+
row={block.itemScopeRow}
|
|
261
|
+
index={block.itemScopeRowIndex ?? 0}
|
|
262
|
+
>
|
|
263
|
+
{rendered}
|
|
264
|
+
</ThemeRowScopeProvider>
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
if (block.itemScopeProduct === undefined) return scopedRendered;
|
|
244
268
|
|
|
245
269
|
// A clone of a collection repeat: the row is the ambient product for every
|
|
246
270
|
// block inside it (see `Block.itemScopeProduct`). Context only — the
|
|
@@ -254,7 +278,7 @@ export default function ThemeBlock({
|
|
|
254
278
|
}
|
|
255
279
|
index={block.itemScopeIndex}
|
|
256
280
|
>
|
|
257
|
-
{
|
|
281
|
+
{scopedRendered}
|
|
258
282
|
</ThemeItemScopeProvider>
|
|
259
283
|
);
|
|
260
284
|
}
|
|
@@ -1726,6 +1726,17 @@ export const buildIteratorBlock = ({
|
|
|
1726
1726
|
),
|
|
1727
1727
|
itemScopeIndex: index
|
|
1728
1728
|
}
|
|
1729
|
+
: {}),
|
|
1730
|
+
// A repeat inside a row (an order's lines inside the order) draws rows
|
|
1731
|
+
// of its own: each clone opens a row scope for its entry, so a block
|
|
1732
|
+
// that acts on "its row" — the rate & review action — answers for that
|
|
1733
|
+
// entry rather than for the row around it (ZERO-5509). Only entries
|
|
1734
|
+
// that are records, and only real ones: padding rows have no row.
|
|
1735
|
+
...(scopedItems !== null && isRecord(displayItems[index])
|
|
1736
|
+
? {
|
|
1737
|
+
itemScopeRow: displayItems[index] as Record<string, unknown>,
|
|
1738
|
+
itemScopeRowIndex: index
|
|
1739
|
+
}
|
|
1729
1740
|
: {})
|
|
1730
1741
|
};
|
|
1731
1742
|
|