@capillarytech/creatives-library 9.0.56-alpha.0 → 9.0.56-alpha.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "9.0.56-alpha.0",
4
+ "version": "9.0.56-alpha.2",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
package/services/api.js CHANGED
@@ -713,6 +713,12 @@ export const editCommDefinition = (commDefinitionId, payload) => {
713
713
  return request(url, getAPICallObject('POST', payload, false, false, false, true));
714
714
  };
715
715
 
716
+ // Sends a real transactional comm via CCS (notify/ui's "Send test" action uses this instead of the legacy createMessageMeta/sendTestMessage flow — see CommonTestAndPreview's handleSendTestMessage).
717
+ export const sendTransactionalComm = (payload) => {
718
+ const url = `${API_ENDPOINT}/comm-definitions/send/transaction`;
719
+ return request(url, getAPICallObject('POST', payload, false, false, false, true));
720
+ };
721
+
716
722
  export const getCentralCommsMetaIds = (metaIds, metaType = TRANSACTION) => {
717
723
  const url = `${API_ENDPOINT}/common/central-comms/meta-id/${metaType}?metaIds=${metaIds}`;
718
724
  return request(url, getAPICallObject('GET', null, false, false, false, true));
@@ -208,6 +208,8 @@ const CommonTestAndPreview = (props) => {
208
208
  wecrmAccounts = [],
209
209
  isLoadingSenderDetails = false,
210
210
  orgUnitId = -1,
211
+ // notify/ui only: { commDefinitionId, referenceId } — when present, "Send test" calls CCS's send/transaction endpoint directly instead of the legacy createMessageMeta/sendTestMessage flow.
212
+ ccsSendTransaction,
211
213
  // Email-specific props
212
214
  beeInstance,
213
215
  currentTab = 1,
@@ -231,6 +233,7 @@ const CommonTestAndPreview = (props) => {
231
233
  const [smsFallbackOptionalTags, setSmsFallbackOptionalTags] = useState([]);
232
234
  const [isExtractingSmsFallbackTags, setIsExtractingSmsFallbackTags] = useState(false);
233
235
  const [customValues, setCustomValues] = useState({});
236
+ const [isSendingCcsTest, setIsSendingCcsTest] = useState(false);
234
237
  const previewCustomValuesRef = useRef({});
235
238
  const [showJSON, setShowJSON] = useState(false);
236
239
  const [tagsExtracted, setTagsExtracted] = useState(false);
@@ -3625,7 +3628,52 @@ const CommonTestAndPreview = (props) => {
3625
3628
  /**
3626
3629
  * Handle send test message
3627
3630
  */
3631
+ // notify/ui: send the test through CCS's own send/transaction endpoint against the alert's
3632
+ // real CommDefinition, instead of the legacy createMessageMeta/sendTestMessage flow below.
3633
+ const handleCcsSendTestMessage = async () => {
3634
+ const allUserIds = [];
3635
+ selectedTestEntities.forEach((entityId) => {
3636
+ const group = testGroups.find((testGroup) => testEntityIdsEqual(testGroup.groupId, entityId));
3637
+ if (group) {
3638
+ allUserIds.push(...group.userIds);
3639
+ } else {
3640
+ allUserIds.push(entityId);
3641
+ }
3642
+ });
3643
+ const uniqueUserIds = [...new Set(allUserIds)];
3644
+ const { commDefinitionId, referenceId } = ccsSendTransaction;
3645
+
3646
+ setIsSendingCcsTest(true);
3647
+ try {
3648
+ const res = await Api.sendTransactionalComm({
3649
+ ...(commDefinitionId ? { commDefinitionId } : { referenceId }),
3650
+ uniqueKey: `test_${Date.now()}`,
3651
+ isTest: true,
3652
+ recipient: {
3653
+ identifiers: uniqueUserIds.map((userId) => ({ type: 'USER_ID', value: String(userId) })),
3654
+ tagValues: customValues,
3655
+ },
3656
+ });
3657
+ if (res?.response?.success === false) {
3658
+ throw new Error(res?.response?.errors?.[0]?.message || 'Failed to send test message');
3659
+ }
3660
+ CapNotification.success({
3661
+ message: formatMessage(messages.testMessageSent),
3662
+ });
3663
+ } catch (error) {
3664
+ CapNotification.error({
3665
+ message: formatMessage(messages.testMessageFailed),
3666
+ });
3667
+ } finally {
3668
+ setIsSendingCcsTest(false);
3669
+ }
3670
+ };
3671
+
3628
3672
  const handleSendTestMessage = () => {
3673
+ if (ccsSendTransaction) {
3674
+ handleCcsSendTestMessage();
3675
+ return;
3676
+ }
3629
3677
  const allUserIds = [];
3630
3678
  selectedTestEntities.forEach((entityId) => {
3631
3679
  const group = testGroups.find((testGroup) => testEntityIdsEqual(testGroup.groupId, entityId));
@@ -3763,7 +3811,7 @@ const CommonTestAndPreview = (props) => {
3763
3811
  formData={formDataForSendTest}
3764
3812
  content={getCurrentContent}
3765
3813
  channel={channel}
3766
- isSendingTestMessage={isSendingTestMessage}
3814
+ isSendingTestMessage={isSendingTestMessage || isSendingCcsTest}
3767
3815
  renderAddTestCustomerButton={renderAddTestCustomerButton}
3768
3816
  formatMessage={formatMessage}
3769
3817
  deliverySettings={testPreviewDeliverySettings[channel]}
@@ -3918,6 +3966,10 @@ CommonTestAndPreview.propTypes = {
3918
3966
  wecrmAccounts: PropTypes.array,
3919
3967
  isLoadingSenderDetails: PropTypes.bool,
3920
3968
  orgUnitId: PropTypes.number,
3969
+ ccsSendTransaction: PropTypes.shape({
3970
+ commDefinitionId: PropTypes.string,
3971
+ referenceId: PropTypes.string,
3972
+ }),
3921
3973
 
3922
3974
  // Email-specific props
3923
3975
  beeInstance: PropTypes.object,
@@ -3957,6 +4009,7 @@ CommonTestAndPreview.defaultProps = {
3957
4009
  wecrmAccounts: [],
3958
4010
  isLoadingSenderDetails: false,
3959
4011
  orgUnitId: -1,
4012
+ ccsSendTransaction: null,
3960
4013
  };
3961
4014
 
3962
4015
  // ============================================
@@ -108,6 +108,11 @@ TestAndPreviewSlidebox.propTypes = {
108
108
  wecrmAccounts: PropTypes.array,
109
109
  isLoadingSenderDetails: PropTypes.bool,
110
110
  orgUnitId: PropTypes.number,
111
+ /** notify/ui only — see CommonTestAndPreview's handleSendTestMessage. */
112
+ ccsSendTransaction: PropTypes.shape({
113
+ commDefinitionId: PropTypes.string,
114
+ referenceId: PropTypes.string,
115
+ }),
111
116
  };
112
117
 
113
118
  TestAndPreviewSlidebox.defaultProps = {
@@ -125,6 +130,7 @@ TestAndPreviewSlidebox.defaultProps = {
125
130
  isLoadingSenderDetails: false,
126
131
  orgUnitId: -1,
127
132
  smsFallbackContent: null,
133
+ ccsSendTransaction: null,
128
134
  };
129
135
 
130
136
  const mapStateToProps = createStructuredSelector({
@@ -70,18 +70,6 @@ const hasDeliverySettingForChannel = (channelSetting, channel) => {
70
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.
71
71
  const isDuplicateReferenceIdError = (res) => res?.success === false && res?.status?.message === 'REFERENCE_ID_EXISTS';
72
72
 
73
- /**
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.
75
- */
76
- const buildCcsReferenceId = (name) => {
77
- const slug = (name || 'COMM')
78
- .trim()
79
- .toUpperCase()
80
- .replace(/[^A-Z0-9]+/g, '_')
81
- .replace(/^_+|_+$/g, '') || 'COMM';
82
- return `${slug}_${Date.now()}`;
83
- };
84
-
85
73
  const CommunicationFlow = ({
86
74
  config,
87
75
  initialData,
@@ -192,12 +180,15 @@ const CommunicationFlow = ({
192
180
  const contentPayload = contentTransform ? contentTransform(contentItem.templateData) : contentItem.templateData;
193
181
  const { dynamicControls = {} } = aggregatedData;
194
182
  const channelSettings = aggregatedData.deliverySetting?.channelSetting?.[channel] || {};
195
- const referenceId = config?.context?.referenceId || buildCcsReferenceId(name);
183
+ // referenceId/description are user-editable, optional fields (up until Send for
184
+ // approval) — sent as '' rather than auto-generated/omitted when left blank.
185
+ const referenceId = config?.context?.referenceId || '';
186
+ const description = config?.context?.description || '';
196
187
 
197
188
  const payload = {
198
189
  referenceId,
199
190
  name,
200
- description: config?.context?.description || undefined,
191
+ description,
201
192
  strategyType: CCS_STRATEGY_TYPE_SINGLE,
202
193
  settings: {
203
194
  additionalSettings: {
@@ -224,6 +215,8 @@ const CommunicationFlow = ({
224
215
  try {
225
216
  const res = existingCommDefinitionId
226
217
  ? await editCommDefinition(existingCommDefinitionId, {
218
+ referenceId: payload.referenceId,
219
+ description: payload.description,
227
220
  strategyType: payload.strategyType,
228
221
  settings: payload.settings,
229
222
  singleChannelStrategy: payload.singleChannelStrategy,
@@ -328,6 +321,8 @@ const CommunicationFlow = ({
328
321
  onChange={(data) => handleStepChange(step, data)}
329
322
  channelsToHide={contentTemplateData.channelsToHide}
330
323
  channelsToDisable={contentTemplateData.channelsToDisable}
324
+ disablePreviewAndTest={!!contentTemplateData.disablePreviewAndTest}
325
+ autoOpenExistingContent={!!contentTemplateData.autoOpenExistingContent}
331
326
  creativesMode={config.mode || 'create'}
332
327
  selectedOfferDetails={stepData.selectedOfferDetails}
333
328
  incentivesData={{
@@ -415,6 +410,8 @@ CommunicationFlow.propTypes = {
415
410
  contentTemplateData: PropTypes.shape({
416
411
  required: PropTypes.bool,
417
412
  channels: PropTypes.object,
413
+ disablePreviewAndTest: PropTypes.bool,
414
+ autoOpenExistingContent: PropTypes.bool,
418
415
  }),
419
416
  enableIncentives: PropTypes.bool,
420
417
  enableDeliverySettings: PropTypes.bool,
@@ -613,10 +613,7 @@ describe('handleSave — CCS flow', () => {
613
613
  );
614
614
  });
615
615
 
616
- it('generates a referenceId from name when config.context.referenceId is absent', async () => {
617
- // moduleMocks.js spies on Date.now globally, but this config's resetMocks:true
618
- // clears that return value before every test — set it explicitly here.
619
- jest.spyOn(Date, 'now').mockReturnValue(1612267539410);
616
+ it('sends an empty referenceId/description when config.context does not supply them', async () => {
620
617
  renderWithFlow({
621
618
  features: {},
622
619
  config: ccsConfig,
@@ -627,7 +624,8 @@ describe('handleSave — CCS flow', () => {
627
624
 
628
625
  await waitFor(() => expect(createCommDefinition).toHaveBeenCalled());
629
626
  const payload = createCommDefinition.mock.calls[0][0];
630
- expect(payload.referenceId).toBe('ORDER_PLACED_NOTIFICATION_1612267539410');
627
+ expect(payload.referenceId).toBe('');
628
+ expect(payload.description).toBe('');
631
629
  });
632
630
 
633
631
  it('uses config.context.referenceId and description when provided', async () => {
@@ -820,7 +818,7 @@ describe('handleSave — CCS flow', () => {
820
818
  features: {},
821
819
  config: {
822
820
  ...ccsConfig,
823
- context: { ...ccsConfig.context, referenceId: 'ORDER_PLACED', existingCommDefinitionId: 'cd_existing_1' },
821
+ context: { ...ccsConfig.context, referenceId: 'ORDER_PLACED', description: 'desc', existingCommDefinitionId: 'cd_existing_1' },
824
822
  },
825
823
  initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
826
824
  onSave,
@@ -832,6 +830,8 @@ describe('handleSave — CCS flow', () => {
832
830
  expect(editCommDefinition).toHaveBeenCalledWith(
833
831
  'cd_existing_1',
834
832
  expect.objectContaining({
833
+ referenceId: 'ORDER_PLACED',
834
+ description: 'desc',
835
835
  strategyType: 'SINGLE',
836
836
  settings: expect.anything(),
837
837
  singleChannelStrategy: expect.anything(),
@@ -4,7 +4,9 @@
4
4
  * Content template selection with channel dropdown
5
5
  */
6
6
 
7
- import React, { useState, useCallback, useMemo } from 'react';
7
+ import React, {
8
+ useState, useCallback, useMemo, useEffect, useRef,
9
+ } from 'react';
8
10
  import PropTypes from 'prop-types';
9
11
  import { injectIntl } from 'react-intl';
10
12
  import CapButton from '@capillarytech/cap-ui-library/CapButton';
@@ -45,6 +47,8 @@ const ChannelSelectionStep = ({
45
47
  onChange,
46
48
  channelsToHide = [],
47
49
  channelsToDisable = [],
50
+ disablePreviewAndTest = false,
51
+ autoOpenExistingContent = false,
48
52
  creativesMode = 'create',
49
53
  selectedOfferDetails = [],
50
54
  incentivesData,
@@ -117,6 +121,21 @@ const ChannelSelectionStep = ({
117
121
  }
118
122
  }, [contentItems]);
119
123
 
124
+ // Editing an existing single-channel alert (e.g. CapNotify's rejected-alert edit page) should
125
+ // land straight in that channel's template editor, not the one-card content list — there's
126
+ // only ever one item under the SINGLE strategy, so the list is just an extra click. Runs once
127
+ // on mount only; the ref guard keeps a later content-item change (e.g. after closing/re-saving)
128
+ // from re-triggering the auto-open.
129
+ const hasAutoOpenedRef = useRef(false);
130
+ useEffect(() => {
131
+ if (hasAutoOpenedRef.current) return;
132
+ hasAutoOpenedRef.current = true;
133
+ if (autoOpenExistingContent && contentItems.length > 0) {
134
+ handleEditContent(contentItems[0].contentId);
135
+ }
136
+ // eslint-disable-next-line react-hooks/exhaustive-deps
137
+ }, []);
138
+
120
139
  const handleDeleteContent = useCallback((contentId) => {
121
140
  onChange({ contentItems: contentItems.filter((c) => c.contentId !== contentId) });
122
141
  }, [contentItems, onChange]);
@@ -358,7 +377,7 @@ const ChannelSelectionStep = ({
358
377
  >
359
378
  {formatMessage(messages.edit)}
360
379
  </CapMenu.Item>
361
- {!PREVIEW_TEST_UNSUPPORTED_CHANNELS.includes(item?.channel?.toUpperCase()) && (
380
+ {!disablePreviewAndTest && !PREVIEW_TEST_UNSUPPORTED_CHANNELS.includes(item?.channel?.toUpperCase()) && (
362
381
  <CapMenu.Item
363
382
  id="preview-menu-item"
364
383
  className="ant-dropdown-menu-item"
@@ -551,6 +570,8 @@ ChannelSelectionStep.propTypes = {
551
570
  onChange: PropTypes.func.isRequired,
552
571
  channelsToHide: PropTypes.array,
553
572
  channelsToDisable: PropTypes.array,
573
+ disablePreviewAndTest: PropTypes.bool,
574
+ autoOpenExistingContent: PropTypes.bool,
554
575
  creativesMode: PropTypes.oneOf(['create', 'edit', 'preview']),
555
576
  selectedOfferDetails: PropTypes.array,
556
577
  incentivesData: PropTypes.shape({
@@ -572,6 +593,8 @@ ChannelSelectionStep.defaultProps = {
572
593
  error: null,
573
594
  channelsToHide: [],
574
595
  channelsToDisable: [],
596
+ disablePreviewAndTest: false,
597
+ autoOpenExistingContent: false,
575
598
  creativesMode: 'create',
576
599
  selectedOfferDetails: [],
577
600
  incentivesData: null,