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

Merge pull request #2002 from ProjectMirador/1874-import-workspace-config-initial-implementation

Initial implementation import workspace config
parents 9a00105c 2a659e59
Branches
No related tags found
No related merge requests found
Showing
with 685 additions and 6 deletions
{
"companionWindows": {
"cw-b86f0558-6f04-4b56-ac9b-34b7e0769db3": {
"content": "info",
"default": true,
"id": "cw-b86f0558-6f04-4b56-ac9b-34b7e0769db3",
"position": "left"
},
"cw-b15df6ac-9069-4f16-bd19-e11c6aaa3911": {
"content": "thumbnail_navigation",
"default": true,
"id": "cw-b15df6ac-9069-4f16-bd19-e11c6aaa3911",
"position": "far-bottom"
},
"cw-c4934b3c-2411-40b1-9088-5e066276620f": {
"content": "info",
"default": true,
"id": "cw-c4934b3c-2411-40b1-9088-5e066276620f",
"position": "left"
},
"cw-76580e14-4d7a-4ea5-84a6-5de052941a3d": {
"content": "thumbnail_navigation",
"default": true,
"id": "cw-76580e14-4d7a-4ea5-84a6-5de052941a3d",
"position": "far-bottom"
}
},
"config": {
"canvasNavigation": {
"height": 50,
"width": 50
},
"theme": {
"palette": {
"type": "light",
"primary": {
"main": "#f5f5f5",
"light": "#ffffff",
"dark": "#eeeeee",
"contrastText": "rgba(0, 0, 0, 0.87)"
},
"secondary": {
"main": "#1967d2",
"light": "#64b5f6",
"dark": "#0d47a1",
"contrastText": "#fff"
},
"error": {
"main": "#b00020",
"light": "rgb(191, 51, 76)",
"dark": "rgb(123, 0, 22)",
"contrastText": "#fff"
}
},
"typography": {
"fontSize": 16,
"body1": {
"fontSize": "1rem",
"letterSpacing": "0em",
"lineHeight": "1.6em"
},
"body2": {
"fontSize": "0.878rem",
"letterSpacing": "0.015em",
"lineHeight": "1.6em"
},
"button": {
"fontSize": "0.878rem",
"letterSpacing": "0.09em",
"lineHeight": "2.25rem",
"textTransform": "uppercase"
},
"caption": {
"fontSize": "0.772rem",
"letterSpacing": "0.033em",
"lineHeight": "1.6rem"
},
"body1Next": {
"fontSize": "1rem",
"letterSpacing": "0em",
"lineHeight": "1.6em"
},
"body2Next": {
"fontSize": "0.878rem",
"letterSpacing": "0.015em",
"lineHeight": "1.6em"
},
"buttonNext": {
"fontSize": "0.878rem",
"letterSpacing": "0.09em",
"lineHeight": "2.25rem"
},
"captionNext": {
"fontSize": "0.772rem",
"letterSpacing": "0.33em",
"lineHeight": "1.6rem"
},
"overline": {
"fontSize": "0.678rem",
"fontWeight": 500,
"letterSpacing": "0.166em",
"lineHeight": "2em",
"textTransform": "uppercase"
},
"h1": {
"fontSize": "2.822rem",
"letterSpacing": "-0.015em",
"lineHeight": "1.2em"
},
"h2": {
"fontSize": "1.575rem",
"letterSpacing": "0em",
"lineHeight": "1.33em"
},
"h3": {
"fontSize": "1.383rem",
"fontWeight": 300,
"letterSpacing": "0em",
"lineHeight": "1.33em"
},
"h4": {
"fontSize": "1.215rem",
"letterSpacing": "0.007em",
"lineHeight": "1.45em"
},
"h5": {
"fontSize": "1.138rem",
"letterSpacing": "0.005em",
"lineHeight": "1.55em"
},
"h6": {
"fontSize": "1.067rem",
"fontWeight": 400,
"letterSpacing": "0.01em",
"lineHeight": "1.6em"
},
"subtitle1": {
"fontSize": "0.937rem",
"letterSpacing": "0.015em",
"lineHeight": "1.6em",
"fontWeight": 300
},
"subtitle2": {
"fontSize": "0.878rem",
"fontWeight": 500,
"letterSpacing": "0.02em",
"lineHeight": "1.75em"
},
"useNextVariants": true
},
"props": {
"MuiButtonBase": {
"disableTouchRipple": true
}
}
},
"language": "en",
"availableLanguages": {
"de": "Deutsch",
"en": "English"
},
"displayAllAnnotations": false,
"translations": {},
"window": {
"allowClose": true,
"allowMaximize": true,
"defaultView": "single"
},
"windows": [
{
"loadedManifest": "https://iiif.harvardartmuseums.org/manifests/object/299843",
"canvasIndex": 2
},
{
"loadedManifest": "https://iiif.bodleian.ox.ac.uk/iiif/manifest/e32a277e-91e2-4a6d-8ba6-cc4bad230410.json",
"thumbnailNavigationPosition": "off"
}
],
"thumbnailNavigation": {
"defaultPosition": "far-bottom",
"height": 150,
"width": 100
},
"workspace": {
"type": "mosaic"
},
"workspaceControlPanel": {
"enabled": true
},
"id": "mirador",
"manifests": {
"https://media.nga.gov/public/manifests/nga_highlights.json": {
"provider": "National Gallery of Art"
},
"https://data.ucd.ie/api/img/manifests/ucdlib:33064": {
"provider": "Irish Architectural Archive"
},
"https://wellcomelibrary.org/iiif/b18035723/manifest": {
"provider": "Wellcome Library"
},
"https://demos.biblissima.fr/iiif/metadata/florus-dispersus/manifest.json": {
"provider": "Biblissima"
},
"https://www.e-codices.unifr.ch/metadata/iiif/gau-Fragment/manifest.json": {
"provider": "e-codices - Virtual Manuscript Library of Switzerland"
},
"https://wellcomelibrary.org/iiif/collection/b18031511": {
"provider": "Wellcome Library"
},
"https://gallica.bnf.fr/iiif/ark:/12148/btv1b10022508f/manifest.json": {
"provider": "Bibliothèque nationale de France"
},
"https://manifests.britishart.yale.edu/Osbornfa1": {
"provider": "Beinecke Rare Book and Manuscript Library, Yale University"
},
"https://iiif.biblissima.fr/chateauroux/B360446201_MS0005/manifest.json": {
"provider": "Biblissima"
},
"https://iiif.durham.ac.uk/manifests/trifle/32150/t1/m4/q7/t1m4q77fr328/manifest": {
"provider": "Durham University Library"
}
}
},
"viewers": {
"window-404c667d-e1e0-4ae7-aaf2-98e08bb627bd": {
"x": 1205.5,
"y": 1686,
"zoom": 0.00021117049395471312
},
"window-f48761a4-4388-4ddd-aff6-9a331410aad3": {
"x": 2217.999999999988,
"y": 3193.5865262629163,
"zoom": 0.00010301562691749936
}
},
"windows": {
"window-404c667d-e1e0-4ae7-aaf2-98e08bb627bd": {
"canvasIndex": 4,
"collectionIndex": 0,
"companionWindowIds": [
"cw-b86f0558-6f04-4b56-ac9b-34b7e0769db3",
"cw-b15df6ac-9069-4f16-bd19-e11c6aaa3911"
],
"displayAllAnnotations": false,
"height": 400,
"id": "window-404c667d-e1e0-4ae7-aaf2-98e08bb627bd",
"manifestId": "https://wellcomelibrary.org/iiif/b18035723/manifest",
"maximized": false,
"rangeId": null,
"rotation": null,
"selectedAnnotations": {},
"sideBarPanel": "info",
"thumbnailNavigationId": "cw-b15df6ac-9069-4f16-bd19-e11c6aaa3911",
"view": "single",
"width": 400,
"x": 200,
"y": 200
},
"window-f48761a4-4388-4ddd-aff6-9a331410aad3": {
"canvasIndex": 1,
"collectionIndex": 0,
"companionWindowIds": [
"cw-c4934b3c-2411-40b1-9088-5e066276620f",
"cw-76580e14-4d7a-4ea5-84a6-5de052941a3d"
],
"displayAllAnnotations": false,
"height": 400,
"id": "window-f48761a4-4388-4ddd-aff6-9a331410aad3",
"manifestId": "https://demos.biblissima.fr/iiif/metadata/florus-dispersus/manifest.json",
"maximized": false,
"rangeId": null,
"rotation": null,
"selectedAnnotations": {},
"sideBarPanel": "info",
"thumbnailNavigationId": "cw-76580e14-4d7a-4ea5-84a6-5de052941a3d",
"view": "single",
"width": 400,
"x": 230,
"y": 250
}
},
"workspace": {
"exposeModeOn": false,
"height": 5000,
"viewportPosition": {
"x": 0,
"y": 0
},
"width": 5000,
"layout": {
"direction": "row",
"first": "window-404c667d-e1e0-4ae7-aaf2-98e08bb627bd",
"second": "window-f48761a4-4388-4ddd-aff6-9a331410aad3"
},
"focusedWindowId": "window-f48761a4-4388-4ddd-aff6-9a331410aad3",
"isWorkspaceAddVisible": false
}
}
\ No newline at end of file
import * as actions from '../../../src/state/actions';
import ActionTypes from '../../../src/state/actions/action-types';
import configFixture from '../../fixtures/config/export.example.json';
describe('config actions', () => {
describe('setConfig', () => {
......@@ -22,4 +23,15 @@ describe('config actions', () => {
expect(actions.updateConfig(config)).toEqual(expectedAction);
});
});
describe('importConfig', () => {
it('imports the config', () => {
const config = configFixture;
const expectedAction = {
config,
type: ActionTypes.IMPORT_CONFIG,
};
expect(actions.importConfig(config)).toEqual(expectedAction);
});
});
});
import * as actions from '../../../src/state/actions';
import ActionTypes from '../../../src/state/actions/action-types';
describe('errors actions', () => {
describe('addError', () => {
it('adds an error', () => {
const errorMessage = 'errorMessage';
const createdAction = actions.addError(errorMessage);
expect(createdAction).toHaveProperty('message', errorMessage);
expect(createdAction).toHaveProperty('id');
expect(createdAction.id).toBeDefined();
});
});
describe('removeError', () => {
it('removes an existing error', () => {
const errorId = 'testId123';
const expectedAction = {
id: errorId,
type: ActionTypes.REMOVE_ERROR,
};
expect(actions.removeError(errorId)).toEqual(expectedAction);
});
});
});
import React from 'react';
import { shallow } from 'enzyme';
import { ErrorDialog } from '../../../src/components/ErrorDialog';
/**
* Helper function to create a shallow wrapper around ErrorDialog
*/
function createWrapper(props) {
return shallow(
<ErrorDialog
t={key => key}
{...props}
/>,
);
}
describe('ErrorDialog', () => {
let wrapper;
it('renders properly', () => {
const errors = { testid123: { id: 'testid123', message: '' } };
wrapper = createWrapper({ errors });
expect(wrapper.find('WithStyles(Dialog)').length).toBe(1);
});
it('shows up error message correctly', () => {
const errorMessage = 'error testMessage 123';
const errors = { testid123: { id: 'testid123', message: errorMessage } };
wrapper = createWrapper({ errors });
expect(wrapper.find('WithStyles(Typography)[variant="body2"]').render().text()).toBe(errorMessage);
});
it('triggers the handleClick prop when clicking the ok button', () => {
const errors = { testid123: { id: 'testid123', message: '' } };
const mockHandleClick = jest.fn();
wrapper = createWrapper({ errors, removeError: mockHandleClick });
wrapper.find('WithStyles(Button)').simulate('click');
expect(mockHandleClick).toHaveBeenCalledTimes(1);
});
});
......@@ -3,6 +3,7 @@ import { shallow } from 'enzyme';
import WorkspaceControlPanel from '../../../src/containers/WorkspaceControlPanel';
import Workspace from '../../../src/containers/Workspace';
import WorkspaceAdd from '../../../src/containers/WorkspaceAdd';
import ErrorDialog from '../../../src/containers/ErrorDialog';
import { WorkspaceArea } from '../../../src/components/WorkspaceArea';
/** */
......@@ -29,6 +30,7 @@ describe('WorkspaceArea', () => {
<main>
<WorkspaceControlPanel />
<Workspace />
<ErrorDialog />
</main>,
)).toBeTruthy();
});
......
import { configReducer } from '../../../src/state/reducers/config';
import ActionTypes from '../../../src/state/actions/action-types';
import configFixture from '../../fixtures/config/export.example.json';
describe('config reducer', () => {
describe('SET_CONFIG', () => {
......@@ -42,4 +43,12 @@ describe('config reducer', () => {
});
});
});
describe('IMPORT_CONFIG', () => {
it('should handle IMPORT_CONFIG', () => {
expect(configReducer([], {
config: configFixture,
type: ActionTypes.IMPORT_CONFIG,
})).toEqual(configFixture);
});
});
});
import { errorsReducer } from '../../../src/state/reducers/errors';
import ActionTypes from '../../../src/state/actions/action-types';
describe('ADD_ERROR', () => {
const errorMessage = 'testErrorMessage';
const errorId = 'errorId123';
it('should handle ADD_ERROR', () => {
const error = {
id: errorId,
message: errorMessage,
};
const ret = errorsReducer(undefined, {
type: ActionTypes.ADD_ERROR,
...error,
});
expect(ret.items).toEqual([error.id]);
expect(ret).toHaveProperty(error.id);
expect(ret[error.id]).toEqual(error);
});
it('should handle REMOVE_ERROR', () => {
const stateBefore = {
errorId: {
id: errorId,
message: errorMessage,
},
items: [errorId],
};
/*
Only the id is removed from the 'items' array. The error itself remains part of the state,
so we are able to provide an error history or some kind of logs later on
*/
expect(errorsReducer(stateBefore, {
id: errorId,
type: ActionTypes.REMOVE_ERROR,
})).toHaveProperty('items', []);
});
});
......@@ -29,7 +29,7 @@
"repository": "https://github.com/ProjectMirador/mirador",
"size-limit": [
{
"limit": "360 KB",
"limit": "390 KB",
"path": "dist/mirador.min.js"
}
],
......
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 PropTypes from 'prop-types';
import { Typography } from '@material-ui/core';
import Button from '@material-ui/core/Button';
import {
first,
isUndefined,
omit,
values,
} from 'lodash';
/**
*/
export class ErrorDialog extends Component {
/**
* render
* @return
*/
render() {
const {
errors, removeError, t,
} = this.props;
/* extract 'items' value and get first key-value-pair (an error) */
const error = first(values(omit(errors, 'items')));
const hasError = !isUndefined(error);
return (
<div>
{ hasError && (
<Dialog onClose={() => removeError(error.id)} open={hasError}>
<DialogTitle>{t('errorDialogTitle')}</DialogTitle>
<DialogContent>
<Typography variant="body2" noWrap color="inherit">
{error.message}
</Typography>
<div>
<Button onClick={() => removeError(error.id)}>
{t('errorDialogConfirm')}
</Button>
</div>
</DialogContent>
</Dialog>
)}
</div>
);
}
}
ErrorDialog.propTypes = {
errors: PropTypes.object, // eslint-disable-line react/forbid-prop-types
removeError: PropTypes.func,
t: PropTypes.func,
};
ErrorDialog.defaultProps = {
errors: null,
removeError: () => {},
t: key => key,
};
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import ErrorDialog from '../containers/ErrorDialog';
import WorkspaceControlPanel from '../containers/WorkspaceControlPanel';
import Workspace from '../containers/Workspace';
import WorkspaceAdd from '../containers/WorkspaceAdd';
......@@ -31,6 +32,7 @@ export class WorkspaceArea extends Component {
? <WorkspaceAdd />
: <Workspace />
}
<ErrorDialog />
</main>
);
}
......
......@@ -13,11 +13,20 @@ export class WorkspaceExport extends Component {
*/
exportableState() {
const { state } = this.props;
const { config, windows } = state;
const {
companionWindows,
config,
viewers,
windows,
workspace,
} = state;
return JSON.stringify({
companionWindows,
config,
viewers,
windows,
workspace,
}, null, 2);
}
......
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 PropTypes from 'prop-types';
import { Input } from '@material-ui/core';
import Button from '@material-ui/core/Button';
/**
*/
export class WorkspaceImport extends Component {
/**
*
* constructor
*/
constructor(props) {
super(props);
this.state = {
configImportValue: '',
};
this.handleClick = this.handleClick.bind(this);
this.handleChange = this.handleChange.bind(this);
}
/**
* @private
*/
handleChange(event) {
event.preventDefault();
this.setState({
configImportValue: event.target.value,
});
}
/**
* @private
*/
handleClick(event) {
const { importConfig } = this.props;
const { configImportValue } = this.state;
event.preventDefault();
try {
const configJSON = JSON.parse(configImportValue);
importConfig(configJSON);
} catch (ex) {
const { addError } = this.props;
addError(ex.toString());
}
}
/**
* render
* @return
*/
render() {
const {
handleClose, open, t,
} = this.props;
return (
<Dialog id="workspace-import" open={open} onClose={handleClose}>
<DialogTitle id="workspace-import-title">{t('import')}</DialogTitle>
<DialogContent>
<Input id="workspace-import-input" rows="15" multiline variant="filled" onChange={this.handleChange} />
<div>
<Button onClick={this.handleClick}>
{t('importWorkspace')}
</Button>
</div>
</DialogContent>
</Dialog>
);
}
}
WorkspaceImport.propTypes = {
addError: PropTypes.func.isRequired,
handleClose: PropTypes.func.isRequired,
importConfig: PropTypes.func.isRequired, // eslint-disable-line react/forbid-prop-types
open: PropTypes.bool, // eslint-disable-line react/forbid-prop-types
t: PropTypes.func,
};
WorkspaceImport.defaultProps = {
open: false,
t: key => key,
};
import React, { Component } from 'react';
import Menu from '@material-ui/core/Menu';
import ImportIcon from '@material-ui/icons/Input';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import MenuItem from '@material-ui/core/MenuItem';
import Typography from '@material-ui/core/Typography';
......@@ -12,6 +13,7 @@ import WindowList from '../containers/WindowList';
import WorkspaceSettings from '../containers/WorkspaceSettings';
import WorkspaceSelectionDialog from '../containers/WorkspaceSelectionDialog';
import WorkspaceExport from '../containers/WorkspaceExport';
import WorkspaceImport from '../containers/WorkspaceImport';
import ns from '../config/css-ns';
/**
......@@ -24,6 +26,7 @@ export class WorkspaceMenu extends Component {
super(props);
this.state = {
exportWorkspace: {},
importWorkspace: {},
settings: {},
toggleZoom: {},
windowList: {},
......@@ -79,6 +82,7 @@ export class WorkspaceMenu extends Component {
} = this.props;
const {
importWorkspace,
windowList,
toggleZoom,
settings,
......@@ -155,6 +159,17 @@ export class WorkspaceMenu extends Component {
</ListItemIcon>
<Typography variant="body1">{t('downloadExportWorkspace')}</Typography>
</MenuItem>
<MenuItem
aria-haspopup="true"
id="workspace-menu-import"
onClick={(e) => { this.handleMenuItemClick('importWorkspace', e); handleClose(e); }}
aria-owns={exportWorkspace.AnchorEl ? 'workspace-import' : undefined}
>
<ListItemIcon>
<ImportIcon />
</ListItemIcon>
<Typography variant="body1">{t('importWorkspace')}</Typography>
</MenuItem>
</Menu>
<WindowList
anchorEl={windowList.anchorEl}
......@@ -180,6 +195,11 @@ export class WorkspaceMenu extends Component {
container={container}
handleClose={this.handleMenuItemClose('exportWorkspace')}
/>
<WorkspaceImport
open={Boolean(importWorkspace.open)}
container={container}
handleClose={this.handleMenuItemClose('importWorkspace')}
/>
</>
);
}
......
export * from './App';
export * from './CanvasThumbnail';
export * from './CompanionWindow';
export * from './ErrorDialog';
export * from './LabelValueMetadata';
export * from './LanguageSettings';
export * from './ManifestForm';
......@@ -33,6 +34,7 @@ export * from './WorkspaceControlPanel';
export * from './WorkspaceControlPanelButtons';
export * from './WorkspaceExport';
export * from './WorkspaceFullScreenButton';
export * from './WorkspaceImport';
export * from './WorkspaceMenu';
export * from './WorkspaceMenuButton';
export * from './WorkspaceMosaic';
......
......@@ -11,7 +11,7 @@ import { CompanionArea } from '../components/CompanionArea';
const mapStateToProps = (state, { windowId, position }) => ({
companionAreaOpen: getCompanionAreaVisibility(state, { position, windowId }),
companionWindows: getCompanionWindowsOfWindow(state, { windowId })
.filter(cw => cw.position === position),
.filter(cw => cw && cw.position === position),
sideBarOpen: getWindow(state, { windowId }).sideBarOpen,
});
......
import { compose } from 'redux';
import { connect } from 'react-redux';
import { withTranslation } from 'react-i18next';
import { withPlugins } from '../extend';
import { ErrorDialog } from '../components/ErrorDialog';
import * as actions from '../state/actions';
/**
* mapStateToProps - to hook up connect
* @memberof ErrorDialog
* @private
*/
const mapStateToProps = state => ({
errors: state.errors,
});
/**
* mapDispatchToProps - used to hook up connect to action creators
* @memberof App
* @private
*/
const mapDispatchToProps = {
removeError: actions.removeError,
};
const enhance = compose(
withTranslation(),
connect(mapStateToProps, mapDispatchToProps),
withPlugins('ErrorDialog'),
);
export default enhance(ErrorDialog);
......@@ -6,7 +6,9 @@ import { withPlugins } from '../extend';
import CanvasGroupings from '../lib/CanvasGroupings';
import * as actions from '../state/actions';
import { ThumbnailNavigation } from '../components/ThumbnailNavigation';
import { getWindow, getManifestCanvases } from '../state/selectors';
import { getWindow } from '../state/selectors/windows';
import { getManifestCanvases } from '../state/selectors/manifests';
/**
* mapStateToProps - used to hook up state to props
* @memberof ThumbnailNavigation
......
import { compose } from 'redux';
import { connect } from 'react-redux';
import { withTranslation } from 'react-i18next';
import { withPlugins } from '../extend';
import { WorkspaceImport } from '../components/WorkspaceImport';
import * as actions from '../state/actions';
/**
* mapDispatchToProps - used to hook up connect to action creators
* @memberof App
* @private
*/
const mapDispatchToProps = {
addError: actions.addError,
importConfig: actions.importWorkspace,
};
const enhance = compose(
withTranslation(),
connect(null, mapDispatchToProps),
withPlugins('WorkspaceImport'),
);
export default enhance(WorkspaceImport);
......@@ -8,7 +8,7 @@ import { WorkspaceMenu } from '../components/WorkspaceMenu';
/**
* mapDispatchToProps - used to hook up connect to action creators
* @memberof ManifestListItem
* @memberof WorkspaceMenu
* @private
*/
const mapDispatchToProps = {
......
......@@ -21,11 +21,15 @@
"downloadExport": "Download/Export Arbeitsfläche",
"downloadExportWorkspace": "Download/Export Arbeitsfläche",
"elastic": "Elastisch",
"errorDialogTitle": "Es ist ein Fehler aufgetreten",
"errorDialogConfirm": "OK",
"exitFullScreen": "Vollbildmodus verlassen",
"expandSidePanel": "Seitenleiste aufklappen",
"fetchManifest": "Hinzufügen",
"gallery": "Galerie",
"hideZoomControls": "Zoomsteuerung verbergen",
"import" : "Import",
"importWorkspace": "Import Arbeitsfläche",
"item": "Objekt: {{label}}",
"language": "Sprache",
"light": "Hell",
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment