Add to campaign modal

This commit is contained in:
Christopher Joe 2016-08-08 16:53:21 +12:00 committed by Ingo Schommer
parent fb64e27960
commit 6a4b29d703
16 changed files with 336 additions and 10 deletions

View File

@ -18,6 +18,8 @@ if (typeof(ss) === 'undefined' || typeof(ss.i18n) === 'undefined') {
"Campaigns.ITEM_SUMMARY_SINGULAR": "%s item",
"Campaigns.PUBLISHCAMPAIGN": "Publish campaign",
"Campaigns.REVERTCAMPAIGN": "Revert",
"Campaigns.AddToCampaign": "Add To Campaign",
"SiteTree.MoreOptions": "More options",
"LeftAndMain.CONFIRMUNSAVED": "Are you sure you want to navigate away from this page?\n\nWARNING: Your changes have not been saved.\n\nPress OK to continue, or Cancel to stay on the current page.",
"LeftAndMain.CONFIRMUNSAVEDSHORT": "WARNING: Your changes have not been saved.",
"LeftAndMain.PAGEWASDELETED": "This page was deleted. To edit a page, select it from the left.",
@ -27,4 +29,4 @@ if (typeof(ss) === 'undefined' || typeof(ss.i18n) === 'undefined') {
"ModelAdmin.VALIDATIONERROR": "Validation Error",
"SecurityAdmin.BATCHACTIONSDELETECONFIRM": "Do you really want to delete %s groups?"
});
}
}

View File

@ -12,6 +12,7 @@ import RecordsReducer from 'state/records/RecordsReducer';
import CampaignReducer from 'state/campaign/CampaignReducer';
import BreadcrumbsReducer from 'state/breadcrumbs/BreadcrumbsReducer';
import TextField from 'components/TextField/TextField';
import SingleSelectField from 'components/SingleSelectField/SingleSelectField';
import HiddenField from 'components/HiddenField/HiddenField';
import GridField from 'components/GridField/GridField';
import FormAction from 'components/FormAction/FormAction';
@ -37,6 +38,7 @@ function appBoot() {
injector.register('TextField', TextField);
injector.register('HiddenField', HiddenField);
injector.register('GridField', GridField);
injector.register('SingleSelectField', SingleSelectField);
injector.register('PopoverField', PopoverField);
injector.register('HeaderField', HeaderField);
injector.register('LiteralField', LiteralField);

View File

@ -0,0 +1,58 @@
import React from 'react';
import { Modal } from 'react-bootstrap-4';
import SilverStripeComponent from 'lib/SilverStripeComponent';
import FormBuilder from 'components/FormBuilder/FormBuilder';
import Config from 'lib/Config';
class AddToCampaignModal extends SilverStripeComponent {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit(event, fieldValues, submitFn) {
if (!fieldValues.Campaign && fieldValues.Campaign !== 0) {
event.preventDefault();
return;
}
if (typeof this.props.handleSubmit === 'function') {
this.props.handleSubmit(event, fieldValues, submitFn);
return;
}
event.preventDefault();
submitFn();
}
getBody() {
const schemaUrl = `${this.props.schemaUrl}/${this.props.fileId}`;
return <FormBuilder schemaUrl={schemaUrl} handleSubmit={this.handleSubmit} />;
}
render() {
const body = this.getBody();
return <Modal show={this.props.show} onHide={this.props.handleHide} container={document.getElementsByClassName('cms-container')[0]}>
<Modal.Header closeButton>
<Modal.Title>{this.props.title + ' - Test'}</Modal.Title>
</Modal.Header>
<Modal.Body>
{body}
</Modal.Body>
</Modal>;
}
}
AddToCampaignModal.propTypes = {
show: React.PropTypes.bool.isRequired,
title: React.PropTypes.string,
handleHide: React.PropTypes.func,
schemaUrl: React.PropTypes.string,
handleSubmit: React.PropTypes.func,
};
export default AddToCampaignModal;

View File

@ -205,9 +205,13 @@ export class FormBuilderComponent extends SilverStripeComponent {
handleSubmit(event) {
const schemaFields = this.props.schemas[this.props.schemaUrl].schema.fields;
const fieldValues = this.props.form[this.getFormId()].fields
.reduce((prev, curr) => Object.assign({}, prev, {
[schemaFields.find(schemaField => schemaField.id === curr.id).name]: curr.value,
}), {});
.reduce((prev, curr) => {
const fieldName = schemaFields.find(schemaField => schemaField.id === curr.id).name;
return Object.assign({}, prev, {
[fieldName]: curr.value,
});
}, {});
const submitFn = () => this.props.formActions.submitForm(
this.submitApi,

View File

@ -0,0 +1,41 @@
# Single Select Field
Generates a `<select><option></option></select>`
## Props
### leftTitle
The label text to display with the field.
### extraClass
Addition CSS classes to apply to the `<select>` element.
### name (required)
Used for the field's `name` attribute.
### onChange
Handler function called when the field's value changes.
### value
The field's value.
### source (required)
The list of possible values that could be selected
### disabled
A list of values within `source` that can be seen but not selected
### hasEmptyDefault
If true, create an empty value option first
### emptyString
When `hasEmptyDefault` is true, this sets the label for the option

View File

@ -0,0 +1,144 @@
import React from 'react';
import SilverStripeComponent from 'lib/SilverStripeComponent';
class SingleSelectField extends SilverStripeComponent {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
render() {
const labelText = this.props.leftTitle !== null
? this.props.leftTitle
: this.props.title;
let field = null;
if (this.props.readOnly) {
field = this.getReadonlyField;
} else {
field = this.getSelectField();
}
// The extraClass property is defined on both the holder and element
// for legacy reasons (same behaviour as PHP rendering)
const classNames = ['form-group', this.props.extraClass].join(' ');
return (
<div className={classNames}>
{labelText &&
<label className="form__field-label" htmlFor={`gallery_${this.props.name}`}>
{labelText}
</label>
}
<div className="form__field-holder">
{field}
</div>
</div>
);
}
/**
* Builds the select field in readonly mode with current props
*
* @returns ReactComponent
*/
getReadonlyField() {
let label = this.props.source[this.props.value];
label = label !== null
? label
: this.props.value;
return <div><i>{label}</i></div>;
}
/**
* Builds the select field with current props
*
* @returns ReactComponent
*/
getSelectField() {
const options = this.props.source.map((item) => {
return Object.assign({},
item,
{disabled: this.props.data.disabled.indexOf(item.value) > -1}
);
});
if (this.props.hasEmptyDefault) {
options.unshift({
value: '',
title: this.props.emptyString
});
}
return <select {...this.getInputProps()}>
{ options.map((item) => {
const key = `${this.props.name}-${item.value || 'null'}`;
return <option key={key} value={item.value} disabled={item.disabled}>{item.title}</option>;
}) }
</select>;
}
/**
* Fetches the properties for the select field
*
* @returns Object properties
*/
getInputProps() {
return {
// The extraClass property is defined on both the holder and element
// for legacy reasons (same behaviour as PHP rendering)
className: ['form-control', this.props.extraClass].join(' '),
id: this.props.id,
name: this.props.name,
onChange: this.handleChange,
value: this.props.value,
};
}
/**
* Handles changes to the select field's value.
*
* @param Object event
*/
handleChange(event) {
if (typeof this.props.onChange === 'undefined') {
return;
}
this.props.onChange(event, {id: this.props.id, value: event.target.value});
}
}
SingleSelectField.propTypes = {
leftTitle: React.PropTypes.string,
extraClass: React.PropTypes.string,
id: React.PropTypes.string,
name: React.PropTypes.string.isRequired,
onChange: React.PropTypes.func,
value: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.number]),
readOnly: React.PropTypes.bool,
source: React.PropTypes.arrayOf(React.PropTypes.shape({
value: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.number]),
title: React.PropTypes.string,
})).isRequired,
data: React.PropTypes.shape({
disabled: React.PropTypes.arrayOf(
React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.number])
),
}),
hasEmptyDefault: React.PropTypes.bool,
emptyString: React.PropTypes.string,
};
SingleSelectField.defaultProps = {
data: {
disabled: [],
},
emptyString: '',
};
export default SingleSelectField;

