@capillarytech/creatives-library 8.0.318 → 8.0.320
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/constants/unified.js +15 -0
- package/package.json +1 -1
- package/services/api.js +6 -0
- package/services/tests/api.test.js +7 -0
- package/utils/common.js +6 -1
- package/utils/templateVarUtils.js +172 -0
- package/utils/tests/templateVarUtils.test.js +160 -0
- package/v2Components/CapTagList/index.js +10 -0
- package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -49
- package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
- package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +207 -21
- package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
- package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
- package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
- package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
- package/v2Components/CommonTestAndPreview/SendTestMessage.js +11 -5
- package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +20 -1
- package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +133 -4
- package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +12 -0
- package/v2Components/CommonTestAndPreview/constants.js +38 -0
- package/v2Components/CommonTestAndPreview/index.js +693 -155
- package/v2Components/CommonTestAndPreview/messages.js +41 -3
- package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
- package/v2Components/CommonTestAndPreview/sagas.js +15 -6
- package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +352 -0
- package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +269 -1
- package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
- package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
- package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +25 -4
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
- package/v2Components/CommonTestAndPreview/tests/index.test.js +132 -4
- package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
- package/v2Components/CommonTestAndPreview/tests/sagas.test.js +2 -2
- package/v2Components/FormBuilder/index.js +7 -1
- package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +87 -0
- package/v2Components/SmsFallback/constants.js +73 -0
- package/v2Components/SmsFallback/index.js +956 -0
- package/v2Components/SmsFallback/index.scss +265 -0
- package/v2Components/SmsFallback/messages.js +78 -0
- package/v2Components/SmsFallback/smsFallbackUtils.js +107 -0
- package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
- package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
- package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
- package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +197 -0
- package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +261 -0
- package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
- package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
- package/v2Components/TestAndPreviewSlidebox/index.js +8 -1
- package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
- package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
- package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
- package/v2Components/VarSegmentMessageEditor/index.js +125 -0
- package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
- package/v2Containers/CommunicationFlow/CommunicationFlow.js +291 -0
- package/v2Containers/CommunicationFlow/CommunicationFlow.scss +25 -0
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +255 -0
- package/v2Containers/CommunicationFlow/constants.js +200 -0
- package/v2Containers/CommunicationFlow/index.js +102 -0
- package/v2Containers/CommunicationFlow/messages.js +346 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +522 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +170 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +796 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/index.js +5 -0
- package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/CommunicationStrategyStep.js +95 -0
- package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/Tests/CommunicationStrategyStep.test.js +133 -0
- package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/index.js +5 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +289 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.scss +70 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +319 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.scss +69 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +616 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +577 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +1111 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +696 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/index.js +7 -0
- package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.js +102 -0
- package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.scss +36 -0
- package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/Tests/DynamicControlsStep.test.js +91 -0
- package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/index.js +5 -0
- package/v2Containers/CommunicationFlow/steps/MessageTypeStep/MessageTypeStep.js +86 -0
- package/v2Containers/CommunicationFlow/steps/MessageTypeStep/Tests/MessageTypeStep.test.js +100 -0
- package/v2Containers/CommunicationFlow/steps/MessageTypeStep/index.js +5 -0
- package/v2Containers/CommunicationFlow/utils/getEnabledSteps.js +30 -0
- package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
- package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
- package/v2Containers/CreativesContainer/SlideBoxFooter.js +10 -1
- package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
- package/v2Containers/CreativesContainer/constants.js +12 -0
- package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +67 -0
- package/v2Containers/CreativesContainer/index.js +289 -99
- package/v2Containers/CreativesContainer/index.scss +51 -1
- package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
- package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +104 -0
- package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +110 -0
- package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
- package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +363 -0
- package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -10
- package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
- package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
- package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
- package/v2Containers/Rcs/constants.js +32 -1
- package/v2Containers/Rcs/index.js +950 -873
- package/v2Containers/Rcs/index.scss +85 -6
- package/v2Containers/Rcs/messages.js +10 -1
- package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +205 -0
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +40834 -1963
- package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
- package/v2Containers/Rcs/tests/index.test.js +41 -38
- package/v2Containers/Rcs/tests/mockData.js +38 -0
- package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +251 -0
- package/v2Containers/Rcs/tests/utils.test.js +379 -1
- package/v2Containers/Rcs/utils.js +358 -10
- package/v2Containers/Sms/Create/index.js +81 -36
- package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
- package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
- package/v2Containers/SmsTrai/Create/index.js +9 -4
- package/v2Containers/SmsTrai/Edit/constants.js +2 -0
- package/v2Containers/SmsTrai/Edit/index.js +609 -128
- package/v2Containers/SmsTrai/Edit/index.scss +121 -0
- package/v2Containers/SmsTrai/Edit/messages.js +9 -4
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4327 -2374
- package/v2Containers/SmsWrapper/index.js +37 -8
- package/v2Containers/TagList/index.js +6 -0
- package/v2Containers/Templates/TemplatesActionBar.js +101 -0
- package/v2Containers/Templates/_templates.scss +61 -2
- package/v2Containers/Templates/actions.js +11 -0
- package/v2Containers/Templates/constants.js +2 -0
- package/v2Containers/Templates/index.js +90 -40
- package/v2Containers/Templates/sagas.js +57 -12
- package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
- package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1043 -1079
- package/v2Containers/Templates/tests/sagas.test.js +193 -12
- package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
- package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
- package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
- package/v2Containers/TemplatesV2/index.js +86 -23
- package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
- package/v2Containers/Whatsapp/index.js +3 -20
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import {CapIcon, CapImage, CapLabel, CapDivider } from '@capillarytech/cap-ui-library';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
RCS,
|
|
5
|
+
RCS_MEDIA_TYPES,
|
|
6
|
+
RCS_NUMERIC_VAR_NAME_REGEX,
|
|
7
|
+
RCS_REGEX_META_CHARS_PATTERN,
|
|
8
|
+
RCS_STRIP_MUSTACHE_DELIMITERS_REGEX,
|
|
9
|
+
} from './constants';
|
|
4
10
|
import './index.scss';
|
|
5
11
|
// import { formatMessage } from '../../../utils/intl';
|
|
6
12
|
import messages from './messages';
|
|
7
|
-
import {
|
|
8
|
-
|
|
13
|
+
import {
|
|
14
|
+
STATUS_OPTIONS,
|
|
15
|
+
RCS_BUTTON_TYPES,
|
|
16
|
+
RCS_STATUSES,
|
|
17
|
+
rcsVarRegex,
|
|
18
|
+
rcsVarTestRegex,
|
|
19
|
+
} from './constants';
|
|
20
|
+
import {
|
|
21
|
+
splitTemplateVarString,
|
|
22
|
+
COMBINED_SMS_TEMPLATE_VAR_REGEX,
|
|
23
|
+
isAnyTemplateVarToken,
|
|
24
|
+
} from '../../utils/templateVarUtils';
|
|
9
25
|
|
|
10
26
|
export const getRcsStatusType = (status) => {
|
|
11
27
|
switch (status) {
|
|
@@ -33,6 +49,334 @@ export const getTemplateStatusType = (templateStatus) => {
|
|
|
33
49
|
}
|
|
34
50
|
};
|
|
35
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Global RegExp matching `{{numericVarName}}` in RCS template strings.
|
|
54
|
+
* `numericVarName` is escaped for regex metacharacters.
|
|
55
|
+
*/
|
|
56
|
+
export function buildRcsNumericMustachePlaceholderRegex(numericVarName) {
|
|
57
|
+
const escaped = String(numericVarName).replace(RCS_REGEX_META_CHARS_PATTERN, '\\$&');
|
|
58
|
+
return new RegExp(`\\{\\{${escaped}\\}\\}`, 'g');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function normalizeCardVarMapped(rawCardVarMapped, orderedTagNames) {
|
|
62
|
+
if (!rawCardVarMapped || typeof rawCardVarMapped !== 'object') return {};
|
|
63
|
+
const normalizedMap = {};
|
|
64
|
+
const templateVarNamesInOrder = Array.isArray(orderedTagNames) ? orderedTagNames : null;
|
|
65
|
+
const hasOrderedSlots =
|
|
66
|
+
Boolean(templateVarNamesInOrder?.length);
|
|
67
|
+
Object.entries(rawCardVarMapped).forEach(([entryKey, entryValue]) => {
|
|
68
|
+
const trimmedValue = entryValue == null ? '' : String(entryValue).trim();
|
|
69
|
+
const entryKeyIsNumericSlot = RCS_NUMERIC_VAR_NAME_REGEX.test(String(entryKey));
|
|
70
|
+
const mustacheInnerMatch = trimmedValue.match(/^\{\{([^}]+)\}\}$/);
|
|
71
|
+
const innerFromMustache =
|
|
72
|
+
mustacheInnerMatch?.[1] != null ? String(mustacheInnerMatch[1]).trim() : null;
|
|
73
|
+
|
|
74
|
+
if (innerFromMustache !== null && entryKeyIsNumericSlot) {
|
|
75
|
+
const slotIndexZeroBased = parseInt(String(entryKey), 10) - 1;
|
|
76
|
+
const expectedVarForSlot =
|
|
77
|
+
hasOrderedSlots
|
|
78
|
+
&& slotIndexZeroBased >= 0
|
|
79
|
+
&& slotIndexZeroBased < templateVarNamesInOrder.length
|
|
80
|
+
? templateVarNamesInOrder[slotIndexZeroBased]
|
|
81
|
+
: null;
|
|
82
|
+
const innerMatchesSlotToken =
|
|
83
|
+
expectedVarForSlot != null && innerFromMustache === expectedVarForSlot;
|
|
84
|
+
const legacyUnorderedPlaceholder = !hasOrderedSlots;
|
|
85
|
+
/* Library: slot "1" + {{user_id_b64}} when token is user_id_b64 → empty semantic. With ordered
|
|
86
|
+
* slots, only clear when inner matches that slot's template token; else keep (e.g. {{1}}+{{FirstName}}). */
|
|
87
|
+
const clearNumericSlotMustacheAsUnfilled =
|
|
88
|
+
!RCS_NUMERIC_VAR_NAME_REGEX.test(innerFromMustache)
|
|
89
|
+
&& (legacyUnorderedPlaceholder || innerMatchesSlotToken);
|
|
90
|
+
if (clearNumericSlotMustacheAsUnfilled) {
|
|
91
|
+
const outputKey = innerFromMustache;
|
|
92
|
+
const existingValue = normalizedMap[outputKey];
|
|
93
|
+
if (existingValue != null && String(existingValue).trim() !== '') {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
normalizedMap[outputKey] = '';
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (RCS_NUMERIC_VAR_NAME_REGEX.test(innerFromMustache)) {
|
|
100
|
+
const outputKey = innerFromMustache;
|
|
101
|
+
const existingValue = normalizedMap[outputKey];
|
|
102
|
+
if (existingValue != null && String(existingValue).trim() !== '') {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
normalizedMap[outputKey] = '';
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (innerFromMustache !== null && !entryKeyIsNumericSlot) {
|
|
111
|
+
if (innerFromMustache === String(entryKey)) {
|
|
112
|
+
const existingValue = normalizedMap[entryKey];
|
|
113
|
+
if (existingValue != null && String(existingValue).trim() !== '') {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
normalizedMap[entryKey] = '';
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (entryKeyIsNumericSlot && templateVarNamesInOrder?.length) {
|
|
122
|
+
const slotIndexZeroBased = parseInt(String(entryKey), 10) - 1;
|
|
123
|
+
if (slotIndexZeroBased >= 0 && slotIndexZeroBased < templateVarNamesInOrder.length) {
|
|
124
|
+
normalizedMap[templateVarNamesInOrder[slotIndexZeroBased]] = trimmedValue;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
normalizedMap[entryKey] = trimmedValue;
|
|
129
|
+
});
|
|
130
|
+
return normalizedMap;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Rebuild `cardVarMapped` so keys match the current title/description tokens (title tokens first,
|
|
135
|
+
* then description), in order. Values are taken from the matching key, else from legacy slot
|
|
136
|
+
* `1`, `2`, … by index. If there are no `{{...}}` tokens, returns a shallow clone of `raw`.
|
|
137
|
+
*/
|
|
138
|
+
export function coalesceCardVarMappedToTemplate(
|
|
139
|
+
sourceCardVarMap,
|
|
140
|
+
templateTitle,
|
|
141
|
+
templateDesc,
|
|
142
|
+
rcsVarRegex,
|
|
143
|
+
) {
|
|
144
|
+
const getVarNameFromToken = (token = '') => token.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '');
|
|
145
|
+
const templateVarTokens = [
|
|
146
|
+
...(templateTitle?.match(rcsVarRegex) ?? []),
|
|
147
|
+
...(templateDesc?.match(rcsVarRegex) ?? []),
|
|
148
|
+
];
|
|
149
|
+
const lookupSourceMap =
|
|
150
|
+
sourceCardVarMap != null && typeof sourceCardVarMap === 'object' ? sourceCardVarMap : {};
|
|
151
|
+
if (!templateVarTokens.length) {
|
|
152
|
+
return { ...lookupSourceMap };
|
|
153
|
+
}
|
|
154
|
+
const coalescedMap = { ...lookupSourceMap };
|
|
155
|
+
const seenSemanticVarNames = new Set();
|
|
156
|
+
templateVarTokens.forEach((token, slotIndexZeroBased) => {
|
|
157
|
+
const semanticVarName = getVarNameFromToken(token);
|
|
158
|
+
if (!semanticVarName) return;
|
|
159
|
+
const numericSlotKey = String(slotIndexZeroBased + 1);
|
|
160
|
+
let valueFromSource = lookupSourceMap[numericSlotKey];
|
|
161
|
+
if (valueFromSource === undefined || valueFromSource === null) {
|
|
162
|
+
valueFromSource = lookupSourceMap[semanticVarName];
|
|
163
|
+
}
|
|
164
|
+
if (valueFromSource === undefined || valueFromSource === null) {
|
|
165
|
+
valueFromSource = lookupSourceMap[String(slotIndexZeroBased + 1)];
|
|
166
|
+
}
|
|
167
|
+
if (valueFromSource === undefined || valueFromSource === null) {
|
|
168
|
+
valueFromSource = lookupSourceMap[slotIndexZeroBased + 1];
|
|
169
|
+
}
|
|
170
|
+
const trimmedSlotValue = valueFromSource == null ? '' : String(valueFromSource).trim();
|
|
171
|
+
coalescedMap[numericSlotKey] = trimmedSlotValue;
|
|
172
|
+
if (!seenSemanticVarNames.has(semanticVarName)) {
|
|
173
|
+
seenSemanticVarNames.add(semanticVarName);
|
|
174
|
+
coalescedMap[semanticVarName] = trimmedSlotValue;
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
return coalescedMap;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Resolve the personalization value for a variable slot — aligned with createPayload:
|
|
182
|
+
* per-slot numeric keys `1`, `2`, … win over legacy semantic keys when both exist (duplicate
|
|
183
|
+
* `{{name}}` in title+desc). If semantic is explicitly cleared to '', that still wins over a
|
|
184
|
+
* stale numeric value (see tests) — except in embedded library / journey mode (`isLibraryMode`).
|
|
185
|
+
*
|
|
186
|
+
* In library mode, campaign payloads often set semantic keys to '' while numeric slot `1`,`2`,…
|
|
187
|
+
* still holds the value selected in the library; prefer that so VarSegment prepopulates.
|
|
188
|
+
*
|
|
189
|
+
* When a numeric slot is present but only whitespace / empty (common after hydration), do not
|
|
190
|
+
* treat it as authoritative — fall through to the semantic key so preview and payload match the
|
|
191
|
+
* tag the user selected (e.g. `1: ''` but `promotion_points: '{{newTag}}'`).
|
|
192
|
+
*/
|
|
193
|
+
export function resolveCardVarMappedSlotValue(
|
|
194
|
+
cardVarMapped,
|
|
195
|
+
varName,
|
|
196
|
+
globalSlotIndexZeroBased,
|
|
197
|
+
isLibraryMode = false,
|
|
198
|
+
) {
|
|
199
|
+
const varMap = cardVarMapped ?? {};
|
|
200
|
+
const slotKey = String(globalSlotIndexZeroBased + 1);
|
|
201
|
+
const semanticEmpty =
|
|
202
|
+
Object.prototype.hasOwnProperty.call(varMap, varName)
|
|
203
|
+
&& String(varMap[varName] ?? '') === '';
|
|
204
|
+
const slotNonEmpty =
|
|
205
|
+
Object.prototype.hasOwnProperty.call(varMap, slotKey)
|
|
206
|
+
&& String(varMap[slotKey] ?? '').trim() !== '';
|
|
207
|
+
|
|
208
|
+
if (semanticEmpty && !(isLibraryMode && slotNonEmpty)) {
|
|
209
|
+
return '';
|
|
210
|
+
}
|
|
211
|
+
let numericSlotValue = '';
|
|
212
|
+
if (Object.prototype.hasOwnProperty.call(varMap, slotKey)) {
|
|
213
|
+
numericSlotValue = String(varMap[slotKey] ?? '');
|
|
214
|
+
} else if (Object.prototype.hasOwnProperty.call(varMap, globalSlotIndexZeroBased + 1)) {
|
|
215
|
+
numericSlotValue = String(varMap[globalSlotIndexZeroBased + 1] ?? '');
|
|
216
|
+
}
|
|
217
|
+
if (numericSlotValue.trim() !== '') {
|
|
218
|
+
return numericSlotValue;
|
|
219
|
+
}
|
|
220
|
+
if (Object.prototype.hasOwnProperty.call(varMap, varName)) {
|
|
221
|
+
return String(varMap[varName] ?? '');
|
|
222
|
+
}
|
|
223
|
+
return '';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Text-only RCS card: editor shows a single “Text message” field (description); title row is hidden. */
|
|
227
|
+
export function isRcsTextOnlyCardMediaType(mediaType) {
|
|
228
|
+
return (
|
|
229
|
+
mediaType === RCS_MEDIA_TYPES.NONE
|
|
230
|
+
|| String(mediaType || '').toUpperCase() === 'TEXT'
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Resolve RCS card title/description for TemplatePreview (e.g. campaign slidebox preview).
|
|
236
|
+
* Mirrors `resolveTemplateWithMap` in the Rcs editor: title vars use global slots 0..n-1, then description.
|
|
237
|
+
* For text-only cards (`textOnlyCard`), ignore persisted `title` and resolve description from slot 0 — matches
|
|
238
|
+
* the editor where only the message body is shown.
|
|
239
|
+
*/
|
|
240
|
+
export function resolveRcsCardPreviewStrings(
|
|
241
|
+
title,
|
|
242
|
+
description,
|
|
243
|
+
cardVarMapped,
|
|
244
|
+
isLibraryMode = false,
|
|
245
|
+
textOnlyCard = false,
|
|
246
|
+
) {
|
|
247
|
+
const splitTemplateVarStringRcs = (str) => splitTemplateVarString(str, rcsVarRegex);
|
|
248
|
+
const getVarNameFromToken = (token = '') =>
|
|
249
|
+
token.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '');
|
|
250
|
+
|
|
251
|
+
const resolveTemplateWithMap = (str = '', slotOffset = 0) => {
|
|
252
|
+
if (!str) return '';
|
|
253
|
+
const arr = splitTemplateVarStringRcs(str);
|
|
254
|
+
let varOrdinal = 0;
|
|
255
|
+
return arr
|
|
256
|
+
.map((elem) => {
|
|
257
|
+
if (rcsVarTestRegex.test(elem)) {
|
|
258
|
+
const key = getVarNameFromToken(elem);
|
|
259
|
+
const globalSlot = slotOffset + varOrdinal;
|
|
260
|
+
varOrdinal += 1;
|
|
261
|
+
const v = resolveCardVarMappedSlotValue(
|
|
262
|
+
cardVarMapped,
|
|
263
|
+
key,
|
|
264
|
+
globalSlot,
|
|
265
|
+
isLibraryMode,
|
|
266
|
+
);
|
|
267
|
+
if (v == null || String(v).trim() === '') return elem;
|
|
268
|
+
return String(v);
|
|
269
|
+
}
|
|
270
|
+
return elem;
|
|
271
|
+
})
|
|
272
|
+
.join('');
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const effectiveTitle = textOnlyCard ? '' : String(title || '');
|
|
276
|
+
const titleVarCount = textOnlyCard
|
|
277
|
+
? 0
|
|
278
|
+
: (effectiveTitle.match(rcsVarRegex) || []).length;
|
|
279
|
+
return {
|
|
280
|
+
rcsTitle: textOnlyCard ? '' : resolveTemplateWithMap(effectiveTitle, 0),
|
|
281
|
+
rcsDesc: resolveTemplateWithMap(String(description || ''), titleVarCount),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Campaign consumer payload: replace each card's `title` / `description` with VarSegment-resolved
|
|
287
|
+
* tag strings (same rules as {@link resolveRcsCardPreviewStrings}). Root `rcsCardVarMapped` merges
|
|
288
|
+
* with per-card `cardVarMapped`; `cardVarMapped` on each card is left unchanged for round-trip.
|
|
289
|
+
*/
|
|
290
|
+
export function mapRcsCardContentForConsumerWithResolvedTags(
|
|
291
|
+
cardContentArray,
|
|
292
|
+
rootRcsCardVarMapped,
|
|
293
|
+
isFullMode,
|
|
294
|
+
) {
|
|
295
|
+
const rootRecord =
|
|
296
|
+
rootRcsCardVarMapped != null && typeof rootRcsCardVarMapped === 'object'
|
|
297
|
+
? rootRcsCardVarMapped
|
|
298
|
+
: {};
|
|
299
|
+
const list = Array.isArray(cardContentArray) ? cardContentArray : [];
|
|
300
|
+
const isLibraryMode = isFullMode !== true;
|
|
301
|
+
return list.map((card) => {
|
|
302
|
+
if (!card || typeof card !== 'object') return card;
|
|
303
|
+
const nested =
|
|
304
|
+
card.cardVarMapped != null && typeof card.cardVarMapped === 'object'
|
|
305
|
+
? card.cardVarMapped
|
|
306
|
+
: {};
|
|
307
|
+
const merged = { ...rootRecord, ...nested };
|
|
308
|
+
const textOnly = isRcsTextOnlyCardMediaType(card.mediaType);
|
|
309
|
+
const { rcsTitle, rcsDesc } = resolveRcsCardPreviewStrings(
|
|
310
|
+
card.title ?? '',
|
|
311
|
+
card.description ?? '',
|
|
312
|
+
merged,
|
|
313
|
+
isLibraryMode,
|
|
314
|
+
textOnly,
|
|
315
|
+
);
|
|
316
|
+
return {
|
|
317
|
+
...card,
|
|
318
|
+
title: rcsTitle,
|
|
319
|
+
description: rcsDesc,
|
|
320
|
+
};
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Before save: strip only legacy numeric self-placeholders (`{{1}}`, `{{2}}`, …) mistakenly stored as
|
|
326
|
+
* slot values. Preserve semantic tokens like `{{FirstName}}` from TagList — those are valid mappings.
|
|
327
|
+
*/
|
|
328
|
+
export function sanitizeCardVarMappedValue(val) {
|
|
329
|
+
if (val == null) return '';
|
|
330
|
+
const trimmedDisplayString = String(val).trim();
|
|
331
|
+
if (/^\{\{\d+\}\}$/.test(trimmedDisplayString)) return '';
|
|
332
|
+
return String(val);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Same completion rule as SmsTraiEdit RCS fallback — used by `isDisableDone` from
|
|
337
|
+
* `smsFallbackData.rcsSmsFallbackVarMapped` + template string.
|
|
338
|
+
* Every variable token (DLT {#…#} or mustache {{…}}) must have a non-empty trimmed value in the map.
|
|
339
|
+
*
|
|
340
|
+
* Slot keys are usually `${token}_${segmentIndex}` (same as VarSegmentMessageEditor). Persisted / API
|
|
341
|
+
* payloads may use `${token}_${varOrdinal}` with a 1-based occurrence index (see SmsTraiEdit init).
|
|
342
|
+
* We try segment index first, then ordinal — so e.g. template `{#var#}` (segment index 0) still matches
|
|
343
|
+
* map `{#var#}_1`.
|
|
344
|
+
*
|
|
345
|
+
* @param {string} templateText
|
|
346
|
+
* @param {Record<string, string>} [varSlotValueMap={}]
|
|
347
|
+
* @returns {boolean}
|
|
348
|
+
*/
|
|
349
|
+
export function areAllRcsSmsFallbackVarSlotsFilled(templateText, varSlotValueMap = {}) {
|
|
350
|
+
if (!templateText || typeof templateText !== 'string') return true;
|
|
351
|
+
const segments = splitTemplateVarString(templateText, COMBINED_SMS_TEMPLATE_VAR_REGEX);
|
|
352
|
+
const hasVarToken = segments.some(
|
|
353
|
+
(segment) =>
|
|
354
|
+
typeof segment === 'string'
|
|
355
|
+
&& isAnyTemplateVarToken(segment),
|
|
356
|
+
);
|
|
357
|
+
if (!hasVarToken) return true;
|
|
358
|
+
let varOrdinal = 0;
|
|
359
|
+
return segments.every((segment, segmentIndex) => {
|
|
360
|
+
if (
|
|
361
|
+
typeof segment !== 'string'
|
|
362
|
+
|| !isAnyTemplateVarToken(segment)
|
|
363
|
+
) return true;
|
|
364
|
+
varOrdinal += 1;
|
|
365
|
+
const indexKey = `${segment}_${segmentIndex}`;
|
|
366
|
+
const ordinalKey = `${segment}_${varOrdinal}`;
|
|
367
|
+
let mappedSlotValue;
|
|
368
|
+
if (Object.prototype.hasOwnProperty.call(varSlotValueMap, indexKey)) {
|
|
369
|
+
mappedSlotValue = varSlotValueMap[indexKey];
|
|
370
|
+
} else if (Object.prototype.hasOwnProperty.call(varSlotValueMap, ordinalKey)) {
|
|
371
|
+
mappedSlotValue = varSlotValueMap[ordinalKey];
|
|
372
|
+
} else {
|
|
373
|
+
mappedSlotValue = undefined;
|
|
374
|
+
}
|
|
375
|
+
if (mappedSlotValue == null) return false;
|
|
376
|
+
return String(mappedSlotValue).trim() !== '';
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
36
380
|
export const getRCSContent = (template) => {
|
|
37
381
|
const renderRcsSuggestionsPreview = (rcsSuggestions) => {
|
|
38
382
|
const renderArray = [];
|
|
@@ -84,8 +428,10 @@ export const getRCSContent = (template) => {
|
|
|
84
428
|
media = {},
|
|
85
429
|
description,
|
|
86
430
|
title,
|
|
431
|
+
mediaType,
|
|
87
432
|
suggestions = [],
|
|
88
433
|
} = cardContent[0];
|
|
434
|
+
const isTextOnlyCard = isRcsTextOnlyCardMediaType(mediaType);
|
|
89
435
|
const mediaPreview = media?.thumbnailUrl ? media.thumbnailUrl : media.mediaUrl;
|
|
90
436
|
return (
|
|
91
437
|
<div className="cap-rcs-creatives">
|
|
@@ -95,13 +441,15 @@ export const getRCSContent = (template) => {
|
|
|
95
441
|
className="rcs-listing-image"
|
|
96
442
|
/>
|
|
97
443
|
)}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
444
|
+
{!isTextOnlyCard && (
|
|
445
|
+
<CapLabel
|
|
446
|
+
type="label19"
|
|
447
|
+
className="rcs-listing-content title"
|
|
448
|
+
fontWeight="bold"
|
|
449
|
+
>
|
|
450
|
+
{title}
|
|
451
|
+
</CapLabel>
|
|
452
|
+
)}
|
|
105
453
|
<CapLabel type="label19" className="rcs-listing-content desc">
|
|
106
454
|
{description}
|
|
107
455
|
</CapLabel>
|
|
@@ -33,6 +33,10 @@ import injectReducer from '../../../utils/injectReducer';
|
|
|
33
33
|
import v2SmsCreateReducer from './reducer';
|
|
34
34
|
import * as globalActions from '../../Cap/actions';
|
|
35
35
|
import TestAndPreviewSlidebox from '../../../v2Components/TestAndPreviewSlidebox';
|
|
36
|
+
import {
|
|
37
|
+
getSmsEmbeddedFooterValidity,
|
|
38
|
+
getSmsMessageFromFormData,
|
|
39
|
+
} from '../smsFormDataHelpers';
|
|
36
40
|
|
|
37
41
|
export class Create extends React.Component { // eslint-disable-line react/prefer-stateless-function
|
|
38
42
|
constructor(props) {
|
|
@@ -159,7 +163,9 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
159
163
|
layout: 'SMS',
|
|
160
164
|
type: 'TAG',
|
|
161
165
|
context: this.props.location.query.type === 'embedded' ? this.props.location.query.module : 'default',
|
|
162
|
-
embedded: this.props.
|
|
166
|
+
embedded: this.props.forceFullTagContext
|
|
167
|
+
? 'full'
|
|
168
|
+
: (this.props.location.query.type === 'embedded' ? this.props.location.query.type : 'full'),
|
|
163
169
|
};
|
|
164
170
|
if (this.props.getDefaultTags) {
|
|
165
171
|
query.context = this.props.getDefaultTags;
|
|
@@ -172,6 +178,16 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
172
178
|
}
|
|
173
179
|
}
|
|
174
180
|
|
|
181
|
+
componentDidUpdate() {
|
|
182
|
+
if (!this.props.embeddedSmsFallback || typeof this.props.onEmbeddedSmsFooterValidity !== 'function') {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
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));
|
|
189
|
+
}
|
|
190
|
+
|
|
175
191
|
componentWillUnmount() {
|
|
176
192
|
if (this.pendingGetFormDataTimeout) {
|
|
177
193
|
clearTimeout(this.pendingGetFormDataTimeout);
|
|
@@ -275,6 +291,10 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
275
291
|
}
|
|
276
292
|
const result = {};
|
|
277
293
|
result.base = baseData;
|
|
294
|
+
/* Root field used by FormBuilder; embedded getFormSubscriptionData reads value.base */
|
|
295
|
+
if (formData['template-name'] !== undefined) {
|
|
296
|
+
result.base['template-name'] = formData['template-name'];
|
|
297
|
+
}
|
|
278
298
|
if (this.state.isValid) {
|
|
279
299
|
const msgObj = charCount.updateCharCount(baseData['sms-editor']);
|
|
280
300
|
result.base.msg_count = msgObj.msgCount;
|
|
@@ -881,7 +901,9 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
881
901
|
layout: 'SMS',
|
|
882
902
|
type: 'TAG',
|
|
883
903
|
context: (data && data.toLowerCase() === 'all') ? 'default' : (data && data.toLowerCase()),
|
|
884
|
-
embedded: this.props.
|
|
904
|
+
embedded: this.props.forceFullTagContext
|
|
905
|
+
? 'full'
|
|
906
|
+
: (this.props.location.query.type === 'embedded' ? this.props.location.query.type : 'full'),
|
|
885
907
|
};
|
|
886
908
|
this.props.globalActions.fetchSchemaForEntity(query);
|
|
887
909
|
}
|
|
@@ -889,10 +911,22 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
889
911
|
removeStandAlone() {
|
|
890
912
|
const schema = _.cloneDeep(this.state.schema);
|
|
891
913
|
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. */
|
|
892
918
|
childSections.splice(2, 1);
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
896
930
|
_.set(schema, 'standalone.sections[0].childSections', childSections);
|
|
897
931
|
this.setState({ schema, loadingStatus: this.state.loadingStatus + 1 });
|
|
898
932
|
}
|
|
@@ -938,37 +972,8 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
938
972
|
this.setState({startValidation: false});
|
|
939
973
|
}
|
|
940
974
|
|
|
941
|
-
getTemplateContent = () =>
|
|
942
|
-
|
|
943
|
-
if (!this.state.formData || !Array.isArray(this.state.formData) || this.state.formData.length === 0) {
|
|
944
|
-
return '';
|
|
945
|
-
}
|
|
946
|
-
const currentTabData = this.state.formData[this.state.currentTab - 1];
|
|
947
|
-
if (!currentTabData) {
|
|
948
|
-
return '';
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
// PRIORITY 1: Check direct path first (most common for SMS)
|
|
952
|
-
// This handles: formData[0]['sms-editor']
|
|
953
|
-
if (currentTabData['sms-editor']) {
|
|
954
|
-
return currentTabData['sms-editor'];
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
// PRIORITY 2: Check activeTab structure (for versioned templates)
|
|
958
|
-
// This handles: formData[0][activeTab]['sms-editor']
|
|
959
|
-
const activeTab = currentTabData?.activeTab || 'base';
|
|
960
|
-
if (currentTabData[activeTab]?.['sms-editor']) {
|
|
961
|
-
return currentTabData[activeTab]['sms-editor'];
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
// PRIORITY 3: Check base explicitly (fallback)
|
|
965
|
-
// This handles: formData[0]['base']['sms-editor']
|
|
966
|
-
if (currentTabData['base']?.['sms-editor']) {
|
|
967
|
-
return currentTabData['base']['sms-editor'];
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
return '';
|
|
971
|
-
}
|
|
975
|
+
getTemplateContent = () =>
|
|
976
|
+
getSmsMessageFromFormData(this.state.formData, this.state.currentTab);
|
|
972
977
|
|
|
973
978
|
handleTestAndPreview = () => {
|
|
974
979
|
// If parent is managing state (props exist), call parent handler
|
|
@@ -997,6 +1002,35 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
997
1002
|
}
|
|
998
1003
|
|
|
999
1004
|
saveFormData() {
|
|
1005
|
+
/*
|
|
1006
|
+
* RCS SMS fallback slidebox (embeddedSmsFallback): parent passes isFullMode from RCS, but we must not
|
|
1007
|
+
* call createTemplate — that spins CapSpin on createTemplateInProgress and is not the embedded contract.
|
|
1008
|
+
* Same as library: hand off form payload via getFormSubscriptionData.
|
|
1009
|
+
*/
|
|
1010
|
+
if (this.props.embeddedSmsFallback && this.props.getFormSubscriptionData) {
|
|
1011
|
+
const { isTemplateNameEmpty, isMessageEmpty } = getSmsEmbeddedFooterValidity(
|
|
1012
|
+
this.state.formData,
|
|
1013
|
+
this.state.tabCount,
|
|
1014
|
+
);
|
|
1015
|
+
if (isTemplateNameEmpty || isMessageEmpty) {
|
|
1016
|
+
this.setState({ startValidation: true, pendingGetFormData: false });
|
|
1017
|
+
if (this.props.onValidationFail) {
|
|
1018
|
+
this.props.onValidationFail();
|
|
1019
|
+
}
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
const payload = this.getFormData();
|
|
1023
|
+
if (!payload.validity) {
|
|
1024
|
+
if (this.props.onValidationFail) {
|
|
1025
|
+
this.props.onValidationFail();
|
|
1026
|
+
}
|
|
1027
|
+
this.setState({ pendingGetFormData: false, startValidation: false });
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
this.props.getFormSubscriptionData(payload);
|
|
1031
|
+
this.setState({ pendingGetFormData: false, startValidation: false });
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1000
1034
|
// In library mode: FormBuilder calls onSubmit only after liquid validation succeeds.
|
|
1001
1035
|
// Submit to parent here so the slidebox can close with valid data.
|
|
1002
1036
|
if (!this.props.isFullMode) {
|
|
@@ -1096,6 +1130,9 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
1096
1130
|
onTestContentClicked={this.props.onTestContentClicked}
|
|
1097
1131
|
onPreviewContentClicked={this.props.onPreviewContentClicked}
|
|
1098
1132
|
eventContextTags={this.props?.eventContextTags}
|
|
1133
|
+
tagListGetPopupContainer={this.props.tagListGetPopupContainer}
|
|
1134
|
+
tagListPopoverOverlayStyle={this.props.tagListPopoverOverlayStyle}
|
|
1135
|
+
tagListPopoverOverlayClassName={this.props.tagListPopoverOverlayClassName}
|
|
1099
1136
|
/>
|
|
1100
1137
|
</CapColumn>
|
|
1101
1138
|
</CapRow>
|
|
@@ -1108,6 +1145,7 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
1108
1145
|
formData={this.state.formData}
|
|
1109
1146
|
content={this.getTemplateContent()}
|
|
1110
1147
|
currentChannel={SMS}
|
|
1148
|
+
smsRegister={this.props.smsRegister}
|
|
1111
1149
|
/>
|
|
1112
1150
|
</div>
|
|
1113
1151
|
);
|
|
@@ -1136,6 +1174,13 @@ Create.propTypes = {
|
|
|
1136
1174
|
handleTestAndPreview: PropTypes.func,
|
|
1137
1175
|
handleCloseTestAndPreview: PropTypes.func,
|
|
1138
1176
|
isTestAndPreviewMode: PropTypes.bool,
|
|
1177
|
+
smsRegister: PropTypes.any,
|
|
1178
|
+
forceFullTagContext: PropTypes.bool,
|
|
1179
|
+
embeddedSmsFallback: PropTypes.bool,
|
|
1180
|
+
onEmbeddedSmsFooterValidity: PropTypes.func,
|
|
1181
|
+
tagListGetPopupContainer: PropTypes.func,
|
|
1182
|
+
tagListPopoverOverlayStyle: PropTypes.object,
|
|
1183
|
+
tagListPopoverOverlayClassName: PropTypes.string,
|
|
1139
1184
|
};
|
|
1140
1185
|
|
|
1141
1186
|
const mapStateToProps = createStructuredSelector({
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared SMS FormBuilder formData helpers for Sms/Create (and any embedded host).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {object} formData FormBuilder state (same shape as Sms/Create `this.state.formData`)
|
|
7
|
+
* @param {number} currentTab 1-based tab index
|
|
8
|
+
* @returns {string} Raw message body or ''
|
|
9
|
+
*/
|
|
10
|
+
export function getSmsMessageFromFormData(formData, currentTab) {
|
|
11
|
+
if (formData == null || typeof formData !== 'object') {
|
|
12
|
+
return '';
|
|
13
|
+
}
|
|
14
|
+
const tab = currentTab != null && currentTab > 0 ? currentTab : 1;
|
|
15
|
+
const currentTabData = formData[tab - 1];
|
|
16
|
+
if (currentTabData && typeof currentTabData === 'object') {
|
|
17
|
+
const versionedKey = tab > 1 ? `sms-editor${tab}` : 'sms-editor';
|
|
18
|
+
if (Object.prototype.hasOwnProperty.call(currentTabData, versionedKey)) {
|
|
19
|
+
const v = currentTabData[versionedKey];
|
|
20
|
+
// Key exists — commit to this version's value rather than falling through to base.
|
|
21
|
+
// Treat null/undefined as empty so a cleared version correctly reports as empty.
|
|
22
|
+
return (v != null && v !== '') ? String(v) : '';
|
|
23
|
+
}
|
|
24
|
+
if (currentTabData['sms-editor'] != null) {
|
|
25
|
+
return String(currentTabData['sms-editor']);
|
|
26
|
+
}
|
|
27
|
+
const activeTab = currentTabData.activeTab || 'base';
|
|
28
|
+
if (currentTabData[activeTab]?.['sms-editor'] != null) {
|
|
29
|
+
return String(currentTabData[activeTab]['sms-editor']);
|
|
30
|
+
}
|
|
31
|
+
if (currentTabData.base?.['sms-editor'] != null) {
|
|
32
|
+
return String(currentTabData.base['sms-editor']);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const rootBase = formData.base;
|
|
36
|
+
if (rootBase && typeof rootBase === 'object' && rootBase['sms-editor'] != null) {
|
|
37
|
+
return String(rootBase['sms-editor']);
|
|
38
|
+
}
|
|
39
|
+
return '';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {number} [tabCount] Total number of versions/tabs. When >1 all versions are checked.
|
|
44
|
+
* @returns {{ isTemplateNameEmpty: boolean, isMessageEmpty: boolean }}
|
|
45
|
+
*/
|
|
46
|
+
export function getSmsEmbeddedFooterValidity(formData, tabCount) {
|
|
47
|
+
const rawName = formData?.['template-name'];
|
|
48
|
+
const name = rawName != null && rawName !== '' ? String(rawName).trim() : '';
|
|
49
|
+
|
|
50
|
+
// Check ALL versions: if any version's message is empty, Done should be disabled.
|
|
51
|
+
// With a single version this is equivalent to the previous single-tab check.
|
|
52
|
+
const count = tabCount != null && tabCount > 1 ? tabCount : 1;
|
|
53
|
+
let isMessageEmpty = false;
|
|
54
|
+
for (let i = 1; i <= count; i++) {
|
|
55
|
+
const content = getSmsMessageFromFormData(formData, i);
|
|
56
|
+
const msg = content != null && content !== '' ? String(content).trim() : '';
|
|
57
|
+
if (!msg) {
|
|
58
|
+
isMessageEmpty = true;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
isTemplateNameEmpty: !name,
|
|
65
|
+
isMessageEmpty,
|
|
66
|
+
};
|
|
67
|
+
}
|