@capillarytech/creatives-library 8.0.316-alpha.3 → 8.0.316-alpha.4

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.
Files changed (105) hide show
  1. package/constants/unified.js +14 -0
  2. package/package.json +1 -1
  3. package/utils/templateVarUtils.js +172 -0
  4. package/utils/tests/templateVarUtils.test.js +160 -0
  5. package/v2Components/CapTagList/index.js +10 -0
  6. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -49
  7. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  8. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +207 -21
  9. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  10. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
  11. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  12. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  13. package/v2Components/CommonTestAndPreview/SendTestMessage.js +11 -5
  14. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +20 -1
  15. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +133 -4
  16. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +12 -0
  17. package/v2Components/CommonTestAndPreview/constants.js +38 -0
  18. package/v2Components/CommonTestAndPreview/index.js +693 -155
  19. package/v2Components/CommonTestAndPreview/messages.js +41 -3
  20. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  21. package/v2Components/CommonTestAndPreview/sagas.js +15 -6
  22. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +172 -0
  23. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +269 -1
  24. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  25. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +245 -0
  26. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +25 -4
  27. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +100 -1
  28. package/v2Components/CommonTestAndPreview/tests/index.test.js +19 -1
  29. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  30. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +2 -2
  31. package/v2Components/FormBuilder/index.js +7 -1
  32. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +87 -0
  33. package/v2Components/SmsFallback/constants.js +73 -0
  34. package/v2Components/SmsFallback/index.js +956 -0
  35. package/v2Components/SmsFallback/index.scss +265 -0
  36. package/v2Components/SmsFallback/messages.js +78 -0
  37. package/v2Components/SmsFallback/smsFallbackUtils.js +107 -0
  38. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  39. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  40. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  41. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +197 -0
  42. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +261 -0
  43. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +327 -0
  44. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  45. package/v2Components/TestAndPreviewSlidebox/index.js +8 -1
  46. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  47. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  48. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  49. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  50. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  51. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  52. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  53. package/v2Containers/CreativesContainer/SlideBoxFooter.js +10 -1
  54. package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  55. package/v2Containers/CreativesContainer/constants.js +9 -0
  56. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +67 -0
  57. package/v2Containers/CreativesContainer/index.js +286 -93
  58. package/v2Containers/CreativesContainer/index.scss +51 -1
  59. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  60. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  61. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -10
  62. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  63. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  64. package/v2Containers/Rcs/constants.js +32 -1
  65. package/v2Containers/Rcs/index.js +950 -873
  66. package/v2Containers/Rcs/index.scss +85 -6
  67. package/v2Containers/Rcs/messages.js +10 -1
  68. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +205 -0
  69. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +40834 -1963
  70. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  71. package/v2Containers/Rcs/tests/index.test.js +41 -38
  72. package/v2Containers/Rcs/tests/mockData.js +38 -0
  73. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +251 -0
  74. package/v2Containers/Rcs/tests/utils.test.js +379 -1
  75. package/v2Containers/Rcs/utils.js +358 -10
  76. package/v2Containers/Sms/Create/index.js +81 -36
  77. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  78. package/v2Containers/SmsTrai/Create/index.js +9 -4
  79. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  80. package/v2Containers/SmsTrai/Edit/index.js +609 -128
  81. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  82. package/v2Containers/SmsTrai/Edit/messages.js +9 -4
  83. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4327 -2374
  84. package/v2Containers/SmsWrapper/index.js +37 -8
  85. package/v2Containers/TagList/index.js +6 -0
  86. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  87. package/v2Containers/Templates/_templates.scss +61 -2
  88. package/v2Containers/Templates/actions.js +11 -0
  89. package/v2Containers/Templates/constants.js +2 -0
  90. package/v2Containers/Templates/index.js +90 -40
  91. package/v2Containers/Templates/sagas.js +57 -12
  92. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  93. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1043 -1079
  94. package/v2Containers/Templates/tests/sagas.test.js +110 -12
  95. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  96. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  97. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  98. package/v2Containers/TemplatesV2/index.js +86 -23
  99. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  100. package/v2Containers/WebPush/Create/components/MessageSection.js +54 -18
  101. package/v2Containers/WebPush/Create/components/MessageSection.test.js +28 -0
  102. package/v2Containers/WebPush/Create/components/__snapshots__/MessageSection.test.js.snap +7 -3
  103. package/v2Containers/Whatsapp/index.js +7 -23
  104. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
  105. package/v2Containers/Whatsapp/tests/index.test.js +172 -0
