@capillarytech/creatives-library 8.0.264 → 8.0.265

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 (158) hide show
  1. package/constants/unified.js +0 -1
  2. package/package.json +1 -1
  3. package/services/api.js +0 -5
  4. package/utils/common.js +0 -6
  5. package/utils/tagValidations.js +1 -2
  6. package/utils/tests/transformerUtils.test.js +0 -297
  7. package/utils/transformerUtils.js +0 -40
  8. package/v2Components/CapImageUpload/constants.js +0 -2
  9. package/v2Components/CapImageUpload/index.js +16 -65
  10. package/v2Components/CapImageUpload/index.scss +1 -4
  11. package/v2Components/CapImageUpload/messages.js +1 -5
  12. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +2 -2
  13. package/v2Components/FormBuilder/index.js +8 -8
  14. package/v2Containers/App/constants.js +0 -5
  15. package/v2Containers/CreativesContainer/SlideBoxContent.js +2 -57
  16. package/v2Containers/CreativesContainer/SlideBoxHeader.js +0 -1
  17. package/v2Containers/CreativesContainer/constants.js +0 -3
  18. package/v2Containers/CreativesContainer/index.js +0 -168
  19. package/v2Containers/CreativesContainer/messages.js +0 -4
  20. package/v2Containers/CreativesContainer/tests/SlideBoxContent.test.js +0 -210
  21. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +0 -304
  22. package/v2Containers/Email/index.js +7 -3
  23. package/v2Containers/FTP/index.js +1 -1
  24. package/v2Containers/InApp/index.js +0 -1
  25. package/v2Containers/Line/Container/Text/index.js +0 -1
  26. package/v2Containers/MobilePushNew/index.js +0 -1
  27. package/v2Containers/Rcs/index.js +0 -3
  28. package/v2Containers/SmsTrai/Edit/index.js +0 -1
  29. package/v2Containers/Templates/ChannelTypeIllustration.js +1 -13
  30. package/v2Containers/Templates/_templates.scss +0 -205
  31. package/v2Containers/Templates/actions.js +1 -2
  32. package/v2Containers/Templates/constants.js +0 -1
  33. package/v2Containers/Templates/index.js +34 -274
  34. package/v2Containers/Templates/messages.js +0 -24
  35. package/v2Containers/Templates/reducer.js +0 -2
  36. package/v2Containers/Templates/tests/index.test.js +0 -10
  37. package/v2Containers/TemplatesV2/index.js +7 -15
  38. package/v2Containers/TemplatesV2/messages.js +0 -4
  39. package/v2Containers/Viber/index.js +0 -1
  40. package/v2Containers/Whatsapp/index.js +0 -1
  41. package/v2Containers/Zalo/index.js +0 -1
  42. package/v2Containers/Zalo/tests/index.test.js +5 -1
  43. package/utils/imageUrlUpload.js +0 -141
  44. package/v2Components/CapImageUrlUpload/constants.js +0 -26
  45. package/v2Components/CapImageUrlUpload/index.js +0 -365
  46. package/v2Components/CapImageUrlUpload/index.scss +0 -35
  47. package/v2Components/CapImageUrlUpload/messages.js +0 -47
  48. package/v2Containers/WebPush/Create/components/BrandIconSection.js +0 -108
  49. package/v2Containers/WebPush/Create/components/ButtonForm.js +0 -172
  50. package/v2Containers/WebPush/Create/components/ButtonItem.js +0 -101
  51. package/v2Containers/WebPush/Create/components/ButtonList.js +0 -145
  52. package/v2Containers/WebPush/Create/components/ButtonsLinksSection.js +0 -164
  53. package/v2Containers/WebPush/Create/components/ButtonsLinksSection.test.js +0 -463
  54. package/v2Containers/WebPush/Create/components/FormActions.js +0 -54
  55. package/v2Containers/WebPush/Create/components/FormActions.test.js +0 -163
  56. package/v2Containers/WebPush/Create/components/MediaSection.js +0 -142
  57. package/v2Containers/WebPush/Create/components/MediaSection.test.js +0 -341
  58. package/v2Containers/WebPush/Create/components/MessageSection.js +0 -103
  59. package/v2Containers/WebPush/Create/components/MessageSection.test.js +0 -268
  60. package/v2Containers/WebPush/Create/components/NotificationTitleSection.js +0 -87
  61. package/v2Containers/WebPush/Create/components/NotificationTitleSection.test.js +0 -210
  62. package/v2Containers/WebPush/Create/components/TemplateNameSection.js +0 -54
  63. package/v2Containers/WebPush/Create/components/TemplateNameSection.test.js +0 -143
  64. package/v2Containers/WebPush/Create/components/__snapshots__/ButtonsLinksSection.test.js.snap +0 -86
  65. package/v2Containers/WebPush/Create/components/__snapshots__/FormActions.test.js.snap +0 -16
  66. package/v2Containers/WebPush/Create/components/__snapshots__/MediaSection.test.js.snap +0 -41
  67. package/v2Containers/WebPush/Create/components/__snapshots__/MessageSection.test.js.snap +0 -54
  68. package/v2Containers/WebPush/Create/components/__snapshots__/NotificationTitleSection.test.js.snap +0 -37
  69. package/v2Containers/WebPush/Create/components/__snapshots__/TemplateNameSection.test.js.snap +0 -21
  70. package/v2Containers/WebPush/Create/components/_buttons.scss +0 -246
  71. package/v2Containers/WebPush/Create/components/tests/ButtonForm.test.js +0 -554
  72. package/v2Containers/WebPush/Create/components/tests/ButtonItem.test.js +0 -607
  73. package/v2Containers/WebPush/Create/components/tests/ButtonList.test.js +0 -633
  74. package/v2Containers/WebPush/Create/components/tests/__snapshots__/ButtonForm.test.js.snap +0 -666
  75. package/v2Containers/WebPush/Create/components/tests/__snapshots__/ButtonItem.test.js.snap +0 -74
  76. package/v2Containers/WebPush/Create/components/tests/__snapshots__/ButtonList.test.js.snap +0 -78
  77. package/v2Containers/WebPush/Create/hooks/useButtonManagement.js +0 -138
  78. package/v2Containers/WebPush/Create/hooks/useButtonManagement.test.js +0 -406
  79. package/v2Containers/WebPush/Create/hooks/useCharacterCount.js +0 -30
  80. package/v2Containers/WebPush/Create/hooks/useCharacterCount.test.js +0 -151
  81. package/v2Containers/WebPush/Create/hooks/useImageUpload.js +0 -104
  82. package/v2Containers/WebPush/Create/hooks/useImageUpload.test.js +0 -538
  83. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +0 -122
  84. package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +0 -633
  85. package/v2Containers/WebPush/Create/index.js +0 -1148
  86. package/v2Containers/WebPush/Create/index.scss +0 -134
  87. package/v2Containers/WebPush/Create/messages.js +0 -211
  88. package/v2Containers/WebPush/Create/preview/DevicePreviewContent.js +0 -228
  89. package/v2Containers/WebPush/Create/preview/NotificationContainer.js +0 -294
  90. package/v2Containers/WebPush/Create/preview/PreviewContent.js +0 -90
  91. package/v2Containers/WebPush/Create/preview/PreviewControls.js +0 -305
  92. package/v2Containers/WebPush/Create/preview/PreviewDisclaimer.js +0 -25
  93. package/v2Containers/WebPush/Create/preview/WebPushPreview.js +0 -155
  94. package/v2Containers/WebPush/Create/preview/assets/Light.svg +0 -53
  95. package/v2Containers/WebPush/Create/preview/assets/Top.svg +0 -5
  96. package/v2Containers/WebPush/Create/preview/assets/android-arrow-down.svg +0 -9
  97. package/v2Containers/WebPush/Create/preview/assets/android-arrow-up.svg +0 -9
  98. package/v2Containers/WebPush/Create/preview/assets/chrome-icon.png +0 -0
  99. package/v2Containers/WebPush/Create/preview/assets/edge-icon.png +0 -0
  100. package/v2Containers/WebPush/Create/preview/assets/firefox-icon.svg +0 -106
  101. package/v2Containers/WebPush/Create/preview/assets/iOS.svg +0 -26
  102. package/v2Containers/WebPush/Create/preview/assets/macos-arrow-down-icon.svg +0 -9
  103. package/v2Containers/WebPush/Create/preview/assets/macos-triple-dot-icon.svg +0 -9
  104. package/v2Containers/WebPush/Create/preview/assets/opera-icon.svg +0 -18
  105. package/v2Containers/WebPush/Create/preview/assets/safari-icon.svg +0 -29
  106. package/v2Containers/WebPush/Create/preview/assets/windows-close-icon.svg +0 -9
  107. package/v2Containers/WebPush/Create/preview/assets/windows-triple-dot-icon.svg +0 -9
  108. package/v2Containers/WebPush/Create/preview/components/AndroidMobileChromeHeader.js +0 -51
  109. package/v2Containers/WebPush/Create/preview/components/AndroidMobileExpanded.js +0 -145
  110. package/v2Containers/WebPush/Create/preview/components/IOSHeader.js +0 -45
  111. package/v2Containers/WebPush/Create/preview/components/NotificationExpandedContent.js +0 -68
  112. package/v2Containers/WebPush/Create/preview/components/NotificationHeader.js +0 -61
  113. package/v2Containers/WebPush/Create/preview/components/WindowsChromeExpanded.js +0 -99
  114. package/v2Containers/WebPush/Create/preview/components/tests/AndroidMobileExpanded.test.js +0 -733
  115. package/v2Containers/WebPush/Create/preview/components/tests/WindowsChromeExpanded.test.js +0 -571
  116. package/v2Containers/WebPush/Create/preview/components/tests/__snapshots__/AndroidMobileExpanded.test.js.snap +0 -85
  117. package/v2Containers/WebPush/Create/preview/components/tests/__snapshots__/WindowsChromeExpanded.test.js.snap +0 -81
  118. package/v2Containers/WebPush/Create/preview/config/notificationMappings.js +0 -50
  119. package/v2Containers/WebPush/Create/preview/constants.js +0 -637
  120. package/v2Containers/WebPush/Create/preview/notification-container.scss +0 -79
  121. package/v2Containers/WebPush/Create/preview/preview.scss +0 -358
  122. package/v2Containers/WebPush/Create/preview/styles/_android-mobile-chrome.scss +0 -370
  123. package/v2Containers/WebPush/Create/preview/styles/_android-mobile-edge.scss +0 -12
  124. package/v2Containers/WebPush/Create/preview/styles/_android-mobile-firefox.scss +0 -12
  125. package/v2Containers/WebPush/Create/preview/styles/_android-mobile-opera.scss +0 -12
  126. package/v2Containers/WebPush/Create/preview/styles/_android-tablet-chrome.scss +0 -47
  127. package/v2Containers/WebPush/Create/preview/styles/_android-tablet-edge.scss +0 -11
  128. package/v2Containers/WebPush/Create/preview/styles/_android-tablet-firefox.scss +0 -11
  129. package/v2Containers/WebPush/Create/preview/styles/_android-tablet-opera.scss +0 -11
  130. package/v2Containers/WebPush/Create/preview/styles/_base.scss +0 -207
  131. package/v2Containers/WebPush/Create/preview/styles/_ios.scss +0 -153
  132. package/v2Containers/WebPush/Create/preview/styles/_ipados.scss +0 -107
  133. package/v2Containers/WebPush/Create/preview/styles/_macos-chrome.scss +0 -101
  134. package/v2Containers/WebPush/Create/preview/styles/_windows-chrome.scss +0 -229
  135. package/v2Containers/WebPush/Create/preview/tests/DevicePreviewContent.test.js +0 -909
  136. package/v2Containers/WebPush/Create/preview/tests/NotificationContainer.test.js +0 -1081
  137. package/v2Containers/WebPush/Create/preview/tests/PreviewControls.test.js +0 -723
  138. package/v2Containers/WebPush/Create/preview/tests/WebPushPreview.test.js +0 -1327
  139. package/v2Containers/WebPush/Create/preview/tests/__snapshots__/DevicePreviewContent.test.js.snap +0 -131
  140. package/v2Containers/WebPush/Create/preview/tests/__snapshots__/NotificationContainer.test.js.snap +0 -112
  141. package/v2Containers/WebPush/Create/preview/tests/__snapshots__/PreviewControls.test.js.snap +0 -144
  142. package/v2Containers/WebPush/Create/preview/tests/__snapshots__/WebPushPreview.test.js.snap +0 -129
  143. package/v2Containers/WebPush/Create/utils/payloadBuilder.js +0 -96
  144. package/v2Containers/WebPush/Create/utils/payloadBuilder.test.js +0 -396
  145. package/v2Containers/WebPush/Create/utils/previewUtils.js +0 -89
  146. package/v2Containers/WebPush/Create/utils/urlValidation.js +0 -115
  147. package/v2Containers/WebPush/Create/utils/urlValidation.test.js +0 -449
  148. package/v2Containers/WebPush/Create/utils/validation.js +0 -76
  149. package/v2Containers/WebPush/Create/utils/validation.test.js +0 -283
  150. package/v2Containers/WebPush/actions.js +0 -60
  151. package/v2Containers/WebPush/constants.js +0 -132
  152. package/v2Containers/WebPush/index.js +0 -2
  153. package/v2Containers/WebPush/reducer.js +0 -104
  154. package/v2Containers/WebPush/sagas.js +0 -119
  155. package/v2Containers/WebPush/selectors.js +0 -65
  156. package/v2Containers/WebPush/tests/reducer.test.js +0 -863
  157. package/v2Containers/WebPush/tests/sagas.test.js +0 -566
  158. package/v2Containers/WebPush/tests/selectors.test.js +0 -960
