diff --git a/__tests__/src/components/WorkspaceMosaic.test.js b/__tests__/src/components/WorkspaceMosaic.test.js index bf6b0a0840568142cb5dd87d1bf07df22b7614d6..e89f5aa6f9e7d5690429cd013b105fe923f220d3 100644 --- a/__tests__/src/components/WorkspaceMosaic.test.js +++ b/__tests__/src/components/WorkspaceMosaic.test.js @@ -39,6 +39,34 @@ describe('WorkspaceMosaic', () => { expect(updateWorkspaceMosaicLayout).toHaveBeenCalled(); }); + it('updates the workspace layout when windows are removed', () => { + const updateWorkspaceMosaicLayout = jest.fn(); + wrapper = createWrapper({ + updateWorkspaceMosaicLayout, + windows, + workspace: { + layout: { first: 1, second: 2 }, + }, + }); + wrapper.instance().windowPaths = { 2: ['second'] }; + wrapper.setProps({ windows: { 1: { id: 1 } } }); + expect(updateWorkspaceMosaicLayout).toHaveBeenLastCalledWith(1); + }); + it('when no windows remain', () => { + const updateWorkspaceMosaicLayout = jest.fn(); + wrapper = createWrapper({ + updateWorkspaceMosaicLayout, + windows, + }); + wrapper.setProps({ windows: {} }); + expect(updateWorkspaceMosaicLayout).toHaveBeenLastCalledWith({}); + }); + }); + describe('bookkeepPath', () => { + it('as windows are rendered keeps a reference to their path in binary tree', () => { + wrapper.instance().tileRenderer('1', 'foo'); + expect(wrapper.instance().windowPaths).toEqual({ 1: 'foo' }); + }); }); describe('determineWorkspaceLayout', () => { it('when window ids do not match workspace layout', () => { @@ -47,9 +75,9 @@ describe('WorkspaceMosaic', () => { direction: 'row', first: '1', second: '2', }); }); - it('when there are no windows', () => { + it('by default use workspace.layout', () => { wrapper = createWrapper({ windows: {}, workspace: { layout: 'foo' } }); - expect(wrapper.instance().determineWorkspaceLayout()).toBeNull(); + expect(wrapper.instance().determineWorkspaceLayout()).toEqual('foo'); }); it('when window ids match workspace layout', () => { wrapper = createWrapper({ windows: { foo: { id: 'foo' } }, workspace: { layout: 'foo' } }); diff --git a/src/components/WorkspaceMosaic.js b/src/components/WorkspaceMosaic.js index 1c7102dd4108a0f3a6f7fbddfa1ce2cdaef097ef..09476da9da2712e2e3dac6165a36086115ceab85 100644 --- a/src/components/WorkspaceMosaic.js +++ b/src/components/WorkspaceMosaic.js @@ -3,7 +3,9 @@ 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 MosaicRenderPreview from '../containers/MosaicRenderPreview'; import Window from '../containers/Window'; @@ -22,6 +24,7 @@ export class WorkspaceMosaic extends React.Component { this.mosaicChange = this.mosaicChange.bind(this); this.determineWorkspaceLayout = this.determineWorkspaceLayout.bind(this); this.zeroStateView = <div />; + this.windowPaths = {}; } /** */ @@ -35,28 +38,58 @@ export class WorkspaceMosaic extends React.Component { /** */ componentDidUpdate(prevProps) { const { windows, workspace, updateWorkspaceMosaicLayout } = this.props; - if (prevProps.windows !== windows || prevProps.workspace !== workspace) { + const prevWindows = Object.keys(prevProps.windows); + const currentWindows = Object.keys(windows); + // Handles when Windows are removed from the state + if (!prevWindows.every(e => currentWindows.includes(e))) { + // There are no more remaining Windows, just return an empty layout + if (currentWindows.length === 0) { + updateWorkspaceMosaicLayout({}); + 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); + } + // Handles when Windows are added (not via Add Resource UI) + // TODO: If a window is added, add it in a better way #2380 + if (!currentWindows.every(e => prevWindows.includes(e))) { const newLayout = this.determineWorkspaceLayout(); if (newLayout !== workspace.layout) updateWorkspaceMosaicLayout(newLayout); } } + /** + * bookkeepPath - used to book keep Window's path's + * @param {String} windowId [description] + * @param {Array} path [description] + */ + bookkeepPath(windowId, path) { + this.windowPaths[windowId] = path; + } + /** * 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. + * 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 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; + // Windows were added + if (!windowKeys.every(e => leaveKeys.includes(e))) { + // No current layout, so just generate a new one + if (leaveKeys.length === 0) { + return createBalancedTreeFromLeaves(windowKeys); + } + // TODO: Here is where we will determine where to add a new Window #2380 + return createBalancedTreeFromLeaves(windowKeys); } return workspace.layout; @@ -69,7 +102,7 @@ export class WorkspaceMosaic extends React.Component { const { windows } = this.props; const window = windows[id]; if (!window) return null; - + this.bookkeepPath(window.id, path); return ( <MosaicWindow toolbarControls={[]}