@akinon/pz-theme 2.0.143-beta.0 → 2.0.143
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 +5 -4
- package/package.json +4 -4
- package/src/__tests__/designer-live-rows.test.tsx +257 -0
- package/src/blocks/__tests__/favorite-button-icons.test.tsx +102 -0
- package/src/blocks/__tests__/listing-price-note.test.ts +47 -0
- package/src/blocks/favorite-button-block.tsx +36 -15
- package/src/blocks/group-block.tsx +52 -1
- package/src/blocks/image-block.tsx +26 -2
- package/src/blocks/listing-context.ts +5 -2
- package/src/blocks/listing-row.ts +64 -4
- package/src/blocks/product-grid-block.tsx +83 -3
- package/src/components/card-list-slider.tsx +374 -0
- package/src/components/card-list-views.tsx +278 -0
- package/src/components/card-zoom.tsx +53 -0
- package/src/components/with-designer-features.tsx +18 -0
- package/src/theme-block.tsx +11 -0
- package/src/theme-placeholder-wrapper.tsx +52 -7
- package/src/utils/__tests__/card-media.test.tsx +789 -0
- package/src/utils/__tests__/card-views.test.tsx +332 -0
- package/src/utils/__tests__/swatch-colors.test.ts +145 -0
- package/src/utils/card-media.ts +331 -0
- package/src/utils/iterator-utils.ts +10 -3
- package/src/utils/live-rows.ts +169 -34
- package/src/utils/property-switches.ts +248 -2
- package/src/utils/swatch-colors.ts +81 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
import { MOBILE_MEDIA_QUERY_COMPACT } from '../utils/breakpoints';
|
|
6
|
+
import {
|
|
7
|
+
CARD_VIEW_COLUMNS,
|
|
8
|
+
CARD_VIEWS,
|
|
9
|
+
type CardView,
|
|
10
|
+
cardViewFromHash,
|
|
11
|
+
type CardViewSettings,
|
|
12
|
+
type CardViewToggles,
|
|
13
|
+
HIDE_IN_CARD_VIEW_ATTRIBUTE
|
|
14
|
+
} from '../utils/card-media';
|
|
15
|
+
import { neutralizeStyleTagBreakout } from '../utils/css-safety';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The GANT listing's grid views for a card list that is not sliding
|
|
19
|
+
* (ZERO-5173): the cards laid out as the view says — catalogue three across
|
|
20
|
+
* (one below 1024px), classic the list's own grid, archive eight across (four
|
|
21
|
+
* below) with the cards' captions left out — in the view the designer picked,
|
|
22
|
+
* and, when the shopper is offered them, the listing toolbar's three toggles.
|
|
23
|
+
*
|
|
24
|
+
* A SHOPPER'S SWITCH IS THE PAGE'S, IN THE HASH — the listing's own contract
|
|
25
|
+
* (`#catalogue` / `#classic` / `#archive`): read on mount and on `hashchange`,
|
|
26
|
+
* rewritten in place on a switch, so a shared link opens the view it was
|
|
27
|
+
* shared in and every list offering the toggles on a page shows the same one.
|
|
28
|
+
* No hash is no choice: each list shows its designer's view, which is also
|
|
29
|
+
* what the server — which never sees the hash — renders.
|
|
30
|
+
*
|
|
31
|
+
* The layout is CSS only: the cards are the list's own elements and keep
|
|
32
|
+
* rendering exactly as they are; a view only overrides the list's column
|
|
33
|
+
* template and hides what the archive leaves out.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** The shopper's choice: undefined before the hash is read, null for none. */
|
|
37
|
+
let current: CardView | null | undefined;
|
|
38
|
+
const listeners = new Set<() => void>();
|
|
39
|
+
|
|
40
|
+
const read = (): CardView | null => {
|
|
41
|
+
if (current === undefined) {
|
|
42
|
+
current = cardViewFromHash(
|
|
43
|
+
typeof window === 'undefined' ? '' : window.location.hash
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return current;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const notify = () => listeners.forEach((listener) => listener());
|
|
50
|
+
|
|
51
|
+
const onHashChange = () => {
|
|
52
|
+
current = cardViewFromHash(window.location.hash);
|
|
53
|
+
notify();
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const subscribe = (listener: () => void) => {
|
|
57
|
+
if (listeners.size === 0) {
|
|
58
|
+
window.addEventListener('hashchange', onHashChange);
|
|
59
|
+
}
|
|
60
|
+
listeners.add(listener);
|
|
61
|
+
|
|
62
|
+
return () => {
|
|
63
|
+
listeners.delete(listener);
|
|
64
|
+
if (listeners.size === 0) {
|
|
65
|
+
window.removeEventListener('hashchange', onHashChange);
|
|
66
|
+
// The next page reads its own hash.
|
|
67
|
+
current = undefined;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Switch the page's view. The hash is rewritten in place — no navigation, no
|
|
74
|
+
* scroll — except on the canvas, where the preview's URL is the editor's.
|
|
75
|
+
*/
|
|
76
|
+
export const setCardView = (
|
|
77
|
+
view: CardView,
|
|
78
|
+
{ writeHash = true }: { writeHash?: boolean } = {}
|
|
79
|
+
): void => {
|
|
80
|
+
current = view;
|
|
81
|
+
if (writeHash && typeof window !== 'undefined') {
|
|
82
|
+
const url = new URL(window.location.href);
|
|
83
|
+
url.hash = `#${view}`;
|
|
84
|
+
window.history.replaceState(window.history.state, '', url.toString());
|
|
85
|
+
}
|
|
86
|
+
notify();
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** The shopper's choice on this page — none on the server and while hydrating. */
|
|
90
|
+
export const useCardViewChoice = (): CardView | null =>
|
|
91
|
+
React.useSyncExternalStore(subscribe, read, () => null);
|
|
92
|
+
|
|
93
|
+
/** Test-only: forget the view between cases. */
|
|
94
|
+
export const __resetCardViewForTests = (): void => {
|
|
95
|
+
current = undefined;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const SAFE_ID = /^[A-Za-z0-9_-]+$/;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The view's layout for the list whose wrapper is `[data-block-id=scopeId]`.
|
|
102
|
+
* The attribute twice outranks the list's own column template without
|
|
103
|
+
* `!important`; classic writes nothing, so it is the list as authored.
|
|
104
|
+
*/
|
|
105
|
+
export const buildCardViewCss = (
|
|
106
|
+
scopeId: string,
|
|
107
|
+
view: CardView,
|
|
108
|
+
onPhoneCanvas: boolean,
|
|
109
|
+
toggles?: CardViewToggles
|
|
110
|
+
): string => {
|
|
111
|
+
if (!SAFE_ID.test(scopeId)) return '';
|
|
112
|
+
|
|
113
|
+
const list = `[data-block-id="${scopeId}"][data-block-id="${scopeId}"]`;
|
|
114
|
+
let css = '';
|
|
115
|
+
|
|
116
|
+
if (toggles) {
|
|
117
|
+
// The toolbar's toggles: buttons 8px wider than their glyph from 1024px
|
|
118
|
+
// up, the gap between them authored; 44px squares flush together below.
|
|
119
|
+
const switcher = `[data-card-view-switcher="${scopeId}"]`;
|
|
120
|
+
const button = toggles.size + 8;
|
|
121
|
+
const touch = Math.max(44, button);
|
|
122
|
+
const phoneSwitcher = `${switcher}{gap:0}${switcher}>button{width:${touch}px;height:${touch}px}`;
|
|
123
|
+
css +=
|
|
124
|
+
`${switcher}{gap:${toggles.gap}px}${switcher}>button{width:${button}px;height:${button}px}` +
|
|
125
|
+
(onPhoneCanvas
|
|
126
|
+
? phoneSwitcher
|
|
127
|
+
: `${MOBILE_MEDIA_QUERY_COMPACT}{${phoneSwitcher}}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (view !== 'classic') {
|
|
131
|
+
const { desktop, mobile } = CARD_VIEW_COLUMNS[view];
|
|
132
|
+
const columns = (count: number) =>
|
|
133
|
+
`${list}{grid-template-columns:repeat(${count},minmax(0,1fr))}`;
|
|
134
|
+
css += onPhoneCanvas
|
|
135
|
+
? columns(mobile)
|
|
136
|
+
: `${columns(desktop)}${MOBILE_MEDIA_QUERY_COMPACT}{${columns(mobile)}}`;
|
|
137
|
+
css += `${list} [${HIDE_IN_CARD_VIEW_ATTRIBUTE}~="${view}"]{display:none}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return neutralizeStyleTagBreakout(css);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/** The listing toolbar's 24-unit glyphs: one cell, four, six. */
|
|
144
|
+
const VIEW_GLYPHS: Record<CardView, string> = {
|
|
145
|
+
catalogue: 'M18.5 17.5h-13v-11h13zm-12-1h11v-9h-11z',
|
|
146
|
+
classic:
|
|
147
|
+
'M18.5 17.5h-13v-11h13zm-12-1h5v-4h-5zm6 0h5v-4h-5zm-6-5h5v-4h-5zm6 0h5v-4h-5z',
|
|
148
|
+
archive:
|
|
149
|
+
'M18.5 17.5h-13v-11h13zm-12-1h3v-4h-3zm4 0h3v-4h-3zm4 0h3v-4h-3zm-8-5h3v-4h-3zm4 0h3v-4h-3zm4 0h3v-4h-3z'
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const VIEW_LABELS: Record<CardView, string> = {
|
|
153
|
+
catalogue: 'Catalogue view',
|
|
154
|
+
classic: 'Classic view',
|
|
155
|
+
archive: 'Archive view'
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export interface CardListViewsProps {
|
|
159
|
+
/** The list's wrapper `data-block-id` — what its own grid styles target. */
|
|
160
|
+
scopeId: string;
|
|
161
|
+
settings: CardViewSettings;
|
|
162
|
+
isDesigner?: boolean;
|
|
163
|
+
currentBreakpoint?: string;
|
|
164
|
+
/**
|
|
165
|
+
* The canvas's selection for a click on the toggles: the list whose settings
|
|
166
|
+
* hold them — they are no block of their own.
|
|
167
|
+
*/
|
|
168
|
+
onSelect?: () => void;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const JUSTIFY: Record<CardViewToggles['align'], string> = {
|
|
172
|
+
left: 'flex-start',
|
|
173
|
+
center: 'center',
|
|
174
|
+
right: 'flex-end'
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The view's stylesheet and, when offered, the toggles — drawn as the first
|
|
179
|
+
* item of the list's grid and spanning its columns. Its own component, so only
|
|
180
|
+
* it re-renders when the view changes; the cards never do.
|
|
181
|
+
*/
|
|
182
|
+
export const CardListViews = ({
|
|
183
|
+
scopeId,
|
|
184
|
+
settings,
|
|
185
|
+
isDesigner = false,
|
|
186
|
+
currentBreakpoint = 'desktop',
|
|
187
|
+
onSelect
|
|
188
|
+
}: CardListViewsProps) => {
|
|
189
|
+
const choice = useCardViewChoice();
|
|
190
|
+
const { toggles } = settings;
|
|
191
|
+
// Only a list that offers the toggles answers to the shopper's switch; one
|
|
192
|
+
// that does not is the view its designer picked, whatever the page says.
|
|
193
|
+
const view = toggles ? choice ?? settings.view : settings.view;
|
|
194
|
+
const css = buildCardViewCss(
|
|
195
|
+
scopeId,
|
|
196
|
+
view,
|
|
197
|
+
isDesigner && currentBreakpoint !== 'desktop',
|
|
198
|
+
toggles
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<>
|
|
203
|
+
<style dangerouslySetInnerHTML={{ __html: css }} />
|
|
204
|
+
{toggles ? (
|
|
205
|
+
<div
|
|
206
|
+
data-card-view-switcher={scopeId}
|
|
207
|
+
role="group"
|
|
208
|
+
aria-label="Grid views"
|
|
209
|
+
style={{
|
|
210
|
+
gridColumn: '1 / -1',
|
|
211
|
+
display: 'flex',
|
|
212
|
+
alignItems: 'center',
|
|
213
|
+
justifyContent: JUSTIFY[toggles.align],
|
|
214
|
+
padding: toggles.padding,
|
|
215
|
+
color: toggles.color
|
|
216
|
+
}}
|
|
217
|
+
>
|
|
218
|
+
{CARD_VIEWS.map((option) => {
|
|
219
|
+
const active = option === view;
|
|
220
|
+
return (
|
|
221
|
+
<button
|
|
222
|
+
key={option}
|
|
223
|
+
type="button"
|
|
224
|
+
data-card-view-option={option}
|
|
225
|
+
aria-pressed={active}
|
|
226
|
+
aria-label={VIEW_LABELS[option]}
|
|
227
|
+
onClick={(event) => {
|
|
228
|
+
// Inside a list the canvas selects on click: the toggle is
|
|
229
|
+
// the whole of what this click means — and on the canvas it
|
|
230
|
+
// also opens the list's settings, where the toggles are set.
|
|
231
|
+
event.preventDefault();
|
|
232
|
+
event.stopPropagation();
|
|
233
|
+
setCardView(option, { writeHash: !isDesigner });
|
|
234
|
+
if (isDesigner) onSelect?.();
|
|
235
|
+
}}
|
|
236
|
+
style={{
|
|
237
|
+
display: 'flex',
|
|
238
|
+
alignItems: 'center',
|
|
239
|
+
justifyContent: 'center',
|
|
240
|
+
padding: 0,
|
|
241
|
+
border: 0,
|
|
242
|
+
background: 'transparent',
|
|
243
|
+
color: 'inherit',
|
|
244
|
+
cursor: 'pointer'
|
|
245
|
+
}}
|
|
246
|
+
>
|
|
247
|
+
<span
|
|
248
|
+
style={{
|
|
249
|
+
display: 'flex',
|
|
250
|
+
alignItems: 'flex-start',
|
|
251
|
+
width: `${toggles.size}px`,
|
|
252
|
+
height: `${toggles.size + 1}px`,
|
|
253
|
+
borderBottom: `1px solid ${
|
|
254
|
+
active ? 'currentColor' : 'transparent'
|
|
255
|
+
}`
|
|
256
|
+
}}
|
|
257
|
+
>
|
|
258
|
+
<svg
|
|
259
|
+
viewBox="0 0 24 24"
|
|
260
|
+
width={toggles.size}
|
|
261
|
+
height={toggles.size}
|
|
262
|
+
fill="none"
|
|
263
|
+
aria-hidden="true"
|
|
264
|
+
style={{ display: 'block' }}
|
|
265
|
+
>
|
|
266
|
+
<path fill="currentColor" d={VIEW_GLYPHS[option]} />
|
|
267
|
+
</svg>
|
|
268
|
+
</span>
|
|
269
|
+
</button>
|
|
270
|
+
);
|
|
271
|
+
})}
|
|
272
|
+
</div>
|
|
273
|
+
) : null}
|
|
274
|
+
</>
|
|
275
|
+
);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
export default CardListViews;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
import { CARD_ZOOM_CSS, CARD_ZOOM_STYLE_ID } from '../utils/card-media';
|
|
6
|
+
|
|
7
|
+
const ensureCardZoomStyles = () => {
|
|
8
|
+
if (typeof document === 'undefined') return;
|
|
9
|
+
if (document.getElementById(CARD_ZOOM_STYLE_ID)) return;
|
|
10
|
+
|
|
11
|
+
const style = document.createElement('style');
|
|
12
|
+
style.id = CARD_ZOOM_STYLE_ID;
|
|
13
|
+
style.textContent = CARD_ZOOM_CSS;
|
|
14
|
+
document.head.appendChild(style);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
interface CardZoomProps {
|
|
18
|
+
/** The box the picture was drawn in, so the card around it does not move. */
|
|
19
|
+
style?: React.CSSProperties;
|
|
20
|
+
/** The host's own marks for its picture, written on the frame. */
|
|
21
|
+
attributes?: Record<string, string>;
|
|
22
|
+
children: React.ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The product card's picture, zooming in on hover (ZERO-5173).
|
|
27
|
+
*
|
|
28
|
+
* A frame that clips the picture to its own box, so the enlarged picture
|
|
29
|
+
* never spills over the caption or a neighbouring card. The hover rule is a
|
|
30
|
+
* stylesheet injected once per document: it only does anything where a
|
|
31
|
+
* pointer hovers, so the server render — and a phone — draw the picture
|
|
32
|
+
* exactly as before.
|
|
33
|
+
*/
|
|
34
|
+
export const CardZoom = ({ style, attributes, children }: CardZoomProps) => {
|
|
35
|
+
React.useInsertionEffect(ensureCardZoomStyles, []);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
data-card-zoom=""
|
|
40
|
+
{...attributes}
|
|
41
|
+
style={{
|
|
42
|
+
position: 'relative',
|
|
43
|
+
display: 'block',
|
|
44
|
+
overflow: 'hidden',
|
|
45
|
+
...style
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
{children}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default CardZoom;
|
|
@@ -15,6 +15,10 @@ import {
|
|
|
15
15
|
HOVER_SCOPE_ATTRIBUTE,
|
|
16
16
|
REVEAL_ON_HOVER_ATTRIBUTE
|
|
17
17
|
} from '../utils/property-switches';
|
|
18
|
+
import {
|
|
19
|
+
HIDE_IN_CARD_VIEW_ATTRIBUTE,
|
|
20
|
+
HIDE_IN_CARD_VIEW_PROPERTY
|
|
21
|
+
} from '../utils/card-media';
|
|
18
22
|
|
|
19
23
|
/** Elements that already act on Enter/Space themselves. */
|
|
20
24
|
export const NATIVE_KEYBOARD_CONTROLS =
|
|
@@ -159,6 +163,17 @@ export function WithDesignerFeatures({
|
|
|
159
163
|
|
|
160
164
|
// In designer mode, keep hidden blocks mounted (prevents remount-driven side effects)
|
|
161
165
|
// but visually hide them.
|
|
166
|
+
// A part of a card that one of the listing's grid views leaves out — the
|
|
167
|
+
// Product Card's caption in the picture-only archive (utils/card-media).
|
|
168
|
+
// On the wrapper, which carries the part's box, so nothing of it remains.
|
|
169
|
+
const hideInCardViewRaw = getResponsiveValue(
|
|
170
|
+
block.properties?.[HIDE_IN_CARD_VIEW_PROPERTY],
|
|
171
|
+
currentBreakpoint,
|
|
172
|
+
''
|
|
173
|
+
);
|
|
174
|
+
const hideInCardView =
|
|
175
|
+
typeof hideInCardViewRaw === 'string' ? hideInCardViewRaw.trim() : '';
|
|
176
|
+
|
|
162
177
|
const mergedStyle: React.CSSProperties = {
|
|
163
178
|
...(style || {}),
|
|
164
179
|
...(isDesigner && block.hidden ? { display: 'none' } : null),
|
|
@@ -173,6 +188,9 @@ export function WithDesignerFeatures({
|
|
|
173
188
|
data-block-id={block.styleSourceId || block.id}
|
|
174
189
|
{...(block.hoverScope ? { [HOVER_SCOPE_ATTRIBUTE]: '' } : null)}
|
|
175
190
|
{...(block.revealOnHover ? { [REVEAL_ON_HOVER_ATTRIBUTE]: '' } : null)}
|
|
191
|
+
{...(hideInCardView
|
|
192
|
+
? { [HIDE_IN_CARD_VIEW_ATTRIBUTE]: hideInCardView }
|
|
193
|
+
: null)}
|
|
176
194
|
className={clsx(
|
|
177
195
|
'relative',
|
|
178
196
|
className,
|
package/src/theme-block.tsx
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
isPublishWindowVisible
|
|
15
15
|
} from './utils';
|
|
16
16
|
import { devWarn } from './utils/dev-warn';
|
|
17
|
+
import type { CardsSliderSettings, CardViewSettings } from './utils/card-media';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Narrows what a designer typed into something usable as a document-wide id.
|
|
@@ -70,6 +71,16 @@ export interface Block {
|
|
|
70
71
|
*/
|
|
71
72
|
revealOnHover?: boolean;
|
|
72
73
|
hoverScope?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Written by the same pass, never authored (utils/card-media.ts): the card's
|
|
76
|
+
* picture zooms in on hover, and this repeat lays the cards it holds out as
|
|
77
|
+
* a slider. Absent on every block no setting reaches, which is every block
|
|
78
|
+
* of an unconfigured list.
|
|
79
|
+
*/
|
|
80
|
+
cardZoom?: boolean;
|
|
81
|
+
cardsSlider?: CardsSliderSettings;
|
|
82
|
+
/** The listing's grid view this repeat is drawn in, and its toggles when offered. */
|
|
83
|
+
cardsViews?: CardViewSettings;
|
|
73
84
|
/**
|
|
74
85
|
* Properties this clone resolved for itself from its row. One generated rule
|
|
75
86
|
* serves every clone, so these are the values that have to be inlined on the
|
|
@@ -36,7 +36,9 @@ import {
|
|
|
36
36
|
import { isPublishWindowVisible } from './utils/publish-window';
|
|
37
37
|
import {
|
|
38
38
|
collectionBindingKey,
|
|
39
|
+
collectionPreviewWidget,
|
|
39
40
|
fetchDesignerLiveRows,
|
|
41
|
+
fetchDesignerPreviewRows,
|
|
40
42
|
hangDesignerDataSource,
|
|
41
43
|
liveRowsEntryKey,
|
|
42
44
|
LiveRowsStore,
|
|
@@ -799,7 +801,8 @@ export default function ThemePlaceholderWrapper({
|
|
|
799
801
|
dataSource: hangDesignerDataSource(
|
|
800
802
|
section,
|
|
801
803
|
dataSource || section.dataSource,
|
|
802
|
-
liveRowsRef.current
|
|
804
|
+
liveRowsRef.current,
|
|
805
|
+
{ previous: section.dataSource }
|
|
803
806
|
)
|
|
804
807
|
};
|
|
805
808
|
}
|
|
@@ -814,11 +817,45 @@ export default function ThemePlaceholderWrapper({
|
|
|
814
817
|
// preview (whose blob path writes no live rows), or the server could not
|
|
815
818
|
// resolve them — asks the storefront for them once. Until they arrive the
|
|
816
819
|
// picked snapshot stays on the canvas: it is the same collection.
|
|
820
|
+
//
|
|
821
|
+
// An unsaved pick asks for its binding's preview widget instead, once per
|
|
822
|
+
// binding, when the editor prepared one: those are the rows the section
|
|
823
|
+
// will show once published, where the snapshot is Omnitron's catalogue.
|
|
817
824
|
useEffect(() => {
|
|
818
825
|
if (!isDesigner) return;
|
|
819
826
|
|
|
820
827
|
sections.forEach((section) => {
|
|
821
|
-
if (
|
|
828
|
+
if (!section.id) return;
|
|
829
|
+
|
|
830
|
+
if (section.dataSourceSaved !== true) {
|
|
831
|
+
// The editor's record, not the one the section is drawn with: while
|
|
832
|
+
// its preview rows are asked for, the section keeps the source it
|
|
833
|
+
// showed before (see hangDesignerDataSource).
|
|
834
|
+
const record =
|
|
835
|
+
dataSources?.find(
|
|
836
|
+
(ds: { id?: string }) => ds.id === section.dataSourceId
|
|
837
|
+
) ?? section.dataSource;
|
|
838
|
+
const previewWidget = collectionPreviewWidget(record);
|
|
839
|
+
if (
|
|
840
|
+
!previewWidget ||
|
|
841
|
+
liveRowsRef.current.has(previewWidget) ||
|
|
842
|
+
liveRowsAttemptedRef.current.has(previewWidget)
|
|
843
|
+
) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
liveRowsAttemptedRef.current.add(previewWidget);
|
|
848
|
+
|
|
849
|
+
fetchDesignerPreviewRows(previewWidget).then((answer) => {
|
|
850
|
+
if (!liveRowsMountedRef.current) return;
|
|
851
|
+
|
|
852
|
+
// Held as `null` when it gave nothing: the section stops holding
|
|
853
|
+
// its previous rows and falls back to the snapshot.
|
|
854
|
+
liveRowsRef.current.set(previewWidget, answer);
|
|
855
|
+
setLiveRowsRevision((revision) => revision + 1);
|
|
856
|
+
});
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
822
859
|
|
|
823
860
|
const bindingKey = collectionBindingKey(section.dataSource);
|
|
824
861
|
if (!bindingKey) return;
|
|
@@ -844,8 +881,9 @@ export default function ThemePlaceholderWrapper({
|
|
|
844
881
|
});
|
|
845
882
|
});
|
|
846
883
|
// `sections` already follows `dataSources`: every change to the records is
|
|
847
|
-
// hung over the sections before this runs
|
|
848
|
-
|
|
884
|
+
// hung over the sections before this runs — except a record held back
|
|
885
|
+
// while its preview rows are asked for, which is read from `dataSources`.
|
|
886
|
+
}, [sections, dataSources, isDesigner]);
|
|
849
887
|
|
|
850
888
|
useEffect(() => {
|
|
851
889
|
if (typeof window === 'undefined') {
|
|
@@ -940,7 +978,7 @@ export default function ThemePlaceholderWrapper({
|
|
|
940
978
|
|
|
941
979
|
// In editor mode: Complete override with theme editor's data
|
|
942
980
|
// This allows unsaved sections to render with editor's product data
|
|
943
|
-
setSections(() => {
|
|
981
|
+
setSections((previousSections) => {
|
|
944
982
|
return placeholder.sections.map((incomingSection: any) => {
|
|
945
983
|
let dataSource = null;
|
|
946
984
|
if (incomingSection.dataSourceId && theme.dataSources) {
|
|
@@ -963,11 +1001,18 @@ export default function ThemePlaceholderWrapper({
|
|
|
963
1001
|
? { ...incomingSection.properties }
|
|
964
1002
|
: {},
|
|
965
1003
|
// Live rows only for a pick the editor reports as
|
|
966
|
-
// saved; otherwise the
|
|
1004
|
+
// saved; otherwise the preview widget's rows or the
|
|
1005
|
+
// picked snapshot — and what the section showed so
|
|
1006
|
+
// far while the preview widget is asked.
|
|
967
1007
|
dataSource: hangDesignerDataSource(
|
|
968
1008
|
incomingSection,
|
|
969
1009
|
dataSource || null,
|
|
970
|
-
liveRowsRef.current
|
|
1010
|
+
liveRowsRef.current,
|
|
1011
|
+
{
|
|
1012
|
+
previous: previousSections.find(
|
|
1013
|
+
(section) => section.id === incomingSection.id
|
|
1014
|
+
)?.dataSource
|
|
1015
|
+
}
|
|
971
1016
|
)
|
|
972
1017
|
};
|
|
973
1018
|
});
|