@capillarytech/creatives-library 9.0.54-0 → 9.0.54-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.
@@ -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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "9.0.54-0",
4
+ "version": "9.0.54-alpha.1",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
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,
@@ -1275,7 +1275,6 @@ const CommonTestAndPreview = (props) => {
1275
1275
  ...(suggestionsFormattedForTestMeta.length > 0 && {
1276
1276
  suggestions: suggestionsFormattedForTestMeta,
1277
1277
  }),
1278
- ...(singleRcsCardPayload?.cardVarMapped && { cardVarMapped: singleRcsCardPayload.cardVarMapped }),
1279
1278
  };
1280
1279
  });
1281
1280
  // Use the component-level smsFallbackContent prop (has rcsSmsFallbackVarMapped) so DLT
@@ -1342,11 +1341,9 @@ const CommonTestAndPreview = (props) => {
1342
1341
  ...(cardContentForTestMetaApi.length > 0 && { cardContent: cardContentForTestMetaApi }),
1343
1342
  };
1344
1343
 
1345
- const vfTemplateId = rcsContentFromForm?.vfTemplateId;
1346
1344
  const rcsMessageContent = {
1347
1345
  channel: CHANNELS.RCS,
1348
1346
  ...(accountIdForMeta && { accountId: accountIdForMeta }),
1349
- ...(vfTemplateId && { templateId: vfTemplateId }),
1350
1347
  rcsRichCardContent,
1351
1348
  ...(smsFallBackContent && { smsFallBackContent }),
1352
1349
  };
@@ -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 { createCentralCommsMetaId, getCentralCommsMetaIds } from '../../services/api';
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,11 @@ import {
35
36
  CHANNELS,
36
37
  INCENTIVE_TYPES,
37
38
  DYNAMIC_CONTROLS_CONFIG,
38
- CHANNEL_CONTENT_KEY_MAP,
39
- CHANNEL_DELIVERY_KEY_MAP,
40
- CAMPAIGNS,
39
+ CCS_STRATEGY_TYPE_SINGLE,
40
+ CCS_CHANNEL_CONTENT_KEY_MAP,
41
+ CCS_CHANNEL_DELIVERY_KEY_MAP,
42
+ CCS_CHANNEL_NAME_MAP,
43
+ CCS_CONTENT_TRANSFORMS,
41
44
  } from './constants';
42
45
  import { getEnabledSteps } from './utils/getEnabledSteps';
43
46
  import messages from './messages';
@@ -64,6 +67,25 @@ const hasDeliverySettingForChannel = (channelSetting, channel) => {
64
67
  return !!settings && Object.values(settings).some((v) => v !== null && v !== '' && v !== undefined);
65
68
  };
