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
opacity: 0.2
@ -8,6 +8,20 @@
.mailpoet_select_all td
text-align: center
table.widefat thead .check-column,
table.widefat tfoot .check-column
.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

@ -89,7 +89,12 @@ define(
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

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

View File

@ -282,42 +282,100 @@ define(
};
},
componentDidUpdate: function(prevProps, prevState) {
// set group to "all" if trash gets emptied
// 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');
}
},
componentDidMount: function() {
if(this.isMounted()) {
var state = this.state || {};
var params = this.props.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;
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 = filter;
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() {
if(this.isMounted()) {
const params = this.props.params || {}
this.initWithParams(params)
}
},
componentWillReceiveProps: function(nextProps) {
const params = nextProps.params || {}
//this.initWithParams(params)
},
getItems: function() {
if(this.isMounted()) {
@ -443,6 +501,7 @@ define(
selection: false,
selected_ids: []
}, function() {
this.setParams();
this.getItems();
}.bind(this));
},
@ -451,6 +510,7 @@ define(
sort_by: sort_by,
sort_order: sort_order,
}, function() {
this.setParams();
this.getItems();
}.bind(this));
},
@ -510,6 +570,7 @@ define(
filter: filters,
page: 1
}, function() {
this.setParams();
this.getItems();
}.bind(this));
},
@ -519,10 +580,11 @@ define(
this.setState({
group: group,
filter: [],
filter: {},
search: '',
page: 1
}, function() {
this.setParams();
this.getItems();
}.bind(this));
},
@ -532,6 +594,7 @@ define(
selection: false,
selected_ids: []
}, function() {
this.setParams();
this.getItems();
}.bind(this));
},
@ -571,7 +634,8 @@ 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',
@ -622,7 +686,7 @@ define(
limit={ this.state.limit }
onSetPage={ this.handleSetPage } />
</div>
<table className={ tableClasses }>
<table className={ table_classes }>
<thead>
<ListingHeader
onSort={ this.handleSort }

View File

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

View File

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

View File

@ -34,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,19 +1,13 @@
define(
[
'react',
'react-router',
'listing/listing.jsx',
'classnames',
'mailpoet'
],
function(
React,
Router,
Listing,
classNames,
MailPoet
) {
var columns = [
import React from 'react'
import { Router, Route, Link } from 'react-router'
import jQuery from 'jquery'
import MailPoet from 'mailpoet'
import classNames from 'classnames'
import Listing from 'listing/listing.jsx'
var columns = [
{
name: 'name',
label: 'Name',
@ -44,9 +38,9 @@ define(
label: 'Created on',
sortable: true
}
];
];
var messages = {
var messages = {
onDelete: function(response) {
var count = ~~response.segments;
var message = null;
@ -101,10 +95,9 @@ define(
MailPoet.Notice.success(message);
}
}
};
};
var Link = Router.Link;
var item_actions = [
var item_actions = [
{
name: 'edit',
link: function(item) {
@ -144,9 +137,9 @@ define(
);
}
}
];
];
var bulk_actions = [
var bulk_actions = [
{
name: 'trash',
label: 'Trash',
@ -157,11 +150,9 @@ define(
},
onSuccess: messages.onDelete
}
];
];
var Link = Router.Link;
var SegmentList = React.createClass({
var SegmentList = React.createClass({
renderItem: function(segment, actions) {
var rowClasses = classNames(
'manage-column',
@ -203,6 +194,8 @@ define(
</h2>
<Listing
location={ this.props.location }
params={ this.props.params }
messages={ messages }
search={ false }
limit={ 1000 }
@ -215,8 +208,6 @@ define(
</div>
);
}
});
});
return SegmentList;
}
);
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,39 +1,17 @@
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 = [
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',
label: 'Email',
sortable: true
},
{
name: 'first_name',
label: 'Firstname',
sortable: true
},
{
name: 'last_name',
label: 'Lastname',
label: 'Subscriber',
sortable: true
},
{
@ -57,12 +35,12 @@ define(
label: 'Last modified on',
sortable: true
},
];
];
var messages = {
const messages = {
onDelete: function(response) {
var count = ~~response.subscribers;
var message = null;
let count = ~~response.subscribers;
let message = null;
if(count === 1) {
message = (
@ -79,8 +57,8 @@ define(
}
},
onConfirmDelete: function(response) {
var count = ~~response.subscribers;
var message = null;
let count = ~~response.subscribers;
let message = null;
if(count === 1) {
message = (
@ -97,8 +75,8 @@ define(
}
},
onRestore: function(response) {
var count = ~~response.subscribers;
var message = null;
let count = ~~response.subscribers;
let message = null;
if(count === 1) {
message = (
@ -114,14 +92,14 @@ define(
MailPoet.Notice.success(message);
}
}
};
};
var bulk_actions = [
const bulk_actions = [
{
name: 'moveToList',
label: 'Move to list...',
onSelect: function() {
var field = {
let field = {
id: 'move_to_segment',
endpoint: 'segments'
};
@ -147,7 +125,7 @@ define(
name: 'addToList',
label: 'Add to list...',
onSelect: function() {
var field = {
let field = {
id: 'add_to_segment',
endpoint: 'segments'
};
@ -173,7 +151,7 @@ define(
name: 'removeFromList',
label: 'Remove from list...',
onSelect: function() {
var field = {
let field = {
id: 'remove_from_segment',
endpoint: 'segments'
};
@ -226,18 +204,18 @@ define(
},
onSuccess: messages.onDelete
}
];
];
var SubscriberList = React.createClass({
const SubscriberList = React.createClass({
renderItem: function(subscriber, actions) {
var row_classes = classNames(
let row_classes = classNames(
'manage-column',
'column-primary',
'has-row-actions',
'column-username'
);
var status = '';
let status = '';
switch(subscriber.status) {
case 'subscribed':
@ -253,13 +231,13 @@ define(
break;
}
var segments = mailpoet_segments.filter(function(segment) {
let segments = mailpoet_segments.filter(function(segment) {
return (jQuery.inArray(segment.id, subscriber.segments) !== -1);
}).map(function(segment) {
return segment.name;
}).join(', ');
var avatar = false;
let avatar = false;
if(subscriber.avatar_url) {
avatar = (
<img
@ -275,18 +253,14 @@ define(
return (
<div>
<td className={ row_classes }>
{ avatar }
<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="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>
@ -310,6 +284,7 @@ define(
</h2>
<Listing
location={ this.props.location }
params={ this.props.params }
endpoint="subscribers"
onRenderItem={ this.renderItem }
@ -318,10 +293,8 @@ define(
messages={ messages }
/>
</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 createHashHistory from 'history/lib/createHashHistory'
let history = createHashHistory({ queryKey: false })
const history = createHashHistory({ queryKey: false })
const App = React.createClass({
render() {
@ -13,7 +13,7 @@ const App = React.createClass({
}
});
let container = document.getElementById('subscribers_container');
const container = document.getElementById('subscribers_container')
if(container) {
ReactDOM.render((
@ -22,7 +22,6 @@ if(container) {
<IndexRoute component={ SubscriberList } />
<Route path="new" component={ SubscriberForm } />
<Route path="edit/:id" component={ SubscriberForm } />
<Route path="filter[:filter]" component={ SubscriberList } />
<Route path="*" component={ SubscriberList } />
</Route>
</Router>

View File

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