mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 12:05:37 +00:00
Add to campaign modal
This commit is contained in:
parent
fb64e27960
commit
6a4b29d703
@ -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?"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
@ -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,
|
||||
|
41
admin/client/src/components/SingleSelectField/README.md
Normal file
41
admin/client/src/components/SingleSelectField/README.md
Normal 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
|
@ -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;
|
@ -0,0 +1,3 @@
|
||||
select.form-control:not([size]):not([multiple]) {
|
||||
height: 2.5rem;
|
||||
}
|
@ -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.
|
||||
|
@ -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,
|
||||
|
@ -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');
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
}));
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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' }
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user