@capillarytech/creatives-library 9.0.52 → 9.0.53-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/constants/unified.js +0 -1
- package/package.json +1 -1
- package/services/api.js +9 -0
- package/utils/common.js +0 -4
- package/v2Containers/CommunicationFlow/CommunicationFlow.js +89 -58
- package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +20 -4
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +174 -51
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlowCard.test.js +16 -0
- package/v2Containers/CommunicationFlow/constants.js +47 -0
- package/v2Containers/CommunicationFlow/index.js +15 -20
- package/v2Containers/CommunicationFlow/messages.js +4 -0
package/constants/unified.js
CHANGED
|
@@ -63,7 +63,6 @@ export const ENABLE_AI_SUGGESTIONS = 'ENABLE_AI_SUGGESTIONS';
|
|
|
63
63
|
export const AI_CONTENT_BOT_DISABLED = 'AI_CONTENT_BOT_DISABLED';
|
|
64
64
|
export const AI_DOCUMENTATION_BOT_ENABLED = 'AI_DOCUMENTATION_BOT_ENABLED';
|
|
65
65
|
export const ENABLE_PRODUCT_SUPPORT_VIDEOS = 'ENABLE_PRODUCT_SUPPORT_VIDEOS';
|
|
66
|
-
export const SUPPORT_ENGAGEMENT_MODULE = 'SUPPORT_ENGAGEMENT_MODULE';
|
|
67
66
|
export const EMBEDDED = 'embedded';
|
|
68
67
|
// --- Tag/Validation Constants ---
|
|
69
68
|
export const CARD_RELATED_TAGS = [
|
package/package.json
CHANGED
package/services/api.js
CHANGED
|
@@ -701,6 +701,15 @@ 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).
|
|
708
|
+
export const createCommDefinition = (payload) => {
|
|
709
|
+
const url = `${API_ENDPOINT}/comm-definitions`;
|
|
710
|
+
return request(url, getAPICallObject('POST', payload, false, false, false, true));
|
|
711
|
+
};
|
|
712
|
+
|
|
704
713
|
export const getCentralCommsMetaIds = (metaIds, metaType = TRANSACTION) => {
|
|
705
714
|
const url = `${API_ENDPOINT}/common/central-comms/meta-id/${metaType}?metaIds=${metaIds}`;
|
|
706
715
|
return request(url, getAPICallObject('GET', null, false, false, false, true));
|
package/utils/common.js
CHANGED
|
@@ -99,10 +99,6 @@ export const hasSupportCKEditor = Auth.hasFeatureAccess.bind(
|
|
|
99
99
|
null,
|
|
100
100
|
SUPPORT_CK_EDITOR,
|
|
101
101
|
);
|
|
102
|
-
export const hasSupportEngagementModule = Auth.hasFeatureAccess.bind(
|
|
103
|
-
null,
|
|
104
|
-
SUPPORT_ENGAGEMENT_MODULE,
|
|
105
|
-
);
|
|
106
102
|
|
|
107
103
|
export const hasGiftVoucherFeature = Auth.hasFeatureAccess.bind(
|
|
108
104
|
null,
|
|
@@ -17,10 +17,11 @@ import { createStructuredSelector } from 'reselect';
|
|
|
17
17
|
import CapRow from '@capillarytech/cap-ui-library/CapRow';
|
|
18
18
|
import CapDivider from '@capillarytech/cap-ui-library/CapDivider';
|
|
19
19
|
import CapButton from '@capillarytech/cap-ui-library/CapButton';
|
|
20
|
+
import CapLabel from '@capillarytech/cap-ui-library/CapLabel';
|
|
20
21
|
// import injectSaga from '../../utils/injectSaga'; // cap-coupons flows disabled
|
|
21
22
|
// import injectReducer from '../../utils/injectReducer';
|
|
22
23
|
import { makeSelectAuthenticated } from '../Cap/selectors';
|
|
23
|
-
import {
|
|
24
|
+
import { createCommDefinition } from '../../services/api';
|
|
24
25
|
import DynamicControlsStep from './steps/DynamicControlsStep';
|
|
25
26
|
import MessageTypeStep from './steps/MessageTypeStep';
|
|
26
27
|
import CommunicationStrategyStep from './steps/CommunicationStrategyStep';
|
|
@@ -35,9 +36,10 @@ import {
|
|
|
35
36
|
CHANNELS,
|
|
36
37
|
INCENTIVE_TYPES,
|
|
37
38
|
DYNAMIC_CONTROLS_CONFIG,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
CCS_STRATEGY_TYPE_SINGLE,
|
|
40
|
+
CCS_CHANNEL_CONTENT_KEY_MAP,
|
|
41
|
+
CCS_CHANNEL_DELIVERY_KEY_MAP,
|
|
42
|
+
CCS_CHANNEL_NAME_MAP,
|
|
41
43
|
} from './constants';
|
|
42
44
|
import { getEnabledSteps } from './utils/getEnabledSteps';
|
|
43
45
|
import messages from './messages';
|
|
@@ -64,6 +66,25 @@ const hasDeliverySettingForChannel = (channelSetting, channel) => {
|
|
|
64
66
|
return !!settings && Object.values(settings).some((v) => v !== null && v !== '' && v !== undefined);
|
|
65
67
|
};
|
|
66
68
|
|
|
69
|
+
// createCommDefinition resolves (rather than rejects) with the CCS error envelope
|
|
70
|
+
// for 4xx/5xx responses — see api.js's request()/checkStatus. A duplicate
|
|
71
|
+
// referenceId within the org comes back as this specific 409.
|
|
72
|
+
const isDuplicateReferenceIdError = (res) => res?.success === false && res?.status?.message === 'REFERENCE_ID_EXISTS';
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* CCS requires referenceId on create; the consumer-supplied Alert/comm name is
|
|
76
|
+
* mandatory but referenceId is optional in every consumer's own form. Generate
|
|
77
|
+
* a stable fallback from the name rather than failing the save.
|
|
78
|
+
*/
|
|
79
|
+
const buildCcsReferenceId = (name) => {
|
|
80
|
+
const slug = (name || 'COMM')
|
|
81
|
+
.trim()
|
|
82
|
+
.toUpperCase()
|
|
83
|
+
.replace(/[^A-Z0-9]+/g, '_')
|
|
84
|
+
.replace(/^_+|_+$/g, '') || 'COMM';
|
|
85
|
+
return `${slug}_${Date.now()}`;
|
|
86
|
+
};
|
|
87
|
+
|
|
67
88
|
const CommunicationFlow = ({
|
|
68
89
|
config,
|
|
69
90
|
initialData,
|
|
@@ -91,6 +112,7 @@ const CommunicationFlow = ({
|
|
|
91
112
|
};
|
|
92
113
|
});
|
|
93
114
|
const [validationErrors, setValidationErrors] = useState({});
|
|
115
|
+
const [saveError, setSaveError] = useState(null);
|
|
94
116
|
|
|
95
117
|
// Memoize enabled steps
|
|
96
118
|
const enabledSteps = useMemo(() => getEnabledSteps(config), [config]);
|
|
@@ -150,74 +172,78 @@ const CommunicationFlow = ({
|
|
|
150
172
|
}, []);
|
|
151
173
|
|
|
152
174
|
const handleSave = useCallback(async () => {
|
|
175
|
+
setSaveError(null);
|
|
153
176
|
const aggregatedData = getAggregatedData();
|
|
154
177
|
const shouldUseCCS = config?.useCCS !== false;
|
|
178
|
+
let ccsCommDefinition = null;
|
|
155
179
|
|
|
156
180
|
if (shouldUseCCS) {
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
|
|
181
|
+
const isMultiChannel = [CHANNEL_PRIORITY, AB_TEST].includes(aggregatedData.communicationStrategy);
|
|
182
|
+
const contentItem = (aggregatedData.contentItems || [])[0];
|
|
183
|
+
// Consumer-supplied name (e.g. CapNotify's Alert Name field) — CommunicationFlow
|
|
184
|
+
// has no name input of its own, so this is mandatory input from config.context.
|
|
185
|
+
const name = config?.context?.name;
|
|
160
186
|
|
|
161
|
-
|
|
162
|
-
|
|
187
|
+
// CCS create is SINGLE-strategy only this phase (D1); CHANNEL_PRIORITY/AB_TEST
|
|
188
|
+
// carry multiple content items with no CCS equivalent yet.
|
|
189
|
+
if (!isMultiChannel && contentItem && name) {
|
|
190
|
+
const rawChannel = (contentItem.channel || '').toUpperCase();
|
|
191
|
+
const channel = CCS_CHANNEL_NAME_MAP[rawChannel] || rawChannel;
|
|
192
|
+
const contentKey = CCS_CHANNEL_CONTENT_KEY_MAP[channel];
|
|
193
|
+
const deliveryKey = CCS_CHANNEL_DELIVERY_KEY_MAP[channel];
|
|
194
|
+
const { dynamicControls = {} } = aggregatedData;
|
|
195
|
+
const channelSettings = aggregatedData.deliverySetting?.channelSetting?.[channel] || {};
|
|
196
|
+
const referenceId = config?.context?.referenceId || buildCcsReferenceId(name);
|
|
163
197
|
|
|
164
|
-
|
|
165
|
-
|
|
198
|
+
const payload = {
|
|
199
|
+
referenceId,
|
|
200
|
+
name,
|
|
201
|
+
description: config?.context?.description || undefined,
|
|
202
|
+
strategyType: CCS_STRATEGY_TYPE_SINGLE,
|
|
203
|
+
settings: {
|
|
204
|
+
additionalSettings: {
|
|
205
|
+
useTinyUrl: dynamicControls.useTinyUrl ?? false,
|
|
206
|
+
encryptUrl: dynamicControls.sendToControlCustomers ?? false,
|
|
207
|
+
linkTrackingEnabled: dynamicControls.overrideDailyLimit ?? false,
|
|
208
|
+
userSubscriptionDisabled: dynamicControls.sendToBrandPocs ?? false,
|
|
209
|
+
},
|
|
210
|
+
executionParams: {},
|
|
211
|
+
},
|
|
212
|
+
singleChannelStrategy: {
|
|
213
|
+
variant: {
|
|
214
|
+
channel,
|
|
215
|
+
...(contentKey && { [contentKey]: contentItem.templateData }),
|
|
216
|
+
...(deliveryKey && { [deliveryKey]: { channelSettings: { channel, ...channelSettings } } }),
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
};
|
|
166
220
|
|
|
167
|
-
const additionalSettings = {
|
|
168
|
-
useTinyUrl: dynamicControls.useTinyUrl ?? false,
|
|
169
|
-
encryptUrl: dynamicControls.sendToControlCustomers ?? false,
|
|
170
|
-
linkTrackingEnabled: dynamicControls.overrideDailyLimit ?? false,
|
|
171
|
-
userSubscriptionDisabled: dynamicControls.sendToBrandPocs ?? false,
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
if (contentItems.length > 0) {
|
|
175
221
|
try {
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
...(deliveryKey && {
|
|
192
|
-
[deliveryKey]: {
|
|
193
|
-
additionalSettings,
|
|
194
|
-
channelSettings: {
|
|
195
|
-
channel,
|
|
196
|
-
...(aggregatedData.deliverySetting?.channelSetting?.[channel] || {}),
|
|
197
|
-
},
|
|
198
|
-
},
|
|
199
|
-
}),
|
|
200
|
-
},
|
|
201
|
-
};
|
|
202
|
-
return createCentralCommsMetaId(payload);
|
|
203
|
-
}),
|
|
204
|
-
);
|
|
205
|
-
|
|
206
|
-
const metaIds = responses
|
|
207
|
-
.map((res) => res?.response?.data?.id)
|
|
208
|
-
.filter(Boolean)
|
|
209
|
-
.join(',');
|
|
210
|
-
|
|
211
|
-
if (metaIds) {
|
|
212
|
-
const getResponse = await getCentralCommsMetaIds(metaIds);
|
|
222
|
+
const res = await createCommDefinition(payload);
|
|
223
|
+
if (isDuplicateReferenceIdError(res)) {
|
|
224
|
+
// Duplicate referenceId in this org — block the save so the user can
|
|
225
|
+
// change it, rather than silently proceeding without a CCS comm.
|
|
226
|
+
setSaveError(formatMessage(messages.duplicateReferenceIdError));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const data = res?.response?.data;
|
|
230
|
+
if (data?.id) {
|
|
231
|
+
ccsCommDefinition = {
|
|
232
|
+
id: data.id,
|
|
233
|
+
referenceId: data.referenceId,
|
|
234
|
+
version: data.version?.version || 1,
|
|
235
|
+
status: data.status,
|
|
236
|
+
};
|
|
213
237
|
}
|
|
214
238
|
} catch (error) {
|
|
215
|
-
console.error('[CommunicationFlow] CCS
|
|
239
|
+
console.error('[CommunicationFlow] CCS createCommDefinition error:', error);
|
|
216
240
|
}
|
|
241
|
+
} else if (!name) {
|
|
242
|
+
console.warn('[CommunicationFlow] Skipping CCS createCommDefinition — config.context.name is required');
|
|
217
243
|
}
|
|
218
244
|
}
|
|
219
245
|
|
|
220
|
-
onSave(aggregatedData);
|
|
246
|
+
onSave(ccsCommDefinition ? { ...aggregatedData, ccsCommDefinition } : aggregatedData);
|
|
221
247
|
}, [getAggregatedData, config, onSave]);
|
|
222
248
|
|
|
223
249
|
// Call onChange callback when stepData changes
|
|
@@ -317,6 +343,11 @@ const CommunicationFlow = ({
|
|
|
317
343
|
{renderSteps()}
|
|
318
344
|
{onSave && (
|
|
319
345
|
<CapRow useLegacy className="communication-flow-container__footer">
|
|
346
|
+
{saveError && (
|
|
347
|
+
<CapLabel type="label2" className="communication-flow-container__save-error">
|
|
348
|
+
{saveError}
|
|
349
|
+
</CapLabel>
|
|
350
|
+
)}
|
|
320
351
|
<CapButton type="primary" onClick={handleSave} disabled={isSaveDisabled}>
|
|
321
352
|
{formatMessage(messages.save)}
|
|
322
353
|
</CapButton>
|
|
@@ -19,6 +19,7 @@ import CapIcon from '@capillarytech/cap-ui-library/CapIcon';
|
|
|
19
19
|
import CapLabel from '@capillarytech/cap-ui-library/CapLabel';
|
|
20
20
|
import CapHeader from '@capillarytech/cap-ui-library/CapHeader';
|
|
21
21
|
import CapSlideBox from '@capillarytech/cap-ui-library/CapSlideBox';
|
|
22
|
+
import CapTooltip from '@capillarytech/cap-ui-library/CapTooltip';
|
|
22
23
|
import CommunicationFlow from './index';
|
|
23
24
|
import { CHANNELS, DEFAULT_COMMUNICATION_STRATEGY_OPTIONS, DYNAMIC_CONTROLS_CONFIG } from './constants';
|
|
24
25
|
import {
|
|
@@ -74,6 +75,8 @@ const CommunicationFlowCard = ({
|
|
|
74
75
|
onChange,
|
|
75
76
|
cap,
|
|
76
77
|
intl,
|
|
78
|
+
disabled,
|
|
79
|
+
disabledTooltip,
|
|
77
80
|
}) => {
|
|
78
81
|
const { formatMessage } = intl || {};
|
|
79
82
|
const [showSlideBox, setShowSlideBox] = useState(false);
|
|
@@ -91,8 +94,9 @@ const CommunicationFlowCard = ({
|
|
|
91
94
|
}, [onCancel]);
|
|
92
95
|
|
|
93
96
|
const handleOpen = useCallback(() => {
|
|
97
|
+
if (disabled) return;
|
|
94
98
|
setShowSlideBox(true);
|
|
95
|
-
}, []);
|
|
99
|
+
}, [disabled]);
|
|
96
100
|
|
|
97
101
|
const firstItem = savedData?.contentItems?.[0];
|
|
98
102
|
const channelConfig = firstItem
|
|
@@ -182,9 +186,17 @@ const CommunicationFlowCard = ({
|
|
|
182
186
|
<CapImage src={addCreativesIllustration} />
|
|
183
187
|
</CapColumn>
|
|
184
188
|
<CapColumn span={14} className="empty-card-action-col">
|
|
185
|
-
|
|
186
|
-
{
|
|
187
|
-
|
|
189
|
+
{disabled && disabledTooltip ? (
|
|
190
|
+
<CapTooltip title={disabledTooltip}>
|
|
191
|
+
<CapButton type="secondary" disabled>
|
|
192
|
+
{formatMessage(messages.addCreatives)}
|
|
193
|
+
</CapButton>
|
|
194
|
+
</CapTooltip>
|
|
195
|
+
) : (
|
|
196
|
+
<CapButton type="secondary" onClick={handleOpen} disabled={disabled}>
|
|
197
|
+
{formatMessage(messages.addCreatives)}
|
|
198
|
+
</CapButton>
|
|
199
|
+
)}
|
|
188
200
|
</CapColumn>
|
|
189
201
|
</CapRow>
|
|
190
202
|
</CapCard>
|
|
@@ -227,6 +239,8 @@ CommunicationFlowCard.propTypes = {
|
|
|
227
239
|
onChange: PropTypes.func,
|
|
228
240
|
cap: PropTypes.object,
|
|
229
241
|
intl: PropTypes.object.isRequired,
|
|
242
|
+
disabled: PropTypes.bool, // Disables the "Add creatives" trigger (e.g. until a consumer-required field is filled)
|
|
243
|
+
disabledTooltip: PropTypes.node, // Shown on hover when disabled is true
|
|
230
244
|
};
|
|
231
245
|
|
|
232
246
|
CommunicationFlowCard.defaultProps = {
|
|
@@ -235,6 +249,8 @@ CommunicationFlowCard.defaultProps = {
|
|
|
235
249
|
onCancel: null,
|
|
236
250
|
onChange: null,
|
|
237
251
|
cap: null,
|
|
252
|
+
disabled: false,
|
|
253
|
+
disabledTooltip: null,
|
|
238
254
|
};
|
|
239
255
|
|
|
240
256
|
export default injectIntl(CommunicationFlowCard);
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
3
|
jest.mock('../../../services/api', () => ({
|
|
4
|
-
|
|
5
|
-
getCentralCommsMetaIds: jest.fn(),
|
|
4
|
+
createCommDefinition: jest.fn(),
|
|
6
5
|
}));
|
|
7
6
|
|
|
8
7
|
jest.mock('../../CreativesContainer', () => function MockCreativesContainer({
|
|
@@ -38,7 +37,7 @@ import { IntlProvider } from 'react-intl';
|
|
|
38
37
|
import history from '../../../utils/history';
|
|
39
38
|
import { initialReducer } from '../../../initialReducer';
|
|
40
39
|
import CommunicationFlow from '../CommunicationFlow';
|
|
41
|
-
import {
|
|
40
|
+
import { createCommDefinition } from '../../../services/api';
|
|
42
41
|
import { getEnabledSteps } from '../utils/getEnabledSteps';
|
|
43
42
|
import {
|
|
44
43
|
CHANNELS,
|
|
@@ -359,74 +358,121 @@ describe('isSaveDisabled', () => {
|
|
|
359
358
|
});
|
|
360
359
|
|
|
361
360
|
describe('handleSave — CCS flow', () => {
|
|
361
|
+
const ccsConfig = { ...baseConfig, context: { name: 'Order Placed Notification' }, features: {} };
|
|
362
|
+
|
|
362
363
|
beforeEach(() => {
|
|
363
|
-
|
|
364
|
-
|
|
364
|
+
createCommDefinition.mockResolvedValue({
|
|
365
|
+
response: { data: { id: 'cd_123', referenceId: 'ORDER_PLACED_123', status: 'DRAFT', version: { version: 1 } } },
|
|
366
|
+
});
|
|
365
367
|
});
|
|
366
368
|
|
|
367
369
|
afterEach(() => {
|
|
368
370
|
jest.clearAllMocks();
|
|
369
371
|
});
|
|
370
372
|
|
|
371
|
-
it('calls
|
|
373
|
+
it('calls createCommDefinition with a SINGLE-strategy payload when useCCS is not false', async () => {
|
|
372
374
|
const onSave = jest.fn();
|
|
373
375
|
renderWithFlow({
|
|
374
376
|
features: {},
|
|
377
|
+
config: ccsConfig,
|
|
375
378
|
initialData: {
|
|
376
|
-
contentItems: [{ channel: 'sms', templateData: {
|
|
379
|
+
contentItems: [{ channel: 'sms', templateData: { message: 'Hello' } }],
|
|
377
380
|
},
|
|
378
381
|
onSave,
|
|
379
382
|
});
|
|
380
383
|
|
|
381
384
|
await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
|
|
382
385
|
|
|
383
|
-
await waitFor(() => expect(
|
|
384
|
-
expect(
|
|
386
|
+
await waitFor(() => expect(createCommDefinition).toHaveBeenCalledTimes(1));
|
|
387
|
+
expect(createCommDefinition).toHaveBeenCalledWith(
|
|
385
388
|
expect.objectContaining({
|
|
386
|
-
|
|
389
|
+
name: 'Order Placed Notification',
|
|
390
|
+
strategyType: 'SINGLE',
|
|
391
|
+
singleChannelStrategy: expect.objectContaining({
|
|
392
|
+
variant: expect.objectContaining({
|
|
393
|
+
channel: 'SMS',
|
|
394
|
+
smsMessageContent: { message: 'Hello' },
|
|
395
|
+
}),
|
|
396
|
+
}),
|
|
387
397
|
}),
|
|
388
398
|
);
|
|
389
399
|
expect(onSave).toHaveBeenCalledTimes(1);
|
|
390
400
|
});
|
|
391
401
|
|
|
392
|
-
it('
|
|
402
|
+
it('normalizes the internal MOBILEPUSH channel to CCS\'s MPUSH enum and key names', async () => {
|
|
403
|
+
const onSave = jest.fn();
|
|
393
404
|
renderWithFlow({
|
|
394
405
|
features: {},
|
|
395
|
-
|
|
406
|
+
config: ccsConfig,
|
|
407
|
+
initialData: {
|
|
408
|
+
contentItems: [{ channel: 'MOBILEPUSH', templateData: { messageSubject: 'Hi' } }],
|
|
409
|
+
},
|
|
410
|
+
onSave,
|
|
396
411
|
});
|
|
397
412
|
|
|
398
413
|
await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
|
|
399
414
|
|
|
400
|
-
await waitFor(() => expect(
|
|
415
|
+
await waitFor(() => expect(createCommDefinition).toHaveBeenCalledTimes(1));
|
|
416
|
+
expect(createCommDefinition).toHaveBeenCalledWith(
|
|
417
|
+
expect.objectContaining({
|
|
418
|
+
singleChannelStrategy: expect.objectContaining({
|
|
419
|
+
variant: expect.objectContaining({
|
|
420
|
+
channel: 'MPUSH',
|
|
421
|
+
mpushMessageContent: { messageSubject: 'Hi' },
|
|
422
|
+
}),
|
|
423
|
+
}),
|
|
424
|
+
}),
|
|
425
|
+
);
|
|
401
426
|
});
|
|
402
427
|
|
|
403
|
-
it('
|
|
404
|
-
|
|
428
|
+
it('merges ccsCommDefinition into the data passed to onSave when create succeeds', async () => {
|
|
429
|
+
const onSave = jest.fn();
|
|
405
430
|
renderWithFlow({
|
|
406
431
|
features: {},
|
|
432
|
+
config: ccsConfig,
|
|
407
433
|
initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
|
|
434
|
+
onSave,
|
|
408
435
|
});
|
|
409
436
|
|
|
410
437
|
await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
|
|
411
438
|
|
|
412
|
-
await waitFor(() => expect(
|
|
413
|
-
expect(
|
|
439
|
+
await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
|
|
440
|
+
expect(onSave).toHaveBeenCalledWith(
|
|
441
|
+
expect.objectContaining({
|
|
442
|
+
ccsCommDefinition: { id: 'cd_123', referenceId: 'ORDER_PLACED_123', version: 1, status: 'DRAFT' },
|
|
443
|
+
}),
|
|
444
|
+
);
|
|
414
445
|
});
|
|
415
446
|
|
|
416
|
-
it('
|
|
447
|
+
it('generates a referenceId from name when config.context.referenceId is absent', async () => {
|
|
448
|
+
// moduleMocks.js spies on Date.now globally, but this config's resetMocks:true
|
|
449
|
+
// clears that return value before every test — set it explicitly here.
|
|
450
|
+
jest.spyOn(Date, 'now').mockReturnValue(1612267539410);
|
|
417
451
|
renderWithFlow({
|
|
418
452
|
features: {},
|
|
419
|
-
config:
|
|
453
|
+
config: ccsConfig,
|
|
420
454
|
initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
|
|
421
455
|
});
|
|
422
456
|
|
|
423
457
|
await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
|
|
424
458
|
|
|
425
|
-
await waitFor(() => expect(
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
459
|
+
await waitFor(() => expect(createCommDefinition).toHaveBeenCalled());
|
|
460
|
+
const payload = createCommDefinition.mock.calls[0][0];
|
|
461
|
+
expect(payload.referenceId).toBe('ORDER_PLACED_NOTIFICATION_1612267539410');
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('uses config.context.referenceId and description when provided', async () => {
|
|
465
|
+
renderWithFlow({
|
|
466
|
+
features: {},
|
|
467
|
+
config: { ...ccsConfig, context: { ...ccsConfig.context, referenceId: 'ORDER_PLACED', description: 'desc' } },
|
|
468
|
+
initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
|
|
472
|
+
|
|
473
|
+
await waitFor(() => expect(createCommDefinition).toHaveBeenCalled());
|
|
474
|
+
expect(createCommDefinition).toHaveBeenCalledWith(
|
|
475
|
+
expect.objectContaining({ referenceId: 'ORDER_PLACED', description: 'desc' }),
|
|
430
476
|
);
|
|
431
477
|
});
|
|
432
478
|
|
|
@@ -434,7 +480,7 @@ describe('handleSave — CCS flow', () => {
|
|
|
434
480
|
const onSave = jest.fn();
|
|
435
481
|
renderWithFlow({
|
|
436
482
|
features: {},
|
|
437
|
-
config: { ...
|
|
483
|
+
config: { ...ccsConfig, useCCS: false },
|
|
438
484
|
initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
|
|
439
485
|
onSave,
|
|
440
486
|
});
|
|
@@ -442,14 +488,15 @@ describe('handleSave — CCS flow', () => {
|
|
|
442
488
|
await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
|
|
443
489
|
|
|
444
490
|
await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
|
|
445
|
-
expect(
|
|
491
|
+
expect(createCommDefinition).not.toHaveBeenCalled();
|
|
492
|
+
expect(onSave).toHaveBeenCalledWith(expect.not.objectContaining({ ccsCommDefinition: expect.anything() }));
|
|
446
493
|
});
|
|
447
494
|
|
|
448
|
-
it('
|
|
449
|
-
createCentralCommsMetaId.mockRejectedValue(new Error('Network error'));
|
|
495
|
+
it('skips createCommDefinition when config.context.name is absent (mandatory)', async () => {
|
|
450
496
|
const onSave = jest.fn();
|
|
451
497
|
renderWithFlow({
|
|
452
498
|
features: {},
|
|
499
|
+
config: { ...baseConfig, features: {} },
|
|
453
500
|
initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
|
|
454
501
|
onSave,
|
|
455
502
|
});
|
|
@@ -457,12 +504,70 @@ describe('handleSave — CCS flow', () => {
|
|
|
457
504
|
await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
|
|
458
505
|
|
|
459
506
|
await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
|
|
507
|
+
expect(createCommDefinition).not.toHaveBeenCalled();
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('skips createCommDefinition for multi-channel strategies (CHANNEL_PRIORITY/AB_TEST)', async () => {
|
|
511
|
+
const onSave = jest.fn();
|
|
512
|
+
renderWithFlow({
|
|
513
|
+
features: {},
|
|
514
|
+
config: ccsConfig,
|
|
515
|
+
initialData: {
|
|
516
|
+
communicationStrategy: CHANNEL_PRIORITY,
|
|
517
|
+
channels: ['SMS', 'EMAIL'],
|
|
518
|
+
contentItems: [{ channel: 'SMS', templateData: {} }, { channel: 'EMAIL', templateData: {} }],
|
|
519
|
+
},
|
|
520
|
+
onSave,
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
|
|
524
|
+
|
|
525
|
+
await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
|
|
526
|
+
expect(createCommDefinition).not.toHaveBeenCalled();
|
|
460
527
|
});
|
|
461
528
|
|
|
462
|
-
it('
|
|
529
|
+
it('still calls onSave (without ccsCommDefinition) when createCommDefinition rejects', async () => {
|
|
530
|
+
createCommDefinition.mockRejectedValue(new Error('Network error'));
|
|
463
531
|
const onSave = jest.fn();
|
|
464
532
|
renderWithFlow({
|
|
465
533
|
features: {},
|
|
534
|
+
config: ccsConfig,
|
|
535
|
+
initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
|
|
536
|
+
onSave,
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
|
|
540
|
+
|
|
541
|
+
await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
|
|
542
|
+
expect(onSave).toHaveBeenCalledWith(expect.not.objectContaining({ ccsCommDefinition: expect.anything() }));
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('blocks save and shows an error when createCommDefinition resolves with a duplicate REFERENCE_ID_EXISTS conflict', async () => {
|
|
546
|
+
createCommDefinition.mockResolvedValue({
|
|
547
|
+
success: false,
|
|
548
|
+
status: { isError: true, code: 409, message: 'REFERENCE_ID_EXISTS' },
|
|
549
|
+
message: 'REFERENCE_ID_EXISTS',
|
|
550
|
+
});
|
|
551
|
+
const onSave = jest.fn();
|
|
552
|
+
renderWithFlow({
|
|
553
|
+
features: {},
|
|
554
|
+
config: ccsConfig,
|
|
555
|
+
initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
|
|
556
|
+
onSave,
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
|
|
560
|
+
|
|
561
|
+
await waitFor(() => expect(createCommDefinition).toHaveBeenCalledTimes(1));
|
|
562
|
+
expect(await screen.findByText(/already in use in your organization/i)).toBeInTheDocument();
|
|
563
|
+
expect(onSave).not.toHaveBeenCalled();
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('skips createCommDefinition when contentItems is empty', async () => {
|
|
567
|
+
const onSave = jest.fn();
|
|
568
|
+
renderWithFlow({
|
|
569
|
+
features: {},
|
|
570
|
+
config: ccsConfig,
|
|
466
571
|
initialData: { contentItems: [] },
|
|
467
572
|
onSave,
|
|
468
573
|
});
|
|
@@ -470,12 +575,13 @@ describe('handleSave — CCS flow', () => {
|
|
|
470
575
|
await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
|
|
471
576
|
|
|
472
577
|
await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
|
|
473
|
-
expect(
|
|
578
|
+
expect(createCommDefinition).not.toHaveBeenCalled();
|
|
474
579
|
});
|
|
475
580
|
|
|
476
|
-
it('
|
|
581
|
+
it('places additionalSettings derived from dynamicControls under settings.additionalSettings (not delivery settings)', async () => {
|
|
477
582
|
renderWithFlow({
|
|
478
583
|
features: {},
|
|
584
|
+
config: ccsConfig,
|
|
479
585
|
initialData: {
|
|
480
586
|
contentItems: [{ channel: 'SMS', templateData: {} }],
|
|
481
587
|
dynamicControls: {
|
|
@@ -489,14 +595,36 @@ describe('handleSave — CCS flow', () => {
|
|
|
489
595
|
|
|
490
596
|
await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
|
|
491
597
|
|
|
492
|
-
await waitFor(() => expect(
|
|
493
|
-
const payload =
|
|
494
|
-
expect(payload.
|
|
598
|
+
await waitFor(() => expect(createCommDefinition).toHaveBeenCalled());
|
|
599
|
+
const payload = createCommDefinition.mock.calls[0][0];
|
|
600
|
+
expect(payload.settings.additionalSettings).toEqual({
|
|
495
601
|
useTinyUrl: true,
|
|
496
602
|
encryptUrl: true,
|
|
497
603
|
linkTrackingEnabled: true,
|
|
498
604
|
userSubscriptionDisabled: true,
|
|
499
605
|
});
|
|
606
|
+
expect(payload.singleChannelStrategy.variant.smsDeliverySettings.additionalSettings).toBeUndefined();
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it('includes channelSettings from deliverySetting.channelSetting in the payload', async () => {
|
|
610
|
+
renderWithFlow({
|
|
611
|
+
features: {},
|
|
612
|
+
config: ccsConfig,
|
|
613
|
+
initialData: {
|
|
614
|
+
contentItems: [{ channel: 'SMS', templateData: {} }],
|
|
615
|
+
deliverySetting: { channelSetting: { SMS: { domainId: 1001, gsmSenderId: 'BRAND1' } } },
|
|
616
|
+
},
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
|
|
620
|
+
|
|
621
|
+
await waitFor(() => expect(createCommDefinition).toHaveBeenCalled());
|
|
622
|
+
const payload = createCommDefinition.mock.calls[0][0];
|
|
623
|
+
expect(payload.singleChannelStrategy.variant.smsDeliverySettings.channelSettings).toEqual({
|
|
624
|
+
channel: 'SMS',
|
|
625
|
+
domainId: 1001,
|
|
626
|
+
gsmSenderId: 'BRAND1',
|
|
627
|
+
});
|
|
500
628
|
});
|
|
501
629
|
});
|
|
502
630
|
|
|
@@ -552,39 +680,34 @@ describe('optional chaining safety', () => {
|
|
|
552
680
|
jest.clearAllMocks();
|
|
553
681
|
});
|
|
554
682
|
|
|
555
|
-
it('
|
|
556
|
-
|
|
683
|
+
it('does not crash and skips CCS when config.context is entirely absent', async () => {
|
|
684
|
+
const onSave = jest.fn();
|
|
557
685
|
renderWithFlow({
|
|
558
686
|
features: {},
|
|
687
|
+
config: { ...baseConfig, features: {} },
|
|
559
688
|
initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
|
|
689
|
+
onSave,
|
|
560
690
|
});
|
|
561
691
|
|
|
562
692
|
await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
|
|
563
693
|
|
|
564
|
-
await waitFor(() => expect(
|
|
565
|
-
expect(
|
|
566
|
-
expect.objectContaining({
|
|
567
|
-
centralCommsPayload: expect.objectContaining({ ouId: -1 }),
|
|
568
|
-
}),
|
|
569
|
-
);
|
|
694
|
+
await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
|
|
695
|
+
expect(createCommDefinition).not.toHaveBeenCalled();
|
|
570
696
|
});
|
|
571
697
|
|
|
572
|
-
it('
|
|
573
|
-
|
|
698
|
+
it('does not crash when config.context is present but empty', async () => {
|
|
699
|
+
const onSave = jest.fn();
|
|
574
700
|
renderWithFlow({
|
|
575
701
|
features: {},
|
|
576
|
-
config: { ...baseConfig,
|
|
702
|
+
config: { ...baseConfig, context: {}, features: {} },
|
|
577
703
|
initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
|
|
704
|
+
onSave,
|
|
578
705
|
});
|
|
579
706
|
|
|
580
707
|
await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
|
|
581
708
|
|
|
582
|
-
await waitFor(() => expect(
|
|
583
|
-
expect(
|
|
584
|
-
expect.objectContaining({
|
|
585
|
-
centralCommsPayload: expect.objectContaining({ module: 'LOYALTY' }),
|
|
586
|
-
}),
|
|
587
|
-
);
|
|
709
|
+
await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
|
|
710
|
+
expect(createCommDefinition).not.toHaveBeenCalled();
|
|
588
711
|
});
|
|
589
712
|
|
|
590
713
|
it('initializes channel from config.channel when initialData.channel is absent', () => {
|
|
@@ -108,6 +108,22 @@ describe('CommunicationFlowCard', () => {
|
|
|
108
108
|
fireEvent.click(screen.getByText('Add creatives'));
|
|
109
109
|
expect(screen.getByTestId('comm-flow-mock')).toHaveAttribute('data-mode', 'create');
|
|
110
110
|
});
|
|
111
|
+
|
|
112
|
+
it('disables the Add creatives button when disabled prop is true', () => {
|
|
113
|
+
renderCard({ disabled: true, disabledTooltip: 'Enter an Alert name to add content' });
|
|
114
|
+
expect(screen.getByText('Add creatives').closest('button')).toBeDisabled();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('does not open SlideBox when Add creatives is clicked while disabled', () => {
|
|
118
|
+
renderCard({ disabled: true, disabledTooltip: 'Enter an Alert name to add content' });
|
|
119
|
+
fireEvent.click(screen.getByText('Add creatives'));
|
|
120
|
+
expect(screen.queryByTestId('slide-box')).not.toBeInTheDocument();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('Add creatives button is enabled by default (disabled prop absent)', () => {
|
|
124
|
+
renderCard();
|
|
125
|
+
expect(screen.getByText('Add creatives').closest('button')).not.toBeDisabled();
|
|
126
|
+
});
|
|
111
127
|
});
|
|
112
128
|
|
|
113
129
|
describe('configured state (initialData provided)', () => {
|
|
@@ -38,6 +38,53 @@ 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.
|
|
44
|
+
export const CCS_STRATEGY_TYPE_SINGLE = 'SINGLE';
|
|
45
|
+
|
|
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.
|
|
50
|
+
export const CCS_CHANNEL_CONTENT_KEY_MAP = {
|
|
51
|
+
SMS: 'smsMessageContent',
|
|
52
|
+
EMAIL: 'emailMessageContent',
|
|
53
|
+
WHATSAPP: 'whatsappMessageContent',
|
|
54
|
+
MPUSH: 'mpushMessageContent',
|
|
55
|
+
INAPP: 'inAppMessageContent',
|
|
56
|
+
ANDROID: 'androidMessageContent',
|
|
57
|
+
IOS: 'iosMessageContent',
|
|
58
|
+
ZALO: 'zaloMessageContent',
|
|
59
|
+
VIBER: 'viberMessageContent',
|
|
60
|
+
LINE: 'lineMessageContent',
|
|
61
|
+
WEBPUSH: 'webPushMessageContent',
|
|
62
|
+
RCS: 'rcsMessageContent',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const CCS_CHANNEL_DELIVERY_KEY_MAP = {
|
|
66
|
+
SMS: 'smsDeliverySettings',
|
|
67
|
+
EMAIL: 'emailDeliverySettings',
|
|
68
|
+
WHATSAPP: 'whatsappDeliverySettings',
|
|
69
|
+
MPUSH: 'mpushDeliverySettings',
|
|
70
|
+
INAPP: 'inAppDeliverySettings',
|
|
71
|
+
ANDROID: 'androidDeliverySettings',
|
|
72
|
+
IOS: 'iosDeliverySettings',
|
|
73
|
+
ZALO: 'zaloDeliverySettings',
|
|
74
|
+
VIBER: 'viberDeliverySettings',
|
|
75
|
+
LINE: 'lineDeliverySettings',
|
|
76
|
+
WEBPUSH: 'webPushDeliverySettings',
|
|
77
|
+
RCS: 'rcsDeliverySettings',
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// The UI's own internal channel identifier for mobile push is 'MOBILEPUSH'
|
|
81
|
+
// (see CreativesContainer/constants.js MOBILE_PUSH), but CCS's channel enum
|
|
82
|
+
// only recognises 'MPUSH' (globals.js CCS_CHANNELS) — normalize before
|
|
83
|
+
// building the CCS payload. Every other channel identifier matches CCS as-is.
|
|
84
|
+
export const CCS_CHANNEL_NAME_MAP = {
|
|
85
|
+
MOBILEPUSH: 'MPUSH',
|
|
86
|
+
};
|
|
87
|
+
|
|
41
88
|
// Channel config - single source of truth for CreativesContainer and TemplatesV2
|
|
42
89
|
// paneKey: TemplatesV2 defaultPanes object key (for channelsToHide)
|
|
43
90
|
// channelProp: CreativesContainer channel prop (must match pane.key for tab to be active)
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
import React from 'react';
|
|
10
10
|
import PropTypes from 'prop-types';
|
|
11
11
|
import CommunicationFlow from './CommunicationFlow';
|
|
12
|
-
import { hasSupportEngagementModule } from '../../utils/common';
|
|
13
12
|
|
|
14
13
|
const CommunicationFlowContainer = ({
|
|
15
14
|
config,
|
|
@@ -18,22 +17,16 @@ const CommunicationFlowContainer = ({
|
|
|
18
17
|
onCancel,
|
|
19
18
|
onChange,
|
|
20
19
|
...otherProps
|
|
21
|
-
}) =>
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
onCancel={onCancel}
|
|
32
|
-
onChange={onChange}
|
|
33
|
-
{...otherProps}
|
|
34
|
-
/>
|
|
35
|
-
);
|
|
36
|
-
};
|
|
20
|
+
}) => (
|
|
21
|
+
<CommunicationFlow
|
|
22
|
+
config={config}
|
|
23
|
+
initialData={initialData}
|
|
24
|
+
onSave={onSave}
|
|
25
|
+
onCancel={onCancel}
|
|
26
|
+
onChange={onChange}
|
|
27
|
+
{...otherProps}
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
37
30
|
|
|
38
31
|
CommunicationFlowContainer.propTypes = {
|
|
39
32
|
config: PropTypes.shape({
|
|
@@ -87,11 +80,13 @@ CommunicationFlowContainer.propTypes = {
|
|
|
87
80
|
controls: PropTypes.array,
|
|
88
81
|
}),
|
|
89
82
|
}),
|
|
90
|
-
context: PropTypes.object, // ouId, campaignId, programId,
|
|
91
|
-
|
|
83
|
+
context: PropTypes.object, // ouId, campaignId, programId; name (required for CCS create),
|
|
84
|
+
// referenceId (optional, auto-generated from name if absent), description (optional).
|
|
85
|
+
useCCS: PropTypes.bool, // If false, skips the CCS createCommDefinition call on save. Defaults to true.
|
|
92
86
|
}).isRequired,
|
|
93
87
|
initialData: PropTypes.object, // for edit/preview mode
|
|
94
|
-
onSave: PropTypes.func.isRequired, // (data) => void - called when user saves
|
|
88
|
+
onSave: PropTypes.func.isRequired, // (data) => void - called when user saves; data.ccsCommDefinition
|
|
89
|
+
// ({id, referenceId, version, status}) is present when the CCS create succeeded
|
|
95
90
|
onCancel: PropTypes.func.isRequired, // () => void - called when user cancels
|
|
96
91
|
onChange: PropTypes.func, // (data) => void - optional, called on data changes
|
|
97
92
|
};
|
|
@@ -363,4 +363,8 @@ export default {
|
|
|
363
363
|
id: `${prefix}.senderNotConfiguredError`,
|
|
364
364
|
defaultMessage: 'Selected domain gateway id is not correct. Please change the domain id or contact the gateway team to register them with Capillary.',
|
|
365
365
|
},
|
|
366
|
+
duplicateReferenceIdError: {
|
|
367
|
+
id: `${prefix}.duplicateReferenceIdError`,
|
|
368
|
+
defaultMessage: 'This reference ID is already in use in your organization. Please use a different reference ID.',
|
|
369
|
+
},
|
|
366
370
|
};
|