@capillarytech/creatives-library 8.0.323 → 8.0.324
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 +1 -1
- package/v2Components/CapTagList/index.js +2 -3
- package/v2Components/SmsFallback/index.js +0 -1
- package/v2Containers/Rcs/constants.js +2 -0
- package/v2Containers/Rcs/index.js +58 -20
- package/v2Containers/Sms/Create/index.js +36 -19
- package/v2Containers/Templates/_templates.scss +2 -0
package/package.json
CHANGED
|
@@ -207,11 +207,11 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
|
|
|
207
207
|
} else {
|
|
208
208
|
this.props.onSelect(selectedKeys, info);
|
|
209
209
|
this.setState({visible: false});
|
|
210
|
+
this.setState({expandedKeys: []})
|
|
210
211
|
}
|
|
211
212
|
} else if (info && info.selectedNodes && info.selectedNodes.length > 0 && !info.selectedNodes[0].props.isLeaf) {
|
|
212
213
|
this.handleOnExpand(selectedKeys[0]);
|
|
213
214
|
}
|
|
214
|
-
this.setState({expandedKeys: []})
|
|
215
215
|
}
|
|
216
216
|
};
|
|
217
217
|
|
|
@@ -237,7 +237,7 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
|
|
|
237
237
|
/** Single-line ellipsis within popover width; full label on hover via CapTooltip. */
|
|
238
238
|
wrapTreeTitle = (displayNode, text) => (
|
|
239
239
|
<CapTooltip title={displayNode}>
|
|
240
|
-
|
|
240
|
+
{text || displayNode}
|
|
241
241
|
</CapTooltip>
|
|
242
242
|
);
|
|
243
243
|
|
|
@@ -467,7 +467,6 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
|
|
|
467
467
|
visible={fetchingSchemaError ? false : visible}
|
|
468
468
|
onVisibleChange={this.togglePopoverVisibility}
|
|
469
469
|
content={contentSection}
|
|
470
|
-
overlayClassName="cap-tag-list-popover-overlay"
|
|
471
470
|
trigger="click"
|
|
472
471
|
placement={this.props.popoverPlacement || (channel === EMAIL.toUpperCase() ? "leftTop" : "rightTop")}
|
|
473
472
|
overlayStyle={overlayStyle}
|
|
@@ -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
|
-
//
|
|
249
|
-
|
|
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
|
-
|
|
914
|
-
//
|
|
915
|
-
//
|
|
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
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
1128
|
-
|
|
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
|
|
1244
|
+
const nextVarMap = { ...previousVarMap };
|
|
1220
1245
|
if (globalSlot !== null && globalSlot !== undefined) {
|
|
1221
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
//
|
|
188
|
-
|
|
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 (
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
if (
|
|
926
|
-
fields.
|
|
927
|
-
|
|
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 count — guard 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
|
-
|
|
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
|
|