@akinon/pz-theme 2.0.36-beta.0 → 2.0.36
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/jest/__mocks__/hookform-resolvers.js +5 -0
- package/jest/__mocks__/next-auth-react.js +5 -0
- package/jest.config.js +3 -0
- package/package.json +4 -4
- package/src/blocks/accordion-block.tsx +35 -3
- package/src/blocks/commerce-format.ts +220 -0
- package/src/blocks/icon-block.tsx +6 -1
- package/src/blocks/image-gallery-block.tsx +1 -1
- package/src/blocks/link-block.tsx +9 -1
- package/src/blocks/slider-block.tsx +2 -1
- package/src/blocks/tab-block.tsx +15 -2
- package/src/blocks/video-block.tsx +6 -0
- package/src/components/__tests__/part-state-preview-scope.test.tsx +148 -0
- package/src/components/action-toolbar.tsx +182 -3
- package/src/components/part-state-preview.tsx +92 -0
- package/src/hooks/__tests__/use-overlay.test.tsx +503 -0
- package/src/hooks/use-designer-features.tsx +16 -1
- package/src/hooks/use-overlay.ts +538 -0
- package/src/hooks/use-visibility-context.ts +14 -2
- package/src/sections/__tests__/divider-section.test.tsx +28 -1
- package/src/sections/contact-form-section.tsx +4 -1
- package/src/sections/coupon-banner-section.tsx +2 -1
- package/src/sections/featured-product-spotlight-section.tsx +4 -4
- package/src/sections/find-in-store-section.tsx +2 -1
- package/src/sections/hover-showcase-section.tsx +2 -1
- package/src/sections/installment-options-section.tsx +2 -1
- package/src/sections/order-tracking-lookup-section.tsx +6 -1
- package/src/sections/posts-slider-section.tsx +8 -3
- package/src/sections/pre-order-launch-banner-section.tsx +2 -1
- package/src/sections/tabs-section.tsx +7 -13
- package/src/theme-block.tsx +81 -16
- package/src/theme-placeholder-client.tsx +8 -1
- package/src/theme-placeholder-wrapper.tsx +17 -3
- package/src/theme-placeholder.tsx +51 -5
- package/src/theme-section.tsx +20 -4
- package/src/utils/__tests__/breakpoint-cascade.test.ts +351 -0
- package/src/utils/__tests__/group-box-chrome.test.ts +83 -0
- package/src/utils/__tests__/iterator-bindings.test.tsx +1178 -0
- package/src/utils/__tests__/part-ownership-guard.test.tsx +298 -0
- package/src/utils/breakpoints.ts +116 -0
- package/src/utils/custom-css.ts +233 -0
- package/src/utils/index.ts +353 -42
- package/src/utils/iterator-utils.ts +675 -62
- package/src/utils/page-context-discovery.test.ts +188 -0
- package/src/utils/page-context-discovery.ts +48 -4
- package/src/utils/part-styles.ts +229 -0
- package/src/utils/row-visibility.ts +273 -0
- package/src/utils/visibility-rules.test.ts +579 -0
- package/src/utils/visibility-rules.ts +346 -3
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Block } from '../theme-block';
|
|
2
2
|
import { Section } from '../theme-section';
|
|
3
|
+
import { BREAKPOINT_FALLBACKS } from './breakpoints';
|
|
3
4
|
import { devWarn } from './dev-warn';
|
|
4
5
|
|
|
5
6
|
export type VisibilityAuthState = 'all' | 'authenticated' | 'guest';
|
|
@@ -9,6 +10,43 @@ export type VisibilityPathMatchType =
|
|
|
9
10
|
| 'contains'
|
|
10
11
|
| 'startsWith';
|
|
11
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Operators a data condition can use. `exists` / `notExists` answer for
|
|
15
|
+
* presence, the rest compare — three textual and four numeric, because a
|
|
16
|
+
* catalogue condition is as often "stock is below 5" as "gender is women".
|
|
17
|
+
*/
|
|
18
|
+
export type VisibilityDataOperator =
|
|
19
|
+
| 'exists'
|
|
20
|
+
| 'notExists'
|
|
21
|
+
| 'equals'
|
|
22
|
+
| 'notEquals'
|
|
23
|
+
| 'contains'
|
|
24
|
+
| 'gt'
|
|
25
|
+
| 'gte'
|
|
26
|
+
| 'lt'
|
|
27
|
+
| 'lte';
|
|
28
|
+
|
|
29
|
+
export const VISIBILITY_DATA_OPERATORS: VisibilityDataOperator[] = [
|
|
30
|
+
'exists',
|
|
31
|
+
'notExists',
|
|
32
|
+
'equals',
|
|
33
|
+
'notEquals',
|
|
34
|
+
'contains',
|
|
35
|
+
'gt',
|
|
36
|
+
'gte',
|
|
37
|
+
'lt',
|
|
38
|
+
'lte'
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/** One condition against the page context, e.g. `product.attributes.gender`. */
|
|
42
|
+
export interface VisibilityDataCondition {
|
|
43
|
+
path: string;
|
|
44
|
+
operator: VisibilityDataOperator;
|
|
45
|
+
value?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type VisibilityDataMatchMode = 'all' | 'any';
|
|
49
|
+
|
|
12
50
|
export interface VisibilityRules {
|
|
13
51
|
enabled?: boolean;
|
|
14
52
|
authState?: VisibilityAuthState;
|
|
@@ -19,8 +57,41 @@ export interface VisibilityRules {
|
|
|
19
57
|
pathnameValue?: string;
|
|
20
58
|
queryParamKey?: string;
|
|
21
59
|
queryParamValue?: string;
|
|
60
|
+
dataConditions?: VisibilityDataCondition[];
|
|
61
|
+
dataMatchMode?: VisibilityDataMatchMode;
|
|
22
62
|
}
|
|
23
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Whether the viewport being rendered is on the rule's allow-list.
|
|
66
|
+
*
|
|
67
|
+
* The list is a set of breakpoint names, and until now there were only two to
|
|
68
|
+
* choose from. Every rule already published that says "mobile only" was written
|
|
69
|
+
* when `mobile` meant *everything below 1024* — so reading such a list
|
|
70
|
+
* literally against a three-way signal would hide a mobile-only banner on every
|
|
71
|
+
* tablet, silently, on compositions nobody touched.
|
|
72
|
+
*
|
|
73
|
+
* So the list falls through the way every other authored value in this system
|
|
74
|
+
* now does: a breakpoint the list does not mention inherits the answer given
|
|
75
|
+
* for the breakpoint it falls back to. A list naming `tablet` is read literally,
|
|
76
|
+
* which is what a rule authored in the current editor does.
|
|
77
|
+
*
|
|
78
|
+
* The one thing this cannot express is "phone but not tablet" — the data has no
|
|
79
|
+
* way to tell a deliberately-unticked tablet from one that could not be ticked
|
|
80
|
+
* when the rule was saved. That is a real limitation, and the alternative is
|
|
81
|
+
* changing what a published rule does, which is worse.
|
|
82
|
+
*/
|
|
83
|
+
export const isBreakpointAllowed = (
|
|
84
|
+
allowed: string[],
|
|
85
|
+
breakpoint: string
|
|
86
|
+
): boolean => {
|
|
87
|
+
if (allowed.includes(breakpoint)) return true;
|
|
88
|
+
|
|
89
|
+
const chain = BREAKPOINT_FALLBACKS[breakpoint];
|
|
90
|
+
if (!chain) return false;
|
|
91
|
+
|
|
92
|
+
return chain.some((next) => allowed.includes(next));
|
|
93
|
+
};
|
|
94
|
+
|
|
24
95
|
export interface VisibilityRuleContext {
|
|
25
96
|
authState: 'authenticated' | 'guest' | 'loading';
|
|
26
97
|
breakpoint: string;
|
|
@@ -30,6 +101,18 @@ export interface VisibilityRuleContext {
|
|
|
30
101
|
searchParams?: {
|
|
31
102
|
get: (key: string) => string | null;
|
|
32
103
|
} | null;
|
|
104
|
+
/**
|
|
105
|
+
* What the page is about — the product, the listing, the category. The server
|
|
106
|
+
* serializes this into the client tree and the walk below is pure, so both
|
|
107
|
+
* renders reach the same answer and the page hydrates into the shape it
|
|
108
|
+
* rendered. Absent (the default) means data conditions have nothing to read.
|
|
109
|
+
*/
|
|
110
|
+
pageContext?: Record<string, unknown> | null;
|
|
111
|
+
/**
|
|
112
|
+
* The editor canvas usually sits on a page with no live product, so data
|
|
113
|
+
* conditions are not evaluated there — see evaluateVisibilityRules.
|
|
114
|
+
*/
|
|
115
|
+
isDesigner?: boolean;
|
|
33
116
|
}
|
|
34
117
|
|
|
35
118
|
const normalizeStringArray = (value: unknown): string[] => {
|
|
@@ -39,6 +122,243 @@ const normalizeStringArray = (value: unknown): string[] => {
|
|
|
39
122
|
.filter(Boolean);
|
|
40
123
|
};
|
|
41
124
|
|
|
125
|
+
/**
|
|
126
|
+
* A row with no path is an unfinished condition, not a condition that fails:
|
|
127
|
+
* it is dropped here. Keeping it and failing closed would let a designer save
|
|
128
|
+
* a half-typed row and blank the block on the storefront while the canvas —
|
|
129
|
+
* which skips data conditions — still shows it. That is the one failure mode
|
|
130
|
+
* nobody can debug from the editor.
|
|
131
|
+
*/
|
|
132
|
+
const normalizeDataConditions = (value: unknown): VisibilityDataCondition[] => {
|
|
133
|
+
if (!Array.isArray(value)) return [];
|
|
134
|
+
|
|
135
|
+
return value
|
|
136
|
+
// Annotated so the element type matches the predicate below: `value` is
|
|
137
|
+
// optional on a condition but always present in the object literal, and a
|
|
138
|
+
// type predicate must be assignable to what it narrows.
|
|
139
|
+
.map((entry): VisibilityDataCondition | null => {
|
|
140
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const candidate = entry as Record<string, unknown>;
|
|
145
|
+
const path = String(candidate.path || '').trim();
|
|
146
|
+
if (!path) return null;
|
|
147
|
+
|
|
148
|
+
const operator = VISIBILITY_DATA_OPERATORS.includes(
|
|
149
|
+
candidate.operator as VisibilityDataOperator
|
|
150
|
+
)
|
|
151
|
+
? (candidate.operator as VisibilityDataOperator)
|
|
152
|
+
: 'exists';
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
path,
|
|
156
|
+
operator,
|
|
157
|
+
value: String(candidate.value ?? '')
|
|
158
|
+
};
|
|
159
|
+
})
|
|
160
|
+
.filter((entry): entry is VisibilityDataCondition => entry !== null);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Same notion of "there is nothing here" the per-row rules use: a product with
|
|
164
|
+
// an empty attributes object carries no attribute, so `exists` must say no.
|
|
165
|
+
const isEmptyValue = (value: unknown): boolean => {
|
|
166
|
+
if (value === undefined || value === null) return true;
|
|
167
|
+
if (typeof value === 'string') return value.trim() === '';
|
|
168
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
169
|
+
if (typeof value === 'object') return Object.keys(value).length === 0;
|
|
170
|
+
return false;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The exact path shapes this file can read, and therefore the only shapes the
|
|
175
|
+
* field picker may offer: a key, `.key`, an index `[0]`, and the bare `[]` the
|
|
176
|
+
* discovery walk emits for "every element of this array".
|
|
177
|
+
*
|
|
178
|
+
* Anything else — `[*]`, a trailing dot, an empty segment — is refused rather
|
|
179
|
+
* than silently mis-tokenised into a shorter path that resolves to the wrong
|
|
180
|
+
* value (or, worse, to a parent that happens to exist).
|
|
181
|
+
*/
|
|
182
|
+
const READABLE_PATH = /^[^.[\]]+(?:\.[^.[\]]+|\[\d*\])*$/;
|
|
183
|
+
const PATH_TOKEN = /[^.[\]]+|\[\d*\]/g;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Segments that would read off the prototype chain rather than off the payload.
|
|
187
|
+
* Merchant data is untrusted, and `product.constructor` answering `exists` on
|
|
188
|
+
* every page is a rule that looks like it works and never does.
|
|
189
|
+
*
|
|
190
|
+
* Naming them is not enough on its own — `toString`, `valueOf` and the rest of
|
|
191
|
+
* Object.prototype answer the same way — so the walk below also reads own
|
|
192
|
+
* properties only. This list stays because a named refusal can warn the
|
|
193
|
+
* designer, where an own-property miss is indistinguishable from a typo.
|
|
194
|
+
*/
|
|
195
|
+
const UNREADABLE_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Resolve a path to the values it selects: one value for a plain path, one per
|
|
199
|
+
* element for each `[]` it crosses. `null` means the path is not readable at
|
|
200
|
+
* all, which is a different answer from "reads as nothing".
|
|
201
|
+
*
|
|
202
|
+
* Same walk the bindings use, so `product.images[0].url` reads identically.
|
|
203
|
+
*/
|
|
204
|
+
const readPageContextPaths = (
|
|
205
|
+
source: unknown,
|
|
206
|
+
path: string
|
|
207
|
+
): unknown[] | null => {
|
|
208
|
+
if (!READABLE_PATH.test(path)) return null;
|
|
209
|
+
|
|
210
|
+
const tokens = path.match(PATH_TOKEN) || [];
|
|
211
|
+
if (tokens.some(token => UNREADABLE_SEGMENTS.has(token))) return null;
|
|
212
|
+
|
|
213
|
+
let values: unknown[] = [source];
|
|
214
|
+
|
|
215
|
+
for (const token of tokens) {
|
|
216
|
+
const next: unknown[] = [];
|
|
217
|
+
|
|
218
|
+
for (const value of values) {
|
|
219
|
+
if (value === undefined || value === null) continue;
|
|
220
|
+
|
|
221
|
+
if (token === '[]') {
|
|
222
|
+
// Pushed one by one: a listing can carry thousands of products and
|
|
223
|
+
// spreading that into an argument list is what blows the stack.
|
|
224
|
+
if (Array.isArray(value)) {
|
|
225
|
+
for (const element of value) next.push(element);
|
|
226
|
+
}
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (token.startsWith('[')) {
|
|
231
|
+
const index = Number.parseInt(token.slice(1, -1), 10);
|
|
232
|
+
if (Array.isArray(value)) next.push(value[index]);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Own properties only. The page context is JSON the server serialized
|
|
237
|
+
// into the client tree, so everything real about the page is an own key;
|
|
238
|
+
// anything inherited is Object.prototype leaking in. It matters because
|
|
239
|
+
// a prototype member is a function, which every operator reads as
|
|
240
|
+
// "present" — `product.toString exists` would show the block on every
|
|
241
|
+
// page ever rendered. Also keeps SSR and hydration honest: a getter that
|
|
242
|
+
// lives on a class prototype server-side does not survive serialization.
|
|
243
|
+
if (Object.prototype.hasOwnProperty.call(value, token)) {
|
|
244
|
+
next.push((value as Record<string, unknown>)[token]);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
values = next;
|
|
249
|
+
if (values.length === 0) return values;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return values;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/** null means "not comparable as text" (object, array, missing). */
|
|
256
|
+
const toScalarString = (value: unknown): string | null => {
|
|
257
|
+
if (typeof value === 'string') return value;
|
|
258
|
+
if (typeof value === 'number') return Number.isFinite(value) ? String(value) : null;
|
|
259
|
+
if (typeof value === 'boolean') return String(value);
|
|
260
|
+
return null;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const toComparableNumber = (value: unknown): number => {
|
|
264
|
+
if (typeof value === 'number') return value;
|
|
265
|
+
if (typeof value === 'string' && value.trim() !== '') return Number(value);
|
|
266
|
+
return Number.NaN;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* A path that resolves to nothing hides the block — every operator except
|
|
271
|
+
* `notExists`, whose whole job is to answer for absence.
|
|
272
|
+
*
|
|
273
|
+
* Failing open is the tempting default and the wrong one: one typo in
|
|
274
|
+
* `product.attributes.gender` would put the women's size guide on every men's
|
|
275
|
+
* PDP and the "no results" copy on a page full of products. Wrong content that
|
|
276
|
+
* looks plausible survives QA; a block that never appears is caught in the
|
|
277
|
+
* first preview. Missing data also genuinely means "this page is not about
|
|
278
|
+
* that thing" — there is no `product` root on a category page — which is
|
|
279
|
+
* exactly when the block should stay away.
|
|
280
|
+
*
|
|
281
|
+
* A path this file cannot read is stricter still: it hides for every operator,
|
|
282
|
+
* `notExists` included, because "unreadable" is not evidence of absence.
|
|
283
|
+
*/
|
|
284
|
+
const evaluateDataCondition = (
|
|
285
|
+
condition: VisibilityDataCondition,
|
|
286
|
+
pageContext: Record<string, unknown> | null | undefined
|
|
287
|
+
): boolean => {
|
|
288
|
+
const resolvedValues = readPageContextPaths(pageContext, condition.path);
|
|
289
|
+
|
|
290
|
+
if (resolvedValues === null) {
|
|
291
|
+
// Not "absent" but "unreadable": no operator can answer it, `notExists`
|
|
292
|
+
// included, so the block stays hidden and the warning names the path.
|
|
293
|
+
devWarn(
|
|
294
|
+
'visibilityRules',
|
|
295
|
+
`Cannot read the field path "${condition.path}"; the condition never passes (content stays hidden)`
|
|
296
|
+
);
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// A `[]` in the path means "any element matches": one element carrying the
|
|
301
|
+
// value shows the block, and `exists` / `notExists` stay exact duals of each
|
|
302
|
+
// other. A path without `[]` selects a single value, so this is the same
|
|
303
|
+
// answer it has always given.
|
|
304
|
+
const presentValues = resolvedValues.filter(value => !isEmptyValue(value));
|
|
305
|
+
const isPresent = presentValues.length > 0;
|
|
306
|
+
|
|
307
|
+
if (condition.operator === 'exists') return isPresent;
|
|
308
|
+
if (condition.operator === 'notExists') return !isPresent;
|
|
309
|
+
|
|
310
|
+
if (!isPresent) return false;
|
|
311
|
+
|
|
312
|
+
const ruleValue = String(condition.value ?? '').trim();
|
|
313
|
+
if (!ruleValue) return false;
|
|
314
|
+
|
|
315
|
+
return presentValues.some(resolved =>
|
|
316
|
+
compareResolvedValue(resolved, condition.operator, ruleValue)
|
|
317
|
+
);
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const compareResolvedValue = (
|
|
321
|
+
resolved: unknown,
|
|
322
|
+
operator: VisibilityDataOperator,
|
|
323
|
+
ruleValue: string
|
|
324
|
+
): boolean => {
|
|
325
|
+
switch (operator) {
|
|
326
|
+
case 'equals':
|
|
327
|
+
case 'notEquals': {
|
|
328
|
+
// Case-insensitive: brands return "Women" while designers type "women",
|
|
329
|
+
// and a fail-closed rule turns that mismatch into an invisible block.
|
|
330
|
+
const scalar = toScalarString(resolved);
|
|
331
|
+
if (scalar === null) return false;
|
|
332
|
+
const matches = scalar.toLowerCase() === ruleValue.toLowerCase();
|
|
333
|
+
return operator === 'equals' ? matches : !matches;
|
|
334
|
+
}
|
|
335
|
+
case 'contains': {
|
|
336
|
+
if (Array.isArray(resolved)) {
|
|
337
|
+
return resolved.some(
|
|
338
|
+
entry => toScalarString(entry)?.toLowerCase() === ruleValue.toLowerCase()
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
const scalar = toScalarString(resolved);
|
|
342
|
+
return scalar !== null && scalar.toLowerCase().includes(ruleValue.toLowerCase());
|
|
343
|
+
}
|
|
344
|
+
case 'gt':
|
|
345
|
+
case 'gte':
|
|
346
|
+
case 'lt':
|
|
347
|
+
case 'lte': {
|
|
348
|
+
const left = toComparableNumber(resolved);
|
|
349
|
+
const right = toComparableNumber(ruleValue);
|
|
350
|
+
if (!Number.isFinite(left) || !Number.isFinite(right)) return false;
|
|
351
|
+
|
|
352
|
+
if (operator === 'gt') return left > right;
|
|
353
|
+
if (operator === 'gte') return left >= right;
|
|
354
|
+
if (operator === 'lt') return left < right;
|
|
355
|
+
return left <= right;
|
|
356
|
+
}
|
|
357
|
+
default:
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
|
|
42
362
|
export const normalizeVisibilityRules = (value: unknown): VisibilityRules => {
|
|
43
363
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
44
364
|
// A truthy-but-non-object value (string, number, array) is a malformed
|
|
@@ -61,7 +381,9 @@ export const normalizeVisibilityRules = (value: unknown): VisibilityRules => {
|
|
|
61
381
|
pathnameMatchType: 'any',
|
|
62
382
|
pathnameValue: '',
|
|
63
383
|
queryParamKey: '',
|
|
64
|
-
queryParamValue: ''
|
|
384
|
+
queryParamValue: '',
|
|
385
|
+
dataConditions: [],
|
|
386
|
+
dataMatchMode: 'all'
|
|
65
387
|
};
|
|
66
388
|
}
|
|
67
389
|
|
|
@@ -85,7 +407,9 @@ export const normalizeVisibilityRules = (value: unknown): VisibilityRules => {
|
|
|
85
407
|
: 'any',
|
|
86
408
|
pathnameValue: String(rules.pathnameValue || '').trim(),
|
|
87
409
|
queryParamKey: String(rules.queryParamKey || '').trim(),
|
|
88
|
-
queryParamValue: String(rules.queryParamValue || '').trim()
|
|
410
|
+
queryParamValue: String(rules.queryParamValue || '').trim(),
|
|
411
|
+
dataConditions: normalizeDataConditions(rules.dataConditions),
|
|
412
|
+
dataMatchMode: rules.dataMatchMode === 'any' ? 'any' : 'all'
|
|
89
413
|
};
|
|
90
414
|
};
|
|
91
415
|
|
|
@@ -108,7 +432,7 @@ export const evaluateVisibilityRules = (
|
|
|
108
432
|
if (
|
|
109
433
|
rules.allowedBreakpoints &&
|
|
110
434
|
rules.allowedBreakpoints.length > 0 &&
|
|
111
|
-
!rules.allowedBreakpoints
|
|
435
|
+
!isBreakpointAllowed(rules.allowedBreakpoints, context.breakpoint)
|
|
112
436
|
) {
|
|
113
437
|
return false;
|
|
114
438
|
}
|
|
@@ -161,6 +485,25 @@ export const evaluateVisibilityRules = (
|
|
|
161
485
|
}
|
|
162
486
|
}
|
|
163
487
|
|
|
488
|
+
// Data conditions are skipped on the canvas. Most editor pages carry no live
|
|
489
|
+
// product, so evaluating them would hide the block the instant the rule is
|
|
490
|
+
// saved — and a designer cannot style what they just made disappear. Visitor
|
|
491
|
+
// conditions above keep applying there, because those the canvas can preview
|
|
492
|
+
// (breakpoint switcher, locale switcher, auth toggle).
|
|
493
|
+
const dataConditions = rules.dataConditions || [];
|
|
494
|
+
if (!context.isDesigner && dataConditions.length > 0) {
|
|
495
|
+
const results = dataConditions.map(condition =>
|
|
496
|
+
evaluateDataCondition(condition, context.pageContext)
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
const isSatisfied =
|
|
500
|
+
rules.dataMatchMode === 'any'
|
|
501
|
+
? results.some(Boolean)
|
|
502
|
+
: results.every(Boolean);
|
|
503
|
+
|
|
504
|
+
if (!isSatisfied) return false;
|
|
505
|
+
}
|
|
506
|
+
|
|
164
507
|
return true;
|
|
165
508
|
};
|
|
166
509
|
|