@capillarytech/creatives-library 8.0.249 → 8.0.250-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.
Files changed (136) hide show
  1. package/assets/Android.png +0 -0
  2. package/assets/iOS.png +0 -0
  3. package/constants/unified.js +2 -1
  4. package/initialReducer.js +2 -0
  5. package/package.json +1 -1
  6. package/services/api.js +10 -0
  7. package/services/tests/api.test.js +18 -0
  8. package/utils/common.js +5 -0
  9. package/utils/commonUtils.js +28 -5
  10. package/utils/tests/commonUtil.test.js +224 -0
  11. package/utils/transformTemplateConfig.js +0 -10
  12. package/v2Components/CapDeviceContent/index.js +61 -56
  13. package/v2Components/CapTagList/index.js +6 -1
  14. package/v2Components/CapTagListWithInput/index.js +5 -1
  15. package/v2Components/CapTagListWithInput/messages.js +1 -1
  16. package/v2Components/CapWhatsappCTA/tests/index.test.js +5 -0
  17. package/v2Components/ErrorInfoNote/index.js +452 -72
  18. package/v2Components/ErrorInfoNote/messages.js +22 -0
  19. package/v2Components/ErrorInfoNote/style.scss +280 -4
  20. package/v2Components/FormBuilder/tests/index.test.js +13 -4
  21. package/v2Components/HtmlEditor/HTMLEditor.js +640 -94
  22. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +874 -0
  23. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +1167 -133
  24. package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +27 -16
  25. package/v2Components/HtmlEditor/_htmlEditor.scss +108 -45
  26. package/v2Components/HtmlEditor/_index.lazy.scss +1 -1
  27. package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +13 -101
  28. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +148 -139
  29. package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +2 -1
  30. package/v2Components/HtmlEditor/components/DeviceToggle/index.js +3 -3
  31. package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +9 -0
  32. package/v2Components/HtmlEditor/components/EditorToolbar/index.js +1 -1
  33. package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +22 -0
  34. package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +4 -7
  35. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +35 -45
  36. package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +1 -3
  37. package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +33 -33
  38. package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +7 -6
  39. package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +3 -6
  40. package/v2Components/HtmlEditor/components/PreviewPane/index.js +11 -13
  41. package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +1 -1
  42. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +49 -31
  43. package/v2Components/HtmlEditor/components/ValidationPanel/index.js +68 -39
  44. package/v2Components/HtmlEditor/components/ValidationTabs/_validationTabs.scss +254 -0
  45. package/v2Components/HtmlEditor/components/ValidationTabs/index.js +391 -0
  46. package/v2Components/HtmlEditor/components/ValidationTabs/messages.js +51 -0
  47. package/v2Components/HtmlEditor/constants.js +42 -20
  48. package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +373 -16
  49. package/v2Components/HtmlEditor/hooks/__tests__/useValidation.apiErrors.test.js +795 -0
  50. package/v2Components/HtmlEditor/hooks/useEditorContent.js +5 -2
  51. package/v2Components/HtmlEditor/hooks/useInAppContent.js +88 -146
  52. package/v2Components/HtmlEditor/hooks/useValidation.js +189 -53
  53. package/v2Components/HtmlEditor/index.js +1 -1
  54. package/v2Components/HtmlEditor/messages.js +95 -85
  55. package/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +94 -45
  56. package/v2Components/HtmlEditor/utils/contentSanitizer.js +40 -41
  57. package/v2Components/HtmlEditor/utils/htmlValidator.js +71 -72
  58. package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +134 -102
  59. package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +23 -25
  60. package/v2Components/HtmlEditor/utils/validationAdapter.js +66 -41
  61. package/v2Components/MobilePushPreviewV2/index.js +32 -7
  62. package/v2Components/TemplatePreview/_templatePreview.scss +44 -24
  63. package/v2Components/TemplatePreview/index.js +47 -32
  64. package/v2Components/TemplatePreview/messages.js +4 -0
  65. package/v2Components/TestAndPreviewSlidebox/_testAndPreviewSlidebox.scss +1 -0
  66. package/v2Containers/BeeEditor/index.js +172 -90
  67. package/v2Containers/BeePopupEditor/constants.js +10 -0
  68. package/v2Containers/BeePopupEditor/index.js +193 -0
  69. package/v2Containers/BeePopupEditor/tests/index.test.js +627 -0
  70. package/v2Containers/CreativesContainer/SlideBoxContent.js +127 -51
  71. package/v2Containers/CreativesContainer/SlideBoxFooter.js +163 -13
  72. package/v2Containers/CreativesContainer/SlideBoxHeader.js +2 -1
  73. package/v2Containers/CreativesContainer/constants.js +1 -0
  74. package/v2Containers/CreativesContainer/index.js +239 -46
  75. package/v2Containers/CreativesContainer/messages.js +8 -0
  76. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +11 -2
  77. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +38 -50
  78. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +106 -0
  79. package/v2Containers/Email/actions.js +7 -0
  80. package/v2Containers/Email/constants.js +5 -1
  81. package/v2Containers/Email/index.js +222 -27
  82. package/v2Containers/Email/messages.js +32 -0
  83. package/v2Containers/Email/reducer.js +12 -1
  84. package/v2Containers/Email/sagas.js +61 -7
  85. package/v2Containers/Email/tests/__snapshots__/reducer.test.js.snap +2 -0
  86. package/v2Containers/Email/tests/sagas.test.js +320 -29
  87. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +1321 -0
  88. package/v2Containers/EmailWrapper/components/EmailWrapperView.js +210 -15
  89. package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +40 -74
  90. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +1749 -0
  91. package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +520 -0
  92. package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +2 -67
  93. package/v2Containers/EmailWrapper/constants.js +2 -0
  94. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +629 -77
  95. package/v2Containers/EmailWrapper/index.js +103 -23
  96. package/v2Containers/EmailWrapper/messages.js +61 -1
  97. package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +643 -0
  98. package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +594 -77
  99. package/v2Containers/InApp/__tests__/InAppHTMLEditor.test.js +376 -0
  100. package/v2Containers/InApp/__tests__/sagas.test.js +363 -0
  101. package/v2Containers/InApp/actions.js +7 -0
  102. package/v2Containers/InApp/constants.js +20 -4
  103. package/v2Containers/InApp/index.js +802 -359
  104. package/v2Containers/InApp/index.scss +4 -3
  105. package/v2Containers/InApp/messages.js +7 -3
  106. package/v2Containers/InApp/reducer.js +21 -3
  107. package/v2Containers/InApp/sagas.js +29 -9
  108. package/v2Containers/InApp/selectors.js +25 -5
  109. package/v2Containers/InApp/tests/index.test.js +154 -50
  110. package/v2Containers/InApp/tests/reducer.test.js +34 -0
  111. package/v2Containers/InApp/tests/sagas.test.js +61 -9
  112. package/v2Containers/InApp/tests/selectors.test.js +612 -0
  113. package/v2Containers/InAppWrapper/components/InAppWrapperView.js +162 -0
  114. package/v2Containers/InAppWrapper/components/__tests__/InAppWrapperView.test.js +267 -0
  115. package/v2Containers/InAppWrapper/components/inAppWrapperView.scss +9 -0
  116. package/v2Containers/InAppWrapper/constants.js +16 -0
  117. package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +473 -0
  118. package/v2Containers/InAppWrapper/hooks/useInAppWrapper.js +198 -0
  119. package/v2Containers/InAppWrapper/index.js +148 -0
  120. package/v2Containers/InAppWrapper/messages.js +49 -0
  121. package/v2Containers/InappAdvance/index.js +1099 -0
  122. package/v2Containers/InappAdvance/index.scss +10 -0
  123. package/v2Containers/InappAdvance/tests/index.test.js +448 -0
  124. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +3 -0
  125. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/index.test.js.snap +2 -0
  126. package/v2Containers/Line/Container/Wrapper/tests/__snapshots__/index.test.js.snap +2 -0
  127. package/v2Containers/Line/Container/tests/__snapshots__/index.test.js.snap +9 -0
  128. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +12 -0
  129. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4 -0
  130. package/v2Containers/TagList/index.js +62 -19
  131. package/v2Containers/Templates/_templates.scss +60 -1
  132. package/v2Containers/Templates/index.js +89 -4
  133. package/v2Containers/Templates/messages.js +4 -0
  134. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +34 -0
  135. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +0 -152
  136. package/v2Containers/EmailWrapper/tests/EmailWrapperView.test.js +0 -214
