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
white-space: nowrap
thead .check-column
tfoot .check-column
padding: 10px 0 0 3px 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,33 +1,23 @@
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;
},
render: function() {
var filters = this.props.filters;
var selected_filters = this.props.filter;
var available_filters = Object.keys(filters) return Object.keys(filters).filter(function(filter) {
.filter(function(filter) {
return !( return !(
filters[filter].length === 0 filters[filter].length === 0
|| ( || (
@ -36,17 +26,26 @@ function(
) )
); );
}) })
},
render: function() {
const filters = this.props.filters;
const selected_filters = this.props.filter;
const available_filters = this.getAvailableFilters()
.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) { { filters[filter].map(function(option, j) {
return ( return (
<option <option
@ -59,7 +58,7 @@ function(
); );
}.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,42 +282,100 @@ 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');
} }
}, },
componentDidMount: function() { getParam: function(param) {
if(this.isMounted()) { var regex = /(.*)\[(.*)\]/
var state = this.state || {}; var matches = regex.exec(param)
var params = this.props.params || {}; return [matches[1], matches[2]]
},
// set filters initWithParams: function(params) {
if(params.filter !== undefined) { let state = this.state || {}
var filter = {}; let original_state = state
var pairs = params.filter // check for url params
.split('&') if(params.splat !== undefined) {
.map(function(pair) { params.splat.split('/').map(param => {
var [key, value] = pair.split('='); let [key, value] = this.getParam(param);
filter[key] = value; switch(key) {
case 'filter':
let filters = {}
value.split('&').map(function(pair) {
let [k, v] = pair.split('=')
filters[k] = v
} }
); )
state.filter = filter; state.filter = filters
break;
default:
state[key] = value
}
})
} }
if(this.props.limit !== undefined) { if(this.props.limit !== undefined) {
state.limit = Math.abs(~~this.props.limit); state.limit = Math.abs(~~this.props.limit);
} }
this.setState(state, function() { this.setState(state, function() {
this.getItems(); this.getItems();
}.bind(this)); }.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() {
if(this.isMounted()) {
const params = this.props.params || {}
this.initWithParams(params)
}
},
componentWillReceiveProps: function(nextProps) {
const params = nextProps.params || {}
//this.initWithParams(params)
}, },
getItems: function() { getItems: function() {
if(this.isMounted()) { if(this.isMounted()) {
@ -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,18 +1,12 @@
define( import React from 'react'
[ import { Router, Route, Link } from 'react-router'
'react',
'react-router', import jQuery from 'jquery'
'listing/listing.jsx', import MailPoet from 'mailpoet'
'classnames', import classNames from 'classnames'
'mailpoet'
], import Listing from 'listing/listing.jsx'
function(
React,
Router,
Listing,
classNames,
MailPoet
) {
var columns = [ var columns = [
{ {
name: 'name', name: 'name',
@ -103,7 +97,6 @@ define(
} }
}; };
var Link = Router.Link;
var item_actions = [ var item_actions = [
{ {
name: 'edit', name: 'edit',
@ -159,8 +152,6 @@ define(
} }
]; ];
var Link = Router.Link;
var SegmentList = React.createClass({ var SegmentList = React.createClass({
renderItem: function(segment, actions) { renderItem: function(segment, actions) {
var rowClasses = classNames( var rowClasses = classNames(
@ -203,6 +194,8 @@ define(
</h2> </h2>
<Listing <Listing
location={ this.props.location }
params={ this.props.params }
messages={ messages } messages={ messages }
search={ false } search={ false }
limit={ 1000 } limit={ 1000 }
@ -217,6 +210,4 @@ define(
} }
}); });
return SegmentList; 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,39 +1,17 @@
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'
import classNames from 'classnames'
import Listing from 'listing/listing.jsx'
import Selection from 'form/fields/selection.jsx'
const columns = [
{ {
name: 'email', name: 'email',
label: 'Email', label: 'Subscriber',
sortable: true
},
{
name: 'first_name',
label: 'Firstname',
sortable: true
},
{
name: 'last_name',
label: 'Lastname',
sortable: true sortable: true
}, },
{ {
@ -59,10 +37,10 @@ define(
}, },
]; ];
var messages = { const messages = {
onDelete: function(response) { onDelete: function(response) {
var count = ~~response.subscribers; let count = ~~response.subscribers;
var message = null; let message = null;
if(count === 1) { if(count === 1) {
message = ( message = (
@ -79,8 +57,8 @@ define(
} }
}, },
onConfirmDelete: 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 = (
@ -97,8 +75,8 @@ define(
} }
}, },
onRestore: function(response) { onRestore: function(response) {
var count = ~~response.subscribers; let count = ~~response.subscribers;
var message = null; let message = null;
if(count === 1) { if(count === 1) {
message = ( message = (
@ -116,12 +94,12 @@ define(
} }
}; };
var bulk_actions = [ const bulk_actions = [
{ {
name: 'moveToList', name: 'moveToList',
label: 'Move to list...', label: 'Move to list...',
onSelect: function() { onSelect: function() {
var field = { let field = {
id: 'move_to_segment', id: 'move_to_segment',
endpoint: 'segments' endpoint: 'segments'
}; };
@ -147,7 +125,7 @@ define(
name: 'addToList', name: 'addToList',
label: 'Add to list...', label: 'Add to list...',
onSelect: function() { onSelect: function() {
var field = { let field = {
id: 'add_to_segment', id: 'add_to_segment',
endpoint: 'segments' endpoint: 'segments'
}; };
@ -173,7 +151,7 @@ define(
name: 'removeFromList', name: 'removeFromList',
label: 'Remove from list...', label: 'Remove from list...',
onSelect: function() { onSelect: function() {
var field = { let field = {
id: 'remove_from_segment', id: 'remove_from_segment',
endpoint: 'segments' endpoint: 'segments'
}; };
@ -228,16 +206,16 @@ define(
} }
]; ];
var SubscriberList = React.createClass({ const SubscriberList = React.createClass({
renderItem: function(subscriber, actions) { renderItem: function(subscriber, actions) {
var row_classes = classNames( let row_classes = classNames(
'manage-column', 'manage-column',
'column-primary', 'column-primary',
'has-row-actions', 'has-row-actions',
'column-username' 'column-username'
); );
var status = ''; let status = '';
switch(subscriber.status) { switch(subscriber.status) {
case 'subscribed': case 'subscribed':
@ -253,13 +231,13 @@ define(
break; break;
} }
var segments = mailpoet_segments.filter(function(segment) { let segments = mailpoet_segments.filter(function(segment) {
return (jQuery.inArray(segment.id, subscriber.segments) !== -1); return (jQuery.inArray(segment.id, subscriber.segments) !== -1);
}).map(function(segment) { }).map(function(segment) {
return segment.name; return segment.name;
}).join(', '); }).join(', ');
var avatar = false; let avatar = false;
if(subscriber.avatar_url) { if(subscriber.avatar_url) {
avatar = ( avatar = (
<img <img
@ -275,18 +253,14 @@ define(
return ( return (
<div> <div>
<td className={ row_classes }> <td className={ row_classes }>
{ avatar }
<strong><Link to={ `/edit/${ subscriber.id }` }> <strong><Link to={ `/edit/${ subscriber.id }` }>
{ subscriber.email } { subscriber.email }
</Link></strong> </Link></strong>
<p style={{margin: 0}}>
{ subscriber.first_name } { subscriber.last_name }
</p>
{ actions } { actions }
</td> </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"> <td className="column" data-colname="Status">
{ status } { status }
</td> </td>
@ -310,6 +284,7 @@ define(
</h2> </h2>
<Listing <Listing
location={ this.props.location }
params={ this.props.params } params={ this.props.params }
endpoint="subscribers" endpoint="subscribers"
onRenderItem={ this.renderItem } onRenderItem={ this.renderItem }
@ -318,10 +293,8 @@ define(
messages={ messages } messages={ messages }
/> />
</div> </div>
); )
} }
}); });
return SubscriberList; 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()');
} }
} }