From 7331bf517b7c61df9d02ff40cdcb726b7e8f2e35 Mon Sep 17 00:00:00 2001 From: Sonja Happ Date: Mon, 4 Nov 2019 15:20:54 +0100 Subject: [PATCH] cherry picked all changes from the cleanup-widgets branch and merged them to the new file/ folder structure and package versions; most likely, further fixes are required to make things work. Improves #209 and #205 --- package-lock.json | 37 +- package.json | 2 +- src/dashboard/dashboard-button-group.js | 96 +++++ src/dashboard/dashboard.js | 494 +++++++++--------------- src/dashboard/widget-area.js | 78 ++++ src/dashboard/widget-context-menu.js | 123 ++++++ src/dashboard/widget-toolbox.js | 77 ++++ src/widget/editable-widget-container.js | 142 +++++++ src/widget/widget-container.js | 47 +++ src/widget/widget.js | 217 +++-------- 10 files changed, 823 insertions(+), 490 deletions(-) create mode 100644 src/dashboard/dashboard-button-group.js create mode 100644 src/dashboard/widget-area.js create mode 100644 src/dashboard/widget-context-menu.js create mode 100644 src/dashboard/widget-toolbox.js create mode 100644 src/widget/editable-widget-container.js create mode 100644 src/widget/widget-container.js diff --git a/package-lock.json b/package-lock.json index dfcc0c4..807685b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5153,11 +5153,18 @@ } }, "eslint-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.0.tgz", - "integrity": "sha512-7ehnzPaP5IIEh1r1tkjuIrxqhNkzUJa9z3R92tLJdZIVdWaczEhr3EbhGtsMrVxi1KeR8qA7Off6SWc5WNQqyQ==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", "requires": { - "eslint-visitor-keys": "^1.0.0" + "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", + "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==" + } } }, "eslint-visitor-keys": { @@ -6048,9 +6055,9 @@ "integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ==" }, "handlebars": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", - "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.1.tgz", + "integrity": "sha512-C29UoFzHe9yM61lOsIlCE5/mQVGrnIOrOq7maQl76L7tYPCgC1og0Ajt6uWnX4ZTxBPnjw+CUvawphwCfJgUnA==", "requires": { "neo-async": "^2.6.0", "optimist": "^0.6.1", @@ -12841,13 +12848,21 @@ "integrity": "sha512-8OaIKfzL5cpx8eCMAhhvTlft8GYF8b2eQr6JkCyVdrgjcytyOmPCXrqXFcUnhonRpLlh5yxEZVohm6mzaowUOw==" }, "uglify-js": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", - "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==", + "version": "3.6.7", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.7.tgz", + "integrity": "sha512-4sXQDzmdnoXiO+xvmTzQsfIiwrjUCSA95rSP4SEd8tDb51W2TiDOlL76Hl+Kw0Ie42PSItCW8/t6pBNCF2R48A==", "optional": true, "requires": { - "commander": "~2.20.0", + "commander": "~2.20.3", "source-map": "~0.6.1" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "optional": true + } } }, "uncontrollable": { diff --git a/package.json b/package.json index 16782d4..9ce2493 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "flux": "^3.1.3", "frontend-collective-react-dnd-scrollzone": "^1.0.2", "gaugeJS": "^1.3.7", - "handlebars": "^4.1.2", + "handlebars": "^4.5.1", "immutable": "^4.0.0-rc.12", "jquery": "^3.4.1", "jszip": "^3.2.2", diff --git a/src/dashboard/dashboard-button-group.js b/src/dashboard/dashboard-button-group.js new file mode 100644 index 0000000..785ac48 --- /dev/null +++ b/src/dashboard/dashboard-button-group.js @@ -0,0 +1,96 @@ +/** + * File: dashboard-button-group.js + * Author: Markus Grigull + * Date: 31.05.2018 + * + * This file is part of VILLASweb. + * + * VILLASweb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VILLASweb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with VILLASweb. If not, see . + ******************************************************************************/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button } from 'react-bootstrap'; + +class DashboardButtonGroup extends React.Component { + render() { + const buttonStyle = { + marginLeft: '8px' + }; + + const buttons = []; + let key = 0; + + if (this.props.fullscreen) { + return null; + } + + if (this.props.editing) { + buttons.push( + , + + ); + } else { + if (this.props.fullscreen !== true) { + buttons.push( + + ); + } + + if (this.props.paused) { + buttons.push( + + ); + } else { + buttons.push( + + ); + } + + buttons.push( + + ); + } + + return
+ {buttons} +
; + } +} + +DashboardButtonGroup.propTypes = { + editing: PropTypes.bool, + fullscreen: PropTypes.bool, + paused: PropTypes.bool, + onEdit: PropTypes.func, + onSave: PropTypes.func, + onCancel: PropTypes.func, + onFullscreen: PropTypes.func, + onPause: PropTypes.func, + onUnpause: PropTypes.func +}; + +export default DashboardButtonGroup; diff --git a/src/dashboard/dashboard.js b/src/dashboard/dashboard.js index 1683bd3..80b5406 100644 --- a/src/dashboard/dashboard.js +++ b/src/dashboard/dashboard.js @@ -21,19 +21,18 @@ import React from 'react'; import { Container } from 'flux/utils'; -import { Button, ButtonToolbar } from 'react-bootstrap'; -import { ContextMenu, Item, Separator } from 'react-contexify'; import Fullscreenable from 'react-fullscreenable'; -import Slider from 'rc-slider'; import classNames from 'classnames'; +import { Map } from 'immutable' -import Icon from '../common/icon'; -import WidgetFactory from '../widget/widget-factory'; -import ToolboxItem from './toolbox-item'; -import Dropzone from './dropzone'; +//import Icon from '../common/icon'; import Widget from '../widget/widget'; import EditWidget from '../widget/edit-widget'; -import Grid from './grid'; + +import WidgetContextMenu from './widget-context-menu'; +import WidgetToolbox from './widget-toolbox'; +import WidgetArea from './widget-area'; +import DashboardButtonGroup from './dashboard-button-group'; import UserStore from '../user/user-store'; import DashboardStore from './dashboard-store'; @@ -42,12 +41,12 @@ import SimulationStore from '../simulation/simulation-store'; import SimulationModelStore from '../simulationmodel/simulation-model-store'; import FileStore from '../file/file-store'; import AppDispatcher from '../common/app-dispatcher'; -import NotificationsDataManager from '../common/data-managers/notifications-data-manager'; -import NotificationsFactory from '../common/data-managers/notifications-factory'; import 'react-contexify/dist/ReactContexify.min.css'; class Dashboard extends React.Component { + + static lastWidgetKey = 0; static getStores() { return [ DashboardStore, ProjectStore, SimulationStore, SimulationModelStore, FileStore, UserStore ]; } @@ -57,19 +56,46 @@ class Dashboard extends React.Component { prevState = {}; } + let dashboard = Map(); + let rawDashboard = DashboardStore.getState().find(v => v._id === props.match.params.dashboard); + + if (rawDashboard != null) { + dashboard = Map(rawDashboard); + + // convert widgets list to a dictionary to be able to reference widgets + const widgets = {}; + + for (let widget of dashboard.get('widgets')) { + widgets[this.getNewWidgetKey()] = widget; + } + + dashboard = dashboard.set('widgets', widgets); + + // this.computeHeightWithWidgets(widgets); + + // this.setState({ dashboard: selectedDashboards, project: null }); + + // AppDispatcher.dispatch({ + // type: 'projects/start-load', + // data: selectedDashboard.get('project'), + // token: this.state.sessionToken + // }); + + } + let simulationModels = []; if (prevState.simulation != null) { simulationModels = SimulationModelStore.getState().filter(m => prevState.simulation.models.includes(m._id)); } return { + dashboard, + sessionToken: UserStore.getState().token, - dashboards: DashboardStore.getState(), projects: ProjectStore.getState(), simulations: SimulationStore.getState(), files: FileStore.getState(), - dashboard: prevState.dashboard || {}, project: prevState.project || null, simulation: prevState.simulation || null, simulationModels, @@ -81,20 +107,30 @@ class Dashboard extends React.Component { modalIndex: prevState.modalIndex || null, maxWidgetHeight: prevState.maxWidgetHeight || 0, - dropZoneHeight: prevState.dropZoneHeight || 0 + dropZoneHeight: prevState.dropZoneHeight || 0, }; + } - componentWillMount() { - // TODO: Don't fetch token from local, use user-store! - const token = localStorage.getItem('token'); + static getNewWidgetKey() { + const widgetKey = this.lastWidgetKey; + this.lastWidgetKey++; + return widgetKey; + } + + + componentWillMount() { //document.addEventListener('keydown', this.handleKeydown.bind(this)); - AppDispatcher.dispatch({ - type: 'dashboards/start-load', - token - }); + if (this.state.dashboard.has('id') === false) { + AppDispatcher.dispatch({ + type: 'dashboards/start-load', + data: this.props.match.params.dashboard, + token: this.state.sessionToken + }); + } + } componentWillUnmount() { @@ -149,12 +185,24 @@ class Dashboard extends React.Component { } }*/ - transformToWidgetsDict(widgets) { - var widgetsDict = {}; - // Create a new key and make a copy of the widget object - var key = 0; - widgets.forEach( (widget) => widgetsDict[key++] = Object.assign({}, widget) ); - return widgetsDict; + /* + * Adapt the area's height with the position of the new widget. + * Return true if the height increased, otherwise false. + */ + increaseHeightWithWidget(widget) { + let increased = false; + let thisWidgetHeight = widget.y + widget.height; + + if (thisWidgetHeight > this.state.maxWidgetHeight) { + increased = true; + + this.setState({ + maxWidgetHeight: thisWidgetHeight, + dropZoneHeight: thisWidgetHeight + 40 + }); + } + + return increased; } transformToWidgetsList(widgets) { @@ -186,64 +234,40 @@ class Dashboard extends React.Component { }); } - snapToGrid(value) { - if (this.state.dashboard.grid === 1) return value; + handleDrop = widget => { + const widgets = this.state.dashboard.get('widgets') || []; - return Math.round(value / this.state.dashboard.grid) * this.state.dashboard.grid; - } + const widgetKey = this.getNewWidgetKey(); + widgets[widgetKey] = widget; - handleDrop(item, position) { + const dashboard = this.state.dashboard.set('widgets'); - let widget = null; - let defaultSimulationModel = null; + // this.increaseHeightWithWidget(widget); - if (this.state.simulation.models && this.state.simulation.models.length === 0) { - NotificationsDataManager.addNotification(NotificationsFactory.NO_SIM_MODEL_AVAILABLE); - } else { - defaultSimulationModel = this.state.simulation.models[0]; - } + this.setState({ dashboard }); + }; - // snap position to grid - position.x = this.snapToGrid(position.x); - position.y = this.snapToGrid(position.y); - - // create new widget - widget = WidgetFactory.createWidgetOfType(item.name, position, defaultSimulationModel); - - var new_widgets = this.state.dashboard.widgets; - var new_key = Object.keys(new_widgets).length; - - new_widgets[new_key] = widget; - - var dashboard = Object.assign({}, this.state.dashboard, { - widgets: new_widgets - }); - - this.increaseHeightWithWidget(widget); - this.setState({ dashboard: dashboard }); - } widgetStatusChange(updated_widget, key) { // Widget changed internally, make changes effective then save them this.widgetChange(updated_widget, key, this.saveChanges); } - widgetChange(updated_widget, key, callback = null) { - var widgets_update = {}; - widgets_update[key] = updated_widget; - var new_widgets = Object.assign({}, this.state.dashboard.widgets, widgets_update); + widgetChange = (widget, index, callback = null) => { + const widgets = this.state.dashboard.get('widgets'); + widgets[index] = widget; - var dashboard = Object.assign({}, this.state.dashboard, { - widgets: new_widgets - }); + const dashboard = this.state.dashboard.set('widgets'); // Check if the height needs to be increased, the section may have shrunk if not - if (!this.increaseHeightWithWidget(updated_widget)) { + if (!this.increaseHeightWithWidget(widget)) { this.computeHeightWithWidgets(dashboard.widgets); } - this.setState({ dashboard: dashboard }, callback); + + this.setState({ dashboard }, callback); } + /* * Set the initial height state based on the existing widgets */ @@ -261,289 +285,137 @@ class Dashboard extends React.Component { dropZoneHeight: maxHeight + 80 }); } - /* - * Adapt the area's height with the position of the new widget. - * Return true if the height increased, otherwise false. - */ - increaseHeightWithWidget(widget) { - let increased = false; - let thisWidgetHeight = widget.y + widget.height; - if (thisWidgetHeight > this.state.maxWidgetHeight) { - increased = true; - this.setState({ - maxWidgetHeight: thisWidgetHeight, - dropZoneHeight: thisWidgetHeight + 40 - }); - } - return increased; + + + editWidget = (widget, index) => { + this.setState({ editModal: true, modalData: widget, modalIndex: index }); } - editWidget(e, data) { - this.setState({ editModal: true, modalData: this.state.dashboard.widgets[data.key], modalIndex: data.key }); - } - closeEdit(data) { - if (data) { - // save changes temporarily - var widgets_update = {}; - widgets_update[this.state.modalIndex] = data; - - var new_widgets = Object.assign({}, this.state.dashboard.widgets, widgets_update); - - var dashboard = Object.assign({}, this.state.dashboard, { - widgets: new_widgets - }); - - this.setState({ editModal: false, dashboard: dashboard }); - } else { + closeEdit = data => { + if (data == null) { this.setState({ editModal: false }); + + return; } - } - deleteWidget(e, data) { - delete this.state.dashboard.widgets[data.key]; - var dashboard = Object.assign({}, this.state.dashboard, { - widgets: this.state.dashboard.widgets - }); - this.setState({ dashboard: dashboard }); - } + const widgets = this.state.dashboard.get('widgets'); + widgets[this.state.modalIndex] = data; - stopEditing() { + const dashboard = this.state.dashboard.set('widgets', widgets); + + this.setState({ editModal: false, dashboard }); + }; + + + deleteWidget = (widget, index) => { + const widgets = this.state.dashboard.get('widgets'); + delete widgets[index]; + + const dashboard = this.state.dashboard.set('widgets'); + + this.setState({ dashboard }); + }; + + + startEditing = () => { + this.setState({ editing: true }); + }; + + saveEditing = () => { // Provide the callback so it can be called when state change is applied + // TODO: Check if callback is needed this.setState({ editing: false }, this.saveChanges ); - } + }; saveChanges() { // Transform to a list - var dashboard = Object.assign({}, this.state.dashboard, { - widgets: this.transformToWidgetsList(this.state.dashboard.widgets) + const dashboard = Object.assign({}, this.state.dashboard.toJS(), { + widgets: this.transformToWidgetsList(this.state.dashboard.get('widgets')) }); - const token = localStorage.getItem('token'); - AppDispatcher.dispatch({ type: 'dashboards/start-edit', data: dashboard, - token + token: this.state.sessionToken }); } - discardChanges() { - this.setState({ editing: false, dashboard: {} }); + cancelEditing = () => { + this.setState({ editing: false, dasboard: {} }); this.reloadDashboard(); - } + }; - moveWidget(e, data, applyDirection) { - var widget = this.state.dashboard.widgets[data.key]; - var updated_widgets = {}; - updated_widgets[data.key] = applyDirection(widget); - var new_widgets = Object.assign({}, this.state.dashboard.widgets, updated_widgets); - - var dashboard = Object.assign({}, this.state.dashboard, { - widgets: new_widgets - }); - - this.setState({ dashboard: dashboard }); - } - - moveAbove(widget) { - // increase z-Order - widget.z++; - return widget; - } - - moveToFront(widget) { - // increase z-Order - widget.z = 100; - return widget; - } - - moveUnderneath(widget) { - // decrease z-Order - widget.z--; - if (widget.z < 0) { - widget.z = 0; - } - return widget; - } - - moveToBack(widget) { - // increase z-Order - widget.z = 0; - return widget; - } - - setGrid(value) { - // value 0 would block all widgets, set 1 as 'grid disabled' - if (value === 0) { - value = 1; - } - - let dashboard = Object.assign({}, this.state.dashboard, { - grid: value - }); + setGrid = value => { + const dashboard = this.state.dashboard.set('grid', value); this.setState({ dashboard }); - } - - lockWidget(data) { - // lock the widget - let widget = this.state.dashboard.widgets[data.key]; - widget.locked = true; - - // update dashboard - let widgets = {}; - widgets[data.key] = widget; - widgets = Object.assign({}, this.state.dashboard.widgets, widgets); - - const dashboard = Object.assign({}, this.state.dashboard, { widgets }); - this.setState({ dashboard }); - } - - unlockWidget(data) { - // lock the widget - let widget = this.state.dashboard.widgets[data.key]; - widget.locked = false; - - // update dashboard - let widgets = {}; - widgets[data.key] = widget; - widgets = Object.assign({}, this.state.dashboard.widgets, widgets); - - const dashboard = Object.assign({}, this.state.dashboard, { widgets }); - this.setState({ dashboard }); - } + }; pauseData = () => { this.setState({ paused: true }); - } + }; unpauseData = () => { this.setState({ paused: false }); - } + }; + render() { - const current_widgets = this.state.dashboard.widgets; + const widgets = this.state.dashboard.get('widgets'); + const grid = this.state.dashboard.get('grid'); - let boxClasses = classNames('section', 'box', { 'fullscreen-container': this.props.isFullscreen }); + const boxClasses = classNames('section', 'box', { 'fullscreen-padding': this.props.isFullscreen }); - let buttons = [] - let editingControls = []; - let gridControl = {}; - - if (this.state.editing) { - buttons.push({ click: () => this.stopEditing(), icon: 'save', text: 'Save' }); - buttons.push({ click: () => this.discardChanges(), icon: 'ban', text: 'Cancel' }); - - gridControl =
- Grid: {this.state.dashboard.grid > 1 ? this.state.dashboard.grid : 'Disabled'} - this.setGrid(value)} /> -
- } - - if (!this.props.isFullscreen) { - buttons.push({ click: this.props.toggleFullscreen, icon: 'expand', text: 'Fullscreen' }); - buttons.push({ click: this.state.paused ? this.unpauseData : this.pauseData, icon: this.state.paused ? 'play' : 'pause', text: this.state.paused ? 'Live' : 'Pause' }); - - if (!this.state.editing) - buttons.push({ click: () => this.setState({ editing: true }), icon: 'edit', text: 'Edit' }); - } - - const buttonList = buttons.map((btn, idx) => - - ); - - // Only one topology widget at the time is supported - let thereIsTopologyWidget = current_widgets && Object.values(current_widgets).filter( widget => widget.type === 'Topology').length > 0; - let topologyItemMsg = !thereIsTopologyWidget? '' : 'Currently only one is supported'; - - return ( -
-
-
- {this.state.dashboard.name} -
- -
- { this.state.editing && gridControl } - { buttonList } -
+ return
+
+
+ {this.state.dashboard.get('name')}
-
e.preventDefault() }> - {this.state.editing && -
- - { editingControls } - - - - - - - - - - - - - - - - - - - -
- } - - this.handleDrop(item, position)} editing={this.state.editing}> - {current_widgets != null && - Object.keys(current_widgets).map(widget_key => ( - this.widgetChange(w, k)} - onWidgetStatusChange={(w, k) => this.widgetStatusChange(w, k)} - editing={this.state.editing} - index={widget_key} - grid={this.state.dashboard.grid} - paused={this.state.paused} - /> - ))} - - - - - {current_widgets != null && - Object.keys(current_widgets).map(widget_key => { - const data = { key: widget_key }; - - const locked = this.state.dashboard.widgets[widget_key].locked; - const disabledMove = locked || this.state.dashboard.widgets[widget_key].type === 'Box'; - - return - this.editWidget(e, data)}>Edit - this.deleteWidget(e, data)}>Delete - - this.moveWidget(e, data, this.moveAbove)}>Move above - this.moveWidget(e, data, this.moveToFront)}>Move to front - this.moveWidget(e, data, this.moveUnderneath)}>Move underneath - this.moveWidget(e, data, this.moveToBack)}>Move to back - - this.lockWidget(data)}>Lock - this.unlockWidget(data)}>Unlock - - })} - - this.closeEdit(data)} widget={this.state.modalData} simulationModels={this.state.simulationModels} files={this.state.files} /> -
+
- ); + +
e.preventDefault() }> + {this.state.editing && + + } + + + {widgets != null && Object.keys(widgets).map(widgetKey => ( + this.widgetChange(w, k)} + onWidgetStatusChange={(w, k) => this.widgetStatusChange(w, k)} + editing={this.state.editing} + index={widgetKey} + grid={grid} + paused={this.state.paused} + /> + ))} + + + {/* TODO: Create only one context menu for all widgets */} + {widgets != null && Object.keys(widgets).map(widgetKey => ( + + ))} + + +
+
; } } diff --git a/src/dashboard/widget-area.js b/src/dashboard/widget-area.js new file mode 100644 index 0000000..cc08afc --- /dev/null +++ b/src/dashboard/widget-area.js @@ -0,0 +1,78 @@ +/** + * File: widget-area.js + * Author: Markus Grigull + * Date: 31.05.2018 + * + * This file is part of VILLASweb. + * + * VILLASweb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VILLASweb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with VILLASweb. If not, see . + ******************************************************************************/ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import Dropzone from './dropzone'; +import Grid from './grid'; + +import WidgetFactory from '../widget/widget-factory'; + +class WidgetArea extends React.Component { + snapToGrid(value) { + if (this.props.grid === 1) { + return value; + } + + return Math.round(value / this.props.grid) * this.props.grid; + } + + handleDrop = (item, position) => { + position.x = this.snapToGrid(position.x); + position.y = this.snapToGrid(position.y); + + const widget = WidgetFactory.createWidgetOfType(item.name, position, this.props.defaultSimulationModel); + + if (this.props.onWidgetAdded != null) { + this.props.onWidgetAdded(widget); + } + } + + render() { + const maxHeight = Object.values(this.props.widgets).reduce((currentHeight, widget) => { + const absolutHeight = widget.y + widget.height; + + return absolutHeight > currentHeight ? absolutHeight : currentHeight; + }, 0); + + return + {this.props.children} + + + ; + } +} + +WidgetArea.propTypes = { + children: PropTypes.node, //TODO is .node correct here? Was .children before leading to compile error + editing: PropTypes.bool, + grid: PropTypes.number, + defaultSimulationModel: PropTypes.string, + widgets: PropTypes.object, + onWidgetAdded: PropTypes.func +}; + +WidgetArea.defaultProps = { + widgets: {} +}; + +export default WidgetArea; diff --git a/src/dashboard/widget-context-menu.js b/src/dashboard/widget-context-menu.js new file mode 100644 index 0000000..69e0683 --- /dev/null +++ b/src/dashboard/widget-context-menu.js @@ -0,0 +1,123 @@ +/** + * File: widget-context-menu.js + * Author: Markus Grigull + * Date: 31.05.2018 + * + * This file is part of VILLASweb. + * + * VILLASweb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VILLASweb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with VILLASweb. If not, see . + ******************************************************************************/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { contextMenu, Item, Separator } from 'react-contexify'; + +class WidgetContextMenu extends React.Component { + editWidget = event => { + if (this.props.onEdit != null) { + this.props.onEdit(this.props.widget, this.props.index); + } + }; + + deleteWidget = event => { + if (this.props.onDelete != null) { + this.props.onDelete(this.props.widget, this.props.index); + } + }; + + moveAbove = event => { + this.props.widget.z++; + if (this.props.widget.z > 100) { + this.props.widget.z = 100; + } + + if (this.props.onChange != null) { + this.props.onChange(this.props.widget, this.props.index); + } + }; + + moveToFront = event => { + this.props.widget.z = 100; + + if (this.props.onChange != null) { + this.props.onChange(this.props.widget, this.props.index); + } + }; + + moveUnderneath = event => { + this.props.widget.z--; + if (this.props.widget.z < 0) { + this.props.widget.z = 0; + } + + if (this.props.onChange != null) { + this.props.onChange(this.props.widget, this.props.index); + } + }; + + moveToBack = event => { + this.props.widget.z = 0; + + if (this.props.onChange != null) { + this.props.onChange(this.props.widget, this.props.index); + } + }; + + lockWidget = event => { + this.props.widget.locked = true; + + if (this.props.onChange != null) { + this.props.onChange(this.props.widget, this.props.index); + } + }; + + unlockWidget = event => { + this.props.widget.locked = false; + + if (this.props.onChange != null) { + this.props.onChange(this.props.widget, this.props.index); + } + }; + + render() { + const isLocked = this.props.widget.locked; + + return + Edit + Delete + + + + Move above + Move to front + Move underneath + Move to back + + + + Lock + Unlock + ; + } +} + +WidgetContextMenu.propTypes = { + index: PropTypes.number.isRequired, + widget: PropTypes.object.isRequired, + onEdit: PropTypes.func, + onDelete: PropTypes.func, + onChange: PropTypes.func +}; + +export default WidgetContextMenu diff --git a/src/dashboard/widget-toolbox.js b/src/dashboard/widget-toolbox.js new file mode 100644 index 0000000..9799ff5 --- /dev/null +++ b/src/dashboard/widget-toolbox.js @@ -0,0 +1,77 @@ +/** + * File: widget-toolbox.js + * Author: Markus Grigull + * Date: 31.05.2018 + * + * This file is part of VILLASweb. + * + * VILLASweb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VILLASweb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with VILLASweb. If not, see . + ******************************************************************************/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import Slider from 'rc-slider'; + +import ToolboxItem from './toolbox-item'; + +class WidgetToolbox extends React.Component { + onGridChange = value => { + // value 0 would block all widgets, set 1 as 'grid disabled' + if (value === 0) { + value = 1; + } + + if (this.props.onGridChange != null) { + this.props.onGridChange(value); + } + }; + + render() { + // Only one topology widget at the time is supported + const thereIsTopologyWidget = this.props.widgets != null && Object.values(this.props.widgets).filter(w => w.type === 'Topology').length > 0; + const topologyItemMsg = thereIsTopologyWidget? 'Currently only one is supported' : ''; + + return
+ + + + + + + + + + + + + + + +
+
+ Grid: { this.props.grid > 1 ? this.props.grid : 'Disabled' } + +
+
+
; + }; +} + +WidgetToolbox.propTypes = { + widgets: PropTypes.array, + grid: PropTypes.number, + onGridChange: PropTypes.func +}; + +export default WidgetToolbox; diff --git a/src/widget/editable-widget-container.js b/src/widget/editable-widget-container.js new file mode 100644 index 0000000..db5e828 --- /dev/null +++ b/src/widget/editable-widget-container.js @@ -0,0 +1,142 @@ +/** + * File: editable-widget-container.js + * Author: Markus Grigull + * Date: 31.05.2018 + * + * This file is part of VILLASweb. + * + * VILLASweb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VILLASweb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with VILLASweb. If not, see . + ******************************************************************************/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { Rnd } from 'react-rnd'; +import { contextMenu } from 'react-contexify'; + +class EditableWidgetContainer extends React.Component { + constructor(props) { + super(props); + + this.rnd = null; + } + + snapToGrid(value) { + if (this.props.grid === 1) { + return value; + } + + return Math.round(value / this.props.grid) * this.props.grid; + } + + borderWasClicked = event => { + if (event.button !== 2) { + return; + } + }; + + drag = (event, data) => { + const x = this.snapToGrid(data.x); + const y = this.snapToGrid(data.y); + + if (x !== data.x || y !== data.y) { + this.rnd.updatePosition({ x, y }); + } + }; + + dragStop = (event, data) => { + const widget = this.props.widget; + + widget.x = this.snapToGrid(data.x); + widget.y = this.snapToGrid(data.y); + + if (this.props.onWidgetChange != null) { + this.props.onWidgetChange(widget, this.props.index); + } + }; + + resizeStop = (direction, delta, ref, event) => { + const widget = this.props.widget; + + // resize depends on direction + if (direction === 'left' || direction === 'topLeft' || direction === 'bottomLeft') { + widget.x -= delta.width; + } + + if (direction === 'top' || direction === 'topLeft' || direction === 'topRight') { + widget.y -= delta.height; + } + + widget.width += delta.width; + widget.height += delta.height; + + if (this.props.onWidgetChange != null) { + this.props.onWidgetChange(widget, this.props.index); + } + }; + + render() { + const widget = this.props.widget; + + const resizing = { + bottom: !widget.locked, + bottomLeft: !widget.locked, + bottomRight: !widget.locked, + left: !widget.locked, + right: !widget.locked, + top: !widget.locked, + topLeft: !widget.locked, + topRight: !widget.locked + }; + + const gridArray = [ this.props.grid, this.props.grid ]; + + const widgetClasses = classNames({ + 'editing-widget': true, + 'locked': widget.locked + }); + + return { this.rnd = c; }} + default={{ x: Number(widget.x), y: Number(widget.y), width: widget.width, height: widget.height }} + minWidth={widget.minWidth} + minHeight={widget.minHeight} + lockAspectRatio={Boolean(widget.lockAspect)} + bounds={'parent'} + className={widgetClasses} + onResizeStart={this.borderWasClicked} + onResizeStop={this.resizeStop} + onDrag={this.drag} + onDragStop={this.dragStop} + dragGrid={gridArray} + resizeGrid={gridArray} + zIndex={widget.z} + enableResizing={resizing} + disableDragging={widget.locked} + > + + {this.props.children} + + ; + } +} + +EditableWidgetContainer.propTypes = { + widget: PropTypes.object.isRequired, + index: PropTypes.number.isRequired, + grid: PropTypes.number, + onWidgetChange: PropTypes.func +}; + +export default EditableWidgetContainer diff --git a/src/widget/widget-container.js b/src/widget/widget-container.js new file mode 100644 index 0000000..4aa62a5 --- /dev/null +++ b/src/widget/widget-container.js @@ -0,0 +1,47 @@ +/** + * File: widget-container.js + * Author: Markus Grigull + * Date: 31.05.2018 + * + * This file is part of VILLASweb. + * + * VILLASweb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VILLASweb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with VILLASweb. If not, see . + ******************************************************************************/ + +import React from 'react'; +import PropTypes from 'prop-types'; + +class WidgetContainer extends React.Component { + render() { + const containerStyle = { + width: Number(this.props.widget.width), + height: Number(this.props.widget.height), + left: Number(this.props.widget.x), + top: Number(this.props.widget.y), + zIndex: Number(this.props.widget.z), + position: 'absolute' + }; + + return
+ {this.props.children} +
; + } +} + +WidgetContainer.propTypes = { + widget: PropTypes.object.isRequired, + children: PropTypes.node, //TODO is .node correct here? Was .children before leading to compile error +}; + +export default WidgetContainer diff --git a/src/widget/widget.js b/src/widget/widget.js index cb9944e..4513cf7 100644 --- a/src/widget/widget.js +++ b/src/widget/widget.js @@ -21,9 +21,6 @@ import React from 'react'; import { Container } from 'flux/utils'; -import { ContextMenuProvider } from 'react-contexify'; -import { Rnd } from 'react-rnd'; -import classNames from 'classnames'; import AppDispatcher from '../common/app-dispatcher'; import UserStore from '../user/user-store'; @@ -31,6 +28,9 @@ import SimulatorDataStore from '../simulator/simulator-data-store'; import SimulationModelStore from '../simulationmodel/simulation-model-store'; import FileStore from '../file/file-store'; +import EditableWidgetContainer from './editable-widget-container'; +import WidgetContainer from './widget-container'; + import WidgetCustomAction from './widgets/custom-action'; import WidgetAction from './widgets/action'; import WidgetLamp from './widgets/lamp'; @@ -56,8 +56,6 @@ class Widget extends React.Component { } static calculateState(prevState, props) { - const sessionToken = UserStore.getState().token; - let simulatorData = {}; if (props.paused) { @@ -68,101 +66,32 @@ class Widget extends React.Component { simulatorData = SimulatorDataStore.getState(); } - if (prevState) { - return { - sessionToken, - simulatorData, - files: FileStore.getState(), - sequence: prevState.sequence + 1, + return { + simulatorData, + files: FileStore.getState(), + simulationModels: SimulationModelStore.getState(), - simulationModels: SimulationModelStore.getState() - }; - } else { - return { - sessionToken, - simulatorData, - files: FileStore.getState(), - sequence: 0, + sequence: prevState != null ? prevState.sequence + 1 : 0, - simulationModels: SimulationModelStore.getState() - }; - } - } - - constructor(props) { - super(props); - - // Reference to the context menu element - this.contextMenuTriggerViaDraggable = null; + sessionToken: UserStore.getState().token + }; } componentWillMount() { - // If loading for the first time - if (this.state.sessionToken) { - AppDispatcher.dispatch({ - type: 'files/start-load', - token: this.state.sessionToken - }); - - AppDispatcher.dispatch({ - type: 'simulationModels/start-load', - token: this.state.sessionToken - }); - } - } - - snapToGrid(value) { - if (this.props.grid === 1) - return value; - - return Math.round(value / this.props.grid) * this.props.grid; - } - - drag(event, data) { - const x = this.snapToGrid(data.x); - const y = this.snapToGrid(data.y); - - if (x !== data.x || y !== data.y) { - this.rnd.updatePosition({ x, y }); - } - } - - dragStop(event, data) { - // update widget - let widget = this.props.data; - widget.x = this.snapToGrid(data.x); - widget.y = this.snapToGrid(data.y); - - this.props.onWidgetChange(widget, this.props.index); - } - - resizeStop(direction, delta, event) { - // update widget - let widget = Object.assign({}, this.props.data); - - // resize depends on direction - if (direction === 'left' || direction === 'topLeft' || direction === 'bottomLeft') { - widget.x -= delta.width; + if (this.state.sessionToken == null) { + return; } - if (direction === 'top' || direction === 'topLeft' || direction === 'topRight') { - widget.y -= delta.height; - } + AppDispatcher.dispatch({ + type: 'files/start-load', + token: this.state.sessionToken + }); - widget.width += delta.width; - widget.height += delta.height; + AppDispatcher.dispatch({ + type: 'simulationModels/start-load', + token: this.state.sessionToken + }); - this.props.onWidgetChange(widget, this.props.index); - } - - borderWasClicked(e) { - // check if it was triggered by the right button - if (e.button === 2) { - // launch the context menu using the reference - if(this.contextMenuTriggerViaDraggable) { - this.contextMenuTriggerViaDraggable.handleContextClick(e); - } - } } inputDataChanged(widget, data) { @@ -184,16 +113,7 @@ class Widget extends React.Component { }); } - render() { - // configure grid - const grid = [this.props.grid, this.props.grid]; - - // get widget element - const widget = this.props.data; - let borderedWidget = false; - let element = null; - let zIndex = Number(widget.z); - + createWidget(widget) { let simulationModel = null; for (let model of this.state.simulationModels) { @@ -204,93 +124,56 @@ class Widget extends React.Component { simulationModel = model; } - // dummy is passed to widgets to keep updating them while in edit mode if (widget.type === 'CustomAction') { - element = + return } else if (widget.type === 'Action') { - element = + return } else if (widget.type === 'Lamp') { - element = + return } else if (widget.type === 'Value') { - element = + return } else if (widget.type === 'Plot') { - element = + return } else if (widget.type === 'Table') { - element = + return } else if (widget.type === 'Label') { - element = + return } else if (widget.type === 'PlotTable') { - element = this.props.onWidgetStatusChange(w, this.props.index)} paused={this.props.paused} /> + return this.props.onWidgetStatusChange(w, this.props.index)} paused={this.props.paused} /> } else if (widget.type === 'Image') { - element = + return } else if (widget.type === 'Button') { - element = this.inputDataChanged(widget, value)} /> - } else if (widget.type === 'Input') { - element = this.inputDataChanged(widget, value)} /> + return this.inputDataChanged(widget, value)} /> + } else if (widget.type === 'NumberInput') { + return this.inputDataChanged(widget, value)} /> } else if (widget.type === 'Slider') { - element = this.inputDataChanged(widget, value)} onWidgetChange={(w) => this.props.onWidgetStatusChange(w, this.props.index) } /> + return this.props.onWidgetStatusChange(w, this.props.index) } onInputChanged={value => this.inputDataChanged(widget, value)} /> } else if (widget.type === 'Gauge') { - element = + return } else if (widget.type === 'Box') { - element = + return } else if (widget.type === 'HTML') { - element = + return } else if (widget.type === 'Topology') { - element = + return } - if (widget.type === 'Box') - zIndex = 0; + return null; + } - const widgetClasses = classNames({ - 'widget': !this.props.editing, - 'editing-widget': this.props.editing, - 'border': borderedWidget, - 'unselectable': false, - 'locked': widget.locked && this.props.editing - }); + + render() { + const element = this.createWidget(this.props.data); if (this.props.editing) { - const resizing = { bottom: !widget.locked, bottomLeft: !widget.locked, bottomRight: !widget.locked, left: !widget.locked, right: !widget.locked, top: !widget.locked, topLeft: !widget.locked, topRight: !widget.locked}; - - return ( - { this.rnd = c; }} - default={{ x: Number(widget.x), y: Number(widget.y), width: widget.width, height: widget.height }} - minWidth={widget.minWidth} - minHeight={widget.minHeight} - lockAspectRatio={Boolean(widget.lockAspect)} - bounds={'parent'} - className={ widgetClasses } - onResizeStart={(event, direction, ref) => this.borderWasClicked(event)} - onResizeStop={(event, direction, ref, delta) => this.resizeStop(direction, delta, event)} - onDrag={(event, data) => this.drag(event, data)} - onDragStop={(event, data) => this.dragStop(event, data)} - dragGrid={grid} - resizeGrid={grid} - z={zIndex} - enableResizing={resizing} - disableDragging={widget.locked} - > - - {element} - - - ); - } else { - return ( -
- {element} -
- ); + return + {element} + ; } + + return + {element} + ; } }