Merge pull request #625 from orangemug/fix/invalid-style-with-duplicate-layer-ids

Fix UI when loading invalid style with duplicate layer ids
This commit is contained in:
Orange Mug 2020-03-30 20:38:25 +01:00 committed by GitHub
commit 34c3015b42
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 111 additions and 42 deletions

View file

@ -102,6 +102,11 @@
"experimentalObjectRestSpread": true, "experimentalObjectRestSpread": true,
"jsx": true "jsx": true
} }
},
"settings": {
"react": {
"version": "detect"
}
} }
}, },
"devDependencies": { "devDependencies": {

View file

@ -36,6 +36,7 @@ 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 queryUtil from '../libs/query-util' import queryUtil from '../libs/query-util'
import {formatLayerId} from './util/format';
import MapboxGl from 'mapbox-gl' import MapboxGl from 'mapbox-gl'
@ -325,7 +326,42 @@ export default class App extends React.Component {
}; };
const errors = validate(newStyle, latest) || []; const errors = validate(newStyle, latest) || [];
const mappedErrors = errors.map(error => {
// The validate function doesn't give us errors for duplicate error with
// empty string for layer.id, manually deal with that here.
const layerErrors = [];
if (newStyle && newStyle.layers) {
const foundLayers = new Map();
newStyle.layers.forEach((layer, index) => {
if (layer.id === "" && foundLayers.has(layer.id)) {
const message = `Duplicate layer: ${formatLayerId(layer.id)}`;
const error = new Error(
`layers[${index}]: duplicate layer id [empty_string], previously used`
);
layerErrors.push(error);
}
foundLayers.set(layer.id, true);
});
}
const mappedErrors = layerErrors.concat(errors).map(error => {
const dupMatch = error.message.match(/layers\[(\d+)\]: (duplicate layer id "?(.*)"?, previously used)/);
if (dupMatch) {
const [matchStr, index, message] = dupMatch;
return {
message: error.message,
parsed: {
type: "layer",
data: {
index,
key: "id",
message,
}
}
}
}
// duplicate layer id
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;
@ -347,7 +383,7 @@ export default class App extends React.Component {
message: error.message, message: error.message,
}; };
} }
}) });
let dirtyMapStyle = undefined; let dirtyMapStyle = undefined;
if (errors.length > 0) { if (errors.length > 0) {
@ -437,56 +473,50 @@ export default class App extends React.Component {
this.onStyleChanged(changedStyle) this.onStyleChanged(changedStyle)
} }
onLayerDestroy = (layerId) => { onLayerDestroy = (index) => {
let layers = this.state.mapStyle.layers; let layers = this.state.mapStyle.layers;
const remainingLayers = layers.slice(0); const remainingLayers = layers.slice(0);
const idx = style.indexOfLayer(remainingLayers, layerId) remainingLayers.splice(index, 1);
remainingLayers.splice(idx, 1);
this.onLayersChange(remainingLayers); this.onLayersChange(remainingLayers);
} }
onLayerCopy = (layerId) => { onLayerCopy = (index) => {
let layers = this.state.mapStyle.layers; let layers = this.state.mapStyle.layers;
const changedLayers = layers.slice(0) const changedLayers = layers.slice(0)
const idx = style.indexOfLayer(changedLayers, layerId)
const clonedLayer = cloneDeep(changedLayers[idx]) const clonedLayer = cloneDeep(changedLayers[index])
clonedLayer.id = clonedLayer.id + "-copy" clonedLayer.id = clonedLayer.id + "-copy"
changedLayers.splice(idx, 0, clonedLayer) changedLayers.splice(index, 0, clonedLayer)
this.onLayersChange(changedLayers) this.onLayersChange(changedLayers)
} }
onLayerVisibilityToggle = (layerId) => { onLayerVisibilityToggle = (index) => {
let layers = this.state.mapStyle.layers; let layers = this.state.mapStyle.layers;
const changedLayers = layers.slice(0) const changedLayers = layers.slice(0)
const idx = style.indexOfLayer(changedLayers, layerId)
const layer = { ...changedLayers[idx] } const layer = { ...changedLayers[index] }
const changedLayout = 'layout' in layer ? {...layer.layout} : {} const changedLayout = 'layout' in layer ? {...layer.layout} : {}
changedLayout.visibility = changedLayout.visibility === 'none' ? 'visible' : 'none' changedLayout.visibility = changedLayout.visibility === 'none' ? 'visible' : 'none'
layer.layout = changedLayout layer.layout = changedLayout
changedLayers[idx] = layer changedLayers[index] = layer
this.onLayersChange(changedLayers) this.onLayersChange(changedLayers)
} }
onLayerIdChange = (oldId, newId) => { onLayerIdChange = (index, oldId, newId) => {
const changedLayers = this.state.mapStyle.layers.slice(0) const changedLayers = this.state.mapStyle.layers.slice(0)
const idx = style.indexOfLayer(changedLayers, oldId) changedLayers[index] = {
...changedLayers[index],
changedLayers[idx] = {
...changedLayers[idx],
id: newId id: newId
} }
this.onLayersChange(changedLayers) this.onLayersChange(changedLayers)
} }
onLayerChanged = (layer) => { onLayerChanged = (index, layer) => {
const changedLayers = this.state.mapStyle.layers.slice(0) const changedLayers = this.state.mapStyle.layers.slice(0)
const idx = style.indexOfLayer(changedLayers, layer.id) changedLayers[index] = layer
changedLayers[idx] = layer
this.onLayersChange(changedLayers) this.onLayersChange(changedLayers)
} }
@ -645,9 +675,8 @@ export default class App extends React.Component {
</div> </div>
} }
onLayerSelect = (layerId) => { onLayerSelect = (index) => {
const idx = style.indexOfLayer(this.state.mapStyle.layers, layerId) this.setState({ selectedLayerIndex: index })
this.setState({ selectedLayerIndex: idx })
} }
setModal(modalName, value) { setModal(modalName, value) {
@ -735,6 +764,7 @@ export default class App extends React.Component {
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}
selectedLayerIndex={this.state.selectedLayerIndex}
onLayerSelect={this.onLayerSelect} onLayerSelect={this.onLayerSelect}
mapStyle={this.state.mapStyle} mapStyle={this.state.mapStyle}
errors={this.state.errors} errors={this.state.errors}

View file

@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import {formatLayerId} from './util/format';
class MessagePanel extends React.Component { class MessagePanel extends React.Component {
static propTypes = { static propTypes = {
@ -8,6 +9,7 @@ class MessagePanel extends React.Component {
mapStyle: PropTypes.object, mapStyle: PropTypes.object,
onLayerSelect: PropTypes.func, onLayerSelect: PropTypes.func,
currentLayer: PropTypes.object, currentLayer: PropTypes.object,
selectedLayerIndex: PropTypes.number,
} }
static defaultProps = { static defaultProps = {
@ -15,6 +17,7 @@ class MessagePanel extends React.Component {
} }
render() { render() {
const {selectedLayerIndex} = this.props;
const errors = this.props.errors.map((error, idx) => { const errors = this.props.errors.map((error, idx) => {
let content; let content;
if (error.parsed && error.parsed.type === "layer") { if (error.parsed && error.parsed.type === "layer") {
@ -23,13 +26,13 @@ class MessagePanel extends React.Component {
const layerId = mapStyle.layers[parsed.data.index].id; const layerId = mapStyle.layers[parsed.data.index].id;
content = ( content = (
<> <>
Layer <span>&apos;{layerId}&apos;</span>: {parsed.data.message} Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
{currentLayer.id !== layerId && {selectedLayerIndex !== parsed.data.index &&
<> <>
&nbsp;&mdash;&nbsp; &nbsp;&mdash;&nbsp;
<button <button
className="maputnik-message-panel__switch-button" className="maputnik-message-panel__switch-button"
onClick={() => this.props.onLayerSelect(layerId)} onClick={() => this.props.onLayerSelect(parsed.data.index)}
> >
switch to layer switch to layer
</button> </button>

View file

@ -79,6 +79,11 @@ class StringInput extends React.Component {
this.props.onChange(this.state.value); this.props.onChange(this.state.value);
} }
}, },
onKeyDown: (e) => {
if (e.keyCode === 13) {
this.props.onChange(this.state.value);
}
},
required: this.props.required, required: this.props.required,
}); });
} }

View file

@ -19,6 +19,7 @@ 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';
function getLayoutForType (type) { function getLayoutForType (type) {
@ -108,7 +109,10 @@ export default class LayerEditor extends React.Component {
} }
changeProperty(group, property, newValue) { changeProperty(group, property, newValue) {
this.props.onLayerChanged(changeProperty(this.props.layer, group, property, newValue)) this.props.onLayerChanged(
this.props.layerIndex,
changeProperty(this.props.layer, group, property, newValue)
)
} }
onGroupToggle(groupTitle, active) { onGroupToggle(groupTitle, active) {
@ -152,13 +156,16 @@ export default class LayerEditor extends React.Component {
value={this.props.layer.id} value={this.props.layer.id}
wdKey="layer-editor.layer-id" wdKey="layer-editor.layer-id"
error={errorData.id} error={errorData.id}
onChange={newId => this.props.onLayerIdChange(this.props.layer.id, newId)} onChange={newId => this.props.onLayerIdChange(this.props.layerIndex, this.props.layer.id, newId)}
/> />
<LayerTypeBlock <LayerTypeBlock
disabled={true} disabled={true}
error={errorData.type} error={errorData.type}
value={this.props.layer.type} value={this.props.layer.type}
onChange={newType => this.props.onLayerChanged(changeType(this.props.layer, newType))} onChange={newType => this.props.onLayerChanged(
this.props.layerIndex,
changeType(this.props.layer, newType)
)}
/> />
{this.props.layer.type !== 'background' && <LayerSourceBlock {this.props.layer.type !== 'background' && <LayerSourceBlock
error={errorData.sources} error={errorData.sources}
@ -210,7 +217,12 @@ export default class LayerEditor extends React.Component {
/> />
case 'jsoneditor': return <JSONEditor case 'jsoneditor': return <JSONEditor
layer={this.props.layer} layer={this.props.layer}
onChange={this.props.onLayerChanged} onChange={(layer) => {
this.props.onLayerChanged(
this.props.layerIndex,
layer
);
}}
/> />
} }
} }
@ -247,15 +259,15 @@ export default class LayerEditor extends React.Component {
const items = { const items = {
delete: { delete: {
text: "Delete", text: "Delete",
handler: () => this.props.onLayerDestroy(this.props.layer.id) handler: () => this.props.onLayerDestroy(this.props.layerIndex)
}, },
duplicate: { duplicate: {
text: "Duplicate", text: "Duplicate",
handler: () => this.props.onLayerCopy(this.props.layer.id) handler: () => this.props.onLayerCopy(this.props.layerIndex)
}, },
hide: { hide: {
text: (layout.visibility === "none") ? "Show" : "Hide", text: (layout.visibility === "none") ? "Show" : "Hide",
handler: () => this.props.onLayerVisibilityToggle(this.props.layer.id) handler: () => this.props.onLayerVisibilityToggle(this.props.layerIndex)
}, },
moveLayerUp: { moveLayerUp: {
text: "Move layer up", text: "Move layer up",
@ -281,7 +293,7 @@ export default class LayerEditor extends React.Component {
<header> <header>
<div className="layer-header"> <div className="layer-header">
<h2 className="layer-header__title"> <h2 className="layer-header__title">
Layer: {this.props.layer.id} Layer: {formatLayerId(this.props.layer.id)}
</h2> </h2>
<div className="layer-header__info"> <div className="layer-header__info">
<Wrapper <Wrapper

View file

@ -10,11 +10,13 @@ class LayerIdBlock extends React.Component {
value: PropTypes.string.isRequired, value: PropTypes.string.isRequired,
wdKey: PropTypes.string.isRequired, wdKey: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
error: PropTypes.object.isRequired,
} }
render() { render() {
return <InputBlock label={"ID"} fieldSpec={latest.layer.id} return <InputBlock label={"ID"} fieldSpec={latest.layer.id}
data-wd-key={this.props.wdKey} data-wd-key={this.props.wdKey}
error={this.props.error}
> >
<StringInput <StringInput
value={this.props.value} value={this.props.value}

View file

@ -165,12 +165,15 @@ class LayerListContainer extends React.Component {
const listItems = [] const listItems = []
let idx = 0 let idx = 0
this.groupedLayers().forEach(layers => { const layerIdCount = new Map();
const layersByGroup = this.groupedLayers();
layersByGroup.forEach(layers => {
const groupPrefix = layerPrefix(layers[0].id) const groupPrefix = layerPrefix(layers[0].id)
if(layers.length > 1) { if(layers.length > 1) {
const grp = <LayerListGroup const grp = <LayerListGroup
data-wd-key={[groupPrefix, idx].join('-')} data-wd-key={[groupPrefix, idx].join('-')}
key={[groupPrefix, idx].join('-')} key={`group-${groupPrefix}`}
title={groupPrefix} title={groupPrefix}
isActive={!this.isCollapsed(groupPrefix, idx) || idx === this.props.selectedLayerIndex} isActive={!this.isCollapsed(groupPrefix, idx) || idx === this.props.selectedLayerIndex}
onActiveToggle={this.toggleLayerGroup.bind(this, groupPrefix, idx)} onActiveToggle={this.toggleLayerGroup.bind(this, groupPrefix, idx)}
@ -189,6 +192,10 @@ class LayerListContainer extends React.Component {
); );
}); });
layerIdCount.set(layer.id,
layerIdCount.has(layer.id) ? layerIdCount.get(layer.id) + 1 : 0
);
const key = `${layer.id}-${layerIdCount.get(layer.id)}`;
const listItem = <LayerListItem const listItem = <LayerListItem
className={classnames({ className={classnames({
'maputnik-layer-list-item-collapsed': layers.length > 1 && this.isCollapsed(groupPrefix, groupIdx) && idx !== this.props.selectedLayerIndex, 'maputnik-layer-list-item-collapsed': layers.length > 1 && this.isCollapsed(groupPrefix, groupIdx) && idx !== this.props.selectedLayerIndex,
@ -196,8 +203,9 @@ class LayerListContainer extends React.Component {
'maputnik-layer-list-item--error': !!layerError 'maputnik-layer-list-item--error': !!layerError
})} })}
index={idx} index={idx}
key={layer.id} key={key}
layerId={layer.id} layerId={layer.id}
layerIndex={idx}
layerType={layer.type} layerType={layer.type}
visibility={(layer.layout || {}).visibility} visibility={(layer.layout || {}).visibility}
isSelected={idx === this.props.selectedLayerIndex} isSelected={idx === this.props.selectedLayerIndex}

View file

@ -62,6 +62,7 @@ class IconAction extends React.Component {
class LayerListItem extends React.Component { class LayerListItem extends React.Component {
static propTypes = { static propTypes = {
layerIndex: PropTypes.number.isRequired,
layerId: PropTypes.string.isRequired, layerId: PropTypes.string.isRequired,
layerType: PropTypes.string.isRequired, layerType: PropTypes.string.isRequired,
isSelected: PropTypes.bool, isSelected: PropTypes.bool,
@ -97,7 +98,7 @@ class LayerListItem extends React.Component {
return <li return <li
key={this.props.layerId} key={this.props.layerId}
onClick={e => this.props.onLayerSelect(this.props.layerId)} 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,
@ -110,20 +111,20 @@ 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.layerId)} 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.layerId)} 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.layerId)} onClick={e => this.props.onLayerVisibilityToggle(this.props.layerIndex)}
/> />
</li> </li>
} }

View file

@ -0,0 +1,3 @@
export function formatLayerId (id) {
return id === "" ? "[empty_string]" : `'${id}'`;
}