Skip to content
Snippets Groups Projects
Commit 30e3bfd1 authored by Jessie Keck's avatar Jessie Keck
Browse files

Add canvas level label, description, and metadata to the info sidebar panel.

Refactors some of the sidebar components to be a bit simpler.
Adds Manifest level metadata to info panel.
parent 9e270fc9
No related branches found
No related tags found
No related merge requests found
Showing
with 478 additions and 97 deletions
/* global miradorInstance */
describe('Window Sidebars', () => {
beforeAll(async () => {
await page.goto('http://127.0.0.1:4488/__tests__/integration/mirador/');
});
it('renders and updates canvas level metadata', async () => {
await expect(page).toClick('#addBtn');
await expect(page).toFill('#manifestURL', 'http://localhost:5000/api/001');
await expect(page).toClick('#fetchBtn');
// TODO: Refactor the app so we get rid of the wait
await page.waitFor(1000);
await expect(page).toMatchElement('li', { text: 'http://localhost:5000/api/001' });
await expect(page).toClick('li button', { text: 'http://localhost:5000/api/001' });
await expect(page).toMatchElement(
'h3',
{ text: 'Bodleian Library Human Freaks 2 (33)' },
);
const windows = await page.evaluate(() => (
miradorInstance.store.getState().windows
));
const windowId = Object.values(windows)
.find(window => window.manifestId === 'http://localhost:5000/api/001')
.id;
await expect(page).toMatchElement(`#${windowId} button[aria-label="Toggle window sidebar"]`);
await expect(page).toClick(`#${windowId} button[aria-label="Toggle window sidebar"]`);
await expect(page).toMatchElement(`#${windowId} button[aria-label="Open information companion window"]`);
});
});
import React from 'react';
import { shallow } from 'enzyme';
import LabelValueMetadata from '../../../src/components/LabelValueMetadata';
describe('LabelValueMetadata', () => {
let wrapper;
let labelValuePair;
describe('when the labelValuePair has content', () => {
beforeEach(() => {
labelValuePair = [
{
label: 'Label 1',
value: 'Value 1',
},
{
label: 'Label 2',
value: 'Value 2',
},
];
wrapper = shallow(
<LabelValueMetadata labelValuePairs={labelValuePair} />,
);
});
it('renders a dt/dd for each label/value pair', () => {
expect(wrapper.find('dl').length).toEqual(1);
expect(wrapper.find('dt').length).toEqual(2);
expect(wrapper.find('dt').first().text()).toEqual('Label 1');
expect(wrapper.find('dt').last().text()).toEqual('Label 2');
expect(wrapper.find('dd').length).toEqual(2);
expect(wrapper.find('dd').first().text()).toEqual('Value 1');
expect(wrapper.find('dd').last().text()).toEqual('Value 2');
});
});
describe('when the labelValuePair has no content', () => {
beforeEach(() => {
labelValuePair = [];
wrapper = shallow(
<LabelValueMetadata labelValuePairs={labelValuePair} />,
);
});
it('renders an empty fragment instead of an empty dl', () => {
expect(wrapper.find('dl').length).toEqual(0);
expect(wrapper.matchesElement(<></>)).toBe(true);
});
});
});
import React from 'react';
import { shallow } from 'enzyme';
import Typography from '@material-ui/core/Typography';
import createStore from '../../../src/state/createStore';
import * as actions from '../../../src/state/actions';
import WindowSideBarInfoPanel from '../../../src/components/WindowSideBarInfoPanel';
import fixture from '../../fixtures/version-2/001.json';
import LabelValueMetadata from '../../../src/components/LabelValueMetadata';
describe('WindowSideBarInfoPanel', () => {
let wrapper;
let manifest;
const store = createStore();
describe('when metadata is present', () => {
beforeEach(() => {
store.dispatch(actions.receiveManifest('foo', fixture));
manifest = store.getState().manifests.foo;
wrapper = shallow(
<WindowSideBarInfoPanel manifest={manifest} />,
<WindowSideBarInfoPanel
canvasLabel="The Canvas Label"
canvasDescription="The Canvas Description"
canvasMetadata={[{ label: {}, value: {} }]}
manifestLabel="The Manifest Label"
manifestDescription="The Manifest Description"
/>,
).dive();
});
it('renders without an error', () => {
it('renders canvas level label, description, and metadata', () => {
expect(
wrapper.find('WithStyles(Typography)[variant="h2"]').first().matchesElement(
<Typography>aboutThisItem</Typography>,
wrapper.find('WithStyles(Typography)[variant="h3"]').first().matchesElement(
<Typography>The Canvas Label</Typography>,
),
).toBe(true);
expect(
wrapper.find('WithStyles(Typography)[variant="h3"]').first().matchesElement(
<Typography>Bodleian Library Human Freaks 2 (33)</Typography>,
wrapper.find('WithStyles(Typography)[variant="body2"]').first().matchesElement(
<Typography>The Canvas Description</Typography>,
),
).toBe(true);
expect(wrapper.find(LabelValueMetadata).length).toBe(2); // one for canvas one for manifest
});
it('renders manifest level label, description, and metadata', () => {
expect(
wrapper.find('WithStyles(Typography)[variant="h3"]').last().matchesElement(
<Typography>The Manifest Label</Typography>,
),
).toBe(true);
expect(
wrapper.find('WithStyles(Typography)[variant="body2"]').first().matchesElement(
<Typography>[Handbill of Mr. Becket, [1787] ]</Typography>,
wrapper.find('WithStyles(Typography)[variant="body2"]').last().matchesElement(
<Typography>The Manifest Description</Typography>,
),
).toBe(true);
expect(wrapper.find(LabelValueMetadata).length).toBe(2); // one for canvas one for manifest
});
});
describe('when metadata is not present', () => {
beforeEach(() => {
wrapper = shallow(
<WindowSideBarInfoPanel />,
).dive();
});
it('does not render empty elements', () => {
expect(
wrapper.find('WithStyles(Typography)[variant="h2"]').first().matchesElement(
<Typography>aboutThisItem</Typography>,
),
).toBe(true);
expect(wrapper.find('WithStyles(Typography)[variant="h3"]').length).toEqual(0);
expect(wrapper.find('WithStyles(Typography)[variant="body2"]').length).toEqual(0);
});
});
});
import React from 'react';
import { shallow } from 'enzyme';
import createStore from '../../../src/state/createStore';
import * as actions from '../../../src/state/actions';
import WindowSideBarPanel from '../../../src/components/WindowSideBarPanel';
import WindowSideBarInfoPanel from '../../../src/containers/WindowSideBarInfoPanel';
import fixture from '../../fixtures/version-2/001.json';
describe('WindowSideBarPanel', () => {
let wrapper;
let manifest;
const store = createStore();
beforeEach(() => {
store.dispatch(actions.receiveManifest('foo', fixture));
manifest = store.getState().manifests.foo;
});
describe('when the sideBarPanel is set to "info"', () => {
beforeEach(() => {
wrapper = shallow(<WindowSideBarPanel sideBarPanel="info" manifest={manifest} />);
wrapper = shallow(<WindowSideBarPanel windowId="abc123" sideBarPanel="info" />);
});
it('renders the WindowSideBarInfoPanel', () => {
......@@ -28,7 +18,7 @@ describe('WindowSideBarPanel', () => {
describe('when the sideBarPanel is set to "closed" (or any other unknown value)', () => {
beforeEach(() => {
wrapper = shallow(<WindowSideBarPanel sideBarPanel="closed" manifest={manifest} />);
wrapper = shallow(<WindowSideBarPanel windowId="abc123" sideBarPanel="closed" />);
});
it('does not render any panel component', () => {
......
import manifesto from 'manifesto.js';
import manifestFixture001 from '../../fixtures/version-2/001.json';
import manifestFixture002 from '../../fixtures/version-2/002.json';
import manifestFixture019 from '../../fixtures/version-2/019.json';
import {
getCanvasLabel,
getDestructuredMetadata,
getSelectedCanvas,
getWindowManifest,
getManifestLogo,
getManifestCanvases,
getManifestDescription,
getThumbnailNavigationPosition,
getManifestTitle,
getWindowViewType,
......@@ -139,3 +145,112 @@ describe('getWindowViewType', () => {
expect(received).toBeUndefined();
});
});
describe('getManifestDescription', () => {
it('should return manifest description', () => {
const manifest = { manifestation: manifesto.create(manifestFixture001) };
const received = getManifestDescription(manifest);
expect(received).toBe('[Handbill of Mr. Becket, [1787] ]');
});
it('should return undefined if manifest undefined', () => {
const received = getManifestDescription(undefined);
expect(received).toBeUndefined();
});
it('should return undefined if no manifestation', () => {
const manifest = {};
const received = getManifestDescription(manifest);
expect(received).toBeUndefined();
});
});
describe('getSelectedCanvas', () => {
const state = {
windows: {
a: {
id: 'a',
manifestId: 'x',
canvasIndex: 1,
},
},
manifests: {
x: {
id: 'x',
manifestation: manifesto.create(manifestFixture019),
},
},
};
const noManifestationState = {
windows: {
a: {
id: 'a',
manifestId: 'x',
canvasIndex: 1,
},
},
manifests: {
x: {
id: 'x',
},
},
};
it('should return canvas based on the canvas index stored window state', () => {
const selectedCanvas = getSelectedCanvas(state, 'a');
expect(selectedCanvas.id).toEqual(
'https://purl.stanford.edu/fr426cg9537/iiif/canvas/fr426cg9537_1',
);
});
it('should return an empty object when there is no manifestation to get a canvas from', () => {
const selectedCanvas = getSelectedCanvas(noManifestationState, 'a');
expect(selectedCanvas).toEqual({});
});
});
describe('getCanvasLabel', () => {
it('should return label of the canvas', () => {
const canvas = manifesto
.create(manifestFixture001)
.getSequences()[0]
.getCanvases()[0];
const received = getCanvasLabel(canvas);
expect(received).toBe('Whole Page');
});
it('should return the given canvas index (+1) if the canvas is undefined', () => {
expect(getCanvasLabel(undefined)).toBe(1);
expect(getCanvasLabel(undefined, 2)).toBe(3);
});
it('should return the canvas index (+1) if no manifestation', () => {
const canvas = { getLabel: () => {} };
const received = getCanvasLabel(canvas);
expect(received).toBe(1);
});
});
describe('getDestructuredMetadata', () => {
it('should return the first value of label/value attributes for each object in the array ', () => {
const iiifResource = manifesto.create(manifestFixture002);
const received = getDestructuredMetadata(iiifResource);
const expected = [{
label: 'date',
value: 'some date',
}];
expect(received).toEqual(expected);
});
it('returns an empty array if there is no metadata', () => {
const iiifResource = manifesto.create(manifestFixture019);
const received = getDestructuredMetadata(iiifResource);
expect(received).toEqual([]);
});
});
import React, { Component } from 'react';
import PropTypes from 'prop-types';
/**
* Renders label/value pair metadata in a dl
* @prop {object} labelValuePair
*/
class LabelValueMetadata extends Component {
/**
* render
* @return {String} - HTML markup for the component
*/
render() {
const { labelValuePairs } = this.props;
if (labelValuePairs.length === 0) {
return (<></>);
}
/* eslint-disable react/no-array-index-key */
// Disabling array index key for dt/dd elements as
// they are intended to display metadata that will not
// need to be re-rendered internally in any meaningful way
return (
<dl>
{labelValuePairs.reduce((acc, labelValuePair, i) => acc.concat([
<dt key={`label-${i}`}>{labelValuePair.label}</dt>,
<dd key={`value-${i}`}>{labelValuePair.value}</dd>,
]), [])}
</dl>
);
/* eslint-enable react/no-array-index-key */
}
}
LabelValueMetadata.propTypes = {
labelValuePairs: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types,
};
export default LabelValueMetadata;
......@@ -16,7 +16,7 @@ class Window extends Component {
render() {
const { manifest, window } = this.props;
return (
<div className={ns('window')}>
<div id={window.id} className={ns('window')}>
<WindowTopBar
windowId={window.id}
manifest={manifest}
......
......@@ -35,12 +35,7 @@ class WindowMiddleContent extends Component {
const { manifest, window } = this.props;
return (
<div className={ns('window-middle-content')}>
<WindowSideBar
windowId={window.id}
manifest={manifest}
sideBarOpen={window.sideBarOpen}
sideBarPanel={window.sideBarPanel}
/>
<WindowSideBar windowId={window.id} />
<CompanionWindow
windowId={window.id}
manifest={manifest}
......
......@@ -5,7 +5,7 @@ import Drawer from '@material-ui/core/Drawer';
import { withStyles } from '@material-ui/core/styles';
import List from '@material-ui/core/List';
import WindowSideBarButtons from '../containers/WindowSideBarButtons';
import WindowSideBarPanel from './WindowSideBarPanel';
import WindowSideBarPanel from '../containers/WindowSideBarPanel';
import ns from '../config/css-ns';
/**
......@@ -18,7 +18,7 @@ class WindowSideBar extends Component {
*/
render() {
const {
classes, manifest, windowId, sideBarOpen, sideBarPanel,
classes, windowId, sideBarOpen, sideBarPanel,
} = this.props;
return (
......@@ -38,7 +38,7 @@ class WindowSideBar extends Component {
}}
>
<List>
<WindowSideBarButtons windowId={windowId} sideBarPanel={sideBarPanel} />
<WindowSideBarButtons windowId={windowId} />
</List>
</Drawer>
<Drawer
......@@ -55,11 +55,7 @@ class WindowSideBar extends Component {
style: { position: 'absolute', width: '200px' },
}}
>
<WindowSideBarPanel
manifest={manifest}
windowId={windowId}
sideBarPanel={sideBarPanel}
/>
<WindowSideBarPanel windowId={windowId} />
</Drawer>
</>
);
......@@ -69,14 +65,12 @@ class WindowSideBar extends Component {
WindowSideBar.propTypes = {
classes: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types,
manifest: PropTypes.object, // eslint-disable-line react/forbid-prop-types
windowId: PropTypes.string.isRequired,
sideBarOpen: PropTypes.bool,
sideBarPanel: PropTypes.string,
};
WindowSideBar.defaultProps = {
manifest: {},
sideBarOpen: false,
sideBarPanel: 'closed',
};
......
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Divider from '@material-ui/core/Divider';
import Typography from '@material-ui/core/Typography';
import { withStyles } from '@material-ui/core/styles';
import LabelValueMetadata from './LabelValueMetadata';
import ns from '../config/css-ns';
/**
* WindowSideBarInfoPanel
*/
class WindowSideBarInfoPanel extends Component {
/**
* manifestLabel - get the label from the manifesto manifestation
* @return String
*/
manifestLabel() {
const { manifest } = this.props;
if (manifest.manifestation) {
return manifest.manifestation.getLabel().map(label => label.value)[0];
}
return '';
}
/**
* manifestDescription - get the description from the manifesto manifestation
* @return String
*/
manifestDescription() {
const { manifest } = this.props;
if (manifest.manifestation) {
return manifest.manifestation.getDescription().map(label => label.value);
}
return '';
}
/**
* render
* @return
*/
render() {
const { classes, t } = this.props;
const {
canvasDescription,
canvasLabel,
canvasMetadata,
classes,
manifestDescription,
manifestLabel,
manifestMetadata,
t,
} = this.props;
return (
<div className={ns('window-sidebar-info-panel')}>
<Typography variant="h2" className={classes.windowSideBarH2}>{t('aboutThisItem')}</Typography>
<Typography variant="h3" className={classes.windowSideBarH3}>{this.manifestLabel()}</Typography>
<Typography variant="body2">{this.manifestDescription()}</Typography>
{canvasLabel && <Typography variant="h3" className={classes.windowSideBarH3}>{canvasLabel}</Typography>}
{canvasDescription && <Typography variant="body2">{canvasDescription}</Typography>}
{canvasMetadata && <LabelValueMetadata labelValuePairs={canvasMetadata} />}
<Divider />
{manifestLabel && <Typography variant="h3" className={classes.windowSideBarH3}>{manifestLabel}</Typography>}
{manifestDescription && <Typography variant="body2">{manifestDescription}</Typography>}
{manifestMetadata && <LabelValueMetadata labelValuePairs={manifestMetadata} />}
</div>
);
}
}
WindowSideBarInfoPanel.propTypes = {
canvasDescription: PropTypes.string,
canvasLabel: PropTypes.string,
canvasMetadata: PropTypes.array, // eslint-disable-line react/forbid-prop-types
classes: PropTypes.object, // eslint-disable-line react/forbid-prop-types
manifest: PropTypes.object, // eslint-disable-line react/forbid-prop-types
manifestLabel: PropTypes.string,
manifestDescription: PropTypes.string,
manifestMetadata: PropTypes.array, // eslint-disable-line react/forbid-prop-types
t: PropTypes.func,
};
WindowSideBarInfoPanel.defaultProps = {
canvasDescription: null,
canvasLabel: null,
canvasMetadata: [],
classes: {},
manifest: {},
manifestLabel: null,
manifestDescription: null,
manifestMetadata: [],
t: key => key,
};
......
......@@ -12,10 +12,10 @@ class WindowSideBarPanel extends Component {
* @return React Component
*/
activePanelComponent() {
const { manifest, sideBarPanel } = this.props;
const { windowId, sideBarPanel } = this.props;
switch (sideBarPanel) {
case 'info':
return <WindowSideBarInfoPanel manifest={manifest} />;
return <WindowSideBarInfoPanel windowId={windowId} />;
default:
return null;
}
......@@ -35,11 +35,10 @@ class WindowSideBarPanel extends Component {
}
WindowSideBarPanel.propTypes = {
manifest: PropTypes.object, // eslint-disable-line react/forbid-prop-types
sideBarPanel: PropTypes.string,
windowId: PropTypes.string.isRequired,
};
WindowSideBarPanel.defaultProps = {
manifest: {},
sideBarPanel: 'closed', // Closed will fall out to the default null case for the actiuve panel
};
......
import { connect } from 'react-redux';
import { compose } from 'redux';
import miradorWithPlugins from '../lib/miradorWithPlugins';
import WindowSideBar from '../components/WindowSideBar';
export default miradorWithPlugins(WindowSideBar);
/**
* mapStateToProps - to hook up connect
* @memberof WindowSideBar
* @private
*/
const mapStateToProps = (state, props) => (
{
sideBarOpen: state.windows[props.windowId].sideBarOpen,
sideBarPanel: state.windows[props.windowId].sideBarPanel,
}
);
const enhance = compose(
connect(mapStateToProps, null),
miradorWithPlugins,
// further HOC
);
export default enhance(WindowSideBar);
......@@ -17,8 +17,19 @@ const mapDispatchToProps = (dispatch, props) => ({
),
});
/**
* mapStateToProps - used to hook up connect to state
* @memberof WindowSideButtons
* @private
*/
const mapStateToProps = (state, { windowId }) => ({
sideBarPanel: state.windows[windowId].sideBarPanel,
});
const enhance = compose(
connect(null, mapDispatchToProps),
connect(mapStateToProps, mapDispatchToProps),
miradorWithPlugins,
withNamespaces(),
// further HOC go here
......
import { compose } from 'redux';
import { connect } from 'react-redux';
import { withNamespaces } from 'react-i18next';
import miradorWithPlugins from '../lib/miradorWithPlugins';
import {
getDestructuredMetadata,
getCanvasLabel,
getManifestDescription,
getManifestTitle,
getSelectedCanvas,
getWindowManifest,
} from '../state/selectors';
import WindowSideBarInfoPanel from '../components/WindowSideBarInfoPanel';
/**
* mapStateToProps - to hook up connect
* @memberof WindowSideBarInfoPanel
* @private
*/
const mapStateToProps = (state, { windowId }) => ({
canvasLabel: getCanvasLabel(
getSelectedCanvas(state, windowId),
state.windows[windowId].canvasIndex,
),
canvasDescription: getSelectedCanvas(state, windowId).getProperty('description'),
canvasMetadata: getDestructuredMetadata(getSelectedCanvas(state, windowId)),
manifestLabel: getManifestTitle(getWindowManifest(state, windowId)),
manifestDescription: getManifestDescription(getWindowManifest(state, windowId)),
manifestMetadata: getDestructuredMetadata(getWindowManifest(state, windowId).manifestation),
});
const enhance = compose(
connect(mapStateToProps, null),
withNamespaces(),
miradorWithPlugins,
// further HOC
......
import { connect } from 'react-redux';
import { compose } from 'redux';
import miradorWithPlugins from '../lib/miradorWithPlugins';
import WindowSideBarPanel from '../components/WindowSideBarPanel';
export default miradorWithPlugins(WindowSideBarPanel);
/** */
const mapStateToProps = (state, { windowId }) => ({
sideBarPanel: state.windows[windowId].sideBarPanel,
});
export default compose(
connect(mapStateToProps, null),
miradorWithPlugins,
// further HOC
)(WindowSideBarPanel);
......@@ -34,6 +34,24 @@ export function getManifestCanvases(manifest) {
return manifest.manifestation.getSequences()[0].getCanvases();
}
/**
* Return the current canvas selected in a window
* @param {object} state
* @param {String} windowId
* @return {Object}
*/
export function getSelectedCanvas(state, windowId) {
const manifest = getWindowManifest(state, windowId);
const { canvasIndex } = state.windows[windowId];
if (!manifest.manifestation) {
return {};
}
return manifest.manifestation.getSequences()[0].getCanvasByIndex(canvasIndex);
}
/** Return position of thumbnail navigation in a certain window.
* @param {object} state
* @param {String} windowId
......@@ -63,3 +81,44 @@ export function getManifestTitle(manifest) {
export function getWindowViewType(state, windowId) {
return state.windows[windowId] && state.windows[windowId].view;
}
/**
* Return manifest description
* @param {object} manifest
* @return {String}
*/
export function getManifestDescription(manifest) {
return manifest
&& manifest.manifestation
&& manifest.manifestation.getDescription().map(label => label.value)[0];
}
/**
* Return canvas label, or alternatively return the given index + 1 to be displayed
* @param {object} canvas
* @return {String|Integer}
*/
export function getCanvasLabel(canvas, canvasIndex) {
return (canvas
&& canvas.getLabel()
&& canvas.getLabel().map(label => label.value)[0])
|| (canvasIndex || 0) + 1;
}
/**
* Return canvas metadata in a label / value structure
* This is a potential seam for pulling the i18n locale from
* state and plucking out the appropriate language.
* For now we're just getting the first.
* @param {object} IIIF Resource
* @return {Array[Object]}
*/
export function getDestructuredMetadata(iiifResoruce) {
return (iiifResoruce
&& iiifResoruce.getMetadata()
&& iiifResoruce.getMetadata().map(resource => ({
label: resource.label[0].value,
value: resource.value[0].value,
}))
);
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment