Index: TFP-WebServer/MarkersMod/ModInfo.xml
===================================================================
--- TFP-WebServer/MarkersMod/ModInfo.xml	(revision 469)
+++ TFP-WebServer/MarkersMod/ModInfo.xml	(revision 485)
@@ -5,5 +5,5 @@
 	<Description value="Allows placing custom markers on the web map" />
 	<Author value="Catalysm and Alloc" />
-	<Version value="21.1.16.4" />
+	<Version value="22.0.1.0" />
 	<Website value="" />
 </xml>
Index: TFP-WebServer/MarkersMod/WebMod/bundle.js
===================================================================
--- TFP-WebServer/MarkersMod/WebMod/bundle.js	(revision 469)
+++ TFP-WebServer/MarkersMod/WebMod/bundle.js	(revision 485)
@@ -1,1 +1,226 @@
-(()=>{"use strict";class e{constructor(e){this.http=e}async getMarkers(){return await this.http.get("/api/markers")}async getMarker(e){return await this.http.get(`/api/markers/${e}`)}async deleteMarker(e){return await this.http.delete(`/api/markers/${e}`)}async updateMarker(e,{x:t,y:a,icon:r}){return await this.http.put(`/api/markers/${e}`,{x:parseInt(t,10),y:parseInt(a,10),icon:r})}async createMarker({x:e,y:t,icon:a}){return await this.http.post("/api/markers",{x:parseInt(e,10),y:parseInt(t,10),icon:a})}}const t="TFP_MarkersExample",a={about:"This mod manages markers on a Leaflet map.",routes:{},settings:{"Map markers":function({React:t,styled:a,HTTP:r,EditableTable:n,checkPermission:i}){const[l,o]=t.useState([]),[s,c]=t.useState(!0),[u,m]=t.useState({x:1,y:2}),d=new e(r),p=i({module:"webapi.Markers",method:"PUT"}),k=[{field:"id",filter:"agTextColumnFilter",checkboxSelection:!0,width:300,flex:0},{field:"x",filter:"agNumberColumnFilter",editable:p,cellEditorPopup:!0,width:50,flex:0},{field:"y",filter:"agNumberColumnFilter",editable:p,cellEditorPopup:!0,width:50,flex:0},{field:"icon",filter:"agTextColumnFilter",editable:p,cellEditorPopup:!0,flex:1}],y=()=>i({module:"webapi.Markers",method:"POST"})?t.createElement("form",{id:"markers-form",onSubmit:w},t.createElement("strong",null,"Create marker"),t.createElement("div",null,t.createElement("label",{htmlFor:"input-x"},"X"),t.createElement("input",{key:"markers-x",id:"input-x",value:u.x,onChange:e=>f(e,"x")})),t.createElement("div",null,t.createElement("label",{htmlFor:"input-y"},"Y"),t.createElement("input",{key:"markers-y",id:"input-y",value:u.y,onChange:e=>f(e,"y")})),t.createElement("button",{type:"submit"},"Create")):null;async function h(){const e=await d.getMarkers();o(e)}function f(e,t){switch(t){case"x":m({...u,x:e.target.value});break;case"y":m({...u,y:e.target.value})}}async function w(e){const{x:t,y:a}=u;e.preventDefault(),await d.createMarker({x:t,y:a}),c(!s)}return t.useEffect((()=>{h()}),[s]),t.createElement(t.Fragment,null,t.createElement(y,null),t.createElement(n,{columnDef:k,rowData:l,reloadFn:h,editRowFn:async function({data:e,newValue:t,column:a}){if(!i({module:"webapi.Markers",method:"PUT"}))return;const r=a.colId;d.updateMarker(e.id,{...e,[r]:t})},deleteRowFn:async function(e){i({module:"webapi.Markers",method:"DELETE"})&&(d.deleteMarker(e.id),c(!s))}}))}},mapComponents:[function({map:t,React:a,HTTP:r,checkPermission:n,LayerGroup:i,LayersControl:l,Marker:o,HideBasedOnAuth:s,L:c}){const u=new e(r),[m,d]=a.useState([]);return a.useEffect((()=>{!async function(){if(n({module:"webapi.Markers",method:"GET"})){const e=(await u.getMarkers()).map((e=>{const{x:t,y:r,icon:n="https://upload.wikimedia.org/wikipedia/commons/thumb/1/11/Blue_question_mark_icon.svg/1200px-Blue_question_mark_icon.svg.png"}=e,i=c.icon({iconSize:[25,25],iconUrl:n});return a.createElement(o,{key:e.id,icon:i,position:{lat:t,lng:r}})}));d(e)}}()}),[]),a.createElement(s,{requiredPermission:{module:"webapi.Markers",method:"GET"}},a.createElement(l.Overlay,{name:"Markers"},a.createElement(i,null,m)))}]};window[t]=a,window.dispatchEvent(new Event(`mod:${t}:ready`))})();
+/******/ (() => { // webpackBootstrap
+/******/ 	"use strict";
+var __webpack_exports__ = {};
+
+;// CONCATENATED MODULE: ../mods/markers/mapComponent.js
+/* eslint-disable react/prop-types */
+
+function MarkersComponent({
+  map,
+  React,
+  HTTP,
+  checkPermission,
+  useQuery,
+  LayerGroup,
+  LayersControl,
+  Marker,
+  HideBasedOnAuth,
+  L
+}) {
+  const [markers, setMarkers] = React.useState([]);
+  const {
+    data
+  } = useQuery('markers', async () => HTTP.get('/api/markers'));
+  React.useEffect(() => {
+    async function getMarkers() {
+      if (checkPermission({
+        module: 'webapi.Markers',
+        method: 'GET'
+      })) {
+        const markerComponents = data.map(marker => {
+          const {
+            x,
+            y,
+            icon = 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/11/Blue_question_mark_icon.svg/1200px-Blue_question_mark_icon.svg.png'
+          } = marker;
+          const iconComponent = L.icon({
+            iconSize: [25, 25],
+            iconUrl: icon
+          });
+          return /*#__PURE__*/React.createElement(Marker, {
+            key: marker.id,
+            icon: iconComponent,
+            position: {
+              lat: x,
+              lng: y
+            }
+          });
+        });
+        setMarkers(markerComponents);
+      }
+    }
+    getMarkers();
+  }, [data]);
+  return /*#__PURE__*/React.createElement(HideBasedOnAuth, {
+    requiredPermission: {
+      module: 'webapi.Markers',
+      method: 'GET'
+    }
+  }, /*#__PURE__*/React.createElement(LayersControl.Overlay, {
+    name: "Markers"
+  }, /*#__PURE__*/React.createElement(LayerGroup, null, markers)));
+}
+;// CONCATENATED MODULE: ../mods/markers/settings.js
+function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
+/* eslint-disable react/prop-types */
+
+function MarkersSettings({
+  React,
+  styled,
+  HTTP,
+  EditableTable,
+  FormElements,
+  TfpForm,
+  useForm,
+  useQuery,
+  useMutation,
+  checkPermission
+}) {
+  const {
+    register,
+    handleSubmit,
+    formState: {
+      errors
+    }
+  } = useForm();
+  const {
+    data,
+    refetch
+  } = useQuery('markers', async () => HTTP.get('/api/markers'));
+  const {
+    mutate: createMarker
+  } = useMutation('createMarker', data => HTTP.post('/api/markers', {
+    x: parseInt(data.x, 10),
+    y: parseInt(data.y, 10)
+  }), {
+    onSuccess: () => refetch()
+  });
+  const {
+    mutate: deleteMarker
+  } = useMutation('deleteMarker', id => HTTP.delete(`/api/markers/${id}`), {
+    onSuccess: () => refetch()
+  });
+  const {
+    mutate: updateMarker
+  } = useMutation('updateMarker', data => HTTP.put(`/api/markers/${data.id}`, {
+    x: parseInt(data.x, 10),
+    y: parseInt(data.y, 10),
+    icon: data.icon
+  }), {
+    onSuccess: () => refetch()
+  });
+  const canEditRows = checkPermission({
+    module: 'webapi.Markers',
+    method: 'PUT'
+  });
+  const columnDef = [{
+    field: 'id',
+    filter: 'agTextColumnFilter',
+    checkboxSelection: true,
+    width: 300,
+    flex: 0
+  }, {
+    field: 'x',
+    filter: 'agNumberColumnFilter',
+    editable: canEditRows,
+    cellEditorPopup: true,
+    width: 50,
+    flex: 0
+  }, {
+    field: 'y',
+    filter: 'agNumberColumnFilter',
+    editable: canEditRows,
+    cellEditorPopup: true,
+    width: 50,
+    flex: 0
+  }, {
+    field: 'icon',
+    filter: 'agTextColumnFilter',
+    editable: canEditRows,
+    cellEditorPopup: true,
+    flex: 1
+  }];
+  const CreateMarker = () => {
+    if (!checkPermission({
+      module: 'webapi.Markers',
+      method: 'POST'
+    })) {
+      return null;
+    }
+    return /*#__PURE__*/React.createElement(TfpForm, {
+      id: "markers-form",
+      handleSubmit: handleSubmit(createMarker),
+      error: errors
+    }, /*#__PURE__*/React.createElement(FormElements.StyledFormItem, null, /*#__PURE__*/React.createElement(FormElements.FormLabel, {
+      htmlFor: "input-x"
+    }, "X"), /*#__PURE__*/React.createElement(FormElements.FormInput, _extends({
+      key: "x",
+      id: "input-x"
+    }, register('x', {
+      required: true
+    })))), /*#__PURE__*/React.createElement(FormElements.StyledFormItem, null, /*#__PURE__*/React.createElement(FormElements.FormLabel, {
+      htmlFor: "input-y"
+    }, "Y"), /*#__PURE__*/React.createElement(FormElements.FormInput, _extends({
+      key: "y",
+      id: "input-y"
+    }, register('y', {
+      required: true
+    })))));
+  };
+  async function cellEdited({
+    data,
+    newValue,
+    column
+  }) {
+    if (!checkPermission({
+      module: 'webapi.Markers',
+      method: 'PUT'
+    })) {
+      return;
+    }
+    const changedField = column.colId;
+    updateMarker({
+      ...data,
+      [changedField]: newValue
+    });
+  }
+  async function cellDeleted(row) {
+    if (!checkPermission({
+      module: 'webapi.Markers',
+      method: 'DELETE'
+    })) {
+      return;
+    }
+    deleteMarker(row.id);
+  }
+  return /*#__PURE__*/React.createElement("div", {
+    style: {
+      flexDirection: 'row',
+      height: '80vh'
+    }
+  }, /*#__PURE__*/React.createElement(CreateMarker, null), /*#__PURE__*/React.createElement(EditableTable, {
+    columnDef: columnDef,
+    rowData: data,
+    reloadFn: refetch,
+    editRowFn: cellEdited,
+    deleteRowFn: cellDeleted,
+    height: '90%'
+  }));
+}
+;// CONCATENATED MODULE: ../mods/markers/index.js
+/* eslint-disable react/prop-types */
+
+
+const modId = 'TFP_MarkersExample';
+const Markers = {
+  about: `This mod manages markers on a Leaflet map.`,
+  routes: {},
+  settings: {
+    'Map markers': MarkersSettings
+  },
+  mapComponents: [MarkersComponent]
+};
+window[modId] = Markers;
+window.dispatchEvent(new Event(`mod:${modId}:ready`));
+/******/ })()
+;
