Add standard newsletter scheduling UI
This commit is contained in:
@ -1,6 +1,7 @@
|
|||||||
@import 'nib'
|
@import 'nib'
|
||||||
|
|
||||||
@require 'select2/dist/css/select2.css'
|
@require 'select2/dist/css/select2.css'
|
||||||
|
@require 'datepicker'
|
||||||
|
|
||||||
@require 'common'
|
@require 'common'
|
||||||
@require 'modal'
|
@require 'modal'
|
||||||
@ -18,4 +19,4 @@
|
|||||||
@require 'settings'
|
@require 'settings'
|
||||||
@require 'progress_bar'
|
@require 'progress_bar'
|
||||||
|
|
||||||
@require 'subscribers'
|
@require 'subscribers'
|
||||||
|
97
assets/css/src/datepicker.styl
Normal file
97
assets/css/src/datepicker.styl
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
/*
|
||||||
|
Taken from: https://github.com/rtsinani/jquery-datepicker-skins
|
||||||
|
Skin: Melon
|
||||||
|
*/
|
||||||
|
|
||||||
|
.ui-widget
|
||||||
|
font-family: "Lucida Grande", "Lucida Sans Unicode", Helvetica, Arial, Verdana, sans-serif;
|
||||||
|
background: #2e3641;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
-webkit-border-radius: 0;
|
||||||
|
-moz-border-radius: 0;
|
||||||
|
|
||||||
|
.ui-datepicker
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
.ui-datepicker-header
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 15px;
|
||||||
|
|
||||||
|
.ui-datepicker-header .ui-state-hover
|
||||||
|
background: transparent;
|
||||||
|
border-color: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 0;
|
||||||
|
-webkit-border-radius: 0;
|
||||||
|
-moz-border-radius: 0;
|
||||||
|
|
||||||
|
.ui-datepicker .ui-datepicker-title
|
||||||
|
margin-top: .4em;
|
||||||
|
margin-bottom: .3em;
|
||||||
|
color: #e9f0f4;
|
||||||
|
|
||||||
|
.ui-datepicker .ui-datepicker-prev-hover
|
||||||
|
.ui-datepicker .ui-datepicker-next-hover
|
||||||
|
.ui-datepicker .ui-datepicker-next
|
||||||
|
.ui-datepicker .ui-datepicker-prev
|
||||||
|
top: .9em;
|
||||||
|
border:none;
|
||||||
|
|
||||||
|
.ui-datepicker .ui-datepicker-prev-hover
|
||||||
|
left: 2px;
|
||||||
|
|
||||||
|
.ui-datepicker .ui-datepicker-next-hover
|
||||||
|
right: 2px;
|
||||||
|
|
||||||
|
.ui-datepicker .ui-datepicker-next span
|
||||||
|
.ui-datepicker .ui-datepicker-prev span
|
||||||
|
background-image: url(../img/datepicker/ui-icons_ffffff_256x240.png);
|
||||||
|
background-position: -32px 0;
|
||||||
|
margin-top: 0;
|
||||||
|
top: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
|
||||||
|
|
||||||
|
.ui-datepicker .ui-datepicker-prev span
|
||||||
|
background-position: -96px 0;
|
||||||
|
|
||||||
|
|
||||||
|
.ui-datepicker table
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
.ui-datepicker th
|
||||||
|
padding: 1em 0;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: normal;
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid #3a414d;
|
||||||
|
|
||||||
|
.ui-datepicker td
|
||||||
|
background: #f97e76;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
|
||||||
|
td .ui-state-default
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
text-align: center;
|
||||||
|
padding: .5em;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #efefef;
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
.ui-state-disabled
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
.ui-state-disabled .ui-state-default
|
||||||
|
color: #fba49e;
|
||||||
|
|
||||||
|
td .ui-state-active
|
||||||
|
td .ui-state-hover
|
||||||
|
background: #2e3641;
|
BIN
assets/img/datepicker/ui-icons_ffffff_256x240.png
Normal file
BIN
assets/img/datepicker/ui-icons_ffffff_256x240.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.2 KiB |
@ -35,9 +35,9 @@ define(
|
|||||||
},
|
},
|
||||||
getFieldsByNewsletter: function(newsletter) {
|
getFieldsByNewsletter: function(newsletter) {
|
||||||
switch(newsletter.type) {
|
switch(newsletter.type) {
|
||||||
case 'notification': return NotificationNewsletterFields;
|
case 'notification': return NotificationNewsletterFields(newsletter);
|
||||||
case 'welcome': return WelcomeNewsletterFields;
|
case 'welcome': return WelcomeNewsletterFields(newsletter);
|
||||||
default: return StandardNewsletterFields;
|
default: return StandardNewsletterFields(newsletter);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isAutomatedNewsletter: function() {
|
isAutomatedNewsletter: function() {
|
||||||
|
@ -1,12 +1,180 @@
|
|||||||
define(
|
define(
|
||||||
[
|
[
|
||||||
'mailpoet'
|
'react',
|
||||||
|
'jquery',
|
||||||
|
'mailpoet',
|
||||||
|
'form/fields/checkbox.jsx',
|
||||||
|
'form/fields/select.jsx',
|
||||||
|
'form/fields/text.jsx',
|
||||||
],
|
],
|
||||||
function(
|
function(
|
||||||
MailPoet
|
React,
|
||||||
|
jQuery,
|
||||||
|
MailPoet,
|
||||||
|
Checkbox,
|
||||||
|
Select,
|
||||||
|
Text
|
||||||
) {
|
) {
|
||||||
|
|
||||||
var settings = window.mailpoet_settings || {};
|
var settings = window.mailpoet_settings || {},
|
||||||
|
currentTime = window.mailpoet_current_time || '00:00',
|
||||||
|
timeOfDayItems = window.mailpoet_schedule_time_of_day;
|
||||||
|
|
||||||
|
var isScheduledField = {
|
||||||
|
name: 'isScheduled',
|
||||||
|
};
|
||||||
|
|
||||||
|
var DateText = React.createClass({
|
||||||
|
componentDidMount: function() {
|
||||||
|
var $element = jQuery(this.refs.dateInput);
|
||||||
|
if ($element.datepicker) {
|
||||||
|
$element.datepicker({
|
||||||
|
dateFormat: "yy-mm-dd",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name={this.props.name || 'date'}
|
||||||
|
value={this.props.value}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
ref="dateInput" />
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
var TimeSelect = React.createClass({
|
||||||
|
render: function() {
|
||||||
|
const options = Object.keys(timeOfDayItems).map(
|
||||||
|
(value, index) => {
|
||||||
|
return (
|
||||||
|
<option
|
||||||
|
key={ 'option-' + index }
|
||||||
|
value={ value }>
|
||||||
|
{ timeOfDayItems[value] }
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
name={this.props.name || 'time'}
|
||||||
|
value={this.props.value}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
>
|
||||||
|
{options}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var DateTime = React.createClass({
|
||||||
|
_DATE_TIME_SEPARATOR: " ",
|
||||||
|
getInitialState: function() {
|
||||||
|
return this._buildStateFromProps(this.props);
|
||||||
|
},
|
||||||
|
componentWillReceiveProps: function(nextProps) {
|
||||||
|
this.setState(this._buildStateFromProps(nextProps));
|
||||||
|
},
|
||||||
|
_buildStateFromProps: function(props) {
|
||||||
|
const [date, time] = props.value.split(this._DATE_TIME_SEPARATOR)
|
||||||
|
return {
|
||||||
|
date: date,
|
||||||
|
time: time,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
handleChange: function(event) {
|
||||||
|
var newState = {};
|
||||||
|
newState[event.target.name] = event.target.value;
|
||||||
|
|
||||||
|
this.setState(newState, function() {
|
||||||
|
this.propagateChange();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
propagateChange: function() {
|
||||||
|
if (this.props.onChange) {
|
||||||
|
this.props.onChange({
|
||||||
|
target: {
|
||||||
|
name: this.props.name || '',
|
||||||
|
value: this.getDateTime(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getDateTime: function() {
|
||||||
|
return [this.state.date, this.state.time].join(this._DATE_TIME_SEPARATOR);
|
||||||
|
},
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<DateText
|
||||||
|
name="date"
|
||||||
|
value={this.state.date}
|
||||||
|
onChange={this.handleChange} />
|
||||||
|
<TimeSelect
|
||||||
|
name="time"
|
||||||
|
value={this.state.time}
|
||||||
|
onChange={this.handleChange} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var StandardScheduling = React.createClass({
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
isScheduled: '0',
|
||||||
|
scheduledAt: '2016-05-04 14:00:00',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
handleChange: function(event) {
|
||||||
|
var newState = {};
|
||||||
|
newState[event.target.name] = event.target.value;
|
||||||
|
this.setState(newState);
|
||||||
|
},
|
||||||
|
handleCheckboxChange: function(event) {
|
||||||
|
event.target.value = this.refs.isScheduled.checked ? '1' : '0';
|
||||||
|
this.handleChange(event);
|
||||||
|
},
|
||||||
|
isScheduled: function() {
|
||||||
|
return this.state.isScheduled === '1';
|
||||||
|
},
|
||||||
|
render: function() {
|
||||||
|
var isChecked = this.isScheduled(),
|
||||||
|
schedulingOptions;
|
||||||
|
|
||||||
|
if (isChecked) {
|
||||||
|
schedulingOptions = (
|
||||||
|
<span>
|
||||||
|
<DateTime
|
||||||
|
name="scheduledAt"
|
||||||
|
value={this.state.scheduledAt}
|
||||||
|
onChange={this.handleChange} />
|
||||||
|
<span>
|
||||||
|
{MailPoet.I18n.t('localTimeIs')} <code>{currentTime}</code>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
ref="isScheduled"
|
||||||
|
type="checkbox"
|
||||||
|
value="1"
|
||||||
|
checked={this.isScheduled()}
|
||||||
|
name="isScheduled"
|
||||||
|
onChange={this.handleCheckboxChange} />
|
||||||
|
|
||||||
|
{schedulingOptions}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
var fields = [
|
var fields = [
|
||||||
{
|
{
|
||||||
@ -84,9 +252,17 @@ define(
|
|||||||
placeholder: MailPoet.I18n.t('replyToAddressPlaceholder')
|
placeholder: MailPoet.I18n.t('replyToAddressPlaceholder')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'options',
|
||||||
|
label: MailPoet.I18n.t('scheduleIt'),
|
||||||
|
type: 'reactComponent',
|
||||||
|
component: StandardScheduling,
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
return fields;
|
return function(newsletter) {
|
||||||
|
return fields;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -71,7 +71,9 @@ define(
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
return fields;
|
return function(newsletter) {
|
||||||
|
return fields;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ use MailPoet\Subscribers\ImportExport\BootStrapMenu;
|
|||||||
use MailPoet\Util\DKIM;
|
use MailPoet\Util\DKIM;
|
||||||
use MailPoet\Util\Permissions;
|
use MailPoet\Util\Permissions;
|
||||||
use MailPoet\Listing;
|
use MailPoet\Listing;
|
||||||
|
use MailPoet\WP\DateTime;
|
||||||
|
|
||||||
if(!defined('ABSPATH')) exit;
|
if(!defined('ABSPATH')) exit;
|
||||||
|
|
||||||
@ -402,6 +403,18 @@ class Menu {
|
|||||||
$data['roles'] = $wp_roles->get_names();
|
$data['roles'] = $wp_roles->get_names();
|
||||||
$data['roles']['mailpoet_all'] = __('In any WordPress role');
|
$data['roles']['mailpoet_all'] = __('In any WordPress role');
|
||||||
|
|
||||||
|
$date_time = new DateTime();
|
||||||
|
$data['current_date'] = $date_time->getCurrentDate(DateTime::INTERNAL_DATE_FORMAT);
|
||||||
|
$data['current_time'] = $date_time->getCurrentTime();
|
||||||
|
$data['schedule_time_of_day'] = $date_time->getTimeInterval(
|
||||||
|
'00:00:00',
|
||||||
|
'+1 hour',
|
||||||
|
24
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_enqueue_script('jquery-ui');
|
||||||
|
wp_enqueue_script('jquery-ui-datepicker');
|
||||||
|
|
||||||
echo $this->renderer->render('newsletters.html', $data);
|
echo $this->renderer->render('newsletters.html', $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,6 +140,14 @@ class Populator {
|
|||||||
|
|
||||||
function newsletter_option_fields() {
|
function newsletter_option_fields() {
|
||||||
return array(
|
return array(
|
||||||
|
array(
|
||||||
|
'name' => 'isScheduled',
|
||||||
|
'newsletter_type' => 'standard',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'name' => 'scheduledAt',
|
||||||
|
'newsletter_type' => 'standard',
|
||||||
|
),
|
||||||
array(
|
array(
|
||||||
'name' => 'event',
|
'name' => 'event',
|
||||||
'newsletter_type' => 'welcome',
|
'newsletter_type' => 'welcome',
|
||||||
|
65
lib/WP/DateTime.php
Normal file
65
lib/WP/DateTime.php
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
namespace MailPoet\WP;
|
||||||
|
|
||||||
|
class DateTime {
|
||||||
|
|
||||||
|
const INTERNAL_DATE_FORMAT = 'Y-m-d';
|
||||||
|
const INTERNAL_TIME_FORMAT = 'H:i:s';
|
||||||
|
const INTERNAL_DATE_TIME_FORMAT = 'Y-m-d H:i:s';
|
||||||
|
|
||||||
|
function __construct() {
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTimeFormat() {
|
||||||
|
$time_format = get_option('time_format');
|
||||||
|
if (empty($time_format)) $time_format = self::INTERNAL_TIME_FORMAT;
|
||||||
|
return $time_format;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDateFormat() {
|
||||||
|
$date_format = get_option('date_format');
|
||||||
|
if (empty($date_format)) $date_format = self::INTERNAL_DATE_FORMAT;
|
||||||
|
return $date_format;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentTime($format=false) {
|
||||||
|
if (empty($format)) $format = $this->getTimeFormat();
|
||||||
|
return current_time($format);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentDate($format=false) {
|
||||||
|
if (empty($format)) $format = $this->getDateFormat();
|
||||||
|
return $this->getCurrentTime($format);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTime($time, $format=false) {
|
||||||
|
if (empty($format)) $format = $this->getTimeFormat();
|
||||||
|
|
||||||
|
return date($format, $time);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a list of time strings within an interval,
|
||||||
|
* formatted and mapped from INTERNAL_TIME_FORMAT to Wordpress time strings.
|
||||||
|
*/
|
||||||
|
function getTimeInterval(
|
||||||
|
$start_time='00:00:00',
|
||||||
|
$time_step='+1 hour',
|
||||||
|
$total_steps=24
|
||||||
|
) {
|
||||||
|
$steps = array();
|
||||||
|
|
||||||
|
$internal_time = $start_time;
|
||||||
|
$timestamp = strtotime($internal_time);
|
||||||
|
|
||||||
|
for ($step = 0; $step < $total_steps; $step += 1) {
|
||||||
|
$wordpress_time = $this->getTime($timestamp);
|
||||||
|
$steps[$internal_time] = $wordpress_time;
|
||||||
|
|
||||||
|
$timestamp = strtotime($time_step, $timestamp);
|
||||||
|
$internal_time = $this->getTime($timestamp, self::INTERNAL_TIME_FORMAT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $steps;
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,9 @@
|
|||||||
var mailpoet_settings = <%= json_encode(settings) %>;
|
var mailpoet_settings = <%= json_encode(settings) %>;
|
||||||
var mailpoet_lists = <%= json_encode(lists) %>;
|
var mailpoet_lists = <%= json_encode(lists) %>;
|
||||||
var mailpoet_roles = <%= json_encode(roles) %>;
|
var mailpoet_roles = <%= json_encode(roles) %>;
|
||||||
|
var mailpoet_current_date = <%= json_encode(current_date) %>;
|
||||||
|
var mailpoet_current_time = <%= json_encode(current_time) %>;
|
||||||
|
var mailpoet_schedule_time_of_day = <%= json_encode(schedule_time_of_day) %>;
|
||||||
</script>
|
</script>
|
||||||
<% endblock %>
|
<% endblock %>
|
||||||
|
|
||||||
@ -62,6 +65,7 @@
|
|||||||
'edit': __('Edit'),
|
'edit': __('Edit'),
|
||||||
'notSentYet': __('Not sent yet.'),
|
'notSentYet': __('Not sent yet.'),
|
||||||
'scheduledFor': __('Scheduled for'),
|
'scheduledFor': __('Scheduled for'),
|
||||||
|
'scheduleIt': __('Schedule it'),
|
||||||
'newsletterQueueCompleted': __('Sent to %$1d of %$2d.'),
|
'newsletterQueueCompleted': __('Sent to %$1d of %$2d.'),
|
||||||
'resume': __('Resume'),
|
'resume': __('Resume'),
|
||||||
'pause': __('Pause'),
|
'pause': __('Pause'),
|
||||||
@ -145,5 +149,6 @@
|
|||||||
'saveDraftAndClose': __('Save as draft and close'),
|
'saveDraftAndClose': __('Save as draft and close'),
|
||||||
'orSimply': __('or simply'),
|
'orSimply': __('or simply'),
|
||||||
'goBackToDesign': __('go back to design'),
|
'goBackToDesign': __('go back to design'),
|
||||||
|
'localTimeIs': __('Local time is'),
|
||||||
}) %>
|
}) %>
|
||||||
<% endblock %>
|
<% endblock %>
|
||||||
|
Reference in New Issue
Block a user