Skip to content
Snippets Groups Projects
Commit 073bb17e authored by Lutz Helm's avatar Lutz Helm
Browse files

Merge branch 'master' into enforce-single-canvas-view-for-annotation-editor

parents 18b35d27 4f576d56
No related branches found
No related tags found
No related merge requests found
......@@ -36,4 +36,16 @@ describe('AnnotationCreation', () => {
wrapper = createWrapper();
expect(wrapper.dive().find(TextEditor).length).toBe(1);
});
it('can handle annotations without target selector', () => {
wrapper = createWrapper({
annotation: {
body: {
purpose: 'commenting',
value: 'Foo bar',
},
target: {},
},
});
wrapper.dive();
});
});
import React from 'react';
import { shallow } from 'enzyme';
import MenuItem from '@material-ui/core/MenuItem';
import AnnotationExportDialog from '../src/AnnotationExportDialog';
window.URL.createObjectURL = jest.fn((data) => ('downloadurl'));
const adapter = jest.fn(() => (
{
all: jest.fn().mockResolvedValue(
{
id: 'pageId/3',
items: [
{ id: 'anno/2' },
],
type: 'AnnotationPage',
},
),
annotationPageId: 'pageId/3',
}
));
/** */
function createWrapper(props) {
return shallow(
<AnnotationExportDialog
canvases={[]}
config={{ annotation: { adapter } }}
handleClose={jest.fn()}
open
{...props}
/>,
);
}
describe('AnnotationExportDialog', () => {
it('renders download link for every annotation page', async () => {
let wrapper = createWrapper({
canvases: [
{ id: 'canvas/1' },
{ id: 'canvas/2' },
],
}).dive();
expect(wrapper.text()).toEqual(expect.stringContaining('No annotations stored yet.'));
wrapper.instance().componentDidUpdate({ open: false });
await new Promise((resolve) => setTimeout(resolve, 50));
wrapper = wrapper.update();
expect(wrapper.text()).toEqual(expect.not.stringContaining('No annotations stored yet.'));
expect(wrapper.find(MenuItem).some({ 'aria-label': 'Export annotations for canvas/1' })).toBe(true);
expect(wrapper.find(MenuItem).some({ 'aria-label': 'Export annotations for canvas/2' })).toBe(true);
});
});
import React from 'react';
import { shallow } from 'enzyme';
import { MiradorMenuButton } from 'mirador/dist/es/src/components/MiradorMenuButton';
import LocalStorageAdapter from '../src/LocalStorageAdapter';
import miradorAnnotationPlugin from '../src/plugins/miradorAnnotationPlugin';
/** */
function createWrapper(props) {
return shallow(
<miradorAnnotationPlugin.component
canvases={[]}
config={{}}
TargetComponent="<div>hello</div>"
targetProps={{}}
......@@ -48,4 +50,38 @@ describe('MiradorAnnotation', () => {
wrapper.find(MiradorMenuButton).simulate('click');
expect(wrapper.instance().state.singleCanvasDialogOpen).toBe(true);
});
it('renders no export button if export or LocalStorageAdapter are not configured', () => {
wrapper = createWrapper();
expect(wrapper.find(MiradorMenuButton).some({ 'aria-label': 'Export local annotations for visible items' })).toBe(false);
wrapper = createWrapper({
config: {
annotation: {
adapter: () => () => {},
exportLocalStorageAnnotations: true,
},
},
});
expect(wrapper.find(MiradorMenuButton).some({ 'aria-label': 'Export local annotations for visible items' })).toBe(false);
wrapper = createWrapper({
config: {
annotation: {
adapter: (canvasId) => new LocalStorageAdapter(`test://?canvasId=${canvasId}`),
},
},
});
expect(wrapper.find(MiradorMenuButton).some({ 'aria-label': 'Export local annotations for visible items' })).toBe(false);
});
it('renders export button if export and LocalStorageAdapter are configured', () => {
wrapper = createWrapper({
config: {
annotation: {
adapter: (canvasId) => new LocalStorageAdapter(`test://?canvasId=${canvasId}`),
exportLocalStorageAnnotations: true,
},
},
});
expect(wrapper.find(MiradorMenuButton).some({ 'aria-label': 'Export local annotations for visible items' })).toBe(true);
});
});
......@@ -9,6 +9,7 @@ const config = {
annotation: {
adapter: (canvasId) => new LocalStorageAdapter(`localStorage://?canvasId=${canvasId}`),
// adapter: (canvasId) => new AnnototAdapter(canvasId, endpointUrl),
exportLocalStorageAnnotations: false, // display annotation JSON export button
},
id: 'demo',
window: {
......
......@@ -50,6 +50,7 @@ class AnnotationCreation extends Component {
} else {
annoState.annoBody = props.annotation.body.value;
}
if (props.annotation.target.selector) {
if (Array.isArray(props.annotation.target.selector)) {
props.annotation.target.selector.forEach((selector) => {
if (selector.type === 'SvgSelector') {
......@@ -62,6 +63,7 @@ class AnnotationCreation extends Component {
annoState.svg = props.annotation.target.selector.value;
}
}
}
this.state = {
activeTool: 'cursor',
annoBody: '',
......
import React, { Component } from 'react';
import Dialog from '@material-ui/core/Dialog';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import GetAppIcon from '@material-ui/icons/GetApp';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import MenuList from '@material-ui/core/MenuList';
import MenuItem from '@material-ui/core/MenuItem';
import Typography from '@material-ui/core/Typography';
import PropTypes, { bool } from 'prop-types';
import { withStyles } from '@material-ui/core';
/** */
const styles = (theme) => ({
listitem: {
'&:focus': {
backgroundColor: theme.palette.action.focus,
},
'&:hover': {
backgroundColor: theme.palette.action.hover,
},
},
});
/** */
class AnnotationExportDialog extends Component {
/** */
constructor(props) {
super(props);
this.state = {
exportLinks: [],
};
this.closeDialog = this.closeDialog.bind(this);
}
/** */
componentDidUpdate(prevProps) {
const { canvases, config, open } = this.props;
const { open: prevOpen } = prevProps || {};
if (prevOpen !== open && open) {
/** */
const reducer = async (acc, canvas) => {
const store = config.annotation.adapter(canvas.id);
const resolvedAcc = await acc;
const content = await store.all();
if (content) {
// eslint-disable-next-line no-underscore-dangle
const label = (canvas.__jsonld && canvas.__jsonld.label) || canvas.id;
const data = new Blob([JSON.stringify(content)], { type: 'application/json' });
const url = window.URL.createObjectURL(data);
return [...resolvedAcc, {
canvasId: canvas.id,
id: content.id || content['@id'],
label,
url,
}];
}
return resolvedAcc;
};
if (canvases && canvases.length > 0) {
canvases.reduce(reducer, []).then((exportLinks) => {
this.setState({ exportLinks });
});
}
}
}
/** */
closeDialog() {
const { handleClose } = this.props;
this.setState({ exportLinks: [] });
handleClose();
}
/** */
render() {
const { classes, handleClose, open } = this.props;
const { exportLinks } = this.state;
return (
<Dialog
aria-labelledby="annotation-export-dialog-title"
id="annotation-export-dialog"
onClose={handleClose}
onEscapeKeyDown={this.closeDialog}
open={open}
>
<DialogTitle id="annotation-export-dialog-title" disableTypography>
<Typography variant="h2">Export Annotations</Typography>
</DialogTitle>
<DialogContent>
{ exportLinks === undefined || exportLinks.length === 0 ? (
<Typography variant="body1">No annotations stored yet.</Typography>
) : (
<MenuList>
{ exportLinks.map((dl) => (
<MenuItem
button
className={classes.listitem}
component="a"
key={dl.canvasId}
aria-label={`Export annotations for ${dl.label}`}
href={dl.url}
download={`${dl.id}.json`}
>
<ListItemIcon>
<GetAppIcon />
</ListItemIcon>
<ListItemText>
{`Export annotations for "${dl.label}"`}
</ListItemText>
</MenuItem>
))}
</MenuList>
)}
</DialogContent>
</Dialog>
);
}
}
AnnotationExportDialog.propTypes = {
canvases: PropTypes.arrayOf(
PropTypes.shape({ id: PropTypes.string }),
).isRequired,
classes: PropTypes.objectOf(PropTypes.string),
config: PropTypes.shape({
annotation: PropTypes.shape({
adapter: PropTypes.func,
}),
}).isRequired,
handleClose: PropTypes.func.isRequired,
open: bool.isRequired,
};
AnnotationExportDialog.defaultProps = {
classes: {},
};
export default withStyles(styles)(AnnotationExportDialog);
......@@ -3,8 +3,12 @@ import PropTypes from 'prop-types';
import * as actions from 'mirador/dist/es/src/state/actions';
import { getWindowViewType } from 'mirador/dist/es/src/state/selectors';
import AddBoxIcon from '@material-ui/icons/AddBox';
import GetAppIcon from '@material-ui/icons/GetApp';
import { MiradorMenuButton } from 'mirador/dist/es/src/components/MiradorMenuButton';
import { getVisibleCanvases } from 'mirador/dist/es/src/state/selectors/canvases';
import SingleCanvasDialog from '../SingleCanvasDialog';
import AnnotationExportDialog from '../AnnotationExportDialog';
import LocalStorageAdapter from '../LocalStorageAdapter';
/** */
class MiradorAnnotation extends Component {
......@@ -12,9 +16,11 @@ class MiradorAnnotation extends Component {
constructor(props) {
super(props);
this.state = {
annotationExportDialogOpen: false,
singleCanvasDialogOpen: false,
};
this.openCreateAnnotationCompanionWindow = this.openCreateAnnotationCompanionWindow.bind(this);
this.toggleCanvasExportDialog = this.toggleCanvasExportDialog.bind(this);
this.toggleSingleCanvasDialogOpen = this.toggleSingleCanvasDialogOpen.bind(this);
}
......@@ -37,15 +43,29 @@ class MiradorAnnotation extends Component {
});
}
/** */
toggleCanvasExportDialog(e) {
const { annotationExportDialogOpen } = this.state;
const newState = {
annotationExportDialogOpen: !annotationExportDialogOpen,
};
this.setState(newState);
}
/** */
render() {
const {
canvases,
config,
switchToSingleCanvasView,
TargetComponent,
targetProps,
windowViewType,
} = this.props;
const { singleCanvasDialogOpen } = this.state;
const { annotationExportDialogOpen, singleCanvasDialogOpen } = this.state;
const storageAdapter = config.annotation && config.annotation.adapter('poke');
const offerExportDialog = config.annotation && storageAdapter instanceof LocalStorageAdapter
&& config.annotation.exportLocalStorageAnnotations;
return (
<div>
<TargetComponent
......@@ -58,15 +78,30 @@ class MiradorAnnotation extends Component {
>
<AddBoxIcon />
</MiradorMenuButton>
{
singleCanvasDialogOpen && (
{ singleCanvasDialogOpen && (
<SingleCanvasDialog
open={singleCanvasDialogOpen}
handleClose={this.toggleSingleCanvasDialogOpen}
switchToSingleCanvasView={switchToSingleCanvasView}
/>
)
}
)}
{ offerExportDialog && (
<MiradorMenuButton
aria-label="Export local annotations for visible items"
onClick={this.toggleCanvasExportDialog}
size="small"
>
<GetAppIcon />
</MiradorMenuButton>
)}
{ offerExportDialog && (
<AnnotationExportDialog
canvases={canvases}
config={config}
handleClose={this.toggleCanvasExportDialog}
open={annotationExportDialogOpen}
/>
)}
</div>
);
}
......@@ -74,6 +109,15 @@ class MiradorAnnotation extends Component {
MiradorAnnotation.propTypes = {
addCompanionWindow: PropTypes.func.isRequired,
canvases: PropTypes.arrayOf(
PropTypes.shape({ id: PropTypes.string, index: PropTypes.number }),
).isRequired,
config: PropTypes.shape({
annotation: PropTypes.shape({
adapter: PropTypes.func,
exportLocalStorageAnnotations: PropTypes.bool,
}),
}).isRequired,
switchToSingleCanvasView: PropTypes.func.isRequired,
TargetComponent: PropTypes.oneOfType([
PropTypes.func,
......@@ -94,8 +138,10 @@ const mapDispatchToProps = (dispatch, props) => ({
});
/** */
const mapStateToProps = (state, props) => ({
windowViewType: getWindowViewType(state, { windowId: props.targetProps.windowId }),
const mapStateToProps = (state, { targetProps: { windowId } }) => ({
canvases: getVisibleCanvases(state, { windowId }),
config: state.config,
windowViewType: getWindowViewType(state, { windowId }),
});
export default {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment