@akinon/pz-theme 2.0.43-rc.0 → 2.0.43
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 +7 -31
- package/package.json +4 -4
- package/src/__tests__/checkout-pieces.test.tsx +420 -0
- package/src/__tests__/chrome-empty-zone.test.tsx +3 -3
- package/src/__tests__/code-owned-region.test.tsx +30 -6
- package/src/__tests__/composable-region.test.tsx +481 -0
- package/src/__tests__/part-styles.test.ts +24 -4
- package/src/__tests__/registry-parity.test.ts +3 -0
- package/src/__tests__/route-slug.test.ts +11 -0
- package/src/blocks/__tests__/checkout-outlet-blocks.test.tsx +623 -0
- package/src/blocks/block-renderer-registry.tsx +6 -0
- package/src/blocks/checkout-flow-outlet-block.tsx +104 -0
- package/src/blocks/checkout-outlet-base.tsx +241 -0
- package/src/blocks/checkout-steps-outlet-block.tsx +57 -0
- package/src/blocks/checkout-summary-outlet-block.tsx +43 -0
- package/src/chrome/checkout-piece.tsx +145 -0
- package/src/chrome/checkout-pieces-provider.tsx +28 -0
- package/src/chrome/checkout-pieces.ts +173 -0
- package/src/chrome/chrome-fallback.tsx +8 -26
- package/src/chrome/chrome-slot.tsx +25 -26
- package/src/chrome/code-owned-region.tsx +21 -9
- package/src/chrome/composable-region-client.tsx +228 -0
- package/src/chrome/composable-region.tsx +58 -0
- package/src/chrome/composition-source.ts +34 -0
- package/src/chrome/composition-state.ts +49 -0
- package/src/chrome/live-checkout-page-context.tsx +3 -2
- package/src/chrome/placeholder-slugs.ts +14 -4
- package/src/chrome/route-slug.ts +6 -3
- package/src/hooks/use-designer-features.tsx +41 -10
- package/src/index.ts +6 -0
- package/src/utils/__tests__/checkout-safety.test.ts +374 -0
- package/src/utils/checkout-safety.ts +177 -0
- package/src/utils/index.ts +47 -8
- package/src/utils/part-styles.ts +4 -0
- package/src/utils/visibility-rules.ts +31 -8
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The last line of defence for "a composition may restyle checkout, never
|
|
3
|
+
* break it".
|
|
4
|
+
*
|
|
5
|
+
* The editor's descriptors already refuse to offer a hide-capable control on
|
|
6
|
+
* the checkout flow outlet. That guard covers exactly one authoring surface.
|
|
7
|
+
* Published compositions do not all come from it — the AI generator, the
|
|
8
|
+
* legacy widget editor and direct API writes all produce the same JSON, and
|
|
9
|
+
* two routes bypass the descriptors even inside the editor:
|
|
10
|
+
*
|
|
11
|
+
* - an ANCESTOR block. The flow is portaled into the outlet's wrapper, which
|
|
12
|
+
* sits inside every parent block's wrapper, so `display:none` on the
|
|
13
|
+
* surrounding column hides the real payment flow just as effectively.
|
|
14
|
+
* - the scoped CUSTOM CSS hatch, which by design emits whatever it is given
|
|
15
|
+
* (`& [data-part="place-order-button"] { display: none }`).
|
|
16
|
+
*
|
|
17
|
+
* The CSS generator is the one place every route converges, so the rule is
|
|
18
|
+
* enforced here: a declaration that would remove the checkout flow from the
|
|
19
|
+
* page, or from the shopper's pointer, is dropped before it is emitted.
|
|
20
|
+
*
|
|
21
|
+
* SCOPE, stated honestly: this drops the declarations that unambiguously
|
|
22
|
+
* remove an element or its interactivity — `display:none`, `visibility`
|
|
23
|
+
* hidden/collapse, `pointer-events:none`, `content-visibility:hidden` and a
|
|
24
|
+
* effectively-zero `opacity`. It is not a general "can a designer make this
|
|
25
|
+
* hard to see" filter: nothing here stops a 1px-tall box or white-on-white
|
|
26
|
+
* text, and it is not meant to. Layout values a real composition needs
|
|
27
|
+
* (`display:flex`, `opacity:.9`) pass through untouched — this only ever
|
|
28
|
+
* subtracts the values that end ordering.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { parsePartStyleKey } from './part-styles';
|
|
32
|
+
|
|
33
|
+
export const CHECKOUT_FLOW_OUTLET_TYPE = 'checkout-flow-outlet';
|
|
34
|
+
|
|
35
|
+
export const CHECKOUT_OUTLET_BLOCK_TYPES = [
|
|
36
|
+
CHECKOUT_FLOW_OUTLET_TYPE,
|
|
37
|
+
'checkout-steps-outlet',
|
|
38
|
+
'checkout-summary-outlet'
|
|
39
|
+
] as const;
|
|
40
|
+
|
|
41
|
+
const OUTLET_TYPES = new Set<string>(CHECKOUT_OUTLET_BLOCK_TYPES);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parts whose disappearance is a LOOK change, not a broken order: the summary
|
|
45
|
+
* is informational and the flow outlet hosts it only because the mobile sheet
|
|
46
|
+
* re-renders it inside the flow subtree. Everything else the flow stamps —
|
|
47
|
+
* sections, payment options, the payment panel, continue and place-order, the
|
|
48
|
+
* agreement row, the step indicator — is on the path to a placed order.
|
|
49
|
+
*/
|
|
50
|
+
const INFORMATIONAL_PART_PREFIX = 'summary';
|
|
51
|
+
|
|
52
|
+
const FLOW_OUTLET_TYPES = new Set<string>([CHECKOUT_FLOW_OUTLET_TYPE]);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* True when the node IS, or contains at any depth, one of the given types.
|
|
56
|
+
* Accepts `unknown` on purpose: sections and blocks come from three different
|
|
57
|
+
* shapes across the codebase (theme JSON, the editor's Block, the renderer's
|
|
58
|
+
* ThemeBlockData) and every caller only needs `type` plus `blocks`.
|
|
59
|
+
*/
|
|
60
|
+
const subtreeHasType = (node: unknown, types: Set<string>): boolean => {
|
|
61
|
+
if (!node || typeof node !== 'object') return false;
|
|
62
|
+
|
|
63
|
+
const { type, blocks } = node as { type?: unknown; blocks?: unknown };
|
|
64
|
+
|
|
65
|
+
if (typeof type === 'string' && types.has(type)) return true;
|
|
66
|
+
|
|
67
|
+
return Array.isArray(blocks)
|
|
68
|
+
? blocks.some((child) => subtreeHasType(child, types))
|
|
69
|
+
: false;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const containsCheckoutFlowOutlet = (node: unknown): boolean =>
|
|
73
|
+
subtreeHasType(node, FLOW_OUTLET_TYPES);
|
|
74
|
+
|
|
75
|
+
export const containsCheckoutOutlet = (node: unknown): boolean =>
|
|
76
|
+
subtreeHasType(node, OUTLET_TYPES);
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A CSS declaration that takes the element off the page or out of reach.
|
|
80
|
+
* Values arrive as authored, so they are trimmed and lower-cased first; a
|
|
81
|
+
* value we cannot read as a number (a var(), a calc()) is left alone rather
|
|
82
|
+
* than guessed at.
|
|
83
|
+
*/
|
|
84
|
+
export const isHidingDeclaration = (
|
|
85
|
+
property: string,
|
|
86
|
+
value: unknown
|
|
87
|
+
): boolean => {
|
|
88
|
+
if (typeof value !== 'string' && typeof value !== 'number') return false;
|
|
89
|
+
|
|
90
|
+
const name = String(property).trim().toLowerCase();
|
|
91
|
+
const raw = String(value).trim().toLowerCase().replace(/\s*!important$/, '');
|
|
92
|
+
|
|
93
|
+
switch (name) {
|
|
94
|
+
case 'display':
|
|
95
|
+
return raw === 'none';
|
|
96
|
+
case 'visibility':
|
|
97
|
+
return raw === 'hidden' || raw === 'collapse';
|
|
98
|
+
case 'pointer-events':
|
|
99
|
+
return raw === 'none';
|
|
100
|
+
case 'content-visibility':
|
|
101
|
+
return raw === 'hidden';
|
|
102
|
+
case 'opacity': {
|
|
103
|
+
const numeric = raw.endsWith('%')
|
|
104
|
+
? Number.parseFloat(raw) / 100
|
|
105
|
+
: Number.parseFloat(raw);
|
|
106
|
+
return Number.isFinite(numeric) && numeric < 0.1;
|
|
107
|
+
}
|
|
108
|
+
default:
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/** Responsive buckets are per breakpoint, so only the offending one is dropped. */
|
|
114
|
+
const stripHidingFromValue = (property: string, value: unknown): unknown => {
|
|
115
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
116
|
+
const kept = Object.entries(value as Record<string, unknown>).filter(
|
|
117
|
+
([, breakpointValue]) => !isHidingDeclaration(property, breakpointValue)
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// Dropping the mobile bucket makes it fall through to desktop, which is
|
|
121
|
+
// the safe direction: the flow stays on the page at every width.
|
|
122
|
+
return kept.length > 0 ? Object.fromEntries(kept) : undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return isHidingDeclaration(property, value) ? undefined : value;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A block's styles with every order-breaking declaration removed.
|
|
130
|
+
*
|
|
131
|
+
* `partScope` decides how part keys are treated: 'critical-parts' keeps the
|
|
132
|
+
* informational `summary-*` groups hideable (the mobile sheet and the totals
|
|
133
|
+
* are not on the ordering path), 'root-only' leaves every part key alone and
|
|
134
|
+
* is what an ancestor block gets — an ancestor's part keys address its own
|
|
135
|
+
* markup, never the teleported flow.
|
|
136
|
+
*/
|
|
137
|
+
export const stripHidingStyles = (
|
|
138
|
+
styles: Record<string, unknown> | undefined | null,
|
|
139
|
+
partScope: 'root-only' | 'critical-parts'
|
|
140
|
+
): Record<string, unknown> => {
|
|
141
|
+
const safe: Record<string, unknown> = {};
|
|
142
|
+
|
|
143
|
+
Object.entries(styles || {}).forEach(([key, value]) => {
|
|
144
|
+
const parsed = parsePartStyleKey(key);
|
|
145
|
+
|
|
146
|
+
const guarded = parsed
|
|
147
|
+
? partScope === 'critical-parts' &&
|
|
148
|
+
!parsed.part.startsWith(INFORMATIONAL_PART_PREFIX)
|
|
149
|
+
: true;
|
|
150
|
+
|
|
151
|
+
if (!guarded) {
|
|
152
|
+
safe[key] = value;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const property = parsed ? parsed.property : key;
|
|
157
|
+
const kept = stripHidingFromValue(property, value);
|
|
158
|
+
|
|
159
|
+
if (kept !== undefined) safe[key] = kept;
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
return safe;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The same rule for the custom-CSS hatch, applied to the generated text.
|
|
167
|
+
*
|
|
168
|
+
* The hatch is free-form, so this works declaration by declaration: the
|
|
169
|
+
* leading delimiter is preserved and only the declaration itself is removed,
|
|
170
|
+
* which keeps every surrounding rule and every other declaration intact.
|
|
171
|
+
*/
|
|
172
|
+
export const stripHidingCustomCss = (css: string): string =>
|
|
173
|
+
css.replace(
|
|
174
|
+
/(^|[;{])(\s*)(display|visibility|pointer-events|content-visibility|opacity)(\s*:\s*)([^;}]*)/gi,
|
|
175
|
+
(match, delimiter, space, property, colon, value) =>
|
|
176
|
+
isHidingDeclaration(property, value) ? delimiter : match
|
|
177
|
+
);
|
package/src/utils/index.ts
CHANGED
|
@@ -7,6 +7,12 @@ import {
|
|
|
7
7
|
} from './css-safety';
|
|
8
8
|
import { buildPartSelectors, splitPartStyles } from './part-styles';
|
|
9
9
|
import { buildCustomCss } from './custom-css';
|
|
10
|
+
import {
|
|
11
|
+
CHECKOUT_FLOW_OUTLET_TYPE,
|
|
12
|
+
containsCheckoutFlowOutlet,
|
|
13
|
+
stripHidingCustomCss,
|
|
14
|
+
stripHidingStyles
|
|
15
|
+
} from './checkout-safety';
|
|
10
16
|
import { hasBindingToken } from './iterator-utils';
|
|
11
17
|
import {
|
|
12
18
|
BREAKPOINT_FALLBACKS,
|
|
@@ -18,6 +24,7 @@ export * from './publish-window';
|
|
|
18
24
|
export * from './part-styles';
|
|
19
25
|
export * from './custom-css';
|
|
20
26
|
export * from './breakpoints';
|
|
27
|
+
export * from './checkout-safety';
|
|
21
28
|
|
|
22
29
|
export type ImageLoadingStrategy = 'eager' | 'lazy';
|
|
23
30
|
export type ImageFetchPriority = 'high' | 'auto' | 'low';
|
|
@@ -490,6 +497,12 @@ const resolveEntryValue = (
|
|
|
490
497
|
|
|
491
498
|
interface Block {
|
|
492
499
|
id: string;
|
|
500
|
+
/**
|
|
501
|
+
* Read only to recognise the checkout outlets: a block that is or holds the
|
|
502
|
+
* flow outlet has the real payment flow portaled into its wrapper, so its
|
|
503
|
+
* hide-capable declarations are dropped (see utils/checkout-safety.ts).
|
|
504
|
+
*/
|
|
505
|
+
type?: string;
|
|
493
506
|
styleSourceId?: string;
|
|
494
507
|
styles?: ResponsiveStyles;
|
|
495
508
|
/** Set by blocks that name their real root with `data-part="root"`. */
|
|
@@ -1125,14 +1138,29 @@ const collectBlockCSS = (
|
|
|
1125
1138
|
let css = '';
|
|
1126
1139
|
|
|
1127
1140
|
blocks.forEach((block) => {
|
|
1128
|
-
|
|
1141
|
+
// A block that IS or CONTAINS the checkout flow outlet has the real
|
|
1142
|
+
// payment flow portaled into its wrapper, so anything that hides the
|
|
1143
|
+
// wrapper hides checkout. The outlet itself additionally guards its part
|
|
1144
|
+
// keys; an ancestor's part keys address its own markup and are left
|
|
1145
|
+
// alone. See utils/checkout-safety.ts.
|
|
1146
|
+
const guardsCheckoutFlow = containsCheckoutFlowOutlet(block);
|
|
1147
|
+
const blockStyles = guardsCheckoutFlow
|
|
1148
|
+
? stripHidingStyles(
|
|
1149
|
+
block.styles,
|
|
1150
|
+
block.type === CHECKOUT_FLOW_OUTLET_TYPE
|
|
1151
|
+
? 'critical-parts'
|
|
1152
|
+
: 'root-only'
|
|
1153
|
+
)
|
|
1154
|
+
: block.styles;
|
|
1155
|
+
|
|
1156
|
+
if (blockStyles && Object.keys(blockStyles).length > 0) {
|
|
1129
1157
|
const selector = `[data-block-id="${escapeCssAttributeValue(
|
|
1130
1158
|
block.styleSourceId || block.id
|
|
1131
1159
|
)}"]`;
|
|
1132
1160
|
// Root styles ignore part keys, so a part override never leaks onto the
|
|
1133
1161
|
// block wrapper (a padding meant for the dropdown would otherwise
|
|
1134
1162
|
// reposition the whole block).
|
|
1135
|
-
const { root } = splitPartStyles(
|
|
1163
|
+
const { root } = splitPartStyles(blockStyles);
|
|
1136
1164
|
|
|
1137
1165
|
if (Object.keys(root).length > 0) {
|
|
1138
1166
|
// `data-block-id` sits on the designer wrapper, not on the element the
|
|
@@ -1148,18 +1176,20 @@ const collectBlockCSS = (
|
|
|
1148
1176
|
css += generateElementCSS(rootSelector, root, targetBreakpoint);
|
|
1149
1177
|
}
|
|
1150
1178
|
|
|
1151
|
-
css += generatePartCSS(selector,
|
|
1179
|
+
css += generatePartCSS(selector, blockStyles, targetBreakpoint);
|
|
1152
1180
|
}
|
|
1153
1181
|
|
|
1154
1182
|
if (block.properties) {
|
|
1155
1183
|
const selector = `[data-block-id="${escapeCssAttributeValue(
|
|
1156
1184
|
block.styleSourceId || block.id
|
|
1157
1185
|
)}"]`;
|
|
1158
|
-
|
|
1186
|
+
const customCss = buildCustomCss(
|
|
1159
1187
|
block.properties as Record<string, unknown>,
|
|
1160
1188
|
selector,
|
|
1161
1189
|
targetBreakpoint
|
|
1162
1190
|
);
|
|
1191
|
+
|
|
1192
|
+
css += guardsCheckoutFlow ? stripHidingCustomCss(customCss) : customCss;
|
|
1163
1193
|
}
|
|
1164
1194
|
|
|
1165
1195
|
if (block.blocks && block.blocks.length > 0) {
|
|
@@ -1181,10 +1211,17 @@ export const generateThemeCSS = (
|
|
|
1181
1211
|
section.id
|
|
1182
1212
|
)}"]`;
|
|
1183
1213
|
|
|
1184
|
-
|
|
1214
|
+
// A section wrapping the flow outlet hides checkout exactly as an
|
|
1215
|
+
// ancestor block does, and its own part keys are its own markup.
|
|
1216
|
+
const guardsCheckoutFlow = containsCheckoutFlowOutlet(section);
|
|
1217
|
+
const sectionStyles = guardsCheckoutFlow
|
|
1218
|
+
? stripHidingStyles(section.styles, 'root-only')
|
|
1219
|
+
: section.styles;
|
|
1220
|
+
|
|
1221
|
+
if (sectionStyles && Object.keys(sectionStyles).length > 0) {
|
|
1185
1222
|
// Sections went straight to generateElementCSS, so a part key on a
|
|
1186
1223
|
// section was emitted as the invalid declaration `dropdown/color:`.
|
|
1187
|
-
const { root: sectionRoot } = splitPartStyles(
|
|
1224
|
+
const { root: sectionRoot } = splitPartStyles(sectionStyles);
|
|
1188
1225
|
|
|
1189
1226
|
if (Object.keys(sectionRoot).length > 0) {
|
|
1190
1227
|
css += generateElementCSS(
|
|
@@ -1196,17 +1233,19 @@ export const generateThemeCSS = (
|
|
|
1196
1233
|
|
|
1197
1234
|
css += generatePartCSS(
|
|
1198
1235
|
sectionSelector,
|
|
1199
|
-
|
|
1236
|
+
sectionStyles,
|
|
1200
1237
|
targetBreakpoint
|
|
1201
1238
|
);
|
|
1202
1239
|
}
|
|
1203
1240
|
|
|
1204
1241
|
if (section.properties) {
|
|
1205
|
-
|
|
1242
|
+
const customCss = buildCustomCss(
|
|
1206
1243
|
section.properties as Record<string, unknown>,
|
|
1207
1244
|
sectionSelector,
|
|
1208
1245
|
targetBreakpoint
|
|
1209
1246
|
);
|
|
1247
|
+
|
|
1248
|
+
css += guardsCheckoutFlow ? stripHidingCustomCss(customCss) : customCss;
|
|
1210
1249
|
}
|
|
1211
1250
|
|
|
1212
1251
|
if (section.blocks && section.blocks.length > 0) {
|
package/src/utils/part-styles.ts
CHANGED
|
@@ -45,6 +45,10 @@ const STATE_SELECTORS: Record<string, string[]> = {
|
|
|
45
45
|
],
|
|
46
46
|
checked: [':checked', '[data-state~="checked"]', '[aria-checked="true"]'],
|
|
47
47
|
open: ['[data-state~="open"]', '[aria-expanded="true"]'],
|
|
48
|
+
// A finished step/section in a flow (checkout steps, wizard sections).
|
|
49
|
+
// data-state only, on purpose: no ARIA attribute honestly means "completed",
|
|
50
|
+
// so inventing an ARIA analog here would let unrelated markup match.
|
|
51
|
+
completed: ['[data-state~="completed"]'],
|
|
48
52
|
invalid: ['[aria-invalid="true"]', '[data-state~="invalid"]'],
|
|
49
53
|
loading: ['[data-state~="loading"]'],
|
|
50
54
|
empty: ['[data-state~="empty"]'],
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Block } from '../theme-block';
|
|
2
2
|
import { Section } from '../theme-section';
|
|
3
3
|
import { BREAKPOINT_FALLBACKS } from './breakpoints';
|
|
4
|
+
import { containsCheckoutOutlet } from './checkout-safety';
|
|
4
5
|
import { devWarn } from './dev-warn';
|
|
5
6
|
|
|
6
7
|
export type VisibilityAuthState = 'all' | 'authenticated' | 'guest';
|
|
@@ -507,15 +508,38 @@ export const evaluateVisibilityRules = (
|
|
|
507
508
|
return true;
|
|
508
509
|
};
|
|
509
510
|
|
|
511
|
+
/**
|
|
512
|
+
* Checkout outlets are exempt from visibility rules — theirs, and those of
|
|
513
|
+
* every block or section wrapping them.
|
|
514
|
+
*
|
|
515
|
+
* An outlet is not content, it is a POSITION: the piece it claims latches
|
|
516
|
+
* once, at page mount, and never re-targets. Both context inputs a rule reads
|
|
517
|
+
* are wrong at exactly that moment — `useSession()` still reports 'loading'
|
|
518
|
+
* on the first client render, and the breakpoint is initialised to 'desktop'
|
|
519
|
+
* before the resize effect corrects it — so a rule that is genuinely
|
|
520
|
+
* satisfied on the shopper's device still hides the outlet through that first
|
|
521
|
+
* render. The outlet then misses the latch, and because late registration on
|
|
522
|
+
* live is ignored by design, the composed placement stays dead for the whole
|
|
523
|
+
* page load while the designer canvas (which follows the live claim instead
|
|
524
|
+
* of latching) shows it working.
|
|
525
|
+
*
|
|
526
|
+
* Rather than race the latch, the outlet is kept mounted. Little is lost:
|
|
527
|
+
* conditional placement was never expressible anyway (a claim is
|
|
528
|
+
* breakpoint-unaware and permanent), the steps and summary outlets stay
|
|
529
|
+
* hideable through their Visibility style, and the editor drops the
|
|
530
|
+
* Visibility Rules box for these blocks so the panel tells the same story.
|
|
531
|
+
*/
|
|
532
|
+
const isVisibilityRuleExempt = (node: Block | Section): boolean =>
|
|
533
|
+
containsCheckoutOutlet(node);
|
|
534
|
+
|
|
510
535
|
const applyBlockVisibility = (
|
|
511
536
|
block: Block,
|
|
512
537
|
context: VisibilityRuleContext
|
|
513
538
|
): Block => {
|
|
514
539
|
const blocks = block.blocks?.map(child => applyBlockVisibility(child, context));
|
|
515
|
-
const isVisibleByRules =
|
|
516
|
-
block
|
|
517
|
-
context
|
|
518
|
-
);
|
|
540
|
+
const isVisibleByRules =
|
|
541
|
+
isVisibilityRuleExempt(block) ||
|
|
542
|
+
evaluateVisibilityRules(block.properties?.visibilityRules, context);
|
|
519
543
|
|
|
520
544
|
return {
|
|
521
545
|
...block,
|
|
@@ -530,10 +554,9 @@ export const applyVisibilityRulesToSections = (
|
|
|
530
554
|
): Section[] =>
|
|
531
555
|
sections.map(section => {
|
|
532
556
|
const blocks = section.blocks.map(block => applyBlockVisibility(block, context));
|
|
533
|
-
const isVisibleByRules =
|
|
534
|
-
section
|
|
535
|
-
context
|
|
536
|
-
);
|
|
557
|
+
const isVisibleByRules =
|
|
558
|
+
isVisibilityRuleExempt(section) ||
|
|
559
|
+
evaluateVisibilityRules(section.properties?.visibilityRules, context);
|
|
537
560
|
|
|
538
561
|
return {
|
|
539
562
|
...section,
|