@capillarytech/creatives-library 9.0.61-alpha.3 → 9.0.62

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 (59) hide show
  1. package/constants/unified.js +8 -2
  2. package/package.json +1 -1
  3. package/services/api.js +5 -0
  4. package/services/tests/api.test.js +44 -0
  5. package/utils/common.js +8 -0
  6. package/utils/downloadFile.js +57 -0
  7. package/utils/tests/downloadFile.test.js +219 -0
  8. package/v2Components/FormBuilder/Classic.js +43 -9
  9. package/v2Components/FormBuilder/Functional/FormBuilderShell.js +324 -69
  10. package/v2Components/FormBuilder/Functional/channels/mobilepush/buildSubmitPayload.js +10 -0
  11. package/v2Components/FormBuilder/Functional/channels/mobilepush/config.js +75 -0
  12. package/v2Components/FormBuilder/Functional/channels/mobilepush/getEditorErrorDescriptor.js +74 -0
  13. package/v2Components/FormBuilder/Functional/channels/mobilepush/index.js +28 -0
  14. package/v2Components/FormBuilder/Functional/channels/mobilepush/modals.js +21 -0
  15. package/v2Components/FormBuilder/Functional/channels/mobilepush/runSubmitPipeline.js +45 -0
  16. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/getEditorErrorDescriptor.test.js +162 -0
  17. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/modals.test.js +37 -0
  18. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/runSubmitPipeline.test.js +70 -0
  19. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/validate.test.js +274 -0
  20. package/v2Components/FormBuilder/Functional/channels/mobilepush/validate.js +251 -0
  21. package/v2Components/FormBuilder/Functional/channels/registry.js +2 -0
  22. package/v2Components/FormBuilder/Functional/constants.js +46 -8
  23. package/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js +175 -28
  24. package/v2Components/FormBuilder/Functional/core/store/formReducer.js +75 -6
  25. package/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js +10 -20
  26. package/v2Components/FormBuilder/Functional/layout/FieldSlot.js +9 -1
  27. package/v2Components/FormBuilder/Functional/layout/SchemaForm.js +20 -3
  28. package/v2Components/FormBuilder/Functional/layout/Section.js +6 -1
  29. package/v2Components/FormBuilder/Functional/layout/TabsContainer.js +85 -0
  30. package/v2Components/FormBuilder/Functional/renderers/mpushRenderers.js +451 -0
  31. package/v2Components/FormBuilder/Functional/tests/fieldSlot.test.js +9 -2
  32. package/v2Components/FormBuilder/Functional/tests/manual.typingPerf.test.js +135 -0
  33. package/v2Components/FormBuilder/Functional/tests/mpush.crossFlowParity.test.js +430 -0
  34. package/v2Components/FormBuilder/Functional/tests/mpush.ctaFlows.parity.test.js +320 -0
  35. package/v2Components/FormBuilder/Functional/tests/mpush.editContainer.parity.test.js +148 -0
  36. package/v2Components/FormBuilder/Functional/tests/mpush.engine.test.js +306 -0
  37. package/v2Components/FormBuilder/Functional/tests/mpush.libraryValidity.parity.test.js +152 -0
  38. package/v2Components/FormBuilder/Functional/tests/mpush.shellEvents.test.js +200 -0
  39. package/v2Components/FormBuilder/Functional/tests/mpushRenderers.test.js +382 -0
  40. package/v2Components/FormBuilder/Functional/tests/schemaForm.test.js +17 -0
  41. package/v2Components/FormBuilder/Functional/tests/tabsContainer.test.js +112 -0
  42. package/v2Components/FormBuilder/_formBuilder.scss +11 -0
  43. package/v2Components/FormBuilder/index.js +27 -17
  44. package/v2Components/FormBuilder/tests/entryGate.test.js +90 -7
  45. package/v2Components/FormBuilder/tests/mpush.characterization.test.js +459 -0
  46. package/v2Containers/BeeEditor/index.js +8 -0
  47. package/v2Containers/Email/index.js +118 -16
  48. package/v2Containers/Email/initialSchema.js +25 -3
  49. package/v2Containers/Email/messages.js +16 -0
  50. package/v2Containers/Email/tests/index.test.js +597 -0
  51. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +55 -1
  52. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +141 -0
  53. package/v2Containers/EmailWrapper/components/_emailHTMLEditor.scss +11 -0
  54. package/v2Containers/Templates/index.js +53 -0
  55. package/v2Containers/Templates/messages.js +8 -0
  56. package/v2Containers/Templates/tests/index.test.js +170 -0
  57. package/v2Containers/Viber/constants.js +0 -3
  58. package/v2Containers/Viber/sagas.js +3 -1
  59. package/v2Containers/Viber/tests/saga.test.js +21 -0
