@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.
- package/constants/unified.js +9 -4
- package/package.json +1 -1
- package/services/api.js +25 -4
- package/utils/common.js +0 -12
- package/utils/templateVarUtils.js +16 -0
- package/utils/tests/templateVarUtils.test.js +44 -0
- package/v2Components/CapActionButton/index.js +73 -60
- package/v2Components/CapActionButton/index.scss +44 -28
- package/v2Components/CapActionButton/messages.js +7 -3
- package/v2Components/CapActionButton/tests/index.test.js +17 -1
- package/v2Components/CapWhatsappCTA/messages.js +4 -0
- package/v2Components/CapWhatsappCarouselButton/index.js +42 -33
- package/v2Components/CapWhatsappCarouselButton/index.scss +44 -2
- package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +13 -0
- package/v2Components/CommonTestAndPreview/actions.js +13 -0
- package/v2Components/CommonTestAndPreview/constants.js +8 -0
- package/v2Components/CommonTestAndPreview/index.js +55 -3
- package/v2Components/CommonTestAndPreview/reducer.js +20 -0
- package/v2Components/CommonTestAndPreview/sagas.js +28 -0
- package/v2Components/CommonTestAndPreview/selectors.js +6 -0
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +106 -0
- package/v2Components/CommonTestAndPreview/tests/actions.test.js +15 -0
- package/v2Components/CommonTestAndPreview/tests/constants.test.js +9 -1
- package/v2Components/CommonTestAndPreview/tests/index.test.js +101 -0
- package/v2Components/CommonTestAndPreview/tests/reducer.test.js +41 -0
- package/v2Components/CommonTestAndPreview/tests/sagas.test.js +84 -0
- package/v2Components/CommonTestAndPreview/tests/selectors.test.js +12 -0
- package/v2Components/FormBuilder/Functional/FormBuilderShell.js +46 -320
- package/v2Components/FormBuilder/Functional/channels/registry.js +0 -2
- package/v2Components/FormBuilder/Functional/constants.js +8 -43
- package/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js +28 -175
- package/v2Components/FormBuilder/Functional/core/store/formReducer.js +6 -75
- package/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js +20 -10
- package/v2Components/FormBuilder/Functional/layout/FieldSlot.js +1 -9
- package/v2Components/FormBuilder/Functional/layout/SchemaForm.js +3 -20
- package/v2Components/FormBuilder/Functional/layout/Section.js +1 -6
- package/v2Components/FormBuilder/Functional/tests/fieldSlot.test.js +2 -9
- package/v2Components/FormBuilder/Functional/tests/schemaForm.test.js +0 -17
- package/v2Components/FormBuilder/index.js +12 -34
- package/v2Components/FormBuilder/tests/entryGate.test.js +7 -94
- package/v2Components/TestAndPreviewSlidebox/index.js +9 -0
- package/v2Containers/CommunicationFlow/CommunicationFlow.js +140 -75
- package/v2Containers/CommunicationFlow/CommunicationFlow.scss +18 -2
- package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +67 -28
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +503 -51
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlowCard.test.js +98 -4
- package/v2Containers/CommunicationFlow/Tests/constants.test.js +85 -0
- package/v2Containers/CommunicationFlow/constants.js +65 -4
- package/v2Containers/CommunicationFlow/index.js +16 -23
- package/v2Containers/CommunicationFlow/messages.js +11 -3
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +27 -11
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +4 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +176 -4
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +22 -4
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +14 -4
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +11 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +51 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +11 -4
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +19 -5
- package/v2Containers/CreativesContainer/SlideBoxContent.js +15 -0
- package/v2Containers/CreativesContainer/SlideBoxFooter.js +6 -2
- package/v2Containers/CreativesContainer/index.js +29 -3
- package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +29 -0
- package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +18 -5
- package/v2Containers/CreativesContainer/tests/index.test.js +12 -0
- package/v2Containers/MobilePush/Create/index.js +18 -1
- package/v2Containers/MobilePush/Create/test/contentValidity.test.js +96 -0
- package/v2Containers/MobilePush/Edit/index.js +18 -1
- package/v2Containers/MobilePush/Edit/test/contentValidity.test.js +116 -0
- package/v2Containers/MobilePush/commonMethods.js +49 -1
- package/v2Containers/MobilePushNew/index.js +29 -4
- package/v2Containers/MobilePushNew/tests/index.test.js +119 -0
- package/v2Containers/MobilePushNew/tests/utils.test.js +82 -0
- package/v2Containers/MobilePushNew/utils.js +34 -1
- package/v2Containers/MobilepushWrapper/index.js +3 -1
- package/v2Containers/Rcs/index.js +34 -0
- package/v2Containers/Rcs/index.scss +15 -0
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +115 -19
- package/v2Containers/Rcs/tests/index.test.js +67 -0
- package/v2Containers/Sms/Create/index.js +3 -1
- package/v2Containers/Sms/Edit/index.js +25 -0
- package/v2Containers/Sms/Edit/tests/index.test.js +85 -0
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +27 -3
- package/v2Containers/Viber/index.js +24 -1
- package/v2Containers/Viber/tests/index.test.js +103 -0
- package/v2Containers/WebPush/Create/index.js +19 -0
- package/v2Containers/WebPush/Create/preview/WebPushPreview.js +17 -12
- package/v2Containers/WebPush/Create/preview/tests/WebPushPreview.test.js +5 -0
- package/v2Containers/WebPush/Create/tests/contentValidity.test.js +294 -0
- package/v2Containers/Whatsapp/index.js +25 -4
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +306 -34
- package/v2Containers/Whatsapp/tests/index.test.js +115 -0
- package/v2Containers/Zalo/index.js +28 -1
- package/v2Containers/Zalo/tests/index.test.js +99 -0
- package/v2Containers/mockdata.js +25 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/buildSubmitPayload.js +0 -10
- package/v2Components/FormBuilder/Functional/channels/mobilepush/config.js +0 -82
- package/v2Components/FormBuilder/Functional/channels/mobilepush/getEditorErrorDescriptor.js +0 -74
- package/v2Components/FormBuilder/Functional/channels/mobilepush/index.js +0 -28
- package/v2Components/FormBuilder/Functional/channels/mobilepush/modals.js +0 -19
- package/v2Components/FormBuilder/Functional/channels/mobilepush/runSubmitPipeline.js +0 -45
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/getEditorErrorDescriptor.test.js +0 -162
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/modals.test.js +0 -37
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/runSubmitPipeline.test.js +0 -70
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/validate.test.js +0 -274
- package/v2Components/FormBuilder/Functional/channels/mobilepush/validate.js +0 -251
- package/v2Components/FormBuilder/Functional/layout/TabsContainer.js +0 -89
- package/v2Components/FormBuilder/Functional/renderers/mpushRenderers.js +0 -440
- package/v2Components/FormBuilder/Functional/tests/mpush.crossFlowParity.test.js +0 -430
- package/v2Components/FormBuilder/Functional/tests/mpush.ctaFlows.parity.test.js +0 -282
- package/v2Components/FormBuilder/Functional/tests/mpush.editContainer.parity.test.js +0 -141
- package/v2Components/FormBuilder/Functional/tests/mpush.engine.test.js +0 -306
- package/v2Components/FormBuilder/Functional/tests/mpush.shellEvents.test.js +0 -151
- package/v2Components/FormBuilder/Functional/tests/mpushRenderers.test.js +0 -342
- package/v2Components/FormBuilder/Functional/tests/tabsContainer.test.js +0 -110
- package/v2Components/FormBuilder/tests/mpush.characterization.test.js +0 -459
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* implementations are mocked with trivial stubs so this stays a fast unit test
|
|
6
6
|
* of the gate, independent of the 4,400-line monolith.
|
|
7
7
|
*
|
|
8
|
-
* Contract pinned here
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
8
|
+
* Contract pinned here:
|
|
9
|
+
* - flag OFF -> Classic, for every channel (the default until rollout)
|
|
10
|
+
* - flag ON + SMS channel (via `channel` prop OR `schema.channel`) -> Functional
|
|
11
|
+
* - flag ON + non-SMS channel -> Classic (other channels untouched)
|
|
12
12
|
* - flag ON + channel unknown (async schema not yet arrived) -> Classic
|
|
13
13
|
* - flag read throws (bootstrap/test race) -> Classic, no crash
|
|
14
14
|
*/
|
|
@@ -25,26 +25,20 @@ jest.mock('../Functional', () => (props) => (
|
|
|
25
25
|
<div data-testid="impl-functional">functional:{(props.schema && props.schema.channel) || props.channel || ''}</div>
|
|
26
26
|
));
|
|
27
27
|
|
|
28
|
-
// Mock only the flag
|
|
28
|
+
// Mock only the one flag reader the gate uses; keep the rest of common.js intact.
|
|
29
29
|
jest.mock('../../../utils/common', () => ({
|
|
30
30
|
...jest.requireActual('../../../utils/common'),
|
|
31
31
|
hasNewFormBuilderEnabledForSms: jest.fn(),
|
|
32
|
-
hasNewFormBuilderEnabledForMpush: jest.fn(),
|
|
33
32
|
}));
|
|
34
33
|
|
|
35
34
|
// eslint-disable-next-line import/first
|
|
36
35
|
import FormBuilder from '../index';
|
|
37
36
|
// eslint-disable-next-line import/first
|
|
38
|
-
import { hasNewFormBuilderEnabledForSms
|
|
37
|
+
import { hasNewFormBuilderEnabledForSms } from '../../../utils/common';
|
|
39
38
|
|
|
40
39
|
const renderGate = (props = {}) => render(<FormBuilder {...props} />);
|
|
41
40
|
|
|
42
41
|
describe('FormBuilder entry gate — routing', () => {
|
|
43
|
-
beforeEach(() => {
|
|
44
|
-
hasNewFormBuilderEnabledForSms.mockReturnValue(false);
|
|
45
|
-
hasNewFormBuilderEnabledForMpush.mockReturnValue(false);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
42
|
describe('flag OFF (default)', () => {
|
|
49
43
|
beforeEach(() => hasNewFormBuilderEnabledForSms.mockReturnValue(false));
|
|
50
44
|
|
|
@@ -99,80 +93,8 @@ describe('FormBuilder entry gate — routing', () => {
|
|
|
99
93
|
});
|
|
100
94
|
});
|
|
101
95
|
|
|
102
|
-
describe('MPUSH flag (ENABLE_NEW_FORMBUILDER_MPUSH — Phase 2)', () => {
|
|
103
|
-
it('flag OFF: MOBILEPUSH renders Classic', () => {
|
|
104
|
-
renderGate({ schema: { channel: 'MOBILEPUSH' } });
|
|
105
|
-
expect(screen.getByTestId('impl-classic')).toBeInTheDocument();
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('flag ON: routes MOBILEPUSH to Functional (schema.channel)', () => {
|
|
109
|
-
hasNewFormBuilderEnabledForMpush.mockReturnValue(true);
|
|
110
|
-
renderGate({ schema: { channel: 'MOBILEPUSH' } });
|
|
111
|
-
expect(screen.getByTestId('impl-functional')).toBeInTheDocument();
|
|
112
|
-
expect(screen.queryByTestId('impl-classic')).toBeNull();
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('flag ON: is case-insensitive and honors the channel prop', () => {
|
|
116
|
-
hasNewFormBuilderEnabledForMpush.mockReturnValue(true);
|
|
117
|
-
renderGate({ channel: 'mobilepush', schema: {} });
|
|
118
|
-
expect(screen.getByTestId('impl-functional')).toBeInTheDocument();
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('flags are independent: MPUSH flag ON does not move SMS (and vice versa)', () => {
|
|
122
|
-
hasNewFormBuilderEnabledForMpush.mockReturnValue(true);
|
|
123
|
-
renderGate({ schema: { channel: 'SMS' } });
|
|
124
|
-
expect(screen.getByTestId('impl-classic')).toBeInTheDocument();
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('SMS flag ON does not move MOBILEPUSH', () => {
|
|
128
|
-
hasNewFormBuilderEnabledForSms.mockReturnValue(true);
|
|
129
|
-
renderGate({ schema: { channel: 'MOBILEPUSH' } });
|
|
130
|
-
expect(screen.getByTestId('impl-classic')).toBeInTheDocument();
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('both flags ON route each channel to Functional', () => {
|
|
134
|
-
hasNewFormBuilderEnabledForSms.mockReturnValue(true);
|
|
135
|
-
hasNewFormBuilderEnabledForMpush.mockReturnValue(true);
|
|
136
|
-
renderGate({ schema: { channel: 'MOBILEPUSH' } });
|
|
137
|
-
expect(screen.getByTestId('impl-functional')).toHaveTextContent('functional:MOBILEPUSH');
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('unmigrated channels stay on Classic even with both flags ON', () => {
|
|
141
|
-
hasNewFormBuilderEnabledForSms.mockReturnValue(true);
|
|
142
|
-
hasNewFormBuilderEnabledForMpush.mockReturnValue(true);
|
|
143
|
-
renderGate({ schema: { channel: 'EMAIL' } });
|
|
144
|
-
expect(screen.getByTestId('impl-classic')).toBeInTheDocument();
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
describe('localStorage session override (QA enabler / kill switch)', () => {
|
|
149
|
-
afterEach(() => {
|
|
150
|
-
window.localStorage.removeItem('ENABLE_NEW_FORMBUILDER_MPUSH');
|
|
151
|
-
window.localStorage.removeItem('ENABLE_NEW_FORMBUILDER_SMS');
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it("'true' override routes the channel to Functional even with the org flag OFF", () => {
|
|
155
|
-
window.localStorage.setItem('ENABLE_NEW_FORMBUILDER_MPUSH', 'true');
|
|
156
|
-
renderGate({ schema: { channel: 'MOBILEPUSH' } });
|
|
157
|
-
expect(screen.getByTestId('impl-functional')).toBeInTheDocument();
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("'false' override (kill switch) forces Classic even with the org flag ON", () => {
|
|
161
|
-
hasNewFormBuilderEnabledForMpush.mockReturnValue(true);
|
|
162
|
-
window.localStorage.setItem('ENABLE_NEW_FORMBUILDER_MPUSH', 'false');
|
|
163
|
-
renderGate({ schema: { channel: 'MOBILEPUSH' } });
|
|
164
|
-
expect(screen.getByTestId('impl-classic')).toBeInTheDocument();
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('an override on one channel does not affect the other', () => {
|
|
168
|
-
window.localStorage.setItem('ENABLE_NEW_FORMBUILDER_MPUSH', 'true');
|
|
169
|
-
renderGate({ schema: { channel: 'SMS' } });
|
|
170
|
-
expect(screen.getByTestId('impl-classic')).toBeInTheDocument();
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
|
|
174
96
|
describe('resilience', () => {
|
|
175
|
-
it('falls back to Classic if
|
|
97
|
+
it('falls back to Classic if the flag read throws (window.capAuth not initialized)', () => {
|
|
176
98
|
hasNewFormBuilderEnabledForSms.mockImplementation(() => {
|
|
177
99
|
throw new Error('capAuth not initialized');
|
|
178
100
|
});
|
|
@@ -180,14 +102,5 @@ describe('FormBuilder entry gate — routing', () => {
|
|
|
180
102
|
expect(screen.getByTestId('impl-classic')).toBeInTheDocument();
|
|
181
103
|
expect(screen.queryByTestId('impl-functional')).toBeNull();
|
|
182
104
|
});
|
|
183
|
-
|
|
184
|
-
it('one flag reader throwing routes EVERY channel to Classic (whole map falls back)', () => {
|
|
185
|
-
hasNewFormBuilderEnabledForSms.mockReturnValue(true);
|
|
186
|
-
hasNewFormBuilderEnabledForMpush.mockImplementation(() => {
|
|
187
|
-
throw new Error('capAuth not initialized');
|
|
188
|
-
});
|
|
189
|
-
renderGate({ schema: { channel: 'SMS' } });
|
|
190
|
-
expect(screen.getByTestId('impl-classic')).toBeInTheDocument();
|
|
191
|
-
});
|
|
192
105
|
});
|
|
193
106
|
});
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
makeSelectMessageMetaConfigId,
|
|
37
37
|
makeSelectPrefilledValues,
|
|
38
38
|
makeSelectIsSendingTestMessage,
|
|
39
|
+
makeSelectIsSendingCcsTestMessage,
|
|
39
40
|
makeSelectUpdatePreviewError,
|
|
40
41
|
makeSelectUpdatePreviewErrors,
|
|
41
42
|
makeSelectFetchPrefilledValuesError,
|
|
@@ -103,11 +104,17 @@ TestAndPreviewSlidebox.propTypes = {
|
|
|
103
104
|
messageMetaConfigId: PropTypes.string,
|
|
104
105
|
prefilledValues: PropTypes.object,
|
|
105
106
|
isSendingTestMessage: PropTypes.bool.isRequired,
|
|
107
|
+
isSendingCcsTestMessage: PropTypes.bool.isRequired,
|
|
106
108
|
intl: PropTypes.object.isRequired,
|
|
107
109
|
senderDetailsByChannel: PropTypes.object,
|
|
108
110
|
wecrmAccounts: PropTypes.array,
|
|
109
111
|
isLoadingSenderDetails: PropTypes.bool,
|
|
110
112
|
orgUnitId: PropTypes.number,
|
|
113
|
+
/** notify/ui only — see CommonTestAndPreview's handleSendTestMessage. */
|
|
114
|
+
ccsSendTransaction: PropTypes.shape({
|
|
115
|
+
commDefinitionId: PropTypes.string,
|
|
116
|
+
referenceId: PropTypes.string,
|
|
117
|
+
}),
|
|
111
118
|
};
|
|
112
119
|
|
|
113
120
|
TestAndPreviewSlidebox.defaultProps = {
|
|
@@ -125,6 +132,7 @@ TestAndPreviewSlidebox.defaultProps = {
|
|
|
125
132
|
isLoadingSenderDetails: false,
|
|
126
133
|
orgUnitId: -1,
|
|
127
134
|
smsFallbackContent: null,
|
|
135
|
+
ccsSendTransaction: null,
|
|
128
136
|
};
|
|
129
137
|
|
|
130
138
|
const mapStateToProps = createStructuredSelector({
|
|
@@ -145,6 +153,7 @@ const mapStateToProps = createStructuredSelector({
|
|
|
145
153
|
fetchPrefilledValuesError: makeSelectFetchPrefilledValuesError(),
|
|
146
154
|
fetchPrefilledValuesErrors: makeSelectFetchPrefilledValuesErrors(),
|
|
147
155
|
isSendingTestMessage: makeSelectIsSendingTestMessage(),
|
|
156
|
+
isSendingCcsTestMessage: makeSelectIsSendingCcsTestMessage(),
|
|
148
157
|
senderDetailsByChannel: makeSelectSenderDetailsByChannel(),
|
|
149
158
|
wecrmAccounts: makeSelectWeCrmAccounts(),
|
|
150
159
|
isLoadingSenderDetails: makeSelectIsLoadingSenderDetails(),
|
|
@@ -13,14 +13,14 @@ import { injectIntl } from 'react-intl';
|
|
|
13
13
|
import { compose } from 'redux';
|
|
14
14
|
import { connect } from 'react-redux';
|
|
15
15
|
import { createStructuredSelector } from 'reselect';
|
|
16
|
-
// import { CouponsCapContainer } from '@capillarytech/cap-coupons'; // Commented - cap-coupons flows disabled
|
|
17
16
|
import CapRow from '@capillarytech/cap-ui-library/CapRow';
|
|
18
17
|
import CapDivider from '@capillarytech/cap-ui-library/CapDivider';
|
|
19
18
|
import CapButton from '@capillarytech/cap-ui-library/CapButton';
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
import CapLabel from '@capillarytech/cap-ui-library/CapLabel';
|
|
20
|
+
import injectReducer from '../../utils/injectReducer';
|
|
22
21
|
import { makeSelectAuthenticated } from '../Cap/selectors';
|
|
23
|
-
import
|
|
22
|
+
import capReducer from '../Cap/reducer';
|
|
23
|
+
import { createCommDefinition, editCommDefinition } from '../../services/api';
|
|
24
24
|
import DynamicControlsStep from './steps/DynamicControlsStep';
|
|
25
25
|
import MessageTypeStep from './steps/MessageTypeStep';
|
|
26
26
|
import CommunicationStrategyStep from './steps/CommunicationStrategyStep';
|
|
@@ -35,23 +35,20 @@ import {
|
|
|
35
35
|
CHANNELS,
|
|
36
36
|
INCENTIVE_TYPES,
|
|
37
37
|
DYNAMIC_CONTROLS_CONFIG,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
CCS_STRATEGY_TYPE_SINGLE,
|
|
39
|
+
CCS_CHANNEL_CONTENT_KEY_MAP,
|
|
40
|
+
CCS_CHANNEL_DELIVERY_KEY_MAP,
|
|
41
|
+
CCS_CHANNEL_NAME_MAP,
|
|
42
|
+
CCS_CONTENT_TRANSFORMS,
|
|
43
|
+
REFERENCE_ID_EXISTS,
|
|
41
44
|
} from './constants';
|
|
42
45
|
import { getEnabledSteps } from './utils/getEnabledSteps';
|
|
43
46
|
import messages from './messages';
|
|
44
47
|
import './CommunicationFlow.scss';
|
|
45
48
|
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
// reducer: CouponsCapContainer.couponsCapReducer,
|
|
50
|
-
// });
|
|
51
|
-
// const withCouponsSaga = injectSaga({
|
|
52
|
-
// key: 'couponsCapSaga',
|
|
53
|
-
// saga: CouponsCapContainer.couponsCapSaga,
|
|
54
|
-
// });
|
|
49
|
+
// Self-injects the same 'cap' reducer CreativesContainer already injects under this key;
|
|
50
|
+
// idempotent, so it's a no-op if the consumer's own store already provides it.
|
|
51
|
+
const withCapReducer = injectReducer({ key: 'cap', reducer: capReducer });
|
|
55
52
|
|
|
56
53
|
const getDeliveryChannels = (contentItems) => {
|
|
57
54
|
const channels = contentItems.map((item) => (item.channel || '').toUpperCase()).filter(Boolean);
|
|
@@ -64,6 +61,9 @@ const hasDeliverySettingForChannel = (channelSetting, channel) => {
|
|
|
64
61
|
return !!settings && Object.values(settings).some((v) => v !== null && v !== '' && v !== undefined);
|
|
65
62
|
};
|
|
66
63
|
|
|
64
|
+
// Duplicate referenceId within the org returns a 409 CCS error envelope.
|
|
65
|
+
const isDuplicateReferenceIdError = (res) => res?.success === false && res?.status?.message === REFERENCE_ID_EXISTS;
|
|
66
|
+
|
|
67
67
|
const CommunicationFlow = ({
|
|
68
68
|
config,
|
|
69
69
|
initialData,
|
|
@@ -91,6 +91,7 @@ const CommunicationFlow = ({
|
|
|
91
91
|
};
|
|
92
92
|
});
|
|
93
93
|
const [validationErrors, setValidationErrors] = useState({});
|
|
94
|
+
const [saveError, setSaveError] = useState(null);
|
|
94
95
|
|
|
95
96
|
// Memoize enabled steps
|
|
96
97
|
const enabledSteps = useMemo(() => getEnabledSteps(config), [config]);
|
|
@@ -150,74 +151,123 @@ const CommunicationFlow = ({
|
|
|
150
151
|
}, []);
|
|
151
152
|
|
|
152
153
|
const handleSave = useCallback(async () => {
|
|
154
|
+
setSaveError(null);
|
|
153
155
|
const aggregatedData = getAggregatedData();
|
|
154
156
|
const shouldUseCCS = config?.useCCS !== false;
|
|
157
|
+
let ccsCommDefinition = null;
|
|
155
158
|
|
|
156
159
|
if (shouldUseCCS) {
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
+
const isMultiChannel = [CHANNEL_PRIORITY, AB_TEST].includes(aggregatedData.communicationStrategy);
|
|
161
|
+
const contentItem = (aggregatedData.contentItems || [])[0];
|
|
162
|
+
// Consumer-supplied (e.g. CapNotify's Alert Name field) — CommunicationFlow has no name input of its own.
|
|
163
|
+
const name = config?.context?.name;
|
|
160
164
|
|
|
161
|
-
|
|
162
|
-
|
|
165
|
+
// CCS create only supports SINGLE strategy; CHANNEL_PRIORITY/AB_TEST have no CCS equivalent yet.
|
|
166
|
+
if (!isMultiChannel && contentItem && name) {
|
|
167
|
+
const rawChannel = (contentItem.channel || '').toUpperCase();
|
|
168
|
+
const channel = CCS_CHANNEL_NAME_MAP[rawChannel] || rawChannel;
|
|
169
|
+
const contentKey = CCS_CHANNEL_CONTENT_KEY_MAP[channel];
|
|
170
|
+
const deliveryKey = CCS_CHANNEL_DELIVERY_KEY_MAP[channel];
|
|
171
|
+
const contentTransform = CCS_CONTENT_TRANSFORMS[channel];
|
|
172
|
+
const contentPayload = contentTransform ? contentTransform(contentItem.templateData) : contentItem.templateData;
|
|
173
|
+
const { dynamicControls = {} } = aggregatedData;
|
|
174
|
+
const channelSettings = aggregatedData.deliverySetting?.channelSetting?.[channel] || {};
|
|
175
|
+
// Optional fields — sent as '' rather than omitted when left blank.
|
|
176
|
+
const referenceId = config?.context?.referenceId || '';
|
|
177
|
+
const description = config?.context?.description || '';
|
|
163
178
|
|
|
164
|
-
|
|
165
|
-
|
|
179
|
+
const payload = {
|
|
180
|
+
referenceId,
|
|
181
|
+
name,
|
|
182
|
+
description,
|
|
183
|
+
strategyType: CCS_STRATEGY_TYPE_SINGLE,
|
|
184
|
+
settings: {
|
|
185
|
+
additionalSettings: {
|
|
186
|
+
useTinyUrl: dynamicControls.useTinyUrl ?? false,
|
|
187
|
+
encryptUrl: dynamicControls.sendToControlCustomers ?? false,
|
|
188
|
+
linkTrackingEnabled: dynamicControls.overrideDailyLimit ?? false,
|
|
189
|
+
userSubscriptionDisabled: dynamicControls.sendToBrandPocs ?? false,
|
|
190
|
+
},
|
|
191
|
+
executionParams: {},
|
|
192
|
+
},
|
|
193
|
+
singleChannelStrategy: {
|
|
194
|
+
variant: {
|
|
195
|
+
channel,
|
|
196
|
+
// `channel` spread last so CCS's normalized enum overrides the UI's internal identifier.
|
|
197
|
+
...(contentKey && { [contentKey]: { ...contentPayload, channel } }),
|
|
198
|
+
...(deliveryKey && { [deliveryKey]: { channelSettings: { ...channelSettings, channel } } }),
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
};
|
|
166
202
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
203
|
+
// existingCommDefinitionId (set when editing, e.g. a rejected alert) creates a new
|
|
204
|
+
// DRAFT version on the same CommDefinition instead of a new one.
|
|
205
|
+
const { existingCommDefinitionId } = config?.context || {};
|
|
206
|
+
const {
|
|
207
|
+
referenceId: payloadReferenceId,
|
|
208
|
+
description: payloadDescription,
|
|
209
|
+
strategyType,
|
|
210
|
+
settings,
|
|
211
|
+
singleChannelStrategy,
|
|
212
|
+
} = payload;
|
|
173
213
|
|
|
174
|
-
if (contentItems.length > 0) {
|
|
175
214
|
try {
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
215
|
+
const res = existingCommDefinitionId
|
|
216
|
+
? await editCommDefinition(existingCommDefinitionId, {
|
|
217
|
+
referenceId: payloadReferenceId,
|
|
218
|
+
description: payloadDescription,
|
|
219
|
+
strategyType,
|
|
220
|
+
settings,
|
|
221
|
+
singleChannelStrategy,
|
|
222
|
+
})
|
|
223
|
+
: await createCommDefinition(payload);
|
|
224
|
+
if (res?.isError) {
|
|
225
|
+
setSaveError(formatMessage(messages.genericSaveError));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (isDuplicateReferenceIdError(res)) {
|
|
229
|
+
setSaveError(formatMessage(messages.duplicateReferenceIdError));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const data = res?.response?.data;
|
|
233
|
+
if (existingCommDefinitionId) {
|
|
234
|
+
if (data) {
|
|
235
|
+
ccsCommDefinition = {
|
|
236
|
+
id: existingCommDefinitionId,
|
|
237
|
+
referenceId: referenceId || data.referenceId,
|
|
238
|
+
version: data.version?.version ?? data.version ?? 1,
|
|
239
|
+
status: data.status,
|
|
201
240
|
};
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
241
|
+
} else {
|
|
242
|
+
setSaveError(formatMessage(messages.genericSaveError));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
} else if (data?.id) {
|
|
246
|
+
ccsCommDefinition = {
|
|
247
|
+
id: data.id,
|
|
248
|
+
referenceId: data.referenceId,
|
|
249
|
+
version: data.version?.version ?? 1,
|
|
250
|
+
status: data.status,
|
|
251
|
+
};
|
|
252
|
+
} else {
|
|
253
|
+
setSaveError(formatMessage(messages.genericSaveError));
|
|
254
|
+
return;
|
|
213
255
|
}
|
|
214
256
|
} catch (error) {
|
|
215
|
-
console.error('[CommunicationFlow] CCS
|
|
257
|
+
console.error('[CommunicationFlow] CCS createCommDefinition error:', error);
|
|
258
|
+
setSaveError(formatMessage(messages.genericSaveError));
|
|
259
|
+
return;
|
|
216
260
|
}
|
|
261
|
+
} else if (!name) {
|
|
262
|
+
console.warn('[CommunicationFlow] Skipping CCS createCommDefinition — config.context.name is required');
|
|
263
|
+
} else if (isMultiChannel) {
|
|
264
|
+
console.warn('[CommunicationFlow] Skipping CCS createCommDefinition — CHANNEL_PRIORITY/AB_TEST strategies are not supported by CCS create yet (SINGLE only)');
|
|
265
|
+
} else if (!contentItem) {
|
|
266
|
+
console.warn('[CommunicationFlow] Skipping CCS createCommDefinition — no content item found (contentItems is empty)');
|
|
217
267
|
}
|
|
218
268
|
}
|
|
219
269
|
|
|
220
|
-
onSave(aggregatedData);
|
|
270
|
+
onSave(ccsCommDefinition ? { ...aggregatedData, ccsCommDefinition } : aggregatedData);
|
|
221
271
|
}, [getAggregatedData, config, onSave]);
|
|
222
272
|
|
|
223
273
|
// Call onChange callback when stepData changes
|
|
@@ -252,9 +302,17 @@ const CommunicationFlow = ({
|
|
|
252
302
|
<CapDivider />
|
|
253
303
|
</CapRow>
|
|
254
304
|
);
|
|
255
|
-
case STEPS.COMMUNICATION_STRATEGY:
|
|
305
|
+
case STEPS.COMMUNICATION_STRATEGY: {
|
|
306
|
+
// No divider when there's no content yet, but the spacing above the Channel
|
|
307
|
+
// card must stay either way — see .communication-strategy-row in the scss.
|
|
308
|
+
const templateStepFollows = enabledSteps.includes(STEPS.CHANNEL_SELECTION);
|
|
309
|
+
const hasContent = stepData.contentItems?.length > 0;
|
|
256
310
|
return (
|
|
257
|
-
<CapRow
|
|
311
|
+
<CapRow
|
|
312
|
+
useLegacy
|
|
313
|
+
key={step}
|
|
314
|
+
className={(!templateStepFollows || hasContent) ? '' : 'communication-strategy-row--no-divider'}
|
|
315
|
+
>
|
|
258
316
|
<CommunicationStrategyStep
|
|
259
317
|
{...commonProps}
|
|
260
318
|
value={stepData.communicationStrategy}
|
|
@@ -263,9 +321,10 @@ const CommunicationFlow = ({
|
|
|
263
321
|
disabled={communicationStrategyData.disabled}
|
|
264
322
|
onChange={(communicationStrategy) => handleStepChange(step, { communicationStrategy })}
|
|
265
323
|
/>
|
|
266
|
-
<CapDivider />
|
|
324
|
+
{(!templateStepFollows || hasContent) && <CapDivider />}
|
|
267
325
|
</CapRow>
|
|
268
326
|
);
|
|
327
|
+
}
|
|
269
328
|
case STEPS.CHANNEL_SELECTION:
|
|
270
329
|
// Only show ChannelSelectionStep if communication strategy is selected
|
|
271
330
|
if (!stepData.communicationStrategy) {
|
|
@@ -280,6 +339,7 @@ const CommunicationFlow = ({
|
|
|
280
339
|
onChange={(data) => handleStepChange(step, data)}
|
|
281
340
|
channelsToHide={contentTemplateData.channelsToHide}
|
|
282
341
|
channelsToDisable={contentTemplateData.channelsToDisable}
|
|
342
|
+
disablePreviewAndTest={!!contentTemplateData.disablePreviewAndTest}
|
|
283
343
|
creativesMode={config.mode || 'create'}
|
|
284
344
|
selectedOfferDetails={stepData.selectedOfferDetails}
|
|
285
345
|
incentivesData={{
|
|
@@ -290,7 +350,7 @@ const CommunicationFlow = ({
|
|
|
290
350
|
config={config}
|
|
291
351
|
capData={cap || capData}
|
|
292
352
|
/>
|
|
293
|
-
<CapDivider />
|
|
353
|
+
{stepData.contentItems?.length > 0 && <CapDivider />}
|
|
294
354
|
</CapRow>
|
|
295
355
|
);
|
|
296
356
|
case STEPS.DYNAMIC_CONTROLS:
|
|
@@ -317,6 +377,11 @@ const CommunicationFlow = ({
|
|
|
317
377
|
{renderSteps()}
|
|
318
378
|
{onSave && (
|
|
319
379
|
<CapRow useLegacy className="communication-flow-container__footer">
|
|
380
|
+
{saveError && (
|
|
381
|
+
<CapLabel type="label2" className="communication-flow-container__save-error">
|
|
382
|
+
{saveError}
|
|
383
|
+
</CapLabel>
|
|
384
|
+
)}
|
|
320
385
|
<CapButton type="primary" onClick={handleSave} disabled={isSaveDisabled}>
|
|
321
386
|
{formatMessage(messages.save)}
|
|
322
387
|
</CapButton>
|
|
@@ -328,7 +393,7 @@ const CommunicationFlow = ({
|
|
|
328
393
|
|
|
329
394
|
CommunicationFlow.propTypes = {
|
|
330
395
|
config: PropTypes.shape({
|
|
331
|
-
consumer: PropTypes.oneOf(['campaigns', 'loyalty', 'adiona']).isRequired,
|
|
396
|
+
consumer: PropTypes.oneOf(['campaigns', 'loyalty', 'adiona', 'coupons', 'cartPromotions']).isRequired,
|
|
332
397
|
channel: PropTypes.string,
|
|
333
398
|
mode: PropTypes.oneOf(['create', 'edit', 'preview']).isRequired,
|
|
334
399
|
channelsToHide: PropTypes.arrayOf(PropTypes.string),
|
|
@@ -362,6 +427,7 @@ CommunicationFlow.propTypes = {
|
|
|
362
427
|
contentTemplateData: PropTypes.shape({
|
|
363
428
|
required: PropTypes.bool,
|
|
364
429
|
channels: PropTypes.object,
|
|
430
|
+
disablePreviewAndTest: PropTypes.bool,
|
|
365
431
|
}),
|
|
366
432
|
enableIncentives: PropTypes.bool,
|
|
367
433
|
enableDeliverySettings: PropTypes.bool,
|
|
@@ -394,8 +460,7 @@ const mapStateToProps = createStructuredSelector({
|
|
|
394
460
|
const withConnect = connect(mapStateToProps);
|
|
395
461
|
|
|
396
462
|
export default compose(
|
|
397
|
-
|
|
398
|
-
// withCouponsSaga, // cap-coupons flows disabled
|
|
463
|
+
withCapReducer,
|
|
399
464
|
withConnect,
|
|
400
465
|
injectIntl,
|
|
401
466
|
)(CommunicationFlow);
|
|
@@ -128,16 +128,32 @@
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
.communication-flow-container {
|
|
131
|
+
// Anchor the Save footer to the bottom without flex, avoiding .ant-row-legacy overrides and flexbox stretching nested fixed-size content.
|
|
132
|
+
&.ant-row-legacy {
|
|
133
|
+
position: relative !important;
|
|
134
|
+
min-height: calc(100vh - 6.571rem) !important;
|
|
135
|
+
padding-bottom: 6rem !important;
|
|
136
|
+
}
|
|
137
|
+
|
|
131
138
|
.step-divider {
|
|
132
139
|
margin: $CAP_SPACE_32 0;
|
|
133
140
|
}
|
|
134
141
|
|
|
142
|
+
// Hide the divider when empty but preserve its margin gap so the section doesn’t sit flush against the Channel card.
|
|
143
|
+
.communication-strategy-row--no-divider {
|
|
144
|
+
margin-bottom: $CAP_SPACE_32;
|
|
145
|
+
}
|
|
146
|
+
|
|
135
147
|
.heading-style {
|
|
136
148
|
margin-bottom: $CAP_SPACE_08;
|
|
137
149
|
}
|
|
138
150
|
|
|
139
|
-
&__footer {
|
|
140
|
-
|
|
151
|
+
&__footer.ant-row-legacy {
|
|
152
|
+
position: absolute !important;
|
|
153
|
+
left: 0;
|
|
154
|
+
right: 0;
|
|
155
|
+
bottom: 0 !important;
|
|
156
|
+
padding: 3.142rem 0 $CAP_SPACE_08;
|
|
141
157
|
}
|
|
142
158
|
}
|
|
143
159
|
|