mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Add ESLint support
See https://github.com/silverstripe/silverstripe-framework/pull/5108
This commit is contained in:
parent
6aa22c38ed
commit
34d40bed5f
@ -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
37
.eslintignore
Normal 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/
|
@ -6,38 +6,38 @@ import $ from 'jQuery';
|
||||
|
||||
$.entwine('ss', function ($) {
|
||||
|
||||
$('.cms-description-toggle').entwine({
|
||||
onadd: function () {
|
||||
var shown = false, // Current state of the description.
|
||||
fieldId = this.prop('id').substr(0, this.prop('id').indexOf('_Holder')),
|
||||
$trigger = this.find('.cms-description-trigger'), // Click target for toggling the description.
|
||||
$description = this.find('.description');
|
||||
$('.cms-description-toggle').entwine({
|
||||
onadd: function () {
|
||||
var shown = false, // Current state of the description.
|
||||
fieldId = this.prop('id').substr(0, this.prop('id').indexOf('_Holder')),
|
||||
$trigger = this.find('.cms-description-trigger'), // Click target for toggling the description.
|
||||
$description = this.find('.description');
|
||||
|
||||
// Prevent multiple events being added.
|
||||
if (this.hasClass('description-toggle-enabled')) {
|
||||
return;
|
||||
}
|
||||
// Prevent multiple events being added.
|
||||
if (this.hasClass('description-toggle-enabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If a custom trigger han't been supplied use a sensible default.
|
||||
if ($trigger.length === 0) {
|
||||
$trigger = this
|
||||
.find('.middleColumn')
|
||||
.first() // Get the first middleColumn so we don't add multiple triggers on composite field types.
|
||||
.after('<label class="right" for="' + fieldId + '"><a class="cms-description-trigger" href="javascript:void(0)"><span class="btn-icon-information"></span></a></label>')
|
||||
.next();
|
||||
}
|
||||
// If a custom trigger han't been supplied use a sensible default.
|
||||
if ($trigger.length === 0) {
|
||||
$trigger = this
|
||||
.find('.middleColumn')
|
||||
.first() // Get the first middleColumn so we don't add multiple triggers on composite field types.
|
||||
.after('<label class="right" for="' + fieldId + '"><a class="cms-description-trigger" href="javascript:void(0)"><span class="btn-icon-information"></span></a></label>')
|
||||
.next();
|
||||
}
|
||||
|
||||
this.addClass('description-toggle-enabled');
|
||||
this.addClass('description-toggle-enabled');
|
||||
|
||||
// Toggle next description when button is clicked.
|
||||
$trigger.on('click', function() {
|
||||
$description[shown ? 'hide' : 'show']();
|
||||
shown = !shown;
|
||||
});
|
||||
// Toggle next description when button is clicked.
|
||||
$trigger.on('click', function() {
|
||||
$description[shown ? 'hide' : 'show']();
|
||||
shown = !shown;
|
||||
});
|
||||
|
||||
// Hide next description by default.
|
||||
$description.hide();
|
||||
}
|
||||
});
|
||||
// Hide next description by default.
|
||||
$description.hide();
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -15,11 +15,11 @@ $.entwine('ss', function($){
|
||||
* <ul class="cms-menu-list">
|
||||
* <li><a href="#">Item 1</a></li>
|
||||
* <li class="current opened">
|
||||
* <a href="#">Item 2</a>
|
||||
* <ul>
|
||||
* <li class="current opened"><a href="#">Item 2.1</a></li>
|
||||
* <li><a href="#">Item 2.2</a></li>
|
||||
* </ul>
|
||||
* <a href="#">Item 2</a>
|
||||
* <ul>
|
||||
* <li class="current opened"><a href="#">Item 2.1</a></li>
|
||||
* <li><a href="#">Item 2.2</a></li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ul>
|
||||
*
|
||||
@ -38,7 +38,7 @@ $.entwine('ss', function($){
|
||||
$(this).addClass('collapse');
|
||||
}
|
||||
});
|
||||
} else { //collapse
|
||||
} else { //collapse
|
||||
$(this).children('ul').each(function() {
|
||||
$(this).addClass('collapsed-flyout');
|
||||
$(this).hasClass('collapse');
|
||||
@ -59,7 +59,7 @@ $.entwine('ss', function($){
|
||||
|
||||
//hide all the flyout-indicator
|
||||
$('.cms-menu-list').find('.child-flyout-indicator').hide();
|
||||
} else { //collapse
|
||||
} else { //collapse
|
||||
//hide the flyout only if it is not the current section
|
||||
$('.collapsed-flyout').find('li').each(function() {
|
||||
//if (!$(this).hasClass('current'))
|
||||
@ -110,11 +110,11 @@ $.entwine('ss', function($){
|
||||
* @func getEvaluatedCollapsedState
|
||||
* @return {boolean} - Returns true if the menu should be collapsed, false if expanded.
|
||||
* @desc Evaluate whether the menu should be collapsed.
|
||||
* The basic rule is "If the SiteTree (middle column) is present, collapse the menu, otherwise expand the menu".
|
||||
* This reason behind this is to give the content area more real estate when the SiteTree is present.
|
||||
* The user may wish to override this automatic behaviour and have the menu expanded or collapsed at all times.
|
||||
* So unlike manually toggling the menu, the automatic behaviour never updates the menu's cookie value.
|
||||
* Here we use the manually set state and the automatic behaviour to evaluate what the collapsed state should be.
|
||||
* The basic rule is "If the SiteTree (middle column) is present, collapse the menu, otherwise expand the menu".
|
||||
* This reason behind this is to give the content area more real estate when the SiteTree is present.
|
||||
* The user may wish to override this automatic behaviour and have the menu expanded or collapsed at all times.
|
||||
* So unlike manually toggling the menu, the automatic behaviour never updates the menu's cookie value.
|
||||
* Here we use the manually set state and the automatic behaviour to evaluate what the collapsed state should be.
|
||||
*/
|
||||
getEvaluatedCollapsedState: function () {
|
||||
var shouldCollapse,
|
||||
@ -269,7 +269,7 @@ $.entwine('ss', function($){
|
||||
$('.collapsed-flyout').show();
|
||||
fly.addClass('opened');
|
||||
fly.children('ul').find('li').fadeIn('fast');
|
||||
} else { //collapse
|
||||
} else { //collapse
|
||||
if(li) {
|
||||
li.remove();
|
||||
}
|
||||
|
@ -501,17 +501,17 @@ $.entwine('ss.preview', function($){
|
||||
_loadCurrentPage: function() {
|
||||
if (!this.getIsPreviewEnabled()) return;
|
||||
|
||||
var doc,
|
||||
containerEl = $('.cms-container');
|
||||
try {
|
||||
doc = this.find('iframe')[0].contentDocument;
|
||||
} catch (e) {
|
||||
// iframe can't be accessed - might be secure?
|
||||
console.warn('Unable to access iframe, possible https mis-match');
|
||||
}
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
var doc,
|
||||
containerEl = $('.cms-container');
|
||||
try {
|
||||
doc = this.find('iframe')[0].contentDocument;
|
||||
} catch (e) {
|
||||
// iframe can't be accessed - might be secure?
|
||||
console.warn('Unable to access iframe, possible https mis-match');
|
||||
}
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Load this page in the admin interface if appropriate
|
||||
var id = $(doc).find('meta[name=x-page-id]').attr('content');
|
||||
@ -529,21 +529,21 @@ $.entwine('ss.preview', function($){
|
||||
* Prepare the iframe content for preview.
|
||||
*/
|
||||
_adjustIframeForPreview: function() {
|
||||
var iframe = this.find('iframe')[0],
|
||||
doc;
|
||||
if(!iframe){
|
||||
return;
|
||||
}
|
||||
var iframe = this.find('iframe')[0],
|
||||
doc;
|
||||
if(!iframe){
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
doc = iframe.contentDocument;
|
||||
} catch (e) {
|
||||
// iframe can't be accessed - might be secure?
|
||||
console.warn('Unable to access iframe, possible https mis-match');
|
||||
}
|
||||
if(!doc) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
doc = iframe.contentDocument;
|
||||
} catch (e) {
|
||||
// iframe can't be accessed - might be secure?
|
||||
console.warn('Unable to access iframe, possible https mis-match');
|
||||
}
|
||||
if(!doc) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Open external links in new window to avoid "escaping" the internal page context in the preview
|
||||
// iframe, which is important to stay in for the CMS logic.
|
||||
|
@ -10,24 +10,27 @@ import SchemaReducer from 'state/schema/reducer';
|
||||
import RecordsReducer from 'state/records/reducer';
|
||||
|
||||
// Sections
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import CampaignAdmin from 'sections/campaign-admin/index';
|
||||
|
||||
function appBoot() {
|
||||
reducerRegister.add('config', ConfigReducer);
|
||||
reducerRegister.add('schemas', SchemaReducer);
|
||||
reducerRegister.add('records', RecordsReducer);
|
||||
reducerRegister.add('config', ConfigReducer);
|
||||
reducerRegister.add('schemas', SchemaReducer);
|
||||
reducerRegister.add('records', RecordsReducer);
|
||||
|
||||
const initialState = {};
|
||||
const rootReducer = combineReducers(reducerRegister.getAll());
|
||||
const createStoreWithMiddleware = applyMiddleware(thunkMiddleware, createLogger())(createStore);
|
||||
const initialState = {};
|
||||
const rootReducer = combineReducers(reducerRegister.getAll());
|
||||
const createStoreWithMiddleware = applyMiddleware(thunkMiddleware, createLogger())(createStore);
|
||||
|
||||
// TODO: The store needs to be passed into route callbacks on the route context.
|
||||
window.store = createStoreWithMiddleware(rootReducer, initialState);
|
||||
// TODO: The store needs to be passed into route callbacks on the route context.
|
||||
window.store = createStoreWithMiddleware(rootReducer, initialState);
|
||||
|
||||
// Set the initial config state.
|
||||
configActions.setConfig(window.ss.config)(window.store.dispatch);
|
||||
// Set the initial config state.
|
||||
configActions.setConfig(window.ss.config)(window.store.dispatch);
|
||||
}
|
||||
|
||||
// TODO: This should be using `window.onload` but isn't because Entwine hooks are being used to set up the <Provider>.
|
||||
// `window.onload` happens AFTER these Entwine hooks which means the store is undefined when the <Provider> is constructed.
|
||||
$('body').entwine({ onadd: function () { appBoot(); } });
|
||||
// TODO: This should be using `window.onload` but isn't because
|
||||
// Entwine hooks are being used to set up the <Provider>.
|
||||
// `window.onload` happens AFTER these Entwine hooks which means
|
||||
// the store is undefined when the <Provider> is constructed.
|
||||
$('body').entwine({ onadd: () => { appBoot(); } });
|
||||
|
@ -2,101 +2,101 @@ import React from 'react';
|
||||
import SilverStripeComponent from 'silverstripe-component';
|
||||
|
||||
class FormActionComponent extends SilverStripeComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<button type={this.props.type} className={this.getButtonClasses()} onClick={this.handleClick}>
|
||||
{this.getLoadingIcon()}
|
||||
{this.props.label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the necessary button classes based on the given props
|
||||
*
|
||||
* @returns string
|
||||
*/
|
||||
getButtonClasses() {
|
||||
let buttonClasses = 'btn';
|
||||
|
||||
// Add 'type' class
|
||||
buttonClasses += ` btn-${this.props.style}`;
|
||||
|
||||
// If there is no text
|
||||
if (typeof this.props.label === 'undefined') {
|
||||
buttonClasses += ' no-text';
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<button type={this.props.type} className={this.getButtonClasses()} onClick={this.handleClick}>
|
||||
{this.getLoadingIcon()}
|
||||
{this.props.label}
|
||||
</button>
|
||||
);
|
||||
// Add icon class
|
||||
if (typeof this.props.icon !== 'undefined') {
|
||||
buttonClasses += ` font-icon-${this.props.icon}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the necessary button classes based on the given props
|
||||
*
|
||||
* @returns string
|
||||
*/
|
||||
getButtonClasses() {
|
||||
var buttonClasses = 'btn';
|
||||
|
||||
// Add 'type' class
|
||||
buttonClasses += ` btn-${this.props.style}`;
|
||||
|
||||
// If there is no text
|
||||
if (typeof this.props.label === 'undefined') {
|
||||
buttonClasses += ' no-text';
|
||||
}
|
||||
|
||||
// Add icon class
|
||||
if (typeof this.props.icon !== 'undefined') {
|
||||
buttonClasses += ` font-icon-${this.props.icon}`;
|
||||
}
|
||||
|
||||
// Add loading class
|
||||
if (this.props.loading === true) {
|
||||
buttonClasses += ' btn--loading';
|
||||
}
|
||||
|
||||
// Add disabled class
|
||||
if (this.props.disabled === true) {
|
||||
buttonClasses += ' disabled';
|
||||
}
|
||||
|
||||
return buttonClasses;
|
||||
// Add loading class
|
||||
if (this.props.loading === true) {
|
||||
buttonClasses += ' btn--loading';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns markup for the loading icon
|
||||
*
|
||||
* @returns object|null
|
||||
*/
|
||||
getLoadingIcon() {
|
||||
if (this.props.loading) {
|
||||
return (
|
||||
<div className="btn__loading-icon" >
|
||||
<svg viewBox="0 0 44 12">
|
||||
<circle cx="6" cy="6" r="6" />
|
||||
<circle cx="22" cy="6" r="6" />
|
||||
<circle cx="38" cy="6" r="6" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
// Add disabled class
|
||||
if (this.props.disabled === true) {
|
||||
buttonClasses += ' disabled';
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler triggered when a user clicks the button.
|
||||
*
|
||||
* @param object event
|
||||
* @returns null
|
||||
*/
|
||||
handleClick(event) {
|
||||
this.props.handleClick(event);
|
||||
return buttonClasses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns markup for the loading icon
|
||||
*
|
||||
* @returns object|null
|
||||
*/
|
||||
getLoadingIcon() {
|
||||
if (this.props.loading) {
|
||||
return (
|
||||
<div className="btn__loading-icon" >
|
||||
<svg viewBox="0 0 44 12">
|
||||
<circle cx="6" cy="6" r="6" />
|
||||
<circle cx="22" cy="6" r="6" />
|
||||
<circle cx="38" cy="6" r="6" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler triggered when a user clicks the button.
|
||||
*
|
||||
* @param object event
|
||||
* @returns null
|
||||
*/
|
||||
handleClick(event) {
|
||||
this.props.handleClick(event);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
FormActionComponent.propTypes = {
|
||||
handleClick: React.PropTypes.func.isRequired,
|
||||
label: React.PropTypes.string,
|
||||
type: React.PropTypes.string,
|
||||
loading: React.PropTypes.bool,
|
||||
icon: React.PropTypes.string,
|
||||
disabled: React.PropTypes.bool,
|
||||
style: React.PropTypes.string
|
||||
handleClick: React.PropTypes.func.isRequired,
|
||||
label: React.PropTypes.string,
|
||||
type: React.PropTypes.string,
|
||||
loading: React.PropTypes.bool,
|
||||
icon: React.PropTypes.string,
|
||||
disabled: React.PropTypes.bool,
|
||||
style: React.PropTypes.string,
|
||||
};
|
||||
|
||||
FormActionComponent.defaultProps = {
|
||||
type: 'button',
|
||||
style: 'secondary'
|
||||
type: 'button',
|
||||
style: 'secondary',
|
||||
};
|
||||
|
||||
export default FormActionComponent;
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import $ from 'jQuery';
|
||||
import * as schemaActions from 'state/schema/actions';
|
||||
import SilverStripeComponent from 'silverstripe-component';
|
||||
import FormComponent from 'components/form/index';
|
||||
@ -14,219 +13,217 @@ import es6promise from 'es6-promise';
|
||||
es6promise.polyfill();
|
||||
|
||||
// Using this to map field types to components until we implement dependency injection.
|
||||
var fakeInjector = {
|
||||
const fakeInjector = {
|
||||
|
||||
/**
|
||||
* Components registered with the fake DI container.
|
||||
*/
|
||||
components: {
|
||||
'TextField': TextField,
|
||||
'GridField': GridField,
|
||||
'HiddenField': HiddenField
|
||||
},
|
||||
/**
|
||||
* Components registered with the fake DI container.
|
||||
*/
|
||||
components: {
|
||||
TextField,
|
||||
GridField,
|
||||
HiddenField,
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the component matching the passed component name.
|
||||
* Used when a component type is provided bt the form schema.
|
||||
*
|
||||
* @param string componentName - The name of the component to get from the injector.
|
||||
*
|
||||
* @return object|null
|
||||
*/
|
||||
getComponentByName: function (componentName) {
|
||||
return this.components[componentName];
|
||||
},
|
||||
/**
|
||||
* Gets the component matching the passed component name.
|
||||
* Used when a component type is provided bt the form schema.
|
||||
*
|
||||
* @param string componentName - The name of the component to get from the injector.
|
||||
*
|
||||
* @return object|null
|
||||
*/
|
||||
getComponentByName(componentName) {
|
||||
return this.components[componentName];
|
||||
},
|
||||
|
||||
/**
|
||||
* Default data type to component mappings.
|
||||
* Used as a fallback when no component type is provided in the form schema.
|
||||
*
|
||||
* @param string dataType - The data type provided by the form schema.
|
||||
*
|
||||
* @return object|null
|
||||
*/
|
||||
getComponentByDataType: function (dataType) {
|
||||
switch (dataType) {
|
||||
case 'String':
|
||||
return this.components.TextField;
|
||||
case 'Hidden':
|
||||
return this.components.HiddenField;
|
||||
case 'Text':
|
||||
// Textarea field (not implemented)
|
||||
return null;
|
||||
case 'HTML':
|
||||
// HTML editor field (not implemented)
|
||||
return null;
|
||||
case 'Integer':
|
||||
// Numeric field (not implemented)
|
||||
return null;
|
||||
case 'Decimal':
|
||||
// Numeric field (not implemented)
|
||||
return null;
|
||||
case 'MultiSelect':
|
||||
// Radio field (not implemented)
|
||||
return null;
|
||||
case 'SingleSelect':
|
||||
// Dropdown field (not implemented)
|
||||
return null;
|
||||
case 'Date':
|
||||
// DateTime field (not implemented)
|
||||
return null;
|
||||
case 'DateTime':
|
||||
// DateTime field (not implemented)
|
||||
return null;
|
||||
case 'Time':
|
||||
// DateTime field (not implemented)
|
||||
return null;
|
||||
case 'Boolean':
|
||||
// Checkbox field (not implemented)
|
||||
return null;
|
||||
case 'Custom':
|
||||
return this.components.GridField;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Default data type to component mappings.
|
||||
* Used as a fallback when no component type is provided in the form schema.
|
||||
*
|
||||
* @param string dataType - The data type provided by the form schema.
|
||||
*
|
||||
* @return object|null
|
||||
*/
|
||||
getComponentByDataType(dataType) {
|
||||
switch (dataType) {
|
||||
case 'String':
|
||||
return this.components.TextField;
|
||||
case 'Hidden':
|
||||
return this.components.HiddenField;
|
||||
case 'Text':
|
||||
// Textarea field (not implemented)
|
||||
return null;
|
||||
case 'HTML':
|
||||
// HTML editor field (not implemented)
|
||||
return null;
|
||||
case 'Integer':
|
||||
// Numeric field (not implemented)
|
||||
return null;
|
||||
case 'Decimal':
|
||||
// Numeric field (not implemented)
|
||||
return null;
|
||||
case 'MultiSelect':
|
||||
// Radio field (not implemented)
|
||||
return null;
|
||||
case 'SingleSelect':
|
||||
// Dropdown field (not implemented)
|
||||
return null;
|
||||
case 'Date':
|
||||
// DateTime field (not implemented)
|
||||
return null;
|
||||
case 'DateTime':
|
||||
// DateTime field (not implemented)
|
||||
return null;
|
||||
case 'Time':
|
||||
// DateTime field (not implemented)
|
||||
return null;
|
||||
case 'Boolean':
|
||||
// Checkbox field (not implemented)
|
||||
return null;
|
||||
case 'Custom':
|
||||
return this.components.GridField;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export class FormBuilderComponent extends SilverStripeComponent {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.formSchemaPromise = null;
|
||||
this.formSchemaPromise = null;
|
||||
this.isFetching = false;
|
||||
|
||||
this.fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches data used to generate a form. This can be form schema and or form state data.
|
||||
* When the response comes back the data is saved to state.
|
||||
*
|
||||
* @param boolean schema - If form schema data should be returned in the response.
|
||||
* @param boolean state - If form state data should be returned in the response.
|
||||
*
|
||||
* @return object - Promise from the AJAX request.
|
||||
*/
|
||||
fetch(schema = true, state = false) {
|
||||
const headerValues = [];
|
||||
|
||||
if (this.isFetching === true) {
|
||||
return this.formSchemaPromise;
|
||||
}
|
||||
|
||||
if (schema === true) {
|
||||
headerValues.push('schema');
|
||||
}
|
||||
|
||||
if (state === true) {
|
||||
headerValues.push('state');
|
||||
}
|
||||
|
||||
this.formSchemaPromise = fetch(this.props.schemaUrl, {
|
||||
headers: { 'X-FormSchema-Request': headerValues.join() },
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
this.isFetching = false;
|
||||
this.props.actions.setSchema(json);
|
||||
});
|
||||
|
||||
this.fetch();
|
||||
this.isFetching = true;
|
||||
|
||||
return this.formSchemaPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a list of schema fields to their React Component.
|
||||
* Only top level form fields are handled here, composite fields (TabSets etc),
|
||||
* are responsible for mapping and rendering their children.
|
||||
*
|
||||
* @param array fields
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
mapFieldsToComponents(fields) {
|
||||
return fields.map((field, i) => {
|
||||
const Component = field.component !== null
|
||||
? fakeInjector.getComponentByName(field.component)
|
||||
: fakeInjector.getComponentByDataType(field.type);
|
||||
|
||||
if (Component === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Props which every form field receives.
|
||||
const props = {
|
||||
attributes: field.attributes,
|
||||
data: field.data,
|
||||
description: field.description,
|
||||
extraClass: field.extraClass,
|
||||
fields: field.children,
|
||||
id: field.id,
|
||||
name: field.name,
|
||||
};
|
||||
|
||||
// Structural fields (like TabSets) are not posted back to
|
||||
// the server and don't receive some props.
|
||||
if (field.type !== 'Structural') {
|
||||
props.rightTitle = field.rightTitle;
|
||||
props.leftTitle = field.leftTitle;
|
||||
props.readOnly = field.readOnly;
|
||||
props.disabled = field.disabled;
|
||||
props.customValidationMessage = field.customValidationMessage;
|
||||
}
|
||||
|
||||
// Dropdown and Radio fields need some options...
|
||||
if (field.type === 'MultiSelect' || field.type === 'SingleSelect') {
|
||||
props.source = field.source;
|
||||
}
|
||||
|
||||
return <Component key={i} {...props} />;
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const schema = this.props.schemas[this.props.schemaUrl];
|
||||
|
||||
// If the response from fetching the initial data
|
||||
// hasn't come back yet, don't render anything.
|
||||
if (!schema) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches data used to generate a form. This can be form schema and or form state data.
|
||||
* When the response comes back the data is saved to state.
|
||||
*
|
||||
* @param boolean schema - If form schema data should be returned in the response.
|
||||
* @param boolean state - If form state data should be returned in the response.
|
||||
*
|
||||
* @return object - Promise from the AJAX request.
|
||||
*/
|
||||
fetch(schema = true, state = false) {
|
||||
var headerValues = [];
|
||||
const formProps = {
|
||||
actions: schema.schema.actions,
|
||||
attributes: schema.schema.attributes,
|
||||
data: schema.schema.data,
|
||||
fields: schema.schema.fields,
|
||||
mapFieldsToComponents: this.mapFieldsToComponents,
|
||||
};
|
||||
|
||||
if (this.isFetching === true) {
|
||||
return this.formSchemaPromise;
|
||||
}
|
||||
|
||||
if (schema === true) {
|
||||
headerValues.push('schema');
|
||||
}
|
||||
|
||||
if (state === true) {
|
||||
headerValues.push('state');
|
||||
}
|
||||
|
||||
this.formSchemaPromise = fetch(this.props.schemaUrl, {
|
||||
headers: { 'X-FormSchema-Request': headerValues.join() },
|
||||
credentials: 'same-origin'
|
||||
})
|
||||
.then(response => {
|
||||
return response.json();
|
||||
})
|
||||
.then(json => {
|
||||
this.isFetching = false;
|
||||
this.props.actions.setSchema(json);
|
||||
});
|
||||
|
||||
this.isFetching = true;
|
||||
|
||||
return this.formSchemaPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a list of schema fields to their React Component.
|
||||
* Only top level form fields are handled here, composite fields (TabSets etc),
|
||||
* are responsible for mapping and rendering their children.
|
||||
*
|
||||
* @param array fields
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
mapFieldsToComponents(fields) {
|
||||
return fields.map((field, i) => {
|
||||
|
||||
const Component = field.component !== null
|
||||
? fakeInjector.getComponentByName(field.component)
|
||||
: fakeInjector.getComponentByDataType(field.type);
|
||||
|
||||
if (Component === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Props which every form field receives.
|
||||
let props = {
|
||||
attributes: field.attributes,
|
||||
data: field.data,
|
||||
description: field.description,
|
||||
extraClass: field.extraClass,
|
||||
fields: field.children,
|
||||
id: field.id,
|
||||
name: field.name
|
||||
};
|
||||
|
||||
// Structural fields (like TabSets) are not posted back to the server and don't receive some props.
|
||||
if (field.type !== 'Structural') {
|
||||
props.rightTitle = field.rightTitle;
|
||||
props.leftTitle = field.leftTitle;
|
||||
props.readOnly = field.readOnly;
|
||||
props.disabled = field.disabled;
|
||||
props.customValidationMessage = field.customValidationMessage;
|
||||
}
|
||||
|
||||
// Dropdown and Radio fields need some options...
|
||||
if (field.type === 'MultiSelect' || field.type === 'SingleSelect') {
|
||||
props.source = field.source;
|
||||
}
|
||||
|
||||
return <Component key={i} {...props} />
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const schema = this.props.schemas[this.props.schemaUrl];
|
||||
|
||||
// If the response from fetching the initial data
|
||||
// hasn't come back yet, don't render anything.
|
||||
if (!schema) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const formProps = {
|
||||
actions: schema.schema.actions,
|
||||
attributes: schema.schema.attributes,
|
||||
data: schema.schema.data,
|
||||
fields: schema.schema.fields,
|
||||
mapFieldsToComponents: this.mapFieldsToComponents
|
||||
};
|
||||
|
||||
return <FormComponent {...formProps} />
|
||||
}
|
||||
return <FormComponent {...formProps} />;
|
||||
}
|
||||
}
|
||||
|
||||
FormBuilderComponent.propTypes = {
|
||||
actions: React.PropTypes.object.isRequired,
|
||||
schemaUrl: React.PropTypes.string.isRequired,
|
||||
schemas: React.PropTypes.object.isRequired
|
||||
actions: React.PropTypes.object.isRequired,
|
||||
schemaUrl: React.PropTypes.string.isRequired,
|
||||
schemas: React.PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
schemas: state.schemas
|
||||
}
|
||||
return {
|
||||
schemas: state.schemas,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(schemaActions, dispatch)
|
||||
}
|
||||
return {
|
||||
actions: bindActionCreators(schemaActions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(FormBuilderComponent);
|
||||
|
@ -5,22 +5,22 @@ import { FormBuilderComponent } from '../';
|
||||
|
||||
describe('FormBuilderComponent', () => {
|
||||
|
||||
describe('getFormSchema()', () => {
|
||||
describe('getFormSchema()', () => {
|
||||
|
||||
var formBuilder;
|
||||
var formBuilder;
|
||||
|
||||
beforeEach(() => {
|
||||
const props = {
|
||||
store: {
|
||||
getState: () => {}
|
||||
},
|
||||
actions: {},
|
||||
schemaUrl: 'admin/assets/schema/1',
|
||||
schema: { forms: [{ schema: { id: '1', schema_url: 'admin/assets/schema/1' } }] }
|
||||
};
|
||||
beforeEach(() => {
|
||||
const props = {
|
||||
store: {
|
||||
getState: () => {}
|
||||
},
|
||||
actions: {},
|
||||
schemaUrl: 'admin/assets/schema/1',
|
||||
schema: { forms: [{ schema: { id: '1', schema_url: 'admin/assets/schema/1' } }] }
|
||||
};
|
||||
|
||||
formBuilder = new FormBuilderComponent(props);
|
||||
});
|
||||
formBuilder = new FormBuilderComponent(props);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -4,56 +4,62 @@ import FormActionComponent from 'components/form-action/index';
|
||||
|
||||
class FormComponent extends SilverStripeComponent {
|
||||
|
||||
/**
|
||||
* Gets the components responsible for perfoming actions on the form.
|
||||
* For example form submission.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
getFormActionComponents() {
|
||||
return this.props.actions.map((action) => {
|
||||
return <FormActionComponent {...action} />;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Gets the components responsible for perfoming actions on the form.
|
||||
* For example form submission.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
getFormActionComponents() {
|
||||
return this.props.actions.map((action) =>
|
||||
<FormActionComponent {...action} />
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const attr = this.props.attributes;
|
||||
const fields = this.props.mapFieldsToComponents(this.props.fields);
|
||||
const actions = this.getFormActionComponents();
|
||||
render() {
|
||||
const attr = this.props.attributes;
|
||||
const fields = this.props.mapFieldsToComponents(this.props.fields);
|
||||
const actions = this.getFormActionComponents();
|
||||
|
||||
return (
|
||||
<form id={attr.id} className={attr.className} encType={attr.enctype} method={attr.method} action={attr.action}>
|
||||
{fields &&
|
||||
<fieldset className='form-group'>
|
||||
{fields}
|
||||
</fieldset>
|
||||
}
|
||||
return (
|
||||
<form
|
||||
id={attr.id}
|
||||
className={attr.className}
|
||||
encType={attr.enctype}
|
||||
method={attr.method}
|
||||
action={attr.action}
|
||||
>
|
||||
{fields &&
|
||||
<fieldset className="form-group">
|
||||
{fields}
|
||||
</fieldset>
|
||||
}
|
||||
|
||||
{actions &&
|
||||
<div className='actions-fix-btm'>
|
||||
<div className='btn-group' role='group'>
|
||||
{actions}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
{actions &&
|
||||
<div className="actions-fix-btm">
|
||||
<div className="btn-group" role="group">
|
||||
{actions}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
FormComponent.propTypes = {
|
||||
actions: React.PropTypes.array,
|
||||
attributes: React.PropTypes.shape({
|
||||
action: React.PropTypes.string.isRequired,
|
||||
'class': React.PropTypes.string.isRequired,
|
||||
enctype: React.PropTypes.string.isRequired,
|
||||
id: React.PropTypes.string.isRequired,
|
||||
method: React.PropTypes.string.isRequired
|
||||
}),
|
||||
data: React.PropTypes.array,
|
||||
fields: React.PropTypes.array.isRequired,
|
||||
mapFieldsToComponents: React.PropTypes.func.isRequired
|
||||
actions: React.PropTypes.array,
|
||||
attributes: React.PropTypes.shape({
|
||||
action: React.PropTypes.string.isRequired,
|
||||
class: React.PropTypes.string.isRequired,
|
||||
enctype: React.PropTypes.string.isRequired,
|
||||
id: React.PropTypes.string.isRequired,
|
||||
method: React.PropTypes.string.isRequired,
|
||||
}),
|
||||
data: React.PropTypes.array,
|
||||
fields: React.PropTypes.array.isRequired,
|
||||
mapFieldsToComponents: React.PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default FormComponent;
|
||||
|
@ -2,27 +2,27 @@ import React from 'react';
|
||||
import SilverStripeComponent from 'silverstripe-component';
|
||||
|
||||
class GridFieldActionComponent extends SilverStripeComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<button
|
||||
className={`grid-field-action-component font-icon-${this.props.icon}`}
|
||||
onClick={this.handleClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<button
|
||||
className={`grid-field-action-component font-icon-${this.props.icon}`}
|
||||
onClick={this.handleClick} />
|
||||
);
|
||||
}
|
||||
|
||||
handleClick(event) {
|
||||
this.props.handleClick(event);
|
||||
}
|
||||
handleClick(event) {
|
||||
this.props.handleClick(event, this.props.record.ID);
|
||||
}
|
||||
}
|
||||
|
||||
GridFieldActionComponent.PropTypes = {
|
||||
handleClick: React.PropTypes.func.isRequired
|
||||
}
|
||||
handleClick: React.PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default GridFieldActionComponent;
|
||||
|
@ -3,16 +3,16 @@ import SilverStripeComponent from 'silverstripe-component';
|
||||
|
||||
class GridFieldCellComponent extends SilverStripeComponent {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className='grid-field-cell-component'>{this.props.children}</div>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div className="grid-field-cell-component">{this.props.children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
GridFieldCellComponent.PropTypes = {
|
||||
width: React.PropTypes.number
|
||||
}
|
||||
width: React.PropTypes.number,
|
||||
};
|
||||
|
||||
export default GridFieldCellComponent;
|
||||
|
@ -3,16 +3,16 @@ import SilverStripeComponent from 'silverstripe-component';
|
||||
|
||||
class GridFieldHeaderCellComponent extends SilverStripeComponent {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className='grid-field-header-cell-component'>{this.props.children}</div>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div className="grid-field-header-cell-component">{this.props.children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
GridFieldHeaderCellComponent.PropTypes = {
|
||||
width: React.PropTypes.number
|
||||
}
|
||||
width: React.PropTypes.number,
|
||||
};
|
||||
|
||||
export default GridFieldHeaderCellComponent;
|
||||
|
@ -4,11 +4,11 @@ import GridFieldRowComponent from './row';
|
||||
|
||||
class GridFieldHeaderComponent extends SilverStripeComponent {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<GridFieldRowComponent>{this.props.children}</GridFieldRowComponent>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<GridFieldRowComponent>{this.props.children}</GridFieldRowComponent>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -14,107 +14,118 @@ import * as actions from 'state/records/actions';
|
||||
* The component acts as a container for a grid field,
|
||||
* with smarts around data retrieval from external sources.
|
||||
*
|
||||
* @todo Convert to higher order component which hooks up form schema data to an API backend as a grid data source
|
||||
* @todo Convert to higher order component which hooks up form
|
||||
* schema data to an API backend as a grid data source
|
||||
* @todo Replace "dumb" inner components with third party library (e.g. https://griddlegriddle.github.io)
|
||||
*/
|
||||
class GridField extends SilverStripeComponent {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.deleteRecord = this.deleteRecord.bind(this);
|
||||
this.editRecord = this.editRecord.bind(this);
|
||||
this.deleteRecord = this.deleteRecord.bind(this);
|
||||
this.editRecord = this.editRecord.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
|
||||
const data = this.props.data;
|
||||
|
||||
this.props.actions.fetchRecords(
|
||||
data.recordType,
|
||||
data.collectionReadEndpoint.method,
|
||||
data.collectionReadEndpoint.url
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const records = this.props.records;
|
||||
if (!records) {
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
const columns = this.props.data.columns;
|
||||
|
||||
let data = this.props.data;
|
||||
// Placeholder to align the headers correctly with the content
|
||||
const actionPlaceholder = <span key={'actionPlaceholder'} />;
|
||||
const headerCells = columns.map((column, i) =>
|
||||
<GridFieldHeaderCell key={i}>{column.name}</GridFieldHeaderCell>
|
||||
);
|
||||
const header = <GridFieldHeader>{headerCells.concat(actionPlaceholder)}</GridFieldHeader>;
|
||||
|
||||
this.props.actions.fetchRecords(data.recordType, data.collectionReadEndpoint.method, data.collectionReadEndpoint.url);
|
||||
}
|
||||
const rows = records.map((record, i) => {
|
||||
const cells = columns.map((column, j) => {
|
||||
// Get value by dot notation
|
||||
const val = column.field.split('.').reduce((a, b) => a[b], record);
|
||||
return <GridFieldCell key={j} width={column.width}>{val}</GridFieldCell>;
|
||||
});
|
||||
|
||||
render() {
|
||||
const records = this.props.records;
|
||||
if(!records) {
|
||||
return <div></div>;
|
||||
}
|
||||
const rowActions = (
|
||||
<GridFieldCell key={`${i}-actions`}>
|
||||
<GridFieldAction
|
||||
icon={'cog'}
|
||||
handleClick={this.editRecord}
|
||||
key={`action-${i}-edit`}
|
||||
record={record}
|
||||
/>,
|
||||
<GridFieldAction
|
||||
icon={'cancel'}
|
||||
handleClick={this.deleteRecord}
|
||||
key={`action-${i}-delete`}
|
||||
record={record}
|
||||
/>,
|
||||
</GridFieldCell>
|
||||
);
|
||||
|
||||
const columns = this.props.data.columns;
|
||||
return <GridFieldRow key={i}>{cells.concat(rowActions)}</GridFieldRow>;
|
||||
});
|
||||
|
||||
// Placeholder to align the headers correctly with the content
|
||||
const actionPlaceholder = <GridFieldCell key={'actionPlaceholder'} />;
|
||||
const headerCells = columns.map((column, i) => <GridFieldHeaderCell key={i} >{column.name}</GridFieldHeaderCell>);
|
||||
const header = <GridFieldHeader>{headerCells.concat(actionPlaceholder)}</GridFieldHeader>;
|
||||
return (
|
||||
<GridFieldTable header={header} rows={rows} />
|
||||
);
|
||||
}
|
||||
|
||||
const rows = records.map((record, i) => {
|
||||
var cells = columns.map((column, i) => {
|
||||
// Get value by dot notation
|
||||
var val = column.field.split('.').reduce((a, b) => a[b], record)
|
||||
return <GridFieldCell key={i}>{val}</GridFieldCell>
|
||||
});
|
||||
/**
|
||||
* @param number int
|
||||
* @param event
|
||||
*/
|
||||
deleteRecord(event, id) {
|
||||
event.preventDefault();
|
||||
this.props.actions.deleteRecord(
|
||||
this.props.data.recordType,
|
||||
id,
|
||||
this.props.data.itemDeleteEndpoint.method,
|
||||
this.props.data.itemDeleteEndpoint.url
|
||||
);
|
||||
}
|
||||
|
||||
var rowActions = <GridFieldCell key={i + '-actions'}>
|
||||
<GridFieldAction
|
||||
icon={'cog'}
|
||||
handleClick={this.editRecord.bind(this, record.ID)}
|
||||
key={"action-" + i + "-edit"}
|
||||
/>
|
||||
<GridFieldAction
|
||||
icon={'cancel'}
|
||||
handleClick={this.deleteRecord.bind(this, record.ID)}
|
||||
key={"action-" + i + "-delete"}
|
||||
/>
|
||||
</GridFieldCell>;
|
||||
|
||||
return <GridFieldRow key={i}>{cells.concat(rowActions)}</GridFieldRow>;
|
||||
});
|
||||
|
||||
return (
|
||||
<GridFieldTable header={header} rows={rows}></GridFieldTable>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param number int
|
||||
* @param event
|
||||
*/
|
||||
deleteRecord(id, event) {
|
||||
event.preventDefault();
|
||||
this.props.actions.deleteRecord(
|
||||
this.props.data.recordType,
|
||||
id,
|
||||
this.props.data.itemDeleteEndpoint.method,
|
||||
this.props.data.itemDeleteEndpoint.url
|
||||
);
|
||||
}
|
||||
|
||||
editRecord(id, event) {
|
||||
event.preventDefault();
|
||||
// TODO
|
||||
}
|
||||
editRecord(event) {
|
||||
event.preventDefault();
|
||||
// TODO
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
GridField.propTypes = {
|
||||
data: React.PropTypes.shape({
|
||||
recordType: React.PropTypes.string.isRequired,
|
||||
headerColumns: React.PropTypes.array,
|
||||
collectionReadEndpoint: React.PropTypes.object
|
||||
})
|
||||
data: React.PropTypes.shape({
|
||||
recordType: React.PropTypes.string.isRequired,
|
||||
headerColumns: React.PropTypes.array,
|
||||
collectionReadEndpoint: React.PropTypes.object,
|
||||
}),
|
||||
};
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
let recordType = ownProps.data ? ownProps.data.recordType : null;
|
||||
return {
|
||||
records: (state.records && recordType) ? state.records[recordType] : []
|
||||
}
|
||||
const recordType = ownProps.data ? ownProps.data.recordType : null;
|
||||
return {
|
||||
records: (state.records && recordType) ? state.records[recordType] : [],
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(actions, dispatch)
|
||||
}
|
||||
return {
|
||||
actions: bindActionCreators(actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(GridField);
|
||||
|
@ -3,11 +3,11 @@ import SilverStripeComponent from 'silverstripe-component';
|
||||
|
||||
class GridFieldRowComponent extends SilverStripeComponent {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<li className='grid-field-row-component [ list-group-item ]'>{this.props.children}</li>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<li className="grid-field-row-component [ list-group-item ]">{this.props.children}</li>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -3,61 +3,61 @@ import SilverStripeComponent from 'silverstripe-component';
|
||||
|
||||
class GridFieldTableComponent extends SilverStripeComponent {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ul className='grid-field-table-component [ list-group ]'>
|
||||
{this.generateHeader()}
|
||||
{this.generateRows()}
|
||||
</ul>
|
||||
);
|
||||
render() {
|
||||
return (
|
||||
<ul className="grid-field-table-component [ list-group ]">
|
||||
{this.generateHeader()}
|
||||
{this.generateRows()}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the header component.
|
||||
*
|
||||
* Uses the header component passed via the `header` prop if it exists.
|
||||
* Otherwise generates a header from the `data` prop.
|
||||
*
|
||||
* @return object|null
|
||||
*/
|
||||
generateHeader() {
|
||||
if (typeof this.props.header !== 'undefined') {
|
||||
return this.props.header;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the header component.
|
||||
*
|
||||
* Uses the header component passed via the `header` prop if it exists.
|
||||
* Otherwise generates a header from the `data` prop.
|
||||
*
|
||||
* @return object|null
|
||||
*/
|
||||
generateHeader() {
|
||||
if (typeof this.props.header !== 'undefined') {
|
||||
return this.props.header;
|
||||
}
|
||||
|
||||
if (typeof this.props.data !== 'undefined') {
|
||||
// TODO: Generate the header.
|
||||
}
|
||||
|
||||
return null;
|
||||
if (typeof this.props.data !== 'undefined') {
|
||||
// TODO: Generate the header.
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the table rows.
|
||||
*
|
||||
* Uses the components passed via the `rows` props if it exists.
|
||||
* Otherwise generates rows from the `data` prop.
|
||||
*
|
||||
* @return object|null
|
||||
*/
|
||||
generateRows() {
|
||||
if (typeof this.props.rows !== 'undefined') {
|
||||
return this.props.rows;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof this.props.data !== 'undefined') {
|
||||
// TODO: Generate the rows.
|
||||
}
|
||||
|
||||
return null;
|
||||
/**
|
||||
* Generates the table rows.
|
||||
*
|
||||
* Uses the components passed via the `rows` props if it exists.
|
||||
* Otherwise generates rows from the `data` prop.
|
||||
*
|
||||
* @return object|null
|
||||
*/
|
||||
generateRows() {
|
||||
if (typeof this.props.rows !== 'undefined') {
|
||||
return this.props.rows;
|
||||
}
|
||||
|
||||
if (typeof this.props.data !== 'undefined') {
|
||||
// TODO: Generate the rows.
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
GridFieldTableComponent.propTypes = {
|
||||
data: React.PropTypes.object,
|
||||
header: React.PropTypes.object,
|
||||
rows: React.PropTypes.array
|
||||
data: React.PropTypes.object,
|
||||
header: React.PropTypes.object,
|
||||
rows: React.PropTypes.array,
|
||||
};
|
||||
|
||||
export default GridFieldTableComponent;
|
||||
|
@ -2,66 +2,66 @@ jest.dontMock('../index');
|
||||
jest.dontMock('../table');
|
||||
|
||||
const React = require('react'),
|
||||
ReactTestUtils = require('react-addons-test-utils'),
|
||||
GridFieldTableComponent = require('../table.js').default;
|
||||
ReactTestUtils = require('react-addons-test-utils'),
|
||||
GridFieldTableComponent = require('../table.js').default;
|
||||
|
||||
describe('GridFieldTableComponent', () => {
|
||||
var props;
|
||||
var props;
|
||||
|
||||
beforeEach(function () {
|
||||
props = {
|
||||
}
|
||||
beforeEach(function () {
|
||||
props = {
|
||||
}
|
||||
});
|
||||
|
||||
describe('generateHeader()', function () {
|
||||
var gridfield;
|
||||
|
||||
it('should return props.header if it is set', function () {
|
||||
props.header = <div className='header'></div>;
|
||||
|
||||
gridfield = ReactTestUtils.renderIntoDocument(
|
||||
<GridFieldTableComponent {...props} />
|
||||
);
|
||||
|
||||
expect(gridfield.generateHeader().props.className).toBe('header');
|
||||
});
|
||||
|
||||
describe('generateHeader()', function () {
|
||||
var gridfield;
|
||||
it('should generate and return a header from props.data if it is set', function () {
|
||||
|
||||
it('should return props.header if it is set', function () {
|
||||
props.header = <div className='header'></div>;
|
||||
|
||||
gridfield = ReactTestUtils.renderIntoDocument(
|
||||
<GridFieldTableComponent {...props} />
|
||||
);
|
||||
|
||||
expect(gridfield.generateHeader().props.className).toBe('header');
|
||||
});
|
||||
|
||||
it('should generate and return a header from props.data if it is set', function () {
|
||||
|
||||
});
|
||||
|
||||
it('should return null if props.header and props.data are both not set', function () {
|
||||
gridfield = ReactTestUtils.renderIntoDocument(
|
||||
<GridFieldTableComponent {...props} />
|
||||
);
|
||||
|
||||
expect(gridfield.generateHeader()).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateRows()', function () {
|
||||
var gridfield;
|
||||
it('should return null if props.header and props.data are both not set', function () {
|
||||
gridfield = ReactTestUtils.renderIntoDocument(
|
||||
<GridFieldTableComponent {...props} />
|
||||
);
|
||||
|
||||
it('should return props.rows if it is set', function () {
|
||||
props.rows = ['row1'];
|
||||
|
||||
gridfield = ReactTestUtils.renderIntoDocument(
|
||||
<GridFieldTableComponent {...props} />
|
||||
);
|
||||
|
||||
expect(gridfield.generateRows()[0]).toBe('row1');
|
||||
});
|
||||
|
||||
it('should generate and return rows from props.data if it is set', function () {
|
||||
|
||||
});
|
||||
|
||||
it('should return null if props.rows and props.data are both not set', function () {
|
||||
gridfield = ReactTestUtils.renderIntoDocument(
|
||||
<GridFieldTableComponent {...props} />
|
||||
);
|
||||
|
||||
expect(gridfield.generateRows()).toBe(null);
|
||||
});
|
||||
expect(gridfield.generateHeader()).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateRows()', function () {
|
||||
var gridfield;
|
||||
|
||||
it('should return props.rows if it is set', function () {
|
||||
props.rows = ['row1'];
|
||||
|
||||
gridfield = ReactTestUtils.renderIntoDocument(
|
||||
<GridFieldTableComponent {...props} />
|
||||
);
|
||||
|
||||
expect(gridfield.generateRows()[0]).toBe('row1');
|
||||
});
|
||||
|
||||
it('should generate and return rows from props.data if it is set', function () {
|
||||
|
||||
});
|
||||
|
||||
it('should return null if props.rows and props.data are both not set', function () {
|
||||
gridfield = ReactTestUtils.renderIntoDocument(
|
||||
<GridFieldTableComponent {...props} />
|
||||
);
|
||||
|
||||
expect(gridfield.generateRows()).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -3,46 +3,46 @@ import SilverStripeComponent from 'silverstripe-component';
|
||||
|
||||
class HiddenFieldComponent extends SilverStripeComponent {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="field hidden">
|
||||
<input {...this.getInputProps()} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getInputProps() {
|
||||
return {
|
||||
className: ['hidden', this.props.extraClass].join(' '),
|
||||
id: this.props.name,
|
||||
name: this.props.name,
|
||||
onChange: this.props.onChange,
|
||||
type: 'hidden',
|
||||
value: this.props.value,
|
||||
};
|
||||
}
|
||||
|
||||
handleChange() {
|
||||
if (typeof this.props.onChange === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className='field hidden'>
|
||||
<input {...this.getInputProps()} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getInputProps() {
|
||||
return {
|
||||
className: ['hidden', this.props.extraClass].join(' '),
|
||||
id: this.props.name,
|
||||
name: this.props.name,
|
||||
onChange: this.props.onChange,
|
||||
type: 'hidden',
|
||||
value: this.props.value
|
||||
};
|
||||
}
|
||||
|
||||
handleChange(event) {
|
||||
if (typeof this.props.onChange === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onChange();
|
||||
}
|
||||
this.props.onChange();
|
||||
}
|
||||
}
|
||||
|
||||
HiddenFieldComponent.propTypes = {
|
||||
label: React.PropTypes.string,
|
||||
extraClass: React.PropTypes.string,
|
||||
name: React.PropTypes.string.isRequired,
|
||||
onChange: React.PropTypes.func,
|
||||
value: React.PropTypes.string
|
||||
label: React.PropTypes.string,
|
||||
extraClass: React.PropTypes.string,
|
||||
name: React.PropTypes.string.isRequired,
|
||||
onChange: React.PropTypes.func,
|
||||
value: React.PropTypes.string,
|
||||
};
|
||||
|
||||
export default HiddenFieldComponent;
|
||||
|
@ -3,37 +3,39 @@ import SilverStripeComponent from 'silverstripe-component';
|
||||
|
||||
class NorthHeaderBreadcrumbsComponent extends SilverStripeComponent {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="cms-content-header-info">
|
||||
<div className="breadcrumbs-wrapper">
|
||||
<h2 id="page-title-heading">
|
||||
{this.getBreadcrumbs()}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
render() {
|
||||
return (
|
||||
<div className="cms-content-header-info">
|
||||
<div className="breadcrumbs-wrapper">
|
||||
<h2 id="page-title-heading">
|
||||
{this.getBreadcrumbs()}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getBreadcrumbs() {
|
||||
if (typeof this.props.crumbs === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
getBreadcrumbs() {
|
||||
if (typeof this.props.crumbs === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
const breadcrumbs = this.props.crumbs.map((crumb, index, crumbs) => {
|
||||
let component;
|
||||
// If its the last item in the array
|
||||
if (index === crumbs.length - 1) {
|
||||
component = <span key={index} className="crumb last">{crumb.text}</span>;
|
||||
} else {
|
||||
component = [
|
||||
<a key={index} className="cms-panel-link crumb" href={crumb.href}>{crumb.text}</a>,
|
||||
<span className="sep">/</span>,
|
||||
];
|
||||
}
|
||||
return component;
|
||||
});
|
||||
|
||||
var breadcrumbs = this.props.crumbs.map((crumb, index, crumbs) => {
|
||||
// If its the last item in the array
|
||||
if (index === crumbs.length - 1) {
|
||||
return <span key={index} className="crumb last">{crumb.text}</span>;
|
||||
} else {
|
||||
return [
|
||||
<a key={index} className="cms-panel-link crumb" href={crumb.href}>{crumb.text}</a>,
|
||||
<span className="sep">/</span>
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
return breadcrumbs;
|
||||
}
|
||||
return breadcrumbs;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -1,40 +1,40 @@
|
||||
jest.dontMock('../index');
|
||||
|
||||
const React = require('react'),
|
||||
ReactTestUtils = require('react-addons-test-utils'),
|
||||
NorthHeaderBreadcrumbsComponent = require('../index').default;
|
||||
ReactTestUtils = require('react-addons-test-utils'),
|
||||
NorthHeaderBreadcrumbsComponent = require('../index').default;
|
||||
|
||||
describe('NorthHeaderBreadcrumbsComponent', () => {
|
||||
var props;
|
||||
var props;
|
||||
|
||||
beforeEach(() => {
|
||||
props = {};
|
||||
beforeEach(() => {
|
||||
props = {};
|
||||
});
|
||||
|
||||
describe('getBreadcrumbs()', () => {
|
||||
var northHeaderBreadcrumbs;
|
||||
|
||||
it('should convert the props.crumbs array into jsx to be rendered', () => {
|
||||
props.crumbs = [
|
||||
{ text: 'breadcrumb1', href: 'href1'},
|
||||
{ text: 'breadcrumb2', href: 'href2'}
|
||||
];
|
||||
|
||||
northHeaderBreadcrumbs = ReactTestUtils.renderIntoDocument(
|
||||
<NorthHeaderBreadcrumbsComponent {...props} />
|
||||
);
|
||||
|
||||
expect(northHeaderBreadcrumbs.getBreadcrumbs()[0][0].props.children).toBe('breadcrumb1');
|
||||
expect(northHeaderBreadcrumbs.getBreadcrumbs()[0][1].props.children).toBe('/');
|
||||
expect(northHeaderBreadcrumbs.getBreadcrumbs()[1].props.children).toBe('breadcrumb2');
|
||||
});
|
||||
|
||||
describe('getBreadcrumbs()', () => {
|
||||
var northHeaderBreadcrumbs;
|
||||
it('should return null if props.crumbs is not set', () => {
|
||||
northHeaderBreadcrumbs = ReactTestUtils.renderIntoDocument(
|
||||
<NorthHeaderBreadcrumbsComponent {...props} />
|
||||
);
|
||||
|
||||
it('should convert the props.crumbs array into jsx to be rendered', () => {
|
||||
props.crumbs = [
|
||||
{ text: 'breadcrumb1', href: 'href1'},
|
||||
{ text: 'breadcrumb2', href: 'href2'}
|
||||
];
|
||||
|
||||
northHeaderBreadcrumbs = ReactTestUtils.renderIntoDocument(
|
||||
<NorthHeaderBreadcrumbsComponent {...props} />
|
||||
);
|
||||
|
||||
expect(northHeaderBreadcrumbs.getBreadcrumbs()[0][0].props.children).toBe('breadcrumb1');
|
||||
expect(northHeaderBreadcrumbs.getBreadcrumbs()[0][1].props.children).toBe('/');
|
||||
expect(northHeaderBreadcrumbs.getBreadcrumbs()[1].props.children).toBe('breadcrumb2');
|
||||
});
|
||||
|
||||
it('should return null if props.crumbs is not set', () => {
|
||||
northHeaderBreadcrumbs = ReactTestUtils.renderIntoDocument(
|
||||
<NorthHeaderBreadcrumbsComponent {...props} />
|
||||
);
|
||||
|
||||
expect(northHeaderBreadcrumbs.getBreadcrumbs()).toBe(null);
|
||||
});
|
||||
expect(northHeaderBreadcrumbs.getBreadcrumbs()).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -4,26 +4,26 @@ import SilverStripeComponent from 'silverstripe-component';
|
||||
|
||||
class NorthHeaderComponent extends SilverStripeComponent {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="north-header-component">
|
||||
<NorthHeaderBreadcrumbsComponent crumbs={this.getBreadcrumbs()}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div className="north-header-component">
|
||||
<NorthHeaderBreadcrumbsComponent crumbs={this.getBreadcrumbs()} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getBreadcrumbs() {
|
||||
return [
|
||||
{
|
||||
text: 'Campaigns',
|
||||
href: 'admin/campaigns'
|
||||
},
|
||||
{
|
||||
text: 'March release',
|
||||
href: 'admin/campaigns/show/1'
|
||||
}
|
||||
]
|
||||
}
|
||||
getBreadcrumbs() {
|
||||
return [
|
||||
{
|
||||
text: 'Campaigns',
|
||||
href: 'admin/campaigns',
|
||||
},
|
||||
{
|
||||
text: 'March release',
|
||||
href: 'admin/campaigns/show/1',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -3,53 +3,53 @@ import SilverStripeComponent from 'silverstripe-component';
|
||||
|
||||
class TextFieldComponent extends SilverStripeComponent {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
}
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className='field text'>
|
||||
{this.props.label &&
|
||||
<label className='left' htmlFor={'gallery_' + this.props.name}>
|
||||
{this.props.label}
|
||||
</label>
|
||||
}
|
||||
<div className='middleColumn'>
|
||||
<input {...this.getInputProps()} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getInputProps() {
|
||||
return {
|
||||
className: ['text', this.props.extraClass].join(' '),
|
||||
id: `gallery_${this.props.name}`,
|
||||
name: this.props.name,
|
||||
onChange: this.props.onChange,
|
||||
type: 'text',
|
||||
value: this.props.value
|
||||
};
|
||||
}
|
||||
|
||||
handleChange(event) {
|
||||
if (typeof this.props.onChange === 'undefined') {
|
||||
return;
|
||||
render() {
|
||||
return (
|
||||
<div className="field text">
|
||||
{this.props.label &&
|
||||
<label className="left" htmlFor={`gallery_${this.props.name}`}>
|
||||
{this.props.label}
|
||||
</label>
|
||||
}
|
||||
<div className="middleColumn">
|
||||
<input {...this.getInputProps()} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
this.props.onChange();
|
||||
getInputProps() {
|
||||
return {
|
||||
className: ['text', this.props.extraClass].join(' '),
|
||||
id: `gallery_${this.props.name}`,
|
||||
name: this.props.name,
|
||||
onChange: this.props.onChange,
|
||||
type: 'text',
|
||||
value: this.props.value,
|
||||
};
|
||||
}
|
||||
|
||||
handleChange() {
|
||||
if (typeof this.props.onChange === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onChange();
|
||||
}
|
||||
}
|
||||
|
||||
TextFieldComponent.propTypes = {
|
||||
label: React.PropTypes.string,
|
||||
extraClass: React.PropTypes.string,
|
||||
name: React.PropTypes.string.isRequired,
|
||||
onChange: React.PropTypes.func,
|
||||
value: React.PropTypes.string
|
||||
label: React.PropTypes.string,
|
||||
extraClass: React.PropTypes.string,
|
||||
name: React.PropTypes.string.isRequired,
|
||||
onChange: React.PropTypes.func,
|
||||
value: React.PropTypes.string,
|
||||
};
|
||||
|
||||
export default TextFieldComponent;
|
||||
|
@ -8,30 +8,30 @@ import TextFieldComponent from '../';
|
||||
|
||||
describe('TextFieldComponent', function() {
|
||||
|
||||
var props;
|
||||
var props;
|
||||
|
||||
beforeEach(function () {
|
||||
props = {
|
||||
label: '',
|
||||
name: '',
|
||||
value: '',
|
||||
onChange: jest.genMockFunction()
|
||||
};
|
||||
});
|
||||
|
||||
describe('handleChange()', function () {
|
||||
var textField;
|
||||
|
||||
beforeEach(function () {
|
||||
props = {
|
||||
label: '',
|
||||
name: '',
|
||||
value: '',
|
||||
onChange: jest.genMockFunction()
|
||||
};
|
||||
textField = ReactTestUtils.renderIntoDocument(
|
||||
<TextFieldComponent {...props} />
|
||||
);
|
||||
});
|
||||
|
||||
describe('handleChange()', function () {
|
||||
var textField;
|
||||
it('should call the onChange function on props', function () {
|
||||
textField.handleChange();
|
||||
|
||||
beforeEach(function () {
|
||||
textField = ReactTestUtils.renderIntoDocument(
|
||||
<TextFieldComponent {...props} />
|
||||
);
|
||||
});
|
||||
|
||||
it('should call the onChange function on props', function () {
|
||||
textField.handleChange();
|
||||
|
||||
expect(textField.props.onChange.mock.calls.length).toBe(1);
|
||||
});
|
||||
expect(textField.props.onChange.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -6,39 +6,37 @@
|
||||
*/
|
||||
class Config {
|
||||
|
||||
/**
|
||||
* Gets the the config for a specific section.
|
||||
*
|
||||
* @param string key - The section config key.
|
||||
*
|
||||
* @return object|undefined
|
||||
*/
|
||||
static getSection(key) {
|
||||
return window.ss.config.sections[key];
|
||||
}
|
||||
/**
|
||||
* Gets the the config for a specific section.
|
||||
*
|
||||
* @param string key - The section config key.
|
||||
*
|
||||
* @return object|undefined
|
||||
*/
|
||||
static getSection(key) {
|
||||
return window.ss.config.sections[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a de-duped list of routes for top level controllers. E.g. 'assets', 'pages', etc.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
static getTopLevelRoutes() {
|
||||
var topLevelRoutes = [];
|
||||
/**
|
||||
* Gets a de-duped list of routes for top level controllers. E.g. 'assets', 'pages', etc.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
static getTopLevelRoutes() {
|
||||
const topLevelRoutes = [];
|
||||
|
||||
Object.keys(window.ss.config.sections).forEach((key) => {
|
||||
const route = window.ss.config.sections[key].route;
|
||||
const isTopLevelRoute = route.indexOf('/') === -1;
|
||||
const isUnique = topLevelRoutes.indexOf(route) === -1;
|
||||
Object.keys(window.ss.config.sections).forEach((key) => {
|
||||
const route = window.ss.config.sections[key].route;
|
||||
const isTopLevelRoute = route.indexOf('/') === -1;
|
||||
const isUnique = topLevelRoutes.indexOf(route) === -1;
|
||||
|
||||
//console.log(this.getSection(key).route);
|
||||
if (isTopLevelRoute && isUnique) {
|
||||
topLevelRoutes.push(route);
|
||||
}
|
||||
});
|
||||
|
||||
if (isTopLevelRoute && isUnique) {
|
||||
topLevelRoutes.push(route);
|
||||
}
|
||||
});
|
||||
|
||||
return topLevelRoutes;
|
||||
}
|
||||
return topLevelRoutes;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -1,15 +1,17 @@
|
||||
/* globals jest */
|
||||
|
||||
function jQuery() {
|
||||
return {
|
||||
// Add jQuery methods such as 'find', 'change', 'trigger' as needed.
|
||||
};
|
||||
return {
|
||||
// Add jQuery methods such as 'find', 'change', 'trigger' as needed.
|
||||
};
|
||||
}
|
||||
|
||||
var mockAjaxFn = jest.genMockFunction();
|
||||
const mockAjaxFn = jest.genMockFunction();
|
||||
|
||||
mockAjaxFn.mockReturnValue({
|
||||
done: jest.genMockFunction(),
|
||||
fail: jest.genMockFunction(),
|
||||
always: jest.genMockFunction()
|
||||
done: jest.genMockFunction(),
|
||||
fail: jest.genMockFunction(),
|
||||
always: jest.genMockFunction(),
|
||||
});
|
||||
|
||||
jQuery.ajax = mockAjaxFn;
|
||||
|
@ -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() {
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -2,63 +2,62 @@
|
||||
* The register of Redux reducers.
|
||||
* @private
|
||||
*/
|
||||
var register = {};
|
||||
const register = {};
|
||||
|
||||
/**
|
||||
* The central register of Redux reducers for the CMS. All registered reducers are combined when the application boots.
|
||||
* The central register of Redux reducers for the CMS.
|
||||
* All registered reducers are combined when the application boots.
|
||||
*/
|
||||
class ReducerRegister {
|
||||
|
||||
/**
|
||||
* Adds a reducer to the register.
|
||||
*
|
||||
* @param string key - The key to register the reducer against.
|
||||
* @param object reducer - Redux reducer.
|
||||
*/
|
||||
add(key, reducer) {
|
||||
if (typeof register[key] !== 'undefined') {
|
||||
throw new Error(`Reducer already exists at '${key}'`);
|
||||
}
|
||||
|
||||
register[key] = reducer;
|
||||
/**
|
||||
* Adds a reducer to the register.
|
||||
*
|
||||
* @param string key - The key to register the reducer against.
|
||||
* @param object reducer - Redux reducer.
|
||||
*/
|
||||
add(key, reducer) {
|
||||
if (typeof register[key] !== 'undefined') {
|
||||
throw new Error(`Reducer already exists at '${key}'`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all reducers from the register.
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
getAll() {
|
||||
return register;
|
||||
}
|
||||
register[key] = reducer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a reducer from the register.
|
||||
*
|
||||
* @param string [key] - The key the reducer is registered against.
|
||||
*
|
||||
* @return object|undefined
|
||||
*/
|
||||
getByKey(key) {
|
||||
return register[key];
|
||||
}
|
||||
/**
|
||||
* Gets all reducers from the register.
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
getAll() {
|
||||
return register;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a reducer from the register.
|
||||
*
|
||||
* @param string [key] - The key the reducer is registered against.
|
||||
*
|
||||
* @return object|undefined
|
||||
*/
|
||||
getByKey(key) {
|
||||
return register[key];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes a reducer from the register.
|
||||
*
|
||||
* @param string key - The key the reducer is registered against.
|
||||
*/
|
||||
remove(key) {
|
||||
delete register[key];
|
||||
}
|
||||
/**
|
||||
* Removes a reducer from the register.
|
||||
*
|
||||
* @param string key - The key the reducer is registered against.
|
||||
*/
|
||||
remove(key) {
|
||||
delete register[key];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Create an instance to export. The same instance is exported to
|
||||
// each script which imports the reducerRegister. This means the
|
||||
// same register is available throughout the application.
|
||||
let reducerRegister = new ReducerRegister();
|
||||
const reducerRegister = new ReducerRegister();
|
||||
|
||||
export default reducerRegister;
|
||||
|
@ -8,48 +8,49 @@ import FormBuilder from 'components/form-builder/index';
|
||||
|
||||
class CampaignAdminContainer extends SilverStripeComponent {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.addCampaign = this.addCampaign.bind(this);
|
||||
}
|
||||
this.addCampaign = this.addCampaign.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
const schemaUrl = this.props.config.forms.editForm.schemaUrl;
|
||||
render() {
|
||||
const schemaUrl = this.props.config.forms.editForm.schemaUrl;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<NorthHeader />
|
||||
<FormAction
|
||||
label={i18n._t('Campaigns.ADDCAMPAIGN')}
|
||||
icon={'plus-circled'}
|
||||
handleClick={this.addCampaign} />
|
||||
<FormBuilder schemaUrl={schemaUrl} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<NorthHeader />
|
||||
<FormAction
|
||||
label={i18n._t('Campaigns.ADDCAMPAIGN')}
|
||||
icon={'plus-circled'}
|
||||
handleClick={this.addCampaign}
|
||||
/>
|
||||
<FormBuilder schemaUrl={schemaUrl} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
addCampaign() {
|
||||
//Add campaign
|
||||
}
|
||||
addCampaign() {
|
||||
// Add campaign
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
CampaignAdminContainer.propTypes = {
|
||||
config: React.PropTypes.shape({
|
||||
forms: React.PropTypes.shape({
|
||||
editForm: React.PropTypes.shape({
|
||||
schemaUrl: React.PropTypes.string
|
||||
})
|
||||
})
|
||||
config: React.PropTypes.shape({
|
||||
forms: React.PropTypes.shape({
|
||||
editForm: React.PropTypes.shape({
|
||||
schemaUrl: React.PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
sectionConfigKey: React.PropTypes.string.isRequired
|
||||
}),
|
||||
sectionConfigKey: React.PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
config: state.config.sections[ownProps.sectionConfigKey]
|
||||
}
|
||||
return {
|
||||
config: state.config.sections[ownProps.sectionConfigKey],
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(CampaignAdminContainer);
|
||||
|
@ -1,24 +1,22 @@
|
||||
import reducerRegister from 'reducer-register';
|
||||
import $ from 'jQuery';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import CampaignAdmin from './controller';
|
||||
|
||||
$.entwine('ss', function ($) {
|
||||
|
||||
$('.cms-content.CampaignAdmin').entwine({
|
||||
onadd: function () {
|
||||
ReactDOM.render(
|
||||
<Provider store={window.store}>
|
||||
<CampaignAdmin sectionConfigKey='CampaignAdmin' />
|
||||
</Provider>
|
||||
, this[0]);
|
||||
},
|
||||
|
||||
onremove: function () {
|
||||
ReactDOM.unmountComponentAtNode(this[0]);
|
||||
}
|
||||
});
|
||||
// eslint-disable-next-line no-shadow
|
||||
$.entwine('ss', ($) => {
|
||||
$('.cms-content.CampaignAdmin').entwine({
|
||||
onadd() {
|
||||
ReactDOM.render(
|
||||
<Provider store={window.store}>
|
||||
<CampaignAdmin sectionConfigKey="CampaignAdmin" />
|
||||
</Provider>
|
||||
, this[0]);
|
||||
},
|
||||
|
||||
onremove() {
|
||||
ReactDOM.unmountComponentAtNode(this[0]);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -6,73 +6,77 @@ es6promise.polyfill();
|
||||
* @see https://github.com/github/fetch#handling-http-error-statuses
|
||||
*/
|
||||
function checkStatus(response) {
|
||||
let ret;
|
||||
let error;
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
return response
|
||||
ret = response;
|
||||
} else {
|
||||
var error = new Error(response.statusText)
|
||||
error.response = response
|
||||
throw error
|
||||
error = new Error(response.statusText);
|
||||
error.response = response;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
class SilverStripeBackend {
|
||||
|
||||
constructor() {
|
||||
// Allow mocking
|
||||
this.fetch = fetch;
|
||||
}
|
||||
constructor() {
|
||||
// Allow mocking
|
||||
this.fetch = fetch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a network request using the GET HTTP verb.
|
||||
*
|
||||
* @param string url - Endpoint URL.
|
||||
* @return object - Promise
|
||||
*/
|
||||
get(url) {
|
||||
return this.fetch(url, { method: 'get', credentials: 'same-origin' })
|
||||
.then(checkStatus);
|
||||
}
|
||||
/**
|
||||
* Makes a network request using the GET HTTP verb.
|
||||
*
|
||||
* @param string url - Endpoint URL.
|
||||
* @return object - Promise
|
||||
*/
|
||||
get(url) {
|
||||
return this.fetch(url, { method: 'get', credentials: 'same-origin' })
|
||||
.then(checkStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a network request using the POST HTTP verb.
|
||||
*
|
||||
* @param string url - Endpoint URL.
|
||||
* @param object data - Data to send with the request.
|
||||
* @return object - Promise
|
||||
*/
|
||||
post(url, data) {
|
||||
return this.fetch(url, { method: 'post', credentials: 'same-origin', body: data })
|
||||
.then(checkStatus);
|
||||
}
|
||||
/**
|
||||
* Makes a network request using the POST HTTP verb.
|
||||
*
|
||||
* @param string url - Endpoint URL.
|
||||
* @param object data - Data to send with the request.
|
||||
* @return object - Promise
|
||||
*/
|
||||
post(url, data) {
|
||||
return this.fetch(url, { method: 'post', credentials: 'same-origin', body: data })
|
||||
.then(checkStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a newtwork request using the PUT HTTP verb.
|
||||
*
|
||||
* @param string url - Endpoint URL.
|
||||
* @param object data - Data to send with the request.
|
||||
* @return object - Promise
|
||||
*/
|
||||
put(url, data) {
|
||||
return this.fetch(url, { method: 'put', credentials: 'same-origin', body: data })
|
||||
.then(checkStatus);
|
||||
}
|
||||
/**
|
||||
* Makes a newtwork request using the PUT HTTP verb.
|
||||
*
|
||||
* @param string url - Endpoint URL.
|
||||
* @param object data - Data to send with the request.
|
||||
* @return object - Promise
|
||||
*/
|
||||
put(url, data) {
|
||||
return this.fetch(url, { method: 'put', credentials: 'same-origin', body: data })
|
||||
.then(checkStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a newtwork request using the DELETE HTTP verb.
|
||||
*
|
||||
* @param string url - Endpoint URL.
|
||||
* @param object data - Data to send with the request.
|
||||
* @return object - Promise
|
||||
*/
|
||||
delete(url, data) {
|
||||
return this.fetch(url, { method: 'delete', credentials: 'same-origin', body: data })
|
||||
.then(checkStatus);
|
||||
}
|
||||
/**
|
||||
* Makes a newtwork request using the DELETE HTTP verb.
|
||||
*
|
||||
* @param string url - Endpoint URL.
|
||||
* @param object data - Data to send with the request.
|
||||
* @return object - Promise
|
||||
*/
|
||||
delete(url, data) {
|
||||
return this.fetch(url, { method: 'delete', credentials: 'same-origin', body: data })
|
||||
.then(checkStatus);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Exported as a singleton so we can implement things like
|
||||
// global caching and request batching at some stage.
|
||||
let backend = new SilverStripeBackend();
|
||||
const backend = new SilverStripeBackend();
|
||||
|
||||
export default backend;
|
||||
|
@ -2,54 +2,107 @@
|
||||
* @file Base component which all SilverStripe ReactJS components should extend from.
|
||||
*/
|
||||
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import $ from '../../../javascript/src/jQuery';
|
||||
|
||||
class SilverStripeComponent extends Component {
|
||||
|
||||
/**
|
||||
* @func componentDidMount
|
||||
* @desc Bind event listeners which are triggered by legacy-land JavaScript.
|
||||
* This lets us update the component when something happens in the outside world.
|
||||
*/
|
||||
componentDidMount() {
|
||||
if (typeof this.props.cmsEvents === 'undefined') {
|
||||
return;
|
||||
}
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// Save some props for later. When we come to unbind these listeners
|
||||
// there's no guarantee these props will be the same or even present.
|
||||
this.cmsEvents = this.props.cmsEvents;
|
||||
// Setup component routing.
|
||||
if (typeof this.props.route !== 'undefined') {
|
||||
// The component's render method gets switched based on the current path.
|
||||
// If the current path matches the component's route, the component is displayed.
|
||||
// Otherwise the component's render method returns null, resulting in
|
||||
// the component not rendering.
|
||||
this._render = this.render;
|
||||
|
||||
for (let cmsEvent in this.cmsEvents) {
|
||||
$(document).on(cmsEvent, this.cmsEvents[cmsEvent].bind(this));
|
||||
}
|
||||
}
|
||||
this.render = () => {
|
||||
let component = null;
|
||||
|
||||
/**
|
||||
* @func componentWillUnmount
|
||||
* @desc Unbind the event listeners we added in componentDidMount.
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
for (let cmsEvent in this.cmsEvents) {
|
||||
$(document).off(cmsEvent);
|
||||
}
|
||||
}
|
||||
if (this.isComponentRoute()) {
|
||||
component = this._render();
|
||||
}
|
||||
|
||||
/**
|
||||
* @func emitCmsEvent
|
||||
* @param string componentEvent - Namespace component event e.g. 'my-component.title-changed'.
|
||||
* @param object|string|array|number [data] - Some data to pass with the event.
|
||||
* @desc Notifies legacy-land something has changed within our component.
|
||||
*/
|
||||
emitCmsEvent(componentEvent, data) {
|
||||
$(document).trigger(componentEvent, data);
|
||||
}
|
||||
return component;
|
||||
};
|
||||
|
||||
window.ss.router(this.props.route, (ctx, next) => {
|
||||
this.handleEnterRoute(ctx, next);
|
||||
});
|
||||
window.ss.router.exit(this.props.route, (ctx, next) => {
|
||||
this.handleExitRoute(ctx, next);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (typeof this.props.cmsEvents === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save some props for later. When we come to unbind these listeners
|
||||
// there's no guarantee these props will be the same or even present.
|
||||
this.cmsEvents = this.props.cmsEvents;
|
||||
|
||||
// Bind event listeners which are triggered by legacy-land JavaScript.
|
||||
// This lets us update the component when something happens in the outside world.
|
||||
for (const cmsEvent in this.cmsEvents) {
|
||||
if ({}.hasOwnProperty.call(this.cmsEvents, cmsEvent)) {
|
||||
$(document).on(cmsEvent, this.cmsEvents[cmsEvent].bind(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// Unbind the event listeners we added in componentDidMount.
|
||||
for (const cmsEvent in this.cmsEvents) {
|
||||
if ({}.hasOwnProperty.call(this.cmsEvents, cmsEvent)) {
|
||||
$(document).off(cmsEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleEnterRoute(ctx, next) {
|
||||
next();
|
||||
}
|
||||
|
||||
handleExitRoute(ctx, next) {
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the component should be rended on the current path.
|
||||
*
|
||||
* @param object [params] - If a params object is passed in it's
|
||||
* mutated by page.js to contains route parans like ':id'.
|
||||
*/
|
||||
isComponentRoute(params = {}) {
|
||||
if (typeof this.props.route === 'undefined') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const route = new window.ss.router.Route(this.props.route);
|
||||
|
||||
return route.match(window.ss.router.current, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies legacy-land something has changed within our component.
|
||||
*
|
||||
* @param string componentEvent - Namespace component event e.g. 'my-component.title-changed'.
|
||||
* @param object|string|array|number [data] - Some data to pass with the event.
|
||||
*/
|
||||
emitCmsEvent(componentEvent, data) {
|
||||
$(document).trigger(componentEvent, data);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
SilverStripeComponent.propTypes = {
|
||||
'cmsEvents': React.PropTypes.object
|
||||
cmsEvents: React.PropTypes.object,
|
||||
route: React.PropTypes.string,
|
||||
};
|
||||
|
||||
export default SilverStripeComponent;
|
||||
|
@ -35,24 +35,24 @@ var $window = $( window ),
|
||||
// URL as well as some other commonly used sub-parts. When used with RegExp.exec()
|
||||
// or String.match, it parses the URL into a results array that looks like this:
|
||||
//
|
||||
// [0]: http://jblas:password@mycompany.com:8080/mail/inbox?msg=1234&type=unread#msg-content
|
||||
// [1]: http://jblas:password@mycompany.com:8080/mail/inbox?msg=1234&type=unread
|
||||
// [2]: http://jblas:password@mycompany.com:8080/mail/inbox
|
||||
// [3]: http://jblas:password@mycompany.com:8080
|
||||
// [4]: http:
|
||||
// [5]: //
|
||||
// [6]: jblas:password@mycompany.com:8080
|
||||
// [7]: jblas:password
|
||||
// [8]: jblas
|
||||
// [9]: password
|
||||
// [10]: mycompany.com:8080
|
||||
// [11]: mycompany.com
|
||||
// [12]: 8080
|
||||
// [13]: /mail/inbox
|
||||
// [14]: /mail/
|
||||
// [15]: inbox
|
||||
// [16]: ?msg=1234&type=unread
|
||||
// [17]: #msg-content
|
||||
// [0]: http://jblas:password@mycompany.com:8080/mail/inbox?msg=1234&type=unread#msg-content
|
||||
// [1]: http://jblas:password@mycompany.com:8080/mail/inbox?msg=1234&type=unread
|
||||
// [2]: http://jblas:password@mycompany.com:8080/mail/inbox
|
||||
// [3]: http://jblas:password@mycompany.com:8080
|
||||
// [4]: http:
|
||||
// [5]: //
|
||||
// [6]: jblas:password@mycompany.com:8080
|
||||
// [7]: jblas:password
|
||||
// [8]: jblas
|
||||
// [9]: password
|
||||
// [10]: mycompany.com:8080
|
||||
// [11]: mycompany.com
|
||||
// [12]: 8080
|
||||
// [13]: /mail/inbox
|
||||
// [14]: /mail/
|
||||
// [15]: inbox
|
||||
// [16]: ?msg=1234&type=unread
|
||||
// [17]: #msg-content
|
||||
//
|
||||
urlParseRE: /^(((([^:\/#\?]+:)?(?:(\/\/)((?:(([^:@\/#\?]+)(?:\:([^:@\/#\?]+))?)@)?(([^:\/#\?\]\[]+|\[[^\/\]@#?]+\])(?:\:([0-9]+))?))?)?)?((\/?(?:[^\/\?#]+\/+)*)([^\?#]*)))?(\?[^#]+)?)(#.*)?/,
|
||||
|
||||
@ -72,23 +72,23 @@ var $window = $( window ),
|
||||
// like all other browsers do, so we normalize everything so its consistent
|
||||
// no matter what browser we're running on.
|
||||
return {
|
||||
href: matches[ 0 ] || "",
|
||||
href: matches[ 0 ] || "",
|
||||
hrefNoHash: matches[ 1 ] || "",
|
||||
hrefNoSearch: matches[ 2 ] || "",
|
||||
domain: matches[ 3 ] || "",
|
||||
protocol: matches[ 4 ] || "",
|
||||
domain: matches[ 3 ] || "",
|
||||
protocol: matches[ 4 ] || "",
|
||||
doubleSlash: matches[ 5 ] || "",
|
||||
authority: matches[ 6 ] || "",
|
||||
username: matches[ 8 ] || "",
|
||||
password: matches[ 9 ] || "",
|
||||
host: matches[ 10 ] || "",
|
||||
hostname: matches[ 11 ] || "",
|
||||
port: matches[ 12 ] || "",
|
||||
pathname: matches[ 13 ] || "",
|
||||
directory: matches[ 14 ] || "",
|
||||
filename: matches[ 15 ] || "",
|
||||
search: matches[ 16 ] || "",
|
||||
hash: matches[ 17 ] || ""
|
||||
authority: matches[ 6 ] || "",
|
||||
username: matches[ 8 ] || "",
|
||||
password: matches[ 9 ] || "",
|
||||
host: matches[ 10 ] || "",
|
||||
hostname: matches[ 11 ] || "",
|
||||
port: matches[ 12 ] || "",
|
||||
pathname: matches[ 13 ] || "",
|
||||
directory: matches[ 14 ] || "",
|
||||
filename: matches[ 15 ] || "",
|
||||
search: matches[ 16 ] || "",
|
||||
hash: matches[ 17 ] || ""
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -1,3 +1,3 @@
|
||||
export default {
|
||||
SET_CONFIG: 'SET_CONFIG'
|
||||
}
|
||||
SET_CONFIG: 'SET_CONFIG',
|
||||
};
|
||||
|
@ -6,10 +6,9 @@ import ACTION_TYPES from './action-types';
|
||||
* @param object config
|
||||
*/
|
||||
export function setConfig(config) {
|
||||
return (dispatch, getState) => {
|
||||
return dispatch({
|
||||
type: ACTION_TYPES.SET_CONFIG,
|
||||
payload: { config }
|
||||
});
|
||||
}
|
||||
return (dispatch) =>
|
||||
dispatch({
|
||||
type: ACTION_TYPES.SET_CONFIG,
|
||||
payload: { config },
|
||||
});
|
||||
}
|
||||
|
@ -2,17 +2,15 @@ import deepFreeze from 'deep-freeze';
|
||||
import ACTION_TYPES from './action-types';
|
||||
|
||||
function configReducer(state = {}, action) {
|
||||
switch (action.type) {
|
||||
|
||||
switch (action.type) {
|
||||
case ACTION_TYPES.SET_CONFIG:
|
||||
return deepFreeze(Object.assign({}, state, action.payload.config));
|
||||
|
||||
case ACTION_TYPES.SET_CONFIG:
|
||||
return deepFreeze(Object.assign({}, state, action.payload.config));
|
||||
|
||||
default:
|
||||
return state;
|
||||
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export default configReducer;
|
||||
|
@ -1,11 +1,11 @@
|
||||
export default {
|
||||
CREATE_RECORD: 'CREATE_RECORD',
|
||||
UPDATE_RECORD: 'UPDATE_RECORD',
|
||||
DELETE_RECORD: 'DELETE_RECORD',
|
||||
FETCH_RECORDS_REQUEST: 'FETCH_RECORDS_REQUEST',
|
||||
FETCH_RECORDS_FAILURE: 'FETCH_RECORDS_FAILURE',
|
||||
FETCH_RECORDS_SUCCESS: 'FETCH_RECORDS_SUCCESS',
|
||||
DELETE_RECORD_REQUEST: 'DELETE_RECORD_REQUEST',
|
||||
DELETE_RECORD_FAILURE: 'DELETE_RECORD_FAILURE',
|
||||
DELETE_RECORD_SUCCESS: 'DELETE_RECORD_SUCCESS'
|
||||
CREATE_RECORD: 'CREATE_RECORD',
|
||||
UPDATE_RECORD: 'UPDATE_RECORD',
|
||||
DELETE_RECORD: 'DELETE_RECORD',
|
||||
FETCH_RECORDS_REQUEST: 'FETCH_RECORDS_REQUEST',
|
||||
FETCH_RECORDS_FAILURE: 'FETCH_RECORDS_FAILURE',
|
||||
FETCH_RECORDS_SUCCESS: 'FETCH_RECORDS_SUCCESS',
|
||||
DELETE_RECORD_REQUEST: 'DELETE_RECORD_REQUEST',
|
||||
DELETE_RECORD_FAILURE: 'DELETE_RECORD_FAILURE',
|
||||
DELETE_RECORD_SUCCESS: 'DELETE_RECORD_SUCCESS',
|
||||
};
|
||||
|
@ -1,5 +1,4 @@
|
||||
import ACTION_TYPES from './action-types';
|
||||
import fetch from 'isomorphic-fetch';
|
||||
import backend from 'silverstripe-backend.js';
|
||||
|
||||
/**
|
||||
@ -14,8 +13,8 @@ import backend from 'silverstripe-backend.js';
|
||||
* @return string
|
||||
*/
|
||||
function populate(str, params) {
|
||||
let names = ['id'];
|
||||
return names.reduce((str, name) => str.replace(`:${name}`, params[name]), str);
|
||||
const names = ['id'];
|
||||
return names.reduce((acc, name) => acc.replace(`:${name}`, params[name]), str);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -26,19 +25,27 @@ function populate(str, params) {
|
||||
* @param string url API endpoint
|
||||
*/
|
||||
export function fetchRecords(recordType, method, url) {
|
||||
let payload = {recordType: recordType};
|
||||
url = populate(url, payload);
|
||||
return (dispatch, getState) => {
|
||||
dispatch ({type: ACTION_TYPES.FETCH_RECORDS_REQUEST, payload: payload});
|
||||
return backend[method.toLowerCase()](url)
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
dispatch({type: ACTION_TYPES.FETCH_RECORDS_SUCCESS, payload: {recordType: recordType, data: json}})
|
||||
})
|
||||
.catch((err) => {
|
||||
dispatch({type: ACTION_TYPES.FETCH_RECORDS_FAILURE, payload: {error: err, recordType: recordType}})
|
||||
});
|
||||
}
|
||||
const payload = { recordType };
|
||||
return (dispatch) => {
|
||||
dispatch({
|
||||
type: ACTION_TYPES.FETCH_RECORDS_REQUEST,
|
||||
payload,
|
||||
});
|
||||
return backend[method.toLowerCase()](populate(url, payload))
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
dispatch({
|
||||
type: ACTION_TYPES.FETCH_RECORDS_SUCCESS,
|
||||
payload: { recordType, data: json },
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
dispatch({
|
||||
type: ACTION_TYPES.FETCH_RECORDS_FAILURE,
|
||||
payload: { error: err, recordType },
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -50,16 +57,24 @@ export function fetchRecords(recordType, method, url) {
|
||||
* @param string url API endpoint
|
||||
*/
|
||||
export function deleteRecord(recordType, id, method, url) {
|
||||
let payload = {recordType: recordType, id: id};
|
||||
url = populate(url, payload);
|
||||
return (dispatch, getState) => {
|
||||
dispatch ({type: ACTION_TYPES.DELETE_RECORD_REQUEST, payload: payload});
|
||||
return backend[method.toLowerCase()](url)
|
||||
.then(json => {
|
||||
dispatch({type: ACTION_TYPES.DELETE_RECORD_SUCCESS, payload: {recordType: recordType, id: id}})
|
||||
})
|
||||
.catch((err) => {
|
||||
dispatch({type: ACTION_TYPES.DELETE_RECORD_FAILURE, payload: {error: err, recordType: recordType, id: id}})
|
||||
});
|
||||
}
|
||||
const payload = { recordType, id };
|
||||
return (dispatch) => {
|
||||
dispatch({
|
||||
type: ACTION_TYPES.DELETE_RECORD_REQUEST,
|
||||
payload,
|
||||
});
|
||||
return backend[method.toLowerCase()](populate(url, payload))
|
||||
.then(() => {
|
||||
dispatch({
|
||||
type: ACTION_TYPES.DELETE_RECORD_SUCCESS,
|
||||
payload: { recordType, id },
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
dispatch({
|
||||
type: ACTION_TYPES.DELETE_RECORD_FAILURE,
|
||||
payload: { error: err, recordType, id },
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -5,59 +5,58 @@ const initialState = {
|
||||
};
|
||||
|
||||
function recordsReducer(state = initialState, action) {
|
||||
let records;
|
||||
let recordType;
|
||||
let records;
|
||||
let recordType;
|
||||
|
||||
switch (action.type) {
|
||||
switch (action.type) {
|
||||
|
||||
case ACTION_TYPES.CREATE_RECORD:
|
||||
return deepFreeze(Object.assign({}, state, {
|
||||
case ACTION_TYPES.CREATE_RECORD:
|
||||
return deepFreeze(Object.assign({}, state, {
|
||||
|
||||
}));
|
||||
}));
|
||||
|
||||
case ACTION_TYPES.UPDATE_RECORD:
|
||||
return deepFreeze(Object.assign({}, state, {
|
||||
case ACTION_TYPES.UPDATE_RECORD:
|
||||
return deepFreeze(Object.assign({}, state, {
|
||||
|
||||
}));
|
||||
}));
|
||||
|
||||
case ACTION_TYPES.DELETE_RECORD:
|
||||
return deepFreeze(Object.assign({}, state, {
|
||||
case ACTION_TYPES.DELETE_RECORD:
|
||||
return deepFreeze(Object.assign({}, state, {
|
||||
|
||||
}));
|
||||
}));
|
||||
|
||||
case ACTION_TYPES.FETCH_RECORDS_REQUEST:
|
||||
return state;
|
||||
case ACTION_TYPES.FETCH_RECORDS_REQUEST:
|
||||
return state;
|
||||
|
||||
case ACTION_TYPES.FETCH_RECORDS_FAILURE:
|
||||
return state;
|
||||
case ACTION_TYPES.FETCH_RECORDS_FAILURE:
|
||||
return state;
|
||||
|
||||
case ACTION_TYPES.FETCH_RECORDS_SUCCESS:
|
||||
recordType = action.payload.recordType;
|
||||
// TODO Automatic pluralisation from recordType
|
||||
records = action.payload.data._embedded[recordType + 's'];
|
||||
return deepFreeze(Object.assign({}, state, {
|
||||
[recordType]: records
|
||||
}));
|
||||
case ACTION_TYPES.FETCH_RECORDS_SUCCESS:
|
||||
recordType = action.payload.recordType;
|
||||
// TODO Automatic pluralisation from recordType
|
||||
records = action.payload.data._embedded[`${recordType}s`];
|
||||
return deepFreeze(Object.assign({}, state, {
|
||||
[recordType]: records,
|
||||
}));
|
||||
|
||||
case ACTION_TYPES.DELETE_RECORD_REQUEST:
|
||||
return state;
|
||||
case ACTION_TYPES.DELETE_RECORD_REQUEST:
|
||||
return state;
|
||||
|
||||
case ACTION_TYPES.DELETE_RECORD_FAILURE:
|
||||
return state;
|
||||
case ACTION_TYPES.DELETE_RECORD_FAILURE:
|
||||
return state;
|
||||
|
||||
case ACTION_TYPES.DELETE_RECORD_SUCCESS:
|
||||
recordType = action.payload.recordType;
|
||||
records = state[recordType]
|
||||
.filter(record => record.ID != action.payload.id)
|
||||
case ACTION_TYPES.DELETE_RECORD_SUCCESS:
|
||||
recordType = action.payload.recordType;
|
||||
records = state[recordType]
|
||||
.filter(record => record.ID !== action.payload.id);
|
||||
|
||||
return deepFreeze(Object.assign({}, state, {
|
||||
[recordType]: records
|
||||
}));
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
return deepFreeze(Object.assign({}, state, {
|
||||
[recordType]: records,
|
||||
}));
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default recordsReducer;
|
||||
|
@ -3,34 +3,34 @@ jest.dontMock('../reducer');
|
||||
jest.dontMock('../action-types');
|
||||
|
||||
var recordsReducer = require('../reducer').default,
|
||||
ACTION_TYPES = require('../action-types').default;
|
||||
ACTION_TYPES = require('../action-types').default;
|
||||
|
||||
describe('recordsReducer', () => {
|
||||
|
||||
describe('DELETE_RECORD_SUCCESS', () => {
|
||||
const initialState = {
|
||||
TypeA: [
|
||||
{ID: 1},
|
||||
{ID: 2}
|
||||
],
|
||||
TypeB: [
|
||||
{ID: 1},
|
||||
{ID: 2}
|
||||
]
|
||||
};
|
||||
describe('DELETE_RECORD_SUCCESS', () => {
|
||||
const initialState = {
|
||||
TypeA: [
|
||||
{ID: 1},
|
||||
{ID: 2}
|
||||
],
|
||||
TypeB: [
|
||||
{ID: 1},
|
||||
{ID: 2}
|
||||
]
|
||||
};
|
||||
|
||||
it('removes records from the declared type', () => {
|
||||
const nextState = recordsReducer(initialState, {
|
||||
type: ACTION_TYPES.DELETE_RECORD_SUCCESS,
|
||||
payload: { recordType: 'TypeA', id: 2 }
|
||||
});
|
||||
it('removes records from the declared type', () => {
|
||||
const nextState = recordsReducer(initialState, {
|
||||
type: ACTION_TYPES.DELETE_RECORD_SUCCESS,
|
||||
payload: { recordType: 'TypeA', id: 2 }
|
||||
});
|
||||
|
||||
expect(nextState.TypeA.length).toBe(1);
|
||||
expect(nextState.TypeA[0].ID).toBe(1);
|
||||
expect(nextState.TypeB.length).toBe(2);
|
||||
expect(nextState.TypeB[0].ID).toBe(1);
|
||||
expect(nextState.TypeB[1].ID).toBe(2);
|
||||
})
|
||||
});
|
||||
expect(nextState.TypeA.length).toBe(1);
|
||||
expect(nextState.TypeA[0].ID).toBe(1);
|
||||
expect(nextState.TypeB.length).toBe(2);
|
||||
expect(nextState.TypeB[0].ID).toBe(1);
|
||||
expect(nextState.TypeB[1].ID).toBe(2);
|
||||
})
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
const ACTION_TYPES = {
|
||||
SET_SCHEMA: 'SET_SCHEMA'
|
||||
SET_SCHEMA: 'SET_SCHEMA',
|
||||
};
|
||||
|
||||
export default ACTION_TYPES;
|
||||
|
@ -6,10 +6,9 @@ import ACTION_TYPES from './action-types';
|
||||
* @param string schema - JSON schema for the layout.
|
||||
*/
|
||||
export function setSchema(schema) {
|
||||
return (dispatch, getState) => {
|
||||
return dispatch ({
|
||||
type: ACTION_TYPES.SET_SCHEMA,
|
||||
payload: schema
|
||||
});
|
||||
}
|
||||
return (dispatch) =>
|
||||
dispatch({
|
||||
type: ACTION_TYPES.SET_SCHEMA,
|
||||
payload: schema,
|
||||
});
|
||||
}
|
||||
|
@ -4,15 +4,14 @@ import ACTION_TYPES from './action-types';
|
||||
const initialState = deepFreeze({});
|
||||
|
||||
export default function schemaReducer(state = initialState, action = null) {
|
||||
switch (action.type) {
|
||||
|
||||
switch (action.type) {
|
||||
|
||||
case ACTION_TYPES.SET_SCHEMA:
|
||||
const id = action.payload.schema.schema_url;
|
||||
return deepFreeze(Object.assign({}, state, {[id]: action.payload}));
|
||||
|
||||
default:
|
||||
return state;
|
||||
case ACTION_TYPES.SET_SCHEMA: {
|
||||
const id = action.payload.schema.schema_url;
|
||||
return deepFreeze(Object.assign({}, state, { [id]: action.payload }));
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
@ -7,18 +7,18 @@ import ACTION_TYPES from '../action-types';
|
||||
|
||||
describe('schemaReducer', () => {
|
||||
|
||||
describe('SET_SCHEMA', () => {
|
||||
describe('SET_SCHEMA', () => {
|
||||
|
||||
it('should create a new form', () => {
|
||||
const initialState = { };
|
||||
const serverResponse = { id: 'TestForm', schema_url: 'URL' };
|
||||
it('should create a new form', () => {
|
||||
const initialState = { };
|
||||
const serverResponse = { id: 'TestForm', schema_url: 'URL' };
|
||||
|
||||
const nextState = schemaReducer(initialState, {
|
||||
type: ACTION_TYPES.SET_SCHEMA,
|
||||
payload: { schema: serverResponse }
|
||||
});
|
||||
const nextState = schemaReducer(initialState, {
|
||||
type: ACTION_TYPES.SET_SCHEMA,
|
||||
payload: { schema: serverResponse }
|
||||
});
|
||||
|
||||
expect(nextState.URL.schema.id).toBe('TestForm');
|
||||
});
|
||||
expect(nextState.URL.schema.id).toBe('TestForm');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -4,41 +4,41 @@ var reducerRegister = require('../reducer-register').default;
|
||||
|
||||
describe('ReducerRegister', () => {
|
||||
|
||||
var reducer = () => null;
|
||||
var reducer = () => null;
|
||||
|
||||
it('should add a reducer to the register', () => {
|
||||
expect(reducerRegister.getAll().test).toBe(undefined);
|
||||
it('should add a reducer to the register', () => {
|
||||
expect(reducerRegister.getAll().test).toBe(undefined);
|
||||
|
||||
reducerRegister.add('test', reducer);
|
||||
expect(reducerRegister.getAll().test).toBe(reducer);
|
||||
reducerRegister.add('test', reducer);
|
||||
expect(reducerRegister.getAll().test).toBe(reducer);
|
||||
|
||||
reducerRegister.remove('test');
|
||||
});
|
||||
reducerRegister.remove('test');
|
||||
});
|
||||
|
||||
it('should remove a reducer from the register', () => {
|
||||
reducerRegister.add('test', reducer);
|
||||
expect(reducerRegister.getAll().test).toBe(reducer);
|
||||
it('should remove a reducer from the register', () => {
|
||||
reducerRegister.add('test', reducer);
|
||||
expect(reducerRegister.getAll().test).toBe(reducer);
|
||||
|
||||
reducerRegister.remove('test');
|
||||
expect(reducerRegister.getAll().test).toBe(undefined);
|
||||
});
|
||||
reducerRegister.remove('test');
|
||||
expect(reducerRegister.getAll().test).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should get all reducers from the register', () => {
|
||||
reducerRegister.add('test1', reducer);
|
||||
reducerRegister.add('test2', reducer);
|
||||
it('should get all reducers from the register', () => {
|
||||
reducerRegister.add('test1', reducer);
|
||||
reducerRegister.add('test2', reducer);
|
||||
|
||||
expect(reducerRegister.getAll().test1).toBe(reducer);
|
||||
expect(reducerRegister.getAll().test2).toBe(reducer);
|
||||
expect(reducerRegister.getAll().test1).toBe(reducer);
|
||||
expect(reducerRegister.getAll().test2).toBe(reducer);
|
||||
|
||||
reducerRegister.remove('test1');
|
||||
reducerRegister.remove('test2');
|
||||
});
|
||||
reducerRegister.remove('test1');
|
||||
reducerRegister.remove('test2');
|
||||
});
|
||||
|
||||
it('should get a single reducer from the register', () => {
|
||||
reducerRegister.add('test', reducer);
|
||||
expect(reducerRegister.getByKey('test')).toBe(reducer);
|
||||
it('should get a single reducer from the register', () => {
|
||||
reducerRegister.add('test', reducer);
|
||||
expect(reducerRegister.getByKey('test')).toBe(reducer);
|
||||
|
||||
reducerRegister.remove('test');
|
||||
});
|
||||
reducerRegister.remove('test');
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -5,98 +5,98 @@ import fetch from 'isomorphic-fetch';
|
||||
import backend from '../silverstripe-backend';
|
||||
|
||||
var getFetchMock = function(data) {
|
||||
let mock = jest.genMockFunction();
|
||||
let promise = new Promise((resolve, reject) => {
|
||||
process.nextTick(() => resolve(data));
|
||||
});
|
||||
mock.mockReturnValue(promise);
|
||||
let mock = jest.genMockFunction();
|
||||
let promise = new Promise((resolve, reject) => {
|
||||
process.nextTick(() => resolve(data));
|
||||
});
|
||||
mock.mockReturnValue(promise);
|
||||
|
||||
return mock;
|
||||
return mock;
|
||||
};
|
||||
|
||||
describe('SilverStripeBackend', () => {
|
||||
|
||||
beforeAll(() => {
|
||||
let fetchMock = getFetchMock();
|
||||
backend.fetch = fetchMock;
|
||||
beforeAll(() => {
|
||||
let fetchMock = getFetchMock();
|
||||
backend.fetch = fetchMock;
|
||||
});
|
||||
|
||||
describe('get()', () => {
|
||||
|
||||
it('should return a promise', () => {
|
||||
var promise = backend.get('http://example.com');
|
||||
expect(typeof promise).toBe('object');
|
||||
});
|
||||
|
||||
describe('get()', () => {
|
||||
|
||||
it('should return a promise', () => {
|
||||
var promise = backend.get('http://example.com');
|
||||
expect(typeof promise).toBe('object');
|
||||
});
|
||||
|
||||
it('should send a GET request to an endpoint', () => {
|
||||
backend.get('http://example.com');
|
||||
expect(backend.fetch).toBeCalledWith(
|
||||
'http://example.com',
|
||||
{method: 'get', credentials: 'same-origin'}
|
||||
);
|
||||
});
|
||||
|
||||
it('should send a GET request to an endpoint', () => {
|
||||
backend.get('http://example.com');
|
||||
expect(backend.fetch).toBeCalledWith(
|
||||
'http://example.com',
|
||||
{method: 'get', credentials: 'same-origin'}
|
||||
);
|
||||
});
|
||||
|
||||
describe('post()', () => {
|
||||
});
|
||||
|
||||
it('should return a promise', () => {
|
||||
var promise = backend.get('http://example.com/item');
|
||||
expect(typeof promise).toBe('object');
|
||||
});
|
||||
|
||||
it('should send a POST request to an endpoint', () => {
|
||||
const postData = { id: 1 };
|
||||
|
||||
backend.post('http://example.com', postData);
|
||||
|
||||
expect(backend.fetch).toBeCalledWith(
|
||||
'http://example.com',
|
||||
{method: 'post', body: postData, credentials: 'same-origin'}
|
||||
);
|
||||
});
|
||||
describe('post()', () => {
|
||||
|
||||
it('should return a promise', () => {
|
||||
var promise = backend.get('http://example.com/item');
|
||||
expect(typeof promise).toBe('object');
|
||||
});
|
||||
|
||||
describe('put()', () => {
|
||||
it('should send a POST request to an endpoint', () => {
|
||||
const postData = { id: 1 };
|
||||
|
||||
it('should return a promise', () => {
|
||||
var promise = backend.get('http://example.com/item');
|
||||
expect(typeof promise).toBe('object');
|
||||
});
|
||||
|
||||
it('should send a PUT request to an endpoint', () => {
|
||||
const putData = { id: 1 };
|
||||
|
||||
backend.put('http://example.com', putData);
|
||||
|
||||
expect(backend.fetch).toBeCalledWith(
|
||||
'http://example.com',
|
||||
{method: 'put', body: putData, credentials: 'same-origin'}
|
||||
);
|
||||
});
|
||||
backend.post('http://example.com', postData);
|
||||
|
||||
expect(backend.fetch).toBeCalledWith(
|
||||
'http://example.com',
|
||||
{method: 'post', body: postData, credentials: 'same-origin'}
|
||||
);
|
||||
});
|
||||
|
||||
describe('delete()', () => {
|
||||
});
|
||||
|
||||
it('should return a promise', () => {
|
||||
var promise = backend.get('http://example.com/item');
|
||||
|
||||
expect(typeof promise).toBe('object');
|
||||
});
|
||||
|
||||
it('should send a DELETE request to an endpoint', () => {
|
||||
const deleteData = { id: 1 };
|
||||
|
||||
backend.delete('http://example.com', deleteData);
|
||||
|
||||
expect(backend.fetch).toBeCalledWith(
|
||||
'http://example.com',
|
||||
{method: 'delete', body: deleteData, credentials: 'same-origin'}
|
||||
);
|
||||
});
|
||||
describe('put()', () => {
|
||||
|
||||
it('should return a promise', () => {
|
||||
var promise = backend.get('http://example.com/item');
|
||||
expect(typeof promise).toBe('object');
|
||||
});
|
||||
|
||||
it('should send a PUT request to an endpoint', () => {
|
||||
const putData = { id: 1 };
|
||||
|
||||
backend.put('http://example.com', putData);
|
||||
|
||||
expect(backend.fetch).toBeCalledWith(
|
||||
'http://example.com',
|
||||
{method: 'put', body: putData, credentials: 'same-origin'}
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('delete()', () => {
|
||||
|
||||
it('should return a promise', () => {
|
||||
var promise = backend.get('http://example.com/item');
|
||||
|
||||
expect(typeof promise).toBe('object');
|
||||
});
|
||||
|
||||
it('should send a DELETE request to an endpoint', () => {
|
||||
const deleteData = { id: 1 };
|
||||
|
||||
backend.delete('http://example.com', deleteData);
|
||||
|
||||
expect(backend.fetch).toBeCalledWith(
|
||||
'http://example.com',
|
||||
{method: 'delete', body: deleteData, credentials: 'same-origin'}
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
499
gulpfile.js
499
gulpfile.js
@ -1,110 +1,112 @@
|
||||
var packageJson = require('./package.json'),
|
||||
autoprefixer = require('autoprefixer'),
|
||||
babelify = require('babelify'),
|
||||
browserify = require('browserify'),
|
||||
eventStream = require('event-stream'),
|
||||
glob = require('glob'),
|
||||
gulp = require('gulp'),
|
||||
babel = require('gulp-babel'),
|
||||
diff = require('gulp-diff'),
|
||||
gulpif = require('gulp-if'),
|
||||
notify = require('gulp-notify'),
|
||||
postcss = require('gulp-postcss'),
|
||||
sass = require('gulp-sass'),
|
||||
sourcemaps = require('gulp-sourcemaps'),
|
||||
uglify = require('gulp-uglify'),
|
||||
gulpUtil = require('gulp-util'),
|
||||
path = require('path'),
|
||||
source = require('vinyl-source-stream'),
|
||||
buffer = require('vinyl-buffer'),
|
||||
semver = require('semver'),
|
||||
sprity = require('sprity'),
|
||||
watchify = require('watchify');
|
||||
const packageJson = require('./package.json');
|
||||
const autoprefixer = require('autoprefixer');
|
||||
const babelify = require('babelify'); // eslint-disable-line no-unused-vars
|
||||
const browserify = require('browserify');
|
||||
const eventStream = require('event-stream');
|
||||
const glob = require('glob');
|
||||
const gulp = require('gulp');
|
||||
const babel = require('gulp-babel');
|
||||
const diff = require('gulp-diff');
|
||||
const gulpif = require('gulp-if');
|
||||
const notify = require('gulp-notify');
|
||||
const postcss = require('gulp-postcss');
|
||||
const sass = require('gulp-sass');
|
||||
const sourcemaps = require('gulp-sourcemaps');
|
||||
const uglify = require('gulp-uglify');
|
||||
const gulpUtil = require('gulp-util');
|
||||
const path = require('path');
|
||||
const source = require('vinyl-source-stream');
|
||||
const buffer = require('vinyl-buffer');
|
||||
const semver = require('semver');
|
||||
const sprity = require('sprity');
|
||||
const watchify = require('watchify');
|
||||
|
||||
var isDev = typeof process.env.npm_config_development !== 'undefined';
|
||||
const isDev = typeof process.env.npm_config_development !== 'undefined';
|
||||
|
||||
var PATHS = {
|
||||
MODULES: './node_modules',
|
||||
ADMIN: './admin',
|
||||
ADMIN_IMAGES: './admin/images',
|
||||
ADMIN_SCSS: './admin/scss',
|
||||
ADMIN_THIRDPARTY: './admin/thirdparty',
|
||||
ADMIN_JAVASCRIPT_SRC: './admin/javascript/src',
|
||||
ADMIN_JAVASCRIPT_DIST: './admin/javascript/dist',
|
||||
FRAMEWORK: '.',
|
||||
FRAMEWORK_THIRDPARTY: './thirdparty',
|
||||
FRAMEWORK_DEV_INSTALL: './dev/install',
|
||||
FRAMEWORK_JAVASCRIPT_SRC: './javascript/src',
|
||||
FRAMEWORK_JAVASCRIPT_DIST: './javascript/dist'
|
||||
process.env.NODE_ENV = isDev ? 'development' : 'production';
|
||||
|
||||
const PATHS = {
|
||||
MODULES: './node_modules',
|
||||
ADMIN: './admin',
|
||||
ADMIN_IMAGES: './admin/images',
|
||||
ADMIN_SCSS: './admin/scss',
|
||||
ADMIN_THIRDPARTY: './admin/thirdparty',
|
||||
ADMIN_JAVASCRIPT_SRC: './admin/javascript/src',
|
||||
ADMIN_JAVASCRIPT_DIST: './admin/javascript/dist',
|
||||
FRAMEWORK: '.',
|
||||
FRAMEWORK_THIRDPARTY: './thirdparty',
|
||||
FRAMEWORK_DEV_INSTALL: './dev/install',
|
||||
FRAMEWORK_JAVASCRIPT_SRC: './javascript/src',
|
||||
FRAMEWORK_JAVASCRIPT_DIST: './javascript/dist',
|
||||
};
|
||||
|
||||
// Folders which contain both scss and css folders to be compiled
|
||||
var rootCompileFolders = [PATHS.FRAMEWORK, PATHS.ADMIN, PATHS.FRAMEWORK_DEV_INSTALL]
|
||||
const rootCompileFolders = [PATHS.FRAMEWORK, PATHS.ADMIN, PATHS.FRAMEWORK_DEV_INSTALL];
|
||||
|
||||
var browserifyOptions = {
|
||||
debug: true,
|
||||
paths: [PATHS.ADMIN_JAVASCRIPT_SRC, PATHS.FRAMEWORK_JAVASCRIPT_SRC]
|
||||
const browserifyOptions = {
|
||||
debug: true,
|
||||
paths: [PATHS.ADMIN_JAVASCRIPT_SRC, PATHS.FRAMEWORK_JAVASCRIPT_SRC],
|
||||
};
|
||||
|
||||
var babelifyOptions = {
|
||||
presets: ['es2015', 'react'],
|
||||
ignore: /(node_modules|thirdparty)/,
|
||||
comments: false
|
||||
const babelifyOptions = {
|
||||
presets: ['es2015', 'react'],
|
||||
ignore: /(node_modules|thirdparty)/,
|
||||
comments: false,
|
||||
};
|
||||
|
||||
// Used for autoprefixing css properties (same as Bootstrap Aplha.2 defaults)
|
||||
var supportedBrowsers = [
|
||||
'Chrome >= 35',
|
||||
'Firefox >= 31',
|
||||
'Edge >= 12',
|
||||
'Explorer >= 9',
|
||||
'iOS >= 8',
|
||||
'Safari >= 8',
|
||||
'Android 2.3',
|
||||
'Android >= 4',
|
||||
'Opera >= 12'
|
||||
const supportedBrowsers = [
|
||||
'Chrome >= 35',
|
||||
'Firefox >= 31',
|
||||
'Edge >= 12',
|
||||
'Explorer >= 9',
|
||||
'iOS >= 8',
|
||||
'Safari >= 8',
|
||||
'Android 2.3',
|
||||
'Android >= 4',
|
||||
'Opera >= 12',
|
||||
];
|
||||
|
||||
var blueimpFileUploadConfig = {
|
||||
src: PATHS.MODULES + '/blueimp-file-upload',
|
||||
dest: PATHS.FRAMEWORK_THIRDPARTY + '/jquery-fileupload',
|
||||
files: [
|
||||
'/cors/jquery.postmessage-transport.js',
|
||||
'/cors/jquery.xdr-transport.js',
|
||||
'/jquery.fileupload-ui.js',
|
||||
'/jquery.fileupload.js',
|
||||
'/jquery.iframe-transport.js'
|
||||
]
|
||||
const blueimpFileUploadConfig = {
|
||||
src: `${PATHS.MODULES}/blueimp-file-upload`,
|
||||
dest: PATHS.FRAMEWORK_THIRDPARTY + '/jquery-fileupload',
|
||||
files: [
|
||||
'/cors/jquery.postmessage-transport.js',
|
||||
'/cors/jquery.xdr-transport.js',
|
||||
'/jquery.fileupload-ui.js',
|
||||
'/jquery.fileupload.js',
|
||||
'/jquery.iframe-transport.js'
|
||||
]
|
||||
};
|
||||
|
||||
var blueimpLoadImageConfig = {
|
||||
src: PATHS.MODULES + '/blueimp-load-image',
|
||||
dest: PATHS.FRAMEWORK_THIRDPARTY + '/javascript-loadimage',
|
||||
files: ['/load-image.js']
|
||||
src: PATHS.MODULES + '/blueimp-load-image',
|
||||
dest: PATHS.FRAMEWORK_THIRDPARTY + '/javascript-loadimage',
|
||||
files: ['/load-image.js']
|
||||
};
|
||||
|
||||
var blueimpTmplConfig = {
|
||||
src: PATHS.MODULES + '/blueimp-tmpl',
|
||||
dest: PATHS.FRAMEWORK_THIRDPARTY + '/javascript-templates',
|
||||
files: ['/tmpl.js']
|
||||
src: PATHS.MODULES + '/blueimp-tmpl',
|
||||
dest: PATHS.FRAMEWORK_THIRDPARTY + '/javascript-templates',
|
||||
files: ['/tmpl.js']
|
||||
};
|
||||
|
||||
var jquerySizesConfig = {
|
||||
src: PATHS.MODULES + '/jquery-sizes',
|
||||
dest: PATHS.ADMIN_THIRDPARTY + '/jsizes',
|
||||
files: ['/lib/jquery.sizes.js']
|
||||
src: PATHS.MODULES + '/jquery-sizes',
|
||||
dest: PATHS.ADMIN_THIRDPARTY + '/jsizes',
|
||||
files: ['/lib/jquery.sizes.js']
|
||||
};
|
||||
|
||||
var tinymceConfig = {
|
||||
src: PATHS.MODULES + '/tinymce',
|
||||
dest: PATHS.FRAMEWORK_THIRDPARTY + '/tinymce',
|
||||
files: [
|
||||
'/tinymce.min.js', // Exclude unminified file to keep repository size down
|
||||
'/jquery.tinymce.min.js',
|
||||
'/themes/**',
|
||||
'/skins/**',
|
||||
'/plugins/**'
|
||||
]
|
||||
src: PATHS.MODULES + '/tinymce',
|
||||
dest: PATHS.FRAMEWORK_THIRDPARTY + '/tinymce',
|
||||
files: [
|
||||
'/tinymce.min.js', // Exclude unminified file to keep repository size down
|
||||
'/jquery.tinymce.min.js',
|
||||
'/themes/**',
|
||||
'/skins/**',
|
||||
'/plugins/**'
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
@ -116,12 +118,12 @@ var tinymceConfig = {
|
||||
* @param array libConfig.files - The list of files to copy from the source to the destination directory
|
||||
*/
|
||||
function copyFiles(libConfig) {
|
||||
libConfig.files.forEach(function (file) {
|
||||
var dir = path.parse(file).dir;
|
||||
libConfig.files.forEach(function (file) {
|
||||
var dir = path.parse(file).dir;
|
||||
|
||||
gulp.src(libConfig.src + file)
|
||||
.pipe(gulp.dest(libConfig.dest + dir));
|
||||
});
|
||||
gulp.src(libConfig.src + file)
|
||||
.pipe(gulp.dest(libConfig.dest + dir));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -133,17 +135,17 @@ function copyFiles(libConfig) {
|
||||
* @param array libConfig.files - The list of files to copy from the source to the destination directory
|
||||
*/
|
||||
function diffFiles(libConfig) {
|
||||
libConfig.files.forEach(function (file) {
|
||||
var dir = path.parse(file).dir;
|
||||
libConfig.files.forEach(function (file) {
|
||||
var dir = path.parse(file).dir;
|
||||
|
||||
gulp.src(libConfig.src + file)
|
||||
.pipe(diff(libConfig.dest + dir))
|
||||
.pipe(diff.reporter({ fail: true, quiet: true }))
|
||||
.on('error', function (error) {
|
||||
console.error(new Error('Sanity check failed. \'' + libConfig.dest + file + '\' has been modified.'));
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
gulp.src(libConfig.src + file)
|
||||
.pipe(diff(libConfig.dest + dir))
|
||||
.pipe(diff.reporter({ fail: true, quiet: true }))
|
||||
.on('error', function (error) {
|
||||
console.error(new Error('Sanity check failed. \'' + libConfig.dest + file + '\' has been modified.'));
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -154,31 +156,31 @@ function diffFiles(libConfig) {
|
||||
* @return object
|
||||
*/
|
||||
function transformToUmd(files, dest) {
|
||||
return eventStream.merge(files.map(function (file) {
|
||||
return gulp.src(file)
|
||||
.pipe(babel({
|
||||
presets: ['es2015'],
|
||||
moduleId: 'ss.' + path.parse(file).name,
|
||||
plugins: ['transform-es2015-modules-umd'],
|
||||
comments: false
|
||||
}))
|
||||
.on('error', notify.onError({
|
||||
message: 'Error: <%= error.message %>',
|
||||
}))
|
||||
.pipe(gulp.dest(dest));
|
||||
}));
|
||||
return eventStream.merge(files.map(function (file) {
|
||||
return gulp.src(file)
|
||||
.pipe(babel({
|
||||
presets: ['es2015'],
|
||||
moduleId: 'ss.' + path.parse(file).name,
|
||||
plugins: ['transform-es2015-modules-umd'],
|
||||
comments: false
|
||||
}))
|
||||
.on('error', notify.onError({
|
||||
message: 'Error: <%= error.message %>',
|
||||
}))
|
||||
.pipe(gulp.dest(dest));
|
||||
}));
|
||||
}
|
||||
|
||||
// Make sure the version of Node being used is valid.
|
||||
if (!semver.satisfies(process.versions.node, packageJson.engines.node)) {
|
||||
console.error('Invalid Node.js version. You need to be using ' + packageJson.engines.node + '. If you want to manage multiple Node.js versions try https://github.com/creationix/nvm');
|
||||
process.exit(1);
|
||||
console.error('Invalid Node.js version. You need to be using ' + packageJson.engines.node + '. If you want to manage multiple Node.js versions try https://github.com/creationix/nvm');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (isDev) {
|
||||
browserifyOptions.cache = {};
|
||||
browserifyOptions.packageCache = {};
|
||||
browserifyOptions.plugin = [watchify];
|
||||
browserifyOptions.cache = {};
|
||||
browserifyOptions.packageCache = {};
|
||||
browserifyOptions.plugin = [watchify];
|
||||
}
|
||||
|
||||
gulp.task('build', ['umd', 'bundle']);
|
||||
@ -186,162 +188,161 @@ gulp.task('build', ['umd', 'bundle']);
|
||||
gulp.task('bundle', ['bundle-lib', 'bundle-legacy', 'bundle-framework']);
|
||||
|
||||
gulp.task('bundle-lib', function bundleLib() {
|
||||
var bundleFileName = 'bundle-lib.js';
|
||||
var bundleFileName = 'bundle-lib.js';
|
||||
|
||||
return browserify(Object.assign({}, browserifyOptions, { entries: PATHS.ADMIN_JAVASCRIPT_SRC + '/bundles/lib.js' }))
|
||||
.on('update', bundleLib)
|
||||
.on('log', function (msg) { gulpUtil.log('Finished', 'bundled ' + bundleFileName + ' ' + msg) })
|
||||
.transform('babelify', babelifyOptions)
|
||||
.require('deep-freeze', { expose: 'deep-freeze' })
|
||||
.require('react', { expose: 'react' })
|
||||
.require('react-addons-css-transition-group', { expose: 'react-addons-css-transition-group' })
|
||||
.require('react-addons-test-utils', { expose: 'react-addons-test-utils' })
|
||||
.require('react-dom', { expose: 'react-dom' })
|
||||
.require('react-redux', { expose: 'react-redux' })
|
||||
.require('redux', { expose: 'redux' })
|
||||
.require('redux-thunk', { expose: 'redux-thunk' })
|
||||
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/form/index', { expose: 'components/form/index' })
|
||||
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/form-action/index', { expose: 'components/form-action/index' })
|
||||
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/form-builder/index', { expose: 'components/form-builder/index' })
|
||||
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/grid-field/index', { expose: 'components/grid-field/index' })
|
||||
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/grid-field/cell', { expose: 'components/grid-field/cell/index' })
|
||||
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/grid-field/header', { expose: 'components/grid-field/header' })
|
||||
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/grid-field/header-cell', { expose: 'components/grid-field/header-cell' })
|
||||
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/grid-field/row', { expose: 'components/grid-field/row' })
|
||||
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/grid-field/table', { expose: 'components/grid-field/table' })
|
||||
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/hidden-field/index', { expose: 'components/hidden-field/index' })
|
||||
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/text-field/index', { expose: 'components/text-field/index' })
|
||||
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/north-header/index', { expose: 'components/north-header/index' })
|
||||
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/north-header-breadcrumbs/index', { expose: 'components/north-header-breadcrumbs/index' })
|
||||
.require(PATHS.FRAMEWORK_JAVASCRIPT_SRC + '/i18n.js', { expose: 'i18n' })
|
||||
.require(PATHS.FRAMEWORK_JAVASCRIPT_SRC + '/jQuery.js', { expose: 'jQuery' })
|
||||
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/reducer-register.js', { expose: 'reducer-register' })
|
||||
.require(PATHS.FRAMEWORK_JAVASCRIPT_SRC + '/router.js', { expose: 'router' })
|
||||
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/silverstripe-component', { expose: 'silverstripe-component' })
|
||||
.bundle()
|
||||
.on('update', bundleLib)
|
||||
.on('error', notify.onError({ message: bundleFileName + ': <%= error.message %>' }))
|
||||
.pipe(source(bundleFileName))
|
||||
.pipe(buffer())
|
||||
.pipe(sourcemaps.init({ loadMaps: true }))
|
||||
.pipe(gulpif(!isDev, uglify()))
|
||||
.pipe(sourcemaps.write('./'))
|
||||
.pipe(gulp.dest(PATHS.ADMIN_JAVASCRIPT_DIST));
|
||||
return browserify(Object.assign({}, browserifyOptions, { entries: PATHS.ADMIN_JAVASCRIPT_SRC + '/bundles/lib.js' }))
|
||||
.on('update', bundleLib)
|
||||
.on('log', function (msg) { gulpUtil.log('Finished', 'bundled ' + bundleFileName + ' ' + msg) })
|
||||
.transform('babelify', babelifyOptions)
|
||||
.require('deep-freeze', { expose: 'deep-freeze' })
|
||||
.require('react', { expose: 'react' })
|
||||
.require('react-addons-css-transition-group', { expose: 'react-addons-css-transition-group' })
|
||||
.require('react-addons-test-utils', { expose: 'react-addons-test-utils' })
|
||||
.require('react-dom', { expose: 'react-dom' })
|
||||
.require('react-redux', { expose: 'react-redux' })
|
||||
.require('redux', { expose: 'redux' })
|
||||
.require('redux-thunk', { expose: 'redux-thunk' })
|
||||
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/form/index', { expose: 'components/form/index' })
|
||||
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/form-action/index', { expose: 'components/form-action/index' })
|
||||
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/form-builder/index', { expose: 'components/form-builder/index' })
|
||||
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/grid-field/index', { expose: 'components/grid-field/index' })
|
||||
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/grid-field/cell', { expose: 'components/grid-field/cell/index' })
|
||||
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/grid-field/header', { expose: 'components/grid-field/header' })
|
||||
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/grid-field/header-cell', { expose: 'components/grid-field/header-cell' })
|
||||
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/grid-field/row', { expose: 'components/grid-field/row' })
|
||||
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/grid-field/table', { expose: 'components/grid-field/table' })
|
||||
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/hidden-field/index', { expose: 'components/hidden-field/index' })
|
||||
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/text-field/index', { expose: 'components/text-field/index' })
|
||||
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/north-header/index', { expose: 'components/north-header/index' })
|
||||
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/components/north-header-breadcrumbs/index', { expose: 'components/north-header-breadcrumbs/index' })
|
||||
.require(PATHS.FRAMEWORK_JAVASCRIPT_SRC + '/i18n.js', { expose: 'i18n' })
|
||||
.require(PATHS.FRAMEWORK_JAVASCRIPT_SRC + '/jQuery.js', { expose: 'jQuery' })
|
||||
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/reducer-register.js', { expose: 'reducer-register' })
|
||||
.require(PATHS.FRAMEWORK_JAVASCRIPT_SRC + '/router.js', { expose: 'router' })
|
||||
.require(PATHS.ADMIN_JAVASCRIPT_SRC + '/silverstripe-component', { expose: 'silverstripe-component' })
|
||||
.bundle()
|
||||
.on('error', notify.onError({ message: bundleFileName + ': <%= error.message %>' }))
|
||||
.pipe(source(bundleFileName))
|
||||
.pipe(buffer())
|
||||
.pipe(sourcemaps.init({ loadMaps: true }))
|
||||
.pipe(gulpif(!isDev, uglify()))
|
||||
.pipe(sourcemaps.write('./'))
|
||||
.pipe(gulp.dest(PATHS.ADMIN_JAVASCRIPT_DIST));
|
||||
});
|
||||
|
||||
|
||||
gulp.task('bundle-legacy', function bundleLeftAndMain() {
|
||||
var bundleFileName = 'bundle-legacy.js';
|
||||
var bundleFileName = 'bundle-legacy.js';
|
||||
|
||||
return browserify(Object.assign({}, browserifyOptions, { entries: PATHS.ADMIN_JAVASCRIPT_SRC + '/bundles/legacy.js' }))
|
||||
.on('update', bundleLeftAndMain)
|
||||
.on('log', function (msg) { gulpUtil.log('Finished', 'bundled ' + bundleFileName + ' ' + msg) })
|
||||
.transform('babelify', babelifyOptions)
|
||||
.external('jQuery')
|
||||
.external('i18n')
|
||||
.external('router')
|
||||
.bundle()
|
||||
.on('update', bundleLeftAndMain)
|
||||
.on('error', notify.onError({ message: bundleFileName + ': <%= error.message %>' }))
|
||||
.pipe(source(bundleFileName))
|
||||
.pipe(buffer())
|
||||
.pipe(sourcemaps.init({ loadMaps: true }))
|
||||
.pipe(gulpif(!isDev, uglify()))
|
||||
.pipe(sourcemaps.write('./'))
|
||||
.pipe(gulp.dest(PATHS.ADMIN_JAVASCRIPT_DIST));
|
||||
return browserify(Object.assign({}, browserifyOptions, { entries: PATHS.ADMIN_JAVASCRIPT_SRC + '/bundles/legacy.js' }))
|
||||
.on('update', bundleLeftAndMain)
|
||||
.on('log', function (msg) { gulpUtil.log('Finished', 'bundled ' + bundleFileName + ' ' + msg) })
|
||||
.transform('babelify', babelifyOptions)
|
||||
.external('jQuery')
|
||||
.external('i18n')
|
||||
.external('router')
|
||||
.bundle()
|
||||
.on('update', bundleLeftAndMain)
|
||||
.on('error', notify.onError({ message: bundleFileName + ': <%= error.message %>' }))
|
||||
.pipe(source(bundleFileName))
|
||||
.pipe(buffer())
|
||||
.pipe(sourcemaps.init({ loadMaps: true }))
|
||||
.pipe(gulpif(!isDev, uglify()))
|
||||
.pipe(sourcemaps.write('./'))
|
||||
.pipe(gulp.dest(PATHS.ADMIN_JAVASCRIPT_DIST));
|
||||
});
|
||||
|
||||
gulp.task('bundle-framework', function bundleBoot() {
|
||||
var bundleFileName = 'bundle-framework.js';
|
||||
var bundleFileName = 'bundle-framework.js';
|
||||
|
||||
return browserify(Object.assign({}, browserifyOptions, { entries: PATHS.ADMIN_JAVASCRIPT_SRC + '/boot/index.js' }))
|
||||
.on('update', bundleBoot)
|
||||
.on('log', function (msg) { gulpUtil.log('Finished', 'bundled ' + bundleFileName + ' ' + msg) })
|
||||
.transform('babelify', babelifyOptions)
|
||||
.external('components/action-button/index')
|
||||
.external('components/north-header/index')
|
||||
.external('components/form-builder/index')
|
||||
.external('deep-freeze')
|
||||
.external('components/grid-field/index')
|
||||
.external('i18n')
|
||||
.external('jQuery')
|
||||
.external('page.js')
|
||||
.external('react-addons-test-utils')
|
||||
.external('react-dom')
|
||||
.external('react-redux')
|
||||
.external('react')
|
||||
.external('reducer-register')
|
||||
.external('redux-thunk')
|
||||
.external('redux')
|
||||
.external('silverstripe-component')
|
||||
.bundle()
|
||||
.on('update', bundleBoot)
|
||||
.on('error', notify.onError({ message: bundleFileName + ': <%= error.message %>' }))
|
||||
.pipe(source(bundleFileName))
|
||||
.pipe(buffer())
|
||||
.pipe(sourcemaps.init({ loadMaps: true }))
|
||||
.pipe(gulpif(!isDev, uglify()))
|
||||
.pipe(sourcemaps.write('./'))
|
||||
.pipe(gulp.dest(PATHS.ADMIN_JAVASCRIPT_DIST));
|
||||
return browserify(Object.assign({}, browserifyOptions, { entries: PATHS.ADMIN_JAVASCRIPT_SRC + '/boot/index.js' }))
|
||||
.on('update', bundleBoot)
|
||||
.on('log', function (msg) { gulpUtil.log('Finished', 'bundled ' + bundleFileName + ' ' + msg) })
|
||||
.transform('babelify', babelifyOptions)
|
||||
.external('components/action-button/index')
|
||||
.external('components/north-header/index')
|
||||
.external('components/form-builder/index')
|
||||
.external('deep-freeze')
|
||||
.external('components/grid-field/index')
|
||||
.external('i18n')
|
||||
.external('jQuery')
|
||||
.external('page.js')
|
||||
.external('react-addons-test-utils')
|
||||
.external('react-dom')
|
||||
.external('react-redux')
|
||||
.external('react')
|
||||
.external('reducer-register')
|
||||
.external('redux-thunk')
|
||||
.external('redux')
|
||||
.external('silverstripe-component')
|
||||
.bundle()
|
||||
.on('update', bundleBoot)
|
||||
.on('error', notify.onError({ message: bundleFileName + ': <%= error.message %>' }))
|
||||
.pipe(source(bundleFileName))
|
||||
.pipe(buffer())
|
||||
.pipe(sourcemaps.init({ loadMaps: true }))
|
||||
.pipe(gulpif(!isDev, uglify()))
|
||||
.pipe(sourcemaps.write('./'))
|
||||
.pipe(gulp.dest(PATHS.ADMIN_JAVASCRIPT_DIST));
|
||||
});
|
||||
|
||||
gulp.task('sanity', function () {
|
||||
diffFiles(blueimpFileUploadConfig);
|
||||
diffFiles(blueimpLoadImageConfig);
|
||||
diffFiles(blueimpTmplConfig);
|
||||
diffFiles(jquerySizesConfig);
|
||||
diffFiles(tinymceConfig);
|
||||
diffFiles(blueimpFileUploadConfig);
|
||||
diffFiles(blueimpLoadImageConfig);
|
||||
diffFiles(blueimpTmplConfig);
|
||||
diffFiles(jquerySizesConfig);
|
||||
diffFiles(tinymceConfig);
|
||||
});
|
||||
|
||||
gulp.task('thirdparty', function () {
|
||||
copyFiles(blueimpFileUploadConfig);
|
||||
copyFiles(blueimpLoadImageConfig);
|
||||
copyFiles(blueimpTmplConfig);
|
||||
copyFiles(jquerySizesConfig);
|
||||
copyFiles(tinymceConfig);
|
||||
copyFiles(blueimpFileUploadConfig);
|
||||
copyFiles(blueimpLoadImageConfig);
|
||||
copyFiles(blueimpTmplConfig);
|
||||
copyFiles(jquerySizesConfig);
|
||||
copyFiles(tinymceConfig);
|
||||
});
|
||||
|
||||
gulp.task('umd', ['umd-admin', 'umd-framework'], function () {
|
||||
if (isDev) {
|
||||
gulp.watch(PATHS.ADMIN_JAVASCRIPT_SRC + '/*.js', ['umd-admin']);
|
||||
gulp.watch(PATHS.FRAMEWORK_JAVASCRIPT_SRC + '/*.js', ['umd-framework']);
|
||||
}
|
||||
if (isDev) {
|
||||
gulp.watch(PATHS.ADMIN_JAVASCRIPT_SRC + '/*.js', ['umd-admin']);
|
||||
gulp.watch(PATHS.FRAMEWORK_JAVASCRIPT_SRC + '/*.js', ['umd-framework']);
|
||||
}
|
||||
});
|
||||
|
||||
gulp.task('umd-admin', function () {
|
||||
var files = glob.sync(PATHS.ADMIN_JAVASCRIPT_SRC + '/*.js', { ignore: PATHS.ADMIN_JAVASCRIPT_SRC + '/LeftAndMain.!(Ping).js' });
|
||||
var files = glob.sync(PATHS.ADMIN_JAVASCRIPT_SRC + '/*.js', { ignore: PATHS.ADMIN_JAVASCRIPT_SRC + '/LeftAndMain.!(Ping).js' });
|
||||
|
||||
return transformToUmd(files, PATHS.ADMIN_JAVASCRIPT_DIST);
|
||||
return transformToUmd(files, PATHS.ADMIN_JAVASCRIPT_DIST);
|
||||
});
|
||||
|
||||
gulp.task('umd-framework', function () {
|
||||
return transformToUmd(glob.sync(PATHS.FRAMEWORK_JAVASCRIPT_SRC + '/*.js'), PATHS.FRAMEWORK_JAVASCRIPT_DIST);
|
||||
return transformToUmd(glob.sync(PATHS.FRAMEWORK_JAVASCRIPT_SRC + '/*.js'), PATHS.FRAMEWORK_JAVASCRIPT_DIST);
|
||||
});
|
||||
|
||||
/*
|
||||
* Takes individual images and compiles them together into sprites
|
||||
*/
|
||||
gulp.task('sprites', function () {
|
||||
return sprity.src({
|
||||
src: PATHS.ADMIN_IMAGES + '/sprites/src/**/*.{png,jpg}',
|
||||
cssPath: '../images/sprites/dist',
|
||||
style: './_spritey.scss',
|
||||
processor: 'sass',
|
||||
split: true,
|
||||
margin: 0
|
||||
})
|
||||
.pipe(gulpif('*.png', gulp.dest(PATHS.ADMIN_IMAGES + '/sprites/dist'), gulp.dest(PATHS.ADMIN_SCSS)))
|
||||
return sprity.src({
|
||||
src: PATHS.ADMIN_IMAGES + '/sprites/src/**/*.{png,jpg}',
|
||||
cssPath: '../images/sprites/dist',
|
||||
style: './_spritey.scss',
|
||||
processor: 'sass',
|
||||
split: true,
|
||||
margin: 0
|
||||
})
|
||||
.pipe(gulpif('*.png', gulp.dest(PATHS.ADMIN_IMAGES + '/sprites/dist'), gulp.dest(PATHS.ADMIN_SCSS)))
|
||||
});
|
||||
|
||||
gulp.task('css', ['compile:css'], function () {
|
||||
if (isDev) {
|
||||
rootCompileFolders.forEach(function (folder) {
|
||||
gulp.watch(folder + '/scss/**/*.scss', ['compile:css']);
|
||||
});
|
||||
if (isDev) {
|
||||
rootCompileFolders.forEach(function (folder) {
|
||||
gulp.watch(folder + '/scss/**/*.scss', ['compile:css']);
|
||||
});
|
||||
|
||||
// Watch the .scss files in react components
|
||||
gulp.watch('./admin/javascript/src/**/*.scss', ['compile:css']);
|
||||
}
|
||||
// Watch the .scss files in react components
|
||||
gulp.watch('./admin/javascript/src/**/*.scss', ['compile:css']);
|
||||
}
|
||||
})
|
||||
|
||||
/*
|
||||
@ -349,20 +350,20 @@ gulp.task('css', ['compile:css'], function () {
|
||||
* Watches for changes if --development flag is given
|
||||
*/
|
||||
gulp.task('compile:css', function () {
|
||||
var outputStyle = isDev ? 'expanded' : 'compressed';
|
||||
var outputStyle = isDev ? 'expanded' : 'compressed';
|
||||
|
||||
var tasks = rootCompileFolders.map(function(folder) {
|
||||
return gulp.src(folder + '/scss/**/*.scss')
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(sass({ outputStyle: outputStyle })
|
||||
.on('error', notify.onError({
|
||||
message: 'Error: <%= error.message %>'
|
||||
}))
|
||||
)
|
||||
.pipe(postcss([autoprefixer({ browsers: supportedBrowsers })]))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest(folder + '/css'))
|
||||
});
|
||||
var tasks = rootCompileFolders.map(function(folder) {
|
||||
return gulp.src(folder + '/scss/**/*.scss')
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(sass({ outputStyle: outputStyle })
|
||||
.on('error', notify.onError({
|
||||
message: 'Error: <%= error.message %>'
|
||||
}))
|
||||
)
|
||||
.pipe(postcss([autoprefixer({ browsers: supportedBrowsers })]))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest(folder + '/css'))
|
||||
});
|
||||
|
||||
return tasks;
|
||||
return tasks;
|
||||
});
|
||||
|
@ -27,7 +27,7 @@ ss.editorWrappers.tinyMCE = (function() {
|
||||
* Initialise the editor
|
||||
*
|
||||
* @param {String} ID of parent textarea domID
|
||||
*/
|
||||
*/
|
||||
init: function(ID) {
|
||||
editorID = ID;
|
||||
|
||||
@ -45,7 +45,7 @@ ss.editorWrappers.tinyMCE = (function() {
|
||||
* Get TinyMCE Editor instance
|
||||
*
|
||||
* @returns Editor
|
||||
*/
|
||||
*/
|
||||
getInstance: function() {
|
||||
return tinymce.EditorManager.get(editorID);
|
||||
},
|
||||
@ -68,7 +68,7 @@ ss.editorWrappers.tinyMCE = (function() {
|
||||
* Get config for this data
|
||||
*
|
||||
* @returns array
|
||||
*/
|
||||
*/
|
||||
getConfig: function() {
|
||||
var selector = "#" + editorID,
|
||||
config = $(selector).data('config'),
|
||||
@ -581,8 +581,8 @@ $.entwine('ss', function($) {
|
||||
break;
|
||||
|
||||
case 'file':
|
||||
var fileid = this.find('.ss-uploadfield .ss-uploadfield-item').attr('data-fileid');
|
||||
href = fileid ? '[file_link,id=' + fileid + ']' : '';
|
||||
var fileid = this.find('.ss-uploadfield .ss-uploadfield-item').attr('data-fileid');
|
||||
href = fileid ? '[file_link,id=' + fileid + ']' : '';
|
||||
break;
|
||||
|
||||
case 'email':
|
||||
|
@ -85,7 +85,7 @@ $.widget('blueimpUIX.fileupload', $.blueimpUI.fileupload, {
|
||||
e.preventDefault(); // Avoid a form submit
|
||||
return false;
|
||||
});
|
||||
} else { //regular file upload
|
||||
} else { //regular file upload
|
||||
return $.blueimpUI.fileupload.prototype._onSend.call(that, e, data);
|
||||
}
|
||||
}
|
||||
|
@ -8,19 +8,20 @@ import page from 'page.js';
|
||||
* Wrapper for `page.show()` with SilverStripe specific behaviour.
|
||||
*/
|
||||
function show(pageShow) {
|
||||
return (path, state, dispatch, push) => {
|
||||
// Normalise `path` so that pattern matching is more robust.
|
||||
// For example if your route is '/pages' it should match when `path` is
|
||||
// 'http://foo.com/admin/pages', '/pages', and 'pages'.
|
||||
var el = document.createElement('a');
|
||||
el.href = path;
|
||||
path = el.pathname;
|
||||
if(el.search) {
|
||||
path += el.search;
|
||||
}
|
||||
|
||||
return pageShow(path, state, dispatch, push);
|
||||
return (path, state, dispatch, push) => {
|
||||
// Normalise `path` so that pattern matching is more robust.
|
||||
// For example if your route is '/pages' it should match when `path` is
|
||||
// 'http://foo.com/admin/pages', '/pages', and 'pages'.
|
||||
const el = document.createElement('a');
|
||||
let pathWithSearch;
|
||||
el.href = path;
|
||||
pathWithSearch = el.pathname;
|
||||
if (el.search) {
|
||||
pathWithSearch += el.search;
|
||||
}
|
||||
|
||||
return pageShow(pathWithSearch, state, dispatch, push);
|
||||
};
|
||||
}
|
||||
|
||||
page.show = show(page.show);
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user