Merge pull request #1017 from mailpoet/rerender-newsletter

Rerender newsletter [MAILPOET-675]
This commit is contained in:
Tautvidas Sipavičius
2017-08-17 16:19:16 +03:00
committed by GitHub
14 changed files with 215 additions and 30 deletions

View File

@ -180,6 +180,7 @@ define([
<select <select
id={ this.props.field.id || this.props.field.name } id={ this.props.field.id || this.props.field.name }
ref="select" ref="select"
disabled={this.props.field.disabled}
data-placeholder={ this.props.field.placeholder } data-placeholder={ this.props.field.placeholder }
multiple={ this.props.field.multiple } multiple={ this.props.field.multiple }
defaultValue={ this.getSelectedValues() } defaultValue={ this.getSelectedValues() }

View File

@ -97,6 +97,17 @@ const bulk_actions = [
}, },
]; ];
const confirmEdit = (newsletter) => {
if(
!newsletter.queue
|| newsletter.status != 'sending'
|| newsletter.queue.status !== null
|| window.confirm(MailPoet.I18n.t('confirmEdit'))
) {
window.location.href = `?page=mailpoet-newsletter-editor&id=${ newsletter.id }`;
}
};
let newsletter_actions = [ let newsletter_actions = [
{ {
name: 'view', name: 'view',
@ -110,13 +121,8 @@ let newsletter_actions = [
}, },
{ {
name: 'edit', name: 'edit',
link: function (newsletter) { label: MailPoet.I18n.t('edit'),
return ( onClick: confirmEdit,
<a href={ `?page=mailpoet-newsletter-editor&id=${ newsletter.id }` }>
{MailPoet.I18n.t('edit')}
</a>
);
},
}, },
{ {
name: 'duplicate', name: 'duplicate',
@ -172,7 +178,8 @@ const NewsletterListStandard = React.createClass({
<strong> <strong>
<a <a
className="row-title" className="row-title"
href={ `?page=mailpoet-newsletter-editor&id=${ newsletter.id }` } href="javascript:;"
onClick={() => confirmEdit(newsletter)}
>{ newsletter.queue.newsletter_rendered_subject || newsletter.subject }</a> >{ newsletter.queue.newsletter_rendered_subject || newsletter.subject }</a>
</strong> </strong>
{ actions } { actions }

View File

@ -168,6 +168,40 @@ define(
} }
return false; return false;
}, },
handleResume: function (e) {
e.preventDefault();
if(!this.isValid()) {
jQuery('#mailpoet_newsletter').parsley().validate();
} else {
this._save(e).done(() => {
this.setState({ loading: true });
}).done(() => {
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'sendingQueue',
action: 'resume',
data: {
newsletter_id: this.state.item.id,
},
}).done(() => {
this.context.router.push(`/${ this.state.item.type || '' }`);
MailPoet.Notice.success(
MailPoet.I18n.t('newsletterSendingHasBeenResumed')
);
}).fail((response) => {
if (response.errors.length > 0) {
MailPoet.Notice.error(
response.errors.map((error) => { return error.message; }),
{ scroll: true }
);
}
});
}).fail(this._showError).always(() => {
this.setState({ loading: false });
});
}
return false;
},
handleSave: function (e) { handleSave: function (e) {
e.preventDefault(); e.preventDefault();
@ -193,6 +227,7 @@ define(
}, },
_save: function () { _save: function () {
const data = this.state.item; const data = this.state.item;
data.queue = undefined;
this.setState({ loading: true }); this.setState({ loading: true });
// Store only properties that can be changed on this page // Store only properties that can be changed on this page
@ -234,6 +269,15 @@ define(
return true; return true;
}, },
render: function () { render: function () {
const isPaused = this.state.item.status == 'sending'
&& this.state.item.queue
&& this.state.item.queue.status == 'paused';
const fields = this.state.fields.map((field) => {
if (field.name == 'segments' || field.name == 'options') {
field.disabled = isPaused;
}
return field;
});
return ( return (
<div> <div>
<h1>{MailPoet.I18n.t('finalNewsletterStep')}</h1> <h1>{MailPoet.I18n.t('finalNewsletterStep')}</h1>
@ -242,20 +286,29 @@ define(
<Form <Form
id="mailpoet_newsletter" id="mailpoet_newsletter"
fields={ this.state.fields } fields={ fields }
item={ this.state.item } item={ this.state.item }
loading={ this.state.loading } loading={ this.state.loading }
onChange={this.handleFormChange} onChange={this.handleFormChange}
onSubmit={this.handleSave} onSubmit={this.handleSave}
> >
<p className="submit"> <p className="submit">
<input {
isPaused ?
<input
className="button button-primary"
type="button"
onClick={ this.handleResume }
value={MailPoet.I18n.t('resume')} />
:
<input
className="button button-primary" className="button button-primary"
type="button" type="button"
onClick={ this.handleSend } onClick={ this.handleSend }
value={MailPoet.I18n.t('send')} value={MailPoet.I18n.t('send')}
{...this.getSendButtonOptions()} {...this.getSendButtonOptions()}
/> />
}
&nbsp; &nbsp;
<input <input
className="button button-secondary" className="button button-secondary"

View File

@ -155,6 +155,7 @@ define(
name={this.getFieldName()} name={this.getFieldName()}
value={this.getDisplayDate(this.props.value)} value={this.getDisplayDate(this.props.value)}
readOnly={ true } readOnly={ true }
disabled={this.props.disabled}
onChange={this.onChange} onChange={this.onChange}
ref="dateInput" ref="dateInput"
{...this.props.validation} /> {...this.props.validation} />
@ -180,6 +181,7 @@ define(
<select <select
name={this.props.name || 'time'} name={this.props.name || 'time'}
value={this.props.value} value={this.props.value}
disabled={this.props.disabled}
onChange={this.props.onChange} onChange={this.props.onChange}
{...this.props.validation} {...this.props.validation}
> >
@ -235,11 +237,13 @@ define(
onChange={this.handleChange} onChange={this.handleChange}
displayFormat={dateDisplayFormat} displayFormat={dateDisplayFormat}
storageFormat={dateStorageFormat} storageFormat={dateStorageFormat}
disabled={this.props.disabled}
validation={this.props.dateValidation}/> validation={this.props.dateValidation}/>
<TimeSelect <TimeSelect
name="time" name="time"
value={this.state.time} value={this.state.time}
onChange={this.handleChange} onChange={this.handleChange}
disabled={this.props.disabled}
validation={this.props.timeValidation} /> validation={this.props.timeValidation} />
</span> </span>
); );
@ -292,6 +296,7 @@ define(
name="scheduledAt" name="scheduledAt"
value={this._getCurrentValue().scheduledAt} value={this._getCurrentValue().scheduledAt}
onChange={this.handleValueChange} onChange={this.handleValueChange}
disabled={this.props.field.disabled}
dateValidation={this.getDateValidation()} /> dateValidation={this.getDateValidation()} />
&nbsp; &nbsp;
<span> <span>
@ -300,7 +305,6 @@ define(
</span> </span>
); );
} }
return ( return (
<div> <div>
<input <input
@ -308,6 +312,7 @@ define(
type="checkbox" type="checkbox"
value="1" value="1"
checked={this.isScheduled()} checked={this.isScheduled()}
disabled={this.props.field.disabled}
name="isScheduled" name="isScheduled"
onChange={this.handleCheckboxChange} /> onChange={this.handleCheckboxChange} />

View File

@ -33,6 +33,7 @@ class Newsletters extends APIEndpoint {
$newsletter = $newsletter $newsletter = $newsletter
->withSegments() ->withSegments()
->withOptions() ->withOptions()
->withSendingQueue()
->asArray(); ->asArray();
$newsletter = Hooks::applyFilters('mailpoet_api_newsletters_get_after', $newsletter); $newsletter = Hooks::applyFilters('mailpoet_api_newsletters_get_after', $newsletter);
return $this->successResponse($newsletter); return $this->successResponse($newsletter);
@ -110,6 +111,13 @@ class Newsletters extends APIEndpoint {
} }
} }
$queue = $newsletter->getQueue();
if($queue) {
$queue->newsletter_rendered_body = null;
$queue->newsletter_rendered_subject = null;
$queue->save();
}
Hooks::doAction('mailpoet_api_newsletters_save_after', $newsletter); Hooks::doAction('mailpoet_api_newsletters_save_after', $newsletter);
return $this->successResponse($newsletter->asArray()); return $this->successResponse($newsletter->asArray());
@ -455,4 +463,4 @@ class Newsletters extends APIEndpoint {
); );
} }
} }
} }

View File

@ -190,4 +190,4 @@ class SendingQueue {
->whereNull('type') ->whereNull('type')
->findMany(); ->findMany();
} }
} }

