@capillarytech/creatives-library 8.0.164 → 8.0.166

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.164",
4
+ "version": "8.0.166",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -6,13 +6,15 @@ export default [
6
6
  {
7
7
  key: 'navigation',
8
8
  saga: function* navigationConfigSaga() {
9
- yield all(CapCollapsibleLeftNavigationSagas.map(saga => saga.call()));
9
+ yield all(CapCollapsibleLeftNavigationSagas.map((saga) => saga.call()));
10
10
  },
11
11
  },
12
12
  {
13
13
  key: 'analyticsBotSaga',
14
14
  saga: function* analyticsBotSagaFn() {
15
- yield all(analyticsBotSaga.map(saga => saga.call()));
15
+ if (analyticsBotSaga && Array.isArray(analyticsBotSaga)) {
16
+ yield all(analyticsBotSaga.map((saga) => saga.call()));
17
+ }
16
18
  },
17
19
  },
18
20
  ];
@@ -1,34 +1,78 @@
1
1
  import { expectSaga } from "redux-saga-test-plan";
2
- import rootSagas from "../saga";
3
2
  import * as matchers from "redux-saga-test-plan/matchers";
4
3
  import CapCollapsibleLeftNavigationSagas from '@capillarytech/cap-ui-library/CapCollapsibleLeftNavigation/saga';
5
- import { analyticsBotSaga } from "@capillarytech/cap-ui-library/CapAskAira";
4
+
5
+ import rootSagas from "../saga";
6
+
7
+ // Mock the analyticsBotSaga module
8
+ jest.mock('@capillarytech/cap-ui-library/CapAskAira', () => ({
9
+ analyticsBotSaga: [
10
+ {
11
+ * call() { yield true; },
12
+ },
13
+ {
14
+ * call() { yield true; },
15
+ },
16
+ ],
17
+ }));
6
18
 
7
19
  describe("analyticsBotSagaFn", () => {
8
- test.concurrent("should call all analytics bot sagas", () => {
9
- const { saga: analyticsBotSagaFn } = rootSagas.find(
10
- s => s.key === "analyticsBotSaga"
11
- );
20
+ const { saga: analyticsBotSagaFn } = rootSagas.find(
21
+ (s) => s.key === "analyticsBotSaga"
22
+ );
23
+
24
+ test.concurrent("should handle when analyticsBotSaga is valid array", () => {
25
+ // Mock module with valid array
26
+ jest.mock('@capillarytech/cap-ui-library/CapAskAira', () => ({
27
+ analyticsBotSaga: [
28
+ {
29
+ * call() { yield true; },
30
+ },
31
+ {
32
+ * call() { yield true; },
33
+ },
34
+ ],
35
+ }));
36
+
12
37
  return expectSaga(analyticsBotSagaFn)
13
38
  .provide([
14
- [matchers.call.fn(analyticsBotSaga[0]), undefined],
15
- [matchers.call.fn(analyticsBotSaga[1]), undefined]
39
+ [matchers.call.fn(() => {}), undefined],
16
40
  ])
17
41
  .run();
18
42
  });
43
+
44
+ test.concurrent("should handle when analyticsBotSaga is undefined", () => {
45
+ // Mock module with undefined analyticsBotSaga
46
+ jest.mock('@capillarytech/cap-ui-library/CapAskAira', () => ({
47
+ analyticsBotSaga: undefined,
48
+ }));
49
+
50
+ return expectSaga(analyticsBotSagaFn)
51
+ .run();
52
+ });
53
+
54
+ test.concurrent("should handle when analyticsBotSaga is not an array", () => {
55
+ // Mock module with non-array analyticsBotSaga
56
+ jest.mock('@capillarytech/cap-ui-library/CapAskAira', () => ({
57
+ analyticsBotSaga: {},
58
+ }));
59
+
60
+ return expectSaga(analyticsBotSagaFn)
61
+ .run();
62
+ });
19
63
  });
20
64
 
21
65
  describe("navigationConfigSaga", () => {
22
- it.concurrent("should call all analytics bot sagas", () => {
23
- const { saga: navigationConfigSaga } = rootSagas.find(
24
- s => s.key === "navigation"
25
- );
26
-
27
- return expectSaga(navigationConfigSaga)
28
- .provide([
29
- [matchers.call.fn(CapCollapsibleLeftNavigationSagas[0]), undefined],
30
- [matchers.call.fn(CapCollapsibleLeftNavigationSagas[1]), undefined]
31
- ])
32
- .run();
33
- });
66
+ it.concurrent("should call all analytics bot sagas", () => {
67
+ const { saga: navigationConfigSaga } = rootSagas.find(
68
+ (s) => s.key === "navigation"
69
+ );
70
+
71
+ return expectSaga(navigationConfigSaga)
72
+ .provide([
73
+ [matchers.call.fn(CapCollapsibleLeftNavigationSagas[0]), undefined],
74
+ [matchers.call.fn(CapCollapsibleLeftNavigationSagas[1]), undefined],
75
+ ])
76
+ .run();
34
77
  });
78
+ });
@@ -179,7 +179,7 @@ const TestAndPreviewSlidebox = (props) => {
179
179
  resolvedTitle: formData['template-subject'] || ''
180
180
  });
181
181
 
182
- // Extract tags with latest content
182
+ // Always extract tags when content changes
183
183
  const payloadContent = convert(htmlFile, GLOBAL_CONVERT_OPTIONS);
184
184
  actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
185
185
  }
@@ -202,7 +202,7 @@ const TestAndPreviewSlidebox = (props) => {
202
202
  resolvedTitle: formData['template-subject'] || ''
203
203
  });
204
204
 
205
- // Extract tags with initial content
205
+ // Always extract tags when showing
206
206
  const payloadContent = convert(templateContent, GLOBAL_CONVERT_OPTIONS);
207
207
  actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
208
208
  } else {
@@ -211,6 +211,7 @@ const TestAndPreviewSlidebox = (props) => {
211
211
  getCurrentContent,
212
212
  GLOBAL_CONVERT_OPTIONS
213
213
  );
214
+ // Always extract tags when showing
214
215
  actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
215
216
  }
216
217
  }
@@ -228,31 +229,30 @@ const TestAndPreviewSlidebox = (props) => {
228
229
  const templateContent = currentTabData?.[activeTab]?.['template-content'];
229
230
 
230
231
  if (templateContent && templateContent.trim() !== '') {
232
+ // Common function to handle content update
233
+ const handleContentUpdate = (content) => {
234
+ setPreviewDataHtml({
235
+ resolvedBody: content,
236
+ resolvedTitle: formData['template-subject'] || ''
237
+ });
238
+
239
+ // Extract tags from content
240
+ const payloadContent = convert(content, GLOBAL_CONVERT_OPTIONS);
241
+ actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
242
+ };
243
+
231
244
  if (isDragDrop) {
232
- // For Bee editor, update both preview and extract tags
245
+ // For Bee editor, update preview
233
246
  if (templateContent !== previousBeeContentRef.current) {
234
-
235
247
  previousBeeContentRef.current = templateContent;
236
248
  setBeeContent(templateContent);
237
- setPreviewDataHtml({
238
- resolvedBody: templateContent,
239
- resolvedTitle: formData['template-subject'] || ''
240
- });
241
-
242
- // Extract tags with latest content
243
- const payloadContent = convert(templateContent, GLOBAL_CONVERT_OPTIONS);
244
- actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
249
+ handleContentUpdate(templateContent);
245
250
  }
246
251
  } else {
247
- // For CKEditor, always update preview and extract tags with latest content
248
- setPreviewDataHtml({
249
- resolvedBody: templateContent,
250
- resolvedTitle: formData['template-subject'] || ''
251
- });
252
-
253
- // Extract tags with latest content
254
- const payloadContent = convert(templateContent, GLOBAL_CONVERT_OPTIONS);
255
- actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
252
+ // For CKEditor, only update if content changed
253
+ if (templateContent !== previewDataHtml?.resolvedBody) {
254
+ handleContentUpdate(templateContent);
255
+ }
256
256
  }
257
257
  }
258
258
  }, [formData, currentTab]);
