@capillarytech/creatives-library 8.0.236-alpha.5 → 8.0.236-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import React, {
|
|
16
|
-
useRef, useCallback, useMemo, useState,
|
|
16
|
+
useRef, useCallback, useMemo, useState, useEffect,
|
|
17
17
|
} from 'react';
|
|
18
18
|
import PropTypes from 'prop-types';
|
|
19
19
|
import { injectIntl, intlShape } from 'react-intl';
|
|
@@ -38,6 +38,7 @@ import { useEditorContent } from './hooks/useEditorContent';
|
|
|
38
38
|
import { useInAppContent } from './hooks/useInAppContent';
|
|
39
39
|
import { useLayoutState } from './hooks/useLayoutState';
|
|
40
40
|
import { useValidation } from './hooks/useValidation';
|
|
41
|
+
import { transformValidationToErrorInfo } from './utils/validationAdapter';
|
|
41
42
|
|
|
42
43
|
// Constants
|
|
43
44
|
import {
|
|
@@ -71,6 +72,7 @@ const HTMLEditor = ({
|
|
|
71
72
|
onTagSelect = null,
|
|
72
73
|
onContextChange = null,
|
|
73
74
|
globalActions = null, // Redux actions for API calls
|
|
75
|
+
onValidationChange = null, // Callback to propagate validation errors to parent
|
|
74
76
|
...props
|
|
75
77
|
}) => {
|
|
76
78
|
// Separate refs for main and modal editors to avoid conflicts
|
|
@@ -275,7 +277,9 @@ const HTMLEditor = ({
|
|
|
275
277
|
editor.insertText(label, cursor);
|
|
276
278
|
|
|
277
279
|
// Focus the editor if focus method is available
|
|
278
|
-
editor
|
|
280
|
+
if (editor && typeof editor.focus === 'function') {
|
|
281
|
+
editor.focus();
|
|
282
|
+
}
|
|
279
283
|
|
|
280
284
|
// Show success notification
|
|
281
285
|
CapNotification.success({
|
|
@@ -305,13 +309,15 @@ const HTMLEditor = ({
|
|
|
305
309
|
const handleSave = useCallback(() => {
|
|
306
310
|
try {
|
|
307
311
|
const { html, css, javascript } = content?.content || {};
|
|
308
|
-
const
|
|
312
|
+
const contentToSave = { html, css, javascript };
|
|
309
313
|
|
|
310
314
|
if (onSave) {
|
|
311
|
-
onSave(
|
|
315
|
+
onSave(contentToSave);
|
|
312
316
|
}
|
|
313
317
|
|
|
314
|
-
markAsSaved
|
|
318
|
+
if (markAsSaved) {
|
|
319
|
+
markAsSaved();
|
|
320
|
+
}
|
|
315
321
|
|
|
316
322
|
CapNotification.success({
|
|
317
323
|
message: intl.formatMessage(messages.contentSaved),
|
|
@@ -337,13 +343,15 @@ const HTMLEditor = ({
|
|
|
337
343
|
// Access the CodeMirror view through the exposed ref methods
|
|
338
344
|
if (editorInstance?.navigateToLine) {
|
|
339
345
|
editorInstance.navigateToLine(line, column);
|
|
340
|
-
} else {
|
|
341
|
-
editorInstance
|
|
346
|
+
} else if (editorInstance && typeof editorInstance.focus === 'function') {
|
|
347
|
+
editorInstance.focus();
|
|
342
348
|
// Fallback: just focus the editor if navigation isn't available
|
|
343
349
|
}
|
|
344
350
|
} catch (err) {
|
|
345
351
|
// Fallback: just focus the editor
|
|
346
|
-
editorInstance
|
|
352
|
+
if (editorInstance && typeof editorInstance.focus === 'function') {
|
|
353
|
+
editorInstance.focus();
|
|
354
|
+
}
|
|
347
355
|
}
|
|
348
356
|
}
|
|
349
357
|
}, [getActiveEditorRef]);
|
|
@@ -357,6 +365,63 @@ const HTMLEditor = ({
|
|
|
357
365
|
setIsFullscreenModalOpen(false);
|
|
358
366
|
}, []);
|
|
359
367
|
|
|
368
|
+
// Propagate validation errors to parent component
|
|
369
|
+
// Use refs to track previous values and prevent infinite loops
|
|
370
|
+
const lastValidationErrorsRef = useRef(null);
|
|
371
|
+
const lastActiveDeviceRef = useRef(variant === HTML_EDITOR_VARIANTS.INAPP ? content?.activeDevice : null);
|
|
372
|
+
|
|
373
|
+
useEffect(() => {
|
|
374
|
+
if (onValidationChange && validation && !validation.isValidating) {
|
|
375
|
+
const errorData = transformValidationToErrorInfo(validation, variant);
|
|
376
|
+
|
|
377
|
+
// Create error messages object
|
|
378
|
+
let errorMessages;
|
|
379
|
+
if (variant === HTML_EDITOR_VARIANTS.INAPP && content?.activeDevice) {
|
|
380
|
+
const currentActiveDevice = content.activeDevice;
|
|
381
|
+
const deviceKey = currentActiveDevice === 'android' ? 'ANDROID' : 'IOS';
|
|
382
|
+
errorMessages = {
|
|
383
|
+
STANDARD_ERROR_MSG: {
|
|
384
|
+
[deviceKey]: errorData.errorMessages.STANDARD_ERROR_MSG || [],
|
|
385
|
+
ANDROID: currentActiveDevice === 'android' ? (errorData.errorMessages.STANDARD_ERROR_MSG || []) : [],
|
|
386
|
+
IOS: currentActiveDevice === 'ios' ? (errorData.errorMessages.STANDARD_ERROR_MSG || []) : [],
|
|
387
|
+
GENERIC: [],
|
|
388
|
+
},
|
|
389
|
+
LIQUID_ERROR_MSG: {
|
|
390
|
+
[deviceKey]: errorData.errorMessages.LIQUID_ERROR_MSG || [],
|
|
391
|
+
ANDROID: currentActiveDevice === 'android' ? (errorData.errorMessages.LIQUID_ERROR_MSG || []) : [],
|
|
392
|
+
IOS: currentActiveDevice === 'ios' ? (errorData.errorMessages.LIQUID_ERROR_MSG || []) : [],
|
|
393
|
+
GENERIC: [],
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
} else {
|
|
397
|
+
// For Email variant or when no active device, use generic format
|
|
398
|
+
errorMessages = {
|
|
399
|
+
STANDARD_ERROR_MSG: {
|
|
400
|
+
ANDROID: [],
|
|
401
|
+
IOS: [],
|
|
402
|
+
GENERIC: errorData.errorMessages.STANDARD_ERROR_MSG || [],
|
|
403
|
+
},
|
|
404
|
+
LIQUID_ERROR_MSG: {
|
|
405
|
+
ANDROID: [],
|
|
406
|
+
IOS: [],
|
|
407
|
+
GENERIC: errorData.errorMessages.LIQUID_ERROR_MSG || [],
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Only call onValidationChange if errors actually changed
|
|
413
|
+
const errorsString = JSON.stringify(errorMessages);
|
|
414
|
+
const deviceForComparison = variant === HTML_EDITOR_VARIANTS.INAPP ? content?.activeDevice : null;
|
|
415
|
+
const deviceChanged = lastActiveDeviceRef.current !== deviceForComparison;
|
|
416
|
+
|
|
417
|
+
if (lastValidationErrorsRef.current !== errorsString || deviceChanged) {
|
|
418
|
+
lastValidationErrorsRef.current = errorsString;
|
|
419
|
+
lastActiveDeviceRef.current = deviceForComparison;
|
|
420
|
+
onValidationChange(errorMessages);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}, [validation?.summary?.totalErrors, validation?.summary?.totalWarnings, validation?.isValidating, variant, content?.activeDevice, onValidationChange]);
|
|
424
|
+
|
|
360
425
|
// Context value for providers
|
|
361
426
|
const contextValue = useMemo(() => ({
|
|
362
427
|
variant,
|
|
@@ -566,6 +631,7 @@ HTMLEditor.propTypes = {
|
|
|
566
631
|
onTagSelect: PropTypes.func,
|
|
567
632
|
onContextChange: PropTypes.func, // Deprecated: use globalActions instead
|
|
568
633
|
globalActions: PropTypes.object, // Redux actions for API calls
|
|
634
|
+
onValidationChange: PropTypes.func, // Callback to propagate validation errors to parent
|
|
569
635
|
};
|
|
570
636
|
|
|
571
637
|
HTMLEditor.defaultProps = {
|
|
@@ -587,6 +653,7 @@ HTMLEditor.defaultProps = {
|
|
|
587
653
|
onTagSelect: null,
|
|
588
654
|
onContextChange: null,
|
|
589
655
|
globalActions: null,
|
|
656
|
+
onValidationChange: null,
|
|
590
657
|
};
|
|
591
658
|
|
|
592
659
|
// Export with forwardRef to allow direct access to CodeEditorPane via ref
|
|
@@ -54,16 +54,31 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
|
|
|
54
54
|
const { ANDROID, IOS } = DEVICE_TYPES;
|
|
55
55
|
|
|
56
56
|
// Device-specific content state with optional chaining
|
|
57
|
+
// FIX: If iOS content is empty but Android has content, use Android content for iOS
|
|
58
|
+
// This handles the case where "keepContentSame" was used during save
|
|
59
|
+
const initialAndroidContent = initialContent?.[ANDROID] || DEFAULT_INAPP_CONTENT[ANDROID];
|
|
60
|
+
const initialIosContent = initialContent?.[IOS] || DEFAULT_INAPP_CONTENT[IOS];
|
|
61
|
+
const iosContentToUse = (!initialIosContent || initialIosContent.trim() === '') && initialAndroidContent && initialAndroidContent.trim() !== ''
|
|
62
|
+
? initialAndroidContent
|
|
63
|
+
: initialIosContent;
|
|
64
|
+
|
|
65
|
+
// Check if both devices have the same content initially
|
|
66
|
+
const initialHasSameContent = initialAndroidContent && iosContentToUse
|
|
67
|
+
&& initialAndroidContent.trim() !== ''
|
|
68
|
+
&& iosContentToUse.trim() !== ''
|
|
69
|
+
&& initialAndroidContent.trim() === iosContentToUse.trim();
|
|
70
|
+
|
|
57
71
|
const [deviceContent, setDeviceContent] = useState(() => ({
|
|
58
|
-
[ANDROID]:
|
|
59
|
-
[IOS]:
|
|
72
|
+
[ANDROID]: initialAndroidContent,
|
|
73
|
+
[IOS]: iosContentToUse,
|
|
60
74
|
}));
|
|
61
75
|
|
|
62
76
|
// Current active device
|
|
63
77
|
const [activeDevice, setActiveDevice] = useState(ANDROID);
|
|
64
78
|
|
|
65
79
|
// Content sync state
|
|
66
|
-
|
|
80
|
+
// FIX: Initialize keepContentSame to true if both devices have the same content initially
|
|
81
|
+
const [keepContentSame, setKeepContentSame] = useState(initialHasSameContent);
|
|
67
82
|
|
|
68
83
|
// Dirty state tracking
|
|
69
84
|
const [isDirty, setIsDirty] = useState(false);
|
|
@@ -105,6 +120,13 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
|
|
|
105
120
|
&& (!previousContentRef.current.ios || previousContentRef.current.ios.trim() === '');
|
|
106
121
|
const isTransitioningToContent = wasEmpty && hasMeaningfulContent;
|
|
107
122
|
|
|
123
|
+
// FIX: Check if both devices have the same content (indicates "keepContentSame" was used)
|
|
124
|
+
// If they do, automatically enable content sync
|
|
125
|
+
const hasSameContent = newAndroidContent && newIosContent
|
|
126
|
+
&& newAndroidContent.trim() !== ''
|
|
127
|
+
&& newIosContent.trim() !== ''
|
|
128
|
+
&& newAndroidContent.trim() === newIosContent.trim();
|
|
129
|
+
|
|
108
130
|
// Only update if:
|
|
109
131
|
// 1. We haven't loaded initial content yet (first load), OR
|
|
110
132
|
// 2. We're transitioning from empty to meaningful content (library mode data fetch)
|
|
@@ -131,6 +153,24 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
|
|
|
131
153
|
return updated ? updatedContent : prev;
|
|
132
154
|
});
|
|
133
155
|
|
|
156
|
+
// FIX: If both devices have the same content, enable content sync
|
|
157
|
+
// This ensures the "keepContentSame" checkbox is checked when loading templates
|
|
158
|
+
// that were saved with the same content for both devices
|
|
159
|
+
// Also check if iOS is empty but Android has content (same content scenario)
|
|
160
|
+
const iosEmptyButAndroidHasContent = (!newIosContent || newIosContent.trim() === '')
|
|
161
|
+
&& newAndroidContent && newAndroidContent.trim() !== '';
|
|
162
|
+
|
|
163
|
+
if ((hasSameContent || iosEmptyButAndroidHasContent) && !keepContentSame) {
|
|
164
|
+
setKeepContentSame(true);
|
|
165
|
+
// If iOS is empty but Android has content, sync iOS to Android
|
|
166
|
+
if (iosEmptyButAndroidHasContent) {
|
|
167
|
+
setDeviceContent((prev) => ({
|
|
168
|
+
...prev,
|
|
169
|
+
[IOS]: newAndroidContent,
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
134
174
|
// Update previous content ref
|
|
135
175
|
previousContentRef.current = {
|
|
136
176
|
android: newAndroidContent || '',
|
|
@@ -138,7 +178,7 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
|
|
|
138
178
|
};
|
|
139
179
|
}
|
|
140
180
|
}
|
|
141
|
-
}, [initialContent, ANDROID, IOS]);
|
|
181
|
+
}, [initialContent, ANDROID, IOS, keepContentSame]);
|
|
142
182
|
|
|
143
183
|
// Get current active content
|
|
144
184
|
const currentContent = useMemo(() => deviceContent?.[activeDevice] || '', [deviceContent, activeDevice]);
|
|
@@ -208,8 +208,14 @@ export const InApp = (props) => {
|
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
210
|
//cleanup code
|
|
211
|
+
// FIX ISSUE #4: Reset edit flow state when component unmounts or id changes
|
|
212
|
+
// This ensures templates can be edited multiple times
|
|
211
213
|
return () => {
|
|
212
214
|
actions.resetEditTemplate();
|
|
215
|
+
setEditFlow(false);
|
|
216
|
+
// Reset content states to allow fresh edit
|
|
217
|
+
setHtmlContentAndroid('');
|
|
218
|
+
setHtmlContentIos('');
|
|
213
219
|
};
|
|
214
220
|
}, [paramObj.id]);
|
|
215
221
|
|
|
@@ -262,7 +268,10 @@ export const InApp = (props) => {
|
|
|
262
268
|
setEditFlow(true);
|
|
263
269
|
setTempName(name);
|
|
264
270
|
setTemplateDate(createdAt);
|
|
265
|
-
|
|
271
|
+
// FIX ISSUE #3: When changing layout type, check both Android and iOS bodyType
|
|
272
|
+
// Prefer Android bodyType, but fall back to iOS if Android is not available
|
|
273
|
+
const layoutType = editContent?.ANDROID?.bodyType || editContent?.IOS?.bodyType;
|
|
274
|
+
setTemplateLayoutType(layoutType);
|
|
266
275
|
// Call showTemplateName callback when in edit mode + full mode to show template name header
|
|
267
276
|
if (showTemplateName && name) {
|
|
268
277
|
showTemplateName({
|
|
@@ -288,7 +297,7 @@ export const InApp = (props) => {
|
|
|
288
297
|
title: androidTitle = '',
|
|
289
298
|
message: androidMessage = '',
|
|
290
299
|
type: androidType = '',
|
|
291
|
-
ctas: androidCta =
|
|
300
|
+
ctas: androidCta = null,
|
|
292
301
|
expandableDetails: androidExpandableDetails = {},
|
|
293
302
|
} = androidContent || {};
|
|
294
303
|
const {
|
|
@@ -315,6 +324,9 @@ export const InApp = (props) => {
|
|
|
315
324
|
// Initialize HTML content for HTMLEditor if feature is enabled and it's an HTML template
|
|
316
325
|
if (androidIsHTML) {
|
|
317
326
|
setHtmlContentAndroid(androidMessage);
|
|
327
|
+
// FIX: Also initialize iOS content with Android content if iOS is empty
|
|
328
|
+
// This handles the case where "keepContentSame" was used during save
|
|
329
|
+
// We'll check iOS content below, but pre-initialize here to ensure it's set
|
|
318
330
|
}
|
|
319
331
|
setTemplateMediaType(
|
|
320
332
|
androidIsHTML ? INAPP_MEDIA_TYPES.HTML
|
|
@@ -323,59 +335,82 @@ export const InApp = (props) => {
|
|
|
323
335
|
: INAPP_MEDIA_TYPES.IMAGE
|
|
324
336
|
);
|
|
325
337
|
setInAppImageSrcAndroid(androidImage);
|
|
326
|
-
setDeepLinkValueAndroid(androidCta[0]?.actionLink
|
|
338
|
+
setDeepLinkValueAndroid(androidCta && Array.isArray(androidCta) && androidCta[0]?.actionLink ? androidCta[0].actionLink : '');
|
|
327
339
|
setAddActionLinkAndroid(!isEmpty(androidCta) && true);
|
|
328
340
|
setButtonTypeAndroid(
|
|
329
341
|
androidCtaLength ? INAPP_BUTTON_TYPES.CTA : INAPP_BUTTON_TYPES.NONE
|
|
330
342
|
);
|
|
331
|
-
if (androidCtaLength > 0) {
|
|
343
|
+
if (androidCtaLength > 0 && androidCtas && Array.isArray(androidCtas)) {
|
|
332
344
|
setCtaDataAndroid(getCtaObject(androidCtas));
|
|
333
345
|
}
|
|
334
346
|
}
|
|
335
347
|
const iosContent = editContent?.IOS;
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
348
|
+
// FIX ISSUE #3: Check for iOS content with deviceType === IOS || IOS_CAPITAL
|
|
349
|
+
const iosDeviceType = get(iosContent, 'deviceType');
|
|
350
|
+
const isIosDevice = iosDeviceType === IOS || iosDeviceType === IOS_CAPITAL;
|
|
351
|
+
|
|
352
|
+
// FIX: Always process iOS content initialization, even if iosContent is empty
|
|
353
|
+
// This handles the case where "keepContentSame" was used and iOS content might be empty
|
|
354
|
+
const {
|
|
355
|
+
title: iosTitle = '',
|
|
356
|
+
message: iosMessage = '',
|
|
357
|
+
type: iosType = '',
|
|
358
|
+
ctas: iosCta = null,
|
|
359
|
+
expandableDetails: iosExpandableDetails = {},
|
|
360
|
+
} = iosContent || {};
|
|
361
|
+
const {
|
|
362
|
+
style: iosStyle,
|
|
363
|
+
image: iosImage,
|
|
364
|
+
ctas: iosCtas,
|
|
365
|
+
} = iosExpandableDetails || {};
|
|
366
|
+
const iosCtaLength = iosCtas?.length;
|
|
367
|
+
|
|
368
|
+
// FIX: If iOS content is empty but Android has content, use Android content for iOS
|
|
369
|
+
// This handles the case where "keepContentSame" was used during save
|
|
370
|
+
const androidMsg = androidContent?.message || '';
|
|
371
|
+
const iosMessageToUse = (iosMessage && iosMessage.trim() !== '') ? iosMessage : androidMsg;
|
|
372
|
+
|
|
373
|
+
// Check if this is an HTML template (if Android wasn't HTML, check iOS)
|
|
374
|
+
// Note: androidIsHTML is in the outer scope from the Android content check above
|
|
375
|
+
if (!androidIsHTML) {
|
|
376
|
+
// Check if this is a Bee editor template
|
|
377
|
+
const isBEEeditor = get(iosContent, 'isBEEeditor');
|
|
378
|
+
const isBeeFreeTemplate = !isEmpty(iosTitle) && iosTitle.toLowerCase() === 'bee free template';
|
|
379
|
+
// Check if this is an HTML editor template (identified by special title)
|
|
380
|
+
const isHTMLEditorTemplate = !isEmpty(iosTitle) && iosTitle.toLowerCase() === 'html editor template';
|
|
381
|
+
// Check if this is an HTML template
|
|
382
|
+
// Prioritize HTML editor template title, then check type/style
|
|
383
|
+
// But exclude if it's a Bee editor template
|
|
384
|
+
const iosIsHTML = isHTMLEditorTemplate || ((iosType === INAPP_MEDIA_TYPES.HTML || iosStyle === BIG_HTML)
|
|
385
|
+
&& !isBEEeditor
|
|
386
|
+
&& !isBeeFreeTemplate);
|
|
387
|
+
setIsHTMLTemplate(iosIsHTML);
|
|
388
|
+
// Initialize HTML content for HTMLEditor if feature is enabled and it's an HTML template
|
|
389
|
+
if (iosIsHTML) {
|
|
390
|
+
setHtmlContentIos(iosMessageToUse);
|
|
375
391
|
}
|
|
376
|
-
|
|
392
|
+
} else {
|
|
393
|
+
// If Android is HTML, also initialize iOS HTML content
|
|
394
|
+
if (androidIsHTML) {
|
|
395
|
+
// Use iOS message if available, otherwise use Android message
|
|
396
|
+
setHtmlContentIos(iosMessageToUse);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// FIX: Also handle the case where iOS content doesn't exist in backend but Android is HTML
|
|
401
|
+
// This ensures iOS content is set even when iOS content object is empty
|
|
402
|
+
// This is critical for "keepContentSame" scenario where iOS might be empty in backend
|
|
403
|
+
if (androidIsHTML && androidContent && (!iosContent || isEmpty(iosContent) || !iosMessage || iosMessage.trim() === '')) {
|
|
404
|
+
const androidMsgForIos = androidContent?.message || '';
|
|
405
|
+
if (androidMsgForIos && androidMsgForIos.trim() !== '') {
|
|
406
|
+
setHtmlContentIos(androidMsgForIos);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Only set other iOS properties if iOS content exists or is a valid iOS device
|
|
411
|
+
if (!isEmpty(iosContent) || isIosDevice) {
|
|
377
412
|
setTitleIos(iosTitle);
|
|
378
|
-
setTemplateMessageIos(
|
|
413
|
+
setTemplateMessageIos(iosMessageToUse);
|
|
379
414
|
// Update templateMediaType if iOS is HTML and Android wasn't
|
|
380
415
|
if (!androidIsHTML) {
|
|
381
416
|
// Check if this is a Bee editor template
|
|
@@ -397,8 +432,8 @@ export const InApp = (props) => {
|
|
|
397
432
|
setInAppImageSrcIos(iosImage);
|
|
398
433
|
setButtonTypeIos(iosCtaLength ? INAPP_BUTTON_TYPES.CTA : INAPP_BUTTON_TYPES.NONE);
|
|
399
434
|
setAddActionLinkIos(!isEmpty(iosCta) && true);
|
|
400
|
-
setDeepLinkValueIos(iosCta[0]?.actionLink
|
|
401
|
-
if (iosCtaLength > 0) {
|
|
435
|
+
setDeepLinkValueIos(iosCta && Array.isArray(iosCta) && iosCta[0]?.actionLink ? iosCta[0].actionLink : '');
|
|
436
|
+
if (iosCtaLength > 0 && iosCtas && Array.isArray(iosCtas)) {
|
|
402
437
|
setCtaDataIos(getCtaObject(iosCtas));
|
|
403
438
|
}
|
|
404
439
|
}
|
|
@@ -501,8 +536,12 @@ export const InApp = (props) => {
|
|
|
501
536
|
// tag Code end
|
|
502
537
|
|
|
503
538
|
const onTemplateLayoutTypeChange = ({ target: { value } }) => {
|
|
539
|
+
// FIX ISSUE #3: When changing layout type, preserve iOS content
|
|
504
540
|
// Update layout type and force HTMLEditor to remount with latest content
|
|
505
541
|
// Increment version to force remount - this ensures editor displays current content
|
|
542
|
+
// Store current content in refs before remounting to preserve it
|
|
543
|
+
htmlContentAndroidRef.current = htmlContentAndroid;
|
|
544
|
+
htmlContentIosRef.current = htmlContentIos;
|
|
506
545
|
setTemplateLayoutType(value);
|
|
507
546
|
setHtmlEditorContentVersion(prev => prev + 1);
|
|
508
547
|
};
|
|
@@ -626,8 +665,8 @@ export const InApp = (props) => {
|
|
|
626
665
|
const iosMediaValid = !iosSupported || (templateMediaType === INAPP_MEDIA_TYPES.TEXT || inAppImageSrcIos !== '');
|
|
627
666
|
|
|
628
667
|
// Check CTA requirements
|
|
629
|
-
const androidCtaValid = !isBtnTypeCtaAndroid || (ctaDataAndroid[0]?.isSaved);
|
|
630
|
-
const iosCtaValid = !isBtnTypeCTaIos || (ctaDataIos[0]?.isSaved);
|
|
668
|
+
const androidCtaValid = !isBtnTypeCtaAndroid || (ctaDataAndroid && Array.isArray(ctaDataAndroid) && ctaDataAndroid[0]?.isSaved);
|
|
669
|
+
const iosCtaValid = !isBtnTypeCTaIos || (ctaDataIos && Array.isArray(ctaDataIos) && ctaDataIos[0]?.isSaved);
|
|
631
670
|
|
|
632
671
|
// Check action link requirements
|
|
633
672
|
const androidActionLinkValid = !addActionLinkAndroid || deepLinkValueAndroid;
|
|
@@ -687,7 +726,7 @@ export const InApp = (props) => {
|
|
|
687
726
|
}
|
|
688
727
|
//cta
|
|
689
728
|
if (isBtnTypeCtaAndroid) {
|
|
690
|
-
if (ctaDataAndroid[0]?.isSaved) {
|
|
729
|
+
if (ctaDataAndroid && Array.isArray(ctaDataAndroid) && ctaDataAndroid[0]?.isSaved) {
|
|
691
730
|
//if button type is cta and 1st button is saved
|
|
692
731
|
return false;
|
|
693
732
|
}
|
|
@@ -695,7 +734,7 @@ export const InApp = (props) => {
|
|
|
695
734
|
return true;
|
|
696
735
|
}
|
|
697
736
|
if (isBtnTypeCTaIos) {
|
|
698
|
-
if (ctaDataIos[0]?.isSaved) {
|
|
737
|
+
if (ctaDataIos && Array.isArray(ctaDataIos) && ctaDataIos[0]?.isSaved) {
|
|
699
738
|
//if button type is cta and 1st button is saved
|
|
700
739
|
return false;
|
|
701
740
|
}
|
|
@@ -714,14 +753,20 @@ export const InApp = (props) => {
|
|
|
714
753
|
return false;
|
|
715
754
|
};
|
|
716
755
|
|
|
717
|
-
const getCtaPayload = (type) =>
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
756
|
+
const getCtaPayload = (type) => {
|
|
757
|
+
const ctaData = type === ANDROID ? ctaDataAndroid : ctaDataIos;
|
|
758
|
+
if (!ctaData || !Array.isArray(ctaData)) {
|
|
759
|
+
return [];
|
|
760
|
+
}
|
|
761
|
+
return ctaData.map((cta) => {
|
|
762
|
+
const { text, urlType, url } = cta;
|
|
763
|
+
return {
|
|
764
|
+
type: urlType,
|
|
765
|
+
actionText: text,
|
|
766
|
+
actionLink: url,
|
|
767
|
+
};
|
|
768
|
+
});
|
|
769
|
+
};
|
|
725
770
|
|
|
726
771
|
const createPayload = () => {
|
|
727
772
|
let androidMediaParams = {};
|
|
@@ -765,11 +810,21 @@ export const InApp = (props) => {
|
|
|
765
810
|
|
|
766
811
|
// Check account-level device support
|
|
767
812
|
const androidSupported = get(accountData, 'selectedWeChatAccount.configs.android') === DEVICE_SUPPORTED || get(editContent, 'ANDROID.deviceType') === ANDROID;
|
|
768
|
-
const iosSupported = get(accountData, 'selectedWeChatAccount.configs.ios') === DEVICE_SUPPORTED || get(editContent, 'IOS.deviceType') === IOS_CAPITAL;
|
|
813
|
+
const iosSupported = get(accountData, 'selectedWeChatAccount.configs.ios') === DEVICE_SUPPORTED || get(editContent, 'IOS.deviceType') === IOS_CAPITAL || get(editContent, 'IOS.deviceType') === IOS;
|
|
769
814
|
|
|
770
815
|
// Check if devices have content (for HTML Editor)
|
|
771
|
-
|
|
772
|
-
|
|
816
|
+
// If Android has content but iOS doesn't, and both devices are supported, check if content should be synced
|
|
817
|
+
// This handles the case where "keepContentSame" checkbox was used during creation
|
|
818
|
+
let hasAndroidContent = htmlContentAndroid && htmlContentAndroid?.trim() !== '';
|
|
819
|
+
let hasIosContent = htmlContentIos && htmlContentIos?.trim() !== '';
|
|
820
|
+
|
|
821
|
+
// FIX ISSUE #2: If Android has content but iOS doesn't, and both are supported,
|
|
822
|
+
// check if we should copy Android content to iOS (for "same content" scenario)
|
|
823
|
+
if (hasAndroidContent && !hasIosContent && androidSupported && iosSupported && isHTMLTemplate) {
|
|
824
|
+
// In create mode, if Android has content and iOS doesn't, use Android content for iOS
|
|
825
|
+
// This handles the "keepContentSame" checkbox scenario
|
|
826
|
+
hasIosContent = true; // Mark as having content so iOS is included in payload
|
|
827
|
+
}
|
|
773
828
|
|
|
774
829
|
// For HTML Editor, check if device has content and is supported
|
|
775
830
|
// For legacy editor, use isDisableDone check
|
|
@@ -802,10 +857,30 @@ export const InApp = (props) => {
|
|
|
802
857
|
} : {};
|
|
803
858
|
|
|
804
859
|
// Use HTML content if HTMLEditor is enabled and it's an HTML template, otherwise use regular message
|
|
805
|
-
|
|
860
|
+
// FIX ISSUE #2: If iOS content is empty but Android has content (same content scenario), use Android content
|
|
861
|
+
let iosMessage = isHTMLTemplate && htmlContentIos
|
|
806
862
|
? htmlContentIos
|
|
807
863
|
: templateMessageIos;
|
|
808
864
|
|
|
865
|
+
// FIX: If both devices have the same content (keepContentSame scenario), ensure iOS uses Android content
|
|
866
|
+
// This handles the case where keepContentSame is enabled and both should have the same content
|
|
867
|
+
const bothHaveSameContent = isHTMLTemplate && androidMessage && iosMessage
|
|
868
|
+
&& androidMessage.trim() === iosMessage.trim()
|
|
869
|
+
&& androidMessage.trim() !== '';
|
|
870
|
+
|
|
871
|
+
// If iOS message is empty but Android has content and both devices are supported, use Android content
|
|
872
|
+
// OR if both have the same content (keepContentSame), ensure they're both set correctly
|
|
873
|
+
if (isHTMLTemplate && androidSupported && iosSupported) {
|
|
874
|
+
if ((!iosMessage || iosMessage.trim() === '') && androidMessage && androidMessage.trim() !== '') {
|
|
875
|
+
iosMessage = androidMessage;
|
|
876
|
+
hasIosContent = true; // Mark as having content
|
|
877
|
+
} else if (bothHaveSameContent) {
|
|
878
|
+
// Ensure both are using the same content (in case of any discrepancies)
|
|
879
|
+
iosMessage = androidMessage;
|
|
880
|
+
hasIosContent = true;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
809
884
|
// Determine type and style for iOS
|
|
810
885
|
const iosType = isHTMLTemplate ? INAPP_MEDIA_TYPES.HTML : templateMediaType;
|
|
811
886
|
const iosExpandableStyle = isHTMLTemplate ? BIG_HTML : BIG_TEXT;
|
|
@@ -909,11 +984,39 @@ export const InApp = (props) => {
|
|
|
909
984
|
);
|
|
910
985
|
};
|
|
911
986
|
|
|
987
|
+
// Handle validation errors from HTMLEditor
|
|
988
|
+
const handleHtmlValidationErrors = useCallback((validationErrors) => {
|
|
989
|
+
// Update errorMessage state with validation errors from HTMLEditor
|
|
990
|
+
setErrorMessage((prev) => {
|
|
991
|
+
// Merge validation errors, preserving any existing errors from other sources
|
|
992
|
+
const newErrorState = {
|
|
993
|
+
STANDARD_ERROR_MSG: {
|
|
994
|
+
ANDROID: validationErrors.STANDARD_ERROR_MSG?.ANDROID || prev.STANDARD_ERROR_MSG?.ANDROID || [],
|
|
995
|
+
IOS: validationErrors.STANDARD_ERROR_MSG?.IOS || prev.STANDARD_ERROR_MSG?.IOS || [],
|
|
996
|
+
GENERIC: validationErrors.STANDARD_ERROR_MSG?.GENERIC || prev.STANDARD_ERROR_MSG?.GENERIC || [],
|
|
997
|
+
},
|
|
998
|
+
LIQUID_ERROR_MSG: {
|
|
999
|
+
ANDROID: validationErrors.LIQUID_ERROR_MSG?.ANDROID || prev.LIQUID_ERROR_MSG?.ANDROID || [],
|
|
1000
|
+
IOS: validationErrors.LIQUID_ERROR_MSG?.IOS || prev.LIQUID_ERROR_MSG?.IOS || [],
|
|
1001
|
+
GENERIC: validationErrors.LIQUID_ERROR_MSG?.GENERIC || prev.LIQUID_ERROR_MSG?.GENERIC || [],
|
|
1002
|
+
},
|
|
1003
|
+
};
|
|
1004
|
+
return newErrorState;
|
|
1005
|
+
});
|
|
1006
|
+
}, []);
|
|
1007
|
+
|
|
912
1008
|
// Handle HTML content changes from HTMLEditor
|
|
913
1009
|
const handleHtmlContentChange = useCallback((deviceContent, changedDevice) => {
|
|
914
1010
|
// The onChange callback from useInAppContent passes the full deviceContent object
|
|
915
|
-
//
|
|
916
|
-
//
|
|
1011
|
+
// When keepContentSame is enabled, both android and ios will have the same content
|
|
1012
|
+
// We need to detect this and update both states accordingly
|
|
1013
|
+
|
|
1014
|
+
// FIX: Check if both devices have the same content (indicates keepContentSame is enabled)
|
|
1015
|
+
// When keepContentSame is enabled, useInAppContent passes both android and ios with the same value
|
|
1016
|
+
const hasBothDevices = deviceContent?.android !== undefined && deviceContent?.ios !== undefined;
|
|
1017
|
+
const androidContent = typeof deviceContent?.android === 'string' ? deviceContent.android : '';
|
|
1018
|
+
const iosContent = typeof deviceContent?.ios === 'string' ? deviceContent.ios : '';
|
|
1019
|
+
const isSyncMode = hasBothDevices && androidContent === iosContent;
|
|
917
1020
|
|
|
918
1021
|
// Clear validation errors when content changes (similar to Bee Editor)
|
|
919
1022
|
// This ensures Done button re-enables after user fixes errors
|
|
@@ -932,8 +1035,18 @@ export const InApp = (props) => {
|
|
|
932
1035
|
}));
|
|
933
1036
|
}
|
|
934
1037
|
|
|
935
|
-
|
|
936
|
-
|
|
1038
|
+
// FIX: If sync mode (keepContentSame is enabled), update both devices
|
|
1039
|
+
if (isSyncMode) {
|
|
1040
|
+
// Update both Android and iOS with the same content
|
|
1041
|
+
// Use the Android content for both to ensure they're exactly the same
|
|
1042
|
+
const syncContent = androidContent || '';
|
|
1043
|
+
setHtmlContentAndroid(syncContent);
|
|
1044
|
+
setHtmlContentIos(syncContent);
|
|
1045
|
+
// Also update refs to ensure latest values are available immediately
|
|
1046
|
+
htmlContentAndroidRef.current = syncContent;
|
|
1047
|
+
htmlContentIosRef.current = syncContent;
|
|
1048
|
+
} else if (changedDevice) {
|
|
1049
|
+
// Only update the device that actually changed (when sync is disabled)
|
|
937
1050
|
if (changedDevice.toUpperCase() === ANDROID && deviceContent?.android !== undefined) {
|
|
938
1051
|
setHtmlContentAndroid(deviceContent.android || '');
|
|
939
1052
|
} else if (changedDevice.toUpperCase() === IOS_CAPITAL && deviceContent?.ios !== undefined) {
|
|
@@ -955,16 +1068,35 @@ export const InApp = (props) => {
|
|
|
955
1068
|
});
|
|
956
1069
|
}
|
|
957
1070
|
}
|
|
958
|
-
}, [ANDROID,
|
|
1071
|
+
}, [ANDROID, IOS_CAPITAL, setErrorMessage]);
|
|
959
1072
|
|
|
960
1073
|
// Handle HTML save from HTMLEditor
|
|
961
1074
|
const handleHtmlSave = useCallback((deviceContent) => {
|
|
962
|
-
//
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1075
|
+
// FIX: Check if both devices have the same content (indicates keepContentSame is enabled)
|
|
1076
|
+
// When keepContentSame is enabled, useInAppContent passes both android and ios with the same value
|
|
1077
|
+
const hasBothDevices = deviceContent?.android !== undefined && deviceContent?.ios !== undefined;
|
|
1078
|
+
const androidContent = typeof deviceContent?.android === 'string' ? deviceContent.android : '';
|
|
1079
|
+
const iosContent = typeof deviceContent?.ios === 'string' ? deviceContent.ios : '';
|
|
1080
|
+
const isSyncMode = hasBothDevices && androidContent === iosContent;
|
|
1081
|
+
|
|
1082
|
+
// FIX: If sync mode (keepContentSame is enabled), update both devices
|
|
1083
|
+
if (isSyncMode) {
|
|
1084
|
+
// Update both Android and iOS with the same content
|
|
1085
|
+
// Use the Android content for both to ensure they're exactly the same
|
|
1086
|
+
const syncContent = androidContent || '';
|
|
1087
|
+
setHtmlContentAndroid(syncContent);
|
|
1088
|
+
setHtmlContentIos(syncContent);
|
|
1089
|
+
// Also update refs to ensure latest values are available immediately
|
|
1090
|
+
htmlContentAndroidRef.current = syncContent;
|
|
1091
|
+
htmlContentIosRef.current = syncContent;
|
|
1092
|
+
} else {
|
|
1093
|
+
// Update state for both devices if present in the callback
|
|
1094
|
+
if (deviceContent?.android !== undefined) {
|
|
1095
|
+
setHtmlContentAndroid(deviceContent.android || '');
|
|
1096
|
+
}
|
|
1097
|
+
if (deviceContent?.ios !== undefined) {
|
|
1098
|
+
setHtmlContentIos(deviceContent.ios || '');
|
|
1099
|
+
}
|
|
968
1100
|
}
|
|
969
1101
|
// Auto-save can trigger this, but we don't want to submit automatically
|
|
970
1102
|
// The user will click the Create/Update button to submit
|
|
@@ -1211,34 +1343,57 @@ export const InApp = (props) => {
|
|
|
1211
1343
|
/>
|
|
1212
1344
|
</>
|
|
1213
1345
|
)}
|
|
1214
|
-
{shouldUseHTMLEditor ? (
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1346
|
+
{shouldUseHTMLEditor ? (() => {
|
|
1347
|
+
// FIX: When loading edit content, check if Android and iOS have the same content
|
|
1348
|
+
// If they do, ensure both are initialized with the same content
|
|
1349
|
+
const androidContentValue = htmlContentAndroidRef.current || htmlContentAndroid || templateMessageAndroid || '';
|
|
1350
|
+
const iosContentValue = htmlContentIosRef.current || htmlContentIos || templateMessageIos || '';
|
|
1351
|
+
|
|
1352
|
+
// Check if both devices have the same content (indicates "keepContentSame" was used)
|
|
1353
|
+
// Also check if iOS is empty but Android has content (same content scenario)
|
|
1354
|
+
const hasSameContent = androidContentValue && iosContentValue && androidContentValue.trim() === iosContentValue.trim();
|
|
1355
|
+
const iosEmptyButAndroidHasContent = (!iosContentValue || iosContentValue.trim() === '') && androidContentValue && androidContentValue.trim() !== '';
|
|
1356
|
+
|
|
1357
|
+
// If content is the same OR iOS is empty but Android has content, ensure both devices get the same content
|
|
1358
|
+
// This ensures the "keepContentSame" checkbox state is correctly reflected
|
|
1359
|
+
const initialContentForEditor = (hasSameContent || iosEmptyButAndroidHasContent)
|
|
1360
|
+
? {
|
|
1361
|
+
android: androidContentValue,
|
|
1362
|
+
ios: androidContentValue, // Use Android content for both
|
|
1363
|
+
}
|
|
1364
|
+
: {
|
|
1365
|
+
android: androidContentValue,
|
|
1366
|
+
ios: iosContentValue,
|
|
1367
|
+
};
|
|
1368
|
+
|
|
1369
|
+
return (
|
|
1370
|
+
<HTMLEditor
|
|
1371
|
+
key={`inapp-html-editor-v${htmlEditorContentVersion}`}
|
|
1372
|
+
variant={HTML_EDITOR_VARIANTS.INAPP}
|
|
1373
|
+
layoutType={templateLayoutType}
|
|
1374
|
+
initialContent={initialContentForEditor}
|
|
1375
|
+
onContentChange={handleHtmlContentChange}
|
|
1376
|
+
onSave={handleHtmlSave}
|
|
1377
|
+
tags={tags}
|
|
1378
|
+
injectedTags={injectedTags}
|
|
1379
|
+
location={location}
|
|
1380
|
+
selectedOfferDetails={selectedOfferDetails}
|
|
1381
|
+
onTagSelect={() => {
|
|
1382
|
+
// Tag insertion is handled by HTMLEditor's CodeEditorPane at cursor position
|
|
1383
|
+
// Content updates will be propagated via onContentChange callback
|
|
1384
|
+
}}
|
|
1385
|
+
// Don't pass globalActions to prevent duplicate API calls
|
|
1386
|
+
// HTMLEditor will use onContextChange instead
|
|
1387
|
+
onContextChange={handleOnTagsContextChange}
|
|
1388
|
+
// Pass validation errors to HTMLEditor for display
|
|
1389
|
+
errors={errorMessage}
|
|
1390
|
+
// Pass callback to receive validation errors from HTMLEditor
|
|
1391
|
+
onValidationChange={handleHtmlValidationErrors}
|
|
1392
|
+
data-test="inapp-html-editor"
|
|
1393
|
+
style={{ width: '138%' }}
|
|
1394
|
+
/>
|
|
1395
|
+
);
|
|
1396
|
+
})() : (
|
|
1242
1397
|
<InappAdvanced
|
|
1243
1398
|
getFormData={getFormData}
|
|
1244
1399
|
setIsLoadingContent={setIsLoadingContent}
|
|
@@ -143,6 +143,17 @@ export const InappAdvanced = (props) => {
|
|
|
143
143
|
} = isFullMode ? editData?.templateDetails || {} : templateData || {};
|
|
144
144
|
editContent = get(versions, `base.content`, {});
|
|
145
145
|
|
|
146
|
+
// LIBRARY MODE FIX: Backend stores bodyType as 'POPUP' but UI expects 'MODAL'
|
|
147
|
+
// Convert back to MODAL for display (same as HTML Editor)
|
|
148
|
+
if (!isFullMode && editContent) {
|
|
149
|
+
if (editContent.ANDROID?.bodyType === INAPP_MESSAGE_LAYOUT_TYPES.POPUP) {
|
|
150
|
+
editContent.ANDROID.bodyType = INAPP_MESSAGE_LAYOUT_TYPES.MODAL;
|
|
151
|
+
}
|
|
152
|
+
if (editContent.IOS?.bodyType === INAPP_MESSAGE_LAYOUT_TYPES.POPUP) {
|
|
153
|
+
editContent.IOS.bodyType = INAPP_MESSAGE_LAYOUT_TYPES.MODAL;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
146
157
|
if (editContent && !isEmpty(editContent)) {
|
|
147
158
|
setEditFlow(true);
|
|
148
159
|
if (setTemplateName && name) {
|
|
@@ -171,7 +182,12 @@ export const InappAdvanced = (props) => {
|
|
|
171
182
|
});
|
|
172
183
|
}
|
|
173
184
|
// Get layout type from Android or iOS, whichever is available
|
|
174
|
-
|
|
185
|
+
// FIX: Convert POPUP to MODAL for UI consistency (backend stores POPUP, UI uses MODAL)
|
|
186
|
+
let layoutType = editContent?.ANDROID?.bodyType || editContent?.IOS?.bodyType;
|
|
187
|
+
// Map POPUP to MODAL so it's treated the same as HTML Editor templates
|
|
188
|
+
if (layoutType === INAPP_MESSAGE_LAYOUT_TYPES.POPUP) {
|
|
189
|
+
layoutType = INAPP_MESSAGE_LAYOUT_TYPES.MODAL;
|
|
190
|
+
}
|
|
175
191
|
if (layoutType) {
|
|
176
192
|
setTemplateLayoutType(layoutType);
|
|
177
193
|
}
|
|
@@ -682,21 +698,80 @@ export const InappAdvanced = (props) => {
|
|
|
682
698
|
// Check if BEE editor has content for a device
|
|
683
699
|
const hasBeeContent = (device) => {
|
|
684
700
|
if (device === ANDROID) {
|
|
685
|
-
// Check
|
|
686
|
-
const stateHtmlValue = androidBeeHtml?.value || (typeof androidBeeHtml === 'string' ? androidBeeHtml : '');
|
|
687
|
-
const htmlContent = stateHtmlValue || latestAndroidHtmlRef.current || '';
|
|
701
|
+
// Check JSON content first - this is updated immediately when content is added
|
|
688
702
|
const jsonContent = androidBeeJson && androidBeeJson !== '{}';
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
703
|
+
|
|
704
|
+
// If JSON is empty, there's no content
|
|
705
|
+
if (!jsonContent) {
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Check if JSON has actual content (not just empty structure)
|
|
710
|
+
// Parse JSON to check if it has meaningful content
|
|
711
|
+
try {
|
|
712
|
+
const parsedJson = typeof androidBeeJson === 'string' ? JSON.parse(androidBeeJson) : androidBeeJson;
|
|
713
|
+
// Check if JSON has rows with content (Bee editor structure)
|
|
714
|
+
const hasRows = parsedJson?.page?.rows && Array.isArray(parsedJson.page.rows) && parsedJson.page.rows.length > 0;
|
|
715
|
+
if (!hasRows) {
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Check if rows have actual modules/content
|
|
720
|
+
const hasContent = parsedJson.page.rows.some((row) => {
|
|
721
|
+
const columns = row?.columns || [];
|
|
722
|
+
return columns.some((col) => {
|
|
723
|
+
const modules = col?.modules || [];
|
|
724
|
+
return modules.length > 0;
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
// If JSON has content, return true (HTML might not be ready yet, but that's okay)
|
|
729
|
+
// The HTML will be generated when saving, but we should enable the button as soon as JSON has content
|
|
730
|
+
return hasContent;
|
|
731
|
+
} catch (e) {
|
|
732
|
+
// If JSON parsing fails, fall back to checking HTML
|
|
733
|
+
const stateHtmlValue = androidBeeHtml?.value || (typeof androidBeeHtml === 'string' ? androidBeeHtml : '');
|
|
734
|
+
const htmlContent = stateHtmlValue || latestAndroidHtmlRef.current || '';
|
|
735
|
+
const htmlContentStr = typeof htmlContent === 'string' ? htmlContent : '';
|
|
736
|
+
return htmlContentStr && htmlContentStr.trim() !== '';
|
|
737
|
+
}
|
|
692
738
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
const htmlContent = stateHtmlValue || latestIosHtmlRef.current || '';
|
|
739
|
+
|
|
740
|
+
// iOS device - same logic as Android
|
|
696
741
|
const jsonContent = iosBeeJson && iosBeeJson !== '{}';
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
742
|
+
|
|
743
|
+
// If JSON is empty, there's no content
|
|
744
|
+
if (!jsonContent) {
|
|
745
|
+
return false;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Check if JSON has actual content (not just empty structure)
|
|
749
|
+
try {
|
|
750
|
+
const parsedJson = typeof iosBeeJson === 'string' ? JSON.parse(iosBeeJson) : iosBeeJson;
|
|
751
|
+
// Check if JSON has rows with content (Bee editor structure)
|
|
752
|
+
const hasRows = parsedJson?.page?.rows && Array.isArray(parsedJson.page.rows) && parsedJson.page.rows.length > 0;
|
|
753
|
+
if (!hasRows) {
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Check if rows have actual modules/content
|
|
758
|
+
const hasContent = parsedJson.page.rows.some((row) => {
|
|
759
|
+
const columns = row?.columns || [];
|
|
760
|
+
return columns.some((col) => {
|
|
761
|
+
const modules = col?.modules || [];
|
|
762
|
+
return modules.length > 0;
|
|
763
|
+
});
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
// If JSON has content, return true (HTML might not be ready yet, but that's okay)
|
|
767
|
+
return hasContent;
|
|
768
|
+
} catch (e) {
|
|
769
|
+
// If JSON parsing fails, fall back to checking HTML
|
|
770
|
+
const stateHtmlValue = iosBeeHtml?.value || (typeof iosBeeHtml === 'string' ? iosBeeHtml : '');
|
|
771
|
+
const htmlContent = stateHtmlValue || latestIosHtmlRef.current || '';
|
|
772
|
+
const htmlContentStr = typeof htmlContent === 'string' ? htmlContent : '';
|
|
773
|
+
return htmlContentStr && htmlContentStr.trim() !== '';
|
|
774
|
+
}
|
|
700
775
|
};
|
|
701
776
|
|
|
702
777
|
// Check if Done button should be disabled
|
|
@@ -709,11 +784,9 @@ export const InappAdvanced = (props) => {
|
|
|
709
784
|
// Get account-level device support
|
|
710
785
|
const androidSupported = get(accountData, 'selectedWeChatAccount.configs.android') === DEVICE_SUPPORTED || get(editContent, 'ANDROID.deviceType') === ANDROID;
|
|
711
786
|
const iosSupported = get(accountData, 'selectedWeChatAccount.configs.ios') === DEVICE_SUPPORTED || get(editContent, 'IOS.deviceType') === IOS_CAPITAL;
|
|
712
|
-
|
|
713
787
|
// Check if devices have content
|
|
714
788
|
const hasAndroidContent = hasBeeContent(ANDROID);
|
|
715
789
|
const hasIosContent = hasBeeContent(IOS);
|
|
716
|
-
|
|
717
790
|
// If no tags are available (e.g., in tests), allow submission even without content
|
|
718
791
|
// This is to support test scenarios where content validation might not be fully set up
|
|
719
792
|
const hasTags = tags && tags.length > 0;
|
|
@@ -1169,7 +1169,11 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
1169
1169
|
return templates.filter((template) => {
|
|
1170
1170
|
const androidBodyType = get(template, 'versions.base.content.ANDROID.bodyType');
|
|
1171
1171
|
const iosBodyType = get(template, 'versions.base.content.IOS.bodyType');
|
|
1172
|
-
|
|
1172
|
+
let inappBodyType = androidBodyType || iosBodyType;
|
|
1173
|
+
// FIX: Map POPUP to MODAL for filter consistency (backend stores POPUP, UI uses MODAL)
|
|
1174
|
+
if (inappBodyType === INAPP_MESSAGE_LAYOUT_TYPES.POPUP) {
|
|
1175
|
+
inappBodyType = INAPP_MESSAGE_LAYOUT_TYPES.MODAL;
|
|
1176
|
+
}
|
|
1173
1177
|
return inappBodyType === selectedInAppLayout;
|
|
1174
1178
|
});
|
|
1175
1179
|
}
|
|
@@ -1275,7 +1279,12 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
1275
1279
|
const cardDataList = filteredTemplates && filteredTemplates.length ? map(filteredTemplates, (template) => {
|
|
1276
1280
|
const androidBodyType = get(template, 'versions.base.content.ANDROID.bodyType');
|
|
1277
1281
|
const iosBodyType = get(template, 'versions.base.content.IOS.bodyType');
|
|
1278
|
-
|
|
1282
|
+
let inappBodyType = androidBodyType || iosBodyType;
|
|
1283
|
+
// FIX: Map POPUP to MODAL for display consistency (backend stores POPUP, UI shows MODAL/Popup)
|
|
1284
|
+
// This ensures Bee Editor templates show the same as HTML Editor templates
|
|
1285
|
+
if (inappBodyType === INAPP_MESSAGE_LAYOUT_TYPES.POPUP) {
|
|
1286
|
+
inappBodyType = INAPP_MESSAGE_LAYOUT_TYPES.MODAL;
|
|
1287
|
+
}
|
|
1279
1288
|
const isZaloPreviewLoading = previewTemplateId === template?._id;
|
|
1280
1289
|
const templateData = {
|
|
1281
1290
|
key: `${currentChannel}-card-${template?.name}`,
|