@@ -10,8 +10,8 @@ import { connect } from 'react-redux';
10
10
  import { injectIntl, intlShape, FormattedMessage } from 'react-intl';
11
11
  import { createStructuredSelector } from 'reselect';
12
12
  import { bindActionCreators, compose } from 'redux';
13
- import { CapTab, CapCustomCard, CapButton, CapHeader, CapSpin, CapIcon, CapTooltip } from '@capillarytech/cap-ui-library';
14
- import { find, get } from 'lodash';
13
+ import { CapTab, CapCustomCard, CapButton, CapHeader, CapIcon, CapSpin, CapTooltip } from '@capillarytech/cap-ui-library';
14
+ import { find, get, pick } from 'lodash';
15
15
  import Helmet from 'react-helmet';
16
16
 
17
17
  import { UserIsAuthenticated } from '../../utils/authWrapper';
@@ -36,13 +36,14 @@ import { makeSelectAuthenticated, selectCurrentOrgDetails } from "../../v2Contai
36
36
  import {
37
37
  CALL_TASK,
38
38
  COMMON_CHANNELS,
39
+ LOCAL_TEMPLATE_CONFIG_KEYS_FOR_PICK,
39
40
  LOYALTY_SUPPORTED_ACTION,
40
41
  MOBILE_PUSH,
41
42
  NORMALIZED_CHANNEL_ALIASES,
42
43
  SMS,
43
44
  } from "../CreativesContainer/constants";
44
45
 
45
- const {CapCustomCardList} = CapCustomCard;
46
+ const { CapCustomCardList } = CapCustomCard;
46
47
 
47
48
  const StyledCapTab = withStyles(CapTab, CapTabStyle);
48
49
  export class TemplatesV2 extends React.Component { // eslint-disable-line react/prefer-stateless-function
@@ -119,9 +120,9 @@ export class TemplatesV2 extends React.Component { // eslint-disable-line react/
119
120
  return !normalizedChannelsToHideSet.has(paneKey);
120
121
  });
121
122
 
