@capillarytech/creatives-library 9.0.61-alpha.3 → 9.0.62
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 +8 -2
- package/package.json +1 -1
- package/services/api.js +5 -0
- package/services/tests/api.test.js +44 -0
- package/utils/common.js +8 -0
- package/utils/downloadFile.js +57 -0
- package/utils/tests/downloadFile.test.js +219 -0
- package/v2Components/FormBuilder/Classic.js +43 -9
- package/v2Components/FormBuilder/Functional/FormBuilderShell.js +324 -69
- package/v2Components/FormBuilder/Functional/channels/mobilepush/buildSubmitPayload.js +10 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/config.js +75 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/getEditorErrorDescriptor.js +74 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/index.js +28 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/modals.js +21 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/runSubmitPipeline.js +45 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/getEditorErrorDescriptor.test.js +162 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/modals.test.js +37 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/runSubmitPipeline.test.js +70 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/validate.test.js +274 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/validate.js +251 -0
- package/v2Components/FormBuilder/Functional/channels/registry.js +2 -0
- package/v2Components/FormBuilder/Functional/constants.js +46 -8
- package/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js +175 -28
- package/v2Components/FormBuilder/Functional/core/store/formReducer.js +75 -6
- package/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js +10 -20
- package/v2Components/FormBuilder/Functional/layout/FieldSlot.js +9 -1
- package/v2Components/FormBuilder/Functional/layout/SchemaForm.js +20 -3
- package/v2Components/FormBuilder/Functional/layout/Section.js +6 -1
- package/v2Components/FormBuilder/Functional/layout/TabsContainer.js +85 -0
- package/v2Components/FormBuilder/Functional/renderers/mpushRenderers.js +451 -0
- package/v2Components/FormBuilder/Functional/tests/fieldSlot.test.js +9 -2
- package/v2Components/FormBuilder/Functional/tests/manual.typingPerf.test.js +135 -0
- package/v2Components/FormBuilder/Functional/tests/mpush.crossFlowParity.test.js +430 -0
- package/v2Components/FormBuilder/Functional/tests/mpush.ctaFlows.parity.test.js +320 -0
- package/v2Components/FormBuilder/Functional/tests/mpush.editContainer.parity.test.js +148 -0
- package/v2Components/FormBuilder/Functional/tests/mpush.engine.test.js +306 -0
- package/v2Components/FormBuilder/Functional/tests/mpush.libraryValidity.parity.test.js +152 -0
- package/v2Components/FormBuilder/Functional/tests/mpush.shellEvents.test.js +200 -0
- package/v2Components/FormBuilder/Functional/tests/mpushRenderers.test.js +382 -0
- package/v2Components/FormBuilder/Functional/tests/schemaForm.test.js +17 -0
- package/v2Components/FormBuilder/Functional/tests/tabsContainer.test.js +112 -0
- package/v2Components/FormBuilder/_formBuilder.scss +11 -0
- package/v2Components/FormBuilder/index.js +27 -17
- package/v2Components/FormBuilder/tests/entryGate.test.js +90 -7
- package/v2Components/FormBuilder/tests/mpush.characterization.test.js +459 -0
- package/v2Containers/BeeEditor/index.js +8 -0
- package/v2Containers/Email/index.js +118 -16
- package/v2Containers/Email/initialSchema.js +25 -3
- package/v2Containers/Email/messages.js +16 -0
- package/v2Containers/Email/tests/index.test.js +597 -0
- package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +55 -1
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +141 -0
- package/v2Containers/EmailWrapper/components/_emailHTMLEditor.scss +11 -0
- package/v2Containers/Templates/index.js +53 -0
- package/v2Containers/Templates/messages.js +8 -0
- package/v2Containers/Templates/tests/index.test.js +170 -0
- package/v2Containers/Viber/constants.js +0 -3
- package/v2Containers/Viber/sagas.js +3 -1
- package/v2Containers/Viber/tests/saga.test.js +21 -0
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
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;
|
|
@@ -0,0 +1,21 @@
|
|
|
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;
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
});
|