From 1a9b20f5214dc7c9e95bd67dea350161197c57f8 Mon Sep 17 00:00:00 2001 From: James Crean Date: Fri, 1 Jun 2018 10:40:20 -0700 Subject: [PATCH] Move gm methods (#307) * Removes graphics method initialization function from backend. Begins creating GM creation modal * Moves template and graphics method state around. Adds a ui model to seperate vcs state from ui state. * Fixes a couple old references to local state. Fixes a typo. * Adds functions to remove GMs. * Adds code to remove temporary Gms and templates on load. Prevents users from making GMs with 2 underscores * Adds more validation to Template creator. Checks if name starts with 2 underscores * Adds toast when a user clicks edit with nothing selected for GMList * Adds tests for GraphicsMethodCreator. Fixes a couple small issues. --- backend/vcdat/GraphicsMethods.py | 19 -- backend/vcdat/app.py | 8 - frontend/package.json | 4 + frontend/src/js/Store.js | 11 +- frontend/src/js/components/GMList.jsx | 148 ++++++++++--- frontend/src/js/components/TemplateList.jsx | 10 +- .../modals/GraphicsMethodCreator.jsx | 198 ++++++++++++++++++ .../js/components/modals/TemplateCreator.jsx | 16 +- frontend/src/js/constants/Actions.js | 22 ++ frontend/src/js/containers/LeftSideBar.jsx | 17 +- frontend/src/js/models/GraphicsMethods.js | 33 ++- frontend/src/js/models/Templates.js | 32 +-- frontend/src/js/models/UI.js | 46 ++++ .../Modals/GraphicsMethodCreatorTest.jsx | 175 ++++++++++++++++ .../components/Modals/TemplateCreatorTest.jsx | 160 +++++++------- 15 files changed, 731 insertions(+), 168 deletions(-) create mode 100644 frontend/src/js/components/modals/GraphicsMethodCreator.jsx create mode 100644 frontend/src/js/models/UI.js create mode 100644 frontend/test/mocha/components/Modals/GraphicsMethodCreatorTest.jsx diff --git a/backend/vcdat/GraphicsMethods.py b/backend/vcdat/GraphicsMethods.py index 280a21b..530b79b 100644 --- a/backend/vcdat/GraphicsMethods.py +++ b/backend/vcdat/GraphicsMethods.py @@ -3,29 +3,10 @@ import numpy _ = vcs.init() -_methods = {} _2d_methods = ('scatter', 'vector', 'xvsy', 'stream', 'glyph', '3d_vector', '3d_dual_scalar') _primitives = ('line', 'marker', 'fillarea', 'text') -def get_gm(): - for t in vcs.graphicsmethodlist(): - _methods[t] = {} - for m in vcs.elements[t].keys(): - gm = vcs.elements[t][m] - _methods[t][m] = vcs.dumpToDict(gm)[0] - if hasattr(gm, "levels"): - arr = numpy.array(gm.levels) - if numpy.allclose(arr, 1e20) and arr.shape[-1] == 2: - _methods[t][m]["levels"] = [1e20, 1e20] - return _methods - -def get_default_gms(): - _defaults = {} - for t in vcs.graphicsmethodlist(): - _defaults[t] = vcs.elements[t].keys() - return _defaults - def detect_nvars(g_type, g_method, g_obj): """Try to return the number of variables required for the plot method. Returns the number of variables required by the plot type. diff --git a/backend/vcdat/app.py b/backend/vcdat/app.py index ac3cf58..9c85b25 100644 --- a/backend/vcdat/app.py +++ b/backend/vcdat/app.py @@ -5,7 +5,6 @@ import cdms2 import json from flask import Flask, send_from_directory, request, send_file, Response, jsonify -from GraphicsMethods import get_gm, get_default_gms from Templates import templ_from_json from Files import getFilesObject from Colormaps import get_cmaps @@ -120,13 +119,6 @@ def plot_template(): return resp -@app.route("/getGraphicsMethods") -@jsonresp -def get_graphics_methods(): - graphics_methods = get_gm() - return json.dumps(graphics_methods) - - @app.route("/getDefaultMethods") @jsonresp def get_default_methods(): diff --git a/frontend/package.json b/frontend/package.json index 88009a8..83de331 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,6 +4,10 @@ "description": "Front-end GUI for CDAT", "main": "src/js/app.js", "private": true, + "prettier": { + "printWidth": 150, + "tabWidth": 4 + }, "dependencies": { "babel-polyfill": "^6.26.0", "bootstrap-slider": "git://github.com/matthewma7/bootstrap-slider", diff --git a/frontend/src/js/Store.js b/frontend/src/js/Store.js index e37ff8f..bcadedc 100644 --- a/frontend/src/js/Store.js +++ b/frontend/src/js/Store.js @@ -6,6 +6,7 @@ import GraphicsMethodModel from './models/GraphicsMethods.js'; import TemplateModel from './models/Templates.js'; import VariableModel from './models/Variables.js'; import SpreadsheetModel from './models/Spreadsheet.js'; +import UIModel from './models/UI.js'; /* global $ */ @@ -16,7 +17,8 @@ const reducers = combineReducers({ graphics_methods: GraphicsMethodModel.reduce, templates: TemplateModel.reduce, sheets_model: SpreadsheetModel.reduce, - colormaps: ColormapModel.reduce + colormaps: ColormapModel.reduce, + ui: UIModel.reduce, }); const undoableReducer = undoable(reducers,{ @@ -31,12 +33,12 @@ var store = null; const configureStore = (initialState = {}) => { let state = Promise.resolve(initialState); if (Object.keys(initialState).length === 0) { - const models = [CachedFileModel, VariableModel, GraphicsMethodModel, TemplateModel, SpreadsheetModel, ColormapModel] + const models = [CachedFileModel, VariableModel, GraphicsMethodModel, TemplateModel, SpreadsheetModel, ColormapModel, UIModel] const initialStates = models.map((m) => { return m.getInitialState(); }); state = Promise.all(initialStates).then((values) => { - const cached_files = values[0], variables = values[1], graphics_methods = values[2], templates = values[3], sheets_model = values[4], colormaps = values[5]; + const cached_files = values[0], variables = values[1], graphics_methods = values[2], templates = values[3], sheets_model = values[4], colormaps = values[5], ui = values[6]; return { present: { cached_files, @@ -44,7 +46,8 @@ const configureStore = (initialState = {}) => { graphics_methods, templates, sheets_model, - colormaps + colormaps, + ui, }, }; }); diff --git a/frontend/src/js/components/GMList.jsx b/frontend/src/js/components/GMList.jsx index 180d19b..baf980f 100644 --- a/frontend/src/js/components/GMList.jsx +++ b/frontend/src/js/components/GMList.jsx @@ -1,6 +1,10 @@ import React, { Component } from 'react' +import { connect } from 'react-redux' +import Actions from '../constants/Actions.js' import PropTypes from 'prop-types' +import Dialog from 'react-bootstrap-dialog' import AddEditRemoveNav from './AddEditRemoveNav/AddEditRemoveNav.jsx' +import GraphicsMethodCreator from './modals/GraphicsMethodCreator.jsx' import GraphicsMethodEditor from './modals/GraphicsMethodEditor.jsx' import Tree from './Tree.jsx' import DragAndDropTypes from '../constants/DragAndDropTypes.js' @@ -29,43 +33,97 @@ class GMList extends Component { constructor(props){ super(props) this.state = { - activeGM: false, - activeGMParent: false, - showModal: false + show_edit_modal: false, + show_create_modal: false, } + this.clickedAdd = this.clickedAdd.bind(this) this.clickedEdit = this.clickedEdit.bind(this) - this.closedModal = this.closedModal.bind(this) + this.confirmRemove = this.confirmRemove.bind(this) + this.removeGM = this.removeGM.bind(this) + this.closeEditModal = this.closeEditModal.bind(this) this.selectedChild = this.selectedChild.bind(this) } + clickedAdd() { + this.setState({show_create_modal: true}) + } + clickedEdit() { - const gm = this.props.graphicsMethods[this.state.activeGMParent][this.state.activeGM] - if (SUPPORTED_GM_EDITORS && !SUPPORTED_GM_EDITORS.includes(gm.g_name)) { + if(!this.props.selected_graphics_type || !this.props.selected_graphics_method) { + toast.info("A Graphics Method must be selected to edit", { position: toast.POSITION.BOTTOM_CENTER }) + return + } + + const gm = this.props.graphics_methods[this.props.selected_graphics_type][this.props.selected_graphics_method] + if(SUPPORTED_GM_EDITORS && !SUPPORTED_GM_EDITORS.includes(gm.g_name)) { toast.warn("This graphics method does not have an editor yet.", { position: toast.POSITION.BOTTOM_CENTER }) } else { - this.setState({showModal: true}) + this.setState({show_edit_modal: true}) + } + } + + confirmRemove() { + const type = this.props.selected_graphics_type + const name = this.props.selected_graphics_method + if( type && name ) { + this.dialog.show({ + body: `Are you sure you want to delete "${name}"?`, + actions: [ + Dialog.DefaultAction( + 'Delete', + () => { + this.removeGM(type, name) + }, + 'btn-danger' + ), + Dialog.CancelAction() + ] + }) + } + else { + toast.info("A Graphics Method must be selected to delete", { position: toast.POSITION.BOTTOM_CENTER }) + } + } + + removeGM(type, name) { + try { + vcs.removegraphicsmethod(type, name).then(() => { + this.props.removeGraphicsMethod(type, name) + }, + (error) => { + console.warn(error) + try { + toast.error(error.data.exception, { position: toast.POSITION.BOTTOM_CENTER }) + } + catch(e){ + toast.error("An error occurred while attempting to delete a graphics method.", { position: toast.POSITION.BOTTOM_CENTER }) + } + }) + } + catch(e){ + console.warn(e) + if(e instanceof ReferenceError) { + toast.error("VCS is not loaded. Try restarting vCDAT", { position: toast.POSITION.BOTTOM_CENTER }) + } } } - closedModal() { - this.setState({showModal: false}) + closeEditModal() { + this.setState({show_edit_modal: false}) } selectedChild(path) { if (path.length === 2) { let gm = path[1] let gm_parent = path[0] - this.setState({ - activeGM: gm, - activeGMParent: gm_parent, - }) + this.props.selectGraphicsMethod(gm_parent, gm) } } render() { - const gmModel = Object.keys(this.props.graphicsMethods).sort().map((gmType) => { - const gms = Object.keys(this.props.graphicsMethods[gmType]).sort().map((gmname) => { + const gmModel = Object.keys(this.props.graphics_methods).sort().map((gmType) => { + const gms = Object.keys(this.props.graphics_methods[gmType]).sort().map((gmname) => { return { 'title': gmname, 'gmType': gmType @@ -82,27 +140,30 @@ class GMList extends Component {
- { - (this.state && this.state.activeGM) ? + {(this.props.selected_graphics_type && + this.props.selected_graphics_method && + this.props.graphics_methods[this.props.selected_graphics_type][this.props.selected_graphics_method]) ? : "" }
+ { this.state.show_create_modal && + {this.setState({show_create_modal: false})}} + graphics_methods={this.props.graphics_methods} + selectGM={this.props.selectGraphicsMethod} + /> + } + {this.dialog = el}} />
) } } GMList.propTypes = { - graphicsMethods: PropTypes.object, - updateGraphicsMethod: PropTypes.func, + graphics_methods: PropTypes.object, colormaps: PropTypes.object, + updateGraphicsMethod: PropTypes.func, + selectGraphicsMethod: PropTypes.func, + removeGraphicsMethod: PropTypes.func, + selected_graphics_method: PropTypes.string, + selected_graphics_type: PropTypes.string, +} + +const mapStateToProps = (state) => { + return { + graphics_methods: state.present.graphics_methods, + selected_graphics_method: state.present.ui.selected_graphics_method, + selected_graphics_type: state.present.ui.selected_graphics_type, + } +} + +const mapDispatchToProps = (dispatch) => { + return { + updateGraphicsMethod: (graphics_method) => { + dispatch(Actions.updateGraphicsMethod(graphics_method)) + }, + selectGraphicsMethod: (type, name) => { + dispatch(Actions.selectGraphicsMethod(type, name)) + }, + removeGraphicsMethod: (type, name) => { + dispatch(Actions.removeGraphicsMethod(type, name)) + } + } } -export default GMList +export default connect(mapStateToProps, mapDispatchToProps)(GMList) diff --git a/frontend/src/js/components/TemplateList.jsx b/frontend/src/js/components/TemplateList.jsx index 01eef43..662cceb 100644 --- a/frontend/src/js/components/TemplateList.jsx +++ b/frontend/src/js/components/TemplateList.jsx @@ -62,7 +62,15 @@ class TemplateList extends Component { else{ this.setState({showTemplateEditor: true, template_data: "loading"}) return vcs.gettemplate(this.props.selected_template).then((data)=>{ // return promise for testing purposes - this.setState({template_data: data}) + if(data) { + this.setState({template_data: data}) + } + else { // data will be null if the selected template doesnt exist + // We can probably do more error handling here. Refresh the names, and deselect the selected template + toast.error(`${this.props.selected_template} doesn't exist on the server. Try refreshing the browser window.`, + { position: toast.POSITION.BOTTOM_CENTER } + ) + } }, (error) => { console.warn(error) diff --git a/frontend/src/js/components/modals/GraphicsMethodCreator.jsx b/frontend/src/js/components/modals/GraphicsMethodCreator.jsx new file mode 100644 index 0000000..093fdc1 --- /dev/null +++ b/frontend/src/js/components/modals/GraphicsMethodCreator.jsx @@ -0,0 +1,198 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { connect } from "react-redux"; +import Actions from "../../constants/Actions.js"; +import { Modal, Button, FormGroup, FormControl, ControlLabel, HelpBlock, Row, Col } from "react-bootstrap"; +import { toast } from "react-toastify"; + +class GraphicsMethodCreator extends Component { + constructor(props) { + super(props); + let default_gm_type = ""; + let default_gm_method = ""; + const gm_types = Object.keys(props.graphics_methods); + + if (gm_types.length > 0) { + default_gm_type = Object.keys(props.graphics_methods)[0]; // just grab the first one each time since there isnt really a default + } + + if (default_gm_type) { + // can't select a graphics method if the types weren't defined + const gm_methods = Object.keys(props.graphics_methods[default_gm_type]); + if (gm_methods.indexOf("default") >= 0) { + default_gm_method = "default"; + } else if (gm_methods.length > 0) { + default_gm_method = gm_methods[0]; + } + } + + this.state = { + new_gm_name: "", + validation_state: null, + selected_gm_type: default_gm_type, + selected_gm_method: default_gm_method, + error_message: "" + }; + + this.createGraphicsMethod = this.createGraphicsMethod.bind(this); + this.handleChange = this.handleChange.bind(this); + this.handleKeyPress = this.handleKeyPress.bind(this) + } + + handleChange(event) { + const name = event.target.value; + const { status: validation_state, message } = this.getValidationState(name, this.state.selected_gm_type); + this.setState({ new_gm_name: name, validation_state: validation_state, error_message: message }); + } + + handleKeyPress(event) { + if(event.charCode === 13 && this.state.validation_state === "success") { + this.createGraphicsMethod() + } + } + + getValidationState(name, gm_type) { + // Checks if the given input is a valid name for a new GM + // returns an object with the following keys: + // status: A string or null that indicates the validity of the input + // message: A string that contains the error message to display when the status is "error" + if (name.length === 0 || gm_type === "") { + return { status: null, message: "" }; + } else if (Object.keys(this.props.graphics_methods[gm_type]).indexOf(name) > -1) { + return { status: "error", message: "A Graphics Method with that name already exists" }; + } else if (name.startsWith("__")) { + return { status: "error", message: "Graphics Method names should not start with two underscores" }; + } + return { status: "success", message: "" }; + } + + createGraphicsMethod() { + try { + return vcs.creategraphicsmethod(this.state.selected_gm_type, this.state.new_gm_name, this.state.selected_gm_method).then( + (/* success */) => { + this.props.createGraphicsMethod(this.state.new_gm_name, this.state.selected_gm_type, this.state.selected_gm_method); + toast.success("Graphics Method created successfully!", { position: toast.POSITION.BOTTOM_CENTER }); + this.props.close(); + }, + /* istanbul ignore next */ + error => { + console.warn("Error while creating Graphics Method: ", error); + try { + toast.error(error.data.exception, { position: toast.POSITION.BOTTOM_CENTER }); + } catch (e) { + toast.error("Failed to create Graphics Method", { position: toast.POSITION.BOTTOM_CENTER }); + } + } + ); + } catch (e) { + /* istanbul ignore next */ + console.warn(e); + /* istanbul ignore next */ + toast.error("An error occurred while creating the Graphics Method. Try restarting vCDAT."); + } + } + + render() { + return ( + + + Create a Graphics Method + + + + + + Base Graphics Type + this.setState({ selected_gm_type: e.target.value })} + > + {Object.keys(this.props.graphics_methods) + .sort(function(a, b) { + return a.toLowerCase().localeCompare(b.toLowerCase()); + }) + .map(name => { + return ( + + ); + })} + + Select a type for your new graphics method + + + + + Base Graphics Method + this.setState({ selected_gm_method: e.target.value })} + > + {this.props.graphics_methods[this.state.selected_gm_type] ? ( + Object.keys(this.props.graphics_methods[this.state.selected_gm_type]) + .sort(function(a, b) { + return a.toLowerCase().localeCompare(b.toLowerCase()); + }) + .map(name => { + return ( + + ); + }) + ) : ( + + Select an existing graphics method to copy and use as a base. + + + + + New Graphics Method Name + + + {this.state.validation_state === "error" ? this.state.error_message : null} + + + + + + + + ); + } +} + +GraphicsMethodCreator.propTypes = { + show: PropTypes.bool, + close: PropTypes.func, + graphics_methods: PropTypes.objectOf(PropTypes.objectOf(PropTypes.object)), + createGraphicsMethod: PropTypes.func, + selectGraphicsMethod: PropTypes.func +}; + +/* istanbul ignore next */ +const mapStateToProps = state => { + return { + gms: state.present.graphics_methods + }; +}; + +/* istanbul ignore next */ +const mapDispatchToProps = dispatch => { + return { + createGraphicsMethod: (name, type, base) => { + dispatch(Actions.createGraphicsMethod(name, type, base)); + } + }; +}; +export { GraphicsMethodCreator as PureGraphicsMethodCreator }; +export default connect(mapStateToProps, mapDispatchToProps)(GraphicsMethodCreator); diff --git a/frontend/src/js/components/modals/TemplateCreator.jsx b/frontend/src/js/components/modals/TemplateCreator.jsx index c33d169..cf8204b 100644 --- a/frontend/src/js/components/modals/TemplateCreator.jsx +++ b/frontend/src/js/components/modals/TemplateCreator.jsx @@ -19,6 +19,7 @@ class TemplateCreator extends Component { this.state = { new_template_name: "", validation_state: null, + error_message: "", selected_base_template: default_base_template } @@ -29,8 +30,8 @@ class TemplateCreator extends Component { handleChange(event) { const name = event.target.value - const validation_state = this.getValidationState(name) - this.setState({new_template_name: name, validation_state: validation_state}) + const {status: validation_state, message} = this.getValidationState(name) + this.setState({new_template_name: name, validation_state: validation_state, error_message: message}) } handleKeyPress(event) { @@ -41,12 +42,15 @@ class TemplateCreator extends Component { getValidationState(name){ if(name.length === 0){ - return null + return {status: null, message: ""} } else if(this.props.templates.indexOf(name) > -1) { - return "error" + return {status: "error", message: "A Template with that name already exists"} } - return "success" + else if(name.startsWith("__")) { + return {status: "error", message: "Template names should not start with two underscores"} + } + return {status: "success", message: ""} } createTemplate() { @@ -111,7 +115,7 @@ class TemplateCreator extends Component { /> - { this.state.validation_state === "error" ? "A template with that name already exists." : null } + { this.state.validation_state === "error" ? this.state.error_message : null } diff --git a/frontend/src/js/constants/Actions.js b/frontend/src/js/constants/Actions.js index 0c4b50c..cfccc43 100644 --- a/frontend/src/js/constants/Actions.js +++ b/frontend/src/js/constants/Actions.js @@ -191,12 +191,34 @@ var Actions = { transforms: transforms, } }, + createGraphicsMethod(name, gm_type, base_method) { + return { + type: "CREATE_GRAPHICS_METHOD", + name: name, + gm_type: gm_type, + base_method: base_method + } + }, + selectGraphicsMethod(type, method) { + return { + type: "SELECT_GRAPHICS_METHOD", + gm_type: type, + method: method + } + }, updateGraphicsMethod(graphics_method) { return { type: 'UPDATE_GRAPHICS_METHOD', graphics_method } }, + removeGraphicsMethod(gm_type, name) { + return { + type: 'REMOVE_GRAPHICS_METHOD', + gm_type: gm_type, + name: name + } + }, initializeColormaps(colormaps) { return { type: 'INITIALIZE_COLORMAPS', diff --git a/frontend/src/js/containers/LeftSideBar.jsx b/frontend/src/js/containers/LeftSideBar.jsx index a67af8d..0a153cf 100644 --- a/frontend/src/js/containers/LeftSideBar.jsx +++ b/frontend/src/js/containers/LeftSideBar.jsx @@ -19,10 +19,13 @@ class LeftSideBar extends Component { selectVariable={this.props.selectVariable} selected_variable={this.props.selected_variable} /> - { return { variables: state.present.variables, - graphics_methods: state.present.graphics_methods, - templates: state.present.templates.names, - selected_template: state.present.templates.selected_template, + templates: state.present.templates, + selected_template: state.present.ui.selected_template, cached_files: state.present.cached_files, sheets_model: state.present.sheets_model, colormaps: state.present.colormaps, @@ -82,9 +83,7 @@ const mapDispatchToProps = (dispatch) => { // error handling here. No variable selected when delete was pressed } }, - updateGraphicsMethod: (graphics_method) => { - dispatch(Actions.updateGraphicsMethod(graphics_method)) - }, + selectTemplate: (name) => dispatch(Actions.selectTemplate(name)), updateTemplate: (template) => dispatch(Actions.updateTemplate(template)), removeTemplate: (name) => dispatch(Actions.removeTemplate(name)), diff --git a/frontend/src/js/models/GraphicsMethods.js b/frontend/src/js/models/GraphicsMethods.js index ac90175..8fb649e 100644 --- a/frontend/src/js/models/GraphicsMethods.js +++ b/frontend/src/js/models/GraphicsMethods.js @@ -42,6 +42,20 @@ class GraphicsMethodModel extends BaseModel { break; } return new_graphics_methods; + case "CREATE_GRAPHICS_METHOD": + new_graphics_methods = $.extend(true, {}, state) + new_graphics_methods[action.gm_type][action.name] = $.extend(true, {}, new_graphics_methods[action.gm_type][action.base_method]) + new_graphics_methods[action.gm_type][action.name].name = action.name + return new_graphics_methods + case "REMOVE_GRAPHICS_METHOD": + new_graphics_methods = $.extend(true, {}, state) + try { + delete new_graphics_methods[action.gm_type][action.name] + } + catch(e) { + console.warn(e) + } + return new_graphics_methods case "DELETE_COLORMAP": new_graphics_methods = Object.assign({}, state) for(let type of Object.keys(new_graphics_methods)){ @@ -58,7 +72,24 @@ class GraphicsMethodModel extends BaseModel { } static getInitialState() { - return $.get("getGraphicsMethods"); + try { + return vcs.getallgraphicsmethods().then((methods) => { + // search through each gm type and check if any names start with "__" + // If any are found, they are removed from being displayed since they are temporary names + for(let type of Object.keys(methods)){ + for(let name of Object.keys(methods[type])){ + if(name.startsWith("__")){ + delete methods[type][name] + } + } + } + return methods + }) + } + catch(e){ + console.warn(e) + return {} + } } } diff --git a/frontend/src/js/models/Templates.js b/frontend/src/js/models/Templates.js index 2ae47e9..f54a851 100644 --- a/frontend/src/js/models/Templates.js +++ b/frontend/src/js/models/Templates.js @@ -1,5 +1,6 @@ -import BaseModel from './BaseModel.js'; +import BaseModel from './BaseModel.js' import { toast } from 'react-toastify' +import $ from 'jquery' class TemplateModel extends BaseModel { static reduce(state={}, action) { @@ -7,29 +8,21 @@ class TemplateModel extends BaseModel { switch (action.type) { case 'INITIALIZE_TEMPLATE_VALUES': return action.templates; - case 'SELECT_TEMPLATE': - new_state = $.extend(true, {}, state); - new_state.selected_template = action.selected_template - return new_state case 'CREATE_TEMPLATE': - new_state = $.extend(true, {}, state); - for(let i=0; i < new_state.names.length; i++){ - if(new_state.names[i].toLocaleLowerCase() > action.name.toLocaleLowerCase()){ - new_state.names.splice(i, 0, action.name) // inserts name into alphabetical index - new_state.selected_template = action.name + new_state = $.extend(true, [], state); + for(let i=0; i < new_state.length; i++){ + if(new_state[i].toLocaleLowerCase() > action.name.toLocaleLowerCase()){ + new_state.splice(i, 0, action.name) // inserts name into alphabetical index break } } return new_state case 'REMOVE_TEMPLATE': - new_state = $.extend(true, {}, state); - new_state.names.splice(new_state.names.indexOf(action.name), 1) - if(new_state.names.indexOf(new_state.selected_template) === -1){ - new_state.selected_template = "" - } + new_state = $.extend(true, [], state); + new_state.splice(new_state.indexOf(action.name), 1) return new_state case 'UPDATE_TEMPLATE': - new_state = $.extend(true, {}, state); + new_state = $.extend(true, [], state); new_state[action.template.name] = action.template; return new_state; default: @@ -40,10 +33,7 @@ class TemplateModel extends BaseModel { static getInitialState() { try{ return vcs.getalltemplatenames().then((names) => { - return { - names: names, - selected_template: "" - } + return names.filter((name) => {return !name.startsWith("__")}) // ["ASD", "default"] etc. filter out temp names like "__boxfill_12345" }) } catch(e){ @@ -54,7 +44,7 @@ class TemplateModel extends BaseModel { else{ console.warn(e) } - return {names: [], selected_template: ""} + return [] } } diff --git a/frontend/src/js/models/UI.js b/frontend/src/js/models/UI.js new file mode 100644 index 0000000..3d1e0fe --- /dev/null +++ b/frontend/src/js/models/UI.js @@ -0,0 +1,46 @@ +import BaseModel from './BaseModel.js'; +import $ from 'jquery' + +class UIModel extends BaseModel { + static reduce(state={}, action) { + let new_state; + switch (action.type) { + case 'CREATE_TEMPLATE': + new_state = $.extend(true, {}, state); + new_state.selected_template = action.name + return new_state + case 'SELECT_TEMPLATE': + new_state = $.extend(true, {}, state); + new_state.selected_template = action.selected_template + return new_state + case 'REMOVE_TEMPLATE': + new_state = $.extend(true, {}, state); + if(new_state.selected_template === action.name){ + new_state.selected_template = "" + } + return new_state + case "CREATE_GRAPHICS_METHOD": + new_state = $.extend(true, {}, state); + new_state.selected_graphics_method = action.name + new_state.selected_graphics_type= action.gm_type + return new_state + case "SELECT_GRAPHICS_METHOD": + new_state = $.extend(true, {}, state); + new_state.selected_graphics_method = action.method + new_state.selected_graphics_type= action.gm_type + return new_state + default: + return state; + } + } + + static getInitialState() { + return { + selected_template: "", + selected_graphics_type: "", + selected_graphics_method: "", + } + } +} + +export default UIModel diff --git a/frontend/test/mocha/components/Modals/GraphicsMethodCreatorTest.jsx b/frontend/test/mocha/components/Modals/GraphicsMethodCreatorTest.jsx new file mode 100644 index 0000000..22016ea --- /dev/null +++ b/frontend/test/mocha/components/Modals/GraphicsMethodCreatorTest.jsx @@ -0,0 +1,175 @@ +/* globals it, describe, before, beforeEach, */ +let chai = require("chai"); +let expect = chai.expect; +let React = require("react"); + +import { PureGraphicsMethodCreator as GraphicsMethodCreator } from "../../../../src/js/components/modals/GraphicsMethodCreator.jsx"; +import Enzyme from "enzyme"; +import Adapter from "enzyme-adapter-react-16"; +Enzyme.configure({ adapter: new Adapter() }); +import { shallow } from "enzyme"; +import sinon from "sinon"; + +const getProps = function() { + return { + show: true, + close: sinon.spy(), + graphics_methods: { + boxfill: { + default: { name: "default" }, + polar: { name: "polar" }, + quick: { name: "quick" } + }, + isofill: { + default: { name: "default" }, + polar: { name: "polar" }, + quick: { name: "quick" } + } + }, + createGraphicsMethod: sinon.spy(), + selectGraphicsMethod: sinon.spy() + }; +}; + +describe("GraphicsMethodCreatorTest.jsx", function() { + it("renders without exploding", () => { + let props = getProps(); + let gm_creator = shallow(); + expect(gm_creator).to.have.lengthOf(1); + }); + + it("Sets default gm type and method when default is present", () => { + let props = getProps(); + let gm_creator = shallow(); + expect(gm_creator.state().selected_gm_type).to.equal("boxfill"); + expect(gm_creator.state().selected_gm_method).to.equal("default"); + }); + + it("Sets default gm type and method when default is not present", () => { + let props = getProps(); + props.graphics_methods = { + boxfill: { + polar: { name: "polar" }, + quick: { name: "quick" } + }, + isofill: { + polar: { name: "polar" }, + quick: { name: "quick" } + } + }; + let gm_creator = shallow(); + expect(gm_creator.state().selected_gm_type).to.equal("boxfill"); + expect(gm_creator.state().selected_gm_method).to.equal("polar"); + }); + + it("getValidationState works", () => { + let props = getProps(); + let gm_creator = shallow(); + + expect(gm_creator.instance().getValidationState("", "")).to.deep.equal({ status: null, message: "" }); // returns null if no name length + expect(gm_creator.instance().getValidationState("default", "boxfill")).to.deep.equal({ + status: "error", + message: "A Graphics Method with that name already exists" + }); + expect(gm_creator.instance().getValidationState("polar", "boxfill")).to.deep.equal({ + status: "error", + message: "A Graphics Method with that name already exists" + }); + expect(gm_creator.instance().getValidationState("quick", "boxfill")).to.deep.equal({ + status: "error", + message: "A Graphics Method with that name already exists" + }); + expect(gm_creator.instance().getValidationState("default", "isofill")).to.deep.equal({ + status: "error", + message: "A Graphics Method with that name already exists" + }); + expect(gm_creator.instance().getValidationState("polar", "isofill")).to.deep.equal({ + status: "error", + message: "A Graphics Method with that name already exists" + }); + expect(gm_creator.instance().getValidationState("quick", "isofill")).to.deep.equal({ + status: "error", + message: "A Graphics Method with that name already exists" + }); + expect(gm_creator.instance().getValidationState("__temp", "boxfill")).to.deep.equal({ + status: "error", + message: "Graphics Method names should not start with two underscores" + }); + expect(gm_creator.instance().getValidationState("test", "boxfill")).to.deep.equal({ status: "success", message: "" }); + expect(gm_creator.instance().getValidationState("valid", "boxfill")).to.deep.equal({ status: "success", message: "" }); + }); + + it("handleChange function sets state", () => { + let props = getProps(); + let gm_creator = shallow(); + const event_with_null_validation = { + target: { + value: "" + } + }; + const event_with_error_validation = { + target: { + value: "default" + } + }; + const event_with_success_validation = { + target: { + value: "valid" + } + }; + gm_creator.setState({ selected_gm_type: "boxfill" }); + gm_creator.instance().handleChange(event_with_null_validation); + expect(gm_creator.state().new_gm_name).to.equal(""); + expect(gm_creator.state().validation_state).to.equal(null); + + gm_creator.instance().handleChange(event_with_error_validation); + expect(gm_creator.state().new_gm_name).to.equal("default"); + expect(gm_creator.state().validation_state).to.equal("error"); + + gm_creator.instance().handleChange(event_with_success_validation); + expect(gm_creator.state().new_gm_name).to.equal("valid"); + expect(gm_creator.state().validation_state).to.equal("success"); + }); + + it("createGraphicsMethod works", () => { + global.vcs = { + creategraphicsmethod: sinon.stub().resolves() + }; + const state = { + selected_gm_type: "boxfill", + new_gm_name: "test_name", + selected_gm_method: "default" + }; + let props = getProps(); + props.createGraphicsMethod = sinon.spy(); + let gm_creator = shallow(); + gm_creator.setState(state); + return gm_creator + .instance() + .createGraphicsMethod() + .then(() => { + expect(vcs.creategraphicsmethod.callCount).to.equal(1); + expect(vcs.creategraphicsmethod.getCall(0).args[0]).to.equal(state.selected_gm_type); + expect(vcs.creategraphicsmethod.getCall(0).args[1]).to.equal(state.new_gm_name); + expect(vcs.creategraphicsmethod.getCall(0).args[2]).to.equal(state.selected_gm_method); + expect(props.close.callCount).to.equal(1); + }); + }); + + it("handleKeyPress only causes a save when valid", () => { + let props = getProps() + let gm_creator = shallow() + let stub = sinon.stub(gm_creator.instance(), "createGraphicsMethod").callsFake(() => {}); + gm_creator.setState({ validation_state: "error" }); + gm_creator.instance().handleKeyPress({ charCode: 100 }); + expect(stub.callCount).to.equal(0); // should not be called with keys besides enter + gm_creator.instance().handleKeyPress({ charCode: 13 }); // 13 is the 'enter' key + expect(stub.callCount).to.equal(0); // should not be called with inwvalid name + + gm_creator.setState({ validation_state: "success" }); + gm_creator.instance().handleKeyPress({ charCode: 100 }); + expect(stub.callCount).to.equal(0); // should not be called with keys besides enter + gm_creator.instance().handleKeyPress({ charCode: 13 }); + expect(stub.callCount).to.equal(1); // should be called now that validation is 'success' + }); +}); diff --git a/frontend/test/mocha/components/Modals/TemplateCreatorTest.jsx b/frontend/test/mocha/components/Modals/TemplateCreatorTest.jsx index f228547..1917bc7 100644 --- a/frontend/test/mocha/components/Modals/TemplateCreatorTest.jsx +++ b/frontend/test/mocha/components/Modals/TemplateCreatorTest.jsx @@ -1,127 +1,141 @@ /* globals it, describe, before, beforeEach, */ -var chai = require('chai') +var chai = require("chai"); var expect = chai.expect; -var React = require('react') +var React = require("react"); -import TemplateCreator from '../../../../src/js/components/modals/TemplateCreator.jsx' -import Enzyme from 'enzyme' -import Adapter from 'enzyme-adapter-react-16' -Enzyme.configure({ adapter: new Adapter() }) -import { shallow, mount } from 'enzyme' -import sinon from 'sinon' -import { createMockStore } from 'redux-test-utils' +import TemplateCreator from "../../../../src/js/components/modals/TemplateCreator.jsx"; +import Enzyme from "enzyme"; +import Adapter from "enzyme-adapter-react-16"; +Enzyme.configure({ adapter: new Adapter() }); +import { shallow } from "enzyme"; +import sinon from "sinon"; +import { createMockStore } from "redux-test-utils"; -const getProps = function(){ +const getProps = function() { return { show: true, close: sinon.spy(), templates: ["ASD", "default", "quick"], createTemplate: sinon.spy(), store: createMockStore({}) - } -} + }; +}; -describe('TemplateCreatorTest.jsx', function() { - it('renders without exploding', () => { - let props = getProps() - var template_creator = shallow() - expect(template_creator).to.have.lengthOf(1) +describe("TemplateCreatorTest.jsx", function() { + it("renders without exploding", () => { + let props = getProps(); + var template_creator = shallow(); + expect(template_creator).to.have.lengthOf(1); }); it('Sets default base template if no template named "default" exists', () => { - let props = getProps() - props.templates = ["ASD", "quick"] - var template_creator = shallow().dive() - expect(template_creator.state().selected_base_template).to.equal("ASD") + let props = getProps(); + props.templates = ["ASD", "quick"]; + var template_creator = shallow().dive(); + expect(template_creator.state().selected_base_template).to.equal("ASD"); }); - - it('getValidationState works', () => { - let props = getProps() - var template_creator = shallow().dive() + it("getValidationState works", () => { + let props = getProps(); + var template_creator = shallow().dive(); - expect(template_creator.instance().getValidationState("")).to.equal(null) // returns null if no name length - expect(template_creator.instance().getValidationState("ASD")).to.equal("error") - expect(template_creator.instance().getValidationState("default")).to.equal("error") - expect(template_creator.instance().getValidationState("quick")).to.equal("error") - expect(template_creator.instance().getValidationState("test")).to.equal("success") - expect(template_creator.instance().getValidationState("valid")).to.equal("success") + expect(template_creator.instance().getValidationState("")).to.deep.equal({ status: null, message: "" }); // returns null if no name length + expect(template_creator.instance().getValidationState("ASD")).to.deep.equal({ + status: "error", + message: "A Template with that name already exists" + }); + expect(template_creator.instance().getValidationState("default")).to.deep.equal({ + status: "error", + message: "A Template with that name already exists" + }); + expect(template_creator.instance().getValidationState("quick")).to.deep.equal({ + status: "error", + message: "A Template with that name already exists" + }); + expect(template_creator.instance().getValidationState("__temp")).to.deep.equal({ + status: "error", + message: "Template names should not start with two underscores" + }); + expect(template_creator.instance().getValidationState("test")).to.deep.equal({ status: "success", message: "" }); + expect(template_creator.instance().getValidationState("valid")).to.deep.equal({ status: "success", message: "" }); }); - it('handleChange function works', () => { - let props = getProps() - var template_creator = shallow().dive() + it("handleChange function works", () => { + let props = getProps(); + var template_creator = shallow().dive(); const event_with_null_validation = { target: { value: "" } - } + }; const event_with_error_validation = { target: { value: "ASD" } - } + }; const event_with_success_validation = { target: { value: "valid" } - } + }; - template_creator.instance().handleChange(event_with_null_validation) - expect(template_creator.state().new_template_name).to.equal("") - expect(template_creator.state().validation_state).to.equal(null) + template_creator.instance().handleChange(event_with_null_validation); + expect(template_creator.state().new_template_name).to.equal(""); + expect(template_creator.state().validation_state).to.equal(null); - template_creator.instance().handleChange(event_with_error_validation) - expect(template_creator.state().new_template_name).to.equal("ASD") - expect(template_creator.state().validation_state).to.equal("error") + template_creator.instance().handleChange(event_with_error_validation); + expect(template_creator.state().new_template_name).to.equal("ASD"); + expect(template_creator.state().validation_state).to.equal("error"); - template_creator.instance().handleChange(event_with_success_validation) - expect(template_creator.state().new_template_name).to.equal("valid") - expect(template_creator.state().validation_state).to.equal("success") + template_creator.instance().handleChange(event_with_success_validation); + expect(template_creator.state().new_template_name).to.equal("valid"); + expect(template_creator.state().validation_state).to.equal("success"); }); - it('createTemplate works', () => { + it("createTemplate works", () => { global.vcs = { createtemplate: sinon.stub().resolves() - } + }; let props = { show: true, close: sinon.spy(), templates: ["ASD", "default", "quick"], createTemplate: sinon.spy(), store: createMockStore({}) - } - var template_creator = shallow().dive() - template_creator.setState({new_template_name: "test_name"}) - return template_creator.instance().createTemplate().then(() => { - expect(vcs.createtemplate.callCount).to.equal(1) - expect(vcs.createtemplate.getCall(0).args[0]).to.equal("test_name") - expect(vcs.createtemplate.getCall(0).args[1]).to.equal("default") - expect(props.close.callCount).to.equal(1) - }) + }; + var template_creator = shallow().dive(); + template_creator.setState({ new_template_name: "test_name" }); + return template_creator + .instance() + .createTemplate() + .then(() => { + expect(vcs.createtemplate.callCount).to.equal(1); + expect(vcs.createtemplate.getCall(0).args[0]).to.equal("test_name"); + expect(vcs.createtemplate.getCall(0).args[1]).to.equal("default"); + expect(props.close.callCount).to.equal(1); + }); }); - it('handleKeyPress only causes a save when valid', () => { + it("handleKeyPress only causes a save when valid", () => { let props = { show: true, close: sinon.spy(), templates: ["ASD", "default", "quick"], createTemplate: sinon.spy(), store: createMockStore({}) - } - var template_creator = shallow().dive() // fuuuuuu - let stub = sinon.stub(template_creator.instance(), 'createTemplate').callsFake(() => {}) - template_creator.instance().forceUpdate() - template_creator.setState({validation_state: "error"}) - template_creator.instance().handleKeyPress({charCode: 13}) // 13 is the 'enter' key - expect(stub.callCount).to.equal(0) // should not be called with invalid name - template_creator.instance().handleKeyPress({charCode: 100}) - expect(stub.callCount).to.equal(0) // should not be called with keys besides enter + }; + var template_creator = shallow().dive(); + let stub = sinon.stub(template_creator.instance(), "createTemplate").callsFake(() => {}); + template_creator.setState({ validation_state: "error" }); + template_creator.instance().handleKeyPress({ charCode: 100 }); + expect(stub.callCount).to.equal(0); // should not be called with keys besides enter + template_creator.instance().handleKeyPress({ charCode: 13 }); // 13 is the 'enter' key + expect(stub.callCount).to.equal(0); // should not be called with invalid name - template_creator.setState({validation_state: "success"}) - template_creator.instance().handleKeyPress({charCode: 100}) - expect(stub.callCount).to.equal(0) // should not be called with keys besides enter - template_creator.instance().handleKeyPress({charCode: 13}) - expect(stub.callCount).to.equal(1) // should be called now that validation is 'success' + template_creator.setState({ validation_state: "success" }); + template_creator.instance().handleKeyPress({ charCode: 100 }); + expect(stub.callCount).to.equal(0); // should not be called with keys besides enter + template_creator.instance().handleKeyPress({ charCode: 13 }); + expect(stub.callCount).to.equal(1); // should be called now that validation is 'success' }); -}); \ No newline at end of file +});