View File

@ -0,0 +1,3 @@
select.form-control:not([size]):not([multiple]) {
height: 2.5rem;
}

View File

@ -16,10 +16,10 @@ Addition CSS classes to apply to the `<input>` element.
Used for the field's `name` attribute.
### handleFieldUpdate
### onChange
Handler function called when the field's value changes.
### value
The field's value.
The field's value.

View File

@ -22,13 +22,18 @@ class TextField extends SilverStripeComponent {
return field;
}
/**
* Fetches the properties for the text field
*
* @returns Object properties
*/
getInputProps() {
// @todo Merge with 'attributes' from formfield schema
return {
// The extraClass property is defined on both the holder and element
// for legacy reasons (same behaviour as PHP rendering)
className: ['form-control', this.props.extraClass].join(' '),
id: `gallery_${this.props.name}`,
id: this.props.id,
name: this.props.name,
onChange: this.handleChange,
type: 'text',
@ -54,6 +59,7 @@ TextField.propTypes = {
leftTitle: React.PropTypes.string,
title: React.PropTypes.string,
extraClass: React.PropTypes.string,
id: React.PropTypes.string,
name: React.PropTypes.string.isRequired,
onChange: React.PropTypes.func,
value: React.PropTypes.string,

View File

@ -1,7 +1,7 @@
import jQuery from 'jQuery';
jQuery.entwine('ss', ($) => {
$('.add-to-campaign-action, #add-to-campaign__action').entwine({
$('#add-to-campaign__dialog .add-to-campaign-action, .cms-content-actions .add-to-campaign-action, #add-to-campaign__action').entwine({
onclick() {
let dialog = $('#add-to-campaign__dialog');

View File

@ -32,6 +32,8 @@ class Injector {
return this.components.TextField;
case 'Hidden':
return this.components.HiddenField;
case 'SingleSelect':
return this.components.SingleSelectField;
case 'Custom':
return this.components.GridField;
default:

View File

@ -57,13 +57,16 @@ export function addForm(formState) {
*/
export function submitForm(submitApi, formId, fieldValues) {
return (dispatch) => {
const header = { 'X-Formschema-Request': 'state' };
const headers = {
'X-Formschema-Request': 'state',
'X-Requested-With': 'XMLHttpRequest',
};
dispatch({
type: ACTION_TYPES.SUBMIT_FORM_REQUEST,
payload: { formId },
});
return submitApi(Object.assign({ ID: formId }, fieldValues), header)
return submitApi(Object.assign({ ID: formId }, fieldValues), headers)
.then((response) => {
dispatch({
type: ACTION_TYPES.SUBMIT_FORM_SUCCESS,

View File

@ -16,6 +16,7 @@ function formReducer(state = initialState, action) {
case ACTION_TYPES.SUBMIT_FORM_REQUEST:
return deepFreeze(updateForm(action.payload.formId, {
error: false,
submitting: true,
}));
@ -33,6 +34,7 @@ function formReducer(state = initialState, action) {
return deepFreeze(Object.assign({}, state, {
[action.payload.formState.id]: {
fields: action.payload.formState.fields,
error: false,
submitting: false,
},
}));
@ -50,12 +52,15 @@ function formReducer(state = initialState, action) {
case ACTION_TYPES.SUBMIT_FORM_SUCCESS:
return deepFreeze(updateForm(action.payload.response.id, {
fields: action.payload.response.state.fields,
error: false,
messages: action.payload.response.state.messages,
submitting: false,
}));
case ACTION_TYPES.SUBMIT_FORM_FAILURE:
return deepFreeze(updateForm(action.payload.formId, {
error: true,
messages: action.payload.error,
submitting: false,
}));

View File

@ -39,6 +39,24 @@ abstract class SelectField extends FormField {
parent::__construct($name, $title, $value);
}
public function getSchemaDataDefaults() {
$data = parent::getSchemaDataDefaults();
// Add options to 'data'
$source = $this->getSource();
$data['source'] = (is_array($source))
? array_map(function ($value, $title) {
return [
'value' => $value,
'title' => $title,
];
}, array_keys($source), $source)
: [];
$data['data']['disabled'] = $this->getDisabledItems();
return $data;
}
/**
* Mark certain elements as disabled,
* regardless of the {@link setDisabled()} settings.
@ -46,6 +64,7 @@ abstract class SelectField extends FormField {
* These should be items that appear in the source list, not in addition to them.
*
* @param array|SS_List $items Collection of values or items
* @return $this
*/
public function setDisabledItems($items){
$this->disabledItems = $this->getListValues($items);
@ -202,6 +221,7 @@ abstract class SelectField extends FormField {
}
public function performReadonlyTransformation() {
/** @var LookupField $field */
$field = $this->castedCopy('LookupField');
$field->setSource($this->getSource());
$field->setReadonly(true);

View File

@ -25,6 +25,32 @@ abstract class SingleSelectField extends SelectField {
protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_SINGLESELECT;
public function getSchemaStateDefaults() {
$data = parent::getSchemaStateDefaults();
// Add options to 'data'
$data['data']['hasEmptyDefault'] = $this->getHasEmptyDefault();
$data['data']['emptyString'] = $this->getHasEmptyDefault() ? $this->getEmptyString() : null;
$data['value'] = $this->getDefaultValue();
return $data;
}
public function getDefaultValue() {
$value = $this->Value();
// assign value to field, such as first option available
if ($value === null) {
if ($this->getHasEmptyDefault()) {
$value = '';
} else {
$values = $this->getValidValues();
$value = array_shift($values);
}
}
return $value;
}
/**
* @param boolean $bool
* @return self Self reference
@ -47,6 +73,7 @@ abstract class SingleSelectField extends SelectField {
* {@link $hasEmptyDefault} to true.
*
* @param string $string
* @return $this
*/
public function setEmptyString($string) {
$this->setHasEmptyDefault(true);

View File

@ -312,6 +312,15 @@ gulp.task('bundle-lib', function bundleLib() {
.require(`${PATHS.ADMIN_JS_SRC}/state/breadcrumbs/BreadcrumbsActions`,
{ expose: 'state/breadcrumbs/BreadcrumbsActions' }
)
.require(`${PATHS.ADMIN_JS_SRC}/components/PopoverField/PopoverField`,
{ expose: 'components/PopoverField/PopoverField' }
)
.require(`${PATHS.ADMIN_JS_SRC}/components/SingleSelectField/SingleSelectField`,
{ expose: 'components/SingleSelectField/SingleSelectField' }
)
.require(`${PATHS.ADMIN_JS_SRC}/components/AddToCampaignModal/AddToCampaignModal`,
{ expose: 'components/AddToCampaignModal/AddToCampaignModal' }
)
.require(`${PATHS.FRAMEWORK_JS_SRC}/i18n.js`,
{ expose: 'i18n' }
)