@capillarytech/creatives-library 8.0.279 → 8.0.281

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.279",
4
+ "version": "8.0.281",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -1519,8 +1519,13 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1519
1519
  response.isBraceError = false;
1520
1520
  response.isContentEmpty = false;
1521
1521
  const contentForValidation = isEmail ? convert(content, GLOBAL_CONVERT_OPTIONS) : content ;
1522
- // Run tag validation (missing + unsupported) for library mode OR for email channel (scenario 4: full mode email with CK)
1523
- if(tags && tags.length && (!isFullMode || isEmail)) {
1522
+ const isModuleTypeOutbound = (this.props?.moduleType || '').toUpperCase() === OUTBOUND;
1523
+ // Run tag validation (missing + unsupported): library mode, or full mode with liquid support, or
1524
+ // legacy Email (CK Editor) when unsubscribe is required (EMAIL_UNSUBSCRIBE_TAG_MANDATORY false) so missing-unsubscribe error shows
1525
+ const shouldRunTagValidation = !isFullMode
1526
+ || (isEmail && hasLiquidSupportFeature())
1527
+ || (isEmail && !isEmailUnsubscribeTagMandatory() && isModuleTypeOutbound);
1528
+ if (tags && tags.length && !isFullMode) {
1524
1529
  _.forEach(tags, (tag) => {
1525
1530
  _.forEach(tag.definition.supportedModules, (module) => {
1526
1531
  if (module.mandatory && (currentModule === module.context)) {
@@ -1531,6 +1536,14 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1531
1536
  }
1532
1537
  });
1533
1538
  });
1539
+ // Legacy Email (CK Editor): when unsubscribe is required, ensure we validate it even if tag schema didn't mark it mandatory for this module
1540
+ if (isEmail && !isEmailUnsubscribeTagMandatory() && isModuleTypeOutbound) {
1541
+ const hasUnsubscribeInContent = /{{unsubscribe(\(#[a-zA-Z\d]{6}\))?}}/g.test(content);
1542
+ if (!hasUnsubscribeInContent && response.missingTags.indexOf('unsubscribe') === -1) {
1543
+ response.valid = false;
1544
+ response.missingTags.push('unsubscribe');
1545
+ }
1546
+ }
1534
1547
  const regex = /{{[(A-Z\w+(\s\w+)*$\(\)@!#$%^&*~.,/\\]+}}/g;
1535
1548
  let match = regex.exec(content);
1536
1549
  const regexImgSrc=/<img[^>]*\bsrc\s*=\s*"[^"]*{{customer_barcode}}[^"]*"/;
@@ -1539,7 +1552,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1539
1552
  let matchCustomerBarcode = regexCustomerBarcode.exec(content);
1540
1553
  // \S matches anything other than a space, a tab, a newline, or a carriage return.
1541
1554
  const validString= /\S/.test(contentForValidation);
1542
- if (isEmailUnsubscribeTagMandatory() && isEmail && this.props?.moduleType === OUTBOUND) {
1555
+ if (isEmailUnsubscribeTagMandatory() && isEmail && isModuleTypeOutbound) {
1543
1556
  const missingTagIndex = response?.missingTags?.indexOf("unsubscribe");
1544
1557
  if(missingTagIndex != -1) { //skip regex tags for mandatory tags also
1545
1558
  response?.missingTags?.splice(missingTagIndex, 1);
@@ -682,8 +682,9 @@ export function SlideBoxContent(props) {
682
682
  {(isEditEmailWithId || isEmailEditWithContent) && (
683
683
  (() => {
684
684
  const supportCKEditor = commonUtil.hasSupportCKEditor();
685
- // When supportCKEditor is true: Always use Email component (legacy flow)
686
- if (supportCKEditor || templateData?.is_drag_drop) {
685
+ // When supportCKEditor is true: Use Email component (legacy flow with CKEditor).
686
+ // When supportCKEditor is false: Always use EmailWrapper (BEE or HTML editor, never CKEditor).
687
+ if (supportCKEditor) {
687
688
  return (
688
689
  <Email
689
690
  key="cretives-container-email-edit"
@@ -710,13 +710,14 @@ const EmailHTMLEditor = (props) => {
710
710
  // 2. Validate Unsubscribe Tag when feature is OFF (when flag is false we require unsubscribe)
711
711
  // When EMAIL_UNSUBSCRIBE_TAG_MANDATORY is true: do NOT validate for unsubscribe (aligned with FormBuilder).
712
712
  // When EMAIL_UNSUBSCRIBE_TAG_MANDATORY is false: validate and require unsubscribe tag.
713
- if (!isFullMode && !isEmailUnsubscribeTagMandatory() && moduleType === OUTBOUND) {
714
- // Check if content contains unsubscribe tag (either {{unsubscribe}} or {{unsubscribe(#...)})
713
+ // Run for both library and full mode so liquid-enabled orgs also get the error (notification + ValidationErrorDisplay).
714
+ const isModuleTypeOutbound = (moduleType || '').toUpperCase() === OUTBOUND;
715
+ if (!isEmailUnsubscribeTagMandatory() && isModuleTypeOutbound) {
715
716
  const unsubscribeRegex = /{{unsubscribe(\(#[a-zA-Z\d]{6}\))?}}/g; // eslint-disable-line no-useless-escape
716
717
  const hasUnsubscribeTag = unsubscribeRegex.test(htmlContent);
717
718
 
718
719
  if (!hasUnsubscribeTag) {
719
- // Show error notification
720
+ setTagValidationError({ valid: false, missingTags: ['unsubscribe'] });
720
721
  const missingTagsMsg = intl.formatMessage(formBuilderMessages.missingTags);
721
722
  const errorMessage = `${missingTagsMsg} unsubscribe`;
722
723
  setTimeout(() => {
@@ -727,11 +728,9 @@ const EmailHTMLEditor = (props) => {
727
728
  });
728
729
  }, 0);
729
730
 
730
- // Reset parent state so next click is detected as a change
731
731
  if (onValidationFail) {
732
732
  onValidationFail();
733
733
  }
734
- // Block save - unsubscribe tag is required when validation is enabled
735
734
  return;
736
735
  }
737
736
  }
@@ -206,7 +206,11 @@ const useEmailWrapper = ({
206
206
 
207
207
  // Only fetch if we have an ID, don't have template data yet, and not already loading
208
208
  if (hasParamsId && !hasTemplateDetails && !hasTemplateDataProp && !isTemplateLoading && emailActions?.getTemplateDetails) {
209
- const templateId = params?.id || location?.query?.id || location?.params?.id;
209
+ let templateId = params?.id || location?.query?.id || location?.params?.id;
210
+ if (!templateId && location?.pathname?.includes('/edit/')) {
211
+ const [, extractedId] = location.pathname.match(/\/edit\/([^/]+)/) || [];
212
+ if (extractedId) templateId = extractedId;
213
+ }
210
214
  if (templateId) {
211
215
  emailActions.getTemplateDetails(templateId, 'email');
212
216
  }
@@ -581,8 +585,6 @@ const useEmailWrapper = ({
581
585
  // CRITICAL: Only treat as edit mode if we have params.id (actual edit URL) or templateData prop
582
586
  // Don't use templateDetails existence alone, as it might persist from previous template
583
587
  const hasParamsId = params?.id || location?.query?.id || location?.params?.id || location?.pathname?.includes('/edit/');
584
- const hasTemplateDetails = Email?.templateDetails && !isEmpty(Email.templateDetails);
585
- const hasBEETemplate = Email?.BEETemplate && !isEmpty(Email.BEETemplate);
586
588
  const hasTemplateDataProp = templateData && !isEmpty(templateData);
587
589
  // CRITICAL: Consider it edit mode if we have params.id OR templateData prop (library mode)
588
590
  // This allows editor type determination even when template data is still loading
@@ -590,13 +592,14 @@ const useEmailWrapper = ({
590
592
 
591
593
  if (isEditMode) {
592
594
  // Edit mode: Determine editor based on template data
593
- // Check if template was created in BEE editor
594
595
  // Priority: Email.templateDetails > Email.BEETemplate > templateData prop
595
- const editTemplateData = Email?.templateDetails || Email?.BEETemplate || templateData;
596
-
596
+ // Use first non-empty source (empty array/object can appear while template is loading)
597
+ const editTemplateData = [Email?.templateDetails, Email?.BEETemplate, templateData].find(
598
+ (d) => d && !isEmpty(d)
599
+ ) || null;
597
600
  // Helper function to safely get is_drag_drop from various possible paths
598
601
  const getIsDragDrop = (data) => {
599
- if (!data) return false;
602
+ if (!data || isEmpty(data)) return false;
600
603
 
601
604
  // Check common paths for is_drag_drop
602
605
  // Path 1: versions.base.is_drag_drop (most common)
@@ -630,8 +633,10 @@ const useEmailWrapper = ({
630
633
  return false;
631
634
  };
632
635
 
633
- const isDragDrop = getIsDragDrop(editTemplateData);
634
-
636
+ // When template data is still loading (editTemplateData empty), trust editor prop so BEE templates open in BEE
637
+ const hasMeaningfulTemplateData = editTemplateData && !isEmpty(editTemplateData);
638
+ const isDragDrop = getIsDragDrop(editTemplateData)
639
+ || (!hasMeaningfulTemplateData && editor === 'BEE');
635
640
  // Check if BEE is enabled for org (equivalent to checkBeeEditorAllowedForLibrary)
636
641
  // For editor selection:
637
642
  // - In full mode: BEE is always enabled
@@ -643,7 +648,6 @@ const useEmailWrapper = ({
643
648
  || (editor === "BEE" && !isFullMode)
644
649
  || beeEnabledFromAPI
645
650
  || (isAPIResponsePending && isDragDrop && !isFullMode); // Optimistic: if template is BEE and API pending, allow BEE
646
-
647
651
  // If template was created in BEE AND BEE is enabled → open in BEE editor
648
652
  // Otherwise → open in HTML editor (fallback)
649
653
  // IMPORTANT: When supportCKEditor is false, default to HTML editor unless explicitly BEE
@@ -790,8 +794,6 @@ const useEmailWrapper = ({
790
794
  // In edit mode (when supportCKEditor is false), always show editor
791
795
  if (!supportCKEditorFlag && isEditMode) {
792
796
  // Check if it's explicitly BEE editor
793
- const isExplicitlyBEE = emailCreateMode === EMAIL_CREATE_MODES.DRAG_DROP
794
- || (emailProps?.editor === 'BEE' && emailProps?.selectedEditorMode === null);
795
797
  // Show editor for both BEE and HTML in edit mode
796
798
  return true;
797
799
  }
@@ -73,7 +73,7 @@ export const Advertisement = (props) => {
73
73
  const FbAdFooter = styled.div`
74
74
  background-color: ${CAP_WHITE};
75
75
  position: fixed;
76
- bottom: 0;
76
+ bottom: 20px;
77
77
  width: 100%;
78
78
  margin-left: -32px;
79
79
  padding: ${CAP_SPACE_32} ${CAP_SPACE_24};
@@ -40,7 +40,6 @@
40
40
  .create-msg,
41
41
  .cancel-msg {
42
42
  position: fixed;
43
- bottom: 20px;
44
43
  }
45
44
  .cancel-msg {
46
45
  margin-left: 100px;
@@ -38,7 +38,7 @@ export default css`
38
38
  .action-section {
39
39
  background-color: ${CAP_WHITE};
40
40
  position: fixed;
41
- bottom: 0;
41
+ bottom: 20px;
42
42
  width: 100%;
43
43
  margin-left: -32px;
44
44
  padding: ${CAP_SPACE_32} ${CAP_SPACE_24};
@@ -63,6 +63,8 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
63
63
  injectedTags: {},
64
64
  };
65
65
  }
66
+ this.hasFetchedInitialTagsRef = false;
67
+ this.lastFetchedTagContextRef = null;
66
68
  }
67
69
  componentWillMount = () => {
68
70
  if (this.props.route.name === 'view') {
@@ -132,16 +134,19 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
132
134
  }
133
135
  schema.standalone.sections.splice(1, 1);
134
136
  this.injectEvents(schema);
135
- const query = {
136
- layout: 'mobilepush',
137
- type: 'TAG',
138
- context: this.props.location.query.type === 'embedded' ? this.props.location.query.module : 'default',
139
- embedded: this.props.location.query.type === 'embedded' ? this.props.location.query.type : 'full',
140
- };
141
- if (this.props.getDefaultTags) {
142
- query.context = this.props.getDefaultTags;
137
+ if (!this.hasFetchedInitialTagsRef) {
138
+ this.hasFetchedInitialTagsRef = true;
139
+ const context = this.props.getDefaultTags
140
+ || (this.props.location.query.type === 'embedded' ? this.props.location.query.module : 'default');
141
+ this.lastFetchedTagContextRef = typeof context === 'string' ? context.toLowerCase() : context;
142
+ const query = {
143
+ layout: 'mobilepush',
144
+ type: 'TAG',
145
+ context: this.lastFetchedTagContextRef,
146
+ embedded: this.props.location.query.type === 'embedded' ? this.props.location.query.type : 'full',
147
+ };
148
+ this.props.globalActions.fetchSchemaForEntity(query);
143
149
  }
144
- this.props.globalActions.fetchSchemaForEntity(query);
145
150
  }
146
151
  };
147
152
  componentWillUnmount = () => {
@@ -1771,10 +1776,15 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
1771
1776
  this.injectEvents(schema);
1772
1777
  };
1773
1778
  handleOnTagsContextChange = (data) => {
1779
+ const context = (data || '').toLowerCase() === 'all' ? 'default' : (data || '').toLowerCase();
1780
+ if (this.lastFetchedTagContextRef === context) {
1781
+ return;
1782
+ }
1783
+ this.lastFetchedTagContextRef = context;
1774
1784
  const query = {
1775
1785
  layout: 'mobilepush',
1776
1786
  type: 'TAG',
1777
- context: (data || '').toLowerCase() === 'all' ? 'default' : (data || '').toLowerCase(),
1787
+ context,
1778
1788
  embedded: this.props.location.query.type === 'embedded' ? this.props.location.query.type : 'full',
1779
1789
  };
1780
1790
  this.props.globalActions.fetchSchemaForEntity(query);
@@ -68,6 +68,10 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
68
68
  this.getPrimaryCtaFields = getPrimaryCtaFields.bind(this);
69
69
  this.getSecondaryCtaFields = getSecondaryCtaFields.bind(this);
70
70
  this.getLinkTypeFields = getLinkTypeFields.bind(this);
71
+ // Guard: only one initial meta/TAG fetch (getTags can be invoked from multiple code paths)
72
+ this.hasFetchedInitialTagsRef = false;
73
+ // Guard: avoid duplicate fetch when multiple TagList instances trigger same context
74
+ this.lastFetchedTagContextRef = null;
71
75
  }
72
76
  componentWillMount() {
73
77
  this.props.actions.getWeCrmAccounts("mobilepush");
@@ -109,6 +113,10 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
109
113
  }
110
114
  }
111
115
  componentWillReceiveProps(nextProps) {
116
+ if (nextProps.params?.id !== this.props.params?.id) {
117
+ this.hasFetchedInitialTagsRef = false;
118
+ this.lastFetchedTagContextRef = null;
119
+ }
112
120
  if (nextProps.isGetFormData && !this.props.isFullMode) {
113
121
  nextProps.getFormLibraryData(this.getFormData());
114
122
  } else if (nextProps.isGetFormData && this.props.isGetFormData !== nextProps.isGetFormData && this.props.isFullMode && !this.props.Create.createTemplateInProgress) {
@@ -167,7 +175,7 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
167
175
  };
168
176
  this.props.actions.getMobilepushTemplatesList('mobilepush', params);
169
177
  }
170
- if (nextProps.metaEntities && nextProps.metaEntities.layouts && nextProps.metaEntities.layouts.length > 0 && _.isEmpty(this.state.fullSchema) && _.isEmpty(this.state.formData)) {
178
+ if (nextProps.metaEntities && nextProps.metaEntities.layouts && nextProps.metaEntities.layouts.length > 0 && _.isEmpty(this.state.fullSchema)) {
171
179
  this.setState({fullSchema: nextProps.metaEntities.layouts[0].definition, schema: (nextProps.location.query.module === 'loyalty') ? nextProps.metaEntities.layouts[0].definition.textSchema : {}}, () => {
172
180
  this.handleEditSchemaOnPropsChange(nextProps, selectedWeChatAccount);
173
181
  const templateId = get(this, "props.params.id");
@@ -968,15 +976,19 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
968
976
  });
969
977
  };
970
978
  getTags = () => {
979
+ if (this.hasFetchedInitialTagsRef) {
980
+ return;
981
+ }
982
+ this.hasFetchedInitialTagsRef = true;
983
+ const context = this.props.getDefaultTags
984
+ || (this.props.location.query.type === 'embedded' ? this.props.location.query.module : 'default');
985
+ this.lastFetchedTagContextRef = typeof context === 'string' ? context.toLowerCase() : context;
971
986
  const query = {
972
987
  layout: 'mobilepush',
973
988
  type: 'TAG',
974
- context: this.props.location.query.type === 'embedded' ? this.props.location.query.module : 'default',
989
+ context: this.lastFetchedTagContextRef,
975
990
  embedded: this.props.location.query.type === 'embedded' ? this.props.location.query.type : 'full',
976
991
  };
977
- if (this.props.getDefaultTags) {
978
- query.context = this.props.getDefaultTags;
979
- }
980
992
  this.props.globalActions.fetchSchemaForEntity(query);
981
993
  }
982
994
  setModalContent = (type) => {
@@ -1967,10 +1979,15 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
1967
1979
  this.setState({ schema });
1968
1980
  };
1969
1981
  handleOnTagsContextChange = (data) => {
1982
+ const context = (data || '').toLowerCase() === 'all' ? 'default' : (data || '').toLowerCase();
1983
+ if (this.lastFetchedTagContextRef === context) {
1984
+ return;
1985
+ }
1986
+ this.lastFetchedTagContextRef = context;
1970
1987
  const query = {
1971
1988
  layout: 'mobilepush',
1972
1989
  type: 'TAG',
1973
- context: (data || '').toLowerCase() === 'all' ? 'default' : (data || '').toLowerCase(),
1990
+ context,
1974
1991
  embedded: this.props.location.query.type === 'embedded' ? this.props.location.query.type : 'full',
1975
1992
  };
1976
1993
  this.props.globalActions.fetchSchemaForEntity(query);
@@ -2024,7 +2041,6 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
2024
2041
  <CapRow>
2025
2042
  <CapColumn>
2026
2043
  <FormBuilder
2027
- key={!_.isEmpty(schema) ? 'has-schema' : 'no-schema'}
2028
2044
  channel={MOBILE_PUSH}
2029
2045
  schema={schema}
2030
2046
  showLiquidErrorInFooter={this.props.showLiquidErrorInFooter}
@@ -98,5 +98,5 @@
98
98
  }
99
99
 
100
100
  .create-dlt-msg {
101
- margin-left: 170px;
101
+ margin-left: 120px;
102
102
  }
@@ -106,6 +106,7 @@ export const SmsTraiEdit = (props) => {
106
106
  padding: ${CAP_SPACE_32} ${CAP_SPACE_24};
107
107
  position: fixed;
108
108
  bottom: 2rem;
109
+ margin-left: -2rem;
109
110
  .ant-btn {
110
111
  margin-right: ${CAP_SPACE_16};
111
112
  }
@@ -687,24 +688,17 @@ export const SmsTraiEdit = (props) => {
687
688
  <CapButton
688
689
  onClick={handleTestAndPreview}
689
690
  type="secondary"
690
- className="create-msg"
691
+ className="create-msg create-dlt-msg"
691
692
  >
692
693
  <FormattedMessage {...messages.testAndPreviewButtonLabel} />
693
694
  </CapButton>
694
695
  <CapButton
695
696
  onClick={isLiquidSupportFeatureEnabled ? onSubmitWrapper : onDoneCallback}
696
- className="create-msg create-dlt-msg"
697
+ className="create-msg"
697
698
  disabled={isTagValidationError || (isLiquidSupportFeatureEnabled && !isObject(metaEntities?.tagLookupMap))}
698
699
  >
699
700
  <FormattedMessage {...messages.saveButtonLabel} />
700
701
  </CapButton>
701
- <CapButton
702
- onClick={handleClose}
703
- className="cancel-dlt-msg"
704
- type="secondary"
705
- >
706
- <FormattedMessage {...messages.cancelButtonLabel} />
707
- </CapButton>
708
702
  </SMSTraiFooter>
709
703
  <TestAndPreviewSlidebox
710
704
  show={showTestAndPreviewSlidebox}