CampaignAdmin and GridField React sections

Also removes watchify because it wasn't working.
Add SilverStripeBackend class used to fetch data from endpoints for the front-end
This commit is contained in:
David Craig 2016-03-16 13:30:39 +13:00 committed by Ingo Schommer
parent 7580d35be8
commit f8c17bed3b
51 changed files with 1017 additions and 84 deletions

1
.gitignore vendored
View File

@ -7,3 +7,4 @@ npm-debug.log
css/GridField_print.css
admin/thirdparty/chosen/node_modules
node_modules/
coverage/

View File

@ -0,0 +1,102 @@
<?php
/**
* Campaign section of the CMS
*
* @package framework
* @subpackage admin
*/
class CampaignAdmin extends LeftAndMain implements PermissionProvider {
private static $allowed_actions = [
'createCampaign',
'readCampaign',
'updateCampaign',
'deleteCampaign',
];
private static $menu_priority = 11;
private static $menu_title = 'Campaigns';
private static $url_handlers = [
'POST item/$ID' => '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;
}
}

View File

@ -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');

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

View File

@ -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(<CampaignAdmin />, this[0]);
},
onremove: function () {
ReactDOM.unmountComponentAtNode(this[0]);
}
});
});

View File

@ -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;

View File

@ -0,0 +1,5 @@
# GridFieldCell
This component represents a data cell in a GridFieldRow.
## Props

View File

@ -0,0 +1,14 @@
import React from 'react';
import SilverStripeComponent from 'silverstripe-component';
class GridFieldCellComponent extends SilverStripeComponent {
render() {
return (
<td className='grid-field-cell-component'>{this.props.children}</td>
);
}
}
export default GridFieldCellComponent;

View File

@ -0,0 +1,3 @@
.grid-field-cell-component {
}

View File

@ -0,0 +1,5 @@
jest.dontMock('../index');
describe('GridFieldCellComponent', () => {
});

View File

@ -0,0 +1,6 @@
# GridFieldHeaderCell
This component is a cell in a GridFirldHeader component.
## Props

View File

@ -0,0 +1,14 @@
import React from 'react';
import SilverStripeComponent from 'silverstripe-component';
class GridFieldHeaderCellComponent extends SilverStripeComponent {
render() {
return (
<th className='grid-field-header-cell-component'>{this.props.children}</th>
);
}
}
export default GridFieldHeaderCellComponent;

View File

@ -0,0 +1,5 @@
jest.dontMock('../index');
describe('GridFieldHeaderCellComponent', () => {
});

View File

@ -0,0 +1,5 @@
# GridFieldHeader
This component is used to display a tabel header row on a GridFieldComponent.
## Props

View File

@ -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 (
<thead className='grid-field-header-component'>
<GridFieldRowComponent>{this.props.children}</GridFieldRowComponent>
</thead>
);
}
}
export default GridFieldHeaderComponent;

View File

@ -0,0 +1,3 @@
.grid-field-header-component {
}

View File

@ -0,0 +1,5 @@
jest.dontMock('../index');
describe('GridFieldHeaderComponent', () => {
});

View File

@ -0,0 +1,9 @@
# GridFieldRow
Represents a row in a GridField.
## Props
### cells (array)
The table data to display in the row.

View File

@ -0,0 +1,14 @@
import React from 'react';
import SilverStripeComponent from 'silverstripe-component';
class GridFieldRowComponent extends SilverStripeComponent {
render() {
return (
<tr className='grid-field-row-component'>{this.props.children}</tr>
);
}
}
export default GridFieldRowComponent;

View File

@ -0,0 +1,3 @@
.grid-field-row-component {
}

View File

@ -0,0 +1,5 @@
jest.dontMock('../index');
describe('GridFieldRowComponent', () => {
});

View File

@ -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.

View File

@ -0,0 +1,65 @@
import React from 'react';
import SilverStripeComponent from 'silverstripe-component';
class GridFieldComponent extends SilverStripeComponent {
render() {
return (
<table className='grid-field-component [ table ]'>
{this.generateHeader()}
<tbody>
{this.generateRows()}
</tbody>
</table>
);
}
/**
* 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;

View File

@ -0,0 +1,3 @@
.grid-field-component {
}

View File

@ -0,0 +1,5 @@
jest.dontMock('../index');
describe('GridFieldComponent', () => {
});

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,3 @@
# CampaignAdmin
This section is used for managing Campaigns in the CMS.

View File

@ -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) => <GridFieldHeaderCell key={i}>{columnName}</GridFieldHeaderCell>);
const header = <GridFieldHeader>{headerCells}</GridFieldHeader>;
const rows = this.mockData.campaigns.map((campaign, i) => {
const cells = columnNames.map((columnName, i) => {
return <GridFieldCell key={i}>{campaign[columnName]}</GridFieldCell>
});
return <GridFieldRow key={i}>{cells}</GridFieldRow>;
});
return (
<div>
<NorthHeader></NorthHeader>
<GridField header={header} rows={rows}></GridField>
</div>
);
}
}
export default CampaignAdminContainer;

View File

@ -0,0 +1,3 @@
.CampaignAdmin {
}

View File

@ -0,0 +1,5 @@
jest.dontMock('../controller');
describe('CampaignAdminContainer', () => {
});

View File

@ -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;

View File

@ -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'
};

View File

@ -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;

View File

@ -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);
});
});
});

View File

@ -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";

View File

@ -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');
});
});

View File

@ -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
});
});
});
});

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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');
}
}

View File

@ -29,7 +29,7 @@
// $gray-dark: #373a3c;
// $gray: #55595c;
// $gray-light: #818a91;
// $gray-lighter: #eceeef;
$gray-lighter: #e8e9ea;
// $gray-lightest: #f7f7f9;
//
// $brand-primary: #0275d8;

View File

@ -51,6 +51,11 @@
@import "SecurityAdmin.scss";
@import "CMSSecurity.scss";
/** -----------------------------
* Include React components' css
* ------------------------------ */
@import "../javascript/src/styles/main.scss";
/** -----------------------------
* Retina graphics
* ----------------------------- */

View File

@ -4,6 +4,8 @@
* and leave the actual styling to _style.scss and auxilliary files.
*/
@import "../bootstrap/variables.scss";
/** -----------------------------------------------
* Colours
* ------------------------------------------------ */

View File

@ -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
```

View File

@ -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));

View File

@ -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": "<rootDir>/node_modules/babel-jest",
"testPathDirs": [
"admin/javascript/src"
],
"testDirectoryName": "tests",
"mocksPattern": "mocks",
"unmockedModulePathPatterns": [
"<rootDir>/node_modules/react"
],
"bail": true,
"testRunner": "<rootDir>/node_modules/jest-cli/src/testRunners/jasmine/jasmine2.js"
},
"babel": {
"presets": [
"es2015"
]
}
}