@capillarytech/creatives-library 9.0.56-betaa.0 → 9.0.56

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 (116) hide show
  1. package/constants/unified.js +9 -4
  2. package/package.json +1 -1
  3. package/services/api.js +25 -4
  4. package/utils/common.js +0 -12
  5. package/utils/templateVarUtils.js +16 -0
  6. package/utils/tests/templateVarUtils.test.js +44 -0
  7. package/v2Components/CapActionButton/index.js +73 -60
  8. package/v2Components/CapActionButton/index.scss +44 -28
  9. package/v2Components/CapActionButton/messages.js +7 -3
  10. package/v2Components/CapActionButton/tests/index.test.js +17 -1
  11. package/v2Components/CapWhatsappCTA/messages.js +4 -0
  12. package/v2Components/CapWhatsappCarouselButton/index.js +42 -33
  13. package/v2Components/CapWhatsappCarouselButton/index.scss +44 -2
  14. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +13 -0
  15. package/v2Components/CommonTestAndPreview/actions.js +13 -0
  16. package/v2Components/CommonTestAndPreview/constants.js +8 -0
  17. package/v2Components/CommonTestAndPreview/index.js +55 -3
  18. package/v2Components/CommonTestAndPreview/reducer.js +20 -0
  19. package/v2Components/CommonTestAndPreview/sagas.js +28 -0
  20. package/v2Components/CommonTestAndPreview/selectors.js +6 -0
  21. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +106 -0
  22. package/v2Components/CommonTestAndPreview/tests/actions.test.js +15 -0
  23. package/v2Components/CommonTestAndPreview/tests/constants.test.js +9 -1
  24. package/v2Components/CommonTestAndPreview/tests/index.test.js +101 -0
  25. package/v2Components/CommonTestAndPreview/tests/reducer.test.js +41 -0
  26. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +84 -0
  27. package/v2Components/CommonTestAndPreview/tests/selectors.test.js +12 -0
  28. package/v2Components/FormBuilder/Functional/FormBuilderShell.js +46 -320
  29. package/v2Components/FormBuilder/Functional/channels/registry.js +0 -2
  30. package/v2Components/FormBuilder/Functional/constants.js +8 -43
  31. package/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js +28 -175
  32. package/v2Components/FormBuilder/Functional/core/store/formReducer.js +6 -75
  33. package/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js +20 -10
  34. package/v2Components/FormBuilder/Functional/layout/FieldSlot.js +1 -9
  35. package/v2Components/FormBuilder/Functional/layout/SchemaForm.js +3 -20
  36. package/v2Components/FormBuilder/Functional/layout/Section.js +1 -6
  37. package/v2Components/FormBuilder/Functional/tests/fieldSlot.test.js +2 -9
  38. package/v2Components/FormBuilder/Functional/tests/schemaForm.test.js +0 -17
  39. package/v2Components/FormBuilder/index.js +12 -34
  40. package/v2Components/FormBuilder/tests/entryGate.test.js +7 -94
  41. package/v2Components/TestAndPreviewSlidebox/index.js +9 -0
  42. package/v2Containers/CommunicationFlow/CommunicationFlow.js +140 -75
  43. package/v2Containers/CommunicationFlow/CommunicationFlow.scss +18 -2
  44. package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +67 -28
  45. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +503 -51
  46. package/v2Containers/CommunicationFlow/Tests/CommunicationFlowCard.test.js +98 -4
  47. package/v2Containers/CommunicationFlow/Tests/constants.test.js +85 -0
  48. package/v2Containers/CommunicationFlow/constants.js +65 -4
  49. package/v2Containers/CommunicationFlow/index.js +16 -23
  50. package/v2Containers/CommunicationFlow/messages.js +11 -3
  51. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +27 -11
  52. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +4 -0
  53. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +176 -4
  54. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +22 -4
  55. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +14 -4
  56. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +11 -0
  57. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +51 -0
  58. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +11 -4
  59. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +19 -5
  60. package/v2Containers/CreativesContainer/SlideBoxContent.js +15 -0
  61. package/v2Containers/CreativesContainer/SlideBoxFooter.js +6 -2
  62. package/v2Containers/CreativesContainer/index.js +29 -3
  63. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +29 -0
  64. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +18 -5
  65. package/v2Containers/CreativesContainer/tests/index.test.js +12 -0
  66. package/v2Containers/MobilePush/Create/index.js +18 -1
  67. package/v2Containers/MobilePush/Create/test/contentValidity.test.js +96 -0
  68. package/v2Containers/MobilePush/Edit/index.js +18 -1
  69. package/v2Containers/MobilePush/Edit/test/contentValidity.test.js +116 -0
  70. package/v2Containers/MobilePush/commonMethods.js +49 -1
  71. package/v2Containers/MobilePushNew/index.js +29 -4
  72. package/v2Containers/MobilePushNew/tests/index.test.js +119 -0
  73. package/v2Containers/MobilePushNew/tests/utils.test.js +82 -0
  74. package/v2Containers/MobilePushNew/utils.js +34 -1
  75. package/v2Containers/MobilepushWrapper/index.js +3 -1
  76. package/v2Containers/Rcs/index.js +34 -0
  77. package/v2Containers/Rcs/index.scss +15 -0
  78. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +115 -19
  79. package/v2Containers/Rcs/tests/index.test.js +67 -0
  80. package/v2Containers/Sms/Create/index.js +3 -1
  81. package/v2Containers/Sms/Edit/index.js +25 -0
  82. package/v2Containers/Sms/Edit/tests/index.test.js +85 -0
  83. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +27 -3
  84. package/v2Containers/Viber/index.js +24 -1
  85. package/v2Containers/Viber/tests/index.test.js +103 -0
  86. package/v2Containers/WebPush/Create/index.js +19 -0
  87. package/v2Containers/WebPush/Create/preview/WebPushPreview.js +17 -12
  88. package/v2Containers/WebPush/Create/preview/tests/WebPushPreview.test.js +5 -0
  89. package/v2Containers/WebPush/Create/tests/contentValidity.test.js +294 -0
  90. package/v2Containers/Whatsapp/index.js +25 -4
  91. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +306 -34
  92. package/v2Containers/Whatsapp/tests/index.test.js +115 -0
  93. package/v2Containers/Zalo/index.js +28 -1
  94. package/v2Containers/Zalo/tests/index.test.js +99 -0
  95. package/v2Containers/mockdata.js +25 -0
  96. package/v2Components/FormBuilder/Functional/channels/mobilepush/buildSubmitPayload.js +0 -10
  97. package/v2Components/FormBuilder/Functional/channels/mobilepush/config.js +0 -82
  98. package/v2Components/FormBuilder/Functional/channels/mobilepush/getEditorErrorDescriptor.js +0 -74
  99. package/v2Components/FormBuilder/Functional/channels/mobilepush/index.js +0 -28
  100. package/v2Components/FormBuilder/Functional/channels/mobilepush/modals.js +0 -19
  101. package/v2Components/FormBuilder/Functional/channels/mobilepush/runSubmitPipeline.js +0 -45
  102. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/getEditorErrorDescriptor.test.js +0 -162
  103. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/modals.test.js +0 -37
  104. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/runSubmitPipeline.test.js +0 -70
  105. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/validate.test.js +0 -274
  106. package/v2Components/FormBuilder/Functional/channels/mobilepush/validate.js +0 -251
  107. package/v2Components/FormBuilder/Functional/layout/TabsContainer.js +0 -89
  108. package/v2Components/FormBuilder/Functional/renderers/mpushRenderers.js +0 -440
  109. package/v2Components/FormBuilder/Functional/tests/mpush.crossFlowParity.test.js +0 -430
  110. package/v2Components/FormBuilder/Functional/tests/mpush.ctaFlows.parity.test.js +0 -282
  111. package/v2Components/FormBuilder/Functional/tests/mpush.editContainer.parity.test.js +0 -141
  112. package/v2Components/FormBuilder/Functional/tests/mpush.engine.test.js +0 -306
  113. package/v2Components/FormBuilder/Functional/tests/mpush.shellEvents.test.js +0 -151
  114. package/v2Components/FormBuilder/Functional/tests/mpushRenderers.test.js +0 -342
  115. package/v2Components/FormBuilder/Functional/tests/tabsContainer.test.js +0 -110
  116. package/v2Components/FormBuilder/tests/mpush.characterization.test.js +0 -459
