From 6e7b171996aee3d331355efccd337759431e193a Mon Sep 17 00:00:00 2001 From: Mauro Bartolomeoli Date: Tue, 7 Feb 2017 14:59:23 +0100 Subject: [PATCH] Fixes #1421, live coding of a plugin in the plugins example --- package.json | 1 + .../components/data/template/jsx/Template.jsx | 12 ++- web/client/examples/plugins/actions/config.js | 17 +++- web/client/examples/plugins/app.jsx | 70 +++++++++++++-- .../plugins/components/PluginCreator.jsx | 89 +++++++++++++++++++ web/client/examples/plugins/context.js | 3 + web/client/examples/plugins/plugins/My.jsx | 41 --------- .../examples/plugins/reducers/config.js | 5 +- web/client/examples/plugins/sample.js.raw | 36 ++++++++ web/client/examples/plugins/store.js | 5 +- 10 files changed, 225 insertions(+), 54 deletions(-) create mode 100644 web/client/examples/plugins/components/PluginCreator.jsx create mode 100644 web/client/examples/plugins/context.js delete mode 100644 web/client/examples/plugins/plugins/My.jsx create mode 100644 web/client/examples/plugins/sample.js.raw diff --git a/package.json b/package.json index fd14f94d5c..9bc8a9e184 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "mocha": "2.4.5", "ncp": "2.0.0", "parallelshell": "1.2.0", + "raw-loader": "0.5.1", "react-addons-css-transition-group": "0.14.8", "react-addons-test-utils": "0.14.8", "react-hot-loader": "1.3.0", diff --git a/web/client/components/data/template/jsx/Template.jsx b/web/client/components/data/template/jsx/Template.jsx index 6255bc0553..2b81f7066d 100644 --- a/web/client/components/data/template/jsx/Template.jsx +++ b/web/client/components/data/template/jsx/Template.jsx @@ -13,12 +13,14 @@ const Template = React.createClass({ propTypes: { template: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.func]), model: React.PropTypes.object, - renderContent: React.PropTypes.func + renderContent: React.PropTypes.func, + onError: React.PropTypes.func }, getDefaultProps() { return { template: "", - model: {} + model: {}, + onError: () => {} }; }, componentWillMount() { @@ -45,7 +47,11 @@ const Template = React.createClass({ }, parseTemplate(temp) { let template = (typeof temp === 'function') ? temp() : temp; - this.comp = Babel.transform(template, { presets: ['es2015', 'react', 'stage-0'] }).code; + try { + this.comp = Babel.transform(template, { presets: ['es2015', 'react', 'stage-0'] }).code; + } catch(e) { + this.props.onError(e.message); + } } }); diff --git a/web/client/examples/plugins/actions/config.js b/web/client/examples/plugins/actions/config.js index be489c344c..9fabd6381d 100644 --- a/web/client/examples/plugins/actions/config.js +++ b/web/client/examples/plugins/actions/config.js @@ -6,6 +6,7 @@ * LICENSE file in the root directory of this source tree. */ const SAVE_PLUGIN_CONFIG = 'SAVE_PLUGIN_CONFIG'; +const COMPILE_ERROR = 'COMPILE_ERROR'; function savePluginConfig(plugin, cfg) { return { @@ -15,4 +16,18 @@ function savePluginConfig(plugin, cfg) { }; } -module.exports = {SAVE_PLUGIN_CONFIG, savePluginConfig}; +function compileError(message) { + return { + type: COMPILE_ERROR, + message + }; +} + +function resetError() { + return { + type: COMPILE_ERROR, + message: null + }; +} + +module.exports = {SAVE_PLUGIN_CONFIG, COMPILE_ERROR, savePluginConfig, compileError, resetError}; diff --git a/web/client/examples/plugins/app.jsx b/web/client/examples/plugins/app.jsx index bf5dd0f819..4bf9a905b9 100644 --- a/web/client/examples/plugins/app.jsx +++ b/web/client/examples/plugins/app.jsx @@ -25,6 +25,9 @@ const startApp = () => { const {plugins} = require('./plugins'); + let userPlugin; + const Template = require('../../components/data/template/jsx/Template'); + let pluginsCfg = { standard: ['Map', 'Toolbar'] }; @@ -38,12 +41,33 @@ const startApp = () => { const SaveAndLoad = require('./components/SaveAndLoad'); const Debug = require('../../components/development/Debug'); - const store = require('./store')(plugins); - const {savePluginConfig} = require('./actions/config'); + const assign = require('object-assign'); + const codeSample = require("raw!./sample.js.raw"); + + let customReducers; + + const context = require('./context'); + + const customReducer = (state={}, action) => { + if (customReducers) { + const newState = assign({}, state); + Object.keys(customReducers).forEach((stateKey) => { + assign(newState, {[stateKey]: customReducers[stateKey](state[stateKey], action)}); + }); + return newState; + } + return state; + }; + + const store = require('./store')(plugins, customReducer); + + const {savePluginConfig, compileError, resetError} = require('./actions/config'); require('./assets/css/plugins.css'); + const Babel = require('babel-standalone'); + let mapType = 'leaflet'; const Localized = connect((state) => ({ @@ -71,8 +95,39 @@ const startApp = () => { callback(); }; + const customPlugin = (callback, code) => { + /*eslint-disable */ + const require = context; + try { + customReducers = eval(Babel.transform(code, { presets: ['es2015', 'react', 'stage-0'] }).code).reducers || null; + + /*eslint-enable */ + userPlugin = connect(() => ({ + template: code, + renderContent: (comp) => { + /*eslint-disable */ + return eval(comp).Plugin; + /*eslint-enable */ + }, + getReducers() { + return this.comp; + } + }), { + onError: compileError + })(Template); + store.dispatch(resetError()); + callback(); + } catch(e) { + store.dispatch(compileError(e.message)); + } + }; + const PluginConfigurator = require('./components/PluginConfigurator'); + const PluginCreator = connect((state) => ({ + error: state.pluginsConfig && state.pluginsConfig.error + }))(require('./components/PluginCreator')); + const renderPlugins = (callback) => { return Object.keys(plugins).map((plugin) => { const pluginName = plugin.substring(0, plugin.length - 6); @@ -93,12 +148,10 @@ const startApp = () => { name: plugin, hide: isHidden(plugin), cfg: userCfg[plugin + 'Plugin'] || {} - })) + })).concat(userPlugin ? ['My'] : []) }; }; - const assign = require('object-assign'); - const changeMapType = (callback, e) => { mapType = e.target.options[e.target.selectedIndex].value; callback(); @@ -131,6 +184,10 @@ const startApp = () => { } }; + const getPlugins = () => { + return assign({}, plugins, userPlugin ? {MyPlugin: {MyPlugin: userPlugin}} : {}); + }; + const renderPage = () => { ReactDOM.render( ( @@ -146,11 +203,12 @@ const startApp = () => {
- +
diff --git a/web/client/examples/plugins/components/PluginCreator.jsx b/web/client/examples/plugins/components/PluginCreator.jsx new file mode 100644 index 0000000000..f26ab92583 --- /dev/null +++ b/web/client/examples/plugins/components/PluginCreator.jsx @@ -0,0 +1,89 @@ +/** + * Copyright 2016, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const React = require('react'); + +const {Button, Glyphicon, Modal, Input} = require('react-bootstrap'); + +const Codemirror = require('react-codemirror'); + + +require('codemirror/lib/codemirror.css'); + +require('codemirror/mode/javascript/javascript'); + +const PluginCreator = React.createClass({ + propTypes: { + pluginCode: React.PropTypes.string, + error: React.PropTypes.string, + onApplyCode: React.PropTypes.func + }, + getDefaultProps() { + return { + pluginCode: '', + onApplyCode: () => {} + }; + }, + getInitialState() { + return { + code: "", + configVisible: false + }; + }, + componentWillMount() { + this.setState({ + code: this.props.pluginCode + }); + }, + componentWillReceiveProps(newProps) { + if (newProps.pluginCode !== this.props.pluginCode) { + this.setState({ + code: newProps.pluginConfig + }); + } + }, + render() { + return (
  • + + + + { + this.setState({ + configVisible: false + }); + }}> + + Live edit your own plugin + + + + +
    {this.props.error}
    +
    +
    +
  • ); + }, + updateCode(newCode) { + this.setState({ + code: newCode + }); + }, + applyCode() { + this.props.onApplyCode(this.state.code); + }, + toggleCfg() { + this.setState({configVisible: !this.state.configVisible}); + } +}); + +module.exports = PluginCreator; diff --git a/web/client/examples/plugins/context.js b/web/client/examples/plugins/context.js new file mode 100644 index 0000000000..30231025d3 --- /dev/null +++ b/web/client/examples/plugins/context.js @@ -0,0 +1,3 @@ +var context = require.context('../..', true, /^\.*((\/components)|(\/actions)|(\/reducers))((?!__tests__).)*jsx?$/); +context.keys().forEach(context); +module.exports = context; diff --git a/web/client/examples/plugins/plugins/My.jsx b/web/client/examples/plugins/plugins/My.jsx deleted file mode 100644 index ebe1986171..0000000000 --- a/web/client/examples/plugins/plugins/My.jsx +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright 2016, GeoSolutions Sas. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ -const React = require('react'); -const {connect} = require('react-redux'); - -const {action} = require('../actions/my'); -const My = React.createClass({ - propTypes: { - action: React.PropTypes.func, - style: React.PropTypes.object, - content: React.PropTypes.string - }, - getDefaultProps() { - return { - style: {}, - content: "Hello!", - action: () => {} - }; - }, - render() { - return ({this.props.content}); - }, - action() { - this.props.action(this.props.content); - } -}); - - -const MyPlugin = connect(() => ({}), { - action -})(My); - -module.exports = { - MyPlugin, - reducers: {my: require('../reducers/my')} -}; diff --git a/web/client/examples/plugins/reducers/config.js b/web/client/examples/plugins/reducers/config.js index 40257a08db..8c0ff6fe10 100644 --- a/web/client/examples/plugins/reducers/config.js +++ b/web/client/examples/plugins/reducers/config.js @@ -6,7 +6,7 @@ * LICENSE file in the root directory of this source tree. */ -const {SAVE_PLUGIN_CONFIG} = require('../actions/config'); +const {SAVE_PLUGIN_CONFIG, COMPILE_ERROR} = require('../actions/config'); const assign = require('object-assign'); function my(state = {}, action) { @@ -14,6 +14,9 @@ function my(state = {}, action) { case SAVE_PLUGIN_CONFIG: { return assign({}, state, {[action.plugin]: action.cfg}); } + case COMPILE_ERROR: { + return assign({}, state, {error: action.message}); + } default: return state; } diff --git a/web/client/examples/plugins/sample.js.raw b/web/client/examples/plugins/sample.js.raw new file mode 100644 index 0000000000..bc711a93f3 --- /dev/null +++ b/web/client/examples/plugins/sample.js.raw @@ -0,0 +1,36 @@ +const clickHandler = () => ({type: 'CLICKED'}); + +const ToggleButton = require('./components/buttons/ToggleButton.jsx'); + +const Comp = connect((state) => ({ + msg: state.custom.comp && state.custom.comp.msg || "Click me!" +}), { + onClick: clickHandler +})( + React.createClass({ + render() { + return ( +
    + {this.props.msg} +
    ); + } + }) +); + +module.exports = { + Plugin: , + reducers: { + comp: (state = {msg: '', counter: 0}, action) => { + switch(action.type) { + case 'CLICKED': { + return { + counter: state.counter + 1, + msg: 'CLICKED ' + (state.counter + 1) + ' times' + }; + } + default: + return state; + } + } + } +}; diff --git a/web/client/examples/plugins/store.js b/web/client/examples/plugins/store.js index a9bbcb2dd6..a03795a307 100644 --- a/web/client/examples/plugins/store.js +++ b/web/client/examples/plugins/store.js @@ -14,14 +14,15 @@ const map = require('../../reducers/map'); const layers = require('../../reducers/layers'); const mapConfig = require('../../reducers/config'); -module.exports = (plugins) => { +module.exports = (plugins, custom) => { const allReducers = combineReducers(plugins, { locale: require('../../reducers/locale'), browser: require('../../reducers/browser'), map: () => {return null; }, mapInitialConfig: () => {return null; }, layers: () => {return null; }, - pluginsConfig: require('./reducers/config') + pluginsConfig: require('./reducers/config'), + custom }); const rootReducer = (state, action) => {