@capillarytech/creatives-library 7.10.39 → 7.10.40
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/containers/Assets/Gallery/tests/__snapshots__/reducer.test.js.snap +0 -2
- package/containers/Email/index.js +2 -3
- package/index.js +6 -0
- package/package.json +1 -1
- package/routes.js +5 -0
- package/services/api.js +6 -1
- package/v2Components/TemplatePreview/_templatePreview.scss +7 -0
- package/v2Components/TemplatePreview/assets/images/whatsapp_mobile_android.svg +2041 -0
- package/v2Components/TemplatePreview/index.js +57 -3
- package/v2Components/TemplatePreview/messages.js +8 -0
- package/v2Containers/Assets/Gallery/tests/__snapshots__/reducer.test.js.snap +0 -2
- package/v2Containers/Assets/images/whatsappIllustration.png +0 -0
- package/v2Containers/CreativesContainer/SlideBoxContent.js +24 -2
- package/v2Containers/CreativesContainer/SlideBoxHeader.js +1 -0
- package/v2Containers/CreativesContainer/constants.js +1 -0
- package/v2Containers/Email/index.js +1 -3
- package/v2Containers/Line/Container/index.js +1 -1
- package/v2Containers/Templates/_templates.scss +43 -0
- package/v2Containers/Templates/actions.js +18 -10
- package/v2Containers/Templates/constants.js +3 -1
- package/v2Containers/Templates/index.js +259 -43
- package/v2Containers/Templates/messages.js +32 -0
- package/v2Containers/Templates/reducer.js +2 -0
- package/v2Containers/TemplatesV2/index.js +2 -0
- package/v2Containers/TemplatesV2/messages.js +4 -0
- package/v2Containers/Whatsapp/actions.js +17 -0
- package/v2Containers/Whatsapp/constants.js +98 -0
- package/v2Containers/Whatsapp/index.js +469 -0
- package/v2Containers/Whatsapp/index.scss +36 -0
- package/v2Containers/Whatsapp/messages.js +184 -0
- package/v2Containers/Whatsapp/reducer.js +39 -0
- package/v2Containers/Whatsapp/sagas.js +48 -0
- package/v2Containers/Whatsapp/selectors.js +25 -0
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +123185 -0
- package/v2Containers/Whatsapp/tests/index.test.js +132 -0
- package/v2Containers/Whatsapp/tests/mockData.js +10 -0
- package/v2Containers/Whatsapp/utils.js +106 -0
|
@@ -22,7 +22,7 @@ import find from 'lodash/find';
|
|
|
22
22
|
import map from 'lodash/map';
|
|
23
23
|
import { bindActionCreators } from 'redux';
|
|
24
24
|
import { Modal, Form} from 'antd';
|
|
25
|
-
import {CapMenu, CapDropdown, CapButton, CapInput, CapIcon, CapRadioCard, CapPopover, CapSpin, CapCustomCard, CapRow, CapSlideBox, CapLink, CapHeading, CapNotification, CapIllustration, CapHeader, CapLabel, CapRadio} from '@capillarytech/cap-ui-library';
|
|
25
|
+
import {CapMenu, CapDropdown, CapButton, CapInput, CapIcon, CapRadioCard, CapPopover, CapSpin, CapCustomCard, CapRow, CapSlideBox, CapLink, CapHeading, CapNotification, CapIllustration, CapHeader, CapLabel, CapRadio, CapSelectFilter, CapTag, CapTooltip, CapAlert } from '@capillarytech/cap-ui-library';
|
|
26
26
|
import { makeSelectTemplates, makeSelectTemplatesResponse } from './selectors';
|
|
27
27
|
import { makeSelectCreate as makeSelectCreateSms} from '../Sms/Create/selectors';
|
|
28
28
|
import { makeSelectCreate as makeSelectCreateMobilePush } from '../MobilePush/Create/selectors';
|
|
@@ -60,13 +60,23 @@ import {
|
|
|
60
60
|
VIBER as VIBER_CHANNEL,
|
|
61
61
|
FACEBOOK as FACEBOOK_CHANNEL,
|
|
62
62
|
} from '../App/constants';
|
|
63
|
-
import {
|
|
63
|
+
import { MAX_WHATSAPP_TEMPLATES } from './constants';
|
|
64
|
+
import {
|
|
65
|
+
CATEGORY_OPTIONS,
|
|
66
|
+
STATUS_OPTIONS,
|
|
67
|
+
CATEGORY as WHATSAPP_CATEGORY,
|
|
68
|
+
STATUS as WHATSAPP_STATUS,
|
|
69
|
+
WHATSAPP_STATUSES,
|
|
70
|
+
} from '../Whatsapp/constants';
|
|
71
|
+
import { getWhatsappContent, getWhatsappStatusContainer, getWhatsappStatus, getWhatsappCategory } from '../Whatsapp/utils';
|
|
72
|
+
import { MOBILE_PUSH, WECHAT, SMS, EMAIL, EBILL, LINE, VIBER, FACEBOOK, WHATSAPP } from '../CreativesContainer/constants';
|
|
64
73
|
|
|
65
74
|
import {CREATIVE} from '../Facebook/constants';
|
|
66
75
|
|
|
67
76
|
import emailIllustration from '../Assets/images/emailIllustration.svg';
|
|
68
77
|
import smsIllustration from '../Assets/images/smsIllustration.svg';
|
|
69
78
|
import pushIllustration from '../Assets/images/pushIllustration.svg';
|
|
79
|
+
import whatsappIllustration from '../Assets/images/whatsappIllustration.png';
|
|
70
80
|
import { CAP_SPACE_16 } from '@capillarytech/cap-ui-library/styled/variables';
|
|
71
81
|
import { GA } from '@capillarytech/cap-ui-utils';
|
|
72
82
|
const { timeTracker } = GA;
|
|
@@ -105,6 +115,7 @@ const SMS_FILTERS = {
|
|
|
105
115
|
SERVICE_IMPLICIT: 'implicit',
|
|
106
116
|
};
|
|
107
117
|
|
|
118
|
+
const WHATSAPP_LOWERCASE = WHATSAPP.toLowerCase();
|
|
108
119
|
export class Templates extends React.Component { // eslint-disable-line react/prefer-stateless-function
|
|
109
120
|
constructor(props) {
|
|
110
121
|
super(props);
|
|
@@ -133,6 +144,8 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
133
144
|
wechatFilter: WECHAT_FILTERS.ALL,
|
|
134
145
|
lineFilter: LINE_FILTERS.ALL,
|
|
135
146
|
smsFilter: SMS_FILTERS.ALL,
|
|
147
|
+
selectedWhatsappStatus: '',
|
|
148
|
+
selectedWhatsappCategory: ''
|
|
136
149
|
};
|
|
137
150
|
|
|
138
151
|
this.handleOnHoverItem = this.handleOnHoverItem.bind(this);
|
|
@@ -198,6 +211,11 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
198
211
|
activeMode = ACCOUNT_SELECTION_MODE;
|
|
199
212
|
this.props.actions.getAccountsSettings();
|
|
200
213
|
break;
|
|
214
|
+
case WHATSAPP:
|
|
215
|
+
channel = 'Whatsapp';
|
|
216
|
+
activeMode = ACCOUNT_SELECTION_MODE;
|
|
217
|
+
this.props.actions.getWeCrmAccounts(channel);
|
|
218
|
+
break;
|
|
201
219
|
default:
|
|
202
220
|
channel = '';
|
|
203
221
|
}
|
|
@@ -245,9 +263,8 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
245
263
|
};
|
|
246
264
|
const { totalCount: nextPropsTotalTemplatesCount } = nextProps?.TemplatesList?.[0] || {};
|
|
247
265
|
const { totalCount: propsTotalTemplatesCount } = this.props?.TemplatesList?.[0] || {};
|
|
248
|
-
const { templatesCount } = this.state || {};
|
|
249
266
|
|
|
250
|
-
if (nextPropsTotalTemplatesCount && nextPropsTotalTemplatesCount !== propsTotalTemplatesCount
|
|
267
|
+
if (nextPropsTotalTemplatesCount && nextPropsTotalTemplatesCount !== propsTotalTemplatesCount) {
|
|
251
268
|
this.setState({ templatesCount: nextPropsTotalTemplatesCount });
|
|
252
269
|
}
|
|
253
270
|
if (!isEqual(nextProps.route.name, this.props.route.name)) {
|
|
@@ -463,12 +480,12 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
463
480
|
this.setState({previewTemplate: nextProps.Templates.templateDetails});
|
|
464
481
|
}
|
|
465
482
|
|
|
466
|
-
if (nextProps.Templates.weCrmAccounts !== undefined && nextProps.Templates.weCrmAccounts.length === 1 && this.state.defaultAccount && (['wechat'].indexOf(this.state.channel.toLowerCase()) > -1)) {
|
|
483
|
+
if (nextProps.Templates.weCrmAccounts !== undefined && nextProps.Templates.weCrmAccounts.length === 1 && this.state.defaultAccount && (['wechat', WHATSAPP_LOWERCASE].indexOf(this.state.channel.toLowerCase()) > -1 )) {
|
|
467
484
|
const paramsDefault = {
|
|
468
485
|
name: this.state.searchText,
|
|
469
486
|
sortBy: this.state.sortBy,
|
|
470
487
|
};
|
|
471
|
-
if (this.state.channel.toLowerCase()
|
|
488
|
+
if (['wechat', WHATSAPP_LOWERCASE].includes(this.state.channel.toLowerCase())) {
|
|
472
489
|
paramsDefault.wecrmId = nextProps.Templates.weCrmAccounts[0].configs.wecrm_app_id;
|
|
473
490
|
paramsDefault.wecrmToken = nextProps.Templates.weCrmAccounts[0].configs.wecrm_token;
|
|
474
491
|
paramsDefault.originalId = nextProps.Templates.weCrmAccounts[0].sourceAccountIdentifier;
|
|
@@ -512,8 +529,10 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
512
529
|
} else {
|
|
513
530
|
this.setState({selectedAccountError: false});
|
|
514
531
|
}
|
|
515
|
-
} else if (this.state.channel.toLowerCase() ===
|
|
516
|
-
this.props.actions.
|
|
532
|
+
} else if (this.state.channel.toLowerCase() === LINE.toLowerCase()) {
|
|
533
|
+
this.props.actions.setChannelAccount(LINE, this.props.Templates.weCrmAccounts.find((item) => item.name === this.state.selectedAccount));
|
|
534
|
+
} else if (this.state.channel.toLowerCase() === WHATSAPP_LOWERCASE) {
|
|
535
|
+
this.props.actions.setChannelAccount(WHATSAPP, this.props.Templates.weCrmAccounts.find((item) => item.name === this.state.selectedAccount));
|
|
517
536
|
}
|
|
518
537
|
|
|
519
538
|
this.setState({activeMode: TEMPLATES_MODE});
|
|
@@ -525,8 +544,9 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
525
544
|
this.getAllTemplates({getNextPage: true});
|
|
526
545
|
}
|
|
527
546
|
onCreateComplete = (isReloadTemplates) => {
|
|
547
|
+
const isWhatsappEdit = this.state.routeParams?.pathname?.includes(`${WHATSAPP_LOWERCASE}/edit`);
|
|
528
548
|
this.setState({routeParams: null, previewOpen: false}, () => {
|
|
529
|
-
if (this.isFullMode() && isReloadTemplates) {
|
|
549
|
+
if (this.isFullMode() && (isReloadTemplates || isWhatsappEdit)) {
|
|
530
550
|
this.getAllTemplates({params: {}}, true);
|
|
531
551
|
}
|
|
532
552
|
});
|
|
@@ -546,7 +566,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
546
566
|
const { pathname } = routeParams;
|
|
547
567
|
if (pathname.includes('create')) {
|
|
548
568
|
creativesParams.mode = 'create';
|
|
549
|
-
} else if (pathname.includes('edit')) {
|
|
569
|
+
} else if (pathname.includes('edit') || pathname.includes('overview')) {
|
|
550
570
|
creativesParams.mode = 'edit';
|
|
551
571
|
if (pathname.includes('richmedia')) {
|
|
552
572
|
creativesParams._id = pathname.split('/')[4];
|
|
@@ -654,6 +674,32 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
654
674
|
return templates;
|
|
655
675
|
}
|
|
656
676
|
}
|
|
677
|
+
|
|
678
|
+
isStatusMatching = (status, selectedWhatsappStatus) => {
|
|
679
|
+
if ( [WHATSAPP_STATUSES.unsubmitted, WHATSAPP_STATUSES.pending].includes(status) ) {
|
|
680
|
+
return selectedWhatsappStatus === WHATSAPP_STATUSES.awaitingApproval;
|
|
681
|
+
}
|
|
682
|
+
return selectedWhatsappStatus === status;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
filterWhatsappTemplates = (templates) => {
|
|
686
|
+
const { selectedWhatsappStatus, selectedWhatsappCategory } = this.state;
|
|
687
|
+
return templates.filter((template) => {
|
|
688
|
+
const { [WHATSAPP_CATEGORY]: category, [WHATSAPP_STATUS]: status } = template?.versions?.base?.content?.[WHATSAPP_LOWERCASE] || {};
|
|
689
|
+
|
|
690
|
+
// if status or category filter is applied, filter templates which match these filters
|
|
691
|
+
return (
|
|
692
|
+
selectedWhatsappStatus
|
|
693
|
+
? this.isStatusMatching(status, selectedWhatsappStatus)
|
|
694
|
+
: true
|
|
695
|
+
) && (
|
|
696
|
+
selectedWhatsappCategory
|
|
697
|
+
? category === selectedWhatsappCategory
|
|
698
|
+
: true
|
|
699
|
+
);
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
|
|
657
703
|
filterSMSTemplates = (templates) => {
|
|
658
704
|
const { smsFilter } = this.state;
|
|
659
705
|
if (SMS_FILTERS.ALL === smsFilter) {
|
|
@@ -662,22 +708,44 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
662
708
|
return templates.filter((template) => template.versions.base.type?.toLowerCase()?.includes(smsFilter));
|
|
663
709
|
}
|
|
664
710
|
|
|
711
|
+
getCreativesEditText = (channel) => {
|
|
712
|
+
if (channel === WHATSAPP_LOWERCASE) {
|
|
713
|
+
return messages.whatsappOverview;
|
|
714
|
+
}
|
|
715
|
+
return messages.creativesTemplatesEdit;
|
|
716
|
+
}
|
|
717
|
+
|
|
665
718
|
getTemplateDataForGrid = ({templates, handlers, filterContent, channel, isLoading, loadingTip}) => {
|
|
666
719
|
const currentChannel = channel.toUpperCase();
|
|
667
720
|
const {channel: stateChannel} = this.state;
|
|
668
721
|
const channelLowerCase = stateChannel.toLowerCase();
|
|
669
722
|
let filteredTemplates = templates;
|
|
670
723
|
let isTraiDltFeature = false;
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
724
|
+
switch (currentChannel) {
|
|
725
|
+
case WECHAT:
|
|
726
|
+
filteredTemplates = this.filterWechatTemplates(templates);
|
|
727
|
+
break;
|
|
728
|
+
case WHATSAPP:
|
|
729
|
+
filteredTemplates = this.filterWhatsappTemplates(templates);
|
|
730
|
+
break;
|
|
731
|
+
case LINE:
|
|
732
|
+
filteredTemplates = this.filterLineTemplates(templates);
|
|
733
|
+
break;
|
|
734
|
+
case SMS:
|
|
735
|
+
filteredTemplates = this.filterSMSTemplates(templates);
|
|
736
|
+
isTraiDltFeature = this.checkDLTfeatureEnable();
|
|
737
|
+
break;
|
|
738
|
+
default:
|
|
739
|
+
break;
|
|
680
740
|
}
|
|
741
|
+
|
|
742
|
+
const hoverButtonText =
|
|
743
|
+
this.props.intl.formatMessage(
|
|
744
|
+
this.props.isFullMode
|
|
745
|
+
? this.getCreativesEditText(channelLowerCase)
|
|
746
|
+
: messages.selectButton
|
|
747
|
+
);
|
|
748
|
+
|
|
681
749
|
const cardDataList = filteredTemplates && filteredTemplates.length ? map(filteredTemplates, (template) => {
|
|
682
750
|
const templateData =
|
|
683
751
|
{
|
|
@@ -691,13 +759,13 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
691
759
|
onClick={() => { if (this.props.isFullMode) { handlers.handlePreviewClick(template); } else { this.props.handlePeviewTemplate(template); } }}
|
|
692
760
|
/>,
|
|
693
761
|
],
|
|
694
|
-
hoverOption: <CapButton className={this.props.isFullMode ? `edit-${channelLowerCase}` : `select-${channelLowerCase}`} onClick={(e) => handlers.handleEditClick(e, template, undefined, undefined, {account: this.state.selectedAccount})}>{
|
|
762
|
+
hoverOption: <CapButton className={this.props.isFullMode ? `edit-${channelLowerCase}` : `select-${channelLowerCase}`} onClick={(e) => handlers.handleEditClick(e, template, undefined, undefined, {account: this.state.selectedAccount})}>{hoverButtonText}</CapButton>,
|
|
695
763
|
};
|
|
696
764
|
if (this.props.isFullMode) {
|
|
697
765
|
templateData.extra.push(<CapDropdown
|
|
698
766
|
overlay={
|
|
699
767
|
<CapMenu>
|
|
700
|
-
{this.state.channel.toUpperCase()
|
|
768
|
+
{ ![WECHAT, WHATSAPP].includes(this.state.channel.toUpperCase()) && !isTraiDltFeature && <CapMenu.Item className={`duplicate-${channelLowerCase}`} onClick={() => this.duplicateTemplate(template)}>
|
|
701
769
|
<FormattedMessage {...messages.duplicateButton} />
|
|
702
770
|
</CapMenu.Item>}
|
|
703
771
|
<CapMenu.Item
|
|
@@ -834,6 +902,16 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
834
902
|
}
|
|
835
903
|
break;
|
|
836
904
|
}
|
|
905
|
+
case WHATSAPP:
|
|
906
|
+
templateData.title = (
|
|
907
|
+
<div className="whatsapp-card-title">
|
|
908
|
+
<span title={template.name} >{template.name}</span>
|
|
909
|
+
{getWhatsappStatusContainer(template)}
|
|
910
|
+
</div>
|
|
911
|
+
);
|
|
912
|
+
|
|
913
|
+
templateData.content = getWhatsappContent(template);
|
|
914
|
+
break;
|
|
837
915
|
default:
|
|
838
916
|
templateData.content = "";
|
|
839
917
|
}
|
|
@@ -842,8 +920,9 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
842
920
|
const accountId = get(this.props, 'Templates.selectedWeChatAccount.uuid');
|
|
843
921
|
const accounts = get(this.props, 'Templates.weCrmAccounts');
|
|
844
922
|
return (<div>
|
|
845
|
-
{
|
|
923
|
+
{[WECHAT, MOBILE_PUSH].includes(currentChannel) && this.showAccountName()}
|
|
846
924
|
{filterContent}
|
|
925
|
+
{[WHATSAPP].includes(currentChannel) && this.selectedFilters()}
|
|
847
926
|
<CapSpin spinning={isLoading} tip={loadingTip} style={{width: '100%'}}>
|
|
848
927
|
{<div>
|
|
849
928
|
{!isEmpty(filteredTemplates) || !isEmpty(this.state.searchText) || !isEmpty(this.props.Templates.templateError) ? (
|
|
@@ -865,7 +944,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
865
944
|
</div>
|
|
866
945
|
}
|
|
867
946
|
|
|
868
|
-
{ !isEmpty(this.props.Templates.selectedWeChatAccount) && this.state.channel.toLowerCase()
|
|
947
|
+
{ !isEmpty(this.props.Templates.selectedWeChatAccount) && ["mobilepush", WHATSAPP_LOWERCASE].includes(this.state.channel.toLowerCase()) && this.state.selectedAccountError ?
|
|
869
948
|
<div style={{ height: '65vh', textAlign: 'center', lineHeight: '65vh'}}>
|
|
870
949
|
<CapHeading type="h6" >
|
|
871
950
|
{this.props.intl.formatMessage(messages.accountConfigError)}
|
|
@@ -874,7 +953,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
874
953
|
</InlineDiv>
|
|
875
954
|
</CapHeading>
|
|
876
955
|
</div> : (
|
|
877
|
-
["mobilepush"].includes(this.state.channel.toLowerCase()) &&
|
|
956
|
+
["mobilepush", WHATSAPP_LOWERCASE].includes(this.state.channel.toLowerCase()) &&
|
|
878
957
|
isEmpty(filteredTemplates) &&
|
|
879
958
|
isEmpty(this.state.searchText) &&
|
|
880
959
|
<div style={this.isFullMode() ? { height: "calc(100vh - 325px)", overflow: 'auto'} : {}}>
|
|
@@ -1456,6 +1535,9 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
1456
1535
|
pathName = `/wechat/edit/${id}`;
|
|
1457
1536
|
}
|
|
1458
1537
|
}
|
|
1538
|
+
if (this.state.channel.toUpperCase() === WHATSAPP) {
|
|
1539
|
+
pathName = `/whatsapp/edit/${id}/`;
|
|
1540
|
+
}
|
|
1459
1541
|
if (this.isEnabledInLibraryModule("callSelectFromProps")) {
|
|
1460
1542
|
let data = id;
|
|
1461
1543
|
if (this.props.fbAdManager) {
|
|
@@ -1716,7 +1798,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
1716
1798
|
if (this.props.location.query.type === "embedded" && this.props.location.query.module === "library") {
|
|
1717
1799
|
switch (property) {
|
|
1718
1800
|
case "showAccountSelection":
|
|
1719
|
-
return (this.props.route && (
|
|
1801
|
+
return (this.props.route && (['wechat', 'mobilepush', WHATSAPP_LOWERCASE].includes(this.props.route.name.toLowerCase())));
|
|
1720
1802
|
case "setLocale":
|
|
1721
1803
|
return true;
|
|
1722
1804
|
case "callCreateFromProps":
|
|
@@ -1734,9 +1816,13 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
1734
1816
|
isCreateDisabled = () => {
|
|
1735
1817
|
let isDisabled = this.isLoading();
|
|
1736
1818
|
const channel = this.state.channel.toUpperCase();
|
|
1737
|
-
if (channel === MOBILE_PUSH || channel === WECHAT) {
|
|
1819
|
+
if (channel === MOBILE_PUSH || channel === WECHAT || channel === WHATSAPP) {
|
|
1738
1820
|
isDisabled = isDisabled || isEmpty(this.props.Templates.selectedWeChatAccount);
|
|
1739
1821
|
}
|
|
1822
|
+
if (channel === WHATSAPP) {
|
|
1823
|
+
// limit on max number of templates allowed for whatsapp
|
|
1824
|
+
isDisabled = this.state.templatesCount >= MAX_WHATSAPP_TEMPLATES;
|
|
1825
|
+
}
|
|
1740
1826
|
if (channel === MOBILE_PUSH) {
|
|
1741
1827
|
isDisabled = this.checkSearchDisabled();
|
|
1742
1828
|
}
|
|
@@ -1758,6 +1844,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
1758
1844
|
const facebookLoader = (getAllTemplatesInProgress && this.state.channel.toLowerCase() === FACEBOOK_CHANNEL);
|
|
1759
1845
|
|
|
1760
1846
|
const wechatLoader = (((this.state.selectedAccount !== '' && getAllTemplatesInProgress) || (fetchingWeCrmAccounts)) && this.state.channel.toLowerCase() === 'wechat');
|
|
1847
|
+
const whatsappLoader = (((this.state.selectedAccount !== '' && getAllTemplatesInProgress) || (fetchingWeCrmAccounts)) && this.state.channel.toLowerCase() === WHATSAPP_LOWERCASE);
|
|
1761
1848
|
const mobilePushLoader = (((getAllTemplatesInProgress) || (fetchingWeCrmAccounts)) && this.state.channel.toLowerCase() === 'mobilepush');
|
|
1762
1849
|
const emailLoader = (
|
|
1763
1850
|
(getAllTemplatesInProgress ||
|
|
@@ -1766,6 +1853,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
1766
1853
|
EmailCreate.duplicateTemplateInProgress) && this.state.channel.toLowerCase() === 'email');
|
|
1767
1854
|
const ebillLoader = ((getAllTemplatesInProgress || sendingFile) && this.state.channel.toLowerCase() === 'ebill');
|
|
1768
1855
|
const isLoading = ((wechatLoader !== undefined) ? wechatLoader : false) ||
|
|
1856
|
+
(whatsappLoader !== undefined ? whatsappLoader : false) ||
|
|
1769
1857
|
(smsLoader !== undefined ? smsLoader : false) ||
|
|
1770
1858
|
(emailLoader !== undefined ? emailLoader : false) ||
|
|
1771
1859
|
(ebillLoader !== undefined ? ebillLoader : false) ||
|
|
@@ -1801,6 +1889,16 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
1801
1889
|
illustrationImage: pushIllustration,
|
|
1802
1890
|
title: <FormattedMessage {...messages.pushTitleIllustartion} values={{ template: templateText }} />,
|
|
1803
1891
|
};
|
|
1892
|
+
case WHATSAPP:
|
|
1893
|
+
return {
|
|
1894
|
+
buttonLabel: <FormattedMessage {...messages.newWhatsappTemplate} values={{ template: templateText }} />,
|
|
1895
|
+
onClick: this.createTemplate,
|
|
1896
|
+
illustrationImage: whatsappIllustration,
|
|
1897
|
+
title: <FormattedMessage {...messages.whatsappTitleIllustration} values={{ template: templateText }} />,
|
|
1898
|
+
description: <FormattedMessage {...messages.whatsappDescIllustration} />,
|
|
1899
|
+
descriptionPosition: 'bottom',
|
|
1900
|
+
descriptionClassName: 'whatsapp-illustration-desc',
|
|
1901
|
+
};
|
|
1804
1902
|
default:
|
|
1805
1903
|
return {};
|
|
1806
1904
|
}
|
|
@@ -1852,13 +1950,11 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
1852
1950
|
const { weCrmAccounts = [], fetchingWeCrmAccounts = false, campaignSettings = {} } = templates;
|
|
1853
1951
|
const isWechatChannel = channel === WECHAT;
|
|
1854
1952
|
const isMobilePushChannel = channel === MOBILE_PUSH;
|
|
1855
|
-
const
|
|
1856
|
-
const isFacebookChannel = channel === FACEBOOK;
|
|
1857
|
-
|
|
1953
|
+
const isFacebookChannel = channel === FACEBOOK;
|
|
1858
1954
|
|
|
1859
|
-
if (
|
|
1955
|
+
if ([WECHAT, MOBILE_PUSH, LINE, WHATSAPP].includes(channel) && !isEmpty(weCrmAccounts) && !isFacebookChannel) {
|
|
1860
1956
|
forEach(weCrmAccounts, (account) => {
|
|
1861
|
-
if ((isWechatChannel && account.configs && account.configs.is_wecrm_enabled) ||
|
|
1957
|
+
if ((isWechatChannel && account.configs && account.configs.is_wecrm_enabled) || [MOBILE_PUSH, LINE, WHATSAPP].includes(channel)) {
|
|
1862
1958
|
if (query.type === 'embedded' && (!query.module || (query.module && query.module !== 'library'))) {
|
|
1863
1959
|
if (query.source_account_id && account.sourceAccountIdentifier === query.source_account_id) {
|
|
1864
1960
|
accountOptions.push(
|
|
@@ -1922,6 +2018,11 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
1922
2018
|
noAccountHeader = messages.noAccountsPresentFacebook;
|
|
1923
2019
|
break;
|
|
1924
2020
|
}
|
|
2021
|
+
case WHATSAPP: {
|
|
2022
|
+
accountHeader = formatMessage(messages.whatsappAccount);
|
|
2023
|
+
noAccountHeader = messages.noAccountsPresentWhatsapp;
|
|
2024
|
+
break;
|
|
2025
|
+
}
|
|
1925
2026
|
default:
|
|
1926
2027
|
break;
|
|
1927
2028
|
}
|
|
@@ -1977,27 +2078,97 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
1977
2078
|
this.setState({activeMode: TEMPLATES_MODE});
|
|
1978
2079
|
}
|
|
1979
2080
|
|
|
2081
|
+
accountNameMessage = (channel) => {
|
|
2082
|
+
const {
|
|
2083
|
+
intl: {
|
|
2084
|
+
formatMessage,
|
|
2085
|
+
},
|
|
2086
|
+
} = this.props;
|
|
2087
|
+
switch (channel?.toUpperCase()) {
|
|
2088
|
+
case MOBILE_PUSH:
|
|
2089
|
+
return formatMessage(messages.mobilepushAccount);
|
|
2090
|
+
case WECHAT:
|
|
2091
|
+
return formatMessage(messages.wechatAccount);
|
|
2092
|
+
case WHATSAPP:
|
|
2093
|
+
return formatMessage(messages.whatsappAccount);
|
|
2094
|
+
default:
|
|
2095
|
+
return '';
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
|
|
1980
2099
|
showAccountName = () => {
|
|
1981
2100
|
const { activeMode, selectedAccount, channel } = this.state;
|
|
1982
2101
|
const { Templates: templates = {}} = this.props;
|
|
1983
|
-
const { weCrmAccounts = []} = templates;
|
|
1984
|
-
const isMobilePushChannel = channel === MOBILE_PUSH;
|
|
2102
|
+
const { weCrmAccounts = [] } = templates;
|
|
1985
2103
|
return (activeMode !== ACCOUNT_CHANGE_MODE &&
|
|
1986
2104
|
<CapHeader
|
|
1987
|
-
title={<CapHeading type="h4">{
|
|
2105
|
+
title={<CapHeading type="h4">{this.accountNameMessage(channel)}</CapHeading>}
|
|
1988
2106
|
description={<><CapHeading type="h3">{selectedAccount} {weCrmAccounts.length > 1 && <CapLink onClick={this.setAccountChangeMode} style={{display: 'inline-block', position: 'relative', top: '3px'}} fontWeight="regular" fontSize="14px" title="change"/>} </CapHeading></>}
|
|
1989
2107
|
size="regular"
|
|
1990
2108
|
/>);
|
|
1991
2109
|
}
|
|
1992
2110
|
|
|
2111
|
+
removeWhatsappFilter = (filter) => {
|
|
2112
|
+
switch (filter) {
|
|
2113
|
+
case WHATSAPP_STATUS:
|
|
2114
|
+
this.setState({ selectedWhatsappStatus: null });
|
|
2115
|
+
break;
|
|
2116
|
+
case WHATSAPP_CATEGORY:
|
|
2117
|
+
this.setState({ selectedWhatsappCategory: null });
|
|
2118
|
+
break;
|
|
2119
|
+
default:
|
|
2120
|
+
break;
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
selectedFilters = () => {
|
|
2125
|
+
const { selectedWhatsappStatus, selectedWhatsappCategory } = this.state;
|
|
2126
|
+
return (
|
|
2127
|
+
<CapRow type="flex" align="middle" className="selected-whatsapp-filters">
|
|
2128
|
+
{
|
|
2129
|
+
selectedWhatsappStatus ? (
|
|
2130
|
+
<CapTag closable onClose={() => this.removeWhatsappFilter(WHATSAPP_STATUS)}>
|
|
2131
|
+
Status: {getWhatsappStatus(selectedWhatsappStatus)?.label || selectedWhatsappStatus}
|
|
2132
|
+
</CapTag>
|
|
2133
|
+
) : null
|
|
2134
|
+
}
|
|
2135
|
+
{
|
|
2136
|
+
selectedWhatsappCategory ? (
|
|
2137
|
+
<CapTag closable onClose={() => this.removeWhatsappFilter(WHATSAPP_CATEGORY)}>
|
|
2138
|
+
Category: {getWhatsappCategory(selectedWhatsappCategory)?.label || selectedWhatsappCategory}
|
|
2139
|
+
</CapTag>
|
|
2140
|
+
) : null
|
|
2141
|
+
}
|
|
2142
|
+
{
|
|
2143
|
+
(selectedWhatsappStatus || selectedWhatsappCategory) ? (
|
|
2144
|
+
<CapLink
|
|
2145
|
+
onClick={() => {
|
|
2146
|
+
this.removeWhatsappFilter(WHATSAPP_STATUS);
|
|
2147
|
+
this.removeWhatsappFilter(WHATSAPP_CATEGORY);
|
|
2148
|
+
}}
|
|
2149
|
+
title={this.props.intl.formatMessage(messages.clearAll)}
|
|
2150
|
+
/>
|
|
2151
|
+
) : null
|
|
2152
|
+
}
|
|
2153
|
+
</CapRow>
|
|
2154
|
+
);
|
|
2155
|
+
}
|
|
2156
|
+
|
|
1993
2157
|
setWechatFilter = (e) => this.setState({wechatFilter: e.target.value});
|
|
2158
|
+
setWhatsappStatus = (value) => this.setState({selectedWhatsappStatus: value?.item?.props?.value});
|
|
2159
|
+
setWhatsappCategory = (value) => this.setState({selectedWhatsappCategory: value?.item?.props?.value});
|
|
1994
2160
|
setLineFilter = (e) => this.setState({lineFilter: e.target.value});
|
|
1995
2161
|
setSMSFilter = (e) => this.setState({smsFilter: e.target.value});
|
|
1996
2162
|
|
|
1997
2163
|
|
|
1998
2164
|
render() {
|
|
1999
2165
|
const isFullMode = this.isFullMode();
|
|
2000
|
-
const { activeMode, channel, wechatFilter, lineFilter, smsFilter } = this.state;
|
|
2166
|
+
const { activeMode, channel, wechatFilter, lineFilter, smsFilter, templatesCount } = this.state;
|
|
2167
|
+
const {
|
|
2168
|
+
intl: {
|
|
2169
|
+
formatMessage,
|
|
2170
|
+
} = {},
|
|
2171
|
+
} = this.props || {};
|
|
2001
2172
|
let showFbAds = false;
|
|
2002
2173
|
if (channel.toLowerCase() === FACEBOOK_CHANNEL) {
|
|
2003
2174
|
const {campaignSettings, fetchedOrgLevelCampaignSettings} = this?.props?.Templates || {};
|
|
@@ -2061,6 +2232,19 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
2061
2232
|
const isWechatEmbedded = !this.props.isFullMode && channel.toUpperCase() === WECHAT;
|
|
2062
2233
|
const channelLowerCase = (channel || '').toLowerCase();
|
|
2063
2234
|
const isTraiDltFeature = this.checkDLTfeatureEnable();
|
|
2235
|
+
|
|
2236
|
+
const createButton = (<CapButton
|
|
2237
|
+
className={`create-new-${channelLowerCase}`}
|
|
2238
|
+
type={"primary"}
|
|
2239
|
+
disabled={this.isCreateDisabled()}
|
|
2240
|
+
style={{marginLeft: '8px', marginBottom: '12px'}}
|
|
2241
|
+
onClick={this.createTemplate} >
|
|
2242
|
+
{ this.props.intl.formatMessage((isTraiDltFeature && channel.toUpperCase() === SMS ) ? messages.uploadTemplate : messages.createNewActionButton) }
|
|
2243
|
+
</CapButton>);
|
|
2244
|
+
|
|
2245
|
+
const isWhatsappCountExeeded = templatesCount >= MAX_WHATSAPP_TEMPLATES;
|
|
2246
|
+
const whatsappCountExceedText = <FormattedMessage {...messages.whatsappMaxTemplates} values={{ maxCount: MAX_WHATSAPP_TEMPLATES }}/>;
|
|
2247
|
+
|
|
2064
2248
|
const filterContent = (( isfilterContentVisisble || [WECHAT, MOBILE_PUSH].includes(this.state.channel.toUpperCase())) && <div className="action-container">
|
|
2065
2249
|
{isfilterContentVisisble && <CapInput.Search
|
|
2066
2250
|
className="search-text"
|
|
@@ -2085,6 +2269,31 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
2085
2269
|
</CapLabel></CapRadio.Button>
|
|
2086
2270
|
</CapRadio.CapRadioGroup>
|
|
2087
2271
|
}
|
|
2272
|
+
{
|
|
2273
|
+
channel.toUpperCase() === WHATSAPP && (
|
|
2274
|
+
<div className="whatsapp-filters">
|
|
2275
|
+
<CapSelectFilter
|
|
2276
|
+
placement="bottomLeft"
|
|
2277
|
+
data={STATUS_OPTIONS}
|
|
2278
|
+
onSelect={this.setWhatsappStatus}
|
|
2279
|
+
selectedValue={this.state.selectedWhatsappStatus}
|
|
2280
|
+
placeholder="Status"
|
|
2281
|
+
showBadge={true}
|
|
2282
|
+
width="90px"
|
|
2283
|
+
/>
|
|
2284
|
+
<CapSelectFilter
|
|
2285
|
+
placement="bottomLeft"
|
|
2286
|
+
data={CATEGORY_OPTIONS}
|
|
2287
|
+
onSelect={this.setWhatsappCategory}
|
|
2288
|
+
selectedValue={this.state.selectedWhatsappCategory}
|
|
2289
|
+
placeholder="Category"
|
|
2290
|
+
showBadge={true}
|
|
2291
|
+
width="105px"
|
|
2292
|
+
/>
|
|
2293
|
+
</div>
|
|
2294
|
+
|
|
2295
|
+
)
|
|
2296
|
+
}
|
|
2088
2297
|
{
|
|
2089
2298
|
channel.toUpperCase() === LINE &&
|
|
2090
2299
|
isFullMode
|
|
@@ -2152,14 +2361,16 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
2152
2361
|
: () => null
|
|
2153
2362
|
}
|
|
2154
2363
|
<div style={{display: "flex", justifyContent: "space-between", alignItems: 'center'}}>
|
|
2155
|
-
{
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2364
|
+
{
|
|
2365
|
+
this.state.channel.toLowerCase() === WHATSAPP_LOWERCASE && isWhatsappCountExeeded ? (
|
|
2366
|
+
<CapTooltip title={whatsappCountExceedText}>
|
|
2367
|
+
<div className="button-disabled-tooltip-wrapper">
|
|
2368
|
+
{createButton}
|
|
2369
|
+
</div>
|
|
2370
|
+
</CapTooltip>
|
|
2371
|
+
)
|
|
2372
|
+
: isfilterContentVisisble && !isWechatEmbedded && createButton
|
|
2373
|
+
}
|
|
2163
2374
|
</div>
|
|
2164
2375
|
|
|
2165
2376
|
</div>);
|
|
@@ -2193,6 +2404,11 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
2193
2404
|
<div className={`creatives-templates-list ${this.props.isFullMode ? 'full-mode' : 'library-mode'}`}>
|
|
2194
2405
|
<input type="file" id="filename" style={{ display: 'none'}} accept=".zip, .html, .htm" onChange={(event) => this.handleFileUpload(event, {files: event.target.files})} />
|
|
2195
2406
|
|
|
2407
|
+
{
|
|
2408
|
+
channel.toLowerCase() === WHATSAPP_LOWERCASE && isWhatsappCountExeeded ? (
|
|
2409
|
+
<CapAlert message={whatsappCountExceedText} type="info" />
|
|
2410
|
+
) : null
|
|
2411
|
+
}
|
|
2196
2412
|
|
|
2197
2413
|
<CapRow>
|
|
2198
2414
|
<Pagination onPageChange={templates.length ? this.onPaginationChange : () => {}} paginationSelector="pagination-container">
|
|
@@ -94,6 +94,10 @@ export default defineMessages({
|
|
|
94
94
|
id: `${scope}.editButton`,
|
|
95
95
|
defaultMessage: "Edit",
|
|
96
96
|
},
|
|
97
|
+
"whatsappOverview": {
|
|
98
|
+
id: `${scope}.whatsappOverview`,
|
|
99
|
+
defaultMessage: "Overview",
|
|
100
|
+
},
|
|
97
101
|
"duplicateButton": {
|
|
98
102
|
id: `${scope}.duplicateButton`,
|
|
99
103
|
defaultMessage: "Duplicate",
|
|
@@ -130,6 +134,10 @@ export default defineMessages({
|
|
|
130
134
|
id: `${scope}.noAccountsPresentLine`,
|
|
131
135
|
defaultMessage: "Line accounts are not setup for your brand",
|
|
132
136
|
},
|
|
137
|
+
"noAccountsPresentWhatsapp": {
|
|
138
|
+
id: `${scope}.noAccountsPresentWhatsapp`,
|
|
139
|
+
defaultMessage: "Whatsapp accounts are not setup for your brand",
|
|
140
|
+
},
|
|
133
141
|
"noTemplatesMessage": {
|
|
134
142
|
id: `${scope}.noTemplatesMessage`,
|
|
135
143
|
defaultMessage: "No templates available",
|
|
@@ -306,6 +314,10 @@ export default defineMessages({
|
|
|
306
314
|
id: `${scope}.newEmailTemplate`,
|
|
307
315
|
defaultMessage: 'New email {template}',
|
|
308
316
|
},
|
|
317
|
+
"newWhatsappTemplate": {
|
|
318
|
+
id: `${scope}.newWhatsappTemplate`,
|
|
319
|
+
defaultMessage: 'Add new Whatsapp {template}',
|
|
320
|
+
},
|
|
309
321
|
"newNotificationTemplate": {
|
|
310
322
|
id: `${scope}.newNotificationTemplate`,
|
|
311
323
|
defaultMessage: 'New notification {template}',
|
|
@@ -318,6 +330,14 @@ export default defineMessages({
|
|
|
318
330
|
id: `${scope}.emailTitleIllustartion`,
|
|
319
331
|
defaultMessage: 'Create an email {template}',
|
|
320
332
|
},
|
|
333
|
+
"whatsappTitleIllustration": {
|
|
334
|
+
id: `${scope}.whatsappTitleIllustration`,
|
|
335
|
+
defaultMessage: 'Add a new Whatsapp creative {template}',
|
|
336
|
+
},
|
|
337
|
+
"whatsappDescIllustration": {
|
|
338
|
+
id: `${scope}.whatsappDescIllustration`,
|
|
339
|
+
defaultMessage: 'These templates can be reused when creating a\nnew message content.',
|
|
340
|
+
},
|
|
321
341
|
"smsTitleIllustartion": {
|
|
322
342
|
id: `${scope}.smsTitleIllustartion`,
|
|
323
343
|
defaultMessage: 'Create an SMS {template}',
|
|
@@ -346,6 +366,10 @@ export default defineMessages({
|
|
|
346
366
|
id: `${scope}.lineAccount`,
|
|
347
367
|
defaultMessage: 'Line account',
|
|
348
368
|
},
|
|
369
|
+
"whatsappAccount": {
|
|
370
|
+
id: `${scope}.whatsappAccount`,
|
|
371
|
+
defaultMessage: 'Whatsapp account',
|
|
372
|
+
},
|
|
349
373
|
"all": {
|
|
350
374
|
id: `${scope}.all`,
|
|
351
375
|
defaultMessage: 'All',
|
|
@@ -390,4 +414,12 @@ export default defineMessages({
|
|
|
390
414
|
id: `${scope}.uploadTemplate`,
|
|
391
415
|
defaultMessage: 'Upload template',
|
|
392
416
|
},
|
|
417
|
+
"clearAll": {
|
|
418
|
+
id: `${scope}.clearAll`,
|
|
419
|
+
defaultMessage: 'Clear all',
|
|
420
|
+
},
|
|
421
|
+
"whatsappMaxTemplates": {
|
|
422
|
+
id: `${scope}.whatsappMaxTemplates`,
|
|
423
|
+
defaultMessage: 'WhatsApp allows {maxCount} approved templates. To accommodate new approved templates either delete the existing one or reach out to Capillary POC to increase the limit.',
|
|
424
|
+
},
|
|
393
425
|
});
|
|
@@ -85,6 +85,8 @@ function templatesReducer(state = initialState, action) {
|
|
|
85
85
|
return state.set('selectedLineAccount', fromJS(action.lineAccount));
|
|
86
86
|
case types.SET_VIBER_ACCOUNT:
|
|
87
87
|
return state.set('selectedViberAccount', fromJS(action.viberAccount));
|
|
88
|
+
case types.SET_WHATSAPP_ACCOUNT:
|
|
89
|
+
return state.set('selectedWhatsappAccount', fromJS(action.whatsappAccount));
|
|
88
90
|
case types.SET_FACEBOOK_ACCOUNT:
|
|
89
91
|
return state.set('selectedFacebookAccount', fromJS(action.faceBookAccount));
|
|
90
92
|
case types.RESET_ACCOUNT:
|
|
@@ -23,6 +23,7 @@ import Templates from '../Templates';
|
|
|
23
23
|
import CallTask from '../CallTask';
|
|
24
24
|
import Facebook from '../Facebook';
|
|
25
25
|
import Viber from '../Viber';
|
|
26
|
+
import Whatsapp from '../Whatsapp';
|
|
26
27
|
import Line from '../Line/Container';
|
|
27
28
|
import FTP from '../FTP';
|
|
28
29
|
import Gallery from '../Assets/Gallery';
|
|
@@ -60,6 +61,7 @@ export class TemplatesV2 extends React.Component { // eslint-disable-line react/
|
|
|
60
61
|
line: {content: <></>, tab: intl.formatMessage(messages.line), key: 'line'},
|
|
61
62
|
viber: {content: <></>, tab: intl.formatMessage(messages.viber), key: 'viber'},
|
|
62
63
|
facebook: {content: <div></div>, tab: intl.formatMessage(messages.facebook), key: 'facebook'},
|
|
64
|
+
whatsapp: { content: <></>, tab: intl.formatMessage(messages.whatsapp), key: 'whatsapp' },
|
|
63
65
|
};
|
|
64
66
|
let filteredPanes = Object.keys(defaultPanes)
|
|
65
67
|
.filter((key) => !channelsToHide.includes(key)).reduce((obj = [], key) => {
|
|
@@ -54,6 +54,10 @@ export default defineMessages({
|
|
|
54
54
|
id: 'creatives.containersV2.TemplatesV2.viber',
|
|
55
55
|
defaultMessage: 'Viber',
|
|
56
56
|
},
|
|
57
|
+
whatsapp: {
|
|
58
|
+
id: 'creatives.containersV2.TemplatesV2.whatsapp',
|
|
59
|
+
defaultMessage: 'WhatsApp',
|
|
60
|
+
},
|
|
57
61
|
gallery: {
|
|
58
62
|
id: 'creatives.containersV2.TemplatesV2.gallery',
|
|
59
63
|
defaultMessage: 'Gallery',
|