@capillarytech/creatives-library 9.0.14-beta.0 → 9.0.14

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 (83) hide show
  1. package/constants/unified.js +0 -3
  2. package/package.json +1 -1
  3. package/services/api.js +10 -0
  4. package/services/tests/api.test.js +83 -0
  5. package/utils/common.js +0 -8
  6. package/v2Components/CommonTestAndPreview/UnifiedPreview/WhatsAppPreviewContent.js +5 -3
  7. package/v2Components/CommonTestAndPreview/index.js +7 -0
  8. package/v2Components/FormBuilder/_formBuilder.scss +0 -5
  9. package/v2Components/FormBuilder/index.js +4479 -41
  10. package/v2Components/NavigationBar/index.js +27 -0
  11. package/v2Components/NavigationBar/messages.js +4 -0
  12. package/v2Components/NavigationBar/tests/index.test.js +19 -0
  13. package/v2Components/NewCallTask/index.js +6 -1
  14. package/v2Components/TemplatePreview/index.js +4 -2
  15. package/v2Containers/Cap/index.js +3 -1
  16. package/v2Containers/CommunicationFlow/CommunicationFlow.js +130 -20
  17. package/v2Containers/CommunicationFlow/CommunicationFlow.scss +154 -0
  18. package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +240 -0
  19. package/v2Containers/CommunicationFlow/DemoPage.js +47 -0
  20. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +369 -2
  21. package/v2Containers/CommunicationFlow/Tests/CommunicationFlowCard.test.js +619 -0
  22. package/v2Containers/CommunicationFlow/Tests/DemoPage.test.js +77 -0
  23. package/v2Containers/CommunicationFlow/Tests/getContentBody.test.js +933 -0
  24. package/v2Containers/CommunicationFlow/constants.js +45 -10
  25. package/v2Containers/CommunicationFlow/index.js +5 -2
  26. package/v2Containers/CommunicationFlow/messages.js +20 -0
  27. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +94 -31
  28. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +14 -11
  29. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +1144 -32
  30. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/extractContentForPreview.js +183 -0
  31. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/CommunicationStrategyStep.js +3 -0
  32. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +39 -0
  33. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +6 -2
  34. package/v2Containers/CommunicationFlow/utils/getContentBody.js +369 -0
  35. package/v2Containers/CommunicationFlow/utils/getContentBody.scss +19 -0
  36. package/v2Containers/CommunicationFlow/utils/getEnabledSteps.js +1 -1
  37. package/v2Containers/CreativesContainer/constants.js +6 -0
  38. package/v2Containers/CreativesContainer/index.js +68 -1
  39. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +2 -2
  40. package/v2Containers/Templates/index.js +2 -2
  41. package/v2Containers/TemplatesV2/index.js +9 -1
  42. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +41 -34
  43. package/v2Components/FormBuilder/Classic.js +0 -4487
  44. package/v2Components/FormBuilder/Functional/FormBuilderShell.js +0 -371
  45. package/v2Components/FormBuilder/Functional/channels/registry.js +0 -17
  46. package/v2Components/FormBuilder/Functional/channels/sms/buildSubmitPayload.js +0 -9
  47. package/v2Components/FormBuilder/Functional/channels/sms/config.js +0 -30
  48. package/v2Components/FormBuilder/Functional/channels/sms/getEditorErrorDescriptor.js +0 -46
  49. package/v2Components/FormBuilder/Functional/channels/sms/getLiquidContent.js +0 -13
  50. package/v2Components/FormBuilder/Functional/channels/sms/index.js +0 -22
  51. package/v2Components/FormBuilder/Functional/channels/sms/tests/getEditorErrorDescriptor.test.js +0 -52
  52. package/v2Components/FormBuilder/Functional/channels/sms/tests/getLiquidContent.test.js +0 -25
  53. package/v2Components/FormBuilder/Functional/channels/sms/tests/validate.test.js +0 -87
  54. package/v2Components/FormBuilder/Functional/channels/sms/validate.js +0 -89
  55. package/v2Components/FormBuilder/Functional/constants.js +0 -42
  56. package/v2Components/FormBuilder/Functional/core/schema/fieldRegistry.js +0 -38
  57. package/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js +0 -85
  58. package/v2Components/FormBuilder/Functional/core/store/formReducer.js +0 -81
  59. package/v2Components/FormBuilder/Functional/core/store/selectors.js +0 -30
  60. package/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js +0 -91
  61. package/v2Components/FormBuilder/Functional/index.js +0 -26
  62. package/v2Components/FormBuilder/Functional/layout/FieldSlot.js +0 -59
  63. package/v2Components/FormBuilder/Functional/layout/SchemaForm.js +0 -31
  64. package/v2Components/FormBuilder/Functional/layout/Section.js +0 -116
  65. package/v2Components/FormBuilder/Functional/renderers/smsRenderers.js +0 -258
  66. package/v2Components/FormBuilder/Functional/tests/channelRegistry.test.js +0 -21
  67. package/v2Components/FormBuilder/Functional/tests/fieldRegistry.test.js +0 -65
  68. package/v2Components/FormBuilder/Functional/tests/fieldSlot.test.js +0 -97
  69. package/v2Components/FormBuilder/Functional/tests/fixtures/smsParityCases.js +0 -192
  70. package/v2Components/FormBuilder/Functional/tests/formReducer.test.js +0 -129
  71. package/v2Components/FormBuilder/Functional/tests/initializeFormState.test.js +0 -132
  72. package/v2Components/FormBuilder/Functional/tests/schemaForm.test.js +0 -40
  73. package/v2Components/FormBuilder/Functional/tests/section.test.js +0 -99
  74. package/v2Components/FormBuilder/Functional/tests/selectors.test.js +0 -67
  75. package/v2Components/FormBuilder/Functional/tests/sms.crossFlowParity.test.js +0 -155
  76. package/v2Components/FormBuilder/Functional/tests/sms.liquid.test.js +0 -172
  77. package/v2Components/FormBuilder/Functional/tests/sms.rollout.test.js +0 -122
  78. package/v2Components/FormBuilder/Functional/tests/sms.shell.parity.test.js +0 -329
  79. package/v2Components/FormBuilder/Functional/tests/smsRenderers.test.js +0 -160
  80. package/v2Components/FormBuilder/Functional/tests/toLegacyFormData.test.js +0 -95
  81. package/v2Components/FormBuilder/tests/__snapshots__/sms.characterization.test.js.snap +0 -114
  82. package/v2Components/FormBuilder/tests/entryGate.test.js +0 -106
  83. package/v2Components/FormBuilder/tests/sms.characterization.test.js +0 -336