@@ -260,9 +260,19 @@ const TestAndPreviewSlidebox = (props) => {
260
260
  // Cleanup effect to reset ref when slidebox closes
261
261
  useEffect(() => {
262
262
  if (!show) {
263
+ // Reset all state
263
264
  previousBeeContentRef.current = '';
264
265
  setBeeContent('');
265
266
  setPreviewDataHtml('');
267
+ setSelectedCustomer(null);
268
+ setRequiredTags([]);
269
+ setOptionalTags([]);
270
+ setCustomValues({});
271
+ setShowJSON(false);
272
+ setTagsExtracted(false);
273
+ setPreviewDevice('desktop');
274
+ setSelectedTestEntities([]);
275
+ actions.clearPrefilledValues();
266
276
  }
267
277
  }, [show]);
268
278
 
@@ -274,14 +284,22 @@ const TestAndPreviewSlidebox = (props) => {
274
284
 
275
285
  // Listen for extract tags API result
276
286
  useEffect(() => {
277
- if (extractedTags?.length > 0) {
278
- // Categorize tags into required and optional
279
- const required = [];
280
- const optional = [];
287
+ // Categorize tags into required and optional
288
+ const required = [];
289
+ const optional = [];
290
+ let hasPersonalizationTags = false;
281
291
 
292
+ if (extractedTags?.length > 0) {
282
293
  const processTag = (tag, parentPath = '') => {
283
294
  const currentPath = parentPath ? `${parentPath}.${tag.name}` : tag.name;
284
295
 
296
+ // Skip unsubscribe tag for input fields
297
+ if (tag?.name === 'unsubscribe') {
298
+ return;
299
+ }
300
+
301
+ hasPersonalizationTags = true;
302
+
285
303
  if (tag?.metaData?.userDriven === false) {
286
304
  required.push({
287
305
  ...tag,
@@ -301,43 +319,75 @@ const TestAndPreviewSlidebox = (props) => {
301
319
 
302
320
  extractedTags.forEach((tag) => processTag(tag));
303
321
 
304
- setRequiredTags(required);
305
- setOptionalTags(optional);
322
+ if (hasPersonalizationTags) {
323
+ setRequiredTags(required);
324
+ setOptionalTags(optional);
306
325
 
307
- // Initialize custom values for required tags
308
- const initialValues = {};
309
- required.forEach((tag) => {
310
- initialValues[tag.fullPath] = '';
311
- });
312
- optional.forEach((tag) => {
313
- initialValues[tag.fullPath] = '';
314
- });
315
- setCustomValues(initialValues);
326
+ // Initialize custom values for required tags
327
+ const initialValues = {};
328
+ required.forEach((tag) => {
329
+ initialValues[tag?.fullPath] = '';
330
+ });
331
+ optional.forEach((tag) => {
332
+ initialValues[tag?.fullPath] = '';
333
+ });
334
+ setCustomValues(initialValues);
335
+ } else {
336
+ // Reset all tag-related state if no personalization tags
337
+ setRequiredTags([]);
338
+ setOptionalTags([]);
339
+ setCustomValues({});
340
+ setTagsExtracted(false);
341
+ }
342
+ } else {
343
+ // Reset all tag-related state if no tags
344
+ setRequiredTags([]);
345
+ setOptionalTags([]);
346
+ setCustomValues({});
347
+ setTagsExtracted(false);
316
348
  }
317
349
  }, [extractedTags]);
318
350
 
319
351
  useEffect(() => {
320
- if (tagsExtracted && selectedCustomer) {
321
- const userDrivenTags = optionalTags?.map((tag) => tag.name);
322
- if (userDrivenTags?.length > 0) {
352
+ if (selectedCustomer) {
353
+ setTagsExtracted(true); // Auto-open custom values editor
354
+
355
+ // Get all available tags
356
+ const allTags = [...requiredTags, ...optionalTags];
357
+
358
+ if (allTags.length > 0) {
323
359
  const payload = {
324
360
  channel: EMAIL,
325
361
  messageTitle: formData['template-subject'],
326
- messageBody: content,
327
- resolvedTags: customValues,
362
+ messageBody: getCurrentContent,
363
+ resolvedTags: {},
328
364
  userId: selectedCustomer?.customerId,
329
365
  };
330
366
  actions.getPrefilledValuesRequested(payload);
331
367
  }
332
368
  }
333
- }, [selectedCustomer, tagsExtracted]);
369
+ }, [selectedCustomer]);
334
370
 
335
371
  useEffect(() => {
336
- if (prefilledValues && !isEmpty(prefilledValues)) {
337
- setCustomValues((prev) => ({
338
- ...prev,
339
- ...prefilledValues,
340
- }));
372
+ if (prefilledValues) {
373
+ // Always replace all values with prefilled values
374
+ const updatedValues = {};
375
+ [...requiredTags, ...optionalTags].forEach((tag) => {
376
+ updatedValues[tag?.fullPath] = prefilledValues[tag?.fullPath] || '';
377
+ });
378
+
379
+
380
+ setCustomValues(updatedValues);
381
+
382
+ // Update preview with prefilled values
383
+ const payload = {
384
+ channel: EMAIL,
385
+ messageTitle: formData['template-subject'],
386
+ messageBody: getCurrentContent,
387
+ resolvedTags: updatedValues,
388
+ userId: selectedCustomer?.customerId,
389
+ };
390
+ actions.updatePreviewRequested(payload);
341
391
  }
342
392
  }, [JSON.stringify(prefilledValues)]);
343
393
 
@@ -371,7 +421,13 @@ const TestAndPreviewSlidebox = (props) => {
371
421
  setPreviewDevice('desktop');
372
422
  setPreviewDataHtml('');
373
423
  setSelectedTestEntities([]);
424
+ setBeeContent('');
425
+ previousBeeContentRef.current = '';
426
+
427
+ // Clear any pending actions
374
428
  actions.clearPrefilledValues();
429
+
430
+ // Call parent's onClose if provided
375
431
  if (onClose) {
376
432
  onClose();
377
433
  }
@@ -384,24 +440,35 @@ const TestAndPreviewSlidebox = (props) => {
384
440
  // Handle customer selection from CustomerSearchSection
385
441
  const handleCustomerSelect = (customer) => {
386
442
  setSelectedCustomer(customer);
387
- setCustomValues((prev) => {
388
- const newValues = { ...prev };
389
- optionalTags.forEach((tag) => {
390
- delete newValues[tag.fullPath];
391
- });
392
- return newValues;
443
+ setTagsExtracted(true); // Auto-open custom values editor
444
+
445
+ // Clear any existing values while waiting for prefilled values
446
+ const emptyValues = {};
447
+ [...requiredTags, ...optionalTags].forEach((tag) => {
448
+ emptyValues[tag?.fullPath] = '';
393
449
  });
450
+ setCustomValues(emptyValues);
394
451
  };
395
452
 
396
453
  const handleClearSelection = () => {
397
454
  setSelectedCustomer(null);
398
- setCustomValues((prev) => {
399
- const newValues = { ...prev };
400
- optionalTags.forEach((tag) => {
401
- delete newValues[tag.fullPath];
402
- });
403
- return newValues;
455
+
456
+ // Initialize empty values for all tags
457
+ const emptyValues = {};
458
+ [...requiredTags, ...optionalTags].forEach((tag) => {
459
+ emptyValues[tag?.fullPath] = '';
404
460
  });
461
+ setCustomValues(emptyValues);
462
+
463
+ // Update preview with empty values
464
+ const payload = {
465
+ channel: EMAIL,
466
+ messageTitle: formData['template-subject'],
467
+ messageBody: getCurrentContent,
468
+ resolvedTags: emptyValues,
469
+ userId: null,
470
+ };
471
+ actions.updatePreviewRequested(payload);
405
472
  };
406
473
 
407
474
  // Handle custom value changes
@@ -426,63 +493,38 @@ const TestAndPreviewSlidebox = (props) => {
426
493
 
427
494
  // Handle discard custom values
428
495
  const handleDiscardCustomValues = () => {
429
- const resetValues = {};
430
- requiredTags.forEach((tag) => {
431
- resetValues[tag?.fullPath] = '';
496
+ // Initialize empty values for all tags
497
+ const emptyValues = {};
498
+ [...requiredTags, ...optionalTags].forEach((tag) => {
499
+ emptyValues[tag?.fullPath] = '';
432
500
  });
433
- optionalTags.forEach((tag) => {
434
- resetValues[tag?.fullPath] = '';
435
- });
436
- setCustomValues(resetValues);
501
+ setCustomValues(emptyValues);
502
+
503
+ // Update preview with empty values
504
+ const payload = {
505
+ channel: EMAIL,
506
+ messageTitle: formData['template-subject'],
507
+ messageBody: getCurrentContent,
508
+ resolvedTags: emptyValues,
509
+ userId: selectedCustomer?.customerId,
510
+ };
511
+ actions.updatePreviewRequested(payload);
437
512
  };
438
513
 
439
514
  // Handle update preview
440
515
  const handleUpdatePreview = async () => {
441
516
  try {
442
- // Store current values to prevent loss during update
443
- const currentCustomValues = { ...customValues };
444
-
445
- const currentTabData = formData[currentTab - 1];
446
- const activeTab = currentTabData?.activeTab;
447
- const isDragDrop = currentTabData?.[activeTab]?.is_drag_drop;
448
-
449
- // For BEE editor, ensure content is saved first
450
- if (isDragDrop && beeInstance) {
451
- // Trigger save to ensure latest content
452
- beeInstance.save();
453
-
454
- // Wait a bit for save to complete and formData to update
455
- await new Promise((resolve) => setTimeout(resolve, 500));
456
-
457
- // Get latest content from formData
458
- const updatedContent = formData[currentTab - 1]?.[activeTab]?.['template-content'];
459
- const payload = {
460
- channel: EMAIL,
461
- messageTitle: formData['template-subject'],
462
- messageBody: updatedContent || getCurrentContent,
463
- resolvedTags: currentCustomValues, // Use stored values
464
- userId: selectedCustomer?.customerId,
465
- };
517
+ // Include unsubscribe tag if content contains it
518
+ const resolvedTags = { ...customValues };
466
519
 
467
- await actions.updatePreviewRequested(payload);
468
- return;
469
- }
470
-
471
- // For CKEditor, get latest content directly from formData
472
- const templateContent = currentTabData?.[activeTab]?.['template-content'];
473
- if (templateContent) {
474
- // Create payload for CKEditor preview update
475
- const payload = {
476
- channel: EMAIL,
477
- messageTitle: formData['template-subject'],
478
- messageBody: templateContent,
479
- resolvedTags: currentCustomValues,
480
- userId: selectedCustomer?.customerId,
481
- };
482
-
483
- // Use the same update preview action for both editors
484
- await actions.updatePreviewRequested(payload);
485
- }
520
+ const payload = {
521
+ channel: EMAIL,
522
+ messageTitle: formData['template-subject'],
523
+ messageBody: getCurrentContent,
524
+ resolvedTags,
525
+ userId: selectedCustomer?.customerId,
526
+ };
527
+ await actions.updatePreviewRequested(payload);
486
528
  } catch (error) {
487
529
  console.error('Error updating preview:', error);
488
530
  CapNotification.error({
@@ -493,7 +535,34 @@ const TestAndPreviewSlidebox = (props) => {
493
535
 
494
536
  // Handle extract tags button click
495
537
  const handleExtractTags = () => {
538
+ // Extract tags from current content
539
+ const currentTabData = formData[currentTab - 1];
540
+ const activeTab = currentTabData?.activeTab;
541
+ const templateContent = currentTabData?.[activeTab]?.['template-content'];
542
+
543
+ // Check for personalization tags (excluding unsubscribe)
544
+ const content = templateContent || getCurrentContent;
545
+ const tags = content.match(/{{[^}]+}}/g) || [];
546
+ const hasPersonalizationTags = tags.some(tag => !tag.includes('unsubscribe'));
547
+
548
+ if (!hasPersonalizationTags && tags.length === 1 && tags[0].includes('unsubscribe')) {
549
+ // If only unsubscribe tag is present, show noTagsExtracted message
550
+ setTagsExtracted(false);
551
+ setRequiredTags([]);
552
+ setOptionalTags([]);
553
+ setCustomValues({});
554
+ return;
555
+ }
556
+
557
+ // Extract tags
496
558
  setTagsExtracted(true);
559
+ if (templateContent) {
560
+ const payloadContent = convert(templateContent, GLOBAL_CONVERT_OPTIONS);
561
+ actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
562
+ } else {
563
+ const payloadContent = convert(getCurrentContent, GLOBAL_CONVERT_OPTIONS);
564
+ actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
565
+ }
497
566
  };
498
567
 
499
568
  const handleTestEntitiesChange = (value) => {
@@ -2696,19 +2696,7 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
2696
2696
  const previewSubject = formData['template-subject'];
2697
2697
  const testOrPreviewProps = {channel: EMAIL, content: previewContent, subject: previewSubject};
2698
2698
 
2699
- // Call our handler instead of the props methods to set the flag
2700
- const handleClick = () => {
2701
- if (action === 'TEST') {
2702
- // For test action, open our Test & Preview slidebox
2703
- this.handleTestAndPreview();
2704
- } else {
2705
- // For preview action, call the original method
2706
- const { onPreviewContentClicked } = this.props;
2707
- if (onPreviewContentClicked) {
2708
- onPreviewContentClicked(testOrPreviewProps);
2709
- }
2710
- }
2711
- };
2699
+ const { onPreviewContentClicked, onTestContentClicked } = this.props;
2712
2700
 
2713
2701
  return (
2714
2702
  <CapButton
@@ -2718,7 +2706,7 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
2718
2706
  padding: '0 6px',
2719
2707
  }}
2720
2708
  type="flat"
2721
- onClick={handleClick}
2709
+ onClick={() => action === 'PREVIEW' ? onPreviewContentClicked(testOrPreviewProps) : onTestContentClicked(testOrPreviewProps)}
2722
2710
  >
2723
2711
  <CapIcon type={action === 'PREVIEW' ? "eye" : "lab"}/>
2724
2712
  {action === 'PREVIEW' ? <FormattedMessage {...messages.preview} /> : <FormattedMessage {...messages.testMessage} />}
@@ -2773,7 +2761,7 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
2773
2761
  tags = this.props.supportedTags;
2774
2762
  }
2775
2763
  const { showImageSelectionBox = false } = this.state;
2776
- const showTestAndPreview = !showImageSelectionBox && getDefaultTags === 'outbound';
2764
+ const showTestAndPreview = !showImageSelectionBox && getDefaultTags === 'outbound' && !this.props.showTestAndPreviewSlidebox;
2777
2765
  return (
2778
2766
  <div className="email-container">
2779
2767
  <CapSpin spinning={isLoading}>