Merge pull request #864 from mailpoet/campaign_stats
Add detailed stats page support in Free, change stats style [PREMIUM-1] [MAILPOET-877]
This commit is contained in:
@@ -1,3 +1,52 @@
|
|||||||
|
$excellent-badge-color = #2993ab
|
||||||
|
$good-badge-color = #f0b849
|
||||||
|
$bad-badge-color = #d54e21
|
||||||
|
$green-badge-color = #55bd56
|
||||||
|
|
||||||
#newsletters_container
|
#newsletters_container
|
||||||
h2.nav-tab-wrapper
|
h2.nav-tab-wrapper
|
||||||
margin-bottom: 1rem
|
margin-bottom: 1rem
|
||||||
|
|
||||||
|
.mailpoet_stats_text
|
||||||
|
font-size: 14px
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
.mailpoet_stat
|
||||||
|
|
||||||
|
&_excellent
|
||||||
|
color: $excellent-badge-color
|
||||||
|
|
||||||
|
&_good
|
||||||
|
color: $good-badge-color
|
||||||
|
|
||||||
|
&_bad
|
||||||
|
color: $bad-badge-color
|
||||||
|
|
||||||
|
&_hidden
|
||||||
|
display: none
|
||||||
|
|
||||||
|
&_link_small
|
||||||
|
text-decoration: underline !important
|
||||||
|
font-size: 0.75rem
|
||||||
|
|
||||||
|
.mailpoet_badge
|
||||||
|
padding: 4px 6px 3px 6px
|
||||||
|
color: #FFFFFF
|
||||||
|
margin-right: 4px
|
||||||
|
text-transform: uppercase
|
||||||
|
font-size: 0.5625rem
|
||||||
|
font-weight: 500
|
||||||
|
border-radius: 3px
|
||||||
|
letter-spacing: 1px
|
||||||
|
|
||||||
|
&_excellent
|
||||||
|
background: $excellent-badge-color
|
||||||
|
|
||||||
|
&_good
|
||||||
|
background: $good-badge-color
|
||||||
|
|
||||||
|
&_bad
|
||||||
|
background: $bad-badge-color
|
||||||
|
|
||||||
|
&_green
|
||||||
|
background: $green-badge-color
|
||||||
|
@@ -209,8 +209,8 @@ const ListingItems = React.createClass({
|
|||||||
className="colspanchange">
|
className="colspanchange">
|
||||||
{
|
{
|
||||||
(this.props.loading === true)
|
(this.props.loading === true)
|
||||||
? MailPoet.I18n.t('loadingItems')
|
? (this.props.messages.onLoadingItems || MailPoet.I18n.t('loadingItems'))
|
||||||
: MailPoet.I18n.t('noItemsFound')
|
: (this.props.messages.onNoItemsFound || MailPoet.I18n.t('noItemsFound'))
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -793,6 +793,12 @@ const Listing = React.createClass({
|
|||||||
groups = false;
|
groups = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// messages
|
||||||
|
let messages = {};
|
||||||
|
if (this.props.messages !== undefined) {
|
||||||
|
messages = this.props.messages;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{ groups }
|
{ groups }
|
||||||
@@ -846,6 +852,7 @@ const Listing = React.createClass({
|
|||||||
count={ this.state.count }
|
count={ this.state.count }
|
||||||
limit={ this.state.limit }
|
limit={ this.state.limit }
|
||||||
item_actions={ item_actions }
|
item_actions={ item_actions }
|
||||||
|
messages={ messages }
|
||||||
items={ items } />
|
items={ items } />
|
||||||
|
|
||||||
<tfoot>
|
<tfoot>
|
||||||
|
37
assets/js/src/newsletters/badges/badge.jsx
Normal file
37
assets/js/src/newsletters/badges/badge.jsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
import ReactTooltip from 'react-tooltip'
|
||||||
|
|
||||||
|
class Badge extends React.Component {
|
||||||
|
render() {
|
||||||
|
const badgeClasses = classNames(
|
||||||
|
'mailpoet_badge',
|
||||||
|
this.props.type ? `mailpoet_badge_${this.props.type}` : ''
|
||||||
|
);
|
||||||
|
|
||||||
|
const tooltip = this.props.tooltip ? this.props.tooltip.replace(/\n/g, '<br />') : false;
|
||||||
|
// tooltip ID must be unique, defaults to tooltip text
|
||||||
|
const tooltipId = this.props.tooltipId || tooltip;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<span
|
||||||
|
className={badgeClasses}
|
||||||
|
data-tip={tooltip}
|
||||||
|
data-for={tooltipId}
|
||||||
|
>
|
||||||
|
{this.props.name}
|
||||||
|
</span>
|
||||||
|
{ tooltip && (
|
||||||
|
<ReactTooltip
|
||||||
|
place="right"
|
||||||
|
multiline={true}
|
||||||
|
id={tooltipId}
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Badge;
|
105
assets/js/src/newsletters/badges/stats.jsx
Normal file
105
assets/js/src/newsletters/badges/stats.jsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import MailPoet from 'mailpoet'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import Badge from './badge.jsx'
|
||||||
|
|
||||||
|
const badges = {
|
||||||
|
excellent: {
|
||||||
|
name: MailPoet.I18n.t('excellentBadgeName'),
|
||||||
|
tooltipTitle: MailPoet.I18n.t('excellentBadgeTooltip')
|
||||||
|
},
|
||||||
|
good: {
|
||||||
|
name: MailPoet.I18n.t('goodBadgeName'),
|
||||||
|
tooltipTitle: MailPoet.I18n.t('goodBadgeTooltip')
|
||||||
|
},
|
||||||
|
bad: {
|
||||||
|
name: MailPoet.I18n.t('badBadgeName'),
|
||||||
|
tooltipTitle: MailPoet.I18n.t('badBadgeTooltip')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stats = {
|
||||||
|
opened: {
|
||||||
|
badgeRanges: [30, 15, 0],
|
||||||
|
badgeTypes: [
|
||||||
|
'excellent',
|
||||||
|
'good',
|
||||||
|
'bad'
|
||||||
|
],
|
||||||
|
tooltipText: MailPoet.I18n.t('openedStatTooltip'),
|
||||||
|
},
|
||||||
|
clicked: {
|
||||||
|
badgeRanges: [3, 1, 0],
|
||||||
|
badgeTypes: [
|
||||||
|
'excellent',
|
||||||
|
'good',
|
||||||
|
'bad'
|
||||||
|
],
|
||||||
|
tooltipText: MailPoet.I18n.t('clickedStatTooltip')
|
||||||
|
},
|
||||||
|
unsubscribed: {
|
||||||
|
badgeRanges: [3, 1, 0],
|
||||||
|
badgeTypes: [
|
||||||
|
'bad',
|
||||||
|
'good',
|
||||||
|
'excellent'
|
||||||
|
],
|
||||||
|
tooltipText: MailPoet.I18n.t('unsubscribedStatTooltip')
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
class StatsBadge extends React.Component {
|
||||||
|
getBadgeType(stat, rate) {
|
||||||
|
const len = stat.badgeRanges.length;
|
||||||
|
for (var i = 0; i < len; i++) {
|
||||||
|
if (rate > stat.badgeRanges[i]) {
|
||||||
|
return stat.badgeTypes[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// rate must be zero at this point
|
||||||
|
return stat.badgeTypes[len - 1];
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
const stat = stats[this.props.stat] || null;
|
||||||
|
if (!stat) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rate = this.props.rate;
|
||||||
|
if (rate < 0 || rate > 100) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const badgeType = this.getBadgeType(stat, rate);
|
||||||
|
const badge = badges[badgeType] || null;
|
||||||
|
if (!badge) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tooltipText = `${badge.tooltipTitle}\n\n${stat.tooltipText}`;
|
||||||
|
const tooltipId = this.props.tooltipId || null;
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<Badge
|
||||||
|
type={badgeType}
|
||||||
|
name={badge.name}
|
||||||
|
tooltip={tooltipText}
|
||||||
|
tooltipId={tooltipId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.props.headline) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span className={`mailpoet_stat_${badgeType}`}>
|
||||||
|
{this.props.headline}
|
||||||
|
</span> {content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StatsBadge;
|
@@ -1,9 +1,13 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import ReactStringReplace from 'react-string-replace'
|
import ReactStringReplace from 'react-string-replace'
|
||||||
|
import { Link } from 'react-router'
|
||||||
import MailPoet from 'mailpoet'
|
import MailPoet from 'mailpoet'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
import moment from 'moment'
|
||||||
import jQuery from 'jquery'
|
import jQuery from 'jquery'
|
||||||
|
import Hooks from 'wp-js-hooks'
|
||||||
|
import StatsBadge from 'newsletters/badges/stats.jsx'
|
||||||
|
|
||||||
const _QueueMixin = {
|
const _QueueMixin = {
|
||||||
pauseSending: function(newsletter) {
|
pauseSending: function(newsletter) {
|
||||||
@@ -140,40 +144,162 @@ const _QueueMixin = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const _StatisticsMixin = {
|
const _StatisticsMixin = {
|
||||||
renderStatistics: function(newsletter) {
|
renderStatistics: function(newsletter, is_sent, current_time) {
|
||||||
if (
|
if (is_sent === undefined) {
|
||||||
newsletter.statistics
|
// condition for standard and post notification listings
|
||||||
|
is_sent = newsletter.statistics
|
||||||
&& newsletter.queue
|
&& newsletter.queue
|
||||||
&& newsletter.queue.status !== 'scheduled'
|
&& newsletter.queue.status !== 'scheduled'
|
||||||
) {
|
}
|
||||||
const total_sent = ~~(newsletter.queue.count_processed);
|
if (!is_sent) {
|
||||||
|
return (
|
||||||
|
<span>{MailPoet.I18n.t('notSentYet')}</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let params = {};
|
||||||
|
params = Hooks.applyFilters('mailpoet_newsletters_listing_stats_before', params, newsletter);
|
||||||
|
|
||||||
|
// welcome emails provide explicit total_sent value
|
||||||
|
const total_sent = ~~(newsletter.total_sent || newsletter.queue.count_processed);
|
||||||
|
|
||||||
let percentage_clicked = 0;
|
let percentage_clicked = 0;
|
||||||
let percentage_opened = 0;
|
let percentage_opened = 0;
|
||||||
let percentage_unsubscribed = 0;
|
let percentage_unsubscribed = 0;
|
||||||
|
|
||||||
if (total_sent > 0) {
|
if (total_sent > 0) {
|
||||||
percentage_clicked = Math.round(
|
percentage_clicked = (newsletter.statistics.clicked * 100) / total_sent;
|
||||||
(~~(newsletter.statistics.clicked) * 100) / total_sent
|
percentage_opened = (newsletter.statistics.opened * 100) / total_sent;
|
||||||
|
percentage_unsubscribed = (newsletter.statistics.unsubscribed * 100) / total_sent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// format to 1 decimal place
|
||||||
|
const percentage_clicked_display = MailPoet.Num.toLocaleFixed(percentage_clicked, 1);
|
||||||
|
const percentage_opened_display = MailPoet.Num.toLocaleFixed(percentage_opened, 1);
|
||||||
|
const percentage_unsubscribed_display = MailPoet.Num.toLocaleFixed(percentage_unsubscribed, 1);
|
||||||
|
|
||||||
|
let show_stats_timeout,
|
||||||
|
newsletter_date,
|
||||||
|
sent_hours_ago,
|
||||||
|
too_early_for_stats,
|
||||||
|
show_kb_link;
|
||||||
|
if (current_time !== undefined) {
|
||||||
|
// standard emails and post notifications:
|
||||||
|
// display green box for newsletters that were just sent
|
||||||
|
show_stats_timeout = 6; // in hours
|
||||||
|
newsletter_date = newsletter.queue.scheduled_at || newsletter.queue.created_at;
|
||||||
|
sent_hours_ago = moment(current_time).diff(moment(newsletter_date), 'hours');
|
||||||
|
too_early_for_stats = sent_hours_ago < show_stats_timeout;
|
||||||
|
show_kb_link = true;
|
||||||
|
} else {
|
||||||
|
// welcome emails: no green box and KB link
|
||||||
|
too_early_for_stats = false;
|
||||||
|
show_kb_link = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const improveStatsKBLink = 'http://beta.docs.mailpoet.com/article/191-how-to-improve-my-open-and-click-rates';
|
||||||
|
|
||||||
|
// thresholds to display badges
|
||||||
|
const min_newsletters_sent = 20;
|
||||||
|
const min_newsletter_opens = 5;
|
||||||
|
|
||||||
|
let content;
|
||||||
|
if (total_sent >= min_newsletters_sent
|
||||||
|
&& newsletter.statistics.opened >= min_newsletter_opens
|
||||||
|
&& !too_early_for_stats
|
||||||
|
) {
|
||||||
|
// display stats with badges
|
||||||
|
content = (
|
||||||
|
<div className="mailpoet_stats_text">
|
||||||
|
<div>
|
||||||
|
<span>{ percentage_opened_display }% </span>
|
||||||
|
<StatsBadge
|
||||||
|
stat="opened"
|
||||||
|
rate={percentage_opened}
|
||||||
|
tooltipId={`opened-${newsletter.id}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>{ percentage_clicked_display }% </span>
|
||||||
|
<StatsBadge
|
||||||
|
stat="clicked"
|
||||||
|
rate={percentage_clicked}
|
||||||
|
tooltipId={`clicked-${newsletter.id}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="mailpoet_stat_hidden">{ percentage_unsubscribed_display }%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
percentage_opened = Math.round(
|
} else {
|
||||||
(~~(newsletter.statistics.opened) * 100) / total_sent
|
// display simple stats
|
||||||
|
content = (
|
||||||
|
<div>
|
||||||
|
<span className="mailpoet_stats_text">
|
||||||
|
{ percentage_opened_display }%,
|
||||||
|
{ " " }
|
||||||
|
{ percentage_clicked_display }%
|
||||||
|
<span className="mailpoet_stat_hidden">
|
||||||
|
, { percentage_unsubscribed_display }%
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
{ too_early_for_stats && (
|
||||||
|
<div className="mailpoet_badge mailpoet_badge_green">
|
||||||
|
{MailPoet.I18n.t('checkBackInHours')
|
||||||
|
.replace('%$1d', show_stats_timeout - sent_hours_ago)}
|
||||||
|
</div>
|
||||||
|
) }
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
percentage_unsubscribed = Math.round(
|
}
|
||||||
(~~(newsletter.statistics.unsubscribed) * 100) / total_sent
|
|
||||||
|
// thresholds to display bad open rate help
|
||||||
|
const max_percentage_opened = 5;
|
||||||
|
const min_sent_hours_ago = 24;
|
||||||
|
const min_total_sent = 10;
|
||||||
|
|
||||||
|
let after_content;
|
||||||
|
if (show_kb_link
|
||||||
|
&& percentage_opened < max_percentage_opened
|
||||||
|
&& sent_hours_ago >= min_sent_hours_ago
|
||||||
|
&& total_sent >= min_total_sent
|
||||||
|
) {
|
||||||
|
// help link for bad open rate
|
||||||
|
after_content = (
|
||||||
|
<div>
|
||||||
|
<a
|
||||||
|
href={improveStatsKBLink}
|
||||||
|
target="_blank"
|
||||||
|
className="mailpoet_stat_link_small"
|
||||||
|
>
|
||||||
|
{MailPoet.I18n.t('improveThisLinkText')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (total_sent > 0 && !too_early_for_stats && params.link) {
|
||||||
|
// wrap content in a link
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Link
|
||||||
|
key={ `stats-${newsletter.id}` }
|
||||||
|
to={ params.link }
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</Link>
|
||||||
|
{after_content}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span>
|
<div>
|
||||||
{ percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }%
|
{content}
|
||||||
</span>
|
{after_content}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<span>{MailPoet.I18n.t('notSentYet')}</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -87,7 +87,7 @@ const NewsletterListNotificationHistory = React.createClass({
|
|||||||
</td>
|
</td>
|
||||||
{ (mailpoet_tracking_enabled === true) ? (
|
{ (mailpoet_tracking_enabled === true) ? (
|
||||||
<td className="column" data-colname={ MailPoet.I18n.t('statistics') }>
|
<td className="column" data-colname={ MailPoet.I18n.t('statistics') }>
|
||||||
{ this.renderStatistics(newsletter) }
|
{ this.renderStatistics(newsletter, undefined, meta.current_time) }
|
||||||
</td>
|
</td>
|
||||||
) : null }
|
) : null }
|
||||||
<td className="column-date" data-colname={ MailPoet.I18n.t('lastModifiedOn') }>
|
<td className="column-date" data-colname={ MailPoet.I18n.t('lastModifiedOn') }>
|
||||||
|
@@ -184,7 +184,7 @@ const NewsletterListStandard = React.createClass({
|
|||||||
</td>
|
</td>
|
||||||
{ (mailpoet_tracking_enabled === true) ? (
|
{ (mailpoet_tracking_enabled === true) ? (
|
||||||
<td className="column" data-colname={ MailPoet.I18n.t('statistics') }>
|
<td className="column" data-colname={ MailPoet.I18n.t('statistics') }>
|
||||||
{ this.renderStatistics(newsletter) }
|
{ this.renderStatistics(newsletter, undefined, meta.current_time) }
|
||||||
</td>
|
</td>
|
||||||
) : null }
|
) : null }
|
||||||
<td className="column-date" data-colname={ MailPoet.I18n.t('lastModifiedOn') }>
|
<td className="column-date" data-colname={ MailPoet.I18n.t('lastModifiedOn') }>
|
||||||
|
@@ -5,12 +5,13 @@ import { createHashHistory } from 'history'
|
|||||||
import Listing from 'listing/listing.jsx'
|
import Listing from 'listing/listing.jsx'
|
||||||
import ListingTabs from 'newsletters/listings/tabs.jsx'
|
import ListingTabs from 'newsletters/listings/tabs.jsx'
|
||||||
|
|
||||||
import { MailerMixin } from 'newsletters/listings/mixins.jsx'
|
import { StatisticsMixin, MailerMixin } from 'newsletters/listings/mixins.jsx'
|
||||||
|
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import jQuery from 'jquery'
|
import jQuery from 'jquery'
|
||||||
import MailPoet from 'mailpoet'
|
import MailPoet from 'mailpoet'
|
||||||
import _ from 'underscore'
|
import _ from 'underscore'
|
||||||
|
import Hooks from 'wp-js-hooks'
|
||||||
|
|
||||||
const mailpoet_roles = window.mailpoet_roles || {};
|
const mailpoet_roles = window.mailpoet_roles || {};
|
||||||
const mailpoet_segments = window.mailpoet_segments || {};
|
const mailpoet_segments = window.mailpoet_segments || {};
|
||||||
@@ -155,7 +156,7 @@ const newsletter_actions = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const NewsletterListWelcome = React.createClass({
|
const NewsletterListWelcome = React.createClass({
|
||||||
mixins: [ MailerMixin ],
|
mixins: [ StatisticsMixin, MailerMixin ],
|
||||||
updateStatus: function(e) {
|
updateStatus: function(e) {
|
||||||
// make the event persist so that we can still override the selected value
|
// make the event persist so that we can still override the selected value
|
||||||
// in the ajax callback
|
// in the ajax callback
|
||||||
@@ -276,35 +277,6 @@ const NewsletterListWelcome = React.createClass({
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
renderStatistics: function(newsletter) {
|
|
||||||
if (mailpoet_tracking_enabled === false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newsletter.total_sent > 0 && newsletter.statistics) {
|
|
||||||
const total_sent = ~~(newsletter.total_sent);
|
|
||||||
|
|
||||||
const percentage_clicked = Math.round(
|
|
||||||
(~~(newsletter.statistics.clicked) * 100) / total_sent
|
|
||||||
);
|
|
||||||
const percentage_opened = Math.round(
|
|
||||||
(~~(newsletter.statistics.opened) * 100) / total_sent
|
|
||||||
);
|
|
||||||
const percentage_unsubscribed = Math.round(
|
|
||||||
(~~(newsletter.statistics.unsubscribed) * 100) / total_sent
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
{ percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }%
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<span>{MailPoet.I18n.t('notSentYet')}</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
renderItem: function(newsletter, actions) {
|
renderItem: function(newsletter, actions) {
|
||||||
const rowClasses = classNames(
|
const rowClasses = classNames(
|
||||||
'manage-column',
|
'manage-column',
|
||||||
@@ -331,7 +303,10 @@ const NewsletterListWelcome = React.createClass({
|
|||||||
</td>
|
</td>
|
||||||
{ (mailpoet_tracking_enabled === true) ? (
|
{ (mailpoet_tracking_enabled === true) ? (
|
||||||
<td className="column" data-colname={ MailPoet.I18n.t('statistics') }>
|
<td className="column" data-colname={ MailPoet.I18n.t('statistics') }>
|
||||||
{ this.renderStatistics(newsletter) }
|
{ this.renderStatistics(
|
||||||
|
newsletter,
|
||||||
|
newsletter.total_sent > 0 && newsletter.statistics
|
||||||
|
) }
|
||||||
</td>
|
</td>
|
||||||
) : null }
|
) : null }
|
||||||
<td className="column-date" data-colname={ MailPoet.I18n.t('lastModifiedOn') }>
|
<td className="column-date" data-colname={ MailPoet.I18n.t('lastModifiedOn') }>
|
||||||
|
@@ -2,6 +2,7 @@ import React from 'react'
|
|||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import { Router, Route, IndexRedirect, Link, useRouterHistory } from 'react-router'
|
import { Router, Route, IndexRedirect, Link, useRouterHistory } from 'react-router'
|
||||||
import { createHashHistory } from 'history'
|
import { createHashHistory } from 'history'
|
||||||
|
import Hooks from 'wp-js-hooks'
|
||||||
|
|
||||||
import NewsletterTypes from 'newsletters/types.jsx'
|
import NewsletterTypes from 'newsletters/types.jsx'
|
||||||
import NewsletterTemplates from 'newsletters/templates.jsx'
|
import NewsletterTemplates from 'newsletters/templates.jsx'
|
||||||
@@ -27,6 +28,9 @@ const App = React.createClass({
|
|||||||
const container = document.getElementById('newsletters_container');
|
const container = document.getElementById('newsletters_container');
|
||||||
|
|
||||||
if(container) {
|
if(container) {
|
||||||
|
let extra_routes = [];
|
||||||
|
extra_routes = Hooks.applyFilters('mailpoet_newsletters_before_router', extra_routes);
|
||||||
|
|
||||||
const mailpoet_listing = ReactDOM.render((
|
const mailpoet_listing = ReactDOM.render((
|
||||||
<Router history={ history }>
|
<Router history={ history }>
|
||||||
<Route path="/" component={ App }>
|
<Route path="/" component={ App }>
|
||||||
@@ -46,6 +50,8 @@ if(container) {
|
|||||||
<Route name="template" path="template/:id" component={ NewsletterTemplates } />
|
<Route name="template" path="template/:id" component={ NewsletterTemplates } />
|
||||||
{/* Sending options */}
|
{/* Sending options */}
|
||||||
<Route path="send/:id" component={ NewsletterSend } />
|
<Route path="send/:id" component={ NewsletterSend } />
|
||||||
|
{/* Extra routes */}
|
||||||
|
{ extra_routes.map(rt => <Route key={rt.path} path={rt.path} component={rt.component} />) }
|
||||||
</Route>
|
</Route>
|
||||||
</Router>
|
</Router>
|
||||||
), container);
|
), container);
|
||||||
|
21
assets/js/src/num.js
Normal file
21
assets/js/src/num.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
define('num',
|
||||||
|
[
|
||||||
|
'mailpoet'
|
||||||
|
], function(
|
||||||
|
MailPoet
|
||||||
|
) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
MailPoet.Num = {
|
||||||
|
toLocaleFixed: function (num, precision) {
|
||||||
|
precision = precision || 0;
|
||||||
|
var factor = Math.pow(10, precision);
|
||||||
|
return (Math.round(num * factor) / factor)
|
||||||
|
.toLocaleString(
|
||||||
|
undefined,
|
||||||
|
{minimumFractionDigits: precision, maximumFractionDigits: precision}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
@@ -372,7 +372,8 @@ class Newsletters extends APIEndpoint {
|
|||||||
'filters' => $listing_data['filters'],
|
'filters' => $listing_data['filters'],
|
||||||
'groups' => $listing_data['groups'],
|
'groups' => $listing_data['groups'],
|
||||||
'mta_log' => Setting::getValue('mta_log'),
|
'mta_log' => Setting::getValue('mta_log'),
|
||||||
'mta_method' => Setting::getValue('mta.method')
|
'mta_method' => Setting::getValue('mta.method'),
|
||||||
|
'current_time' => current_time('mysql')
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -300,7 +300,8 @@ class Migrator {
|
|||||||
'subscriber_id mediumint(9) NOT NULL,',
|
'subscriber_id mediumint(9) NOT NULL,',
|
||||||
'queue_id mediumint(9) NOT NULL,',
|
'queue_id mediumint(9) NOT NULL,',
|
||||||
'sent_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
|
'sent_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
|
||||||
'PRIMARY KEY (id)',
|
'PRIMARY KEY (id),',
|
||||||
|
'KEY newsletter_id (newsletter_id)',
|
||||||
);
|
);
|
||||||
return $this->sqlify(__FUNCTION__, $attributes);
|
return $this->sqlify(__FUNCTION__, $attributes);
|
||||||
}
|
}
|
||||||
@@ -316,6 +317,7 @@ class Migrator {
|
|||||||
'created_at TIMESTAMP NULL,',
|
'created_at TIMESTAMP NULL,',
|
||||||
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
|
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
|
||||||
'PRIMARY KEY (id),',
|
'PRIMARY KEY (id),',
|
||||||
|
'KEY newsletter_id (newsletter_id),',
|
||||||
'KEY queue_id (queue_id)',
|
'KEY queue_id (queue_id)',
|
||||||
);
|
);
|
||||||
return $this->sqlify(__FUNCTION__, $attributes);
|
return $this->sqlify(__FUNCTION__, $attributes);
|
||||||
@@ -329,6 +331,7 @@ class Migrator {
|
|||||||
'queue_id mediumint(9) NOT NULL,',
|
'queue_id mediumint(9) NOT NULL,',
|
||||||
'created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,',
|
'created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,',
|
||||||
'PRIMARY KEY (id),',
|
'PRIMARY KEY (id),',
|
||||||
|
'KEY newsletter_id (newsletter_id),',
|
||||||
'KEY queue_id (queue_id)',
|
'KEY queue_id (queue_id)',
|
||||||
);
|
);
|
||||||
return $this->sqlify(__FUNCTION__, $attributes);
|
return $this->sqlify(__FUNCTION__, $attributes);
|
||||||
@@ -342,6 +345,7 @@ class Migrator {
|
|||||||
'queue_id mediumint(9) NOT NULL,',
|
'queue_id mediumint(9) NOT NULL,',
|
||||||
'created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,',
|
'created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,',
|
||||||
'PRIMARY KEY (id),',
|
'PRIMARY KEY (id),',
|
||||||
|
'KEY newsletter_id (newsletter_id),',
|
||||||
'KEY queue_id (queue_id)',
|
'KEY queue_id (queue_id)',
|
||||||
);
|
);
|
||||||
return $this->sqlify(__FUNCTION__, $attributes);
|
return $this->sqlify(__FUNCTION__, $attributes);
|
||||||
|
@@ -264,7 +264,8 @@ class Newsletter extends Model {
|
|||||||
'status',
|
'status',
|
||||||
'count_processed',
|
'count_processed',
|
||||||
'count_total',
|
'count_total',
|
||||||
'scheduled_at'
|
'scheduled_at',
|
||||||
|
'created_at'
|
||||||
));
|
));
|
||||||
if($queue === false) {
|
if($queue === false) {
|
||||||
$this->queue = false;
|
$this->queue = false;
|
||||||
|
@@ -27,6 +27,7 @@
|
|||||||
"react-dom": "~15.4.2",
|
"react-dom": "~15.4.2",
|
||||||
"react-router": "~3.0.2",
|
"react-router": "~3.0.2",
|
||||||
"react-string-replace": "^0.3.2",
|
"react-string-replace": "^0.3.2",
|
||||||
|
"react-tooltip": "^3.2.10",
|
||||||
"select2": "^4.0.0",
|
"select2": "^4.0.0",
|
||||||
"spectrum-colorpicker": "^1.6.2",
|
"spectrum-colorpicker": "^1.6.2",
|
||||||
"tinymce": "4.5.6",
|
"tinymce": "4.5.6",
|
||||||
|
@@ -3,7 +3,6 @@
|
|||||||
use Codeception\Util\Stub;
|
use Codeception\Util\Stub;
|
||||||
use MailPoet\Models\Newsletter;
|
use MailPoet\Models\Newsletter;
|
||||||
use MailPoet\Models\SendingQueue;
|
use MailPoet\Models\SendingQueue;
|
||||||
use MailPoet\Models\StatisticsClicks;
|
|
||||||
use MailPoet\Models\StatisticsOpens;
|
use MailPoet\Models\StatisticsOpens;
|
||||||
use MailPoet\Models\Subscriber;
|
use MailPoet\Models\Subscriber;
|
||||||
use MailPoet\Statistics\Track\Opens;
|
use MailPoet\Statistics\Track\Opens;
|
||||||
@@ -90,6 +89,5 @@ class OpensTest extends MailPoetTest {
|
|||||||
ORM::raw_execute('TRUNCATE ' . Subscriber::$_table);
|
ORM::raw_execute('TRUNCATE ' . Subscriber::$_table);
|
||||||
ORM::raw_execute('TRUNCATE ' . SendingQueue::$_table);
|
ORM::raw_execute('TRUNCATE ' . SendingQueue::$_table);
|
||||||
ORM::raw_execute('TRUNCATE ' . StatisticsOpens::$_table);
|
ORM::raw_execute('TRUNCATE ' . StatisticsOpens::$_table);
|
||||||
ORM::raw_execute('TRUNCATE ' . StatisticsClicks::$_table);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -31,6 +31,9 @@ jQuery('.toplevel_page_mailpoet-newsletters.menu-top-last')
|
|||||||
<%= stylesheet(
|
<%= stylesheet(
|
||||||
'admin.css'
|
'admin.css'
|
||||||
)%>
|
)%>
|
||||||
|
|
||||||
|
<%= do_action('mailpoet_styles_admin_after') %>
|
||||||
|
|
||||||
<!-- rtl specific stylesheet -->
|
<!-- rtl specific stylesheet -->
|
||||||
<% if is_rtl %>
|
<% if is_rtl %>
|
||||||
<%= stylesheet('rtl.css') %>
|
<%= stylesheet('rtl.css') %>
|
||||||
|
@@ -60,7 +60,7 @@
|
|||||||
|
|
||||||
'subject': __('Subject'),
|
'subject': __('Subject'),
|
||||||
'status': __('Status'),
|
'status': __('Status'),
|
||||||
'statistics': __('Opened, Clicked, Unsubscribed'),
|
'statistics': __('Opened, Clicked'),
|
||||||
'lists': __('Lists'),
|
'lists': __('Lists'),
|
||||||
'settings': __('Settings'),
|
'settings': __('Settings'),
|
||||||
'history': __('History'),
|
'history': __('History'),
|
||||||
@@ -90,6 +90,18 @@
|
|||||||
'paused': __('Paused'),
|
'paused': __('Paused'),
|
||||||
'new': __('Add New'),
|
'new': __('Add New'),
|
||||||
|
|
||||||
|
'excellentBadgeName': __('Excellent'),
|
||||||
|
'excellentBadgeTooltip': __('Congrats!'),
|
||||||
|
'goodBadgeName': __('Good'),
|
||||||
|
'goodBadgeTooltip': __('Good stuff.'),
|
||||||
|
'badBadgeName': __('Bad'),
|
||||||
|
'badBadgeTooltip': __('Something to improve.'),
|
||||||
|
'openedStatTooltip': __('Above 30% is excellent.\\nBetween 15 and 30% is good.\\nUnder 15% is bad.'),
|
||||||
|
'clickedStatTooltip': __('Above 3% is excellent.\\nBetween 1 and 3% is good.\\nUnder 1% is bad.'),
|
||||||
|
'unsubscribedStatTooltip': __('Under 1% is excellent.\\nBetween 1 and 3% is good.\\nOver 3% is bad.'),
|
||||||
|
'checkBackInHours': __('Nice job! Check back in %$1d hour(s) for more stats.'),
|
||||||
|
'improveThisLinkText': __('What can I do to improve this?'),
|
||||||
|
|
||||||
'templateFileMalformedError': __('This template file appears to be damaged. Please try another one.'),
|
'templateFileMalformedError': __('This template file appears to be damaged. Please try another one.'),
|
||||||
'importTemplateTitle': __('Import a template'),
|
'importTemplateTitle': __('Import a template'),
|
||||||
'selectJsonFileToUpload': __('Select a .json file to upload'),
|
'selectJsonFileToUpload': __('Select a .json file to upload'),
|
||||||
|
@@ -65,6 +65,10 @@ baseConfig = {
|
|||||||
include: require.resolve('react'),
|
include: require.resolve('react'),
|
||||||
loader: 'expose-loader?' + globalPrefix + '.React',
|
loader: 'expose-loader?' + globalPrefix + '.React',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
include: require.resolve('react-router'),
|
||||||
|
loader: 'expose-loader?' + globalPrefix + '.ReactRouter',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
include: require.resolve('react-string-replace'),
|
include: require.resolve('react-string-replace'),
|
||||||
loader: 'expose-loader?' + globalPrefix + '.ReactStringReplace',
|
loader: 'expose-loader?' + globalPrefix + '.ReactStringReplace',
|
||||||
@@ -73,6 +77,14 @@ baseConfig = {
|
|||||||
test: /wp-js-hooks/i,
|
test: /wp-js-hooks/i,
|
||||||
loader: 'expose-loader?' + globalPrefix + '.Hooks!exports-loader?wp.hooks',
|
loader: 'expose-loader?' + globalPrefix + '.Hooks!exports-loader?wp.hooks',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
test: /listing.jsx/i,
|
||||||
|
loader: 'expose-loader?' + globalPrefix + '.Listing!babel-loader',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
include: path.resolve(__dirname, 'assets/js/src/newsletters/badges/stats.jsx'),
|
||||||
|
loader: 'expose-loader?' + globalPrefix + '.StatsBadge!babel-loader',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
include: /Blob.js$/,
|
include: /Blob.js$/,
|
||||||
loader: 'exports-loader?window.Blob',
|
loader: 'exports-loader?window.Blob',
|
||||||
@@ -117,6 +129,7 @@ config.push(_.extend({}, baseConfig, {
|
|||||||
'i18n',
|
'i18n',
|
||||||
'modal',
|
'modal',
|
||||||
'notice',
|
'notice',
|
||||||
|
'num',
|
||||||
'jquery.serialize_object',
|
'jquery.serialize_object',
|
||||||
'parsleyjs'
|
'parsleyjs'
|
||||||
],
|
],
|
||||||
@@ -124,7 +137,9 @@ config.push(_.extend({}, baseConfig, {
|
|||||||
'react',
|
'react',
|
||||||
'react-dom',
|
'react-dom',
|
||||||
'react-router',
|
'react-router',
|
||||||
'react-string-replace'
|
'react-string-replace',
|
||||||
|
'listing/listing.jsx',
|
||||||
|
'newsletters/badges/stats.jsx'
|
||||||
],
|
],
|
||||||
admin: [
|
admin: [
|
||||||
'subscribers/subscribers.jsx',
|
'subscribers/subscribers.jsx',
|
||||||
|
Reference in New Issue
Block a user