@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
@@ -1538,3 +1538,118 @@ describe('WhatsApp edit Done button whitespace validation', () => {
1538
1538
  expect(getDoneButton().props().disabled).toBe(true);
1539
1539
  });
1540
1540
  });
1541
+
1542
+ describe('Whatsapp onContentValidityChange reporting', () => {
1543
+ let renderedComponent;
1544
+
1545
+ const createWhatsappTemplate = jest.fn();
1546
+ const getFormData = jest.fn();
1547
+ const clearCreateResponse = jest.fn();
1548
+ const getTemplateDetails = jest.fn();
1549
+ const resetEditTemplate = jest.fn();
1550
+ const fetchSchemaForEntity = jest.fn();
1551
+ const handleClose = jest.fn();
1552
+ const onCreateComplete = jest.fn();
1553
+ const formatMessage = jest.fn();
1554
+ const getMetaTags = jest.fn();
1555
+ const resetMetaTags = jest.fn();
1556
+
1557
+ const mountCreateFlow = (onContentValidityChange) => {
1558
+ renderedComponent = mountWithIntl(
1559
+ <Provider store={store}>
1560
+ <Whatsapp
1561
+ actions={{
1562
+ createWhatsappTemplate,
1563
+ clearCreateResponse,
1564
+ getTemplateDetails,
1565
+ resetEditTemplate,
1566
+ getMetaTags,
1567
+ resetMetaTags,
1568
+ }}
1569
+ globalActions={{ fetchSchemaForEntity }}
1570
+ onCreateComplete={onCreateComplete}
1571
+ handleClose={handleClose}
1572
+ getFormData={getFormData}
1573
+ params={{}}
1574
+ editData={{}}
1575
+ templateData={{}}
1576
+ accountData={mockData.accountData1}
1577
+ location={mockData.location}
1578
+ intl={{ formatMessage }}
1579
+ isFullMode={true}
1580
+ loadingTags={false}
1581
+ metaEntities={[]}
1582
+ getDefaultTags={true}
1583
+ onContentValidityChange={onContentValidityChange}
1584
+ Templates={{
1585
+ senderDetails: {
1586
+ status: 'SUCCESS',
1587
+ domainProperties: [
1588
+ {
1589
+ domainProperties: {
1590
+ connectionProperties: {
1591
+ sourceAccountIdentifier: '2000222347',
1592
+ baseUrl: 'https://api.gupshup.io/whatsapp/v1',
1593
+ },
1594
+ },
1595
+ },
1596
+ ],
1597
+ },
1598
+ }}
1599
+ />
1600
+ </Provider>,
1601
+ );
1602
+ };
1603
+
1604
+ const changeTemplateMessage = (val) => {
1605
+ renderedComponent
1606
+ .find('textarea#whatsapp-create-template-message-input')
1607
+ .at(0)
1608
+ .simulate('change', { target: { value: val } });
1609
+ renderedComponent.update();
1610
+ };
1611
+
1612
+ it('reports isContentEmpty: true on mount when the message body is empty', () => {
1613
+ const onContentValidityChange = jest.fn();
1614
+ mountCreateFlow(onContentValidityChange);
1615
+
1616
+ expect(onContentValidityChange).toHaveBeenCalledWith({ isContentEmpty: true });
1617
+ });
1618
+
1619
+ it('reports isContentEmpty: false once the message body has content', () => {
1620
+ const onContentValidityChange = jest.fn();
1621
+ mountCreateFlow(onContentValidityChange);
1622
+ onContentValidityChange.mockClear();
1623
+
1624
+ changeTemplateMessage('Hi there');
1625
+
1626
+ expect(onContentValidityChange).toHaveBeenCalledWith({ isContentEmpty: false });
1627
+ });
1628
+
1629
+ it('re-reports empty when the message body is cleared out', () => {
1630
+ const onContentValidityChange = jest.fn();
1631
+ mountCreateFlow(onContentValidityChange);
1632
+ changeTemplateMessage('Hi there');
1633
+ onContentValidityChange.mockClear();
1634
+
1635
+ changeTemplateMessage('');
1636
+
1637
+ expect(onContentValidityChange).toHaveBeenCalledWith({ isContentEmpty: true });
1638
+ });
1639
+
1640
+ it('does not throw and does not report when onContentValidityChange is not provided', () => {
1641
+ expect(() => mountCreateFlow(undefined)).not.toThrow();
1642
+ });
1643
+
1644
+ it('does not report again on a re-render that does not change validity (dedup guard)', () => {
1645
+ const onContentValidityChange = jest.fn();
1646
+ mountCreateFlow(onContentValidityChange);
1647
+ changeTemplateMessage('Hi there');
1648
+ onContentValidityChange.mockClear();
1649
+
1650
+ // Same non-empty value re-set — validity boolean is unchanged.
1651
+ changeTemplateMessage('Hi there again');
1652
+
1653
+ expect(onContentValidityChange).not.toHaveBeenCalled();
1654
+ });
1655
+ });
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable camelcase */
2
2
  /* eslint-disable react/jsx-indent */