@@ -108,6 +108,22 @@ describe('CommunicationFlowCard', () => {
108
108
  fireEvent.click(screen.getByText('Add creatives'));
109
109
  expect(screen.getByTestId('comm-flow-mock')).toHaveAttribute('data-mode', 'create');
110
110
  });
111
+
112
+ it('disables the Add creatives button when disabled prop is true', () => {
113
+ renderCard({ disabled: true, disabledTooltip: 'Enter an Alert name to add content' });
114
+ expect(screen.getByText('Add creatives').closest('button')).toBeDisabled();
115
+ });
116
+
117
+ it('does not open SlideBox when Add creatives is clicked while disabled', () => {
118
+ renderCard({ disabled: true, disabledTooltip: 'Enter an Alert name to add content' });
119
+ fireEvent.click(screen.getByText('Add creatives'));
120
+ expect(screen.queryByTestId('slide-box')).not.toBeInTheDocument();
121
+ });
122
+
123
+ it('Add creatives button is enabled by default (disabled prop absent)', () => {
124
+ renderCard();
125
+ expect(screen.getByText('Add creatives').closest('button')).not.toBeDisabled();
126
+ });
111
127
  });
112
128
 
113
129
  describe('configured state (initialData provided)', () => {
@@ -186,6 +202,26 @@ describe('CommunicationFlowCard', () => {
186
202
  renderCard({ initialData: makeSavedData() });
187
203
  expect(screen.getByText('Other controls')).toBeInTheDocument();
188
204
  });
205
+
206
+ it('hides the Other controls column when config.features.showDynamicControls is false', () => {
207
+ renderCard({
208
+ config: { ...baseConfig, features: { showDynamicControls: false } },
209
+ initialData: makeSavedData({
210
+ dynamicControls: {
211
+ sendToControlCustomers: false, sendToBrandPocs: false, useTinyUrl: false, overrideDailyLimit: false,
212
+ },
213
+ }),
214
+ });
215
+ expect(screen.queryByText('Other controls')).not.toBeInTheDocument();
216
+ });
217
+
218
+ it('still shows the Other controls column when showDynamicControls is unset (Engagement Module default)', () => {
219
+ renderCard({
220
+ config: { ...baseConfig, features: {} },
221
+ initialData: makeSavedData(),
222
+ });
223
+ expect(screen.getByText('Other controls')).toBeInTheDocument();
224
+ });
189
225
  });
