@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.
- package/package.json +1 -1
- package/services/api.js +2 -10
- package/styles/containers/layout/_layoutPage.scss +2 -1
- package/translations/en.json +2 -2
- package/v2Components/AccessForbidden/_accessForbidden.scss +22 -12
- package/v2Components/AccessForbidden/index.js +27 -8
- package/v2Components/AccessForbidden/messages.js +2 -3
- package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +1 -8
- package/v2Components/NavigationBar/mfeModuleHeader.config.js +1 -1
- package/v2Containers/Cap/tests/__snapshots__/index.test.js.snap +2 -2
- package/v2Containers/CommunicationFlow/CommunicationFlow.js +4 -13
- package/v2Containers/CommunicationFlow/CommunicationFlow.scss +1 -1
- package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +38 -21
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +19 -0
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlowCard.test.js +20 -0
- package/v2Containers/CommunicationFlow/Tests/constants.test.js +85 -0
- package/v2Containers/CommunicationFlow/constants.js +8 -38
- package/v2Containers/CommunicationFlow/messages.js +3 -3
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +0 -4
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +3 -8
- package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +5 -5
- package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +6 -6
- package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/index.test.js.snap +4 -4
- package/v2Containers/Line/Container/Wrapper/tests/__snapshots__/index.test.js.snap +32 -32
- package/v2Containers/Line/Container/tests/__snapshots__/index.test.js.snap +36 -36
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +130 -130
- package/v2Containers/SmsTrai/Create/tests/__snapshots__/index.test.js.snap +8 -8
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +33 -33
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +302 -302
package/package.json
CHANGED
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
|
|
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:
|
|
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
|
}
|
package/translations/en.json
CHANGED
|
@@ -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": "
|
|
67
|
-
"creatives.components.AccessForbidden.forbiddenHeader": "
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
.
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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 {
|
|
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
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
</
|
|
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:
|
|
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
|
|
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:
|
|
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": "
|
|
145
|
-
"creatives.components.AccessForbidden.forbiddenHeader": "
|
|
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 (
|
|
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
|
|
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`
|
|
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)
|
|
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 {
|
|
@@ -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">
|
|
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">
|
|
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
|
-
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
<
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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',
|
|
@@ -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
|
|
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
|
|
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,
|