@capillarytech/creatives-library 8.0.168 → 8.0.170-beta.1
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/app.js +4 -0
- package/containers/App/constants.js +2 -0
- package/package.json +2 -1
- package/utils/commonUtils.js +50 -0
- package/utils/test-utils.js +6 -2
- package/utils/tests/newrelic.test.js +546 -0
- package/v2Components/CapActionButton/index.js +52 -12
- package/v2Components/CapActionButton/messages.js +4 -0
- package/v2Components/CapActionButton/tests/index.test.js +135 -0
- package/v2Components/CapDeviceContent/index.js +5 -0
- package/v2Components/CapInAppCTA/index.js +29 -14
- package/v2Components/CapInAppCTA/index.scss +0 -2
- package/v2Components/CapInAppCTA/messages.js +4 -0
- package/v2Components/CapMpushCTA/index.js +54 -38
- package/v2Components/CapMpushCTA/index.scss +2 -2
- package/v2Components/CapMpushCTA/messages.js +4 -0
- package/v2Components/CapTagListWithInput/index.js +169 -0
- package/v2Components/CapTagListWithInput/messages.js +10 -0
- package/v2Components/FormBuilder/index.js +93 -1
- package/v2Components/TestAndPreviewSlidebox/PreviewSection.js +1 -1
- package/v2Components/TestAndPreviewSlidebox/index.js +24 -4
- package/v2Containers/Email/index.js +64 -3
- package/v2Containers/Email/initialSchema.js +7 -21
- package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +2 -2
- package/v2Containers/Line/Container/Wrapper/tests/index.test.js +56 -1
- package/v2Containers/Line/Container/Wrapper/utils.js +6 -4
- package/v2Containers/MobilePush/Create/index.js +24 -3
- package/v2Containers/MobilePush/commonMethods.js +25 -3
- package/v2Containers/MobilePushNew/components/CtaButtons.js +20 -0
- package/v2Containers/MobilePushNew/components/MediaUploaders.js +31 -3
- package/v2Containers/MobilePushNew/components/PlatformContentFields.js +34 -3
- package/v2Containers/MobilePushNew/components/tests/MediaUploaders.test.js +200 -5
- package/v2Containers/MobilePushNew/components/tests/PlatformContentFields.test.js +59 -1
- package/v2Containers/MobilePushNew/index.js +9 -0
- package/v2Containers/MobilePushNew/index.scss +2 -1
- package/v2Containers/Rcs/index.js +77 -71
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +15270 -492
- package/v2Containers/Viber/index.js +102 -76
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { injectIntl, intlShape } from 'react-intl';
|
|
4
|
+
import CapRow from '@capillarytech/cap-ui-library/CapRow';
|
|
5
|
+
import CapHeading from '@capillarytech/cap-ui-library/CapHeading';
|
|
6
|
+
import CapInput from '@capillarytech/cap-ui-library/CapInput';
|
|
7
|
+
import CapColumn from '@capillarytech/cap-ui-library/CapColumn';
|
|
8
|
+
import TagList from '../../v2Containers/TagList';
|
|
9
|
+
import messages from './messages';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* CapTagListWithInput - A reusable component that combines TagList and CapInput
|
|
13
|
+
* for external links/URLs that can be used across all channels
|
|
14
|
+
*/
|
|
15
|
+
export const CapTagListWithInput = (props) => {
|
|
16
|
+
const {
|
|
17
|
+
intl,
|
|
18
|
+
// TagList props
|
|
19
|
+
tags = [],
|
|
20
|
+
injectedTags = {},
|
|
21
|
+
location = {},
|
|
22
|
+
selectedOfferDetails = [],
|
|
23
|
+
onTagSelect,
|
|
24
|
+
onContextChange,
|
|
25
|
+
moduleFilterEnabled = true,
|
|
26
|
+
className = '',
|
|
27
|
+
userLocale = 'en',
|
|
28
|
+
eventContextTags = [],
|
|
29
|
+
// CapInput props
|
|
30
|
+
inputId,
|
|
31
|
+
inputValue = '',
|
|
32
|
+
inputOnChange,
|
|
33
|
+
inputPlaceholder,
|
|
34
|
+
inputErrorMessage = '',
|
|
35
|
+
inputSize = 'default',
|
|
36
|
+
inputMaxLength,
|
|
37
|
+
inputRequired = false,
|
|
38
|
+
inputDisabled = false,
|
|
39
|
+
// Layout props
|
|
40
|
+
headingText,
|
|
41
|
+
headingType = 'h4',
|
|
42
|
+
headingStyle = {},
|
|
43
|
+
containerStyle = {},
|
|
44
|
+
tagListStyle = {},
|
|
45
|
+
inputStyle = {},
|
|
46
|
+
// Custom props
|
|
47
|
+
showHeading = true,
|
|
48
|
+
showTagList = true,
|
|
49
|
+
showInput = true,
|
|
50
|
+
inputProps = {},
|
|
51
|
+
} = props;
|
|
52
|
+
|
|
53
|
+
const { formatMessage } = intl;
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<CapColumn style={containerStyle}>
|
|
57
|
+
<CapRow style={{display: 'flex', flexDirection: 'row'}}>
|
|
58
|
+
{showHeading && headingText && (
|
|
59
|
+
<CapHeading type={headingType} style={headingStyle}>
|
|
60
|
+
{headingText}
|
|
61
|
+
</CapHeading>
|
|
62
|
+
)}
|
|
63
|
+
|
|
64
|
+
{showTagList && (
|
|
65
|
+
<TagList
|
|
66
|
+
key={`${inputId}_tags`}
|
|
67
|
+
className={className}
|
|
68
|
+
moduleFilterEnabled={moduleFilterEnabled}
|
|
69
|
+
label={formatMessage(messages.addLabels)}
|
|
70
|
+
onTagSelect={onTagSelect}
|
|
71
|
+
onContextChange={onContextChange}
|
|
72
|
+
location={location}
|
|
73
|
+
tags={tags}
|
|
74
|
+
injectedTags={injectedTags}
|
|
75
|
+
userLocale={userLocale}
|
|
76
|
+
selectedOfferDetails={selectedOfferDetails}
|
|
77
|
+
eventContextTags={eventContextTags}
|
|
78
|
+
style={tagListStyle}
|
|
79
|
+
/>
|
|
80
|
+
)}
|
|
81
|
+
</CapRow>
|
|
82
|
+
{showInput && (
|
|
83
|
+
<CapInput
|
|
84
|
+
id={inputId}
|
|
85
|
+
onChange={inputOnChange}
|
|
86
|
+
placeholder={inputPlaceholder}
|
|
87
|
+
value={inputValue}
|
|
88
|
+
size={inputSize}
|
|
89
|
+
maxLength={inputMaxLength}
|
|
90
|
+
errorMessage={inputErrorMessage}
|
|
91
|
+
isRequired={inputRequired}
|
|
92
|
+
disabled={inputDisabled}
|
|
93
|
+
style={inputStyle}
|
|
94
|
+
{...inputProps}
|
|
95
|
+
/>
|
|
96
|
+
)}
|
|
97
|
+
</CapColumn>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
CapTagListWithInput.propTypes = {
|
|
102
|
+
intl: intlShape.isRequired,
|
|
103
|
+
// TagList props
|
|
104
|
+
tags: PropTypes.array,
|
|
105
|
+
injectedTags: PropTypes.object,
|
|
106
|
+
location: PropTypes.object,
|
|
107
|
+
selectedOfferDetails: PropTypes.array,
|
|
108
|
+
onTagSelect: PropTypes.func.isRequired,
|
|
109
|
+
onContextChange: PropTypes.func.isRequired,
|
|
110
|
+
moduleFilterEnabled: PropTypes.bool,
|
|
111
|
+
className: PropTypes.string,
|
|
112
|
+
userLocale: PropTypes.string,
|
|
113
|
+
eventContextTags: PropTypes.array,
|
|
114
|
+
|
|
115
|
+
// CapInput props
|
|
116
|
+
inputId: PropTypes.string.isRequired,
|
|
117
|
+
inputValue: PropTypes.string,
|
|
118
|
+
inputOnChange: PropTypes.func.isRequired,
|
|
119
|
+
inputPlaceholder: PropTypes.string,
|
|
120
|
+
inputErrorMessage: PropTypes.string,
|
|
121
|
+
inputSize: PropTypes.oneOf(['small', 'default', 'large']),
|
|
122
|
+
inputMaxLength: PropTypes.number,
|
|
123
|
+
inputRequired: PropTypes.bool,
|
|
124
|
+
inputDisabled: PropTypes.bool,
|
|
125
|
+
inputProps: PropTypes.object,
|
|
126
|
+
|
|
127
|
+
// Layout props
|
|
128
|
+
headingText: PropTypes.string,
|
|
129
|
+
headingType: PropTypes.oneOf(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']),
|
|
130
|
+
headingStyle: PropTypes.object,
|
|
131
|
+
containerStyle: PropTypes.object,
|
|
132
|
+
tagListStyle: PropTypes.object,
|
|
133
|
+
inputStyle: PropTypes.object,
|
|
134
|
+
|
|
135
|
+
// Custom props
|
|
136
|
+
showHeading: PropTypes.bool,
|
|
137
|
+
showTagList: PropTypes.bool,
|
|
138
|
+
showInput: PropTypes.bool,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
CapTagListWithInput.defaultProps = {
|
|
142
|
+
tags: [],
|
|
143
|
+
injectedTags: {},
|
|
144
|
+
location: {},
|
|
145
|
+
selectedOfferDetails: [],
|
|
146
|
+
moduleFilterEnabled: true,
|
|
147
|
+
className: '',
|
|
148
|
+
userLocale: 'en',
|
|
149
|
+
eventContextTags: [],
|
|
150
|
+
inputValue: '',
|
|
151
|
+
inputSize: 'default',
|
|
152
|
+
inputRequired: false,
|
|
153
|
+
inputDisabled: false,
|
|
154
|
+
inputPlaceholder: '',
|
|
155
|
+
inputErrorMessage: '',
|
|
156
|
+
inputMaxLength: undefined,
|
|
157
|
+
headingText: '',
|
|
158
|
+
headingType: 'h4',
|
|
159
|
+
headingStyle: {},
|
|
160
|
+
containerStyle: {},
|
|
161
|
+
tagListStyle: {},
|
|
162
|
+
inputStyle: {},
|
|
163
|
+
showHeading: true,
|
|
164
|
+
showTagList: true,
|
|
165
|
+
showInput: true,
|
|
166
|
+
inputProps: {},
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export default injectIntl(CapTagListWithInput);
|
|
@@ -37,6 +37,7 @@ import { createStructuredSelector } from 'reselect';
|
|
|
37
37
|
import { CAP_SPACE_12, CAP_SPACE_08, FONT_COLOR_05, FONT_COLOR_04 } from '@capillarytech/cap-ui-library/styled/variables';
|
|
38
38
|
import TemplatePreview from '../TemplatePreview';
|
|
39
39
|
import TagList from '../../v2Containers/TagList';
|
|
40
|
+
import CapTagListWithInput from '../CapTagListWithInput';
|
|
40
41
|
import SlideBox from '../SlideBox';
|
|
41
42
|
import CardGrid from '../CardGrid';
|
|
42
43
|
import CKEditor from "../Ckeditor/";
|
|
@@ -2743,6 +2744,40 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
2743
2744
|
</CapColumn>
|
|
2744
2745
|
);
|
|
2745
2746
|
break;
|
|
2747
|
+
case "cap-tag-list-with-input":
|
|
2748
|
+
ifError = this.state.checkValidation && this.state.errorData[val.id];
|
|
2749
|
+
columns.push(
|
|
2750
|
+
<CapColumn key={`input-${val.id}`} span={val.width || 10} offset={val.offset}>
|
|
2751
|
+
<CapTagListWithInput
|
|
2752
|
+
key={`input-${val.id}`}
|
|
2753
|
+
inputId={val.id}
|
|
2754
|
+
inputValue={this.state.formData[val.id] || ''}
|
|
2755
|
+
inputOnChange={(e) => this.updateFormData(e.target.value, val)}
|
|
2756
|
+
inputPlaceholder={val.placeholder || ''}
|
|
2757
|
+
inputErrorMessage={val.errorMessage && ifError ? val.errorMessage : ''}
|
|
2758
|
+
inputRequired={val.required || false}
|
|
2759
|
+
inputDisabled={val.disabled || false}
|
|
2760
|
+
headingText={val.label || ''}
|
|
2761
|
+
headingStyle={val.headingStyle || { marginTop: '3%', marginRight: '79%' }}
|
|
2762
|
+
headingType="h4"
|
|
2763
|
+
onTagSelect={(data) => this.callChildEvent(data, val, 'onTagSelect')}
|
|
2764
|
+
onContextChange={this.props.onContextChange}
|
|
2765
|
+
location={this.props.location}
|
|
2766
|
+
tags={this.props.tags ? this.props.tags : []}
|
|
2767
|
+
injectedTags={this.props.injectedTags ? this.props.injectedTags : {}}
|
|
2768
|
+
className={val.className ? val.className : ''}
|
|
2769
|
+
userLocale={this.props.userLocale}
|
|
2770
|
+
selectedOfferDetails={this.props.selectedOfferDetails}
|
|
2771
|
+
eventContextTags={this.props?.eventContextTags}
|
|
2772
|
+
moduleFilterEnabled={this.props.location && this.props.location.query && this.props.location.query.type !== 'embedded'}
|
|
2773
|
+
containerStyle={val.style || {}}
|
|
2774
|
+
inputProps={val.inputProps || {}}
|
|
2775
|
+
showInput={val.showInput !== false}
|
|
2776
|
+
showTagList={val.showTagList !== false}
|
|
2777
|
+
/>
|
|
2778
|
+
</CapColumn>
|
|
2779
|
+
);
|
|
2780
|
+
break;
|
|
2746
2781
|
case "tabs":
|
|
2747
2782
|
columns.push(
|
|
2748
2783
|
<CapColumn key="input" span={10}>
|
|
@@ -3343,7 +3378,13 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
3343
3378
|
const currentLang = (!_.isEmpty(this.state.formData) && !_.isEmpty(this.state.formData[`${this.state.currentTab - 1}`]) && !_.isEmpty(this.state.formData[`${this.state.currentTab - 1}`].selectedLanguages) && this.state.formData[`${this.state.currentTab - 1}`].selectedLanguages[langIndex]) ? this.state.formData[`${this.state.currentTab - 1}`].selectedLanguages[langIndex] : this.props.baseLanguage;
|
|
3344
3379
|
const isBEEAppEnable = this.checkBeeEditorAllowedForLibrary();
|
|
3345
3380
|
|
|
3346
|
-
|
|
3381
|
+
// Always render Subject TagList (title-tagList) even when Bee editor is active for EMAIL channel
|
|
3382
|
+
if (
|
|
3383
|
+
val.id === 'title-tagList' ||
|
|
3384
|
+
!(_.get(this.state, `formData[${this.state.currentTab - 1}][${currentLang}].is_drag_drop`, false)) ||
|
|
3385
|
+
isBEEAppEnable === false ||
|
|
3386
|
+
channel !== 'EMAIL'
|
|
3387
|
+
) {
|
|
3347
3388
|
columns.push(
|
|
3348
3389
|
<CapColumn key={`input-${val.id}`} offset={val.offset} span={val.width ? val.width : ''} style={val.style ? val.style : {marginBottom: '16px'}}>
|
|
3349
3390
|
<TagList
|
|
@@ -3366,6 +3407,57 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
3366
3407
|
);
|
|
3367
3408
|
}
|
|
3368
3409
|
break;
|
|
3410
|
+
case "cap-tag-list-with-input":
|
|
3411
|
+
let moduleFilterEnabledForCapTagList = this.props.location && this.props.location.query && this.props.location.query.type !== 'embedded';
|
|
3412
|
+
const channelForCapTagList = _.get(this.props, 'schema.channel', "");
|
|
3413
|
+
if (channelForCapTagList === 'EMAIL') {
|
|
3414
|
+
moduleFilterEnabledForCapTagList = this.props.isFullMode;
|
|
3415
|
+
}
|
|
3416
|
+
const langIndexForCapTagList = 0;
|
|
3417
|
+
const currentLangForCapTagList = (!_.isEmpty(this.state.formData) && !_.isEmpty(this.state.formData[`${this.state.currentTab - 1}`]) && !_.isEmpty(this.state.formData[`${this.state.currentTab - 1}`].selectedLanguages) && this.state.formData[`${this.state.currentTab - 1}`].selectedLanguages[langIndexForCapTagList]) ? this.state.formData[`${this.state.currentTab - 1}`].selectedLanguages[langIndexForCapTagList] : this.props.baseLanguage;
|
|
3418
|
+
const isBEEAppEnableForCapTagList = this.checkBeeEditorAllowedForLibrary();
|
|
3419
|
+
ifError = this.state.checkValidation && (isVersionEnable ? this.state.errorData[`${this.state.currentTab - 1}`][val.id] : this.state.errorData[val.id]);
|
|
3420
|
+
|
|
3421
|
+
// Always render Subject CapTagListWithInput even when Bee editor is active for EMAIL channel
|
|
3422
|
+
if (
|
|
3423
|
+
val.id === 'template-subject' ||
|
|
3424
|
+
!(_.get(this.state, `formData[${this.state.currentTab - 1}][${currentLangForCapTagList}].is_drag_drop`, false)) ||
|
|
3425
|
+
isBEEAppEnableForCapTagList === false ||
|
|
3426
|
+
channelForCapTagList !== 'EMAIL'
|
|
3427
|
+
) {
|
|
3428
|
+
columns.push(
|
|
3429
|
+
<CapColumn key={`input-${val.id}`} offset={val.offset} span={val.width ? val.width : ''} style={val.style ? val.style : {marginBottom: '16px'}}>
|
|
3430
|
+
<CapTagListWithInput
|
|
3431
|
+
key={`input-${val.id}`}
|
|
3432
|
+
inputId={val.id}
|
|
3433
|
+
inputValue={this.state.formData[val.id] || ''}
|
|
3434
|
+
inputOnChange={(e) => this.updateFormData(e.target.value, val)}
|
|
3435
|
+
inputPlaceholder={val.placeholder || ''}
|
|
3436
|
+
inputErrorMessage={val.errorMessage && ifError ? val.errorMessage : ''}
|
|
3437
|
+
inputRequired={val.required || false}
|
|
3438
|
+
inputDisabled={val.disabled || false}
|
|
3439
|
+
headingText={val.label || ''}
|
|
3440
|
+
headingStyle={val.headingStyle || { marginTop: '3%', marginRight: '79%' }}
|
|
3441
|
+
headingType="h4"
|
|
3442
|
+
onTagSelect={(data) => this.callChildEvent(data, val, 'onTagSelect')}
|
|
3443
|
+
onContextChange={this.props.onContextChange}
|
|
3444
|
+
location={this.props.location}
|
|
3445
|
+
tags={this.props.tags ? this.props.tags : []}
|
|
3446
|
+
injectedTags={this.props.injectedTags ? this.props.injectedTags : {}}
|
|
3447
|
+
className={val.className ? val.className : ''}
|
|
3448
|
+
userLocale={this.state.translationLang}
|
|
3449
|
+
selectedOfferDetails={this.props.selectedOfferDetails}
|
|
3450
|
+
eventContextTags={this.props?.eventContextTags}
|
|
3451
|
+
moduleFilterEnabled={moduleFilterEnabledForCapTagList}
|
|
3452
|
+
containerStyle={val.style || {}}
|
|
3453
|
+
inputProps={val.inputProps || {}}
|
|
3454
|
+
showInput={val.showInput !== false}
|
|
3455
|
+
showTagList={val.showTagList !== false}
|
|
3456
|
+
/>
|
|
3457
|
+
</CapColumn>
|
|
3458
|
+
);
|
|
3459
|
+
}
|
|
3460
|
+
break;
|
|
3369
3461
|
case "button":
|
|
3370
3462
|
if (styling === 'semantic' && val.metaType === 'submit-button') {
|
|
3371
3463
|
columns.push(
|
|
@@ -20,7 +20,7 @@ const PreviewSection = ({
|
|
|
20
20
|
device={previewDevice}
|
|
21
21
|
onDeviceChange={setPreviewDevice}
|
|
22
22
|
customer={selectedCustomer}
|
|
23
|
-
subject={formData['template-subject']}
|
|
23
|
+
subject={previewDataHtml?.resolvedTitle || formData['template-subject']}
|
|
24
24
|
>
|
|
25
25
|
{isUpdatingPreview && (
|
|
26
26
|
<CapRow className="loading-container">
|
|
@@ -108,6 +108,21 @@ const TestAndPreviewSlidebox = (props) => {
|
|
|
108
108
|
requiredTags.some((tag) => !customValues[tag.fullPath])
|
|
109
109
|
), [requiredTags, customValues]);
|
|
110
110
|
|
|
111
|
+
// Function to resolve tags in text with custom values
|
|
112
|
+
const resolveTagsInText = (text, tagValues) => {
|
|
113
|
+
if (!text) return text;
|
|
114
|
+
let resolvedText = text;
|
|
115
|
+
|
|
116
|
+
// Replace each tag with its custom value
|
|
117
|
+
Object.keys(tagValues).forEach((tagPath) => {
|
|
118
|
+
const tagName = tagPath.split('.').pop(); // Get the actual tag name from the path
|
|
119
|
+
const tagRegex = new RegExp(`{{${tagName}}}`, 'g');
|
|
120
|
+
resolvedText = resolvedText.replace(tagRegex, tagValues[tagPath] || `{{${tagName}}}`);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return resolvedText;
|
|
124
|
+
};
|
|
125
|
+
|
|
111
126
|
// Get the current content based on editor type
|
|
112
127
|
const getCurrentContent = useMemo(() => {
|
|
113
128
|
const currentTabData = formData[currentTab - 1];
|
|
@@ -380,9 +395,10 @@ const TestAndPreviewSlidebox = (props) => {
|
|
|
380
395
|
setCustomValues(updatedValues);
|
|
381
396
|
|
|
382
397
|
// Update preview with prefilled values
|
|
398
|
+
const resolvedSubject = resolveTagsInText(formData['template-subject'], updatedValues);
|
|
383
399
|
const payload = {
|
|
384
400
|
channel: EMAIL,
|
|
385
|
-
messageTitle:
|
|
401
|
+
messageTitle: resolvedSubject,
|
|
386
402
|
messageBody: getCurrentContent,
|
|
387
403
|
resolvedTags: updatedValues,
|
|
388
404
|
userId: selectedCustomer?.customerId,
|
|
@@ -501,9 +517,10 @@ const TestAndPreviewSlidebox = (props) => {
|
|
|
501
517
|
setCustomValues(emptyValues);
|
|
502
518
|
|
|
503
519
|
// Update preview with empty values
|
|
520
|
+
const resolvedSubject = resolveTagsInText(formData['template-subject'], emptyValues);
|
|
504
521
|
const payload = {
|
|
505
522
|
channel: EMAIL,
|
|
506
|
-
messageTitle:
|
|
523
|
+
messageTitle: resolvedSubject,
|
|
507
524
|
messageBody: getCurrentContent,
|
|
508
525
|
resolvedTags: emptyValues,
|
|
509
526
|
userId: selectedCustomer?.customerId,
|
|
@@ -517,9 +534,12 @@ const TestAndPreviewSlidebox = (props) => {
|
|
|
517
534
|
// Include unsubscribe tag if content contains it
|
|
518
535
|
const resolvedTags = { ...customValues };
|
|
519
536
|
|
|
537
|
+
// Resolve subject tags with custom values
|
|
538
|
+
const resolvedSubject = resolveTagsInText(formData['template-subject'], customValues);
|
|
539
|
+
|
|
520
540
|
const payload = {
|
|
521
541
|
channel: EMAIL,
|
|
522
|
-
messageTitle:
|
|
542
|
+
messageTitle: resolvedSubject,
|
|
523
543
|
messageBody: getCurrentContent,
|
|
524
544
|
resolvedTags,
|
|
525
545
|
userId: selectedCustomer?.customerId,
|
|
@@ -585,7 +605,7 @@ const TestAndPreviewSlidebox = (props) => {
|
|
|
585
605
|
emailMessageContent: {
|
|
586
606
|
channel: EMAIL,
|
|
587
607
|
messageBody: previewData?.resolvedBody || getCurrentContent,
|
|
588
|
-
messageSubject: previewData?.resolvedTitle || formData['template-subject'],
|
|
608
|
+
messageSubject: previewData?.resolvedTitle || resolveTagsInText(formData['template-subject'], customValues),
|
|
589
609
|
},
|
|
590
610
|
}, messageMetaConfigId, (response) => {
|
|
591
611
|
const payload = {
|
|
@@ -100,6 +100,14 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
|
|
|
100
100
|
"template-name": {
|
|
101
101
|
onChange: this.onTemplateNameChange,
|
|
102
102
|
},
|
|
103
|
+
// allow tag insertion into Subject input field via a dedicated TagList
|
|
104
|
+
"title-tagList": {
|
|
105
|
+
onTagSelect: this.onTagSelect,
|
|
106
|
+
},
|
|
107
|
+
// unified subject field with input and tag selection
|
|
108
|
+
"template-subject": {
|
|
109
|
+
onTagSelect: this.onTagSelect,
|
|
110
|
+
},
|
|
103
111
|
"template-version": {
|
|
104
112
|
onSelect: this.handleVersionSelect,
|
|
105
113
|
addVersion: this.addVersion,
|
|
@@ -646,7 +654,44 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
|
|
|
646
654
|
|
|
647
655
|
}
|
|
648
656
|
|
|
649
|
-
onTagSelect = (data) => {
|
|
657
|
+
onTagSelect = (data, currentTab, sourceVal) => {
|
|
658
|
+
// If tag is selected from Subject's TagList or unified subject field, insert into subject input
|
|
659
|
+
if (sourceVal && (sourceVal.id === 'title-tagList' || sourceVal.id === 'template-subject')) {
|
|
660
|
+
const tagToInsert = `{{${data}}}`;
|
|
661
|
+
const formData = _.cloneDeep(this.state.formData);
|
|
662
|
+
const fieldId = 'template-subject';
|
|
663
|
+
const input = document.getElementById(fieldId) || document.querySelector(`#${fieldId} input`);
|
|
664
|
+
let subjectValue = formData[fieldId] || '';
|
|
665
|
+
try {
|
|
666
|
+
if (input && (typeof input.selectionStart === 'number')) {
|
|
667
|
+
const startPos = input.selectionStart;
|
|
668
|
+
const endPos = input.selectionEnd;
|
|
669
|
+
subjectValue = `${subjectValue.substring(0, startPos)}${tagToInsert}${subjectValue.substring(endPos)}`;
|
|
670
|
+
formData[fieldId] = subjectValue;
|
|
671
|
+
this.setState({ formData }, () => {
|
|
672
|
+
try {
|
|
673
|
+
input.focus();
|
|
674
|
+
const newPos = startPos + tagToInsert.length;
|
|
675
|
+
input.selectionStart = newPos;
|
|
676
|
+
input.selectionEnd = newPos;
|
|
677
|
+
} catch (e) {}
|
|
678
|
+
});
|
|
679
|
+
} else {
|
|
680
|
+
subjectValue = `${subjectValue}${tagToInsert}`;
|
|
681
|
+
formData[fieldId] = subjectValue;
|
|
682
|
+
this.setState({ formData });
|
|
683
|
+
if (input) {
|
|
684
|
+
try { input.value = subjectValue; } catch (e) {}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
} catch (e) {
|
|
688
|
+
// fallback safe append
|
|
689
|
+
formData[fieldId] = `${subjectValue}${tagToInsert}`;
|
|
690
|
+
this.setState({ formData });
|
|
691
|
+
}
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
650
695
|
const isDragDrop = this.state.formData[`${this.state.currentTab - 1}`][this.state.formData[`${this.state.currentTab - 1}`].activeTab].is_drag_drop;
|
|
651
696
|
const formData = _.cloneDeep(this.state.formData);
|
|
652
697
|
const isBEESupport = (this.props.location.query.isBEESupport !== "false") || false;
|
|
@@ -1546,8 +1591,24 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
|
|
|
1546
1591
|
if (schema.standalone) {
|
|
1547
1592
|
if ((this.props.location.query.module === "loyalty" || this.props.location.query.module === "dvs" || this.props.location.query.module === "timeline" || this.props.location.query.module === "coupon_expiry" ||
|
|
1548
1593
|
this.props.location.query.module === "library")) {
|
|
1549
|
-
|
|
1550
|
-
|
|
1594
|
+
// For the new unified cap-tag-list-with-input component, we want to show the Add label button
|
|
1595
|
+
// but hide the input field in library mode
|
|
1596
|
+
const subjectField = schema.standalone.sections[0].inputFields[0];
|
|
1597
|
+
if (subjectField && subjectField.type === "cap-tag-list-with-input") {
|
|
1598
|
+
// Only hide input in library mode, keep it visible in other modes
|
|
1599
|
+
if (this.props.location.query.module === "library") {
|
|
1600
|
+
_.set(schema, 'standalone.sections[0].inputFields[0].showInput', false);
|
|
1601
|
+
_.set(schema, 'standalone.sections[0].inputFields[0].showTagList', true);
|
|
1602
|
+
} else {
|
|
1603
|
+
// In other modes (loyalty, dvs, timeline, coupon_expiry), keep both input and tag list visible
|
|
1604
|
+
_.set(schema, 'standalone.sections[0].inputFields[0].showInput', true);
|
|
1605
|
+
_.set(schema, 'standalone.sections[0].inputFields[0].showTagList', true);
|
|
1606
|
+
}
|
|
1607
|
+
} else {
|
|
1608
|
+
// For backward compatibility with old separate fields
|
|
1609
|
+
_.set(schema, 'standalone.sections[0].inputFields[0].style.display', "" );
|
|
1610
|
+
_.set(schema, 'standalone.sections[0].inputFields[0].labelStyle.display', "");
|
|
1611
|
+
}
|
|
1551
1612
|
}
|
|
1552
1613
|
this.setState({schema});
|
|
1553
1614
|
}
|
|
@@ -20,7 +20,7 @@ export const response = {
|
|
|
20
20
|
{
|
|
21
21
|
id: "template-subject",
|
|
22
22
|
placeholder: "Enter Email Subject",
|
|
23
|
-
type: "input",
|
|
23
|
+
type: "cap-tag-list-with-input",
|
|
24
24
|
metaType: "text",
|
|
25
25
|
datatype: "string",
|
|
26
26
|
required: true,
|
|
@@ -28,29 +28,15 @@ export const response = {
|
|
|
28
28
|
styling: "semantic",
|
|
29
29
|
order: 1,
|
|
30
30
|
width: 16,
|
|
31
|
-
customComponent:
|
|
31
|
+
customComponent: true,
|
|
32
32
|
standalone: true,
|
|
33
33
|
onlyDisplay: false,
|
|
34
34
|
errorMessage: "Email Subject cannot be empty.",
|
|
35
|
+
supportedEvents: [
|
|
36
|
+
"onChange",
|
|
37
|
+
"onTagSelect",
|
|
38
|
+
],
|
|
35
39
|
},
|
|
36
|
-
// {
|
|
37
|
-
// offset: 20,
|
|
38
|
-
// id: "title-tagList",
|
|
39
|
-
// label: "Tags",
|
|
40
|
-
// target: "message-title2",
|
|
41
|
-
// type: "tag-list",
|
|
42
|
-
// metaType: "List",
|
|
43
|
-
// datatype: "select",
|
|
44
|
-
// required: true,
|
|
45
|
-
// fluid: true,
|
|
46
|
-
// onlyDisplay: true,
|
|
47
|
-
// styling: "semantic",
|
|
48
|
-
// order: 1,
|
|
49
|
-
// customComponent: true,
|
|
50
|
-
// supportedEvents: [
|
|
51
|
-
// "onTagSelect",
|
|
52
|
-
// ],
|
|
53
|
-
// },
|
|
54
40
|
],
|
|
55
41
|
},
|
|
56
42
|
],
|
|
@@ -283,7 +269,7 @@ export const response = {
|
|
|
283
269
|
metaType: "label",
|
|
284
270
|
type: "div",
|
|
285
271
|
primitive: true,
|
|
286
|
-
value: <CapButton isAddBtn type="flat"><FormattedMessage {...messages.image}/></CapButton>,
|
|
272
|
+
value: <CapButton isAddBtn type="flat"><FormattedMessage {...messages.image} /></CapButton>,
|
|
287
273
|
fluid: true,
|
|
288
274
|
onlyDisplay: false,
|
|
289
275
|
styling: "semantic",
|
|
@@ -188,7 +188,7 @@ const useEmailWrapper = ({
|
|
|
188
188
|
CapNotification.error(message);
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
|
-
}, [isUploading, formatMessage,
|
|
191
|
+
}, [isUploading, formatMessage, stopTimerGA, handleZipUploadError, showNextStep]);
|
|
192
192
|
|
|
193
193
|
const handleEdmDefaultTemplateSelection = useCallback((id) => {
|
|
194
194
|
const data = find(CmsTemplates, { _id: id });
|
|
@@ -251,7 +251,7 @@ const useEmailWrapper = ({
|
|
|
251
251
|
getCmsTemplatesInProgress,
|
|
252
252
|
modeContent.id,
|
|
253
253
|
SelectedEdmDefaultTemplate,
|
|
254
|
-
templatesActions,
|
|
254
|
+
// templatesActions,
|
|
255
255
|
handleEdmDefaultTemplateSelection
|
|
256
256
|
]);
|
|
257
257
|
|
|
@@ -5,7 +5,7 @@ import { initialReducer } from '../../../../../initialReducer';
|
|
|
5
5
|
import { mountWithIntl } from '../../../../../helpers/intl-enzym-test-helpers';
|
|
6
6
|
import { LineWrapper } from '../index';
|
|
7
7
|
import { mockData } from './mockData';
|
|
8
|
-
import {isValidText} from '../utils';
|
|
8
|
+
import {isValidText, isValidWhatsappCtaText} from '../utils';
|
|
9
9
|
import history from '../../../../../utils/history';
|
|
10
10
|
|
|
11
11
|
let store;
|
|
@@ -92,5 +92,60 @@ describe('line wrapper test/>', () => {
|
|
|
92
92
|
expect(isValidText(emptyString)).toBe(true);
|
|
93
93
|
});
|
|
94
94
|
});
|
|
95
|
+
|
|
96
|
+
describe('isValidWhatsappCtaText', () => {
|
|
97
|
+
it('should return false for non-string input', () => {
|
|
98
|
+
expect(isValidWhatsappCtaText(123)).toBe(false);
|
|
99
|
+
expect(isValidWhatsappCtaText(null)).toBe(false);
|
|
100
|
+
expect(isValidWhatsappCtaText(undefined)).toBe(false);
|
|
101
|
+
expect(isValidWhatsappCtaText({})).toBe(false);
|
|
102
|
+
expect(isValidWhatsappCtaText([])).toBe(false);
|
|
103
|
+
expect(isValidWhatsappCtaText(true)).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should return true for multilingual and symbol text', () => {
|
|
107
|
+
const valid = '確認一下 Привет مرحبا मान्य 123 @#&!?.:, -+/\\\'"=&₹€£$^(){}';
|
|
108
|
+
expect(isValidWhatsappCtaText(valid)).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should return true for full allowed punctuation set', () => {
|
|
112
|
+
const allowed = 'A1 z9 @#&!?.:, -+/\\\'"=&₹€£$^(){}';
|
|
113
|
+
expect(isValidWhatsappCtaText(allowed)).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should return false for whitespace-only', () => {
|
|
117
|
+
expect(isValidWhatsappCtaText(' ')).toBe(false);
|
|
118
|
+
expect(isValidWhatsappCtaText('\t\t')).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should return true for leading and trailing spaces with valid text', () => {
|
|
122
|
+
expect(isValidWhatsappCtaText(' Hi')).toBe(true);
|
|
123
|
+
expect(isValidWhatsappCtaText('Hi ')).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should return false for control or markdown chars', () => {
|
|
127
|
+
expect(isValidWhatsappCtaText('Hello\nWorld')).toBe(false);
|
|
128
|
+
expect(isValidWhatsappCtaText('Hello\tWorld')).toBe(false);
|
|
129
|
+
expect(isValidWhatsappCtaText('*bold*')).toBe(false);
|
|
130
|
+
expect(isValidWhatsappCtaText('_under_')).toBe(false);
|
|
131
|
+
expect(isValidWhatsappCtaText('~strike~')).toBe(false);
|
|
132
|
+
expect(isValidWhatsappCtaText('`code`')).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should return false for emojis, flags, ZWJ sequences and VS16', () => {
|
|
136
|
+
expect(isValidWhatsappCtaText('Heart ❤️')).toBe(false);
|
|
137
|
+
expect(isValidWhatsappCtaText('Flag 🇮🇳')).toBe(false);
|
|
138
|
+
expect(isValidWhatsappCtaText('Family 👨👩👧👦')).toBe(false);
|
|
139
|
+
expect(isValidWhatsappCtaText('Sun ☀️')).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should return false for mixed valid text with emoji inside', () => {
|
|
143
|
+
expect(isValidWhatsappCtaText('Hello @# hi ❤️')).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should return true for backslash usage', () => {
|
|
147
|
+
expect(isValidWhatsappCtaText('C:\\Path')).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
95
150
|
|
|
96
151
|
});
|
|
@@ -78,10 +78,12 @@ export const isValidText = (text) => {
|
|
|
78
78
|
return regex.test(text);
|
|
79
79
|
};
|
|
80
80
|
|
|
81
|
-
// WhatsApp CTA button text validation-
|
|
81
|
+
// WhatsApp CTA button text validation - whitelist approach
|
|
82
|
+
// Allow: all letters and numbers (any language), regular space, and safe symbols
|
|
83
|
+
// Safe symbols: @ # & ! ? . : , - + / \ ' " = & ₹ € £ $ ^ ( ) { }
|
|
82
84
|
export const isValidWhatsappCtaText = (text) => {
|
|
83
85
|
if (typeof text !== 'string') return false;
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
return
|
|
86
|
+
if (!/\S/.test(text)) return false; // disallow whitespace-only
|
|
87
|
+
const regex = /^[\p{L}\p{M}\p{N} @#&!?.:,\-+/\\'"=&₹€£$^(){}]*$/u;
|
|
88
|
+
return regex.test(text);
|
|
87
89
|
};
|
|
@@ -291,7 +291,29 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
291
291
|
rowClassName: 'mobile-push-row',
|
|
292
292
|
id: 'cta-external-link',
|
|
293
293
|
cols: [
|
|
294
|
-
|
|
294
|
+
{
|
|
295
|
+
// Add label control for external link input
|
|
296
|
+
id: "title-tagList",
|
|
297
|
+
label: "Add label",
|
|
298
|
+
type: "tag-list",
|
|
299
|
+
metaType: "List",
|
|
300
|
+
datatype: "select",
|
|
301
|
+
required: true,
|
|
302
|
+
fluid: true,
|
|
303
|
+
onlyDisplay: true,
|
|
304
|
+
style: {
|
|
305
|
+
marginRight: "10%",
|
|
306
|
+
marginTop: "-3%",
|
|
307
|
+
},
|
|
308
|
+
styling: "semantic",
|
|
309
|
+
order: 2,
|
|
310
|
+
customComponent: true,
|
|
311
|
+
// Helps handler identify the destination input
|
|
312
|
+
target: inputId,
|
|
313
|
+
supportedEvents: [
|
|
314
|
+
"onTagSelect",
|
|
315
|
+
],
|
|
316
|
+
},
|
|
295
317
|
{
|
|
296
318
|
id: inputId,
|
|
297
319
|
placeholder: this.props.intl.formatMessage(messages.externalLink),
|
|
@@ -304,8 +326,7 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
304
326
|
width: 18,
|
|
305
327
|
offset: 1,
|
|
306
328
|
style: {
|
|
307
|
-
width: '
|
|
308
|
-
|
|
329
|
+
width: '95%',
|
|
309
330
|
},
|
|
310
331
|
styling: "semantic",
|
|
311
332
|
order: 1,
|