Compare commits

...

23 Commits
0.0.1 ... 0.0.2

Author SHA1 Message Date
d85f51e9fc Update composer lockfile. 2015-10-30 22:26:40 +01:00
40a62687cf Update Composer lockfile. 2015-10-30 22:13:00 +01:00
136e09e9fb Merge pull request #202 from mailpoet/newsletter_width
Increase newsletter width to 660px from 600px
2015-10-30 12:37:16 +01:00
f509dc0d7e Increase newsletter width to 660px from 600px 2015-10-30 13:25:45 +02:00
c100130f39 Merge pull request #201 from mailpoet/forms
Listing/Model/Router refactoring + Forms
2015-10-30 11:45:34 +01:00
9922ecd93c Merge pull request #200 from mailpoet/notification_type
Add notification email type
2015-10-30 11:40:50 +01:00
a4cf2f9c76 Major refactoring of listing/router/model relation
- updated Subscribers listing
- udpated Segments listing
- added Forms router
2015-10-29 15:30:24 +01:00
576fbf2085 Add notification email type 2015-10-29 15:59:09 +02:00
5c63971314 Merge pull request #198 from mailpoet/editor_select2
Update select2 version to 4.0 for newsletter editor
2015-10-28 15:30:08 +01:00
7418923bbc Remove glow and margins from select2 input 2015-10-28 16:01:23 +02:00
a8f8134f67 Adapt select2 integration code to select2 4.0 2015-10-28 16:01:23 +02:00
103da61d45 basic listing files 2015-10-28 13:19:48 +01:00
01e6a5e6b2 forms table 2015-10-28 13:19:48 +01:00
f5ccf3b38a Merge pull request #195 from mailpoet/subscribers_round_1
Subscriber & Segment listings
2015-10-28 12:24:19 +01:00
c8929351ba Merge pull request #196 from mailpoet/openssl
use of Crypt\RSA library in order to generate dkim keys
2015-10-28 12:23:33 +01:00
6ca536e9ca use of Crypt\RSA library in order to generate dkim keys 2015-10-27 16:41:28 +01:00
89b04e8691 Subscriber & Segment listings
- fixed filters
- added load/save state from url
- added goBack on forms in order to get back listing states
wx# Please enter the commit message for your changes. Lines starting
2015-10-27 16:24:00 +01:00
588b441fb1 Merge pull request #194 from mailpoet/lists_round_1
Lists round 1
2015-10-27 10:30:45 +01:00
13dc3577f1 lotta fixes for filtering + listing 2015-10-26 18:23:32 +01:00
505b979ac5 Segment actions
- added duplicate
- added view subscribers
2015-10-23 17:34:35 +02:00
3b4c5c83e1 added segment stats in listing 2015-10-22 20:40:46 +02:00
056e79eeac bugfix 2015-10-22 19:24:58 +02:00
4bde705f04 listing modifications + added description to segments 2015-10-22 19:19:40 +02:00
52 changed files with 2438 additions and 1036 deletions

View File

@ -19,8 +19,10 @@ a:focus
// select 2
.select2-container
// textareas
textarea.regular-text
width: 25em !important
@media screen and (max-width: 782px)
.select2-container
width: 100% !important
width: 100% !important

View File

@ -1,4 +1,4 @@
.mailpoet_listing_loading tbody tr,
.mailpoet_listing_loading tbody tr
.mailpoet_form_loading tbody tr
opacity: 0.2
@ -8,6 +8,20 @@
.mailpoet_select_all td
text-align: center
table.widefat thead .check-column,
table.widefat tfoot .check-column
padding: 10px 0 0 3px
.mailpoet_listing_table
th span
white-space: nowrap
thead .check-column
tfoot .check-column
padding: 10px 0 0 3px
thead th.column-primary
tfoot th.column-primary
width: 25em
// responsive
@media screen and (max-width: 782px)
thead th.column-primary
tfoot th.column-primary
width: 100%

View File

@ -1,3 +1,8 @@
$column-margin = 20px
$one-column-width = $newsletter-width - (2 * $column-margin)
$two-column-width = ($newsletter-width / 2) - (2 * $column-margin)
$three-column-width = ($newsletter-width / 3) - (2 * $column-margin)
.mailpoet_container
width: 100%
min-height: 15px
@ -44,7 +49,7 @@
.mailpoet_container_horizontal > .mailpoet_container_block
margin-bottom: 0
width: 20px + 560px + 20px
width: $column-margin + $one-column-width + $column-margin
// More than one column
& > .mailpoet_container_block > .mailpoet_container > .mailpoet_container_block > .mailpoet_container_horizontal
@ -57,14 +62,14 @@
& > .mailpoet_block:first-child:nth-last-child(2) ~ .mailpoet_block
//padding-left: 20px
//padding-right: 20px
width: 260px + 20px + 20px
width: $column-margin + $two-column-width + $column-margin
// Three columns
& > .mailpoet_block:first-child:nth-last-child(3)
& > .mailpoet_block:first-child:nth-last-child(3) ~ .mailpoet_block
//padding-left: 20px
//padding-right: 20px
width: 160px + 20px + 20px
width: $column-margin + $three-column-width + $column-margin
.mailpoet_container_empty
text-align: center

View File

@ -1,5 +1,5 @@
/* Fix select2 z-index to work with MailPoet.Modal */
.select2-drop
.select2-dropdown
z-index: 101000
/* Remove input field styles from select2 type input */
@ -7,6 +7,19 @@
border: none
padding: 0
/* Fix select2 input glow and margins that wordpress may insert */
.select2 input,
.select2 input:focus
border-color: none
box-shadow: none
margin: 0
padding: 0
/* Fix width overrides for select2 */
.mailpoet_editor_settings .select2-container
width: 100% !important
/* Fix inline TinyMCE toolbar to have minimal width instead of being close to 100% of the screen */
div.mce-toolbar-grp.mce-container
position: absolute

View File

@ -23,4 +23,4 @@ $warning-alternate-text-color = #f4c6c8
$error-text-color = #d54e21
// Dimensions
$newsletter-width = 600px
$newsletter-width = 660px

View File

