From fbc0a3ad8d0f25f4f95f007401c73e7c5e8371a7 Mon Sep 17 00:00:00 2001 From: Alexey Stoletniy Date: Tue, 4 Apr 2017 15:37:16 +0300 Subject: [PATCH 1/8] Add detailed stats page support in Free [PREMIUM-1] --- assets/js/src/listing/listing.jsx | 11 ++++-- assets/js/src/newsletters/listings/mixins.jsx | 35 +++++++++++++------ .../js/src/newsletters/listings/welcome.jsx | 34 ++++++++++++------ assets/js/src/newsletters/newsletters.jsx | 6 ++++ lib/Config/Migrator.php | 6 +++- lib/Models/Newsletter.php | 3 +- tests/unit/Statistics/Track/OpensTest.php | 2 -- webpack.config.js | 13 +++++-- 8 files changed, 82 insertions(+), 28 deletions(-) diff --git a/assets/js/src/listing/listing.jsx b/assets/js/src/listing/listing.jsx index 2c28bf5cbb..3cf9b7799d 100644 --- a/assets/js/src/listing/listing.jsx +++ b/assets/js/src/listing/listing.jsx @@ -209,8 +209,8 @@ const ListingItems = React.createClass({ className="colspanchange"> { (this.props.loading === true) - ? MailPoet.I18n.t('loadingItems') - : MailPoet.I18n.t('noItemsFound') + ? (this.props.messages.onLoadingItems || MailPoet.I18n.t('loadingItems')) + : (this.props.messages.onNoItemsFound || MailPoet.I18n.t('noItemsFound')) } @@ -793,6 +793,12 @@ const Listing = React.createClass({ groups = false; } + // messages + let messages = {}; + if (this.props.messages !== undefined) { + messages = this.props.messages; + } + return (
{ groups } @@ -846,6 +852,7 @@ const Listing = React.createClass({ count={ this.state.count } limit={ this.state.limit } item_actions={ item_actions } + messages={ messages } items={ items } /> diff --git a/assets/js/src/newsletters/listings/mixins.jsx b/assets/js/src/newsletters/listings/mixins.jsx index 98555a8815..1249c52ade 100644 --- a/assets/js/src/newsletters/listings/mixins.jsx +++ b/assets/js/src/newsletters/listings/mixins.jsx @@ -1,9 +1,11 @@ import React from 'react' import ReactDOM from 'react-dom' import ReactStringReplace from 'react-string-replace' +import { Link } from 'react-router' import MailPoet from 'mailpoet' import classNames from 'classnames' import jQuery from 'jquery' +import Hooks from 'wp-js-hooks' const _QueueMixin = { pauseSending: function(newsletter) { @@ -146,6 +148,9 @@ const _StatisticsMixin = { && newsletter.queue && newsletter.queue.status !== 'scheduled' ) { + let params = {}; + params = Hooks.applyFilters('mailpoet_newsletters_listing_stats_before', params, newsletter); + const total_sent = ~~(newsletter.queue.count_processed); let percentage_clicked = 0; @@ -153,22 +158,32 @@ const _StatisticsMixin = { let percentage_unsubscribed = 0; if (total_sent > 0) { - percentage_clicked = Math.round( - (~~(newsletter.statistics.clicked) * 100) / total_sent - ); - percentage_opened = Math.round( - (~~(newsletter.statistics.opened) * 100) / total_sent - ); - percentage_unsubscribed = Math.round( - (~~(newsletter.statistics.unsubscribed) * 100) / total_sent - ); + percentage_clicked = (newsletter.statistics.clicked * 100) / total_sent; + percentage_opened = (newsletter.statistics.opened * 100) / total_sent; + percentage_unsubscribed = (newsletter.statistics.unsubscribed * 100) / total_sent; } - return ( + // format to 1 decimal place + percentage_clicked = percentage_clicked.toFixed(1); + percentage_opened = percentage_opened.toFixed(1); + percentage_unsubscribed = percentage_unsubscribed.toFixed(1); + + const content = ( { percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }% ); + + if (total_sent > 0 && params.link) { + return ( + { content } + ); + } + + return content; } else { return ( {MailPoet.I18n.t('notSentYet')} diff --git a/assets/js/src/newsletters/listings/welcome.jsx b/assets/js/src/newsletters/listings/welcome.jsx index ac4d269089..114aaa6d76 100644 --- a/assets/js/src/newsletters/listings/welcome.jsx +++ b/assets/js/src/newsletters/listings/welcome.jsx @@ -11,6 +11,7 @@ import classNames from 'classnames' import jQuery from 'jquery' import MailPoet from 'mailpoet' import _ from 'underscore' +import Hooks from 'wp-js-hooks' const mailpoet_roles = window.mailpoet_roles || {}; const mailpoet_segments = window.mailpoet_segments || {}; @@ -281,24 +282,37 @@ const NewsletterListWelcome = React.createClass({ return; } + let params = {}; + params = Hooks.applyFilters('mailpoet_newsletters_listing_stats_before', params, newsletter); + 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 - ); + let percentage_clicked = (newsletter.statistics.clicked * 100) / total_sent; + let percentage_opened = (newsletter.statistics.opened * 100) / total_sent; + let percentage_unsubscribed = (newsletter.statistics.unsubscribed * 100) / total_sent; - return ( + // format to 1 decimal place + percentage_clicked = percentage_clicked.toFixed(1); + percentage_opened = percentage_opened.toFixed(1); + percentage_unsubscribed = percentage_unsubscribed.toFixed(1); + + const content = ( { percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }% ); + + if (params.link) { + return ( + { content } + ); + } + + return content; } else { return ( {MailPoet.I18n.t('notSentYet')} diff --git a/assets/js/src/newsletters/newsletters.jsx b/assets/js/src/newsletters/newsletters.jsx index fd82973843..e03772c039 100644 --- a/assets/js/src/newsletters/newsletters.jsx +++ b/assets/js/src/newsletters/newsletters.jsx @@ -2,6 +2,7 @@ import React from 'react' import ReactDOM from 'react-dom' import { Router, Route, IndexRedirect, Link, useRouterHistory } from 'react-router' import { createHashHistory } from 'history' +import Hooks from 'wp-js-hooks' import NewsletterTypes from 'newsletters/types.jsx' import NewsletterTemplates from 'newsletters/templates.jsx' @@ -27,6 +28,9 @@ const App = React.createClass({ const container = document.getElementById('newsletters_container'); if(container) { + let extra_routes = []; + extra_routes = Hooks.applyFilters('mailpoet_newsletters_before_router', extra_routes); + const mailpoet_listing = ReactDOM.render(( @@ -46,6 +50,8 @@ if(container) { {/* Sending options */} + {/* Extra routes */} + { extra_routes.map(rt => ) } ), container); diff --git a/lib/Config/Migrator.php b/lib/Config/Migrator.php index 6e8275dede..9d4a2509c6 100644 --- a/lib/Config/Migrator.php +++ b/lib/Config/Migrator.php @@ -300,7 +300,8 @@ class Migrator { 'subscriber_id mediumint(9) NOT NULL,', 'queue_id mediumint(9) NOT NULL,', '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); } @@ -316,6 +317,7 @@ class Migrator { 'created_at TIMESTAMP NULL,', 'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,', 'PRIMARY KEY (id),', + 'KEY newsletter_id (newsletter_id),', 'KEY queue_id (queue_id)', ); return $this->sqlify(__FUNCTION__, $attributes); @@ -329,6 +331,7 @@ class Migrator { 'queue_id mediumint(9) NOT NULL,', 'created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,', 'PRIMARY KEY (id),', + 'KEY newsletter_id (newsletter_id),', 'KEY queue_id (queue_id)', ); return $this->sqlify(__FUNCTION__, $attributes); @@ -342,6 +345,7 @@ class Migrator { 'queue_id mediumint(9) NOT NULL,', 'created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,', 'PRIMARY KEY (id),', + 'KEY newsletter_id (newsletter_id),', 'KEY queue_id (queue_id)', ); return $this->sqlify(__FUNCTION__, $attributes); diff --git a/lib/Models/Newsletter.php b/lib/Models/Newsletter.php index 83e6b766f7..3f42f9136b 100644 --- a/lib/Models/Newsletter.php +++ b/lib/Models/Newsletter.php @@ -264,7 +264,8 @@ class Newsletter extends Model { 'status', 'count_processed', 'count_total', - 'scheduled_at' + 'scheduled_at', + 'created_at' )); if($queue === false) { $this->queue = false; diff --git a/tests/unit/Statistics/Track/OpensTest.php b/tests/unit/Statistics/Track/OpensTest.php index 13b5d12405..4fa4d90501 100644 --- a/tests/unit/Statistics/Track/OpensTest.php +++ b/tests/unit/Statistics/Track/OpensTest.php @@ -3,7 +3,6 @@ use Codeception\Util\Stub; use MailPoet\Models\Newsletter; use MailPoet\Models\SendingQueue; -use MailPoet\Models\StatisticsClicks; use MailPoet\Models\StatisticsOpens; use MailPoet\Models\Subscriber; use MailPoet\Statistics\Track\Opens; @@ -90,6 +89,5 @@ class OpensTest extends MailPoetTest { ORM::raw_execute('TRUNCATE ' . Subscriber::$_table); ORM::raw_execute('TRUNCATE ' . SendingQueue::$_table); ORM::raw_execute('TRUNCATE ' . StatisticsOpens::$_table); - ORM::raw_execute('TRUNCATE ' . StatisticsClicks::$_table); } } \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 42312f945a..d85fc01884 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -65,7 +65,11 @@ baseConfig = { include: require.resolve('react'), loader: 'expose-loader?' + globalPrefix + '.React', }, - { + { + include: require.resolve('react-router'), + loader: 'expose-loader?' + globalPrefix + '.ReactRouter', + }, + { include: require.resolve('react-string-replace'), loader: 'expose-loader?' + globalPrefix + '.ReactStringReplace', }, @@ -73,6 +77,10 @@ baseConfig = { test: /wp-js-hooks/i, loader: 'expose-loader?' + globalPrefix + '.Hooks!exports-loader?wp.hooks', }, + { + test: /listing.jsx/i, + loader: 'expose-loader?' + globalPrefix + '.Listing!babel-loader', + }, { include: /Blob.js$/, loader: 'exports-loader?window.Blob', @@ -124,7 +132,8 @@ config.push(_.extend({}, baseConfig, { 'react', 'react-dom', 'react-router', - 'react-string-replace' + 'react-string-replace', + 'listing/listing.jsx' ], admin: [ 'subscribers/subscribers.jsx', From 72aa0874117d1ef35a58d9789db4febfb9e54e7f Mon Sep 17 00:00:00 2001 From: Alexey Stoletniy Date: Thu, 6 Apr 2017 18:44:14 +0300 Subject: [PATCH 2/8] Localize formatting to 1 decimal [PREMIUM-1] --- assets/js/src/newsletters/listings/mixins.jsx | 6 +++--- .../js/src/newsletters/listings/welcome.jsx | 6 +++--- assets/js/src/num.js | 21 +++++++++++++++++++ webpack.config.js | 1 + 4 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 assets/js/src/num.js diff --git a/assets/js/src/newsletters/listings/mixins.jsx b/assets/js/src/newsletters/listings/mixins.jsx index 1249c52ade..ce2f1ee508 100644 --- a/assets/js/src/newsletters/listings/mixins.jsx +++ b/assets/js/src/newsletters/listings/mixins.jsx @@ -164,9 +164,9 @@ const _StatisticsMixin = { } // format to 1 decimal place - percentage_clicked = percentage_clicked.toFixed(1); - percentage_opened = percentage_opened.toFixed(1); - percentage_unsubscribed = percentage_unsubscribed.toFixed(1); + percentage_clicked = MailPoet.Num.toLocaleFixed(percentage_clicked, 1); + percentage_opened = MailPoet.Num.toLocaleFixed(percentage_opened, 1); + percentage_unsubscribed = MailPoet.Num.toLocaleFixed(percentage_unsubscribed, 1); const content = ( diff --git a/assets/js/src/newsletters/listings/welcome.jsx b/assets/js/src/newsletters/listings/welcome.jsx index 114aaa6d76..9a90d4e870 100644 --- a/assets/js/src/newsletters/listings/welcome.jsx +++ b/assets/js/src/newsletters/listings/welcome.jsx @@ -293,9 +293,9 @@ const NewsletterListWelcome = React.createClass({ let percentage_unsubscribed = (newsletter.statistics.unsubscribed * 100) / total_sent; // format to 1 decimal place - percentage_clicked = percentage_clicked.toFixed(1); - percentage_opened = percentage_opened.toFixed(1); - percentage_unsubscribed = percentage_unsubscribed.toFixed(1); + percentage_clicked = MailPoet.Num.toLocaleFixed(percentage_clicked, 1); + percentage_opened = MailPoet.Num.toLocaleFixed(percentage_opened, 1); + percentage_unsubscribed = MailPoet.Num.toLocaleFixed(percentage_unsubscribed, 1); const content = ( diff --git a/assets/js/src/num.js b/assets/js/src/num.js new file mode 100644 index 0000000000..80568be959 --- /dev/null +++ b/assets/js/src/num.js @@ -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} + ); + } + }; + +}); diff --git a/webpack.config.js b/webpack.config.js index d85fc01884..f4f53d7410 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -125,6 +125,7 @@ config.push(_.extend({}, baseConfig, { 'i18n', 'modal', 'notice', + 'num', 'jquery.serialize_object', 'parsleyjs' ], From e9070de9c4af6e94aeccbf027836f7c84bb31f80 Mon Sep 17 00:00:00 2001 From: Alexey Stoletniy Date: Wed, 12 Apr 2017 19:52:29 +0300 Subject: [PATCH 3/8] Add badges to stats in a newsletter listing, change stats style [PREMIUM-1] [MAILPOET-877] --- assets/css/src/listing/newsletters.styl | 58 +++++++- assets/js/src/newsletters/badges/badge.jsx | 38 ++++++ assets/js/src/newsletters/badges/stats.jsx | 111 ++++++++++++++++ assets/js/src/newsletters/listings/mixins.jsx | 124 ++++++++++++------ .../js/src/newsletters/listings/welcome.jsx | 51 +------ package.json | 1 + views/newsletters.html | 12 +- webpack.config.js | 7 +- 8 files changed, 311 insertions(+), 91 deletions(-) create mode 100644 assets/js/src/newsletters/badges/badge.jsx create mode 100644 assets/js/src/newsletters/badges/stats.jsx diff --git a/assets/css/src/listing/newsletters.styl b/assets/css/src/listing/newsletters.styl index 329c08c0c3..ae34606d22 100644 --- a/assets/css/src/listing/newsletters.styl +++ b/assets/css/src/listing/newsletters.styl @@ -1,3 +1,59 @@ +$excellent-badge-color = #2993ab +$good-badge-color = #f0b849 +$bad-badge-color = #d54e21 +$grey-stat-color = #707070 + #newsletters_container h2.nav-tab-wrapper - margin-bottom: 1rem \ No newline at end of file + 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 + + &_grey + color: $grey-stat-color + + &_big + font-size: 23px + font-weight: 600 + line-height: normal + + &_spaced + margin-bottom: 1rem + + &_tplspaced + margin-bottom: 3rem + + &_hidden + display: none + +.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 diff --git a/assets/js/src/newsletters/badges/badge.jsx b/assets/js/src/newsletters/badges/badge.jsx new file mode 100644 index 0000000000..9bd039f994 --- /dev/null +++ b/assets/js/src/newsletters/badges/badge.jsx @@ -0,0 +1,38 @@ +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}` : '', + this.props.size ? `mailpoet_badge_size_${this.props.size}` : '' + ); + + const tooltip = this.props.tooltip.replace(/\n/g, '
') || false; + // tooltip ID must be unique, defaults to tooltip text + const tooltipId = this.props.tooltipId || tooltip; + + return ( + + + {this.props.name} + + { tooltip && ( + + ) } + + ); + } +} + +export default Badge; diff --git a/assets/js/src/newsletters/badges/stats.jsx b/assets/js/src/newsletters/badges/stats.jsx new file mode 100644 index 0000000000..6f3230b53b --- /dev/null +++ b/assets/js/src/newsletters/badges/stats.jsx @@ -0,0 +1,111 @@ +import MailPoet from 'mailpoet' +import React from 'react' +import classNames from 'classnames' + +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 = ( + + ); + + if (this.props.headline) { + const headlineClasses = classNames( + `mailpoet_stat_${badgeType}`, + this.props.size ? `mailpoet_badge_size_${this.props.size}` : '' + ); + + return ( +
+ + {this.props.headline} + {content} +
+ ); + } + + return content; + } +} + +export default StatsBadge; diff --git a/assets/js/src/newsletters/listings/mixins.jsx b/assets/js/src/newsletters/listings/mixins.jsx index ce2f1ee508..22db6cd1d1 100644 --- a/assets/js/src/newsletters/listings/mixins.jsx +++ b/assets/js/src/newsletters/listings/mixins.jsx @@ -6,6 +6,7 @@ import MailPoet from 'mailpoet' import classNames from 'classnames' import jQuery from 'jquery' import Hooks from 'wp-js-hooks' +import StatsBadge from 'newsletters/badges/stats.jsx' const _QueueMixin = { pauseSending: function(newsletter) { @@ -142,53 +143,90 @@ const _QueueMixin = { }; const _StatisticsMixin = { - renderStatistics: function(newsletter) { - if ( - newsletter.statistics - && newsletter.queue - && newsletter.queue.status !== 'scheduled' - ) { - let params = {}; - params = Hooks.applyFilters('mailpoet_newsletters_listing_stats_before', params, newsletter); - - const total_sent = ~~(newsletter.queue.count_processed); - - let percentage_clicked = 0; - let percentage_opened = 0; - let percentage_unsubscribed = 0; - - if (total_sent > 0) { - percentage_clicked = (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 - percentage_clicked = MailPoet.Num.toLocaleFixed(percentage_clicked, 1); - percentage_opened = MailPoet.Num.toLocaleFixed(percentage_opened, 1); - percentage_unsubscribed = MailPoet.Num.toLocaleFixed(percentage_unsubscribed, 1); - - const content = ( - - { percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }% - - ); - - if (total_sent > 0 && params.link) { - return ( - { content } - ); - } - - return content; - } else { + renderStatistics: function(newsletter, sentCondition) { + if (sentCondition === undefined) { + // condition for standard and post notification listings + sentCondition = newsletter.statistics + && newsletter.queue + && newsletter.queue.status !== 'scheduled' + } + if (!sentCondition) { return ( {MailPoet.I18n.t('notSentYet')} ); } + + 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_opened = 0; + let percentage_unsubscribed = 0; + + if (total_sent > 0) { + percentage_clicked = (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 content; + if (total_sent >= 20 && newsletter.statistics.opened >= 5) { + // display stats with badges + content = ( +
+
+ { percentage_opened_display }% + +
+
+ { percentage_clicked_display }% + +
+
+ { percentage_unsubscribed_display }% +
+
+ ); + } else { + // display simple stats + content = ( + + { percentage_opened_display }%, + { " " } + { percentage_clicked_display }% + + , { percentage_unsubscribed_display }% + + + ); + } + + if (total_sent > 0 && params.link) { + return ( + { content } + ); + } + + return content; } } diff --git a/assets/js/src/newsletters/listings/welcome.jsx b/assets/js/src/newsletters/listings/welcome.jsx index 9a90d4e870..226eefcf9b 100644 --- a/assets/js/src/newsletters/listings/welcome.jsx +++ b/assets/js/src/newsletters/listings/welcome.jsx @@ -5,7 +5,7 @@ import { createHashHistory } from 'history' import Listing from 'listing/listing.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 jQuery from 'jquery' @@ -156,7 +156,7 @@ const newsletter_actions = [ ]; const NewsletterListWelcome = React.createClass({ - mixins: [ MailerMixin ], + mixins: [ StatisticsMixin, MailerMixin ], updateStatus: function(e) { // make the event persist so that we can still override the selected value // in the ajax callback @@ -277,48 +277,6 @@ const NewsletterListWelcome = React.createClass({
); }, - renderStatistics: function(newsletter) { - if (mailpoet_tracking_enabled === false) { - return; - } - - let params = {}; - params = Hooks.applyFilters('mailpoet_newsletters_listing_stats_before', params, newsletter); - - if (newsletter.total_sent > 0 && newsletter.statistics) { - const total_sent = ~~(newsletter.total_sent); - - let percentage_clicked = (newsletter.statistics.clicked * 100) / total_sent; - let percentage_opened = (newsletter.statistics.opened * 100) / total_sent; - let percentage_unsubscribed = (newsletter.statistics.unsubscribed * 100) / total_sent; - - // format to 1 decimal place - percentage_clicked = MailPoet.Num.toLocaleFixed(percentage_clicked, 1); - percentage_opened = MailPoet.Num.toLocaleFixed(percentage_opened, 1); - percentage_unsubscribed = MailPoet.Num.toLocaleFixed(percentage_unsubscribed, 1); - - const content = ( - - { percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }% - - ); - - if (params.link) { - return ( - { content } - ); - } - - return content; - } else { - return ( - {MailPoet.I18n.t('notSentYet')} - ); - } - }, renderItem: function(newsletter, actions) { const rowClasses = classNames( 'manage-column', @@ -345,7 +303,10 @@ const NewsletterListWelcome = React.createClass({ { (mailpoet_tracking_enabled === true) ? ( - { this.renderStatistics(newsletter) } + { this.renderStatistics( + newsletter, + newsletter.total_sent > 0 && newsletter.statistics + ) } ) : null } diff --git a/package.json b/package.json index ae5c11c6da..cd924bc121 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "react-dom": "~15.4.2", "react-router": "~3.0.2", "react-string-replace": "^0.3.2", + "react-tooltip": "^3.2.10", "select2": "^4.0.0", "spectrum-colorpicker": "^1.6.2", "tinymce": "4.5.6", diff --git a/views/newsletters.html b/views/newsletters.html index 40d74f3b8f..83bff9cebc 100644 --- a/views/newsletters.html +++ b/views/newsletters.html @@ -60,7 +60,7 @@ 'subject': __('Subject'), 'status': __('Status'), - 'statistics': __('Opened, Clicked, Unsubscribed'), + 'statistics': __('Opened, Clicked'), 'lists': __('Lists'), 'settings': __('Settings'), 'history': __('History'), @@ -90,6 +90,16 @@ 'paused': __('Paused'), '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.'), + 'templateFileMalformedError': __('This template file appears to be damaged. Please try another one.'), 'importTemplateTitle': __('Import a template'), 'selectJsonFileToUpload': __('Select a .json file to upload'), diff --git a/webpack.config.js b/webpack.config.js index f4f53d7410..ea3d5e179a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -81,6 +81,10 @@ baseConfig = { 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$/, loader: 'exports-loader?window.Blob', @@ -134,7 +138,8 @@ config.push(_.extend({}, baseConfig, { 'react-dom', 'react-router', 'react-string-replace', - 'listing/listing.jsx' + 'listing/listing.jsx', + 'newsletters/badges/stats.jsx' ], admin: [ 'subscribers/subscribers.jsx', From 95ff83557f2ae9c3d3c2ce9d8daf45b567ea0a6b Mon Sep 17 00:00:00 2001 From: Alexey Stoletniy Date: Tue, 18 Apr 2017 15:30:55 +0300 Subject: [PATCH 4/8] Add a green box in stats for recently sent newsletters, add help KB link [MAILPOET-877] --- assets/css/src/listing/newsletters.styl | 8 ++ assets/js/src/newsletters/badges/badge.jsx | 2 +- assets/js/src/newsletters/listings/mixins.jsx | 86 +++++++++++++++---- .../listings/notification_history.jsx | 2 +- .../js/src/newsletters/listings/standard.jsx | 2 +- lib/API/Endpoints/v1/Newsletters.php | 3 +- views/newsletters.html | 2 + 7 files changed, 83 insertions(+), 22 deletions(-) diff --git a/assets/css/src/listing/newsletters.styl b/assets/css/src/listing/newsletters.styl index ae34606d22..1b27ad892a 100644 --- a/assets/css/src/listing/newsletters.styl +++ b/assets/css/src/listing/newsletters.styl @@ -1,6 +1,7 @@ $excellent-badge-color = #2993ab $good-badge-color = #f0b849 $bad-badge-color = #d54e21 +$green-badge-color = #55bd56 $grey-stat-color = #707070 #newsletters_container @@ -39,6 +40,10 @@ $grey-stat-color = #707070 &_hidden display: none + &_link_small + text-decoration: underline !important + font-size: 0.75rem + .mailpoet_badge padding: 4px 6px 3px 6px color: #FFFFFF @@ -57,3 +62,6 @@ $grey-stat-color = #707070 &_bad background: $bad-badge-color + + &_green + background: $green-badge-color diff --git a/assets/js/src/newsletters/badges/badge.jsx b/assets/js/src/newsletters/badges/badge.jsx index 9bd039f994..8e4c873c1a 100644 --- a/assets/js/src/newsletters/badges/badge.jsx +++ b/assets/js/src/newsletters/badges/badge.jsx @@ -10,7 +10,7 @@ class Badge extends React.Component { this.props.size ? `mailpoet_badge_size_${this.props.size}` : '' ); - const tooltip = this.props.tooltip.replace(/\n/g, '
') || false; + const tooltip = this.props.tooltip ? this.props.tooltip.replace(/\n/g, '
') : false; // tooltip ID must be unique, defaults to tooltip text const tooltipId = this.props.tooltipId || tooltip; diff --git a/assets/js/src/newsletters/listings/mixins.jsx b/assets/js/src/newsletters/listings/mixins.jsx index 22db6cd1d1..0fe27e4f37 100644 --- a/assets/js/src/newsletters/listings/mixins.jsx +++ b/assets/js/src/newsletters/listings/mixins.jsx @@ -4,6 +4,7 @@ import ReactStringReplace from 'react-string-replace' import { Link } from 'react-router' import MailPoet from 'mailpoet' import classNames from 'classnames' +import moment from 'moment' import jQuery from 'jquery' import Hooks from 'wp-js-hooks' import StatsBadge from 'newsletters/badges/stats.jsx' @@ -143,14 +144,14 @@ const _QueueMixin = { }; const _StatisticsMixin = { - renderStatistics: function(newsletter, sentCondition) { - if (sentCondition === undefined) { + renderStatistics: function(newsletter, sent_condition, current_time) { + if (sent_condition === undefined) { // condition for standard and post notification listings - sentCondition = newsletter.statistics + sent_condition = newsletter.statistics && newsletter.queue && newsletter.queue.status !== 'scheduled' } - if (!sentCondition) { + if (!sent_condition) { return ( {MailPoet.I18n.t('notSentYet')} ); @@ -177,8 +178,19 @@ const _StatisticsMixin = { const percentage_opened_display = MailPoet.Num.toLocaleFixed(percentage_opened, 1); const percentage_unsubscribed_display = MailPoet.Num.toLocaleFixed(percentage_unsubscribed, 1); + // green box for newsletters that were just sent + const show_stats_timeout = 6; // in hours + const newsletter_date = newsletter.queue.scheduled_at || newsletter.queue.created_at; + const sent_hours_ago = moment(current_time).diff(moment(newsletter_date), 'hours'); + const too_early_for_stats = sent_hours_ago < show_stats_timeout; + + const improveStatsKBLink = 'http://beta.docs.mailpoet.com/article/190-whats-a-good-email-open-rate'; + let content; - if (total_sent >= 20 && newsletter.statistics.opened >= 5) { + if (total_sent >= 20 + && newsletter.statistics.opened >= 5 + && !too_early_for_stats + ) { // display stats with badges content = (
@@ -206,27 +218,65 @@ const _StatisticsMixin = { } else { // display simple stats content = ( - - { percentage_opened_display }%, - { " " } - { percentage_clicked_display }% - - , { percentage_unsubscribed_display }% +
+ + { percentage_opened_display }%, + { " " } + { percentage_clicked_display }% + + , { percentage_unsubscribed_display }% + - + { too_early_for_stats && ( +
+ {MailPoet.I18n.t('checkBackInHours') + .replace('%$1d', show_stats_timeout - sent_hours_ago)} +
+ ) } +
); } - if (total_sent > 0 && params.link) { + let after_content; + if (percentage_opened < 5 + && sent_hours_ago >= 24 + && total_sent >= 10 + ) { + // help link for bad open rate + after_content = ( + + ) + } + + if (total_sent > 0 && !too_early_for_stats && params.link) { + // wrap content in a link return ( - { content } +
+ + {content} + + {after_content} +
); } - return content; + return ( +
+ {content} + {after_content} +
+ ); } } diff --git a/assets/js/src/newsletters/listings/notification_history.jsx b/assets/js/src/newsletters/listings/notification_history.jsx index 7820516839..dd5fd35211 100644 --- a/assets/js/src/newsletters/listings/notification_history.jsx +++ b/assets/js/src/newsletters/listings/notification_history.jsx @@ -87,7 +87,7 @@ const NewsletterListNotificationHistory = React.createClass({ { (mailpoet_tracking_enabled === true) ? ( - { this.renderStatistics(newsletter) } + { this.renderStatistics(newsletter, undefined, meta.current_time) } ) : null } diff --git a/assets/js/src/newsletters/listings/standard.jsx b/assets/js/src/newsletters/listings/standard.jsx index f399df617e..116521ecaa 100644 --- a/assets/js/src/newsletters/listings/standard.jsx +++ b/assets/js/src/newsletters/listings/standard.jsx @@ -184,7 +184,7 @@ const NewsletterListStandard = React.createClass({ { (mailpoet_tracking_enabled === true) ? ( - { this.renderStatistics(newsletter) } + { this.renderStatistics(newsletter, undefined, meta.current_time) } ) : null } diff --git a/lib/API/Endpoints/v1/Newsletters.php b/lib/API/Endpoints/v1/Newsletters.php index 0573d236ad..22473d67f7 100644 --- a/lib/API/Endpoints/v1/Newsletters.php +++ b/lib/API/Endpoints/v1/Newsletters.php @@ -372,7 +372,8 @@ class Newsletters extends APIEndpoint { 'filters' => $listing_data['filters'], 'groups' => $listing_data['groups'], 'mta_log' => Setting::getValue('mta_log'), - 'mta_method' => Setting::getValue('mta.method') + 'mta_method' => Setting::getValue('mta.method'), + 'current_time' => current_time('mysql') )); } diff --git a/views/newsletters.html b/views/newsletters.html index 83bff9cebc..0ed3ca06ca 100644 --- a/views/newsletters.html +++ b/views/newsletters.html @@ -99,6 +99,8 @@ '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.'), 'importTemplateTitle': __('Import a template'), From 4257aa634e7c73bff2d1153752fdd7e005d28076 Mon Sep 17 00:00:00 2001 From: Alexey Stoletniy Date: Tue, 18 Apr 2017 19:52:46 +0300 Subject: [PATCH 5/8] Don't show green box and KB link in stats for welcome emails [MAILPOET-877] --- assets/js/src/newsletters/listings/mixins.jsx | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/assets/js/src/newsletters/listings/mixins.jsx b/assets/js/src/newsletters/listings/mixins.jsx index 0fe27e4f37..63946babd2 100644 --- a/assets/js/src/newsletters/listings/mixins.jsx +++ b/assets/js/src/newsletters/listings/mixins.jsx @@ -178,11 +178,24 @@ const _StatisticsMixin = { const percentage_opened_display = MailPoet.Num.toLocaleFixed(percentage_opened, 1); const percentage_unsubscribed_display = MailPoet.Num.toLocaleFixed(percentage_unsubscribed, 1); - // green box for newsletters that were just sent - const show_stats_timeout = 6; // in hours - const newsletter_date = newsletter.queue.scheduled_at || newsletter.queue.created_at; - const sent_hours_ago = moment(current_time).diff(moment(newsletter_date), 'hours'); - const too_early_for_stats = sent_hours_ago < show_stats_timeout; + 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/190-whats-a-good-email-open-rate'; @@ -238,7 +251,8 @@ const _StatisticsMixin = { } let after_content; - if (percentage_opened < 5 + if (show_kb_link + && percentage_opened < 5 && sent_hours_ago >= 24 && total_sent >= 10 ) { @@ -253,7 +267,7 @@ const _StatisticsMixin = { {MailPoet.I18n.t('improveThisLinkText')}
- ) + ); } if (total_sent > 0 && !too_early_for_stats && params.link) { From 121a78f42a1ad7987a9cdf1e061a03ce2e38e530 Mon Sep 17 00:00:00 2001 From: Alexey Stoletniy Date: Tue, 18 Apr 2017 23:42:28 +0300 Subject: [PATCH 6/8] Update an open rate improvement KB link URL [MAILPOET-877] --- assets/js/src/newsletters/listings/mixins.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/src/newsletters/listings/mixins.jsx b/assets/js/src/newsletters/listings/mixins.jsx index 63946babd2..d7d5195041 100644 --- a/assets/js/src/newsletters/listings/mixins.jsx +++ b/assets/js/src/newsletters/listings/mixins.jsx @@ -197,7 +197,7 @@ const _StatisticsMixin = { show_kb_link = false; } - const improveStatsKBLink = 'http://beta.docs.mailpoet.com/article/190-whats-a-good-email-open-rate'; + const improveStatsKBLink = 'http://beta.docs.mailpoet.com/article/191-how-to-improve-my-open-and-click-rates'; let content; if (total_sent >= 20 From bb220baf6a6cd2a8c650f60ca5a8fb100a059aa1 Mon Sep 17 00:00:00 2001 From: Alexey Stoletniy Date: Mon, 24 Apr 2017 12:31:52 +0300 Subject: [PATCH 7/8] Add names to constants, rename vars for clarity [MAILPOET-877] --- assets/css/src/listing/newsletters.styl | 2 +- assets/js/src/newsletters/listings/mixins.jsx | 27 ++++++++++++------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/assets/css/src/listing/newsletters.styl b/assets/css/src/listing/newsletters.styl index 1b27ad892a..eb46c81e8d 100644 --- a/assets/css/src/listing/newsletters.styl +++ b/assets/css/src/listing/newsletters.styl @@ -34,7 +34,7 @@ $grey-stat-color = #707070 &_spaced margin-bottom: 1rem - &_tplspaced + &_triple-spaced margin-bottom: 3rem &_hidden diff --git a/assets/js/src/newsletters/listings/mixins.jsx b/assets/js/src/newsletters/listings/mixins.jsx index d7d5195041..3e37a995a7 100644 --- a/assets/js/src/newsletters/listings/mixins.jsx +++ b/assets/js/src/newsletters/listings/mixins.jsx @@ -144,14 +144,14 @@ const _QueueMixin = { }; const _StatisticsMixin = { - renderStatistics: function(newsletter, sent_condition, current_time) { - if (sent_condition === undefined) { + renderStatistics: function(newsletter, is_sent, current_time) { + if (is_sent === undefined) { // condition for standard and post notification listings - sent_condition = newsletter.statistics + is_sent = newsletter.statistics && newsletter.queue && newsletter.queue.status !== 'scheduled' } - if (!sent_condition) { + if (!is_sent) { return ( {MailPoet.I18n.t('notSentYet')} ); @@ -199,9 +199,13 @@ const _StatisticsMixin = { 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 >= 20 - && newsletter.statistics.opened >= 5 + if (total_sent >= min_newsletters_sent + && newsletter.statistics.opened >= min_newsletter_opens && !too_early_for_stats ) { // display stats with badges @@ -250,11 +254,16 @@ const _StatisticsMixin = { ); } + // 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 < 5 - && sent_hours_ago >= 24 - && total_sent >= 10 + && 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 = ( From 3bc53f9f09c13bb1b9f6dc736b940899f28a41f2 Mon Sep 17 00:00:00 2001 From: Alexey Stoletniy Date: Tue, 25 Apr 2017 11:47:22 +0300 Subject: [PATCH 8/8] Remove Premium-only styles from Free, cleanup styles [PREMIUM-1] --- assets/css/src/listing/newsletters.styl | 15 --------------- assets/js/src/newsletters/badges/badge.jsx | 3 +-- assets/js/src/newsletters/badges/stats.jsx | 8 +------- views/layout.html | 3 +++ 4 files changed, 5 insertions(+), 24 deletions(-) diff --git a/assets/css/src/listing/newsletters.styl b/assets/css/src/listing/newsletters.styl index eb46c81e8d..a8a030f3f6 100644 --- a/assets/css/src/listing/newsletters.styl +++ b/assets/css/src/listing/newsletters.styl @@ -2,7 +2,6 @@ $excellent-badge-color = #2993ab $good-badge-color = #f0b849 $bad-badge-color = #d54e21 $green-badge-color = #55bd56 -$grey-stat-color = #707070 #newsletters_container h2.nav-tab-wrapper @@ -23,20 +22,6 @@ $grey-stat-color = #707070 &_bad color: $bad-badge-color - &_grey - color: $grey-stat-color - - &_big - font-size: 23px - font-weight: 600 - line-height: normal - - &_spaced - margin-bottom: 1rem - - &_triple-spaced - margin-bottom: 3rem - &_hidden display: none diff --git a/assets/js/src/newsletters/badges/badge.jsx b/assets/js/src/newsletters/badges/badge.jsx index 8e4c873c1a..04d10512ea 100644 --- a/assets/js/src/newsletters/badges/badge.jsx +++ b/assets/js/src/newsletters/badges/badge.jsx @@ -6,8 +6,7 @@ class Badge extends React.Component { render() { const badgeClasses = classNames( 'mailpoet_badge', - this.props.type ? `mailpoet_badge_${this.props.type}` : '', - this.props.size ? `mailpoet_badge_size_${this.props.size}` : '' + this.props.type ? `mailpoet_badge_${this.props.type}` : '' ); const tooltip = this.props.tooltip ? this.props.tooltip.replace(/\n/g, '
') : false; diff --git a/assets/js/src/newsletters/badges/stats.jsx b/assets/js/src/newsletters/badges/stats.jsx index 6f3230b53b..1926a6e9d8 100644 --- a/assets/js/src/newsletters/badges/stats.jsx +++ b/assets/js/src/newsletters/badges/stats.jsx @@ -1,6 +1,5 @@ import MailPoet from 'mailpoet' import React from 'react' -import classNames from 'classnames' import Badge from './badge.jsx' @@ -90,14 +89,9 @@ class StatsBadge extends React.Component { ); if (this.props.headline) { - const headlineClasses = classNames( - `mailpoet_stat_${badgeType}`, - this.props.size ? `mailpoet_badge_size_${this.props.size}` : '' - ); - return (
- + {this.props.headline} {content}
diff --git a/views/layout.html b/views/layout.html index 091ae71d5d..bbdb4e7782 100644 --- a/views/layout.html +++ b/views/layout.html @@ -31,6 +31,9 @@ jQuery('.toplevel_page_mailpoet-newsletters.menu-top-last') <%= stylesheet( 'admin.css' )%> + +<%= do_action('mailpoet_styles_admin_after') %> + <% if is_rtl %> <%= stylesheet('rtl.css') %>