@capillarytech/creatives-library 9.0.57 → 9.0.58

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.57",
4
+ "version": "9.0.58",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -16,7 +16,7 @@ import { createStructuredSelector } from 'reselect';
16
16
  import CapRow from '@capillarytech/cap-ui-library/CapRow';
17
17
  import CapDivider from '@capillarytech/cap-ui-library/CapDivider';
18
18
  import CapButton from '@capillarytech/cap-ui-library/CapButton';
19
- import CapLabel from '@capillarytech/cap-ui-library/CapLabel';
19
+ import CapNotification from '@capillarytech/cap-ui-library/CapNotification';
20
20
  import injectReducer from '../../utils/injectReducer';
21
21
  import { makeSelectAuthenticated } from '../Cap/selectors';
22
22
  import capReducer from '../Cap/reducer';
@@ -91,7 +91,6 @@ const CommunicationFlow = ({
91
91
  };
92
92
  });
93
93
  const [validationErrors, setValidationErrors] = useState({});
94
- const [saveError, setSaveError] = useState(null);
95
94
 
96
95
  // Memoize enabled steps
97
96
  const enabledSteps = useMemo(() => getEnabledSteps(config), [config]);
@@ -151,7 +150,6 @@ const CommunicationFlow = ({
151
150
  }, []);
152
151
 