@@ -22,7 +22,6 @@ 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();
26
25
  let renderedComponent;
27
26
 
28
27
  beforeEach(() => {
@@ -55,7 +54,6 @@ describe('Test Templates container', () => {
55
54
  getAllTemplates,
56
55
  getUserList,
57
56
  getSenderDetails,
58
- resetTemplate,
59
57
  }}
60
58
  location={{
61
59
  pathname: `/${channel}`,
@@ -81,8 +79,6 @@ describe('Test Templates container', () => {
81
79
  channel: 'WHATSAPP',
82
80
  orgUnitId: -1,
83
81
  });
84
- // resetTemplate should be called when entering account selection mode
85
- expect(resetTemplate).toHaveBeenCalled();
86
82
  });
87
83
 
88
84
  it('Should render temlates when whatsapp templates are passed', () => {
@@ -107,8 +103,6 @@ describe('Test Templates container', () => {
107
103
  Templates: {},
108
104
  });
109
105
  expect(renderedComponent).toMatchSnapshot();
110
- // SMS doesn't enter account selection mode, so resetTemplate shouldn't be called on mount
111
- expect(resetTemplate).not.toHaveBeenCalled();
112
106
  });
113
107
 
114
108
  it('Should render temlates when whatsapp templates are passed in full mode', () => {
@@ -129,8 +123,6 @@ describe('Test Templates container', () => {
129
123
  it('Should render correct component for zalo channel', () => {
130
124
  RenderFunctionFor('zalo');
131
125
  expect(renderedComponent).toMatchSnapshot();
132
- // resetTemplate should be called when entering account selection mode
133
- expect(resetTemplate).toHaveBeenCalled();
134
126
  });
135
127
  it('Should render temlates when zalo templates are passed', () => {
136
128
  RenderFunctionFor('zalo');
@@ -209,8 +201,6 @@ describe('Test Templates container', () => {
209
201
  channel: 'RCS',
210
202
  orgUnitId: -1,
211
203
  });
212
- // resetTemplate should be called when entering account selection mode
213
- expect(resetTemplate).toHaveBeenCalled();
214
204
  });
215
205
 
216
206
  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, WEBPUSH } from '../App/constants';
33
+ import { CREATIVES_UI_VIEW, LOYALTY, WHATSAPP, RCS, LINE, EMAIL, ASSETS, JP_LOCALE_HIDE_FEATURE, ZALO, INAPP } 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, COMMON_CHANNELS } from "../CreativesContainer/constants";
37
+ import { LOYALTY_SUPPORTED_ACTION } from "../CreativesContainer/constants";
38
38
 
39
39
  const {CapCustomCardList} = CapCustomCard;
40
40
 
@@ -65,13 +65,6 @@ 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
- } : {}),
75
68
  viber: {content: <></>, tab: intl.formatMessage(messages.viber), key: 'viber'},
76
69
  whatsapp: { content: <></>, tab: intl.formatMessage(messages.whatsapp), key: WHATSAPP },
77
70
  zalo: { content: <div></div>, tab: intl.formatMessage(messages.zalo), key: ZALO },
@@ -95,7 +88,7 @@ export class TemplatesV2 extends React.Component { // eslint-disable-line react/
95
88
  return obj;
96
89
  }, []);
97
90
 
98
- if (isFullMode ) {
91
+ if (isFullMode ) {
99
92
  filteredPanes.push({content: <div></div>, tab: intl.formatMessage(messages.gallery), key: 'assets'});
100
93
  } else {
101
94
  if (!channelsToHide.includes('callTask')) {
@@ -105,19 +98,18 @@ export class TemplatesV2 extends React.Component { // eslint-disable-line react/
105
98
  filteredPanes.push({content: <></>, tab: intl.formatMessage(messages.FTP), key: 'ftp'});
106
99
  defaultChannel = 'FTP';
107
100
  }
101
+ const commonChannels = ['sms', 'email', 'wechat', 'mobilepush', 'line', 'viber', 'facebook', 'call_task', 'ftp', 'assets'];
108
102
 
109
- // Create a local copy of COMMON_CHANNELS to avoid mutating the imported array
110
- const channels = [...COMMON_CHANNELS];
111
103
  const { actionName = ''} = loyaltyMetaData;
112
104
  if (isLoyaltyModule && actionName === LOYALTY_SUPPORTED_ACTION) {
113
- channels.push(WHATSAPP, ZALO);
105
+ commonChannels.push(WHATSAPP, ZALO);
114
106
  }
115
107
 
116
- // we only show channels which other than COMMON_CHANNELS
108
+ // we only show channels which other than commonChannels
117
109
  // if it is coming in enableNewChannels array
118
110
  filteredPanes = filteredPanes.filter((item) => {
119
111
  const channel = item.key;
120
- if (!channels.includes(channel)) {
112
+ if (!commonChannels.includes(channel)) {
121
113
  return enableNewChannels.includes(channel.toUpperCase());
122
114
  }
123
115
  return true;
@@ -86,8 +86,4 @@ 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
- },
93
89
  });
@@ -237,7 +237,6 @@ export const Viber = (props) => {
237
237
  injectedTagsParams: injectedTags,
238
238
  location,
239
239
  tagModule: 'outbound',
240
- isFullMode,
241
240
  }) || {};
242
241
  if (value.trim() === '') {
243
242
  errorMessage = formatMessage(messages.emptyContentErrorMessage);
@@ -641,7 +641,6 @@ export const Whatsapp = (props) => {
641
641
  location,
642
642
  tagModule: getDefaultTags,
643
643
  eventContextTags,
644
- isFullMode,
645
644
  }) || {};
646
645
  const unsupportedTagsLengthCheck =
647
646
  validationResponse?.unsupportedTags?.length > 0;
@@ -274,7 +274,6 @@ export const Zalo = (props) => {
274
274
  location,
275
275
  tagModule: getDefaultTags,
276
276
  eventContextTags,
277
- isFullMode,
278
277
  }) || {};
279
278
  const { unsupportedTags = [], isBraceError } = tagValidationResponse;
280
279
  let tagError = '';
@@ -117,7 +117,11 @@ describe('Test activity zalo container', () => {
117
117
  target: { value: 'Hello, welcome {{fsdaf}}' },
118
118
  });
119
119
  inputBox[0].focus();
120
- expect(inputBox[0].value).toBe('Hello, welcome {{fsdaf}}');
120
+ expect(
121
+ screen.getByText(
122
+ 'Unsupported tags: fsdaf. Please remove them from this message.',
123
+ ),
124
+ ).toBeInTheDocument();
121
125
  });
