@capillarytech/creatives-library 9.0.11 → 9.0.13-alpha.0

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 (51) hide show
  1. package/constants/unified.js +3 -0
  2. package/package.json +1 -1
  3. package/utils/common.js +8 -0
  4. package/v2Components/FormBuilder/Classic.js +4487 -0
  5. package/v2Components/FormBuilder/Functional/FormBuilderShell.js +369 -0
  6. package/v2Components/FormBuilder/Functional/channels/registry.js +17 -0
  7. package/v2Components/FormBuilder/Functional/channels/sms/buildSubmitPayload.js +9 -0
  8. package/v2Components/FormBuilder/Functional/channels/sms/config.js +30 -0
  9. package/v2Components/FormBuilder/Functional/channels/sms/getEditorErrorDescriptor.js +46 -0
  10. package/v2Components/FormBuilder/Functional/channels/sms/getLiquidContent.js +13 -0
  11. package/v2Components/FormBuilder/Functional/channels/sms/index.js +22 -0
  12. package/v2Components/FormBuilder/Functional/channels/sms/tests/getEditorErrorDescriptor.test.js +52 -0
  13. package/v2Components/FormBuilder/Functional/channels/sms/tests/getLiquidContent.test.js +25 -0
  14. package/v2Components/FormBuilder/Functional/channels/sms/tests/validate.test.js +87 -0
  15. package/v2Components/FormBuilder/Functional/channels/sms/validate.js +89 -0
  16. package/v2Components/FormBuilder/Functional/constants.js +39 -0
  17. package/v2Components/FormBuilder/Functional/core/schema/fieldRegistry.js +38 -0
  18. package/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js +85 -0
  19. package/v2Components/FormBuilder/Functional/core/store/formReducer.js +81 -0
  20. package/v2Components/FormBuilder/Functional/core/store/selectors.js +30 -0
  21. package/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js +91 -0
  22. package/v2Components/FormBuilder/Functional/index.js +26 -0
  23. package/v2Components/FormBuilder/Functional/layout/FieldSlot.js +59 -0
  24. package/v2Components/FormBuilder/Functional/layout/SchemaForm.js +32 -0
  25. package/v2Components/FormBuilder/Functional/layout/Section.js +118 -0
  26. package/v2Components/FormBuilder/Functional/renderers/smsRenderers.js +265 -0
  27. package/v2Components/FormBuilder/Functional/tests/channelRegistry.test.js +21 -0
  28. package/v2Components/FormBuilder/Functional/tests/fieldRegistry.test.js +65 -0
  29. package/v2Components/FormBuilder/Functional/tests/fieldSlot.test.js +97 -0
  30. package/v2Components/FormBuilder/Functional/tests/fixtures/smsParityCases.js +192 -0
  31. package/v2Components/FormBuilder/Functional/tests/formReducer.test.js +129 -0
  32. package/v2Components/FormBuilder/Functional/tests/initializeFormState.test.js +132 -0
  33. package/v2Components/FormBuilder/Functional/tests/schemaForm.test.js +40 -0
  34. package/v2Components/FormBuilder/Functional/tests/section.test.js +99 -0
  35. package/v2Components/FormBuilder/Functional/tests/selectors.test.js +67 -0
  36. package/v2Components/FormBuilder/Functional/tests/sms.crossFlowParity.test.js +155 -0
  37. package/v2Components/FormBuilder/Functional/tests/sms.liquid.test.js +172 -0
  38. package/v2Components/FormBuilder/Functional/tests/sms.rollout.test.js +122 -0
  39. package/v2Components/FormBuilder/Functional/tests/sms.shell.parity.test.js +329 -0
  40. package/v2Components/FormBuilder/Functional/tests/smsRenderers.test.js +162 -0
  41. package/v2Components/FormBuilder/Functional/tests/toLegacyFormData.test.js +95 -0
  42. package/v2Components/FormBuilder/_formBuilder.scss +8 -0
  43. package/v2Components/FormBuilder/index.js +41 -4479
  44. package/v2Components/FormBuilder/tests/__snapshots__/sms.characterization.test.js.snap +114 -0
  45. package/v2Components/FormBuilder/tests/entryGate.test.js +106 -0
  46. package/v2Components/FormBuilder/tests/sms.characterization.test.js +336 -0
  47. package/v2Components/TemplatePreview/coderabbits_comments +171 -0
  48. package/v2Containers/CreativesContainer/SlideBoxHeader.js +7 -0
  49. package/v2Containers/Ebill/index.js +31 -7
  50. package/v2Containers/Templates/index.js +1 -1
  51. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +96 -51