153
152
  const handleSave = useCallback(async () => {
154
- setSaveError(null);
155
153
  const aggregatedData = getAggregatedData();
156
154
  const shouldUseCCS = config?.useCCS !== false;
157
155
  let ccsCommDefinition = null;
@@ -222,11 +220,11 @@ const CommunicationFlow = ({
222
220
  })
223
221
  : await createCommDefinition(payload);
224
222
  if (res?.isError) {
225
- setSaveError(formatMessage(messages.genericSaveError));
223
+ CapNotification.error({ message: formatMessage(messages.genericSaveError) });
226
224
  return;
227
225
  }
228
226
  if (isDuplicateReferenceIdError(res)) {
229
- setSaveError(formatMessage(messages.duplicateReferenceIdError));
227
+ CapNotification.error({ message: formatMessage(messages.duplicateReferenceIdError) });
230
228
  return;
231
229
  }
232
230
  const data = res?.response?.data;
@@ -239,7 +237,7 @@ const CommunicationFlow = ({
239
237
  status: data.status,
240
238
  };
241
239
  } else {
242
- setSaveError(formatMessage(messages.genericSaveError));
240
+ CapNotification.error({ message: formatMessage(messages.genericSaveError) });
243
241
  return;
244
242
  }
245
243
  } else if (data?.id) {
@@ -250,12 +248,12 @@ const CommunicationFlow = ({
250
248
  status: data.status,
251
249
  };
252
250
  } else {
253
- setSaveError(formatMessage(messages.genericSaveError));
251
+ CapNotification.error({ message: formatMessage(messages.genericSaveError) });
254
252
  return;
255
253
  }
256
254
  } catch (error) {
257
255
  console.error('[CommunicationFlow] CCS createCommDefinition error:', error);
258
- setSaveError(formatMessage(messages.genericSaveError));
256
+ CapNotification.error({ message: formatMessage(messages.genericSaveError) });
259
257
  return;
260
258
  }
261
259
  } else if (!name) {
@@ -377,11 +375,6 @@ const CommunicationFlow = ({
377
375
  {renderSteps()}
378
376
  {onSave && (
379
377
  <CapRow useLegacy className="communication-flow-container__footer">
380
- {saveError && (
381
- <CapLabel type="label2" className="communication-flow-container__save-error">
382
- {saveError}
383
- </CapLabel>
384
- )}
385
378
  <CapButton type="primary" onClick={handleSave} disabled={isSaveDisabled}>
386
379
  {formatMessage(messages.save)}
387
380
  </CapButton>
@@ -5,6 +5,13 @@ jest.mock('../../../services/api', () => ({
5
5
  editCommDefinition: jest.fn(),
6
6
  }));
7
7
 
8
+ jest.mock('@capillarytech/cap-ui-library/CapNotification', () => ({
9
+ error: jest.fn(),
10
+ success: jest.fn(),
11
+ warning: jest.fn(),
12
+ info: jest.fn(),
13
+ }));
14
+
8
15
  jest.mock('../../CreativesContainer', () => function MockCreativesContainer({
9
16
  getCreativesData,
10
17
  handleCloseCreatives,
@@ -38,6 +45,7 @@ import { IntlProvider } from 'react-intl';
38
45
  import history from '../../../utils/history';
39
46
  import { initialReducer } from '../../../initialReducer';
40
47
  import CommunicationFlow from '../CommunicationFlow';
48
+ import CapNotification from '@capillarytech/cap-ui-library/CapNotification';
41
49
  import { createCommDefinition, editCommDefinition } from '../../../services/api';
42
50
  import { getEnabledSteps } from '../utils/getEnabledSteps';
43
51
  import {
@@ -705,7 +713,7 @@ describe('handleSave — CCS flow', () => {
705
713
 
706
714
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
707
715
 
708
- await waitFor(() => expect(screen.getByText('Something went wrong while saving your content. Please try again.')).toBeInTheDocument());
716
+ await waitFor(() => expect(CapNotification.error).toHaveBeenCalledWith({ message: 'Something went wrong while saving your content. Please try again.' }));
709
717
  expect(onSave).not.toHaveBeenCalled();
710
718
  });
711
719
 
@@ -721,7 +729,7 @@ describe('handleSave — CCS flow', () => {
721
729
 
722
730
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
723
731
 
724
- await waitFor(() => expect(screen.getByText('Something went wrong while saving your content. Please try again.')).toBeInTheDocument());
732
+ await waitFor(() => expect(CapNotification.error).toHaveBeenCalledWith({ message: 'Something went wrong while saving your content. Please try again.' }));
725
733
  expect(onSave).not.toHaveBeenCalled();
726
734
  });
727
735
 
@@ -737,7 +745,7 @@ describe('handleSave — CCS flow', () => {
737
745
 
738
746
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
739
747
 
740
- await waitFor(() => expect(screen.getByText('Something went wrong while saving your content. Please try again.')).toBeInTheDocument());
748
+ await waitFor(() => expect(CapNotification.error).toHaveBeenCalledWith({ message: 'Something went wrong while saving your content. Please try again.' }));
741
749
  expect(onSave).not.toHaveBeenCalled();
742
750
  });
743
751
 
@@ -758,7 +766,9 @@ describe('handleSave — CCS flow', () => {
758
766
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
759
767
 
760
768
  await waitFor(() => expect(createCommDefinition).toHaveBeenCalledTimes(1));
761
- expect(await screen.findByText(/already in use in your organization/i)).toBeInTheDocument();
769
+ await waitFor(() => expect(CapNotification.error).toHaveBeenCalledWith({
770
+ message: 'This reference ID is already in use in your organization. Please use a different reference ID.',
771
+ }));
762
772
  expect(onSave).not.toHaveBeenCalled();
763
773
  });
764
774
 
@@ -904,7 +914,9 @@ describe('handleSave — CCS flow', () => {
904
914
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
905
915
 
906
916
  await waitFor(() => expect(editCommDefinition).toHaveBeenCalledTimes(1));
907
- expect(await screen.findByText(/already in use in your organization/i)).toBeInTheDocument();
917
+ await waitFor(() => expect(CapNotification.error).toHaveBeenCalledWith({
918
+ message: 'This reference ID is already in use in your organization. Please use a different reference ID.',
919
+ }));
908
920
  expect(onSave).not.toHaveBeenCalled();
909
921
  });
910
922
  });
@@ -79,7 +79,7 @@ describe('MobilePush/Edit — onContentValidityChange reporting', () => {
79
79
  );
80
80
  });
81
81
 
82
- it('treats content as empty when the iOS tab (tabCount 2) is cleared even if Android has content', () => {
82
+ it('treats content as present when the iOS tab (tabCount 2) is cleared but Android has content (single-device template allowed)', () => {
83
83
  const onContentValidityChange = jest.fn();
84
84
  const wrapper = shallow(<Edit {...baseProps()} onContentValidityChange={onContentValidityChange} />);
85
85
  wrapper.setState({
@@ -90,6 +90,22 @@ describe('MobilePush/Edit — onContentValidityChange reporting', () => {
90
90
  tabCount: 2,
91
91
  });
92
92
 
93
+ expect(onContentValidityChange).toHaveBeenCalledWith(
94
+ expect.objectContaining({ isContentEmpty: false }),
95
+ );
96
+ });
97
+
98
+ it('treats content as empty when both Android and iOS tabs (tabCount 2) are cleared', () => {
99
+ const onContentValidityChange = jest.fn();
100
+ const wrapper = shallow(<Edit {...baseProps()} onContentValidityChange={onContentValidityChange} />);
101
+ wrapper.setState({
102
+ formData: {
103
+ 0: { 'message-title': '', 'message-editor': '' },
104
+ 1: { 'message-title2': '', 'message-editor2': '' },
105
+ },
106
+ tabCount: 2,
107
+ });
108
+
93
109
  expect(onContentValidityChange).toHaveBeenCalledWith(
94
110
  expect.objectContaining({ isContentEmpty: true }),
95
111
  );
@@ -309,24 +309,23 @@ function getMobilePushTabContent(tabIndex, tabData) {
309
309
  * Live "is content empty" check for the legacy Mobile Push Edit/Create forms — the
310
310
  * Mobile Push analogue of Sms's `getSmsEmbeddedFooterValidity` (see
311
311
  * app/v2Containers/Sms/smsFormDataHelpers.js). Checks every active tab (Android, and
312
- * iOS when tabCount > 1); if any active tab's title+message are both empty, the
313
- * overall content is considered empty so Done/Preview-and-test can be disabled.
312
+ * iOS when tabCount > 1); Mobile Push allows saving with just one active platform
313
+ * filled in, so the overall content is empty only when EVERY active tab's
314
+ * title+message are both empty.
314
315
  * @param {object} formData FormBuilder state (same shape as this.state.formData)
315
316
  * @param {number} [tabCount] Total number of active tabs (1 = Android only, 2 = Android + iOS)
316
317
  * @returns {{ isContentEmpty: boolean }}
317
318
  */
318
319
  function getMobilePushEmbeddedContentValidity(formData, tabCount) {
319
320
  const count = tabCount != null && tabCount > 1 ? tabCount : 1;
320
- let isContentEmpty = false;
321
321
  for (let i = 0; i < count; i++) {
322
322
  const content = getMobilePushTabContent(i, formData?.[i]);
323
323
  const trimmed = content != null && content !== '' ? String(content).trim() : '';
324
- if (!trimmed) {
325
- isContentEmpty = true;
326
- break;
324
+ if (trimmed) {
325
+ return { isContentEmpty: false };
327
326
  }
328
327
  }
329
- return { isContentEmpty };
328
+ return { isContentEmpty: true };
330
329
  }
331
330
 
332
331
  export {
@@ -1758,9 +1758,14 @@ export const MobilePushNew = ({
1758
1758
  onPersonalizationTokensChange,
1759
1759
  ]);
1760
1760
 
1761
- // Validation logic for template creation/update
1761
+ // Validation logic for template creation/update.
1762
1762
  const isAndroidFieldsMissing = isPlatformFieldsMissing(isAndroidSupported, androidContent);
1763
1763
  const isIosFieldsMissing = isPlatformFieldsMissing(isIosSupported, iosContent);
1764
+ const isMobilePushContentMissing = (
1765
+ (isAndroidSupported || isIosSupported)
1766
+ && (!isAndroidSupported || isAndroidFieldsMissing)
1767
+ && (!isIosSupported || isIosFieldsMissing)
1768
+ );
1764
1769
 
1765
1770
  // Ref to dedupe reporting of content emptiness to the parent, so we only
1766
1771
  // call onContentValidityChange when the computed value actually changes.
@@ -1770,19 +1775,10 @@ export const MobilePushNew = ({
1770
1775
  // supported platforms) changes, mirroring the Save-button gating logic.
1771
1776
  useEffect(() => {
1772
1777
  if (typeof onContentValidityChange !== 'function') return;
1773
- const isContentEmpty = isAndroidFieldsMissing || isIosFieldsMissing;
1774
- if (lastReportedIsContentEmptyRef.current === isContentEmpty) return;
1775
- lastReportedIsContentEmptyRef.current = isContentEmpty;
1776
- onContentValidityChange({ isContentEmpty });
1777
- }, [
1778
- androidContent,
1779
- iosContent,
1780
- isAndroidSupported,
1781
- isIosSupported,
1782
- isAndroidFieldsMissing,
1783
- isIosFieldsMissing,
1784
- onContentValidityChange,
1785
- ]);
1778
+ if (lastReportedIsContentEmptyRef.current === isMobilePushContentMissing) return;
1779
+ lastReportedIsContentEmptyRef.current = isMobilePushContentMissing;
1780
+ onContentValidityChange({ isContentEmpty: isMobilePushContentMissing });
1781
+ }, [isMobilePushContentMissing, onContentValidityChange]);
1786
1782
 
1787
1783
  // Add changeSourceRef for debounced sync
1788
1784
  const changeSourceRef = useRef(null);
@@ -2201,28 +2197,24 @@ export const MobilePushNew = ({
2201
2197
  const errorInTitle = activeTab === ANDROID ? androidTitleError : iosTitleError;
2202
2198
  const errorInMessage = activeTab === ANDROID ? androidMessageError : iosMessageError;
2203
2199
 
2204
- // Save button disabled logic: only check enabled platforms
2200
+ // Save button disabled logic: only check enabled platforms, and allow
2201
+ // saving with just one supported platform filled in (see isMobilePushContentMissing).
2205
2202
  const isSaveDisabled = (
2206
- (isAndroidSupported && (!androidContent?.title?.trim() || !androidContent?.message?.trim()))
2207
- || (isIosSupported && (!iosContent?.title?.trim() || !iosContent?.message?.trim()))
2203
+ isMobilePushContentMissing
2208
2204
  || templateNameError
2209
2205
  || Object.values(carouselLinkErrors).some((error) => error !== null && error !== "")
2210
2206
  || !isCarouselDataValid()
2211
2207
  || errorInTitle || errorInMessage
2212
2208
  );
2213
2209
 
2214
- // Validation in handleSave: only show errors for enabled platforms
2210
+ // Validation in handleSave: only enforce content on enabled platforms, and
2211
+ // allow saving with just one supported platform filled in.
2215
2212
  const handleSave = useCallback(() => {
2216
- if (isAndroidSupported && (!androidContent?.title?.trim() || !androidContent?.message?.trim())) {
2213
+ if (isMobilePushContentMissing) {
2217
2214
  CapNotification.error({
2218
- message: formatMessage(messages.androidValidationError),
2219
- });
2220
- if (onValidationFail) onValidationFail();
2221
- return;
2222
- }
2223
- if (isIosSupported && (!iosContent?.title?.trim() || !iosContent?.message?.trim())) {
2224
- CapNotification.error({
2225
- message: formatMessage(messages.iosValidationError),
2215
+ message: formatMessage(
2216
+ isAndroidSupported ? messages.androidValidationError : messages.iosValidationError,
2217
+ ),
2226
2218
  });
2227
2219
  if (onValidationFail) onValidationFail();
2228
2220
  return;
@@ -2583,8 +2575,7 @@ export const MobilePushNew = ({
2583
2575
  createTimeoutRef,
2584
2576
  isAndroidSupported,
2585
2577
  isIosSupported,
2586
- isAndroidFieldsMissing,
2587
- isIosFieldsMissing,
2578
+ isMobilePushContentMissing,
2588
2579
  isCarouselDataValid,
2589
2580
  isFullMode,
2590
2581
  templateId,
@@ -459,12 +459,21 @@ describe("utils.js", () => {
459
459
  })).toBe(true);
460
460
  });
461
461
 
462
- it("returns true when both platforms are supported and one is missing fields", () => {
462
+ it("returns false when both platforms are supported and only one has valid fields (single-device template allowed)", () => {
463
463
  expect(computeIsContentEmpty({
464
464
  isAndroidSupported: true,
465
465
  isIosSupported: true,
466
466
  androidContent: { title: "t", message: "m" },
467
467
  iosContent: { title: "", message: "" },
468
+ })).toBe(false);
469
+ });
470
+
471
+ it("returns true when both platforms are supported and both are missing fields", () => {
472
+ expect(computeIsContentEmpty({
473
+ isAndroidSupported: true,
474
+ isIosSupported: true,
475
+ androidContent: { title: "", message: "" },
476
+ iosContent: { title: "", message: "" },
468
477
  })).toBe(true);
469
478
  });
470
479
 
@@ -97,8 +97,10 @@ export const isPlatformFieldsMissing = (isSupported, content) => (
97
97
 
98
98
  /**
99
99
  * Compute whether the Mobile Push content is empty/invalid for reporting to
100
- * a parent component (e.g. via onContentValidityChange). Content is
101
- * considered empty when any supported platform is missing required fields.
100
+ * a parent component (e.g. via onContentValidityChange). Mobile Push allows
101
+ * saving a template with just one supported platform filled in, so content
102
+ * is only considered empty when EVERY supported platform is missing its
103
+ * required fields.
102
104
  * @param {Object} params
103
105
  * @param {boolean} params.isAndroidSupported
104
106
  * @param {boolean} params.isIosSupported
@@ -111,7 +113,9 @@ export const computeIsContentEmpty = ({
111
113
  isIosSupported,
112
114
  androidContent,
113
115
  iosContent,
114
- }) => (
115
- isPlatformFieldsMissing(isAndroidSupported, androidContent)
116
- || isPlatformFieldsMissing(isIosSupported, iosContent)
117
- );
116
+ }) => {
117
+ if (!isAndroidSupported && !isIosSupported) return false;
118
+ const isAndroidMissing = isPlatformFieldsMissing(isAndroidSupported, androidContent);
119
+ const isIosMissing = isPlatformFieldsMissing(isIosSupported, iosContent);
120
+ return (!isAndroidSupported || isAndroidMissing) && (!isIosSupported || isIosMissing);
121
+ };