@capillarytech/creatives-library 9.0.59-alpha.2 → 9.0.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/constants/unified.js +2 -3
- package/package.json +1 -1
- package/services/api.js +1 -3
- package/utils/common.js +0 -8
- package/v2Components/FormBuilder/Functional/FormBuilderShell.js +69 -324
- package/v2Components/FormBuilder/Functional/channels/registry.js +0 -2
- package/v2Components/FormBuilder/Functional/constants.js +8 -46
- package/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js +28 -175
- package/v2Components/FormBuilder/Functional/core/store/formReducer.js +6 -75
- package/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js +20 -10
- package/v2Components/FormBuilder/Functional/layout/FieldSlot.js +1 -9
- package/v2Components/FormBuilder/Functional/layout/SchemaForm.js +3 -20
- package/v2Components/FormBuilder/Functional/layout/Section.js +1 -6
- package/v2Components/FormBuilder/Functional/tests/fieldSlot.test.js +2 -9
- package/v2Components/FormBuilder/Functional/tests/schemaForm.test.js +0 -17
- package/v2Components/FormBuilder/_formBuilder.scss +0 -11
- package/v2Components/FormBuilder/index.js +13 -20
- package/v2Components/FormBuilder/tests/entryGate.test.js +7 -87
- package/v2Containers/CommunicationFlow/CommunicationFlow.js +13 -32
- package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +10 -36
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +83 -0
- package/v2Containers/CommunicationFlow/hooks/Tests/useFetchCommDefinition.test.js +118 -0
- package/v2Containers/CommunicationFlow/hooks/useFetchCommDefinition.js +49 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +1 -5
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +28 -0
- package/v2Containers/CommunicationFlow/utils/Tests/mapCommDefinitionToStepData.test.js +29 -0
- package/v2Containers/CommunicationFlow/utils/mapCommDefinitionToStepData.js +6 -13
- package/v2Components/FormBuilder/Functional/channels/mobilepush/buildSubmitPayload.js +0 -10
- package/v2Components/FormBuilder/Functional/channels/mobilepush/config.js +0 -75
- package/v2Components/FormBuilder/Functional/channels/mobilepush/getEditorErrorDescriptor.js +0 -74
- package/v2Components/FormBuilder/Functional/channels/mobilepush/index.js +0 -28
- package/v2Components/FormBuilder/Functional/channels/mobilepush/modals.js +0 -21
- package/v2Components/FormBuilder/Functional/channels/mobilepush/runSubmitPipeline.js +0 -45
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/getEditorErrorDescriptor.test.js +0 -162
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/modals.test.js +0 -37
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/runSubmitPipeline.test.js +0 -70
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/validate.test.js +0 -274
- package/v2Components/FormBuilder/Functional/channels/mobilepush/validate.js +0 -251
- package/v2Components/FormBuilder/Functional/layout/TabsContainer.js +0 -85
- package/v2Components/FormBuilder/Functional/renderers/mpushRenderers.js +0 -448
- package/v2Components/FormBuilder/Functional/tests/manual.typingPerf.test.js +0 -135
- package/v2Components/FormBuilder/Functional/tests/mpush.crossFlowParity.test.js +0 -430
- package/v2Components/FormBuilder/Functional/tests/mpush.ctaFlows.parity.test.js +0 -320
- package/v2Components/FormBuilder/Functional/tests/mpush.editContainer.parity.test.js +0 -148
- package/v2Components/FormBuilder/Functional/tests/mpush.engine.test.js +0 -306
- package/v2Components/FormBuilder/Functional/tests/mpush.libraryValidity.parity.test.js +0 -147
- package/v2Components/FormBuilder/Functional/tests/mpush.shellEvents.test.js +0 -200
- package/v2Components/FormBuilder/Functional/tests/mpushRenderers.test.js +0 -376
- package/v2Components/FormBuilder/Functional/tests/tabsContainer.test.js +0 -110
- package/v2Components/FormBuilder/tests/mpush.characterization.test.js +0 -459
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
});
|
package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/runSubmitPipeline.test.js
DELETED
|
@@ -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
|
-
});
|