mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Generic state management for React GridField
Renaming state operations from 'campaign' to 'record'. Implemented API endpoint retrieval of GridField data. Added more mock data into CampaignAdmin (rather than hardcoding in client), to be replaced by CampaignAdmin API endpoint querying the real datamodel. Using more native isomorphic-fetch instead of jQuery.ajax to minimise dependencies and get into a more forward-thinking API. Also catching errors in ReactJS API backend: Emulate jQuery.ajax() behaviour. Might change at a later point if we have a general purpose backend with a promise-based catch() implementation.
This commit is contained in:
parent
47dd7b48af
commit
2a5c92e491
@ -9,12 +9,15 @@
|
|||||||
class CampaignAdmin extends LeftAndMain implements PermissionProvider {
|
class CampaignAdmin extends LeftAndMain implements PermissionProvider {
|
||||||
|
|
||||||
private static $allowed_actions = [
|
private static $allowed_actions = [
|
||||||
|
'item',
|
||||||
|
'items',
|
||||||
|
'schema',
|
||||||
|
'DetailEditForm',
|
||||||
|
'readCampaigns',
|
||||||
'createCampaign',
|
'createCampaign',
|
||||||
'readCampaign',
|
'readCampaign',
|
||||||
'updateCampaign',
|
'updateCampaign',
|
||||||
'deleteCampaign',
|
'deleteCampaign',
|
||||||
'schema',
|
|
||||||
'DetailEditForm'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
private static $menu_priority = 11;
|
private static $menu_priority = 11;
|
||||||
@ -96,27 +99,33 @@ class CampaignAdmin extends LeftAndMain implements PermissionProvider {
|
|||||||
"customValidationMessage": "",
|
"customValidationMessage": "",
|
||||||
"attributes": [],
|
"attributes": [],
|
||||||
"data": {
|
"data": {
|
||||||
"collectionReadUrl": {
|
"recordType": "ChangeSet",
|
||||||
|
"collectionReadEndpoint": {
|
||||||
"url": "admin\/campaigns\/items",
|
"url": "admin\/campaigns\/items",
|
||||||
"method": "GET"
|
"method": "GET"
|
||||||
},
|
},
|
||||||
"itemReadUrl": {
|
"itemReadEndpoint": {
|
||||||
"url": "admin\/campaigns\/item\/:id",
|
"url": "admin\/campaigns\/item\/:id",
|
||||||
"method": "GET"
|
"method": "GET"
|
||||||
},
|
},
|
||||||
"itemUpdateUrl": {
|
"itemUpdateEndpoint": {
|
||||||
"url": "admin\/campaigns\/item\/:id",
|
"url": "admin\/campaigns\/item\/:id",
|
||||||
"method": "PUT"
|
"method": "PUT"
|
||||||
},
|
},
|
||||||
"itemCreateUrl": {
|
"itemCreateEndpoint": {
|
||||||
"url": "admin\/campaigns\/item\/:id",
|
"url": "admin\/campaigns\/item\/:id",
|
||||||
"method": "POST"
|
"method": "POST"
|
||||||
},
|
},
|
||||||
"itemDeleteUrl": {
|
"itemDeleteEndpoint": {
|
||||||
"url": "admin\/campaigns\/item\/:id",
|
"url": "admin\/campaigns\/item\/:id",
|
||||||
"method": "DELETE"
|
"method": "DELETE"
|
||||||
},
|
},
|
||||||
"editFormSchemaUrl": "admin\/campaigns\/schema\/DetailEditForm"
|
"editFormSchemaEndpoint": "admin\/campaigns\/schema\/DetailEditForm",
|
||||||
|
"columns": [
|
||||||
|
{"name": "Title", "field": "Name"},
|
||||||
|
{"name": "Changes", "field": "_embedded.ChangeSetItems.length"},
|
||||||
|
{"name": "Description", "field": "Description"}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
"name": "SecurityID",
|
"name": "SecurityID",
|
||||||
@ -177,7 +186,182 @@ JSON;
|
|||||||
public function readCampaigns(SS_HTTPRequest $request) {
|
public function readCampaigns(SS_HTTPRequest $request) {
|
||||||
$response = new SS_HTTPResponse();
|
$response = new SS_HTTPResponse();
|
||||||
$response->addHeader('Content-Type', 'application/json');
|
$response->addHeader('Content-Type', 'application/json');
|
||||||
$response->setBody(Convert::raw2json(['campaigns' => 'read']));
|
$json = <<<JSON
|
||||||
|
{
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "/api/ChangeSet/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"count": 3,
|
||||||
|
"total": 3,
|
||||||
|
"_embedded": {
|
||||||
|
"ChangeSets": [
|
||||||
|
{
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "/api/ChangeSet/show/1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ID": 1,
|
||||||
|
"Created": "2016-01-01 00:00:00",
|
||||||
|
"LastEdited": "2016-01-01 00:00:00",
|
||||||
|
"Name": "March 2016 release",
|
||||||
|
"Description": "All the stuff related to the 4.0 announcement",
|
||||||
|
"State": "open",
|
||||||
|
"_embedded": {
|
||||||
|
"ChangeSetItems": [
|
||||||
|
{
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "/api/ChangeSetItem/show/1"
|
||||||
|
},
|
||||||
|
"owns": [
|
||||||
|
{"href": "/api/ChangeSetItem/show/3"},
|
||||||
|
{"href": "/api/ChangeSetItem/show/4"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ID": 1,
|
||||||
|
"Created": "2016-01-01 00:00:00",
|
||||||
|
"LastEdited": "2016-01-01 00:00:00",
|
||||||
|
"VersionBefore": 1,
|
||||||
|
"VersionAfter": 2,
|
||||||
|
"State": "open",
|
||||||
|
"_embedded": {
|
||||||
|
"Object": [
|
||||||
|
{
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "/api/SiteTree/show/1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ID": 1,
|
||||||
|
"ChangeSetCategory": "Page",
|
||||||
|
"Title": "Home",
|
||||||
|
"StatusFlags": ["addedtodraft"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "/api/ChangeSetItem/show/2"
|
||||||
|
},
|
||||||
|
"owns": [
|
||||||
|
{"href": "/api/ChangeSetItem/show/4"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ID": 2,
|
||||||
|
"Created": "2016-01-01 00:00:00",
|
||||||
|
"LastEdited": "2016-01-01 00:00:00",
|
||||||
|
"VersionBefore": 1,
|
||||||
|
"VersionAfter": 2,
|
||||||
|
"State": "open",
|
||||||
|
"_embedded": {
|
||||||
|
"Object": [
|
||||||
|
{
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "/api/SiteTree/show/2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ID": 2,
|
||||||
|
"ChangeSetCategory": "Page",
|
||||||
|
"Title": "Features",
|
||||||
|
"StatusFlags": ["modified"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "/api/ChangeSetItem/show/3"
|
||||||
|
},
|
||||||
|
"ownedby": [
|
||||||
|
{"href": "/api/ChangeSetItem/show/1"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ID": 3,
|
||||||
|
"Created": "2016-01-01 00:00:00",
|
||||||
|
"LastEdited": "2016-01-01 00:00:00",
|
||||||
|
"VersionBefore": 1,
|
||||||
|
"VersionAfter": 2,
|
||||||
|
"State": "open",
|
||||||
|
"_embedded": {
|
||||||
|
"Object": [
|
||||||
|
{
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "/api/File/show/1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ID": 1,
|
||||||
|
"ChangeSetCategory": "File",
|
||||||
|
"Title": "A picture of George",
|
||||||
|
"PreviewThumbnailURL": "/george.jpg",
|
||||||
|
"StatusFlags": ["modified"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "/api/ChangeSetItem/show/4"
|
||||||
|
},
|
||||||
|
"ownedby": [
|
||||||
|
{"href": "/api/ChangeSetItem/show/1"},
|
||||||
|
{"href": "/api/ChangeSetItem/show/2"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ID": 4,
|
||||||
|
"Created": "2016-01-01 00:00:00",
|
||||||
|
"LastEdited": "2016-01-01 00:00:00",
|
||||||
|
"VersionBefore": 1,
|
||||||
|
"VersionAfter": 2,
|
||||||
|
"State": "open",
|
||||||
|
"_embedded": {
|
||||||
|
"Object": [
|
||||||
|
{
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "/api/File/show/2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ID": 2,
|
||||||
|
"ChangeSetCategory": "File",
|
||||||
|
"Title": "Out team",
|
||||||
|
"PreviewThumbnailURL": "/team.jpg",
|
||||||
|
"StatusFlags": ["modified"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "/api/ChangeSet/show/2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ID": 2,
|
||||||
|
"Created": "2016-02-01 00:00:00",
|
||||||
|
"LastEdited": "2016-02-01 00:00:00",
|
||||||
|
"Name": "Shop products",
|
||||||
|
"State": "open",
|
||||||
|
"_embedded": {
|
||||||
|
"ChangeSetItems": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JSON;
|
||||||
|
$response->setBody($json);
|
||||||
|
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
@ -192,7 +376,7 @@ JSON;
|
|||||||
public function readCampaign(SS_HTTPRequest $request) {
|
public function readCampaign(SS_HTTPRequest $request) {
|
||||||
$response = new SS_HTTPResponse();
|
$response = new SS_HTTPResponse();
|
||||||
$response->addHeader('Content-Type', 'application/json');
|
$response->addHeader('Content-Type', 'application/json');
|
||||||
$response->setBody(Convert::raw2json(['campaign' => 'read']));
|
$response->setBody('');
|
||||||
|
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import reducerRegister from 'reducer-register';
|
|||||||
import * as configActions from 'state/config/actions';
|
import * as configActions from 'state/config/actions';
|
||||||
import ConfigReducer from 'state/config/reducer';
|
import ConfigReducer from 'state/config/reducer';
|
||||||
import SchemaReducer from 'state/schema/reducer';
|
import SchemaReducer from 'state/schema/reducer';
|
||||||
|
import RecordsReducer from 'state/records/reducer';
|
||||||
|
|
||||||
// Sections
|
// Sections
|
||||||
import CampaignAdmin from 'sections/campaign-admin';
|
import CampaignAdmin from 'sections/campaign-admin';
|
||||||
@ -14,6 +15,7 @@ import CampaignAdmin from 'sections/campaign-admin';
|
|||||||
function appBoot() {
|
function appBoot() {
|
||||||
reducerRegister.add('config', ConfigReducer);
|
reducerRegister.add('config', ConfigReducer);
|
||||||
reducerRegister.add('schemas', SchemaReducer);
|
reducerRegister.add('schemas', SchemaReducer);
|
||||||
|
reducerRegister.add('records', RecordsReducer);
|
||||||
|
|
||||||
const initialState = {};
|
const initialState = {};
|
||||||
const rootReducer = combineReducers(reducerRegister.getAll());
|
const rootReducer = combineReducers(reducerRegister.getAll());
|
||||||
|
@ -8,6 +8,10 @@ import FormComponent from 'components/form';
|
|||||||
import TextField from 'components/text-field';
|
import TextField from 'components/text-field';
|
||||||
import HiddenField from 'components/hidden-field';
|
import HiddenField from 'components/hidden-field';
|
||||||
import GridField from 'components/grid-field';
|
import GridField from 'components/grid-field';
|
||||||
|
import fetch from 'isomorphic-fetch';
|
||||||
|
|
||||||
|
import es6promise from 'es6-promise';
|
||||||
|
es6promise.polyfill();
|
||||||
|
|
||||||
// Using this to map field types to components until we implement dependency injection.
|
// Using this to map field types to components until we implement dependency injection.
|
||||||
var fakeInjector = {
|
var fakeInjector = {
|
||||||
@ -120,14 +124,17 @@ export class FormBuilderComponent extends SilverStripeComponent {
|
|||||||
headerValues.push('state');
|
headerValues.push('state');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.formSchemaPromise = $.ajax({
|
this.formSchemaPromise = fetch(this.props.schemaUrl, {
|
||||||
method: 'GET',
|
|
||||||
headers: { 'X-FormSchema-Request': headerValues.join() },
|
headers: { 'X-FormSchema-Request': headerValues.join() },
|
||||||
url: this.props.schemaUrl
|
credentials: 'same-origin'
|
||||||
}).done((data, status, xhr) => {
|
})
|
||||||
this.isFetching = false;
|
.then(response => {
|
||||||
this.props.actions.setSchema(data);
|
return response.json();
|
||||||
});
|
})
|
||||||
|
.then(json => {
|
||||||
|
this.isFetching = false;
|
||||||
|
this.props.actions.setSchema(json);
|
||||||
|
});
|
||||||
|
|
||||||
this.isFetching = true;
|
this.isFetching = true;
|
||||||
|
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
import SilverStripeComponent from 'silverstripe-component.js';
|
import SilverStripeComponent from 'silverstripe-component.js';
|
||||||
import GridFieldTable from './table';
|
import GridFieldTable from './table';
|
||||||
import GridFieldHeader from './header';
|
import GridFieldHeader from './header';
|
||||||
@ -6,55 +8,43 @@ import GridFieldHeaderCell from './header-cell';
|
|||||||
import GridFieldRow from './row';
|
import GridFieldRow from './row';
|
||||||
import GridFieldCell from './cell';
|
import GridFieldCell from './cell';
|
||||||
import GridFieldAction from './action';
|
import GridFieldAction from './action';
|
||||||
|
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 Replace "dumb" inner components with third party library (e.g. https://griddlegriddle.github.io)
|
||||||
|
*/
|
||||||
class GridField extends SilverStripeComponent {
|
class GridField extends SilverStripeComponent {
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.deleteCampaign = this.deleteCampaign.bind(this);
|
this.deleteRecord = this.deleteRecord.bind(this);
|
||||||
this.editCampaign = this.editCampaign.bind(this);
|
this.editRecord = this.editRecord.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: This will be an AJAX call and it's response stored in state.
|
componentDidMount() {
|
||||||
this.mockData = {
|
super.componentDidMount();
|
||||||
campaigns: [
|
|
||||||
{
|
let data = this.props.data;
|
||||||
title: 'SilverStripe 4.0 release',
|
|
||||||
description: 'All the stuff related to the 4.0 announcement',
|
this.props.actions.fetchRecords(data.recordType, data.collectionReadEndpoint.method, data.collectionReadEndpoint.url);
|
||||||
changes: 20
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'March release',
|
|
||||||
description: 'march release stuff',
|
|
||||||
changes: 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'About us',
|
|
||||||
description: 'The team',
|
|
||||||
changes: 1345
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const columns = [
|
const records = this.props.records;
|
||||||
{
|
if(!records) {
|
||||||
name: 'title'
|
return <div></div>;
|
||||||
},
|
}
|
||||||
{
|
|
||||||
name: 'changes',
|
const columns = this.props.data.columns;
|
||||||
width: 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'description',
|
|
||||||
width: 6
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const actions = [
|
const actions = [
|
||||||
<GridFieldAction icon={'cog'} handleClick={this.editCampaign} />,
|
<GridFieldAction icon={'cog'} handleClick={this.editRecord} />,
|
||||||
<GridFieldAction icon={'cancel'} handleClick={this.deleteCampaign} />
|
<GridFieldAction icon={'cancel'} handleClick={this.deleteRecord} />
|
||||||
];
|
];
|
||||||
|
|
||||||
// Placeholder to align the headers correctly with the content
|
// Placeholder to align the headers correctly with the content
|
||||||
@ -63,9 +53,11 @@ class GridField extends SilverStripeComponent {
|
|||||||
const headerCells = columns.map((column, i) => <GridFieldHeaderCell key={i} width={column.width}>{column.name}</GridFieldHeaderCell>);
|
const headerCells = columns.map((column, i) => <GridFieldHeaderCell key={i} width={column.width}>{column.name}</GridFieldHeaderCell>);
|
||||||
const header = <GridFieldHeader>{headerCells.concat(actionPlaceholder)}</GridFieldHeader>;
|
const header = <GridFieldHeader>{headerCells.concat(actionPlaceholder)}</GridFieldHeader>;
|
||||||
|
|
||||||
const rows = this.mockData.campaigns.map((campaign, i) => {
|
const rows = records.map((record, i) => {
|
||||||
var cells = columns.map((column, i) => {
|
var cells = columns.map((column, i) => {
|
||||||
return <GridFieldCell key={i} width={column.width}>{campaign[column.name]}</GridFieldCell>
|
// Get value by dot notation
|
||||||
|
var val = column.field.split('.').reduce((a, b) => a[b], record)
|
||||||
|
return <GridFieldCell key={i} width={column.width}>{val}</GridFieldCell>
|
||||||
});
|
});
|
||||||
|
|
||||||
var rowActions = actions.map((action, j) => {
|
var rowActions = actions.map((action, j) => {
|
||||||
@ -82,14 +74,35 @@ class GridField extends SilverStripeComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteCampaign(event) {
|
deleteRecord(event) {
|
||||||
// delete campaign
|
// delete record
|
||||||
}
|
}
|
||||||
|
|
||||||
editCampaign(event) {
|
editRecord(event) {
|
||||||
// edit campaign
|
// edit record
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GridField;
|
GridField.propTypes = {
|
||||||
|
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] : []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return {
|
||||||
|
actions: bindActionCreators(actions, dispatch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(GridField);
|
||||||
|
@ -4,10 +4,6 @@ import React from 'react';
|
|||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import CampaignAdmin from './controller';
|
import CampaignAdmin from './controller';
|
||||||
import campaignsReducer from 'state/campaigns/reducer';
|
|
||||||
|
|
||||||
// TODO: Move this to the controller.
|
|
||||||
reducerRegister.add('campaigns', campaignsReducer);
|
|
||||||
|
|
||||||
$.entwine('ss', function ($) {
|
$.entwine('ss', function ($) {
|
||||||
|
|
||||||
|
@ -1,16 +1,36 @@
|
|||||||
import $ from 'jQuery';
|
import fetch from 'isomorphic-fetch';
|
||||||
|
import es6promise from 'es6-promise';
|
||||||
|
es6promise.polyfill();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://github.com/github/fetch#handling-http-error-statuses
|
||||||
|
*/
|
||||||
|
function checkStatus(response) {
|
||||||
|
if (response.status >= 200 && response.status < 300) {
|
||||||
|
return response
|
||||||
|
} else {
|
||||||
|
var error = new Error(response.statusText)
|
||||||
|
error.response = response
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class SilverStripeBackend {
|
class SilverStripeBackend {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Allow mocking
|
||||||
|
this.fetch = fetch;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes a network request using the GET HTTP verb.
|
* Makes a network request using the GET HTTP verb.
|
||||||
*
|
*
|
||||||
* @param string url - Endpoint URL.
|
* @param string url - Endpoint URL.
|
||||||
*
|
* @return object - Promise
|
||||||
* @return object - jqXHR. See http://api.jquery.com/Types/#jqXHR
|
|
||||||
*/
|
*/
|
||||||
get(url) {
|
get(url) {
|
||||||
return $.ajax({ type: 'GET', url });
|
return this.fetch(url, { method: 'get', credentials: 'same-origin' })
|
||||||
|
.then(checkStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -18,11 +38,11 @@ class SilverStripeBackend {
|
|||||||
*
|
*
|
||||||
* @param string url - Endpoint URL.
|
* @param string url - Endpoint URL.
|
||||||
* @param object data - Data to send with the request.
|
* @param object data - Data to send with the request.
|
||||||
*
|
* @return object - Promise
|
||||||
* @return object - jqXHR. See http://api.jquery.com/Types/#jqXHR
|
|
||||||
*/
|
*/
|
||||||
post(url, data) {
|
post(url, data) {
|
||||||
return $.ajax({ type: 'POST', url, data });
|
return this.fetch(url, { method: 'post', credentials: 'same-origin', body: data })
|
||||||
|
.then(checkStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -30,11 +50,11 @@ class SilverStripeBackend {
|
|||||||
*
|
*
|
||||||
* @param string url - Endpoint URL.
|
* @param string url - Endpoint URL.
|
||||||
* @param object data - Data to send with the request.
|
* @param object data - Data to send with the request.
|
||||||
*
|
* @return object - Promise
|
||||||
* @return object - jqXHR. See http://api.jquery.com/Types/#jqXHR
|
|
||||||
*/
|
*/
|
||||||
put(url, data) {
|
put(url, data) {
|
||||||
return $.ajax({ type: 'PUT', url, data });
|
return this.fetch(url, { method: 'put', credentials: 'same-origin', body: data })
|
||||||
|
.then(checkStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -42,11 +62,11 @@ class SilverStripeBackend {
|
|||||||
*
|
*
|
||||||
* @param string url - Endpoint URL.
|
* @param string url - Endpoint URL.
|
||||||
* @param object data - Data to send with the request.
|
* @param object data - Data to send with the request.
|
||||||
*
|
* @return object - Promise
|
||||||
* @return object - jqXHR. See http://api.jquery.com/Types/#jqXHR
|
|
||||||
*/
|
*/
|
||||||
delete(url, data) {
|
delete(url, data) {
|
||||||
return $.ajax({ type: 'DELETE', url, data });
|
return this.fetch(url, { method: 'delete', credentials: 'same-origin', body: data })
|
||||||
|
.then(checkStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
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'
|
|
||||||
};
|
|
@ -1,49 +0,0 @@
|
|||||||
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;
|
|
@ -1,70 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
8
admin/javascript/src/state/records/action-types.js
Normal file
8
admin/javascript/src/state/records/action-types.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
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'
|
||||||
|
};
|
22
admin/javascript/src/state/records/actions.js
Normal file
22
admin/javascript/src/state/records/actions.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import ACTION_TYPES from './action-types';
|
||||||
|
import fetch from 'isomorphic-fetch';
|
||||||
|
import backend from 'silverstripe-backend.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all records
|
||||||
|
*
|
||||||
|
* @param string recordType Type of record (the "class name")
|
||||||
|
* @param string method HTTP methods
|
||||||
|
* @param string url API endpoint
|
||||||
|
*/
|
||||||
|
export function fetchRecords(recordType, method, url) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch ({type: ACTION_TYPES.FETCH_RECORDS_REQUEST, payload: {recordType: recordType}});
|
||||||
|
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}})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
45
admin/javascript/src/state/records/reducer.js
Normal file
45
admin/javascript/src/state/records/reducer.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import deepFreeze from 'deep-freeze';
|
||||||
|
import ACTION_TYPES from './action-types';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
};
|
||||||
|
|
||||||
|
function recordsReducer(state = initialState, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
|
||||||
|
case ACTION_TYPES.CREATE_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.FETCH_RECORDS_REQUEST:
|
||||||
|
return state;
|
||||||
|
|
||||||
|
case ACTION_TYPES.FETCH_RECORDS_FAILURE:
|
||||||
|
return state;
|
||||||
|
|
||||||
|
case ACTION_TYPES.FETCH_RECORDS_SUCCESS:
|
||||||
|
let recordType = action.payload.recordType;
|
||||||
|
// TODO Automatic pluralisation from recordType
|
||||||
|
let records = action.payload.data._embedded[recordType + 's'];
|
||||||
|
return deepFreeze(Object.assign({}, state, {
|
||||||
|
[recordType]: records
|
||||||
|
}));
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default recordsReducer;
|
36
admin/javascript/src/state/records/tests/reducer-test.js
Normal file
36
admin/javascript/src/state/records/tests/reducer-test.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
jest.dontMock('deep-freeze');
|
||||||
|
jest.dontMock('../reducer');
|
||||||
|
jest.dontMock('../action-types');
|
||||||
|
|
||||||
|
var recordsReducer = require('../reducer').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}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -1,42 +1,48 @@
|
|||||||
jest.mock('jQuery');
|
jest.mock('isomorphic-fetch');
|
||||||
jest.unmock('../silverstripe-backend');
|
jest.unmock('../silverstripe-backend');
|
||||||
|
|
||||||
import $ from 'jQuery';
|
import fetch from 'isomorphic-fetch';
|
||||||
import backend from '../silverstripe-backend';
|
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);
|
||||||
|
|
||||||
|
return mock;
|
||||||
|
};
|
||||||
|
|
||||||
describe('SilverStripeBackend', () => {
|
describe('SilverStripeBackend', () => {
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
let fetchMock = getFetchMock();
|
||||||
|
backend.fetch = fetchMock;
|
||||||
|
});
|
||||||
|
|
||||||
describe('get()', () => {
|
describe('get()', () => {
|
||||||
|
|
||||||
it('should return a jqXHR', () => {
|
it('should return a promise', () => {
|
||||||
var jqxhr = backend.get('http://example.com');
|
var promise = backend.get('http://example.com');
|
||||||
|
expect(typeof promise).toBe('object');
|
||||||
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', () => {
|
it('should send a GET request to an endpoint', () => {
|
||||||
backend.get('http://example.com');
|
backend.get('http://example.com');
|
||||||
|
expect(backend.fetch).toBeCalledWith(
|
||||||
expect($.ajax).toBeCalledWith({
|
'http://example.com',
|
||||||
type: 'GET',
|
{method: 'get', credentials: 'same-origin'}
|
||||||
url: 'http://example.com'
|
);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('post()', () => {
|
describe('post()', () => {
|
||||||
|
|
||||||
it('should return a jqXHR', () => {
|
it('should return a promise', () => {
|
||||||
var jqxhr = backend.get('http://example.com/item');
|
var promise = backend.get('http://example.com/item');
|
||||||
|
expect(typeof promise).toBe('object');
|
||||||
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', () => {
|
it('should send a POST request to an endpoint', () => {
|
||||||
@ -44,24 +50,19 @@ describe('SilverStripeBackend', () => {
|
|||||||
|
|
||||||
backend.post('http://example.com', postData);
|
backend.post('http://example.com', postData);
|
||||||
|
|
||||||
expect($.ajax).toBeCalledWith({
|
expect(backend.fetch).toBeCalledWith(
|
||||||
type: 'POST',
|
'http://example.com',
|
||||||
url: 'http://example.com',
|
{method: 'post', body: postData, credentials: 'same-origin'}
|
||||||
data: postData
|
);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('put()', () => {
|
describe('put()', () => {
|
||||||
|
|
||||||
it('should return a jqXHR', () => {
|
it('should return a promise', () => {
|
||||||
var jqxhr = backend.get('http://example.com/item');
|
var promise = backend.get('http://example.com/item');
|
||||||
|
expect(typeof promise).toBe('object');
|
||||||
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', () => {
|
it('should send a PUT request to an endpoint', () => {
|
||||||
@ -69,24 +70,20 @@ describe('SilverStripeBackend', () => {
|
|||||||
|
|
||||||
backend.put('http://example.com', putData);
|
backend.put('http://example.com', putData);
|
||||||
|
|
||||||
expect($.ajax).toBeCalledWith({
|
expect(backend.fetch).toBeCalledWith(
|
||||||
type: 'PUT',
|
'http://example.com',
|
||||||
url: 'http://example.com',
|
{method: 'put', body: putData, credentials: 'same-origin'}
|
||||||
data: putData
|
);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('delete()', () => {
|
describe('delete()', () => {
|
||||||
|
|
||||||
it('should return a jqXHR', () => {
|
it('should return a promise', () => {
|
||||||
var jqxhr = backend.get('http://example.com/item');
|
var promise = backend.get('http://example.com/item');
|
||||||
|
|
||||||
expect(typeof jqxhr).toBe('object');
|
expect(typeof promise).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', () => {
|
it('should send a DELETE request to an endpoint', () => {
|
||||||
@ -94,11 +91,10 @@ describe('SilverStripeBackend', () => {
|
|||||||
|
|
||||||
backend.delete('http://example.com', deleteData);
|
backend.delete('http://example.com', deleteData);
|
||||||
|
|
||||||
expect($.ajax).toBeCalledWith({
|
expect(backend.fetch).toBeCalledWith(
|
||||||
type: 'DELETE',
|
'http://example.com',
|
||||||
url: 'http://example.com',
|
{method: 'delete', body: deleteData, credentials: 'same-origin'}
|
||||||
data: deleteData
|
);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -39,15 +39,15 @@
|
|||||||
"blueimp-tmpl": "^1.0.2",
|
"blueimp-tmpl": "^1.0.2",
|
||||||
"bootstrap": "^4.0.0-alpha.2",
|
"bootstrap": "^4.0.0-alpha.2",
|
||||||
"deep-freeze": "0.0.1",
|
"deep-freeze": "0.0.1",
|
||||||
|
"es6-promise": "^3.1.2",
|
||||||
|
"isomorphic-fetch": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz",
|
||||||
"jquery-sizes": "^0.33.0",
|
"jquery-sizes": "^0.33.0",
|
||||||
"npm-shrinkwrap": "^5.4.1",
|
|
||||||
"page.js": "^4.13.3",
|
"page.js": "^4.13.3",
|
||||||
"react": "^0.14.7",
|
"react": "^0.14.7",
|
||||||
"react-addons-css-transition-group": "^0.14.7",
|
"react-addons-css-transition-group": "^0.14.7",
|
||||||
"react-addons-test-utils": "^0.14.7",
|
|
||||||
"react-dom": "^0.14.7",
|
"react-dom": "^0.14.7",
|
||||||
"react-redux": "^4.0.6",
|
"react-redux": "^4.0.6",
|
||||||
"redux": "^3.3.1",
|
"redux": "https://registry.npmjs.org/redux/-/redux-3.0.5.tgz",
|
||||||
"redux-thunk": "^1.0.3",
|
"redux-thunk": "^1.0.3",
|
||||||
"tinymce": "^4.3.3"
|
"tinymce": "^4.3.3"
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user