@capillarytech/creatives-library 8.0.318 → 8.0.320

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 (139) hide show
  1. package/constants/unified.js +15 -0
  2. package/package.json +1 -1
  3. package/services/api.js +6 -0
  4. package/services/tests/api.test.js +7 -0
  5. package/utils/common.js +6 -1
  6. package/utils/templateVarUtils.js +172 -0
  7. package/utils/tests/templateVarUtils.test.js +160 -0
  8. package/v2Components/CapTagList/index.js +10 -0
  9. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -49
  10. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  11. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +207 -21
  12. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  13. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
  14. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  15. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  16. package/v2Components/CommonTestAndPreview/SendTestMessage.js +11 -5
  17. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +20 -1
  18. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +133 -4
  19. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +12 -0
  20. package/v2Components/CommonTestAndPreview/constants.js +38 -0
  21. package/v2Components/CommonTestAndPreview/index.js +693 -155
  22. package/v2Components/CommonTestAndPreview/messages.js +41 -3
  23. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  24. package/v2Components/CommonTestAndPreview/sagas.js +15 -6
  25. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +352 -0
  26. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +269 -1
  27. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  28. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  29. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +25 -4
  30. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  31. package/v2Components/CommonTestAndPreview/tests/index.test.js +132 -4
  32. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  33. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +2 -2
  34. package/v2Components/FormBuilder/index.js +7 -1
  35. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +87 -0
  36. package/v2Components/SmsFallback/constants.js +73 -0
  37. package/v2Components/SmsFallback/index.js +956 -0
  38. package/v2Components/SmsFallback/index.scss +265 -0
  39. package/v2Components/SmsFallback/messages.js +78 -0
  40. package/v2Components/SmsFallback/smsFallbackUtils.js +107 -0
  41. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  42. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  43. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  44. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +197 -0
  45. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +261 -0
  46. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  47. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  48. package/v2Components/TestAndPreviewSlidebox/index.js +8 -1
  49. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  50. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  51. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  52. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  53. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  54. package/v2Containers/CommunicationFlow/CommunicationFlow.js +291 -0
  55. package/v2Containers/CommunicationFlow/CommunicationFlow.scss +25 -0
  56. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +255 -0
  57. package/v2Containers/CommunicationFlow/constants.js +200 -0
  58. package/v2Containers/CommunicationFlow/index.js +102 -0
  59. package/v2Containers/CommunicationFlow/messages.js +346 -0
  60. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +522 -0
  61. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +170 -0
  62. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +796 -0
  63. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/index.js +5 -0
  64. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/CommunicationStrategyStep.js +95 -0
  65. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/Tests/CommunicationStrategyStep.test.js +133 -0
  66. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/index.js +5 -0
  67. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +289 -0
  68. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.scss +70 -0
  69. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +319 -0
  70. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.scss +69 -0
  71. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +616 -0
  72. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +577 -0
  73. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +1111 -0
  74. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +696 -0
  75. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/index.js +7 -0
  76. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.js +102 -0
  77. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.scss +36 -0
  78. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/Tests/DynamicControlsStep.test.js +91 -0
  79. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/index.js +5 -0
  80. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/MessageTypeStep.js +86 -0
  81. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/Tests/MessageTypeStep.test.js +100 -0
  82. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/index.js +5 -0
  83. package/v2Containers/CommunicationFlow/utils/getEnabledSteps.js +30 -0
  84. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  85. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  86. package/v2Containers/CreativesContainer/SlideBoxFooter.js +10 -1
  87. package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  88. package/v2Containers/CreativesContainer/constants.js +12 -0
  89. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +67 -0
  90. package/v2Containers/CreativesContainer/index.js +289 -99
  91. package/v2Containers/CreativesContainer/index.scss +51 -1
  92. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  93. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +104 -0
  94. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +110 -0
  95. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  96. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +363 -0
  97. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -10
  98. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  99. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  100. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  101. package/v2Containers/Rcs/constants.js +32 -1
  102. package/v2Containers/Rcs/index.js +950 -873
  103. package/v2Containers/Rcs/index.scss +85 -6
  104. package/v2Containers/Rcs/messages.js +10 -1
  105. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +205 -0
  106. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +40834 -1963
  107. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  108. package/v2Containers/Rcs/tests/index.test.js +41 -38
  109. package/v2Containers/Rcs/tests/mockData.js +38 -0
  110. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +251 -0
  111. package/v2Containers/Rcs/tests/utils.test.js +379 -1
  112. package/v2Containers/Rcs/utils.js +358 -10
  113. package/v2Containers/Sms/Create/index.js +81 -36
  114. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  115. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  116. package/v2Containers/SmsTrai/Create/index.js +9 -4
  117. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  118. package/v2Containers/SmsTrai/Edit/index.js +609 -128
  119. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  120. package/v2Containers/SmsTrai/Edit/messages.js +9 -4
  121. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4327 -2374
  122. package/v2Containers/SmsWrapper/index.js +37 -8
  123. package/v2Containers/TagList/index.js +6 -0
  124. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  125. package/v2Containers/Templates/_templates.scss +61 -2
  126. package/v2Containers/Templates/actions.js +11 -0
  127. package/v2Containers/Templates/constants.js +2 -0
  128. package/v2Containers/Templates/index.js +90 -40
  129. package/v2Containers/Templates/sagas.js +57 -12
  130. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  131. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1043 -1079
  132. package/v2Containers/Templates/tests/sagas.test.js +193 -12
  133. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  134. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  135. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  136. package/v2Containers/TemplatesV2/index.js +86 -23
  137. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  138. package/v2Containers/Whatsapp/index.js +3 -20
  139. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -0,0 +1,5 @@