@@ -0,0 +1,1321 @@
1
+ import React, {
2
+ useState, useEffect, useMemo, useCallback, useRef, useImperativeHandle, forwardRef,
3
+ } from 'react';
4
+ import PropTypes from 'prop-types';
5
+ import { injectIntl, FormattedMessage } from 'react-intl';
6
+ import isEmpty from 'lodash/isEmpty';
7
+ import get from 'lodash/get';
8
+ import _ from 'lodash';
9
+ import { CAP_SPACE_16 } from '@capillarytech/cap-ui-library/styled/variables';
10
+ import CapSpin from '@capillarytech/cap-ui-library/CapSpin';
11
+ import CapRow from '@capillarytech/cap-ui-library/CapRow';
12
+ import CapColumn from '@capillarytech/cap-ui-library/CapColumn';
13
+ import CapNotification from '@capillarytech/cap-ui-library/CapNotification';
14
+ import HTMLEditor from '../../../v2Components/HtmlEditor';
15
+ import CapTagListWithInput from '../../../v2Components/CapTagListWithInput';
16
+ import formBuilderMessages from '../../../v2Components/FormBuilder/messages';
17
+ import { validateLiquidTemplateContent } from '../../../utils/commonUtils';
18
+ import { hasLiquidSupportFeature, isEmailUnsubscribeTagMandatory } from '../../../utils/common';
19
+ import history from '../../../utils/history';
20
+ import messages from '../messages';
21
+ import emailMessages from '../../Email/messages';
22
+ import { validateTags } from '../../../utils/tagValidations';
23
+ import {
24
+ TAG, EMBEDDED, DEFAULT, FULL, LIBRARY,
25
+ } from '../../Whatsapp/constants';
26
+ import { EMAIL } from '../../CreativesContainer/constants';
27
+ import { OUTBOUND } from '../../../v2Components/FormBuilder/constants';
28
+
29
+ /**
30
+ * EmailHTMLEditor Component
31
+ *
32
+ * IMPORTANT: This component is ONLY used when supportCKEditor flag is FALSE (new flow).
33
+ * When supportCKEditor is TRUE, the existing Email component with FormBuilder is used (legacy flow).
34
+ *
35
+ * A completely self-contained component for Email HTML Editor that handles:
36
+ * - Tag loading and management
37
+ * - Tag validation
38
+ * - Content editing with HTMLEditor
39
+ * - Template data extraction for edit mode
40
+ * - Save logic (full mode & library mode) with liquid validation
41
+ * - API calls via Email actions/sagas
42
+ *
43
+ * This component is independent and reusable, similar to Whatsapp and InApp channels.
44
+ */
45
+ const EmailHTMLEditor = (props) => {
46
+ const {
47
+ intl,
48
+ location,
49
+ params,
50
+ getDefaultTags,
51
+ supportedTags,
52
+ metaEntities,
53
+ injectedTags,
54
+ globalActions,
55
+ loadingTags,
56
+ eventContextTags,
57
+ forwardedTags,
58
+ selectedOfferDetails,
59
+ currentOrgDetails,
60
+ isReadOnly = false,
61
+ fetchingLiquidTags = false,
62
+ createTemplateInProgress = false,
63
+ fetchingCmsData = false,
64
+ // Email Redux state
65
+ Email,
66
+ // Email actions for API calls
67
+ emailActions,
68
+ // Full mode props
69
+ isFullMode,
70
+ templateName,
71
+ showTemplateName,
72
+ isGetFormData,
73
+ getFormdata,
74
+ // Library mode props
75
+ templateData: templateDataProp,
76
+ // Uploaded content from zip file
77
+ EmailLayout,
78
+ // Liquid validation
79
+ getLiquidTags,
80
+ showLiquidErrorInFooter,
81
+ onValidationFail,
82
+ // Preview/Test
83
+ // Parent loading control
84
+ setIsLoadingContent,
85
+ forwardedRef,
86
+ // Module type for unsubscribe tag validation
87
+ moduleType,
88
+ // Validation state callback
89
+ onHtmlEditorValidationStateChange,
90
+ } = props;
91
+
92
+ const { formatMessage } = intl;
93
+
94
+ // State for content and subject
95
+ const [htmlContent, setHtmlContent] = useState('');
96
+ const [loadedHtmlContent, setLoadedHtmlContent] = useState(''); // Stable content for HTMLEditor initialization
97
+ const [subject, setSubject] = useState('');
98
+ const [subjectError, setSubjectError] = useState('');
99
+ // State for template name (extracted from template data in Edit mode)
100
+ const [extractedTemplateName, setExtractedTemplateName] = useState('');
101
+ const [tagValidationError, setTagValidationError] = useState(null);
102
+ const [isLoading, setIsLoading] = useState(true);
103
+ const [errorsAcknowledged, setErrorsAcknowledged] = useState(false); // Track if user has acknowledged errors by clicking redirection icon
104
+ // State for API validation errors (from validateLiquidTemplateContent)
105
+ const [apiValidationErrors, setApiValidationErrors] = useState({
106
+ liquidErrors: [],
107
+ standardErrors: [],
108
+ });
109
+
110
+ // Refs for tracking initialization and previous values
111
+ const contentInitializedRef = useRef(false);
112
+ const subjectInitializedRef = useRef(false);
113
+ const lastTemplateIdRef = useRef(null);
114
+ const fetchingTemplateIdRef = useRef(null); // Track which template we're currently fetching
115
+ const prevIsGetFormDataRef = useRef(false);
116
+ const prevErrorCountRef = useRef(0); // Track previous error count to detect new errors
117
+ const handleSaveRef = useRef(null);
118
+
119
+ // Ref to HTMLEditor to access validation state
120
+ const htmlEditorRef = useRef(null);
121
+
122
+ // Compute tags directly from metaEntities (same approach as Email component)
123
+ // Use deep equality check to prevent TagList from re-processing when tags haven't actually changed
124
+ // This fixes the slow expansion and React Intl errors when clicking nested tags
125
+ const tags = useMemo(() => {
126
+ let tagList = get(metaEntities, 'tags.standard', []);
127
+ const { type, module } = location?.query || {};
128
+ if (type === EMBEDDED && module === LIBRARY && !getDefaultTags) {
129
+ tagList = supportedTags || [];
130
+ }
131
+ return tagList;
132
+ }, [metaEntities, supportedTags, location, getDefaultTags]);
133
+
134
+ // Expose method to get formData for TestAndPreviewSlidebox
135
+ useImperativeHandle(forwardedRef, () => ({
136
+ getFormDataForPreview: () => {
137
+ const baseLanguage = get(currentOrgDetails, 'basic_details.base_language', 'en');
138
+ return {
139
+ "0": {
140
+ [baseLanguage]: {
141
+ 'template-content': htmlContent || '',
142
+ 'is_drag_drop': false,
143
+ },
144
+ activeTab: baseLanguage,
145
+ selectedLanguages: [baseLanguage],
146
+ base: true,
147
+ },
148
+ 'template-subject': subject || '',
149
+ };
150
+ },
151
+ getContentForPreview: () => htmlContent || '',
152
+ getValidationState: () => {
153
+ if (htmlEditorRef.current && htmlEditorRef.current.getValidation) {
154
+ return htmlEditorRef.current.getValidation();
155
+ }
156
+ return null;
157
+ },
158
+ isContentEmpty: () => {
159
+ if (htmlEditorRef.current && htmlEditorRef.current.isContentEmpty) {
160
+ return htmlEditorRef.current.isContentEmpty();
161
+ }
162
+ return !htmlContent || htmlContent.trim() === '';
163
+ },
164
+ getIssueCounts: () => {
165
+ if (htmlEditorRef.current && htmlEditorRef.current.getIssueCounts) {
166
+ return htmlEditorRef.current.getIssueCounts();
167
+ }
168
+ return {
169
+ html: 0, label: 0, liquid: 0, total: 0,
170
+ };
171
+ },
172
+ }), [htmlContent, subject, currentOrgDetails]);
173
+
174
+ // Check if liquid support is enabled
175
+ const isLiquidEnabled = hasLiquidSupportFeature();
176
+
177
+ // Detect edit mode
178
+ const hasParamsId = params?.id || location?.query?.id || location?.params?.id || location?.pathname?.includes('/edit/');
179
+ const currentTemplateId = templateDataProp?._id || params?.id || location?.query?.id || location?.params?.id
180
+ || location?.pathname?.match(/\/edit\/([^/]+)/)?.[1];
181
+ const isEditMode = !!currentTemplateId || !!hasParamsId;
182
+
183
+ // Load tags on component mount
184
+ useEffect(() => {
185
+ const { type, module } = location?.query || {};
186
+ const isEmbedded = type === EMBEDDED;
187
+ const query = {
188
+ layout: EMAIL,
189
+ type: TAG,
190
+ context: isEmbedded ? module : DEFAULT,
191
+ embedded: isEmbedded ? type : FULL,
192
+ };
193
+ if (getDefaultTags) {
194
+ query.context = getDefaultTags;
195
+ }
196
+ if (globalActions && globalActions.fetchSchemaForEntity) {
197
+ globalActions.fetchSchemaForEntity(query);
198
+ }
199
+ }, []);
200
+
201
+ // Initialize content from EmailLayout (uploaded zip) in create mode
202
+ useEffect(() => {
203
+ // Only check EmailLayout in create mode (not edit mode)
204
+ if (isEditMode) {
205
+ return;
206
+ }
207
+
208
+ // Check if EmailLayout has content from zip upload
209
+ if (EmailLayout) {
210
+ // EmailLayout can be a string (HTML content) or an object with html property
211
+ const uploadedContent = typeof EmailLayout === 'string'
212
+ ? EmailLayout
213
+ : (EmailLayout.html || EmailLayout.content || '');
214
+
215
+ if (uploadedContent) {
216
+ // IMPORTANT: Set both htmlContent and loadedHtmlContent for ZIP upload
217
+ // loadedHtmlContent is used by HTMLEditor's initialContent prop
218
+ setHtmlContent(uploadedContent);
219
+ setLoadedHtmlContent(uploadedContent);
220
+ setIsLoading(false);
221
+ if (setIsLoadingContent) {
222
+ setIsLoadingContent(false);
223
+ }
224
+ } else {
225
+ // No uploaded content, stop loading
226
+ setIsLoading(false);
227
+ if (setIsLoadingContent) {
228
+ setIsLoadingContent(false);
229
+ }
230
+ }
231
+ } else {
232
+ // No EmailLayout, stop loading
233
+ setIsLoading(false);
234
+ if (setIsLoadingContent) {
235
+ setIsLoadingContent(false);
236
+ }
237
+ }
238
+ }, [EmailLayout, isEditMode, setIsLoadingContent]);
239
+
240
+ // Edit mode: Extract template data and load content/subject
241
+ useEffect(() => {
242
+ if (!isEditMode) {
243
+ // Create mode: stop loading immediately
244
+ setIsLoading(false);
245
+ return;
246
+ }
247
+
248
+ const templateDataFromRedux = Email?.templateDetails || Email?.BEETemplate;
249
+ const isTemplateLoading = Email?.getTemplateDetailsInProgress || Email?.fetchingCmsData;
250
+
251
+ // Check if template ID changed (switching templates)
252
+ const templateIdChanged = currentTemplateId
253
+ && lastTemplateIdRef.current
254
+ && currentTemplateId !== lastTemplateIdRef.current;
255
+
256
+ // Reset refs when switching templates
257
+ if (templateIdChanged) {
258
+ contentInitializedRef.current = false;
259
+ subjectInitializedRef.current = false;
260
+ lastTemplateIdRef.current = currentTemplateId;
261
+ setHtmlContent('');
262
+ setSubject('');
263
+ setExtractedTemplateName('');
264
+ }
265
+
266
+ // Set last template ID on first load
267
+ if (currentTemplateId && !lastTemplateIdRef.current) {
268
+ lastTemplateIdRef.current = currentTemplateId;
269
+ }
270
+
271
+ // Check if templateDataProp has complete data
272
+ // Check if templateDataProp has complete data (must have actual content string)
273
+ const hasCompleteTemplateData = templateDataProp && (
274
+ (templateDataProp.emailBody || templateDataProp.html_content || templateDataProp['template-content'])
275
+ || (templateDataProp.base && (templateDataProp.base.html_content || templateDataProp.base.emailBody || templateDataProp.base['template-content']))
276
+ || (templateDataProp.versions?.base && (
277
+ // Check active tab content in versions
278
+ (() => {
279
+ const { base } = templateDataProp.versions;
280
+ const activeTab = base.activeTab || 'en';
281
+ const langData = base[activeTab] || {};
282
+ return langData['template-content'] || langData.html_content || base.html_content || base.emailBody;
283
+ })()
284
+ ))
285
+ );
286
+ if (hasCompleteTemplateData && !contentInitializedRef.current) {
287
+ let extractedContent = '';
288
+ let extractedSubject = '';
289
+ let extractedName = '';
290
+
291
+ if (templateDataProp.base) {
292
+ extractedContent = templateDataProp.base['template-content'] || templateDataProp.base.html_content || templateDataProp.base.emailBody || '';
293
+ extractedSubject = templateDataProp.base.subject || templateDataProp.base.emailSubject || '';
294
+ } else if (templateDataProp.versions && templateDataProp.versions.base) {
295
+ const { base } = templateDataProp.versions;
296
+ const activeTab = base.activeTab || 'en';
297
+ const languageData = base[activeTab] || {};
298
+
299
+ extractedContent = languageData['template-content']
300
+ || languageData.html_content
301
+ || base.html_content
302
+ || base.emailBody
303
+ || '';
304
+
305
+ extractedSubject = base.subject || base.emailSubject || '';
306
+ } else {
307
+ extractedContent = templateDataProp['template-content'] || templateDataProp.emailBody || templateDataProp.html_content || '';
308
+ extractedSubject = templateDataProp.emailSubject || templateDataProp.subject || '';
309
+ }
310
+
311
+ // Extract template name from templateDataProp
312
+ extractedName = templateDataProp.name || get(templateDataProp, 'versions.base.name') || '';
313
+
314
+ // IMPORTANT: Set both htmlContent and loadedHtmlContent
315
+ // loadedHtmlContent is used by HTMLEditor's initialContent prop
316
+ setHtmlContent(extractedContent);
317
+ setLoadedHtmlContent(extractedContent);
318
+ setSubject(extractedSubject);
319
+ setExtractedTemplateName(extractedName);
320
+ contentInitializedRef.current = true;
321
+ subjectInitializedRef.current = true;
322
+ setIsLoading(false);
323
+ if (setIsLoadingContent) {
324
+ setIsLoadingContent(false);
325
+ }
326
+ return;
327
+ }
328
+
329
+ if (currentTemplateId && emailActions?.getTemplateDetails && !isTemplateLoading) {
330
+ const isTemplateIdChanged = lastTemplateIdRef.current !== currentTemplateId;
331
+ const needsContent = !contentInitializedRef.current;
332
+ const alreadyFetching = fetchingTemplateIdRef.current === currentTemplateId;
333
+ const shouldFetch = (isTemplateIdChanged || needsContent) && !alreadyFetching;
334
+
335
+ // Fetch fresh data when template ID changes OR when we don't have content loaded yet
336
+ // BUT only if we're not already fetching this template
337
+ if (shouldFetch) {
338
+ fetchingTemplateIdRef.current = currentTemplateId; // Mark as fetching
339
+ emailActions.getTemplateDetails(currentTemplateId, 'email');
340
+ lastTemplateIdRef.current = currentTemplateId;
341
+ }
342
+ }
343
+
344
+ // **IMPORTANT: Clear stale Redux data in create mode**
345
+ // When not in edit mode (create mode), clear any existing template data from Redux
346
+ // This prevents previous template data from persisting when switching from Edit to Create
347
+ if (!currentTemplateId && !isEditMode && (templateDataFromRedux?._id || templateDataFromRedux?.name)) {
348
+ // Clear stale template data - component will handle via resetTemplateData or clearAllValues
349
+ if (emailActions?.clearAllValues) {
350
+ emailActions.clearAllValues();
351
+ }
352
+ // Reset component state to ensure clean slate for create mode
353
+ setExtractedTemplateName('');
354
+ contentInitializedRef.current = false;
355
+ subjectInitializedRef.current = false;
356
+ lastTemplateIdRef.current = null;
357
+ }
358
+
359
+ // Stop loading if template is loading
360
+ if (isTemplateLoading) {
361
+ return;
362
+ }
363
+
364
+ // Extract from Redux data
365
+ const hasTemplateDataFromRedux = templateDataFromRedux
366
+ && (templateDataFromRedux._id || templateDataFromRedux.name || templateDataFromRedux.versions);
367
+
368
+ if (hasTemplateDataFromRedux && currentTemplateId) {
369
+ const reduxTemplateId = templateDataFromRedux?._id;
370
+
371
+ if (reduxTemplateId === currentTemplateId) {
372
+ const baseData = get(templateDataFromRedux, 'versions.base') || get(templateDataFromRedux, 'base') || {};
373
+ const activeTab = baseData.activeTab
374
+ || get(currentOrgDetails, 'basic_details.base_language', 'en');
375
+ const languageData = baseData[activeTab] || {};
376
+
377
+ const extractedContent = languageData['template-content']
378
+ || languageData.html_content
379
+ || baseData.html_content
380
+ || baseData['template-content']
381
+ || get(templateDataFromRedux, 'html_content')
382
+ || get(templateDataFromRedux, 'template-content')
383
+ || '';
384
+
385
+ const extractedSubject = baseData.subject
386
+ || get(templateDataFromRedux, 'subject')
387
+ || get(templateDataFromRedux, 'versions.base.subject')
388
+ || '';
389
+
390
+ // Extract template name from Redux data
391
+ const extractedName = templateDataFromRedux.name || '';
392
+
393
+ // Smart update logic:
394
+ // 1. Always update loadedHtmlContent if Redux data is different (keep sync with backend)
395
+ // 2. Update htmlContent ONLY if it matches the OLD loadedHtmlContent (user hasn't edited)
396
+ // OR if it's the first initialization
397
+ if (extractedContent !== loadedHtmlContent) {
398
+ setLoadedHtmlContent(extractedContent);
399
+
400
+ if (!contentInitializedRef.current || htmlContent === loadedHtmlContent) {
401
+ setHtmlContent(extractedContent);
402
+ setSubject(extractedSubject);
403
+ setExtractedTemplateName(extractedName);
404
+ }
405
+ } else if (!contentInitializedRef.current) {
406
+ // First load, even if content matches (e.g. empty), ensure we set state
407
+ setHtmlContent(extractedContent);
408
+ setLoadedHtmlContent(extractedContent);
409
+ setSubject(extractedSubject);
410
+ setExtractedTemplateName(extractedName);
411
+ }
412
+
413
+ contentInitializedRef.current = true;
414
+ subjectInitializedRef.current = true;
415
+
416
+ // Only clear fetching ref if we are not loading anymore
417
+ // This prevents race conditions where we load stale data while real fetch is pending
418
+ if (!isTemplateLoading) {
419
+ fetchingTemplateIdRef.current = null;
420
+ }
421
+ }
422
+
423
+ setIsLoading(false);
424
+ if (setIsLoadingContent) {
425
+ setIsLoadingContent(false);
426
+ }
427
+ return;
428
+ }
429
+
430
+ // Fallback: stop loading anyway
431
+ if (!isTemplateLoading && !hasTemplateDataFromRedux && !hasCompleteTemplateData) {
432
+ setHtmlContent('');
433
+ setLoadedHtmlContent(''); // Set stable loaded content
434
+ setSubject('');
435
+ setExtractedTemplateName('');
436
+ setIsLoading(false);
437
+ if (setIsLoadingContent) {
438
+ setIsLoadingContent(false);
439
+ }
440
+ }
441
+ }, [
442
+ Email?.templateDetails,
443
+ Email?.BEETemplate,
444
+ Email?.getTemplateDetailsInProgress,
445
+ Email?.fetchingCmsData,
446
+ templateDataProp,
447
+ currentTemplateId,
448
+ isEditMode,
449
+ emailActions,
450
+ currentOrgDetails,
451
+ ]);
452
+
453
+ // Handle loading state
454
+ useEffect(() => {
455
+ const isAnyApiInProgress = loadingTags || fetchingLiquidTags || createTemplateInProgress || fetchingCmsData;
456
+
457
+ // Stop loading when no API is in progress and tags are loaded
458
+ // Use optional chaining to safely access tags
459
+ if (!isAnyApiInProgress && Array.isArray(tags) && tags?.length >= 0) {
460
+ setIsLoading(false);
461
+ } else if (isAnyApiInProgress) {
462
+ setIsLoading(true);
463
+ }
464
+ }, [loadingTags, tags, fetchingLiquidTags, createTemplateInProgress, fetchingCmsData]);
465
+
466
+ // Handle content change from HTMLEditor
467
+ const handleContentChange = useCallback((content) => {
468
+ setHtmlContent(content);
469
+
470
+ // Validate tags
471
+ if (tags.length > 0 || !isEmpty(injectedTags)) {
472
+ const validationResult = validateTags({
473
+ content,
474
+ tagsParam: tags,
475
+ injectedTagsParams: injectedTags,
476
+ location,
477
+ tagModule: getDefaultTags,
478
+ eventContextTags,
479
+ });
480
+
481
+ if (!validationResult.valid) {
482
+ setTagValidationError(validationResult);
483
+ } else {
484
+ setTagValidationError(null);
485
+ }
486
+ }
487
+ }, [tags, injectedTags, location, getDefaultTags, eventContextTags]);
488
+
489
+ // Store the last validation state received from HTMLEditor
490
+ const lastValidationStateRef = useRef(null);
491
+
492
+ // Use ref to store callback to avoid infinite loops (callback in deps would cause re-runs)
493
+ const onValidationStateChangeRef = useRef(onHtmlEditorValidationStateChange);
494
+ useEffect(() => {
495
+ onValidationStateChangeRef.current = onHtmlEditorValidationStateChange;
496
+ }, [onHtmlEditorValidationStateChange]);
497
+
498
+ // Handle error acknowledgment - called when user clicks redirection icon
499
+ const handleErrorAcknowledged = useCallback(() => {
500
+ setErrorsAcknowledged(true);
501
+ // Immediately update parent with acknowledged state
502
+ if (onValidationStateChangeRef.current && lastValidationStateRef.current) {
503
+ onValidationStateChangeRef.current({
504
+ ...lastValidationStateRef.current,
505
+ errorsAcknowledged: true,
506
+ });
507
+ }
508
+ }, []); // No deps needed - using ref
509
+
510
+ // Track last sent state to parent to prevent duplicate updates
511
+ const lastSentToParentRef = useRef(null);
512
+
513
+ // Callback to receive validation state from HTMLEditor
514
+ const handleValidationChange = useCallback((validationState) => {
515
+ const {
516
+ isContentEmpty, issueCounts, validationComplete, hasErrors,
517
+ } = validationState;
518
+ const currentErrorCount = issueCounts?.total || 0;
519
+
520
+ // Reset acknowledgment when new errors appear
521
+ let shouldAcknowledgeErrors = errorsAcknowledged;
522
+ if (hasErrors && validationComplete) {
523
+ const isFirstTimeErrors = prevErrorCountRef.current === 0 || prevErrorCountRef.current === undefined;
524
+ const errorCountChanged = currentErrorCount !== prevErrorCountRef.current;
525
+
526
+ if (isFirstTimeErrors || (errorCountChanged && currentErrorCount > 0)) {
527
+ setErrorsAcknowledged(false);
528
+ shouldAcknowledgeErrors = false;
529
+ }
530
+ } else if (!hasErrors && validationComplete) {
531
+ // No errors - allow buttons to be enabled
532
+ prevErrorCountRef.current = 0;
533
+ shouldAcknowledgeErrors = true;
534
+ }
535
+
536
+ // Update previous error count
537
+ if (validationComplete) {
538
+ prevErrorCountRef.current = currentErrorCount;
539
+ }
540
+
541
+ // Store last validation state (hasErrors = Rule Group #1 only, for save gating)
542
+ lastValidationStateRef.current = {
543
+ isContentEmpty,
544
+ issueCounts,
545
+ validationComplete,
546
+ hasErrors: hasErrors === true,
547
+ };
548
+
549
+ // Notify parent about validation state (using ref to avoid infinite loops)
550
+ // hasErrors = only Rule Group #1 (Input & Sanitization) – used by SlideBoxFooter for button gating
551
+ if (onValidationStateChangeRef.current) {
552
+ const newState = {
553
+ isContentEmpty,
554
+ issueCounts,
555
+ validationComplete,
556
+ hasErrors: hasErrors === true,
557
+ errorsAcknowledged: hasErrors ? shouldAcknowledgeErrors : true,
558
+ };
559
+
560
+ const lastState = lastSentToParentRef.current;
561
+ const hasChanged = !lastState
562
+ || lastState.isContentEmpty !== newState.isContentEmpty
563
+ || lastState.validationComplete !== newState.validationComplete
564
+ || lastState.hasErrors !== newState.hasErrors
565
+ || lastState.errorsAcknowledged !== newState.errorsAcknowledged
566
+ || lastState.issueCounts?.total !== newState.issueCounts?.total;
567
+
568
+ if (hasChanged) {
569
+ lastSentToParentRef.current = newState;
570
+ onValidationStateChangeRef.current(newState);
571
+ }
572
+ }
573
+ }, [errorsAcknowledged]);
574
+
575
+ // Note: Initial validation state is now sent by HTMLEditor's onValidationChange callback
576
+ // Removed the separate useEffect that was causing infinite loops by depending on htmlContent
577
+
578
+ // Handle tag insertion into Subject field
579
+ const handleSubjectTagSelect = useCallback((data) => {
580
+ if (data) {
581
+ const tagToInsert = `{{${data}}}`;
582
+ const input = document.getElementById('template-subject') || document.querySelector('#template-subject input');
583
+ let subjectValue = subject || '';
584
+ try {
585
+ if (input && (typeof input.selectionStart === 'number')) {
586
+ const startPos = input.selectionStart;
587
+ const endPos = input.selectionEnd;
588
+ subjectValue = `${subjectValue.substring(0, startPos)}${tagToInsert}${subjectValue.substring(endPos)}`;
589
+ setSubject(subjectValue);
590
+ try {
591
+ input.focus();
592
+ const newPos = startPos + tagToInsert.length;
593
+ input.selectionStart = newPos;
594
+ input.selectionEnd = newPos;
595
+ } catch (e) {
596
+ // Ignore focus errors
597
+ }
598
+ } else {
599
+ subjectValue = `${subjectValue}${tagToInsert}`;
600
+ setSubject(subjectValue);
601
+ if (input) {
602
+ try {
603
+ input.value = subjectValue;
604
+ } catch (e) {
605
+ // Ignore value setting errors
606
+ }
607
+ }
608
+ }
609
+ } catch (e) {
610
+ // Fallback: safe append
611
+ subjectValue = `${subjectValue}${tagToInsert}`;
612
+ setSubject(subjectValue);
613
+ }
614
+ }
615
+ }, [subject]);
616
+
617
+ // Handle subject change
618
+ const handleSubjectChange = useCallback((e) => {
619
+ const newSubject = e.target.value;
620
+ setSubject(newSubject);
621
+ if (newSubject && subjectError) {
622
+ setSubjectError('');
623
+ }
624
+ }, [subjectError]);
625
+
626
+ // Handle Save/Update with liquid validation
627
+ const handleSave = useCallback(() => {
628
+ // IMPORTANT: Clear API validation errors FIRST before checking for validation errors
629
+ // This ensures that old API errors don't block the save when user fixes content and clicks Update again
630
+ // We'll re-validate with fresh API call anyway
631
+ if (isLiquidEnabled && getLiquidTags) {
632
+ setApiValidationErrors({
633
+ liquidErrors: [],
634
+ standardErrors: [],
635
+ });
636
+ }
637
+
638
+ // 1. Validate Subject - BLOCKING (matches CK/BEE behavior)
639
+ if (!subject || !subject.trim()) {
640
+ const errorMessage = formatMessage(emailMessages["Email Subject cannot be empty."]);
641
+ setSubjectError(errorMessage);
642
+ // Reset parent state so next click is detected as a change
643
+ if (onValidationFail) {
644
+ onValidationFail();
645
+ }
646
+ // IMPORTANT: Return here to block save - matches CK/BEE editor behavior
647
+ return;
648
+ }
649
+ // Clear error if subject is valid
650
+ if (subjectError) {
651
+ setSubjectError('');
652
+ }
653
+
654
+ // 1.5. Check for HTML/Label/Liquid validation errors (excluding API errors which we just cleared)
655
+ // If errors exist, block save and reset acknowledgment
656
+ // Try to get issue counts from ref first, fallback to stored state
657
+ let issueCounts = {
658
+ html: 0, label: 0, liquid: 0, total: 0,
659
+ };
660
+ let allIssues = [];
661
+ if (htmlEditorRef.current && typeof htmlEditorRef.current.getAllIssues === 'function') {
662
+ allIssues = htmlEditorRef.current.getAllIssues();
663
+ // Filter out API validation errors - they're validated separately via API call
664
+ const clientSideIssues = allIssues.filter((issue) => issue.rule !== 'liquid-api-validation' && issue.rule !== 'standard-api-validation');
665
+
666
+ // Count only client-side errors
667
+ issueCounts = {
668
+ html: 0,
669
+ label: 0,
670
+ liquid: 0,
671
+ total: clientSideIssues.length,
672
+ };
673
+
674
+ clientSideIssues.forEach((issue) => {
675
+ const { source, rule, message } = issue;
676
+ const messageLower = (message || '').toLowerCase();
677
+ const ruleLower = (rule || '').toLowerCase();
678
+
679
+ // Check if it's a liquid error (client-side only, not API)
680
+ if (source === 'liquid-validator' && rule !== 'liquid-api-validation') {
681
+ issueCounts.liquid++;
682
+ } else if (
683
+ messageLower.includes('tag must be paired')
684
+ || messageLower.includes('open tag match failed')
685
+ || messageLower.includes('closed tag match failed')
686
+ || messageLower.includes('unclosed')
687
+ || messageLower.includes('missing required')
688
+ || ruleLower.includes('tag-pair')
689
+ || ruleLower.includes('attr-value-not-empty')
690
+ || ruleLower.includes('attr-no-duplication')
691
+ || ruleLower.includes('tag-self-close')
692
+ || ruleLower.includes('spec-char-escape')
693
+ || ruleLower.includes('tagname-lowercase')
694
+ || ruleLower.includes('attr-lowercase')
695
+ || ruleLower.includes('src-not-empty')
696
+ || ruleLower.includes('alt-require')
697
+ ) {
698
+ issueCounts.label++;
699
+ } else {
700
+ issueCounts.html++;
701
+ }
702
+ });
703
+ } else if (lastValidationStateRef.current?.issueCounts) {
704
+ issueCounts = lastValidationStateRef.current.issueCounts;
705
+ }
706
+
707
+ // Only Rule Group #1 (Input & Sanitization) blocks save; warnings do not block
708
+ // Check both lastValidationStateRef (from onValidationChange callback) and getValidationState (current state)
709
+ const validationState = htmlEditorRef.current && typeof htmlEditorRef.current.getValidationState === 'function'
710
+ ? htmlEditorRef.current.getValidationState()
711
+ : null;
712
+ const hasBlockingErrors = lastValidationStateRef.current?.hasErrors === true
713
+ || (validationState && validationState.hasErrors === true);
714
+
715
+ if (hasBlockingErrors) {
716
+ setErrorsAcknowledged(false);
717
+ if (onValidationStateChangeRef.current) {
718
+ onValidationStateChangeRef.current({
719
+ isContentEmpty: !htmlContent || !htmlContent.trim(),
720
+ issueCounts,
721
+ validationComplete: true,
722
+ hasErrors: true,
723
+ errorsAcknowledged: false,
724
+ });
725
+ }
726
+ if (onValidationFail) {
727
+ onValidationFail();
728
+ }
729
+ return;
730
+ }
731
+
732
+ // 2. Validate Unsubscribe Tag (if mandatory)
733
+ // Check if unsubscribe tag is mandatory and if it exists in content
734
+ if (isEmailUnsubscribeTagMandatory() && moduleType === OUTBOUND) {
735
+ // Check if content contains unsubscribe tag (either {{unsubscribe}} or {{unsubscribe(#...)})
736
+ const unsubscribeRegex = /{{unsubscribe(\(#[a-zA-Z\d]{6}\))?}}/g; // eslint-disable-line no-useless-escape
737
+ const hasUnsubscribeTag = unsubscribeRegex.test(htmlContent);
738
+
739
+ if (!hasUnsubscribeTag) {
740
+ // Show error notification
741
+ const missingTagsMsg = intl.formatMessage(formBuilderMessages.missingTags);
742
+ const errorMessage = `${missingTagsMsg} unsubscribe`;
743
+ CapNotification.error({
744
+ message: 'ERROR ! ! !',
745
+ description: errorMessage,
746
+ duration: 5,
747
+ });
748
+
749
+ // Reset parent state so next click is detected as a change
750
+ if (onValidationFail) {
751
+ onValidationFail();
752
+ }
753
+ // Block save - unsubscribe tag is mandatory
754
+ return;
755
+ }
756
+ }
757
+
758
+ // 3. Validate Content Tags
759
+ // For NON-liquid orgs: BLOCKING validation (matches CK/BEE behavior)
760
+ // For liquid orgs: Non-blocking (extractTags API will validate)
761
+ if (tags.length > 0 || !isEmpty(injectedTags)) {
762
+ const validationResult = validateTags({
763
+ content: htmlContent,
764
+ tagsParam: tags,
765
+ injectedTagsParams: injectedTags,
766
+ location,
767
+ tagModule: getDefaultTags,
768
+ eventContextTags,
769
+ });
770
+
771
+ const hasUnsupportedTags = validationResult?.unsupportedTags?.length > 0;
772
+ if (!validationResult?.valid || hasUnsupportedTags) {
773
+ setTagValidationError(validationResult);
774
+
775
+ // IMPORTANT: For non-liquid orgs, block save (like CK/BEE editor)
776
+ // For liquid orgs, continue (extractTags API will validate)
777
+ if (!isLiquidEnabled) {
778
+ // Show notification popup like CK/BEE editor
779
+ const baseLanguage = get(currentOrgDetails, 'basic_details.base_language', 'en');
780
+
781
+ const contentNotValidMsg = intl.formatMessage(formBuilderMessages.contentNotValidLanguage);
782
+ let errorMessage = `${contentNotValidMsg} ${baseLanguage}`;
783
+
784
+ if (hasUnsupportedTags) {
785
+ const unsupportedTagsMsg = intl.formatMessage(formBuilderMessages.unsupportedTags);
786
+ errorMessage += `\n${unsupportedTagsMsg} ${validationResult?.unsupportedTags?.join(', ')}`;
787
+ }
788
+ if (validationResult?.missingTags?.length > 0) {
789
+ const missingTagsMsg = intl.formatMessage(formBuilderMessages.missingTags);
790
+ errorMessage += `\n${missingTagsMsg} ${validationResult?.missingTags?.join(', ')}`;
791
+ }
792
+
793
+ const type = 'error';
794
+ CapNotification[type]({
795
+ message: `${type.toUpperCase()} ! ! ! `,
796
+ description: errorMessage,
797
+ duration: 5,
798
+ });
799
+
800
+ // Reset parent state so next click is detected as a change
801
+ if (onValidationFail) {
802
+ onValidationFail();
803
+ }
804
+ // Block save for non-liquid orgs
805
+ return;
806
+ }
807
+ // For liquid orgs, just show warning and continue
808
+ }
809
+ // Clear tag errors if valid
810
+ if (tagValidationError && validationResult?.valid && !hasUnsupportedTags) {
811
+ setTagValidationError(null);
812
+ }
813
+ }
814
+
815
+
816
+ const baseLanguage = get(currentOrgDetails, 'basic_details.base_language', 'en');
817
+
818
+ // Actual save function - called after liquid validation (if enabled) or directly
819
+ const performSave = () => {
820
+ if (isFullMode) {
821
+ // Full mode: Call email actions directly
822
+ const templateDataFromRedux = Email?.templateDetails || Email?.BEETemplate;
823
+ // IMPORTANT: Only use currentTemplateId from URL/params - don't fallback to Redux data
824
+ // which might be stale from previous template in create mode
825
+ const templateId = currentTemplateId;
826
+ const isEditModeForSave = !!templateId;
827
+
828
+ const langId = get(currentOrgDetails, `basic_details.languages.${baseLanguage}.lang_id`, '');
829
+ const language = get(currentOrgDetails, `basic_details.languages.${baseLanguage}.language`, baseLanguage);
830
+
831
+ // Generate or reuse tabKey
832
+ let tabKey = _.uniqueId();
833
+ if (isEditMode && templateDataFromRedux) {
834
+ const existingTabKey = get(templateDataFromRedux, 'versions.base.tabKey')
835
+ || get(templateDataFromRedux, 'base.tabKey');
836
+ if (existingTabKey) {
837
+ tabKey = existingTabKey;
838
+ }
839
+ }
840
+
841
+ const languageData = {
842
+ 'template-content': htmlContent || '',
843
+ "is_drag_drop": false,
844
+ "drag_drop_id": '',
845
+ "lang_id": langId,
846
+ "iso_code": baseLanguage,
847
+ "language": language,
848
+ "tabKey": tabKey,
849
+ };
850
+
851
+ const baseStructure = {
852
+ [baseLanguage]: languageData,
853
+ activeTab: baseLanguage,
854
+ selectedLanguages: [baseLanguage],
855
+ base: true,
856
+ tabKey,
857
+ subject: subject || '',
858
+ };
859
+
860
+ const historyEntry = {
861
+ [baseLanguage]: { ...languageData },
862
+ activeTab: baseLanguage,
863
+ selectedLanguages: [baseLanguage],
864
+ base: true,
865
+ tabKey,
866
+ subject: subject || '',
867
+ };
868
+
869
+ const finalTemplateName = isEditModeForSave ? (extractedTemplateName || '') : (templateName || '');
870
+
871
+ const obj = {
872
+ type: EMAIL,
873
+ // In Edit mode, use extractedTemplateName; in Create mode, use templateName prop
874
+ name: finalTemplateName,
875
+ versions: {
876
+ base: baseStructure,
877
+ history: [historyEntry],
878
+ },
879
+ };
880
+
881
+ if (isEditModeForSave && templateId) {
882
+ obj._id = templateId;
883
+ }
884
+
885
+ if (emailActions?.transformEmailTemplate && emailActions?.createTemplate) {
886
+ emailActions.transformEmailTemplate(obj, (newEmail) => {
887
+ emailActions.createTemplate(newEmail, (createResponse) => {
888
+ // Handle error response (from 409 or other failures)
889
+ if (createResponse?.error) {
890
+ // Error already shown by Email component via stopValidation
891
+ // Reset acknowledgment so buttons are disabled again
892
+ setErrorsAcknowledged(false);
893
+ // Just reset parent state so next click is detected
894
+ if (onValidationFail) {
895
+ onValidationFail();
896
+ }
897
+ return;
898
+ }
899
+
900
+ // Handle success response
901
+ if (createResponse && createResponse.templateId) {
902
+ const successMessage = formatMessage(emailMessages.emailCreateSuccess);
903
+ CapNotification.success({ message: successMessage, key: 'create-template-success' });
904
+
905
+ const module = location?.query?.module || 'default';
906
+ const type = location?.query?.type;
907
+ const isLanguageSupport = location?.query?.isLanguageSupport || false;
908
+ const isBEESupport = (location?.query?.isBEESupport !== "false") || false;
909
+
910
+ if (getFormdata) {
911
+ const { versions, ...rest } = createResponse.templateId;
912
+ getFormdata({ value: versions, ...rest, validity: true });
913
+ } else {
914
+ const queryParams = type === 'embedded'
915
+ ? {
916
+ type: 'embedded', module, isLanguageSupport, isBEESupport,
917
+ }
918
+ : { module, isLanguageSupport, isBEESupport };
919
+
920
+ const searchParams = new URLSearchParams();
921
+ Object.keys(queryParams).forEach((key) => {
922
+ if (queryParams[key] !== undefined && queryParams[key] !== null) {
923
+ searchParams.append(key, queryParams[key]);
924
+ }
925
+ });
926
+
927
+ history.push({
928
+ pathname: '/email',
929
+ search: searchParams.toString() ? `?${searchParams.toString()}` : '',
930
+ });
931
+ }
932
+ }
933
+ });
934
+ });
935
+ }
936
+ } else {
937
+ // Library mode: Use getFormdata flow
938
+ const tmpData = {
939
+ html_content: htmlContent || '',
940
+ is_drag_drop: false,
941
+ lang_id: get(currentOrgDetails, `basic_details.languages.${baseLanguage}.lang_id`, ''),
942
+ iso_code: baseLanguage,
943
+ language: get(currentOrgDetails, `basic_details.languages.${baseLanguage}.language`, baseLanguage),
944
+ };
945
+
946
+ const libraryPayload = {
947
+ base: tmpData,
948
+ secondary_templates: [{ template_data: tmpData }],
949
+ };
950
+ libraryPayload.base.subject = subject || '';
951
+
952
+ if (location?.query?.module === 'library' && isGetFormData && getFormdata) {
953
+ const response = {
954
+ action: "getFormData",
955
+ postAction: 'next',
956
+ id: get(Email, 'templateDetails._id', ''),
957
+ value: libraryPayload,
958
+ validity: true,
959
+ type: EMAIL,
960
+ };
961
+ getFormdata(response);
962
+ } else if (getFormdata) {
963
+ getFormdata({
964
+ value: libraryPayload,
965
+ validity: true,
966
+ type: EMAIL,
967
+ });
968
+ }
969
+ }
970
+ };
971
+
972
+ // If liquid enabled, validate first using extractTags API
973
+ if (isLiquidEnabled && getLiquidTags) {
974
+ // Note: API validation errors are already cleared at the start of handleSave
975
+ // This ensures fresh validation on every save attempt
976
+
977
+ const onError = ({ standardErrors, liquidErrors }) => {
978
+ // Store API validation errors in state so they can be displayed in UI
979
+ setApiValidationErrors({
980
+ liquidErrors: liquidErrors || [],
981
+ standardErrors: standardErrors || [],
982
+ });
983
+
984
+ if (showLiquidErrorInFooter) {
985
+ showLiquidErrorInFooter({
986
+ STANDARD_ERROR_MSG: standardErrors || [],
987
+ LIQUID_ERROR_MSG: liquidErrors || [],
988
+ });
989
+ }
990
+ // Don't reset ref here - liquid validation is async and resetting causes infinite loop
991
+ // The parent's isGetFormData will be reset by onValidationFail, and the next click will be detected
992
+ if (onValidationFail) {
993
+ onValidationFail();
994
+ }
995
+ };
996
+
997
+ const onSuccess = () => {
998
+ // Clear API validation errors on success
999
+ setApiValidationErrors({
1000
+ liquidErrors: [],
1001
+ standardErrors: [],
1002
+ });
1003
+ performSave();
1004
+ };
1005
+
1006
+ validateLiquidTemplateContent(htmlContent || '', {
1007
+ getLiquidTags: getLiquidTags
1008
+ ? (inputContent, callback) => getLiquidTags(inputContent, callback)
1009
+ : (inputContent, callback) => globalActions?.getLiquidTags?.(inputContent, callback),
1010
+ formatMessage: intl.formatMessage,
1011
+ messages: formBuilderMessages,
1012
+ onError,
1013
+ onSuccess,
1014
+ tagLookupMap: metaEntities?.tagLookupMap,
1015
+ eventContextTags,
1016
+ isLiquidFlow: true,
1017
+ forwardedTags: forwardedTags || {},
1018
+ });
1019
+ } else {
1020
+ performSave();
1021
+ }
1022
+ }, [
1023
+ subject,
1024
+ htmlContent,
1025
+ tags,
1026
+ injectedTags,
1027
+ location,
1028
+ getDefaultTags,
1029
+ eventContextTags,
1030
+ formatMessage,
1031
+ subjectError,
1032
+ isFullMode,
1033
+ currentOrgDetails,
1034
+ Email,
1035
+ currentTemplateId,
1036
+ templateDataProp,
1037
+ templateName,
1038
+ emailActions,
1039
+ getFormdata,
1040
+ isGetFormData,
1041
+ isLiquidEnabled,
1042
+ getLiquidTags,
1043
+ showLiquidErrorInFooter,
1044
+ metaEntities,
1045
+ forwardedTags,
1046
+ globalActions,
1047
+ intl,
1048
+ extractedTemplateName,
1049
+ ]);
1050
+
1051
+ // Keep handleSave ref up to date - MUST be before isGetFormData effect
1052
+ useEffect(() => {
1053
+ handleSaveRef.current = handleSave;
1054
+ }, [handleSave]);
1055
+
1056
+ // Trigger save when isGetFormData becomes true (Create/Done button clicked)
1057
+ useEffect(() => {
1058
+ const isGetFormDataChanged = isGetFormData && !prevIsGetFormDataRef.current;
1059
+ const wasReset = !isGetFormData && prevIsGetFormDataRef.current;
1060
+
1061
+ if (isGetFormDataChanged) {
1062
+ // Update ref immediately to prevent duplicate calls
1063
+ prevIsGetFormDataRef.current = true;
1064
+ // Call handleSave via ref to ensure we're using the latest version
1065
+ // This avoids the effect re-running when handleSave changes
1066
+ if (handleSaveRef.current) {
1067
+ handleSaveRef.current();
1068
+ } else {
1069
+ console.warn('[EmailHTMLEditor] handleSaveRef.current is null!');
1070
+ }
1071
+ } else if (wasReset) {
1072
+ // Reset ref when parent resets isGetFormData (e.g., after validation failure)
1073
+ prevIsGetFormDataRef.current = false;
1074
+ } else {
1075
+ // Update ref to current value for next comparison
1076
+ prevIsGetFormDataRef.current = isGetFormData;
1077
+ }
1078
+ }, [isGetFormData, isEditMode]); // Include isEditMode for logging
1079
+
1080
+ // Handle tag context change
1081
+ const handleOnTagsContextChange = useCallback((data) => {
1082
+ const { type, module } = location?.query || {};
1083
+ const isEmbedded = type === EMBEDDED;
1084
+ const contextValue = data || (isEmbedded ? module : DEFAULT);
1085
+ const query = {
1086
+ layout: EMAIL,
1087
+ type: TAG,
1088
+ context: contextValue ? contextValue.toLowerCase() : contextValue,
1089
+ embedded: isEmbedded ? type : FULL,
1090
+ };
1091
+ if (globalActions && globalActions.fetchSchemaForEntity) {
1092
+ globalActions.fetchSchemaForEntity(query);
1093
+ }
1094
+ }, [location, globalActions]);
1095
+
1096
+
1097
+ const spinTip = fetchingLiquidTags ? <FormattedMessage {...formBuilderMessages.liquidSpinText} /> : '';
1098
+
1099
+ // Handle template name change
1100
+ // Call showTemplateName when templateName is available (for CreativesContainer header)
1101
+ // This matches the behavior of Email component which calls showTemplateName in onFormDataChange
1102
+ // In Edit mode, use extractedTemplateName from template data; in Create mode, use templateName prop
1103
+ const { onFormDataChange: onFormDataChangeProp } = props;
1104
+
1105
+ // Create onFormDataChange callback that updates extractedTemplateName in Edit mode
1106
+ // This matches the Email component's onFormDataChange which updates its state
1107
+ const handleFormDataChange = useCallback((updatedFormData) => {
1108
+ const newTemplateName = updatedFormData?.['template-name'] || '';
1109
+
1110
+ // In Edit mode, update extractedTemplateName state (similar to Email component updating its formData state)
1111
+ if (isEditMode && newTemplateName !== extractedTemplateName) {
1112
+ setExtractedTemplateName(newTemplateName);
1113
+ }
1114
+
1115
+ // Call the parent's onFormDataChange if provided
1116
+ if (onFormDataChangeProp) {
1117
+ onFormDataChangeProp(updatedFormData);
1118
+ }
1119
+
1120
+ // Call showTemplateName again with updated formData (same pattern as Email component)
1121
+ if (showTemplateName && isFullMode) {
1122
+ showTemplateName({ formData: updatedFormData, onFormDataChange: handleFormDataChange });
1123
+ }
1124
+ }, [isEditMode, extractedTemplateName, onFormDataChangeProp, showTemplateName, isFullMode]);
1125
+
1126
+ useEffect(() => {
1127
+ if (showTemplateName && isFullMode) {
1128
+ // In Edit mode, use extractedTemplateName; in Create mode, use templateName prop
1129
+ const nameToUse = isEditMode ? extractedTemplateName : templateName;
1130
+ const formData = {
1131
+ 'template-name': nameToUse || '',
1132
+ };
1133
+ // Pass handleFormDataChange callback so CreativesContainer can update template name
1134
+ // This is the same pattern used in Email component
1135
+ showTemplateName({ formData, onFormDataChange: handleFormDataChange });
1136
+ }
1137
+ }, [showTemplateName, isFullMode, templateName, extractedTemplateName, isEditMode, handleFormDataChange]);
1138
+
1139
+ return (
1140
+ <CapSpin spinning={isLoading} tip={spinTip}>
1141
+ <CapRow>
1142
+ <CapColumn span={24}>
1143
+ {/* Subject Field */}
1144
+ <CapColumn span={24} style={{ marginBottom: CAP_SPACE_16 }}>
1145
+ <CapTagListWithInput
1146
+ inputId="template-subject"
1147
+ inputValue={subject}
1148
+ inputOnChange={handleSubjectChange}
1149
+ inputPlaceholder={formatMessage(messages.enterEmailSubject)}
1150
+ inputRequired
1151
+ inputErrorMessage={subjectError}
1152
+ headingText={formatMessage(messages.subject)}
1153
+ headingType="h4"
1154
+ headingStyle={{ marginRight: '85%' }}
1155
+ onTagSelect={handleSubjectTagSelect}
1156
+ onContextChange={handleOnTagsContextChange}
1157
+ location={location}
1158
+ tags={tags}
1159
+ injectedTags={injectedTags || {}}
1160
+ selectedOfferDetails={selectedOfferDetails}
1161
+ eventContextTags={eventContextTags}
1162
+ showHeading
1163
+ showTagList
1164
+ showInput
1165
+ containerStyle={{
1166
+ display: 'flex',
1167
+ flexDirection: 'column',
1168
+ }}
1169
+ popoverPlacement="bottomRight"
1170
+ />
1171
+ </CapColumn>
1172
+
1173
+ {/* Tag Validation Error Display */}
1174
+ {/* {tagValidationError && !tagValidationError.valid && (
1175
+ <CapColumn span={24} style={{ marginBottom: CAP_SPACE_16 }}>
1176
+ <CapNotification
1177
+ type="error"
1178
+ message={formatMessage({
1179
+ id: 'creatives.containersV2.Email.tagValidationError',
1180
+ defaultMessage: 'Tag validation error',
1181
+ })}
1182
+ description={(
1183
+ <CapRow>
1184
+ {tagValidationError?.missingTags?.length > 0 && (
1185
+ <CapColumn span={24}>
1186
+ Missing tags:
1187
+ {' '}
1188
+ {tagValidationError.missingTags.join(', ')}
1189
+ </CapColumn>
1190
+ )}
1191
+ {tagValidationError?.unsupportedTags?.length > 0 && (
1192
+ <CapColumn span={24}>
1193
+ Unsupported tags:
1194
+ {' '}
1195
+ {tagValidationError.unsupportedTags.join(', ')}
1196
+ </CapColumn>
1197
+ )}
1198
+ {tagValidationError?.isBraceError && (
1199
+ <CapColumn span={24}>
1200
+ Unbalanced brackets detected
1201
+ </CapColumn>
1202
+ )}
1203
+ </CapRow>
1204
+ )}
1205
+ />
1206
+ </CapColumn>
1207
+ )} */}
1208
+
1209
+ <HTMLEditor
1210
+ ref={htmlEditorRef}
1211
+ variant="email"
1212
+ initialContent={loadedHtmlContent || ''}
1213
+ onContentChange={handleContentChange}
1214
+ onSave={handleSave}
1215
+ readOnly={isReadOnly}
1216
+ showFullscreenButton
1217
+ autoSave={false}
1218
+ tags={tags}
1219
+ injectedTags={injectedTags}
1220
+ location={location}
1221
+ eventContextTags={eventContextTags}
1222
+ selectedOfferDetails={selectedOfferDetails}
1223
+ channel={EMAIL}
1224
+ userLocale={intl.locale || 'en'}
1225
+ moduleFilterEnabled={location?.query?.type !== EMBEDDED}
1226
+ onTagContextChange={handleOnTagsContextChange}
1227
+ isLiquidEnabled={isLiquidEnabled}
1228
+ isFullMode={isFullMode}
1229
+ onErrorAcknowledged={handleErrorAcknowledged}
1230
+ onValidationChange={handleValidationChange}
1231
+ apiValidationErrors={apiValidationErrors}
1232
+ />
1233
+ </CapColumn>
1234
+ </CapRow>
1235
+ </CapSpin>
1236
+ );
1237
+ };
1238
+
1239
+ EmailHTMLEditor.propTypes = {
1240
+ intl: PropTypes.object.isRequired,
1241
+ location: PropTypes.object,
1242
+ params: PropTypes.object,
1243
+ getDefaultTags: PropTypes.string,
1244
+ supportedTags: PropTypes.array,
1245
+ metaEntities: PropTypes.object,
1246
+ injectedTags: PropTypes.object,
1247
+ globalActions: PropTypes.object,
1248
+ loadingTags: PropTypes.bool,
1249
+ eventContextTags: PropTypes.array,
1250
+ forwardedTags: PropTypes.object,
1251
+ selectedOfferDetails: PropTypes.array,
1252
+ currentOrgDetails: PropTypes.object,
1253
+ isReadOnly: PropTypes.bool,
1254
+ fetchingLiquidTags: PropTypes.bool,
1255
+ createTemplateInProgress: PropTypes.bool,
1256
+ fetchingCmsData: PropTypes.bool,
1257
+ // Email Redux state
1258
+ Email: PropTypes.object,
1259
+ // Email actions
1260
+ emailActions: PropTypes.object,
1261
+ // Full mode props
1262
+ isFullMode: PropTypes.bool,
1263
+ templateName: PropTypes.string,
1264
+ showTemplateName: PropTypes.func,
1265
+ onFormDataChange: PropTypes.func,
1266
+ isGetFormData: PropTypes.bool,
1267
+ getFormdata: PropTypes.func,
1268
+ // Library mode props
1269
+ templateData: PropTypes.object,
1270
+ // Uploaded content from zip file
1271
+ EmailLayout: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), // eslint-disable-line react/require-default-props
1272
+ // Liquid validation
1273
+ getLiquidTags: PropTypes.func,
1274
+ showLiquidErrorInFooter: PropTypes.func,
1275
+ // Preview/Test
1276
+ // Parent loading control
1277
+ setIsLoadingContent: PropTypes.func,
1278
+ forwardedRef: PropTypes.object,
1279
+ onValidationFail: PropTypes.func,
1280
+ moduleType: PropTypes.string,
1281
+ onHtmlEditorValidationStateChange: PropTypes.func,
1282
+ };
1283
+
1284
+ EmailHTMLEditor.defaultProps = {
1285
+ location: {},
1286
+ params: {},
1287
+ getDefaultTags: null,
1288
+ supportedTags: [],
1289
+ showTemplateName: null,
1290
+ metaEntities: {},
1291
+ injectedTags: {},
1292
+ globalActions: {},
1293
+ loadingTags: false,
1294
+ eventContextTags: [],
1295
+ forwardedTags: {},
1296
+ selectedOfferDetails: [],
1297
+ currentOrgDetails: {},
1298
+ isReadOnly: false,
1299
+ fetchingLiquidTags: false,
1300
+ createTemplateInProgress: false,
1301
+ fetchingCmsData: false,
1302
+ Email: {},
1303
+ emailActions: {},
1304
+ isFullMode: false,
1305
+ templateName: '',
1306
+ onFormDataChange: null,
1307
+ isGetFormData: false,
1308
+ getFormdata: null,
1309
+ templateData: null,
1310
+ getLiquidTags: null,
1311
+ showLiquidErrorInFooter: null,
1312
+ setIsLoadingContent: null,
1313
+ forwardedRef: null,
1314
+ onValidationFail: null,
1315
+ moduleType: null,
1316
+ onHtmlEditorValidationStateChange: null,
1317
+ };
1318
+
1319
+ const EmailHTMLEditorWithIntl = injectIntl(EmailHTMLEditor);
1320
+
1321
+ export default forwardRef((props, ref) => <EmailHTMLEditorWithIntl {...props} forwardedRef={ref} />);