diff --git a/.gitignore b/.gitignore index 3ce22d3a4..57479a462 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ npm-debug.log css/GridField_print.css admin/thirdparty/chosen/node_modules node_modules/ +coverage/ diff --git a/admin/code/CampaignAdmin.php b/admin/code/CampaignAdmin.php new file mode 100644 index 000000000..dae35a644 --- /dev/null +++ b/admin/code/CampaignAdmin.php @@ -0,0 +1,102 @@ + 'createCampaign', + 'GET item/$ID' => 'readCampaign', + 'PUT item/$ID' => 'updateCampaign', + 'DELETE item/$ID' => 'deleteCampaign', + ]; + + private static $url_segment = 'campaigns'; + + public function init() { + parent::init(); + + Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/javascript/dist/bundle-react.js'); + Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/javascript/dist/campaign-admin.js'); + } + + public function getEditForm($id = null, $fields = null) { + return ''; + } + + /** + * REST endpoint to create a campaign. + * + * @param SS_HTTPRequest $request + * + * @return SS_HTTPResponse + */ + public function createCampaign(SS_HTTPRequest $request) { + $response = new SS_HTTPResponse(); + $response->addHeader('Content-Type', 'application/json'); + $response->setBody(Convert::raw2json(['campaign' => 'create'])); + + return $response; + } + + /** + * REST endpoint to get a campaign. + * + * @param SS_HTTPRequest $request + * + * @return SS_HTTPResponse + */ + public function readCampaign(SS_HTTPRequest $request) { + $response = new SS_HTTPResponse(); + $response->addHeader('Content-Type', 'application/json'); + $response->setBody(Convert::raw2json(['campaign' => 'read'])); + + return $response; + } + + /** + * REST endpoint to update a campaign. + * + * @param SS_HTTPRequest $request + * + * @return SS_HTTPResponse + */ + public function updateCampaign(SS_HTTPRequest $request) { + $response = new SS_HTTPResponse(); + $response->addHeader('Content-Type', 'application/json'); + $response->setBody(Convert::raw2json(['campaign' => 'update'])); + + return $response; + } + + /** + * REST endpoint to delete a campaign. + * + * @param SS_HTTPRequest $request + * + * @return SS_HTTPResponse + */ + public function deleteCampaign(SS_HTTPRequest $request) { + $response = new SS_HTTPResponse(); + $response->addHeader('Content-Type', 'application/json'); + $response->setBody(Convert::raw2json(['campaign' => 'delete'])); + + return $response; + } + +} diff --git a/admin/code/LeftAndMain.php b/admin/code/LeftAndMain.php index 2dfd69c80..15818f604 100644 --- a/admin/code/LeftAndMain.php +++ b/admin/code/LeftAndMain.php @@ -481,6 +481,8 @@ class LeftAndMain extends Controller implements PermissionProvider { Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/src/jquery.entwine.inspector.js'); Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/javascript/dist/leaktools.js'); } + + Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/javascript/dist/boot.js'); Requirements::css(FRAMEWORK_ADMIN_DIR . '/css/bootstrap/bootstrap.css'); Requirements::css(FRAMEWORK_ADMIN_DIR . '/thirdparty/jquery-notice/jquery.notice.css'); diff --git a/admin/images/sprites/src/menu-icons/16x16-2x/collection.png b/admin/images/sprites/src/menu-icons/16x16-2x/collection.png new file mode 100644 index 000000000..2728d8ddf Binary files /dev/null and b/admin/images/sprites/src/menu-icons/16x16-2x/collection.png differ diff --git a/admin/images/sprites/src/menu-icons/16x16/collection.png b/admin/images/sprites/src/menu-icons/16x16/collection.png new file mode 100644 index 000000000..df0c2db4d Binary files /dev/null and b/admin/images/sprites/src/menu-icons/16x16/collection.png differ diff --git a/admin/images/sprites/src/menu-icons/24x24/collection.png b/admin/images/sprites/src/menu-icons/24x24/collection.png new file mode 100644 index 000000000..d3ef3844f Binary files /dev/null and b/admin/images/sprites/src/menu-icons/24x24/collection.png differ diff --git a/admin/javascript/src/boot/campaign-admin.js b/admin/javascript/src/boot/campaign-admin.js new file mode 100644 index 000000000..e336ed65f --- /dev/null +++ b/admin/javascript/src/boot/campaign-admin.js @@ -0,0 +1,23 @@ +import reducerRegister from 'reducer-register'; +import $ from 'jQuery'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import CampaignAdmin from '../sections/campaign-admin/controller'; +import campaignsReducer from '../state/campaigns/reducer'; + +// TODO: Move this to the controller. +reducerRegister.add('campaigns', campaignsReducer); + +$.entwine('ss', function ($) { + + $('.cms-content.CampaignAdmin').entwine({ + onadd: function () { + ReactDOM.render(, this[0]); + }, + + onremove: function () { + ReactDOM.unmountComponentAtNode(this[0]); + } + }); + +}); diff --git a/admin/javascript/src/boot/index.js b/admin/javascript/src/boot/index.js new file mode 100644 index 000000000..6abb0ed5b --- /dev/null +++ b/admin/javascript/src/boot/index.js @@ -0,0 +1,15 @@ +import { combineReducers, createStore, applyMiddleware } from 'redux'; +import thunkMiddleware from 'redux-thunk'; +import createLogger from 'redux-logger'; +import reducerRegister from 'reducer-register'; + +function appBoot() { + const initialState = {}; + const rootReducer = combineReducers(reducerRegister.getAll()); + const createStoreWithMiddleware = applyMiddleware(thunkMiddleware, createLogger())(createStore); + const store = createStoreWithMiddleware(rootReducer, initialState); + + console.log(store.getState()); +} + +window.onload = appBoot; diff --git a/admin/javascript/src/components/grid-field-cell/README.md b/admin/javascript/src/components/grid-field-cell/README.md new file mode 100644 index 000000000..98912dae6 --- /dev/null +++ b/admin/javascript/src/components/grid-field-cell/README.md @@ -0,0 +1,5 @@ +# GridFieldCell + +This component represents a data cell in a GridFieldRow. + +## Props diff --git a/admin/javascript/src/components/grid-field-cell/index.js b/admin/javascript/src/components/grid-field-cell/index.js new file mode 100644 index 000000000..e6a6c137b --- /dev/null +++ b/admin/javascript/src/components/grid-field-cell/index.js @@ -0,0 +1,14 @@ +import React from 'react'; +import SilverStripeComponent from 'silverstripe-component'; + +class GridFieldCellComponent extends SilverStripeComponent { + + render() { + return ( + {this.props.children} + ); + } + +} + +export default GridFieldCellComponent; diff --git a/admin/javascript/src/components/grid-field-cell/styles.scss b/admin/javascript/src/components/grid-field-cell/styles.scss new file mode 100644 index 000000000..521563a1e --- /dev/null +++ b/admin/javascript/src/components/grid-field-cell/styles.scss @@ -0,0 +1,3 @@ +.grid-field-cell-component { + +} diff --git a/admin/javascript/src/components/grid-field-cell/tests/grid-field-cell-test.js b/admin/javascript/src/components/grid-field-cell/tests/grid-field-cell-test.js new file mode 100644 index 000000000..7ae444b05 --- /dev/null +++ b/admin/javascript/src/components/grid-field-cell/tests/grid-field-cell-test.js @@ -0,0 +1,5 @@ +jest.dontMock('../index'); + +describe('GridFieldCellComponent', () => { + +}); diff --git a/admin/javascript/src/components/grid-field-header-cell/README.md b/admin/javascript/src/components/grid-field-header-cell/README.md new file mode 100644 index 000000000..c917cd964 --- /dev/null +++ b/admin/javascript/src/components/grid-field-header-cell/README.md @@ -0,0 +1,6 @@ +# GridFieldHeaderCell + +This component is a cell in a GridFirldHeader component. + +## Props + diff --git a/admin/javascript/src/components/grid-field-header-cell/index.js b/admin/javascript/src/components/grid-field-header-cell/index.js new file mode 100644 index 000000000..70aab6f55 --- /dev/null +++ b/admin/javascript/src/components/grid-field-header-cell/index.js @@ -0,0 +1,14 @@ +import React from 'react'; +import SilverStripeComponent from 'silverstripe-component'; + +class GridFieldHeaderCellComponent extends SilverStripeComponent { + + render() { + return ( + {this.props.children} + ); + } + +} + +export default GridFieldHeaderCellComponent; diff --git a/admin/javascript/src/components/grid-field-header-cell/styles.scss b/admin/javascript/src/components/grid-field-header-cell/styles.scss new file mode 100644 index 000000000..e69de29bb diff --git a/admin/javascript/src/components/grid-field-header-cell/tests/grid-field-header-cell-test.js b/admin/javascript/src/components/grid-field-header-cell/tests/grid-field-header-cell-test.js new file mode 100644 index 000000000..3ac2048b3 --- /dev/null +++ b/admin/javascript/src/components/grid-field-header-cell/tests/grid-field-header-cell-test.js @@ -0,0 +1,5 @@ +jest.dontMock('../index'); + +describe('GridFieldHeaderCellComponent', () => { + +}); diff --git a/admin/javascript/src/components/grid-field-header/README.md b/admin/javascript/src/components/grid-field-header/README.md new file mode 100644 index 000000000..3b6a0c345 --- /dev/null +++ b/admin/javascript/src/components/grid-field-header/README.md @@ -0,0 +1,5 @@ +# GridFieldHeader + +This component is used to display a tabel header row on a GridFieldComponent. + +## Props diff --git a/admin/javascript/src/components/grid-field-header/index.js b/admin/javascript/src/components/grid-field-header/index.js new file mode 100644 index 000000000..c5fda9753 --- /dev/null +++ b/admin/javascript/src/components/grid-field-header/index.js @@ -0,0 +1,17 @@ +import React from 'react'; +import SilverStripeComponent from 'silverstripe-component'; +import GridFieldRowComponent from '../grid-field-row'; + +class GridFieldHeaderComponent extends SilverStripeComponent { + + render() { + return ( + + {this.props.children} + + ); + } + +} + +export default GridFieldHeaderComponent; diff --git a/admin/javascript/src/components/grid-field-header/styles.scss b/admin/javascript/src/components/grid-field-header/styles.scss new file mode 100644 index 000000000..39a7db6ec --- /dev/null +++ b/admin/javascript/src/components/grid-field-header/styles.scss @@ -0,0 +1,3 @@ +.grid-field-header-component { + +} diff --git a/admin/javascript/src/components/grid-field-header/tests/grid-field-header-test.js b/admin/javascript/src/components/grid-field-header/tests/grid-field-header-test.js new file mode 100644 index 000000000..cb5eb4bf1 --- /dev/null +++ b/admin/javascript/src/components/grid-field-header/tests/grid-field-header-test.js @@ -0,0 +1,5 @@ +jest.dontMock('../index'); + +describe('GridFieldHeaderComponent', () => { + +}); diff --git a/admin/javascript/src/components/grid-field-row/README.md b/admin/javascript/src/components/grid-field-row/README.md new file mode 100644 index 000000000..9cdee86ca --- /dev/null +++ b/admin/javascript/src/components/grid-field-row/README.md @@ -0,0 +1,9 @@ +# GridFieldRow + +Represents a row in a GridField. + +## Props + +### cells (array) + +The table data to display in the row. diff --git a/admin/javascript/src/components/grid-field-row/index.js b/admin/javascript/src/components/grid-field-row/index.js new file mode 100644 index 000000000..3818c00db --- /dev/null +++ b/admin/javascript/src/components/grid-field-row/index.js @@ -0,0 +1,14 @@ +import React from 'react'; +import SilverStripeComponent from 'silverstripe-component'; + +class GridFieldRowComponent extends SilverStripeComponent { + + render() { + return ( + {this.props.children} + ); + } + +} + +export default GridFieldRowComponent; diff --git a/admin/javascript/src/components/grid-field-row/styles.scss b/admin/javascript/src/components/grid-field-row/styles.scss new file mode 100644 index 000000000..3f8dbc1a9 --- /dev/null +++ b/admin/javascript/src/components/grid-field-row/styles.scss @@ -0,0 +1,3 @@ +.grid-field-row-component { + +} diff --git a/admin/javascript/src/components/grid-field-row/tests/grid-field-row-test.js b/admin/javascript/src/components/grid-field-row/tests/grid-field-row-test.js new file mode 100644 index 000000000..d660e32f0 --- /dev/null +++ b/admin/javascript/src/components/grid-field-row/tests/grid-field-row-test.js @@ -0,0 +1,5 @@ +jest.dontMock('../index'); + +describe('GridFieldRowComponent', () => { + +}); diff --git a/admin/javascript/src/components/grid-field/README.md b/admin/javascript/src/components/grid-field/README.md new file mode 100644 index 000000000..42e40c433 --- /dev/null +++ b/admin/javascript/src/components/grid-field/README.md @@ -0,0 +1,13 @@ +# GridField + +This component is used to display structured data in an extendible table layout. + +## Props + +### Headings (array) + +The column headings. + +### Rows (array) + +The table rows. diff --git a/admin/javascript/src/components/grid-field/index.js b/admin/javascript/src/components/grid-field/index.js new file mode 100644 index 000000000..c072b00dc --- /dev/null +++ b/admin/javascript/src/components/grid-field/index.js @@ -0,0 +1,65 @@ +import React from 'react'; +import SilverStripeComponent from 'silverstripe-component'; + +class GridFieldComponent extends SilverStripeComponent { + + render() { + return ( + + {this.generateHeader()} + + {this.generateRows()} + +
+ ); + } + + /** + * 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; + } + + /** + * 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; + } + +} + +GridFieldComponent.propTypes = { + data: React.PropTypes.object, + header: React.PropTypes.object, + rows: React.PropTypes.array +}; + +export default GridFieldComponent; diff --git a/admin/javascript/src/components/grid-field/styles.scss b/admin/javascript/src/components/grid-field/styles.scss new file mode 100644 index 000000000..1254bf0d1 --- /dev/null +++ b/admin/javascript/src/components/grid-field/styles.scss @@ -0,0 +1,3 @@ +.grid-field-component { + +} diff --git a/admin/javascript/src/components/grid-field/tests/grid-field-test.js b/admin/javascript/src/components/grid-field/tests/grid-field-test.js new file mode 100644 index 000000000..426c20aec --- /dev/null +++ b/admin/javascript/src/components/grid-field/tests/grid-field-test.js @@ -0,0 +1,5 @@ +jest.dontMock('../index'); + +describe('GridFieldComponent', () => { + +}); diff --git a/admin/javascript/src/mocks/jQuery.js b/admin/javascript/src/mocks/jQuery.js new file mode 100644 index 000000000..b19a7680d --- /dev/null +++ b/admin/javascript/src/mocks/jQuery.js @@ -0,0 +1,17 @@ +function jQuery() { + return { + // Add jQuery methods such as 'find', 'change', 'trigger' as needed. + }; +} + +var mockAjaxFn = jest.genMockFunction(); + +mockAjaxFn.mockReturnValue({ + done: jest.genMockFunction(), + fail: jest.genMockFunction(), + always: jest.genMockFunction() +}); + +jQuery.ajax = mockAjaxFn; + +export default jQuery; diff --git a/admin/javascript/src/reducer-register.js b/admin/javascript/src/reducer-register.js new file mode 100644 index 000000000..5b0ac5e5d --- /dev/null +++ b/admin/javascript/src/reducer-register.js @@ -0,0 +1,64 @@ +/** + * The register of Redux reducers. + * @private + */ +var register = {}; + +/** + * 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; + } + + /** + * 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]; + } + +} + +// 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(); + +export default reducerRegister; diff --git a/admin/javascript/src/sections/campaign-admin/README.md b/admin/javascript/src/sections/campaign-admin/README.md new file mode 100644 index 000000000..c4655da39 --- /dev/null +++ b/admin/javascript/src/sections/campaign-admin/README.md @@ -0,0 +1,3 @@ +# CampaignAdmin + +This section is used for managing Campaigns in the CMS. diff --git a/admin/javascript/src/sections/campaign-admin/controller.js b/admin/javascript/src/sections/campaign-admin/controller.js new file mode 100644 index 000000000..f8d3cca14 --- /dev/null +++ b/admin/javascript/src/sections/campaign-admin/controller.js @@ -0,0 +1,60 @@ +import React from 'react'; +import SilverStripeComponent from 'silverstripe-component'; +import NorthHeader from '../../components/north-header'; +import GridField from '../../components/grid-field'; +import GridFieldHeader from '../../components/grid-field-header'; +import GridFieldHeaderCell from '../../components/grid-field-header-cell'; +import GridFieldRow from '../../components/grid-field-row'; +import GridFieldCell from '../../components/grid-field-cell'; + +class CampaignAdminContainer extends SilverStripeComponent { + + constructor(props) { + super(props); + + // TODO: This will be an AJAX call and it's response stored in state. + this.mockData = { + campaigns: [ + { + title: 'SilverStripe 4.0 release', + description: 'All the stuff related to the 4.0 announcement', + changes: 20 + }, + { + title: 'March release', + description: 'march release stuff', + changes: 2 + }, + { + title: 'About us', + description: 'The team', + changes: 1345 + } + ] + }; + } + + render() { + const columnNames = ['title', 'changes', 'description']; + + const headerCells = columnNames.map((columnName, i) => {columnName}); + const header = {headerCells}; + + const rows = this.mockData.campaigns.map((campaign, i) => { + const cells = columnNames.map((columnName, i) => { + return {campaign[columnName]} + }); + return {cells}; + }); + + return ( +
+ + +
+ ); + } + +} + +export default CampaignAdminContainer; diff --git a/admin/javascript/src/sections/campaign-admin/styles.scss b/admin/javascript/src/sections/campaign-admin/styles.scss new file mode 100644 index 000000000..173bfabdd --- /dev/null +++ b/admin/javascript/src/sections/campaign-admin/styles.scss @@ -0,0 +1,3 @@ +.CampaignAdmin { + +} diff --git a/admin/javascript/src/sections/campaign-admin/tests/campaign-admin-test.js b/admin/javascript/src/sections/campaign-admin/tests/campaign-admin-test.js new file mode 100644 index 000000000..fc29b6172 --- /dev/null +++ b/admin/javascript/src/sections/campaign-admin/tests/campaign-admin-test.js @@ -0,0 +1,5 @@ +jest.dontMock('../controller'); + +describe('CampaignAdminContainer', () => { + +}); diff --git a/admin/javascript/src/silverstripe-backend.js b/admin/javascript/src/silverstripe-backend.js new file mode 100644 index 000000000..e896765f6 --- /dev/null +++ b/admin/javascript/src/silverstripe-backend.js @@ -0,0 +1,58 @@ +import $ from 'jQuery'; + +class SilverStripeBackend { + + /** + * Makes a network request using the GET HTTP verb. + * + * @param string url - Endpoint URL. + * + * @return object - jqXHR. See http://api.jquery.com/Types/#jqXHR + */ + get(url) { + return $.ajax({ type: 'GET', url }); + } + + /** + * 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 - jqXHR. See http://api.jquery.com/Types/#jqXHR + */ + post(url, data) { + return $.ajax({ type: 'POST', url, data }); + } + + /** + * 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 - jqXHR. See http://api.jquery.com/Types/#jqXHR + */ + put(url, data) { + return $.ajax({ type: 'PUT', url, data }); + } + + /** + * 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 - jqXHR. See http://api.jquery.com/Types/#jqXHR + */ + delete(url, data) { + return $.ajax({ type: 'DELETE', url, data }); + } + +} + +// Exported as a singleton so we can implement things like +// global caching and request batching at some stage. +let backend = new SilverStripeBackend(); + +export default backend; diff --git a/admin/javascript/src/state/campaigns/action-types.js b/admin/javascript/src/state/campaigns/action-types.js new file mode 100644 index 000000000..d52b75776 --- /dev/null +++ b/admin/javascript/src/state/campaigns/action-types.js @@ -0,0 +1,8 @@ +export default { + CREATE_CAMPAIGN: 'CREATE_CAMPAIGN', + UPDATE_CAMPAIGN: 'UPDATE_CAMPAIGN', + DELETE_CAMPAIGN: 'DELETE_CAMPAIGN', + FETCH_CAMPAIGN_REQUEST: 'FETCH_CAMPAIGN_REQUEST', + FETCH_CAMPAIGN_FAILURE: 'FETCH_CAMPAIGN_FAILURE', + FETCH_CAMPAIGN_SUCCESS: 'FETCH_CAMPAIGN_SUCCESS' +}; diff --git a/admin/javascript/src/state/campaigns/actions.js b/admin/javascript/src/state/campaigns/actions.js new file mode 100644 index 000000000..e69de29bb diff --git a/admin/javascript/src/state/campaigns/reducer.js b/admin/javascript/src/state/campaigns/reducer.js new file mode 100644 index 000000000..f9e8d271d --- /dev/null +++ b/admin/javascript/src/state/campaigns/reducer.js @@ -0,0 +1,49 @@ +import deepFreeze from 'deep-freeze'; +import ACTION_TYPES from './action-types'; + +const initialState = { + isFetching: false, + items: [] +}; + +function campaignsReducer(state = initialState, action) { + + switch (action.type) { + + case ACTION_TYPES.CREATE_CAMPAIGN: + return deepFreeze(Object.assign({}, state, { + + })); + + case ACTION_TYPES.UPDATE_CAMPAIGN: + return deepFreeze(Object.assign({}, state, { + + })); + + case ACTION_TYPES.DELETE_CAMPAIGN: + return deepFreeze(Object.assign({}, state, { + + })); + + case ACTION_TYPES.FETCH_CAMPAIGN_REQUEST: + return deepFreeze(Object.assign({}, state, { + isFetching: true + })); + + case ACTION_TYPES.FETCH_CAMPAIGN_FAILURE: + return deepFreeze(Object.assign({}, state, { + isFetching: false + })); + + case ACTION_TYPES.FETCH_CAMPAIGN_SUCCESS: + return deepFreeze(Object.assign({}, state, { + isFetching: false + })); + + default: + return state; + } + +} + +export default campaignsReducer; diff --git a/admin/javascript/src/state/campaigns/tests/campaigns-reducer-test.js b/admin/javascript/src/state/campaigns/tests/campaigns-reducer-test.js new file mode 100644 index 000000000..b9d28fa28 --- /dev/null +++ b/admin/javascript/src/state/campaigns/tests/campaigns-reducer-test.js @@ -0,0 +1,70 @@ +jest.dontMock('deep-freeze'); +jest.dontMock('../reducer'); +jest.dontMock('../action-types'); + +var campaignsReducer = require('../reducer').default, + ACTION_TYPES = require('../action-types').default; + +describe('campaignsReducer', () => { + + describe('CREATE_CAMPAIGN', () => { + + }); + + describe('UPDATE_CAMPAIGN', () => { + + }); + + describe('DELETE_CAMPAIGN', () => { + + }); + + describe('FETCH_CAMPAIGN_REQUEST', () => { + + it('should set the "isFetching" flag', () => { + const initialState = { + isFetching: false + }; + + const action = { type: ACTION_TYPES.FETCH_CAMPAIGN_REQUEST }; + + const nextState = campaignsReducer(initialState, action); + + expect(nextState.isFetching).toBe(true); + }); + + }); + + describe('FETCH_CAMPAIGN_FAILURE', () => { + + it('should unset the "isFetching" flag', () => { + const initialState = { + isFetching: true + }; + + const action = { type: ACTION_TYPES.FETCH_CAMPAIGN_FAILURE }; + + const nextState = campaignsReducer(initialState, action); + + expect(nextState.isFetching).toBe(false); + }); + + }); + + describe('FETCH_CAMPAIGN_SUCCESS', () => { + + it('should unset the "isFetching" flag', () => { + const initialState = { + isFetching: true + }; + + const action = { type: ACTION_TYPES.FETCH_CAMPAIGN_FAILURE }; + + const nextState = campaignsReducer(initialState, action); + + expect(nextState.isFetching).toBe(false); + }); + + }); + +}); diff --git a/admin/javascript/src/styles/main.scss b/admin/javascript/src/styles/main.scss new file mode 100644 index 000000000..e7e3af67b --- /dev/null +++ b/admin/javascript/src/styles/main.scss @@ -0,0 +1,14 @@ +/** ----------------------------- + * Sections + * ------------------------------ */ +@import "../sections/campaign-admin/styles"; + +/** ----------------------------- + * components + * ------------------------------ */ +@import "../components/grid-field/styles"; +@import "../components/grid-field-cell/styles"; +@import "../components/grid-field-header/styles"; +@import "../components/grid-field-header-cell/styles"; +@import "../components/grid-field-row/styles"; +@import "../components/north-header/styles"; diff --git a/admin/javascript/src/tests/reducer-register-test.js b/admin/javascript/src/tests/reducer-register-test.js new file mode 100644 index 000000000..1935c2b37 --- /dev/null +++ b/admin/javascript/src/tests/reducer-register-test.js @@ -0,0 +1,44 @@ +jest.dontMock('../reducer-register'); + +var reducerRegister = require('../reducer-register').default; + +describe('ReducerRegister', () => { + + var reducer = () => null; + + 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.remove('test'); + }); + + 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); + }); + + 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); + + 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); + + reducerRegister.remove('test'); + }); + +}); diff --git a/admin/javascript/src/tests/silverstripe-backend-test.js b/admin/javascript/src/tests/silverstripe-backend-test.js new file mode 100644 index 000000000..cded489e9 --- /dev/null +++ b/admin/javascript/src/tests/silverstripe-backend-test.js @@ -0,0 +1,106 @@ +jest.mock('jQuery'); +jest.unmock('../silverstripe-backend'); + +import $ from 'jQuery'; +import backend from '../silverstripe-backend'; + +describe('SilverStripeBackend', () => { + + describe('get()', () => { + + it('should return a jqXHR', () => { + var jqxhr = backend.get('http://example.com'); + + expect(typeof jqxhr).toBe('object'); + expect(typeof jqxhr.done).toBe('function'); + expect(typeof jqxhr.fail).toBe('function'); + expect(typeof jqxhr.always).toBe('function'); + }); + + it('should send a GET request to an endpoint', () => { + backend.get('http://example.com'); + + expect($.ajax).toBeCalledWith({ + type: 'GET', + url: 'http://example.com' + }); + }); + + }); + + describe('post()', () => { + + it('should return a jqXHR', () => { + var jqxhr = backend.get('http://example.com/item'); + + expect(typeof jqxhr).toBe('object'); + expect(typeof jqxhr.done).toBe('function'); + expect(typeof jqxhr.fail).toBe('function'); + expect(typeof jqxhr.always).toBe('function'); + }); + + it('should send a POST request to an endpoint', () => { + const postData = { id: 1 }; + + backend.post('http://example.com', postData); + + expect($.ajax).toBeCalledWith({ + type: 'POST', + url: 'http://example.com', + data: postData + }); + }); + + }); + + describe('put()', () => { + + it('should return a jqXHR', () => { + var jqxhr = backend.get('http://example.com/item'); + + expect(typeof jqxhr).toBe('object'); + expect(typeof jqxhr.done).toBe('function'); + expect(typeof jqxhr.fail).toBe('function'); + expect(typeof jqxhr.always).toBe('function'); + }); + + it('should send a PUT request to an endpoint', () => { + const putData = { id: 1 }; + + backend.put('http://example.com', putData); + + expect($.ajax).toBeCalledWith({ + type: 'PUT', + url: 'http://example.com', + data: putData + }); + }); + + }); + + describe('delete()', () => { + + it('should return a jqXHR', () => { + var jqxhr = backend.get('http://example.com/item'); + + expect(typeof jqxhr).toBe('object'); + expect(typeof jqxhr.done).toBe('function'); + expect(typeof jqxhr.fail).toBe('function'); + expect(typeof jqxhr.always).toBe('function'); + }); + + it('should send a DELETE request to an endpoint', () => { + const deleteData = { id: 1 }; + + backend.delete('http://example.com', deleteData); + + expect($.ajax).toBeCalledWith({ + type: 'DELETE', + url: 'http://example.com', + data: deleteData + }); + }); + + }); + +}); diff --git a/admin/scss/_retina.scss b/admin/scss/_retina.scss index 0b8487806..01e1c3bcc 100644 --- a/admin/scss/_retina.scss +++ b/admin/scss/_retina.scss @@ -16,6 +16,9 @@ background-image: sprite-url($sprite); background-size: ceil(image-width(sprite-path($sprite)) / 2) auto; + &.icon-campaignadmin { + background-position: 0 round(nth(sprite-position($sprite, "collection"), 2) / 2); + } &.icon-assetadmin { background-position: 0 round(nth(sprite-position($sprite, "picture"), 2) / 2); } @@ -289,7 +292,11 @@ height: 16px; @extend .retina-menu-icons-16x16-2x; - &.icon-assetadmin { + &.icon-campaignadmin { + @include retina-sprite($menu-icons-16x16-2x-collection); + display: inline-block; + } + &.icon-assetadmin { @include retina-sprite($menu-icons-16x16-2x-picture); display: inline-block; } diff --git a/admin/scss/_sprites.scss b/admin/scss/_sprites.scss index e60170729..ed96fa1ef 100644 --- a/admin/scss/_sprites.scss +++ b/admin/scss/_sprites.scss @@ -68,6 +68,9 @@ height: 24px; @extend .icon-menu-icons-24x24; + &.icon-campaignadmin { + @include sprite($menu-icons-24x24-collection, inline-block); + } &.icon-assetadmin { @include sprite($menu-icons-24x24-picture, inline-block); } @@ -99,6 +102,9 @@ height: 16px; @extend .icon-menu-icons-16x16; + &.icon-campaignadmin { + @include sprite($menu-icons-16x16-collection, inline-block); + } &.icon-assetadmin { @include sprite($menu-icons-16x16-picture, inline-block); } diff --git a/admin/scss/_spritey.scss b/admin/scss/_spritey.scss index b0a7be053..96c5f18d4 100644 --- a/admin/scss/_spritey.scss +++ b/admin/scss/_spritey.scss @@ -120,39 +120,42 @@ $sprites-64x64-2x-tab-list-hover: -0px -160px 80px 80px; $sprites-64x64-2x-tab-list: -0px -240px 80px 80px; $sprites-64x64-2x-tab-tree-hover: -0px -320px 80px 80px; $sprites-64x64-2x-tab-tree: -0px -400px 80px 80px; -$menu-icons-24x24-home: -0px -0px 24px 24px; -$menu-icons-24x24-blog: -0px -24px 24px 24px; -$menu-icons-24x24-db: -0px -48px 24px 24px; -$menu-icons-24x24-document: -0px -72px 24px 24px; -$menu-icons-24x24-gears: -0px -96px 24px 24px; -$menu-icons-24x24-community: -0px -120px 24px 24px; -$menu-icons-24x24-information: -0px -144px 24px 24px; -$menu-icons-24x24-network: -0px -168px 24px 24px; -$menu-icons-24x24-pencil: -0px -192px 24px 24px; -$menu-icons-24x24-picture: -0px -216px 24px 24px; -$menu-icons-24x24-pie-chart: -0px -240px 24px 24px; -$menu-icons-16x16-2x-home: -0px -0px 32px 32px; -$menu-icons-16x16-2x-blog: -0px -32px 32px 32px; -$menu-icons-16x16-2x-db: -0px -64px 32px 32px; -$menu-icons-16x16-2x-document: -0px -96px 32px 32px; -$menu-icons-16x16-2x-gears: -0px -128px 32px 32px; -$menu-icons-16x16-2x-community: -0px -160px 32px 32px; -$menu-icons-16x16-2x-information: -0px -192px 32px 32px; -$menu-icons-16x16-2x-network: -0px -224px 32px 32px; -$menu-icons-16x16-2x-pencil: -0px -256px 32px 32px; -$menu-icons-16x16-2x-picture: -0px -288px 32px 32px; -$menu-icons-16x16-2x-pie-chart: -0px -320px 32px 32px; $menu-icons-16x16-home: -0px -0px 16px 16px; $menu-icons-16x16-blog: -0px -16px 16px 16px; -$menu-icons-16x16-db: -0px -32px 16px 16px; -$menu-icons-16x16-document: -0px -48px 16px 16px; -$menu-icons-16x16-gears: -0px -64px 16px 16px; -$menu-icons-16x16-community: -0px -80px 16px 16px; -$menu-icons-16x16-information: -0px -96px 16px 16px; -$menu-icons-16x16-network: -0px -112px 16px 16px; -$menu-icons-16x16-pencil: -0px -128px 16px 16px; -$menu-icons-16x16-picture: -0px -144px 16px 16px; -$menu-icons-16x16-pie-chart: -0px -160px 16px 16px; +$menu-icons-16x16-community: -0px -32px 16px 16px; +$menu-icons-16x16-db: -0px -48px 16px 16px; +$menu-icons-16x16-document: -0px -64px 16px 16px; +$menu-icons-16x16-gears: -0px -80px 16px 16px; +$menu-icons-16x16-collection: -0px -96px 16px 16px; +$menu-icons-16x16-information: -0px -112px 16px 16px; +$menu-icons-16x16-network: -0px -128px 16px 16px; +$menu-icons-16x16-pencil: -0px -144px 16px 16px; +$menu-icons-16x16-picture: -0px -160px 16px 16px; +$menu-icons-16x16-pie-chart: -0px -176px 16px 16px; +$menu-icons-16x16-2x-home: -0px -0px 32px 32px; +$menu-icons-16x16-2x-blog: -0px -32px 32px 32px; +$menu-icons-16x16-2x-community: -0px -64px 32px 32px; +$menu-icons-16x16-2x-db: -0px -96px 32px 32px; +$menu-icons-16x16-2x-document: -0px -128px 32px 32px; +$menu-icons-16x16-2x-gears: -0px -160px 32px 32px; +$menu-icons-16x16-2x-collection: -0px -192px 32px 32px; +$menu-icons-16x16-2x-information: -0px -224px 32px 32px; +$menu-icons-16x16-2x-network: -0px -256px 32px 32px; +$menu-icons-16x16-2x-pencil: -0px -288px 32px 32px; +$menu-icons-16x16-2x-picture: -0px -320px 32px 32px; +$menu-icons-16x16-2x-pie-chart: -0px -352px 32px 32px; +$menu-icons-24x24-home: -0px -0px 24px 24px; +$menu-icons-24x24-blog: -0px -24px 24px 24px; +$menu-icons-24x24-community: -0px -48px 24px 24px; +$menu-icons-24x24-db: -0px -72px 24px 24px; +$menu-icons-24x24-document: -0px -96px 24px 24px; +$menu-icons-24x24-gears: -0px -120px 24px 24px; +$menu-icons-24x24-collection: -0px -144px 24px 24px; +$menu-icons-24x24-information: -0px -168px 24px 24px; +$menu-icons-24x24-network: -0px -192px 24px 24px; +$menu-icons-24x24-pencil: -0px -216px 24px 24px; +$menu-icons-24x24-picture: -0px -240px 24px 24px; +$menu-icons-24x24-pie-chart: -0px -264px 24px 24px; $menu-icons-24x24-2x-home: -0px -0px 48px 48px; $menu-icons-24x24-2x-blog: -0px -48px 48px 48px; $menu-icons-24x24-2x-db: -0px -96px 48px 48px; @@ -204,15 +207,15 @@ $menu-icons-24x24-2x-pie-chart: -0px -480px 48px 48px; .icon-sprites-64x64-2x { background-image: url('../images/sprites/dist/sprite-sprites-64x64-2x.png'); } -.icon-menu-icons-24x24 { - background-image: url('../images/sprites/dist/sprite-menu-icons-24x24.png'); +.icon-menu-icons-16x16 { + background-image: url('../images/sprites/dist/sprite-menu-icons-16x16.png'); } .icon-menu-icons-16x16-2x { background-image: url('../images/sprites/dist/sprite-menu-icons-16x16-2x.png'); } -.icon-menu-icons-16x16 { - background-image: url('../images/sprites/dist/sprite-menu-icons-16x16.png'); +.icon-menu-icons-24x24 { + background-image: url('../images/sprites/dist/sprite-menu-icons-24x24.png'); } .icon-menu-icons-24x24-2x { background-image: url('../images/sprites/dist/sprite-menu-icons-24x24-2x.png'); -} +} \ No newline at end of file diff --git a/admin/scss/bootstrap/_variables.scss b/admin/scss/bootstrap/_variables.scss index af7af8a7c..0bc6a242d 100644 --- a/admin/scss/bootstrap/_variables.scss +++ b/admin/scss/bootstrap/_variables.scss @@ -29,7 +29,7 @@ // $gray-dark: #373a3c; // $gray: #55595c; // $gray-light: #818a91; -// $gray-lighter: #eceeef; +$gray-lighter: #e8e9ea; // $gray-lightest: #f7f7f9; // // $brand-primary: #0275d8; diff --git a/admin/scss/screen.scss b/admin/scss/screen.scss index 06a4dffca..58b95e698 100644 --- a/admin/scss/screen.scss +++ b/admin/scss/screen.scss @@ -51,6 +51,11 @@ @import "SecurityAdmin.scss"; @import "CMSSecurity.scss"; +/** ----------------------------- + * Include React components' css + * ------------------------------ */ +@import "../javascript/src/styles/main.scss"; + /** ----------------------------- * Retina graphics * ----------------------------- */ diff --git a/admin/scss/themes/_default.scss b/admin/scss/themes/_default.scss index 7253124a1..0a350be10 100644 --- a/admin/scss/themes/_default.scss +++ b/admin/scss/themes/_default.scss @@ -4,6 +4,8 @@ * and leave the actual styling to _style.scss and auxilliary files. */ +@import "../bootstrap/variables.scss"; + /** ----------------------------------------------- * Colours * ------------------------------------------------ */ diff --git a/docs/en/05_Contributing/01_Code.md b/docs/en/05_Contributing/01_Code.md index 322ed9c3a..f4e8b6776 100644 --- a/docs/en/05_Contributing/01_Code.md +++ b/docs/en/05_Contributing/01_Code.md @@ -295,6 +295,18 @@ $ npm run sanity `sanity` makes sure files in `thirdparty` match files copied from `node_modules`. You should never commit custom changes to a library file. This script will catch them if you do :smile: +``` +$ npm run test +``` + +This script runs the JavaScript unit tests. + +``` +$ npm run coverage +``` + +This script generates a coverage report for the JavaScript unit tests. The report is generated in the `coverage` directory. + ``` $ npm run css ``` diff --git a/gulpfile.js b/gulpfile.js index 2dfb05605..4404d50bf 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -10,7 +10,6 @@ var gulp = require('gulp'), autoprefixer = require('autoprefixer'), browserify = require('browserify'), babelify = require('babelify'), - watchify = require('watchify'), source = require('vinyl-source-stream'), buffer = require('vinyl-buffer'), path = require('path'), @@ -42,11 +41,7 @@ var PATHS = { // Folders which contain both scss and css folders to be compiled var rootCompileFolders = [PATHS.FRAMEWORK, PATHS.ADMIN, PATHS.FRAMEWORK_DEV_INSTALL] -var browserifyOptions = { - cache: {}, - packageCache: {}, - poll: true -}; +var browserifyOptions = {}; // Used for autoprefixing css properties (same as Bootstrap Aplha.2 defaults) var supportedBrowsers = [ @@ -173,17 +168,23 @@ if (!semver.satisfies(process.versions.node, packageJson.engines.node)) { if (isDev) { browserifyOptions.debug = true; - browserifyOptions.plugin = [watchify]; } -gulp.task('build', ['umd', 'bundle']); +gulp.task('build', ['umd', 'bundle'], function () { + if (isDev) { + gulp.watch([ + PATHS.ADMIN_JAVASCRIPT_SRC + '/**/*.js', + PATHS.FRAMEWORK_JAVASCRIPT_SRC + '/**/*.js', + ], ['build']); + } +}); -gulp.task('bundle', ['bundle-lib', 'bundle-leftandmain', 'bundle-react']); +gulp.task('bundle', ['bundle-lib', 'bundle-leftandmain', 'bundle-boot', 'bundle-react', 'bundle-campaign-admin']); gulp.task('bundle-leftandmain', function bundleLeftAndMain() { - return browserify(Object.assign({}, browserifyOptions, { - entries: PATHS.ADMIN_JAVASCRIPT_SRC + '/bundles/leftandmain.js' - })) + var bundleFileName = 'bundle-leftandmain.js'; + + return browserify(Object.assign({}, browserifyOptions, { entries: PATHS.ADMIN_JAVASCRIPT_SRC + '/bundles/leftandmain.js' })) .transform(babelify.configure({ presets: ['es2015'], ignore: /(thirdparty)/, @@ -194,18 +195,18 @@ gulp.task('bundle-leftandmain', function bundleLeftAndMain() { .external('i18n') .external('router') .bundle() - .on('update', bundleLeftAndMain) - .on('error', notify.onError({ message: 'Error: <%= error.message %>' })) - .pipe(source('bundle-leftandmain.js')) + .on('update', bundleLeftAndMain) + .on('error', notify.onError({ message: bundleFileName + ': <%= error.message %>' })) + .pipe(source(bundleFileName)) .pipe(buffer()) .pipe(gulpif(!isDev, uglify())) .pipe(gulp.dest(PATHS.ADMIN_JAVASCRIPT_DIST)); }); gulp.task('bundle-lib', function bundleLib() { - return browserify(Object.assign({}, browserifyOptions, { - entries: PATHS.ADMIN_JAVASCRIPT_SRC + '/bundles/lib.js' - })) + var bundleFileName = 'bundle-lib.js'; + + return browserify(Object.assign({}, browserifyOptions, { entries: PATHS.ADMIN_JAVASCRIPT_SRC + '/bundles/lib.js' })) .transform(babelify.configure({ presets: ['es2015'], ignore: /(thirdparty)/, @@ -215,10 +216,11 @@ gulp.task('bundle-lib', function bundleLib() { .require(PATHS.FRAMEWORK_JAVASCRIPT_SRC + '/jQuery.js', { expose: 'jQuery' }) .require(PATHS.FRAMEWORK_JAVASCRIPT_SRC + '/i18n.js', { expose: 'i18n' }) .require(PATHS.FRAMEWORK_JAVASCRIPT_SRC + '/router.js', { expose: 'router' }) + .require(PATHS.ADMIN_JAVASCRIPT_SRC + '/reducer-register.js', { expose: 'reducer-register' }) .bundle() - .on('update', bundleLib) - .on('error', notify.onError({ message: 'Error: <%= error.message %>' })) - .pipe(source('bundle-lib.js')) + .on('update', bundleLib) + .on('error', notify.onError({ message: bundleFileName + ': <%= error.message %>' })) + .pipe(source(bundleFileName)) .pipe(buffer()) .pipe(gulpif(!isDev, uglify())) .pipe(gulp.dest(PATHS.ADMIN_JAVASCRIPT_DIST)); @@ -235,9 +237,54 @@ gulp.task('bundle-react', function bundleReact() { .require('isomorphic-fetch', { expose: 'isomorphic-fetch' }) .require(PATHS.ADMIN_JAVASCRIPT_DIST + '/SilverStripeComponent', { expose: 'silverstripe-component' }) .bundle() - .on('update', bundleReact) - .on('error', notify.onError({ message: 'Error: <%= error.message %>' })) - .pipe(source('bundle-react.js')) + .on('update', bundleReact) + .on('error', notify.onError({ message: bundleFileName + ': <%= error.message %>' })) + .pipe(source(bundleFileName)) + .pipe(buffer()) + .pipe(gulpif(!isDev, uglify())) + .pipe(gulp.dest(PATHS.ADMIN_JAVASCRIPT_DIST)); +}); + +gulp.task('bundle-boot', function bundleBoot() { + var bundleFileName = 'boot.js'; + + return browserify(Object.assign({}, browserifyOptions, { entries: PATHS.ADMIN_JAVASCRIPT_SRC + '/boot/index.js' })) + .transform(babelify.configure({ + presets: ['es2015'], + ignore: /(node_modules)/ + })) + .external('reducer-register') + .bundle() + .on('error', notify.onError({ message: bundleFileName + ': <%= error.message %>' })) + .pipe(source(bundleFileName)) + .pipe(buffer()) + .pipe(gulpif(!isDev, uglify())) + .pipe(gulp.dest(PATHS.ADMIN_JAVASCRIPT_DIST)); +}); + +gulp.task('bundle-campaign-admin', function bundleCampaignAdmin() { + var bundleFileName = 'campaign-admin.js'; + + return browserify(Object.assign({}, browserifyOptions, { entries: PATHS.ADMIN_JAVASCRIPT_SRC + '/boot/campaign-admin.js' })) + .transform(babelify.configure({ + presets: ['es2015', 'react'], + ignore: /(node_modules)/ + })) + .external('deep-freeze') + .external('i18n') + .external('jQuery') + .external('page.js') + .external('react') + .external('react-addons-test-utils') + .external('react-dom') + .external('react-redux') + .external('redux') + .external('redux-thunk') + .external('silverstripe-component') + .external('reducer-register') + .bundle() + .on('error', notify.onError({ message: bundleFileName + ': <%= error.message %>' })) + .pipe(source(bundleFileName)) .pipe(buffer()) .pipe(gulpif(!isDev, uglify())) .pipe(gulp.dest(PATHS.ADMIN_JAVASCRIPT_DIST)); diff --git a/package.json b/package.json index 716c60441..28d66a896 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,13 @@ "scripts": { "build": "gulp build", "bundle": "gulp bundle", - "sanity": "gulp sanity", - "thirdparty": "gulp thirdparty", + "coverage": "jest --coverage", "css": "gulp css", + "lock": "npm-shrinkwrap --dev", + "sanity": "gulp sanity", "sprites": "gulp sprites", - "lock": "npm-shrinkwrap --dev" + "test": "jest", + "thirdparty": "gulp thirdparty" }, "repository": { "type": "git", @@ -35,8 +37,10 @@ "devDependencies": { "autoprefixer": "^6.3.1", "babel-core": "^6.4.0", + "babel-jest": "^9.0.3", "babel-plugin-transform-es2015-modules-umd": "^6.4.0", - "babel-preset-es2015": "^6.3.13", + "babel-preset-es2015": "^6.6.0", + "babel-preset-react": "^6.5.0", "babelify": "^7.2.0", "browserify": "^13.0.0", "event-stream": "^3.3.2", @@ -51,29 +55,33 @@ "gulp-sourcemaps": "^1.6.0", "gulp-uglify": "^1.5.1", "gulp-util": "^3.0.7", + "jest-cli": "^0.9.2", + "npm-shrinkwrap": "^5.4.1", + "react-addons-test-utils": "^0.14.6", + "redux-logger": "^2.6.1", "semver": "^5.1.0", "sprity": "^1.0.8", "sprity-sass": "^1.0.4", "vinyl-buffer": "^1.0.0", "vinyl-source-stream": "^1.1.0", - "watchify": "^3.7.0" + "watchify": "^3.7.0" }, - "dependencies": { - "blueimp-file-upload": "^6.0.3", - "blueimp-load-image": "^1.1.3", - "blueimp-tmpl": "^1.0.2", - "bootstrap": "^4.0.0-alpha.2", - "isomorphic-fetch": "^2.2.1", - "jquery-sizes": "^0.33.0", - "json-js": "^1.1.2", - "npm-shrinkwrap": "^5.4.1", - "page.js": "^4.13.3", - "react": "^0.14.6", - "react-addons-test-utils": "^0.14.6", - "react-dom": "^0.14.6", - "react-redux": "^4.0.6", - "redux": "^3.0.5", - "redux-thunk": "^1.0.3", - "tinymce": "^4.3.3" + "jest": { + "scriptPreprocessor": "/node_modules/babel-jest", + "testPathDirs": [ + "admin/javascript/src" + ], + "testDirectoryName": "tests", + "mocksPattern": "mocks", + "unmockedModulePathPatterns": [ + "/node_modules/react" + ], + "bail": true, + "testRunner": "/node_modules/jest-cli/src/testRunners/jasmine/jasmine2.js" + }, + "babel": { + "presets": [ + "es2015" + ] } }