Skip to content
Snippets Groups Projects
Unverified Commit 82f70f8c authored by Chris Beer's avatar Chris Beer Committed by GitHub
Browse files

Merge pull request #1855 from ProjectMirador/1780-canvas-level-md

Add canvas (and manifest) level metadata
parents 25dfb2a3 30e3bfd1
Branches
Tags
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