@capillarytech/creatives-library 9.0.59-alpha.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "9.0.59-alpha.1",
4
+ "version": "9.0.59-alpha.4",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
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, getCommDefinition, getCommDefinitionVersion } from '../../services/api';
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
- // Pre-set to CCS's only supported strategy so the Channel Selection step (which only
97
- // renders once communicationStrategy is set) is already mounted — showing its own
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>
@@ -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 && isContentLoading ? (
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 !== prevTags
114
- || injectedTags !== prevInjectedTags
115
- || selectedOfferDetails !== prevSelectedOfferDetails
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 || templateData]);
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={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={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={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
- {/* Test and Preview Slidebox */}
1582
- <TestAndPreviewSlidebox
1583
- show={showTestAndPreviewSlidebox || propsShowTestAndPreviewSlidebox}
1584
- onClose={handleCloseTestAndPreview}
1585
- channel={VIBER}
1586
- formData={getTemplateContent()} // Pass templateContent as formData (contains all payload fields)
1587
- content={getTemplateContent()} // Also pass as content for preview (contains viberPreviewContent)
1588
- formatMessage={formatMessage}
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
- };