66
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.
73
+ const isDuplicateReferenceIdError = (res) => res?.success === false && res?.status?.message === 'REFERENCE_ID_EXISTS';
74
+
75
+ /**
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.
79
+ */
80
+ const buildCcsReferenceId = (name) => {
81
+ const slug = (name || 'COMM')
82
+ .trim()
83
+ .toUpperCase()
84
+ .replace(/[^A-Z0-9]+/g, '_')
85
+ .replace(/^_+|_+$/g, '') || 'COMM';
86
+ return `${slug}_${Date.now()}`;
87
+ };
88
+
67
89
  const CommunicationFlow = ({
68
90
  config,
69
91
  initialData,
@@ -91,6 +113,7 @@ const CommunicationFlow = ({
91
113
  };
92
114
  });
93
115
  const [validationErrors, setValidationErrors] = useState({});
116
+ const [saveError, setSaveError] = useState(null);
94
117
 
95
118
  // Memoize enabled steps
96
119
  const enabledSteps = useMemo(() => getEnabledSteps(config), [config]);
@@ -150,74 +173,84 @@ const CommunicationFlow = ({
150
173
  }, []);
151
174
 
152
175
  const handleSave = useCallback(async () => {
176
+ setSaveError(null);
153
177
  const aggregatedData = getAggregatedData();
154
178
  const shouldUseCCS = config?.useCCS !== false;
179
+ let ccsCommDefinition = null;
155
180
 
156
181
  if (shouldUseCCS) {
157
- const ouId = config?.context?.ouId || -1;
158
- const module = config?.context?.module
159
- || (config?.consumer ? config.consumer.toUpperCase() : CAMPAIGNS);
182
+ const isMultiChannel = [CHANNEL_PRIORITY, AB_TEST].includes(aggregatedData.communicationStrategy);
183
+ const contentItem = (aggregatedData.contentItems || [])[0];
184
+ // Consumer-supplied name (e.g. CapNotify's Alert Name field) CommunicationFlow
185
+ // has no name input of its own, so this is mandatory input from config.context.
186
+ const name = config?.context?.name;
160
187
 
161
- const channelContentKeyMap = CHANNEL_CONTENT_KEY_MAP;
162
- const channelDeliveryKeyMap = CHANNEL_DELIVERY_KEY_MAP;
188
+ // CCS create is SINGLE-strategy only this phase (D1); CHANNEL_PRIORITY/AB_TEST
189
+ // carry multiple content items with no CCS equivalent yet.
190
+ if (!isMultiChannel && contentItem && name) {
191
+ const rawChannel = (contentItem.channel || '').toUpperCase();
192
+ const channel = CCS_CHANNEL_NAME_MAP[rawChannel] || rawChannel;
193
+ const contentKey = CCS_CHANNEL_CONTENT_KEY_MAP[channel];
194
+ const deliveryKey = CCS_CHANNEL_DELIVERY_KEY_MAP[channel];
195
+ const contentTransform = CCS_CONTENT_TRANSFORMS[channel];
196
+ const contentPayload = contentTransform ? contentTransform(contentItem.templateData) : contentItem.templateData;
197
+ const { dynamicControls = {} } = aggregatedData;
198
+ const channelSettings = aggregatedData.deliverySetting?.channelSetting?.[channel] || {};
199
+ const referenceId = config?.context?.referenceId || buildCcsReferenceId(name);
163
200
 
164
- const contentItems = aggregatedData.contentItems || [];
165
- const { dynamicControls = {} } = aggregatedData;
201
+ const payload = {
202
+ referenceId,
203
+ name,
204
+ description: config?.context?.description || undefined,
205
+ strategyType: CCS_STRATEGY_TYPE_SINGLE,
206
+ settings: {
207
+ additionalSettings: {
208
+ useTinyUrl: dynamicControls.useTinyUrl ?? false,
209
+ encryptUrl: dynamicControls.sendToControlCustomers ?? false,
210
+ linkTrackingEnabled: dynamicControls.overrideDailyLimit ?? false,
211
+ userSubscriptionDisabled: dynamicControls.sendToBrandPocs ?? false,
212
+ },
213
+ executionParams: {},
214
+ },
215
+ singleChannelStrategy: {
216
+ variant: {
217
+ channel,
218
+ ...(contentKey && { [contentKey]: { channel, ...contentPayload } }),
219
+ ...(deliveryKey && { [deliveryKey]: { channelSettings: { channel, ...channelSettings } } }),
220
+ },
221
+ },
222
+ };
166
223
 
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
224
  try {
176
- const responses = await Promise.all(
177
- contentItems.map((item) => {
178
- const channel = (item.channel || '').toUpperCase();
179
- const contentKey = channelContentKeyMap[channel];
180
- const deliveryKey = channelDeliveryKeyMap[channel];
181
- const payload = {
182
- centralCommsPayload: {
183
- ouId,
184
- channel,
185
- module,
186
- executionParams: {},
187
- clientName: 'EMF',
188
- ...(contentKey && {
189
- [contentKey]: { channel, ...item.templateData },
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);
225
+ const res = await createCommDefinition(payload);
226
+ if (isDuplicateReferenceIdError(res)) {
227
+ // Duplicate referenceId in this org — block the save so the user can
228
+ // change it, rather than silently proceeding without a CCS comm.
229
+ setSaveError(formatMessage(messages.duplicateReferenceIdError));
230
+ return;
231
+ }
232
+ const data = res?.response?.data;
233
+ if (data?.id) {
234
+ ccsCommDefinition = {
235
+ id: data.id,
236
+ referenceId: data.referenceId,
237
+ version: data.version?.version ?? 1,
238
+ status: data.status,
239
+ };
213
240
  }
214
241
  } catch (error) {
215
- console.error('[CommunicationFlow] CCS createCentralCommsMetaId error:', error);
242
+ console.error('[CommunicationFlow] CCS createCommDefinition error:', error);
216
243
  }
244
+ } else if (!name) {
245
+ console.warn('[CommunicationFlow] Skipping CCS createCommDefinition — config.context.name is required');
246
+ } else if (isMultiChannel) {
247
+ console.warn('[CommunicationFlow] Skipping CCS createCommDefinition — CHANNEL_PRIORITY/AB_TEST strategies are not supported by CCS create yet (SINGLE only)');
248
+ } else if (!contentItem) {
249
+ console.warn('[CommunicationFlow] Skipping CCS createCommDefinition — no content item found (contentItems is empty)');
217
250
  }
218
251
  }
219
252
 
220
- onSave(aggregatedData);
253
+ onSave(ccsCommDefinition ? { ...aggregatedData, ccsCommDefinition } : aggregatedData);
221
254
  }, [getAggregatedData, config, onSave]);
222
255
 
223
256
  // Call onChange callback when stepData changes
@@ -317,6 +350,11 @@ const CommunicationFlow = ({
317
350
  {renderSteps()}
318
351
  {onSave && (
319
352
  <CapRow useLegacy className="communication-flow-container__footer">
353
+ {saveError && (
354
+ <CapLabel type="label2" className="communication-flow-container__save-error">
355
+ {saveError}
356
+ </CapLabel>
357
+ )}
320
358
  <CapButton type="primary" onClick={handleSave} disabled={isSaveDisabled}>
321
359
  {formatMessage(messages.save)}
322
360
  </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
- <CapButton type="secondary" onClick={handleOpen}>
186
- {formatMessage(messages.addCreatives)}
187
- </CapButton>
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);