View File

@ -15,15 +15,15 @@ if(!defined('ABSPATH')) exit;
class Links { class Links {
static function process($rendered_newsletter, $newsletter, $queue) { static function process($rendered_newsletter, $newsletter, $queue) {
list($rendered_newsletter, $links) = list($rendered_newsletter, $links) =
self::hashAndReplaceLinks($rendered_newsletter); self::hashAndReplaceLinks($rendered_newsletter, $newsletter->id, $queue->id);
self::saveLinks($links, $newsletter, $queue); self::saveLinks($links, $newsletter, $queue);
return $rendered_newsletter; return $rendered_newsletter;
} }
static function hashAndReplaceLinks($rendered_newsletter) { static function hashAndReplaceLinks($rendered_newsletter, $newsletter_id, $queue_id) {
// join HTML and TEXT rendered body into a text string // join HTML and TEXT rendered body into a text string
$content = Helpers::joinObject($rendered_newsletter); $content = Helpers::joinObject($rendered_newsletter);
list($content, $links) = NewsletterLinks::process($content); list($content, $links) = NewsletterLinks::process($content, $newsletter_id, $queue_id);
// split the processed body with hashed links back to HTML and TEXT // split the processed body with hashed links back to HTML and TEXT
list($rendered_newsletter['html'], $rendered_newsletter['text']) list($rendered_newsletter['html'], $rendered_newsletter['text'])
= Helpers::splitObject($content); = Helpers::splitObject($content);

View File

@ -175,4 +175,4 @@ class Newsletter {
__('There was an error processing your newsletter during sending. If possible, please contact us and report this issue.') __('There was an error processing your newsletter during sending. If possible, please contact us and report this issue.')
); );
} }
} }

