@capillarytech/creatives-library 9.0.59-alpha.2 → 9.0.59-alpha.4
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/package.json +1 -1
- package/services/api.js +0 -15
- package/services/tests/api.test.js +0 -55
- package/v2Containers/CommunicationFlow/CommunicationFlow.js +3 -43
- package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +1 -47
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlowCard.test.js +1 -61
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +1 -13
- package/v2Containers/TagList/index.js +3 -3
- package/v2Containers/TagList/tests/TagList.test.js +16 -0
- package/v2Containers/Viber/constants.js +6 -0
- package/v2Containers/Viber/index.js +22 -16
- package/v2Containers/CommunicationFlow/utils/Tests/mapCommDefinitionToStepData.test.js +0 -268
- package/v2Containers/CommunicationFlow/utils/mapCommDefinitionToStepData.js +0 -155
package/package.json
CHANGED
package/services/api.js
CHANGED
|
@@ -722,21 +722,6 @@ export const sendTransactionalComm = (payload) => {
|
|
|
722
722
|
return request(url, getAPICallObject(HTTP_METHODS.POST, payload, false, false, false, true));
|
|
723
723
|
};
|
|
724
724
|
|
|
725
|
-
// CommunicationFlow's own fetch-by-id path (see mapCommDefinitionToStepData in
|
|
726
|
-
// CommunicationFlow/utils) — lets a consumer that only stored a commDefinitionId (no local
|
|
727
|
-
// content copy) hand CommunicationFlow just that id on edit; it fetches metadata + content itself.
|
|
728
|
-
export const getCommDefinition = (commDefinitionId, include) => {
|
|
729
|
-
const query = include ? `?include=${encodeURIComponent(include)}` : '';
|
|
730
|
-
const url = `${COMM_DEFINITIONS_PATH}/${commDefinitionId}${query}`;
|
|
731
|
-
return request(url, getAPICallObject(HTTP_METHODS.GET, false, false, false, true));
|
|
732
|
-
};
|
|
733
|
-
|
|
734
|
-
// CommDefinition GET returns metadata only; fetch the version separately for message content.
|
|
735
|
-
export const getCommDefinitionVersion = (commDefinitionId, version) => {
|
|
736
|
-
const url = `${COMM_DEFINITIONS_PATH}/${commDefinitionId}/version/${version}`;
|
|
737
|
-
return request(url, getAPICallObject(HTTP_METHODS.GET, false, false, false, true));
|
|
738
|
-
};
|
|
739
|
-
|
|
740
725
|
export const getCentralCommsMetaIds = (metaIds, metaType = TRANSACTION) => {
|
|
741
726
|
const url = `${API_ENDPOINT}/common/central-comms/meta-id/${metaType}?metaIds=${metaIds}`;
|
|
742
727
|
return request(url, getAPICallObject(HTTP_METHODS.GET, null, false, false, false, true));
|
|
@@ -31,8 +31,6 @@ import {
|
|
|
31
31
|
getCmsAccounts,
|
|
32
32
|
getMembersLookup,
|
|
33
33
|
createTestCustomer,
|
|
34
|
-
getCommDefinition,
|
|
35
|
-
getCommDefinitionVersion,
|
|
36
34
|
} from '../api';
|
|
37
35
|
import { mockData } from './mockData';
|
|
38
36
|
import getSchema from '../getSchema';
|
|
@@ -1245,56 +1243,3 @@ describe('bulkClaimAndApprove', () => {
|
|
|
1245
1243
|
expect(result).toEqual({ error: 'Network error' });
|
|
1246
1244
|
});
|
|
1247
1245
|
});
|
|
1248
|
-
|
|
1249
|
-
describe('getCommDefinition', () => {
|
|
1250
|
-
beforeEach(() => {
|
|
1251
|
-
global.fetch = jest.fn();
|
|
1252
|
-
});
|
|
1253
|
-
|
|
1254
|
-
afterEach(() => {
|
|
1255
|
-
jest.restoreAllMocks();
|
|
1256
|
-
});
|
|
1257
|
-
|
|
1258
|
-
it('builds the URL without an include query param when include is not supplied', async () => {
|
|
1259
|
-
global.fetch.mockReturnValue(Promise.resolve({
|
|
1260
|
-
status: 200,
|
|
1261
|
-
json: () => Promise.resolve({ status: 200, response: { data: { id: 'cd_1' } } }),
|
|
1262
|
-
}));
|
|
1263
|
-
await getCommDefinition('cd_1');
|
|
1264
|
-
expect(global.fetch).toHaveBeenCalled();
|
|
1265
|
-
const lastCall = global.fetch.mock.calls[global.fetch.mock.calls.length - 1];
|
|
1266
|
-
expect(lastCall[0]).toContain('/comm-definitions/cd_1?');
|
|
1267
|
-
expect(lastCall[0]).not.toContain('include=');
|
|
1268
|
-
});
|
|
1269
|
-
|
|
1270
|
-
it('appends an include query param when supplied', async () => {
|
|
1271
|
-
global.fetch.mockReturnValue(Promise.resolve({
|
|
1272
|
-
status: 200,
|
|
1273
|
-
json: () => Promise.resolve({ status: 200, response: { data: { id: 'cd_1' } } }),
|
|
1274
|
-
}));
|
|
1275
|
-
await getCommDefinition('cd_1', 'versions');
|
|
1276
|
-
const lastCall = global.fetch.mock.calls[global.fetch.mock.calls.length - 1];
|
|
1277
|
-
expect(lastCall[0]).toContain('/comm-definitions/cd_1?include=versions');
|
|
1278
|
-
});
|
|
1279
|
-
});
|
|
1280
|
-
|
|
1281
|
-
describe('getCommDefinitionVersion', () => {
|
|
1282
|
-
beforeEach(() => {
|
|
1283
|
-
global.fetch = jest.fn();
|
|
1284
|
-
});
|
|
1285
|
-
|
|
1286
|
-
afterEach(() => {
|
|
1287
|
-
jest.restoreAllMocks();
|
|
1288
|
-
});
|
|
1289
|
-
|
|
1290
|
-
it('builds the URL with the commDefinitionId and version segments', async () => {
|
|
1291
|
-
global.fetch.mockReturnValue(Promise.resolve({
|
|
1292
|
-
status: 200,
|
|
1293
|
-
json: () => Promise.resolve({ status: 200, response: { data: {} } }),
|
|
1294
|
-
}));
|
|
1295
|
-
await getCommDefinitionVersion('cd_1', 2);
|
|
1296
|
-
const lastCall = global.fetch.mock.calls[global.fetch.mock.calls.length - 1];
|
|
1297
|
-
expect(lastCall[0]).toContain('/comm-definitions/cd_1/version/2');
|
|
1298
|
-
expect(lastCall[1].method).toBe('GET');
|
|
1299
|
-
});
|
|
1300
|
-
});
|
|
@@ -20,7 +20,7 @@ import CapNotification from '@capillarytech/cap-ui-library/CapNotification';
|
|
|
20
20
|
import injectReducer from '../../utils/injectReducer';
|
|
21
21
|
import { makeSelectAuthenticated } from '../Cap/selectors';
|
|
22
22
|
import capReducer from '../Cap/reducer';
|
|
23
|
-
import { createCommDefinition, editCommDefinition
|
|
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';
|
|
@@ -41,10 +41,8 @@ import {
|
|
|
41
41
|
CCS_CHANNEL_NAME_MAP,
|
|
42
42
|
CCS_CONTENT_TRANSFORMS,
|
|
43
43
|
REFERENCE_ID_EXISTS,
|
|
44
|
-
SINGLE_TEMPLATE,
|
|
45
44
|
} from './constants';
|
|
46
45
|
import { getEnabledSteps } from './utils/getEnabledSteps';
|
|
47
|
-
import { fetchAndMapCommDefinition } from './utils/mapCommDefinitionToStepData';
|
|
48
46
|
import messages from './messages';
|
|
49
47
|
import './CommunicationFlow.scss';
|
|
50
48
|
|
|
@@ -78,26 +76,12 @@ const CommunicationFlow = ({
|
|
|
78
76
|
}) => {
|
|
79
77
|
const { formatMessage } = intl || {};
|
|
80
78
|
const { messageTypeData = {}, communicationStrategyData = {}, contentTemplateData = {} } = config?.features || {};
|
|
81
|
-
|
|
82
|
-
// A consumer other than CapNotify (which always pre-fetches and passes full initialData
|
|
83
|
-
// itself) may hand CommunicationFlow only a commDefinitionId — no local content copy at all
|
|
84
|
-
// — and rely on CommunicationFlow to fetch + populate its own content (see the fetch effect
|
|
85
|
-
// below, and mapCommDefinitionToStepData). Captured once at mount: initialData/config aren't
|
|
86
|
-
// expected to change identity across the component's lifetime for this decision.
|
|
87
|
-
const [shouldFetchCommDefinition] = useState(
|
|
88
|
-
() => !!config?.context?.existingCommDefinitionId && !initialData?.contentItems?.length,
|
|
89
|
-
);
|
|
90
|
-
const [isFetchingCommDefinition, setIsFetchingCommDefinition] = useState(shouldFetchCommDefinition);
|
|
91
|
-
|
|
92
79
|
// Initialize step data from initialData or defaults
|
|
93
80
|
const [stepData, setStepData] = useState(() => {
|
|
94
81
|
const defaultMessageType = messageTypeData.defaultOption?.value || MESSAGE_TYPES_OPTIONS?.[1]?.value || null;
|
|
95
82
|
return {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// loading state — while the fetch below is in flight, instead of staying blank/unmounted.
|
|
99
|
-
messageType: initialData?.messageType || (shouldFetchCommDefinition ? 'transactional' : defaultMessageType),
|
|
100
|
-
communicationStrategy: initialData?.communicationStrategy || (shouldFetchCommDefinition ? SINGLE_TEMPLATE : null),
|
|
83
|
+
messageType: initialData?.messageType || defaultMessageType,
|
|
84
|
+
communicationStrategy: initialData?.communicationStrategy || null,
|
|
101
85
|
channel: initialData?.channel || config.channel || null,
|
|
102
86
|
channels: initialData?.channels || [],
|
|
103
87
|
selectedOfferDetails: initialData?.selectedOfferDetails || [],
|
|
@@ -108,29 +92,6 @@ const CommunicationFlow = ({
|
|
|
108
92
|
});
|
|
109
93
|
const [validationErrors, setValidationErrors] = useState({});
|
|
110
94
|
|
|
111
|
-
// Fetch-by-id: populate stepData from the CommDefinition itself when the consumer only
|
|
112
|
-
// supplied a commDefinitionId (see shouldFetchCommDefinition above). Runs once on mount.
|
|
113
|
-
useEffect(() => {
|
|
114
|
-
if (!shouldFetchCommDefinition) return;
|
|
115
|
-
let cancelled = false;
|
|
116
|
-
(async () => {
|
|
117
|
-
try {
|
|
118
|
-
const mapped = await fetchAndMapCommDefinition(config.context.existingCommDefinitionId, {
|
|
119
|
-
getCommDefinition,
|
|
120
|
-
getCommDefinitionVersion,
|
|
121
|
-
});
|
|
122
|
-
if (!cancelled && mapped) {
|
|
123
|
-
setStepData((prevStepData) => ({ ...prevStepData, ...mapped }));
|
|
124
|
-
}
|
|
125
|
-
} catch (error) {
|
|
126
|
-
console.error('[CommunicationFlow] fetchAndMapCommDefinition error:', error);
|
|
127
|
-
} finally {
|
|
128
|
-
if (!cancelled) setIsFetchingCommDefinition(false);
|
|
129
|
-
}
|
|
130
|
-
})();
|
|
131
|
-
return () => { cancelled = true; };
|
|
132
|
-
}, []);
|
|
133
|
-
|
|
134
95
|
// Memoize enabled steps
|
|
135
96
|
const enabledSteps = useMemo(() => getEnabledSteps(config), [config]);
|
|
136
97
|
|
|
@@ -386,7 +347,6 @@ const CommunicationFlow = ({
|
|
|
386
347
|
deliverySettingsData={config.features?.deliverySettingsData}
|
|
387
348
|
config={config}
|
|
388
349
|
capData={cap || capData}
|
|
389
|
-
isContentLoading={isFetchingCommDefinition}
|
|
390
350
|
/>
|
|
391
351
|
{stepData.contentItems?.length > 0 && <CapDivider />}
|
|
392
352
|
</CapRow>
|
|
@@ -20,15 +20,12 @@ import CapLabel from '@capillarytech/cap-ui-library/CapLabel';
|
|
|
20
20
|
import CapHeader from '@capillarytech/cap-ui-library/CapHeader';
|
|
21
21
|
import CapSlideBox from '@capillarytech/cap-ui-library/CapSlideBox';
|
|
22
22
|
import CapTooltip from '@capillarytech/cap-ui-library/CapTooltip';
|
|
23
|
-
import CapSpin from '@capillarytech/cap-ui-library/CapSpin';
|
|
24
23
|
import CommunicationFlow from './index';
|
|
25
24
|
import { CHANNELS, DEFAULT_COMMUNICATION_STRATEGY_OPTIONS, DYNAMIC_CONTROLS_CONFIG } from './constants';
|
|
26
25
|
import {
|
|
27
26
|
VIBER, SMS, EMAIL, WHATSAPP, RCS, ZALO,
|
|
28
27
|
} from '../CreativesContainer/constants';
|
|
29
28
|
import getContentBody from './utils/getContentBody';
|
|
30
|
-
import { getCommDefinition, getCommDefinitionVersion } from '../../services/api';
|
|
31
|
-
import { fetchAndMapCommDefinition } from './utils/mapCommDefinitionToStepData';
|
|
32
29
|
import messages from './messages';
|
|
33
30
|
import addCreativesIllustration from '../Assets/images/addCreativesIllustration.svg';
|
|
34
31
|
import './CommunicationFlow.scss';
|
|
@@ -93,40 +90,6 @@ const CommunicationFlowCard = ({
|
|
|
93
90
|
}
|
|
94
91
|
}, [initialData]);
|
|
95
92
|
|
|
96
|
-
// A consumer other than CapNotify (which always pre-fetches and passes full initialData
|
|
97
|
-
// itself) may hand this card only a commDefinitionId — no local content copy at all — and
|
|
98
|
-
// rely on CommunicationFlowCard to fetch + show the configured summary itself, instead of
|
|
99
|
-
// the empty "Add creatives" state. Mirrors CommunicationFlow.js's own fetch-by-id path
|
|
100
|
-
// (config.context.existingCommDefinitionId); doing it here too means the SUMMARY CARD
|
|
101
|
-
// reflects fetched content immediately, without the user having to open the slidebox and
|
|
102
|
-
// click Save first — CommunicationFlow's own fetch would only ever reach this card's
|
|
103
|
-
// savedData via that save round-trip, since nothing here listens for its internal onChange.
|
|
104
|
-
const [isFetchingCommDefinition, setIsFetchingCommDefinition] = useState(
|
|
105
|
-
() => !!config?.context?.existingCommDefinitionId && !initialData?.contentItems?.length,
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
useEffect(() => {
|
|
109
|
-
const existingCommDefinitionId = config?.context?.existingCommDefinitionId;
|
|
110
|
-
if (!existingCommDefinitionId || initialData?.contentItems?.length) return undefined;
|
|
111
|
-
let cancelled = false;
|
|
112
|
-
(async () => {
|
|
113
|
-
try {
|
|
114
|
-
const mapped = await fetchAndMapCommDefinition(existingCommDefinitionId, {
|
|
115
|
-
getCommDefinition,
|
|
116
|
-
getCommDefinitionVersion,
|
|
117
|
-
});
|
|
118
|
-
if (!cancelled && mapped) {
|
|
119
|
-
setSavedData((prevSavedData) => ({ ...prevSavedData, ...mapped }));
|
|
120
|
-
}
|
|
121
|
-
} catch (error) {
|
|
122
|
-
console.error('[CommunicationFlowCard] fetchAndMapCommDefinition error:', error);
|
|
123
|
-
} finally {
|
|
124
|
-
if (!cancelled) setIsFetchingCommDefinition(false);
|
|
125
|
-
}
|
|
126
|
-
})();
|
|
127
|
-
return () => { cancelled = true; };
|
|
128
|
-
}, []);
|
|
129
|
-
|
|
130
93
|
const handleSave = useCallback((data) => {
|
|
131
94
|
setSavedData(data);
|
|
132
95
|
setShowSlideBox(false);
|
|
@@ -236,16 +199,7 @@ const CommunicationFlowCard = ({
|
|
|
236
199
|
|
|
237
200
|
return (
|
|
238
201
|
<>
|
|
239
|
-
{
|
|
240
|
-
<CapCard
|
|
241
|
-
className="communication-flow-card-loading"
|
|
242
|
-
bodyStyle={{ padding: 0, height: 152 }}
|
|
243
|
-
>
|
|
244
|
-
<CapRow type="flex" justify="center" align="middle" style={{ height: '100%' }}>
|
|
245
|
-
<CapSpin spinning />
|
|
246
|
-
</CapRow>
|
|
247
|
-
</CapCard>
|
|
248
|
-
) : !firstItem ? (
|
|
202
|
+
{!firstItem ? (
|
|
249
203
|
<CapCard
|
|
250
204
|
className="communication-flow-card-empty"
|
|
251
205
|
bodyStyle={{ padding: 0, height: 152 }}
|
|
@@ -38,25 +38,14 @@ jest.mock('@capillarytech/cap-ui-library/CapIcon', () => function MockCapIcon({
|
|
|
38
38
|
|
|
39
39
|
jest.mock('../utils/getContentBody', () => jest.fn());
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
getCommDefinition: jest.fn(),
|
|
43
|
-
getCommDefinitionVersion: jest.fn(),
|
|
44
|
-
}));
|
|
45
|
-
|
|
46
|
-
jest.mock('../utils/mapCommDefinitionToStepData', () => ({
|
|
47
|
-
fetchAndMapCommDefinition: jest.fn(),
|
|
48
|
-
}));
|
|
49
|
-
|
|
50
|
-
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
41
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
51
42
|
import '@testing-library/jest-dom';
|
|
52
43
|
import { IntlProvider } from 'react-intl';
|
|
53
44
|
import CommunicationFlowCard from '../CommunicationFlowCard';
|
|
54
45
|
import getContentBody from '../utils/getContentBody';
|
|
55
|
-
import { fetchAndMapCommDefinition } from '../utils/mapCommDefinitionToStepData';
|
|
56
46
|
|
|
57
47
|
beforeEach(() => {
|
|
58
48
|
getContentBody.mockReturnValue(<span data-testid="content-body">Content</span>);
|
|
59
|
-
fetchAndMapCommDefinition.mockReset();
|
|
60
49
|
});
|
|
61
50
|
|
|
62
51
|
const baseConfig = {
|
|
@@ -722,52 +711,3 @@ describe('CommunicationFlowCard', () => {
|
|
|
722
711
|
});
|
|
723
712
|
});
|
|
724
713
|
});
|
|
725
|
-
|
|
726
|
-
describe('CommunicationFlowCard — fetch-by-id (config.context.existingCommDefinitionId)', () => {
|
|
727
|
-
const configWithId = {
|
|
728
|
-
...baseConfig,
|
|
729
|
-
context: { existingCommDefinitionId: 'cd_1' },
|
|
730
|
-
};
|
|
731
|
-
|
|
732
|
-
it('shows a loading state (not the empty "Add creatives" state) while fetching', () => {
|
|
733
|
-
fetchAndMapCommDefinition.mockReturnValue(new Promise(() => {})); // never resolves
|
|
734
|
-
renderCard({ config: configWithId, initialData: null });
|
|
735
|
-
expect(screen.queryByText('Add creatives')).not.toBeInTheDocument();
|
|
736
|
-
expect(screen.queryByText('Message:')).not.toBeInTheDocument();
|
|
737
|
-
});
|
|
738
|
-
|
|
739
|
-
it('shows the configured summary card once the fetch resolves, without opening the slidebox', async () => {
|
|
740
|
-
fetchAndMapCommDefinition.mockResolvedValue(makeSavedData());
|
|
741
|
-
renderCard({ config: configWithId, initialData: null });
|
|
742
|
-
|
|
743
|
-
await waitFor(() => expect(screen.getByText('Message:')).toBeInTheDocument());
|
|
744
|
-
expect(screen.queryByText('Add creatives')).not.toBeInTheDocument();
|
|
745
|
-
expect(screen.queryByTestId('slide-box')).not.toBeInTheDocument();
|
|
746
|
-
});
|
|
747
|
-
|
|
748
|
-
it('falls back to the empty "Add creatives" state when the fetch resolves with nothing mappable', async () => {
|
|
749
|
-
fetchAndMapCommDefinition.mockResolvedValue(null);
|
|
750
|
-
renderCard({ config: configWithId, initialData: null });
|
|
751
|
-
|
|
752
|
-
await waitFor(() => expect(screen.getByText('Add creatives')).toBeInTheDocument());
|
|
753
|
-
});
|
|
754
|
-
|
|
755
|
-
it('falls back to the empty "Add creatives" state when the fetch rejects', async () => {
|
|
756
|
-
fetchAndMapCommDefinition.mockRejectedValue(new Error('network error'));
|
|
757
|
-
renderCard({ config: configWithId, initialData: null });
|
|
758
|
-
|
|
759
|
-
await waitFor(() => expect(screen.getByText('Add creatives')).toBeInTheDocument());
|
|
760
|
-
});
|
|
761
|
-
|
|
762
|
-
it('does not fetch when initialData already has content (e.g. CapNotify, which always pre-fetches itself)', () => {
|
|
763
|
-
renderCard({ config: configWithId, initialData: makeSavedData() });
|
|
764
|
-
expect(fetchAndMapCommDefinition).not.toHaveBeenCalled();
|
|
765
|
-
expect(screen.getByText('Message:')).toBeInTheDocument();
|
|
766
|
-
});
|
|
767
|
-
|
|
768
|
-
it('does not fetch when config.context.existingCommDefinitionId is absent', () => {
|
|
769
|
-
renderCard({ config: baseConfig, initialData: null });
|
|
770
|
-
expect(fetchAndMapCommDefinition).not.toHaveBeenCalled();
|
|
771
|
-
expect(screen.getByText('Add creatives')).toBeInTheDocument();
|
|
772
|
-
});
|
|
773
|
-
});
|
|
@@ -16,7 +16,6 @@ import CapIcon from '@capillarytech/cap-ui-library/CapIcon';
|
|
|
16
16
|
import CapLabel from '@capillarytech/cap-ui-library/CapLabel';
|
|
17
17
|
import CapHeader from '@capillarytech/cap-ui-library/CapHeader';
|
|
18
18
|
import CapCustomCard from '@capillarytech/cap-ui-library/CapCustomCard';
|
|
19
|
-
import CapSpin from '@capillarytech/cap-ui-library/CapSpin';
|
|
20
19
|
import CreativesContainer from '../../../CreativesContainer';
|
|
21
20
|
import TestAndPreviewSlidebox from '../../../../v2Components/TestAndPreviewSlidebox';
|
|
22
21
|
import { DeliverySettingsSection } from '../DeliverySettingsStep';
|
|
@@ -55,11 +54,6 @@ const ChannelSelectionStep = ({
|
|
|
55
54
|
intl,
|
|
56
55
|
capData, // From Redux - contains user/org info needed by CouponsWrapper
|
|
57
56
|
config,
|
|
58
|
-
// True while CommunicationFlow is fetching an existing CommDefinition's content by id (see
|
|
59
|
-
// config.context.existingCommDefinitionId in CommunicationFlow.js) — shown in place of the
|
|
60
|
-
// empty "Add creative" state so a consumer that only stored a commDefinitionId doesn't flash
|
|
61
|
-
// an empty content step before the fetched content arrives.
|
|
62
|
-
isContentLoading = false,
|
|
63
57
|
}) => {
|
|
64
58
|
const contentItems = value?.contentItems || [];
|
|
65
59
|
const [showCreativesContainer, setShowCreativesContainer] = useState(false);
|
|
@@ -439,11 +433,7 @@ const ChannelSelectionStep = ({
|
|
|
439
433
|
</CapHeading>
|
|
440
434
|
)}
|
|
441
435
|
|
|
442
|
-
{contentItems?.length === 0
|
|
443
|
-
<CapRow type="flex" justify="center" align="middle" className="content-template-section content-loading">
|
|
444
|
-
<CapSpin spinning />
|
|
445
|
-
</CapRow>
|
|
446
|
-
) : contentItems?.length === 0 ? (
|
|
436
|
+
{contentItems?.length === 0 ? (
|
|
447
437
|
<CapRow className={`content-template-section ${contentItems?.length === 0 ? 'no-content-items' : ''}`}>
|
|
448
438
|
<CapDropdown
|
|
449
439
|
overlay={renderChannelDropdownOverlay() || <CapMenu />}
|
|
@@ -592,7 +582,6 @@ ChannelSelectionStep.propTypes = {
|
|
|
592
582
|
intl: PropTypes.object.isRequired,
|
|
593
583
|
capData: PropTypes.object, // Cap data from Redux (user/org info)
|
|
594
584
|
config: PropTypes.object,
|
|
595
|
-
isContentLoading: PropTypes.bool,
|
|
596
585
|
};
|
|
597
586
|
|
|
598
587
|
ChannelSelectionStep.defaultProps = {
|
|
@@ -608,7 +597,6 @@ ChannelSelectionStep.defaultProps = {
|
|
|
608
597
|
incentivesData: null,
|
|
609
598
|
capData: {},
|
|
610
599
|
config: {},
|
|
611
|
-
isContentLoading: false,
|
|
612
600
|
};
|
|
613
601
|
|
|
614
602
|
export default injectIntl(ChannelSelectionStep);
|
|
@@ -110,9 +110,9 @@ export class TagList extends React.Component { // eslint-disable-line react/pref
|
|
|
110
110
|
} = prevProps;
|
|
111
111
|
|
|
112
112
|
if (
|
|
113
|
-
tags
|
|
114
|
-
|| injectedTags
|
|
115
|
-
|| selectedOfferDetails
|
|
113
|
+
!_.isEqual(tags, prevTags)
|
|
114
|
+
|| !_.isEqual(injectedTags, prevInjectedTags)
|
|
115
|
+
|| !_.isEqual(selectedOfferDetails, prevSelectedOfferDetails)
|
|
116
116
|
|| !_.isEqual(eventContextTags, prevEventContextTags)
|
|
117
117
|
|| !_.isEqual(waitEventContextTags, prevWaitEventContextTags)
|
|
118
118
|
) {
|
|
@@ -134,6 +134,22 @@ describe("TagList test : UNIT", () => {
|
|
|
134
134
|
expect(() => unmount()).not.toThrow();
|
|
135
135
|
});
|
|
136
136
|
|
|
137
|
+
it('does not regenerate tags when selectedOfferDetails gets a new [] reference with same content', () => {
|
|
138
|
+
const spy = jest.spyOn(TagList.prototype, 'populateTags');
|
|
139
|
+
const { rerender, Component, store } = initializeTagList({
|
|
140
|
+
selectedOfferDetails: [],
|
|
141
|
+
tags: TagListData.tags,
|
|
142
|
+
});
|
|
143
|
+
spy.mockClear();
|
|
144
|
+
rerender(
|
|
145
|
+
<Provider store={store}>
|
|
146
|
+
<Component {...buildProps({ selectedOfferDetails: [], tags: TagListData.tags })} />
|
|
147
|
+
</Provider>
|
|
148
|
+
);
|
|
149
|
+
expect(spy).not.toHaveBeenCalled();
|
|
150
|
+
spy.mockRestore();
|
|
151
|
+
});
|
|
152
|
+
|
|
137
153
|
it('regenerates tags when props.tags change (componentDidUpdate)', () => {
|
|
138
154
|
const { rerender, Component, store } = initializeTagList({ tags: TagListData.tags });
|
|
139
155
|
const extra = [
|
|
@@ -13,6 +13,12 @@ export const VIBER_IMG_SIZE = 10000000;
|
|
|
13
13
|
export const VIBER_VIDEO_SIZE = 209715200;
|
|
14
14
|
export const charLimit = 1000;
|
|
15
15
|
|
|
16
|
+
/** Stable fallbacks for TagList props — inline `= []` / `|| {}` allocate new refs every render. */
|
|
17
|
+
export const EMPTY_OFFER_DETAILS = [];
|
|
18
|
+
export const EMPTY_INJECTED_TAGS = {};
|
|
19
|
+
export const EMPTY_TAGS = [];
|
|
20
|
+
export const EMPTY_TEMPLATE_DATA = {};
|
|
21
|
+
|
|
16
22
|
export const UPLOAD_VIBER_ASSET_REQUEST = 'app/v2Containers/Viber/UPLOAD_ASSET_REQUEST';
|
|
17
23
|
export const UPLOAD_VIBER_ASSET_SUCCESS = 'app/v2Containers/Viber/UPLOAD_ASSET_SUCCESS';
|
|
18
24
|
export const UPLOAD_VIBER_ASSET_FAILURE = 'app/v2Containers/Viber/UPLOAD_ASSET_FAILURE';
|
|
@@ -63,6 +63,10 @@ import {
|
|
|
63
63
|
VIBER_CAROUSEL_IMG_SIZE,
|
|
64
64
|
STATIC_URL,
|
|
65
65
|
DYNAMIC_URL,
|
|
66
|
+
EMPTY_OFFER_DETAILS,
|
|
67
|
+
EMPTY_INJECTED_TAGS,
|
|
68
|
+
EMPTY_TAGS,
|
|
69
|
+
EMPTY_TEMPLATE_DATA,
|
|
66
70
|
} from './constants';
|
|
67
71
|
import withCreatives from '../../hoc/withCreatives';
|
|
68
72
|
import {
|
|
@@ -129,12 +133,12 @@ export const Viber = (props) => {
|
|
|
129
133
|
handleClose,
|
|
130
134
|
onCreateComplete,
|
|
131
135
|
params,
|
|
132
|
-
templateData =
|
|
136
|
+
templateData = EMPTY_TEMPLATE_DATA,
|
|
133
137
|
actions,
|
|
134
138
|
viber = {},
|
|
135
139
|
getFormSubscriptionData,
|
|
136
140
|
viberData = {},
|
|
137
|
-
selectedOfferDetails =
|
|
141
|
+
selectedOfferDetails = EMPTY_OFFER_DETAILS,
|
|
138
142
|
eventContextTags,
|
|
139
143
|
waitEventContextTags,
|
|
140
144
|
// TestAndPreviewSlidebox props
|
|
@@ -258,7 +262,7 @@ export const Viber = (props) => {
|
|
|
258
262
|
setTemplateMediaType(VIBER_MEDIA_TYPES.TEXT);
|
|
259
263
|
}
|
|
260
264
|
}
|
|
261
|
-
}, [viber.templateDetails
|
|
265
|
+
}, [params?.id, viber.templateDetails, templateData]);
|
|
262
266
|
|
|
263
267
|
// Reports live message-content validity to a parent (e.g. CreativesContainer's
|
|
264
268
|
// slidebox) so it can disable its own Done/Preview-and-test buttons whenever the
|
|
@@ -332,7 +336,8 @@ export const Viber = (props) => {
|
|
|
332
336
|
globalActionsProps.fetchSchemaForEntity(query);
|
|
333
337
|
};
|
|
334
338
|
|
|
335
|
-
const tags = metaEntities?.tags?.standard
|
|
339
|
+
const tags = metaEntities?.tags?.standard ?? EMPTY_TAGS;
|
|
340
|
+
const resolvedInjectedTags = injectedTags ?? EMPTY_INJECTED_TAGS;
|
|
336
341
|
// tags Code end here
|
|
337
342
|
|
|
338
343
|
// validation on Text area and tags validation
|
|
@@ -393,7 +398,7 @@ export const Viber = (props) => {
|
|
|
393
398
|
onContextChange={handleOnTagsContextChange}
|
|
394
399
|
location={location}
|
|
395
400
|
tags={tags}
|
|
396
|
-
injectedTags={
|
|
401
|
+
injectedTags={resolvedInjectedTags}
|
|
397
402
|
id="viber_tags"
|
|
398
403
|
userLocale={localStorage.getItem("jlocale") || "en"}
|
|
399
404
|
selectedOfferDetails={selectedOfferDetails}
|
|
@@ -882,7 +887,7 @@ export const Viber = (props) => {
|
|
|
882
887
|
onContextChange={handleOnTagsContextChange}
|
|
883
888
|
location={location}
|
|
884
889
|
tags={tags}
|
|
885
|
-
injectedTags={
|
|
890
|
+
injectedTags={resolvedInjectedTags}
|
|
886
891
|
id={`viber_carousel_card_tags_${cardIndex}`}
|
|
887
892
|
userLocale={localStorage.getItem("jlocale") || "en"}
|
|
888
893
|
selectedOfferDetails={selectedOfferDetails}
|
|
@@ -1260,7 +1265,7 @@ export const Viber = (props) => {
|
|
|
1260
1265
|
onContextChange={handleOnTagsContextChange}
|
|
1261
1266
|
location={location}
|
|
1262
1267
|
tags={tags}
|
|
1263
|
-
injectedTags={
|
|
1268
|
+
injectedTags={resolvedInjectedTags}
|
|
1264
1269
|
userLocale={localStorage.getItem("jlocale") || "en"}
|
|
1265
1270
|
selectedOfferDetails={selectedOfferDetails}
|
|
1266
1271
|
eventContextTags={eventContextTags}
|
|
@@ -1578,15 +1583,16 @@ export const Viber = (props) => {
|
|
|
1578
1583
|
</CapButton>
|
|
1579
1584
|
</ViberFooter>
|
|
1580
1585
|
</CapSpin>
|
|
1581
|
-
{
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1586
|
+
{(showTestAndPreviewSlidebox || propsShowTestAndPreviewSlidebox) && (
|
|
1587
|
+
<TestAndPreviewSlidebox
|
|
1588
|
+
show={showTestAndPreviewSlidebox || propsShowTestAndPreviewSlidebox}
|
|
1589
|
+
onClose={handleCloseTestAndPreview}
|
|
1590
|
+
channel={VIBER}
|
|
1591
|
+
formData={getTemplateContent()}
|
|
1592
|
+
content={getTemplateContent()}
|
|
1593
|
+
formatMessage={formatMessage}
|
|
1594
|
+
/>
|
|
1595
|
+
)}
|
|
1590
1596
|
</>
|
|
1591
1597
|
);
|
|
1592
1598
|
};
|
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
mapCommDefinitionToStepData,
|
|
3
|
-
fetchAndMapCommDefinition,
|
|
4
|
-
getLatestVersionFromAuditInfos,
|
|
5
|
-
} from '../mapCommDefinitionToStepData';
|
|
6
|
-
|
|
7
|
-
describe('getLatestVersionFromAuditInfos', () => {
|
|
8
|
-
it('returns currentVersion (or 0) when there are no SUBMITTED audit entries', () => {
|
|
9
|
-
expect(getLatestVersionFromAuditInfos({ auditInfos: [], currentVersion: 3 })).toBe(3);
|
|
10
|
-
expect(getLatestVersionFromAuditInfos({})).toBe(0);
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it('returns one less than the SUBMITTED count when there are submitted entries', () => {
|
|
14
|
-
const commDefinition = {
|
|
15
|
-
auditInfos: [
|
|
16
|
-
{ action: 'CREATED' },
|
|
17
|
-
{ action: 'SUBMITTED' },
|
|
18
|
-
{ action: 'APPROVED' },
|
|
19
|
-
{ action: 'SUBMITTED' },
|
|
20
|
-
],
|
|
21
|
-
};
|
|
22
|
-
expect(getLatestVersionFromAuditInfos(commDefinition)).toBe(1);
|
|
23
|
-
});
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
describe('mapCommDefinitionToStepData', () => {
|
|
27
|
-
it('returns null when there is no variant at all', () => {
|
|
28
|
-
expect(mapCommDefinitionToStepData({})).toBeNull();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('returns null when the variant has no recognized channel content', () => {
|
|
32
|
-
const commDefinition = {
|
|
33
|
-
id: 'cd_1',
|
|
34
|
-
singleChannelStrategy: { variant: { channel: 'SMS' } },
|
|
35
|
-
};
|
|
36
|
-
expect(mapCommDefinitionToStepData(commDefinition)).toBeNull();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('maps an SMS CommDefinition (inverse of the SMS forward transform) into stepData', () => {
|
|
40
|
-
const commDefinition = {
|
|
41
|
-
id: 'cd_1',
|
|
42
|
-
referenceId: 'ref_1',
|
|
43
|
-
status: 'DRAFT',
|
|
44
|
-
singleChannelStrategy: {
|
|
45
|
-
variant: {
|
|
46
|
-
channel: 'SMS',
|
|
47
|
-
smsMessageContent: { message: 'Hello {{first_name}}', channel: 'SMS' },
|
|
48
|
-
smsDeliverySettings: { channelSettings: { senderId: 'ABC' } },
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const result = mapCommDefinitionToStepData(commDefinition);
|
|
54
|
-
|
|
55
|
-
expect(result).toMatchObject({
|
|
56
|
-
messageType: 'transactional',
|
|
57
|
-
communicationStrategy: 'SINGLE_TEMPLATE',
|
|
58
|
-
channel: 'SMS',
|
|
59
|
-
contentItems: [
|
|
60
|
-
{
|
|
61
|
-
contentId: 'cd_1',
|
|
62
|
-
channel: 'SMS',
|
|
63
|
-
templateData: { messageBody: 'Hello {{first_name}}', channel: 'SMS' },
|
|
64
|
-
},
|
|
65
|
-
],
|
|
66
|
-
deliverySetting: { channelSetting: { SMS: { senderId: 'ABC' } } },
|
|
67
|
-
ccsCommDefinition: {
|
|
68
|
-
id: 'cd_1',
|
|
69
|
-
referenceId: 'ref_1',
|
|
70
|
-
status: 'DRAFT',
|
|
71
|
-
},
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('maps a mobile push (PUSH) CommDefinition back to the MOBILEPUSH UI channel via CCS_CHANNEL_NAME_MAP', () => {
|
|
76
|
-
const commDefinition = {
|
|
77
|
-
id: 'cd_2',
|
|
78
|
-
singleChannelStrategy: {
|
|
79
|
-
variant: {
|
|
80
|
-
channel: 'PUSH',
|
|
81
|
-
mpushMessageContent: {
|
|
82
|
-
channel: 'PUSH',
|
|
83
|
-
androidContent: { title: 'Hi', message: 'There' },
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
const result = mapCommDefinitionToStepData(commDefinition);
|
|
90
|
-
|
|
91
|
-
expect(result.channel).toBe('MOBILEPUSH');
|
|
92
|
-
expect(result.contentItems[0].channel).toBe('MOBILEPUSH');
|
|
93
|
-
// channel is stamped last so it always reads MOBILEPUSH, not the raw CCS "PUSH".
|
|
94
|
-
expect(result.contentItems[0].templateData.channel).toBe('MOBILEPUSH');
|
|
95
|
-
expect(result.contentItems[0].templateData.androidContent).toEqual({ title: 'Hi', message: 'There' });
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('maps an EMAIL CommDefinition using the dedicated reverse transform', () => {
|
|
99
|
-
const commDefinition = {
|
|
100
|
-
id: 'cd_3',
|
|
101
|
-
singleChannelStrategy: {
|
|
102
|
-
variant: {
|
|
103
|
-
channel: 'EMAIL',
|
|
104
|
-
emailMessageContent: { messageSubject: 'Subject', messageBody: '<p>Body</p>' },
|
|
105
|
-
},
|
|
106
|
-
},
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
const result = mapCommDefinitionToStepData(commDefinition);
|
|
110
|
-
|
|
111
|
-
expect(result.contentItems[0].templateData).toEqual({
|
|
112
|
-
emailSubject: 'Subject',
|
|
113
|
-
emailBody: '<p>Body</p>',
|
|
114
|
-
channel: 'EMAIL',
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('reads the variant from commDefinition.version when top-level singleChannelStrategy is absent', () => {
|
|
119
|
-
const commDefinition = {
|
|
120
|
-
id: 'cd_4',
|
|
121
|
-
version: {
|
|
122
|
-
version: 2,
|
|
123
|
-
status: 'DRAFT',
|
|
124
|
-
singleChannelStrategy: {
|
|
125
|
-
variant: {
|
|
126
|
-
channel: 'SMS',
|
|
127
|
-
smsMessageContent: { message: 'From version' },
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
},
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
const result = mapCommDefinitionToStepData(commDefinition);
|
|
134
|
-
|
|
135
|
-
expect(result.contentItems[0].templateData.messageBody).toBe('From version');
|
|
136
|
-
expect(result.ccsCommDefinition.version).toBe(2);
|
|
137
|
-
expect(result.ccsCommDefinition.status).toBe('DRAFT');
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('falls back to getLatestVersionFromAuditInfos when version.version is absent', () => {
|
|
141
|
-
const commDefinition = {
|
|
142
|
-
id: 'cd_5',
|
|
143
|
-
auditInfos: [{ action: 'SUBMITTED' }, { action: 'SUBMITTED' }],
|
|
144
|
-
singleChannelStrategy: {
|
|
145
|
-
variant: { channel: 'SMS', smsMessageContent: { message: 'Hi' } },
|
|
146
|
-
},
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
const result = mapCommDefinitionToStepData(commDefinition);
|
|
150
|
-
|
|
151
|
-
expect(result.ccsCommDefinition.version).toBe(1);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('maps additionalSettings into dynamicControls', () => {
|
|
155
|
-
const commDefinition = {
|
|
156
|
-
id: 'cd_6',
|
|
157
|
-
version: {
|
|
158
|
-
settings: {
|
|
159
|
-
additionalSettings: {
|
|
160
|
-
useTinyUrl: true,
|
|
161
|
-
encryptUrl: true,
|
|
162
|
-
linkTrackingEnabled: true,
|
|
163
|
-
userSubscriptionDisabled: true,
|
|
164
|
-
},
|
|
165
|
-
},
|
|
166
|
-
},
|
|
167
|
-
singleChannelStrategy: {
|
|
168
|
-
variant: { channel: 'SMS', smsMessageContent: { message: 'Hi' } },
|
|
169
|
-
},
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
const result = mapCommDefinitionToStepData(commDefinition);
|
|
173
|
-
|
|
174
|
-
expect(result.dynamicControls).toEqual({
|
|
175
|
-
useTinyUrl: true,
|
|
176
|
-
sendToControlCustomers: true,
|
|
177
|
-
overrideDailyLimit: true,
|
|
178
|
-
sendToBrandPocs: true,
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it('passes through content for a channel with no dedicated reverse transform (e.g. RCS)', () => {
|
|
183
|
-
const commDefinition = {
|
|
184
|
-
id: 'cd_7',
|
|
185
|
-
singleChannelStrategy: {
|
|
186
|
-
variant: {
|
|
187
|
-
channel: 'RCS',
|
|
188
|
-
rcsMessageContent: {
|
|
189
|
-
channel: 'RCS',
|
|
190
|
-
accountId: 123,
|
|
191
|
-
rcsRichCardContent: { cardContent: [{ title: 'Card' }] },
|
|
192
|
-
},
|
|
193
|
-
},
|
|
194
|
-
},
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
const result = mapCommDefinitionToStepData(commDefinition);
|
|
198
|
-
|
|
199
|
-
expect(result.contentItems[0].templateData).toEqual({
|
|
200
|
-
accountId: 123,
|
|
201
|
-
rcsRichCardContent: { cardContent: [{ title: 'Card' }] },
|
|
202
|
-
channel: 'RCS',
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
describe('fetchAndMapCommDefinition', () => {
|
|
208
|
-
it('returns null when getCommDefinition resolves with no data', async () => {
|
|
209
|
-
const getCommDefinitionMock = jest.fn().mockResolvedValue({ response: {} });
|
|
210
|
-
const getCommDefinitionVersionMock = jest.fn();
|
|
211
|
-
|
|
212
|
-
const result = await fetchAndMapCommDefinition('cd_1', {
|
|
213
|
-
getCommDefinition: getCommDefinitionMock,
|
|
214
|
-
getCommDefinitionVersion: getCommDefinitionVersionMock,
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
expect(result).toBeNull();
|
|
218
|
-
expect(getCommDefinitionMock).toHaveBeenCalledWith('cd_1', 'versions');
|
|
219
|
-
expect(getCommDefinitionVersionMock).not.toHaveBeenCalled();
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it('fetches metadata + latest version, merges them, and maps the result', async () => {
|
|
223
|
-
const getCommDefinitionMock = jest.fn().mockResolvedValue({
|
|
224
|
-
response: {
|
|
225
|
-
data: {
|
|
226
|
-
id: 'cd_1',
|
|
227
|
-
referenceId: 'ref_1',
|
|
228
|
-
status: 'DRAFT',
|
|
229
|
-
auditInfos: [],
|
|
230
|
-
currentVersion: 0,
|
|
231
|
-
},
|
|
232
|
-
},
|
|
233
|
-
});
|
|
234
|
-
const getCommDefinitionVersionMock = jest.fn().mockResolvedValue({
|
|
235
|
-
response: {
|
|
236
|
-
data: {
|
|
237
|
-
singleChannelStrategy: {
|
|
238
|
-
variant: { channel: 'SMS', smsMessageContent: { message: 'Hello' } },
|
|
239
|
-
},
|
|
240
|
-
},
|
|
241
|
-
},
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
const result = await fetchAndMapCommDefinition('cd_1', {
|
|
245
|
-
getCommDefinition: getCommDefinitionMock,
|
|
246
|
-
getCommDefinitionVersion: getCommDefinitionVersionMock,
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
expect(getCommDefinitionVersionMock).toHaveBeenCalledWith('cd_1', 0);
|
|
250
|
-
expect(result.contentItems[0].templateData.messageBody).toBe('Hello');
|
|
251
|
-
expect(result.ccsCommDefinition.id).toBe('cd_1');
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it('swallows a version-fetch failure and still returns metadata-only mapping (null, since there is no content)', async () => {
|
|
255
|
-
const getCommDefinitionMock = jest.fn().mockResolvedValue({
|
|
256
|
-
response: { data: { id: 'cd_1', auditInfos: [] } },
|
|
257
|
-
});
|
|
258
|
-
const getCommDefinitionVersionMock = jest.fn().mockRejectedValue(new Error('network error'));
|
|
259
|
-
|
|
260
|
-
const result = await fetchAndMapCommDefinition('cd_1', {
|
|
261
|
-
getCommDefinition: getCommDefinitionMock,
|
|
262
|
-
getCommDefinitionVersion: getCommDefinitionVersionMock,
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
// No content ever arrived (version fetch failed), so there's nothing mappable.
|
|
266
|
-
expect(result).toBeNull();
|
|
267
|
-
});
|
|
268
|
-
});
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reverse-maps a CCS CommDefinition back into CommunicationFlow's own `stepData` shape.
|
|
3
|
-
*
|
|
4
|
-
* This is the general-purpose counterpart of cap-campaigns-v2's own
|
|
5
|
-
* mapCommDefinitionToCommunicationFlowData (app/containers/CapNotifySettings/utils.js) — that
|
|
6
|
-
* version is CapNotify/Alert-specific and lives in the consumer app. Other consumers of
|
|
7
|
-
* CommunicationFlow ("Pluggable Modules") only store a commDefinitionId, with no local content
|
|
8
|
-
* copy, so CommunicationFlow needs to be able to fetch + reverse-map a CommDefinition itself
|
|
9
|
-
* (see fetchAndMapCommDefinition below, wired into CommunicationFlow.js via
|
|
10
|
-
* config.context.existingCommDefinitionId).
|
|
11
|
-
*/
|
|
12
|
-
import {
|
|
13
|
-
CCS_CHANNEL_CONTENT_KEY_MAP,
|
|
14
|
-
CCS_CHANNEL_DELIVERY_KEY_MAP,
|
|
15
|
-
CCS_CHANNEL_NAME_MAP,
|
|
16
|
-
} from '../constants';
|
|
17
|
-
|
|
18
|
-
// Reverse of CCS_CHANNEL_NAME_MAP (UI channel -> CCS channel); only PUSH -> MOBILEPUSH differs today.
|
|
19
|
-
const CCS_TO_UI_CHANNEL_MAP = Object.entries(CCS_CHANNEL_NAME_MAP).reduce(
|
|
20
|
-
(acc, [uiChannel, ccsChannel]) => ({ ...acc, [ccsChannel]: uiChannel }),
|
|
21
|
-
{},
|
|
22
|
-
);
|
|
23
|
-
const reverseUiChannel = (ccsChannel) => CCS_TO_UI_CHANNEL_MAP[ccsChannel] || ccsChannel;
|
|
24
|
-
|
|
25
|
-
// Inverse of CCS_CONTENT_TRANSFORMS (./constants.js) — only channels with a dedicated forward
|
|
26
|
-
// transform there need one here; every other channel's CCS content already matches the legacy
|
|
27
|
-
// editor's templateData shape and is passed through unchanged (see the default branch below).
|
|
28
|
-
const CCS_CONTENT_REVERSE_TRANSFORMS = {
|
|
29
|
-
EMAIL: (payload = {}) => ({
|
|
30
|
-
emailSubject: payload.messageSubject,
|
|
31
|
-
emailBody: payload.messageBody,
|
|
32
|
-
}),
|
|
33
|
-
SMS: (payload = {}) => ({
|
|
34
|
-
messageBody: payload.message || '',
|
|
35
|
-
}),
|
|
36
|
-
WEBPUSH: (payload = {}) => ({
|
|
37
|
-
messageContent: {
|
|
38
|
-
content: {
|
|
39
|
-
messageSubject: payload.messageSubject,
|
|
40
|
-
accountId: payload.accountId,
|
|
41
|
-
content: payload.content,
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
}),
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const getCommDefinitionVariant = (commDefinition = {}) => (
|
|
48
|
-
commDefinition.singleChannelStrategy?.variant
|
|
49
|
-
|| commDefinition.version?.singleChannelStrategy?.variant
|
|
50
|
-
|| null
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
// Counts SUBMITTED audit entries to derive the latest version number when the CommDefinition
|
|
54
|
-
// itself doesn't carry an explicit current version (mirrors cap-campaigns-v2's own helper).
|
|
55
|
-
export const getLatestVersionFromAuditInfos = (commDefinition = {}) => {
|
|
56
|
-
const submittedCount = (commDefinition.auditInfos || []).filter(
|
|
57
|
-
(entry) => entry?.action === 'SUBMITTED',
|
|
58
|
-
).length;
|
|
59
|
-
if (submittedCount === 0) return commDefinition.currentVersion ?? 0;
|
|
60
|
-
return submittedCount - 1;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
// Reverse of CommunicationFlow's own additionalSettings mapping (see CommunicationFlow.js's
|
|
64
|
-
// handleSave) — used to pre-populate the Advanced Controls step when editing existing content.
|
|
65
|
-
const mapAdditionalSettingsToDynamicControls = (additionalSettings = {}) => ({
|
|
66
|
-
useTinyUrl: additionalSettings.useTinyUrl ?? false,
|
|
67
|
-
sendToControlCustomers: additionalSettings.encryptUrl ?? false,
|
|
68
|
-
overrideDailyLimit: additionalSettings.linkTrackingEnabled ?? false,
|
|
69
|
-
sendToBrandPocs: additionalSettings.userSubscriptionDisabled ?? false,
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Maps a CCS CommDefinition (metadata + version content merged onto it — see
|
|
74
|
-
* fetchAndMapCommDefinition) into the subset of `stepData` CommunicationFlow needs to re-open
|
|
75
|
-
* its "Add content" editor with existing content, instead of starting empty. Returns null when
|
|
76
|
-
* there's no mappable channel/content (e.g. the version fetch failed or came back empty).
|
|
77
|
-
*/
|
|
78
|
-
export const mapCommDefinitionToStepData = (commDefinition = {}) => {
|
|
79
|
-
const variant = getCommDefinitionVariant(commDefinition);
|
|
80
|
-
const ccsChannel = variant?.channel;
|
|
81
|
-
if (!variant || !ccsChannel) return null;
|
|
82
|
-
|
|
83
|
-
const contentKey = CCS_CHANNEL_CONTENT_KEY_MAP[ccsChannel];
|
|
84
|
-
const rawContent = contentKey ? variant[contentKey] : null;
|
|
85
|
-
if (!rawContent) return null;
|
|
86
|
-
|
|
87
|
-
const uiChannel = reverseUiChannel(ccsChannel);
|
|
88
|
-
const reverseTransform = CCS_CONTENT_REVERSE_TRANSFORMS[ccsChannel];
|
|
89
|
-
// Spread channel after content: CCS content may carry its own raw `channel` ("PUSH"), which
|
|
90
|
-
// would otherwise overwrite `uiChannel` ("MOBILEPUSH") and break template rendering.
|
|
91
|
-
const templateData = reverseTransform
|
|
92
|
-
? { ...reverseTransform(rawContent), channel: uiChannel }
|
|
93
|
-
: { ...rawContent, channel: uiChannel };
|
|
94
|
-
|
|
95
|
-
const deliveryKey = CCS_CHANNEL_DELIVERY_KEY_MAP[ccsChannel];
|
|
96
|
-
const channelSettings = deliveryKey ? variant[deliveryKey]?.channelSettings : undefined;
|
|
97
|
-
|
|
98
|
-
const version = commDefinition.version || {};
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
// CCS create/edit only supports the SINGLE strategy today (see CommunicationFlow.js's
|
|
102
|
-
// handleSave, which skips CHANNEL_PRIORITY/AB_TEST) — a fetched CommDefinition is always one.
|
|
103
|
-
messageType: 'transactional',
|
|
104
|
-
communicationStrategy: 'SINGLE_TEMPLATE',
|
|
105
|
-
channel: uiChannel,
|
|
106
|
-
channels: [],
|
|
107
|
-
// contentId must be truthy — ChannelSelectionStep matches contentItems by it to resolve
|
|
108
|
-
// editingContentId; a falsy id collapses the reopen back to an empty template picker.
|
|
109
|
-
contentItems: [
|
|
110
|
-
{ contentId: commDefinition.id, channel: uiChannel, templateData },
|
|
111
|
-
],
|
|
112
|
-
deliverySetting: channelSettings
|
|
113
|
-
? { channelSetting: { [uiChannel]: channelSettings } }
|
|
114
|
-
: undefined,
|
|
115
|
-
dynamicControls: mapAdditionalSettingsToDynamicControls(
|
|
116
|
-
version.settings?.additionalSettings || commDefinition.settings?.additionalSettings,
|
|
117
|
-
),
|
|
118
|
-
ccsCommDefinition: {
|
|
119
|
-
id: commDefinition.id,
|
|
120
|
-
referenceId: commDefinition.referenceId,
|
|
121
|
-
version: version.version ?? getLatestVersionFromAuditInfos(commDefinition),
|
|
122
|
-
status: version.status ?? commDefinition.status,
|
|
123
|
-
},
|
|
124
|
-
};
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Fetches a CommDefinition's metadata + latest version content (same two calls, same merge, as
|
|
129
|
-
* cap-campaigns-v2's getCapNotifyAlertById saga) and reverse-maps the result via
|
|
130
|
-
* mapCommDefinitionToStepData. Returns null if the CommDefinition can't be found; content-fetch
|
|
131
|
-
* failures are swallowed (matching the saga) so the caller still gets back whatever metadata it
|
|
132
|
-
* could — the "Add content" editor just won't have pre-filled content in that case.
|
|
133
|
-
*
|
|
134
|
-
* @param {string} commDefinitionId
|
|
135
|
-
* @param {{ getCommDefinition: Function, getCommDefinitionVersion: Function }} api - the two
|
|
136
|
-
* service functions (app/services/api.js), injected so this stays testable without mocking fetch.
|
|
137
|
-
*/
|
|
138
|
-
export const fetchAndMapCommDefinition = async (commDefinitionId, { getCommDefinition, getCommDefinitionVersion }) => {
|
|
139
|
-
const response = await getCommDefinition(commDefinitionId, 'versions');
|
|
140
|
-
const commDefinition = response?.response?.data;
|
|
141
|
-
if (!commDefinition) return null;
|
|
142
|
-
|
|
143
|
-
const latestVersion = getLatestVersionFromAuditInfos(commDefinition);
|
|
144
|
-
try {
|
|
145
|
-
const versionResponse = await getCommDefinitionVersion(commDefinitionId, latestVersion);
|
|
146
|
-
const versionData = versionResponse?.response?.data;
|
|
147
|
-
if (versionData) {
|
|
148
|
-
commDefinition.version = { ...commDefinition.version, ...versionData };
|
|
149
|
-
}
|
|
150
|
-
} catch (versionError) {
|
|
151
|
-
// swallow — content just won't be available; caller still gets the CommDefinition's metadata.
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return mapCommDefinitionToStepData(commDefinition);
|
|
155
|
-
};
|