@capillarytech/creatives-library 9.0.56-betaa.0 → 9.0.56

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.
Files changed (116) hide show
  1. package/constants/unified.js +9 -4
  2. package/package.json +1 -1
  3. package/services/api.js +25 -4
  4. package/utils/common.js +0 -12
  5. package/utils/templateVarUtils.js +16 -0
  6. package/utils/tests/templateVarUtils.test.js +44 -0
  7. package/v2Components/CapActionButton/index.js +73 -60
  8. package/v2Components/CapActionButton/index.scss +44 -28
  9. package/v2Components/CapActionButton/messages.js +7 -3
  10. package/v2Components/CapActionButton/tests/index.test.js +17 -1
  11. package/v2Components/CapWhatsappCTA/messages.js +4 -0
  12. package/v2Components/CapWhatsappCarouselButton/index.js +42 -33
  13. package/v2Components/CapWhatsappCarouselButton/index.scss +44 -2
  14. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +13 -0
  15. package/v2Components/CommonTestAndPreview/actions.js +13 -0
  16. package/v2Components/CommonTestAndPreview/constants.js +8 -0
  17. package/v2Components/CommonTestAndPreview/index.js +55 -3
  18. package/v2Components/CommonTestAndPreview/reducer.js +20 -0
  19. package/v2Components/CommonTestAndPreview/sagas.js +28 -0
  20. package/v2Components/CommonTestAndPreview/selectors.js +6 -0
  21. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +106 -0
  22. package/v2Components/CommonTestAndPreview/tests/actions.test.js +15 -0
  23. package/v2Components/CommonTestAndPreview/tests/constants.test.js +9 -1
  24. package/v2Components/CommonTestAndPreview/tests/index.test.js +101 -0
  25. package/v2Components/CommonTestAndPreview/tests/reducer.test.js +41 -0
  26. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +84 -0
  27. package/v2Components/CommonTestAndPreview/tests/selectors.test.js +12 -0
  28. package/v2Components/FormBuilder/Functional/FormBuilderShell.js +46 -320
  29. package/v2Components/FormBuilder/Functional/channels/registry.js +0 -2
  30. package/v2Components/FormBuilder/Functional/constants.js +8 -43
  31. package/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js +28 -175
  32. package/v2Components/FormBuilder/Functional/core/store/formReducer.js +6 -75
  33. package/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js +20 -10
  34. package/v2Components/FormBuilder/Functional/layout/FieldSlot.js +1 -9
  35. package/v2Components/FormBuilder/Functional/layout/SchemaForm.js +3 -20
  36. package/v2Components/FormBuilder/Functional/layout/Section.js +1 -6
  37. package/v2Components/FormBuilder/Functional/tests/fieldSlot.test.js +2 -9
  38. package/v2Components/FormBuilder/Functional/tests/schemaForm.test.js +0 -17
  39. package/v2Components/FormBuilder/index.js +12 -34
  40. package/v2Components/FormBuilder/tests/entryGate.test.js +7 -94
  41. package/v2Components/TestAndPreviewSlidebox/index.js +9 -0
  42. package/v2Containers/CommunicationFlow/CommunicationFlow.js +140 -75
  43. package/v2Containers/CommunicationFlow/CommunicationFlow.scss +18 -2
  44. package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +67 -28
  45. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +503 -51
  46. package/v2Containers/CommunicationFlow/Tests/CommunicationFlowCard.test.js +98 -4
  47. package/v2Containers/CommunicationFlow/Tests/constants.test.js +85 -0
  48. package/v2Containers/CommunicationFlow/constants.js +65 -4
  49. package/v2Containers/CommunicationFlow/index.js +16 -23
  50. package/v2Containers/CommunicationFlow/messages.js +11 -3
  51. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +27 -11
  52. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +4 -0
  53. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +176 -4
  54. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +22 -4
  55. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +14 -4
  56. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +11 -0
  57. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +51 -0
  58. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +11 -4
  59. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +19 -5
  60. package/v2Containers/CreativesContainer/SlideBoxContent.js +15 -0
  61. package/v2Containers/CreativesContainer/SlideBoxFooter.js +6 -2
  62. package/v2Containers/CreativesContainer/index.js +29 -3
  63. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +29 -0
  64. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +18 -5
  65. package/v2Containers/CreativesContainer/tests/index.test.js +12 -0
  66. package/v2Containers/MobilePush/Create/index.js +18 -1
  67. package/v2Containers/MobilePush/Create/test/contentValidity.test.js +96 -0
  68. package/v2Containers/MobilePush/Edit/index.js +18 -1
  69. package/v2Containers/MobilePush/Edit/test/contentValidity.test.js +116 -0
  70. package/v2Containers/MobilePush/commonMethods.js +49 -1
  71. package/v2Containers/MobilePushNew/index.js +29 -4
  72. package/v2Containers/MobilePushNew/tests/index.test.js +119 -0
  73. package/v2Containers/MobilePushNew/tests/utils.test.js +82 -0
  74. package/v2Containers/MobilePushNew/utils.js +34 -1
  75. package/v2Containers/MobilepushWrapper/index.js +3 -1
  76. package/v2Containers/Rcs/index.js +34 -0
  77. package/v2Containers/Rcs/index.scss +15 -0
  78. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +115 -19
  79. package/v2Containers/Rcs/tests/index.test.js +67 -0
  80. package/v2Containers/Sms/Create/index.js +3 -1
  81. package/v2Containers/Sms/Edit/index.js +25 -0
  82. package/v2Containers/Sms/Edit/tests/index.test.js +85 -0
  83. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +27 -3
  84. package/v2Containers/Viber/index.js +24 -1
  85. package/v2Containers/Viber/tests/index.test.js +103 -0
  86. package/v2Containers/WebPush/Create/index.js +19 -0
  87. package/v2Containers/WebPush/Create/preview/WebPushPreview.js +17 -12
  88. package/v2Containers/WebPush/Create/preview/tests/WebPushPreview.test.js +5 -0
  89. package/v2Containers/WebPush/Create/tests/contentValidity.test.js +294 -0
  90. package/v2Containers/Whatsapp/index.js +25 -4
  91. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +306 -34
  92. package/v2Containers/Whatsapp/tests/index.test.js +115 -0
  93. package/v2Containers/Zalo/index.js +28 -1
  94. package/v2Containers/Zalo/tests/index.test.js +99 -0
  95. package/v2Containers/mockdata.js +25 -0
  96. package/v2Components/FormBuilder/Functional/channels/mobilepush/buildSubmitPayload.js +0 -10
  97. package/v2Components/FormBuilder/Functional/channels/mobilepush/config.js +0 -82
  98. package/v2Components/FormBuilder/Functional/channels/mobilepush/getEditorErrorDescriptor.js +0 -74
  99. package/v2Components/FormBuilder/Functional/channels/mobilepush/index.js +0 -28
  100. package/v2Components/FormBuilder/Functional/channels/mobilepush/modals.js +0 -19
  101. package/v2Components/FormBuilder/Functional/channels/mobilepush/runSubmitPipeline.js +0 -45
  102. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/getEditorErrorDescriptor.test.js +0 -162
  103. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/modals.test.js +0 -37
  104. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/runSubmitPipeline.test.js +0 -70
  105. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/validate.test.js +0 -274
  106. package/v2Components/FormBuilder/Functional/channels/mobilepush/validate.js +0 -251
  107. package/v2Components/FormBuilder/Functional/layout/TabsContainer.js +0 -89
  108. package/v2Components/FormBuilder/Functional/renderers/mpushRenderers.js +0 -440
  109. package/v2Components/FormBuilder/Functional/tests/mpush.crossFlowParity.test.js +0 -430
  110. package/v2Components/FormBuilder/Functional/tests/mpush.ctaFlows.parity.test.js +0 -282
  111. package/v2Components/FormBuilder/Functional/tests/mpush.editContainer.parity.test.js +0 -141
  112. package/v2Components/FormBuilder/Functional/tests/mpush.engine.test.js +0 -306
  113. package/v2Components/FormBuilder/Functional/tests/mpush.shellEvents.test.js +0 -151
  114. package/v2Components/FormBuilder/Functional/tests/mpushRenderers.test.js +0 -342
  115. package/v2Components/FormBuilder/Functional/tests/tabsContainer.test.js +0 -110
  116. package/v2Components/FormBuilder/tests/mpush.characterization.test.js +0 -459
@@ -1,8 +1,7 @@
1
1
  /**
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.
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, { reconcileFormState } from './core/schema/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,19 +58,11 @@ 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
- // 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
- );
65
+ const registry = useMemo(() => registryFor(), [channel]);
79
66
 
80
67
  const [state, dispatch] = useReducer(formReducer, channel, createInitialState);
81
68
  const [errorData, setErrorData] = useState({});
@@ -84,10 +71,6 @@ const FormBuilderShell = (props) => {
84
71
  // is still computed + emitted via onFormValidityChange on mount; only the inline
85
72
  // display is gated. Can also be driven by the container's checkValidation prop.
86
73
  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);
91
74
 
92
75
  // refs to latest values for use inside stable/debounced callbacks
93
76
  const stateRef = useRef(state);
@@ -97,25 +80,6 @@ const FormBuilderShell = (props) => {
97
80
  const lastEmittedRef = useRef(null);
98
81
  const didInitRef = useRef(false);
99
82
  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);
119
83
 
120
84
  // Stable handler identities for the memoized FieldSlot: these wrappers delegate to
121
85
  // the latest handler bodies (via a ref), so React.memo can bail out on sibling changes.
@@ -127,20 +91,11 @@ const FormBuilderShell = (props) => {
127
91
  const isEmbedded = location?.query?.type === EMBEDDED;
128
92
  const currentModule = tagModule || location?.query?.module || DEFAULT_MODULE;
129
93
 
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, {
94
+ const runValidate = (legacy) => adapter.validate(legacy, {
136
95
  tags: propsRef.current.tags,
137
96
  isFullMode,
138
97
  isEmbedded,
139
98
  currentModule,
140
- restrictPersonalization: propsRef.current.restrictPersonalization,
141
- currentTab: (stateRef.current?.meta?.activeTabIndex ?? ACTIVE_TAB_INDEX) + 1,
142
- schema: propsRef.current.schema,
143
- ...extra,
144
99
  });
145
100
 
146
101
  // Footer state (mirrors Classic's `liquidErrorMessage`). Holds the last
@@ -148,66 +103,45 @@ const FormBuilderShell = (props) => {
148
103
  // clobber each other before forwarding to the container's showLiquidErrorInFooter.
149
104
  const footerRef = useRef({ STANDARD_ERROR_MSG: [], LIQUID_ERROR_MSG: [] });
150
105
  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);
156
106
 
157
107
  const pushFooter = (next) => {
158
108
  footerRef.current = { ...footerRef.current, ...next };
159
109
  if (propsRef.current.showLiquidErrorInFooter) {
160
- propsRef.current.showLiquidErrorInFooter(footerRef.current, footerTabArg());
110
+ propsRef.current.showLiquidErrorInFooter(
111
+ footerRef.current,
112
+ channel === SMS ? null : propsRef.current.currentTab,
113
+ );
161
114
  }
162
115
  };
163
116
 
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) => {
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) => {
170
121
  if (isFullMode || !adapter.getEditorErrorDescriptor || !intl) return [];
171
122
  const descriptor = adapter.getEditorErrorDescriptor(legacy, {
172
123
  tags: propsRef.current.tags,
173
124
  isFullMode,
174
125
  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,
182
126
  });
183
- if (!descriptor) return [];
184
- if (descriptor.text) return [descriptor.text];
185
- return [intl.formatMessage(descriptor.message, descriptor.values)];
127
+ return descriptor ? [intl.formatMessage(descriptor.message, descriptor.values)] : [];
186
128
  };
187
129
 
188
130
  const emitValidity = (result, legacy) => {
189
131
  setErrorData(result.errorData);
190
- if (result.platform) platformRef.current = result.platform;
191
132
  if (propsRef.current.onFormValidityChange) {
192
133
  propsRef.current.onFormValidityChange(result.isValid, result.errorData);
193
134
  }
194
135
  // Footer: clear standard errors when the form is valid; otherwise surface the
195
- // body-error message (empty/generic gated per channel). Liquid errors are preserved.
136
+ // brace/missing-tag message (empty/generic => none). Liquid errors are preserved.
196
137
  if (liquidEnabled) {
197
- pushFooter({ STANDARD_ERROR_MSG: result.isValid ? [] : standardFooterMsg(legacy, result) });
138
+ pushFooter({ STANDARD_ERROR_MSG: result.isValid ? [] : standardFooterMsg(legacy) });
198
139
  }
199
140
  };
200
141
 
201
142
  const emitChange = (legacy, field) => {
202
143
  lastEmittedRef.current = legacy;
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
- }
144
+ if (propsRef.current.onChange) propsRef.current.onChange(legacy, 1, 1, field);
211
145
  };
212
146
 
213
147
  const debouncedEmit = useMemo(
@@ -217,130 +151,30 @@ const FormBuilderShell = (props) => {
217
151
  useEffect(() => () => debouncedEmit.cancel(), [debouncedEmit]);
218
152
 
219
153
  // ---- initialize once the (possibly async) schema is ready, then validate+emit ----
220
- const schemaReady = Boolean(schema?.standalone || schema?.containers?.length);
154
+ const schemaReady = Boolean(schema?.standalone);
221
155
  useEffect(() => {
222
156
  if (schemaReady && !didInitRef.current) {
223
157
  didInitRef.current = true;
224
- prevSchemaRef.current = schema;
225
158
  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
- }
231
159
  dispatch(hydrate(init));
232
- lastTagsRef.current = cloneDeep(propsRef.current.tags || null);
233
160
  const legacy = toLegacy(init);
234
161
  emitValidity(runValidate(legacy), legacy);
235
162
  }
236
163
  }, [schemaReady]);
237
164
 
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.
165
+ // ---- re-hydrate on a GENUINE external parent push (edit load / discard) ----
267
166
  useEffect(() => {
268
167
  if (!didInitRef.current || !isNonEmpty(parentFormData)) return;
269
- if (isEqual(parentFormData, lastSeenParentRef.current)) return; // nothing new from the parent
270
- lastSeenParentRef.current = cloneDeep(parentFormData);
271
168
  const current = toLegacy(stateRef.current);
272
169
  const isEcho = isEqual(parentFormData, lastEmittedRef.current) || isEqual(parentFormData, current);
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]);
170
+ if (!isEcho) dispatch(hydrate(fromLegacy(parentFormData, { channel })));
171
+ }, [parentFormData]);
316
172
 
317
173
  // Submit a form that passed sync validation (mirrors Classic onSubmitWrapper).
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
174
+ // Full mode submits directly; outside it, liquid-supported channels run liquid-tag
321
175
  // validation first — a clean result submits, an error goes to the footer and blocks.
322
- const submitValidForm = (legacy, validErrorData, { singleTab = null } = {}) => {
176
+ const submitValidForm = (legacy, validErrorData) => {
323
177
  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
-
344
178
  const runLiquid = liquidEnabled && !isFullMode;
345
179
  const getLiquidTags = currentProps.actions?.getLiquidTags;
346
180
  if (!runLiquid || typeof getLiquidTags !== 'function') {
@@ -367,28 +201,18 @@ const FormBuilderShell = (props) => {
367
201
  });
368
202
  };
369
203
 
370
- // ---- startValidation rising edge -> validate, then save / prompt / stop ----
204
+ // ---- startValidation rising edge -> validate, then save or stop ----
371
205
  useEffect(() => {
372
206
  if (startValidation && !prevStartValidationRef.current) {
373
207
  setInternalCheckValidation(true); // reveal field error messages from now on
374
208
  debouncedEmit.flush();
375
209
  const legacy = toLegacy(stateRef.current);
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
- }
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();
392
216
  }
393
217
  }
394
218
  prevStartValidationRef.current = startValidation;
@@ -400,20 +224,18 @@ const FormBuilderShell = (props) => {
400
224
  const resolvedFieldErrors = useMemo(() => {
401
225
  if (!isFullMode) return {}; // library mode: unchanged old flow (footer only)
402
226
  const editorId = adapter.config?.fieldIds?.editor;
403
- const editorError = editorId && errorData?.[activeTabIndex]?.[editorId];
227
+ const editorError = editorId && errorData?.[ACTIVE_TAB_INDEX]?.[editorId];
404
228
  if (!editorError || !adapter.getEditorErrorDescriptor || !intl) return {};
405
229
  const descriptor = adapter.getEditorErrorDescriptor(toLegacy(stateRef.current), { tags, isFullMode, currentModule });
406
230
  if (descriptor?.kind !== EDITOR_ERROR_KIND.BRACE) return {};
407
231
  return { [editorId]: intl.formatMessage(descriptor.message, descriptor.values) };
408
- }, [errorData, intl, isFullMode, currentModule, activeTabIndex]);
232
+ }, [errorData, intl, isFullMode, currentModule]);
409
233
 
410
234
  // ---- field change ----
411
235
  const onFieldChange = (field, value) => {
412
- const liveActiveTab = stateRef.current?.meta?.activeTabIndex ?? ACTIVE_TAB_INDEX;
413
- const tabIndex = field.standalone ? null : liveActiveTab;
236
+ const tabIndex = field.standalone ? null : ACTIVE_TAB_INDEX;
414
237
  const action = fieldChanged({ fieldId: field.id, value, tabIndex });
415
238
  const nextState = formReducer(stateRef.current, action);
416
- lastNextStateRef.current = nextState;
417
239
  dispatch(action); // immediate UI update on next render
418
240
  const legacy = toLegacy(nextState);
419
241
 
@@ -423,13 +245,11 @@ const FormBuilderShell = (props) => {
423
245
  emitChange(legacy, field);
424
246
  }
425
247
 
426
- // Re-validate on change while errors are shown, so a revealed error clears as
427
- // 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.
428
251
  const validationActive = propsRef.current.startValidation || internalCheckValidation || Boolean(propsRef.current.checkValidation);
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;
252
+ const validateLive = liquidEnabled && !isFullMode;
433
253
  if (validationActive || validateLive) {
434
254
  emitValidity(runValidate(legacy), legacy);
435
255
  }
@@ -446,78 +266,14 @@ const FormBuilderShell = (props) => {
446
266
  };
447
267
 
448
268
  // ---- 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]
455
269
  const onEvent = (field, eventName, data) => {
456
270
  const injected = field.injectedEvents?.[eventName];
457
271
  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
- }
474
272
  if (eventName === 'onTagSelect') {
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);
273
+ injected.call(propsRef.current.parent, data, 1, field); // Classic signature
482
274
  } else {
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;
275
+ injected.call(propsRef.current.parent, data, field.id); // saveFormData / onClick
496
276
  }
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();
521
277
  };
522
278
 
523
279
  // Point the stable wrappers at the latest handler bodies (read at call time).
@@ -530,16 +286,11 @@ const FormBuilderShell = (props) => {
530
286
  errorData,
531
287
  registry,
532
288
  checkValidation: internalCheckValidation || Boolean(checkValidation),
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,
289
+ activeTabIndex: ACTIVE_TAB_INDEX,
290
+ currentTab: 1,
539
291
  onFieldChange: stableOnFieldChange,
540
292
  onFieldBlur: stableOnFieldBlur,
541
293
  onEvent: stableOnEvent,
542
- onTabSwitch,
543
294
  resolvedFieldErrors,
544
295
  legacyFormData: toLegacy(state),
545
296
  channel,
@@ -563,27 +314,9 @@ const FormBuilderShell = (props) => {
563
314
  const spinning = Boolean((liquidEnabled && liquidExtractionInProgress) || metaDataStatus === REQUEST);
564
315
  const spinTip = intl?.formatMessage ? intl.formatMessage(formMessages.liquidSpinText) : '';
565
316
 
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
-
583
317
  return (
584
318
  <CapSpin spinning={spinning} tip={spinTip}>
585
319
  <SchemaForm schema={schema} renderContext={renderContext} />
586
- {confirmModal}
587
320
  </CapSpin>
588
321
  );
589
322
  };
@@ -615,10 +348,6 @@ FormBuilderShell.propTypes = {
615
348
  waitEventContextTags: PropTypes.object,
616
349
  restrictPersonalization: PropTypes.bool,
617
350
  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,
622
351
  };
623
352
 
624
353
  FormBuilderShell.defaultProps = {
@@ -632,9 +361,6 @@ FormBuilderShell.defaultProps = {
632
361
  waitEventContextTags: {},
633
362
  restrictPersonalization: false,
634
363
  refs: undefined,
635
- currentTab: undefined,
636
- showModal: false,
637
- modal: null,
638
364
  };
639
365
 
640
366
  export default FormBuilderShell;
@@ -4,11 +4,9 @@
4
4
  * is a new folder + one line here.
5
5
  */
