@capillarytech/creatives-library 8.0.354 → 8.0.356
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/index.html +0 -1
- package/package.json +1 -1
- package/utils/cdnTransformation.js +3 -63
- package/utils/tests/cdnTransformation.test.js +0 -111
- package/v2Components/CommonTestAndPreview/UnifiedPreview/PreviewHeader.js +16 -0
- package/v2Components/CommonTestAndPreview/UnifiedPreview/WebPushPreviewContent.js +169 -0
- package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +54 -0
- package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +52 -6
- package/v2Components/CommonTestAndPreview/constants.js +2 -0
- package/v2Components/CommonTestAndPreview/index.js +51 -2
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/PreviewHeader.test.js +163 -0
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/WebPushPreviewContent.test.js +522 -0
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +255 -0
- package/v2Components/CommonTestAndPreview/tests/constants.test.js +2 -1
- package/v2Components/CommonTestAndPreview/tests/index.test.js +194 -0
- package/v2Components/FormBuilder/index.js +162 -52
- package/v2Components/TestAndPreviewSlidebox/index.js +2 -2
- package/v2Containers/App/constants.js +3 -0
- package/v2Containers/App/tests/constants.test.js +61 -0
- package/v2Containers/CreativesContainer/index.js +60 -24
- package/v2Containers/Templates/index.js +72 -2
- package/v2Containers/Templates/sagas.js +1 -6
- package/v2Containers/Templates/tests/sagas.test.js +6 -23
- package/v2Containers/Templates/tests/webpush.test.js +375 -0
- package/v2Containers/WebPush/Create/index.js +91 -8
- package/v2Containers/WebPush/Create/index.scss +7 -0
- package/v2Containers/WebPush/Create/tests/getTemplateContent.test.js +348 -0
- package/v2Containers/WebPush/Create/tests/testAndPreviewIntegration.test.js +325 -0
|
@@ -79,6 +79,69 @@ const errorMessageForTags = {
|
|
|
79
79
|
TAG_BRACKET_COUNT_MISMATCH_ERROR: 'tagBracketCountMismatchError'
|
|
80
80
|
};
|
|
81
81
|
|
|
82
|
+
// Isolated input for EMAIL template-name: only this tiny component re-renders on each keystroke.
|
|
83
|
+
// formData is updated only on blur (onCommit), eliminating all re-renders during typing.
|
|
84
|
+
class HighFreqInput extends React.Component {
|
|
85
|
+
constructor(props) {
|
|
86
|
+
super(props);
|
|
87
|
+
this.state = { localValue: props.value || '' };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
componentDidUpdate(prevProps) {
|
|
91
|
+
if (prevProps.value !== this.props.value && this.state.localValue !== this.props.value) {
|
|
92
|
+
this.setState({ localValue: this.props.value || '' });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
handleChange = (e) => {
|
|
97
|
+
this.setState({ localValue: e.target.value });
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
handleBlur = (e) => {
|
|
101
|
+
this.props.onCommit(this.state.localValue);
|
|
102
|
+
if (this.props.onBlur) this.props.onBlur(e);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
render() {
|
|
106
|
+
const { value: _v, onCommit: _oc, onBlur: _ob, ...rest } = this.props;
|
|
107
|
+
return <CapInput {...rest} value={this.state.localValue} onChange={this.handleChange} onBlur={this.handleBlur} />;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Isolated wrapper for EMAIL template-subject: blur-only commit, same as HighFreqInput.
|
|
112
|
+
class HighFreqTagInput extends React.Component {
|
|
113
|
+
constructor(props) {
|
|
114
|
+
super(props);
|
|
115
|
+
this.state = { localInputValue: props.inputValue || '' };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
componentDidUpdate(prevProps) {
|
|
119
|
+
if (prevProps.inputValue !== this.props.inputValue && this.state.localInputValue !== this.props.inputValue) {
|
|
120
|
+
this.setState({ localInputValue: this.props.inputValue || '' });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
handleInputChange = (e) => {
|
|
125
|
+
this.setState({ localInputValue: e.target.value });
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
handleBlur = () => {
|
|
129
|
+
this.props.onCommit(this.state.localInputValue);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
render() {
|
|
133
|
+
const { inputValue: _iv, onCommit: _oc, inputOnChange: _ic, ...rest } = this.props;
|
|
134
|
+
return (
|
|
135
|
+
<CapTagListWithInput
|
|
136
|
+
{...rest}
|
|
137
|
+
inputValue={this.state.localInputValue}
|
|
138
|
+
inputOnChange={this.handleInputChange}
|
|
139
|
+
inputProps={{ ...(this.props.inputProps || {}), onBlur: this.handleBlur }}
|
|
140
|
+
/>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
82
145
|
class FormBuilder extends React.Component { // eslint-disable-line react/prefer-stateless-function
|
|
83
146
|
constructor(props) {
|
|
84
147
|
super(props);
|
|
@@ -352,6 +415,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
352
415
|
this.setState({tabCount: nextProps.tabCount});
|
|
353
416
|
}
|
|
354
417
|
if (nextProps.startValidation && nextProps.startValidation !== false && this.props.startValidation !== nextProps.startValidation) {
|
|
418
|
+
if (this.debouncedUpdateFormData) this.debouncedUpdateFormData.flush();
|
|
355
419
|
this.setState({checkValidation: true});
|
|
356
420
|
this.validateForm(null, null, true, true, () => {
|
|
357
421
|
//triggering the saveFormData or onSubmit when validation sets isFormValid to TRUE
|
|
@@ -3410,26 +3474,25 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
3410
3474
|
? formatMessage(messages.personalizationTagsErrorMessage)
|
|
3411
3475
|
: (errorType === TAG_BRACKET_COUNT_MISMATCH_ERROR ? formatMessage(globalMessages.unbalanacedCurlyBraces) : (val.errorMessage && ifError ? val.errorMessage : ''));
|
|
3412
3476
|
if (styling === 'semantic') {
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
!aiContentBotDisabled && (
|
|
3477
|
+
const isEmailStandaloneHighFreq = val.standalone && this.props.schema?.channel?.toUpperCase() === EMAIL;
|
|
3478
|
+
if (isEmailStandaloneHighFreq) {
|
|
3479
|
+
columns.push(
|
|
3480
|
+
<CapColumn key={val.id} span={val.width} offset={val.offset} style={val.style || {}}>
|
|
3481
|
+
<HighFreqInput
|
|
3482
|
+
id={val.id}
|
|
3483
|
+
errorMessage={errorMessageText}
|
|
3484
|
+
label={val.label}
|
|
3485
|
+
inductiveText={val.inductiveText}
|
|
3486
|
+
className={`input-primary chart-name-input${ifError ? ' error' : ''}`}
|
|
3487
|
+
style={val.style ? val.style : {}}
|
|
3488
|
+
placeholder={val.placeholder}
|
|
3489
|
+
onCommit={(newValue) => this.performFormDataUpdate(newValue, val)}
|
|
3490
|
+
onBlur={(e) => this.handleFieldBlur(e, val)}
|
|
3491
|
+
value={value || ""}
|
|
3492
|
+
disabled={val.disabled}
|
|
3493
|
+
size={val.size || "default"}
|
|
3494
|
+
/>
|
|
3495
|
+
{!aiContentBotDisabled && (
|
|
3433
3496
|
<CapAskAira.ContentGenerationBot
|
|
3434
3497
|
text={value || ""}
|
|
3435
3498
|
setText={this.handleSetText.bind(this, val)}
|
|
@@ -3438,12 +3501,48 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
3438
3501
|
rootStyle={{
|
|
3439
3502
|
bottom: "0.2rem",
|
|
3440
3503
|
right: "0.2rem",
|
|
3441
|
-
left: "auto",
|
|
3504
|
+
left: "auto",
|
|
3442
3505
|
}}
|
|
3443
3506
|
/>
|
|
3444
3507
|
)}
|
|
3445
|
-
|
|
3446
|
-
|
|
3508
|
+
</CapColumn>
|
|
3509
|
+
);
|
|
3510
|
+
} else {
|
|
3511
|
+
columns.push(
|
|
3512
|
+
<CapColumn key={val.id} span={val.width} offset={val.offset} style={val.style || {}}>
|
|
3513
|
+
<CapInput
|
|
3514
|
+
id={val.id}
|
|
3515
|
+
errorMessage={errorMessageText}
|
|
3516
|
+
label={val.label}
|
|
3517
|
+
inductiveText={val.inductiveText}
|
|
3518
|
+
className={`input-primary chart-name-input${ifError ? ' error' : ''}`}
|
|
3519
|
+
// fluid={val.fluid}
|
|
3520
|
+
style={val.style ? val.style : {}}
|
|
3521
|
+
placeholder={val.placeholder}
|
|
3522
|
+
onChange={(e) => this.updateFormData(e.target.value, val)}
|
|
3523
|
+
onBlur={(e) => this.handleFieldBlur(e, val)}
|
|
3524
|
+
value={value || ""}
|
|
3525
|
+
defaultValue={isVersionEnable ? this.state.formData[`${this.state.currentTab - 1}`][val.id] : this.state.formData[val.id]}
|
|
3526
|
+
disabled={val.disabled}
|
|
3527
|
+
size={val.size || "default"}
|
|
3528
|
+
/>
|
|
3529
|
+
{this.props.schema?.channel === EMAIL &&
|
|
3530
|
+
!aiContentBotDisabled && (
|
|
3531
|
+
<CapAskAira.ContentGenerationBot
|
|
3532
|
+
text={value || ""}
|
|
3533
|
+
setText={this.handleSetText.bind(this, val)}
|
|
3534
|
+
iconPlacement="float-br"
|
|
3535
|
+
iconSize="1.6rem"
|
|
3536
|
+
rootStyle={{
|
|
3537
|
+
bottom: "0.2rem",
|
|
3538
|
+
right: "0.2rem",
|
|
3539
|
+
left: "auto",
|
|
3540
|
+
}}
|
|
3541
|
+
/>
|
|
3542
|
+
)}
|
|
3543
|
+
</CapColumn>
|
|
3544
|
+
);
|
|
3545
|
+
}
|
|
3447
3546
|
}
|
|
3448
3547
|
break;
|
|
3449
3548
|
|
|
@@ -3684,37 +3783,48 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
3684
3783
|
isBEEAppEnableForCapTagList === false ||
|
|
3685
3784
|
channelForCapTagList !== 'EMAIL'
|
|
3686
3785
|
) {
|
|
3786
|
+
const isEmailStandaloneSubject = val.standalone && channelForCapTagList === EMAIL && val.id === 'template-subject';
|
|
3787
|
+
const tagListProps = {
|
|
3788
|
+
key: `input-${val.id}`,
|
|
3789
|
+
inputId: val.id,
|
|
3790
|
+
inputValue: this.state.formData[val.id] || '',
|
|
3791
|
+
inputPlaceholder: val.placeholder || '',
|
|
3792
|
+
inputErrorMessage: val.errorMessage && ifError ? val.errorMessage : '',
|
|
3793
|
+
inputRequired: val.required || false,
|
|
3794
|
+
inputDisabled: val.disabled || false,
|
|
3795
|
+
headingText: val.label || '',
|
|
3796
|
+
headingStyle: val.headingStyle || { marginTop: '3%', marginRight: '79%' },
|
|
3797
|
+
headingType: "h4",
|
|
3798
|
+
onTagSelect: (data) => this.callChildEvent(data, val, 'onTagSelect'),
|
|
3799
|
+
onContextChange: this.props.onContextChange,
|
|
3800
|
+
location: this.props.location,
|
|
3801
|
+
tags: this.props.tags ? this.props.tags : [],
|
|
3802
|
+
injectedTags: this.props.injectedTags ? this.props.injectedTags : {},
|
|
3803
|
+
className: val.className ? val.className : '',
|
|
3804
|
+
userLocale: this.state.translationLang,
|
|
3805
|
+
selectedOfferDetails: this.props.selectedOfferDetails,
|
|
3806
|
+
eventContextTags: this.props?.eventContextTags,
|
|
3807
|
+
waitEventContextTags: this.props?.waitEventContextTags,
|
|
3808
|
+
moduleFilterEnabled: moduleFilterEnabledForCapTagList,
|
|
3809
|
+
containerStyle: val.style || {},
|
|
3810
|
+
inputProps: val.inputProps || {},
|
|
3811
|
+
showInput: val.showInput !== false,
|
|
3812
|
+
showTagList: val.showTagList !== false,
|
|
3813
|
+
restrictPersonalization: this.props.restrictPersonalization,
|
|
3814
|
+
};
|
|
3687
3815
|
columns.push(
|
|
3688
3816
|
<CapColumn key={`input-${val.id}`} offset={val.offset} span={val.width ? val.width : ''} style={val.style ? val.style : {marginBottom: '16px'}}>
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
headingType="h4"
|
|
3701
|
-
onTagSelect={(data) => this.callChildEvent(data, val, 'onTagSelect')}
|
|
3702
|
-
onContextChange={this.props.onContextChange}
|
|
3703
|
-
location={this.props.location}
|
|
3704
|
-
tags={this.props.tags ? this.props.tags : []}
|
|
3705
|
-
injectedTags={this.props.injectedTags ? this.props.injectedTags : {}}
|
|
3706
|
-
className={val.className ? val.className : ''}
|
|
3707
|
-
userLocale={this.state.translationLang}
|
|
3708
|
-
selectedOfferDetails={this.props.selectedOfferDetails}
|
|
3709
|
-
eventContextTags={this.props?.eventContextTags}
|
|
3710
|
-
waitEventContextTags={this.props?.waitEventContextTags}
|
|
3711
|
-
moduleFilterEnabled={moduleFilterEnabledForCapTagList}
|
|
3712
|
-
containerStyle={val.style || {}}
|
|
3713
|
-
inputProps={val.inputProps || {}}
|
|
3714
|
-
showInput={val.showInput !== false}
|
|
3715
|
-
showTagList={val.showTagList !== false}
|
|
3716
|
-
restrictPersonalization={this.props.restrictPersonalization}
|
|
3717
|
-
/>
|
|
3817
|
+
{isEmailStandaloneSubject ? (
|
|
3818
|
+
<HighFreqTagInput
|
|
3819
|
+
{...tagListProps}
|
|
3820
|
+
onCommit={(newValue) => this.performFormDataUpdate(newValue, val)}
|
|
3821
|
+
/>
|
|
3822
|
+
) : (
|
|
3823
|
+
<CapTagListWithInput
|
|
3824
|
+
{...tagListProps}
|
|
3825
|
+
inputOnChange={(e) => this.updateFormData(e.target.value, val)}
|
|
3826
|
+
/>
|
|
3827
|
+
)}
|
|
3718
3828
|
</CapColumn>
|
|
3719
3829
|
);
|
|
3720
3830
|
}
|
|
@@ -69,8 +69,8 @@ const TestAndPreviewSlidebox = (props) => {
|
|
|
69
69
|
|
|
70
70
|
TestAndPreviewSlidebox.propTypes = {
|
|
71
71
|
// Channel prop - supports all channels
|
|
72
|
-
channel: PropTypes.oneOf([CHANNELS.EMAIL, CHANNELS.SMS, CHANNELS.RCS, CHANNELS.WHATSAPP, CHANNELS.INAPP, CHANNELS.MOBILEPUSH]),
|
|
73
|
-
currentChannel: PropTypes.oneOf([CHANNELS.EMAIL, CHANNELS.SMS, CHANNELS.RCS, CHANNELS.WHATSAPP, CHANNELS.INAPP, CHANNELS.MOBILEPUSH]), // Alternative prop name for backward compatibility
|
|
72
|
+
channel: PropTypes.oneOf([CHANNELS.EMAIL, CHANNELS.SMS, CHANNELS.RCS, CHANNELS.WHATSAPP, CHANNELS.INAPP, CHANNELS.MOBILEPUSH, CHANNELS.VIBER, CHANNELS.ZALO, CHANNELS.WEBPUSH]),
|
|
73
|
+
currentChannel: PropTypes.oneOf([CHANNELS.EMAIL, CHANNELS.SMS, CHANNELS.RCS, CHANNELS.WHATSAPP, CHANNELS.INAPP, CHANNELS.MOBILEPUSH, CHANNELS.VIBER, CHANNELS.ZALO, CHANNELS.WEBPUSH]), // Alternative prop name for backward compatibility
|
|
74
74
|
// All original props are passed through
|
|
75
75
|
show: PropTypes.bool.isRequired,
|
|
76
76
|
onClose: PropTypes.func.isRequired,
|
|
@@ -124,3 +124,6 @@ export const LOYALTY = 'loyalty';
|
|
|
124
124
|
export const FAILURE = 'FAILURE';
|
|
125
125
|
export const DATE_DISPLAY_FORMAT = 'D MMM YYYY';
|
|
126
126
|
export const TIME_DISPLAY_FORMAT = 'hh:mm A';
|
|
127
|
+
export const EXTERNAL_URL = 'EXTERNAL_URL';
|
|
128
|
+
export const URL = 'URL';
|
|
129
|
+
export const SITE_URL = 'SITE_URL';
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for App container constants
|
|
3
|
+
*
|
|
4
|
+
* Covers newly added URL type constants: EXTERNAL_URL, URL, SITE_URL
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
EXTERNAL_URL,
|
|
9
|
+
URL,
|
|
10
|
+
SITE_URL,
|
|
11
|
+
LOYALTY,
|
|
12
|
+
FAILURE,
|
|
13
|
+
DATE_DISPLAY_FORMAT,
|
|
14
|
+
TIME_DISPLAY_FORMAT,
|
|
15
|
+
} from '../constants';
|
|
16
|
+
|
|
17
|
+
describe('App constants', () => {
|
|
18
|
+
describe('URL type constants', () => {
|
|
19
|
+
it('should export EXTERNAL_URL with correct value', () => {
|
|
20
|
+
expect(EXTERNAL_URL).toBe('EXTERNAL_URL');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should export URL with correct value', () => {
|
|
24
|
+
expect(URL).toBe('URL');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should export SITE_URL with correct value', () => {
|
|
28
|
+
expect(SITE_URL).toBe('SITE_URL');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should have distinct values for all three URL constants', () => {
|
|
32
|
+
expect(EXTERNAL_URL).not.toBe(URL);
|
|
33
|
+
expect(EXTERNAL_URL).not.toBe(SITE_URL);
|
|
34
|
+
expect(URL).not.toBe(SITE_URL);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should be strings', () => {
|
|
38
|
+
expect(typeof EXTERNAL_URL).toBe('string');
|
|
39
|
+
expect(typeof URL).toBe('string');
|
|
40
|
+
expect(typeof SITE_URL).toBe('string');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('Other existing constants still exported correctly', () => {
|
|
45
|
+
it('should export LOYALTY', () => {
|
|
46
|
+
expect(LOYALTY).toBe('loyalty');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should export FAILURE', () => {
|
|
50
|
+
expect(FAILURE).toBe('FAILURE');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should export DATE_DISPLAY_FORMAT', () => {
|
|
54
|
+
expect(DATE_DISPLAY_FORMAT).toBe('D MMM YYYY');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should export TIME_DISPLAY_FORMAT', () => {
|
|
58
|
+
expect(TIME_DISPLAY_FORMAT).toBe('hh:mm A');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -1,4 +1,48 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
|
|
3
|
+
// Isolated input for the email template name field.
|
|
4
|
+
// Manages its own value in local state so keystrokes only re-render this
|
|
5
|
+
// small component, not the entire CreativesContainer → Email → FormBuilder tree.
|
|
6
|
+
class TemplateNameInputField extends React.Component {
|
|
7
|
+
constructor(props) {
|
|
8
|
+
super(props);
|
|
9
|
+
this.state = { localValue: props.initialValue || '' };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
componentDidUpdate(prevProps) {
|
|
13
|
+
// Sync from props only when the external value changed AND the user hasn't
|
|
14
|
+
// diverged from the previous prop value. This handles async data-load in edit
|
|
15
|
+
// mode without overwriting what the user is actively typing.
|
|
16
|
+
if (
|
|
17
|
+
prevProps.initialValue !== this.props.initialValue &&
|
|
18
|
+
this.state.localValue === (prevProps.initialValue || '')
|
|
19
|
+
) {
|
|
20
|
+
this.setState({ localValue: this.props.initialValue || '' });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
handleChange = (ev) => {
|
|
25
|
+
const { value } = ev.currentTarget;
|
|
26
|
+
this.setState({ localValue: value });
|
|
27
|
+
if (this.props.onChange) this.props.onChange(value);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
handleBlur = () => {
|
|
31
|
+
if (this.props.onBlur) this.props.onBlur(this.state.localValue);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
render() {
|
|
35
|
+
const { onChange: _onChange, initialValue: _initialValue, onBlur: _ob, ...rest } = this.props;
|
|
36
|
+
return (
|
|
37
|
+
<CapInput
|
|
38
|
+
{...rest}
|
|
39
|
+
value={this.state.localValue}
|
|
40
|
+
onChange={this.handleChange}
|
|
41
|
+
onBlur={this.handleBlur}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
2
46
|
import PropTypes from 'prop-types';
|
|
3
47
|
import {
|
|
4
48
|
CAP_SPACE_16, CAP_SPACE_32, CAP_SPACE_56, CAP_SPACE_64,
|
|
@@ -1753,30 +1797,22 @@ export class Creatives extends React.Component {
|
|
|
1753
1797
|
} />
|
|
1754
1798
|
)
|
|
1755
1799
|
|
|
1756
|
-
templateNameComponentInput = ({ formData, onFormDataChange, name }) =>
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
onChange={(ev) => {
|
|
1773
|
-
const { value } = ev.currentTarget;
|
|
1774
|
-
// Use optimized update for better performance
|
|
1775
|
-
this.updateTemplateNameImmediately(value, formData, onFormDataChange);
|
|
1776
|
-
}}
|
|
1777
|
-
/>
|
|
1778
|
-
);
|
|
1779
|
-
}
|
|
1800
|
+
templateNameComponentInput = ({ formData, onFormDataChange, name }) => (
|
|
1801
|
+
<TemplateNameInputField
|
|
1802
|
+
initialValue={name}
|
|
1803
|
+
suffix={<span />}
|
|
1804
|
+
onBlur={(committedValue) => {
|
|
1805
|
+
this.performTemplateNameUpdate(committedValue, formData, onFormDataChange);
|
|
1806
|
+
this.setState({ isEditName: false });
|
|
1807
|
+
}}
|
|
1808
|
+
onChange={(value) => {
|
|
1809
|
+
const isEmptyTemplateName = !value.trim();
|
|
1810
|
+
if (this.state.isTemplateNameEmpty !== isEmptyTemplateName) {
|
|
1811
|
+
this.setState({ isTemplateNameEmpty: isEmptyTemplateName });
|
|
1812
|
+
}
|
|
1813
|
+
}}
|
|
1814
|
+
/>
|
|
1815
|
+
)
|
|
1780
1816
|
|
|
1781
1817
|
showTemplateName = ({ formData, onFormDataChange }) => { //gets called from email/index after template data is fetched
|
|
1782
1818
|
const {
|
|
@@ -104,6 +104,9 @@ import {
|
|
|
104
104
|
VIBER as VIBER_CHANNEL,
|
|
105
105
|
FACEBOOK as FACEBOOK_CHANNEL,
|
|
106
106
|
CREATE,
|
|
107
|
+
EXTERNAL_URL,
|
|
108
|
+
URL,
|
|
109
|
+
SITE_URL,
|
|
107
110
|
} from '../App/constants';
|
|
108
111
|
import {MAX_WHATSAPP_TEMPLATES, WARNING_WHATSAPP_TEMPLATES , ACCOUNT_MAPPING_ON_CHANNEL, noFilteredWhatsappZaloTemplatesTitle, noFilteredWhatsappZaloTemplatesDesc, noApprovedWhatsappZaloTemplatesTitle, noApprovedWhatsappTemplatesDesc, zaloDescIllustration, noApprovedRcsTemplatesTitle, noApprovedRcsTemplatesDesc, ARCHIVE_STATUS_ACTIVE, ARCHIVE_STATUS_ARCHIVED, ARCHIVE_REFRESH_TYPE_ARCHIVE, ARCHIVE_REFRESH_TYPE_UNARCHIVE} from './constants';
|
|
109
112
|
import { COPY_OF, EMBEDDED } from '../../constants/unified';
|
|
@@ -1399,6 +1402,73 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
1399
1402
|
};
|
|
1400
1403
|
}
|
|
1401
1404
|
|
|
1405
|
+
case WEBPUSH: {
|
|
1406
|
+
// WebPush content is stored in creatives format (brandIcon, onClickAction, ctas)
|
|
1407
|
+
// Must be transformed to campaign/test-message format matching getTemplateContent() in WebPush/Create/index.js
|
|
1408
|
+
const webpushContent = get(baseContent, 'content.webpush', {});
|
|
1409
|
+
const {
|
|
1410
|
+
title = '',
|
|
1411
|
+
message = '',
|
|
1412
|
+
brandIcon,
|
|
1413
|
+
iconImageUrl: existingIconImageUrl,
|
|
1414
|
+
onClickAction,
|
|
1415
|
+
cta: existingCta,
|
|
1416
|
+
image,
|
|
1417
|
+
ctas: rawCtas,
|
|
1418
|
+
expandableDetails: existingExpandable,
|
|
1419
|
+
} = webpushContent;
|
|
1420
|
+
const accountId = get(template, 'definition.accountId', null);
|
|
1421
|
+
const templateName = template?.name || '';
|
|
1422
|
+
|
|
1423
|
+
// iconImageUrl stored as brandIcon in creatives format
|
|
1424
|
+
const iconImageUrl = brandIcon || existingIconImageUrl || undefined;
|
|
1425
|
+
|
|
1426
|
+
// cta stored as onClickAction in creatives format (type: URL|SITE_URL, url)
|
|
1427
|
+
// or already as cta in campaign format (type: EXTERNAL_URL|SITE_URL, actionLink)
|
|
1428
|
+
let cta = null;
|
|
1429
|
+
if (onClickAction) {
|
|
1430
|
+
const ctaType = onClickAction.type === URL ? EXTERNAL_URL : (onClickAction.type || SITE_URL);
|
|
1431
|
+
cta = { type: ctaType, actionLink: onClickAction.url || '' };
|
|
1432
|
+
} else if (existingCta) {
|
|
1433
|
+
cta = { type: existingCta.type || EXTERNAL_URL, actionLink: existingCta.actionLink || '' };
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
// expandableDetails: image → media[], ctas[] → mapped ctas
|
|
1437
|
+
let expandableDetails = null;
|
|
1438
|
+
const hasImage = !!image;
|
|
1439
|
+
const hasCtas = Array.isArray(rawCtas) && rawCtas.length > 0;
|
|
1440
|
+
if (hasImage || hasCtas) {
|
|
1441
|
+
expandableDetails = {
|
|
1442
|
+
media: hasImage ? [{ url: image, type: IMAGE }] : [],
|
|
1443
|
+
ctas: hasCtas ? rawCtas.map((ctaItem) => ({
|
|
1444
|
+
type: ctaItem?.type === URL ? EXTERNAL_URL : (ctaItem?.type || EXTERNAL_URL),
|
|
1445
|
+
action: ctaItem?.action || '',
|
|
1446
|
+
title: ctaItem?.actionText || ctaItem?.title || '',
|
|
1447
|
+
actionLink: ctaItem?.actionLink || '',
|
|
1448
|
+
})) : [],
|
|
1449
|
+
};
|
|
1450
|
+
} else if (existingExpandable) {
|
|
1451
|
+
expandableDetails = {
|
|
1452
|
+
media: existingExpandable?.media || [],
|
|
1453
|
+
ctas: existingExpandable?.ctas || [],
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
return {
|
|
1458
|
+
channel: WEBPUSH,
|
|
1459
|
+
accountId,
|
|
1460
|
+
content: {
|
|
1461
|
+
title,
|
|
1462
|
+
message,
|
|
1463
|
+
...(iconImageUrl ? { iconImageUrl } : {}),
|
|
1464
|
+
...(cta ? { cta } : {}),
|
|
1465
|
+
...(expandableDetails ? { expandableDetails } : {}),
|
|
1466
|
+
},
|
|
1467
|
+
messageSubject: templateName || title,
|
|
1468
|
+
offers: [],
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1402
1472
|
default:
|
|
1403
1473
|
console.warn(`Unsupported channel for content extraction: ${channelUpper}`);
|
|
1404
1474
|
return null;
|
|
@@ -1411,7 +1481,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
1411
1481
|
* @returns {Boolean} - True if channel supports Test and Preview
|
|
1412
1482
|
*/
|
|
1413
1483
|
isTestAndPreviewSupported = () => {
|
|
1414
|
-
const supportedChannels = [EMAIL, SMS, INAPP, MOBILE_PUSH, VIBER, ZALO];
|
|
1484
|
+
const supportedChannels = [EMAIL, SMS, INAPP, MOBILE_PUSH, VIBER, ZALO, WEBPUSH];
|
|
1415
1485
|
return supportedChannels.includes(this.state.channel.toUpperCase());
|
|
1416
1486
|
}
|
|
1417
1487
|
|
|
@@ -1971,7 +2041,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
1971
2041
|
// Show preview icon only for channels that don't support Test and Preview
|
|
1972
2042
|
(() => {
|
|
1973
2043
|
// Channels that have Test and Preview integrated
|
|
1974
|
-
const testAndPreviewChannels = [EMAIL, SMS, WHATSAPP, RCS, INAPP, MOBILE_PUSH, VIBER, ZALO];
|
|
2044
|
+
const testAndPreviewChannels = [EMAIL, SMS, WHATSAPP, RCS, INAPP, MOBILE_PUSH, VIBER, ZALO, WEBPUSH];
|
|
1975
2045
|
const isTestAndPreviewSupported = testAndPreviewChannels.includes(currentChannel.toUpperCase());
|
|
1976
2046
|
|
|
1977
2047
|
// Don't show preview icon if channel supports Test and Preview
|
|
@@ -6,7 +6,7 @@ import { CapNotification } from '@capillarytech/cap-ui-library';
|
|
|
6
6
|
// import { schema, normalize } from 'normalizr';
|
|
7
7
|
import * as Api from '../../services/api';
|
|
8
8
|
import * as types from './constants';
|
|
9
|
-
import { saveCdnConfigs, removeAllCdnLocalStorageItems
|
|
9
|
+
import { saveCdnConfigs, removeAllCdnLocalStorageItems } from '../../utils/cdnTransformation';
|
|
10
10
|
import { COPY_OF } from '../../constants/unified';
|
|
11
11
|
import { ZALO_TEMPLATE_INFO_REQUEST } from '../Zalo/constants';
|
|
12
12
|
import { getTemplateInfoById } from '../Zalo/saga';
|
|
@@ -107,11 +107,6 @@ export function* getOrgLevelCampaignSettings() {
|
|
|
107
107
|
|
|
108
108
|
export function* getCdnTransformationConfig() {
|
|
109
109
|
try {
|
|
110
|
-
// VAPT CAP-183204: prefer env vars injected via window.APP_ENV — avoids the
|
|
111
|
-
// API response that leaked CDN/S3 infrastructure details. Fallback to API
|
|
112
|
-
// keeps clusters that haven't received the env vars yet working during rollout.
|
|
113
|
-
if (initCdnConfigFromEnv()) return;
|
|
114
|
-
|
|
115
110
|
const res = yield call(Api.getCdnTransformationConfig);
|
|
116
111
|
if (res?.success && res?.status?.code === 200) {
|
|
117
112
|
const cdnConfigs = res?.response;
|
|
@@ -54,25 +54,10 @@ jest.mock('@capillarytech/cap-ui-library', () => ({
|
|
|
54
54
|
}));
|
|
55
55
|
|
|
56
56
|
describe('getCdnTransformationConfig saga', () => {
|
|
57
|
-
|
|
58
|
-
delete window.APP_ENV;
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("short-circuits to env config and skips the API call when window.APP_ENV is set", async () => {
|
|
62
|
-
const initSpy = jest.spyOn(cdnUtils, "initCdnConfigFromEnv").mockReturnValue(true);
|
|
63
|
-
const apiSpy = jest.spyOn(api, "getCdnTransformationConfig");
|
|
64
|
-
|
|
65
|
-
await expectSaga(getCdnTransformationConfig).run();
|
|
66
|
-
|
|
67
|
-
expect(initSpy).toHaveBeenCalled();
|
|
68
|
-
expect(apiSpy).not.toHaveBeenCalled();
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("handle valid response from api", async () => {
|
|
72
|
-
jest.spyOn(cdnUtils, "initCdnConfigFromEnv").mockReturnValue(false);
|
|
57
|
+
it("handle valid response from api", () => {
|
|
73
58
|
const saveCdnConfigsSpy = jest.spyOn(cdnUtils, "saveCdnConfigs");
|
|
74
59
|
|
|
75
|
-
|
|
60
|
+
expectSaga(getCdnTransformationConfig)
|
|
76
61
|
.provide([
|
|
77
62
|
[
|
|
78
63
|
call(api.getCdnTransformationConfig),
|
|
@@ -83,14 +68,13 @@ describe('getCdnTransformationConfig saga', () => {
|
|
|
83
68
|
expect(saveCdnConfigsSpy).toHaveBeenCalled();
|
|
84
69
|
});
|
|
85
70
|
|
|
86
|
-
it("handle error response from api",
|
|
87
|
-
jest.spyOn(cdnUtils, "initCdnConfigFromEnv").mockReturnValue(false);
|
|
71
|
+
it("handle error response from api", () => {
|
|
88
72
|
const removeAllCdnLocalStorageItemsSpy = jest.spyOn(
|
|
89
73
|
cdnUtils,
|
|
90
74
|
"removeAllCdnLocalStorageItems"
|
|
91
75
|
);
|
|
92
76
|
|
|
93
|
-
|
|
77
|
+
expectSaga(getCdnTransformationConfig)
|
|
94
78
|
.provide([
|
|
95
79
|
[
|
|
96
80
|
call(api.getCdnTransformationConfig),
|
|
@@ -101,8 +85,7 @@ describe('getCdnTransformationConfig saga', () => {
|
|
|
101
85
|
expect(removeAllCdnLocalStorageItemsSpy).toHaveBeenCalled();
|
|
102
86
|
});
|
|
103
87
|
|
|
104
|
-
it("remove localStorage items when an error is thrown",
|
|
105
|
-
jest.spyOn(cdnUtils, "initCdnConfigFromEnv").mockReturnValue(false);
|
|
88
|
+
it("remove localStorage items when an error is thrown", () => {
|
|
106
89
|
const saveCdnConfigsSpy = jest.spyOn(cdnUtils, "saveCdnConfigs").mockImplementation(()=>{
|
|
107
90
|
throw new Error()
|
|
108
91
|
});
|
|
@@ -111,7 +94,7 @@ describe('getCdnTransformationConfig saga', () => {
|
|
|
111
94
|
"removeAllCdnLocalStorageItems"
|
|
112
95
|
);
|
|
113
96
|
|
|
114
|
-
|
|
97
|
+
expectSaga(getCdnTransformationConfig)
|
|
115
98
|
.provide([
|
|
116
99
|
[
|
|
117
100
|
call(api.getCdnTransformationConfig),
|