diff --git a/CHANGELOG.md b/CHANGELOG.md index f2a3232a..70826293 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ - _...Add new stuff here..._ ### 🐞 Bug fixes +- Prevent a crash when converting a function to an expression +- Prevent a crash when deleting either stop from a function with two stops - Preserve expanded layer groups when deleting layers, including the first layer of a group - The map's data listener now fires on tile loads again, so source and vector layer field autocompletion is populated - The `maputnik` desktop binary now opens the default browser automatically on startup (opt out with `--no-browser`) diff --git a/e2e/layer-editor.spec.ts b/e2e/layer-editor.spec.ts index 4c2c49ce..8b35ef9d 100644 --- a/e2e/layer-editor.spec.ts +++ b/e2e/layer-editor.spec.ts @@ -289,6 +289,20 @@ describe("layer editor", () => { }); }); + test("should leave a plain editable value after deleting a stop", async () => { + await when.setFunctionStopValue("circle-radius", "Output value", 1, "0"); + await when.deleteFunctionStop("circle-radius"); + await then(get.elementByTestId("spec-field:circle-radius")).shouldBeVisible(); + await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ + layers: [{ id, paint: { "circle-radius": 0 } }], + }); + + await when.setValue("spec-field-input:circle-radius", "7"); + await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ + layers: [{ id, paint: { "circle-radius": 7 } }], + }); + }); + test("should set the base", async () => { await when.setFunctionBase("circle-radius", "2"); await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ @@ -310,8 +324,9 @@ describe("layer editor", () => { }); }); - test("should convert to an expression", async () => { + test("should convert to an expression without crashing", async () => { await when.makeExpression("circle-radius"); + await then(get.element("[data-wd-key='spec-field-container:circle-radius'] .maputnik-expression-editor")).shouldBeVisible(); await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [{ id, paint: { "circle-radius": ["interpolate", ["linear"], ["zoom"], 6, 5, 10, 5] } }], }); diff --git a/src/components/FieldFunction.tsx b/src/components/FieldFunction.tsx index eb995477..3a761520 100644 --- a/src/components/FieldFunction.tsx +++ b/src/components/FieldFunction.tsx @@ -129,16 +129,10 @@ type FieldFunctionProps = { * https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property */ export const FieldFunction: React.FC = (props) => { - const [dataType, setDataType] = React.useState( - getDataType(props.value, props.fieldSpec) - ); const [isEditing, setIsEditing] = React.useState(false); - - React.useEffect(() => { - if (!isEditing) { - setDataType(getDataType(props.value, props.fieldSpec)); - } - }, [props.value, props.fieldSpec, isEditing]); + // Keep the expression editor mounted while typing, but otherwise select the + // editor from the current value so a collapsed function never renders as stops. + const dataType = isEditing ? "expression" : getDataType(props.value, props.fieldSpec); const getFieldFunctionType = (fieldSpec: any) => { if (fieldSpec.expression.interpolated) { @@ -173,7 +167,7 @@ export const FieldFunction: React.FC = (props) => { const deleteExpression = () => { const { fieldSpec, fieldName } = props; props.onChange(fieldName, fieldSpec.default); - setDataType("value"); + setIsEditing(false); }; const deleteStop = (stopIdx: number) => { @@ -233,10 +227,10 @@ export const FieldFunction: React.FC = (props) => { type: "identity", property: value[1], }); - setDataType("value"); + setIsEditing(false); } else if (isLiteralExpression(value)) { props.onChange(fieldName, value[1]); - setDataType("value"); + setIsEditing(false); } };