@capillarytech/creatives-library 8.0.199 → 8.0.200-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/constants/unified.js +0 -1
- package/package.json +1 -1
- package/utils/common.js +1 -7
- package/utils/createMobilePushPayload.js +27 -6
- package/utils/tests/createMobilePushPayload.test.js +53 -0
- package/v2Components/FormBuilder/index.js +300 -0
- package/v2Containers/BeeEditor/index.js +1 -1
- package/v2Containers/CreativesContainer/SlideBoxContent.js +2 -6
- package/v2Containers/CreativesContainer/index.js +62 -17
- package/v2Containers/CreativesContainer/tests/SlideBoxContent.test.js +135 -0
- package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +145 -0
- package/v2Containers/Email/index.js +3 -0
- package/v2Containers/MobilePushNew/index.js +172 -70
- package/v2Containers/MobilePushNew/messages.js +17 -0
- package/v2Containers/Templates/index.js +11 -15
package/constants/unified.js
CHANGED
|
@@ -44,7 +44,6 @@ export const REGISTRATION_CUSTOM_FIELD = 'Registration custom fields';
|
|
|
44
44
|
export const GIFT_CARDS = 'GIFT_CARDS';
|
|
45
45
|
export const PROMO_ENGINE = 'PROMO_ENGINE';
|
|
46
46
|
export const LIQUID_SUPPORT = 'ENABLE_LIQUID_SUPPORT';
|
|
47
|
-
export const ENABLE_NEW_MPUSH = 'ENABLE_NEW_MPUSH';
|
|
48
47
|
export const CUSTOM_TAG = 'CustomTagMessage';
|
|
49
48
|
export const CUSTOMER_EXTENDED_FIELD = 'Customer extended fields';
|
|
50
49
|
export const EXTENDED_TAG = 'ExtendedTagMessage';
|
package/package.json
CHANGED
package/utils/common.js
CHANGED
|
@@ -21,8 +21,7 @@ import {
|
|
|
21
21
|
EMAIL_UNSUBSCRIBE_TAG_MANDATORY,
|
|
22
22
|
BADGES_ISSUE,
|
|
23
23
|
ENABLE_WECHAT,
|
|
24
|
-
LIQUID_SUPPORT
|
|
25
|
-
ENABLE_NEW_MPUSH
|
|
24
|
+
LIQUID_SUPPORT
|
|
26
25
|
} from '../constants/unified';
|
|
27
26
|
import { apiMessageFormatHandler } from './commonUtils';
|
|
28
27
|
|
|
@@ -126,11 +125,6 @@ export const isEmailUnsubscribeTagMandatory = Auth.hasFeatureAccess.bind(
|
|
|
126
125
|
EMAIL_UNSUBSCRIBE_TAG_MANDATORY,
|
|
127
126
|
);
|
|
128
127
|
|
|
129
|
-
export const hasNewMobilePushFeatureEnabled = Auth.hasFeatureAccess.bind(
|
|
130
|
-
null,
|
|
131
|
-
ENABLE_NEW_MPUSH,
|
|
132
|
-
);
|
|
133
|
-
|
|
134
128
|
//filtering tags based on scope
|
|
135
129
|
export const filterTags = (tagsToFilter, tagsList) => tagsList?.filter(
|
|
136
130
|
(tag) => !tagsToFilter?.includes(tag?.definition?.value)
|
|
@@ -62,12 +62,33 @@ const createMobilePushPayload = ({
|
|
|
62
62
|
throw new Error(intl.formatMessage(messages.templateNameEmptyError));
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
// Validate content
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
// Validate content - allow single platform if explicitly allowed
|
|
66
|
+
const allowSinglePlatform = options?.allowSinglePlatform || false;
|
|
67
|
+
|
|
68
|
+
if (!allowSinglePlatform) {
|
|
69
|
+
// Normal validation: require all supported platforms to have content
|
|
70
|
+
if (isAndroidSupported && (!androidContent?.title || !androidContent?.message)) {
|
|
71
|
+
throw new Error(intl.formatMessage(messages.contentValidationError, { platform: 'Android' }));
|
|
72
|
+
}
|
|
73
|
+
if (isIosSupported && (!iosContent?.title || !iosContent?.message)) {
|
|
74
|
+
throw new Error(intl.formatMessage(messages.contentValidationError, { platform: 'iOS' }));
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
// Single platform mode: require at least one platform to have content
|
|
78
|
+
const hasAndroidContent = isAndroidSupported && androidContent?.title?.trim() && androidContent?.message?.trim();
|
|
79
|
+
const hasIosContent = isIosSupported && iosContent?.title?.trim() && iosContent?.message?.trim();
|
|
80
|
+
|
|
81
|
+
if (!hasAndroidContent && !hasIosContent) {
|
|
82
|
+
throw new Error(intl.formatMessage(messages.singlePlatformContentMissing));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Validate individual platforms that are supported but have incomplete content
|
|
86
|
+
if (isAndroidSupported && !hasAndroidContent && androidContent) {
|
|
87
|
+
throw new Error(intl.formatMessage(messages.contentValidationError, { platform: 'Android' }));
|
|
88
|
+
}
|
|
89
|
+
if (isIosSupported && !hasIosContent && iosContent) {
|
|
90
|
+
throw new Error(intl.formatMessage(messages.contentValidationError, { platform: 'iOS' }));
|
|
91
|
+
}
|
|
71
92
|
}
|
|
72
93
|
|
|
73
94
|
// Ensure imageSrc has the required properties
|
|
@@ -261,6 +261,59 @@ describe('createMobilePushPayload', () => {
|
|
|
261
261
|
accountData: unsupportedAccountData,
|
|
262
262
|
})).not.toThrow();
|
|
263
263
|
});
|
|
264
|
+
|
|
265
|
+
// Single Platform Mode Tests
|
|
266
|
+
it('should allow single platform when allowSinglePlatform is true - Android only', () => {
|
|
267
|
+
expect(() => callWithIntl({
|
|
268
|
+
templateName: 'Test',
|
|
269
|
+
androidContent: { title: 'Title', message: 'Message' },
|
|
270
|
+
iosContent: null,
|
|
271
|
+
accountData: mockAccountData,
|
|
272
|
+
options: { allowSinglePlatform: true },
|
|
273
|
+
})).not.toThrow();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should allow single platform when allowSinglePlatform is true - iOS only', () => {
|
|
277
|
+
expect(() => callWithIntl({
|
|
278
|
+
templateName: 'Test',
|
|
279
|
+
androidContent: null,
|
|
280
|
+
iosContent: { title: 'Title', message: 'Message' },
|
|
281
|
+
accountData: mockAccountData,
|
|
282
|
+
options: { allowSinglePlatform: true },
|
|
283
|
+
})).not.toThrow();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('should throw error when no platforms have content even with allowSinglePlatform', () => {
|
|
287
|
+
expect(() => callWithIntl({
|
|
288
|
+
templateName: 'Test',
|
|
289
|
+
androidContent: null,
|
|
290
|
+
iosContent: null,
|
|
291
|
+
accountData: mockAccountData,
|
|
292
|
+
options: { allowSinglePlatform: true },
|
|
293
|
+
})).toThrow(intl.formatMessage(messages.singlePlatformContentMissing));
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should validate individual platforms in single platform mode - Android incomplete', () => {
|
|
297
|
+
// Test case where iOS has valid content but Android has incomplete content
|
|
298
|
+
expect(() => callWithIntl({
|
|
299
|
+
templateName: 'Test',
|
|
300
|
+
androidContent: { title: 'Valid Title' }, // Missing message property
|
|
301
|
+
iosContent: { title: 'Valid Title', message: 'Valid Message' }, // Complete content
|
|
302
|
+
accountData: mockAccountData,
|
|
303
|
+
options: { allowSinglePlatform: true },
|
|
304
|
+
})).toThrow(intl.formatMessage(messages.contentValidationError, { platform: 'Android' }));
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should validate individual platforms in single platform mode - iOS incomplete', () => {
|
|
308
|
+
// Test case where Android has valid content but iOS has incomplete content
|
|
309
|
+
expect(() => callWithIntl({
|
|
310
|
+
templateName: 'Test',
|
|
311
|
+
androidContent: { title: 'Valid Title', message: 'Valid Message' }, // Complete content
|
|
312
|
+
iosContent: { title: 'Valid Title' }, // Missing message property
|
|
313
|
+
accountData: mockAccountData,
|
|
314
|
+
options: { allowSinglePlatform: true },
|
|
315
|
+
})).toThrow(intl.formatMessage(messages.contentValidationError, { platform: 'iOS' }));
|
|
316
|
+
});
|
|
264
317
|
});
|
|
265
318
|
|
|
266
319
|
// Account Data Validation Tests
|
|
@@ -139,8 +139,196 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
139
139
|
// Check if the liquid flow feature is supported and the channel is in the supported list.
|
|
140
140
|
this.liquidFlow = this.isLiquidFlowSupported.bind(this);
|
|
141
141
|
this.onSubmitWrapper = this.onSubmitWrapper.bind(this);
|
|
142
|
+
|
|
143
|
+
// Performance optimization: Debounced functions for high-frequency updates
|
|
144
|
+
this.debouncedUpdateFormData = _.debounce((data, val, event, skipStateUpdate) => {
|
|
145
|
+
this.performFormDataUpdate(data, val, event, skipStateUpdate);
|
|
146
|
+
}, 300);
|
|
147
|
+
this.debouncedValidation = _.debounce(this.validateForm.bind(this), 500);
|
|
148
|
+
|
|
149
|
+
// Memoized validation cache
|
|
150
|
+
this.validationCache = new Map();
|
|
151
|
+
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Helper function to generate unique tab ID
|
|
155
|
+
generateUniqueTabId(formData, tabIndex) {
|
|
156
|
+
let id = _.uniqueId();
|
|
157
|
+
let validId = false;
|
|
158
|
+
|
|
159
|
+
while (!validId) {
|
|
160
|
+
validId = true;
|
|
161
|
+
for (let idx = 0; idx < formData[tabIndex].selectedLanguages.length; idx += 1) {
|
|
162
|
+
if (!formData[tabIndex]) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (id === formData[tabIndex][formData[tabIndex].selectedLanguages[idx]].tabKey) {
|
|
166
|
+
validId = false;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (!validId) {
|
|
171
|
+
id = _.uniqueId();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return id;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Performance optimized form data update function
|
|
179
|
+
performFormDataUpdate(data, val, event, skipStateUpdate = false) {
|
|
180
|
+
|
|
181
|
+
// Use optimized state update instead of deep cloning
|
|
182
|
+
const formData = this.optimizedFormDataUpdate(data, val, event);
|
|
183
|
+
|
|
184
|
+
const tabIndex = this.state.currentTab - 1;
|
|
185
|
+
let currentTab = this.state.currentTab;
|
|
186
|
+
|
|
187
|
+
if (this.state.usingTabContainer && !val.standalone) {
|
|
188
|
+
const data1 = data;
|
|
189
|
+
if (event === "addLanguage") {
|
|
190
|
+
const addLanguageType = this.props.addLanguageType;
|
|
191
|
+
if (addLanguageType === '') {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const currentLang = formData[tabIndex].activeTab;
|
|
195
|
+
let baseTab;
|
|
196
|
+
|
|
197
|
+
switch (addLanguageType) {
|
|
198
|
+
case "upload":
|
|
199
|
+
baseTab = _.cloneDeep(formData[tabIndex][currentLang]);
|
|
200
|
+
|
|
201
|
+
baseTab.iso_code = data.iso_code;
|
|
202
|
+
baseTab.lang_id = data.lang_id;
|
|
203
|
+
baseTab.language = data.language;
|
|
204
|
+
baseTab.tabKey = this.generateUniqueTabId(formData, tabIndex);
|
|
205
|
+
|
|
206
|
+
formData[tabIndex][data.iso_code] = baseTab;
|
|
207
|
+
formData[tabIndex].activeTab = data.iso_code;
|
|
208
|
+
formData[tabIndex].tabKey = baseTab.tabKey;
|
|
209
|
+
break;
|
|
210
|
+
case "copyPrimaryLanguage":
|
|
211
|
+
case "useEditor":
|
|
212
|
+
baseTab = _.cloneDeep(formData[tabIndex][this.props.baseLanguage]);
|
|
213
|
+
|
|
214
|
+
baseTab.iso_code = data.iso_code;
|
|
215
|
+
baseTab.lang_id = data.lang_id;
|
|
216
|
+
baseTab.language = data.language;
|
|
217
|
+
baseTab.tabKey = this.generateUniqueTabId(formData, tabIndex);
|
|
218
|
+
|
|
219
|
+
formData[tabIndex].selectedLanguages.push(data.iso_code);
|
|
220
|
+
formData[tabIndex][data.iso_code] = baseTab;
|
|
221
|
+
formData[tabIndex].activeTab = data.iso_code;
|
|
222
|
+
formData[tabIndex].tabKey = baseTab.tabKey;
|
|
223
|
+
break;
|
|
224
|
+
case '':
|
|
225
|
+
return;
|
|
226
|
+
default:
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
const that = this;
|
|
230
|
+
setTimeout(() => {
|
|
231
|
+
that.setState({tabKey: baseTab.tabKey});
|
|
232
|
+
}, 0);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!this.props.isNewVersionFlow) {
|
|
236
|
+
formData[tabIndex][val.id] = data1;
|
|
237
|
+
} else if (this.props.isNewVersionFlow && event !== "addLanguage" && event !== "onContentChange") {
|
|
238
|
+
formData[tabIndex][this.props.baseLanguage][val.id] = data1;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (formData[tabIndex].base) {
|
|
242
|
+
if (!this.props.isNewVersionFlow) {
|
|
243
|
+
formData.base[val.id] = data1;
|
|
244
|
+
} else {
|
|
245
|
+
formData.base[data1.iso_code] = formData[tabIndex][data1.iso_code];
|
|
246
|
+
formData.base.tabKey = formData[tabIndex].tabKey;
|
|
247
|
+
formData.base.activeTab = formData[tabIndex].activeTab;
|
|
248
|
+
formData.base.selectedLanguages = formData[tabIndex].selectedLanguages;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
formData[val.id] = data;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (this.props.isNewVersionFlow) {
|
|
256
|
+
if (event === 'onSelect' && data === 'New Version') {
|
|
257
|
+
this.callChildEvent(data, val, 'addVersion', event);
|
|
258
|
+
} else if (event === 'onSelect' && data !== 'New Version') {
|
|
259
|
+
currentTab = _.findIndex(this.state.formData['template-version-options'], { key: data}) + 1;
|
|
260
|
+
this.setState({currentTab, tabKey: formData[`${currentTab - 1}`].tabKey}, () => {
|
|
261
|
+
val.injectedEvents[event].call(this, this.state.formData['template-version-options'][currentTab - 1].key, formData, val);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (event === 'onContentChange') {
|
|
266
|
+
// Content change handling
|
|
267
|
+
}
|
|
268
|
+
}
|
|
142
269
|
|
|
270
|
+
// Only update state if not already updated (for immediate UI feedback)
|
|
271
|
+
if (!skipStateUpdate) {
|
|
272
|
+
this.setState({formData}, () => {
|
|
273
|
+
if (this.props.startValidation) {
|
|
274
|
+
this.debouncedValidation();
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
} else {
|
|
278
|
+
// Just trigger validation if state was already updated
|
|
279
|
+
if (this.props.startValidation) {
|
|
280
|
+
this.debouncedValidation();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (event && val.injectedEvents[event]) {
|
|
285
|
+
if (event === "onRowClick") {
|
|
286
|
+
val.injectedEvents[event].call(this, data);
|
|
287
|
+
} else if (this.props.isNewVersionFlow && event !== 'onSelect') {
|
|
288
|
+
if (event === 'addLanguage') {
|
|
289
|
+
val.injectedEvents[event].call(this, data, formData, val);
|
|
290
|
+
this.setState({currentEventVal: {}, currentEvent: {}, currentEventData: {}});
|
|
291
|
+
} else {
|
|
292
|
+
val.injectedEvents[event].call(this, true, formData, val);
|
|
293
|
+
}
|
|
294
|
+
} else if (!this.props.isNewVersionFlow) {
|
|
295
|
+
val.injectedEvents[event].call(this, true, formData, val);
|
|
296
|
+
}
|
|
297
|
+
} else if (val.injectedEvents && val.injectedEvents.onChange) {
|
|
298
|
+
val.injectedEvents.onChange.call(this, true, formData, val);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!((event === 'onSelect' && data === 'New Version') || event === 'onContentChange')) {
|
|
302
|
+
this.props.onChange(formData, this.state.tabCount, currentTab, val);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Optimized form data update - only clone what's necessary
|
|
307
|
+
optimizedFormDataUpdate(data, val, event) {
|
|
308
|
+
const currentFormData = this.state.formData;
|
|
309
|
+
|
|
310
|
+
// For simple field updates, use spread operator instead of deep clone
|
|
311
|
+
if (!this.state.usingTabContainer || val.standalone) {
|
|
312
|
+
return {
|
|
313
|
+
...currentFormData,
|
|
314
|
+
[val.id]: data
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// For tab container updates, only clone the affected tab
|
|
319
|
+
const tabIndex = this.state.currentTab - 1;
|
|
320
|
+
const updatedFormData = { ...currentFormData };
|
|
321
|
+
|
|
322
|
+
if (updatedFormData[tabIndex]) {
|
|
323
|
+
updatedFormData[tabIndex] = {
|
|
324
|
+
...updatedFormData[tabIndex],
|
|
325
|
+
[val.id]: data
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return updatedFormData;
|
|
143
330
|
}
|
|
331
|
+
|
|
144
332
|
isLiquidFlowSupported = () => {
|
|
145
333
|
return Boolean(LIQUID_SUPPORTED_CHANNELS.includes(this.props?.schema?.channel?.toUpperCase()) && hasLiquidSupportFeature());
|
|
146
334
|
}
|
|
@@ -2027,6 +2215,64 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
2027
2215
|
}
|
|
2028
2216
|
|
|
2029
2217
|
updateFormData(data, val, event) {
|
|
2218
|
+
|
|
2219
|
+
// Check if this is a high-frequency input field that should be optimized
|
|
2220
|
+
const isHighFrequencyField = val && (
|
|
2221
|
+
val.id === 'template-name' ||
|
|
2222
|
+
val.id === 'template-subject' ||
|
|
2223
|
+
val.type === 'input' ||
|
|
2224
|
+
val.type === 'textarea'
|
|
2225
|
+
);
|
|
2226
|
+
|
|
2227
|
+
if (isHighFrequencyField && !event) {
|
|
2228
|
+
// For high-frequency fields: immediate UI update + debounced expensive operations
|
|
2229
|
+
this.updateFormDataOptimized(data, val, event);
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
// For non-high-frequency fields or special events, use immediate update
|
|
2234
|
+
this.performFormDataUpdate(data, val, event);
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
// Optimized update for high-frequency fields
|
|
2238
|
+
updateFormDataOptimized(data, val, event) {
|
|
2239
|
+
// 1. Immediate UI update - update the field value instantly
|
|
2240
|
+
this.updateFieldValueImmediately(data, val);
|
|
2241
|
+
|
|
2242
|
+
// 2. Debounce expensive operations (validation, parent updates) - skip state update since we already did it
|
|
2243
|
+
this.debouncedUpdateFormData(data, val, event, true);
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
// Update field value immediately for UI feedback
|
|
2247
|
+
updateFieldValueImmediately(data, val) {
|
|
2248
|
+
const currentFormData = this.state.formData;
|
|
2249
|
+
let updatedFormData;
|
|
2250
|
+
|
|
2251
|
+
if (!this.state.usingTabContainer || val.standalone) {
|
|
2252
|
+
// Simple field update
|
|
2253
|
+
updatedFormData = {
|
|
2254
|
+
...currentFormData,
|
|
2255
|
+
[val.id]: data
|
|
2256
|
+
};
|
|
2257
|
+
} else {
|
|
2258
|
+
// Tab container update
|
|
2259
|
+
const tabIndex = this.state.currentTab - 1;
|
|
2260
|
+
updatedFormData = { ...currentFormData };
|
|
2261
|
+
|
|
2262
|
+
if (updatedFormData[tabIndex]) {
|
|
2263
|
+
updatedFormData[tabIndex] = {
|
|
2264
|
+
...updatedFormData[tabIndex],
|
|
2265
|
+
[val.id]: data
|
|
2266
|
+
};
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
// Update state immediately for UI feedback
|
|
2271
|
+
this.setState({ formData: updatedFormData });
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
// Legacy updateFormData function - kept for backward compatibility
|
|
2275
|
+
updateFormDataLegacy(data, val, event) {
|
|
2030
2276
|
const formData = _.cloneDeep(this.state.formData);
|
|
2031
2277
|
|
|
2032
2278
|
const tabIndex = this.state.currentTab - 1;
|
|
@@ -2186,6 +2432,57 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
2186
2432
|
hasClass(element, className) {
|
|
2187
2433
|
return (` ${element.className} `).indexOf(` ${className} `) > -1;
|
|
2188
2434
|
}
|
|
2435
|
+
|
|
2436
|
+
// Handle field blur for validation
|
|
2437
|
+
handleFieldBlur = (e, val) => {
|
|
2438
|
+
// Trigger validation on blur for high-frequency fields
|
|
2439
|
+
if (val && (val.id === 'template-name' || val.id === 'template-subject' || val.type === 'input' || val.type === 'textarea')) {
|
|
2440
|
+
this.debouncedValidation();
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
// Memoized validation for specific fields
|
|
2445
|
+
getMemoizedValidation = (fieldId, fieldValue) => {
|
|
2446
|
+
const cacheKey = `${fieldId}_${fieldValue}`;
|
|
2447
|
+
|
|
2448
|
+
if (this.validationCache.has(cacheKey)) {
|
|
2449
|
+
return this.validationCache.get(cacheKey);
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
// Perform validation logic here
|
|
2453
|
+
const isValid = this.performFieldValidation(fieldId, fieldValue);
|
|
2454
|
+
this.validationCache.set(cacheKey, isValid);
|
|
2455
|
+
|
|
2456
|
+
// Clear cache if it gets too large
|
|
2457
|
+
if (this.validationCache.size > 100) {
|
|
2458
|
+
this.validationCache.clear();
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
return isValid;
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
// Perform validation for a specific field
|
|
2465
|
+
performFieldValidation = (fieldId, fieldValue) => {
|
|
2466
|
+
// Basic validation logic for template-name and template-subject
|
|
2467
|
+
if (fieldId === 'template-name' || fieldId === 'template-subject') {
|
|
2468
|
+
return fieldValue && fieldValue.trim().length > 0;
|
|
2469
|
+
}
|
|
2470
|
+
return true;
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
// Cleanup debounced functions on component unmount
|
|
2474
|
+
componentWillUnmount() {
|
|
2475
|
+
if (this.debouncedUpdateFormData) {
|
|
2476
|
+
this.debouncedUpdateFormData.cancel();
|
|
2477
|
+
}
|
|
2478
|
+
if (this.debouncedValidation) {
|
|
2479
|
+
this.debouncedValidation.cancel();
|
|
2480
|
+
}
|
|
2481
|
+
// Clear validation cache
|
|
2482
|
+
if (this.validationCache) {
|
|
2483
|
+
this.validationCache.clear();
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2189
2486
|
allowAddSecondaryCta = (val) => {
|
|
2190
2487
|
if (val.fieldsCount > 0) {
|
|
2191
2488
|
const errorData = _.cloneDeep(this.state.errorData);
|
|
@@ -2601,6 +2898,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
2601
2898
|
label={val.label}
|
|
2602
2899
|
autosize={val.autosize ? val.autosizeParams : false}
|
|
2603
2900
|
onChange={(e) => this.updateFormData(e.target.value, val)}
|
|
2901
|
+
onBlur={(e) => this.handleFieldBlur(e, val)}
|
|
2604
2902
|
style={val.style ? val.style : {}}
|
|
2605
2903
|
defaultValue={messageContent || ''}
|
|
2606
2904
|
value={messageContent || ""}
|
|
@@ -2682,6 +2980,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
2682
2980
|
style={val.style ? val.style : {}}
|
|
2683
2981
|
placeholder={val.placeholder}
|
|
2684
2982
|
onChange={(e) => this.updateFormData(e.target.value, val)}
|
|
2983
|
+
onBlur={(e) => this.handleFieldBlur(e, val)}
|
|
2685
2984
|
defaultValue={isVersionEnable ? this.state.formData[`${this.state.currentTab - 1}`][val.id] : this.state.formData[val.id]}
|
|
2686
2985
|
value={value || ""}
|
|
2687
2986
|
disabled={val.disabled}
|
|
@@ -3166,6 +3465,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
3166
3465
|
style={val.style ? val.style : {}}
|
|
3167
3466
|
placeholder={val.placeholder}
|
|
3168
3467
|
onChange={(e) => this.updateFormData(e.target.value, val)}
|
|
3468
|
+
onBlur={(e) => this.handleFieldBlur(e, val)}
|
|
3169
3469
|
value={value || ""}
|
|
3170
3470
|
defaultValue={isVersionEnable ? this.state.formData[`${this.state.currentTab - 1}`][val.id] : this.state.formData[val.id]}
|
|
3171
3471
|
disabled={val.disabled}
|
|
@@ -349,4 +349,4 @@ const withConnect = connect(mapStateToProps, mapDispatchToProps);
|
|
|
349
349
|
const withSaga = injectSaga({ key: 'beeEditor', saga: v2BeeEditionSagas });
|
|
350
350
|
const withReducer = injectReducer({ key: 'beeEditor', reducer: v2BeeEditionReducer });
|
|
351
351
|
|
|
352
|
-
export default compose(withReducer, withSaga, withConnect)(injectIntl(BeeEditor));
|
|
352
|
+
export default compose(withReducer, withSaga, withConnect)(injectIntl(React.memo(BeeEditor)));
|
|
@@ -668,9 +668,7 @@ export function SlideBoxContent(props) {
|
|
|
668
668
|
/>
|
|
669
669
|
)}
|
|
670
670
|
{isEditMPush && (
|
|
671
|
-
(isFullMode &&
|
|
672
|
-
(!isFullMode && isLoyaltyModule) ||
|
|
673
|
-
(!isFullMode && !isLoyaltyModule && !commonUtil.hasNewMobilePushFeatureEnabled()) ? (
|
|
671
|
+
(!isFullMode && isLoyaltyModule) ? (
|
|
674
672
|
<MobliPushEdit
|
|
675
673
|
getFormLibraryData={getFormData}
|
|
676
674
|
setIsLoadingContent={setIsLoadingContent}
|
|
@@ -726,9 +724,7 @@ export function SlideBoxContent(props) {
|
|
|
726
724
|
)
|
|
727
725
|
)}
|
|
728
726
|
{isCreateMPush && (
|
|
729
|
-
(isFullMode &&
|
|
730
|
-
(!isFullMode && isLoyaltyModule) ||
|
|
731
|
-
(!isFullMode && !isLoyaltyModule && !commonUtil.hasNewMobilePushFeatureEnabled()) ? (
|
|
727
|
+
(!isFullMode && isLoyaltyModule) ? (
|
|
732
728
|
<MobilepushWrapper
|
|
733
729
|
key="creatives-mobilepush-wrapper"
|
|
734
730
|
date={new Date().getMilliseconds()}
|
|
@@ -11,7 +11,7 @@ import CapNotification from '@capillarytech/cap-ui-library/CapNotification';
|
|
|
11
11
|
import { injectIntl, FormattedMessage } from 'react-intl';
|
|
12
12
|
import classnames from 'classnames';
|
|
13
13
|
import {
|
|
14
|
-
isEmpty, get, forEach, cloneDeep,
|
|
14
|
+
isEmpty, get, forEach, cloneDeep, debounce,
|
|
15
15
|
} from 'lodash';
|
|
16
16
|
import { connect } from 'react-redux';
|
|
17
17
|
import { createStructuredSelector } from 'reselect';
|
|
@@ -105,6 +105,8 @@ export class Creatives extends React.Component {
|
|
|
105
105
|
// NEW: Test and Preview feature state
|
|
106
106
|
showTestAndPreviewSlidebox: false,
|
|
107
107
|
isTestAndPreviewMode: false, // Add flag to track Test & Preview mode
|
|
108
|
+
// Performance optimization: Local template name for immediate UI feedback
|
|
109
|
+
localTemplateName: '',
|
|
108
110
|
};
|
|
109
111
|
this.liquidFlow = Boolean(commonUtil.hasLiquidSupportFeature());
|
|
110
112
|
this.creativesTemplateSteps = {
|
|
@@ -112,6 +114,9 @@ export class Creatives extends React.Component {
|
|
|
112
114
|
2: 'templateSelection', // only for email in current flows wil be used for mpush, line and wechat as well.
|
|
113
115
|
3: 'createTemplateContent',
|
|
114
116
|
};
|
|
117
|
+
|
|
118
|
+
// Performance optimization: Debounced template name update
|
|
119
|
+
this.debouncedTemplateNameUpdate = debounce(this.performTemplateNameUpdate.bind(this), 300);
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
componentWillUnmount() {
|
|
@@ -119,6 +124,11 @@ export class Creatives extends React.Component {
|
|
|
119
124
|
this.props.templateActions.resetTemplateStoreData();
|
|
120
125
|
}
|
|
121
126
|
this.props.globalActions.clearMetaEntities();
|
|
127
|
+
|
|
128
|
+
// Cleanup debounced function
|
|
129
|
+
if (this.debouncedTemplateNameUpdate) {
|
|
130
|
+
this.debouncedTemplateNameUpdate.cancel();
|
|
131
|
+
}
|
|
122
132
|
}
|
|
123
133
|
|
|
124
134
|
componentDidMount() {
|
|
@@ -136,6 +146,29 @@ export class Creatives extends React.Component {
|
|
|
136
146
|
|
|
137
147
|
onEnterTemplateName = () => {
|
|
138
148
|
this.setState({ templateNameExists: true });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Performance optimized template name update
|
|
152
|
+
performTemplateNameUpdate = (value, formData, onFormDataChange) => {
|
|
153
|
+
const isEmptyTemplateName = !value.trim();
|
|
154
|
+
const newFormData = { ...formData, 'template-name': value, 'isTemplateNameEdited': true };
|
|
155
|
+
|
|
156
|
+
this.setState({ isTemplateNameEmpty: isEmptyTemplateName });
|
|
157
|
+
onFormDataChange(newFormData);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Update template name immediately for UI feedback
|
|
161
|
+
updateTemplateNameImmediately = (value, formData, onFormDataChange) => {
|
|
162
|
+
const isEmptyTemplateName = !value.trim();
|
|
163
|
+
|
|
164
|
+
// 1. IMMEDIATE: Update local state for instant UI feedback
|
|
165
|
+
this.setState({
|
|
166
|
+
isTemplateNameEmpty: isEmptyTemplateName,
|
|
167
|
+
localTemplateName: value
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// 2. DEBOUNCED: Only debounce the expensive onFormDataChange call
|
|
171
|
+
this.debouncedTemplateNameUpdate(value, formData, onFormDataChange);
|
|
139
172
|
};
|
|
140
173
|
|
|
141
174
|
onRemoveTemplateName = () => {
|
|
@@ -1395,21 +1428,30 @@ export class Creatives extends React.Component {
|
|
|
1395
1428
|
} />
|
|
1396
1429
|
)
|
|
1397
1430
|
|
|
1398
|
-
templateNameComponentInput = ({ formData, onFormDataChange, name }) =>
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1431
|
+
templateNameComponentInput = ({ formData, onFormDataChange, name }) => {
|
|
1432
|
+
// Use local state for immediate UI feedback, fallback to prop value
|
|
1433
|
+
const displayValue = this.state.localTemplateName !== '' ? this.state.localTemplateName : name;
|
|
1434
|
+
|
|
1435
|
+
return (
|
|
1436
|
+
<CapInput
|
|
1437
|
+
value={displayValue}
|
|
1438
|
+
suffix={<span />}
|
|
1439
|
+
onBlur={() => {
|
|
1440
|
+
this.setState({
|
|
1441
|
+
isEditName: false,
|
|
1442
|
+
localTemplateName: '' // Clear local state on blur
|
|
1443
|
+
}, () => {
|
|
1444
|
+
this.showTemplateName({ formData, onFormDataChange });
|
|
1445
|
+
});
|
|
1446
|
+
}}
|
|
1447
|
+
onChange={(ev) => {
|
|
1448
|
+
const { value } = ev.currentTarget;
|
|
1449
|
+
// Use optimized update for better performance
|
|
1450
|
+
this.updateTemplateNameImmediately(value, formData, onFormDataChange);
|
|
1451
|
+
}}
|
|
1452
|
+
/>
|
|
1453
|
+
);
|
|
1454
|
+
}
|
|
1413
1455
|
|
|
1414
1456
|
showTemplateName = ({ formData, onFormDataChange }) => { //gets called from email/index after template data is fetched
|
|
1415
1457
|
const { slidBoxContent, currentChannel, isEditName } = this.state;
|
|
@@ -1423,7 +1465,10 @@ export class Creatives extends React.Component {
|
|
|
1423
1465
|
if (name && !isEditName) {
|
|
1424
1466
|
this.setState({ showTemplateNameComponentEdit: false });
|
|
1425
1467
|
} else if (isEditName) {
|
|
1426
|
-
this.setState({
|
|
1468
|
+
this.setState({
|
|
1469
|
+
showTemplateNameComponentEdit: true,
|
|
1470
|
+
localTemplateName: name || '' // Initialize local state with current value
|
|
1471
|
+
});
|
|
1427
1472
|
}
|
|
1428
1473
|
}
|
|
1429
1474
|
}
|
|
@@ -700,4 +700,139 @@ describe('Test SlideBoxContent container', () => {
|
|
|
700
700
|
expect(renderedComponent).toMatchSnapshot();
|
|
701
701
|
});
|
|
702
702
|
});
|
|
703
|
+
|
|
704
|
+
// Mobile Push Loyalty Module Tests
|
|
705
|
+
describe('Mobile Push with Loyalty Module', () => {
|
|
706
|
+
it('Should render MobliPushEdit for loyalty module in library mode (edit)', () => {
|
|
707
|
+
renderFunction('MOBILE_PUSH', 'editTemplate', { _id: 'test-id' }, {
|
|
708
|
+
isFullMode: false,
|
|
709
|
+
isLoyaltyModule: true
|
|
710
|
+
});
|
|
711
|
+
expect(renderedComponent).toMatchSnapshot();
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it('Should render MobilePushNew for loyalty module in full mode (edit)', () => {
|
|
715
|
+
renderFunction('MOBILE_PUSH', 'editTemplate', { _id: 'test-id' }, {
|
|
716
|
+
isFullMode: true,
|
|
717
|
+
isLoyaltyModule: true
|
|
718
|
+
});
|
|
719
|
+
expect(renderedComponent).toMatchSnapshot();
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
it('Should render MobilepushWrapper for loyalty module in library mode (create)', () => {
|
|
723
|
+
renderFunction('MOBILE_PUSH', 'createTemplate', { mode: 'create' }, {
|
|
724
|
+
isFullMode: false,
|
|
725
|
+
isLoyaltyModule: true
|
|
726
|
+
});
|
|
727
|
+
expect(renderedComponent).toMatchSnapshot();
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
it('Should render MobilePushNew for loyalty module in full mode (create)', () => {
|
|
731
|
+
renderFunction('MOBILE_PUSH', 'createTemplate', { mode: 'create' }, {
|
|
732
|
+
isFullMode: true,
|
|
733
|
+
isLoyaltyModule: true
|
|
734
|
+
});
|
|
735
|
+
expect(renderedComponent).toMatchSnapshot();
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
it('Should render MobilePushNew for non-loyalty module in library mode (edit)', () => {
|
|
739
|
+
renderFunction('MOBILE_PUSH', 'editTemplate', { _id: 'test-id' }, {
|
|
740
|
+
isFullMode: false,
|
|
741
|
+
isLoyaltyModule: false
|
|
742
|
+
});
|
|
743
|
+
expect(renderedComponent).toMatchSnapshot();
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
it('Should render MobilePushNew for non-loyalty module in library mode (create)', () => {
|
|
747
|
+
renderFunction('MOBILE_PUSH', 'createTemplate', { mode: 'create' }, {
|
|
748
|
+
isFullMode: false,
|
|
749
|
+
isLoyaltyModule: false
|
|
750
|
+
});
|
|
751
|
+
expect(renderedComponent).toMatchSnapshot();
|
|
752
|
+
});
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
// Additional edge case tests to cover uncovered lines
|
|
756
|
+
describe('Edge Cases for Uncovered Lines', () => {
|
|
757
|
+
it('Should handle getLineType with LINE channel and empty templateData', () => {
|
|
758
|
+
// This should cover lines 72-74 in getLineType function
|
|
759
|
+
// LINE channel, not full mode, templateData exists but has no _id, no messageBody, and not isDefault
|
|
760
|
+
const emptyLineTemplateData = {
|
|
761
|
+
type: 'LINE',
|
|
762
|
+
isDefault: false,
|
|
763
|
+
// No _id and no versions.base.content.messages[0]
|
|
764
|
+
};
|
|
765
|
+
renderFunction('LINE', 'editTemplate', emptyLineTemplateData, {
|
|
766
|
+
isFullMode: false
|
|
767
|
+
});
|
|
768
|
+
expect(renderedComponent).toMatchSnapshot();
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
it('Should handle SMS channel with DLT content processing', () => {
|
|
772
|
+
// This should cover line 251 in SMS content processing
|
|
773
|
+
// Need isDltEnabled = true and updatedSmsEditor as array
|
|
774
|
+
// Mock hasTraiDltFeature to return true
|
|
775
|
+
const commonUtils = require('../../../utils/common');
|
|
776
|
+
const originalHasTraiDltFeature = commonUtils.hasTraiDltFeature;
|
|
777
|
+
commonUtils.hasTraiDltFeature = jest.fn(() => true);
|
|
778
|
+
|
|
779
|
+
const smsTemplateData = {
|
|
780
|
+
type: 'SMS',
|
|
781
|
+
versions: {
|
|
782
|
+
base: {
|
|
783
|
+
'updated-sms-editor': ['Line 1', 'Line 2', 'Line 3'], // Array to trigger join()
|
|
784
|
+
'sms-editor': 'Fallback SMS content'
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
renderFunction('SMS', 'preview', smsTemplateData, {
|
|
789
|
+
smsRegister: 'DLT', // Enable DLT for library mode
|
|
790
|
+
isFullMode: false
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
// Restore original function
|
|
794
|
+
commonUtils.hasTraiDltFeature = originalHasTraiDltFeature;
|
|
795
|
+
expect(renderedComponent).toMatchSnapshot();
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
it('Should handle LINE channel with video content', () => {
|
|
799
|
+
// This should cover lines 314-315 for video detection
|
|
800
|
+
// Need a LINE message with video property but no text, previewImageUrl, selectedSticker, baseUrl, template, or contents
|
|
801
|
+
const lineTemplateDataWithVideo = {
|
|
802
|
+
type: 'LINE',
|
|
803
|
+
versions: {
|
|
804
|
+
base: {
|
|
805
|
+
content: {
|
|
806
|
+
messages: [{
|
|
807
|
+
video: {
|
|
808
|
+
originalContentUrl: 'https://example.com/video.mp4',
|
|
809
|
+
externalLink: {
|
|
810
|
+
linkUri: 'https://example.com/link'
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
// No text, previewImageUrl, selectedSticker, baseUrl, template, or contents
|
|
814
|
+
}]
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
renderFunction('LINE', 'preview', lineTemplateDataWithVideo);
|
|
820
|
+
expect(renderedComponent).toMatchSnapshot();
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
it('Should handle default case in getChannelPreviewContent', () => {
|
|
824
|
+
// This should cover line 395 - default case return
|
|
825
|
+
const unknownChannelData = {
|
|
826
|
+
versions: {
|
|
827
|
+
base: {
|
|
828
|
+
content: {
|
|
829
|
+
customField: 'custom content'
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
renderFunction('UNKNOWN_CHANNEL', 'preview', unknownChannelData);
|
|
835
|
+
expect(renderedComponent).toMatchSnapshot();
|
|
836
|
+
});
|
|
837
|
+
});
|
|
703
838
|
});
|
|
@@ -1,5 +1,138 @@
|
|
|
1
1
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
2
|
|
|
3
|
+
exports[`Test SlideBoxContent container Edge Cases for Uncovered Lines Should handle LINE channel with video content 1`] = `
|
|
4
|
+
<SlideBoxContent__CreativesWrapper>
|
|
5
|
+
<InjectIntl(TemplatePreview)
|
|
6
|
+
channel="line"
|
|
7
|
+
charCounterEnabled={false}
|
|
8
|
+
content={
|
|
9
|
+
Array [
|
|
10
|
+
Object {
|
|
11
|
+
"actionUrl": "https://example.com/link",
|
|
12
|
+
"baseUrl": undefined,
|
|
13
|
+
"imageCarousel": Array [
|
|
14
|
+
Object {
|
|
15
|
+
"actionLabel": undefined,
|
|
16
|
+
"imageUrl": undefined,
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
"messageContent": undefined,
|
|
20
|
+
"previewImageUrl": undefined,
|
|
21
|
+
"selectedSticker": Object {
|
|
22
|
+
"animatedStickerUrl": undefined,
|
|
23
|
+
"packageId": undefined,
|
|
24
|
+
"stickerId": undefined,
|
|
25
|
+
"stickerUrl": undefined,
|
|
26
|
+
},
|
|
27
|
+
"type": "image_carousel",
|
|
28
|
+
"video": "https://example.com/video.mp4",
|
|
29
|
+
},
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
showCount={true}
|
|
33
|
+
templateData={
|
|
34
|
+
Object {
|
|
35
|
+
"type": "LINE",
|
|
36
|
+
"versions": Object {
|
|
37
|
+
"base": Object {
|
|
38
|
+
"content": Object {
|
|
39
|
+
"messages": Array [
|
|
40
|
+
Object {
|
|
41
|
+
"video": Object {
|
|
42
|
+
"externalLink": Object {
|
|
43
|
+
"linkUri": "https://example.com/link",
|
|
44
|
+
},
|
|
45
|
+
"originalContentUrl": "https://example.com/video.mp4",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
viberBrandName=""
|
|
55
|
+
/>
|
|
56
|
+
<Connect(Connect(UserIsAuthenticated(InjectIntl(CreativesCommon))))
|
|
57
|
+
location={
|
|
58
|
+
Object {
|
|
59
|
+
"pathname": "/sms/edit",
|
|
60
|
+
"query": Object {
|
|
61
|
+
"isEditFromCampaigns": undefined,
|
|
62
|
+
"module": "library",
|
|
63
|
+
"type": "embedded",
|
|
64
|
+
},
|
|
65
|
+
"search": "",
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
onCreateComplete={[MockFunction]}
|
|
69
|
+
params={
|
|
70
|
+
Object {
|
|
71
|
+
"id": undefined,
|
|
72
|
+
"mode": undefined,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
route={
|
|
76
|
+
Object {
|
|
77
|
+
"name": "edit_text",
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
templateData={
|
|
81
|
+
Object {
|
|
82
|
+
"type": "LINE",
|
|
83
|
+
"versions": Object {
|
|
84
|
+
"base": Object {
|
|
85
|
+
"content": Object {
|
|
86
|
+
"messages": Array [
|
|
87
|
+
Object {
|
|
88
|
+
"video": Object {
|
|
89
|
+
"externalLink": Object {
|
|
90
|
+
"linkUri": "https://example.com/link",
|
|
91
|
+
},
|
|
92
|
+
"originalContentUrl": "https://example.com/video.mp4",
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/>
|
|
102
|
+
</SlideBoxContent__CreativesWrapper>
|
|
103
|
+
`;
|
|
104
|
+
|
|
105
|
+
exports[`Test SlideBoxContent container Edge Cases for Uncovered Lines Should handle SMS channel with DLT content processing 1`] = `
|
|
106
|
+
<SlideBoxContent__CreativesWrapper>
|
|
107
|
+
<InjectIntl(TemplatePreview)
|
|
108
|
+
channel="sms"
|
|
109
|
+
charCounterEnabled={true}
|
|
110
|
+
content="Fallback SMS content"
|
|
111
|
+
showCount={true}
|
|
112
|
+
templateData={
|
|
113
|
+
Object {
|
|
114
|
+
"type": "SMS",
|
|
115
|
+
"versions": Object {
|
|
116
|
+
"base": Object {
|
|
117
|
+
"sms-editor": "Fallback SMS content",
|
|
118
|
+
"updated-sms-editor": Array [
|
|
119
|
+
"Line 1",
|
|
120
|
+
"Line 2",
|
|
121
|
+
"Line 3",
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
viberBrandName=""
|
|
128
|
+
/>
|
|
129
|
+
</SlideBoxContent__CreativesWrapper>
|
|
130
|
+
`;
|
|
131
|
+
|
|
132
|
+
exports[`Test SlideBoxContent container Edge Cases for Uncovered Lines Should handle default case in getChannelPreviewContent 1`] = `<SlideBoxContent__CreativesWrapper />`;
|
|
133
|
+
|
|
134
|
+
exports[`Test SlideBoxContent container Edge Cases for Uncovered Lines Should handle getLineType with LINE channel and empty templateData 1`] = `<SlideBoxContent__CreativesWrapper />`;
|
|
135
|
+
|
|
3
136
|
exports[`Test SlideBoxContent container Email component isTestAndPreviewMode IIFE Should handle isTestAndPreviewMode IIFE in Email edit mode with ID 1`] = `
|
|
4
137
|
<SlideBoxContent__CreativesWrapper>
|
|
5
138
|
<ForwardRef
|
|
@@ -202,6 +335,18 @@ exports[`Test SlideBoxContent container Email component isTestAndPreviewMode IIF
|
|
|
202
335
|
</SlideBoxContent__CreativesWrapper>
|
|
203
336
|
`;
|
|
204
337
|
|
|
338
|
+
exports[`Test SlideBoxContent container Mobile Push with Loyalty Module Should render MobilePushNew for loyalty module in full mode (create) 1`] = `<SlideBoxContent__CreativesWrapper />`;
|
|
339
|
+
|
|
340
|
+
exports[`Test SlideBoxContent container Mobile Push with Loyalty Module Should render MobilePushNew for loyalty module in full mode (edit) 1`] = `<SlideBoxContent__CreativesWrapper />`;
|
|
341
|
+
|
|
342
|
+
exports[`Test SlideBoxContent container Mobile Push with Loyalty Module Should render MobilePushNew for non-loyalty module in library mode (create) 1`] = `<SlideBoxContent__CreativesWrapper />`;
|
|
343
|
+
|
|
344
|
+
exports[`Test SlideBoxContent container Mobile Push with Loyalty Module Should render MobilePushNew for non-loyalty module in library mode (edit) 1`] = `<SlideBoxContent__CreativesWrapper />`;
|
|
345
|
+
|
|
346
|
+
exports[`Test SlideBoxContent container Mobile Push with Loyalty Module Should render MobilepushWrapper for loyalty module in library mode (create) 1`] = `<SlideBoxContent__CreativesWrapper />`;
|
|
347
|
+
|
|
348
|
+
exports[`Test SlideBoxContent container Mobile Push with Loyalty Module Should render MobliPushEdit for loyalty module in library mode (edit) 1`] = `<SlideBoxContent__CreativesWrapper />`;
|
|
349
|
+
|
|
205
350
|
exports[`Test SlideBoxContent container Should handle isTestAndPreviewMode IIFE implementation correctly 1`] = `
|
|
206
351
|
<SlideBoxContent__CreativesWrapper>
|
|
207
352
|
<ForwardRef
|
|
@@ -2516,6 +2516,9 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
|
|
|
2516
2516
|
});
|
|
2517
2517
|
obj.versions.history.push(newdata);
|
|
2518
2518
|
}
|
|
2519
|
+
if (index === "template-subject") {
|
|
2520
|
+
obj.versions.history[0].subject = newdata;
|
|
2521
|
+
}
|
|
2519
2522
|
});
|
|
2520
2523
|
//const data = formData[`${this.state.currentTab - 1}`];
|
|
2521
2524
|
obj.name = formData['template-name'];
|
|
@@ -21,6 +21,7 @@ import { intlShape } from "react-intl";
|
|
|
21
21
|
import "./index.scss";
|
|
22
22
|
import { GA } from "@capillarytech/cap-ui-utils";
|
|
23
23
|
import CapNotification from "@capillarytech/cap-ui-library/CapNotification";
|
|
24
|
+
import { CapModal } from "@capillarytech/cap-react-ui-library";
|
|
24
25
|
import { DAEMON } from "@capillarytech/vulcan-react-sdk/utils/sagaInjectorTypes";
|
|
25
26
|
import globalMessages from "../Cap/messages";
|
|
26
27
|
import * as actions from "./actions";
|
|
@@ -529,6 +530,9 @@ const MobilePushNew = ({
|
|
|
529
530
|
STANDARD_ERROR_MSG: {},
|
|
530
531
|
LIQUID_ERROR_MSG: {},
|
|
531
532
|
});
|
|
533
|
+
// Modal state for single platform confirmation
|
|
534
|
+
const [showModal, setShowModal] = useState(false);
|
|
535
|
+
const [modalContent, setModalContent] = useState({});
|
|
532
536
|
const [androidContent, setAndroidContent] = useState(INITIAL_CONTENT);
|
|
533
537
|
|
|
534
538
|
const [iosContent, setIosContent] = useState(INITIAL_CONTENT);
|
|
@@ -2130,39 +2134,93 @@ const MobilePushNew = ({
|
|
|
2130
2134
|
return panes;
|
|
2131
2135
|
}, [isAndroidSupported, isIosSupported, renderContentFields, formatMessage]);
|
|
2132
2136
|
|
|
2133
|
-
// Save button disabled logic:
|
|
2137
|
+
// Save button disabled logic: require at least one platform to have data
|
|
2138
|
+
const hasAndroidData = isAndroidSupported && androidContent?.title?.trim() && androidContent?.message?.trim();
|
|
2139
|
+
const hasIosData = isIosSupported && iosContent?.title?.trim() && iosContent?.message?.trim();
|
|
2140
|
+
const hasAnyPlatformData = hasAndroidData || hasIosData;
|
|
2141
|
+
|
|
2142
|
+
// Validation checks for save button
|
|
2143
|
+
const carouselErrors = Object.values(carouselLinkErrors).some((error) => error !== null && error !== "");
|
|
2144
|
+
const carouselValid = isCarouselDataValid();
|
|
2145
|
+
|
|
2134
2146
|
const isSaveDisabled = (
|
|
2135
|
-
|
|
2136
|
-
|| (
|
|
2137
|
-
||
|
|
2138
|
-
||
|
|
2139
|
-
|| !isCarouselDataValid()
|
|
2147
|
+
!hasAnyPlatformData // At least one supported platform must have data
|
|
2148
|
+
|| (isFullMode && (!templateName || !templateName.trim())) // Template name required in full mode
|
|
2149
|
+
|| carouselErrors
|
|
2150
|
+
|| !carouselValid
|
|
2140
2151
|
);
|
|
2141
2152
|
|
|
2142
|
-
//
|
|
2143
|
-
const
|
|
2144
|
-
if (
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2153
|
+
// Modal handler functions
|
|
2154
|
+
const setModalContentHandler = useCallback((type) => {
|
|
2155
|
+
if (type === 'ios') {
|
|
2156
|
+
const content = {
|
|
2157
|
+
title: formatMessage(messages.alertMessage),
|
|
2158
|
+
body: formatMessage(messages.iosTemplateNotConfigured),
|
|
2159
|
+
type: 'confirm',
|
|
2160
|
+
id: 'ios',
|
|
2161
|
+
};
|
|
2162
|
+
setModalContent(content);
|
|
2163
|
+
setShowModal(true);
|
|
2164
|
+
} else if (type === 'android') {
|
|
2165
|
+
const content = {
|
|
2166
|
+
title: formatMessage(messages.alertMessage),
|
|
2167
|
+
body: formatMessage(messages.androidTemplateNotConfigured),
|
|
2168
|
+
type: 'confirm',
|
|
2169
|
+
id: 'android',
|
|
2170
|
+
};
|
|
2171
|
+
setModalContent(content);
|
|
2172
|
+
setShowModal(true);
|
|
2150
2173
|
}
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2174
|
+
}, [formatMessage]);
|
|
2175
|
+
|
|
2176
|
+
const handleCancelModal = useCallback(() => {
|
|
2177
|
+
setShowModal(false);
|
|
2178
|
+
setModalContent({});
|
|
2179
|
+
}, []);
|
|
2180
|
+
|
|
2181
|
+
// Internal save function with optional modal validation skip
|
|
2182
|
+
const handleSaveInternal = useCallback((skipModalValidation = false) => {
|
|
2183
|
+
// Check for single platform data and show modal confirmation (unless skipped)
|
|
2184
|
+
if (!skipModalValidation) {
|
|
2185
|
+
const androidHasData = androidContent?.title?.trim() && androidContent?.message?.trim();
|
|
2186
|
+
const iosHasData = iosContent?.title?.trim() && iosContent?.message?.trim();
|
|
2187
|
+
|
|
2188
|
+
// If both platforms are supported but only one has data, show confirmation modal
|
|
2189
|
+
if (isAndroidSupported && isIosSupported) {
|
|
2190
|
+
if (androidHasData && !iosHasData) {
|
|
2191
|
+
setModalContentHandler('ios'); // Show iOS not configured modal
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2194
|
+
if (iosHasData && !androidHasData) {
|
|
2195
|
+
setModalContentHandler('android'); // Show Android not configured modal
|
|
2196
|
+
return;
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2157
2199
|
}
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2200
|
+
|
|
2201
|
+
// Only validate platforms that have data or are required
|
|
2202
|
+
// If user confirmed via modal, we allow single platform data
|
|
2203
|
+
if (!skipModalValidation) {
|
|
2204
|
+
// In normal flow, require data for all supported platforms
|
|
2205
|
+
if (isAndroidSupported && (!androidContent?.title?.trim() || !androidContent?.message?.trim())) {
|
|
2206
|
+
CapNotification.error({
|
|
2207
|
+
message: formatMessage(messages.androidValidationError),
|
|
2208
|
+
});
|
|
2209
|
+
if (onValidationFail) onValidationFail();
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
if (isIosSupported && (!iosContent?.title?.trim() || !iosContent?.message?.trim())) {
|
|
2213
|
+
CapNotification.error({
|
|
2214
|
+
message: formatMessage(messages.iosValidationError),
|
|
2215
|
+
});
|
|
2216
|
+
if (onValidationFail) onValidationFail();
|
|
2217
|
+
return;
|
|
2218
|
+
}
|
|
2163
2219
|
}
|
|
2164
|
-
//
|
|
2165
|
-
//
|
|
2220
|
+
// Note: In modal-confirmed flow (skipModalValidation = true), we skip platform validation
|
|
2221
|
+
// since user has already confirmed they want to save with incomplete platform data
|
|
2222
|
+
|
|
2223
|
+
// Template name validation - only required in full mode
|
|
2166
2224
|
const currentTemplateName = templateNameRef.current || templateName;
|
|
2167
2225
|
if (isFullMode && !currentTemplateName?.trim()) {
|
|
2168
2226
|
CapNotification.error({
|
|
@@ -2327,6 +2385,8 @@ const MobilePushNew = ({
|
|
|
2327
2385
|
}
|
|
2328
2386
|
|
|
2329
2387
|
// Create payload with enabled platform content and intl
|
|
2388
|
+
// In modal-confirmed flow, we may have intentionally missing platform data
|
|
2389
|
+
const allowSinglePlatform = skipModalValidation;
|
|
2330
2390
|
const payload = createMobilePushPayloadWithIntl({
|
|
2331
2391
|
templateName: finalTemplateName,
|
|
2332
2392
|
androidContent: isAndroidSupported ? processedAndroidContent : undefined,
|
|
@@ -2337,6 +2397,7 @@ const MobilePushNew = ({
|
|
|
2337
2397
|
sameContent,
|
|
2338
2398
|
options: {
|
|
2339
2399
|
mode: params?.mode,
|
|
2400
|
+
allowSinglePlatform,
|
|
2340
2401
|
},
|
|
2341
2402
|
intl,
|
|
2342
2403
|
});
|
|
@@ -2516,6 +2577,68 @@ const MobilePushNew = ({
|
|
|
2516
2577
|
isCarouselDataValid,
|
|
2517
2578
|
isFullMode,
|
|
2518
2579
|
templateId,
|
|
2580
|
+
activeTab,
|
|
2581
|
+
setModalContentHandler,
|
|
2582
|
+
]);
|
|
2583
|
+
|
|
2584
|
+
// Public save function (with modal validation)
|
|
2585
|
+
const handleSave = useCallback(() => {
|
|
2586
|
+
handleSaveInternal(false);
|
|
2587
|
+
}, [handleSaveInternal]);
|
|
2588
|
+
|
|
2589
|
+
// Save function that skips single platform modal validation
|
|
2590
|
+
const handleSaveWithoutModal = useCallback(() => {
|
|
2591
|
+
handleSaveInternal(true);
|
|
2592
|
+
}, [handleSaveInternal]);
|
|
2593
|
+
|
|
2594
|
+
const handleConfirmModal = useCallback(() => {
|
|
2595
|
+
setShowModal(false);
|
|
2596
|
+
setModalContent({});
|
|
2597
|
+
// Proceed with save after modal confirmation - skip single platform validation
|
|
2598
|
+
handleSaveWithoutModal();
|
|
2599
|
+
}, [handleSaveWithoutModal]);
|
|
2600
|
+
|
|
2601
|
+
const liquidMiddleWare = useCallback(() => {
|
|
2602
|
+
const onError = ({ standardErrors, liquidErrors }) => {
|
|
2603
|
+
setErrorMessage((prev) => ({
|
|
2604
|
+
STANDARD_ERROR_MSG: { ...prev.STANDARD_ERROR_MSG, ...standardErrors },
|
|
2605
|
+
LIQUID_ERROR_MSG: { ...prev.LIQUID_ERROR_MSG, ...liquidErrors },
|
|
2606
|
+
}));
|
|
2607
|
+
};
|
|
2608
|
+
const onSuccess = () => handleSave();
|
|
2609
|
+
|
|
2610
|
+
validateMobilePushContent([androidContent, iosContent], {
|
|
2611
|
+
currentTab: activeTab === ANDROID ? 1 : 2,
|
|
2612
|
+
onError,
|
|
2613
|
+
onSuccess,
|
|
2614
|
+
getLiquidTags: globalActionsProps.getLiquidTags,
|
|
2615
|
+
formatMessage,
|
|
2616
|
+
messages: formBuilderMessages,
|
|
2617
|
+
tagLookupMap: metaEntities?.tagLookupMap || {},
|
|
2618
|
+
eventContextTags: metaEntities?.eventContextTags || [],
|
|
2619
|
+
isLiquidFlow: hasLiquidSupportFeature(),
|
|
2620
|
+
forwardedTags: {},
|
|
2621
|
+
skipTags: (tag) => {
|
|
2622
|
+
const skipRegexes = [
|
|
2623
|
+
/dynamic_expiry_date_after_\d+_days\.FORMAT_\d/,
|
|
2624
|
+
/unsubscribe\(#[a-zA-Z\d]{6}\)/,
|
|
2625
|
+
/Link_to_[a-zA-z]/,
|
|
2626
|
+
/SURVEY.*\.TOKEN/,
|
|
2627
|
+
/^[A-Za-z].*\([a-zA-Z\d]*\)/,
|
|
2628
|
+
];
|
|
2629
|
+
return skipRegexes.some((regex) => regex.test(tag));
|
|
2630
|
+
},
|
|
2631
|
+
singleTab: getSingleTab(accountData),
|
|
2632
|
+
});
|
|
2633
|
+
}, [
|
|
2634
|
+
androidContent,
|
|
2635
|
+
iosContent,
|
|
2636
|
+
activeTab,
|
|
2637
|
+
globalActionsProps,
|
|
2638
|
+
formatMessage,
|
|
2639
|
+
metaEntities,
|
|
2640
|
+
accountData,
|
|
2641
|
+
handleSave,
|
|
2519
2642
|
]);
|
|
2520
2643
|
|
|
2521
2644
|
// Helper to sync content between platforms
|
|
@@ -2547,7 +2670,9 @@ const MobilePushNew = ({
|
|
|
2547
2670
|
setTemplateName(value);
|
|
2548
2671
|
// Update ref to always have the latest value
|
|
2549
2672
|
templateNameRef.current = value;
|
|
2550
|
-
|
|
2673
|
+
// Only set error if user has interacted and field is empty
|
|
2674
|
+
// In full mode, template name is required only at save time, not during typing
|
|
2675
|
+
const isInvalid = isFullMode && value.trim() === "";
|
|
2551
2676
|
setTemplateNameError(isInvalid);
|
|
2552
2677
|
if (value && onEnterTemplateName) {
|
|
2553
2678
|
onEnterTemplateName();
|
|
@@ -2555,7 +2680,7 @@ const MobilePushNew = ({
|
|
|
2555
2680
|
onRemoveTemplateName();
|
|
2556
2681
|
}
|
|
2557
2682
|
},
|
|
2558
|
-
[onEnterTemplateName, onRemoveTemplateName]
|
|
2683
|
+
[onEnterTemplateName, onRemoveTemplateName, isFullMode]
|
|
2559
2684
|
);
|
|
2560
2685
|
|
|
2561
2686
|
// --- Only show template name input in full mode (not library/consumer mode) ---
|
|
@@ -2584,47 +2709,6 @@ const MobilePushNew = ({
|
|
|
2584
2709
|
[isFullMode, formatMessage, templateName, onTemplateNameChange, templateNameError, params?.id]
|
|
2585
2710
|
);
|
|
2586
2711
|
|
|
2587
|
-
const liquidMiddleWare = useCallback(() => {
|
|
2588
|
-
const onError = ({ standardErrors, liquidErrors }) => {
|
|
2589
|
-
setErrorMessage((prev) => ({
|
|
2590
|
-
STANDARD_ERROR_MSG: { ...prev.STANDARD_ERROR_MSG, ...standardErrors },
|
|
2591
|
-
LIQUID_ERROR_MSG: { ...prev.LIQUID_ERROR_MSG, ...liquidErrors },
|
|
2592
|
-
}));
|
|
2593
|
-
};
|
|
2594
|
-
const onSuccess = () => handleSave();
|
|
2595
|
-
|
|
2596
|
-
validateMobilePushContent([androidContent, iosContent], {
|
|
2597
|
-
currentTab: activeTab === ANDROID ? 1 : 2,
|
|
2598
|
-
onError,
|
|
2599
|
-
onSuccess,
|
|
2600
|
-
getLiquidTags: globalActionsProps.getLiquidTags,
|
|
2601
|
-
formatMessage,
|
|
2602
|
-
messages: formBuilderMessages,
|
|
2603
|
-
tagLookupMap: metaEntities?.tagLookupMap || {},
|
|
2604
|
-
eventContextTags: metaEntities?.eventContextTags || [],
|
|
2605
|
-
isLiquidFlow: hasLiquidSupportFeature(),
|
|
2606
|
-
forwardedTags: {},
|
|
2607
|
-
skipTags: (tag) => {
|
|
2608
|
-
const skipRegexes = [
|
|
2609
|
-
/dynamic_expiry_date_after_\d+_days\.FORMAT_\d/,
|
|
2610
|
-
/unsubscribe\(#[a-zA-Z\d]{6}\)/,
|
|
2611
|
-
/Link_to_[a-zA-z]/,
|
|
2612
|
-
/SURVEY.*\.TOKEN/,
|
|
2613
|
-
/^[A-Za-z].*\([a-zA-Z\d]*\)/,
|
|
2614
|
-
];
|
|
2615
|
-
return skipRegexes.some((regex) => regex.test(tag));
|
|
2616
|
-
},
|
|
2617
|
-
singleTab: getSingleTab(accountData),
|
|
2618
|
-
});
|
|
2619
|
-
}, [
|
|
2620
|
-
androidContent,
|
|
2621
|
-
iosContent,
|
|
2622
|
-
activeTab,
|
|
2623
|
-
globalActionsProps,
|
|
2624
|
-
formatMessage,
|
|
2625
|
-
metaEntities,
|
|
2626
|
-
accountData,
|
|
2627
|
-
]);
|
|
2628
2712
|
|
|
2629
2713
|
const isLiquidFlow = hasLiquidSupportFeature();
|
|
2630
2714
|
|
|
@@ -2945,6 +3029,24 @@ const MobilePushNew = ({
|
|
|
2945
3029
|
/>
|
|
2946
3030
|
</CapColumn>
|
|
2947
3031
|
</CapRow>
|
|
3032
|
+
|
|
3033
|
+
{/* Modal for single platform confirmation */}
|
|
3034
|
+
<CapModal
|
|
3035
|
+
visible={showModal}
|
|
3036
|
+
title={modalContent.title}
|
|
3037
|
+
onOk={handleConfirmModal}
|
|
3038
|
+
onCancel={handleCancelModal}
|
|
3039
|
+
footer={[
|
|
3040
|
+
<CapButton key="cancel" onClick={handleCancelModal}>
|
|
3041
|
+
No
|
|
3042
|
+
</CapButton>,
|
|
3043
|
+
<CapButton key="confirm" type="primary" onClick={handleConfirmModal}>
|
|
3044
|
+
Yes
|
|
3045
|
+
</CapButton>,
|
|
3046
|
+
]}
|
|
3047
|
+
>
|
|
3048
|
+
{modalContent.body}
|
|
3049
|
+
</CapModal>
|
|
2948
3050
|
</CapSpin>
|
|
2949
3051
|
);
|
|
2950
3052
|
};
|
|
@@ -252,6 +252,10 @@ export default defineMessages({
|
|
|
252
252
|
id: `${scope}.contentValidationError`,
|
|
253
253
|
defaultMessage: '{platform} content must have title and message',
|
|
254
254
|
},
|
|
255
|
+
singlePlatformContentMissing: {
|
|
256
|
+
id: `${scope}.singlePlatformContentMissing`,
|
|
257
|
+
defaultMessage: 'At least one platform must have title and message',
|
|
258
|
+
},
|
|
255
259
|
// File validation error messages for useUpload.js
|
|
256
260
|
fileSizeError: {
|
|
257
261
|
id: `${scope}.fileSizeError`,
|
|
@@ -269,4 +273,17 @@ export default defineMessages({
|
|
|
269
273
|
id: `${scope}.gifFileTypeError`,
|
|
270
274
|
defaultMessage: 'Only GIF files are allowed',
|
|
271
275
|
},
|
|
276
|
+
// Modal confirmation messages for single platform data
|
|
277
|
+
alertMessage: {
|
|
278
|
+
id: `${scope}.alertMessage`,
|
|
279
|
+
defaultMessage: 'Alert',
|
|
280
|
+
},
|
|
281
|
+
androidTemplateNotConfigured: {
|
|
282
|
+
id: `${scope}.androidTemplateNotConfigured`,
|
|
283
|
+
defaultMessage: 'Android template is not configured. Continue save?',
|
|
284
|
+
},
|
|
285
|
+
iosTemplateNotConfigured: {
|
|
286
|
+
id: `${scope}.iosTemplateNotConfigured`,
|
|
287
|
+
defaultMessage: 'IOS template is not configured, Save without IOS template',
|
|
288
|
+
},
|
|
272
289
|
});
|
|
@@ -1357,19 +1357,16 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
1357
1357
|
break;
|
|
1358
1358
|
}
|
|
1359
1359
|
case MOBILE_PUSH:
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
const { title, message, expandableDetails: { style = '', image, carouselData = [], ctas = [], media = [] } = {} } = mpushListingData || {};
|
|
1371
|
-
const {url = ''} = media?.[0] || {};
|
|
1372
|
-
templateData.content = (
|
|
1360
|
+
const mpushData = get(template, 'versions.base', template);
|
|
1361
|
+
const androidData = get(mpushData, 'ANDROID') || get(mpushData, 'androidContent');
|
|
1362
|
+
const iosData = get(mpushData, 'IOS') || get(mpushData, 'iosContent');
|
|
1363
|
+
let mpushListingData = androidData;
|
|
1364
|
+
if (isEmpty(androidData) || !androidData?.title) {
|
|
1365
|
+
mpushListingData = iosData;
|
|
1366
|
+
};
|
|
1367
|
+
const { title, message, expandableDetails: { style = '', image, carouselData = [], ctas = [], media = [] } = {} } = mpushListingData || {};
|
|
1368
|
+
const {url = ''} = media?.[0] || {};
|
|
1369
|
+
templateData.content = (
|
|
1373
1370
|
<div className='mobilepush-container'>
|
|
1374
1371
|
<div className="app-header">
|
|
1375
1372
|
<div className="app-header-left">
|
|
@@ -1441,8 +1438,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
1441
1438
|
)}
|
|
1442
1439
|
</div>
|
|
1443
1440
|
);
|
|
1444
|
-
|
|
1445
|
-
}
|
|
1441
|
+
templateData.isNewMobilePush = true;
|
|
1446
1442
|
break;
|
|
1447
1443
|
case INAPP:
|
|
1448
1444
|
templateData.content = template;
|