@capillarytech/creatives-library 9.0.54 → 9.0.55-alpha.0

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",
4
+ "version": "9.0.55-alpha.0",
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,26 @@ 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
+
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.
719
+ export const editCommDefinition = (commDefinitionId, payload) => {
720
+ const url = `${API_ENDPOINT}/comm-definitions/${commDefinitionId}/edit`;
721
+ return request(url, getAPICallObject('POST', payload, false, false, false, true));
722
+ };
723
+
704
724
  export const getCentralCommsMetaIds = (metaIds, metaType = TRANSACTION) => {
705
725
  const url = `${API_ENDPOINT}/common/central-comms/meta-id/${metaType}?metaIds=${metaIds}`;
706
726
  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,
@@ -3813,7 +3813,7 @@ const CommonTestAndPreview = (props) => {
3813
3813
  header={slideboxHeader}
3814
3814
  handleClose={handleClose}
3815
3815
  show={show}
3816
- size="size-xl"
3816
+ size="size-l"
3817
3817
  content={(
3818
3818
  <CapSpin
3819
3819
  spinning={isCustomerDataLoading}
@@ -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, editCommDefinition } 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,108 @@ 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
+ // `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.
222
+ ...(contentKey && { [contentKey]: { ...contentPayload, channel } }),
223
+ ...(deliveryKey && { [deliveryKey]: { channelSettings: { ...channelSettings, channel } } }),
224
+ },
225
+ },
226
+ };
166
227
 
167
- const additionalSettings = {
168
- useTinyUrl: dynamicControls.useTinyUrl ?? false,
169
- encryptUrl: dynamicControls.sendToControlCustomers ?? false,
170
- linkTrackingEnabled: dynamicControls.overrideDailyLimit ?? false,
171
- userSubscriptionDisabled: dynamicControls.sendToBrandPocs ?? false,
172
- };
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.
231
+ const { existingCommDefinitionId } = config?.context || {};
173
232
 
174
- if (contentItems.length > 0) {
175
233
  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
- },
234
+ const res = existingCommDefinitionId
235
+ ? await editCommDefinition(existingCommDefinitionId, {
236
+ strategyType: payload.strategyType,
237
+ settings: payload.settings,
238
+ singleChannelStrategy: payload.singleChannelStrategy,
239
+ })
240
+ : await createCommDefinition(payload);
241
+ if (isDuplicateReferenceIdError(res)) {
242
+ // Duplicate referenceId in this org — block the save so the user can
243
+ // change it, rather than silently proceeding without a CCS comm.
244
+ setSaveError(formatMessage(messages.duplicateReferenceIdError));
245
+ return;
246
+ }
247
+ const data = res?.response?.data;
248
+ if (existingCommDefinitionId) {
249
+ if (data) {
250
+ ccsCommDefinition = {
251
+ id: existingCommDefinitionId,
252
+ referenceId: referenceId || data.referenceId,
253
+ version: data.version?.version ?? data.version ?? 1,
254
+ status: data.status,
201
255
  };
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);
256
+ }
257
+ } else if (data?.id) {
258
+ ccsCommDefinition = {
259
+ id: data.id,
260
+ referenceId: data.referenceId,
261
+ version: data.version?.version ?? 1,
262
+ status: data.status,
263
+ };
213
264
  }
214
265
  } catch (error) {
215
- console.error('[CommunicationFlow] CCS createCentralCommsMetaId error:', error);
266
+ console.error('[CommunicationFlow] CCS createCommDefinition error:', error);
216
267
  }
268
+ } else if (!name) {
269
+ console.warn('[CommunicationFlow] Skipping CCS createCommDefinition — config.context.name is required');
270
+ } else if (isMultiChannel) {
271
+ console.warn('[CommunicationFlow] Skipping CCS createCommDefinition — CHANNEL_PRIORITY/AB_TEST strategies are not supported by CCS create yet (SINGLE only)');
272
+ } else if (!contentItem) {
273
+ console.warn('[CommunicationFlow] Skipping CCS createCommDefinition — no content item found (contentItems is empty)');
217
274
  }
218
275
  }
219
276
 
220
- onSave(aggregatedData);
277
+ onSave(ccsCommDefinition ? { ...aggregatedData, ccsCommDefinition } : aggregatedData);
221
278
  }, [getAggregatedData, config, onSave]);
222
279
 
223
280
  // Call onChange callback when stepData changes
@@ -317,6 +374,11 @@ const CommunicationFlow = ({
317
374
  {renderSteps()}
318
375
  {onSave && (
319
376
  <CapRow useLegacy className="communication-flow-container__footer">
377
+ {saveError && (
378
+ <CapLabel type="label2" className="communication-flow-container__save-error">
379
+ {saveError}
380
+ </CapLabel>
381
+ )}
320
382
  <CapButton type="primary" onClick={handleSave} disabled={isSaveDisabled}>
321
383
  {formatMessage(messages.save)}
322
384
  </CapButton>
@@ -137,7 +137,7 @@
137
137
  }
138
138
 
139
139
  &__footer {
140
- padding: $CAP_SPACE_16 0 $CAP_SPACE_08;
140
+ padding: 4.142rem 0 $CAP_SPACE_08;
141
141
  }
142
142
  }
143
143
 
@@ -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 {
@@ -41,7 +42,7 @@ const resolveValue = (val) => {
41
42
 
42
43
  const SENDER_ID_RESOLVERS = {
43
44
  [SMS]: (setting) => resolveValue(setting?.gsmSenderId),
44
- [EMAIL]: (setting) => resolveValue(setting?.senderEmail),
45
+ [EMAIL]: (setting) => resolveValue(setting?.senderId),
45
46
  [VIBER]: (setting) => resolveValue(setting?.sender) || resolveValue(setting?.gsmSenderId),
46
47
  [WHATSAPP]: (setting) => resolveValue(setting?.senderMobNum),
47
48
  [RCS]: (setting) => resolveValue(setting?.senderMobNum) || resolveValue(setting?.rcsSender),
@@ -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>
@@ -196,7 +208,7 @@ const CommunicationFlowCard = ({
196
208
  <CapSlideBox
197
209
  show={showSlideBox}
198
210
  handleClose={handleClose}
199
- size="size-xl"
211
+ size="size-l"
200
212
  header={(
201
213
  <CapHeader
202
214
  title={formatMessage(messages.addMessage)}
@@ -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);