@capillarytech/creatives-library 8.0.236-beta.0 → 8.0.236

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 (38) hide show
  1. package/constants/unified.js +0 -1
  2. package/package.json +1 -1
  3. package/services/api.js +0 -5
  4. package/utils/common.js +1 -6
  5. package/v2Components/CapTagList/index.js +1 -2
  6. package/v2Components/CapTagListWithInput/index.js +1 -5
  7. package/v2Components/CapTagListWithInput/messages.js +1 -1
  8. package/v2Components/ErrorInfoNote/style.scss +1 -1
  9. package/v2Components/HtmlEditor/HTMLEditor.js +14 -86
  10. package/v2Components/HtmlEditor/_htmlEditor.scss +4 -0
  11. package/v2Components/HtmlEditor/_index.lazy.scss +1 -1
  12. package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +98 -11
  13. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +115 -174
  14. package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +1 -1
  15. package/v2Components/HtmlEditor/hooks/useEditorContent.js +2 -5
  16. package/v2Components/TestAndPreviewSlidebox/index.js +25 -31
  17. package/v2Containers/CreativesContainer/SlideBoxContent.js +35 -83
  18. package/v2Containers/CreativesContainer/SlideBoxFooter.js +3 -9
  19. package/v2Containers/CreativesContainer/index.js +11 -83
  20. package/v2Containers/CreativesContainer/messages.js +0 -4
  21. package/v2Containers/Email/actions.js +0 -7
  22. package/v2Containers/Email/constants.js +1 -5
  23. package/v2Containers/Email/index.js +0 -13
  24. package/v2Containers/Email/messages.js +0 -32
  25. package/v2Containers/Email/reducer.js +1 -12
  26. package/v2Containers/Email/sagas.js +0 -17
  27. package/v2Containers/EmailWrapper/components/EmailWrapperView.js +7 -193
  28. package/v2Containers/EmailWrapper/constants.js +0 -2
  29. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +67 -436
  30. package/v2Containers/EmailWrapper/index.js +23 -99
  31. package/v2Containers/EmailWrapper/messages.js +1 -61
  32. package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +49 -49
  33. package/v2Containers/TagList/index.js +0 -2
  34. package/v2Containers/Templates/index.js +0 -5
  35. package/HOW_BEE_EDITOR_WORKS.md +0 -375
  36. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +0 -1034
  37. package/v2Containers/EmailWrapper/tests/EmailHTMLEditor.test.js +0 -177
  38. package/v2Containers/EmailWrapper/tests/EmailHTMLEditorValidation.test.js +0 -90
