@capillarytech/creatives-library 8.0.114 → 8.0.115
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/package.json +1 -1
- package/utils/commonUtils.js +353 -3
- package/utils/tagValidations.js +22 -5
- package/utils/tests/commonUtil.test.js +563 -169
- package/utils/tests/tagValidations.test.js +129 -3
- package/v2Components/ErrorInfoNote/ErrorTypeRenderer.js +125 -0
- package/v2Components/ErrorInfoNote/ErrorTypeRenderer.test.js +147 -0
- package/v2Components/ErrorInfoNote/index.js +114 -47
- package/v2Components/ErrorInfoNote/messages.js +25 -0
- package/v2Components/ErrorInfoNote/style.scss +14 -1
- package/v2Components/ErrorInfoNote/utils.js +50 -0
- package/v2Components/ErrorInfoNote/utils.test.js +189 -0
- package/v2Components/FormBuilder/index.js +203 -127
- package/v2Components/FormBuilder/messages.js +1 -1
- package/v2Containers/Cap/reducer.js +4 -4
- package/v2Containers/Cap/sagas.js +9 -3
- package/v2Containers/Cap/tests/saga.test.js +12 -0
- package/v2Containers/CreativesContainer/SlideBoxContent.js +26 -3
- package/v2Containers/CreativesContainer/SlideBoxFooter.js +3 -1
- package/v2Containers/CreativesContainer/constants.js +4 -1
- package/v2Containers/CreativesContainer/index.js +46 -19
- package/v2Containers/CreativesContainer/messages.js +4 -0
- package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +21 -3
- package/v2Containers/CreativesContainer/tests/index.test.js +1 -0
- package/v2Containers/Ebill/index.js +3 -3
- package/v2Containers/EmailWrapper/components/EmailWrapperView.js +1 -1
- package/v2Containers/InApp/index.js +126 -50
- package/v2Containers/InApp/tests/index.test.js +1 -1
- package/v2Containers/InApp/tests/sagas.test.js +1 -1
- package/v2Containers/InApp/tests/utils.test.js +85 -0
- package/v2Containers/InApp/utils.js +57 -0
- package/v2Containers/InApp/utils.test.js +70 -0
- package/v2Containers/MobilePush/Create/index.js +24 -20
- package/v2Containers/MobilePush/Edit/index.js +6 -2
- package/v2Containers/MobilepushWrapper/index.js +2 -0
- package/v2Containers/Sms/Create/index.js +1 -0
- package/v2Containers/Sms/Edit/index.js +2 -0
- package/v2Containers/SmsTrai/Edit/index.js +49 -10
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +112 -36
- package/v2Containers/SmsTrai/Edit/tests/index.test.js +1 -3
- package/v2Containers/SmsWrapper/index.js +5 -1
- package/v2Containers/Templates/sagas.js +1 -1
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
1
|
import '@testing-library/jest-dom';
|
|
3
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
checkSupport,
|
|
4
|
+
extractNames,
|
|
5
|
+
getTagMapValue,
|
|
6
|
+
getForwardedMapValues,
|
|
7
|
+
preprocessHtml,
|
|
8
|
+
validateIfTagClosed,
|
|
9
|
+
validateTags,
|
|
10
|
+
skipTags,
|
|
11
|
+
isInsideLiquidBlock,
|
|
12
|
+
} from '../tagValidations';
|
|
4
13
|
import { eventContextTags } from '../../v2Containers/TagList/tests/mockdata';
|
|
5
14
|
|
|
6
15
|
describe("check if curly brackets are balanced", () => {
|
|
@@ -170,6 +179,45 @@ describe("validateTags", () => {
|
|
|
170
179
|
expect(result.unsupportedTags).toEqual([]);
|
|
171
180
|
expect(result.isBraceError).toEqual(true);
|
|
172
181
|
});
|
|
182
|
+
|
|
183
|
+
it("should remove 'unsubscribe' from missingTags if skipTags logic is triggered", () => {
|
|
184
|
+
const tagsParam = [
|
|
185
|
+
{
|
|
186
|
+
definition: {
|
|
187
|
+
supportedModules: [{ context: "DEFAULT", mandatory: true }],
|
|
188
|
+
value: "unsubscribe",
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
];
|
|
192
|
+
// Content does not include {{unsubscribe}}, so it would be missing
|
|
193
|
+
const contentMissing = "Hello {{tag1}}";
|
|
194
|
+
const injectedTagsParams = [];
|
|
195
|
+
const location = { query: { module: "DEFAULT" } };
|
|
196
|
+
const tagModule = null;
|
|
197
|
+
|
|
198
|
+
// First, verify unsubscribe is missing if not present
|
|
199
|
+
const resultMissing = validateTags({
|
|
200
|
+
content: contentMissing,
|
|
201
|
+
tagsParam,
|
|
202
|
+
injectedTagsParams,
|
|
203
|
+
location,
|
|
204
|
+
tagModule,
|
|
205
|
+
});
|
|
206
|
+
expect(resultMissing.missingTags).toContain("unsubscribe");
|
|
207
|
+
|
|
208
|
+
// Now, content includes a tag that triggers skipTags logic for unsubscribe
|
|
209
|
+
// e.g., {{unsubscribe(#123456)}} matches the skipTags regex
|
|
210
|
+
const contentWithSkippedUnsubscribe = "Hello {{unsubscribe(#123456)}}";
|
|
211
|
+
const resultSkipped = validateTags({
|
|
212
|
+
content: contentWithSkippedUnsubscribe,
|
|
213
|
+
tagsParam,
|
|
214
|
+
injectedTagsParams,
|
|
215
|
+
location,
|
|
216
|
+
tagModule,
|
|
217
|
+
});
|
|
218
|
+
expect(resultSkipped.missingTags).not.toContain("unsubscribe");
|
|
219
|
+
expect(resultSkipped.valid).toBe(true);
|
|
220
|
+
});
|
|
173
221
|
});
|
|
174
222
|
|
|
175
223
|
describe("extractNames", () => {
|
|
@@ -806,4 +854,82 @@ describe('getForwardedMapValues', () => {
|
|
|
806
854
|
|
|
807
855
|
expect(input).toEqual(inputCopy);
|
|
808
856
|
});
|
|
809
|
-
});
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
describe('isInsideLiquidBlock', () => {
|
|
860
|
+
it('returns true for index inside a single block', () => {
|
|
861
|
+
const content = 'Hello {% assign foo = 1 %} World';
|
|
862
|
+
// Index of 'a' in 'assign' inside the block
|
|
863
|
+
const tagIndex = content.indexOf('assign');
|
|
864
|
+
expect(isInsideLiquidBlock(content, tagIndex)).toBe(true);
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
it('returns false for index outside any block', () => {
|
|
868
|
+
const content = 'Hello {% assign foo = 1 %} World';
|
|
869
|
+
// Index of 'H' in 'Hello'
|
|
870
|
+
expect(isInsideLiquidBlock(content, 0)).toBe(false);
|
|
871
|
+
// Index of 'W' in 'World'
|
|
872
|
+
expect(isInsideLiquidBlock(content, content.indexOf('World'))).toBe(false);
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
it('returns true for index at the start of a block', () => {
|
|
876
|
+
const content = 'Hello {% assign foo = 1 %} World';
|
|
877
|
+
// Index of '{' in '{%'
|
|
878
|
+
const tagIndex = content.indexOf('{%');
|
|
879
|
+
expect(isInsideLiquidBlock(content, tagIndex)).toBe(true);
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
it('returns false for index at the end of a block (exclusive)', () => {
|
|
883
|
+
const content = 'Hello {% assign foo = 1 %} World';
|
|
884
|
+
// Index just after the closing '%}'
|
|
885
|
+
const blockEnd = content.indexOf('%}') + 2;
|
|
886
|
+
expect(isInsideLiquidBlock(content, blockEnd)).toBe(false);
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
it('returns true for index inside the second of multiple blocks', () => {
|
|
890
|
+
const content = 'A {% first %} B {% second %} C';
|
|
891
|
+
const tagIndex = content.indexOf('second');
|
|
892
|
+
expect(isInsideLiquidBlock(content, tagIndex)).toBe(true);
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
it('returns false for index between blocks', () => {
|
|
896
|
+
const content = 'A {% first %} B {% second %} C';
|
|
897
|
+
// Index of 'B' (between blocks)
|
|
898
|
+
const tagIndex = content.indexOf('B');
|
|
899
|
+
expect(isInsideLiquidBlock(content, tagIndex)).toBe(false);
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
it('returns false for empty string', () => {
|
|
903
|
+
expect(isInsideLiquidBlock('', 0)).toBe(false);
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
it('returns false if there are no blocks', () => {
|
|
907
|
+
const content = 'Just some text with no blocks';
|
|
908
|
+
expect(isInsideLiquidBlock(content, 5)).toBe(false);
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
it('returns false for negative index', () => {
|
|
912
|
+
const content = 'Hello {% assign foo = 1 %} World';
|
|
913
|
+
expect(isInsideLiquidBlock(content, -1)).toBe(false);
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
it('returns false for index beyond string length', () => {
|
|
917
|
+
const content = 'Hello {% assign foo = 1 %} World';
|
|
918
|
+
expect(isInsideLiquidBlock(content, 100)).toBe(false);
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
it('works for nested-like blocks (not truly nested)', () => {
|
|
922
|
+
const content = 'A {% outer {% inner %} outer %} B';
|
|
923
|
+
// Index of 'inner' (should be inside the first block)
|
|
924
|
+
const tagIndex = content.indexOf('inner');
|
|
925
|
+
expect(isInsideLiquidBlock(content, tagIndex)).toBe(true);
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
it('returns true for index at last char inside block', () => {
|
|
929
|
+
const content = 'A {% foo %} B';
|
|
930
|
+
// Index of last char inside block (just before %})
|
|
931
|
+
const blockStart = content.indexOf('{%');
|
|
932
|
+
const blockEnd = content.indexOf('%}');
|
|
933
|
+
expect(isInsideLiquidBlock(content, blockEnd - 1)).toBe(true);
|
|
934
|
+
});
|
|
935
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// app/v2Components/ErrorInfoNote/ErrorTypeRenderer.js
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import PropTypes from 'prop-types';
|
|
4
|
+
import CapDivider from "@capillarytech/cap-ui-library/CapDivider";
|
|
5
|
+
import { getLiquidProps, hasPlatformErrors } from './utils';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Renders error sections for either generic (non-platform) or platform-specific (Android/iOS) errors.
|
|
9
|
+
* - If `genericErrors` is provided, renders standard and liquid errors (if present) without platform labels.
|
|
10
|
+
* - If `androidErrors`/`iosErrors` are provided, renders merged errors for each platform with platform labels and dividers.
|
|
11
|
+
*/
|
|
12
|
+
const ErrorTypeRenderer = ({
|
|
13
|
+
genericErrors,
|
|
14
|
+
androidErrors,
|
|
15
|
+
iosErrors,
|
|
16
|
+
ErrorSectionComponent,
|
|
17
|
+
}) => {
|
|
18
|
+
// Render generic (non-platform) errors
|
|
19
|
+
if (genericErrors) {
|
|
20
|
+
const hasStandard = genericErrors?.standard?.errorsToShow?.length > 0;
|
|
21
|
+
const hasLiquid = genericErrors?.liquid?.errorsToShow?.length > 0;
|
|
22
|
+
if (!hasStandard && !hasLiquid) return null;
|
|
23
|
+
|
|
24
|
+
const liquidGenericProps = {
|
|
25
|
+
title: genericErrors?.liquid?.title,
|
|
26
|
+
errors: genericErrors?.liquid?.errorsToShow,
|
|
27
|
+
platformLabel: null,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className="error-container">
|
|
32
|
+
{hasStandard && (
|
|
33
|
+
<ErrorSectionComponent
|
|
34
|
+
title={genericErrors?.standard?.title}
|
|
35
|
+
errors={genericErrors?.standard?.errorsToShow}
|
|
36
|
+
liquidError={false}
|
|
37
|
+
platformLabel={null}
|
|
38
|
+
/>
|
|
39
|
+
)}
|
|
40
|
+
{hasStandard && hasLiquid && <CapDivider className="liquid-divider" />}
|
|
41
|
+
{hasLiquid && (
|
|
42
|
+
<ErrorSectionComponent
|
|
43
|
+
{...liquidGenericProps}
|
|
44
|
+
liquidError
|
|
45
|
+
/>
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Render platform-specific errors (Android/iOS)
|
|
52
|
+
const hasAndroid = hasPlatformErrors(androidErrors);
|
|
53
|
+
const hasIos = hasPlatformErrors(iosErrors);
|
|
54
|
+
|
|
55
|
+
if (!hasAndroid && !hasIos) return null;
|
|
56
|
+
|
|
57
|
+
const showPlatformLabels = !!androidErrors?.standard?.platformLabel || !!iosErrors?.standard?.platformLabel;
|
|
58
|
+
const showTitle = hasAndroid && hasIos && showPlatformLabels;
|
|
59
|
+
const liquidAndroidProps = getLiquidProps(androidErrors, true, showTitle, showPlatformLabels);
|
|
60
|
+
const liquidIosProps = getLiquidProps(iosErrors, false, showTitle, showPlatformLabels);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="error-container">
|
|
64
|
+
{hasAndroid && (
|
|
65
|
+
<ErrorSectionComponent
|
|
66
|
+
{...liquidAndroidProps}
|
|
67
|
+
/>
|
|
68
|
+
)}
|
|
69
|
+
{hasAndroid && hasIos && showPlatformLabels && (
|
|
70
|
+
<CapDivider className="platform-divider" />
|
|
71
|
+
)}
|
|
72
|
+
{hasIos && (
|
|
73
|
+
<ErrorSectionComponent
|
|
74
|
+
{...liquidIosProps}
|
|
75
|
+
/>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
ErrorTypeRenderer.propTypes = {
|
|
82
|
+
genericErrors: PropTypes.shape({
|
|
83
|
+
standard: PropTypes.shape({
|
|
84
|
+
errorsToShow: PropTypes.array.isRequired,
|
|
85
|
+
title: PropTypes.node.isRequired,
|
|
86
|
+
}),
|
|
87
|
+
liquid: PropTypes.shape({
|
|
88
|
+
errorsToShow: PropTypes.array.isRequired,
|
|
89
|
+
title: PropTypes.node.isRequired,
|
|
90
|
+
}),
|
|
91
|
+
}),
|
|
92
|
+
androidErrors: PropTypes.shape({
|
|
93
|
+
standard: PropTypes.shape({
|
|
94
|
+
errorsToShow: PropTypes.array.isRequired,
|
|
95
|
+
title: PropTypes.node.isRequired,
|
|
96
|
+
platformLabel: PropTypes.string,
|
|
97
|
+
}),
|
|
98
|
+
liquid: PropTypes.shape({
|
|
99
|
+
errorsToShow: PropTypes.array.isRequired,
|
|
100
|
+
title: PropTypes.node.isRequired,
|
|
101
|
+
platformLabel: PropTypes.string,
|
|
102
|
+
}),
|
|
103
|
+
}),
|
|
104
|
+
iosErrors: PropTypes.shape({
|
|
105
|
+
standard: PropTypes.shape({
|
|
106
|
+
errorsToShow: PropTypes.array.isRequired,
|
|
107
|
+
title: PropTypes.node.isRequired,
|
|
108
|
+
platformLabel: PropTypes.string,
|
|
109
|
+
}),
|
|
110
|
+
liquid: PropTypes.shape({
|
|
111
|
+
errorsToShow: PropTypes.array.isRequired,
|
|
112
|
+
title: PropTypes.node.isRequired,
|
|
113
|
+
platformLabel: PropTypes.string,
|
|
114
|
+
}),
|
|
115
|
+
}),
|
|
116
|
+
ErrorSectionComponent: PropTypes.elementType.isRequired,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
ErrorTypeRenderer.defaultProps = {
|
|
120
|
+
genericErrors: null,
|
|
121
|
+
androidErrors: null,
|
|
122
|
+
iosErrors: null,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export default ErrorTypeRenderer;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import ErrorTypeRenderer from './ErrorTypeRenderer';
|
|
4
|
+
import { ANDROID, IOS } from '../../v2Containers/CreativesContainer/constants';
|
|
5
|
+
|
|
6
|
+
describe('ErrorTypeRenderer', () => { const ErrorSectionComponent = jest.fn(({
|
|
7
|
+
title,
|
|
8
|
+
errors,
|
|
9
|
+
liquidError,
|
|
10
|
+
platformLabel,
|
|
11
|
+
}) => (
|
|
12
|
+
<div data-testid="error-section">
|
|
13
|
+
<span>{title}</span>
|
|
14
|
+
<span>{errors && errors.join(',')}</span>
|
|
15
|
+
<span>{liquidError ? 'liquid' : 'standard'}</span>
|
|
16
|
+
<span>{platformLabel}</span>
|
|
17
|
+
</div>
|
|
18
|
+
));
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
ErrorSectionComponent.mockClear();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('renders nothing if all errors are empty', () => {
|
|
25
|
+
const { container } = render(
|
|
26
|
+
<ErrorTypeRenderer
|
|
27
|
+
genericErrors={{ standard: { errorsToShow: [], title: 'Standard' }, liquid: { errorsToShow: [], title: 'Liquid' } }}
|
|
28
|
+
ErrorSectionComponent={ErrorSectionComponent}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
31
|
+
expect(container.firstChild).toBeNull();
|
|
32
|
+
expect(ErrorSectionComponent).not.toHaveBeenCalled();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('renders generic standard errors only', () => {
|
|
36
|
+
render(
|
|
37
|
+
<ErrorTypeRenderer
|
|
38
|
+
genericErrors={{ standard: { errorsToShow: ['err1', 'err2'], title: 'Standard' }, liquid: { errorsToShow: [], title: 'Liquid' } }}
|
|
39
|
+
ErrorSectionComponent={ErrorSectionComponent}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
expect(ErrorSectionComponent).toHaveBeenCalledWith(
|
|
43
|
+
expect.objectContaining({
|
|
44
|
+
title: 'Standard',
|
|
45
|
+
errors: ['err1', 'err2'],
|
|
46
|
+
liquidError: false,
|
|
47
|
+
platformLabel: null,
|
|
48
|
+
}),
|
|
49
|
+
expect.anything()
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('renders generic liquid errors only', () => {
|
|
54
|
+
render(
|
|
55
|
+
<ErrorTypeRenderer
|
|
56
|
+
genericErrors={{ standard: { errorsToShow: [], title: 'Standard' }, liquid: { errorsToShow: ['l1'], title: 'Liquid' } }}
|
|
57
|
+
ErrorSectionComponent={ErrorSectionComponent}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
expect(ErrorSectionComponent).toHaveBeenCalledWith(
|
|
61
|
+
expect.objectContaining({
|
|
62
|
+
title: 'Liquid',
|
|
63
|
+
errors: ['l1'],
|
|
64
|
+
liquidError: true,
|
|
65
|
+
platformLabel: null,
|
|
66
|
+
}),
|
|
67
|
+
expect.anything()
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('renders both generic standard and liquid errors', () => {
|
|
72
|
+
render(
|
|
73
|
+
<ErrorTypeRenderer
|
|
74
|
+
genericErrors={{ standard: { errorsToShow: ['err1'], title: 'Standard' }, liquid: { errorsToShow: ['l1'], title: 'Liquid' } }}
|
|
75
|
+
ErrorSectionComponent={ErrorSectionComponent}
|
|
76
|
+
/>
|
|
77
|
+
);
|
|
78
|
+
expect(ErrorSectionComponent).toHaveBeenCalledWith(
|
|
79
|
+
expect.objectContaining({
|
|
80
|
+
title: 'Standard',
|
|
81
|
+
errors: ['err1'],
|
|
82
|
+
liquidError: false,
|
|
83
|
+
platformLabel: null,
|
|
84
|
+
}),
|
|
85
|
+
expect.anything()
|
|
86
|
+
);
|
|
87
|
+
expect(ErrorSectionComponent).toHaveBeenCalledWith(
|
|
88
|
+
expect.objectContaining({
|
|
89
|
+
title: 'Liquid',
|
|
90
|
+
errors: ['l1'],
|
|
91
|
+
liquidError: true,
|
|
92
|
+
platformLabel: null,
|
|
93
|
+
}),
|
|
94
|
+
expect.anything()
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('renders merged android errors with platform label', () => {
|
|
99
|
+
render(
|
|
100
|
+
<ErrorTypeRenderer
|
|
101
|
+
androidErrors={{
|
|
102
|
+
standard: { errorsToShow: ['a1'], title: 'Android Standard', platformLabel: ANDROID },
|
|
103
|
+
liquid: { errorsToShow: ['al1'], title: 'Android Liquid', platformLabel: ANDROID },
|
|
104
|
+
}}
|
|
105
|
+
iosErrors={{
|
|
106
|
+
standard: { errorsToShow: [], title: 'iOS Standard', platformLabel: IOS },
|
|
107
|
+
liquid: { errorsToShow: [], title: 'iOS Liquid', platformLabel: IOS },
|
|
108
|
+
}}
|
|
109
|
+
ErrorSectionComponent={ErrorSectionComponent}
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
112
|
+
expect(ErrorSectionComponent).toHaveBeenCalledWith(
|
|
113
|
+
expect.objectContaining({
|
|
114
|
+
title: false,
|
|
115
|
+
errors: ['a1', 'al1'],
|
|
116
|
+
liquidError: true,
|
|
117
|
+
platformLabel: ANDROID,
|
|
118
|
+
}),
|
|
119
|
+
expect.anything()
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('renders merged ios errors with platform label', () => {
|
|
124
|
+
render(
|
|
125
|
+
<ErrorTypeRenderer
|
|
126
|
+
androidErrors={{
|
|
127
|
+
standard: { errorsToShow: [], title: 'Android Standard', platformLabel: ANDROID },
|
|
128
|
+
liquid: { errorsToShow: [], title: 'Android Liquid', platformLabel: ANDROID },
|
|
129
|
+
}}
|
|
130
|
+
iosErrors={{
|
|
131
|
+
standard: { errorsToShow: ['i1'], title: 'iOS Standard', platformLabel: IOS },
|
|
132
|
+
liquid: { errorsToShow: ['il1'], title: 'iOS Liquid', platformLabel: IOS },
|
|
133
|
+
}}
|
|
134
|
+
ErrorSectionComponent={ErrorSectionComponent}
|
|
135
|
+
/>
|
|
136
|
+
);
|
|
137
|
+
expect(ErrorSectionComponent).toHaveBeenCalledWith(
|
|
138
|
+
expect.objectContaining({
|
|
139
|
+
title: 'iOS Liquid',
|
|
140
|
+
errors: ['i1', 'il1'],
|
|
141
|
+
liquidError: true,
|
|
142
|
+
platformLabel: IOS,
|
|
143
|
+
}),
|
|
144
|
+
expect.anything()
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -9,19 +9,25 @@ import {
|
|
|
9
9
|
FormattedMessage,
|
|
10
10
|
FormattedNumber,
|
|
11
11
|
injectIntl,
|
|
12
|
-
intlShape,
|
|
13
12
|
} from "react-intl";
|
|
14
13
|
import "./style.scss";
|
|
15
14
|
import messages from "./messages";
|
|
15
|
+
import { processErrors } from "./utils";
|
|
16
|
+
import ErrorTypeRenderer from "./ErrorTypeRenderer";
|
|
17
|
+
import { ANDROID, GENERIC, IOS } from "../../v2Containers/CreativesContainer/constants";
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
const ErrorSection = ({
|
|
20
|
+
title,
|
|
21
|
+
errors,
|
|
22
|
+
liquidError,
|
|
23
|
+
platformLabel,
|
|
24
|
+
}) => (
|
|
25
|
+
<>
|
|
26
|
+
{title && (
|
|
23
27
|
<CapRow
|
|
24
|
-
className={`error-header ${
|
|
28
|
+
className={`error-header ${
|
|
29
|
+
!liquidError ? "standard-error-header" : ""
|
|
30
|
+
}`}
|
|
25
31
|
>
|
|
26
32
|
<>
|
|
27
33
|
<CapIcon size="s" type="warning" className="warning-icon" />
|
|
@@ -32,8 +38,7 @@ export const ErrorInfoNote = (props) => {
|
|
|
32
38
|
<CapButton
|
|
33
39
|
type="flat"
|
|
34
40
|
className="add-btn"
|
|
35
|
-
onClick={() =>
|
|
36
|
-
window.open(`https://docs.capillarytech.com/docs/liquid-language-in-messages`, "_blank")
|
|
41
|
+
onClick={() => window.open("https://docs.capillarytech.com/docs/liquid-language-in-messages", "_blank")
|
|
37
42
|
}
|
|
38
43
|
>
|
|
39
44
|
<FormattedMessage {...messages.liquidDoc} />
|
|
@@ -42,48 +47,110 @@ export const ErrorInfoNote = (props) => {
|
|
|
42
47
|
)}
|
|
43
48
|
</>
|
|
44
49
|
</CapRow>
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
)}
|
|
51
|
+
{platformLabel && (
|
|
52
|
+
<CapRow className="error-header-sub-title">
|
|
53
|
+
<CapLabel type="label2">{platformLabel}</CapLabel>
|
|
54
|
+
</CapRow>
|
|
55
|
+
)}
|
|
56
|
+
<CapList
|
|
57
|
+
className="error-list"
|
|
58
|
+
size="small"
|
|
59
|
+
dataSource={errors}
|
|
60
|
+
renderItem={(error, index) => (
|
|
61
|
+
<CapList.Item>
|
|
62
|
+
<CapLabel type="label2" className="cap-list-v2-error-item">
|
|
63
|
+
<CapLabel type="label2">
|
|
64
|
+
<FormattedNumber value={index + 1} />.
|
|
54
65
|
</CapLabel>
|
|
55
|
-
|
|
56
|
-
|
|
66
|
+
<CapLabel type="label2">{error}</CapLabel>
|
|
67
|
+
</CapLabel>
|
|
68
|
+
</CapList.Item>
|
|
69
|
+
)}
|
|
70
|
+
/>
|
|
71
|
+
</>
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
ErrorSection.propTypes = {
|
|
75
|
+
title: PropTypes.node.isRequired,
|
|
76
|
+
errors: PropTypes.array,
|
|
77
|
+
liquidError: PropTypes.bool,
|
|
78
|
+
platformLabel: PropTypes.string,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
ErrorSection.defaultProps = {
|
|
82
|
+
errors: [],
|
|
83
|
+
liquidError: false,
|
|
84
|
+
platformLabel: null,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const ErrorInfoNote = (props) => {
|
|
88
|
+
const { errorMessages } = props;
|
|
89
|
+
|
|
90
|
+
const {
|
|
91
|
+
LIQUID_ERROR_MSG: rawLiquidErrors = [],
|
|
92
|
+
STANDARD_ERROR_MSG: rawStandardErrors = [],
|
|
93
|
+
} = errorMessages || {};
|
|
94
|
+
|
|
95
|
+
// Detect if platform-specific (ANDROID/IOS) or GENERIC
|
|
96
|
+
const isObject = typeof rawStandardErrors === 'object' && rawStandardErrors !== null;
|
|
97
|
+
const isNotArray = !Array.isArray(rawStandardErrors);
|
|
98
|
+
const hasPlatformKeys = isObject && isNotArray && [ANDROID, IOS, GENERIC].some((key) => key in rawStandardErrors);
|
|
99
|
+
|
|
100
|
+
if (hasPlatformKeys) {
|
|
101
|
+
// Platform-specific
|
|
102
|
+
const androidErrors = {
|
|
103
|
+
standard: processErrors(rawStandardErrors, 'standard', ANDROID, messages),
|
|
104
|
+
liquid: processErrors(rawLiquidErrors, 'liquid', ANDROID, messages),
|
|
105
|
+
};
|
|
106
|
+
const iosErrors = {
|
|
107
|
+
standard: processErrors(rawStandardErrors, 'standard', IOS, messages),
|
|
108
|
+
liquid: processErrors(rawLiquidErrors, 'liquid', IOS, messages),
|
|
109
|
+
};
|
|
110
|
+
return (
|
|
111
|
+
<ErrorTypeRenderer
|
|
112
|
+
androidErrors={androidErrors}
|
|
113
|
+
iosErrors={iosErrors}
|
|
114
|
+
ErrorSectionComponent={ErrorSection}
|
|
57
115
|
/>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
// GENERIC (not platform-specific)
|
|
119
|
+
const genericStandard = processErrors(rawStandardErrors, 'standard', null, messages);
|
|
120
|
+
const genericLiquid = processErrors(rawLiquidErrors, 'liquid', null, messages);
|
|
61
121
|
return (
|
|
62
|
-
<
|
|
63
|
-
{
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
title={
|
|
67
|
-
<FormattedMessage {...messages.standardErrorHeader} />
|
|
68
|
-
}
|
|
69
|
-
errors={STANDARD_ERROR_MSG}
|
|
70
|
-
/>
|
|
71
|
-
</>
|
|
72
|
-
)}
|
|
73
|
-
{liquidErrors > 0 && (
|
|
74
|
-
<ErrorSection
|
|
75
|
-
title={<FormattedMessage {...messages.dynamicErrorHeader} />}
|
|
76
|
-
errors={LIQUID_ERROR_MSG}
|
|
77
|
-
liquidError={true}
|
|
78
|
-
/>
|
|
79
|
-
)}
|
|
80
|
-
</div>
|
|
122
|
+
<ErrorTypeRenderer
|
|
123
|
+
genericErrors={{ standard: genericStandard, liquid: genericLiquid }}
|
|
124
|
+
ErrorSectionComponent={ErrorSection}
|
|
125
|
+
/>
|
|
81
126
|
);
|
|
82
127
|
};
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
128
|
+
|
|
129
|
+
ErrorInfoNote.defaultProps = {
|
|
130
|
+
errorMessages: {
|
|
131
|
+
LIQUID_ERROR_MSG: [],
|
|
132
|
+
STANDARD_ERROR_MSG: [],
|
|
133
|
+
},
|
|
87
134
|
};
|
|
88
135
|
|
|
136
|
+
ErrorInfoNote.propTypes = {
|
|
137
|
+
errorMessages: PropTypes.shape({
|
|
138
|
+
LIQUID_ERROR_MSG: PropTypes.oneOfType([
|
|
139
|
+
PropTypes.array,
|
|
140
|
+
PropTypes.shape({
|
|
141
|
+
ANDROID: PropTypes.array,
|
|
142
|
+
IOS: PropTypes.array,
|
|
143
|
+
GENERIC: PropTypes.array,
|
|
144
|
+
}),
|
|
145
|
+
]),
|
|
146
|
+
STANDARD_ERROR_MSG: PropTypes.oneOfType([
|
|
147
|
+
PropTypes.array,
|
|
148
|
+
PropTypes.shape({
|
|
149
|
+
ANDROID: PropTypes.array,
|
|
150
|
+
IOS: PropTypes.array,
|
|
151
|
+
GENERIC: PropTypes.array,
|
|
152
|
+
}),
|
|
153
|
+
]),
|
|
154
|
+
}),
|
|
155
|
+
};
|
|
89
156
|
export default injectIntl(ErrorInfoNote);
|
|
@@ -26,4 +26,29 @@ export default defineMessages({
|
|
|
26
26
|
defaultMessage:
|
|
27
27
|
"Aira can make mistakes. Please verify the suggestions before applying them"
|
|
28
28
|
},
|
|
29
|
+
liquidDocLink: {
|
|
30
|
+
id: `${scope}.liquidDocLink`,
|
|
31
|
+
defaultMessage:
|
|
32
|
+
"https://docs.capillarytech.com/docs/liquid-language-in-messages"
|
|
33
|
+
},
|
|
34
|
+
androidDynamicErrorHeader: {
|
|
35
|
+
id: `${scope}.androidDynamicErrorHeader`,
|
|
36
|
+
defaultMessage:
|
|
37
|
+
"Errors found in Android Dynamic Tags"
|
|
38
|
+
},
|
|
39
|
+
iosDynamicErrorHeader: {
|
|
40
|
+
id: `${scope}.iosDynamicErrorHeader`,
|
|
41
|
+
defaultMessage:
|
|
42
|
+
"Errors found in iOS Dynamic Tags"
|
|
43
|
+
},
|
|
44
|
+
androidStandardErrorHeader: {
|
|
45
|
+
id: `${scope}.androidStandardErrorHeader`,
|
|
46
|
+
defaultMessage:
|
|
47
|
+
"Errors found in Android Standard Tags"
|
|
48
|
+
},
|
|
49
|
+
iosStandardErrorHeader: {
|
|
50
|
+
id: `${scope}.iosStandardErrorHeader`,
|
|
51
|
+
defaultMessage:
|
|
52
|
+
"Errors found in iOS Standard Tags"
|
|
53
|
+
},
|
|
29
54
|
});
|
|
@@ -58,15 +58,28 @@
|
|
|
58
58
|
gap: $CAP_SPACE_04;
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
+
|
|
61
62
|
.liquid-divider {
|
|
62
|
-
&.ant-divider.cap-divider-v2{
|
|
63
|
+
&.ant-divider.cap-divider-v2 {
|
|
63
64
|
background: $FONT_COLOR_04;
|
|
64
65
|
}
|
|
65
66
|
margin: $CAP_SPACE_08 0;
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
.platform-divider {
|
|
70
|
+
&.ant-divider.cap-divider-v2 {
|
|
71
|
+
background: $FONT_COLOR_04;
|
|
72
|
+
margin: $CAP_SPACE_12 0;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
68
76
|
.aria-error-footer {
|
|
69
77
|
display: flex;
|
|
70
78
|
align-items: center;
|
|
71
79
|
gap: $CAP_SPACE_04;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.error-header-sub-title {
|
|
83
|
+
margin-left: $CAP_SPACE_04;
|
|
84
|
+
font-weight: 500;
|
|
72
85
|
}
|