@capillarytech/creatives-library 8.0.353-alpha.0 → 8.0.353-alpha.2

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.353-alpha.0",
4
+ "version": "8.0.353-alpha.2",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -17,7 +17,7 @@ import CapTooltip from '@capillarytech/cap-ui-library/CapTooltip';
17
17
  import CapRow from '@capillarytech/cap-ui-library/CapRow';
18
18
  import { ANDROID, IOS } from '../constants';
19
19
  import messages from '../messages';
20
- import { getWhatsappQuickReply, getWhatsappCarouselButtonView, getWhatsappDocPreview } from '../../../v2Containers/Whatsapp/utils';
20
+ import { getWhatsappQuickReply, getWhatsappCarouselButtonView } from '../../../v2Containers/Whatsapp/utils';
21
21
  import { QUICK_REPLY, PHONE_NUMBER, WHATSAPP_CATEGORIES, TEMPLATE_VARIABLE_REGEX } from '../../../v2Containers/Whatsapp/constants';
22
22
  import videoPlay from '../../../assets/videoPlay.svg';
23
23
  import whatsappImageEmptyPreview from '../../TemplatePreview/assets/images/empty_image_preview.svg';
@@ -256,9 +256,9 @@ const WhatsAppPreviewContent = ({
256
256
  )}
257
257
 
258
258
  {/* Document Preview */}
259
- {content?.whatsappDocSource && content?.whatsappDocParams && (
259
+ {content?.docPreview && (
260
260
  <CapRow className="whatsapp-image">
261
- {getWhatsappDocPreview(content.whatsappDocParams)}
261
+ {content.docPreview}
262
262
  </CapRow>
263
263
  )}
264
264
 
@@ -27,7 +27,6 @@ jest.mock('../../../../v2Containers/Whatsapp/constants', () => ({
27
27
  authentication: 'authentication',
28
28
  },
29
29
  TEMPLATE_VARIABLE_REGEX: /\{\{.*?\}\}/,
30
- SIZE_UNITS: ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
31
30
  }));
32
31
 
33
32
  // Convert messages object to format expected by IntlProvider
@@ -311,13 +310,7 @@ describe('WhatsAppPreviewContent', () => {
311
310
  ...defaultProps,
312
311
  content: {
313
312
  templateMsg: 'Message',
314
- whatsappDocSource: 'https://example.com/doc.pdf',
315
- whatsappDocParams: {
316
- whatsappDocImg: 'https://example.com/doc-preview.png',
317
- whatsappDocName: 'test.pdf',
318
- whatsappDocPages: 2,
319
- whatsappDocSize: 12345,
320
- },
313
+ docPreview: <div data-testid="doc-preview">Document</div>,
321
314
  },
322
315
  };
323
316
 
@@ -327,7 +320,7 @@ describe('WhatsAppPreviewContent', () => {
327
320
  </TestWrapper>
328
321
  );
329
322
 
330
- expect(screen.getByAltText('upload-document-src')).toBeTruthy();
323
+ expect(screen.getByTestId('doc-preview')).toBeTruthy();
331
324
  });
332
325
  });
333
326
 
@@ -382,7 +382,12 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
382
382
  this.setState({formData: nextProps.formData, tabCount: nextProps.tabCount});
383
383
  // this.resetTabKeys(nextProps.formData, nextProps.tabCount);
384
384
  } else if (this.props.schema && this.props.schema.channel && this.props.schema.channel.toUpperCase() === 'EMAIL') {
385
- this.setState({formData: nextProps.formData});
385
+ // Skip state overwrite when only high-frequency fields changed — FormBuilder
386
+ // already updated them via updateFieldValueImmediately, so overwriting here
387
+ // would cause a redundant full re-render ~300ms after every keystroke.
388
+ if (!this._isOnlyHighFreqUpdate(nextProps.formData, this.state.formData)) {
389
+ this.setState({formData: nextProps.formData});
390
+ }
386
391
  }
387
392
 
