@capillarytech/creatives-library 8.0.268 → 8.0.270

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 (159) hide show
  1. package/constants/unified.js +1 -0
  2. package/package.json +1 -1
  3. package/services/api.js +5 -0
  4. package/utils/common.js +6 -0
  5. package/utils/imageUrlUpload.js +141 -0
  6. package/utils/tagValidations.js +2 -1
  7. package/utils/tests/transformerUtils.test.js +297 -0
  8. package/utils/transformerUtils.js +40 -0
  9. package/v2Components/CapImageUpload/constants.js +2 -0
  10. package/v2Components/CapImageUpload/index.js +65 -16
  11. package/v2Components/CapImageUpload/index.scss +4 -1
  12. package/v2Components/CapImageUpload/messages.js +5 -1
  13. package/v2Components/CapImageUrlUpload/constants.js +26 -0
  14. package/v2Components/CapImageUrlUpload/index.js +365 -0
  15. package/v2Components/CapImageUrlUpload/index.scss +35 -0
  16. package/v2Components/CapImageUrlUpload/messages.js +47 -0
  17. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +2 -2
  18. package/v2Components/CommonTestAndPreview/index.js +4 -15
  19. package/v2Components/FormBuilder/index.js +8 -8
  20. package/v2Containers/App/constants.js +5 -0
  21. package/v2Containers/CreativesContainer/SlideBoxContent.js +57 -2
  22. package/v2Containers/CreativesContainer/SlideBoxHeader.js +1 -0
  23. package/v2Containers/CreativesContainer/constants.js +3 -0
  24. package/v2Containers/CreativesContainer/index.js +168 -0
  25. package/v2Containers/CreativesContainer/messages.js +4 -0
  26. package/v2Containers/CreativesContainer/tests/SlideBoxContent.test.js +210 -0
  27. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +304 -0
  28. package/v2Containers/FTP/index.js +1 -1
  29. package/v2Containers/InApp/index.js +1 -0
  30. package/v2Containers/Line/Container/Text/index.js +1 -0
  31. package/v2Containers/MobilePushNew/index.js +1 -0
  32. package/v2Containers/Rcs/index.js +3 -0
  33. package/v2Containers/SmsTrai/Edit/index.js +2 -12
  34. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +36 -648
  35. package/v2Containers/Templates/ChannelTypeIllustration.js +13 -1
  36. package/v2Containers/Templates/_templates.scss +205 -0
  37. package/v2Containers/Templates/actions.js +2 -1
  38. package/v2Containers/Templates/constants.js +1 -0
  39. package/v2Containers/Templates/index.js +274 -34
  40. package/v2Containers/Templates/messages.js +24 -0
  41. package/v2Containers/Templates/reducer.js +2 -0
  42. package/v2Containers/Templates/tests/index.test.js +10 -0
  43. package/v2Containers/TemplatesV2/index.js +15 -7
  44. package/v2Containers/TemplatesV2/messages.js +4 -0
  45. package/v2Containers/Viber/index.js +1 -0
  46. package/v2Containers/WebPush/Create/components/BrandIconSection.js +108 -0
  47. package/v2Containers/WebPush/Create/components/ButtonForm.js +172 -0
  48. package/v2Containers/WebPush/Create/components/ButtonItem.js +101 -0
  49. package/v2Containers/WebPush/Create/components/ButtonList.js +145 -0
  50. package/v2Containers/WebPush/Create/components/ButtonsLinksSection.js +164 -0
  51. package/v2Containers/WebPush/Create/components/ButtonsLinksSection.test.js +463 -0
  52. package/v2Containers/WebPush/Create/components/FormActions.js +54 -0
  53. package/v2Containers/WebPush/Create/components/FormActions.test.js +163 -0
  54. package/v2Containers/WebPush/Create/components/MediaSection.js +142 -0
  55. package/v2Containers/WebPush/Create/components/MediaSection.test.js +341 -0
  56. package/v2Containers/WebPush/Create/components/MessageSection.js +103 -0
  57. package/v2Containers/WebPush/Create/components/MessageSection.test.js +268 -0
  58. package/v2Containers/WebPush/Create/components/NotificationTitleSection.js +87 -0
  59. package/v2Containers/WebPush/Create/components/NotificationTitleSection.test.js +210 -0
  60. package/v2Containers/WebPush/Create/components/TemplateNameSection.js +54 -0
  61. package/v2Containers/WebPush/Create/components/TemplateNameSection.test.js +143 -0
  62. package/v2Containers/WebPush/Create/components/__snapshots__/ButtonsLinksSection.test.js.snap +86 -0
  63. package/v2Containers/WebPush/Create/components/__snapshots__/FormActions.test.js.snap +16 -0
  64. package/v2Containers/WebPush/Create/components/__snapshots__/MediaSection.test.js.snap +41 -0
  65. package/v2Containers/WebPush/Create/components/__snapshots__/MessageSection.test.js.snap +54 -0
  66. package/v2Containers/WebPush/Create/components/__snapshots__/NotificationTitleSection.test.js.snap +37 -0
  67. package/v2Containers/WebPush/Create/components/__snapshots__/TemplateNameSection.test.js.snap +21 -0
  68. package/v2Containers/WebPush/Create/components/_buttons.scss +246 -0
  69. package/v2Containers/WebPush/Create/components/tests/ButtonForm.test.js +554 -0
  70. package/v2Containers/WebPush/Create/components/tests/ButtonItem.test.js +607 -0
  71. package/v2Containers/WebPush/Create/components/tests/ButtonList.test.js +633 -0
  72. package/v2Containers/WebPush/Create/components/tests/__snapshots__/ButtonForm.test.js.snap +666 -0
  73. package/v2Containers/WebPush/Create/components/tests/__snapshots__/ButtonItem.test.js.snap +74 -0
  74. package/v2Containers/WebPush/Create/components/tests/__snapshots__/ButtonList.test.js.snap +78 -0
  75. package/v2Containers/WebPush/Create/hooks/useButtonManagement.js +138 -0
  76. package/v2Containers/WebPush/Create/hooks/useButtonManagement.test.js +406 -0
  77. package/v2Containers/WebPush/Create/hooks/useCharacterCount.js +30 -0
  78. package/v2Containers/WebPush/Create/hooks/useCharacterCount.test.js +151 -0
  79. package/v2Containers/WebPush/Create/hooks/useImageUpload.js +104 -0
  80. package/v2Containers/WebPush/Create/hooks/useImageUpload.test.js +538 -0
  81. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +122 -0
  82. package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +633 -0
  83. package/v2Containers/WebPush/Create/index.js +1148 -0
  84. package/v2Containers/WebPush/Create/index.scss +134 -0
  85. package/v2Containers/WebPush/Create/messages.js +211 -0
  86. package/v2Containers/WebPush/Create/preview/DevicePreviewContent.js +228 -0
  87. package/v2Containers/WebPush/Create/preview/NotificationContainer.js +294 -0
  88. package/v2Containers/WebPush/Create/preview/PreviewContent.js +90 -0
  89. package/v2Containers/WebPush/Create/preview/PreviewControls.js +305 -0
  90. package/v2Containers/WebPush/Create/preview/PreviewDisclaimer.js +25 -0
  91. package/v2Containers/WebPush/Create/preview/WebPushPreview.js +156 -0
  92. package/v2Containers/WebPush/Create/preview/assets/Light.svg +53 -0
  93. package/v2Containers/WebPush/Create/preview/assets/Top.svg +5 -0
  94. package/v2Containers/WebPush/Create/preview/assets/android-arrow-down.svg +9 -0
  95. package/v2Containers/WebPush/Create/preview/assets/android-arrow-up.svg +9 -0
  96. package/v2Containers/WebPush/Create/preview/assets/chrome-icon.png +0 -0
  97. package/v2Containers/WebPush/Create/preview/assets/edge-icon.png +0 -0
  98. package/v2Containers/WebPush/Create/preview/assets/firefox-icon.svg +106 -0
  99. package/v2Containers/WebPush/Create/preview/assets/iOS.svg +26 -0
  100. package/v2Containers/WebPush/Create/preview/assets/macos-arrow-down-icon.svg +9 -0
  101. package/v2Containers/WebPush/Create/preview/assets/macos-triple-dot-icon.svg +9 -0
  102. package/v2Containers/WebPush/Create/preview/assets/opera-icon.svg +18 -0
  103. package/v2Containers/WebPush/Create/preview/assets/safari-icon.svg +29 -0
  104. package/v2Containers/WebPush/Create/preview/assets/windows-close-icon.svg +9 -0
  105. package/v2Containers/WebPush/Create/preview/assets/windows-triple-dot-icon.svg +9 -0
  106. package/v2Containers/WebPush/Create/preview/components/AndroidMobileChromeHeader.js +51 -0
  107. package/v2Containers/WebPush/Create/preview/components/AndroidMobileExpanded.js +145 -0
  108. package/v2Containers/WebPush/Create/preview/components/IOSHeader.js +45 -0
  109. package/v2Containers/WebPush/Create/preview/components/NotificationExpandedContent.js +68 -0
  110. package/v2Containers/WebPush/Create/preview/components/NotificationHeader.js +61 -0
  111. package/v2Containers/WebPush/Create/preview/components/WindowsChromeExpanded.js +99 -0
  112. package/v2Containers/WebPush/Create/preview/components/tests/AndroidMobileExpanded.test.js +733 -0
  113. package/v2Containers/WebPush/Create/preview/components/tests/WindowsChromeExpanded.test.js +571 -0
  114. package/v2Containers/WebPush/Create/preview/components/tests/__snapshots__/AndroidMobileExpanded.test.js.snap +85 -0
  115. package/v2Containers/WebPush/Create/preview/components/tests/__snapshots__/WindowsChromeExpanded.test.js.snap +81 -0
  116. package/v2Containers/WebPush/Create/preview/config/notificationMappings.js +50 -0
  117. package/v2Containers/WebPush/Create/preview/constants.js +637 -0
  118. package/v2Containers/WebPush/Create/preview/notification-container.scss +79 -0
  119. package/v2Containers/WebPush/Create/preview/preview.scss +358 -0
  120. package/v2Containers/WebPush/Create/preview/styles/_android-mobile-chrome.scss +370 -0
  121. package/v2Containers/WebPush/Create/preview/styles/_android-mobile-edge.scss +12 -0
  122. package/v2Containers/WebPush/Create/preview/styles/_android-mobile-firefox.scss +12 -0
  123. package/v2Containers/WebPush/Create/preview/styles/_android-mobile-opera.scss +12 -0
  124. package/v2Containers/WebPush/Create/preview/styles/_android-tablet-chrome.scss +47 -0
  125. package/v2Containers/WebPush/Create/preview/styles/_android-tablet-edge.scss +11 -0
  126. package/v2Containers/WebPush/Create/preview/styles/_android-tablet-firefox.scss +11 -0
  127. package/v2Containers/WebPush/Create/preview/styles/_android-tablet-opera.scss +11 -0
  128. package/v2Containers/WebPush/Create/preview/styles/_base.scss +207 -0
  129. package/v2Containers/WebPush/Create/preview/styles/_ios.scss +153 -0
  130. package/v2Containers/WebPush/Create/preview/styles/_ipados.scss +107 -0
  131. package/v2Containers/WebPush/Create/preview/styles/_macos-chrome.scss +101 -0
  132. package/v2Containers/WebPush/Create/preview/styles/_windows-chrome.scss +229 -0
  133. package/v2Containers/WebPush/Create/preview/tests/DevicePreviewContent.test.js +906 -0
  134. package/v2Containers/WebPush/Create/preview/tests/NotificationContainer.test.js +1081 -0
  135. package/v2Containers/WebPush/Create/preview/tests/PreviewControls.test.js +723 -0
  136. package/v2Containers/WebPush/Create/preview/tests/WebPushPreview.test.js +1327 -0
  137. package/v2Containers/WebPush/Create/preview/tests/__snapshots__/DevicePreviewContent.test.js.snap +131 -0
  138. package/v2Containers/WebPush/Create/preview/tests/__snapshots__/NotificationContainer.test.js.snap +112 -0
  139. package/v2Containers/WebPush/Create/preview/tests/__snapshots__/PreviewControls.test.js.snap +144 -0
  140. package/v2Containers/WebPush/Create/preview/tests/__snapshots__/WebPushPreview.test.js.snap +129 -0
  141. package/v2Containers/WebPush/Create/utils/payloadBuilder.js +96 -0
  142. package/v2Containers/WebPush/Create/utils/payloadBuilder.test.js +396 -0
  143. package/v2Containers/WebPush/Create/utils/previewUtils.js +89 -0
  144. package/v2Containers/WebPush/Create/utils/urlValidation.js +115 -0
  145. package/v2Containers/WebPush/Create/utils/urlValidation.test.js +449 -0
  146. package/v2Containers/WebPush/Create/utils/validation.js +76 -0
  147. package/v2Containers/WebPush/Create/utils/validation.test.js +283 -0
  148. package/v2Containers/WebPush/actions.js +60 -0
  149. package/v2Containers/WebPush/constants.js +132 -0
  150. package/v2Containers/WebPush/index.js +2 -0
  151. package/v2Containers/WebPush/reducer.js +104 -0
  152. package/v2Containers/WebPush/sagas.js +119 -0
  153. package/v2Containers/WebPush/selectors.js +65 -0
  154. package/v2Containers/WebPush/tests/reducer.test.js +863 -0
  155. package/v2Containers/WebPush/tests/sagas.test.js +566 -0
  156. package/v2Containers/WebPush/tests/selectors.test.js +960 -0
  157. package/v2Containers/Whatsapp/index.js +1 -0
  158. package/v2Containers/Zalo/index.js +1 -0
  159. package/v2Containers/Zalo/tests/index.test.js +1 -5
