Add ESLint support

See https://github.com/silverstripe/silverstripe-framework/pull/5108
This commit is contained in:
Ingo Schommer 2016-03-31 10:45:54 +13:00
parent 6aa22c38ed
commit 34d40bed5f
52 changed files with 1675 additions and 1552 deletions

View File

@ -17,6 +17,10 @@ trim_trailing_whitespace = false
indent_size = 2
indent_style = space
[*.js]
indent_size = 2
indent_style = space
# Don't perform any clean-up on thirdparty files
[thirdparty/**]

37
.eslintignore Normal file
View File

@ -0,0 +1,37 @@
# Ignore dist files
javascript/dist/
# Ignore legacy files
javascript/src/AssetUploadField.js
javascript/src/ConfirmedPasswordField.js
javascript/src/DateField.js
javascript/src/GridField.js
javascript/src/HtmlEditorField.js
javascript/src/InlineFormAction.js
javascript/src/PermissionCheckboxSetField.js
javascript/src/SelectionGroup.js
javascript/src/TabSet.js
javascript/src/TinyMCE_SSPlugin.js
javascript/src/ToggleCompositeField.js
javascript/src/ToggleField.js
javascript/src/TreeDropdownField.js
javascript/src/UploadField.js
javascript/src/UploadField_downloadtemplate.js
javascript/src/UploadField_select.js
javascript/src/UploadField_uploadtemplate.js
javascript/src/i18n.js
javascript/src/i18nx.js
javascript/src/jQuery.js
admin/javascript/src/LeftAndMain.js
admin/javascript/src/LeftAndMain.*.js
admin/javascript/src/CMSSecurity.js
admin/javascript/src/MemberDatetimeOptionsetField.js
admin/javascript/src/MemberImportForm.js
admin/javascript/src/ModelAdmin.js
admin/javascript/src/SecurityAdmin.js
admin/javascript/src/leaktools.js
admin/javascript/src/sspath.js
admin/javascript/src/ssui.core.js
# Ignore tests
admin/javascript/**/tests/

3
.eslintrc Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "airbnb"
}

View File

@ -6,38 +6,38 @@ import $ from 'jQuery';
$.entwine('ss', function ($) {
$('.cms-description-toggle').entwine({
onadd: function () {
var shown = false, // Current state of the description.
fieldId = this.prop('id').substr(0, this.prop('id').indexOf('_Holder')),
$trigger = this.find('.cms-description-trigger'), // Click target for toggling the description.
$description = this.find('.description');
$('.cms-description-toggle').entwine({
onadd: function () {
var shown = false, // Current state of the description.
fieldId = this.prop('id').substr(0, this.prop('id').indexOf('_Holder')),
$trigger = this.find('.cms-description-trigger'), // Click target for toggling the description.
$description = this.find('.description');
// Prevent multiple events being added.
if (this.hasClass('description-toggle-enabled')) {
return;
}
// Prevent multiple events being added.
if (this.hasClass('description-toggle-enabled')) {
return;
}
// If a custom trigger han't been supplied use a sensible default.
if ($trigger.length === 0) {
$trigger = this
.find('.middleColumn')
.first() // Get the first middleColumn so we don't add multiple triggers on composite field types.
.after('<label class="right" for="' + fieldId + '"><a class="cms-description-trigger" href="javascript:void(0)"><span class="btn-icon-information"></span></a></label>')
.next();
}
// If a custom trigger han't been supplied use a sensible default.
if ($trigger.length === 0) {
$trigger = this
.find('.middleColumn')
.first() // Get the first middleColumn so we don't add multiple triggers on composite field types.
.after('<label class="right" for="' + fieldId + '"><a class="cms-description-trigger" href="javascript:void(0)"><span class="btn-icon-information"></span></a></label>')
.next();
}
this.addClass('description-toggle-enabled');
this.addClass('description-toggle-enabled');
// Toggle next description when button is clicked.
$trigger.on('click', function() {
$description[shown ? 'hide' : 'show']();
shown = !shown;
});
// Toggle next description when button is clicked.
$trigger.on('click', function() {
$description[shown ? 'hide' : 'show']();
shown = !shown;
});
// Hide next description by default.
$description.hide();
}
});
// Hide next description by default.
$description.hide();
}
});
});

View File

