Create listings tab for re-engagement emails
[MAILPOET-3763]
This commit is contained in:
306
assets/js/src/newsletters/listings/re_engagement.jsx
Normal file
306
assets/js/src/newsletters/listings/re_engagement.jsx
Normal file
@ -0,0 +1,306 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
|
||||
import Toggle from 'common/form/toggle/toggle';
|
||||
import Listing from 'listing/listing.jsx';
|
||||
import Statistics from 'newsletters/listings/statistics.jsx';
|
||||
import {
|
||||
addStatsCTAAction,
|
||||
checkCronStatus,
|
||||
checkMailerStatus,
|
||||
} from 'newsletters/listings/utils.jsx';
|
||||
import NewsletterTypes from 'newsletters/types';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import MailPoet from 'mailpoet';
|
||||
|
||||
const mailpoetTrackingEnabled = (!!(window.mailpoet_tracking_enabled));
|
||||
|
||||
const messages = {
|
||||
onNoItemsFound: (group, search) => MailPoet.I18n.t(search ? 'noItemsFound' : 'emptyListing'),
|
||||
onTrash: (response) => {
|
||||
const count = Number(response.meta.count);
|
||||
let message = null;
|
||||
|
||||
if (count === 1) {
|
||||
message = (
|
||||
MailPoet.I18n.t('oneNewsletterTrashed')
|
||||
);
|
||||
} else {
|
||||
message = (
|
||||
MailPoet.I18n.t('multipleNewslettersTrashed')
|
||||
).replace('%$1d', count.toLocaleString());
|
||||
}
|
||||
MailPoet.Notice.success(message);
|
||||
},
|
||||
onDelete: (response) => {
|
||||
const count = Number(response.meta.count);
|
||||
let message = null;
|
||||
|
||||
if (count === 1) {
|
||||
message = (
|
||||
MailPoet.I18n.t('oneNewsletterDeleted')
|
||||
);
|
||||
} else {
|
||||
message = (
|
||||
MailPoet.I18n.t('multipleNewslettersDeleted')
|
||||
).replace('%$1d', count.toLocaleString());
|
||||
}
|
||||
MailPoet.Notice.success(message);
|
||||
},
|
||||
onRestore: (response) => {
|
||||
const count = Number(response.meta.count);
|
||||
let message = null;
|
||||
|
||||
if (count === 1) {
|
||||
message = (
|
||||
MailPoet.I18n.t('oneNewsletterRestored')
|
||||
);
|
||||
} else {
|
||||
message = (
|
||||
MailPoet.I18n.t('multipleNewslettersRestored')
|
||||
).replace('%$1d', count.toLocaleString());
|
||||
}
|
||||
MailPoet.Notice.success(message);
|
||||
},
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'subject',
|
||||
label: MailPoet.I18n.t('subject'),
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'statistics',
|
||||
label: MailPoet.I18n.t('statistics'),
|
||||
display: mailpoetTrackingEnabled,
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
label: MailPoet.I18n.t('status'),
|
||||
width: 145,
|
||||
},
|
||||
{
|
||||
name: 'updated_at',
|
||||
label: MailPoet.I18n.t('lastModifiedOn'),
|
||||
sortable: true,
|
||||
},
|
||||
];
|
||||
|
||||
const bulkActions = [
|
||||
{
|
||||
name: 'trash',
|
||||
label: MailPoet.I18n.t('moveToTrash'),
|
||||
onSuccess: messages.onTrash,
|
||||
},
|
||||
];
|
||||
|
||||
let newsletterActions = [
|
||||
{
|
||||
name: 'view',
|
||||
link: function link(newsletter) {
|
||||
return (
|
||||
<a href={newsletter.preview_url} target="_blank" rel="noopener noreferrer">
|
||||
{MailPoet.I18n.t('preview')}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'duplicate',
|
||||
className: 'mailpoet-hide-on-mobile',
|
||||
label: MailPoet.I18n.t('duplicate'),
|
||||
onClick: (newsletter, refresh) => MailPoet.Ajax.post({
|
||||
api_version: window.mailpoet_api_version,
|
||||
endpoint: 'newsletters',
|
||||
action: 'duplicate',
|
||||
data: {
|
||||
id: newsletter.id,
|
||||
},
|
||||
}).done((response) => {
|
||||
MailPoet.Notice.success((MailPoet.I18n.t('newsletterDuplicated')).replace('%$1s', response.data.subject));
|
||||
refresh();
|
||||
}).fail((response) => {
|
||||
if (response.errors.length > 0) {
|
||||
MailPoet.Notice.error(
|
||||
response.errors.map((error) => error.message),
|
||||
{ scroll: true }
|
||||
);
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'edit',
|
||||
className: 'mailpoet-hide-on-mobile',
|
||||
link: function link(newsletter) {
|
||||
return (
|
||||
<a href={`?page=mailpoet-newsletter-editor&id=${newsletter.id}`}>
|
||||
{MailPoet.I18n.t('edit')}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'trash',
|
||||
className: 'mailpoet-hide-on-mobile',
|
||||
},
|
||||
];
|
||||
newsletterActions = addStatsCTAAction(newsletterActions);
|
||||
|
||||
class NewsletterListReEngagement extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
newslettersCount: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
updateStatus = (checked, e) => {
|
||||
// make the event persist so that we can still override the selected value
|
||||
// in the ajax callback
|
||||
e.persist();
|
||||
|
||||
MailPoet.Ajax.post({
|
||||
api_version: window.mailpoet_api_version,
|
||||
endpoint: 'newsletters',
|
||||
action: 'setStatus',
|
||||
data: {
|
||||
id: Number(e.target.getAttribute('data-id')),
|
||||
status: checked ? 'active' : 'draft',
|
||||
},
|
||||
}).done((response) => {
|
||||
if (response.data.status === 'active') {
|
||||
MailPoet.Notice.success(MailPoet.I18n.t('reEngagementEmailActivated'));
|
||||
}
|
||||
// force refresh of listing so that groups are updated
|
||||
this.forceUpdate();
|
||||
}).fail((response) => {
|
||||
MailPoet.Notice.showApiErrorNotice(response);
|
||||
|
||||
// reset value to previous newsletter's status
|
||||
e.target.checked = !checked;
|
||||
});
|
||||
};
|
||||
|
||||
renderStatus = (newsletter) => {
|
||||
const totalSentMessage = MailPoet.I18n.t('sentToXSubscribers')
|
||||
.replace('%$1d', newsletter.total_sent.toLocaleString());
|
||||
const totalScheduledMessage = MailPoet.I18n.t('scheduledToXSubscribers')
|
||||
.replace('%$1d', newsletter.total_scheduled.toLocaleString());
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Toggle
|
||||
className="mailpoet-listing-status-toggle"
|
||||
onCheck={this.updateStatus}
|
||||
data-id={newsletter.id}
|
||||
dimension="small"
|
||||
defaultChecked={newsletter.status === 'active'}
|
||||
/>
|
||||
<p className="mailpoet-listing-stats-description">
|
||||
<Link
|
||||
to={`/sending-status/${newsletter.id}`}
|
||||
data-automation-id={`sending_status_${newsletter.id}`}
|
||||
>
|
||||
{ totalSentMessage }
|
||||
</Link>
|
||||
{' '}
|
||||
<br />
|
||||
{ totalScheduledMessage }
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderItem = (newsletter, actions) => {
|
||||
const rowClasses = classNames(
|
||||
'manage-column',
|
||||
'column-primary',
|
||||
'has-row-actions'
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<td className={rowClasses}>
|
||||
<a
|
||||
className="mailpoet-listing-title"
|
||||
href={`?page=mailpoet-newsletter-editor&id=${newsletter.id}`}
|
||||
>
|
||||
{ newsletter.subject }
|
||||
</a>
|
||||
{ actions }
|
||||
</td>
|
||||
{ (mailpoetTrackingEnabled === true) ? (
|
||||
<td className="column mailpoet-listing-stats-column" data-colname={MailPoet.I18n.t('statistics')}>
|
||||
<Statistics
|
||||
newsletter={newsletter}
|
||||
isSent={newsletter.total_sent > 0 && !!newsletter.statistics}
|
||||
/>
|
||||
</td>
|
||||
) : null }
|
||||
<td className="column" data-colname={MailPoet.I18n.t('status')}>
|
||||
{ this.renderStatus(newsletter) }
|
||||
</td>
|
||||
<td className="column-date mailpoet-hide-on-mobile" data-colname={MailPoet.I18n.t('lastModifiedOn')}>
|
||||
{ MailPoet.Date.short(newsletter.updated_at) }
|
||||
<br />
|
||||
{ MailPoet.Date.time(newsletter.updated_at) }
|
||||
</td>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
isItemInactive = (newsletter) => newsletter.status === 'draft';
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
{this.state.newslettersCount === 0 && (
|
||||
<NewsletterTypes
|
||||
filter={(type) => type.slug === 're_engagement'}
|
||||
hideScreenOptions={false}
|
||||
hideClosingButton
|
||||
/>
|
||||
)}
|
||||
{this.state.newslettersCount !== 0 && (
|
||||
<Listing
|
||||
limit={window.mailpoet_listing_per_page}
|
||||
location={this.props.location}
|
||||
params={this.props.match.params}
|
||||
endpoint="newsletters"
|
||||
type="re_engagement"
|
||||
base_url="re_engagement"
|
||||
onRenderItem={this.renderItem}
|
||||
isItemInactive={this.isItemInactive}
|
||||
columns={columns}
|
||||
bulk_actions={bulkActions}
|
||||
item_actions={newsletterActions}
|
||||
messages={messages}
|
||||
auto_refresh
|
||||
sort_by="updated_at"
|
||||
sort_order="desc"
|
||||
afterGetItems={(state) => {
|
||||
if (!state.loading) {
|
||||
const total = state.groups.reduce((count, group) => (count + group.count), 0);
|
||||
this.setState({ newslettersCount: total });
|
||||
}
|
||||
checkMailerStatus(state);
|
||||
checkCronStatus(state);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NewsletterListReEngagement.propTypes = {
|
||||
location: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
match: PropTypes.shape({
|
||||
params: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default withRouter(NewsletterListReEngagement);
|
@ -19,6 +19,7 @@ import EventsConditions from 'newsletters/automatic_emails/events_conditions.jsx
|
||||
import NewsletterListStandard from 'newsletters/listings/standard.jsx';
|
||||
import NewsletterListWelcome from 'newsletters/listings/welcome.jsx';
|
||||
import NewsletterListNotification from 'newsletters/listings/notification.jsx';
|
||||
import NewsletterListReEngagement from 'newsletters/listings/re_engagement.jsx';
|
||||
import NewsletterListNotificationHistory from 'newsletters/listings/notification_history.jsx';
|
||||
import NewsletterSendingStatus from 'newsletters/sending_status.jsx';
|
||||
import Listings from 'newsletters/automatic_emails/listings.jsx';
|
||||
@ -84,6 +85,14 @@ const Tabs = withNpsPoll(() => {
|
||||
: <NewsletterListNotification />
|
||||
}
|
||||
</Tab>
|
||||
<Tab
|
||||
key="re_engagement"
|
||||
route="re_engagement/(.*)?"
|
||||
title={MailPoet.I18n.t('tabReEngagementTitle')}
|
||||
automationId={`tab-${MailPoet.I18n.t('tabReEngagementTitle')}`}
|
||||
>
|
||||
<NewsletterListReEngagement />
|
||||
</Tab>
|
||||
{window.mailpoet_woocommerce_active && _.map(automaticEmails, (email) => (
|
||||
<Tab
|
||||
key={email.slug}
|
||||
@ -162,7 +171,7 @@ const routes = [
|
||||
component: Tabs,
|
||||
},
|
||||
{
|
||||
path: '/(standard|welcome|notification)/(.*)?',
|
||||
path: '/(standard|welcome|notification|re_engagement)/(.*)?',
|
||||
component: Tabs,
|
||||
},
|
||||
/* New newsletter: types */
|
||||
|
@ -146,7 +146,7 @@ class NewslettersResponseBuilder {
|
||||
if ($newsletter->getType() === NewsletterEntity::TYPE_STANDARD) {
|
||||
$data['segments'] = $this->buildSegments($newsletter);
|
||||
$data['queue'] = $latestQueue ? $this->buildQueue($latestQueue) : false; // false for BC
|
||||
} elseif (in_array($newsletter->getType(), [NewsletterEntity::TYPE_WELCOME, NewsletterEntity::TYPE_AUTOMATIC], true)) {
|
||||
} elseif (in_array($newsletter->getType(), [NewsletterEntity::TYPE_WELCOME, NewsletterEntity::TYPE_AUTOMATIC, NewsletterEntity::TYPE_RE_ENGAGEMENT], true)) {
|
||||
$data['segments'] = [];
|
||||
$data['options'] = $this->buildOptions($newsletter);
|
||||
$data['total_sent'] = $statistics ? $statistics->getTotalSentCount() : 0;
|
||||
|
@ -20,6 +20,7 @@ class NewsletterListingRepository extends ListingRepository {
|
||||
|
||||
private static $supportedTypes = [
|
||||
NewsletterEntity::TYPE_STANDARD,
|
||||
NewsletterEntity::TYPE_RE_ENGAGEMENT,
|
||||
NewsletterEntity::TYPE_WELCOME,
|
||||
NewsletterEntity::TYPE_AUTOMATIC,
|
||||
NewsletterEntity::TYPE_NOTIFICATION,
|
||||
@ -151,6 +152,7 @@ class NewsletterListingRepository extends ListingRepository {
|
||||
break;
|
||||
|
||||
case NewsletterEntity::TYPE_WELCOME:
|
||||
case NewsletterEntity::TYPE_RE_ENGAGEMENT:
|
||||
case NewsletterEntity::TYPE_NOTIFICATION:
|
||||
case NewsletterEntity::TYPE_AUTOMATIC:
|
||||
$groups = array_merge($groups, [
|
||||
|
@ -101,6 +101,7 @@
|
||||
'tabWelcomeTitle': __('Welcome Emails'),
|
||||
'tabNotificationTitle': __('Post Notifications'),
|
||||
'tabWoocommerceTitle': __('WooCommerce Emails'),
|
||||
'tabReEngagementTitle': __('Re-engagement Email'),
|
||||
'tabBlankTitle': __('Simple text'),
|
||||
|
||||
'reEngagementTextPre': __('After no activity for'),
|
||||
@ -308,6 +309,8 @@
|
||||
'newsletterInvalidFromAddress': _x('You need to authorize the email address <i>%$1s</i> to be able to send with it. [link]Authorize my email address[/link]', 'Users need to confirm that they own the email address they want to use to send their newsletter'),
|
||||
'welcomeEmailActivated': __('Your Welcome Email is now activated!'),
|
||||
'welcomeEmailActivationFailed': __('Your Welcome Email could not be activated, please check the settings.'),
|
||||
'reEngagementEmailActivated': __('Your ReEngagement Email is now activated!'),
|
||||
'reEngagementEmailActivationFailed': __('Your ReEngagement Email could not be activated, please check the settings.'),
|
||||
'postNotificationActivated': __('Your post notification is now active!'),
|
||||
'postNotificationActivationFailed': __('Your Post Notification could not be activated, check the settings.'),
|
||||
'welcomeEventSegment': __('Sent when someone subscribes to the list: "%$1s".'),
|
||||
|
Reference in New Issue
Block a user