Add ESLint support

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

View File

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

37
.eslintignore Normal file
View File

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

3
.eslintrc Normal file
View File

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

View File

@ -10,6 +10,7 @@ import SchemaReducer from 'state/schema/reducer';
import RecordsReducer from 'state/records/reducer';
// Sections
// eslint-disable-next-line no-unused-vars
import CampaignAdmin from 'sections/campaign-admin/index';
function appBoot() {
@ -28,6 +29,8 @@ function appBoot() {
configActions.setConfig(window.ss.config)(window.store.dispatch);
}
// TODO: This should be using `window.onload` but isn't because Entwine hooks are being used to set up the <Provider>.
// `window.onload` happens AFTER these Entwine hooks which means the store is undefined when the <Provider> is constructed.
$('body').entwine({ onadd: function () { appBoot(); } });
// TODO: This should be using `window.onload` but isn't because
// Entwine hooks are being used to set up the <Provider>.
// `window.onload` happens AFTER these Entwine hooks which means
// the store is undefined when the <Provider> is constructed.
$('body').entwine({ onadd: () => { appBoot(); } });

View File

@ -23,7 +23,7 @@ class FormActionComponent extends SilverStripeComponent {
* @returns string
*/
getButtonClasses() {
var buttonClasses = 'btn';
let buttonClasses = 'btn';
// Add 'type' class
buttonClasses += ` btn-${this.props.style}`;
@ -91,12 +91,12 @@ FormActionComponent.propTypes = {
loading: React.PropTypes.bool,
icon: React.PropTypes.string,
disabled: React.PropTypes.bool,
style: React.PropTypes.string
style: React.PropTypes.string,
};
FormActionComponent.defaultProps = {
type: 'button',
style: 'secondary'
style: 'secondary',
};
export default FormActionComponent;

View File

@ -1,7 +1,6 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import $ from 'jQuery';
import * as schemaActions from 'state/schema/actions';
import SilverStripeComponent from 'silverstripe-component';
import FormComponent from 'components/form/index';
@ -14,15 +13,15 @@ import es6promise from 'es6-promise';
es6promise.polyfill();
// Using this to map field types to components until we implement dependency injection.
var fakeInjector = {
const fakeInjector = {
/**
* Components registered with the fake DI container.
*/
components: {
'TextField': TextField,
'GridField': GridField,
'HiddenField': HiddenField
TextField,
GridField,
HiddenField,
},
/**
@ -33,7 +32,7 @@ var fakeInjector = {
*
* @return object|null
*/
getComponentByName: function (componentName) {
getComponentByName(componentName) {
return this.components[componentName];
},
@ -45,7 +44,7 @@ var fakeInjector = {
*
* @return object|null
*/
getComponentByDataType: function (dataType) {
getComponentByDataType(dataType) {
switch (dataType) {
case 'String':
return this.components.TextField;
@ -86,8 +85,8 @@ var fakeInjector = {
default:
return null;
}
}
}
},
};
export class FormBuilderComponent extends SilverStripeComponent {
@ -110,7 +109,7 @@ export class FormBuilderComponent extends SilverStripeComponent {
* @return object - Promise from the AJAX request.
*/
fetch(schema = true, state = false) {
var headerValues = [];
const headerValues = [];
if (this.isFetching === true) {
return this.formSchemaPromise;
@ -126,11 +125,9 @@ export class FormBuilderComponent extends SilverStripeComponent {
this.formSchemaPromise = fetch(this.props.schemaUrl, {
headers: { 'X-FormSchema-Request': headerValues.join() },
credentials: 'same-origin'
})
.then(response => {
return response.json();
credentials: 'same-origin',
})
.then(response => response.json())
.then(json => {
this.isFetching = false;
this.props.actions.setSchema(json);
@ -152,7 +149,6 @@ export class FormBuilderComponent extends SilverStripeComponent {
*/
mapFieldsToComponents(fields) {
return fields.map((field, i) => {
const Component = field.component !== null
? fakeInjector.getComponentByName(field.component)
: fakeInjector.getComponentByDataType(field.type);
@ -162,17 +158,18 @@ export class FormBuilderComponent extends SilverStripeComponent {
}
// Props which every form field receives.
let props = {
const props = {
attributes: field.attributes,
data: field.data,
description: field.description,
extraClass: field.extraClass,
fields: field.children,
id: field.id,
name: field.name
name: field.name,
};
// Structural fields (like TabSets) are not posted back to the server and don't receive some props.
// 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;
@ -186,7 +183,7 @@ export class FormBuilderComponent extends SilverStripeComponent {
props.source = field.source;
}
return <Component key={i} {...props} />
return <Component key={i} {...props} />;
});
}
@ -204,29 +201,29 @@ export class FormBuilderComponent extends SilverStripeComponent {
attributes: schema.schema.attributes,
data: schema.schema.data,
fields: schema.schema.fields,
mapFieldsToComponents: this.mapFieldsToComponents
mapFieldsToComponents: this.mapFieldsToComponents,
};
return <FormComponent {...formProps} />
return <FormComponent {...formProps} />;
}
}
FormBuilderComponent.propTypes = {
actions: React.PropTypes.object.isRequired,
schemaUrl: React.PropTypes.string.isRequired,
schemas: React.PropTypes.object.isRequired
schemas: React.PropTypes.object.isRequired,
};
function mapStateToProps(state) {
return {
schemas: state.schemas
}
schemas: state.schemas,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(schemaActions, dispatch)
}
actions: bindActionCreators(schemaActions, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(FormBuilderComponent);

View File

@ -11,9 +11,9 @@ class FormComponent extends SilverStripeComponent {
* @return array|null
*/
getFormActionComponents() {
return this.props.actions.map((action) => {
return <FormActionComponent {...action} />;
});
return this.props.actions.map((action) =>
<FormActionComponent {...action} />
);
}
render() {
@ -22,16 +22,22 @@ class FormComponent extends SilverStripeComponent {
const actions = this.getFormActionComponents();
return (
<form id={attr.id} className={attr.className} encType={attr.enctype} method={attr.method} action={attr.action}>
<form
id={attr.id}
className={attr.className}
encType={attr.enctype}
method={attr.method}
action={attr.action}
>
{fields &&
<fieldset className='form-group'>
<fieldset className="form-group">
{fields}
</fieldset>
}
{actions &&
<div className='actions-fix-btm'>
<div className='btn-group' role='group'>
<div className="actions-fix-btm">
<div className="btn-group" role="group">
{actions}
</div>
</div>
@ -46,14 +52,14 @@ FormComponent.propTypes = {
actions: React.PropTypes.array,
attributes: React.PropTypes.shape({
action: React.PropTypes.string.isRequired,
'class': React.PropTypes.string.isRequired,
class: React.PropTypes.string.isRequired,
enctype: React.PropTypes.string.isRequired,
id: React.PropTypes.string.isRequired,
method: React.PropTypes.string.isRequired
method: React.PropTypes.string.isRequired,
}),
data: React.PropTypes.array,
fields: React.PropTypes.array.isRequired,
mapFieldsToComponents: React.PropTypes.func.isRequired
mapFieldsToComponents: React.PropTypes.func.isRequired,
};
export default FormComponent;

View File

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

View File

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

View File

@ -5,14 +5,14 @@ class GridFieldHeaderCellComponent extends SilverStripeComponent {
render() {
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 = {
width: React.PropTypes.number
}
width: React.PropTypes.number,
};
export default GridFieldHeaderCellComponent;

View File

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

View File

@ -5,7 +5,7 @@ class GridFieldRowComponent extends SilverStripeComponent {
render() {
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

@ -5,7 +5,7 @@ class GridFieldTableComponent extends SilverStripeComponent {
render() {
return (
<ul className='grid-field-table-component [ list-group ]'>
<ul className="grid-field-table-component [ list-group ]">
{this.generateHeader()}
{this.generateRows()}
</ul>
@ -57,7 +57,7 @@ class GridFieldTableComponent extends SilverStripeComponent {
GridFieldTableComponent.propTypes = {
data: React.PropTypes.object,
header: React.PropTypes.object,
rows: React.PropTypes.array
rows: React.PropTypes.array,
};
export default GridFieldTableComponent;

View File

@ -11,7 +11,7 @@ class HiddenFieldComponent extends SilverStripeComponent {
render() {
return (
<div className='field hidden'>
<div className="field hidden">
<input {...this.getInputProps()} />
</div>
);
@ -24,11 +24,11 @@ class HiddenFieldComponent extends SilverStripeComponent {
name: this.props.name,
onChange: this.props.onChange,
type: 'hidden',
value: this.props.value
value: this.props.value,
};
}
handleChange(event) {
handleChange() {
if (typeof this.props.onChange === 'undefined') {
return;
}
@ -42,7 +42,7 @@ HiddenFieldComponent.propTypes = {
extraClass: React.PropTypes.string,
name: React.PropTypes.string.isRequired,
onChange: React.PropTypes.func,
value: React.PropTypes.string
value: React.PropTypes.string,
};
export default HiddenFieldComponent;

View File

@ -20,16 +20,18 @@ class NorthHeaderBreadcrumbsComponent extends SilverStripeComponent {
return null;
}
var breadcrumbs = this.props.crumbs.map((crumb, index, crumbs) => {
const breadcrumbs = this.props.crumbs.map((crumb, index, crumbs) => {
let component;
// If its the last item in the array
if (index === crumbs.length - 1) {
return <span key={index} className="crumb last">{crumb.text}</span>;
component = <span key={index} className="crumb last">{crumb.text}</span>;
} else {
return [
component = [
<a key={index} className="cms-panel-link crumb" href={crumb.href}>{crumb.text}</a>,
<span className="sep">/</span>
<span className="sep">/</span>,
];
}
return component;
});
return breadcrumbs;

View File

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

View File

@ -11,13 +11,13 @@ class TextFieldComponent extends SilverStripeComponent {
render() {
return (
<div className='field text'>
<div className="field text">
{this.props.label &&
<label className='left' htmlFor={'gallery_' + this.props.name}>
<label className="left" htmlFor={`gallery_${this.props.name}`}>
{this.props.label}
</label>
}
<div className='middleColumn'>
<div className="middleColumn">
<input {...this.getInputProps()} />
</div>
</div>
@ -31,11 +31,11 @@ class TextFieldComponent extends SilverStripeComponent {
name: this.props.name,
onChange: this.props.onChange,
type: 'text',
value: this.props.value
value: this.props.value,
};
}
handleChange(event) {
handleChange() {
if (typeof this.props.onChange === 'undefined') {
return;
}
@ -49,7 +49,7 @@ TextFieldComponent.propTypes = {
extraClass: React.PropTypes.string,
name: React.PropTypes.string.isRequired,
onChange: React.PropTypes.func,
value: React.PropTypes.string
value: React.PropTypes.string,
};
export default TextFieldComponent;

View File

@ -23,15 +23,13 @@ class Config {
* @return array
*/
static getTopLevelRoutes() {
var topLevelRoutes = [];
const topLevelRoutes = [];
Object.keys(window.ss.config.sections).forEach((key) => {
const route = window.ss.config.sections[key].route;
const isTopLevelRoute = route.indexOf('/') === -1;
const isUnique = topLevelRoutes.indexOf(route) === -1;
//console.log(this.getSection(key).route);
if (isTopLevelRoute && isUnique) {
topLevelRoutes.push(route);
}

View File

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

View File

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

View File

@ -2,10 +2,11 @@
* The register of Redux reducers.
* @private
*/
var register = {};
const register = {};
/**
* The central register of Redux reducers for the CMS. All registered reducers are combined when the application boots.
* The central register of Redux reducers for the CMS.
* All registered reducers are combined when the application boots.
*/
class ReducerRegister {
@ -43,8 +44,6 @@ class ReducerRegister {
return register[key];
}
/**
* Removes a reducer from the register.
*
@ -59,6 +58,6 @@ class ReducerRegister {
// Create an instance to export. The same instance is exported to
// each script which imports the reducerRegister. This means the
// same register is available throughout the application.
let reducerRegister = new ReducerRegister();
const reducerRegister = new ReducerRegister();
export default reducerRegister;

View File

@ -23,14 +23,15 @@ class CampaignAdminContainer extends SilverStripeComponent {
<FormAction
label={i18n._t('Campaigns.ADDCAMPAIGN')}
icon={'plus-circled'}
handleClick={this.addCampaign} />
handleClick={this.addCampaign}
/>
<FormBuilder schemaUrl={schemaUrl} />
</div>
);
}
addCampaign() {
//Add campaign
// Add campaign
}
}
@ -39,17 +40,17 @@ CampaignAdminContainer.propTypes = {
config: React.PropTypes.shape({
forms: 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) {
return {
config: state.config.sections[ownProps.sectionConfigKey]
}
config: state.config.sections[ownProps.sectionConfigKey],
};
}
export default connect(mapStateToProps)(CampaignAdminContainer);

View File

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

View File

@ -6,13 +6,17 @@ es6promise.polyfill();
* @see https://github.com/github/fetch#handling-http-error-statuses
*/
function checkStatus(response) {
let ret;
let error;
if (response.status >= 200 && response.status < 300) {
return response
ret = response;
} else {
var error = new Error(response.statusText)
error.response = response
throw error
error = new Error(response.statusText);
error.response = response;
throw error;
}
return ret;
}
class SilverStripeBackend {
@ -73,6 +77,6 @@ class SilverStripeBackend {
// Exported as a singleton so we can implement things like
// global caching and request batching at some stage.
let backend = new SilverStripeBackend();
const backend = new SilverStripeBackend();
export default backend;

View File

@ -2,16 +2,41 @@
* @file Base component which all SilverStripe ReactJS components should extend from.
*/
import React, { PropTypes, Component } from 'react';
import React, { Component } from 'react';
import $ from '../../../javascript/src/jQuery';
class SilverStripeComponent extends Component {
/**
* @func componentDidMount
* @desc Bind event listeners which are triggered by legacy-land JavaScript.
* This lets us update the component when something happens in the outside world.
*/
constructor(props) {
super(props);
// Setup component routing.
if (typeof this.props.route !== 'undefined') {
// The component's render method gets switched based on the current path.
// If the current path matches the component's route, the component is displayed.
// Otherwise the component's render method returns null, resulting in
// the component not rendering.
this._render = this.render;
this.render = () => {
let component = null;
if (this.isComponentRoute()) {
component = this._render();
}
return component;
};
window.ss.router(this.props.route, (ctx, next) => {
this.handleEnterRoute(ctx, next);
});
window.ss.router.exit(this.props.route, (ctx, next) => {
this.handleExitRoute(ctx, next);
});
}
}
componentDidMount() {
if (typeof this.props.cmsEvents === 'undefined') {
return;
@ -21,26 +46,53 @@ class SilverStripeComponent extends Component {
// there's no guarantee these props will be the same or even present.
this.cmsEvents = this.props.cmsEvents;
for (let cmsEvent in this.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));
}
}
}
/**
* @func componentWillUnmount
* @desc Unbind the event listeners we added in componentDidMount.
*/
componentWillUnmount() {
for (let cmsEvent in this.cmsEvents) {
// 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();
}
/**
* @func emitCmsEvent
* 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.
* @desc Notifies legacy-land something has changed within our component.
*/
emitCmsEvent(componentEvent, data) {
$(document).trigger(componentEvent, data);
@ -49,7 +101,8 @@ class SilverStripeComponent extends Component {
}
SilverStripeComponent.propTypes = {
'cmsEvents': React.PropTypes.object
cmsEvents: React.PropTypes.object,
route: React.PropTypes.string,
};
export default SilverStripeComponent;

View File

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

View File

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

View File

@ -2,7 +2,6 @@ import deepFreeze from 'deep-freeze';
import ACTION_TYPES from './action-types';
function configReducer(state = {}, action) {
switch (action.type) {
case ACTION_TYPES.SET_CONFIG:
@ -12,7 +11,6 @@ function configReducer(state = {}, action) {
return state;
}
}
export default configReducer;

View File

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

View File

@ -34,9 +34,9 @@ function recordsReducer(state = initialState, action) {
case ACTION_TYPES.FETCH_RECORDS_SUCCESS:
recordType = action.payload.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, {
[recordType]: records
[recordType]: records,
}));
case ACTION_TYPES.DELETE_RECORD_REQUEST:
@ -48,16 +48,15 @@ function recordsReducer(state = initialState, action) {
case ACTION_TYPES.DELETE_RECORD_SUCCESS:
recordType = action.payload.recordType;
records = state[recordType]
.filter(record => record.ID != action.payload.id)
.filter(record => record.ID !== action.payload.id);
return deepFreeze(Object.assign({}, state, {
[recordType]: records
[recordType]: records,
}));
default:
return state;
}
}
export default recordsReducer;

View File

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

View File

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

View File

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

View File

@ -1,29 +1,31 @@
var packageJson = require('./package.json'),
autoprefixer = require('autoprefixer'),
babelify = require('babelify'),
browserify = require('browserify'),
eventStream = require('event-stream'),
glob = require('glob'),
gulp = require('gulp'),
babel = require('gulp-babel'),
diff = require('gulp-diff'),
gulpif = require('gulp-if'),
notify = require('gulp-notify'),
postcss = require('gulp-postcss'),
sass = require('gulp-sass'),
sourcemaps = require('gulp-sourcemaps'),
uglify = require('gulp-uglify'),
gulpUtil = require('gulp-util'),
path = require('path'),
source = require('vinyl-source-stream'),
buffer = require('vinyl-buffer'),
semver = require('semver'),
sprity = require('sprity'),
watchify = require('watchify');
const packageJson = require('./package.json');
const autoprefixer = require('autoprefixer');
const babelify = require('babelify'); // eslint-disable-line no-unused-vars
const browserify = require('browserify');
const eventStream = require('event-stream');
const glob = require('glob');
const gulp = require('gulp');
const babel = require('gulp-babel');
const diff = require('gulp-diff');
const gulpif = require('gulp-if');
const notify = require('gulp-notify');
const postcss = require('gulp-postcss');
const sass = require('gulp-sass');
const sourcemaps = require('gulp-sourcemaps');
const uglify = require('gulp-uglify');
const gulpUtil = require('gulp-util');
const path = require('path');
const source = require('vinyl-source-stream');
const buffer = require('vinyl-buffer');
const semver = require('semver');
const sprity = require('sprity');
const watchify = require('watchify');
var isDev = typeof process.env.npm_config_development !== 'undefined';
const isDev = typeof process.env.npm_config_development !== 'undefined';
var PATHS = {
process.env.NODE_ENV = isDev ? 'development' : 'production';
const PATHS = {
MODULES: './node_modules',
ADMIN: './admin',
ADMIN_IMAGES: './admin/images',
@ -35,25 +37,25 @@ var PATHS = {
FRAMEWORK_THIRDPARTY: './thirdparty',
FRAMEWORK_DEV_INSTALL: './dev/install',
FRAMEWORK_JAVASCRIPT_SRC: './javascript/src',
FRAMEWORK_JAVASCRIPT_DIST: './javascript/dist'
FRAMEWORK_JAVASCRIPT_DIST: './javascript/dist',
};
// Folders which contain both scss and css folders to be compiled
var rootCompileFolders = [PATHS.FRAMEWORK, PATHS.ADMIN, PATHS.FRAMEWORK_DEV_INSTALL]
const rootCompileFolders = [PATHS.FRAMEWORK, PATHS.ADMIN, PATHS.FRAMEWORK_DEV_INSTALL];
var browserifyOptions = {
const browserifyOptions = {
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'],
ignore: /(node_modules|thirdparty)/,
comments: false
comments: false,
};
// Used for autoprefixing css properties (same as Bootstrap Aplha.2 defaults)
var supportedBrowsers = [
const supportedBrowsers = [
'Chrome >= 35',
'Firefox >= 31',
'Edge >= 12',
@ -62,11 +64,11 @@ var supportedBrowsers = [
'Safari >= 8',
'Android 2.3',
'Android >= 4',
'Opera >= 12'
'Opera >= 12',
];
var blueimpFileUploadConfig = {
src: PATHS.MODULES + '/blueimp-file-upload',
const blueimpFileUploadConfig = {
src: `${PATHS.MODULES}/blueimp-file-upload`,
dest: PATHS.FRAMEWORK_THIRDPARTY + '/jquery-fileupload',
files: [
'/cors/jquery.postmessage-transport.js',
@ -219,7 +221,6 @@ gulp.task('bundle-lib', function bundleLib() {
.require(PATHS.FRAMEWORK_JAVASCRIPT_SRC + '/router.js', { expose: 'router' })
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/silverstripe-component', { expose: 'silverstripe-component' })
.bundle()
.on('update', bundleLib)
.on('error', notify.onError({ message: bundleFileName + ': <%= error.message %>' }))
.pipe(source(bundleFileName))
.pipe(buffer())

View File

@ -12,15 +12,16 @@ function show(pageShow) {
// Normalise `path` so that pattern matching is more robust.
// For example if your route is '/pages' it should match when `path` is
// 'http://foo.com/admin/pages', '/pages', and 'pages'.
var el = document.createElement('a');
const el = document.createElement('a');
let pathWithSearch;
el.href = path;
path = el.pathname;
if(el.search) {
path += el.search;
pathWithSearch = el.pathname;
if (el.search) {
pathWithSearch += el.search;
}
return pageShow(path, state, dispatch, push);
}
return pageShow(pathWithSearch, state, dispatch, push);
};
}
page.show = show(page.show);

View File

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