@capillarytech/creatives-library 9.0.59-alpha.2 → 9.0.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/constants/unified.js +2 -3
  2. package/package.json +1 -1
  3. package/services/api.js +1 -3
  4. package/utils/common.js +0 -8
  5. package/v2Components/FormBuilder/Functional/FormBuilderShell.js +69 -324
  6. package/v2Components/FormBuilder/Functional/channels/registry.js +0 -2
  7. package/v2Components/FormBuilder/Functional/constants.js +8 -46
  8. package/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js +28 -175
  9. package/v2Components/FormBuilder/Functional/core/store/formReducer.js +6 -75
  10. package/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js +20 -10
  11. package/v2Components/FormBuilder/Functional/layout/FieldSlot.js +1 -9
  12. package/v2Components/FormBuilder/Functional/layout/SchemaForm.js +3 -20
  13. package/v2Components/FormBuilder/Functional/layout/Section.js +1 -6
  14. package/v2Components/FormBuilder/Functional/tests/fieldSlot.test.js +2 -9
  15. package/v2Components/FormBuilder/Functional/tests/schemaForm.test.js +0 -17
  16. package/v2Components/FormBuilder/_formBuilder.scss +0 -11
  17. package/v2Components/FormBuilder/index.js +13 -20
  18. package/v2Components/FormBuilder/tests/entryGate.test.js +7 -87
  19. package/v2Containers/CommunicationFlow/CommunicationFlow.js +13 -32
  20. package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +10 -36
  21. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +83 -0
  22. package/v2Containers/CommunicationFlow/hooks/Tests/useFetchCommDefinition.test.js +118 -0
  23. package/v2Containers/CommunicationFlow/hooks/useFetchCommDefinition.js +49 -0
  24. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +1 -5
  25. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +28 -0
  26. package/v2Containers/CommunicationFlow/utils/Tests/mapCommDefinitionToStepData.test.js +29 -0
  27. package/v2Containers/CommunicationFlow/utils/mapCommDefinitionToStepData.js +6 -13
  28. package/v2Components/FormBuilder/Functional/channels/mobilepush/buildSubmitPayload.js +0 -10
  29. package/v2Components/FormBuilder/Functional/channels/mobilepush/config.js +0 -75
  30. package/v2Components/FormBuilder/Functional/channels/mobilepush/getEditorErrorDescriptor.js +0 -74
  31. package/v2Components/FormBuilder/Functional/channels/mobilepush/index.js +0 -28
  32. package/v2Components/FormBuilder/Functional/channels/mobilepush/modals.js +0 -21
  33. package/v2Components/FormBuilder/Functional/channels/mobilepush/runSubmitPipeline.js +0 -45
  34. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/getEditorErrorDescriptor.test.js +0 -162
  35. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/modals.test.js +0 -37
  36. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/runSubmitPipeline.test.js +0 -70
  37. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/validate.test.js +0 -274
  38. package/v2Components/FormBuilder/Functional/channels/mobilepush/validate.js +0 -251
  39. package/v2Components/FormBuilder/Functional/layout/TabsContainer.js +0 -85
  40. package/v2Components/FormBuilder/Functional/renderers/mpushRenderers.js +0 -448
  41. package/v2Components/FormBuilder/Functional/tests/manual.typingPerf.test.js +0 -135
  42. package/v2Components/FormBuilder/Functional/tests/mpush.crossFlowParity.test.js +0 -430
  43. package/v2Components/FormBuilder/Functional/tests/mpush.ctaFlows.parity.test.js +0 -320
  44. package/v2Components/FormBuilder/Functional/tests/mpush.editContainer.parity.test.js +0 -148
  45. package/v2Components/FormBuilder/Functional/tests/mpush.engine.test.js +0 -306
  46. package/v2Components/FormBuilder/Functional/tests/mpush.libraryValidity.parity.test.js +0 -147
  47. package/v2Components/FormBuilder/Functional/tests/mpush.shellEvents.test.js +0 -200
  48. package/v2Components/FormBuilder/Functional/tests/mpushRenderers.test.js +0 -376
  49. package/v2Components/FormBuilder/Functional/tests/tabsContainer.test.js +0 -110
  50. package/v2Components/FormBuilder/tests/mpush.characterization.test.js +0 -459
