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

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.2",
4
+ "version": "8.0.353-alpha.4",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -79,6 +79,69 @@ const errorMessageForTags = {
79
79
  TAG_BRACKET_COUNT_MISMATCH_ERROR: 'tagBracketCountMismatchError'
80
80
  };
81
81
 
82
+ // Isolated input for EMAIL template-name: only this tiny component re-renders on each keystroke.
83
+ // formData is updated only on blur (onCommit), eliminating all re-renders during typing.
84
+ class HighFreqInput extends React.Component {
85
+ constructor(props) {
86
+ super(props);
87
+ this.state = { localValue: props.value || '' };
88
+ }
89
+
90
+ componentDidUpdate(prevProps) {
91
+ if (prevProps.value !== this.props.value && this.state.localValue !== this.props.value) {
92
+ this.setState({ localValue: this.props.value || '' });
93
+ }
94
+ }
95
+
96
+ handleChange = (e) => {
97
+ this.setState({ localValue: e.target.value });
98
+ };
99
+
100
+ handleBlur = (e) => {
101
+ this.props.onCommit(this.state.localValue);
102
+ if (this.props.onBlur) this.props.onBlur(e);
103
+ };
104
+
105
+ render() {
106
+ const { value: _v, onCommit: _oc, onBlur: _ob, ...rest } = this.props;
107
+ return <CapInput {...rest} value={this.state.localValue} onChange={this.handleChange} onBlur={this.handleBlur} />;
108
+ }
109
+ }
110
+
111
+ // Isolated wrapper for EMAIL template-subject: blur-only commit, same as HighFreqInput.
112
+ class HighFreqTagInput extends React.Component {
113
+ constructor(props) {
114
+ super(props);
115
+ this.state = { localInputValue: props.inputValue || '' };
116
+ }
117
+
118
+ componentDidUpdate(prevProps) {
119
+ if (prevProps.inputValue !== this.props.inputValue && this.state.localInputValue !== this.props.inputValue) {
120
+ this.setState({ localInputValue: this.props.inputValue || '' });
121
+ }
122
+ }
123
+
124
+ handleInputChange = (e) => {
125
+ this.setState({ localInputValue: e.target.value });
126
+ };
127
+
128
+ handleBlur = () => {
129
+ this.props.onCommit(this.state.localInputValue);
130
+ };
131
+
132
+ render() {
133
+ const { inputValue: _iv, onCommit: _oc, inputOnChange: _ic, ...rest } = this.props;
134
+ return (
135
+ <CapTagListWithInput
136
+ {...rest}
137
+ inputValue={this.state.localInputValue}
138
+ inputOnChange={this.handleInputChange}
139
+ inputProps={{ ...(this.props.inputProps || {}), onBlur: this.handleBlur }}
140
+ />
141
+ );
142
+ }
143
+ }
144
+
82
145
  class FormBuilder extends React.Component { // eslint-disable-line react/prefer-stateless-function
83
146
  constructor(props) {
84
147
  super(props);
@@ -352,6 +415,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
352
415
  this.setState({tabCount: nextProps.tabCount});
353
416
  }
354
417
  if (nextProps.startValidation && nextProps.startValidation !== false && this.props.startValidation !== nextProps.startValidation) {
418
+ if (this.debouncedUpdateFormData) this.debouncedUpdateFormData.flush();
355
419
  this.setState({checkValidation: true});
356
420
  this.validateForm(null, null, true, true, () => {
357
421
  //triggering the saveFormData or onSubmit when validation sets isFormValid to TRUE
@@ -382,12 +446,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
382
446
  this.setState({formData: nextProps.formData, tabCount: nextProps.tabCount});
383
447
  // this.resetTabKeys(nextProps.formData, nextProps.tabCount);
384
448
  } else if (this.props.schema && this.props.schema.channel && this.props.schema.channel.toUpperCase() === 'EMAIL') {
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
- }
449
+ this.setState({formData: nextProps.formData});
391
450
  }
392
451
 
393
452
  if (this.state.usingTabContainer && this.state.tabKey === '') {
@@ -428,24 +487,14 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
428
487
  ( !this.state.usingTabContainer || (this.state.usingTabContainer && nextProps.tabKey !== ''))
429
488
  && !_.isEqual(nextProps.formData, this.state.formData) &&
430
489
  !_.isEqual(nextProps.formData, this.props.formData)) {
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
- }
490
+ // Don't run validation if we're in Test & Preview mode
491
+ if (!nextProps.isTestAndPreviewMode) {
492
+ this.setState({formData: nextProps.formData, tabKey: nextProps.tabKey}, () => {
493
+ this.validateForm();
494
+ });
495
+ } else {
496
+ // Just update formData without validation
497
+ this.setState({formData: nextProps.formData, tabKey: nextProps.tabKey});
449
498
  }
450
499
  //this.resetTabKeys(nextProps.formData, nextProps.tabCount);
451
500
  } else if ((_.isEmpty(this.props.formData) || !this.props.formData) && _.isEmpty(this.state.formData)) {
@@ -463,16 +512,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
463
512
  this.setState({currentTab: nextProps.currentTab});
464
513
  }
465
514
 
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)) {
515
+ if (!_.isEmpty(nextProps.formData) && !_.isEqual(this.state.formData, nextProps.formData)) {
476
516
  if (nextProps.isNewVersionFlow) {
477
517
  const tabKey = (this.state.tabKey !== nextProps.formData[nextProps.currentTab - 1].tabKey) ? nextProps.formData[nextProps.currentTab - 1].tabKey : this.state.tabKey;
478
518
 
@@ -2205,20 +2245,6 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2205
2245
  this.debouncedUpdateFormData(data, val, event, true);
2206
2246
  }
2207
2247
 
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
-
2222
2248
  // Update field value immediately for UI feedback
2223
2249
  updateFieldValueImmediately(data, val) {
2224
2250
  const currentFormData = this.state.formData;
@@ -3448,40 +3474,62 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3448
3474
  ? formatMessage(messages.personalizationTagsErrorMessage)
3449
3475
  : (errorType === TAG_BRACKET_COUNT_MISMATCH_ERROR ? formatMessage(globalMessages.unbalanacedCurlyBraces) : (val.errorMessage && ifError ? val.errorMessage : ''));
3450
3476
  if (styling === 'semantic') {
3451
- columns.push(
3452
- <CapColumn key={val.id} span={val.width} offset={val.offset} style={val.style || {}}>
3453
- <CapInput
3454
- id={val.id}
3455
- errorMessage={errorMessageText}
3456
- label={val.label}
3457
- inductiveText={val.inductiveText}
3458
- className={`input-primary chart-name-input${ifError ? ' error' : ''}`}
3459
- // fluid={val.fluid}
3460
- style={val.style ? val.style : {}}
3461
- placeholder={val.placeholder}
3462
- onChange={(e) => this.updateFormData(e.target.value, val)}
3463
- onBlur={(e) => this.handleFieldBlur(e, val)}
3464
- value={value || ""}
3465
- defaultValue={isVersionEnable ? this.state.formData[`${this.state.currentTab - 1}`][val.id] : this.state.formData[val.id]}
3466
- disabled={val.disabled}
3467
- size={val.size || "default"}
3468
- />
3469
- {this.props.schema?.channel === EMAIL &&
3470
- !aiContentBotDisabled && (
3471
- <CapAskAira.ContentGenerationBot
3472
- text={value || ""}
3473
- setText={this.handleSetText.bind(this, val)}
3474
- iconPlacement="float-br"
3475
- iconSize="1.6rem"
3476
- rootStyle={{
3477
- bottom: "0.2rem",
3478
- right: "0.2rem",
3479
- left: "auto",
3480
- }}
3481
- />
3482
- )}
3483
- </CapColumn>
3484
- );
3477
+ const isEmailStandaloneHighFreq = val.standalone && this.props.schema?.channel?.toUpperCase() === 'EMAIL';
3478
+ if (isEmailStandaloneHighFreq) {
3479
+ columns.push(
3480
+ <CapColumn key={val.id} span={val.width} offset={val.offset} style={val.style || {}}>
3481
+ <HighFreqInput
3482
+ id={val.id}
3483
+ errorMessage={errorMessageText}
3484
+ label={val.label}
3485
+ inductiveText={val.inductiveText}
3486
+ className={`input-primary chart-name-input${ifError ? ' error' : ''}`}
3487
+ style={val.style ? val.style : {}}
3488
+ placeholder={val.placeholder}
3489
+ onCommit={(newValue) => this.performFormDataUpdate(newValue, val)}
3490
+ onBlur={(e) => this.handleFieldBlur(e, val)}
3491
+ value={value || ""}
3492
+ disabled={val.disabled}
3493
+ size={val.size || "default"}
3494
+ />
3495
+ </CapColumn>
3496
+ );
3497
+ } else {
3498
+ columns.push(
3499
+ <CapColumn key={val.id} span={val.width} offset={val.offset} style={val.style || {}}>
3500
+ <CapInput
3501
+ id={val.id}
3502
+ errorMessage={errorMessageText}
3503
+ label={val.label}
3504
+ inductiveText={val.inductiveText}
3505
+ className={`input-primary chart-name-input${ifError ? ' error' : ''}`}
3506
+ // fluid={val.fluid}
3507
+ style={val.style ? val.style : {}}
3508
+ placeholder={val.placeholder}
3509
+ onChange={(e) => this.updateFormData(e.target.value, val)}
3510
+ onBlur={(e) => this.handleFieldBlur(e, val)}
3511
+ value={value || ""}
3512
+ defaultValue={isVersionEnable ? this.state.formData[`${this.state.currentTab - 1}`][val.id] : this.state.formData[val.id]}
3513
+ disabled={val.disabled}
3514
+ size={val.size || "default"}
3515
+ />
3516
+ {this.props.schema?.channel === EMAIL &&
3517
+ !aiContentBotDisabled && (
3518
+ <CapAskAira.ContentGenerationBot
3519
+ text={value || ""}
3520
+ setText={this.handleSetText.bind(this, val)}
3521
+ iconPlacement="float-br"
3522
+ iconSize="1.6rem"
3523
+ rootStyle={{
3524
+ bottom: "0.2rem",
3525
+ right: "0.2rem",
3526
+ left: "auto",
3527
+ }}
3528
+ />
3529
+ )}
3530
+ </CapColumn>
3531
+ );
3532
+ }
3485
3533
  }
3486
3534
  break;
3487
3535
 
@@ -3722,37 +3770,48 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3722
3770
  isBEEAppEnableForCapTagList === false ||
3723
3771
  channelForCapTagList !== 'EMAIL'
3724
3772
  ) {
3773
+ const isEmailStandaloneSubject = val.standalone && channelForCapTagList === 'EMAIL' && val.id === 'template-subject';
3774
+ const tagListProps = {
3775
+ key: `input-${val.id}`,
3776
+ inputId: val.id,
3777
+ inputValue: this.state.formData[val.id] || '',
3778
+ inputPlaceholder: val.placeholder || '',
3779
+ inputErrorMessage: val.errorMessage && ifError ? val.errorMessage : '',
3780
+ inputRequired: val.required || false,
3781
+ inputDisabled: val.disabled || false,
3782
+ headingText: val.label || '',
3783
+ headingStyle: val.headingStyle || { marginTop: '3%', marginRight: '79%' },
3784
+ headingType: "h4",
3785
+ onTagSelect: (data) => this.callChildEvent(data, val, 'onTagSelect'),
3786
+ onContextChange: this.props.onContextChange,
3787
+ location: this.props.location,
3788
+ tags: this.props.tags ? this.props.tags : [],
3789
+ injectedTags: this.props.injectedTags ? this.props.injectedTags : {},
3790
+ className: val.className ? val.className : '',
3791
+ userLocale: this.state.translationLang,
3792
+ selectedOfferDetails: this.props.selectedOfferDetails,
3793
+ eventContextTags: this.props?.eventContextTags,
3794
+ waitEventContextTags: this.props?.waitEventContextTags,
3795
+ moduleFilterEnabled: moduleFilterEnabledForCapTagList,
3796
+ containerStyle: val.style || {},
3797
+ inputProps: val.inputProps || {},
3798
+ showInput: val.showInput !== false,
3799
+ showTagList: val.showTagList !== false,
3800
+ restrictPersonalization: this.props.restrictPersonalization,
3801
+ };
3725
3802
  columns.push(
3726
3803
  <CapColumn key={`input-${val.id}`} offset={val.offset} span={val.width ? val.width : ''} style={val.style ? val.style : {marginBottom: '16px'}}>
3727
- <CapTagListWithInput
3728
- key={`input-${val.id}`}
3729
- inputId={val.id}
3730
- inputValue={this.state.formData[val.id] || ''}
3731
- inputOnChange={(e) => this.updateFormData(e.target.value, val)}
3732
- inputPlaceholder={val.placeholder || ''}
3733
- inputErrorMessage={val.errorMessage && ifError ? val.errorMessage : ''}
3734
- inputRequired={val.required || false}
3735
- inputDisabled={val.disabled || false}
3736
- headingText={val.label || ''}
3737
- headingStyle={val.headingStyle || { marginTop: '3%', marginRight: '79%' }}
3738
- headingType="h4"
3739
- onTagSelect={(data) => this.callChildEvent(data, val, 'onTagSelect')}
3740
- onContextChange={this.props.onContextChange}
3741
- location={this.props.location}
3742
- tags={this.props.tags ? this.props.tags : []}
3743
- injectedTags={this.props.injectedTags ? this.props.injectedTags : {}}
3744
- className={val.className ? val.className : ''}
3745
- userLocale={this.state.translationLang}
3746
- selectedOfferDetails={this.props.selectedOfferDetails}
3747
- eventContextTags={this.props?.eventContextTags}
3748
- waitEventContextTags={this.props?.waitEventContextTags}
3749
- moduleFilterEnabled={moduleFilterEnabledForCapTagList}
3750
- containerStyle={val.style || {}}
3751
- inputProps={val.inputProps || {}}
3752
- showInput={val.showInput !== false}
3753
- showTagList={val.showTagList !== false}
3754
- restrictPersonalization={this.props.restrictPersonalization}
3755
- />
3804
+ {isEmailStandaloneSubject ? (
3805
+ <HighFreqTagInput
3806
+ {...tagListProps}
3807
+ onCommit={(newValue) => this.performFormDataUpdate(newValue, val)}
3808
+ />
3809
+ ) : (
3810
+ <CapTagListWithInput
3811
+ {...tagListProps}
3812
+ inputOnChange={(e) => this.updateFormData(e.target.value, val)}
3813
+ />
3814
+ )}
3756
3815
  </CapColumn>
3757
3816
  );
3758
3817
  }
@@ -24,16 +24,21 @@ class TemplateNameInputField extends React.Component {
24
24
  handleChange = (ev) => {
25
25
  const { value } = ev.currentTarget;
26
26
  this.setState({ localValue: value });
27
- this.props.onChange(value);
27
+ if (this.props.onChange) this.props.onChange(value);
28
+ };
29
+
30
+ handleBlur = () => {
31
+ if (this.props.onBlur) this.props.onBlur(this.state.localValue);
28
32
  };
29
33
 
30
34
  render() {
31
- const { onChange: _onChange, initialValue: _initialValue, ...rest } = this.props;
35
+ const { onChange: _onChange, initialValue: _initialValue, onBlur: _ob, ...rest } = this.props;
32
36
  return (
33
37
  <CapInput
34
38
  {...rest}
35
39
  value={this.state.localValue}
36
40
  onChange={this.handleChange}
41
+ onBlur={this.handleBlur}
37
42
  />
38
43
  );
39
44
  }
@@ -230,10 +235,7 @@ export class Creatives extends React.Component {
230
235
  // Performance optimized template name update
231
236
  performTemplateNameUpdate = (value, formData, onFormDataChange) => {
232
237
  const isEmptyTemplateName = !value.trim();
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' };
238
+ const newFormData = { ...formData, 'template-name': value, 'isTemplateNameEdited': true };
237
239
 
238
240
  this.setState({ isTemplateNameEmpty: isEmptyTemplateName });
239
241
  onFormDataChange(newFormData);
@@ -1799,17 +1801,15 @@ export class Creatives extends React.Component {
1799
1801
  <TemplateNameInputField
1800
1802
  initialValue={name}
1801
1803
  suffix={<span />}
1802
- onBlur={() => {
1803
- this.setState({ isEditName: false }, () => {
1804
- this.showTemplateName({ formData, onFormDataChange });
1805
- });
1804
+ onBlur={(committedValue) => {
1805
+ this.performTemplateNameUpdate(committedValue, formData, onFormDataChange);
1806
+ this.setState({ isEditName: false });
1806
1807
  }}
1807
1808
  onChange={(value) => {
1808
1809
  const isEmptyTemplateName = !value.trim();
1809
1810
  if (this.state.isTemplateNameEmpty !== isEmptyTemplateName) {
1810
1811
  this.setState({ isTemplateNameEmpty: isEmptyTemplateName });
1811
1812
  }
1812
- this.debouncedTemplateNameUpdate(value, formData, onFormDataChange);
1813
1813
  }}
1814
1814
  />
1815
1815
  )
@@ -795,15 +795,9 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
795
795
  delete window?.[CREATIVES_S3_ASSET_FILESIZES];
796
796
  }
797
797
 
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) => {
798
+ onFormDataChange = (updatedFormData, tabCount, currentTab) => {
802
799
  // this.transformFormData(formData);
803
800
  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
802
  const {defaultData = {}, isFullMode, showTemplateName} = this.props;
809
803
  const templateName = formData['template-name'];
@@ -816,10 +810,6 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
816
810
  formData['template-name'] = templateName;
817
811
  }
818
812
 
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));
822
-
823
813
  this.setState({formData, tabCount, isSchemaChanged: false}, () => {
824
814
  if (this.props.isFullMode && showTemplateName) {
825
815
  showTemplateName({formData, onFormDataChange: this.onFormDataChange});
@@ -831,27 +821,6 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
831
821
  //this.resetCkEditorInstance(currentTab, formData);
832
822
  }
833
823
 
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
-
855
824
  onChange = (evt) => {
856
825
  const {isFullMode, showTemplateName} = this.props;
857
826
  const formData = _.cloneDeep(this.state.formData);
@@ -3160,7 +3129,7 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
3160
3129
  onChange={this.onFormDataChange}
3161
3130
  currentTab={this.state.currentTab}
3162
3131
  parent={this}
3163
- formData={this.getFormDataForBuilder()}
3132
+ formData={_.cloneDeep(this.state.formData)}
3164
3133
  location={this.props.location}
3165
3134
  tabKey={this.state.tabKey}
3166
3135
  tags={tags}