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:
Ingo Schommer 2016-03-29 15:38:48 +13:00
parent 47dd7b48af
commit 2a5c92e491
16 changed files with 459 additions and 257 deletions

View File

@ -9,12 +9,15 @@
class CampaignAdmin extends LeftAndMain implements PermissionProvider {
private static $allowed_actions = [
'item',
'items',
'schema',
'DetailEditForm',
'readCampaigns',
'createCampaign',
'readCampaign',
'updateCampaign',
'deleteCampaign',
'schema',
'DetailEditForm'
];
private static $menu_priority = 11;
@ -96,27 +99,33 @@ class CampaignAdmin extends LeftAndMain implements PermissionProvider {
"customValidationMessage": "",
"attributes": [],
"data": {
"collectionReadUrl": {
"recordType": "ChangeSet",
"collectionReadEndpoint": {
"url": "admin\/campaigns\/items",
"method": "GET"
},
"itemReadUrl": {
"itemReadEndpoint": {
"url": "admin\/campaigns\/item\/:id",
"method": "GET"
},
"itemUpdateUrl": {
"itemUpdateEndpoint": {
"url": "admin\/campaigns\/item\/:id",
"method": "PUT"
},
"itemCreateUrl": {
"itemCreateEndpoint": {
"url": "admin\/campaigns\/item\/:id",
"method": "POST"
},
"itemDeleteUrl": {
"itemDeleteEndpoint": {
"url": "admin\/campaigns\/item\/:id",
"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",
@ -177,7 +186,182 @@ JSON;
public function readCampaigns(SS_HTTPRequest $request) {
$response = new SS_HTTPResponse();
$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;
}
@ -192,7 +376,7 @@ JSON;
public function readCampaign(SS_HTTPRequest $request) {
$response = new SS_HTTPResponse();
$response->addHeader('Content-Type', 'application/json');
$response->setBody(Convert::raw2json(['campaign' => 'read']));
$response->setBody('');
return $response;
}

View File

@ -7,6 +7,7 @@ import reducerRegister from 'reducer-register';
import * as configActions from 'state/config/actions';
import ConfigReducer from 'state/config/reducer';
import SchemaReducer from 'state/schema/reducer';
import RecordsReducer from 'state/records/reducer';
// Sections
import CampaignAdmin from 'sections/campaign-admin';
@ -14,6 +15,7 @@ import CampaignAdmin from 'sections/campaign-admin';
function appBoot() {
reducerRegister.add('config', ConfigReducer);
reducerRegister.add('schemas', SchemaReducer);
reducerRegister.add('records', RecordsReducer);
const initialState = {};
const rootReducer = combineReducers(reducerRegister.getAll());

View File

@ -8,6 +8,10 @@ import FormComponent from 'components/form';
import TextField from 'components/text-field';
import HiddenField from 'components/hidden-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.
var fakeInjector = {
@ -120,14 +124,17 @@ export class FormBuilderComponent extends SilverStripeComponent {
headerValues.push('state');
}
this.formSchemaPromise = $.ajax({
method: 'GET',
this.formSchemaPromise = fetch(this.props.schemaUrl, {
headers: { 'X-FormSchema-Request': headerValues.join() },
url: this.props.schemaUrl
}).done((data, status, xhr) => {
this.isFetching = false;
this.props.actions.setSchema(data);
});
credentials: 'same-origin'
})
.then(response => {
return response.json();
})
.then(json => {
this.isFetching = false;
this.props.actions.setSchema(json);
});
this.isFetching = true;

View File

@ -1,4 +1,6 @@
import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import SilverStripeComponent from 'silverstripe-component.js';
import GridFieldTable from './table';
import GridFieldHeader from './header';
@ -6,55 +8,43 @@ import GridFieldHeaderCell from './header-cell';
import GridFieldRow from './row';
import GridFieldCell from './cell';
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 {
constructor(props) {
super(props);
this.deleteCampaign = this.deleteCampaign.bind(this);
this.editCampaign = this.editCampaign.bind(this);
this.deleteRecord = this.deleteRecord.bind(this);
this.editRecord = this.editRecord.bind(this);
}
// 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
}
]
};
componentDidMount() {
super.componentDidMount();
let data = this.props.data;
this.props.actions.fetchRecords(data.recordType, data.collectionReadEndpoint.method, data.collectionReadEndpoint.url);
}
render() {
const columns = [
{
name: 'title'
},
{
name: 'changes',
width: 2
},
{
name: 'description',
width: 6
}
];
const records = this.props.records;
if(!records) {
return <div></div>;
}
const columns = this.props.data.columns;
const actions = [
<GridFieldAction icon={'cog'} handleClick={this.editCampaign} />,
<GridFieldAction icon={'cancel'} handleClick={this.deleteCampaign} />
<GridFieldAction icon={'cog'} handleClick={this.editRecord} />,
<GridFieldAction icon={'cancel'} handleClick={this.deleteRecord} />
];
// 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 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) => {
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) => {
@ -82,14 +74,35 @@ class GridField extends SilverStripeComponent {
);
}
deleteCampaign(event) {
// delete campaign
deleteRecord(event) {
// delete record
}
editCampaign(event) {
// edit campaign
editRecord(event) {
// 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);

View File

@ -4,10 +4,6 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import CampaignAdmin from './controller';
import campaignsReducer from 'state/campaigns/reducer';
// TODO: Move this to the controller.
reducerRegister.add('campaigns', campaignsReducer);
$.entwine('ss', function ($) {

View File

@ -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 {
constructor() {
// Allow mocking
this.fetch = fetch;
}
/**
* Makes a network request using the GET HTTP verb.
*
* @param string url - Endpoint URL.
*
* @return object - jqXHR. See http://api.jquery.com/Types/#jqXHR
* @return object - Promise
*/
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 object data - Data to send with the request.
*
* @return object - jqXHR. See http://api.jquery.com/Types/#jqXHR
* @return object - Promise
*/
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 object data - Data to send with the request.
*
* @return object - jqXHR. See http://api.jquery.com/Types/#jqXHR
* @return object - Promise
*/
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 object data - Data to send with the request.
*
* @return object - jqXHR. See http://api.jquery.com/Types/#jqXHR
* @return object - Promise
*/
delete(url, data) {
return $.ajax({ type: 'DELETE', url, data });
return this.fetch(url, { method: 'delete', credentials: 'same-origin', body: data })
.then(checkStatus);
}
}

View File

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

View File

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

View File

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

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

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

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

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

View File

@ -1,42 +1,48 @@
jest.mock('jQuery');
jest.mock('isomorphic-fetch');
jest.unmock('../silverstripe-backend');
import $ from 'jQuery';
import fetch from 'isomorphic-fetch';
import backend from '../silverstripe-backend';
var getFetchMock = function(data) {
let mock = jest.genMockFunction();
let promise = new Promise((resolve, reject) => {
process.nextTick(() => resolve(data));
});
mock.mockReturnValue(promise);
return mock;
};
describe('SilverStripeBackend', () => {
beforeAll(() => {
let fetchMock = getFetchMock();
backend.fetch = fetchMock;
});
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 return a promise', () => {
var promise = backend.get('http://example.com');
expect(typeof promise).toBe('object');
});
it('should send a GET request to an endpoint', () => {
backend.get('http://example.com');
expect($.ajax).toBeCalledWith({
type: 'GET',
url: 'http://example.com'
});
expect(backend.fetch).toBeCalledWith(
'http://example.com',
{method: 'get', credentials: 'same-origin'}
);
});
});
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 return a promise', () => {
var promise = backend.get('http://example.com/item');
expect(typeof promise).toBe('object');
});
it('should send a POST request to an endpoint', () => {
@ -44,24 +50,19 @@ describe('SilverStripeBackend', () => {
backend.post('http://example.com', postData);
expect($.ajax).toBeCalledWith({
type: 'POST',
url: 'http://example.com',
data: postData
});
expect(backend.fetch).toBeCalledWith(
'http://example.com',
{method: 'post', body: postData, credentials: 'same-origin'}
);
});
});
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 return a promise', () => {
var promise = backend.get('http://example.com/item');
expect(typeof promise).toBe('object');
});
it('should send a PUT request to an endpoint', () => {
@ -69,24 +70,20 @@ describe('SilverStripeBackend', () => {
backend.put('http://example.com', putData);
expect($.ajax).toBeCalledWith({
type: 'PUT',
url: 'http://example.com',
data: putData
});
expect(backend.fetch).toBeCalledWith(
'http://example.com',
{method: 'put', body: putData, credentials: 'same-origin'}
);
});
});
describe('delete()', () => {
it('should return a jqXHR', () => {
var jqxhr = backend.get('http://example.com/item');
it('should return a promise', () => {
var promise = 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');
expect(typeof promise).toBe('object');
});
it('should send a DELETE request to an endpoint', () => {
@ -94,11 +91,10 @@ describe('SilverStripeBackend', () => {
backend.delete('http://example.com', deleteData);
expect($.ajax).toBeCalledWith({
type: 'DELETE',
url: 'http://example.com',
data: deleteData
});
expect(backend.fetch).toBeCalledWith(
'http://example.com',
{method: 'delete', body: deleteData, credentials: 'same-origin'}
);
});
});

View File

@ -39,15 +39,15 @@
"blueimp-tmpl": "^1.0.2",
"bootstrap": "^4.0.0-alpha.2",
"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",
"npm-shrinkwrap": "^5.4.1",
"page.js": "^4.13.3",
"react": "^0.14.7",
"react-addons-css-transition-group": "^0.14.7",
"react-addons-test-utils": "^0.14.7",
"react-dom": "^0.14.7",
"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",
"tinymce": "^4.3.3"
},