From 2a5c92e491dd6280cfb22c31c7bed1b200012466 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Tue, 29 Mar 2016 15:38:48 +1300 Subject: [PATCH] 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. --- admin/code/CampaignAdmin.php | 204 +++++++++++++++++- admin/javascript/src/boot/index.js | 2 + .../src/components/form-builder/index.js | 21 +- .../src/components/grid-field/index.js | 101 +++++---- .../src/sections/campaign-admin/index.js | 4 - admin/javascript/src/silverstripe-backend.js | 46 ++-- .../src/state/campaigns/action-types.js | 8 - .../javascript/src/state/campaigns/actions.js | 0 .../javascript/src/state/campaigns/reducer.js | 49 ----- .../campaigns/tests/campaigns-reducer-test.js | 70 ------ .../src/state/records/action-types.js | 8 + admin/javascript/src/state/records/actions.js | 22 ++ admin/javascript/src/state/records/reducer.js | 45 ++++ .../src/state/records/tests/reducer-test.js | 36 ++++ .../src/tests/silverstripe-backend-test.js | 94 ++++---- package.json | 6 +- 16 files changed, 459 insertions(+), 257 deletions(-) delete mode 100644 admin/javascript/src/state/campaigns/action-types.js delete mode 100644 admin/javascript/src/state/campaigns/actions.js delete mode 100644 admin/javascript/src/state/campaigns/reducer.js delete mode 100644 admin/javascript/src/state/campaigns/tests/campaigns-reducer-test.js create mode 100644 admin/javascript/src/state/records/action-types.js create mode 100644 admin/javascript/src/state/records/actions.js create mode 100644 admin/javascript/src/state/records/reducer.js create mode 100644 admin/javascript/src/state/records/tests/reducer-test.js diff --git a/admin/code/CampaignAdmin.php b/admin/code/CampaignAdmin.php index fd12131b9..02e1b5807 100644 --- a/admin/code/CampaignAdmin.php +++ b/admin/code/CampaignAdmin.php @@ -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 = <<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; } diff --git a/admin/javascript/src/boot/index.js b/admin/javascript/src/boot/index.js index 38066e280..3a450d278 100644 --- a/admin/javascript/src/boot/index.js +++ b/admin/javascript/src/boot/index.js @@ -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()); diff --git a/admin/javascript/src/components/form-builder/index.js b/admin/javascript/src/components/form-builder/index.js index c4ae70928..9ada30800 100644 --- a/admin/javascript/src/components/form-builder/index.js +++ b/admin/javascript/src/components/form-builder/index.js @@ -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; diff --git a/admin/javascript/src/components/grid-field/index.js b/admin/javascript/src/components/grid-field/index.js index 5a09e67c8..1b23c4c8c 100644 --- a/admin/javascript/src/components/grid-field/index.js +++ b/admin/javascript/src/components/grid-field/index.js @@ -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
; + } + + const columns = this.props.data.columns; const actions = [ - , - + , + ]; // Placeholder to align the headers correctly with the content @@ -63,9 +53,11 @@ class GridField extends SilverStripeComponent { const headerCells = columns.map((column, i) => {column.name}); const header = {headerCells.concat(actionPlaceholder)}; - const rows = this.mockData.campaigns.map((campaign, i) => { + const rows = records.map((record, i) => { var cells = columns.map((column, i) => { - return {campaign[column.name]} + // Get value by dot notation + var val = column.field.split('.').reduce((a, b) => a[b], record) + return {val} }); 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); diff --git a/admin/javascript/src/sections/campaign-admin/index.js b/admin/javascript/src/sections/campaign-admin/index.js index 001c09e6e..69c15f30d 100644 --- a/admin/javascript/src/sections/campaign-admin/index.js +++ b/admin/javascript/src/sections/campaign-admin/index.js @@ -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 ($) { diff --git a/admin/javascript/src/silverstripe-backend.js b/admin/javascript/src/silverstripe-backend.js index e896765f6..685bd84af 100644 --- a/admin/javascript/src/silverstripe-backend.js +++ b/admin/javascript/src/silverstripe-backend.js @@ -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); } } diff --git a/admin/javascript/src/state/campaigns/action-types.js b/admin/javascript/src/state/campaigns/action-types.js deleted file mode 100644 index d52b75776..000000000 --- a/admin/javascript/src/state/campaigns/action-types.js +++ /dev/null @@ -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' -}; diff --git a/admin/javascript/src/state/campaigns/actions.js b/admin/javascript/src/state/campaigns/actions.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/admin/javascript/src/state/campaigns/reducer.js b/admin/javascript/src/state/campaigns/reducer.js deleted file mode 100644 index f9e8d271d..000000000 --- a/admin/javascript/src/state/campaigns/reducer.js +++ /dev/null @@ -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; diff --git a/admin/javascript/src/state/campaigns/tests/campaigns-reducer-test.js b/admin/javascript/src/state/campaigns/tests/campaigns-reducer-test.js deleted file mode 100644 index b9d28fa28..000000000 --- a/admin/javascript/src/state/campaigns/tests/campaigns-reducer-test.js +++ /dev/null @@ -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); - }); - - }); - -}); diff --git a/admin/javascript/src/state/records/action-types.js b/admin/javascript/src/state/records/action-types.js new file mode 100644 index 000000000..6982bf82f --- /dev/null +++ b/admin/javascript/src/state/records/action-types.js @@ -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' +}; diff --git a/admin/javascript/src/state/records/actions.js b/admin/javascript/src/state/records/actions.js new file mode 100644 index 000000000..3b7f6750d --- /dev/null +++ b/admin/javascript/src/state/records/actions.js @@ -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}}) + }); + } +} diff --git a/admin/javascript/src/state/records/reducer.js b/admin/javascript/src/state/records/reducer.js new file mode 100644 index 000000000..dbc516aaf --- /dev/null +++ b/admin/javascript/src/state/records/reducer.js @@ -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; diff --git a/admin/javascript/src/state/records/tests/reducer-test.js b/admin/javascript/src/state/records/tests/reducer-test.js new file mode 100644 index 000000000..870d5f122 --- /dev/null +++ b/admin/javascript/src/state/records/tests/reducer-test.js @@ -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); + }) + }); + +}); diff --git a/admin/javascript/src/tests/silverstripe-backend-test.js b/admin/javascript/src/tests/silverstripe-backend-test.js index cded489e9..c3edb3b05 100644 --- a/admin/javascript/src/tests/silverstripe-backend-test.js +++ b/admin/javascript/src/tests/silverstripe-backend-test.js @@ -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'} + ); }); }); diff --git a/package.json b/package.json index c05f64681..cbf73993c 100644 --- a/package.json +++ b/package.json @@ -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" },