@capillarytech/creatives-library 8.0.340-beta.0.4 → 8.0.340-beta.0.6

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 (67) hide show
  1. package/constants/unified.js +1 -0
  2. package/package.json +1 -1
  3. package/services/api.js +20 -0
  4. package/services/tests/api.test.js +59 -0
  5. package/utils/common.js +6 -0
  6. package/utils/test-utils.js +2 -2
  7. package/utils/tests/v2Common.test.js +46 -1
  8. package/utils/v2common.js +18 -0
  9. package/v2Components/CapTagList/index.js +5 -6
  10. package/v2Components/CapTagListWithInput/index.js +1 -1
  11. package/v2Components/CommonTestAndPreview/UnifiedPreview/WhatsAppPreviewContent.js +18 -6
  12. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +27 -0
  13. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/WhatsAppPreviewContent.test.js +48 -0
  14. package/v2Components/TemplatePreview/_templatePreview.scss +22 -1
  15. package/v2Components/TemplatePreview/index.js +21 -9
  16. package/v2Components/TemplatePreview/tests/__snapshots__/index.test.js.snap +1 -0
  17. package/v2Containers/Assets/images/archive_Empty_Illustration.svg +9 -0
  18. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +28 -20
  19. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +24 -16
  20. package/v2Containers/CreativesContainer/SlideBoxContent.js +16 -5
  21. package/v2Containers/CreativesContainer/SlideBoxFooter.js +3 -1
  22. package/v2Containers/CreativesContainer/SlideBoxHeader.js +4 -4
  23. package/v2Containers/CreativesContainer/index.js +14 -1
  24. package/v2Containers/CreativesContainer/messages.js +4 -0
  25. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +2 -4
  26. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +4 -4
  27. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +3 -0
  28. package/v2Containers/Email/reducer.js +12 -3
  29. package/v2Containers/Email/sagas.js +9 -4
  30. package/v2Containers/Email/tests/__snapshots__/reducer.test.js.snap +4 -0
  31. package/v2Containers/Email/tests/reducer.test.js +47 -0
  32. package/v2Containers/Email/tests/sagas.test.js +146 -6
  33. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +8 -1
  34. package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +1 -0
  35. package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +7 -0
  36. package/v2Containers/MobilePush/Create/index.js +1 -1
  37. package/v2Containers/MobilePush/Edit/index.js +1 -1
  38. package/v2Containers/Sms/Create/index.js +3 -0
  39. package/v2Containers/Sms/SCHEMA_FORMBUILDER_MAP.md +1 -1
  40. package/v2Containers/Templates/ChannelTypeIllustration.js +23 -6
  41. package/v2Containers/Templates/_templates.scss +155 -24
  42. package/v2Containers/Templates/actions.js +44 -0
  43. package/v2Containers/Templates/constants.js +31 -0
  44. package/v2Containers/Templates/index.js +400 -59
  45. package/v2Containers/Templates/messages.js +96 -0
  46. package/v2Containers/Templates/reducer.js +84 -1
  47. package/v2Containers/Templates/sagas.js +64 -0
  48. package/v2Containers/Templates/selectors.js +12 -0
  49. package/v2Containers/Templates/tests/ChannelTypeIllustration.test.js +12 -0
  50. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1166 -1112
  51. package/v2Containers/Templates/tests/index.test.js +6 -0
  52. package/v2Containers/Templates/tests/reducer.test.js +178 -0
  53. package/v2Containers/Templates/tests/sagas.test.js +390 -8
  54. package/v2Containers/Templates/tests/selector.test.js +32 -0
  55. package/v2Containers/TemplatesV2/TemplatesV2.style.js +1 -1
  56. package/v2Containers/Viber/constants.js +8 -0
  57. package/v2Containers/Viber/index.js +5 -0
  58. package/v2Containers/Viber/messages.js +4 -0
  59. package/v2Containers/Viber/reducer.js +26 -3
  60. package/v2Containers/Viber/sagas.js +50 -8
  61. package/v2Containers/Viber/tests/index.test.js +80 -0
  62. package/v2Containers/Viber/tests/reducer.test.js +297 -0
  63. package/v2Containers/Viber/tests/saga.test.js +412 -40
  64. package/v2Containers/Whatsapp/constants.js +8 -0
  65. package/v2Containers/Whatsapp/index.js +145 -5
  66. package/v2Containers/Whatsapp/index.scss +12 -0
  67. package/v2Containers/Whatsapp/messages.js +16 -0
@@ -9,6 +9,14 @@ import { IntlProvider } from 'react-intl';
9
9
  import ChannelSelectionStep from '../ChannelSelectionStep';
10
10
  import { CHANNELS } from '../../../constants';
11
11
 