190
226
 
191
227
  describe('channel config resolution', () => {
@@ -231,11 +267,11 @@ describe('CommunicationFlowCard', () => {
231
267
  expect(screen.getByText(/Sender ID/)).toBeInTheDocument();
232
268
  });
233
269
 
234
- it('shows senderEmail for EMAIL with Sender ID label', () => {
270
+ it('shows senderId for EMAIL with Sender ID label', () => {
235
271
  renderCard({
236
272
  initialData: makeSavedData({
237
273
  contentItems: [{ channel: 'EMAIL', templateData: {} }],
238
- deliverySetting: { channelSetting: { EMAIL: { senderEmail: 'test@example.com' } } },
274
+ deliverySetting: { channelSetting: { EMAIL: { senderId: 'test@example.com' } } },
239
275
  }),
240
276
  });
241
277
  expect(screen.getByText(/test@example.com/)).toBeInTheDocument();
@@ -545,10 +581,10 @@ describe('CommunicationFlowCard', () => {
545
581
  });
546
582
 
547
583
  describe('SlideBox header', () => {
548
- it('renders Add message title in the slidebox header', () => {
584
+ it('renders Add content title in the slidebox header', () => {
549
585
  renderCard();
550
586
  fireEvent.click(screen.getByText('Add creatives'));
551
- expect(screen.getByText('Add message')).toBeInTheDocument();
587
+ expect(screen.getByText('Add content')).toBeInTheDocument();
552
588
  });
553
589
  });
554
590
 
@@ -616,4 +652,62 @@ describe('CommunicationFlowCard', () => {
616
652
  expect(screen.getByText('Message:')).toBeInTheDocument();
617
653
  });
618
654
  });
655
+
656
+ describe('async initialData (edit page hard reload)', () => {
657
+ // On a hard reload of an edit page, the consumer's own by-id fetch hasn't
658
+ // resolved at first mount, so initialData starts as null and arrives a
659
+ // render or two later. savedData must pick it up then, not just at mount.
660
+ it('shows the summary card once initialData arrives after mount, instead of staying stuck on the empty "Add creatives" state', () => {
661
+ const { rerender } = render(
662
+ <IntlProvider locale="en" messages={{}} defaultLocale="en">
663
+ <CommunicationFlowCard
664
+ config={baseConfig}
665
+ initialData={null}
666
+ onSave={jest.fn()}
667
+ onCancel={jest.fn()}
668
+ onChange={jest.fn()}
669
+ cap={{}}
670
+ />
671
+ </IntlProvider>,
672
+ );
673
+ expect(screen.getByText('Add creatives')).toBeInTheDocument();
674
+
675
+ rerender(
676
+ <IntlProvider locale="en" messages={{}} defaultLocale="en">
677
+ <CommunicationFlowCard
678
+ config={baseConfig}
679
+ initialData={makeSavedData()}
680
+ onSave={jest.fn()}
681
+ onCancel={jest.fn()}
682
+ onChange={jest.fn()}
683
+ cap={{}}
684
+ />
685
+ </IntlProvider>,
686
+ );
687
+ expect(screen.getByText('Message:')).toBeInTheDocument();
688
+ expect(screen.queryByText('Add creatives')).not.toBeInTheDocument();
689
+ });
690
+
691
+ it('does not clobber content the user already saved locally when a later render passes a different initialData', () => {
692
+ const { rerender } = renderCard({ initialData: null });
693
+ fireEvent.click(screen.getByText('Add creatives'));
694
+ fireEvent.click(screen.getByTestId('flow-save'));
695
+ expect(screen.getByText('Message:')).toBeInTheDocument();
696
+
697
+ rerender(
698
+ <IntlProvider locale="en" messages={{}} defaultLocale="en">
699
+ <CommunicationFlowCard
700
+ config={baseConfig}
701
+ initialData={makeSavedData({ contentItems: [{ channel: 'EMAIL', templateData: {} }] })}
702
+ onSave={jest.fn()}
703
+ onCancel={jest.fn()}
704
+ onChange={jest.fn()}
705
+ cap={{}}
706
+ />
707
+ </IntlProvider>,
708
+ );
709
+ // Locally-saved SMS content must survive; the later initialData prop must not overwrite it.
710
+ expect(screen.getByText('SMS')).toBeInTheDocument();
711
+ });
712
+ });
619
713
  });
@@ -0,0 +1,85 @@
1
+ import {
2
+ CCS_CONTENT_TRANSFORMS,
3
+ CCS_CHANNEL_NAME_MAP,
4
+ CCS_CHANNEL_CONTENT_KEY_MAP,
5
+ CCS_CHANNEL_DELIVERY_KEY_MAP,
6
+ } from '../constants';
7
+
8
+ describe('CCS_CONTENT_TRANSFORMS', () => {
9
+ describe('EMAIL', () => {
10
+ it('prefers emailBody over emailHtml when both are present', () => {
11
+ expect(
12
+ CCS_CONTENT_TRANSFORMS.EMAIL({
13
+ emailSubject: 'Subject', emailBody: '<p>Body</p>', emailHtml: '<p>Html</p>',
14
+ }),
15
+ ).toEqual({ messageSubject: 'Subject', messageBody: '<p>Body</p>' });
16
+ });
17
+
18
+ it('falls back to emailHtml when emailBody is absent', () => {
19
+ expect(
20
+ CCS_CONTENT_TRANSFORMS.EMAIL({ emailSubject: 'Subject', emailHtml: '<p>Html</p>' }),
21
+ ).toEqual({ messageSubject: 'Subject', messageBody: '<p>Html</p>' });
22
+ });
23
+
24
+ it('defaults templateData to {} when called with no argument', () => {
25
+ expect(CCS_CONTENT_TRANSFORMS.EMAIL()).toEqual({
26
+ messageSubject: undefined, messageBody: undefined,
27
+ });
28
+ });
29
+ });
30
+
31
+ describe('SMS', () => {
32
+ it('uses messageBody when present', () => {
33
+ expect(CCS_CONTENT_TRANSFORMS.SMS({ messageBody: 'Hello' })).toEqual({ message: 'Hello' });
34
+ });
35
+
36
+ it('falls back to an empty string when messageBody is absent', () => {
37
+ expect(CCS_CONTENT_TRANSFORMS.SMS({})).toEqual({ message: '' });
38
+ });
39
+
40
+ it('defaults templateData to {} when called with no argument', () => {
41
+ expect(CCS_CONTENT_TRANSFORMS.SMS()).toEqual({ message: '' });
42
+ });
43
+ });
44
+
45
+ describe('WEBPUSH', () => {
46
+ it('extracts fields from the nested messageContent.content wrapper', () => {
47
+ expect(
48
+ CCS_CONTENT_TRANSFORMS.WEBPUSH({
49
+ messageContent: {
50
+ content: {
51
+ messageSubject: 'Subject', accountId: 'acc_1', content: 'Body', offers: [{ id: 1 }],
52
+ },
53
+ },
54
+ }),
55
+ ).toEqual({ messageSubject: 'Subject', accountId: 'acc_1', content: 'Body' });
56
+ });
57
+
58
+ it('falls back to the flat templateData when messageContent.content is absent', () => {
59
+ expect(
60
+ CCS_CONTENT_TRANSFORMS.WEBPUSH({
61
+ messageSubject: 'Subject', accountId: 'acc_1', content: 'Body',
62
+ }),
63
+ ).toEqual({ messageSubject: 'Subject', accountId: 'acc_1', content: 'Body' });
64
+ });
65
+
66
+ it('defaults templateData to {} when called with no argument', () => {
67
+ expect(CCS_CONTENT_TRANSFORMS.WEBPUSH()).toEqual({
68
+ messageSubject: undefined, accountId: undefined, content: undefined,
69
+ });
70
+ });
71
+ });
72
+ });
73
+
74
+ describe('CCS_CHANNEL_NAME_MAP', () => {
75
+ it('normalizes the UI\'s MOBILEPUSH identifier to CCS\'s PUSH enum value', () => {
76
+ expect(CCS_CHANNEL_NAME_MAP.MOBILEPUSH).toBe('PUSH');
77
+ });
78
+ });
79
+
80
+ describe('CCS_CHANNEL_CONTENT_KEY_MAP / CCS_CHANNEL_DELIVERY_KEY_MAP', () => {
81
+ it('maps the PUSH channel to its mpush-prefixed content and delivery keys', () => {
82
+ expect(CCS_CHANNEL_CONTENT_KEY_MAP.PUSH).toBe('mpushMessageContent');
83
+ expect(CCS_CHANNEL_DELIVERY_KEY_MAP.PUSH).toBe('mpushDeliverySettings');
84
+ });
85
+ });
@@ -38,9 +38,68 @@ export const CHANNEL_PRIORITY = 'CHANNEL_PRIORITY';
38
38
  export const AB_TEST = 'AB_TEST';
39
39
  export const SINGLE_TEMPLATE = 'SINGLE_TEMPLATE';
40
40
 
41
- // Channel config - single source of truth for CreativesContainer and TemplatesV2
42
- // paneKey: TemplatesV2 defaultPanes object key (for channelsToHide)
43
- // channelProp: CreativesContainer channel prop (must match pane.key for tab to be active)
41
+ // CCS create is only supported for SINGLE (CHANNEL_PRIORITY/AB_TEST skip CCS entirely).
42
+ export const CCS_STRATEGY_TYPE_SINGLE = 'SINGLE';
43
+
44
+ // Mirrors CCS SingleChannelStrategy field names; kept separate from the legacy
45
+ // messageMeta CHANNEL_CONTENT_KEY_MAP/CHANNEL_DELIVERY_KEY_MAP.
46
+ export const CCS_CHANNEL_CONTENT_KEY_MAP = {
47
+ SMS: 'smsMessageContent',
48
+ EMAIL: 'emailMessageContent',
49
+ WHATSAPP: 'whatsappMessageContent',
50
+ // CCS's channel enum for mobile push is 'PUSH', but content/delivery keys stay 'mpush*'.
51
+ PUSH: 'mpushMessageContent',
52
+ INAPP: 'inAppMessageContent',
53
+ ANDROID: 'androidMessageContent',
54
+ IOS: 'iosMessageContent',
55
+ ZALO: 'zaloMessageContent',
56
+ VIBER: 'viberMessageContent',
57
+ LINE: 'lineMessageContent',
58
+ WEBPUSH: 'webPushMessageContent',
59
+ RCS: 'rcsMessageContent',
60
+ };
61
+
62
+ // Transforms legacy messageMeta field names to CCS's before sending, for channels
63
+ // where the two shapes don't already match.
64
+ export const CCS_CONTENT_TRANSFORMS = {
65
+ EMAIL: (templateData = {}) => ({
66
+ messageSubject: templateData.emailSubject,
67
+ messageBody: templateData.emailBody || templateData.emailHtml,
68
+ }),
69
+ SMS: (templateData = {}) => ({
70
+ message: templateData.messageBody || '',
71
+ }),
72
+ WEBPUSH: (templateData = {}) => {
73
+ const inner = templateData.messageContent?.content || templateData;
74
+ return {
75
+ messageSubject: inner.messageSubject,
76
+ accountId: inner.accountId,
77
+ content: inner.content,
78
+ };
79
+ },
80
+ };
81
+
82
+ export const CCS_CHANNEL_DELIVERY_KEY_MAP = {
83
+ SMS: 'smsDeliverySettings',
84
+ EMAIL: 'emailDeliverySettings',
85
+ WHATSAPP: 'whatsappDeliverySettings',
86
+ PUSH: 'mpushDeliverySettings',
87
+ INAPP: 'inAppDeliverySettings',
88
+ ANDROID: 'androidDeliverySettings',
89
+ IOS: 'iosDeliverySettings',
90
+ ZALO: 'zaloDeliverySettings',
91
+ VIBER: 'viberDeliverySettings',
92
+ LINE: 'lineDeliverySettings',
93
+ WEBPUSH: 'webPushDeliverySettings',
94
+ RCS: 'rcsDeliverySettings',
95
+ };
96
+
97
+ // UI uses `MOBILEPUSH`; CCS uses `PUSH` for the channel enum. Other channels pass through.
98
+ export const CCS_CHANNEL_NAME_MAP = {
99
+ MOBILEPUSH: 'PUSH',
100
+ };
101
+
102
+ // Channel config is the shared source of truth: paneKey maps TemplatesV2 panes, channelProp maps CreativesContainer tabs.
44
103
  export const CHANNELS = [
45
104
  {
46
105
  value: 'SMS',
@@ -232,4 +291,6 @@ export const INCENTIVE_TYPES = [
232
291
  {
233
292
  value: 'badges', label: <FormattedMessage {...messages.badges} />, isActive: false, disabled: false,
234
293
  },
235
- ];
294
+ ];
295
+
296
+ export const REFERENCE_ID_EXISTS = 'REFERENCE_ID_EXISTS';
@@ -9,7 +9,6 @@
9
9
  import React from 'react';
10
10
  import PropTypes from 'prop-types';
11
11
  import CommunicationFlow from './CommunicationFlow';
12
- import { hasSupportEngagementModule } from '../../utils/common';
13
12
 
14
13
  const CommunicationFlowContainer = ({
15
14
  config,
@@ -18,26 +17,20 @@ const CommunicationFlowContainer = ({
18
17
  onCancel,
19
18
  onChange,
20
19
  ...otherProps
21
- }) => {
22
- if (!hasSupportEngagementModule()) {
23
- return null;
24
- }
25
-
26
- return (
27
- <CommunicationFlow
28
- config={config}
29
- initialData={initialData}
30
- onSave={onSave}
31
- onCancel={onCancel}
32
- onChange={onChange}
33
- {...otherProps}
34
- />
35
- );
36
- };
20
+ }) => (
21
+ <CommunicationFlow
22
+ config={config}
23
+ initialData={initialData}
24
+ onSave={onSave}
25
+ onCancel={onCancel}
26
+ onChange={onChange}
27
+ {...otherProps}
28
+ />
29
+ );
37
30
 
38
31
  CommunicationFlowContainer.propTypes = {
39
32
  config: PropTypes.shape({
40
- consumer: PropTypes.oneOf(['campaigns', 'loyalty', 'adiona']).isRequired,
33
+ consumer: PropTypes.oneOf(['campaigns', 'loyalty', 'adiona', 'coupons', 'cartPromotions']).isRequired,
41
34
  channel: PropTypes.string, // optional if channel step is skipped
42
35
  mode: PropTypes.oneOf(['create', 'edit', 'preview']).isRequired,
43
36
  channelsToHide: PropTypes.arrayOf(PropTypes.string),
@@ -87,13 +80,13 @@ CommunicationFlowContainer.propTypes = {
87
80
  controls: PropTypes.array,
88
81
  }),
89
82
  }),
90
- context: PropTypes.object, // ouId, campaignId, programId, etc.
91
- useCCS: PropTypes.bool, // If false, skips CCS bulk-claim-approve on save. Defaults to true.
83
+ context: PropTypes.object, // ouId, campaignId, programId, name, referenceId, description
84
+ useCCS: PropTypes.bool, // skips the CCS createCommDefinition call on save when false; defaults to true
92
85
  }).isRequired,
93
86
  initialData: PropTypes.object, // for edit/preview mode
94
- onSave: PropTypes.func.isRequired, // (data) => void - called when user saves
95
- onCancel: PropTypes.func.isRequired, // () => void - called when user cancels
96
- onChange: PropTypes.func, // (data) => void - optional, called on data changes
87
+ onSave: PropTypes.func.isRequired, // (data) => void, data.ccsCommDefinition set when CCS create succeeded
88
+ onCancel: PropTypes.func.isRequired, // () => void
89
+ onChange: PropTypes.func, // (data) => void
97
90
  };
98
91
 
99
92
  CommunicationFlowContainer.defaultProps = {
@@ -64,9 +64,9 @@ export default {
64
64
  },
65
65
 
66
66
  // Slidebox header
67
- addMessage: {
68
- id: `${prefix}.addMessage`,
69
- defaultMessage: 'Add message',
67
+ addContent: {
68
+ id: `${prefix}.addContent`,
69
+ defaultMessage: 'Add content',
70
70
  },
71
71
 
72
72
  // Step prompts
@@ -363,4 +363,12 @@ export default {
363
363
  id: `${prefix}.senderNotConfiguredError`,
364
364
  defaultMessage: 'Selected domain gateway id is not correct. Please change the domain id or contact the gateway team to register them with Capillary.',
365
365
  },
366
+ duplicateReferenceIdError: {
367
+ id: `${prefix}.duplicateReferenceIdError`,
368
+ defaultMessage: 'This reference ID is already in use in your organization. Please use a different reference ID.',
369
+ },
370
+ genericSaveError: {
371
+ id: `${prefix}.genericSaveError`,
372
+ defaultMessage: 'Something went wrong while saving your content. Please try again.',
373
+ },
366
374
  };
@@ -45,6 +45,7 @@ const ChannelSelectionStep = ({
45
45
  onChange,
46
46
  channelsToHide = [],
47
47
  channelsToDisable = [],
48
+ disablePreviewAndTest = false,
48
49
  creativesMode = 'create',
49
50
  selectedOfferDetails = [],
50
51
  incentivesData,
@@ -63,6 +64,7 @@ const ChannelSelectionStep = ({
63
64
  const [showIncentivesMenuMap, setShowIncentivesMenuMap] = useState({});
64
65
  const [showTestAndPreview, setShowTestAndPreview] = useState(false);
65
66
  const [testAndPreviewItem, setTestAndPreviewItem] = useState(null);
67
+ const [domainPropertiesData, setDomainPropertiesData] = useState(null);
66
68
  const { formatMessage } = intl || {};
67
69
 
68
70
  // Available channels (filter out hidden ones)
@@ -76,6 +78,19 @@ const ChannelSelectionStep = ({
76
78
  const selectedChannelLowerCase = selectedChannel?.toLowerCase();
77
79
  return CHANNELS.find((channel) => channel?.value?.toLowerCase() === selectedChannelLowerCase || channel?.channelProp === selectedChannelLowerCase) || null;
78
80
  }, [selectedChannel]);
81
+
82
+ const editingZaloHostName = useMemo(() => {
83
+ if (!editingContentId) return '';
84
+ const editingItem = contentItems.find((c) => c.contentId === editingContentId);
85
+ if (editingItem?.channel?.toUpperCase() !== ZALO) return '';
86
+ const accountId = editingItem?.templateData?.accountId;
87
+ if (!accountId) return '';
88
+ const zaloDomains = domainPropertiesData?.ZALO || [];
89
+ const matchedDomain = zaloDomains.find(
90
+ (domain) => String(domain?.domainProperties?.connectionProperties?.oa_id) === String(accountId),
91
+ );
92
+ return matchedDomain?.domainProperties?.hostName || '';
93
+ }, [editingContentId, contentItems, domainPropertiesData]);
79
94
  /**
80
95
  * Handle CreativesContainer close
81
96
  */
@@ -236,6 +251,7 @@ const ChannelSelectionStep = ({
236
251
  };
237
252
 
238
253
  const handleChannelsVisibleChange = (visible) => {
254
+ if (visible && renderChannelDropdownOverlay() === null) return;
239
255
  setShowChannelsMenu(visible);
240
256
  };
241
257
 
@@ -358,7 +374,7 @@ const ChannelSelectionStep = ({
358
374
  >
359
375
  {formatMessage(messages.edit)}
360
376
  </CapMenu.Item>
361
- {!PREVIEW_TEST_UNSUPPORTED_CHANNELS.includes(item?.channel?.toUpperCase()) && (
377
+ {!disablePreviewAndTest && !PREVIEW_TEST_UNSUPPORTED_CHANNELS.includes(item?.channel?.toUpperCase()) && (
362
378
  <CapMenu.Item
363
379
  id="preview-menu-item"
364
380
  className="ant-dropdown-menu-item"
@@ -407,10 +423,6 @@ const ChannelSelectionStep = ({
407
423
  return (
408
424
  <>
409
425
  <CapRow className="channel-selection-step">
410
- <CapHeading type="h3" className="margin-b-8">
411
- {formatMessage(messages.contentTemplate)}
412
- </CapHeading>
413
-
414
426
  <CapRow
415
427
  useLegacy
416
428
  className={`content-template-container ${contentItems?.length > 0 ? 'content-template-container--has-content' : ''}`}
@@ -422,11 +434,12 @@ const ChannelSelectionStep = ({
422
434
  )}
423
435
 
424
436
  {contentItems?.length === 0 ? (
425
- <CapRow className="content-template-section">
437
+ <CapRow className={`content-template-section ${contentItems?.length === 0 ? 'no-content-items' : ''}`}>
426
438
  <CapDropdown
427
439
  overlay={renderChannelDropdownOverlay() || <CapMenu />}
428
440
  onVisibleChange={handleChannelsVisibleChange}
429
441
  visible={showChannelsMenu}
442
+ trigger={['click']}
430
443
  >
431
444
  <CapButton
432
445
  isAddBtn
@@ -470,6 +483,8 @@ const ChannelSelectionStep = ({
470
483
  deliverySettingsData={deliverySettingsData}
471
484
  deliverySetting={value?.deliverySetting}
472
485
  onDeliverySettingChange={(deliverySetting) => onChange({ deliverySetting })}
486
+ onDomainPropertiesLoaded={setDomainPropertiesData}
487
+ orgUnitId={config?.context?.ouId}
473
488
  intl={intl}
474
489
  />
475
490
  )}
@@ -490,14 +505,13 @@ const ChannelSelectionStep = ({
490
505
  getCreativesData={handleCreativesData}
491
506
  handleCloseCreatives={handleCloseCreatives}
492
507
  isFullMode={false}
493
- messageDetails={{ type: 'default' }}
508
+ hostName={editingZaloHostName}
509
+ messageDetails={{ type: (config?.context?.module || '').toLowerCase() || 'default' }}
510
+ location={{ query: { type: 'embedded', module: config?.context?.module } }}
494
511
  templateData={editingContentId ? (() => {
495
512
  const saved = contentItems.find((c) => c.contentId === editingContentId)?.templateData;
496
- // getTemplateData in CreativesContainer reads 'content', 'accountId', 'messageSubject' at
497
- // top-level and needs 'type' for SlideBoxContent to set isEditWebPush. Our stored WEBPUSH
498
- // templateData wraps those fields inside messageContent.content, so extract them here.
499
513
  if (saved?.channel?.toUpperCase() === WEBPUSH && saved?.messageContent?.content) {
500
- return { ...saved.messageContent.content, type: WEBPUSH };
514
+ return { ...saved.messageContent.content, type: WEBPUSH, channel: WEBPUSH };
501
515
  }
502
516
  return saved;
503
517
  })() : null}
@@ -555,6 +569,7 @@ ChannelSelectionStep.propTypes = {
555
569
  onChange: PropTypes.func.isRequired,
556
570
  channelsToHide: PropTypes.array,
557
571
  channelsToDisable: PropTypes.array,
572
+ disablePreviewAndTest: PropTypes.bool,
558
573
  creativesMode: PropTypes.oneOf(['create', 'edit', 'preview']),
559
574
  selectedOfferDetails: PropTypes.array,
560
575
  incentivesData: PropTypes.shape({
@@ -576,6 +591,7 @@ ChannelSelectionStep.defaultProps = {
576
591
  error: null,
577
592
  channelsToHide: [],
578
593
  channelsToDisable: [],
594
+ disablePreviewAndTest: false,
579
595
  creativesMode: 'create',
580
596
  selectedOfferDetails: [],
581
597
  incentivesData: null,
@@ -130,6 +130,10 @@
130
130
  border-bottom: 1px solid $CAP_G07;
131
131
  }
132
132
 
133
+ .no-content-items {
134
+ border-bottom: none;
135
+ }
136
+
133
137
  .add-content-template-button {
134
138
  padding: $CAP_SPACE_12 $CAP_SPACE_16;
135
139
  align-self: center !important;