Migration of jsx files to tsx 3 (#851)

This is in continue to:
- #850
- #848

The last files should be converted as part of this PR, there are only a
handful left.
This commit is contained in:
Harel M
2023-12-25 15:48:46 +02:00
committed by GitHub
parent 974dd7bfd9
commit 656264f2bc
54 changed files with 583 additions and 385 deletions

54
package-lock.json generated
View File

@@ -71,10 +71,15 @@
"@types/color": "^3.0.6", "@types/color": "^3.0.6",
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@types/json-to-ast": "^2.1.4",
"@types/lodash.capitalize": "^4.2.9", "@types/lodash.capitalize": "^4.2.9",
"@types/lodash.clamp": "^4.0.9",
"@types/lodash.clonedeep": "^4.5.9",
"@types/lodash.get": "^4.4.9",
"@types/lodash.isequal": "^4.5.8", "@types/lodash.isequal": "^4.5.8",
"@types/lodash.throttle": "^4.1.9", "@types/lodash.throttle": "^4.1.9",
"@types/react": "^16.14.52", "@types/react": "^16.14.52",
"@types/react-aria-menubutton": "^6.2.13",
"@types/react-aria-modal": "^4.0.9", "@types/react-aria-modal": "^4.0.9",
"@types/react-autocomplete": "^1.8.9", "@types/react-autocomplete": "^1.8.9",
"@types/react-collapse": "^5.0.4", "@types/react-collapse": "^5.0.4",
@@ -82,6 +87,7 @@
"@types/react-dom": "^16.9.24", "@types/react-dom": "^16.9.24",
"@types/react-file-reader-input": "^2.0.4", "@types/react-file-reader-input": "^2.0.4",
"@types/react-icon-base": "^2.1.6", "@types/react-icon-base": "^2.1.6",
"@types/string-hash": "^1.1.3",
"@types/uuid": "^9.0.7", "@types/uuid": "^9.0.7",
"@vitejs/plugin-react": "^4.2.0", "@vitejs/plugin-react": "^4.2.0",
"cors": "^2.8.5", "cors": "^2.8.5",
@@ -4746,6 +4752,12 @@
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
"dev": true "dev": true
}, },
"node_modules/@types/json-to-ast": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@types/json-to-ast/-/json-to-ast-2.1.4.tgz",
"integrity": "sha512-131wOmuwDg8ypYCSQ437bGdP+K2lJ8GJUu+ng4iQQxAc3irRnb7mGHbexsPChBcKWLctTR9V5LJdX5A8WWk44A==",
"dev": true
},
"node_modules/@types/lodash": { "node_modules/@types/lodash": {
"version": "4.14.202", "version": "4.14.202",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz",
@@ -4761,6 +4773,33 @@
"@types/lodash": "*" "@types/lodash": "*"
} }
}, },
"node_modules/@types/lodash.clamp": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/lodash.clamp/-/lodash.clamp-4.0.9.tgz",
"integrity": "sha512-t+hBIPHXyBVYkl0KEAEchOJwBrG8czt3E7r5fdpwMRrn3g+hkRzw6cjzWl+nJg3Z2QqRaQLt+W2n4ikwGr1u2g==",
"dev": true,
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/lodash.clonedeep": {
"version": "4.5.9",
"resolved": "https://registry.npmjs.org/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.9.tgz",
"integrity": "sha512-19429mWC+FyaAhOLzsS8kZUsI+/GmBAQ0HFiCPsKGU+7pBXOQWhyrY6xNNDwUSX8SMZMJvuFVMF9O5dQOlQK9Q==",
"dev": true,
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/lodash.get": {
"version": "4.4.9",
"resolved": "https://registry.npmjs.org/@types/lodash.get/-/lodash.get-4.4.9.tgz",
"integrity": "sha512-J5dvW98sxmGnamqf+/aLP87PYXyrha9xIgc2ZlHl6OHMFR2Ejdxep50QfU0abO1+CH6+ugx+8wEUN1toImAinA==",
"dev": true,
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/lodash.isequal": { "node_modules/@types/lodash.isequal": {
"version": "4.5.8", "version": "4.5.8",
"resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.8.tgz", "resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.8.tgz",
@@ -4889,6 +4928,15 @@
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
}, },
"node_modules/@types/react-aria-menubutton": {
"version": "6.2.13",
"resolved": "https://registry.npmjs.org/@types/react-aria-menubutton/-/react-aria-menubutton-6.2.13.tgz",
"integrity": "sha512-olSjeIzNzn0KrbShOmBwchHS++khDXBYFTO2U802o8LDHANLms7zUsJhdecfqFpwdFMHxFiMMlCn2nJNCEHWlQ==",
"dev": true,
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/react-aria-modal": { "node_modules/@types/react-aria-modal": {
"version": "4.0.9", "version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/react-aria-modal/-/react-aria-modal-4.0.9.tgz", "resolved": "https://registry.npmjs.org/@types/react-aria-modal/-/react-aria-modal-4.0.9.tgz",
@@ -5013,6 +5061,12 @@
"integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==", "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==",
"dev": true "dev": true
}, },
"node_modules/@types/string-hash": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@types/string-hash/-/string-hash-1.1.3.tgz",
"integrity": "sha512-p6skq756fJWiA59g2Uss+cMl6tpoDGuCBuxG0SI1t0NwJmYOU66LAMS6QiCgu7cUh3/hYCaMl5phcCW1JP5wOA==",
"dev": true
},
"node_modules/@types/tern": { "node_modules/@types/tern": {
"version": "0.23.9", "version": "0.23.9",
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",

View File

@@ -100,10 +100,15 @@
"@types/color": "^3.0.6", "@types/color": "^3.0.6",
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@types/json-to-ast": "^2.1.4",
"@types/lodash.capitalize": "^4.2.9", "@types/lodash.capitalize": "^4.2.9",
"@types/lodash.clamp": "^4.0.9",
"@types/lodash.clonedeep": "^4.5.9",
"@types/lodash.get": "^4.4.9",
"@types/lodash.isequal": "^4.5.8", "@types/lodash.isequal": "^4.5.8",
"@types/lodash.throttle": "^4.1.9", "@types/lodash.throttle": "^4.1.9",
"@types/react": "^16.14.52", "@types/react": "^16.14.52",
"@types/react-aria-menubutton": "^6.2.13",
"@types/react-aria-modal": "^4.0.9", "@types/react-aria-modal": "^4.0.9",
"@types/react-autocomplete": "^1.8.9", "@types/react-autocomplete": "^1.8.9",
"@types/react-collapse": "^5.0.4", "@types/react-collapse": "^5.0.4",
@@ -111,6 +116,7 @@
"@types/react-dom": "^16.9.24", "@types/react-dom": "^16.9.24",
"@types/react-file-reader-input": "^2.0.4", "@types/react-file-reader-input": "^2.0.4",
"@types/react-icon-base": "^2.1.6", "@types/react-icon-base": "^2.1.6",
"@types/string-hash": "^1.1.3",
"@types/uuid": "^9.0.7", "@types/uuid": "^9.0.7",
"@vitejs/plugin-react": "^4.2.0", "@vitejs/plugin-react": "^4.2.0",
"cors": "^2.8.5", "cors": "^2.8.5",

View File

@@ -1,3 +1,4 @@
// @ts-ignore - this can be easily replaced with arrow functions
import autoBind from 'react-autobind'; import autoBind from 'react-autobind';
import React from 'react' import React from 'react'
import cloneDeep from 'lodash.clonedeep' import cloneDeep from 'lodash.clonedeep'
@@ -6,14 +7,15 @@ import buffer from 'buffer'
import get from 'lodash.get' import get from 'lodash.get'
import {unset} from 'lodash' import {unset} from 'lodash'
import {arrayMoveMutable} from 'array-move' import {arrayMoveMutable} from 'array-move'
import url from 'url'
import hash from "string-hash"; import hash from "string-hash";
import {Map, LayerSpecification, StyleSpecification, ValidationError, SourceSpecification} from 'maplibre-gl'
import {latest, validate} from '@maplibre/maplibre-gl-style-spec'
import MapMaplibreGl from './MapMaplibreGl' import MapMaplibreGl from './MapMaplibreGl'
import MapOpenLayers from './MapOpenLayers' import MapOpenLayers from './MapOpenLayers'
import LayerList from './LayerList' import LayerList from './LayerList'
import LayerEditor from './LayerEditor' import LayerEditor from './LayerEditor'
import AppToolbar from './AppToolbar' import AppToolbar, { MapState } from './AppToolbar'
import AppLayout from './AppLayout' import AppLayout from './AppLayout'
import MessagePanel from './AppMessagePanel' import MessagePanel from './AppMessagePanel'
@@ -25,8 +27,7 @@ import ModalShortcuts from './ModalShortcuts'
import ModalSurvey from './ModalSurvey' import ModalSurvey from './ModalSurvey'
import ModalDebug from './ModalDebug' import ModalDebug from './ModalDebug'
import { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata' import {downloadGlyphsMetadata, downloadSpriteMetadata} from '../libs/metadata'
import {latest, validate} from '@maplibre/maplibre-gl-style-spec'
import style from '../libs/style' import style from '../libs/style'
import { initialStyleUrl, loadStyleUrl, removeStyleQuerystring } from '../libs/urlopen' import { initialStyleUrl, loadStyleUrl, removeStyleQuerystring } from '../libs/urlopen'
import { undoMessages, redoMessages } from '../libs/diffmessage' import { undoMessages, redoMessages } from '../libs/diffmessage'
@@ -37,12 +38,13 @@ import LayerWatcher from '../libs/layerwatcher'
import tokens from '../config/tokens.json' import tokens from '../config/tokens.json'
import isEqual from 'lodash.isequal' import isEqual from 'lodash.isequal'
import Debug from '../libs/debug' import Debug from '../libs/debug'
import {formatLayerId} from '../util/format'; import { SortEnd } from 'react-sortable-hoc';
import { MapOptions } from 'maplibre-gl';
// Buffer must be defined globally for @maplibre/maplibre-gl-style-spec validate() function to succeed. // Buffer must be defined globally for @maplibre/maplibre-gl-style-spec validate() function to succeed.
window.Buffer = buffer.Buffer; window.Buffer = buffer.Buffer;
function setFetchAccessToken(url, mapStyle) { function setFetchAccessToken(url: string, mapStyle: StyleSpecification) {
const matchesTilehosting = url.match(/\.tilehosting\.com/); const matchesTilehosting = url.match(/\.tilehosting\.com/);
const matchesMaptiler = url.match(/\.maptiler\.com/); const matchesMaptiler = url.match(/\.maptiler\.com/);
const matchesThunderforest = url.match(/\.thunderforest\.com/); const matchesThunderforest = url.match(/\.thunderforest\.com/);
@@ -63,7 +65,7 @@ function setFetchAccessToken(url, mapStyle) {
} }
} }
function updateRootSpec(spec, fieldName, newValues) { function updateRootSpec(spec: any, fieldName: string, newValues: any) {
return { return {
...spec, ...spec,
$root: { $root: {
@@ -76,15 +78,75 @@ function updateRootSpec(spec, fieldName, newValues) {
} }
} }
export default class App extends React.Component { type OnStyleChangedOpts = {
constructor(props) { save?: boolean
addRevision?: boolean
initialLoad?: boolean
}
type MappedErrors = {
message: string
parsed?: {
type: string
data: {
index: number
key: string
message: string
}
}
}
type AppState = {
errors: MappedErrors[],
infos: string[],
mapStyle: StyleSpecification & {id: string},
dirtyMapStyle?: StyleSpecification,
selectedLayerIndex: number,
selectedLayerOriginalId?: string,
sources: {[key: string]: SourceSpecification},
vectorLayers: {},
spec: any,
mapView: {
zoom: number,
center: {
lng: number,
lat: number,
},
},
maplibreGlDebugOptions: Partial<MapOptions> & {
showTileBoundaries: boolean,
showCollisionBoxes: boolean,
showOverdrawInspector: boolean,
},
openlayersDebugOptions: {
debugToolbox: boolean,
},
mapState: MapState
isOpen: {
settings: boolean
sources: boolean
open: boolean
shortcuts: boolean
export: boolean
survey: boolean
debug: boolean
}
}
export default class App extends React.Component<any, AppState> {
revisionStore: RevisionStore;
styleStore: StyleStore | ApiStyleStore;
layerWatcher: LayerWatcher;
shortcutEl: ModalShortcuts | null = null;
constructor(props: any) {
super(props) super(props)
autoBind(this); autoBind(this);
this.revisionStore = new RevisionStore() this.revisionStore = new RevisionStore()
const params = new URLSearchParams(window.location.search.substring(1)) const params = new URLSearchParams(window.location.search.substring(1))
let port = params.get("localport") let port = params.get("localport")
if (port == null && (window.location.port != 80 && window.location.port != 443)) { if (port == null && (window.location.port !== "80" && window.location.port !== "443")) {
port = window.location.port port = window.location.port
} }
this.styleStore = new ApiStyleStore({ this.styleStore = new ApiStyleStore({
@@ -136,7 +198,7 @@ export default class App extends React.Component {
{ {
key: "m", key: "m",
handler: () => { handler: () => {
document.querySelector(".maplibregl-canvas").focus(); (document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus();
} }
}, },
{ {
@@ -149,7 +211,7 @@ export default class App extends React.Component {
document.body.addEventListener("keyup", (e) => { document.body.addEventListener("keyup", (e) => {
if(e.key === "Escape") { if(e.key === "Escape") {
e.target.blur(); (e.target as HTMLElement).blur();
document.body.focus(); document.body.focus();
} }
else if(this.state.isOpen.shortcuts || document.activeElement === document.body) { else if(this.state.isOpen.shortcuts || document.activeElement === document.body) {
@@ -159,7 +221,7 @@ export default class App extends React.Component {
if(shortcut) { if(shortcut) {
this.setModal("shortcuts", false); this.setModal("shortcuts", false);
shortcut.handler(e); shortcut.handler();
} }
} }
}) })
@@ -192,8 +254,6 @@ export default class App extends React.Component {
Debug.set("maputnik", "styleStore", this.styleStore); Debug.set("maputnik", "styleStore", this.styleStore);
} }
const queryObj = url.parse(window.location.href, true).query;
this.state = { this.state = {
errors: [], errors: [],
infos: [], infos: [],
@@ -235,25 +295,25 @@ export default class App extends React.Component {
}) })
} }
handleKeyPress = (e) => { handleKeyPress = (e: KeyboardEvent) => {
if(navigator.platform.toUpperCase().indexOf('MAC') >= 0) { if(navigator.platform.toUpperCase().indexOf('MAC') >= 0) {
if(e.metaKey && e.shiftKey && e.keyCode === 90) { if(e.metaKey && e.shiftKey && e.keyCode === 90) {
e.preventDefault(); e.preventDefault();
this.onRedo(e); this.onRedo();
} }
else if(e.metaKey && e.keyCode === 90) { else if(e.metaKey && e.keyCode === 90) {
e.preventDefault(); e.preventDefault();
this.onUndo(e); this.onUndo();
} }
} }
else { else {
if(e.ctrlKey && e.keyCode === 90) { if(e.ctrlKey && e.keyCode === 90) {
e.preventDefault(); e.preventDefault();
this.onUndo(e); this.onUndo();
} }
else if(e.ctrlKey && e.keyCode === 89) { else if(e.ctrlKey && e.keyCode === 89) {
e.preventDefault(); e.preventDefault();
this.onRedo(e); this.onRedo();
} }
} }
} }
@@ -266,12 +326,12 @@ export default class App extends React.Component {
window.removeEventListener("keydown", this.handleKeyPress); window.removeEventListener("keydown", this.handleKeyPress);
} }
saveStyle(snapshotStyle) { saveStyle(snapshotStyle: StyleSpecification & {id: string}) {
this.styleStore.save(snapshotStyle) this.styleStore.save(snapshotStyle)
} }
updateFonts(urlTemplate) { updateFonts(urlTemplate: string) {
const metadata = this.state.mapStyle.metadata || {} const metadata: {[key: string]: string} = this.state.mapStyle.metadata || {} as any
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
let glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate; let glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate;
@@ -280,13 +340,13 @@ export default class App extends React.Component {
}) })
} }
updateIcons(baseUrl) { updateIcons(baseUrl: string) {
downloadSpriteMetadata(baseUrl, icons => { downloadSpriteMetadata(baseUrl, icons => {
this.setState({ spec: updateRootSpec(this.state.spec, 'sprite', icons)}) this.setState({ spec: updateRootSpec(this.state.spec, 'sprite', icons)})
}) })
} }
onChangeMetadataProperty = (property, value) => { onChangeMetadataProperty = (property: string, value: any) => {
// If we're changing renderer reset the map state. // If we're changing renderer reset the map state.
if ( if (
property === 'maputnik:renderer' && property === 'maputnik:renderer' &&
@@ -300,14 +360,14 @@ export default class App extends React.Component {
const changedStyle = { const changedStyle = {
...this.state.mapStyle, ...this.state.mapStyle,
metadata: { metadata: {
...this.state.mapStyle.metadata, ...(this.state.mapStyle as any).metadata,
[property]: value [property]: value
} }
} }
this.onStyleChanged(changedStyle) this.onStyleChanged(changedStyle)
} }
onStyleChanged = (newStyle, opts={}) => { onStyleChanged = (newStyle: StyleSpecification & {id: string}, opts: OnStyleChangedOpts={}) => {
opts = { opts = {
save: true, save: true,
addRevision: true, addRevision: true,
@@ -319,16 +379,16 @@ export default class App extends React.Component {
this.getInitialStateFromUrl(newStyle); this.getInitialStateFromUrl(newStyle);
} }
const errors = validate(newStyle, latest) || []; // This "any" can be removed in latest version of maplibre where maplibre re-exported types from style-spec
const errors = validate(newStyle as any, latest) || [];
// The validate function doesn't give us errors for duplicate error with // The validate function doesn't give us errors for duplicate error with
// empty string for layer.id, manually deal with that here. // empty string for layer.id, manually deal with that here.
const layerErrors = []; const layerErrors: (Error | ValidationError)[] = [];
if (newStyle && newStyle.layers) { if (newStyle && newStyle.layers) {
const foundLayers = new Map(); const foundLayers = new global.Map();
newStyle.layers.forEach((layer, index) => { newStyle.layers.forEach((layer, index) => {
if (layer.id === "" && foundLayers.has(layer.id)) { if (layer.id === "" && foundLayers.has(layer.id)) {
const message = `Duplicate layer: ${formatLayerId(layer.id)}`;
const error = new Error( const error = new Error(
`layers[${index}]: duplicate layer id [empty_string], previously used` `layers[${index}]: duplicate layer id [empty_string], previously used`
); );
@@ -342,7 +402,7 @@ export default class App extends React.Component {
// Special case: Duplicate layer id // Special case: Duplicate layer id
const dupMatch = error.message.match(/layers\[(\d+)\]: (duplicate layer id "?(.*)"?, previously used)/); const dupMatch = error.message.match(/layers\[(\d+)\]: (duplicate layer id "?(.*)"?, previously used)/);
if (dupMatch) { if (dupMatch) {
const [matchStr, index, message] = dupMatch; const [_matchStr, index, message] = dupMatch;
return { return {
message: error.message, message: error.message,
parsed: { parsed: {
@@ -359,7 +419,7 @@ export default class App extends React.Component {
// Special case: Invalid source // Special case: Invalid source
const invalidSourceMatch = error.message.match(/layers\[(\d+)\]: (source "(?:.*)" not found)/); const invalidSourceMatch = error.message.match(/layers\[(\d+)\]: (source "(?:.*)" not found)/);
if (invalidSourceMatch) { if (invalidSourceMatch) {
const [matchStr, index, message] = invalidSourceMatch; const [_matchStr, index, message] = invalidSourceMatch;
return { return {
message: error.message, message: error.message,
parsed: { parsed: {
@@ -375,7 +435,7 @@ export default class App extends React.Component {
const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/); const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/);
if (layerMatch) { if (layerMatch) {
const [matchStr, index, group, property, message] = layerMatch; const [_matchStr, index, group, property, message] = layerMatch;
const key = (group && property) ? [group, property].join(".") : property; const key = (group && property) ? [group, property].join(".") : property;
return { return {
message: error.message, message: error.message,
@@ -396,7 +456,7 @@ export default class App extends React.Component {
} }
}); });
let dirtyMapStyle = undefined; let dirtyMapStyle: StyleSpecification | undefined = undefined;
if (errors.length > 0) { if (errors.length > 0) {
dirtyMapStyle = cloneDeep(newStyle); dirtyMapStyle = cloneDeep(newStyle);
@@ -406,7 +466,7 @@ export default class App extends React.Component {
try { try {
const objPath = message.split(":")[0]; const objPath = message.split(":")[0];
// Errors can be deply nested for example 'layers[0].filter[1][1][0]' we only care upto the property 'layers[0].filter' // Errors can be deply nested for example 'layers[0].filter[1][1][0]' we only care upto the property 'layers[0].filter'
const unsetPath = objPath.match(/^\S+?\[\d+\]\.[^\[]+/)[0]; const unsetPath = objPath.match(/^\S+?\[\d+\]\.[^\[]+/)![0];
unset(dirtyMapStyle, unsetPath); unset(dirtyMapStyle, unsetPath);
} }
catch (err) { catch (err) {
@@ -417,17 +477,17 @@ export default class App extends React.Component {
} }
if(newStyle.glyphs !== this.state.mapStyle.glyphs) { if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
this.updateFonts(newStyle.glyphs) this.updateFonts(newStyle.glyphs as string)
} }
if(newStyle.sprite !== this.state.mapStyle.sprite) { if(newStyle.sprite !== this.state.mapStyle.sprite) {
this.updateIcons(newStyle.sprite) this.updateIcons(newStyle.sprite as string)
} }
if (opts.addRevision) { if (opts.addRevision) {
this.revisionStore.addRevision(newStyle); this.revisionStore.addRevision(newStyle);
} }
if (opts.save) { if (opts.save) {
this.saveStyle(newStyle); this.saveStyle(newStyle as StyleSpecification & {id: string});
} }
this.setState({ this.setState({
@@ -460,7 +520,7 @@ export default class App extends React.Component {
}) })
} }
onMoveLayer = (move) => { onMoveLayer = (move: SortEnd) => {
let { oldIndex, newIndex } = move; let { oldIndex, newIndex } = move;
let layers = this.state.mapStyle.layers; let layers = this.state.mapStyle.layers;
oldIndex = clamp(oldIndex, 0, layers.length-1); oldIndex = clamp(oldIndex, 0, layers.length-1);
@@ -478,7 +538,7 @@ export default class App extends React.Component {
this.onLayersChange(layers); this.onLayersChange(layers);
} }
onLayersChange = (changedLayers) => { onLayersChange = (changedLayers: LayerSpecification[]) => {
const changedStyle = { const changedStyle = {
...this.state.mapStyle, ...this.state.mapStyle,
layers: changedLayers layers: changedLayers
@@ -486,14 +546,14 @@ export default class App extends React.Component {
this.onStyleChanged(changedStyle) this.onStyleChanged(changedStyle)
} }
onLayerDestroy = (index) => { onLayerDestroy = (index: number) => {
let layers = this.state.mapStyle.layers; let layers = this.state.mapStyle.layers;
const remainingLayers = layers.slice(0); const remainingLayers = layers.slice(0);
remainingLayers.splice(index, 1); remainingLayers.splice(index, 1);
this.onLayersChange(remainingLayers); this.onLayersChange(remainingLayers);
} }
onLayerCopy = (index) => { onLayerCopy = (index: number) => {
let layers = this.state.mapStyle.layers; let layers = this.state.mapStyle.layers;
const changedLayers = layers.slice(0) const changedLayers = layers.slice(0)
@@ -503,7 +563,7 @@ export default class App extends React.Component {
this.onLayersChange(changedLayers) this.onLayersChange(changedLayers)
} }
onLayerVisibilityToggle = (index) => { onLayerVisibilityToggle = (index: number) => {
let layers = this.state.mapStyle.layers; let layers = this.state.mapStyle.layers;
const changedLayers = layers.slice(0) const changedLayers = layers.slice(0)
@@ -517,7 +577,7 @@ export default class App extends React.Component {
} }
onLayerIdChange = (index, oldId, newId) => { onLayerIdChange = (index: number, _oldId: string, newId: string) => {
const changedLayers = this.state.mapStyle.layers.slice(0) const changedLayers = this.state.mapStyle.layers.slice(0)
changedLayers[index] = { changedLayers[index] = {
...changedLayers[index], ...changedLayers[index],
@@ -527,26 +587,26 @@ export default class App extends React.Component {
this.onLayersChange(changedLayers) this.onLayersChange(changedLayers)
} }
onLayerChanged = (index, layer) => { onLayerChanged = (index: number, layer: LayerSpecification) => {
const changedLayers = this.state.mapStyle.layers.slice(0) const changedLayers = this.state.mapStyle.layers.slice(0)
changedLayers[index] = layer changedLayers[index] = layer
this.onLayersChange(changedLayers) this.onLayersChange(changedLayers)
} }
setMapState = (newState) => { setMapState = (newState: MapState) => {
this.setState({ this.setState({
mapState: newState mapState: newState
}, this.setStateInUrl); }, this.setStateInUrl);
} }
setDefaultValues = (styleObj) => { setDefaultValues = (styleObj: StyleSpecification & {id: string}) => {
const metadata = styleObj.metadata || {} const metadata: {[key: string]: string} = styleObj.metadata || {} as any
if(metadata['maputnik:renderer'] === undefined) { if(metadata['maputnik:renderer'] === undefined) {
const changedStyle = { const changedStyle = {
...styleObj, ...styleObj,
metadata: { metadata: {
...styleObj.metadata, ...styleObj.metadata as any,
'maputnik:renderer': 'mlgljs' 'maputnik:renderer': 'mlgljs'
} }
} }
@@ -556,13 +616,13 @@ export default class App extends React.Component {
} }
} }
openStyle = (styleObj) => { openStyle = (styleObj: StyleSpecification & {id: string}) => {
styleObj = this.setDefaultValues(styleObj) styleObj = this.setDefaultValues(styleObj)
this.onStyleChanged(styleObj) this.onStyleChanged(styleObj)
} }
fetchSources() { fetchSources() {
const sourceList = {}; const sourceList: {[key: string]: any} = {};
for(let [key, val] of Object.entries(this.state.mapStyle.sources)) { for(let [key, val] of Object.entries(this.state.mapStyle.sources)) {
if( if(
@@ -578,12 +638,12 @@ export default class App extends React.Component {
let url = val.url; let url = val.url;
try { try {
url = setFetchAccessToken(url, this.state.mapStyle) url = setFetchAccessToken(url!, this.state.mapStyle)
} catch(err) { } catch(err) {
console.warn("Failed to setFetchAccessToken: ", err); console.warn("Failed to setFetchAccessToken: ", err);
} }
fetch(url, { fetch(url!, {
mode: 'cors', mode: 'cors',
}) })
.then(response => response.json()) .then(response => response.json())
@@ -599,7 +659,7 @@ export default class App extends React.Component {
}); });
for(let layer of json.vector_layers) { for(let layer of json.vector_layers) {
sources[key].layers.push(layer.id) (sources[key] as any).layers.push(layer.id)
} }
console.debug("Updating source: "+key); console.debug("Updating source: "+key);
@@ -625,11 +685,17 @@ export default class App extends React.Component {
} }
_getRenderer () { _getRenderer () {
const metadata = this.state.mapStyle.metadata || {}; const metadata: {[key:string]: string} = this.state.mapStyle.metadata || {} as any;
return metadata['maputnik:renderer'] || 'mlgljs'; return metadata['maputnik:renderer'] || 'mlgljs';
} }
onMapChange = (mapView) => { onMapChange = (mapView: {
zoom: number,
center: {
lng: number,
lat: number,
},
}) => {
this.setState({ this.setState({
mapView, mapView,
}); });
@@ -637,16 +703,15 @@ export default class App extends React.Component {
mapRenderer() { mapRenderer() {
const {mapStyle, dirtyMapStyle} = this.state; const {mapStyle, dirtyMapStyle} = this.state;
const metadata = this.state.mapStyle.metadata || {};
const mapProps = { const mapProps = {
mapStyle: (dirtyMapStyle || mapStyle), mapStyle: (dirtyMapStyle || mapStyle),
replaceAccessTokens: (mapStyle) => { replaceAccessTokens: (mapStyle: StyleSpecification) => {
return style.replaceAccessTokens(mapStyle, { return style.replaceAccessTokens(mapStyle, {
allowFallback: true allowFallback: true
}); });
}, },
onDataChange: (e) => { onDataChange: (e: {map: Map}) => {
this.layerWatcher.analyzeMap(e.map) this.layerWatcher.analyzeMap(e.map)
this.fetchSources(); this.fetchSources();
}, },
@@ -677,7 +742,7 @@ export default class App extends React.Component {
if(this.state.mapState.match(/^filter-/)) { if(this.state.mapState.match(/^filter-/)) {
filterName = this.state.mapState.replace(/^filter-/, ""); filterName = this.state.mapState.replace(/^filter-/, "");
} }
const elementStyle = {}; const elementStyle: {filter?: string} = {};
if (filterName) { if (filterName) {
elementStyle.filter = `url('#${filterName}')`; elementStyle.filter = `url('#${filterName}')`;
} }
@@ -715,12 +780,12 @@ export default class App extends React.Component {
history.replaceState({selectedLayerIndex}, "Maputnik", url.href); history.replaceState({selectedLayerIndex}, "Maputnik", url.href);
} }
getInitialStateFromUrl = (mapStyle) => { getInitialStateFromUrl = (mapStyle: StyleSpecification) => {
const url = new URL(location.href); const url = new URL(location.href);
const modalParam = url.searchParams.get("modal"); const modalParam = url.searchParams.get("modal");
if (modalParam && modalParam !== "") { if (modalParam && modalParam !== "") {
const modals = modalParam.split(","); const modals = modalParam.split(",");
const modalObj = {}; const modalObj: {[key: string]: boolean} = {};
modals.forEach(modalName => { modals.forEach(modalName => {
modalObj[modalName] = true; modalObj[modalName] = true;
}); });
@@ -735,7 +800,7 @@ export default class App extends React.Component {
const view = url.searchParams.get("view"); const view = url.searchParams.get("view");
if (view && view !== "") { if (view && view !== "") {
this.setMapState(view); this.setMapState(view as MapState);
} }
const path = url.searchParams.get("layer"); const path = url.searchParams.get("layer");
@@ -767,14 +832,14 @@ export default class App extends React.Component {
} }
} }
onLayerSelect = (index) => { onLayerSelect = (index: number) => {
this.setState({ this.setState({
selectedLayerIndex: index, selectedLayerIndex: index,
selectedLayerOriginalId: this.state.mapStyle.layers[index].id, selectedLayerOriginalId: this.state.mapStyle.layers[index].id,
}, this.setStateInUrl); }, this.setStateInUrl);
} }
setModal(modalName, value) { setModal(modalName: keyof AppState["isOpen"], value: boolean) {
if(modalName === 'survey' && value === false) { if(modalName === 'survey' && value === false) {
localStorage.setItem('survey', ''); localStorage.setItem('survey', '');
} }
@@ -787,11 +852,11 @@ export default class App extends React.Component {
}, this.setStateInUrl) }, this.setStateInUrl)
} }
toggleModal(modalName) { toggleModal(modalName: keyof AppState["isOpen"]) {
this.setModal(modalName, !this.state.isOpen[modalName]); this.setModal(modalName, !this.state.isOpen[modalName]);
} }
onChangeOpenlayersDebug = (key, value) => { onChangeOpenlayersDebug = (key: keyof AppState["openlayersDebugOptions"], value: boolean) => {
this.setState({ this.setState({
openlayersDebugOptions: { openlayersDebugOptions: {
...this.state.openlayersDebugOptions, ...this.state.openlayersDebugOptions,
@@ -800,7 +865,7 @@ export default class App extends React.Component {
}); });
} }
onChangeMaplibreGlDebug = (key, value) => { onChangeMaplibreGlDebug = (key: keyof AppState["maplibreGlDebugOptions"], value: any) => {
this.setState({ this.setState({
maplibreGlDebugOptions: { maplibreGlDebugOptions: {
...this.state.maplibreGlDebugOptions, ...this.state.maplibreGlDebugOptions,
@@ -811,8 +876,7 @@ export default class App extends React.Component {
render() { render() {
const layers = this.state.mapStyle.layers || [] const layers = this.state.mapStyle.layers || []
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : null const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : undefined
const metadata = this.state.mapStyle.metadata || {}
const toolbar = <AppToolbar const toolbar = <AppToolbar
renderer={this._getRenderer()} renderer={this._getRenderer()}
@@ -855,7 +919,7 @@ export default class App extends React.Component {
onLayerVisibilityToggle={this.onLayerVisibilityToggle} onLayerVisibilityToggle={this.onLayerVisibilityToggle}
onLayerIdChange={this.onLayerIdChange} onLayerIdChange={this.onLayerIdChange}
errors={this.state.errors} errors={this.state.errors}
/> : null /> : undefined
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
currentLayer={selectedLayer} currentLayer={selectedLayer}
@@ -864,7 +928,7 @@ export default class App extends React.Component {
mapStyle={this.state.mapStyle} mapStyle={this.state.mapStyle}
errors={this.state.errors} errors={this.state.errors}
infos={this.state.infos} infos={this.state.infos}
/> : null /> : undefined
const modals = <div> const modals = <div>
@@ -889,7 +953,6 @@ export default class App extends React.Component {
onChangeMetadataProperty={this.onChangeMetadataProperty} onChangeMetadataProperty={this.onChangeMetadataProperty}
isOpen={this.state.isOpen.settings} isOpen={this.state.isOpen.settings}
onOpenToggle={this.toggleModal.bind(this, 'settings')} onOpenToggle={this.toggleModal.bind(this, 'settings')}
openlayersDebugOptions={this.state.openlayersDebugOptions}
/> />
<ModalExport <ModalExport
mapStyle={this.state.mapStyle} mapStyle={this.state.mapStyle}

View File

@@ -1,13 +1,13 @@
import React from 'react' import React from 'react'
import {formatLayerId} from '../util/format'; import {formatLayerId} from '../util/format';
import { StyleSpecification } from '@maplibre/maplibre-gl-style-spec'; import {LayerSpecification, StyleSpecification} from 'maplibre-gl';
type AppMessagePanelProps = { type AppMessagePanelProps = {
errors?: unknown[] errors?: unknown[]
infos?: unknown[] infos?: unknown[]
mapStyle?: StyleSpecification mapStyle?: StyleSpecification
onLayerSelect?(...args: unknown[]): unknown onLayerSelect?(...args: unknown[]): unknown
currentLayer?: object currentLayer?: LayerSpecification
selectedLayerIndex?: number selectedLayerIndex?: number
}; };

View File

@@ -98,6 +98,8 @@ class ToolbarAction extends React.Component<ToolbarActionProps> {
} }
} }
export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deuteranopia" | "filter-protanopia" | "filter-tritanopia";
type AppToolbarProps = { type AppToolbarProps = {
mapStyle: object mapStyle: object
inspectModeEnabled: boolean inspectModeEnabled: boolean
@@ -108,8 +110,8 @@ type AppToolbarProps = {
sources: object sources: object
children?: React.ReactNode children?: React.ReactNode
onToggleModal(...args: unknown[]): unknown onToggleModal(...args: unknown[]): unknown
onSetMapState(...args: unknown[]): unknown onSetMapState(mapState: MapState): unknown
mapState?: string mapState?: MapState
renderer?: string renderer?: string
}; };
@@ -124,7 +126,7 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
} }
} }
handleSelection(val: string | undefined) { handleSelection(val: MapState) {
this.props.onSetMapState(val); this.props.onSetMapState(val);
} }
@@ -245,7 +247,7 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
<select <select
className="maputnik-select" className="maputnik-select"
data-wd-key="maputnik-select" data-wd-key="maputnik-select"
onChange={(e) => this.handleSelection(e.target.value)} onChange={(e) => this.handleSelection(e.target.value as MapState)}
value={currentView?.id} value={currentView?.id}
> >
{views.filter(v => v.group === "general").map((item) => { {views.filter(v => v.group === "general").map((item) => {

View File

@@ -12,7 +12,7 @@ type BlockProps = {
onChange?(...args: unknown[]): unknown onChange?(...args: unknown[]): unknown
fieldSpec?: object fieldSpec?: object
wideMode?: boolean wideMode?: boolean
error?: unknown[] error?: {message: string}
}; };
type BlockState = { type BlockState = {

View File

@@ -5,7 +5,8 @@ import InputString from './InputString'
type FieldCommentProps = { type FieldCommentProps = {
value?: string value?: string
onChange(...args: unknown[]): unknown onChange(value: string | undefined): unknown
error: {message: string}
}; };
export default class FieldComment extends React.Component<FieldCommentProps> { export default class FieldComment extends React.Component<FieldCommentProps> {
@@ -18,6 +19,7 @@ export default class FieldComment extends React.Component<FieldCommentProps> {
label={"Comments"} label={"Comments"}
fieldSpec={fieldSpec} fieldSpec={fieldSpec}
data-wd-key="layer-comment" data-wd-key="layer-comment"
error={this.props.error}
> >
<InputString <InputString
multi={true} multi={true}

View File

@@ -107,7 +107,7 @@ type FieldFunctionProps = {
fieldName: string fieldName: string
fieldType: string fieldType: string
fieldSpec: any fieldSpec: any
errors?: unknown[] errors?: {[key: string]: {message: string}}
value?: any value?: any
}; };

View File

@@ -7,8 +7,8 @@ import InputString from './InputString'
type FieldIdProps = { type FieldIdProps = {
value: string value: string
wdKey: string wdKey: string
onChange(...args: unknown[]): unknown onChange(value: string | undefined): unknown
error?: unknown[] error?: {message: string}
}; };
export default class FieldId extends React.Component<FieldIdProps> { export default class FieldId extends React.Component<FieldIdProps> {

View File

@@ -6,8 +6,8 @@ import InputNumber from './InputNumber'
type FieldMaxZoomProps = { type FieldMaxZoomProps = {
value?: number value?: number
onChange(...args: unknown[]): unknown onChange(value: number | undefined): unknown
error?: unknown[] error?: {message: string}
}; };
export default class FieldMaxZoom extends React.Component<FieldMaxZoomProps> { export default class FieldMaxZoom extends React.Component<FieldMaxZoomProps> {

View File

@@ -7,7 +7,7 @@ import InputNumber from './InputNumber'
type FieldMinZoomProps = { type FieldMinZoomProps = {
value?: number value?: number
onChange(...args: unknown[]): unknown onChange(...args: unknown[]): unknown
error?: unknown[] error?: {message: string}
}; };
export default class FieldMinZoom extends React.Component<FieldMinZoomProps> { export default class FieldMinZoom extends React.Component<FieldMinZoomProps> {

View File

@@ -7,9 +7,9 @@ import InputAutocomplete from './InputAutocomplete'
type FieldSourceProps = { type FieldSourceProps = {
value?: string value?: string
wdKey?: string wdKey?: string
onChange?(...args: unknown[]): unknown onChange?(value: string| undefined): unknown
sourceIds?: unknown[] sourceIds?: unknown[]
error?: unknown[] error?: {message: string}
}; };
export default class FieldSource extends React.Component<FieldSourceProps> { export default class FieldSource extends React.Component<FieldSourceProps> {

View File

@@ -9,6 +9,7 @@ type FieldSourceLayerProps = {
onChange?(...args: unknown[]): unknown onChange?(...args: unknown[]): unknown
sourceLayerIds?: unknown[] sourceLayerIds?: unknown[]
isFixed?: boolean isFixed?: boolean
error?: {message: string}
}; };
export default class FieldSourceLayer extends React.Component<FieldSourceLayerProps> { export default class FieldSourceLayer extends React.Component<FieldSourceLayerProps> {
@@ -19,8 +20,11 @@ export default class FieldSourceLayer extends React.Component<FieldSourceLayerPr
} }
render() { render() {
return <Block label={"Source Layer"} fieldSpec={latest.layer['source-layer']} return <Block
label={"Source Layer"}
fieldSpec={latest.layer['source-layer']}
data-wd-key="layer-source-layer" data-wd-key="layer-source-layer"
error={this.props.error}
> >
<InputAutocomplete <InputAutocomplete
keepMenuWithinWindowBounds={!!this.props.isFixed} keepMenuWithinWindowBounds={!!this.props.isFixed}

View File

@@ -8,8 +8,8 @@ import InputString from './InputString'
type FieldTypeProps = { type FieldTypeProps = {
value: string value: string
wdKey?: string wdKey?: string
onChange(...args: unknown[]): unknown onChange(value: string): unknown
error?: unknown[] | undefined error?: {message: string}
disabled?: boolean disabled?: boolean
}; };

View File

@@ -1,10 +1,11 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types'
import {combiningFilterOps} from '../libs/filterops'
import {mdiTableRowPlusAfter} from '@mdi/js'; import {mdiTableRowPlusAfter} from '@mdi/js';
import {isEqual} from 'lodash'; import {isEqual} from 'lodash';
import {ExpressionSpecification, LegacyFilterSpecification, StyleSpecification} from 'maplibre-gl'
import {latest, migrate, convertFilter} from '@maplibre/maplibre-gl-style-spec' import {latest, migrate, convertFilter} from '@maplibre/maplibre-gl-style-spec'
import {mdiFunctionVariant} from '@mdi/js';
import {combiningFilterOps} from '../libs/filterops'
import InputSelect from './InputSelect' import InputSelect from './InputSelect'
import Block from './Block' import Block from './Block'
import SingleFilterEditor from './SingleFilterEditor' import SingleFilterEditor from './SingleFilterEditor'
@@ -12,10 +13,9 @@ import FilterEditorBlock from './FilterEditorBlock'
import InputButton from './InputButton' import InputButton from './InputButton'
import Doc from './Doc' import Doc from './Doc'
import ExpressionProperty from './_ExpressionProperty'; import ExpressionProperty from './_ExpressionProperty';
import {mdiFunctionVariant} from '@mdi/js';
function combiningFilter (props) { function combiningFilter(props: FilterEditorProps): LegacyFilterSpecification | ExpressionSpecification {
let filter = props.filter || ['all']; let filter = props.filter || ['all'];
if (!Array.isArray(filter)) { if (!Array.isArray(filter)) {
@@ -30,14 +30,15 @@ function combiningFilter (props) {
filters = [filter.slice(0)]; filters = [filter.slice(0)];
} }
return [combiningOp, ...filters]; return [combiningOp, ...filters] as LegacyFilterSpecification | ExpressionSpecification;
} }
function migrateFilter (filter) { function migrateFilter(filter: LegacyFilterSpecification | ExpressionSpecification) {
return migrate(createStyleFromFilter(filter)).layers[0].filter; // This "any" can be removed in latest version of maplibre where maplibre re-exported types from style-spec
return (migrate(createStyleFromFilter(filter) as any).layers[0] as any).filter;
} }
function createStyleFromFilter (filter) { function createStyleFromFilter(filter: LegacyFilterSpecification | ExpressionSpecification): StyleSpecification & {id: string} {
return { return {
"id": "tmp", "id": "tmp",
"version": 8, "version": 8,
@@ -69,7 +70,7 @@ const FILTER_OPS = [
]; ];
// If we convert a filter that is an expression to an expression it'll remain the same in value // If we convert a filter that is an expression to an expression it'll remain the same in value
function checkIfSimpleFilter (filter) { function checkIfSimpleFilter (filter: LegacyFilterSpecification | ExpressionSpecification) {
if (filter.length === 1 && FILTER_OPS.includes(filter[0])) { if (filter.length === 1 && FILTER_OPS.includes(filter[0])) {
return true; return true;
} }
@@ -77,33 +78,38 @@ function checkIfSimpleFilter (filter) {
return !isEqual(expression, filter); return !isEqual(expression, filter);
} }
function hasCombiningFilter(filter) { function hasCombiningFilter(filter: LegacyFilterSpecification | ExpressionSpecification) {
return combiningFilterOps.indexOf(filter[0]) >= 0 return combiningFilterOps.indexOf(filter[0]) >= 0
} }
function hasNestedCombiningFilter(filter) { function hasNestedCombiningFilter(filter: LegacyFilterSpecification | ExpressionSpecification) {
if(hasCombiningFilter(filter)) { if(hasCombiningFilter(filter)) {
const combinedFilters = filter.slice(1) return filter.slice(1).map(f => hasCombiningFilter(f as any)).filter(f => f == true).length > 0
return filter.slice(1).map(f => hasCombiningFilter(f)).filter(f => f == true).length > 0
} }
return false return false
} }
export default class FilterEditor extends React.Component { type FilterEditorProps = {
static propTypes = { /** Properties of the vector layer and the available fields */
/** Properties of the vector layer and the available fields */ properties?: {[key:string]: any}
properties: PropTypes.object, filter?: any[]
filter: PropTypes.array, errors?: {[key:string]: any}
errors: PropTypes.object, onChange(value: LegacyFilterSpecification | ExpressionSpecification): unknown
onChange: PropTypes.func.isRequired, };
}
type FilterEditorState = {
showDoc: boolean
displaySimpleFilter: boolean
valueIsSimpleFilter?: boolean
};
export default class FilterEditor extends React.Component<FilterEditorProps, FilterEditorState> {
static defaultProps = { static defaultProps = {
filter: ["all"], filter: ["all"],
} }
constructor (props) { constructor (props: FilterEditorProps) {
super(); super(props);
this.state = { this.state = {
showDoc: false, showDoc: false,
displaySimpleFilter: checkIfSimpleFilter(combiningFilter(props)), displaySimpleFilter: checkIfSimpleFilter(combiningFilter(props)),
@@ -111,25 +117,25 @@ export default class FilterEditor extends React.Component {
} }
// Convert filter to combining filter // Convert filter to combining filter
onFilterPartChanged(filterIdx, newPart) { onFilterPartChanged(filterIdx: number, newPart: any[]) {
const newFilter = combiningFilter(this.props).slice(0) const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification
newFilter[filterIdx] = newPart newFilter[filterIdx] = newPart
this.props.onChange(newFilter) this.props.onChange(newFilter)
} }
deleteFilterItem(filterIdx) { deleteFilterItem(filterIdx: number) {
const newFilter = combiningFilter(this.props).slice(0) const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification
newFilter.splice(filterIdx + 1, 1) newFilter.splice(filterIdx + 1, 1)
this.props.onChange(newFilter) this.props.onChange(newFilter)
} }
addFilterItem = () => { addFilterItem = () => {
const newFilterItem = combiningFilter(this.props).slice(0) const newFilterItem = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification
newFilterItem.push(['==', 'name', '']) (newFilterItem as any[]).push(['==', 'name', ''])
this.props.onChange(newFilterItem) this.props.onChange(newFilterItem)
} }
onToggleDoc = (val) => { onToggleDoc = (val: boolean) => {
this.setState({ this.setState({
showDoc: val showDoc: val
}); });
@@ -149,8 +155,7 @@ export default class FilterEditor extends React.Component {
}) })
} }
static getDerivedStateFromProps (props, currentState) { static getDerivedStateFromProps(props: FilterEditorProps, currentState: FilterEditorState) {
const {filter} = props;
const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props)); const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props));
// Upgrade but never downgrade // Upgrade but never downgrade
@@ -178,7 +183,7 @@ export default class FilterEditor extends React.Component {
const fieldSpec={ const fieldSpec={
doc: latest.layer.filter.doc + " Combine multiple filters together by using a compound filter." doc: latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."
}; };
const defaultFilter = ["all"]; const defaultFilter = ["all"] as LegacyFilterSpecification | ExpressionSpecification;
const isNestedCombiningFilter = displaySimpleFilter && hasNestedCombiningFilter(combiningFilter(this.props)); const isNestedCombiningFilter = displaySimpleFilter && hasNestedCombiningFilter(combiningFilter(this.props));
@@ -201,7 +206,7 @@ export default class FilterEditor extends React.Component {
else if (displaySimpleFilter) { else if (displaySimpleFilter) {
const filter = combiningFilter(this.props); const filter = combiningFilter(this.props);
let combiningOp = filter[0]; let combiningOp = filter[0];
let filters = filter.slice(1) let filters = filter.slice(1) as (LegacyFilterSpecification | ExpressionSpecification)[];
const actions = ( const actions = (
<div> <div>
@@ -218,7 +223,7 @@ export default class FilterEditor extends React.Component {
); );
const editorBlocks = filters.map((f, idx) => { const editorBlocks = filters.map((f, idx) => {
const error = errors[`filter[${idx+1}]`]; const error = errors![`filter[${idx+1}]`];
return ( return (
<div key={`block-${idx}`}> <div key={`block-${idx}`}>
@@ -247,7 +252,7 @@ export default class FilterEditor extends React.Component {
> >
<InputSelect <InputSelect
value={combiningOp} value={combiningOp}
onChange={this.onFilterPartChanged.bind(this, 0)} onChange={(v: [string, any]) => this.onFilterPartChanged(0, v)}
options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]} options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
/> />
</Block> </Block>

View File

@@ -10,6 +10,7 @@ import IconMissing from './IconMissing'
type IconLayerProps = { type IconLayerProps = {
type: string type: string
style?: object style?: object
className?: string
}; };
export default class IconLayer extends React.Component<IconLayerProps> { export default class IconLayer extends React.Component<IconLayerProps> {

View File

@@ -8,7 +8,7 @@ const MAX_HEIGHT = 140;
export type InputAutocompleteProps = { export type InputAutocompleteProps = {
value?: string value?: string
options: any[] options: any[]
onChange(...args: unknown[]): unknown onChange(value: string | undefined): unknown
keepMenuWithinWindowBounds?: boolean keepMenuWithinWindowBounds?: boolean
'aria-label'?: string 'aria-label'?: string
}; };

View File

@@ -137,7 +137,6 @@ class DeleteValueInputButton extends React.Component<DeleteValueInputButtonProps
> >
<FieldDocLabel <FieldDocLabel
label={<MdDelete />} label={<MdDelete />}
fieldSpec={{doc:" Remove array item."}}
/> />
</InputButton> </InputButton>
} }

View File

@@ -5,7 +5,7 @@ export type InputSelectProps = {
"data-wd-key"?: string "data-wd-key"?: string
options: [string, any][] | string[] options: [string, any][] | string[]
style?: object style?: object
onChange(value: string): unknown onChange(value: string | [string, any]): unknown
title?: string title?: string
'aria-label'?: string 'aria-label'?: string
}; };

View File

@@ -1,6 +1,9 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Wrapper, Button, Menu, MenuItem } from 'react-aria-menubutton' import { Wrapper, Button, Menu, MenuItem } from 'react-aria-menubutton'
import {Accordion} from 'react-accessible-accordion';
import {MdMoreVert} from 'react-icons/md'
import {BackgroundLayerSpecification, LayerSpecification, SourceSpecification} from 'maplibre-gl';
import FieldJson from './FieldJson' import FieldJson from './FieldJson'
import FilterEditor from './FilterEditor' import FilterEditor from './FilterEditor'
@@ -13,20 +16,16 @@ import FieldMaxZoom from './FieldMaxZoom'
import FieldComment from './FieldComment' import FieldComment from './FieldComment'
import FieldSource from './FieldSource' import FieldSource from './FieldSource'
import FieldSourceLayer from './FieldSourceLayer' import FieldSourceLayer from './FieldSourceLayer'
import {Accordion} from 'react-accessible-accordion';
import {MdMoreVert} from 'react-icons/md'
import { changeType, changeProperty } from '../libs/layer' import { changeType, changeProperty } from '../libs/layer'
import layout from '../config/layout.json' import layout from '../config/layout.json'
import {formatLayerId} from '../util/format'; import {formatLayerId} from '../util/format';
function getLayoutForType (type) { function getLayoutForType(type: LayerSpecification["type"]) {
return layout[type] ? layout[type] : layout.invalid; return layout[type] ? layout[type] : layout.invalid;
} }
function layoutGroups(layerType) { function layoutGroups(layerType: LayerSpecification["type"]): {title: string, type: string, fields?: string[]}[] {
const layerGroup = { const layerGroup = {
title: 'Layer', title: 'Layer',
type: 'layer' type: 'layer'
@@ -44,25 +43,29 @@ function layoutGroups(layerType) {
.concat([editorGroup]) .concat([editorGroup])
} }
/** Layer editor supporting multiple types of layers. */ type LayerEditorProps = {
export default class LayerEditor extends React.Component { layer: LayerSpecification
static propTypes = { sources: {[key: string]: SourceSpecification}
layer: PropTypes.object.isRequired, vectorLayers: {[key: string]: any}
sources: PropTypes.object, spec: object
vectorLayers: PropTypes.object, onLayerChanged(...args: unknown[]): unknown
spec: PropTypes.object.isRequired, onLayerIdChange(...args: unknown[]): unknown
onLayerChanged: PropTypes.func, onMoveLayer(...args: unknown[]): unknown
onLayerIdChange: PropTypes.func, onLayerDestroy(...args: unknown[]): unknown
onMoveLayer: PropTypes.func, onLayerCopy(...args: unknown[]): unknown
onLayerDestroy: PropTypes.func, onLayerVisibilityToggle(...args: unknown[]): unknown
onLayerCopy: PropTypes.func, isFirstLayer?: boolean
onLayerVisibilityToggle: PropTypes.func, isLastLayer?: boolean
isFirstLayer: PropTypes.bool, layerIndex: number
isLastLayer: PropTypes.bool, errors?: any[]
layerIndex: PropTypes.number, };
errors: PropTypes.array,
}
type LayerEditorState = {
editorGroups: {[keys:string]: boolean}
};
/** Layer editor supporting multiple types of layers. */
export default class LayerEditor extends React.Component<LayerEditorProps, LayerEditorState> {
static defaultProps = { static defaultProps = {
onLayerChanged: () => {}, onLayerChanged: () => {},
onLayerIdChange: () => {}, onLayerIdChange: () => {},
@@ -73,11 +76,11 @@ export default class LayerEditor extends React.Component {
reactIconBase: PropTypes.object reactIconBase: PropTypes.object
} }
constructor(props) { constructor(props: LayerEditorProps) {
super(props) super(props)
//TODO: Clean this up and refactor into function //TODO: Clean this up and refactor into function
const editorGroups = {} const editorGroups: {[keys:string]: boolean} = {}
layoutGroups(this.props.layer.type).forEach(group => { layoutGroups(this.props.layer.type).forEach(group => {
editorGroups[group.title] = true editorGroups[group.title] = true
}) })
@@ -85,7 +88,7 @@ export default class LayerEditor extends React.Component {
this.state = { editorGroups } this.state = { editorGroups }
} }
static getDerivedStateFromProps(props, state) { static getDerivedStateFromProps(props: LayerEditorProps, state: LayerEditorState) {
const additionalGroups = { ...state.editorGroups } const additionalGroups = { ...state.editorGroups }
getLayoutForType(props.layer.type).groups.forEach(group => { getLayoutForType(props.layer.type).groups.forEach(group => {
@@ -108,14 +111,14 @@ export default class LayerEditor extends React.Component {
} }
} }
changeProperty(group, property, newValue) { changeProperty(group: keyof LayerSpecification | null, property: string, newValue: any) {
this.props.onLayerChanged( this.props.onLayerChanged(
this.props.layerIndex, this.props.layerIndex,
changeProperty(this.props.layer, group, property, newValue) changeProperty(this.props.layer, group, property, newValue)
) )
} }
onGroupToggle(groupTitle, active) { onGroupToggle(groupTitle: string, active: boolean) {
const changedActiveGroups = { const changedActiveGroups = {
...this.state.editorGroups, ...this.state.editorGroups,
[groupTitle]: active, [groupTitle]: active,
@@ -125,15 +128,15 @@ export default class LayerEditor extends React.Component {
}) })
} }
renderGroupType(type, fields) { renderGroupType(type: string, fields?: string[]): JSX.Element {
let comment = "" let comment = ""
if(this.props.layer.metadata) { if(this.props.layer.metadata) {
comment = this.props.layer.metadata['maputnik:comment'] comment = (this.props.layer.metadata as any)['maputnik:comment']
} }
const {errors, layerIndex} = this.props; const {errors, layerIndex} = this.props;
const errorData = {}; const errorData: {[key in LayerSpecification as string]: {message: string}} = {};
errors.forEach(error => { errors!.forEach(error => {
if ( if (
error.parsed && error.parsed &&
error.parsed.type === "layer" && error.parsed.type === "layer" &&
@@ -146,8 +149,9 @@ export default class LayerEditor extends React.Component {
}) })
let sourceLayerIds; let sourceLayerIds;
if(this.props.sources.hasOwnProperty(this.props.layer.source)) { const layer = this.props.layer as Exclude<LayerSpecification, BackgroundLayerSpecification>;
sourceLayerIds = this.props.sources[this.props.layer.source].layers; if(this.props.sources.hasOwnProperty(layer.source)) {
sourceLayerIds = (this.props.sources[layer.source] as any).layers;
} }
switch(type) { switch(type) {
@@ -169,7 +173,7 @@ export default class LayerEditor extends React.Component {
/> />
{this.props.layer.type !== 'background' && <FieldSource {this.props.layer.type !== 'background' && <FieldSource
error={errorData.source} error={errorData.source}
sourceIds={Object.keys(this.props.sources)} sourceIds={Object.keys(this.props.sources!)}
value={this.props.layer.source} value={this.props.layer.source}
onChange={v => this.changeProperty(null, 'source', v)} onChange={v => this.changeProperty(null, 'source', v)}
/> />
@@ -178,7 +182,7 @@ export default class LayerEditor extends React.Component {
<FieldSourceLayer <FieldSourceLayer
error={errorData['source-layer']} error={errorData['source-layer']}
sourceLayerIds={sourceLayerIds} sourceLayerIds={sourceLayerIds}
value={this.props.layer['source-layer']} value={(this.props.layer as any)['source-layer']}
onChange={v => this.changeProperty(null, 'source-layer', v)} onChange={v => this.changeProperty(null, 'source-layer', v)}
/> />
} }
@@ -202,8 +206,8 @@ export default class LayerEditor extends React.Component {
<div className="maputnik-filter-editor-wrapper"> <div className="maputnik-filter-editor-wrapper">
<FilterEditor <FilterEditor
errors={errorData} errors={errorData}
filter={this.props.layer.filter} filter={(this.props.layer as any).filter}
properties={this.props.vectorLayers[this.props.layer['source-layer']]} properties={this.props.vectorLayers[(this.props.layer as any)['source-layer']]}
onChange={f => this.changeProperty(null, 'filter', f)} onChange={f => this.changeProperty(null, 'filter', f)}
/> />
</div> </div>
@@ -212,7 +216,7 @@ export default class LayerEditor extends React.Component {
return <PropertyGroup return <PropertyGroup
errors={errorData} errors={errorData}
layer={this.props.layer} layer={this.props.layer}
groupFields={fields} groupFields={fields!}
spec={this.props.spec} spec={this.props.spec}
onChange={this.changeProperty.bind(this)} onChange={this.changeProperty.bind(this)}
/> />
@@ -226,10 +230,11 @@ export default class LayerEditor extends React.Component {
); );
}} }}
/> />
default: return <></>
} }
} }
moveLayer(offset) { moveLayer(offset: number) {
this.props.onMoveLayer({ this.props.onMoveLayer({
oldIndex: this.props.layerIndex, oldIndex: this.props.layerIndex,
newIndex: this.props.layerIndex+offset newIndex: this.props.layerIndex+offset
@@ -237,7 +242,7 @@ export default class LayerEditor extends React.Component {
} }
render() { render() {
const groupIds = []; const groupIds: string[] = [];
const layerType = this.props.layer.type const layerType = this.props.layer.type
const groups = layoutGroups(layerType).filter(group => { const groups = layoutGroups(layerType).filter(group => {
return !(layerType === 'background' && group.type === 'source') return !(layerType === 'background' && group.type === 'source')
@@ -258,7 +263,7 @@ export default class LayerEditor extends React.Component {
const layout = this.props.layer.layout || {} const layout = this.props.layer.layout || {}
const items = { const items: {[key: string]: {text: string, handler: () => void, disabled?: boolean}} = {
delete: { delete: {
text: "Delete", text: "Delete",
handler: () => this.props.onLayerDestroy(this.props.layerIndex) handler: () => this.props.onLayerDestroy(this.props.layerIndex)
@@ -285,8 +290,8 @@ export default class LayerEditor extends React.Component {
} }
} }
function handleSelection(id, event) { function handleSelection(id: string, event: React.SyntheticEvent) {
event.stopPropagation; event.stopPropagation();
items[id].handler(); items[id].handler();
} }
@@ -310,7 +315,7 @@ export default class LayerEditor extends React.Component {
</Button> </Button>
<Menu> <Menu>
<ul className="more-menu__menu"> <ul className="more-menu__menu">
{Object.keys(items).map((id, idx) => { {Object.keys(items).map((id) => {
const item = items[id]; const item = items[id];
return <li key={id}> return <li key={id}>
<MenuItem value={id} className='more-menu__menu__item'> <MenuItem value={id} className='more-menu__menu__item'>

View File

@@ -18,7 +18,7 @@ type LayerEditorGroupProps = {
title: string title: string
isActive: boolean isActive: boolean
children: React.ReactElement children: React.ReactElement
onActiveToggle(...args: unknown[]): unknown onActiveToggle(active: boolean): unknown
}; };

View File

@@ -1,5 +1,4 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames' import classnames from 'classnames'
import lodash from 'lodash'; import lodash from 'lodash';
@@ -7,21 +6,14 @@ import LayerListGroup from './LayerListGroup'
import LayerListItem from './LayerListItem' import LayerListItem from './LayerListItem'
import ModalAdd from './ModalAdd' import ModalAdd from './ModalAdd'
import {SortableContainer} from 'react-sortable-hoc'; import {SortEndHandler, SortableContainer} from 'react-sortable-hoc';
import type {LayerSpecification} from 'maplibre-gl';
const layerListPropTypes = { function layerPrefix(name: string) {
layers: PropTypes.array.isRequired,
selectedLayerIndex: PropTypes.number.isRequired,
onLayersChange: PropTypes.func.isRequired,
onLayerSelect: PropTypes.func,
sources: PropTypes.object.isRequired,
}
function layerPrefix(name) {
return name.replace(' ', '-').replace('_', '-').split('-')[0] return name.replace(' ', '-').replace('_', '-').split('-')[0]
} }
function findClosestCommonPrefix(layers, idx) { function findClosestCommonPrefix(layers: LayerSpecification[], idx: number) {
const currentLayerPrefix = layerPrefix(layers[idx].id) const currentLayerPrefix = layerPrefix(layers[idx].id)
let closestIdx = idx let closestIdx = idx
for (let i = idx; i > 0; i--) { for (let i = idx; i > 0; i--) {
@@ -37,14 +29,34 @@ function findClosestCommonPrefix(layers, idx) {
let UID = 0; let UID = 0;
type LayerListContainerProps = {
layers: LayerSpecification[]
selectedLayerIndex: number
onLayersChange(layers: LayerSpecification[]): unknown
onLayerSelect(...args: unknown[]): unknown
onLayerDestroy?(...args: unknown[]): unknown
onLayerCopy(...args: unknown[]): unknown
onLayerVisibilityToggle(...args: unknown[]): unknown
sources: object
errors: any[]
};
type LayerListContainerState = {
collapsedGroups: {[ket: string]: boolean}
areAllGroupsExpanded: boolean
keys: {[key: string]: number}
isOpen: {[key: string]: boolean}
};
// List of collapsible layer editors // List of collapsible layer editors
class LayerListContainer extends React.Component { class LayerListContainer extends React.Component<LayerListContainerProps, LayerListContainerState> {
static propTypes = {...layerListPropTypes}
static defaultProps = { static defaultProps = {
onLayerSelect: () => {}, onLayerSelect: () => {},
} }
selectedItemRef: React.RefObject<any>;
scrollContainerRef: React.RefObject<HTMLElement>;
constructor(props) { constructor(props: LayerListContainerProps) {
super(props); super(props);
this.selectedItemRef = React.createRef(); this.selectedItemRef = React.createRef();
this.scrollContainerRef = React.createRef(); this.scrollContainerRef = React.createRef();
@@ -60,7 +72,7 @@ class LayerListContainer extends React.Component {
} }
} }
toggleModal(modalName) { toggleModal(modalName: string) {
this.setState({ this.setState({
keys: { keys: {
...this.state.keys, ...this.state.keys,
@@ -74,9 +86,9 @@ class LayerListContainer extends React.Component {
} }
toggleLayers = () => { toggleLayers = () => {
let idx=0 let idx = 0
let newGroups=[] let newGroups: {[key:string]: boolean} = {}
this.groupedLayers().forEach(layers => { this.groupedLayers().forEach(layers => {
const groupPrefix = layerPrefix(layers[0].id) const groupPrefix = layerPrefix(layers[0].id)
@@ -87,7 +99,7 @@ class LayerListContainer extends React.Component {
newGroups[lookupKey] = this.state.areAllGroupsExpanded newGroups[lookupKey] = this.state.areAllGroupsExpanded
} }
layers.forEach((layer) => { layers.forEach((_layer) => {
idx += 1 idx += 1
}) })
}); });
@@ -98,7 +110,7 @@ class LayerListContainer extends React.Component {
}) })
} }
groupedLayers() { groupedLayers(): (LayerSpecification & {key: string})[][] {
const groups = [] const groups = []
const layerIdCount = new Map(); const layerIdCount = new Map();
@@ -122,7 +134,7 @@ class LayerListContainer extends React.Component {
return groups return groups
} }
toggleLayerGroup(groupPrefix, idx) { toggleLayerGroup(groupPrefix: string, idx: number) {
const lookupKey = [groupPrefix, idx].join('-') const lookupKey = [groupPrefix, idx].join('-')
const newGroups = { ...this.state.collapsedGroups } const newGroups = { ...this.state.collapsedGroups }
if(lookupKey in this.state.collapsedGroups) { if(lookupKey in this.state.collapsedGroups) {
@@ -135,12 +147,12 @@ class LayerListContainer extends React.Component {
}) })
} }
isCollapsed(groupPrefix, idx) { isCollapsed(groupPrefix: string, idx: number) {
const collapsed = this.state.collapsedGroups[[groupPrefix, idx].join('-')] const collapsed = this.state.collapsedGroups[[groupPrefix, idx].join('-')]
return collapsed === undefined ? true : collapsed return collapsed === undefined ? true : collapsed
} }
shouldComponentUpdate (nextProps, nextState) { shouldComponentUpdate (nextProps: LayerListContainerProps, nextState: LayerListContainerState) {
// Always update on state change // Always update on state change
if (this.state !== nextState) { if (this.state !== nextState) {
return true; return true;
@@ -148,8 +160,8 @@ class LayerListContainer extends React.Component {
// This component tree only requires id and visibility from the layers // This component tree only requires id and visibility from the layers
// objects // objects
function getRequiredProps (layer) { function getRequiredProps(layer: LayerSpecification) {
const out = { const out: {id: string, layout?: { visibility: any}} = {
id: layer.id, id: layer.id,
}; };
@@ -165,10 +177,10 @@ class LayerListContainer extends React.Component {
this.props.layers.map(getRequiredProps), this.props.layers.map(getRequiredProps),
); );
function withoutLayers (props) { function withoutLayers(props: LayerListContainerProps) {
const out = { const out = {
...props ...props
}; } as LayerListContainerProps & { layers?: any };
delete out['layers']; delete out['layers'];
return out; return out;
} }
@@ -184,7 +196,7 @@ class LayerListContainer extends React.Component {
return propsChanged; return propsChanged;
} }
componentDidUpdate (prevProps) { componentDidUpdate (prevProps: LayerListContainerProps) {
if (prevProps.selectedLayerIndex !== this.props.selectedLayerIndex) { if (prevProps.selectedLayerIndex !== this.props.selectedLayerIndex) {
const selectedItemNode = this.selectedItemRef.current; const selectedItemNode = this.selectedItemRef.current;
if (selectedItemNode && selectedItemNode.node) { if (selectedItemNode && selectedItemNode.node) {
@@ -207,7 +219,7 @@ class LayerListContainer extends React.Component {
render() { render() {
const listItems = [] const listItems: JSX.Element[] = []
let idx = 0 let idx = 0
const layersByGroup = this.groupedLayers(); const layersByGroup = this.groupedLayers();
layersByGroup.forEach(layers => { layersByGroup.forEach(layers => {
@@ -235,7 +247,7 @@ class LayerListContainer extends React.Component {
); );
}); });
const additionalProps = {}; const additionalProps: {ref?: React.RefObject<any>} = {};
if (idx === this.props.selectedLayerIndex) { if (idx === this.props.selectedLayerIndex) {
additionalProps.ref = this.selectedItemRef; additionalProps.ref = this.selectedItemRef;
} }
@@ -255,7 +267,7 @@ class LayerListContainer extends React.Component {
visibility={(layer.layout || {}).visibility} visibility={(layer.layout || {}).visibility}
isSelected={idx === this.props.selectedLayerIndex} isSelected={idx === this.props.selectedLayerIndex}
onLayerSelect={this.props.onLayerSelect} onLayerSelect={this.props.onLayerSelect}
onLayerDestroy={this.props.onLayerDestroy.bind(this)} onLayerDestroy={this.props.onLayerDestroy?.bind(this)}
onLayerCopy={this.props.onLayerCopy.bind(this)} onLayerCopy={this.props.onLayerCopy.bind(this)}
onLayerVisibilityToggle={this.props.onLayerVisibilityToggle.bind(this)} onLayerVisibilityToggle={this.props.onLayerVisibilityToggle.bind(this)}
{...additionalProps} {...additionalProps}
@@ -316,11 +328,13 @@ class LayerListContainer extends React.Component {
} }
} }
const LayerListContainerSortable = SortableContainer((props) => <LayerListContainer {...props} />) const LayerListContainerSortable = SortableContainer((props: LayerListContainerProps) => <LayerListContainer {...props} />)
export default class LayerList extends React.Component { type LayerListProps = LayerListContainerProps & {
static propTypes = {...layerListPropTypes} onMoveLayer: SortEndHandler
};
export default class LayerList extends React.Component<LayerListProps> {
render() { render() {
return <LayerListContainerSortable return <LayerListContainerSortable
{...this.props} {...this.props}

View File

@@ -8,7 +8,12 @@ import IconLayer from './IconLayer'
import {SortableElement, SortableHandle} from 'react-sortable-hoc' import {SortableElement, SortableHandle} from 'react-sortable-hoc'
const DraggableLabel = SortableHandle((props) => { type DraggableLabelProps = {
layerId: string
layerType: string
};
const DraggableLabel = SortableHandle((props: DraggableLabelProps) => {
return <div className="maputnik-layer-list-item-handle"> return <div className="maputnik-layer-list-item-handle">
<IconLayer <IconLayer
className="layer-handle__icon" className="layer-handle__icon"
@@ -20,15 +25,15 @@ const DraggableLabel = SortableHandle((props) => {
</div> </div>
}); });
class IconAction extends React.Component { type IconActionProps = {
static propTypes = { action: string
action: PropTypes.string.isRequired, onClick(...args: unknown[]): unknown
onClick: PropTypes.func.isRequired, wdKey?: string
wdKey: PropTypes.string, classBlockName?: string
classBlockName: PropTypes.string, classBlockModifier?: string
classBlockModifier: PropTypes.string, };
}
class IconAction extends React.Component<IconActionProps> {
renderIcon() { renderIcon() {
switch(this.props.action) { switch(this.props.action) {
case 'duplicate': return <MdContentCopy /> case 'duplicate': return <MdContentCopy />
@@ -51,7 +56,7 @@ class IconAction extends React.Component {
} }
return <button return <button
tabIndex="-1" tabIndex={-1}
title={this.props.action} title={this.props.action}
className={`maputnik-layer-list-icon-action ${classAdditions}`} className={`maputnik-layer-list-icon-action ${classAdditions}`}
data-wd-key={this.props.wdKey} data-wd-key={this.props.wdKey}
@@ -63,21 +68,21 @@ class IconAction extends React.Component {
} }
} }
class LayerListItem extends React.Component { type LayerListItemProps = {
static propTypes = { id?: string
layerIndex: PropTypes.number.isRequired, layerIndex: number
layerId: PropTypes.string.isRequired, layerId: string
layerType: PropTypes.string.isRequired, layerType: string
isSelected: PropTypes.bool, isSelected?: boolean
visibility: PropTypes.string, visibility?: string
className: PropTypes.string, className?: string
onLayerSelect(...args: unknown[]): unknown
onLayerSelect: PropTypes.func.isRequired, onLayerCopy?(...args: unknown[]): unknown
onLayerCopy: PropTypes.func, onLayerDestroy?(...args: unknown[]): unknown
onLayerDestroy: PropTypes.func, onLayerVisibilityToggle?(...args: unknown[]): unknown
onLayerVisibilityToggle: PropTypes.func, };
}
class LayerListItem extends React.Component<LayerListItemProps> {
static defaultProps = { static defaultProps = {
isSelected: false, isSelected: false,
visibility: 'visible', visibility: 'visible',
@@ -102,12 +107,12 @@ class LayerListItem extends React.Component {
return <li return <li
id={this.props.id} id={this.props.id}
key={this.props.layerId} key={this.props.layerId}
onClick={e => this.props.onLayerSelect(this.props.layerIndex)} onClick={_e => this.props.onLayerSelect(this.props.layerIndex)}
data-wd-key={"layer-list-item:"+this.props.layerId} data-wd-key={"layer-list-item:"+this.props.layerId}
className={classnames({ className={classnames({
"maputnik-layer-list-item": true, "maputnik-layer-list-item": true,
"maputnik-layer-list-item-selected": this.props.isSelected, "maputnik-layer-list-item-selected": this.props.isSelected,
[this.props.className]: true, [this.props.className!]: true,
})}> })}>
<DraggableLabel {...this.props} /> <DraggableLabel {...this.props} />
<span style={{flexGrow: 1}} /> <span style={{flexGrow: 1}} />
@@ -115,25 +120,25 @@ class LayerListItem extends React.Component {
wdKey={"layer-list-item:"+this.props.layerId+":delete"} wdKey={"layer-list-item:"+this.props.layerId+":delete"}
action={'delete'} action={'delete'}
classBlockName="delete" classBlockName="delete"
onClick={e => this.props.onLayerDestroy(this.props.layerIndex)} onClick={_e => this.props.onLayerDestroy!(this.props.layerIndex)}
/> />
<IconAction <IconAction
wdKey={"layer-list-item:"+this.props.layerId+":copy"} wdKey={"layer-list-item:"+this.props.layerId+":copy"}
action={'duplicate'} action={'duplicate'}
classBlockName="duplicate" classBlockName="duplicate"
onClick={e => this.props.onLayerCopy(this.props.layerIndex)} onClick={_e => this.props.onLayerCopy!(this.props.layerIndex)}
/> />
<IconAction <IconAction
wdKey={"layer-list-item:"+this.props.layerId+":toggle-visibility"} wdKey={"layer-list-item:"+this.props.layerId+":toggle-visibility"}
action={visibilityAction} action={visibilityAction}
classBlockName="visibility" classBlockName="visibility"
classBlockModifier={visibilityAction} classBlockModifier={visibilityAction}
onClick={e => this.props.onLayerVisibilityToggle(this.props.layerIndex)} onClick={_e => this.props.onLayerVisibilityToggle!(this.props.layerIndex)}
/> />
</li> </li>
} }
} }
const LayerListItemSortable = SortableElement((props) => <LayerListItem {...props} />); const LayerListItemSortable = SortableElement((props: LayerListItemProps) => <LayerListItem {...props} />);
export default LayerListItemSortable; export default LayerListItemSortable;

View File

@@ -1,15 +1,15 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import MapLibreGl from 'maplibre-gl' import MapLibreGl, {LayerSpecification, LngLat, Map, MapOptions, SourceSpecification, StyleSpecification} from 'maplibre-gl'
// @ts-ignore
import MapboxInspect from 'mapbox-gl-inspect' import MapboxInspect from 'mapbox-gl-inspect'
import MapMaplibreGlLayerPopup from './MapMaplibreGlLayerPopup' // @ts-ignore
import MapMaplibreGlFeaturePropertyPopup from './MapMaplibreGlFeaturePropertyPopup'
import tokens from '../config/tokens.json'
import colors from 'mapbox-gl-inspect/lib/colors' import colors from 'mapbox-gl-inspect/lib/colors'
import MapMaplibreGlLayerPopup from './MapMaplibreGlLayerPopup'
import MapMaplibreGlFeaturePropertyPopup, { InspectFeature } from './MapMaplibreGlFeaturePropertyPopup'
import Color from 'color' import Color from 'color'
import ZoomControl from '../libs/zoomcontrol' import ZoomControl from '../libs/zoomcontrol'
import { colorHighlightedLayer } from '../libs/highlight' import { HighlightedLayer, colorHighlightedLayer } from '../libs/highlight'
import 'maplibre-gl/dist/maplibre-gl.css' import 'maplibre-gl/dist/maplibre-gl.css'
import '../maplibregl.css' import '../maplibregl.css'
import '../libs/maplibre-rtl' import '../libs/maplibre-rtl'
@@ -17,26 +17,26 @@ import '../libs/maplibre-rtl'
const IS_SUPPORTED = MapLibreGl.supported(); const IS_SUPPORTED = MapLibreGl.supported();
function renderPopup(popup, mountNode) { function renderPopup(popup: JSX.Element, mountNode: ReactDOM.Container) {
ReactDOM.render(popup, mountNode); ReactDOM.render(popup, mountNode);
return mountNode; return mountNode;
} }
function buildInspectStyle(originalMapStyle, coloredLayers, highlightedLayer) { function buildInspectStyle(originalMapStyle: StyleSpecification, coloredLayers: HighlightedLayer[], highlightedLayer?: HighlightedLayer) {
const backgroundLayer = { const backgroundLayer = {
"id": "background", "id": "background",
"type": "background", "type": "background",
"paint": { "paint": {
"background-color": '#1c1f24', "background-color": '#1c1f24',
} }
} } as LayerSpecification
const layer = colorHighlightedLayer(highlightedLayer) const layer = colorHighlightedLayer(highlightedLayer)
if(layer) { if(layer) {
coloredLayers.push(layer) coloredLayers.push(layer)
} }
const sources = {} const sources: {[key:string]: SourceSpecification} = {}
Object.keys(originalMapStyle.sources).forEach(sourceId => { Object.keys(originalMapStyle.sources).forEach(sourceId => {
const source = originalMapStyle.sources[sourceId] const source = originalMapStyle.sources[sourceId]
if(source.type !== 'raster' && source.type !== 'raster-dem') { if(source.type !== 'raster' && source.type !== 'raster-dem') {
@@ -47,32 +47,43 @@ function buildInspectStyle(originalMapStyle, coloredLayers, highlightedLayer) {
const inspectStyle = { const inspectStyle = {
...originalMapStyle, ...originalMapStyle,
sources: sources, sources: sources,
layers: [backgroundLayer].concat(coloredLayers) layers: [backgroundLayer].concat(coloredLayers as LayerSpecification[])
} }
return inspectStyle return inspectStyle
} }
export default class MapMaplibreGl extends React.Component { type MapMaplibreGlProps = {
static propTypes = { onDataChange?(event: {map: Map | null}): unknown
onDataChange: PropTypes.func, onLayerSelect(...args: unknown[]): unknown
onLayerSelect: PropTypes.func.isRequired, mapStyle: StyleSpecification
mapStyle: PropTypes.object.isRequired, inspectModeEnabled: boolean
inspectModeEnabled: PropTypes.bool.isRequired, highlightedLayer?: HighlightedLayer
highlightedLayer: PropTypes.object, options?: Partial<MapOptions> & {
options: PropTypes.object, showTileBoundaries?: boolean
replaceAccessTokens: PropTypes.func.isRequired, showCollisionBoxes?: boolean
onChange: PropTypes.func.isRequired, showOverdrawInspector?: boolean
} }
replaceAccessTokens(mapStyle: StyleSpecification): StyleSpecification
onChange(value: {center: LngLat, zoom: number}): unknown
};
type MapMaplibreGlState = {
map: Map | null
inspect: MapboxInspect | null
zoom?: number
};
export default class MapMaplibreGl extends React.Component<MapMaplibreGlProps, MapMaplibreGlState> {
static defaultProps = { static defaultProps = {
onMapLoaded: () => {}, onMapLoaded: () => {},
onDataChange: () => {}, onDataChange: () => {},
onLayerSelect: () => {}, onLayerSelect: () => {},
onChange: () => {}, onChange: () => {},
options: {}, options: {} as MapOptions,
} }
container: HTMLDivElement | null = null
constructor(props) { constructor(props: MapMaplibreGlProps) {
super(props) super(props)
this.state = { this.state = {
map: null, map: null,
@@ -80,7 +91,7 @@ export default class MapMaplibreGl extends React.Component {
} }
} }
updateMapFromProps(props) { updateMapFromProps(props: MapMaplibreGlProps) {
if(!IS_SUPPORTED) return; if(!IS_SUPPORTED) return;
if(!this.state.map) return if(!this.state.map) return
@@ -93,7 +104,7 @@ export default class MapMaplibreGl extends React.Component {
) )
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps: MapMaplibreGlProps, nextState: MapMaplibreGlState) {
let should = false; let should = false;
try { try {
should = JSON.stringify(this.props) !== JSON.stringify(nextProps) || JSON.stringify(this.state) !== JSON.stringify(nextState); should = JSON.stringify(this.props) !== JSON.stringify(nextProps) || JSON.stringify(this.state) !== JSON.stringify(nextState);
@@ -103,7 +114,7 @@ export default class MapMaplibreGl extends React.Component {
return should; return should;
} }
componentDidUpdate(prevProps, prevState, snapshot) { componentDidUpdate() {
if(!IS_SUPPORTED) return; if(!IS_SUPPORTED) return;
const map = this.state.map; const map = this.state.map;
@@ -128,9 +139,9 @@ export default class MapMaplibreGl extends React.Component {
} }
} }
map.showTileBoundaries = this.props.options.showTileBoundaries; map.showTileBoundaries = this.props.options?.showTileBoundaries!;
map.showCollisionBoxes = this.props.options.showCollisionBoxes; map.showCollisionBoxes = this.props.options?.showCollisionBoxes!;
map.showOverdrawInspector = this.props.options.showOverdrawInspector; map.showOverdrawInspector = this.props.options?.showOverdrawInspector!;
} }
} }
@@ -139,7 +150,7 @@ export default class MapMaplibreGl extends React.Component {
const mapOpts = { const mapOpts = {
...this.props.options, ...this.props.options,
container: this.container, container: this.container!,
style: this.props.mapStyle, style: this.props.mapStyle,
hash: true, hash: true,
maxZoom: 24 maxZoom: 24
@@ -154,9 +165,9 @@ export default class MapMaplibreGl extends React.Component {
} }
mapViewChange(); mapViewChange();
map.showTileBoundaries = mapOpts.showTileBoundaries; map.showTileBoundaries = mapOpts.showTileBoundaries!;
map.showCollisionBoxes = mapOpts.showCollisionBoxes; map.showCollisionBoxes = mapOpts.showCollisionBoxes!;
map.showOverdrawInspector = mapOpts.showOverdrawInspector; map.showOverdrawInspector = mapOpts.showOverdrawInspector!;
const zoomControl = new ZoomControl; const zoomControl = new ZoomControl;
map.addControl(zoomControl, 'top-right'); map.addControl(zoomControl, 'top-right');
@@ -175,11 +186,11 @@ export default class MapMaplibreGl extends React.Component {
showInspectMapPopupOnHover: true, showInspectMapPopupOnHover: true,
showInspectButton: false, showInspectButton: false,
blockHoverPopupOnClick: true, blockHoverPopupOnClick: true,
assignLayerColor: (layerId, alpha) => { assignLayerColor: (layerId: string, alpha: number) => {
return Color(colors.brightColor(layerId, alpha)).desaturate(0.5).string() return Color(colors.brightColor(layerId, alpha)).desaturate(0.5).string()
}, },
buildInspectStyle: (originalMapStyle, coloredLayers) => buildInspectStyle(originalMapStyle, coloredLayers, this.props.highlightedLayer), buildInspectStyle: (originalMapStyle: StyleSpecification, coloredLayers: HighlightedLayer[]) => buildInspectStyle(originalMapStyle, coloredLayers, this.props.highlightedLayer),
renderPopup: features => { renderPopup: (features: InspectFeature[]) => {
if(this.props.inspectModeEnabled) { if(this.props.inspectModeEnabled) {
return renderPopup(<MapMaplibreGlFeaturePropertyPopup features={features} />, tmpNode); return renderPopup(<MapMaplibreGlFeaturePropertyPopup features={features} />, tmpNode);
} else { } else {
@@ -199,7 +210,7 @@ export default class MapMaplibreGl extends React.Component {
map.on("data", e => { map.on("data", e => {
if(e.dataType !== 'tile') return if(e.dataType !== 'tile') return
this.props.onDataChange({ this.props.onDataChange!({
map: this.state.map map: this.state.map
}) })
}) })
@@ -208,7 +219,7 @@ export default class MapMaplibreGl extends React.Component {
console.log("ERROR", e); console.log("ERROR", e);
}) })
map.on("zoom", e => { map.on("zoom", _e => {
this.setState({ this.setState({
zoom: map.getZoom() zoom: map.getZoom()
}); });
@@ -218,7 +229,7 @@ export default class MapMaplibreGl extends React.Component {
map.on("zoomend", mapViewChange); map.on("zoomend", mapViewChange);
} }
onLayerSelectById = (id) => { onLayerSelectById = (id: string) => {
const index = this.props.mapStyle.layers.findIndex(layer => layer.id === id); const index = this.props.mapStyle.layers.findIndex(layer => layer.id === id);
this.props.onLayerSelect(index); this.props.onLayerSelect(index);
} }

View File

@@ -1,9 +1,18 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types'
import Block from './Block' import Block from './Block'
import FieldString from './FieldString' import FieldString from './FieldString'
function displayValue(value) { export type InspectFeature = {
id: string
properties: {[key:string]: any}
layer: {[key:string]: any}
geometry: GeoJSON.Geometry
sourceLayer: string
inspectModeCounter?: number
counter?: number
}
function displayValue(value: string | number | Date | object) {
if (typeof value === 'undefined' || value === null) return value; if (typeof value === 'undefined' || value === null) return value;
if (value instanceof Date) return value.toLocaleString(); if (value instanceof Date) return value.toLocaleString();
if (typeof value === 'object' || if (typeof value === 'object' ||
@@ -12,7 +21,7 @@ function displayValue(value) {
return value; return value;
} }
function renderProperties(feature) { function renderProperties(feature: InspectFeature) {
return Object.keys(feature.properties).map(propertyName => { return Object.keys(feature.properties).map(propertyName => {
const property = feature.properties[propertyName] const property = feature.properties[propertyName]
return <Block key={propertyName} label={propertyName}> return <Block key={propertyName} label={propertyName}>
@@ -21,13 +30,13 @@ function renderProperties(feature) {
}) })
} }
function renderFeatureId(feature) { function renderFeatureId(feature: InspectFeature) {
return <Block key={"feature-id"} label={"feature_id"}> return <Block key={"feature-id"} label={"feature_id"}>
<FieldString value={displayValue(feature.id)} style={{backgroundColor: 'transparent'}} /> <FieldString value={displayValue(feature.id)} style={{backgroundColor: 'transparent'}} />
</Block> </Block>
} }
function renderFeature(feature, idx) { function renderFeature(feature: InspectFeature, idx: number) {
return <div key={`${feature.sourceLayer}-${idx}`}> return <div key={`${feature.sourceLayer}-${idx}`}>
<div className="maputnik-popup-layer-id">{feature.layer['source']}: {feature.layer['source-layer']}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</div> <div className="maputnik-popup-layer-id">{feature.layer['source']}: {feature.layer['source-layer']}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</div>
<Block key={"property-type"} label={"$type"}> <Block key={"property-type"} label={"$type"}>
@@ -38,8 +47,8 @@ function renderFeature(feature, idx) {
</div> </div>
} }
function removeDuplicatedFeatures(features) { function removeDuplicatedFeatures(features: InspectFeature[]) {
let uniqueFeatures = []; let uniqueFeatures: InspectFeature[] = [];
features.forEach(feature => { features.forEach(feature => {
const featureIndex = uniqueFeatures.findIndex(feature2 => { const featureIndex = uniqueFeatures.findIndex(feature2 => {
@@ -50,8 +59,8 @@ function removeDuplicatedFeatures(features) {
if(featureIndex === -1) { if(featureIndex === -1) {
uniqueFeatures.push(feature) uniqueFeatures.push(feature)
} else { } else {
if(uniqueFeatures[featureIndex].hasOwnProperty('inspectModeCounter')) { if('inspectModeCounter' in uniqueFeatures[featureIndex]) {
uniqueFeatures[featureIndex].inspectModeCounter++ uniqueFeatures[featureIndex].inspectModeCounter!++
} else { } else {
uniqueFeatures[featureIndex].inspectModeCounter = 2 uniqueFeatures[featureIndex].inspectModeCounter = 2
} }
@@ -61,11 +70,11 @@ function removeDuplicatedFeatures(features) {
return uniqueFeatures return uniqueFeatures
} }
class FeaturePropertyPopup extends React.Component { type FeaturePropertyPopupProps = {
static propTypes = { features: InspectFeature[]
features: PropTypes.array };
}
class FeaturePropertyPopup extends React.Component<FeaturePropertyPopupProps> {
render() { render() {
const features = removeDuplicatedFeatures(this.props.features) const features = removeDuplicatedFeatures(this.props.features)
return <div className="maputnik-feature-property-popup"> return <div className="maputnik-feature-property-popup">

View File

@@ -1,18 +1,19 @@
import React from 'react' import React from 'react'
import IconLayer from './IconLayer' import IconLayer from './IconLayer'
import type {InspectFeature} from './MapMaplibreGlFeaturePropertyPopup';
function groupFeaturesBySourceLayer(features: any[]) { function groupFeaturesBySourceLayer(features: InspectFeature[]) {
const sources = {} as any const sources: {[key: string]: InspectFeature[]} = {}
let returnedFeatures = {} as any; let returnedFeatures: {[key: string]: number} = {}
features.forEach(feature => { features.forEach(feature => {
if(returnedFeatures.hasOwnProperty(feature.layer.id)) { if(returnedFeatures.hasOwnProperty(feature.layer.id)) {
returnedFeatures[feature.layer.id]++ returnedFeatures[feature.layer.id]++
const featureObject = sources[feature.layer['source-layer']].find((f: any) => f.layer.id === feature.layer.id) const featureObject = sources[feature.layer['source-layer']].find((f: InspectFeature) => f.layer.id === feature.layer.id)
featureObject.counter = returnedFeatures[feature.layer.id] featureObject!.counter = returnedFeatures[feature.layer.id]
} else { } else {
sources[feature.layer['source-layer']] = sources[feature.layer['source-layer']] || [] sources[feature.layer['source-layer']] = sources[feature.layer['source-layer']] || []
sources[feature.layer['source-layer']].push(feature) sources[feature.layer['source-layer']].push(feature)
@@ -25,13 +26,13 @@ function groupFeaturesBySourceLayer(features: any[]) {
} }
type FeatureLayerPopupProps = { type FeatureLayerPopupProps = {
onLayerSelect(...args: unknown[]): unknown onLayerSelect(layerId: string): unknown
features: any[] features: InspectFeature[]
zoom?: number zoom?: number
}; };
class FeatureLayerPopup extends React.Component<FeatureLayerPopupProps> { class FeatureLayerPopup extends React.Component<FeatureLayerPopupProps> {
_getFeatureColor(feature: any, _zoom?: number) { _getFeatureColor(feature: InspectFeature, _zoom?: number) {
// Guard because openlayers won't have this // Guard because openlayers won't have this
if (!feature.layer.paint) { if (!feature.layer.paint) {
return; return;
@@ -75,7 +76,7 @@ class FeatureLayerPopup extends React.Component<FeatureLayerPopupProps> {
const sources = groupFeaturesBySourceLayer(this.props.features) const sources = groupFeaturesBySourceLayer(this.props.features)
const items = Object.keys(sources).map(vectorLayerId => { const items = Object.keys(sources).map(vectorLayerId => {
const layers = sources[vectorLayerId].map((feature: any, idx: number) => { const layers = sources[vectorLayerId].map((feature: InspectFeature, idx: number) => {
const featureColor = this._getFeatureColor(feature, this.props.zoom); const featureColor = this._getFeatureColor(feature, this.props.zoom);
return <div return <div

View File

@@ -8,7 +8,7 @@ import {apply} from 'ol-mapbox-style';
import {Map, View, Overlay} from 'ol'; import {Map, View, Overlay} from 'ol';
import {toLonLat} from 'ol/proj'; import {toLonLat} from 'ol/proj';
import { StyleSpecification } from '@maplibre/maplibre-gl-style-spec'; import type {StyleSpecification} from 'maplibre-gl';
function renderCoords (coords: string[]) { function renderCoords (coords: string[]) {

View File

@@ -8,7 +8,7 @@ type ModalProps = {
"data-wd-key"?: string "data-wd-key"?: string
isOpen: boolean isOpen: boolean
title: string title: string
onOpenToggle(...args: unknown[]): unknown onOpenToggle(value: boolean): unknown
underlayClickExits?: boolean underlayClickExits?: boolean
underlayProps?: any underlayProps?: any
className?: string className?: string

View File

@@ -6,18 +6,19 @@ import FieldType from './FieldType'
import FieldId from './FieldId' import FieldId from './FieldId'
import FieldSource from './FieldSource' import FieldSource from './FieldSource'
import FieldSourceLayer from './FieldSourceLayer' import FieldSourceLayer from './FieldSourceLayer'
import type {LayerSpecification} from 'maplibre-gl'
type ModalAddProps = { type ModalAddProps = {
layers: unknown[] layers: LayerSpecification[]
onLayersChange(...args: unknown[]): unknown onLayersChange(layers: LayerSpecification[]): unknown
isOpen: boolean isOpen: boolean
onOpenToggle(...args: unknown[]): unknown onOpenToggle(open: boolean): unknown
// A dict of source id's and the available source layers // A dict of source id's and the available source layers
sources: any sources: any
}; };
type ModalAddState = { type ModalAddState = {
type: string type: LayerSpecification["type"]
id: string id: string
source?: string source?: string
'source-layer'?: string 'source-layer'?: string
@@ -38,7 +39,7 @@ export default class ModalAdd extends React.Component<ModalAddProps, ModalAddSta
} }
} }
changedLayers.push(layer) changedLayers.push(layer as LayerSpecification)
this.props.onLayersChange(changedLayers) this.props.onLayersChange(changedLayers)
this.props.onOpenToggle(false) this.props.onOpenToggle(false)
@@ -145,7 +146,7 @@ export default class ModalAdd extends React.Component<ModalAddProps, ModalAddSta
<FieldType <FieldType
value={this.state.type} value={this.state.type}
wdKey="add-layer.layer-type" wdKey="add-layer.layer-type"
onChange={(v: string) => this.setState({ type: v })} onChange={(v: LayerSpecification["type"]) => this.setState({ type: v })}
/> />
{this.state.type !== 'background' && {this.state.type !== 'background' &&
<FieldSource <FieldSource

View File

@@ -6,16 +6,16 @@ import Modal from './Modal'
type ModalDebugProps = { type ModalDebugProps = {
isOpen: boolean isOpen: boolean
renderer: string renderer: string
onChangeMaboxGlDebug(...args: unknown[]): unknown onChangeMaplibreGlDebug(key: string, checked: boolean): unknown
onChangeOpenlayersDebug(...args: unknown[]): unknown onChangeOpenlayersDebug(key: string, checked: boolean): unknown
onOpenToggle(...args: unknown[]): unknown onOpenToggle(value: boolean): unknown
maplibreGlDebugOptions?: object maplibreGlDebugOptions?: object
openlayersDebugOptions?: object openlayersDebugOptions?: object
mapView: { mapView: {
zoom: number zoom: number
center: { center: {
lng: string lng: number
lat: string lat: number
} }
} }
}; };
@@ -26,8 +26,8 @@ export default class ModalDebug extends React.Component<ModalDebugProps> {
const {mapView} = this.props; const {mapView} = this.props;
const osmZoom = Math.round(mapView.zoom)+1; const osmZoom = Math.round(mapView.zoom)+1;
const osmLon = Number.parseFloat(mapView.center.lng).toFixed(5); const osmLon = +(mapView.center.lng).toFixed(5);
const osmLat = Number.parseFloat(mapView.center.lat).toFixed(5); const osmLat = +(mapView.center.lat).toFixed(5);
return <Modal return <Modal
data-wd-key="modal:debug" data-wd-key="modal:debug"
@@ -42,7 +42,7 @@ export default class ModalDebug extends React.Component<ModalDebugProps> {
{Object.entries(this.props.maplibreGlDebugOptions!).map(([key, val]) => { {Object.entries(this.props.maplibreGlDebugOptions!).map(([key, val]) => {
return <li key={key}> return <li key={key}>
<label> <label>
<input type="checkbox" checked={val} onChange={(e) => this.props.onChangeMaboxGlDebug(key, e.target.checked)} /> {key} <input type="checkbox" checked={val} onChange={(e) => this.props.onChangeMaplibreGlDebug(key, e.target.checked)} /> {key}
</label> </label>
</li> </li>
})} })}

View File

@@ -2,7 +2,8 @@ import React from 'react'
import Slugify from 'slugify' import Slugify from 'slugify'
import {saveAs} from 'file-saver' import {saveAs} from 'file-saver'
import {version} from 'maplibre-gl/package.json' import {version} from 'maplibre-gl/package.json'
import {StyleSpecification, format} from '@maplibre/maplibre-gl-style-spec' import {format} from '@maplibre/maplibre-gl-style-spec'
import type {StyleSpecification} from 'maplibre-gl'
import {MdFileDownload} from 'react-icons/md' import {MdFileDownload} from 'react-icons/md'
import FieldString from './FieldString' import FieldString from './FieldString'

View File

@@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import {LightSpecification, StyleSpecification, TransitionSpecification, latest} from '@maplibre/maplibre-gl-style-spec' import {latest} from '@maplibre/maplibre-gl-style-spec'
import type {LightSpecification, StyleSpecification, TransitionSpecification} from 'maplibre-gl'
import FieldArray from './FieldArray' import FieldArray from './FieldArray'
import FieldNumber from './FieldNumber' import FieldNumber from './FieldNumber'

View File

@@ -1,5 +1,8 @@
import React from 'react' import React from 'react'
import {GeoJSONSourceSpecification, RasterDEMSourceSpecification, RasterSourceSpecification, SourceSpecification, StyleSpecification, VectorSourceSpecification, latest} from '@maplibre/maplibre-gl-style-spec' import {MdAddCircleOutline, MdDelete} from 'react-icons/md'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import type {GeoJSONSourceSpecification, RasterDEMSourceSpecification, RasterSourceSpecification, SourceSpecification, StyleSpecification, VectorSourceSpecification} from 'maplibre-gl'
import Modal from './Modal' import Modal from './Modal'
import InputButton from './InputButton' import InputButton from './InputButton'
import FieldString from './FieldString' import FieldString from './FieldString'
@@ -10,7 +13,6 @@ import style from '../libs/style'
import { deleteSource, addSource, changeSource } from '../libs/source' import { deleteSource, addSource, changeSource } from '../libs/source'
import publicSources from '../config/tilesets.json' import publicSources from '../config/tilesets.json'
import {MdAddCircleOutline, MdDelete} from 'react-icons/md'
type PublicSourceProps = { type PublicSourceProps = {
id: string id: string

View File

@@ -1,7 +1,8 @@
import React from 'react' import React from 'react'
import FieldFunction from './FieldFunction' import FieldFunction from './FieldFunction'
import { LayerSpecification } from '@maplibre/maplibre-gl-style-spec' import type {LayerSpecification} from 'maplibre-gl'
const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image'] const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image']
/** Extract field spec by {@fieldName} from the {@layerType} in the /** Extract field spec by {@fieldName} from the {@layerType} in the
@@ -39,7 +40,7 @@ type PropertyGroupProps = {
groupFields: string[] groupFields: string[]
onChange(...args: unknown[]): unknown onChange(...args: unknown[]): unknown
spec: any spec: any
errors?: unknown[] errors?: {[key: string]: {message: string}}
}; };
export default class PropertyGroup extends React.Component<PropertyGroupProps> { export default class PropertyGroup extends React.Component<PropertyGroupProps> {

View File

@@ -36,7 +36,7 @@ function parseFilter(v: string | boolean | number) {
type SingleFilterEditorProps = { type SingleFilterEditorProps = {
filter: any[] filter: any[]
onChange(...args: unknown[]): unknown onChange(filter: any[]): unknown
properties?: {[key: string]: string} properties?: {[key: string]: string}
}; };

View File

@@ -13,6 +13,7 @@ const typeMap = {
number: () => Block, number: () => Block,
string: () => Block, string: () => Block,
formatted: () => Block, formatted: () => Block,
padding: () => Block,
}; };
export type SpecFieldProps = InputFieldSpecProps & { export type SpecFieldProps = InputFieldSpecProps & {

View File

@@ -288,7 +288,7 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
<div className="maputnik-data-spec-property-input"> <div className="maputnik-data-spec-property-input">
<InputSelect <InputSelect
value={this.props.value!.type} value={this.props.value!.type}
onChange={propVal => this.changeDataType(propVal)} onChange={(propVal: string) => this.changeDataType(propVal)}
title={"Select a type of data scale (default is 'categorical')."} title={"Select a type of data scale (default is 'categorical')."}
options={this.getDataFunctionTypes(this.props.fieldSpec)} options={this.getDataFunctionTypes(this.props.fieldSpec)}
/> />

View File

@@ -14,7 +14,7 @@ type ExpressionPropertyProps = {
fieldType?: string fieldType?: string
fieldSpec?: object fieldSpec?: object
value?: any value?: any
errors?: {[key: string]: any} errors?: {[key: string]: {message: string}}
onChange?(...args: unknown[]): unknown onChange?(...args: unknown[]): unknown
onUndo?(...args: unknown[]): unknown onUndo?(...args: unknown[]): unknown
canUndo?(...args: unknown[]): unknown canUndo?(...args: unknown[]): unknown
@@ -109,7 +109,8 @@ export default class ExpressionProperty extends React.Component<ExpressionProper
} }
return <Block return <Block
error={foundErrors} // this feels like an incorrect type...? `foundErrors` is an array of objects, not a single object
error={foundErrors as any}
fieldSpec={this.props.fieldSpec} fieldSpec={this.props.fieldSpec}
label={labelFromFieldName(this.props.fieldName)} label={labelFromFieldName(this.props.fieldName)}
action={deleteStopBtn} action={deleteStopBtn}

View File

@@ -8,7 +8,7 @@ type BlockIdProps = {
value: string value: string
wdKey: string wdKey: string
onChange(...args: unknown[]): unknown onChange(...args: unknown[]): unknown
error?: unknown[] error?: {message: string}
}; };
export default class BlockId extends React.Component<BlockIdProps> { export default class BlockId extends React.Component<BlockIdProps> {

View File

@@ -7,7 +7,7 @@ import FieldNumber from './FieldNumber'
type BlockMaxZoomProps = { type BlockMaxZoomProps = {
value?: number value?: number
onChange(...args: unknown[]): unknown onChange(...args: unknown[]): unknown
error?: unknown[] error?: {message: string}
}; };
export default class BlockMaxZoom extends React.Component<BlockMaxZoomProps> { export default class BlockMaxZoom extends React.Component<BlockMaxZoomProps> {

View File

@@ -7,7 +7,7 @@ import FieldNumber from './FieldNumber'
type BlockMinZoomProps = { type BlockMinZoomProps = {
value?: number value?: number
onChange(...args: unknown[]): unknown onChange(...args: unknown[]): unknown
error?: unknown[] error?: {message: string}
}; };
export default class BlockMinZoom extends React.Component<BlockMinZoomProps> { export default class BlockMinZoom extends React.Component<BlockMinZoomProps> {

View File

@@ -9,7 +9,7 @@ type BlockSourceProps = {
wdKey?: string wdKey?: string
onChange?(...args: unknown[]): unknown onChange?(...args: unknown[]): unknown
sourceIds?: unknown[] sourceIds?: unknown[]
error?: unknown[] error?: {message: string}
}; };
export default class BlockSource extends React.Component<BlockSourceProps> { export default class BlockSource extends React.Component<BlockSourceProps> {

View File

@@ -9,7 +9,7 @@ type BlockTypeProps = {
value: string value: string
wdKey?: string wdKey?: string
onChange(...args: unknown[]): unknown onChange(...args: unknown[]): unknown
error?: unknown[] error?: {message: string}
disabled?: boolean disabled?: boolean
}; };

View File

@@ -13,7 +13,7 @@ type SpecPropertyProps = SpecFieldProps & {
fieldType?: string fieldType?: string
fieldSpec?: any fieldSpec?: any
value?: any value?: any
errors?: unknown[] errors?: {[key: string]: {message: string}}
onExpressionClick?(...args: unknown[]): unknown onExpressionClick?(...args: unknown[]): unknown
}; };

View File

@@ -31,10 +31,11 @@ function setStopRefs(props: ZoomPropertyProps, state: ZoomPropertyState) {
newRefs = {...state}; newRefs = {...state};
} }
newRefs[idx] = docUid("stop-"); newRefs[idx] = docUid("stop-");
} else {
newRefs[idx] = state.refs[idx];
} }
}) })
} }
return newRefs; return newRefs;
} }
@@ -156,7 +157,6 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
const key = this.state.refs[idx]; const key = this.state.refs[idx];
const value = stop[1] const value = stop[1]
const deleteStopBtn= <DeleteStopButton onClick={this.props.onDeleteStop?.bind(this, idx)} /> const deleteStopBtn= <DeleteStopButton onClick={this.props.onDeleteStop?.bind(this, idx)} />
return <tr return <tr
key={key} key={key}
> >
@@ -195,7 +195,7 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
<div className="maputnik-data-spec-property-input"> <div className="maputnik-data-spec-property-input">
<InputSelect <InputSelect
value={"interpolate"} value={"interpolate"}
onChange={propVal => this.changeDataType(propVal)} onChange={(propVal: string) => this.changeDataType(propVal)}
title={"Select a type of data scale (default is 'categorical')."} title={"Select a type of data scale (default is 'categorical')."}
options={this.getDataFunctionTypes(this.props.fieldSpec!)} options={this.getDataFunctionTypes(this.props.fieldSpec!)}
/> />

View File

@@ -1,10 +1,11 @@
import style from './style.js' import style from './style.js'
import {StyleSpecification, format} from '@maplibre/maplibre-gl-style-spec' import {format} from '@maplibre/maplibre-gl-style-spec'
import type {StyleSpecification} from 'maplibre-gl'
import ReconnectingWebSocket from 'reconnecting-websocket' import ReconnectingWebSocket from 'reconnecting-websocket'
export type ApiStyleStoreOptions = { export type ApiStyleStoreOptions = {
port?: string port: string | null
host?: string host: string | null
onLocalStyleChange?: (style: any) => void onLocalStyleChange?: (style: any) => void
} }

View File

@@ -1,4 +1,5 @@
import {StyleSpecification, diff} from '@maplibre/maplibre-gl-style-spec' import {diff} from '@maplibre/maplibre-gl-style-spec'
import type {StyleSpecification} from 'maplibre-gl'
function diffMessages(beforeStyle: StyleSpecification, afterStyle: StyleSpecification) { function diffMessages(beforeStyle: StyleSpecification, afterStyle: StyleSpecification) {
const changes = diff(beforeStyle, afterStyle) const changes = diff(beforeStyle, afterStyle)

View File

@@ -2,40 +2,42 @@
import stylegen from 'mapbox-gl-inspect/lib/stylegen' import stylegen from 'mapbox-gl-inspect/lib/stylegen'
// @ts-ignore // @ts-ignore
import colors from 'mapbox-gl-inspect/lib/colors' import colors from 'mapbox-gl-inspect/lib/colors'
import {FilterSpecification,LayerSpecification } from '@maplibre/maplibre-gl-style-spec' import type {FilterSpecification,LayerSpecification } from 'maplibre-gl'
export function colorHighlightedLayer(layer: LayerSpecification) { export type HighlightedLayer = LayerSpecification & {filter?: FilterSpecification};
if(!layer || layer.type === 'background' || layer.type === 'raster') return null
function changeLayer(l: LayerSpecification & {filter?: FilterSpecification}) { function changeLayer(l: HighlightedLayer, layer: LayerSpecification) {
if(l.type === 'circle') { if(l.type === 'circle') {
l.paint!['circle-radius'] = 3 l.paint!['circle-radius'] = 3
} else if(l.type === 'line') { } else if(l.type === 'line') {
l.paint!['line-width'] = 2 l.paint!['line-width'] = 2
}
if("filter" in layer) {
l.filter = layer.filter
} else {
delete l['filter']
}
l.id = l.id + '_highlight'
return l
} }
if("filter" in layer) {
l.filter = layer.filter
} else {
delete l['filter']
}
l.id = l.id + '_highlight'
return l
}
export function colorHighlightedLayer(layer?: LayerSpecification): HighlightedLayer | null {
if(!layer || layer.type === 'background' || layer.type === 'raster') return null
const sourceLayerId = layer['source-layer'] || '' const sourceLayerId = layer['source-layer'] || ''
const color = colors.brightColor(sourceLayerId, 1); const color = colors.brightColor(sourceLayerId, 1);
if(layer.type === "fill" || layer.type === 'fill-extrusion') { if(layer.type === "fill" || layer.type === 'fill-extrusion') {
return changeLayer(stylegen.polygonLayer(color, color, layer.source, layer['source-layer'])) return changeLayer(stylegen.polygonLayer(color, color, layer.source, layer['source-layer']), layer)
} }
if(layer.type === "symbol" || layer.type === 'circle') { if(layer.type === "symbol" || layer.type === 'circle') {
return changeLayer(stylegen.circleLayer(color, layer.source, layer['source-layer'])) return changeLayer(stylegen.circleLayer(color, layer.source, layer['source-layer']), layer)
} }
if(layer.type === 'line') { if(layer.type === 'line') {
return changeLayer(stylegen.lineLayer(color, layer.source, layer['source-layer'])) return changeLayer(stylegen.lineLayer(color, layer.source, layer['source-layer']), layer)
} }
return null return null

View File

@@ -27,7 +27,7 @@ export function changeType(layer: LayerSpecification, newType: string) {
/** A {@property} in either the paint our layout {@group} has changed /** A {@property} in either the paint our layout {@group} has changed
* to a {@newValue}. * to a {@newValue}.
*/ */
export function changeProperty(layer: LayerSpecification, group: keyof LayerSpecification, property: string, newValue: any) { export function changeProperty(layer: LayerSpecification, group: keyof LayerSpecification | null, property: string, newValue: any) {
// Remove the property if undefined // Remove the property if undefined
if(newValue === undefined) { if(newValue === undefined) {
if(group) { if(group) {

View File

@@ -1,7 +1,7 @@
import type {StyleSpecification} from "@maplibre/maplibre-gl-style-spec"; import type {StyleSpecification} from "maplibre-gl";
export class RevisionStore { export class RevisionStore {
revisions: StyleSpecification[]; revisions: (StyleSpecification & {id: string})[];
currentIdx: number; currentIdx: number;
@@ -18,7 +18,7 @@ export class RevisionStore {
return this.revisions[this.currentIdx] return this.revisions[this.currentIdx]
} }
addRevision(revision: StyleSpecification) { addRevision(revision: StyleSpecification & {id: string}) {
//TODO: compare new revision style id with old ones //TODO: compare new revision style id with old ones
//and ensure that it is always the same id //and ensure that it is always the same id
this.revisions.push(revision) this.revisions.push(revision)

View File

@@ -1,4 +1,4 @@
import type {StyleSpecification, SourceSpecification} from "@maplibre/maplibre-gl-style-spec"; import type {StyleSpecification, SourceSpecification} from "maplibre-gl";
export function deleteSource(mapStyle: StyleSpecification, sourceId: string) { export function deleteSource(mapStyle: StyleSpecification, sourceId: string) {
const remainingSources = { ...mapStyle.sources} const remainingSources = { ...mapStyle.sources}

View File

@@ -1,4 +1,5 @@
import {derefLayers, StyleSpecification, LayerSpecification} from '@maplibre/maplibre-gl-style-spec' import {derefLayers} from '@maplibre/maplibre-gl-style-spec'
import type {StyleSpecification, LayerSpecification} from 'maplibre-gl'
import tokens from '../config/tokens.json' import tokens from '../config/tokens.json'
// Empty style is always used if no style could be restored or fetched // Empty style is always used if no style could be restored or fetched

View File

@@ -1,7 +1,7 @@
import style from './style' import style from './style'
import {loadStyleUrl} from './urlopen' import {loadStyleUrl} from './urlopen'
import publicSources from '../config/styles.json' import publicSources from '../config/styles.json'
import { StyleSpecification } from '@maplibre/maplibre-gl-style-spec' import type {StyleSpecification} from 'maplibre-gl'
const storagePrefix = "maputnik" const storagePrefix = "maputnik"
const stylePrefix = 'style' const stylePrefix = 'style'

View File

@@ -1,24 +1,27 @@
// @ts-ignore - this is a fork of jsonlint
import jsonlint from 'jsonlint'; import jsonlint from 'jsonlint';
import CodeMirror from 'codemirror'; import CodeMirror, { MarkerRange } from 'codemirror';
import jsonToAst from 'json-to-ast'; import jsonToAst from 'json-to-ast';
import {expression, validate} from '@maplibre/maplibre-gl-style-spec'; import {expression, validate} from '@maplibre/maplibre-gl-style-spec';
type MarkerRangeWithMessage = MarkerRange & {message: string};
CodeMirror.defineMode("mgl", function(config, parserConfig) {
CodeMirror.defineMode("mgl", (config, parserConfig) => {
// Just using the javascript mode with json enabled. Our logic is in the linter below. // Just using the javascript mode with json enabled. Our logic is in the linter below.
return CodeMirror.modes.javascript( return CodeMirror.modes.javascript(
{...config, json: true}, {...config, json: true} as any,
parserConfig parserConfig
); );
}); });
CodeMirror.registerHelper("lint", "json", function(text) { CodeMirror.registerHelper("lint", "json", (text: string) => {
const found = []; const found: MarkerRangeWithMessage[] = [];
// NOTE: This was modified from the original to remove the global, also the // NOTE: This was modified from the original to remove the global, also the
// old jsonlint API was 'jsonlint.parseError' its now // old jsonlint API was 'jsonlint.parseError' its now
// 'jsonlint.parser.parseError' // 'jsonlint.parser.parseError'
jsonlint.parser.parseError = function(str, hash) { (jsonlint as any).parser.parseError = (str: string, hash: any) => {
const loc = hash.loc; const loc = hash.loc;
found.push({ found.push({
from: CodeMirror.Pos(loc.first_line - 1, loc.first_column), from: CodeMirror.Pos(loc.first_line - 1, loc.first_column),
@@ -36,12 +39,12 @@ CodeMirror.registerHelper("lint", "json", function(text) {
return found; return found;
}); });
CodeMirror.registerHelper("lint", "mgl", function(text, opts, doc) { CodeMirror.registerHelper("lint", "mgl", (text: string, opts: any, doc: any) => {
const found = []; const found: MarkerRangeWithMessage[] = [];
const {parser} = jsonlint; const {parser} = jsonlint as any;
const {context} = opts; const {context} = opts;
parser.parseError = function(str, hash) { parser.parseError = (str: string, hash: any) => {
const loc = hash.loc; const loc = hash.loc;
found.push({ found.push({
from: CodeMirror.Pos(loc.first_line - 1, loc.first_column), from: CodeMirror.Pos(loc.first_line - 1, loc.first_column),
@@ -62,7 +65,7 @@ CodeMirror.registerHelper("lint", "mgl", function(text, opts, doc) {
const ast = jsonToAst(text); const ast = jsonToAst(text);
const input = JSON.parse(text); const input = JSON.parse(text);
function getArrayPositionalFromAst (node, path) { function getArrayPositionalFromAst(node: any, path: string[]) {
if (!node) { if (!node) {
return undefined; return undefined;
} }
@@ -79,7 +82,7 @@ CodeMirror.registerHelper("lint", "mgl", function(text, opts, doc) {
newNode = node.children[path[0]]; newNode = node.children[path[0]];
} }
else { else {
newNode = node.children.find(childNode => { newNode = node.children.find((childNode: any) => {
return ( return (
childNode.key && childNode.key &&
childNode.key.type === "Identifier" && childNode.key.type === "Identifier" &&
@@ -94,7 +97,7 @@ CodeMirror.registerHelper("lint", "mgl", function(text, opts, doc) {
} }
} }
let out; let out: ReturnType<typeof expression.createExpression> | null = null;
if (context === "layer") { if (context === "layer") {
// Just an empty style so we can validate a layer. // Just an empty style so we can validate a layer.
const errors = validate({ const errors = validate({
@@ -121,6 +124,7 @@ CodeMirror.registerHelper("lint", "mgl", function(text, opts, doc) {
// Remove the 'layers[0].' as we're validating the layer only here // Remove the 'layers[0].' as we're validating the layer only here
const errMessageParts = err.message.replace(/^layers\[0\]./, "").split(":"); const errMessageParts = err.message.replace(/^layers\[0\]./, "").split(":");
return { return {
name: '',
key: errMessageParts[0], key: errMessageParts[0],
message: errMessageParts[1], message: errMessageParts[1],
}; };
@@ -135,7 +139,7 @@ CodeMirror.registerHelper("lint", "mgl", function(text, opts, doc) {
throw new Error(`Invalid context ${context}`); throw new Error(`Invalid context ${context}`);
} }
if (out.result === "error") { if (out?.result === "error") {
const errors = out.value; const errors = out.value;
errors.forEach(error => { errors.forEach(error => {
const {key, message} = error; const {key, message} = error;