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 { shallow } from 'enzyme';
import { Utils } from 'manifesto.js/dist-esmodule/Utils';
import { WindowViewer } from '../../../src/components/WindowViewer';
import OSDViewer from '../../../src/containers/OpenSeadragonViewer';
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 */
function createWrapper(props) {
return shallow(
<WindowViewer
canvasIndex={0}
canvasLabel="label"
infoResponses={{}}
fetchInfoResponse={() => {}}
currentCanvases={[currentCanvases[1]]}
view="single"
windowId="xyz"
{...props}
/>,
......@@ -35,111 +24,4 @@ describe('WindowViewer', () => {
</OSDViewer>,
)).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 {
getSelectedContentSearchAnnotationIds,
getSortedSearchAnnotationsForCompanionWindow,
getVisibleCanvasIds, getCanvasForAnnotation,
getCanvases, selectInfoResponses,
} from '../../../src/state/selectors';
import { fetchManifest } from '../../../src/state/sagas/iiif';
import {
......@@ -25,6 +26,7 @@ import {
setCanvasforSelectedAnnotation,
panToFocusedWindow,
setCurrentAnnotationsOnCurrentCanvas,
fetchInfoResponses,
} from '../../../src/state/sagas/windows';
import fixture from '../../fixtures/version-2/019.json';
......@@ -398,4 +400,50 @@ describe('window-level sagas', () => {
.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 PropTypes from 'prop-types';
import difference from 'lodash/difference';
import flatten from 'lodash/flatten';
import OSDViewer from '../containers/OpenSeadragonViewer';
import WindowCanvasNavigationControls from '../containers/WindowCanvasNavigationControls';
import MiradorCanvas from '../lib/MiradorCanvas';
/**
* Represents a WindowViewer in the mirador workspace. Responsible for mounting
......@@ -23,91 +20,6 @@ export class WindowViewer extends Component {
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
*/
......@@ -122,20 +34,14 @@ export class WindowViewer extends Component {
return (
<OSDViewer
infoResponses={this.infoResponsesFetchedFromStore()}
windowId={windowId}
>
<WindowCanvasNavigationControls key="canvas_nav" windowId={windowId} />
<WindowCanvasNavigationControls windowId={windowId} />
</OSDViewer>
);
}
}
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,
};
......@@ -2,6 +2,7 @@ import { compose } from 'redux';
import { connect } from 'react-redux';
import { withTranslation } from 'react-i18next';
import { withStyles } from '@material-ui/core/styles';
import flatten from 'lodash/flatten';
import { withPlugins } from '../extend/withPlugins';
import { OpenSeadragonViewer } from '../components/OpenSeadragonViewer';
import * as actions from '../state/actions';
......@@ -14,6 +15,7 @@ import {
getViewer,
getConfig,
getCompanionWindowsForContent,
selectInfoResponses,
} from '../state/selectors';
/**
......@@ -21,19 +23,30 @@ import {
* @memberof Window
* @private
*/
const mapStateToProps = (state, { windowId }) => ({
canvasWorld: new CanvasWorld(
const mapStateToProps = (state, { windowId }) => {
const canvasWorld = new CanvasWorld(
getVisibleCanvases(state, { windowId }),
getLayersForVisibleCanvases(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
|| getCompanionWindowsForContent(state, { content: 'annotations', 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 }),
osdConfig: state.config.osdConfig,
viewerConfig: getViewer(state, { windowId }),
});
};
};
/**
* mapDispatchToProps - used to hook up connect to action creators
......
......@@ -3,19 +3,13 @@ import { connect } from 'react-redux';
import { withPlugins } from '../extend/withPlugins';
import * as actions from '../state/actions';
import { WindowViewer } from '../components/WindowViewer';
import { getVisibleCanvases } from '../state/selectors';
/**
* mapStateToProps - to hook up connect
* @memberof WindowViewer
* @private
*/
const mapStateToProps = (state, { windowId }) => (
{
currentCanvases: getVisibleCanvases(state, { windowId }) || [],
infoResponses: state.infoResponses,
}
);
const mapStateToProps = (state, { windowId }) => ({});
/**
* mapDispatchToProps - used to hook up connect to action creators
......
......@@ -3,6 +3,7 @@ import {
} from 'redux-saga/effects';
import ActionTypes from '../actions/action-types';
import MiradorManifest from '../../lib/MiradorManifest';
import MiradorCanvas from '../../lib/MiradorCanvas';
import {
setContentSearchCurrentAnnotation,
selectAnnotation,
......@@ -11,6 +12,7 @@ import {
setCanvas,
fetchSearch,
receiveManifest,
fetchInfoResponse,
} from '../actions';
import {
getSearchForWindow, getSearchAnnotationsForCompanionWindow,
......@@ -22,6 +24,8 @@ import {
getVisibleCanvasIds,
getWorkspace,
getElasticLayout,
getCanvases,
selectInfoResponses,
} from '../selectors';
import { fetchManifest } from './iiif';
......@@ -183,12 +187,28 @@ export function* setCanvasforSelectedAnnotation({ annotationId, windowId }) {
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() {
yield all([
takeEvery(ActionTypes.ADD_WINDOW, fetchWindowManifest),
takeEvery(ActionTypes.UPDATE_WINDOW, fetchWindowManifest),
takeEvery(ActionTypes.SET_CANVAS, setCurrentAnnotationsOnCurrentCanvas),
takeEvery(ActionTypes.SET_CANVAS, fetchInfoResponses),
takeEvery(ActionTypes.SET_WINDOW_VIEW_TYPE, updateVisibleCanvases),
takeEvery(ActionTypes.RECEIVE_SEARCH, setCanvasOfFirstSearchResult),
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