@capillarytech/creatives-library 9.0.56-alpha.7 → 9.0.56-betaa.0
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 +4 -2
- package/package.json +1 -1
- package/services/api.js +0 -20
- package/utils/common.js +12 -0
- package/utils/templateVarUtils.js +0 -16
- package/utils/tests/templateVarUtils.test.js +0 -44
- package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +0 -13
- package/v2Components/CommonTestAndPreview/actions.js +0 -13
- package/v2Components/CommonTestAndPreview/constants.js +0 -8
- package/v2Components/CommonTestAndPreview/index.js +3 -55
- package/v2Components/CommonTestAndPreview/reducer.js +0 -20
- package/v2Components/CommonTestAndPreview/sagas.js +0 -28
- package/v2Components/CommonTestAndPreview/selectors.js +0 -6
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +0 -106
- package/v2Components/CommonTestAndPreview/tests/actions.test.js +0 -15
- package/v2Components/CommonTestAndPreview/tests/constants.test.js +1 -9
- package/v2Components/CommonTestAndPreview/tests/index.test.js +0 -101
- package/v2Components/CommonTestAndPreview/tests/reducer.test.js +0 -41
- package/v2Components/CommonTestAndPreview/tests/sagas.test.js +0 -84
- package/v2Components/CommonTestAndPreview/tests/selectors.test.js +0 -12
- package/v2Components/FormBuilder/Functional/FormBuilderShell.js +320 -46
- package/v2Components/FormBuilder/Functional/channels/mobilepush/buildSubmitPayload.js +10 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/config.js +82 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/getEditorErrorDescriptor.js +74 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/index.js +28 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/modals.js +19 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/runSubmitPipeline.js +45 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/getEditorErrorDescriptor.test.js +162 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/modals.test.js +37 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/runSubmitPipeline.test.js +70 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/validate.test.js +274 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/validate.js +251 -0
- package/v2Components/FormBuilder/Functional/channels/registry.js +2 -0
- package/v2Components/FormBuilder/Functional/constants.js +43 -8
- package/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js +175 -28
- package/v2Components/FormBuilder/Functional/core/store/formReducer.js +75 -6
- package/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js +10 -20
- package/v2Components/FormBuilder/Functional/layout/FieldSlot.js +9 -1
- package/v2Components/FormBuilder/Functional/layout/SchemaForm.js +20 -3
- package/v2Components/FormBuilder/Functional/layout/Section.js +6 -1
- package/v2Components/FormBuilder/Functional/layout/TabsContainer.js +89 -0
- package/v2Components/FormBuilder/Functional/renderers/mpushRenderers.js +440 -0
- package/v2Components/FormBuilder/Functional/tests/fieldSlot.test.js +9 -2
- package/v2Components/FormBuilder/Functional/tests/mpush.crossFlowParity.test.js +430 -0
- package/v2Components/FormBuilder/Functional/tests/mpush.ctaFlows.parity.test.js +282 -0
- package/v2Components/FormBuilder/Functional/tests/mpush.editContainer.parity.test.js +141 -0
- package/v2Components/FormBuilder/Functional/tests/mpush.engine.test.js +306 -0
- package/v2Components/FormBuilder/Functional/tests/mpush.shellEvents.test.js +151 -0
- package/v2Components/FormBuilder/Functional/tests/mpushRenderers.test.js +342 -0
- package/v2Components/FormBuilder/Functional/tests/schemaForm.test.js +17 -0
- package/v2Components/FormBuilder/Functional/tests/tabsContainer.test.js +110 -0
- package/v2Components/FormBuilder/index.js +34 -12
- package/v2Components/FormBuilder/tests/entryGate.test.js +94 -7
- package/v2Components/FormBuilder/tests/mpush.characterization.test.js +459 -0
- package/v2Components/TestAndPreviewSlidebox/index.js +0 -9
- package/v2Containers/CommunicationFlow/CommunicationFlow.js +75 -140
- package/v2Containers/CommunicationFlow/CommunicationFlow.scss +2 -18
- package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +27 -58
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +51 -503
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlowCard.test.js +4 -40
- package/v2Containers/CommunicationFlow/constants.js +4 -65
- package/v2Containers/CommunicationFlow/index.js +23 -16
- package/v2Containers/CommunicationFlow/messages.js +3 -11
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +11 -24
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +0 -4
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +4 -176
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +0 -3
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +0 -6
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +0 -25
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +4 -11
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +5 -19
- package/v2Containers/CreativesContainer/index.js +3 -2
- package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +5 -5
- package/v2Containers/CreativesContainer/tests/index.test.js +0 -12
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +19 -115
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +3 -27
- package/v2Containers/WebPush/Create/preview/WebPushPreview.js +12 -17
- package/v2Containers/WebPush/Create/preview/tests/WebPushPreview.test.js +0 -5
- package/v2Containers/Whatsapp/index.js +3 -6
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +34 -306
- package/v2Containers/mockdata.js +0 -25
- package/v2Containers/CommunicationFlow/Tests/constants.test.js +0 -85
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* FormBuilderShell — the functional FormBuilder (Phase 1: SMS
|
|
3
|
-
* useReducer, bridges to the legacy formData contract via the
|
|
4
|
-
* through the channel adapter. Stage A keeps the
|
|
2
|
+
* FormBuilderShell — the functional FormBuilder (Phase 1: SMS; Phase 2: MOBILEPUSH).
|
|
3
|
+
* Owns form state in a useReducer, bridges to the legacy formData contract via the
|
|
4
|
+
* codec, and validates/submits through the channel adapter. Stage A keeps the
|
|
5
|
+
* channel containers unchanged.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import React, {
|
|
@@ -10,14 +11,19 @@ import React, {
|
|
|
10
11
|
import PropTypes from 'prop-types';
|
|
11
12
|
import debounce from 'lodash/debounce';
|
|
12
13
|
import isEqual from 'lodash/isEqual';
|
|
14
|
+
import cloneDeep from 'lodash/cloneDeep';
|
|
13
15
|
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';
|
|
14
18
|
import formReducer, {
|
|
15
19
|
createInitialState,
|
|
16
20
|
hydrate,
|
|
17
21
|
fieldChanged,
|
|
22
|
+
setActiveTab,
|
|
23
|
+
reconcileSchema,
|
|
18
24
|
} from './core/store/formReducer';
|
|
19
25
|
import { toLegacy, fromLegacy } from './core/store/toLegacyFormData';
|
|
20
|
-
import initializeFormState from './core/schema/initializeFormState';
|
|
26
|
+
import initializeFormState, { reconcileFormState } from './core/schema/initializeFormState';
|
|
21
27
|
import { getChannelAdapter } from './channels/registry';
|
|
22
28
|
import { createSmsRegistry } from './renderers/smsRenderers';
|
|
23
29
|
import { validateLiquidTemplateContent } from '../../../utils/commonUtils';
|
|
@@ -26,7 +32,9 @@ import { SMS } from '../../../v2Containers/CreativesContainer/constants';
|
|
|
26
32
|
import { DEFAULT as DEFAULT_MODULE, EMBEDDED } from '../../../constants/unified';
|
|
27
33
|
import formMessages from '../messages';
|
|
28
34
|
import SchemaForm from './layout/SchemaForm';
|
|
29
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
ACTIVE_TAB_INDEX, DATA_COMMIT_EVENTS, DATA_COMMIT_FIELD_TYPES, EDITOR_ERROR_KIND, HIGH_FREQ_FIELDS,
|
|
37
|
+
} from './constants';
|
|
30
38
|
|
|
31
39
|
const isNonEmpty = (obj) => obj && Object.keys(obj).length > 0;
|
|
32
40
|
|
|
@@ -34,9 +42,6 @@ const buildInitialState = (schema, parentFormData, channel) => (isNonEmpty(paren
|
|
|
34
42
|
? fromLegacy(parentFormData, { channel })
|
|
35
43
|
: initializeFormState(schema || {}));
|
|
36
44
|
|
|
37
|
-
// Phase 1: only SMS has renderers. Later channels return their own registry.
|
|
38
|
-
const registryFor = () => createSmsRegistry();
|
|
39
|
-
|
|
40
45
|
const FormBuilderShell = (props) => {
|
|
41
46
|
const {
|
|
42
47
|
schema,
|
|
@@ -58,11 +63,19 @@ const FormBuilderShell = (props) => {
|
|
|
58
63
|
waitEventContextTags,
|
|
59
64
|
restrictPersonalization,
|
|
60
65
|
refs,
|
|
66
|
+
currentTab: currentTabProp,
|
|
67
|
+
showModal,
|
|
68
|
+
modal,
|
|
61
69
|
} = props;
|
|
62
70
|
|
|
63
71
|
const channel = (channelProp || schema?.channel || SMS).toUpperCase();
|
|
64
72
|
const adapter = getChannelAdapter(channel);
|
|
65
|
-
|
|
73
|
+
// Channels ship their own registry via the adapter; SMS predates the hook and
|
|
74
|
+
// stays on its original factory.
|
|
75
|
+
const registry = useMemo(
|
|
76
|
+
() => (adapter?.createRegistry ? adapter.createRegistry() : createSmsRegistry()),
|
|
77
|
+
[channel],
|
|
78
|
+
);
|
|
66
79
|
|
|
67
80
|
const [state, dispatch] = useReducer(formReducer, channel, createInitialState);
|
|
68
81
|
const [errorData, setErrorData] = useState({});
|
|
@@ -71,6 +84,10 @@ const FormBuilderShell = (props) => {
|
|
|
71
84
|
// is still computed + emitted via onFormValidityChange on mount; only the inline
|
|
72
85
|
// display is gated. Can also be driven by the container's checkValidation prop.
|
|
73
86
|
const [internalCheckValidation, setInternalCheckValidation] = useState(false);
|
|
87
|
+
// Container-driven confirmation modal (MOBILEPUSH single-platform save). The
|
|
88
|
+
// container owns the content (setModalContent -> showModal/modal props); the
|
|
89
|
+
// shell owns visibility after a click, mirroring Classic's local showModal state.
|
|
90
|
+
const [modalVisible, setModalVisible] = useState(false);
|
|
74
91
|
|
|
75
92
|
// refs to latest values for use inside stable/debounced callbacks
|
|
76
93
|
const stateRef = useRef(state);
|
|
@@ -80,6 +97,25 @@ const FormBuilderShell = (props) => {
|
|
|
80
97
|
const lastEmittedRef = useRef(null);
|
|
81
98
|
const didInitRef = useRef(false);
|
|
82
99
|
const prevStartValidationRef = useRef(startValidation);
|
|
100
|
+
const prevSchemaRef = useRef(null);
|
|
101
|
+
// Deep snapshot of the last parent formData we processed. The MOBILEPUSH
|
|
102
|
+
// containers mutate their formData IN PLACE (copy-content, tag insertion), so a
|
|
103
|
+
// reference-keyed effect would miss genuine pushes — we deep-compare on every
|
|
104
|
+
// render instead, exactly like Classic's componentWillReceiveProps.
|
|
105
|
+
const lastSeenParentRef = useRef(null);
|
|
106
|
+
// Platform validity flags from the last save-validation (MOBILEPUSH android/ios
|
|
107
|
+
// modal contract): Classic keeps these in state only after a validate pass.
|
|
108
|
+
const platformRef = useRef(null);
|
|
109
|
+
// Content snapshot of the tags prop for the tags-change re-validation
|
|
110
|
+
// (Classic.js:569-578) — containers rebuild the array each render, so identity
|
|
111
|
+
// is useless. Seeded by the init effect (init already validates with those tags).
|
|
112
|
+
const lastTagsRef = useRef(null);
|
|
113
|
+
const lastNextStateRef = useRef(null);
|
|
114
|
+
// GOLDEN parity (pinned by mpush.characterization): Classic emits
|
|
115
|
+
// this.state.tabCount in onChange, which starts at 1 and is mirrored from the
|
|
116
|
+
// tabCount PROP only when hydrating from a parent formData push (edit sync) —
|
|
117
|
+
// so the MPUSH create flow emits tabCount 1 even though the containers pass 2.
|
|
118
|
+
const emittedTabCountRef = useRef(1);
|
|
83
119
|
|
|
84
120
|
// Stable handler identities for the memoized FieldSlot: these wrappers delegate to
|
|
85
121
|
// the latest handler bodies (via a ref), so React.memo can bail out on sibling changes.
|
|
@@ -91,11 +127,20 @@ const FormBuilderShell = (props) => {
|
|
|
91
127
|
const isEmbedded = location?.query?.type === EMBEDDED;
|
|
92
128
|
const currentModule = tagModule || location?.query?.module || DEFAULT_MODULE;
|
|
93
129
|
|
|
94
|
-
const
|
|
130
|
+
const activeTabIndex = state?.meta?.activeTabIndex ?? ACTIVE_TAB_INDEX;
|
|
131
|
+
const currentTab = activeTabIndex + 1; // legacy 1-based (MOBILEPUSH: 1=Android, 2=iOS)
|
|
132
|
+
const tabCount = state?.tabs?.length || 1;
|
|
133
|
+
const hasTabs = Boolean(adapter?.config?.features?.tabs);
|
|
134
|
+
|
|
135
|
+
const runValidate = (legacy, extra = {}) => adapter.validate(legacy, {
|
|
95
136
|
tags: propsRef.current.tags,
|
|
96
137
|
isFullMode,
|
|
97
138
|
isEmbedded,
|
|
98
139
|
currentModule,
|
|
140
|
+
restrictPersonalization: propsRef.current.restrictPersonalization,
|
|
141
|
+
currentTab: (stateRef.current?.meta?.activeTabIndex ?? ACTIVE_TAB_INDEX) + 1,
|
|
142
|
+
schema: propsRef.current.schema,
|
|
143
|
+
...extra,
|
|
99
144
|
});
|
|
100
145
|
|
|
101
146
|
// Footer state (mirrors Classic's `liquidErrorMessage`). Holds the last
|
|
@@ -103,45 +148,66 @@ const FormBuilderShell = (props) => {
|
|
|
103
148
|
// clobber each other before forwarding to the container's showLiquidErrorInFooter.
|
|
104
149
|
const footerRef = useRef({ STANDARD_ERROR_MSG: [], LIQUID_ERROR_MSG: [] });
|
|
105
150
|
const liquidEnabled = Boolean(adapter.config?.features?.liquid);
|
|
151
|
+
// Classic passes null as the footer's tab argument for SMS and the live 1-based
|
|
152
|
+
// currentTab for every other channel (Classic.js:1352, 1487).
|
|
153
|
+
const footerTabArg = () => (adapter.config?.footerTabArg === 'currentTab'
|
|
154
|
+
? (stateRef.current?.meta?.activeTabIndex ?? ACTIVE_TAB_INDEX) + 1
|
|
155
|
+
: null);
|
|
106
156
|
|
|
107
157
|
const pushFooter = (next) => {
|
|
108
158
|
footerRef.current = { ...footerRef.current, ...next };
|
|
109
159
|
if (propsRef.current.showLiquidErrorInFooter) {
|
|
110
|
-
propsRef.current.showLiquidErrorInFooter(
|
|
111
|
-
footerRef.current,
|
|
112
|
-
channel === SMS ? null : propsRef.current.currentTab,
|
|
113
|
-
);
|
|
160
|
+
propsRef.current.showLiquidErrorInFooter(footerRef.current, footerTabArg());
|
|
114
161
|
}
|
|
115
162
|
};
|
|
116
163
|
|
|
117
|
-
// Footer-eligible body message
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
|
|
164
|
+
// Footer-eligible body message for `legacy`, or [] when none. Full mode shows
|
|
165
|
+
// errors inline instead, so the footer is suppressed there; library mode keeps
|
|
166
|
+
// the old flow (message goes to the footer). The extended ctx (errorData,
|
|
167
|
+
// currentTab, schema, checkValidation) feeds the MPUSH footer echo — the SMS
|
|
168
|
+
// descriptor derives everything from `legacy` and ignores the extras.
|
|
169
|
+
const standardFooterMsg = (legacy, result) => {
|
|
121
170
|
if (isFullMode || !adapter.getEditorErrorDescriptor || !intl) return [];
|
|
122
171
|
const descriptor = adapter.getEditorErrorDescriptor(legacy, {
|
|
123
172
|
tags: propsRef.current.tags,
|
|
124
173
|
isFullMode,
|
|
125
174
|
currentModule,
|
|
175
|
+
errorData: result?.errorData,
|
|
176
|
+
currentTab: (stateRef.current?.meta?.activeTabIndex ?? ACTIVE_TAB_INDEX) + 1,
|
|
177
|
+
schema: propsRef.current.schema,
|
|
178
|
+
checkValidation: internalCheckValidation
|
|
179
|
+
|| Boolean(propsRef.current.checkValidation)
|
|
180
|
+
|| Boolean(propsRef.current.startValidation),
|
|
181
|
+
restrictPersonalization: propsRef.current.restrictPersonalization,
|
|
126
182
|
});
|
|
127
|
-
|
|
183
|
+
if (!descriptor) return [];
|
|
184
|
+
if (descriptor.text) return [descriptor.text];
|
|
185
|
+
return [intl.formatMessage(descriptor.message, descriptor.values)];
|
|
128
186
|
};
|
|
129
187
|
|
|
130
188
|
const emitValidity = (result, legacy) => {
|
|
131
189
|
setErrorData(result.errorData);
|
|
190
|
+
if (result.platform) platformRef.current = result.platform;
|
|
132
191
|
if (propsRef.current.onFormValidityChange) {
|
|
133
192
|
propsRef.current.onFormValidityChange(result.isValid, result.errorData);
|
|
134
193
|
}
|
|
135
194
|
// Footer: clear standard errors when the form is valid; otherwise surface the
|
|
136
|
-
//
|
|
195
|
+
// body-error message (empty/generic gated per channel). Liquid errors are preserved.
|
|
137
196
|
if (liquidEnabled) {
|
|
138
|
-
pushFooter({ STANDARD_ERROR_MSG: result.isValid ? [] : standardFooterMsg(legacy) });
|
|
197
|
+
pushFooter({ STANDARD_ERROR_MSG: result.isValid ? [] : standardFooterMsg(legacy, result) });
|
|
139
198
|
}
|
|
140
199
|
};
|
|
141
200
|
|
|
142
201
|
const emitChange = (legacy, field) => {
|
|
143
202
|
lastEmittedRef.current = legacy;
|
|
144
|
-
if (propsRef.current.onChange)
|
|
203
|
+
if (propsRef.current.onChange) {
|
|
204
|
+
propsRef.current.onChange(
|
|
205
|
+
legacy,
|
|
206
|
+
emittedTabCountRef.current,
|
|
207
|
+
(stateRef.current?.meta?.activeTabIndex ?? ACTIVE_TAB_INDEX) + 1,
|
|
208
|
+
field,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
145
211
|
};
|
|
146
212
|
|
|
147
213
|
const debouncedEmit = useMemo(
|
|
@@ -151,30 +217,130 @@ const FormBuilderShell = (props) => {
|
|
|
151
217
|
useEffect(() => () => debouncedEmit.cancel(), [debouncedEmit]);
|
|
152
218
|
|
|
153
219
|
// ---- initialize once the (possibly async) schema is ready, then validate+emit ----
|
|
154
|
-
const schemaReady = Boolean(schema?.standalone);
|
|
220
|
+
const schemaReady = Boolean(schema?.standalone || schema?.containers?.length);
|
|
155
221
|
useEffect(() => {
|
|
156
222
|
if (schemaReady && !didInitRef.current) {
|
|
157
223
|
didInitRef.current = true;
|
|
224
|
+
prevSchemaRef.current = schema;
|
|
158
225
|
const init = buildInitialState(schema, propsRef.current.formData, channel);
|
|
226
|
+
if (isNonEmpty(propsRef.current.formData)) {
|
|
227
|
+
lastSeenParentRef.current = cloneDeep(propsRef.current.formData);
|
|
228
|
+
// Classic mirrors the tabCount prop only alongside a parent formData sync.
|
|
229
|
+
emittedTabCountRef.current = propsRef.current.tabCount || init.tabs?.length || 1;
|
|
230
|
+
}
|
|
159
231
|
dispatch(hydrate(init));
|
|
232
|
+
lastTagsRef.current = cloneDeep(propsRef.current.tags || null);
|
|
160
233
|
const legacy = toLegacy(init);
|
|
161
234
|
emitValidity(runValidate(legacy), legacy);
|
|
162
235
|
}
|
|
163
236
|
}, [schemaReady]);
|
|
164
237
|
|
|
165
|
-
//
|
|
238
|
+
// Classic's validateForm tail always emits onFormValidityChange (Classic.js:1361)
|
|
239
|
+
// and runs on every schema change (542-559), deep-unequal parent push (516-537)
|
|
240
|
+
// and tags change (569-578) — display stays gated. The MPUSH containers gate
|
|
241
|
+
// their save flows on that maintained validity (Create/index.js:101), so these
|
|
242
|
+
// background emissions are load-bearing. SMS keeps its shipped behavior.
|
|
243
|
+
const validateOnExternalChange = Boolean(adapter.config?.features?.validateOnExternalChange);
|
|
244
|
+
|
|
245
|
+
// Background emissions must read the POST-dispatch state (a stateRef snapshot can
|
|
246
|
+
// be stale mid container-setState-cascade): hydration-family effects only ARM this
|
|
247
|
+
// flag; the [state]-keyed effect below emits once the new state has rendered.
|
|
248
|
+
const pendingBackgroundValidateRef = useRef(false);
|
|
249
|
+
|
|
250
|
+
// ---- reconcile on a genuine schema change (CTA splices, text<->image swap) ----
|
|
251
|
+
// Runs inside the reducer (reconcileSchema) — see the action's note on stale snapshots.
|
|
252
|
+
useEffect(() => {
|
|
253
|
+
if (!didInitRef.current || !schema || prevSchemaRef.current === schema) return;
|
|
254
|
+
prevSchemaRef.current = schema;
|
|
255
|
+
dispatch(reconcileSchema(schema, reconcileFormState));
|
|
256
|
+
// Classic re-validates on EVERY schema change (Classic.js:542-559).
|
|
257
|
+
const validationActive = propsRef.current.startValidation || internalCheckValidation || Boolean(propsRef.current.checkValidation);
|
|
258
|
+
if (validationActive || validateOnExternalChange) {
|
|
259
|
+
pendingBackgroundValidateRef.current = true;
|
|
260
|
+
}
|
|
261
|
+
}, [schema]);
|
|
262
|
+
|
|
263
|
+
// ---- re-hydrate on a GENUINE external parent push (edit load / discard / copy) ----
|
|
264
|
+
// No dependency array on purpose: the parent may mutate the same formData object
|
|
265
|
+
// in place (MOBILEPUSH copy-content, tag insertion), which a [parentFormData]-keyed
|
|
266
|
+
// effect would never see. The deep-compare guard keeps this a no-op on quiet renders.
|
|
166
267
|
useEffect(() => {
|
|
167
268
|
if (!didInitRef.current || !isNonEmpty(parentFormData)) return;
|
|
269
|
+
if (isEqual(parentFormData, lastSeenParentRef.current)) return; // nothing new from the parent
|
|
270
|
+
lastSeenParentRef.current = cloneDeep(parentFormData);
|
|
168
271
|
const current = toLegacy(stateRef.current);
|
|
169
272
|
const isEcho = isEqual(parentFormData, lastEmittedRef.current) || isEqual(parentFormData, current);
|
|
170
|
-
if (!isEcho)
|
|
171
|
-
|
|
273
|
+
if (!isEcho) {
|
|
274
|
+
dispatch(hydrate(fromLegacy(parentFormData, { channel })));
|
|
275
|
+
// Classic mirrors the tabCount prop only alongside a parent formData sync.
|
|
276
|
+
emittedTabCountRef.current = propsRef.current.tabCount || emittedTabCountRef.current;
|
|
277
|
+
// Classic validates on every deep-unequal parent push (Classic.js:516-537) —
|
|
278
|
+
// this is what turns the container's isFormValid true before Create/Done.
|
|
279
|
+
if (validateOnExternalChange) {
|
|
280
|
+
pendingBackgroundValidateRef.current = true;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// ---- background validity emission from the SETTLED state (see flag above) ----
|
|
286
|
+
useEffect(() => {
|
|
287
|
+
if (!didInitRef.current || !pendingBackgroundValidateRef.current) return;
|
|
288
|
+
pendingBackgroundValidateRef.current = false;
|
|
289
|
+
const legacy = toLegacy(state);
|
|
290
|
+
emitValidity(runValidate(legacy), legacy);
|
|
291
|
+
}, [state]);
|
|
292
|
+
|
|
293
|
+
// ---- re-validate when the tag catalog changes (Classic.js:569-578) ----
|
|
294
|
+
// The containers rebuild the tags array every render (Create/index.js:1928), so
|
|
295
|
+
// compare by content, not identity. Emits validity only; display stays gated.
|
|
296
|
+
useEffect(() => {
|
|
297
|
+
if (!didInitRef.current || !validateOnExternalChange) return;
|
|
298
|
+
const nextTags = propsRef.current.tags;
|
|
299
|
+
if (!Array.isArray(nextTags) || !nextTags.length) return;
|
|
300
|
+
if (isEqual(nextTags, lastTagsRef.current)) return;
|
|
301
|
+
lastTagsRef.current = cloneDeep(nextTags);
|
|
302
|
+
const legacy = toLegacy(stateRef.current);
|
|
303
|
+
emitValidity(runValidate(legacy), legacy);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// ---- container-driven tab position (MOBILEPUSH: Create forces iOS when Android unsupported) ----
|
|
307
|
+
useEffect(() => {
|
|
308
|
+
if (!hasTabs || typeof currentTabProp !== 'number') return;
|
|
309
|
+
dispatch(setActiveTab(currentTabProp - 1));
|
|
310
|
+
}, [currentTabProp, hasTabs]);
|
|
311
|
+
|
|
312
|
+
// ---- container-driven modal visibility (mirrors Classic's props->state modal sync) ----
|
|
313
|
+
useEffect(() => {
|
|
314
|
+
setModalVisible(Boolean(showModal));
|
|
315
|
+
}, [showModal, modal]);
|
|
172
316
|
|
|
173
317
|
// Submit a form that passed sync validation (mirrors Classic onSubmitWrapper).
|
|
174
|
-
//
|
|
318
|
+
// Channels with their own pipeline (MOBILEPUSH: validateMobilePushContent) run it
|
|
319
|
+
// through adapter.runSubmitPipeline; otherwise the default liquid flow applies:
|
|
320
|
+
// full mode submits directly; outside it, liquid-supported channels run liquid-tag
|
|
175
321
|
// validation first — a clean result submits, an error goes to the footer and blocks.
|
|
176
|
-
const submitValidForm = (legacy, validErrorData) => {
|
|
322
|
+
const submitValidForm = (legacy, validErrorData, { singleTab = null } = {}) => {
|
|
177
323
|
const currentProps = propsRef.current;
|
|
324
|
+
|
|
325
|
+
if (typeof adapter.runSubmitPipeline === 'function') {
|
|
326
|
+
adapter.runSubmitPipeline(legacy, {
|
|
327
|
+
isFullMode,
|
|
328
|
+
singleTab,
|
|
329
|
+
currentTab: (stateRef.current?.meta?.activeTabIndex ?? ACTIVE_TAB_INDEX) + 1,
|
|
330
|
+
baseLanguage: currentProps.baseLanguage,
|
|
331
|
+
getLiquidTags: currentProps.actions?.getLiquidTags,
|
|
332
|
+
formatMessage: currentProps.intl?.formatMessage,
|
|
333
|
+
messages: formMessages,
|
|
334
|
+
}, {
|
|
335
|
+
onSubmit: (payload) => currentProps.onSubmit && currentProps.onSubmit(payload),
|
|
336
|
+
pushFooter,
|
|
337
|
+
stopValidation: () => currentProps.stopValidation && currentProps.stopValidation(),
|
|
338
|
+
onFormValidityChange: (isValid, nextErrorData) => currentProps.onFormValidityChange
|
|
339
|
+
&& currentProps.onFormValidityChange(isValid, nextErrorData !== undefined ? nextErrorData : validErrorData),
|
|
340
|
+
});
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
178
344
|
const runLiquid = liquidEnabled && !isFullMode;
|
|
179
345
|
const getLiquidTags = currentProps.actions?.getLiquidTags;
|
|
180
346
|
if (!runLiquid || typeof getLiquidTags !== 'function') {
|
|
@@ -201,18 +367,28 @@ const FormBuilderShell = (props) => {
|
|
|
201
367
|
});
|
|
202
368
|
};
|
|
203
369
|
|
|
204
|
-
// ---- startValidation rising edge -> validate, then save
|
|
370
|
+
// ---- startValidation rising edge -> validate, then save / prompt / stop ----
|
|
205
371
|
useEffect(() => {
|
|
206
372
|
if (startValidation && !prevStartValidationRef.current) {
|
|
207
373
|
setInternalCheckValidation(true); // reveal field error messages from now on
|
|
208
374
|
debouncedEmit.flush();
|
|
209
375
|
const legacy = toLegacy(stateRef.current);
|
|
210
|
-
const result = runValidate(legacy);
|
|
211
|
-
|
|
212
|
-
if (result.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
376
|
+
const result = runValidate(legacy, { isSave: true });
|
|
377
|
+
if (result.platform) platformRef.current = result.platform;
|
|
378
|
+
if (result.singlePlatformPrompt) {
|
|
379
|
+
// Classic parity (Classic.js:1180-1204): the single-platform confirmation
|
|
380
|
+
// takes over — errorData is stored, the container builds the modal, and
|
|
381
|
+
// NOTHING else runs: no validity emission, no footer push, no
|
|
382
|
+
// stopValidation, no submit. The modal is the only continuation.
|
|
383
|
+
setErrorData(result.errorData);
|
|
384
|
+
if (propsRef.current.setModalContent) propsRef.current.setModalContent(result.singlePlatformPrompt);
|
|
385
|
+
} else {
|
|
386
|
+
emitValidity(result, legacy);
|
|
387
|
+
if (result.isValid) {
|
|
388
|
+
submitValidForm(legacy, result.errorData);
|
|
389
|
+
} else if (propsRef.current.stopValidation) {
|
|
390
|
+
propsRef.current.stopValidation();
|
|
391
|
+
}
|
|
216
392
|
}
|
|
217
393
|
}
|
|
218
394
|
prevStartValidationRef.current = startValidation;
|
|
@@ -224,18 +400,20 @@ const FormBuilderShell = (props) => {
|
|
|
224
400
|
const resolvedFieldErrors = useMemo(() => {
|
|
225
401
|
if (!isFullMode) return {}; // library mode: unchanged old flow (footer only)
|
|
226
402
|
const editorId = adapter.config?.fieldIds?.editor;
|
|
227
|
-
const editorError = editorId && errorData?.[
|
|
403
|
+
const editorError = editorId && errorData?.[activeTabIndex]?.[editorId];
|
|
228
404
|
if (!editorError || !adapter.getEditorErrorDescriptor || !intl) return {};
|
|
229
405
|
const descriptor = adapter.getEditorErrorDescriptor(toLegacy(stateRef.current), { tags, isFullMode, currentModule });
|
|
230
406
|
if (descriptor?.kind !== EDITOR_ERROR_KIND.BRACE) return {};
|
|
231
407
|
return { [editorId]: intl.formatMessage(descriptor.message, descriptor.values) };
|
|
232
|
-
}, [errorData, intl, isFullMode, currentModule]);
|
|
408
|
+
}, [errorData, intl, isFullMode, currentModule, activeTabIndex]);
|
|
233
409
|
|
|
234
410
|
// ---- field change ----
|
|
235
411
|
const onFieldChange = (field, value) => {
|
|
236
|
-
const
|
|
412
|
+
const liveActiveTab = stateRef.current?.meta?.activeTabIndex ?? ACTIVE_TAB_INDEX;
|
|
413
|
+
const tabIndex = field.standalone ? null : liveActiveTab;
|
|
237
414
|
const action = fieldChanged({ fieldId: field.id, value, tabIndex });
|
|
238
415
|
const nextState = formReducer(stateRef.current, action);
|
|
416
|
+
lastNextStateRef.current = nextState;
|
|
239
417
|
dispatch(action); // immediate UI update on next render
|
|
240
418
|
const legacy = toLegacy(nextState);
|
|
241
419
|
|
|
@@ -245,11 +423,13 @@ const FormBuilderShell = (props) => {
|
|
|
245
423
|
emitChange(legacy, field);
|
|
246
424
|
}
|
|
247
425
|
|
|
248
|
-
// Re-validate on change while errors are
|
|
249
|
-
//
|
|
250
|
-
// CapInput's antd suffix (see InputField), so the <input> never remounts.
|
|
426
|
+
// Re-validate on change while errors are shown, so a revealed error clears as
|
|
427
|
+
// soon as the field becomes valid.
|
|
251
428
|
const validationActive = propsRef.current.startValidation || internalCheckValidation || Boolean(propsRef.current.checkValidation);
|
|
252
|
-
|
|
429
|
+
// Classic re-validates pre-save typing only under startValidation (Classic.js:334-345);
|
|
430
|
+
// MPUSH opts out of live validation via features.liveValidation:false, SMS keeps it.
|
|
431
|
+
const validateLive = liquidEnabled && !isFullMode
|
|
432
|
+
&& adapter.config?.features?.liveValidation !== false;
|
|
253
433
|
if (validationActive || validateLive) {
|
|
254
434
|
emitValidity(runValidate(legacy), legacy);
|
|
255
435
|
}
|
|
@@ -266,14 +446,78 @@ const FormBuilderShell = (props) => {
|
|
|
266
446
|
};
|
|
267
447
|
|
|
268
448
|
// ---- parent bridge: invoke container-injected handlers (Stage A compat) ----
|
|
449
|
+
// Signatures mirror Classic exactly:
|
|
450
|
+
// - onTagSelect -> (data, currentTab, field) [Classic.js:2588-2591]
|
|
451
|
+
// - data-commit events -> (true, updatedLegacyFormData, field) [performFormDataUpdate]
|
|
452
|
+
// (checkbox submit-actions, radio onChange, select onSelect — MOBILEPUSH only,
|
|
453
|
+
// gated on config.features.bridgeFieldEvents so SMS behavior is untouched)
|
|
454
|
+
// - everything else -> (data, field.id) [callChildEvent tail]
|
|
269
455
|
const onEvent = (field, eventName, data) => {
|
|
270
456
|
const injected = field.injectedEvents?.[eventName];
|
|
271
457
|
if (typeof injected !== 'function') return;
|
|
458
|
+
const parent = propsRef.current.parent;
|
|
459
|
+
if (eventName === 'discardValues') {
|
|
460
|
+
// Classic resets its own state on discard (callChildEvent 2617-2625); the
|
|
461
|
+
// create-flow container then pushes EMPTY formData, which rehydrate ignores —
|
|
462
|
+
// so the local re-init must happen here.
|
|
463
|
+
injected.call(parent, data, field.id);
|
|
464
|
+
const fresh = initializeFormState(propsRef.current.schema || {});
|
|
465
|
+
lastEmittedRef.current = null;
|
|
466
|
+
lastSeenParentRef.current = null;
|
|
467
|
+
dispatch(hydrate(fresh));
|
|
468
|
+
setErrorData({});
|
|
469
|
+
setInternalCheckValidation(false);
|
|
470
|
+
const legacy = toLegacy(fresh);
|
|
471
|
+
emitValidity(runValidate(legacy), legacy);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
272
474
|
if (eventName === 'onTagSelect') {
|
|
273
|
-
injected.call(
|
|
475
|
+
injected.call(parent, data, (stateRef.current?.meta?.activeTabIndex ?? ACTIVE_TAB_INDEX) + 1, field);
|
|
476
|
+
} else if (
|
|
477
|
+
DATA_COMMIT_EVENTS.has(eventName)
|
|
478
|
+
&& DATA_COMMIT_FIELD_TYPES.has(field.type)
|
|
479
|
+
&& adapter.config?.features?.bridgeFieldEvents
|
|
480
|
+
) {
|
|
481
|
+
injected.call(parent, true, toLegacy(lastNextStateRef.current || stateRef.current), field);
|
|
274
482
|
} else {
|
|
275
|
-
injected.call(
|
|
483
|
+
injected.call(parent, data, field.id); // saveFormData / onClick / onUpload / div onInput / …
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
// ---- tab switch (from TabsContainer): state dispatch + container bridge ----
|
|
488
|
+
// Replaces Classic's DOM scan (Classic.js:2526-2551) with the same observable
|
|
489
|
+
// contract: the container's injected onTabChange receives the 1-based index.
|
|
490
|
+
const onTabSwitch = (container, key) => {
|
|
491
|
+
const tabs = stateRef.current?.tabs || [];
|
|
492
|
+
let index = tabs.findIndex((tab) => `${tab?.tabKey}` === `${key}`);
|
|
493
|
+
if (index === -1) {
|
|
494
|
+
const numeric = Number(key);
|
|
495
|
+
index = Number.isNaN(numeric) ? 0 : numeric;
|
|
276
496
|
}
|
|
497
|
+
dispatch(setActiveTab(index));
|
|
498
|
+
const injected = container?.injectedEvents?.onTabChange;
|
|
499
|
+
if (typeof injected === 'function') injected.call(propsRef.current.parent, index + 1);
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
// ---- confirmation modal (container-owned content, adapter-owned OK semantics) ----
|
|
503
|
+
const handleModalOk = (ev) => {
|
|
504
|
+
// antd v6 wraps button text in a <span>; use currentTarget so the button id resolves.
|
|
505
|
+
const id = (ev?.currentTarget && ev.currentTarget.id) || ev?.target?.id;
|
|
506
|
+
setModalVisible(false);
|
|
507
|
+
if (adapter.modals?.handleOk) {
|
|
508
|
+
adapter.modals.handleOk(id, {
|
|
509
|
+
platform: platformRef.current,
|
|
510
|
+
submitSingleTab: (singleTab) => {
|
|
511
|
+
const legacy = toLegacy(stateRef.current);
|
|
512
|
+
submitValidForm(legacy, errorData, { singleTab });
|
|
513
|
+
},
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
const handleModalCancel = () => {
|
|
519
|
+
setModalVisible(false);
|
|
520
|
+
if (propsRef.current.handleCancelModal) propsRef.current.handleCancelModal();
|
|
277
521
|
};
|
|
278
522
|
|
|
279
523
|
// Point the stable wrappers at the latest handler bodies (read at call time).
|
|
@@ -286,11 +530,16 @@ const FormBuilderShell = (props) => {
|
|
|
286
530
|
errorData,
|
|
287
531
|
registry,
|
|
288
532
|
checkValidation: internalCheckValidation || Boolean(checkValidation),
|
|
289
|
-
|
|
290
|
-
|
|
533
|
+
// Classic's upload case is the only renderer that ALSO honors startValidation
|
|
534
|
+
// for error display (Classic.js:3663) — exposed separately for it.
|
|
535
|
+
startValidation: Boolean(startValidation),
|
|
536
|
+
activeTabIndex,
|
|
537
|
+
currentTab,
|
|
538
|
+
tabCount,
|
|
291
539
|
onFieldChange: stableOnFieldChange,
|
|
292
540
|
onFieldBlur: stableOnFieldBlur,
|
|
293
541
|
onEvent: stableOnEvent,
|
|
542
|
+
onTabSwitch,
|
|
294
543
|
resolvedFieldErrors,
|
|
295
544
|
legacyFormData: toLegacy(state),
|
|
296
545
|
channel,
|
|
@@ -314,9 +563,27 @@ const FormBuilderShell = (props) => {
|
|
|
314
563
|
const spinning = Boolean((liquidEnabled && liquidExtractionInProgress) || metaDataStatus === REQUEST);
|
|
315
564
|
const spinTip = intl?.formatMessage ? intl.formatMessage(formMessages.liquidSpinText) : '';
|
|
316
565
|
|
|
566
|
+
// Confirmation modal (Classic getModal 'confirm' variant, Classic.js:597-641).
|
|
567
|
+
// Only channels that declare modal semantics render it (MOBILEPUSH); the footer's
|
|
568
|
+
// primary button carries the modal id — that id IS the handleOk dispatch key.
|
|
569
|
+
const confirmModal = adapter.modals && modal && (
|
|
570
|
+
<CapModal
|
|
571
|
+
visible={modalVisible}
|
|
572
|
+
title={modal.title || ''}
|
|
573
|
+
onCancel={handleModalCancel}
|
|
574
|
+
footer={[
|
|
575
|
+
<CapButton key="back" onClick={handleModalCancel}>Cancel</CapButton>,
|
|
576
|
+
<CapButton key="submit" type="primary" id={modal.id} onClick={handleModalOk}>Yes</CapButton>,
|
|
577
|
+
]}
|
|
578
|
+
>
|
|
579
|
+
{modal.body || ''}
|
|
580
|
+
</CapModal>
|
|
581
|
+
);
|
|
582
|
+
|
|
317
583
|
return (
|
|
318
584
|
<CapSpin spinning={spinning} tip={spinTip}>
|
|
319
585
|
<SchemaForm schema={schema} renderContext={renderContext} />
|
|
586
|
+
{confirmModal}
|
|
320
587
|
</CapSpin>
|
|
321
588
|
);
|
|
322
589
|
};
|
|
@@ -348,6 +615,10 @@ FormBuilderShell.propTypes = {
|
|
|
348
615
|
waitEventContextTags: PropTypes.object,
|
|
349
616
|
restrictPersonalization: PropTypes.bool,
|
|
350
617
|
refs: PropTypes.object,
|
|
618
|
+
// Tabbed channels (MOBILEPUSH): container-pushed tab position + confirmation modal.
|
|
619
|
+
currentTab: PropTypes.number,
|
|
620
|
+
showModal: PropTypes.bool,
|
|
621
|
+
modal: PropTypes.object,
|
|
351
622
|
};
|
|
352
623
|
|
|
353
624
|
FormBuilderShell.defaultProps = {
|
|
@@ -361,6 +632,9 @@ FormBuilderShell.defaultProps = {
|
|
|
361
632
|
waitEventContextTags: {},
|
|
362
633
|
restrictPersonalization: false,
|
|
363
634
|
refs: undefined,
|
|
635
|
+
currentTab: undefined,
|
|
636
|
+
showModal: false,
|
|
637
|
+
modal: null,
|
|
364
638
|
};
|
|
365
639
|
|
|
366
640
|
export default FormBuilderShell;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MOBILEPUSH submit payload — identity, exactly like Classic: the container
|
|
3
|
+
* receives the full legacy formData object and builds the API payload itself
|
|
4
|
+
* (getTransformedData). Note that getChannelData has NO MOBILEPUSH branch —
|
|
5
|
+
* Classic short-circuits before reaching it (Classic.js:1402-1405) — so this
|
|
6
|
+
* adapter must never call it.
|
|
7
|
+
*/
|
|
8
|
+
const buildSubmitPayload = (legacyFormData) => legacyFormData;
|
|
9
|
+
|
|
10
|
+
export default buildSubmitPayload;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MOBILEPUSH channel config — declarative capabilities + field ids for the MPUSH
|
|
3
|
+
* adapter. MOBILEPUSH is a two-tab channel (Android=tab 0, iOS=tab 1); iOS field
|
|
4
|
+
* ids carry the legacy `2` suffix (message-editor2). The containers mutate the
|
|
5
|
+
* schema at runtime (CTA row splices), so the engine's reconcile path is active.
|
|
6
|
+
*/
|
|
7
|
+
import { MOBILE_PUSH } from '../../../../../v2Containers/CreativesContainer/constants';
|
|
8
|
+
import { TEMPLATE_NAME_FIELD } from '../../constants';
|
|
9
|
+
|
|
10
|
+
// The exact legacy error VALUE written for brace mismatches — containers pattern
|
|
11
|
+
// on it (string ≠ boolean true matters: Classic's cross-tab toast reacts only to
|
|
12
|
+
// boolean true). Mirrors errorMessageForTags.TAG_BRACKET_COUNT_MISMATCH_ERROR.
|
|
13
|
+
export const ERROR_VALUE = {
|
|
14
|
+
BRACKET: 'tagBracketCountMismatchError',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Legacy tab semantics: 1-based currentTab, 1=Android, 2=iOS (Classic.js:483-486).
|
|
18
|
+
export const TABS = {
|
|
19
|
+
ANDROID: 0,
|
|
20
|
+
IOS: 1,
|
|
21
|
+
/** iOS field ids get suffix '2'; Android none (Classic.js:967). */
|
|
22
|
+
suffixFor: (tabIndex) => (tabIndex > 0 ? `${Number(tabIndex) + 1}` : ''),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const fieldIds = {
|
|
26
|
+
name: TEMPLATE_NAME_FIELD, // root field
|
|
27
|
+
editor: 'message-editor', // per-tab, suffixed on iOS
|
|
28
|
+
title: 'message-title', // per-tab, suffixed on iOS
|
|
29
|
+
image: 'image', // per-tab (imageSchema only), same id on both panes
|
|
30
|
+
bracketError: 'bracket-error',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// The 13 fixed CTA/deeplink rules of Classic's validateForm MPUSH block
|
|
34
|
+
// (Classic.js:1037-1135), in source order. kind: 'empty' => `=== ""` fails
|
|
35
|
+
// (absent key passes); kind: 'url' => isValidExternalLink semantics.
|
|
36
|
+
export const CTA_RULES = [
|
|
37
|
+
{ key: 'cta-deeplink-select', kind: 'empty' },
|
|
38
|
+
{ key: 'cta-deeplink2-select', kind: 'empty' },
|
|
39
|
+
{ key: 'cta-deeplink-text', kind: 'url' },
|
|
40
|
+
{ key: 'cta-deeplink2-text', kind: 'url' },
|
|
41
|
+
{ key: 'secondary-cta-0-link', kind: 'empty' },
|
|
42
|
+
{ key: 'secondary-cta-0-label', kind: 'empty' },
|
|
43
|
+
{ key: 'cta-deeplink-secondary-cta-0-text', kind: 'url' },
|
|
44
|
+
{ key: 'cta-deeplink-secondary-cta-0-select', kind: 'empty' },
|
|
45
|
+
{ key: 'secondary-cta-1-label', kind: 'empty' },
|
|
46
|
+
{ key: 'cta-deeplink-secondary-cta-1-text', kind: 'url' },
|
|
47
|
+
{ key: 'cta-deeplink-secondary-cta-1-select', kind: 'empty' },
|
|
48
|
+
{ key: 'cta-deeplink-secondary-cta-1-text2', kind: 'url' },
|
|
49
|
+
{ key: 'cta-deeplink-secondary-cta-1-select2', kind: 'empty' },
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
export default {
|
|
53
|
+
channel: MOBILE_PUSH,
|
|
54
|
+
fieldIds,
|
|
55
|
+
// Classic passes the live 1-based currentTab as showLiquidErrorInFooter's second
|
|
56
|
+
// argument for MPUSH (Classic.js:1352, 1487); SMS is the only null channel.
|
|
57
|
+
footerTabArg: 'currentTab',
|
|
58
|
+
features: {
|
|
59
|
+
tabs: true,
|
|
60
|
+
versions: false,
|
|
61
|
+
languages: false,
|
|
62
|
+
unicode: false,
|
|
63
|
+
liquid: true, // LIQUID_SUPPORTED_CHANNELS member; pipeline runs only outside isFullMode
|
|
64
|
+
askAira: true, // textarea only (Classic.js:2927)
|
|
65
|
+
preview: true,
|
|
66
|
+
imageUpload: true,
|
|
67
|
+
cta: true,
|
|
68
|
+
// Bridge checkbox/radio/select commit events to the container-injected handlers
|
|
69
|
+
// with the performFormDataUpdate signature (true, formData, field). MPUSH-only.
|
|
70
|
+
bridgeFieldEvents: true,
|
|
71
|
+
// MPUSH keeps the inline textarea error even though liquid is supported
|
|
72
|
+
// (Classic.js:2908-2915) — encoded in the MPUSH renderers.
|
|
73
|
+
inlineErrorWithLiquid: true,
|
|
74
|
+
// Classic re-validates typing only under startValidation (Classic.js:334-345):
|
|
75
|
+
// no pre-save errorData, so no tag/footer message before a save attempt.
|
|
76
|
+
liveValidation: false,
|
|
77
|
+
// Validity is still maintained in the background (Classic validates + emits on
|
|
78
|
+
// parent pushes / schema changes / tags changes, Classic.js:516-578, 1361) —
|
|
79
|
+
// the containers gate their save flows on it (Create/index.js:101).
|
|
80
|
+
validateOnExternalChange: true,
|
|
81
|
+
},
|
|
82
|
+
};
|