@@ -1,87 +0,0 @@
1
- /**
2
- * Unit tests for the pure SMS validator (mirrors the Classic SMS validateForm
3
- * block). Operates on the legacy formData shape; returns legacy errorData.
4
- */
5
-
6
- import validateSms, { ERROR } from '../validate';
7
- import * as tagValidations from '../../../../../../utils/tagValidations';
8
-
9
- const fd = ({ name = 'My SMS', content = 'hello world', unicode = false } = {}) => ({
10
- 'template-name': name,
11
- 0: { 'sms-editor': content, 'unicode-validity': unicode, base: true, tabKey: 'k0' },
12
- });
13
-
14
- const baseCtx = { tags: [], isFullMode: true, isEmbedded: false, currentModule: 'default' };
15
-
16
- describe('validateSms', () => {
17
- it('valid: ascii content + name', () => {
18
- const { isValid, errorData } = validateSms(fd(), baseCtx);
19
- expect(isValid).toBe(true);
20
- expect(errorData['template-name']).toBe(false);
21
- expect(errorData[0]['sms-editor']).toBe(false);
22
- expect(errorData[0]['unicode-validity']).toBe(false);
23
- });
24
-
25
- it('tolerates default args (no formData / no options)', () => {
26
- // empty form, default options -> empty content + required name both flagged
27
- const { isValid, errorData } = validateSms();
28
- expect(isValid).toBe(false);
29
- expect(errorData[0]['sms-editor']).toBe(ERROR.GENERIC);
30
- expect(errorData['template-name']).toBe(true);
31
- });
32
-
33
- it('invalid: empty content flags sms-editor with a generic error', () => {
34
- const { isValid, errorData } = validateSms(fd({ content: '' }), baseCtx);
35
- expect(isValid).toBe(false);
36
- expect(errorData[0]['sms-editor']).toBe(ERROR.GENERIC);
37
- });
38
-
39
- it('invalid: empty template-name (non-embedded) flags template-name at root', () => {
40
- const { isValid, errorData } = validateSms(fd({ name: '' }), baseCtx);
41
- expect(isValid).toBe(false);
42
- expect(errorData['template-name']).toBe(true);
43
- });
44
-
45
- it('invalid: unicode content with the checkbox OFF', () => {
46
- const { isValid, errorData } = validateSms(fd({ content: 'नमस्ते', unicode: false }), baseCtx);
47
- expect(isValid).toBe(false);
48
- expect(errorData[0]['unicode-validity']).toBe(true);
49
- });
50
-
51
- it('valid: unicode content with the checkbox ON', () => {
52
- const { isValid, errorData } = validateSms(fd({ content: 'नमस्ते', unicode: true }), baseCtx);
53
- expect(isValid).toBe(true);
54
- expect(errorData[0]['unicode-validity']).toBe(false);
55
- });
56
-
57
- it('embedded: template-name is not required', () => {
58
- const { isValid, errorData } = validateSms(fd({ name: '' }), { ...baseCtx, isEmbedded: true });
59
- expect(errorData['template-name']).toBe(false);
60
- expect(isValid).toBe(true);
61
- });
62
-
63
- it('invalid: unbalanced braces flag a bracket error on sms-editor', () => {
64
- const { isValid, errorData } = validateSms(fd({ content: 'hi {{name}' }), baseCtx);
65
- expect(isValid).toBe(false);
66
- expect(errorData[0]['sms-editor']).toBe(ERROR.BRACKET);
67
- expect(errorData[0]['bracket-error']).toBe(ERROR.BRACKET);
68
- });
69
-
70
- it('maps a missing-tag core result -> MISSING_TAG editor error', () => {
71
- // SMS passes initialMissingTags:[] so the core never reports missing tags in
72
- // practice; stub it to verify validateSms maps a missing-tag result correctly.
73
- jest.spyOn(tagValidations, 'validateTagsCore').mockReturnValue({ valid: false, missingTags: ['unsubscribe'], isBraceError: false });
74
- const { isValid, errorData } = validateSms(fd({ content: 'hi' }), { ...baseCtx, isFullMode: false });
75
- expect(isValid).toBe(false);
76
- expect(errorData[0]['sms-editor']).toBe(ERROR.MISSING_TAG);
77
- tagValidations.validateTagsCore.mockRestore();
78
- });
79
-
80
- it('falls back to a generic error when the core returns a non-object', () => {
81
- jest.spyOn(tagValidations, 'validateTagsCore').mockReturnValue(undefined);
82
- const { isValid, errorData } = validateSms(fd({ content: 'hi' }), baseCtx);
83
- expect(isValid).toBe(false);
84
- expect(errorData[0]['sms-editor']).toBe(ERROR.GENERIC);
85
- tagValidations.validateTagsCore.mockRestore();
86
- });
87
- });
@@ -1,89 +0,0 @@
1
- /**
2
- * SMS validation — pure mirror of the class component's SMS validateForm block,
3
- * operating on the legacy formData so the emitted errorData is byte-identical:
4
- * tags/braces, empty content, unicode-without-checkbox, and required template-name.
5
- */
6
-
7
- import { checkUnicode } from '../../../../../utils/smsCharCountV2';
8
- import { validateTagsCore } from '../../../../../utils/tagValidations';
9
- import { DEFAULT as DEFAULT_MODULE } from '../../../../../constants/unified';
10
- import { fieldIds, SMS_TAB } from './config';
11
-
12
- const { name: NAME_FIELD, editor: EDITOR_FIELD, unicode: UNICODE_FIELD } = fieldIds;
13
-
14
- // Same error constants the class uses (Classic.js errorMessageForTags).
15
- export const ERROR = {
16
- MISSING_TAG: 'missingTagsError',
17
- GENERIC: 'genericValidationError',
18
- BRACKET: 'tagBracketCountMismatchError',
19
- };
20
-
21
- /**
22
- * @param {object} formData legacy formData ({ 'template-name', 0: {...}, base })
23
- * @param {object} options { tags, isFullMode, isEmbedded, currentModule }
24
- * @returns {{ isValid: boolean, errorData: object }}
25
- */
26
- export default function validateSms(formData = {}, options = {}) {
27
- const {
28
- tags = [],
29
- isFullMode,
30
- isEmbedded = false,
31
- currentModule = DEFAULT_MODULE,
32
- } = options;
33
-
34
- let isValid = true;
35
- const tabErrors = {};
36
- const errorData = { [SMS_TAB]: tabErrors };
37
- const tab = formData[SMS_TAB] || {};
38
- const content = tab[EDITOR_FIELD];
39
- const unicodeAllowed = tab[UNICODE_FIELD];
40
- const hasUnicodeChars = content ? checkUnicode(content) : false;
41
-
42
- // --- tags / braces ---
43
- let tagValidation = { valid: false, missingTags: [], isBraceError: false };
44
- if (content) {
45
- const result = validateTagsCore({
46
- contentForBraceCheck: content,
47
- contentForUnsubscribeScan: content,
48
- tags,
49
- currentModule,
50
- isFullMode,
51
- initialMissingTags: [], // SMS is not email — no unsubscribe enforcement
52
- includeIsContentEmpty: true,
53
- });
54
- if (result && typeof result === 'object') tagValidation = result;
55
- }
56
-
57
- if (tagValidation.valid) {
58
- tabErrors[EDITOR_FIELD] = false;
59
- } else {
60
- const { missingTags, isBraceError } = tagValidation;
61
- let editorError = ERROR.GENERIC;
62
- if (missingTags?.length) editorError = ERROR.MISSING_TAG;
63
- else if (isBraceError) editorError = ERROR.BRACKET;
64
- tabErrors[EDITOR_FIELD] = editorError;
65
- tabErrors['bracket-error'] = isBraceError && ERROR.BRACKET;
66
- isValid = false;
67
- }
68
-
69
- // --- unicode / empty (can overwrite the editor error, matching the class) ---
70
- if (content && hasUnicodeChars && !unicodeAllowed) {
71
- tabErrors[UNICODE_FIELD] = true;
72
- isValid = false;
73
- } else if (!content) {
74
- tabErrors[EDITOR_FIELD] = ERROR.GENERIC;
75
- isValid = false;
76
- } else {
77
- tabErrors[UNICODE_FIELD] = false;
78
- }
79
-
80
- // --- template-name (required unless embedded) ---
81
- if (!isEmbedded && !formData[NAME_FIELD]) {
82
- errorData[NAME_FIELD] = true;
83
- isValid = false;
84
- } else {
85
- errorData[NAME_FIELD] = false;
86
- }
87
-
88
- return { isValid, errorData };
89
- }
@@ -1,42 +0,0 @@
1
- /**
2
- * Engine-level constants for the functional FormBuilder (channel-agnostic).
3
- */
4
-
5
- // Structural section types in the layout schema.
6
- export const SECTION_TYPE = {
7
- PARENT: 'parent',
8
- MULTICOLS: 'multicols',
9
- COL_LABEL: 'col-label',
10
- };
11
-
12
- // Leaf field types the SMS schema uses (registry keys + memoization decisions).
13
- export const FIELD_TYPE = {
14
- INPUT: 'input',
15
- TEXTAREA: 'textarea',
16
- CHECKBOX: 'checkbox',
17
- BUTTON: 'button',
18
- TAG_LIST: 'tag-list',
19
- SMS_PREVIEW: 'sms-preview',
20
- DIV: 'div',
21
- };
22
-
23
- // Editor-error descriptor kinds (getEditorErrorDescriptor): the brace error is shown
24
- // inline + footer, missing-tag is footer-only.
25
- export const EDITOR_ERROR_KIND = {
26
- BRACE: 'brace',
27
- MISSING: 'missing',
28
- };
29
-
30
- // Field types whose render depends only on their own value/error (+ the
31
- // checkValidation/channel primitives). These go through the memoized FieldSlot;
32
- // derived/control fields (sms-preview, sms-count, tag-list, button) render directly.
33
- export const MEMOIZED_TYPES = new Set([FIELD_TYPE.INPUT, FIELD_TYPE.TEXTAREA, FIELD_TYPE.CHECKBOX]);
34
-
35
- // Phase 1 renders a single active tab (index 0); multi-tab / version machinery
36
- // arrives with later channels.
37
- export const ACTIVE_TAB_INDEX = 0;
38
-
39
- export const HIGH_FREQ_FIELDS = ['template-name', 'template-subject'];
40
-
41
- export const ASCII = 'ASCII';
42
- export const UNICODE = 'Unicode';
@@ -1,38 +0,0 @@
1
- /**
2
- * fieldRegistry — maps a schema field `type` to the React component that renders
3
- * it. This replaces the monolith's ~800-line render switch: adding a field type
4
- * means registering a renderer; a channel-specific variant is registered as an
5
- * override on top of the base registry, scoped to that channel's form.
6
- *
7
- * A factory (not a module-level singleton) so each FormBuilder instance / test
8
- * gets an isolated registry — channels never mutate a shared global.
9
- */
10
-
11
- export function createFieldRegistry(initial = {}) {
12
- const map = new Map(Object.entries(initial));
13
-
14
- const register = (type, component) => {
15
- map.set(type, component);
16
- return api; // chainable
17
- };
18
-
19
- const registerAll = (componentsByType = {}) => {
20
- Object.entries(componentsByType).forEach(([type, component]) => map.set(type, component));
21
- return api;
22
- };
23
-
24
- /** Resolve a renderer for a type, or null if none is registered. */
25
- const resolve = (type) => (map.has(type) ? map.get(type) : null);
26
-
27
- const has = (type) => map.has(type);
28
-
29
- /** A child registry that inherits this one and can shadow entries (channel overrides). */
30
- const extend = (overrides = {}) => createFieldRegistry({ ...Object.fromEntries(map), ...overrides });
31
-
32
- const api = {
33
- register, registerAll, resolve, has, extend,
34
- };
35
- return api;
36
- }
37
-
38
- export default createFieldRegistry;
@@ -1,85 +0,0 @@
1
- /**
2
- * initializeFormState — pure walk of a layout schema that seeds default field
3
- * values into the normalized state (the V3 equivalent of the class component's
4
- * initializeStandAloneSections / initializeMultiColSection family).
5
- *
6
- * Scope (Phase 1): SMS — a single-tab, standalone-only schema. Fields marked
7
- * `standalone: true` seed into `root`; all other data fields seed into tab 0.
8
- * `onlyDisplay` fields (buttons, preview, counter, tag-list) are NOT seeded —
9
- * they are non-data UI controls. Defaults match the class component: checkbox
10
- * => false; otherwise the schema's `value` if defined, else ''.
11
- *
12
- * NOTE: tab/version/language seeding is intentionally out of scope here — those
13
- * arrive with MobilePush (tabs) and Email (versions/languages). Parity of the
14
- * seeded shape against the monolith is asserted in PR3 against real fixtures.
15
- */
16
-
17
- import {
18
- createInitialState,
19
- emptyLiquidErrors,
20
- } from '../store/formReducer';
21
- import { SECTION_TYPE, FIELD_TYPE } from '../../constants';
22
-
23
- const defaultValueFor = (field) => {
24
- if (field.type === FIELD_TYPE.CHECKBOX) return false;
25
- return field.value !== undefined ? field.value : '';
26
- };
27
-
28
- const isDataField = (field) => Boolean(field?.id) && !field.onlyDisplay;
29
-
30
- /** Walk a section subtree (parent / multicols / col-label) and call visit() on every leaf field. */
31
- const visitSectionFields = (section, visit) => {
32
- if (!section) return;
33
- switch (section.type) {
34
- case SECTION_TYPE.PARENT:
35
- (section.childSections || []).forEach((child) => visitSectionFields(child, visit));
36
- break;
37
- case SECTION_TYPE.MULTICOLS:
38
- (section.inputFields || []).forEach((row) => (row?.cols || []).forEach(visit));
39
- (section.actionFields || []).forEach((row) => (row?.cols || []).forEach(visit));
40
- break;
41
- case SECTION_TYPE.COL_LABEL:
42
- (section.inputFields || []).forEach(visit);
43
- (section.actionFields || []).forEach(visit);
44
- break;
45
- default:
46
- break;
47
- }
48
- };
49
-
50
- let autoId = 0;
51
- const defaultGenerateId = () => {
52
- autoId += 1;
53
- return `fb-${autoId}`;
54
- };
55
-
56
- /**
57
- * @param {object} schema the layout `definition` (with `.channel`, `.standalone`, …)
58
- * @param {object} [opts]
59
- * @param {() => string} [opts.generateId] tab-key generator (injectable for tests)
60
- * @returns normalized state with defaults seeded
61
- */
62
- export default function initializeFormState(schema = {}, opts = {}) {
63
- const generateId = opts.generateId || defaultGenerateId;
64
- const channel = schema.channel || null;
65
-
66
- const state = createInitialState(channel);
67
- // SMS uses exactly one tab (index 0), marked base.
68
- const tab0 = { fields: {}, base: true, tabKey: generateId() };
69
- state.tabs = [tab0];
70
- state.errors = { root: {}, tabs: [{}] };
71
- state.validity = { isFormValid: false, liquidErrorMessage: emptyLiquidErrors() };
72
-
73
- // Seed each leaf field's default in a single walk: standalone fields go to `root`,
74
- // the rest to tab 0. onlyDisplay (non-data) fields are skipped.
75
- const seedField = (field) => {
76
- if (!isDataField(field)) return;
77
- const target = field.standalone ? state.root : tab0.fields;
78
- if (target[field.id] === undefined) target[field.id] = defaultValueFor(field);
79
- };
80
-
81
- const sections = schema.standalone?.sections;
82
- if (sections) sections.forEach((section) => visitSectionFields(section, seedField));
83
-
84
- return state;
85
- }
@@ -1,81 +0,0 @@
1
- /**
2
- * formReducer — the single source of truth for FormBuilder form state (V3).
3
- */
4
-
5
- export const ActionTypes = {
6
- HYDRATE: 'fb/HYDRATE',
7
- FIELD_CHANGED: 'fb/FIELD_CHANGED',
8
- VALIDATION_RESULT: 'fb/VALIDATION_RESULT',
9
- };
10
-
11
- export const emptyLiquidErrors = () => ({ STANDARD_ERROR_MSG: [], LIQUID_ERROR_MSG: [] });
12
-
13
- /** Build a clean initial normalized state for a channel (no fields seeded). */
14
- export const createInitialState = (channel = null) => ({
15
- meta: { channel, baseTabIndex: 0 },
16
- root: {},
17
- tabs: [],
18
- errors: { root: {}, tabs: [] },
19
- validity: { isFormValid: false, liquidErrorMessage: emptyLiquidErrors() },
20
- });
21
-
22
- // ---- action creators ---------------------------------------------------------
23
-
24
- /** Replace the whole normalized state (used to load an initialized/hydrated tree). */
25
- export const hydrate = (state) => ({ type: ActionTypes.HYDRATE, state });
26
-
27
- /**
28
- * Set one field's value. `tabIndex == null` targets a root (standalone) field;
29
- * otherwise it targets tabs[tabIndex].fields.
30
- */
31
- export const fieldChanged = ({ fieldId, value, tabIndex = null }) => ({
32
- type: ActionTypes?.FIELD_CHANGED,
33
- fieldId,
34
- value,
35
- tabIndex,
36
- });
37
-
38
- /** Merge a validation outcome (errors + validity) into state. */
39
- export const validationResult = ({ errors, isFormValid, liquidErrorMessage }) => ({
40
- type: ActionTypes.VALIDATION_RESULT,
41
- errors,
42
- isFormValid,
43
- liquidErrorMessage,
44
- });
45
-
46
- // ---- reducer -----------------------------------------------------------------
47
-
48
- export default function formReducer(state, action) {
49
- switch (action?.type) {
50
- case ActionTypes.HYDRATE:
51
- return action?.state || state;
52
-
53
- case ActionTypes.FIELD_CHANGED: {
54
- const { fieldId, value, tabIndex } = action;
55
- if (tabIndex == null) {
56
- if (state?.root[fieldId] === value) return state; // no-op keeps identity
57
- return { ...state, root: { ...state.root, [fieldId]: value } };
58
- }
59
- const tab = state?.tabs[tabIndex];
60
- if (!tab) return state;
61
- if (tab?.fields?.[fieldId] === value) return state; // no-op keeps identity
62
- return {
63
- ...state,
64
- tabs: state?.tabs?.map((tabData, index) => index === tabIndex ? { ...tabData, fields: { ...tabData?.fields, [fieldId]: value } } : tabData),
65
- };
66
- }
67
-
68
- case ActionTypes.VALIDATION_RESULT:
69
- return {
70
- ...state,
71
- errors: action?.errors || state?.errors,
72
- validity: {
73
- isFormValid: Boolean(action?.isFormValid),
74
- liquidErrorMessage: action?.liquidErrorMessage || state?.validity?.liquidErrorMessage,
75
- },
76
- };
77
-
78
- default:
79
- return state;
80
- }
81
- }
@@ -1,30 +0,0 @@
1
- /**
2
- * selectors — pure read helpers over the normalized form state.
3
- *
4
- * Field components read their value/error through these so they receive
5
- * PRIMITIVE props (string/boolean) — that is what lets React.memo field
6
- * boundaries bail out cheaply (see the State Management Decision in the design
7
- * doc). Never return freshly-constructed objects from a hot-path selector.
8
- */
9
-
10
- /** Value of a field. `tabIndex == null` => a root (standalone) field. */
11
- export const selectFieldValue = (state, { fieldId, tabIndex = null }) => {
12
- const { root = {}, tabs = [] } = state || {};
13
- if (tabIndex == null) return root?.[fieldId];
14
- return tabs?.[tabIndex]?.fields?.[fieldId];
15
- };
16
-
17
- /** Validation error for a field (string message or boolean), mirrors selectFieldValue. */
18
- export const selectFieldError = (state, { fieldId, tabIndex = null }) => {
19
- const { root = {}, tabs = [] } = state?.errors || {};
20
- if (tabIndex == null) return root?.[fieldId];
21
- return tabs?.[tabIndex]?.[fieldId];
22
- };
23
-
24
- export const selectIsFormValid = (state) => state.validity.isFormValid;
25
-
26
- export const selectLiquidErrorMessage = (state) => state.validity.liquidErrorMessage;
27
-
28
- export const selectBaseTabIndex = (state) => state.meta.baseTabIndex;
29
-
30
- export const selectChannel = (state) => state.meta.channel;
@@ -1,91 +0,0 @@
1
- /**
2
- * toLegacyFormData — codec between the normalized V3 state and the legacy
3
- * `formData` dialect that the channel containers + `getChannelData` consume.
4
- *
5
- * This is the compatibility contract that keeps the migration invisible to the
6
- * containers and the backend: the functional engine works in the normalized
7
- * shape, but emits `onChange(legacyFormData, ...)` and builds the save payload
8
- * from the exact same shape the class component produced.
9
- *
10
- * LEGACY DIALECT (SMS example):
11
- * {
12
- * 'template-name': 'My SMS', // root (standalone) fields
13
- * 0: { 'sms-editor': 'hi', 'unicode-validity': false, // tab fields
14
- * base: true, tabKey: '...' }, // tab metadata
15
- * base: <SAME REFERENCE as formData[0]>, // alias of the base tab
16
- * }
17
- *
18
- * INVARIANT (unit-tested): toLegacy(fromLegacy(x)) deep-equals x.
19
- * - root fields <-> top-level non-numeric keys (excluding `base`)
20
- * - tabs[i] <-> numeric key i; base/tabKey/name are metadata, the rest are fields
21
- * - `base` <-> the SAME object reference as the base tab (never a copy)
22
- */
23
-
24
- const isTabKey = (key) => /^\d+$/.test(key);
25
-
26
- /** Parse a legacy `formData` object into normalized state. */
27
- export const fromLegacy = (legacy = {}, { channel = null } = {}) => {
28
- const root = {};
29
- const tabsByIndex = {};
30
-
31
- Object.keys(legacy).forEach((key) => {
32
- if (key === 'base') return; // alias — reconstructed by toLegacy, never stored
33
- if (isTabKey(key)) {
34
- const tabObj = legacy[key] || {};
35
- const {
36
- base, tabKey, name, ...fields
37
- } = tabObj;
38
- tabsByIndex[Number(key)] = {
39
- fields: { ...fields },
40
- base: Boolean(base),
41
- tabKey,
42
- name,
43
- };
44
- } else {
45
- root[key] = legacy[key];
46
- }
47
- });
48
-
49
- // Safe to compact: legacy tab indices are always dense 0..n-1 (class seeds formData[0];
50
- // deleteVersion re-indexes without holes). Revisit only if a channel emits sparse keys.
51
- const orderedIndices = Object.keys(tabsByIndex)
52
- .map(Number)
53
- .sort((a, b) => a - b);
54
- const tabs = orderedIndices.map((index) => tabsByIndex[index]);
55
-
56
- const foundBase = tabs.findIndex((tab) => tab?.base);
57
- const baseTabIndex = foundBase === -1 ? 0 : foundBase;
58
-
59
- return {
60
- meta: { channel, baseTabIndex },
61
- root,
62
- tabs,
63
- errors: { root: {}, tabs: tabs?.map(() => ({})) },
64
- validity: { isFormValid: false, liquidErrorMessage: { STANDARD_ERROR_MSG: [], LIQUID_ERROR_MSG: [] } },
65
- };
66
- };
67
-
68
- /** Serialize normalized state back into the legacy `formData` dialect. */
69
- export const toLegacy = (state) => {
70
- const out = {};
71
-
72
- // root (standalone) fields land at the top level
73
- Object.assign(out, state.root);
74
-
75
- // each tab becomes a numeric-keyed object carrying its fields + metadata
76
- state.tabs.forEach((tab, index) => {
77
- const tabObj = { ...tab.fields };
78
- if (tab?.base) tabObj.base = true; // legacy omits `base` on non-base tabs
79
- if (tab?.tabKey !== undefined) tabObj.tabKey = tab.tabKey;
80
- if (tab?.name !== undefined) tabObj.name = tab.name;
81
- out[index] = tabObj;
82
- });
83
-
84
- // `base` is an ALIAS (same reference) of the base tab, matching the class component
85
- const baseIdx = state.meta.baseTabIndex;
86
- if (out[baseIdx] !== undefined) {
87
- out.base = out[baseIdx];
88
- }
89
-
90
- return out;
91
- };
@@ -1,26 +0,0 @@
1
- /**
2
- * FunctionalFormBuilder — Redux-connected entry for the functional FormBuilder.
3
- * Injects intl and sources the liquid action + spinner state the same way the class
4
- * component does, so the channel containers stay unchanged (Stage A).
5
- */
6
-
7
- import { connect } from 'react-redux';
8
- import { bindActionCreators } from 'redux';
9
- import { injectIntl } from 'react-intl';
10
- import { createStructuredSelector } from 'reselect';
11
- import FormBuilderShell from './FormBuilderShell';
12
- import { selectLiquidStateDetails, selectMetaDataStatus } from '../../../v2Containers/Cap/selectors';
13
- import * as actions from '../../../v2Containers/Cap/actions';
14
-
15
- const mapStateToProps = createStructuredSelector({
16
- liquidExtractionInProgress: selectLiquidStateDetails(),
17
- metaDataStatus: selectMetaDataStatus(),
18
- });
19
-
20
- function mapDispatchToProps(dispatch) {
21
- return {
22
- actions: bindActionCreators(actions, dispatch),
23
- };
24
- }
25
-
26
- export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(FormBuilderShell));
@@ -1,59 +0,0 @@
1
- /**
2
- * FieldSlot — React.memo boundary for self-contained data fields (input/textarea/
3
- * checkbox). Receives only primitive per-field props + stable handler refs, so a
4
- * keystroke in one field re-renders only its slot. Derived/control fields render directly.
5
- */
6
- import React from 'react';
7
- import PropTypes from 'prop-types';
8
-
9
- const FieldSlot = React.memo(({
10
- Renderer,
11
- field,
12
- value,
13
- error,
14
- resolvedError,
15
- checkValidation,
16
- channel,
17
- onFieldChange,
18
- onFieldBlur,
19
- onEvent,
20
- }) => {
21
- const renderContext = { checkValidation, channel };
22
- return (
23
- <Renderer
24
- field={field}
25
- value={value}
26
- error={error}
27
- resolvedError={resolvedError}
28
- onChange={(newValue) => onFieldChange(field, newValue)}
29
- onBlur={() => onFieldBlur?.(field)}
30
- onEvent={(eventName, data) => onEvent?.(field, eventName, data)}
31
- renderContext={renderContext}
32
- />
33
- );
34
- });
35
-
36
- FieldSlot.propTypes = {
37
- Renderer: PropTypes.elementType.isRequired,
38
- field: PropTypes.object.isRequired,
39
- value: PropTypes.any,
40
- error: PropTypes.any,
41
- resolvedError: PropTypes.string,
42
- checkValidation: PropTypes.bool,
43
- channel: PropTypes.string,
44
- onFieldChange: PropTypes.func.isRequired,
45
- onFieldBlur: PropTypes.func,
46
- onEvent: PropTypes.func,
47
- };
48
-
49
- FieldSlot.defaultProps = {
50
- value: undefined,
51
- error: undefined,
52
- resolvedError: undefined,
53
- checkValidation: false,
54
- channel: '',
55
- onFieldBlur: undefined,
56
- onEvent: undefined,
57
- };
58
-
59
- export default FieldSlot;
@@ -1,31 +0,0 @@
1
- /**
2
- * SchemaForm — top of the schema-driven render. Walks the standalone sections
3
- * (SMS shape). Container/tabs handling is added when MobilePush/Email migrate.
4
- */
5
-
6
- import React from 'react';
7
- import PropTypes from 'prop-types';
8
- import Section from './Section';
9
-
10
- const SchemaForm = ({ schema, renderContext }) => {
11
- if (!schema?.standalone) return null;
12
- return (
13
- <>
14
- {schema.standalone?.sections?.map((section, index) => (
15
- // The schema is a frozen, never-reordered layout descriptor, so the index is a stable key here.
16
- <Section key={index} section={section} renderContext={renderContext} />
17
- ))}
18
- </>
19
- );
20
- };
21
-
22
- SchemaForm.propTypes = {
23
- schema: PropTypes.object,
24
- renderContext: PropTypes.object.isRequired,
25
- };
26
-
27
- SchemaForm.defaultProps = {
28
- schema: null,
29
- };
30
-
31
- export default SchemaForm;