Skip to content
Snippets Groups Projects
Commit d80412f4 authored by Jack Reed's avatar Jack Reed
Browse files

Implement basic React/Redux windowing in Mosaic mode

Using react-mosaic, implements a basic Redux backed windowing
system.
parent 72e3efb0
Branches update-configs
No related tags found
No related merge requests found
......@@ -13,4 +13,15 @@ describe('workspace actions', () => {
expect(actions.fullscreenWorkspace(options)).toEqual(expectedAction);
});
});
describe('updateWorkspaceMosaicLayout', () => {
it('should updates mosaic layout', () => {
const options = { foo: 'bar' };
const expectedAction = {
type: ActionTypes.UPDATE_WORKSPACE_MOSAIC_LAYOUT,
layout: { foo: 'bar' },
};
expect(actions.updateWorkspaceMosaicLayout(options)).toEqual(expectedAction);
});
});
});
......@@ -10,8 +10,6 @@ describe('Window', () => {
it('should render outer element', () => {
wrapper = shallow(<Window window={window} />);
expect(wrapper.find('.mirador-window')).toHaveLength(1);
expect(wrapper.instance().styleAttributes())
.toEqual({ width: '400px', height: '500px' });
});
it('should render <WindowTopBar>', () => {
wrapper = shallow(<Window window={window} />);
......
import React from 'react';
import { shallow } from 'enzyme';
import { Mosaic } from 'react-mosaic-component';
import Workspace from '../../../src/components/Workspace';
import Window from '../../../src/containers/Window';
describe('Workspace', () => {
const windows = { 1: { id: 1 }, 2: { id: 2 } };
it('should render properly', () => {
const wrapper = shallow(<Workspace windows={windows} />);
let wrapper;
beforeEach(() => {
wrapper = shallow(
<Workspace
windows={windows}
workspace={{}}
updateWorkspaceMosaicLayout={() => {}}
/>,
);
});
it('should render properly with an initialValue', () => {
expect(wrapper.matchesElement(
<div className="mirador-workspace">
<Window window={{ id: 1 }} />
<Window window={{ id: 2 }} />
<Mosaic initialValue={{ direction: 'row', first: '1', second: '2' }} />
</div>,
)).toBe(true);
});
describe('determineWorkspaceLayout', () => {
it('when window ids do not match workspace layout', () => {
wrapper = shallow(
<Workspace
windows={windows}
workspace={{ layout: 'foo' }}
updateWorkspaceMosaicLayout={() => {}}
/>,
);
expect(wrapper.instance().determineWorkspaceLayout()).toMatchObject({
direction: 'row', first: '1', second: '2',
});
});
it('when window ids match workspace layout', () => {
wrapper = shallow(
<Workspace
windows={{ foo: { id: 'foo' } }}
workspace={{ layout: 'foo' }}
updateWorkspaceMosaicLayout={() => {}}
/>,
);
expect(wrapper.instance().determineWorkspaceLayout()).toBeNull();
});
});
describe('tileRenderer', () => {
it('when window is available', () => {
expect(wrapper.instance().tileRenderer('1')).not.toBeNull();
});
it('when window is not available', () => {
expect(wrapper.instance().tileRenderer('bar')).toBeNull();
});
});
describe('mosaicChange', () => {
it('calls the provided prop to update layout', () => {
const mock = jest.fn();
wrapper = shallow(
<Workspace
windows={{ foo: { id: 'foo' } }}
workspace={{ layout: 'foo' }}
updateWorkspaceMosaicLayout={mock}
/>,
);
wrapper.instance().mosaicChange();
expect(mock).toBeCalled();
});
});
});
......@@ -18,4 +18,12 @@ describe('workspace reducer', () => {
fullscreen: true,
});
});
it('should handle UPDATE_WORKSPACE_MOSAIC_LAYOUT', () => {
expect(reducer([], {
type: ActionTypes.UPDATE_WORKSPACE_MOSAIC_LAYOUT,
layout: { foo: 'bar' },
})).toEqual({
layout: { foo: 'bar' },
});
});
});
......@@ -10,21 +10,13 @@ import ThumbnailNavigation from '../containers/ThumbnailNavigation';
* @param {object} window
*/
class Window extends Component {
/**
* Return style attributes
*/
styleAttributes() {
const { window } = this.props;
return { width: `${window.xywh[2]}px`, height: `${window.xywh[3]}px` };
}
/**
* Renders things
*/
render() {
const { manifest, window } = this.props;
return (
<div className={ns('window')} style={this.styleAttributes()}>
<div className={ns('window')}>
<WindowTopBar
windowId={window.id}
manifest={manifest}
......
import React from 'react';
import PropTypes from 'prop-types';
import {
Mosaic, getLeaves, createBalancedTreeFromLeaves,
} from 'react-mosaic-component';
import 'react-mosaic-component/react-mosaic-component.css';
import Window from '../containers/Window';
import ns from '../config/css-ns';
......@@ -10,27 +14,81 @@ import ns from '../config/css-ns';
*/
class Workspace extends React.Component {
/**
* render
*/
render() {
constructor(props) {
super(props);
this.tileRenderer = this.tileRenderer.bind(this);
this.mosaicChange = this.mosaicChange.bind(this);
this.determineWorkspaceLayout = this.determineWorkspaceLayout.bind(this);
}
/**
* Render a tile (Window) in the Mosaic.
*/
tileRenderer(id, path) {
const { windows } = this.props;
const window = windows[id];
if (!window) return null;
return (
<div className={ns('workspace')}>
{
Object.values(windows).map(window => (
<Window
key={window.id}
window={window}
/>
))
);
}
/**
* Update the redux store when the Mosaic is changed.
*/
mosaicChange(newLayout) {
const { updateWorkspaceMosaicLayout } = this.props;
updateWorkspaceMosaicLayout(newLayout);
}
/**
* Used to determine whether or not a "new" layout should be autogenerated.
* If a Window is added or removed, generate that new layout and use that for
* this render. When the Mosaic changes, that will trigger a new store update.
*/
determineWorkspaceLayout() {
const { windows, workspace } = this.props;
const windowKeys = Object.keys(windows);
const leaveKeys = getLeaves(workspace.layout);
// Check every window is in the layout, and all layout windows are present
// in store
if (!windowKeys.every(e => leaveKeys.includes(e))
|| !leaveKeys.every(e => windowKeys.includes(e))) {
const newLayout = createBalancedTreeFromLeaves(windowKeys);
return newLayout;
}
return null;
}
/**
* render
*/
render() {
const { workspace } = this.props;
const newLayout = this.determineWorkspaceLayout();
return (
<div className={ns('workspace')}>
<Mosaic
renderTile={this.tileRenderer}
initialValue={newLayout || workspace.layout}
onChange={this.mosaicChange}
className="mirador-mosaic"
zeroStateView={<div />}
/>
</div>
);
}
}
Workspace.propTypes = {
updateWorkspaceMosaicLayout: PropTypes.func.isRequired,
windows: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
workspace: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
};
export default Workspace;
......@@ -7,8 +7,9 @@ import Window from '../components/Window';
* @memberof Window
* @private
*/
const mapStateToProps = ({ manifests }, props) => ({
const mapStateToProps = ({ manifests, windows }, props) => ({
manifest: manifests[props.window.manifestId],
window: windows[props.window.id],
});
const enhance = compose(
......
import { compose } from 'redux';
import { connect } from 'react-redux';
import * as actions from '../state/actions';
import Workspace from '../components/Workspace';
/**
......@@ -10,11 +11,20 @@ import Workspace from '../components/Workspace';
const mapStateToProps = state => (
{
windows: state.windows,
workspace: state.workspace,
}
);
/**
* mapDispatchToProps - used to hook up connect to action creators
* @memberof Workspace
* @private
*/
const mapDispatchToProps = { updateWorkspaceMosaicLayout: actions.updateWorkspaceMosaicLayout };
const enhance = compose(
connect(mapStateToProps),
connect(mapStateToProps, mapDispatchToProps),
// further HOC go here
);
......
......@@ -20,6 +20,7 @@ const ActionTypes = {
RECEIVE_INFO_RESPONSE: 'RECEIVE_INFO_RESPONSE',
RECEIVE_INFO_RESPONSE_FAILURE: 'RECEIVE_INFO_RESPONSE_FAILURE',
REMOVE_INFO_RESPONSE: 'REMOVE_INFO_RESPONSE',
UPDATE_WORKSPACE_MOSAIC_LAYOUT: 'UPDATE_WORKSPACE_MOSAIC_LAYOUT',
};
export default ActionTypes;
......@@ -10,3 +10,13 @@ import ActionTypes from './action-types';
export function fullscreenWorkspace(fullscreen) {
return { type: ActionTypes.FULLSCREEN_WORKSPACE, fullscreen };
}
/**
* updateWorkspaceMosaicLayout - action creator
*
* @param {Object} layout
* @memberof ActionCreators
*/
export function updateWorkspaceMosaicLayout(layout) {
return { type: ActionTypes.UPDATE_WORKSPACE_MOSAIC_LAYOUT, layout };
}
......@@ -9,6 +9,8 @@ const workspaceReducer = (state = {}, action) => {
return { ...state, focusedWindowId: action.windowId };
case ActionTypes.FULLSCREEN_WORKSPACE:
return { ...state, fullscreen: action.fullscreen };
case ActionTypes.UPDATE_WORKSPACE_MOSAIC_LAYOUT:
return { ...state, layout: action.layout };
default:
return state;
}
......
......@@ -27,6 +27,7 @@ body {
&-window {
display: flex;
flex-direction: column;
height: 100%;
}
&-osd-container {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment