From 725b752e353a56d696bb85a3517aea4ebed87efa Mon Sep 17 00:00:00 2001 From: orangemug Date: Wed, 29 Jan 2020 08:22:03 +0000 Subject: [PATCH 01/39] Added initial expression work and UI errors. --- src/components/App.jsx | 73 +++++++++++----- src/components/MessagePanel.jsx | 19 +++- src/components/fields/FunctionSpecField.jsx | 43 ++++++++- src/components/fields/PropertyGroup.jsx | 4 + src/components/fields/_DataProperty.jsx | 1 + src/components/fields/_ExpressionProperty.jsx | 87 +++++++++++++++++++ src/components/fields/_FunctionButtons.jsx | 41 ++++++++- src/components/fields/_SpecProperty.jsx | 3 + src/components/fields/_ZoomProperty.jsx | 1 + src/components/filter/FilterEditor.jsx | 24 +++-- src/components/icons/LayerIcon.jsx | 2 + src/components/icons/MissingIcon.jsx | 11 +++ src/components/inputs/InputBlock.jsx | 5 ++ src/components/inputs/StringInput.jsx | 8 +- src/components/layers/LayerEditor.jsx | 33 ++++++- src/components/layers/LayerList.jsx | 11 ++- src/components/layers/LayerTypeBlock.jsx | 17 +--- src/components/layers/MaxZoomBlock.jsx | 1 + src/components/layers/MinZoomBlock.jsx | 1 + src/config/layout.json | 3 + src/styles/_components.scss | 4 +- src/styles/_input.scss | 5 ++ src/styles/_layer.scss | 5 ++ src/styles/_zoomproperty.scss | 2 + src/styles/index.scss | 9 ++ 25 files changed, 360 insertions(+), 53 deletions(-) create mode 100644 src/components/fields/_ExpressionProperty.jsx create mode 100644 src/components/icons/MissingIcon.jsx diff --git a/src/components/App.jsx b/src/components/App.jsx index 47f57b16..d8d6d16c 100644 --- a/src/components/App.jsx +++ b/src/components/App.jsx @@ -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 = const layerEditor = selectedLayer ? : null const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? : null @@ -704,7 +735,7 @@ export default class App extends React.Component { onOpenToggle={this.toggleModal.bind(this, 'shortcuts')} /> { - return

{m}

+ 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 '{mapStyle.layers[parsed.data.index].id}': {parsed.data.message} + + ); + } + else { + content = error.message; + } + return

+ {content} +

}) const infos = this.props.infos.map((m, i) => { diff --git a/src/components/fields/FunctionSpecField.jsx b/src/components/fields/FunctionSpecField.jsx index e2542c5a..a6920442 100644 --- a/src/components/fields/FunctionSpecField.jsx +++ b/src/components/fields/FunctionSpecField.jsx @@ -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 = ( + + ); + } + else if (isZoomField(this.props.value)) { specField = ( ) } diff --git a/src/components/fields/PropertyGroup.jsx b/src/components/fields/PropertyGroup.jsx index 35577244..30515c3e 100644 --- a/src/components/fields/PropertyGroup.jsx +++ b/src/components/fields/PropertyGroup.jsx @@ -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
diff --git a/src/components/fields/_ExpressionProperty.jsx b/src/components/fields/_ExpressionProperty.jsx new file mode 100644 index 00000000..037a040d --- /dev/null +++ b/src/components/fields/_ExpressionProperty.jsx @@ -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 = ( + + ); + + return + + + } +} diff --git a/src/components/fields/_FunctionButtons.jsx b/src/components/fields/_FunctionButtons.jsx index 9048a608..0645e62c 100644 --- a/src/components/fields/_FunctionButtons.jsx +++ b/src/components/fields/_FunctionButtons.jsx @@ -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 = ( + + ); + makeZoomButton = } - return
{makeDataButton}{makeZoomButton}
+ return
+ {expressionButton} + {makeDataButton} + {makeZoomButton} +
} else { - return null + return
{expressionButton}
} } } diff --git a/src/components/fields/_SpecProperty.jsx b/src/components/fields/_SpecProperty.jsx index 0e0fd9f7..16a156ed 100644 --- a/src/components/fields/_SpecProperty.jsx +++ b/src/components/fields/_SpecProperty.jsx @@ -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 return { - return - - + const error = errors[`filter[${idx+1}]`]; + + return ( + <> + + + + {error && +
{error.message}
+ } + + ); }) //TODO: Implement support for nested filter diff --git a/src/components/icons/LayerIcon.jsx b/src/components/icons/LayerIcon.jsx index 7cba8cef..fb5d3277 100644 --- a/src/components/icons/LayerIcon.jsx +++ b/src/components/icons/LayerIcon.jsx @@ -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 case 'symbol': return case 'circle': return + default: return } } } diff --git a/src/components/icons/MissingIcon.jsx b/src/components/icons/MissingIcon.jsx new file mode 100644 index 00000000..71f76ddd --- /dev/null +++ b/src/components/icons/MissingIcon.jsx @@ -0,0 +1,11 @@ +import React from 'react' +import {MdPriorityHigh} from 'react-icons/md' + + +export default class MissingIcon extends React.Component { + render() { + return ( + + ) + } +} diff --git a/src/components/inputs/InputBlock.jsx b/src/components/inputs/InputBlock.jsx index 117ba3ee..9cfaacff 100644 --- a/src/components/inputs/InputBlock.jsx +++ b/src/components/inputs/InputBlock.jsx @@ -52,6 +52,11 @@ class InputBlock extends React.Component {
{this.props.children}
+ {this.props.error && +
+ {this.props.error.message} +
+ }
} } diff --git a/src/components/inputs/StringInput.jsx b/src/components/inputs/StringInput.jsx index 87bdd31c..bbb7b227 100644 --- a/src/components/inputs/StringInput.jsx +++ b/src/components/inputs/StringInput.jsx @@ -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, diff --git a/src/components/layers/LayerEditor.jsx b/src/components/layers/LayerEditor.jsx index ed161163..538b11a5 100644 --- a/src/components/layers/LayerEditor.jsx +++ b/src/components/layers/LayerEditor.jsx @@ -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 { this.props.onLayerIdChange(this.props.layer.id, newId)} /> this.props.onLayerChanged(changeType(this.props.layer, newType))} /> {this.props.layer.type !== 'background' && 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 && this.changeProperty(null, 'source-layer', v)} /> } this.changeProperty(null, 'minzoom', v)} /> this.changeProperty(null, 'maxzoom', v)} /> this.changeProperty('metadata', 'maputnik:comment', v == "" ? undefined : v)} /> @@ -164,6 +191,7 @@ export default class LayerEditor extends React.Component { case 'filter': return
this.changeProperty(null, 'filter', f)} @@ -171,6 +199,7 @@ export default class LayerEditor extends React.Component {
case 'properties': return { 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 = 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} diff --git a/src/components/layers/LayerTypeBlock.jsx b/src/components/layers/LayerTypeBlock.jsx index 90884646..9612ecb5 100644 --- a/src/components/layers/LayerTypeBlock.jsx +++ b/src/components/layers/LayerTypeBlock.jsx @@ -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 - } diff --git a/src/components/layers/MaxZoomBlock.jsx b/src/components/layers/MaxZoomBlock.jsx index 74475935..6e86916c 100644 --- a/src/components/layers/MaxZoomBlock.jsx +++ b/src/components/layers/MaxZoomBlock.jsx @@ -13,6 +13,7 @@ class MaxZoomBlock extends React.Component { render() { return div { diff --git a/src/styles/_input.scss b/src/styles/_input.scss index 6c196a2b..39219e7e 100644 --- a/src/styles/_input.scss +++ b/src/styles/_input.scss @@ -25,6 +25,11 @@ resize: vertical; height: 78px; } + + &--disabled { + background: transparent; + border: none; + } } .maputnik-number-container { diff --git a/src/styles/_layer.scss b/src/styles/_layer.scss index e933da4f..1648bd44 100644 --- a/src/styles/_layer.scss +++ b/src/styles/_layer.scss @@ -99,6 +99,11 @@ } } + + .maputnik-layer-list-item--error { + color: $color-red; + } + &-item-selected { color: $color-white; } diff --git a/src/styles/_zoomproperty.scss b/src/styles/_zoomproperty.scss index 2b9ce14b..7f45d985 100644 --- a/src/styles/_zoomproperty.scss +++ b/src/styles/_zoomproperty.scss @@ -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; } diff --git a/src/styles/index.scss b/src/styles/index.scss index 759b73cf..019857a9 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -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; +} From c5c3e93aff9b06e0c74cd6b1607ef7fe28c168bd Mon Sep 17 00:00:00 2001 From: orangemug Date: Sat, 1 Feb 2020 17:07:52 +0000 Subject: [PATCH 02/39] Better support for expressions - Expression editing state - CodeMirror JSON editor - Improved styling --- src/components/fields/FunctionSpecField.jsx | 23 +++++++++- src/components/fields/_ExpressionProperty.jsx | 44 +++++++++---------- src/components/inputs/InputBlock.jsx | 1 + src/components/layers/JSONEditor.jsx | 19 ++++++-- src/styles/index.scss | 20 +++++++++ 5 files changed, 78 insertions(+), 29 deletions(-) diff --git a/src/components/fields/FunctionSpecField.jsx b/src/components/fields/FunctionSpecField.jsx index a6920442..b0c0afc6 100644 --- a/src/components/fields/FunctionSpecField.jsx +++ b/src/components/fields/FunctionSpecField.jsx @@ -22,7 +22,7 @@ function isDataField(value) { * properties accept arrays as values, for example `text-font`. So we must try * and create an expression. */ -function isExpression(value, fieldSpec={}) { +function checkIsExpression (value, fieldSpec={}) { if (!Array.isArray(value)) { return false; } @@ -72,6 +72,13 @@ export default class FunctionSpecProperty extends React.Component { ]), } + constructor (props) { + super(); + this.state = { + isExpression: checkIsExpression(props.value, props.fieldSpec), + } + } + getFieldFunctionType(fieldSpec) { if (fieldSpec.expression.interpolated) { return "exponential" @@ -103,6 +110,13 @@ export default class FunctionSpecProperty extends React.Component { this.props.onChange(this.props.fieldName, changedValue) } + deleteExpression = (newValue) => { + this.props.onChange(this.props.fieldName, newValue); + this.setState({ + isExpression: false, + }); + } + deleteStop = (stopIdx) => { const stops = this.props.value.stops.slice(0) stops.splice(stopIdx, 1) @@ -132,6 +146,10 @@ export default class FunctionSpecProperty extends React.Component { makeExpression = () => { const expression = ["literal", this.props.value || this.props.fieldSpec.default]; this.props.onChange(this.props.fieldName, expression); + + this.setState({ + isExpression: true, + }); } makeDataFunction = () => { @@ -152,11 +170,12 @@ 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 (isExpression(this.props.value, this.props.fieldSpec)) { + if (this.state.isExpression) { specField = ( { - try { - const jsonVal = JSON.parse(value); - - if (isLiteralExpression(jsonVal)) { - this.setState({ - lastValue: jsonVal - }); - } - - this.props.onChange(jsonVal); - } - catch (err) { - // TODO: Handle JSON parse error + onChange = (jsonVal) => { + if (isLiteralExpression(jsonVal)) { + this.setState({ + lastValue: jsonVal + }); } + + this.props.onChange(jsonVal); } onDelete = () => { @@ -50,13 +44,13 @@ export default class ExpressionProperty extends React.Component { const {value, fieldName, fieldSpec} = this.props; if (isLiteralExpression(value)) { - this.props.onChange(value[1]); + this.props.onDelete(value[1]); } else if (isLiteralExpression(lastValue)) { - this.props.onChange(lastValue[1]); + this.props.onDelete(lastValue[1]); } else { - this.props.onChange(fieldSpec.default); + this.props.onDelete(fieldSpec.default); } } @@ -75,12 +69,16 @@ export default class ExpressionProperty extends React.Component { doc={this.props.fieldSpec.doc} label={labelFromFieldName(this.props.fieldName)} action={deleteStopBtn} + wideMode={true} > - stringifyPretty(data, {indent: 2, maxLength: 50})} + onChange={this.onChange} /> } diff --git a/src/components/inputs/InputBlock.jsx b/src/components/inputs/InputBlock.jsx index 9cfaacff..f700e463 100644 --- a/src/components/inputs/InputBlock.jsx +++ b/src/components/inputs/InputBlock.jsx @@ -28,6 +28,7 @@ class InputBlock extends React.Component { data-wd-key={this.props["data-wd-key"]} className={classnames({ "maputnik-input-block": true, + "maputnik-input-block--wide": this.props.wideMode, "maputnik-action-block": this.props.action })} > diff --git a/src/components/layers/JSONEditor.jsx b/src/components/layers/JSONEditor.jsx index e0a99ec9..eec03431 100644 --- a/src/components/layers/JSONEditor.jsx +++ b/src/components/layers/JSONEditor.jsx @@ -1,5 +1,6 @@ import React from 'react' import PropTypes from 'prop-types' +import classnames from 'classnames'; import InputBlock from '../inputs/InputBlock' import StringInput from '../inputs/StringInput' @@ -22,6 +23,15 @@ class JSONEditor extends React.Component { layer: PropTypes.object.isRequired, maxHeight: PropTypes.number, onChange: PropTypes.func, + lineNumbers: PropTypes.bool, + } + + static defaultProps = { + lineNumbers: true, + gutters: ["CodeMirror-lint-markers"], + getValue: (data) => { + return JSON.stringify(data, null, 2) + } } constructor(props) { @@ -33,7 +43,7 @@ class JSONEditor extends React.Component { } getValue () { - return JSON.stringify(this.props.layer, null, 2); + return this.props.getValue(this.props.layer); } componentDidMount () { @@ -43,13 +53,14 @@ class JSONEditor extends React.Component { name: "javascript", json: true }, + lineWrapping: this.props.lineWrapping, tabSize: 2, theme: 'maputnik', viewportMargin: Infinity, - lineNumbers: true, + lineNumbers: this.props.lineNumbers, lint: true, matchBrackets: true, - gutters: ["CodeMirror-lint-markers"], + gutters: this.props.gutters, scrollbarStyle: "null", }); @@ -113,7 +124,7 @@ class JSONEditor extends React.Component { } return
this._el = el} style={style} /> diff --git a/src/styles/index.scss b/src/styles/index.scss index 019857a9..54c11c4b 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -35,6 +35,7 @@ height: 14px; } +// TODO: Move these into correct *.scss files .maputnik-inline-error { color: #a4a4a4; padding: 0.4em 0.4em; @@ -43,3 +44,22 @@ border-radius: 2px; margin: $margin-2 0px; } + +.maputnik-expression-editor { + border: solid 1px $color-gray; +} + +.maputnik-input-block--wide { + .maputnik-input-block-content { + display: block; + width: auto; + } + + .maputnik-input-block-label { + width: 82%; + } + + .maputnik-input-block-action { + text-align: right; + } +} From 3e2927e6a481f4c29453c98be85d11e85f8edc53 Mon Sep 17 00:00:00 2001 From: orangemug Date: Sat, 1 Feb 2020 17:53:01 +0000 Subject: [PATCH 03/39] Fixed undo/redo --- src/components/App.jsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/components/App.jsx b/src/components/App.jsx index d8d6d16c..79424e72 100644 --- a/src/components/App.jsx +++ b/src/components/App.jsx @@ -370,12 +370,23 @@ export default class App extends React.Component { } onUndo = () => { - const activeStyle = this.revisionStore.undo() + let activeStyle; + + // Check our dirty style state first, otherwise just undo to that state. + if (isEqual(this.state.mapStyle, this.state.hopefulMapStyle)) { + activeStyle = this.revisionStore.undo() + } + else { + activeStyle = this.state.mapStyle; + } + const messages = undoMessages(this.state.hopefulMapStyle, activeStyle) this.saveStyle(activeStyle) this.setState({ - mapStyle: activeStyle, infos: messages, + mapStyle: activeStyle, + hopefulMapStyle: activeStyle, + errors: [], }) } @@ -384,8 +395,10 @@ export default class App extends React.Component { const messages = redoMessages(this.state.hopefulMapStyle, activeStyle) this.saveStyle(activeStyle) this.setState({ - mapStyle: activeStyle, infos: messages, + mapStyle: activeStyle, + hopefulMapStyle: activeStyle, + errors: [], }) } From 5792c632f91777f30137b25320b58107bc1b2c9f Mon Sep 17 00:00:00 2001 From: orangemug Date: Sat, 1 Feb 2020 17:55:26 +0000 Subject: [PATCH 04/39] hopefulMapStyle -> dirtyMapStyle --- src/components/App.jsx | 52 +++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/components/App.jsx b/src/components/App.jsx index 79424e72..32e7624c 100644 --- a/src/components/App.jsx +++ b/src/components/App.jsx @@ -207,7 +207,7 @@ export default class App extends React.Component { errors: [], infos: [], mapStyle: style.emptyStyle, - hopefulMapStyle: style.emptyStyle, + dirtyMapStyle: style.emptyStyle, selectedLayerIndex: 0, sources: {}, vectorLayers: {}, @@ -280,7 +280,7 @@ export default class App extends React.Component { } updateFonts(urlTemplate) { - const metadata = this.state.hopefulMapStyle.metadata || {} + const metadata = this.state.dirtyMapStyle.metadata || {} const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles let glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate; @@ -345,23 +345,23 @@ export default class App extends React.Component { if(errors.length === 0) { - if(newStyle.glyphs !== this.state.hopefulMapStyle.glyphs) { + if(newStyle.glyphs !== this.state.dirtyMapStyle.glyphs) { this.updateFonts(newStyle.glyphs) } - if(newStyle.sprite !== this.state.hopefulMapStyle.sprite) { + if(newStyle.sprite !== this.state.dirtyMapStyle.sprite) { this.updateIcons(newStyle.sprite) } this.revisionStore.addRevision(newStyle) if(save) this.saveStyle(newStyle) this.setState({ - hopefulMapStyle: newStyle, + dirtyMapStyle: newStyle, mapStyle: newStyle, errors: [], }) } else { this.setState({ - hopefulMapStyle: newStyle, + dirtyMapStyle: newStyle, errors: mappedErrors, }) } @@ -373,38 +373,38 @@ export default class App extends React.Component { let activeStyle; // Check our dirty style state first, otherwise just undo to that state. - if (isEqual(this.state.mapStyle, this.state.hopefulMapStyle)) { + if (isEqual(this.state.mapStyle, this.state.dirtyMapStyle)) { activeStyle = this.revisionStore.undo() } else { activeStyle = this.state.mapStyle; } - const messages = undoMessages(this.state.hopefulMapStyle, activeStyle) + const messages = undoMessages(this.state.dirtyMapStyle, activeStyle) this.saveStyle(activeStyle) this.setState({ infos: messages, mapStyle: activeStyle, - hopefulMapStyle: activeStyle, + dirtyMapStyle: activeStyle, errors: [], }) } onRedo = () => { const activeStyle = this.revisionStore.redo() - const messages = redoMessages(this.state.hopefulMapStyle, activeStyle) + const messages = redoMessages(this.state.dirtyMapStyle, activeStyle) this.saveStyle(activeStyle) this.setState({ infos: messages, mapStyle: activeStyle, - hopefulMapStyle: activeStyle, + dirtyMapStyle: activeStyle, errors: [], }) } onMoveLayer = (move) => { let { oldIndex, newIndex } = move; - let layers = this.state.hopefulMapStyle.layers; + let layers = this.state.dirtyMapStyle.layers; oldIndex = clamp(oldIndex, 0, layers.length-1); newIndex = clamp(newIndex, 0, layers.length-1); if(oldIndex === newIndex) return; @@ -422,14 +422,14 @@ export default class App extends React.Component { onLayersChange = (changedLayers) => { const changedStyle = { - ...this.state.hopefulMapStyle, + ...this.state.dirtyMapStyle, layers: changedLayers } this.onStyleChanged(changedStyle) } onLayerDestroy = (layerId) => { - let layers = this.state.hopefulMapStyle.layers; + let layers = this.state.dirtyMapStyle.layers; const remainingLayers = layers.slice(0); const idx = style.indexOfLayer(remainingLayers, layerId) remainingLayers.splice(idx, 1); @@ -448,7 +448,7 @@ export default class App extends React.Component { } onLayerVisibilityToggle = (layerId) => { - let layers = this.state.hopefulMapStyle.layers; + let layers = this.state.dirtyMapStyle.layers; const changedLayers = layers.slice(0) const idx = style.indexOfLayer(changedLayers, layerId) @@ -463,7 +463,7 @@ export default class App extends React.Component { onLayerIdChange = (oldId, newId) => { - const changedLayers = this.state.hopefulMapStyle.layers.slice(0) + const changedLayers = this.state.dirtyMapStyle.layers.slice(0) const idx = style.indexOfLayer(changedLayers, oldId) changedLayers[idx] = { @@ -476,7 +476,7 @@ export default class App extends React.Component { onLayerChanged = (layer) => { console.log("test: onLayerChanged", layer); - const changedLayers = this.state.hopefulMapStyle.layers.slice(0) + const changedLayers = this.state.dirtyMapStyle.layers.slice(0) const idx = style.indexOfLayer(changedLayers, layer.id) changedLayers[idx] = layer @@ -513,7 +513,7 @@ export default class App extends React.Component { fetchSources() { const sourceList = {...this.state.sources}; - for(let [key, val] of Object.entries(this.state.hopefulMapStyle.sources)) { + for(let [key, val] of Object.entries(this.state.dirtyMapStyle.sources)) { if(sourceList.hasOwnProperty(key)) { continue; } @@ -637,7 +637,7 @@ export default class App extends React.Component { } onLayerSelect = (layerId) => { - const idx = style.indexOfLayer(this.state.hopefulMapStyle.layers, layerId) + const idx = style.indexOfLayer(this.state.dirtyMapStyle.layers, layerId) this.setState({ selectedLayerIndex: idx }) } @@ -677,14 +677,14 @@ export default class App extends React.Component { } render() { - const layers = this.state.hopefulMapStyle.layers || [] + const layers = this.state.dirtyMapStyle.layers || [] const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : null - const metadata = this.state.hopefulMapStyle.metadata || {} + const metadata = this.state.dirtyMapStyle.metadata || {} const toolbar = Date: Sun, 2 Feb 2020 07:41:31 +0000 Subject: [PATCH 05/39] Fixed lint errors --- src/components/MessagePanel.jsx | 3 ++- src/components/fields/FunctionSpecField.jsx | 1 + src/components/fields/PropertyGroup.jsx | 1 + src/components/fields/_DataProperty.jsx | 2 +- src/components/fields/_ExpressionProperty.jsx | 5 ++++- src/components/fields/_SpecProperty.jsx | 6 ++++-- src/components/fields/_ZoomProperty.jsx | 1 + src/components/filter/FilterEditor.jsx | 1 + src/components/inputs/InputBlock.jsx | 2 ++ src/components/inputs/StringInput.jsx | 1 + src/components/layers/JSONEditor.jsx | 5 +++++ src/components/layers/LayerEditor.jsx | 1 + src/components/layers/LayerTypeBlock.jsx | 1 + src/components/layers/MaxZoomBlock.jsx | 1 + src/components/layers/MinZoomBlock.jsx | 1 + src/styles/_zoomproperty.scss | 4 ---- 16 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/components/MessagePanel.jsx b/src/components/MessagePanel.jsx index c7ade55e..1433b57b 100644 --- a/src/components/MessagePanel.jsx +++ b/src/components/MessagePanel.jsx @@ -5,6 +5,7 @@ class MessagePanel extends React.Component { static propTypes = { errors: PropTypes.array, infos: PropTypes.array, + mapStyle: PropTypes.object, } render() { @@ -15,7 +16,7 @@ class MessagePanel extends React.Component { const {mapStyle} = this.props; content = ( <> - Layer '{mapStyle.layers[parsed.data.index].id}': {parsed.data.message} + Layer '{mapStyle.layers[parsed.data.index].id}': {parsed.data.message} ); } diff --git a/src/components/fields/FunctionSpecField.jsx b/src/components/fields/FunctionSpecField.jsx index b0c0afc6..eb8c1d22 100644 --- a/src/components/fields/FunctionSpecField.jsx +++ b/src/components/fields/FunctionSpecField.jsx @@ -62,6 +62,7 @@ export default class FunctionSpecProperty extends React.Component { onChange: PropTypes.func.isRequired, fieldName: PropTypes.string.isRequired, fieldSpec: PropTypes.object.isRequired, + error: PropTypes.object, value: PropTypes.oneOfType([ PropTypes.object, diff --git a/src/components/fields/PropertyGroup.jsx b/src/components/fields/PropertyGroup.jsx index 30515c3e..e07c15f2 100644 --- a/src/components/fields/PropertyGroup.jsx +++ b/src/components/fields/PropertyGroup.jsx @@ -40,6 +40,7 @@ export default class PropertyGroup extends React.Component { groupFields: PropTypes.array.isRequired, onChange: PropTypes.func.isRequired, spec: PropTypes.object.isRequired, + errors: PropTypes.array, } onPropertyChange = (property, newValue) => { diff --git a/src/components/fields/_DataProperty.jsx b/src/components/fields/_DataProperty.jsx index 4eaf37ed..be48ddce 100644 --- a/src/components/fields/_DataProperty.jsx +++ b/src/components/fields/_DataProperty.jsx @@ -48,6 +48,7 @@ export default class DataProperty extends React.Component { PropTypes.bool, PropTypes.array ]), + error: PropTypes.object, } state = { @@ -202,7 +203,6 @@ export default class DataProperty extends React.Component {
diff --git a/src/components/fields/_ExpressionProperty.jsx b/src/components/fields/_ExpressionProperty.jsx index 097457fa..03fb2015 100644 --- a/src/components/fields/_ExpressionProperty.jsx +++ b/src/components/fields/_ExpressionProperty.jsx @@ -19,7 +19,10 @@ export default class ExpressionProperty extends React.Component { static propTypes = { onDelete: PropTypes.func, fieldName: PropTypes.string, - fieldSpec: PropTypes.object + fieldSpec: PropTypes.object, + value: PropTypes.object, + error: PropTypes.object, + onChange: PropTypes.func, } constructor (props) { diff --git a/src/components/fields/_SpecProperty.jsx b/src/components/fields/_SpecProperty.jsx index 50215efc..e944cd91 100644 --- a/src/components/fields/_SpecProperty.jsx +++ b/src/components/fields/_SpecProperty.jsx @@ -13,7 +13,10 @@ export default class SpecProperty extends React.Component { onZoomClick: PropTypes.func.isRequired, onDataClick: PropTypes.func.isRequired, fieldName: PropTypes.string, - fieldSpec: PropTypes.object + fieldSpec: PropTypes.object, + value: PropTypes.any, + error: PropTypes.object, + onExpressionClick: PropTypes.func, } render() { @@ -28,7 +31,6 @@ export default class SpecProperty extends React.Component { return diff --git a/src/components/fields/_ZoomProperty.jsx b/src/components/fields/_ZoomProperty.jsx index f40b97a4..1a1c494f 100644 --- a/src/components/fields/_ZoomProperty.jsx +++ b/src/components/fields/_ZoomProperty.jsx @@ -44,6 +44,7 @@ export default class ZoomProperty extends React.Component { onAddStop: PropTypes.func, fieldName: PropTypes.string, fieldSpec: PropTypes.object, + error: PropTypes.object, value: PropTypes.oneOfType([ PropTypes.object, PropTypes.string, diff --git a/src/components/filter/FilterEditor.jsx b/src/components/filter/FilterEditor.jsx index 5e41aea4..1f7a1c5b 100644 --- a/src/components/filter/FilterEditor.jsx +++ b/src/components/filter/FilterEditor.jsx @@ -27,6 +27,7 @@ export default class CombiningFilterEditor extends React.Component { /** Properties of the vector layer and the available fields */ properties: PropTypes.object, filter: PropTypes.array, + errors: PropTypes.array, onChange: PropTypes.func.isRequired, } diff --git a/src/components/inputs/InputBlock.jsx b/src/components/inputs/InputBlock.jsx index f480b2d4..378d3450 100644 --- a/src/components/inputs/InputBlock.jsx +++ b/src/components/inputs/InputBlock.jsx @@ -18,6 +18,8 @@ class InputBlock extends React.Component { style: PropTypes.object, onChange: PropTypes.func, fieldSpec: PropTypes.object, + wideMode: PropTypes.bool, + error: PropTypes.object, } constructor (props) { diff --git a/src/components/inputs/StringInput.jsx b/src/components/inputs/StringInput.jsx index bbb7b227..c45e02cf 100644 --- a/src/components/inputs/StringInput.jsx +++ b/src/components/inputs/StringInput.jsx @@ -12,6 +12,7 @@ class StringInput extends React.Component { multi: PropTypes.bool, required: PropTypes.bool, disabled: PropTypes.bool, + spellCheck: PropTypes.bool, } static defaultProps = { diff --git a/src/components/layers/JSONEditor.jsx b/src/components/layers/JSONEditor.jsx index eec03431..fe212c23 100644 --- a/src/components/layers/JSONEditor.jsx +++ b/src/components/layers/JSONEditor.jsx @@ -24,10 +24,15 @@ class JSONEditor extends React.Component { maxHeight: PropTypes.number, onChange: PropTypes.func, lineNumbers: PropTypes.bool, + lineWrapping: PropTypes.bool, + getValue: PropTypes.func, + gutters: PropTypes.array, + className: PropTypes.string, } static defaultProps = { lineNumbers: true, + lineWrapping: false, gutters: ["CodeMirror-lint-markers"], getValue: (data) => { return JSON.stringify(data, null, 2) diff --git a/src/components/layers/LayerEditor.jsx b/src/components/layers/LayerEditor.jsx index 538b11a5..4c82a8f1 100644 --- a/src/components/layers/LayerEditor.jsx +++ b/src/components/layers/LayerEditor.jsx @@ -58,6 +58,7 @@ export default class LayerEditor extends React.Component { isFirstLayer: PropTypes.bool, isLastLayer: PropTypes.bool, layerIndex: PropTypes.number, + errors: PropTypes.array, } static defaultProps = { diff --git a/src/components/layers/LayerTypeBlock.jsx b/src/components/layers/LayerTypeBlock.jsx index ea31bd1b..4a4b2b36 100644 --- a/src/components/layers/LayerTypeBlock.jsx +++ b/src/components/layers/LayerTypeBlock.jsx @@ -11,6 +11,7 @@ class LayerTypeBlock extends React.Component { value: PropTypes.string.isRequired, wdKey: PropTypes.string, onChange: PropTypes.func.isRequired, + error: PropTypes.object, } render() { diff --git a/src/components/layers/MaxZoomBlock.jsx b/src/components/layers/MaxZoomBlock.jsx index 32ce51ae..ee75f42f 100644 --- a/src/components/layers/MaxZoomBlock.jsx +++ b/src/components/layers/MaxZoomBlock.jsx @@ -9,6 +9,7 @@ class MaxZoomBlock extends React.Component { static propTypes = { value: PropTypes.number, onChange: PropTypes.func.isRequired, + error: PropTypes.object, } render() { diff --git a/src/components/layers/MinZoomBlock.jsx b/src/components/layers/MinZoomBlock.jsx index b554bae3..f3f9f1e6 100644 --- a/src/components/layers/MinZoomBlock.jsx +++ b/src/components/layers/MinZoomBlock.jsx @@ -9,6 +9,7 @@ class MinZoomBlock extends React.Component { static propTypes = { value: PropTypes.number, onChange: PropTypes.func.isRequired, + error: PropTypes.object, } render() { diff --git a/src/styles/_zoomproperty.scss b/src/styles/_zoomproperty.scss index 7f45d985..1d59c9f1 100644 --- a/src/styles/_zoomproperty.scss +++ b/src/styles/_zoomproperty.scss @@ -2,8 +2,6 @@ .maputnik-make-zoom-function { background-color: transparent; display: inline-block; - padding-bottom: 0; - padding-top: 0; vertical-align: middle; padding: 0 $margin-2 0 0; @@ -64,8 +62,6 @@ .maputnik-make-data-function { background-color: transparent; display: inline-block; - padding-bottom: 0; - padding-top: 0; vertical-align: middle; padding: 0 $margin-2 0 0; From 532bbecb47d9f8e644666a7771e18193da655b2b Mon Sep 17 00:00:00 2001 From: orangemug Date: Sun, 2 Feb 2020 08:05:01 +0000 Subject: [PATCH 06/39] Only disable in --- src/components/layers/LayerEditor.jsx | 1 + src/components/layers/LayerTypeBlock.jsx | 32 +++++++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/components/layers/LayerEditor.jsx b/src/components/layers/LayerEditor.jsx index 4c82a8f1..79b4c161 100644 --- a/src/components/layers/LayerEditor.jsx +++ b/src/components/layers/LayerEditor.jsx @@ -154,6 +154,7 @@ export default class LayerEditor extends React.Component { onChange={newId => this.props.onLayerIdChange(this.props.layer.id, newId)} /> this.props.onLayerChanged(changeType(this.props.layer, newType))} diff --git a/src/components/layers/LayerTypeBlock.jsx b/src/components/layers/LayerTypeBlock.jsx index 4a4b2b36..1c685986 100644 --- a/src/components/layers/LayerTypeBlock.jsx +++ b/src/components/layers/LayerTypeBlock.jsx @@ -12,6 +12,11 @@ class LayerTypeBlock extends React.Component { wdKey: PropTypes.string, onChange: PropTypes.func.isRequired, error: PropTypes.object, + disabled: PropTypes.bool, + } + + static defaultProps = { + disabled: false, } render() { @@ -19,10 +24,29 @@ class LayerTypeBlock extends React.Component { data-wd-key={this.props.wdKey} error={this.props.error} > - + {this.props.disabled && + + } + {!this.props.disabled && + + } } } From be36eec93d16e7cb90207887f482bae1b902b32e Mon Sep 17 00:00:00 2001 From: orangemug Date: Sat, 8 Feb 2020 10:38:31 +0000 Subject: [PATCH 07/39] Added 'undo' alongside 'trash' button --- src/components/fields/_ExpressionProperty.jsx | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/src/components/fields/_ExpressionProperty.jsx b/src/components/fields/_ExpressionProperty.jsx index 03fb2015..8f489b12 100644 --- a/src/components/fields/_ExpressionProperty.jsx +++ b/src/components/fields/_ExpressionProperty.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types' import InputBlock from '../inputs/InputBlock' import Button from '../Button' -import {MdDelete} from 'react-icons/md' +import {MdDelete, MdUndo} from 'react-icons/md' import StringInput from '../inputs/StringInput' import labelFromFieldName from './_labelFromFieldName' @@ -42,9 +42,9 @@ export default class ExpressionProperty extends React.Component { this.props.onChange(jsonVal); } - onDelete = () => { + onReset = () => { const {lastValue} = this.state; - const {value, fieldName, fieldSpec} = this.props; + const {value, fieldSpec} = this.props; if (isLiteralExpression(value)) { this.props.onDelete(value[1]); @@ -52,19 +52,34 @@ export default class ExpressionProperty extends React.Component { else if (isLiteralExpression(lastValue)) { this.props.onDelete(lastValue[1]); } - else { - this.props.onDelete(fieldSpec.default); - } + } + + onDelete = () => { + const {fieldSpec} = this.props; + this.props.onDelete(fieldSpec.default); } render() { + const {lastValue} = this.state; + const {value} = this.props; + + const canUndo = isLiteralExpression(value) || isLiteralExpression(lastValue); const deleteStopBtn = ( - + <> + + + ); return Date: Sun, 9 Feb 2020 15:08:24 +0000 Subject: [PATCH 08/39] Added expression support for filters. --- src/components/App.jsx | 2 +- src/components/fields/FunctionSpecField.jsx | 27 +- src/components/fields/PropertyGroup.jsx | 2 +- src/components/fields/_ExpressionProperty.jsx | 62 ++--- src/components/filter/FilterEditor.jsx | 254 ++++++++++++++---- src/components/layers/JSONEditor.jsx | 5 +- src/styles/_filtereditor.scss | 5 + 7 files changed, 248 insertions(+), 109 deletions(-) diff --git a/src/components/App.jsx b/src/components/App.jsx index 32e7624c..bb9f9ec9 100644 --- a/src/components/App.jsx +++ b/src/components/App.jsx @@ -318,7 +318,7 @@ export default class App extends React.Component { 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) { diff --git a/src/components/fields/FunctionSpecField.jsx b/src/components/fields/FunctionSpecField.jsx index eb8c1d22..72e48171 100644 --- a/src/components/fields/FunctionSpecField.jsx +++ b/src/components/fields/FunctionSpecField.jsx @@ -8,6 +8,9 @@ import ExpressionProperty from './_ExpressionProperty' import {expression} from '@mapbox/mapbox-gl-style-spec' +function isLiteralExpression (value) { + return (Array.isArray(value) && value.length === 2 && value[0] === "literal"); +} function isZoomField(value) { return typeof value === 'object' && value.stops && typeof value.property === 'undefined' @@ -22,6 +25,7 @@ function isDataField(value) { * properties accept arrays as values, for example `text-font`. So we must try * and create an expression. */ +// TODO: Use function from filter checks. function checkIsExpression (value, fieldSpec={}) { if (!Array.isArray(value)) { return false; @@ -111,8 +115,9 @@ export default class FunctionSpecProperty extends React.Component { this.props.onChange(this.props.fieldName, changedValue) } - deleteExpression = (newValue) => { - this.props.onChange(this.props.fieldName, newValue); + deleteExpression = () => { + const {fieldSpec, fieldName} = this.props; + this.props.onChange(fieldName, fieldSpec.default); this.setState({ isExpression: false, }); @@ -144,6 +149,22 @@ export default class FunctionSpecProperty extends React.Component { this.props.onChange(this.props.fieldName, zoomFunc) } + undoExpression = () => { + const {value, fieldName} = this.props; + + if (isLiteralExpression(value)) { + this.props.onChange(fieldName, value[1]); + this.setState({ + isExpression: false + }); + } + } + + canUndo = () => { + const {value} = this.props; + return isLiteralExpression(value); + } + makeExpression = () => { const expression = ["literal", this.props.value || this.props.fieldSpec.default]; this.props.onChange(this.props.fieldName, expression); @@ -176,6 +197,8 @@ export default class FunctionSpecProperty extends React.Component { { diff --git a/src/components/fields/_ExpressionProperty.jsx b/src/components/fields/_ExpressionProperty.jsx index 8f489b12..e79ed665 100644 --- a/src/components/fields/_ExpressionProperty.jsx +++ b/src/components/fields/_ExpressionProperty.jsx @@ -11,70 +11,38 @@ import stringifyPretty from 'json-stringify-pretty-compact' import JSONEditor from '../layers/JSONEditor' -function isLiteralExpression (value) { - return (Array.isArray(value) && value.length === 2 && value[0] === "literal"); -} - export default class ExpressionProperty extends React.Component { static propTypes = { onDelete: PropTypes.func, fieldName: PropTypes.string, fieldSpec: PropTypes.object, - value: PropTypes.object, + value: PropTypes.any, error: PropTypes.object, onChange: PropTypes.func, } constructor (props) { super(); - this.state = { - lastValue: props.value, - }; - } - - onChange = (jsonVal) => { - if (isLiteralExpression(jsonVal)) { - this.setState({ - lastValue: jsonVal - }); - } - - this.props.onChange(jsonVal); - } - - onReset = () => { - const {lastValue} = this.state; - const {value, fieldSpec} = this.props; - - if (isLiteralExpression(value)) { - this.props.onDelete(value[1]); - } - else if (isLiteralExpression(lastValue)) { - this.props.onDelete(lastValue[1]); - } - } - - onDelete = () => { - const {fieldSpec} = this.props; - this.props.onDelete(fieldSpec.default); } render() { - const {lastValue} = this.state; - const {value} = this.props; + const {value, canUndo} = this.props; - const canUndo = isLiteralExpression(value) || isLiteralExpression(lastValue); const deleteStopBtn = ( <> + {this.props.onUndo && + + } - +
+ } + else if (isSimpleFilter) { + const filter = this.combiningFilter(); + let combiningOp = filter[0]; + let filters = filter.slice(1) + + const actions = ( +
+ +
+ ); + + const editorBlocks = filters.map((f, idx) => { + const error = errors[`filter[${idx+1}]`]; + + return ( + <> + + + + {error && +
{error.message}
+ } + + ); + }) - const editorBlocks = filters.map((f, idx) => { - const error = errors[`filter[${idx+1}]`]; return ( <> - - + - - {error && -
{error.message}
- } +
+ {editorBlocks} +
+ +
+
+ +
); - }) - - //TODO: Implement support for nested filter - if(hasNestedCombiningFilter(filter)) { - return
- Nested filters are not supported. -
} + else { + let {filter} = this.props; - return
-
- k.match(/filter(\[\d+\])?/)) + .map(([k, v]) => { + return v.message; + }) + .join("\n") + const error = errorMessage ? {message: errorMessage} : null; + + return ( + { + this.setState({isSimpleFilter: true}); + this.props.onChange(defaultFilter); + }} + fieldName="filter-compound-filter" fieldSpec={fieldSpec} + value={filter} + error={error} + onChange={this.props.onChange} /> - -
- {editorBlocks} -
- -
-
- -
-
+ ); + } } } diff --git a/src/components/layers/JSONEditor.jsx b/src/components/layers/JSONEditor.jsx index fe212c23..393eb618 100644 --- a/src/components/layers/JSONEditor.jsx +++ b/src/components/layers/JSONEditor.jsx @@ -13,6 +13,7 @@ import 'codemirror/lib/codemirror.css' import 'codemirror/addon/lint/lint.css' import '../../codemirror-maputnik.css' import jsonlint from 'jsonlint' +import stringifyPretty from 'json-stringify-pretty-compact' // This is mainly because of this issue also the API has changed, see comment in file import '../../vendor/codemirror/addon/lint/json-lint' @@ -20,7 +21,7 @@ import '../../vendor/codemirror/addon/lint/json-lint' class JSONEditor extends React.Component { static propTypes = { - layer: PropTypes.object.isRequired, + layer: PropTypes.any.isRequired, maxHeight: PropTypes.number, onChange: PropTypes.func, lineNumbers: PropTypes.bool, @@ -35,7 +36,7 @@ class JSONEditor extends React.Component { lineWrapping: false, gutters: ["CodeMirror-lint-markers"], getValue: (data) => { - return JSON.stringify(data, null, 2) + return stringifyPretty(data, {indent: 2, maxLength: 50} ); } } diff --git a/src/styles/_filtereditor.scss b/src/styles/_filtereditor.scss index 7c3d1542..d836be9e 100644 --- a/src/styles/_filtereditor.scss +++ b/src/styles/_filtereditor.scss @@ -1,5 +1,10 @@ .maputnik-filter-editor-wrapper { padding: $margin-3; + overflow: hidden; + + .maputnik-input-block { + margin: 0; + } } .maputnik-filter-editor { From 109261ba00d8fdbdd65bd72c00c0bf514f89dd64 Mon Sep 17 00:00:00 2001 From: orangemug Date: Sun, 9 Feb 2020 16:33:47 +0000 Subject: [PATCH 09/39] Fixed filter defaults and removed auto-migrate. --- src/components/filter/FilterEditor.jsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/components/filter/FilterEditor.jsx b/src/components/filter/FilterEditor.jsx index 333e3e8b..a458cf25 100644 --- a/src/components/filter/FilterEditor.jsx +++ b/src/components/filter/FilterEditor.jsx @@ -84,6 +84,10 @@ export default class CombiningFilterEditor extends React.Component { onChange: PropTypes.func.isRequired, } + static defaultProps = { + filter: ["all"], + } + constructor (props) { super(); this.state = { @@ -256,13 +260,6 @@ export default class CombiningFilterEditor extends React.Component { else { let {filter} = this.props; - if (!filter) { - filter = defaultFilter; - } - else if (isNestedCombiningFilter) { - filter = migrateFilter(filter); - } - const errorMessage = Object.entries(errors) .filter(([k, v]) => k.match(/filter(\[\d+\])?/)) .map(([k, v]) => { From dc6006fd6d4ff266d0eac0e09c63042453bf828f Mon Sep 17 00:00:00 2001 From: orangemug Date: Sun, 9 Feb 2020 16:36:17 +0000 Subject: [PATCH 10/39] Fixed lint errors. --- src/components/fields/_ExpressionProperty.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/fields/_ExpressionProperty.jsx b/src/components/fields/_ExpressionProperty.jsx index e79ed665..fe1a0433 100644 --- a/src/components/fields/_ExpressionProperty.jsx +++ b/src/components/fields/_ExpressionProperty.jsx @@ -19,6 +19,8 @@ export default class ExpressionProperty extends React.Component { value: PropTypes.any, error: PropTypes.object, onChange: PropTypes.func, + onUndo: PropTypes.func, + canUndo: PropTypes.bool, } constructor (props) { From 0567b098ec75d359fc4715fe265b11f28e96a5b8 Mon Sep 17 00:00:00 2001 From: orangemug Date: Sun, 9 Feb 2020 16:48:52 +0000 Subject: [PATCH 11/39] Fixed undo button disabled state for expressions. --- src/components/fields/_ExpressionProperty.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/fields/_ExpressionProperty.jsx b/src/components/fields/_ExpressionProperty.jsx index fe1a0433..675dc3f0 100644 --- a/src/components/fields/_ExpressionProperty.jsx +++ b/src/components/fields/_ExpressionProperty.jsx @@ -20,7 +20,7 @@ export default class ExpressionProperty extends React.Component { error: PropTypes.object, onChange: PropTypes.func, onUndo: PropTypes.func, - canUndo: PropTypes.bool, + canUndo: PropTypes.func, } constructor (props) { @@ -29,6 +29,7 @@ export default class ExpressionProperty extends React.Component { render() { const {value, canUndo} = this.props; + const undoDisabled = canUndo ? !canUndo() : true; const deleteStopBtn = ( <> @@ -36,7 +37,7 @@ export default class ExpressionProperty extends React.Component {
} - return + const errorKeyStart = `${fieldType}.${fieldName}.stops[${idx}]`; + const foundErrors = Object.entries(errors).filter(([key, error]) => { + return key.startsWith(errorKeyStart); + }); + + const message = foundErrors.map(([key, error]) => { + return error.message; + }).join(""); + const error = message ? {message} : undefined; + + return {zoomInput}
{dataInput} @@ -201,7 +218,6 @@ export default class DataProperty extends React.Component { return
diff --git a/src/components/fields/_ExpressionProperty.jsx b/src/components/fields/_ExpressionProperty.jsx index 675dc3f0..8c5ab96a 100644 --- a/src/components/fields/_ExpressionProperty.jsx +++ b/src/components/fields/_ExpressionProperty.jsx @@ -17,18 +17,22 @@ export default class ExpressionProperty extends React.Component { fieldName: PropTypes.string, fieldSpec: PropTypes.object, value: PropTypes.any, - error: PropTypes.object, + errors: PropTypes.object, onChange: PropTypes.func, onUndo: PropTypes.func, canUndo: PropTypes.func, } + static defaultProps = { + errors: {}, + } + constructor (props) { super(); } render() { - const {value, canUndo} = this.props; + const {errors, fieldName, fieldType, value, canUndo} = this.props; const undoDisabled = canUndo ? !canUndo() : true; const deleteStopBtn = ( @@ -53,8 +57,10 @@ export default class ExpressionProperty extends React.Component { ); + const error = errors[fieldType+"."+fieldName]; + return + const error = errors[fieldType+"."+fieldName]; + return { const zoomLevel = stop[0] const key = this.state.refs[idx]; const value = stop[1] const deleteStopBtn= + const errorKeyStart = `${fieldType}.${fieldName}.stops[${idx}]`; + const foundErrors = Object.entries(errors).filter(([key, error]) => { + return key.startsWith(errorKeyStart); + }); + + const message = foundErrors.map(([key, error]) => { + return error.message; + }).join(""); + const error = message ? {message} : undefined; + return Date: Sun, 16 Feb 2020 21:30:27 +0000 Subject: [PATCH 18/39] Fixed lint errors. --- src/components/fields/_DataProperty.jsx | 1 + src/components/fields/_ExpressionProperty.jsx | 1 + 2 files changed, 2 insertions(+) diff --git a/src/components/fields/_DataProperty.jsx b/src/components/fields/_DataProperty.jsx index d2bbaba4..73f0c4d5 100644 --- a/src/components/fields/_DataProperty.jsx +++ b/src/components/fields/_DataProperty.jsx @@ -40,6 +40,7 @@ export default class DataProperty extends React.Component { onDeleteStop: PropTypes.func, onAddStop: PropTypes.func, fieldName: PropTypes.string, + fieldType: PropTypes.string, fieldSpec: PropTypes.object, value: PropTypes.oneOfType([ PropTypes.object, diff --git a/src/components/fields/_ExpressionProperty.jsx b/src/components/fields/_ExpressionProperty.jsx index 8c5ab96a..d00dfd6b 100644 --- a/src/components/fields/_ExpressionProperty.jsx +++ b/src/components/fields/_ExpressionProperty.jsx @@ -15,6 +15,7 @@ export default class ExpressionProperty extends React.Component { static propTypes = { onDelete: PropTypes.func, fieldName: PropTypes.string, + fieldType: PropTypes.string, fieldSpec: PropTypes.object, value: PropTypes.any, errors: PropTypes.object, From b7d08dfaa62025d0495b851e9bc23ab13c23801a Mon Sep 17 00:00:00 2001 From: orangemug Date: Sun, 16 Feb 2020 21:56:03 +0000 Subject: [PATCH 19/39] Fixed data function display issues. --- src/components/fields/DocLabel.jsx | 9 ++- src/components/fields/_DataProperty.jsx | 102 ++++++++++++------------ src/styles/_zoomproperty.scss | 4 - 3 files changed, 59 insertions(+), 56 deletions(-) diff --git a/src/components/fields/DocLabel.jsx b/src/components/fields/DocLabel.jsx index 83fd6248..5375fbb8 100644 --- a/src/components/fields/DocLabel.jsx +++ b/src/components/fields/DocLabel.jsx @@ -49,8 +49,15 @@ export default class DocLabel extends React.Component {
} + else if (label) { + return + } else { - return
+
} } } diff --git a/src/components/fields/_DataProperty.jsx b/src/components/fields/_DataProperty.jsx index 73f0c4d5..f3cef010 100644 --- a/src/components/fields/_DataProperty.jsx +++ b/src/components/fields/_DataProperty.jsx @@ -217,58 +217,58 @@ export default class DataProperty extends React.Component { }) return
-
- -
- -
- this.changeDataProperty("property", propVal)} - /> -
-
-
- -
- this.changeDataProperty("type", propVal)} - title={"Select a type of data scale (default is 'categorical')."} - options={this.getDataFunctionTypes(this.props.fieldSpec)} - /> -
-
-
- -
- this.changeDataProperty("default", propVal)} - /> -
-
- {dataFields} - -
-
+
+ +
+ this.changeDataProperty("property", propVal)} + /> +
+
+
+ +
+ this.changeDataProperty("type", propVal)} + title={"Select a type of data scale (default is 'categorical')."} + options={this.getDataFunctionTypes(this.props.fieldSpec)} + /> +
+
+
+ +
+ this.changeDataProperty("default", propVal)} + /> +
+
+ +
+ {dataFields} +
} } diff --git a/src/styles/_zoomproperty.scss b/src/styles/_zoomproperty.scss index 1d59c9f1..38e26c55 100644 --- a/src/styles/_zoomproperty.scss +++ b/src/styles/_zoomproperty.scss @@ -96,10 +96,6 @@ .maputnik-data-spec-property-input { width: 75%; display: inline-block; - - .maputnik-string { - margin-bottom: 3%; - } } } } From 029eff93176db3785a0f748e1dba236a2532e671 Mon Sep 17 00:00:00 2001 From: orangemug Date: Sun, 16 Feb 2020 22:02:05 +0000 Subject: [PATCH 20/39] Added margin-left to function errors to line up with values. --- src/styles/_components.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/styles/_components.scss b/src/styles/_components.scss index a4d666f8..620228fd 100644 --- a/src/styles/_components.scss +++ b/src/styles/_components.scss @@ -194,6 +194,10 @@ .maputnik-input-block-action > div { text-align: right; } + + .maputnik-inline-error { + margin-left: 32%; + } } // SPACE HELPER From cff32696cc2ef3ebe9e167a5adadfdffd8dd3249 Mon Sep 17 00:00:00 2001 From: orangemug Date: Mon, 17 Feb 2020 11:29:05 +0000 Subject: [PATCH 21/39] Better handling of undo history for expressions. --- src/components/fields/FunctionSpecField.jsx | 74 +++++++++++++------ src/components/fields/_ExpressionProperty.jsx | 4 + src/components/layers/JSONEditor.jsx | 21 +++--- 3 files changed, 67 insertions(+), 32 deletions(-) diff --git a/src/components/fields/FunctionSpecField.jsx b/src/components/fields/FunctionSpecField.jsx index d1e30d3c..fb12a45b 100644 --- a/src/components/fields/FunctionSpecField.jsx +++ b/src/components/fields/FunctionSpecField.jsx @@ -45,27 +45,30 @@ function isPrimative (value) { } function isArrayOfPrimatives (values) { - if (Array.isArray(value)) { + if (Array.isArray(values)) { return values.every(isPrimative); } return false; } -function checkIsExpression (value, fieldSpec={}) { +function getDataType (value, fieldSpec={}) { if (value === undefined) { - return false; + return "value"; } else if (isPrimative(value)) { - return false; + return "value"; } else if (fieldSpec.type === "array" && isArrayOfPrimatives(value)) { - return false; + return "value"; } - else if (isZoomField(value) || isDataField(value)) { - return false; + else if (isZoomField(value)) { + return "zoom_function"; + } + else if (isDataField(value)) { + return "data_function"; } else { - return true; + return "expression"; } } @@ -111,7 +114,21 @@ export default class FunctionSpecProperty extends React.Component { constructor (props) { super(); this.state = { - isExpression: checkIsExpression(props.value, props.fieldSpec), + dataType: getDataType(props.value, props.fieldSpec), + isEditing: false, + } + } + + static getDerivedStateFromProps(props, state) { + // Because otherwise when editing values we end up accidentally changing field type. + if (state.isEditing) { + return {}; + } + else { + return { + isEditing: false, + dataType: getDataType(props.value, props.fieldSpec) + }; } } @@ -150,7 +167,7 @@ export default class FunctionSpecProperty extends React.Component { const {fieldSpec, fieldName} = this.props; this.props.onChange(fieldName, fieldSpec.default); this.setState({ - isExpression: false, + dataType: "value", }); } @@ -184,25 +201,25 @@ export default class FunctionSpecProperty extends React.Component { const {value, fieldName} = this.props; if (isLiteralExpression(value)) { - this.props.onChange(fieldName, value[1]); - this.setState({ - isExpression: false - }); + this.props.onChange(fieldName, value[1]); + this.setState({ + dataType: "value", + }); } } canUndo = () => { - const {value} = this.props; - return isLiteralExpression(value); + const {value, fieldSpec} = this.props; + return ( + isLiteralExpression(value) || + isPrimative(value) || + (Array.isArray(value) && fieldSpec.type === "array") + ); } makeExpression = () => { const expression = ["literal", this.props.value || this.props.fieldSpec.default]; this.props.onChange(this.props.fieldName, expression); - - this.setState({ - isExpression: true, - }); } makeDataFunction = () => { @@ -219,11 +236,20 @@ export default class FunctionSpecProperty extends React.Component { this.props.onChange(this.props.fieldName, dataFunc) } + onMarkEditing = () => { + this.setState({isEditing: true}); + } + + onUnmarkEditing = () => { + this.setState({isEditing: false}); + } + render() { + const {dataType} = this.state; const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property" let specField; - if (this.state.isExpression) { + if (dataType === "expression") { specField = ( ); } - else if (isZoomField(this.props.value)) { + else if (dataType === "zoom_function") { specField = ( ) } - else if (isDataField(this.props.value)) { + else if (dataType === "data_function") { specField = ( {}, + onBlur: () => {}, } constructor (props) { @@ -69,6 +71,8 @@ export default class ExpressionProperty extends React.Component { > { - return stringifyPretty(data, {indent: 2, maxLength: 50} ); - } + return stringifyPretty(data, {indent: 2, maxLength: 50}); + }, + onFocus: () => {}, + onBlur: () => {}, } constructor(props) { super(props) this.state = { isEditing: false, - prevValue: this.getValue(), + prevValue: this.props.getValue(this.props.layer), }; } - getValue () { - return this.props.getValue(this.props.layer); - } - componentDidMount () { this._doc = CodeMirror(this._el, { - value: this.getValue(), + value: this.props.getValue(this.props.layer), mode: { name: "javascript", json: true @@ -75,12 +73,14 @@ class JSONEditor extends React.Component { } onFocus = () => { + this.props.onFocus(); this.setState({ isEditing: true }); } onBlur = () => { + this.props.onBlur(); this.setState({ isEditing: false }); @@ -96,7 +96,7 @@ class JSONEditor extends React.Component { if (!this.state.isEditing && prevProps.layer !== this.props.layer) { this._cancelNextChange = true; this._doc.setValue( - this.getValue(), + this.props.getValue(this.props.layer), ) } } @@ -104,6 +104,9 @@ class JSONEditor extends React.Component { onChange = (e) => { if (this._cancelNextChange) { this._cancelNextChange = false; + this.setState({ + prevValue: this._doc.getValue(), + }) return; } const newCode = this._doc.getValue(); From 793b5d15ad7514b183f87f42c5c91f4542df460b Mon Sep 17 00:00:00 2001 From: orangemug Date: Mon, 17 Feb 2020 11:30:48 +0000 Subject: [PATCH 22/39] Fixed lint errors --- src/components/fields/_ExpressionProperty.jsx | 2 ++ src/components/layers/JSONEditor.jsx | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/components/fields/_ExpressionProperty.jsx b/src/components/fields/_ExpressionProperty.jsx index bb31b81f..00470f39 100644 --- a/src/components/fields/_ExpressionProperty.jsx +++ b/src/components/fields/_ExpressionProperty.jsx @@ -22,6 +22,8 @@ export default class ExpressionProperty extends React.Component { onChange: PropTypes.func, onUndo: PropTypes.func, canUndo: PropTypes.func, + onFocus: PropTypes.func, + onBlur: PropTypes.func, } static defaultProps = { diff --git a/src/components/layers/JSONEditor.jsx b/src/components/layers/JSONEditor.jsx index ed5d7921..0e2ae5bd 100644 --- a/src/components/layers/JSONEditor.jsx +++ b/src/components/layers/JSONEditor.jsx @@ -28,6 +28,8 @@ class JSONEditor extends React.Component { getValue: PropTypes.func, gutters: PropTypes.array, className: PropTypes.string, + onFocus: PropTypes.func, + onBlur: PropTypes.func, } static defaultProps = { From 3ffdcc9639764f41d0a87a0f1fd6810feb05ba46 Mon Sep 17 00:00:00 2001 From: orangemug Date: Mon, 17 Feb 2020 11:52:40 +0000 Subject: [PATCH 23/39] Added the ability to convert from data/zoom function to expression --- src/components/fields/FunctionSpecField.jsx | 13 ++++++++++++- src/components/fields/_DataProperty.jsx | 6 ++++++ src/components/fields/_ZoomProperty.jsx | 6 ++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/components/fields/FunctionSpecField.jsx b/src/components/fields/FunctionSpecField.jsx index fb12a45b..e5a45375 100644 --- a/src/components/fields/FunctionSpecField.jsx +++ b/src/components/fields/FunctionSpecField.jsx @@ -5,6 +5,7 @@ import SpecProperty from './_SpecProperty' import DataProperty from './_DataProperty' import ZoomProperty from './_ZoomProperty' import ExpressionProperty from './_ExpressionProperty' +import {function as styleFunction} from '@mapbox/mapbox-gl-style-spec'; function isLiteralExpression (value) { @@ -218,7 +219,15 @@ export default class FunctionSpecProperty extends React.Component { } makeExpression = () => { - const expression = ["literal", this.props.value || this.props.fieldSpec.default]; + const {value, fieldSpec} = this.props; + let expression; + + if (typeof(value) === "object" && 'stops' in value) { + expression = styleFunction.convertFunction(value, fieldSpec); + } + else { + expression = ["literal", value || this.props.fieldSpec.default]; + } this.props.onChange(this.props.fieldName, expression); } @@ -277,6 +286,7 @@ export default class FunctionSpecProperty extends React.Component { value={this.props.value} onDeleteStop={this.deleteStop} onAddStop={this.addStop} + onExpressionClick={this.makeExpression} /> ) } @@ -291,6 +301,7 @@ export default class FunctionSpecProperty extends React.Component { value={this.props.value} onDeleteStop={this.deleteStop} onAddStop={this.addStop} + onExpressionClick={this.makeExpression} /> ) } diff --git a/src/components/fields/_DataProperty.jsx b/src/components/fields/_DataProperty.jsx index f3cef010..7503d62c 100644 --- a/src/components/fields/_DataProperty.jsx +++ b/src/components/fields/_DataProperty.jsx @@ -269,6 +269,12 @@ export default class DataProperty extends React.Component { > Add stop +
} } diff --git a/src/components/fields/_ZoomProperty.jsx b/src/components/fields/_ZoomProperty.jsx index 21bb7f7b..3db421f2 100644 --- a/src/components/fields/_ZoomProperty.jsx +++ b/src/components/fields/_ZoomProperty.jsx @@ -177,6 +177,12 @@ export default class ZoomProperty extends React.Component { > Add stop +
} } From d1cb2690fc262aabf0bf4c3ec655d4eb718da46d Mon Sep 17 00:00:00 2001 From: orangemug Date: Mon, 17 Feb 2020 11:55:16 +0000 Subject: [PATCH 24/39] Fixed lint errors. --- src/components/fields/_DataProperty.jsx | 1 + src/components/fields/_ZoomProperty.jsx | 1 + 2 files changed, 2 insertions(+) diff --git a/src/components/fields/_DataProperty.jsx b/src/components/fields/_DataProperty.jsx index 7503d62c..80961b79 100644 --- a/src/components/fields/_DataProperty.jsx +++ b/src/components/fields/_DataProperty.jsx @@ -39,6 +39,7 @@ export default class DataProperty extends React.Component { onChange: PropTypes.func, onDeleteStop: PropTypes.func, onAddStop: PropTypes.func, + onExpressionClick: PropTypes.func, fieldName: PropTypes.string, fieldType: PropTypes.string, fieldSpec: PropTypes.object, diff --git a/src/components/fields/_ZoomProperty.jsx b/src/components/fields/_ZoomProperty.jsx index 3db421f2..829d3567 100644 --- a/src/components/fields/_ZoomProperty.jsx +++ b/src/components/fields/_ZoomProperty.jsx @@ -42,6 +42,7 @@ export default class ZoomProperty extends React.Component { onChange: PropTypes.func, onDeleteStop: PropTypes.func, onAddStop: PropTypes.func, + onExpressionClick: PropTypes.func, fieldType: PropTypes.string, fieldName: PropTypes.string, fieldSpec: PropTypes.object, From adea3d0f13f2e0e27181acb27722974d17ec75be Mon Sep 17 00:00:00 2001 From: orangemug Date: Mon, 17 Feb 2020 13:19:26 +0000 Subject: [PATCH 25/39] Added ability to switch to layer from global error panel. --- src/components/App.jsx | 1 + src/components/MessagePanel.jsx | 14 +++++++++++++- src/styles/_components.scss | 6 ++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/components/App.jsx b/src/components/App.jsx index dc870b95..3e0158c8 100644 --- a/src/components/App.jsx +++ b/src/components/App.jsx @@ -732,6 +732,7 @@ export default class App extends React.Component { /> : null const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? {}, } render() { @@ -14,9 +19,16 @@ class MessagePanel extends React.Component { if (error.parsed && error.parsed.type === "layer") { const {parsed} = error; const {mapStyle} = this.props; + const layerId = mapStyle.layers[parsed.data.index].id; content = ( <> - Layer '{mapStyle.layers[parsed.data.index].id}': {parsed.data.message} + Layer '{layerId}': {parsed.data.message} —  + ); } diff --git a/src/styles/_components.scss b/src/styles/_components.scss index 620228fd..87128e95 100644 --- a/src/styles/_components.scss +++ b/src/styles/_components.scss @@ -212,6 +212,12 @@ &-error { color: $color-red; } + + &__switch-button { + all: unset; + text-decoration: underline; + cursor: pointer; + } } .maputnik-dialog { From 39333953d70e4112ba138c4e37ce4024d7eb3421 Mon Sep 17 00:00:00 2001 From: orangemug Date: Mon, 17 Feb 2020 13:24:14 +0000 Subject: [PATCH 26/39] Only show 'switch to layer' button if layer not already selected. --- src/components/App.jsx | 1 + src/components/MessagePanel.jsx | 22 ++++++++++++++-------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/components/App.jsx b/src/components/App.jsx index 3e0158c8..6273df33 100644 --- a/src/components/App.jsx +++ b/src/components/App.jsx @@ -732,6 +732,7 @@ export default class App extends React.Component { /> : null const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? - Layer '{layerId}': {parsed.data.message} —  - + Layer '{layerId}': {parsed.data.message} + {currentLayer.id !== layerId && + <> +  —  + + + } ); } From 35098111ac31e689fb23ffda4efde5c48a6af758 Mon Sep 17 00:00:00 2001 From: orangemug Date: Mon, 17 Feb 2020 13:41:11 +0000 Subject: [PATCH 27/39] Include missing errors from expressions UI. --- src/components/fields/_ExpressionProperty.jsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/fields/_ExpressionProperty.jsx b/src/components/fields/_ExpressionProperty.jsx index 00470f39..f9214eff 100644 --- a/src/components/fields/_ExpressionProperty.jsx +++ b/src/components/fields/_ExpressionProperty.jsx @@ -62,7 +62,18 @@ export default class ExpressionProperty extends React.Component { ); - const error = errors[fieldType+"."+fieldName]; + const fieldError = errors[fieldType+"."+fieldName]; + const errorKeyStart = `${fieldType}.${fieldName}[`; + const foundErrors = Object.entries(errors).filter(([key, error]) => { + return key.startsWith(errorKeyStart); + }); + let message = foundErrors.map(([key, error]) => { + return error.message; + }).join(""); + if (fieldError) { + message = fieldError.message + message; + } + const error = message ? {message} : undefined; return Date: Mon, 17 Feb 2020 13:44:05 +0000 Subject: [PATCH 28/39] Fixed width issue with expression error UI. --- src/styles/_components.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/styles/_components.scss b/src/styles/_components.scss index 87128e95..375f55fe 100644 --- a/src/styles/_components.scss +++ b/src/styles/_components.scss @@ -195,6 +195,10 @@ text-align: right; } +} + +.maputnik-data-spec-block, +.maputnik-zoom-spec-property { .maputnik-inline-error { margin-left: 32%; } From 52e8b21b3dac80f7f18eeff78953ec0c13106f01 Mon Sep 17 00:00:00 2001 From: orangemug Date: Tue, 18 Feb 2020 08:06:36 +0000 Subject: [PATCH 29/39] Tidy function. --- src/components/fields/FunctionSpecField.jsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/fields/FunctionSpecField.jsx b/src/components/fields/FunctionSpecField.jsx index e5a45375..cc8fb16c 100644 --- a/src/components/fields/FunctionSpecField.jsx +++ b/src/components/fields/FunctionSpecField.jsx @@ -19,7 +19,12 @@ function isZoomField(value) { typeof(value.property) === 'undefined' && Array.isArray(value.stops) && value.stops.length > 1 && - value.stops.every(stop => Array.isArray(stop) && stop.length === 2) + value.stops.every(stop => { + return ( + Array.isArray(stop) && + stop.length === 2 + ); + }) ); } From a693f6db4ee9157b3b72c141d2b911476fec8cb6 Mon Sep 17 00:00:00 2001 From: orangemug Date: Sat, 22 Feb 2020 11:54:59 +0000 Subject: [PATCH 30/39] Added infobox for old style filters to allow you to switch to the filter editor. --- src/components/filter/FilterEditor.jsx | 104 +++++++++++++++---------- src/styles/index.scss | 19 +++++ 2 files changed, 84 insertions(+), 39 deletions(-) diff --git a/src/components/filter/FilterEditor.jsx b/src/components/filter/FilterEditor.jsx index a458cf25..4fd890b9 100644 --- a/src/components/filter/FilterEditor.jsx +++ b/src/components/filter/FilterEditor.jsx @@ -13,6 +13,19 @@ import SpecDoc from '../inputs/SpecDoc' import ExpressionProperty from '../fields/_ExpressionProperty'; +function combiningFilter (props) { + let filter = props.filter || ['all']; + + let combiningOp = filter[0]; + let filters = filter.slice(1); + + if(combiningFilterOps.indexOf(combiningOp) < 0) { + combiningOp = 'all'; + filters = [filter.slice(0)]; + } + + return [combiningOp, ...filters]; +} function migrateFilter (filter) { return migrate(createStyleFromFilter(filter)).layers[0].filter; @@ -92,39 +105,25 @@ export default class CombiningFilterEditor extends React.Component { super(); this.state = { showDoc: false, - isSimpleFilter: checkIfSimpleFilter(this.combiningFilter(props)), + displaySimpleFilter: checkIfSimpleFilter(combiningFilter(props)), }; } // Convert filter to combining filter - combiningFilter(props=this.props) { - let filter = props.filter || ['all'] - - let combiningOp = filter[0] - let filters = filter.slice(1) - - if(combiningFilterOps.indexOf(combiningOp) < 0) { - combiningOp = 'all' - filters = [filter.slice(0)] - } - - return [combiningOp, ...filters] - } - onFilterPartChanged(filterIdx, newPart) { - const newFilter = this.combiningFilter().slice(0) + const newFilter = combiningFilter(this.props).slice(0) newFilter[filterIdx] = newPart this.props.onChange(newFilter) } deleteFilterItem(filterIdx) { - const newFilter = this.combiningFilter().slice(0) + const newFilter = combiningFilter(this.props).slice(0) newFilter.splice(filterIdx + 1, 1) this.props.onChange(newFilter) } addFilterItem = () => { - const newFilterItem = this.combiningFilter().slice(0) + const newFilterItem = combiningFilter(this.props).slice(0) newFilterItem.push(['==', 'name', '']) this.props.onChange(newFilterItem) } @@ -135,38 +134,52 @@ export default class CombiningFilterEditor extends React.Component { }); } + makeFilter = () => { + this.setState({ + displaySimpleFilter: true, + }) + } + makeExpression = () => { - let filter = this.combiningFilter(); + let filter = combiningFilter(this.props); this.props.onChange(migrateFilter(filter)); this.setState({ - isSimpleFilter: false, + displaySimpleFilter: false, }) } static getDerivedStateFromProps (props, currentState) { const {filter} = props; - const isSimpleFilter = checkIfSimpleFilter(props.filter); + const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props)); // Upgrade but never downgrade - if (!isSimpleFilter && currentState.isSimpleFilter === true) { + if (!displaySimpleFilter && currentState.displaySimpleFilter === true) { return { - isSimpleFilter: false, + displaySimpleFilter: false, + valueIsSimpleFilter: false, }; } + else if (displaySimpleFilter && currentState.displaySimpleFilter === false) { + return { + valueIsSimpleFilter: true, + } + } else { - return {}; + return { + valueIsSimpleFilter: false, + }; } } render() { const {errors} = this.props; - const {isSimpleFilter} = this.state; + const {displaySimpleFilter} = this.state; const fieldSpec={ doc: latest.layer.filter.doc + " Combine multiple filters together by using a compound filter." }; const defaultFilter = ["all"]; - const isNestedCombiningFilter = isSimpleFilter && hasNestedCombiningFilter(this.combiningFilter()); + const isNestedCombiningFilter = displaySimpleFilter && hasNestedCombiningFilter(combiningFilter(this.props)); if (isNestedCombiningFilter) { return
@@ -183,8 +196,8 @@ export default class CombiningFilterEditor extends React.Component {
} - else if (isSimpleFilter) { - const filter = this.combiningFilter(); + else if (displaySimpleFilter) { + const filter = combiningFilter(this.props); let combiningOp = filter[0]; let filters = filter.slice(1) @@ -269,17 +282,30 @@ export default class CombiningFilterEditor extends React.Component { const error = errorMessage ? {message: errorMessage} : null; return ( - { - this.setState({isSimpleFilter: true}); - this.props.onChange(defaultFilter); - }} - fieldName="filter-compound-filter" - fieldSpec={fieldSpec} - value={filter} - error={error} - onChange={this.props.onChange} - /> + <> + { + this.setState({displaySimpleFilter: true}); + this.props.onChange(defaultFilter); + }} + fieldName="filter-compound-filter" + fieldSpec={fieldSpec} + value={filter} + error={error} + onChange={this.props.onChange} + /> + {this.state.valueIsSimpleFilter && +
+ You've entered a old style filter,{' '} + +
+ } + ); } } diff --git a/src/styles/index.scss b/src/styles/index.scss index 54c11c4b..1cd9a18c 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -63,3 +63,22 @@ text-align: right; } } + +.maputnik-expr-infobox { + font-size: $font-size-6; + background: $color-midgray; + padding: $margin-2; + border-radius: 2px; + border-top-right-radius: 0px; + border-top-left-radius: 0px; + color: $color-white; +} +.maputnik-expr-infobox__button { + unset: all; + background: none; + border: none; + padding: 0; + text-decoration: underline; + color: currentColor; + cursor: pointer; +} From 3b5ba6c59e2c81cf88f402875bc0b9f08b7c8b29 Mon Sep 17 00:00:00 2001 From: orangemug Date: Sat, 22 Feb 2020 13:15:55 +0000 Subject: [PATCH 31/39] Fixed lint errors. --- src/components/filter/FilterEditor.jsx | 2 +- src/styles/index.scss | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/filter/FilterEditor.jsx b/src/components/filter/FilterEditor.jsx index 4fd890b9..9b05fe8b 100644 --- a/src/components/filter/FilterEditor.jsx +++ b/src/components/filter/FilterEditor.jsx @@ -296,7 +296,7 @@ export default class CombiningFilterEditor extends React.Component { /> {this.state.valueIsSimpleFilter &&
- You've entered a old style filter,{' '} + You've entered a old style filter,{' '}
{errors.length > 0 &&
- {[].concat(this.props.error).map(error => { - return
{error.message}
+ {[].concat(this.props.error).map((error, idx) => { + return
{error.message}
})}
} From ce976991d4f725ae5b6f3658d057d6c18fa4db76 Mon Sep 17 00:00:00 2001 From: orangemug Date: Sun, 8 Mar 2020 18:38:32 +0000 Subject: [PATCH 35/39] Added inline errors to the code-mirror editors based on field spec. --- package.json | 3 +- src/components/App.jsx | 4 +- src/components/fields/_ExpressionProperty.jsx | 11 +- src/components/filter/FilterEditor.jsx | 4 + src/components/layers/JSONEditor.jsx | 17 +- src/components/util/codemirror-mgl.js | 154 ++++++++++++++++++ 6 files changed, 185 insertions(+), 8 deletions(-) create mode 100644 src/components/util/codemirror-mgl.js diff --git a/package.json b/package.json index de5583d2..29aaec48 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ "color": "^3.1.2", "detect-browser": "^4.8.0", "file-saver": "^2.0.2", - "jsonlint": "github:josdejong/jsonlint#85a19d7", + "json-to-ast": "^2.1.0", + "jsonlint": "^1.6.3", "lodash": "^4.17.15", "lodash.capitalize": "^4.2.1", "lodash.clamp": "^4.0.3", diff --git a/src/components/App.jsx b/src/components/App.jsx index 05e41aec..94632298 100644 --- a/src/components/App.jsx +++ b/src/components/App.jsx @@ -358,7 +358,9 @@ export default class App extends React.Component { if (message) { try { const objPath = message.split(":")[0]; - unset(dirtyMapStyle, objPath); + // Errors can be deply nested for example 'layers[0].filter[1][1][0]' we only care upto the property 'layers[0].filter' + const unsetPath = objPath.match(/^\S+?\[\d+\]\.[^\[]+/)[0]; + unset(dirtyMapStyle, unsetPath); } catch (err) { console.warn(err); diff --git a/src/components/fields/_ExpressionProperty.jsx b/src/components/fields/_ExpressionProperty.jsx index b50fbb52..99005991 100644 --- a/src/components/fields/_ExpressionProperty.jsx +++ b/src/components/fields/_ExpressionProperty.jsx @@ -83,6 +83,10 @@ export default class ExpressionProperty extends React.Component { const fieldError = errors[fieldKey]; const errorKeyStart = `${fieldKey}[`; const foundErrors = []; + + function getValue (data) { + return stringifyPretty(data, {indent: 2, maxLength: 38}) + } if (jsonError) { foundErrors.push({message: "Invalid JSON"}); @@ -109,6 +113,11 @@ export default class ExpressionProperty extends React.Component { wideMode={true} > stringifyPretty(data, {indent: 2, maxLength: 50})} + getValue={getValue} onChange={this.props.onChange} />
diff --git a/src/components/filter/FilterEditor.jsx b/src/components/filter/FilterEditor.jsx index 57094766..01a0d8c9 100644 --- a/src/components/filter/FilterEditor.jsx +++ b/src/components/filter/FilterEditor.jsx @@ -16,6 +16,10 @@ import ExpressionProperty from '../fields/_ExpressionProperty'; function combiningFilter (props) { let filter = props.filter || ['all']; + if (!Array.isArray(filter)) { + return filter; + } + let combiningOp = filter[0]; let filters = filter.slice(1); diff --git a/src/components/layers/JSONEditor.jsx b/src/components/layers/JSONEditor.jsx index 80ce055e..05b23f45 100644 --- a/src/components/layers/JSONEditor.jsx +++ b/src/components/layers/JSONEditor.jsx @@ -13,6 +13,7 @@ import 'codemirror/lib/codemirror.css' import 'codemirror/addon/lint/lint.css' import jsonlint from 'jsonlint' import stringifyPretty from 'json-stringify-pretty-compact' +import '../util/codemirror-mgl'; // This is mainly because of this issue also the API has changed, see comment in file import '../../vendor/codemirror/addon/lint/json-lint' @@ -32,6 +33,11 @@ class JSONEditor extends React.Component { onBlur: PropTypes.func, onJSONValid: PropTypes.func, onJSONInvalid: PropTypes.func, + mode: PropTypes.object, + lint: PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.object, + ]), } static defaultProps = { @@ -39,7 +45,7 @@ class JSONEditor extends React.Component { lineWrapping: false, gutters: ["CodeMirror-lint-markers"], getValue: (data) => { - return stringifyPretty(data, {indent: 2, maxLength: 50}); + return stringifyPretty(data, {indent: 2, maxLength: 40}); }, onFocus: () => {}, onBlur: () => {}, @@ -58,16 +64,17 @@ class JSONEditor extends React.Component { componentDidMount () { this._doc = CodeMirror(this._el, { value: this.props.getValue(this.props.layer), - mode: { - name: "javascript", - json: true + mode: this.props.mode || { + name: "mgl", }, lineWrapping: this.props.lineWrapping, tabSize: 2, theme: 'maputnik', viewportMargin: Infinity, lineNumbers: this.props.lineNumbers, - lint: true, + lint: this.props.lint || { + context: "layer" + }, matchBrackets: true, gutters: this.props.gutters, scrollbarStyle: "null", diff --git a/src/components/util/codemirror-mgl.js b/src/components/util/codemirror-mgl.js new file mode 100644 index 00000000..f4ab3d4b --- /dev/null +++ b/src/components/util/codemirror-mgl.js @@ -0,0 +1,154 @@ +import jsonlint from 'jsonlint'; +import CodeMirror from 'codemirror'; +import jsonToAst from 'json-to-ast'; +import {expression, validate, latest} from '@mapbox/mapbox-gl-style-spec'; + + +CodeMirror.defineMode("mgl", function(config, parserConfig) { + // Just using the javascript mode with json enabled. Our logic is in the linter below. + return CodeMirror.modes.javascript( + {...config, json: true}, + parserConfig + ); +}); + +CodeMirror.registerHelper("lint", "mgl", function(text, opts, doc) { + const found = []; + const {parser} = jsonlint; + const {context} = opts; + + parser.parseError = function(str, hash) { + const loc = hash.loc; + found.push({ + from: CodeMirror.Pos(loc.first_line - 1, loc.first_column), + to: CodeMirror.Pos(loc.last_line - 1, loc.last_column), + message: str + }); + }; + try { + parser.parse(text); + } + catch (e) {} + + if (found.length > 0) { + // JSON invalid so don't go any further + return found; + } + + const ast = jsonToAst(text); + const input = JSON.parse(text); + + function getArrayPositionalFromAst (node, path) { + if (!node) { + return undefined; + } + else if (path.length < 1) { + return node; + } + else if (!node.children) { + return undefined; + } + else { + const key = path[0]; + let newNode; + if (key.match(/^[0-9]+$/)) { + newNode = node.children[path[0]]; + } + else { + newNode = node.children.find(childNode => { + return ( + childNode.key && + childNode.key.type === "Identifier" && + childNode.key.value === key + ); + }); + if (newNode) { + newNode = newNode.value; + } + } + return getArrayPositionalFromAst(newNode, path.slice(1)) + } + } + + let out; + if (context === "layer") { + // Just an empty style so we can validate a layer. + const errors = validate({ + "version": 8, + "name": "Empty Style", + "metadata": {}, + "sources": {}, + "sprite": "", + "glyphs": "https://example.com/glyphs/{fontstack}/{range}.pbf", + "layers": [ + input + ] + }); + + if (errors) { + out = { + result: "error", + value: errors + .filter(err => { + // Remove missing 'layer source' errors, because we don't include them + if (err.message.match(/^layers\[0\]: source ".*" not found$/)) { + return false; + } + else { + return true; + } + }) + .map(err => { + // Remove the 'layers[0].' as we're validating the layer only here + const errMessageParts = err.message.replace(/^layers\[0\]./, "").split(":"); + return { + key: errMessageParts[0], + message: errMessageParts[1], + }; + }) + } + } + } + else if (context === "expression") { + out = expression.createExpression(input, opts.spec); + } + else { + throw new Error(`Invalid context ${context}`); + } + + if (out.result === "error") { + const errors = out.value; + errors.forEach(error => { + const {key, message} = error; + + if (!key) { + const lastLineHandle = doc.getLineHandle(doc.lastLine()); + const err = { + from: CodeMirror.Pos(doc.firstLine(), 0), + to: CodeMirror.Pos(doc.lastLine(), lastLineHandle.text.length), + message: message, + } + found.push(err); + } + else if (key) { + const path = key.replace(/^\[|\]$/g, "").split(/\.|[\[\]]+/).filter(Boolean) + const parsedError = getArrayPositionalFromAst(ast, path); + if (!parsedError) { + console.warn("Something went wrong parsing error:", error); + return; + } + + const {loc} = parsedError; + const {start, end} = loc; + + found.push({ + from: CodeMirror.Pos(start.line - 1, start.column), + to: CodeMirror.Pos(end.line - 1, end.column), + message: message, + }); + } + }) + } + + return found; +}); From 1c953bc296c640d236a0894760692536ea60c69b Mon Sep 17 00:00:00 2001 From: orangemug Date: Sun, 8 Mar 2020 18:43:46 +0000 Subject: [PATCH 36/39] Remove old hack 'src/vendor/codemirror/addon/lint/json-lint' --- src/components/layers/JSONEditor.jsx | 3 -- src/vendor/codemirror/addon/lint/json-lint.js | 31 ------------------- 2 files changed, 34 deletions(-) delete mode 100644 src/vendor/codemirror/addon/lint/json-lint.js diff --git a/src/components/layers/JSONEditor.jsx b/src/components/layers/JSONEditor.jsx index 05b23f45..2b8f6a84 100644 --- a/src/components/layers/JSONEditor.jsx +++ b/src/components/layers/JSONEditor.jsx @@ -15,9 +15,6 @@ import jsonlint from 'jsonlint' import stringifyPretty from 'json-stringify-pretty-compact' import '../util/codemirror-mgl'; -// This is mainly because of this issue also the API has changed, see comment in file -import '../../vendor/codemirror/addon/lint/json-lint' - class JSONEditor extends React.Component { static propTypes = { diff --git a/src/vendor/codemirror/addon/lint/json-lint.js b/src/vendor/codemirror/addon/lint/json-lint.js deleted file mode 100644 index 5c80a840..00000000 --- a/src/vendor/codemirror/addon/lint/json-lint.js +++ /dev/null @@ -1,31 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE - -// Depends on fork of jsonlint from -// becuase of -var jsonlint = require("jsonlint"); -var CodeMirror = require("codemirror"); - -CodeMirror.registerHelper("lint", "json", function(text) { - var found = []; - - // NOTE: This was modified from the original to remove the global, also the - // old jsonlint API was 'jsonlint.parseError' its now - // 'jsonlint.parser.parseError' - jsonlint.parser.parseError = function(str, hash) { - var loc = hash.loc; - found.push({ - from: CodeMirror.Pos(loc.first_line - 1, loc.first_column), - to: CodeMirror.Pos(loc.last_line - 1, loc.last_column), - message: str - }); - }; - - try { - jsonlint.parse(text); - } - catch(e) { - // Do nothing we catch the error above - } - return found; -}); From 87fb0f6a5c1a1d2d3c0a0599fdb244a3cba20d52 Mon Sep 17 00:00:00 2001 From: orangemug Date: Sun, 8 Mar 2020 18:44:04 +0000 Subject: [PATCH 37/39] Updated lockfile. --- package-lock.json | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index e70bddac..d57eb434 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3168,6 +3168,11 @@ } } }, + "code-error-fragment": { + "version": "0.0.230", + "resolved": "https://registry.npmjs.org/code-error-fragment/-/code-error-fragment-0.0.230.tgz", + "integrity": "sha512-cadkfKp6932H8UkhzE/gcUqhRMNf8jHzkAN7+5Myabswaghu4xABTgPHDCjW+dBAJxj/SpkTYokpzDqY4pCzQw==" + }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -5783,8 +5788,7 @@ "grapheme-splitter": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", - "dev": true + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" }, "grid-index": { "version": "1.1.0", @@ -7141,6 +7145,15 @@ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", "dev": true }, + "json-to-ast": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/json-to-ast/-/json-to-ast-2.1.0.tgz", + "integrity": "sha512-W9Lq347r8tA1DfMvAGn9QNcgYm4Wm7Yc+k8e6vezpMnRT+NHbtlxgNBXRVjXe9YM6eTn6+p/MKOlV/aABJcSnQ==", + "requires": { + "code-error-fragment": "0.0.230", + "grapheme-splitter": "^1.0.4" + } + }, "json3": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz", @@ -7174,11 +7187,12 @@ } }, "jsonlint": { - "version": "github:josdejong/jsonlint#85a19d77126771f3177582e3d09c6ffae185d391", - "from": "github:josdejong/jsonlint#85a19d7", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/jsonlint/-/jsonlint-1.6.3.tgz", + "integrity": "sha512-jMVTMzP+7gU/IyC6hvKyWpUU8tmTkK5b3BPNuMI9U8Sit+YAWLlZwB6Y6YrdCxfg2kNz05p3XY3Bmm4m26Nv3A==", "requires": { - "JSV": ">= 4.0.x", - "nomnom": ">= 1.5.x" + "JSV": "^4.0.x", + "nomnom": "^1.5.x" } }, "jsprim": { From 8f581956e878e267a8d636312c8c9e8e746f439a Mon Sep 17 00:00:00 2001 From: orangemug Date: Sun, 8 Mar 2020 19:00:36 +0000 Subject: [PATCH 38/39] Revert jsonlint --- package-lock.json | 9 ++++----- package.json | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index d57eb434..334dbc5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7187,12 +7187,11 @@ } }, "jsonlint": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/jsonlint/-/jsonlint-1.6.3.tgz", - "integrity": "sha512-jMVTMzP+7gU/IyC6hvKyWpUU8tmTkK5b3BPNuMI9U8Sit+YAWLlZwB6Y6YrdCxfg2kNz05p3XY3Bmm4m26Nv3A==", + "version": "github:josdejong/jsonlint#85a19d77126771f3177582e3d09c6ffae185d391", + "from": "github:josdejong/jsonlint#85a19d7", "requires": { - "JSV": "^4.0.x", - "nomnom": "^1.5.x" + "JSV": ">= 4.0.x", + "nomnom": ">= 1.5.x" } }, "jsprim": { diff --git a/package.json b/package.json index 29aaec48..ebfb4f0b 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "detect-browser": "^4.8.0", "file-saver": "^2.0.2", "json-to-ast": "^2.1.0", - "jsonlint": "^1.6.3", + "jsonlint": "github:josdejong/jsonlint#85a19d7", "lodash": "^4.17.15", "lodash.capitalize": "^4.2.1", "lodash.clamp": "^4.0.3", From f23f60807ac4832fca17e74d1c27bb1666b5bbee Mon Sep 17 00:00:00 2001 From: orangemug Date: Mon, 16 Mar 2020 10:17:04 +0000 Subject: [PATCH 39/39] Tidy --- src/components/App.jsx | 3 +- src/components/fields/_FunctionButtons.jsx | 3 +- src/components/filter/FilterEditor.jsx | 5 ++- src/styles/_components.scss | 48 ++++++++++++++++++++++ src/styles/index.scss | 46 --------------------- 5 files changed, 54 insertions(+), 51 deletions(-) diff --git a/src/components/App.jsx b/src/components/App.jsx index 94632298..839129d4 100644 --- a/src/components/App.jsx +++ b/src/components/App.jsx @@ -393,8 +393,7 @@ export default class App extends React.Component { } onUndo = () => { - let activeStyle; - activeStyle = this.revisionStore.undo() + const activeStyle = this.revisionStore.undo() const messages = undoMessages(this.state.mapStyle, activeStyle) this.onStyleChanged(activeStyle, {addRevision: false}); diff --git a/src/components/fields/_FunctionButtons.jsx b/src/components/fields/_FunctionButtons.jsx index 674f55ed..e96b1351 100644 --- a/src/components/fields/_FunctionButtons.jsx +++ b/src/components/fields/_FunctionButtons.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types' import Button from '../Button' import {MdFunctions, MdInsertChart} from 'react-icons/md' +import {mdiFunctionVariant} from '@mdi/js'; /** @@ -41,7 +42,7 @@ export default class FunctionButtons extends React.Component { onClick={this.props.onExpressionClick} > - + ); diff --git a/src/components/filter/FilterEditor.jsx b/src/components/filter/FilterEditor.jsx index 01a0d8c9..e11125b2 100644 --- a/src/components/filter/FilterEditor.jsx +++ b/src/components/filter/FilterEditor.jsx @@ -11,6 +11,7 @@ import FilterEditorBlock from './FilterEditorBlock' import Button from '../Button' import SpecDoc from '../inputs/SpecDoc' import ExpressionProperty from '../fields/_ExpressionProperty'; +import {mdiFunctionVariant} from '@mdi/js'; function combiningFilter (props) { @@ -194,7 +195,7 @@ export default class CombiningFilterEditor extends React.Component { onClick={this.makeExpression} > - + Upgrade to expression @@ -212,7 +213,7 @@ export default class CombiningFilterEditor extends React.Component { className="maputnik-make-zoom-function" > - +
diff --git a/src/styles/_components.scss b/src/styles/_components.scss index 375f55fe..eedefb50 100644 --- a/src/styles/_components.scss +++ b/src/styles/_components.scss @@ -252,3 +252,51 @@ } } } + +.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; +} + +.maputnik-expression-editor { + border: solid 1px $color-gray; +} + +.maputnik-input-block--wide { + .maputnik-input-block-content { + display: block; + width: auto; + } + + .maputnik-input-block-label { + width: 82%; + } + + .maputnik-input-block-action { + text-align: right; + } +} + +.maputnik-expr-infobox { + font-size: $font-size-6; + background: $color-midgray; + padding: $margin-2; + border-radius: 2px; + border-top-right-radius: 0px; + border-top-left-radius: 0px; + color: $color-white; +} + +.maputnik-expr-infobox__button { + background: none; + border: none; + padding: 0; + text-decoration: underline; + color: currentColor; + cursor: pointer; +} + diff --git a/src/styles/index.scss b/src/styles/index.scss index 2f027e1f..11cca10a 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -35,49 +35,3 @@ height: 14px; } -// TODO: Move these into correct *.scss files -.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; -} - -.maputnik-expression-editor { - border: solid 1px $color-gray; -} - -.maputnik-input-block--wide { - .maputnik-input-block-content { - display: block; - width: auto; - } - - .maputnik-input-block-label { - width: 82%; - } - - .maputnik-input-block-action { - text-align: right; - } -} - -.maputnik-expr-infobox { - font-size: $font-size-6; - background: $color-midgray; - padding: $margin-2; - border-radius: 2px; - border-top-right-radius: 0px; - border-top-left-radius: 0px; - color: $color-white; -} -.maputnik-expr-infobox__button { - background: none; - border: none; - padding: 0; - text-decoration: underline; - color: currentColor; - cursor: pointer; -}