12
+ // CapDropdown opens via @rc-component portal animations that need real time;
13
+ // under jest -w 90% parallel load the default 5s budget is too tight for
14
+ // click → menu-paint → menu-item-click chains. Bump per-test timeout.
15
+ jest.setTimeout(30000);
16
+
17
+ // Larger wait budget for individual menu/portal transitions under parallel load.
18
+ const WAIT_OPTIONS = { timeout: 10000 };
19
+
12
20
  jest.mock('../../../../CreativesContainer', () => function MockCreativesContainer({
13
21
  getCreativesData,
14
22
  handleCloseCreatives,
@@ -126,7 +134,7 @@ describe('ChannelSelectionStep', () => {
126
134
  await userEvent.click(screen.getByLabelText('Show more content options icon'));
127
135
  await waitFor(() => {
128
136
  expect(screen.getByRole('menu')).toBeInTheDocument();
129
- });
137
+ }, WAIT_OPTIONS);
130
138
  await userEvent.click(within(screen.getByRole('menu')).getByText('Remove'));
131
139
 
132
140
  expect(onChange).toHaveBeenCalledWith({ contentItems: [] });
@@ -145,12 +153,12 @@ describe('ChannelSelectionStep', () => {
145
153
  await userEvent.click(screen.getByRole('button', { name: /content template/i }));
146
154
  await waitFor(() => {
147
155
  expect(screen.getByRole('menu')).toBeInTheDocument();
148
- });
156
+ }, WAIT_OPTIONS);
149
157
  await userEvent.click(within(screen.getByRole('menu')).getByText('SMS'));
150
158
 
151
159
  await waitFor(() => {
152
160
  expect(screen.getByTestId('creatives-mock')).toBeInTheDocument();
153
- });
161
+ }, WAIT_OPTIONS);
154
162
 
155
163
  expect(screen.getByTestId('creatives-mock')).toHaveAttribute('data-creatives-mode', 'create');
156
164
 
@@ -181,9 +189,9 @@ describe('ChannelSelectionStep', () => {
181
189
  );
182
190
 
183
191
  await userEvent.click(screen.getByRole('button', { name: /content template/i }));
184
- await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
192
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
185
193
  await userEvent.click(within(screen.getByRole('menu')).getByText('SMS'));
186
- await waitFor(() => expect(screen.getByTestId('creatives-mock')).toBeInTheDocument());
194
+ await waitFor(() => expect(screen.getByTestId('creatives-mock')).toBeInTheDocument(), WAIT_OPTIONS);
187
195
 
188
196
  await userEvent.click(screen.getByTestId('creatives-close'));
189
197
 
@@ -209,10 +217,10 @@ describe('ChannelSelectionStep', () => {
209
217
  );
210
218
 
211
219
  await userEvent.click(screen.getByLabelText('Show more content options icon'));
212
- await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
220
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
213
221
  await userEvent.click(within(screen.getByRole('menu')).getByText('Edit'));
214
222
 
215
- await waitFor(() => expect(screen.getByTestId('creatives-mock')).toBeInTheDocument());
223
+ await waitFor(() => expect(screen.getByTestId('creatives-mock')).toBeInTheDocument(), WAIT_OPTIONS);
216
224
  expect(screen.getByTestId('creatives-mock')).toHaveAttribute('data-creatives-mode', 'edit');
217
225
 
218
226
  await userEvent.click(screen.getByTestId('creatives-save'));
@@ -242,7 +250,7 @@ describe('ChannelSelectionStep', () => {
242
250
  );
243
251
 
244
252
  await userEvent.click(screen.getByLabelText('Show more content options icon'));
245
- await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
253
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
246
254
  await userEvent.click(within(screen.getByRole('menu')).getByText('Preview and Test'));
247
255
 
248
256
  expect(console.log).toHaveBeenCalledWith('Preview content', 'p1');
@@ -288,7 +296,7 @@ describe('ChannelSelectionStep', () => {
288
296
  await userEvent.click(screen.getByRole('button', { name: /^add incentive$/i }));
289
297
  await waitFor(() => {
290
298
  expect(screen.getByRole('menu')).toBeInTheDocument();
291
- });
299
+ }, WAIT_OPTIONS);
292
300
  await userEvent.click(within(screen.getByRole('menu')).getByText('Badges'));
293
301
 
294
302
  expect(onChange).toHaveBeenCalledWith({
@@ -316,7 +324,7 @@ describe('ChannelSelectionStep', () => {
316
324
  const addIncentiveLinks = screen.getAllByRole('button', { name: /add incentive/i });
317
325
  expect(addIncentiveLinks.length).toBeGreaterThanOrEqual(1);
318
326
  await userEvent.click(addIncentiveLinks[addIncentiveLinks.length - 1]);
319
- await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
327
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
320
328
  await userEvent.click(within(screen.getByRole('menu')).getByText('Badges'));
321
329
 
322
330
  expect(onChange).toHaveBeenCalledWith({
@@ -462,7 +470,7 @@ describe('ChannelSelectionStep', () => {
462
470
  <ChannelSelectionStep value={{ contentItems: [] }} onChange={jest.fn()} channels={null} />,
463
471
  );
464
472
  await userEvent.click(screen.getByRole('button', { name: /content template/i }));
465
- await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
473
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
466
474
  expect(within(screen.getByRole('menu')).getByText('SMS')).toBeInTheDocument();
467
475
  });
468
476
 
@@ -485,7 +493,7 @@ describe('ChannelSelectionStep', () => {
485
493
  />,
486
494
  );
487
495
  await userEvent.click(screen.getByRole('button', { name: /content template/i }));
488
- await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
496
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
489
497
  await userEvent.click(screen.getByText('Custom'));
490
498
  expect(onChange).toHaveBeenCalledWith({ channel: 'CUSTOM', channels: [] });
491
499
  expect(screen.queryByTestId('creatives-mock')).not.toBeInTheDocument();
@@ -497,9 +505,9 @@ describe('ChannelSelectionStep', () => {
497
505
  <ChannelSelectionStep value={{ contentItems: [] }} onChange={onChange} channels={CHANNELS} />,
498
506
  );
499
507
  await userEvent.click(screen.getByRole('button', { name: /content template/i }));
500
- await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
508
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
501
509
  await userEvent.click(within(screen.getByRole('menu')).getByText('SMS'));
502
- await waitFor(() => expect(screen.getByTestId('creatives-mock')).toBeInTheDocument());
510
+ await waitFor(() => expect(screen.getByTestId('creatives-mock')).toBeInTheDocument(), WAIT_OPTIONS);
503
511
 
504
512
  await userEvent.click(screen.getByTestId('creatives-save-null'));
505
513
 
@@ -522,7 +530,7 @@ describe('ChannelSelectionStep', () => {
522
530
  );
523
531
 
524
532
  await userEvent.click(screen.getByLabelText('Show more content options icon'));
525
- await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
533
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
526
534
  await userEvent.click(within(screen.getByRole('menu')).getByText('Edit'));
527
535
 
528
536
  expect(screen.queryByTestId('creatives-mock')).not.toBeInTheDocument();
@@ -538,9 +546,9 @@ describe('ChannelSelectionStep', () => {
538
546
  />,
539
547
  );
540
548
  await userEvent.click(screen.getByRole('button', { name: /content template/i }));
541
- await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
549
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
542
550
  await userEvent.click(within(screen.getByRole('menu')).getByText('SMS'));
543
- await waitFor(() => expect(screen.getByTestId('creatives-mock')).toBeInTheDocument());
551
+ await waitFor(() => expect(screen.getByTestId('creatives-mock')).toBeInTheDocument(), WAIT_OPTIONS);
544
552
  expect(screen.getByTestId('creatives-mock')).toHaveAttribute('data-creatives-mode', 'preview');
545
553
  });
546
554
 
@@ -558,7 +566,7 @@ describe('ChannelSelectionStep', () => {
558
566
  />,
559
567
  );
560
568
  await userEvent.click(screen.getByRole('button', { name: /^add incentive$/i }));
561
- await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
569
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
562
570
  await userEvent.click(within(screen.getByRole('menu')).getByText('Badges'));
563
571
  expect(onChange).toHaveBeenCalledWith({
564
572
  selectedOfferDetails: [{ type: 'existing' }, { type: 'badges' }],
@@ -712,7 +720,7 @@ describe('ChannelSelectionStep', () => {
712
720
  />,
713
721
  );
714
722
  await userEvent.click(screen.getByRole('button', { name: /content template/i }));
715
- await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
723
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
716
724
  // SMS is in channelsToDisable — clicking it should not call onChange or show creatives
717
725
  await userEvent.click(within(screen.getByRole('menu')).getByText('SMS'));
718
726
  expect(screen.queryByTestId('creatives-mock')).not.toBeInTheDocument();
@@ -767,7 +775,7 @@ describe('ChannelSelectionStep', () => {
767
775
  />,
768
776
  );
769
777
  await userEvent.click(screen.getByRole('button', { name: /content template/i }));
770
- await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
778
+ await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument(), WAIT_OPTIONS);
771
779
  // Click the disabled EMAIL menu item — handleChannelSelect should return early
772
780
  await userEvent.click(within(screen.getByRole('menu')).getByText('Email'));
773
781
  expect(onChange).not.toHaveBeenCalled();
@@ -9,6 +9,11 @@ import SenderDetails, { parseSenderDetailsFromEntity } from '../SenderDetails';
9
9
  import * as deliverySettingsConfig from '../deliverySettingsConfig';
10
10
  import { findVisibleSelectOption } from '../../../../../utils/test-utils';
11
11
 
12
+ // antd v6 Selects rely on real-time animations + portal mounts; under the full
13
+ // parallel suite (jest -w 90%) the default 5s budget is too tight for
14
+ // click → listbox-paint → option-click chains. Bump per-test timeout.
15
+ jest.setTimeout(30000);
16
+
12
17
  const { parseEntityForDisplay } = deliverySettingsConfig;
13
18
 
14
19
  /** Shared domainProperties entity shapes (see deliverySettingsConfig). */
@@ -173,11 +178,14 @@ function renderSenderDetails(override = {}) {
173
178
  return { ...view, onClose, onSave };
174
179
  }
175
180
 
181
+ // Larger wait budget for individual portal/listbox transitions under parallel load.
182
+ const WAIT_OPTIONS = { timeout: 10000 };
183
+
176
184
  async function openSelectAndChoose(root, comboIndex, optionText) {
177
185
  const combos = within(root).getAllByRole('combobox');
178
186
  await userEvent.click(combos[comboIndex]);
179
- await waitFor(() => expect(screen.getByRole('listbox')).toBeInTheDocument());
180
- await userEvent.click(await findVisibleSelectOption(optionText));
187
+ await waitFor(() => expect(screen.getByRole('listbox')).toBeInTheDocument(), WAIT_OPTIONS);
188
+ await userEvent.click(await findVisibleSelectOption(optionText, WAIT_OPTIONS));
181
189
  }
182
190
 
183
191
  describe('SenderDetails', () => {
@@ -266,8 +274,8 @@ describe('SenderDetails', () => {
266
274
 
267
275
  const combos = within(root).getAllByRole('combobox');
268
276
  await userEvent.click(combos[1]);
269
- await waitFor(() => expect(screen.getByRole('listbox')).toBeInTheDocument());
270
- await userEvent.click(await findVisibleSelectOption('+1002003002'));
277
+ await waitFor(() => expect(screen.getByRole('listbox')).toBeInTheDocument(), WAIT_OPTIONS);
278
+ await userEvent.click(await findVisibleSelectOption('+1002003002', WAIT_OPTIONS));
271
279
 
272
280
  const saveBtn = within(root).getByRole('button', { name: /save changes/i });
273
281
  await waitFor(() => expect(saveBtn).not.toBeDisabled());
@@ -342,8 +350,8 @@ describe('SenderDetails', () => {
342
350
 
343
351
  const combos = within(root).getAllByRole('combobox');
344
352
  await userEvent.click(combos[1]);
345
- await waitFor(() => expect(screen.getByRole('listbox')).toBeInTheDocument());
346
- await userEvent.click(await findVisibleSelectOption('+1002003002'));
353
+ await waitFor(() => expect(screen.getByRole('listbox')).toBeInTheDocument(), WAIT_OPTIONS);
354
+ await userEvent.click(await findVisibleSelectOption('+1002003002', WAIT_OPTIONS));
347
355
 
348
356
  const resetLinks = within(root).getAllByText('Reset');
349
357
  await userEvent.click(resetLinks[1]);
@@ -363,8 +371,8 @@ describe('SenderDetails', () => {
363
371
 
364
372
  const combos = within(root).getAllByRole('combobox');
365
373
  await userEvent.click(combos[combos.length - 1]);
366
- await waitFor(() => expect(screen.getByRole('listbox')).toBeInTheDocument());
367
- await userEvent.click(await findVisibleSelectOption('+91900111333'));
374
+ await waitFor(() => expect(screen.getByRole('listbox')).toBeInTheDocument(), WAIT_OPTIONS);
375
+ await userEvent.click(await findVisibleSelectOption('+91900111333', WAIT_OPTIONS));
368
376
 
369
377
  const saveBtn = within(root).getByRole('button', { name: /save changes/i });
370
378
  await waitFor(() => expect(saveBtn).not.toBeDisabled());
@@ -418,8 +426,8 @@ describe('SenderDetails', () => {
418
426
 
419
427
  const combos = within(root).getAllByRole('combobox');
420
428
  await userEvent.click(combos[0]);
421
- await waitFor(() => expect(screen.getByRole('listbox')).toBeInTheDocument());
422
- await userEvent.click(await findVisibleSelectOption('Gateway B'));
429
+ await waitFor(() => expect(screen.getByRole('listbox')).toBeInTheDocument(), WAIT_OPTIONS);
430
+ await userEvent.click(await findVisibleSelectOption('Gateway B', WAIT_OPTIONS));
423
431
 
424
432
  // The sender ID should now auto-update to Gateway B's sender
425
433
  await waitFor(() => expect(within(root).getByText('+222')).toBeInTheDocument());
@@ -437,8 +445,8 @@ describe('SenderDetails', () => {
437
445
  // Switch to Mail Beta domain
438
446
  const combos = within(root).getAllByRole('combobox');
439
447
  await userEvent.click(combos[0]);
440
- await waitFor(() => expect(screen.getByRole('listbox')).toBeInTheDocument());
441
- await userEvent.click(await findVisibleSelectOption('Mail Beta'));
448
+ await waitFor(() => expect(screen.getByRole('listbox')).toBeInTheDocument(), WAIT_OPTIONS);
449
+ await userEvent.click(await findVisibleSelectOption('Mail Beta', WAIT_OPTIONS));
442
450
 
443
451
  // Sender ID should auto-update to beta@other.com
444
452
  await waitFor(() => expect(within(root).getByText('beta@other.com')).toBeInTheDocument());
@@ -461,8 +469,8 @@ describe('SenderDetails', () => {
461
469
  const combos = within(root).getAllByRole('combobox');
462
470
  const senderCombo = combos[combos.length - 1];
463
471
  await userEvent.click(senderCombo);
464
- await waitFor(() => expect(screen.getByRole('listbox')).toBeInTheDocument());
465
- await userEvent.click(await findVisibleSelectOption('+91900111333'));
472
+ await waitFor(() => expect(screen.getByRole('listbox')).toBeInTheDocument(), WAIT_OPTIONS);
473
+ await userEvent.click(await findVisibleSelectOption('+91900111333', WAIT_OPTIONS));
466
474
 
467
475
  const saveBtn = within(root).getByRole('button', { name: /save changes/i });
468
476
  await waitFor(() => expect(saveBtn).not.toBeDisabled());
@@ -500,8 +508,8 @@ describe('SenderDetails', () => {
500
508
  const combos = within(root).getAllByRole('combobox');
501
509
  const senderCombo = combos[combos.length - 1];
502
510
  await userEvent.click(senderCombo);
503
- await waitFor(() => expect(screen.getByRole('listbox')).toBeInTheDocument());
504
- await userEvent.click(await findVisibleSelectOption('+viber-alt'));
511
+ await waitFor(() => expect(screen.getByRole('listbox')).toBeInTheDocument(), WAIT_OPTIONS);
512
+ await userEvent.click(await findVisibleSelectOption('+viber-alt', WAIT_OPTIONS));
505
513
 
506
514
  // antd v6 also renders selected option in an aria-live polite region for
507
515
  // screen readers, so the text appears twice. Match only the visible select
@@ -698,9 +698,16 @@ export function SlideBoxContent(props) {
698
698
  {(isEditEmailWithId || isEmailEditWithContent) && (
699
699
  (() => {
700
700
  const supportCKEditor = commonUtil.hasSupportCKEditor();
701
- // When supportCKEditor is true: Use Email component (legacy flow with CKEditor).
702
- // When supportCKEditor is false: Always use EmailWrapper (BEE or HTML editor, never CKEditor).
703
- if (supportCKEditor) {
701
+ // BEE templates always use the <Email> component directly (the same
702
+ // path the SUPPORT_CK_EDITOR=true branch uses). EmailWrapper's
703
+ // useEmailWrapper strips templateData from props, so library-mode
704
+ // pencil-edit on a BEE template would never reach startTemplateCreation
705
+ // and getDetails?type=BEE_PLUGIN would never fire. Routing BEE
706
+ // templates through the direct <Email> path — which is already
707
+ // battle-tested for the CKEditor flow — sidesteps that entirely.
708
+ const isBEETemplate = templateData?.base?.is_drag_drop === true
709
+ || templateData?.base?.is_drag_drop === 1;
710
+ if (supportCKEditor || isBEETemplate) {
704
711
  return (
705
712
  <Email
706
713
  key="cretives-container-email-edit"
@@ -776,8 +783,12 @@ export function SlideBoxContent(props) {
776
783
  isTestAndPreviewMode={isTestAndPreviewMode}
777
784
  onHtmlEditorValidationStateChange={onHtmlEditorValidationStateChange}
778
785
  location={{
779
- pathname: `/email/edit/${templateData._id}`,
780
- query: { type: 'embedded', module: 'library', id: templateData._id },
786
+ pathname: `/email/edit/${templateData._id || ''}`,
787
+ query: {
788
+ type: 'embedded',
789
+ module: 'library',
790
+ ...(templateData._id ? { id: templateData._id } : {}),
791
+ },
781
792
  }}
782
793
  params={{ id: templateData._id }}
783
794
  />
@@ -24,6 +24,7 @@ function SlideBoxFooter(props) {
24
24
  slidBoxContent,
25
25
  onSave,
26
26
  onEditTemplate,
27
+ isTemplateArchived,
27
28
  onCreateNextStep,
28
29
  isFullMode,
29
30
  fetchingCmsData,
@@ -214,7 +215,7 @@ function SlideBoxFooter(props) {
214
215
  <FormattedMessage {...(continueButtonLabel || messages.continue)} />
215
216
  </CapButton>
216
217
  )}
217
- {slidBoxContent === PREVIEW && (
218
+ {slidBoxContent === PREVIEW && !isTemplateArchived && (
218
219
  <CapButton onClick={onEditTemplate} type="secondary">
219
220
  <FormattedMessage {...messages.creativesTemplatesEdit} />
220
221
  </CapButton>
@@ -227,6 +228,7 @@ SlideBoxFooter.propTypes = {
227
228
  slidBoxContent: PropTypes.node,
228
229
  onSave: PropTypes.func,
229
230
  onEditTemplate: PropTypes.func,
231
+ isTemplateArchived: PropTypes.bool,
230
232
  onCreateNextStep: PropTypes.func,
231
233
  shouldShowContinueFooter: PropTypes.func,
232
234
  shouldShowDoneFooter: PropTypes.func,
@@ -91,7 +91,7 @@ export function SlideBoxHeader(props) {
91
91
  }
92
92
  prefix={!isFullMode && moduleType === JOURNEY &&
93
93
  <PrefixWrapper>
94
- <CapIcons.backIcon onClick={handleClose} />
94
+ <CapIcons.BackIcon onClick={handleClose} />
95
95
  </PrefixWrapper>
96
96
  }
97
97
  />
@@ -106,7 +106,7 @@ export function SlideBoxHeader(props) {
106
106
  </CapLabel>}
107
107
  prefix={!isFullMode &&
108
108
  <PrefixWrapper>
109
- <CapIcons.backIcon onClick={onShowTemplates} />
109
+ <CapIcons.BackIcon onClick={onShowTemplates} />
110
110
  </PrefixWrapper>
111
111
  }
112
112
  />
@@ -137,7 +137,7 @@ export function SlideBoxHeader(props) {
137
137
  }
138
138
  prefix={creativesMode !== 'edit' && !isFullMode && showPrefix &&
139
139
  <PrefixWrapper>
140
- <CapIcons.backIcon onClick={onShowTemplates} />
140
+ <CapIcons.BackIcon onClick={onShowTemplates} />
141
141
  </PrefixWrapper>
142
142
  }
143
143
  />
@@ -148,7 +148,7 @@ export function SlideBoxHeader(props) {
148
148
  title={<FormattedMessage {...(showCreateTraiSMSHeader ? messages.UploadSMStemplates : messages.createMessageContent)} values={{channel: getChannelLabel(channel)}}/>}
149
149
  prefix={!isFullMode && showPrefix &&
150
150
  <PrefixWrapper>
151
- <CapIcons.backIcon onClick={onShowTemplates} />
151
+ <CapIcons.BackIcon onClick={onShowTemplates} />
152
152
  </PrefixWrapper>
153
153
  }
154
154
  />
@@ -50,7 +50,7 @@ import { RCS_STATUSES } from '../Rcs/constants';
50
50
  import { CREATIVE } from '../Facebook/constants';
51
51
  import { LOYALTY } from '../App/constants';
52
52
  import {
53
- WHATSAPP_STATUSES, WHATSAPP_MEDIA_TYPES, PHONE_NUMBER, URL,
53
+ WHATSAPP_STATUSES, WHATSAPP_MEDIA_TYPES, PHONE_NUMBER, URL, ADD_IMAGE_URL_PREVIEW_MARKER,
54
54
  } from '../Whatsapp/constants';
55
55
  import { updateImagesInHtml } from '../../utils/cdnTransformation';
56
56
 
@@ -260,6 +260,10 @@ export class Creatives extends React.Component {
260
260
  };
261
261
 
262
262
  onEditTemplate = () => {
263
+ if (commonUtil.hasCreativesArchivalEnabled() && this.props.templateData?.isArchived) {
264
+ CapNotification.error({ message: this.props.intl.formatMessage(messages.cannotEditArchivedTemplate) });
265
+ return;
266
+ }
263
267
  this.setState({ slidBoxContent: 'editTemplate', showSlideBox: true, templateNameExists: true });
264
268
  };
265
269
 
@@ -684,6 +688,8 @@ export class Creatives extends React.Component {
684
688
  switch (mediaType) {
685
689
  case (WHATSAPP_MEDIA_TYPES.IMAGE):
686
690
  mediaParams.imageUrl = url;
691
+ // Detect isAddImageUrl from previewUrl marker (previewUrl is unused for IMAGE type)
692
+ mediaParams.isAddImageUrl = previewUrl === ADD_IMAGE_URL_PREVIEW_MARKER;
687
693
  break;
688
694
  case (WHATSAPP_MEDIA_TYPES.VIDEO):
689
695
  mediaParams.videoUrl = url;
@@ -1142,6 +1148,7 @@ export class Creatives extends React.Component {
1142
1148
  headerTemplate = '',
1143
1149
  } = {},
1144
1150
  isPreviewUrl = false,
1151
+ isAddImageUrl = false,
1145
1152
  carouselData = [],
1146
1153
  } = cloneDeep(versions.base.content.whatsapp);
1147
1154
 
@@ -1178,6 +1185,11 @@ export class Creatives extends React.Component {
1178
1185
  switch (mediaType) {
1179
1186
  case (WHATSAPP_MEDIA_TYPES.IMAGE):
1180
1187
  whatsappMedia.url = imageUrl;
1188
+ // previewUrl is unused for IMAGE type — reuse it to persist isAddImageUrl flag
1189
+ // without adding any new field that the backend would reject
1190
+ if (isAddImageUrl) {
1191
+ whatsappMedia.previewUrl = ADD_IMAGE_URL_PREVIEW_MARKER;
1192
+ }
1181
1193
  break;
1182
1194
  case (WHATSAPP_MEDIA_TYPES.VIDEO):
1183
1195
  whatsappMedia.url = videoUrl;
@@ -2136,6 +2148,7 @@ export class Creatives extends React.Component {
2136
2148
  onSave={this.saveMessage}
2137
2149
  onDiscard={this.discardMessage}
2138
2150
  onEditTemplate={this.onEditTemplate}
2151
+ isTemplateArchived={!!(commonUtil.hasCreativesArchivalEnabled() && this.props.templateData && this.props.templateData.isArchived)}
2139
2152
  slidBoxContent={slidBoxContent}
2140
2153
  onCreateNextStep={this.onCreateNextStep}
2141
2154
  currentChannel={currentChannel.toUpperCase()}
@@ -390,4 +390,8 @@ export default defineMessages({
390
390
  id: `${scope}.personalizationTokensErrorMessage`,
391
391
  defaultMessage: `Personalization tags are not supported for anonymous customers, please remove the tags.`,
392
392
  },
393
+ "cannotEditArchivedTemplate": {
394
+ id: `${scope}.cannotEditArchivedTemplate`,
395
+ defaultMessage: 'Cannot edit an archived template. Please unarchive it first.',
396
+ },
393
397
  });
@@ -120,9 +120,8 @@ exports[`Test SlideBoxContent container Email component isTestAndPreviewMode IIF
120
120
  key="cretives-container-email-edit-wrapper"
121
121
  location={
122
122
  Object {
123
- "pathname": "/email/edit/undefined",
123
+ "pathname": "/email/edit/",
124
124
  "query": Object {
125
- "id": undefined,
126
125
  "module": "library",
127
126
  "type": "embedded",
128
127
  },
@@ -171,9 +170,8 @@ exports[`Test SlideBoxContent container Email component isTestAndPreviewMode IIF
171
170
  key="cretives-container-email-edit-wrapper"
172
171
  location={
173
172
  Object {
174
- "pathname": "/email/edit/undefined",
173
+ "pathname": "/email/edit/",
175
174
  "query": Object {
176
- "id": undefined,
177
175
  "module": "library",
178
176
  "type": "embedded",
179
177
  },
@@ -116,7 +116,7 @@ exports[`Test SlideBoxHeader container Should render correct component for rcs c
116
116
  }
117
117
  prefix={
118
118
  <SlideBoxHeader__PrefixWrapper>
119
- <UNDEFINED />
119
+ <BackIcon />
120
120
  </SlideBoxHeader__PrefixWrapper>
121
121
  }
122
122
  title={
@@ -145,7 +145,7 @@ exports[`Test SlideBoxHeader container Should render correct component for rcs c
145
145
  }
146
146
  prefix={
147
147
  <SlideBoxHeader__PrefixWrapper>
148
- <UNDEFINED />
148
+ <BackIcon />
149
149
  </SlideBoxHeader__PrefixWrapper>
150
150
  }
151
151
  title={
@@ -313,7 +313,7 @@ exports[`Test SlideBoxHeader container Should render correct component for whats
313
313
  }
314
314
  prefix={
315
315
  <SlideBoxHeader__PrefixWrapper>
316
- <UNDEFINED />
316
+ <BackIcon />
317
317
  </SlideBoxHeader__PrefixWrapper>
318
318
  }
319
319
  title={
@@ -342,7 +342,7 @@ exports[`Test SlideBoxHeader container Should render correct component for whats
342
342
  }
343
343
  prefix={
344
344
  <SlideBoxHeader__PrefixWrapper>
345
- <UNDEFINED />
345
+ <BackIcon />
346
346
  </SlideBoxHeader__PrefixWrapper>
347
347
  }
348
348
  title={
@@ -284,6 +284,7 @@ exports[`Test SlideBoxContent container campaign message, whatsapp edit all data
284
284
  isEmptyContent={false}
285
285
  isFullMode={false}
286
286
  isLiquidValidationError={false}
287
+ isTemplateArchived={false}
287
288
  isTemplateNameEmpty={false}
288
289
  onCreateNextStep={[Function]}
289
290
  onDiscard={[Function]}
@@ -419,6 +420,7 @@ exports[`Test SlideBoxContent container campaign message, whatsapp edit min data
419
420
  isEmptyContent={false}
420
421
  isFullMode={false}
421
422
  isLiquidValidationError={false}
423
+ isTemplateArchived={false}
422
424
  isTemplateNameEmpty={false}
423
425
  onCreateNextStep={[Function]}
424
426
  onDiscard={[Function]}
@@ -554,6 +556,7 @@ exports[`Test SlideBoxContent container it should clear the url, on channel chan
554
556
  isEmptyContent={false}
555
557
  isFullMode={false}
556
558
  isLiquidValidationError={false}
559
+ isTemplateArchived={false}
557
560
  isTemplateNameEmpty={false}
558
561
  onCreateNextStep={[Function]}
559
562
  onDiscard={[Function]}
@@ -12,6 +12,8 @@ const initialState = fromJS({
12
12
  createTemplateInProgress: false,
13
13
  createResponse: {},
14
14
  isBeeEnabled: null,
15
+ fetchingCmsAccounts: false,
16
+ cmsAccountsLoaded: false,
15
17
  });
16
18
 
17
19
  function emailReducer(state = initialState, action) {
@@ -85,6 +87,7 @@ function emailReducer(state = initialState, action) {
85
87
  case types.GET_CMS_EDITOR_DETAILS_SUCCESS:
86
88
  return state
87
89
  .set('fetchingCmsSettings', false)
90
+ .set('isBeeEnabled', action.isBeeEnabled)
88
91
  .set('CmsSettings', fromJS(action.settings));
89
92
  case types.GET_CMS_EDITOR_DETAILS_FAILURE:
90
93
  return state
@@ -110,13 +113,19 @@ function emailReducer(state = initialState, action) {
110
113
  .set('fetchingCmsDataFailed', true);
111
114
  case types.GET_CMS_ACCOUNTS_REQUEST:
112
115
  return state
113
- .set('isBeeEnabled', false); // default to false
116
+ .set('isBeeEnabled', false)
117
+ .set('fetchingCmsAccounts', true)
118
+ .set('cmsAccountsLoaded', false);
114
119
  case types.GET_CMS_ACCOUNTS_SUCCESS:
115
120
  return state
116
- .set('isBeeEnabled', action.isBeeEnabled);
121
+ .set('isBeeEnabled', action.isBeeEnabled)
122
+ .set('fetchingCmsAccounts', false)
123
+ .set('cmsAccountsLoaded', true);
117
124
  case types.GET_CMS_ACCOUNTS_FAILURE:
118
125
  return state
119
- .set('isBeeEnabled', false);
126
+ .set('isBeeEnabled', false)
127
+ .set('fetchingCmsAccounts', false)
128
+ .set('cmsAccountsLoaded', true);
120
129
  case types.CLEAR_EMAIL_CRUD_RESPONSE_REQUEST:
121
130
  return state
122
131
  .set('createResponse', fromJS({}));
@@ -1,5 +1,5 @@
1
1
  import {
2
- call, put, takeLatest, takeEvery, all,
2
+ call, put, takeLatest, takeEvery, all, select, take,
3
3
  } from 'redux-saga/effects';
4
4
  import CapNotification from '@capillarytech/cap-ui-library/CapNotification';
5
5
  import * as Api from '../../services/api';
@@ -103,14 +103,19 @@ export function* getAllAssets(assetType, queryParams) {
103
103
  }
104
104
 
105
105
  export function* getCmsSetting({
106
- cmsType, projectId, cmsMode, langId, isEdmSupport, isBEEAppEnable,
106
+ cmsType, projectId, cmsMode, langId, isEdmSupport, isBEEAppEnable: isBEEAppEnableFromAction,
107
107
  }) {
108
108
  try {
109
+ const emailState = yield select((state) => state.get('email'));
110
+ if (!emailState.get('cmsAccountsLoaded') && emailState.get('fetchingCmsAccounts')) {
111
+ yield take([types.GET_CMS_ACCOUNTS_SUCCESS, types.GET_CMS_ACCOUNTS_FAILURE]);
112
+ }
113
+ const updatedState = yield select((state) => state.get('email'));
114
+ const isBEEAppEnable = updatedState.get('isBeeEnabled') ?? isBEEAppEnableFromAction;
109
115
  const result = yield call(Api.getCmsTemplateSettingsV2, cmsType, projectId, cmsMode, langId, isEdmSupport, isBEEAppEnable);
110
116
  const cmsAccountDetail = result.data?.response.cmsDetails || {};
111
117
  const isBeeEnabled = cmsAccountDetail?.type === cmsType;
112
- yield put({ type: types.GET_CMS_ACCOUNTS_SUCCESS, isBeeEnabled });
113
- yield put({ type: types.GET_CMS_EDITOR_DETAILS_SUCCESS, settings: result.data.response.cmsDetails });
118
+ yield put({ type: types.GET_CMS_EDITOR_DETAILS_SUCCESS, settings: cmsAccountDetail, isBeeEnabled });
114
119
  } catch (error) {
115
120
  yield put({ type: types.GET_CMS_EDITOR_DETAILS_FAILURE, error });
116
121
  }
@@ -5,6 +5,8 @@ Immutable.Map {
5
5
  "createTemplateInProgress": false,
6
6
  "createResponse": Immutable.Map {},
7
7
  "isBeeEnabled": null,
8
+ "fetchingCmsAccounts": false,
9
+ "cmsAccountsLoaded": false,
8
10
  }
9
11
  `;
10
12
 
@@ -13,5 +15,7 @@ Immutable.Map {
13
15
  "createTemplateInProgress": true,
14
16
  "createResponse": Immutable.Map {},
15
17
  "isBeeEnabled": null,
18
+ "fetchingCmsAccounts": false,
19
+ "cmsAccountsLoaded": false,
16
20
  }
17
21
  `;