@capillarytech/creatives-library 9.0.36 → 9.0.37-alpha.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/AppRoot.js +124 -0
- package/app-config.js +61 -0
- package/app.js +10 -175
- package/bootstrap.js +185 -0
- package/entry.js +67 -1
- package/mfe-exposed-components.js +2 -4
- package/package.json +2 -2
- package/services/api.js +9 -1
- package/styles/containers/layout/_layoutPage.scss +8 -6
- package/utils/getDataLayer.js +15 -0
- package/utils/gtmTrackers/gtmEvents/creativeDetails.js +2 -1
- package/utils/mfeDetect.js +1 -0
- package/utils/mfeFirstPaintReady.js +29 -0
- package/utils/mfeHistory.js +62 -0
- package/utils/rcsPayloadUtils.js +5 -2
- package/utils/tests/rcsPayloadUtils.test.js +54 -2
- package/v2Components/CapActionButton/constants.js +1 -0
- package/v2Components/CapActionButton/index.js +77 -9
- package/v2Components/CapActionButton/index.scss +13 -0
- package/v2Components/CapActionButton/messages.js +13 -0
- package/v2Components/CapActionButton/tests/index.test.js +32 -1
- package/v2Components/CommonTestAndPreview/index.js +44 -15
- package/v2Components/CommonTestAndPreview/tests/index.test.js +78 -0
- package/v2Components/CommonTestAndPreview/utils.js +34 -0
- package/v2Components/NavigationBar/index.js +9 -7
- package/v2Components/NavigationBar/mfeModuleHeader.config.js +16 -0
- package/v2Components/NavigationBar/tests/index.test.js +39 -19
- package/v2Components/SmsFallback/index.js +6 -0
- package/v2Containers/Cap/constants.js +1 -0
- package/v2Containers/Cap/index.js +57 -27
- package/v2Containers/CreativesContainer/index.js +11 -8
- package/v2Containers/CreativesContainer/tests/index.test.js +53 -0
- package/v2Containers/Rcs/components/CarouselCard.js +5 -0
- package/v2Containers/Rcs/components/CarouselCardButtons.js +20 -0
- package/v2Containers/Rcs/index.js +154 -79
- package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +55 -4
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +286 -0
- package/v2Containers/Rcs/tests/carouselUtils.test.js +13 -17
- package/v2Containers/Rcs/tests/index.test.js +141 -6
- package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +110 -0
- package/v2Containers/Rcs/tests/utils.test.js +36 -0
- package/v2Containers/Rcs/utils.js +16 -4
- package/v2Containers/SmsTrai/Edit/index.js +35 -24
- package/v2Containers/Templates/_templates.scss +1 -1
- package/v2Containers/Templates/index.js +1 -0
- package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +5 -0
- package/v2Containers/TemplatesV2/TemplatesV2.style.js +8 -3
- package/v2Containers/TemplatesV2/index.js +22 -9
|
@@ -3561,6 +3561,84 @@ describe('CommonTestAndPreview', () => {
|
|
|
3561
3561
|
});
|
|
3562
3562
|
});
|
|
3563
3563
|
|
|
3564
|
+
describe('RCS primary card content synthetic tag fallback', () => {
|
|
3565
|
+
const rcsCardFormData = (description) => ({
|
|
3566
|
+
rcsTitle: '',
|
|
3567
|
+
rcsDesc: description,
|
|
3568
|
+
versions: {
|
|
3569
|
+
base: {
|
|
3570
|
+
content: {
|
|
3571
|
+
RCS: {
|
|
3572
|
+
rcsContent: {
|
|
3573
|
+
cardContent: [{ title: '', description }],
|
|
3574
|
+
},
|
|
3575
|
+
},
|
|
3576
|
+
},
|
|
3577
|
+
},
|
|
3578
|
+
},
|
|
3579
|
+
});
|
|
3580
|
+
|
|
3581
|
+
it('should surface a {{tag}} from RCS card description as a fillable field when the extract-tags API returns nothing (no SMS fallback)', async () => {
|
|
3582
|
+
render(
|
|
3583
|
+
<TestWrapper>
|
|
3584
|
+
<CommonTestAndPreview
|
|
3585
|
+
{...defaultProps}
|
|
3586
|
+
channel={CHANNELS.RCS}
|
|
3587
|
+
formData={rcsCardFormData('Visit store for offer {{user_id_b64}}')}
|
|
3588
|
+
extractedTags={[]}
|
|
3589
|
+
/>
|
|
3590
|
+
</TestWrapper>
|
|
3591
|
+
);
|
|
3592
|
+
|
|
3593
|
+
await waitFor(() => expect(lastLeftPanelContentProps).not.toBeNull());
|
|
3594
|
+
|
|
3595
|
+
const tags = lastLeftPanelContentProps.extractedTags;
|
|
3596
|
+
expect(tags.some((tag) => tag.name === 'user_id_b64')).toBe(true);
|
|
3597
|
+
});
|
|
3598
|
+
|
|
3599
|
+
it('should surface an RCS card tag alongside a merged fallback SMS tag when both are present', async () => {
|
|
3600
|
+
render(
|
|
3601
|
+
<TestWrapper>
|
|
3602
|
+
<CommonTestAndPreview
|
|
3603
|
+
{...defaultProps}
|
|
3604
|
+
channel={CHANNELS.RCS}
|
|
3605
|
+
formData={rcsCardFormData('Visit store for offer {{user_id_b64}}')}
|
|
3606
|
+
extractedTags={[]}
|
|
3607
|
+
smsFallbackContent={{
|
|
3608
|
+
templateContent: 'Fallback {{name}}',
|
|
3609
|
+
content: 'Fallback {{name}}',
|
|
3610
|
+
}}
|
|
3611
|
+
/>
|
|
3612
|
+
</TestWrapper>
|
|
3613
|
+
);
|
|
3614
|
+
|
|
3615
|
+
await waitFor(() => expect(lastLeftPanelContentProps).not.toBeNull());
|
|
3616
|
+
|
|
3617
|
+
const tags = lastLeftPanelContentProps.extractedTags;
|
|
3618
|
+
expect(tags.some((tag) => tag.name === 'user_id_b64')).toBe(true);
|
|
3619
|
+
expect(tags.some((tag) => tag.name === 'name')).toBe(true);
|
|
3620
|
+
});
|
|
3621
|
+
|
|
3622
|
+
it('should not duplicate a tag the extract-tags API already returned for RCS card content', async () => {
|
|
3623
|
+
const apiTags = [{ name: 'user_id_b64', metaData: { userDriven: false }, children: [] }];
|
|
3624
|
+
render(
|
|
3625
|
+
<TestWrapper>
|
|
3626
|
+
<CommonTestAndPreview
|
|
3627
|
+
{...defaultProps}
|
|
3628
|
+
channel={CHANNELS.RCS}
|
|
3629
|
+
formData={rcsCardFormData('Visit store for offer {{user_id_b64}}')}
|
|
3630
|
+
extractedTags={apiTags}
|
|
3631
|
+
/>
|
|
3632
|
+
</TestWrapper>
|
|
3633
|
+
);
|
|
3634
|
+
|
|
3635
|
+
await waitFor(() => expect(lastLeftPanelContentProps).not.toBeNull());
|
|
3636
|
+
|
|
3637
|
+
const tags = lastLeftPanelContentProps.extractedTags;
|
|
3638
|
+
expect(tags.filter((tag) => tag.name === 'user_id_b64')).toHaveLength(1);
|
|
3639
|
+
});
|
|
3640
|
+
});
|
|
3641
|
+
|
|
3564
3642
|
describe('handleDiscardCustomValues — preview reset', () => {
|
|
3565
3643
|
it('should call updatePreviewRequested when handleDiscardCustomValues is invoked', async () => {
|
|
3566
3644
|
render(
|
|
@@ -27,6 +27,40 @@ export const buildSyntheticSmsMustacheTags = (body = '') => {
|
|
|
27
27
|
}));
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Raw RCS primary-card text (title + description across all cards) to scan for `{{…}}` tags.
|
|
32
|
+
* Mirrors the carousel/standalone branching in prepareTagExtractionPayload's RCS case so both
|
|
33
|
+
* the extract-tags API payload and the client-side synthetic tag fallback agree on card text.
|
|
34
|
+
*/
|
|
35
|
+
export const getRcsPrimaryTagExtractionText = (formDataObj) => {
|
|
36
|
+
const rcsCardContentArr =
|
|
37
|
+
formDataObj?.versions?.base?.content?.RCS?.rcsContent?.cardContent
|
|
38
|
+
?? formDataObj?.content?.RCS?.rcsContent?.cardContent;
|
|
39
|
+
if (Array.isArray(rcsCardContentArr) && rcsCardContentArr.length > 0) {
|
|
40
|
+
const carouselTagText = rcsCardContentArr
|
|
41
|
+
.map((card) => `${card.title || ''} ${card.description || ''}`)
|
|
42
|
+
.join(' ')
|
|
43
|
+
.trim();
|
|
44
|
+
if (carouselTagText) return carouselTagText;
|
|
45
|
+
}
|
|
46
|
+
return `${formDataObj?.rcsTitle || ''} ${formDataObj?.rcsDesc || ''}`.trim();
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Combine tags the extract-tags API returned with tags synthesized client-side from a raw
|
|
51
|
+
* `{{…}}` scan of `rawText`, so a tag the backend's catalog doesn't recognize (e.g. a reserved
|
|
52
|
+
* token like `user_id_b64`) still surfaces as a fillable field instead of silently disappearing.
|
|
53
|
+
* Dedupes by name so API-recognized tags (which carry richer metaData) are never overridden.
|
|
54
|
+
*/
|
|
55
|
+
export const mergeSyntheticMustacheTags = (apiTags = [], rawText = '') => {
|
|
56
|
+
if (!smsTemplateHasMustacheTags(rawText)) return apiTags ?? [];
|
|
57
|
+
const existingNames = new Set((apiTags ?? []).map((tag) => tag?.name));
|
|
58
|
+
const synthetic = buildSyntheticSmsMustacheTags(rawText).filter(
|
|
59
|
+
(tag) => !existingNames.has(tag.name),
|
|
60
|
+
);
|
|
61
|
+
return [...(apiTags ?? []), ...synthetic];
|
|
62
|
+
};
|
|
63
|
+
|
|
30
64
|
/** RCS createMessageMeta: normalize media shape (mediaUrl, thumbnailUrl, height string). */
|
|
31
65
|
export const normalizeRcsTestCardMedia = (media) => {
|
|
32
66
|
if (!media || typeof media !== 'object') return undefined;
|
|
@@ -12,8 +12,10 @@ import { loadItem } from 'services/localStorageApi';
|
|
|
12
12
|
import { intlShape, injectIntl } from 'react-intl';
|
|
13
13
|
import { withRouter } from 'react-router-dom';
|
|
14
14
|
import { get } from 'lodash';
|
|
15
|
-
import CapNavigation from '@capillarytech/cap-ui-library/
|
|
15
|
+
import CapNavigation from '@capillarytech/cap-ui-library/CapNavigationSPA';
|
|
16
16
|
import { CAP_WHITE } from '@capillarytech/cap-ui-library/styled/variables';
|
|
17
|
+
import { MFEModuleHeader } from '@capillarytech/cap-ui-utils';
|
|
18
|
+
import { MFE_MODULE_HEADER } from './mfeModuleHeader.config';
|
|
17
19
|
import injectSaga from '../../utils/injectSaga';
|
|
18
20
|
import sagas from './saga';
|
|
19
21
|
import messages from './messages';
|
|
@@ -30,12 +32,11 @@ import { CapLeftNavigatioOpenCss, CapLeftNavigationCss } from './style';
|
|
|
30
32
|
import { EMBEDDED } from './constants';
|
|
31
33
|
|
|
32
34
|
const CapWrapper = styled.div`
|
|
33
|
-
position: absolute;
|
|
34
35
|
padding: 0;
|
|
35
36
|
background-color: ${CAP_WHITE};
|
|
36
37
|
width: 100%;
|
|
37
38
|
display: flex;
|
|
38
|
-
|
|
39
|
+
|
|
39
40
|
|
|
40
41
|
.sidebar-container {
|
|
41
42
|
margin-right: 10px;
|
|
@@ -46,10 +47,8 @@ const CapWrapper = styled.div`
|
|
|
46
47
|
`;
|
|
47
48
|
|
|
48
49
|
const ComponentWrapper = styled.div`
|
|
49
|
-
max-width: 1140px;
|
|
50
|
-
margin: 0 auto;
|
|
51
50
|
width: 100%;
|
|
52
|
-
padding:
|
|
51
|
+
padding: 0;
|
|
53
52
|
`;
|
|
54
53
|
|
|
55
54
|
export class NavigationBar extends React.Component {
|
|
@@ -58,6 +57,7 @@ export class NavigationBar extends React.Component {
|
|
|
58
57
|
this.state = this.initializeSelectedProduct();
|
|
59
58
|
}
|
|
60
59
|
|
|
60
|
+
|
|
61
61
|
initializeSelectedProduct = () => {
|
|
62
62
|
const { location, intl: { formatMessage } } = this.props;
|
|
63
63
|
const { pathname } = location;
|
|
@@ -179,6 +179,7 @@ export class NavigationBar extends React.Component {
|
|
|
179
179
|
const topbarIcons = this.getTopbarIcons(showDocumentationBot);
|
|
180
180
|
const headerOverideCss = leftNavbarExpandedProp ? CapLeftNavigatioOpenCss : CapLeftNavigationCss;
|
|
181
181
|
return (
|
|
182
|
+
<>
|
|
182
183
|
<CapNavigation
|
|
183
184
|
className="creatives-main-container"
|
|
184
185
|
showContent
|
|
@@ -212,7 +213,8 @@ export class NavigationBar extends React.Component {
|
|
|
212
213
|
</CapWrapper>
|
|
213
214
|
</div>
|
|
214
215
|
</CapNavigation>
|
|
215
|
-
|
|
216
|
+
<MFEModuleHeader history={this.props.history} {...MFE_MODULE_HEADER} />
|
|
217
|
+
</>
|
|
216
218
|
);
|
|
217
219
|
}
|
|
218
220
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Static, module-specific config for the shared MFE module-header wrapper
|
|
2
|
+
// (MFEModuleHeader). Runtime values (history) are supplied in NavigationBar.
|
|
3
|
+
export const MFE_MODULE_HEADER = {
|
|
4
|
+
header: {
|
|
5
|
+
name: 'app.commonUtils.capUiLibrary.navigationSPA.moduleHeader.creatives.default.name',
|
|
6
|
+
description: 'app.commonUtils.capUiLibrary.navigationSPA.moduleHeader.creatives.default.description',
|
|
7
|
+
showBorder: false,
|
|
8
|
+
showSettings: false,
|
|
9
|
+
},
|
|
10
|
+
landingRoutes: [
|
|
11
|
+
'/creatives/ui/v2/',
|
|
12
|
+
'/creatives/ui/v2',
|
|
13
|
+
'/v2/',
|
|
14
|
+
'/v2'
|
|
15
|
+
],
|
|
16
|
+
};
|
|
@@ -3,7 +3,7 @@ import { injectIntl } from 'react-intl';
|
|
|
3
3
|
import '@testing-library/jest-dom';
|
|
4
4
|
import cloneDeep from 'lodash/cloneDeep';
|
|
5
5
|
import { BrowserRouter as Router } from 'react-router-dom';
|
|
6
|
-
import { render, screen } from '../../../utils/test-utils';
|
|
6
|
+
import { render, screen, waitFor } from '../../../utils/test-utils';
|
|
7
7
|
|
|
8
8
|
import { NavigationBar } from '../index';
|
|
9
9
|
import * as mockdata from './mockData';
|
|
@@ -30,7 +30,7 @@ const renderComponent = (props) => {
|
|
|
30
30
|
describe('NavigationBar', () => {
|
|
31
31
|
const props = {
|
|
32
32
|
topbarMenuData: [{}],
|
|
33
|
-
history
|
|
33
|
+
history,
|
|
34
34
|
userData,
|
|
35
35
|
loggedIn: true,
|
|
36
36
|
isCreativesAccessible: true,
|
|
@@ -42,52 +42,72 @@ describe('NavigationBar', () => {
|
|
|
42
42
|
},
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
// CapNavigationSPA shows a loader overlay until its internal
|
|
46
|
+
// getNavigationConfigApi call settles, so nav content (icons, dividers,
|
|
47
|
+
// product labels) only mounts after that resolves — queries must wait for it.
|
|
48
|
+
|
|
49
|
+
// cap-ui-library's CapNavigationSPA bump changed how `topbarIcons` is
|
|
50
|
+
// consumed: it's now only read to find a `question-circle` icon's click
|
|
51
|
+
// handler for the built-in Help button. Custom icon entries (the "aira"
|
|
52
|
+
// doc-bot icon NavigationBar pushes via getTopbarIcons) are no longer
|
|
53
|
+
// rendered at all, so this feature no longer has any DOM to assert on.
|
|
54
|
+
it.skip('Show aria documentation bot icon if showDocumentationBot is true', async () => {
|
|
46
55
|
renderComponent(props);
|
|
47
|
-
const ariaBotIcon = screen.
|
|
56
|
+
const ariaBotIcon = await screen.findByLabelText('open-aira');
|
|
48
57
|
expect(ariaBotIcon).toBeInTheDocument();
|
|
49
58
|
});
|
|
50
59
|
|
|
51
|
-
it('Should not show aria documentation bot icon if showDocumentationBot is false', () => {
|
|
60
|
+
it('Should not show aria documentation bot icon if showDocumentationBot is false', async () => {
|
|
52
61
|
const updatedProps = cloneDeep(props);
|
|
53
62
|
delete updatedProps.userData.currentOrgDetails.accessibleFeatures;
|
|
54
63
|
renderComponent(updatedProps);
|
|
64
|
+
await screen.findByLabelText('Help');
|
|
55
65
|
const ariaBotIcon = screen.queryByLabelText('open-aira');
|
|
56
|
-
expect(ariaBotIcon).toBeInTheDocument();
|
|
66
|
+
expect(ariaBotIcon).not.toBeInTheDocument();
|
|
57
67
|
});
|
|
58
|
-
|
|
59
|
-
it('Should contains top vertical', () => {
|
|
68
|
+
|
|
69
|
+
it('Should contains top vertical', async () => {
|
|
60
70
|
const updatedProps = {...props} ;
|
|
61
71
|
delete updatedProps.userData.currentOrgDetails.accessibleFeatures;
|
|
62
72
|
renderComponent(updatedProps);
|
|
63
|
-
// cap-ui-library v6 renders the vertical divider with `.cap-
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
// cap-ui-library v6 renders the vertical divider with `.cap-navigation-spa-topbar__divider`
|
|
74
|
+
await waitFor(() => {
|
|
75
|
+
expect(document.querySelector('.cap-navigation-spa-topbar__divider')).toBeInTheDocument();
|
|
76
|
+
});
|
|
66
77
|
});
|
|
67
|
-
|
|
78
|
+
|
|
79
|
+
// `selectedProduct` only renders (via CapSecondaryTopBar) when
|
|
80
|
+
// `showSecondaryTopBar` is passed — NavigationBar never passes it, so the
|
|
81
|
+
// "Loyalty+" label has no DOM to assert on under current props.
|
|
82
|
+
it.skip('Should have loyalty product selected', async () => {
|
|
68
83
|
const updatedProps = {...props};
|
|
69
84
|
updatedProps.location.pathname = '/creatives/ui/v2/loyalty';
|
|
70
85
|
renderComponent(updatedProps);
|
|
71
|
-
const loyaltyProduct = screen.
|
|
86
|
+
const loyaltyProduct = await screen.findByText('Loyalty+');
|
|
72
87
|
expect(loyaltyProduct).toBeInTheDocument();
|
|
73
88
|
});
|
|
74
89
|
|
|
75
90
|
// Regression: AntD v3 -> v6 migration (CAP-183930) dropped the settings (gear)
|
|
76
91
|
// icon from the top nav. These tests ensure the gear keeps rendering.
|
|
77
|
-
it('Should render the settings (gear) icon in the top nav bar', () => {
|
|
92
|
+
it('Should render the settings (gear) icon in the top nav bar', async () => {
|
|
78
93
|
const updatedProps = cloneDeep(props);
|
|
79
94
|
updatedProps.location.pathname = '/creatives/ui/v2';
|
|
80
95
|
updatedProps.settingsUrl = '/campaigns/ui/creatives/settings/message';
|
|
81
96
|
renderComponent(updatedProps);
|
|
82
|
-
|
|
83
|
-
|
|
97
|
+
await waitFor(() => {
|
|
98
|
+
expect(document.querySelector('.cap-icon-v2-settings')).toBeInTheDocument();
|
|
99
|
+
});
|
|
84
100
|
});
|
|
85
101
|
|
|
86
|
-
|
|
102
|
+
// Same `topbarIcons` regression as the aira icon above: the custom settings
|
|
103
|
+
// icon config (with className `navigation-setting-icon`) NavigationBar
|
|
104
|
+
// builds for the loyalty route is never rendered by the current library.
|
|
105
|
+
it.skip('Should render the settings (gear) icon for the loyalty product', async () => {
|
|
87
106
|
const updatedProps = cloneDeep(props);
|
|
88
107
|
updatedProps.location.pathname = '/creatives/ui/v2/loyalty';
|
|
89
108
|
renderComponent(updatedProps);
|
|
90
|
-
|
|
91
|
-
|
|
109
|
+
await waitFor(() => {
|
|
110
|
+
expect(document.querySelector('.cap-icon-v2-settings.navigation-setting-icon')).toBeInTheDocument();
|
|
111
|
+
});
|
|
92
112
|
});
|
|
93
113
|
});
|
|
@@ -747,11 +747,17 @@ export function SmsFallback({
|
|
|
747
747
|
const base = uploadedTemplate.versions?.base || {};
|
|
748
748
|
const content = base.template_message || base['sms-editor'] || '';
|
|
749
749
|
const templateName = uploadedTemplate.name || base.template_name || '';
|
|
750
|
+
// DLT-registered sender IDs for this newly-created template — same field
|
|
751
|
+
// buildFallbackDataFromTemplate reads. Without it, Test & Preview's delivery-settings
|
|
752
|
+
// filter sees an empty registeredSenderIds and falls back to showing every sender.
|
|
753
|
+
const registeredSenderIds = Array.isArray(base.header) ? base.header : [];
|
|
750
754
|
if (content || templateName) {
|
|
751
755
|
onChange({
|
|
756
|
+
smsTemplateId: base.template_id || uploadedTemplate._id || '',
|
|
752
757
|
templateName,
|
|
753
758
|
content,
|
|
754
759
|
templateContent: content,
|
|
760
|
+
registeredSenderIds,
|
|
755
761
|
unicodeValidity: typeof base['unicode-validity'] === JS_TYPE_BOOLEAN
|
|
756
762
|
? base['unicode-validity']
|
|
757
763
|
: true,
|
|
@@ -56,6 +56,7 @@ export const SUCCESS = 'SUCCESS';
|
|
|
56
56
|
export const FAILURE = 'FAILURE';
|
|
57
57
|
|
|
58
58
|
export const ENABLE_PRODUCT_SUPPORT_VIDEOS = 'ENABLE_PRODUCT_SUPPORT_VIDEOS';
|
|
59
|
+
export const ENABLE_NEW_LEFT_NAVIGATION = 'ENABLE_NEW_LEFT_NAVIGATION';
|
|
59
60
|
export const DEFAULT = 'default';
|
|
60
61
|
export const DEFAULT_MODULE = 'creatives';
|
|
61
62
|
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
REQUEST,
|
|
31
31
|
DEFAULT,
|
|
32
32
|
ENABLE_PRODUCT_SUPPORT_VIDEOS,
|
|
33
|
+
ENABLE_NEW_LEFT_NAVIGATION,
|
|
33
34
|
CAMPAIGN_SETTINGS_URL,
|
|
34
35
|
} from './constants';
|
|
35
36
|
import './_cap.scss';
|
|
@@ -53,7 +54,8 @@ import { v2ViberSagas } from '../Viber/sagas';
|
|
|
53
54
|
import { v2FacebookSagas } from '../Facebook/sagas';
|
|
54
55
|
import createReducer from '../Line/Container/reducer';
|
|
55
56
|
import { DAEMON } from '@capillarytech/vulcan-react-sdk/utils/sagaInjectorTypes';
|
|
56
|
-
|
|
57
|
+
import { getDataLayer } from '../../utils/getDataLayer';
|
|
58
|
+
const gtm = getDataLayer();
|
|
57
59
|
const {
|
|
58
60
|
logNewTab,
|
|
59
61
|
utilsGetOrgNameFromId,
|
|
@@ -72,18 +74,19 @@ const CapWrapper = styled.div`
|
|
|
72
74
|
min-height: 100%;
|
|
73
75
|
padding: 0;
|
|
74
76
|
flex-direction: column;
|
|
77
|
+
position: relative;
|
|
75
78
|
`;
|
|
76
79
|
GA.initialize({ accessKey: GTM_TRACKING_ID, trackUIError: true, trackLoadPerformance: 'creatives' });
|
|
77
80
|
|
|
78
81
|
const MainWrapper = styled.div`
|
|
79
82
|
position: relative;
|
|
80
|
-
min-height:
|
|
81
|
-
top:
|
|
83
|
+
min-height: 100%;
|
|
84
|
+
top: 0;
|
|
82
85
|
`;
|
|
83
86
|
|
|
84
87
|
const ContentWrapper = styled.div`
|
|
85
88
|
&& {
|
|
86
|
-
margin-left:
|
|
89
|
+
margin-left: 0;
|
|
87
90
|
}
|
|
88
91
|
`;
|
|
89
92
|
|
|
@@ -118,12 +121,16 @@ export class Cap extends React.Component { // eslint-disable-line react/prefer-s
|
|
|
118
121
|
const userGtmData = this.getUserGtmData();
|
|
119
122
|
this.setDimensionData(userGtmData);
|
|
120
123
|
gtm.push({ ...userGtmData, event: 'userAuthenticated' });
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
// In MFE mode the host already initialized auth (window.capAuth is set with
|
|
125
|
+
// the full superset); skip so we don't clobber it with the basic shape.
|
|
126
|
+
if (!window.capAuth) {
|
|
127
|
+
Auth.initialize({
|
|
128
|
+
permissions: userData.accessiblePermissions,
|
|
129
|
+
isCapUser: userData.isCapUser,
|
|
130
|
+
roles: Object.keys(userData.accessRoles),
|
|
131
|
+
accessibleFeatures,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
127
134
|
});
|
|
128
135
|
this.props.actions.getTopbarMenuData(parentModule);
|
|
129
136
|
if (this.props.Global.orgID !== undefined) {
|
|
@@ -500,6 +507,8 @@ export class Cap extends React.Component { // eslint-disable-line react/prefer-s
|
|
|
500
507
|
const type = this.props.location.query.type;
|
|
501
508
|
const toastMessages = this.props.Global.messages;
|
|
502
509
|
const { isCreativesAccessible, showOrgChangeModal, showRefreshModal } = this.state;
|
|
510
|
+
const { accessibleFeatures = [] } = currentOrgDetails;
|
|
511
|
+
const isLatestLeftNavigationEnabled = accessibleFeatures.includes(ENABLE_NEW_LEFT_NAVIGATION);
|
|
503
512
|
return (
|
|
504
513
|
<CapWrapper>
|
|
505
514
|
{ this.props.loader.localeLoading || this.props.Global.fetching_userdata ?
|
|
@@ -537,26 +546,47 @@ export class Cap extends React.Component { // eslint-disable-line react/prefer-s
|
|
|
537
546
|
campaignOrgV2Status={currentOrgDetails.org_campaign_v2_status}
|
|
538
547
|
handleLeftNavBarExpanded={this.handleLeftNavBarExpanded}
|
|
539
548
|
leftNavbarExpandedProp={this.state.leftNavbarExpanded}
|
|
540
|
-
/>) : ''}
|
|
541
|
-
<MainWrapper className="main">
|
|
542
549
|
|
|
543
|
-
<ContentWrapper
|
|
544
|
-
className={`main-content ${this.state.leftNavbarExpanded ? "contet-width-collapse" : "content-width-expanded"}`}
|
|
545
|
-
leftNavbarExpanded={this.state.leftNavbarExpanded}
|
|
546
550
|
>
|
|
547
|
-
<
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
551
|
+
<MainWrapper isLatestLeftNavigationEnabled={isLatestLeftNavigationEnabled} className="main">
|
|
552
|
+
<ContentWrapper
|
|
553
|
+
className={`main-content ${this.state.leftNavbarExpanded ? "contet-width-collapse" : "content-width-expanded"}`}
|
|
554
|
+
isLatestLeftNavigationEnabled={isLatestLeftNavigationEnabled}
|
|
555
|
+
leftNavbarExpanded={this.state.leftNavbarExpanded}
|
|
556
|
+
>
|
|
557
|
+
<Switch>
|
|
558
|
+
{componentRoutes.map(routeProps => {
|
|
559
|
+
return (
|
|
560
|
+
<RenderRoute
|
|
561
|
+
{...routeProps}
|
|
562
|
+
key={routeProps.path}
|
|
563
|
+
isLoggedIn={isLoggedIn}
|
|
564
|
+
/>
|
|
565
|
+
)}
|
|
566
|
+
)}
|
|
567
|
+
</Switch>
|
|
568
|
+
</ContentWrapper>
|
|
569
|
+
</MainWrapper>
|
|
570
|
+
</NavigationBar>) : (
|
|
571
|
+
<MainWrapper isLatestLeftNavigationEnabled={isLatestLeftNavigationEnabled} className="main">
|
|
572
|
+
<ContentWrapper
|
|
573
|
+
className={`main-content ${this.state.leftNavbarExpanded ? "contet-width-collapse" : "content-width-expanded"}`}
|
|
574
|
+
isLatestLeftNavigationEnabled={isLatestLeftNavigationEnabled}
|
|
575
|
+
leftNavbarExpanded={this.state.leftNavbarExpanded}
|
|
576
|
+
>
|
|
577
|
+
<Switch>
|
|
578
|
+
{componentRoutes.map(routeProps => {
|
|
579
|
+
return (
|
|
580
|
+
<RenderRoute
|
|
581
|
+
{...routeProps}
|
|
582
|
+
key={routeProps.path}
|
|
583
|
+
isLoggedIn={isLoggedIn}
|
|
584
|
+
/>
|
|
585
|
+
)}
|
|
555
586
|
)}
|
|
556
|
-
|
|
557
|
-
</
|
|
558
|
-
</
|
|
559
|
-
</MainWrapper>
|
|
587
|
+
</Switch>
|
|
588
|
+
</ContentWrapper>
|
|
589
|
+
</MainWrapper>)}
|
|
560
590
|
</div>
|
|
561
591
|
{(toastMessages && toastMessages.length > 0) &&
|
|
562
592
|
toastMessages.map((message) => {
|
|
@@ -90,8 +90,8 @@ import {
|
|
|
90
90
|
import {EXTERNAL_URL, SITE_URL, WEBPUSH_MEDIA_TYPES} from '../WebPush/constants';
|
|
91
91
|
import { IMAGE, VIDEO } from '../Facebook/Advertisement/constant';
|
|
92
92
|
import { RCS_STATUSES, contentType as RCS_CONTENT_TYPE, EMBEDDED } from '../Rcs/constants';
|
|
93
|
-
import { mapRcsCardContentForConsumerWithResolvedTags } from '../Rcs/utils';
|
|
94
93
|
import { pickRcsCardVarMappedEntries } from '../Rcs/rcsLibraryHydrationUtils';
|
|
94
|
+
import { mapRcsCardContentForConsumerWithResolvedTags } from '../Rcs/utils';
|
|
95
95
|
import { RCS_SMS_FALLBACK_VAR_MAPPED_PROP } from '../../v2Components/CommonTestAndPreview/constants';
|
|
96
96
|
import { CREATIVE } from '../Facebook/constants';
|
|
97
97
|
import { LOYALTY } from '../App/constants';
|
|
@@ -1442,13 +1442,21 @@ export class Creatives extends React.Component {
|
|
|
1442
1442
|
cardSettings = {},
|
|
1443
1443
|
accountId = "",
|
|
1444
1444
|
} = get(versions, 'base.content.RCS.rcsContent', {});
|
|
1445
|
-
const rootRcsCardVarMappedFromSubmit = get(template, 'value.rcsCardVarMapped');
|
|
1446
1445
|
const firstCardFromSubmit = Array.isArray(cardContentFromSubmit)
|
|
1447
1446
|
? cardContentFromSubmit[0]
|
|
1448
1447
|
: null;
|
|
1448
|
+
const cardVarMappedFromFirstRcsCard =
|
|
1449
|
+
firstCardFromSubmit?.cardVarMapped != null
|
|
1450
|
+
&& typeof firstCardFromSubmit.cardVarMapped === 'object'
|
|
1451
|
+
? pickRcsCardVarMappedEntries(firstCardFromSubmit.cardVarMapped)
|
|
1452
|
+
: null;
|
|
1453
|
+
// Campaigns/consumers should see the literal tag (e.g. `{{first_name}}`) in title/description,
|
|
1454
|
+
// not the internal numeric slot placeholder (`{{1}}`) — that slot scheme is only meaningful
|
|
1455
|
+
// to this editor's own reopen/hydration (see getFormData, which persists raw cardContentFromSubmit
|
|
1456
|
+
// separately for that round-trip and is unaffected by this).
|
|
1449
1457
|
const cardContent = mapRcsCardContentForConsumerWithResolvedTags(
|
|
1450
1458
|
cardContentFromSubmit,
|
|
1451
|
-
|
|
1459
|
+
cardVarMappedFromFirstRcsCard,
|
|
1452
1460
|
isFullModeForRcsConsumerPayload,
|
|
1453
1461
|
);
|
|
1454
1462
|
const rcsContent = {
|
|
@@ -1457,11 +1465,6 @@ export class Creatives extends React.Component {
|
|
|
1457
1465
|
cardSettings,
|
|
1458
1466
|
cardContent,
|
|
1459
1467
|
};
|
|
1460
|
-
const cardVarMappedFromFirstRcsCard =
|
|
1461
|
-
firstCardFromSubmit?.cardVarMapped != null
|
|
1462
|
-
&& typeof firstCardFromSubmit.cardVarMapped === 'object'
|
|
1463
|
-
? pickRcsCardVarMappedEntries(firstCardFromSubmit.cardVarMapped)
|
|
1464
|
-
: null;
|
|
1465
1468
|
const includeRootRcsCardVarMappedOnConsumerPayload =
|
|
1466
1469
|
cardVarMappedFromFirstRcsCard
|
|
1467
1470
|
&& Object.keys(cardVarMappedFromFirstRcsCard).length > 0
|
|
@@ -267,6 +267,59 @@ describe('Test SlideBoxContent container', () => {
|
|
|
267
267
|
expect(instance.state.templateData.rcsCardVarMapped).toEqual(cardVarMapped);
|
|
268
268
|
});
|
|
269
269
|
|
|
270
|
+
it('RCS getCreativesData sends the literal tag (not the numeric slot) in title/description to consumers', async () => {
|
|
271
|
+
renderedComponent = shallowWithIntl(
|
|
272
|
+
<Creatives
|
|
273
|
+
loyaltyMetaData={loyaltyMetaData}
|
|
274
|
+
Templates={rcsTemplates}
|
|
275
|
+
channel="RCS"
|
|
276
|
+
slidBoxContent="editTemplate"
|
|
277
|
+
templateData={rcsEditTemplateData}
|
|
278
|
+
handleCloseCreatives={handleCloseCreatives}
|
|
279
|
+
getCreativesData={getCreativesData}
|
|
280
|
+
isFullMode={false}
|
|
281
|
+
templateActions={{
|
|
282
|
+
getCdnTransformationConfig,
|
|
283
|
+
}}
|
|
284
|
+
/>,
|
|
285
|
+
);
|
|
286
|
+
renderedComponent.instance().onEditTemplate();
|
|
287
|
+
|
|
288
|
+
const mockValue = {
|
|
289
|
+
validity: true,
|
|
290
|
+
value: {
|
|
291
|
+
name: 'rcs_creative_name',
|
|
292
|
+
type: 'RCS',
|
|
293
|
+
versions: {
|
|
294
|
+
base: {
|
|
295
|
+
content: {
|
|
296
|
+
RCS: {
|
|
297
|
+
rcsContent: {
|
|
298
|
+
contentType: 'RICHCARD',
|
|
299
|
+
cardType: 'STANDALONE',
|
|
300
|
+
cardSettings: {},
|
|
301
|
+
cardContent: [{
|
|
302
|
+
title: 'Hi {{1}}',
|
|
303
|
+
description: 'Visit {{2}}',
|
|
304
|
+
cardVarMapped: { 1: '{{first_name}}', 2: '{{loyalty_points}}' },
|
|
305
|
+
}],
|
|
306
|
+
},
|
|
307
|
+
smsFallBackContent: {},
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
renderedComponent.instance().getFormData(mockValue);
|
|
316
|
+
await tick();
|
|
317
|
+
|
|
318
|
+
const sentCardContent = getCreativesData.mock.calls[0][0].rcsContent.cardContent[0];
|
|
319
|
+
expect(sentCardContent.title).toBe('Hi {{first_name}}');
|
|
320
|
+
expect(sentCardContent.description).toBe('Visit {{loyalty_points}}');
|
|
321
|
+
});
|
|
322
|
+
|
|
270
323
|
it('Text getCreatives data for rcs, data from creatives done to campaigns', async () => {
|
|
271
324
|
renderFunction('SMS', 'editTemplate', smsTemplates, smsEditTemplateData );
|
|
272
325
|
renderedComponent.instance().onEditTemplate();
|
|
@@ -251,6 +251,11 @@ const CarouselCard = ({
|
|
|
251
251
|
hostName={hostName}
|
|
252
252
|
formatMessage={formatMessage}
|
|
253
253
|
onSuggestionsChange={onSuggestionsChange}
|
|
254
|
+
location={location}
|
|
255
|
+
tags={tagsForPicker}
|
|
256
|
+
injectedTags={injectedTags}
|
|
257
|
+
selectedOfferDetails={selectedOfferDetails}
|
|
258
|
+
onContextChange={onContextChange}
|
|
254
259
|
/>
|
|
255
260
|
</CapCard>
|
|
256
261
|
);
|
|
@@ -18,6 +18,11 @@ const CarouselCardButtons = ({
|
|
|
18
18
|
hostName,
|
|
19
19
|
formatMessage,
|
|
20
20
|
onSuggestionsChange,
|
|
21
|
+
location,
|
|
22
|
+
tags,
|
|
23
|
+
injectedTags,
|
|
24
|
+
selectedOfferDetails,
|
|
25
|
+
onContextChange,
|
|
21
26
|
}) => {
|
|
22
27
|
const suggestionsForCard = suggestions || [];
|
|
23
28
|
return (
|
|
@@ -68,6 +73,11 @@ const CarouselCardButtons = ({
|
|
|
68
73
|
host={hostName}
|
|
69
74
|
minSavedSuggestions={cardIndex === 0 ? 1 : 0}
|
|
70
75
|
hideDeleteSuggestionIndexes={cardIndex === 0 ? [0] : []}
|
|
76
|
+
location={location}
|
|
77
|
+
tags={tags}
|
|
78
|
+
injectedTags={injectedTags}
|
|
79
|
+
selectedOfferDetails={selectedOfferDetails}
|
|
80
|
+
onContextChange={onContextChange}
|
|
71
81
|
/>
|
|
72
82
|
</>
|
|
73
83
|
);
|
|
@@ -81,6 +91,11 @@ CarouselCardButtons.propTypes = {
|
|
|
81
91
|
hostName: PropTypes.string,
|
|
82
92
|
formatMessage: PropTypes.func.isRequired,
|
|
83
93
|
onSuggestionsChange: PropTypes.func.isRequired,
|
|
94
|
+
location: PropTypes.object,
|
|
95
|
+
tags: PropTypes.array,
|
|
96
|
+
injectedTags: PropTypes.object,
|
|
97
|
+
selectedOfferDetails: PropTypes.array,
|
|
98
|
+
onContextChange: PropTypes.func,
|
|
84
99
|
};
|
|
85
100
|
|
|
86
101
|
CarouselCardButtons.defaultProps = {
|
|
@@ -88,6 +103,11 @@ CarouselCardButtons.defaultProps = {
|
|
|
88
103
|
isEditFlow: false,
|
|
89
104
|
isFullMode: false,
|
|
90
105
|
hostName: '',
|
|
106
|
+
location: {},
|
|
107
|
+
tags: [],
|
|
108
|
+
injectedTags: {},
|
|
109
|
+
selectedOfferDetails: [],
|
|
110
|
+
onContextChange: () => {},
|
|
91
111
|
};
|
|
92
112
|
|
|
93
113
|
export default React.memo(CarouselCardButtons);
|