Listings
- listing component - subscribers listing - newsletters listing
This commit is contained in:
@@ -6,4 +6,4 @@
|
||||
right: 0.5em
|
||||
top: 0.5em
|
||||
color: #999
|
||||
text-decoration: none
|
||||
text-decoration: none !important
|
||||
|
@@ -31,6 +31,10 @@ define('bulk_actions', ['react'], function(React) {
|
||||
return null;
|
||||
},
|
||||
render: function() {
|
||||
if(this.props.actions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="alignleft actions bulkactions">
|
||||
<label
|
||||
|
10
assets/js/src/listing/filters.jsx
Normal file
10
assets/js/src/listing/filters.jsx
Normal file
@@ -0,0 +1,10 @@
|
||||
define('filters', ['react'], function(React) {
|
||||
|
||||
var ListingFilters = React.createClass({
|
||||
render: function() {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
return ListingFilters;
|
||||
});
|
36
assets/js/src/listing/groups.jsx
Normal file
36
assets/js/src/listing/groups.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
define('groups', ['react', 'classnames'], function(React, classNames) {
|
||||
var ListingGroups = React.createClass({
|
||||
handleSelect: function(group) {
|
||||
return this.props.onSelect(group);
|
||||
},
|
||||
render: function() {
|
||||
var count = this.props.groups.length;
|
||||
var groups = this.props.groups.map(function(group, index) {
|
||||
|
||||
var classes = classNames(
|
||||
{ 'current' : (group.name === this.props.selected) }
|
||||
);
|
||||
|
||||
return (
|
||||
<li key={index}>
|
||||
<a
|
||||
href="javascript:;"
|
||||
className={classes}
|
||||
onClick={this.handleSelect.bind(this, group.name)} >
|
||||
{group.label}
|
||||
<span className="count">({ group.count })</span>
|
||||
</a>{(index < (count - 1)) ? ' | ' : ''}
|
||||
</li>
|
||||
);
|
||||
}.bind(this));
|
||||
|
||||
return (
|
||||
<ul className="subsubsub">
|
||||
{ groups }
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return ListingGroups;
|
||||
});
|
@@ -19,14 +19,15 @@ define('header', ['react', 'classnames'], function(React, classNames) {
|
||||
);
|
||||
},
|
||||
render: function() {
|
||||
var columns = this.props.columns.map(function(column) {
|
||||
return (
|
||||
<ListingColumn
|
||||
onSort={this.props.onSort}
|
||||
sort_by={this.props.sort_by}
|
||||
key={column.name}
|
||||
column={column} />
|
||||
);
|
||||
var columns = this.props.columns.map(function(column, index) {
|
||||
column.is_primary = (index === 0);
|
||||
return (
|
||||
<ListingColumn
|
||||
onSort={this.props.onSort}
|
||||
sort_by={this.props.sort_by}
|
||||
key={ 'column-' + index }
|
||||
column={column} />
|
||||
);
|
||||
}.bind(this));
|
||||
|
||||
return (
|
||||
@@ -55,11 +56,11 @@ define('header', ['react', 'classnames'], function(React, classNames) {
|
||||
render: function() {
|
||||
var classes = classNames(
|
||||
'manage-column',
|
||||
{ 'column-primary': this.props.column.is_primary },
|
||||
{ 'sortable': this.props.column.sortable },
|
||||
this.props.column.sorted,
|
||||
{ 'sorted': (this.props.sort_by === this.props.column.name) }
|
||||
);
|
||||
|
||||
var label;
|
||||
|
||||
if(this.props.column.sortable === true) {
|
||||
|
262
assets/js/src/listing/listing.jsx
Normal file
262
assets/js/src/listing/listing.jsx
Normal file
@@ -0,0 +1,262 @@
|
||||
define(
|
||||
'listing',
|
||||
[
|
||||
'mailpoet',
|
||||
'jquery',
|
||||
'react',
|
||||
'classnames',
|
||||
'listing/bulk_actions.jsx',
|
||||
'listing/header.jsx',
|
||||
'listing/pages.jsx',
|
||||
'listing/search.jsx',
|
||||
'listing/groups.jsx',
|
||||
'listing/filters.jsx'
|
||||
],
|
||||
function(
|
||||
MailPoet,
|
||||
jQuery,
|
||||
React,
|
||||
classNames,
|
||||
ListingBulkActions,
|
||||
ListingHeader,
|
||||
ListingPages,
|
||||
ListingSearch,
|
||||
ListingGroups,
|
||||
ListingFilters
|
||||
) {
|
||||
var ListingItem = React.createClass({
|
||||
handleSelect: function(e) {
|
||||
var is_checked = jQuery(e.target).is(':checked');
|
||||
|
||||
this.props.onSelect(
|
||||
parseInt(e.target.value, 10),
|
||||
is_checked
|
||||
);
|
||||
|
||||
return !e.target.checked;
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<tr>
|
||||
<th className="check-column" scope="row">
|
||||
<label className="screen-reader-text">
|
||||
{ 'Select ' + this.props.item.email }</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
defaultValue={ this.props.item.id }
|
||||
defaultChecked={ this.props.item.selected }
|
||||
onChange={ this.handleSelect } />
|
||||
</th>
|
||||
{ this.props.onRenderItem(this.props.item) }
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var ListingItems = React.createClass({
|
||||
render: function() {
|
||||
if(this.props.items.length === 0) {
|
||||
return (
|
||||
<tbody>
|
||||
<td
|
||||
colSpan={this.props.columns.length + 1}
|
||||
className="colspanchange">
|
||||
{
|
||||
(this.props.loading === true)
|
||||
? MailPoetI18n.loading
|
||||
: MailPoetI18n.noRecordFound
|
||||
}
|
||||
</td>
|
||||
</tbody>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<tbody>
|
||||
{this.props.items.map(function(item) {
|
||||
item.selected = (this.props.selected.indexOf(item.id) !== -1);
|
||||
return (
|
||||
<ListingItem
|
||||
columns={ this.props.columns }
|
||||
onSelect={ this.props.onSelect }
|
||||
onRenderItem={ this.props.onRenderItem }
|
||||
key={ 'item-' + item.id }
|
||||
item={ item } />
|
||||
);
|
||||
}.bind(this))}
|
||||
</tbody>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var Listing = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {
|
||||
loading: false,
|
||||
search: '',
|
||||
page: 1,
|
||||
count: 0,
|
||||
limit: 10,
|
||||
sort_by: 'id',
|
||||
sort_order: 'desc',
|
||||
items: [],
|
||||
groups: [],
|
||||
group: 'all',
|
||||
filters: [],
|
||||
selected: []
|
||||
};
|
||||
},
|
||||
componentDidMount: function() {
|
||||
this.getItems();
|
||||
},
|
||||
getItems: function() {
|
||||
this.setState({ loading: true });
|
||||
this.props.items.bind(null, this)();
|
||||
},
|
||||
handleSearch: function(search) {
|
||||
this.setState({
|
||||
search: search
|
||||
}, function() {
|
||||
this.getItems();
|
||||
}.bind(this));
|
||||
},
|
||||
handleSort: function(sort_by, sort_order = 'asc') {
|
||||
this.setState({
|
||||
sort_by: sort_by,
|
||||
sort_order: sort_order
|
||||
}, function() {
|
||||
this.getItems();
|
||||
}.bind(this));
|
||||
},
|
||||
handleSelect: function(id, is_checked) {
|
||||
var selected = this.state.selected;
|
||||
|
||||
if(is_checked) {
|
||||
selected = jQuery.merge(selected, [ id ]);
|
||||
} else {
|
||||
selected.splice(selected.indexOf(id), 1);
|
||||
}
|
||||
this.setState({
|
||||
selected: selected
|
||||
});
|
||||
},
|
||||
handleGroup: function(group) {
|
||||
// reset search
|
||||
jQuery('#search_input').val('');
|
||||
|
||||
this.setState({
|
||||
group: group,
|
||||
filters: [],
|
||||
selected: [],
|
||||
search: '',
|
||||
page: 1
|
||||
}, function() {
|
||||
this.getItems();
|
||||
}.bind(this));
|
||||
},
|
||||
handleSelectAll: function(is_checked) {
|
||||
if(is_checked === false) {
|
||||
this.setState({ selected: [] });
|
||||
} else {
|
||||
var selected = this.state.items.map(function(item) {
|
||||
return ~~item.id;
|
||||
});
|
||||
|
||||
this.setState({
|
||||
selected: selected
|
||||
});
|
||||
}
|
||||
},
|
||||
handleSetPage: function(page) {
|
||||
this.setState({ page: page }, function() {
|
||||
this.getItems();
|
||||
}.bind(this));
|
||||
},
|
||||
handleRenderItem: function(item) {
|
||||
return this.props.onRenderItem(item);
|
||||
},
|
||||
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;
|
||||
});
|
||||
|
||||
var tableClasses = classNames(
|
||||
'wp-list-table',
|
||||
'widefat',
|
||||
'fixed',
|
||||
'striped',
|
||||
{ 'mailpoet_listing_loading': this.state.loading }
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<ListingGroups
|
||||
groups={ this.state.groups }
|
||||
selected={ this.state.group }
|
||||
onSelect={ this.handleGroup } />
|
||||
<ListingSearch
|
||||
onSearch={ this.handleSearch }
|
||||
search={ this.state.search } />
|
||||
<div className="tablenav top clearfix">
|
||||
<ListingBulkActions
|
||||
actions={ this.props.actions }
|
||||
selected={ this.state.selected } />
|
||||
<ListingFilters filters={ this.state.filters } />
|
||||
<ListingPages
|
||||
count={ this.state.count }
|
||||
page={ this.state.page }
|
||||
limit={ this.state.limit }
|
||||
onSetPage={ this.handleSetPage } />
|
||||
</div>
|
||||
<table className={ tableClasses }>
|
||||
<thead>
|
||||
<ListingHeader
|
||||
onSort={ this.handleSort }
|
||||
onSelectAll={ this.handleSelectAll }
|
||||
sort_by={ this.state.sort_by }
|
||||
sort_order={ this.state.sort_order }
|
||||
columns={ this.props.columns } />
|
||||
</thead>
|
||||
|
||||
<ListingItems
|
||||
onRenderItem={ this.handleRenderItem }
|
||||
columns={ this.props.columns }
|
||||
selected={ this.state.selected }
|
||||
onSelect={ this.handleSelect }
|
||||
loading= { this.state.loading }
|
||||
items={ items } />
|
||||
|
||||
<tfoot>
|
||||
<ListingHeader
|
||||
onSort={ this.handleSort }
|
||||
onSelectAll={ this.handleSelectAll }
|
||||
sort_by={ this.state.sort_by }
|
||||
sort_order={ this.state.sort_order }
|
||||
columns={ this.props.columns } />
|
||||
</tfoot>
|
||||
|
||||
</table>
|
||||
<div className="tablenav bottom">
|
||||
<ListingBulkActions
|
||||
actions={ this.props.actions }
|
||||
selected={ this.state.selected } />
|
||||
<ListingPages
|
||||
count={ this.state.count }
|
||||
page={ this.state.page }
|
||||
limit={ this.state.limit }
|
||||
onSetPage={ this.handleSetPage } />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return Listing;
|
||||
}
|
||||
);
|
@@ -20,11 +20,13 @@ define('search', ['react'], function(React) {
|
||||
</label>
|
||||
<input
|
||||
type="search"
|
||||
id="search_input"
|
||||
ref="search"
|
||||
name="s"
|
||||
defaultValue={this.props.search} />
|
||||
<input
|
||||
type="submit"
|
||||
defaultValue="Search"
|
||||
defaultValue={MailPoetI18n.searchLabel}
|
||||
className="button" />
|
||||
</p>
|
||||
</form>
|
||||
|
@@ -3,78 +3,99 @@ define(
|
||||
[
|
||||
'react',
|
||||
'jquery',
|
||||
'mailpoet'
|
||||
'mailpoet',
|
||||
'listing/listing.jsx',
|
||||
'classnames'
|
||||
],
|
||||
function(
|
||||
React,
|
||||
jQuery,
|
||||
MailPoet
|
||||
MailPoet,
|
||||
Listing,
|
||||
classNames
|
||||
) {
|
||||
|
||||
var Newsletter = React.createClass({
|
||||
send: function(e) {
|
||||
e.preventDefault();
|
||||
MailPoet.Ajax.post({
|
||||
endpoint: 'newsletters',
|
||||
action: 'send',
|
||||
data: this.props.newsletter.id,
|
||||
onSuccess: function(response) {
|
||||
alert('Sent!');
|
||||
},
|
||||
onError: function(response) {
|
||||
alert('Cannot send. Set the settings and add some subscribers!');
|
||||
}
|
||||
})
|
||||
var columns = [
|
||||
{
|
||||
name: 'subject',
|
||||
label: 'Subject',
|
||||
sortable: true
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div className="newsletter">
|
||||
<p className="subject">
|
||||
{this.props.newsletter.subject} - <a href="" onClick={this.send}>
|
||||
Send
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
{
|
||||
name: 'created_at',
|
||||
label: 'Created on',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'updated_at',
|
||||
label: 'Last modified on',
|
||||
sortable: true
|
||||
}
|
||||
});
|
||||
];
|
||||
|
||||
var actions = [
|
||||
];
|
||||
|
||||
var List = React.createClass({
|
||||
load: function() {
|
||||
getItems: function(listing) {
|
||||
MailPoet.Ajax.post({
|
||||
endpoint: 'newsletters',
|
||||
action: 'get',
|
||||
data: {},
|
||||
data: {
|
||||
offset: (listing.state.page - 1) * listing.state.limit,
|
||||
limit: listing.state.limit,
|
||||
group: listing.state.group,
|
||||
search: listing.state.search,
|
||||
sort_by: listing.state.sort_by,
|
||||
sort_order: listing.state.sort_order
|
||||
},
|
||||
onSuccess: function(response) {
|
||||
this.setState({data: response});
|
||||
}.bind(this)
|
||||
if(listing.isMounted()) {
|
||||
listing.setState({
|
||||
items: response.items || [],
|
||||
filters: response.filters || [],
|
||||
groups: response.groups || [],
|
||||
count: response.count || 0,
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
}.bind(listing)
|
||||
});
|
||||
},
|
||||
renderItem: function(newsletter) {
|
||||
var rowClasses = classNames(
|
||||
'manage-column',
|
||||
'column-primary',
|
||||
'has-row-actions'
|
||||
);
|
||||
|
||||
getInitialState: function() {
|
||||
return {data: []};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.load();
|
||||
setInterval(this.load, 2000);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var nodes = this.state.data.map(function (newsletter) {
|
||||
return (
|
||||
<Newsletter key={newsletter.id} newsletter={newsletter} />
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div className="newslettersList">
|
||||
<h1>Newsletters</h1>
|
||||
{nodes}
|
||||
<div>
|
||||
<td className={ rowClasses }>
|
||||
<strong>
|
||||
<a>{ newsletter.subject }</a>
|
||||
</strong>
|
||||
</td>
|
||||
<td className="column-date" data-colname="Subscribed on">
|
||||
<abbr>{ newsletter.created_at }</abbr>
|
||||
</td>
|
||||
<td className="column-date" data-colname="Last modified on">
|
||||
<abbr>{ newsletter.updated_at }</abbr>
|
||||
</td>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<Listing
|
||||
onRenderItem={this.renderItem}
|
||||
items={this.getItems}
|
||||
columns={columns}
|
||||
actions={actions} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return List;
|
||||
});
|
||||
}
|
||||
);
|
@@ -22,16 +22,15 @@ define(
|
||||
render: function() {
|
||||
return (
|
||||
<div>
|
||||
<header>
|
||||
<ul>
|
||||
<li>
|
||||
<Link to="list">Newsletters</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="form">New</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
<h1>
|
||||
{ MailPoetI18n.pageTitle }
|
||||
<span>
|
||||
<Link className="add-new-h2" to="list">Newsletters</Link>
|
||||
</span>
|
||||
<span>
|
||||
<Link className="add-new-h2" to="form">New newsletter</Link>
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<RouteHandler/>
|
||||
</div>
|
||||
@@ -49,8 +48,11 @@ define(
|
||||
|
||||
var hook = document.getElementById('newsletters');
|
||||
if (hook) {
|
||||
Router.run(routes, function(Handler) {
|
||||
React.render(<Handler/>, hook);
|
||||
Router.run(routes, function(Handler, state) {
|
||||
React.render(
|
||||
<Handler params={state.params} query={state.query} />,
|
||||
hook
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@@ -1,69 +0,0 @@
|
||||
define('subscribers', ['react', 'jquery', 'mailpoet'], function(React, jQuery, MailPoet) {
|
||||
|
||||
var data = [
|
||||
{
|
||||
first_name: "John",
|
||||
last_name: "Mailer",
|
||||
email: 'john@mailpoet.com'
|
||||
},
|
||||
{
|
||||
first_name: "Mark",
|
||||
last_name: "Trailer",
|
||||
email: 'mark@mailpoet.com'
|
||||
}
|
||||
];
|
||||
|
||||
var Subscriber = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<div className="subscriber">
|
||||
<h3 className="name">
|
||||
{this.props.subscriber.first_name} {this.props.subscriber.last_name}
|
||||
</h3>
|
||||
{this.props.subscriber.email}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var SubscribersList = React.createClass({
|
||||
load: function() {
|
||||
MailPoet.Ajax.post({
|
||||
endpoint: 'subscribers',
|
||||
action: 'get',
|
||||
data: {},
|
||||
onSuccess: function(response) {
|
||||
this.setState({data: response});
|
||||
}.bind(this)
|
||||
});
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {data: []};
|
||||
},
|
||||
componentDidMount: function() {
|
||||
this.load();
|
||||
setInterval(this.load, this.props.pollInterval);
|
||||
},
|
||||
render: function() {
|
||||
var nodes = this.state.data.map(function (subscriber) {
|
||||
return (
|
||||
<Subscriber key={subscriber.id} subscriber={subscriber} />
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div className="subscribersList">
|
||||
{nodes}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var element = jQuery('#mailpoet_subscribers');
|
||||
|
||||
if(element.length > 0) {
|
||||
React.render(
|
||||
<SubscribersList data={data} pollInterval={2000} />,
|
||||
element[0]
|
||||
);
|
||||
}
|
||||
});
|
179
assets/js/src/subscribers/list.jsx
Normal file
179
assets/js/src/subscribers/list.jsx
Normal file
@@ -0,0 +1,179 @@
|
||||
define(
|
||||
'list',
|
||||
[
|
||||
'react',
|
||||
'jquery',
|
||||
'mailpoet',
|
||||
'listing/listing.jsx',
|
||||
'classnames'
|
||||
],
|
||||
function(
|
||||
React,
|
||||
jQuery,
|
||||
MailPoet,
|
||||
Listing,
|
||||
classNames
|
||||
) {
|
||||
|
||||
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: 'created_at',
|
||||
label: 'Subscribed on',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'updated_at',
|
||||
label: 'Last modified on',
|
||||
sortable: true
|
||||
},
|
||||
];
|
||||
|
||||
var actions = [
|
||||
{
|
||||
name: 'move',
|
||||
label: 'Move to list...',
|
||||
onSelect: function(e) {
|
||||
// display list selector
|
||||
jQuery(e.target).after(
|
||||
'<select id="bulk_action_list">'+
|
||||
'<option value="">Select a list</option>'+
|
||||
'<option value="1">List #1</option>'+
|
||||
'<option value="2">List #2</option>'+
|
||||
'<option value="3">List #3</option>'+
|
||||
'</select>'
|
||||
);
|
||||
},
|
||||
onApply: function(selected) {
|
||||
var list = jQuery('#bulk_action_list').val();
|
||||
|
||||
MailPoet.Ajax.post({
|
||||
endpoint: 'subscribers',
|
||||
action: 'move',
|
||||
data: {
|
||||
selected: selected,
|
||||
list: list
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'add',
|
||||
label: 'Add to list...'
|
||||
},
|
||||
{
|
||||
name: 'remove',
|
||||
label: 'Remove from list...'
|
||||
}
|
||||
];
|
||||
|
||||
var List = React.createClass({
|
||||
getItems: function(listing) {
|
||||
MailPoet.Ajax.post({
|
||||
endpoint: 'subscribers',
|
||||
action: 'get',
|
||||
data: {
|
||||
offset: (listing.state.page - 1) * listing.state.limit,
|
||||
limit: listing.state.limit,
|
||||
group: listing.state.group,
|
||||
search: listing.state.search,
|
||||
sort_by: listing.state.sort_by,
|
||||
sort_order: listing.state.sort_order
|
||||
},
|
||||
onSuccess: function(response) {
|
||||
if(listing.isMounted()) {
|
||||
listing.setState({
|
||||
items: response.items || [],
|
||||
filters: response.filters || [],
|
||||
groups: response.groups || [],
|
||||
count: response.count || 0,
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
}.bind(listing)
|
||||
});
|
||||
},
|
||||
renderItem: function(subscriber) {
|
||||
var rowClasses = classNames(
|
||||
'manage-column',
|
||||
'column-primary',
|
||||
'has-row-actions'
|
||||
);
|
||||
|
||||
var status;
|
||||
|
||||
switch(parseInt(subscriber.status, 10)) {
|
||||
case 1:
|
||||
status = 'Subscribed';
|
||||
break;
|
||||
|
||||
case 0:
|
||||
status = 'Unconfirmed';
|
||||
break;
|
||||
|
||||
case -1:
|
||||
status = 'Unsubscribed';
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<td className={ rowClasses }>
|
||||
<strong>
|
||||
<a>{ subscriber.email }</a>
|
||||
</strong>
|
||||
<button className="toggle-row" type="button">
|
||||
<span className="screen-reader-text">Show more details</span>
|
||||
</button>
|
||||
</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-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 (
|
||||
<Listing
|
||||
onRenderItem={this.renderItem}
|
||||
items={this.getItems}
|
||||
columns={columns}
|
||||
actions={actions} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return List;
|
||||
}
|
||||
);
|
@@ -1,399 +0,0 @@
|
||||
define(
|
||||
'listing',
|
||||
[
|
||||
'mailpoet',
|
||||
'jquery',
|
||||
'react',
|
||||
'classnames',
|
||||
'listing/bulk_actions.jsx',
|
||||
'listing/header.jsx',
|
||||
'listing/pages.jsx',
|
||||
'listing/search.jsx'
|
||||
],
|
||||
function(
|
||||
MailPoet,
|
||||
jQuery,
|
||||
React,
|
||||
classNames,
|
||||
ListingBulkActions,
|
||||
ListingHeader,
|
||||
ListingPages,
|
||||
ListingSearch
|
||||
) {
|
||||
|
||||
var ListingGroups = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<ul className="subsubsub">
|
||||
<li>
|
||||
<a className="current">
|
||||
All
|
||||
<span className="count">({ this.props.count })</span>
|
||||
</a> |
|
||||
</li>
|
||||
<li>
|
||||
<a>
|
||||
Subscribed
|
||||
<span className="count">(0)</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var ListingFilters = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<div></div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var ListSelector = React.createClass({
|
||||
getList: function(e) {
|
||||
e.preventDefault();
|
||||
MailPoet.Modal.popup({
|
||||
title: 'Bulk action',
|
||||
template: '',
|
||||
onInit: function(modal) {
|
||||
var target = modal.getContentContainer();
|
||||
React.render(
|
||||
<ListSelector />,
|
||||
target[0]
|
||||
);
|
||||
}
|
||||
})
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<div>
|
||||
<select >
|
||||
<option>Select a list</option>
|
||||
</select>
|
||||
<a onClick={this.getList}>Test me</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var ListingItem = React.createClass({
|
||||
handleSelect: function(e) {
|
||||
var is_checked = jQuery(e.target).is(':checked');
|
||||
|
||||
this.props.onSelect(
|
||||
parseInt(e.target.value, 10),
|
||||
is_checked
|
||||
);
|
||||
|
||||
return !e.target.checked;
|
||||
},
|
||||
render: function() {
|
||||
var rowClasses = classNames(
|
||||
'title',
|
||||
'column-title',
|
||||
'has-row-actions',
|
||||
'column-primary',
|
||||
'page-title'
|
||||
);
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<th className="check-column" scope="row">
|
||||
<label className="screen-reader-text">
|
||||
{ 'Select ' + this.props.item.email }</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
defaultValue={ this.props.item.id }
|
||||
defaultChecked={ this.props.item.selected }
|
||||
onChange={ this.handleSelect } />
|
||||
</th>
|
||||
<td className={rowClasses}>
|
||||
<strong>
|
||||
<a className="row-title">{ this.props.item.email }</a>
|
||||
</strong>
|
||||
</td>
|
||||
<td>
|
||||
{ this.props.item.first_name }
|
||||
</td>
|
||||
<td>
|
||||
{ this.props.item.last_name }
|
||||
</td>
|
||||
<td className="date column-date">
|
||||
<abbr title="">{ this.props.item.created_at }</abbr>
|
||||
</td>
|
||||
<td className="date column-date">
|
||||
<abbr title="">{ this.props.item.updated_at }</abbr>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var ListingItems = React.createClass({
|
||||
render: function() {
|
||||
if(this.props.items.length === 0) {
|
||||
return (
|
||||
<tbody>
|
||||
<td
|
||||
colSpan={this.props.columns.length + 1}
|
||||
className="colspanchange">No subscribers found.</td>
|
||||
</tbody>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<tbody>
|
||||
{this.props.items.map(function(item) {
|
||||
item.selected = (this.props.selected.indexOf(item.id) !== -1);
|
||||
return (
|
||||
<ListingItem
|
||||
columns={this.props.columns}
|
||||
onSelect={this.props.onSelect}
|
||||
key={item.id}
|
||||
item={item} />
|
||||
);
|
||||
}.bind(this))}
|
||||
</tbody>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var Listing = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {
|
||||
loading: false,
|
||||
search: '',
|
||||
page: 1,
|
||||
count: 0,
|
||||
limit: 10,
|
||||
sort_by: 'email',
|
||||
sort_order: 'asc',
|
||||
items: [],
|
||||
selected: []
|
||||
};
|
||||
},
|
||||
componentDidMount: function() {
|
||||
this.getItems();
|
||||
},
|
||||
getItems: function() {
|
||||
this.setState({ loading: true });
|
||||
|
||||
MailPoet.Ajax.post({
|
||||
endpoint: 'subscribers',
|
||||
action: 'get',
|
||||
data: {
|
||||
offset: (this.state.page - 1) * this.state.limit,
|
||||
limit: this.state.limit,
|
||||
search: this.state.search,
|
||||
sort_by: this.state.sort_by,
|
||||
sort_order: this.state.sort_order
|
||||
},
|
||||
onSuccess: function(response) {
|
||||
if(this.isMounted()) {
|
||||
this.setState({
|
||||
items: response.items,
|
||||
count: response.count,
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
}.bind(this)
|
||||
});
|
||||
},
|
||||
handleSearch: function(search) {
|
||||
this.setState({ search: search }, function() {
|
||||
this.getItems();
|
||||
}.bind(this));
|
||||
},
|
||||
handleSort: function(sort_by, sort_order = 'asc') {
|
||||
this.setState({
|
||||
sort_by: sort_by,
|
||||
sort_order: sort_order
|
||||
}, function() {
|
||||
this.getItems();
|
||||
}.bind(this));
|
||||
},
|
||||
handleSelect: function(id, is_checked) {
|
||||
var selected = this.state.selected;
|
||||
|
||||
if(is_checked) {
|
||||
selected = jQuery.merge(selected, [ id ]);
|
||||
} else {
|
||||
selected.splice(selected.indexOf(id), 1);
|
||||
}
|
||||
this.setState({
|
||||
selected: selected
|
||||
});
|
||||
},
|
||||
handleSelectAll: function(is_checked) {
|
||||
if(is_checked === false) {
|
||||
this.setState({ selected: [] });
|
||||
} else {
|
||||
var selected = this.state.items.map(function(item) {
|
||||
return ~~item.id;
|
||||
});
|
||||
|
||||
this.setState({
|
||||
selected: selected
|
||||
});
|
||||
}
|
||||
},
|
||||
handleSetPage: function(page) {
|
||||
this.setState({ page: page }, function() {
|
||||
this.getItems();
|
||||
}.bind(this));
|
||||
},
|
||||
render: function() {
|
||||
var items = this.state.items,
|
||||
sort_by = this.state.sort_by,
|
||||
sort_order = this.state.sort_order;
|
||||
|
||||
// set sortable columns
|
||||
columns = columns.map(function(column) {
|
||||
column.sorted = (column.name === sort_by) ? sort_order : false;
|
||||
return column;
|
||||
});
|
||||
|
||||
var tableClasses = classNames(
|
||||
'wp-list-table',
|
||||
'widefat',
|
||||
'fixed',
|
||||
'striped',
|
||||
{ 'mailpoet_listing_loading': this.state.loading }
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ListingGroups count={this.state.count} />
|
||||
<ListingSearch
|
||||
onSearch={this.handleSearch}
|
||||
search={this.state.search} />
|
||||
<div className="tablenav top clearfix">
|
||||
<ListingBulkActions
|
||||
actions={this.props.actions}
|
||||
selected={this.state.selected} />
|
||||
<ListingFilters />
|
||||
<ListingPages
|
||||
count={this.state.count}
|
||||
page={this.state.page}
|
||||
limit={this.state.limit}
|
||||
onSetPage={this.handleSetPage} />
|
||||
</div>
|
||||
<table className={tableClasses}>
|
||||
<thead>
|
||||
<ListingHeader
|
||||
onSort={this.handleSort}
|
||||
onSelectAll={this.handleSelectAll}
|
||||
sort_by={this.state.sort_by}
|
||||
sort_order={this.state.sort_order}
|
||||
columns={this.props.columns} />
|
||||
</thead>
|
||||
|
||||
<ListingItems
|
||||
columns={this.props.columns}
|
||||
selected={this.state.selected}
|
||||
onSelect={this.handleSelect}
|
||||
items={items} />
|
||||
|
||||
<tfoot>
|
||||
<ListingHeader
|
||||
onSort={this.handleSort}
|
||||
onSelectAll={this.handleSelectAll}
|
||||
sort_by={this.state.sort_by}
|
||||
sort_order={this.state.sort_order}
|
||||
columns={this.props.columns} />
|
||||
</tfoot>
|
||||
|
||||
</table>
|
||||
<div className="tablenav bottom">
|
||||
<ListingBulkActions
|
||||
actions={this.props.actions}
|
||||
selected={this.state.selected} />
|
||||
<ListingPages
|
||||
count={this.state.count}
|
||||
page={this.state.page}
|
||||
limit={this.state.limit}
|
||||
onSetPage={this.handleSetPage} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var columns = [
|
||||
{
|
||||
name: 'email',
|
||||
label: 'Email',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'first_name',
|
||||
label: 'Firstname',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'last_name',
|
||||
label: 'Lastname',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'created_at',
|
||||
label: 'Subscribed on',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'updated_at',
|
||||
label: 'Last modified on',
|
||||
sortable: true
|
||||
},
|
||||
];
|
||||
|
||||
var actions = [
|
||||
{
|
||||
name: 'move',
|
||||
label: 'Move to...',
|
||||
onSelect: function(e) {
|
||||
// display list selector
|
||||
jQuery(e.target).after(
|
||||
'<select id="bulk_action_list">'+
|
||||
'<option value="">Select a list</option>'+
|
||||
'<option value="1">List #1</option>'+
|
||||
'<option value="2">List #2</option>'+
|
||||
'<option value="3">List #3</option>'+
|
||||
'</select>'
|
||||
);
|
||||
},
|
||||
onApply: function(selected) {
|
||||
var list = jQuery('#bulk_action_list').val();
|
||||
|
||||
MailPoet.Ajax.post({
|
||||
endpoint: 'subscribers',
|
||||
action: 'move',
|
||||
data: {
|
||||
selected: selected,
|
||||
list: list
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'add',
|
||||
label: 'Add to...'
|
||||
},
|
||||
{
|
||||
name: 'remove',
|
||||
label: 'Remove from...'
|
||||
}
|
||||
];
|
||||
|
||||
var element = jQuery('#mailpoet_subscribers_listing');
|
||||
|
||||
if(element.length > 0) {
|
||||
React.render(
|
||||
<Listing columns={columns} actions={actions} />,
|
||||
element[0]
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
52
assets/js/src/subscribers/subscribers.jsx
Normal file
52
assets/js/src/subscribers/subscribers.jsx
Normal file
@@ -0,0 +1,52 @@
|
||||
define(
|
||||
'subscribers',
|
||||
[
|
||||
'react',
|
||||
'react-router',
|
||||
'subscribers/list.jsx'
|
||||
],
|
||||
function(
|
||||
React,
|
||||
Router,
|
||||
List
|
||||
) {
|
||||
|
||||
var DefaultRoute = Router.DefaultRoute;
|
||||
var Link = Router.Link;
|
||||
var Route = Router.Route;
|
||||
var RouteHandler = Router.RouteHandler;
|
||||
|
||||
var App = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<div>
|
||||
<h1>
|
||||
{ MailPoetI18n.pageTitle }
|
||||
<span>
|
||||
<Link className="add-new-h2" to="list">Subscribers</Link>
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<RouteHandler/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var routes = (
|
||||
<Route name="app" path="/" handler={App}>
|
||||
<Route name="list" handler={List} />
|
||||
<DefaultRoute handler={List} />
|
||||
</Route>
|
||||
);
|
||||
|
||||
var hook = document.getElementById('subscribers');
|
||||
if(hook) {
|
||||
Router.run(routes, function(Handler, state) {
|
||||
React.render(
|
||||
<Handler params={state.params} query={state.query} />,
|
||||
hook
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
@@ -5,6 +5,9 @@ if (!defined('ABSPATH')) exit;
|
||||
|
||||
class Subscriber extends Model {
|
||||
public static $_table = MP_SUBSCRIBERS_TABLE;
|
||||
const STATE_SUBSCRIBED = 1;
|
||||
const STATE_UNCONFIRMED = 0;
|
||||
const STATE_UNSUBSCRIBED = -1;
|
||||
|
||||
function __construct() {
|
||||
parent::__construct();
|
||||
|
@@ -1,7 +1,6 @@
|
||||
<?php
|
||||
namespace MailPoet\Router;
|
||||
use \MailPoet\Models\Newsletter;
|
||||
use \MailPoet\Models\Subscriber;
|
||||
use \MailPoet\Mailer\Bridge;
|
||||
|
||||
if(!defined('ABSPATH')) exit;
|
||||
@@ -10,7 +9,51 @@ class Newsletters {
|
||||
function __construct() {
|
||||
}
|
||||
|
||||
function get() {
|
||||
function get($data = array()) {
|
||||
// pagination
|
||||
$offset = (isset($data['offset']) ? (int)$data['offset'] : 0);
|
||||
$limit = (isset($data['limit']) ? (int)$data['limit'] : 50);
|
||||
// searching
|
||||
$search = (isset($data['search']) ? $data['search'] : null);
|
||||
// sorting
|
||||
$sort_by = (isset($data['sort_by']) ? $data['sort_by'] : 'id');
|
||||
$sort_order = (isset($data['sort_order']) ? $data['sort_order'] : 'asc');
|
||||
// grouping
|
||||
$group = (isset($data['group']) ? $data['group'] : null);
|
||||
$groups = array(
|
||||
array(
|
||||
'name' => 'all',
|
||||
'label' => __('All'),
|
||||
'count' => Newsletter::count()
|
||||
)
|
||||
);
|
||||
|
||||
// instantiate subscriber collection
|
||||
$collection = Newsletter::{'order_by_'.$sort_order}($sort_by);
|
||||
|
||||
// handle search
|
||||
if($search !== null) {
|
||||
$collection->where_like('subject', '%'.$search.'%');
|
||||
}
|
||||
|
||||
// handle filters
|
||||
$filters = array();
|
||||
|
||||
// return result
|
||||
$collection = array(
|
||||
'count' => $collection->count(),
|
||||
'filters' => $filters,
|
||||
'groups' => $groups,
|
||||
'items' => $collection
|
||||
->offset($offset)
|
||||
->limit($limit)
|
||||
->find_array()
|
||||
);
|
||||
|
||||
wp_send_json($collection);
|
||||
}
|
||||
|
||||
function getAll() {
|
||||
$collection = Newsletter::find_array();
|
||||
wp_send_json($collection);
|
||||
}
|
||||
@@ -37,7 +80,7 @@ class Newsletters {
|
||||
|
||||
function send($id) {
|
||||
$newsletter = Newsletter::find_one($id)->as_array();
|
||||
$subscribers = Subscriber::find_array();
|
||||
$subscribers = Newsletter::find_array();
|
||||
$mailer = new Bridge($newsletter, $subscribers);
|
||||
wp_send_json($mailer->send());
|
||||
}
|
||||
|
@@ -9,14 +9,58 @@ class Subscribers {
|
||||
}
|
||||
|
||||
function get($data = array()) {
|
||||
// pagination
|
||||
$offset = (isset($data['offset']) ? (int)$data['offset'] : 0);
|
||||
$limit = (isset($data['limit']) ? (int)$data['limit'] : 50);
|
||||
// searching
|
||||
$search = (isset($data['search']) ? $data['search'] : null);
|
||||
// sorting
|
||||
$sort_by = (isset($data['sort_by']) ? $data['sort_by'] : 'id');
|
||||
$sort_order = (isset($data['sort_order']) ? $data['sort_order'] : 'desc');
|
||||
$sort_order = (isset($data['sort_order']) ? $data['sort_order'] : 'asc');
|
||||
// grouping
|
||||
$group = (isset($data['group']) ? $data['group'] : null);
|
||||
$groups = array(
|
||||
array(
|
||||
'name' => 'all',
|
||||
'label' => __('All'),
|
||||
'count' => Subscriber::count()
|
||||
),
|
||||
array(
|
||||
'name' => 'subscribed',
|
||||
'label' => __('Subscribed'),
|
||||
'count' => Subscriber::where('status', Subscriber::STATE_SUBSCRIBED)->count()
|
||||
),
|
||||
array(
|
||||
'name' => 'unconfirmed',
|
||||
'label' => __('Unconfirmed'),
|
||||
'count' => Subscriber::where('status', Subscriber::STATE_UNCONFIRMED)->count()
|
||||
),
|
||||
array(
|
||||
'name' => 'unsubscribed',
|
||||
'label' => __('Unsubscribed'),
|
||||
'count' => Subscriber::where('status', Subscriber::STATE_UNSUBSCRIBED)->count()
|
||||
)
|
||||
);
|
||||
|
||||
// instantiate subscriber collection
|
||||
$collection = Subscriber::{'order_by_'.$sort_order}($sort_by);
|
||||
|
||||
// handle group
|
||||
switch($group) {
|
||||
case 'subscribed':
|
||||
$collection = $collection->where('status', Subscriber::STATE_SUBSCRIBED);
|
||||
break;
|
||||
|
||||
case 'unconfirmed':
|
||||
$collection = $collection->where('status', Subscriber::STATE_UNCONFIRMED);
|
||||
break;
|
||||
|
||||
case 'unsubscribed':
|
||||
$collection = $collection->where('status', Subscriber::STATE_UNSUBSCRIBED);
|
||||
break;
|
||||
}
|
||||
|
||||
// handle search
|
||||
if($search !== null) {
|
||||
$collection->where_raw(
|
||||
'(`email` LIKE ? OR `first_name` LIKE ? OR `last_name` LIKE ?)',
|
||||
@@ -24,14 +68,14 @@ class Subscribers {
|
||||
);
|
||||
}
|
||||
|
||||
// filters
|
||||
$filters = array(
|
||||
|
||||
);
|
||||
// handle filters
|
||||
$filters = array();
|
||||
|
||||
// return result
|
||||
$collection = array(
|
||||
'count' => $collection->count(),
|
||||
'filters' => $filters,
|
||||
'groups' => $groups,
|
||||
'items' => $collection
|
||||
->offset($offset)
|
||||
->limit($limit)
|
||||
|
@@ -5,20 +5,20 @@ class i18n extends \Twig_Extension {
|
||||
|
||||
private $_text_domain;
|
||||
|
||||
public function __construct($text_domain) {
|
||||
function __construct($text_domain) {
|
||||
// set text domain
|
||||
$this->_text_domain = $text_domain;
|
||||
}
|
||||
|
||||
public function getName() {
|
||||
function getName() {
|
||||
return 'i18n';
|
||||
}
|
||||
|
||||
public function getFunctions() {
|
||||
function getFunctions() {
|
||||
// twig custom functions
|
||||
$twig_functions = array();
|
||||
// list of WP functions to map
|
||||
$functions = array('__', '_n');
|
||||
$functions = array('localize', '__', '_n');
|
||||
|
||||
foreach($functions as $function) {
|
||||
$twig_functions[] = new \Twig_SimpleFunction(
|
||||
@@ -30,13 +30,27 @@ class i18n extends \Twig_Extension {
|
||||
return $twig_functions;
|
||||
}
|
||||
|
||||
public function __() {
|
||||
function localize() {
|
||||
$args = func_get_args();
|
||||
$translations = array_shift($args);
|
||||
$output = array();
|
||||
|
||||
$output[] = '<script type="text/javascript">';
|
||||
$output[] = ' var MailPoetI18n = MailPoetI18n || {}';
|
||||
foreach($translations as $key => $translation) {
|
||||
$output[] = 'MailPoetI18n[\''.$key.'\'] = "'.$translation.'";';
|
||||
}
|
||||
$output[] = '</script>';
|
||||
return join("\n", $output);
|
||||
}
|
||||
|
||||
function __() {
|
||||
$args = func_get_args();
|
||||
|
||||
return call_user_func_array('__', $this->setTextDomain($args));
|
||||
}
|
||||
|
||||
public function _n() {
|
||||
function _n() {
|
||||
$args = func_get_args();
|
||||
|
||||
return call_user_func_array('_n', $this->setTextDomain($args));
|
||||
|
@@ -2,4 +2,11 @@
|
||||
|
||||
<% block content %>
|
||||
<div id="newsletters"></div>
|
||||
|
||||
<%= localize({
|
||||
'pageTitle': __('Newsletters'),
|
||||
'searchLabel': __('Search'),
|
||||
'loading': __('Loading newsletters...'),
|
||||
'noRecordFound': __('No newsletters found.')
|
||||
}) %>
|
||||
<% endblock %>
|
||||
|
@@ -1,6 +1,12 @@
|
||||
<% extends 'layout.html' %>
|
||||
|
||||
<% block content %>
|
||||
<h1><%= __('Subscribers') %></h1>
|
||||
<div id="mailpoet_subscribers_listing"></div>
|
||||
<div id="subscribers"></div>
|
||||
|
||||
<%= localize({
|
||||
'pageTitle': __('Subscribers'),
|
||||
'searchLabel': __('Search'),
|
||||
'loading': __('Loading subscribers...'),
|
||||
'noRecordFound': __('No subscribers found.')
|
||||
}) %>
|
||||
<% endblock %>
|
||||
|
@@ -60,12 +60,9 @@ config.push(_.extend({}, baseConfig, {
|
||||
vendor: ['handlebars', 'handlebars_helpers'],
|
||||
mailpoet: ['mailpoet', 'ajax', 'modal', 'notice'],
|
||||
admin: [
|
||||
'subscribers/listing.jsx',
|
||||
'settings.jsx',
|
||||
'subscribers.jsx',
|
||||
'newsletters/newsletters.jsx',
|
||||
'newsletters/list.jsx',
|
||||
'newsletters/form.jsx'
|
||||
'subscribers/subscribers.jsx',
|
||||
'newsletters/newsletters.jsx'
|
||||
],
|
||||
newsletter_editor: [
|
||||
'underscore',
|
||||
|
Reference in New Issue
Block a user