Skip to content
Snippets Groups Projects
Commit e78e9c61 authored by Chris Beer's avatar Chris Beer
Browse files

Handle info response loading in a saga

parent a529bea6
No related branches found
No related tags found
No related merge requests found
import React from 'react'; import React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import { Utils } from 'manifesto.js/dist-esmodule/Utils';
import { WindowViewer } from '../../../src/components/WindowViewer'; import { WindowViewer } from '../../../src/components/WindowViewer';
import OSDViewer from '../../../src/containers/OpenSeadragonViewer'; import OSDViewer from '../../../src/containers/OpenSeadragonViewer';
import WindowCanvasNavigationControls from '../../../src/containers/WindowCanvasNavigationControls'; import WindowCanvasNavigationControls from '../../../src/containers/WindowCanvasNavigationControls';
import fixture from '../../fixtures/version-2/019.json';
import emptyCanvasFixture from '../../fixtures/version-2/emptyCanvas.json';
let currentCanvases = Utils.parseManifest(fixture).getSequences()[0].getCanvases();
/** create wrapper */ /** create wrapper */
function createWrapper(props) { function createWrapper(props) {
return shallow( return shallow(
<WindowViewer <WindowViewer
canvasIndex={0}
canvasLabel="label"
infoResponses={{}}
fetchInfoResponse={() => {}}
currentCanvases={[currentCanvases[1]]}
view="single"
windowId="xyz" windowId="xyz"
{...props} {...props}
/>, />,
...@@ -35,111 +24,4 @@ describe('WindowViewer', () => { ...@@ -35,111 +24,4 @@ describe('WindowViewer', () => {
</OSDViewer>, </OSDViewer>,
)).toBe(true); )).toBe(true);
}); });
describe('currentInfoResponses', () => {
describe('returns only available infoResponses', () => {
it('isFetching is false', () => {
wrapper = createWrapper(
{
currentCanvasId: 1,
infoResponses: {
'https://stacks.stanford.edu/image/iiif/fr426cg9537%2FSC1094_s3_b14_f17_Cats_1976_0005': {
isFetching: false,
},
'https://stacks.stanford.edu/image/iiif/rz176rt6531%2FPC0170_s3_Tree_Calendar_20081101_152516_0410': {
isFetching: true,
},
},
view: 'book',
},
);
expect(wrapper.instance().currentInfoResponses().length).toEqual(1);
});
it('infoResponse is undefined', () => {
wrapper = createWrapper(
{
currentCanvasId: 1,
infoResponses: {
foo: {
isFetching: false,
},
'https://stacks.stanford.edu/image/iiif/fr426cg9537%2FSC1094_s3_b14_f17_Cats_1976_0005': {
isFetching: false,
},
},
view: 'book',
},
);
expect(wrapper.instance().currentInfoResponses().length).toEqual(1);
});
it('error is not present', () => {
wrapper = createWrapper(
{
currentCanvasId: 1,
infoResponses: {
'https://stacks.stanford.edu/image/iiif/fr426cg9537%2FSC1094_s3_b14_f17_Cats_1976_0005': {
isFetching: false,
},
'https://stacks.stanford.edu/image/iiif/rz176rt6531%2FPC0170_s3_Tree_Calendar_20081101_152516_0410': {
error: 'yikes!',
isFetching: false,
},
},
view: 'book',
},
);
expect(wrapper.instance().currentInfoResponses().length).toEqual(1);
});
});
});
describe('componentDidMount', () => {
it('does not call fetchInfoResponse for a canvas that has no images', () => {
const mockFnCanvas0 = jest.fn();
const mockFnCanvas2 = jest.fn();
const canvases = Utils.parseManifest(emptyCanvasFixture).getSequences()[0].getCanvases();
currentCanvases = [canvases[0]];
wrapper = createWrapper(
{
currentCanvases,
currentCanvasId: 0,
fetchInfoResponse: mockFnCanvas0,
view: 'single',
},
);
expect(mockFnCanvas0).toHaveBeenCalledTimes(1);
currentCanvases = [canvases[2]];
wrapper = createWrapper(
{
currentCanvases,
currentCanvasId: 2,
fetchInfoResponse: mockFnCanvas2,
view: 'single',
},
);
expect(mockFnCanvas2).toHaveBeenCalledTimes(0);
});
});
describe('componentDidUpdate', () => {
it('does not call fetchInfoResponse for a canvas that has no images', () => {
const mockFn = jest.fn();
const canvases = Utils.parseManifest(emptyCanvasFixture).getSequences()[0].getCanvases();
currentCanvases = [canvases[2]];
wrapper = createWrapper(
{
currentCanvases,
currentCanvasId: 2,
fetchInfoResponse: mockFn,
view: 'single',
},
);
wrapper.setProps({ currentCanvases: [canvases[3]], currentCanvasId: 3, view: 'single' });
expect(mockFn).toHaveBeenCalledTimes(0);
});
});
}); });
...@@ -13,6 +13,7 @@ import { ...@@ -13,6 +13,7 @@ import {
getSelectedContentSearchAnnotationIds, getSelectedContentSearchAnnotationIds,
getSortedSearchAnnotationsForCompanionWindow, getSortedSearchAnnotationsForCompanionWindow,
getVisibleCanvasIds, getCanvasForAnnotation, getVisibleCanvasIds, getCanvasForAnnotation,
getCanvases, selectInfoResponses,
} from '../../../src/state/selectors'; } from '../../../src/state/selectors';
import { fetchManifest } from '../../../src/state/sagas/iiif'; import { fetchManifest } from '../../../src/state/sagas/iiif';
import { import {
...@@ -25,6 +26,7 @@ import { ...@@ -25,6 +26,7 @@ import {
setCanvasforSelectedAnnotation, setCanvasforSelectedAnnotation,
panToFocusedWindow, panToFocusedWindow,
setCurrentAnnotationsOnCurrentCanvas, setCurrentAnnotationsOnCurrentCanvas,
fetchInfoResponses,
} from '../../../src/state/sagas/windows'; } from '../../../src/state/sagas/windows';
import fixture from '../../fixtures/version-2/019.json'; import fixture from '../../fixtures/version-2/019.json';
...@@ -398,4 +400,50 @@ describe('window-level sagas', () => { ...@@ -398,4 +400,50 @@ describe('window-level sagas', () => {
.run().then(({ allEffects }) => allEffects.length === 0); .run().then(({ allEffects }) => allEffects.length === 0);
}); });
}); });
describe('fetchInfoResponses', () => {
it('requests info responses for each visible canvas', () => {
const action = {
visibleCanvases: ['http://iiif.io/api/presentation/2.0/example/fixtures/canvas/24/c1.json'],
windowId: 'foo',
};
const manifest = Utils.parseManifest(fixture);
return expectSaga(fetchInfoResponses, action)
.provide([
[select(getCanvases, { windowId: 'foo' }), manifest.getSequences()[0].getCanvases()],
[select(selectInfoResponses), {}],
])
.put.like({
action: {
infoId: 'https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44',
type: 'mirador/REQUEST_INFO_RESPONSE',
},
})
.run();
});
it('requests nothing if the response is already in the store', () => {
const action = {
visibleCanvases: ['http://iiif.io/api/presentation/2.0/example/fixtures/canvas/24/c1.json'],
windowId: 'foo',
};
const manifest = Utils.parseManifest(fixture);
return expectSaga(fetchInfoResponses, action)
.provide([
[select(getCanvases, { windowId: 'foo' }), manifest.getSequences()[0].getCanvases()],
[select(selectInfoResponses), { 'https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44': {} }],
])
.not.put.like({
action: {
infoId: 'https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44',
type: 'mirador/REQUEST_INFO_RESPONSE',
},
})
.run();
});
});
}); });
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import difference from 'lodash/difference';
import flatten from 'lodash/flatten';
import OSDViewer from '../containers/OpenSeadragonViewer'; import OSDViewer from '../containers/OpenSeadragonViewer';
import WindowCanvasNavigationControls from '../containers/WindowCanvasNavigationControls'; import WindowCanvasNavigationControls from '../containers/WindowCanvasNavigationControls';
import MiradorCanvas from '../lib/MiradorCanvas';
/** /**
* Represents a WindowViewer in the mirador workspace. Responsible for mounting * Represents a WindowViewer in the mirador workspace. Responsible for mounting
...@@ -23,91 +20,6 @@ export class WindowViewer extends Component { ...@@ -23,91 +20,6 @@ export class WindowViewer extends Component {
return { hasError: true }; return { hasError: true };
} }
/**
* componentDidMount - React lifecycle method
* Request the initial canvas on mount
*/
componentDidMount() {
const {
currentCanvases, fetchInfoResponse,
} = this.props;
if (!this.infoResponseIsInStore()) {
currentCanvases.forEach((canvas) => {
const miradorCanvas = new MiradorCanvas(canvas);
miradorCanvas.iiifImageResources.forEach((imageResource) => {
fetchInfoResponse({ imageResource });
});
});
}
}
/**
* componentDidUpdate - React lifecycle method
* Request a new canvas if it is needed
*/
componentDidUpdate(prevProps) {
const {
currentCanvases, fetchInfoResponse,
} = this.props;
if (difference(currentCanvases, prevProps.currentCanvases).length > 0
&& !this.infoResponseIsInStore()) {
currentCanvases.forEach((canvas) => {
const miradorCanvas = new MiradorCanvas(canvas);
miradorCanvas.iiifImageResources.forEach((imageResource) => {
fetchInfoResponse({ imageResource });
});
});
}
}
/**
* infoResponseIsInStore - checks whether or not an info response is already
* in the store. No need to request it again.
* @return [Boolean]
*/
infoResponseIsInStore() {
const responses = this.currentInfoResponses();
if (responses.length === this.imageServiceIds().length) {
return true;
}
return false;
}
/** */
imageServiceIds() {
const { currentCanvases } = this.props;
return flatten(currentCanvases.map(canvas => new MiradorCanvas(canvas).imageServiceIds));
}
/**
* currentInfoResponses - Selects infoResponses that are relevent to existing
* canvases to be displayed.
*/
currentInfoResponses() {
const { infoResponses } = this.props;
return this.imageServiceIds().map(imageId => (
infoResponses[imageId]
)).filter(infoResponse => (infoResponse !== undefined
&& infoResponse.isFetching === false
&& infoResponse.error === undefined));
}
/**
* Return an image information response from the store for the correct image
*/
infoResponsesFetchedFromStore() {
const responses = this.currentInfoResponses();
// Only return actual tileSources when all current canvases have completed.
if (responses.length === this.imageServiceIds().length) {
return responses;
}
return [];
}
/** /**
* Renders things * Renders things
*/ */
...@@ -122,20 +34,14 @@ export class WindowViewer extends Component { ...@@ -122,20 +34,14 @@ export class WindowViewer extends Component {
return ( return (
<OSDViewer <OSDViewer
infoResponses={this.infoResponsesFetchedFromStore()}
windowId={windowId} windowId={windowId}
> >
<WindowCanvasNavigationControls key="canvas_nav" windowId={windowId} /> <WindowCanvasNavigationControls windowId={windowId} />
</OSDViewer> </OSDViewer>
); );
} }
} }
WindowViewer.propTypes = { WindowViewer.propTypes = {
currentCanvases: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
fetchAnnotation: PropTypes.func.isRequired,
fetchInfoResponse: PropTypes.func.isRequired,
infoResponses: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
receiveAnnotation: PropTypes.func.isRequired,
windowId: PropTypes.string.isRequired, windowId: PropTypes.string.isRequired,
}; };
...@@ -2,6 +2,7 @@ import { compose } from 'redux'; ...@@ -2,6 +2,7 @@ import { compose } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { withTranslation } from 'react-i18next'; import { withTranslation } from 'react-i18next';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import flatten from 'lodash/flatten';
import { withPlugins } from '../extend/withPlugins'; import { withPlugins } from '../extend/withPlugins';
import { OpenSeadragonViewer } from '../components/OpenSeadragonViewer'; import { OpenSeadragonViewer } from '../components/OpenSeadragonViewer';
import * as actions from '../state/actions'; import * as actions from '../state/actions';
...@@ -14,6 +15,7 @@ import { ...@@ -14,6 +15,7 @@ import {
getViewer, getViewer,
getConfig, getConfig,
getCompanionWindowsForContent, getCompanionWindowsForContent,
selectInfoResponses,
} from '../state/selectors'; } from '../state/selectors';
/** /**
...@@ -21,19 +23,30 @@ import { ...@@ -21,19 +23,30 @@ import {
* @memberof Window * @memberof Window
* @private * @private
*/ */
const mapStateToProps = (state, { windowId }) => ({ const mapStateToProps = (state, { windowId }) => {
canvasWorld: new CanvasWorld( const canvasWorld = new CanvasWorld(
getVisibleCanvases(state, { windowId }), getVisibleCanvases(state, { windowId }),
getLayersForVisibleCanvases(state, { windowId }), getLayersForVisibleCanvases(state, { windowId }),
getSequenceViewingDirection(state, { windowId }), getSequenceViewingDirection(state, { windowId }),
), );
const infoResponses = selectInfoResponses(state);
const imageServiceIds = flatten(canvasWorld.canvases.map(c => c.imageServiceIds));
return {
canvasWorld,
drawAnnotations: getConfig(state).window.forceDrawAnnotations drawAnnotations: getConfig(state).window.forceDrawAnnotations
|| getCompanionWindowsForContent(state, { content: 'annotations', windowId }).length > 0 || getCompanionWindowsForContent(state, { content: 'annotations', windowId }).length > 0
|| getCompanionWindowsForContent(state, { content: 'search', windowId }).length > 0, || getCompanionWindowsForContent(state, { content: 'search', windowId }).length > 0,
infoResponses: imageServiceIds.map(id => infoResponses[id])
.filter(infoResponse => (infoResponse !== undefined
&& infoResponse.isFetching === false
&& infoResponse.error === undefined)),
nonTiledImages: getVisibleCanvasNonTiledResources(state, { windowId }), nonTiledImages: getVisibleCanvasNonTiledResources(state, { windowId }),
osdConfig: state.config.osdConfig, osdConfig: state.config.osdConfig,
viewerConfig: getViewer(state, { windowId }), viewerConfig: getViewer(state, { windowId }),
}); };
};
/** /**
* mapDispatchToProps - used to hook up connect to action creators * mapDispatchToProps - used to hook up connect to action creators
......
...@@ -3,19 +3,13 @@ import { connect } from 'react-redux'; ...@@ -3,19 +3,13 @@ import { connect } from 'react-redux';
import { withPlugins } from '../extend/withPlugins'; import { withPlugins } from '../extend/withPlugins';
import * as actions from '../state/actions'; import * as actions from '../state/actions';
import { WindowViewer } from '../components/WindowViewer'; import { WindowViewer } from '../components/WindowViewer';
import { getVisibleCanvases } from '../state/selectors';
/** /**
* mapStateToProps - to hook up connect * mapStateToProps - to hook up connect
* @memberof WindowViewer * @memberof WindowViewer
* @private * @private
*/ */
const mapStateToProps = (state, { windowId }) => ( const mapStateToProps = (state, { windowId }) => ({});
{
currentCanvases: getVisibleCanvases(state, { windowId }) || [],
infoResponses: state.infoResponses,
}
);
/** /**
* mapDispatchToProps - used to hook up connect to action creators * mapDispatchToProps - used to hook up connect to action creators
......
...@@ -3,6 +3,7 @@ import { ...@@ -3,6 +3,7 @@ import {
} from 'redux-saga/effects'; } from 'redux-saga/effects';
import ActionTypes from '../actions/action-types'; import ActionTypes from '../actions/action-types';
import MiradorManifest from '../../lib/MiradorManifest'; import MiradorManifest from '../../lib/MiradorManifest';
import MiradorCanvas from '../../lib/MiradorCanvas';
import { import {
setContentSearchCurrentAnnotation, setContentSearchCurrentAnnotation,
selectAnnotation, selectAnnotation,
...@@ -11,6 +12,7 @@ import { ...@@ -11,6 +12,7 @@ import {
setCanvas, setCanvas,
fetchSearch, fetchSearch,
receiveManifest, receiveManifest,
fetchInfoResponse,
} from '../actions'; } from '../actions';
import { import {
getSearchForWindow, getSearchAnnotationsForCompanionWindow, getSearchForWindow, getSearchAnnotationsForCompanionWindow,
...@@ -22,6 +24,8 @@ import { ...@@ -22,6 +24,8 @@ import {
getVisibleCanvasIds, getVisibleCanvasIds,
getWorkspace, getWorkspace,
getElasticLayout, getElasticLayout,
getCanvases,
selectInfoResponses,
} from '../selectors'; } from '../selectors';
import { fetchManifest } from './iiif'; import { fetchManifest } from './iiif';
...@@ -183,12 +187,28 @@ export function* setCanvasforSelectedAnnotation({ annotationId, windowId }) { ...@@ -183,12 +187,28 @@ export function* setCanvasforSelectedAnnotation({ annotationId, windowId }) {
yield put(thunk); yield put(thunk);
} }
/** Fetch info responses for the visible canvases */
export function* fetchInfoResponses({ visibleCanvases: visibleCanvasIds, windowId }) {
const canvases = yield select(getCanvases, { windowId });
const infoResponses = yield select(selectInfoResponses);
const visibleCanvases = (canvases || []).filter(c => visibleCanvasIds.includes(c.id));
yield all(visibleCanvases.map((canvas) => {
const miradorCanvas = new MiradorCanvas(canvas);
return all(miradorCanvas.iiifImageResources.map(imageResource => (
!infoResponses[imageResource.getServices()[0].id]
&& put(fetchInfoResponse({ imageResource }))
)).filter(Boolean));
}));
}
/** */ /** */
export default function* windowsSaga() { export default function* windowsSaga() {
yield all([ yield all([
takeEvery(ActionTypes.ADD_WINDOW, fetchWindowManifest), takeEvery(ActionTypes.ADD_WINDOW, fetchWindowManifest),
takeEvery(ActionTypes.UPDATE_WINDOW, fetchWindowManifest), takeEvery(ActionTypes.UPDATE_WINDOW, fetchWindowManifest),
takeEvery(ActionTypes.SET_CANVAS, setCurrentAnnotationsOnCurrentCanvas), takeEvery(ActionTypes.SET_CANVAS, setCurrentAnnotationsOnCurrentCanvas),
takeEvery(ActionTypes.SET_CANVAS, fetchInfoResponses),
takeEvery(ActionTypes.SET_WINDOW_VIEW_TYPE, updateVisibleCanvases), takeEvery(ActionTypes.SET_WINDOW_VIEW_TYPE, updateVisibleCanvases),
takeEvery(ActionTypes.RECEIVE_SEARCH, setCanvasOfFirstSearchResult), takeEvery(ActionTypes.RECEIVE_SEARCH, setCanvasOfFirstSearchResult),
takeEvery(ActionTypes.SELECT_ANNOTATION, setCanvasforSelectedAnnotation), takeEvery(ActionTypes.SELECT_ANNOTATION, setCanvasforSelectedAnnotation),
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment