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: [],
infos: [],
mapStyle: style.emptyStyle,
hopefulMapStyle: style.emptyStyle,
selectedLayerIndex: 0,
sources: {},
vectorLayers: {},
@ -279,7 +280,7 @@ export default class App extends React.Component {
}
updateFonts(urlTemplate) {
const metadata = this.state.mapStyle.metadata || {}
const metadata = this.state.hopefulMapStyle.metadata || {}
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
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) => {
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(newStyle.glyphs !== this.state.mapStyle.glyphs) {
if(newStyle.glyphs !== this.state.hopefulMapStyle.glyphs) {
this.updateFonts(newStyle.glyphs)
}
if(newStyle.sprite !== this.state.mapStyle.sprite) {
if(newStyle.sprite !== this.state.hopefulMapStyle.sprite) {
this.updateIcons(newStyle.sprite)
}
this.revisionStore.addRevision(newStyle)
if(save) this.saveStyle(newStyle)
this.setState({
hopefulMapStyle: newStyle,
mapStyle: newStyle,
errors: [],
})
} else {
this.setState({
errors: errors.map(err => err.message)
hopefulMapStyle: newStyle,
errors: mappedErrors,
})
}
@ -344,7 +371,7 @@ export default class App extends React.Component {
onUndo = () => {
const activeStyle = this.revisionStore.undo()
const messages = undoMessages(this.state.mapStyle, activeStyle)
const messages = undoMessages(this.state.hopefulMapStyle, activeStyle)
this.saveStyle(activeStyle)
this.setState({
mapStyle: activeStyle,
@ -354,7 +381,7 @@ export default class App extends React.Component {
onRedo = () => {
const activeStyle = this.revisionStore.redo()
const messages = redoMessages(this.state.mapStyle, activeStyle)
const messages = redoMessages(this.state.hopefulMapStyle, activeStyle)
this.saveStyle(activeStyle)
this.setState({
mapStyle: activeStyle,
@ -364,7 +391,7 @@ export default class App extends React.Component {
onMoveLayer = (move) => {
let { oldIndex, newIndex } = move;
let layers = this.state.mapStyle.layers;
let layers = this.state.hopefulMapStyle.layers;
oldIndex = clamp(oldIndex, 0, layers.length-1);
newIndex = clamp(newIndex, 0, layers.length-1);
if(oldIndex === newIndex) return;
@ -382,14 +409,14 @@ export default class App extends React.Component {
onLayersChange = (changedLayers) => {
const changedStyle = {
...this.state.mapStyle,
...this.state.hopefulMapStyle,
layers: changedLayers
}
this.onStyleChanged(changedStyle)
}
onLayerDestroy = (layerId) => {
let layers = this.state.mapStyle.layers;
let layers = this.state.hopefulMapStyle.layers;
const remainingLayers = layers.slice(0);
const idx = style.indexOfLayer(remainingLayers, layerId)
remainingLayers.splice(idx, 1);
@ -408,7 +435,7 @@ export default class App extends React.Component {
}
onLayerVisibilityToggle = (layerId) => {
let layers = this.state.mapStyle.layers;
let layers = this.state.hopefulMapStyle.layers;
const changedLayers = layers.slice(0)
const idx = style.indexOfLayer(changedLayers, layerId)
@ -423,7 +450,7 @@ export default class App extends React.Component {
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)
changedLayers[idx] = {
@ -435,7 +462,8 @@ export default class App extends React.Component {
}
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)
changedLayers[idx] = layer
@ -472,7 +500,7 @@ export default class App extends React.Component {
fetchSources() {
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)) {
continue;
}
@ -596,7 +624,7 @@ export default class App extends React.Component {
}
onLayerSelect = (layerId) => {
const idx = style.indexOfLayer(this.state.mapStyle.layers, layerId)
const idx = style.indexOfLayer(this.state.hopefulMapStyle.layers, layerId)
this.setState({ selectedLayerIndex: idx })
}
@ -636,14 +664,14 @@ export default class App extends React.Component {
}
render() {
const layers = this.state.mapStyle.layers || []
const layers = this.state.hopefulMapStyle.layers || []
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
renderer={this._getRenderer()}
mapState={this.state.mapState}
mapStyle={this.state.mapStyle}
mapStyle={this.state.hopefulMapStyle}
inspectModeEnabled={this.state.mapState === "inspect"}
sources={this.state.sources}
onStyleChanged={this.onStyleChanged}
@ -662,6 +690,7 @@ export default class App extends React.Component {
selectedLayerIndex={this.state.selectedLayerIndex}
layers={layers}
sources={this.state.sources}
errors={this.state.errors}
/>
const layerEditor = selectedLayer ? <LayerEditor
@ -669,7 +698,7 @@ export default class App extends React.Component {
layer={selectedLayer}
layerIndex={this.state.selectedLayerIndex}
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}
vectorLayers={this.state.vectorLayers}
spec={this.state.spec}
@ -679,9 +708,11 @@ export default class App extends React.Component {
onLayerCopy={this.onLayerCopy}
onLayerVisibilityToggle={this.onLayerVisibilityToggle}
onLayerIdChange={this.onLayerIdChange}
errors={this.state.errors}
/> : null
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
mapStyle={this.state.mapStyle}
errors={this.state.errors}
infos={this.state.infos}
/> : null
@ -704,7 +735,7 @@ export default class App extends React.Component {
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')}
/>
<SettingsModal
mapStyle={this.state.mapStyle}
mapStyle={this.state.hopefulMapStyle}
onStyleChanged={this.onStyleChanged}
onChangeMetadataProperty={this.onChangeMetadataProperty}
isOpen={this.state.isOpen.settings}
@ -712,7 +743,7 @@ export default class App extends React.Component {
openlayersDebugOptions={this.state.openlayersDebugOptions}
/>
<ExportModal
mapStyle={this.state.mapStyle}
mapStyle={this.state.hopefulMapStyle}
onStyleChanged={this.onStyleChanged}
isOpen={this.state.isOpen.export}
onOpenToggle={this.toggleModal.bind(this, 'export')}
@ -723,7 +754,7 @@ export default class App extends React.Component {
onOpenToggle={this.toggleModal.bind(this, 'open')}
/>
<SourcesModal
mapStyle={this.state.mapStyle}
mapStyle={this.state.hopefulMapStyle}
onStyleChanged={this.onStyleChanged}
isOpen={this.state.isOpen.sources}
onOpenToggle={this.toggleModal.bind(this, 'sources')}

View file

@ -8,8 +8,23 @@ class MessagePanel extends React.Component {
}
render() {
const errors = this.props.errors.map((m, i) => {
return <p key={"error-"+i} className="maputnik-message-panel-error">{m}</p>
const errors = this.props.errors.map((error, idx) => {
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) => {

View file

@ -4,6 +4,9 @@ import PropTypes from 'prop-types'
import SpecProperty from './_SpecProperty'
import DataProperty from './_DataProperty'
import ZoomProperty from './_ZoomProperty'
import ExpressionProperty from './_ExpressionProperty'
import {expression} from '@mapbox/mapbox-gl-style-spec'
function isZoomField(value) {
@ -14,6 +17,24 @@ function isDataField(value) {
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
*/
@ -108,6 +129,11 @@ export default class FunctionSpecProperty extends React.Component {
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 = () => {
const functionType = this.getFieldFunctionType(this.props.fieldSpec);
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"
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 = (
<ZoomProperty
error={this.props.error}
onChange={this.props.onChange.bind(this)}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
@ -141,6 +179,7 @@ export default class FunctionSpecProperty extends React.Component {
else if (isDataField(this.props.value)) {
specField = (
<DataProperty
error={this.props.error}
onChange={this.props.onChange.bind(this)}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
@ -153,12 +192,14 @@ export default class FunctionSpecProperty extends React.Component {
else {
specField = (
<SpecProperty
error={this.props.error}
onChange={this.props.onChange.bind(this)}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={this.props.value}
onZoomClick={this.makeZoomFunction}
onDataClick={this.makeDataFunction}
onExpressionClick={this.makeExpression}
/>
)
}

View file

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

View file

@ -200,6 +200,7 @@ export default class DataProperty extends React.Component {
return <div className="maputnik-data-spec-block">
<div className="maputnik-data-spec-property">
<InputBlock
error={this.props.error}
doc={this.props.fieldSpec.doc}
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'
/**
* 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 {
static propTypes = {
fieldSpec: PropTypes.object,
onZoomClick: PropTypes.func,
onDataClick: PropTypes.func,
onExpressionClick: PropTypes.func,
}
render() {
let makeZoomButton, makeDataButton
let makeZoomButton, makeDataButton, expressionButton;
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
className="maputnik-make-zoom-function"
onClick={this.props.onZoomClick}
@ -39,10 +70,14 @@ export default class FunctionButtons extends React.Component {
/>
</Button>
}
return <div>{makeDataButton}{makeZoomButton}</div>
return <div>
{expressionButton}
{makeDataButton}
{makeZoomButton}
</div>
}
else {
return null
return <div>{expressionButton}</div>
}
}
}

View file

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

View file

@ -67,15 +67,25 @@ export default class CombiningFilterEditor extends React.Component {
const filter = this.combiningFilter()
let combiningOp = filter[0]
let filters = filter.slice(1)
const {errors} = this.props;
const editorBlocks = filters.map((f, idx) => {
return <FilterEditorBlock key={idx} onDelete={this.deleteFilterItem.bind(this, idx)}>
<SingleFilterEditor
properties={this.props.properties}
filter={f}
onChange={this.onFilterPartChanged.bind(this, idx + 1)}
/>
</FilterEditorBlock>
const error = errors[`filter[${idx+1}]`];
return (
<>
<FilterEditorBlock key={idx} onDelete={this.deleteFilterItem.bind(this, idx)}>
<SingleFilterEditor
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

View file

@ -6,6 +6,7 @@ import FillIcon from './FillIcon.jsx'
import SymbolIcon from './SymbolIcon.jsx'
import BackgroundIcon from './BackgroundIcon.jsx'
import CircleIcon from './CircleIcon.jsx'
import MissingIcon from './MissingIcon.jsx'
class LayerIcon extends React.Component {
static propTypes = {
@ -25,6 +26,7 @@ class LayerIcon extends React.Component {
case 'line': return <LineIcon {...iconProps} />
case 'symbol': return <SymbolIcon {...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">
{this.props.children}
</div>
{this.props.error &&
<div className="maputnik-inline-error">
{this.props.error.message}
</div>
}
</div>
}
}

View file

@ -11,6 +11,7 @@ class StringInput extends React.Component {
onInput: PropTypes.func,
multi: PropTypes.bool,
required: PropTypes.bool,
disabled: PropTypes.bool,
}
static defaultProps = {
@ -51,9 +52,14 @@ class StringInput extends React.Component {
]
}
if(!!this.props.disabled) {
classes.push("maputnik-string--disabled");
}
return React.createElement(tag, {
"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(" "),
style: this.props.style,
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'
function getLayoutForType (type) {
return layout[type] ? layout[type] : layout.invalid;
}
function layoutGroups(layerType) {
const layerGroup = {
title: 'Layer',
@ -33,7 +37,9 @@ function layoutGroups(layerType) {
title: 'JSON Editor',
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. */
@ -79,7 +85,7 @@ export default class LayerEditor extends React.Component {
static getDerivedStateFromProps(props, state) {
const additionalGroups = { ...state.editorGroups }
layout[props.layer.type].groups.forEach(group => {
getLayoutForType(props.layer.type).groups.forEach(group => {
if(!(group.title in additionalGroups)) {
additionalGroups[group.title] = true
}
@ -118,6 +124,20 @@ export default class LayerEditor extends React.Component {
if(this.props.layer.metadata) {
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;
if(this.props.sources.hasOwnProperty(this.props.layer.source)) {
@ -129,13 +149,16 @@ export default class LayerEditor extends React.Component {
<LayerIdBlock
value={this.props.layer.id}
wdKey="layer-editor.layer-id"
error={errorData.id}
onChange={newId => this.props.onLayerIdChange(this.props.layer.id, newId)}
/>
<LayerTypeBlock
error={errorData.type}
value={this.props.layer.type}
onChange={newType => this.props.onLayerChanged(changeType(this.props.layer, newType))}
/>
{this.props.layer.type !== 'background' && <LayerSourceBlock
error={errorData.sources}
sourceIds={Object.keys(this.props.sources)}
value={this.props.layer.source}
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 &&
<LayerSourceLayerBlock
error={errorData['source-layer']}
sourceLayerIds={sourceLayerIds}
value={this.props.layer['source-layer']}
onChange={v => this.changeProperty(null, 'source-layer', v)}
/>
}
<MinZoomBlock
error={errorData.minzoom}
value={this.props.layer.minzoom}
onChange={v => this.changeProperty(null, 'minzoom', v)}
/>
<MaxZoomBlock
error={errorData.maxzoom}
value={this.props.layer.maxzoom}
onChange={v => this.changeProperty(null, 'maxzoom', v)}
/>
<CommentBlock
error={errorData.comment}
value={comment}
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>
<div className="maputnik-filter-editor-wrapper">
<FilterEditor
errors={errorData}
filter={this.props.layer.filter}
properties={this.props.vectorLayers[this.props.layer['source-layer']]}
onChange={f => this.changeProperty(null, 'filter', f)}
@ -171,6 +199,7 @@ export default class LayerEditor extends React.Component {
</div>
</div>
case 'properties': return <PropertyGroup
errors={errorData}
layer={this.props.layer}
groupFields={fields}
spec={this.props.spec}

View file

@ -181,10 +181,19 @@ class LayerListContainer extends React.Component {
layers.forEach((layer, idxInGroup) => {
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
className={classnames({
'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}
key={layer.id}

View file

@ -4,6 +4,7 @@ import PropTypes from 'prop-types'
import {latest} from '@mapbox/mapbox-gl-style-spec'
import InputBlock from '../inputs/InputBlock'
import SelectInput from '../inputs/SelectInput'
import StringInput from '../inputs/StringInput'
class LayerTypeBlock extends React.Component {
static propTypes = {
@ -15,21 +16,11 @@ class LayerTypeBlock extends React.Component {
render() {
return <InputBlock label={"Type"} doc={latest.layer.type.doc}
data-wd-key={this.props.wdKey}
error={this.props.error}
>
<SelectInput
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}
<StringInput
value={this.props.value}
disabled={true}
/>
</InputBlock>
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -34,3 +34,12 @@
width: 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;
}