388
393
  if (this.state.usingTabContainer && this.state.tabKey === '') {
@@ -423,14 +428,24 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
423
428
  ( !this.state.usingTabContainer || (this.state.usingTabContainer && nextProps.tabKey !== ''))
424
429
  && !_.isEqual(nextProps.formData, this.state.formData) &&
425
430
  !_.isEqual(nextProps.formData, this.props.formData)) {
426
- // Don't run validation if we're in Test & Preview mode
427
- if (!nextProps.isTestAndPreviewMode) {
428
- this.setState({formData: nextProps.formData, tabKey: nextProps.tabKey}, () => {
429
- this.validateForm();
430
- });
431
- } else {
432
- // Just update formData without validation
433
- this.setState({formData: nextProps.formData, tabKey: nextProps.tabKey});
431
+ // For EMAIL: skip state overwrite when only high-frequency fields (template-name /
432
+ // template-subject) changed — they are already correct via updateFieldValueImmediately.
433
+ const isEmailHighFreqOnly = (
434
+ this.props.schema &&
435
+ this.props.schema.channel &&
436
+ this.props.schema.channel.toUpperCase() === 'EMAIL' &&
437
+ this._isOnlyHighFreqUpdate(nextProps.formData, this.state.formData)
438
+ );
439
+ if (!isEmailHighFreqOnly) {
440
+ // Don't run validation if we're in Test & Preview mode
441
+ if (!nextProps.isTestAndPreviewMode) {
442
+ this.setState({formData: nextProps.formData, tabKey: nextProps.tabKey}, () => {
443
+ this.validateForm();
444
+ });
445
+ } else {
446
+ // Just update formData without validation
447
+ this.setState({formData: nextProps.formData, tabKey: nextProps.tabKey});
448
+ }
434
449
  }
435
450
  //this.resetTabKeys(nextProps.formData, nextProps.tabCount);
436
451
  } else if ((_.isEmpty(this.props.formData) || !this.props.formData) && _.isEmpty(this.state.formData)) {
@@ -448,7 +463,16 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
448
463
  this.setState({currentTab: nextProps.currentTab});
449
464
  }
450
465
 
451
- if (!_.isEmpty(nextProps.formData) && !_.isEqual(this.state.formData, nextProps.formData)) {
466
+ // For EMAIL: check high-freq first (cheap) to avoid the expensive _.isEqual
467
+ // and the setState + validateForm cascade triggered by every debounced keystroke.
468
+ const isEmailHighFreqOnly = (
469
+ !_.isEmpty(nextProps.formData) &&
470
+ this.props.schema &&
471
+ this.props.schema.channel &&
472
+ this.props.schema.channel.toUpperCase() === 'EMAIL' &&
473
+ this._isOnlyHighFreqUpdate(nextProps.formData, this.state.formData)
474
+ );
475
+ if (!isEmailHighFreqOnly && !_.isEmpty(nextProps.formData) && !_.isEqual(this.state.formData, nextProps.formData)) {
452
476
  if (nextProps.isNewVersionFlow) {
453
477
  const tabKey = (this.state.tabKey !== nextProps.formData[nextProps.currentTab - 1].tabKey) ? nextProps.formData[nextProps.currentTab - 1].tabKey : this.state.tabKey;
454
478
 
@@ -2181,6 +2205,20 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2181
2205
  this.debouncedUpdateFormData(data, val, event, true);
2182
2206
  }
2183
2207
 
2208
+ // Returns true when the only differences between newData and currentData are
2209
+ // the high-frequency standalone fields (template-name / template-subject).
2210
+ // Uses reference equality for all other keys — safe because shallow spreads in
2211
+ // optimizedFormDataUpdate and updateFieldValueImmediately preserve nested refs.
2212
+ _isOnlyHighFreqUpdate(newData, currentData) {
2213
+ if (!newData || !currentData) return false;
2214
+ // isTemplateNameEdited is set alongside template-name by performTemplateNameUpdate
2215
+ // and treated as a high-freq field so it doesn't break the reference equality check.
2216
+ const HIGH_FREQ_FIELDS = ['template-name', 'template-subject', 'isTemplateNameEdited'];
2217
+ return Object.keys(newData).every(
2218
+ key => HIGH_FREQ_FIELDS.includes(key) || newData[key] === currentData[key]
2219
+ );
2220
+ }
2221
+
2184
2222
  // Update field value immediately for UI feedback
2185
2223
  updateFieldValueImmediately(data, val) {
2186
2224
  const currentFormData = this.state.formData;
@@ -1,4 +1,43 @@
1
1
  import React from 'react';
2
+
3
+ // Isolated input for the email template name field.
4
+ // Manages its own value in local state so keystrokes only re-render this
5
+ // small component, not the entire CreativesContainer → Email → FormBuilder tree.
6
+ class TemplateNameInputField extends React.Component {
7
+ constructor(props) {
8
+ super(props);
9
+ this.state = { localValue: props.initialValue || '' };
10
+ }
11
+
12
+ componentDidUpdate(prevProps) {
13
+ // Sync from props only when the external value changed AND the user hasn't
14
+ // diverged from the previous prop value. This handles async data-load in edit
15
+ // mode without overwriting what the user is actively typing.
16
+ if (
17
+ prevProps.initialValue !== this.props.initialValue &&
18
+ this.state.localValue === (prevProps.initialValue || '')
19
+ ) {
20
+ this.setState({ localValue: this.props.initialValue || '' });
21
+ }
22
+ }
23
+
24
+ handleChange = (ev) => {
25
+ const { value } = ev.currentTarget;
26
+ this.setState({ localValue: value });
27
+ this.props.onChange(value);
28
+ };
29
+
30
+ render() {
31
+ const { onChange: _onChange, initialValue: _initialValue, ...rest } = this.props;
32
+ return (
33
+ <CapInput
34
+ {...rest}
35
+ value={this.state.localValue}
36
+ onChange={this.handleChange}
37
+ />
38
+ );
39
+ }
40
+ }
2
41
  import PropTypes from 'prop-types';
3
42
  import {
4
43
  CAP_SPACE_16, CAP_SPACE_32, CAP_SPACE_56, CAP_SPACE_64,
@@ -191,7 +230,10 @@ export class Creatives extends React.Component {
191
230
  // Performance optimized template name update
192
231
  performTemplateNameUpdate = (value, formData, onFormDataChange) => {
193
232
  const isEmptyTemplateName = !value.trim();
194
- const newFormData = { ...formData, 'template-name': value, 'isTemplateNameEdited': true };
233
+ // _highFreqField signals Email's onFormDataChange that only a high-frequency
234
+ // standalone field changed, enabling the fast-path cache in getFormDataForBuilder
235
+ // and skipping the expensive FormBuilder re-render + validateForm cascade.
236
+ const newFormData = { ...formData, 'template-name': value, 'isTemplateNameEdited': true, _highFreqField: 'template-name' };
195
237
 
196
238
  this.setState({ isTemplateNameEmpty: isEmptyTemplateName });
197
239
  onFormDataChange(newFormData);
@@ -1753,30 +1795,24 @@ export class Creatives extends React.Component {
1753
1795
  } />
1754
1796
  )
1755
1797
 
1756
- templateNameComponentInput = ({ formData, onFormDataChange, name }) => {
1757
- // Use local state for immediate UI feedback, fallback to prop value
1758
- const displayValue = this.state.localTemplateName !== '' ? this.state.localTemplateName : name;
1759
-
1760
- return (
1761
- <CapInput
1762
- value={displayValue}
1763
- suffix={<span />}
1764
- onBlur={() => {
1765
- this.setState({
1766
- isEditName: false,
1767
- localTemplateName: '', // Clear local state on blur
1768
- }, () => {
1769
- this.showTemplateName({ formData, onFormDataChange });
1770
- });
1771
- }}
1772
- onChange={(ev) => {
1773
- const { value } = ev.currentTarget;
1774
- // Use optimized update for better performance
1775
- this.updateTemplateNameImmediately(value, formData, onFormDataChange);
1776
- }}
1777
- />
1778
- );
1779
- }
1798
+ templateNameComponentInput = ({ formData, onFormDataChange, name }) => (
1799
+ <TemplateNameInputField
1800
+ initialValue={name}
1801
+ suffix={<span />}
1802
+ onBlur={() => {
1803
+ this.setState({ isEditName: false }, () => {
1804
+ this.showTemplateName({ formData, onFormDataChange });
1805
+ });
1806
+ }}
1807
+ onChange={(value) => {
1808
+ const isEmptyTemplateName = !value.trim();
1809
+ if (this.state.isTemplateNameEmpty !== isEmptyTemplateName) {
1810
+ this.setState({ isTemplateNameEmpty: isEmptyTemplateName });
1811
+ }
1812
+ this.debouncedTemplateNameUpdate(value, formData, onFormDataChange);
1813
+ }}
1814
+ />
1815
+ )
1780
1816
 
1781
1817
  showTemplateName = ({ formData, onFormDataChange }) => { //gets called from email/index after template data is fetched
1782
1818
  const {
@@ -795,9 +795,16 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
795
795
  delete window?.[CREATIVES_S3_ASSET_FILESIZES];
796
796
  }
797
797
 
798
- onFormDataChange = (updatedFormData, tabCount, currentTab) => {
798
+ // performFormDataUpdate in FormBuilder passes `val` as the 4th arg to props.onChange.
799
+ // CreativesContainer.performTemplateNameUpdate passes _highFreqField on the formData object.
800
+ // Both paths set _highFreqUpdate so getFormDataForBuilder can use the fast-path cache.
801
+ onFormDataChange = (updatedFormData, tabCount, currentTab, val) => {
799
802
  // this.transformFormData(formData);
800
803
  const formData = {...updatedFormData};
804
+ // Consume and clean up the CC-path signal before storing in state
805
+ const highFreqField = (val && val.id) || updatedFormData._highFreqField;
806
+ delete formData._highFreqField;
807
+
801
808
  const {defaultData = {}, isFullMode, showTemplateName} = this.props;
802
809
  const templateName = formData['template-name'];
803
810
  const defaultTemplateName = _.get(defaultData, 'template-name', "");
@@ -809,6 +816,9 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
809
816
  formData['template-name'] = templateName;
810
817
  }
811
818
 
819
+ // Must be set before setState so getFormDataForBuilder reads it during the triggered re-render.
820
+ const HIGH_FREQ_FIELDS = ['template-name', 'template-subject'];
821
+ this._highFreqUpdate = !!(highFreqField && HIGH_FREQ_FIELDS.includes(highFreqField));
812
822
 
813
823
  this.setState({formData, tabCount, isSchemaChanged: false}, () => {
814
824
  if (this.props.isFullMode && showTemplateName) {
@@ -821,6 +831,27 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
821
831
  //this.resetCkEditorInstance(currentTab, formData);
822
832
  }
823
833
 
834
+ // Returns a formData object safe to pass to FormBuilder.
835
+ // For high-frequency field updates (template-name / template-subject) patches only
836
+ // those fields into the existing cache, avoiding an expensive _.cloneDeep of the
837
+ // entire email formData (HTML content, tabs, language variants) on every keystroke.
838
+ // All other operations (tab changes, language add/delete, etc.) still get a full clone.
839
+ getFormDataForBuilder = () => {
840
+ const formData = this.state.formData;
841
+ if (this._highFreqUpdate && this._formDataBuilderCache) {
842
+ this._formDataBuilderCache = {
843
+ ...this._formDataBuilderCache,
844
+ 'template-name': formData['template-name'],
845
+ 'template-subject': formData['template-subject'],
846
+ 'isTemplateNameEdited': formData['isTemplateNameEdited'],
847
+ };
848
+ } else {
849
+ this._formDataBuilderCache = _.cloneDeep(formData);
850
+ }
851
+ this._highFreqUpdate = false;
852
+ return this._formDataBuilderCache;
853
+ }
854
+
824
855
  onChange = (evt) => {
825
856
  const {isFullMode, showTemplateName} = this.props;
826
857
  const formData = _.cloneDeep(this.state.formData);
@@ -3129,7 +3160,7 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
3129
3160
  onChange={this.onFormDataChange}
3130
3161
  currentTab={this.state.currentTab}
3131
3162
  parent={this}
3132
- formData={_.cloneDeep(this.state.formData)}
3163
+ formData={this.getFormDataForBuilder()}
3133
3164
  location={this.props.location}
3134
3165
  tabKey={this.state.tabKey}
3135
3166
  tags={tags}
@@ -3058,8 +3058,7 @@ const isAuthenticationTemplate = isEqual(templateCategory, WHATSAPP_CATEGORIES.a
3058
3058
  whatsappVideoSrcAndPreview?.whatsappVideoPreviewImg;
3059
3059
  break;
3060
3060
  case WHATSAPP_MEDIA_TYPES.DOCUMENT:
3061
- mediaPreview.whatsappDocSource = whatsappDocSource;
3062
- mediaPreview.whatsappDocParams = whatsappDocParams;
3061
+ mediaPreview.docPreview = docPreview;
3063
3062
  break;
3064
3063
  default:
3065
3064
  break;
@@ -3137,9 +3136,7 @@ const isAuthenticationTemplate = isEqual(templateCategory, WHATSAPP_CATEGORIES.a
3137
3136
  whatsappVideoSrcAndPreview?.whatsappVideoPreviewImg;
3138
3137
  break;
3139
3138
  case WHATSAPP_MEDIA_TYPES.DOCUMENT:
3140
- // Intentionally not setting mediaPreview.docPreview = docPreview here — docPreview is JSX
3141
- // and this object is JSON.stringified by CommonTestAndPreview (circular ref crash).
3142
- // whatsappDocParams below carries the raw data; WhatsAppPreviewContent renders from that.
3139
+ mediaPreview.docPreview = docPreview;
3143
3140
  break;
3144
3141
  default:
3145
3142
  break;