@akinon/pz-theme 2.0.154-rc.6 → 2.0.154-rc.7
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 +8 -0
- package/package.json +4 -4
- package/src/__tests__/account-slot-coverage.test.ts +1 -0
- package/src/__tests__/custom-css.test.ts +12 -0
- package/src/__tests__/route-slug.test.ts +7 -0
- package/src/blocks/__tests__/group-order-items.test.ts +87 -0
- package/src/blocks/__tests__/order-detail-return.test.tsx +208 -0
- package/src/blocks/order-detail-block.tsx +219 -8
- package/src/blocks/product-block-utils.tsx +49 -13
- package/src/chrome/placeholder-slugs.ts +4 -0
- package/src/chrome/route-slug.ts +7 -3
- package/src/utils/custom-css.ts +35 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @akinon/pz-theme
|
|
2
2
|
|
|
3
|
+
## 2.0.154-rc.7
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
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-rc.7
|
|
9
|
+
- @akinon/theme-contract@2.0.154-rc.7
|
|
10
|
+
|
|
3
11
|
## 2.0.154-rc.6
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akinon/pz-theme",
|
|
3
3
|
"description": "Theme package for Project Zero Next — ThemePlaceholder system and theme editor infrastructure",
|
|
4
|
-
"version": "2.0.154-rc.
|
|
4
|
+
"version": "2.0.154-rc.7",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "jest"
|
|
9
9
|
},
|
|
10
10
|
"peerDependencies": {
|
|
11
|
-
"@akinon/next": "2.0.154-rc.
|
|
11
|
+
"@akinon/next": "2.0.154-rc.7",
|
|
12
12
|
"react": "^18.0.0 || ^19.0.0",
|
|
13
13
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@akinon/theme-contract": "2.0.154-rc.
|
|
16
|
+
"@akinon/theme-contract": "2.0.154-rc.7",
|
|
17
17
|
"clsx": "2.1.1",
|
|
18
18
|
"lottie-web": "^5.13.0",
|
|
19
19
|
"react-multi-carousel": "2.8.4",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"zod": "4.4.1"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@akinon/next": "2.0.154-rc.
|
|
24
|
+
"@akinon/next": "2.0.154-rc.7",
|
|
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",
|
|
@@ -27,6 +27,7 @@ const EXPECTED_MOUNTS: Record<string, string> = {
|
|
|
27
27
|
accountDashboard: '(dashboard)/layout.tsx',
|
|
28
28
|
accountOrders: 'orders/(list)/layout.tsx',
|
|
29
29
|
accountOrderDetail: 'orders/[id]/(detail)/layout.tsx',
|
|
30
|
+
accountOrderCancellation: 'orders/[id]/cancellation/layout.tsx',
|
|
30
31
|
accountAddresses: 'address/layout.tsx',
|
|
31
32
|
accountProfile: 'profile/layout.tsx',
|
|
32
33
|
accountChangePassword: 'change-password/layout.tsx',
|
|
@@ -61,6 +61,18 @@ describe('scoped custom CSS', () => {
|
|
|
61
61
|
expect(css).toContain(`${SCOPE} button`);
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
+
it('leaves commas inside :has(), :is() and attribute values alone', () => {
|
|
65
|
+
// ZERO-5183: the second half of `:has(a, b)` used to come out as
|
|
66
|
+
// `:has(a, <scope> b)` — a selector that can never match.
|
|
67
|
+
const css = scopeCustomCss(
|
|
68
|
+
'& li:has([data-a], [data-b]) { display: flex; }\n& :is(p, span)[title="x, y"], em { color: red; }',
|
|
69
|
+
SCOPE
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
expect(css).toContain(`${SCOPE} li:has([data-a], [data-b]) {`);
|
|
73
|
+
expect(css).toContain(`${SCOPE} :is(p, span)[title="x, y"], ${SCOPE} em {`);
|
|
74
|
+
});
|
|
75
|
+
|
|
64
76
|
it('scopes inside a media query and keeps the condition', () => {
|
|
65
77
|
const css = scopeCustomCss(
|
|
66
78
|
'@media (max-width: 768px) { & [data-part="option"] { flex: 1 1 100%; } }',
|
|
@@ -80,6 +80,13 @@ describe('resolveBodySlug', () => {
|
|
|
80
80
|
PLACEHOLDER_SLUGS.accountAddresses
|
|
81
81
|
);
|
|
82
82
|
expect(resolveBodySlug('/users/orders/5/cancellation')).toBe(
|
|
83
|
+
PLACEHOLDER_SLUGS.accountOrderCancellation
|
|
84
|
+
);
|
|
85
|
+
expect(resolveBodySlug('/account/orders/5/cancellation/')).toBe(
|
|
86
|
+
PLACEHOLDER_SLUGS.accountOrderCancellation
|
|
87
|
+
);
|
|
88
|
+
// Any other screen under an order stays on the detail body.
|
|
89
|
+
expect(resolveBodySlug('/account/orders/5/invoice')).toBe(
|
|
83
90
|
PLACEHOLDER_SLUGS.accountOrderDetail
|
|
84
91
|
);
|
|
85
92
|
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { groupOrderItems } from '../product-block-utils';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ZERO-5183 — pz writes one order line per unit and the order blocks collapse
|
|
5
|
+
* the units of a product into one row. A row shows its first unit's status and
|
|
6
|
+
* return details, so units only share it while they are in the same state:
|
|
7
|
+
* returning 1 of 2 must not read as both returned.
|
|
8
|
+
*/
|
|
9
|
+
const product = { pk: 7, name: 'Gömlek', image: '/x.jpg', absolute_url: '#' };
|
|
10
|
+
|
|
11
|
+
const unit = (id: number, extra: Record<string, unknown> = {}) => ({
|
|
12
|
+
id,
|
|
13
|
+
price: '100.00',
|
|
14
|
+
quantity: 1,
|
|
15
|
+
status: { value: '550', label: 'Teslim Edildi' },
|
|
16
|
+
active_cancellation_request: null,
|
|
17
|
+
product,
|
|
18
|
+
...extra
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('groupOrderItems', () => {
|
|
22
|
+
it('still collapses units of a product that share a state', () => {
|
|
23
|
+
const rows = groupOrderItems([unit(1), unit(2), unit(3)]);
|
|
24
|
+
|
|
25
|
+
expect(rows).toHaveLength(1);
|
|
26
|
+
expect(rows[0]).toMatchObject({ key: '7', quantity: 3, total: 300 });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('gives a returned unit its own row', () => {
|
|
30
|
+
const returned = unit(2, {
|
|
31
|
+
status: { value: '600', label: 'İade Edildi' },
|
|
32
|
+
active_cancellation_request: { easy_return: { code: 'R1' } }
|
|
33
|
+
});
|
|
34
|
+
const rows = groupOrderItems([unit(1), returned]);
|
|
35
|
+
|
|
36
|
+
expect(rows.map((row) => [row.key, row.quantity, row.item.id])).toEqual([
|
|
37
|
+
['7', 1, 1],
|
|
38
|
+
['7-2', 1, 2]
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('splits on a pending request even before the status moves', () => {
|
|
43
|
+
const requested = unit(2, {
|
|
44
|
+
active_cancellation_request: [{ id: 31, easy_return: { code: 'R1' } }]
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(groupOrderItems([unit(1), requested])).toHaveLength(2);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('keeps units returned under the same request together, and separate requests apart', () => {
|
|
51
|
+
const returned = (id: number, code: string) =>
|
|
52
|
+
unit(id, {
|
|
53
|
+
status: { value: '600', label: 'İade Edildi' },
|
|
54
|
+
active_cancellation_request: { easy_return: { code } }
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(
|
|
58
|
+
groupOrderItems([returned(1, 'R1'), returned(2, 'R1')]).map(
|
|
59
|
+
(row) => row.quantity
|
|
60
|
+
)
|
|
61
|
+
).toEqual([2]);
|
|
62
|
+
expect(
|
|
63
|
+
groupOrderItems([returned(1, 'R1'), returned(2, 'R2')]).map(
|
|
64
|
+
(row) => row.key
|
|
65
|
+
)
|
|
66
|
+
).toEqual(['7', '7-2']);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('merges a later unit back into the row of its state', () => {
|
|
70
|
+
const cancelled = unit(2, { status: { value: '100', label: 'İptal' } });
|
|
71
|
+
const rows = groupOrderItems([unit(1), cancelled, unit(3)]);
|
|
72
|
+
|
|
73
|
+
expect(rows.map((row) => [row.key, row.quantity])).toEqual([
|
|
74
|
+
['7', 2],
|
|
75
|
+
['7-2', 1]
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('never merges lines without a product pk', () => {
|
|
80
|
+
const rows = groupOrderItems([
|
|
81
|
+
{ id: 1, price: '1', quantity: 1, product: { name: 'A' } },
|
|
82
|
+
{ id: 2, price: '1', quantity: 1, product: { name: 'A' } }
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
expect(rows.map((row) => row.key)).toEqual(['1', '2']);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import OrderDetailBlock from '../order-detail-block';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ZERO-5183 — the order detail is where an easy return starts and where a
|
|
7
|
+
* returned line says how it goes back: a "Kolay iade" link on an order that
|
|
8
|
+
* can be returned, and per line the method (a booked pickup or a standard
|
|
9
|
+
* drop-off), the carrier and the pickup day, each behind its own switch.
|
|
10
|
+
*/
|
|
11
|
+
let order: Record<string, unknown> | null = null;
|
|
12
|
+
|
|
13
|
+
jest.mock('next/navigation', () => ({
|
|
14
|
+
useParams: () => ({ id: '42' }),
|
|
15
|
+
useSearchParams: () => null
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
jest.mock('next-auth/react', () => ({
|
|
19
|
+
useSession: () => ({ data: null, status: 'authenticated' })
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
jest.mock('@akinon/next/hooks', () => ({
|
|
23
|
+
__esModule: true,
|
|
24
|
+
useLocalization: () => ({ locale: 'tr', currency: 'try' })
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
jest.mock('@akinon/next/data/client/account', () => ({
|
|
28
|
+
__esModule: true,
|
|
29
|
+
useGetOrderQuery: () => ({ data: order, isLoading: false })
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
const line = (id: number, request: unknown) => ({
|
|
33
|
+
id,
|
|
34
|
+
price: '5499.00',
|
|
35
|
+
quantity: 1,
|
|
36
|
+
status: { value: '600', label: 'İade Edildi' },
|
|
37
|
+
active_cancellation_request: request,
|
|
38
|
+
product: { pk: id, name: `Gömlek ${id}`, image: '/x.jpg', absolute_url: '#' }
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const blockOf = (properties: Record<string, unknown>) => ({
|
|
42
|
+
id: 'order-detail-1',
|
|
43
|
+
type: 'order-detail',
|
|
44
|
+
properties,
|
|
45
|
+
styles: {},
|
|
46
|
+
blocks: []
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const ALL_ON = {
|
|
50
|
+
showReturnLink: { desktop: true },
|
|
51
|
+
returnLinkLabel: { desktop: 'Kolay iade' },
|
|
52
|
+
showReturnMethod: { desktop: true },
|
|
53
|
+
returnMethodLabel: { desktop: 'İade yöntemi:' },
|
|
54
|
+
appointmentReturnLabel: { desktop: 'Randevulu' },
|
|
55
|
+
standardReturnLabel: { desktop: 'Standart' },
|
|
56
|
+
showReturnCarrier: { desktop: true },
|
|
57
|
+
returnCarrierLabel: { desktop: 'Kargo:' },
|
|
58
|
+
showPickupDate: { desktop: true },
|
|
59
|
+
pickupDateLabel: { desktop: 'Randevu tarihi:' }
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const texts = (root: ParentNode, part: string) =>
|
|
63
|
+
Array.from(root.querySelectorAll(`[data-part="${part}"]`)).map((e) =>
|
|
64
|
+
(e.textContent || '').replace(/\s+/g, ' ').trim()
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
order = {
|
|
69
|
+
id: 42,
|
|
70
|
+
number: 'G-42',
|
|
71
|
+
amount: '10998.00',
|
|
72
|
+
is_cancellable: false,
|
|
73
|
+
is_refundable: true,
|
|
74
|
+
status: { value: '550', label: 'Teslim edildi' },
|
|
75
|
+
orderitem_set: [
|
|
76
|
+
line(1, [
|
|
77
|
+
{
|
|
78
|
+
pickup_date: { date: '2026-04-11' },
|
|
79
|
+
easy_return: {
|
|
80
|
+
code: 'R-1',
|
|
81
|
+
shipping_company: { label: 'Hepsijet' }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
]),
|
|
85
|
+
line(2, {
|
|
86
|
+
easy_return: {
|
|
87
|
+
code: 'R-2',
|
|
88
|
+
shipping_company: { label: 'Yurtiçi Kargo' }
|
|
89
|
+
}
|
|
90
|
+
}),
|
|
91
|
+
{ ...line(3, null), status: { value: '550', label: 'Teslim edildi' } }
|
|
92
|
+
]
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('order detail — easy returns', () => {
|
|
97
|
+
it('links a returnable order to the easy-return screen', () => {
|
|
98
|
+
const { container } = render(
|
|
99
|
+
<OrderDetailBlock
|
|
100
|
+
block={blockOf(ALL_ON) as never}
|
|
101
|
+
placeholderId="p1"
|
|
102
|
+
sectionId="s1"
|
|
103
|
+
isDesigner={false}
|
|
104
|
+
/>
|
|
105
|
+
);
|
|
106
|
+
const link = container.querySelector('[data-part="return-link"]');
|
|
107
|
+
|
|
108
|
+
expect(link?.textContent).toContain('Kolay iade');
|
|
109
|
+
expect(link?.getAttribute('href')).toBe('/account/orders/42/cancellation');
|
|
110
|
+
// The cancel link stays for orders that can still be cancelled.
|
|
111
|
+
expect(container.querySelector('[data-part="cancel-link"]')).toBeNull();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('says how each returned line goes back', () => {
|
|
115
|
+
const { container } = render(
|
|
116
|
+
<OrderDetailBlock
|
|
117
|
+
block={blockOf(ALL_ON) as never}
|
|
118
|
+
placeholderId="p1"
|
|
119
|
+
sectionId="s1"
|
|
120
|
+
isDesigner={false}
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
expect(texts(container, 'item-return-method')).toEqual([
|
|
125
|
+
'İade yöntemi: Randevulu',
|
|
126
|
+
'İade yöntemi: Standart'
|
|
127
|
+
]);
|
|
128
|
+
expect(texts(container, 'item-return-carrier')).toEqual([
|
|
129
|
+
'Kargo: Hepsijet',
|
|
130
|
+
'Kargo: Yurtiçi Kargo'
|
|
131
|
+
]);
|
|
132
|
+
expect(texts(container, 'item-pickup-date')).toEqual([
|
|
133
|
+
'Randevu tarihi: 11.04.2026'
|
|
134
|
+
]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('keeps every new line behind its switch', () => {
|
|
138
|
+
const { container } = render(
|
|
139
|
+
<OrderDetailBlock
|
|
140
|
+
block={blockOf({}) as never}
|
|
141
|
+
placeholderId="p1"
|
|
142
|
+
sectionId="s1"
|
|
143
|
+
isDesigner={false}
|
|
144
|
+
/>
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
expect(container.querySelector('[data-part="return-link"]')).toBeNull();
|
|
148
|
+
expect(texts(container, 'item-return-method')).toEqual([]);
|
|
149
|
+
expect(texts(container, 'item-return-carrier')).toEqual([]);
|
|
150
|
+
expect(texts(container, 'item-pickup-date')).toEqual([]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('offers no return link on an order that cannot be returned', () => {
|
|
154
|
+
order = { ...order, is_refundable: false };
|
|
155
|
+
const { container } = render(
|
|
156
|
+
<OrderDetailBlock
|
|
157
|
+
block={blockOf(ALL_ON) as never}
|
|
158
|
+
placeholderId="p1"
|
|
159
|
+
sectionId="s1"
|
|
160
|
+
isDesigner={false}
|
|
161
|
+
/>
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
expect(container.querySelector('[data-part="return-link"]')).toBeNull();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('links only a delivered line that has no request yet', () => {
|
|
168
|
+
// As commerce answers after a return: the order and its lines stay
|
|
169
|
+
// refundable while the request is open.
|
|
170
|
+
const flagged: Record<string, unknown>[] = (
|
|
171
|
+
order.orderitem_set as Record<string, unknown>[]
|
|
172
|
+
).map((item) => ({ ...item, is_refundable: true }));
|
|
173
|
+
const returnLink = () =>
|
|
174
|
+
render(
|
|
175
|
+
<OrderDetailBlock
|
|
176
|
+
block={blockOf(ALL_ON) as never}
|
|
177
|
+
placeholderId="p1"
|
|
178
|
+
sectionId="s1"
|
|
179
|
+
isDesigner={false}
|
|
180
|
+
/>
|
|
181
|
+
).container.querySelector('[data-part="return-link"]');
|
|
182
|
+
|
|
183
|
+
// Line 3 has no request yet: it can still be returned.
|
|
184
|
+
order = { ...order, orderitem_set: flagged };
|
|
185
|
+
expect(returnLink()).not.toBeNull();
|
|
186
|
+
|
|
187
|
+
order = {
|
|
188
|
+
...order,
|
|
189
|
+
orderitem_set: flagged.map((item) =>
|
|
190
|
+
item.id === 3
|
|
191
|
+
? { ...item, active_cancellation_request: { id: 54766 } }
|
|
192
|
+
: item
|
|
193
|
+
)
|
|
194
|
+
};
|
|
195
|
+
expect(returnLink()).toBeNull();
|
|
196
|
+
|
|
197
|
+
// Commerce calls a shipped line refundable too; it is not returned yet.
|
|
198
|
+
order = {
|
|
199
|
+
...order,
|
|
200
|
+
orderitem_set: flagged.map((item) =>
|
|
201
|
+
item.id === 3
|
|
202
|
+
? { ...item, status: { value: '500', label: 'Gönderildi' } }
|
|
203
|
+
: item
|
|
204
|
+
)
|
|
205
|
+
};
|
|
206
|
+
expect(returnLink()).toBeNull();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -27,6 +27,7 @@ type OrderItemLike = {
|
|
|
27
27
|
tracking_number?: string;
|
|
28
28
|
estimated_delivery_date?: string;
|
|
29
29
|
price_currency?: { value?: string; label?: string };
|
|
30
|
+
is_refundable?: boolean;
|
|
30
31
|
active_cancellation_request?: unknown;
|
|
31
32
|
product?: {
|
|
32
33
|
pk?: number;
|
|
@@ -66,6 +67,7 @@ interface OrderLike {
|
|
|
66
67
|
orderitem_set?: OrderItemLike[];
|
|
67
68
|
discountitem_set?: DiscountItemLike[];
|
|
68
69
|
is_cancellable?: boolean;
|
|
70
|
+
is_refundable?: boolean;
|
|
69
71
|
e_archive_url?: string | null;
|
|
70
72
|
shipping_address?: AddressLike | null;
|
|
71
73
|
billing_address?: AddressLike | null;
|
|
@@ -113,6 +115,37 @@ const firstString = (
|
|
|
113
115
|
return '';
|
|
114
116
|
};
|
|
115
117
|
|
|
118
|
+
const hasOpenRequest = (request: unknown): boolean =>
|
|
119
|
+
Array.isArray(request) ? request.length > 0 : Boolean(request);
|
|
120
|
+
|
|
121
|
+
/** Commerce's "Teslim edildi". */
|
|
122
|
+
const DELIVERED = '550';
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Whether the order still has a line to return: delivered, refundable, with
|
|
126
|
+
* no request open on it. Commerce also marks a shipped line refundable, and
|
|
127
|
+
* keeps `is_refundable` true on the order and the line while a return request
|
|
128
|
+
* is open, so the order flag alone would offer a return before the parcel
|
|
129
|
+
* arrives and again after the shopper has asked. Lines that carry no flag at
|
|
130
|
+
* all (an older payload) leave the decision to the order's flag and status.
|
|
131
|
+
*/
|
|
132
|
+
const hasReturnableLine = (order: OrderLike): boolean => {
|
|
133
|
+
if (!order.is_refundable) return false;
|
|
134
|
+
|
|
135
|
+
const lines = order.orderitem_set ?? [];
|
|
136
|
+
|
|
137
|
+
if (!lines.some((item) => item && 'is_refundable' in item)) {
|
|
138
|
+
return String(order.status?.value ?? '') === DELIVERED;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return lines.some(
|
|
142
|
+
(item) =>
|
|
143
|
+
String(item?.status?.value ?? '') === DELIVERED &&
|
|
144
|
+
item?.is_refundable &&
|
|
145
|
+
!hasOpenRequest(item.active_cancellation_request)
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
116
149
|
/** Positive integers only; anything else falls back to the shipped size. */
|
|
117
150
|
const readSize = (value: unknown, fallback: number): number => {
|
|
118
151
|
const numeric = Math.round(Number(value));
|
|
@@ -146,6 +179,7 @@ const DESIGNER_SAMPLE_ORDER: OrderLike = {
|
|
|
146
179
|
status: { value: '10', label: 'Approved' },
|
|
147
180
|
currency: { value: 'try', label: 'TRY' },
|
|
148
181
|
is_cancellable: true,
|
|
182
|
+
is_refundable: true,
|
|
149
183
|
e_archive_url: '#',
|
|
150
184
|
installment_count: 3,
|
|
151
185
|
orderitem_set: [
|
|
@@ -159,7 +193,16 @@ const DESIGNER_SAMPLE_ORDER: OrderLike = {
|
|
|
159
193
|
tracking_number: '1Z999AA10123456784',
|
|
160
194
|
tracking_url: '#',
|
|
161
195
|
active_cancellation_request: [
|
|
162
|
-
{
|
|
196
|
+
{
|
|
197
|
+
easy_return: {
|
|
198
|
+
code: '12345678912345',
|
|
199
|
+
shipping_company: { value: 'carrier', label: 'Carrier' }
|
|
200
|
+
},
|
|
201
|
+
pickup_date: {
|
|
202
|
+
date: '2026-04-11',
|
|
203
|
+
hour_range: { start: '09:00:00', end: '18:00:00' }
|
|
204
|
+
}
|
|
205
|
+
}
|
|
163
206
|
],
|
|
164
207
|
product: {
|
|
165
208
|
pk: 1,
|
|
@@ -250,6 +293,63 @@ const resolveReturnCode = (value: unknown): string => {
|
|
|
250
293
|
return typeof code === 'string' ? code : '';
|
|
251
294
|
};
|
|
252
295
|
|
|
296
|
+
interface ReturnRequestLike {
|
|
297
|
+
pickup_date?: { date?: string } | null;
|
|
298
|
+
easy_return?: {
|
|
299
|
+
code?: string;
|
|
300
|
+
shipping_company?: { label?: string } | null;
|
|
301
|
+
} | null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* How a returned line goes back: `appointment` when the request booked a
|
|
306
|
+
* pickup (it carries a pickup day), `standard` for an easy return dropped off
|
|
307
|
+
* at a branch, nothing for a plain cancellation. The carrier and the pickup
|
|
308
|
+
* day come from the same request; both shapes of the field are probed, as
|
|
309
|
+
* for the return code.
|
|
310
|
+
*/
|
|
311
|
+
const resolveReturnDetails = (
|
|
312
|
+
value: unknown
|
|
313
|
+
): {
|
|
314
|
+
method: 'appointment' | 'standard' | null;
|
|
315
|
+
carrier: string;
|
|
316
|
+
pickupDate: string;
|
|
317
|
+
} => {
|
|
318
|
+
const entry = (Array.isArray(value) ? value[0] : value) as
|
|
319
|
+
| ReturnRequestLike
|
|
320
|
+
| null
|
|
321
|
+
| undefined;
|
|
322
|
+
const pickupDate = entry?.pickup_date?.date;
|
|
323
|
+
const carrier = entry?.easy_return?.shipping_company?.label;
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
method: pickupDate ? 'appointment' : entry?.easy_return ? 'standard' : null,
|
|
327
|
+
carrier: typeof carrier === 'string' ? carrier : '',
|
|
328
|
+
pickupDate: typeof pickupDate === 'string' ? pickupDate : ''
|
|
329
|
+
};
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
/** "2026-04-11" read as a calendar day in the page's locale (no time zone shift). */
|
|
333
|
+
const formatPickupDate = (value: string, locale?: string): string => {
|
|
334
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})/.exec(value);
|
|
335
|
+
if (!match) return value;
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
return new Intl.DateTimeFormat(locale || undefined, {
|
|
339
|
+
timeZone: 'UTC',
|
|
340
|
+
day: '2-digit',
|
|
341
|
+
month: '2-digit',
|
|
342
|
+
year: 'numeric'
|
|
343
|
+
}).format(
|
|
344
|
+
new Date(
|
|
345
|
+
Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]), 12)
|
|
346
|
+
)
|
|
347
|
+
);
|
|
348
|
+
} catch {
|
|
349
|
+
return value;
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
253
353
|
const formatAddress = (address: AddressLike | null | undefined): string[] => {
|
|
254
354
|
if (!address) return [];
|
|
255
355
|
const name = [address.first_name, address.last_name]
|
|
@@ -278,11 +378,13 @@ const formatAddress = (address: AddressLike | null | undefined): string[] => {
|
|
|
278
378
|
* line's `item-actions` part.
|
|
279
379
|
* Properties: numberLabel, numberTag, dateLabel, headerTotalLabel,
|
|
280
380
|
* showInvoiceLink, invoiceLinkLabel, showCancelLink, cancelLinkLabel,
|
|
281
|
-
* cancelHrefBase, showShipmentStatus, showItems, showImage, imageWidth,
|
|
381
|
+
* showReturnLink, returnLinkLabel, cancelHrefBase, showShipmentStatus, showItems, showImage, imageWidth,
|
|
282
382
|
* imageHeight, showQuantity, quantityLabel, showColor,
|
|
283
383
|
* colorLabel, colorAttributeKey, showSize, sizeLabel, sizeAttributeKey,
|
|
284
384
|
* showLineTracking, lineTrackingLabel, showReturnCode,
|
|
285
|
-
* returnCodeLabel,
|
|
385
|
+
* returnCodeLabel, showReturnMethod, returnMethodLabel,
|
|
386
|
+
* appointmentReturnLabel, standardReturnLabel, showReturnCarrier,
|
|
387
|
+
* returnCarrierLabel, showPickupDate, pickupDateLabel, showShippingAddress, shippingAddressLabel,
|
|
286
388
|
* showBillingAddress, billingAddressLabel, showTotals, showPieceCount,
|
|
287
389
|
* pieceCountLabel, showDiscountItems, showInstallmentInfo,
|
|
288
390
|
* installmentInfoLabel, subtotalLabel, discountLabel, shippingLabel,
|
|
@@ -290,9 +392,10 @@ const formatAddress = (address: AddressLike | null | undefined): string[] => {
|
|
|
290
392
|
* showPaymentInfo, paymentInfoLabel, decimals, currencyDisplay,
|
|
291
393
|
* currencyText, currencyPosition.
|
|
292
394
|
* Parts: detail, header, number, date, header-total, status,
|
|
293
|
-
* invoice-link, cancel-link, list, item, image, name, quantity,
|
|
294
|
-
* item-status, item-tracking, item-return-code,
|
|
295
|
-
*
|
|
395
|
+
* invoice-link, cancel-link, return-link, list, item, image, name, quantity,
|
|
396
|
+
* color, size, item-status, item-tracking, item-return-code,
|
|
397
|
+
* item-return-method, item-return-carrier, item-pickup-date, item-actions,
|
|
398
|
+
* price, shipment, delivery, tracking, addresses, address, address-label, totals, total-row,
|
|
296
399
|
* total-label, total-value, grand-total, payment-info, payment-info-label.
|
|
297
400
|
*/
|
|
298
401
|
const OrderDetailBlock = ({
|
|
@@ -402,6 +505,38 @@ const OrderDetailBlock = ({
|
|
|
402
505
|
false
|
|
403
506
|
);
|
|
404
507
|
const returnCodeLabel = text('returnCodeLabel', 'Return code:');
|
|
508
|
+
// ZERO-5183: the easy-return screens start from here, and a returned line
|
|
509
|
+
// says how it goes back. The cancel link only shows while the order can
|
|
510
|
+
// still be cancelled; a delivered order needs its own way in.
|
|
511
|
+
const showReturnLink = readToggle(
|
|
512
|
+
block.properties?.showReturnLink,
|
|
513
|
+
currentBreakpoint,
|
|
514
|
+
false
|
|
515
|
+
);
|
|
516
|
+
const returnLinkLabel = text('returnLinkLabel', 'Easy return');
|
|
517
|
+
const showReturnMethod = readToggle(
|
|
518
|
+
block.properties?.showReturnMethod,
|
|
519
|
+
currentBreakpoint,
|
|
520
|
+
false
|
|
521
|
+
);
|
|
522
|
+
const returnMethodLabel = text('returnMethodLabel', 'Return method:');
|
|
523
|
+
const appointmentReturnLabel = text(
|
|
524
|
+
'appointmentReturnLabel',
|
|
525
|
+
'Scheduled pickup'
|
|
526
|
+
);
|
|
527
|
+
const standardReturnLabel = text('standardReturnLabel', 'Standard');
|
|
528
|
+
const showReturnCarrier = readToggle(
|
|
529
|
+
block.properties?.showReturnCarrier,
|
|
530
|
+
currentBreakpoint,
|
|
531
|
+
false
|
|
532
|
+
);
|
|
533
|
+
const returnCarrierLabel = text('returnCarrierLabel', 'Carrier:');
|
|
534
|
+
const showPickupDate = readToggle(
|
|
535
|
+
block.properties?.showPickupDate,
|
|
536
|
+
currentBreakpoint,
|
|
537
|
+
false
|
|
538
|
+
);
|
|
539
|
+
const pickupDateLabel = text('pickupDateLabel', 'Pickup date:');
|
|
405
540
|
|
|
406
541
|
const showShippingAddress = readToggle(
|
|
407
542
|
block.properties?.showShippingAddress,
|
|
@@ -489,6 +624,12 @@ const OrderDetailBlock = ({
|
|
|
489
624
|
const money = (value: unknown) =>
|
|
490
625
|
formatMoney(value, { locale, currency, ...moneyStyle });
|
|
491
626
|
|
|
627
|
+
// The sample order is not a delivered one; it shows the link for styling.
|
|
628
|
+
const offersReturn =
|
|
629
|
+
order === DESIGNER_SAMPLE_ORDER
|
|
630
|
+
? Boolean(order.is_refundable)
|
|
631
|
+
: hasReturnableLine(order);
|
|
632
|
+
|
|
492
633
|
// Navigating inside the designer iframe would drop the author out of the page
|
|
493
634
|
// they are editing, so every link is inert there.
|
|
494
635
|
const guard = (event: React.MouseEvent) => {
|
|
@@ -667,7 +808,7 @@ const OrderDetailBlock = ({
|
|
|
667
808
|
{headerTotalLabel} {money(order.amount)}
|
|
668
809
|
</span>
|
|
669
810
|
)}
|
|
670
|
-
{(showInvoiceLink || showCancelLink) && (
|
|
811
|
+
{(showInvoiceLink || showCancelLink || showReturnLink) && (
|
|
671
812
|
<div
|
|
672
813
|
style={{
|
|
673
814
|
display: 'flex',
|
|
@@ -707,6 +848,21 @@ const OrderDetailBlock = ({
|
|
|
707
848
|
→ {cancelLinkLabel}
|
|
708
849
|
</NextLink>
|
|
709
850
|
)}
|
|
851
|
+
{showReturnLink && offersReturn && cancelHref && (
|
|
852
|
+
<NextLink
|
|
853
|
+
href={isDesigner ? '#' : cancelHref}
|
|
854
|
+
onClick={guard}
|
|
855
|
+
data-order-detail-return-link=""
|
|
856
|
+
{...partAttrs('return-link')}
|
|
857
|
+
style={{
|
|
858
|
+
color: 'inherit',
|
|
859
|
+
textDecoration: 'none',
|
|
860
|
+
whiteSpace: 'nowrap'
|
|
861
|
+
}}
|
|
862
|
+
>
|
|
863
|
+
→ {returnLinkLabel}
|
|
864
|
+
</NextLink>
|
|
865
|
+
)}
|
|
710
866
|
</div>
|
|
711
867
|
)}
|
|
712
868
|
</div>
|
|
@@ -761,6 +917,22 @@ const OrderDetailBlock = ({
|
|
|
761
917
|
const returnCodeValue = showReturnCode
|
|
762
918
|
? resolveReturnCode(item.active_cancellation_request)
|
|
763
919
|
: '';
|
|
920
|
+
const returnDetails = resolveReturnDetails(
|
|
921
|
+
item.active_cancellation_request
|
|
922
|
+
);
|
|
923
|
+
const returnMethodValue =
|
|
924
|
+
showReturnMethod && returnDetails.method
|
|
925
|
+
? returnDetails.method === 'appointment'
|
|
926
|
+
? appointmentReturnLabel
|
|
927
|
+
: standardReturnLabel
|
|
928
|
+
: '';
|
|
929
|
+
const returnCarrierValue = showReturnCarrier
|
|
930
|
+
? returnDetails.carrier
|
|
931
|
+
: '';
|
|
932
|
+
const pickupDateValue =
|
|
933
|
+
showPickupDate && returnDetails.pickupDate
|
|
934
|
+
? formatPickupDate(returnDetails.pickupDate, locale)
|
|
935
|
+
: '';
|
|
764
936
|
const lineActions = renderOrderLineActions({
|
|
765
937
|
order,
|
|
766
938
|
line,
|
|
@@ -768,7 +940,14 @@ const OrderDetailBlock = ({
|
|
|
768
940
|
});
|
|
769
941
|
const showLineMeta =
|
|
770
942
|
showShipmentStatus &&
|
|
771
|
-
Boolean(
|
|
943
|
+
Boolean(
|
|
944
|
+
item.status?.label ||
|
|
945
|
+
trackingValue ||
|
|
946
|
+
returnCodeValue ||
|
|
947
|
+
returnMethodValue ||
|
|
948
|
+
returnCarrierValue ||
|
|
949
|
+
pickupDateValue
|
|
950
|
+
);
|
|
772
951
|
|
|
773
952
|
return (
|
|
774
953
|
<li
|
|
@@ -913,6 +1092,38 @@ const OrderDetailBlock = ({
|
|
|
913
1092
|
{item.status.label}
|
|
914
1093
|
</span>
|
|
915
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
|
+
)}
|
|
916
1127
|
{showLineMeta && trackingValue && (
|
|
917
1128
|
<span
|
|
918
1129
|
data-order-detail-item-tracking=""
|
|
@@ -240,6 +240,8 @@ export interface OrderLineLike {
|
|
|
240
240
|
price?: string | number;
|
|
241
241
|
quantity?: string | number;
|
|
242
242
|
total_amount?: string | number;
|
|
243
|
+
status?: { value?: string | number; label?: string } | null;
|
|
244
|
+
active_cancellation_request?: unknown;
|
|
243
245
|
product?: {
|
|
244
246
|
pk?: number;
|
|
245
247
|
name?: string;
|
|
@@ -278,7 +280,10 @@ export const getLineImage = (
|
|
|
278
280
|
};
|
|
279
281
|
|
|
280
282
|
export interface GroupedOrderLine<T extends OrderLineLike> {
|
|
281
|
-
/**
|
|
283
|
+
/**
|
|
284
|
+
* Stable React key: the product pk when the payload carries one (with a
|
|
285
|
+
* `-2`, `-3`… suffix for the product's further rows, see groupOrderItems).
|
|
286
|
+
*/
|
|
282
287
|
key: string;
|
|
283
288
|
name: string;
|
|
284
289
|
image: string;
|
|
@@ -301,21 +306,28 @@ export interface GroupedOrderLine<T extends OrderLineLike> {
|
|
|
301
306
|
* `quantity` is summed rather than counted so a payload that does carry a line
|
|
302
307
|
* quantity (older commerce versions, and brands that merge lines server-side)
|
|
303
308
|
* is respected instead of being flattened back to one.
|
|
309
|
+
*
|
|
310
|
+
* Units of one product only share a row while nothing sets them apart: once
|
|
311
|
+
* one of them is cancelled or returned (its status moves on, or it carries a
|
|
312
|
+
* cancellation / return request) it gets a row of its own. A shared row shows
|
|
313
|
+
* its first unit's status and return details, so "return 1 of 2" would
|
|
314
|
+
* otherwise read as both units returned.
|
|
304
315
|
*/
|
|
305
316
|
export const groupOrderItems = <T extends OrderLineLike>(
|
|
306
317
|
items: T[] | null | undefined
|
|
307
318
|
): GroupedOrderLine<T>[] => {
|
|
308
319
|
const source = Array.isArray(items) ? items : [];
|
|
320
|
+
const groups: GroupedOrderLine<T>[] = [];
|
|
321
|
+
const byIdentity = new Map<string, GroupedOrderLine<T>>();
|
|
322
|
+
const rowsPerKey = new Map<string, number>();
|
|
309
323
|
|
|
310
|
-
|
|
324
|
+
source.forEach((item) => {
|
|
311
325
|
const pk = item.product?.pk;
|
|
312
|
-
const key = String(pk ?? item.id ?? accumulator.length);
|
|
313
326
|
// Lines without a product pk cannot be proven identical, so they stay
|
|
314
327
|
// separate rows instead of collapsing into an arbitrary neighbour.
|
|
315
|
-
const
|
|
316
|
-
pk === undefined || pk === null
|
|
317
|
-
|
|
318
|
-
: accumulator.find((entry) => entry.key === key);
|
|
328
|
+
const identity =
|
|
329
|
+
pk === undefined || pk === null ? null : `${pk}#${lineState(item)}`;
|
|
330
|
+
const existing = identity === null ? undefined : byIdentity.get(identity);
|
|
319
331
|
const quantity = parseAmount(item.quantity) ?? 1;
|
|
320
332
|
const lineTotal = parseAmount(item.total_amount || item.price);
|
|
321
333
|
|
|
@@ -324,11 +336,15 @@ export const groupOrderItems = <T extends OrderLineLike>(
|
|
|
324
336
|
existing.total =
|
|
325
337
|
lineTotal === null ? existing.total : (existing.total ?? 0) + lineTotal;
|
|
326
338
|
|
|
327
|
-
return
|
|
339
|
+
return;
|
|
328
340
|
}
|
|
329
341
|
|
|
330
|
-
|
|
331
|
-
|
|
342
|
+
const baseKey = String(pk ?? item.id ?? groups.length);
|
|
343
|
+
const rows = (rowsPerKey.get(baseKey) ?? 0) + 1;
|
|
344
|
+
rowsPerKey.set(baseKey, rows);
|
|
345
|
+
|
|
346
|
+
const group: GroupedOrderLine<T> = {
|
|
347
|
+
key: rows === 1 ? baseKey : `${baseKey}-${rows}`,
|
|
332
348
|
name: item.product?.name || '',
|
|
333
349
|
image: getLineImage(item),
|
|
334
350
|
href: item.product?.absolute_url || '',
|
|
@@ -336,10 +352,30 @@ export const groupOrderItems = <T extends OrderLineLike>(
|
|
|
336
352
|
total: lineTotal,
|
|
337
353
|
quantity,
|
|
338
354
|
item
|
|
339
|
-
}
|
|
355
|
+
};
|
|
356
|
+
groups.push(group);
|
|
357
|
+
if (identity !== null) byIdentity.set(identity, group);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
return groups;
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* What tells one unit of a product apart from another in the same order: its
|
|
365
|
+
* status and the cancellation / return request it carries (one object or a
|
|
366
|
+
* one-item array, depending on the pz version).
|
|
367
|
+
*/
|
|
368
|
+
const lineState = (item: OrderLineLike): string => {
|
|
369
|
+
const value = item.active_cancellation_request;
|
|
370
|
+
const request = (Array.isArray(value) ? value[0] : value) as
|
|
371
|
+
| { id?: unknown; easy_return?: { code?: unknown } | null }
|
|
372
|
+
| null
|
|
373
|
+
| undefined;
|
|
374
|
+
const requestKey = request
|
|
375
|
+
? String(request.id ?? request.easy_return?.code ?? 'request')
|
|
376
|
+
: '';
|
|
340
377
|
|
|
341
|
-
|
|
342
|
-
}, []);
|
|
378
|
+
return `${item.status?.value ?? ''}|${requestKey}`;
|
|
343
379
|
};
|
|
344
380
|
|
|
345
381
|
/**
|
|
@@ -29,6 +29,10 @@ export const PLACEHOLDER_SLUGS = {
|
|
|
29
29
|
accountDashboard: 'account-dashboard-body',
|
|
30
30
|
accountOrders: 'account-orders-body',
|
|
31
31
|
accountOrderDetail: 'account-order-detail-body',
|
|
32
|
+
// The easy-return / cancellation screen under one order. It used to resolve
|
|
33
|
+
// to the detail slug and stay code-owned; its own slug lets a brand compose
|
|
34
|
+
// it without the detail composition swallowing it.
|
|
35
|
+
accountOrderCancellation: 'account-order-cancellation-body',
|
|
32
36
|
accountAddresses: 'account-address-body',
|
|
33
37
|
accountProfile: 'account-profile-body',
|
|
34
38
|
accountChangePassword: 'account-change-password-body',
|
package/src/chrome/route-slug.ts
CHANGED
|
@@ -19,9 +19,12 @@ export type RouteSlugRule = [RegExp, string];
|
|
|
19
19
|
* resolves to the order detail body and not to the order list.
|
|
20
20
|
*/
|
|
21
21
|
const DEFAULT_RULES: RouteSlugRule[] = [
|
|
22
|
-
// Account — deepest first. The
|
|
23
|
-
//
|
|
24
|
-
|
|
22
|
+
// Account — deepest first. The cancellation screen under an order has its
|
|
23
|
+
// own body; the detail rule still matches any other deeper order screen.
|
|
24
|
+
[
|
|
25
|
+
/^\/(account|users)\/orders\/[^/]+\/cancellation(\/|$)/,
|
|
26
|
+
PLACEHOLDER_SLUGS.accountOrderCancellation
|
|
27
|
+
],
|
|
25
28
|
[
|
|
26
29
|
/^\/(account|users)\/orders\/[^/]+/,
|
|
27
30
|
PLACEHOLDER_SLUGS.accountOrderDetail
|
|
@@ -130,6 +133,7 @@ const PAGE_MOUNTED_SLUGS = new Set<string>([
|
|
|
130
133
|
PLACEHOLDER_SLUGS.accountDashboard,
|
|
131
134
|
PLACEHOLDER_SLUGS.accountOrders,
|
|
132
135
|
PLACEHOLDER_SLUGS.accountOrderDetail,
|
|
136
|
+
PLACEHOLDER_SLUGS.accountOrderCancellation,
|
|
133
137
|
PLACEHOLDER_SLUGS.accountAddresses,
|
|
134
138
|
PLACEHOLDER_SLUGS.accountProfile,
|
|
135
139
|
PLACEHOLDER_SLUGS.accountChangePassword,
|
package/src/utils/custom-css.ts
CHANGED
|
@@ -77,9 +77,42 @@ const splitTopLevelRules = (css: string): string[] => {
|
|
|
77
77
|
return rules.map(rule => rule.trim()).filter(Boolean);
|
|
78
78
|
};
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Splits a selector list on its top-level commas only. A comma inside
|
|
82
|
+
* `:has()`, `:is()`, `:not()`, `:where()`, an attribute selector or a string
|
|
83
|
+
* belongs to that one selector — splitting there scoped the second half of
|
|
84
|
+
* `:has(a, b)` as if it were a selector of its own, so it never matched.
|
|
85
|
+
*/
|
|
86
|
+
const splitSelectorList = (selectors: string): string[] => {
|
|
87
|
+
const parts: string[] = [];
|
|
88
|
+
let depth = 0;
|
|
89
|
+
let quote: string | null = null;
|
|
90
|
+
let start = 0;
|
|
91
|
+
|
|
92
|
+
for (let i = 0; i < selectors.length; i++) {
|
|
93
|
+
const char = selectors[i];
|
|
94
|
+
|
|
95
|
+
if (quote) {
|
|
96
|
+
if (char === '\\') i += 1;
|
|
97
|
+
else if (char === quote) quote = null;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (char === '"' || char === "'") quote = char;
|
|
102
|
+
else if (char === '(' || char === '[') depth += 1;
|
|
103
|
+
else if (char === ')' || char === ']') depth = Math.max(0, depth - 1);
|
|
104
|
+
else if (char === ',' && depth === 0) {
|
|
105
|
+
parts.push(selectors.slice(start, i));
|
|
106
|
+
start = i + 1;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
parts.push(selectors.slice(start));
|
|
110
|
+
|
|
111
|
+
return parts;
|
|
112
|
+
};
|
|
113
|
+
|
|
80
114
|
const scopeSelectorList = (selectors: string, scope: string): string =>
|
|
81
|
-
selectors
|
|
82
|
-
.split(',')
|
|
115
|
+
splitSelectorList(selectors)
|
|
83
116
|
.map(selector => selector.trim())
|
|
84
117
|
.filter(Boolean)
|
|
85
118
|
.map(selector =>
|