@@ -0,0 +1,171 @@
1
+ @coderabbitai
2
+ coderabbitai Bot
3
+ 2 days ago
4
+ ⚠️ Potential issue | 🟠 Major | ⚡ Quick win
5
+
6
+ Guard non-string channel values before calling toUpperCase().
7
+
8
+ Current normalization crashes for truthy non-string channel values instead of returning null.
9
+
10
+ Proposed fix
11
+ -export const getChannelAdapter = (channel) => adapters[(channel || '').toUpperCase()] || null;
12
+ +export const getChannelAdapter = (channel) => {
13
+ + const key = typeof channel === 'string' ? channel.toUpperCase() : '';
14
+ + return adapters[key] || null;
15
+ +};
16
+ 🤖 Prompt for AI Agents
17
+ Verify each finding against current code. Fix only still-valid issues, skip the
18
+ rest with a brief reason, keep changes minimal, and validate.
19
+
20
+ In `@app/v2Components/FormBuilder/Functional/channels/registry.js` at line 12, The
21
+ getChannelAdapter function does not guard against non-string values for the
22
+ channel parameter. When a truthy non-string value (like a number or object) is
23
+ passed, calling toUpperCase() on it will crash. Add a type check to ensure the
24
+ channel value is a string before attempting to call toUpperCase() on it. If
25
+ channel is not a string, return null directly. This prevents the crash while
26
+ maintaining the existing behavior for null, undefined, and string values.
27
+
28
+
29
+
30
+
31
+ coderabbitai Bot
32
+ 2 days ago
33
+ ⚠️ Potential issue | 🟠 Major | ⚡ Quick win
34
+
35
+ Guard MULTICOLS row dereference to prevent schema-driven crashes.
36
+
37
+ Line 38/39 assumes every row exists. A null row from schema data will throw before form initialization completes.
38
+
39
+ Proposed fix
40
+ - (section.inputFields || []).forEach((row) => (row.cols || []).forEach(visit));
41
+ - (section.actionFields || []).forEach((row) => (row.cols || []).forEach(visit));
42
+ + (section.inputFields || []).forEach((row) => (row?.cols || []).forEach(visit));
43
+ + (section.actionFields || []).forEach((row) => (row?.cols || []).forEach(visit));
44
+ 🤖 Prompt for AI Agents
45
+ Verify each finding against current code. Fix only still-valid issues, skip the
46
+ rest with a brief reason, keep changes minimal, and validate.
47
+
48
+ In `@app/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js`
49
+ around lines 37 - 40, The MULTICOLS case in the section type handling does not
50
+ guard against null or undefined row values before accessing the cols property,
51
+ which will cause a crash if schema data contains null rows. Add a null check for
52
+ each row within the forEach callbacks for both inputFields and actionFields
53
+ processing to ensure the row exists before attempting to access row.cols,
54
+ preventing dereferencing of null values during form initialization.
55
+
56
+
57
+
58
+
59
+ coderabbitai Bot
60
+ 2 days ago
61
+ ⚠️ Potential issue | 🟠 Major | ⚡ Quick win
62
+
63
+ Preserve original numeric tab indices when decoding legacy formData.
64
+
65
+ Line 52 compacts tab keys into a dense array. For sparse legacy keys (e.g., only 1 present), toLegacy writes them back as 0, changing payload shape and base-tab addressing.
66
+
67
+ Proposed fix
68
+ 📝 Committable suggestion
69
+ ‼️ IMPORTANT
70
+ Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
71
+
72
+ Suggested change
73
+ const orderedIndices = Object.keys(tabsByIndex)
74
+ .map(Number)
75
+ .sort((a, b) => a - b);
76
+ const tabs = orderedIndices.map((index) => tabsByIndex[index]);
77
+ const foundBase = tabs.findIndex((tab) => tab?.base);
78
+ const baseTabIndex = foundBase === -1 ? 0 : foundBase;
79
+ const orderedIndices = Object.keys(tabsByIndex)
80
+ .map(Number)
81
+ .sort((a, b) => a - b);
82
+ const tabs = [];
83
+ orderedIndices.forEach((index) => {
84
+ tabs[index] = tabsByIndex[index];
85
+ });
86
+ const foundBase = orderedIndices.find((index) => tabsByIndex[index]?.base);
87
+ const baseTabIndex = foundBase === undefined ? 0 : foundBase;
88
+ 🤖 Prompt for AI Agents
89
+ Verify each finding against current code. Fix only still-valid issues, skip the
90
+ rest with a brief reason, keep changes minimal, and validate.
91
+
92
+ In `@app/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js`
93
+ around lines 49 - 56, The issue is that the code compacts sparse tab indices
94
+ into a dense array by mapping orderedIndices to a new tabs array, which loses
95
+ the original numeric indices and changes the payload shape when converting back
96
+ to legacy format. Instead of creating a compacted tabs array using
97
+ orderedIndices.map, preserve the sparse structure by keeping the original
98
+ numeric keys from tabsByIndex and adjusting the baseTabIndex calculation to work
99
+ with the actual tab indices rather than array positions.
100
+
101
+
102
+ @coderabbitai
103
+ coderabbitai Bot
104
+ 2 days ago
105
+ ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
106
+
107
+ Fix TagList prop contract types/defaults in FormBuilderShell.
108
+
109
+ selectedOfferDetails and waitEventContextTags are typed opposite to how app/v2Containers/TagList/index.js consumes them (array and object respectively), which weakens contract checks and can hide wrong-shape regressions.
110
+
111
+ Suggested fix
112
+ FormBuilderShell.propTypes = {
113
+ @@
114
+ - selectedOfferDetails: PropTypes.object,
115
+ + selectedOfferDetails: PropTypes.array,
116
+ @@
117
+ - waitEventContextTags: PropTypes.array,
118
+ + waitEventContextTags: PropTypes.object,
119
+ @@
120
+ FormBuilderShell.defaultProps = {
121
+ @@
122
+ - selectedOfferDetails: undefined,
123
+ + selectedOfferDetails: [],
124
+ @@
125
+ - waitEventContextTags: [],
126
+ + waitEventContextTags: {},
127
+ Also applies to: 361-364
128
+
129
+ 🤖 Prompt for AI Agents
130
+ Verify each finding against current code. Fix only still-valid issues, skip the
131
+ rest with a brief reason, keep changes minimal, and validate.
132
+
133
+ In `@app/v2Components/FormBuilder/Functional/FormBuilderShell.js` around lines 348
134
+ - 350, The PropTypes definitions for selectedOfferDetails and
135
+ waitEventContextTags in the FormBuilderShell propTypes are typed incorrectly
136
+ compared to how they are consumed in the TagList component. Change
137
+ selectedOfferDetails from PropTypes.object to PropTypes.array, and change
138
+ waitEventContextTags from PropTypes.array to PropTypes.object. Apply the same
139
+ type corrections to any other instances of these props mentioned in the file
140
+ (lines 361-364).
141
+
142
+
143
+ @coderabbitai
144
+ coderabbitai Bot
145
+ 2 days ago
146
+ ⚠️ Potential issue | 🟠 Major | ⚡ Quick win
147
+
148
+ Characterization can unintentionally run Functional instead of Classic.
149
+
150
+ Line 22 imports the entry-gated FormBuilder (../index), and that gate can route SMS to Functional based on feature access. For a Classic golden suite, this makes the target non-deterministic and can hide Classic regressions.
151
+
152
+ Suggested fix
153
+ -import FormBuilder from '../index';
154
+ +import ClassicFormBuilder from '../Classic';
155
+ @@
156
+ -const ComponentToRender = injectIntl(FormBuilder);
157
+ +const ComponentToRender = injectIntl(ClassicFormBuilder);
158
+ Also applies to: 47-47
159
+
160
+ 🤖 Prompt for AI Agents
161
+ Verify each finding against current code. Fix only still-valid issues, skip the
162
+ rest with a brief reason, keep changes minimal, and validate.
163
+
164
+ In `@app/v2Components/FormBuilder/tests/sms.characterization.test.js` around lines
165
+ 22 - 23, The imports on line 22 and line 47 use the entry-gated FormBuilder from
166
+ ../index which can route SMS to Functional based on feature access. For the
167
+ Classic characterization test to be deterministic and properly test Classic
168
+ behavior, import the Classic FormBuilder implementation directly rather than
169
+ going through the gated entry point. Replace the imports from ../index with
170
+ direct imports to the Classic FormBuilder implementation to ensure the test
171
+ always uses the Classic version.
@@ -16,6 +16,7 @@ import { isTraiDLTEnable } from '../../utils/common';
16
16
  import { formatString } from '../../utils/Formatter';
17
17
  import {
18
18
  CAP_SPACE_12,
19
+ ICON_SIZE_M,
19
20
  } from '@capillarytech/cap-ui-library/styled/variables';
20
21
  import { WHATSAPP_HELP_DOC_LINK, JOURNEY } from './constants';
21
22
 
@@ -23,8 +24,14 @@ const { CapLabelInline } = CapLabel;
23
24
  const StyledLabel = styled(CapLabelInline)`
24
25
  margin-right: ${CAP_SPACE_12};
25
26
  `;
27
+ // CapIcons.BackIcon renders an <svg width="1em" height="1em">, so its size follows
28
+ // the inherited font-size. Pin it to the medium icon size (24px) and center it so
29
+ // the back arrow matches the header title instead of collapsing to a tiny glyph.
26
30
  const PrefixWrapper = styled.div`
27
31
  margin-right: 16px;
32
+ display: inline-flex;
33
+ align-items: center;
34
+ font-size: ${ICON_SIZE_M};
28
35
  `;
29
36
  const renderData = (type, value, channel) => (
30
37
  <StyledLabel className={channel?.toLowerCase() === ZALO ? 'zalo-template-name-spacing' : ''} type={type}>
@@ -39,6 +39,7 @@ export class Ebill extends React.Component { // eslint-disable-line react/prefer
39
39
  currentTab: 1,
40
40
  isEdit: false,
41
41
  loading: false,
42
+ isSaving: false,
42
43
  isFormValid: true,
43
44
  injectedTags: {},
44
45
  checkValidation: false,
@@ -570,26 +571,33 @@ export class Ebill extends React.Component { // eslint-disable-line react/prefer
570
571
  },
571
572
  };
572
573
  this.isTagLoaded = false;
574
+ this.isSubmitting = false;
573
575
  }
574
576
 
575
577
  componentWillMount() {
578
+ // NOTE: API dispatches must NOT live here. componentWillMount runs in React's
579
+ // render phase, which React 18 (createRoot/concurrent) may execute more than
580
+ // once per commit — causing every dispatched request to fire twice. All
581
+ // side-effects are in componentDidMount (commit phase, runs exactly once).
576
582
  if (this.props.params.id) {
577
- this.props.actions.getTemplateDetails(this.props.params.id, 'ebill');
578
583
  this.setState({isEdit: true});
579
584
  }
585
+ }
586
+
587
+ componentDidMount() {
588
+ if (this.props.params.id) {
589
+ this.props.actions.getTemplateDetails(this.props.params.id, 'ebill');
590
+ }
580
591
 
581
592
  if (!_.isEmpty(this.props.Templates.edmTemplate) && this.props.Templates.edmTemplate.versions.base.is_drag_drop) {
582
593
  this.props.actions.getCmsData('edm', this.props.Templates.edmTemplate.versions.base.drag_drop_id);
583
594
  }
584
- }
585
595
 
586
- componentDidMount() {
587
596
  const query = {
588
597
  layout: 'EBILL',
589
598
  type: 'LAYOUT',
590
599
  };
591
600
  this.props.globalActions.fetchSchemaForEntity(query);
592
-
593
601
  }
594
602
 
595
603
  componentWillReceiveProps(nextProps) {
@@ -609,7 +617,7 @@ export class Ebill extends React.Component { // eslint-disable-line react/prefer
609
617
  this.setEditState(nextProps.Ebill.templateDetails);
610
618
  }
611
619
 
612
- if (nextProps.Ebill.createResponse && nextProps.Ebill.createResponse.templateId) {
620
+ if (nextProps.Ebill.createResponse && nextProps.Ebill.createResponse.templateId && !_.isEqual(nextProps.Ebill.createResponse, this.props.Ebill.createResponse)) {
613
621
  let message;
614
622
  if (this.state.isEdit) {
615
623
  message = getMessageObject('success', this.props.intl.formatMessage(messages.ebillEditSuccess), true);
@@ -618,6 +626,8 @@ export class Ebill extends React.Component { // eslint-disable-line react/prefer
618
626
  }
619
627
  this.props.globalActions.addMessageToQueue(message);
620
628
  this.props.actions.clearCRUDResponse();
629
+ this.isSubmitting = false;
630
+ this.setState({ isSaving: false });
621
631
  const module = this.props.location.query.module ? this.props.location.query.module : 'default';
622
632
  const type = this.props.location.query.type;
623
633
  this.props.router.push({
@@ -629,6 +639,8 @@ export class Ebill extends React.Component { // eslint-disable-line react/prefer
629
639
  if (nextProps.Ebill.createTemplateError && !_.isEqual(nextProps.Ebill.createTemplateError, this.props.Ebill.createTemplateError)) {
630
640
  const message = getMessageObject('error', (nextProps.Ebill.createTemplateErrorMessage && nextProps.Ebill.createTemplateErrorMessage !== '') ? nextProps.Ebill.createTemplateErrorMessage : this.props.intl.formatMessage(messages.somethingWentWrong), true);
631
641
  this.props.globalActions.addMessageToQueue(message);
642
+ this.isSubmitting = false;
643
+ this.setState({ isSaving: false });
632
644
  }
633
645
 
634
646
 
@@ -773,11 +785,23 @@ export class Ebill extends React.Component { // eslint-disable-line react/prefer
773
785
  saveFormData = (formData) => {
774
786
  //Logic to save in db etc
775
787
  // const isValidated = this.validateFormData(formData);
776
- //
788
+ //
777
789
  // if (!isValidated) {
778
790
  // return;
779
791
  // }
780
-
792
+
793
+ // Guard against duplicate submissions. Uses a synchronous instance flag
794
+ // (NOT React state) because setState is async — two onSubmit invocations in
795
+ // the same tick would both read a stale `false` and each dispatch a
796
+ // createTemplate, firing POST /templates/EBILL twice. The instance flag
797
+ // flips synchronously, so the second call is dropped immediately.
798
+ // Reset once the create/update response or error is received in componentWillReceiveProps.
799
+ if (this.isSubmitting) {
800
+ return;
801
+ }
802
+ this.isSubmitting = true;
803
+ this.setState({ isSaving: true });
804
+
781
805
  const obj = {};
782
806
  obj.versions = {
783
807
  base: {},
@@ -646,7 +646,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
646
646
  }
647
647
 
648
648
 
649
- if (selectedChannel === "ebill" && nextProps.CreateEbill && nextProps.CreateEbill.createResponse && nextProps.CreateEbill.createResponse.templateId && nextProps.CreateEbill.createResponse.templateId !== '') {
649
+ if (selectedChannel === "ebill" && nextProps.CreateEbill && nextProps.CreateEbill.createResponse && nextProps.CreateEbill.createResponse.templateId && nextProps.CreateEbill.createResponse.templateId !== '' && !isEqual(nextProps.CreateEbill.createResponse, this.props.CreateEbill && this.props.CreateEbill.createResponse)) {
650
650
  const message = `${this.state.channel} ${this.props.intl.formatMessage(messages.templateDuplicateSuccess)}`;
651
651
  CapNotification.success({key: 'duplicateSucess', message});
652
652
  this.getAllTemplates({params, resetPage: true});