3
- import React, { useState, useEffect } from 'react';
3
+ import React, { useState, useEffect, useRef } from 'react';
4
4
  import { bindActionCreators } from 'redux';
5
5
  import { createStructuredSelector } from 'reselect';
6
6
  import { FormattedMessage } from 'react-intl';
@@ -82,6 +82,7 @@ export const Zalo = (props) => {
82
82
  showTestAndPreviewSlidebox: propsShowTestAndPreviewSlidebox,
83
83
  handleTestAndPreview: propsHandleTestAndPreview,
84
84
  handleCloseTestAndPreview: propsHandleCloseTestAndPreview,
85
+ onContentValidityChange,
85
86
  } = props || {};
86
87
  // Since zalo supports now multiple accounts, we need to get the hostname from the selected account or passed props.
87
88
  const hostName = selectedZaloAccount?.hostName || zaloHostName;
@@ -368,6 +369,32 @@ export const Zalo = (props) => {
368
369
  });
369
370
  return disableCheck;
370
371
  };
372
+
373
+ // Content is considered empty when every variable field's value is blank
374
+ // (i.e. the Zalo message body carries no text and no injected tags).
375
+ const isZaloContentEmpty = (listParams) => {
376
+ if (!Array.isArray(listParams) || listParams.length === 0) {
377
+ return true;
378
+ }
379
+ return listParams.every((listParam) => !(listParam?.value || '').trim());
380
+ };
381
+
382
+ // Tracks the last validity value reported to the parent so this effect does not
383
+ // dispatch on every render. Intentionally undefined (not true) so the first render
384
+ // always reports the real validity rather than assuming the form starts invalid.
385
+ const lastReportedZaloContentEmptyRef = useRef(undefined);
386
+ useEffect(() => {
387
+ if (typeof onContentValidityChange !== 'function') {
388
+ return;
389
+ }
390
+ const isContentEmpty = isZaloContentEmpty(templateListParams);
391
+ if (lastReportedZaloContentEmptyRef.current === isContentEmpty) {
392
+ return;
393
+ }
394
+ lastReportedZaloContentEmptyRef.current = isContentEmpty;
395
+ onContentValidityChange({ isContentEmpty });
396
+ }, [templateListParams, onContentValidityChange]);
397
+
371
398
  const createPayload = () => {
372
399
  const varMap = {};
373
400
  templateListParams?.forEach((listParam) => {
@@ -150,4 +150,103 @@ describe('Test activity zalo container', () => {
150
150
  ),
151
151
  ).toBeInTheDocument();
152
152
  });
153
+
154
+ // Fully self-contained fixtures for the tests below: the "set template data" test above
155
+ // mutates the shared `editData`/`templateData` fixtures in place (handleSetValues mutates
156
+ // listParam objects directly, and that test also reassigns templateData.templateConfigs),
157
+ // so building independent literals here — rather than reusing or cloning the shared
158
+ // fixtures at test time (which would just clone the already-mutated state) — keeps these
159
+ // tests correct regardless of suite execution order.
160
+ const buildFreshEditData = () => ({
161
+ zaloTemplateInfoStatus: 'success',
162
+ zaloTemplateInfoValue: {
163
+ name: 'Fresh Template',
164
+ versions: {
165
+ base: {
166
+ content: {
167
+ zalo: {
168
+ templateId: '1',
169
+ templateName: 'Fresh Template',
170
+ listParams: [
171
+ {
172
+ name: 'danh_xung',
173
+ require: true,
174
+ type: 'STRING',
175
+ maxLength: 30,
176
+ minLength: 0,
177
+ acceptNull: false,
178
+ error: '',
179
+ value: '',
180
+ },
181
+ ],
182
+ previewUrl: 'https://account.zalo.cloud/preview',
183
+ status: 'ENABLE',
184
+ },
185
+ },
186
+ },
187
+ },
188
+ type: 'ZALO',
189
+ },
190
+ zaloTemplateInfoError: null,
191
+ });
192
+ const buildFreshTemplateData = () => ({
193
+ _id: 999001,
194
+ templateConfigs: { id: '', varMapped: {} },
195
+ });
196
+
197
+ it('reports content emptiness to the parent via onContentValidityChange', async () => {
198
+ const onContentValidityChange = jest.fn();
199
+ renderComponent({
200
+ actions: mockActions,
201
+ globalActions: mockGlobalActions,
202
+ editData: buildFreshEditData(),
203
+ metaEntities,
204
+ location,
205
+ getDefaultTags,
206
+ injectedTags,
207
+ accountData,
208
+ templateData: buildFreshTemplateData(),
209
+ isFullMode: true,
210
+ onContentValidityChange,
211
+ });
212
+ const inputBox = await screen.findAllByPlaceholderText(
213
+ 'Add labels or text or combination of both',
214
+ );
215
+ // Initial values in the mock data are blank, so the first report should be "empty".
216
+ expect(onContentValidityChange).toHaveBeenCalledWith({ isContentEmpty: true });
217
+ onContentValidityChange.mockClear();
218
+
219
+ fireEvent.change(inputBox[0], { target: { value: 'Hello, welcome' } });
220
+ expect(onContentValidityChange).toHaveBeenCalledWith({ isContentEmpty: false });
221
+ });
222
+
223
+ it('does not re-report validity when it has not changed (dedup guard)', async () => {
224
+ const onContentValidityChange = jest.fn();
225
+ renderComponent({
226
+ actions: mockActions,
227
+ globalActions: mockGlobalActions,
228
+ editData: buildFreshEditData(),
229
+ metaEntities,
230
+ location,
231
+ getDefaultTags,
232
+ injectedTags,
233
+ accountData,
234
+ templateData: buildFreshTemplateData(),
235
+ isFullMode: true,
236
+ onContentValidityChange,
237
+ });
238
+ const inputBox = await screen.findAllByPlaceholderText(
239
+ 'Add labels or text or combination of both',
240
+ );
241
+ onContentValidityChange.mockClear();
242
+
243
+ fireEvent.change(inputBox[0], { target: { value: 'Hello' } });
244
+ expect(onContentValidityChange).toHaveBeenCalledTimes(1);
245
+ expect(onContentValidityChange).toHaveBeenCalledWith({ isContentEmpty: false });
246
+ onContentValidityChange.mockClear();
247
+
248
+ // Changing to another non-empty value keeps isContentEmpty === false, so no new report.
249
+ fireEvent.change(inputBox[0], { target: { value: 'Hello again' } });
250
+ expect(onContentValidityChange).not.toHaveBeenCalled();
251
+ });
153
252
  });
