Fix UI when loading invalid style with duplicate layer ids.

Also fix for throwing error when 2 layers exist with empty strings as ids.
This commit is contained in:
orangemug 2020-03-28 10:58:47 +00:00
parent f70d078ec6
commit 74b47e7e74
6 changed files with 82 additions and 38 deletions

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,6 +326,30 @@ export default class App extends React.Component {
}; };
const errors = validate(newStyle, latest) || []; const errors = validate(newStyle, latest) || [];
// 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)}`;
layerErrors.push({
message,
parsed: {
type: "layer",
data: {
index,
message,
}
}
});
}
foundLayers.set(layer.id, true);
});
}
const mappedErrors = errors.map(error => { const mappedErrors = errors.map(error => {
const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/); const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/);
if (layerMatch) { if (layerMatch) {
@ -347,7 +372,7 @@ export default class App extends React.Component {
message: error.message, message: error.message,
}; };
} }
}) }).concat(layerErrors);
let dirtyMapStyle = undefined; let dirtyMapStyle = undefined;
if (errors.length > 0) { if (errors.length > 0) {
@ -437,56 +462,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 +664,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) {

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 = {
@ -23,7 +24,7 @@ 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 && {currentLayer.id !== layerId &&
<> <>
&nbsp;&mdash;&nbsp; &nbsp;&mdash;&nbsp;

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

@ -107,7 +107,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) {
@ -151,13 +154,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}
@ -209,7 +215,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
);
}}
/> />
} }
} }
@ -242,15 +253,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",

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>
} }