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
This commit is contained in:
Jonathan Labreuille
2015-10-27 16:24:00 +01:00
parent 588b441fb1
commit 89b04e8691
13 changed files with 686 additions and 621 deletions

View File

@ -1,4 +1,4 @@
.mailpoet_listing_loading tbody tr, .mailpoet_listing_loading tbody tr
.mailpoet_form_loading tbody tr .mailpoet_form_loading tbody tr
opacity: 0.2 opacity: 0.2
@ -8,6 +8,20 @@
.mailpoet_select_all td .mailpoet_select_all td
text-align: center text-align: center
table.widefat thead .check-column, .mailpoet_listing_table
table.widefat tfoot .check-column th span
padding: 10px 0 0 3px 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

@ -89,7 +89,12 @@ define(
this.setState({ loading: false }); this.setState({ loading: false });
if(response === true) { 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) { if(this.props.params.id !== undefined) {
this.props.messages['updated'](); this.props.messages['updated']();
} else { } else {

View File

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

View File

@ -282,43 +282,101 @@ define(
}; };
}, },
componentDidUpdate: function(prevProps, prevState) { componentDidUpdate: function(prevProps, prevState) {
// set group to "all" if trash gets emptied // reset group to "all" if trash gets emptied
if( if(
// we were viewing the trash
(prevState.group === 'trash' && prevState.count > 0) (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) (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'); 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() { componentDidMount: function() {
if(this.isMounted()) { if(this.isMounted()) {
var state = this.state || {}; const params = this.props.params || {}
var params = this.props.params || {}; this.initWithParams(params)
// set filters
if(params.filter !== undefined) {
var filter = {};
var pairs = params.filter
.split('&')
.map(function(pair) {
var [key, value] = pair.split('=');
filter[key] = value;
}
);
state.filter = filter;
}
if(this.props.limit !== undefined) {
state.limit = Math.abs(~~this.props.limit);
}
this.setState(state, function() {
this.getItems();
}.bind(this));
} }
}, },
componentWillReceiveProps: function(nextProps) {
const params = nextProps.params || {}
//this.initWithParams(params)
},
getItems: function() { getItems: function() {
if(this.isMounted()) { if(this.isMounted()) {
this.setState({ loading: true }); this.setState({ loading: true });
@ -443,6 +501,7 @@ define(
selection: false, selection: false,
selected_ids: [] selected_ids: []
}, function() { }, function() {
this.setParams();
this.getItems(); this.getItems();
}.bind(this)); }.bind(this));
}, },
@ -451,6 +510,7 @@ define(
sort_by: sort_by, sort_by: sort_by,
sort_order: sort_order, sort_order: sort_order,
}, function() { }, function() {
this.setParams();
this.getItems(); this.getItems();
}.bind(this)); }.bind(this));
}, },
@ -510,6 +570,7 @@ define(
filter: filters, filter: filters,
page: 1 page: 1
}, function() { }, function() {
this.setParams();
this.getItems(); this.getItems();
}.bind(this)); }.bind(this));
}, },
@ -519,10 +580,11 @@ define(
this.setState({ this.setState({
group: group, group: group,
filter: [], filter: {},
search: '', search: '',
page: 1 page: 1
}, function() { }, function() {
this.setParams();
this.getItems(); this.getItems();
}.bind(this)); }.bind(this));
}, },
@ -532,6 +594,7 @@ define(
selection: false, selection: false,
selected_ids: [] selected_ids: []
}, function() { }, function() {
this.setParams();
this.getItems(); this.getItems();
}.bind(this)); }.bind(this));
}, },
@ -571,7 +634,8 @@ define(
// item actions // item actions
var item_actions = this.props.item_actions || []; var item_actions = this.props.item_actions || [];
var tableClasses = classNames( var table_classes = classNames(
'mailpoet_listing_table',
'wp-list-table', 'wp-list-table',
'widefat', 'widefat',
'fixed', 'fixed',
@ -622,7 +686,7 @@ define(
limit={ this.state.limit } limit={ this.state.limit }
onSetPage={ this.handleSetPage } /> onSetPage={ this.handleSetPage } />
</div> </div>
<table className={ tableClasses }> <table className={ table_classes }>
<thead> <thead>
<ListingHeader <ListingHeader
onSort={ this.handleSort } onSort={ this.handleSort }

View File

@ -104,6 +104,7 @@ define(['react', 'classnames'], function(React, classNames) {
pagination = ( pagination = (
<span className="pagination-links"> <span className="pagination-links">
{firstPage} {firstPage}
&nbsp;
{previousPage} {previousPage}
&nbsp; &nbsp;
<span className="paging-input"> <span className="paging-input">
@ -128,6 +129,7 @@ define(['react', 'classnames'], function(React, classNames) {
</span> </span>
&nbsp; &nbsp;
{nextPage} {nextPage}
&nbsp;
{lastPage} {lastPage}
</span> </span>
); );
@ -140,7 +142,7 @@ define(['react', 'classnames'], function(React, classNames) {
return ( return (
<div className={ classes }> <div className={ classes }>
<span className="displaying-num">{ this.props.count } item(s)</span> <span className="displaying-num">{ this.props.count } items</span>
{ pagination } { pagination }
</div> </div>
); );

View File

@ -7,6 +7,9 @@ define(['react'], function(React) {
this.refs.search.value this.refs.search.value
); );
}, },
componentWillReceiveProps: function(nextProps) {
this.refs.search.value = nextProps.search
},
render: function() { render: function() {
if(this.props.search === false) { if(this.props.search === false) {
return false; return false;

View File

@ -163,6 +163,7 @@ define(
</h2> </h2>
<Listing <Listing
params={ this.props.params }
endpoint="newsletters" endpoint="newsletters"
onRenderItem={this.renderItem} onRenderItem={this.renderItem}
columns={columns} columns={columns}

View File

@ -34,21 +34,27 @@ define(
} }
}; };
var Link = Router.Link;
var SegmentForm = React.createClass({ var SegmentForm = React.createClass({
mixins: [
Router.History
],
render: function() { render: function() {
return ( return (
<div> <div>
<h2 className="title"> <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> </h2>
<Form <Form
endpoint="segments" endpoint="segments"
fields={ fields } fields={ fields }
params={ this.props.params } params={ this.props.params }
messages={ messages } /> messages={ messages }
onSuccess={ this.history.goBack } />
</div> </div>
); );
} }

View File

@ -1,222 +1,213 @@
define( import React from 'react'
[ import { Router, Route, Link } from 'react-router'
'react',
'react-router',
'listing/listing.jsx',
'classnames',
'mailpoet'
],
function(
React,
Router,
Listing,
classNames,
MailPoet
) {
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 = { import jQuery from 'jquery'
onDelete: function(response) { import MailPoet from 'mailpoet'
var count = ~~response.segments; import classNames from 'classnames'
var message = null;
if(count === 1 || response === true) { import Listing from 'listing/listing.jsx'
message = (
'1 segment was moved to the trash.'
);
} else if(count > 1) {
message = (
'%$1d segments were moved to the trash.'
).replace('%$1d', count);
}
if(message !== null) { var columns = [
MailPoet.Notice.success(message); {
} name: 'name',
}, label: 'Name',
onConfirmDelete: function(response) { sortable: true
var count = ~~response.segments; },
var message = null; {
name: 'description',
if(count === 1 || response === true) { label: 'Description',
message = ( sortable: false
'1 segment was permanently deleted.' },
); {
} else if(count > 1) { name: 'subscribed',
message = ( label: 'Subscribed',
'%$1d segments were permanently deleted.' sortable: false
).replace('%$1d', count); },
} {
name: 'unconfirmed',
if(message !== null) { label: 'Unconfirmed',
MailPoet.Notice.success(message); sortable: false
} },
}, {
onRestore: function(response) { name: 'unsubscribed',
var count = ~~response.segments; label: 'Unsubscribed',
var message = null; sortable: false
},
if(count === 1 || response === true) { {
message = ( name: 'created_at',
'1 segment has been restored from the trash.' label: 'Created on',
); sortable: true
} else if(count > 1) {
message = (
'%$1d segments have been restored from the trash.'
).replace('%$1d', count);
}
if(message !== null) {
MailPoet.Notice.success(message);
}
}
};
var Link = Router.Link;
var item_actions = [
{
name: 'edit',
link: function(item) {
return (
<Link to={ `/edit/${item.id}` }>Edit</Link>
);
}
},
{
name: 'duplicate_segment',
refresh: true,
link: function(item) {
return (
<a
href="javascript:;"
onClick={ this.onDuplicate.bind(null, item) }
>Duplicate</a>
);
},
onDuplicate: function(item) {
MailPoet.Ajax.post({
endpoint: 'segments',
action: 'duplicate',
data: item.id
}).done(function() {
MailPoet.Notice.success(
('List "%$1s" has been duplicated.').replace('%$1s', item.name)
);
});
}
},
{
name: 'view_subscribers',
link: function(item) {
return (
<a href={ item.subscribers_url }>View subscribers</a>
);
}
}
];
var bulk_actions = [
{
name: 'trash',
label: 'Trash',
getData: function() {
return {
confirm: false
}
},
onSuccess: messages.onDelete
}
];
var Link = Router.Link;
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
messages={ messages }
search={ false }
limit={ 1000 }
endpoint="segments"
onRenderItem={ this.renderItem }
columns={ columns }
bulk_actions={ bulk_actions }
item_actions={ item_actions }
/>
</div>
);
}
});
return SegmentList;
} }
); ];
var messages = {
onDelete: function(response) {
var count = ~~response.segments;
var message = null;
if(count === 1 || response === true) {
message = (
'1 segment was moved to the trash.'
);
} else if(count > 1) {
message = (
'%$1d segments were moved to the trash.'
).replace('%$1d', count);
}
if(message !== null) {
MailPoet.Notice.success(message);
}
},
onConfirmDelete: function(response) {
var count = ~~response.segments;
var message = null;
if(count === 1 || response === true) {
message = (
'1 segment was permanently deleted.'
);
} else if(count > 1) {
message = (
'%$1d segments were permanently deleted.'
).replace('%$1d', count);
}
if(message !== null) {
MailPoet.Notice.success(message);
}
},
onRestore: function(response) {
var count = ~~response.segments;
var message = null;
if(count === 1 || response === true) {
message = (
'1 segment has been restored from the trash.'
);
} else if(count > 1) {
message = (
'%$1d segments have been restored from the trash.'
).replace('%$1d', count);
}
if(message !== null) {
MailPoet.Notice.success(message);
}
}
};
var item_actions = [
{
name: 'edit',
link: function(item) {
return (
<Link to={ `/edit/${item.id}` }>Edit</Link>
);
}
},
{
name: 'duplicate_segment',
refresh: true,
link: function(item) {
return (
<a
href="javascript:;"
onClick={ this.onDuplicate.bind(null, item) }
>Duplicate</a>
);
},
onDuplicate: function(item) {
MailPoet.Ajax.post({
endpoint: 'segments',
action: 'duplicate',
data: item.id
}).done(function() {
MailPoet.Notice.success(
('List "%$1s" has been duplicated.').replace('%$1s', item.name)
);
});
}
},
{
name: 'view_subscribers',
link: function(item) {
return (
<a href={ item.subscribers_url }>View subscribers</a>
);
}
}
];
var bulk_actions = [
{
name: 'trash',
label: 'Trash',
getData: function() {
return {
confirm: false
}
},
onSuccess: messages.onDelete
}
];
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 Link = Router.Link;
var SubscriberForm = React.createClass({ var SubscriberForm = React.createClass({
mixins: [
Router.History
],
render: function() { render: function() {
return ( return (
<div> <div>
<h2 className="title"> <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> </h2>
<Form <Form
endpoint="subscribers" endpoint="subscribers"
fields={ fields } fields={ fields }
params={ this.props.params } params={ this.props.params }
messages={ messages } /> messages={ messages }
onSuccess={ this.history.goBack } />
</div> </div>
); );
} }

View File

@ -1,327 +1,300 @@
define( import React from 'react'
[ import { Router, Route, Link } from 'react-router'
'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;
var columns = [ import jQuery from 'jquery'
{ import MailPoet from 'mailpoet'
name: 'email', import classNames from 'classnames'
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 Listing from 'listing/listing.jsx'
name: 'created_at', import Selection from 'form/fields/selection.jsx'
label: 'Subscribed on',
sortable: true
},
{
name: 'updated_at',
label: 'Last modified on',
sortable: true
},
];
var messages = { const columns = [
onDelete: function(response) { {
var count = ~~response.subscribers; name: 'email',
var message = null; label: 'Subscriber',
sortable: true
},
{
name: 'status',
label: 'Status',
sortable: true
},
{
name: 'segments',
label: 'Lists',
sortable: false
},
if(count === 1) { {
message = ( name: 'created_at',
'1 subscriber was moved to the trash.' label: 'Subscribed on',
).replace('%$1d', count); sortable: true
} else if(count > 1) { },
message = ( {
'%$1d subscribers were moved to the trash.' name: 'updated_at',
).replace('%$1d', count); label: 'Last modified on',
} sortable: true
},
];
if(message !== null) { const messages = {
MailPoet.Notice.success(message); onDelete: function(response) {
} let count = ~~response.subscribers;
}, let message = null;
onConfirmDelete: function(response) {
var count = ~~response.subscribers;
var message = null;
if(count === 1) { if(count === 1) {
message = ( message = (
'1 subscriber was permanently deleted.' '1 subscriber was moved to the trash.'
).replace('%$1d', count); ).replace('%$1d', count);
} else if(count > 1) { } else if(count > 1) {
message = ( message = (
'%$1d subscribers were permanently deleted.' '%$1d subscribers were moved to the trash.'
).replace('%$1d', count); ).replace('%$1d', count);
} }
if(message !== null) { if(message !== null) {
MailPoet.Notice.success(message); MailPoet.Notice.success(message);
} }
}, },
onRestore: function(response) { onConfirmDelete: function(response) {
var count = ~~response.subscribers; let count = ~~response.subscribers;
var message = null; let message = null;
if(count === 1) { if(count === 1) {
message = ( message = (
'1 subscriber has been restored from the trash.' '1 subscriber was permanently deleted.'
).replace('%$1d', count); ).replace('%$1d', count);
} else if(count > 1) { } else if(count > 1) {
message = ( message = (
'%$1d subscribers have been restored from the trash.' '%$1d subscribers were permanently deleted.'
).replace('%$1d', count); ).replace('%$1d', count);
} }
if(message !== null) { if(message !== null) {
MailPoet.Notice.success(message); MailPoet.Notice.success(message);
} }
} },
}; onRestore: function(response) {
let count = ~~response.subscribers;
let message = null;
var bulk_actions = [ if(count === 1) {
{ message = (
name: 'moveToList', '1 subscriber has been restored from the trash.'
label: 'Move to list...', ).replace('%$1d', count);
onSelect: function() { } else if(count > 1) {
var field = { message = (
id: 'move_to_segment', '%$1d subscribers have been restored from the trash.'
endpoint: 'segments' ).replace('%$1d', count);
}; }
return ( if(message !== null) {
<Selection field={ field }/> MailPoet.Notice.success(message);
); }
},
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',
'column-username'
);
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 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 }>
{ avatar }
<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
params={ this.props.params }
endpoint="subscribers"
onRenderItem={ this.renderItem }
columns={ columns }
bulk_actions={ bulk_actions }
messages={ messages }
/>
</div>
);
}
});
return SubscriberList;
} }
); };
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.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
}
];
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,7 +5,7 @@ import SubscriberList from 'subscribers/list.jsx'
import SubscriberForm from 'subscribers/form.jsx' import SubscriberForm from 'subscribers/form.jsx'
import createHashHistory from 'history/lib/createHashHistory' import createHashHistory from 'history/lib/createHashHistory'
let history = createHashHistory({ queryKey: false }) const history = createHashHistory({ queryKey: false })
const App = React.createClass({ const App = React.createClass({
render() { render() {
@ -13,7 +13,7 @@ const App = React.createClass({
} }
}); });
let container = document.getElementById('subscribers_container'); const container = document.getElementById('subscribers_container')
if(container) { if(container) {
ReactDOM.render(( ReactDOM.render((
@ -22,7 +22,6 @@ if(container) {
<IndexRoute component={ SubscriberList } /> <IndexRoute component={ SubscriberList } />
<Route path="new" component={ SubscriberForm } /> <Route path="new" component={ SubscriberForm } />
<Route path="edit/:id" component={ SubscriberForm } /> <Route path="edit/:id" component={ SubscriberForm } />
<Route path="filter[:filter]" component={ SubscriberList } />
<Route path="*" component={ SubscriberList } /> <Route path="*" component={ SubscriberList } />
</Route> </Route>
</Router> </Router>

View File

@ -23,7 +23,7 @@ class Model extends \Sudzy\ValidModel {
private function setTimestamp() { private function setTimestamp() {
if($this->created_at === null) { if($this->created_at === null) {
$this->created_at = date('Y-m-d H:i:s'); $this->set_expr('created_at', 'NOW()');
} }
} }