@capillarytech/creatives-library 8.0.323 → 8.0.324-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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.323",
4
+ "version": "8.0.324-alpha.0",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -150,8 +150,7 @@
150
150
  margin-right: 0.5rem;
151
151
  }
152
152
 
153
- /* CapSlideBox defaults to z-index 100; RCS SMS fallback slidebox + tag popovers use 10010–10020 — must stack above or close control is not clickable. */
154
- .cap-slide-box-v2.common-test-and-preview-slidebox {
153
+ .cap-slide-box-v2.common-test-and-preview-slidebox .cap-slide-box-v2-container {
155
154
  z-index: 10030;
156
155
  }
157
156
 
@@ -789,7 +789,6 @@ export function SmsFallback({
789
789
  ? { rcsSmsFallbackVarMapped: rcsFallbackVarMappedFromBase }
790
790
  : {}),
791
791
  });
792
- slideboxDispatch({ type: 'SET_PENDING_FALLBACK_DATA', payload: null });
793
792
  if (!skipClose) closeSlidebox();
794
793
  },
795
794
  [onChange, closeSlidebox, value, pendingFallbackData]
@@ -93,6 +93,8 @@ export const RCS_MEDIA_TYPES = {
93
93
  export const rcsVarRegex = /\{\{\w+\}\}/g;
94
94
  export const rcsVarTestRegex = /\{\{\w+\}\}/;
95
95
 
96
+ /** Matches all `{{N}}` numeric-index variable tokens in a template string (global). */
97
+ export const RCS_NUMERIC_VAR_TOKEN_REGEX = /\{\{(\d+)\}\}/g;
96
98
  /** `cardVarMapped` slot keys that are numeric only (legacy ordering). */
97
99
  export const RCS_NUMERIC_VAR_NAME_REGEX = /^\d+$/;
98
100
  /** Escape `RegExp` metacharacters when building a pattern from user/tag text. */
@@ -88,6 +88,7 @@ import {
88
88
  MEDIUM,
89
89
  RICHCARD,
90
90
  RCS_NUMERIC_VAR_NAME_REGEX,
91
+ RCS_NUMERIC_VAR_TOKEN_REGEX,
91
92
  RCS_TAG_AREA_FIELD_TITLE,
92
93
  RCS_TAG_AREA_FIELD_DESC,
93
94
  } from './constants';
@@ -245,8 +246,12 @@ export const Rcs = (props) => {
245
246
  const handleSmsFallbackEditorStateChange = useCallback((patch) => {
246
247
  setSmsFallbackData((prev) => {
247
248
  if (!patch || typeof patch !== 'object') return prev;
248
- // Merge even when `prev` is null so tag/slot updates apply on top of `smsFromApiShape` in createPayload.
249
- return { ...(prev || {}), ...patch };
249
+ // Bail out when no template has been selected yet SmsTraiEdit fires
250
+ // onRcsFallbackEditorStateChange on mount (unicodeValidity, varMapped),
251
+ // which would create a non-null smsFallbackData from nothing and cause
252
+ // the card to appear as "Untitled creative" before any save.
253
+ if (!prev) return prev;
254
+ return { ...prev, ...patch };
250
255
  });
251
256
  }, []);
252
257
 
@@ -910,11 +915,17 @@ export const Rcs = (props) => {
910
915
  delete next[variableName];
911
916
  next[data] = nextVal;
912
917
  } else {
913
- next[variableName] = nextVal;
914
- // resolveCardVarMappedSlotValue prefers numeric slot keys ("1","2",…) over semantic names;
915
- // hydration may set "1": ''. Use global slot index suffix is segment index (includes static text), not var ordinal.
918
+ // Use the global slot index key only — writing by semantic name (variableName)
919
+ // would contaminate any other field that shares the same var name (e.g. {{gt}}
920
+ // in both title and description), causing the label value to bleed across fields.
916
921
  if (globalSlotForArea !== null && globalSlotForArea !== undefined) {
917
922
  next[String(globalSlotForArea + 1)] = nextVal;
923
+ // Same reasoning as handleRcsVarChange: delete the semantic key so
924
+ // resolveCardVarMappedSlotValue uses only the numeric slot and the
925
+ // value cannot bleed across fields that share the same var name.
926
+ delete next[variableName];
927
+ } else {
928
+ next[variableName] = nextVal;
918
929
  }
919
930
  }
920
931
  return next;
@@ -1102,15 +1113,31 @@ export const Rcs = (props) => {
1102
1113
  onAddVar(templateDesc);
1103
1114
  };
1104
1115
 
1105
- const onAddVar = (messageContent) => {
1106
- // Always append the next variable at the end, like WhatsApp
1107
- const existingVars = messageContent.match(/\{\{(\d+)\}\}/g) || [];
1108
- const existingNumbers = existingVars.map(v => parseInt(v.match(/\d+/)[0], 10));
1116
+ /**
1117
+ * Returns the smallest positive integer not already used as a `{{N}}` variable
1118
+ * in either the title or description fields, or null if the limit (19) is reached.
1119
+ * Scans both fields so title and description vars never share the same number
1120
+ * (duplicate numbers would share a cardVarMapped key and bleed values across fields).
1121
+ */
1122
+ const getNextRcsNumericVarNumber = (titleStr, descStr) => {
1123
+ const allExistingVars = [
1124
+ ...(titleStr.match(RCS_NUMERIC_VAR_TOKEN_REGEX) || []),
1125
+ ...(descStr.match(RCS_NUMERIC_VAR_TOKEN_REGEX) || []),
1126
+ ];
1127
+ const existingNumbers = allExistingVars.flatMap(v => {
1128
+ const m = v.match(/\d+/);
1129
+ return m ? [parseInt(m[0], 10)] : [];
1130
+ });
1109
1131
  let nextNumber = 1;
1110
1132
  while (existingNumbers.includes(nextNumber)) {
1111
1133
  nextNumber++;
1112
1134
  }
1113
- if (nextNumber > 19) {
1135
+ return nextNumber > 19 ? null : nextNumber;
1136
+ };
1137
+
1138
+ const onAddVar = (messageContent) => {
1139
+ const nextNumber = getNextRcsNumericVarNumber(templateTitle, messageContent);
1140
+ if (nextNumber === null) {
1114
1141
  return;
1115
1142
  }
1116
1143
  const nextVar = `{{${nextNumber}}}`;
@@ -1122,15 +1149,13 @@ const onAddVar = (messageContent) => {
1122
1149
  };
1123
1150
 
1124
1151
  const onTitleAddVar = () => {
1125
- // Always append the next variable at the end, like WhatsApp
1152
+ // Scan both title AND description so the new title var number doesn't
1153
+ // duplicate a number already used in the description. Duplicate numeric
1154
+ // names would share the same cardVarMapped semantic key, causing the
1155
+ // description slot to reflect the title slot value and vice-versa.
1126
1156
  const messageContent = templateTitle;
1127
- const existingVars = messageContent.match(/\{\{(\d+)\}\}/g) || [];
1128
- const existingNumbers = existingVars.map(v => parseInt(v.match(/\d+/)[0], 10));
1129
- let nextNumber = 1;
1130
- while (existingNumbers.includes(nextNumber)) {
1131
- nextNumber++;
1132
- }
1133
- if (nextNumber > 19) {
1157
+ const nextNumber = getNextRcsNumericVarNumber(templateTitle, templateDesc);
1158
+ if (nextNumber === null) {
1134
1159
  return;
1135
1160
  }
1136
1161
  const nextVar = `{{${nextNumber}}}`;
@@ -1216,9 +1241,22 @@ const onTitleAddVar = () => {
1216
1241
  const fieldStr = type === TITLE_TEXT ? templateTitle : templateDesc;
1217
1242
  const globalSlot = getGlobalSlotIndexForRcsFieldId(id, fieldStr, type);
1218
1243
  setCardVarMapped((previousVarMap) => {
1219
- const nextVarMap = { ...previousVarMap, [variableName]: coercedSlotValue };
1244
+ const nextVarMap = { ...previousVarMap };
1220
1245
  if (globalSlot !== null && globalSlot !== undefined) {
1221
- nextVarMap[String(globalSlot + 1)] = coercedSlotValue;
1246
+ // Write by global slot index only — title and description can share the
1247
+ // same var name (e.g. {{gt}}), and writing by semantic name would cause
1248
+ // the description slot to resolve the title's value and vice-versa.
1249
+ const numericKey = String(globalSlot + 1);
1250
+ nextVarMap[numericKey] = coercedSlotValue;
1251
+ // Remove any stale semantic key so resolveCardVarMappedSlotValue never
1252
+ // falls back to it. Guard: when variableName is already the numeric slot
1253
+ // key (e.g. {{1}} at slot 0 → both equal "1"), skip the delete or it
1254
+ // would erase the value we just wrote.
1255
+ if (variableName !== numericKey) {
1256
+ delete nextVarMap[variableName];
1257
+ }
1258
+ } else {
1259
+ nextVarMap[variableName] = coercedSlotValue;
1222
1260
  }
1223
1261
  return nextVarMap;
1224
1262
  });
@@ -57,6 +57,11 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
57
57
  isTestAndPreviewMode: false,
58
58
  pendingGetFormData: false,
59
59
  };
60
+ // Tracks the last validity value reported to SmsFallback so componentDidUpdate
61
+ // does not dispatch on every render and create an infinite update loop.
62
+ // Intentionally undefined (not true) so the first render always reports the
63
+ // real validity rather than assuming the form starts invalid.
64
+ this._lastReportedSmsFooterInvalid = undefined;
60
65
  this.saveFormData = this.saveFormData.bind(this);
61
66
  this.onFormDataChange = this.onFormDataChange.bind(this);
62
67
  this.onTemplateNameChange = this.onTemplateNameChange.bind(this);
@@ -182,10 +187,16 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
182
187
  if (!this.props.embeddedSmsFallback || typeof this.props.onEmbeddedSmsFooterValidity !== 'function') {
183
188
  return;
184
189
  }
185
- // Report validity on every update. The reducer in SmsFallback bails out (returns same
186
- // state reference) when the value is unchanged, so no re-render loop is triggered.
187
- // Calling unconditionally handles both mutation-based and reference-based FormBuilder updates.
188
- this.props.onEmbeddedSmsFooterValidity(getSmsEmbeddedFooterValidity(this.state.formData, this.state.tabCount));
190
+ const validity = getSmsEmbeddedFooterValidity(this.state.formData, this.state.tabCount);
191
+ const isInvalid = !!validity.isTemplateNameEmpty || !!validity.isMessageEmpty;
192
+ // Only dispatch when the validity value changes. Dispatching unconditionally caused
193
+ // an infinite loop: componentDidUpdate → dispatch → SmsFallback re-render →
194
+ // SmsCreate re-render → componentDidUpdate → ... even when state was unchanged.
195
+ // The instance variable handles both reference-based and mutation-based FormBuilder
196
+ // updates: validity is recomputed from current formData content, not by reference.
197
+ if (this._lastReportedSmsFooterInvalid === isInvalid) return;
198
+ this._lastReportedSmsFooterInvalid = isInvalid;
199
+ this.props.onEmbeddedSmsFooterValidity(validity);
189
200
  }
190
201
 
191
202
  componentWillUnmount() {
@@ -911,23 +922,29 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
911
922
  removeStandAlone() {
912
923
  const schema = _.cloneDeep(this.state.schema);
913
924
  const childSections = _.get(schema, 'standalone.sections[0].childSections');
914
- if (!childSections || childSections.length <= 2) {
915
- return;
916
- }
917
- /* In-form Save / Test row removed for embedded SMS; slidebox footer (SlideBoxFooter) provides actions — see CreativesContainer. */
918
- childSections.splice(2, 1);
919
- /*
920
- * Creatives library also drops the standalone template-name block because `SlideBoxHeader` shows the name.
921
- * RCS SMS fallback uses the same slidebox footer but no template-name header keep Creative name in the form.
922
- */
923
- if (!this.props.embeddedSmsFallback) {
924
- const fields = _.get(childSections, '[1].childSections[0].childSections');
925
- if (fields && fields.length > 0) {
926
- fields.splice(0, 1);
927
- _.set(childSections, '[1].childSections[0].childSections', fields);
925
+ if (childSections) {
926
+ /* In-form Save / Test row (index 2) only exists when the schema has > 2 sections. */
927
+ if (childSections.length > 2) {
928
+ childSections.splice(2, 1);
929
+ }
930
+ /*
931
+ * Creatives library drops the standalone template-name block because `SlideBoxHeader`
932
+ * shows the name. This is independent of the section countguard it separately so
933
+ * it still runs even when childSections.length <= 2 (e.g. schema arrives pre-trimmed).
934
+ * RCS SMS fallback uses the same slidebox footer but keeps the name in the form.
935
+ */
936
+ if (!this.props.embeddedSmsFallback) {
937
+ const fields = _.get(childSections, '[1].childSections[0].childSections');
938
+ if (fields && fields.length > 0) {
939
+ fields.splice(0, 1);
940
+ _.set(childSections, '[1].childSections[0].childSections', fields);
941
+ }
928
942
  }
943
+ _.set(schema, 'standalone.sections[0].childSections', childSections);
929
944
  }
930
- _.set(schema, 'standalone.sections[0].childSections', childSections);
945
+ // Always increment loadingStatus — isSmsLoading() requires >= 2 in library mode
946
+ // (isFullMode=false). The early return previously skipped this, leaving the
947
+ // spinner stuck forever when the schema had <= 2 childSections.
931
948
  this.setState({ schema, loadingStatus: this.state.loadingStatus + 1 });
932
949
  }
933
950
 
@@ -673,6 +673,8 @@
673
673
 
674
674
  &__toolbar-row .search-text {
675
675
  width: 13.125rem;
676
+ min-width: 13.125rem;
677
+ flex: 1;
676
678
  }
677
679
 
678
680
  &__create-row {