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

View File

@ -15,11 +15,11 @@ $.entwine('ss', function($){
* <ul class="cms-menu-list"> * <ul class="cms-menu-list">
* <li><a href="#">Item 1</a></li> * <li><a href="#">Item 1</a></li>
* <li class="current opened"> * <li class="current opened">
* <a href="#">Item 2</a> * <a href="#">Item 2</a>
* <ul> * <ul>
* <li class="current opened"><a href="#">Item 2.1</a></li> * <li class="current opened"><a href="#">Item 2.1</a></li>
* <li><a href="#">Item 2.2</a></li> * <li><a href="#">Item 2.2</a></li>
* </ul> * </ul>
* </li> * </li>
* </ul> * </ul>
* *
@ -38,7 +38,7 @@ $.entwine('ss', function($){
$(this).addClass('collapse'); $(this).addClass('collapse');
} }
}); });
} else { //collapse } else { //collapse
$(this).children('ul').each(function() { $(this).children('ul').each(function() {
$(this).addClass('collapsed-flyout'); $(this).addClass('collapsed-flyout');
$(this).hasClass('collapse'); $(this).hasClass('collapse');
@ -59,7 +59,7 @@ $.entwine('ss', function($){
//hide all the flyout-indicator //hide all the flyout-indicator
$('.cms-menu-list').find('.child-flyout-indicator').hide(); $('.cms-menu-list').find('.child-flyout-indicator').hide();
} else { //collapse } else { //collapse
//hide the flyout only if it is not the current section //hide the flyout only if it is not the current section
$('.collapsed-flyout').find('li').each(function() { $('.collapsed-flyout').find('li').each(function() {
//if (!$(this).hasClass('current')) //if (!$(this).hasClass('current'))
@ -110,11 +110,11 @@ $.entwine('ss', function($){
* @func getEvaluatedCollapsedState * @func getEvaluatedCollapsedState
* @return {boolean} - Returns true if the menu should be collapsed, false if expanded. * @return {boolean} - Returns true if the menu should be collapsed, false if expanded.
* @desc Evaluate whether the menu should be collapsed. * @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". * 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. * 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. * 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. * 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. * Here we use the manually set state and the automatic behaviour to evaluate what the collapsed state should be.
*/ */
getEvaluatedCollapsedState: function () { getEvaluatedCollapsedState: function () {
var shouldCollapse, var shouldCollapse,
@ -269,7 +269,7 @@ $.entwine('ss', function($){
$('.collapsed-flyout').show(); $('.collapsed-flyout').show();
fly.addClass('opened'); fly.addClass('opened');
fly.children('ul').find('li').fadeIn('fast'); fly.children('ul').find('li').fadeIn('fast');
} else { //collapse } else { //collapse
if(li) { if(li) {
li.remove(); li.remove();
} }

View File

@ -501,17 +501,17 @@ $.entwine('ss.preview', function($){
_loadCurrentPage: function() { _loadCurrentPage: function() {
if (!this.getIsPreviewEnabled()) return; if (!this.getIsPreviewEnabled()) return;
var doc, var doc,
containerEl = $('.cms-container'); containerEl = $('.cms-container');
try { try {
doc = this.find('iframe')[0].contentDocument; doc = this.find('iframe')[0].contentDocument;
} catch (e) { } catch (e) {
// iframe can't be accessed - might be secure? // iframe can't be accessed - might be secure?
console.warn('Unable to access iframe, possible https mis-match'); console.warn('Unable to access iframe, possible https mis-match');
} }
if (!doc) { if (!doc) {
return; return;
} }
// Load this page in the admin interface if appropriate // Load this page in the admin interface if appropriate
var id = $(doc).find('meta[name=x-page-id]').attr('content'); 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. * Prepare the iframe content for preview.
*/ */
_adjustIframeForPreview: function() { _adjustIframeForPreview: function() {
var iframe = this.find('iframe')[0], var iframe = this.find('iframe')[0],
doc; doc;
if(!iframe){ if(!iframe){
return; return;
} }
try { try {
doc = iframe.contentDocument; doc = iframe.contentDocument;
} catch (e) { } catch (e) {
// iframe can't be accessed - might be secure? // iframe can't be accessed - might be secure?
console.warn('Unable to access iframe, possible https mis-match'); console.warn('Unable to access iframe, possible https mis-match');
} }
if(!doc) { if(!doc) {
return; return;
} }
// Open external links in new window to avoid "escaping" the internal page context in the preview // 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. // 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'; import RecordsReducer from 'state/records/reducer';
// Sections // Sections
// eslint-disable-next-line no-unused-vars
import CampaignAdmin from 'sections/campaign-admin/index'; import CampaignAdmin from 'sections/campaign-admin/index';
function appBoot() { function appBoot() {
reducerRegister.add('config', ConfigReducer); reducerRegister.add('config', ConfigReducer);
reducerRegister.add('schemas', SchemaReducer); reducerRegister.add('schemas', SchemaReducer);
reducerRegister.add('records', RecordsReducer); reducerRegister.add('records', RecordsReducer);
const initialState = {}; const initialState = {};
const rootReducer = combineReducers(reducerRegister.getAll()); const rootReducer = combineReducers(reducerRegister.getAll());
const createStoreWithMiddleware = applyMiddleware(thunkMiddleware, createLogger())(createStore); const createStoreWithMiddleware = applyMiddleware(thunkMiddleware, createLogger())(createStore);
// TODO: The store needs to be passed into route callbacks on the route context. // TODO: The store needs to be passed into route callbacks on the route context.
window.store = createStoreWithMiddleware(rootReducer, initialState); window.store = createStoreWithMiddleware(rootReducer, initialState);
// Set the initial config state. // Set the initial config state.
configActions.setConfig(window.ss.config)(window.store.dispatch); 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>. // TODO: This should be using `window.onload` but isn't because
// `window.onload` happens AFTER these Entwine hooks which means the store is undefined when the <Provider> is constructed. // Entwine hooks are being used to set up the <Provider>.
$('body').entwine({ onadd: function () { appBoot(); } }); // `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'; import SilverStripeComponent from 'silverstripe-component';
class FormActionComponent extends SilverStripeComponent { class FormActionComponent extends SilverStripeComponent {
constructor(props) { constructor(props) {
super(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() { // Add icon class
return ( if (typeof this.props.icon !== 'undefined') {
<button type={this.props.type} className={this.getButtonClasses()} onClick={this.handleClick}> buttonClasses += ` font-icon-${this.props.icon}`;
{this.getLoadingIcon()}
{this.props.label}
</button>
);
} }
/** // Add loading class
* Returns the necessary button classes based on the given props if (this.props.loading === true) {
* buttonClasses += ' btn--loading';
* @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 disabled class
* Returns markup for the loading icon if (this.props.disabled === true) {
* buttonClasses += ' disabled';
* @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;
} }
/** return buttonClasses;
* Event handler triggered when a user clicks the button. }
*
* @param object event /**
* @returns null * Returns markup for the loading icon
*/ *
handleClick(event) { * @returns object|null
this.props.handleClick(event); */
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 = { FormActionComponent.propTypes = {
handleClick: React.PropTypes.func.isRequired, handleClick: React.PropTypes.func.isRequired,
label: React.PropTypes.string, label: React.PropTypes.string,
type: React.PropTypes.string, type: React.PropTypes.string,
loading: React.PropTypes.bool, loading: React.PropTypes.bool,
icon: React.PropTypes.string, icon: React.PropTypes.string,
disabled: React.PropTypes.bool, disabled: React.PropTypes.bool,
style: React.PropTypes.string style: React.PropTypes.string,
}; };
FormActionComponent.defaultProps = { FormActionComponent.defaultProps = {
type: 'button', type: 'button',
style: 'secondary' style: 'secondary',
}; };
export default FormActionComponent; export default FormActionComponent;

View File

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import $ from 'jQuery';
import * as schemaActions from 'state/schema/actions'; import * as schemaActions from 'state/schema/actions';
import SilverStripeComponent from 'silverstripe-component'; import SilverStripeComponent from 'silverstripe-component';
import FormComponent from 'components/form/index'; import FormComponent from 'components/form/index';
@ -14,219 +13,217 @@ import es6promise from 'es6-promise';
es6promise.polyfill(); es6promise.polyfill();
// Using this to map field types to components until we implement dependency injection. // 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 registered with the fake DI container.
*/ */
components: { components: {
'TextField': TextField, TextField,
'GridField': GridField, GridField,
'HiddenField': HiddenField HiddenField,
}, },
/** /**
* Gets the component matching the passed component name. * Gets the component matching the passed component name.
* Used when a component type is provided bt the form schema. * Used when a component type is provided bt the form schema.
* *
* @param string componentName - The name of the component to get from the injector. * @param string componentName - The name of the component to get from the injector.
* *
* @return object|null * @return object|null
*/ */
getComponentByName: function (componentName) { getComponentByName(componentName) {
return this.components[componentName]; return this.components[componentName];
}, },
/** /**
* Default data type to component mappings. * Default data type to component mappings.
* Used as a fallback when no component type is provided in the form schema. * 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. * @param string dataType - The data type provided by the form schema.
* *
* @return object|null * @return object|null
*/ */
getComponentByDataType: function (dataType) { getComponentByDataType(dataType) {
switch (dataType) { switch (dataType) {
case 'String': case 'String':
return this.components.TextField; return this.components.TextField;
case 'Hidden': case 'Hidden':
return this.components.HiddenField; return this.components.HiddenField;
case 'Text': case 'Text':
// Textarea field (not implemented) // Textarea field (not implemented)
return null; return null;
case 'HTML': case 'HTML':
// HTML editor field (not implemented) // HTML editor field (not implemented)
return null; return null;
case 'Integer': case 'Integer':
// Numeric field (not implemented) // Numeric field (not implemented)
return null; return null;
case 'Decimal': case 'Decimal':
// Numeric field (not implemented) // Numeric field (not implemented)
return null; return null;
case 'MultiSelect': case 'MultiSelect':
// Radio field (not implemented) // Radio field (not implemented)
return null; return null;
case 'SingleSelect': case 'SingleSelect':
// Dropdown field (not implemented) // Dropdown field (not implemented)
return null; return null;
case 'Date': case 'Date':
// DateTime field (not implemented) // DateTime field (not implemented)
return null; return null;
case 'DateTime': case 'DateTime':
// DateTime field (not implemented) // DateTime field (not implemented)
return null; return null;
case 'Time': case 'Time':
// DateTime field (not implemented) // DateTime field (not implemented)
return null; return null;
case 'Boolean': case 'Boolean':
// Checkbox field (not implemented) // Checkbox field (not implemented)
return null; return null;
case 'Custom': case 'Custom':
return this.components.GridField; return this.components.GridField;
default: default:
return null; return null;
}
} }
} },
};
export class FormBuilderComponent extends SilverStripeComponent { export class FormBuilderComponent extends SilverStripeComponent {
constructor(props) { constructor(props) {
super(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.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;
} }
/** const formProps = {
* Fetches data used to generate a form. This can be form schema and or form state data. actions: schema.schema.actions,
* When the response comes back the data is saved to state. attributes: schema.schema.attributes,
* data: schema.schema.data,
* @param boolean schema - If form schema data should be returned in the response. fields: schema.schema.fields,
* @param boolean state - If form state data should be returned in the response. mapFieldsToComponents: this.mapFieldsToComponents,
* };
* @return object - Promise from the AJAX request.
*/
fetch(schema = true, state = false) {
var headerValues = [];
if (this.isFetching === true) { return <FormComponent {...formProps} />;
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} />
}
} }
FormBuilderComponent.propTypes = { FormBuilderComponent.propTypes = {
actions: React.PropTypes.object.isRequired, actions: React.PropTypes.object.isRequired,
schemaUrl: React.PropTypes.string.isRequired, schemaUrl: React.PropTypes.string.isRequired,
schemas: React.PropTypes.object.isRequired schemas: React.PropTypes.object.isRequired,
}; };
function mapStateToProps(state) { function mapStateToProps(state) {
return { return {
schemas: state.schemas schemas: state.schemas,
} };
} }
function mapDispatchToProps(dispatch) { function mapDispatchToProps(dispatch) {
return { return {
actions: bindActionCreators(schemaActions, dispatch) actions: bindActionCreators(schemaActions, dispatch),
} };
} }
export default connect(mapStateToProps, mapDispatchToProps)(FormBuilderComponent); export default connect(mapStateToProps, mapDispatchToProps)(FormBuilderComponent);

View File

@ -5,22 +5,22 @@ import { FormBuilderComponent } from '../';
describe('FormBuilderComponent', () => { describe('FormBuilderComponent', () => {
describe('getFormSchema()', () => { describe('getFormSchema()', () => {
var formBuilder; var formBuilder;
beforeEach(() => { beforeEach(() => {
const props = { const props = {
store: { store: {
getState: () => {} getState: () => {}
}, },
actions: {}, actions: {},
schemaUrl: 'admin/assets/schema/1', schemaUrl: 'admin/assets/schema/1',
schema: { forms: [{ schema: { id: '1', schema_url: '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 { class FormComponent extends SilverStripeComponent {
/** /**
* Gets the components responsible for perfoming actions on the form. * Gets the components responsible for perfoming actions on the form.
* For example form submission. * For example form submission.
* *
* @return array|null * @return array|null
*/ */
getFormActionComponents() { getFormActionComponents() {
return this.props.actions.map((action) => { return this.props.actions.map((action) =>
return <FormActionComponent {...action} />; <FormActionComponent {...action} />
}); );
} }
render() { render() {
const attr = this.props.attributes; const attr = this.props.attributes;
const fields = this.props.mapFieldsToComponents(this.props.fields); const fields = this.props.mapFieldsToComponents(this.props.fields);
const actions = this.getFormActionComponents(); const actions = this.getFormActionComponents();
return ( return (
<form id={attr.id} className={attr.className} encType={attr.enctype} method={attr.method} action={attr.action}> <form
{fields && id={attr.id}
<fieldset className='form-group'> className={attr.className}
{fields} encType={attr.enctype}
</fieldset> method={attr.method}
} action={attr.action}
>
{fields &&
<fieldset className="form-group">
{fields}
</fieldset>
}
{actions && {actions &&
<div className='actions-fix-btm'> <div className="actions-fix-btm">
<div className='btn-group' role='group'> <div className="btn-group" role="group">
{actions} {actions}
</div> </div>
</div> </div>
} }
</form> </form>
); );
} }
} }
FormComponent.propTypes = { FormComponent.propTypes = {
actions: React.PropTypes.array, actions: React.PropTypes.array,
attributes: React.PropTypes.shape({ attributes: React.PropTypes.shape({
action: React.PropTypes.string.isRequired, action: React.PropTypes.string.isRequired,
'class': React.PropTypes.string.isRequired, class: React.PropTypes.string.isRequired,
enctype: React.PropTypes.string.isRequired, enctype: React.PropTypes.string.isRequired,
id: React.PropTypes.string.isRequired, id: React.PropTypes.string.isRequired,
method: React.PropTypes.string.isRequired method: React.PropTypes.string.isRequired,
}), }),
data: React.PropTypes.array, data: React.PropTypes.array,
fields: React.PropTypes.array.isRequired, fields: React.PropTypes.array.isRequired,
mapFieldsToComponents: React.PropTypes.func.isRequired mapFieldsToComponents: React.PropTypes.func.isRequired,
}; };
export default FormComponent; export default FormComponent;

View File

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

View File

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

View File

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

View File

@ -4,11 +4,11 @@ import GridFieldRowComponent from './row';
class GridFieldHeaderComponent extends SilverStripeComponent { class GridFieldHeaderComponent extends SilverStripeComponent {
render() { render() {
return ( return (
<GridFieldRowComponent>{this.props.children}</GridFieldRowComponent> <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, * The component acts as a container for a grid field,
* with smarts around data retrieval from external sources. * 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) * @todo Replace "dumb" inner components with third party library (e.g. https://griddlegriddle.github.io)
*/ */
class GridField extends SilverStripeComponent { class GridField extends SilverStripeComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.deleteRecord = this.deleteRecord.bind(this); this.deleteRecord = this.deleteRecord.bind(this);
this.editRecord = this.editRecord.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() { const columns = this.props.data.columns;
super.componentDidMount();
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 rowActions = (
const records = this.props.records; <GridFieldCell key={`${i}-actions`}>
if(!records) { <GridFieldAction
return <div></div>; 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 return (
const actionPlaceholder = <GridFieldCell key={'actionPlaceholder'} />; <GridFieldTable header={header} rows={rows} />
const headerCells = columns.map((column, i) => <GridFieldHeaderCell key={i} >{column.name}</GridFieldHeaderCell>); );
const header = <GridFieldHeader>{headerCells.concat(actionPlaceholder)}</GridFieldHeader>; }
const rows = records.map((record, i) => { /**
var cells = columns.map((column, i) => { * @param number int
// Get value by dot notation * @param event
var val = column.field.split('.').reduce((a, b) => a[b], record) */
return <GridFieldCell key={i}>{val}</GridFieldCell> 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'}> editRecord(event) {
<GridFieldAction event.preventDefault();
icon={'cog'} // TODO
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
}
} }
GridField.propTypes = { GridField.propTypes = {
data: React.PropTypes.shape({ data: React.PropTypes.shape({
recordType: React.PropTypes.string.isRequired, recordType: React.PropTypes.string.isRequired,
headerColumns: React.PropTypes.array, headerColumns: React.PropTypes.array,
collectionReadEndpoint: React.PropTypes.object collectionReadEndpoint: React.PropTypes.object,
}) }),
}; };
function mapStateToProps(state, ownProps) { function mapStateToProps(state, ownProps) {
let recordType = ownProps.data ? ownProps.data.recordType : null; const recordType = ownProps.data ? ownProps.data.recordType : null;
return { return {
records: (state.records && recordType) ? state.records[recordType] : [] records: (state.records && recordType) ? state.records[recordType] : [],
} };
} }
function mapDispatchToProps(dispatch) { function mapDispatchToProps(dispatch) {
return { return {
actions: bindActionCreators(actions, dispatch) actions: bindActionCreators(actions, dispatch),
} };
} }
export default connect(mapStateToProps, mapDispatchToProps)(GridField); export default connect(mapStateToProps, mapDispatchToProps)(GridField);

View File

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

View File

@ -2,66 +2,66 @@ jest.dontMock('../index');
jest.dontMock('../table'); jest.dontMock('../table');
const React = require('react'), const React = require('react'),
ReactTestUtils = require('react-addons-test-utils'), ReactTestUtils = require('react-addons-test-utils'),
GridFieldTableComponent = require('../table.js').default; GridFieldTableComponent = require('../table.js').default;
describe('GridFieldTableComponent', () => { describe('GridFieldTableComponent', () => {
var props; var props;
beforeEach(function () { beforeEach(function () {
props = { 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 () { it('should generate and return a header from props.data if it is set', 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');
});
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 () { it('should return null if props.header and props.data are both not set', function () {
var gridfield; gridfield = ReactTestUtils.renderIntoDocument(
<GridFieldTableComponent {...props} />
);
it('should return props.rows if it is set', function () { expect(gridfield.generateHeader()).toBe(null);
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);
});
}); });
});
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 { class HiddenFieldComponent extends SilverStripeComponent {
constructor(props) { constructor(props) {
super(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() { this.props.onChange();
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();
}
} }
HiddenFieldComponent.propTypes = { HiddenFieldComponent.propTypes = {
label: React.PropTypes.string, label: React.PropTypes.string,
extraClass: React.PropTypes.string, extraClass: React.PropTypes.string,
name: React.PropTypes.string.isRequired, name: React.PropTypes.string.isRequired,
onChange: React.PropTypes.func, onChange: React.PropTypes.func,
value: React.PropTypes.string value: React.PropTypes.string,
}; };
export default HiddenFieldComponent; export default HiddenFieldComponent;

View File

@ -3,37 +3,39 @@ import SilverStripeComponent from 'silverstripe-component';
class NorthHeaderBreadcrumbsComponent extends SilverStripeComponent { class NorthHeaderBreadcrumbsComponent extends SilverStripeComponent {
render() { render() {
return ( return (
<div className="cms-content-header-info"> <div className="cms-content-header-info">
<div className="breadcrumbs-wrapper"> <div className="breadcrumbs-wrapper">
<h2 id="page-title-heading"> <h2 id="page-title-heading">
{this.getBreadcrumbs()} {this.getBreadcrumbs()}
</h2> </h2>
</div> </div>
</div> </div>
); );
}
getBreadcrumbs() {
if (typeof this.props.crumbs === 'undefined') {
return null;
} }
getBreadcrumbs() { const breadcrumbs = this.props.crumbs.map((crumb, index, crumbs) => {
if (typeof this.props.crumbs === 'undefined') { let component;
return null; // 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) => { return breadcrumbs;
// 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;
}
} }

View File

@ -1,40 +1,40 @@
jest.dontMock('../index'); jest.dontMock('../index');
const React = require('react'), const React = require('react'),
ReactTestUtils = require('react-addons-test-utils'), ReactTestUtils = require('react-addons-test-utils'),
NorthHeaderBreadcrumbsComponent = require('../index').default; NorthHeaderBreadcrumbsComponent = require('../index').default;
describe('NorthHeaderBreadcrumbsComponent', () => { describe('NorthHeaderBreadcrumbsComponent', () => {
var props; var props;
beforeEach(() => { beforeEach(() => {
props = {}; 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()', () => { it('should return null if props.crumbs is not set', () => {
var northHeaderBreadcrumbs; northHeaderBreadcrumbs = ReactTestUtils.renderIntoDocument(
<NorthHeaderBreadcrumbsComponent {...props} />
);
it('should convert the props.crumbs array into jsx to be rendered', () => { expect(northHeaderBreadcrumbs.getBreadcrumbs()).toBe(null);
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);
});
}); });
});
}); });

View File

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

View File

@ -3,53 +3,53 @@ import SilverStripeComponent from 'silverstripe-component';
class TextFieldComponent extends SilverStripeComponent { class TextFieldComponent extends SilverStripeComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.handleChange = this.handleChange.bind(this); this.handleChange = this.handleChange.bind(this);
} }
render() { render() {
return ( return (
<div className='field text'> <div className="field text">
{this.props.label && {this.props.label &&
<label className='left' htmlFor={'gallery_' + this.props.name}> <label className="left" htmlFor={`gallery_${this.props.name}`}>
{this.props.label} {this.props.label}
</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;
} }
<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 = { TextFieldComponent.propTypes = {
label: React.PropTypes.string, label: React.PropTypes.string,
extraClass: React.PropTypes.string, extraClass: React.PropTypes.string,
name: React.PropTypes.string.isRequired, name: React.PropTypes.string.isRequired,
onChange: React.PropTypes.func, onChange: React.PropTypes.func,
value: React.PropTypes.string value: React.PropTypes.string,
}; };
export default TextFieldComponent; export default TextFieldComponent;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,24 +1,22 @@
import reducerRegister from 'reducer-register';
import $ from 'jQuery'; import $ from 'jQuery';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import CampaignAdmin from './controller'; import CampaignAdmin from './controller';
$.entwine('ss', function ($) { // eslint-disable-next-line no-shadow
$.entwine('ss', ($) => {
$('.cms-content.CampaignAdmin').entwine({ $('.cms-content.CampaignAdmin').entwine({
onadd: function () { onadd() {
ReactDOM.render( ReactDOM.render(
<Provider store={window.store}> <Provider store={window.store}>
<CampaignAdmin sectionConfigKey='CampaignAdmin' /> <CampaignAdmin sectionConfigKey="CampaignAdmin" />
</Provider> </Provider>
, this[0]); , this[0]);
}, },
onremove: function () {
ReactDOM.unmountComponentAtNode(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 * @see https://github.com/github/fetch#handling-http-error-statuses
*/ */
function checkStatus(response) { function checkStatus(response) {
let ret;
let error;
if (response.status >= 200 && response.status < 300) { if (response.status >= 200 && response.status < 300) {
return response ret = response;
} else { } else {
var error = new Error(response.statusText) error = new Error(response.statusText);
error.response = response error.response = response;
throw error throw error;
} }
return ret;
} }
class SilverStripeBackend { class SilverStripeBackend {
constructor() { constructor() {
// Allow mocking // Allow mocking
this.fetch = fetch; this.fetch = fetch;
} }
/** /**
* Makes a network request using the GET HTTP verb. * Makes a network request using the GET HTTP verb.
* *
* @param string url - Endpoint URL. * @param string url - Endpoint URL.
* @return object - Promise * @return object - Promise
*/ */
get(url) { get(url) {
return this.fetch(url, { method: 'get', credentials: 'same-origin' }) return this.fetch(url, { method: 'get', credentials: 'same-origin' })
.then(checkStatus); .then(checkStatus);
} }
/** /**
* Makes a network request using the POST HTTP verb. * Makes a network request using the POST HTTP verb.
* *
* @param string url - Endpoint URL. * @param string url - Endpoint URL.
* @param object data - Data to send with the request. * @param object data - Data to send with the request.
* @return object - Promise * @return object - Promise
*/ */
post(url, data) { post(url, data) {
return this.fetch(url, { method: 'post', credentials: 'same-origin', body: data }) return this.fetch(url, { method: 'post', credentials: 'same-origin', body: data })
.then(checkStatus); .then(checkStatus);
} }
/** /**
* Makes a newtwork request using the PUT HTTP verb. * Makes a newtwork request using the PUT HTTP verb.
* *
* @param string url - Endpoint URL. * @param string url - Endpoint URL.
* @param object data - Data to send with the request. * @param object data - Data to send with the request.
* @return object - Promise * @return object - Promise
*/ */
put(url, data) { put(url, data) {
return this.fetch(url, { method: 'put', credentials: 'same-origin', body: data }) return this.fetch(url, { method: 'put', credentials: 'same-origin', body: data })
.then(checkStatus); .then(checkStatus);
} }
/** /**
* Makes a newtwork request using the DELETE HTTP verb. * Makes a newtwork request using the DELETE HTTP verb.
* *
* @param string url - Endpoint URL. * @param string url - Endpoint URL.
* @param object data - Data to send with the request. * @param object data - Data to send with the request.
* @return object - Promise * @return object - Promise
*/ */
delete(url, data) { delete(url, data) {
return this.fetch(url, { method: 'delete', credentials: 'same-origin', body: data }) return this.fetch(url, { method: 'delete', credentials: 'same-origin', body: data })
.then(checkStatus); .then(checkStatus);
} }
} }
// Exported as a singleton so we can implement things like // Exported as a singleton so we can implement things like
// global caching and request batching at some stage. // global caching and request batching at some stage.
let backend = new SilverStripeBackend(); const backend = new SilverStripeBackend();
export default backend; export default backend;

View File

@ -2,54 +2,107 @@
* @file Base component which all SilverStripe ReactJS components should extend from. * @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'; import $ from '../../../javascript/src/jQuery';
class SilverStripeComponent extends Component { class SilverStripeComponent extends Component {
/** constructor(props) {
* @func componentDidMount super(props);
* @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;
}
// Save some props for later. When we come to unbind these listeners // Setup component routing.
// there's no guarantee these props will be the same or even present. if (typeof this.props.route !== 'undefined') {
this.cmsEvents = this.props.cmsEvents; // 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) { this.render = () => {
$(document).on(cmsEvent, this.cmsEvents[cmsEvent].bind(this)); let component = null;
}
}
/** if (this.isComponentRoute()) {
* @func componentWillUnmount component = this._render();
* @desc Unbind the event listeners we added in componentDidMount. }
*/
componentWillUnmount() {
for (let cmsEvent in this.cmsEvents) {
$(document).off(cmsEvent);
}
}
/** return component;
* @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. window.ss.router(this.props.route, (ctx, next) => {
* @desc Notifies legacy-land something has changed within our component. this.handleEnterRoute(ctx, next);
*/ });
emitCmsEvent(componentEvent, data) { window.ss.router.exit(this.props.route, (ctx, next) => {
$(document).trigger(componentEvent, data); 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 = { SilverStripeComponent.propTypes = {
'cmsEvents': React.PropTypes.object cmsEvents: React.PropTypes.object,
route: React.PropTypes.string,
}; };
export default SilverStripeComponent; 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() // 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: // 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 // [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 // [1]: http://jblas:password@mycompany.com:8080/mail/inbox?msg=1234&type=unread
// [2]: http://jblas:password@mycompany.com:8080/mail/inbox // [2]: http://jblas:password@mycompany.com:8080/mail/inbox
// [3]: http://jblas:password@mycompany.com:8080 // [3]: http://jblas:password@mycompany.com:8080
// [4]: http: // [4]: http:
// [5]: // // [5]: //
// [6]: jblas:password@mycompany.com:8080 // [6]: jblas:password@mycompany.com:8080
// [7]: jblas:password // [7]: jblas:password
// [8]: jblas // [8]: jblas
// [9]: password // [9]: password
// [10]: mycompany.com:8080 // [10]: mycompany.com:8080
// [11]: mycompany.com // [11]: mycompany.com
// [12]: 8080 // [12]: 8080
// [13]: /mail/inbox // [13]: /mail/inbox
// [14]: /mail/ // [14]: /mail/
// [15]: inbox // [15]: inbox
// [16]: ?msg=1234&type=unread // [16]: ?msg=1234&type=unread
// [17]: #msg-content // [17]: #msg-content
// //
urlParseRE: /^(((([^:\/#\?]+:)?(?:(\/\/)((?:(([^:@\/#\?]+)(?:\:([^:@\/#\?]+))?)@)?(([^:\/#\?\]\[]+|\[[^\/\]@#?]+\])(?:\:([0-9]+))?))?)?)?((\/?(?:[^\/\?#]+\/+)*)([^\?#]*)))?(\?[^#]+)?)(#.*)?/, urlParseRE: /^(((([^:\/#\?]+:)?(?:(\/\/)((?:(([^:@\/#\?]+)(?:\:([^:@\/#\?]+))?)@)?(([^:\/#\?\]\[]+|\[[^\/\]@#?]+\])(?:\:([0-9]+))?))?)?)?((\/?(?:[^\/\?#]+\/+)*)([^\?#]*)))?(\?[^#]+)?)(#.*)?/,
@ -72,23 +72,23 @@ var $window = $( window ),
// like all other browsers do, so we normalize everything so its consistent // like all other browsers do, so we normalize everything so its consistent
// no matter what browser we're running on. // no matter what browser we're running on.
return { return {
href: matches[ 0 ] || "", href: matches[ 0 ] || "",
hrefNoHash: matches[ 1 ] || "", hrefNoHash: matches[ 1 ] || "",
hrefNoSearch: matches[ 2 ] || "", hrefNoSearch: matches[ 2 ] || "",
domain: matches[ 3 ] || "", domain: matches[ 3 ] || "",
protocol: matches[ 4 ] || "", protocol: matches[ 4 ] || "",
doubleSlash: matches[ 5 ] || "", doubleSlash: matches[ 5 ] || "",
authority: matches[ 6 ] || "", authority: matches[ 6 ] || "",
username: matches[ 8 ] || "", username: matches[ 8 ] || "",
password: matches[ 9 ] || "", password: matches[ 9 ] || "",
host: matches[ 10 ] || "", host: matches[ 10 ] || "",
hostname: matches[ 11 ] || "", hostname: matches[ 11 ] || "",
port: matches[ 12 ] || "", port: matches[ 12 ] || "",
pathname: matches[ 13 ] || "", pathname: matches[ 13 ] || "",
directory: matches[ 14 ] || "", directory: matches[ 14 ] || "",
filename: matches[ 15 ] || "", filename: matches[ 15 ] || "",
search: matches[ 16 ] || "", search: matches[ 16 ] || "",
hash: matches[ 17 ] || "" hash: matches[ 17 ] || ""
}; };
}, },

View File

@ -1,3 +1,3 @@
export default { 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 * @param object config
*/ */
export function setConfig(config) { export function setConfig(config) {
return (dispatch, getState) => { return (dispatch) =>
return dispatch({ dispatch({
type: ACTION_TYPES.SET_CONFIG, type: ACTION_TYPES.SET_CONFIG,
payload: { config } payload: { config },
}); });
}
} }

View File

@ -2,17 +2,15 @@ import deepFreeze from 'deep-freeze';
import ACTION_TYPES from './action-types'; import ACTION_TYPES from './action-types';
function configReducer(state = {}, action) { 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: default:
return deepFreeze(Object.assign({}, state, action.payload.config)); return state;
default:
return state;
}
}
} }
export default configReducer; export default configReducer;

View File

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

View File

@ -1,5 +1,4 @@
import ACTION_TYPES from './action-types'; import ACTION_TYPES from './action-types';
import fetch from 'isomorphic-fetch';
import backend from 'silverstripe-backend.js'; import backend from 'silverstripe-backend.js';
/** /**
@ -14,8 +13,8 @@ import backend from 'silverstripe-backend.js';
* @return string * @return string
*/ */
function populate(str, params) { function populate(str, params) {
let names = ['id']; const names = ['id'];
return names.reduce((str, name) => str.replace(`:${name}`, params[name]), str); return names.reduce((acc, name) => acc.replace(`:${name}`, params[name]), str);
} }
/** /**
@ -26,19 +25,27 @@ function populate(str, params) {
* @param string url API endpoint * @param string url API endpoint
*/ */
export function fetchRecords(recordType, method, url) { export function fetchRecords(recordType, method, url) {
let payload = {recordType: recordType}; const payload = { recordType };
url = populate(url, payload); return (dispatch) => {
return (dispatch, getState) => { dispatch({
dispatch ({type: ACTION_TYPES.FETCH_RECORDS_REQUEST, payload: payload}); type: ACTION_TYPES.FETCH_RECORDS_REQUEST,
return backend[method.toLowerCase()](url) payload,
.then(response => response.json()) });
.then(json => { return backend[method.toLowerCase()](populate(url, payload))
dispatch({type: ACTION_TYPES.FETCH_RECORDS_SUCCESS, payload: {recordType: recordType, data: json}}) .then(response => response.json())
}) .then(json => {
.catch((err) => { dispatch({
dispatch({type: ACTION_TYPES.FETCH_RECORDS_FAILURE, payload: {error: err, recordType: recordType}}) 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 * @param string url API endpoint
*/ */
export function deleteRecord(recordType, id, method, url) { export function deleteRecord(recordType, id, method, url) {
let payload = {recordType: recordType, id: id}; const payload = { recordType, id };
url = populate(url, payload); return (dispatch) => {
return (dispatch, getState) => { dispatch({
dispatch ({type: ACTION_TYPES.DELETE_RECORD_REQUEST, payload: payload}); type: ACTION_TYPES.DELETE_RECORD_REQUEST,
return backend[method.toLowerCase()](url) payload,
.then(json => { });
dispatch({type: ACTION_TYPES.DELETE_RECORD_SUCCESS, payload: {recordType: recordType, id: id}}) return backend[method.toLowerCase()](populate(url, payload))
}) .then(() => {
.catch((err) => { dispatch({
dispatch({type: ACTION_TYPES.DELETE_RECORD_FAILURE, payload: {error: err, recordType: recordType, id: id}}) 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) { function recordsReducer(state = initialState, action) {
let records; let records;
let recordType; let recordType;
switch (action.type) { switch (action.type) {
case ACTION_TYPES.CREATE_RECORD: case ACTION_TYPES.CREATE_RECORD:
return deepFreeze(Object.assign({}, state, { return deepFreeze(Object.assign({}, state, {
})); }));
case ACTION_TYPES.UPDATE_RECORD: case ACTION_TYPES.UPDATE_RECORD:
return deepFreeze(Object.assign({}, state, { return deepFreeze(Object.assign({}, state, {
})); }));
case ACTION_TYPES.DELETE_RECORD: case ACTION_TYPES.DELETE_RECORD:
return deepFreeze(Object.assign({}, state, { return deepFreeze(Object.assign({}, state, {
})); }));
case ACTION_TYPES.FETCH_RECORDS_REQUEST: case ACTION_TYPES.FETCH_RECORDS_REQUEST:
return state; return state;
case ACTION_TYPES.FETCH_RECORDS_FAILURE: case ACTION_TYPES.FETCH_RECORDS_FAILURE:
return state; return state;
case ACTION_TYPES.FETCH_RECORDS_SUCCESS: case ACTION_TYPES.FETCH_RECORDS_SUCCESS:
recordType = action.payload.recordType; recordType = action.payload.recordType;
// TODO Automatic pluralisation from recordType // TODO Automatic pluralisation from recordType
records = action.payload.data._embedded[recordType + 's']; records = action.payload.data._embedded[`${recordType}s`];
return deepFreeze(Object.assign({}, state, { return deepFreeze(Object.assign({}, state, {
[recordType]: records [recordType]: records,
})); }));
case ACTION_TYPES.DELETE_RECORD_REQUEST: case ACTION_TYPES.DELETE_RECORD_REQUEST:
return state; return state;
case ACTION_TYPES.DELETE_RECORD_FAILURE: case ACTION_TYPES.DELETE_RECORD_FAILURE:
return state; return state;
case ACTION_TYPES.DELETE_RECORD_SUCCESS: case ACTION_TYPES.DELETE_RECORD_SUCCESS:
recordType = action.payload.recordType; recordType = action.payload.recordType;
records = state[recordType] records = state[recordType]
.filter(record => record.ID != action.payload.id) .filter(record => record.ID !== action.payload.id);
return deepFreeze(Object.assign({}, state, { return deepFreeze(Object.assign({}, state, {
[recordType]: records [recordType]: records,
})); }));
default:
return state;
}
default:
return state;
}
} }
export default recordsReducer; export default recordsReducer;

View File

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

View File

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

View File

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

View File

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

View File

@ -7,18 +7,18 @@ import ACTION_TYPES from '../action-types';
describe('schemaReducer', () => { describe('schemaReducer', () => {
describe('SET_SCHEMA', () => { describe('SET_SCHEMA', () => {
it('should create a new form', () => { it('should create a new form', () => {
const initialState = { }; const initialState = { };
const serverResponse = { id: 'TestForm', schema_url: 'URL' }; const serverResponse = { id: 'TestForm', schema_url: 'URL' };
const nextState = schemaReducer(initialState, { const nextState = schemaReducer(initialState, {
type: ACTION_TYPES.SET_SCHEMA, type: ACTION_TYPES.SET_SCHEMA,
payload: { schema: serverResponse } 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', () => { describe('ReducerRegister', () => {
var reducer = () => null; var reducer = () => null;
it('should add a reducer to the register', () => { it('should add a reducer to the register', () => {
expect(reducerRegister.getAll().test).toBe(undefined); expect(reducerRegister.getAll().test).toBe(undefined);
reducerRegister.add('test', reducer); reducerRegister.add('test', reducer);
expect(reducerRegister.getAll().test).toBe(reducer); expect(reducerRegister.getAll().test).toBe(reducer);
reducerRegister.remove('test'); reducerRegister.remove('test');
}); });
it('should remove a reducer from the register', () => { it('should remove a reducer from the register', () => {
reducerRegister.add('test', reducer); reducerRegister.add('test', reducer);
expect(reducerRegister.getAll().test).toBe(reducer); expect(reducerRegister.getAll().test).toBe(reducer);
reducerRegister.remove('test'); reducerRegister.remove('test');
expect(reducerRegister.getAll().test).toBe(undefined); expect(reducerRegister.getAll().test).toBe(undefined);
}); });
it('should get all reducers from the register', () => { it('should get all reducers from the register', () => {
reducerRegister.add('test1', reducer); reducerRegister.add('test1', reducer);
reducerRegister.add('test2', reducer); reducerRegister.add('test2', reducer);
expect(reducerRegister.getAll().test1).toBe(reducer); expect(reducerRegister.getAll().test1).toBe(reducer);
expect(reducerRegister.getAll().test2).toBe(reducer); expect(reducerRegister.getAll().test2).toBe(reducer);
reducerRegister.remove('test1'); reducerRegister.remove('test1');
reducerRegister.remove('test2'); reducerRegister.remove('test2');
}); });
it('should get a single reducer from the register', () => { it('should get a single reducer from the register', () => {
reducerRegister.add('test', reducer); reducerRegister.add('test', reducer);
expect(reducerRegister.getByKey('test')).toBe(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'; import backend from '../silverstripe-backend';
var getFetchMock = function(data) { var getFetchMock = function(data) {
let mock = jest.genMockFunction(); let mock = jest.genMockFunction();
let promise = new Promise((resolve, reject) => { let promise = new Promise((resolve, reject) => {
process.nextTick(() => resolve(data)); process.nextTick(() => resolve(data));
}); });
mock.mockReturnValue(promise); mock.mockReturnValue(promise);
return mock; return mock;
}; };
describe('SilverStripeBackend', () => { describe('SilverStripeBackend', () => {
beforeAll(() => { beforeAll(() => {
let fetchMock = getFetchMock(); let fetchMock = getFetchMock();
backend.fetch = fetchMock; 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 send a GET request to an endpoint', () => {
backend.get('http://example.com');
it('should return a promise', () => { expect(backend.fetch).toBeCalledWith(
var promise = backend.get('http://example.com'); 'http://example.com',
expect(typeof promise).toBe('object'); {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', () => { describe('post()', () => {
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'}
);
});
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', () => { backend.post('http://example.com', postData);
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'}
);
});
expect(backend.fetch).toBeCalledWith(
'http://example.com',
{method: 'post', body: postData, credentials: 'same-origin'}
);
}); });
describe('delete()', () => { });
it('should return a promise', () => { describe('put()', () => {
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'}
);
});
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'), const packageJson = require('./package.json');
autoprefixer = require('autoprefixer'), const autoprefixer = require('autoprefixer');
babelify = require('babelify'), const babelify = require('babelify'); // eslint-disable-line no-unused-vars
browserify = require('browserify'), const browserify = require('browserify');
eventStream = require('event-stream'), const eventStream = require('event-stream');
glob = require('glob'), const glob = require('glob');
gulp = require('gulp'), const gulp = require('gulp');
babel = require('gulp-babel'), const babel = require('gulp-babel');
diff = require('gulp-diff'), const diff = require('gulp-diff');
gulpif = require('gulp-if'), const gulpif = require('gulp-if');
notify = require('gulp-notify'), const notify = require('gulp-notify');
postcss = require('gulp-postcss'), const postcss = require('gulp-postcss');
sass = require('gulp-sass'), const sass = require('gulp-sass');
sourcemaps = require('gulp-sourcemaps'), const sourcemaps = require('gulp-sourcemaps');
uglify = require('gulp-uglify'), const uglify = require('gulp-uglify');
gulpUtil = require('gulp-util'), const gulpUtil = require('gulp-util');
path = require('path'), const path = require('path');
source = require('vinyl-source-stream'), const source = require('vinyl-source-stream');
buffer = require('vinyl-buffer'), const buffer = require('vinyl-buffer');
semver = require('semver'), const semver = require('semver');
sprity = require('sprity'), const sprity = require('sprity');
watchify = require('watchify'); const watchify = require('watchify');
var isDev = typeof process.env.npm_config_development !== 'undefined'; const isDev = typeof process.env.npm_config_development !== 'undefined';
var PATHS = { process.env.NODE_ENV = isDev ? 'development' : 'production';
MODULES: './node_modules',
ADMIN: './admin', const PATHS = {
ADMIN_IMAGES: './admin/images', MODULES: './node_modules',
ADMIN_SCSS: './admin/scss', ADMIN: './admin',
ADMIN_THIRDPARTY: './admin/thirdparty', ADMIN_IMAGES: './admin/images',
ADMIN_JAVASCRIPT_SRC: './admin/javascript/src', ADMIN_SCSS: './admin/scss',
ADMIN_JAVASCRIPT_DIST: './admin/javascript/dist', ADMIN_THIRDPARTY: './admin/thirdparty',
FRAMEWORK: '.', ADMIN_JAVASCRIPT_SRC: './admin/javascript/src',
FRAMEWORK_THIRDPARTY: './thirdparty', ADMIN_JAVASCRIPT_DIST: './admin/javascript/dist',
FRAMEWORK_DEV_INSTALL: './dev/install', FRAMEWORK: '.',
FRAMEWORK_JAVASCRIPT_SRC: './javascript/src', FRAMEWORK_THIRDPARTY: './thirdparty',
FRAMEWORK_JAVASCRIPT_DIST: './javascript/dist' 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 // 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 = { const browserifyOptions = {
debug: true, debug: true,
paths: [PATHS.ADMIN_JAVASCRIPT_SRC, PATHS.FRAMEWORK_JAVASCRIPT_SRC] paths: [PATHS.ADMIN_JAVASCRIPT_SRC, PATHS.FRAMEWORK_JAVASCRIPT_SRC],
}; };
var babelifyOptions = { const babelifyOptions = {
presets: ['es2015', 'react'], presets: ['es2015', 'react'],
ignore: /(node_modules|thirdparty)/, ignore: /(node_modules|thirdparty)/,
comments: false comments: false,
}; };
// Used for autoprefixing css properties (same as Bootstrap Aplha.2 defaults) // Used for autoprefixing css properties (same as Bootstrap Aplha.2 defaults)
var supportedBrowsers = [ const supportedBrowsers = [
'Chrome >= 35', 'Chrome >= 35',
'Firefox >= 31', 'Firefox >= 31',
'Edge >= 12', 'Edge >= 12',
'Explorer >= 9', 'Explorer >= 9',
'iOS >= 8', 'iOS >= 8',
'Safari >= 8', 'Safari >= 8',
'Android 2.3', 'Android 2.3',
'Android >= 4', 'Android >= 4',
'Opera >= 12' 'Opera >= 12',
]; ];
var blueimpFileUploadConfig = { const blueimpFileUploadConfig = {
src: PATHS.MODULES + '/blueimp-file-upload', src: `${PATHS.MODULES}/blueimp-file-upload`,
dest: PATHS.FRAMEWORK_THIRDPARTY + '/jquery-fileupload', dest: PATHS.FRAMEWORK_THIRDPARTY + '/jquery-fileupload',
files: [ files: [
'/cors/jquery.postmessage-transport.js', '/cors/jquery.postmessage-transport.js',
'/cors/jquery.xdr-transport.js', '/cors/jquery.xdr-transport.js',
'/jquery.fileupload-ui.js', '/jquery.fileupload-ui.js',
'/jquery.fileupload.js', '/jquery.fileupload.js',
'/jquery.iframe-transport.js' '/jquery.iframe-transport.js'
] ]
}; };
var blueimpLoadImageConfig = { var blueimpLoadImageConfig = {
src: PATHS.MODULES + '/blueimp-load-image', src: PATHS.MODULES + '/blueimp-load-image',
dest: PATHS.FRAMEWORK_THIRDPARTY + '/javascript-loadimage', dest: PATHS.FRAMEWORK_THIRDPARTY + '/javascript-loadimage',
files: ['/load-image.js'] files: ['/load-image.js']
}; };
var blueimpTmplConfig = { var blueimpTmplConfig = {
src: PATHS.MODULES + '/blueimp-tmpl', src: PATHS.MODULES + '/blueimp-tmpl',
dest: PATHS.FRAMEWORK_THIRDPARTY + '/javascript-templates', dest: PATHS.FRAMEWORK_THIRDPARTY + '/javascript-templates',
files: ['/tmpl.js'] files: ['/tmpl.js']
}; };
var jquerySizesConfig = { var jquerySizesConfig = {
src: PATHS.MODULES + '/jquery-sizes', src: PATHS.MODULES + '/jquery-sizes',
dest: PATHS.ADMIN_THIRDPARTY + '/jsizes', dest: PATHS.ADMIN_THIRDPARTY + '/jsizes',
files: ['/lib/jquery.sizes.js'] files: ['/lib/jquery.sizes.js']
}; };
var tinymceConfig = { var tinymceConfig = {
src: PATHS.MODULES + '/tinymce', src: PATHS.MODULES + '/tinymce',
dest: PATHS.FRAMEWORK_THIRDPARTY + '/tinymce', dest: PATHS.FRAMEWORK_THIRDPARTY + '/tinymce',
files: [ files: [
'/tinymce.min.js', // Exclude unminified file to keep repository size down '/tinymce.min.js', // Exclude unminified file to keep repository size down
'/jquery.tinymce.min.js', '/jquery.tinymce.min.js',
'/themes/**', '/themes/**',
'/skins/**', '/skins/**',
'/plugins/**' '/plugins/**'
] ]
}; };
/** /**
@ -116,12 +118,12 @@ var tinymceConfig = {
* @param array libConfig.files - The list of files to copy from the source to the destination directory * @param array libConfig.files - The list of files to copy from the source to the destination directory
*/ */
function copyFiles(libConfig) { function copyFiles(libConfig) {
libConfig.files.forEach(function (file) { libConfig.files.forEach(function (file) {
var dir = path.parse(file).dir; var dir = path.parse(file).dir;
gulp.src(libConfig.src + file) gulp.src(libConfig.src + file)
.pipe(gulp.dest(libConfig.dest + dir)); .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 * @param array libConfig.files - The list of files to copy from the source to the destination directory
*/ */
function diffFiles(libConfig) { function diffFiles(libConfig) {
libConfig.files.forEach(function (file) { libConfig.files.forEach(function (file) {
var dir = path.parse(file).dir; var dir = path.parse(file).dir;
gulp.src(libConfig.src + file) gulp.src(libConfig.src + file)
.pipe(diff(libConfig.dest + dir)) .pipe(diff(libConfig.dest + dir))
.pipe(diff.reporter({ fail: true, quiet: true })) .pipe(diff.reporter({ fail: true, quiet: true }))
.on('error', function (error) { .on('error', function (error) {
console.error(new Error('Sanity check failed. \'' + libConfig.dest + file + '\' has been modified.')); console.error(new Error('Sanity check failed. \'' + libConfig.dest + file + '\' has been modified.'));
process.exit(1); process.exit(1);
}); });
}); });
} }
/** /**
@ -154,31 +156,31 @@ function diffFiles(libConfig) {
* @return object * @return object
*/ */
function transformToUmd(files, dest) { function transformToUmd(files, dest) {
return eventStream.merge(files.map(function (file) { return eventStream.merge(files.map(function (file) {
return gulp.src(file) return gulp.src(file)
.pipe(babel({ .pipe(babel({
presets: ['es2015'], presets: ['es2015'],
moduleId: 'ss.' + path.parse(file).name, moduleId: 'ss.' + path.parse(file).name,
plugins: ['transform-es2015-modules-umd'], plugins: ['transform-es2015-modules-umd'],
comments: false comments: false
})) }))
.on('error', notify.onError({ .on('error', notify.onError({
message: 'Error: <%= error.message %>', message: 'Error: <%= error.message %>',
})) }))
.pipe(gulp.dest(dest)); .pipe(gulp.dest(dest));
})); }));
} }
// Make sure the version of Node being used is valid. // Make sure the version of Node being used is valid.
if (!semver.satisfies(process.versions.node, packageJson.engines.node)) { 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'); 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); process.exit(1);
} }
if (isDev) { if (isDev) {
browserifyOptions.cache = {}; browserifyOptions.cache = {};
browserifyOptions.packageCache = {}; browserifyOptions.packageCache = {};
browserifyOptions.plugin = [watchify]; browserifyOptions.plugin = [watchify];
} }
gulp.task('build', ['umd', 'bundle']); 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', ['bundle-lib', 'bundle-legacy', 'bundle-framework']);
gulp.task('bundle-lib', function bundleLib() { 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' })) return browserify(Object.assign({}, browserifyOptions, { entries: PATHS.ADMIN_JAVASCRIPT_SRC + '/bundles/lib.js' }))
.on('update', bundleLib) .on('update', bundleLib)
.on('log', function (msg) { gulpUtil.log('Finished', 'bundled ' + bundleFileName + ' ' + msg) }) .on('log', function (msg) { gulpUtil.log('Finished', 'bundled ' + bundleFileName + ' ' + msg) })
.transform('babelify', babelifyOptions) .transform('babelify', babelifyOptions)
.require('deep-freeze', { expose: 'deep-freeze' }) .require('deep-freeze', { expose: 'deep-freeze' })
.require('react', { expose: 'react' }) .require('react', { expose: 'react' })
.require('react-addons-css-transition-group', { expose: 'react-addons-css-transition-group' }) .require('react-addons-css-transition-group', { expose: 'react-addons-css-transition-group' })
.require('react-addons-test-utils', { expose: 'react-addons-test-utils' }) .require('react-addons-test-utils', { expose: 'react-addons-test-utils' })
.require('react-dom', { expose: 'react-dom' }) .require('react-dom', { expose: 'react-dom' })
.require('react-redux', { expose: 'react-redux' }) .require('react-redux', { expose: 'react-redux' })
.require('redux', { expose: 'redux' }) .require('redux', { expose: 'redux' })
.require('redux-thunk', { expose: 'redux-thunk' }) .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/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-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/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/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/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', { 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/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/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/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/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/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/index', { expose: 'components/north-header/index' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/north-header-breadcrumbs/index', { expose: 'components/north-header-breadcrumbs/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 + '/i18n.js', { expose: 'i18n' })
.require(PATHS.FRAMEWORK_JAVASCRIPT_SRC + '/jQuery.js', { expose: 'jQuery' }) .require(PATHS.FRAMEWORK_JAVASCRIPT_SRC + '/jQuery.js', { expose: 'jQuery' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/reducer-register.js', { expose: 'reducer-register' }) .require(PATHS.ADMIN_JAVASCRIPT_SRC + '/reducer-register.js', { expose: 'reducer-register' })
.require(PATHS.FRAMEWORK_JAVASCRIPT_SRC + '/router.js', { expose: 'router' }) .require(PATHS.FRAMEWORK_JAVASCRIPT_SRC + '/router.js', { expose: 'router' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/silverstripe-component', { expose: 'silverstripe-component' }) .require(PATHS.ADMIN_JAVASCRIPT_SRC + '/silverstripe-component', { expose: 'silverstripe-component' })
.bundle() .bundle()
.on('update', bundleLib) .on('error', notify.onError({ message: bundleFileName + ': <%= error.message %>' }))
.on('error', notify.onError({ message: bundleFileName + ': <%= error.message %>' })) .pipe(source(bundleFileName))
.pipe(source(bundleFileName)) .pipe(buffer())
.pipe(buffer()) .pipe(sourcemaps.init({ loadMaps: true }))
.pipe(sourcemaps.init({ loadMaps: true })) .pipe(gulpif(!isDev, uglify()))
.pipe(gulpif(!isDev, uglify())) .pipe(sourcemaps.write('./'))
.pipe(sourcemaps.write('./')) .pipe(gulp.dest(PATHS.ADMIN_JAVASCRIPT_DIST));
.pipe(gulp.dest(PATHS.ADMIN_JAVASCRIPT_DIST));
}); });
gulp.task('bundle-legacy', function bundleLeftAndMain() { 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' })) return browserify(Object.assign({}, browserifyOptions, { entries: PATHS.ADMIN_JAVASCRIPT_SRC + '/bundles/legacy.js' }))
.on('update', bundleLeftAndMain) .on('update', bundleLeftAndMain)
.on('log', function (msg) { gulpUtil.log('Finished', 'bundled ' + bundleFileName + ' ' + msg) }) .on('log', function (msg) { gulpUtil.log('Finished', 'bundled ' + bundleFileName + ' ' + msg) })
.transform('babelify', babelifyOptions) .transform('babelify', babelifyOptions)
.external('jQuery') .external('jQuery')
.external('i18n') .external('i18n')
.external('router') .external('router')
.bundle() .bundle()
.on('update', bundleLeftAndMain) .on('update', bundleLeftAndMain)
.on('error', notify.onError({ message: bundleFileName + ': <%= error.message %>' })) .on('error', notify.onError({ message: bundleFileName + ': <%= error.message %>' }))
.pipe(source(bundleFileName)) .pipe(source(bundleFileName))
.pipe(buffer()) .pipe(buffer())
.pipe(sourcemaps.init({ loadMaps: true })) .pipe(sourcemaps.init({ loadMaps: true }))
.pipe(gulpif(!isDev, uglify())) .pipe(gulpif(!isDev, uglify()))
.pipe(sourcemaps.write('./')) .pipe(sourcemaps.write('./'))
.pipe(gulp.dest(PATHS.ADMIN_JAVASCRIPT_DIST)); .pipe(gulp.dest(PATHS.ADMIN_JAVASCRIPT_DIST));
}); });
gulp.task('bundle-framework', function bundleBoot() { 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' })) return browserify(Object.assign({}, browserifyOptions, { entries: PATHS.ADMIN_JAVASCRIPT_SRC + '/boot/index.js' }))
.on('update', bundleBoot) .on('update', bundleBoot)
.on('log', function (msg) { gulpUtil.log('Finished', 'bundled ' + bundleFileName + ' ' + msg) }) .on('log', function (msg) { gulpUtil.log('Finished', 'bundled ' + bundleFileName + ' ' + msg) })
.transform('babelify', babelifyOptions) .transform('babelify', babelifyOptions)
.external('components/action-button/index') .external('components/action-button/index')
.external('components/north-header/index') .external('components/north-header/index')
.external('components/form-builder/index') .external('components/form-builder/index')
.external('deep-freeze') .external('deep-freeze')
.external('components/grid-field/index') .external('components/grid-field/index')
.external('i18n') .external('i18n')
.external('jQuery') .external('jQuery')
.external('page.js') .external('page.js')
.external('react-addons-test-utils') .external('react-addons-test-utils')
.external('react-dom') .external('react-dom')
.external('react-redux') .external('react-redux')
.external('react') .external('react')
.external('reducer-register') .external('reducer-register')
.external('redux-thunk') .external('redux-thunk')
.external('redux') .external('redux')
.external('silverstripe-component') .external('silverstripe-component')
.bundle() .bundle()
.on('update', bundleBoot) .on('update', bundleBoot)
.on('error', notify.onError({ message: bundleFileName + ': <%= error.message %>' })) .on('error', notify.onError({ message: bundleFileName + ': <%= error.message %>' }))
.pipe(source(bundleFileName)) .pipe(source(bundleFileName))
.pipe(buffer()) .pipe(buffer())
.pipe(sourcemaps.init({ loadMaps: true })) .pipe(sourcemaps.init({ loadMaps: true }))
.pipe(gulpif(!isDev, uglify())) .pipe(gulpif(!isDev, uglify()))
.pipe(sourcemaps.write('./')) .pipe(sourcemaps.write('./'))
.pipe(gulp.dest(PATHS.ADMIN_JAVASCRIPT_DIST)); .pipe(gulp.dest(PATHS.ADMIN_JAVASCRIPT_DIST));
}); });
gulp.task('sanity', function () { gulp.task('sanity', function () {
diffFiles(blueimpFileUploadConfig); diffFiles(blueimpFileUploadConfig);
diffFiles(blueimpLoadImageConfig); diffFiles(blueimpLoadImageConfig);
diffFiles(blueimpTmplConfig); diffFiles(blueimpTmplConfig);
diffFiles(jquerySizesConfig); diffFiles(jquerySizesConfig);
diffFiles(tinymceConfig); diffFiles(tinymceConfig);
}); });
gulp.task('thirdparty', function () { gulp.task('thirdparty', function () {
copyFiles(blueimpFileUploadConfig); copyFiles(blueimpFileUploadConfig);
copyFiles(blueimpLoadImageConfig); copyFiles(blueimpLoadImageConfig);
copyFiles(blueimpTmplConfig); copyFiles(blueimpTmplConfig);
copyFiles(jquerySizesConfig); copyFiles(jquerySizesConfig);
copyFiles(tinymceConfig); copyFiles(tinymceConfig);
}); });
gulp.task('umd', ['umd-admin', 'umd-framework'], function () { gulp.task('umd', ['umd-admin', 'umd-framework'], function () {
if (isDev) { if (isDev) {
gulp.watch(PATHS.ADMIN_JAVASCRIPT_SRC + '/*.js', ['umd-admin']); gulp.watch(PATHS.ADMIN_JAVASCRIPT_SRC + '/*.js', ['umd-admin']);
gulp.watch(PATHS.FRAMEWORK_JAVASCRIPT_SRC + '/*.js', ['umd-framework']); gulp.watch(PATHS.FRAMEWORK_JAVASCRIPT_SRC + '/*.js', ['umd-framework']);
} }
}); });
gulp.task('umd-admin', function () { 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 () { 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 * Takes individual images and compiles them together into sprites
*/ */
gulp.task('sprites', function () { gulp.task('sprites', function () {
return sprity.src({ return sprity.src({
src: PATHS.ADMIN_IMAGES + '/sprites/src/**/*.{png,jpg}', src: PATHS.ADMIN_IMAGES + '/sprites/src/**/*.{png,jpg}',
cssPath: '../images/sprites/dist', cssPath: '../images/sprites/dist',
style: './_spritey.scss', style: './_spritey.scss',
processor: 'sass', processor: 'sass',
split: true, split: true,
margin: 0 margin: 0
}) })
.pipe(gulpif('*.png', gulp.dest(PATHS.ADMIN_IMAGES + '/sprites/dist'), gulp.dest(PATHS.ADMIN_SCSS))) .pipe(gulpif('*.png', gulp.dest(PATHS.ADMIN_IMAGES + '/sprites/dist'), gulp.dest(PATHS.ADMIN_SCSS)))
}); });
gulp.task('css', ['compile:css'], function () { gulp.task('css', ['compile:css'], function () {
if (isDev) { if (isDev) {
rootCompileFolders.forEach(function (folder) { rootCompileFolders.forEach(function (folder) {
gulp.watch(folder + '/scss/**/*.scss', ['compile:css']); gulp.watch(folder + '/scss/**/*.scss', ['compile:css']);
}); });
// Watch the .scss files in react components // Watch the .scss files in react components
gulp.watch('./admin/javascript/src/**/*.scss', ['compile:css']); 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 * Watches for changes if --development flag is given
*/ */
gulp.task('compile:css', function () { gulp.task('compile:css', function () {
var outputStyle = isDev ? 'expanded' : 'compressed'; var outputStyle = isDev ? 'expanded' : 'compressed';
var tasks = rootCompileFolders.map(function(folder) { var tasks = rootCompileFolders.map(function(folder) {
return gulp.src(folder + '/scss/**/*.scss') return gulp.src(folder + '/scss/**/*.scss')
.pipe(sourcemaps.init()) .pipe(sourcemaps.init())
.pipe(sass({ outputStyle: outputStyle }) .pipe(sass({ outputStyle: outputStyle })
.on('error', notify.onError({ .on('error', notify.onError({
message: 'Error: <%= error.message %>' message: 'Error: <%= error.message %>'
})) }))
) )
.pipe(postcss([autoprefixer({ browsers: supportedBrowsers })])) .pipe(postcss([autoprefixer({ browsers: supportedBrowsers })]))
.pipe(sourcemaps.write()) .pipe(sourcemaps.write())
.pipe(gulp.dest(folder + '/css')) .pipe(gulp.dest(folder + '/css'))
}); });
return tasks; return tasks;
}); });

View File

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

View File

@ -85,7 +85,7 @@ $.widget('blueimpUIX.fileupload', $.blueimpUI.fileupload, {
e.preventDefault(); // Avoid a form submit e.preventDefault(); // Avoid a form submit
return false; return false;
}); });
} else { //regular file upload } else { //regular file upload
return $.blueimpUI.fileupload.prototype._onSend.call(that, e, data); 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. * Wrapper for `page.show()` with SilverStripe specific behaviour.
*/ */
function show(pageShow) { function show(pageShow) {
return (path, state, dispatch, push) => { return (path, state, dispatch, push) => {
// Normalise `path` so that pattern matching is more robust. // Normalise `path` so that pattern matching is more robust.
// For example if your route is '/pages' it should match when `path` is // For example if your route is '/pages' it should match when `path` is
// 'http://foo.com/admin/pages', '/pages', and 'pages'. // 'http://foo.com/admin/pages', '/pages', and 'pages'.
var el = document.createElement('a'); const el = document.createElement('a');
el.href = path; let pathWithSearch;
path = el.pathname; el.href = path;
if(el.search) { pathWithSearch = el.pathname;
path += el.search; if (el.search) {
} pathWithSearch += el.search;
return pageShow(path, state, dispatch, push);
} }
return pageShow(pathWithSearch, state, dispatch, push);
};
} }
page.show = show(page.show); page.show = show(page.show);

View File

@ -17,7 +17,8 @@
"sprites": "gulp sprites", "sprites": "gulp sprites",
"test": "NODE_PATH=\"./javascript/src:./admin/javascript/src\" jest", "test": "NODE_PATH=\"./javascript/src:./admin/javascript/src\" jest",
"coverage": "NODE_PATH=\"./javascript/src:./admin/javascript/src\" jest --coverage", "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": { "repository": {
"type": "git", "type": "git",
@ -60,6 +61,9 @@
"babel-preset-react": "^6.5.0", "babel-preset-react": "^6.5.0",
"babelify": "^7.2.0", "babelify": "^7.2.0",
"browserify": "^13.0.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", "event-stream": "^3.3.2",
"glob": "^6.0.4", "glob": "^6.0.4",
"gulp": "^3.9.0", "gulp": "^3.9.0",