@@ -35,6 +35,9 @@ export const DEFAULT = 'default';
35
35
  export const DEFAULT_MODULE = 'creatives';
36
36
  export const DEFAULT_LOCALE = 'en';
37
37
 
38
+ export const EXPORT_HTML_LABEL_ID = 'export-html-label';
39
+ export const DISPLAY_NONE = 'none';
40
+
38
41
  // --- Feature Flags & Validation ---
39
42
  export const STORE2DOOR_PLUS_ENABLED = 'STORE2DOOR_PLUS_ENABLED';
40
43
  export const TRAI_DLT = 'TRAI_DLT';
@@ -45,9 +48,10 @@ export const GIFT_CARDS = 'GIFT_CARDS';
45
48
  export const PROMO_ENGINE = 'PROMO_ENGINE';
46
49
  export const ENABLE_NEW_MPUSH = 'ENABLE_NEW_MPUSH';
47
50
  export const ENABLE_NEW_EDITOR_FLOW_INAPP = 'ENABLE_NEW_EDITOR_FLOW_INAPP';
48
- // Per-channel flag for the FormBuilder V3 migration. Routes SMS templates to the
49
- // new functional FormBuilder; later channels get their own flags (…_MPUSH, …_EMAIL).
51
+ // Per-channel flags for the FormBuilder V3 migration. Each routes its channel to
52
+ // the new functional FormBuilder; later channels get their own flags (…_EMAIL).
50
53
  export const ENABLE_NEW_FORMBUILDER_SMS = 'ENABLE_NEW_FORMBUILDER_SMS';
54
+ export const ENABLE_NEW_FORMBUILDER_MPUSH = 'ENABLE_NEW_FORMBUILDER_MPUSH';
51
55
  export const SUPPORT_CK_EDITOR = 'SUPPORT_CK_EDITOR';
52
56
  export const CUSTOM_TAG = 'CustomTagMessage';
53
57
  export const CUSTOMER_EXTENDED_FIELD = 'Customer extended fields';
