Skip to content
Snippets Groups Projects
Unverified Commit 4f576d56 authored by Jack Reed's avatar Jack Reed Committed by GitHub
Browse files

Merge pull request #45 from ProjectMirador/offer-localstorage-annotations-for-download

Offer localstorage annotations for "download"
parents 467d1507 cdb77c6f
Branches
No related tags found
No related merge requests found
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={{}}
......@@ -38,4 +40,38 @@ describe('MiradorAnnotation', () => {
},
);
});
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: {
......
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);
......@@ -2,14 +2,22 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import * as actions from 'mirador/dist/es/src/state/actions';
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 AnnotationExportDialog from '../AnnotationExportDialog';
import LocalStorageAdapter from '../LocalStorageAdapter';
/** */
class MiradorAnnotation extends Component {
/** */
constructor(props) {
super(props);
this.state = {
annotationExportDialogOpen: false,
};
this.openCreateAnnotationCompanionWindow = this.openCreateAnnotationCompanionWindow.bind(this);
this.toggleCanvasExportDialog = this.toggleCanvasExportDialog.bind(this);
}
/** */
......@@ -23,9 +31,24 @@ class MiradorAnnotation extends Component {
});
}
/** */
toggleCanvasExportDialog(e) {
const { annotationExportDialogOpen } = this.state;
const newState = {
annotationExportDialogOpen: !annotationExportDialogOpen,
};
this.setState(newState);
}
/** */
render() {
const { TargetComponent, targetProps } = this.props;
const {
canvases, config, TargetComponent, targetProps,
} = this.props;
const { annotationExportDialogOpen } = this.state;
const storageAdapter = config.annotation && config.annotation.adapter('poke');
const offerExportDialog = config.annotation && storageAdapter instanceof LocalStorageAdapter
&& config.annotation.exportLocalStorageAnnotations;
return (
<div>
<TargetComponent
......@@ -38,6 +61,23 @@ class MiradorAnnotation extends Component {
>
<AddBoxIcon />
</MiradorMenuButton>
{ 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>
);
}
......@@ -45,6 +85,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,
TargetComponent: PropTypes.oneOfType([
PropTypes.func,
PropTypes.node,
......@@ -59,9 +108,16 @@ const mapDispatchToProps = (dispatch, props) => ({
),
});
/** */
const mapStateToProps = (state, { targetProps: { windowId } }) => ({
canvases: getVisibleCanvases(state, { windowId }),
config: state.config,
});
export default {
component: MiradorAnnotation,
mapDispatchToProps,
mapStateToProps,
mode: 'wrap',
target: 'AnnotationSettings',
};
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment