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

View File

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

View File

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

View File

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

View File

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

View File

@@ -233,7 +233,8 @@ define(
var row_classes = classNames(
'manage-column',
'column-primary',
'has-row-actions'
'has-row-actions',
'column-username'
);
var status = '';
@@ -258,9 +259,23 @@ define(
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>
@@ -295,6 +310,7 @@ define(
</h2>
<Listing
params={ this.props.params }
endpoint="subscribers"
onRenderItem={ this.renderItem }
columns={ columns }

View File

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

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",
"This file is @generated automatically"
],
"hash": "d72500818c46823a667d76239af98609",
"hash": "92704d2679fce692438b9e6f1dc6e02f",
"content-hash": "3297411fcec47a02bc4f456fbf3751d1",
"packages": [
{
@@ -1274,16 +1274,16 @@
},
{
"name": "phpunit/phpunit",
"version": "4.8.14",
"version": "4.8.16",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "b4900675926860bef091644849305399b986efa2"
"reference": "625f8c345606ed0f3a141dfb88f4116f0e22978e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b4900675926860bef091644849305399b986efa2",
"reference": "b4900675926860bef091644849305399b986efa2",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/625f8c345606ed0f3a141dfb88f4116f0e22978e",
"reference": "625f8c345606ed0f3a141dfb88f4116f0e22978e",
"shasum": ""
},
"require": {
@@ -1342,7 +1342,7 @@
"testing",
"xunit"
],
"time": "2015-10-17 15:03:30"
"time": "2015-10-23 06:48:33"
},
{
"name": "phpunit/phpunit-mock-objects",

View File

@@ -66,7 +66,7 @@ class Handler {
function getSelection() {
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;
}

View File

@@ -39,10 +39,10 @@ class Newsletter extends Model {
'label' => __('All lists'),
'value' => ''
);
foreach($segments as $segment) {
$newsletters_count = $segment->newsletters()->count();
if($newsletters_count > 0) {
$segment_list[] = array(
'label' => sprintf('%s (%d)', $segment->name, $newsletters_count),
'value' => $segment->id()
@@ -51,34 +51,21 @@ class Newsletter extends Model {
}
$filters = array(
array(
'name' => 'segment',
'options' => $segment_list
)
'segment' => $segment_list
);
return $filters;
}
static function filterBy($orm, $filters = null) {
if(empty($filters)) {
if(empty($filters)) {
return $orm;
}
foreach($filters as $filter) {
if($filter['name'] === 'segment') {
$segment = Segment::findOne($filter['value']);
foreach($filters as $key => $value) {
if($key === 'segment') {
$segment = Segment::findOne($value);
if($segment !== false) {
$orm = $orm
->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']);
$orm = $segment->newsletters();
}
}
}
@@ -112,12 +99,21 @@ class Newsletter extends Model {
array(
'name' => '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()) {
@@ -148,8 +144,45 @@ class Newsletter extends Model {
return false;
}
static function trash($listing) {
return $listing->getSelection()
->deleteMany();
static function trash($listing, $data = array()) {
$confirm_delete = filter_var($data['confirm'], FILTER_VALIDATE_BOOLEAN);
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() {
$segments = Segment::orderByAsc('name')->findMany();
$segment_list = array();
$segment_list[] = array(
'label' => __('All lists'),
'value' => ''
@@ -53,10 +52,7 @@ class Subscriber extends Model {
}
$filters = array(
array(
'name' => 'segment',
'options' => $segment_list
)
'segment' => $segment_list
);
return $filters;
@@ -66,10 +62,9 @@ class Subscriber extends Model {
if(empty($filters)) {
return $orm;
}
foreach($filters as $filter) {
if($filter['name'] === 'segment') {
$segment = Segment::findOne($filter['value']);
foreach($filters as $key => $value) {
if($key === 'segment') {
$segment = Segment::findOne($value);
if($segment !== false) {
$orm = $segment->subscribers();
}
@@ -197,10 +192,9 @@ class Subscriber extends Model {
static function moveToList($listing, $data = array()) {
$segment_id = (isset($data['segment_id']) ? (int)$data['segment_id'] : 0);
$segment = Segment::findOne($segment_id);
if($segment !== false) {
$subscribers_count = 0;
$subscribers = $listing->getSelection()->findMany();
$subscribers = $listing->getSelection()->findResultSet();
foreach($subscribers as $subscriber) {
// remove subscriber from all segments
SubscriberSegment::where('subscriber_id', $subscriber->id)->deleteMany();

View File

@@ -60,10 +60,28 @@ class Newsletters {
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);
if($newsletter !== false) {
$result = $newsletter->delete();
$newsletter->set_expr('deleted_at', 'NULL');
$result = array('newsletters' => (int)$newsletter->save());
} else {
$result = false;
}

View File

@@ -58,7 +58,7 @@ class Segments {
$item = array_merge($item, $stats);
$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
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')
->where('subscriber_id', $item['id'])
->findMany();

View File

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