6
6
  import smsAdapter from './sms';
7
- import mpushAdapter from './mobilepush';
8
7
 
9
8
  const adapters = {
10
9
  SMS: smsAdapter,
11
- MOBILEPUSH: mpushAdapter,
12
10
  };
13
11
 
14
12
  export const getChannelAdapter = (channel) => {
@@ -9,13 +9,7 @@ export const SECTION_TYPE = {
9
9
  COL_LABEL: 'col-label',
10
10
  };
11
11
 
12
- // Container types in the layout schema (schema.containers[]). MOBILEPUSH uses a
13
- // single 'tabs' container carrying the Android/iOS panes.
14
- export const CONTAINER_TYPE = {
15
- TABS: 'tabs',
16
- };
17
-
18
- // Leaf field types the migrated channels use (registry keys + memoization decisions).
12
+ // Leaf field types the SMS schema uses (registry keys + memoization decisions).
19
13
  export const FIELD_TYPE = {
20
14
  INPUT: 'input',
21
15
  TEXTAREA: 'textarea',
@@ -24,12 +18,6 @@ export const FIELD_TYPE = {
24
18
  TAG_LIST: 'tag-list',
25
19
  SMS_PREVIEW: 'sms-preview',
26
20
  DIV: 'div',
27
- // MOBILEPUSH (Phase 2)
28
- RADIO_GROUP: 'radioGroup',
29
- SELECT: 'select',
30
- UPLOAD: 'upload',
31
- ICON: 'icon',
32
- MOBILE_PUSH_PREVIEW: 'mobile-push-preview',
33
21
  };
34
22
 
35
23
  // Editor-error descriptor kinds (getEditorErrorDescriptor): the brace error is shown
@@ -39,39 +27,16 @@ export const EDITOR_ERROR_KIND = {
39
27
  MISSING: 'missing',
40
28
  };
41
29
 
42
- // Types rendered through the memoized FieldSlot (depend only on own value/error).
43
- // radioGroup/select stay direct: they resolve value/options from the full legacy formData.
44
- export const MEMOIZED_TYPES = new Set([
45
- FIELD_TYPE.INPUT,
46
- FIELD_TYPE.TEXTAREA,
47
- FIELD_TYPE.CHECKBOX,
48
- ]);
30
+ // Field types whose render depends only on their own value/error (+ the
31
+ // checkValidation/channel primitives). These go through the memoized FieldSlot;
32
+ // derived/control fields (sms-preview, sms-count, tag-list, button) render directly.
33
+ export const MEMOIZED_TYPES = new Set([FIELD_TYPE.INPUT, FIELD_TYPE.TEXTAREA, FIELD_TYPE.CHECKBOX]);
49
34
 
50
- // Fallback active tab for single-tab channels (SMS). Tabbed channels keep the live
51
- // value in reducer state (meta.activeTabIndex).
35
+ // Phase 1 renders a single active tab (index 0); multi-tab / version machinery
36
+ // arrives with later channels.
52
37
  export const ACTIVE_TAB_INDEX = 0;
53
38
 
54
- // Injected events called with Classic's performFormDataUpdate signature
55
- // (true, updatedLegacyFormData, field); everything else takes (data, field.id).
56
- export const DATA_COMMIT_EVENTS = new Set([
57
- 'addPrimaryCta',
58
- 'addSecondaryCta',
59
- 'addSecondaryCtaIos',
60
- 'onChange',
61
- 'onSelect',
62
- ]);
63
-
64
- // Only these types reach Classic's commit path — a div's injected 'onChange'
65
- // must keep the tail signature, hence the type guard.
66
- export const DATA_COMMIT_FIELD_TYPES = new Set([
67
- FIELD_TYPE.CHECKBOX,
68
- FIELD_TYPE.RADIO_GROUP,
69
- FIELD_TYPE.SELECT,
70
- ]);
71
-
72
- export const TEMPLATE_NAME_FIELD = 'template-name';
73
-
74
- export const HIGH_FREQ_FIELDS = [TEMPLATE_NAME_FIELD, 'template-subject'];
39
+ export const HIGH_FREQ_FIELDS = ['template-name', 'template-subject'];
75
40
 
76
41
  export const ASCII = 'ASCII';
77
42
  export const UNICODE = 'Unicode';