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

@ -10,6 +10,7 @@ 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() {
@ -28,6 +29,8 @@ function appBoot() {
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

@ -23,7 +23,7 @@ class FormActionComponent extends SilverStripeComponent {
* @returns string * @returns string
*/ */
getButtonClasses() { getButtonClasses() {
var buttonClasses = 'btn'; let buttonClasses = 'btn';
// Add 'type' class // Add 'type' class
buttonClasses += ` btn-${this.props.style}`; buttonClasses += ` btn-${this.props.style}`;
@ -91,12 +91,12 @@ FormActionComponent.propTypes = {
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,15 +13,15 @@ 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,
}, },
/** /**
@ -33,7 +32,7 @@ var fakeInjector = {
* *
* @return object|null * @return object|null
*/ */
getComponentByName: function (componentName) { getComponentByName(componentName) {
return this.components[componentName]; return this.components[componentName];
}, },
@ -45,7 +44,7 @@ var fakeInjector = {
* *
* @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;
@ -86,8 +85,8 @@ var fakeInjector = {
default: default:
return null; return null;
} }
} },
} };
export class FormBuilderComponent extends SilverStripeComponent { export class FormBuilderComponent extends SilverStripeComponent {
@ -110,7 +109,7 @@ export class FormBuilderComponent extends SilverStripeComponent {
* @return object - Promise from the AJAX request. * @return object - Promise from the AJAX request.
*/ */
fetch(schema = true, state = false) { fetch(schema = true, state = false) {
var headerValues = []; const headerValues = [];
if (this.isFetching === true) { if (this.isFetching === true) {
return this.formSchemaPromise; return this.formSchemaPromise;
@ -126,11 +125,9 @@ export class FormBuilderComponent extends SilverStripeComponent {
this.formSchemaPromise = fetch(this.props.schemaUrl, { this.formSchemaPromise = fetch(this.props.schemaUrl, {
headers: { 'X-FormSchema-Request': headerValues.join() }, headers: { 'X-FormSchema-Request': headerValues.join() },
credentials: 'same-origin' credentials: 'same-origin',
})
.then(response => {
return response.json();
}) })
.then(response => response.json())
.then(json => { .then(json => {
this.isFetching = false; this.isFetching = false;
this.props.actions.setSchema(json); this.props.actions.setSchema(json);
@ -152,7 +149,6 @@ export class FormBuilderComponent extends SilverStripeComponent {
*/ */
mapFieldsToComponents(fields) { mapFieldsToComponents(fields) {
return fields.map((field, i) => { return fields.map((field, i) => {
const Component = field.component !== null const Component = field.component !== null
? fakeInjector.getComponentByName(field.component) ? fakeInjector.getComponentByName(field.component)
: fakeInjector.getComponentByDataType(field.type); : fakeInjector.getComponentByDataType(field.type);
@ -162,17 +158,18 @@ export class FormBuilderComponent extends SilverStripeComponent {
} }
// Props which every form field receives. // Props which every form field receives.
let props = { const props = {
attributes: field.attributes, attributes: field.attributes,
data: field.data, data: field.data,
description: field.description, description: field.description,
extraClass: field.extraClass, extraClass: field.extraClass,
fields: field.children, fields: field.children,
id: field.id, 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') { if (field.type !== 'Structural') {
props.rightTitle = field.rightTitle; props.rightTitle = field.rightTitle;
props.leftTitle = field.leftTitle; props.leftTitle = field.leftTitle;
@ -186,7 +183,7 @@ export class FormBuilderComponent extends SilverStripeComponent {
props.source = field.source; 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, attributes: schema.schema.attributes,
data: schema.schema.data, data: schema.schema.data,
fields: schema.schema.fields, fields: schema.schema.fields,
mapFieldsToComponents: this.mapFieldsToComponents mapFieldsToComponents: this.mapFieldsToComponents,
}; };
return <FormComponent {...formProps} /> 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

@ -11,9 +11,9 @@ class FormComponent extends SilverStripeComponent {
* @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() {
@ -22,16 +22,22 @@ class FormComponent extends SilverStripeComponent {
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
id={attr.id}
className={attr.className}
encType={attr.enctype}
method={attr.method}
action={attr.action}
>
{fields && {fields &&
<fieldset className='form-group'> <fieldset className="form-group">
{fields} {fields}
</fieldset> </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>
@ -46,14 +52,14 @@ 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

@ -4,7 +4,6 @@ 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);
} }
@ -12,17 +11,18 @@ class GridFieldActionComponent extends SilverStripeComponent {
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

@ -5,14 +5,14 @@ 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

@ -5,14 +5,14 @@ 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

@ -14,7 +14,8 @@ 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 {
@ -29,9 +30,13 @@ class GridField extends SilverStripeComponent {
componentDidMount() { componentDidMount() {
super.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() { render() {
@ -43,35 +48,41 @@ class GridField extends SilverStripeComponent {
const columns = this.props.data.columns; const columns = this.props.data.columns;
// Placeholder to align the headers correctly with the content // Placeholder to align the headers correctly with the content
const actionPlaceholder = <GridFieldCell key={'actionPlaceholder'} />; const actionPlaceholder = <span key={'actionPlaceholder'} />;
const headerCells = columns.map((column, i) => <GridFieldHeaderCell key={i} >{column.name}</GridFieldHeaderCell>); const headerCells = columns.map((column, i) =>
<GridFieldHeaderCell key={i}>{column.name}</GridFieldHeaderCell>
);
const header = <GridFieldHeader>{headerCells.concat(actionPlaceholder)}</GridFieldHeader>; const header = <GridFieldHeader>{headerCells.concat(actionPlaceholder)}</GridFieldHeader>;
const rows = records.map((record, i) => { const rows = records.map((record, i) => {
var cells = columns.map((column, i) => { const cells = columns.map((column, j) => {
// Get value by dot notation // Get value by dot notation
var val = column.field.split('.').reduce((a, b) => a[b], record) const val = column.field.split('.').reduce((a, b) => a[b], record);
return <GridFieldCell key={i}>{val}</GridFieldCell> return <GridFieldCell key={j} width={column.width}>{val}</GridFieldCell>;
}); });
var rowActions = <GridFieldCell key={i + '-actions'}> const rowActions = (
<GridFieldCell key={`${i}-actions`}>
<GridFieldAction <GridFieldAction
icon={'cog'} icon={'cog'}
handleClick={this.editRecord.bind(this, record.ID)} handleClick={this.editRecord}
key={"action-" + i + "-edit"} key={`action-${i}-edit`}
/> record={record}
/>,
<GridFieldAction <GridFieldAction
icon={'cancel'} icon={'cancel'}
handleClick={this.deleteRecord.bind(this, record.ID)} handleClick={this.deleteRecord}
key={"action-" + i + "-delete"} key={`action-${i}-delete`}
/> record={record}
</GridFieldCell>; />,
</GridFieldCell>
);
return <GridFieldRow key={i}>{cells.concat(rowActions)}</GridFieldRow>; return <GridFieldRow key={i}>{cells.concat(rowActions)}</GridFieldRow>;
}); });
return ( return (
<GridFieldTable header={header} rows={rows}></GridFieldTable> <GridFieldTable header={header} rows={rows} />
); );
} }
@ -79,7 +90,7 @@ class GridField extends SilverStripeComponent {
* @param number int * @param number int
* @param event * @param event
*/ */
deleteRecord(id, event) { deleteRecord(event, id) {
event.preventDefault(); event.preventDefault();
this.props.actions.deleteRecord( this.props.actions.deleteRecord(
this.props.data.recordType, this.props.data.recordType,
@ -89,7 +100,7 @@ class GridField extends SilverStripeComponent {
); );
} }
editRecord(id, event) { editRecord(event) {
event.preventDefault(); event.preventDefault();
// TODO // TODO
} }
@ -100,21 +111,21 @@ 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

@ -5,7 +5,7 @@ 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

@ -5,7 +5,7 @@ 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>
@ -57,7 +57,7 @@ class GridFieldTableComponent extends SilverStripeComponent {
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

@ -11,7 +11,7 @@ class HiddenFieldComponent extends SilverStripeComponent {
render() { render() {
return ( return (
<div className='field hidden'> <div className="field hidden">
<input {...this.getInputProps()} /> <input {...this.getInputProps()} />
</div> </div>
); );
@ -24,11 +24,11 @@ class HiddenFieldComponent extends SilverStripeComponent {
name: this.props.name, name: this.props.name,
onChange: this.props.onChange, onChange: this.props.onChange,
type: 'hidden', type: 'hidden',
value: this.props.value value: this.props.value,
}; };
} }
handleChange(event) { handleChange() {
if (typeof this.props.onChange === 'undefined') { if (typeof this.props.onChange === 'undefined') {
return; return;
} }
@ -42,7 +42,7 @@ HiddenFieldComponent.propTypes = {
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

@ -20,16 +20,18 @@ class NorthHeaderBreadcrumbsComponent extends SilverStripeComponent {
return null; 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 its the last item in the array
if (index === crumbs.length - 1) { 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 { } else {
return [ component = [
<a key={index} className="cms-panel-link crumb" href={crumb.href}>{crumb.text}</a>, <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; return breadcrumbs;

View File

@ -16,13 +16,13 @@ class NorthHeaderComponent extends SilverStripeComponent {
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

@ -11,13 +11,13 @@ class TextFieldComponent extends SilverStripeComponent {
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'> <div className="middleColumn">
<input {...this.getInputProps()} /> <input {...this.getInputProps()} />
</div> </div>
</div> </div>
@ -31,11 +31,11 @@ class TextFieldComponent extends SilverStripeComponent {
name: this.props.name, name: this.props.name,
onChange: this.props.onChange, onChange: this.props.onChange,
type: 'text', type: 'text',
value: this.props.value value: this.props.value,
}; };
} }
handleChange(event) { handleChange() {
if (typeof this.props.onChange === 'undefined') { if (typeof this.props.onChange === 'undefined') {
return; return;
} }
@ -49,7 +49,7 @@ TextFieldComponent.propTypes = {
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

@ -23,15 +23,13 @@ class Config {
* @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) { if (isTopLevelRoute && isUnique) {
topLevelRoutes.push(route); topLevelRoutes.push(route);
} }

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,10 +2,11 @@
* 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 {
@ -43,8 +44,6 @@ class ReducerRegister {
return register[key]; return register[key];
} }
/** /**
* Removes a reducer from the register. * Removes a reducer from the register.
* *
@ -59,6 +58,6 @@ class ReducerRegister {
// 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

@ -23,7 +23,8 @@ class CampaignAdminContainer extends SilverStripeComponent {
<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} /> <FormBuilder schemaUrl={schemaUrl} />
</div> </div>
); );
@ -39,17 +40,17 @@ 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 () { onremove() {
ReactDOM.unmountComponentAtNode(this[0]); ReactDOM.unmountComponentAtNode(this[0]);
} },
}); });
}); });

View File

@ -6,13 +6,17 @@ 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 {
@ -73,6 +77,6 @@ class SilverStripeBackend {
// 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,16 +2,41 @@
* @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. // 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() { componentDidMount() {
if (typeof this.props.cmsEvents === 'undefined') { if (typeof this.props.cmsEvents === 'undefined') {
return; return;
@ -21,26 +46,53 @@ class SilverStripeComponent extends Component {
// there's no guarantee these props will be the same or even present. // there's no guarantee these props will be the same or even present.
this.cmsEvents = this.props.cmsEvents; 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)); $(document).on(cmsEvent, this.cmsEvents[cmsEvent].bind(this));
} }
} }
}
/**
* @func componentWillUnmount
* @desc Unbind the event listeners we added in componentDidMount.
*/
componentWillUnmount() { 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); $(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 string componentEvent - Namespace component event e.g. 'my-component.title-changed'.
* @param object|string|array|number [data] - Some data to pass with the event. * @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) { emitCmsEvent(componentEvent, data) {
$(document).trigger(componentEvent, data); $(document).trigger(componentEvent, data);
@ -49,7 +101,8 @@ class SilverStripeComponent extends Component {
} }
SilverStripeComponent.propTypes = { SilverStripeComponent.propTypes = {
'cmsEvents': React.PropTypes.object cmsEvents: React.PropTypes.object,
route: React.PropTypes.string,
}; };
export default SilverStripeComponent; export default SilverStripeComponent;

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,7 +2,6 @@ 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: case ACTION_TYPES.SET_CONFIG:
@ -12,7 +11,6 @@ function configReducer(state = {}, action) {
return state; return state;
} }
} }
export default configReducer; export default configReducer;

View File

@ -7,5 +7,5 @@ export default {
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,
});
return backend[method.toLowerCase()](populate(url, payload))
.then(response => response.json()) .then(response => response.json())
.then(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) => { .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 * @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(() => {
dispatch({
type: ACTION_TYPES.DELETE_RECORD_SUCCESS,
payload: { recordType, id },
});
}) })
.catch((err) => { .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: 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:
@ -48,16 +48,15 @@ function recordsReducer(state = initialState, action) {
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: default:
return state; return state;
} }
} }
export default recordsReducer; export default recordsReducer;

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: case ACTION_TYPES.SET_SCHEMA: {
const id = action.payload.schema.schema_url; const id = action.payload.schema.schema_url;
return deepFreeze(Object.assign({}, state, { [id]: action.payload })); return deepFreeze(Object.assign({}, state, { [id]: action.payload }));
}
default: default:
return state; return state;
} }
} }

View File

@ -1,29 +1,31 @@
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';
const PATHS = {
MODULES: './node_modules', MODULES: './node_modules',
ADMIN: './admin', ADMIN: './admin',
ADMIN_IMAGES: './admin/images', ADMIN_IMAGES: './admin/images',
@ -35,25 +37,25 @@ var PATHS = {
FRAMEWORK_THIRDPARTY: './thirdparty', FRAMEWORK_THIRDPARTY: './thirdparty',
FRAMEWORK_DEV_INSTALL: './dev/install', FRAMEWORK_DEV_INSTALL: './dev/install',
FRAMEWORK_JAVASCRIPT_SRC: './javascript/src', 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 // 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',
@ -62,11 +64,11 @@ var supportedBrowsers = [
'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',
@ -219,7 +221,6 @@ gulp.task('bundle-lib', function bundleLib() {
.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())

View File

@ -12,15 +12,16 @@ function show(pageShow) {
// 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');
let pathWithSearch;
el.href = path; el.href = path;
path = el.pathname; pathWithSearch = el.pathname;
if (el.search) { if (el.search) {
path += 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",