@capillarytech/creatives-library 9.0.59 → 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 (44) hide show
  1. package/constants/unified.js +2 -3
  2. package/package.json +1 -1
  3. package/utils/common.js +0 -8
  4. package/v2Components/FormBuilder/Functional/FormBuilderShell.js +69 -324
  5. package/v2Components/FormBuilder/Functional/channels/registry.js +0 -2
  6. package/v2Components/FormBuilder/Functional/constants.js +8 -46
  7. package/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js +28 -175
  8. package/v2Components/FormBuilder/Functional/core/store/formReducer.js +6 -75
  9. package/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js +20 -10
  10. package/v2Components/FormBuilder/Functional/layout/FieldSlot.js +1 -9
  11. package/v2Components/FormBuilder/Functional/layout/SchemaForm.js +3 -20
  12. package/v2Components/FormBuilder/Functional/layout/Section.js +1 -6
  13. package/v2Components/FormBuilder/Functional/tests/fieldSlot.test.js +2 -9
  14. package/v2Components/FormBuilder/Functional/tests/schemaForm.test.js +0 -17
  15. package/v2Components/FormBuilder/_formBuilder.scss +0 -11
  16. package/v2Components/FormBuilder/index.js +13 -20
  17. package/v2Components/FormBuilder/tests/entryGate.test.js +7 -87
  18. package/v2Containers/CommunicationFlow/CommunicationFlow.js +12 -25
  19. package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +9 -28
  20. package/v2Containers/CommunicationFlow/hooks/Tests/useFetchCommDefinition.test.js +118 -0
  21. package/v2Containers/CommunicationFlow/hooks/useFetchCommDefinition.js +49 -0
  22. package/v2Components/FormBuilder/Functional/channels/mobilepush/buildSubmitPayload.js +0 -10
  23. package/v2Components/FormBuilder/Functional/channels/mobilepush/config.js +0 -75
  24. package/v2Components/FormBuilder/Functional/channels/mobilepush/getEditorErrorDescriptor.js +0 -74
  25. package/v2Components/FormBuilder/Functional/channels/mobilepush/index.js +0 -28
  26. package/v2Components/FormBuilder/Functional/channels/mobilepush/modals.js +0 -21
  27. package/v2Components/FormBuilder/Functional/channels/mobilepush/runSubmitPipeline.js +0 -45
  28. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/getEditorErrorDescriptor.test.js +0 -162
  29. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/modals.test.js +0 -37
  30. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/runSubmitPipeline.test.js +0 -70
  31. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/validate.test.js +0 -274
  32. package/v2Components/FormBuilder/Functional/channels/mobilepush/validate.js +0 -251
  33. package/v2Components/FormBuilder/Functional/layout/TabsContainer.js +0 -85
  34. package/v2Components/FormBuilder/Functional/renderers/mpushRenderers.js +0 -448
  35. package/v2Components/FormBuilder/Functional/tests/manual.typingPerf.test.js +0 -135
  36. package/v2Components/FormBuilder/Functional/tests/mpush.crossFlowParity.test.js +0 -430
  37. package/v2Components/FormBuilder/Functional/tests/mpush.ctaFlows.parity.test.js +0 -320
  38. package/v2Components/FormBuilder/Functional/tests/mpush.editContainer.parity.test.js +0 -148
  39. package/v2Components/FormBuilder/Functional/tests/mpush.engine.test.js +0 -306
  40. package/v2Components/FormBuilder/Functional/tests/mpush.libraryValidity.parity.test.js +0 -147
  41. package/v2Components/FormBuilder/Functional/tests/mpush.shellEvents.test.js +0 -200
  42. package/v2Components/FormBuilder/Functional/tests/mpushRenderers.test.js +0 -376
  43. package/v2Components/FormBuilder/Functional/tests/tabsContainer.test.js +0 -110
  44. package/v2Components/FormBuilder/tests/mpush.characterization.test.js +0 -459
