Skip to content
Snippets Groups Projects
Commit 5d9ab89b authored by Jack Reed's avatar Jack Reed
Browse files

refactor image information requests to be handles through redux actions

parent 1c35cc4a
Branches
Tags
No related merge requests found
......@@ -133,4 +133,77 @@ describe('actions', () => {
expect(actions.removeManifest('foo')).toEqual(expectedAction);
});
});
describe('requestInfoResponse', () => {
it('requests an infoResponse from given a url', () => {
const id = 'abc123';
const expectedAction = {
type: ActionTypes.REQUEST_INFO_RESPONSE,
infoId: id,
};
expect(actions.requestInfoResponse(id)).toEqual(expectedAction);
});
});
describe('receiveInfoResponse', () => {
it('recieves an infoResponse', () => {
const id = 'abc123';
const json = {
id,
content: 'image information request',
};
const expectedAction = {
type: ActionTypes.RECEIVE_INFO_RESPONSE,
infoId: id,
infoJson: json,
};
expect(actions.receiveInfoResponse(id, json)).toEqual(expectedAction);
});
});
describe('fetchInfoResponse', () => {
let store = null;
beforeEach(() => {
store = mockStore({});
});
describe('success response', () => {
beforeEach(() => {
fetch.mockResponseOnce(JSON.stringify({ data: '12345' })); // eslint-disable-line no-undef
});
it('dispatches the REQUEST_MANIFEST action', () => {
store.dispatch(actions.fetchInfoResponse('https://stacks.stanford.edu/image/iiif/sn904cj3429%2F12027000/info.json'));
expect(store.getActions()).toEqual([
{ infoId: 'https://stacks.stanford.edu/image/iiif/sn904cj3429%2F12027000/info.json', type: 'REQUEST_INFO_RESPONSE' },
]);
});
it('dispatches the REQUEST_MANIFEST and then RECEIVE_MANIFEST', () => {
store.dispatch(actions.fetchInfoResponse('https://stacks.stanford.edu/image/iiif/sn904cj3429%2F12027000/info.json'))
.then(() => {
const expectedActions = store.getActions();
expect(expectedActions).toEqual([
{ infoId: 'https://stacks.stanford.edu/image/iiif/sn904cj3429%2F12027000/info.json', type: 'REQUEST_INFO_RESPONSE' },
{ infoId: 'https://stacks.stanford.edu/image/iiif/sn904cj3429%2F12027000/info.json', infoJson: { data: '12345' }, type: 'RECEIVE_INFO_RESPONSE' },
]);
});
});
});
describe('error response', () => {
it('dispatches the REQUEST_INFO_RESPONSE and then RECEIVE_INFO_RESPONSE', () => {
store.dispatch(actions.fetchInfoResponse('https://stacks.stanford.edu/image/iiif/sn904cj3429%2F12027000/info.json'))
.then(() => {
const expectedActions = store.getActions();
expect(expectedActions).toEqual([
{ infoId: 'https://stacks.stanford.edu/image/iiif/sn904cj3429%2F12027000/info.json', type: 'REQUEST_INFO_RESPONSE' },
{ infoId: 'https://stacks.stanford.edu/image/iiif/sn904cj3429%2F12027000/info.json', error: new Error('invalid json response body at undefined reason: Unexpected end of JSON input'), type: 'RECEIVE_INFO_RESPONSE_FAILURE' },
]);
});
});
});
});
describe('removeInfoResponse', () => {
it('removes an existing infoResponse', () => {
const expectedAction = {
type: ActionTypes.REMOVE_INFO_RESPONSE,
infoId: 'foo',
};
expect(actions.removeInfoResponse('foo')).toEqual(expectedAction);
});
});
});
import reducer from '../../../src/reducers/infoResponses';
import ActionTypes from '../../../src/action-types';
describe('manifests reducer', () => {
it('should handle REQUEST_INFO_RESPONSE', () => {
expect(reducer({}, {
type: ActionTypes.REQUEST_INFO_RESPONSE,
infoId: 'abc123',
})).toEqual({
abc123: {
isFetching: true,
},
});
});
it('should handle RECEIVE_INFO_RESPONSE', () => {
expect(reducer(
{
abc123: {
isFetching: true,
},
},
{
type: ActionTypes.RECEIVE_INFO_RESPONSE,
infoId: 'abc123',
infoJson: {
id: 'abc123',
'@type': 'sc:Manifest',
content: 'lots of canvases and metadata and such',
},
},
)).toMatchObject({
abc123: {
isFetching: false,
json: {},
},
});
});
it('should handle RECEIVE_INFO_RESPONSE_FAILURE', () => {
expect(reducer(
{
abc123: {
isFetching: true,
},
},
{
type: ActionTypes.RECEIVE_INFO_RESPONSE_FAILURE,
infoId: 'abc123',
error: "This institution didn't enable CORS.",
},
)).toEqual({
abc123: {
isFetching: false,
error: "This institution didn't enable CORS.",
},
});
});
it('should handle REMOVE_INFO_RESPONSE', () => {
expect(reducer(
{
abc123: {
stuff: 'foo',
},
},
{
type: ActionTypes.REMOVE_INFO_RESPONSE,
infoId: 'abc123',
},
)).toEqual({});
});
});
......@@ -12,6 +12,10 @@ const ActionTypes = {
SET_CONFIG: 'SET_CONFIG',
UPDATE_CONFIG: 'UPDATE_CONFIG',
REMOVE_MANIFEST: 'REMOVE_MANIFEST',
REQUEST_INFO_RESPONSE: 'REQUEST_INFO_RESPONSE',
RECEIVE_INFO_RESPONSE: 'RECEIVE_INFO_RESPONSE',
RECEIVE_INFO_RESPONSE_FAILURE: 'RECEIVE_INFO_RESPONSE_FAILURE',
REMOVE_INFO_RESPONSE: 'REMOVE_INFO_RESPONSE',
};
export default ActionTypes;
......@@ -156,3 +156,72 @@ export function fetchManifest(manifestId) {
export function removeManifest(manifestId) {
return { type: ActionTypes.REMOVE_MANIFEST, manifestId };
}
/**
* requestInfoResponse - action creator
*
* @param {String} infoId
* @memberof ActionCreators
*/
export function requestInfoResponse(infoId) {
return {
type: ActionTypes.REQUEST_INFO_RESPONSE,
infoId,
};
}
/**
* receiveInfoResponse - action creator
*
* @param {String} infoId
* @param {Object} manifestJson
* @memberof ActionCreators
*/
export function receiveInfoResponse(infoId, infoJson) {
return {
type: ActionTypes.RECEIVE_INFO_RESPONSE,
infoId,
infoJson,
};
}
/**
* receiveInfoResponseFailure - action creator
*
* @param {String} infoId
* @param {String} error
* @memberof ActionCreators
*/
export function receiveInfoResponseFailure(infoId, error) {
return {
type: ActionTypes.RECEIVE_INFO_RESPONSE_FAILURE,
infoId,
error,
};
}
/**
* fetchInfoResponse - action creator
*
* @param {String} infoId
* @memberof ActionCreators
*/
export function fetchInfoResponse(infoId) {
return ((dispatch) => {
dispatch(requestInfoResponse(infoId));
return fetch(infoId)
.then(response => response.json())
.then(json => dispatch(receiveInfoResponse(infoId, json)))
.catch(error => dispatch(receiveInfoResponseFailure(infoId, error)));
});
}
/**
* removeInfoResponse - action creator
*
* @param {String} infoId
* @memberof ActionCreators
*/
export function removeInfoResponse(infoId) {
return { type: ActionTypes.REMOVE_INFO_RESPONSE, infoId };
}
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import fetch from 'node-fetch';
import { connect } from 'react-redux';
import { actions } from '../store';
import miradorWithPlugins from '../lib/miradorWithPlugins';
import OpenSeadragonViewer from './OpenSeadragonViewer';
import ViewerNavigation from './ViewerNavigation';
......@@ -18,9 +19,6 @@ class WindowViewer extends Component {
const { manifest } = this.props;
this.canvases = manifest.manifestation.getSequences()[0].getCanvases();
this.state = {
tileSources: [],
};
}
/**
......@@ -28,7 +26,8 @@ class WindowViewer extends Component {
* Request the initial canvas on mount
*/
componentDidMount() {
this.requestAndUpdateTileSources();
const { fetchInfoResponse } = this.props;
fetchInfoResponse(this.imageInformationUri());
}
/**
......@@ -36,23 +35,43 @@ class WindowViewer extends Component {
* Request a new canvas if it is needed
*/
componentDidUpdate(prevProps) {
const { window } = this.props;
if (prevProps.window.canvasIndex !== window.canvasIndex) {
this.requestAndUpdateTileSources();
const { window, fetchInfoResponse } = this.props;
if (prevProps.window.canvasIndex !== window.canvasIndex && !this.infoResponseIsInStore()) {
fetchInfoResponse(this.imageInformationUri());
}
}
/**
* infoResponseIsInStore - checks whether or not an info response is already
* in the store. No need to request it again.
* @return [Boolean]
*/
infoResponseIsInStore() {
const { infoResponses } = this.props;
const currentInfoResponse = infoResponses[this.imageInformationUri()];
return (currentInfoResponse !== undefined
&& currentInfoResponse.isFetching === false
&& currentInfoResponse.json !== undefined);
}
/**
* Constructs an image information URI to request from a canvas
*/
requestAndUpdateTileSources() {
imageInformationUri() {
const { window } = this.props;
fetch(`${this.canvases[window.canvasIndex].getImages()[0].getResource().getServices()[0].id}/info.json`)
.then(response => response.json())
.then((json) => {
this.setState({
tileSources: [json],
});
});
return `${this.canvases[window.canvasIndex].getImages()[0].getResource().getServices()[0].id}/info.json`;
}
/**
* Return an image information response from the store for the correct image
*/
tileInfoFetchedFromStore() {
const { infoResponses } = this.props;
return [infoResponses[this.imageInformationUri()]]
.filter(infoResponse => (infoResponse !== undefined
&& infoResponse.isFetching === false
&& infoResponse.error === undefined))
.map(infoResponse => infoResponse.json);
}
/**
......@@ -60,19 +79,43 @@ class WindowViewer extends Component {
*/
render() {
const { window } = this.props;
const { tileSources } = this.state;
return (
<Fragment>
<OpenSeadragonViewer tileSources={tileSources} window={window} />
<OpenSeadragonViewer
tileSources={this.tileInfoFetchedFromStore()}
window={window}
/>
<ViewerNavigation window={window} canvases={this.canvases} />
</Fragment>
);
}
}
/**
* mapStateToProps - to hook up connect
* @memberof WindowViewer
* @private
*/
const mapStateToProps = state => (
{
infoResponses: state.infoResponses,
}
);
/**
* mapDispatchToProps - used to hook up connect to action creators
* @memberof WindowViewer
* @private
*/
const mapDispatchToProps = dispatch => ({
fetchInfoResponse: infoId => dispatch(actions.fetchInfoResponse(infoId)),
});
WindowViewer.propTypes = {
infoResponses: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
fetchInfoResponse: PropTypes.func.isRequired,
manifest: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
window: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
};
export default miradorWithPlugins(WindowViewer);
export default connect(mapStateToProps, mapDispatchToProps)(miradorWithPlugins(WindowViewer));
......@@ -3,6 +3,7 @@ import workspaceReducer from './workspace';
import windowsReducer from './windows';
import manifestsReducer from './manifests';
import configReducer from './config';
import infoResponsesReducer from './infoResponses';
/**
* Action Creators for Mirador
......@@ -14,6 +15,7 @@ const rootReducer = combineReducers({
windows: windowsReducer,
manifests: manifestsReducer,
config: configReducer,
infoResponses: infoResponsesReducer,
});
export default rootReducer;
import ActionTypes from '../action-types';
/**
* infoResponsesReducer
*/
const infoResponsesReducer = (state = {}, action) => {
switch (action.type) {
case ActionTypes.REQUEST_INFO_RESPONSE:
return Object.assign({}, state, {
[action.infoId]: {
isFetching: true,
},
});
case ActionTypes.RECEIVE_INFO_RESPONSE:
return Object.assign({}, state, {
[action.infoId]: {
json: action.infoJson,
isFetching: false,
},
});
case ActionTypes.RECEIVE_INFO_RESPONSE_FAILURE:
return Object.assign({}, state, {
[action.infoId]: {
error: action.error,
isFetching: false,
},
});
case ActionTypes.REMOVE_INFO_RESPONSE:
return Object.keys(state).reduce((object, key) => {
if (key !== action.infoId) {
object[key] = state[key]; // eslint-disable-line no-param-reassign
}
return object;
}, {});
default: return state;
}
};
export default infoResponsesReducer;
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment