lotta fixes for filtering + listing

This commit is contained in:
Jonathan Labreuille
2015-10-26 18:23:32 +01:00
parent 505b979ac5
commit 13dc3577f1
16 changed files with 290 additions and 116 deletions

View File

@@ -6,38 +6,48 @@ function(
) { ) {
var ListingFilters = React.createClass({ var ListingFilters = React.createClass({
handleFilterAction: function() { handleFilterAction: function() {
var filters = this.props.filters.map(function(filter, index) { var filters = this.props.filters;
var value = this.refs['filter-'+index].value; var selected_filters = Object.keys(filters)
if(value) { .map(function(filter, index) {
return { var value = this.refs.filter.value;
'name': filter.name, if(value) {
'value': value var output = {};
}; output[filter] = value;
} return output;
}.bind(this)); }
return this.props.onSelectFilter(filters); }.bind(this)
);
return this.props.onSelectFilter(selected_filters);
}, },
handleChangeAction: function() { handleChangeAction: function() {
return true; return this.refs.filter.value;
}, },
render: function() { render: function() {
var filters = this.props.filters var filters = this.props.filters;
var selected_filters = this.props.filter;
var available_filters = Object.keys(filters)
.filter(function(filter) { .filter(function(filter) {
return !( return !(
filter.options.length === 0 filters[filter].length === 0
|| ( || (
filter.options.length === 1 filters[filter].length === 1
&& !filter.options[0].value && !filters[filter][0].value
) )
); );
}) })
.map(function(filter, i) { .map(function(filter, i) {
var defaultValue = false;
if(selected_filters[filter] !== undefined) {
defaultValue = selected_filters[filter];
}
return ( return (
<select <select
ref={ 'filter-'+i } ref={ 'filter' }
key={ 'filter-'+i } key={ 'filter-'+i }
defaultValue={ defaultValue }
onChange={ this.handleChangeAction }> onChange={ this.handleChangeAction }>
{ filter.options.map(function(option, j) { { filters[filter].map(function(option, j) {
return ( return (
<option <option
value={ option.value } value={ option.value }
@@ -51,7 +61,7 @@ function(
var button = false; var button = false;
if(filters.length > 0) { if(available_filters.length > 0) {
button = ( button = (
<input <input
onClick={ this.handleFilterAction } onClick={ this.handleFilterAction }
@@ -63,7 +73,7 @@ function(
return ( return (
<div className="alignleft actions actions"> <div className="alignleft actions actions">
{ filters } { available_filters }
{ button } { button }
</div> </div>
); );

View File

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

View File

@@ -77,12 +77,23 @@ define(
if(custom_actions.length > 0) { if(custom_actions.length > 0) {
item_actions = custom_actions.map(function(action, index) { item_actions = custom_actions.map(function(action, index) {
return ( if(action.refresh) {
<span key={ 'action-'+index } className={ action.name }> return (
{ action.link(this.props.item) } <span
{(index < (custom_actions.length - 1)) ? ' | ' : ''} onClick={ this.props.onRefreshItems }
</span> key={ 'action-'+index } className={ action.name }>
); { action.link(this.props.item) }
{(index < (custom_actions.length - 1)) ? ' | ' : ''}
</span>
);
} else {
return (
<span key={ 'action-'+index } className={ action.name }>
{ action.link(this.props.item) }
{(index < (custom_actions.length - 1)) ? ' | ' : ''}
</span>
);
}
}.bind(this)); }.bind(this));
} else { } else {
item_actions = ( item_actions = (
@@ -233,6 +244,7 @@ define(
onRenderItem={ this.props.onRenderItem } onRenderItem={ this.props.onRenderItem }
onDeleteItem={ this.props.onDeleteItem } onDeleteItem={ this.props.onDeleteItem }
onRestoreItem={ this.props.onRestoreItem } onRestoreItem={ this.props.onRestoreItem }
onRefreshItems={ this.props.onRefreshItems }
selection={ this.props.selection } selection={ this.props.selection }
is_selectable={ this.props.is_selectable } is_selectable={ this.props.is_selectable }
item_actions={ this.props.item_actions } item_actions={ this.props.item_actions }
@@ -248,6 +260,9 @@ define(
}); });
var Listing = React.createClass({ var Listing = React.createClass({
mixins: [
Router.History
],
getInitialState: function() { getInitialState: function() {
return { return {
loading: false, loading: false,
@@ -260,8 +275,8 @@ define(
items: [], items: [],
groups: [], groups: [],
group: 'all', group: 'all',
filters: [], filters: {},
filter: [], filter: {},
selected_ids: [], selected_ids: [],
selection: false selection: false
}; };
@@ -277,44 +292,61 @@ define(
} }
}, },
componentDidMount: function() { componentDidMount: function() {
if(this.props.limit !== undefined) { if(this.isMounted()) {
this.setState({ var state = this.state || {};
limit: Math.abs(~~this.props.limit) var params = this.props.params || {};
}, function() {
// 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(); this.getItems();
}.bind(this)); }.bind(this));
} else {
this.getItems();
} }
}, },
getItems: function() { getItems: function() {
this.setState({ loading: true }); if(this.isMounted()) {
this.setState({ loading: true });
this.clearSelection(); this.clearSelection();
MailPoet.Ajax.post({ MailPoet.Ajax.post({
endpoint: this.props.endpoint, endpoint: this.props.endpoint,
action: 'listing', action: 'listing',
data: { data: {
offset: (this.state.page - 1) * this.state.limit, offset: (this.state.page - 1) * this.state.limit,
limit: this.state.limit, limit: this.state.limit,
group: this.state.group, group: this.state.group,
filter: this.state.filter, filter: this.state.filter,
search: this.state.search, search: this.state.search,
sort_by: this.state.sort_by, sort_by: this.state.sort_by,
sort_order: this.state.sort_order sort_order: this.state.sort_order
} }
}).done(function(response) { }).done(function(response) {
if(this.isMounted()) {
this.setState({ this.setState({
items: response.items || [], items: response.items || [],
filters: response.filters || [], filters: response.filters || {},
groups: response.groups || [], groups: response.groups || [],
count: response.count || 0, count: response.count || 0,
loading: false loading: false
}); });
} }.bind(this));
}.bind(this)); }
}, },
handleRestoreItem: function(id) { handleRestoreItem: function(id) {
this.setState({ this.setState({
@@ -507,17 +539,14 @@ define(
var render = this.props.onRenderItem(item, actions); var render = this.props.onRenderItem(item, actions);
return render.props.children; return render.props.children;
}, },
handleRefreshItems: function() {
this.getItems();
},
render: function() { render: function() {
var items = this.state.items, var items = this.state.items,
sort_by = this.state.sort_by, sort_by = this.state.sort_by,
sort_order = this.state.sort_order; sort_order = this.state.sort_order;
// set sortable columns
columns = this.props.columns.map(function(column) {
column.sorted = (column.name === sort_by) ? sort_order : false;
return column;
});
// bulk actions // bulk actions
var bulk_actions = this.props.bulk_actions || []; var bulk_actions = this.props.bulk_actions || [];
@@ -573,9 +602,6 @@ define(
groups = false; groups = false;
} }
// filters
var filter = this.state.filter;
return ( return (
<div> <div>
{ groups } { groups }
@@ -588,7 +614,7 @@ define(
onBulkAction={ this.handleBulkAction } /> onBulkAction={ this.handleBulkAction } />
<ListingFilters <ListingFilters
filters={ this.state.filters } filters={ this.state.filters }
filter={ filter } filter={ this.state.filter }
onSelectFilter={ this.handleFilter } /> onSelectFilter={ this.handleFilter } />
<ListingPages <ListingPages
count={ this.state.count } count={ this.state.count }
@@ -612,6 +638,7 @@ define(
onRenderItem={ this.handleRenderItem } onRenderItem={ this.handleRenderItem }
onDeleteItem={ this.handleDeleteItem } onDeleteItem={ this.handleDeleteItem }
onRestoreItem={ this.handleRestoreItem } onRestoreItem={ this.handleRestoreItem }
onRefreshItems={ this.handleRefreshItems }
columns={ this.props.columns } columns={ this.props.columns }
is_selectable={ bulk_actions.length > 0 } is_selectable={ bulk_actions.length > 0 }
onSelectItem={ this.handleSelectItem } onSelectItem={ this.handleSelectItem }

View File

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

View File

@@ -25,6 +25,7 @@ if(container) {
<Route path="new" component={ NewsletterTypes } /> <Route path="new" component={ NewsletterTypes } />
<Route path="new/:type" component={ NewsletterTemplates } /> <Route path="new/:type" component={ NewsletterTemplates } />
<Route path="send/:id" component={ NewsletterSend } /> <Route path="send/:id" component={ NewsletterSend } />
<Route path="filter[:filter]" component={ NewsletterList } />
<Route path="*" component={ NewsletterList } /> <Route path="*" component={ NewsletterList } />
</Route> </Route>
</Router> </Router>

View File

@@ -115,6 +115,7 @@ define(
}, },
{ {
name: 'duplicate_segment', name: 'duplicate_segment',
refresh: true,
link: function(item) { link: function(item) {
return ( return (
<a <a

View File

@@ -233,7 +233,8 @@ define(
var row_classes = classNames( var row_classes = classNames(
'manage-column', 'manage-column',
'column-primary', 'column-primary',
'has-row-actions' 'has-row-actions',
'column-username'
); );
var status = ''; var status = '';
@@ -258,9 +259,23 @@ define(
return segment.name; return segment.name;
}).join(', '); }).join(', ');
var avatar = false;
if(subscriber.avatar_url) {
avatar = (
<img
className="avatar"
src={ subscriber.avatar_url }
title=""
width="32"
height="32"
/>
);
}
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>
@@ -295,6 +310,7 @@ define(
</h2> </h2>
<Listing <Listing
params={ this.props.params }
endpoint="subscribers" endpoint="subscribers"
onRenderItem={ this.renderItem } onRenderItem={ this.renderItem }
columns={ columns } columns={ columns }

View File

@@ -22,6 +22,7 @@ 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>

12
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"hash": "d72500818c46823a667d76239af98609", "hash": "92704d2679fce692438b9e6f1dc6e02f",
"content-hash": "3297411fcec47a02bc4f456fbf3751d1", "content-hash": "3297411fcec47a02bc4f456fbf3751d1",
"packages": [ "packages": [
{ {
@@ -1274,16 +1274,16 @@
}, },
{ {
"name": "phpunit/phpunit", "name": "phpunit/phpunit",
"version": "4.8.14", "version": "4.8.16",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git", "url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "b4900675926860bef091644849305399b986efa2" "reference": "625f8c345606ed0f3a141dfb88f4116f0e22978e"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b4900675926860bef091644849305399b986efa2", "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/625f8c345606ed0f3a141dfb88f4116f0e22978e",
"reference": "b4900675926860bef091644849305399b986efa2", "reference": "625f8c345606ed0f3a141dfb88f4116f0e22978e",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -1342,7 +1342,7 @@
"testing", "testing",
"xunit" "xunit"
], ],
"time": "2015-10-17 15:03:30" "time": "2015-10-23 06:48:33"
}, },
{ {
"name": "phpunit/phpunit-mock-objects", "name": "phpunit/phpunit-mock-objects",

View File

@@ -66,7 +66,7 @@ class Handler {
function getSelection() { function getSelection() {
if(!empty($this->data['selection'])) { if(!empty($this->data['selection'])) {
$this->model->whereIn('id', $this->data['selection']); $this->model->whereIn($this->table_name.'.id', $this->data['selection']);
} }
return $this->model; return $this->model;
} }

View File

@@ -39,10 +39,10 @@ class Newsletter extends Model {
'label' => __('All lists'), 'label' => __('All lists'),
'value' => '' 'value' => ''
); );
foreach($segments as $segment) { foreach($segments as $segment) {
$newsletters_count = $segment->newsletters()->count(); $newsletters_count = $segment->newsletters()->count();
if($newsletters_count > 0) { if($newsletters_count > 0) {
$segment_list[] = array( $segment_list[] = array(
'label' => sprintf('%s (%d)', $segment->name, $newsletters_count), 'label' => sprintf('%s (%d)', $segment->name, $newsletters_count),
'value' => $segment->id() 'value' => $segment->id()
@@ -51,34 +51,21 @@ class Newsletter extends Model {
} }
$filters = array( $filters = array(
array( 'segment' => $segment_list
'name' => 'segment',
'options' => $segment_list
)
); );
return $filters; return $filters;
} }
static function filterBy($orm, $filters = null) { static function filterBy($orm, $filters = null) {
if(empty($filters)) { if(empty($filters)) {
return $orm; return $orm;
} }
foreach($filters as $key => $value) {
foreach($filters as $filter) { if($key === 'segment') {
if($filter['name'] === 'segment') { $segment = Segment::findOne($value);
$segment = Segment::findOne($filter['value']);
if($segment !== false) { if($segment !== false) {
$orm = $orm $orm = $segment->newsletters();
->select(MP_NEWSLETTERS_TABLE.'.*')
->select('newsletter_segment.id', 'newsletter_segment_id')
->join(
MP_NEWSLETTER_SEGMENT_TABLE,
MP_NEWSLETTERS_TABLE.'.id = newsletter_segment.newsletter_id',
'newsletter_segment'
)
->where('newsletter_segment.segment_id', (int)$filter['value']);
} }
} }
} }
@@ -112,12 +99,21 @@ class Newsletter extends Model {
array( array(
'name' => 'all', 'name' => 'all',
'label' => __('All'), 'label' => __('All'),
'count' => Newsletter::count() 'count' => Newsletter::whereNull('deleted_at')->count()
),
array(
'name' => 'trash',
'label' => __('Trash'),
'count' => Newsletter::whereNotNull('deleted_at')->count()
) )
); );
} }
static function group($orm, $group = null) { static function groupBy($orm, $group = null) {
if($group === 'trash') {
return $orm->whereNotNull('deleted_at');
}
return $orm->whereNull('deleted_at');
} }
static function createOrUpdate($data = array()) { static function createOrUpdate($data = array()) {
@@ -148,8 +144,45 @@ class Newsletter extends Model {
return false; return false;
} }
static function trash($listing) { static function trash($listing, $data = array()) {
return $listing->getSelection() $confirm_delete = filter_var($data['confirm'], FILTER_VALIDATE_BOOLEAN);
->deleteMany(); if($confirm_delete) {
// delete relations with all segments
$newsletters = $listing->getSelection()->findResultSet();
if(!empty($newsletters)) {
$newsletters_count = 0;
foreach($newsletters as $newsletter) {
if($newsletter->delete()) {
$newsletters_count++;
}
}
return array(
'newsletters' => $newsletters_count
);
}
return false;
} else {
// soft delete
$newsletters = $listing->getSelection()
->findResultSet()
->set_expr('deleted_at', 'NOW()')
->save();
return array(
'newsletters' => $newsletters->count()
);
}
}
static function restore($listing, $data = array()) {
$newsletters = $listing->getSelection()
->findResultSet()
->set_expr('deleted_at', 'NULL')
->save();
return array(
'newsletters' => $newsletters->count()
);
} }
} }

View File

@@ -36,7 +36,6 @@ class Subscriber extends Model {
static function filters() { static function filters() {
$segments = Segment::orderByAsc('name')->findMany(); $segments = Segment::orderByAsc('name')->findMany();
$segment_list = array(); $segment_list = array();
$segment_list[] = array( $segment_list[] = array(
'label' => __('All lists'), 'label' => __('All lists'),
'value' => '' 'value' => ''
@@ -53,10 +52,7 @@ class Subscriber extends Model {
} }
$filters = array( $filters = array(
array( 'segment' => $segment_list
'name' => 'segment',
'options' => $segment_list
)
); );
return $filters; return $filters;
@@ -66,10 +62,9 @@ class Subscriber extends Model {
if(empty($filters)) { if(empty($filters)) {
return $orm; return $orm;
} }
foreach($filters as $key => $value) {
foreach($filters as $filter) { if($key === 'segment') {
if($filter['name'] === 'segment') { $segment = Segment::findOne($value);
$segment = Segment::findOne($filter['value']);
if($segment !== false) { if($segment !== false) {
$orm = $segment->subscribers(); $orm = $segment->subscribers();
} }
@@ -197,10 +192,9 @@ class Subscriber extends Model {
static function moveToList($listing, $data = array()) { static function moveToList($listing, $data = array()) {
$segment_id = (isset($data['segment_id']) ? (int)$data['segment_id'] : 0); $segment_id = (isset($data['segment_id']) ? (int)$data['segment_id'] : 0);
$segment = Segment::findOne($segment_id); $segment = Segment::findOne($segment_id);
if($segment !== false) { if($segment !== false) {
$subscribers_count = 0; $subscribers_count = 0;
$subscribers = $listing->getSelection()->findMany(); $subscribers = $listing->getSelection()->findResultSet();
foreach($subscribers as $subscriber) { foreach($subscribers as $subscriber) {
// remove subscriber from all segments // remove subscriber from all segments
SubscriberSegment::where('subscriber_id', $subscriber->id)->deleteMany(); SubscriberSegment::where('subscriber_id', $subscriber->id)->deleteMany();

View File

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

View File

@@ -58,7 +58,7 @@ class Segments {
$item = array_merge($item, $stats); $item = array_merge($item, $stats);
$item['subscribers_url'] = admin_url( $item['subscribers_url'] = admin_url(
'admin.php?page=mailpoet-subscribers#segment='.$item['id'] 'admin.php?page=mailpoet-subscribers#/filter[segment='.$item['id'].']'
); );
} }

View File

@@ -32,6 +32,12 @@ class Subscribers {
// fetch segments relations for each returned item // fetch segments relations for each returned item
foreach($listing_data['items'] as &$item) { foreach($listing_data['items'] as &$item) {
// avatar
$item['avatar_url'] = get_avatar_url($item['email'], array(
'size' => 32
));
// subscriber's segments
$relations = SubscriberSegment::select('segment_id') $relations = SubscriberSegment::select('segment_id')
->where('subscriber_id', $item['id']) ->where('subscriber_id', $item['id'])
->findMany(); ->findMany();

View File

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