@@ -1,251 +0,0 @@
1
- /**
2
- * MOBILEPUSH validation — line-faithful port of Classic's validateForm MPUSH block
3
- * (Classic.js:935-1218) over the legacy formData; emitted errorData is byte-identical.
4
- * Every oddity below is a deliberate Classic quirk pinned by the characterization
5
- * suite — see FormBuilder-V3-MPUSH-Technical-Design.md §6.1 before "fixing" any.
6
- */
7
-
8
- import { validateTagsCore } from '../../../../../utils/tagValidations';
9
- import { hasPersonalizationTags } from '../../../../../utils/commonUtils';
10
- import { isUrl } from '../../../../../v2Containers/Line/Container/Wrapper/utils';
11
- import { DEFAULT as DEFAULT_MODULE, ANDROID, IOS } from '../../../../../constants/unified';
12
- import { TEMPLATE_NAME_FIELD } from '../../constants';
13
- import { ERROR_VALUE, TABS, CTA_RULES } from './config';
14
-
15
- const TAB_KEY_REGEX = /^\d+$/;
16
- const isTabKey = (key) => TAB_KEY_REGEX.test(`${key}`);
17
-
18
- // Tab metadata keys skipped when seeding a tab's error map (Classic.js:1627-1656).
19
- const ERROR_SEED_SKIP_KEYS = ['tabKey', 'base', 'selectedLanguages'];
20
-
21
- /** Truthy-value projection of a tab, excluding the given metadata keys. */
22
- const pickTruthy = (tab, excluded) => {
23
- const out = {};
24
- Object.keys(tab || {}).forEach((key) => {
25
- if (tab[key] && !excluded.includes(key)) out[key] = tab[key];
26
- });
27
- return out;
28
- };
29
-
30
- const isEmptyObject = (obj) => !obj || Object.keys(obj).length === 0;
31
-
32
- // Classic.js:752-758 — key absent => valid; present but empty/invalid URL => invalid.
33
- export const isValidExternalLink = (tab, key) => {
34
- if (key in (tab || {})) {
35
- const url = tab[key];
36
- return Boolean(url && url !== '' && isUrl(url));
37
- }
38
- return true;
39
- };
40
-
41
- // Classic's this.validateTags(content, tags, false /* isEmail */, isFullMode):
42
- // MPUSH consumes only .valid and .isBraceError from the core result.
43
- const runTagValidation = (content, { tags, currentModule, isFullMode }) => {
44
- const result = validateTagsCore({
45
- contentForBraceCheck: content,
46
- contentForUnsubscribeScan: content,
47
- tags,
48
- currentModule,
49
- isFullMode,
50
- initialMissingTags: [], // not email — no unsubscribe enforcement
51
- includeIsContentEmpty: true,
52
- });
53
- return result && typeof result === 'object'
54
- ? result
55
- : { valid: false, isBraceError: false };
56
- };
57
-
58
- // Classic seeds EVERY tab's error map from tab 0's keys (initializeEditErrorData).
59
- const seedTabErrors = (tab0) => {
60
- const seeded = {};
61
- Object.keys(tab0 || {}).forEach((key) => {
62
- if (!ERROR_SEED_SKIP_KEYS.includes(key)) seeded[key] = false;
63
- });
64
- return seeded;
65
- };
66
-
67
- /**
68
- * @param {object} formData legacy formData ({ 'template-name', 0: {...}, 1: {...}, base })
69
- * @param {object} options { tags, isFullMode, isEmbedded, currentModule,
70
- * restrictPersonalization, isSave, currentTab (1-based) }
71
- * @returns {{ isValid, errorData, platform: { androidValid, iosValid, isOnlyAndroid,
72
- * isOnlyIos }, singlePlatformPrompt: 'android'|'ios'|null }}
73
- */
74
- export default function validateMobilePush(formData = {}, options = {}) {
75
- const {
76
- tags = [],
77
- isFullMode,
78
- isEmbedded = false,
79
- currentModule = DEFAULT_MODULE,
80
- restrictPersonalization = false,
81
- isSave = false,
82
- currentTab = 1,
83
- } = options;
84
-
85
- let isValid = true;
86
- let androidValid = true;
87
- let iosValid = true;
88
- let isOnlyAndroid = false;
89
- let isOnlyIos = false;
90
- const errorData = {};
91
- let singlePlatformPrompt = null;
92
-
93
- // Classic.js:941 — no Android tab => the WHOLE block (template-name included)
94
- // is skipped and validity stays true. Quirk, preserved.
95
- if (formData && formData[TABS.ANDROID]) {
96
- if (isEmptyObject(pickTruthy(formData[TABS.ANDROID], ['tabKey', 'base']))) isOnlyIos = true;
97
- if (isEmptyObject(pickTruthy(formData[TABS.IOS], ['tabKey', 'base']))) isOnlyAndroid = true;
98
-
99
- Object.keys(formData).forEach((index) => {
100
- const tab = formData[index];
101
- let isCurrentTabValid = true;
102
- // Embedded mode skips the tab that is not open (Classic.js:961-965).
103
- const skipValidation = isEmbedded
104
- && ((isOnlyAndroid && parseInt(index, 10) === 1) || (isOnlyIos && parseInt(index, 10) === 0))
105
- && isOnlyAndroid !== isOnlyIos;
106
-
107
- if (isTabKey(index) && tab && !skipValidation) {
108
- const tabIdx = parseInt(index, 10);
109
- const suffix = TABS.suffixFor(tabIdx);
110
- errorData[tabIdx] = errorData[tabIdx] || seedTabErrors(formData[TABS.ANDROID]);
111
- const tabErrors = errorData[tabIdx];
112
-
113
- // --- message-editor{suffix} (Classic.js:971-998) ---
114
- let message = tab[`message-editor${suffix}`];
115
- if (message) {
116
- message = message.trim();
117
- if (message === '') {
118
- tabErrors[`message-editor${suffix}`] = true;
119
- isValid = false;
120
- isCurrentTabValid = false;
121
- } else if (restrictPersonalization && hasPersonalizationTags(message)) {
122
- tabErrors[`message-editor${suffix}`] = true;
123
- isValid = false;
124
- isCurrentTabValid = false;
125
- } else {
126
- tabErrors[`message-editor${suffix}`] = false;
127
- const tagValidation = runTagValidation(message, { tags, currentModule, isFullMode });
128
- if (tagValidation.valid) {
129
- tabErrors[`message-editor${suffix}`] = false;
130
- } else {
131
- const { isBraceError } = tagValidation;
132
- tabErrors[`message-editor${suffix}`] = isBraceError ? ERROR_VALUE.BRACKET : true;
133
- tabErrors['bracket-error'] = isBraceError && ERROR_VALUE.BRACKET;
134
- isValid = false; // NOTE: isCurrentTabValid deliberately NOT cleared (Classic parity)
135
- }
136
- }
137
- } else {
138
- tabErrors[`message-editor${suffix}`] = true;
139
- isValid = false; // NOTE: isCurrentTabValid deliberately NOT cleared (Classic parity)
140
- }
141
-
142
- // --- message-title{suffix} (Classic.js:1002-1030) ---
143
- let title = tab[`message-title${suffix}`];
144
- if (title) {
145
- title = title.trim();
146
- if (title === '') {
147
- tabErrors[`message-title${suffix}`] = true;
148
- isValid = false;
149
- isCurrentTabValid = false;
150
- } else if (restrictPersonalization && hasPersonalizationTags(title)) {
151
- tabErrors[`message-title${suffix}`] = true;
152
- isValid = false;
153
- isCurrentTabValid = false;
154
- } else {
155
- tabErrors[`message-title${suffix}`] = false;
156
- const tagValidation = runTagValidation(title, { tags, currentModule, isFullMode });
157
- if (tagValidation.valid) {
158
- tabErrors[`message-title${suffix}`] = false;
159
- } else {
160
- const { isBraceError } = tagValidation;
161
- tabErrors[`message-title${suffix}`] = isBraceError ? ERROR_VALUE.BRACKET : true;
162
- tabErrors['bracket-error'] = isBraceError && ERROR_VALUE.BRACKET;
163
- isValid = false; // isCurrentTabValid NOT cleared (Classic parity)
164
- }
165
- }
166
- } else {
167
- // Unlike the editor, a missing title DOES clear the tab flag (Classic.js:1026-1029).
168
- tabErrors[`message-title${suffix}`] = true;
169
- isValid = false;
170
- isCurrentTabValid = false;
171
- }
172
-
173
- // --- fixed CTA/deeplink rules (Classic.js:1037-1135), unsuffixed by design ---
174
- CTA_RULES.forEach(({ fieldId, kind }) => {
175
- const failed = kind === 'url'
176
- ? !isValidExternalLink(tab, fieldId)
177
- : tab[fieldId] === '';
178
- if (failed) {
179
- tabErrors[fieldId] = true;
180
- isValid = false;
181
- isCurrentTabValid = false;
182
- } else {
183
- tabErrors[fieldId] = false;
184
- }
185
- });
186
-
187
- // --- image: required only when the key exists (imageSchema) (Classic.js:1137-1143) ---
188
- if (typeof tab.image !== 'undefined' && tab.image === '') {
189
- tabErrors.image = true;
190
- isValid = false;
191
- isCurrentTabValid = false;
192
- } else {
193
- tabErrors.image = false;
194
- }
195
-
196
- // --- dynamic deeplink custom-value keys (Classic.js:1144-1155) ---
197
- Object.keys(tab).forEach((key) => {
198
- if (key.indexOf('custom-value') > -1) {
199
- if (tab[key] === '') {
200
- tabErrors[key] = true;
201
- isValid = false;
202
- isCurrentTabValid = false;
203
- } else {
204
- tabErrors[key] = false;
205
- }
206
- }
207
- });
208
- }
209
-
210
- // index is a string key ('0'/'1'/metadata) — Number() keeps Classic's `==` semantics.
211
- if (Number(index) === TABS.ANDROID) androidValid = isCurrentTabValid;
212
- else if (Number(index) === TABS.IOS) iosValid = isCurrentTabValid;
213
- });
214
-
215
- // --- template-name: root, skipped in embedded mode; failure also suppresses
216
- // the single-platform modal by clearing both platform flags (Classic.js:1163-1170) ---
217
- if (!isEmbedded && (!formData[TEMPLATE_NAME_FIELD] || formData[TEMPLATE_NAME_FIELD] === '')) {
218
- errorData[TEMPLATE_NAME_FIELD] = true;
219
- isValid = false;
220
- androidValid = false;
221
- iosValid = false;
222
- } else {
223
- errorData[TEMPLATE_NAME_FIELD] = false;
224
- }
225
-
226
- // --- single-platform save prompt (Classic.js:1171-1204) ---
227
- if (isSave && (androidValid || iosValid)) {
228
- const androidData = pickTruthy(formData[TABS.ANDROID], ['tabKey', 'base']);
229
- if (isEmptyObject(androidData) && Number(currentTab) === 2) {
230
- singlePlatformPrompt = IOS;
231
- } else {
232
- // Classic's inner rebuild excludes only tabKey — NOT base (line 1191). Preserved.
233
- const iosData = pickTruthy(formData[TABS.IOS], ['tabKey']);
234
- if (isEmptyObject(iosData) && Number(currentTab) === 1) {
235
- singlePlatformPrompt = ANDROID;
236
- }
237
- }
238
- }
239
- // Classic.js:1209-1215 — the three roll-up branches are `isValid = isValid && true`
240
- // no-ops; the accumulated isValid stands. isLiquidValid = isValid for MPUSH.
241
- }
242
-
243
- return {
244
- isValid,
245
- errorData,
246
- platform: {
247
- androidValid, iosValid, isOnlyAndroid, isOnlyIos,
248
- },
249
- singlePlatformPrompt,
250
- };
251
- }
@@ -1,85 +0,0 @@
1
- /**
2
- * TabsContainer — channel-agnostic renderer for a schema 'tabs' container
3
- * (MOBILEPUSH: the Android/iOS platform panes). Replaces the class component's
4
- * renderContainer 'tabs' case (Classic.js:4246-4328) with state-driven tab
5
- * activation — no DOM scans, no setTimeout — while keeping the exact container
6
- * contract: pane keys come from the bound tab's tabKey, unsupported panes are
7
- * skipped WITHOUT reserving an index (pane→tab binding shifts, matching the
8
- * class's paneIndex behavior), and the container's injected onTabChange still
9
- * receives the 1-based active index (bridged by the shell via onTabSwitch).
10
- */
11
-
12
- import React from 'react';
13
- import PropTypes from 'prop-types';
14
- import CapTab from '@capillarytech/cap-ui-library/CapTab';
15
- import Section from './Section';
16
-
17
- const hasTabKey = (key) => key !== undefined && key !== '';
18
-
19
- const TabsContainer = ({ container, renderContext }) => {
20
- if (!container?.panes?.length) return null;
21
-
22
- const tabs = renderContext?.state?.tabs || [];
23
- const activeTabIndex = renderContext?.activeTabIndex ?? 0;
24
-
25
- const renderedPanes = [];
26
- let supportedOrdinal = 0; // class parity: only supported panes advance the index
27
- container.panes.forEach((pane, paneIndex) => {
28
- const isPaneSupported = typeof pane.isSupported === 'boolean' ? pane.isSupported : true;
29
- if (!isPaneSupported) return;
30
- const tabIndex = supportedOrdinal;
31
- supportedOrdinal += 1;
32
-
33
- // Fields inside this pane read/write their OWN tab; everything else in the
34
- // context is shared. (paneTabIndex is consumed by Section.renderField.)
35
- const paneContext = { ...renderContext, paneTabIndex: tabIndex };
36
- const paneTabKey = tabs[tabIndex]?.tabKey;
37
- const headerSections = pane.sectionsHeaders || container.tabContent?.sections || [];
38
-
39
- renderedPanes.push({
40
- // Class parity: panes render eagerly (forceRender). Hidden-pane reconciliation
41
- // stays cheap through the FieldSlot memo boundaries. Lazy activation is a
42
- // deliberate post-rollout follow-up (see the MPUSH design doc §9).
43
- forceRender: true,
44
- // The div wrapper + onInput stop are Classic verbatim (Classic.js:4278).
45
- tab: (
46
- <div className="form-tab-header" onInput={(e) => e.stopPropagation()}>
47
- {headerSections.map((section, index) => (
48
- <Section key={`hdr-${index}`} section={section} renderContext={paneContext} />
49
- ))}
50
- </div>
51
- ),
52
- key: hasTabKey(paneTabKey) ? `${paneTabKey}` : `${paneIndex}`,
53
- content: (pane.sections || []).map((section, index) => (
54
- <Section key={`sec-${index}`} section={section} renderContext={paneContext} />
55
- )),
56
- });
57
- });
58
-
59
- // Must resolve through the SAME fallback chain as the pane keys — a divergent
60
- // fallback leaves no pane matching activeKey and CapTab renders nothing.
61
- const activeKey = tabs[activeTabIndex]?.tabKey;
62
- const resolvedActiveKey = hasTabKey(activeKey)
63
- ? `${activeKey}`
64
- : (renderedPanes[activeTabIndex]?.key ?? renderedPanes[0]?.key ?? '1');
65
-
66
- return (
67
- <CapTab
68
- className={`cap-tabs-${container.id} fb-tabs-full-width`}
69
- activeKey={resolvedActiveKey}
70
- onChange={(key) => renderContext?.onTabSwitch?.(container, key)}
71
- panes={renderedPanes}
72
- />
73
- );
74
- };
75
-
76
- TabsContainer.propTypes = {
77
- container: PropTypes.object,
78
- renderContext: PropTypes.object.isRequired,
79
- };
80
-
81
- TabsContainer.defaultProps = {
82
- container: null,
83
- };
84
-
85
- export default TabsContainer;