Improve typings of exising components and migrated a few more

This commit is contained in:
HarelM
2023-12-22 09:37:40 +02:00
parent 1318dd279e
commit 6af3eeefb6
12 changed files with 180 additions and 187 deletions
+6 -6
View File
@@ -3,17 +3,17 @@ import InputString from './InputString'
import InputNumber from './InputNumber' import InputNumber from './InputNumber'
export type FieldArrayProps = { export type FieldArrayProps = {
value: string[] | number[] value: (string | number | undefined)[]
type?: string type?: string
length?: number length?: number
default?: string[] | number[] default?: (string | number | undefined)[]
onChange?(...args: unknown[]): unknown onChange?(value: (string | number | undefined)[] | undefined): unknown
'aria-label'?: string 'aria-label'?: string
label?: string label?: string
}; };
type FieldArrayState = { type FieldArrayState = {
value: string[] | number[] value: (string | number | undefined)[]
initialPropsValue: unknown[] initialPropsValue: unknown[]
} }
@@ -59,7 +59,7 @@ export default class FieldArray extends React.Component<FieldArrayProps, FieldAr
}); });
} }
changeValue(idx: number, newValue: string) { changeValue(idx: number, newValue: string | number | undefined) {
const value = this.state.value.slice(0); const value = this.state.value.slice(0);
value[idx] = newValue; value[idx] = newValue;
@@ -93,7 +93,7 @@ export default class FieldArray extends React.Component<FieldArrayProps, FieldAr
default={containsValues || !this.props.default ? undefined : this.props.default[i] as number} default={containsValues || !this.props.default ? undefined : this.props.default[i] as number}
value={value[i] as number} value={value[i] as number}
required={containsValues ? true : false} required={containsValues ? true : false}
onChange={this.changeValue.bind(this, i)} onChange={(v) => this.changeValue(i, v)}
aria-label={this.props['aria-label'] || this.props.label} aria-label={this.props['aria-label'] || this.props.label}
/> />
} else { } else {
+4 -4
View File
@@ -11,10 +11,10 @@ import InputUrl from './InputUrl'
export type FieldDynamicArrayProps = { export type FieldDynamicArrayProps = {
value?: (string | number)[] value?: (string | number | undefined)[]
type?: 'url' | 'number' | 'enum' | 'string' type?: 'url' | 'number' | 'enum' | 'string'
default?: (string | number)[] default?: (string | number | undefined)[]
onChange?(...args: unknown[]): unknown onChange?(values: (string | number | undefined)[] | undefined): unknown
style?: object style?: object
fieldSpec?: { fieldSpec?: {
values?: any values?: any
@@ -25,7 +25,7 @@ export type FieldDynamicArrayProps = {
export default class FieldDynamicArray extends React.Component<FieldDynamicArrayProps> { export default class FieldDynamicArray extends React.Component<FieldDynamicArrayProps> {
changeValue(idx: number, newValue: string | number) { changeValue(idx: number, newValue: string | number | undefined) {
const values = this.values.slice(0) const values = this.values.slice(0)
values[idx] = newValue values[idx] = newValue
if (this.props.onChange) this.props.onChange(values) if (this.props.onChange) this.props.onChange(values)
+1 -1
View File
@@ -7,7 +7,7 @@ export type InputNumberProps = {
default?: number default?: number
min?: number min?: number
max?: number max?: number
onChange?(...args: unknown[]): unknown onChange?(value: number | undefined): unknown
allowRange?: boolean allowRange?: boolean
rangeStep?: number rangeStep?: number
wdKey?: string wdKey?: string
+1 -1
View File
@@ -5,7 +5,7 @@ export type InputSelectProps = {
"data-wd-key"?: string "data-wd-key"?: string
options: [string, any][] | string[] options: [string, any][] | string[]
style?: object style?: object
onChange(...args: unknown[]): unknown onChange(value: string): unknown
title?: string title?: string
'aria-label'?: string 'aria-label'?: string
}; };
+9 -9
View File
@@ -14,11 +14,11 @@ import capitalize from 'lodash.capitalize'
const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image'] const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image']
export type SpecFieldProps = { export type SpecFieldProps = {
onChange?(...args: unknown[]): unknown onChange?(fieldName: string | undefined, value: number | undefined | (string | number | undefined)[]): unknown
fieldName?: string fieldName?: string
fieldSpec: { fieldSpec?: {
default?: unknown default?: unknown
type: 'number' | 'enum' | 'resolvedImage' | 'formatted' | 'string' | 'color' | 'boolean' | 'array' type?: 'number' | 'enum' | 'resolvedImage' | 'formatted' | 'string' | 'color' | 'boolean' | 'array'
minimum?: number minimum?: number
maximum?: number maximum?: number
values?: unknown[] values?: unknown[]
@@ -29,9 +29,9 @@ export type SpecFieldProps = {
/** Override the style of the field */ /** Override the style of the field */
style?: object style?: object
'aria-label'?: string 'aria-label'?: string
error: unknown[] error?: unknown[]
label: string label?: string
action: ReactElement action?: ReactElement
}; };
/** Display any field from the Maplibre GL style spec and /** Display any field from the Maplibre GL style spec and
@@ -47,12 +47,12 @@ export default class SpecField extends React.Component<SpecFieldProps> {
action: this.props.action, action: this.props.action,
style: this.props.style, style: this.props.style,
value: this.props.value, value: this.props.value,
default: this.props.fieldSpec.default, default: this.props.fieldSpec?.default,
name: this.props.fieldName, name: this.props.fieldName,
onChange: (newValue: string) => this.props.onChange!(this.props.fieldName, newValue), onChange: (newValue: number | undefined | (string | number | undefined)[]) => this.props.onChange!(this.props.fieldName, newValue),
'aria-label': this.props['aria-label'], 'aria-label': this.props['aria-label'],
} }
switch(this.props.fieldSpec.type) { switch(this.props.fieldSpec?.type) {
case 'number': return ( case 'number': return (
<InputNumber <InputNumber
{...commonProps as InputNumberProps} {...commonProps as InputNumberProps}
+2 -2
View File
@@ -5,8 +5,8 @@ export type InputStringProps = {
value?: string value?: string
style?: object style?: object
default?: string default?: string
onChange?(...args: unknown[]): unknown onChange?(value: string | undefined): unknown
onInput?(...args: unknown[]): unknown onInput?(value: string | undefined): unknown
multi?: boolean multi?: boolean
required?: boolean required?: boolean
disabled?: boolean disabled?: boolean
+1 -1
View File
@@ -233,7 +233,7 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
['image', 'Image'], ['image', 'Image'],
['video', 'Video'], ['video', 'Video'],
]} ]}
onChange={(mode: EditorMode) => this.setState({mode: mode, source: this.defaultSource(mode)})} onChange={mode => this.setState({mode: mode as EditorMode, source: this.defaultSource(mode as EditorMode)})}
value={this.state.mode as string} value={this.state.mode as string}
/> />
<ModalSourcesTypeEditor <ModalSourcesTypeEditor
+2 -2
View File
@@ -21,9 +21,9 @@ type SpecFieldProps = InputFieldSpecProps & {
export default class SpecField extends React.Component<SpecFieldProps> { export default class SpecField extends React.Component<SpecFieldProps> {
render() { render() {
const fieldType = this.props.fieldSpec.type; const fieldType = this.props.fieldSpec?.type;
const typeBlockFn = typeMap[fieldType]; const typeBlockFn = typeMap[fieldType!];
let TypeBlock; let TypeBlock;
if (typeBlockFn) { if (typeBlockFn) {
@@ -1,5 +1,4 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types'
import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js'; import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js';
import {latest} from '@maplibre/maplibre-gl-style-spec' import {latest} from '@maplibre/maplibre-gl-style-spec'
@@ -8,7 +7,6 @@ import InputSpec from './InputSpec'
import InputNumber from './InputNumber' import InputNumber from './InputNumber'
import InputString from './InputString' import InputString from './InputString'
import InputSelect from './InputSelect' import InputSelect from './InputSelect'
import FieldDocLabel from './FieldDocLabel'
import Block from './Block' import Block from './Block'
import docUid from '../libs/document-uid' import docUid from '../libs/document-uid'
import sortNumerically from '../libs/sort-numerically' import sortNumerically from '../libs/sort-numerically'
@@ -19,12 +17,12 @@ import DeleteStopButton from './_DeleteStopButton'
function setStopRefs(props, state) { function setStopRefs(props: DataPropertyProps, state: DataPropertyState) {
// This is initialsed below only if required to improved performance. // This is initialsed below only if required to improved performance.
let newRefs; let newRefs: {[key: number]: string} | undefined;
if(props.value && props.value.stops) { if(props.value && props.value.stops) {
props.value.stops.forEach((val, idx) => { props.value.stops.forEach((_val, idx) => {
if(!state.refs.hasOwnProperty(idx)) { if(!state.refs.hasOwnProperty(idx)) {
if(!newRefs) { if(!newRefs) {
newRefs = {...state}; newRefs = {...state};
@@ -37,27 +35,39 @@ function setStopRefs(props, state) {
return newRefs; return newRefs;
} }
export default class DataProperty extends React.Component { type DataPropertyProps = {
static propTypes = { onChange?(fieldName: string, value: any): unknown
onChange: PropTypes.func, onDeleteStop?(...args: unknown[]): unknown
onDeleteStop: PropTypes.func, onAddStop?(...args: unknown[]): unknown
onAddStop: PropTypes.func, onExpressionClick?(...args: unknown[]): unknown
onExpressionClick: PropTypes.func, onChangeToZoomFunction?(...args: unknown[]): unknown
fieldName: PropTypes.string, fieldName: string
fieldType: PropTypes.string, fieldType?: string
fieldSpec: PropTypes.object, fieldSpec?: object
value: PropTypes.oneOfType([ value?: DataPropertyValue
PropTypes.object, errors?: object
PropTypes.string, };
PropTypes.number,
PropTypes.bool,
PropTypes.array
]),
errors: PropTypes.object,
}
type DataPropertyState = {
refs: {[key: number]: string}
};
type DataPropertyValue = {
default?: any
property?: string
base?: number
type?: string
stops: Stops
}
type Stops = [{
zoom: number
value: number
}, number][]
export default class DataProperty extends React.Component<DataPropertyProps, DataPropertyState> {
state = { state = {
refs: {} refs: {} as {[key: number]: string}
} }
componentDidMount() { componentDidMount() {
@@ -70,7 +80,7 @@ export default class DataProperty extends React.Component {
} }
} }
static getDerivedStateFromProps(props, state) { static getDerivedStateFromProps(props: DataPropertyProps, state: DataPropertyState) {
const newRefs = setStopRefs(props, state); const newRefs = setStopRefs(props, state);
if(newRefs) { if(newRefs) {
return { return {
@@ -80,7 +90,7 @@ export default class DataProperty extends React.Component {
return null; return null;
} }
getFieldFunctionType(fieldSpec) { getFieldFunctionType(fieldSpec: any) {
if (fieldSpec.expression.interpolated) { if (fieldSpec.expression.interpolated) {
return "exponential" return "exponential"
} }
@@ -90,7 +100,7 @@ export default class DataProperty extends React.Component {
return "categorical" return "categorical"
} }
getDataFunctionTypes(fieldSpec) { getDataFunctionTypes(fieldSpec: any) {
if (fieldSpec.expression.interpolated) { if (fieldSpec.expression.interpolated) {
return ["interpolate", "categorical", "interval", "exponential", "identity"] return ["interpolate", "categorical", "interval", "exponential", "identity"]
} }
@@ -100,7 +110,7 @@ export default class DataProperty extends React.Component {
} }
// Order the stops altering the refs to reflect their new position. // Order the stops altering the refs to reflect their new position.
orderStopsByZoom(stops) { orderStopsByZoom(stops: Stops) {
const mappedWithRef = stops const mappedWithRef = stops
.map((stop, idx) => { .map((stop, idx) => {
return { return {
@@ -112,7 +122,7 @@ export default class DataProperty extends React.Component {
.sort((a, b) => sortNumerically(a.data[0].zoom, b.data[0].zoom)); .sort((a, b) => sortNumerically(a.data[0].zoom, b.data[0].zoom));
// Fetch the new position of the stops // Fetch the new position of the stops
const newRefs = {}; const newRefs = {} as {[key: number]: string};
mappedWithRef mappedWithRef
.forEach((stop, idx) =>{ .forEach((stop, idx) =>{
newRefs[idx] = stop.ref; newRefs[idx] = stop.ref;
@@ -125,7 +135,7 @@ export default class DataProperty extends React.Component {
return mappedWithRef.map((item) => item.data); return mappedWithRef.map((item) => item.data);
} }
onChange = (fieldName, value) => { onChange = (fieldName: string, value: any) => {
if (value.type === "identity") { if (value.type === "identity") {
value = { value = {
type: value.type, type: value.type,
@@ -139,21 +149,21 @@ export default class DataProperty extends React.Component {
type: value.type, type: value.type,
// Default props if they don't already exist. // Default props if they don't already exist.
stops: [ stops: [
[{zoom: 6, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)], [{zoom: 6, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec as any)],
[{zoom: 10, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)] [{zoom: 10, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec as any)]
], ],
...value, ...value,
} }
} }
this.props.onChange(fieldName, value); this.props.onChange!(fieldName, value);
} }
changeStop(changeIdx, stopData, value) { changeStop(changeIdx: number, stopData: { zoom: number | undefined, value: number }, value: number) {
const stops = this.props.value.stops.slice(0) const stops = this.props.value?.stops.slice(0) || []
// const changedStop = stopData.zoom === undefined ? stopData.value : stopData // const changedStop = stopData.zoom === undefined ? stopData.value : stopData
stops[changeIdx] = [ stops[changeIdx] = [
{ {
...stopData, value: stopData.value,
zoom: (stopData.zoom === undefined) ? 0 : stopData.zoom, zoom: (stopData.zoom === undefined) ? 0 : stopData.zoom,
}, },
value value
@@ -168,7 +178,7 @@ export default class DataProperty extends React.Component {
this.onChange(this.props.fieldName, changedValue) this.onChange(this.props.fieldName, changedValue)
} }
changeBase(newValue) { changeBase(newValue: number | undefined) {
const changedValue = { const changedValue = {
...this.props.value, ...this.props.value,
base: newValue base: newValue
@@ -177,11 +187,11 @@ export default class DataProperty extends React.Component {
if (changedValue.base === undefined) { if (changedValue.base === undefined) {
delete changedValue["base"]; delete changedValue["base"];
} }
this.props.onChange(this.props.fieldName, changedValue) this.props.onChange!(this.props.fieldName, changedValue)
} }
changeDataType(propVal) { changeDataType(propVal: string) {
if (propVal === "interpolate") { if (propVal === "interpolate" && this.props.onChangeToZoomFunction) {
this.props.onChangeToZoomFunction(); this.props.onChangeToZoomFunction();
} }
else { else {
@@ -192,41 +202,39 @@ export default class DataProperty extends React.Component {
} }
} }
changeDataProperty(propName, propVal) { changeDataProperty(propName: "property" | "default", propVal: any) {
if (propVal) { if (propVal) {
this.props.value[propName] = propVal this.props.value![propName] = propVal
} }
else { else {
delete this.props.value[propName] delete this.props.value![propName]
} }
this.onChange(this.props.fieldName, this.props.value) this.onChange(this.props.fieldName, this.props.value)
} }
render() { render() {
const {fieldName, fieldType, errors} = this.props; if (typeof this.props.value?.type === "undefined") {
this.props.value!.type = this.getFieldFunctionType(this.props.fieldSpec)
if (typeof this.props.value.type === "undefined") {
this.props.value.type = this.getFieldFunctionType(this.props.fieldSpec)
} }
let dataFields; let dataFields;
if (this.props.value.stops) { if (this.props.value?.stops) {
dataFields = this.props.value.stops.map((stop, idx) => { dataFields = this.props.value.stops.map((stop, idx) => {
const zoomLevel = typeof stop[0] === 'object' ? stop[0].zoom : undefined; const zoomLevel = typeof stop[0] === 'object' ? stop[0].zoom : undefined;
const key = this.state.refs[idx]; const key = this.state.refs[idx];
const dataLevel = typeof stop[0] === 'object' ? stop[0].value : stop[0]; const dataLevel = typeof stop[0] === 'object' ? stop[0].value : stop[0];
const value = stop[1] const value = stop[1]
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} /> const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop?.bind(this, idx)} />
const dataProps = { const dataProps = {
'aria-label': "Input value", 'aria-label': "Input value",
label: "Data value", label: "Data value",
value: dataLevel, value: dataLevel as any,
onChange: newData => this.changeStop(idx, { zoom: zoomLevel, value: newData }, value) onChange: (newData: string | number | undefined) => this.changeStop(idx, { zoom: zoomLevel, value: newData as number }, value)
} }
let dataInput; let dataInput;
if(this.props.value.type === "categorical") { if(this.props.value?.type === "categorical") {
dataInput = <InputString {...dataProps} /> dataInput = <InputString {...dataProps} />
} }
else { else {
@@ -246,16 +254,6 @@ export default class DataProperty extends React.Component {
</div> </div>
} }
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 <tr key={key}> return <tr key={key}>
<td> <td>
{zoomInput} {zoomInput}
@@ -269,7 +267,7 @@ export default class DataProperty extends React.Component {
fieldName={this.props.fieldName} fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec} fieldSpec={this.props.fieldSpec}
value={value} value={value}
onChange={(_, newValue) => this.changeStop(idx, {zoom: zoomLevel, value: dataLevel}, newValue)} onChange={(_, newValue) => this.changeStop(idx, {zoom: zoomLevel, value: dataLevel}, newValue as number)}
/> />
</td> </td>
<td> <td>
@@ -289,14 +287,14 @@ export default class DataProperty extends React.Component {
> >
<div className="maputnik-data-spec-property-input"> <div className="maputnik-data-spec-property-input">
<InputSelect <InputSelect
value={this.props.value.type} value={this.props.value!.type}
onChange={propVal => this.changeDataType(propVal)} onChange={propVal => this.changeDataType(propVal)}
title={"Select a type of data scale (default is 'categorical')."} title={"Select a type of data scale (default is 'categorical')."}
options={this.getDataFunctionTypes(this.props.fieldSpec)} options={this.getDataFunctionTypes(this.props.fieldSpec)}
/> />
</div> </div>
</Block> </Block>
{this.props.value.type !== "identity" && {this.props.value?.type !== "identity" &&
<Block <Block
label={"Base"} label={"Base"}
key="base" key="base"
@@ -305,8 +303,8 @@ export default class DataProperty extends React.Component {
<InputSpec <InputSpec
fieldName={"base"} fieldName={"base"}
fieldSpec={latest.function.base} fieldSpec={latest.function.base}
value={this.props.value.base} value={this.props.value?.base}
onChange={(_, newValue) => this.changeBase(newValue)} onChange={(_, newValue) => this.changeBase(newValue as number)}
/> />
</div> </div>
</Block> </Block>
@@ -317,8 +315,7 @@ export default class DataProperty extends React.Component {
> >
<div className="maputnik-data-spec-property-input"> <div className="maputnik-data-spec-property-input">
<InputString <InputString
value={this.props.value.property} value={this.props.value?.property}
title={"Input a data property to base styles off of."}
onChange={propVal => this.changeDataProperty("property", propVal)} onChange={propVal => this.changeDataProperty("property", propVal)}
/> />
</div> </div>
@@ -331,7 +328,7 @@ export default class DataProperty extends React.Component {
<InputSpec <InputSpec
fieldName={this.props.fieldName} fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec} fieldSpec={this.props.fieldSpec}
value={this.props.value.default} value={this.props.value?.default}
onChange={(_, propVal) => this.changeDataProperty("default", propVal)} onChange={(_, propVal) => this.changeDataProperty("default", propVal)}
/> />
</Block> </Block>
@@ -344,7 +341,7 @@ export default class DataProperty extends React.Component {
<tr> <tr>
<th>Zoom</th> <th>Zoom</th>
<th>Input value</th> <th>Input value</th>
<th rowSpan="2">Output value</th> <th rowSpan={2}>Output value</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -357,7 +354,7 @@ export default class DataProperty extends React.Component {
{dataFields && {dataFields &&
<InputButton <InputButton
className="maputnik-add-stop" className="maputnik-add-stop"
onClick={this.props.onAddStop.bind(this)} onClick={this.props.onAddStop?.bind(this)}
> >
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24"> <svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiTableRowPlusAfter} /> <path fill="currentColor" d={mdiTableRowPlusAfter} />
@@ -366,7 +363,7 @@ export default class DataProperty extends React.Component {
} }
<InputButton <InputButton
className="maputnik-add-stop" className="maputnik-add-stop"
onClick={this.props.onExpressionClick.bind(this)} onClick={this.props.onExpressionClick?.bind(this)}
> >
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24"> <svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiFunctionVariant} /> <path fill="currentColor" d={mdiFunctionVariant} />
@@ -1,45 +1,46 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import {MdDelete, MdUndo} from 'react-icons/md'
import stringifyPretty from 'json-stringify-pretty-compact'
import Block from './Block' import Block from './Block'
import InputButton from './InputButton' import InputButton from './InputButton'
import {MdDelete, MdUndo} from 'react-icons/md'
import FieldString from './FieldString'
import labelFromFieldName from './_labelFromFieldName' import labelFromFieldName from './_labelFromFieldName'
import stringifyPretty from 'json-stringify-pretty-compact'
import FieldJson from './FieldJson' import FieldJson from './FieldJson'
export default class ExpressionProperty extends React.Component { type ExpressionPropertyProps = {
static propTypes = { onDelete?(...args: unknown[]): unknown
onDelete: PropTypes.func, fieldName: string
fieldName: PropTypes.string, fieldType?: string
fieldType: PropTypes.string, fieldSpec?: object
fieldSpec: PropTypes.object, value?: any
value: PropTypes.any, errors?: {[key: string]: any}
errors: PropTypes.object, onChange?(...args: unknown[]): unknown
onChange: PropTypes.func, onUndo?(...args: unknown[]): unknown
onUndo: PropTypes.func, canUndo?(...args: unknown[]): unknown
canUndo: PropTypes.func, onFocus?(...args: unknown[]): unknown
onFocus: PropTypes.func, onBlur?(...args: unknown[]): unknown
onBlur: PropTypes.func, };
}
type ExpressionPropertyState = {
jsonError: boolean
};
export default class ExpressionProperty extends React.Component<ExpressionPropertyProps, ExpressionPropertyState> {
static defaultProps = { static defaultProps = {
errors: {}, errors: {},
onFocus: () => {}, onFocus: () => {},
onBlur: () => {}, onBlur: () => {},
} }
constructor (props) { constructor (props:ExpressionPropertyProps) {
super(); super(props);
this.state = { this.state = {
jsonError: false, jsonError: false,
}; };
} }
onJSONInvalid = (err) => { onJSONInvalid = (_err: Error) => {
this.setState({ this.setState({
jsonError: true, jsonError: true,
}) })
@@ -82,11 +83,11 @@ export default class ExpressionProperty extends React.Component {
const fieldKey = fieldType === undefined ? fieldName : `${fieldType}.${fieldName}`; const fieldKey = fieldType === undefined ? fieldName : `${fieldType}.${fieldName}`;
const fieldError = errors[fieldKey]; const fieldError = errors![fieldKey];
const errorKeyStart = `${fieldKey}[`; const errorKeyStart = `${fieldKey}[`;
const foundErrors = []; const foundErrors = [];
function getValue (data) { function getValue(data: any) {
return stringifyPretty(data, {indent: 2, maxLength: 38}) return stringifyPretty(data, {indent: 2, maxLength: 38})
} }
@@ -94,11 +95,11 @@ export default class ExpressionProperty extends React.Component {
foundErrors.push({message: "Invalid JSON"}); foundErrors.push({message: "Invalid JSON"});
} }
else { else {
Object.entries(errors) Object.entries(errors!)
.filter(([key, error]) => { .filter(([key, _error]) => {
return key.startsWith(errorKeyStart); return key.startsWith(errorKeyStart);
}) })
.forEach(([key, error]) => { .forEach(([_key, error]) => {
return foundErrors.push(error); return foundErrors.push(error);
}) })
@@ -1,5 +1,4 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types'
import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js'; import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js';
import {latest} from '@maplibre/maplibre-gl-style-spec' import {latest} from '@maplibre/maplibre-gl-style-spec'
@@ -7,7 +6,6 @@ import InputButton from './InputButton'
import InputSpec from './InputSpec' import InputSpec from './InputSpec'
import InputNumber from './InputNumber' import InputNumber from './InputNumber'
import InputSelect from './InputSelect' import InputSelect from './InputSelect'
import FieldDocLabel from './FieldDocLabel'
import Block from './Block' import Block from './Block'
import DeleteStopButton from './_DeleteStopButton' import DeleteStopButton from './_DeleteStopButton'
@@ -22,12 +20,12 @@ import sortNumerically from '../libs/sort-numerically'
* *
* When the stops are reordered the references are also updated (see this.orderStops) this allows React to use the same key for the element and keep keyboard focus. * When the stops are reordered the references are also updated (see this.orderStops) this allows React to use the same key for the element and keep keyboard focus.
*/ */
function setStopRefs(props, state) { function setStopRefs(props: ZoomPropertyProps, state: ZoomPropertyState) {
// This is initialsed below only if required to improved performance. // This is initialsed below only if required to improved performance.
let newRefs; let newRefs: {[key: number]: string} = {};
if(props.value && props.value.stops) { if(props.value && (props.value as ZoomWithStops).stops) {
props.value.stops.forEach((val, idx) => { (props.value as ZoomWithStops).stops.forEach((_val, idx: number) => {
if(!state.refs.hasOwnProperty(idx)) { if(!state.refs.hasOwnProperty(idx)) {
if(!newRefs) { if(!newRefs) {
newRefs = {...state}; newRefs = {...state};
@@ -40,32 +38,39 @@ function setStopRefs(props, state) {
return newRefs; return newRefs;
} }
type ZoomWithStops = {
stops: [number | undefined, number][]
base?: number
}
export default class ZoomProperty extends React.Component {
static propTypes = { type ZoomPropertyProps = {
onChange: PropTypes.func, onChange?(...args: unknown[]): unknown
onDeleteStop: PropTypes.func, onChangeToDataFunction?(...args: unknown[]): unknown
onAddStop: PropTypes.func, onDeleteStop?(...args: unknown[]): unknown
onExpressionClick: PropTypes.func, onAddStop?(...args: unknown[]): unknown
fieldType: PropTypes.string, onExpressionClick?(...args: unknown[]): unknown
fieldName: PropTypes.string, fieldType?: string
fieldSpec: PropTypes.object, fieldName: string
errors: PropTypes.object, fieldSpec?: {
value: PropTypes.oneOfType([ "property-type"?: string
PropTypes.object, "function-type"?: string
PropTypes.string,
PropTypes.number,
PropTypes.bool,
PropTypes.array
]),
} }
errors?: object
value?: ZoomWithStops
};
type ZoomPropertyState = {
refs: {[key: number]: string}
}
export default class ZoomProperty extends React.Component<ZoomPropertyProps, ZoomPropertyState> {
static defaultProps = { static defaultProps = {
errors: {}, errors: {},
} }
state = { state = {
refs: {} refs: {} as {[key: number]: string}
} }
componentDidMount() { componentDidMount() {
@@ -78,7 +83,7 @@ export default class ZoomProperty extends React.Component {
} }
} }
static getDerivedStateFromProps(props, state) { static getDerivedStateFromProps(props: ZoomPropertyProps, state: ZoomPropertyState) {
const newRefs = setStopRefs(props, state); const newRefs = setStopRefs(props, state);
if(newRefs) { if(newRefs) {
return { return {
@@ -89,7 +94,7 @@ export default class ZoomProperty extends React.Component {
} }
// Order the stops altering the refs to reflect their new position. // Order the stops altering the refs to reflect their new position.
orderStopsByZoom(stops) { orderStopsByZoom(stops: ZoomWithStops["stops"]) {
const mappedWithRef = stops const mappedWithRef = stops
.map((stop, idx) => { .map((stop, idx) => {
return { return {
@@ -98,10 +103,10 @@ export default class ZoomProperty extends React.Component {
} }
}) })
// Sort by zoom // Sort by zoom
.sort((a, b) => sortNumerically(a.data[0], b.data[0])); .sort((a, b) => sortNumerically(a.data[0]!, b.data[0]!));
// Fetch the new position of the stops // Fetch the new position of the stops
const newRefs = {}; const newRefs: {[key:number]: string} = {};
mappedWithRef mappedWithRef
.forEach((stop, idx) =>{ .forEach((stop, idx) =>{
newRefs[idx] = stop.ref; newRefs[idx] = stop.ref;
@@ -114,20 +119,20 @@ export default class ZoomProperty extends React.Component {
return mappedWithRef.map((item) => item.data); return mappedWithRef.map((item) => item.data);
} }
changeZoomStop(changeIdx, stopData, value) { changeZoomStop(changeIdx: number, stopData: number | undefined, value: number) {
const stops = this.props.value.stops.slice(0); const stops = (this.props.value as ZoomWithStops).stops.slice(0);
stops[changeIdx] = [stopData, value]; stops[changeIdx] = [stopData, value];
const orderedStops = this.orderStopsByZoom(stops); const orderedStops = this.orderStopsByZoom(stops);
const changedValue = { const changedValue = {
...this.props.value, ...this.props.value as ZoomWithStops,
stops: orderedStops stops: orderedStops
} }
this.props.onChange(this.props.fieldName, changedValue) this.props.onChange!(this.props.fieldName, changedValue)
} }
changeBase(newValue) { changeBase(newValue: number | undefined) {
const changedValue = { const changedValue = {
...this.props.value, ...this.props.value,
base: newValue base: newValue
@@ -136,33 +141,21 @@ export default class ZoomProperty extends React.Component {
if (changedValue.base === undefined) { if (changedValue.base === undefined) {
delete changedValue["base"]; delete changedValue["base"];
} }
this.props.onChange(this.props.fieldName, changedValue) this.props.onChange!(this.props.fieldName, changedValue)
} }
changeDataType = (type) => { changeDataType = (type: string) => {
if (type !== "interpolate") { if (type !== "interpolate" && this.props.onChangeToDataFunction) {
this.props.onChangeToDataFunction(type); this.props.onChangeToDataFunction(type);
} }
} }
render() { render() {
const {fieldName, fieldType, errors} = this.props; const zoomFields = this.props.value?.stops.map((stop, idx) => {
const zoomFields = this.props.value.stops.map((stop, idx) => {
const zoomLevel = stop[0] const zoomLevel = stop[0]
const key = this.state.refs[idx]; const key = this.state.refs[idx];
const value = stop[1] const value = stop[1]
const deleteStopBtn= <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} /> const deleteStopBtn= <DeleteStopButton onClick={this.props.onDeleteStop?.bind(this, idx)} />
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 <tr return <tr
key={key} key={key}
@@ -180,9 +173,9 @@ export default class ZoomProperty extends React.Component {
<InputSpec <InputSpec
aria-label="Output value" aria-label="Output value"
fieldName={this.props.fieldName} fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec} fieldSpec={this.props.fieldSpec as any}
value={value} value={value}
onChange={(_, newValue) => this.changeZoomStop(idx, zoomLevel, newValue)} onChange={(_, newValue) => this.changeZoomStop(idx, zoomLevel, newValue as number)}
/> />
</td> </td>
<td> <td>
@@ -204,7 +197,7 @@ export default class ZoomProperty extends React.Component {
value={"interpolate"} value={"interpolate"}
onChange={propVal => this.changeDataType(propVal)} onChange={propVal => this.changeDataType(propVal)}
title={"Select a type of data scale (default is 'categorical')."} title={"Select a type of data scale (default is 'categorical')."}
options={this.getDataFunctionTypes(this.props.fieldSpec)} options={this.getDataFunctionTypes(this.props.fieldSpec!)}
/> />
</div> </div>
</Block> </Block>
@@ -215,8 +208,8 @@ export default class ZoomProperty extends React.Component {
<InputSpec <InputSpec
fieldName={"base"} fieldName={"base"}
fieldSpec={latest.function.base} fieldSpec={latest.function.base}
value={this.props.value.base} value={this.props.value?.base}
onChange={(_, newValue) => this.changeBase(newValue)} onChange={(_, newValue) => this.changeBase(newValue as number | undefined)}
/> />
</div> </div>
</Block> </Block>
@@ -226,7 +219,7 @@ export default class ZoomProperty extends React.Component {
<thead> <thead>
<tr> <tr>
<th>Zoom</th> <th>Zoom</th>
<th rowSpan="2">Output value</th> <th rowSpan={2}>Output value</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -237,7 +230,7 @@ export default class ZoomProperty extends React.Component {
<div className="maputnik-toolbox"> <div className="maputnik-toolbox">
<InputButton <InputButton
className="maputnik-add-stop" className="maputnik-add-stop"
onClick={this.props.onAddStop.bind(this)} onClick={this.props.onAddStop?.bind(this)}
> >
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24"> <svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiTableRowPlusAfter} /> <path fill="currentColor" d={mdiTableRowPlusAfter} />
@@ -245,7 +238,7 @@ export default class ZoomProperty extends React.Component {
</InputButton> </InputButton>
<InputButton <InputButton
className="maputnik-add-stop" className="maputnik-add-stop"
onClick={this.props.onExpressionClick.bind(this)} onClick={this.props.onExpressionClick?.bind(this)}
> >
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24"> <svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiFunctionVariant} /> <path fill="currentColor" d={mdiFunctionVariant} />
@@ -257,7 +250,10 @@ export default class ZoomProperty extends React.Component {
</div> </div>
} }
getDataFunctionTypes(fieldSpec) { getDataFunctionTypes(fieldSpec: {
"property-type"?: string
"function-type"?: string
}) {
if (fieldSpec['property-type'] === 'data-driven') { if (fieldSpec['property-type'] === 'data-driven') {
return ["interpolate", "categorical", "interval", "exponential", "identity"]; return ["interpolate", "categorical", "interval", "exponential", "identity"];
} }
@@ -265,5 +261,4 @@ export default class ZoomProperty extends React.Component {
return ["interpolate"]; return ["interpolate"];
} }
} }
} }
@@ -1,7 +1,7 @@
/** /**
* If we don't have a default value just make one up * If we don't have a default value just make one up
*/ */
export function findDefaultFromSpec (spec) { export function findDefaultFromSpec(spec: { type: 'string' | 'color' | 'boolean' | 'array', default?: any }) {
if (spec.hasOwnProperty('default')) { if (spec.hasOwnProperty('default')) {
return spec.default; return spec.default;
} }