diff --git a/web/client/actions/__tests__/plugins-test.js b/web/client/actions/__tests__/plugins-test.js new file mode 100644 index 0000000000..7154a6b4f9 --- /dev/null +++ b/web/client/actions/__tests__/plugins-test.js @@ -0,0 +1,21 @@ +/** + * 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 expect = require('expect'); +const {loadPlugins, LOAD_PLUGINS} = require('../plugins'); + +describe('Test plugins related actions', () => { + it('test load plugins action', () => { + const action = loadPlugins([{}]); + + expect(action).toExist(); + expect(action.type).toBe(LOAD_PLUGINS); + expect(action.plugins).toExist(); + expect(action.plugins.length).toBe(1); + }); +}); diff --git a/web/client/actions/plugins.js b/web/client/actions/plugins.js new file mode 100644 index 0000000000..43f065dea7 --- /dev/null +++ b/web/client/actions/plugins.js @@ -0,0 +1,16 @@ +/** + * 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 LOAD_PLUGINS = 'LOAD_PLUGINS'; + +function loadPlugins(plugins) { + return { + type: LOAD_PLUGINS, + plugins + }; +} +module.exports = {LOAD_PLUGINS, loadPlugins}; diff --git a/web/client/components/app/StandardApp.jsx b/web/client/components/app/StandardApp.jsx index 55458c874a..7bc1cfadaf 100644 --- a/web/client/components/app/StandardApp.jsx +++ b/web/client/components/app/StandardApp.jsx @@ -36,7 +36,7 @@ const StandardApp = React.createClass({ pluginsDef: {plugins: {}, requires: {}}, initialActions: [], printingEnabled: false, - appStore: () => ({dispatch: () => {}}), + appStore: () => ({dispatch: () => {}, subscribe: () => {}}), appComponent: () => }; }, @@ -57,6 +57,16 @@ const StandardApp = React.createClass({ onPersist: onInit }); this.store = this.props.appStore(this.props.pluginsDef.plugins, opts); + let newPlugins; + this.store.subscribe(() => { + const state = this.store.getState(); + if (state.plugins && Object.keys(state.plugins).length > 0) { + if (state.plugins !== newPlugins) { + newPlugins = state.plugins; + this.store.replaceReducer(this.props.appStore(assign({}, this.props.pluginsDef.plugins, newPlugins), {updateReducers: true})); + } + } + }); if (!opts.persist) { onInit(); } diff --git a/web/client/components/app/StandardRouter.jsx b/web/client/components/app/StandardRouter.jsx index 99159daeb0..f9f672e5f4 100644 --- a/web/client/components/app/StandardRouter.jsx +++ b/web/client/components/app/StandardRouter.jsx @@ -13,28 +13,43 @@ const Debug = require('../development/Debug'); const {Router, Route, hashHistory} = require('react-router'); const Localized = require('../I18N/Localized'); +const assign = require('object-assign'); +const PluginsUtils = require('../../utils/PluginsUtils'); const StandardRouter = React.createClass({ propTypes: { plugins: React.PropTypes.object, locale: React.PropTypes.object, - pages: React.PropTypes.array + pages: React.PropTypes.array, + loadPlugins: React.PropTypes.func }, getDefaultProps() { return { plugins: {}, locale: {messages: {}, current: ''}, - pages: [] + pages: [], + loadPlugins: () => {} }; }, + getInitialState() { + return {plugins: {}}; + }, renderPages() { return this.props.pages.map((page) => { const pageConfig = page.pageConfig || {}; const Component = connect(() => ({ - plugins: this.props.plugins, + plugins: assign({}, this.props.plugins, this.state.plugins), ...pageConfig }))(page.component); - return (); + return ( { + page.plugins((newPlugins) => { + this.setState({ + plugins: assign({}, this.state.plugins, PluginsUtils.getPlugins(newPlugins)) + }); + this.props.loadPlugins(newPlugins); + callback(); + }); + } : null}/>); }); }, render() { diff --git a/web/client/components/app/__tests__/StandardApp-test.jsx b/web/client/components/app/__tests__/StandardApp-test.jsx index 35b9f21291..ea8dec6a47 100644 --- a/web/client/components/app/__tests__/StandardApp-test.jsx +++ b/web/client/components/app/__tests__/StandardApp-test.jsx @@ -56,7 +56,8 @@ describe('StandardApp', () => { const store = () => ({ dispatch() { dispatched++; - } + }, + subscribe() {} }); @@ -75,7 +76,8 @@ describe('StandardApp', () => { if (value === 10) { done(); } - } + }, + subscribe() {} }); diff --git a/web/client/components/app/__tests__/StandardRouter-test.jsx b/web/client/components/app/__tests__/StandardRouter-test.jsx index cafa1893d1..448c715c9d 100644 --- a/web/client/components/app/__tests__/StandardRouter-test.jsx +++ b/web/client/components/app/__tests__/StandardRouter-test.jsx @@ -33,7 +33,7 @@ const mycomponent = React.createClass({ } }); -describe('StandardApp', () => { +describe('StandardRouter', () => { beforeEach((done) => { document.body.innerHTML = '
'; ConfigUtils.setLocalConfigurationFile('base/web/client/test-resources/localConfig.json'); @@ -93,4 +93,34 @@ describe('StandardApp', () => { expect(dom.getElementsByClassName('MyPlugin').length).toBe(1); }); + + it('creates a default router app with pages and on page plugins', () => { + const handlers = { + loadPlugins: () => {} + }; + const plugins = { + MyPlugin: {} + }; + const spy = expect.spyOn(handlers, "loadPlugins"); + const pluginsFunc = (callback) => { + callback(plugins); + }; + + const store = { + dispatch: () => {}, + subscribe: () => {}, + getState: () => ({}) + }; + const pages = [{ + name: 'mypage', + path: '/', + component: mycomponent, + plugins: pluginsFunc + }]; + const app = ReactDOM.render(, document.getElementById("container")); + expect(app).toExist(); + + expect(spy.calls.length).toBe(1); + expect(spy.calls[0].arguments[0]).toBe(plugins); + }); }); diff --git a/web/client/components/map/leaflet/Layer.jsx b/web/client/components/map/leaflet/Layer.jsx index 878406d19c..cadd5d1232 100644 --- a/web/client/components/map/leaflet/Layer.jsx +++ b/web/client/components/map/leaflet/Layer.jsx @@ -104,6 +104,7 @@ const LeafletLayer = React.createClass({ if (this.layer) { this.layer.layerName = options.name; this.layer.layerId = options.id; + this.setState({}); } } }, diff --git a/web/client/components/map/openlayers/Layer.jsx b/web/client/components/map/openlayers/Layer.jsx index 63a1d55ed5..09d5c21ebc 100644 --- a/web/client/components/map/openlayers/Layer.jsx +++ b/web/client/components/map/openlayers/Layer.jsx @@ -101,8 +101,11 @@ const OpenlayersLayer = React.createClass({ if (type) { const layerOptions = this.generateOpts(options, position, CoordinatesUtils.normalizeSRS(this.props.srs)); this.layer = Layers.createLayer(type, layerOptions, this.props.map, this.props.mapId); - if (this.layer && !this.layer.detached) { - this.addLayer(options); + if (this.layer) { + this.setState({}); + if (!this.layer.detached) { + this.addLayer(options); + } } } }, diff --git a/web/client/containers/MapViewer.jsx b/web/client/containers/MapViewer.jsx index 583d1a55e3..6790ba54ad 100644 --- a/web/client/containers/MapViewer.jsx +++ b/web/client/containers/MapViewer.jsx @@ -15,7 +15,7 @@ const urlQuery = url.parse(window.location.href, true).query; const ConfigUtils = require('../utils/ConfigUtils'); const PluginsContainer = connect((state) => ({ - pluginsConfig: state.plugins || ConfigUtils.getConfigProp('plugins') || null, + pluginsConfig: ConfigUtils.getConfigProp('plugins') || null, mode: (urlQuery.mode || (state.browser && state.browser.mobile ? 'mobile' : 'desktop')), pluginsState: state && state.controls || {} }))(require('../components/plugins/PluginsContainer')); diff --git a/web/client/examples/rasterstyler/pages/MapViewer.jsx b/web/client/examples/rasterstyler/pages/MapViewer.jsx index 552fd93ed1..589df3a952 100644 --- a/web/client/examples/rasterstyler/pages/MapViewer.jsx +++ b/web/client/examples/rasterstyler/pages/MapViewer.jsx @@ -22,7 +22,7 @@ const {resetControls} = require('../../../actions/controls'); const urlQuery = url.parse(window.location.href, true).query; const PluginsContainer = connect((state) => ({ - pluginsConfig: state.plugins || ConfigUtils.getConfigProp('plugins') || null, + pluginsConfig: ConfigUtils.getConfigProp('plugins') || null, mode: (urlQuery.mobile || (state.browser && state.browser.touch)) ? 'mobile' : 'desktop' }))(require('../../../components/plugins/PluginsContainer')); diff --git a/web/client/examples/styler/pages/MapViewer.jsx b/web/client/examples/styler/pages/MapViewer.jsx index 50cc307961..eed5c968a4 100644 --- a/web/client/examples/styler/pages/MapViewer.jsx +++ b/web/client/examples/styler/pages/MapViewer.jsx @@ -22,7 +22,7 @@ const {resetControls} = require('../../../actions/controls'); const urlQuery = url.parse(window.location.href, true).query; const PluginsContainer = connect((state) => ({ - pluginsConfig: state.plugins || ConfigUtils.getConfigProp('plugins') || null, + pluginsConfig: ConfigUtils.getConfigProp('plugins') || null, mode: (urlQuery.mobile || (state.browser && state.browser.touch)) ? 'mobile' : 'desktop' }))(require('../../../components/plugins/PluginsContainer')); @@ -65,4 +65,3 @@ module.exports = connect((state) => ({ loadMapConfig, reset: resetControls })(MapViewer); - diff --git a/web/client/plugins/Save.jsx b/web/client/plugins/Save.jsx index 26cd5b3145..7d6daf00c9 100644 --- a/web/client/plugins/Save.jsx +++ b/web/client/plugins/Save.jsx @@ -145,5 +145,8 @@ module.exports = { return { style: {display: "none"} }; } } - })) + })), + reducers: { + currentMap: require('../reducers/currentMap') + } }; diff --git a/web/client/plugins/SaveAs.jsx b/web/client/plugins/SaveAs.jsx index 56873414ee..118d7f30da 100644 --- a/web/client/plugins/SaveAs.jsx +++ b/web/client/plugins/SaveAs.jsx @@ -186,5 +186,8 @@ module.exports = { return state && state.security && state.security.user ? {} : { style: {display: "none"} }; } } - })) + })), + reducers: { + currentMap: require('../reducers/currentMap') + } }; diff --git a/web/client/product/app.jsx b/web/client/product/app.jsx index 7349f7592a..d3761efc91 100644 --- a/web/client/product/app.jsx +++ b/web/client/product/app.jsx @@ -9,6 +9,7 @@ const React = require('react'); const ReactDOM = require('react-dom'); const {connect} = require('react-redux'); const LocaleUtils = require('../utils/LocaleUtils'); +const {loadPlugins} = require('../actions/plugins'); const startApp = () => { const ConfigUtils = require('../utils/ConfigUtils'); @@ -22,7 +23,9 @@ const startApp = () => { const StandardRouter = connect((state) => ({ locale: state.locale || {}, pages - }))(require('../components/app/StandardRouter')); + }), { + loadPlugins + })(require('../components/app/StandardRouter')); const appStore = require('../stores/StandardStore').bind(null, initialState, { home: require('./reducers/home'), diff --git a/web/client/product/appConfig.js b/web/client/product/appConfig.js index 9bcc57547f..be6112bea7 100644 --- a/web/client/product/appConfig.js +++ b/web/client/product/appConfig.js @@ -10,23 +10,75 @@ module.exports = { pages: [{ name: "home", path: "/", - component: require('./pages/Maps') - }, { - name: "maps", - path: "/maps", - component: require('./pages/Maps') + component: require('./pages/Maps'), + plugins: (resolve) => { + return require.ensure([], () => { + resolve({ + MapsPlugin: require('../plugins/Maps'), + MapSearchPlugin: require('../plugins/MapSearch'), + HomeDescriptionPlugin: require('./plugins/HomeDescription'), + ExamplesPlugin: require('./plugins/Examples'), + MapTypePlugin: require('./plugins/MapType'), + ForkPlugin: require('./plugins/Fork'), + ManagerPlugin: require('../plugins/manager/Manager'), + CreateNewMapPlugin: require('../plugins/CreateNewMap') + }); + }); + } }, { name: "mapviewer", path: "/viewer/:mapType/:mapId", - component: require('./pages/MapViewer') - }, { - name: "manager", - path: "/manager", - component: require('./pages/Manager') + component: require('./pages/MapViewer'), + plugins: (resolve) => { + return require.ensure([], () => { + resolve({ + MousePositionPlugin: require('../plugins/MousePosition'), + PrintPlugin: require('../plugins/Print'), + IdentifyPlugin: require('../plugins/Identify'), + TOCPlugin: require('../plugins/TOC'), + BackgroundSwitcherPlugin: require('../plugins/BackgroundSwitcher'), + MeasurePlugin: require('../plugins/Measure'), + MeasureResultsPlugin: require('../plugins/MeasureResults'), + MapPlugin: require('../plugins/Map'), + ToolbarPlugin: require('../plugins/Toolbar'), + DrawerMenuPlugin: require('../plugins/DrawerMenu'), + ShapeFilePlugin: require('../plugins/ShapeFile'), + SnapshotPlugin: require('../plugins/Snapshot'), + SettingsPlugin: require('../plugins/Settings'), + ExpanderPlugin: require('../plugins/Expander'), + SearchPlugin: require('../plugins/Search'), + ScaleBoxPlugin: require('../plugins/ScaleBox'), + LocatePlugin: require('../plugins/Locate'), + ZoomInPlugin: require('../plugins/ZoomIn'), + ZoomOutPlugin: require('../plugins/ZoomOut'), + ZoomAllPlugin: require('../plugins/ZoomAll'), + MapLoadingPlugin: require('../plugins/MapLoading'), + AboutPlugin: require('./plugins/About'), + HelpPlugin: require('../plugins/Help'), + MadeWithLovePlugin: require('./plugins/MadeWithLove'), + MetadataExplorerPlugin: require('../plugins/MetadataExplorer'), + BurgerMenuPlugin: require('../plugins/BurgerMenu'), + UndoPlugin: require('../plugins/History'), + RedoPlugin: require('../plugins/History'), + SavePlugin: require('../plugins/Save'), + SaveAsPlugin: require('../plugins/SaveAs'), + SharePlugin: require('../plugins/Share') + }); + }); + } }, { name: "manager", - path: "/manager/:tool", - component: require('./pages/Manager') + path: "/manager(/:tool)", + component: require('./pages/Manager'), + plugins: (resolve) => { + return require.ensure([], () => { + resolve({ + UserManagerPlugin: require('../plugins/manager/UserManager'), + RulesManagerPlugin: require('../plugins/manager/RulesManager'), + ManagerPlugin: require('../plugins/manager/Manager') + }); + }); + } }], pluginsDef: require('./plugins.js'), initialState: { diff --git a/web/client/product/plugins.js b/web/client/product/plugins.js index ac68ddcd33..883220692f 100644 --- a/web/client/product/plugins.js +++ b/web/client/product/plugins.js @@ -8,56 +8,15 @@ module.exports = { plugins: { - MousePositionPlugin: require('../plugins/MousePosition'), - PrintPlugin: require('../plugins/Print'), - IdentifyPlugin: require('../plugins/Identify'), - TOCPlugin: require('../plugins/TOC'), - BackgroundSwitcherPlugin: require('../plugins/BackgroundSwitcher'), - MeasurePlugin: require('../plugins/Measure'), - MeasureResultsPlugin: require('../plugins/MeasureResults'), - MapPlugin: require('../plugins/Map'), - ToolbarPlugin: require('../plugins/Toolbar'), - DrawerMenuPlugin: require('../plugins/DrawerMenu'), - ShapeFilePlugin: require('../plugins/ShapeFile'), - SnapshotPlugin: require('../plugins/Snapshot'), - SettingsPlugin: require('../plugins/Settings'), - ExpanderPlugin: require('../plugins/Expander'), - SearchPlugin: require('../plugins/Search'), - ScaleBoxPlugin: require('../plugins/ScaleBox'), - LocatePlugin: require('../plugins/Locate'), - ZoomInPlugin: require('../plugins/ZoomIn'), - ZoomOutPlugin: require('../plugins/ZoomOut'), - ZoomAllPlugin: require('../plugins/ZoomAll'), - MapLoadingPlugin: require('../plugins/MapLoading'), - AboutPlugin: require('./plugins/About'), - HelpPlugin: require('../plugins/Help'), + OmniBarPlugin: require('../plugins/OmniBar'), HomePlugin: require('../plugins/Home'), - MadeWithLovePlugin: require('./plugins/MadeWithLove'), - MetadataExplorerPlugin: require('../plugins/MetadataExplorer'), LoginPlugin: require('../plugins/Login'), - OmniBarPlugin: require('../plugins/OmniBar'), - BurgerMenuPlugin: require('../plugins/BurgerMenu'), - UndoPlugin: require('../plugins/History'), - RedoPlugin: require('../plugins/History'), - MapsPlugin: require('../plugins/Maps'), - MapSearchPlugin: require('../plugins/MapSearch'), - HomeDescriptionPlugin: require('./plugins/HomeDescription'), - ExamplesPlugin: require('./plugins/Examples'), - MapTypePlugin: require('./plugins/MapType'), + ManagerMenuPlugin: require('../plugins/manager/ManagerMenu'), + RedirectPlugin: require('../plugins/Redirect'), LanguagePlugin: require('../plugins/Language'), AttributionPlugin: require('./plugins/Attribution'), HeaderPlugin: require('./plugins/Header'), - ForkPlugin: require('./plugins/Fork'), - FooterPlugin: require('./plugins/Footer'), - ManagerPlugin: require('../plugins/manager/Manager'), - UserManagerPlugin: require('../plugins/manager/UserManager'), - RulesManagerPlugin: require('../plugins/manager/RulesManager'), - ManagerMenuPlugin: require('../plugins/manager/ManagerMenu'), - RedirectPlugin: require('../plugins/Redirect'), - SharePlugin: require('../plugins/Share'), - SavePlugin: require('../plugins/Save'), - SaveAsPlugin: require('../plugins/SaveAs'), - CreateNewMapPlugin: require('../plugins/CreateNewMap') + FooterPlugin: require('./plugins/Footer') }, requires: { ReactSwipe: require('react-swipeable-views').default, diff --git a/web/client/reducers/__tests__/plugins-test.js b/web/client/reducers/__tests__/plugins-test.js new file mode 100644 index 0000000000..71d536d4cd --- /dev/null +++ b/web/client/reducers/__tests__/plugins-test.js @@ -0,0 +1,17 @@ +/** + * Copyright 2015, 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 expect = require('expect'); + +const plugins = require('../plugins'); + +describe('Test the plugins reducer', () => { + it('load plugins', () => { + const state = plugins({}, {type: 'LOAD_PLUGINS', plugins: {MyPlugin: {}}}); + expect(state.MyPlugin).toExist(); + }); +}); diff --git a/web/client/reducers/plugins.js b/web/client/reducers/plugins.js new file mode 100644 index 0000000000..a372dc0bb1 --- /dev/null +++ b/web/client/reducers/plugins.js @@ -0,0 +1,24 @@ +/** + * 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. + */ + +var { LOAD_PLUGINS } = require('../actions/plugins'); +var assign = require('object-assign'); + +function plugins(state = {}, action) { + switch (action.type) { + case LOAD_PLUGINS: { + return assign({}, state, + action.plugins + ); + } + default: + return state; + } +} + +module.exports = plugins; diff --git a/web/client/stores/StandardStore.js b/web/client/stores/StandardStore.js index 5ee43d734a..d377d616c7 100644 --- a/web/client/stores/StandardStore.js +++ b/web/client/stores/StandardStore.js @@ -31,6 +31,7 @@ module.exports = (initialState = {defaultState: {}, mobile: {}}, appReducers = { browser: require('../reducers/browser'), controls: require('../reducers/controls'), help: require('../reducers/help'), + plugins: require('../reducers/plugins'), map: () => {return null; }, mapInitialConfig: () => {return null; }, layers: () => {return null; } @@ -51,15 +52,24 @@ module.exports = (initialState = {defaultState: {}, mobile: {}}, appReducers = { if (action && action.type === CHANGE_BROWSER_PROPERTIES && newState.browser.mobile) { newState = assign(newState, mobileOverride); } - + if (action.type === '@@redux/INIT') { + Object.keys(newState).forEach((key) => { + if (defaultState[key]) { + newState[key] = assign(newState[key], defaultState[key]); + } + }); + } return newState; }; + if (storeOpts.updateReducers) { + return rootReducer; + } let store; if (storeOpts && storeOpts.persist) { - store = DebugUtils.createDebugStore(rootReducer, defaultState, [], autoRehydrate()); + store = DebugUtils.createDebugStore(rootReducer, {}, [], autoRehydrate()); persistStore(store, storeOpts.persist, storeOpts.onPersist); } else { - store = DebugUtils.createDebugStore(rootReducer, defaultState); + store = DebugUtils.createDebugStore(rootReducer, {}); } SecurityUtils.setStore(store); return store;