@@ -1111,6 +1111,31 @@ export default {
1111
1111
  },
1112
1112
  accountName: "WhatsappAccount",
1113
1113
  },
1114
+ // CCS sends an explicit `cards: null` (not omitted) for a non-carousel WhatsApp
1115
+ // template — regression fixture for a crash reading `.map` off null.
1116
+ whatsappGetTemplateDataNullCards: {
1117
+ channel: "WHATSAPP",
1118
+ storeType: "REGISTERED_STORE",
1119
+ accountId: 12721,
1120
+ messagePartsCount: 1,
1121
+ messageBody: "Hey test, this is a plain WhatsApp template with no carousel.",
1122
+ templateConfigs: {
1123
+ name: "creatives_whatsapp6",
1124
+ language: "en",
1125
+ varMapped: {
1126
+ "{{1}}_1": "test",
1127
+ },
1128
+ template: "Hey {{1}}, this is a plain WhatsApp template with no carousel.",
1129
+ id: "creatives_whatsapp6",
1130
+ category: "MARKETING",
1131
+ buttonType: "NONE",
1132
+ buttons: null,
1133
+ mediaType: "TEXT",
1134
+ whatsappMedia: null,
1135
+ cards: null,
1136
+ },
1137
+ accountName: "WhatsappAccount",
1138
+ },
1114
1139
  whatsappGetCreativeData1: {
1115
1140
  value: {
1116
1141
  name: "creatives_whatsapp6",
@@ -1,10 +0,0 @@
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;
@@ -1,82 +0,0 @@
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
- };
@@ -1,74 +0,0 @@
1
- /**
2
- * MOBILEPUSH editor-error descriptor — the library-mode footer echo of the ACTIVE
3
- * tab's editor error (Classic's render-phase footer push, Classic.js:2885-2895).
4
- * Mirrored rules: full mode returns null (MPUSH errors show inline); the whole echo
5
- * is gated on checkValidation (Classic computes no errorData pre-save, 334-345);
6
- * personalization restriction has its own message (2877-2879); otherwise the schema
7
- * field's errorMessage (already localized by the containers) is echoed as-is.
8
- */
9
-
10
- import { hasPersonalizationTags } from '../../../../../utils/commonUtils';
11
- import formMessages from '../../../messages';
12
- import { EDITOR_ERROR_KIND, SECTION_TYPE, CONTAINER_TYPE } from '../../constants';
13
- import { ERROR_VALUE, TABS } from './config';
14
-
15
- /** Find a leaf field by id inside one pane of the tabs container. */
16
- const findPaneField = (schema, tabIndex, fieldId) => {
17
- const container = (schema?.containers || []).find(
18
- (candidate) => candidate?.type === CONTAINER_TYPE.TABS
19
- && (candidate.isActive === undefined || candidate.isActive),
20
- );
21
- const pane = container?.panes?.[tabIndex];
22
- let found = null;
23
- const visitSection = (section) => {
24
- if (!section || found) return;
25
- if (section.type === SECTION_TYPE.PARENT) {
26
- (section.childSections || []).forEach(visitSection);
27
- } else if (section.type === SECTION_TYPE.MULTICOLS) {
28
- (section.inputFields || []).concat(section.actionFields || []).forEach((row) => {
29
- (row?.cols || []).forEach((col) => { if (col?.id === fieldId) found = col; });
30
- });
31
- } else if (section.type === SECTION_TYPE.COL_LABEL) {
32
- (section.inputFields || []).concat(section.actionFields || []).forEach((col) => {
33
- if (col?.id === fieldId) found = col;
34
- });
35
- }
36
- };
37
- (pane?.sections || []).forEach(visitSection);
38
- return found;
39
- };
40
-
41
- /**
42
- * @param {object} legacyFormData the legacy dialect ({0: {...}, 1: {...}, ...})
43
- * @param {object} ctx { isFullMode, currentTab (1-based), errorData, schema,
44
- * checkValidation, restrictPersonalization }
45
- * @returns null | { text, kind } | { message: intlDescriptor, kind }
46
- */
47
- export default function getEditorErrorDescriptor(legacyFormData = {}, ctx = {}) {
48
- if (ctx.isFullMode) return null;
49
- // Classic parity: no footer echo before a save attempt (see header).
50
- if (!ctx.checkValidation) return null;
51
-
52
- const tabIndex = Math.max(((ctx.currentTab || 1) - 1), 0);
53
- const suffix = TABS.suffixFor(tabIndex);
54
- const editorId = `message-editor${suffix}`;
55
- const editorError = ctx.errorData?.[tabIndex]?.[editorId];
56
- if (!editorError) return null;
57
-
58
- const tab = legacyFormData?.[tabIndex] || {};
59
- const content = tab[editorId];
60
-
61
- if (ctx.restrictPersonalization && content && hasPersonalizationTags(String(content))) {
62
- return {
63
- message: formMessages.personalizationTagsErrorMessage,
64
- kind: EDITOR_ERROR_KIND.MISSING,
65
- };
66
- }
67
-
68
- const field = findPaneField(ctx.schema, tabIndex, editorId);
69
- if (!field?.errorMessage) return null;
70
- return {
71
- text: field.errorMessage,
72
- kind: editorError === ERROR_VALUE.BRACKET ? EDITOR_ERROR_KIND.BRACE : EDITOR_ERROR_KIND.MISSING,
73
- };
74
- }
@@ -1,28 +0,0 @@
1
- /**
2
- * MOBILEPUSH channel adapter — the isolation unit for MPUSH. Bundles config + the
3
- * pure validate + the identity payload builder + the platform-specific submit
4
- * pipeline + the single-platform modal semantics + the channel's renderer
5
- * registry. The shell looks this up via channels/registry by `schema.channel`;
6
- * it imports nothing from other channels.
7
- */
8
- import { MOBILE_PUSH } from '../../../../../v2Containers/CreativesContainer/constants';
9
- import config from './config';
10
- import validate from './validate';
11
- import buildSubmitPayload from './buildSubmitPayload';
12
- import runSubmitPipeline from './runSubmitPipeline';
13
- import modals from './modals';
14
- import getEditorErrorDescriptor from './getEditorErrorDescriptor';
15
- import { createMpushRegistry } from '../../renderers/mpushRenderers';
16
-
17
- const mpushAdapter = {
18
- channel: MOBILE_PUSH,
19
- config,
20
- validate,
21
- buildSubmitPayload,
22
- runSubmitPipeline,
23
- modals,
24
- getEditorErrorDescriptor,
25
- createRegistry: createMpushRegistry,
26
- };
27
-
28
- export default mpushAdapter;
@@ -1,19 +0,0 @@
1
- /**
2
- * Single-platform save confirmation (Classic handleOk, Classic.js:2674-2709).
3
- * The OK button's id ('android'|'ios') is the dispatch key: confirming saves that
4
- * platform alone, but only if its validity flag is true — a falsy flag silently
5
- * closes the modal (Classic parity). Modal CONTENT stays container-owned.
6
- */
7
-
8
- const mpushModals = {
9
- handleOk: (id, { platform, submitSingleTab } = {}) => {
10
- if (!platform || typeof submitSingleTab !== 'function') return;
11
- if (id === 'android' && platform.androidValid) {
12
- submitSingleTab('android');
13
- } else if (id === 'ios' && platform.iosValid) {
14
- submitSingleTab('ios');
15
- }
16
- },
17
- };
18
-
19
- export default mpushModals;
@@ -1,45 +0,0 @@
1
- /**
2
- * MOBILEPUSH submit pipeline (Classic.js:1397-1405, 1473-1522). Full mode submits
3
- * the raw legacy formData (no liquid, no getChannelData); library mode runs
4
- * validateMobilePushContent over [androidTab, iosTab]. Classic quirks preserved:
5
- * the util's nested per-platform error objects go to the footer AS-IS, its initial
6
- * empty "clear" call still triggers stopValidation + onFormValidityChange(false),
7
- * its onSuccess contentToSubmit is IGNORED (the full formData is submitted), and
8
- * singleTab from the single-platform modal skips the other platform (1519).
9
- */
10
-
11
- import { validateMobilePushContent } from '../../../../../utils/commonUtils';
12
-
13
- export default function runSubmitPipeline(legacyFormData, ctx = {}, callbacks = {}) {
14
- const {
15
- isFullMode, singleTab, currentTab, getLiquidTags, formatMessage, messages,
16
- } = ctx;
17
- const {
18
- onSubmit, pushFooter, stopValidation, onFormValidityChange,
19
- } = callbacks;
20
-
21
- const runLiquid = !isFullMode && typeof getLiquidTags === 'function';
22
- if (!runLiquid) {
23
- if (onSubmit) onSubmit(legacyFormData);
24
- return;
25
- }
26
-
27
- validateMobilePushContent(legacyFormData, {
28
- currentTab,
29
- onError: ({ standardErrors, liquidErrors }) => {
30
- if (pushFooter) pushFooter({ STANDARD_ERROR_MSG: standardErrors, LIQUID_ERROR_MSG: liquidErrors });
31
- if (stopValidation) stopValidation();
32
- // Block save: tell the parent the form is invalid (Classic passes the last
33
- // sync errorData — the shell's callback supplies it).
34
- if (onFormValidityChange) onFormValidityChange(false);
35
- },
36
- onSuccess: () => {
37
- if (pushFooter) pushFooter({ STANDARD_ERROR_MSG: [], LIQUID_ERROR_MSG: [] });
38
- if (onSubmit) onSubmit(legacyFormData);
39
- },
40
- getLiquidTags,
41
- formatMessage,
42
- messages,
43
- singleTab: singleTab ? `${singleTab}`.toUpperCase() : undefined,
44
- });
45
- }