Listing & Form & Refactoring

- finished implementing forms
- fixed form reset when clicking on new
- fixed responsive for select all checkbox and toggle item details
- refactored listing items' actions
- added Trash action
- cleaned up validations on models
- fixed syntax
- fixed tests
This commit is contained in:
Jonathan Labreuille
2015-09-15 16:00:07 +02:00
parent 79f1896cf3
commit 2a4945c956
21 changed files with 196 additions and 183 deletions

View File

@ -1,24 +1,9 @@
.mailpoet_listing_loading tbody tr,
.mailpoet_form_loading tbody tr
opacity: 0.2;
.widefat tfoot td.mailpoet_check_column,
.widefat thead td.mailpoet_check_column
padding-top: 10px;
.widefat tbody th.mailpoet_check_column,
.widefat tfoot td.mailpoet_check_column,
.widefat thead td.mailpoet_check_column
padding: 11px 0 0 3px;
.widefat .mailpoet_check_column
padding: 6px 0 25px;
vertical-align: top;
width: 2.2em;
opacity: 0.2
.mailpoet_select_all
background-color: #f1f1f1
.mailpoet_select_all td
text-align: center

View File

@ -34,7 +34,7 @@ define(
name={ this.props.field.name }
id={ 'field_'+this.props.field.name }
value={ this.props.item[this.props.field.name] }
onChange={ this.props.onValueChange } >
onChange={ this.props.onValueChange }>
{options}
</select>
);
@ -228,6 +228,7 @@ define(
loading: false,
item: {}
});
this.refs.form.getDOMNode().reset();
} else {
this.loadItem(props.params.id);
}
@ -315,6 +316,7 @@ define(
return (
<form
ref="form"
className={ formClasses }
onSubmit={ this.handleSubmit }>

View File

@ -22,7 +22,8 @@ define(['react', 'classnames'], function(React, classNames) {
if(this.props.is_selectable === true) {
checkbox = (
<td className="manage-column column-cb mailpoet_check_column">
<td
className="manage-column column-cb check-column">
<label className="screen-reader-text">
{ 'Select All' }
</label>

View File

@ -3,6 +3,7 @@ define(
'mailpoet',
'jquery',
'react',
'react-router',
'classnames',
'listing/bulk_actions.jsx',
'listing/header.jsx',
@ -15,6 +16,7 @@ define(
MailPoet,
jQuery,
React,
Router,
classNames,
ListingBulkActions,
ListingHeader,
@ -23,7 +25,14 @@ define(
ListingGroups,
ListingFilters
) {
var Link = Router.Link;
var ListingItem = React.createClass({
getInitialState: function() {
return {
toggled: true
};
},
handleSelectItem: function(e) {
var is_checked = jQuery(e.target).is(':checked');
@ -34,13 +43,18 @@ define(
return !e.target.checked;
},
handleDeleteItem: function(id) {
this.props.onDeleteItem(id);
},
handleToggleItem: function(id) {
this.setState({ toggled: !this.state.toggled });
},
render: function() {
var checkbox = false;
if(this.props.is_selectable === true) {
checkbox = (
<th className="mailpoet_check_column" scope="row">
<th className="check-column" scope="row">
<label className="screen-reader-text">
{ 'Select ' + this.props.item.email }</label>
<input
@ -55,10 +69,35 @@ define(
);
}
var item_actions = (
<div>
<div className="row-actions">
<span className="edit">
<Link to="edit" params={{ id: this.props.item.id }}>Edit</Link>
</span>
&nbsp;|&nbsp;
<span className="trash">
<a
href="javascript:;"
onClick={ this.handleDeleteItem.bind(null, this.props.item.id) }>
Trash
</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>
);
var row_classes = classNames({ 'is-expanded': !this.state.toggled })
return (
<tr>
<tr className={ row_classes }>
{ checkbox }
{ this.props.onRenderItem(this.props.item) }
{ this.props.onRenderItem(this.props.item, item_actions) }
</tr>
);
}
@ -124,6 +163,7 @@ define(
columns={ this.props.columns }
onSelectItem={ this.props.onSelectItem }
onRenderItem={ this.props.onRenderItem }
onDeleteItem={ this.props.onDeleteItem }
selection={ this.props.selection }
is_selectable={ this.props.is_selectable }
key={ 'item-' + item.id }
@ -170,8 +210,8 @@ define(
search: this.state.search,
sort_by: this.state.sort_by,
sort_order: this.state.sort_order
},
onSuccess: function(response) {
}
}).done(function(response) {
if(this.isMounted()) {
this.setState({
items: response.items || [],
@ -181,8 +221,18 @@ define(
loading: false
});
}
}.bind(this)
});
}.bind(this));
},
handleDeleteItem: function(id) {
this.setState({ loading: true });
MailPoet.Ajax.post({
endpoint: this.props.endpoint,
action: 'delete',
data: id
}).done(function() {
this.getItems();
}.bind(this));
},
handleSearch: function(search) {
this.setState({
@ -269,8 +319,8 @@ define(
this.getItems();
}.bind(this));
},
handleRenderItem: function(item) {
return this.props.onRenderItem(item);
handleRenderItem: function(item, actions) {
return this.props.onRenderItem(item, actions);
},
render: function() {
var items = this.state.items,
@ -326,6 +376,7 @@ define(
<ListingItems
onRenderItem={ this.handleRenderItem }
onDeleteItem={ this.handleDeleteItem }
columns={ this.props.columns }
is_selectable={ bulk_actions.length > 0 }
onSelectItem={ this.handleSelectItem }

View File

@ -34,7 +34,6 @@ define(
var NewsletterForm = React.createClass({
render: function() {
return (
<Form
endpoint="newsletters"

View File

@ -1,18 +1,14 @@
define(
[
'react',
'react-router',
'listing/listing.jsx',
'classnames',
'classnames'
],
function(
React,
Router,
Listing,
classNames
) {
var Link = Router.Link;
var columns = [
{
name: 'subject',
@ -32,7 +28,7 @@ define(
];
var NewsletterList = React.createClass({
renderItem: function(newsletter) {
renderItem: function(newsletter, actions) {
var rowClasses = classNames(
'manage-column',
'column-primary',
@ -45,12 +41,7 @@ define(
<strong>
<a>{ newsletter.subject }</a>
</strong>
<div className="row-actions">
<span className="edit">
<Link to="edit" params={{ id: newsletter.id }}>Edit</Link>
</span>
</div>
{ actions }
</td>
<td className="column-date" data-colname="Subscribed on">
<abbr>{ newsletter.created_at }</abbr>

View File

@ -1,18 +1,14 @@
define(
[
'react',
'react-router',
'listing/listing.jsx',
'classnames',
'classnames'
],
function(
React,
Router,
Listing,
classNames
) {
var Link = Router.Link;
var columns = [
{
name: 'name',
@ -32,7 +28,7 @@ define(
];
var SegmentList = React.createClass({
renderItem: function(segment) {
renderItem: function(segment, actions) {
var rowClasses = classNames(
'manage-column',
'column-primary',
@ -45,12 +41,7 @@ define(
<strong>
<a>{ segment.name }</a>
</strong>
<div className="row-actions">
<span className="edit">
<Link to="edit" params={{ id: segment.id }}>Edit</Link>
</span>
</div>
{ actions }
</td>
<td className="column-date" data-colname="Subscribed on">
<abbr>{ segment.created_at }</abbr>

View File

@ -31,8 +31,8 @@ define(
label: 'Status',
type: 'select',
values: {
'subscribed': 'Subscribed',
'unconfirmed': 'Unconfirmed',
'subscribed': 'Subscribed',
'unsubscribed': 'Unsubscribed'
}
}

View File

@ -1,18 +1,14 @@
define(
[
'react',
'react-router',
'listing/listing.jsx',
'classnames',
'classnames'
],
function(
React,
Router,
Listing,
classNames
) {
var Link = Router.Link;
var columns = [
{
name: 'email',
@ -62,8 +58,8 @@ define(
];
var List = React.createClass({
renderItem: function(subscriber) {
var rowClasses = classNames(
renderItem: function(subscriber, actions) {
var row_classes = classNames(
'manage-column',
'column-primary',
'has-row-actions'
@ -87,20 +83,11 @@ define(
return (
<div>
<td className={ rowClasses }>
<td className={ row_classes }>
<strong>
<a>{ subscriber.email }</a>
</strong>
<div className="row-actions">
<span className="edit">
<Link to="edit" params={{ id: subscriber.id }}>Edit</Link>
</span>
</div>
<button className="toggle-row" type="button">
<span className="screen-reader-text">Show more details</span>
</button>
{ actions }
</td>
<td className="column" data-colname="First name">
{ subscriber.first_name }
@ -125,7 +112,6 @@ define(
<Listing
endpoint="subscribers"
onRenderItem={ this.renderItem }
items={ this.getItems }
columns={ columns }
bulk_actions={ bulk_actions } />
);

View File

@ -1,7 +1,7 @@
<?php
namespace MailPoet\Models;
if (!defined('ABSPATH')) exit;
if(!defined('ABSPATH')) exit;
class Newsletter extends Model {
public static $_table = MP_NEWSLETTERS_TABLE;
@ -10,12 +10,7 @@ class Newsletter extends Model {
parent::__construct();
$this->addValidations('subject', array(
'required' => 'subject_is_blank',
'isString' => 'subject_is_not_string'
));
$this->addValidations('body', array(
'required' => 'body_is_blank',
'isString' => 'body_is_not_string'
'required' => __('You need to specify a subject.')
));
}
@ -36,7 +31,7 @@ class Newsletter extends Model {
static function group($orm, $group = null) {
}
public static function createOrUpdate($data = array()) {
static function createOrUpdate($data = array()) {
$newsletter = false;
if(isset($data['id']) && (int)$data['id'] > 0) {

View File

@ -4,18 +4,17 @@ namespace MailPoet\Models;
if(!defined('ABSPATH')) exit;
class Segment extends Model {
public static $_table = MP_SEGMENTS_TABLE;
static $_table = MP_SEGMENTS_TABLE;
function __construct() {
parent::__construct();
$this->addValidations('name', array(
'required' => 'name_is_blank',
'isString' => 'name_is_not_string'
'required' => __('You need to specify a name.')
));
}
public function subscribers() {
function subscribers() {
return $this->has_many_through(
__NAMESPACE__.'\Subscriber',
__NAMESPACE__.'\SubscriberSegment',
@ -41,7 +40,7 @@ class Segment extends Model {
static function group($orm, $group = null) {
}
public static function createOrUpdate($data = array()) {
static function createOrUpdate($data = array()) {
$segment = false;
if(isset($data['id']) && (int)$data['id'] > 0) {

View File

@ -62,7 +62,7 @@ class Subscriber extends Model {
return $orm->where('status', $group);
}
public function segments() {
function segments() {
return $this->has_many_through(
__NAMESPACE__.'\Segment',
__NAMESPACE__.'\SubscriberSegment',
@ -71,7 +71,7 @@ class Subscriber extends Model {
);
}
public static function createOrUpdate($data = array()) {
static function createOrUpdate($data = array()) {
$subscriber = false;
if(isset($data['id']) && (int)$data['id'] > 0) {

View File

@ -45,12 +45,15 @@ class Newsletters {
}
}
function update($args) {
function delete($id) {
$newsletter = Newsletter::findOne($id);
if($newsletter !== false) {
$result = $newsletter->delete();
} else {
$result = false;
}
function delete($id) {
wp_send_json($result);
}
function send($id) {

View File

@ -43,11 +43,14 @@ class Segments {
}
}
function update($args) {
function delete($id) {
$segment = Segment::findOne($id);
if($segment !== false) {
$result = $segment->delete();
} else {
$result = false;
}
function delete($id) {
wp_send_json($result);
}
}

View File

@ -39,11 +39,13 @@ class Subscribers {
wp_send_json($result);
}
function update($data) {
}
function delete($id) {
$subscriber = Subscriber::findOne($id);
if($subscriber !== false) {
$result = $subscriber->delete();
} else {
$result = false;
}
wp_send_json($result);
}
}

View File

@ -15,6 +15,6 @@ $models = array(
);
$destroy = function ($model) {
Model::factory('\MailPoet\Models\\' . $model)
->delete_many();
->deleteMany();
};
array_map($destroy, $models);

View File

@ -7,8 +7,8 @@ class NewslettersPageCest {
function _before(AcceptanceTester $I) {
$I->login();
$I->resizeWindow(1024, 768);
$this->firstElementInList = '//*[@id="newsletters"]/div/div/table/tbody/tr[1]';
$this->waitTime = 2;
$this->first_row = 'id("newsletters")//table/tbody/tr[2]';
$this->timeout = 3;
}
function iCanSeeTheTitle(AcceptanceTester $I) {
@ -16,50 +16,51 @@ class NewslettersPageCest {
$I->see('Newsletters');
}
function iCanAddNewsletterFromListingPage(AcceptanceTester $I) {
$I->waitForElement('.no-items', $this->waitTime);
function iCanAddANewsletter(AcceptanceTester $I) {
$I->amOnPage('/wp-admin/admin.php?page=mailpoet-newsletters');
$I->see('No newsletters found');
$I->click('New', '#newsletters');
$I->fillField('Subject', 'first newsletter');
$I->fillField('subject', 'first newsletter');
$I->fillField('Body', 'some body');
$I->click('Save');
$I->waitForText('1 item', $this->waitTime);
$I->waitForText('1 item', $this->timeout);
}
function iCanAddNewsletterFromNewNewsletterPage(AcceptanceTester $I) {
$I->amOnPage('/wp-admin/admin.php?page=mailpoet-newsletters#/form');
$I->fillField('Subject', 'second newsletter');
function iCanAddAnotherNewsletter(AcceptanceTester $I) {
$I->amOnPage('/wp-admin/admin.php?page=mailpoet-newsletters#/new');
$I->fillField('subject', 'second newsletter');
$I->fillField('Body', 'some body');
$I->click('Save');
$I->waitForText('2 item', $this->waitTime);
$I->waitForText('2 item', $this->timeout);
}
function iCanSortNewsletterBySubject(AcceptanceTester $I) {
$I->click('Subject');
$I->waitForText('first', $this->waitTime, $this->firstElementInList);
$I->waitForText('first', $this->timeout, $this->first_row);
$I->click('Subject');
$I->waitForText('second', $this->waitTime, $this->firstElementInList);
$I->waitForText('second', $this->timeout, $this->first_row);
}
function iCanSortNewsletterByCreatedDate(AcceptanceTester $I) {
$I->click('Created on');
$I->waitForText('first', $this->waitTime, $this->firstElementInList);
$I->waitForText('first', $this->timeout, $this->first_row);
$I->click('Created on');
$I->waitForText('second', $this->waitTime, $this->firstElementInList);
$I->waitForText('second', $this->timeout, $this->first_row);
}
function iCanSearchNewsletters(AcceptanceTester $I) {
$searchTerm = 'second';
$I->fillField('Search', $searchTerm);
$search_term = 'second';
$I->fillField('Search', $search_term);
$I->click('Search');
$I->waitForText($searchTerm, $this->waitTime, $this->firstElementInList);
$I->waitForText($search_term, $this->timeout, $this->first_row);
}
function iCanSeeMobileView(AcceptanceTester $I) {
$listingHeadings = '//*[@id="newsletters"]/div/div/table/thead';
$listing_header = 'id("newsletters")//table/thead';
$I->resizeWindow(640, 480);
$I->dontSee('Created on', $listingHeadings);
$I->dontSee('Last modified', $listingHeadings);
$I->see('Subject', $listingHeadings);
$I->dontSee('Created on', $listing_header);
$I->dontSee('Last modified', $listing_header);
$I->see('Subject', $listing_header);
}
function _after(AcceptanceTester $I) {

View File

@ -4,28 +4,33 @@ use MailPoet\Models\Newsletter;
class NewsletterCest {
function _before() {
$this->before_time = time();
$this->data = array(
'subject' => 'My First Newsletter',
'body' => 'a verrryyyyy long body :)'
);
$newsletter = Newsletter::create();
$newsletter->hydrate($this->data);
$this->saved = $newsletter->save();
}
function itCanBeCreated() {
expect($this->saved)->equals(true);
function itCanCreateOrUpdate() {
$is_created = Newsletter::createOrUpdate(array(
'subject' => 'new newsletter'
));
expect($is_created)->equals(true);
$newsletter = Newsletter::where('subject', 'new newsletter')->findOne();
expect($newsletter->subject)->equals('new newsletter');
$is_updated = Newsletter::createOrUpdate(array(
'id' => $newsletter->id,
'subject' => 'updated newsletter'
));
$newsletter = Newsletter::where('subject', 'updated newsletter')->findOne();
expect($newsletter->subject)->equals('updated newsletter');
}
function itHasASearchFilter() {
$newsletter = Newsletter::filter('search', 'first')->findOne();
expect($newsletter->subject)->equals($this->data['subject']);
Newsletter::createOrUpdate(array('subject' => 'search for "pineapple"'));
$newsletter = Newsletter::filter('search', 'pineapple')->findOne();
expect($newsletter->subject)->contains('pineapple');
}
function _after() {
ORM::for_table(Newsletter::$_table)
->delete_many();
->deleteMany();
}
}

View File

@ -25,7 +25,7 @@ class SegmentCest {
$empty_model = Segment::create();
expect($empty_model->save())->notEquals(true);
$validations = $empty_model->getValidationErrors();
expect(count($validations))->equals(2);
expect(count($validations))->equals(1);
}
function itHasACreatedAtOnCreation() {
@ -62,20 +62,20 @@ class SegmentCest {
}
function itCanCreateOrUpdate() {
$data = array(
'name' => 'some other new name'
);
$createNewRecord = Segment::createOrUpdate($data);
$is_created = Segment::createOrUpdate(array(
'name' => 'new list'
));
expect($is_created)->equals(true);
$data = array(
'name' => $this->data['name'],
'name_updated' => 'updated name',
);
$updateExistingRecord = Segment::createOrUpdate($data);
$segment = Segment::where('name', 'new list')->findOne();
expect($segment->name)->equals('new list');
$allRecords = Segment::find_array();
expect(count($allRecords))->equals(2);
expect($allRecords[0]['name'])->equals($data['name_updated']);
$is_updated = Segment::createOrUpdate(array(
'id' => $segment->id,
'name' => 'updated list'
));
$segment = Segment::where('name', 'updated list')->findOne();
expect($segment->name)->equals('updated list');
}
function itCanHaveMultipleSubscribers() {
@ -108,12 +108,12 @@ class SegmentCest {
}
function _after() {
ORM::for_table(Segment::$_table)
->delete_many();
ORM::for_table(Subscriber::$_table)
->delete_many();
ORM::for_table(SubscriberSegment::$_table)
->delete_many();
ORM::forTable(Segment::$_table)
->deleteMany();
ORM::forTable(Subscriber::$_table)
->deleteMany();
ORM::forTable(SubscriberSegment::$_table)
->deleteMany();
}

View File

@ -80,7 +80,7 @@ class SettingCest {
}
function _after() {
ORM::for_table(Setting::$_table)
->delete_many();
ORM::forTable(Setting::$_table)
->deleteMany();
}
}

View File

@ -6,7 +6,6 @@ use MailPoet\Models\SubscriberSegment;
class SubscriberCest {
function _before() {
$this->before_time = time();
$this->data = array(
'first_name' => 'John',
'last_name' => 'Mailer',
@ -143,11 +142,11 @@ class SubscriberCest {
}
function _after() {
ORM::for_table(Subscriber::$_table)
->delete_many();
ORM::for_table(Segment::$_table)
->delete_many();
ORM::for_table(SubscriberSegment::$_table)
->delete_many();
ORM::forTable(Subscriber::$_table)
->deleteMany();
ORM::forTable(Segment::$_table)
->deleteMany();
ORM::forTable(SubscriberSegment::$_table)
->deleteMany();
}
}