@@ -159,6 +163,8 @@ export const UNSUBSCRIBE_TAG_REGEX = new RegExp(`\\{\\{\\s*${UNSUBSCRIBE_TAG}\\s
159
163
  /** Matches {{ ... }} and captures the inner tag content (for scanning content for tags). */
160
164
  export const TAG_CONTENT_REGEX = /{{([^}]+)}}/g;
161
165
  export const ENTRY_TRIGGER_TAG_REGEX = /\bentryTrigger\.\w+(?:\.\w+)?(?:\(\w+\))?/g;
166
+ /** Strips characters not allowed in a downloaded file name, keeping alphanumerics, `-`, `_` and spaces. */
167
+ export const SAFE_FILE_NAME_REGEX = /[^a-zA-Z0-9-_ ]/g;
162
168
  export const SKIP_TAGS_REGEX_GROUPS = ["dynamic_expiry_date_after_\\d+_days.FORMAT_\\d", "unsubscribe\\(#[a-zA-Z\\d]{6}\\)", "Link_to_[a-zA-Z]", "SURVEY.*.TOKEN", "^[A-Za-z].*\\([a-zA-Z\\d]*\\)", "referral_unique_(code|url).*userid"];
163
169
 
164
170
  // --- Template variable tokens (`{{var}}`, DLT `{#var#}`) ---
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "9.0.61-alpha.3",
4
+ "version": "9.0.62",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
package/services/api.js CHANGED
@@ -505,6 +505,11 @@ export const getCmsTemplateSettingsV2 = (cmsType, projectId, cmsMode, langId, is
505
505
  return API.get(url);
506
506
  };
507
507
 
508
+ export const exportBeeTemplateHtml = (jsonContent) => {
509
+ const url = `${API_ENDPOINT}/cms/v2/exportHtml`;
510
+ return request(url, getAPICallObject('POST', { jsonContent }));
511
+ };
512
+
508
513
  export const getCmsAccounts = (cmsType) => {
509
514
  const url = `${API_ENDPOINT}/cms/accounts?type=${cmsType}`;
510
515
  return API.get(url);
@@ -33,6 +33,7 @@ import {
33
33
  createTestCustomer,
34
34
  getCommDefinition,
35
35
  getCommDefinitionVersion,
36
+ exportBeeTemplateHtml,
36
37
  } from '../api';
37
38
  import { mockData } from './mockData';
38
39
  import getSchema from '../getSchema';
@@ -1298,3 +1299,46 @@ describe('getCommDefinitionVersion', () => {
1298
1299
  expect(lastCall[1].method).toBe('GET');
1299
1300
  });
1300
1301
  });
1302
+
1303
+ describe('exportBeeTemplateHtml', () => {
1304
+ beforeEach(() => {
1305
+ global.fetch = jest.fn();
1306
+ });
1307
+
1308
+ afterEach(() => {
1309
+ jest.restoreAllMocks();
1310
+ });
1311
+
1312
+ it('POSTs the given jsonContent to the exportHtml endpoint and resolves the parsed response', async () => {
1313
+ global.fetch.mockReturnValue(Promise.resolve({
1314
+ status: 200,
1315
+ json: () => Promise.resolve({ response: { html: '<html>ok</html>' } }),
1316
+ }));
1317
+
1318
+ const result = await exportBeeTemplateHtml({ page: { rows: [] } });
1319
+
1320
+ expect(global.fetch).toHaveBeenCalledTimes(1);
1321
+ const [calledUrl, calledOptions] = global.fetch.mock.calls[0];
1322
+ expect(calledUrl).toContain('/cms/v2/exportHtml');
1323
+ expect(calledOptions.method).toBe('POST');
1324
+ expect(JSON.parse(calledOptions.body)).toEqual({ jsonContent: { page: { rows: [] } } });
1325
+ expect(result).toEqual({ response: { html: '<html>ok</html>' } });
1326
+ });
1327
+
1328
+ it('wraps a jsonContent string as-is (no parsing) in the request body', async () => {
1329
+ global.fetch.mockReturnValue(Promise.resolve({
1330
+ status: 200,
1331
+ json: () => Promise.resolve({ response: { html: '<html>ok</html>' } }),
1332
+ }));
1333
+
1334
+ await exportBeeTemplateHtml('{"page":{"rows":[]}}');
1335
+
1336
+ const [, calledOptions] = global.fetch.mock.calls[0];
1337
+ expect(JSON.parse(calledOptions.body)).toEqual({ jsonContent: '{"page":{"rows":[]}}' });
1338
+ });
1339
+
1340
+ it('resolves (rather than rejects) when the underlying request fails', () => {
1341
+ global.fetch.mockReturnValue(Promise.reject(new Error('network error')));
1342
+ return expect(exportBeeTemplateHtml({})).toEqual(Promise.resolve());
1343
+ });
1344
+ });
package/utils/common.js CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  ENABLE_NEW_MPUSH,
28
28
  ENABLE_NEW_EDITOR_FLOW_INAPP,
29
29
  ENABLE_NEW_FORMBUILDER_SMS,
30
+ ENABLE_NEW_FORMBUILDER_MPUSH,
30
31
  SUPPORT_ENGAGEMENT_MODULE,
31
32
  ENABLE_CREATIVES_ARCHIVAL,
32
33
  } from '../constants/unified';
@@ -166,6 +167,13 @@ export const hasNewFormBuilderEnabledForSms = Auth.hasFeatureAccess.bind(
166
167
  ENABLE_NEW_FORMBUILDER_SMS,
167
168
  );
168
169
 
170
+ // FormBuilder V3 — per-org flag that routes the MOBILEPUSH channel to the new
171
+ // functional FormBuilder. Off => the legacy class implementation (Classic).
172
+ export const hasNewFormBuilderEnabledForMpush = Auth.hasFeatureAccess.bind(
173
+ null,
174
+ ENABLE_NEW_FORMBUILDER_MPUSH,
175
+ );
176
+
169
177
  //filtering tags based on scope
