@capillarytech/creatives-library 8.0.317 → 8.0.319
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/constants/unified.js +1 -0
- package/package.json +1 -1
- package/services/api.js +6 -0
- package/services/tests/api.test.js +7 -0
- package/utils/common.js +6 -1
- package/v2Containers/CommunicationFlow/CommunicationFlow.js +291 -0
- package/v2Containers/CommunicationFlow/CommunicationFlow.scss +25 -0
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +255 -0
- package/v2Containers/CommunicationFlow/constants.js +200 -0
- package/v2Containers/CommunicationFlow/index.js +102 -0
- package/v2Containers/CommunicationFlow/messages.js +346 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +522 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +170 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +796 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/index.js +5 -0
- package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/CommunicationStrategyStep.js +95 -0
- package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/Tests/CommunicationStrategyStep.test.js +133 -0
- package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/index.js +5 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +289 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.scss +70 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +319 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.scss +69 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +616 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +577 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +1111 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +696 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/index.js +7 -0
- package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.js +102 -0
- package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.scss +36 -0
- package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/Tests/DynamicControlsStep.test.js +91 -0
- package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/index.js +5 -0
- package/v2Containers/CommunicationFlow/steps/MessageTypeStep/MessageTypeStep.js +86 -0
- package/v2Containers/CommunicationFlow/steps/MessageTypeStep/Tests/MessageTypeStep.test.js +100 -0
- package/v2Containers/CommunicationFlow/steps/MessageTypeStep/index.js +5 -0
- package/v2Containers/CommunicationFlow/utils/getEnabledSteps.js +30 -0
- package/v2Containers/CreativesContainer/constants.js +3 -0
- package/v2Containers/CreativesContainer/index.js +1 -1
- package/v2Containers/Rcs/index.js +4 -2
package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/CommunicationStrategyStep.js
ADDED
|
@@ -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,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);
|
package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.scss
ADDED
|
@@ -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
|
+
}
|