@@ -22,6 +22,7 @@ describe('Test Templates container', () => {
22
22
  const getAllTemplates = jest.fn();
23
23
  const getUserList = jest.fn();
24
24
  const getSenderDetails = jest.fn();
25
+ const resetTemplate = jest.fn();
25
26
  let renderedComponent;
26
27
 
27
28
  beforeEach(() => {
@@ -54,6 +55,7 @@ describe('Test Templates container', () => {
54
55
  getAllTemplates,
55
56
  getUserList,
56
57
  getSenderDetails,
58
+ resetTemplate,
57
59
  }}
58
60
  location={{
59
61
  pathname: `/${channel}`,
@@ -79,6 +81,8 @@ describe('Test Templates container', () => {
79
81
  channel: 'WHATSAPP',
80
82
  orgUnitId: -1,
81
83
  });
84
+ // resetTemplate should be called when entering account selection mode
85
+ expect(resetTemplate).toHaveBeenCalled();
82
86
  });
83
87
 
84
88
  it('Should render temlates when whatsapp templates are passed', () => {
@@ -103,6 +107,8 @@ describe('Test Templates container', () => {
103
107
  Templates: {},
104
108
  });
105
109
  expect(renderedComponent).toMatchSnapshot();
110
+ // SMS doesn't enter account selection mode, so resetTemplate shouldn't be called on mount
111
+ expect(resetTemplate).not.toHaveBeenCalled();
106
112
  });
107
113
 
108
114
  it('Should render temlates when whatsapp templates are passed in full mode', () => {
@@ -123,6 +129,8 @@ describe('Test Templates container', () => {
123
129
  it('Should render correct component for zalo channel', () => {
124
130
  RenderFunctionFor('zalo');
125
131
  expect(renderedComponent).toMatchSnapshot();
132
+ // resetTemplate should be called when entering account selection mode
133
+ expect(resetTemplate).toHaveBeenCalled();
126
134
  });
127
135
  it('Should render temlates when zalo templates are passed', () => {
128
136
  RenderFunctionFor('zalo');
@@ -201,6 +209,8 @@ describe('Test Templates container', () => {
201
209
  channel: 'RCS',
202
210
  orgUnitId: -1,
203
211
  });
212
+ // resetTemplate should be called when entering account selection mode
213
+ expect(resetTemplate).toHaveBeenCalled();
204
214
  });
205
215
 
206
216
  it('Should render templates when RCS templates are passed', () => {
@@ -30,11 +30,11 @@ import FTP from '../FTP';
30
30
  import Gallery from '../Assets/Gallery';
31
31
  import withStyles from '../../hoc/withStyles';
32
32
  import styles, { CapTabStyle } from './TemplatesV2.style';
33
- import { CREATIVES_UI_VIEW, LOYALTY, WHATSAPP, RCS, LINE, EMAIL, ASSETS, JP_LOCALE_HIDE_FEATURE, ZALO, INAPP } from '../App/constants';
33
+ import { CREATIVES_UI_VIEW, LOYALTY, WHATSAPP, RCS, LINE, EMAIL, ASSETS, JP_LOCALE_HIDE_FEATURE, ZALO, INAPP, WEBPUSH } from '../App/constants';
34
34
  import AccessForbidden from '../../v2Components/AccessForbidden';
35
35
  import { getObjFromQueryParams } from '../../utils/v2common';
36
36
  import { makeSelectAuthenticated, selectCurrentOrgDetails } from "../../v2Containers/Cap/selectors";
37
- import { LOYALTY_SUPPORTED_ACTION } from "../CreativesContainer/constants";
37
+ import { LOYALTY_SUPPORTED_ACTION, COMMON_CHANNELS } from "../CreativesContainer/constants";
38
38
 
39
39
  const {CapCustomCardList} = CapCustomCard;
40
40
 
@@ -65,6 +65,13 @@ export class TemplatesV2 extends React.Component { // eslint-disable-line react/
65
65
  email: {content: <></>, tab: intl.formatMessage(messages.email), key: 'email'},
66
66
  //'wechat': {content: this.getTemplatesComponent('wechat'), tab: 'Wechat', key: 'wechat'},
67
67
  mPush: {content: <></>, tab: intl.formatMessage(messages.pushNotification), key: 'mobilepush'},
68
+ ...(commonUtil.hasWebPushFeatureEnabled() ? {
69
+ webpush: {
70
+ content: <div></div>,
71
+ tab: intl.formatMessage(messages.webPush),
72
+ key: WEBPUSH,
73
+ }
74
+ } : {}),
68
75
  viber: {content: <></>, tab: intl.formatMessage(messages.viber), key: 'viber'},
69
76
  whatsapp: { content: <></>, tab: intl.formatMessage(messages.whatsapp), key: WHATSAPP },
70
77
  zalo: { content: <div></div>, tab: intl.formatMessage(messages.zalo), key: ZALO },
@@ -88,7 +95,7 @@ export class TemplatesV2 extends React.Component { // eslint-disable-line react/
88
95
  return obj;
89
96
  }, []);
90
97
 
91
- if (isFullMode ) {
98
+ if (isFullMode ) {
92
99
  filteredPanes.push({content: <div></div>, tab: intl.formatMessage(messages.gallery), key: 'assets'});
93
100
  } else {
94
101
  if (!channelsToHide.includes('callTask')) {
@@ -98,18 +105,19 @@ export class TemplatesV2 extends React.Component { // eslint-disable-line react/
98
105
  filteredPanes.push({content: <></>, tab: intl.formatMessage(messages.FTP), key: 'ftp'});
99
106
  defaultChannel = 'FTP';
100
107
  }
101
- const commonChannels = ['sms', 'email', 'wechat', 'mobilepush', 'line', 'viber', 'facebook', 'call_task', 'ftp', 'assets'];
102
108
 
109
+ // Create a local copy of COMMON_CHANNELS to avoid mutating the imported array
110
+ const channels = [...COMMON_CHANNELS];
103
111
  const { actionName = ''} = loyaltyMetaData;
104
112
  if (isLoyaltyModule && actionName === LOYALTY_SUPPORTED_ACTION) {
105
- commonChannels.push(WHATSAPP, ZALO);
113
+ channels.push(WHATSAPP, ZALO);
106
114
  }
107
115
 
108
- // we only show channels which other than commonChannels
116
+ // we only show channels which other than COMMON_CHANNELS
109
117
  // if it is coming in enableNewChannels array
110
118
  filteredPanes = filteredPanes.filter((item) => {
111
119
  const channel = item.key;
112
- if (!commonChannels.includes(channel)) {
120
+ if (!channels.includes(channel)) {
113
121
  return enableNewChannels.includes(channel.toUpperCase());
114
122
  }
115
123
  return true;
@@ -86,4 +86,8 @@ export default defineMessages({
86
86
  id: `creatives.containersV2.TemplatesV2.inapp`,
87
87
  defaultMessage: 'In app message',
88
88
  },
89
+ webPush: {
90
+ id: `creatives.containersV2.TemplatesV2.webPush`,
91
+ defaultMessage: 'Web Push',
92
+ },
89
93
  });
@@ -237,6 +237,7 @@ export const Viber = (props) => {
237
237
  injectedTagsParams: injectedTags,
238
238
  location,
239
239
  tagModule: 'outbound',
240
+ isFullMode,
240
241
  }) || {};
241
242
  if (value.trim() === '') {
242
243
  errorMessage = formatMessage(messages.emptyContentErrorMessage);
@@ -0,0 +1,108 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { FormattedMessage } from 'react-intl';
4
+ import CapRow from '@capillarytech/cap-ui-library/CapRow';
5
+ import CapHeading from '@capillarytech/cap-ui-library/CapHeading';
6
+ import CapRadioGroup from '@capillarytech/cap-ui-library/CapRadioGroup';
7
+ import CapDivider from '@capillarytech/cap-ui-library/CapDivider';
8
+ import CapImageUpload from '../../../../v2Components/CapImageUpload';
9
+ import { WEBPUSH_BRAND_ICON } from '../../../CreativesContainer/constants';
10
+ import {
11
+ BRAND_ICON_OPTIONS,
12
+ ALLOWED_IMAGE_EXTENSIONS_REGEX,
13
+ WEBPUSH_BRAND_ICON_SIZE,
14
+ WEBPUSH_BRAND_ICON_RECOMMENDED_DIMENSIONS,
15
+ } from '../../constants';
16
+
17
+ /**
18
+ * BrandIconSection component - Brand icon/logo upload options
19
+ */
20
+ export const BrandIconSection = ({
21
+ brandIconOption,
22
+ onBrandIconChange,
23
+ brandIconUpload,
24
+ isLocked,
25
+ isAnyUploadActive,
26
+ formatMessage,
27
+ messages,
28
+ webPush,
29
+ isFullMode,
30
+ }) => {
31
+ const {
32
+ imageSrc: brandIconSrc,
33
+ uploadAsset: uploadBrandIconAsset,
34
+ setUpdateImageSrc: setUpdateBrandIconSrc,
35
+ updateOnReUpload: updateOnBrandIconReUpload,
36
+ } = brandIconUpload;
37
+
38
+ const brandIconOptions = [
39
+ { value: BRAND_ICON_OPTIONS.DONT_SHOW, label: formatMessage(messages.dontShow) },
40
+ { value: BRAND_ICON_OPTIONS.UPLOAD_IMAGE, label: formatMessage(messages.uploadImage) },
41
+ // NOTE: Commented out due to technical blocker - "Add image URL" option for Brand icon/logo
42
+ // { value: BRAND_ICON_OPTIONS.ADD_IMAGE_URL, label: formatMessage(messages.addImageUrl) },
43
+ ];
44
+
45
+ return (
46
+ <>
47
+ <CapRow className="creatives-webpush-brand-icon">
48
+ <CapHeading type="h4" className="webpush-brand-icon">
49
+ <FormattedMessage {...messages.brandIconLogo} />
50
+ </CapHeading>
51
+ <CapRadioGroup
52
+ options={brandIconOptions}
53
+ value={brandIconOption}
54
+ onChange={onBrandIconChange}
55
+ disabled={isAnyUploadActive}
56
+ />
57
+ </CapRow>
58
+ {brandIconOption === BRAND_ICON_OPTIONS.UPLOAD_IMAGE && (
59
+ <CapRow
60
+ className="webpush-brand-icon-upload-section"
61
+ style={isLocked ? { pointerEvents: 'none', opacity: 0.5 } : undefined}
62
+ aria-disabled={isLocked}
63
+ >
64
+ <CapImageUpload
65
+ allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
66
+ imgSize={WEBPUSH_BRAND_ICON_SIZE}
67
+ uploadAsset={uploadBrandIconAsset}
68
+ isFullMode={isFullMode}
69
+ imageSrc={brandIconSrc}
70
+ updateImageSrc={setUpdateBrandIconSrc}
71
+ updateOnReUpload={updateOnBrandIconReUpload}
72
+ index={1}
73
+ className="cap-custom-image-upload"
74
+ key="webpush-brand-icon-uploaded-image"
75
+ imageData={webPush}
76
+ channel={WEBPUSH_BRAND_ICON}
77
+ showReUploadButton
78
+ recommendedDimensions={WEBPUSH_BRAND_ICON_RECOMMENDED_DIMENSIONS}
79
+ disabled={isLocked}
80
+ />
81
+ </CapRow>
82
+ )}
83
+ <CapDivider />
84
+ </>
85
+ );
86
+ };
87
+
88
+ BrandIconSection.propTypes = {
89
+ brandIconOption: PropTypes.string.isRequired,
90
+ onBrandIconChange: PropTypes.func.isRequired,
91
+ brandIconUpload: PropTypes.object.isRequired,
92
+ isLocked: PropTypes.bool,
93
+ isAnyUploadActive: PropTypes.bool,
94
+ formatMessage: PropTypes.func.isRequired,
95
+ messages: PropTypes.object.isRequired,
96
+ webPush: PropTypes.object,
97
+ isFullMode: PropTypes.bool,
98
+ };
99
+
100
+ BrandIconSection.defaultProps = {
101
+ isLocked: false,
102
+ isAnyUploadActive: false,
103
+ webPush: {},
104
+ isFullMode: true,
105
+ };
106
+
107
+ export default BrandIconSection;
108
+
@@ -0,0 +1,172 @@
1
+ import React, { useState, useCallback } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { FormattedMessage } from 'react-intl';
4
+ import CapRow from '@capillarytech/cap-ui-library/CapRow';
5
+ import CapInput from '@capillarytech/cap-ui-library/CapInput';
6
+ import CapButton from '@capillarytech/cap-ui-library/CapButton';
7
+ import CapHeading from '@capillarytech/cap-ui-library/CapHeading';
8
+ import messages from '../messages';
9
+ import { isValidHttpUrl } from '../utils/urlValidation';
10
+ import { WEBPUSH_BUTTON_TYPES, BUTTON_TEXT_MAX_LENGTH } from '../../constants';
11
+
12
+ const ButtonForm = ({
13
+ buttonType, // 'primary' or 'secondary'
14
+ formatMessage,
15
+ onSave,
16
+ onCancel,
17
+ initialData,
18
+ isEditMode,
19
+ }) => {
20
+ const [buttonText, setButtonText] = useState(initialData?.text || '');
21
+ const [buttonUrl, setButtonUrl] = useState(initialData?.url || '');
22
+ const [buttonTextError, setButtonTextError] = useState('');
23
+ const [buttonUrlError, setButtonUrlError] = useState('');
24
+
25
+ const validateButtonText = useCallback((value) => {
26
+ if (!value || value.trim() === '') {
27
+ return formatMessage(messages.buttonTextRequired);
28
+ }
29
+ return '';
30
+ }, [formatMessage]);
31
+
32
+ const validateButtonUrl = useCallback((value) => {
33
+ if (!value || value.trim() === '') {
34
+ return formatMessage(messages.buttonUrlRequired);
35
+ }
36
+
37
+ if (!isValidHttpUrl(value)) {
38
+ return formatMessage(messages.buttonUrlInvalid);
39
+ }
40
+
41
+ return '';
42
+ }, [formatMessage]);
43
+
44
+ const handleButtonTextChange = useCallback((e) => {
45
+ const { value } = e.target;
46
+ setButtonText(value);
47
+ const nextError = validateButtonText(value);
48
+ setButtonTextError((prev) => (prev === nextError ? prev : nextError));
49
+ }, [validateButtonText]);
50
+
51
+ const handleButtonUrlChange = useCallback((e) => {
52
+ const { value } = e.target;
53
+ setButtonUrl(value);
54
+ const nextError = validateButtonUrl(value);
55
+ setButtonUrlError((prev) => (prev === nextError ? prev : nextError));
56
+ }, [validateButtonUrl]);
57
+
58
+ const handleSave = useCallback(() => {
59
+ const textError = validateButtonText(buttonText);
60
+ const urlError = validateButtonUrl(buttonUrl);
61
+
62
+ if (textError || urlError) {
63
+ setButtonTextError(textError);
64
+ setButtonUrlError(urlError);
65
+ return;
66
+ }
67
+
68
+ onSave({
69
+ text: buttonText.trim(),
70
+ url: buttonUrl.trim(),
71
+ type: buttonType,
72
+ });
73
+ }, [buttonText, buttonUrl, buttonType, onSave, validateButtonText, validateButtonUrl]);
74
+
75
+ const handleCancel = useCallback(() => {
76
+ onCancel();
77
+ }, [onCancel]);
78
+
79
+ const isSaveDisabled = !buttonText.trim() || !buttonUrl.trim() || buttonTextError || buttonUrlError;
80
+
81
+ const renderCharacterCountSuffix = () => {
82
+ const currentLength = buttonText.length;
83
+ const maxLength = BUTTON_TEXT_MAX_LENGTH;
84
+
85
+ return (
86
+ <span className="button-character-count-suffix">
87
+ {`${currentLength}/${maxLength}`}
88
+ </span>
89
+ );
90
+ };
91
+
92
+ const handleKeyDown = useCallback((e) => {
93
+ if (e.key === 'Enter' && !isSaveDisabled) {
94
+ e.preventDefault();
95
+ handleSave();
96
+ }
97
+ }, [handleSave, isSaveDisabled]);
98
+
99
+ return (
100
+ <div className="webpush-button-form" onKeyDown={handleKeyDown}>
101
+ <CapRow className="button-form-row">
102
+ <CapHeading type="h4" className="button-form-heading">
103
+ <FormattedMessage {...messages.buttonText} />
104
+ </CapHeading>
105
+ <div className="button-form-field">
106
+ <CapInput
107
+ id={`webpush-button-text-input-${buttonType}`}
108
+ placeholder={formatMessage(messages.buttonTextPlaceholder)}
109
+ value={buttonText}
110
+ onChange={handleButtonTextChange}
111
+ maxLength={BUTTON_TEXT_MAX_LENGTH}
112
+ size="default"
113
+ status={buttonTextError ? 'error' : ''}
114
+ help={buttonTextError || ''}
115
+ suffix={renderCharacterCountSuffix()}
116
+ />
117
+ </div>
118
+ </CapRow>
119
+ <CapRow className="button-form-row">
120
+ <CapHeading type="h4" className="button-form-heading">
121
+ <FormattedMessage {...messages.buttonUrlLabel} />
122
+ </CapHeading>
123
+ <CapInput
124
+ id={`webpush-button-url-input-${buttonType}`}
125
+ placeholder={formatMessage(messages.buttonUrlPlaceholder)}
126
+ value={buttonUrl}
127
+ onChange={handleButtonUrlChange}
128
+ size="default"
129
+ status={buttonUrlError ? 'error' : ''}
130
+ help={buttonUrlError || ''}
131
+ />
132
+ </CapRow>
133
+ <CapRow className="button-form-actions">
134
+ <CapButton
135
+ type="primary"
136
+ onClick={handleSave}
137
+ disabled={isSaveDisabled}
138
+ className="button-form-save"
139
+ >
140
+ <FormattedMessage {...messages.saveButton} />
141
+ </CapButton>
142
+ <CapButton
143
+ type="secondary"
144
+ onClick={handleCancel}
145
+ className="button-form-cancel"
146
+ >
147
+ <FormattedMessage {...(isEditMode ? messages.cancelButton : messages.deleteButton)} />
148
+ </CapButton>
149
+ </CapRow>
150
+ </div>
151
+ );
152
+ };
153
+
154
+ ButtonForm.propTypes = {
155
+ buttonType: PropTypes.oneOf([WEBPUSH_BUTTON_TYPES.PRIMARY, WEBPUSH_BUTTON_TYPES.SECONDARY]).isRequired,
156
+ formatMessage: PropTypes.func.isRequired,
157
+ onSave: PropTypes.func.isRequired,
158
+ onCancel: PropTypes.func.isRequired,
159
+ initialData: PropTypes.shape({
160
+ text: PropTypes.string,
161
+ url: PropTypes.string,
162
+ }),
163
+ isEditMode: PropTypes.bool,
164
+ };
165
+
166
+ ButtonForm.defaultProps = {
167
+ initialData: null,
168
+ isEditMode: false,
169
+ };
170
+
171
+ export default ButtonForm;
172
+
@@ -0,0 +1,101 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import CapRow from '@capillarytech/cap-ui-library/CapRow';
4
+ import CapColumn from '@capillarytech/cap-ui-library/CapColumn';
5
+ import CapIcon from '@capillarytech/cap-ui-library/CapIcon';
6
+
7
+ const ButtonItem = ({
8
+ button,
9
+ index,
10
+ onEdit,
11
+ onDelete,
12
+ onDragStart,
13
+ onDragOver,
14
+ onDrop,
15
+ onDragEnd,
16
+ disabled,
17
+ }) => {
18
+ const handleDragStart = (e) => {
19
+ if (disabled) return;
20
+ e.dataTransfer.effectAllowed = 'move';
21
+ e.dataTransfer.setData('text/html', e.currentTarget);
22
+ onDragStart(index);
23
+ };
24
+
25
+ const handleDragOver = (e) => {
26
+ if (disabled) return;
27
+ e.preventDefault();
28
+ e.dataTransfer.dropEffect = 'move';
29
+ onDragOver(index);
30
+ };
31
+
32
+ const handleDrop = (e) => {
33
+ if (disabled) return;
34
+ e.preventDefault();
35
+ e.stopPropagation();
36
+ onDrop(index);
37
+ };
38
+
39
+ return (
40
+ <div
41
+ className={`webpush-button-item ${disabled ? 'disabled' : ''}`}
42
+ draggable={!disabled}
43
+ onDragStart={handleDragStart}
44
+ onDragOver={handleDragOver}
45
+ onDrop={handleDrop}
46
+ onDragEnd={onDragEnd}
47
+ >
48
+ <CapRow align="middle" className="button-item-content">
49
+ <CapColumn span={1} className="button-item-drag-handle">
50
+ <CapIcon type="drag" className="drag-icon" />
51
+ </CapColumn>
52
+ <CapColumn span={1} className="button-item-icon">
53
+ <CapIcon type="link" className="link-icon" />
54
+ </CapColumn>
55
+ <CapColumn span={14} className="button-item-info">
56
+ <div className="button-item-text-row">
57
+ <span className="button-item-text">{button.text}</span>
58
+ </div>
59
+ </CapColumn>
60
+ <CapColumn span={8} className="button-item-actions">
61
+ <div className="button-item-url">{button.url}</div>
62
+ <div className="action-icons">
63
+ <CapIcon
64
+ type="edit"
65
+ className="action-icon"
66
+ onClick={() => !disabled && onEdit(index)}
67
+ />
68
+ <CapIcon
69
+ type="delete"
70
+ className="action-icon delete-icon"
71
+ onClick={() => !disabled && onDelete(index)}
72
+ />
73
+ </div>
74
+ </CapColumn>
75
+ </CapRow>
76
+ </div>
77
+ );
78
+ };
79
+
80
+ ButtonItem.propTypes = {
81
+ button: PropTypes.shape({
82
+ text: PropTypes.string.isRequired,
83
+ url: PropTypes.string.isRequired,
84
+ type: PropTypes.string.isRequired,
85
+ }).isRequired,
86
+ index: PropTypes.number.isRequired,
87
+ onEdit: PropTypes.func.isRequired,
88
+ onDelete: PropTypes.func.isRequired,
89
+ onDragStart: PropTypes.func.isRequired,
90
+ onDragOver: PropTypes.func.isRequired,
91
+ onDrop: PropTypes.func.isRequired,
92
+ onDragEnd: PropTypes.func.isRequired,
93
+ disabled: PropTypes.bool,
94
+ };
95
+
96
+ ButtonItem.defaultProps = {
97
+ disabled: false,
98
+ };
99
+
100
+ export default ButtonItem;
101
+
@@ -0,0 +1,145 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { FormattedMessage } from 'react-intl';
4
+ import CapRow from '@capillarytech/cap-ui-library/CapRow';
5
+ import CapButton from '@capillarytech/cap-ui-library/CapButton';
6
+ import ButtonItem from './ButtonItem';
7
+ import messages from '../messages';
8
+
9
+ const ButtonList = ({
10
+ buttons,
11
+ onEdit,
12
+ onDelete,
13
+ onReorder,
14
+ onAddPrimary,
15
+ onAddSecondary,
16
+ showAddPrimary,
17
+ showAddSecondary,
18
+ disabled,
19
+ disableSecondaryButton,
20
+ isInlineFormVisible,
21
+ inlineFormIndex,
22
+ renderInlineForm,
23
+ }) => {
24
+ const [draggedIndex, setDraggedIndex] = useState(null);
25
+
26
+ const handleDragStart = (index) => {
27
+ if (disabled) return;
28
+ setDraggedIndex(index);
29
+ };
30
+
31
+ const handleDragOver = (index) => {
32
+ if (disabled) return;
33
+ if (draggedIndex === null || draggedIndex === index) return;
34
+
35
+ // Reorder buttons
36
+ onReorder(draggedIndex, index);
37
+ setDraggedIndex(index);
38
+ };
39
+
40
+ const handleDrop = () => {
41
+ if (disabled) return;
42
+ };
43
+
44
+ const handleDragEnd = () => {
45
+ if (disabled) return;
46
+ setDraggedIndex(null);
47
+ };
48
+
49
+ const shouldRenderInlineForm = isInlineFormVisible
50
+ && typeof inlineFormIndex === 'number'
51
+ && inlineFormIndex >= 0;
52
+
53
+ // Don't render the container if there are no buttons
54
+ if (buttons.length === 0) {
55
+ return null;
56
+ }
57
+
58
+ return (
59
+ <div className="webpush-button-list">
60
+ {buttons.map((button, index) => {
61
+ if (shouldRenderInlineForm && inlineFormIndex === index && typeof renderInlineForm === 'function') {
62
+ return (
63
+ <div key={`button-inline-form-${button.id}`} className="button-inline-form">
64
+ {renderInlineForm()}
65
+ </div>
66
+ );
67
+ }
68
+ // Only disable items that are not being edited
69
+ const isItemDisabled = disabled || (isInlineFormVisible && inlineFormIndex !== null && inlineFormIndex !== index);
70
+ return (
71
+ <ButtonItem
72
+ key={button.id}
73
+ button={button}
74
+ index={index}
75
+ onEdit={onEdit}
76
+ onDelete={onDelete}
77
+ onDragStart={handleDragStart}
78
+ onDragOver={handleDragOver}
79
+ onDrop={handleDrop}
80
+ onDragEnd={handleDragEnd}
81
+ disabled={isItemDisabled}
82
+ />
83
+ );
84
+ })}
85
+ {showAddPrimary && (
86
+ <CapRow className="button-list-add-button">
87
+ <CapButton
88
+ type="flat"
89
+ onClick={onAddPrimary}
90
+ className="add-primary-button button-add-trigger"
91
+ icon="plus"
92
+ disabled={disabled}
93
+ >
94
+ <FormattedMessage {...messages.addPrimaryButton} />
95
+ </CapButton>
96
+ </CapRow>
97
+ )}
98
+ {showAddSecondary && (
99
+ <CapRow className="button-list-add-button">
100
+ <CapButton
101
+ type="flat"
102
+ onClick={onAddSecondary}
103
+ className="add-secondary-button button-add-trigger"
104
+ icon="plus"
105
+ disabled={disableSecondaryButton || disabled}
106
+ >
107
+ <FormattedMessage {...messages.addSecondaryButton} />
108
+ </CapButton>
109
+ </CapRow>
110
+ )}
111
+ </div>
112
+ );
113
+ };
114
+
115
+ ButtonList.propTypes = {
116
+ buttons: PropTypes.arrayOf(PropTypes.shape({
117
+ id: PropTypes.string.isRequired,
118
+ text: PropTypes.string.isRequired,
119
+ url: PropTypes.string.isRequired,
120
+ type: PropTypes.string.isRequired,
121
+ })).isRequired,
122
+ onEdit: PropTypes.func.isRequired,
123
+ onDelete: PropTypes.func.isRequired,
124
+ onReorder: PropTypes.func.isRequired,
125
+ onAddPrimary: PropTypes.func.isRequired,
126
+ onAddSecondary: PropTypes.func.isRequired,
127
+ showAddPrimary: PropTypes.bool.isRequired,
128
+ showAddSecondary: PropTypes.bool.isRequired,
129
+ disabled: PropTypes.bool,
130
+ disableSecondaryButton: PropTypes.bool,
131
+ isInlineFormVisible: PropTypes.bool,
132
+ inlineFormIndex: PropTypes.number,
133
+ renderInlineForm: PropTypes.func,
134
+ };
135
+
136
+ ButtonList.defaultProps = {
137
+ disabled: false,
138
+ disableSecondaryButton: false,
139
+ isInlineFormVisible: false,
140
+ inlineFormIndex: null,
141
+ renderInlineForm: null,
142
+ };
143
+
144
+ export default ButtonList;
145
+