@ -70,15 +70,31 @@ define(
this.setState({ loading: true });
// only get values from displayed fields
item = {};
this.props.fields.map(function(field) {
item[field.name] = this.state.item[field.name];
}.bind(this));
// set id if specified
if(this.props.params.id !== undefined) {
item.id = this.props.params.id;
}
MailPoet.Ajax.post({
endpoint: this.props.endpoint,
action: 'save',
data: this.state.item
data: item
}).done(function(response) {
this.setState({ loading: false });
if(response === true) {
this.history.pushState(null, '/');
if(this.props.onSuccess !== undefined) {
this.props.onSuccess()
} else {
this.history.pushState(null, '/')
}
if(this.props.params.id !== undefined) {
this.props.messages['updated']();
} else {

View File

@ -0,0 +1,56 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { Router, History } from 'react-router'
import MailPoet from 'mailpoet'
import Form from 'form/form.jsx'
const fields = [
{
name: 'name',
label: 'Name',
type: 'text'
},
{
name: 'segments',
label: 'Lists',
type: 'selection',
endpoint: 'segments'
}
]
const messages = {
updated: function() {
MailPoet.Notice.success('Form successfully updated!');
},
created: function() {
MailPoet.Notice.success('Form successfully added!');
}
}
const FormForm = React.createClass({
mixins: [
History
],
render() {
return (
<div>
<h2 className="title">
Form <a
href="javascript:;"
className="add-new-h2"
onClick={ this.history.goBack }
>Back to list</a>
</h2>
<Form
endpoint="forms"
fields={ fields }
params={ this.props.params }
messages={ messages }
onSuccess={ this.history.goBack } />
</div>
);
}
});
module.exports = FormForm

View File

@ -0,0 +1,29 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { Router, Route, IndexRoute } from 'react-router'
import FormList from 'forms/list.jsx'
import FormForm from 'forms/form.jsx'
import createHashHistory from 'history/lib/createHashHistory'
let history = createHashHistory({ queryKey: false })
const App = React.createClass({
render() {
return this.props.children
}
});
let container = document.getElementById('forms_container');
if(container) {
ReactDOM.render((
<Router history={ history }>
<Route path="/" component={ App }>
<IndexRoute component={ FormList } />
<Route path="new" component={ FormForm } />
<Route path="edit/:id" component={ FormForm } />
<Route path="*" component={ FormList } />
</Route>
</Router>
), container);
}

View File

@ -0,0 +1,178 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { Router, Link, History } from 'react-router'
import Listing from 'listing/listing.jsx'
import classNames from 'classnames'
import MailPoet from 'mailpoet'
const columns = [
{
name: 'name',
label: 'Name',
sortable: true
},
{
name: 'created_at',
label: 'Created on',
sortable: true
}
];
const messages = {
onTrash: function(response) {
let count = ~~response.forms;
let message = null;
if(count === 1 || response === true) {
message = (
'1 form was moved to the trash.'
);
} else if(count > 1) {
message = (
'%$1d forms were moved to the trash.'
).replace('%$1d', count);
}
if(message !== null) {
MailPoet.Notice.success(message);
}
},
onDelete: function(response) {
let count = ~~response.forms;
let message = null;
if(count === 1 || response === true) {
message = (
'1 form was permanently deleted.'
);
} else if(count > 1) {
message = (
'%$1d forms were permanently deleted.'
).replace('%$1d', count);
}
if(message !== null) {
MailPoet.Notice.success(message);
}
},
onRestore: function(response) {
let count = ~~response.forms;
let message = null;
if(count === 1 || response === true) {
message = (
'1 form has been restored from the trash.'
);
} else if(count > 1) {
message = (
'%$1d forms have been restored from the trash.'
).replace('%$1d', count);
}
if(message !== null) {
MailPoet.Notice.success(message);
}
}
};
const item_actions = [
{
name: 'edit',
link: function(item) {
return (
<Link to={ `/edit/${item.id}` }>Edit</Link>
);
}
},
{
name: 'duplicate_form',
refresh: true,
link: function(item) {
return (
<a
href="javascript:;"
onClick={ this.onDuplicate.bind(null, item) }
>Duplicate</a>
);
},
onDuplicate: function(item) {
MailPoet.Ajax.post({
endpoint: 'forms',
action: 'duplicate',
data: item.id
}).done(function() {
MailPoet.Notice.success(
('List "%$1s" has been duplicated.').replace('%$1s', item.name)
);
});
}
}
];
const bulk_actions = [
{
name: 'trash',
label: 'Trash',
getData: function() {
return {
confirm: false
}
},
onSuccess: messages.onDelete
}
];
const FormList = React.createClass({
renderItem: function(form, actions) {
let row_classes = classNames(
'manage-column',
'column-primary',
'has-row-actions'
);
let segments = mailpoet_segments.filter(function(segment) {
return (jQuery.inArray(segment.id, form.segments) !== -1);
}).map(function(segment) {
return segment.name;
}).join(', ');
return (
<div>
<td className={ row_classes }>
<strong>
<a>{ form.name }</a>
</strong>
{ actions }
</td>
<td className="column-format" data-colname="Lists">
{ segments }
</td>
<td className="column-date" data-colname="Created on">
<abbr>{ form.created_at }</abbr>
</td>
</div>
);
},
render() {
return (
<div>
<h2 className="title">
Forms <Link className="add-new-h2" to="/new">New</Link>
</h2>
<Listing
messages={ messages }
search={ false }
limit={ 1000 }
endpoint="forms"
onRenderItem={ this.renderItem }
columns={ columns }
bulk_actions={ bulk_actions }
item_actions={ item_actions }
/>
</div>
);
}
});
module.exports = FormList;

View File

@ -45,12 +45,13 @@ function(
data.action = this.state.action;
var callback = function() {};
if(action['onSuccess'] !== undefined) {
data.onSuccess = action.onSuccess;
callback = action.onSuccess;
}
if(data.action) {
this.props.onBulkAction(selected_ids, data);
this.props.onBulkAction(selected_ids, data, callback);
}
this.setState({

View File

@ -1,57 +1,67 @@
define([
'react'
'react',
'jquery'
],
function(
React
React,
jQuery
) {
var ListingFilters = React.createClass({
handleFilterAction: function() {
var filters = this.props.filters.map(function(filter, index) {
var value = this.refs['filter-'+index].value;
if(value) {
return {
'name': filter.name,
'value': value
};
}
}.bind(this));
let filters = {}
this.getAvailableFilters().map((filter, i) => {
filters[this.refs['filter-'+i].name] = this.refs['filter-'+i].value
})
return this.props.onSelectFilter(filters);
},
handleChangeAction: function() {
return true;
getAvailableFilters: function() {
let filters = this.props.filters;
return Object.keys(filters).filter(function(filter) {
return !(
filters[filter].length === 0
|| (
filters[filter].length === 1
&& !filters[filter][0].value
)
);
})
},
render: function() {
var filters = this.props.filters
.filter(function(filter) {
return !(
filter.options.length === 0
|| (
filter.options.length === 1
&& !filter.options[0].value
)
);
})
const filters = this.props.filters;
const selected_filters = this.props.filter;
const available_filters = this.getAvailableFilters()
.map(function(filter, i) {
let default_value = false;
if(selected_filters[filter] !== undefined && selected_filters[filter]) {
default_value = selected_filters[filter]
} else {
jQuery(`select[name="${filter}"]`).val('');
}
return (
<select
ref={ 'filter-'+i }
key={ 'filter-'+i }
onChange={ this.handleChangeAction }>
{ filter.options.map(function(option, j) {
return (
<option
value={ option.value }
key={ 'filter-option-' + j }
>{ option.label }</option>
);
}.bind(this)) }
ref={ `filter-${i}` }
key={ `filter-${i}` }
name={ filter }
defaultValue={ default_value }
>
{ filters[filter].map(function(option, j) {
return (
<option
value={ option.value }
key={ 'filter-option-' + j }
>{ option.label }</option>
);
}.bind(this)) }
</select>
);
}.bind(this));
var button = false;
let button = false;
if(filters.length > 0) {
if(available_filters.length > 0) {
button = (
<input
onClick={ this.handleFilterAction }
@ -63,7 +73,7 @@ function(
return (
<div className="alignleft actions actions">
{ filters }
{ available_filters }
{ button }
</div>
);

View File

@ -9,6 +9,9 @@ define(['react', 'classnames'], function(React, classNames) {
render: function() {
var columns = this.props.columns.map(function(column, index) {
column.is_primary = (index === 0);
column.sorted = (this.props.sort_by === column.name)
? this.props.sort_order
: 'asc';
return (
<ListingColumn
onSort={this.props.onSort}

View File

@ -46,8 +46,11 @@ define(
handleRestoreItem: function(id) {
this.props.onRestoreItem(id);
},
handleDeleteItem: function(id, confirm = false) {
this.props.onDeleteItem(id, confirm);
handleTrashItem: function(id) {
this.props.onTrashItem(id);
},
handleDeleteItem: function(id) {
this.props.onDeleteItem(id);
},
handleToggleItem: function(id) {
this.setState({ toggled: !this.state.toggled });
@ -77,12 +80,39 @@ define(
if(custom_actions.length > 0) {
item_actions = custom_actions.map(function(action, index) {
return (
<span key={ 'action-'+index } className={ action.name }>
{ action.link(this.props.item.id) }
{(index < (custom_actions.length - 1)) ? ' | ' : ''}
</span>
);
if(action.refresh) {
return (
<span
onClick={ this.props.onRefreshItems }
key={ 'action-'+index } className={ action.name }>
{ action.link(this.props.item) }
{(index < (custom_actions.length - 1)) ? ' | ' : ''}
</span>
);
} else if(action.link) {
return (
<span
key={ 'action-'+index } className={ action.name }>
{ action.link(this.props.item) }
{(index < (custom_actions.length - 1)) ? ' | ' : ''}
</span>
);
} else {
return (
<span
key={ 'action-'+index } className={ action.name }>
<a href="javascript:;" onClick={
(action.onClick !== undefined)
? action.onClick.bind(null,
this.props.item,
this.props.onRefreshItems
)
: false
}>{ action.label }</a>
{(index < (custom_actions.length - 1)) ? ' | ' : ''}
</span>
);
}
}.bind(this));
} else {
item_actions = (
@ -112,8 +142,7 @@ define(
href="javascript:;"
onClick={ this.handleDeleteItem.bind(
null,
this.props.item.id,
true
this.props.item.id
)}
>Delete permanently</a>
</span>
@ -134,10 +163,9 @@ define(
<span className="trash">
<a
href="javascript:;"
onClick={ this.handleDeleteItem.bind(
onClick={ this.handleTrashItem.bind(
null,
this.props.item.id,
false
this.props.item.id
) }>
Trash
</a>
@ -222,7 +250,7 @@ define(
</td>
</tr>
{this.props.items.map(function(item) {
{this.props.items.map(function(item, index) {
item.id = parseInt(item.id, 10);
item.selected = (this.props.selected_ids.indexOf(item.id) !== -1);
@ -233,11 +261,13 @@ define(
onRenderItem={ this.props.onRenderItem }
onDeleteItem={ this.props.onDeleteItem }
onRestoreItem={ this.props.onRestoreItem }
onTrashItem={ this.props.onTrashItem }
onRefreshItems={ this.props.onRefreshItems }
selection={ this.props.selection }
is_selectable={ this.props.is_selectable }
item_actions={ this.props.item_actions }
group={ this.props.group }
key={ 'item-' + item.id }
key={ 'item-' + index }
item={ item } />
);
}.bind(this))}
@ -248,6 +278,9 @@ define(
});
var Listing = React.createClass({
mixins: [
Router.History
],
getInitialState: function() {
return {
loading: false,
@ -260,43 +293,136 @@ define(
items: [],
groups: [],
group: 'all',
filters: [],
filter: [],
filters: {},
filter: {},
selected_ids: [],
selection: false
};
},
componentDidUpdate: function(prevProps, prevState) {
// reset group to "all" if trash gets emptied
if(
// we were viewing the trash
(prevState.group === 'trash' && prevState.count > 0)
&&
// we are still viewing the trash but there are no items left
(this.state.group === 'trash' && this.state.count === 0)
&&
// only do this when no filter is set
(Object.keys(this.state.filter).length === 0)
) {
this.handleGroup('all');
}
},
getParam: function(param) {
var regex = /(.*)\[(.*)\]/
var matches = regex.exec(param)
return [matches[1], matches[2]]
},
initWithParams: function(params) {
let state = this.state || {}
let original_state = state
// check for url params
if(params.splat !== undefined) {
params.splat.split('/').map(param => {
let [key, value] = this.getParam(param);
switch(key) {
case 'filter':
let filters = {}
value.split('&').map(function(pair) {
let [k, v] = pair.split('=')
filters[k] = v
}
)
state.filter = filters
break;
default:
state[key] = value
}
})
}
if(this.props.limit !== undefined) {
state.limit = Math.abs(~~this.props.limit);
}
this.setState(state, function() {
this.getItems();
}.bind(this));
},
setParams: function() {
var params = Object.keys(this.state)
.filter(key => {
return (
[
'group',
'filter',
'search',
'page',
'sort_by',
'sort_order'
].indexOf(key) !== -1
)
})
.map(key => {
let value = this.state[key]
if(value === Object(value)) {
value = jQuery.param(value)
} else if(value === Boolean(value)) {
value = value.toString()
}
if(value !== '') {
return `${key}[${value}]`
}
})
.filter(key => { return (key !== undefined) })
.join('/');
params = '/'+params
if(this.props.location) {
if(this.props.location.pathname !== params) {
this.history.pushState(null, `${params}`)
}
}
},
componentDidMount: function() {
this.getItems();
if(this.isMounted()) {
const params = this.props.params || {}
this.initWithParams(params)
}
},
componentWillReceiveProps: function(nextProps) {
const params = nextProps.params || {}
//this.initWithParams(params)
},
getItems: function() {
this.setState({ loading: true });
if(this.isMounted()) {
this.setState({ loading: true });
this.clearSelection();
this.clearSelection();
MailPoet.Ajax.post({
endpoint: this.props.endpoint,
action: 'listing',
data: {
offset: (this.state.page - 1) * this.state.limit,
limit: this.state.limit,
group: this.state.group,
filter: this.state.filter,
search: this.state.search,
sort_by: this.state.sort_by,
sort_order: this.state.sort_order
}
}).done(function(response) {
if(this.isMounted()) {
MailPoet.Ajax.post({
endpoint: this.props.endpoint,
action: 'listing',
data: {
offset: (this.state.page - 1) * this.state.limit,
limit: this.state.limit,
group: this.state.group,
filter: this.state.filter,
search: this.state.search,
sort_by: this.state.sort_by,
sort_order: this.state.sort_order
}
}).done(function(response) {
this.setState({
items: response.items || [],
filters: response.filters || [],
filters: response.filters || {},
groups: response.groups || [],
count: response.count || 0,
loading: false
});
}
}.bind(this));
}.bind(this));
}
},
handleRestoreItem: function(id) {
this.setState({
@ -318,7 +444,27 @@ define(
this.getItems();
}.bind(this));
},
handleDeleteItem: function(id, confirm = false) {
handleTrashItem: function(id) {
this.setState({
loading: true,
page: 1
});
MailPoet.Ajax.post({
endpoint: this.props.endpoint,
action: 'trash',
data: id
}).done(function(response) {
if(
this.props.messages !== undefined
&& this.props.messages['onTrash'] !== undefined
) {
this.props.messages.onTrash(response);
}
this.getItems();
}.bind(this));
},
handleDeleteItem: function(id) {
this.setState({
loading: true,
page: 1
@ -327,31 +473,18 @@ define(
MailPoet.Ajax.post({
endpoint: this.props.endpoint,
action: 'delete',
data: {
id: id,
confirm: confirm
}
data: id
}).done(function(response) {
if(confirm === true) {
if(
this.props.messages !== undefined
&& this.props.messages['onConfirmDelete'] !== undefined
) {
this.props.messages.onConfirmDelete(response);
}
} else {
if(
this.props.messages !== undefined
&& this.props.messages['onDelete'] !== undefined
) {
this.props.messages.onDelete(response);
}
if(
this.props.messages !== undefined
&& this.props.messages['onDelete'] !== undefined
) {
this.props.messages.onDelete(response);
}
this.getItems();
}.bind(this));
},
handleBulkAction: function(selected_ids, params) {
handleBulkAction: function(selected_ids, params, callback) {
if(
this.state.selection === false
&& this.state.selected_ids.length === 0
@ -362,12 +495,6 @@ define(
this.setState({ loading: true });
var data = params || {};
var callback = ((data['onSuccess'] !== undefined)
? data['onSuccess']
: function() {}
);
delete data.onSuccess;
data.listing = {
offset: 0,
limit: 0,
@ -393,6 +520,7 @@ define(
selection: false,
selected_ids: []
}, function() {
this.setParams();
this.getItems();
}.bind(this));
},
@ -401,6 +529,7 @@ define(
sort_by: sort_by,
sort_order: sort_order,
}, function() {
this.setParams();
this.getItems();
}.bind(this));
},
@ -460,6 +589,7 @@ define(
filter: filters,
page: 1
}, function() {
this.setParams();
this.getItems();
}.bind(this));
},
@ -469,10 +599,11 @@ define(
this.setState({
group: group,
filter: [],
filter: {},
search: '',
page: 1
}, function() {
this.setParams();
this.getItems();
}.bind(this));
},
@ -482,6 +613,7 @@ define(
selection: false,
selected_ids: []
}, function() {
this.setParams();
this.getItems();
}.bind(this));
},
@ -489,17 +621,14 @@ define(
var render = this.props.onRenderItem(item, actions);
return render.props.children;
},
handleRefreshItems: function() {
this.getItems();
},
render: function() {
var items = this.state.items,
sort_by = this.state.sort_by,
sort_order = this.state.sort_order;
// set sortable columns
columns = this.props.columns.map(function(column) {
column.sorted = (column.name === sort_by) ? sort_order : false;
return column;
});
// bulk actions
var bulk_actions = this.props.bulk_actions || [];
@ -511,12 +640,9 @@ define(
onSuccess: this.props.messages.onRestore
},
{
name: 'trash',
name: 'delete',
label: 'Delete permanently',
onSuccess: this.props.messages.onConfirmDelete,
getData: function() {
return { confirm: true };
}
onSuccess: this.props.messages.onDelete
}
];
}
@ -524,22 +650,42 @@ define(
// item actions
var item_actions = this.props.item_actions || [];
var tableClasses = classNames(
var table_classes = classNames(
'mailpoet_listing_table',
'wp-list-table',
'widefat',
'fixed',
'striped',
{ 'mailpoet_listing_loading': this.state.loading }
);
// search
var search = (
<ListingSearch
onSearch={ this.handleSearch }
search={ this.state.search }
/>
);
if(this.props.search === false) {
search = false;
}
// groups
var groups = (
<ListingGroups
groups={ this.state.groups }
group={ this.state.group }
onSelectGroup={ this.handleGroup }
/>
);
if(this.props.groups === false) {
groups = false;
}
return (
<div>
<ListingGroups
groups={ this.state.groups }
group={ this.state.group }
onSelectGroup={ this.handleGroup } />
<ListingSearch
onSearch={ this.handleSearch }
search={ this.state.search } />
{ groups }
{ search }
<div className="tablenav top clearfix">
<ListingBulkActions
bulk_actions={ bulk_actions }
@ -548,7 +694,7 @@ define(
onBulkAction={ this.handleBulkAction } />
<ListingFilters
filters={ this.state.filters }
filter={ this.state.filter }
filter={ this.state.filter }
onSelectFilter={ this.handleFilter } />
<ListingPages
count={ this.state.count }
@ -556,7 +702,7 @@ define(
limit={ this.state.limit }
onSetPage={ this.handleSetPage } />
</div>
<table className={ tableClasses }>
<table className={ table_classes }>
<thead>
<ListingHeader
onSort={ this.handleSort }
@ -572,6 +718,8 @@ define(
onRenderItem={ this.handleRenderItem }
onDeleteItem={ this.handleDeleteItem }
onRestoreItem={ this.handleRestoreItem }
onTrashItem={ this.handleTrashItem }
onRefreshItems={ this.handleRefreshItems }
columns={ this.props.columns }
is_selectable={ bulk_actions.length > 0 }
onSelectItem={ this.handleSelectItem }

View File

@ -40,23 +40,23 @@ define(['react', 'classnames'], function(React, classNames) {
},
render: function() {
if(this.props.count === 0) {
return (<div></div>);
return false;
} else {
var pagination,
firstPage = (
var pagination = false;
var firstPage = (
<span aria-hidden="true" className="tablenav-pages-navspan">«</span>
),
previousPage = (
);
var previousPage = (
<span aria-hidden="true" className="tablenav-pages-navspan"></span>
),
nextPage = (
);
var nextPage = (
<span aria-hidden="true" className="tablenav-pages-navspan"></span>
),
lastPage = (
);
var lastPage = (
<span aria-hidden="true" className="tablenav-pages-navspan">»</span>
);
if(this.props.count > this.props.limit) {
if(this.props.limit > 0 && this.props.count > this.props.limit) {
if(this.props.page > 1) {
previousPage = (
<a href="javascript:;"
@ -104,6 +104,7 @@ define(['react', 'classnames'], function(React, classNames) {
pagination = (
<span className="pagination-links">
{firstPage}
&nbsp;
{previousPage}
&nbsp;
<span className="paging-input">
@ -128,6 +129,7 @@ define(['react', 'classnames'], function(React, classNames) {
</span>
&nbsp;
{nextPage}
&nbsp;
{lastPage}
</span>
);
@ -140,7 +142,7 @@ define(['react', 'classnames'], function(React, classNames) {
return (
<div className={ classes }>
<span className="displaying-num">{ this.props.count } item(s)</span>
<span className="displaying-num">{ this.props.count } items</span>
{ pagination }
</div>
);

View File

@ -7,26 +7,33 @@ define(['react'], function(React) {
this.refs.search.value
);
},
componentWillReceiveProps: function(nextProps) {
this.refs.search.value = nextProps.search
},
render: function() {
return (
<form name="search" onSubmit={this.handleSearch}>
<p className="search-box">
<label htmlFor="search_input" className="screen-reader-text">
Search
</label>
<input
type="search"
id="search_input"
ref="search"
name="s"
defaultValue={this.props.search} />
<input
type="submit"
defaultValue={MailPoetI18n.searchLabel}
className="button" />
</p>
</form>
);
if(this.props.search === false) {
return false;
} else {
return (
<form name="search" onSubmit={this.handleSearch}>
<p className="search-box">
<label htmlFor="search_input" className="screen-reader-text">
Search
</label>
<input
type="search"
id="search_input"
ref="search"
name="s"
defaultValue={this.props.search} />
<input
type="submit"
defaultValue={MailPoetI18n.searchLabel}
className="button" />
</p>
</form>
);
}
}
});

View File

@ -166,81 +166,62 @@ define([
this.$('.mailpoet_automated_latest_content_categories_and_tags').select2({
multiple: true,
allowClear: true,
query: function(options) {
var taxonomies = [];
// Delegate data loading to our own endpoints
WordpressComponent.getTaxonomies(that.model.get('contentType')).then(function(tax) {
taxonomies = tax;
// Fetch available terms based on the list of taxonomies already fetched
var promise = WordpressComponent.getTerms({
search: options.term,
taxonomies: _.keys(taxonomies)
}).then(function(terms) {
return {
taxonomies: taxonomies,
terms: terms,
};
ajax: {
data: function (params) {
return {
term: params.term
};
},
transport: function(options, success, failure) {
var taxonomies,
promise = WordpressComponent.getTaxonomies(that.model.get('contentType')).then(function(tax) {
taxonomies = tax;
// Fetch available terms based on the list of taxonomies already fetched
var promise = WordpressComponent.getTerms({
search: options.data.term,
taxonomies: _.keys(taxonomies)
}).then(function(terms) {
return {
taxonomies: taxonomies,
terms: terms,
};
});
return promise;
});
promise.then(success);
promise.fail(failure);
return promise;
}).done(function(args) {
},
processResults: function(data) {
// Transform taxonomies and terms into select2 compatible format
options.callback({
return {
results: _.map(
args.terms,
data.terms,
function(item) {
return _.defaults({
text: args.taxonomies[item.taxonomy].labels.singular_name + ': ' + item.name,
text: data.taxonomies[item.taxonomy].labels.singular_name + ': ' + item.name,
id: item.term_id
}, item);
}
)
});
});
};
},
},
initSelection: function(element, callback) {
// On external data load tell select2 which terms to preselect
callback(_.map(
that.model.get('terms').toJSON(),
function(item) {
return {
id: item.id,
text: item.text,
};
}
));
}).on({
'select2:select': function(event) {
var terms = that.model.get('terms');
terms.add(event.params.data);
// Reset whole model in order for change events to propagate properly
that.model.set('terms', terms.toJSON());
},
}).trigger( 'change' ).on({
'change': function(e){
var data = jQuery(this).data('selected');
if (typeof data === 'string') {
if (data === '') {
data = [];
} else {
data = JSON.parse(data);
}
}
if ( e.added ){
data.push(e.added);
} else {
data = _.filter(data, function(item) {
return item.id !== e.removed.id;
});
}
// Update ALC model
that.model.set('terms', data);
jQuery(this).data('selected', JSON.stringify(data));
}
});
},
onBeforeDestroy: function() {
base.BlockSettingsView.prototype.onBeforeDestroy.apply(this, arguments);
// Force close select2 if it hasn't closed yet
this.$('.mailpoet_automated_latest_content_categories_and_tags').select2('close');
'select2:unselect': function(event) {
var terms = that.model.get('terms');
terms.remove(event.params.data);
// Reset whole model in order for change events to propagate properly
that.model.set('terms', terms.toJSON());
},
}).trigger( 'change' );
},
toggleDisplayOptions: function(event) {
var el = this.$('.mailpoet_automated_latest_content_display_options'),

View File

@ -297,9 +297,9 @@ define([
// Following advice from Becs, the target width should
// be a double of one column width to render well on
// retina screen devices
targetImageWidth = 1200,
targetImageWidth = 1320,
// For main image use the size, that's closest to being 600px in width
// For main image use the size, that's closest to being 660px in width
sizeKeys = _.keys(sizes),
// Pick the width that is closest to target width

View File

@ -21,7 +21,8 @@ define([
'newsletter_editor/components/wordpress',
'newsletter_editor/blocks/base',
'newsletter_editor/blocks/button',
'newsletter_editor/blocks/divider'
'newsletter_editor/blocks/divider',
'select2'
], function(Backbone, Marionette, Radio, _, jQuery, MailPoet, App, WordpressComponent, BaseBlock, ButtonBlock, DividerBlock) {
"use strict";
@ -249,59 +250,62 @@ define([
this.$('.mailpoet_posts_categories_and_tags').select2({
multiple: true,
allowClear: true,
query: function(options) {
var taxonomies = [];
// Delegate data loading to our own endpoints
WordpressComponent.getTaxonomies(that.model.get('contentType')).then(function(tax) {
taxonomies = tax;
// Fetch available terms based on the list of taxonomies already fetched
var promise = WordpressComponent.getTerms({
search: options.term,
taxonomies: _.keys(taxonomies)
}).then(function(terms) {
return {
taxonomies: taxonomies,
terms: terms,
};
ajax: {
data: function (params) {
return {
term: params.term
};
},
transport: function(options, success, failure) {
var taxonomies,
promise = WordpressComponent.getTaxonomies(that.model.get('contentType')).then(function(tax) {
taxonomies = tax;
// Fetch available terms based on the list of taxonomies already fetched
var promise = WordpressComponent.getTerms({
search: options.data.term,
taxonomies: _.keys(taxonomies)
}).then(function(terms) {
return {
taxonomies: taxonomies,
terms: terms,
};
});
return promise;
});
promise.then(success);
promise.fail(failure);
return promise;
}).done(function(args) {
},
processResults: function(data) {
// Transform taxonomies and terms into select2 compatible format
options.callback({
return {
results: _.map(
args.terms,
data.terms,
function(item) {
return _.defaults({
text: args.taxonomies[item.taxonomy].labels.singular_name + ': ' + item.name,
text: data.taxonomies[item.taxonomy].labels.singular_name + ': ' + item.name,
id: item.term_id
}, item);
}
)
});
});
};
},
},
}).trigger( 'change' ).on({
'change': function(e){
var data = [];
if (typeof data === 'string') {
if (data === '') {
data = [];
} else {
data = JSON.parse(data);
}
}
if ( e.added ){
data.push(e.added);
}
// Update ALC model
that.model.set('terms', data);
jQuery(this).data('selected', JSON.stringify(data));
}
});
}).on({
'select2:select': function(event) {
var terms = that.model.get('terms');
terms.add(event.params.data);
// Reset whole model in order for change events to propagate properly
that.model.set('terms', terms.toJSON());
},
'select2:unselect': function(event) {
var terms = that.model.get('terms');
terms.remove(event.params.data);
// Reset whole model in order for change events to propagate properly
that.model.set('terms', terms.toJSON());
},
}).trigger( 'change' );
},
onBeforeDestroy: function() {
base.BlockSettingsView.prototype.onBeforeDestroy.apply(this, arguments);

View File

@ -37,19 +37,77 @@ define(
}
];
var messages = {
onTrash: function(response) {
var count = ~~response.newsletters;
var message = null;
if(count === 1 || response === true) {
message = (
'1 newsletter was moved to the trash.'
);
} else if(count > 1) {
message = (
'%$1d newsletters were moved to the trash.'
).replace('%$1d', count);
}
if(message !== null) {
MailPoet.Notice.success(message);
}
},
onDelete: function(response) {
var count = ~~response.newsletters;
var message = null;
if(count === 1 || response === true) {
message = (
'1 newsletter was permanently deleted.'
);
} else if(count > 1) {
message = (
'%$1d newsletters were permanently deleted.'
).replace('%$1d', count);
}
if(message !== null) {
MailPoet.Notice.success(message);
}
},
onRestore: function(response) {
var count = ~~response.newsletters;
var message = null;
if(count === 1 || response === true) {
message = (
'1 newsletter has been restored from the trash.'
);
} else if(count > 1) {
message = (
'%$1d newsletters have been restored from the trash.'
).replace('%$1d', count);
}
if(message !== null) {
MailPoet.Notice.success(message);
}
}
};
var bulk_actions = [
{
name: 'trash',
label: 'Trash'
label: 'Trash',
onSuccess: messages.onTrash
}
];
var item_actions = [
{
name: 'edit',
link: function(id) {
link: function(item) {
return (
<a href={ '?page=mailpoet-newsletter-editor&id=' + id }>
<a href={ `?page=mailpoet-newsletter-editor&id=${ item.id }` }>
Edit
</a>
);
@ -100,11 +158,13 @@ define(
</h2>
<Listing
params={ this.props.params }
endpoint="newsletters"
onRenderItem={this.renderItem}
columns={columns}
bulk_actions={ bulk_actions }
item_actions={ item_actions } />
item_actions={ item_actions }
messages={ messages } />
</div>
);
}

View File

@ -7,6 +7,7 @@ import NewsletterTemplates from 'newsletters/templates.jsx'
import NewsletterSend from 'newsletters/send.jsx'
import NewsletterStandard from 'newsletters/types/standard.jsx'
import NewsletterWelcome from 'newsletters/types/welcome.jsx'
import NewsletterNotification from 'newsletters/types/notification.jsx'
import createHashHistory from 'history/lib/createHashHistory'
let history = createHashHistory({ queryKey: false })
@ -27,8 +28,10 @@ if(container) {
<Route path="new" component={ NewsletterTypes } />
<Route name="standard" path="new/standard" component={ NewsletterStandard } />
<Route name="welcome" path="new/welcome" component={ NewsletterWelcome } />
<Route name="notification" path="new/notification" component={ NewsletterNotification } />
<Route name="template" path="template/:id" component={ NewsletterTemplates } />
<Route path="send/:id" component={ NewsletterSend } />
<Route path="filter[:filter]" component={ NewsletterList } />
<Route path="*" component={ NewsletterList } />
</Route>
</Router>

View File

@ -85,6 +85,26 @@ define(
</a>
</div>
</li>
<li data-type="notification">
<div className="mailpoet_thumbnail"></div>
<div className="mailpoet_description">
<h3>Post notifications</h3>
<p>
Automatically send posts immediately, daily, weekly or monthly. Filter by categories, if you like.
</p>
</div>
<div className="mailpoet_actions">
<a
className="button button-primary"
onClick={ this.setupNewsletter.bind(null, 'notification') }
>
Set up
</a>
</div>
</li>
</ul>
</div>
);

View File

@ -0,0 +1,225 @@
define(
[
'underscore',
'react',
'react-router',
'mailpoet',
'form/form.jsx',
'form/fields/select.jsx',
'form/fields/selection.jsx',
'form/fields/text.jsx',
'newsletters/breadcrumb.jsx'
],
function(
_,
React,
Router,
MailPoet,
Form,
Select,
Selection,
Text,
Breadcrumb
) {
var intervalField = {
name: 'interval',
values: {
'daily': 'Once a day at...',
'weekly': 'Weekly on...',
'monthly': 'Monthly on the...',
'nthWeekDay': 'Monthly every...',
'immediately': 'Immediately.',
},
};
var SECONDS_IN_DAY = 86400;
var TIME_STEP_SECONDS = 3600; // Default: 3600
var numberOfTimeSteps = SECONDS_IN_DAY / TIME_STEP_SECONDS;
var timeOfDayValues = _.object(_.map(
_.times(numberOfTimeSteps, function(step) { return step * TIME_STEP_SECONDS; }),
function(seconds) {
var date = new Date(null);
date.setSeconds(seconds);
var timeLabel = date.toISOString().substr(11, 5);
return [seconds, timeLabel];
}
));
var timeOfDayField = {
name: 'timeOfDay',
values: timeOfDayValues,
};
var weekDayField = {
name: 'weekDay',
values: {
0: 'Monday',
1: 'Tuesday',
2: 'Wednesday',
3: 'Thursday',
4: 'Friday',
5: 'Saturday',
6: 'Sunday',
},
};
var NUMBER_OF_DAYS_IN_MONTH = 28; // 28 for compatibility with MP2
var monthDayField = {
name: 'monthDay',
values: _.object(_.map(
_.times(NUMBER_OF_DAYS_IN_MONTH, function(day) { return day; }),
function(day) {
var suffixes = {
0: 'st',
1: 'nd',
2: 'rd'
};
var suffix = suffixes[day] || 'th';
return [day, (day + 1).toString() + suffix];
},
)),
};
var nthWeekDayField = {
name: 'nthWeekDay',
values: {
'0': '1st',
'1': '2nd',
'2': '3rd',
'3': 'last',
},
};
var NewsletterWelcome = React.createClass({
mixins: [
Router.History
],
getInitialState: function() {
return {
intervalType: 'immediate', // 'immediate'|'daily'|'weekly'|'monthly'
timeOfDay: 0,
weekDay: 0,
monthDay: 0,
nthWeekDay: 0,
};
},
handleIntervalChange: function(event) {
this.setState({
intervalType: event.target.value,
});
},
handleTimeOfDayChange: function(event) {
this.setState({
timeOfDay: event.target.value,
});
},
handleWeekDayChange: function(event) {
this.setState({
weekDay: event.target.value,
});
},
handleMonthDayChange: function(event) {
this.setState({
monthDay: event.target.value,
});
},
handleNthWeekDayChange: function(event) {
this.setState({
nthWeekDay: event.target.value,
});
},
handleNext: function() {
MailPoet.Ajax.post({
endpoint: 'newsletters',
action: 'create',
data: {
type: 'notification',
options: this.state,
},
}).done(function(response) {
if(response.id !== undefined) {
this.showTemplateSelection(response.id);
} else {
response.map(function(error) {
MailPoet.Notice.error(error);
});
}
}.bind(this));
},
showTemplateSelection: function(newsletterId) {
this.history.pushState(null, `/template/${newsletterId}`);
},
render: function() {
var timeOfDaySelection,
weekDaySelection,
monthDaySelection,
nthWeekDaySelection;
if (this.state.intervalType !== 'immediately') {
timeOfDaySelection = (
<Select
field={timeOfDayField}
item={this.state}
onValueChange={this.handleTimeOfDayChange} />
);
}
if (this.state.intervalType === 'weekly'
|| this.state.intervalType === 'nthWeekDay') {
weekDaySelection = (
<Select
field={weekDayField}
item={this.state}
onValueChange={this.handleWeekDayChange} />
);
}
if (this.state.intervalType === 'monthly') {
monthDaySelection = (
<Select
field={monthDayField}
item={this.state}
onValueChange={this.handleMonthDayChange} />
);
}
if (this.state.intervalType === 'nthWeekDay') {
nthWeekDaySelection = (
<Select
field={nthWeekDayField}
item={this.state}
onValueChange={this.handleNthWeekDayChange} />
);
}
return (
<div>
<h1>Post notifications</h1>
<Breadcrumb step="type" />
<Select
field={intervalField}
item={this.state}
onValueChange={this.handleIntervalChange} />
{nthWeekDaySelection}
{monthDaySelection}
{weekDaySelection}
{timeOfDaySelection}
<p className="submit">
<input
className="button button-primary"
type="button"
onClick={ this.handleNext }
value="Next" />
</p>
</div>
);
},
});
return NewsletterWelcome;
}
);

View File

@ -17,6 +17,11 @@ define(
name: 'name',
label: 'Name',
type: 'text'
},
{
name: 'description',
label: 'Description',
type: 'textarea'
}
];
@ -29,21 +34,27 @@ define(
}
};
var Link = Router.Link;
var SegmentForm = React.createClass({
mixins: [
Router.History
],
render: function() {
return (
<div>
<h2 className="title">
Segment <Link className="add-new-h2" to="/">Back to list</Link>
Segment <a
href="javascript:;"
className="add-new-h2"
onClick={ this.history.goBack }
>Back to list</a>
</h2>
<Form
endpoint="segments"
fields={ fields }
params={ this.props.params }
messages={ messages } />
messages={ messages }
onSuccess={ this.history.goBack } />
</div>
);
}

View File

@ -1,85 +1,202 @@
define(
[
'react',
'react-router',
'listing/listing.jsx',
'classnames'
],
function(
React,
Router,
Listing,
classNames
) {
var columns = [
{
name: 'name',
label: 'Name',
sortable: true
},
{
name: 'created_at',
label: 'Created on',
sortable: true
},
{
name: 'updated_at',
label: 'Last modified on',
sortable: true
}
];
import React from 'react'
import { Router, Route, Link } from 'react-router'
var bulk_actions = [
{
name: 'trash',
label: 'Trash'
}
];
import jQuery from 'jquery'
import MailPoet from 'mailpoet'
import classNames from 'classnames'
var Link = Router.Link;
import Listing from 'listing/listing.jsx'
var SegmentList = React.createClass({
renderItem: function(segment, actions) {
var rowClasses = classNames(
'manage-column',
'column-primary',
'has-row-actions'
);
return (
<div>
<td className={ rowClasses }>
<strong>
<a>{ segment.name }</a>
</strong>
{ actions }
</td>
<td className="column-date" data-colname="Subscribed on">
<abbr>{ segment.created_at }</abbr>
</td>
<td className="column-date" data-colname="Last modified on">
<abbr>{ segment.updated_at }</abbr>
</td>
</div>
);
},
render: function() {
return (
<div>
<h2 className="title">
Segments <Link className="add-new-h2" to="/new">New</Link>
</h2>
<Listing
endpoint="segments"
onRenderItem={this.renderItem}
columns={columns}
bulk_actions={ bulk_actions } />
</div>
);
}
});
return SegmentList;
var columns = [
{
name: 'name',
label: 'Name',
sortable: true
},
{
name: 'description',
label: 'Description',
sortable: false
},
{
name: 'subscribed',
label: 'Subscribed',
sortable: false
},
{
name: 'unconfirmed',
label: 'Unconfirmed',
sortable: false
},
{
name: 'unsubscribed',
label: 'Unsubscribed',
sortable: false
},
{
name: 'created_at',
label: 'Created on',
sortable: true
}
);
];
var messages = {
onTrash: function(response) {
if(response) {
var message = null;
if(~~response === 1) {
message = (
'1 segment was moved to the trash.'
);
} else if(~~response > 1) {
message = (
'%$1d segments were moved to the trash.'
).replace('%$1d', ~~response);
}
if(message !== null) {
MailPoet.Notice.success(message);
}
}
},
onDelete: function(response) {
if(response) {
var message = null;
if(~~response === 1) {
message = (
'1 segment was permanently deleted.'
);
} else if(~~response > 1) {
message = (
'%$1d segments were permanently deleted.'
).replace('%$1d', ~~response);
}
if(message !== null) {
MailPoet.Notice.success(message);
}
}
},
onRestore: function(response) {
if(response) {
var message = null;
if(~~response === 1) {
message = (
'1 segment has been restored from the trash.'
);
} else if(~~response > 1) {
message = (
'%$1d segments have been restored from the trash.'
).replace('%$1d', ~~response);
}
if(message !== null) {
MailPoet.Notice.success(message);
}
}
}
};
var item_actions = [
{
name: 'edit',
label: 'Edit',
link: function(item) {
return (
<Link to={ `/edit/${item.id}` }>Edit</Link>
);
}
},
{
name: 'duplicate_segment',
label: 'Duplicate',
onClick: function(item, refresh) {
return MailPoet.Ajax.post({
endpoint: 'segments',
action: 'duplicate',
data: item.id
}).done(function(response) {
MailPoet.Notice.success(
('List "%$1s" has been duplicated.').replace('%$1s', response.name)
);
refresh();
});
}
},
{
name: 'view_subscribers',
link: function(item) {
return (
<a href={ item.subscribers_url }>View subscribers</a>
);
}
}
];
var bulk_actions = [
{
name: 'trash',
label: 'Trash',
onSuccess: messages.onTrash
}
];
var SegmentList = React.createClass({
renderItem: function(segment, actions) {
var rowClasses = classNames(
'manage-column',
'column-primary',
'has-row-actions'
);
return (
<div>
<td className={ rowClasses }>
<strong>
<a>{ segment.name }</a>
</strong>
{ actions }
</td>
<td className="column-date" data-colname="Description">
<abbr>{ segment.description }</abbr>
</td>
<td className="column-date" data-colname="Subscribed">
<abbr>{ segment.subscribed || 0 }</abbr>
</td>
<td className="column-date" data-colname="Unconfirmed">
<abbr>{ segment.unconfirmed || 0 }</abbr>
</td>
<td className="column-date" data-colname="Unsubscribed">
<abbr>{ segment.unsubscribed || 0 }</abbr>
</td>
<td className="column-date" data-colname="Created on">
<abbr>{ segment.created_at }</abbr>
</td>
</div>
);
},
render: function() {
return (
<div>
<h2 className="title">
Segments <Link className="add-new-h2" to="/new">New</Link>
</h2>
<Listing
location={ this.props.location }
params={ this.props.params }
messages={ messages }
search={ false }
limit={ 1000 }
endpoint="segments"
onRenderItem={ this.renderItem }
columns={ columns }
bulk_actions={ bulk_actions }
item_actions={ item_actions }
/>
</div>
);
}
});
module.exports = SegmentList;

View File

@ -52,18 +52,26 @@ define(
var Link = Router.Link;
var SubscriberForm = React.createClass({
mixins: [
Router.History
],
render: function() {
return (
<div>
<h2 className="title">
Subscriber <Link className="add-new-h2" to="/">Back to list</Link>
Subscriber <a
href="javascript:;"
className="add-new-h2"
onClick={ this.history.goBack }
>Back to list</a>
</h2>
<Form
endpoint="subscribers"
fields={ fields }
params={ this.props.params }
messages={ messages } />
messages={ messages }
onSuccess={ this.history.goBack } />
</div>
);
}

View File

@ -1,313 +1,294 @@
define(
[
'react',
'react-router',
'listing/listing.jsx',
'form/fields/selection.jsx',
'classnames',
'mailpoet',
'jquery',
'select2'
],
function(
React,
Router,
Listing,
Selection,
classNames,
MailPoet,
jQuery
) {
var Link = Router.Link;
import React from 'react'
import { Router, Route, Link } from 'react-router'
var columns = [
{
name: 'email',
label: 'Email',
sortable: true
},
{
name: 'first_name',
label: 'Firstname',
sortable: true
},
{
name: 'last_name',
label: 'Lastname',
sortable: true
},
{
name: 'status',
label: 'Status',
sortable: true
},
{
name: 'segments',
label: 'Lists',
sortable: false
},
import jQuery from 'jquery'
import MailPoet from 'mailpoet'
import classNames from 'classnames'
{
name: 'created_at',
label: 'Subscribed on',
sortable: true
},
{
name: 'updated_at',
label: 'Last modified on',
sortable: true
},
];
import Listing from 'listing/listing.jsx'
import Selection from 'form/fields/selection.jsx'
var messages = {
onDelete: function(response) {
var count = ~~response.subscribers;
var message = null;
const columns = [
{
name: 'email',
label: 'Subscriber',
sortable: true
},
{
name: 'status',
label: 'Status',
sortable: true
},
{
name: 'segments',
label: 'Lists',
sortable: false
},
if(count === 1) {
message = (
'1 subscriber was moved to the trash.'
).replace('%$1d', count);
} else if(count > 1) {
message = (
'%$1d subscribers were moved to the trash.'
).replace('%$1d', count);
}
{
name: 'created_at',
label: 'Subscribed on',
sortable: true
},
{
name: 'updated_at',
label: 'Last modified on',
sortable: true
},
];
if(message !== null) {
MailPoet.Notice.success(message);
}
},
onConfirmDelete: function(response) {
var count = ~~response.subscribers;
var message = null;
if(count === 1) {
message = (
'1 subscriber was permanently deleted.'
).replace('%$1d', count);
} else if(count > 1) {
message = (
'%$1d subscribers were permanently deleted.'
).replace('%$1d', count);
}
if(message !== null) {
MailPoet.Notice.success(message);
}
},
onRestore: function(response) {
var count = ~~response.subscribers;
var message = null;
if(count === 1) {
message = (
'1 subscriber has been restored from the trash.'
).replace('%$1d', count);
} else if(count > 1) {
message = (
'%$1d subscribers have been restored from the trash.'
).replace('%$1d', count);
}
if(message !== null) {
MailPoet.Notice.success(message);
}
}
};
var bulk_actions = [
{
name: 'moveToList',
label: 'Move to list...',
onSelect: function() {
var field = {
id: 'move_to_segment',
endpoint: 'segments'
};
return (
<Selection field={ field }/>
);
},
getData: function() {
return {
segment_id: ~~(jQuery('#move_to_segment').val())
}
},
onSuccess: function(response) {
MailPoet.Notice.success(
'%$1d subscribers were moved to list <strong>%$2s</strong>.'
.replace('%$1d', ~~response.subscribers)
.replace('%$2s', response.segment)
);
}
},
{
name: 'addToList',
label: 'Add to list...',
onSelect: function() {
var field = {
id: 'add_to_segment',
endpoint: 'segments'
};
return (
<Selection field={ field }/>
);
},
getData: function() {
return {
segment_id: ~~(jQuery('#add_to_segment').val())
}
},
onSuccess: function(response) {
MailPoet.Notice.success(
'%$1d subscribers were added to list <strong>%$2s</strong>.'
.replace('%$1d', ~~response.subscribers)
.replace('%$2s', response.segment)
);
}
},
{
name: 'removeFromList',
label: 'Remove from list...',
onSelect: function() {
var field = {
id: 'remove_from_segment',
endpoint: 'segments'
};
return (
<Selection field={ field }/>
);
},
getData: function() {
return {
segment_id: ~~(jQuery('#remove_from_segment').val())
}
},
onSuccess: function(response) {
MailPoet.Notice.success(
'%$1d subscribers were removed from list <strong>%$2s</strong>.'
.replace('%$1d', ~~response.subscribers)
.replace('%$2s', response.segment)
);
}
},
{
name: 'removeFromAllLists',
label: 'Remove from all lists',
onSuccess: function(response) {
MailPoet.Notice.success(
'%$1d subscribers were removed from all lists.'
.replace('%$1d', ~~response.subscribers)
.replace('%$2s', response.segment)
);
}
},
{
name: 'confirmUnconfirmed',
label: 'Confirm unconfirmed',
onSuccess: function(response) {
MailPoet.Notice.success(
'%$1d subscribers have been confirmed.'
.replace('%$1d', ~~response.subscribers)
);
}
},
{
name: 'trash',
label: 'Trash',
getData: function() {
return {
confirm: false
}
},
onSuccess: messages.onDelete
}
];
var SubscriberList = React.createClass({
renderItem: function(subscriber, actions) {
var row_classes = classNames(
'manage-column',
'column-primary',
'has-row-actions'
);
var status = '';
switch(subscriber.status) {
case 'subscribed':
status = 'Subscribed';
break;
case 'unconfirmed':
status = 'Unconfirmed';
break;
case 'unsubscribed':
status = 'Unsubscribed';
break;
}
var segments = mailpoet_segments.filter(function(segment) {
return (jQuery.inArray(segment.id, subscriber.segments) !== -1);
}).map(function(segment) {
return segment.name;
}).join(', ');
var row_actions = false;
return (
<div>
<td className={ row_classes }>
<strong><Link to={ `/edit/${ subscriber.id }` }>
{ subscriber.email }
</Link></strong>
{ actions }
</td>
<td className="column" data-colname="First name">
{ subscriber.first_name }
</td>
<td className="column" data-colname="Last name">
{ subscriber.last_name }
</td>
<td className="column" data-colname="Status">
{ status }
</td>
<td className="column" data-colname="Lists">
{ segments }
</td>
<td className="column-date" data-colname="Subscribed on">
<abbr>{ subscriber.created_at }</abbr>
</td>
<td className="column-date" data-colname="Last modified on">
<abbr>{ subscriber.updated_at }</abbr>
</td>
</div>
);
},
render: function() {
return (
<div>
<h2 className="title">
Subscribers <Link className="add-new-h2" to="/new">New</Link>
</h2>
<Listing
endpoint="subscribers"
onRenderItem={ this.renderItem }
columns={ columns }
bulk_actions={ bulk_actions }
messages={ messages }
/>
</div>
const messages = {
onTrash: function(response) {
if(response) {
var message = null;
if(~~response === 1) {
message = (
'1 subscriber was moved to the trash.'
);
} else if(~~response > 1) {
message = (
'%$1d subscribers were moved to the trash.'
).replace('%$1d', ~~response);
}
});
return SubscriberList;
if(message !== null) {
MailPoet.Notice.success(message);
}
}
},
onDelete: function(response) {
if(response) {
var message = null;
if(~~response === 1) {
message = (
'1 subscriber was permanently deleted.'
);
} else if(~~response > 1) {
message = (
'%$1d subscribers were permanently deleted.'
).replace('%$1d', ~~response);
}
if(message !== null) {
MailPoet.Notice.success(message);
}
}
},
onRestore: function(response) {
if(response) {
var message = null;
if(~~response === 1) {
message = (
'1 subscriber has been restored from the trash.'
);
} else if(~~response > 1) {
message = (
'%$1d subscribers have been restored from the trash.'
).replace('%$1d', ~~response);
}
if(message !== null) {
MailPoet.Notice.success(message);
}
}
}
);
};
const bulk_actions = [
{
name: 'moveToList',
label: 'Move to list...',
onSelect: function() {
let field = {
id: 'move_to_segment',
endpoint: 'segments'
};
return (
<Selection field={ field }/>
);
},
getData: function() {
return {
segment_id: ~~(jQuery('#move_to_segment').val())
}
},
onSuccess: function(response) {
MailPoet.Notice.success(
'%$1d subscribers were moved to list <strong>%$2s</strong>.'
.replace('%$1d', ~~response.subscribers)
.replace('%$2s', response.segment)
);
}
},
{
name: 'addToList',
label: 'Add to list...',
onSelect: function() {
let field = {
id: 'add_to_segment',
endpoint: 'segments'
};
return (
<Selection field={ field }/>
);
},
getData: function() {
return {
segment_id: ~~(jQuery('#add_to_segment').val())
}
},
onSuccess: function(response) {
MailPoet.Notice.success(
'%$1d subscribers were added to list <strong>%$2s</strong>.'
.replace('%$1d', ~~response.subscribers)
.replace('%$2s', response.segment)
);
}
},
{
name: 'removeFromList',
label: 'Remove from list...',
onSelect: function() {
let field = {
id: 'remove_from_segment',
endpoint: 'segments'
};
return (
<Selection field={ field }/>
);
},
getData: function() {
return {
segment_id: ~~(jQuery('#remove_from_segment').val())
}
},
onSuccess: function(response) {
MailPoet.Notice.success(
'%$1d subscribers were removed from list <strong>%$2s</strong>.'
.replace('%$1d', ~~response.subscribers)
.replace('%$2s', response.segment)
);
}
},
{
name: 'removeFromAllLists',
label: 'Remove from all lists',
onSuccess: function(response) {
MailPoet.Notice.success(
'%$1d subscribers were removed from all lists.'
.replace('%$1d', ~~response)
);
}
},
{
name: 'confirmUnconfirmed',
label: 'Confirm unconfirmed',
onSuccess: function(response) {
MailPoet.Notice.success(
'%$1d subscribers have been confirmed.'
.replace('%$1d', ~~response)
);
}
},
{
name: 'trash',
label: 'Trash',
onSuccess: messages.onTrash
}
];
const SubscriberList = React.createClass({
renderItem: function(subscriber, actions) {
let row_classes = classNames(
'manage-column',
'column-primary',
'has-row-actions',
'column-username'
);
let status = '';
switch(subscriber.status) {
case 'subscribed':
status = 'Subscribed';
break;
case 'unconfirmed':
status = 'Unconfirmed';
break;
case 'unsubscribed':
status = 'Unsubscribed';
break;
}
let segments = mailpoet_segments.filter(function(segment) {
return (jQuery.inArray(segment.id, subscriber.segments) !== -1);
}).map(function(segment) {
return segment.name;
}).join(', ');
let avatar = false;
if(subscriber.avatar_url) {
avatar = (
<img
className="avatar"
src={ subscriber.avatar_url }
title=""
width="32"
height="32"
/>
);
}
return (
<div>
<td className={ row_classes }>
<strong><Link to={ `/edit/${ subscriber.id }` }>
{ subscriber.email }
</Link></strong>
<p style={{margin: 0}}>
{ subscriber.first_name } { subscriber.last_name }
</p>
{ actions }
</td>
<td className="column" data-colname="Status">
{ status }
</td>
<td className="column" data-colname="Lists">
{ segments }
</td>
<td className="column-date" data-colname="Subscribed on">
<abbr>{ subscriber.created_at }</abbr>
</td>
<td className="column-date" data-colname="Last modified on">
<abbr>{ subscriber.updated_at }</abbr>
</td>
</div>
);
},
render: function() {
return (
<div>
<h2 className="title">
Subscribers <Link className="add-new-h2" to="/new">New</Link>
</h2>
<Listing
location={ this.props.location }
params={ this.props.params }
endpoint="subscribers"
onRenderItem={ this.renderItem }
columns={ columns }
bulk_actions={ bulk_actions }
messages={ messages }
/>
</div>
)
}
});
module.exports = SubscriberList;

View File

@ -5,15 +5,15 @@ import SubscriberList from 'subscribers/list.jsx'
import SubscriberForm from 'subscribers/form.jsx'
import createHashHistory from 'history/lib/createHashHistory'
let history = createHashHistory({ queryKey: false })
const history = createHashHistory({ queryKey: false })
const App = React.createClass({
render() {
return this.props.children
return this.props.children;
}
});
let container = document.getElementById('subscribers_container');
const container = document.getElementById('subscribers_container')
if(container) {
ReactDOM.render((

View File

@ -7,7 +7,8 @@
"sunra/php-simple-html-dom-parser": "*",
"tburry/pquery": "*",
"j4mie/paris": "1.5.4",
"swiftmailer/swiftmailer": "^5.4"
"swiftmailer/swiftmailer": "^5.4",
"phpseclib/phpseclib": "*"
},
"require-dev": {
"codeception/codeception": "*",

359
composer.lock generated
View File

@ -4,8 +4,8 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"hash": "92704d2679fce692438b9e6f1dc6e02f",
"content-hash": "3297411fcec47a02bc4f456fbf3751d1",
"hash": "7d7ef94b6e40ac2b2d594e5832d7e16d",
"content-hash": "2e70c335edf7429df0794ebf49e2f210",
"packages": [
{
"name": "cerdic/css-tidy",
@ -218,6 +218,94 @@
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
"time": "2015-09-14 09:18:12"
},
{
"name": "phpseclib/phpseclib",
"version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/phpseclib/phpseclib.git",
"reference": "a74aa9efbe61430fcb60157c8e025a48ec8ff604"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/a74aa9efbe61430fcb60157c8e025a48ec8ff604",
"reference": "a74aa9efbe61430fcb60157c8e025a48ec8ff604",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"require-dev": {
"phing/phing": "~2.7",
"phpunit/phpunit": "~4.0",
"sami/sami": "~2.0",
"squizlabs/php_codesniffer": "~2.0"
},
"suggest": {
"ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.",
"ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.",
"ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.",
"ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations.",
"pear-pear/PHP_Compat": "Install PHP_Compat to get phpseclib working on PHP < 5.0.0."
},
"type": "library",
"autoload": {
"psr-4": {
"phpseclib\\": "phpseclib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"include-path": [
"phpseclib/"
],
"license": [
"MIT"
],
"authors": [
{
"name": "Jim Wigginton",
"email": "terrafrost@php.net",
"role": "Lead Developer"
},
{
"name": "Patrick Monnerat",
"email": "pm@datasphere.ch",
"role": "Developer"
},
{
"name": "Andreas Fischer",
"email": "bantu@phpbb.com",
"role": "Developer"
},
{
"name": "Hans-Jürgen Petrich",
"email": "petrich@tronic-media.com",
"role": "Developer"
}
],
"description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.",
"homepage": "http://phpseclib.sourceforge.net",
"keywords": [
"BigInteger",
"aes",
"asn.1",
"asn1",
"blowfish",
"crypto",
"cryptography",
"encryption",
"rsa",
"security",
"sftp",
"signature",
"signing",
"ssh",
"twofish",
"x.509",
"x509"
],
"time": "2015-08-04 04:48:03"
},
{
"name": "sunra/php-simple-html-dom-parser",
"version": "v1.5.0",
@ -368,16 +456,16 @@
},
{
"name": "twig/twig",
"version": "v1.22.3",
"version": "v1.23.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "ebfc36b7e77b0c1175afe30459cf943010245540"
"reference": "5868cd822fd6cf626d5f805439575f9c323cee2a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/ebfc36b7e77b0c1175afe30459cf943010245540",
"reference": "ebfc36b7e77b0c1175afe30459cf943010245540",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/5868cd822fd6cf626d5f805439575f9c323cee2a",
"reference": "5868cd822fd6cf626d5f805439575f9c323cee2a",
"shasum": ""
},
"require": {
@ -390,7 +478,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.22-dev"
"dev-master": "1.23-dev"
}
},
"autoload": {
@ -425,7 +513,7 @@
"keywords": [
"templating"
],
"time": "2015-10-13 07:07:02"
"time": "2015-10-29 23:29:01"
}
],
"packages-dev": [
@ -545,16 +633,16 @@
},
{
"name": "codegyre/robo",
"version": "0.5.4",
"version": "0.6.0",
"source": {
"type": "git",
"url": "https://github.com/Codegyre/Robo.git",
"reference": "10aa223f6d1db182dc81d723bf1545dfc6ff380d"
"reference": "d18185f0494c854d36aa5ee0ad931ee23bbef552"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Codegyre/Robo/zipball/10aa223f6d1db182dc81d723bf1545dfc6ff380d",
"reference": "10aa223f6d1db182dc81d723bf1545dfc6ff380d",
"url": "https://api.github.com/repos/Codegyre/Robo/zipball/d18185f0494c854d36aa5ee0ad931ee23bbef552",
"reference": "d18185f0494c854d36aa5ee0ad931ee23bbef552",
"shasum": ""
},
"require": {
@ -568,6 +656,7 @@
"require-dev": {
"codeception/aspect-mock": "0.5.*",
"codeception/base": "~2.1",
"codeception/codeception": "2.1",
"codeception/verify": "0.2.*",
"natxet/cssmin": "~3.0",
"patchwork/jsqueeze": "~1.0"
@ -592,7 +681,7 @@
}
],
"description": "Modern task runner",
"time": "2015-08-31 17:35:30"
"time": "2015-10-30 11:29:52"
},
{
"name": "doctrine/instantiator",
@ -867,12 +956,12 @@
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/henrikbjorn/Lurker.git",
"url": "https://github.com/flint/Lurker.git",
"reference": "a020d45b3bc37810aeafe27343c51af8a74c9419"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/henrikbjorn/Lurker/zipball/a020d45b3bc37810aeafe27343c51af8a74c9419",
"url": "https://api.github.com/repos/flint/Lurker/zipball/a020d45b3bc37810aeafe27343c51af8a74c9419",
"reference": "a020d45b3bc37810aeafe27343c51af8a74c9419",
"shasum": ""
},
@ -901,18 +990,16 @@
],
"authors": [
{
"name": "Henrik Bjornskov",
"email": "henrik@bjrnskov.dk",
"homepage": "http://henrik.bjrnskov.dk"
"name": "Yaroslav Kiliba",
"email": "om.dattaya@gmail.com"
},
{
"name": "Konstantin Kudryashov",
"email": "ever.zet@gmail.com",
"homepage": "http://everzet.com"
"email": "ever.zet@gmail.com"
},
{
"name": "Yaroslav Kiliba",
"email": "om.dattaya@gmail.com"
"name": "Henrik Bjrnskov",
"email": "henrik@bjrnskov.dk"
}
],
"description": "Resource Watcher.",
@ -1873,16 +1960,16 @@
},
{
"name": "symfony/browser-kit",
"version": "v2.7.5",
"version": "v2.7.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/browser-kit.git",
"reference": "277a2457776d4cc25706fbdd9d1e4ab2dac884e4"
"reference": "07d664a052572ccc28eb2ab7dbbe82155b1ad367"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/browser-kit/zipball/277a2457776d4cc25706fbdd9d1e4ab2dac884e4",
"reference": "277a2457776d4cc25706fbdd9d1e4ab2dac884e4",
"url": "https://api.github.com/repos/symfony/browser-kit/zipball/07d664a052572ccc28eb2ab7dbbe82155b1ad367",
"reference": "07d664a052572ccc28eb2ab7dbbe82155b1ad367",
"shasum": ""
},
"require": {
@ -1891,8 +1978,7 @@
},
"require-dev": {
"symfony/css-selector": "~2.0,>=2.0.5",
"symfony/phpunit-bridge": "~2.7",
"symfony/process": "~2.0,>=2.0.5"
"symfony/process": "~2.3.34|~2.7,>=2.7.6"
},
"suggest": {
"symfony/process": ""
@ -1924,29 +2010,26 @@
],
"description": "Symfony BrowserKit Component",
"homepage": "https://symfony.com",
"time": "2015-09-06 08:36:38"
"time": "2015-10-23 14:47:27"
},
{
"name": "symfony/config",
"version": "v2.7.5",
"version": "v2.7.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/config.git",
"reference": "9698fdf0a750d6887d5e7729d5cf099765b20e61"
"reference": "831f88908b51b9ce945f5e6f402931d1ac544423"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/config/zipball/9698fdf0a750d6887d5e7729d5cf099765b20e61",
"reference": "9698fdf0a750d6887d5e7729d5cf099765b20e61",
"url": "https://api.github.com/repos/symfony/config/zipball/831f88908b51b9ce945f5e6f402931d1ac544423",
"reference": "831f88908b51b9ce945f5e6f402931d1ac544423",
"shasum": ""
},
"require": {
"php": ">=5.3.9",
"symfony/filesystem": "~2.3"
},
"require-dev": {
"symfony/phpunit-bridge": "~2.7"
},
"type": "library",
"extra": {
"branch-alias": {
@ -1974,20 +2057,20 @@
],
"description": "Symfony Config Component",
"homepage": "https://symfony.com",
"time": "2015-09-21 15:02:29"
"time": "2015-10-11 09:39:48"
},
{
"name": "symfony/console",
"version": "v2.7.5",
"version": "v2.7.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "06cb17c013a82f94a3d840682b49425cd00a2161"
"reference": "5efd632294c8320ea52492db22292ff853a43766"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/06cb17c013a82f94a3d840682b49425cd00a2161",
"reference": "06cb17c013a82f94a3d840682b49425cd00a2161",
"url": "https://api.github.com/repos/symfony/console/zipball/5efd632294c8320ea52492db22292ff853a43766",
"reference": "5efd632294c8320ea52492db22292ff853a43766",
"shasum": ""
},
"require": {
@ -1996,7 +2079,6 @@
"require-dev": {
"psr/log": "~1.0",
"symfony/event-dispatcher": "~2.1",
"symfony/phpunit-bridge": "~2.7",
"symfony/process": "~2.1"
},
"suggest": {
@ -2031,28 +2113,25 @@
],
"description": "Symfony Console Component",
"homepage": "https://symfony.com",
"time": "2015-09-25 08:32:23"
"time": "2015-10-20 14:38:46"
},
{
"name": "symfony/css-selector",
"version": "v2.7.5",
"version": "v2.7.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
"reference": "abe19cc0429a06be0c133056d1f9859854860970"
"reference": "e1b865b26be4a56d22a8dee398375044a80c865b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/abe19cc0429a06be0c133056d1f9859854860970",
"reference": "abe19cc0429a06be0c133056d1f9859854860970",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/e1b865b26be4a56d22a8dee398375044a80c865b",
"reference": "e1b865b26be4a56d22a8dee398375044a80c865b",
"shasum": ""
},
"require": {
"php": ">=5.3.9"
},
"require-dev": {
"symfony/phpunit-bridge": "~2.7"
},
"type": "library",
"extra": {
"branch-alias": {
@ -2084,28 +2163,27 @@
],
"description": "Symfony CssSelector Component",
"homepage": "https://symfony.com",
"time": "2015-09-22 13:49:29"
"time": "2015-10-11 09:39:48"
},
{
"name": "symfony/dom-crawler",
"version": "v2.7.5",
"version": "v2.7.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/dom-crawler.git",
"reference": "2e185ca136399f902b948694987e62c80099c052"
"reference": "5fef7d8b80d8f9992df99d8ee283f420484c9612"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/dom-crawler/zipball/2e185ca136399f902b948694987e62c80099c052",
"reference": "2e185ca136399f902b948694987e62c80099c052",
"url": "https://api.github.com/repos/symfony/dom-crawler/zipball/5fef7d8b80d8f9992df99d8ee283f420484c9612",
"reference": "5fef7d8b80d8f9992df99d8ee283f420484c9612",
"shasum": ""
},
"require": {
"php": ">=5.3.9"
},
"require-dev": {
"symfony/css-selector": "~2.3",
"symfony/phpunit-bridge": "~2.7"
"symfony/css-selector": "~2.3"
},
"suggest": {
"symfony/css-selector": ""
@ -2137,20 +2215,20 @@
],
"description": "Symfony DomCrawler Component",
"homepage": "https://symfony.com",
"time": "2015-09-20 21:13:58"
"time": "2015-10-11 09:39:48"
},
{
"name": "symfony/event-dispatcher",
"version": "v2.7.5",
"version": "v2.7.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
"reference": "ae4dcc2a8d3de98bd794167a3ccda1311597c5d9"
"reference": "87a5db5ea887763fa3a31a5471b512ff1596d9b8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/ae4dcc2a8d3de98bd794167a3ccda1311597c5d9",
"reference": "ae4dcc2a8d3de98bd794167a3ccda1311597c5d9",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/87a5db5ea887763fa3a31a5471b512ff1596d9b8",
"reference": "87a5db5ea887763fa3a31a5471b512ff1596d9b8",
"shasum": ""
},
"require": {
@ -2161,7 +2239,6 @@
"symfony/config": "~2.0,>=2.0.5",
"symfony/dependency-injection": "~2.6",
"symfony/expression-language": "~2.6",
"symfony/phpunit-bridge": "~2.7",
"symfony/stopwatch": "~2.3"
},
"suggest": {
@ -2195,28 +2272,25 @@
],
"description": "Symfony EventDispatcher Component",
"homepage": "https://symfony.com",
"time": "2015-09-22 13:49:29"
"time": "2015-10-11 09:39:48"
},
{
"name": "symfony/filesystem",
"version": "v2.7.5",
"version": "v2.7.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
"reference": "a17f8a17c20e8614c15b8e116e2f4bcde102cfab"
"reference": "56fd6df73be859323ff97418d97edc1d756df6df"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/a17f8a17c20e8614c15b8e116e2f4bcde102cfab",
"reference": "a17f8a17c20e8614c15b8e116e2f4bcde102cfab",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/56fd6df73be859323ff97418d97edc1d756df6df",
"reference": "56fd6df73be859323ff97418d97edc1d756df6df",
"shasum": ""
},
"require": {
"php": ">=5.3.9"
},
"require-dev": {
"symfony/phpunit-bridge": "~2.7"
},
"type": "library",
"extra": {
"branch-alias": {
@ -2244,28 +2318,25 @@
],
"description": "Symfony Filesystem Component",
"homepage": "https://symfony.com",
"time": "2015-09-09 17:42:36"
"time": "2015-10-18 20:23:18"
},
{
"name": "symfony/finder",
"version": "v2.7.5",
"version": "v2.7.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "8262ab605973afbb3ef74b945daabf086f58366f"
"reference": "2ffb4e9598db3c48eb6d0ae73b04bbf09280c59d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/8262ab605973afbb3ef74b945daabf086f58366f",
"reference": "8262ab605973afbb3ef74b945daabf086f58366f",
"url": "https://api.github.com/repos/symfony/finder/zipball/2ffb4e9598db3c48eb6d0ae73b04bbf09280c59d",
"reference": "2ffb4e9598db3c48eb6d0ae73b04bbf09280c59d",
"shasum": ""
},
"require": {
"php": ">=5.3.9"
},
"require-dev": {
"symfony/phpunit-bridge": "~2.7"
},
"type": "library",
"extra": {
"branch-alias": {
@ -2293,20 +2364,20 @@
],
"description": "Symfony Finder Component",
"homepage": "https://symfony.com",
"time": "2015-09-19 19:59:23"
"time": "2015-10-11 09:39:48"
},
{
"name": "symfony/form",
"version": "v2.7.5",
"version": "v2.7.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/form.git",
"reference": "d4a990d2ebe4dd39cac52c5a40a5aac84b12b237"
"reference": "b93fcb816bec2b8470ea9d54e4b6658b2461b83c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/form/zipball/d4a990d2ebe4dd39cac52c5a40a5aac84b12b237",
"reference": "d4a990d2ebe4dd39cac52c5a40a5aac84b12b237",
"url": "https://api.github.com/repos/symfony/form/zipball/b93fcb816bec2b8470ea9d54e4b6658b2461b83c",
"reference": "b93fcb816bec2b8470ea9d54e4b6658b2461b83c",
"shasum": ""
},
"require": {
@ -2325,7 +2396,6 @@
"doctrine/collections": "~1.0",
"symfony/http-foundation": "~2.2",
"symfony/http-kernel": "~2.4",
"symfony/phpunit-bridge": "~2.7",
"symfony/security-csrf": "~2.4",
"symfony/translation": "~2.0,>=2.0.5",
"symfony/validator": "~2.6,>=2.6.8"
@ -2363,28 +2433,27 @@
],
"description": "Symfony Form Component",
"homepage": "https://symfony.com",
"time": "2015-09-22 13:49:29"
"time": "2015-10-27 15:38:06"
},
{
"name": "symfony/intl",
"version": "v2.7.5",
"version": "v2.7.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/intl.git",
"reference": "35f902b232c10056e17d94a842160d44bb540838"
"reference": "330f52a996749eb6a2fdc1506c7a4868e070d678"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/intl/zipball/35f902b232c10056e17d94a842160d44bb540838",
"reference": "35f902b232c10056e17d94a842160d44bb540838",
"url": "https://api.github.com/repos/symfony/intl/zipball/330f52a996749eb6a2fdc1506c7a4868e070d678",
"reference": "330f52a996749eb6a2fdc1506c7a4868e070d678",
"shasum": ""
},
"require": {
"php": ">=5.3.9"
},
"require-dev": {
"symfony/filesystem": "~2.1",
"symfony/phpunit-bridge": "~2.7"
"symfony/filesystem": "~2.1"
},
"suggest": {
"ext-intl": "to use the component with locales other than \"en\""
@ -2438,28 +2507,25 @@
"l10n",
"localization"
],
"time": "2015-09-09 17:53:06"
"time": "2015-10-11 09:39:48"
},
{
"name": "symfony/options-resolver",
"version": "v2.7.5",
"version": "v2.7.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/options-resolver.git",
"reference": "75389f6f948edfdf0c0ebdbe00c4f84ab5d1a03e"
"reference": "85fd10e551677d3c9a4632def78b8ec4670b247d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/75389f6f948edfdf0c0ebdbe00c4f84ab5d1a03e",
"reference": "75389f6f948edfdf0c0ebdbe00c4f84ab5d1a03e",
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/85fd10e551677d3c9a4632def78b8ec4670b247d",
"reference": "85fd10e551677d3c9a4632def78b8ec4670b247d",
"shasum": ""
},
"require": {
"php": ">=5.3.9"
},
"require-dev": {
"symfony/phpunit-bridge": "~2.7"
},
"type": "library",
"extra": {
"branch-alias": {
@ -2492,28 +2558,25 @@
"configuration",
"options"
],
"time": "2015-09-25 06:59:16"
"time": "2015-10-11 09:39:48"
},
{
"name": "symfony/process",
"version": "v2.7.5",
"version": "v2.7.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "b27c8e317922cd3cdd3600850273cf6b82b2e8e9"
"reference": "4a959dd4e19c2c5d7512689413921e0a74386ec7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/b27c8e317922cd3cdd3600850273cf6b82b2e8e9",
"reference": "b27c8e317922cd3cdd3600850273cf6b82b2e8e9",
"url": "https://api.github.com/repos/symfony/process/zipball/4a959dd4e19c2c5d7512689413921e0a74386ec7",
"reference": "4a959dd4e19c2c5d7512689413921e0a74386ec7",
"shasum": ""
},
"require": {
"php": ">=5.3.9"
},
"require-dev": {
"symfony/phpunit-bridge": "~2.7"
},
"type": "library",
"extra": {
"branch-alias": {
@ -2541,28 +2604,25 @@
],
"description": "Symfony Process Component",
"homepage": "https://symfony.com",
"time": "2015-09-19 19:59:23"
"time": "2015-10-23 14:47:27"
},
{
"name": "symfony/property-access",
"version": "v2.7.5",
"version": "v2.7.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/property-access.git",
"reference": "f8ea7aa472f0e3f8cdf43287caa72a70ff5c088c"
"reference": "368b784738fa932e6d86866038312b03e073a824"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/property-access/zipball/f8ea7aa472f0e3f8cdf43287caa72a70ff5c088c",
"reference": "f8ea7aa472f0e3f8cdf43287caa72a70ff5c088c",
"url": "https://api.github.com/repos/symfony/property-access/zipball/368b784738fa932e6d86866038312b03e073a824",
"reference": "368b784738fa932e6d86866038312b03e073a824",
"shasum": ""
},
"require": {
"php": ">=5.3.9"
},
"require-dev": {
"symfony/phpunit-bridge": "~2.7"
},
"type": "library",
"extra": {
"branch-alias": {
@ -2601,20 +2661,20 @@
"property path",
"reflection"
],
"time": "2015-08-24 07:13:45"
"time": "2015-10-23 14:47:27"
},
{
"name": "symfony/routing",
"version": "v2.7.5",
"version": "v2.7.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/routing.git",
"reference": "6c5fae83efa20baf166fcf4582f57094e9f60f16"
"reference": "f353e1f588679c3ec987624e6c617646bd01ba38"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/routing/zipball/6c5fae83efa20baf166fcf4582f57094e9f60f16",
"reference": "6c5fae83efa20baf166fcf4582f57094e9f60f16",
"url": "https://api.github.com/repos/symfony/routing/zipball/f353e1f588679c3ec987624e6c617646bd01ba38",
"reference": "f353e1f588679c3ec987624e6c617646bd01ba38",
"shasum": ""
},
"require": {
@ -2630,7 +2690,6 @@
"symfony/config": "~2.7",
"symfony/expression-language": "~2.4",
"symfony/http-foundation": "~2.3",
"symfony/phpunit-bridge": "~2.7",
"symfony/yaml": "~2.0,>=2.0.5"
},
"suggest": {
@ -2672,20 +2731,20 @@
"uri",
"url"
],
"time": "2015-09-14 14:14:09"
"time": "2015-10-27 15:38:06"
},
{
"name": "symfony/translation",
"version": "v2.7.5",
"version": "v2.7.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
"reference": "485877661835e188cd78345c6d4eef1290d17571"
"reference": "6ccd9289ec1c71d01a49d83480de3b5293ce30c8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation/zipball/485877661835e188cd78345c6d4eef1290d17571",
"reference": "485877661835e188cd78345c6d4eef1290d17571",
"url": "https://api.github.com/repos/symfony/translation/zipball/6ccd9289ec1c71d01a49d83480de3b5293ce30c8",
"reference": "6ccd9289ec1c71d01a49d83480de3b5293ce30c8",
"shasum": ""
},
"require": {
@ -2698,7 +2757,6 @@
"psr/log": "~1.0",
"symfony/config": "~2.7",
"symfony/intl": "~2.4",
"symfony/phpunit-bridge": "~2.7",
"symfony/yaml": "~2.2"
},
"suggest": {
@ -2733,20 +2791,20 @@
],
"description": "Symfony Translation Component",
"homepage": "https://symfony.com",
"time": "2015-09-06 08:36:38"
"time": "2015-10-27 15:38:06"
},
{
"name": "symfony/twig-bridge",
"version": "v2.7.5",
"version": "v2.7.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/twig-bridge.git",
"reference": "bce37975610a46bde48dbf2f67f724401251d199"
"reference": "3dd44937b1e08af8c8f6b14850f4b9c4d1039c6f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/twig-bridge/zipball/bce37975610a46bde48dbf2f67f724401251d199",
"reference": "bce37975610a46bde48dbf2f67f724401251d199",
"url": "https://api.github.com/repos/symfony/twig-bridge/zipball/3dd44937b1e08af8c8f6b14850f4b9c4d1039c6f",
"reference": "3dd44937b1e08af8c8f6b14850f4b9c4d1039c6f",
"shasum": ""
},
"require": {
@ -2758,10 +2816,9 @@
"symfony/console": "~2.7",
"symfony/expression-language": "~2.4",
"symfony/finder": "~2.3",
"symfony/form": "~2.7,>=2.7.2",
"symfony/form": "~2.7,>=2.7.6",
"symfony/http-kernel": "~2.3",
"symfony/intl": "~2.3",
"symfony/phpunit-bridge": "~2.7",
"symfony/routing": "~2.2",
"symfony/security": "~2.6",
"symfony/security-acl": "~2.6",
@ -2812,28 +2869,25 @@
],
"description": "Symfony Twig Bridge",
"homepage": "https://symfony.com",
"time": "2015-09-23 09:17:11"
"time": "2015-10-11 09:39:48"
},
{
"name": "symfony/yaml",
"version": "v2.7.5",
"version": "v2.7.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "31cb2ad0155c95b88ee55fe12bc7ff92232c1770"
"reference": "eca9019c88fbe250164affd107bc8057771f3f4d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/31cb2ad0155c95b88ee55fe12bc7ff92232c1770",
"reference": "31cb2ad0155c95b88ee55fe12bc7ff92232c1770",
"url": "https://api.github.com/repos/symfony/yaml/zipball/eca9019c88fbe250164affd107bc8057771f3f4d",
"reference": "eca9019c88fbe250164affd107bc8057771f3f4d",
"shasum": ""
},
"require": {
"php": ">=5.3.9"
},
"require-dev": {
"symfony/phpunit-bridge": "~2.7"
},
"type": "library",
"extra": {
"branch-alias": {
@ -2861,7 +2915,7 @@
],
"description": "Symfony Yaml Component",
"homepage": "https://symfony.com",
"time": "2015-09-14 14:14:09"
"time": "2015-10-11 09:39:48"
},
{
"name": "twig/extensions",
@ -2966,25 +3020,30 @@
},
{
"name": "vlucas/phpdotenv",
"version": "v2.0.1",
"version": "v2.1.0",
"source": {
"type": "git",
"url": "https://github.com/vlucas/phpdotenv.git",
"reference": "91064290f5b53a09bdff1b939d7f69fb0e7531b5"
"reference": "c10040e0df17d2ee88e9212b50cbe9319e878f59"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/91064290f5b53a09bdff1b939d7f69fb0e7531b5",
"reference": "91064290f5b53a09bdff1b939d7f69fb0e7531b5",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/c10040e0df17d2ee88e9212b50cbe9319e878f59",
"reference": "c10040e0df17d2ee88e9212b50cbe9319e878f59",
"shasum": ""
},
"require": {
"php": ">=5.3.2"
"php": ">=5.3.9"
},
"require-dev": {
"phpunit/phpunit": "~4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.1-dev"
}
},
"autoload": {
"psr-4": {
"Dotenv\\": "src/"
@ -3008,7 +3067,7 @@
"env",
"environment"
],
"time": "2015-05-30 16:15:01"
"time": "2015-10-28 18:53:35"
}
],
"aliases": [],

View File

@ -39,6 +39,7 @@ class Initializer {
$newsletters = Env::$db_prefix . 'newsletters';
$newsletter_templates = Env::$db_prefix . 'newsletter_templates';
$segments = Env::$db_prefix . 'segments';
$forms = Env::$db_prefix . 'forms';
$subscriber_segment = Env::$db_prefix . 'subscriber_segment';
$newsletter_segment = Env::$db_prefix . 'newsletter_segment';
$custom_fields = Env::$db_prefix . 'custom_fields';
@ -50,6 +51,7 @@ class Initializer {
define('MP_SETTINGS_TABLE', $settings);
define('MP_NEWSLETTERS_TABLE', $newsletters);
define('MP_SEGMENTS_TABLE', $segments);
define('MP_FORMS_TABLE', $forms);
define('MP_SUBSCRIBER_SEGMENT_TABLE', $subscriber_segment);
define('MP_NEWSLETTER_TEMPLATES_TABLE', $newsletter_templates);
define('MP_NEWSLETTER_SEGMENT_TABLE', $newsletter_segment);

View File

@ -41,6 +41,14 @@ class Menu {
'mailpoet-newsletters',
array($this, 'newsletters')
);
add_submenu_page(
'mailpoet',
__('Forms'),
__('Forms'),
'manage_options',
'mailpoet-forms',
array($this, 'forms')
);
add_submenu_page(
'mailpoet',
__('Subscribers'),
@ -163,6 +171,13 @@ class Menu {
echo $this->renderer->render('segments.html', $data);
}
function forms() {
$data = array();
$data['segments'] = Segment::findArray();
echo $this->renderer->render('forms.html', $data);
}
function newsletters() {
global $wp_roles;

View File

@ -21,6 +21,7 @@ class Migrator {
'subscriber_custom_field',
'newsletter_option_fields',
'newsletter_option',
'forms'
);
}
@ -107,6 +108,7 @@ class Migrator {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'name varchar(90) NOT NULL,',
'description varchar(250) NOT NULL,',
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
'deleted_at TIMESTAMP NULL DEFAULT NULL,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
@ -192,6 +194,19 @@ class Migrator {
return $this->sqlify(__FUNCTION__, $attributes);
}
function forms() {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'name varchar(90) NOT NULL,',
'body longtext,',
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
'deleted_at TIMESTAMP NULL DEFAULT NULL,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id)'
);
return $this->sqlify(__FUNCTION__, $attributes);
}
private function sqlify($model, $attributes) {
$table = $this->prefix . $model;

View File

@ -60,6 +60,27 @@ class Populator {
'name' => 'afterTimeType',
'newsletter_type' => 'welcome',
),
array(
'name' => 'intervalType',
'newsletter_type' => 'notification',
),
array(
'name' => 'timeOfDay',
'newsletter_type' => 'notification',
),
array(
'name' => 'weekDay',
'newsletter_type' => 'notification',
),
array(
'name' => 'monthDay',
'newsletter_type' => 'notification',
),
array(
'name' => 'nthWeekDay',
'newsletter_type' => 'notification',
),
);
}

View File

@ -5,15 +5,17 @@ if(!defined('ABSPATH')) exit;
class BulkAction {
private $listing = null;
private $action = null;
private $data = null;
private $model_class = null;
function __construct($model_class, $data) {
$this->model_class = $model_class;
$this->action = $data['action'];
unset($data['action']);
$this->data = $data;
$this->model_class = $model_class;
$this->listing = new Handler(
$this->model_class,
$model_class,
$this->data['listing']
);
return $this;
@ -21,8 +23,9 @@ class BulkAction {
function apply() {
return call_user_func_array(
array($this->model_class, $this->data['action']),
array($this->listing, $this->data)
array($this->model_class, 'bulk'.ucfirst($this->action)),
array($this->listing->getSelection(), $this->data)
);
return $models->count();
}
}

View File

@ -4,16 +4,13 @@ namespace MailPoet\Listing;
if(!defined('ABSPATH')) exit;
class Handler {
private $data = array();
private $model = null;
function __construct($model_class, $data = array()) {
$class = new \ReflectionClass($model_class);
$this->table_name = $class->getStaticPropertyValue('_table');
$this->model = \Model::factory($model_class);
$this->model = $model_class::select('*');
$this->data = array(
// pagination
'offset' => (isset($data['offset']) ? (int)$data['offset'] : 0),
@ -31,7 +28,7 @@ class Handler {
'selection' => (isset($data['selection']) ? $data['selection'] : null)
);
$this->model = $this->setFilter();
$this->setFilter();
$this->setSearch();
$this->setGroup();
$this->setOrder();
@ -59,22 +56,18 @@ class Handler {
private function setFilter() {
if($this->data['filter'] === null) {
return $this->model;
return;
}
return $this->model->filter('filterBy', $this->data['filter']);
$this->model = $this->model->filter('filterBy', $this->data['filter']);
}
function getSelection() {
if(!empty($this->data['selection'])) {
$this->model->whereIn('id', $this->data['selection']);
$this->model->whereIn($this->table_name.'.id', $this->data['selection']);
}
return $this->model;
}
function count() {
return (int)$this->model->count();
}
function getSelectionIds() {
$models = $this->getSelection()
->select('id')
@ -86,14 +79,18 @@ class Handler {
}
function get() {
$count = $this->model->count();
$items = $this->model
->offset($this->data['offset'])
->limit($this->data['limit'])
->findArray();
return array(
'count' => $this->model->count(),
'count' => $count,
'filters' => $this->model->filter('filters'),
'groups' => $this->model->filter('groups'),
'items' => $this->model
->offset($this->data['offset'])
->limit($this->data['limit'])
->findArray()
'items' => $items
);
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace MailPoet\Listing;
if(!defined('ABSPATH')) exit;
class ItemAction {
private $model = null;
private $action = null;
private $data = null;
function __construct($model_class, $data) {
$id = (int)$data['id'];
unset($data['id']);
$this->action = $data['action'];
unset($data['action']);
$this->model = $model_class::findOne($id);
if(!empty($data)) {
$this->data = $data;
}
return $this;
}
function apply() {
if($this->data === null) {
return call_user_func_array(
array($this->model, $this->action),
array()
);
} else {
return call_user_func_array(
array($this->model, $this->action),
array($this->data)
);
}
}
}

112
lib/Models/Form.php Normal file
View File

@ -0,0 +1,112 @@
<?php
namespace MailPoet\Models;
if(!defined('ABSPATH')) exit;
class Form extends Model {
static $_table = MP_FORMS_TABLE;
function __construct() {
parent::__construct();
$this->addValidations('name', array(
'required' => __('You need to specify a name.')
));
}
static function search($orm, $search = '') {
return $orm->where_like('name', '%'.$search.'%');
}
static function groups() {
return array(
array(
'name' => 'all',
'label' => __('All'),
'count' => Form::whereNull('deleted_at')->count()
),
array(
'name' => 'trash',
'label' => __('Trash'),
'count' => Form::whereNotNull('deleted_at')->count()
)
);
}
static function groupBy($orm, $group = null) {
if($group === 'trash') {
return $orm->whereNotNull('deleted_at');
} else {
$orm = $orm->whereNull('deleted_at');
}
}
static function createOrUpdate($data = array()) {
$form = false;
if(isset($data['id']) && (int)$data['id'] > 0) {
$form = self::findOne((int)$data['id']);
}
if($form === false) {
$form = self::create();
$form->hydrate($data);
} else {
unset($data['id']);
$form->set($data);
}
$saved = $form->save();
if($saved === true) {
return true;
} else {
$errors = $form->getValidationErrors();
if(!empty($errors)) {
return $errors;
}
}
return false;
}
static function trash($listing, $data = array()) {
$confirm_delete = filter_var($data['confirm'], FILTER_VALIDATE_BOOLEAN);
if($confirm_delete) {
// delete relations with all segments
$forms = $listing->getSelection()->findResultSet();
if(!empty($forms)) {
$forms_count = 0;
foreach($forms as $form) {
if($form->delete()) {
$forms_count++;
}
}
return array(
'segments' => $forms_count
);
}
return false;
} else {
// soft delete
$forms = $listing->getSelection()
->findResultSet()
->set_expr('deleted_at', 'NOW()')
->save();
return array(
'segments' => $forms->count()
);
}
}
static function restore($listing, $data = array()) {
$forms = $listing->getSelection()
->findResultSet()
->set_expr('deleted_at', 'NULL')
->save();
return array(
'segments' => $forms->count()
);
}
}

View File

@ -9,6 +9,10 @@ class Model extends \Sudzy\ValidModel {
parent::__construct($customValidators->init());
}
static function create() {
return parent::create();
}
function save() {
$this->setTimestamp();
try {
@ -21,9 +25,57 @@ class Model extends \Sudzy\ValidModel {
}
}
function trash() {
return $this->set_expr('deleted_at', 'NOW()')->save();
}
static function bulkTrash($orm) {
$models = $orm->findResultSet();
$models->set_expr('deleted_at', 'NOW()')->save();
return $models->count();
}
static function bulkDelete($orm) {
$models = $orm->findMany();
$count = 0;
foreach($models as $model) {
$model->delete();
$count++;
}
return $count;
}
function restore() {
return $this->set_expr('deleted_at', 'NULl')->save();
}
static function bulkRestore($orm) {
$models = $orm->findResultSet();
$models->set_expr('deleted_at', 'NULL')->save();
return $models->count();
}
function duplicate($data = array()) {
$model = get_called_class();
$model_data = array_merge($this->asArray(), $data);
unset($model_data['id']);
$duplicate = $model::create();
$duplicate->hydrate($model_data);
$duplicate->set_expr('created_at', 'NOW()');
$duplicate->set_expr('updated_at', 'NOW()');
$duplicate->set_expr('deleted_at', 'NULL');
if($duplicate->save()) {
return $duplicate;
} else {
return false;
}
}
private function setTimestamp() {
if($this->created_at === null) {
$this->created_at = date('Y-m-d H:i:s');
$this->set_expr('created_at', 'NOW()');
}
}

View File

@ -39,10 +39,10 @@ class Newsletter extends Model {
'label' => __('All lists'),
'value' => ''
);
foreach($segments as $segment) {
$newsletters_count = $segment->newsletters()->count();
if($newsletters_count > 0) {
$segment_list[] = array(
'label' => sprintf('%s (%d)', $segment->name, $newsletters_count),
'value' => $segment->id()
@ -51,34 +51,21 @@ class Newsletter extends Model {
}
$filters = array(
array(
'name' => 'segment',
'options' => $segment_list
)
'segment' => $segment_list
);
return $filters;
}
static function filterBy($orm, $filters = null) {
if(empty($filters)) {
if(empty($filters)) {
return $orm;
}
foreach($filters as $filter) {
if($filter['name'] === 'segment') {
$segment = Segment::findOne($filter['value']);
foreach($filters as $key => $value) {
if($key === 'segment') {
$segment = Segment::findOne($value);
if($segment !== false) {
$orm = $orm
->select(MP_NEWSLETTERS_TABLE.'.*')
->select('newsletter_segment.id', 'newsletter_segment_id')
->join(
MP_NEWSLETTER_SEGMENT_TABLE,
MP_NEWSLETTERS_TABLE.'.id = newsletter_segment.newsletter_id',
'newsletter_segment'
)
->where('newsletter_segment.segment_id', (int)$filter['value']);
$orm = $segment->newsletters();
}
}
}
@ -112,12 +99,21 @@ class Newsletter extends Model {
array(
'name' => 'all',
'label' => __('All'),
'count' => Newsletter::count()
'count' => Newsletter::whereNull('deleted_at')->count()
),
array(
'name' => 'trash',
'label' => __('Trash'),
'count' => Newsletter::whereNotNull('deleted_at')->count()
)
);
}
static function group($orm, $group = null) {
static function groupBy($orm, $group = null) {
if($group === 'trash') {
return $orm->whereNotNull('deleted_at');
}
return $orm->whereNull('deleted_at');
}
static function createOrUpdate($data = array()) {
@ -147,9 +143,4 @@ class Newsletter extends Model {
}
return false;
}
static function trash($listing) {
return $listing->getSelection()
->deleteMany();
}
}

View File

@ -14,13 +14,10 @@ class Segment extends Model {
));
}
function subscribers() {
return $this->has_many_through(
__NAMESPACE__.'\Subscriber',
__NAMESPACE__.'\SubscriberSegment',
'segment_id',
'subscriber_id'
);
function delete() {
// delete all relations to subscribers
SubscriberSegment::where('segment_id', $this->id)->deleteMany();
parent::delete();
}
function newsletters() {
@ -41,12 +38,22 @@ class Segment extends Model {
array(
'name' => 'all',
'label' => __('All'),
'count' => Segment::count()
'count' => Segment::whereNull('deleted_at')->count()
),
array(
'name' => 'trash',
'label' => __('Trash'),
'count' => Segment::whereNotNull('deleted_at')->count()
)
);
}
static function group($orm, $group = null) {
static function groupBy($orm, $group = null) {
if($group === 'trash') {
return $orm->whereNotNull('deleted_at');
} else {
$orm = $orm->whereNull('deleted_at');
}
}
static function createOrUpdate($data = array()) {
@ -77,7 +84,28 @@ class Segment extends Model {
return false;
}
static function trash($listing) {
return $listing->getSelection()->deleteMany();
function duplicate($data = array()) {
$duplicate = parent::duplicate($data);
if($duplicate !== false) {
foreach($this->subscribers()->findResultSet() as $relation) {
$new_relation = SubscriberSegment::create();
$new_relation->set('subscriber_id', $relation->id);
$new_relation->set('segment_id', $duplicate->id);
$new_relation->save();
}
return $duplicate;
}
return false;
}
function subscribers() {
return $this->has_many_through(
__NAMESPACE__.'\Subscriber',
__NAMESPACE__.'\SubscriberSegment',
'segment_id',
'subscriber_id'
);
}
}

View File

@ -19,7 +19,7 @@ class Subscriber extends Model {
// delete all relations to segments
SubscriberSegment::where('subscriber_id', $this->id)->deleteMany();
parent::delete();
return parent::delete();
}
static function search($orm, $search = '') {
@ -36,7 +36,6 @@ class Subscriber extends Model {
static function filters() {
$segments = Segment::orderByAsc('name')->findMany();
$segment_list = array();
$segment_list[] = array(
'label' => __('All lists'),
'value' => ''
@ -53,10 +52,7 @@ class Subscriber extends Model {
}
$filters = array(
array(
'name' => 'segment',
'options' => $segment_list
)
'segment' => $segment_list
);
return $filters;
@ -66,12 +62,11 @@ class Subscriber extends Model {
if(empty($filters)) {
return $orm;
}
foreach($filters as $filter) {
if($filter['name'] === 'segment') {
$segment = Segment::findOne($filter['value']);
foreach($filters as $key => $value) {
if($key === 'segment') {
$segment = Segment::findOne($value);
if($segment !== false) {
$orm = $segment->subscribers();
return $segment->subscribers();
}
}
}
@ -194,13 +189,11 @@ class Subscriber extends Model {
return false;
}
static function moveToList($listing, $data = array()) {
static function bulkMoveToList($orm, $data = array()) {
$segment_id = (isset($data['segment_id']) ? (int)$data['segment_id'] : 0);
$segment = Segment::findOne($segment_id);
if($segment !== false) {
$subscribers_count = 0;
$subscribers = $listing->getSelection()->findMany();
$subscribers = $orm->findResultSet();
foreach($subscribers as $subscriber) {
// remove subscriber from all segments
SubscriberSegment::where('subscriber_id', $subscriber->id)->deleteMany();
@ -210,37 +203,37 @@ class Subscriber extends Model {
$association->subscriber_id = $subscriber->id;
$association->segment_id = $segment->id;
$association->save();
$subscribers_count++;
}
return array(
'subscribers' => $subscribers_count,
'subscribers' => $subscribers->count(),
'segment' => $segment->name
);
}
return false;
}
static function removeFromList($listing, $data = array()) {
static function bulkRemoveFromList($orm, $data = array()) {
$segment_id = (isset($data['segment_id']) ? (int)$data['segment_id'] : 0);
$segment = Segment::findOne($segment_id);
if($segment !== false) {
// delete relations with segment
$subscriber_ids = $listing->getSelectionIds();
SubscriberSegment::whereIn('subscriber_id', $subscriber_ids)
->where('segment_id', $segment->id)
->deleteMany();
$subscribers = $orm->findResultSet();
foreach($subscribers as $subscriber) {
SubscriberSegment::where('subscriber_id', $subscriber->id)
->where('segment_id', $segment->id)
->deleteMany();
}
return array(
'subscribers' => count($subscriber_ids),
'subscribers' => $subscribers->count(),
'segment' => $segment->name
);
}
return false;
}
static function removeFromAllLists($listing) {
static function bulkRemoveFromAllLists($orm) {
$segments = Segment::findMany();
$segment_ids = array_map(function($segment) {
return $segment->id();
@ -248,62 +241,48 @@ class Subscriber extends Model {
if(!empty($segment_ids)) {
// delete relations with segment
$subscriber_ids = $listing->getSelectionIds();
SubscriberSegment::whereIn('subscriber_id', $subscriber_ids)
->whereIn('segment_id', $segment_ids)
->deleteMany();
return array(
'subscribers' => count($subscriber_ids)
);
}
return false;
}
static function confirmUnconfirmed($listing) {
$subscriber_ids = $listing->getSelectionIds();
$subscribers = Subscriber::whereIn('id', $subscriber_ids)
->where('status', 'unconfirmed')
->findMany();
if(!empty($subscribers)) {
$subscribers_count = 0;
$subscribers = $orm->findResultSet();
foreach($subscribers as $subscriber) {
$subscriber->set('status', 'subscribed');
if($subscriber->save() === true) {
$subscribers_count++;
}
SubscriberSegment::where('subscriber_id', $subscriber->id)
->whereIn('segment_id', $segment_ids)
->deleteMany();
}
return array(
'subscribers' => $subscribers_count
);
return $subscribers->count();
}
return false;
}
static function resendConfirmationEmail($listing) {
$subscriber_ids = $listing->getSelectionIds();
$subscribers = Subscriber::whereIn('id', $subscriber_ids)
static function bulkConfirmUnconfirmed($orm) {
$subscribers = $orm->findResultSet();
$subscribers->set('status', 'subscribed')->save();
return $subscribers->count();
}
static function bulkResendConfirmationEmail($orm) {
$subscribers = $orm
->where('status', 'unconfirmed')
->findMany();
->findResultSet();
if(!empty($subscribers)) {
foreach($subscribers as $subscriber) {
// TODO: resend confirmation email
// TODO: send confirmation email
// $subscriber->sendConfirmationEmail()
}
return true;
return $subscribers->count();
}
return false;
}
static function addToList($listing, $data = array()) {
static function bulkAddToList($orm, $data = array()) {
$segment_id = (isset($data['segment_id']) ? (int)$data['segment_id'] : 0);
$segment = Segment::findOne($segment_id);
if($segment !== false) {
$subscribers_count = 0;
$subscribers = $listing->getSelection()->findMany();
$subscribers = $orm->findMany();
foreach($subscribers as $subscriber) {
// create relation with segment
$association = \MailPoet\Models\SubscriberSegment::create();
@ -320,46 +299,4 @@ class Subscriber extends Model {
}
return false;
}
static function trash($listing, $data = array()) {
$confirm_delete = filter_var($data['confirm'], FILTER_VALIDATE_BOOLEAN);
if($confirm_delete) {
// delete relations with all segments
$subscribers = $listing->getSelection()->findResultSet();
if(!empty($subscribers)) {
$subscribers_count = 0;
foreach($subscribers as $subscriber) {
if($subscriber->delete()) {
$subscribers_count++;
}
}
return array(
'subscribers' => $subscribers_count
);
}
return false;
} else {
// soft delete
$subscribers = $listing->getSelection()
->findResultSet()
->set_expr('deleted_at', 'NOW()')
->save();
return array(
'subscribers' => $subscribers->count()
);
}
}
static function restore($listing, $data = array()) {
$subscribers = $listing->getSelection()
->findResultSet()
->set_expr('deleted_at', 'NULL')
->save();
return array(
'subscribers' => $subscribers->count()
);
}
}

95
lib/Router/Forms.php Normal file
View File

@ -0,0 +1,95 @@
<?php
namespace MailPoet\Router;
use \MailPoet\Models\Form;
use \MailPoet\Listing;
if(!defined('ABSPATH')) exit;
class Forms {
function __construct() {
}
function get($data = array()) {
$id = (isset($data['id']) ? (int)$data['id'] : 0);
$form = Form::findOne($id);
if($form === false) {
wp_send_json(false);
} else {
wp_send_json($form->asArray());
}
}
function listing($data = array()) {
$listing = new Listing\Handler(
'\MailPoet\Models\Form',
$data
);
$listing_data = $listing->get();
wp_send_json($listing_data);
}
function getAll() {
$collection = Form::findArray();
wp_send_json($collection);
}
function save($data = array()) {
$result = Form::createOrUpdate($data);
if($result !== true) {
wp_send_json($result);
} else {
wp_send_json(true);
}
}
function restore($id) {
$form = Form::findOne($id);
if($form !== false) {
$form->set_expr('deleted_at', 'NULL');
$result = $form->save();
} else {
$result = false;
}
wp_send_json($result);
}
function delete($data = array()) {
$form = Form::findOne($data['id']);
$confirm_delete = filter_var($data['confirm'], FILTER_VALIDATE_BOOLEAN);
if($form !== false) {
if($confirm_delete) {
$form->delete();
$result = true;
} else {
$form->set_expr('deleted_at', 'NOW()');
$result = $form->save();
}
} else {
$result = false;
}
wp_send_json($result);
}
function duplicate($id) {
$result = false;
$form = Form::duplicate($id);
if($form !== false) {
$result = $form;
}
wp_send_json($result);
}
function bulk_action($data = array()) {
$bulk_action = new Listing\BulkAction(
'\MailPoet\Models\Form',
$data
);
wp_send_json($bulk_action->apply());
}
}

View File

@ -88,10 +88,28 @@ class Newsletters {
wp_send_json(($newsletter_id !== false));
}
function delete($id) {
function delete($data = array()) {
$newsletter = newsletter::findOne($data['id']);
$confirm_delete = filter_var($data['confirm'], FILTER_VALIDATE_BOOLEAN);
if($newsletter !== false) {
if($confirm_delete) {
$newsletter->delete();
$result = array('newsletters' => 1);
} else {
$newsletter->set_expr('deleted_at', 'NOW()');
$result = array('newsletters' => (int)$newsletter->save());
}
} else {
$result = false;
}
wp_send_json($result);
}
function restore($id) {
$newsletter = Newsletter::findOne($id);
if($newsletter !== false) {
$result = $newsletter->delete();
$newsletter->set_expr('deleted_at', 'NULL');
$result = array('newsletters' => (int)$newsletter->save());
} else {
$result = false;
}

View File

@ -1,6 +1,7 @@
<?php
namespace MailPoet\Router;
use \MailPoet\Models\Segment;
use \MailPoet\Models\SubscriberSegment;
use \MailPoet\Listing;
if(!defined('ABSPATH')) exit;
@ -25,11 +26,47 @@ class Segments {
'\MailPoet\Models\Segment',
$data
);
wp_send_json($listing->get());
$listing_data = $listing->get();
// fetch segments relations for each returned item
foreach($listing_data['items'] as &$item) {
$stats = SubscriberSegment::table_alias('relation')
->where(
'relation.segment_id',
$item['id']
)
->join(
MP_SUBSCRIBERS_TABLE,
'subscribers.id = relation.subscriber_id',
'subscribers'
)
->select_expr(
'SUM(CASE status WHEN "subscribed" THEN 1 ELSE 0 END)',
'subscribed'
)
->select_expr(
'SUM(CASE status WHEN "unsubscribed" THEN 1 ELSE 0 END)',
'unsubscribed'
)
->select_expr(
'SUM(CASE status WHEN "unconfirmed" THEN 1 ELSE 0 END)',
'unconfirmed'
)
->findOne()->asArray();
$item = array_merge($item, $stats);
$item['subscribers_url'] = admin_url(
'admin.php?page=mailpoet-subscribers#/filter[segment='.$item['id'].']'
);
}
wp_send_json($listing_data);
}
function getAll() {
$collection = Segment::find_array();
$collection = Segment::findArray();
wp_send_json($collection);
}
@ -43,17 +80,63 @@ class Segments {
}
}
function delete($id) {
function restore($id) {
$result = false;
$segment = Segment::findOne($id);
if($segment !== false) {
$result = $segment->delete();
} else {
$result = false;
$result = $segment->restore();
}
wp_send_json($result);
}
function trash($id) {
$result = false;
$segment = Segment::findOne($id);
if($segment !== false) {
$result = $segment->trash();
}
wp_send_json($result);
}
function delete($id) {
$result = false;
$segment = Segment::findOne($id);
if($segment !== false) {
$segment->delete();
$result = 1;
}
wp_send_json($result);
}
function duplicate($id) {
$result = false;
$segment = Segment::findOne($id);
if($segment !== false) {
$data = array(
'name' => sprintf(__('Copy of %s'), $segment->name)
);
$result = $segment->duplicate($data)->asArray();
}
wp_send_json($result);
}
function item_action($data = array()) {
$item_action = new Listing\ItemAction(
'\MailPoet\Models\Segment',
$data
);
wp_send_json($item_action->apply());
}
function bulk_action($data = array()) {
$bulk_action = new Listing\BulkAction(
'\MailPoet\Models\Segment',

View File

@ -32,12 +32,18 @@ class Subscribers {
// fetch segments relations for each returned item
foreach($listing_data['items'] as &$item) {
$segments = SubscriberSegment::select('segment_id')
// avatar
$item['avatar_url'] = get_avatar_url($item['email'], array(
'size' => 32
));
// subscriber's segments
$relations = SubscriberSegment::select('segment_id')
->where('subscriber_id', $item['id'])
->findMany();
$item['segments'] = array_map(function($relation) {
return $relation->segment_id;
}, $segments);
}, $relations);
}
wp_send_json($listing_data);
@ -54,33 +60,48 @@ class Subscribers {
}
function restore($id) {
$result = false;
$subscriber = Subscriber::findOne($id);
if($subscriber !== false) {
$subscriber->set_expr('deleted_at', 'NULL');
$result = array('subscribers' => (int)$subscriber->save());
} else {
$result = false;
$result = $subscriber->restore();
}
wp_send_json($result);
}
function delete($data = array()) {
$subscriber = Subscriber::findOne($data['id']);
$confirm_delete = filter_var($data['confirm'], FILTER_VALIDATE_BOOLEAN);
function trash($id) {
$result = false;
$subscriber = Subscriber::findOne($id);
if($subscriber !== false) {
if($confirm_delete) {
$subscriber->delete();
$result = array('subscribers' => 1);
} else {
$subscriber->set_expr('deleted_at', 'NOW()');
$result = array('subscribers' => (int)$subscriber->save());
}
} else {
$result = false;
$result = $subscriber->trash();
}
wp_send_json($result);
}
function delete($id) {
$result = false;
$subscriber = Subscriber::findOne($id);
if($subscriber !== false) {
$subscriber->delete();
$result = 1;
}
wp_send_json($result);
}
function item_action($data = array()) {
$item_action = new Listing\ItemAction(
'\MailPoet\Models\Segment',
$data
);
wp_send_json($item_action->apply());
}
function bulk_action($data = array()) {
$bulk_action = new Listing\BulkAction(
'\MailPoet\Models\Subscriber',

View File

@ -1,24 +1,17 @@
<?php
namespace MailPoet\Util;
use \phpseclib\Crypt\RSA;
class DKIM {
static function generateKeys() {
try {
$certificate = openssl_pkey_new(array('private_bits' => 1024));
$rsa = new RSA();
$rsa_keys = $rsa->createKey();
$keys = array('public' => '', 'private' => '');
// get private key
openssl_pkey_export($certificate, $keys['private']);
// get public key
$public = openssl_pkey_get_details($certificate);
// trim keys by removing BEGIN/END lines
$keys['public'] = self::trimKey($public['key']);
$keys['private'] = self::trimKey($keys['private']);
return $keys;
return array(
'public' => self::trimKey($rsa_keys['publickey']),
'private' => self::trimKey($rsa_keys['privatekey'])
);
} catch(Exception $e) {
return false;
}

View File

@ -36,9 +36,9 @@
"underscore": "1.8.3"
},
"devDependencies": {
"export-loader": "webpack/exports-loader.git",
"import-loader": "webpack/imports-loader.git",
"expose-loader": "webpack/expose-loader.git",
"expose-loader": "latest",
"exports-loader": "latest",
"imports-loader": "latest",
"babel-core": "^5.8.22",
"babel-loader": "^5.3.2",
"amd-inject-loader": "latest",

View File

@ -18,7 +18,11 @@ $models = array(
'SubscriberSegment'
);
$destroy = function ($model) {
Model::factory('\MailPoet\Models\\' . $model)
->deleteMany();
$class = new \ReflectionClass('\MailPoet\Models\\' . $model);
$table = $class->getStaticPropertyValue('_table');
$db = ORM::getDb();
$db->beginTransaction();
$db->exec('TRUNCATE '.$table);
$db->commit();
};
array_map($destroy, $models);

16
views/forms.html Normal file
View File

@ -0,0 +1,16 @@
<% extends 'layout.html' %>
<% block content %>
<div id="forms_container"></div>
<%= localize({
'pageTitle': __('Forms'),
'searchLabel': __('Search'),
'loadingItems': __('Loading forms...'),
'noItemsFound': __('No forms found.')
}) %>
<script type="text/javascript">
var mailpoet_segments = <%= json_encode(segments) %>;
</script>
<% endblock %>

View File

@ -14,8 +14,12 @@
<div class="mailpoet_form_field">
<div class="mailpoet_form_field_title"><%= __('Categories & tags:') %></div>
<div class="mailpoet_form_field_input_option">
<input type="hidden" class="mailpoet_input mailpoet_automated_latest_content_categories_and_tags" value="{{json_encode terms}}" data-selected="{{json_encode terms}}" />
<div class="mailpoet_form_field_select_option">
<select class="mailpoet_select mailpoet_automated_latest_content_categories_and_tags" multiple="multiple">
{{#each terms}}
<option value="{{ id }}" selected="selected">{{ text }}</option>
{{/each}}
</select>
</div>
<div class="mailpoet_form_field_radio_option">
<label>

View File

@ -14,7 +14,11 @@
<option value="private"><%= __('Private') %></option>
</select></div>
<div class="mailpoet_post_selection_filter_row">
<input type="hidden" class="mailpoet_input mailpoet_posts_categories_and_tags" value="{{json_encode terms}}" data-selected="{{json_encode terms}}" data-placeholder="<%= __('Filter by category or tag') %>" />
<select class="mailpoet_select mailpoet_posts_categories_and_tags" multiple="multiple">
{{#each terms}}
<option value="{{ id }}" selected="selected">{{ text }}</option>
{{/each}}
</select>
</div>
</div>
<div class="mailpoet_post_selection_container">

View File

@ -21,7 +21,7 @@ baseConfig = {
'backbone.supermodel$': 'backbone.supermodel/build/backbone.supermodel.js',
'sticky-kit': 'sticky-kit/jquery.sticky-kit',
'interact$': 'interact.js/interact.js',
'spectrum$': 'spectrum-colorpicker/spectrum.js',
'spectrum$': 'spectrum-colorpicker/spectrum.js'
},
},
node: {
@ -69,6 +69,7 @@ config.push(_.extend({}, baseConfig, {
'subscribers/subscribers.jsx',
'newsletters/newsletters.jsx',
'segments/segments.jsx',
'forms/forms.jsx',
'settings/tabs.js'
],
newsletter_editor: [