diff --git a/app/lib/index.js b/app/lib/index.js index 3aaa24f416..74fbd224d9 100644 --- a/app/lib/index.js +++ b/app/lib/index.js @@ -273,11 +273,12 @@ app.openFiles = []; app.on('app:parse-cmd', function(argv, cwd) { console.log('app:parse-cmd', argv.join(' '), cwd); - var files = Cli.extractFiles(argv, cwd); + // will result in opening dev.js as file + // var files = Cli.extractFiles(argv, cwd); - files.forEach(function(file) { - app.emit('app:open-file', file); - }); + // files.forEach(function(file) { + // app.emit('app:open-file', file); + // }); }); app.on('app:open-file', function(filePath) { @@ -313,7 +314,7 @@ app.on('app:client-ready', function() { } }); - renderer.send('client:open-files', files); + // renderer.send('client:open-files', files); renderer.send('client:started'); }); diff --git a/app/lib/workspace.js b/app/lib/workspace.js index 2c04b947ba..1b4c4595fe 100644 --- a/app/lib/workspace.js +++ b/app/lib/workspace.js @@ -12,20 +12,20 @@ var renderer = require('./util/renderer'); function Workspace(config) { renderer.on('workspace:restore', function(defaultConfig, done) { - var tabs = [], + var files = [], workspace = config.get('workspace', null); if (!workspace) { return done(null, defaultConfig); } - forEach(workspace.tabs, function(diagram) { + forEach(workspace.files, function(diagram) { try { var contents = fs.readFileSync(diagram.path, { encoding: 'utf8' }); diagram.contents = contents; - tabs.push(diagram); + files.push(diagram); console.log('[workspace]', 'restore', diagram.path); } catch (err) { @@ -33,7 +33,7 @@ function Workspace(config) { } }); - workspace.tabs = tabs; + workspace.files = files; done(null, workspace); }); diff --git a/client/src/app/App.js b/client/src/app/App.js index 2e30c40617..b7cad1623a 100644 --- a/client/src/app/App.js +++ b/client/src/app/App.js @@ -30,6 +30,7 @@ import History from './History'; import css from './App.less'; import { + assign, merge } from 'min-dash'; @@ -44,29 +45,20 @@ export const EMPTY_TAB = { type: 'empty' }; +const INITIAL_STATE = { + activeTab: -1, + dirtyTabs: {}, + layout: {}, + tabs: [], + tabState: {} +}; export class App extends Component { constructor(props, context) { super(); - - this.state = { - tabs: [], - activeTab: EMPTY_TAB, - dirtyTabs: {}, - tabState: {}, - layout: { - - // TODO get layout from workspace - minimap: { - open: true - }, - propertiesPanel: { - open: true - } - } - }; + this.state = INITIAL_STATE; // TODO(nikku): make state this.tabHistory = new History(); @@ -207,6 +199,8 @@ export class App extends Component { } await this._removeTab(tab); + + this.saveWorkspace(); } isDirty = (tab) => { @@ -338,11 +332,69 @@ export class App extends Component { this.setState({ layout: merge(layout, newLayout) + }, () => { + + // wait for new state + this.saveWorkspace(); }); + } + + saveWorkspace = () => { + console.log('App#saveWorkspace'); + + const { + workspace + } = this.props.globals; + + const { + activeTab, + tabs, + layout + } = this.state; + + const config = { + files: [], + activeTab: -1 + }; + + // save tabs + tabs.forEach((tab, index) => { + const { + file + } = tab; + + // do not save unsaved tabs + if (isNew(tab)) { + return; + } + + if (tab === activeTab) { + config.activeTab = index; + } + + config.files.push(assign({}, file)); + }); + + // save layout + config.layout = layout; + + console.log('saving workspace', config); - console.log('App#onLayoutChanged', merge(layout, newLayout)); + workspace.save(config); + } + + restoreWorkspace = () => { + const { + workspace + } = this.props.globals; + + const defaultConfig = { + activeTab: -1, + files: [], + layout: {} + }; - // TODO persist to workspace + return workspace.restore(defaultConfig); } /** @@ -368,6 +420,8 @@ export class App extends Component { [activeTab.id]: true }, tabLoadingState: 'shown' + }, () => { + this.saveWorkspace(); }); } @@ -423,6 +477,8 @@ export class App extends Component { ...dirtyTabs, [tab.id]: false } + }, () => { + this.saveWorkspace(); }); } @@ -456,7 +512,7 @@ export class App extends Component { return LoadingTab; } - componentDidMount() { + async componentDidMount() { const { onReady } = this.props; @@ -464,6 +520,24 @@ export class App extends Component { if (typeof onReady === 'function') { onReady(); } + + const { + activeTab, + files, + layout + } = await this.restoreWorkspace(); + + await this.openFiles(files); + + if (activeTab === -1) { + this.selectTab(this.state.tabs[ this.state.tabs.length - 1 ]); + } else { + this.selectTab(this.state.tabs[ activeTab ]); + } + + this.setState({ + layout: merge(this.state.layout, layout) + }); } componentDidUpdate(prevProps, prevState) { @@ -901,25 +975,4 @@ function isNew(tab) { return tab.file && !tab.file.path; } -/** - - - shouldComponentUpdate(newProps, newState) { - - function compare(type, o, n) { - - Object.keys(o).forEach(function(k) { - if (o[k] !== n[k]) { - console.log('%s[%s] changed', type, k, o[k], n[k]); - } - }); - } - - compare('props', this.props, newProps); - compare('state', this.state, newState); - - return true; - } - - */ export default WithCache(App); \ No newline at end of file diff --git a/client/src/app/AppParent.js b/client/src/app/AppParent.js index 0d49f84c9e..98929b9c4c 100644 --- a/client/src/app/AppParent.js +++ b/client/src/app/AppParent.js @@ -51,15 +51,15 @@ export default class AppParent extends Component { this.getBackend().sendReady(); - setTimeout(() => { - const app = this.getApp(); - - app.createDiagram('bpmn'); - app.createDiagram('bpmn'); - app.createDiagram('dmn'); - app.createDiagram('dmn', { table: true }); - app.createDiagram('cmmn'); - }, 0); + // setTimeout(() => { + // const app = this.getApp(); + + // app.createDiagram('bpmn'); + // app.createDiagram('bpmn'); + // app.createDiagram('dmn'); + // app.createDiagram('dmn', { table: true }); + // app.createDiagram('cmmn'); + // }, 0); } diff --git a/client/src/app/__tests__/AppSpec.js b/client/src/app/__tests__/AppSpec.js index 4bd4d3563c..9c03a3b12c 100644 --- a/client/src/app/__tests__/AppSpec.js +++ b/client/src/app/__tests__/AppSpec.js @@ -14,9 +14,12 @@ import { Backend, Dialog, FileSystem, - TabsProvider + TabsProvider, + Workspace } from './mocks'; +import mitt from 'mitt'; + /* global sinon */ const { spy } = sinon; @@ -40,6 +43,9 @@ describe('', function() { // given const backend = new Backend(); + const eventBus = new mitt(); + const fileSystem = new FileSystem(); + const workspace = new Workspace(); const spy = sinon.spy(backend, 'sendUpdateMenu'); @@ -47,7 +53,10 @@ describe('', function() { app } = createApp({ globals: { - backend + backend, + eventBus, + fileSystem, + workspace } }); @@ -86,7 +95,7 @@ describe('', function() { } = app.state; expect(tabs).to.be.empty; - expect(activeTab).to.equal(EMPTY_TAB); + expect(activeTab).to.equal(-1); }); @@ -253,7 +262,9 @@ describe('', function() { // given const dialog = new Dialog(); + const eventBus = new mitt(); const fileSystem = new FileSystem(); + const workspace = new Workspace(); dialog.setAskSaveResponse(Promise.resolve('save')); @@ -263,7 +274,9 @@ describe('', function() { const rendered = createApp({ globals: { dialog, - fileSystem + eventBus, + fileSystem, + workspace } }, mount); @@ -365,7 +378,9 @@ describe('', function() { // given const dialog = new Dialog(); + const eventBus = mitt(); const fileSystem = new FileSystem(); + const workspace = new Workspace(); dialog.setAskExportAsResponse(Promise.resolve({ fileType: 'svg', @@ -379,7 +394,9 @@ describe('', function() { const rendered = createApp({ globals: { dialog, - fileSystem + eventBus, + fileSystem, + workspace } }, mount); @@ -682,6 +699,174 @@ describe('', function() { }); + + describe('workspace', function() { + + describe('restore workspace', function() { + + let app, + eventBus, + restoreSpy, + tab, + workspace; + + beforeEach(async function() { + tab = new TabsProvider().createTabForFile(createFile('1.bpmn')); + + eventBus = mitt(); + + workspace = new Workspace({ + activeTab: 0, + files: [ tab.file ], + layout: { + minimap: { + open: true + }, + propertiesPanel: { + open: false + } + } + }); + + restoreSpy = spy(workspace, 'restore'); + + const rendered = createApp({ + globals: { + dialog: new Dialog(), + eventBus, + fileSystem: new FileSystem(), + workspace + } + }, mount); + + app = rendered.app; + }); + + + it.skip('should retrieve workspace on mount', function() { + + // then + expect(restoreSpy).to.have.been.called; + + expect(app.state.tabs).to.eql([ tab ]); + + // TODO(fix): layout will be requested in componentDidMount + expect(app.state.layout).to.eql({ + minimap: { + open: true + }, + propertiesPanel: { + open: false + } + }); + }); + + }); + + + describe('save workspace', function() { + + let app, openedTabs, workspace; + + beforeEach(async function() { + workspace = new Workspace(); + + const rendered = createApp({ + globals: { + dialog: new Dialog(), + eventBus: mitt(), + fileSystem: new FileSystem(), + workspace + } + }, mount); + + app = rendered.app; + + const file1 = createFile('1.bpmn'); + const file2 = createFile('2.bpmn'); + + openedTabs = await app.openFiles([ file1, file2 ]); + + // assume + const { + tabs, + activeTab + } = app.state; + + expect(tabs).to.eql(openedTabs); + expect(activeTab).to.eql(openedTabs[1]); + }); + + + it('should save workspace on tab save', async function() { + + // given + const saveSpy = spy(workspace, 'save'); + + // when + await app.saveTab(openedTabs[0]); + + // then + expect(saveSpy).to.have.been.calledWith({ + activeTab: 0, + layout: {}, + files: [{ + name: '1.bpmn', + path: '1.bpmn' + }, { + name: '2.bpmn', + path: '2.bpmn' + }] + }); + }); + + + it('should save workspace on tab select', async function() { + + // given + const saveSpy = spy(workspace, 'save'); + + // when + await app.selectTab(openedTabs[0]); + + // then + expect(saveSpy).to.have.been.calledWith({ + activeTab: 0, + layout: {}, + files: [{ + name: '1.bpmn', + path: '1.bpmn' + }, { + name: '2.bpmn', + path: '2.bpmn' + }] + }); + }); + + + it('should save workspace on tab close', async function() { + + // given + const saveSpy = spy(workspace, 'save'); + + // when + await app.closeTab(openedTabs[1]); + + // then + expect(saveSpy).to.have.been.calledWith({ + activeTab: 0, + layout: {}, + files: [{ + name: '1.bpmn', + path: '1.bpmn' + }] + }); + }); + + }); + + }); + }); }); @@ -705,7 +890,9 @@ function createApp(options = {}, mountFn=shallow) { const globals = options.globals || { dialog: new Dialog(), - fileSystem: new FileSystem() + eventBus: mitt(), + fileSystem: new FileSystem(), + workspace: new Workspace() }; const tabsProvider = options.tabsProvider || new TabsProvider(); diff --git a/client/src/app/__tests__/mocks/index.js b/client/src/app/__tests__/mocks/index.js index c13ac0aedc..53a271991f 100644 --- a/client/src/app/__tests__/mocks/index.js +++ b/client/src/app/__tests__/mocks/index.js @@ -117,4 +117,20 @@ export class FileSystem { export class Backend { sendUpdateMenu() {} +} + +export class Workspace { + constructor(config) { + this.config = config; + } + + save() {} + + restore(defaultConfig) { + return this.config || defaultConfig; + } + + setConfig(config) { + this.config = config; + } } \ No newline at end of file diff --git a/client/src/app/tabs/bpmn/BpmnEditor.js b/client/src/app/tabs/bpmn/BpmnEditor.js index adc92ef215..7f465d83c2 100644 --- a/client/src/app/tabs/bpmn/BpmnEditor.js +++ b/client/src/app/tabs/bpmn/BpmnEditor.js @@ -29,6 +29,8 @@ import classNames from 'classnames'; import { merge } from 'min-dash'; +import defaultLayout from '../defaultLayout'; + const COLORS = [{ title: 'White', fill: 'white', @@ -66,7 +68,7 @@ export class BpmnEditor extends CachedComponent { } = this.props; this.state = { - layout + layout: merge({}, defaultLayout, layout) }; this.ref = React.createRef(); @@ -176,9 +178,6 @@ export class BpmnEditor extends CachedComponent { } handleError = (event) => { - - debugger - const { error } = event; @@ -422,7 +421,7 @@ export class BpmnEditor extends CachedComponent { loading } = this.state; - const propertiesPanelOpen = layout.propertiesPanel && layout.propertiesPanel.open; + const propertiesPanel = layout.propertiesPanel || defaultLayout.propertiesPanel; return (
@@ -532,7 +531,7 @@ export class BpmnEditor extends CachedComponent { onContextMenu={ this.handleContextMenu } >
-
+
Properties Panel
@@ -547,11 +546,13 @@ export class BpmnEditor extends CachedComponent { layout } = props; + const minimap = layout.minimap || defaultLayout.minimap; + // TODO(nikku): wire element template loading const modeler = new CamundaBpmnModeler({ position: 'absolute', minimap: { - open: layout.minimap.open + open: minimap.open } }); diff --git a/client/src/app/tabs/bpmn/__tests__/BpmnEditorSpec.js b/client/src/app/tabs/bpmn/__tests__/BpmnEditorSpec.js index 450406dba0..b897727bdc 100644 --- a/client/src/app/tabs/bpmn/__tests__/BpmnEditorSpec.js +++ b/client/src/app/tabs/bpmn/__tests__/BpmnEditorSpec.js @@ -150,6 +150,55 @@ describe('', function() { expect(bpmnEditor.state.layout.propertiesPanel.open).to.be.false; }); + + it('should handle missing layout', function() { + + // when + const { + bpmnEditor + } = renderBpmnEditor(diagramXML, { + layout: {} + }); + + // then + expect(bpmnEditor.state.layout).to.eql({ + minimap: { + open: false + }, + propertiesPanel: { + open: true + } + }); + }); + + + it('it should apply received layout', function() { + + // when + const { + bpmnEditor + } = renderBpmnEditor(diagramXML, { + layout: { + minimap: { + open: true + }, + propertiesPanel: { + open: false + } + } + }); + + // then + expect(bpmnEditor.state.layout).to.eql({ + minimap: { + open: true + }, + propertiesPanel: { + open: false + } + }); + }); + }); @@ -198,9 +247,6 @@ function renderBpmnEditor(xml, options = {}) { onLayoutChanged } = options; - const minimap = layout && layout.minimap, - propertiesPanel = layout && layout.propertiesPanel; - const slotFillRoot = mount( diff --git a/client/src/app/tabs/cmmn/CmmnEditor.js b/client/src/app/tabs/cmmn/CmmnEditor.js index 9228812f0b..9414c06b54 100644 --- a/client/src/app/tabs/cmmn/CmmnEditor.js +++ b/client/src/app/tabs/cmmn/CmmnEditor.js @@ -20,6 +20,8 @@ import { merge } from 'min-dash'; import classNames from 'classnames'; +import defaultLayout from '../defaultLayout'; + export class CmmnEditor extends CachedComponent { @@ -31,7 +33,7 @@ export class CmmnEditor extends CachedComponent { } = this.props; this.state = { - layout + layout: merge({}, defaultLayout, layout) }; this.ref = React.createRef(); @@ -347,7 +349,7 @@ export class CmmnEditor extends CachedComponent { layout } = this.state; - const propertiesPanelOpen = layout.propertiesPanel && layout.propertiesPanel.open; + const propertiesPanel = layout.propertiesPanel || defaultLayout.propertiesPanel; return (
@@ -359,7 +361,7 @@ export class CmmnEditor extends CachedComponent { onContextMenu={ this.handleContextMenu } >
-
+
Properties Panel
@@ -374,11 +376,13 @@ export class CmmnEditor extends CachedComponent { layout } = props; + const minimap = layout.minimap || defaultLayout.minimap; + // TODO(nikku): wire element template loading const modeler = new CamundaCmmnModeler({ position: 'absolute', minimap: { - open: layout.minimap.open + open: minimap.open } }); diff --git a/client/src/app/tabs/defaultLayout.js b/client/src/app/tabs/defaultLayout.js new file mode 100644 index 0000000000..1f6e64df48 --- /dev/null +++ b/client/src/app/tabs/defaultLayout.js @@ -0,0 +1,8 @@ +export default { + minimap: { + open: false + }, + propertiesPanel: { + open: true + } +}; \ No newline at end of file diff --git a/client/src/app/tabs/dmn/DmnEditor.js b/client/src/app/tabs/dmn/DmnEditor.js index 95fd6821e3..0b6b80b649 100644 --- a/client/src/app/tabs/dmn/DmnEditor.js +++ b/client/src/app/tabs/dmn/DmnEditor.js @@ -31,6 +31,8 @@ import { merge } from 'min-dash'; import classNames from 'classnames'; +import defaultLayout from '../defaultLayout'; + class DmnEditor extends CachedComponent { @@ -42,7 +44,7 @@ class DmnEditor extends CachedComponent { } = this.props; this.state = { - layout + layout: merge({}, defaultLayout, layout) }; this.ref = React.createRef(); @@ -167,7 +169,7 @@ class DmnEditor extends CachedComponent { return this.handleError({ error }); } - if (warnings.length) { + if (warnings && warnings.length) { console.error('imported with warnings', warnings); } @@ -466,7 +468,7 @@ class DmnEditor extends CachedComponent { layout } = this.state; - const propertiesPanelOpen = layout.propertiesPanel && layout.propertiesPanel.open; + const propertiesPanel = layout.propertiesPanel || defaultLayout.propertiesPanel; return (
@@ -485,7 +487,7 @@ class DmnEditor extends CachedComponent {
-
+
Properties Panel
@@ -499,9 +501,11 @@ class DmnEditor extends CachedComponent { layout } = props; + const minimap = layout.minimap || defaultLayout.minimap; + const modeler = new CamundaDmnModeler({ minimap: { - open: layout.minimap.open + open: minimap.open } }); diff --git a/client/src/index.js b/client/src/index.js index 4cd206fc3f..5acbd625b1 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -8,7 +8,8 @@ import mitt from 'mitt'; import { backend, dialog, - fileSystem + fileSystem, + workspace } from './remote'; const eventBus = mitt(); @@ -19,7 +20,8 @@ const globals = { backend, dialog, eventBus, - fileSystem + fileSystem, + workspace }; const rootElement = document.getElementById('root'); diff --git a/client/src/remote/Workspace.js b/client/src/remote/Workspace.js new file mode 100644 index 0000000000..da6bd68271 --- /dev/null +++ b/client/src/remote/Workspace.js @@ -0,0 +1,16 @@ +/** + * Workspace API used by app. + */ +export default class Workspace { + constructor(backend) { + this.backend = backend; + } + + save(config) { + return this.backend.send('workspace:save', config); + } + + restore(defaultConfig) { + return this.backend.send('workspace:restore', defaultConfig); + } +} \ No newline at end of file diff --git a/client/src/remote/index.js b/client/src/remote/index.js index 0a6acea0b5..7c79e9f066 100644 --- a/client/src/remote/index.js +++ b/client/src/remote/index.js @@ -3,6 +3,7 @@ import { electronRequire } from './electron'; import Backend from './Backend'; import Dialog from './Dialog'; import FileSystem from './FileSystem'; +import Workspace from './Workspace'; export const ipcRenderer = electronRequire('ipcRenderer'); @@ -10,4 +11,6 @@ export const backend = new Backend(ipcRenderer); export const fileSystem = new FileSystem(backend); -export const dialog = new Dialog(backend); \ No newline at end of file +export const dialog = new Dialog(backend); + +export const workspace = new Workspace(backend); \ No newline at end of file