forms listing complete

This commit is contained in:
Jonathan Labreuille
2015-10-30 16:40:34 +01:00
parent 5df0475b1a
commit e24f8c9653
13 changed files with 241 additions and 166 deletions

View File

@ -1,10 +1,12 @@
define([ define([
'react', 'react',
'react-dom',
'jquery', 'jquery',
'select2' 'select2'
], ],
function( function(
React, React,
ReactDOM,
jQuery jQuery
) { ) {
var Selection = React.createClass({ var Selection = React.createClass({
@ -21,12 +23,12 @@ function(
this.setupSelect2(); this.setupSelect2();
}, },
setupSelect2: function() { setupSelect2: function() {
if(this.state.initialized === true) { if(!this.props.field.multiple || this.state.initialized === true) {
return; return;
} }
if(this.props.field.select2 && Object.keys(this.props.item).length > 0) { if(Object.keys(this.props.item).length > 0) {
var select2 = jQuery('#'+this.props.field.id).select2({ var select2 = jQuery('#'+this.refs.select.id).select2({
width: (this.props.width || ''), width: (this.props.width || ''),
templateResult: function(item) { templateResult: function(item) {
if (item.element && item.element.selected) { if (item.element && item.element.selected) {
@ -37,8 +39,7 @@ function(
} }
}); });
select2.on('change', this.handleChange) select2.on('change', this.handleChange);
select2.select2( select2.select2(
'val', 'val',
this.props.item[this.props.field.name] this.props.item[this.props.field.name]
@ -55,11 +56,16 @@ function(
}); });
} }
}, },
handleChange: function() { handleChange: function(e) {
if(this.props.onValueChange !== undefined) { if(this.props.onValueChange !== undefined) {
if(this.props.field.multiple) {
value = jQuery('#'+this.refs.select.id).select2('val');
} else {
value = e.target.value;
}
this.props.onValueChange({ this.props.onValueChange({
target: { target: {
value: jQuery('#'+this.props.field.id).select2('val'), value: value,
name: this.props.field.name name: this.props.field.name
} }
}); });
@ -89,7 +95,8 @@ function(
return ( return (
<select <select
id={ this.props.field.id } id={ this.props.field.id || this.props.field.name }
ref="select"
placeholder={ this.props.field.placeholder } placeholder={ this.props.field.placeholder }
multiple={ this.props.field.multiple } multiple={ this.props.field.multiple }
onChange={ this.handleChange } onChange={ this.handleChange }

View File

@ -14,7 +14,8 @@ const fields = [
name: 'segments', name: 'segments',
label: 'Lists', label: 'Lists',
type: 'selection', type: 'selection',
endpoint: 'segments' endpoint: 'segments',
multiple: true
} }
] ]

View File

@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import { Router, Link, History } from 'react-router' import { Router, Link } from 'react-router'
import Listing from 'listing/listing.jsx' import Listing from 'listing/listing.jsx'
import classNames from 'classnames' import classNames from 'classnames'
import MailPoet from 'mailpoet' import MailPoet from 'mailpoet'
@ -11,6 +11,11 @@ const columns = [
label: 'Name', label: 'Name',
sortable: true sortable: true
}, },
{
name: 'segments',
label: 'Lists',
sortable: false
},
{ {
name: 'created_at', name: 'created_at',
label: 'Created on', label: 'Created on',
@ -20,57 +25,57 @@ const columns = [
const messages = { const messages = {
onTrash: function(response) { onTrash: function(response) {
let count = ~~response.forms; if(response) {
let message = null; let message = null;
if(~~response === 1) {
message = (
'1 form was moved to the trash.'
);
} else if(~~response > 1) {
message = (
'%$1d forms were moved to the trash.'
).replace('%$1d', ~~response);
}
if(count === 1 || response === true) { if(message !== null) {
message = ( MailPoet.Notice.success(message);
'1 form was moved to the trash.' }
);
} else if(count > 1) {
message = (
'%$1d forms were moved to the trash.'
).replace('%$1d', count);
}
if(message !== null) {
MailPoet.Notice.success(message);
} }
}, },
onDelete: function(response) { onDelete: function(response) {
let count = ~~response.forms; if(response) {
let message = null; let message = null;
if(~~response === 1) {
message = (
'1 form was permanently deleted.'
);
} else if(~~response > 1) {
message = (
'%$1d forms were permanently deleted.'
).replace('%$1d', ~~response);
}
if(count === 1 || response === true) { if(message !== null) {
message = ( MailPoet.Notice.success(message);
'1 form was permanently deleted.' }
);
} else if(count > 1) {
message = (
'%$1d forms were permanently deleted.'
).replace('%$1d', count);
}
if(message !== null) {
MailPoet.Notice.success(message);
} }
}, },
onRestore: function(response) { onRestore: function(response) {
let count = ~~response.forms; if(response) {
let message = null; let message = null;
if(~~response === 1) {
message = (
'1 form has been restored from the trash.'
);
} else if(~~response > 1) {
message = (
'%$1d forms have been restored from the trash.'
).replace('%$1d', ~~response);
}
if(count === 1 || response === true) { if(message !== null) {
message = ( MailPoet.Notice.success(message);
'1 form has been restored from the trash.' }
);
} else if(count > 1) {
message = (
'%$1d forms have been restored from the trash.'
).replace('%$1d', count);
}
if(message !== null) {
MailPoet.Notice.success(message);
} }
} }
}; };
@ -78,6 +83,7 @@ const messages = {
const item_actions = [ const item_actions = [
{ {
name: 'edit', name: 'edit',
label: 'Edit',
link: function(item) { link: function(item) {
return ( return (
<Link to={ `/edit/${item.id}` }>Edit</Link> <Link to={ `/edit/${item.id}` }>Edit</Link>
@ -86,24 +92,17 @@ const item_actions = [
}, },
{ {
name: 'duplicate_form', name: 'duplicate_form',
refresh: true, label: 'Duplicate',
link: function(item) { onClick: function(item, refresh) {
return ( return MailPoet.Ajax.post({
<a
href="javascript:;"
onClick={ this.onDuplicate.bind(null, item) }
>Duplicate</a>
);
},
onDuplicate: function(item) {
MailPoet.Ajax.post({
endpoint: 'forms', endpoint: 'forms',
action: 'duplicate', action: 'duplicate',
data: item.id data: item.id
}).done(function() { }).done(function(response) {
MailPoet.Notice.success( MailPoet.Notice.success(
('List "%$1s" has been duplicated.').replace('%$1s', item.name) ('Form "%$1s" has been duplicated.').replace('%$1s', response.name)
); );
refresh();
}); });
} }
} }
@ -113,12 +112,7 @@ const bulk_actions = [
{ {
name: 'trash', name: 'trash',
label: 'Trash', label: 'Trash',
getData: function() { onSuccess: messages.onTrash
return {
confirm: false
}
},
onSuccess: messages.onDelete
} }
]; ];
@ -161,6 +155,8 @@ const FormList = React.createClass({
</h2> </h2>
<Listing <Listing
location={ this.props.location }
params={ this.props.params }
messages={ messages } messages={ messages }
search={ false } search={ false }
limit={ 1000 } limit={ 1000 }

View File

@ -34,11 +34,9 @@ define(
}; };
}, },
handleSelectItem: function(e) { handleSelectItem: function(e) {
var is_checked = jQuery(e.target).is(':checked');
this.props.onSelectItem( this.props.onSelectItem(
parseInt(e.target.value, 10), parseInt(e.target.value, 10),
is_checked e.target.checked
); );
return !e.target.checked; return !e.target.checked;
@ -61,11 +59,12 @@ define(
if(this.props.is_selectable === true) { if(this.props.is_selectable === true) {
checkbox = ( checkbox = (
<th className="check-column" scope="row"> <th className="check-column" scope="row">
<label className="screen-reader-text"> <label className="screen-reader-text">{
{ 'Select ' + this.props.item.email }</label> 'Select ' + this.props.item[this.props.columns[0].name]
}</label>
<input <input
type="checkbox" type="checkbox"
defaultValue={ this.props.item.id } value={ this.props.item.id }
checked={ checked={
this.props.item.selected || this.props.selection === 'all' this.props.item.selected || this.props.selection === 'all'
} }
@ -267,7 +266,7 @@ define(
is_selectable={ this.props.is_selectable } is_selectable={ this.props.is_selectable }
item_actions={ this.props.item_actions } item_actions={ this.props.item_actions }
group={ this.props.group } group={ this.props.group }
key={ 'item-' + index } key={ `item-${item.id}-${index}` }
item={ item } /> item={ item } />
); );
}.bind(this))} }.bind(this))}

View File

@ -34,8 +34,7 @@ define(
placeholder: "Select a list", placeholder: "Select a list",
id: "mailpoet_segments", id: "mailpoet_segments",
endpoint: "segments", endpoint: "segments",
multiple: true, multiple: true
select2: true
}, },
{ {
name: 'sender', name: 'sender',

View File

@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import { Router, Route, Link } from 'react-router' import { Router, Link } from 'react-router'
import jQuery from 'jquery' import jQuery from 'jquery'
import MailPoet from 'mailpoet' import MailPoet from 'mailpoet'
@ -40,10 +40,10 @@ var columns = [
} }
]; ];
var messages = { const messages = {
onTrash: function(response) { onTrash: function(response) {
if(response) { if(response) {
var message = null; let message = null;
if(~~response === 1) { if(~~response === 1) {
message = ( message = (
'1 segment was moved to the trash.' '1 segment was moved to the trash.'
@ -61,7 +61,7 @@ var messages = {
}, },
onDelete: function(response) { onDelete: function(response) {
if(response) { if(response) {
var message = null; let message = null;
if(~~response === 1) { if(~~response === 1) {
message = ( message = (
'1 segment was permanently deleted.' '1 segment was permanently deleted.'
@ -79,7 +79,7 @@ var messages = {
}, },
onRestore: function(response) { onRestore: function(response) {
if(response) { if(response) {
var message = null; let message = null;
if(~~response === 1) { if(~~response === 1) {
message = ( message = (
'1 segment has been restored from the trash.' '1 segment has been restored from the trash.'
@ -97,7 +97,7 @@ var messages = {
} }
}; };
var item_actions = [ const item_actions = [
{ {
name: 'edit', name: 'edit',
label: 'Edit', label: 'Edit',
@ -133,7 +133,7 @@ var item_actions = [
} }
]; ];
var bulk_actions = [ const bulk_actions = [
{ {
name: 'trash', name: 'trash',
label: 'Trash', label: 'Trash',
@ -141,7 +141,7 @@ var bulk_actions = [
} }
]; ];
var SegmentList = React.createClass({ const SegmentList = React.createClass({
renderItem: function(segment, actions) { renderItem: function(segment, actions) {
var rowClasses = classNames( var rowClasses = classNames(
'manage-column', 'manage-column',

View File

@ -42,6 +42,7 @@ class Initializer {
$forms = Env::$db_prefix . 'forms'; $forms = Env::$db_prefix . 'forms';
$subscriber_segment = Env::$db_prefix . 'subscriber_segment'; $subscriber_segment = Env::$db_prefix . 'subscriber_segment';
$newsletter_segment = Env::$db_prefix . 'newsletter_segment'; $newsletter_segment = Env::$db_prefix . 'newsletter_segment';
$form_segment = Env::$db_prefix . 'form_segment';
$custom_fields = Env::$db_prefix . 'custom_fields'; $custom_fields = Env::$db_prefix . 'custom_fields';
$subscriber_custom_field = Env::$db_prefix . 'subscriber_custom_field'; $subscriber_custom_field = Env::$db_prefix . 'subscriber_custom_field';
$newsletter_option_fields = Env::$db_prefix . 'newsletter_option_fields'; $newsletter_option_fields = Env::$db_prefix . 'newsletter_option_fields';
@ -55,6 +56,7 @@ class Initializer {
define('MP_SUBSCRIBER_SEGMENT_TABLE', $subscriber_segment); define('MP_SUBSCRIBER_SEGMENT_TABLE', $subscriber_segment);
define('MP_NEWSLETTER_TEMPLATES_TABLE', $newsletter_templates); define('MP_NEWSLETTER_TEMPLATES_TABLE', $newsletter_templates);
define('MP_NEWSLETTER_SEGMENT_TABLE', $newsletter_segment); define('MP_NEWSLETTER_SEGMENT_TABLE', $newsletter_segment);
define('MP_FORM_SEGMENT_TABLE', $form_segment);
define('MP_CUSTOM_FIELDS_TABLE', $custom_fields); define('MP_CUSTOM_FIELDS_TABLE', $custom_fields);
define('MP_SUBSCRIBER_CUSTOM_FIELD_TABLE', $subscriber_custom_field); define('MP_SUBSCRIBER_CUSTOM_FIELD_TABLE', $subscriber_custom_field);
define('MP_NEWSLETTER_OPTION_FIELDS_TABLE', $newsletter_option_fields); define('MP_NEWSLETTER_OPTION_FIELDS_TABLE', $newsletter_option_fields);

View File

@ -17,6 +17,7 @@ class Migrator {
'segments', 'segments',
'subscriber_segment', 'subscriber_segment',
'newsletter_segment', 'newsletter_segment',
'form_segment',
'custom_fields', 'custom_fields',
'subscriber_custom_field', 'subscriber_custom_field',
'newsletter_option_fields', 'newsletter_option_fields',
@ -142,6 +143,18 @@ class Migrator {
return $this->sqlify(__FUNCTION__, $attributes); return $this->sqlify(__FUNCTION__, $attributes);
} }
function form_segment() {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'form_id mediumint(9) NOT NULL,',
'segment_id mediumint(9) NOT NULL,',
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id)'
);
return $this->sqlify(__FUNCTION__, $attributes);
}
function custom_fields() { function custom_fields() {
$attributes = array( $attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,', 'id mediumint(9) NOT NULL AUTO_INCREMENT,',

View File

@ -14,6 +14,15 @@ class Form extends Model {
)); ));
} }
function segments() {
return $this->has_many_through(
__NAMESPACE__.'\Segment',
__NAMESPACE__.'\FormSegment',
'form_id',
'segment_id'
);
}
static function search($orm, $search = '') { static function search($orm, $search = '') {
return $orm->where_like('name', '%'.$search.'%'); return $orm->where_like('name', '%'.$search.'%');
} }
@ -56,57 +65,28 @@ class Form extends Model {
$form->set($data); $form->set($data);
} }
$saved = $form->save(); try {
$form->save();
if($saved === true) { return $form;
return true; } catch(Exception $e) {
} else { return $form->getValidationErrors();
$errors = $form->getValidationErrors();
if(!empty($errors)) {
return $errors;
}
} }
return false; return false;
} }
static function trash($listing, $data = array()) { function duplicate($data = array()) {
$confirm_delete = filter_var($data['confirm'], FILTER_VALIDATE_BOOLEAN); $duplicate = parent::duplicate($data);
if($confirm_delete) {
// delete relations with all segments if($duplicate !== false) {
$forms = $listing->getSelection()->findResultSet(); foreach($this->segments()->findResultSet() as $relation) {
if(!empty($forms)) { $new_relation = FormSegment::create();
$forms_count = 0; $new_relation->set('segment_id', $relation->id);
foreach($forms as $form) { $new_relation->set('form_id', $duplicate->id);
if($form->delete()) { $new_relation->save();
$forms_count++;
}
}
return array(
'segments' => $forms_count
);
} }
return false;
} else {
// soft delete
$forms = $listing->getSelection()
->findResultSet()
->set_expr('deleted_at', 'NOW()')
->save();
return array( return $duplicate;
'segments' => $forms->count()
);
} }
} return false;
static function restore($listing, $data = array()) {
$forms = $listing->getSelection()
->findResultSet()
->set_expr('deleted_at', 'NULL')
->save();
return array(
'segments' => $forms->count()
);
} }
} }

View File

@ -0,0 +1,12 @@
<?php
namespace MailPoet\Models;
if(!defined('ABSPATH')) exit;
class FormSegment extends Model {
public static $_table = MP_FORM_SEGMENT_TABLE;
function __construct() {
parent::__construct();
}
}

View File

@ -29,6 +29,15 @@ class Segment extends Model {
); );
} }
function forms() {
return $this->has_many_through(
__NAMESPACE__.'\Form',
__NAMESPACE__.'\FormSegment',
'segment_id',
'form_id'
);
}
static function search($orm, $search = '') { static function search($orm, $search = '') {
return $orm->where_like('name', '%'.$search.'%'); return $orm->where_like('name', '%'.$search.'%');
} }

View File

@ -15,7 +15,16 @@ class Subscriber extends Model {
)); ));
} }
function delete() { function segments() {
return $this->has_many_through(
__NAMESPACE__.'\Segment',
__NAMESPACE__.'\SubscriberSegment',
'subscriber_id',
'segment_id'
);
}
function delete() {
// delete all relations to segments // delete all relations to segments
SubscriberSegment::where('subscriber_id', $this->id)->deleteMany(); SubscriberSegment::where('subscriber_id', $this->id)->deleteMany();
@ -42,7 +51,9 @@ class Subscriber extends Model {
); );
foreach($segments as $segment) { foreach($segments as $segment) {
$subscribers_count = $segment->subscribers()->count(); $subscribers_count = $segment->subscribers()
->whereNull('deleted_at')
->count();
if($subscribers_count > 0) { if($subscribers_count > 0) {
$segment_list[] = array( $segment_list[] = array(
'label' => sprintf('%s (%d)', $segment->name, $subscribers_count), 'label' => sprintf('%s (%d)', $segment->name, $subscribers_count),
@ -143,15 +154,6 @@ class Subscriber extends Model {
return $orm; return $orm;
} }
function segments() {
return $this->has_many_through(
__NAMESPACE__.'\Segment',
__NAMESPACE__.'\SubscriberSegment',
'subscriber_id',
'segment_id'
);
}
function customFields() { function customFields() {
return $this->has_many_through( return $this->has_many_through(
__NAMESPACE__.'\CustomField', __NAMESPACE__.'\CustomField',

View File

@ -1,6 +1,7 @@
<?php <?php
namespace MailPoet\Router; namespace MailPoet\Router;
use \MailPoet\Models\Form; use \MailPoet\Models\Form;
use \MailPoet\Models\FormSegment;
use \MailPoet\Listing; use \MailPoet\Listing;
if(!defined('ABSPATH')) exit; if(!defined('ABSPATH')) exit;
@ -16,7 +17,13 @@ class Forms {
if($form === false) { if($form === false) {
wp_send_json(false); wp_send_json(false);
} else { } else {
wp_send_json($form->asArray()); $segments = $form->segments();
$form = $form->asArray();
$form['segments'] = array_map(function($segment) {
return $segment['id'];
}, $segments->findArray());
wp_send_json($form);
} }
} }
@ -28,6 +35,17 @@ class Forms {
$listing_data = $listing->get(); $listing_data = $listing->get();
// fetch segments relations for each returned item
foreach($listing_data['items'] as &$item) {
// form's segments
$relations = FormSegment::select('segment_id')
->where('form_id', $item['id'])
->findMany();
$item['segments'] = array_map(function($relation) {
return $relation->segment_id;
}, $relations);
}
wp_send_json($listing_data); wp_send_json($listing_data);
} }
@ -37,53 +55,90 @@ class Forms {
} }
function save($data = array()) { function save($data = array()) {
$result = Form::createOrUpdate($data); if(isset($data['segments'])) {
$segment_ids = $data['segments'];
unset($data['segments']);
}
if($result !== true) { $form = Form::createOrUpdate($data);
wp_send_json($result);
if($form->id() && !empty($segment_ids)) {
// remove previous relationships with segments
FormSegment::where('form_id', $form->id())->deleteMany();
// create relationship with segments
foreach($segment_ids as $segment_id) {
$relation = FormSegment::create();
$relation->segment_id = $segment_id;
$relation->form_id = $form->id();
$relation->save();
}
}
if($form === false) {
wp_send_json($form->getValidationErrors());
} else { } else {
wp_send_json(true); wp_send_json(true);
} }
} }
function restore($id) { function restore($id) {
$result = false;
$form = Form::findOne($id); $form = Form::findOne($id);
if($form !== false) { if($form !== false) {
$form->set_expr('deleted_at', 'NULL'); $result = $form->restore();
$result = $form->save();
} else {
$result = false;
} }
wp_send_json($result); wp_send_json($result);
} }
function delete($data = array()) { function trash($id) {
$form = Form::findOne($data['id']); $result = false;
$confirm_delete = filter_var($data['confirm'], FILTER_VALIDATE_BOOLEAN);
$form = Form::findOne($id);
if($form !== false) { if($form !== false) {
if($confirm_delete) { $result = $form->trash();
$form->delete();
$result = true;
} else {
$form->set_expr('deleted_at', 'NOW()');
$result = $form->save();
}
} else {
$result = false;
} }
wp_send_json($result);
}
function delete($id) {
$result = false;
$form = Form::findOne($id);
if($form !== false) {
$form->delete();
$result = 1;
}
wp_send_json($result); wp_send_json($result);
} }
function duplicate($id) { function duplicate($id) {
$result = false; $result = false;
$form = Form::duplicate($id); $form = Form::findOne($id);
if($form !== false) { if($form !== false) {
$result = $form; $data = array(
'name' => sprintf(__('Copy of %s'), $form->name)
);
$result = $form->duplicate($data)->asArray();
} }
wp_send_json($result); wp_send_json($result);
} }
function item_action($data = array()) {
$item_action = new Listing\ItemAction(
'\MailPoet\Models\Form',
$data
);
wp_send_json($item_action->apply());
}
function bulk_action($data = array()) { function bulk_action($data = array()) {
$bulk_action = new Listing\BulkAction( $bulk_action = new Listing\BulkAction(
'\MailPoet\Models\Form', '\MailPoet\Models\Form',