@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.
Files changed (27) hide show
  1. package/constants/unified.js +3 -0
  2. package/package.json +1 -1
  3. package/utils/tagValidations.js +4 -6
  4. package/utils/tests/tagValidations.test.js +161 -0
  5. package/v2Components/FormBuilder/index.js +56 -42
  6. package/v2Containers/CreativesContainer/SlideBoxContent.js +5 -1
  7. package/v2Containers/CreativesContainer/SlideBoxFooter.js +13 -7
  8. package/v2Containers/CreativesContainer/index.js +11 -1
  9. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +15 -10
  10. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +19 -24
  11. package/v2Containers/InApp/index.js +4 -9
  12. package/v2Containers/InappAdvance/index.js +3 -6
  13. package/v2Containers/InappAdvance/tests/index.test.js +2 -0
  14. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +24 -3
  15. package/v2Containers/MobilePush/Create/index.js +36 -3
  16. package/v2Containers/MobilePush/Edit/index.js +36 -3
  17. package/v2Containers/MobilePushNew/index.js +15 -4
  18. package/v2Containers/MobilepushWrapper/index.js +3 -1
  19. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +18 -4
  20. package/v2Containers/Sms/Create/index.js +22 -18
  21. package/v2Containers/Sms/Edit/index.js +18 -16
  22. package/v2Containers/Sms/commonMethods.js +0 -3
  23. package/v2Containers/Sms/tests/commonMethods.test.js +122 -0
  24. package/v2Containers/SmsTrai/Edit/index.js +5 -0
  25. package/v2Containers/SmsWrapper/index.js +2 -0
  26. package/v2Containers/WebPush/Create/utils/validation.test.js +59 -0
  27. 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
- if (!nextProps.isFullMode && nextProps.isGetFormData) {
145
- // Trigger validation first; response will be sent in setFormValidity callback so pasted content is validated
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 && this.props.getFormSubscriptionData) {
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 the template is submitted via getFormSubscriptionData (triggered by startValidation →
999
- // onFormValidityChange setFormValidity). Calling createTemplate API here would set
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 && this.props.getFormSubscriptionData) {
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 the template is submitted via getFormSubscriptionData, not editTemplate API.
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