@capillarytech/creatives-library 9.0.73-alpha.sprint-testpreview.22 → 9.1.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/package.json +1 -1
- package/v2Components/CapTagListWithInput/__tests__/CapTagListWithInput.test.js +58 -1
- package/v2Components/CapTagListWithInput/index.js +64 -35
- package/v2Components/CapTagListWithInput/index.scss +9 -0
- package/v2Components/CommonTestAndPreview/UnifiedPreview/EmailPreviewContent.js +8 -0
- package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +7 -0
- package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +1 -0
- package/v2Components/CommonTestAndPreview/actions.js +1 -1
- package/v2Components/CommonTestAndPreview/index.js +36 -75
- package/v2Components/CommonTestAndPreview/sagas.js +2 -1
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/EmailPreviewContent.test.js +27 -0
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +40 -0
- package/v2Components/CommonTestAndPreview/tests/index.test.js +92 -97
- package/v2Components/CommonTestAndPreview/tests/sagas.test.js +24 -0
- package/v2Components/CommonTestAndPreview/tests/utils.test.js +0 -66
- package/v2Components/CommonTestAndPreview/utils.js +0 -14
- package/v2Components/EmailMobilePreview/index.js +8 -1
- package/v2Components/EmailMobilePreview/index.scss +12 -3
- package/v2Components/EmailMobilePreview/tests/index.test.js +31 -0
- package/v2Components/EmailPreviewV2/_emailPreviewV2.scss +37 -5
- package/v2Components/EmailPreviewV2/index.js +18 -6
- package/v2Components/EmailPreviewV2/messages.js +4 -0
- package/v2Components/EmailPreviewV2/tests/index.test.js +43 -0
- package/v2Components/FormBuilder/Classic.js +134 -14
- package/v2Components/FormBuilder/Functional/FormBuilderShell.js +9 -0
- package/v2Components/FormBuilder/Functional/layout/TabsContainer.js +11 -12
- package/v2Components/FormBuilder/Functional/tests/mpush.singlePlatform.test.js +144 -0
- package/v2Components/FormBuilder/Functional/tests/tabsContainer.test.js +17 -5
- package/v2Components/FormBuilder/_formBuilder.scss +21 -0
- package/v2Components/FormBuilder/messages.js +12 -0
- package/v2Components/FormBuilder/tests/mpush.characterization.test.js +45 -0
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +2 -1
- package/v2Containers/CommunicationFlow/Tests/constants.test.js +4 -4
- package/v2Containers/CommunicationFlow/constants.js +1 -0
- package/v2Containers/CommunicationFlow/utils/Tests/mapCommDefinitionToStepData.test.js +2 -1
- package/v2Containers/CommunicationFlow/utils/mapCommDefinitionToStepData.js +1 -0
- package/v2Containers/CreativesContainer/SlideBoxContent.js +0 -2
- package/v2Containers/CreativesContainer/index.js +7 -1
- package/v2Containers/CreativesContainer/tests/index.test.js +81 -0
- package/v2Containers/Email/index.js +31 -4
- package/v2Containers/Email/initialSchema.js +30 -0
- package/v2Containers/Email/messages.js +8 -0
- package/v2Containers/Email/tests/index.test.js +107 -0
- package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +172 -8
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +264 -5
- package/v2Containers/EmailWrapper/components/_emailHTMLEditor.scss +29 -0
- package/v2Containers/EmailWrapper/constants.js +5 -0
- package/v2Containers/EmailWrapper/messages.js +24 -0
- package/v2Containers/MobilePush/Create/constants.js +1 -0
- package/v2Containers/MobilePush/Create/index.js +2 -2
- package/v2Containers/MobilePush/Create/test/contentValidity.test.js +58 -0
- package/v2Containers/MobilePush/Edit/index.js +2 -2
- package/v2Containers/MobilePush/Edit/test/contentValidity.test.js +17 -0
- package/v2Containers/MobilePush/commonMethods.js +7 -8
- package/v2Containers/Rcs/index.js +1 -27
- package/v2Containers/TagList/index.js +7 -0
- package/v2Containers/TagList/tests/TagList.test.js +42 -0
- package/v2Containers/Templates/index.js +4 -9
- package/v2Containers/Templates/tests/index.test.js +35 -107
- package/v2Containers/Viber/index.scss +31 -0
- package/v2Containers/Whatsapp/index.js +1 -18
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +2 -2
- package/v2Containers/Zalo/index.js +0 -18
- package/v2Containers/Rcs/tests/testAndPreviewFromListing.test.js +0 -132
- package/v2Containers/Whatsapp/tests/testAndPreviewFromListing.test.js +0 -91
- package/v2Containers/Zalo/tests/testAndPreviewFromListing.test.js +0 -98
|
@@ -9,3 +9,32 @@
|
|
|
9
9
|
.html-editor-options-icon {
|
|
10
10
|
cursor: pointer;
|
|
11
11
|
}
|
|
12
|
+
|
|
13
|
+
// Subject/Pre-header field rows
|
|
14
|
+
.email-html-editor-field {
|
|
15
|
+
margin-bottom: $CAP_SPACE_16;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.email-html-editor-add-preheader-btn {
|
|
19
|
+
white-space: nowrap;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Subject/Pre-header inline prefix label and Pre-header suffix (counter + Remove)
|
|
23
|
+
.email-html-editor-prefix {
|
|
24
|
+
padding-right: $CAP_SPACE_08;
|
|
25
|
+
margin-right: $CAP_SPACE_08;
|
|
26
|
+
border-right: 0.0625rem solid $CAP_COLOR_16;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.email-html-editor-prefix-label {
|
|
30
|
+
font-weight: 600;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.email-html-editor-counter {
|
|
34
|
+
white-space: nowrap;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.email-html-editor-remove-link {
|
|
38
|
+
white-space: nowrap;
|
|
39
|
+
cursor: pointer;
|
|
40
|
+
}
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { UNSUBSCRIBE_TAG } from '../../constants/unified';
|
|
8
|
+
|
|
7
9
|
export const DEFAULT_ACTION = "app/EmailWrapper/DEFAULT_ACTION";
|
|
8
10
|
export const EMAIL_CREATE_MODES = {
|
|
9
11
|
UPLOAD: "upload",
|
|
@@ -17,3 +19,6 @@ export const STEPS = {
|
|
|
17
19
|
TEMPLATE_SELECTION: "templateSelection",
|
|
18
20
|
CREATE_TEMPLATE_CONTENT: "createTemplateContent",
|
|
19
21
|
};
|
|
22
|
+
|
|
23
|
+
// Unsubscribe is not a valid Pre-header tag — hide it from the "Add Label" dropdown there.
|
|
24
|
+
export const PREHEADER_EXCLUDED_TAGS = [UNSUBSCRIBE_TAG];
|
|
@@ -122,6 +122,30 @@ export default defineMessages({
|
|
|
122
122
|
id: 'creatives.containersV2.EmailWrapper.enterEmailSubject',
|
|
123
123
|
defaultMessage: 'Enter Email Subject',
|
|
124
124
|
},
|
|
125
|
+
preheader: {
|
|
126
|
+
id: 'creatives.containersV2.EmailWrapper.preheader',
|
|
127
|
+
defaultMessage: 'Pre-header',
|
|
128
|
+
},
|
|
129
|
+
enterEmailPreheader: {
|
|
130
|
+
id: 'creatives.containersV2.EmailWrapper.enterEmailPreheader',
|
|
131
|
+
defaultMessage: 'e.g. Get 20% off your next order',
|
|
132
|
+
},
|
|
133
|
+
preheaderInfoTooltip: {
|
|
134
|
+
id: 'creatives.containersV2.EmailWrapper.preheaderInfoTooltip',
|
|
135
|
+
defaultMessage: 'Shows next to the subject line in the inbox, use it to add context and improve open rates. Keep it to 40–85 characters.',
|
|
136
|
+
},
|
|
137
|
+
addPreheader: {
|
|
138
|
+
id: 'creatives.containersV2.EmailWrapper.addPreheader',
|
|
139
|
+
defaultMessage: '+ Pre-header',
|
|
140
|
+
},
|
|
141
|
+
removePreheader: {
|
|
142
|
+
id: 'creatives.containersV2.EmailWrapper.removePreheader',
|
|
143
|
+
defaultMessage: 'Remove',
|
|
144
|
+
},
|
|
145
|
+
unsubscribeNotAllowedInPreheader: {
|
|
146
|
+
id: 'creatives.containersV2.EmailWrapper.unsubscribeNotAllowedInPreheader',
|
|
147
|
+
defaultMessage: 'Unsubscribe tag is not allowed in Pre-header.',
|
|
148
|
+
},
|
|
125
149
|
editorType: {
|
|
126
150
|
id: 'creatives.containersV2.EmailWrapper.editorType',
|
|
127
151
|
defaultMessage: 'Editor type',
|
|
@@ -23,3 +23,4 @@ export const GET_IOS_CTAS_SUCCESS = 'app/v2Containers/PushMessage/Create/GET_IOS
|
|
|
23
23
|
export const GET_IOS_CTAS_FAILURE = 'app/v2Containers/PushMessage/Create/GET_IOS_CTAS_FAILURE';
|
|
24
24
|
export const RESET_STORE = 'app/v2Containers/PushMessage/Create/RESET_STORE';
|
|
25
25
|
export const EXTERNAL_LINK_LOWERCASE = 'external link';
|
|
26
|
+
export const MOBILE_PUSH_PLATFORM_COUNT = 2;
|
|
@@ -81,7 +81,7 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
81
81
|
if (typeof this.props.onContentValidityChange !== 'function') {
|
|
82
82
|
return;
|
|
83
83
|
}
|
|
84
|
-
const validity = getMobilePushEmbeddedContentValidity(this.state?.formData
|
|
84
|
+
const validity = getMobilePushEmbeddedContentValidity(this.state?.formData);
|
|
85
85
|
const isContentEmpty = !!validity.isContentEmpty;
|
|
86
86
|
if (this._lastReportedMobilePushContentEmpty === isContentEmpty) return;
|
|
87
87
|
this._lastReportedMobilePushContentEmpty = isContentEmpty;
|
|
@@ -187,7 +187,7 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
187
187
|
this.setState({showIosCtaTable: true});
|
|
188
188
|
}
|
|
189
189
|
if (nextProps.metaEntities && nextProps.metaEntities.layouts && nextProps.metaEntities.layouts.length > 0 && isEmpty(this.state.schema)) {
|
|
190
|
-
const schema = this.props.params.mode === "text" ? nextProps.metaEntities.layouts[0].definition.textSchema : nextProps.metaEntities.layouts[0].definition.imageSchema;
|
|
190
|
+
const schema = cloneDeep(this.props.params.mode === "text" ? nextProps.metaEntities.layouts[0].definition.textSchema : nextProps.metaEntities.layouts[0].definition.imageSchema);
|
|
191
191
|
const isAndroidSupported = get(this, "props.Templates.selectedWeChatAccount.configs.android") === '1';
|
|
192
192
|
const isIosSupported = get(this, "props.Templates.selectedWeChatAccount.configs.ios") === '1';
|
|
193
193
|
if (!isAndroidSupported) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { shallow } from 'enzyme';
|
|
3
|
+
import { cloneDeep } from 'lodash';
|
|
3
4
|
import { Create } from '../index';
|
|
5
|
+
import { response as mpushSchemaResponse } from '../../initialSchema';
|
|
4
6
|
|
|
5
7
|
// Shallow-render the plain class (bypassing the withCreatives/Redux/saga HOC) so this test
|
|
6
8
|
// only exercises the componentDidUpdate → onContentValidityChange wiring in isolation.
|
|
@@ -56,6 +58,25 @@ describe('MobilePush/Create — onContentValidityChange reporting', () => {
|
|
|
56
58
|
);
|
|
57
59
|
});
|
|
58
60
|
|
|
61
|
+
it('reports isContentEmpty: false when only iOS has content', () => {
|
|
62
|
+
const onContentValidityChange = jest.fn();
|
|
63
|
+
const wrapper = shallow(<Create {...baseProps()} onContentValidityChange={onContentValidityChange} />);
|
|
64
|
+
wrapper.setState({
|
|
65
|
+
formData: { 0: { 'message-title': '', 'message-editor': '' }, 1: {} },
|
|
66
|
+
tabCount: 1,
|
|
67
|
+
});
|
|
68
|
+
onContentValidityChange.mockClear();
|
|
69
|
+
|
|
70
|
+
wrapper.setState({
|
|
71
|
+
formData: { 0: { 'message-title': '', 'message-editor': '' }, 1: { 'message-title2': 'test', 'message-editor2': 'test' } },
|
|
72
|
+
tabCount: 1,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(onContentValidityChange).toHaveBeenCalledWith(
|
|
76
|
+
expect.objectContaining({ isContentEmpty: false }),
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
59
80
|
it('re-reports empty when a previously entered message is cleared out', () => {
|
|
60
81
|
const onContentValidityChange = jest.fn();
|
|
61
82
|
const wrapper = shallow(<Create {...baseProps()} onContentValidityChange={onContentValidityChange} />);
|
|
@@ -94,3 +115,40 @@ describe('MobilePush/Create — onContentValidityChange reporting', () => {
|
|
|
94
115
|
expect(onContentValidityChange).not.toHaveBeenCalled();
|
|
95
116
|
});
|
|
96
117
|
});
|
|
118
|
+
|
|
119
|
+
describe('MobilePush/Create — shared layout schema', () => {
|
|
120
|
+
it('hides Android on its own schema copy, leaving the store layout untouched', () => {
|
|
121
|
+
const storeSchema = cloneDeep(mpushSchemaResponse.metaEntities[0].definition.textSchema);
|
|
122
|
+
const standaloneSectionCount = storeSchema.standalone.sections.length;
|
|
123
|
+
const wrapper = shallow(<Create
|
|
124
|
+
{...baseProps()}
|
|
125
|
+
params={{ mode: 'text' }}
|
|
126
|
+
Templates={{ selectedWeChatAccount: { id: 669, configs: { android: '0', ios: '1' } } }}
|
|
127
|
+
/>);
|
|
128
|
+
wrapper.instance().injectEvents = jest.fn();
|
|
129
|
+
wrapper.setProps({ metaEntities: { layouts: [{ definition: { textSchema: storeSchema } }], tags: {} } });
|
|
130
|
+
|
|
131
|
+
const [ownSchema] = wrapper.instance().injectEvents.mock.calls[0];
|
|
132
|
+
expect(ownSchema).not.toBe(storeSchema);
|
|
133
|
+
expect(ownSchema.containers[0].panes[0].isSupported).toBe(false);
|
|
134
|
+
expect(wrapper.state('currentTab')).toBe(2);
|
|
135
|
+
expect(storeSchema.containers[0].panes[0].isSupported).toBe(true);
|
|
136
|
+
expect(storeSchema.standalone.sections).toHaveLength(standaloneSectionCount);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('uses its own copy of the image schema in image mode', () => {
|
|
140
|
+
const storeSchema = cloneDeep(mpushSchemaResponse.metaEntities[0].definition.imageSchema);
|
|
141
|
+
const wrapper = shallow(<Create
|
|
142
|
+
{...baseProps()}
|
|
143
|
+
params={{ mode: 'image' }}
|
|
144
|
+
Templates={{ selectedWeChatAccount: { id: 703, configs: { android: '1', ios: '1' } } }}
|
|
145
|
+
/>);
|
|
146
|
+
wrapper.instance().injectEvents = jest.fn();
|
|
147
|
+
wrapper.setProps({ metaEntities: { layouts: [{ definition: { imageSchema: storeSchema } }], tags: {} } });
|
|
148
|
+
|
|
149
|
+
const [ownSchema] = wrapper.instance().injectEvents.mock.calls[0];
|
|
150
|
+
expect(ownSchema).not.toBe(storeSchema);
|
|
151
|
+
expect(ownSchema.containers[0].panes[0].isSupported).not.toBe(false);
|
|
152
|
+
expect(ownSchema.containers[0].panes[1].isSupported).not.toBe(false);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -136,7 +136,7 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
136
136
|
if (typeof this.props.onContentValidityChange !== 'function') {
|
|
137
137
|
return;
|
|
138
138
|
}
|
|
139
|
-
const validity = getMobilePushEmbeddedContentValidity(this.state.formData
|
|
139
|
+
const validity = getMobilePushEmbeddedContentValidity(this.state.formData);
|
|
140
140
|
const isContentEmpty = !!validity.isContentEmpty;
|
|
141
141
|
if (this._lastReportedMobilePushContentEmpty === isContentEmpty) return;
|
|
142
142
|
this._lastReportedMobilePushContentEmpty = isContentEmpty;
|
|
@@ -242,7 +242,7 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
242
242
|
this.props.actions.getMobilepushTemplatesList('mobilepush', params);
|
|
243
243
|
}
|
|
244
244
|
if (nextProps.metaEntities && nextProps.metaEntities.layouts && nextProps.metaEntities.layouts.length > 0 && _.isEmpty(this.state.fullSchema)) {
|
|
245
|
-
const definition = nextProps.metaEntities.layouts[0].definition;
|
|
245
|
+
const definition = cloneDeep(nextProps.metaEntities.layouts[0].definition);
|
|
246
246
|
const initialSchema = definition?.textSchema || definition?.imageSchema || {};
|
|
247
247
|
this.setState({fullSchema: definition, schema: initialSchema}, () => {
|
|
248
248
|
// Use this.props (latest) in callback to avoid race: templateDetails may have arrived by now
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { shallow } from 'enzyme';
|
|
3
|
+
import { cloneDeep } from 'lodash';
|
|
3
4
|
import { Edit } from '../index';
|
|
5
|
+
import { response as mpushSchemaResponse } from '../../initialSchema';
|
|
4
6
|
|
|
5
7
|
// Shallow-render the plain class (bypassing the withCreatives/Redux/saga HOC) so this test
|
|
6
8
|
// only exercises the componentDidUpdate → onContentValidityChange wiring in isolation.
|
|
@@ -130,3 +132,18 @@ describe('MobilePush/Edit — onContentValidityChange reporting', () => {
|
|
|
130
132
|
expect(onContentValidityChange).not.toHaveBeenCalled();
|
|
131
133
|
});
|
|
132
134
|
});
|
|
135
|
+
|
|
136
|
+
describe('MobilePush/Edit — shared layout schema', () => {
|
|
137
|
+
it('keeps its own copy of the layout definition, leaving the store layout untouched', () => {
|
|
138
|
+
const storeDefinition = cloneDeep(mpushSchemaResponse.metaEntities[0].definition);
|
|
139
|
+
const wrapper = shallow(<Edit {...baseProps()} />);
|
|
140
|
+
wrapper.instance().handleEditSchemaOnPropsChange = jest.fn();
|
|
141
|
+
wrapper.setProps({ metaEntities: { layouts: [{ definition: storeDefinition }], tags: {} } });
|
|
142
|
+
|
|
143
|
+
const ownDefinition = wrapper.state('fullSchema');
|
|
144
|
+
expect(ownDefinition).not.toBe(storeDefinition);
|
|
145
|
+
expect(ownDefinition).toEqual(storeDefinition);
|
|
146
|
+
ownDefinition.textSchema.containers[0].panes[0].isSupported = false;
|
|
147
|
+
expect(storeDefinition.textSchema.containers[0].panes[0].isSupported).not.toBe(false);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -2,6 +2,7 @@ import {get, filter, cloneDeep} from 'lodash';
|
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import {FormattedMessage} from 'react-intl';
|
|
4
4
|
import messages from './Create/messages';
|
|
5
|
+
import { MOBILE_PUSH_PLATFORM_COUNT } from './Create/constants';
|
|
5
6
|
import { extractContent } from '../../utils/commonUtils';
|
|
6
7
|
function getPrimaryCtaFields({inputFields, fieldIndex, deepLinkOptions, tab}) {
|
|
7
8
|
const currentTab = tab || this.state.currentTab;
|
|
@@ -308,17 +309,15 @@ function getMobilePushTabContent(tabIndex, tabData) {
|
|
|
308
309
|
/**
|
|
309
310
|
* Live "is content empty" check for the legacy Mobile Push Edit/Create forms — the
|
|
310
311
|
* Mobile Push analogue of Sms's `getSmsEmbeddedFooterValidity` (see
|
|
311
|
-
* app/v2Containers/Sms/smsFormDataHelpers.js). Checks
|
|
312
|
-
* iOS
|
|
313
|
-
* filled in, so the overall content is empty only when
|
|
314
|
-
*
|
|
312
|
+
* app/v2Containers/Sms/smsFormDataHelpers.js). Checks both platform tabs (Android =
|
|
313
|
+
* formData[0], iOS = formData[1]); Mobile Push allows saving with just one platform
|
|
314
|
+
* filled in, so the overall content is empty only when BOTH tabs' title+message
|
|
315
|
+
* are empty.
|
|
315
316
|
* @param {object} formData FormBuilder state (same shape as this.state.formData)
|
|
316
|
-
* @param {number} [tabCount] Total number of active tabs (1 = Android only, 2 = Android + iOS)
|
|
317
317
|
* @returns {{ isContentEmpty: boolean }}
|
|
318
318
|
*/
|
|
319
|
-
function getMobilePushEmbeddedContentValidity(formData
|
|
320
|
-
|
|
321
|
-
for (let i = 0; i < count; i++) {
|
|
319
|
+
function getMobilePushEmbeddedContentValidity(formData) {
|
|
320
|
+
for (let i = 0; i < MOBILE_PUSH_PLATFORM_COUNT; i++) {
|
|
322
321
|
const content = getMobilePushTabContent(i, formData?.[i]);
|
|
323
322
|
const trimmed = content != null && content !== '' ? String(content).trim() : '';
|
|
324
323
|
if (trimmed) {
|
|
@@ -312,8 +312,6 @@ export const Rcs = (props) => {
|
|
|
312
312
|
|
|
313
313
|
// TestAndPreviewSlidebox state
|
|
314
314
|
const [showTestAndPreviewSlidebox, setShowTestAndPreviewSlidebox] = useState(false);
|
|
315
|
-
// Set when opened from listing "Preview and test"; closing then returns to the listing.
|
|
316
|
-
const isTestAndPreviewFromListingRef = useRef(false);
|
|
317
315
|
|
|
318
316
|
// Handle Test and Preview button click
|
|
319
317
|
const handleTestAndPreview = useCallback(() => {
|
|
@@ -330,11 +328,7 @@ export const Rcs = (props) => {
|
|
|
330
328
|
if (propsHandleCloseTestAndPreview) {
|
|
331
329
|
propsHandleCloseTestAndPreview();
|
|
332
330
|
}
|
|
333
|
-
|
|
334
|
-
isTestAndPreviewFromListingRef.current = false;
|
|
335
|
-
handleClose();
|
|
336
|
-
}
|
|
337
|
-
}, [propsHandleCloseTestAndPreview, handleClose]);
|
|
331
|
+
}, [propsHandleCloseTestAndPreview]);
|
|
338
332
|
|
|
339
333
|
// Helper to get RCS orientation from selectedDimension
|
|
340
334
|
const getRcsOrientation = () => {
|
|
@@ -1443,26 +1437,6 @@ export const Rcs = (props) => {
|
|
|
1443
1437
|
}
|
|
1444
1438
|
}, [rcsHydrationDetails, isFullMode, isHostInfoBip]);
|
|
1445
1439
|
|
|
1446
|
-
// Listing "Preview and test": open Test & Preview once the template is hydrated.
|
|
1447
|
-
const hasAutoOpenedTestAndPreviewRef = useRef(false);
|
|
1448
|
-
useEffect(() => {
|
|
1449
|
-
if (!isFullMode || !templateData?.openTestAndPreview || hasAutoOpenedTestAndPreviewRef.current) return;
|
|
1450
|
-
if (spin || !isEditFlow || smsFallbackData === undefined) return;
|
|
1451
|
-
if (!(isHostInfoBip || templateStatus === RCS_STATUSES.approved)) return;
|
|
1452
|
-
hasAutoOpenedTestAndPreviewRef.current = true;
|
|
1453
|
-
isTestAndPreviewFromListingRef.current = true;
|
|
1454
|
-
handleTestAndPreview();
|
|
1455
|
-
}, [
|
|
1456
|
-
isFullMode,
|
|
1457
|
-
templateData?.openTestAndPreview,
|
|
1458
|
-
spin,
|
|
1459
|
-
isEditFlow,
|
|
1460
|
-
smsFallbackData,
|
|
1461
|
-
isHostInfoBip,
|
|
1462
|
-
templateStatus,
|
|
1463
|
-
handleTestAndPreview,
|
|
1464
|
-
]);
|
|
1465
|
-
|
|
1466
1440
|
useEffect(() => {
|
|
1467
1441
|
if (templateType === contentType.text_message) {
|
|
1468
1442
|
setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
|
|
@@ -287,6 +287,10 @@ export class TagList extends React.Component { // eslint-disable-line react/pref
|
|
|
287
287
|
//filtering BADGES_RELATED_TAGS if org does not have BADGES feature enabled
|
|
288
288
|
excludedTags.push(...BADGES_RELATED_TAGS);
|
|
289
289
|
}
|
|
290
|
+
if (this.props.excludeTags?.length) {
|
|
291
|
+
// Caller-specified exclusions (e.g. unsubscribe tag not allowed in the Pre-header field)
|
|
292
|
+
excludedTags.push(...this.props.excludeTags);
|
|
293
|
+
}
|
|
290
294
|
//Form tags object with tag headers
|
|
291
295
|
_.forEach(tagsList, (temp) => {
|
|
292
296
|
const tag = temp.definition;
|
|
@@ -491,12 +495,15 @@ TagList.defaultProps = {
|
|
|
491
495
|
userLocale: 'en',
|
|
492
496
|
eventContextTags: [],
|
|
493
497
|
waitEventContextTags: {},
|
|
498
|
+
excludeTags: [],
|
|
494
499
|
};
|
|
495
500
|
|
|
496
501
|
TagList.propTypes = {
|
|
497
502
|
onTagSelect: PropTypes.func,
|
|
498
503
|
tags: PropTypes.array,
|
|
499
504
|
injectedTags: PropTypes.object,
|
|
505
|
+
// Tag values (e.g. 'unsubscribe') to hide from the Add Label dropdown for this instance
|
|
506
|
+
excludeTags: PropTypes.array,
|
|
500
507
|
label: PropTypes.string,
|
|
501
508
|
onContextChange: PropTypes.func,
|
|
502
509
|
moduleFilterEnabled: PropTypes.bool,
|
|
@@ -105,6 +105,48 @@ describe("TagList test : UNIT", () => {
|
|
|
105
105
|
expect(btn).toBeDisabled();
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
+
it('hides a tag named in excludeTags from the Add Label dropdown, keeping other tags visible', () => {
|
|
109
|
+
const unsubscribeTag = {
|
|
110
|
+
_id: 'unsubscribe-tag-id',
|
|
111
|
+
type: 'TAG',
|
|
112
|
+
definition: {
|
|
113
|
+
label: { en: 'Unsubscribe' },
|
|
114
|
+
value: 'unsubscribe',
|
|
115
|
+
subtags: [],
|
|
116
|
+
'tag-header': false,
|
|
117
|
+
supportedModules: [{ context: 'default', layout: 'email', mandatory: false }],
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
initializeTagList({
|
|
121
|
+
tags: [...TagListData.tags, unsubscribeTag],
|
|
122
|
+
excludeTags: ['unsubscribe'],
|
|
123
|
+
moduleFilterEnabled: false,
|
|
124
|
+
});
|
|
125
|
+
addLabelBtnAssertion();
|
|
126
|
+
expect(queryByText(/Unsubscribe/i)).not.toBeInTheDocument();
|
|
127
|
+
expect(getByText(/Optout/i)).toBeInTheDocument();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('does not filter any tag when excludeTags is not provided (default behavior unaffected)', () => {
|
|
131
|
+
const unsubscribeTag = {
|
|
132
|
+
_id: 'unsubscribe-tag-id',
|
|
133
|
+
type: 'TAG',
|
|
134
|
+
definition: {
|
|
135
|
+
label: { en: 'Unsubscribe' },
|
|
136
|
+
value: 'unsubscribe',
|
|
137
|
+
subtags: [],
|
|
138
|
+
'tag-header': false,
|
|
139
|
+
supportedModules: [{ context: 'default', layout: 'email', mandatory: false }],
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
initializeTagList({
|
|
143
|
+
tags: [...TagListData.tags, unsubscribeTag],
|
|
144
|
+
moduleFilterEnabled: false,
|
|
145
|
+
});
|
|
146
|
+
addLabelBtnAssertion();
|
|
147
|
+
expect(getByText(/Unsubscribe/i)).toBeInTheDocument();
|
|
148
|
+
});
|
|
149
|
+
|
|
108
150
|
it('calls transformCouponTags when selectedOfferDetails and coupon tags are present', () => {
|
|
109
151
|
const spy = jest.spyOn(TagList.prototype, 'transformCouponTags');
|
|
110
152
|
initializeTagList({
|
|
@@ -253,6 +253,7 @@ const duplicateEnum = {
|
|
|
253
253
|
line: "lineActions",
|
|
254
254
|
[VIBER_CHANNEL]: "viberActions",
|
|
255
255
|
[FACEBOOK_CHANNEL]: "facebookActions",
|
|
256
|
+
[RCS_LOWERCASE]: "rcsActions",
|
|
256
257
|
};
|
|
257
258
|
export class Templates extends React.Component { // eslint-disable-line react/prefer-stateless-function
|
|
258
259
|
constructor(props) {
|
|
@@ -1788,9 +1789,6 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
1788
1789
|
}
|
|
1789
1790
|
}
|
|
1790
1791
|
creativesParams.type = this.state.channel.toUpperCase();
|
|
1791
|
-
if (routeParams.openTestAndPreview) {
|
|
1792
|
-
creativesParams.openTestAndPreview = true;
|
|
1793
|
-
}
|
|
1794
1792
|
}
|
|
1795
1793
|
}
|
|
1796
1794
|
}
|
|
@@ -2279,9 +2277,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
2279
2277
|
onClick={() => {
|
|
2280
2278
|
if ([ZALO, WHATSAPP, RCS].includes(this.state.channel.toUpperCase()) || (isTraiDltFeature && this.state.channel.toUpperCase() === SMS)) {
|
|
2281
2279
|
handlers.handleEditClick(undefined, template, undefined, undefined, {
|
|
2282
|
-
account: this.state.selectedAccount
|
|
2283
|
-
// Test panel fills unmapped variables for these; SMS DLT still needs Overview.
|
|
2284
|
-
openTestAndPreview: [RCS, WHATSAPP, ZALO].includes(this.state.channel.toUpperCase()),
|
|
2280
|
+
account: this.state.selectedAccount
|
|
2285
2281
|
})
|
|
2286
2282
|
} else {
|
|
2287
2283
|
this.handleTestAndPreviewFromListing(template)
|
|
@@ -2292,7 +2288,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
2292
2288
|
</CapMenu.Item>
|
|
2293
2289
|
)}
|
|
2294
2290
|
{/* Duplicate menu item */}
|
|
2295
|
-
{![WECHAT, WHATSAPP, ZALO
|
|
2291
|
+
{![WECHAT, WHATSAPP, ZALO].includes(
|
|
2296
2292
|
this.state.channel.toUpperCase()
|
|
2297
2293
|
) &&
|
|
2298
2294
|
!isTraiDltFeature && !template.isArchived && this.gatedMenuItem({
|
|
@@ -3703,7 +3699,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
3703
3699
|
};
|
|
3704
3700
|
this.getAllTemplates({params, resetPage: true});
|
|
3705
3701
|
});
|
|
3706
|
-
} else if ([
|
|
3702
|
+
} else if (["sms", "line", VIBER_CHANNEL, FACEBOOK_CHANNEL, RCS_LOWERCASE].includes(this.state.channel.toLowerCase())) {
|
|
3707
3703
|
this.commonDuplicate(duplicateEnum[this.state.channel.toLowerCase()], duplicateObj);
|
|
3708
3704
|
}
|
|
3709
3705
|
}
|
|
@@ -3862,7 +3858,6 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
3862
3858
|
this.setState({routeParams: {
|
|
3863
3859
|
pathname: pathName,
|
|
3864
3860
|
query: getQuery,
|
|
3865
|
-
...(options?.openTestAndPreview && { openTestAndPreview: true }),
|
|
3866
3861
|
}});
|
|
3867
3862
|
}
|
|
3868
3863
|
}
|
|
@@ -5,7 +5,6 @@ import { Templates } from '../index';
|
|
|
5
5
|
import mockdata from '../../mockdata';
|
|
6
6
|
import { rcsTemplates, senderDetailsRcs } from './mockData';
|
|
7
7
|
import { exportEmailTemplateHtml } from '../../../utils/downloadFile';
|
|
8
|
-
import { PERMISSIONS } from '../../../utils/permissions';
|
|
9
8
|
|
|
10
9
|
const { whatsappTemplates: TemplatesProp, getCreativesParamsOutput, zaloTemplates: ZaloTemplateProp } = mockdata;
|
|
11
10
|
|
|
@@ -28,41 +27,6 @@ jest.mock('../../CreativesContainer', () => ({
|
|
|
28
27
|
),
|
|
29
28
|
}));
|
|
30
29
|
|
|
31
|
-
// Recursively walks a plain-object/array/React-element tree (as returned by
|
|
32
|
-
// getTemplateDataForGrid, which is never mounted here) looking for a node whose
|
|
33
|
-
// className includes the given substring - robust against the exact nesting of
|
|
34
|
-
// CapDropdown > CapMenu > CapMenu.Item without hardcoding array indices.
|
|
35
|
-
function findByClassName(node, className, seen = new Set()) {
|
|
36
|
-
if (!node || typeof node !== 'object') return null;
|
|
37
|
-
if (seen.has(node)) return null;
|
|
38
|
-
seen.add(node);
|
|
39
|
-
if (Array.isArray(node)) {
|
|
40
|
-
for (let i = 0; i < node.length; i += 1) {
|
|
41
|
-
const found = findByClassName(node[i], className, seen);
|
|
42
|
-
if (found) return found;
|
|
43
|
-
}
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
// React element (has .props) - check its className, then search its own prop values.
|
|
47
|
-
if (node.props) {
|
|
48
|
-
if (typeof node.props.className === 'string' && node.props.className.includes(className)) {
|
|
49
|
-
return node;
|
|
50
|
-
}
|
|
51
|
-
return findByClassName(node.props, className, seen);
|
|
52
|
-
}
|
|
53
|
-
// Plain object (e.g. a getTemplateDataForGrid card-data entry: {key, title, extra, hoverOption, ...})
|
|
54
|
-
// or a React element's .props object - search every own value.
|
|
55
|
-
const values = Object.values(node);
|
|
56
|
-
for (let i = 0; i < values.length; i += 1) {
|
|
57
|
-
const value = values[i];
|
|
58
|
-
if (value && typeof value === 'object') {
|
|
59
|
-
const found = findByClassName(value, className, seen);
|
|
60
|
-
if (found) return found;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
30
|
describe('Test Templates container', () => {
|
|
67
31
|
const getWeCrmAccounts = jest.fn();
|
|
68
32
|
const setChannelAccount = jest.fn();
|
|
@@ -307,6 +271,41 @@ describe('Test Templates container', () => {
|
|
|
307
271
|
describe('Export HTML (EMAIL channel)', () => {
|
|
308
272
|
const emailTemplate = { _id: 'email_1', name: 'My Email Template', versions: { base: {} } };
|
|
309
273
|
|
|
274
|
+
// Recursively walks a plain-object/array/React-element tree (as returned by
|
|
275
|
+
// getTemplateDataForGrid, which is never mounted here) looking for a node whose
|
|
276
|
+
// className includes the given substring - robust against the exact nesting of
|
|
277
|
+
// CapDropdown > CapMenu > CapMenu.Item without hardcoding array indices.
|
|
278
|
+
function findByClassName(node, className, seen = new Set()) {
|
|
279
|
+
if (!node || typeof node !== 'object') return null;
|
|
280
|
+
if (seen.has(node)) return null;
|
|
281
|
+
seen.add(node);
|
|
282
|
+
if (Array.isArray(node)) {
|
|
283
|
+
for (let i = 0; i < node.length; i += 1) {
|
|
284
|
+
const found = findByClassName(node[i], className, seen);
|
|
285
|
+
if (found) return found;
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
// React element (has .props) - check its className, then search its own prop values.
|
|
290
|
+
if (node.props) {
|
|
291
|
+
if (typeof node.props.className === 'string' && node.props.className.includes(className)) {
|
|
292
|
+
return node;
|
|
293
|
+
}
|
|
294
|
+
return findByClassName(node.props, className, seen);
|
|
295
|
+
}
|
|
296
|
+
// Plain object (e.g. a getTemplateDataForGrid card-data entry: {key, title, extra, hoverOption, ...})
|
|
297
|
+
// or a React element's .props object - search every own value.
|
|
298
|
+
const values = Object.values(node);
|
|
299
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
300
|
+
const value = values[i];
|
|
301
|
+
if (value && typeof value === 'object') {
|
|
302
|
+
const found = findByClassName(value, className, seen);
|
|
303
|
+
if (found) return found;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
310
309
|
beforeEach(() => {
|
|
311
310
|
jest.clearAllMocks();
|
|
312
311
|
// jest config sets resetMocks: true, wiping the default implementation baked into
|
|
@@ -427,75 +426,4 @@ describe('Test Templates container', () => {
|
|
|
427
426
|
expect(findByClassName(cardDataList, 'export-html-email')).toBeNull();
|
|
428
427
|
});
|
|
429
428
|
});
|
|
430
|
-
|
|
431
|
-
describe('RCS listing menu', () => {
|
|
432
|
-
const approvedRcsTemplate = {
|
|
433
|
-
_id: 'rcs_1',
|
|
434
|
-
name: 'RCS Template',
|
|
435
|
-
versions: { base: { content: { RCS: { rcsContent: { cardContent: [{ Status: 'approved' }] } } } } },
|
|
436
|
-
};
|
|
437
|
-
const handlers = { handlePreviewClick: jest.fn(), handleEditClick: jest.fn() };
|
|
438
|
-
const getCardData = () => renderedComponent.instance().getTemplateDataForGrid({
|
|
439
|
-
templates: [approvedRcsTemplate],
|
|
440
|
-
handlers,
|
|
441
|
-
filterContent: null,
|
|
442
|
-
channel: 'rcs',
|
|
443
|
-
isLoading: false,
|
|
444
|
-
isInitialLoading: false,
|
|
445
|
-
loadingTip: '',
|
|
446
|
-
previewTemplateId: null,
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
beforeEach(() => {
|
|
450
|
-
RenderFunctionFor('rcs', true);
|
|
451
|
-
renderedComponent.setProps({
|
|
452
|
-
isFullMode: true,
|
|
453
|
-
isDltFromRcs: false,
|
|
454
|
-
Global: { user: { accessiblePermissions: [PERMISSIONS.CREATIVE_EDIT] } },
|
|
455
|
-
actions: { ...renderedComponent.props().actions, resetTemplateData: jest.fn() },
|
|
456
|
-
});
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
it('does not render the Duplicate menu item for RCS', () => {
|
|
460
|
-
expect(findByClassName(getCardData(), 'duplicate-rcs')).toBeNull();
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
it('"Preview and test" opens the editor with openTestAndPreview', () => {
|
|
464
|
-
const menuItem = findByClassName(getCardData(), 'test-and-preview-rcs');
|
|
465
|
-
expect(menuItem).not.toBeNull();
|
|
466
|
-
|
|
467
|
-
menuItem.props.onClick();
|
|
468
|
-
expect(handlers.handleEditClick).toHaveBeenCalledWith(
|
|
469
|
-
undefined,
|
|
470
|
-
approvedRcsTemplate,
|
|
471
|
-
undefined,
|
|
472
|
-
undefined,
|
|
473
|
-
expect.objectContaining({ openTestAndPreview: true }),
|
|
474
|
-
);
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
it('handleEditClick keeps openTestAndPreview in routeParams', () => {
|
|
478
|
-
renderedComponent.instance().handleEditClick(undefined, approvedRcsTemplate, undefined, undefined, {
|
|
479
|
-
openTestAndPreview: true,
|
|
480
|
-
});
|
|
481
|
-
expect(renderedComponent.state('routeParams')).toEqual(
|
|
482
|
-
expect.objectContaining({ pathname: '/rcs/edit/rcs_1/', openTestAndPreview: true }),
|
|
483
|
-
);
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
it('handleEditClick omits openTestAndPreview for Overview', () => {
|
|
487
|
-
renderedComponent.instance().handleEditClick(undefined, approvedRcsTemplate);
|
|
488
|
-
expect(renderedComponent.state('routeParams')).not.toHaveProperty('openTestAndPreview');
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
it('getCreativesParams forwards openTestAndPreview to the editor', () => {
|
|
492
|
-
renderedComponent.setState({ routeParams: { pathname: '/rcs/edit/rcs_1/', openTestAndPreview: true } });
|
|
493
|
-
expect(renderedComponent.instance().getCreativesParams()).toEqual(
|
|
494
|
-
expect.objectContaining({ mode: 'edit', _id: 'rcs_1', type: 'RCS', openTestAndPreview: true }),
|
|
495
|
-
);
|
|
496
|
-
|
|
497
|
-
renderedComponent.setState({ routeParams: { pathname: '/rcs/edit/rcs_1/' } });
|
|
498
|
-
expect(renderedComponent.instance().getCreativesParams()).not.toHaveProperty('openTestAndPreview');
|
|
499
|
-
});
|
|
500
|
-
});
|
|
501
429
|
});
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
margin-bottom: $CAP_SPACE_06;
|
|
13
13
|
}
|
|
14
14
|
.cap-custom-image-upload {
|
|
15
|
+
padding-top: $CAP_SPACE_20;
|
|
16
|
+
form {
|
|
17
|
+
padding: 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
.dragger-button.re-upload {
|
|
16
21
|
top: -11rem;
|
|
17
22
|
position: relative;
|
|
@@ -131,6 +136,32 @@
|
|
|
131
136
|
margin-top: $CAP_SPACE_16;
|
|
132
137
|
}
|
|
133
138
|
|
|
139
|
+
.viber-carousel-tab {
|
|
140
|
+
.ant-tabs-nav .ant-tabs-nav-wrap {
|
|
141
|
+
flex: none;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.ant-tabs-nav-list .ant-tabs-tab {
|
|
145
|
+
padding: $CAP_SPACE_08;
|
|
146
|
+
justify-content: center;
|
|
147
|
+
font-size: $FONT_SIZE_L;
|
|
148
|
+
|
|
149
|
+
+ .ant-tabs-tab {
|
|
150
|
+
margin-left: 0;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.ant-tabs-extra-content {
|
|
155
|
+
display: flex;
|
|
156
|
+
align-items: center;
|
|
157
|
+
|
|
158
|
+
.ant-divider-vertical {
|
|
159
|
+
height: $CAP_SPACE_24;
|
|
160
|
+
margin: 0 $CAP_SPACE_12;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
134
165
|
.viber-carousel-card {
|
|
135
166
|
border: 1px solid $CAP_G06;
|
|
136
167
|
border-radius: $CAP_SPACE_04;
|