122
- if (isFullMode) {
123
- filteredPanes.push({ content: <div></div>, tab: intl.formatMessage(messages.gallery), key: 'assets' });
124
- } else {
123
+ if (isFullMode && !normalizedChannelsToHideSet.has(normalizeChannel(ASSETS))) {
124
+ filteredPanes.push({ content: <div></div>, tab: intl.formatMessage(messages.gallery), key: ASSETS });
125
+ } else if (!isFullMode) {
125
126
  // Add special-mode panes only when not hidden (use normalized checks)
126
127
  if (!normalizedChannelsToHideSet.has(CALL_TASK.toLowerCase())) {
127
128
  filteredPanes.push({ content: <div></div>, tab: intl.formatMessage(messages.callTask), key: CALL_TASK.toLowerCase() });
@@ -222,7 +223,8 @@ export class TemplatesV2 extends React.Component { // eslint-disable-line react/
222
223
  this.setState({selectedChannel: nextProps.channel, panes });
223
224
  }
224
225
  }
225
- getTemplateDataForGrid = ({templates, handlers, filterContent, channel, isLoading, loadingTip}) => {
226
+
227
+ getTemplateDataForGrid = ({ templates, handlers, filterContent, channel, isLoading, loadingTip }) => {
226
228
  const currentChannel = channel.toUpperCase();
227
229
  const cardDataList = templates.map((template) => {
228
230
  const templateData =
@@ -248,7 +250,8 @@ export class TemplatesV2 extends React.Component { // eslint-disable-line react/
248
250
  </CapSpin>
249
251
 
250
252
  </div>);
251
- }
253
+ };
254
+
252
255
  getGalleryComponent = (location) => <Gallery location={location} isFullMode={this.props.isFullMode}/>
253
256
  getCallTaskComponent = () => (
254
257
  <CallTask
@@ -312,6 +315,29 @@ export class TemplatesV2 extends React.Component { // eslint-disable-line react/
312
315
  if (messageStrategy !== "X_ENGAGE" && channel === 'facebook' && !isFullMode) {
313
316
  return this.getFacebookComponent();
314
317
  }
318
+ const localConfig = this.props.localTemplatesConfig || pick(this.props, LOCAL_TEMPLATE_CONFIG_KEYS_FOR_PICK);
319
+ const useLocalTemplates = localConfig.useLocalTemplates;
320
+ if (useLocalTemplates && channel === (this.props.channel || 'sms')) {
321
+ // Reuse full Templates component (same UI as Redux flow) with local data only
322
+ const location = { pathname: `/${channel}`, search: '', query: !this.props.isFullMode ? { type: 'embedded', module: 'library' } : {} };
323
+ return (
324
+ <Templates
325
+ key={`${channel}-local`}
326
+ location={location}
327
+ route={{ name: channel }}
328
+ router={this.props.router}
329
+ isFullMode={this.props.isFullMode}
330
+ createNew={this.props.createNew}
331
+ onSelectTemplate={this.props.onSelectTemplate}
332
+ handlePeviewTemplate={this.props.handlePeviewTemplate}
333
+ messageStrategy={this.props.messageStrategy}
334
+ smsRegister={this.props.smsRegister}
335
+ hideTestAndPreviewBtn={this.props.hideTestAndPreviewBtn}
336
+ localTemplatesConfig={localConfig}
337
+ />
338
+ );
339
+ }
340
+
315
341
  const location = {pathname: `/${channel}`, search: '', query};
316
342
  switch (channel) {
317
343
  case 'call_task':
@@ -361,29 +387,55 @@ export class TemplatesV2 extends React.Component { // eslint-disable-line react/
361
387
  }
362
388
  render() {
363
389
  const { isFullMode, className, cap = {}, Global = {}} = this.props;
390
+ const useLocalTemplates = get(this.props, 'localTemplatesConfig.useLocalTemplates', false);
364
391
  const { accessiblePermissions = []} = cap.user || Global.user || {};
365
392
  let isCreativeAccessible = true;
366
393
  if (!accessiblePermissions.includes(CREATIVES_UI_VIEW)) {
367
394
  isCreativeAccessible = false;
368
395
  }
396
+ // Recompute active pane content every render so local-list mode updates
397
+ // (templates/loading/search UI) are not stuck with the initial cached pane.
398
+ const panes = this.setChannelContent(this.state.selectedChannel, this.state.panes);
399
+ const hideChannelTabsForLocalSms = useLocalTemplates && panes.length === 1;
400
+ const activeLocalPane = hideChannelTabsForLocalSms
401
+ ? (panes.find(
402
+ (p) => String(p.key).toLowerCase() === String(this.state.selectedChannel).toLowerCase(),
403
+ ) || panes[0])
404
+ : null;
369
405
  return (
370
406
  !isCreativeAccessible ? <AccessForbidden /> : (
371
- <div className={`${className} creatives-templates-container ${isFullMode ? 'fullmode' : 'library-mode'}`} data-testid="cap-wrapper">
372
- {isFullMode && <Helmet
373
- title={this.props.intl.formatMessage(messages.creatives)}
374
- meta={[
375
- { name: 'description', content: this.props.intl.formatMessage(messages.creativesDesc) },
376
- ]}
377
- />}
378
- <div className="component-wrapper">
379
- {isFullMode && <CapHeader title={<FormattedMessage {...messages.creatives}/>} description={<FormattedMessage {...messages.creativesDesc}/>}/>}
380
- <StyledCapTab
381
- panes={this.state.panes}
382
- onChange={this.channelChange}
383
- activeKey={this.state.selectedChannel}
384
- defaultActiveKey={this.state.selectedChannel}
385
- isFullMode={isFullMode}
407
+ <div
408
+ className={`${className} creatives-templates-container ${isFullMode ? 'fullmode' : 'library-mode'}${useLocalTemplates ? ' creatives-templates-container--local-sms' : ''}`}
409
+ data-testid="cap-wrapper"
410
+ >
411
+ {isFullMode && !useLocalTemplates && (
412
+ <Helmet
413
+ title={this.props.intl.formatMessage(messages.creatives)}
414
+ meta={[
415
+ { name: 'description', content: this.props.intl.formatMessage(messages.creativesDesc) },
416
+ ]}
386
417
  />
418
+ )}
419
+ <div className="component-wrapper">
420
+ {isFullMode && (
421
+ <CapHeader
422
+ title={<FormattedMessage {...messages.creatives} />}
423
+ {...(!useLocalTemplates && {
424
+ description: <FormattedMessage {...messages.creativesDesc} />,
425
+ })}
426
+ />
427
+ )}
428
+ {hideChannelTabsForLocalSms ? (
429
+ <div className="templates-v2-local-sms-pane">{activeLocalPane?.content}</div>
430
+ ) : (
431
+ <StyledCapTab
432
+ panes={panes}
433
+ onChange={this.channelChange}
434
+ activeKey={this.state.selectedChannel}
435
+ defaultActiveKey={this.state.selectedChannel}
436
+ isFullMode={isFullMode}
437
+ />
438
+ )}
387
439
  </div>
388
440
  </div>
389
441
  )
@@ -415,6 +467,17 @@ TemplatesV2.propTypes = {
415
467
  currentOrgDetails: PropTypes.object,
416
468
  restrictPersonalization: PropTypes.bool,
417
469
  isAnonymousType: PropTypes.bool,
470
+ // Optional: reuse grid UI with local template list (e.g. SMS fallback). Pass object or same keys as individual props.
471
+ localTemplatesConfig: PropTypes.shape({
472
+ useLocalTemplates: PropTypes.bool,
473
+ localTemplates: PropTypes.arrayOf(PropTypes.object),
474
+ localTemplatesLoading: PropTypes.bool,
475
+ localTemplatesLoadingTip: PropTypes.string,
476
+ localTemplatesFilterContent: PropTypes.node,
477
+ localTemplatesFooterContent: PropTypes.node,
478
+ localTemplatesOnPageChange: PropTypes.func,
479
+ localTemplatesUseSkeleton: PropTypes.bool,
480
+ }),
418
481
  };
419
482
 
420
483
  TemplatesV2.defaultProps = {
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Embedded SMS template list: localTemplatesConfig + SMS-only channel visibility (RCS SMS fallback).
3
+ */
4
+ import React from 'react';
5
+ import { injectIntl } from 'react-intl';
6
+ import '@testing-library/jest-dom';
7
+ import cloneDeep from 'lodash/cloneDeep';
8
+ import { Provider } from 'react-redux';
9
+ import { configureStore } from '@capillarytech/vulcan-react-sdk/utils';
10
+ import history from '../../../utils/history';
11
+ import { initialReducer } from '../../../initialReducer';
12
+ import { render, screen } from '../../../utils/test-utils';
13
+ import { TemplatesV2 } from '../index';
14
+ import { Templates, authData, currentOrgDetails as currentOrgDetailsMock } from './mockData';
15
+ import { CHANNELS_TO_HIDE_FOR_SMS_ONLY } from '../../../v2Components/SmsFallback/constants';
16
+
17
+ const mockTemplates = jest.fn(() => <div data-testid="templates-mock">Templates</div>);
18
+ jest.mock('v2Containers/Templates', () => ({
19
+ __esModule: true,
20
+ default: (props) => mockTemplates(props),
21
+ }));
22
+
23
+ jest.mock('../../../utils/authWrapper', () => ({
24
+ UserIsAuthenticated: jest.fn((config) => config),
25
+ }));
26
+
27
+ const ComponentToRender = injectIntl(TemplatesV2);
28
+ const renderComponent = (p) => {
29
+ const store = configureStore({}, initialReducer, history);
30
+ return render(
31
+ <Provider store={store}>
32
+ <ComponentToRender {...p} />
33
+ </Provider>,
34
+ );
35
+ };
36
+
37
+ describe('TemplatesV2 local SMS templates (embedded)', () => {
38
+ const templateActions = {
39
+ templateActions: jest.fn(),
40
+ deleteTemplate: jest.fn(),
41
+ getAccountsSettings: jest.fn(),
42
+ getAllTemplates: jest.fn(),
43
+ getCdnTransformationConfig: jest.fn(),
44
+ getDefaultBeeTemplates: jest.fn(),
45
+ getSenderDetails: jest.fn(),
46
+ getTemplateDetails: jest.fn(),
47
+ getUserList: jest.fn(),
48
+ getWeCrmAccounts: jest.fn(),
49
+ handleHtmlUpload: jest.fn(),
50
+ handleZipUpload: jest.fn(),
51
+ resetAccount: jest.fn(),
52
+ resetTemplate: jest.fn(),
53
+ resetTemplateData: jest.fn(),
54
+ resetTemplateStoreData: jest.fn(),
55
+ resetUploadData: jest.fn(),
56
+ setBEETemplate: jest.fn(),
57
+ setChannelAccount: jest.fn(),
58
+ setEdmTemplate: jest.fn(),
59
+ setFacebookAccount: jest.fn(),
60
+ setViberAccount: jest.fn(),
61
+ setWeChatAccount: jest.fn(),
62
+ };
63
+
64
+ const baseProps = {
65
+ cap: {
66
+ user: { accessiblePermissions: ['CREATIVES_UI_VIEW'] },
67
+ },
68
+ actions: { defaultAction: jest.fn(), getTemplates: jest.fn() },
69
+ Templates,
70
+ TemplatesList: Templates?.templates,
71
+ authData,
72
+ templateActions,
73
+ isFullMode: false,
74
+ className: 'embed-test',
75
+ channel: 'sms',
76
+ channelsToHide: CHANNELS_TO_HIDE_FOR_SMS_ONLY,
77
+ channelsToDisable: [],
78
+ onChannelChange: jest.fn(),
79
+ enableNewChannels: [],
80
+ /** Without JP_LOCALE_HIDE_FEATURE so SMS panes are not stripped to Email/Line/Gallery only */
81
+ currentOrgDetails: {
82
+ ...currentOrgDetailsMock,
83
+ accessibleFeatures: (currentOrgDetailsMock.accessibleFeatures || []).filter(
84
+ (f) => f !== 'JP_LOCALE_HIDE_FEATURE',
85
+ ),
86
+ },
87
+ location: {
88
+ pathname: 'v2',
89
+ basename: '/creatives/ui/',
90
+ query: {},
91
+ },
92
+ router: { push: jest.fn() },
93
+ };
94
+
95
+ beforeEach(() => {
96
+ mockTemplates.mockClear();
97
+ });
98
+
99
+ it('adds local-sms container class and single-pane layout when only SMS is visible', () => {
100
+ const p = cloneDeep(baseProps);
101
+ p.localTemplatesConfig = {
102
+ useLocalTemplates: true,
103
+ localTemplates: [],
104
+ localTemplatesLoading: false,
105
+ };
106
+ renderComponent(p);
107
+
108
+ const wrapper = screen.getByTestId('cap-wrapper');
109
+ expect(wrapper).toHaveClass('creatives-templates-container--local-sms');
110
+ expect(document.querySelector('.templates-v2-local-sms-pane')).toBeTruthy();
111
+ expect(mockTemplates).toHaveBeenCalled();
112
+ });
113
+
114
+ it('passes localTemplatesConfig into Templates for the SMS pane', () => {
115
+ const localConfig = {
116
+ useLocalTemplates: true,
117
+ localTemplates: [{ _id: '1', name: 'A' }],
118
+ localTemplatesLoading: false,
119
+ };
120
+ const p = cloneDeep(baseProps);
121
+ p.localTemplatesConfig = localConfig;
122
+ renderComponent(p);
123
+
124
+ expect(mockTemplates).toHaveBeenCalled();
125
+ const passed = mockTemplates.mock.calls.find(
126
+ (call) => call[0] && call[0].localTemplatesConfig && call[0].localTemplatesConfig.useLocalTemplates,
127
+ );
128
+ expect(passed).toBeTruthy();
129
+ expect(passed[0].localTemplatesConfig).toMatchObject(localConfig);
130
+ });
131
+ });
@@ -12,6 +12,52 @@ import CapAskAira from '@capillarytech/cap-ui-library/CapAskAira';
12
12
  import { MESSAGE_MAX_LENGTH, SHOW_CHARACTER_COUNT } from '../../constants';
13
13
  import { useAiraTriggerPosition } from '../hooks/useAiraTriggerPosition';
14
14
 
15
+ const InputWrapper = ({
16
+ children,
17
+ inputRef,
18
+ error,
19
+ value,
20
+ onChange,
21
+ isAiContentBotDisabled,
22
+ }) => {
23
+ const wrapperRef = useRef(null);
24
+ const airaRootStyle = useAiraTriggerPosition({ wrapperRef, inputRef, error });
25
+
26
+ return (
27
+ <div className="webpush-message-input-wrapper" ref={wrapperRef}>
28
+ {children}
29
+ {!isAiContentBotDisabled && (
30
+ <CapAskAira.ContentGenerationBot
31
+ text={value || ''}
32
+ setText={(text) => onChange(text)}
33
+ iconPlacement="float-br"
34
+ iconSize="1.6rem"
35
+ rootStyle={airaRootStyle}
36
+ />
37
+ )}
38
+ </div>
39
+ );
40
+ };
41
+
42
+ InputWrapper.propTypes = {
43
+ children: PropTypes.node.isRequired,
44
+ inputRef: PropTypes.oneOfType([
45
+ PropTypes.shape({ current: PropTypes.any }),
46
+ PropTypes.func,
47
+ ]),
48
+ error: PropTypes.string,
49
+ value: PropTypes.string,
50
+ onChange: PropTypes.func.isRequired,
51
+ isAiContentBotDisabled: PropTypes.bool,
52
+ };
53
+
54
+ InputWrapper.defaultProps = {
55
+ inputRef: null,
56
+ error: '',
57
+ value: '',
58
+ isAiContentBotDisabled: false,
59
+ };
60
+
15
61
  /**
16
62
  * MessageSection component - Message textarea with tags, emoji picker, and character count
17
63
  */
@@ -27,13 +73,6 @@ export const MessageSection = ({
27
73
  handleMessageTextAreaRef,
28
74
  isAiContentBotDisabled,
29
75
  }) => {
30
- const messageInputWrapperRef = useRef(null);
31
- const airaRootStyle = useAiraTriggerPosition({
32
- wrapperRef: messageInputWrapperRef,
33
- inputRef: messageTextAreaRef,
34
- error,
35
- });
36
-
37
76
  const renderCharacterCount = () => {
38
77
  if (!SHOW_CHARACTER_COUNT) return null;
39
78
 
@@ -60,7 +99,13 @@ export const MessageSection = ({
60
99
  <CapHeading type="h4" className="webpush-message">
61
100
  <FormattedMessage {...messages.message} />
62
101
  </CapHeading>
63
- <div className="webpush-message-input-wrapper" ref={messageInputWrapperRef}>
102
+ <InputWrapper
103
+ inputRef={messageTextAreaRef}
104
+ error={error}
105
+ value={value}
106
+ onChange={onChange}
107
+ isAiContentBotDisabled={isAiContentBotDisabled}
108
+ >
64
109
  <CapEmojiPicker.Wrapper
65
110
  value={value}
66
111
  onChange={onChange}
@@ -84,16 +129,7 @@ export const MessageSection = ({
84
129
  }
85
130
  />
86
131
  </CapEmojiPicker.Wrapper>
87
- {!isAiContentBotDisabled && (
88
- <CapAskAira.ContentGenerationBot
89
- text={value || ''}
90
- setText={(text) => onChange(text)}
91
- iconPlacement="float-br"
92
- iconSize="1.6rem"
93
- rootStyle={airaRootStyle}
94
- />
95
- )}
96
- </div>
132
+ </InputWrapper>
97
133
  {renderCharacterCount()}
98
134
  </CapRow>
99
135
  <CapDivider className="webpush-message-divider" />
@@ -318,5 +318,33 @@ describe('MessageSection', () => {
318
318
  expect(textArea.prop('value')).toBe(longValue);
319
319
  });
320
320
  });
321
+
322
+ describe('InputWrapper', () => {
323
+ it('should render the wrapper div with correct class', () => {
324
+ const wrapper = mountWithIntl(<MessageSection {...defaultProps} />);
325
+ const inputWrapper = wrapper.find('.webpush-message-input-wrapper');
326
+ expect(inputWrapper.exists()).toBe(true);
327
+ });
328
+
329
+ it('should render children inside InputWrapper', () => {
330
+ const wrapper = mountWithIntl(<MessageSection {...defaultProps} />);
331
+ const inputWrapper = wrapper.find('.webpush-message-input-wrapper');
332
+ expect(inputWrapper.find(CapEmojiPicker.Wrapper).exists()).toBe(true);
333
+ });
334
+
335
+ it('should not render aiRA bot inside InputWrapper when disabled', () => {
336
+ const wrapper = mountWithIntl(<MessageSection {...defaultProps} isAiContentBotDisabled />);
337
+ const inputWrapper = wrapper.find('.webpush-message-input-wrapper');
338
+ expect(inputWrapper.find(CapAskAira.ContentGenerationBot).exists()).toBe(false);
339
+ });
340
+
341
+ it('should render aiRA bot inside InputWrapper when enabled', () => {
342
+ const wrapper = mountWithIntl(
343
+ <MessageSection {...defaultProps} isAiContentBotDisabled={false} />
344
+ );
345
+ const inputWrapper = wrapper.find('.webpush-message-input-wrapper');
346
+ expect(inputWrapper.find(CapAskAira.ContentGenerationBot).exists()).toBe(true);
347
+ });
348
+ });
321
349
  });
322
350
 
@@ -18,8 +18,12 @@ exports[`MessageSection Rendering should render correctly with default props 1`]
18
18
  values={Object {}}
19
19
  />
20
20
  </CapHeading>
21
- <div
22
- className="webpush-message-input-wrapper"
21
+ <InputWrapper
22
+ error=""
23
+ inputRef={null}
24
+ isAiContentBotDisabled={true}
25
+ onChange={[MockFunction]}
26
+ value="Test Message"
23
27
  >
24
28
  <InjectIntl(Wrapper)
25
29
  onChange={[MockFunction]}
@@ -43,7 +47,7 @@ exports[`MessageSection Rendering should render correctly with default props 1`]
43
47
  value="Test Message"
44
48
  />
45
49
  </InjectIntl(Wrapper)>
46
- </div>
50
+ </InputWrapper>
47
51
  <CapLabel
48
52
  className="webpush-character-count"
49
53
  type="label2"
@@ -118,6 +118,7 @@ import { ANDROID } from '../../v2Components/CommonTestAndPreview/constants';
118
118
  import CapImageUpload from '../../v2Components/CapImageUpload';
119
119
  import TagList from '../TagList';
120
120
  import { validateTags } from '../../utils/tagValidations';
121
+ import { splitContentByOrderedVarTokens } from '../../utils/templateVarUtils';
121
122
  import { capitalizeString } from '../../utils/Formatter';
122
123
  import CapWhatsappCTA from '../../v2Components/CapWhatsappCTA';
123
124
  import {
@@ -482,28 +483,10 @@ export const Whatsapp = (props) => {
482
483
  );
483
484
  };
484
485
 
485
- const converStringToVarArr = (validVarArr, content) => {
486
- const templateVarArray = [];
487
- while (content?.length !== 0) {
488
- //converting content string to an array split at var
489
- const index = content.indexOf(validVarArr?.[0]);
490
- if (index !== -1) {
491
- templateVarArray.push(content.substring(0, index)); //push string before var
492
- templateVarArray.push(validVarArr?.[0]); //push var
493
- content = content.substring(index + validVarArr?.[0]?.length, content?.length); //remaining str
494
- validVarArr?.shift(); //remove considered var
495
- } else {
496
- templateVarArray.push(content); //remaining str
497
- break;
498
- }
499
- }
500
- return templateVarArray;
501
- }
502
-
503
486
  const computeTextMessage = (msg, varMap, regex) => {
504
487
  const validVarArr = msg?.match(regex) || [];
505
488
  //conerting msg string to variable arr
506
- const templateHeaderArray = converStringToVarArr(validVarArr, msg);
489
+ const templateHeaderArray = splitContentByOrderedVarTokens(validVarArr, msg);
507
490
  if (templateHeaderArray?.length !== 0) {
508
491
  let clonedVarMap = {};
509
492
  if (!isEmpty(varMap)) {
@@ -557,7 +540,7 @@ export const Whatsapp = (props) => {
557
540
  setUnsubscribeRequired(true);
558
541
  }
559
542
  //converting msg string to variable arr
560
- const templateMessageArray = converStringToVarArr(validVarArr, msg);
543
+ const templateMessageArray = splitContentByOrderedVarTokens(validVarArr, msg);
561
544
  updateTempMsgArray(templateMessageArray.filter((i) => i === 0 || i));
562
545
  };
563
546
 
@@ -2957,12 +2940,13 @@ const isAuthenticationTemplate = isEqual(templateCategory, WHATSAPP_CATEGORIES.a
2957
2940
  };
2958
2941
 
2959
2942
  const isEditDoneDisabled = () => {
2943
+ const isBlankInput = (inputValue) => !String(inputValue ?? '').trim();
2960
2944
  let carouselDisableCheck = false;
2961
2945
  if (isMediaTypeCarousel) {
2962
2946
  carouselDisableCheck = carouselData.some((data) => {
2963
2947
  return (
2964
2948
  data.carouselTagValidationErr ||
2965
- Object.values(data.varMap).some((inputValue) => inputValue === "") ||
2949
+ Object.values(data.varMap).some((inputValue) => isBlankInput(inputValue)) ||
2966
2950
  computeTextLength(CAROUSEL_TEXT, data) > TEMPLATE_MESSAGE_MAX_LENGTH ||
2967
2951
  (carouselMediaType === IMAGE.toLowerCase() && !data.imageSrc) ||
2968
2952
  (carouselMediaType === VIDEO.toLowerCase() && !data.videoSrc) ||
@@ -2972,8 +2956,8 @@ const isAuthenticationTemplate = isEqual(templateCategory, WHATSAPP_CATEGORIES.a
2972
2956
  }
2973
2957
  return (isTagValidationError ||
2974
2958
  isHeaderTagValidationError ||
2975
- Object.values(varMap).some((inputValue) => inputValue === "") ||
2976
- Object.values(headerVarMappedData).some((inputValue) => inputValue === "") ||
2959
+ Object.values(varMap).some((inputValue) => isBlankInput(inputValue)) ||
2960
+ Object.values(headerVarMappedData).some((inputValue) => isBlankInput(inputValue)) ||
2977
2961
  computeTextLength(MESSAGE_TEXT) > TEMPLATE_MESSAGE_MAX_LENGTH ||
2978
2962
  computeTextLength(HEADER_TEXT) > TEMPLATE_HEADER_MAX_LENGTH ||
2979
2963
  (isBtnTypeCta && ctaData.some((btn) => btn?.url?.includes("{{1}}"))) || isMediatypeValid()) || carouselDisableCheck;