@akinon/pz-theme 2.0.164-rc.0 → 2.0.164
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 -50
- package/package.json +4 -4
- package/src/__tests__/editor-locale-bridge.test.tsx +1 -32
- package/src/__tests__/theme-settings-defaults.test.tsx +395 -0
- package/src/__tests__/theme-text-defaults-code-owned.test.tsx +340 -0
- package/src/blocks/__tests__/breadcrumb-theme-separator.test.tsx +148 -0
- package/src/blocks/__tests__/social-icons-theme-fallback.test.tsx +268 -0
- package/src/blocks/__tests__/text-block.test.tsx +6 -34
- package/src/blocks/accordion-block.tsx +10 -0
- package/src/blocks/hotspot-block.tsx +6 -1
- package/src/blocks/listing-breadcrumb-block.tsx +10 -1
- package/src/blocks/order-detail-block.tsx +95 -113
- package/src/blocks/product-breadcrumb-block.tsx +9 -1
- package/src/blocks/social-glyphs.tsx +31 -0
- package/src/blocks/social-icons-list-block.tsx +72 -8
- package/src/blocks/text-block.tsx +15 -6
- package/src/chrome/__tests__/theme-page-up.test.tsx +522 -0
- package/src/chrome/theme-page-up.tsx +303 -0
- package/src/chrome/use-editor-theme-settings.ts +104 -0
- package/src/components/with-designer-features.tsx +27 -0
- package/src/custom/README.md +219 -100
- package/src/custom/__tests__/use-theme-setting.test.tsx +0 -97
- package/src/custom/collect.ts +9 -4
- package/src/custom/property-types.ts +15 -15
- package/src/custom/use-theme-setting.ts +5 -21
- package/src/editor-locale-bridge.tsx +11 -39
- package/src/index.ts +15 -0
- package/src/overlays/theme-modals.tsx +10 -0
- package/src/sections/__tests__/section-wrapper.test.tsx +76 -0
- package/src/sections/coupon-banner-section.tsx +15 -1
- package/src/sections/section-wrapper.tsx +7 -0
- package/src/sections/tabs-section.tsx +13 -0
- package/src/theme-placeholder-wrapper.tsx +50 -3
- package/src/theme-section.tsx +15 -0
- package/src/utils/__tests__/svg-image-source.test.ts +123 -0
- package/src/utils/__tests__/theme-settings-css.test.ts +801 -0
- package/src/utils/__tests__/theme-settings-readers.test.ts +213 -0
- package/src/utils/__tests__/theme-settings-resolver.test.ts +232 -0
- package/src/utils/index.ts +36 -1
- package/src/utils/svg-image-source.ts +84 -0
- package/src/utils/theme-settings-css.ts +717 -0
- package/src/utils/theme-settings-readers.ts +205 -0
- package/src/blocks/__tests__/order-line-actions.test.tsx +0 -54
- package/src/blocks/order-line-actions.ts +0 -45
- package/src/utils/__tests__/sanitize-rich-text.test.ts +0 -80
- package/src/utils/sanitize-rich-text.ts +0 -215
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
|
|
4
|
+
import type { BlockRendererProps } from '../block-renderer-registry';
|
|
5
|
+
import SocialIconsListBlock from '../social-icons-list-block';
|
|
6
|
+
import { ThemeSettingsProvider } from '../../theme-settings-context';
|
|
7
|
+
import {
|
|
8
|
+
SOCIAL_GLYPHS,
|
|
9
|
+
platformLabel,
|
|
10
|
+
resolveSocialGlyph
|
|
11
|
+
} from '../social-glyphs';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The settings page's "Sosyal Ağlar" (`socialNetworks`) can feed a
|
|
15
|
+
* social-icons-list block — but only one that opts in with
|
|
16
|
+
* `useThemeSocialNetworks` and has no entries of its own. The block saves
|
|
17
|
+
* `icons: []` by default and the old settings page already stored networks,
|
|
18
|
+
* so an automatic fallback would make blocks that render nothing today start
|
|
19
|
+
* drawing icons on live sites.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const props = (
|
|
23
|
+
properties: Record<string, unknown>,
|
|
24
|
+
extra: Partial<BlockRendererProps> = {}
|
|
25
|
+
): BlockRendererProps =>
|
|
26
|
+
({
|
|
27
|
+
block: {
|
|
28
|
+
id: 'b1',
|
|
29
|
+
type: 'social-icons-list',
|
|
30
|
+
label: 'Social Icons',
|
|
31
|
+
value: '',
|
|
32
|
+
properties,
|
|
33
|
+
styles: {},
|
|
34
|
+
order: 0,
|
|
35
|
+
hidden: false
|
|
36
|
+
},
|
|
37
|
+
placeholderId: 'footer',
|
|
38
|
+
sectionId: 's1',
|
|
39
|
+
isDesigner: false,
|
|
40
|
+
currentBreakpoint: 'desktop',
|
|
41
|
+
...extra
|
|
42
|
+
} as unknown as BlockRendererProps);
|
|
43
|
+
|
|
44
|
+
const NETWORKS = {
|
|
45
|
+
instagram: { url: 'https://instagram.com/lulu', icon: '' },
|
|
46
|
+
x: { url: 'https://x.com/lulu', icon: '' },
|
|
47
|
+
facebook: { url: 'https://facebook.com/lulu', icon: '' }
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const renderWith = (
|
|
51
|
+
properties: Record<string, unknown>,
|
|
52
|
+
settings: Record<string, unknown> | null,
|
|
53
|
+
extra: Partial<BlockRendererProps> = {}
|
|
54
|
+
) =>
|
|
55
|
+
render(
|
|
56
|
+
<ThemeSettingsProvider value={settings}>
|
|
57
|
+
<SocialIconsListBlock {...props(properties, extra)} />
|
|
58
|
+
</ThemeSettingsProvider>
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const platforms = (container: HTMLElement) =>
|
|
62
|
+
Array.from(container.querySelectorAll('[data-social-icon]')).map((node) =>
|
|
63
|
+
node.getAttribute('data-social-icon')
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
describe('social-icons-list — theme social networks fallback', () => {
|
|
67
|
+
it('ignores the theme networks without the toggle', () => {
|
|
68
|
+
const { container } = renderWith(
|
|
69
|
+
{ icons: [] },
|
|
70
|
+
{ socialNetworks: NETWORKS }
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
expect(container.querySelector('[data-social-icon]')).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('draws the theme networks, in stored order, when the toggle is on', () => {
|
|
77
|
+
const { container } = renderWith(
|
|
78
|
+
{ icons: [], useThemeSocialNetworks: true },
|
|
79
|
+
{ socialNetworks: NETWORKS }
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(platforms(container)).toEqual(['instagram', 'x', 'facebook']);
|
|
83
|
+
|
|
84
|
+
const links = container.querySelectorAll('[data-social-icon]');
|
|
85
|
+
expect(links[0].getAttribute('href')).toBe('https://instagram.com/lulu');
|
|
86
|
+
expect(links[0].getAttribute('aria-label')).toBe('Instagram');
|
|
87
|
+
expect(links[0].getAttribute('target')).toBe('_blank');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('reads the toggle per breakpoint and as a stored string', () => {
|
|
91
|
+
const settings = { socialNetworks: NETWORKS };
|
|
92
|
+
|
|
93
|
+
const mobileOff = renderWith(
|
|
94
|
+
{ useThemeSocialNetworks: { desktop: true, mobile: false } },
|
|
95
|
+
settings,
|
|
96
|
+
{ currentBreakpoint: 'mobile' }
|
|
97
|
+
);
|
|
98
|
+
expect(mobileOff.container.querySelector('[data-social-icon]')).toBeNull();
|
|
99
|
+
|
|
100
|
+
const stringOn = renderWith({ useThemeSocialNetworks: 'true' }, settings);
|
|
101
|
+
expect(platforms(stringOn.container)).toHaveLength(3);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("keeps the block's own entries when it has any", () => {
|
|
105
|
+
const { container } = renderWith(
|
|
106
|
+
{
|
|
107
|
+
icons: [{ icon: 'youtube', link: 'https://youtube.com/@lulu' }],
|
|
108
|
+
useThemeSocialNetworks: true
|
|
109
|
+
},
|
|
110
|
+
{ socialNetworks: NETWORKS }
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
expect(platforms(container)).toEqual(['youtube']);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('draws the X mark for the settings key `x`', () => {
|
|
117
|
+
const { container } = renderWith(
|
|
118
|
+
{ useThemeSocialNetworks: true },
|
|
119
|
+
{ socialNetworks: { x: { url: 'https://x.com/lulu' } } }
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const link = container.querySelector('[data-social-icon="x"]')!;
|
|
123
|
+
expect(link.getAttribute('aria-label')).toBe('X');
|
|
124
|
+
// The twitter mark is one path; the globe it used to fall back to is
|
|
125
|
+
// three shapes.
|
|
126
|
+
const glyph = link.querySelector('svg')!;
|
|
127
|
+
expect(glyph.children.length).toBe(1);
|
|
128
|
+
expect(glyph.querySelector('path')!.getAttribute('d')).toBe(
|
|
129
|
+
'M4 4l16 16M20 4L4 20'
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('treats legacy `twitter` and `x` as one network', () => {
|
|
134
|
+
const { container } = renderWith(
|
|
135
|
+
{ useThemeSocialNetworks: true },
|
|
136
|
+
{
|
|
137
|
+
socialNetworks: {
|
|
138
|
+
twitter: { url: 'https://twitter.com/lulu' },
|
|
139
|
+
x: { url: 'https://x.com/lulu' }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
expect(platforms(container)).toEqual(['twitter']);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('drops a network whose link is not a safe href', () => {
|
|
148
|
+
const { container } = renderWith(
|
|
149
|
+
{ useThemeSocialNetworks: true },
|
|
150
|
+
{
|
|
151
|
+
socialNetworks: {
|
|
152
|
+
facebook: { url: 'javascript:alert(1)' },
|
|
153
|
+
instagram: { url: '//evil.example/lulu' },
|
|
154
|
+
youtube: { url: '' },
|
|
155
|
+
tiktok: { url: '/\t/evil.example' },
|
|
156
|
+
linkedin: { url: 'https://linkedin.com/company/lulu' }
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
expect(platforms(container)).toEqual(['linkedin']);
|
|
162
|
+
expect(container.innerHTML).not.toContain('javascript:');
|
|
163
|
+
expect(container.innerHTML).not.toContain('evil.example');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('gives a bare domain https and keeps mailto/tel links', () => {
|
|
167
|
+
const { container } = renderWith(
|
|
168
|
+
{ useThemeSocialNetworks: true },
|
|
169
|
+
{
|
|
170
|
+
socialNetworks: {
|
|
171
|
+
instagram: { url: 'www.instagram.com/lulu' },
|
|
172
|
+
email: { url: 'mailto:hello@lulu.example' },
|
|
173
|
+
phone: { url: 'tel:+905550000000' }
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const hrefs = Array.from(
|
|
179
|
+
container.querySelectorAll('[data-social-icon]')
|
|
180
|
+
).map((node) => node.getAttribute('href'));
|
|
181
|
+
expect(hrefs).toEqual([
|
|
182
|
+
'https://www.instagram.com/lulu',
|
|
183
|
+
'mailto:hello@lulu.example',
|
|
184
|
+
'tel:+905550000000'
|
|
185
|
+
]);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('renders an uploaded SVG only as an image, never as markup', () => {
|
|
189
|
+
const markup =
|
|
190
|
+
'<svg xmlns="http://www.w3.org/2000/svg" onload="alert(1)"><script>alert(2)</script><path d="M0 0h24v24H0z"/></svg>';
|
|
191
|
+
const { container } = renderWith(
|
|
192
|
+
{ useThemeSocialNetworks: true },
|
|
193
|
+
{
|
|
194
|
+
socialNetworks: {
|
|
195
|
+
facebook: { url: 'https://facebook.com/lulu', icon: markup }
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const link = container.querySelector('[data-social-icon="facebook"]')!;
|
|
201
|
+
const image = link.querySelector('img')!;
|
|
202
|
+
|
|
203
|
+
expect(image.getAttribute('src')).toMatch(
|
|
204
|
+
/^data:image\/svg\+xml;charset=utf-8,/
|
|
205
|
+
);
|
|
206
|
+
expect(decodeURIComponent(image.getAttribute('src')!.split(',')[1])).toBe(
|
|
207
|
+
markup
|
|
208
|
+
);
|
|
209
|
+
expect(image.getAttribute('alt')).toBe('');
|
|
210
|
+
expect(image.getAttribute('aria-hidden')).toBe('true');
|
|
211
|
+
expect(image.getAttribute('data-part')).toBe('icon');
|
|
212
|
+
expect(link.querySelector('svg')).toBeNull();
|
|
213
|
+
expect(container.querySelector('script')).toBeNull();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('falls back to the platform glyph when the uploaded icon is unusable', () => {
|
|
217
|
+
const { container } = renderWith(
|
|
218
|
+
{ useThemeSocialNetworks: true },
|
|
219
|
+
{
|
|
220
|
+
socialNetworks: {
|
|
221
|
+
facebook: {
|
|
222
|
+
url: 'https://facebook.com/lulu',
|
|
223
|
+
icon: 'javascript:alert(1)'
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
const link = container.querySelector('[data-social-icon="facebook"]')!;
|
|
230
|
+
expect(link.querySelector('img')).toBeNull();
|
|
231
|
+
expect(link.querySelector('svg[data-part="icon"]')).not.toBeNull();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('says why it is empty in the designer', () => {
|
|
235
|
+
const { container } = renderWith(
|
|
236
|
+
{ useThemeSocialNetworks: true },
|
|
237
|
+
{ socialNetworks: {} },
|
|
238
|
+
{ isDesigner: true }
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
expect(container.textContent).toContain(
|
|
242
|
+
'Social icons (no theme social networks)'
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('renders nothing on the storefront when the theme has no usable network', () => {
|
|
247
|
+
const { container } = renderWith({ useThemeSocialNetworks: true }, null);
|
|
248
|
+
|
|
249
|
+
expect(container.innerHTML).toBe('');
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe('social glyph aliases', () => {
|
|
254
|
+
it('resolves `x` to the twitter mark and keeps the table itself unchanged', () => {
|
|
255
|
+
expect(resolveSocialGlyph('x')).toBe(SOCIAL_GLYPHS.twitter);
|
|
256
|
+
expect(resolveSocialGlyph('facebook')).toBe(SOCIAL_GLYPHS.facebook);
|
|
257
|
+
expect(Object.prototype.hasOwnProperty.call(SOCIAL_GLYPHS, 'x')).toBe(
|
|
258
|
+
false
|
|
259
|
+
);
|
|
260
|
+
expect(platformLabel('x')).toBe('X');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('never resolves an inherited property as a glyph', () => {
|
|
264
|
+
expect(resolveSocialGlyph('constructor')).toBeUndefined();
|
|
265
|
+
expect(resolveSocialGlyph('toString')).toBeUndefined();
|
|
266
|
+
expect(resolveSocialGlyph('myspace')).toBeUndefined();
|
|
267
|
+
});
|
|
268
|
+
});
|
|
@@ -2,31 +2,9 @@ import React from 'react';
|
|
|
2
2
|
import { render } from '@testing-library/react';
|
|
3
3
|
import TextBlock from '../text-block';
|
|
4
4
|
|
|
5
|
-
// The
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
const mockLocalization = { locale: 'en-us', defaultLocaleValue: 'en-us' };
|
|
9
|
-
|
|
10
|
-
jest.mock('@akinon/next/hooks', () => ({
|
|
11
|
-
__esModule: true,
|
|
12
|
-
useLocalization: () => ({
|
|
13
|
-
t: (path: string) => path,
|
|
14
|
-
setLocale: () => {},
|
|
15
|
-
setCurrency: () => {},
|
|
16
|
-
locale: mockLocalization.locale,
|
|
17
|
-
currency: 'USD',
|
|
18
|
-
locales: [{ value: 'en-us' }, { value: 'tr-tr' }],
|
|
19
|
-
currencies: ['USD'],
|
|
20
|
-
defaultLocaleValue: mockLocalization.defaultLocaleValue,
|
|
21
|
-
defaultCurrencyCode: 'USD',
|
|
22
|
-
localeUrlStrategy: 'hide-default-locale'
|
|
23
|
-
})
|
|
24
|
-
}));
|
|
25
|
-
|
|
26
|
-
beforeEach(() => {
|
|
27
|
-
mockLocalization.locale = 'en-us';
|
|
28
|
-
mockLocalization.defaultLocaleValue = 'en-us';
|
|
29
|
-
});
|
|
5
|
+
// The mocked useLocalization (jest/__mocks__/akinon-next-hooks.js) returns
|
|
6
|
+
// locale: 'en-us'. process.env.NEXT_PUBLIC_DEFAULT_LOCALE is unset in the
|
|
7
|
+
// jest env, so the component's defaultLocale falls back to 'en'.
|
|
30
8
|
|
|
31
9
|
const renderBlock = (value: unknown, styles?: Record<string, unknown>) => {
|
|
32
10
|
const block: any = { id: 'b1', type: 'text', value, styles };
|
|
@@ -56,16 +34,10 @@ describe('TextBlock value-shape resolution', () => {
|
|
|
56
34
|
);
|
|
57
35
|
});
|
|
58
36
|
|
|
59
|
-
it(
|
|
60
|
-
|
|
61
|
-
// the block used to show: its default was read from an env variable
|
|
62
|
-
// that falls back to English, not from the storefront (ZERO-5296).
|
|
63
|
-
mockLocalization.locale = 'de-de';
|
|
64
|
-
mockLocalization.defaultLocaleValue = 'tr-tr';
|
|
65
|
-
|
|
66
|
-
const { container } = renderBlock({ en: 'EN', 'tr-tr': 'TR' });
|
|
37
|
+
it('falls back to defaultLocale ("en") when active locale is absent', () => {
|
|
38
|
+
const { container } = renderBlock({ en: 'EN', fr: 'FR' });
|
|
67
39
|
expect((container.querySelector('div') as HTMLDivElement).innerHTML).toBe(
|
|
68
|
-
'
|
|
40
|
+
'EN'
|
|
69
41
|
);
|
|
70
42
|
});
|
|
71
43
|
|
|
@@ -2,6 +2,8 @@ import React, { useState } from 'react';
|
|
|
2
2
|
import { BlockRendererProps } from './block-renderer-registry';
|
|
3
3
|
import ThemeBlock from '../theme-block';
|
|
4
4
|
import { partAttrs } from '../utils';
|
|
5
|
+
import { useThemeSettingsContext } from '../theme-settings-context';
|
|
6
|
+
import { themeOwnPartStyleAttributes } from '../utils/theme-settings-css';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Accordion: a heading row that toggles a content panel.
|
|
@@ -30,6 +32,11 @@ const AccordionBlock: React.FC<BlockRendererProps> = ({
|
|
|
30
32
|
currentBreakpoint = 'desktop'
|
|
31
33
|
}) => {
|
|
32
34
|
const childBlocks = block.blocks || [];
|
|
35
|
+
const themeSettings = useThemeSettingsContext();
|
|
36
|
+
// The header, panel and content parts hold the child blocks, so a colour
|
|
37
|
+
// or type authored on one of them has to keep reaching the text inside.
|
|
38
|
+
const ownPartStyle = (part: string) =>
|
|
39
|
+
themeOwnPartStyleAttributes(block.styles, part, themeSettings);
|
|
33
40
|
|
|
34
41
|
const defaultOpen = block.properties?.defaultOpen === true;
|
|
35
42
|
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
@@ -140,6 +147,7 @@ const AccordionBlock: React.FC<BlockRendererProps> = ({
|
|
|
140
147
|
<>
|
|
141
148
|
<div
|
|
142
149
|
{...partAttrs('header', { open: isOpen })}
|
|
150
|
+
{...ownPartStyle('header')}
|
|
143
151
|
style={{ cursor: 'pointer' }}
|
|
144
152
|
onClick={toggleAccordion}
|
|
145
153
|
>
|
|
@@ -148,6 +156,7 @@ const AccordionBlock: React.FC<BlockRendererProps> = ({
|
|
|
148
156
|
|
|
149
157
|
<div
|
|
150
158
|
{...partAttrs('panel', { open: isOpen })}
|
|
159
|
+
{...ownPartStyle('panel')}
|
|
151
160
|
style={{
|
|
152
161
|
display: 'grid',
|
|
153
162
|
gridTemplateRows: isOpen ? '1fr' : '0fr',
|
|
@@ -156,6 +165,7 @@ const AccordionBlock: React.FC<BlockRendererProps> = ({
|
|
|
156
165
|
>
|
|
157
166
|
<div
|
|
158
167
|
{...partAttrs('content', { open: isOpen })}
|
|
168
|
+
{...ownPartStyle('content')}
|
|
159
169
|
style={{ overflow: 'hidden' }}
|
|
160
170
|
>
|
|
161
171
|
{renderContent()}
|
|
@@ -4,6 +4,7 @@ import React, { useState } from 'react';
|
|
|
4
4
|
import { BlockRendererProps } from './block-renderer-registry';
|
|
5
5
|
import { getResponsiveValue, getCSSStyles, resolveForceOpen } from '../utils';
|
|
6
6
|
import { useThemeSettingsContext } from '../theme-settings-context';
|
|
7
|
+
import { themeOwnStyleAttributes } from '../utils/theme-settings-css';
|
|
7
8
|
import { useLocalization } from '@akinon/next/hooks';
|
|
8
9
|
import { useLocalizeHref } from '../hooks/use-localize-href';
|
|
9
10
|
import ThemeBlock from '../theme-block';
|
|
@@ -109,7 +110,11 @@ const HotspotBlock = ({
|
|
|
109
110
|
</button>
|
|
110
111
|
|
|
111
112
|
{(isOpen || forcedOpen) && (
|
|
112
|
-
<div
|
|
113
|
+
<div
|
|
114
|
+
className="absolute left-1/2 bottom-full mb-2 -translate-x-1/2 w-64 bg-white p-4 rounded-lg shadow-xl z-20 text-left text-sm pointer-events-auto"
|
|
115
|
+
// `text-sm` sets the size of the blocks inside the tooltip.
|
|
116
|
+
{...themeOwnStyleAttributes(null, themeSettings, { type: true })}
|
|
117
|
+
>
|
|
113
118
|
<div className="flex justify-between items-start mb-2">
|
|
114
119
|
{title && <h3 className="font-bold text-gray-900">{title}</h3>}
|
|
115
120
|
<button
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import { ThemeLink as NextLink } from '../components/theme-link';
|
|
5
5
|
import { getResponsiveValue, partAttrs } from '../utils';
|
|
6
|
+
import { readThemeBreadcrumbSeparator } from '../utils/theme-settings-readers';
|
|
7
|
+
import { useThemeSettingsContext } from '../theme-settings-context';
|
|
6
8
|
import { BlockRendererProps } from './block-renderer-registry';
|
|
7
9
|
import {
|
|
8
10
|
listingPlaceholderStyle,
|
|
@@ -33,9 +35,16 @@ const ListingBreadcrumbBlock = ({
|
|
|
33
35
|
isDesigner = false
|
|
34
36
|
}: BlockRendererProps) => {
|
|
35
37
|
const listing = useListingContext();
|
|
38
|
+
const themeSettings = useThemeSettingsContext();
|
|
36
39
|
|
|
40
|
+
// An unset separator follows the theme's breadcrumb setting; a
|
|
41
|
+
// stored one (including '/') keeps winning, so saved blocks render as before.
|
|
37
42
|
const separator = String(
|
|
38
|
-
getResponsiveValue(
|
|
43
|
+
getResponsiveValue(
|
|
44
|
+
block.properties?.separator,
|
|
45
|
+
currentBreakpoint,
|
|
46
|
+
readThemeBreadcrumbSeparator(themeSettings) ?? '/'
|
|
47
|
+
)
|
|
39
48
|
);
|
|
40
49
|
const showHome = readToggle(
|
|
41
50
|
block.properties?.showHome,
|
|
@@ -9,7 +9,6 @@ import { getResponsiveValue, partAttrs } from '../utils';
|
|
|
9
9
|
import { formatDate, formatMoney } from './commerce-format';
|
|
10
10
|
import { readMoneyStyle } from './money-style';
|
|
11
11
|
import { hasItemTemplate, ItemTemplateList } from './item-template';
|
|
12
|
-
import { renderOrderLineActions } from './order-line-actions';
|
|
13
12
|
import { BlockRendererProps } from './block-renderer-registry';
|
|
14
13
|
import {
|
|
15
14
|
DesignerPlaceholder,
|
|
@@ -25,7 +24,7 @@ import {
|
|
|
25
24
|
useOrderActionRules
|
|
26
25
|
} from '../utils/order-action-rules';
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
interface OrderItemLike {
|
|
29
28
|
id?: number;
|
|
30
29
|
price?: string;
|
|
31
30
|
quantity?: string | number;
|
|
@@ -44,7 +43,7 @@ type OrderItemLike = {
|
|
|
44
43
|
absolute_url?: string;
|
|
45
44
|
attributes?: Record<string, unknown>;
|
|
46
45
|
} | null;
|
|
47
|
-
}
|
|
46
|
+
}
|
|
48
47
|
|
|
49
48
|
interface AddressLike {
|
|
50
49
|
first_name?: string;
|
|
@@ -409,7 +408,7 @@ const formatAddress = (address: AddressLike | null | undefined): string[] => {
|
|
|
409
408
|
* currencyText, currencyPosition.
|
|
410
409
|
* Parts: detail, header, number, date, header-total, status,
|
|
411
410
|
* invoice-link, cancel-link, return-link, list, item, image, name, quantity,
|
|
412
|
-
* color, size, item-status, item-tracking, item-return-code,
|
|
411
|
+
* color, size, item-status, item-tracking, item-return-code,
|
|
413
412
|
* item-return-method, item-return-carrier, item-pickup-date, price, shipment,
|
|
414
413
|
* delivery, tracking, addresses, address, address-label, totals, total-row,
|
|
415
414
|
* total-label, total-value, grand-total, payment-info, payment-info-label.
|
|
@@ -956,21 +955,6 @@ const OrderDetailBlock = ({
|
|
|
956
955
|
showPickupDate && returnDetails.pickupDate
|
|
957
956
|
? formatPickupDate(returnDetails.pickupDate, locale)
|
|
958
957
|
: '';
|
|
959
|
-
const lineActions = renderOrderLineActions({
|
|
960
|
-
order,
|
|
961
|
-
line,
|
|
962
|
-
isDesigner
|
|
963
|
-
});
|
|
964
|
-
const showLineMeta =
|
|
965
|
-
showShipmentStatus &&
|
|
966
|
-
Boolean(
|
|
967
|
-
item.status?.label ||
|
|
968
|
-
trackingValue ||
|
|
969
|
-
returnCodeValue ||
|
|
970
|
-
returnMethodValue ||
|
|
971
|
-
returnCarrierValue ||
|
|
972
|
-
pickupDateValue
|
|
973
|
-
);
|
|
974
958
|
|
|
975
959
|
return (
|
|
976
960
|
<li
|
|
@@ -1086,100 +1070,98 @@ const OrderDetailBlock = ({
|
|
|
1086
1070
|
</div>
|
|
1087
1071
|
</div>
|
|
1088
1072
|
|
|
1089
|
-
{
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
</div>
|
|
1182
|
-
)}
|
|
1073
|
+
{showShipmentStatus &&
|
|
1074
|
+
(item.status?.label ||
|
|
1075
|
+
trackingValue ||
|
|
1076
|
+
returnCodeValue ||
|
|
1077
|
+
returnMethodValue ||
|
|
1078
|
+
returnCarrierValue ||
|
|
1079
|
+
pickupDateValue) && (
|
|
1080
|
+
<div
|
|
1081
|
+
style={{
|
|
1082
|
+
display: 'flex',
|
|
1083
|
+
flexDirection: 'column',
|
|
1084
|
+
alignItems: isMobile ? 'flex-start' : 'flex-end',
|
|
1085
|
+
textAlign: isMobile ? 'left' : 'right',
|
|
1086
|
+
gap: '4px',
|
|
1087
|
+
flex: isMobile ? '1 1 100%' : '0 0 auto'
|
|
1088
|
+
}}
|
|
1089
|
+
>
|
|
1090
|
+
{item.status?.label && (
|
|
1091
|
+
<span
|
|
1092
|
+
data-order-detail-item-status=""
|
|
1093
|
+
data-order-status-value={item.status.value}
|
|
1094
|
+
{...partAttrs('item-status')}
|
|
1095
|
+
style={{ display: 'inline-block' }}
|
|
1096
|
+
>
|
|
1097
|
+
{item.status.label}
|
|
1098
|
+
</span>
|
|
1099
|
+
)}
|
|
1100
|
+
{/* Label and value in their own spans, so a
|
|
1101
|
+
composition can colour them apart. */}
|
|
1102
|
+
{returnMethodValue && (
|
|
1103
|
+
<span
|
|
1104
|
+
data-order-detail-item-return-method=""
|
|
1105
|
+
{...partAttrs('item-return-method')}
|
|
1106
|
+
style={{ opacity: 0.8 }}
|
|
1107
|
+
>
|
|
1108
|
+
<span data-label="">{returnMethodLabel}</span>{' '}
|
|
1109
|
+
<span data-value="">{returnMethodValue}</span>
|
|
1110
|
+
</span>
|
|
1111
|
+
)}
|
|
1112
|
+
{returnCarrierValue && (
|
|
1113
|
+
<span
|
|
1114
|
+
data-order-detail-item-return-carrier=""
|
|
1115
|
+
{...partAttrs('item-return-carrier')}
|
|
1116
|
+
style={{ opacity: 0.8 }}
|
|
1117
|
+
>
|
|
1118
|
+
<span data-label="">{returnCarrierLabel}</span>{' '}
|
|
1119
|
+
<span data-value="">{returnCarrierValue}</span>
|
|
1120
|
+
</span>
|
|
1121
|
+
)}
|
|
1122
|
+
{pickupDateValue && (
|
|
1123
|
+
<span
|
|
1124
|
+
data-order-detail-item-pickup-date=""
|
|
1125
|
+
{...partAttrs('item-pickup-date')}
|
|
1126
|
+
style={{ opacity: 0.8 }}
|
|
1127
|
+
>
|
|
1128
|
+
<span data-label="">{pickupDateLabel}</span>{' '}
|
|
1129
|
+
<span data-value="">{pickupDateValue}</span>
|
|
1130
|
+
</span>
|
|
1131
|
+
)}
|
|
1132
|
+
{trackingValue && (
|
|
1133
|
+
<span
|
|
1134
|
+
data-order-detail-item-tracking=""
|
|
1135
|
+
{...partAttrs('item-tracking')}
|
|
1136
|
+
style={{ opacity: 0.8 }}
|
|
1137
|
+
>
|
|
1138
|
+
{lineTrackingLabel}{' '}
|
|
1139
|
+
{item.tracking_url ? (
|
|
1140
|
+
<a
|
|
1141
|
+
href={isDesigner ? '#' : item.tracking_url}
|
|
1142
|
+
onClick={guard}
|
|
1143
|
+
target={isDesigner ? undefined : '_blank'}
|
|
1144
|
+
rel="noreferrer"
|
|
1145
|
+
style={{ color: 'inherit' }}
|
|
1146
|
+
>
|
|
1147
|
+
{item.tracking_number || item.tracking_url}
|
|
1148
|
+
</a>
|
|
1149
|
+
) : (
|
|
1150
|
+
trackingValue
|
|
1151
|
+
)}
|
|
1152
|
+
</span>
|
|
1153
|
+
)}
|
|
1154
|
+
{returnCodeValue && (
|
|
1155
|
+
<span
|
|
1156
|
+
data-order-detail-item-return-code=""
|
|
1157
|
+
{...partAttrs('item-return-code')}
|
|
1158
|
+
style={{ opacity: 0.8 }}
|
|
1159
|
+
>
|
|
1160
|
+
{returnCodeLabel} {returnCodeValue}
|
|
1161
|
+
</span>
|
|
1162
|
+
)}
|
|
1163
|
+
</div>
|
|
1164
|
+
)}
|
|
1183
1165
|
</div>
|
|
1184
1166
|
</li>
|
|
1185
1167
|
);
|