Develop a Geo Tour Plugin¶
In this section we will dive into the creation of a plugin and the possibilities are many so we can take this opportunity to learn the important stuff
In our case we will do more by including some advanced topics like plugin container
How to use a plugin container like sidebar menu
Extend plugin by adding actions, reducers, selectors, epics
Add custom theme which will include theme parts for the GeoTour plugin
Add unit tests of the codebase added
Add documentation of the codebase added
How to render a plugin conditionally using monitored state
Extending or customizing lint configuration
Setup the development environment¶
Since we want to develop things, let’s developing let’s start our dev instance
npm run fe:start
In a different shell start up the backend
npm run be:start
Now navigate with your favorite browsers to http://localhost:8081/?debug=true#/
Create a new plugin¶
A plugin is the main development unit used to add functionalities to MapStore
Normally to develop a plugin you have to do 3 things:
Create plugin file with the code of the plugin, the exports plugin in the defined format (e.g.
js/plugins/GeoTour.jsx
)Add the definition to your
js/plugins.js
file.Configure it in
configs/localConfig.json
in the dedicated section for a page (e.g.plugins.desktop
)
These operations will make your new plugin available for your page (desktop is associated to the main mapstore viewer).
Create plugin file¶
Let’s create a new plugin in our project
js/plugins
folderweb/client/plugins
if you are working on the core).mkdir js/plugins
cd js/plugins
touch GeoTour.jsx
Copy the following in js/plugins/GeoTour.jsx
// js/plugins/GeoTour.jsx
import React from "react";
import { Glyphicon } from "react-bootstrap";
import { createSelector } from "reselect";
import { connect, createPlugin } from "@mapstore/utils/PluginsUtils";
import Dialog from "@mapstore/components/misc/Dialog";
import Message from "@mapstore/components/I18N/Message";
import { setControlProperty } from "@mapstore/actions/controls";
import { geotourEnabledSelector } from '@js/selectors/geotour';
/**
* The GeoTour plugin allows you to
* @memberof plugins
* @prop {object} cfg properties that can be configured for this plugin
* @prop {object} [cfg.width=500] width of the panel
*/
const PanelContainer = ({
// local properties
id = "geo-tour-plugin",
width = 500,
// coming from the connect (connected to the store)
enabled = true,
// coming from the connect function (mapDispatchToProps)
onClose = () => {}
}) => {
return enabled ? (<Dialog
id={id}
style={{
zIndex: 10000,
position: "absolute",
left: "17%",
top: "50px",
margin: 0,
width
}}
modal={false}
draggable
bsStyle="primary"
>
<span role="header">
<Glyphicon glyph="globe" />
<Message msgId="geotour.name"/>
<button onClick={() => onClose()} className="close">
<Glyphicon glyph="1-close" />
</button>
</span>
<div role="body">
This will be an awesome plugin
</div>
</Dialog>) : null;
};
const ConnectedPlugin = connect(createSelector([
state => state?.controls?.geotour?.enabled
], (enabled) => ({
enabled
})), {
onClose: setControlProperty.bind(null, "geotour", "enabled", false, false)
})(PanelContainer);
// export the plugin using the createPlugin utils which is the recommended way
export default createPlugin("GeoTour", {
component: ConnectedPlugin
});
This is the simplest plugin you can imagine (we are going to add more soon :)
Note
You will find several ways how plugins are exported inside the framework and inside the code and documentation.
The suggested way is to use the createPlugin
function from web/client/utils/pluginUtils
Add the definition¶
cd js
touch plugins.js
// js/plugins.js
import GeoTour from "@js/plugins/GeoTour";
import productPlugins from "@mapstore/product/plugins.js";
/**
* This will compose the list of plugins that can be accessed from your application, and that wil lbe included in the bundle
*/
export default {
requires: {
...productPlugins.requires
},
plugins: {
...productPlugins.plugins,
GeoTour
}
};
Edit js/app.jsx
in order to use a custom version of localConfig.json
and use a custom list of plugins
+ import plugins from '@js/plugins.js';
- const plugins = require('@mapstore/product/plugins').default;
+ ConfigUtils.setLocalConfigurationFile('configs/localConfig.json');
- ConfigUtils.setLocalConfigurationFile('MapStore2/web/client/configs/localConfig.json');
- ConfigUtils.setConfigProp('translationsPath', ['./MapStore2/web/client/translations']);
+ ConfigUtils.setConfigProp('translationsPath', ['./MapStore2/web/client/translations', './translations']);
since we are preparing the ground for custom translations let’s add them already
# from the root
mkdir translations
cd translations
touch data.en-US.json
add the following to the translations/data.en-US.json
{
"locale": "en-US",
"messages": {
"geotour": {
"name": "GeoTour"
}
}
}
In this way we will extend/override official translations coming from mapstore
Configure plugin¶
configs/localConfig.json
is the main config file used by mapstore that allows you to chose which plugins can be rendered and where. It allows you to edit other properties used globally like, projection definitions, custom cors rules, default catalog services etc., you can refer to this guide to know more about it
This file has an object called plugins and it will contain all the mode/page used by mapstore to render
adding to plugins --> desktop
(for now)
- "LayerDownload",
+ "LayerDownload",
+ {
+ "name": "GeoTour"
+ },
Note
Whenever you edit & save this config file, you have to refresh the page to see updates
Add functionalities¶
Plugin container¶
A plugin container is a component (usually in a plugin or a page) that can contain other plugin’s parts and render them accordingly.
For instance:
a toolbar like navigation bar or the sidebar
a dropdown menu like the burger menu
a panel like the drawer menu
In order to make a button appear in the sidebar menu we have apply the following changes to the js/plugins/GeoTour.jsx
file
- import { Panel } from 'react-bootstrap';
+ import { Panel, Glyphicon } from 'react-bootstrap';
- import { setControlProperty } from '@mapstore/actions/controls';
+ import { toggleControl, setControlProperty } from "@mapstore/actions/controls";
- component: ConnectedPlugin
+ component: ConnectedPlugin,
+ containers: {
+ SidebarMenu: {
+ name: 'geotour',
+ position: 10000, // this will put the button at the bottom of the list
+ tooltip: <Message msgId="geotour.name"/>,
+ icon: <Glyphicon glyph="globe"/>,
+ action: toggleControl.bind(null, 'geotour', null)
+ }
+ }
In this way we will render a button that will toggle visibility of the geotour plugin and it will interact with the state.controls.geotour.enabled flag
icon icon component to use, icons available can be checked in this glyphicon set
tooltip optional text to use that will be rendered in the list, after the icon
position is number that will define the order of items in the list
action is an action to dispatch when the item in the burger menu is clicked
Component¶
Since we are talking about a lot of changes, let’s apply them first, then we will explain them.
// js/plugins/GeoTour.jsx
import React, {useState} from "react";
import { Button, Panel, Glyphicon } from "react-bootstrap";
import { createSelector } from "reselect";
import Dropzone from 'react-dropzone';
import bbox from '@turf/bbox';
import { connect, createPlugin } from "@mapstore/utils/PluginsUtils";
import Dialog from "@mapstore/components/misc/Dialog";
import Message from "@mapstore/components/I18N/Message";
import BorderLayout from "@mapstore/components/layout/BorderLayout";
import { toggleControl, setControlProperty } from "@mapstore/actions/controls";
import { readJson } from '@mapstore/utils/FileUtils';
import { updateAdditionalLayer } from '@mapstore/actions/additionallayers';
import { zoomToExtent } from '@mapstore/actions/map';
import { uploadFile } from '@js/actions/geotour';
import { geotourFileSelector, geotourEnabledSelector, geotourReverseGeocodeDataSelector } from '@js/selectors/geotour';
import geotour from '@js/reducers/geotour';
import * as geotourEpics from '@js/epics/geotour';
/**
* The GeoTour plugin allows you to
* @memberof plugins
* @prop {object} cfg properties that can be configured for this plugin
* @prop {object} [cfg.showCloseButton=false] if true, it shows the close button, default is false
*/
const PanelContainer = ({
// local properties
id = "geo-tour-plugin",
dropMessage = "Drop here a geojson that can be used to fly to its features",
// coming from the configuration cfg object of the plugin, defined in localConfig.plugins.[desktop|tour|embedded]...
showCloseButton = false,
// coming from the connect (connected to the store)
enabled = false,
file = null,
reverseGeocodeData = "", // coming from the connect (connected to the store)
// coming from the connect function (mapDispatchToProps)
onClose = () => {},
onUpload = () => {},
addMarkers = () => {},
flyTo = () => {}
}) => {
const [flyToEnabled, setFlyToEnabled] = useState(false);
const [size, setSize] = useState({width: 500, height: 500});
const uploadFiles = (files) => {
if (!files) return;
const fileToParse = files[0];
readJson(fileToParse).then(f => {
onUpload({...f});
addMarkers(
"geotour-layer",
"goutour",
'overlay',
{
id: "geotour-layer",
name: "geotour-layer",
type: "vector",
features: f.features.map(ft => ({
...ft,
style: {
"iconGlyph": "comment",
"iconShape": "square",
"iconColor": "blue",
"iconAnchor": [ 0.5, 0.5],
"filtering": true
}
}))
});
});
};
return enabled ? (<Dialog
id={id}
style={{
zIndex: 10000,
position: "absolute",
left: "17%",
top: "50px",
margin: 0,
width: size.width
}}
modal={false}
draggable
bsStyle="primary"
>
<span role="header">
<Glyphicon glyph="globe" />
<Message msgId="geotour.name"/>
<button onClick={() => onClose()} className="close">
<Glyphicon glyph="1-close" />
</button>
</span>
<Panel role="body" id={id} >
<BorderLayout>
<Dropzone
key="DragZone"
rejectClassName="alert-danger"
className="alert alert-info"
onDrop={uploadFiles}
style={{}}
activeStyle={{}}>
<div style={{
display: "flex",
alignItems: "center",
width: "100%",
height: "100%",
justifyContent: "center"
}}>
<span style={{
textAlign: "center",
cursor: "pointer"
}}>
<Glyphicon glyph="upload"/>
{` ${dropMessage}`}
</span>
</div>
</Dropzone>
{file ? <>
<div>
<table className="geotour-table-results">
<tr>
<td> action </td>
<td> # </td>
<td className="table-geom-type"> geometry type </td>
<td> coordinates </td>
</tr>
{ file.features.map((ft, index) => {
return (<tr>
<td>
<Button disabled={!flyToEnabled} onClick={() => {
flyTo(bbox(ft), "EPSG:4326", 5, {duration: 1000, point: ft.geometry.coordinates});
}}>fly to</Button>
</td>
<td>{index + 1}</td>
<td>{ft.geometry.type}</td>
<td>{ft.geometry.coordinates.toString()}</td>
</tr>);
}) }
</table>
</div>
<p>Click next button in order to {flyToEnabled ? "disable" : "enable"} fly to</p>
<Button onClick={() => {
setFlyToEnabled(!flyToEnabled);
}}>fly to</Button>
{
reverseGeocodeData ? (
<>
<p>ReverseGeocodeData address</p>
<li> Region: {reverseGeocodeData?.address?.state || "N.A." }</li>
<li> Country: {reverseGeocodeData?.address?.country || "N.A." }</li>
<li> City: {reverseGeocodeData?.address?.city || "N.A." }</li>
</>
) : null }
</>
: null }
</BorderLayout>
</Panel>
</Dialog>) : null;
};
const ConnectedPlugin = connect(createSelector([
geotourEnabledSelector,
geotourFileSelector,
geotourReverseGeocodeDataSelector
], (enabled, file, reverseGeocodeData) => ({
enabled,
file,
reverseGeocodeData
})), {
onClose: setControlProperty.bind(null, "geotour", "enabled", false, false),
onUpload: uploadFile,
addMarkers: updateAdditionalLayer,
flyTo: zoomToExtent
})(PanelContainer);
// export the plugin using the createPlugin utils which is the recommended way
export default createPlugin("GeoTour", {
component: ConnectedPlugin,
containers: {
SidebarMenu: {
name: "geotour",
position: 10000,
tooltip: <Message msgId="geotour.name"/>,
icon: <Glyphicon glyph="globe"/>,
action: toggleControl.bind(null, "geotour", null)
}
},
reducers: {
geotour
},
epics: geotourEpics
});
The <Dropzone>
component has an onDrop function that will bring the files dropped
this files are passed to an uploadFiles function which assumes it is a json
and then uses a readJson utility function present in mapstore framework
Once we have these information we use an action (updateAdditionalLayer) coming from mapstore framework to create a layer that will only be rendered and not added in the toc. They are called additionalLayers We also push the content of the geojson dropped with uploadFile action that we are going to create
If we wanted to add them to the TOC too we had to used a different action called addLayer from @mapstore/actions/layers
In order to interact with the state we need to connect These functions in the final part of the connect function
})), {
onClose: setControlProperty.bind(null, "geotour", "enabled", false, false),
onUpload: uploadFile,
addMarkers: updateAdditionalLayer,
flyTo: zoomToExtent
})(PanelContainer);
If you have noticed we use also internal or local state for managing the status of some action buttons in the table since this information is not relevant across the application we decided to use the dedicated react hooks and use a local state
const [flyToEnabled, setFlyToEnabled] = useState(false);
Also take a note on the addition to the createPlugin function because this is the way you can add reducers and epics to a plugin
reducers: {
geotour
},
epics: geotourEpics
Note
Now let’s prepare the ground for the other changes we need
# from the root
mkdir js/actions
mkdir js/reducers
mkdir js/selectors
mkdir js/epics
touch js/actions/geotour.js
touch js/reducers/geotour.js
touch js/selectors/geotour.js
touch js/epics/geotour.js
Reducer¶
Since our application has a global store, which is a js object, we use reducers that are functions that manipulate a particular piece of the store. A reducer accept as parameters a state and an action object and returns a new state
The reason we store things in the store through reducers and actions is because this information can be globally accessed across the entire application
put this in the js/reducers/geotour.js
// js/reducers/geotour.js
import { SHOW_REVERSE_GEOCODE } from '@mapstore/actions/mapInfo';
import { GEOTOUR_UPLOAD_FILE } from '@js/actions/geotour';
export const geotour = (state = {}, action) => {
switch (action.type) {
case GEOTOUR_UPLOAD_FILE: {
return {
...state,
file: action.file
};
}
case SHOW_REVERSE_GEOCODE: {
return {
...state,
reverseGeocodeData: action.reverseGeocodeData
};
}
default:
return state;
}
};
export default geotour;
This file is pretty simple. It’s a normal switch that based on the action type manipulates the state accordingly.
The GEOTOUR_UPLOAD_FILE constant is usually stored near the action creator and it is used here to store the file dropped in the dropzone Note: Check the console or redux dev tool later for this
The SHOW_REVERSE_GEOCODE justs store information coming from a request sent to Nominatim as we will see in the epics file
Note
Never mutate objects in the state, always return new objects
Actions¶
The actions are simply functions that allows you to manipulate the store by describing how the manipulation should happen
Add the following to js/actions/geotour.js
export const GEOTOUR_UPLOAD_FILE = "GEOTOUR:GEOTOUR_UPLOAD_FILE";
export const uploadFile = (file) => ({
type: GEOTOUR_UPLOAD_FILE,
file
});
Selectors¶
Add the following to js/selectors/geotour.js
export const geotourReverseGeocodeDataSelector = state => state?.geotour?.reverseGeocodeData;
export const geotourFileSelector = state => state?.geotour?.file;
export const geotourEnabledSelector = state => state?.controls?.geotour?.enabled;
A selector is a function that takes the state as argument in and returns something, it can be reused in different places like in epics or other plugins so it is suggested to place them in a dedicated folder
Epics¶
Epics are basically manipulation of action events, for handling side effects
check here the Detailed explanation or in the developer doc here
Add the following to js/epics/geotour.js
import Rx from 'rxjs';
import { ZOOM_TO_EXTENT } from '@mapstore/actions/map';
import { showMapinfoRevGeocode } from '@mapstore/actions/mapInfo';
/**
* Epics for geotour plugin. Intercept `ZOOM_TO_EXTENT` action and dispatch a `showMapinfoRevGeocode` action.
* The `showMapinfoRevGeocode` is a thunk action that invokes the GeoCodingApi, reverse GeoCodes the given lat-lon and emits
* an action of type `SHOW_REVERSE_GEOCODE` with the result.
export const addFeatureInfoEpic = (action$) => action$.ofType(ZOOM_TO_EXTENT)
.switchMap(
(action) => {
return Rx.Observable.of(
[showMapinfoRevGeocode({lng: action.options.point[0], lat: action.options.point[1] })]
);
}
);
Here we react to the ZOOM_TO_EXTENT action in order to call Nominatim and to obtain reverse geocode data from a point
Caveat: this will be triggered every time a ZOOM_TO_EXTENT action is dispatched so is better to dispatch another action when clicking on the fly to that can be listened here and:
either add the dispatch of the zoomToExtent from here
or create a thunk action and dispatch two actions there one for updating the reverse geocode data and one for the zoomToExtent
Here we are returning the showMapinfoRevGeocode which will be dispatched by the middleware we have in mapstore core. Notice that this is a thunk and thunk can be used to fetch asynchronous data
Another way is to uses the RxJs.defer like has been done here for contextcreator epics
Test file¶
for testing our plugin we need a sample file to upload. So let’s create it in assets
folder
# from the root
touch assets/markers.json
Add the following to assets/markers.json
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {},
"geometry": {
"type": "Point",
"coordinates": [
0.1318359375,
43.77109381775651
]
}
},
{
"type": "Feature",
"properties": {},
"geometry": {
"type": "Point",
"coordinates": [
9.217529296875,
45.47554027158593
]
}
},
{
"type": "Feature",
"properties": {},
"geometry": {
"type": "Point",
"coordinates": [
21.0498046875,
44.62175409623324
]
}
}
]
}
Create a Custom theme¶
Let’s include the needed style as part of the theme as explained here
# go to the root folder of your project (where package.json is located)
cd ..
# create needed folders
mkdir themes/geo-theme/less -p
# create needed files
touch themes/geo-theme/theme.less
touch themes/geo-theme/variables.less
touch themes/geo-theme/less/geo-tour.less
Inside themes/geo-theme/theme.less
put
// themes/geo-theme/theme.less
@import "../../MapStore2/web/client/themes/default/theme.less";
@import "./variables.less";
@import "./less/geo-tour.less";
Inside themes/geo-theme/variables.less
put
/* change primary color to orange */
@ms-primary: #ff7b00;
Inside themes/geo-theme/less/geo-tour.less
put
// **************
// Theme
// **************
#ms-components-theme(@theme-vars) {
// here all the selectors related to the theme
// use the mixins declared in the web/client/theme/default/less/mixins/css-properties.less
// to use one of the @theme-vars
// eg: use the main background and text colors to paint my plugin
/* .my-plugin-class {
.color-var(@theme-vars[main-color]);
.background-color-var(@theme-vars[main-bg]);
}*/
}
// **************
// Layout
// **************
// eg: fill all the available space in the parent container with my plugin
#page-tour {
#map {
bottom: 0;
}
}
#geo-tour-plugin {
.geotour-table-results {
td {
padding: 10px;
}
.table-geom-type {
display: flex;
width: 130px;
}
}
}
Update webpack configuration to use the custom style (webpack.config.js
, prod-webpack.config.js
)
- themeEntries,
+ themeEntries: {
+ "themes/default": path.join(__dirname, "themes", "geo-theme", "theme.less")
+ },
Once reached this point, you can load the viewer of mapstore and see the plugin being rendered.
Note
Mapstore uses bootstrap and react-bootstrap components for creating the UI, most of the times is enough to pick them or to reuse components from @mapstore/components/misc
Extra topics¶
Documentation¶
Plugins (but also actions, reducers, selectors and epics) documentation is defined with JsDoc and generated using generated using docma
Mapstore documentation is then hosted here
Files included in the docma documentation are included by the docma-config.json
and this is not available in mapstore projects but only in the framework
See here for more details about building the framework documentation with docma
If you check the js/plugins/GeoTour.jsx
you will get an idea on how to add documentation
Unit tests¶
Tests in mapstore can be checked in two ways:
by running them all with npm test
by running a subset of tests in watch mode with npm run test:watch
First let’s create a test for our selectors, actions, reducers and plugin
# from the root
mkdir js/selectors/__tests__
touch js/selectors/__tests__/geotour-test.js
mkdir js/actions/__tests__
touch js/actions/__tests__/geotour-test.js
mkdir js/reducers/__tests__
touch js/reducers/__tests__/geotour-test.js
mkdir js/plugins/__tests__
touch js/plugins/__tests__/GeoTour-test.js
Inside js/selectors/__tests__/geotour-test.js
put
import expect from 'expect';
import { geotourFileSelector, geotourEnabledSelector } from '@js/selectors/geotour';
describe("geotour selectors tests", () => {
it("geotourFileSelector test", () => {
expect(geotourFileSelector({})).toEqual(undefined);
});
it("geotourEnabledSelector test", () => {
expect(geotourEnabledSelector({})).toEqual(undefined);
});
});
Inside js/actions/__tests__/geotour-test.js
put
import expect from 'expect';
import { uploadFile, GEOTOUR_UPLOAD_FILE } from '@js/actions/geotour';
describe("geotour actions tests", () => {
it("uploadFile test", () => {
const file = {};
expect(uploadFile(
file
)).toEqual({
type: GEOTOUR_UPLOAD_FILE,
file
});
});
});
Inside js/reducers/__tests__/geotour-test.js
put
import expect from 'expect';
import { uploadFile } from '@js/actions/geotour';
import geotour from '@js/reducers/geotour';
describe("geotour reducers tests", () => {
it("uploadFile test", () => {
const file = {
type: "FeatureCollection"
};
const initialState = {};
let state = geotour(initialState, uploadFile(file));
expect(state).toBeTruthy();
expect(state).toEqual({
file
});
});
it.skip("TODO SHOW_REVERSE_GEOCODE test", () => {
// complete this test autonomously
const file = {
type: "FeatureCollection"
};
const initialState = {};
let state = geotour(initialState, uploadFile(file));
expect(state).toBeTruthy();
expect(state).toEqual({
file
});
});
});
Inside js/plugins/__tests__/GeoTour-test.js
put
import expect from 'expect';
import React from 'react';
import ReactDOM from 'react-dom';
import GeoTourPlugin from '@js/plugins/GeoTour';
import { getPluginForTest } from '@mapstore/plugins/__tests__/pluginsTestUtils';
describe('Geostories Plugin', () => {
beforeEach((done) => {
document.body.innerHTML = '<div id="container"></div>';
setTimeout(done);
});
afterEach((done) => {
ReactDOM.unmountComponentAtNode(document.getElementById("container"));
document.body.innerHTML = '';
setTimeout(done);
});
it('creates GeoTour plugin with default configuration', () => {
const { Plugin } = getPluginForTest(GeoTourPlugin, {
controls: {
geotour: {
enabled: true
}
}
});
ReactDOM.render(<Plugin/>, document.getElementById("container"));
const buttons = document.querySelectorAll('.glyphicon-upload');
expect(buttons.length).toBe(1);
});
});
We have added all of our tests, now try to run
npm test
You will see something like this
Note
In version 2023.01.00 the test will fail because of a bug.
You can workaround the bug by removing the istanbul instrumenter rule from karma.conf.single-run.js
This will not run the coverage report but will allow you to run the tests.
- testConfig.webpack.module.rules = [{
- test: /\.jsx?$/,
- exclude: /(__tests__|node_modules|legacy|libs\\Cesium|libs\\html2canvas)\\|(__tests__|node_modules|legacy|libs\/Cesium|libs\/html2canvas)\/|webpack\.js|utils\/(openlayers|leaflet)/,
- enforce: "post",
- use: [
- {
- loader: 'istanbul-instrumenter-loader',
- options: { esModules: true }
- }
- ]
- }, ...testConfig.webpack.module.rules];
open it in your browser
Whenever you run tests wth this command “npm test” a report is generated inside coverage/report-html/index.html
this is helpful to know which are the parts less covered by unit tests
Q: What if you have a big list if tests and you want to check only the one you are adding?
A: Use npm run test:watch
in combination with edits to tests.webpack.js
like this
- var context = require.context('./js', true, /-test\.jsx?$/);
+ var context = require.context('./js/reducers', true, /-test\.jsx?$/);
in this way when you will run npm run test:watch
you will tests only files inside this folder
with the following you can also specify which is the filename of the test file you are gonna test, this will speed up things
- var context = require.context('./js/reducers', true, /-test\.jsx?$/);
+ var context = require.context('./js/reducers', true, /geotour-test\.jsx?$/);
You can also add .skip or .only to the it or describe function to respectively skip tests or chose only which one will be run by the test runner
Conditional rendering¶
Sometimes happens that you want a plugin to be rendered based on a particular situation, like the present of an authenticated user or the url path you are in Some other times you want the opposite to hide a plugin based on a particular situation.
For this you can add to your plugin definition the following
- export default createPlugin("GeoTour", {
+ export default createPlugin("GeoTour", {
+ options: {
+ disablePluginIf: "{state('userrole') !== 'ADMIN'}"
+ },
This will automatically disable this plugin when you user is not an administrator. This is useful when you want to create a plugin that only administrators can use.
Note
you cannot uses everything inside this state function, only the state parts of the state defined in the configs/localconfig.json
at monitoredState object
Customize linter rules¶
Linter configuration of MapStore is stored in a specific folder and is published in npm
You can check the eslintConfig in package.json
to see how it’s included
for example if you hate double quotes you can add the following to disable the related lint rules
"eslintConfig": {
"extends": [
"@mapstore/eslint-config-mapstore"
],
"parserOptions": {
"babelOptions": {
"configFile": "./MapStore2/build/babel.config.js"
}
},
+ "rules": {
+ "quotes": [ "error","double" ]
+ }
},