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:
Tautvidas Sipavičius
2017-04-26 14:30:51 +03:00
committed by GitHub
18 changed files with 436 additions and 75 deletions

View File

@@ -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

View File

@@ -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>

View 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;

View 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;

View File

@@ -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>
);
}
} }
} }

View File

@@ -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') }>

View File

@@ -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') }>

View File

@@ -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') }>

View File

@@ -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
View 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}
);
}
};
});

View File

@@ -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')
)); ));
} }

View File

@@ -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);

View File

@@ -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;

View File

@@ -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",

View File

@@ -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);
} }
} }

View File

@@ -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') %>

View File

@@ -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'),

View File

@@ -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',