@ -15,11 +15,11 @@ $.entwine('ss', function($){
* <ul class="cms-menu-list">
* <li><a href="#">Item 1</a></li>
* <li class="current opened">
* <a href="#">Item 2</a>
* <ul>
* <li class="current opened"><a href="#">Item 2.1</a></li>
* <li><a href="#">Item 2.2</a></li>
* </ul>
* <a href="#">Item 2</a>
* <ul>
* <li class="current opened"><a href="#">Item 2.1</a></li>
* <li><a href="#">Item 2.2</a></li>
* </ul>
* </li>
* </ul>
*
@ -38,7 +38,7 @@ $.entwine('ss', function($){
$(this).addClass('collapse');
}
});
} else { //collapse
} else { //collapse
$(this).children('ul').each(function() {
$(this).addClass('collapsed-flyout');
$(this).hasClass('collapse');
@ -59,7 +59,7 @@ $.entwine('ss', function($){
//hide all the flyout-indicator
$('.cms-menu-list').find('.child-flyout-indicator').hide();
} else { //collapse
} else { //collapse
//hide the flyout only if it is not the current section
$('.collapsed-flyout').find('li').each(function() {
//if (!$(this).hasClass('current'))
@ -110,11 +110,11 @@ $.entwine('ss', function($){
* @func getEvaluatedCollapsedState
* @return {boolean} - Returns true if the menu should be collapsed, false if expanded.
* @desc Evaluate whether the menu should be collapsed.
* The basic rule is "If the SiteTree (middle column) is present, collapse the menu, otherwise expand the menu".
* This reason behind this is to give the content area more real estate when the SiteTree is present.
* The user may wish to override this automatic behaviour and have the menu expanded or collapsed at all times.
* So unlike manually toggling the menu, the automatic behaviour never updates the menu's cookie value.
* Here we use the manually set state and the automatic behaviour to evaluate what the collapsed state should be.
* The basic rule is "If the SiteTree (middle column) is present, collapse the menu, otherwise expand the menu".
* This reason behind this is to give the content area more real estate when the SiteTree is present.
* The user may wish to override this automatic behaviour and have the menu expanded or collapsed at all times.
* So unlike manually toggling the menu, the automatic behaviour never updates the menu's cookie value.
* Here we use the manually set state and the automatic behaviour to evaluate what the collapsed state should be.
*/
getEvaluatedCollapsedState: function () {
var shouldCollapse,
@ -269,7 +269,7 @@ $.entwine('ss', function($){
$('.collapsed-flyout').show();
fly.addClass('opened');
fly.children('ul').find('li').fadeIn('fast');
} else { //collapse
} else { //collapse
if(li) {
li.remove();
}

View File

@ -501,17 +501,17 @@ $.entwine('ss.preview', function($){
_loadCurrentPage: function() {
if (!this.getIsPreviewEnabled()) return;
var doc,
containerEl = $('.cms-container');
try {
doc = this.find('iframe')[0].contentDocument;
} catch (e) {
// iframe can't be accessed - might be secure?
console.warn('Unable to access iframe, possible https mis-match');
}
if (!doc) {
return;
}
var doc,
containerEl = $('.cms-container');
try {
doc = this.find('iframe')[0].contentDocument;
} catch (e) {
// iframe can't be accessed - might be secure?
console.warn('Unable to access iframe, possible https mis-match');
}
if (!doc) {
return;
}
// Load this page in the admin interface if appropriate
var id = $(doc).find('meta[name=x-page-id]').attr('content');
@ -529,21 +529,21 @@ $.entwine('ss.preview', function($){
* Prepare the iframe content for preview.
*/
_adjustIframeForPreview: function() {
var iframe = this.find('iframe')[0],
doc;
if(!iframe){
return;
}
var iframe = this.find('iframe')[0],
doc;
if(!iframe){
return;
}
try {
doc = iframe.contentDocument;
} catch (e) {
// iframe can't be accessed - might be secure?
console.warn('Unable to access iframe, possible https mis-match');
}
if(!doc) {
return;
}
try {
doc = iframe.contentDocument;
} catch (e) {
// iframe can't be accessed - might be secure?
console.warn('Unable to access iframe, possible https mis-match');
}
if(!doc) {
return;
}
// Open external links in new window to avoid "escaping" the internal page context in the preview
// iframe, which is important to stay in for the CMS logic.

View File

@ -10,24 +10,27 @@ import SchemaReducer from 'state/schema/reducer';
import RecordsReducer from 'state/records/reducer';
// Sections
// eslint-disable-next-line no-unused-vars
import CampaignAdmin from 'sections/campaign-admin/index';
function appBoot() {
reducerRegister.add('config', ConfigReducer);
reducerRegister.add('schemas', SchemaReducer);
reducerRegister.add('records', RecordsReducer);
reducerRegister.add('config', ConfigReducer);
reducerRegister.add('schemas', SchemaReducer);
reducerRegister.add('records', RecordsReducer);
const initialState = {};
const rootReducer = combineReducers(reducerRegister.getAll());
const createStoreWithMiddleware = applyMiddleware(thunkMiddleware, createLogger())(createStore);
const initialState = {};
const rootReducer = combineReducers(reducerRegister.getAll());
const createStoreWithMiddleware = applyMiddleware(thunkMiddleware, createLogger())(createStore);
// TODO: The store needs to be passed into route callbacks on the route context.
window.store = createStoreWithMiddleware(rootReducer, initialState);
// TODO: The store needs to be passed into route callbacks on the route context.
window.store = createStoreWithMiddleware(rootReducer, initialState);
// Set the initial config state.
configActions.setConfig(window.ss.config)(window.store.dispatch);
// Set the initial config state.
configActions.setConfig(window.ss.config)(window.store.dispatch);
}
// TODO: This should be using `window.onload` but isn't because Entwine hooks are being used to set up the <Provider>.
// `window.onload` happens AFTER these Entwine hooks which means the store is undefined when the <Provider> is constructed.
$('body').entwine({ onadd: function () { appBoot(); } });
// TODO: This should be using `window.onload` but isn't because
// Entwine hooks are being used to set up the <Provider>.
// `window.onload` happens AFTER these Entwine hooks which means
// the store is undefined when the <Provider> is constructed.
$('body').entwine({ onadd: () => { appBoot(); } });

View File

@ -2,101 +2,101 @@ import React from 'react';
import SilverStripeComponent from 'silverstripe-component';
class FormActionComponent extends SilverStripeComponent {
constructor(props) {
super(props);
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.handleClick = this.handleClick.bind(this);
}
render() {
return (
<button type={this.props.type} className={this.getButtonClasses()} onClick={this.handleClick}>
{this.getLoadingIcon()}
{this.props.label}
</button>
);
}
/**
* Returns the necessary button classes based on the given props
*
* @returns string
*/
getButtonClasses() {
let buttonClasses = 'btn';
// Add 'type' class
buttonClasses += ` btn-${this.props.style}`;
// If there is no text
if (typeof this.props.label === 'undefined') {
buttonClasses += ' no-text';
}
render() {
return (
<button type={this.props.type} className={this.getButtonClasses()} onClick={this.handleClick}>
{this.getLoadingIcon()}
{this.props.label}
</button>
);
// Add icon class
if (typeof this.props.icon !== 'undefined') {
buttonClasses += ` font-icon-${this.props.icon}`;
}
/**
* Returns the necessary button classes based on the given props
*
* @returns string
*/
getButtonClasses() {
var buttonClasses = 'btn';
// Add 'type' class
buttonClasses += ` btn-${this.props.style}`;
// If there is no text
if (typeof this.props.label === 'undefined') {
buttonClasses += ' no-text';
}
// Add icon class
if (typeof this.props.icon !== 'undefined') {
buttonClasses += ` font-icon-${this.props.icon}`;
}
// Add loading class
if (this.props.loading === true) {
buttonClasses += ' btn--loading';
}
// Add disabled class
if (this.props.disabled === true) {
buttonClasses += ' disabled';
}
return buttonClasses;
// Add loading class
if (this.props.loading === true) {
buttonClasses += ' btn--loading';
}
/**
* Returns markup for the loading icon
*
* @returns object|null
*/
getLoadingIcon() {
if (this.props.loading) {
return (
<div className="btn__loading-icon" >
<svg viewBox="0 0 44 12">
<circle cx="6" cy="6" r="6" />
<circle cx="22" cy="6" r="6" />
<circle cx="38" cy="6" r="6" />
</svg>
</div>
);
}
return null;
// Add disabled class
if (this.props.disabled === true) {
buttonClasses += ' disabled';
}
/**
* Event handler triggered when a user clicks the button.
*
* @param object event
* @returns null
*/
handleClick(event) {
this.props.handleClick(event);
return buttonClasses;
}
/**
* Returns markup for the loading icon
*
* @returns object|null
*/
getLoadingIcon() {
if (this.props.loading) {
return (
<div className="btn__loading-icon" >
<svg viewBox="0 0 44 12">
<circle cx="6" cy="6" r="6" />
<circle cx="22" cy="6" r="6" />
<circle cx="38" cy="6" r="6" />
</svg>
</div>
);
}
return null;
}
/**
* Event handler triggered when a user clicks the button.
*
* @param object event
* @returns null
*/
handleClick(event) {
this.props.handleClick(event);
}
}
FormActionComponent.propTypes = {
handleClick: React.PropTypes.func.isRequired,
label: React.PropTypes.string,
type: React.PropTypes.string,
loading: React.PropTypes.bool,
icon: React.PropTypes.string,
disabled: React.PropTypes.bool,
style: React.PropTypes.string
handleClick: React.PropTypes.func.isRequired,
label: React.PropTypes.string,
type: React.PropTypes.string,
loading: React.PropTypes.bool,
icon: React.PropTypes.string,
disabled: React.PropTypes.bool,
style: React.PropTypes.string,
};
FormActionComponent.defaultProps = {
type: 'button',
style: 'secondary'
type: 'button',
style: 'secondary',
};
export default FormActionComponent;

View File

@ -1,7 +1,6 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import $ from 'jQuery';
import * as schemaActions from 'state/schema/actions';
import SilverStripeComponent from 'silverstripe-component';
import FormComponent from 'components/form/index';
@ -14,219 +13,217 @@ import es6promise from 'es6-promise';
es6promise.polyfill();
// Using this to map field types to components until we implement dependency injection.
var fakeInjector = {
const fakeInjector = {
/**
* Components registered with the fake DI container.
*/
components: {
'TextField': TextField,
'GridField': GridField,
'HiddenField': HiddenField
},
/**
* Components registered with the fake DI container.
*/
components: {
TextField,
GridField,
HiddenField,
},
/**
* Gets the component matching the passed component name.
* Used when a component type is provided bt the form schema.
*
* @param string componentName - The name of the component to get from the injector.
*
* @return object|null
*/
getComponentByName: function (componentName) {
return this.components[componentName];
},
/**
* Gets the component matching the passed component name.
* Used when a component type is provided bt the form schema.
*
* @param string componentName - The name of the component to get from the injector.
*
* @return object|null
*/
getComponentByName(componentName) {
return this.components[componentName];
},
/**
* Default data type to component mappings.
* Used as a fallback when no component type is provided in the form schema.
*
* @param string dataType - The data type provided by the form schema.
*
* @return object|null
*/
getComponentByDataType: function (dataType) {
switch (dataType) {
case 'String':
return this.components.TextField;
case 'Hidden':
return this.components.HiddenField;
case 'Text':
// Textarea field (not implemented)
return null;
case 'HTML':
// HTML editor field (not implemented)
return null;
case 'Integer':
// Numeric field (not implemented)
return null;
case 'Decimal':
// Numeric field (not implemented)
return null;
case 'MultiSelect':
// Radio field (not implemented)
return null;
case 'SingleSelect':
// Dropdown field (not implemented)
return null;
case 'Date':
// DateTime field (not implemented)
return null;
case 'DateTime':
// DateTime field (not implemented)
return null;
case 'Time':
// DateTime field (not implemented)
return null;
case 'Boolean':
// Checkbox field (not implemented)
return null;
case 'Custom':
return this.components.GridField;
default:
return null;
}
/**
* Default data type to component mappings.
* Used as a fallback when no component type is provided in the form schema.
*
* @param string dataType - The data type provided by the form schema.
*
* @return object|null
*/
getComponentByDataType(dataType) {
switch (dataType) {
case 'String':
return this.components.TextField;
case 'Hidden':
return this.components.HiddenField;
case 'Text':
// Textarea field (not implemented)
return null;
case 'HTML':
// HTML editor field (not implemented)
return null;
case 'Integer':
// Numeric field (not implemented)
return null;
case 'Decimal':
// Numeric field (not implemented)
return null;
case 'MultiSelect':
// Radio field (not implemented)
return null;
case 'SingleSelect':
// Dropdown field (not implemented)
return null;
case 'Date':
// DateTime field (not implemented)
return null;
case 'DateTime':
// DateTime field (not implemented)
return null;
case 'Time':
// DateTime field (not implemented)
return null;
case 'Boolean':
// Checkbox field (not implemented)
return null;
case 'Custom':
return this.components.GridField;
default:
return null;
}
}
},
};
export class FormBuilderComponent extends SilverStripeComponent {
constructor(props) {
super(props);
constructor(props) {
super(props);
this.formSchemaPromise = null;
this.formSchemaPromise = null;
this.isFetching = false;
this.fetch();
}
/**
* Fetches data used to generate a form. This can be form schema and or form state data.
* When the response comes back the data is saved to state.
*
* @param boolean schema - If form schema data should be returned in the response.
* @param boolean state - If form state data should be returned in the response.
*
* @return object - Promise from the AJAX request.
*/
fetch(schema = true, state = false) {
const headerValues = [];
if (this.isFetching === true) {
return this.formSchemaPromise;
}
if (schema === true) {
headerValues.push('schema');
}
if (state === true) {
headerValues.push('state');
}
this.formSchemaPromise = fetch(this.props.schemaUrl, {
headers: { 'X-FormSchema-Request': headerValues.join() },
credentials: 'same-origin',
})
.then(response => response.json())
.then(json => {
this.isFetching = false;
this.props.actions.setSchema(json);
});
this.fetch();
this.isFetching = true;
return this.formSchemaPromise;
}
/**
* Maps a list of schema fields to their React Component.
* Only top level form fields are handled here, composite fields (TabSets etc),
* are responsible for mapping and rendering their children.
*
* @param array fields
*
* @return array
*/
mapFieldsToComponents(fields) {
return fields.map((field, i) => {
const Component = field.component !== null
? fakeInjector.getComponentByName(field.component)
: fakeInjector.getComponentByDataType(field.type);
if (Component === null) {
return null;
}
// Props which every form field receives.
const props = {
attributes: field.attributes,
data: field.data,
description: field.description,
extraClass: field.extraClass,
fields: field.children,
id: field.id,
name: field.name,
};
// Structural fields (like TabSets) are not posted back to
// the server and don't receive some props.
if (field.type !== 'Structural') {
props.rightTitle = field.rightTitle;
props.leftTitle = field.leftTitle;
props.readOnly = field.readOnly;
props.disabled = field.disabled;
props.customValidationMessage = field.customValidationMessage;
}
// Dropdown and Radio fields need some options...
if (field.type === 'MultiSelect' || field.type === 'SingleSelect') {
props.source = field.source;
}
return <Component key={i} {...props} />;
});
}
render() {
const schema = this.props.schemas[this.props.schemaUrl];
// If the response from fetching the initial data
// hasn't come back yet, don't render anything.
if (!schema) {
return null;
}
/**
* Fetches data used to generate a form. This can be form schema and or form state data.
* When the response comes back the data is saved to state.
*
* @param boolean schema - If form schema data should be returned in the response.
* @param boolean state - If form state data should be returned in the response.
*
* @return object - Promise from the AJAX request.
*/
fetch(schema = true, state = false) {
var headerValues = [];
const formProps = {
actions: schema.schema.actions,
attributes: schema.schema.attributes,
data: schema.schema.data,
fields: schema.schema.fields,
mapFieldsToComponents: this.mapFieldsToComponents,
};
if (this.isFetching === true) {
return this.formSchemaPromise;
}
if (schema === true) {
headerValues.push('schema');
}
if (state === true) {
headerValues.push('state');
}
this.formSchemaPromise = fetch(this.props.schemaUrl, {
headers: { 'X-FormSchema-Request': headerValues.join() },
credentials: 'same-origin'
})
.then(response => {
return response.json();
})
.then(json => {
this.isFetching = false;
this.props.actions.setSchema(json);
});
this.isFetching = true;
return this.formSchemaPromise;
}
/**
* Maps a list of schema fields to their React Component.
* Only top level form fields are handled here, composite fields (TabSets etc),
* are responsible for mapping and rendering their children.
*
* @param array fields
*
* @return array
*/
mapFieldsToComponents(fields) {
return fields.map((field, i) => {
const Component = field.component !== null
? fakeInjector.getComponentByName(field.component)
: fakeInjector.getComponentByDataType(field.type);
if (Component === null) {
return null;
}
// Props which every form field receives.
let props = {
attributes: field.attributes,
data: field.data,
description: field.description,
extraClass: field.extraClass,
fields: field.children,
id: field.id,
name: field.name
};
// Structural fields (like TabSets) are not posted back to the server and don't receive some props.
if (field.type !== 'Structural') {
props.rightTitle = field.rightTitle;
props.leftTitle = field.leftTitle;
props.readOnly = field.readOnly;
props.disabled = field.disabled;
props.customValidationMessage = field.customValidationMessage;
}
// Dropdown and Radio fields need some options...
if (field.type === 'MultiSelect' || field.type === 'SingleSelect') {
props.source = field.source;
}
return <Component key={i} {...props} />
});
}
render() {
const schema = this.props.schemas[this.props.schemaUrl];
// If the response from fetching the initial data
// hasn't come back yet, don't render anything.
if (!schema) {
return null;
}
const formProps = {
actions: schema.schema.actions,
attributes: schema.schema.attributes,
data: schema.schema.data,
fields: schema.schema.fields,
mapFieldsToComponents: this.mapFieldsToComponents
};
return <FormComponent {...formProps} />
}
return <FormComponent {...formProps} />;
}
}
FormBuilderComponent.propTypes = {
actions: React.PropTypes.object.isRequired,
schemaUrl: React.PropTypes.string.isRequired,
schemas: React.PropTypes.object.isRequired
actions: React.PropTypes.object.isRequired,
schemaUrl: React.PropTypes.string.isRequired,
schemas: React.PropTypes.object.isRequired,
};
function mapStateToProps(state) {
return {
schemas: state.schemas
}
return {
schemas: state.schemas,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(schemaActions, dispatch)
}
return {
actions: bindActionCreators(schemaActions, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(FormBuilderComponent);

View File

@ -5,22 +5,22 @@ import { FormBuilderComponent } from '../';
describe('FormBuilderComponent', () => {
describe('getFormSchema()', () => {
describe('getFormSchema()', () => {
var formBuilder;
var formBuilder;
beforeEach(() => {
const props = {
store: {
getState: () => {}
},
actions: {},
schemaUrl: 'admin/assets/schema/1',
schema: { forms: [{ schema: { id: '1', schema_url: 'admin/assets/schema/1' } }] }
};
beforeEach(() => {
const props = {
store: {
getState: () => {}
},
actions: {},
schemaUrl: 'admin/assets/schema/1',
schema: { forms: [{ schema: { id: '1', schema_url: 'admin/assets/schema/1' } }] }
};
formBuilder = new FormBuilderComponent(props);
});
formBuilder = new FormBuilderComponent(props);
});
});
});

View File

@ -4,56 +4,62 @@ import FormActionComponent from 'components/form-action/index';
class FormComponent extends SilverStripeComponent {
/**
* Gets the components responsible for perfoming actions on the form.
* For example form submission.
*
* @return array|null
*/
getFormActionComponents() {
return this.props.actions.map((action) => {
return <FormActionComponent {...action} />;
});
}
/**
* Gets the components responsible for perfoming actions on the form.
* For example form submission.
*
* @return array|null
*/
getFormActionComponents() {
return this.props.actions.map((action) =>
<FormActionComponent {...action} />
);
}
render() {
const attr = this.props.attributes;
const fields = this.props.mapFieldsToComponents(this.props.fields);
const actions = this.getFormActionComponents();
render() {
const attr = this.props.attributes;
const fields = this.props.mapFieldsToComponents(this.props.fields);
const actions = this.getFormActionComponents();
return (
<form id={attr.id} className={attr.className} encType={attr.enctype} method={attr.method} action={attr.action}>
{fields &&
<fieldset className='form-group'>
{fields}
</fieldset>
}
return (
<form
id={attr.id}
className={attr.className}
encType={attr.enctype}
method={attr.method}
action={attr.action}
>
{fields &&
<fieldset className="form-group">
{fields}
</fieldset>
}
{actions &&
<div className='actions-fix-btm'>
<div className='btn-group' role='group'>
{actions}
</div>
</div>
}
</form>
);
}
{actions &&
<div className="actions-fix-btm">
<div className="btn-group" role="group">
{actions}
</div>
</div>
}
</form>
);
}
}
FormComponent.propTypes = {
actions: React.PropTypes.array,
attributes: React.PropTypes.shape({
action: React.PropTypes.string.isRequired,
'class': React.PropTypes.string.isRequired,
enctype: React.PropTypes.string.isRequired,
id: React.PropTypes.string.isRequired,
method: React.PropTypes.string.isRequired
}),
data: React.PropTypes.array,
fields: React.PropTypes.array.isRequired,
mapFieldsToComponents: React.PropTypes.func.isRequired
actions: React.PropTypes.array,
attributes: React.PropTypes.shape({
action: React.PropTypes.string.isRequired,
class: React.PropTypes.string.isRequired,
enctype: React.PropTypes.string.isRequired,
id: React.PropTypes.string.isRequired,
method: React.PropTypes.string.isRequired,
}),
data: React.PropTypes.array,
fields: React.PropTypes.array.isRequired,
mapFieldsToComponents: React.PropTypes.func.isRequired,
};
export default FormComponent;

View File

@ -2,27 +2,27 @@ import React from 'react';
import SilverStripeComponent from 'silverstripe-component';
class GridFieldActionComponent extends SilverStripeComponent {
constructor(props) {
super(props);
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
this.handleClick = this.handleClick.bind(this);
}
render() {
return (
<button
className={`grid-field-action-component font-icon-${this.props.icon}`}
onClick={this.handleClick}
/>
);
}
render() {
return (
<button
className={`grid-field-action-component font-icon-${this.props.icon}`}
onClick={this.handleClick} />
);
}
handleClick(event) {
this.props.handleClick(event);
}
handleClick(event) {
this.props.handleClick(event, this.props.record.ID);
}
}
GridFieldActionComponent.PropTypes = {
handleClick: React.PropTypes.func.isRequired
}
handleClick: React.PropTypes.func.isRequired,
};
export default GridFieldActionComponent;

View File

@ -3,16 +3,16 @@ import SilverStripeComponent from 'silverstripe-component';
class GridFieldCellComponent extends SilverStripeComponent {
render() {
return (
<div className='grid-field-cell-component'>{this.props.children}</div>
);
}
render() {
return (
<div className="grid-field-cell-component">{this.props.children}</div>
);
}
}
GridFieldCellComponent.PropTypes = {
width: React.PropTypes.number
}
width: React.PropTypes.number,
};
export default GridFieldCellComponent;

View File

@ -3,16 +3,16 @@ import SilverStripeComponent from 'silverstripe-component';
class GridFieldHeaderCellComponent extends SilverStripeComponent {
render() {
return (
<div className='grid-field-header-cell-component'>{this.props.children}</div>
);
}
render() {
return (
<div className="grid-field-header-cell-component">{this.props.children}</div>
);
}
}
GridFieldHeaderCellComponent.PropTypes = {
width: React.PropTypes.number
}
width: React.PropTypes.number,
};
export default GridFieldHeaderCellComponent;

View File

@ -4,11 +4,11 @@ import GridFieldRowComponent from './row';
class GridFieldHeaderComponent extends SilverStripeComponent {
render() {
return (
<GridFieldRowComponent>{this.props.children}</GridFieldRowComponent>
);
}
render() {
return (
<GridFieldRowComponent>{this.props.children}</GridFieldRowComponent>
);
}
}

View File

@ -14,107 +14,118 @@ import * as actions from 'state/records/actions';
* The component acts as a container for a grid field,
* with smarts around data retrieval from external sources.
*
* @todo Convert to higher order component which hooks up form schema data to an API backend as a grid data source
* @todo Convert to higher order component which hooks up form
* schema data to an API backend as a grid data source
* @todo Replace "dumb" inner components with third party library (e.g. https://griddlegriddle.github.io)
*/
class GridField extends SilverStripeComponent {
constructor(props) {
super(props);
constructor(props) {
super(props);
this.deleteRecord = this.deleteRecord.bind(this);
this.editRecord = this.editRecord.bind(this);
this.deleteRecord = this.deleteRecord.bind(this);
this.editRecord = this.editRecord.bind(this);
}
componentDidMount() {
super.componentDidMount();
const data = this.props.data;
this.props.actions.fetchRecords(
data.recordType,
data.collectionReadEndpoint.method,
data.collectionReadEndpoint.url
);
}
render() {
const records = this.props.records;
if (!records) {
return <div></div>;
}
componentDidMount() {
super.componentDidMount();
const columns = this.props.data.columns;
let data = this.props.data;
// Placeholder to align the headers correctly with the content
const actionPlaceholder = <span key={'actionPlaceholder'} />;
const headerCells = columns.map((column, i) =>
<GridFieldHeaderCell key={i}>{column.name}</GridFieldHeaderCell>
);
const header = <GridFieldHeader>{headerCells.concat(actionPlaceholder)}</GridFieldHeader>;
this.props.actions.fetchRecords(data.recordType, data.collectionReadEndpoint.method, data.collectionReadEndpoint.url);
}
const rows = records.map((record, i) => {
const cells = columns.map((column, j) => {
// Get value by dot notation
const val = column.field.split('.').reduce((a, b) => a[b], record);
return <GridFieldCell key={j} width={column.width}>{val}</GridFieldCell>;
});
render() {
const records = this.props.records;
if(!records) {
return <div></div>;
}
const rowActions = (
<GridFieldCell key={`${i}-actions`}>
<GridFieldAction
icon={'cog'}
handleClick={this.editRecord}
key={`action-${i}-edit`}
record={record}
/>,
<GridFieldAction
icon={'cancel'}
handleClick={this.deleteRecord}
key={`action-${i}-delete`}
record={record}
/>,
</GridFieldCell>
);
const columns = this.props.data.columns;
return <GridFieldRow key={i}>{cells.concat(rowActions)}</GridFieldRow>;
});
// Placeholder to align the headers correctly with the content
const actionPlaceholder = <GridFieldCell key={'actionPlaceholder'} />;
const headerCells = columns.map((column, i) => <GridFieldHeaderCell key={i} >{column.name}</GridFieldHeaderCell>);
const header = <GridFieldHeader>{headerCells.concat(actionPlaceholder)}</GridFieldHeader>;
return (
<GridFieldTable header={header} rows={rows} />
);
}
const rows = records.map((record, i) => {
var cells = columns.map((column, i) => {
// Get value by dot notation
var val = column.field.split('.').reduce((a, b) => a[b], record)
return <GridFieldCell key={i}>{val}</GridFieldCell>
});
/**
* @param number int
* @param event
*/
deleteRecord(event, id) {
event.preventDefault();
this.props.actions.deleteRecord(
this.props.data.recordType,
id,
this.props.data.itemDeleteEndpoint.method,
this.props.data.itemDeleteEndpoint.url
);
}
var rowActions = <GridFieldCell key={i + '-actions'}>
<GridFieldAction
icon={'cog'}
handleClick={this.editRecord.bind(this, record.ID)}
key={"action-" + i + "-edit"}
/>
<GridFieldAction
icon={'cancel'}
handleClick={this.deleteRecord.bind(this, record.ID)}
key={"action-" + i + "-delete"}
/>
</GridFieldCell>;
return <GridFieldRow key={i}>{cells.concat(rowActions)}</GridFieldRow>;
});
return (
<GridFieldTable header={header} rows={rows}></GridFieldTable>
);
}
/**
* @param number int
* @param event
*/
deleteRecord(id, event) {
event.preventDefault();
this.props.actions.deleteRecord(
this.props.data.recordType,
id,
this.props.data.itemDeleteEndpoint.method,
this.props.data.itemDeleteEndpoint.url
);
}
editRecord(id, event) {
event.preventDefault();
// TODO
}
editRecord(event) {
event.preventDefault();
// TODO
}
}
GridField.propTypes = {
data: React.PropTypes.shape({
recordType: React.PropTypes.string.isRequired,
headerColumns: React.PropTypes.array,
collectionReadEndpoint: React.PropTypes.object
})
data: React.PropTypes.shape({
recordType: React.PropTypes.string.isRequired,
headerColumns: React.PropTypes.array,
collectionReadEndpoint: React.PropTypes.object,
}),
};
function mapStateToProps(state, ownProps) {
let recordType = ownProps.data ? ownProps.data.recordType : null;
return {
records: (state.records && recordType) ? state.records[recordType] : []
}
const recordType = ownProps.data ? ownProps.data.recordType : null;
return {
records: (state.records && recordType) ? state.records[recordType] : [],
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(actions, dispatch)
}
return {
actions: bindActionCreators(actions, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(GridField);

View File

@ -3,11 +3,11 @@ import SilverStripeComponent from 'silverstripe-component';
class GridFieldRowComponent extends SilverStripeComponent {
render() {
return (
<li className='grid-field-row-component [ list-group-item ]'>{this.props.children}</li>
);
}
render() {
return (
<li className="grid-field-row-component [ list-group-item ]">{this.props.children}</li>
);
}
}

View File

@ -3,61 +3,61 @@ import SilverStripeComponent from 'silverstripe-component';
class GridFieldTableComponent extends SilverStripeComponent {
render() {
return (
<ul className='grid-field-table-component [ list-group ]'>
{this.generateHeader()}
{this.generateRows()}
</ul>
);
render() {
return (
<ul className="grid-field-table-component [ list-group ]">
{this.generateHeader()}
{this.generateRows()}
</ul>
);
}
/**
* Generates the header component.
*
* Uses the header component passed via the `header` prop if it exists.
* Otherwise generates a header from the `data` prop.
*
* @return object|null
*/
generateHeader() {
if (typeof this.props.header !== 'undefined') {
return this.props.header;
}
/**
* Generates the header component.
*
* Uses the header component passed via the `header` prop if it exists.
* Otherwise generates a header from the `data` prop.
*
* @return object|null
*/
generateHeader() {
if (typeof this.props.header !== 'undefined') {
return this.props.header;
}
if (typeof this.props.data !== 'undefined') {
// TODO: Generate the header.
}
return null;
if (typeof this.props.data !== 'undefined') {
// TODO: Generate the header.
}
/**
* Generates the table rows.
*
* Uses the components passed via the `rows` props if it exists.
* Otherwise generates rows from the `data` prop.
*
* @return object|null
*/
generateRows() {
if (typeof this.props.rows !== 'undefined') {
return this.props.rows;
}
return null;
}
if (typeof this.props.data !== 'undefined') {
// TODO: Generate the rows.
}
return null;
/**
* Generates the table rows.
*
* Uses the components passed via the `rows` props if it exists.
* Otherwise generates rows from the `data` prop.
*
* @return object|null
*/
generateRows() {
if (typeof this.props.rows !== 'undefined') {
return this.props.rows;
}
if (typeof this.props.data !== 'undefined') {
// TODO: Generate the rows.
}
return null;
}
}
GridFieldTableComponent.propTypes = {
data: React.PropTypes.object,
header: React.PropTypes.object,
rows: React.PropTypes.array
data: React.PropTypes.object,
header: React.PropTypes.object,
rows: React.PropTypes.array,
};
export default GridFieldTableComponent;

View File

@ -2,66 +2,66 @@ jest.dontMock('../index');
jest.dontMock('../table');
const React = require('react'),
ReactTestUtils = require('react-addons-test-utils'),
GridFieldTableComponent = require('../table.js').default;
ReactTestUtils = require('react-addons-test-utils'),
GridFieldTableComponent = require('../table.js').default;
describe('GridFieldTableComponent', () => {
var props;
var props;
beforeEach(function () {
props = {
}
beforeEach(function () {
props = {
}
});
describe('generateHeader()', function () {
var gridfield;
it('should return props.header if it is set', function () {
props.header = <div className='header'></div>;
gridfield = ReactTestUtils.renderIntoDocument(
<GridFieldTableComponent {...props} />
);
expect(gridfield.generateHeader().props.className).toBe('header');
});
describe('generateHeader()', function () {
var gridfield;
it('should generate and return a header from props.data if it is set', function () {
it('should return props.header if it is set', function () {
props.header = <div className='header'></div>;
gridfield = ReactTestUtils.renderIntoDocument(
<GridFieldTableComponent {...props} />
);
expect(gridfield.generateHeader().props.className).toBe('header');
});
it('should generate and return a header from props.data if it is set', function () {
});
it('should return null if props.header and props.data are both not set', function () {
gridfield = ReactTestUtils.renderIntoDocument(
<GridFieldTableComponent {...props} />
);
expect(gridfield.generateHeader()).toBe(null);
});
});
describe('generateRows()', function () {
var gridfield;
it('should return null if props.header and props.data are both not set', function () {
gridfield = ReactTestUtils.renderIntoDocument(
<GridFieldTableComponent {...props} />
);
it('should return props.rows if it is set', function () {
props.rows = ['row1'];
gridfield = ReactTestUtils.renderIntoDocument(
<GridFieldTableComponent {...props} />
);
expect(gridfield.generateRows()[0]).toBe('row1');
});
it('should generate and return rows from props.data if it is set', function () {
});
it('should return null if props.rows and props.data are both not set', function () {
gridfield = ReactTestUtils.renderIntoDocument(
<GridFieldTableComponent {...props} />
);
expect(gridfield.generateRows()).toBe(null);
});
expect(gridfield.generateHeader()).toBe(null);
});
});
describe('generateRows()', function () {
var gridfield;
it('should return props.rows if it is set', function () {
props.rows = ['row1'];
gridfield = ReactTestUtils.renderIntoDocument(
<GridFieldTableComponent {...props} />
);
expect(gridfield.generateRows()[0]).toBe('row1');
});
it('should generate and return rows from props.data if it is set', function () {
});
it('should return null if props.rows and props.data are both not set', function () {
gridfield = ReactTestUtils.renderIntoDocument(
<GridFieldTableComponent {...props} />
);
expect(gridfield.generateRows()).toBe(null);
});
});
});

View File

@ -3,46 +3,46 @@ import SilverStripeComponent from 'silverstripe-component';
class HiddenFieldComponent extends SilverStripeComponent {
constructor(props) {
super(props);
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleChange = this.handleChange.bind(this);
}
render() {
return (
<div className="field hidden">
<input {...this.getInputProps()} />
</div>
);
}
getInputProps() {
return {
className: ['hidden', this.props.extraClass].join(' '),
id: this.props.name,
name: this.props.name,
onChange: this.props.onChange,
type: 'hidden',
value: this.props.value,
};
}
handleChange() {
if (typeof this.props.onChange === 'undefined') {
return;
}
render() {
return (
<div className='field hidden'>
<input {...this.getInputProps()} />
</div>
);
}
getInputProps() {
return {
className: ['hidden', this.props.extraClass].join(' '),
id: this.props.name,
name: this.props.name,
onChange: this.props.onChange,
type: 'hidden',
value: this.props.value
};
}
handleChange(event) {
if (typeof this.props.onChange === 'undefined') {
return;
}
this.props.onChange();
}
this.props.onChange();
}
}
HiddenFieldComponent.propTypes = {
label: React.PropTypes.string,
extraClass: React.PropTypes.string,
name: React.PropTypes.string.isRequired,
onChange: React.PropTypes.func,
value: React.PropTypes.string
label: React.PropTypes.string,
extraClass: React.PropTypes.string,
name: React.PropTypes.string.isRequired,
onChange: React.PropTypes.func,
value: React.PropTypes.string,
};
export default HiddenFieldComponent;

View File

@ -3,37 +3,39 @@ import SilverStripeComponent from 'silverstripe-component';
class NorthHeaderBreadcrumbsComponent extends SilverStripeComponent {
render() {
return (
<div className="cms-content-header-info">
<div className="breadcrumbs-wrapper">
<h2 id="page-title-heading">
{this.getBreadcrumbs()}
</h2>
</div>
</div>
);
render() {
return (
<div className="cms-content-header-info">
<div className="breadcrumbs-wrapper">
<h2 id="page-title-heading">
{this.getBreadcrumbs()}
</h2>
</div>
</div>
);
}
getBreadcrumbs() {
if (typeof this.props.crumbs === 'undefined') {
return null;
}
getBreadcrumbs() {
if (typeof this.props.crumbs === 'undefined') {
return null;
}
const breadcrumbs = this.props.crumbs.map((crumb, index, crumbs) => {
let component;
// If its the last item in the array
if (index === crumbs.length - 1) {
component = <span key={index} className="crumb last">{crumb.text}</span>;
} else {
component = [
<a key={index} className="cms-panel-link crumb" href={crumb.href}>{crumb.text}</a>,
<span className="sep">/</span>,
];
}
return component;
});
var breadcrumbs = this.props.crumbs.map((crumb, index, crumbs) => {
// If its the last item in the array
if (index === crumbs.length - 1) {
return <span key={index} className="crumb last">{crumb.text}</span>;
} else {
return [
<a key={index} className="cms-panel-link crumb" href={crumb.href}>{crumb.text}</a>,
<span className="sep">/</span>
];
}
});
return breadcrumbs;
}
return breadcrumbs;
}
}

View File

@ -1,40 +1,40 @@
jest.dontMock('../index');
const React = require('react'),
ReactTestUtils = require('react-addons-test-utils'),
NorthHeaderBreadcrumbsComponent = require('../index').default;
ReactTestUtils = require('react-addons-test-utils'),
NorthHeaderBreadcrumbsComponent = require('../index').default;
describe('NorthHeaderBreadcrumbsComponent', () => {
var props;
var props;
beforeEach(() => {
props = {};
beforeEach(() => {
props = {};
});
describe('getBreadcrumbs()', () => {
var northHeaderBreadcrumbs;
it('should convert the props.crumbs array into jsx to be rendered', () => {
props.crumbs = [
{ text: 'breadcrumb1', href: 'href1'},
{ text: 'breadcrumb2', href: 'href2'}
];
northHeaderBreadcrumbs = ReactTestUtils.renderIntoDocument(
<NorthHeaderBreadcrumbsComponent {...props} />
);
expect(northHeaderBreadcrumbs.getBreadcrumbs()[0][0].props.children).toBe('breadcrumb1');
expect(northHeaderBreadcrumbs.getBreadcrumbs()[0][1].props.children).toBe('/');
expect(northHeaderBreadcrumbs.getBreadcrumbs()[1].props.children).toBe('breadcrumb2');
});
describe('getBreadcrumbs()', () => {
var northHeaderBreadcrumbs;
it('should return null if props.crumbs is not set', () => {
northHeaderBreadcrumbs = ReactTestUtils.renderIntoDocument(
<NorthHeaderBreadcrumbsComponent {...props} />
);
it('should convert the props.crumbs array into jsx to be rendered', () => {
props.crumbs = [
{ text: 'breadcrumb1', href: 'href1'},
{ text: 'breadcrumb2', href: 'href2'}
];
northHeaderBreadcrumbs = ReactTestUtils.renderIntoDocument(
<NorthHeaderBreadcrumbsComponent {...props} />
);
expect(northHeaderBreadcrumbs.getBreadcrumbs()[0][0].props.children).toBe('breadcrumb1');
expect(northHeaderBreadcrumbs.getBreadcrumbs()[0][1].props.children).toBe('/');
expect(northHeaderBreadcrumbs.getBreadcrumbs()[1].props.children).toBe('breadcrumb2');
});
it('should return null if props.crumbs is not set', () => {
northHeaderBreadcrumbs = ReactTestUtils.renderIntoDocument(
<NorthHeaderBreadcrumbsComponent {...props} />
);
expect(northHeaderBreadcrumbs.getBreadcrumbs()).toBe(null);
});
expect(northHeaderBreadcrumbs.getBreadcrumbs()).toBe(null);
});
});
});

View File

@ -4,26 +4,26 @@ import SilverStripeComponent from 'silverstripe-component';
class NorthHeaderComponent extends SilverStripeComponent {
render() {
return (
<div className="north-header-component">
<NorthHeaderBreadcrumbsComponent crumbs={this.getBreadcrumbs()}/>
</div>
);
}
render() {
return (
<div className="north-header-component">
<NorthHeaderBreadcrumbsComponent crumbs={this.getBreadcrumbs()} />
</div>
);
}
getBreadcrumbs() {
return [
{
text: 'Campaigns',
href: 'admin/campaigns'
},
{
text: 'March release',
href: 'admin/campaigns/show/1'
}
]
}
getBreadcrumbs() {
return [
{
text: 'Campaigns',
href: 'admin/campaigns',
},
{
text: 'March release',
href: 'admin/campaigns/show/1',
},
];
}
}

View File

@ -3,53 +3,53 @@ import SilverStripeComponent from 'silverstripe-component';
class TextFieldComponent extends SilverStripeComponent {
constructor(props) {
super(props);
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
this.handleChange = this.handleChange.bind(this);
}
render() {
return (
<div className='field text'>
{this.props.label &&
<label className='left' htmlFor={'gallery_' + this.props.name}>
{this.props.label}
</label>
}
<div className='middleColumn'>
<input {...this.getInputProps()} />
</div>
</div>
);
}
getInputProps() {
return {
className: ['text', this.props.extraClass].join(' '),
id: `gallery_${this.props.name}`,
name: this.props.name,
onChange: this.props.onChange,
type: 'text',
value: this.props.value
};
}
handleChange(event) {
if (typeof this.props.onChange === 'undefined') {
return;
render() {
return (
<div className="field text">
{this.props.label &&
<label className="left" htmlFor={`gallery_${this.props.name}`}>
{this.props.label}
</label>
}
<div className="middleColumn">
<input {...this.getInputProps()} />
</div>
</div>
);
}
this.props.onChange();
getInputProps() {
return {
className: ['text', this.props.extraClass].join(' '),
id: `gallery_${this.props.name}`,
name: this.props.name,
onChange: this.props.onChange,
type: 'text',
value: this.props.value,
};
}
handleChange() {
if (typeof this.props.onChange === 'undefined') {
return;
}
this.props.onChange();
}
}
TextFieldComponent.propTypes = {
label: React.PropTypes.string,
extraClass: React.PropTypes.string,
name: React.PropTypes.string.isRequired,
onChange: React.PropTypes.func,
value: React.PropTypes.string
label: React.PropTypes.string,
extraClass: React.PropTypes.string,
name: React.PropTypes.string.isRequired,
onChange: React.PropTypes.func,
value: React.PropTypes.string,
};
export default TextFieldComponent;

View File

@ -8,30 +8,30 @@ import TextFieldComponent from '../';
describe('TextFieldComponent', function() {
var props;
var props;
beforeEach(function () {
props = {
label: '',
name: '',
value: '',
onChange: jest.genMockFunction()
};
});
describe('handleChange()', function () {
var textField;
beforeEach(function () {
props = {
label: '',
name: '',
value: '',
onChange: jest.genMockFunction()
};
textField = ReactTestUtils.renderIntoDocument(
<TextFieldComponent {...props} />
);
});
describe('handleChange()', function () {
var textField;
it('should call the onChange function on props', function () {
textField.handleChange();
beforeEach(function () {
textField = ReactTestUtils.renderIntoDocument(
<TextFieldComponent {...props} />
);
});
it('should call the onChange function on props', function () {
textField.handleChange();
expect(textField.props.onChange.mock.calls.length).toBe(1);
});
expect(textField.props.onChange.mock.calls.length).toBe(1);
});
});
});

View File

@ -6,39 +6,37 @@
*/
class Config {
/**
* Gets the the config for a specific section.
*
* @param string key - The section config key.
*
* @return object|undefined
*/
static getSection(key) {
return window.ss.config.sections[key];
}
/**
* Gets the the config for a specific section.
*
* @param string key - The section config key.
*
* @return object|undefined
*/
static getSection(key) {
return window.ss.config.sections[key];
}
/**
* Gets a de-duped list of routes for top level controllers. E.g. 'assets', 'pages', etc.
*
* @return array
*/
static getTopLevelRoutes() {
var topLevelRoutes = [];
/**
* Gets a de-duped list of routes for top level controllers. E.g. 'assets', 'pages', etc.
*
* @return array
*/
static getTopLevelRoutes() {
const topLevelRoutes = [];
Object.keys(window.ss.config.sections).forEach((key) => {
const route = window.ss.config.sections[key].route;
const isTopLevelRoute = route.indexOf('/') === -1;
const isUnique = topLevelRoutes.indexOf(route) === -1;
Object.keys(window.ss.config.sections).forEach((key) => {
const route = window.ss.config.sections[key].route;
const isTopLevelRoute = route.indexOf('/') === -1;
const isUnique = topLevelRoutes.indexOf(route) === -1;
//console.log(this.getSection(key).route);
if (isTopLevelRoute && isUnique) {
topLevelRoutes.push(route);
}
});
if (isTopLevelRoute && isUnique) {
topLevelRoutes.push(route);
}
});
return topLevelRoutes;
}
return topLevelRoutes;
}
}

View File

@ -1,15 +1,17 @@
/* globals jest */
function jQuery() {
return {
// Add jQuery methods such as 'find', 'change', 'trigger' as needed.
};
return {
// Add jQuery methods such as 'find', 'change', 'trigger' as needed.
};
}
var mockAjaxFn = jest.genMockFunction();
const mockAjaxFn = jest.genMockFunction();
mockAjaxFn.mockReturnValue({
done: jest.genMockFunction(),
fail: jest.genMockFunction(),
always: jest.genMockFunction()
done: jest.genMockFunction(),
fail: jest.genMockFunction(),
always: jest.genMockFunction(),
});
jQuery.ajax = mockAjaxFn;

View File

@ -1,15 +1,5 @@
import { Component } from 'react';
// eslint-disable-next-line react/prefer-stateless-function
export default class SilverStripeComponent extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
}
componentWillUnmount() {
}
};
}

View File

@ -2,63 +2,62 @@
* The register of Redux reducers.
* @private
*/
var register = {};
const register = {};
/**
* The central register of Redux reducers for the CMS. All registered reducers are combined when the application boots.
* The central register of Redux reducers for the CMS.
* All registered reducers are combined when the application boots.
*/
class ReducerRegister {
/**
* Adds a reducer to the register.
*
* @param string key - The key to register the reducer against.
* @param object reducer - Redux reducer.
*/
add(key, reducer) {
if (typeof register[key] !== 'undefined') {
throw new Error(`Reducer already exists at '${key}'`);
}
register[key] = reducer;
/**
* Adds a reducer to the register.
*
* @param string key - The key to register the reducer against.
* @param object reducer - Redux reducer.
*/
add(key, reducer) {
if (typeof register[key] !== 'undefined') {
throw new Error(`Reducer already exists at '${key}'`);
}
/**
* Gets all reducers from the register.
*
* @return object
*/
getAll() {
return register;
}
register[key] = reducer;
}
/**
* Gets a reducer from the register.
*
* @param string [key] - The key the reducer is registered against.
*
* @return object|undefined
*/
getByKey(key) {
return register[key];
}
/**
* Gets all reducers from the register.
*
* @return object
*/
getAll() {
return register;
}
/**
* Gets a reducer from the register.
*
* @param string [key] - The key the reducer is registered against.
*
* @return object|undefined
*/
getByKey(key) {
return register[key];
}
/**
* Removes a reducer from the register.
*
* @param string key - The key the reducer is registered against.
*/
remove(key) {
delete register[key];
}
/**
* Removes a reducer from the register.
*
* @param string key - The key the reducer is registered against.
*/
remove(key) {
delete register[key];
}
}
// Create an instance to export. The same instance is exported to
// each script which imports the reducerRegister. This means the
// same register is available throughout the application.
let reducerRegister = new ReducerRegister();
const reducerRegister = new ReducerRegister();
export default reducerRegister;

View File

@ -8,48 +8,49 @@ import FormBuilder from 'components/form-builder/index';
class CampaignAdminContainer extends SilverStripeComponent {
constructor(props) {
super(props);
constructor(props) {
super(props);
this.addCampaign = this.addCampaign.bind(this);
}
this.addCampaign = this.addCampaign.bind(this);
}
render() {
const schemaUrl = this.props.config.forms.editForm.schemaUrl;
render() {
const schemaUrl = this.props.config.forms.editForm.schemaUrl;
return (
<div>
<NorthHeader />
<FormAction
label={i18n._t('Campaigns.ADDCAMPAIGN')}
icon={'plus-circled'}
handleClick={this.addCampaign} />
<FormBuilder schemaUrl={schemaUrl} />
</div>
);
}
return (
<div>
<NorthHeader />
<FormAction
label={i18n._t('Campaigns.ADDCAMPAIGN')}
icon={'plus-circled'}
handleClick={this.addCampaign}
/>
<FormBuilder schemaUrl={schemaUrl} />
</div>
);
}
addCampaign() {
//Add campaign
}
addCampaign() {
// Add campaign
}
}
CampaignAdminContainer.propTypes = {
config: React.PropTypes.shape({
forms: React.PropTypes.shape({
editForm: React.PropTypes.shape({
schemaUrl: React.PropTypes.string
})
})
config: React.PropTypes.shape({
forms: React.PropTypes.shape({
editForm: React.PropTypes.shape({
schemaUrl: React.PropTypes.string,
}),
}),
sectionConfigKey: React.PropTypes.string.isRequired
}),
sectionConfigKey: React.PropTypes.string.isRequired,
};
function mapStateToProps(state, ownProps) {
return {
config: state.config.sections[ownProps.sectionConfigKey]
}
return {
config: state.config.sections[ownProps.sectionConfigKey],
};
}
export default connect(mapStateToProps)(CampaignAdminContainer);

View File

@ -1,24 +1,22 @@
import reducerRegister from 'reducer-register';
import $ from 'jQuery';
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import CampaignAdmin from './controller';
$.entwine('ss', function ($) {
$('.cms-content.CampaignAdmin').entwine({
onadd: function () {
ReactDOM.render(
<Provider store={window.store}>
<CampaignAdmin sectionConfigKey='CampaignAdmin' />
</Provider>
, this[0]);
},
onremove: function () {
ReactDOM.unmountComponentAtNode(this[0]);
}
});
// eslint-disable-next-line no-shadow
$.entwine('ss', ($) => {
$('.cms-content.CampaignAdmin').entwine({
onadd() {
ReactDOM.render(
<Provider store={window.store}>
<CampaignAdmin sectionConfigKey="CampaignAdmin" />
</Provider>
, this[0]);
},
onremove() {
ReactDOM.unmountComponentAtNode(this[0]);
},
});
});

View File

@ -6,73 +6,77 @@ es6promise.polyfill();
* @see https://github.com/github/fetch#handling-http-error-statuses
*/
function checkStatus(response) {
let ret;
let error;
if (response.status >= 200 && response.status < 300) {
return response
ret = response;
} else {
var error = new Error(response.statusText)
error.response = response
throw error
error = new Error(response.statusText);
error.response = response;
throw error;
}
return ret;
}
class SilverStripeBackend {
constructor() {
// Allow mocking
this.fetch = fetch;
}
constructor() {
// Allow mocking
this.fetch = fetch;
}
/**
* Makes a network request using the GET HTTP verb.
*
* @param string url - Endpoint URL.
* @return object - Promise
*/
get(url) {
return this.fetch(url, { method: 'get', credentials: 'same-origin' })
.then(checkStatus);
}
/**
* Makes a network request using the GET HTTP verb.
*
* @param string url - Endpoint URL.
* @return object - Promise
*/
get(url) {
return this.fetch(url, { method: 'get', credentials: 'same-origin' })
.then(checkStatus);
}
/**
* Makes a network request using the POST HTTP verb.
*
* @param string url - Endpoint URL.
* @param object data - Data to send with the request.
* @return object - Promise
*/
post(url, data) {
return this.fetch(url, { method: 'post', credentials: 'same-origin', body: data })
.then(checkStatus);
}
/**
* Makes a network request using the POST HTTP verb.
*
* @param string url - Endpoint URL.
* @param object data - Data to send with the request.
* @return object - Promise
*/
post(url, data) {
return this.fetch(url, { method: 'post', credentials: 'same-origin', body: data })
.then(checkStatus);
}
/**
* Makes a newtwork request using the PUT HTTP verb.
*
* @param string url - Endpoint URL.
* @param object data - Data to send with the request.
* @return object - Promise
*/
put(url, data) {
return this.fetch(url, { method: 'put', credentials: 'same-origin', body: data })
.then(checkStatus);
}
/**
* Makes a newtwork request using the PUT HTTP verb.
*
* @param string url - Endpoint URL.
* @param object data - Data to send with the request.
* @return object - Promise
*/
put(url, data) {
return this.fetch(url, { method: 'put', credentials: 'same-origin', body: data })
.then(checkStatus);
}
/**
* Makes a newtwork request using the DELETE HTTP verb.
*
* @param string url - Endpoint URL.
* @param object data - Data to send with the request.
* @return object - Promise
*/
delete(url, data) {
return this.fetch(url, { method: 'delete', credentials: 'same-origin', body: data })
.then(checkStatus);
}
/**
* Makes a newtwork request using the DELETE HTTP verb.
*
* @param string url - Endpoint URL.
* @param object data - Data to send with the request.
* @return object - Promise
*/
delete(url, data) {
return this.fetch(url, { method: 'delete', credentials: 'same-origin', body: data })
.then(checkStatus);
}
}
// Exported as a singleton so we can implement things like
// global caching and request batching at some stage.
let backend = new SilverStripeBackend();
const backend = new SilverStripeBackend();
export default backend;

View File

@ -2,54 +2,107 @@
* @file Base component which all SilverStripe ReactJS components should extend from.
*/
import React, { PropTypes, Component } from 'react';
import React, { Component } from 'react';
import $ from '../../../javascript/src/jQuery';
class SilverStripeComponent extends Component {
/**
* @func componentDidMount
* @desc Bind event listeners which are triggered by legacy-land JavaScript.
* This lets us update the component when something happens in the outside world.
*/
componentDidMount() {
if (typeof this.props.cmsEvents === 'undefined') {
return;
}
constructor(props) {
super(props);
// Save some props for later. When we come to unbind these listeners
// there's no guarantee these props will be the same or even present.
this.cmsEvents = this.props.cmsEvents;
// Setup component routing.
if (typeof this.props.route !== 'undefined') {
// The component's render method gets switched based on the current path.
// If the current path matches the component's route, the component is displayed.
// Otherwise the component's render method returns null, resulting in
// the component not rendering.
this._render = this.render;
for (let cmsEvent in this.cmsEvents) {
$(document).on(cmsEvent, this.cmsEvents[cmsEvent].bind(this));
}
}
this.render = () => {
let component = null;
/**
* @func componentWillUnmount
* @desc Unbind the event listeners we added in componentDidMount.
*/
componentWillUnmount() {
for (let cmsEvent in this.cmsEvents) {
$(document).off(cmsEvent);
}
}
if (this.isComponentRoute()) {
component = this._render();
}
/**
* @func emitCmsEvent
* @param string componentEvent - Namespace component event e.g. 'my-component.title-changed'.
* @param object|string|array|number [data] - Some data to pass with the event.
* @desc Notifies legacy-land something has changed within our component.
*/
emitCmsEvent(componentEvent, data) {
$(document).trigger(componentEvent, data);
}
return component;
};
window.ss.router(this.props.route, (ctx, next) => {
this.handleEnterRoute(ctx, next);
});
window.ss.router.exit(this.props.route, (ctx, next) => {
this.handleExitRoute(ctx, next);
});
}
}
componentDidMount() {
if (typeof this.props.cmsEvents === 'undefined') {
return;
}
// Save some props for later. When we come to unbind these listeners
// there's no guarantee these props will be the same or even present.
this.cmsEvents = this.props.cmsEvents;
// Bind event listeners which are triggered by legacy-land JavaScript.
// This lets us update the component when something happens in the outside world.
for (const cmsEvent in this.cmsEvents) {
if ({}.hasOwnProperty.call(this.cmsEvents, cmsEvent)) {
$(document).on(cmsEvent, this.cmsEvents[cmsEvent].bind(this));
}
}
}
componentWillUnmount() {
// Unbind the event listeners we added in componentDidMount.
for (const cmsEvent in this.cmsEvents) {
if ({}.hasOwnProperty.call(this.cmsEvents, cmsEvent)) {
$(document).off(cmsEvent);
}
}
}
handleEnterRoute(ctx, next) {
next();
}
handleExitRoute(ctx, next) {
next();
}
/**
* Checks if the component should be rended on the current path.
*
* @param object [params] - If a params object is passed in it's
* mutated by page.js to contains route parans like ':id'.
*/
isComponentRoute(params = {}) {
if (typeof this.props.route === 'undefined') {
return true;
}
const route = new window.ss.router.Route(this.props.route);
return route.match(window.ss.router.current, params);
}
/**
* Notifies legacy-land something has changed within our component.
*
* @param string componentEvent - Namespace component event e.g. 'my-component.title-changed'.
* @param object|string|array|number [data] - Some data to pass with the event.
*/
emitCmsEvent(componentEvent, data) {
$(document).trigger(componentEvent, data);
}
}
SilverStripeComponent.propTypes = {
'cmsEvents': React.PropTypes.object
cmsEvents: React.PropTypes.object,
route: React.PropTypes.string,
};
export default SilverStripeComponent;

View File

@ -35,24 +35,24 @@ var $window = $( window ),
// URL as well as some other commonly used sub-parts. When used with RegExp.exec()
// or String.match, it parses the URL into a results array that looks like this:
//
// [0]: http://jblas:password@mycompany.com:8080/mail/inbox?msg=1234&type=unread#msg-content
// [1]: http://jblas:password@mycompany.com:8080/mail/inbox?msg=1234&type=unread
// [2]: http://jblas:password@mycompany.com:8080/mail/inbox
// [3]: http://jblas:password@mycompany.com:8080
// [4]: http:
// [5]: //
// [6]: jblas:password@mycompany.com:8080
// [7]: jblas:password
// [8]: jblas
// [9]: password
// [10]: mycompany.com:8080
// [11]: mycompany.com
// [12]: 8080
// [13]: /mail/inbox
// [14]: /mail/
// [15]: inbox
// [16]: ?msg=1234&type=unread
// [17]: #msg-content
// [0]: http://jblas:password@mycompany.com:8080/mail/inbox?msg=1234&type=unread#msg-content
// [1]: http://jblas:password@mycompany.com:8080/mail/inbox?msg=1234&type=unread
// [2]: http://jblas:password@mycompany.com:8080/mail/inbox
// [3]: http://jblas:password@mycompany.com:8080
// [4]: http:
// [5]: //
// [6]: jblas:password@mycompany.com:8080
// [7]: jblas:password
// [8]: jblas
// [9]: password
// [10]: mycompany.com:8080
// [11]: mycompany.com
// [12]: 8080
// [13]: /mail/inbox
// [14]: /mail/
// [15]: inbox
// [16]: ?msg=1234&type=unread
// [17]: #msg-content
//
urlParseRE: /^(((([^:\/#\?]+:)?(?:(\/\/)((?:(([^:@\/#\?]+)(?:\:([^:@\/#\?]+))?)@)?(([^:\/#\?\]\[]+|\[[^\/\]@#?]+\])(?:\:([0-9]+))?))?)?)?((\/?(?:[^\/\?#]+\/+)*)([^\?#]*)))?(\?[^#]+)?)(#.*)?/,
@ -72,23 +72,23 @@ var $window = $( window ),
// like all other browsers do, so we normalize everything so its consistent
// no matter what browser we're running on.
return {
href: matches[ 0 ] || "",
href: matches[ 0 ] || "",
hrefNoHash: matches[ 1 ] || "",
hrefNoSearch: matches[ 2 ] || "",
domain: matches[ 3 ] || "",
protocol: matches[ 4 ] || "",
domain: matches[ 3 ] || "",
protocol: matches[ 4 ] || "",
doubleSlash: matches[ 5 ] || "",
authority: matches[ 6 ] || "",
username: matches[ 8 ] || "",
password: matches[ 9 ] || "",
host: matches[ 10 ] || "",
hostname: matches[ 11 ] || "",
port: matches[ 12 ] || "",
pathname: matches[ 13 ] || "",
directory: matches[ 14 ] || "",
filename: matches[ 15 ] || "",
search: matches[ 16 ] || "",
hash: matches[ 17 ] || ""
authority: matches[ 6 ] || "",
username: matches[ 8 ] || "",
password: matches[ 9 ] || "",
host: matches[ 10 ] || "",
hostname: matches[ 11 ] || "",
port: matches[ 12 ] || "",
pathname: matches[ 13 ] || "",
directory: matches[ 14 ] || "",
filename: matches[ 15 ] || "",
search: matches[ 16 ] || "",
hash: matches[ 17 ] || ""
};
},

View File

@ -1,3 +1,3 @@
export default {
SET_CONFIG: 'SET_CONFIG'
}
SET_CONFIG: 'SET_CONFIG',
};

View File

@ -6,10 +6,9 @@ import ACTION_TYPES from './action-types';
* @param object config
*/
export function setConfig(config) {
return (dispatch, getState) => {
return dispatch({
type: ACTION_TYPES.SET_CONFIG,
payload: { config }
});
}
return (dispatch) =>
dispatch({
type: ACTION_TYPES.SET_CONFIG,
payload: { config },
});
}

View File

@ -2,17 +2,15 @@ import deepFreeze from 'deep-freeze';
import ACTION_TYPES from './action-types';
function configReducer(state = {}, action) {
switch (action.type) {
switch (action.type) {
case ACTION_TYPES.SET_CONFIG:
return deepFreeze(Object.assign({}, state, action.payload.config));
case ACTION_TYPES.SET_CONFIG:
return deepFreeze(Object.assign({}, state, action.payload.config));
default:
return state;
}
default:
return state;
}
}
export default configReducer;

View File

@ -1,11 +1,11 @@
export default {
CREATE_RECORD: 'CREATE_RECORD',
UPDATE_RECORD: 'UPDATE_RECORD',
DELETE_RECORD: 'DELETE_RECORD',
FETCH_RECORDS_REQUEST: 'FETCH_RECORDS_REQUEST',
FETCH_RECORDS_FAILURE: 'FETCH_RECORDS_FAILURE',
FETCH_RECORDS_SUCCESS: 'FETCH_RECORDS_SUCCESS',
DELETE_RECORD_REQUEST: 'DELETE_RECORD_REQUEST',
DELETE_RECORD_FAILURE: 'DELETE_RECORD_FAILURE',
DELETE_RECORD_SUCCESS: 'DELETE_RECORD_SUCCESS'
CREATE_RECORD: 'CREATE_RECORD',
UPDATE_RECORD: 'UPDATE_RECORD',
DELETE_RECORD: 'DELETE_RECORD',
FETCH_RECORDS_REQUEST: 'FETCH_RECORDS_REQUEST',
FETCH_RECORDS_FAILURE: 'FETCH_RECORDS_FAILURE',
FETCH_RECORDS_SUCCESS: 'FETCH_RECORDS_SUCCESS',
DELETE_RECORD_REQUEST: 'DELETE_RECORD_REQUEST',
DELETE_RECORD_FAILURE: 'DELETE_RECORD_FAILURE',
DELETE_RECORD_SUCCESS: 'DELETE_RECORD_SUCCESS',
};

View File

@ -1,5 +1,4 @@
import ACTION_TYPES from './action-types';
import fetch from 'isomorphic-fetch';
import backend from 'silverstripe-backend.js';
/**
@ -14,8 +13,8 @@ import backend from 'silverstripe-backend.js';
* @return string
*/
function populate(str, params) {
let names = ['id'];
return names.reduce((str, name) => str.replace(`:${name}`, params[name]), str);
const names = ['id'];
return names.reduce((acc, name) => acc.replace(`:${name}`, params[name]), str);
}
/**
@ -26,19 +25,27 @@ function populate(str, params) {
* @param string url API endpoint
*/
export function fetchRecords(recordType, method, url) {
let payload = {recordType: recordType};
url = populate(url, payload);
return (dispatch, getState) => {
dispatch ({type: ACTION_TYPES.FETCH_RECORDS_REQUEST, payload: payload});
return backend[method.toLowerCase()](url)
.then(response => response.json())
.then(json => {
dispatch({type: ACTION_TYPES.FETCH_RECORDS_SUCCESS, payload: {recordType: recordType, data: json}})
})
.catch((err) => {
dispatch({type: ACTION_TYPES.FETCH_RECORDS_FAILURE, payload: {error: err, recordType: recordType}})
});
}
const payload = { recordType };
return (dispatch) => {
dispatch({
type: ACTION_TYPES.FETCH_RECORDS_REQUEST,
payload,
});
return backend[method.toLowerCase()](populate(url, payload))
.then(response => response.json())
.then(json => {
dispatch({
type: ACTION_TYPES.FETCH_RECORDS_SUCCESS,
payload: { recordType, data: json },
});
})
.catch((err) => {
dispatch({
type: ACTION_TYPES.FETCH_RECORDS_FAILURE,
payload: { error: err, recordType },
});
});
};
}
/**
@ -50,16 +57,24 @@ export function fetchRecords(recordType, method, url) {
* @param string url API endpoint
*/
export function deleteRecord(recordType, id, method, url) {
let payload = {recordType: recordType, id: id};
url = populate(url, payload);
return (dispatch, getState) => {
dispatch ({type: ACTION_TYPES.DELETE_RECORD_REQUEST, payload: payload});
return backend[method.toLowerCase()](url)
.then(json => {
dispatch({type: ACTION_TYPES.DELETE_RECORD_SUCCESS, payload: {recordType: recordType, id: id}})
})
.catch((err) => {
dispatch({type: ACTION_TYPES.DELETE_RECORD_FAILURE, payload: {error: err, recordType: recordType, id: id}})
});
}
const payload = { recordType, id };
return (dispatch) => {
dispatch({
type: ACTION_TYPES.DELETE_RECORD_REQUEST,
payload,
});
return backend[method.toLowerCase()](populate(url, payload))
.then(() => {
dispatch({
type: ACTION_TYPES.DELETE_RECORD_SUCCESS,
payload: { recordType, id },
});
})
.catch((err) => {
dispatch({
type: ACTION_TYPES.DELETE_RECORD_FAILURE,
payload: { error: err, recordType, id },
});
});
};
}

View File

@ -5,59 +5,58 @@ const initialState = {
};
function recordsReducer(state = initialState, action) {
let records;
let recordType;
let records;
let recordType;
switch (action.type) {
switch (action.type) {
case ACTION_TYPES.CREATE_RECORD:
return deepFreeze(Object.assign({}, state, {
case ACTION_TYPES.CREATE_RECORD:
return deepFreeze(Object.assign({}, state, {
}));
}));
case ACTION_TYPES.UPDATE_RECORD:
return deepFreeze(Object.assign({}, state, {
case ACTION_TYPES.UPDATE_RECORD:
return deepFreeze(Object.assign({}, state, {
}));
}));
case ACTION_TYPES.DELETE_RECORD:
return deepFreeze(Object.assign({}, state, {
case ACTION_TYPES.DELETE_RECORD:
return deepFreeze(Object.assign({}, state, {
}));
}));
case ACTION_TYPES.FETCH_RECORDS_REQUEST:
return state;
case ACTION_TYPES.FETCH_RECORDS_REQUEST:
return state;
case ACTION_TYPES.FETCH_RECORDS_FAILURE:
return state;
case ACTION_TYPES.FETCH_RECORDS_FAILURE:
return state;
case ACTION_TYPES.FETCH_RECORDS_SUCCESS:
recordType = action.payload.recordType;
// TODO Automatic pluralisation from recordType
records = action.payload.data._embedded[recordType + 's'];
return deepFreeze(Object.assign({}, state, {
[recordType]: records
}));
case ACTION_TYPES.FETCH_RECORDS_SUCCESS:
recordType = action.payload.recordType;
// TODO Automatic pluralisation from recordType
records = action.payload.data._embedded[`${recordType}s`];
return deepFreeze(Object.assign({}, state, {
[recordType]: records,
}));
case ACTION_TYPES.DELETE_RECORD_REQUEST:
return state;
case ACTION_TYPES.DELETE_RECORD_REQUEST:
return state;
case ACTION_TYPES.DELETE_RECORD_FAILURE:
return state;
case ACTION_TYPES.DELETE_RECORD_FAILURE:
return state;
case ACTION_TYPES.DELETE_RECORD_SUCCESS:
recordType = action.payload.recordType;
records = state[recordType]
.filter(record => record.ID != action.payload.id)
case ACTION_TYPES.DELETE_RECORD_SUCCESS:
recordType = action.payload.recordType;
records = state[recordType]
.filter(record => record.ID !== action.payload.id);
return deepFreeze(Object.assign({}, state, {
[recordType]: records
}));
default:
return state;
}
return deepFreeze(Object.assign({}, state, {
[recordType]: records,
}));
default:
return state;
}
}
export default recordsReducer;

View File

@ -3,34 +3,34 @@ jest.dontMock('../reducer');
jest.dontMock('../action-types');
var recordsReducer = require('../reducer').default,
ACTION_TYPES = require('../action-types').default;
ACTION_TYPES = require('../action-types').default;
describe('recordsReducer', () => {
describe('DELETE_RECORD_SUCCESS', () => {
const initialState = {
TypeA: [
{ID: 1},
{ID: 2}
],
TypeB: [
{ID: 1},
{ID: 2}
]
};
describe('DELETE_RECORD_SUCCESS', () => {
const initialState = {
TypeA: [
{ID: 1},
{ID: 2}
],
TypeB: [
{ID: 1},
{ID: 2}
]
};
it('removes records from the declared type', () => {
const nextState = recordsReducer(initialState, {
type: ACTION_TYPES.DELETE_RECORD_SUCCESS,
payload: { recordType: 'TypeA', id: 2 }
});
it('removes records from the declared type', () => {
const nextState = recordsReducer(initialState, {
type: ACTION_TYPES.DELETE_RECORD_SUCCESS,
payload: { recordType: 'TypeA', id: 2 }
});
expect(nextState.TypeA.length).toBe(1);
expect(nextState.TypeA[0].ID).toBe(1);
expect(nextState.TypeB.length).toBe(2);
expect(nextState.TypeB[0].ID).toBe(1);
expect(nextState.TypeB[1].ID).toBe(2);
})
});
expect(nextState.TypeA.length).toBe(1);
expect(nextState.TypeA[0].ID).toBe(1);
expect(nextState.TypeB.length).toBe(2);
expect(nextState.TypeB[0].ID).toBe(1);
expect(nextState.TypeB[1].ID).toBe(2);
})
});
});

View File

@ -1,5 +1,5 @@
const ACTION_TYPES = {
SET_SCHEMA: 'SET_SCHEMA'
SET_SCHEMA: 'SET_SCHEMA',
};
export default ACTION_TYPES;

View File

@ -6,10 +6,9 @@ import ACTION_TYPES from './action-types';
* @param string schema - JSON schema for the layout.
*/
export function setSchema(schema) {
return (dispatch, getState) => {
return dispatch ({
type: ACTION_TYPES.SET_SCHEMA,
payload: schema
});
}
return (dispatch) =>
dispatch({
type: ACTION_TYPES.SET_SCHEMA,
payload: schema,
});
}

View File

@ -4,15 +4,14 @@ import ACTION_TYPES from './action-types';
const initialState = deepFreeze({});
export default function schemaReducer(state = initialState, action = null) {
switch (action.type) {
switch (action.type) {
case ACTION_TYPES.SET_SCHEMA:
const id = action.payload.schema.schema_url;
return deepFreeze(Object.assign({}, state, {[id]: action.payload}));
default:
return state;
case ACTION_TYPES.SET_SCHEMA: {
const id = action.payload.schema.schema_url;
return deepFreeze(Object.assign({}, state, { [id]: action.payload }));
}
default:
return state;
}
}

View File

@ -7,18 +7,18 @@ import ACTION_TYPES from '../action-types';
describe('schemaReducer', () => {
describe('SET_SCHEMA', () => {
describe('SET_SCHEMA', () => {
it('should create a new form', () => {
const initialState = { };
const serverResponse = { id: 'TestForm', schema_url: 'URL' };
it('should create a new form', () => {
const initialState = { };
const serverResponse = { id: 'TestForm', schema_url: 'URL' };
const nextState = schemaReducer(initialState, {
type: ACTION_TYPES.SET_SCHEMA,
payload: { schema: serverResponse }
});
const nextState = schemaReducer(initialState, {
type: ACTION_TYPES.SET_SCHEMA,
payload: { schema: serverResponse }
});
expect(nextState.URL.schema.id).toBe('TestForm');
});
expect(nextState.URL.schema.id).toBe('TestForm');
});
});
});

View File

@ -4,41 +4,41 @@ var reducerRegister = require('../reducer-register').default;
describe('ReducerRegister', () => {
var reducer = () => null;
var reducer = () => null;
it('should add a reducer to the register', () => {
expect(reducerRegister.getAll().test).toBe(undefined);
it('should add a reducer to the register', () => {
expect(reducerRegister.getAll().test).toBe(undefined);
reducerRegister.add('test', reducer);
expect(reducerRegister.getAll().test).toBe(reducer);
reducerRegister.add('test', reducer);
expect(reducerRegister.getAll().test).toBe(reducer);
reducerRegister.remove('test');
});
reducerRegister.remove('test');
});
it('should remove a reducer from the register', () => {
reducerRegister.add('test', reducer);
expect(reducerRegister.getAll().test).toBe(reducer);
it('should remove a reducer from the register', () => {
reducerRegister.add('test', reducer);
expect(reducerRegister.getAll().test).toBe(reducer);
reducerRegister.remove('test');
expect(reducerRegister.getAll().test).toBe(undefined);
});
reducerRegister.remove('test');
expect(reducerRegister.getAll().test).toBe(undefined);
});
it('should get all reducers from the register', () => {
reducerRegister.add('test1', reducer);
reducerRegister.add('test2', reducer);
it('should get all reducers from the register', () => {
reducerRegister.add('test1', reducer);
reducerRegister.add('test2', reducer);
expect(reducerRegister.getAll().test1).toBe(reducer);
expect(reducerRegister.getAll().test2).toBe(reducer);
expect(reducerRegister.getAll().test1).toBe(reducer);
expect(reducerRegister.getAll().test2).toBe(reducer);
reducerRegister.remove('test1');
reducerRegister.remove('test2');
});
reducerRegister.remove('test1');
reducerRegister.remove('test2');
});
it('should get a single reducer from the register', () => {
reducerRegister.add('test', reducer);
expect(reducerRegister.getByKey('test')).toBe(reducer);
it('should get a single reducer from the register', () => {
reducerRegister.add('test', reducer);
expect(reducerRegister.getByKey('test')).toBe(reducer);
reducerRegister.remove('test');
});
reducerRegister.remove('test');
});
});

View File

@ -5,98 +5,98 @@ import fetch from 'isomorphic-fetch';
import backend from '../silverstripe-backend';
var getFetchMock = function(data) {
let mock = jest.genMockFunction();
let promise = new Promise((resolve, reject) => {
process.nextTick(() => resolve(data));
});
mock.mockReturnValue(promise);
let mock = jest.genMockFunction();
let promise = new Promise((resolve, reject) => {
process.nextTick(() => resolve(data));
});
mock.mockReturnValue(promise);
return mock;
return mock;
};
describe('SilverStripeBackend', () => {
beforeAll(() => {
let fetchMock = getFetchMock();
backend.fetch = fetchMock;
beforeAll(() => {
let fetchMock = getFetchMock();
backend.fetch = fetchMock;
});
describe('get()', () => {
it('should return a promise', () => {
var promise = backend.get('http://example.com');
expect(typeof promise).toBe('object');
});
describe('get()', () => {
it('should return a promise', () => {
var promise = backend.get('http://example.com');
expect(typeof promise).toBe('object');
});
it('should send a GET request to an endpoint', () => {
backend.get('http://example.com');
expect(backend.fetch).toBeCalledWith(
'http://example.com',
{method: 'get', credentials: 'same-origin'}
);
});
it('should send a GET request to an endpoint', () => {
backend.get('http://example.com');
expect(backend.fetch).toBeCalledWith(
'http://example.com',
{method: 'get', credentials: 'same-origin'}
);
});
describe('post()', () => {
});
it('should return a promise', () => {
var promise = backend.get('http://example.com/item');
expect(typeof promise).toBe('object');
});
it('should send a POST request to an endpoint', () => {
const postData = { id: 1 };
backend.post('http://example.com', postData);
expect(backend.fetch).toBeCalledWith(
'http://example.com',
{method: 'post', body: postData, credentials: 'same-origin'}
);
});
describe('post()', () => {
it('should return a promise', () => {
var promise = backend.get('http://example.com/item');
expect(typeof promise).toBe('object');
});
describe('put()', () => {
it('should send a POST request to an endpoint', () => {
const postData = { id: 1 };
it('should return a promise', () => {
var promise = backend.get('http://example.com/item');
expect(typeof promise).toBe('object');
});
it('should send a PUT request to an endpoint', () => {
const putData = { id: 1 };
backend.put('http://example.com', putData);
expect(backend.fetch).toBeCalledWith(
'http://example.com',
{method: 'put', body: putData, credentials: 'same-origin'}
);
});
backend.post('http://example.com', postData);
expect(backend.fetch).toBeCalledWith(
'http://example.com',
{method: 'post', body: postData, credentials: 'same-origin'}
);
});
describe('delete()', () => {
});
it('should return a promise', () => {
var promise = backend.get('http://example.com/item');
expect(typeof promise).toBe('object');
});
it('should send a DELETE request to an endpoint', () => {
const deleteData = { id: 1 };
backend.delete('http://example.com', deleteData);
expect(backend.fetch).toBeCalledWith(
'http://example.com',
{method: 'delete', body: deleteData, credentials: 'same-origin'}
);
});
describe('put()', () => {
it('should return a promise', () => {
var promise = backend.get('http://example.com/item');
expect(typeof promise).toBe('object');
});
it('should send a PUT request to an endpoint', () => {
const putData = { id: 1 };
backend.put('http://example.com', putData);
expect(backend.fetch).toBeCalledWith(
'http://example.com',
{method: 'put', body: putData, credentials: 'same-origin'}
);
});
});
describe('delete()', () => {
it('should return a promise', () => {
var promise = backend.get('http://example.com/item');
expect(typeof promise).toBe('object');
});
it('should send a DELETE request to an endpoint', () => {
const deleteData = { id: 1 };
backend.delete('http://example.com', deleteData);
expect(backend.fetch).toBeCalledWith(
'http://example.com',
{method: 'delete', body: deleteData, credentials: 'same-origin'}
);
});
});
});

View File

@ -1,110 +1,112 @@
var packageJson = require('./package.json'),
autoprefixer = require('autoprefixer'),
babelify = require('babelify'),
browserify = require('browserify'),
eventStream = require('event-stream'),
glob = require('glob'),
gulp = require('gulp'),
babel = require('gulp-babel'),
diff = require('gulp-diff'),
gulpif = require('gulp-if'),
notify = require('gulp-notify'),
postcss = require('gulp-postcss'),
sass = require('gulp-sass'),
sourcemaps = require('gulp-sourcemaps'),
uglify = require('gulp-uglify'),
gulpUtil = require('gulp-util'),
path = require('path'),
source = require('vinyl-source-stream'),
buffer = require('vinyl-buffer'),
semver = require('semver'),
sprity = require('sprity'),
watchify = require('watchify');
const packageJson = require('./package.json');
const autoprefixer = require('autoprefixer');
const babelify = require('babelify'); // eslint-disable-line no-unused-vars
const browserify = require('browserify');
const eventStream = require('event-stream');
const glob = require('glob');
const gulp = require('gulp');
const babel = require('gulp-babel');
const diff = require('gulp-diff');
const gulpif = require('gulp-if');
const notify = require('gulp-notify');
const postcss = require('gulp-postcss');
const sass = require('gulp-sass');
const sourcemaps = require('gulp-sourcemaps');
const uglify = require('gulp-uglify');
const gulpUtil = require('gulp-util');
const path = require('path');
const source = require('vinyl-source-stream');
const buffer = require('vinyl-buffer');
const semver = require('semver');
const sprity = require('sprity');
const watchify = require('watchify');
var isDev = typeof process.env.npm_config_development !== 'undefined';
const isDev = typeof process.env.npm_config_development !== 'undefined';
var PATHS = {
MODULES: './node_modules',
ADMIN: './admin',
ADMIN_IMAGES: './admin/images',
ADMIN_SCSS: './admin/scss',
ADMIN_THIRDPARTY: './admin/thirdparty',
ADMIN_JAVASCRIPT_SRC: './admin/javascript/src',
ADMIN_JAVASCRIPT_DIST: './admin/javascript/dist',
FRAMEWORK: '.',
FRAMEWORK_THIRDPARTY: './thirdparty',
FRAMEWORK_DEV_INSTALL: './dev/install',
FRAMEWORK_JAVASCRIPT_SRC: './javascript/src',
FRAMEWORK_JAVASCRIPT_DIST: './javascript/dist'
process.env.NODE_ENV = isDev ? 'development' : 'production';
const PATHS = {
MODULES: './node_modules',
ADMIN: './admin',
ADMIN_IMAGES: './admin/images',
ADMIN_SCSS: './admin/scss',
ADMIN_THIRDPARTY: './admin/thirdparty',
ADMIN_JAVASCRIPT_SRC: './admin/javascript/src',
ADMIN_JAVASCRIPT_DIST: './admin/javascript/dist',
FRAMEWORK: '.',
FRAMEWORK_THIRDPARTY: './thirdparty',
FRAMEWORK_DEV_INSTALL: './dev/install',
FRAMEWORK_JAVASCRIPT_SRC: './javascript/src',
FRAMEWORK_JAVASCRIPT_DIST: './javascript/dist',
};
// Folders which contain both scss and css folders to be compiled
var rootCompileFolders = [PATHS.FRAMEWORK, PATHS.ADMIN, PATHS.FRAMEWORK_DEV_INSTALL]
const rootCompileFolders = [PATHS.FRAMEWORK, PATHS.ADMIN, PATHS.FRAMEWORK_DEV_INSTALL];
var browserifyOptions = {
debug: true,
paths: [PATHS.ADMIN_JAVASCRIPT_SRC, PATHS.FRAMEWORK_JAVASCRIPT_SRC]
const browserifyOptions = {
debug: true,
paths: [PATHS.ADMIN_JAVASCRIPT_SRC, PATHS.FRAMEWORK_JAVASCRIPT_SRC],
};
var babelifyOptions = {
presets: ['es2015', 'react'],
ignore: /(node_modules|thirdparty)/,
comments: false
const babelifyOptions = {
presets: ['es2015', 'react'],
ignore: /(node_modules|thirdparty)/,
comments: false,
};
// Used for autoprefixing css properties (same as Bootstrap Aplha.2 defaults)
var supportedBrowsers = [
'Chrome >= 35',
'Firefox >= 31',
'Edge >= 12',
'Explorer >= 9',
'iOS >= 8',
'Safari >= 8',
'Android 2.3',
'Android >= 4',
'Opera >= 12'
const supportedBrowsers = [
'Chrome >= 35',
'Firefox >= 31',
'Edge >= 12',
'Explorer >= 9',
'iOS >= 8',
'Safari >= 8',
'Android 2.3',
'Android >= 4',
'Opera >= 12',
];
var blueimpFileUploadConfig = {
src: PATHS.MODULES + '/blueimp-file-upload',
dest: PATHS.FRAMEWORK_THIRDPARTY + '/jquery-fileupload',
files: [
'/cors/jquery.postmessage-transport.js',
'/cors/jquery.xdr-transport.js',
'/jquery.fileupload-ui.js',
'/jquery.fileupload.js',
'/jquery.iframe-transport.js'
]
const blueimpFileUploadConfig = {
src: `${PATHS.MODULES}/blueimp-file-upload`,
dest: PATHS.FRAMEWORK_THIRDPARTY + '/jquery-fileupload',
files: [
'/cors/jquery.postmessage-transport.js',
'/cors/jquery.xdr-transport.js',
'/jquery.fileupload-ui.js',
'/jquery.fileupload.js',
'/jquery.iframe-transport.js'
]
};
var blueimpLoadImageConfig = {
src: PATHS.MODULES + '/blueimp-load-image',
dest: PATHS.FRAMEWORK_THIRDPARTY + '/javascript-loadimage',
files: ['/load-image.js']
src: PATHS.MODULES + '/blueimp-load-image',
dest: PATHS.FRAMEWORK_THIRDPARTY + '/javascript-loadimage',
files: ['/load-image.js']
};
var blueimpTmplConfig = {
src: PATHS.MODULES + '/blueimp-tmpl',
dest: PATHS.FRAMEWORK_THIRDPARTY + '/javascript-templates',
files: ['/tmpl.js']
src: PATHS.MODULES + '/blueimp-tmpl',
dest: PATHS.FRAMEWORK_THIRDPARTY + '/javascript-templates',
files: ['/tmpl.js']
};
var jquerySizesConfig = {
src: PATHS.MODULES + '/jquery-sizes',
dest: PATHS.ADMIN_THIRDPARTY + '/jsizes',
files: ['/lib/jquery.sizes.js']
src: PATHS.MODULES + '/jquery-sizes',
dest: PATHS.ADMIN_THIRDPARTY + '/jsizes',
files: ['/lib/jquery.sizes.js']
};
var tinymceConfig = {
src: PATHS.MODULES + '/tinymce',
dest: PATHS.FRAMEWORK_THIRDPARTY + '/tinymce',
files: [
'/tinymce.min.js', // Exclude unminified file to keep repository size down
'/jquery.tinymce.min.js',
'/themes/**',
'/skins/**',
'/plugins/**'
]
src: PATHS.MODULES + '/tinymce',
dest: PATHS.FRAMEWORK_THIRDPARTY + '/tinymce',
files: [
'/tinymce.min.js', // Exclude unminified file to keep repository size down
'/jquery.tinymce.min.js',
'/themes/**',
'/skins/**',
'/plugins/**'
]
};
/**
@ -116,12 +118,12 @@ var tinymceConfig = {
* @param array libConfig.files - The list of files to copy from the source to the destination directory
*/
function copyFiles(libConfig) {
libConfig.files.forEach(function (file) {
var dir = path.parse(file).dir;
libConfig.files.forEach(function (file) {
var dir = path.parse(file).dir;
gulp.src(libConfig.src + file)
.pipe(gulp.dest(libConfig.dest + dir));
});
gulp.src(libConfig.src + file)
.pipe(gulp.dest(libConfig.dest + dir));
});
}
/**
@ -133,17 +135,17 @@ function copyFiles(libConfig) {
* @param array libConfig.files - The list of files to copy from the source to the destination directory
*/
function diffFiles(libConfig) {
libConfig.files.forEach(function (file) {
var dir = path.parse(file).dir;
libConfig.files.forEach(function (file) {
var dir = path.parse(file).dir;
gulp.src(libConfig.src + file)
.pipe(diff(libConfig.dest + dir))
.pipe(diff.reporter({ fail: true, quiet: true }))
.on('error', function (error) {
console.error(new Error('Sanity check failed. \'' + libConfig.dest + file + '\' has been modified.'));
process.exit(1);
});
});
gulp.src(libConfig.src + file)
.pipe(diff(libConfig.dest + dir))
.pipe(diff.reporter({ fail: true, quiet: true }))
.on('error', function (error) {
console.error(new Error('Sanity check failed. \'' + libConfig.dest + file + '\' has been modified.'));
process.exit(1);
});
});
}
/**
@ -154,31 +156,31 @@ function diffFiles(libConfig) {
* @return object
*/
function transformToUmd(files, dest) {
return eventStream.merge(files.map(function (file) {
return gulp.src(file)
.pipe(babel({
presets: ['es2015'],
moduleId: 'ss.' + path.parse(file).name,
plugins: ['transform-es2015-modules-umd'],
comments: false
}))
.on('error', notify.onError({
message: 'Error: <%= error.message %>',
}))
.pipe(gulp.dest(dest));
}));
return eventStream.merge(files.map(function (file) {
return gulp.src(file)
.pipe(babel({
presets: ['es2015'],
moduleId: 'ss.' + path.parse(file).name,
plugins: ['transform-es2015-modules-umd'],
comments: false
}))
.on('error', notify.onError({
message: 'Error: <%= error.message %>',
}))
.pipe(gulp.dest(dest));
}));
}
// Make sure the version of Node being used is valid.
if (!semver.satisfies(process.versions.node, packageJson.engines.node)) {
console.error('Invalid Node.js version. You need to be using ' + packageJson.engines.node + '. If you want to manage multiple Node.js versions try https://github.com/creationix/nvm');
process.exit(1);
console.error('Invalid Node.js version. You need to be using ' + packageJson.engines.node + '. If you want to manage multiple Node.js versions try https://github.com/creationix/nvm');
process.exit(1);
}
if (isDev) {
browserifyOptions.cache = {};
browserifyOptions.packageCache = {};
browserifyOptions.plugin = [watchify];
browserifyOptions.cache = {};
browserifyOptions.packageCache = {};
browserifyOptions.plugin = [watchify];
}
gulp.task('build', ['umd', 'bundle']);
@ -186,162 +188,161 @@ gulp.task('build', ['umd', 'bundle']);
gulp.task('bundle', ['bundle-lib', 'bundle-legacy', 'bundle-framework']);
gulp.task('bundle-lib', function bundleLib() {
var bundleFileName = 'bundle-lib.js';
var bundleFileName = 'bundle-lib.js';
return browserify(Object.assign({}, browserifyOptions, { entries: PATHS.ADMIN_JAVASCRIPT_SRC + '/bundles/lib.js' }))
.on('update', bundleLib)
.on('log', function (msg) { gulpUtil.log('Finished', 'bundled ' + bundleFileName + ' ' + msg) })
.transform('babelify', babelifyOptions)
.require('deep-freeze', { expose: 'deep-freeze' })
.require('react', { expose: 'react' })
.require('react-addons-css-transition-group', { expose: 'react-addons-css-transition-group' })
.require('react-addons-test-utils', { expose: 'react-addons-test-utils' })
.require('react-dom', { expose: 'react-dom' })
.require('react-redux', { expose: 'react-redux' })
.require('redux', { expose: 'redux' })
.require('redux-thunk', { expose: 'redux-thunk' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/form/index', { expose: 'components/form/index' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/form-action/index', { expose: 'components/form-action/index' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/form-builder/index', { expose: 'components/form-builder/index' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/grid-field/index', { expose: 'components/grid-field/index' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/grid-field/cell', { expose: 'components/grid-field/cell/index' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/grid-field/header', { expose: 'components/grid-field/header' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/grid-field/header-cell', { expose: 'components/grid-field/header-cell' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/grid-field/row', { expose: 'components/grid-field/row' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/grid-field/table', { expose: 'components/grid-field/table' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/hidden-field/index', { expose: 'components/hidden-field/index' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/text-field/index', { expose: 'components/text-field/index' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/north-header/index', { expose: 'components/north-header/index' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/north-header-breadcrumbs/index', { expose: 'components/north-header-breadcrumbs/index' })
.require(PATHS.FRAMEWORK_JAVASCRIPT_SRC + '/i18n.js', { expose: 'i18n' })
.require(PATHS.FRAMEWORK_JAVASCRIPT_SRC + '/jQuery.js', { expose: 'jQuery' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/reducer-register.js', { expose: 'reducer-register' })
.require(PATHS.FRAMEWORK_JAVASCRIPT_SRC + '/router.js', { expose: 'router' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/silverstripe-component', { expose: 'silverstripe-component' })
.bundle()
.on('update', bundleLib)
.on('error', notify.onError({ message: bundleFileName + ': <%= error.message %>' }))
.pipe(source(bundleFileName))
.pipe(buffer())
.pipe(sourcemaps.init({ loadMaps: true }))
.pipe(gulpif(!isDev, uglify()))
.pipe(sourcemaps.write('./'))
.pipe(gulp.dest(PATHS.ADMIN_JAVASCRIPT_DIST));
return browserify(Object.assign({}, browserifyOptions, { entries: PATHS.ADMIN_JAVASCRIPT_SRC + '/bundles/lib.js' }))
.on('update', bundleLib)
.on('log', function (msg) { gulpUtil.log('Finished', 'bundled ' + bundleFileName + ' ' + msg) })
.transform('babelify', babelifyOptions)
.require('deep-freeze', { expose: 'deep-freeze' })
.require('react', { expose: 'react' })
.require('react-addons-css-transition-group', { expose: 'react-addons-css-transition-group' })
.require('react-addons-test-utils', { expose: 'react-addons-test-utils' })
.require('react-dom', { expose: 'react-dom' })
.require('react-redux', { expose: 'react-redux' })
.require('redux', { expose: 'redux' })
.require('redux-thunk', { expose: 'redux-thunk' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/form/index', { expose: 'components/form/index' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/form-action/index', { expose: 'components/form-action/index' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/form-builder/index', { expose: 'components/form-builder/index' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/grid-field/index', { expose: 'components/grid-field/index' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/grid-field/cell', { expose: 'components/grid-field/cell/index' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/grid-field/header', { expose: 'components/grid-field/header' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/grid-field/header-cell', { expose: 'components/grid-field/header-cell' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/grid-field/row', { expose: 'components/grid-field/row' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/grid-field/table', { expose: 'components/grid-field/table' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/hidden-field/index', { expose: 'components/hidden-field/index' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/text-field/index', { expose: 'components/text-field/index' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/north-header/index', { expose: 'components/north-header/index' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/north-header-breadcrumbs/index', { expose: 'components/north-header-breadcrumbs/index' })
.require(PATHS.FRAMEWORK_JAVASCRIPT_SRC + '/i18n.js', { expose: 'i18n' })
.require(PATHS.FRAMEWORK_JAVASCRIPT_SRC + '/jQuery.js', { expose: 'jQuery' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/reducer-register.js', { expose: 'reducer-register' })
.require(PATHS.FRAMEWORK_JAVASCRIPT_SRC + '/router.js', { expose: 'router' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/silverstripe-component', { expose: 'silverstripe-component' })
.bundle()
.on('error', notify.onError({ message: bundleFileName + ': <%= error.message %>' }))
.pipe(source(bundleFileName))
.pipe(buffer())
.pipe(sourcemaps.init({ loadMaps: true }))
.pipe(gulpif(!isDev, uglify()))
.pipe(sourcemaps.write('./'))
.pipe(gulp.dest(PATHS.ADMIN_JAVASCRIPT_DIST));
});
gulp.task('bundle-legacy', function bundleLeftAndMain() {
var bundleFileName = 'bundle-legacy.js';
var bundleFileName = 'bundle-legacy.js';
return browserify(Object.assign({}, browserifyOptions, { entries: PATHS.ADMIN_JAVASCRIPT_SRC + '/bundles/legacy.js' }))
.on('update', bundleLeftAndMain)
.on('log', function (msg) { gulpUtil.log('Finished', 'bundled ' + bundleFileName + ' ' + msg) })
.transform('babelify', babelifyOptions)
.external('jQuery')
.external('i18n')
.external('router')
.bundle()
.on('update', bundleLeftAndMain)
.on('error', notify.onError({ message: bundleFileName + ': <%= error.message %>' }))
.pipe(source(bundleFileName))
.pipe(buffer())
.pipe(sourcemaps.init({ loadMaps: true }))
.pipe(gulpif(!isDev, uglify()))
.pipe(sourcemaps.write('./'))
.pipe(gulp.dest(PATHS.ADMIN_JAVASCRIPT_DIST));
return browserify(Object.assign({}, browserifyOptions, { entries: PATHS.ADMIN_JAVASCRIPT_SRC + '/bundles/legacy.js' }))
.on('update', bundleLeftAndMain)
.on('log', function (msg) { gulpUtil.log('Finished', 'bundled ' + bundleFileName + ' ' + msg) })
.transform('babelify', babelifyOptions)
.external('jQuery')
.external('i18n')
.external('router')
.bundle()
.on('update', bundleLeftAndMain)
.on('error', notify.onError({ message: bundleFileName + ': <%= error.message %>' }))
.pipe(source(bundleFileName))
.pipe(buffer())
.pipe(sourcemaps.init({ loadMaps: true }))
.pipe(gulpif(!isDev, uglify()))
.pipe(sourcemaps.write('./'))
.pipe(gulp.dest(PATHS.ADMIN_JAVASCRIPT_DIST));
});
gulp.task('bundle-framework', function bundleBoot() {
var bundleFileName = 'bundle-framework.js';
var bundleFileName = 'bundle-framework.js';
return browserify(Object.assign({}, browserifyOptions, { entries: PATHS.ADMIN_JAVASCRIPT_SRC + '/boot/index.js' }))
.on('update', bundleBoot)
.on('log', function (msg) { gulpUtil.log('Finished', 'bundled ' + bundleFileName + ' ' + msg) })
.transform('babelify', babelifyOptions)
.external('components/action-button/index')
.external('components/north-header/index')
.external('components/form-builder/index')
.external('deep-freeze')
.external('components/grid-field/index')
.external('i18n')
.external('jQuery')
.external('page.js')
.external('react-addons-test-utils')
.external('react-dom')
.external('react-redux')
.external('react')
.external('reducer-register')
.external('redux-thunk')
.external('redux')
.external('silverstripe-component')
.bundle()
.on('update', bundleBoot)
.on('error', notify.onError({ message: bundleFileName + ': <%= error.message %>' }))
.pipe(source(bundleFileName))
.pipe(buffer())
.pipe(sourcemaps.init({ loadMaps: true }))
.pipe(gulpif(!isDev, uglify()))
.pipe(sourcemaps.write('./'))
.pipe(gulp.dest(PATHS.ADMIN_JAVASCRIPT_DIST));
return browserify(Object.assign({}, browserifyOptions, { entries: PATHS.ADMIN_JAVASCRIPT_SRC + '/boot/index.js' }))
.on('update', bundleBoot)
.on('log', function (msg) { gulpUtil.log('Finished', 'bundled ' + bundleFileName + ' ' + msg) })
.transform('babelify', babelifyOptions)
.external('components/action-button/index')
.external('components/north-header/index')
.external('components/form-builder/index')
.external('deep-freeze')
.external('components/grid-field/index')
.external('i18n')
.external('jQuery')
.external('page.js')
.external('react-addons-test-utils')
.external('react-dom')
.external('react-redux')
.external('react')
.external('reducer-register')
.external('redux-thunk')
.external('redux')
.external('silverstripe-component')
.bundle()
.on('update', bundleBoot)
.on('error', notify.onError({ message: bundleFileName + ': <%= error.message %>' }))
.pipe(source(bundleFileName))
.pipe(buffer())
.pipe(sourcemaps.init({ loadMaps: true }))
.pipe(gulpif(!isDev, uglify()))
.pipe(sourcemaps.write('./'))
.pipe(gulp.dest(PATHS.ADMIN_JAVASCRIPT_DIST));
});
gulp.task('sanity', function () {
diffFiles(blueimpFileUploadConfig);
diffFiles(blueimpLoadImageConfig);
diffFiles(blueimpTmplConfig);
diffFiles(jquerySizesConfig);
diffFiles(tinymceConfig);
diffFiles(blueimpFileUploadConfig);
diffFiles(blueimpLoadImageConfig);
diffFiles(blueimpTmplConfig);
diffFiles(jquerySizesConfig);
diffFiles(tinymceConfig);
});
gulp.task('thirdparty', function () {
copyFiles(blueimpFileUploadConfig);
copyFiles(blueimpLoadImageConfig);
copyFiles(blueimpTmplConfig);
copyFiles(jquerySizesConfig);
copyFiles(tinymceConfig);
copyFiles(blueimpFileUploadConfig);
copyFiles(blueimpLoadImageConfig);
copyFiles(blueimpTmplConfig);
copyFiles(jquerySizesConfig);
copyFiles(tinymceConfig);
});
gulp.task('umd', ['umd-admin', 'umd-framework'], function () {
if (isDev) {
gulp.watch(PATHS.ADMIN_JAVASCRIPT_SRC + '/*.js', ['umd-admin']);
gulp.watch(PATHS.FRAMEWORK_JAVASCRIPT_SRC + '/*.js', ['umd-framework']);
}
if (isDev) {
gulp.watch(PATHS.ADMIN_JAVASCRIPT_SRC + '/*.js', ['umd-admin']);
gulp.watch(PATHS.FRAMEWORK_JAVASCRIPT_SRC + '/*.js', ['umd-framework']);
}
});
gulp.task('umd-admin', function () {
var files = glob.sync(PATHS.ADMIN_JAVASCRIPT_SRC + '/*.js', { ignore: PATHS.ADMIN_JAVASCRIPT_SRC + '/LeftAndMain.!(Ping).js' });
var files = glob.sync(PATHS.ADMIN_JAVASCRIPT_SRC + '/*.js', { ignore: PATHS.ADMIN_JAVASCRIPT_SRC + '/LeftAndMain.!(Ping).js' });
return transformToUmd(files, PATHS.ADMIN_JAVASCRIPT_DIST);
return transformToUmd(files, PATHS.ADMIN_JAVASCRIPT_DIST);
});
gulp.task('umd-framework', function () {
return transformToUmd(glob.sync(PATHS.FRAMEWORK_JAVASCRIPT_SRC + '/*.js'), PATHS.FRAMEWORK_JAVASCRIPT_DIST);
return transformToUmd(glob.sync(PATHS.FRAMEWORK_JAVASCRIPT_SRC + '/*.js'), PATHS.FRAMEWORK_JAVASCRIPT_DIST);
});
/*
* Takes individual images and compiles them together into sprites
*/
gulp.task('sprites', function () {
return sprity.src({
src: PATHS.ADMIN_IMAGES + '/sprites/src/**/*.{png,jpg}',
cssPath: '../images/sprites/dist',
style: './_spritey.scss',
processor: 'sass',
split: true,
margin: 0
})
.pipe(gulpif('*.png', gulp.dest(PATHS.ADMIN_IMAGES + '/sprites/dist'), gulp.dest(PATHS.ADMIN_SCSS)))
return sprity.src({
src: PATHS.ADMIN_IMAGES + '/sprites/src/**/*.{png,jpg}',
cssPath: '../images/sprites/dist',
style: './_spritey.scss',
processor: 'sass',
split: true,
margin: 0
})
.pipe(gulpif('*.png', gulp.dest(PATHS.ADMIN_IMAGES + '/sprites/dist'), gulp.dest(PATHS.ADMIN_SCSS)))
});
gulp.task('css', ['compile:css'], function () {
if (isDev) {
rootCompileFolders.forEach(function (folder) {
gulp.watch(folder + '/scss/**/*.scss', ['compile:css']);
});
if (isDev) {
rootCompileFolders.forEach(function (folder) {
gulp.watch(folder + '/scss/**/*.scss', ['compile:css']);
});
// Watch the .scss files in react components
gulp.watch('./admin/javascript/src/**/*.scss', ['compile:css']);
}
// Watch the .scss files in react components
gulp.watch('./admin/javascript/src/**/*.scss', ['compile:css']);
}
})
/*
@ -349,20 +350,20 @@ gulp.task('css', ['compile:css'], function () {
* Watches for changes if --development flag is given
*/
gulp.task('compile:css', function () {
var outputStyle = isDev ? 'expanded' : 'compressed';
var outputStyle = isDev ? 'expanded' : 'compressed';
var tasks = rootCompileFolders.map(function(folder) {
return gulp.src(folder + '/scss/**/*.scss')
.pipe(sourcemaps.init())
.pipe(sass({ outputStyle: outputStyle })
.on('error', notify.onError({
message: 'Error: <%= error.message %>'
}))
)
.pipe(postcss([autoprefixer({ browsers: supportedBrowsers })]))
.pipe(sourcemaps.write())
.pipe(gulp.dest(folder + '/css'))
});
var tasks = rootCompileFolders.map(function(folder) {
return gulp.src(folder + '/scss/**/*.scss')
.pipe(sourcemaps.init())
.pipe(sass({ outputStyle: outputStyle })
.on('error', notify.onError({
message: 'Error: <%= error.message %>'
}))
)
.pipe(postcss([autoprefixer({ browsers: supportedBrowsers })]))
.pipe(sourcemaps.write())
.pipe(gulp.dest(folder + '/css'))
});
return tasks;
return tasks;
});

View File

@ -27,7 +27,7 @@ ss.editorWrappers.tinyMCE = (function() {
* Initialise the editor
*
* @param {String} ID of parent textarea domID
*/
*/
init: function(ID) {
editorID = ID;
@ -45,7 +45,7 @@ ss.editorWrappers.tinyMCE = (function() {
* Get TinyMCE Editor instance
*
* @returns Editor
*/
*/
getInstance: function() {
return tinymce.EditorManager.get(editorID);
},
@ -68,7 +68,7 @@ ss.editorWrappers.tinyMCE = (function() {
* Get config for this data
*
* @returns array
*/
*/
getConfig: function() {
var selector = "#" + editorID,
config = $(selector).data('config'),
@ -581,8 +581,8 @@ $.entwine('ss', function($) {
break;
case 'file':
var fileid = this.find('.ss-uploadfield .ss-uploadfield-item').attr('data-fileid');
href = fileid ? '[file_link,id=' + fileid + ']' : '';
var fileid = this.find('.ss-uploadfield .ss-uploadfield-item').attr('data-fileid');
href = fileid ? '[file_link,id=' + fileid + ']' : '';
break;
case 'email':

View File

@ -85,7 +85,7 @@ $.widget('blueimpUIX.fileupload', $.blueimpUI.fileupload, {
e.preventDefault(); // Avoid a form submit
return false;
});
} else { //regular file upload
} else { //regular file upload
return $.blueimpUI.fileupload.prototype._onSend.call(that, e, data);
}
}

View File

@ -8,19 +8,20 @@ import page from 'page.js';
* Wrapper for `page.show()` with SilverStripe specific behaviour.
*/
function show(pageShow) {
return (path, state, dispatch, push) => {
// Normalise `path` so that pattern matching is more robust.
// For example if your route is '/pages' it should match when `path` is
// 'http://foo.com/admin/pages', '/pages', and 'pages'.
var el = document.createElement('a');
el.href = path;
path = el.pathname;
if(el.search) {
path += el.search;
}
return pageShow(path, state, dispatch, push);
return (path, state, dispatch, push) => {
// Normalise `path` so that pattern matching is more robust.
// For example if your route is '/pages' it should match when `path` is
// 'http://foo.com/admin/pages', '/pages', and 'pages'.
const el = document.createElement('a');
let pathWithSearch;
el.href = path;
pathWithSearch = el.pathname;
if (el.search) {
pathWithSearch += el.search;
}
return pageShow(pathWithSearch, state, dispatch, push);
};
}
page.show = show(page.show);

View File

@ -17,7 +17,8 @@
"sprites": "gulp sprites",
"test": "NODE_PATH=\"./javascript/src:./admin/javascript/src\" jest",
"coverage": "NODE_PATH=\"./javascript/src:./admin/javascript/src\" jest --coverage",
"thirdparty": "gulp thirdparty"
"thirdparty": "gulp thirdparty",
"lint": "eslint javascript/src & eslint admin/javascript/src"
},
"repository": {
"type": "git",
@ -60,6 +61,9 @@
"babel-preset-react": "^6.5.0",
"babelify": "^7.2.0",
"browserify": "^13.0.0",
"eslint": "^2.5.3",
"eslint-config-airbnb": "^6.2.0",
"eslint-plugin-react": "^4.2.3",
"event-stream": "^3.3.2",
"glob": "^6.0.4",
"gulp": "^3.9.0",