Last step
- improved watch command (much simpler robofile + truly recursive) - split all form fields into separate files (JSX) - improved form to allow multiple fields per row - added selection react class for multi select using select2 - added missing files for select2 (webpack doesn't include them)
This commit is contained in:
43
RoboFile.php
43
RoboFile.php
@ -2,18 +2,6 @@
|
|||||||
|
|
||||||
class RoboFile extends \Robo\Tasks {
|
class RoboFile extends \Robo\Tasks {
|
||||||
|
|
||||||
private $css_files = array(
|
|
||||||
'assets/css/src/*.styl',
|
|
||||||
'assets/css/src/**/*.styl'
|
|
||||||
);
|
|
||||||
|
|
||||||
private $js_files = array(
|
|
||||||
'assets/js/src/*.js',
|
|
||||||
'assets/js/src/*.jsx',
|
|
||||||
'assets/js/src/**/*.js',
|
|
||||||
'assets/js/src/**/*.jsx'
|
|
||||||
);
|
|
||||||
|
|
||||||
function install() {
|
function install() {
|
||||||
$this->_exec('./composer.phar install');
|
$this->_exec('./composer.phar install');
|
||||||
$this->_exec('npm install');
|
$this->_exec('npm install');
|
||||||
@ -25,16 +13,29 @@ class RoboFile extends \Robo\Tasks {
|
|||||||
$this->_exec('npm update');
|
$this->_exec('npm update');
|
||||||
}
|
}
|
||||||
|
|
||||||
function watch() {
|
protected function rsearch($folder, $extensions = array()) {
|
||||||
$js_files = array();
|
$dir = new RecursiveDirectoryIterator($folder);
|
||||||
array_map(function($path) use(&$js_files) {
|
$iterator = new RecursiveIteratorIterator($dir);
|
||||||
$js_files = array_merge($js_files, glob($path));
|
|
||||||
}, $this->js_files);
|
|
||||||
|
|
||||||
$css_files = array();
|
$pattern = '/^.+\.('.join($extensions, '|').')$/i';
|
||||||
array_map(function($path) use(&$css_files) {
|
|
||||||
$css_files = array_merge($css_files, glob($path));
|
$files = new RegexIterator(
|
||||||
}, $this->css_files);
|
$iterator,
|
||||||
|
$pattern,
|
||||||
|
RecursiveRegexIterator::GET_MATCH
|
||||||
|
);
|
||||||
|
|
||||||
|
$list = array();
|
||||||
|
foreach($files as $file) {
|
||||||
|
$list[] = $file[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $list;
|
||||||
|
}
|
||||||
|
|
||||||
|
function watch() {
|
||||||
|
$css_files = $this->rsearch('assets/css/src/', array('styl'));
|
||||||
|
$js_files = $this->rsearch('assets/js/src/', array('js', 'jsx'));
|
||||||
|
|
||||||
$this->taskWatch()
|
$this->taskWatch()
|
||||||
->monitor($js_files, function() {
|
->monitor($js_files, function() {
|
||||||
|
BIN
assets/css/select2-spinner.gif
Normal file
BIN
assets/css/select2-spinner.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
BIN
assets/css/select2.png
Normal file
BIN
assets/css/select2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 613 B |
BIN
assets/css/select2x2.png
Normal file
BIN
assets/css/select2x2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 845 B |
@ -10,4 +10,5 @@
|
|||||||
@require 'form_editor'
|
@require 'form_editor'
|
||||||
@require 'listing'
|
@require 'listing'
|
||||||
@require 'box'
|
@require 'box'
|
||||||
@require 'breadcrumb'
|
@require 'breadcrumb'
|
||||||
|
@require 'form'
|
2
assets/css/src/form.styl
Normal file
2
assets/css/src/form.styl
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
.mailpoet_form td
|
||||||
|
vertical-align: top
|
@ -1,41 +0,0 @@
|
|||||||
if (!Function.prototype.bind) {
|
|
||||||
var Empty = function() {};
|
|
||||||
Function.prototype.bind = function bind(that) { // .length is 1
|
|
||||||
var target = this;
|
|
||||||
if (typeof target != "function") {
|
|
||||||
throw new TypeError("Function.prototype.bind called on incompatible " + target);
|
|
||||||
}
|
|
||||||
var args = Array.prototype.slice.call(arguments, 1); // for normal call
|
|
||||||
var binder = function() {
|
|
||||||
if (this instanceof bound) {
|
|
||||||
var result = target.apply(
|
|
||||||
this,
|
|
||||||
args.concat(Array.prototype.slice.call(arguments))
|
|
||||||
);
|
|
||||||
if (Object(result) === result) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
} else {
|
|
||||||
return target.apply(
|
|
||||||
that,
|
|
||||||
args.concat(Array.prototype.slice.call(arguments))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var boundLength = Math.max(0, target.length - args.length);
|
|
||||||
var boundArgs = [];
|
|
||||||
for (var i = 0; i < boundLength; i++) {
|
|
||||||
boundArgs.push("$" + i);
|
|
||||||
}
|
|
||||||
var bound = Function("binder", "return function(" + boundArgs.join(",") + "){return binder.apply(this,arguments)}")(binder);
|
|
||||||
|
|
||||||
if (target.prototype) {
|
|
||||||
Empty.prototype = target.prototype;
|
|
||||||
bound.prototype = new Empty();
|
|
||||||
// Clean up dangling references.
|
|
||||||
Empty.prototype = null;
|
|
||||||
}
|
|
||||||
return bound;
|
|
||||||
}
|
|
||||||
};
|
|
64
assets/js/src/form/fields/checkbox.jsx
Normal file
64
assets/js/src/form/fields/checkbox.jsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
define([
|
||||||
|
'react',
|
||||||
|
'react-checkbox-group'
|
||||||
|
],
|
||||||
|
function(
|
||||||
|
React,
|
||||||
|
CheckboxGroup
|
||||||
|
) {
|
||||||
|
var FormFieldCheckbox = React.createClass({
|
||||||
|
render: function() {
|
||||||
|
var selected_values = this.props.item[this.props.field.name] || '';
|
||||||
|
if(
|
||||||
|
selected_values !== undefined
|
||||||
|
&& selected_values.constructor !== Array
|
||||||
|
) {
|
||||||
|
selected_values = selected_values.split(';').map(function(value) {
|
||||||
|
return value.trim();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var count = Object.keys(this.props.field.values).length;
|
||||||
|
|
||||||
|
var options = Object.keys(this.props.field.values).map(
|
||||||
|
function(value, index) {
|
||||||
|
return (
|
||||||
|
<p key={ 'checkbox-' + index }>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" value={ value } />
|
||||||
|
{ this.props.field.values[value] }
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}.bind(this)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CheckboxGroup
|
||||||
|
name={ this.props.field.name }
|
||||||
|
value={ selected_values }
|
||||||
|
ref={ this.props.field.name }
|
||||||
|
onChange={ this.handleValueChange }>
|
||||||
|
{ options }
|
||||||
|
</CheckboxGroup>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
handleValueChange: function() {
|
||||||
|
var field = this.props.field.name;
|
||||||
|
var group = this.refs[field];
|
||||||
|
var selected_values = [];
|
||||||
|
|
||||||
|
if(group !== undefined) {
|
||||||
|
selected_values = group.getCheckedValues();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.onValueChange({
|
||||||
|
target: {
|
||||||
|
name: field,
|
||||||
|
value: selected_values.join(';')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return FormFieldCheckbox;
|
||||||
|
});
|
110
assets/js/src/form/fields/field.jsx
Normal file
110
assets/js/src/form/fields/field.jsx
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
define([
|
||||||
|
'react',
|
||||||
|
'form/fields/text.jsx',
|
||||||
|
'form/fields/textarea.jsx',
|
||||||
|
'form/fields/select.jsx',
|
||||||
|
'form/fields/radio.jsx',
|
||||||
|
'form/fields/checkbox.jsx'
|
||||||
|
],
|
||||||
|
function(
|
||||||
|
React,
|
||||||
|
FormFieldText,
|
||||||
|
FormFieldTextarea,
|
||||||
|
FormFieldSelect,
|
||||||
|
FormFieldRadio,
|
||||||
|
FormFieldCheckbox
|
||||||
|
) {
|
||||||
|
var FormField = React.createClass({
|
||||||
|
renderField: function(data, inline = true) {
|
||||||
|
var description = false;
|
||||||
|
if(data.field.description) {
|
||||||
|
description = (
|
||||||
|
<p className="description">{ data.field.description }</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var field = false;
|
||||||
|
|
||||||
|
if(data.field['field'] !== undefined) {
|
||||||
|
field = data.field.field;
|
||||||
|
} else{
|
||||||
|
switch(data.field.type) {
|
||||||
|
case 'text':
|
||||||
|
field = (<FormFieldText {...data} />);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'textarea':
|
||||||
|
field = (<FormFieldTextarea {...data} />);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'select':
|
||||||
|
field = (<FormFieldSelect {...data} />);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'radio':
|
||||||
|
field = (<FormFieldRadio {...data} />);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'checkbox':
|
||||||
|
field = (<FormFieldCheckbox {...data} />);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(inline === true) {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{ field }
|
||||||
|
{ description }
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{ field }
|
||||||
|
{ description }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
render: function() {
|
||||||
|
var field = false;
|
||||||
|
|
||||||
|
if(this.props.field['fields'] !== undefined) {
|
||||||
|
field = this.props.field.fields.map(function(subfield) {
|
||||||
|
return this.renderField({
|
||||||
|
field: subfield,
|
||||||
|
item: this.props.item
|
||||||
|
});
|
||||||
|
}.bind(this));
|
||||||
|
} else {
|
||||||
|
field = this.renderField(this.props);
|
||||||
|
}
|
||||||
|
|
||||||
|
var tip = false;
|
||||||
|
if(this.props.field.tip) {
|
||||||
|
tip = (
|
||||||
|
<p className="description">{ this.props.field.tip }</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label
|
||||||
|
htmlFor={ 'field_'+this.props.field.name }
|
||||||
|
>
|
||||||
|
{ this.props.field.label }
|
||||||
|
{ tip }
|
||||||
|
</label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
{ field }
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return FormField;
|
||||||
|
});
|
39
assets/js/src/form/fields/radio.jsx
Normal file
39
assets/js/src/form/fields/radio.jsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
define([
|
||||||
|
'react'
|
||||||
|
],
|
||||||
|
function(
|
||||||
|
React
|
||||||
|
) {
|
||||||
|
var FormFieldRadio = React.createClass({
|
||||||
|
render: function() {
|
||||||
|
var selected_value = this.props.item[this.props.field.name];
|
||||||
|
var count = Object.keys(this.props.field.values).length;
|
||||||
|
|
||||||
|
var options = Object.keys(this.props.field.values).map(
|
||||||
|
function(value, index) {
|
||||||
|
return (
|
||||||
|
<p key={ 'radio-' + index }>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
checked={ selected_value === value }
|
||||||
|
value={ value }
|
||||||
|
onChange={ this.props.onValueChange }
|
||||||
|
name={ this.props.field.name } />
|
||||||
|
{ this.props.field.values[value] }
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}.bind(this)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{ options }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return FormFieldRadio;
|
||||||
|
});
|
34
assets/js/src/form/fields/select.jsx
Normal file
34
assets/js/src/form/fields/select.jsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
define([
|
||||||
|
'react'
|
||||||
|
],
|
||||||
|
function(
|
||||||
|
React
|
||||||
|
) {
|
||||||
|
var FormFieldSelect = React.createClass({
|
||||||
|
render: function() {
|
||||||
|
var options =
|
||||||
|
Object.keys(this.props.field.values).map(function(value, index) {
|
||||||
|
return (
|
||||||
|
<option
|
||||||
|
key={ 'option-' + index }
|
||||||
|
value={ value }>
|
||||||
|
{ this.props.field.values[value] }
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
}.bind(this)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
name={ this.props.field.name }
|
||||||
|
id={ 'field_'+this.props.field.name }
|
||||||
|
value={ this.props.item[this.props.field.name] }
|
||||||
|
onChange={ this.props.onValueChange }>
|
||||||
|
{options}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return FormFieldSelect;
|
||||||
|
});
|
119
assets/js/src/form/fields/selection.jsx
Normal file
119
assets/js/src/form/fields/selection.jsx
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
define([
|
||||||
|
'react',
|
||||||
|
'jquery'
|
||||||
|
],
|
||||||
|
function(
|
||||||
|
React,
|
||||||
|
jQuery
|
||||||
|
) {
|
||||||
|
var Selection = React.createClass({
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
items: [],
|
||||||
|
selected: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
componentWillMount: function() {
|
||||||
|
this.loadCachedItems();
|
||||||
|
},
|
||||||
|
componentDidMount: function() {
|
||||||
|
jQuery('#'+this.props.id).select2({
|
||||||
|
width: '25em'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
loadCachedItems: function() {
|
||||||
|
if(typeof(window['mailpoet_'+this.props.endpoint]) !== 'undefined') {
|
||||||
|
var items = window['mailpoet_'+this.props.endpoint];
|
||||||
|
this.setState({
|
||||||
|
items: items
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loadItems: function() {
|
||||||
|
this.setState({ loading: true });
|
||||||
|
|
||||||
|
MailPoet.Ajax.post({
|
||||||
|
endpoint: this.props.endpoint,
|
||||||
|
action: 'listing',
|
||||||
|
data: {
|
||||||
|
'offset': 0,
|
||||||
|
'limit': 100,
|
||||||
|
'search': '',
|
||||||
|
'sort_by': 'name',
|
||||||
|
'sort_order': 'asc'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.done(function(response) {
|
||||||
|
if(this.isMounted()) {
|
||||||
|
if(response === false) {
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
items: []
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
items: response.items
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.bind(this));
|
||||||
|
},
|
||||||
|
handleChange: function() {
|
||||||
|
var new_value = this.refs.selection.getDOMNode().value;
|
||||||
|
|
||||||
|
if(this.props.multiple === false) {
|
||||||
|
if(new_value.trim().length === 0) {
|
||||||
|
new_value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
selected: new_value
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
var selected_values = this.state.selected || [];
|
||||||
|
|
||||||
|
if(selected_values.indexOf(new_value) !== -1) {
|
||||||
|
// value already present so remove it
|
||||||
|
selected_values.splice(selected_values.indexOf(new_value), 1);
|
||||||
|
} else {
|
||||||
|
selected_values.push(new_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
selected: selected_values
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getSelected: function() {
|
||||||
|
return this.state.selected;
|
||||||
|
},
|
||||||
|
render: function() {
|
||||||
|
var options = this.state.items.map(function(item, index) {
|
||||||
|
return (
|
||||||
|
<option
|
||||||
|
key={ 'action-' + index }
|
||||||
|
value={ item.id }>
|
||||||
|
{ item.name }
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
ref="selection"
|
||||||
|
id={ this.props.id }
|
||||||
|
value={ this.state.selected }
|
||||||
|
onChange={ this.handleChange }
|
||||||
|
placeholder={ this.props.placeholder }
|
||||||
|
multiple
|
||||||
|
>
|
||||||
|
{ options }
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Selection;
|
||||||
|
});
|
24
assets/js/src/form/fields/text.jsx
Normal file
24
assets/js/src/form/fields/text.jsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
define([
|
||||||
|
'react'
|
||||||
|
],
|
||||||
|
function(
|
||||||
|
React
|
||||||
|
) {
|
||||||
|
var FormFieldText = React.createClass({
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={ (this.props.field.size) ? '' : 'regular-text' }
|
||||||
|
size={ (this.props.field.size !== 'auto') ? this.props.field.size : false }
|
||||||
|
name={ this.props.field.name }
|
||||||
|
id={ 'field_'+this.props.field.name }
|
||||||
|
value={ this.props.item[this.props.field.name] }
|
||||||
|
placeholder={ this.props.field.placeholder }
|
||||||
|
onChange={ this.props.onValueChange } />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return FormFieldText;
|
||||||
|
});
|
22
assets/js/src/form/fields/textarea.jsx
Normal file
22
assets/js/src/form/fields/textarea.jsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
define([
|
||||||
|
'react'
|
||||||
|
],
|
||||||
|
function(
|
||||||
|
React
|
||||||
|
) {
|
||||||
|
var FormFieldTextarea = React.createClass({
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
type="text"
|
||||||
|
className="regular-text"
|
||||||
|
name={ this.props.field.name }
|
||||||
|
id={ 'field_'+this.props.field.name }
|
||||||
|
value={ this.props.item[this.props.field.name] }
|
||||||
|
onChange={ this.props.onValueChange } />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return FormFieldTextarea;
|
||||||
|
});
|
@ -4,206 +4,15 @@ define(
|
|||||||
'mailpoet',
|
'mailpoet',
|
||||||
'classnames',
|
'classnames',
|
||||||
'react-router',
|
'react-router',
|
||||||
'react-checkbox-group'
|
'form/fields/field.jsx'
|
||||||
],
|
],
|
||||||
function(
|
function(
|
||||||
React,
|
React,
|
||||||
MailPoet,
|
MailPoet,
|
||||||
classNames,
|
classNames,
|
||||||
Router,
|
Router,
|
||||||
CheckboxGroup
|
FormField
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
||||||
var FormFieldSelect = React.createClass({
|
|
||||||
render: function() {
|
|
||||||
var options =
|
|
||||||
Object.keys(this.props.field.values).map(function(value, index) {
|
|
||||||
return (
|
|
||||||
<option
|
|
||||||
key={ 'option-' + index }
|
|
||||||
value={ value }>
|
|
||||||
{ this.props.field.values[value] }
|
|
||||||
</option>
|
|
||||||
);
|
|
||||||
}.bind(this)
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<select
|
|
||||||
name={ this.props.field.name }
|
|
||||||
id={ 'field_'+this.props.field.name }
|
|
||||||
value={ this.props.item[this.props.field.name] }
|
|
||||||
onChange={ this.props.onValueChange }>
|
|
||||||
{options}
|
|
||||||
</select>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var FormFieldRadio = React.createClass({
|
|
||||||
render: function() {
|
|
||||||
var selected_value = this.props.item[this.props.field.name];
|
|
||||||
var count = Object.keys(this.props.field.values).length;
|
|
||||||
|
|
||||||
var options = Object.keys(this.props.field.values).map(
|
|
||||||
function(value, index) {
|
|
||||||
return (
|
|
||||||
<p key={ 'radio-' + index }>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
checked={ selected_value === value }
|
|
||||||
value={ value }
|
|
||||||
onChange={ this.props.onValueChange }
|
|
||||||
name={ this.props.field.name } />
|
|
||||||
{ this.props.field.values[value] }
|
|
||||||
</label>
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}.bind(this)
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{ options }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var FormFieldCheckbox = React.createClass({
|
|
||||||
render: function() {
|
|
||||||
var selected_values = this.props.item[this.props.field.name] || '';
|
|
||||||
if(
|
|
||||||
selected_values !== undefined
|
|
||||||
&& selected_values.constructor !== Array
|
|
||||||
) {
|
|
||||||
selected_values = selected_values.split(';').map(function(value) {
|
|
||||||
return value.trim();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
var count = Object.keys(this.props.field.values).length;
|
|
||||||
|
|
||||||
var options = Object.keys(this.props.field.values).map(
|
|
||||||
function(value, index) {
|
|
||||||
return (
|
|
||||||
<p key={ 'checkbox-' + index }>
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" value={ value } />
|
|
||||||
{ this.props.field.values[value] }
|
|
||||||
</label>
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}.bind(this)
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CheckboxGroup
|
|
||||||
name={ this.props.field.name }
|
|
||||||
value={ selected_values }
|
|
||||||
ref={ this.props.field.name }
|
|
||||||
onChange={ this.handleValueChange }>
|
|
||||||
{ options }
|
|
||||||
</CheckboxGroup>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
handleValueChange: function() {
|
|
||||||
var field = this.props.field.name;
|
|
||||||
var group = this.refs[field];
|
|
||||||
var selected_values = [];
|
|
||||||
|
|
||||||
if(group !== undefined) {
|
|
||||||
selected_values = group.getCheckedValues();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.props.onValueChange({
|
|
||||||
target: {
|
|
||||||
name: field,
|
|
||||||
value: selected_values.join(';')
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var FormFieldText = React.createClass({
|
|
||||||
render: function() {
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="regular-text"
|
|
||||||
name={ this.props.field.name }
|
|
||||||
id={ 'field_'+this.props.field.name }
|
|
||||||
value={ this.props.item[this.props.field.name] }
|
|
||||||
onChange={ this.props.onValueChange } />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var FormFieldTextarea = React.createClass({
|
|
||||||
render: function() {
|
|
||||||
return (
|
|
||||||
<textarea
|
|
||||||
type="text"
|
|
||||||
className="regular-text"
|
|
||||||
name={ this.props.field.name }
|
|
||||||
id={ 'field_'+this.props.field.name }
|
|
||||||
value={ this.props.item[this.props.field.name] }
|
|
||||||
onChange={ this.props.onValueChange } />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var FormField = React.createClass({
|
|
||||||
render: function() {
|
|
||||||
|
|
||||||
var description = false;
|
|
||||||
if(this.props.field.description) {
|
|
||||||
description = (
|
|
||||||
<p className="description">{ this.props.field.description }</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var field = false;
|
|
||||||
|
|
||||||
switch(this.props.field.type) {
|
|
||||||
case 'text':
|
|
||||||
field = (<FormFieldText {...this.props} />);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'textarea':
|
|
||||||
field = (<FormFieldTextarea {...this.props} />);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'select':
|
|
||||||
field = (<FormFieldSelect {...this.props} />);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'radio':
|
|
||||||
field = (<FormFieldRadio {...this.props} />);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'checkbox':
|
|
||||||
field = (<FormFieldCheckbox {...this.props} />);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr>
|
|
||||||
<th scope="row">
|
|
||||||
<label
|
|
||||||
htmlFor={ 'field_'+this.props.field.name }
|
|
||||||
>{ this.props.field.label }</label>
|
|
||||||
</th>
|
|
||||||
<td>
|
|
||||||
{ field }
|
|
||||||
{ description }
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var Form = React.createClass({
|
var Form = React.createClass({
|
||||||
mixins: [
|
mixins: [
|
||||||
Router.Navigation
|
Router.Navigation
|
||||||
@ -305,25 +114,42 @@ define(
|
|||||||
});
|
});
|
||||||
|
|
||||||
var formClasses = classNames(
|
var formClasses = classNames(
|
||||||
|
'mailpoet_form',
|
||||||
{ 'mailpoet_form_loading': this.state.loading }
|
{ 'mailpoet_form_loading': this.state.loading }
|
||||||
);
|
);
|
||||||
|
|
||||||
var fields = this.props.fields.map(function(field, index) {
|
var fields = this.props.fields.map(function(field, i) {
|
||||||
return (
|
// if(field['fields'] !== undefined) {
|
||||||
<FormField
|
// return field.fields.map(function(subfield, j) {
|
||||||
field={ field }
|
// return (
|
||||||
item={ this.state.item }
|
// <FormField
|
||||||
onValueChange={ this.handleValueChange }
|
// field={ subfield }
|
||||||
key={ 'field-'+index } />
|
// item={ this.state.item }
|
||||||
);
|
// onValueChange={ this.handleValueChange }
|
||||||
|
// key={ 'subfield-'+j } />
|
||||||
|
// );
|
||||||
|
// }.bind(this));
|
||||||
|
// } else {
|
||||||
|
return (
|
||||||
|
<FormField
|
||||||
|
field={ field }
|
||||||
|
item={ this.state.item }
|
||||||
|
onValueChange={ this.handleValueChange }
|
||||||
|
key={ 'field-'+i } />
|
||||||
|
);
|
||||||
|
// }
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
ref="form"
|
ref="form"
|
||||||
className={ formClasses }
|
className={ formClasses }
|
||||||
onSubmit={ this.handleSubmit }>
|
onSubmit={
|
||||||
|
(this.props.onSubmit !== undefined)
|
||||||
|
? this.props.onSubmit
|
||||||
|
: this.handleSubmit
|
||||||
|
}
|
||||||
|
>
|
||||||
{ errors }
|
{ errors }
|
||||||
|
|
||||||
<table className="form-table">
|
<table className="form-table">
|
||||||
|
@ -4,14 +4,16 @@ define(
|
|||||||
'react-router',
|
'react-router',
|
||||||
'newsletters/list.jsx',
|
'newsletters/list.jsx',
|
||||||
'newsletters/types.jsx',
|
'newsletters/types.jsx',
|
||||||
'newsletters/templates.jsx'
|
'newsletters/templates.jsx',
|
||||||
|
'newsletters/send.jsx'
|
||||||
],
|
],
|
||||||
function(
|
function(
|
||||||
React,
|
React,
|
||||||
Router,
|
Router,
|
||||||
NewsletterList,
|
NewsletterList,
|
||||||
NewsletterTypes,
|
NewsletterTypes,
|
||||||
NewsletterTemplates
|
NewsletterTemplates,
|
||||||
|
NewsletterSend
|
||||||
) {
|
) {
|
||||||
var DefaultRoute = Router.DefaultRoute;
|
var DefaultRoute = Router.DefaultRoute;
|
||||||
var Link = Router.Link;
|
var Link = Router.Link;
|
||||||
@ -31,6 +33,7 @@ define(
|
|||||||
<Route name="app" path="/" handler={App}>
|
<Route name="app" path="/" handler={App}>
|
||||||
<Route name="new" path="/new" handler={ NewsletterTypes } />
|
<Route name="new" path="/new" handler={ NewsletterTypes } />
|
||||||
<Route name="template" path="/new/:type" handler={ NewsletterTemplates } />
|
<Route name="template" path="/new/:type" handler={ NewsletterTemplates } />
|
||||||
|
<Route name="send" path="/send/:id" handler={ NewsletterSend } />
|
||||||
<NotFoundRoute handler={ NewsletterList } />
|
<NotFoundRoute handler={ NewsletterList } />
|
||||||
<DefaultRoute handler={ NewsletterList } />
|
<DefaultRoute handler={ NewsletterList } />
|
||||||
</Route>
|
</Route>
|
||||||
|
103
assets/js/src/newsletters/send.jsx
Normal file
103
assets/js/src/newsletters/send.jsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
define(
|
||||||
|
[
|
||||||
|
'react',
|
||||||
|
'mailpoet',
|
||||||
|
'form/form.jsx',
|
||||||
|
'form/fields/selection.jsx',
|
||||||
|
'newsletters/breadcrumb.jsx'
|
||||||
|
],
|
||||||
|
function(
|
||||||
|
React,
|
||||||
|
MailPoet,
|
||||||
|
Form,
|
||||||
|
Selection,
|
||||||
|
Breadcrumb
|
||||||
|
) {
|
||||||
|
var fields = [
|
||||||
|
{
|
||||||
|
name: 'subject',
|
||||||
|
label: 'Subject line',
|
||||||
|
tip: "Be creative! It's the first thing your subscribers see."+
|
||||||
|
"Tempt them to open your email.",
|
||||||
|
type: 'text'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'list',
|
||||||
|
label: 'Lists',
|
||||||
|
tip: "The subscriber list that will be used for this campaign.",
|
||||||
|
field: (
|
||||||
|
<Selection
|
||||||
|
placeholder="Select a list"
|
||||||
|
id="mailpoet_segments"
|
||||||
|
endpoint="segments" />
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sender',
|
||||||
|
label: 'Sender',
|
||||||
|
tip: "Name & email of yourself or your company.",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'from_name',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: 'John Doe',
|
||||||
|
size: 'auto'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'from_email',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: 'john.doe@email.com',
|
||||||
|
size: 'auto'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'reply-to',
|
||||||
|
label: 'Reply-to',
|
||||||
|
tip: 'When the subscribers hit "reply" this is who will receive their '+
|
||||||
|
'email.',
|
||||||
|
inline: true,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'reply_to_name',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: 'John Doe',
|
||||||
|
size: 'auto'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'reply_to_email',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: 'john.doe@email.com',
|
||||||
|
size: 'auto'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
var messages = {
|
||||||
|
updated: function() {
|
||||||
|
MailPoet.Notice.success('Newsletter succesfully updated!');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var NewsletterSend = React.createClass({
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Final step: last details</h1>
|
||||||
|
|
||||||
|
<Breadcrumb step="send" />
|
||||||
|
|
||||||
|
<Form
|
||||||
|
endpoint="newsletters"
|
||||||
|
fields={ fields }
|
||||||
|
params={ this.props.params }
|
||||||
|
messages={ messages } />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return NewsletterSend;
|
||||||
|
}
|
||||||
|
);
|
@ -60,7 +60,7 @@ define(
|
|||||||
}.bind(this));
|
}.bind(this));
|
||||||
},
|
},
|
||||||
handlePreviewTemplate: function(template) {
|
handlePreviewTemplate: function(template) {
|
||||||
console.log('preview '+template.id);
|
console.log('preview template #'+template.id);
|
||||||
},
|
},
|
||||||
handleDeleteTemplate: function(template) {
|
handleDeleteTemplate: function(template) {
|
||||||
this.setState({ loading: true });
|
this.setState({ loading: true });
|
||||||
@ -101,6 +101,7 @@ define(
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
|
style={ { display: 'none' }}
|
||||||
className="button button-secondary"
|
className="button button-secondary"
|
||||||
onClick={ this.handlePreviewTemplate.bind(null, template) }
|
onClick={ this.handlePreviewTemplate.bind(null, template) }
|
||||||
>
|
>
|
||||||
|
@ -3,6 +3,7 @@ define(
|
|||||||
'react',
|
'react',
|
||||||
'react-router',
|
'react-router',
|
||||||
'listing/listing.jsx',
|
'listing/listing.jsx',
|
||||||
|
'form/fields/selection.jsx',
|
||||||
'classnames',
|
'classnames',
|
||||||
'mailpoet',
|
'mailpoet',
|
||||||
'jquery',
|
'jquery',
|
||||||
@ -12,6 +13,7 @@ define(
|
|||||||
React,
|
React,
|
||||||
Router,
|
Router,
|
||||||
Listing,
|
Listing,
|
||||||
|
Selection,
|
||||||
classNames,
|
classNames,
|
||||||
MailPoet,
|
MailPoet,
|
||||||
jQuery
|
jQuery
|
||||||
@ -57,111 +59,6 @@ define(
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
var ItemSelection = React.createClass({
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
loading: false,
|
|
||||||
items: [],
|
|
||||||
selected: false,
|
|
||||||
multiple: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
componentDidMount: function() {
|
|
||||||
// this.loadItems();
|
|
||||||
this.loadCachedItems();
|
|
||||||
},
|
|
||||||
loadCachedItems: function() {
|
|
||||||
if(typeof(window['mailpoet_'+this.props.endpoint]) !== 'undefined') {
|
|
||||||
var items = window['mailpoet_'+this.props.endpoint];
|
|
||||||
this.setState({
|
|
||||||
items: items
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
loadItems: function() {
|
|
||||||
this.setState({ loading: true });
|
|
||||||
|
|
||||||
MailPoet.Ajax.post({
|
|
||||||
endpoint: this.props.endpoint,
|
|
||||||
action: 'listing',
|
|
||||||
data: {
|
|
||||||
'offset': 0,
|
|
||||||
'limit': 100,
|
|
||||||
'search': '',
|
|
||||||
'sort_by': 'name',
|
|
||||||
'sort_order': 'asc'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.done(function(response) {
|
|
||||||
if(this.isMounted()) {
|
|
||||||
if(response === false) {
|
|
||||||
this.setState({
|
|
||||||
loading: false,
|
|
||||||
items: []
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
loading: false,
|
|
||||||
items: response.items
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.bind(this));
|
|
||||||
},
|
|
||||||
handleChange: function() {
|
|
||||||
var new_value = this.refs.selection.getDOMNode().value;
|
|
||||||
|
|
||||||
if(this.state.multiple === false) {
|
|
||||||
if(new_value.trim().length === 0) {
|
|
||||||
new_value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
selected: new_value
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
var selected_values = this.state.selected || [];
|
|
||||||
|
|
||||||
if(selected_values.indexOf(new_value) !== -1) {
|
|
||||||
// value already present so remove it
|
|
||||||
selected_values.splice(selected_values.indexOf(new_value), 1);
|
|
||||||
} else {
|
|
||||||
selected_values.push(new_value);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
selected: selected_values
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getSelected: function() {
|
|
||||||
return this.state.selected;
|
|
||||||
},
|
|
||||||
render: function() {
|
|
||||||
var options = this.state.items.map(function(item, index) {
|
|
||||||
return (
|
|
||||||
<option
|
|
||||||
key={ 'action-' + index }
|
|
||||||
value={ item.id }>
|
|
||||||
{ item.name }
|
|
||||||
</option>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<select
|
|
||||||
ref="selection"
|
|
||||||
id={ this.props.id }
|
|
||||||
value={ this.state.selected }
|
|
||||||
onChange={ this.handleChange }
|
|
||||||
multiple={ this.state.multiple }>
|
|
||||||
<option value="">Select a list</option>
|
|
||||||
{ options }
|
|
||||||
</select>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var bulk_actions = [
|
var bulk_actions = [
|
||||||
{
|
{
|
||||||
name: 'moveToList',
|
name: 'moveToList',
|
||||||
|
@ -110,6 +110,9 @@ class Menu {
|
|||||||
|
|
||||||
function newsletters() {
|
function newsletters() {
|
||||||
$data = array();
|
$data = array();
|
||||||
|
|
||||||
|
$data['segments'] = Segment::findArray();
|
||||||
|
|
||||||
echo $this->renderer->render('newsletters.html', $data);
|
echo $this->renderer->render('newsletters.html', $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,7 +23,6 @@
|
|||||||
|
|
||||||
<!-- javascripts -->
|
<!-- javascripts -->
|
||||||
<%= javascript(
|
<%= javascript(
|
||||||
'src/_phantomjs_shim.js',
|
|
||||||
'vendor.js',
|
'vendor.js',
|
||||||
'mailpoet.js',
|
'mailpoet.js',
|
||||||
'admin.js'
|
'admin.js'
|
||||||
|
@ -9,4 +9,8 @@
|
|||||||
'loadingItems': __('Loading newsletters...'),
|
'loadingItems': __('Loading newsletters...'),
|
||||||
'noItemsFound': __('No newsletters found.')
|
'noItemsFound': __('No newsletters found.')
|
||||||
}) %>
|
}) %>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
var mailpoet_segments = <%= json_encode(segments) %>;
|
||||||
|
</script>
|
||||||
<% endblock %>
|
<% endblock %>
|
||||||
|
Reference in New Issue
Block a user