@capillarytech/creatives-library 8.0.290-alpha.2 → 8.0.290-alpha.4
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 +3 -0
- package/package.json +1 -1
- package/utils/tagValidations.js +4 -6
- package/utils/tests/tagValidations.test.js +161 -0
- package/v2Components/FormBuilder/index.js +56 -42
- package/v2Containers/CreativesContainer/SlideBoxContent.js +5 -1
- package/v2Containers/CreativesContainer/SlideBoxFooter.js +13 -7
- package/v2Containers/CreativesContainer/index.js +11 -1
- package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +15 -10
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +19 -24
- package/v2Containers/InApp/index.js +4 -9
- package/v2Containers/InappAdvance/index.js +3 -6
- package/v2Containers/InappAdvance/tests/index.test.js +2 -0
- package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +24 -3
- package/v2Containers/MobilePush/Create/index.js +36 -3
- package/v2Containers/MobilePush/Edit/index.js +36 -3
- package/v2Containers/MobilePushNew/index.js +15 -4
- package/v2Containers/MobilepushWrapper/index.js +3 -1
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +18 -4
- package/v2Containers/Sms/Create/index.js +22 -18
- package/v2Containers/Sms/Edit/index.js +18 -16
- package/v2Containers/Sms/commonMethods.js +0 -3
- package/v2Containers/Sms/tests/commonMethods.test.js +122 -0
- package/v2Containers/SmsTrai/Edit/index.js +5 -0
- package/v2Containers/SmsWrapper/index.js +2 -0
- package/v2Containers/WebPush/Create/utils/validation.test.js +59 -0
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +624 -248
|
@@ -141,16 +141,9 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
componentWillReceiveProps(nextProps) {
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
// Only trigger on actual Done click (isGetFormData false -> true). Prevents auto-submit after user fixes brace error.
|
|
145
|
+
if (!nextProps.isFullMode && nextProps.isGetFormData && !this.props.isGetFormData) {
|
|
146
146
|
this.setState({ startValidation: true, pendingGetFormData: true });
|
|
147
|
-
// Fallback: if FormBuilder never calls onFormValidityChange (e.g. early return), still respond to parent
|
|
148
|
-
this.pendingGetFormDataTimeout = setTimeout(() => {
|
|
149
|
-
if (this.state.pendingGetFormData && this.props.getFormSubscriptionData) {
|
|
150
|
-
this.props.getFormSubscriptionData(this.getFormData());
|
|
151
|
-
this.setState({ pendingGetFormData: false, startValidation: false });
|
|
152
|
-
}
|
|
153
|
-
}, 300);
|
|
154
147
|
} else if (nextProps.isGetFormData && this.props.isFullMode && !this.props.Create.createTemplateInProgress) {
|
|
155
148
|
this.startValidation();
|
|
156
149
|
}
|
|
@@ -211,6 +204,10 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
211
204
|
if (currentTab) {
|
|
212
205
|
this.setState({currentTab});
|
|
213
206
|
}
|
|
207
|
+
// Clear footer validation errors on input change so they refresh on next validation
|
|
208
|
+
if (this.props.showLiquidErrorInFooter) {
|
|
209
|
+
this.props.showLiquidErrorInFooter({ STANDARD_ERROR_MSG: [], LIQUID_ERROR_MSG: [] });
|
|
210
|
+
}
|
|
214
211
|
}
|
|
215
212
|
|
|
216
213
|
onTagSelect(data, currentTab) {
|
|
@@ -362,17 +359,22 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
362
359
|
|
|
363
360
|
setFormValidity(isFormValid, errorData) {
|
|
364
361
|
this.setState({ isFormValid, errorData }, () => {
|
|
365
|
-
if (this.state.pendingGetFormData &&
|
|
362
|
+
if (this.state.pendingGetFormData && !isFormValid) {
|
|
363
|
+
this.setState({ pendingGetFormData: false, startValidation: false });
|
|
364
|
+
// Reset parent's Done state so next Done click is a fresh attempt
|
|
365
|
+
if (this.props.onValidationFail) {
|
|
366
|
+
this.props.onValidationFail();
|
|
367
|
+
}
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
// In library mode with SMS, submit only when FormBuilder calls onSubmit (after liquid validation).
|
|
371
|
+
if (this.state.pendingGetFormData && this.props.getFormSubscriptionData && this.props.isFullMode) {
|
|
366
372
|
if (this.pendingGetFormDataTimeout) {
|
|
367
373
|
clearTimeout(this.pendingGetFormDataTimeout);
|
|
368
374
|
this.pendingGetFormDataTimeout = null;
|
|
369
375
|
}
|
|
370
376
|
this.props.getFormSubscriptionData(this.getFormData());
|
|
371
377
|
this.setState({ pendingGetFormData: false, startValidation: false });
|
|
372
|
-
} else if (this.state.pendingGetFormData && !isFormValid) {
|
|
373
|
-
// When the user clicked "Done" and validation failed, discard the save intent so that
|
|
374
|
-
// fixing the error later doesn't auto-submit. The user must click "Done" again.
|
|
375
|
-
this.setState({ pendingGetFormData: false, startValidation: false });
|
|
376
378
|
}
|
|
377
379
|
});
|
|
378
380
|
}
|
|
@@ -995,11 +997,13 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
995
997
|
}
|
|
996
998
|
|
|
997
999
|
saveFormData() {
|
|
998
|
-
// In library mode
|
|
999
|
-
//
|
|
1000
|
-
// createTemplateInProgress: true in redux and, because the slidebox closes before the API responds,
|
|
1001
|
-
// that flag would never be reset – causing the spinner to be stuck on the next open.
|
|
1000
|
+
// In library mode: FormBuilder calls onSubmit only after liquid validation succeeds.
|
|
1001
|
+
// Submit to parent here so the slidebox can close with valid data.
|
|
1002
1002
|
if (!this.props.isFullMode) {
|
|
1003
|
+
if (this.state.pendingGetFormData && this.props.getFormSubscriptionData) {
|
|
1004
|
+
this.props.getFormSubscriptionData(this.getFormData());
|
|
1005
|
+
this.setState({ pendingGetFormData: false, startValidation: false });
|
|
1006
|
+
}
|
|
1003
1007
|
return;
|
|
1004
1008
|
}
|
|
1005
1009
|
//Logic to save in db etc
|
|
@@ -135,14 +135,8 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
componentWillReceiveProps(nextProps) {
|
|
138
|
-
if (!nextProps.isFullMode && nextProps.isGetFormData) {
|
|
138
|
+
if (!nextProps.isFullMode && nextProps.isGetFormData && !this.props.isGetFormData) {
|
|
139
139
|
this.setState({ startValidation: true, pendingGetFormData: true });
|
|
140
|
-
this.pendingGetFormDataTimeout = setTimeout(() => {
|
|
141
|
-
if (this.state.pendingGetFormData && this.props.getFormSubscriptionData) {
|
|
142
|
-
this.props.getFormSubscriptionData(this.getFormData());
|
|
143
|
-
this.setState({ pendingGetFormData: false, startValidation: false });
|
|
144
|
-
}
|
|
145
|
-
}, 300);
|
|
146
140
|
}
|
|
147
141
|
if ( nextProps.location.query.module === 'library' && nextProps.subscriptionTemplateDetails && nextProps.subscriptionTemplateDetails.name && _.isEmpty(this.state.editData) && !_.isEmpty(this.state.schema)) {
|
|
148
142
|
this.setEditState(nextProps.subscriptionTemplateDetails);
|
|
@@ -224,6 +218,10 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
224
218
|
if (currentTab) {
|
|
225
219
|
this.setState({currentTab});
|
|
226
220
|
}
|
|
221
|
+
// Clear footer validation errors on input change so they refresh on next validation
|
|
222
|
+
if (this.props.showLiquidErrorInFooter) {
|
|
223
|
+
this.props.showLiquidErrorInFooter({ STANDARD_ERROR_MSG: [], LIQUID_ERROR_MSG: [] });
|
|
224
|
+
}
|
|
227
225
|
}
|
|
228
226
|
|
|
229
227
|
onVersionNameChange() {
|
|
@@ -328,17 +326,20 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
328
326
|
|
|
329
327
|
setFormValidity(isFormValid, errorData) {
|
|
330
328
|
this.setState({ isFormValid, errorData }, () => {
|
|
331
|
-
if (this.state.pendingGetFormData &&
|
|
329
|
+
if (this.state.pendingGetFormData && !isFormValid) {
|
|
330
|
+
this.setState({ pendingGetFormData: false, startValidation: false });
|
|
331
|
+
if (this.props.onValidationFail) {
|
|
332
|
+
this.props.onValidationFail();
|
|
333
|
+
}
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (this.state.pendingGetFormData && this.props.getFormSubscriptionData && this.props.isFullMode) {
|
|
332
337
|
if (this.pendingGetFormDataTimeout) {
|
|
333
338
|
clearTimeout(this.pendingGetFormDataTimeout);
|
|
334
339
|
this.pendingGetFormDataTimeout = null;
|
|
335
340
|
}
|
|
336
341
|
this.props.getFormSubscriptionData(this.getFormData());
|
|
337
342
|
this.setState({ pendingGetFormData: false, startValidation: false });
|
|
338
|
-
} else if (this.state.pendingGetFormData && !isFormValid) {
|
|
339
|
-
// When the user clicked "Done" and validation failed, discard the save intent so that
|
|
340
|
-
// fixing the error later doesn't auto-submit. The user must click "Done" again.
|
|
341
|
-
this.setState({ pendingGetFormData: false, startValidation: false });
|
|
342
343
|
}
|
|
343
344
|
});
|
|
344
345
|
}
|
|
@@ -946,11 +947,12 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
946
947
|
this.setState({startValidation: false});
|
|
947
948
|
}
|
|
948
949
|
saveFormData() {
|
|
949
|
-
// In library mode
|
|
950
|
-
// Calling editTemplate here would set editTemplateInProgress: true in redux and, because the
|
|
951
|
-
// slidebox closes before the API responds, that flag would never be reset – causing the spinner
|
|
952
|
-
// to be stuck on the next open.
|
|
950
|
+
// In library mode: FormBuilder calls onSubmit only after liquid validation succeeds.
|
|
953
951
|
if (!this.props.isFullMode) {
|
|
952
|
+
if (this.state.pendingGetFormData && this.props.getFormSubscriptionData) {
|
|
953
|
+
this.props.getFormSubscriptionData(this.getFormData());
|
|
954
|
+
this.setState({ pendingGetFormData: false, startValidation: false });
|
|
955
|
+
}
|
|
954
956
|
return;
|
|
955
957
|
}
|
|
956
958
|
//Logic to save in db etc
|
|
@@ -8,11 +8,8 @@ export function showError() {
|
|
|
8
8
|
if (!isEmpty(this.state.formData) && !this.state.isFormValid) {
|
|
9
9
|
const err0 = errorData[0] || {};
|
|
10
10
|
const isSmsInvalid = Object.values(err0).includes(true);
|
|
11
|
-
const isBraceError = Boolean(err0['bracket-error']);
|
|
12
11
|
if (isSmsInvalid) {
|
|
13
12
|
CapNotification.error(errorMessage);
|
|
14
|
-
} else if (isBraceError) {
|
|
15
|
-
// Do not trigger toast for this path; footer error is the reliable UX in SMS library flow.
|
|
16
13
|
}
|
|
17
14
|
}
|
|
18
15
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import CapNotification from '@capillarytech/cap-ui-library/CapNotification';
|
|
2
|
+
import { showError } from '../commonMethods';
|
|
3
|
+
|
|
4
|
+
jest.mock('@capillarytech/cap-ui-library/CapNotification', () => ({
|
|
5
|
+
error: jest.fn(),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
jest.mock('../Create/messages', () => ({
|
|
9
|
+
__esModule: true,
|
|
10
|
+
default: {
|
|
11
|
+
validationError: { defaultMessage: 'Validation error' },
|
|
12
|
+
},
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
describe('Sms commonMethods', () => {
|
|
16
|
+
describe('showError', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
jest.clearAllMocks();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should call CapNotification.error when formData is not empty, isFormValid is false, and errorData has at least one true value', () => {
|
|
22
|
+
const context = {
|
|
23
|
+
props: {
|
|
24
|
+
intl: { formatMessage: jest.fn((msg) => msg.defaultMessage || 'Validation error') },
|
|
25
|
+
},
|
|
26
|
+
state: {
|
|
27
|
+
formData: { message: 'test' },
|
|
28
|
+
isFormValid: false,
|
|
29
|
+
errorData: [{ message: true }],
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
showError.call(context);
|
|
33
|
+
expect(CapNotification.error).toHaveBeenCalledWith({
|
|
34
|
+
key: 'validation-error',
|
|
35
|
+
message: 'Validation error',
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should not call CapNotification.error when formData is empty', () => {
|
|
40
|
+
const context = {
|
|
41
|
+
props: { intl: { formatMessage: jest.fn() } },
|
|
42
|
+
state: {
|
|
43
|
+
formData: {},
|
|
44
|
+
isFormValid: false,
|
|
45
|
+
errorData: [{ message: true }],
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
showError.call(context);
|
|
49
|
+
expect(CapNotification.error).not.toHaveBeenCalled();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should not call CapNotification.error when isFormValid is true', () => {
|
|
53
|
+
const context = {
|
|
54
|
+
props: { intl: { formatMessage: jest.fn() } },
|
|
55
|
+
state: {
|
|
56
|
+
formData: { message: 'test' },
|
|
57
|
+
isFormValid: true,
|
|
58
|
+
errorData: [{ message: true }],
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
showError.call(context);
|
|
62
|
+
expect(CapNotification.error).not.toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should not call CapNotification.error when no errorData entry has a true value', () => {
|
|
66
|
+
const context = {
|
|
67
|
+
props: { intl: { formatMessage: jest.fn() } },
|
|
68
|
+
state: {
|
|
69
|
+
formData: { message: 'test' },
|
|
70
|
+
isFormValid: false,
|
|
71
|
+
errorData: [{ message: false, title: false }],
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
showError.call(context);
|
|
75
|
+
expect(CapNotification.error).not.toHaveBeenCalled();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should not call CapNotification.error when errorData is empty', () => {
|
|
79
|
+
const context = {
|
|
80
|
+
props: { intl: { formatMessage: jest.fn() } },
|
|
81
|
+
state: {
|
|
82
|
+
formData: { message: 'test' },
|
|
83
|
+
isFormValid: false,
|
|
84
|
+
errorData: [],
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
showError.call(context);
|
|
88
|
+
expect(CapNotification.error).not.toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should not call CapNotification.error when errorData[0] is undefined', () => {
|
|
92
|
+
const context = {
|
|
93
|
+
props: { intl: { formatMessage: jest.fn() } },
|
|
94
|
+
state: {
|
|
95
|
+
formData: { message: 'test' },
|
|
96
|
+
isFormValid: false,
|
|
97
|
+
errorData: [],
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
showError.call(context);
|
|
101
|
+
expect(CapNotification.error).not.toHaveBeenCalled();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should call CapNotification.error when errorData[0] has multiple keys and one is true', () => {
|
|
105
|
+
const context = {
|
|
106
|
+
props: {
|
|
107
|
+
intl: { formatMessage: jest.fn((msg) => msg.defaultMessage || 'Validation error') },
|
|
108
|
+
},
|
|
109
|
+
state: {
|
|
110
|
+
formData: { message: 'hello' },
|
|
111
|
+
isFormValid: false,
|
|
112
|
+
errorData: [{ message: false, title: true }],
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
showError.call(context);
|
|
116
|
+
expect(CapNotification.error).toHaveBeenCalledWith({
|
|
117
|
+
key: 'validation-error',
|
|
118
|
+
message: 'Validation error',
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -263,6 +263,11 @@ export const SmsTraiEdit = (props) => {
|
|
|
263
263
|
const onSubmitWrapper = () => {
|
|
264
264
|
setIsLiquidValidationError(false);
|
|
265
265
|
setLiquidErrorMessages({});
|
|
266
|
+
// Liquid validation (extractTags) only in library mode
|
|
267
|
+
if (isFullMode) {
|
|
268
|
+
onDoneCallback();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
266
271
|
const content = updatedSmsEditor.join('');
|
|
267
272
|
const onError = ({ standardErrors, liquidErrors }) => {
|
|
268
273
|
setLiquidErrorMessages({
|
|
@@ -36,6 +36,7 @@ const SmsWrapper = (props) => {
|
|
|
36
36
|
handleTestAndPreview,
|
|
37
37
|
handleCloseTestAndPreview,
|
|
38
38
|
isTestAndPreviewMode,
|
|
39
|
+
onValidationFail,
|
|
39
40
|
} = props;
|
|
40
41
|
|
|
41
42
|
const smsProps = {
|
|
@@ -58,6 +59,7 @@ const SmsWrapper = (props) => {
|
|
|
58
59
|
handleTestAndPreview,
|
|
59
60
|
handleCloseTestAndPreview,
|
|
60
61
|
isTestAndPreviewMode,
|
|
62
|
+
onValidationFail,
|
|
61
63
|
};
|
|
62
64
|
const isTraiDlt = isTraiDLTEnable(isFullMode, smsRegister);
|
|
63
65
|
return <>
|
|
@@ -127,6 +127,40 @@ describe('validation', () => {
|
|
|
127
127
|
expect(result).toBe('Personalization tags are not supported for anonymous customers');
|
|
128
128
|
expect(mockFormatMessage).toHaveBeenCalledWith(mockMessages.personalizationTokensErrorMessage);
|
|
129
129
|
});
|
|
130
|
+
|
|
131
|
+
it('should return brace error when validationConfig is provided and validateTags returns isBraceError', () => {
|
|
132
|
+
const validationConfig = { tagsParam: [], location: {}, tagModule: '' };
|
|
133
|
+
validateTags.mockReturnValue({ isBraceError: true });
|
|
134
|
+
const result = validateTitle(
|
|
135
|
+
'Valid Title',
|
|
136
|
+
mockFormatMessage,
|
|
137
|
+
mockMessages,
|
|
138
|
+
false,
|
|
139
|
+
validationConfig,
|
|
140
|
+
false
|
|
141
|
+
);
|
|
142
|
+
expect(result).toBe('Unbalanced curly braces');
|
|
143
|
+
expect(mockFormatMessage).toHaveBeenCalledWith(globalMessages.unbalanacedCurlyBraces);
|
|
144
|
+
expect(validateTags).toHaveBeenCalledWith({
|
|
145
|
+
content: 'Valid Title',
|
|
146
|
+
...validationConfig,
|
|
147
|
+
isFullMode: false,
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should not run tag validation when validationConfig is null', () => {
|
|
152
|
+
validateTags.mockClear();
|
|
153
|
+
const result = validateTitle('Valid Title', mockFormatMessage, mockMessages, false, null);
|
|
154
|
+
expect(result).toBe('');
|
|
155
|
+
expect(validateTags).not.toHaveBeenCalled();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should not run tag validation when validationConfig is undefined', () => {
|
|
159
|
+
validateTags.mockClear();
|
|
160
|
+
const result = validateTitle('Valid Title', mockFormatMessage, mockMessages, false, undefined);
|
|
161
|
+
expect(result).toBe('');
|
|
162
|
+
expect(validateTags).not.toHaveBeenCalled();
|
|
163
|
+
});
|
|
130
164
|
});
|
|
131
165
|
|
|
132
166
|
describe('validateUrl', () => {
|
|
@@ -282,6 +316,31 @@ describe('validation', () => {
|
|
|
282
316
|
expect(result).toBe('Personalization tags are not supported for anonymous customers');
|
|
283
317
|
expect(mockFormatMessage).toHaveBeenCalledWith(mockMessages.personalizationTokensErrorMessage);
|
|
284
318
|
});
|
|
319
|
+
|
|
320
|
+
it('should return unsupported tags error when validateTags returns unsupportedTags', () => {
|
|
321
|
+
validateTags.mockReturnValue({ unsupportedTags: ['invalidTag', 'otherTag'] });
|
|
322
|
+
const result = validateMessageContent(
|
|
323
|
+
'Hello {{invalidTag}}',
|
|
324
|
+
mockFormatMessage,
|
|
325
|
+
mockMessages,
|
|
326
|
+
mockValidationConfig
|
|
327
|
+
);
|
|
328
|
+
expect(result).toBe('Unsupported tags: invalidTag, otherTag');
|
|
329
|
+
expect(mockFormatMessage).toHaveBeenCalledWith(
|
|
330
|
+
globalMessages.unsupportedTagsValidationError,
|
|
331
|
+
{ unsupportedTags: 'invalidTag, otherTag' }
|
|
332
|
+
);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('should pass isFullMode to validateTags when provided', () => {
|
|
336
|
+
validateTags.mockReturnValue({});
|
|
337
|
+
validateMessageContent('Valid message', mockFormatMessage, mockMessages, mockValidationConfig, true);
|
|
338
|
+
expect(validateTags).toHaveBeenCalledWith({
|
|
339
|
+
content: 'Valid message',
|
|
340
|
+
...mockValidationConfig,
|
|
341
|
+
isFullMode: true,
|
|
342
|
+
});
|
|
343
|
+
});
|
|
285
344
|
});
|
|
286
345
|
});
|
|
287
346
|
|