@capillarytech/creatives-library 9.0.55-alpha.1 → 9.0.55-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/package.json +1 -1
  2. package/services/api.js +2 -10
  3. package/styles/containers/layout/_layoutPage.scss +2 -1
  4. package/translations/en.json +2 -2
  5. package/v2Components/AccessForbidden/_accessForbidden.scss +22 -12
  6. package/v2Components/AccessForbidden/index.js +27 -8
  7. package/v2Components/AccessForbidden/messages.js +2 -3
  8. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +1 -8
  9. package/v2Components/NavigationBar/mfeModuleHeader.config.js +1 -1
  10. package/v2Containers/Cap/tests/__snapshots__/index.test.js.snap +2 -2
  11. package/v2Containers/CommunicationFlow/CommunicationFlow.js +4 -13
  12. package/v2Containers/CommunicationFlow/CommunicationFlow.scss +1 -1
  13. package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +38 -21
  14. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +19 -0
  15. package/v2Containers/CommunicationFlow/Tests/CommunicationFlowCard.test.js +20 -0
  16. package/v2Containers/CommunicationFlow/Tests/constants.test.js +85 -0
  17. package/v2Containers/CommunicationFlow/constants.js +8 -38
  18. package/v2Containers/CommunicationFlow/messages.js +3 -3
  19. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +0 -4
  20. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +3 -8
  21. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +5 -5
  22. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +6 -6
  23. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/index.test.js.snap +4 -4
  24. package/v2Containers/Line/Container/Wrapper/tests/__snapshots__/index.test.js.snap +32 -32
  25. package/v2Containers/Line/Container/tests/__snapshots__/index.test.js.snap +36 -36
  26. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +130 -130
  27. package/v2Containers/SmsTrai/Create/tests/__snapshots__/index.test.js.snap +8 -8
  28. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +33 -33
  29. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +302 -302
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "9.0.55-alpha.1",
4
+ "version": "9.0.55-alpha.3",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
package/services/api.js CHANGED
@@ -701,21 +701,13 @@ export const createCentralCommsMetaId = (payload, metaType = TRANSACTION) => {
701
701
  return request(url, getAPICallObject('POST', payload, false, false, false, true));
702
702
  };
703
703
 
704
- // CCS CommDefinition API (new Veyron /api/v1/commdefinition surface, proxied
705
- // through cap-creatives-api's /comm-definitions). Used by CommunicationFlow's
706
- // own Save action — separate from the legacy messageMeta functions above,
707
- // which remain in use by CreativesContainer's own save flow (Cap/sagas.js).
704
+ // CCS CommDefinition API (new Veyron /api/v1/commdefinition surface, proxied through cap-creatives-api /comm-definitions); used by CommunicationFlow's Save, while legacy messageMeta functions remain for CreativesContainer's save flow (Cap/sagas.js).
708
705
  export const createCommDefinition = (payload) => {
709
706
  const url = `${API_ENDPOINT}/comm-definitions`;
710
707
  return request(url, getAPICallObject('POST', payload, false, false, false, true));
711
708
  };
712
709
 
713
- // Opens a new DRAFT version with the given content on an EXISTING
714
- // CommDefinition (same id/referenceId) — used by CommunicationFlow's edit-mode
715
- // save (re-editing a rejected alert) instead of createCommDefinition, so the
716
- // alert keeps its identity rather than becoming a brand-new CommDefinition.
717
- // Route verified against a real Postman sample against Veyron's own
718
- // /commdefinition/{id}/edit endpoint.
710
+ // Opens a new DRAFT version with the supplied content on an existing CommDefinition (same id/referenceId); used by CommunicationFlow edit-mode saves to preserve the alert's identity instead of creating a new CommDefinition. Route verified against Veyron's /commdefinition/{id}/edit endpoint.
719
711
  export const editCommDefinition = (commDefinitionId, payload) => {
720
712
  const url = `${API_ENDPOINT}/comm-definitions/${commDefinitionId}/edit`;
721
713
  return request(url, getAPICallObject('POST', payload, false, false, false, true));
@@ -77,7 +77,8 @@
77
77
  -webkit-flex: 1 1 auto;
78
78
  -ms-flex: 1 1 auto;
79
79
  flex: 1;
80
- height: calc(100vh - 50px);
80
+ // No height: flex stretch inside `.main` sizes this. A hardcoded
81
+ // calc(100vh - 50px) under-counted the chrome and overhung it by ~83px.
81
82
  overflow: auto;
82
83
  color: #333333;
83
84
  }
@@ -63,8 +63,8 @@
63
63
  "app.v2containers.SmsTrai.Edit.traiEditTitle": "Message",