@@ -1,1034 +0,0 @@
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 } 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
-
28
- /**
29
- * EmailHTMLEditor Component
30
- *
31
- * IMPORTANT: This component is ONLY used when supportCKEditor flag is FALSE (new flow).
32
- * When supportCKEditor is TRUE, the existing Email component with FormBuilder is used (legacy flow).
33
- *
34
- * A completely self-contained component for Email HTML Editor that handles:
35
- * - Tag loading and management
36
- * - Tag validation
37
- * - Content editing with HTMLEditor
38
- * - Template data extraction for edit mode
39
- * - Save logic (full mode & library mode) with liquid validation
40
- * - API calls via Email actions/sagas
41
- *
42
- * This component is independent and reusable, similar to Whatsapp and InApp channels.
43
- */
44
- const EmailHTMLEditor = (props) => {
45
- const {
46
- intl,
47
- location,
48
- params,
49
- getDefaultTags,
50
- supportedTags,
51
- metaEntities,
52
- injectedTags,
53
- globalActions,
54
- loadingTags,
55
- eventContextTags,
56
- forwardedTags,
57
- selectedOfferDetails,
58
- currentOrgDetails,
59
- isReadOnly = false,
60
- fetchingLiquidTags = false,
61
- createTemplateInProgress = false,
62
- fetchingCmsData = false,
63
- // Email Redux state
64
- Email,
65
- // Email actions for API calls
66
- emailActions,
67
- // Full mode props
68
- isFullMode,
69
- templateName,
70
- showTemplateName,
71
- isGetFormData,
72
- getFormdata,
73
- // Library mode props
74
- templateData: templateDataProp,
75
- // Uploaded content from zip file
76
- EmailLayout,
77
- // Liquid validation
78
- getLiquidTags,
79
- showLiquidErrorInFooter,
80
- onValidationFail,
81
- // Preview/Test
82
- // Parent loading control
83
- setIsLoadingContent,
84
- forwardedRef,
85
- } = props;
86
-
87
-
88
-
89
- const { formatMessage } = intl;
90
-
91
- // State for content and subject
92
- const [htmlContent, setHtmlContent] = useState('');
93
- const [loadedHtmlContent, setLoadedHtmlContent] = useState(''); // Stable content for HTMLEditor initialization
94
- const [subject, setSubject] = useState('');
95
- const [subjectError, setSubjectError] = useState('');
96
- // State for template name (extracted from template data in Edit mode)
97
- const [extractedTemplateName, setExtractedTemplateName] = useState('');
98
- const [tagValidationError, setTagValidationError] = useState(null);
99
- const [isLoading, setIsLoading] = useState(true);
100
-
101
- // Refs for tracking initialization and previous values
102
- const contentInitializedRef = useRef(false);
103
- const subjectInitializedRef = useRef(false);
104
- const lastTemplateIdRef = useRef(null);
105
- const fetchingTemplateIdRef = useRef(null); // Track which template we're currently fetching
106
- const prevIsGetFormDataRef = useRef(false);
107
-
108
- // Compute tags directly from metaEntities (same approach as Email component)
109
- // Use deep equality check to prevent TagList from re-processing when tags haven't actually changed
110
- // This fixes the slow expansion and React Intl errors when clicking nested tags
111
- const tags = useMemo(() => {
112
- let tagList = get(metaEntities, 'tags.standard', []);
113
- const { type, module } = location?.query || {};
114
- if (type === EMBEDDED && module === LIBRARY && !getDefaultTags) {
115
- tagList = supportedTags || [];
116
- }
117
- return tagList;
118
- }, [metaEntities, supportedTags, location, getDefaultTags]);
119
-
120
- // Expose method to get formData for TestAndPreviewSlidebox
121
- useImperativeHandle(forwardedRef, () => ({
122
- getFormDataForPreview: () => {
123
- const baseLanguage = get(currentOrgDetails, 'basic_details.base_language', 'en');
124
- return {
125
- "0": {
126
- [baseLanguage]: {
127
- 'template-content': htmlContent || '',
128
- 'is_drag_drop': false,
129
- },
130
- activeTab: baseLanguage,
131
- selectedLanguages: [baseLanguage],
132
- base: true,
133
- },
134
- 'template-subject': subject || '',
135
- };
136
- },
137
- getContentForPreview: () => {
138
- return htmlContent || '';
139
- },
140
- }), [htmlContent, subject, currentOrgDetails]);
141
-
142
- // Check if liquid support is enabled
143
- const isLiquidEnabled = hasLiquidSupportFeature();
144
-
145
- // Detect edit mode
146
- const hasParamsId = params?.id || location?.query?.id || location?.params?.id || location?.pathname?.includes('/edit/');
147
- const currentTemplateId = templateDataProp?._id || params?.id || location?.query?.id || location?.params?.id
148
- || location?.pathname?.match(/\/edit\/([^/]+)/)?.[1];
149
- const isEditMode = !!currentTemplateId || !!hasParamsId;
150
-
151
- // Load tags on component mount
152
- useEffect(() => {
153
- const { type, module } = location?.query || {};
154
- const isEmbedded = type === EMBEDDED;
155
- const query = {
156
- layout: EMAIL,
157
- type: TAG,
158
- context: isEmbedded ? module : DEFAULT,
159
- embedded: isEmbedded ? type : FULL,
160
- };
161
- if (getDefaultTags) {
162
- query.context = getDefaultTags;
163
- }
164
- if (globalActions && globalActions.fetchSchemaForEntity) {
165
- globalActions.fetchSchemaForEntity(query);
166
- }
167
- }, []);
168
-
169
- // Initialize content from EmailLayout (uploaded zip) in create mode
170
- useEffect(() => {
171
- // Only check EmailLayout in create mode (not edit mode)
172
- if (isEditMode) {
173
- return;
174
- }
175
-
176
- // Check if EmailLayout has content from zip upload
177
- if (EmailLayout) {
178
- // EmailLayout can be a string (HTML content) or an object with html property
179
- const uploadedContent = typeof EmailLayout === 'string'
180
- ? EmailLayout
181
- : (EmailLayout.html || EmailLayout.content || '');
182
-
183
- if (uploadedContent) {
184
- setHtmlContent(uploadedContent);
185
- setIsLoading(false);
186
- if (setIsLoadingContent) {
187
- setIsLoadingContent(false);
188
- }
189
- } else {
190
- // No uploaded content, stop loading
191
- setIsLoading(false);
192
- if (setIsLoadingContent) {
193
- setIsLoadingContent(false);
194
- }
195
- }
196
- } else {
197
- // No EmailLayout, stop loading
198
- setIsLoading(false);
199
- if (setIsLoadingContent) {
200
- setIsLoadingContent(false);
201
- }
202
- }
203
- }, [EmailLayout, isEditMode, setIsLoadingContent]);
204
-
205
- // Edit mode: Extract template data and load content/subject
206
- useEffect(() => {
207
- if (!isEditMode) {
208
- // Create mode: stop loading immediately
209
- setIsLoading(false);
210
- return;
211
- }
212
-
213
- const templateDataFromRedux = Email?.templateDetails || Email?.BEETemplate;
214
- const isTemplateLoading = Email?.getTemplateDetailsInProgress || Email?.fetchingCmsData;
215
-
216
- // Check if template ID changed (switching templates)
217
- const templateIdChanged = currentTemplateId
218
- && lastTemplateIdRef.current
219
- && currentTemplateId !== lastTemplateIdRef.current;
220
-
221
- // Reset refs when switching templates
222
- if (templateIdChanged) {
223
- contentInitializedRef.current = false;
224
- subjectInitializedRef.current = false;
225
- lastTemplateIdRef.current = currentTemplateId;
226
- setHtmlContent('');
227
- setSubject('');
228
- setExtractedTemplateName('');
229
- }
230
-
231
- // Set last template ID on first load
232
- if (currentTemplateId && !lastTemplateIdRef.current) {
233
- lastTemplateIdRef.current = currentTemplateId;
234
- }
235
-
236
- // Check if templateDataProp has complete data
237
- // Check if templateDataProp has complete data (must have actual content string)
238
- const hasCompleteTemplateData = templateDataProp && (
239
- (templateDataProp.emailBody || templateDataProp.html_content || templateDataProp['template-content'])
240
- || (templateDataProp.base && (templateDataProp.base.html_content || templateDataProp.base.emailBody || templateDataProp.base['template-content']))
241
- || (templateDataProp.versions?.base && (
242
- // Check active tab content in versions
243
- (() => {
244
- const { base } = templateDataProp.versions;
245
- const activeTab = base.activeTab || 'en';
246
- const langData = base[activeTab] || {};
247
- return langData['template-content'] || langData.html_content || base.html_content || base.emailBody;
248
- })()
249
- ))
250
- );
251
- if (hasCompleteTemplateData && !contentInitializedRef.current) {
252
- let extractedContent = '';
253
- let extractedSubject = '';
254
- let extractedName = '';
255
-
256
- if (templateDataProp.base) {
257
- extractedContent = templateDataProp.base['template-content'] || templateDataProp.base.html_content || templateDataProp.base.emailBody || '';
258
- extractedSubject = templateDataProp.base.subject || templateDataProp.base.emailSubject || '';
259
- } else if (templateDataProp.versions && templateDataProp.versions.base) {
260
- const { base } = templateDataProp.versions;
261
- const activeTab = base.activeTab || 'en';
262
- const languageData = base[activeTab] || {};
263
-
264
- extractedContent = languageData['template-content']
265
- || languageData.html_content
266
- || base.html_content
267
- || base.emailBody
268
- || '';
269
-
270
- extractedSubject = base.subject || base.emailSubject || '';
271
- } else {
272
- extractedContent = templateDataProp['template-content'] || templateDataProp.emailBody || templateDataProp.html_content || '';
273
- extractedSubject = templateDataProp.emailSubject || templateDataProp.subject || '';
274
- }
275
-
276
- // Extract template name from templateDataProp
277
- extractedName = templateDataProp.name || get(templateDataProp, 'versions.base.name') || '';
278
-
279
- // IMPORTANT: Set both htmlContent and loadedHtmlContent
280
- // loadedHtmlContent is used by HTMLEditor's initialContent prop
281
- setHtmlContent(extractedContent);
282
- setLoadedHtmlContent(extractedContent);
283
- setSubject(extractedSubject);
284
- setExtractedTemplateName(extractedName);
285
- contentInitializedRef.current = true;
286
- subjectInitializedRef.current = true;
287
- setIsLoading(false);
288
- if (setIsLoadingContent) {
289
- setIsLoadingContent(false);
290
- }
291
- return;
292
- }
293
-
294
- if (currentTemplateId && emailActions?.getTemplateDetails && !isTemplateLoading) {
295
- const isTemplateIdChanged = lastTemplateIdRef.current !== currentTemplateId;
296
- const needsContent = !contentInitializedRef.current;
297
- const alreadyFetching = fetchingTemplateIdRef.current === currentTemplateId;
298
- const shouldFetch = (isTemplateIdChanged || needsContent) && !alreadyFetching;
299
-
300
- // Fetch fresh data when template ID changes OR when we don't have content loaded yet
301
- // BUT only if we're not already fetching this template
302
- if (shouldFetch) {
303
- fetchingTemplateIdRef.current = currentTemplateId; // Mark as fetching
304
- emailActions.getTemplateDetails(currentTemplateId, 'email');
305
- lastTemplateIdRef.current = currentTemplateId;
306
- }
307
- }
308
-
309
- // **IMPORTANT: Clear stale Redux data in create mode**
310
- // When not in edit mode (create mode), clear any existing template data from Redux
311
- // This prevents previous template data from persisting when switching from Edit to Create
312
- if (!currentTemplateId && !isEditMode && (templateDataFromRedux?._id || templateDataFromRedux?.name)) {
313
- // Clear stale template data - component will handle via resetTemplateData or clearAllValues
314
- if (emailActions?.clearAllValues) {
315
- emailActions.clearAllValues();
316
- }
317
- // Reset component state to ensure clean slate for create mode
318
- setExtractedTemplateName('');
319
- contentInitializedRef.current = false;
320
- subjectInitializedRef.current = false;
321
- lastTemplateIdRef.current = null;
322
- }
323
-
324
- // Stop loading if template is loading
325
- if (isTemplateLoading) {
326
- return;
327
- }
328
-
329
- // Extract from Redux data
330
- const hasTemplateDataFromRedux = templateDataFromRedux
331
- && (templateDataFromRedux._id || templateDataFromRedux.name || templateDataFromRedux.versions);
332
-
333
- if (hasTemplateDataFromRedux && currentTemplateId) {
334
- const reduxTemplateId = templateDataFromRedux?._id;
335
-
336
- if (reduxTemplateId === currentTemplateId) {
337
- const baseData = get(templateDataFromRedux, 'versions.base') || get(templateDataFromRedux, 'base') || {};
338
- const activeTab = baseData.activeTab
339
- || get(currentOrgDetails, 'basic_details.base_language', 'en');
340
- const languageData = baseData[activeTab] || {};
341
-
342
- const extractedContent = languageData['template-content']
343
- || languageData.html_content
344
- || baseData.html_content
345
- || baseData['template-content']
346
- || get(templateDataFromRedux, 'html_content')
347
- || get(templateDataFromRedux, 'template-content')
348
- || '';
349
-
350
- const extractedSubject = baseData.subject
351
- || get(templateDataFromRedux, 'subject')
352
- || get(templateDataFromRedux, 'versions.base.subject')
353
- || '';
354
-
355
- // Extract template name from Redux data
356
- const extractedName = templateDataFromRedux.name || '';
357
-
358
- // Smart update logic:
359
- // 1. Always update loadedHtmlContent if Redux data is different (keep sync with backend)
360
- // 2. Update htmlContent ONLY if it matches the OLD loadedHtmlContent (user hasn't edited)
361
- // OR if it's the first initialization
362
- if (extractedContent !== loadedHtmlContent) {
363
- setLoadedHtmlContent(extractedContent);
364
-
365
- if (!contentInitializedRef.current || htmlContent === loadedHtmlContent) {
366
- setHtmlContent(extractedContent);
367
- setSubject(extractedSubject);
368
- setExtractedTemplateName(extractedName);
369
- }
370
- } else if (!contentInitializedRef.current) {
371
- // First load, even if content matches (e.g. empty), ensure we set state
372
- setHtmlContent(extractedContent);
373
- setLoadedHtmlContent(extractedContent);
374
- setSubject(extractedSubject);
375
- setExtractedTemplateName(extractedName);
376
- }
377
-
378
- contentInitializedRef.current = true;
379
- subjectInitializedRef.current = true;
380
-
381
- // Only clear fetching ref if we are not loading anymore
382
- // This prevents race conditions where we load stale data while real fetch is pending
383
- if (!isTemplateLoading) {
384
- fetchingTemplateIdRef.current = null;
385
- }
386
- }
387
-
388
- setIsLoading(false);
389
- if (setIsLoadingContent) {
390
- setIsLoadingContent(false);
391
- }
392
- return;
393
- }
394
-
395
- // Fallback: stop loading anyway
396
- if (!isTemplateLoading && !hasTemplateDataFromRedux && !hasCompleteTemplateData) {
397
- setHtmlContent('');
398
- setLoadedHtmlContent(''); // Set stable loaded content
399
- setSubject('');
400
- setExtractedTemplateName('');
401
- setIsLoading(false);
402
- if (setIsLoadingContent) {
403
- setIsLoadingContent(false);
404
- }
405
- }
406
- }, [
407
- Email?.templateDetails,
408
- Email?.BEETemplate,
409
- Email?.getTemplateDetailsInProgress,
410
- Email?.fetchingCmsData,
411
- templateDataProp,
412
- currentTemplateId,
413
- isEditMode,
414
- emailActions,
415
- currentOrgDetails,
416
- ]);
417
-
418
- // Handle loading state
419
- useEffect(() => {
420
- const isAnyApiInProgress = loadingTags || fetchingLiquidTags || createTemplateInProgress || fetchingCmsData;
421
-
422
- // Stop loading when no API is in progress and tags are loaded
423
- // Use optional chaining to safely access tags
424
- if (!isAnyApiInProgress && Array.isArray(tags) && tags?.length >= 0) {
425
- setIsLoading(false);
426
- } else if (isAnyApiInProgress) {
427
- setIsLoading(true);
428
- }
429
- }, [loadingTags, tags, fetchingLiquidTags, createTemplateInProgress, fetchingCmsData]);
430
-
431
- // Handle content change from HTMLEditor
432
- const handleContentChange = useCallback((content) => {
433
- setHtmlContent(content);
434
-
435
- // Validate tags
436
- if (tags.length > 0 || !isEmpty(injectedTags)) {
437
- const validationResult = validateTags({
438
- content,
439
- tagsParam: tags,
440
- injectedTagsParams: injectedTags,
441
- location,
442
- tagModule: getDefaultTags,
443
- eventContextTags,
444
- });
445
-
446
- if (!validationResult.valid) {
447
- setTagValidationError(validationResult);
448
- } else {
449
- setTagValidationError(null);
450
- }
451
- }
452
- }, [tags, injectedTags, location, getDefaultTags, eventContextTags]);
453
-
454
- // Handle tag insertion into Subject field
455
- const handleSubjectTagSelect = useCallback((data) => {
456
- if (data) {
457
- const tagToInsert = `{{${data}}}`;
458
- const input = document.getElementById('template-subject') || document.querySelector('#template-subject input');
459
- let subjectValue = subject || '';
460
- try {
461
- if (input && (typeof input.selectionStart === 'number')) {
462
- const startPos = input.selectionStart;
463
- const endPos = input.selectionEnd;
464
- subjectValue = `${subjectValue.substring(0, startPos)}${tagToInsert}${subjectValue.substring(endPos)}`;
465
- setSubject(subjectValue);
466
- try {
467
- input.focus();
468
- const newPos = startPos + tagToInsert.length;
469
- input.selectionStart = newPos;
470
- input.selectionEnd = newPos;
471
- } catch (e) {
472
- // Ignore focus errors
473
- }
474
- } else {
475
- subjectValue = `${subjectValue}${tagToInsert}`;
476
- setSubject(subjectValue);
477
- if (input) {
478
- try {
479
- input.value = subjectValue;
480
- } catch (e) {
481
- // Ignore value setting errors
482
- }
483
- }
484
- }
485
- } catch (e) {
486
- // Fallback: safe append
487
- subjectValue = `${subjectValue}${tagToInsert}`;
488
- setSubject(subjectValue);
489
- }
490
- }
491
- }, [subject]);
492
-
493
- // Handle subject change
494
- const handleSubjectChange = useCallback((e) => {
495
- const newSubject = e.target.value;
496
- setSubject(newSubject);
497
- if (newSubject && subjectError) {
498
- setSubjectError('');
499
- }
500
- }, [subjectError]);
501
-
502
- // Handle Save/Update with liquid validation
503
- const handleSave = useCallback(() => {
504
- // 1. Validate Subject - BLOCKING (matches CK/BEE behavior)
505
- if (!subject || !subject.trim()) {
506
- const errorMessage = formatMessage(emailMessages["Email Subject cannot be empty."]);
507
- setSubjectError(errorMessage);
508
- // Reset parent state so next click is detected as a change
509
- if (onValidationFail) {
510
- onValidationFail();
511
- }
512
- // IMPORTANT: Return here to block save - matches CK/BEE editor behavior
513
- return;
514
- }
515
- // Clear error if subject is valid
516
- if (subjectError) {
517
- setSubjectError('');
518
- }
519
-
520
-
521
- // 2. Validate Content Tags
522
- // For NON-liquid orgs: BLOCKING validation (matches CK/BEE behavior)
523
- // For liquid orgs: Non-blocking (extractTags API will validate)
524
- if (tags.length > 0 || !isEmpty(injectedTags)) {
525
- const validationResult = validateTags({
526
- content: htmlContent,
527
- tagsParam: tags,
528
- injectedTagsParams: injectedTags,
529
- location,
530
- tagModule: getDefaultTags,
531
- eventContextTags,
532
- });
533
-
534
- const hasUnsupportedTags = validationResult?.unsupportedTags?.length > 0;
535
- if (!validationResult?.valid || hasUnsupportedTags) {
536
- setTagValidationError(validationResult);
537
-
538
- // IMPORTANT: For non-liquid orgs, block save (like CK/BEE editor)
539
- // For liquid orgs, continue (extractTags API will validate)
540
- if (!isLiquidEnabled) {
541
- // Show notification popup like CK/BEE editor
542
- const baseLanguage = get(currentOrgDetails, 'basic_details.base_language', 'en');
543
-
544
- const contentNotValidMsg = intl.formatMessage(formBuilderMessages.contentNotValidLanguage);
545
- let errorMessage = `${contentNotValidMsg} ${baseLanguage}`;
546
-
547
- if (hasUnsupportedTags) {
548
- const unsupportedTagsMsg = intl.formatMessage(formBuilderMessages.unsupportedTags);
549
- errorMessage += `\n${unsupportedTagsMsg} ${validationResult?.unsupportedTags?.join(', ')}`;
550
- }
551
- if (validationResult?.missingTags?.length > 0) {
552
- const missingTagsMsg = intl.formatMessage(formBuilderMessages.missingTags);
553
- errorMessage += `\n${missingTagsMsg} ${validationResult?.missingTags?.join(', ')}`;
554
- }
555
-
556
- const type = 'error';
557
- CapNotification[type]({
558
- message: `${type.toUpperCase()} ! ! ! `,
559
- description: errorMessage,
560
- duration: 5,
561
- });
562
-
563
- // Reset parent state so next click is detected as a change
564
- if (onValidationFail) {
565
- onValidationFail();
566
- }
567
- // Block save for non-liquid orgs
568
- return;
569
- }
570
- // For liquid orgs, just show warning and continue
571
- } else {
572
- // Clear tag errors if valid
573
- if (tagValidationError) {
574
- setTagValidationError(null);
575
- }
576
- }
577
- }
578
-
579
-
580
- const baseLanguage = get(currentOrgDetails, 'basic_details.base_language', 'en');
581
-
582
- // Actual save function - called after liquid validation (if enabled) or directly
583
- const performSave = () => {
584
- if (isFullMode) {
585
- // Full mode: Call email actions directly
586
- const templateDataFromRedux = Email?.templateDetails || Email?.BEETemplate;
587
- // IMPORTANT: Only use currentTemplateId from URL/params - don't fallback to Redux data
588
- // which might be stale from previous template in create mode
589
- const templateId = currentTemplateId;
590
- const isEditModeForSave = !!templateId;
591
-
592
- const langId = get(currentOrgDetails, `basic_details.languages.${baseLanguage}.lang_id`, '');
593
- const language = get(currentOrgDetails, `basic_details.languages.${baseLanguage}.language`, baseLanguage);
594
-
595
- // Generate or reuse tabKey
596
- let tabKey = _.uniqueId();
597
- if (isEditMode && templateDataFromRedux) {
598
- const existingTabKey = get(templateDataFromRedux, 'versions.base.tabKey')
599
- || get(templateDataFromRedux, 'base.tabKey')
600
- if (existingTabKey) {
601
- tabKey = existingTabKey;
602
- }
603
- }
604
-
605
- const languageData = {
606
- 'template-content': htmlContent || '',
607
- "is_drag_drop": false,
608
- "drag_drop_id": '',
609
- "lang_id": langId,
610
- "iso_code": baseLanguage,
611
- "language": language,
612
- "tabKey": tabKey,
613
- };
614
-
615
- const baseStructure = {
616
- [baseLanguage]: languageData,
617
- activeTab: baseLanguage,
618
- selectedLanguages: [baseLanguage],
619
- base: true,
620
- tabKey,
621
- subject: subject || '',
622
- };
623
-
624
- const historyEntry = {
625
- [baseLanguage]: { ...languageData },
626
- activeTab: baseLanguage,
627
- selectedLanguages: [baseLanguage],
628
- base: true,
629
- tabKey,
630
- subject: subject || '',
631
- };
632
-
633
- const finalTemplateName = isEditModeForSave ? (extractedTemplateName || '') : (templateName || '');
634
-
635
- const obj = {
636
- type: EMAIL,
637
- // In Edit mode, use extractedTemplateName; in Create mode, use templateName prop
638
- name: finalTemplateName,
639
- versions: {
640
- base: baseStructure,
641
- history: [historyEntry],
642
- },
643
- };
644
-
645
- if (isEditModeForSave && templateId) {
646
- obj._id = templateId;
647
- }
648
-
649
- if (emailActions?.transformEmailTemplate && emailActions?.createTemplate) {
650
- emailActions.transformEmailTemplate(obj, (newEmail) => {
651
- emailActions.createTemplate(newEmail, (createResponse) => {
652
- if (createResponse && createResponse.templateId) {
653
- const successMessage = formatMessage(emailMessages.emailCreateSuccess);
654
- CapNotification.success({ message: successMessage, key: 'create-template-success' });
655
-
656
- const module = location?.query?.module || 'default';
657
- const type = location?.query?.type;
658
- const isLanguageSupport = location?.query?.isLanguageSupport || false;
659
- const isBEESupport = (location?.query?.isBEESupport !== "false") || false;
660
-
661
- if (getFormdata) {
662
- const { versions, ...rest } = createResponse.templateId;
663
- getFormdata({ value: versions, ...rest, validity: true });
664
- } else {
665
- const queryParams = type === 'embedded'
666
- ? {
667
- type: 'embedded', module, isLanguageSupport, isBEESupport,
668
- }
669
- : { module, isLanguageSupport, isBEESupport };
670
-
671
- const searchParams = new URLSearchParams();
672
- Object.keys(queryParams).forEach((key) => {
673
- if (queryParams[key] !== undefined && queryParams[key] !== null) {
674
- searchParams.append(key, queryParams[key]);
675
- }
676
- });
677
-
678
- history.push({
679
- pathname: '/email',
680
- search: searchParams.toString() ? `?${searchParams.toString()}` : '',
681
- });
682
- }
683
- }
684
- });
685
- });
686
- }
687
- } else {
688
- // Library mode: Use getFormdata flow
689
- const tmpData = {
690
- html_content: htmlContent || '',
691
- is_drag_drop: false,
692
- lang_id: get(currentOrgDetails, `basic_details.languages.${baseLanguage}.lang_id`, ''),
693
- iso_code: baseLanguage,
694
- language: get(currentOrgDetails, `basic_details.languages.${baseLanguage}.language`, baseLanguage),
695
- };
696
-
697
- const libraryPayload = {
698
- base: tmpData,
699
- secondary_templates: [{ template_data: tmpData }],
700
- };
701
- libraryPayload.base.subject = subject || '';
702
-
703
- if (location?.query?.module === 'library' && isGetFormData && getFormdata) {
704
- const response = {
705
- action: "getFormData",
706
- postAction: 'next',
707
- id: get(Email, 'templateDetails._id', ''),
708
- value: libraryPayload,
709
- validity: true,
710
- type: EMAIL,
711
- };
712
- getFormdata(response);
713
- } else if (getFormdata) {
714
- getFormdata({
715
- value: libraryPayload,
716
- validity: true,
717
- type: EMAIL,
718
- });
719
- }
720
- }
721
- };
722
-
723
- // If liquid enabled, validate first using extractTags API
724
- if (isLiquidEnabled && getLiquidTags) {
725
- const onError = ({ standardErrors, liquidErrors }) => {
726
- if (showLiquidErrorInFooter) {
727
- showLiquidErrorInFooter({
728
- STANDARD_ERROR_MSG: standardErrors || [],
729
- LIQUID_ERROR_MSG: liquidErrors || [],
730
- });
731
- }
732
- // Don't reset ref here - liquid validation is async and resetting causes infinite loop
733
- // The parent's isGetFormData will be reset by onValidationFail, and the next click will be detected
734
- if (onValidationFail) {
735
- onValidationFail();
736
- }
737
- };
738
-
739
- const onSuccess = () => {
740
- performSave();
741
- };
742
-
743
- validateLiquidTemplateContent(htmlContent || '', {
744
- getLiquidTags: getLiquidTags
745
- ? (inputContent, callback) => getLiquidTags(inputContent, callback)
746
- : (inputContent, callback) => globalActions?.getLiquidTags?.(inputContent, callback),
747
- formatMessage: intl.formatMessage,
748
- messages: formBuilderMessages,
749
- onError,
750
- onSuccess,
751
- tagLookupMap: metaEntities?.tagLookupMap,
752
- eventContextTags,
753
- isLiquidFlow: true,
754
- forwardedTags: forwardedTags || {},
755
- });
756
- } else {
757
- performSave();
758
- }
759
- }, [
760
- subject,
761
- htmlContent,
762
- tags,
763
- injectedTags,
764
- location,
765
- getDefaultTags,
766
- eventContextTags,
767
- formatMessage,
768
- subjectError,
769
- isFullMode,
770
- currentOrgDetails,
771
- Email,
772
- currentTemplateId,
773
- templateDataProp,
774
- templateName,
775
- emailActions,
776
- getFormdata,
777
- isGetFormData,
778
- isLiquidEnabled,
779
- getLiquidTags,
780
- showLiquidErrorInFooter,
781
- metaEntities,
782
- forwardedTags,
783
- globalActions,
784
- intl,
785
- extractedTemplateName,
786
- ]);
787
-
788
- // Trigger save when isGetFormData becomes true (Create/Done button clicked)
789
- useEffect(() => {
790
- const isGetFormDataChanged = isGetFormData && !prevIsGetFormDataRef.current;
791
- const wasReset = !isGetFormData && prevIsGetFormDataRef.current;
792
-
793
- if (isGetFormDataChanged) {
794
- handleSave();
795
- }
796
-
797
- // Reset ref when parent resets isGetFormData (e.g., after validation failure)
798
- if (wasReset) {
799
- prevIsGetFormDataRef.current = false;
800
- return;
801
- }
802
-
803
- prevIsGetFormDataRef.current = isGetFormData;
804
- }, [isGetFormData, handleSave]);
805
-
806
- // Handle tag context change
807
- const handleOnTagsContextChange = useCallback((data) => {
808
- const { type, module } = location?.query || {};
809
- const isEmbedded = type === EMBEDDED;
810
- const contextValue = data || (isEmbedded ? module : DEFAULT);
811
- const query = {
812
- layout: EMAIL,
813
- type: TAG,
814
- context: contextValue ? contextValue.toLowerCase() : contextValue,
815
- embedded: isEmbedded ? type : FULL,
816
- };
817
- if (globalActions && globalActions.fetchSchemaForEntity) {
818
- globalActions.fetchSchemaForEntity(query);
819
- }
820
- }, [location, globalActions]);
821
-
822
-
823
-
824
- const spinTip = fetchingLiquidTags ? <FormattedMessage {...formBuilderMessages.liquidSpinText} /> : '';
825
-
826
- // Handle template name change
827
- // Call showTemplateName when templateName is available (for CreativesContainer header)
828
- // This matches the behavior of Email component which calls showTemplateName in onFormDataChange
829
- // In Edit mode, use extractedTemplateName from template data; in Create mode, use templateName prop
830
- const { onFormDataChange: onFormDataChangeProp } = props;
831
-
832
- // Create onFormDataChange callback that updates extractedTemplateName in Edit mode
833
- // This matches the Email component's onFormDataChange which updates its state
834
- const handleFormDataChange = useCallback((updatedFormData) => {
835
- const newTemplateName = updatedFormData?.['template-name'] || '';
836
-
837
- // In Edit mode, update extractedTemplateName state (similar to Email component updating its formData state)
838
- if (isEditMode && newTemplateName !== extractedTemplateName) {
839
- setExtractedTemplateName(newTemplateName);
840
- }
841
-
842
- // Call the parent's onFormDataChange if provided
843
- if (onFormDataChangeProp) {
844
- onFormDataChangeProp(updatedFormData);
845
- }
846
-
847
- // Call showTemplateName again with updated formData (same pattern as Email component)
848
- if (showTemplateName && isFullMode) {
849
- showTemplateName({ formData: updatedFormData, onFormDataChange: handleFormDataChange });
850
- }
851
- }, [isEditMode, extractedTemplateName, onFormDataChangeProp, showTemplateName, isFullMode]);
852
-
853
- useEffect(() => {
854
- if (showTemplateName && isFullMode) {
855
- // In Edit mode, use extractedTemplateName; in Create mode, use templateName prop
856
- const nameToUse = isEditMode ? extractedTemplateName : templateName;
857
- const formData = {
858
- 'template-name': nameToUse || '',
859
- };
860
- // Pass handleFormDataChange callback so CreativesContainer can update template name
861
- // This is the same pattern used in Email component
862
- showTemplateName({ formData, onFormDataChange: handleFormDataChange });
863
- }
864
- }, [showTemplateName, isFullMode, templateName, extractedTemplateName, isEditMode, handleFormDataChange]);
865
-
866
- return (
867
- <CapSpin spinning={isLoading} tip={spinTip}>
868
- <CapRow>
869
- <CapColumn span={24}>
870
- {/* Subject Field */}
871
- <CapColumn span={24} style={{ marginBottom: CAP_SPACE_16 }}>
872
- <CapTagListWithInput
873
- inputId="template-subject"
874
- inputValue={subject}
875
- inputOnChange={handleSubjectChange}
876
- inputPlaceholder={formatMessage(messages.enterEmailSubject)}
877
- inputRequired
878
- inputErrorMessage={subjectError}
879
- headingText={formatMessage(messages.subject)}
880
- headingType="h4"
881
- headingStyle={{ marginRight: '85%' }}
882
- onTagSelect={handleSubjectTagSelect}
883
- onContextChange={handleOnTagsContextChange}
884
- location={location}
885
- tags={tags}
886
- injectedTags={injectedTags || {}}
887
- selectedOfferDetails={selectedOfferDetails}
888
- eventContextTags={eventContextTags}
889
- showHeading
890
- showTagList
891
- showInput
892
- containerStyle={{
893
- display: 'flex',
894
- flexDirection: 'column',
895
- }}
896
- popoverPlacement="bottomRight"
897
- />
898
- </CapColumn>
899
-
900
- {/* Tag Validation Error Display */}
901
- {/* {tagValidationError && !tagValidationError.valid && (
902
- <CapColumn span={24} style={{ marginBottom: CAP_SPACE_16 }}>
903
- <CapNotification
904
- type="error"
905
- message={formatMessage({
906
- id: 'creatives.containersV2.Email.tagValidationError',
907
- defaultMessage: 'Tag validation error',
908
- })}
909
- description={(
910
- <CapRow>
911
- {tagValidationError?.missingTags?.length > 0 && (
912
- <CapColumn span={24}>
913
- Missing tags:
914
- {' '}
915
- {tagValidationError.missingTags.join(', ')}
916
- </CapColumn>
917
- )}
918
- {tagValidationError?.unsupportedTags?.length > 0 && (
919
- <CapColumn span={24}>
920
- Unsupported tags:
921
- {' '}
922
- {tagValidationError.unsupportedTags.join(', ')}
923
- </CapColumn>
924
- )}
925
- {tagValidationError?.isBraceError && (
926
- <CapColumn span={24}>
927
- Unbalanced brackets detected
928
- </CapColumn>
929
- )}
930
- </CapRow>
931
- )}
932
- />
933
- </CapColumn>
934
- )} */}
935
-
936
- <HTMLEditor
937
- variant="email"
938
- initialContent={loadedHtmlContent || ''}
939
- onContentChange={handleContentChange}
940
- onSave={handleSave}
941
- readOnly={isReadOnly}
942
- showFullscreenButton
943
- autoSave={false}
944
- tags={tags}
945
- injectedTags={injectedTags}
946
- location={location}
947
- eventContextTags={eventContextTags}
948
- selectedOfferDetails={selectedOfferDetails}
949
- channel={EMAIL}
950
- userLocale={intl.locale || 'en'}
951
- moduleFilterEnabled={location?.query?.type !== EMBEDDED}
952
- onTagContextChange={handleOnTagsContextChange}
953
- />
954
- </CapColumn>
955
- </CapRow>
956
- </CapSpin>
957
- );
958
- };
959
-
960
- EmailHTMLEditor.propTypes = {
961
- intl: PropTypes.object.isRequired,
962
- location: PropTypes.object,
963
- params: PropTypes.object,
964
- getDefaultTags: PropTypes.string,
965
- supportedTags: PropTypes.array,
966
- metaEntities: PropTypes.object,
967
- injectedTags: PropTypes.object,
968
- globalActions: PropTypes.object,
969
- loadingTags: PropTypes.bool,
970
- eventContextTags: PropTypes.array,
971
- forwardedTags: PropTypes.object,
972
- selectedOfferDetails: PropTypes.array,
973
- currentOrgDetails: PropTypes.object,
974
- isReadOnly: PropTypes.bool,
975
- fetchingLiquidTags: PropTypes.bool,
976
- createTemplateInProgress: PropTypes.bool,
977
- fetchingCmsData: PropTypes.bool,
978
- // Email Redux state
979
- Email: PropTypes.object,
980
- // Email actions
981
- emailActions: PropTypes.object,
982
- // Full mode props
983
- isFullMode: PropTypes.bool,
984
- templateName: PropTypes.string,
985
- showTemplateName: PropTypes.func,
986
- onFormDataChange: PropTypes.func,
987
- isGetFormData: PropTypes.bool,
988
- getFormdata: PropTypes.func,
989
- // Library mode props
990
- templateData: PropTypes.object,
991
- // Uploaded content from zip file
992
- EmailLayout: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), // eslint-disable-line react/require-default-props
993
- // Liquid validation
994
- getLiquidTags: PropTypes.func,
995
- showLiquidErrorInFooter: PropTypes.func,
996
- // Preview/Test
997
- // Parent loading control
998
- setIsLoadingContent: PropTypes.func,
999
- };
1000
-
1001
- EmailHTMLEditor.defaultProps = {
1002
- location: {},
1003
- params: {},
1004
- getDefaultTags: null,
1005
- supportedTags: [],
1006
- showTemplateName: null,
1007
- metaEntities: {},
1008
- injectedTags: {},
1009
- globalActions: {},
1010
- loadingTags: false,
1011
- eventContextTags: [],
1012
- forwardedTags: {},
1013
- selectedOfferDetails: [],
1014
- currentOrgDetails: {},
1015
- isReadOnly: false,
1016
- fetchingLiquidTags: false,
1017
- createTemplateInProgress: false,
1018
- fetchingCmsData: false,
1019
- Email: {},
1020
- emailActions: {},
1021
- isFullMode: false,
1022
- templateName: '',
1023
- onFormDataChange: null,
1024
- isGetFormData: false,
1025
- getFormdata: null,
1026
- templateData: null,
1027
- getLiquidTags: null,
1028
- showLiquidErrorInFooter: null,
1029
- setIsLoadingContent: null,
1030
- };
1031
-
1032
- const EmailHTMLEditorWithIntl = injectIntl(EmailHTMLEditor);
1033
-
1034
- export default forwardRef((props, ref) => <EmailHTMLEditorWithIntl {...props} forwardedRef={ref} />);