@capillarytech/creatives-library 9.0.59-alpha.2 → 9.0.60
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/constants/unified.js +2 -3
- package/package.json +1 -1
- package/services/api.js +1 -3
- package/utils/common.js +0 -8
- package/v2Components/FormBuilder/Functional/FormBuilderShell.js +69 -324
- package/v2Components/FormBuilder/Functional/channels/registry.js +0 -2
- package/v2Components/FormBuilder/Functional/constants.js +8 -46
- package/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js +28 -175
- package/v2Components/FormBuilder/Functional/core/store/formReducer.js +6 -75
- package/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js +20 -10
- package/v2Components/FormBuilder/Functional/layout/FieldSlot.js +1 -9
- package/v2Components/FormBuilder/Functional/layout/SchemaForm.js +3 -20
- package/v2Components/FormBuilder/Functional/layout/Section.js +1 -6
- package/v2Components/FormBuilder/Functional/tests/fieldSlot.test.js +2 -9
- package/v2Components/FormBuilder/Functional/tests/schemaForm.test.js +0 -17
- package/v2Components/FormBuilder/_formBuilder.scss +0 -11
- package/v2Components/FormBuilder/index.js +13 -20
- package/v2Components/FormBuilder/tests/entryGate.test.js +7 -87
- package/v2Containers/CommunicationFlow/CommunicationFlow.js +13 -32
- package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +10 -36
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +83 -0
- package/v2Containers/CommunicationFlow/hooks/Tests/useFetchCommDefinition.test.js +118 -0
- package/v2Containers/CommunicationFlow/hooks/useFetchCommDefinition.js +49 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +1 -5
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +28 -0
- package/v2Containers/CommunicationFlow/utils/Tests/mapCommDefinitionToStepData.test.js +29 -0
- package/v2Containers/CommunicationFlow/utils/mapCommDefinitionToStepData.js +6 -13
- package/v2Components/FormBuilder/Functional/channels/mobilepush/buildSubmitPayload.js +0 -10
- package/v2Components/FormBuilder/Functional/channels/mobilepush/config.js +0 -75
- package/v2Components/FormBuilder/Functional/channels/mobilepush/getEditorErrorDescriptor.js +0 -74
- package/v2Components/FormBuilder/Functional/channels/mobilepush/index.js +0 -28
- package/v2Components/FormBuilder/Functional/channels/mobilepush/modals.js +0 -21
- package/v2Components/FormBuilder/Functional/channels/mobilepush/runSubmitPipeline.js +0 -45
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/getEditorErrorDescriptor.test.js +0 -162
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/modals.test.js +0 -37
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/runSubmitPipeline.test.js +0 -70
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/validate.test.js +0 -274
- package/v2Components/FormBuilder/Functional/channels/mobilepush/validate.js +0 -251
- package/v2Components/FormBuilder/Functional/layout/TabsContainer.js +0 -85
- package/v2Components/FormBuilder/Functional/renderers/mpushRenderers.js +0 -448
- package/v2Components/FormBuilder/Functional/tests/manual.typingPerf.test.js +0 -135
- package/v2Components/FormBuilder/Functional/tests/mpush.crossFlowParity.test.js +0 -430
- package/v2Components/FormBuilder/Functional/tests/mpush.ctaFlows.parity.test.js +0 -320
- package/v2Components/FormBuilder/Functional/tests/mpush.editContainer.parity.test.js +0 -148
- package/v2Components/FormBuilder/Functional/tests/mpush.engine.test.js +0 -306
- package/v2Components/FormBuilder/Functional/tests/mpush.libraryValidity.parity.test.js +0 -147
- package/v2Components/FormBuilder/Functional/tests/mpush.shellEvents.test.js +0 -200
- package/v2Components/FormBuilder/Functional/tests/mpushRenderers.test.js +0 -376
- package/v2Components/FormBuilder/Functional/tests/tabsContainer.test.js +0 -110
- package/v2Components/FormBuilder/tests/mpush.characterization.test.js +0 -459
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* FormBuilderShell — the functional FormBuilder (Phase 1: SMS
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* channel containers unchanged.
|
|
2
|
+
* FormBuilderShell — the functional FormBuilder (Phase 1: SMS). Owns form state in a
|
|
3
|
+
* useReducer, bridges to the legacy formData contract via the codec, and validates
|
|
4
|
+
* through the channel adapter. Stage A keeps the channel containers unchanged.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
import React, {
|
|
@@ -11,19 +10,14 @@ import React, {
|
|
|
11
10
|
import PropTypes from 'prop-types';
|
|
12
11
|
import debounce from 'lodash/debounce';
|
|
13
12
|
import isEqual from 'lodash/isEqual';
|
|
14
|
-
import cloneDeep from 'lodash/cloneDeep';
|
|
15
13
|
import CapSpin from '@capillarytech/cap-ui-library/CapSpin';
|
|
16
|
-
import CapModal from '@capillarytech/cap-ui-library/CapModal';
|
|
17
|
-
import CapButton from '@capillarytech/cap-ui-library/CapButton';
|
|
18
14
|
import formReducer, {
|
|
19
15
|
createInitialState,
|
|
20
16
|
hydrate,
|
|
21
17
|
fieldChanged,
|
|
22
|
-
setActiveTab,
|
|
23
|
-
reconcileSchema,
|
|
24
18
|
} from './core/store/formReducer';
|
|
25
19
|
import { toLegacy, fromLegacy } from './core/store/toLegacyFormData';
|
|
26
|
-
import initializeFormState
|
|
20
|
+
import initializeFormState from './core/schema/initializeFormState';
|
|
27
21
|
import { getChannelAdapter } from './channels/registry';
|
|
28
22
|
import { createSmsRegistry } from './renderers/smsRenderers';
|
|
29
23
|
import { validateLiquidTemplateContent } from '../../../utils/commonUtils';
|
|
@@ -32,9 +26,7 @@ import { SMS } from '../../../v2Containers/CreativesContainer/constants';
|
|
|
32
26
|
import { DEFAULT as DEFAULT_MODULE, EMBEDDED } from '../../../constants/unified';
|
|
33
27
|
import formMessages from '../messages';
|
|
34
28
|
import SchemaForm from './layout/SchemaForm';
|
|
35
|
-
import {
|
|
36
|
-
ACTIVE_TAB_INDEX, DATA_COMMIT_EVENTS, DATA_COMMIT_FIELD_TYPES, EDITOR_ERROR_KIND, HIGH_FREQ_FIELDS,
|
|
37
|
-
} from './constants';
|
|
29
|
+
import { ACTIVE_TAB_INDEX, EDITOR_ERROR_KIND, HIGH_FREQ_FIELDS } from './constants';
|
|
38
30
|
|
|
39
31
|
const isNonEmpty = (obj) => obj && Object.keys(obj).length > 0;
|
|
40
32
|
|
|
@@ -42,6 +34,9 @@ const buildInitialState = (schema, parentFormData, channel) => (isNonEmpty(paren
|
|
|
42
34
|
? fromLegacy(parentFormData, { channel })
|
|
43
35
|
: initializeFormState(schema || {}));
|
|
44
36
|
|
|
37
|
+
// Phase 1: only SMS has renderers. Later channels return their own registry.
|
|
38
|
+
const registryFor = () => createSmsRegistry();
|
|
39
|
+
|
|
45
40
|
const FormBuilderShell = (props) => {
|
|
46
41
|
const {
|
|
47
42
|
schema,
|
|
@@ -63,26 +58,19 @@ const FormBuilderShell = (props) => {
|
|
|
63
58
|
waitEventContextTags,
|
|
64
59
|
restrictPersonalization,
|
|
65
60
|
refs,
|
|
66
|
-
currentTab: currentTabProp,
|
|
67
|
-
showModal,
|
|
68
|
-
modal,
|
|
69
61
|
} = props;
|
|
70
62
|
|
|
71
63
|
const channel = (channelProp || schema?.channel || SMS).toUpperCase();
|
|
72
64
|
const adapter = getChannelAdapter(channel);
|
|
73
|
-
|
|
74
|
-
const registry = useMemo(
|
|
75
|
-
() => (adapter?.createRegistry ? adapter.createRegistry() : createSmsRegistry()),
|
|
76
|
-
[channel],
|
|
77
|
-
);
|
|
65
|
+
const registry = useMemo(() => registryFor(), [channel]);
|
|
78
66
|
|
|
79
67
|
const [state, dispatch] = useReducer(formReducer, channel, createInitialState);
|
|
80
68
|
const [errorData, setErrorData] = useState({});
|
|
81
|
-
//
|
|
82
|
-
//
|
|
69
|
+
// Field error MESSAGES are hidden until the user triggers validation (clicks
|
|
70
|
+
// Save / "Done"), matching the class component's `checkValidation` gate. errorData
|
|
71
|
+
// is still computed + emitted via onFormValidityChange on mount; only the inline
|
|
72
|
+
// display is gated. Can also be driven by the container's checkValidation prop.
|
|
83
73
|
const [internalCheckValidation, setInternalCheckValidation] = useState(false);
|
|
84
|
-
// Single-platform save modal: container owns content, shell owns visibility (Classic parity).
|
|
85
|
-
const [modalVisible, setModalVisible] = useState(false);
|
|
86
74
|
|
|
87
75
|
// refs to latest values for use inside stable/debounced callbacks
|
|
88
76
|
const stateRef = useRef(state);
|
|
@@ -92,18 +80,6 @@ const FormBuilderShell = (props) => {
|
|
|
92
80
|
const lastEmittedRef = useRef(null);
|
|
93
81
|
const didInitRef = useRef(false);
|
|
94
82
|
const prevStartValidationRef = useRef(startValidation);
|
|
95
|
-
const prevSchemaRef = useRef(null);
|
|
96
|
-
// Deep snapshot of the last processed parent formData — containers mutate it in
|
|
97
|
-
// place (copy-content, tag insertion), so pushes are deep-compared, not ref-keyed.
|
|
98
|
-
const lastSeenParentRef = useRef(null);
|
|
99
|
-
// android/ios validity from the last save-validation (single-platform modal contract).
|
|
100
|
-
const platformRef = useRef(null);
|
|
101
|
-
// Content snapshot of the tags prop (containers rebuild the array each render).
|
|
102
|
-
const lastTagsRef = useRef(null);
|
|
103
|
-
const lastNextStateRef = useRef(null);
|
|
104
|
-
// GOLDEN parity: Classic emits tabCount starting at 1, mirroring the prop only
|
|
105
|
-
// on a parent formData sync — the create flow emits 1 though the containers pass 2.
|
|
106
|
-
const emittedTabCountRef = useRef(1);
|
|
107
83
|
|
|
108
84
|
// Stable handler identities for the memoized FieldSlot: these wrappers delegate to
|
|
109
85
|
// the latest handler bodies (via a ref), so React.memo can bail out on sibling changes.
|
|
@@ -115,82 +91,57 @@ const FormBuilderShell = (props) => {
|
|
|
115
91
|
const isEmbedded = location?.query?.type === EMBEDDED;
|
|
116
92
|
const currentModule = tagModule || location?.query?.module || DEFAULT_MODULE;
|
|
117
93
|
|
|
118
|
-
const
|
|
119
|
-
const currentTab = activeTabIndex + 1; // legacy 1-based (MOBILEPUSH: 1=Android, 2=iOS)
|
|
120
|
-
const tabCount = state?.tabs?.length || 1;
|
|
121
|
-
const hasTabs = Boolean(adapter?.config?.features?.tabs);
|
|
122
|
-
|
|
123
|
-
const runValidate = (legacy, extra = {}) => adapter.validate(legacy, {
|
|
94
|
+
const runValidate = (legacy) => adapter.validate(legacy, {
|
|
124
95
|
tags: propsRef.current.tags,
|
|
125
96
|
isFullMode,
|
|
126
97
|
isEmbedded,
|
|
127
98
|
currentModule,
|
|
128
|
-
restrictPersonalization: propsRef.current.restrictPersonalization,
|
|
129
|
-
currentTab: (stateRef.current?.meta?.activeTabIndex ?? ACTIVE_TAB_INDEX) + 1,
|
|
130
|
-
schema: propsRef.current.schema,
|
|
131
|
-
...extra,
|
|
132
99
|
});
|
|
133
100
|
|
|
134
|
-
// Classic's liquidErrorMessage
|
|
101
|
+
// Footer state (mirrors Classic's `liquidErrorMessage`). Holds the last
|
|
102
|
+
// {STANDARD_ERROR_MSG, LIQUID_ERROR_MSG} so standard- and liquid-error pushes don't
|
|
103
|
+
// clobber each other before forwarding to the container's showLiquidErrorInFooter.
|
|
135
104
|
const footerRef = useRef({ STANDARD_ERROR_MSG: [], LIQUID_ERROR_MSG: [] });
|
|
136
105
|
const liquidEnabled = Boolean(adapter.config?.features?.liquid);
|
|
137
|
-
// Classic passes null as the footer's tab argument for SMS and the live 1-based
|
|
138
|
-
// currentTab for every other channel (Classic.js:1352, 1487).
|
|
139
|
-
const footerTabArg = () => (adapter.config?.footerTabArg === 'currentTab'
|
|
140
|
-
? (stateRef.current?.meta?.activeTabIndex ?? ACTIVE_TAB_INDEX) + 1
|
|
141
|
-
: null);
|
|
142
106
|
|
|
143
107
|
const pushFooter = (next) => {
|
|
144
108
|
footerRef.current = { ...footerRef.current, ...next };
|
|
145
109
|
if (propsRef.current.showLiquidErrorInFooter) {
|
|
146
|
-
propsRef.current.showLiquidErrorInFooter(
|
|
110
|
+
propsRef.current.showLiquidErrorInFooter(
|
|
111
|
+
footerRef.current,
|
|
112
|
+
channel === SMS ? null : propsRef.current.currentTab,
|
|
113
|
+
);
|
|
147
114
|
}
|
|
148
115
|
};
|
|
149
116
|
|
|
150
|
-
// Footer body message for `legacy
|
|
151
|
-
//
|
|
152
|
-
|
|
117
|
+
// Footer-eligible body message (brace/missing-tag) for `legacy`, or [] when none.
|
|
118
|
+
// Full mode shows the brace error inline instead, so the footer is suppressed there;
|
|
119
|
+
// library mode keeps the old flow (message goes to the footer).
|
|
120
|
+
const standardFooterMsg = (legacy) => {
|
|
153
121
|
if (isFullMode || !adapter.getEditorErrorDescriptor || !intl) return [];
|
|
154
122
|
const descriptor = adapter.getEditorErrorDescriptor(legacy, {
|
|
155
123
|
tags: propsRef.current.tags,
|
|
156
124
|
isFullMode,
|
|
157
125
|
currentModule,
|
|
158
|
-
errorData: result?.errorData,
|
|
159
|
-
currentTab: (stateRef.current?.meta?.activeTabIndex ?? ACTIVE_TAB_INDEX) + 1,
|
|
160
|
-
schema: propsRef.current.schema,
|
|
161
|
-
checkValidation: internalCheckValidation
|
|
162
|
-
|| Boolean(propsRef.current.checkValidation)
|
|
163
|
-
|| Boolean(propsRef.current.startValidation),
|
|
164
|
-
restrictPersonalization: propsRef.current.restrictPersonalization,
|
|
165
126
|
});
|
|
166
|
-
|
|
167
|
-
if (descriptor.text) return [descriptor.text];
|
|
168
|
-
return [intl.formatMessage(descriptor.message, descriptor.values)];
|
|
127
|
+
return descriptor ? [intl.formatMessage(descriptor.message, descriptor.values)] : [];
|
|
169
128
|
};
|
|
170
129
|
|
|
171
130
|
const emitValidity = (result, legacy) => {
|
|
172
131
|
setErrorData(result.errorData);
|
|
173
|
-
if (result.platform) platformRef.current = result.platform;
|
|
174
132
|
if (propsRef.current.onFormValidityChange) {
|
|
175
133
|
propsRef.current.onFormValidityChange(result.isValid, result.errorData);
|
|
176
134
|
}
|
|
177
135
|
// Footer: clear standard errors when the form is valid; otherwise surface the
|
|
178
|
-
//
|
|
136
|
+
// brace/missing-tag message (empty/generic => none). Liquid errors are preserved.
|
|
179
137
|
if (liquidEnabled) {
|
|
180
|
-
pushFooter({ STANDARD_ERROR_MSG: result.isValid ? [] : standardFooterMsg(legacy
|
|
138
|
+
pushFooter({ STANDARD_ERROR_MSG: result.isValid ? [] : standardFooterMsg(legacy) });
|
|
181
139
|
}
|
|
182
140
|
};
|
|
183
141
|
|
|
184
142
|
const emitChange = (legacy, field) => {
|
|
185
143
|
lastEmittedRef.current = legacy;
|
|
186
|
-
if (propsRef.current.onChange)
|
|
187
|
-
propsRef.current.onChange(
|
|
188
|
-
legacy,
|
|
189
|
-
emittedTabCountRef.current,
|
|
190
|
-
(stateRef.current?.meta?.activeTabIndex ?? ACTIVE_TAB_INDEX) + 1,
|
|
191
|
-
field,
|
|
192
|
-
);
|
|
193
|
-
}
|
|
144
|
+
if (propsRef.current.onChange) propsRef.current.onChange(legacy, 1, 1, field);
|
|
194
145
|
};
|
|
195
146
|
|
|
196
147
|
const debouncedEmit = useMemo(
|
|
@@ -199,139 +150,31 @@ const FormBuilderShell = (props) => {
|
|
|
199
150
|
);
|
|
200
151
|
useEffect(() => () => debouncedEmit.cancel(), [debouncedEmit]);
|
|
201
152
|
|
|
202
|
-
// Classic pushes formData up on init and on every schema re-init (Classic.js:1906-1911 ->
|
|
203
|
-
// 740-748); the MPUSH copy-content handlers read that copy. MPUSH-only, SMS unchanged.
|
|
204
|
-
const emitFormDataOnSchemaSync = Boolean(adapter.config?.features?.emitFormDataOnSchemaSync);
|
|
205
|
-
|
|
206
153
|
// ---- initialize once the (possibly async) schema is ready, then validate+emit ----
|
|
207
|
-
const schemaReady = Boolean(schema?.standalone
|
|
154
|
+
const schemaReady = Boolean(schema?.standalone);
|
|
208
155
|
useEffect(() => {
|
|
209
156
|
if (schemaReady && !didInitRef.current) {
|
|
210
157
|
didInitRef.current = true;
|
|
211
|
-
prevSchemaRef.current = schema;
|
|
212
158
|
const init = buildInitialState(schema, propsRef.current.formData, channel);
|
|
213
|
-
if (isNonEmpty(propsRef.current.formData)) {
|
|
214
|
-
lastSeenParentRef.current = cloneDeep(propsRef.current.formData);
|
|
215
|
-
// Classic mirrors the tabCount prop only alongside a parent formData sync.
|
|
216
|
-
emittedTabCountRef.current = propsRef.current.tabCount || init.tabs?.length || 1;
|
|
217
|
-
}
|
|
218
159
|
dispatch(hydrate(init));
|
|
219
|
-
lastTagsRef.current = cloneDeep(propsRef.current.tags || null);
|
|
220
160
|
const legacy = toLegacy(init);
|
|
221
161
|
emitValidity(runValidate(legacy), legacy);
|
|
222
|
-
if (emitFormDataOnSchemaSync) emitChange(legacy); // seed the container's formData copy
|
|
223
|
-
|
|
224
162
|
}
|
|
225
163
|
}, [schemaReady]);
|
|
226
164
|
|
|
227
|
-
//
|
|
228
|
-
// the MPUSH containers gate their save flows on that validity. SMS keeps shipped behavior.
|
|
229
|
-
const validateOnExternalChange = Boolean(adapter.config?.features?.validateOnExternalChange);
|
|
230
|
-
|
|
231
|
-
// Background emissions must read POST-dispatch state (stateRef can be stale mid
|
|
232
|
-
// container-setState-cascade): effects ARM these; the [state] effect below emits.
|
|
233
|
-
const pendingBackgroundValidateRef = useRef(false);
|
|
234
|
-
const pendingEmitChangeRef = useRef(false);
|
|
235
|
-
|
|
236
|
-
// ---- reconcile on a genuine schema change (CTA splices, text<->image swap) ----
|
|
237
|
-
// Runs inside the reducer (reconcileSchema) — see the action's note on stale snapshots.
|
|
238
|
-
useEffect(() => {
|
|
239
|
-
if (!didInitRef.current || !schema || prevSchemaRef.current === schema) return;
|
|
240
|
-
prevSchemaRef.current = schema;
|
|
241
|
-
dispatch(reconcileSchema(schema, reconcileFormState));
|
|
242
|
-
// Classic re-validates on EVERY schema change (Classic.js:542-559).
|
|
243
|
-
const validationActive = propsRef.current.startValidation || internalCheckValidation || Boolean(propsRef.current.checkValidation);
|
|
244
|
-
if (validationActive || validateOnExternalChange) {
|
|
245
|
-
pendingBackgroundValidateRef.current = true;
|
|
246
|
-
}
|
|
247
|
-
// Classic re-emits onChange after a schema-change re-init (Classic.js:1908-1909).
|
|
248
|
-
if (emitFormDataOnSchemaSync) pendingEmitChangeRef.current = true;
|
|
249
|
-
}, [schema]);
|
|
250
|
-
|
|
251
|
-
// ---- re-hydrate on a GENUINE external parent push (edit load / discard / copy) ----
|
|
252
|
-
// Contract: parent pushes must arrive as NEW references (all containers comply);
|
|
253
|
-
// the deep-compare guards below are the real gate, mirroring Classic's CWRP (Classic.js:516).
|
|
165
|
+
// ---- re-hydrate on a GENUINE external parent push (edit load / discard) ----
|
|
254
166
|
useEffect(() => {
|
|
255
167
|
if (!didInitRef.current || !isNonEmpty(parentFormData)) return;
|
|
256
|
-
if (isEqual(parentFormData, lastSeenParentRef.current)) return; // nothing new from the parent
|
|
257
|
-
lastSeenParentRef.current = cloneDeep(parentFormData);
|
|
258
168
|
const current = toLegacy(stateRef.current);
|
|
259
169
|
const isEcho = isEqual(parentFormData, lastEmittedRef.current) || isEqual(parentFormData, current);
|
|
260
|
-
if (!isEcho) {
|
|
261
|
-
dispatch(hydrate(fromLegacy(parentFormData, { channel })));
|
|
262
|
-
// Classic mirrors the tabCount prop on a data sync only in EDIT mode (Classic.js:446-447);
|
|
263
|
-
// adopting the create flow's hardcoded 2 makes the emptiness check scan the empty iOS tab.
|
|
264
|
-
if (propsRef.current.isEdit) {
|
|
265
|
-
emittedTabCountRef.current = propsRef.current.tabCount || emittedTabCountRef.current;
|
|
266
|
-
}
|
|
267
|
-
// Classic validates on every deep-unequal parent push (Classic.js:516-537).
|
|
268
|
-
if (validateOnExternalChange) {
|
|
269
|
-
pendingBackgroundValidateRef.current = true;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
170
|
+
if (!isEcho) dispatch(hydrate(fromLegacy(parentFormData, { channel })));
|
|
272
171
|
}, [parentFormData]);
|
|
273
172
|
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
if (pendingBackgroundValidateRef.current) {
|
|
279
|
-
pendingBackgroundValidateRef.current = false;
|
|
280
|
-
emitValidity(runValidate(legacy), legacy);
|
|
281
|
-
}
|
|
282
|
-
if (pendingEmitChangeRef.current) {
|
|
283
|
-
pendingEmitChangeRef.current = false;
|
|
284
|
-
emitChange(legacy);
|
|
285
|
-
}
|
|
286
|
-
}, [state]);
|
|
287
|
-
|
|
288
|
-
// ---- re-validate on a tags-catalog change (Classic.js:569-578) — containers rebuild
|
|
289
|
-
// the array every render, so compare by content; emits validity only, display stays gated ----
|
|
290
|
-
useEffect(() => {
|
|
291
|
-
if (!didInitRef.current || !validateOnExternalChange) return;
|
|
292
|
-
const nextTags = propsRef.current.tags;
|
|
293
|
-
if (!Array.isArray(nextTags) || !nextTags.length) return;
|
|
294
|
-
if (isEqual(nextTags, lastTagsRef.current)) return;
|
|
295
|
-
lastTagsRef.current = cloneDeep(nextTags);
|
|
296
|
-
const legacy = toLegacy(stateRef.current);
|
|
297
|
-
emitValidity(runValidate(legacy), legacy);
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
// ---- container-driven tab position (MOBILEPUSH: Create forces iOS when Android unsupported) ----
|
|
301
|
-
useEffect(() => {
|
|
302
|
-
if (!hasTabs || typeof currentTabProp !== 'number') return;
|
|
303
|
-
dispatch(setActiveTab(currentTabProp - 1));
|
|
304
|
-
}, [currentTabProp, hasTabs]);
|
|
305
|
-
|
|
306
|
-
// ---- container-driven modal visibility (mirrors Classic's props->state modal sync) ----
|
|
307
|
-
useEffect(() => {
|
|
308
|
-
setModalVisible(Boolean(showModal));
|
|
309
|
-
}, [showModal, modal]);
|
|
310
|
-
|
|
311
|
-
// Submit after sync validation (Classic onSubmitWrapper): adapter.runSubmitPipeline
|
|
312
|
-
// when the channel has one, else the default liquid flow (validate outside full mode).
|
|
313
|
-
const submitValidForm = (legacy, validErrorData, { singleTab = null } = {}) => {
|
|
173
|
+
// Submit a form that passed sync validation (mirrors Classic onSubmitWrapper).
|
|
174
|
+
// Full mode submits directly; outside it, liquid-supported channels run liquid-tag
|
|
175
|
+
// validation first — a clean result submits, an error goes to the footer and blocks.
|
|
176
|
+
const submitValidForm = (legacy, validErrorData) => {
|
|
314
177
|
const currentProps = propsRef.current;
|
|
315
|
-
|
|
316
|
-
if (typeof adapter.runSubmitPipeline === 'function') {
|
|
317
|
-
adapter.runSubmitPipeline(legacy, {
|
|
318
|
-
isFullMode,
|
|
319
|
-
singleTab,
|
|
320
|
-
currentTab: (stateRef.current?.meta?.activeTabIndex ?? ACTIVE_TAB_INDEX) + 1,
|
|
321
|
-
baseLanguage: currentProps.baseLanguage,
|
|
322
|
-
getLiquidTags: currentProps.actions?.getLiquidTags,
|
|
323
|
-
formatMessage: currentProps.intl?.formatMessage,
|
|
324
|
-
messages: formMessages,
|
|
325
|
-
}, {
|
|
326
|
-
onSubmit: (payload) => currentProps.onSubmit && currentProps.onSubmit(payload),
|
|
327
|
-
pushFooter,
|
|
328
|
-
stopValidation: () => currentProps.stopValidation && currentProps.stopValidation(),
|
|
329
|
-
onFormValidityChange: (isValid, nextErrorData) => currentProps.onFormValidityChange
|
|
330
|
-
&& currentProps.onFormValidityChange(isValid, nextErrorData !== undefined ? nextErrorData : validErrorData),
|
|
331
|
-
});
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
178
|
const runLiquid = liquidEnabled && !isFullMode;
|
|
336
179
|
const getLiquidTags = currentProps.actions?.getLiquidTags;
|
|
337
180
|
if (!runLiquid || typeof getLiquidTags !== 'function') {
|
|
@@ -348,7 +191,8 @@ const FormBuilderShell = (props) => {
|
|
|
348
191
|
onError: ({ standardErrors, liquidErrors }) => {
|
|
349
192
|
pushFooter({ STANDARD_ERROR_MSG: standardErrors, LIQUID_ERROR_MSG: liquidErrors });
|
|
350
193
|
if (currentProps.stopValidation) currentProps.stopValidation();
|
|
351
|
-
// Classic passes the clean sync errorData: the footer carries the
|
|
194
|
+
// Classic passes the (clean) sync errorData here: the footer carries the
|
|
195
|
+
// liquid error, not a per-field error.
|
|
352
196
|
if (currentProps.onFormValidityChange) currentProps.onFormValidityChange(false, validErrorData);
|
|
353
197
|
},
|
|
354
198
|
onSuccess: () => {
|
|
@@ -357,50 +201,41 @@ const FormBuilderShell = (props) => {
|
|
|
357
201
|
});
|
|
358
202
|
};
|
|
359
203
|
|
|
360
|
-
// ---- startValidation rising edge -> validate, then save
|
|
204
|
+
// ---- startValidation rising edge -> validate, then save or stop ----
|
|
361
205
|
useEffect(() => {
|
|
362
206
|
if (startValidation && !prevStartValidationRef.current) {
|
|
363
207
|
setInternalCheckValidation(true); // reveal field error messages from now on
|
|
364
208
|
debouncedEmit.flush();
|
|
365
209
|
const legacy = toLegacy(stateRef.current);
|
|
366
|
-
const result = runValidate(legacy
|
|
367
|
-
|
|
368
|
-
if (result.
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
if (propsRef.current.setModalContent) propsRef.current.setModalContent(result.singlePlatformPrompt);
|
|
373
|
-
} else {
|
|
374
|
-
emitValidity(result, legacy);
|
|
375
|
-
if (result.isValid) {
|
|
376
|
-
submitValidForm(legacy, result.errorData);
|
|
377
|
-
} else if (propsRef.current.stopValidation) {
|
|
378
|
-
propsRef.current.stopValidation();
|
|
379
|
-
}
|
|
210
|
+
const result = runValidate(legacy);
|
|
211
|
+
emitValidity(result, legacy);
|
|
212
|
+
if (result.isValid) {
|
|
213
|
+
submitValidForm(legacy, result.errorData);
|
|
214
|
+
} else if (propsRef.current.stopValidation) {
|
|
215
|
+
propsRef.current.stopValidation();
|
|
380
216
|
}
|
|
381
217
|
}
|
|
382
218
|
prevStartValidationRef.current = startValidation;
|
|
383
219
|
}, [startValidation]);
|
|
384
220
|
|
|
385
|
-
// Inline message map
|
|
386
|
-
//
|
|
221
|
+
// Inline (below-the-box) message map. The only change vs the old flow: in full mode
|
|
222
|
+
// the brace error shows inline instead of the footer. Library mode and all other
|
|
223
|
+
// body errors keep the old placement.
|
|
387
224
|
const resolvedFieldErrors = useMemo(() => {
|
|
388
225
|
if (!isFullMode) return {}; // library mode: unchanged old flow (footer only)
|
|
389
226
|
const editorId = adapter.config?.fieldIds?.editor;
|
|
390
|
-
const editorError = editorId && errorData?.[
|
|
227
|
+
const editorError = editorId && errorData?.[ACTIVE_TAB_INDEX]?.[editorId];
|
|
391
228
|
if (!editorError || !adapter.getEditorErrorDescriptor || !intl) return {};
|
|
392
229
|
const descriptor = adapter.getEditorErrorDescriptor(toLegacy(stateRef.current), { tags, isFullMode, currentModule });
|
|
393
230
|
if (descriptor?.kind !== EDITOR_ERROR_KIND.BRACE) return {};
|
|
394
231
|
return { [editorId]: intl.formatMessage(descriptor.message, descriptor.values) };
|
|
395
|
-
}, [errorData, intl, isFullMode, currentModule
|
|
232
|
+
}, [errorData, intl, isFullMode, currentModule]);
|
|
396
233
|
|
|
397
234
|
// ---- field change ----
|
|
398
235
|
const onFieldChange = (field, value) => {
|
|
399
|
-
const
|
|
400
|
-
const tabIndex = field.standalone ? null : liveActiveTab;
|
|
236
|
+
const tabIndex = field.standalone ? null : ACTIVE_TAB_INDEX;
|
|
401
237
|
const action = fieldChanged({ fieldId: field.id, value, tabIndex });
|
|
402
238
|
const nextState = formReducer(stateRef.current, action);
|
|
403
|
-
lastNextStateRef.current = nextState;
|
|
404
239
|
dispatch(action); // immediate UI update on next render
|
|
405
240
|
const legacy = toLegacy(nextState);
|
|
406
241
|
|
|
@@ -410,22 +245,19 @@ const FormBuilderShell = (props) => {
|
|
|
410
245
|
emitChange(legacy, field);
|
|
411
246
|
}
|
|
412
247
|
|
|
413
|
-
// Re-validate on change while errors are shown, so a revealed error clears
|
|
414
|
-
// soon as the field becomes valid.
|
|
248
|
+
// Re-validate on change while errors are being shown, so a revealed error clears
|
|
249
|
+
// as soon as the field becomes valid. Focus-safe because renderers don't toggle
|
|
250
|
+
// CapInput's antd suffix (see InputField), so the <input> never remounts.
|
|
415
251
|
const validationActive = propsRef.current.startValidation || internalCheckValidation || Boolean(propsRef.current.checkValidation);
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
const validateLive = liquidEnabled && !isFullMode
|
|
419
|
-
&& adapter.config?.features?.liveValidation !== false;
|
|
420
|
-
// MPUSH: Classic re-validates every change via the container round-trip (Classic.js:516-537)
|
|
421
|
-
// — keeps the container's isFormValid current for the library Done gate; display stays gated.
|
|
422
|
-
if (validationActive || validateLive || validateOnExternalChange) {
|
|
252
|
+
const validateLive = liquidEnabled && !isFullMode;
|
|
253
|
+
if (validationActive || validateLive) {
|
|
423
254
|
emitValidity(runValidate(legacy), legacy);
|
|
424
255
|
}
|
|
425
256
|
};
|
|
426
257
|
|
|
427
|
-
//
|
|
428
|
-
//
|
|
258
|
+
// Re-validate the high-frequency inputs (template-name/subject) on blur,
|
|
259
|
+
// unconditionally — mirroring Classic's handleFieldBlur. Flush the debounced
|
|
260
|
+
// onChange first so the parent has the latest typed value before we validate.
|
|
429
261
|
const onFieldBlur = (field) => {
|
|
430
262
|
if (!field || !HIGH_FREQ_FIELDS.includes(field.id)) return;
|
|
431
263
|
debouncedEmit.flush();
|
|
@@ -433,93 +265,32 @@ const FormBuilderShell = (props) => {
|
|
|
433
265
|
emitValidity(runValidate(legacy), legacy);
|
|
434
266
|
};
|
|
435
267
|
|
|
436
|
-
// ---- parent bridge: container-injected handlers
|
|
437
|
-
// onTagSelect (data, tab, field); data-commit events (true, formData, field) MPUSH-only;
|
|
438
|
-
// everything else takes callChildEvent's tail (data, field.id).
|
|
268
|
+
// ---- parent bridge: invoke container-injected handlers (Stage A compat) ----
|
|
439
269
|
const onEvent = (field, eventName, data) => {
|
|
440
270
|
const injected = field.injectedEvents?.[eventName];
|
|
441
271
|
if (typeof injected !== 'function') return;
|
|
442
|
-
const parent = propsRef.current.parent;
|
|
443
|
-
if (eventName === 'discardValues') {
|
|
444
|
-
// Classic self-resets on discard (2617-2625); the container's empty push is
|
|
445
|
-
// ignored by rehydrate, so the local re-init happens here.
|
|
446
|
-
injected.call(parent, data, field.id);
|
|
447
|
-
const fresh = initializeFormState(propsRef.current.schema || {});
|
|
448
|
-
lastEmittedRef.current = null;
|
|
449
|
-
lastSeenParentRef.current = null;
|
|
450
|
-
dispatch(hydrate(fresh));
|
|
451
|
-
setErrorData({});
|
|
452
|
-
setInternalCheckValidation(false);
|
|
453
|
-
const legacy = toLegacy(fresh);
|
|
454
|
-
emitValidity(runValidate(legacy), legacy);
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
272
|
if (eventName === 'onTagSelect') {
|
|
458
|
-
injected.call(parent, data,
|
|
459
|
-
} else if (
|
|
460
|
-
DATA_COMMIT_EVENTS.has(eventName)
|
|
461
|
-
&& DATA_COMMIT_FIELD_TYPES.has(field.type)
|
|
462
|
-
&& adapter.config?.features?.bridgeFieldEvents
|
|
463
|
-
) {
|
|
464
|
-
injected.call(parent, true, toLegacy(lastNextStateRef.current || stateRef.current), field);
|
|
273
|
+
injected.call(propsRef.current.parent, data, 1, field); // Classic signature
|
|
465
274
|
} else {
|
|
466
|
-
injected.call(parent, data, field.id); // saveFormData / onClick
|
|
467
|
-
}
|
|
468
|
-
};
|
|
469
|
-
|
|
470
|
-
// ---- tab switch: replaces Classic's DOM scan (2526-2551) with the same contract —
|
|
471
|
-
// the container's injected onTabChange receives the 1-based index ----
|
|
472
|
-
const onTabSwitch = (container, key) => {
|
|
473
|
-
const tabs = stateRef.current?.tabs || [];
|
|
474
|
-
let index = tabs.findIndex((tab) => `${tab?.tabKey}` === `${key}`);
|
|
475
|
-
if (index === -1) {
|
|
476
|
-
const numeric = Number(key);
|
|
477
|
-
index = Number.isNaN(numeric) ? 0 : numeric;
|
|
478
|
-
}
|
|
479
|
-
dispatch(setActiveTab(index));
|
|
480
|
-
const injected = container?.injectedEvents?.onTabChange;
|
|
481
|
-
if (typeof injected === 'function') injected.call(propsRef.current.parent, index + 1);
|
|
482
|
-
};
|
|
483
|
-
|
|
484
|
-
// ---- confirmation modal (container-owned content, adapter-owned OK semantics) ----
|
|
485
|
-
const handleModalOk = (ev) => {
|
|
486
|
-
// antd v6 wraps button text in a <span>; use currentTarget so the button id resolves.
|
|
487
|
-
const id = (ev?.currentTarget && ev.currentTarget.id) || ev?.target?.id;
|
|
488
|
-
setModalVisible(false);
|
|
489
|
-
if (adapter.modals?.handleOk) {
|
|
490
|
-
adapter.modals.handleOk(id, {
|
|
491
|
-
platform: platformRef.current,
|
|
492
|
-
submitSingleTab: (singleTab) => {
|
|
493
|
-
const legacy = toLegacy(stateRef.current);
|
|
494
|
-
submitValidForm(legacy, errorData, { singleTab });
|
|
495
|
-
},
|
|
496
|
-
});
|
|
275
|
+
injected.call(propsRef.current.parent, data, field.id); // saveFormData / onClick
|
|
497
276
|
}
|
|
498
277
|
};
|
|
499
278
|
|
|
500
|
-
const handleModalCancel = () => {
|
|
501
|
-
setModalVisible(false);
|
|
502
|
-
if (propsRef.current.handleCancelModal) propsRef.current.handleCancelModal();
|
|
503
|
-
};
|
|
504
|
-
|
|
505
279
|
// Point the stable wrappers at the latest handler bodies (read at call time).
|
|
506
280
|
handlersRef.current = { onFieldChange, onFieldBlur, onEvent };
|
|
507
281
|
|
|
508
|
-
// Render context
|
|
282
|
+
// Render context handed to every field renderer (the `renderContext` prop the
|
|
283
|
+
// renderers / Section / FieldSlot consume).
|
|
509
284
|
const renderContext = {
|
|
510
285
|
state,
|
|
511
286
|
errorData,
|
|
512
287
|
registry,
|
|
513
288
|
checkValidation: internalCheckValidation || Boolean(checkValidation),
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
activeTabIndex,
|
|
517
|
-
currentTab,
|
|
518
|
-
tabCount,
|
|
289
|
+
activeTabIndex: ACTIVE_TAB_INDEX,
|
|
290
|
+
currentTab: 1,
|
|
519
291
|
onFieldChange: stableOnFieldChange,
|
|
520
292
|
onFieldBlur: stableOnFieldBlur,
|
|
521
293
|
onEvent: stableOnEvent,
|
|
522
|
-
onTabSwitch,
|
|
523
294
|
resolvedFieldErrors,
|
|
524
295
|
legacyFormData: toLegacy(state),
|
|
525
296
|
channel,
|
|
@@ -537,33 +308,15 @@ const FormBuilderShell = (props) => {
|
|
|
537
308
|
refs,
|
|
538
309
|
};
|
|
539
310
|
|
|
540
|
-
//
|
|
311
|
+
// Loading overlay — mirrors Classic: spin while liquid-tag extraction is in
|
|
312
|
+
// progress (only for liquid-supported channels) or while the layout metadata
|
|
313
|
+
// is still being fetched.
|
|
541
314
|
const spinning = Boolean((liquidEnabled && liquidExtractionInProgress) || metaDataStatus === REQUEST);
|
|
542
315
|
const spinTip = intl?.formatMessage ? intl.formatMessage(formMessages.liquidSpinText) : '';
|
|
543
316
|
|
|
544
|
-
// Classic's 'confirm' modal (597-641), MOBILEPUSH only; the primary button's id is the
|
|
545
|
-
// handleOk dispatch key. width 520 matches Classic's antd default (CapModal's 324 wraps).
|
|
546
|
-
const confirmModal = adapter.modals && modal && (
|
|
547
|
-
<CapModal
|
|
548
|
-
open={modalVisible}
|
|
549
|
-
width={520}
|
|
550
|
-
title={modal.title || ''}
|
|
551
|
-
onCancel={handleModalCancel}
|
|
552
|
-
footer={[
|
|
553
|
-
<CapButton key="back" onClick={handleModalCancel}>{intl.formatMessage(formMessages.cancel)}</CapButton>,
|
|
554
|
-
<CapButton key="submit" type="primary" id={modal.id} onClick={handleModalOk}>
|
|
555
|
-
{intl.formatMessage(formMessages.yes)}
|
|
556
|
-
</CapButton>,
|
|
557
|
-
]}
|
|
558
|
-
>
|
|
559
|
-
{modal.body || ''}
|
|
560
|
-
</CapModal>
|
|
561
|
-
);
|
|
562
|
-
|
|
563
317
|
return (
|
|
564
318
|
<CapSpin spinning={spinning} tip={spinTip}>
|
|
565
319
|
<SchemaForm schema={schema} renderContext={renderContext} />
|
|
566
|
-
{confirmModal}
|
|
567
320
|
</CapSpin>
|
|
568
321
|
);
|
|
569
322
|
};
|
|
@@ -587,7 +340,6 @@ FormBuilderShell.propTypes = {
|
|
|
587
340
|
tagModule: PropTypes.string,
|
|
588
341
|
tags: PropTypes.array,
|
|
589
342
|
checkValidation: PropTypes.bool,
|
|
590
|
-
isEdit: PropTypes.bool,
|
|
591
343
|
injectedTags: PropTypes.object,
|
|
592
344
|
onContextChange: PropTypes.func,
|
|
593
345
|
// Shapes match TagList's own contract (array of offer objects / keyed-by-blockId map).
|
|
@@ -596,10 +348,6 @@ FormBuilderShell.propTypes = {
|
|
|
596
348
|
waitEventContextTags: PropTypes.object,
|
|
597
349
|
restrictPersonalization: PropTypes.bool,
|
|
598
350
|
refs: PropTypes.object,
|
|
599
|
-
// Tabbed channels (MOBILEPUSH): container-pushed tab position + confirmation modal.
|
|
600
|
-
currentTab: PropTypes.number,
|
|
601
|
-
showModal: PropTypes.bool,
|
|
602
|
-
modal: PropTypes.object,
|
|
603
351
|
};
|
|
604
352
|
|
|
605
353
|
FormBuilderShell.defaultProps = {
|
|
@@ -613,9 +361,6 @@ FormBuilderShell.defaultProps = {
|
|
|
613
361
|
waitEventContextTags: {},
|
|
614
362
|
restrictPersonalization: false,
|
|
615
363
|
refs: undefined,
|
|
616
|
-
currentTab: undefined,
|
|
617
|
-
showModal: false,
|
|
618
|
-
modal: null,
|
|
619
364
|
};
|
|
620
365
|
|
|
621
366
|
export default FormBuilderShell;
|