View File

@ -8,8 +8,8 @@ use MailPoet\Newsletter\Shortcodes\Shortcodes;
use MailPoet\Router\Endpoints\Track as TrackEndpoint; use MailPoet\Router\Endpoints\Track as TrackEndpoint;
use MailPoet\Router\Router; use MailPoet\Router\Router;
use MailPoet\Util\Helpers; use MailPoet\Util\Helpers;
use MailPoet\Util\pQuery\pQuery as DomParser;
use MailPoet\Util\Security; use MailPoet\Util\Security;
use MailPoet\Util\pQuery\pQuery as DomParser;
class Links { class Links {
const DATA_TAG_CLICK = '[mailpoet_click_data]'; const DATA_TAG_CLICK = '[mailpoet_click_data]';
@ -17,9 +17,10 @@ class Links {
const LINK_TYPE_SHORTCODE = 'shortcode'; const LINK_TYPE_SHORTCODE = 'shortcode';
const LINK_TYPE_URL = 'link'; const LINK_TYPE_URL = 'link';
static function process($content) { static function process($content, $newsletter_id, $queue_id) {
$extracted_links = self::extract($content); $extracted_links = self::extract($content);
$processed_links = self::hash($extracted_links); $saved_links = self::load($newsletter_id, $queue_id);
$processed_links = self::hash($extracted_links, $saved_links);
return self::replace($content, $processed_links); return self::replace($content, $processed_links);
} }
@ -51,13 +52,31 @@ class Links {
return array_unique($extracted_links, SORT_REGULAR); return array_unique($extracted_links, SORT_REGULAR);
} }
static function hash($extracted_links) { static function load($newsletter_id, $queue_id) {
$processed_links = array(); $links = NewsletterLink::whereEqual('newsletter_id', $newsletter_id)
->whereEqual('queue_id', $queue_id)
->findMany();
$saved_links = array();
foreach($links as $link) {
$saved_links[$link->url] = $link->asArray();
}
return $saved_links;
}
static function hash($extracted_links, $saved_links) {
$processed_links = array_map(function(&$link) {
$link['type'] = Links::LINK_TYPE_URL;
$link['link'] = $link['url'];
$link['processed_link'] = self::DATA_TAG_CLICK . '-' . $link['hash'];
return $link;
}, $saved_links);
foreach($extracted_links as $extracted_link) { foreach($extracted_links as $extracted_link) {
$link = $extracted_link['link'];
if (array_key_exists($link, $processed_links))
continue;
$hash = Security::generateHash(); $hash = Security::generateHash();
// Use URL as a key to map between extracted and processed links // Use URL as a key to map between extracted and processed links
// regardless of their sequential position (useful for link skips etc.) // regardless of their sequential position (useful for link skips etc.)
$link = $extracted_link['link'];
$processed_links[$link] = array( $processed_links[$link] = array(
'type' => $extracted_link['type'], 'type' => $extracted_link['type'],
'hash' => $hash, 'hash' => $hash,
@ -137,6 +156,8 @@ class Links {
static function save(array $links, $newsletter_id, $queue_id) { static function save(array $links, $newsletter_id, $queue_id) {
foreach($links as $link) { foreach($links as $link) {
if (isset($link['id']))
continue;
if(empty($link['hash']) || empty($link['link'])) continue; if(empty($link['hash']) || empty($link['link'])) continue;
$newsletter_link = NewsletterLink::create(); $newsletter_link = NewsletterLink::create();
$newsletter_link->newsletter_id = $newsletter_id; $newsletter_link->newsletter_id = $newsletter_id;
@ -198,4 +219,4 @@ class Links {
$transformed_data['preview'] = (!empty($data[4])) ? $data[4] : false; $transformed_data['preview'] = (!empty($data[4])) ? $data[4] : false;
return $transformed_data; return $transformed_data;
} }
} }

View File

@ -99,6 +99,7 @@ class NewslettersTest extends \MailPoetTest {
Newsletter::findOne($this->newsletter->id) Newsletter::findOne($this->newsletter->id)
->withSegments() ->withSegments()
->withOptions() ->withOptions()
->withSendingQueue()
->asArray() ->asArray()
); );
$hook_name = 'mailpoet_api_newsletters_get_after'; $hook_name = 'mailpoet_api_newsletters_get_after';
@ -163,6 +164,31 @@ class NewslettersTest extends \MailPoetTest {
expect($updated_newsletter->subject)->equals('My Updated Newsletter'); expect($updated_newsletter->subject)->equals('My Updated Newsletter');
} }
function testItCanClearRenderedQueueUponSave() {
$sending_queue = SendingQueue::create();
$sending_queue->newsletter_id = $this->newsletter->id;
$sending_queue->status = SendingQueue::STATUS_SCHEDULED;
$sending_queue->newsletter_rendered_body = 'Rendered body ...';
$sending_queue->newsletter_rendered_subject = 'Rendered subject ...';
$sending_queue->save();
$router = new Newsletters();
$newsletter_data = array(
'id' => $this->newsletter->id,
'subject' => 'My Updated Newsletter'
);
$response = $router->save($newsletter_data);
$updated_newsletter = Newsletter::findOne($this->newsletter->id)
->withSendingQueue()
->asArray();
$updated_queue = $updated_newsletter['queue'];
expect($response->status)->equals(APIResponse::STATUS_OK);
expect(unserialize($updated_queue['newsletter_rendered_body']))->equals(null);
expect(unserialize($updated_queue['newsletter_rendered_subject']))->equals(null);
}
function testItCanUpdatePostNotificationScheduleUponSave() { function testItCanUpdatePostNotificationScheduleUponSave() {
$newsletter_options = array( $newsletter_options = array(
'intervalType', 'intervalType',

View File

@ -30,7 +30,7 @@ class LinksTest extends \MailPoetTest {
'html' => '<a href="http://example.com">Example Link</a>', 'html' => '<a href="http://example.com">Example Link</a>',
'text' => '<a href="http://example.com">Example Link</a>' 'text' => '<a href="http://example.com">Example Link</a>'
); );
$result = Links::hashAndReplaceLinks($rendered_newsletter); $result = Links::hashAndReplaceLinks($rendered_newsletter, 0, 0);
$processed_rendered_newsletter_body = $result[0]; $processed_rendered_newsletter_body = $result[0];
$processed_and_hashed_links = $result[1]; $processed_and_hashed_links = $result[1];
expect($processed_rendered_newsletter_body['html']) expect($processed_rendered_newsletter_body['html'])
@ -59,4 +59,4 @@ class LinksTest extends \MailPoetTest {
\ORM::raw_execute('TRUNCATE ' . Newsletter::$_table); \ORM::raw_execute('TRUNCATE ' . Newsletter::$_table);
\ORM::raw_execute('TRUNCATE ' . NewsletterLink::$_table); \ORM::raw_execute('TRUNCATE ' . NewsletterLink::$_table);
} }
} }

View File

@ -24,7 +24,7 @@ class LinksTest extends \MailPoetTest {
function testItOnlyHashesAndReplacesLinksInAnchorTags() { function testItOnlyHashesAndReplacesLinksInAnchorTags() {
$template = '<a href="http://example.com"><img src="http://example.com" /></a>'; $template = '<a href="http://example.com"><img src="http://example.com" /></a>';
$result = Links::process($template); $result = Links::process($template, 0, 0);
expect($result[0])->equals( expect($result[0])->equals(
sprintf( sprintf(
'<a href="%s-%s"><img src="http://example.com" /></a>', '<a href="%s-%s"><img src="http://example.com" /></a>',
@ -34,6 +34,25 @@ class LinksTest extends \MailPoetTest {
); );
} }
function testItDoesNotRehashExistingLinks() {
$link = NewsletterLink::create();
$link->newsletter_id = 3;
$link->queue_id = 3;
$link->hash = 123;
$link->url = 'http://example.com';
$link->save();
$template = '<a href="http://example.com"><img src="http://example.com" /></a>';
$result = Links::process($template, 3, 3);
expect($result[0])->equals(
sprintf(
'<a href="%s-%s"><img src="http://example.com" /></a>',
Links::DATA_TAG_CLICK,
123
)
);
}
function testItCanExtactLinkShortcodes() { function testItCanExtactLinkShortcodes() {
$template = '[notlink:shortcode] [link:some_link_shortcode]'; $template = '[notlink:shortcode] [link:some_link_shortcode]';
$result = Links::extract($template); $result = Links::extract($template);
@ -48,7 +67,7 @@ class LinksTest extends \MailPoetTest {
function testItHashesAndReplacesLinks() { function testItHashesAndReplacesLinks() {
$template = '<a href="http://example.com">some site</a> [link:some_link_shortcode]'; $template = '<a href="http://example.com">some site</a> [link:some_link_shortcode]';
list($updated_content, $hashed_links) = Links::process($template); list($updated_content, $hashed_links) = Links::process($template, 0, 0);
// 2 links were hashed // 2 links were hashed
expect(count($hashed_links))->equals(2); expect(count($hashed_links))->equals(2);
@ -63,7 +82,7 @@ class LinksTest extends \MailPoetTest {
function testItHashesAndReplacesLinksWithSpecialCharacters() { function testItHashesAndReplacesLinksWithSpecialCharacters() {
$template = '<a href="http://сайт.cóm/彌撒時間">some site</a>'; $template = '<a href="http://сайт.cóm/彌撒時間">some site</a>';
$result = Links::process($template); $result = Links::process($template, 0, 0);
expect($result[0])->equals( expect($result[0])->equals(
sprintf( sprintf(
'<a href="%s-%s">some site</a>', '<a href="%s-%s">some site</a>',
@ -171,6 +190,28 @@ class LinksTest extends \MailPoetTest {
expect($newsltter_link->url)->equals('http://example.com'); expect($newsltter_link->url)->equals('http://example.com');
} }
function testItCanLoadLinks() {
$link = NewsletterLink::create();
$link->newsletter_id = 1;
$link->queue_id = 2;
$link->hash = 123;
$link->url = 'http://example.com';
$link->save();
$link = NewsletterLink::create();
$link->newsletter_id = 1;
$link->queue_id = 3;
$link->hash = 456;
$link->url = 'http://demo.com';
$link->save();
$links = Links::load(1, 2);
expect(is_array($links))->true();
expect(count($links))->equals(1);
expect($links['http://example.com']['hash'])->equals(123);
expect($links['http://example.com']['url'])->equals('http://example.com');
}
function testItMatchesHashedLinks() { function testItMatchesHashedLinks() {
$regex = Links::getLinkRegex(); $regex = Links::getLinkRegex();
expect((boolean)preg_match($regex, '[some_tag]-123'))->false(); expect((boolean)preg_match($regex, '[some_tag]-123'))->false();

View File

@ -350,6 +350,7 @@
'unsavedChangesWillBeLost': __('There are unsaved changes which will be lost if you leave this page.'), 'unsavedChangesWillBeLost': __('There are unsaved changes which will be lost if you leave this page.'),
'selectColor': _x('Select', 'select color'), 'selectColor': _x('Select', 'select color'),
'cancelColorSelection': _x('Cancel', 'cancel color selection'), 'cancelColorSelection': _x('Cancel', 'cancel color selection'),
'newsletterIsPaused': __('Email sending has been paused.'),
}) %> }) %>
<% endblock %> <% endblock %>
@ -1223,6 +1224,26 @@
newsletter: response.data, newsletter: response.data,
config: config, config: config,
}); });
var queue = response.data.queue;
if (response.data.status == 'sending' && queue && queue.status === null) {
MailPoet.Ajax.post({
api_version: window.mailpoet_api_version,
endpoint: 'sending_queue',
action: 'pause',
data: {
newsletter_id: response.data.id
}
}).done(response =>
MailPoet.Notice.success(MailPoet.I18n.t('newsletterIsPaused'))
).fail(function(response) {
if (response.errors.length > 0) {
MailPoet.Notice.error(
response.errors.map(function(error) { return error.message; }),
{ scroll: true, static: true }
);
}
});
}
}).fail(function(response) { }).fail(function(response) {
if (response.errors.length > 0) { if (response.errors.length > 0) {
MailPoet.Notice.error( MailPoet.Notice.error(

View File

@ -231,6 +231,7 @@
'previous': __('Previous'), 'previous': __('Previous'),
'newsletterBeingSent': __('The newsletter is being sent...'), 'newsletterBeingSent': __('The newsletter is being sent...'),
'newsletterHasBeenScheduled': __('The newsletter has been scheduled.'), 'newsletterHasBeenScheduled': __('The newsletter has been scheduled.'),
'newsletterSendingHasBeenResumed': __('The newsletter sending has been resumed.'),
'welcomeEmailActivated': __('Your Welcome Email is now activated!'), 'welcomeEmailActivated': __('Your Welcome Email is now activated!'),
'welcomeEmailActivationFailed': __('Your Welcome Email could not be activated, please check the settings.'), 'welcomeEmailActivationFailed': __('Your Welcome Email could not be activated, please check the settings.'),
'postNotificationActivated': __('Your post notification is now active!'), 'postNotificationActivated': __('Your post notification is now active!'),
@ -256,7 +257,8 @@
'mailerConnectionErrorNotice': __('Sending is paused because the following connection issue prevents MailPoet from delivering emails: %$1s'), 'mailerConnectionErrorNotice': __('Sending is paused because the following connection issue prevents MailPoet from delivering emails: %$1s'),
'mailerCheckSettingsNotice': __('Check your [link]sending method settings[/link].'), 'mailerCheckSettingsNotice': __('Check your [link]sending method settings[/link].'),
'mailerResumeSendingButton': __('Resume sending'), 'mailerResumeSendingButton': __('Resume sending'),
'mailerSendingResumedNotice': __('Sending has been resumed.') 'mailerSendingResumedNotice': __('Sending has been resumed.'),
'confirmEdit': __('Sending is in progress. Do you want to pause sending and edit the newsletter?')
}) %> }) %>
<% endblock %> <% endblock %>