170
178
  export const filterTags = (tagsToFilter, tagsList) => tagsList?.filter(
171
179
  (tag) => !tagsToFilter?.includes(tag?.definition?.value)
@@ -0,0 +1,57 @@
1
+ import { exportBeeTemplateHtml } from '../services/api';
2
+ import { SAFE_FILE_NAME_REGEX } from 'constants/unified';
3
+ import emailMessages from '../v2Containers/Email/messages';
4
+
5
+ export function downloadHtmlFile(html, filename) {
6
+ const blob = new Blob([html], { type: 'text/html' });
7
+ const blobUrl = URL.createObjectURL(blob);
8
+ const link = document.createElement('a');
9
+ link.href = blobUrl;
10
+ link.download = filename;
11
+ document.body.appendChild(link);
12
+ link.click();
13
+ document.body.removeChild(link);
14
+ URL.revokeObjectURL(blobUrl);
15
+ }
16
+
17
+ export function sanitizeFileName(name) {
18
+ return (name || 'template').replace(SAFE_FILE_NAME_REGEX, '').trim() || 'template';
19
+ }
20
+
21
+ // Shared by the schema-driven FormBuilder popover (Email/index.js) and the in-house HTML editor
22
+ export function getExportHtmlDisabledTooltip(isEditMode, isContentLocked, formatMessage) {
23
+ if (!isEditMode) return formatMessage(emailMessages.exportHtmlDisabledCreateTooltip);
24
+ if (isContentLocked) return formatMessage(emailMessages.exportHtmlDisabledEditedTooltip);
25
+ return '';
26
+ }
27
+
28
+ // Resolves an EMAIL template's base-language content and downloads it as HTML - either directly
29
+ // (CKEditor) or by converting the Bee design JSON via the backend first. Callers own error
30
+ // notification (via .catch) so each keeps its own module-scoped i18n message.
31
+ export function exportEmailTemplateHtml(templateDetails, filename, baseLanguage) {
32
+ try {
33
+ const baseVersion = templateDetails?.versions?.base || {};
34
+ const languageContent = baseVersion[baseLanguage] || baseVersion['en'] || {};
35
+
36
+ if (languageContent.is_drag_drop) {
37
+ const jsonContent = languageContent['json-content'];
38
+ return exportBeeTemplateHtml(jsonContent).then((result) => {
39
+ const html = result?.response?.html;
40
+ if (!html) {
41
+ return Promise.reject(new Error('exportBeeTemplateHtml returned no html'));
42
+ }
43
+ downloadHtmlFile(html, filename);
44
+ return { html, filename };
45
+ });
46
+ }
47
+
48
+ const html = languageContent['template-content'];
49
+ if (!html) {
50
+ return Promise.reject(new Error('No template-content found for the selected language'));
51
+ }
52
+ downloadHtmlFile(html, filename);
53
+ return Promise.resolve({ html, filename });
54
+ } catch (error) {
55
+ return Promise.reject(error);
56
+ }
57
+ }
@@ -0,0 +1,219 @@
1
+ import { exportBeeTemplateHtml } from '../../services/api';
2
+ import { downloadHtmlFile, sanitizeFileName, exportEmailTemplateHtml } from '../downloadFile';
3
+
4
+ jest.mock('../../services/api', () => ({
5
+ exportBeeTemplateHtml: jest.fn(),
6
+ }));
7
+
8
+ describe('downloadHtmlFile', () => {
9
+ let createObjectURLMock;
10
+ let revokeObjectURLMock;
11
+ let clickMock;
12
+ let capturedLink;
13
+
14
+ beforeEach(() => {
15
+ createObjectURLMock = jest.fn(() => 'blob:mock-url');
16
+ revokeObjectURLMock = jest.fn();
17
+ global.URL.createObjectURL = createObjectURLMock;
18
+ global.URL.revokeObjectURL = revokeObjectURLMock;
19
+ clickMock = jest.fn();
20
+ capturedLink = null;
21
+ const originalCreateElement = document.createElement.bind(document);
22
+ jest.spyOn(document, 'createElement').mockImplementation((tagName) => {
23
+ const el = originalCreateElement(tagName);
24
+ if (tagName === 'a') {
25
+ el.click = clickMock;
26
+ capturedLink = el;
27
+ }
28
+ return el;
29
+ });
30
+ });
31
+
32
+ afterEach(() => {
33
+ jest.restoreAllMocks();
34
+ });
35
+
36
+ it('creates a Blob, triggers a click on a temporary link, and revokes the object URL', () => {
37
+ downloadHtmlFile('<html>hi</html>', 'my-template.html');
38
+
39
+ expect(createObjectURLMock).toHaveBeenCalledTimes(1);
40
+ const [blobArg] = createObjectURLMock.mock.calls[0];
41
+ expect(blobArg.type).toBe('text/html');
42
+
43
+ expect(clickMock).toHaveBeenCalledTimes(1);
44
+ expect(revokeObjectURLMock).toHaveBeenCalledWith('blob:mock-url');
45
+ });
46
+
47
+ it('sets the download filename and href on the temporary link before clicking', () => {
48
+ downloadHtmlFile('<html>hi</html>', 'export.html');
49
+
50
+ expect(capturedLink.download).toBe('export.html');
51
+ expect(capturedLink.href).toContain('blob:mock-url');
52
+ });
53
+ });
54
+
55
+ describe('sanitizeFileName', () => {
56
+ it('strips characters outside the allowed set (alphanumeric, dash, underscore, space)', () => {
57
+ expect(sanitizeFileName('My Template! @#2024')).toBe('My Template 2024');
58
+ });
59
+
60
+ it('trims leading/trailing whitespace left after stripping', () => {
61
+ expect(sanitizeFileName(' Weekly Promo!! ')).toBe('Weekly Promo');
62
+ });
63
+
64
+ it('falls back to "template" when name is empty', () => {
65
+ expect(sanitizeFileName('')).toBe('template');
66
+ });
67
+
68
+ it('falls back to "template" when name is null/undefined', () => {
69
+ expect(sanitizeFileName(null)).toBe('template');
70
+ expect(sanitizeFileName(undefined)).toBe('template');
71
+ });
72
+
73
+ it('falls back to "template" when name contains only disallowed characters', () => {
74
+ expect(sanitizeFileName('!!!///???')).toBe('template');
75
+ });
76
+
77
+ it('keeps dashes, underscores, and spaces', () => {
78
+ expect(sanitizeFileName('my-file_name here')).toBe('my-file_name here');
79
+ });
80
+ });
81
+
82
+ describe('exportEmailTemplateHtml', () => {
83
+ let createObjectURLMock;
84
+ let revokeObjectURLMock;
85
+ let clickMock;
86
+
87
+ beforeEach(() => {
88
+ jest.clearAllMocks();
89
+ createObjectURLMock = jest.fn(() => 'blob:mock-url');
90
+ revokeObjectURLMock = jest.fn();
91
+ global.URL.createObjectURL = createObjectURLMock;
92
+ global.URL.revokeObjectURL = revokeObjectURLMock;
93
+ clickMock = jest.fn();
94
+ const originalCreateElement = document.createElement.bind(document);
95
+ jest.spyOn(document, 'createElement').mockImplementation((tagName) => {
96
+ const el = originalCreateElement(tagName);
97
+ if (tagName === 'a') {
98
+ el.click = clickMock;
99
+ }
100
+ return el;
101
+ });
102
+ });
103
+
104
+ afterEach(() => {
105
+ jest.restoreAllMocks();
106
+ });
107
+
108
+ it('downloads the CKEditor (non drag-drop) content directly for the resolved base language', async () => {
109
+ const templateDetails = {
110
+ versions: {
111
+ base: {
112
+ en: { is_drag_drop: false, 'template-content': '<p>Hello</p>' },
113
+ },
114
+ },
115
+ };
116
+
117
+ await exportEmailTemplateHtml(templateDetails, 'file.html', 'en');
118
+
119
+ expect(exportBeeTemplateHtml).not.toHaveBeenCalled();
120
+ expect(clickMock).toHaveBeenCalledTimes(1);
121
+ });
122
+
123
+ it('falls back to the "en" language content when baseLanguage is not present in the base version', async () => {
124
+ const templateDetails = {
125
+ versions: {
126
+ base: {
127
+ en: { is_drag_drop: false, 'template-content': '<p>English fallback</p>' },
128
+ },
129
+ },
130
+ };
131
+
132
+ await exportEmailTemplateHtml(templateDetails, 'file.html', 'fr');
133
+
134
+ expect(clickMock).toHaveBeenCalledTimes(1);
135
+ });
136
+
137
+ it('rejects instead of downloading an empty file when neither baseLanguage nor "en" content exists', async () => {
138
+ const templateDetails = { versions: { base: {} } };
139
+
140
+ await expect(exportEmailTemplateHtml(templateDetails, 'file.html', 'fr')).rejects.toThrow(
141
+ 'No template-content found for the selected language'
142
+ );
143
+
144
+ expect(clickMock).not.toHaveBeenCalled();
145
+ expect(exportBeeTemplateHtml).not.toHaveBeenCalled();
146
+ });
147
+
148
+ it('converts Bee design JSON to HTML via the backend and downloads the result when is_drag_drop is true', async () => {
149
+ const jsonContent = { page: { rows: [] } };
150
+ const templateDetails = {
151
+ versions: {
152
+ base: {
153
+ en: { is_drag_drop: true, 'json-content': jsonContent },
154
+ },
155
+ },
156
+ };
157
+ exportBeeTemplateHtml.mockResolvedValue({ response: { html: '<html>bee</html>' } });
158
+
159
+ await exportEmailTemplateHtml(templateDetails, 'file.html', 'en');
160
+
161
+ expect(exportBeeTemplateHtml).toHaveBeenCalledWith(jsonContent);
162
+ expect(clickMock).toHaveBeenCalledTimes(1);
163
+ });
164
+
165
+ it('rejects when the Bee export API resolves without an html field', async () => {
166
+ const templateDetails = {
167
+ versions: {
168
+ base: {
169
+ en: { is_drag_drop: true, 'json-content': {} },
170
+ },
171
+ },
172
+ };
173
+ exportBeeTemplateHtml.mockResolvedValue({ response: {} });
174
+
175
+ await expect(exportEmailTemplateHtml(templateDetails, 'file.html', 'en')).rejects.toThrow(
176
+ 'exportBeeTemplateHtml returned no html'
177
+ );
178
+ expect(clickMock).not.toHaveBeenCalled();
179
+ });
180
+
181
+ it('propagates a rejection from the Bee export API without downloading', async () => {
182
+ const templateDetails = {
183
+ versions: {
184
+ base: {
185
+ en: { is_drag_drop: true, 'json-content': {} },
186
+ },
187
+ },
188
+ };
189
+ const apiError = new Error('network down');
190
+ exportBeeTemplateHtml.mockRejectedValue(apiError);
191
+
192
+ await expect(exportEmailTemplateHtml(templateDetails, 'file.html', 'en')).rejects.toBe(apiError);
193
+ expect(clickMock).not.toHaveBeenCalled();
194
+ });
195
+
196
+ it('rejects (via the optional chaining fallback to {}) when templateDetails is missing entirely', async () => {
197
+ await expect(exportEmailTemplateHtml(undefined, 'file.html', 'en')).rejects.toThrow(
198
+ 'No template-content found for the selected language'
199
+ );
200
+ expect(clickMock).not.toHaveBeenCalled();
201
+ expect(exportBeeTemplateHtml).not.toHaveBeenCalled();
202
+ });
203
+
204
+ it('rejects (instead of throwing) when the synchronous download itself throws', async () => {
205
+ const templateDetails = {
206
+ versions: {
207
+ base: {
208
+ en: { is_drag_drop: false, 'template-content': '<p>content</p>' },
209
+ },
210
+ },
211
+ };
212
+ const downloadError = new Error('createObjectURL exploded');
213
+ createObjectURLMock.mockImplementation(() => {
214
+ throw downloadError;
215
+ });
216
+
217
+ await expect(exportEmailTemplateHtml(templateDetails, 'file.html', 'en')).rejects.toBe(downloadError);
218
+ });
219
+ });
@@ -37,6 +37,7 @@ import LabelHOC from '@capillarytech/cap-ui-library/assets/HOCs/ComponentWithLab
37
37
  import { createStructuredSelector } from 'reselect';
38
38
  import { CAP_SPACE_12, CAP_SPACE_08, FONT_COLOR_05, FONT_COLOR_04 } from '@capillarytech/cap-ui-library/styled/variables';
39
39
  import UnifiedPreview from '../CommonTestAndPreview/UnifiedPreview';
40
+ import { EXPORT_HTML_LABEL_ID } from '../../constants/unified';
40
41
  import TemplatePreview from '../TemplatePreview';
41
42
  import { ANDROID } from '../CommonTestAndPreview/constants';
42
43
  import TagList from '../../v2Containers/TagList';
@@ -3308,20 +3309,36 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3308
3309
  switch (type) {
3309
3310
  case "span":
3310
3311
  return <span id={`${val.id}${childIndex > 1 ? childIndex : ''}`}>{val.dynamicTab ? tabName : val.value}</span>;
3311
- case "div":
3312
+ case "div": {
3312
3313
  let children = _.get(this.state, `formData[${this.state.currentTab - 1}][${val.id}]`) || value;
3313
3314
  if (_.isEmpty(value) && !_.isEmpty(val.value)) {
3314
3315
  children = val.value;
3315
3316
  }
3316
3317
  const ref = _.get(this, `props.refs[${val.id}]`);
3317
- return (<div
3318
- ref={ref}
3319
- className={val.className ? val.className : ''}
3320
- id={`${val.id}${childIndex > 1 ? childIndex : ''}`}
3321
- onClick={(data) => this.callChildEvent(data, val, val.submitAction)}
3322
- style={val.style ? val.style : {}}
3323
- onInput={(e) => { e.stopPropagation(); this.callChildEvent(e.target.textContent, val, 'onChange'); }}
3324
- >{children}</div>);
3318
+ // Disabled state comes from exportHtmlLocked prop, not schema mutation, so Bee editors elsewhere don't see a schema diff and reinitialize (see Email/index.js).
3319
+ let computedStyle = val.style ?? {};
3320
+ if (val.id === EXPORT_HTML_LABEL_ID && this.props?.exportHtmlLocked) {
3321
+ computedStyle = { ...computedStyle, opacity: 0.5, cursor: 'not-allowed', pointerEvents: 'none' };
3322
+ }
3323
+ const divElement = (
3324
+ <div
3325
+ ref={ref}
3326
+ className={val.className ? val.className : ''}
3327
+ id={`${val.id}${childIndex > 1 ? childIndex : ''}`}
3328
+ onClick={(data) => this.callChildEvent(data, val, val.submitAction)}
3329
+ style={computedStyle}
3330
+ onInput={(e) => { e.stopPropagation(); this.callChildEvent(e.target.textContent, val, 'onChange'); }}
3331
+ >{children}</div>
3332
+ );
3333
+ if (val.id === EXPORT_HTML_LABEL_ID && this.props?.exportHtmlTooltip) {
3334
+ return (
3335
+ <CapTooltip title={this.props.exportHtmlTooltip} placement="topLeft">
3336
+ {divElement}
3337
+ </CapTooltip>
3338
+ );
3339
+ }
3340
+ return divElement;
3341
+ }
3325
3342
  default:
3326
3343
  return '';
3327
3344
  }
@@ -4000,6 +4017,22 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
4000
4017
  </CapColumn>
4001
4018
  );