1
+ /**
2
+ * ChannelSelectionStep - Entry point
3
+ */
4
+
5
+ export { default } from './ChannelSelectionStep';
@@ -0,0 +1,95 @@
1
+ /**
2
+ * CommunicationStrategyStep Component
3
+ *
4
+ * Dropdown for selecting Communication Strategy: Single template, Channel priority, A/B Test, etc.
5
+ */
6
+
7
+ import React, { useMemo } from 'react';
8
+ import PropTypes from 'prop-types';
9
+ import { injectIntl } from 'react-intl';
10
+ import CapSelect from '@capillarytech/cap-ui-library/CapSelect';
11
+ import CapRow from '@capillarytech/cap-ui-library/CapRow';
12
+ import CapHeading from '@capillarytech/cap-ui-library/CapHeading';
13
+ import messages from '../../messages';
14
+ import '../../CommunicationFlow.scss';
15
+
16
+ const CommunicationStrategyStep = ({
17
+ value,
18
+ onChange,
19
+ options: optionsProp,
20
+ disabled,
21
+ error,
22
+ intl,
23
+ }) => {
24
+ const { formatMessage } = intl || {};
25
+
26
+ // Normalize options: create custom labels with strategy name and channel type
27
+ const options = useMemo(() => optionsProp?.map((option) => {
28
+ const channelTypeText = option?.isMultiChannel
29
+ ? formatMessage(messages.multipleChannel)
30
+ : formatMessage(messages.singleChannel);
31
+
32
+ const customLabel = (
33
+ <CapRow align="middle" type="flex" justify="space-between">
34
+ <CapHeading type="h5">{option?.label}</CapHeading>
35
+ <CapHeading type="label3">{channelTypeText}</CapHeading>
36
+ </CapRow>
37
+ );
38
+
39
+ return {
40
+ key: option?.value,
41
+ value: option?.value,
42
+ label: customLabel,
43
+ disabled: option?.disabled,
44
+ };
45
+ }), [optionsProp, formatMessage]);
46
+
47
+ const handleChange = (selectedValue) => {
48
+ onChange(selectedValue);
49
+ };
50
+
51
+ // Normalize value to undefined if null/empty to show placeholder
52
+ // CapSelect shows placeholder when value is undefined
53
+ const selectValue = (value === null || value === '') ? undefined : value;
54
+
55
+ return (
56
+ <CapRow className="communication-strategy-step">
57
+ <CapHeading type="h3" className="heading-style">
58
+ {formatMessage(messages.communicationStrategyHeading)}
59
+ </CapHeading>
60
+
61
+ <CapSelect
62
+ value={selectValue}
63
+ onChange={handleChange}
64
+ placeholder={formatMessage(messages.communicationStrategyPlaceholder)}
65
+ options={options}
66
+ style={{ width: '20.375rem' }}
67
+ disabled={disabled}
68
+ />
69
+
70
+ {error && (
71
+ <CapRow className="validation-error">
72
+ {error}
73
+ </CapRow>
74
+ )}
75
+ </CapRow>
76
+ );
77
+ };
78
+
79
+ CommunicationStrategyStep.propTypes = {
80
+ value: PropTypes.string,
81
+ onChange: PropTypes.func.isRequired,
82
+ options: PropTypes.array,
83
+ disabled: PropTypes.bool,
84
+ error: PropTypes.string,
85
+ intl: PropTypes.object.isRequired,
86
+ };
87
+
88
+ CommunicationStrategyStep.defaultProps = {
89
+ value: null,
90
+ error: null,
91
+ disabled: false,
92
+ options: [],
93
+ };
94
+
95
+ export default injectIntl(CommunicationStrategyStep);
@@ -0,0 +1,133 @@
1
+ import React from 'react';
2
+ import { render, screen, waitFor, within } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import '@testing-library/jest-dom';
5
+ import { IntlProvider } from 'react-intl';
6
+ import CommunicationStrategyStep from '../CommunicationStrategyStep';
7
+
8
+ const STRATEGY_OPTIONS = [
9
+ { value: 'SINGLE_TEMPLATE', label: 'Single template', isMultiChannel: false, disabled: false },
10
+ { value: 'CHANNEL_PRIORITY', label: 'Channel priority', isMultiChannel: true, disabled: false },
11
+ ];
12
+
13
+ function renderWithIntl(ui) {
14
+ return render(
15
+ <IntlProvider locale="en" messages={{}} defaultLocale="en">
16
+ {ui}
17
+ </IntlProvider>,
18
+ );
19
+ }
20
+
21
+ async function openStrategySelect() {
22
+ const combo = screen.getByRole('combobox');
23
+ await userEvent.click(combo);
24
+ await waitFor(() => {
25
+ expect(screen.getByRole('listbox')).toBeInTheDocument();
26
+ });
27
+ }
28
+
29
+ describe('CommunicationStrategyStep', () => {
30
+ it('renders heading, placeholder, and strategy dropdown', () => {
31
+ const onChange = jest.fn();
32
+ renderWithIntl(
33
+ <CommunicationStrategyStep
34
+ value={null}
35
+ onChange={onChange}
36
+ options={STRATEGY_OPTIONS}
37
+ disabled={false}
38
+ />,
39
+ );
40
+
41
+ expect(screen.getByText(/communication strategy/i)).toBeInTheDocument();
42
+ expect(screen.getByText('Select strategy')).toBeInTheDocument();
43
+ expect(screen.getByRole('combobox')).toBeInTheDocument();
44
+ });
45
+
46
+ it('calls onChange with the strategy value when user picks an option', async () => {
47
+ const onChange = jest.fn();
48
+ renderWithIntl(
49
+ <CommunicationStrategyStep
50
+ value={null}
51
+ onChange={onChange}
52
+ options={STRATEGY_OPTIONS}
53
+ />,
54
+ );
55
+
56
+ await openStrategySelect();
57
+ await userEvent.click(within(screen.getByRole('listbox')).getByText('Single template'));
58
+
59
+ expect(onChange).toHaveBeenCalledWith('SINGLE_TEMPLATE');
60
+ });
61
+
62
+ it('shows the current value in the select when one is chosen', () => {
63
+ const onChange = jest.fn();
64
+ renderWithIntl(
65
+ <CommunicationStrategyStep
66
+ value="CHANNEL_PRIORITY"
67
+ onChange={onChange}
68
+ options={STRATEGY_OPTIONS}
69
+ />,
70
+ );
71
+
72
+ expect(screen.getByRole('combobox')).toBeInTheDocument();
73
+ expect(screen.getByText('Channel priority')).toBeInTheDocument();
74
+ });
75
+
76
+ it('treats empty string like no selection so placeholder can show', () => {
77
+ const onChange = jest.fn();
78
+ renderWithIntl(
79
+ <CommunicationStrategyStep
80
+ value=""
81
+ onChange={onChange}
82
+ options={STRATEGY_OPTIONS}
83
+ />,
84
+ );
85
+
86
+ expect(screen.getByText('Select strategy')).toBeInTheDocument();
87
+ });
88
+
89
+ it('annotates each option with single vs multiple channel copy', async () => {
90
+ const onChange = jest.fn();
91
+ renderWithIntl(
92
+ <CommunicationStrategyStep
93
+ value={null}
94
+ onChange={onChange}
95
+ options={STRATEGY_OPTIONS}
96
+ />,
97
+ );
98
+
99
+ await openStrategySelect();
100
+ const listbox = screen.getByRole('listbox');
101
+ expect(within(listbox).getByText('Single channel')).toBeInTheDocument();
102
+ expect(within(listbox).getByText('Multiple channel')).toBeInTheDocument();
103
+ });
104
+
105
+ it('disables the select when the step is read-only', () => {
106
+ const onChange = jest.fn();
107
+ renderWithIntl(
108
+ <CommunicationStrategyStep
109
+ value="SINGLE_TEMPLATE"
110
+ onChange={onChange}
111
+ options={STRATEGY_OPTIONS}
112
+ disabled
113
+ />,
114
+ );
115
+
116
+ const combo = screen.getByRole('combobox');
117
+ expect(combo.closest('.ant-select')).toHaveClass('ant-select-disabled');
118
+ });
119
+
120
+ it('shows validation error under the select', () => {
121
+ const onChange = jest.fn();
122
+ renderWithIntl(
123
+ <CommunicationStrategyStep
124
+ value={null}
125
+ onChange={onChange}
126
+ options={STRATEGY_OPTIONS}
127
+ error="Strategy is required"
128
+ />,
129
+ );
130
+
131
+ expect(screen.getByText('Strategy is required')).toBeInTheDocument();
132
+ });
133
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * CommunicationStrategyStep - Entry point
3
+ */
4
+
5
+ export { default } from './CommunicationStrategyStep';
@@ -0,0 +1,289 @@
1
+ /**
2
+ * DeliverySettingsSection Component
3
+ *
4
+ * Self-contained delivery settings section shown below content when configured.
5
+ * Fetches domainProperties, displays sender ID/number, opens slidebox on click.
6
+ * Integrated into ChannelSelectionStep (similar to how steps are integrated in CommunicationFlow).
7
+ */
8
+
9
+ import React, {
10
+ useState, useMemo, useEffect, useRef, useCallback,
11
+ } from 'react';
12
+ import PropTypes from 'prop-types';
13
+ import { injectIntl } from 'react-intl';
14
+ import CapRow from '@capillarytech/cap-ui-library/CapRow';
15
+ import CapHeading from '@capillarytech/cap-ui-library/CapHeading';
16
+ import CapIcon from '@capillarytech/cap-ui-library/CapIcon';
17
+ import CapLabel from '@capillarytech/cap-ui-library/CapLabel';
18
+ import SenderDetails, { parseSenderDetailsFromEntity } from './SenderDetails';
19
+ import {
20
+ buildChannelSettingFromFieldValues,
21
+ parseChannelSettingForDisplay,
22
+ getWhatsappAccountName,
23
+ } from './deliverySettingsConfig';
24
+ import { getDomainProperties, fetchWeCrmAccounts } from '../../../../services/api';
25
+ import { loadItem } from '../../../../services/localStorageApi';
26
+ import { CHANNELS_WITHOUT_DELIVERY } from '../../constants';
27
+ import messages from '../../messages';
28
+ import './DeliverySettingsSection.scss';
29
+ import { WHATSAPP } from "../../../CreativesContainer/constants";
30
+
31
+ const DeliverySettingsSection = ({
32
+ contentItems = [],
33
+ deliverySettingsData,
34
+ deliverySetting = {},
35
+ onDeliverySettingChange,
36
+ intl,
37
+ }) => {
38
+ const [showSlidebox, setShowSlidebox] = useState(false);
39
+ const [domainPropertiesData, setDomainPropertiesData] = useState(null);
40
+ const [wecrmViberData, setWecrmViberData] = useState(null);
41
+ const fetchInFlightRef = useRef(false);
42
+ const lastChannelKeyRef = useRef('');
43
+ const { formatMessage } = intl || {};
44
+
45
+ // Channels from contentItems
46
+ const contentChannels = useMemo(
47
+ () => [...new Set(contentItems.map((contentItem) => (contentItem.channel || '')?.toUpperCase())?.filter(Boolean))],
48
+ [contentItems],
49
+ );
50
+
51
+ // Channels that need delivery settings (exclude MPUSH, INAPP, WEBPUSH)
52
+ const deliveryChannels = useMemo(
53
+ () => contentChannels.filter((channel) => !CHANNELS_WITHOUT_DELIVERY.includes(channel)),
54
+ [contentChannels],
55
+ );
56
+
57
+ // Hide section when all configured channels are MPUSH, INAPP, WEBPUSH
58
+ const shouldShow = useMemo(() => {
59
+ if (contentChannels.length === 0) return false;
60
+ return contentChannels.some((channel) => !CHANNELS_WITHOUT_DELIVERY.includes(channel));
61
+ }, [contentChannels]);
62
+
63
+ // Extract WhatsApp sourceAccountIdentifier from contentItems (set by Whatsapp/index.js when template is created)
64
+ const whatsappSourceAccountId = useMemo(() => {
65
+ const waContentItem = contentItems.find((contentItem) => (contentItem.channel)?.toUpperCase() === WHATSAPP);
66
+ return waContentItem?.templateData?.sourceAccountIdentifier || '';
67
+ }, [contentItems]);
68
+
69
+ // Extract WhatsApp account name from contentItems (set by Whatsapp/index.js)
70
+ const whatsappAccNameFromContent = useMemo(() => {
71
+ const waContentItem = contentItems.find((contentItem) => (contentItem.channel)?.toUpperCase() === WHATSAPP);
72
+ return waContentItem?.templateData?.accountName || '';
73
+ }, [contentItems]);
74
+
75
+ // Stable key for deduplication - avoid re-fetch when array reference changes
76
+ const deliveryChannelKey = deliveryChannels.slice().sort().join(',');
77
+ const deliveryEnabled = !!deliverySettingsData;
78
+ // Fetch domainProperties when content is configured (single source, deduplicated)
79
+ useEffect(() => {
80
+ if (!deliveryEnabled || deliveryChannels.length === 0) {
81
+ setDomainPropertiesData(null);
82
+ lastChannelKeyRef.current = '';
83
+ return undefined;
84
+ }
85
+ if (lastChannelKeyRef.current === deliveryChannelKey && domainPropertiesData) {
86
+ return undefined;
87
+ }
88
+ if (fetchInFlightRef.current) return undefined;
89
+ fetchInFlightRef.current = true;
90
+ lastChannelKeyRef.current = deliveryChannelKey;
91
+ let cancelled = false;
92
+ const fetchData = async () => {
93
+ try {
94
+ const orgUnitId = loadItem('ouId') || loadItem('orgID');
95
+ const response = await getDomainProperties(deliveryChannels, orgUnitId);
96
+ if (!cancelled) {
97
+ const raw = response?.entity || response;
98
+ // Normalize channel keys to uppercase (API may return Viber, viber, etc.)
99
+ const entity = raw && typeof raw === 'object'
100
+ ? Object.fromEntries(
101
+ Object.entries(raw).map(([channelKey, channelData]) => [String(channelKey)?.toUpperCase(), channelData]),
102
+ )
103
+ : raw;
104
+ setDomainPropertiesData(entity);
105
+ }
106
+ } catch (err) {
107
+ if (!cancelled) setDomainPropertiesData(null);
108
+ } finally {
109
+ if (!cancelled) fetchInFlightRef.current = false;
110
+ }
111
+ };
112
+ fetchData();
113
+ return function cleanup() {
114
+ cancelled = true;
115
+ // Do NOT reset fetchInFlightRef here - it would allow duplicate fetches when effect re-runs.
116
+ // fetchInFlightRef is reset in finally when the fetch completes.
117
+ };
118
+ }, [deliveryEnabled, deliveryChannelKey]); // eslint-disable-line react-hooks/exhaustive-deps
119
+
120
+ // Fetch WeCRM accounts for VIBER when domainProperties has empty contactInfo
121
+ const needsWecrmViber = deliveryChannels.includes('VIBER');
122
+ useEffect(() => {
123
+ if (!needsWecrmViber) {
124
+ setWecrmViberData(null);
125
+ return undefined;
126
+ }
127
+ let cancelled = false;
128
+ const fetchWecrm = async () => {
129
+ try {
130
+ const res = await fetchWeCrmAccounts('VIBER');
131
+ if (!cancelled && res?.response) {
132
+ setWecrmViberData(Array.isArray(res.response) ? res.response : [res.response]);
133
+ } else if (!cancelled) {
134
+ setWecrmViberData(null);
135
+ }
136
+ } catch {
137
+ if (!cancelled) setWecrmViberData(null);
138
+ }
139
+ };
140
+ fetchWecrm();
141
+ return () => { cancelled = true; };
142
+ }, [needsWecrmViber]);
143
+
144
+ // Merge WeCRM sender options into entity when VIBER domains have empty contactInfo
145
+ const entityWithWecrmViber = useMemo(() => {
146
+ if (!domainPropertiesData || !wecrmViberData?.length) return domainPropertiesData;
147
+ const viberArr = domainPropertiesData.VIBER;
148
+ if (!Array.isArray(viberArr) || viberArr.length === 0) return domainPropertiesData;
149
+ const hasContactInfo = viberArr.some((domain) => (domain?.domainProperties?.contactInfo?.length ?? 0) > 0);
150
+ if (hasContactInfo) return domainPropertiesData;
151
+ const syntheticContactInfo = wecrmViberData
152
+ ?.filter((account) => account?.isActive !== false)
153
+ ?.map((account, index) => ({
154
+ type: 'viber_sender_id',
155
+ value: account?.sourceAccountIdentifier,
156
+ label: account?.name || account?.configs?.viber_account_name || account?.sourceAccountIdentifier,
157
+ valid: true,
158
+ default: index === 0,
159
+ }));
160
+ if (syntheticContactInfo?.length === 0) return domainPropertiesData;
161
+ const merged = { ...domainPropertiesData };
162
+ merged.VIBER = viberArr?.map((domain, index) => {
163
+ if (index === 0) {
164
+ return {
165
+ ...domain,
166
+ domainProperties: {
167
+ ...(domain?.domainProperties || {}),
168
+ contactInfo: [...(domain?.domainProperties?.contactInfo || []), ...syntheticContactInfo],
169
+ },
170
+ };
171
+ }
172
+ return domain;
173
+ });
174
+ return merged;
175
+ }, [domainPropertiesData, wecrmViberData]);
176
+
177
+ const parsedSenderDetails = useMemo(() => {
178
+ if (!entityWithWecrmViber) return null;
179
+ const fromEntity = parseSenderDetailsFromEntity(entityWithWecrmViber, { whatsappSourceAccountId });
180
+ const fromSaved = parseChannelSettingForDisplay(deliverySetting?.channelSetting);
181
+ // Derive WhatsApp account name: from entity domain or from content templateData
182
+ const waAccountName = whatsappAccNameFromContent
183
+ || getWhatsappAccountName(entityWithWecrmViber, whatsappSourceAccountId);
184
+ return { ...fromEntity, ...fromSaved, whatsappAccountName: waAccountName || fromEntity.whatsappAccountName };
185
+ }, [entityWithWecrmViber, deliverySetting, whatsappSourceAccountId, whatsappAccNameFromContent]);
186
+
187
+ // Map channel -> { labelKey, valueKey } or array of { labelKey, valueKey } for summary display
188
+ // SMS, RCS, EMAIL, VIBER: Sender ID | WHATSAPP: Sender number | LINE: Account
189
+ const channelDisplayMap = {
190
+ SMS: { labelKey: 'senderIdLabel', valueKey: 'smsSenderId' },
191
+ EMAIL: { labelKey: 'senderIdLabel', valueKey: 'emailSenderId' },
192
+ VIBER: { labelKey: 'senderIdLabel', valueKey: 'viberSenderId' },
193
+ ZALO: { labelKey: 'senderIdLabel', valueKey: 'zaloSenderId' },
194
+ LINE: { labelKey: 'accountLabel', valueKey: 'lineSenderId' },
195
+ WHATSAPP: [
196
+ { labelKey: 'senderNumberLabel', valueKey: 'whatsappSenderNumber' },
197
+ ],
198
+ RCS: { labelKey: 'senderNumberLabel', valueKey: 'rcsSenderNumber' },
199
+ };
200
+
201
+ const handleDeliverySettingSave = useCallback((fieldValues) => {
202
+ if (!onDeliverySettingChange) return;
203
+ onDeliverySettingChange({
204
+ ...deliverySetting,
205
+ channelSetting: {
206
+ ...(deliverySetting?.channelSetting || {}),
207
+ ...buildChannelSettingFromFieldValues(fieldValues),
208
+ },
209
+ });
210
+ }, [onDeliverySettingChange, deliverySetting]);
211
+
212
+ if (!deliverySettingsData || !shouldShow) {
213
+ return null;
214
+ }
215
+
216
+ return (
217
+ <>
218
+ <CapRow
219
+ type="flex"
220
+ align="middle"
221
+ justify="space-between"
222
+ className="delivery-settings-section delivery-settings-section--clickable"
223
+ onClick={() => setShowSlidebox(true)}
224
+ >
225
+ <CapRow type="flex" className="delivery-settings-section__content">
226
+ <CapHeading type="h5">
227
+ {formatMessage(messages.senderDetails)}
228
+ </CapHeading>
229
+ {deliveryChannels?.flatMap((channel) => {
230
+ const config = channelDisplayMap[channel];
231
+ if (!config || !parsedSenderDetails) return [];
232
+ const configs = Array.isArray(config) ? config : [config];
233
+ return configs
234
+ .filter((channelConfig) => {
235
+ const value = parsedSenderDetails[channelConfig.valueKey];
236
+ return value != null && value !== '';
237
+ })
238
+ .map((channelConfig) => (
239
+ <CapRow key={`${channel}-${channelConfig.valueKey}`} type="flex" align="middle" justify="space-between" className="delivery-settings-section__field-row">
240
+ <CapHeading type="label4" className="delivery-settings-section__label">
241
+ {formatMessage(messages[channelConfig.labelKey])}
242
+ </CapHeading>
243
+ <CapLabel type="label9" className="delivery-settings-section__value" title={parsedSenderDetails[channelConfig.valueKey]}>
244
+ {parsedSenderDetails[channelConfig.valueKey]}
245
+ </CapLabel>
246
+ </CapRow>
247
+ ));
248
+ })}
249
+ </CapRow>
250
+ <CapIcon
251
+ type="chevron-right"
252
+ size="s"
253
+ className="delivery-settings-section__chevron"
254
+ aria-label="Open delivery settings details"
255
+ />
256
+ </CapRow>
257
+
258
+ {showSlidebox && (
259
+ <SenderDetails
260
+ show={showSlidebox}
261
+ onClose={() => setShowSlidebox(false)}
262
+ channels={deliveryChannels}
263
+ preloadedDomainProperties={entityWithWecrmViber}
264
+ savedFieldValues={parseChannelSettingForDisplay(deliverySetting?.channelSetting)}
265
+ whatsappSourceAccountId={whatsappSourceAccountId}
266
+ whatsappAccountName={whatsappAccNameFromContent || getWhatsappAccountName(entityWithWecrmViber, whatsappSourceAccountId)}
267
+ onSave={handleDeliverySettingSave}
268
+ />
269
+ )}
270
+ </>
271
+ );
272
+ };
273
+
274
+ DeliverySettingsSection.propTypes = {
275
+ contentItems: PropTypes.arrayOf(PropTypes.object),
276
+ deliverySettingsData: PropTypes.object,
277
+ deliverySetting: PropTypes.object,
278
+ onDeliverySettingChange: PropTypes.func,
279
+ intl: PropTypes.object.isRequired,
280
+ };
281
+
282
+ DeliverySettingsSection.defaultProps = {
283
+ contentItems: [],
284
+ deliverySettingsData: null,
285
+ deliverySetting: null,
286
+ onDeliverySettingChange: null,
287
+ };
288
+
289
+ export default injectIntl(DeliverySettingsSection);
@@ -0,0 +1,70 @@
1
+ @import '~@capillarytech/cap-ui-library/styles/_variables.scss';
2
+
3
+ .delivery-settings-section {
4
+ width: 100%;
5
+ min-width: 0;
6
+ max-width: 100%;
7
+ border-bottom: 1px solid $CAP_G07;
8
+ border-left: 1px solid $CAP_G07;
9
+ border-right: 1px solid $CAP_G07;
10
+ border-radius: 0 0 0.25rem 0.25rem;
11
+ margin-top: -0.125rem;
12
+ padding: $CAP_SPACE_12 $CAP_SPACE_16;
13
+ box-sizing: border-box;
14
+
15
+ &--clickable {
16
+ cursor: pointer;
17
+
18
+ &:hover {
19
+ background-color: $CAP_G08;
20
+ }
21
+ }
22
+
23
+ &__content {
24
+ flex-direction: column;
25
+ align-items: flex-start;
26
+ gap: $CAP_SPACE_04;
27
+ min-width: 0;
28
+ flex: 1;
29
+ overflow: hidden;
30
+ max-width: 100%;
31
+ }
32
+
33
+ &__field-row {
34
+ gap: $CAP_SPACE_04;
35
+ min-width: 0;
36
+ width: 100%;
37
+ max-width: 100%;
38
+ }
39
+
40
+ &__label {
41
+ flex-shrink: 0;
42
+ }
43
+
44
+ &__value {
45
+ min-width: 0;
46
+ flex: 1;
47
+ overflow: hidden;
48
+ text-overflow: ellipsis;
49
+ white-space: nowrap;
50
+ word-break: break-all;
51
+ overflow-wrap: anywhere;
52
+ max-width: 100%;
53
+
54
+ // Override cap-ui-library / ant-typography that may force wrapping
55
+ &,
56
+ .ant-typography,
57
+ span {
58
+ overflow: hidden;
59
+ text-overflow: ellipsis;
60
+ white-space: nowrap;
61
+ word-break: break-all;
62
+ }
63
+ }
64
+
65
+ &__chevron {
66
+ flex-shrink: 0;
67
+ color: $CAP_G01;
68
+ }
69
+
70
+ }