Added parent_id to Newsletters table
- added NOTIFICATION_HISTORY Newsletter's type - implement basic UI for notification_history - TODO: implement passing extra parameters in order to handle the :id part
This commit is contained in:
@@ -379,8 +379,16 @@ const Listing = React.createClass({
|
||||
.join('/');
|
||||
|
||||
// prepend url with "tab" if specified
|
||||
if (this.props.tab !== undefined) {
|
||||
params = `/${ this.props.tab }/${ params }`;
|
||||
if (this.props.tab !== undefined || this.props.base_path !== undefined) {
|
||||
let base_path = (this.props.base_path !== undefined)
|
||||
? this.props.base_path
|
||||
: this.props.tab;
|
||||
|
||||
// TODO: add method to replace dynamic path elements
|
||||
base_path = base_path.replace(':id', this.props.params.id);
|
||||
// /-TODO--
|
||||
|
||||
params = `/${ base_path }/${ params }`;
|
||||
} else {
|
||||
params = `/${ params }`;
|
||||
}
|
||||
|
@@ -277,7 +277,9 @@ const NewsletterListNotification = React.createClass({
|
||||
{ this.renderSettings(newsletter) }
|
||||
</td>
|
||||
<td className="column" data-colname={ MailPoet.I18n.t('history') }>
|
||||
<a href="#TODO">{ MailPoet.I18n.t('viewHistory') }</a>
|
||||
<Link
|
||||
to={ `/notification/history/${ newsletter.id }` }
|
||||
>{ MailPoet.I18n.t('viewHistory') }</Link>
|
||||
</td>
|
||||
<td className="column-date" data-colname={ MailPoet.I18n.t('lastModifiedOn') }>
|
||||
<abbr>{ MailPoet.Date.format(newsletter.updated_at) }</abbr>
|
||||
|
251
assets/js/src/newsletters/listings/notification_history.jsx
Normal file
251
assets/js/src/newsletters/listings/notification_history.jsx
Normal file
@@ -0,0 +1,251 @@
|
||||
import React from 'react'
|
||||
import { Router, Link } from 'react-router'
|
||||
import classNames from 'classnames'
|
||||
import jQuery from 'jquery'
|
||||
import MailPoet from 'mailpoet'
|
||||
|
||||
import Listing from 'listing/listing.jsx'
|
||||
import ListingTabs from 'newsletters/listings/tabs.jsx'
|
||||
|
||||
const mailpoet_tracking_enabled = (!!(window['mailpoet_tracking_enabled']));
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'subject',
|
||||
label: MailPoet.I18n.t('subject'),
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
label: MailPoet.I18n.t('status')
|
||||
},
|
||||
{
|
||||
name: 'segments',
|
||||
label: MailPoet.I18n.t('lists')
|
||||
},
|
||||
{
|
||||
name: 'statistics',
|
||||
label: MailPoet.I18n.t('statistics'),
|
||||
display: mailpoet_tracking_enabled
|
||||
},
|
||||
{
|
||||
name: 'processed_at',
|
||||
label: MailPoet.I18n.t('sentOn'),
|
||||
}
|
||||
];
|
||||
|
||||
const newsletter_actions = [
|
||||
{
|
||||
name: 'view',
|
||||
link: function(newsletter) {
|
||||
return (
|
||||
<a href={ newsletter.preview_url } target="_blank">
|
||||
{MailPoet.I18n.t('preview')}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const NewsletterListNotificationHistory = React.createClass({
|
||||
pauseSending: function(newsletter) {
|
||||
MailPoet.Ajax.post({
|
||||
endpoint: 'sendingQueue',
|
||||
action: 'pause',
|
||||
data: newsletter.id
|
||||
}).done(function() {
|
||||
jQuery('#resume_'+newsletter.id).show();
|
||||
jQuery('#pause_'+newsletter.id).hide();
|
||||
});
|
||||
},
|
||||
resumeSending: function(newsletter) {
|
||||
MailPoet.Ajax.post({
|
||||
endpoint: 'sendingQueue',
|
||||
action: 'resume',
|
||||
data: newsletter.id
|
||||
}).done(function() {
|
||||
jQuery('#pause_'+newsletter.id).show();
|
||||
jQuery('#resume_'+newsletter.id).hide();
|
||||
});
|
||||
},
|
||||
renderStatus: function(newsletter) {
|
||||
if (!newsletter.queue) {
|
||||
return (
|
||||
<span>{MailPoet.I18n.t('notSentYet')}</span>
|
||||
);
|
||||
} else {
|
||||
if (newsletter.queue.status === 'scheduled') {
|
||||
return (
|
||||
<span>{MailPoet.I18n.t('scheduledFor')} { MailPoet.Date.format(newsletter.queue.scheduled_at) } </span>
|
||||
)
|
||||
}
|
||||
const progressClasses = classNames(
|
||||
'mailpoet_progress',
|
||||
{ 'mailpoet_progress_complete': newsletter.queue.status === 'completed'}
|
||||
);
|
||||
|
||||
// calculate percentage done
|
||||
const percentage = Math.round(
|
||||
(newsletter.queue.count_processed * 100) / (newsletter.queue.count_total)
|
||||
);
|
||||
|
||||
let label;
|
||||
|
||||
if (newsletter.queue.status === 'completed') {
|
||||
label = (
|
||||
<span>
|
||||
{
|
||||
MailPoet.I18n.t('newsletterQueueCompleted')
|
||||
.replace("%$1d", newsletter.queue.count_processed - newsletter.queue.count_failed)
|
||||
.replace("%$2d", newsletter.queue.count_total)
|
||||
}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
label = (
|
||||
<span>
|
||||
{ newsletter.queue.count_processed } / { newsletter.queue.count_total }
|
||||
|
||||
<a
|
||||
id={ 'resume_'+newsletter.id }
|
||||
className="button"
|
||||
style={{ display: (newsletter.queue.status === 'paused') ? 'inline-block': 'none' }}
|
||||
href="javascript:;"
|
||||
onClick={ this.resumeSending.bind(null, newsletter) }
|
||||
>{MailPoet.I18n.t('resume')}</a>
|
||||
<a
|
||||
id={ 'pause_'+newsletter.id }
|
||||
className="button mailpoet_pause"
|
||||
style={{ display: (newsletter.queue.status === null) ? 'inline-block': 'none' }}
|
||||
href="javascript:;"
|
||||
onClick={ this.pauseSending.bind(null, newsletter) }
|
||||
>{MailPoet.I18n.t('pause')}</a>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={ progressClasses }>
|
||||
<span
|
||||
className="mailpoet_progress_bar"
|
||||
style={ { width: percentage + "%"} }
|
||||
></span>
|
||||
<span className="mailpoet_progress_label">
|
||||
{ percentage + "%" }
|
||||
</span>
|
||||
</div>
|
||||
<p style={{ textAlign:'center' }}>
|
||||
{ label }
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
renderStatistics: function(newsletter) {
|
||||
if (mailpoet_tracking_enabled === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newsletter.statistics && newsletter.queue && newsletter.queue.status !== 'scheduled') {
|
||||
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 = 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
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
{ percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }%
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<span>{MailPoet.I18n.t('notSentYet')}</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
renderItem: function(newsletter, actions) {
|
||||
const rowClasses = classNames(
|
||||
'manage-column',
|
||||
'column-primary',
|
||||
'has-row-actions'
|
||||
);
|
||||
|
||||
const segments = newsletter.segments.map(function(segment) {
|
||||
return segment.name
|
||||
}).join(', ');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<td className={ rowClasses }>
|
||||
<strong>
|
||||
<a
|
||||
className="row-title"
|
||||
href={ `?page=mailpoet-newsletter-editor&id=${ newsletter.id }` }
|
||||
>{ newsletter.subject }</a>
|
||||
</strong>
|
||||
{ actions }
|
||||
</td>
|
||||
<td className="column" data-colname={ MailPoet.I18n.t('status') }>
|
||||
{ this.renderStatus(newsletter) }
|
||||
</td>
|
||||
<td className="column" data-colname={ MailPoet.I18n.t('lists') }>
|
||||
{ segments }
|
||||
</td>
|
||||
{ (mailpoet_tracking_enabled === true) ? (
|
||||
<td className="column" data-colname={ MailPoet.I18n.t('statistics') }>
|
||||
{ this.renderStatistics(newsletter) }
|
||||
</td>
|
||||
) : null }
|
||||
<td className="column-date" data-colname={ MailPoet.I18n.t('lastModifiedOn') }>
|
||||
<abbr>{ MailPoet.Date.format(newsletter.updated_at) }</abbr>
|
||||
</td>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="title">
|
||||
{MailPoet.I18n.t('pageTitle')} <Link className="page-title-action" to="/new">{MailPoet.I18n.t('new')}</Link>
|
||||
</h1>
|
||||
|
||||
<ListingTabs tab="notification" />
|
||||
|
||||
<Link
|
||||
className="page-title-action"
|
||||
to="/notification"
|
||||
>{MailPoet.I18n.t('backToPostNotifications')}</Link>
|
||||
|
||||
<Listing
|
||||
limit={ mailpoet_listing_per_page }
|
||||
location={ this.props.location }
|
||||
params={ this.props.params }
|
||||
endpoint="newsletters"
|
||||
tab="notification_history"
|
||||
base_path="notification/history/:id"
|
||||
onRenderItem={ this.renderItem }
|
||||
columns={columns}
|
||||
item_actions={ newsletter_actions }
|
||||
auto_refresh={ true }
|
||||
sort_by="updated_at"
|
||||
sort_order="desc"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = NewsletterListNotificationHistory;
|
@@ -14,6 +14,7 @@ import NewsletterTypeNotification from 'newsletters/types/notification/notificat
|
||||
import NewsletterListStandard from 'newsletters/listings/standard.jsx'
|
||||
import NewsletterListWelcome from 'newsletters/listings/welcome.jsx'
|
||||
import NewsletterListNotification from 'newsletters/listings/notification.jsx'
|
||||
import NewsletterListNotificationHistory from 'newsletters/listings/notification_history.jsx'
|
||||
|
||||
const history = useRouterHistory(createHashHistory)({ queryKey: false });
|
||||
|
||||
@@ -34,8 +35,11 @@ if(container) {
|
||||
<Route name="listing/standard" path="standard" component={ NewsletterListStandard } />
|
||||
<Route name="listing/welcome" path="welcome" component={ NewsletterListWelcome } />
|
||||
<Route name="listing/notification" path="notification" component={ NewsletterListNotification } />
|
||||
<Route name="listing/notification/history" path="notification/history/:id" component={ NewsletterListNotificationHistory } />
|
||||
|
||||
<Route path="standard/*" component={ NewsletterListStandard } />
|
||||
<Route path="welcome/*" component={ NewsletterListWelcome } />
|
||||
<Route path="notification/history/:id/*" component={ NewsletterListNotificationHistory } />
|
||||
<Route path="notification/*" component={ NewsletterListNotification } />
|
||||
{/* Newsletter: type selection */}
|
||||
<Route path="new" component={ NewsletterTypes } />
|
||||
|
@@ -173,6 +173,7 @@ class Migrator {
|
||||
function newsletters() {
|
||||
$attributes = array(
|
||||
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
|
||||
'parent_id mediumint(9) NULL,',
|
||||
'subject varchar(250) NOT NULL DEFAULT "",',
|
||||
'type varchar(20) NOT NULL DEFAULT "standard",',
|
||||
'sender_address varchar(150) NOT NULL DEFAULT "",',
|
||||
|
@@ -73,12 +73,14 @@ class Scheduler {
|
||||
return;
|
||||
}
|
||||
$segment_ids = array_map(function($segment) {
|
||||
return $segment['id'];
|
||||
return (int)$segment['id'];
|
||||
}, $segments);
|
||||
|
||||
$subscribers = Subscriber::getSubscribedInSegments($segment_ids)
|
||||
->findArray();
|
||||
$subscribers = Helpers::arrayColumn($subscribers, 'subscriber_id');
|
||||
$subscribers = array_unique($subscribers);
|
||||
|
||||
if(empty($subscribers)) {
|
||||
$this->deleteQueueOrUpdateNextRunDate($queue, $newsletter);
|
||||
return;
|
||||
|
@@ -121,7 +121,6 @@ class Handler {
|
||||
$this->table_name.'.'.$this->data['sort_by']
|
||||
)
|
||||
->findMany();
|
||||
|
||||
} else {
|
||||
$this->setFilter();
|
||||
$this->setGroup();
|
||||
|
@@ -10,6 +10,7 @@ class Newsletter extends Model {
|
||||
const TYPE_STANDARD = 'standard';
|
||||
const TYPE_WELCOME = 'welcome';
|
||||
const TYPE_NOTIFICATION = 'notification';
|
||||
const TYPE_NOTIFICATION_HISTORY = 'notification_history';
|
||||
|
||||
// standard newsletters
|
||||
const STATUS_DRAFT = 'draft';
|
||||
@@ -259,6 +260,15 @@ class Newsletter extends Model {
|
||||
}
|
||||
|
||||
static function filters($data = array()) {
|
||||
$type = isset($data['tab']) ? $data['tab'] : null;
|
||||
|
||||
// newsletter types without filters
|
||||
if(in_array($type, array(
|
||||
self::TYPE_NOTIFICATION_HISTORY
|
||||
))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$segments = Segment::orderByAsc('name')->findMany();
|
||||
$segment_list = array();
|
||||
$segment_list[] = array(
|
||||
@@ -268,7 +278,7 @@ class Newsletter extends Model {
|
||||
|
||||
foreach($segments as $segment) {
|
||||
$newsletters = $segment->newsletters()
|
||||
->filter('filterType', $data['tab'])
|
||||
->filter('filterType', $type)
|
||||
->filter('groupBy', $data);
|
||||
|
||||
$newsletters_count = $newsletters->count();
|
||||
@@ -337,6 +347,13 @@ class Newsletter extends Model {
|
||||
static function groups($data = array()) {
|
||||
$type = isset($data['tab']) ? $data['tab'] : null;
|
||||
|
||||
// newsletter types without groups
|
||||
if(in_array($type, array(
|
||||
self::TYPE_NOTIFICATION_HISTORY
|
||||
))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$groups = array(
|
||||
array(
|
||||
'name' => 'all',
|
||||
@@ -460,7 +477,8 @@ class Newsletter extends Model {
|
||||
if(in_array($type, array(
|
||||
self::TYPE_STANDARD,
|
||||
self::TYPE_WELCOME,
|
||||
self::TYPE_NOTIFICATION
|
||||
self::TYPE_NOTIFICATION,
|
||||
self::TYPE_NOTIFICATION_HISTORY
|
||||
))) {
|
||||
$orm->where('type', $type);
|
||||
}
|
||||
|
@@ -245,6 +245,7 @@ class Newsletters {
|
||||
}
|
||||
|
||||
function listing($data = array()) {
|
||||
|
||||
$listing = new Listing\Handler(
|
||||
'\MailPoet\Models\Newsletter',
|
||||
$data
|
||||
@@ -266,7 +267,11 @@ class Newsletters {
|
||||
} else if($newsletter->type === Newsletter::TYPE_NOTIFICATION) {
|
||||
$newsletter
|
||||
->withOptions()
|
||||
->withSegments();
|
||||
} else if($newsletter->type === Newsletter::TYPE_NOTIFICATION_HISTORY) {
|
||||
$newsletter
|
||||
->withSegments()
|
||||
->withSendingQueue()
|
||||
->withStatistics();
|
||||
}
|
||||
|
||||
|
@@ -229,6 +229,9 @@
|
||||
'sendNthWeekDay': __('Send every %$1s %$2s of the month at %$3s'),
|
||||
'sendImmediately': __('Send immediately'),
|
||||
'ifNewContentToSegments': __("if there's new content to %$1s."),
|
||||
'sendingToSegmentsNotSpecified': __('You need to select a segment to send to.')
|
||||
'sendingToSegmentsNotSpecified': __('You need to select a segment to send to.'),
|
||||
|
||||
'backToPostNotifications': __('Back to Post notifications'),
|
||||
'sentOn': __('Sent on')
|
||||
}) %>
|
||||
<% endblock %>
|
||||
|
Reference in New Issue
Block a user