4002
4019
  break;
4020
+
4021
+ case "cap-icon":
4022
+ columns.push(
4023
+ <CapColumn
4024
+ key={`icon-${val.id}`}
4025
+ id={val.id}
4026
+ offset={val.offset}
4027
+ width={val.width ? val.width : 1}
4028
+ onClick={(data) => this.callChildEvent(data, val, val.submitAction)}
4029
+ className={val.className ? val.className : ''}
4030
+ style={val.colStyle ? val.colStyle : {}}
4031
+ >
4032
+ <CapIcon type={val.value} rotate={val.rotate} style={val.style ? val.style : {}} />
4033
+ </CapColumn>
4034
+ );
4035
+ break;
4003
4036
  case "select": {
4004
4037
  ifError = this.state.checkValidation && (isVersionEnable ? this.state.errorData[`${this.state.currentTab - 1}`][val.id] : this.state.errorData[val.id]);
4005
4038
  const options = (this.state.formData[`${val.id}-options`]) ? (this.state.formData[`${val.id}-options`]) : val.options;
@@ -4118,6 +4151,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
4118
4151
  waitEventContextTags={this.props?.waitEventContextTags}
4119
4152
  isGetBeeData={this.props?.isGetBeeData}
4120
4153
  getBEEData={this.props?.getBEEData}
4154
+ onContentChange={this.props?.onContentChange}
4121
4155
  />
4122
4156
  </CapColumn>
4123
4157
  );