listing improvements

- added Trash group
- added soft delete for subscribers
- added restore/delete permanently features (including bulk actions and row actions)
- listing bugfixes
This commit is contained in:
Jonathan Labreuille
2015-10-21 18:18:34 +02:00
parent b5834828a2
commit dcb094fcd1
6 changed files with 244 additions and 60 deletions

View File

@@ -5,8 +5,10 @@ define(['react', 'classnames'], function(React, classNames) {
return this.props.onSelectGroup(group);
},
render: function() {
var count = this.props.groups.length;
var groups = this.props.groups.map(function(group, index) {
if(group.name === 'trash' && group.count === 0) {
return false;
}
var classes = classNames(
{ 'current' : (group.name === this.props.group) }
@@ -14,12 +16,13 @@ define(['react', 'classnames'], function(React, classNames) {
return (
<li key={index}>
{(index > 0) ? ' |' : ''}
<a
href="javascript:;"
className={classes}
onClick={this.handleSelect.bind(this, group.name)} >
{group.label} <span className="count">({ group.count })</span>
</a>{(index < (count - 1)) ? ' | ' : ''}
</a>
</li>
);
}.bind(this));

View File

@@ -43,8 +43,11 @@ define(
return !e.target.checked;
},
handleDeleteItem: function(id) {
this.props.onDeleteItem(id);
handleRestoreItem: function(id) {
this.props.onRestoreItem(id);
},
handleDeleteItem: function(id, confirm = false) {
this.props.onDeleteItem(id, confirm);
},
handleToggleItem: function(id) {
this.setState({ toggled: !this.state.toggled });
@@ -89,6 +92,40 @@ define(
);
}
if(this.props.group === 'trash') {
var actions = (
<div>
<div className="row-actions">
<span>
<a
href="javascript:;"
onClick={ this.handleRestoreItem.bind(
null,
this.props.item.id
)}
>Restore</a>
</span>
{ ' | ' }
<span className="delete">
<a
className="submitdelete"
href="javascript:;"
onClick={ this.handleDeleteItem.bind(
null,
this.props.item.id,
true
)}
>Delete permanently</a>
</span>
</div>
<button
onClick={ this.handleToggleItem.bind(null, this.props.item.id) }
className="toggle-row" type="button">
<span className="screen-reader-text">Show more details</span>
</button>
</div>
);
} else {
var actions = (
<div>
<div className="row-actions">
@@ -97,7 +134,11 @@ define(
<span className="trash">
<a
href="javascript:;"
onClick={ this.handleDeleteItem.bind(null, this.props.item.id) }>
onClick={ this.handleDeleteItem.bind(
null,
this.props.item.id,
false
) }>
Trash
</a>
</span>
@@ -109,6 +150,7 @@ define(
</button>
</div>
);
}
var row_classes = classNames({ 'is-expanded': !this.state.toggled })
@@ -190,9 +232,11 @@ define(
onSelectItem={ this.props.onSelectItem }
onRenderItem={ this.props.onRenderItem }
onDeleteItem={ this.props.onDeleteItem }
onRestoreItem={ this.props.onRestoreItem }
selection={ this.props.selection }
is_selectable={ this.props.is_selectable }
item_actions={ this.props.item_actions }
group={ this.props.group }
key={ 'item-' + item.id }
item={ item } />
);
@@ -254,7 +298,21 @@ define(
}
}.bind(this));
},
handleDeleteItem: function(id) {
handleRestoreItem: function(id) {
this.setState({
loading: true,
page: 1
});
MailPoet.Ajax.post({
endpoint: this.props.endpoint,
action: 'restore',
data: id
}).done(function() {
this.getItems();
}.bind(this));
},
handleDeleteItem: function(id, confirm = false) {
this.setState({
loading: true,
page: 1
@@ -263,7 +321,10 @@ define(
MailPoet.Ajax.post({
endpoint: this.props.endpoint,
action: 'delete',
data: id
data: {
id: id,
confirm: confirm
}
}).done(function() {
this.getItems();
}.bind(this));
@@ -414,6 +475,22 @@ define(
// bulk actions
var bulk_actions = this.props.bulk_actions || [];
if(this.state.group === 'trash') {
bulk_actions = [
{
name: 'restore',
label: 'Restore'
},
{
name: 'trash',
label: 'Delete permanently',
getData: function() {
return { confirm: true };
}
}
];
}
// item actions
var item_actions = this.props.item_actions || [];
@@ -464,6 +541,7 @@ define(
<ListingItems
onRenderItem={ this.handleRenderItem }
onDeleteItem={ this.handleDeleteItem }
onRestoreItem={ this.handleRestoreItem }
columns={ this.props.columns }
is_selectable={ bulk_actions.length > 0 }
onSelectItem={ this.handleSelectItem }
@@ -471,6 +549,7 @@ define(
selection={ this.state.selection }
selected_ids={ this.state.selected_ids }
loading={ this.state.loading }
group={ this.state.group }
count={ this.state.count }
limit={ this.state.limit }
item_actions={ item_actions }

View File

@@ -117,6 +117,14 @@ define(
}
}
},
{
name: 'removeFromAllLists',
label: 'Remove from all lists'
},
{
name: 'confirmUnconfirmed',
label: 'Confirm unconfirmed'
},
{
name: 'trash',
label: 'Trash'
@@ -153,14 +161,16 @@ define(
return segment.name;
}).join(', ');
var row_actions = false;
return (
<div>
<td className={ row_classes }>
<strong>
<Link to={ `/edit/${ subscriber.id }` }>
<strong><Link to={ `/edit/${ subscriber.id }` }>
{ subscriber.email }
</Link>
</strong>
</Link></strong>
{ actions }
</td>
<td className="column" data-colname="First name">

View File

@@ -54,6 +54,7 @@ class Migrator {
'email varchar(150) NOT NULL,',
'status varchar(12) NOT NULL DEFAULT "unconfirmed",',
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
'deleted_at TIMESTAMP NULL DEFAULT NULL,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'UNIQUE KEY email (email)'
@@ -82,6 +83,7 @@ class Migrator {
'preheader varchar(250) NOT NULL,',
'body longtext,',
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
'deleted_at TIMESTAMP NULL DEFAULT NULL,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id)'
);
@@ -106,6 +108,7 @@ class Migrator {
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'name varchar(90) NOT NULL,',
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
'deleted_at TIMESTAMP NULL DEFAULT NULL,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'UNIQUE KEY name (name)'
@@ -170,8 +173,7 @@ class Migrator {
'newsletter_type varchar(90) NOT NULL,',
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'UNIQUE KEY name_newsletter_type (newsletter_type, name)'
'PRIMARY KEY (id)'
);
return $this->sqlify(__FUNCTION__, $attributes);
}

View File

@@ -69,18 +69,8 @@ class Subscriber extends Model {
foreach($filters as $filter) {
if($filter['name'] === 'segment') {
$segment = Segment::findOne($filter['value']);
if($segment !== false) {
/*$orm = $orm
->select(MP_SUBSCRIBERS_TABLE.'.*')
->select('subscriber_segment.id', 'subscriber_segment_id')
->join(
MP_SUBSCRIBER_SEGMENT_TABLE,
MP_SUBSCRIBERS_TABLE.'.id = subscriber_segment.subscriber_id',
'subscriber_segment'
)
->where('subscriber_segment.segment_id', (int)$filter['value']);*/
$orm = $segment->subscribers();
}
}
@@ -93,36 +83,48 @@ class Subscriber extends Model {
array(
'name' => 'all',
'label' => __('All'),
'count' => Subscriber::count()
'count' => Subscriber::whereNull('deleted_at')->count()
),
array(
'name' => 'subscribed',
'label' => __('Subscribed'),
'count' => Subscriber::where('status', 'subscribed')->count()
'count' => Subscriber::whereNull('deleted_at')
->where('status', 'subscribed')
->count()
),
array(
'name' => 'unconfirmed',
'label' => __('Unconfirmed'),
'count' => Subscriber::where('status', 'unconfirmed')->count()
'count' => Subscriber::whereNull('deleted_at')
->where('status', 'unconfirmed')
->count()
),
array(
'name' => 'unsubscribed',
'label' => __('Unsubscribed'),
'count' => Subscriber::where('status', 'unsubscribed')->count()
'count' => Subscriber::whereNull('deleted_at')
->where('status', 'unsubscribed')
->count()
),
array(
'name' => 'trash',
'label' => __('Trash'),
'count' => Subscriber::whereNotNull('deleted_at')->count()
)
);
}
static function groupBy($orm, $group = null) {
if($group === null or !in_array(
$group,
array('subscribed', 'unconfirmed', 'unsubscribed')
)) {
return $orm;
}
if($group === 'trash') {
return $orm->whereNotNull('deleted_at');
} else {
$orm = $orm->whereNull('deleted_at');
if(in_array($group, array('subscribed', 'unsubscribed', 'unconfirmed'))) {
return $orm->where('status', $group);
}
}
}
static function filterWithCustomFields($orm) {
$orm = $orm->select(MP_SUBSCRIBERS_TABLE.'.*');
@@ -228,6 +230,54 @@ class Subscriber extends Model {
return false;
}
static function removeFromAllLists($listing) {
$segments = Segment::findMany();
$segment_ids = array_map(function($segment) {
return $segment->id();
}, $segments);
if(!empty($segment_ids)) {
// delete relations with segment
$subscriber_ids = $listing->getSelectionIds();
SubscriberSegment::whereIn('subscriber_id', $subscriber_ids)
->whereIn('segment_id', $segment_ids)
->deleteMany();
return true;
}
return false;
}
static function confirmUnconfirmed($listing) {
$subscriber_ids = $listing->getSelectionIds();
$subscribers = Subscriber::whereIn('id', $subscriber_ids)
->where('status', 'unconfirmed')
->findMany();
if(!empty($subscribers)) {
foreach($subscribers as $subscriber) {
$subscriber->set('status', 'subscribed');
$subscriber->save();
}
return true;
}
return false;
}
static function resendConfirmationEmail($listing) {
$subscriber_ids = $listing->getSelectionIds();
$subscribers = Subscriber::whereIn('id', $subscriber_ids)
->where('status', 'unconfirmed')
->findMany();
if(!empty($subscribers)) {
foreach($subscribers as $subscriber) {
// TODO: resend confirmation email
}
return true;
}
return false;
}
static function addToList($listing, $data = array()) {
$segment_id = (isset($data['segment_id']) ? (int)$data['segment_id'] : 0);
$segment = Segment::findOne($segment_id);
@@ -246,11 +296,35 @@ class Subscriber extends Model {
return false;
}
static function trash($listing) {
static function trash($listing, $data = array()) {
if(isset($data['confirm']) && (bool)$data['confirm'] === true) {
// delete relations with all segments
$subscriber_ids = $listing->getSelectionIds();
\MailPoet\Models\SubscriberSegment::whereIn('subscriber_id', $subscriber_ids)->deleteMany();
$subscribers = $listing->getSelection()->findMany();
return $listing->getSelection()->deleteMany();
if(!empty($subscribers)) {
foreach($subscribers as $subscriber) {
$subscriber->delete();
}
return true;
}
return false;
} else {
// soft delete
$subscribers = $listing->getSelection()->findResultSet();
if(!empty($subscribers)) {
$subscribers->set_expr('deleted_at', 'NOW()');
return $subscribers->save();
}
return false;
}
}
static function restore($listing, $data = array()) {
$subscribers = $listing->getSelection()->findResultSet();
if(!empty($subscribers)) {
$subscribers->set_expr('deleted_at', 'NULL');
return $subscribers->save();
}
return false;
}
}

View File

@@ -53,10 +53,26 @@ class Subscribers {
wp_send_json($result);
}
function delete($id) {
function restore($id) {
$subscriber = Subscriber::findOne($id);
if($subscriber !== false) {
$subscriber->set_expr('deleted_at', 'NULL');
$result = $subscriber->save();
} else {
$result = false;
}
wp_send_json($result);
}
function delete($data = array()) {
$subscriber = Subscriber::findOne($data['id']);
if($subscriber !== false) {
if(isset($data['confirm']) && (bool)$data['confirm'] === true) {
$result = $subscriber->delete();
} else {
$subscriber->set_expr('deleted_at', 'NOW()');
$result = $subscriber->save();
}
} else {
$result = false;
}