From 103da61d450c9712091a143fd89f2cc1d99dca59 Mon Sep 17 00:00:00 2001 From: Jonathan Labreuille Date: Wed, 28 Oct 2015 13:18:56 +0100 Subject: [PATCH] basic listing files --- assets/js/src/forms/form.jsx | 56 ++++++++++ assets/js/src/forms/forms.jsx | 29 +++++ assets/js/src/forms/list.jsx | 202 ++++++++++++++++++++++++++++++++++ lib/Config/Initializer.php | 2 + lib/Config/Menu.php | 15 +++ lib/Config/Migrator.php | 3 +- lib/Models/Form.php | 112 +++++++++++++++++++ lib/Models/Model.php | 23 ++++ views/forms.html | 16 +++ webpack.config.js | 1 + 10 files changed, 457 insertions(+), 2 deletions(-) create mode 100644 assets/js/src/forms/form.jsx create mode 100644 assets/js/src/forms/forms.jsx create mode 100644 assets/js/src/forms/list.jsx create mode 100644 lib/Models/Form.php create mode 100644 views/forms.html diff --git a/assets/js/src/forms/form.jsx b/assets/js/src/forms/form.jsx new file mode 100644 index 0000000000..60ee6e3b10 --- /dev/null +++ b/assets/js/src/forms/form.jsx @@ -0,0 +1,56 @@ +import React from 'react' +import ReactDOM from 'react-dom' +import Router from 'react-router' +import MailPoet from 'mailpoet' +import Form from 'form/form.jsx' + +const fields = [ + { + name: 'name', + label: 'Name', + type: 'text' + }, + { + name: 'segments', + label: 'Lists', + type: 'selection', + endpoint: 'segments' + } +] + +const messages = { + updated: function() { + MailPoet.Notice.success('Form successfully updated!'); + }, + created: function() { + MailPoet.Notice.success('Form successfully added!'); + } +} + +const FormForm = React.createClass({ + mixins: [ + Router.History + ], + render() { + return ( +
+

+ Form Back to list +

+ +
+
+ ); + } +}); + +module.exports = FormForm \ No newline at end of file diff --git a/assets/js/src/forms/forms.jsx b/assets/js/src/forms/forms.jsx new file mode 100644 index 0000000000..406d795e7b --- /dev/null +++ b/assets/js/src/forms/forms.jsx @@ -0,0 +1,29 @@ +import React from 'react' +import ReactDOM from 'react-dom' +import { Router, Route, IndexRoute, Link } from 'react-router' +import FormList from 'forms/list.jsx' +import FormForm from 'forms/form.jsx' +import createHashHistory from 'history/lib/createHashHistory' + +let history = createHashHistory({ queryKey: false }) + +const App = React.createClass({ + render() { + return this.props.children + } +}); + +let container = document.getElementById('forms_container'); + +if(container) { + ReactDOM.render(( + + + + + + + + + ), container); +} \ No newline at end of file diff --git a/assets/js/src/forms/list.jsx b/assets/js/src/forms/list.jsx new file mode 100644 index 0000000000..bf7a7bb839 --- /dev/null +++ b/assets/js/src/forms/list.jsx @@ -0,0 +1,202 @@ +define( + [ + 'react', + 'react-router', + 'listing/listing.jsx', + 'classnames', + 'mailpoet' + ], + function( + React, + Router, + Listing, + classNames, + MailPoet + ) { + var columns = [ + { + name: 'name', + label: 'Name', + sortable: true + }, + { + name: 'created_at', + label: 'Created on', + sortable: true + } + ]; + + var messages = { + onDelete: function(response) { + var count = ~~response.segments; + var message = null; + + if(count === 1 || response === true) { + message = ( + '1 segment was moved to the trash.' + ); + } else if(count > 1) { + message = ( + '%$1d segments were moved to the trash.' + ).replace('%$1d', count); + } + + if(message !== null) { + MailPoet.Notice.success(message); + } + }, + onConfirmDelete: function(response) { + var count = ~~response.segments; + var message = null; + + if(count === 1 || response === true) { + message = ( + '1 segment was permanently deleted.' + ); + } else if(count > 1) { + message = ( + '%$1d segments were permanently deleted.' + ).replace('%$1d', count); + } + + if(message !== null) { + MailPoet.Notice.success(message); + } + }, + onRestore: function(response) { + var count = ~~response.segments; + var message = null; + + if(count === 1 || response === true) { + message = ( + '1 segment has been restored from the trash.' + ); + } else if(count > 1) { + message = ( + '%$1d segments have been restored from the trash.' + ).replace('%$1d', count); + } + + if(message !== null) { + MailPoet.Notice.success(message); + } + } + }; + + var Link = Router.Link; + var item_actions = [ + { + name: 'edit', + link: function(item) { + return ( + Edit + ); + } + }, + { + name: 'duplicate_segment', + refresh: true, + link: function(item) { + return ( + Duplicate + ); + }, + onDuplicate: function(item) { + MailPoet.Ajax.post({ + endpoint: 'segments', + action: 'duplicate', + data: item.id + }).done(function() { + MailPoet.Notice.success( + ('List "%$1s" has been duplicated.').replace('%$1s', item.name) + ); + }); + } + }, + { + name: 'view_subscribers', + link: function(item) { + return ( + View subscribers + ); + } + } + ]; + + var bulk_actions = [ + { + name: 'trash', + label: 'Trash', + getData: function() { + return { + confirm: false + } + }, + onSuccess: messages.onDelete + } + ]; + + var Link = Router.Link; + + var SegmentList = React.createClass({ + renderItem: function(segment, actions) { + var rowClasses = classNames( + 'manage-column', + 'column-primary', + 'has-row-actions' + ); + + return ( +
+ + + { segment.name } + + { actions } + + + { segment.description } + + + { segment.subscribed || 0 } + + + { segment.unconfirmed || 0 } + + + { segment.unsubscribed || 0 } + + + { segment.created_at } + +
+ ); + }, + render: function() { + return ( +
+

+ Segments New +

+ + +
+ ); + } + }); + + return SegmentList; + } +); \ No newline at end of file diff --git a/lib/Config/Initializer.php b/lib/Config/Initializer.php index e64019e7f8..d0ca7ef5bd 100644 --- a/lib/Config/Initializer.php +++ b/lib/Config/Initializer.php @@ -39,6 +39,7 @@ class Initializer { $newsletters = Env::$db_prefix . 'newsletters'; $newsletter_templates = Env::$db_prefix . 'newsletter_templates'; $segments = Env::$db_prefix . 'segments'; + $forms = Env::$db_prefix . 'forms'; $subscriber_segment = Env::$db_prefix . 'subscriber_segment'; $newsletter_segment = Env::$db_prefix . 'newsletter_segment'; $custom_fields = Env::$db_prefix . 'custom_fields'; @@ -50,6 +51,7 @@ class Initializer { define('MP_SETTINGS_TABLE', $settings); define('MP_NEWSLETTERS_TABLE', $newsletters); define('MP_SEGMENTS_TABLE', $segments); + define('MP_FORMS_TABLE', $forms); define('MP_SUBSCRIBER_SEGMENT_TABLE', $subscriber_segment); define('MP_NEWSLETTER_TEMPLATES_TABLE', $newsletter_templates); define('MP_NEWSLETTER_SEGMENT_TABLE', $newsletter_segment); diff --git a/lib/Config/Menu.php b/lib/Config/Menu.php index 493f4f6f02..3728188fc4 100644 --- a/lib/Config/Menu.php +++ b/lib/Config/Menu.php @@ -41,6 +41,14 @@ class Menu { 'mailpoet-newsletters', array($this, 'newsletters') ); + add_submenu_page( + 'mailpoet', + __('Forms'), + __('Forms'), + 'manage_options', + 'mailpoet-forms', + array($this, 'forms') + ); add_submenu_page( 'mailpoet', __('Subscribers'), @@ -163,6 +171,13 @@ class Menu { echo $this->renderer->render('segments.html', $data); } + function forms() { + $data = array(); + $data['segments'] = Segment::findArray(); + + echo $this->renderer->render('forms.html', $data); + } + function newsletters() { global $wp_roles; diff --git a/lib/Config/Migrator.php b/lib/Config/Migrator.php index 89cabc4303..c2a00f000b 100644 --- a/lib/Config/Migrator.php +++ b/lib/Config/Migrator.php @@ -202,8 +202,7 @@ class Migrator { '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)' + 'PRIMARY KEY (id)' ); return $this->sqlify(__FUNCTION__, $attributes); } diff --git a/lib/Models/Form.php b/lib/Models/Form.php new file mode 100644 index 0000000000..fcb7f92d76 --- /dev/null +++ b/lib/Models/Form.php @@ -0,0 +1,112 @@ +addValidations('name', array( + 'required' => __('You need to specify a name.') + )); + } + + static function search($orm, $search = '') { + return $orm->where_like('name', '%'.$search.'%'); + } + + static function groups() { + return array( + array( + 'name' => 'all', + 'label' => __('All'), + 'count' => Form::whereNull('deleted_at')->count() + ), + array( + 'name' => 'trash', + 'label' => __('Trash'), + 'count' => Form::whereNotNull('deleted_at')->count() + ) + ); + } + + static function groupBy($orm, $group = null) { + if($group === 'trash') { + return $orm->whereNotNull('deleted_at'); + } else { + $orm = $orm->whereNull('deleted_at'); + } + } + + static function createOrUpdate($data = array()) { + $form = false; + + if(isset($data['id']) && (int)$data['id'] > 0) { + $form = self::findOne((int)$data['id']); + } + + if($form === false) { + $form = self::create(); + $form->hydrate($data); + } else { + unset($data['id']); + $form->set($data); + } + + $saved = $form->save(); + + if($saved === true) { + return true; + } else { + $errors = $form->getValidationErrors(); + if(!empty($errors)) { + return $errors; + } + } + return false; + } + + static function trash($listing, $data = array()) { + $confirm_delete = filter_var($data['confirm'], FILTER_VALIDATE_BOOLEAN); + if($confirm_delete) { + // delete relations with all segments + $forms = $listing->getSelection()->findResultSet(); + if(!empty($forms)) { + $forms_count = 0; + foreach($forms as $form) { + if($form->delete()) { + $forms_count++; + } + } + return array( + 'segments' => $forms_count + ); + } + return false; + } else { + // soft delete + $forms = $listing->getSelection() + ->findResultSet() + ->set_expr('deleted_at', 'NOW()') + ->save(); + + return array( + 'segments' => $forms->count() + ); + } + } + + static function restore($listing, $data = array()) { + $forms = $listing->getSelection() + ->findResultSet() + ->set_expr('deleted_at', 'NULL') + ->save(); + + return array( + 'segments' => $forms->count() + ); + } +} diff --git a/lib/Models/Model.php b/lib/Models/Model.php index 93716e9be8..49cf4d1e0d 100644 --- a/lib/Models/Model.php +++ b/lib/Models/Model.php @@ -21,6 +21,29 @@ class Model extends \Sudzy\ValidModel { } } + static function restore($orm) { + $models = $orm->findResultSet(); + if(empty($models)) return false; + + $models->setExpr('deleted_at', 'NULL')->save(); + return $models->count(); + } + + static function trash($orm, $confirm = false) { + $models = $orm->findResultSet(); + if(empty($models)) return false; + + if($confirm === true) { + foreach($models as $model) { + $model->delete(); + } + } else { + $models = $orm->findResultSet(); + $models->setExpr('deleted_at', 'NOW()')->save(); + } + return $models->count(); + } + private function setTimestamp() { if($this->created_at === null) { $this->set_expr('created_at', 'NOW()'); diff --git a/views/forms.html b/views/forms.html new file mode 100644 index 0000000000..c3c4820797 --- /dev/null +++ b/views/forms.html @@ -0,0 +1,16 @@ +<% extends 'layout.html' %> + +<% block content %> +
+ + <%= localize({ + 'pageTitle': __('Forms'), + 'searchLabel': __('Search'), + 'loadingItems': __('Loading forms...'), + 'noItemsFound': __('No forms found.') + }) %> + + +<% endblock %> diff --git a/webpack.config.js b/webpack.config.js index 051aee0c36..f28518e2de 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -69,6 +69,7 @@ config.push(_.extend({}, baseConfig, { 'subscribers/subscribers.jsx', 'newsletters/newsletters.jsx', 'segments/segments.jsx', + 'forms/forms.jsx', 'settings/tabs.js' ], newsletter_editor: [