@capillarytech/creatives-library 8.0.266 → 8.0.267
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/constants/unified.js +0 -1
- package/package.json +1 -1
- package/services/api.js +0 -5
- package/utils/common.js +0 -6
- package/utils/tests/transformerUtils.test.js +0 -297
- package/utils/transformerUtils.js +0 -40
- package/v2Components/CapImageUpload/constants.js +0 -2
- package/v2Components/CapImageUpload/index.js +16 -65
- package/v2Components/CapImageUpload/index.scss +1 -4
- package/v2Components/CapImageUpload/messages.js +1 -5
- package/v2Components/CommonTestAndPreview/index.js +15 -4
- package/v2Containers/App/constants.js +0 -5
- package/v2Containers/CreativesContainer/SlideBoxContent.js +2 -57
- package/v2Containers/CreativesContainer/SlideBoxHeader.js +0 -1
- package/v2Containers/CreativesContainer/constants.js +0 -3
- package/v2Containers/CreativesContainer/index.js +0 -168
- package/v2Containers/CreativesContainer/messages.js +0 -4
- package/v2Containers/CreativesContainer/tests/SlideBoxContent.test.js +0 -210
- package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +0 -304
- package/v2Containers/SmsTrai/Edit/index.js +12 -1
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +648 -36
- package/v2Containers/Templates/ChannelTypeIllustration.js +1 -13
- package/v2Containers/Templates/_templates.scss +0 -205
- package/v2Containers/Templates/actions.js +1 -2
- package/v2Containers/Templates/constants.js +0 -1
- package/v2Containers/Templates/index.js +34 -274
- package/v2Containers/Templates/messages.js +0 -24
- package/v2Containers/Templates/reducer.js +0 -2
- package/v2Containers/Templates/tests/index.test.js +0 -10
- package/v2Containers/TemplatesV2/index.js +7 -15
- package/v2Containers/TemplatesV2/messages.js +0 -4
- package/utils/imageUrlUpload.js +0 -141
- package/v2Components/CapImageUrlUpload/constants.js +0 -26
- package/v2Components/CapImageUrlUpload/index.js +0 -365
- package/v2Components/CapImageUrlUpload/index.scss +0 -35
- package/v2Components/CapImageUrlUpload/messages.js +0 -47
- package/v2Containers/WebPush/Create/components/BrandIconSection.js +0 -108
- package/v2Containers/WebPush/Create/components/ButtonForm.js +0 -172
- package/v2Containers/WebPush/Create/components/ButtonItem.js +0 -101
- package/v2Containers/WebPush/Create/components/ButtonList.js +0 -145
- package/v2Containers/WebPush/Create/components/ButtonsLinksSection.js +0 -164
- package/v2Containers/WebPush/Create/components/ButtonsLinksSection.test.js +0 -463
- package/v2Containers/WebPush/Create/components/FormActions.js +0 -54
- package/v2Containers/WebPush/Create/components/FormActions.test.js +0 -163
- package/v2Containers/WebPush/Create/components/MediaSection.js +0 -142
- package/v2Containers/WebPush/Create/components/MediaSection.test.js +0 -341
- package/v2Containers/WebPush/Create/components/MessageSection.js +0 -103
- package/v2Containers/WebPush/Create/components/MessageSection.test.js +0 -268
- package/v2Containers/WebPush/Create/components/NotificationTitleSection.js +0 -87
- package/v2Containers/WebPush/Create/components/NotificationTitleSection.test.js +0 -210
- package/v2Containers/WebPush/Create/components/TemplateNameSection.js +0 -54
- package/v2Containers/WebPush/Create/components/TemplateNameSection.test.js +0 -143
- package/v2Containers/WebPush/Create/components/__snapshots__/ButtonsLinksSection.test.js.snap +0 -86
- package/v2Containers/WebPush/Create/components/__snapshots__/FormActions.test.js.snap +0 -16
- package/v2Containers/WebPush/Create/components/__snapshots__/MediaSection.test.js.snap +0 -41
- package/v2Containers/WebPush/Create/components/__snapshots__/MessageSection.test.js.snap +0 -54
- package/v2Containers/WebPush/Create/components/__snapshots__/NotificationTitleSection.test.js.snap +0 -37
- package/v2Containers/WebPush/Create/components/__snapshots__/TemplateNameSection.test.js.snap +0 -21
- package/v2Containers/WebPush/Create/components/_buttons.scss +0 -246
- package/v2Containers/WebPush/Create/components/tests/ButtonForm.test.js +0 -554
- package/v2Containers/WebPush/Create/components/tests/ButtonItem.test.js +0 -607
- package/v2Containers/WebPush/Create/components/tests/ButtonList.test.js +0 -633
- package/v2Containers/WebPush/Create/components/tests/__snapshots__/ButtonForm.test.js.snap +0 -666
- package/v2Containers/WebPush/Create/components/tests/__snapshots__/ButtonItem.test.js.snap +0 -74
- package/v2Containers/WebPush/Create/components/tests/__snapshots__/ButtonList.test.js.snap +0 -78
- package/v2Containers/WebPush/Create/hooks/useButtonManagement.js +0 -138
- package/v2Containers/WebPush/Create/hooks/useButtonManagement.test.js +0 -406
- package/v2Containers/WebPush/Create/hooks/useCharacterCount.js +0 -30
- package/v2Containers/WebPush/Create/hooks/useCharacterCount.test.js +0 -151
- package/v2Containers/WebPush/Create/hooks/useImageUpload.js +0 -104
- package/v2Containers/WebPush/Create/hooks/useImageUpload.test.js +0 -538
- package/v2Containers/WebPush/Create/hooks/useTagManagement.js +0 -122
- package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +0 -633
- package/v2Containers/WebPush/Create/index.js +0 -1148
- package/v2Containers/WebPush/Create/index.scss +0 -134
- package/v2Containers/WebPush/Create/messages.js +0 -211
- package/v2Containers/WebPush/Create/preview/DevicePreviewContent.js +0 -228
- package/v2Containers/WebPush/Create/preview/NotificationContainer.js +0 -294
- package/v2Containers/WebPush/Create/preview/PreviewContent.js +0 -90
- package/v2Containers/WebPush/Create/preview/PreviewControls.js +0 -305
- package/v2Containers/WebPush/Create/preview/PreviewDisclaimer.js +0 -25
- package/v2Containers/WebPush/Create/preview/WebPushPreview.js +0 -156
- package/v2Containers/WebPush/Create/preview/assets/Light.svg +0 -53
- package/v2Containers/WebPush/Create/preview/assets/Top.svg +0 -5
- package/v2Containers/WebPush/Create/preview/assets/android-arrow-down.svg +0 -9
- package/v2Containers/WebPush/Create/preview/assets/android-arrow-up.svg +0 -9
- package/v2Containers/WebPush/Create/preview/assets/chrome-icon.png +0 -0
- package/v2Containers/WebPush/Create/preview/assets/edge-icon.png +0 -0
- package/v2Containers/WebPush/Create/preview/assets/firefox-icon.svg +0 -106
- package/v2Containers/WebPush/Create/preview/assets/iOS.svg +0 -26
- package/v2Containers/WebPush/Create/preview/assets/macos-arrow-down-icon.svg +0 -9
- package/v2Containers/WebPush/Create/preview/assets/macos-triple-dot-icon.svg +0 -9
- package/v2Containers/WebPush/Create/preview/assets/opera-icon.svg +0 -18
- package/v2Containers/WebPush/Create/preview/assets/safari-icon.svg +0 -29
- package/v2Containers/WebPush/Create/preview/assets/windows-close-icon.svg +0 -9
- package/v2Containers/WebPush/Create/preview/assets/windows-triple-dot-icon.svg +0 -9
- package/v2Containers/WebPush/Create/preview/components/AndroidMobileChromeHeader.js +0 -51
- package/v2Containers/WebPush/Create/preview/components/AndroidMobileExpanded.js +0 -145
- package/v2Containers/WebPush/Create/preview/components/IOSHeader.js +0 -45
- package/v2Containers/WebPush/Create/preview/components/NotificationExpandedContent.js +0 -68
- package/v2Containers/WebPush/Create/preview/components/NotificationHeader.js +0 -61
- package/v2Containers/WebPush/Create/preview/components/WindowsChromeExpanded.js +0 -99
- package/v2Containers/WebPush/Create/preview/components/tests/AndroidMobileExpanded.test.js +0 -733
- package/v2Containers/WebPush/Create/preview/components/tests/WindowsChromeExpanded.test.js +0 -571
- package/v2Containers/WebPush/Create/preview/components/tests/__snapshots__/AndroidMobileExpanded.test.js.snap +0 -85
- package/v2Containers/WebPush/Create/preview/components/tests/__snapshots__/WindowsChromeExpanded.test.js.snap +0 -81
- package/v2Containers/WebPush/Create/preview/config/notificationMappings.js +0 -50
- package/v2Containers/WebPush/Create/preview/constants.js +0 -637
- package/v2Containers/WebPush/Create/preview/notification-container.scss +0 -79
- package/v2Containers/WebPush/Create/preview/preview.scss +0 -358
- package/v2Containers/WebPush/Create/preview/styles/_android-mobile-chrome.scss +0 -370
- package/v2Containers/WebPush/Create/preview/styles/_android-mobile-edge.scss +0 -12
- package/v2Containers/WebPush/Create/preview/styles/_android-mobile-firefox.scss +0 -12
- package/v2Containers/WebPush/Create/preview/styles/_android-mobile-opera.scss +0 -12
- package/v2Containers/WebPush/Create/preview/styles/_android-tablet-chrome.scss +0 -47
- package/v2Containers/WebPush/Create/preview/styles/_android-tablet-edge.scss +0 -11
- package/v2Containers/WebPush/Create/preview/styles/_android-tablet-firefox.scss +0 -11
- package/v2Containers/WebPush/Create/preview/styles/_android-tablet-opera.scss +0 -11
- package/v2Containers/WebPush/Create/preview/styles/_base.scss +0 -207
- package/v2Containers/WebPush/Create/preview/styles/_ios.scss +0 -153
- package/v2Containers/WebPush/Create/preview/styles/_ipados.scss +0 -107
- package/v2Containers/WebPush/Create/preview/styles/_macos-chrome.scss +0 -101
- package/v2Containers/WebPush/Create/preview/styles/_windows-chrome.scss +0 -229
- package/v2Containers/WebPush/Create/preview/tests/DevicePreviewContent.test.js +0 -906
- package/v2Containers/WebPush/Create/preview/tests/NotificationContainer.test.js +0 -1081
- package/v2Containers/WebPush/Create/preview/tests/PreviewControls.test.js +0 -723
- package/v2Containers/WebPush/Create/preview/tests/WebPushPreview.test.js +0 -1327
- package/v2Containers/WebPush/Create/preview/tests/__snapshots__/DevicePreviewContent.test.js.snap +0 -131
- package/v2Containers/WebPush/Create/preview/tests/__snapshots__/NotificationContainer.test.js.snap +0 -112
- package/v2Containers/WebPush/Create/preview/tests/__snapshots__/PreviewControls.test.js.snap +0 -144
- package/v2Containers/WebPush/Create/preview/tests/__snapshots__/WebPushPreview.test.js.snap +0 -129
- package/v2Containers/WebPush/Create/utils/payloadBuilder.js +0 -96
- package/v2Containers/WebPush/Create/utils/payloadBuilder.test.js +0 -396
- package/v2Containers/WebPush/Create/utils/previewUtils.js +0 -89
- package/v2Containers/WebPush/Create/utils/urlValidation.js +0 -115
- package/v2Containers/WebPush/Create/utils/urlValidation.test.js +0 -449
- package/v2Containers/WebPush/Create/utils/validation.js +0 -75
- package/v2Containers/WebPush/Create/utils/validation.test.js +0 -283
- package/v2Containers/WebPush/actions.js +0 -60
- package/v2Containers/WebPush/constants.js +0 -132
- package/v2Containers/WebPush/index.js +0 -2
- package/v2Containers/WebPush/reducer.js +0 -104
- package/v2Containers/WebPush/sagas.js +0 -119
- package/v2Containers/WebPush/selectors.js +0 -65
- package/v2Containers/WebPush/tests/reducer.test.js +0 -863
- package/v2Containers/WebPush/tests/sagas.test.js +0 -566
- 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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
105
|
+
commonChannels.push(WHATSAPP, ZALO);
|
|
114
106
|
}
|
|
115
107
|
|
|
116
|
-
// we only show channels which other than
|
|
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 (!
|
|
112
|
+
if (!commonChannels.includes(channel)) {
|
|
121
113
|
return enableNewChannels.includes(channel.toUpperCase());
|
|
122
114
|
}
|
|
123
115
|
return true;
|
package/utils/imageUrlUpload.js
DELETED
|
@@ -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
|
-
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
@import '~@capillarytech/cap-ui-library/styles/_variables.scss';
|
|
2
|
-
|
|
3
|
-
@mixin cap-image-url-spec-text {
|
|
4
|
-
.image-dimension,
|
|
5
|
-
.image-size,
|
|
6
|
-
.image-format {
|
|
7
|
-
color: $CAP_G01;
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
.cap-image-url-upload {
|
|
12
|
-
.image-url-input {
|
|
13
|
-
width: 100%;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
.validation-label,
|
|
17
|
-
.uploading-label {
|
|
18
|
-
margin-top: $CAP_SPACE_08;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
.image-url-error {
|
|
22
|
-
margin-top: $CAP_SPACE_04;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
.webpush-image-specs {
|
|
26
|
-
@include cap-image-url-spec-text;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
.webpush-container & {
|
|
30
|
-
.webpush-image-specs {
|
|
31
|
-
@include cap-image-url-spec-text;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|