diff --git a/__tests__/src/actions/window.test.js b/__tests__/src/actions/window.test.js index 5f1dd790ae..ba0e755427 100644 --- a/__tests__/src/actions/window.test.js +++ b/__tests__/src/actions/window.test.js @@ -86,6 +86,7 @@ describe('window actions', () => { collectionIndex: 0, height: 400, id: 'helloworld', + layoutOrder: 3, manifestId: null, maximized: false, rangeId: null, diff --git a/__tests__/src/components/WorkspaceMosaic.test.js b/__tests__/src/components/WorkspaceMosaic.test.js index e89f5aa6f9..ed15fe9f8a 100644 --- a/__tests__/src/components/WorkspaceMosaic.test.js +++ b/__tests__/src/components/WorkspaceMosaic.test.js @@ -76,7 +76,11 @@ describe('WorkspaceMosaic', () => { }); }); it('by default use workspace.layout', () => { - wrapper = createWrapper({ windows: {}, workspace: { layout: 'foo' } }); + wrapper = createWrapper({ windows: { foo: 'bar' }, workspace: { layout: 'foo' } }); + expect(wrapper.instance().determineWorkspaceLayout()).toEqual('foo'); + }); + it('generates a new layout if windows do not match current layout', () => { + wrapper = createWrapper({ windows: { foo: 'bar' }, workspace: { layout: { first: 'foo', second: 'bark' } } }); expect(wrapper.instance().determineWorkspaceLayout()).toEqual('foo'); }); it('when window ids match workspace layout', () => { diff --git a/__tests__/src/lib/MosaicLayout.test.js b/__tests__/src/lib/MosaicLayout.test.js new file mode 100644 index 0000000000..a364e77393 --- /dev/null +++ b/__tests__/src/lib/MosaicLayout.test.js @@ -0,0 +1,51 @@ +import MosaicLayout from '../../../src/lib/MosaicLayout'; + +describe('MosaicLayout', () => { + describe('constructor', () => { + it('sets layout', () => { + expect(new MosaicLayout('foo').layout).toEqual('foo'); + }); + }); + describe('addWindows', () => { + let instance; + beforeEach(() => { + instance = new MosaicLayout('foo'); + }); + it('case 1 window: adds to the top right', () => { + expect(instance.layout).toEqual('foo'); + instance.addWindows(['bar']); + expect(instance.layout).toEqual({ + direction: 'row', + first: 'foo', + second: 'bar', + }); + }); + it('case 3 windows: adds to the top right', () => { + expect(instance.layout).toEqual('foo'); + instance.addWindows(['bar', 'bat', 'bark']); + expect(instance.layout).toEqual({ + direction: 'row', + first: 'foo', + second: { + direction: 'column', + first: { + direction: 'row', + first: 'bat', + second: 'bark', + }, + second: 'bar', + }, + }); + }); + }); + describe('removeWindows', () => { + let instance; + beforeEach(() => { + instance = new MosaicLayout({ first: 'foo', second: 'bar' }); + }); + it('case 1 window: returns a single window', () => { + instance.removeWindows(['bar'], { bar: ['second'] }); + expect(instance.layout).toEqual('foo'); + }); + }); +}); diff --git a/src/components/WorkspaceMosaic.js b/src/components/WorkspaceMosaic.js index db11272d89..db403ad5be 100644 --- a/src/components/WorkspaceMosaic.js +++ b/src/components/WorkspaceMosaic.js @@ -3,11 +3,12 @@ import PropTypes from 'prop-types'; import { Mosaic, MosaicWindow, getLeaves, createBalancedTreeFromLeaves, } from 'react-mosaic-component'; -import { createRemoveUpdate, updateTree } from 'react-mosaic-component/lib/util/mosaicUpdates'; import 'react-mosaic-component/react-mosaic-component.css'; import difference from 'lodash/difference'; +import toPairs from 'lodash/toPairs'; import MosaicRenderPreview from '../containers/MosaicRenderPreview'; import Window from '../containers/Window'; +import MosaicLayout from '../lib/MosaicLayout'; /** * Represents a work area that contains any number of windows @@ -48,14 +49,10 @@ export class WorkspaceMosaic extends React.Component { return; } - // Generate a set of "removeUpdates" to update layout binary tree const removedWindows = difference(prevWindows, currentWindows); - const removeUpdates = removedWindows - .map(windowId => ( - createRemoveUpdate(workspace.layout, this.windowPaths[windowId]) - )); - const newTree = updateTree(workspace.layout, removeUpdates); - updateWorkspaceMosaicLayout(newTree); + const layout = new MosaicLayout(workspace.layout); + layout.removeWindows(removedWindows, this.windowPaths); + updateWorkspaceMosaicLayout(layout.layout); } // Handles when Windows are added (not via Add Resource UI) // TODO: If a window is added, add it in a better way #2380 @@ -76,22 +73,29 @@ export class WorkspaceMosaic extends React.Component { /** * Used to determine whether or not a "new" layout should be autogenerated. - * TODO: If a window is added, add it in a better way #2380 */ determineWorkspaceLayout() { const { windows, workspace } = this.props; - const windowKeys = Object.keys(windows).sort(); + const sortedWindows = toPairs(windows) + .sort((a, b) => a.layoutOrder - b.layoutOrder).map(val => val[0]); const leaveKeys = getLeaves(workspace.layout); // Windows were added - if (!windowKeys.every(e => leaveKeys.includes(e))) { + if (!sortedWindows.every(e => leaveKeys.includes(e))) { // No current layout, so just generate a new one - if (leaveKeys.length === 0) { - return createBalancedTreeFromLeaves(windowKeys); + if (leaveKeys.length < 2) { + return createBalancedTreeFromLeaves(sortedWindows); } - // TODO: Here is where we will determine where to add a new Window #2380 - return createBalancedTreeFromLeaves(windowKeys); + // Add new windows to layout + const addedWindows = difference(sortedWindows, leaveKeys); + const layout = new MosaicLayout(workspace.layout); + layout.addWindows(addedWindows); + return layout.layout; + } + // Windows were removed (perhaps in a different Workspace). We don't have a + // way to reconfigure.. so we have to random generate + if (!leaveKeys.every(e => sortedWindows.includes(e))) { + return createBalancedTreeFromLeaves(sortedWindows); } - return workspace.layout; } diff --git a/src/lib/MosaicLayout.js b/src/lib/MosaicLayout.js new file mode 100644 index 0000000000..c64710a7c3 --- /dev/null +++ b/src/lib/MosaicLayout.js @@ -0,0 +1,79 @@ +import { createRemoveUpdate, updateTree } from 'react-mosaic-component/lib/util/mosaicUpdates'; +import { + getNodeAtPath, getOtherDirection, getPathToCorner, Corner, +} from 'react-mosaic-component/lib/util/mosaicUtilities'; +import dropRight from 'lodash/dropRight'; + +/** */ +export default class MosaicLayout { + /** */ + constructor(layout) { + this.layout = layout; + } + + /** */ + pathToCorner(corner = Corner.TOP_RIGHT) { + return getPathToCorner(this.layout, corner); + } + + /** */ + pathToParent(path) { + return getNodeAtPath(this.layout, dropRight(path)); + } + + /** */ + nodeAtPath(path) { + return getNodeAtPath(this.layout, path); + } + + /** + * addWindows - updates the layout with new windows using an algorithm ported + * from the react-mosaic-components examples. Will always add to the Top Right + * https://github.com/nomcopter/react-mosaic/blob/5081df8d1528d4c3b83a72763a46a30b3048fe95/demo/ExampleApp.tsx#L119-L154 + * @param {Array} addedWindowIds [description] + */ + addWindows(addedWindowIds) { + addedWindowIds.forEach((windowId, i) => { + const path = this.pathToCorner(); + const parent = this.pathToParent(path); + const destination = this.nodeAtPath(path); + const direction = parent ? getOtherDirection(parent.direction) : 'row'; + let first; + let second; + if (direction === 'row') { + first = destination; + second = addedWindowIds[i]; + } else { + first = addedWindowIds[i]; + second = destination; + } + const update = { + path, + spec: { + $set: { + direction, + first, + second, + }, + }, + }; + // We cannot batch the updates together because we need to recalculate + // the new location for each new window + this.layout = updateTree(this.layout, [update]); + }); + } + + /** + * removeWindows - Generate a set of "removeUpdates" to update layout binary + * tree. Then update the layout. + * @param {Array} removedWindowIds + * @param {Object} windowPaths - a lookup table for window paths + */ + removeWindows(removedWindowIds, windowPaths) { + const removeUpdates = removedWindowIds + .map(windowId => ( + createRemoveUpdate(this.layout, windowPaths[windowId]) + )); + this.layout = updateTree(this.layout, removeUpdates); + } +} diff --git a/src/state/actions/window.js b/src/state/actions/window.js index b48a5b78b8..b9af390403 100644 --- a/src/state/actions/window.js +++ b/src/state/actions/window.js @@ -52,6 +52,7 @@ export function addWindow(options) { displayAllAnnotations: config.displayAllAnnotations || false, height: 400, id: `window-${uuid()}`, + layoutOrder: numWindows + 1, manifestId: null, maximized: false, rangeId: null,