Added initial expression work and UI errors.

This commit is contained in:
orangemug 2020-01-29 08:22:03 +00:00
parent 63ed8c1de3
commit 725b752e35
25 changed files with 360 additions and 53 deletions

View file

@ -207,6 +207,7 @@ export default class App extends React.Component {
errors: [], errors: [],
infos: [], infos: [],
mapStyle: style.emptyStyle, mapStyle: style.emptyStyle,
hopefulMapStyle: style.emptyStyle,
selectedLayerIndex: 0, selectedLayerIndex: 0,
sources: {}, sources: {},
vectorLayers: {}, vectorLayers: {},
@ -279,7 +280,7 @@ export default class App extends React.Component {
} }
updateFonts(urlTemplate) { updateFonts(urlTemplate) {
const metadata = this.state.mapStyle.metadata || {} const metadata = this.state.hopefulMapStyle.metadata || {}
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;
@ -318,24 +319,50 @@ export default class App extends React.Component {
onStyleChanged = (newStyle, save=true) => { onStyleChanged = (newStyle, save=true) => {
const errors = validate(newStyle, latest) const errors = validate(newStyle, latest)
const mappedErrors = errors.map(error => {
const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/);
if (layerMatch) {
const [matchStr, index, group, property, message] = layerMatch;
const key = (group && property) ? [group, property].join(".") : property;
return {
message: error.message,
parsed: {
type: "layer",
data: {
index,
key,
message
}
}
}
}
else {
return {
message: error.message,
};
}
})
if(errors.length === 0) { if(errors.length === 0) {
if(newStyle.glyphs !== this.state.mapStyle.glyphs) { if(newStyle.glyphs !== this.state.hopefulMapStyle.glyphs) {
this.updateFonts(newStyle.glyphs) this.updateFonts(newStyle.glyphs)
} }
if(newStyle.sprite !== this.state.mapStyle.sprite) { if(newStyle.sprite !== this.state.hopefulMapStyle.sprite) {
this.updateIcons(newStyle.sprite) this.updateIcons(newStyle.sprite)
} }
this.revisionStore.addRevision(newStyle) this.revisionStore.addRevision(newStyle)
if(save) this.saveStyle(newStyle) if(save) this.saveStyle(newStyle)
this.setState({ this.setState({
hopefulMapStyle: newStyle,
mapStyle: newStyle, mapStyle: newStyle,
errors: [], errors: [],
}) })
} else { } else {
this.setState({ this.setState({
errors: errors.map(err => err.message) hopefulMapStyle: newStyle,
errors: mappedErrors,
}) })
} }
@ -344,7 +371,7 @@ export default class App extends React.Component {
onUndo = () => { onUndo = () => {
const activeStyle = this.revisionStore.undo() const activeStyle = this.revisionStore.undo()
const messages = undoMessages(this.state.mapStyle, activeStyle) const messages = undoMessages(this.state.hopefulMapStyle, activeStyle)
this.saveStyle(activeStyle) this.saveStyle(activeStyle)
this.setState({ this.setState({
mapStyle: activeStyle, mapStyle: activeStyle,
@ -354,7 +381,7 @@ export default class App extends React.Component {
onRedo = () => { onRedo = () => {
const activeStyle = this.revisionStore.redo() const activeStyle = this.revisionStore.redo()
const messages = redoMessages(this.state.mapStyle, activeStyle) const messages = redoMessages(this.state.hopefulMapStyle, activeStyle)
this.saveStyle(activeStyle) this.saveStyle(activeStyle)
this.setState({ this.setState({
mapStyle: activeStyle, mapStyle: activeStyle,
@ -364,7 +391,7 @@ export default class App extends React.Component {
onMoveLayer = (move) => { onMoveLayer = (move) => {
let { oldIndex, newIndex } = move; let { oldIndex, newIndex } = move;
let layers = this.state.mapStyle.layers; let layers = this.state.hopefulMapStyle.layers;
oldIndex = clamp(oldIndex, 0, layers.length-1); oldIndex = clamp(oldIndex, 0, layers.length-1);
newIndex = clamp(newIndex, 0, layers.length-1); newIndex = clamp(newIndex, 0, layers.length-1);
if(oldIndex === newIndex) return; if(oldIndex === newIndex) return;
@ -382,14 +409,14 @@ export default class App extends React.Component {
onLayersChange = (changedLayers) => { onLayersChange = (changedLayers) => {
const changedStyle = { const changedStyle = {
...this.state.mapStyle, ...this.state.hopefulMapStyle,
layers: changedLayers layers: changedLayers
} }
this.onStyleChanged(changedStyle) this.onStyleChanged(changedStyle)
} }
onLayerDestroy = (layerId) => { onLayerDestroy = (layerId) => {
let layers = this.state.mapStyle.layers; let layers = this.state.hopefulMapStyle.layers;
const remainingLayers = layers.slice(0); const remainingLayers = layers.slice(0);
const idx = style.indexOfLayer(remainingLayers, layerId) const idx = style.indexOfLayer(remainingLayers, layerId)
remainingLayers.splice(idx, 1); remainingLayers.splice(idx, 1);
@ -408,7 +435,7 @@ export default class App extends React.Component {
} }
onLayerVisibilityToggle = (layerId) => { onLayerVisibilityToggle = (layerId) => {
let layers = this.state.mapStyle.layers; let layers = this.state.hopefulMapStyle.layers;
const changedLayers = layers.slice(0) const changedLayers = layers.slice(0)
const idx = style.indexOfLayer(changedLayers, layerId) const idx = style.indexOfLayer(changedLayers, layerId)
@ -423,7 +450,7 @@ export default class App extends React.Component {
onLayerIdChange = (oldId, newId) => { onLayerIdChange = (oldId, newId) => {
const changedLayers = this.state.mapStyle.layers.slice(0) const changedLayers = this.state.hopefulMapStyle.layers.slice(0)
const idx = style.indexOfLayer(changedLayers, oldId) const idx = style.indexOfLayer(changedLayers, oldId)
changedLayers[idx] = { changedLayers[idx] = {
@ -435,7 +462,8 @@ export default class App extends React.Component {
} }
onLayerChanged = (layer) => { onLayerChanged = (layer) => {
const changedLayers = this.state.mapStyle.layers.slice(0) console.log("test: onLayerChanged", layer);
const changedLayers = this.state.hopefulMapStyle.layers.slice(0)
const idx = style.indexOfLayer(changedLayers, layer.id) const idx = style.indexOfLayer(changedLayers, layer.id)
changedLayers[idx] = layer changedLayers[idx] = layer
@ -472,7 +500,7 @@ export default class App extends React.Component {
fetchSources() { fetchSources() {
const sourceList = {...this.state.sources}; const sourceList = {...this.state.sources};
for(let [key, val] of Object.entries(this.state.mapStyle.sources)) { for(let [key, val] of Object.entries(this.state.hopefulMapStyle.sources)) {
if(sourceList.hasOwnProperty(key)) { if(sourceList.hasOwnProperty(key)) {
continue; continue;
} }
@ -596,7 +624,7 @@ export default class App extends React.Component {
} }
onLayerSelect = (layerId) => { onLayerSelect = (layerId) => {
const idx = style.indexOfLayer(this.state.mapStyle.layers, layerId) const idx = style.indexOfLayer(this.state.hopefulMapStyle.layers, layerId)
this.setState({ selectedLayerIndex: idx }) this.setState({ selectedLayerIndex: idx })
} }
@ -636,14 +664,14 @@ export default class App extends React.Component {
} }
render() { render() {
const layers = this.state.mapStyle.layers || [] const layers = this.state.hopefulMapStyle.layers || []
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : null const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : null
const metadata = this.state.mapStyle.metadata || {} const metadata = this.state.hopefulMapStyle.metadata || {}
const toolbar = <Toolbar const toolbar = <Toolbar
renderer={this._getRenderer()} renderer={this._getRenderer()}
mapState={this.state.mapState} mapState={this.state.mapState}
mapStyle={this.state.mapStyle} mapStyle={this.state.hopefulMapStyle}
inspectModeEnabled={this.state.mapState === "inspect"} inspectModeEnabled={this.state.mapState === "inspect"}
sources={this.state.sources} sources={this.state.sources}
onStyleChanged={this.onStyleChanged} onStyleChanged={this.onStyleChanged}
@ -662,6 +690,7 @@ export default class App extends React.Component {
selectedLayerIndex={this.state.selectedLayerIndex} selectedLayerIndex={this.state.selectedLayerIndex}
layers={layers} layers={layers}
sources={this.state.sources} sources={this.state.sources}
errors={this.state.errors}
/> />
const layerEditor = selectedLayer ? <LayerEditor const layerEditor = selectedLayer ? <LayerEditor
@ -669,7 +698,7 @@ export default class App extends React.Component {
layer={selectedLayer} layer={selectedLayer}
layerIndex={this.state.selectedLayerIndex} layerIndex={this.state.selectedLayerIndex}
isFirstLayer={this.state.selectedLayerIndex < 1} isFirstLayer={this.state.selectedLayerIndex < 1}
isLastLayer={this.state.selectedLayerIndex === this.state.mapStyle.layers.length-1} isLastLayer={this.state.selectedLayerIndex === this.state.hopefulMapStyle.layers.length-1}
sources={this.state.sources} sources={this.state.sources}
vectorLayers={this.state.vectorLayers} vectorLayers={this.state.vectorLayers}
spec={this.state.spec} spec={this.state.spec}
@ -679,9 +708,11 @@ export default class App extends React.Component {
onLayerCopy={this.onLayerCopy} onLayerCopy={this.onLayerCopy}
onLayerVisibilityToggle={this.onLayerVisibilityToggle} onLayerVisibilityToggle={this.onLayerVisibilityToggle}
onLayerIdChange={this.onLayerIdChange} onLayerIdChange={this.onLayerIdChange}
errors={this.state.errors}
/> : null /> : null
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
mapStyle={this.state.mapStyle}
errors={this.state.errors} errors={this.state.errors}
infos={this.state.infos} infos={this.state.infos}
/> : null /> : null
@ -704,7 +735,7 @@ export default class App extends React.Component {
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')} onOpenToggle={this.toggleModal.bind(this, 'shortcuts')}
/> />
<SettingsModal <SettingsModal
mapStyle={this.state.mapStyle} mapStyle={this.state.hopefulMapStyle}
onStyleChanged={this.onStyleChanged} onStyleChanged={this.onStyleChanged}
onChangeMetadataProperty={this.onChangeMetadataProperty} onChangeMetadataProperty={this.onChangeMetadataProperty}
isOpen={this.state.isOpen.settings} isOpen={this.state.isOpen.settings}
@ -712,7 +743,7 @@ export default class App extends React.Component {
openlayersDebugOptions={this.state.openlayersDebugOptions} openlayersDebugOptions={this.state.openlayersDebugOptions}
/> />
<ExportModal <ExportModal
mapStyle={this.state.mapStyle} mapStyle={this.state.hopefulMapStyle}
onStyleChanged={this.onStyleChanged} onStyleChanged={this.onStyleChanged}
isOpen={this.state.isOpen.export} isOpen={this.state.isOpen.export}
onOpenToggle={this.toggleModal.bind(this, 'export')} onOpenToggle={this.toggleModal.bind(this, 'export')}
@ -723,7 +754,7 @@ export default class App extends React.Component {
onOpenToggle={this.toggleModal.bind(this, 'open')} onOpenToggle={this.toggleModal.bind(this, 'open')}
/> />
<SourcesModal <SourcesModal
mapStyle={this.state.mapStyle} mapStyle={this.state.hopefulMapStyle}
onStyleChanged={this.onStyleChanged} onStyleChanged={this.onStyleChanged}
isOpen={this.state.isOpen.sources} isOpen={this.state.isOpen.sources}
onOpenToggle={this.toggleModal.bind(this, 'sources')} onOpenToggle={this.toggleModal.bind(this, 'sources')}

View file

@ -8,8 +8,23 @@ class MessagePanel extends React.Component {
} }
render() { render() {
const errors = this.props.errors.map((m, i) => { const errors = this.props.errors.map((error, idx) => {
return <p key={"error-"+i} className="maputnik-message-panel-error">{m}</p> let content;
if (error.parsed && error.parsed.type === "layer") {
const {parsed} = error;
const {mapStyle} = this.props;
content = (
<>
Layer <span>'{mapStyle.layers[parsed.data.index].id}'</span>: {parsed.data.message}
</>
);
}
else {
content = error.message;
}
return <p key={"error-"+idx} className="maputnik-message-panel-error">
{content}
</p>
}) })
const infos = this.props.infos.map((m, i) => { const infos = this.props.infos.map((m, i) => {

View file

@ -4,6 +4,9 @@ import PropTypes from 'prop-types'
import SpecProperty from './_SpecProperty' import SpecProperty from './_SpecProperty'
import DataProperty from './_DataProperty' import DataProperty from './_DataProperty'
import ZoomProperty from './_ZoomProperty' import ZoomProperty from './_ZoomProperty'
import ExpressionProperty from './_ExpressionProperty'
import {expression} from '@mapbox/mapbox-gl-style-spec'
function isZoomField(value) { function isZoomField(value) {
@ -14,6 +17,24 @@ function isDataField(value) {
return typeof value === 'object' && value.stops && typeof value.property !== 'undefined' return typeof value === 'object' && value.stops && typeof value.property !== 'undefined'
} }
/**
* So here we can't just check is `Array.isArray(value)` because certain
* properties accept arrays as values, for example `text-font`. So we must try
* and create an expression.
*/
function isExpression(value, fieldSpec={}) {
if (!Array.isArray(value)) {
return false;
}
try {
const out = expression.createExpression(value, fieldSpec);
return (out.result === "success");
}
catch (err) {
return false;
}
}
/** /**
* If we don't have a default value just make one up * If we don't have a default value just make one up
*/ */
@ -108,6 +129,11 @@ export default class FunctionSpecProperty extends React.Component {
this.props.onChange(this.props.fieldName, zoomFunc) this.props.onChange(this.props.fieldName, zoomFunc)
} }
makeExpression = () => {
const expression = ["literal", this.props.value || this.props.fieldSpec.default];
this.props.onChange(this.props.fieldName, expression);
}
makeDataFunction = () => { makeDataFunction = () => {
const functionType = this.getFieldFunctionType(this.props.fieldSpec); const functionType = this.getFieldFunctionType(this.props.fieldSpec);
const stopValue = functionType === 'categorical' ? '' : 0; const stopValue = functionType === 'categorical' ? '' : 0;
@ -126,9 +152,21 @@ export default class FunctionSpecProperty extends React.Component {
const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property" const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property"
let specField; let specField;
if (isZoomField(this.props.value)) { if (isExpression(this.props.value, this.props.fieldSpec)) {
specField = (
<ExpressionProperty
error={this.props.error}
onChange={this.props.onChange.bind(this, this.props.fieldName)}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={this.props.value}
/>
);
}
else if (isZoomField(this.props.value)) {
specField = ( specField = (
<ZoomProperty <ZoomProperty
error={this.props.error}
onChange={this.props.onChange.bind(this)} onChange={this.props.onChange.bind(this)}
fieldName={this.props.fieldName} fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec} fieldSpec={this.props.fieldSpec}
@ -141,6 +179,7 @@ export default class FunctionSpecProperty extends React.Component {
else if (isDataField(this.props.value)) { else if (isDataField(this.props.value)) {
specField = ( specField = (
<DataProperty <DataProperty
error={this.props.error}
onChange={this.props.onChange.bind(this)} onChange={this.props.onChange.bind(this)}
fieldName={this.props.fieldName} fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec} fieldSpec={this.props.fieldSpec}
@ -153,12 +192,14 @@ export default class FunctionSpecProperty extends React.Component {
else { else {
specField = ( specField = (
<SpecProperty <SpecProperty
error={this.props.error}
onChange={this.props.onChange.bind(this)} onChange={this.props.onChange.bind(this)}
fieldName={this.props.fieldName} fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec} fieldSpec={this.props.fieldSpec}
value={this.props.value} value={this.props.value}
onZoomClick={this.makeZoomFunction} onZoomClick={this.makeZoomFunction}
onDataClick={this.makeDataFunction} onDataClick={this.makeDataFunction}
onExpressionClick={this.makeExpression}
/> />
) )
} }

View file

@ -48,14 +48,18 @@ export default class PropertyGroup extends React.Component {
} }
render() { render() {
const {errors} = this.props;
const fields = this.props.groupFields.map(fieldName => { const fields = this.props.groupFields.map(fieldName => {
const fieldSpec = getFieldSpec(this.props.spec, this.props.layer.type, fieldName) const fieldSpec = getFieldSpec(this.props.spec, this.props.layer.type, fieldName)
const paint = this.props.layer.paint || {} const paint = this.props.layer.paint || {}
const layout = this.props.layer.layout || {} const layout = this.props.layer.layout || {}
const fieldValue = fieldName in paint ? paint[fieldName] : layout[fieldName] const fieldValue = fieldName in paint ? paint[fieldName] : layout[fieldName]
const fieldType = fieldName in paint ? 'paint' : 'layout';
const errorKey = fieldType+"."+fieldName;
return <FunctionSpecField return <FunctionSpecField
error={errors[errorKey]}
onChange={this.onPropertyChange} onChange={this.onPropertyChange}
key={fieldName} key={fieldName}
fieldName={fieldName} fieldName={fieldName}

View file

@ -200,6 +200,7 @@ export default class DataProperty extends React.Component {
return <div className="maputnik-data-spec-block"> return <div className="maputnik-data-spec-block">
<div className="maputnik-data-spec-property"> <div className="maputnik-data-spec-property">
<InputBlock <InputBlock
error={this.props.error}
doc={this.props.fieldSpec.doc} doc={this.props.fieldSpec.doc}
label={labelFromFieldName(this.props.fieldName)} label={labelFromFieldName(this.props.fieldName)}
> >

View file

@ -0,0 +1,87 @@
import React from 'react'
import PropTypes from 'prop-types'
import InputBlock from '../inputs/InputBlock'
import Button from '../Button'
import {MdDelete} from 'react-icons/md'
import StringInput from '../inputs/StringInput'
import labelFromFieldName from './_labelFromFieldName'
import stringifyPretty from 'json-stringify-pretty-compact'
function isLiteralExpression (value) {
return (Array.isArray(value) && value.length === 2 && value[0] === "literal");
}
export default class ExpressionProperty extends React.Component {
static propTypes = {
onDeleteStop: PropTypes.func,
fieldName: PropTypes.string,
fieldSpec: PropTypes.object
}
constructor (props) {
super();
this.state = {
lastValue: props.value,
};
}
onChange = (value) => {
try {
const jsonVal = JSON.parse(value);
if (isLiteralExpression(jsonVal)) {
this.setState({
lastValue: jsonVal
});
}
this.props.onChange(jsonVal);
}
catch (err) {
// TODO: Handle JSON parse error
}
}
onDelete = () => {
const {lastValue} = this.state;
const {value, fieldName, fieldSpec} = this.props;
if (isLiteralExpression(value)) {
this.props.onChange(value[1]);
}
else if (isLiteralExpression(lastValue)) {
this.props.onChange(lastValue[1]);
}
else {
this.props.onChange(fieldSpec.default);
}
}
render() {
const deleteStopBtn = (
<Button
onClick={this.onDelete}
className="maputnik-delete-stop"
>
<MdDelete />
</Button>
);
return <InputBlock
error={this.props.error}
doc={this.props.fieldSpec.doc}
label={labelFromFieldName(this.props.fieldName)}
action={deleteStopBtn}
>
<StringInput
multi={true}
value={stringifyPretty(this.props.value, {indent: 2})}
spellCheck={false}
onInput={this.onChange}
/>
</InputBlock>
}
}

View file

@ -6,16 +6,47 @@ import Button from '../Button'
import {MdFunctions, MdInsertChart} from 'react-icons/md' import {MdFunctions, MdInsertChart} from 'react-icons/md'
/**
* So here we can't just check is `Array.isArray(value)` because certain
* properties accept arrays as values, for example `text-font`. So we must try
* and create an expression.
*/
function isExpression(value, fieldSpec={}) {
if (!Array.isArray(value)) {
return false;
}
try {
expression.createExpression(value, fieldSpec);
return true;
}
catch (err) {
return false;
}
}
export default class FunctionButtons extends React.Component { export default class FunctionButtons extends React.Component {
static propTypes = { static propTypes = {
fieldSpec: PropTypes.object, fieldSpec: PropTypes.object,
onZoomClick: PropTypes.func, onZoomClick: PropTypes.func,
onDataClick: PropTypes.func, onDataClick: PropTypes.func,
onExpressionClick: PropTypes.func,
} }
render() { render() {
let makeZoomButton, makeDataButton let makeZoomButton, makeDataButton, expressionButton;
if (this.props.fieldSpec.expression.parameters.includes('zoom')) { if (this.props.fieldSpec.expression.parameters.includes('zoom')) {
expressionButton = (
<Button
className="maputnik-make-zoom-function"
onClick={this.props.onExpressionClick}
>
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
<path fill="currentColor" d="M12.42,5.29C11.32,5.19 10.35,6 10.25,7.11L10,10H12.82V12H9.82L9.38,17.07C9.18,19.27 7.24,20.9 5.04,20.7C3.79,20.59 2.66,19.9 2,18.83L3.5,17.33C3.83,18.38 4.96,18.97 6,18.63C6.78,18.39 7.33,17.7 7.4,16.89L7.82,12H4.82V10H8L8.27,6.93C8.46,4.73 10.39,3.1 12.6,3.28C13.86,3.39 15,4.09 15.66,5.17L14.16,6.67C13.91,5.9 13.23,5.36 12.42,5.29M22,13.65L20.59,12.24L17.76,15.07L14.93,12.24L13.5,13.65L16.35,16.5L13.5,19.31L14.93,20.72L17.76,17.89L20.59,20.72L22,19.31L19.17,16.5L22,13.65Z" />
</svg>
</Button>
);
makeZoomButton = <Button makeZoomButton = <Button
className="maputnik-make-zoom-function" className="maputnik-make-zoom-function"
onClick={this.props.onZoomClick} onClick={this.props.onZoomClick}
@ -39,10 +70,14 @@ export default class FunctionButtons extends React.Component {
/> />
</Button> </Button>
} }
return <div>{makeDataButton}{makeZoomButton}</div> return <div>
{expressionButton}
{makeDataButton}
{makeZoomButton}
</div>
} }
else { else {
return null return <div>{expressionButton}</div>
} }
} }
} }

View file

@ -21,9 +21,12 @@ export default class SpecProperty extends React.Component {
fieldSpec={this.props.fieldSpec} fieldSpec={this.props.fieldSpec}
onZoomClick={this.props.onZoomClick} onZoomClick={this.props.onZoomClick}
onDataClick={this.props.onDataClick} onDataClick={this.props.onDataClick}
value={this.props.value}
onExpressionClick={this.props.onExpressionClick}
/> />
return <InputBlock return <InputBlock
error={this.props.error}
doc={this.props.fieldSpec.doc} doc={this.props.fieldSpec.doc}
label={labelFromFieldName(this.props.fieldName)} label={labelFromFieldName(this.props.fieldName)}
action={functionBtn} action={functionBtn}

View file

@ -124,6 +124,7 @@ export default class ZoomProperty extends React.Component {
const deleteStopBtn= <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} /> const deleteStopBtn= <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} />
return <InputBlock return <InputBlock
error={this.props.error}
key={key} key={key}
doc={this.props.fieldSpec.doc} doc={this.props.fieldSpec.doc}
label={labelFromFieldName(this.props.fieldName)} label={labelFromFieldName(this.props.fieldName)}

View file

@ -67,15 +67,25 @@ export default class CombiningFilterEditor extends React.Component {
const filter = this.combiningFilter() const filter = this.combiningFilter()
let combiningOp = filter[0] let combiningOp = filter[0]
let filters = filter.slice(1) let filters = filter.slice(1)
const {errors} = this.props;
const editorBlocks = filters.map((f, idx) => { const editorBlocks = filters.map((f, idx) => {
return <FilterEditorBlock key={idx} onDelete={this.deleteFilterItem.bind(this, idx)}> const error = errors[`filter[${idx+1}]`];
<SingleFilterEditor
properties={this.props.properties} return (
filter={f} <>
onChange={this.onFilterPartChanged.bind(this, idx + 1)} <FilterEditorBlock key={idx} onDelete={this.deleteFilterItem.bind(this, idx)}>
/> <SingleFilterEditor
</FilterEditorBlock> properties={this.props.properties}
filter={f}
onChange={this.onFilterPartChanged.bind(this, idx + 1)}
/>
</FilterEditorBlock>
{error &&
<div className="maputnik-inline-error">{error.message}</div>
}
</>
);
}) })
//TODO: Implement support for nested filter //TODO: Implement support for nested filter

View file

@ -6,6 +6,7 @@ import FillIcon from './FillIcon.jsx'
import SymbolIcon from './SymbolIcon.jsx' import SymbolIcon from './SymbolIcon.jsx'
import BackgroundIcon from './BackgroundIcon.jsx' import BackgroundIcon from './BackgroundIcon.jsx'
import CircleIcon from './CircleIcon.jsx' import CircleIcon from './CircleIcon.jsx'
import MissingIcon from './MissingIcon.jsx'
class LayerIcon extends React.Component { class LayerIcon extends React.Component {
static propTypes = { static propTypes = {
@ -25,6 +26,7 @@ class LayerIcon extends React.Component {
case 'line': return <LineIcon {...iconProps} /> case 'line': return <LineIcon {...iconProps} />
case 'symbol': return <SymbolIcon {...iconProps} /> case 'symbol': return <SymbolIcon {...iconProps} />
case 'circle': return <CircleIcon {...iconProps} /> case 'circle': return <CircleIcon {...iconProps} />
default: return <MissingIcon {...iconProps} />
} }
} }
} }

View file

@ -0,0 +1,11 @@
import React from 'react'
import {MdPriorityHigh} from 'react-icons/md'
export default class MissingIcon extends React.Component {
render() {
return (
<MdPriorityHigh {...this.props} />
)
}
}

View file

@ -52,6 +52,11 @@ class InputBlock extends React.Component {
<div className="maputnik-input-block-content"> <div className="maputnik-input-block-content">
{this.props.children} {this.props.children}
</div> </div>
{this.props.error &&
<div className="maputnik-inline-error">
{this.props.error.message}
</div>
}
</div> </div>
} }
} }

View file

@ -11,6 +11,7 @@ class StringInput extends React.Component {
onInput: PropTypes.func, onInput: PropTypes.func,
multi: PropTypes.bool, multi: PropTypes.bool,
required: PropTypes.bool, required: PropTypes.bool,
disabled: PropTypes.bool,
} }
static defaultProps = { static defaultProps = {
@ -51,9 +52,14 @@ class StringInput extends React.Component {
] ]
} }
if(!!this.props.disabled) {
classes.push("maputnik-string--disabled");
}
return React.createElement(tag, { return React.createElement(tag, {
"data-wd-key": this.props["data-wd-key"], "data-wd-key": this.props["data-wd-key"],
spellCheck: !(tag === "input"), spellCheck: this.props.hasOwnProperty("spellCheck") ? this.props.spellCheck : !(tag === "input"),
disabled: this.props.disabled,
className: classes.join(" "), className: classes.join(" "),
style: this.props.style, style: this.props.style,
value: this.state.value === undefined ? "" : this.state.value, value: this.state.value === undefined ? "" : this.state.value,

View file

@ -20,6 +20,10 @@ import { changeType, changeProperty } from '../../libs/layer'
import layout from '../../config/layout.json' import layout from '../../config/layout.json'
function getLayoutForType (type) {
return layout[type] ? layout[type] : layout.invalid;
}
function layoutGroups(layerType) { function layoutGroups(layerType) {
const layerGroup = { const layerGroup = {
title: 'Layer', title: 'Layer',
@ -33,7 +37,9 @@ function layoutGroups(layerType) {
title: 'JSON Editor', title: 'JSON Editor',
type: 'jsoneditor' type: 'jsoneditor'
} }
return [layerGroup, filterGroup].concat(layout[layerType].groups).concat([editorGroup]) return [layerGroup, filterGroup]
.concat(getLayoutForType(layerType).groups)
.concat([editorGroup])
} }
/** Layer editor supporting multiple types of layers. */ /** Layer editor supporting multiple types of layers. */
@ -79,7 +85,7 @@ export default class LayerEditor extends React.Component {
static getDerivedStateFromProps(props, state) { static getDerivedStateFromProps(props, state) {
const additionalGroups = { ...state.editorGroups } const additionalGroups = { ...state.editorGroups }
layout[props.layer.type].groups.forEach(group => { getLayoutForType(props.layer.type).groups.forEach(group => {
if(!(group.title in additionalGroups)) { if(!(group.title in additionalGroups)) {
additionalGroups[group.title] = true additionalGroups[group.title] = true
} }
@ -118,6 +124,20 @@ export default class LayerEditor extends React.Component {
if(this.props.layer.metadata) { if(this.props.layer.metadata) {
comment = this.props.layer.metadata['maputnik:comment'] comment = this.props.layer.metadata['maputnik:comment']
} }
const {errors, layerIndex} = this.props;
const errorData = {};
errors.forEach(error => {
if (
error.parsed &&
error.parsed.type === "layer" &&
error.parsed.data.index == layerIndex
) {
errorData[error.parsed.data.key] = {
message: error.parsed.data.message
};
}
})
let sourceLayerIds; let sourceLayerIds;
if(this.props.sources.hasOwnProperty(this.props.layer.source)) { if(this.props.sources.hasOwnProperty(this.props.layer.source)) {
@ -129,13 +149,16 @@ export default class LayerEditor extends React.Component {
<LayerIdBlock <LayerIdBlock
value={this.props.layer.id} value={this.props.layer.id}
wdKey="layer-editor.layer-id" wdKey="layer-editor.layer-id"
error={errorData.id}
onChange={newId => this.props.onLayerIdChange(this.props.layer.id, newId)} onChange={newId => this.props.onLayerIdChange(this.props.layer.id, newId)}
/> />
<LayerTypeBlock <LayerTypeBlock
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(changeType(this.props.layer, newType))}
/> />
{this.props.layer.type !== 'background' && <LayerSourceBlock {this.props.layer.type !== 'background' && <LayerSourceBlock
error={errorData.sources}
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)}
@ -143,20 +166,24 @@ export default class LayerEditor extends React.Component {
} }
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.props.layer.type) < 0 && {['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.props.layer.type) < 0 &&
<LayerSourceLayerBlock <LayerSourceLayerBlock
error={errorData['source-layer']}
sourceLayerIds={sourceLayerIds} sourceLayerIds={sourceLayerIds}
value={this.props.layer['source-layer']} value={this.props.layer['source-layer']}
onChange={v => this.changeProperty(null, 'source-layer', v)} onChange={v => this.changeProperty(null, 'source-layer', v)}
/> />
} }
<MinZoomBlock <MinZoomBlock
error={errorData.minzoom}
value={this.props.layer.minzoom} value={this.props.layer.minzoom}
onChange={v => this.changeProperty(null, 'minzoom', v)} onChange={v => this.changeProperty(null, 'minzoom', v)}
/> />
<MaxZoomBlock <MaxZoomBlock
error={errorData.maxzoom}
value={this.props.layer.maxzoom} value={this.props.layer.maxzoom}
onChange={v => this.changeProperty(null, 'maxzoom', v)} onChange={v => this.changeProperty(null, 'maxzoom', v)}
/> />
<CommentBlock <CommentBlock
error={errorData.comment}
value={comment} value={comment}
onChange={v => this.changeProperty('metadata', 'maputnik:comment', v == "" ? undefined : v)} onChange={v => this.changeProperty('metadata', 'maputnik:comment', v == "" ? undefined : v)}
/> />
@ -164,6 +191,7 @@ export default class LayerEditor extends React.Component {
case 'filter': return <div> case 'filter': return <div>
<div className="maputnik-filter-editor-wrapper"> <div className="maputnik-filter-editor-wrapper">
<FilterEditor <FilterEditor
errors={errorData}
filter={this.props.layer.filter} filter={this.props.layer.filter}
properties={this.props.vectorLayers[this.props.layer['source-layer']]} properties={this.props.vectorLayers[this.props.layer['source-layer']]}
onChange={f => this.changeProperty(null, 'filter', f)} onChange={f => this.changeProperty(null, 'filter', f)}
@ -171,6 +199,7 @@ export default class LayerEditor extends React.Component {
</div> </div>
</div> </div>
case 'properties': return <PropertyGroup case 'properties': return <PropertyGroup
errors={errorData}
layer={this.props.layer} layer={this.props.layer}
groupFields={fields} groupFields={fields}
spec={this.props.spec} spec={this.props.spec}

View file

@ -181,10 +181,19 @@ class LayerListContainer extends React.Component {
layers.forEach((layer, idxInGroup) => { layers.forEach((layer, idxInGroup) => {
const groupIdx = findClosestCommonPrefix(this.props.layers, idx) const groupIdx = findClosestCommonPrefix(this.props.layers, idx)
const layerError = this.props.errors.find(error => {
return (
error.parsed &&
error.parsed.type === "layer" &&
error.parsed.data.index == idx
);
});
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,
'maputnik-layer-list-item-group-last': idxInGroup == layers.length - 1 && layers.length > 1 'maputnik-layer-list-item-group-last': idxInGroup == layers.length - 1 && layers.length > 1,
'maputnik-layer-list-item--error': !!layerError
})} })}
index={idx} index={idx}
key={layer.id} key={layer.id}

View file

@ -4,6 +4,7 @@ import PropTypes from 'prop-types'
import {latest} from '@mapbox/mapbox-gl-style-spec' import {latest} from '@mapbox/mapbox-gl-style-spec'
import InputBlock from '../inputs/InputBlock' import InputBlock from '../inputs/InputBlock'
import SelectInput from '../inputs/SelectInput' import SelectInput from '../inputs/SelectInput'
import StringInput from '../inputs/StringInput'
class LayerTypeBlock extends React.Component { class LayerTypeBlock extends React.Component {
static propTypes = { static propTypes = {
@ -15,21 +16,11 @@ class LayerTypeBlock extends React.Component {
render() { render() {
return <InputBlock label={"Type"} doc={latest.layer.type.doc} return <InputBlock label={"Type"} doc={latest.layer.type.doc}
data-wd-key={this.props.wdKey} data-wd-key={this.props.wdKey}
error={this.props.error}
> >
<SelectInput <StringInput
options={[
['background', 'Background'],
['fill', 'Fill'],
['line', 'Line'],
['symbol', 'Symbol'],
['raster', 'Raster'],
['circle', 'Circle'],
['fill-extrusion', 'Fill Extrusion'],
['hillshade', 'Hillshade'],
['heatmap', 'Heatmap'],
]}
onChange={this.props.onChange}
value={this.props.value} value={this.props.value}
disabled={true}
/> />
</InputBlock> </InputBlock>
} }

View file

@ -13,6 +13,7 @@ class MaxZoomBlock extends React.Component {
render() { render() {
return <InputBlock label={"Max Zoom"} doc={latest.layer.maxzoom.doc} return <InputBlock label={"Max Zoom"} doc={latest.layer.maxzoom.doc}
error={this.props.error}
data-wd-key="max-zoom" data-wd-key="max-zoom"
> >
<NumberInput <NumberInput

View file

@ -13,6 +13,7 @@ class MinZoomBlock extends React.Component {
render() { render() {
return <InputBlock label={"Min Zoom"} doc={latest.layer.minzoom.doc} return <InputBlock label={"Min Zoom"} doc={latest.layer.minzoom.doc}
error={this.props.error}
data-wd-key="min-zoom" data-wd-key="min-zoom"
> >
<NumberInput <NumberInput

View file

@ -233,5 +233,8 @@
] ]
} }
] ]
},
"invalid": {
"groups": []
} }
} }

View file

@ -150,13 +150,13 @@
.maputnik-action-block { .maputnik-action-block {
.maputnik-input-block-label { .maputnik-input-block-label {
display: inline-block; display: inline-block;
width: 35%; width: 32%;
} }
.maputnik-input-block-action { .maputnik-input-block-action {
vertical-align: top; vertical-align: top;
display: inline-block; display: inline-block;
width: 15%; width: 18%;
} }
.maputnik-input-block-action > div { .maputnik-input-block-action > div {

View file

@ -25,6 +25,11 @@
resize: vertical; resize: vertical;
height: 78px; height: 78px;
} }
&--disabled {
background: transparent;
border: none;
}
} }
.maputnik-number-container { .maputnik-number-container {

View file

@ -99,6 +99,11 @@
} }
} }
.maputnik-layer-list-item--error {
color: $color-red;
}
&-item-selected { &-item-selected {
color: $color-white; color: $color-white;
} }

View file

@ -5,6 +5,7 @@
padding-bottom: 0; padding-bottom: 0;
padding-top: 0; padding-top: 0;
vertical-align: middle; vertical-align: middle;
padding: 0 $margin-2 0 0;
@extend .maputnik-icon-button; @extend .maputnik-icon-button;
} }
@ -66,6 +67,7 @@
padding-bottom: 0; padding-bottom: 0;
padding-top: 0; padding-top: 0;
vertical-align: middle; vertical-align: middle;
padding: 0 $margin-2 0 0;
@extend .maputnik-icon-button; @extend .maputnik-icon-button;
} }

View file

@ -34,3 +34,12 @@
width: 14px; width: 14px;
height: 14px; height: 14px;
} }
.maputnik-inline-error {
color: #a4a4a4;
padding: 0.4em 0.4em;
font-size: 0.9em;
border: solid 1px $color-red;
border-radius: 2px;
margin: $margin-2 0px;
}