122
126
 
123
127
  it('test case for set template data', async () => {
@@ -1,141 +0,0 @@
1
- /**
2
- * Utility functions for uploading images from URLs
3
- *
4
- * NOTE: CORS-limited; will be replaced with backend implementation.
5
- * Flow currently hidden (not removed) from frontend.
6
- */
7
-
8
- import {
9
- DEFAULT_ALLOWED_CONTENT_TYPES,
10
- MIME_TYPE_TO_EXTENSION,
11
- DEFAULT_IMAGE_EXTENSION,
12
- } from '../v2Components/CapImageUrlUpload/constants';
13
-
14
- /**
15
- * Fetches an image from a URL
16
- *
17
- * @param {string} url - The image URL to fetch
18
- * @returns {Promise<Response>} - The fetch response object
19
- * @throws {Error} - If the fetch fails (network/CORS error)
20
- */
21
- export const fetchImageFromUrl = async (url) => {
22
- const trimmedUrl = url?.trim() || '';
23
-
24
- if (!trimmedUrl) {
25
- throw new Error('URL is required');
26
- }
27
-
28
- // CORS-limited: fails for images without proper CORS headers
29
- const response = await fetch(trimmedUrl, {
30
- method: 'GET',
31
- redirect: 'follow',
32
- mode: 'cors',
33
- });
34
-
35
- if (!response.ok) {
36
- throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`);
37
- }
38
-
39
- return response;
40
- };
41
-
42
- /**
43
- * Helper function to upload image from URL
44
- * Fetches image, validates content type and size, converts to File, and uploads via uploadAsset
45
- *
46
- * @param {string} url - The image URL to upload
47
- * @param {Function} formatMessage - React Intl formatMessage function
48
- * @param {Object} messages - React Intl messages object
49
- * @param {Function} uploadAssetFn - Function to upload the asset (file, type, fileParams)
50
- * @param {string} fileNamePrefix - Prefix for the generated file name
51
- * @param {number} maxSize - Maximum file size in bytes
52
- * @param {string[]} allowedContentTypes - Array of allowed MIME types (defaults to DEFAULT_ALLOWED_CONTENT_TYPES)
53
- * @returns {Promise<{success: boolean, error: string}>} - Result object with success status and error message
54
- *
55
- * @example
56
- * const result = await uploadImageFromUrlHelper(
57
- * 'https://example.com/image.jpg',
58
- * formatMessage,
59
- * messages,
60
- * uploadAsset,
61
- * 'my-image',
62
- * 5000000,
63
- * ['image/jpeg', 'image/png']
64
- * );
65
- */
66
- export const uploadImageFromUrlHelper = async (
67
- url,
68
- formatMessage,
69
- messages,
70
- uploadAssetFn,
71
- fileNamePrefix,
72
- maxSize,
73
- allowedContentTypes = DEFAULT_ALLOWED_CONTENT_TYPES,
74
- ) => {
75
- const trimmedUrl = url?.trim() || '';
76
-
77
- try {
78
- const response = await fetchImageFromUrl(trimmedUrl);
79
-
80
- // Validate Content-Type
81
- const contentType = response.headers?.get('Content-Type') || '';
82
- const normalizedContentType = contentType.split(';')[0].toLowerCase().trim();
83
-
84
- if (!allowedContentTypes.includes(normalizedContentType)) {
85
- return {
86
- success: false,
87
- error: formatMessage(messages.imageTypeInvalid),
88
- };
89
- }
90
-
91
- const blob = await response.blob();
92
-
93
- if (blob.size > maxSize) {
94
- return {
95
- success: false,
96
- error: formatMessage(messages.imageSizeInvalid),
97
- };
98
- }
99
-
100
- // Load image to get dimensions and verify validity
101
- return new Promise((resolve) => {
102
- const img = new Image();
103
- const objectUrl = URL.createObjectURL(blob);
104
-
105
- img.onload = () => {
106
- const extension = MIME_TYPE_TO_EXTENSION[normalizedContentType] || DEFAULT_IMAGE_EXTENSION;
107
- const fileName = `${fileNamePrefix}.${extension}`;
108
- const file = new File([blob], fileName, { type: blob.type });
109
- const fileParams = {
110
- width: img.width,
111
- height: img.height,
112
- error: false,
113
- };
114
-
115
- uploadAssetFn(file, 'image', fileParams);
116
- URL.revokeObjectURL(objectUrl);
117
-
118
- resolve({
119
- success: true,
120
- error: '',
121
- });
122
- };
123
-
124
- img.onerror = () => {
125
- URL.revokeObjectURL(objectUrl);
126
- resolve({
127
- success: false,
128
- error: formatMessage(messages.imageLoadError),
129
- });
130
- };
131
-
132
- img.src = objectUrl;
133
- });
134
- } catch (error) {
135
- return {
136
- success: false,
137
- error: formatMessage(messages.imageLoadError),
138
- };
139
- }
140
- };
141
-
@@ -1,26 +0,0 @@
1
- // Default allowed content types for image URL validation
2
- export const DEFAULT_ALLOWED_CONTENT_TYPES = ['image/jpeg', 'image/jpg', 'image/png'];
3
-
4
- // Default maximum file size (5MB)
5
- export const DEFAULT_MAX_SIZE = 5000000;
6
-
7
- // Default allowed extensions regex
8
- export const DEFAULT_ALLOWED_EXTENSIONS_REGEX = /\.(jpe?g|png)$/i;
9
-
10
- // MIME type to file extension mapping
11
- export const MIME_TYPE_TO_EXTENSION = {
12
- 'image/jpeg': 'jpg',
13
- 'image/jpg': 'jpg',
14
- 'image/png': 'png',
15
- };
16
-
17
- // Default image extension fallback
18
- export const DEFAULT_IMAGE_EXTENSION = 'png';
19
-
20
- // Upload status state machine states
21
- export const UPLOAD_STATUS = {
22
- IDLE: 'idle',
23
- UPLOADING: 'uploading',
24
- WAITING: 'waiting',
25
- };
26
-
@@ -1,365 +0,0 @@
1
- /**
2
- *
3
- * CapImageUrlUpload
4
- *
5
- * A modular component for uploading images from a URL.
6
- * Validates URL format, image type, and size before uploading to gallery.
7
- * Can be used in any form context (creatives, profiles, etc.)
8
- */
9
-
10
- import React, { useState, useCallback, useEffect } from 'react';
11
- import PropTypes from 'prop-types';
12
- import { injectIntl, intlShape, FormattedMessage } from 'react-intl';
13
- import CapInput from '@capillarytech/cap-ui-library/CapInput';
14
- import CapHeading from '@capillarytech/cap-ui-library/CapHeading';
15
- import CapLabel from '@capillarytech/cap-ui-library/CapLabel';
16
- import CapError from '@capillarytech/cap-ui-library/CapError';
17
- import messages from './messages';
18
- import { DEFAULT_ALLOWED_CONTENT_TYPES, DEFAULT_MAX_SIZE, UPLOAD_STATUS } from './constants';
19
- import { fetchImageFromUrl, uploadImageFromUrlHelper } from '../../utils/imageUrlUpload';
20
- import './index.scss';
21
-
22
- function CapImageUrlUpload(props) {
23
- const {
24
- intl,
25
- uploadAsset,
26
- imgSize = DEFAULT_MAX_SIZE,
27
- allowedContentTypes = DEFAULT_ALLOWED_CONTENT_TYPES,
28
- recommendedDimensions = [],
29
- sizeLabel = '',
30
- formatLabel = '',
31
- imageUrl = '',
32
- imageSrc = '', // Secure file path from parent after upload completes
33
- onUrlChange,
34
- onValidationStateChange, // Callback to notify parent of validation state
35
- onUploadStateChange, // Callback to notify parent of upload state
36
- isExternalUploading = false, // Upload state from parent (e.g., redux)
37
- className = '',
38
- placeholder,
39
- disabled = false,
40
- fileNamePrefix = 'image',
41
- } = props;
42
-
43
- const { formatMessage } = intl ?? {};
44
- const { CapHeadingSpan } = CapHeading;
45
-
46
- const [isValidating, setIsValidating] = useState(false);
47
- const [isInternalUploading, setIsInternalUploading] = useState(false);
48
- const [error, setError] = useState('');
49
- // State machine for upload lifecycle
50
- const [uploadStatus, setUploadStatus] = useState(UPLOAD_STATUS.IDLE);
51
-
52
- /**
53
- * Validate image URL
54
- * Checks URL format, fetches as Blob, validates Content-Type, file size, and image validity
55
- */
56
- const validateImageUrl = useCallback(async (url) => {
57
- const trimmedUrl = url?.trim() || '';
58
-
59
- if (!trimmedUrl) {
60
- return { isValid: false, error: '' };
61
- }
62
-
63
- setIsValidating(true);
64
- setError('');
65
-
66
- try {
67
- // Validate URL format
68
- let urlObj;
69
- try {
70
- urlObj = new URL(trimmedUrl);
71
- if (!['http:', 'https:'].includes(urlObj.protocol)) {
72
- setIsValidating(false);
73
- return {
74
- isValid: false,
75
- error: formatMessage(messages.imageUrlInvalid),
76
- };
77
- }
78
- } catch (urlError) {
79
- setIsValidating(false);
80
- return {
81
- isValid: false,
82
- error: formatMessage(messages.imageUrlInvalid),
83
- };
84
- }
85
-
86
- // Fetch image as Blob to check file size
87
- let response;
88
- try {
89
- response = await fetchImageFromUrl(trimmedUrl);
90
- } catch (fetchError) {
91
- setIsValidating(false);
92
- return {
93
- isValid: false,
94
- error: formatMessage(messages.imageLoadError),
95
- };
96
- }
97
-
98
- // Validate Content-Type
99
- const contentType = response.headers?.get('Content-Type') || '';
100
- const normalizedContentType = contentType.split(';')[0].toLowerCase().trim();
101
-
102
- if (!allowedContentTypes.includes(normalizedContentType)) {
103
- setIsValidating(false);
104
- return {
105
- isValid: false,
106
- error: formatMessage(messages.imageTypeInvalid),
107
- };
108
- }
109
-
110
- // Validate file size
111
- const blob = await response.blob();
112
- if (blob.size > imgSize) {
113
- setIsValidating(false);
114
- return {
115
- isValid: false,
116
- error: formatMessage(messages.imageSizeInvalid),
117
- };
118
- }
119
-
120
- // Verify image validity by loading in Image object
121
- return new Promise((resolve) => {
122
- const img = new Image();
123
- const objectUrl = URL.createObjectURL(blob);
124
-
125
- img.onload = () => {
126
- URL.revokeObjectURL(objectUrl);
127
- setIsValidating(false);
128
- resolve({
129
- isValid: true,
130
- error: '',
131
- });
132
- };
133
-
134
- img.onerror = () => {
135
- URL.revokeObjectURL(objectUrl);
136
- setIsValidating(false);
137
- resolve({
138
- isValid: false,
139
- error: formatMessage(messages.imageLoadError),
140
- });
141
- };
142
-
143
- img.src = objectUrl;
144
- });
145
- } catch (error) {
146
- setIsValidating(false);
147
- return {
148
- isValid: false,
149
- error: formatMessage(messages.imageLoadError),
150
- };
151
- }
152
- }, [formatMessage, allowedContentTypes, imgSize]);
153
-
154
- /**
155
- * Upload image from URL to media gallery
156
- * Fetches image, converts to File, uploads via uploadAsset
157
- */
158
- const uploadImageFromUrl = useCallback(async (url) => {
159
- setIsInternalUploading(true);
160
- setUploadStatus(UPLOAD_STATUS.UPLOADING);
161
-
162
- // Notify parent that upload is starting
163
- if (onUploadStateChange) {
164
- onUploadStateChange(true);
165
- }
166
-
167
- const result = await uploadImageFromUrlHelper(
168
- url,
169
- formatMessage,
170
- messages,
171
- uploadAsset,
172
- fileNamePrefix,
173
- imgSize,
174
- allowedContentTypes,
175
- );
176
-
177
- setIsInternalUploading(false);
178
-
179
- // Check if imageSrc is already available (handles React batching/timing issues)
180
- if (result.success && imageSrc && imageSrc !== '') {
181
- // Upload already complete - imageSrc was set before we entered waiting state
182
- setUploadStatus(UPLOAD_STATUS.IDLE);
183
- if (onUploadStateChange) {
184
- onUploadStateChange(false);
185
- }
186
- } else if (result.success) {
187
- // Transition to waiting state - we've triggered the upload, now wait for imageSrc
188
- setUploadStatus(UPLOAD_STATUS.WAITING);
189
- } else {
190
- // Upload failed
191
- setUploadStatus(UPLOAD_STATUS.IDLE);
192
- if (onUploadStateChange) {
193
- onUploadStateChange(false);
194
- }
195
- }
196
-
197
- return result;
198
- }, [formatMessage, uploadAsset, fileNamePrefix, imgSize, allowedContentTypes, onUploadStateChange, imageSrc]);
199
-
200
- /**
201
- * Handle image URL change
202
- * Validates URL and triggers upload on success
203
- */
204
- const handleImageUrlChange = useCallback(async (e) => {
205
- const url = e.target.value;
206
- const trimmedUrl = url?.trim() || '';
207
-
208
- // Reset upload status when URL changes
209
- setUploadStatus(UPLOAD_STATUS.IDLE);
210
-
211
- // Call parent's onUrlChange if provided
212
- if (onUrlChange) {
213
- onUrlChange(url);
214
- }
215
-
216
- if (trimmedUrl !== '') {
217
- // Notify parent that validation is starting
218
- if (onValidationStateChange) {
219
- onValidationStateChange(true);
220
- }
221
-
222
- const validation = await validateImageUrl(trimmedUrl);
223
-
224
- // Notify parent that validation is complete
225
- if (onValidationStateChange) {
226
- onValidationStateChange(false);
227
- }
228
-
229
- if (!validation.isValid) {
230
- setError(validation.error);
231
- if (onUploadStateChange) {
232
- onUploadStateChange(false);
233
- }
234
- } else {
235
- setError('');
236
- // Upload image from URL when validation succeeds
237
- const uploadResult = await uploadImageFromUrl(trimmedUrl);
238
- if (!uploadResult.success) {
239
- setError(uploadResult.error);
240
- if (onUploadStateChange) {
241
- onUploadStateChange(false);
242
- }
243
- }
244
- }
245
- } else {
246
- setError('');
247
- if (onValidationStateChange) {
248
- onValidationStateChange(false);
249
- }
250
- if (onUploadStateChange) {
251
- onUploadStateChange(false);
252
- }
253
- }
254
- }, [validateImageUrl, uploadImageFromUrl, onUrlChange, onValidationStateChange, onUploadStateChange]);
255
-
256
- /**
257
- * Monitor upload status and imageSrc to detect when upload is complete
258
- * Handles React batching and timing issues by checking state transitions
259
- */
260
- useEffect(() => {
261
- if ((uploadStatus === UPLOAD_STATUS.UPLOADING || uploadStatus === UPLOAD_STATUS.WAITING) && imageSrc && imageSrc !== '') {
262
- // Upload is complete - we have the secure file path
263
- setUploadStatus(UPLOAD_STATUS.IDLE);
264
-
265
- // Notify parent that upload is complete
266
- if (onUploadStateChange) {
267
- onUploadStateChange(false);
268
- }
269
- }
270
- }, [uploadStatus, imageSrc, onUploadStateChange]);
271
-
272
- // Determine if we should show "uploading" state
273
- // Show uploading when:
274
- // 1. Internal upload is in progress, OR
275
- // 2. External upload is in progress (from parent's redux), OR
276
- // 3. State machine indicates we're uploading or waiting for completion
277
- const isActuallyUploading = isInternalUploading || isExternalUploading || uploadStatus !== UPLOAD_STATUS.IDLE;
278
-
279
- return (
280
- <div className={`cap-image-url-upload ${className}`}>
281
- <CapInput
282
- className="image-url-input"
283
- placeholder={placeholder || formatMessage(messages.imageUrlPlaceholder)}
284
- value={imageUrl}
285
- onChange={handleImageUrlChange}
286
- size="default"
287
- errorMessage={
288
- error && (
289
- <CapError className="image-url-error">
290
- {error}
291
- </CapError>
292
- )
293
- }
294
- disabled={disabled || isValidating || isActuallyUploading}
295
- />
296
-
297
- {isValidating && (
298
- <CapLabel type="label2" className="validation-label">
299
- <FormattedMessage {...messages.validatingUrl} />
300
- </CapLabel>
301
- )}
302
-
303
- {isActuallyUploading && !isValidating && (
304
- <CapLabel type="label2" className="uploading-label">
305
- <FormattedMessage {...messages.uploadingImage} />
306
- </CapLabel>
307
- )}
308
-
309
- <div className="webpush-image-specs">
310
- {recommendedDimensions?.length > 0 && (
311
- <CapHeadingSpan type="label2" className="image-dimension">
312
- <FormattedMessage
313
- {...messages.recommendedDimensions}
314
- values={{
315
- dimensions: recommendedDimensions
316
- .map((dim) => `${dim.width} x ${dim.height}px`)
317
- .join(', '),
318
- }}
319
- />
320
- </CapHeadingSpan>
321
- )}
322
-
323
- {sizeLabel && (
324
- <CapHeadingSpan type="label2" className="image-size">
325
- {sizeLabel}
326
- </CapHeadingSpan>
327
- )}
328
-
329
- {formatLabel && (
330
- <CapHeadingSpan type="label2" className="image-format">
331
- {formatLabel}
332
- </CapHeadingSpan>
333
- )}
334
- </div>
335
- </div>
336
- );
337
- }
338
-
339
- CapImageUrlUpload.propTypes = {
340
- intl: intlShape.isRequired,
341
- uploadAsset: PropTypes.func.isRequired,
342
- imgSize: PropTypes.number,
343
- allowedContentTypes: PropTypes.arrayOf(PropTypes.string),
344
- recommendedDimensions: PropTypes.arrayOf(
345
- PropTypes.shape({
346
- width: PropTypes.number.isRequired,
347
- height: PropTypes.number.isRequired,
348
- })
349
- ),
350
- sizeLabel: PropTypes.string,
351
- formatLabel: PropTypes.string,
352
- imageUrl: PropTypes.string,
353
- imageSrc: PropTypes.string, // Secure file path from parent after upload completes
354
- onUrlChange: PropTypes.func,
355
- onValidationStateChange: PropTypes.func, // Callback(isValidating: bool)
356
- onUploadStateChange: PropTypes.func, // Callback(isUploading: bool)
357
- isExternalUploading: PropTypes.bool, // Upload state from parent (e.g., redux)
358
- className: PropTypes.string,
359
- placeholder: PropTypes.string,
360
- disabled: PropTypes.bool,
361
- fileNamePrefix: PropTypes.string,
362
- };
363
-
364
- export default injectIntl(CapImageUrlUpload);
365
-