@@ -115,6 +115,35 @@ describe('mapCommDefinitionToStepData', () => {
115
115
  });
116
116
  });
117
117
 
118
+ it('maps a WEBPUSH CommDefinition using the dedicated reverse transform (nested messageContent.content shape)', () => {
119
+ const commDefinition = {
120
+ id: 'cd_webpush_1',
121
+ singleChannelStrategy: {
122
+ variant: {
123
+ channel: 'WEBPUSH',
124
+ webPushMessageContent: {
125
+ messageSubject: 'New offer',
126
+ accountId: 'acc_1',
127
+ content: { title: 'Title', body: 'Body text' },
128
+ },
129
+ },
130
+ },
131
+ };
132
+
133
+ const result = mapCommDefinitionToStepData(commDefinition);
134
+
135
+ expect(result.contentItems[0].templateData).toEqual({
136
+ messageContent: {
137
+ content: {
138
+ messageSubject: 'New offer',
139
+ accountId: 'acc_1',
140
+ content: { title: 'Title', body: 'Body text' },
141
+ },
142
+ },
143
+ channel: 'WEBPUSH',
144
+ });
145
+ });
146
+
118
147
  it('reads the variant from commDefinition.version when top-level singleChannelStrategy is absent', () => {
119
148
  const commDefinition = {
120
149
  id: 'cd_4',
@@ -22,9 +22,7 @@ const CCS_TO_UI_CHANNEL_MAP = Object.entries(CCS_CHANNEL_NAME_MAP).reduce(
22
22
  );
23
23
  const reverseUiChannel = (ccsChannel) => CCS_TO_UI_CHANNEL_MAP[ccsChannel] || ccsChannel;
24
24
 
25
- // Inverse of CCS_CONTENT_TRANSFORMS (./constants.js) only channels with a dedicated forward
26
- // transform there need one here; every other channel's CCS content already matches the legacy
27
- // editor's templateData shape and is passed through unchanged (see the default branch below).
25
+ // Inverse of CCS_CONTENT_TRANSFORMS — transforms CCS content back to the legacy editor's templateData shape.
28
26
  const CCS_CONTENT_REVERSE_TRANSFORMS = {
29
27
  EMAIL: (payload = {}) => ({
30
28
  emailSubject: payload.messageSubject,
@@ -50,8 +48,7 @@ const getCommDefinitionVariant = (commDefinition = {}) => (
50
48
  || null
51
49
  );
52
50
 
53
- // Counts SUBMITTED audit entries to derive the latest version number when the CommDefinition
54
- // itself doesn't carry an explicit current version (mirrors cap-campaigns-v2's own helper).
51
+ // Counts SUBMITTED audit entries to derive the latest version when CommDefinition has no explicit current version.
55
52
  export const getLatestVersionFromAuditInfos = (commDefinition = {}) => {
56
53
  const submittedCount = (commDefinition.auditInfos || []).filter(
57
54
  (entry) => entry?.action === 'SUBMITTED',
@@ -60,8 +57,7 @@ export const getLatestVersionFromAuditInfos = (commDefinition = {}) => {
60
57
  return submittedCount - 1;
61
58
  };
62
59
 
63
- // Reverse of CommunicationFlow's own additionalSettings mapping (see CommunicationFlow.js's
64
- // handleSave) — used to pre-populate the Advanced Controls step when editing existing content.
60
+ // Reverse-maps CommunicationFlow's additionalSettings to pre-populate Advanced Controls when editing.
65
61
  const mapAdditionalSettingsToDynamicControls = (additionalSettings = {}) => ({
66
62
  useTinyUrl: additionalSettings.useTinyUrl ?? false,
67
63
  sendToControlCustomers: additionalSettings.encryptUrl ?? false,
@@ -86,8 +82,7 @@ export const mapCommDefinitionToStepData = (commDefinition = {}) => {
86
82
 
87
83
  const uiChannel = reverseUiChannel(ccsChannel);
88
84
  const reverseTransform = CCS_CONTENT_REVERSE_TRANSFORMS[ccsChannel];
89
- // Spread channel after content: CCS content may carry its own raw `channel` ("PUSH"), which
90
- // would otherwise overwrite `uiChannel` ("MOBILEPUSH") and break template rendering.
85
+ // Spread channel after content so CCS's raw `channel` ("PUSH") cannot overwrite `uiChannel` ("MOBILEPUSH").
91
86
  const templateData = reverseTransform
92
87
  ? { ...reverseTransform(rawContent), channel: uiChannel }
93
88
  : { ...rawContent, channel: uiChannel };
@@ -98,14 +93,12 @@ export const mapCommDefinitionToStepData = (commDefinition = {}) => {
98
93
  const version = commDefinition.version || {};
99
94
 
100
95
  return {
101
- // CCS create/edit only supports the SINGLE strategy today (see CommunicationFlow.js's
102
- // handleSave, which skips CHANNEL_PRIORITY/AB_TEST) — a fetched CommDefinition is always one.
96
+ // CCS create/edit currently supports only SINGLE strategy; fetched CommDefinitions are always single-strategy.
103
97
  messageType: 'transactional',
104
98
  communicationStrategy: 'SINGLE_TEMPLATE',
105
99
  channel: uiChannel,
106
100
  channels: [],
107
- // contentId must be truthy ChannelSelectionStep matches contentItems by it to resolve
108
- // editingContentId; a falsy id collapses the reopen back to an empty template picker.
101
+ // contentId must be truthy so ChannelSelectionStep can resolve editingContentId instead of reopening the empty template picker.
109
102
  contentItems: [
110
103
  { contentId: commDefinition.id, channel: uiChannel, templateData },
111
104
  ],
@@ -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,75 +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 field passes); kind: 'url' => isValidExternalLink semantics.
36
- // Named fieldId (not `key:`) so gitleaks' generic-api-key rule stops matching.
37
- export const CTA_RULES = [
38
- { fieldId: 'cta-deeplink-select', kind: 'empty' },
39
- { fieldId: 'cta-deeplink2-select', kind: 'empty' },
40
- { fieldId: 'cta-deeplink-text', kind: 'url' },
41
- { fieldId: 'cta-deeplink2-text', kind: 'url' },
42
- { fieldId: 'secondary-cta-0-link', kind: 'empty' },
43
- { fieldId: 'secondary-cta-0-label', kind: 'empty' },
44
- { fieldId: 'cta-deeplink-secondary-cta-0-text', kind: 'url' },
45
- { fieldId: 'cta-deeplink-secondary-cta-0-select', kind: 'empty' },
46
- { fieldId: 'secondary-cta-1-label', kind: 'empty' },
47
- { fieldId: 'cta-deeplink-secondary-cta-1-text', kind: 'url' },
48
- { fieldId: 'cta-deeplink-secondary-cta-1-select', kind: 'empty' },
49
- { fieldId: 'cta-deeplink-secondary-cta-1-text2', kind: 'url' },
50
- { fieldId: 'cta-deeplink-secondary-cta-1-select2', kind: 'empty' },
51
- ];
52
-
53
- export default {
54
- channel: MOBILE_PUSH,
55
- fieldIds,
56
- // Classic passes the live 1-based currentTab as showLiquidErrorInFooter's second
57
- // argument for MPUSH (Classic.js:1352, 1487); SMS is the only null channel.
58
- footerTabArg: 'currentTab',
59
- features: {
60
- tabs: true,
61
- versions: false,
62
- languages: false,
63
- unicode: false,
64
- liquid: true, // LIQUID_SUPPORTED_CHANNELS member; pipeline runs only outside isFullMode
65
- askAira: true, // textarea only (Classic.js:2927)
66
- preview: true,
67
- imageUpload: true,
68
- cta: true,
69
- bridgeFieldEvents: true, // checkbox/radio/select commits use the performFormDataUpdate signature (true, formData, field)
70
- inlineErrorWithLiquid: true, // MPUSH keeps the inline textarea error despite liquid (Classic.js:2908-2915)
71
- liveValidation: false, // Classic re-validates typing only under startValidation (Classic.js:334-345)
72
- validateOnExternalChange: true, // background validity on parent/schema/tags changes gates the save flows (Classic.js:516-578, 1361)
73
- emitFormDataOnSchemaSync: true, // push formData up on init + schema re-inits; copy-content handlers read it (Classic.js:1906-1911)
74
- },
75
- };
@@ -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,21 +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
- import { ANDROID, IOS } from '../../../../../constants/unified';
9
-
10
- const mpushModals = {
11
- handleOk: (id, { platform, submitSingleTab } = {}) => {
12
- if (!platform || typeof submitSingleTab !== 'function') return;
13
- if (id === ANDROID && platform.androidValid) {
14
- submitSingleTab(ANDROID);
15
- } else if (id === IOS && platform.iosValid) {
16
- submitSingleTab(IOS);
17
- }
18
- },
19
- };
20
-
21
- 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
- }
@@ -1,162 +0,0 @@
1
- /**
2
- * MPUSH getEditorErrorDescriptor — the library-mode footer echo of the active
3
- * tab's editor error (mirror of Classic's render-phase push, Classic.js:2885-2895).
4
- */
5
- import getEditorErrorDescriptor from '../getEditorErrorDescriptor';
6
- import { ERROR_VALUE } from '../config';
7
- import { EDITOR_ERROR_KIND } from '../../../constants';
8
- import formMessages from '../../../../messages';
9
-
10
- const EDITOR_ERROR_TEXT = 'Template message has unsupported/missing tags!';
11
-
12
- const schemaWithEditor = (fieldId = 'message-editor', paneIndex = 0) => {
13
- const panes = [{ sections: [] }, { sections: [] }];
14
- panes[paneIndex] = {
15
- sections: [{
16
- type: 'multicols',
17
- inputFields: [{ cols: [{ id: fieldId, type: 'textarea', errorMessage: EDITOR_ERROR_TEXT }] }],
18
- }],
19
- };
20
- return { containers: [{ id: 'pane', type: 'tabs', panes }] };
21
- };
22
-
23
- // Same editor field, but reached through parent -> col-label sections (the other
24
- // findPaneField walk branches).
25
- const colLabelSchema = (fieldId = 'message-editor') => ({
26
- containers: [{
27
- id: 'pane',
28
- type: 'tabs',
29
- panes: [{
30
- sections: [{
31
- type: 'parent',
32
- childSections: [{
33
- type: 'col-label',
34
- inputFields: [{ id: fieldId, type: 'textarea', errorMessage: EDITOR_ERROR_TEXT }],
35
- }],
36
- }],
37
- }, { sections: [] }],
38
- }],
39
- });
40
-
41
- const legacyWith = (content, tabIndex = 0, fieldId = 'message-editor') => ({
42
- [tabIndex]: { [fieldId]: content, tabKey: `k${tabIndex}` },
43
- });
44
-
45
- const baseCtx = (overrides = {}) => ({
46
- isFullMode: false,
47
- currentTab: 1,
48
- schema: schemaWithEditor(),
49
- checkValidation: true,
50
- restrictPersonalization: false,
51
- ...overrides,
52
- });
53
-
54
- describe('channels/mobilepush/getEditorErrorDescriptor', () => {
55
- it('returns null in full mode (MPUSH errors show inline there, never as an inline-brace override)', () => {
56
- const ctx = baseCtx({ isFullMode: true, errorData: { 0: { 'message-editor': true } } });
57
- expect(getEditorErrorDescriptor(legacyWith('abc'), ctx)).toBeNull();
58
- });
59
-
60
- it('returns null when the active tab has no editor error', () => {
61
- const ctx = baseCtx({ errorData: { 0: { 'message-editor': false } } });
62
- expect(getEditorErrorDescriptor(legacyWith('abc'), ctx)).toBeNull();
63
- });
64
-
65
- it('echoes the schema errorMessage for a generic editor error', () => {
66
- const ctx = baseCtx({ errorData: { 0: { 'message-editor': true } } });
67
- expect(getEditorErrorDescriptor(legacyWith('{{unknown}}'), ctx)).toEqual({
68
- text: EDITOR_ERROR_TEXT,
69
- kind: EDITOR_ERROR_KIND.MISSING,
70
- });
71
- });
72
-
73
- it('finds the editor field through parent -> col-label pane sections too', () => {
74
- const ctx = baseCtx({ schema: colLabelSchema(), errorData: { 0: { 'message-editor': true } } });
75
- expect(getEditorErrorDescriptor(legacyWith('{{unknown}}'), ctx)).toEqual({
76
- text: EDITOR_ERROR_TEXT,
77
- kind: EDITOR_ERROR_KIND.MISSING,
78
- });
79
- });
80
-
81
- it('defaults: no arguments => null; missing currentTab resolves to tab 0', () => {
82
- expect(getEditorErrorDescriptor()).toBeNull();
83
- const ctx = baseCtx({ errorData: { 0: { 'message-editor': true } } });
84
- delete ctx.currentTab;
85
- expect(getEditorErrorDescriptor(legacyWith('abc'), ctx)).toEqual({
86
- text: EDITOR_ERROR_TEXT,
87
- kind: EDITOR_ERROR_KIND.MISSING,
88
- });
89
- });
90
-
91
- it('an inactive tabs container hides the field => no echo (null)', () => {
92
- const schema = schemaWithEditor();
93
- schema.containers[0].isActive = false;
94
- const ctx = baseCtx({ schema, errorData: { 0: { 'message-editor': true } } });
95
- expect(getEditorErrorDescriptor(legacyWith('abc'), ctx)).toBeNull();
96
- });
97
-
98
- it('also locates the editor field among a multicols row\'s actionFields', () => {
99
- const schema = schemaWithEditor();
100
- const section = schema.containers[0].panes[0].sections[0];
101
- section.actionFields = section.inputFields;
102
- section.inputFields = [];
103
- const ctx = baseCtx({ schema, errorData: { 0: { 'message-editor': true } } });
104
- expect(getEditorErrorDescriptor(legacyWith('abc'), ctx)).toEqual({
105
- text: EDITOR_ERROR_TEXT,
106
- kind: EDITOR_ERROR_KIND.MISSING,
107
- });
108
- });
109
-
110
- it('marks a brace error with the BRACE kind', () => {
111
- const ctx = baseCtx({ errorData: { 0: { 'message-editor': ERROR_VALUE.BRACKET } } });
112
- expect(getEditorErrorDescriptor(legacyWith('abc {{full_name'), ctx)).toEqual({
113
- text: EDITOR_ERROR_TEXT,
114
- kind: EDITOR_ERROR_KIND.BRACE,
115
- });
116
- });
117
-
118
- it('gates EVERY error on checkValidation — no footer echo before a save attempt (Classic parity: errorData is only computed under startValidation, Classic.js:334-345)', () => {
119
- const generic = { 0: { 'message-editor': true } };
120
- expect(getEditorErrorDescriptor(legacyWith('{{unknown}}'), baseCtx({ errorData: generic, checkValidation: false }))).toBeNull();
121
- const brace = { 0: { 'message-editor': ERROR_VALUE.BRACKET } };
122
- expect(getEditorErrorDescriptor(legacyWith('abc {{full_name'), baseCtx({ errorData: brace, checkValidation: false }))).toBeNull();
123
- });
124
-
125
- it('gates the EMPTY-content error on checkValidation (hidden until Save, like the inline message)', () => {
126
- const errorData = { 0: { 'message-editor': true } };
127
- expect(getEditorErrorDescriptor(legacyWith(''), baseCtx({ errorData, checkValidation: false }))).toBeNull();
128
- expect(getEditorErrorDescriptor(legacyWith(' '), baseCtx({ errorData, checkValidation: false }))).toBeNull();
129
- expect(getEditorErrorDescriptor(legacyWith(''), baseCtx({ errorData, checkValidation: true }))).toEqual({
130
- text: EDITOR_ERROR_TEXT,
131
- kind: EDITOR_ERROR_KIND.MISSING,
132
- });
133
- });
134
-
135
- it('returns the personalization intl descriptor when restricted tokens are present', () => {
136
- const ctx = baseCtx({
137
- restrictPersonalization: true,
138
- errorData: { 0: { 'message-editor': true } },
139
- });
140
- expect(getEditorErrorDescriptor(legacyWith('hi [eventAttr]'), ctx)).toEqual({
141
- message: formMessages.personalizationTagsErrorMessage,
142
- kind: EDITOR_ERROR_KIND.MISSING,
143
- });
144
- });
145
-
146
- it('resolves the iOS tab through the suffixed field id (currentTab=2 => message-editor2)', () => {
147
- const ctx = baseCtx({
148
- currentTab: 2,
149
- schema: schemaWithEditor('message-editor2', 1),
150
- errorData: { 1: { 'message-editor2': true } },
151
- });
152
- expect(getEditorErrorDescriptor(legacyWith('{{x}}', 1, 'message-editor2'), ctx)).toEqual({
153
- text: EDITOR_ERROR_TEXT,
154
- kind: EDITOR_ERROR_KIND.MISSING,
155
- });
156
- });
157
-
158
- it('returns null when the schema pane has no errorMessage to echo', () => {
159
- const ctx = baseCtx({ schema: { containers: [] }, errorData: { 0: { 'message-editor': true } } });
160
- expect(getEditorErrorDescriptor(legacyWith('abc'), ctx)).toBeNull();
161
- });
162
- });
@@ -1,37 +0,0 @@
1
- /**
2
- * MPUSH modal semantics — Classic's single-platform handleOk (Classic.js:2674-2709):
3
- * the OK id dispatches a single-platform save only when that platform's last
4
- * validation flag is true; anything else silently closes the modal.
5
- */
6
- import mpushModals from '../modals';
7
-
8
- describe('channels/mobilepush/modals.handleOk', () => {
9
- const run = (id, platform) => {
10
- const submitSingleTab = jest.fn();
11
- mpushModals.handleOk(id, { platform, submitSingleTab });
12
- return submitSingleTab;
13
- };
14
-
15
- it('android OK + androidValid => saves android alone', () => {
16
- expect(run('android', { androidValid: true, iosValid: false }))
17
- .toHaveBeenCalledWith('android');
18
- });
19
-
20
- it('ios OK + iosValid => saves ios alone', () => {
21
- expect(run('ios', { androidValid: false, iosValid: true }))
22
- .toHaveBeenCalledWith('ios');
23
- });
24
-
25
- it('a falsy platform flag silently closes (no submit) — Classic parity', () => {
26
- expect(run('android', { androidValid: false, iosValid: true })).not.toHaveBeenCalled();
27
- expect(run('ios', { androidValid: true, iosValid: false })).not.toHaveBeenCalled();
28
- });
29
-
30
- it('unknown id / missing platform / missing callback are all no-ops', () => {
31
- expect(run('template-back-confirm-modal', { androidValid: true, iosValid: true }))
32
- .not.toHaveBeenCalled();
33
- expect(run('android', null)).not.toHaveBeenCalled();
34
- expect(() => mpushModals.handleOk('android', { platform: { androidValid: true } })).not.toThrow();
35
- expect(() => mpushModals.handleOk('android')).not.toThrow();
36
- });
37
- });
@@ -1,70 +0,0 @@
1
- /**
2
- * MPUSH submit pipeline — Classic's onSubmitWrapper short-circuit +
3
- * validateFormBuilderMPush contract (Classic.js:1397-1405, 1473-1522).
4
- */
5
- import runSubmitPipeline from '../runSubmitPipeline';
6
- import { validateMobilePushContent } from '../../../../../../utils/commonUtils';
7
-
8
- jest.mock('../../../../../../utils/commonUtils', () => ({
9
- validateMobilePushContent: jest.fn(),
10
- }));
11
-
12
- const legacy = { 'template-name': 'p', 0: { 'message-editor': 'a' }, 1: {} };
13
-
14
- const buildCallbacks = () => ({
15
- onSubmit: jest.fn(),
16
- pushFooter: jest.fn(),
17
- stopValidation: jest.fn(),
18
- onFormValidityChange: jest.fn(),
19
- });
20
-
21
- describe('channels/mobilepush/runSubmitPipeline', () => {
22
- beforeEach(() => jest.clearAllMocks());
23
-
24
- it('full mode submits the raw legacy formData directly — no liquid validation', () => {
25
- const callbacks = buildCallbacks();
26
- runSubmitPipeline(legacy, { isFullMode: true, getLiquidTags: jest.fn() }, callbacks);
27
- expect(callbacks.onSubmit).toHaveBeenCalledWith(legacy);
28
- expect(validateMobilePushContent).not.toHaveBeenCalled();
29
- });
30
-
31
- it('library mode without getLiquidTags also submits directly', () => {
32
- const callbacks = buildCallbacks();
33
- runSubmitPipeline(legacy, { isFullMode: false }, callbacks);
34
- expect(callbacks.onSubmit).toHaveBeenCalledWith(legacy);
35
- expect(validateMobilePushContent).not.toHaveBeenCalled();
36
- });
37
-
38
- it('library mode runs the liquid pipeline; singleTab is upper-cased; success submits the FULL formData', () => {
39
- const callbacks = buildCallbacks();
40
- runSubmitPipeline(legacy, {
41
- isFullMode: false, getLiquidTags: jest.fn(), singleTab: 'android', currentTab: 1,
42
- }, callbacks);
43
- const options = validateMobilePushContent.mock.calls[0][1];
44
- expect(options.singleTab).toBe('ANDROID');
45
- options.onSuccess('{"ignored":"single-tab json"}', 'android');
46
- expect(callbacks.onSubmit).toHaveBeenCalledWith(legacy); // contentToSubmit IGNORED
47
- expect(callbacks.pushFooter).toHaveBeenCalledWith({ STANDARD_ERROR_MSG: [], LIQUID_ERROR_MSG: [] });
48
- });
49
-
50
- it('error callback forwards the nested per-platform objects AS-IS, stops validation, and blocks the save', () => {
51
- const callbacks = buildCallbacks();
52
- runSubmitPipeline(legacy, { isFullMode: false, getLiquidTags: jest.fn() }, callbacks);
53
- const options = validateMobilePushContent.mock.calls[0][1];
54
- const standardErrors = { ANDROID: ['bad tag'], IOS: [], generic: [] };
55
- options.onError({ standardErrors, liquidErrors: [] });
56
- expect(callbacks.pushFooter).toHaveBeenCalledWith({ STANDARD_ERROR_MSG: standardErrors, LIQUID_ERROR_MSG: [] });
57
- expect(callbacks.stopValidation).toHaveBeenCalled();
58
- expect(callbacks.onFormValidityChange).toHaveBeenCalledWith(false);
59
- expect(callbacks.onSubmit).not.toHaveBeenCalled();
60
- expect(options.singleTab).toBeUndefined(); // no modal => both platforms validate
61
- });
62
-
63
- it('tolerates missing callbacks (every callback is optional)', () => {
64
- runSubmitPipeline(legacy, { isFullMode: false, getLiquidTags: jest.fn() }, {});
65
- const options = validateMobilePushContent.mock.calls[0][1];
66
- expect(() => options.onError({ standardErrors: [], liquidErrors: [] })).not.toThrow();
67
- expect(() => options.onSuccess('x')).not.toThrow();
68
- expect(() => runSubmitPipeline(legacy, { isFullMode: true }, {})).not.toThrow();
69
- });
70
- });