64
64
  "app.v2containers.SmsTrai.Edit.unicodeLabel": "Allow unicode characters",
65
65
  "app.v2containers.SmsTrai.Edit.unsupportedTagsValidationError": "Unsupported tags: {unsupportedTags}. Please remove them from this message.",
66
- "creatives.components.AccessForbidden.forbiddenDesc": "Please contact your organisation administrator to request access.",
67
- "creatives.components.AccessForbidden.forbiddenHeader": "You don't have permission to view this page.",
66
+ "creatives.components.AccessForbidden.forbiddenDesc": "This page access has not been provided to you.",
67
+ "creatives.components.AccessForbidden.forbiddenHeader": "Access Denied",
68
68
  "creatives.components.BreadCrumbs.header": "This is the BreadCrumbs component !",
69
69
  "creatives.components.CapTagList.Cancel": "Cancel",
70
70
  "creatives.components.CapTagList.Ok": "Ok",
@@ -1,14 +1,24 @@
1
- // Access-denied screen: rbac-lock icon + heading + subtext (matches the shared
2
- // "no access" screen used across Loyalty+ / Org Settings).
3
- .no-access-icon {
4
- font-size: 48px;
5
- color: #8c8c8c;
6
- }
7
- .no-access-title {
1
+ @import '~@capillarytech/cap-ui-library/styles/_variables';
2
+
3
+ .access-denied {
4
+ width: 100%;
5
+ // This module's ComponentWrapper has no padding, unlike the other MFEs'.
8
6
  margin-top: 16px;
9
- }
10
- .no-access-text {
11
- margin-top: 8px;
12
- color: #6b778c;
13
- max-width: 420px;
7
+ padding-top: $CAP_SPACE_80;
8
+ text-align: center;
9
+
10
+ .access-denied-illustration {
11
+ width: 20rem;
12
+ height: 20rem;
13
+ margin-bottom: $CAP_SPACE_12;
14
+ }
15
+
16
+ .access-denied-title-container {
17
+ gap: $CAP_SPACE_08;
18
+ margin-bottom: $CAP_SPACE_08;
19
+ }
20
+
21
+ .access-denied-description {
22
+ color: $CAP_G04;
23
+ }
14
24
  }
@@ -1,18 +1,37 @@
1
1
  import React from 'react';
2
2
  import { FormattedMessage } from 'react-intl';
3
- import { CapRow, CapHeading, CapIcon } from '@capillarytech/cap-ui-library';
3
+ import {
4
+ CapRow,
5
+ CapImage,
6
+ CapIcon,
7
+ CapHeading,
8
+ CapLabel,
9
+ } from '@capillarytech/cap-ui-library';
10
+ import accessDeniedIllustration from '@capillarytech/cap-ui-library/assets/images/createNewIllustration.svg';
4
11
  import messages from './messages';
5
12
  import './_accessForbidden.scss';
6
13
 
7
14
  const AccessForbidden = () => (
8
- <CapRow type='flex' vertical justify='center' align='middle' className="margin-t-32 text-align-center">
9
- <CapIcon type="rbac-lock" className="no-access-icon" />
10
- <CapHeading type="h3" className="no-access-title">
11
- <FormattedMessage {...messages.forbiddenHeader} />
12
- </CapHeading>
13
- <CapHeading type="h6" className="no-access-text">
15
+ <CapRow vertical align="middle" justify="center" className="access-denied">
16
+ <CapImage
17
+ src={accessDeniedIllustration}
18
+ className="access-denied-illustration"
19
+ alt=""
20
+ />
21
+ <CapRow
22
+ type="flex"
23
+ align="middle"
24
+ justify="center"
25
+ className="access-denied-title-container"
26
+ >
27
+ <CapIcon type="rbac-lock" size="m" />
28
+ <CapHeading type="h2">
29
+ <FormattedMessage {...messages.forbiddenHeader} />
30
+ </CapHeading>
31
+ </CapRow>
32
+ <CapLabel type="label2" className="access-denied-description">
14
33
  <FormattedMessage {...messages.forbiddenDesc} />
15
- </CapHeading>
34
+ </CapLabel>
16
35
  </CapRow>
17
36
  );
18
37
 
@@ -5,11 +5,10 @@ const prefix = 'creatives.components.AccessForbidden';
5
5
  export default defineMessages({
6
6
  forbiddenHeader: {
7
7
  id: `${prefix}.forbiddenHeader`,
8
- defaultMessage: "You don't have permission to view this page.",
8
+ defaultMessage: 'Access Denied',
9
9
  },
10
10
  forbiddenDesc: {
11
11
  id: `${prefix}.forbiddenDesc`,
12
- defaultMessage:
13
- 'Please contact your organisation administrator to request access.',
12
+ defaultMessage: 'This page access has not been provided to you.',
14
13
  },
15
14
  });
@@ -140,14 +140,7 @@ const UnifiedPreview = ({
140
140
  }
141
141
 
142
142
  case CHANNELS.LINE:
143
- // LINE's real content today is a single text message (CCS's
144
- // lineMessageContent.messageBody is a JSON string of LINE's native
145
- // messages[] array — see testAndPreviewDataTransform.js's LINE case,
146
- // which already resolves it down to plain text before it reaches
147
- // here) — reuse the same simple chat-bubble preview as SMS. Rich LINE
148
- // message types (image/video/sticker/imageMap/carousel/flex) are NOT
149
- // rendered specially yet; they'll show as empty/raw text until this
150
- // gets richer LINE-specific handling.
143
+ // LINE currently carries a single text message; CCS stores lineMessageContent.messageBody as JSON for LINE's native messages[] (see testAndPreviewDataTransform.js), already reduced to plain text before this point, so reuse the SMS chat-bubble preview. Rich LINE types (image/video/sticker/imageMap/carousel/flex) are not specially rendered yet.
151
144
  return (
152
145
  <SmsPreviewContent
153
146
  content={typeof content === 'string' ? content : (content?.resolvedBody || '')}
@@ -4,7 +4,7 @@ export const MFE_MODULE_HEADER = {
4
4
  header: {
5
5
  name: 'app.commonUtils.capUiLibrary.navigationSPA.moduleHeader.creatives.default.name',
6
6
  description: 'app.commonUtils.capUiLibrary.navigationSPA.moduleHeader.creatives.default.description',
7
- showBorder: false,
7
+ showBorder: true,
8
8
  showSettings: false,
9
9
  },
10
10
  landingRoutes: [
@@ -141,8 +141,8 @@ exports[`<Cap /> should render correct component 1`] = `
141
141
  "app.v2containers.SmsTrai.Edit.traiEditTitle": "Message",
142
142
  "app.v2containers.SmsTrai.Edit.unicodeLabel": "Allow unicode characters",
143
143
  "app.v2containers.SmsTrai.Edit.unsupportedTagsValidationError": "Unsupported tags: {unsupportedTags}. Please remove them from this message.",
144
- "creatives.components.AccessForbidden.forbiddenDesc": "Please contact your organisation administrator to request access.",
145
- "creatives.components.AccessForbidden.forbiddenHeader": "You don't have permission to view this page.",
144
+ "creatives.components.AccessForbidden.forbiddenDesc": "This page access has not been provided to you.",
145
+ "creatives.components.AccessForbidden.forbiddenHeader": "Access Denied",
146
146
  "creatives.components.BreadCrumbs.header": "This is the BreadCrumbs component !",
147
147
  "creatives.components.CapTagList.Cancel": "Cancel",
148
148
  "creatives.components.CapTagList.Ok": "Ok",
@@ -67,15 +67,11 @@ const hasDeliverySettingForChannel = (channelSetting, channel) => {
67
67
  return !!settings && Object.values(settings).some((v) => v !== null && v !== '' && v !== undefined);
68
68
  };
69
69
 
70
- // createCommDefinition resolves (rather than rejects) with the CCS error envelope
71
- // for 4xx/5xx responses — see api.js's request()/checkStatus. A duplicate
72
- // referenceId within the org comes back as this specific 409.
70
+ // createCommDefinition resolves with the CCS error envelope for 4xx/5xx responses (see api.js request()/checkStatus); duplicate referenceId within the org returns a 409.
73
71
  const isDuplicateReferenceIdError = (res) => res?.success === false && res?.status?.message === 'REFERENCE_ID_EXISTS';
74
72
 
75
73
  /**
76
- * CCS requires referenceId on create; the consumer-supplied Alert/comm name is
77
- * mandatory but referenceId is optional in every consumer's own form. Generate
78
- * a stable fallback from the name rather than failing the save.
74
+ * CCS requires referenceId on create, while consumers require only the Alert/comm name; generate a stable fallback from the name when referenceId is omitted instead of failing the save.
79
75
  */
80
76
  const buildCcsReferenceId = (name) => {
81
77
  const slug = (name || 'COMM')
@@ -215,19 +211,14 @@ const CommunicationFlow = ({
215
211
  singleChannelStrategy: {
216
212
  variant: {
217
213
  channel,
218
- // `channel` spreads LAST in both objects below contentItem.templateData
219
- // (e.g. mobile push) carries the UI's own internal channel identifier
220
- // (MOBILEPUSH), which must never win over CCS's normalized enum value
221
- // (PUSH) once spread order puts it after.
214
+ // `channel` must be spread last in both objects so CCS's normalized enum (e.g. `PUSH`) overrides the UI's internal identifier (e.g. `MOBILEPUSH`) from templateData.
222
215
  ...(contentKey && { [contentKey]: { ...contentPayload, channel } }),
223
216
  ...(deliveryKey && { [deliveryKey]: { channelSettings: { ...channelSettings, channel } } }),
224
217
  },
225
218
  },
226
219
  };
227
220
 
228
- // Editing an existing (e.g. rejected) alert: config.context.existingCommDefinitionId
229
- // opens a new DRAFT version on that SAME CommDefinition instead of creating a
230
- // brand-new one, so the alert keeps its id/referenceId across the edit.
221
+ // Editing an existing alert (e.g. rejected) uses config.context.existingCommDefinitionId to create a new DRAFT version on the same CommDefinition, preserving its id/referenceId instead of creating a new alert.
231
222
  const { existingCommDefinitionId } = config?.context || {};
232
223
 
233
224
  try {
@@ -137,7 +137,7 @@
137
137
  }
138
138
 
139
139
  &__footer {
140
- padding: 4.142rem 0 $CAP_SPACE_08;
140
+ padding: 3.142rem 0 $CAP_SPACE_08;
141
141
  }
142
142
  }
143
143
 
@@ -111,6 +111,11 @@ const CommunicationFlowCard = ({
111
111
  const senderIdValue = getSenderIdValue(firstItem, savedData);
112
112
  const controls = config?.features?.dynamicControlsData?.controls || DYNAMIC_CONTROLS_CONFIG;
113
113
  const dynamicControlKeys = Object.keys(savedData?.dynamicControls || {});
114
+ // Mirrors getEnabledSteps.js's own gate on this flag (which skips the in-slidebox
115
+ // DynamicControlsStep) — consumers like CapNotify that set showDynamicControls:
116
+ // false render their own "Advanced controls" section elsewhere, so this summary
117
+ // card's right column would otherwise show a redundant, always-empty control list.
118
+ const showDynamicControls = config?.features?.showDynamicControls !== false;
114
119
  const channel = firstItem.channel?.toUpperCase();
115
120
  const senderLabel = [WHATSAPP, RCS].includes(channel)
116
121
  ? formatMessage(messages.senderNumberLabel)
@@ -124,7 +129,11 @@ const CommunicationFlowCard = ({
124
129
  <CapLabel type="label8">Message:</CapLabel>
125
130
  <CapLabel type="label2">{strategyLabel}</CapLabel>
126
131
  {messageTypeLabel && (
127
- <CapLabel type="label1">({messageTypeLabel})</CapLabel>
132
+ <CapLabel type="label1">
133
+ (
134
+ {messageTypeLabel}
135
+ )
136
+ </CapLabel>
128
137
  )}
129
138
  </CapRow>
130
139
  )}
@@ -139,33 +148,41 @@ const CommunicationFlowCard = ({
139
148
  bodyStyle={{ padding: 0 }}
140
149
  >
141
150
  <CapRow gutter={0} className="card-body-row">
142
- <CapColumn span={12} className="card-body-left-col">
151
+ <CapColumn span={showDynamicControls ? 12 : 24} className="card-body-left-col">
143
152
  <CapRow type="flex" align="middle" className="channel-header-row">
144
153
  <CapIcon type={channelConfig?.iconType || 'sms'} size="s" />
145
154
  <CapLabel type="label8">{channelConfig?.label || firstItem.channel}</CapLabel>
146
155
  {senderIdValue && (
147
- <CapLabel type="label1">({senderLabel}: {senderIdValue})</CapLabel>
156
+ <CapLabel type="label1">
157
+ (
158
+ {senderLabel}
159
+ :
160
+ {senderIdValue}
161
+ )
162
+ </CapLabel>
148
163
  )}
149
164
  </CapRow>
150
165
  {getContentBody(firstItem)}
151
166
  </CapColumn>
152
- <CapColumn span={12} className="card-body-right-col">
153
- <CapLabel type="label8">{formatMessage(messages.dynamicControlsTitle)}</CapLabel>
154
- {dynamicControlKeys.map((key) => {
155
- const controlConfig = controls.find((c) => c.key === key);
156
- if (!controlConfig) return null;
157
- return (
158
- <CapRow key={key} type="flex" justify="space-between" className="control-row">
159
- <CapLabel type="label2">{controlConfig.label}</CapLabel>
160
- <CapLabel type="label2">
161
- {savedData.dynamicControls[key]
162
- ? formatMessage(messages.yes)
163
- : formatMessage(messages.no)}
164
- </CapLabel>
165
- </CapRow>
166
- );
167
- })}
168
- </CapColumn>
167
+ {showDynamicControls && (
168
+ <CapColumn span={12} className="card-body-right-col">
169
+ <CapLabel type="label8">{formatMessage(messages.dynamicControlsTitle)}</CapLabel>
170
+ {dynamicControlKeys.map((key) => {
171
+ const controlConfig = controls.find((c) => c.key === key);
172
+ if (!controlConfig) return null;
173
+ return (
174
+ <CapRow key={key} type="flex" justify="space-between" className="control-row">
175
+ <CapLabel type="label2">{controlConfig.label}</CapLabel>
176
+ <CapLabel type="label2">
177
+ {savedData.dynamicControls[key]
178
+ ? formatMessage(messages.yes)
179
+ : formatMessage(messages.no)}
180
+ </CapLabel>
181
+ </CapRow>
182
+ );
183
+ })}
184
+ </CapColumn>
185
+ )}
169
186
  </CapRow>
170
187
  </CapCard>
171
188
  );
@@ -211,7 +228,7 @@ const CommunicationFlowCard = ({
211
228
  size="size-l"
212
229
  header={(
213
230
  <CapHeader
214
- title={formatMessage(messages.addMessage)}
231
+ title={formatMessage(messages.addContent)}
215
232
  handleClose={handleClose}
216
233
  />
217
234
  )}
@@ -1000,3 +1000,22 @@ describe('optional chaining safety', () => {
1000
1000
  expect(screen.getByRole('button', { name: /^save$/i })).toBeDisabled();
1001
1001
  });
1002
1002
  });
1003
+
1004
+ describe('step dividers', () => {
1005
+ it('renders exactly one divider when the strategy and channel-selection steps render together', () => {
1006
+ const { container } = renderWithFlow({
1007
+ features: {
1008
+ communicationStrategyData: { required: true, options: STRATEGY_OPTIONS },
1009
+ contentTemplateData: { required: true, channels: CHANNELS },
1010
+ },
1011
+ initialData: {
1012
+ communicationStrategy: SINGLE_TEMPLATE,
1013
+ contentItems: [{ channel: 'SMS', templateData: {} }],
1014
+ },
1015
+ });
1016
+ // Only ChannelSelectionStep's own divider (after the content-preview card) should
1017
+ // render — CommunicationStrategyStep no longer emits one of its own, otherwise two
1018
+ // dividers would stack back-to-back with nothing between them.
1019
+ expect(container.querySelectorAll('.ant-divider')).toHaveLength(1);
1020
+ });
1021
+ });
@@ -202,6 +202,26 @@ describe('CommunicationFlowCard', () => {
202
202
  renderCard({ initialData: makeSavedData() });
203
203
  expect(screen.getByText('Other controls')).toBeInTheDocument();
204
204
  });
205
+
206
+ it('hides the Other controls column when config.features.showDynamicControls is false', () => {
207
+ renderCard({
208
+ config: { ...baseConfig, features: { showDynamicControls: false } },
209
+ initialData: makeSavedData({
210
+ dynamicControls: {
211
+ sendToControlCustomers: false, sendToBrandPocs: false, useTinyUrl: false, overrideDailyLimit: false,
212
+ },
213
+ }),
214
+ });
215
+ expect(screen.queryByText('Other controls')).not.toBeInTheDocument();
216
+ });
217
+
218
+ it('still shows the Other controls column when showDynamicControls is unset (Engagement Module default)', () => {
219
+ renderCard({
220
+ config: { ...baseConfig, features: {} },
221
+ initialData: makeSavedData(),
222
+ });
223
+ expect(screen.getByText('Other controls')).toBeInTheDocument();
224
+ });
205
225
  });
206
226
 
207
227
  describe('channel config resolution', () => {
@@ -0,0 +1,85 @@
1
+ import {
2
+ CCS_CONTENT_TRANSFORMS,
3
+ CCS_CHANNEL_NAME_MAP,
4
+ CCS_CHANNEL_CONTENT_KEY_MAP,
5
+ CCS_CHANNEL_DELIVERY_KEY_MAP,
6
+ } from '../constants';
7
+
8
+ describe('CCS_CONTENT_TRANSFORMS', () => {
9
+ describe('EMAIL', () => {
10
+ it('prefers emailBody over emailHtml when both are present', () => {
11
+ expect(
12
+ CCS_CONTENT_TRANSFORMS.EMAIL({
13
+ emailSubject: 'Subject', emailBody: '<p>Body</p>', emailHtml: '<p>Html</p>',
14
+ }),
15
+ ).toEqual({ messageSubject: 'Subject', messageBody: '<p>Body</p>' });
16
+ });
17
+
18
+ it('falls back to emailHtml when emailBody is absent', () => {
19
+ expect(
20
+ CCS_CONTENT_TRANSFORMS.EMAIL({ emailSubject: 'Subject', emailHtml: '<p>Html</p>' }),
21
+ ).toEqual({ messageSubject: 'Subject', messageBody: '<p>Html</p>' });
22
+ });
23
+
24
+ it('defaults templateData to {} when called with no argument', () => {
25
+ expect(CCS_CONTENT_TRANSFORMS.EMAIL()).toEqual({
26
+ messageSubject: undefined, messageBody: undefined,
27
+ });
28
+ });
29
+ });
30
+
31
+ describe('SMS', () => {
32
+ it('uses messageBody when present', () => {
33
+ expect(CCS_CONTENT_TRANSFORMS.SMS({ messageBody: 'Hello' })).toEqual({ message: 'Hello' });
34
+ });
35
+
36
+ it('falls back to an empty string when messageBody is absent', () => {
37
+ expect(CCS_CONTENT_TRANSFORMS.SMS({})).toEqual({ message: '' });
38
+ });
39
+
40
+ it('defaults templateData to {} when called with no argument', () => {
41
+ expect(CCS_CONTENT_TRANSFORMS.SMS()).toEqual({ message: '' });
42
+ });
43
+ });
44
+
45
+ describe('WEBPUSH', () => {
46
+ it('extracts fields from the nested messageContent.content wrapper', () => {
47
+ expect(
48
+ CCS_CONTENT_TRANSFORMS.WEBPUSH({
49
+ messageContent: {
50
+ content: {
51
+ messageSubject: 'Subject', accountId: 'acc_1', content: 'Body', offers: [{ id: 1 }],
52
+ },
53
+ },
54
+ }),
55
+ ).toEqual({ messageSubject: 'Subject', accountId: 'acc_1', content: 'Body' });
56
+ });
57
+
58
+ it('falls back to the flat templateData when messageContent.content is absent', () => {
59
+ expect(
60
+ CCS_CONTENT_TRANSFORMS.WEBPUSH({
61
+ messageSubject: 'Subject', accountId: 'acc_1', content: 'Body',
62
+ }),
63
+ ).toEqual({ messageSubject: 'Subject', accountId: 'acc_1', content: 'Body' });
64
+ });
65
+
66
+ it('defaults templateData to {} when called with no argument', () => {
67
+ expect(CCS_CONTENT_TRANSFORMS.WEBPUSH()).toEqual({
68
+ messageSubject: undefined, accountId: undefined, content: undefined,
69
+ });
70
+ });
71
+ });
72
+ });
73
+
74
+ describe('CCS_CHANNEL_NAME_MAP', () => {
75
+ it('normalizes the UI\'s MOBILEPUSH identifier to CCS\'s PUSH enum value', () => {
76
+ expect(CCS_CHANNEL_NAME_MAP.MOBILEPUSH).toBe('PUSH');
77
+ });
78
+ });
79
+
80
+ describe('CCS_CHANNEL_CONTENT_KEY_MAP / CCS_CHANNEL_DELIVERY_KEY_MAP', () => {
81
+ it('maps the PUSH channel to its mpush-prefixed content and delivery keys', () => {
82
+ expect(CCS_CHANNEL_CONTENT_KEY_MAP.PUSH).toBe('mpushMessageContent');
83
+ expect(CCS_CHANNEL_DELIVERY_KEY_MAP.PUSH).toBe('mpushDeliverySettings');
84
+ });
85
+ });
@@ -38,21 +38,15 @@ export const CHANNEL_PRIORITY = 'CHANNEL_PRIORITY';
38
38
  export const AB_TEST = 'AB_TEST';
39
39
  export const SINGLE_TEMPLATE = 'SINGLE_TEMPLATE';
40
40
 
41
- // CCS CommDefinition strategyType — distinct from SINGLE_TEMPLATE above, which
42
- // is this UI's own communicationStrategy value. Only SINGLE is supported this
43
- // phase; CCS create is skipped entirely for CHANNEL_PRIORITY/AB_TEST.
41
+ // CCS CommDefinition strategyType — distinct from SINGLE_TEMPLATE above, which is this UI's own communicationStrategy value. Only SINGLE is supported this phase; CCS create is skipped entirely for CHANNEL_PRIORITY/AB_TEST.
44
42
  export const CCS_STRATEGY_TYPE_SINGLE = 'SINGLE';
45
43
 
46
- // Mirrors CCS's SingleChannelStrategy field names exactly (Cap Notify API
47
- // Design doc sample payloads, verified against real create responses). Kept
48
- // separate from CHANNEL_CONTENT_KEY_MAP/CHANNEL_DELIVERY_KEY_MAP below, which
49
- // are the legacy messageMeta payload's keys.
44
+ // Mirrors CCS SingleChannelStrategy field names exactly (per Cap Notify API design samples and real create responses); kept separate from the legacy messageMeta CHANNEL_CONTENT_KEY_MAP/CHANNEL_DELIVERY_KEY_MAP.
50
45
  export const CCS_CHANNEL_CONTENT_KEY_MAP = {
51
46
  SMS: 'smsMessageContent',
52
47
  EMAIL: 'emailMessageContent',
53
48
  WHATSAPP: 'whatsappMessageContent',
54
- // CCS's channel enum value for mobile push is 'PUSH' (verified against real
55
- // create responses), even though the content/delivery keys stay 'mpush*'.
49
+ // CCS's channel enum value for mobile push is 'PUSH' (verified against real create responses), even though the content/delivery keys stay 'mpush*'.
56
50
  PUSH: 'mpushMessageContent',
57
51
  INAPP: 'inAppMessageContent',
58
52
  ANDROID: 'androidMessageContent',
@@ -64,34 +58,17 @@ export const CCS_CHANNEL_CONTENT_KEY_MAP = {
64
58
  RCS: 'rcsMessageContent',
65
59
  };
66
60
 
67
- // contentItem.templateData is built by the legacy CreativesContainer content
68
- // editor (messageMeta-oriented field names) — for channels whose legacy field
69
- // names don't match CCS's own content schema, transform before sending rather
70
- // than dumping templateData as-is. EMAIL: legacy stores the subject as
71
- // `emailSubject` and the HTML body as `emailBody` (see
72
- // extractContentForPreview.js and CreativesContainer/index.js, where the Bee
73
- // editor's output is written back into templateData.emailBody — also
74
- // mirrored by the existing legacy emailMessageContent builder in
75
- // utils/transformerUtils.js). CCS expects `messageSubject`/`messageBody`.
61
+ // contentItem.templateData uses legacy messageMeta field names, so transform mismatched channel fields before sending to CCS; for EMAIL, map legacy `emailSubject`/`emailBody` to CCS's `messageSubject`/`messageBody`.
76
62
  export const CCS_CONTENT_TRANSFORMS = {
77
63
  EMAIL: (templateData = {}) => ({
78
64
  messageSubject: templateData.emailSubject,
79
65
  messageBody: templateData.emailBody || templateData.emailHtml,
80
66
  }),
81
- // SMS: legacy stores the typed text as `messageBody` (see
82
- // CreativesContainer/index.js's getCreativesData SMS case, and the existing
83
- // legacy smsMessageContent builder in utils/transformerUtils.js, both of
84
- // which read/write this same field) — CCS's schema expects it as `message`
85
- // (confirmed against a real CCS response showing smsMessageContent.message
86
- // stayed null when messageBody was sent instead).
67
+ // SMS: legacy stores text as `messageBody`, while CCS expects `message`; map the field explicitly to avoid CCS returning smsMessageContent.message as null.
87
68
  SMS: (templateData = {}) => ({
88
69
  message: templateData.messageBody || '',
89
70
  }),
90
- // WEBPUSH: the legacy editor nests the real data under
91
- // messageContent.content (see CommunicationFlowCard.js's own comment on
92
- // this same wrapping, for the edit-mode case) — extracting it flat here
93
- // matches CCS's shape ({ messageSubject, accountId, content }); `offers`
94
- // (a legacy-only field) is deliberately dropped, not part of CCS's schema.
71
+ // WEBPUSH: the legacy editor nests data under messageContent.content; flatten it to CCS's { messageSubject, accountId, content } shape and drop the legacy-only `offers` field.
95
72
  WEBPUSH: (templateData = {}) => {
96
73
  const inner = templateData.messageContent?.content || templateData;
97
74
  return {
@@ -117,19 +94,12 @@ export const CCS_CHANNEL_DELIVERY_KEY_MAP = {
117
94
  RCS: 'rcsDeliverySettings',
118
95
  };
119
96
 
120
- // The UI's own internal channel identifier for mobile push is 'MOBILEPUSH'
121
- // (see CreativesContainer/constants.js MOBILE_PUSH), but CCS's channel enum
122
- // value is 'PUSH' (verified against real create responses — the content/
123
- // delivery KEYS still say 'mpush*', only the enum VALUE is 'PUSH') —
124
- // normalize before building the CCS payload. Every other channel identifier
125
- // matches CCS as-is.
97
+ // The UI uses `MOBILEPUSH`, while CCS uses `PUSH` for the channel enum (verified against real create responses); normalize this value before building the CCS payload, while retaining the `mpush*` content/delivery keys. All other channel identifiers pass through unchanged.
126
98
  export const CCS_CHANNEL_NAME_MAP = {
127
99
  MOBILEPUSH: 'PUSH',
128
100
  };
129
101
 
130
- // Channel config - single source of truth for CreativesContainer and TemplatesV2
131
- // paneKey: TemplatesV2 defaultPanes object key (for channelsToHide)
132
- // channelProp: CreativesContainer channel prop (must match pane.key for tab to be active)
102
+ // Channel config single source of truth for CreativesContainer and TemplatesV2; paneKey is the TemplatesV2 defaultPanes key used by channelsToHide, and channelProp is the CreativesContainer channel prop that must match pane.key for the tab to be active.
133
103
  export const CHANNELS = [
134
104
  {
135
105
  value: 'SMS',
@@ -64,9 +64,9 @@ export default {
64
64
  },
65
65
 
66
66
  // Slidebox header
67
- addMessage: {
68
- id: `${prefix}.addMessage`,
69
- defaultMessage: 'Add message',
67
+ addContent: {
68
+ id: `${prefix}.addContent`,
69
+ defaultMessage: 'Add content',
70
70
  },
71
71
 
72
72
  // Step prompts
@@ -407,10 +407,6 @@ const ChannelSelectionStep = ({
407
407
  return (
408
408
  <>
409
409
  <CapRow className="channel-selection-step">
410
- <CapHeading type="h3" className="margin-b-8">
411
- {formatMessage(messages.contentTemplate)}
412
- </CapHeading>
413
-
414
410
  <CapRow
415
411
  useLegacy
416
412
  className={`content-template-container ${contentItems?.length > 0 ? 'content-template-container--has-content' : ''}`}
@@ -181,9 +181,7 @@ const getEmailDomainValue = (entity) => {
181
181
  return emailData.domainProperties?.id ?? emailData.domainId ?? emailData.id ?? '';
182
182
  };
183
183
 
184
- // CCS's domainGatewayMapId is the domainProperties API's own outer `id`
185
- // (distinct from `domainProperties.id`, which is the numeric domainId) - the
186
- // domain-gateway-mapping id for the selected domain entry.
184
+ // CCS's domainGatewayMapId is the domainProperties API's outer `id`, distinct from domainProperties.id (the numeric domainId); it identifies the domain-gateway mapping for the selected domain.
187
185
  const getEmailDomainGatewayMapIdByDomainId = (entity, domainId) => {
188
186
  const domainEntry = getEmailDomainById(entity, domainId);
189
187
  return domainEntry?.id ?? '';
@@ -584,8 +582,7 @@ export const parseEntityForDisplay = (entity, context = {}) => {
584
582
  senderDetails: entity,
585
583
  smsSenderId: getSmsSenderIdValue(entity),
586
584
  smsDomain: getSmsDomainValue(entity),
587
- // emailDomain here is the domain NAME (for display only) - buildChannelSettingFromFieldValues
588
- // needs the numeric domainId/domainGatewayMapId, sourced separately below.
585
+ // emailDomain here is the domain NAME (for display only) - buildChannelSettingFromFieldValues needs the numeric domainId/domainGatewayMapId, sourced separately below.
589
586
  emailDomain: getEmailDomainNameForDisplay(entity),
590
587
  emailDomainId: getEmailDomainValue(entity),
591
588
  emailDomainGatewayMapId: getEmailDomainGatewayMapIdByDomainId(entity, getEmailDomainValue(entity)),
@@ -614,9 +611,7 @@ export const buildChannelSettingFromFieldValues = (fieldValues = {}) => {
614
611
  }
615
612
  if (fieldValues.emailDomain != null || fieldValues.emailSenderId != null || fieldValues.emailSenderName != null || fieldValues.emailReplyToId != null) {
616
613
  channelSetting.EMAIL = {
617
- // emailDomainId (numeric, from the auto-save-defaults path via parseEntityForDisplay)
618
- // takes priority over emailDomain, which is numeric when sourced from SenderDetails'
619
- // own field state but a display NAME string when sourced from parseEntityForDisplay.
614
+ // emailDomainId (numeric, from auto-save defaults via parseEntityForDisplay) takes precedence over emailDomain, which may be a numeric ID in SenderDetails state or a display name from parseEntityForDisplay.
620
615
  domainId: fieldValues.emailDomainId ?? fieldValues.emailDomain,
621
616
  domainGatewayMapId: fieldValues.emailDomainGatewayMapId,
622
617
  senderId: fieldValues.emailSenderId,