Merge pull request #552 from mailpoet/newsletter_listing
Post notification history listing
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
import MailPoet from 'mailpoet'
|
||||
import jQuery from 'jquery'
|
||||
import React from 'react'
|
||||
import _ from 'underscore'
|
||||
import { Router, Link } from 'react-router'
|
||||
import classNames from 'classnames'
|
||||
import ListingBulkActions from 'listing/bulk_actions.jsx'
|
||||
@ -13,7 +14,7 @@ import ListingFilters from 'listing/filters.jsx'
|
||||
const ListingItem = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {
|
||||
toggled: true
|
||||
expanded: false
|
||||
};
|
||||
},
|
||||
handleSelectItem: function(e) {
|
||||
@ -34,7 +35,7 @@ const ListingItem = React.createClass({
|
||||
this.props.onDeleteItem(id);
|
||||
},
|
||||
handleToggleItem: function(id) {
|
||||
this.setState({ toggled: !this.state.toggled });
|
||||
this.setState({ expanded: !this.state.expanded });
|
||||
},
|
||||
render: function() {
|
||||
var checkbox = false;
|
||||
@ -182,7 +183,7 @@ const ListingItem = React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
const row_classes = classNames({ 'is-expanded': !this.state.toggled });
|
||||
const row_classes = classNames({ 'is-expanded': this.state.expanded });
|
||||
|
||||
return (
|
||||
<tr className={ row_classes }>
|
||||
@ -303,13 +304,12 @@ const Listing = React.createClass({
|
||||
getParam: function(param) {
|
||||
const regex = /(.*)\[(.*)\]/;
|
||||
const matches = regex.exec(param);
|
||||
return [matches[1], matches[2]]
|
||||
return [matches[1], matches[2]];
|
||||
},
|
||||
initWithParams: function(params) {
|
||||
let state = this.getInitialState();
|
||||
|
||||
// check for url params
|
||||
if (params.splat !== undefined) {
|
||||
if (params.splat) {
|
||||
params.splat.split('/').map(param => {
|
||||
let [key, value] = this.getParam(param);
|
||||
switch(key) {
|
||||
@ -348,6 +348,17 @@ const Listing = React.createClass({
|
||||
this.getItems();
|
||||
}.bind(this));
|
||||
},
|
||||
getParams: function() {
|
||||
// get all route parameters (without the "splat")
|
||||
let params = _.omit(this.props.params, 'splat');
|
||||
// TODO:
|
||||
// find a way to set the "type" in the routes definition
|
||||
// so that it appears in `this.props.params`
|
||||
if (this.props.type) {
|
||||
params.type = this.props.type;
|
||||
}
|
||||
return params;
|
||||
},
|
||||
setParams: function() {
|
||||
if (this.props.location) {
|
||||
let params = Object.keys(this.state)
|
||||
@ -378,17 +389,37 @@ const Listing = React.createClass({
|
||||
.filter(key => { return (key !== undefined) })
|
||||
.join('/');
|
||||
|
||||
// prepend url with "tab" if specified
|
||||
if (this.props.tab !== undefined) {
|
||||
params = `/${ this.props.tab }/${ params }`;
|
||||
// set url
|
||||
let url = this.getUrlWithParams(params);
|
||||
|
||||
if (this.props.location.pathname !== url) {
|
||||
this.context.router.push(`${url}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
getUrlWithParams: function(params) {
|
||||
let base_url = (this.props.base_url !== undefined)
|
||||
? this.props.base_url
|
||||
: null;
|
||||
|
||||
if (base_url !== null) {
|
||||
base_url = this.setBaseUrlParams(base_url);
|
||||
return `/${ base_url }/${ params }`;
|
||||
} else {
|
||||
params = `/${ params }`;
|
||||
return `/${ params }`;
|
||||
}
|
||||
},
|
||||
setBaseUrlParams: function(base_url) {
|
||||
if (base_url.indexOf(':') !== -1) {
|
||||
const params = this.getParams();
|
||||
Object.keys(params).map((key) => {
|
||||
if (base_url.indexOf(':'+key) !== -1) {
|
||||
base_url = base_url.replace(':'+key, params[key]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this.props.location.pathname !== params) {
|
||||
this.context.router.push(`${params}`);
|
||||
}
|
||||
}
|
||||
return base_url;
|
||||
},
|
||||
componentDidMount: function() {
|
||||
if (this.isMounted()) {
|
||||
@ -416,7 +447,7 @@ const Listing = React.createClass({
|
||||
endpoint: this.props.endpoint,
|
||||
action: 'listing',
|
||||
data: {
|
||||
tab: (this.props.tab) ? this.props.tab : '',
|
||||
params: this.getParams(),
|
||||
offset: (this.state.page - 1) * this.state.limit,
|
||||
limit: this.state.limit,
|
||||
group: this.state.group,
|
||||
@ -531,7 +562,7 @@ const Listing = React.createClass({
|
||||
|
||||
var data = params || {};
|
||||
data.listing = {
|
||||
tab: (this.props.tab) ? this.props.tab : '',
|
||||
params: this.getParams(),
|
||||
offset: 0,
|
||||
limit: 0,
|
||||
filter: this.state.filter,
|
||||
|
152
assets/js/src/newsletters/listings/mixins.jsx
Normal file
152
assets/js/src/newsletters/listings/mixins.jsx
Normal file
@ -0,0 +1,152 @@
|
||||
import React from 'react'
|
||||
import MailPoet from 'mailpoet'
|
||||
import classNames from 'classnames'
|
||||
import jQuery from 'jquery'
|
||||
|
||||
const _QueueMixin = {
|
||||
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();
|
||||
});
|
||||
},
|
||||
renderQueueStatus: 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>
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const _StatisticsMixin = {
|
||||
renderStatistics: function(newsletter) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { _QueueMixin as QueueMixin };
|
||||
export { _StatisticsMixin as StatisticsMixin };
|
@ -252,6 +252,20 @@ const NewsletterListNotification = React.createClass({
|
||||
</span>
|
||||
);
|
||||
},
|
||||
renderHistoryLink: function(newsletter) {
|
||||
const childrenCount = ~~(newsletter.children_count);
|
||||
if (childrenCount === 0) {
|
||||
return (
|
||||
MailPoet.I18n.t('notSentYet')
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Link
|
||||
to={ `/notification/history/${ newsletter.id }` }
|
||||
>{ MailPoet.I18n.t('viewHistory') }</Link>
|
||||
);
|
||||
}
|
||||
},
|
||||
renderItem: function(newsletter, actions) {
|
||||
const rowClasses = classNames(
|
||||
'manage-column',
|
||||
@ -277,7 +291,7 @@ 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>
|
||||
{ this.renderHistoryLink(newsletter) }
|
||||
</td>
|
||||
<td className="column-date" data-colname={ MailPoet.I18n.t('lastModifiedOn') }>
|
||||
<abbr>{ MailPoet.Date.format(newsletter.updated_at) }</abbr>
|
||||
@ -299,7 +313,8 @@ const NewsletterListNotification = React.createClass({
|
||||
location={ this.props.location }
|
||||
params={ this.props.params }
|
||||
endpoint="newsletters"
|
||||
tab="notification"
|
||||
type="notification"
|
||||
base_url="notification"
|
||||
onRenderItem={ this.renderItem }
|
||||
columns={ columns }
|
||||
bulk_actions={ bulk_actions }
|
||||
|
125
assets/js/src/newsletters/listings/notification_history.jsx
Normal file
125
assets/js/src/newsletters/listings/notification_history.jsx
Normal file
@ -0,0 +1,125 @@
|
||||
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'
|
||||
|
||||
import { QueueMixin, StatisticsMixin } from 'newsletters/listings/mixins.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({
|
||||
mixins: [QueueMixin, StatisticsMixin],
|
||||
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
|
||||
href={ newsletter.preview_url }
|
||||
target="_blank"
|
||||
>{ newsletter.subject }</a>
|
||||
</strong>
|
||||
{ actions }
|
||||
</td>
|
||||
<td className="column" data-colname={ MailPoet.I18n.t('status') }>
|
||||
{ this.renderQueueStatus(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"
|
||||
type="notification_history"
|
||||
base_url="notification/history/:parent_id"
|
||||
onRenderItem={ this.renderItem }
|
||||
columns={columns}
|
||||
item_actions={ newsletter_actions }
|
||||
auto_refresh={ true }
|
||||
sort_by="updated_at"
|
||||
sort_order="desc"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = NewsletterListNotificationHistory;
|
@ -7,6 +7,8 @@ import MailPoet from 'mailpoet'
|
||||
import Listing from 'listing/listing.jsx'
|
||||
import ListingTabs from 'newsletters/listings/tabs.jsx'
|
||||
|
||||
import { QueueMixin, StatisticsMixin } from 'newsletters/listings/mixins.jsx'
|
||||
|
||||
const mailpoet_tracking_enabled = (!!(window['mailpoet_tracking_enabled']));
|
||||
|
||||
const messages = {
|
||||
@ -139,135 +141,7 @@ const newsletter_actions = [
|
||||
];
|
||||
|
||||
const NewsletterListStandard = 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>
|
||||
);
|
||||
}
|
||||
},
|
||||
mixins: [QueueMixin, StatisticsMixin],
|
||||
renderItem: function(newsletter, actions) {
|
||||
const rowClasses = classNames(
|
||||
'manage-column',
|
||||
@ -291,7 +165,7 @@ const NewsletterListStandard = React.createClass({
|
||||
{ actions }
|
||||
</td>
|
||||
<td className="column" data-colname={ MailPoet.I18n.t('status') }>
|
||||
{ this.renderStatus(newsletter) }
|
||||
{ this.renderQueueStatus(newsletter) }
|
||||
</td>
|
||||
<td className="column" data-colname={ MailPoet.I18n.t('lists') }>
|
||||
{ segments }
|
||||
@ -321,7 +195,8 @@ const NewsletterListStandard = React.createClass({
|
||||
location={ this.props.location }
|
||||
params={ this.props.params }
|
||||
endpoint="newsletters"
|
||||
tab="standard"
|
||||
type="standard"
|
||||
base_url="standard"
|
||||
onRenderItem={this.renderItem}
|
||||
columns={columns}
|
||||
bulk_actions={ bulk_actions }
|
||||
|
@ -343,7 +343,8 @@ const NewsletterListWelcome = React.createClass({
|
||||
location={ this.props.location }
|
||||
params={ this.props.params }
|
||||
endpoint="newsletters"
|
||||
tab="welcome"
|
||||
type="welcome"
|
||||
base_url="welcome"
|
||||
onRenderItem={ this.renderItem }
|
||||
columns={ columns }
|
||||
bulk_actions={ bulk_actions }
|
||||
|
@ -14,12 +14,13 @@ 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 });
|
||||
|
||||
const App = React.createClass({
|
||||
render() {
|
||||
return this.props.children
|
||||
return this.props.children;
|
||||
}
|
||||
});
|
||||
|
||||
@ -31,18 +32,16 @@ if(container) {
|
||||
<Route path="/" component={ App }>
|
||||
<IndexRedirect to="standard" />
|
||||
{/* Listings */}
|
||||
<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 path="standard/*" component={ NewsletterListStandard } />
|
||||
<Route path="welcome/*" component={ NewsletterListWelcome } />
|
||||
<Route path="notification/*" component={ NewsletterListNotification } />
|
||||
<Route path="standard(/)**" params={{ tab: 'standard' }} component={ NewsletterListStandard } />
|
||||
<Route path="welcome(/)**" component={ NewsletterListWelcome } />
|
||||
<Route path="notification/history/:parent_id(/)**" component={ NewsletterListNotificationHistory } />
|
||||
<Route path="notification(/)**" component={ NewsletterListNotification } />
|
||||
{/* Newsletter: type selection */}
|
||||
<Route path="new" component={ NewsletterTypes } />
|
||||
{/* New newsletter: types */}
|
||||
<Route name="new/standard" path="new/standard" component={ NewsletterTypeStandard } />
|
||||
<Route name="new/welcome" path="new/welcome" component={ NewsletterTypeWelcome } />
|
||||
<Route name="new/notification" path="new/notification" component={ NewsletterTypeNotification } />
|
||||
<Route path="new/standard" component={ NewsletterTypeStandard } />
|
||||
<Route path="new/welcome" component={ NewsletterTypeWelcome } />
|
||||
<Route path="new/notification" component={ NewsletterTypeNotification } />
|
||||
{/* Template selection */}
|
||||
<Route name="template" path="template/:id" component={ NewsletterTemplates } />
|
||||
{/* Sending options */}
|
||||
|
@ -32,7 +32,7 @@ class API {
|
||||
$endpoint = self::ENDPOINT_NAMESPACE . ucfirst($this->endpoint);
|
||||
if(!$this->api_request) return;
|
||||
if(!$this->endpoint || !class_exists($endpoint)) {
|
||||
$this->terminateRequest(self::RESPONSE_ERROR, __('Invalid API endpoint.'));
|
||||
self::terminateRequest(self::RESPONSE_ERROR, __('Invalid API endpoint.'));
|
||||
}
|
||||
$this->callEndpoint(
|
||||
$endpoint,
|
||||
@ -43,7 +43,7 @@ class API {
|
||||
|
||||
function callEndpoint($endpoint, $action, $data) {
|
||||
if(!method_exists($endpoint, $action)) {
|
||||
$this->terminateRequest(self::RESPONSE_ERROR, __('Invalid API action.'));
|
||||
self::terminateRequest(self::RESPONSE_ERROR, __('Invalid API action.'));
|
||||
}
|
||||
call_user_func(
|
||||
array(
|
||||
@ -83,7 +83,7 @@ class API {
|
||||
return add_query_arg($params, home_url());
|
||||
}
|
||||
|
||||
function terminateRequest($code, $message) {
|
||||
static function terminateRequest($code, $message) {
|
||||
status_header($code, $message);
|
||||
exit;
|
||||
}
|
||||
|
@ -56,6 +56,7 @@ class Initializer {
|
||||
\ORM::configure(Env::$db_source_name);
|
||||
\ORM::configure('username', Env::$db_username);
|
||||
\ORM::configure('password', Env::$db_password);
|
||||
\ORM::configure('logging', WP_DEBUG);
|
||||
\ORM::configure('driver_options', array(
|
||||
\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
|
||||
\PDO::MYSQL_ATTR_INIT_COMMAND =>
|
||||
|
@ -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 "",',
|
||||
|
@ -67,29 +67,33 @@ class Scheduler {
|
||||
}
|
||||
|
||||
function processPostNotificationNewsletter($newsletter, $queue) {
|
||||
// ensure that segments exist
|
||||
$segments = $newsletter->segments()->findArray();
|
||||
if(empty($segments)) {
|
||||
$this->deleteQueueOrUpdateNextRunDate($queue, $newsletter);
|
||||
return;
|
||||
}
|
||||
$segment_ids = array_map(function($segment) {
|
||||
return $segment['id'];
|
||||
return (int)$segment['id'];
|
||||
}, $segments);
|
||||
|
||||
// ensure that subscribers are in 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;
|
||||
}
|
||||
// schedule new queue if the post notification is not destined for immediate delivery
|
||||
if($newsletter->intervalType !== NewsletterScheduler::INTERVAL_IMMEDIATELY) {
|
||||
$new_queue = SendingQueue::create();
|
||||
$new_queue->newsletter_id = $newsletter->id;
|
||||
$new_queue->status = SendingQueue::STATUS_SCHEDULED;
|
||||
self::deleteQueueOrUpdateNextRunDate($new_queue, $newsletter);
|
||||
}
|
||||
|
||||
// create a duplicate newsletter that acts as a history record
|
||||
$notification_history = $this->createNotificationHistory($newsletter->id);
|
||||
if(!$notification_history) return;
|
||||
|
||||
// queue newsletter for delivery
|
||||
$queue->newsletter_id = $notification_history->id;
|
||||
$queue->subscribers = serialize(
|
||||
array(
|
||||
'to_process' => $subscribers
|
||||
@ -164,7 +168,7 @@ class Scheduler {
|
||||
return true;
|
||||
}
|
||||
|
||||
private function deleteQueueOrUpdateNextRunDate($queue, $newsletter) {
|
||||
function deleteQueueOrUpdateNextRunDate($queue, $newsletter) {
|
||||
if($newsletter->intervalType === NewsletterScheduler::INTERVAL_IMMEDIATELY) {
|
||||
$queue->delete();
|
||||
} else {
|
||||
@ -173,4 +177,12 @@ class Scheduler {
|
||||
$queue->save();
|
||||
}
|
||||
}
|
||||
|
||||
function createNotificationHistory($newsletter_id) {
|
||||
$newsletter = Newsletter::findOne($newsletter_id);
|
||||
$notification_history = $newsletter->createNotificationHistory();
|
||||
return ($notification_history->getErrors() === false) ?
|
||||
$notification_history :
|
||||
false;
|
||||
}
|
||||
}
|
@ -178,10 +178,11 @@ class SendingQueue {
|
||||
if(!$queue->count_to_process) {
|
||||
$queue->processed_at = current_time('mysql');
|
||||
$queue->status = SendingQueueModel::STATUS_COMPLETED;
|
||||
// set newsletter status to sent
|
||||
// if it's a standard or post notificaiton newsletter, update its status to sent
|
||||
$newsletter = NewsletterModel::findOne($queue->newsletter_id);
|
||||
// if it's a standard newsletter, update its status
|
||||
if($newsletter->type === NewsletterModel::TYPE_STANDARD) {
|
||||
if($newsletter->type === NewsletterModel::TYPE_STANDARD ||
|
||||
$newsletter->type === NewsletterModel::TYPE_NOTIFICATION_HISTORY
|
||||
) {
|
||||
$newsletter->setStatus(NewsletterModel::STATUS_SENT);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
<?php
|
||||
namespace MailPoet\Cron\Workers\SendingQueue\Tasks;
|
||||
|
||||
use MailPoet\Models\Newsletter;
|
||||
use MailPoet\Models\NewsletterPost;
|
||||
|
||||
if(!defined('ABSPATH')) exit;
|
||||
@ -18,9 +19,12 @@ class Posts {
|
||||
if(!count($matched_posts_ids)) {
|
||||
return $newsletter;
|
||||
}
|
||||
$newsletter_id = ($newsletter['type'] === Newsletter::TYPE_NOTIFICATION_HISTORY) ?
|
||||
$newsletter['parent_id'] :
|
||||
$newsletter['id'];
|
||||
foreach($matched_posts_ids as $post_id) {
|
||||
$newletter_post = NewsletterPost::create();
|
||||
$newletter_post->newsletter_id = $newsletter['id'];
|
||||
$newletter_post->newsletter_id = $newsletter_id;
|
||||
$newletter_post->post_id = $post_id;
|
||||
$newletter_post->save();
|
||||
}
|
||||
|
@ -16,8 +16,8 @@ class Handler {
|
||||
$this->model = \Model::factory($this->model_class);
|
||||
|
||||
$this->data = array(
|
||||
// tabs
|
||||
'tab' => (isset($data['tab']) ? $data['tab'] : false),
|
||||
// extra parameters
|
||||
'params' => (isset($data['params']) ? $data['params'] : array()),
|
||||
// pagination
|
||||
'offset' => (isset($data['offset']) ? (int)$data['offset'] : 0),
|
||||
'limit' => (isset($data['limit'])
|
||||
@ -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';
|
||||
@ -19,7 +20,6 @@ class Newsletter extends Model {
|
||||
// automatic newsletters status
|
||||
const STATUS_ACTIVE = 'active';
|
||||
|
||||
|
||||
function __construct() {
|
||||
parent::__construct();
|
||||
|
||||
@ -56,7 +56,6 @@ class Newsletter extends Model {
|
||||
}
|
||||
|
||||
function duplicate($data = array()) {
|
||||
// get current newsletter's data as an array
|
||||
$newsletter_data = $this->asArray();
|
||||
|
||||
// remove id so that it creates a new record
|
||||
@ -109,6 +108,43 @@ class Newsletter extends Model {
|
||||
return $duplicate;
|
||||
}
|
||||
|
||||
function createNotificationHistory() {
|
||||
$newsletter_data = $this->asArray();
|
||||
|
||||
// remove id so that it creates a new record
|
||||
unset($newsletter_data['id']);
|
||||
|
||||
$data = array_merge(
|
||||
$newsletter_data,
|
||||
array(
|
||||
'parent_id' => $this->id,
|
||||
'type' => self::TYPE_NOTIFICATION_HISTORY,
|
||||
'status' => self::STATUS_SENDING
|
||||
)
|
||||
);
|
||||
|
||||
$notification_history = self::create();
|
||||
$notification_history->hydrate($data);
|
||||
|
||||
$notification_history->save();
|
||||
|
||||
if($notification_history->getErrors() === false) {
|
||||
// create relationships between notification history and segments
|
||||
$segments = $this->segments()->findArray();
|
||||
|
||||
if(!empty($segments)) {
|
||||
foreach($segments as $segment) {
|
||||
$relation = NewsletterSegment::create();
|
||||
$relation->segment_id = $segment['id'];
|
||||
$relation->newsletter_id = $notification_history->id;
|
||||
$relation->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $notification_history;
|
||||
}
|
||||
|
||||
function asArray() {
|
||||
$model = parent::asArray();
|
||||
|
||||
@ -125,6 +161,14 @@ class Newsletter extends Model {
|
||||
return parent::delete();
|
||||
}
|
||||
|
||||
function children() {
|
||||
return $this->has_many(
|
||||
__NAMESPACE__.'\Newsletter',
|
||||
'parent_id',
|
||||
'id'
|
||||
);
|
||||
}
|
||||
|
||||
function segments() {
|
||||
return $this->has_many_through(
|
||||
__NAMESPACE__.'\Segment',
|
||||
@ -139,6 +183,11 @@ class Newsletter extends Model {
|
||||
return $this;
|
||||
}
|
||||
|
||||
function withChildrenCount() {
|
||||
$this->children_count = $this->children()->count();
|
||||
return $this;
|
||||
}
|
||||
|
||||
function options() {
|
||||
return $this->has_many_through(
|
||||
__NAMESPACE__.'\NewsletterOptionField',
|
||||
@ -189,15 +238,11 @@ class Newsletter extends Model {
|
||||
} else {
|
||||
$this->statistics = $statistics->asArray();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
function getStatistics() {
|
||||
if($this->queue === false) {
|
||||
return false;
|
||||
}
|
||||
return SendingQueue::tableAlias('queues')
|
||||
$statistics_query = SendingQueue::tableAlias('queues')
|
||||
->selectExpr(
|
||||
'COUNT(DISTINCT(clicks.subscriber_id)) as clicked, ' .
|
||||
'COUNT(DISTINCT(opens.subscriber_id)) as opened, ' .
|
||||
@ -217,10 +262,23 @@ class Newsletter extends Model {
|
||||
MP_STATISTICS_UNSUBSCRIBES_TABLE,
|
||||
'queues.id = unsubscribes.queue_id',
|
||||
'unsubscribes'
|
||||
)
|
||||
);
|
||||
|
||||
if($this->type === self::TYPE_WELCOME) {
|
||||
return $statistics_query
|
||||
->where('queues.newsletter_id', $this->id)
|
||||
->where('queues.status', SendingQueue::STATUS_COMPLETED)
|
||||
->findOne();
|
||||
} else {
|
||||
if($this->queue === false) {
|
||||
return false;
|
||||
} else {
|
||||
return $statistics_query
|
||||
->where('queues.id', $this->queue['id'])
|
||||
->findOne();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static function search($orm, $search = '') {
|
||||
if(strlen(trim($search)) > 0) {
|
||||
@ -230,6 +288,15 @@ class Newsletter extends Model {
|
||||
}
|
||||
|
||||
static function filters($data = array()) {
|
||||
$type = isset($data['params']['type']) ? $data['params']['type'] : 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(
|
||||
@ -239,7 +306,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();
|
||||
@ -260,18 +327,32 @@ class Newsletter extends Model {
|
||||
}
|
||||
|
||||
static function filterBy($orm, $data = array()) {
|
||||
$type = isset($data['tab']) ? $data['tab'] : null;
|
||||
|
||||
// apply filters
|
||||
if(!empty($data['filter'])) {
|
||||
foreach($data['filter'] as $key => $value) {
|
||||
if($key === 'segment') {
|
||||
$segment = Segment::findOne($value);
|
||||
if($segment !== false) {
|
||||
$orm = $segment->newsletters()->filter('filterType', $type);
|
||||
$orm = $segment->newsletters();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// filter by type
|
||||
$type = isset($data['params']['type']) ? $data['params']['type'] : null;
|
||||
if($type !== null) {
|
||||
$orm->filter('filterType', $type);
|
||||
}
|
||||
|
||||
// filter by parent id
|
||||
$parent_id = isset($data['params']['parent_id'])
|
||||
? (int)$data['params']['parent_id']
|
||||
: null;
|
||||
if($parent_id !== null) {
|
||||
$orm->where('parent_id', $parent_id);
|
||||
}
|
||||
|
||||
return $orm;
|
||||
}
|
||||
|
||||
@ -306,7 +387,14 @@ class Newsletter extends Model {
|
||||
}
|
||||
|
||||
static function groups($data = array()) {
|
||||
$type = isset($data['tab']) ? $data['tab'] : null;
|
||||
$type = isset($data['params']['type']) ? $data['params']['type'] : null;
|
||||
|
||||
// newsletter types without groups
|
||||
if(in_array($type, array(
|
||||
self::TYPE_NOTIFICATION_HISTORY
|
||||
))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$groups = array(
|
||||
array(
|
||||
@ -431,7 +519,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);
|
||||
}
|
||||
@ -447,7 +536,6 @@ class Newsletter extends Model {
|
||||
'updated_at',
|
||||
'deleted_at'
|
||||
))
|
||||
->filter('filterType', $data['tab'])
|
||||
->filter('filterBy', $data)
|
||||
->filter('groupBy', $data)
|
||||
->filter('search', $data['search']);
|
||||
|
@ -1,6 +1,7 @@
|
||||
<?php
|
||||
namespace MailPoet\Newsletter\Renderer\Blocks;
|
||||
|
||||
use MailPoet\Models\Newsletter;
|
||||
use MailPoet\Newsletter\Renderer\StylesHelper;
|
||||
|
||||
class Renderer {
|
||||
@ -10,7 +11,10 @@ class Renderer {
|
||||
function __construct(array $newsletter, $posts = false) {
|
||||
$this->newsletter = $newsletter;
|
||||
$this->posts = array();
|
||||
$this->ALC = new \MailPoet\Newsletter\AutomatedLatestContent($this->newsletter['id']);
|
||||
$newsletter_id = ($newsletter['type'] === Newsletter::TYPE_NOTIFICATION_HISTORY) ?
|
||||
$newsletter['parent_id'] :
|
||||
$newsletter['id'];
|
||||
$this->ALC = new \MailPoet\Newsletter\AutomatedLatestContent($newsletter_id);
|
||||
}
|
||||
|
||||
function render($data, $column_count) {
|
||||
@ -45,12 +49,12 @@ class Renderer {
|
||||
|
||||
function processAutomatedLatestContent($args, $column_count) {
|
||||
$posts_to_exclude = $this->getPosts();
|
||||
$ALCPosts = $this->ALC->getPosts($args, $posts_to_exclude);
|
||||
foreach($ALCPosts as $post) {
|
||||
$ALC_posts = $this->ALC->getPosts($args, $posts_to_exclude);
|
||||
foreach($ALC_posts as $post) {
|
||||
$posts_to_exclude[] = $post->ID;
|
||||
}
|
||||
$transformed_posts = array(
|
||||
'blocks' => $this->ALC->transformPosts($args, $ALCPosts)
|
||||
'blocks' => $this->ALC->transformPosts($args, $ALC_posts)
|
||||
);
|
||||
$this->setPosts($posts_to_exclude);
|
||||
$transformed_posts = StylesHelper::applyTextAlignment($transformed_posts);
|
||||
|
@ -245,6 +245,7 @@ class Newsletters {
|
||||
}
|
||||
|
||||
function listing($data = array()) {
|
||||
|
||||
$listing = new Listing\Handler(
|
||||
'\MailPoet\Models\Newsletter',
|
||||
$data
|
||||
@ -267,6 +268,11 @@ class Newsletters {
|
||||
$newsletter
|
||||
->withOptions()
|
||||
->withSegments()
|
||||
->withChildrenCount();
|
||||
} else if($newsletter->type === Newsletter::TYPE_NOTIFICATION_HISTORY) {
|
||||
$newsletter
|
||||
->withSegments()
|
||||
->withSendingQueue()
|
||||
->withStatistics();
|
||||
}
|
||||
|
||||
|
@ -37,8 +37,10 @@ class SendingQueue {
|
||||
);
|
||||
}
|
||||
|
||||
if($newsletter->type === Newsletter::TYPE_WELCOME) {
|
||||
// set welcome email active
|
||||
if($newsletter->type === Newsletter::TYPE_WELCOME ||
|
||||
$newsletter->type === Newsletter::TYPE_NOTIFICATION
|
||||
) {
|
||||
// set newsletter status to active
|
||||
$result = $newsletter->setStatus(Newsletter::STATUS_ACTIVE);
|
||||
$errors = $result->getErrors();
|
||||
|
||||
@ -48,20 +50,16 @@ class SendingQueue {
|
||||
'errors' => $errors
|
||||
);
|
||||
} else {
|
||||
$message = ($newsletter->type === Newsletter::TYPE_WELCOME) ?
|
||||
__('Your welcome email has been activated') :
|
||||
__('Your post notification has been activated');
|
||||
return array(
|
||||
'result' => true,
|
||||
'data' => array(
|
||||
'message' => __('Your welcome email has been activated')
|
||||
'message' => $message
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if($newsletter->type === Newsletter::TYPE_NOTIFICATION) {
|
||||
// Post Notifications
|
||||
$newsletter = Scheduler::processPostNotificationSchedule($newsletter->id);
|
||||
Scheduler::createPostNotificationQueue($newsletter);
|
||||
|
||||
// set post notification active
|
||||
$newsletter->setStatus(Newsletter::STATUS_ACTIVE);
|
||||
}
|
||||
|
||||
$queue = SendingQueueModel::whereNull('status')
|
||||
@ -83,18 +81,6 @@ class SendingQueue {
|
||||
$queue->newsletter_id = $newsletter->id;
|
||||
}
|
||||
|
||||
if($newsletter->type === Newsletter::TYPE_NOTIFICATION) {
|
||||
$queue->scheduled_at = Scheduler::getNextRunDate($newsletter->schedule);
|
||||
$queue->status = SendingQueueModel::STATUS_SCHEDULED;
|
||||
$queue->save();
|
||||
return array(
|
||||
'result' => true,
|
||||
'data' => array(
|
||||
'message' => __('Your post notification has been activated')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if((bool)$newsletter->isScheduled) {
|
||||
// set newsletter status
|
||||
$newsletter->setStatus(Newsletter::STATUS_SCHEDULED);
|
||||
|
@ -19,7 +19,9 @@ class NewsletterRendererTest extends MailPoetTest {
|
||||
),
|
||||
'id' => 1,
|
||||
'subject' => 'Some subject',
|
||||
'preheader' => 'Some preheader'
|
||||
'preheader' => 'Some preheader',
|
||||
'type' => 'standard',
|
||||
'status' => 'active'
|
||||
);
|
||||
$this->renderer = new Renderer($this->newsletter);
|
||||
$this->column_renderer = new ColumnRenderer();
|
||||
|
@ -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