Files
editor/src/components/modals/ModalAdd.tsx
T
Harel M 69143ea5d6 Add global state modal (#1377)
## Launch Checklist

This adds the ability to edit the global state.
I think it deserves a modal of its own since I don't think it should be
part of other modals...
Here are some images:
<img width="1274" height="254" alt="image"
src="https://github.com/user-attachments/assets/4b6f2564-6c71-47da-9f8c-3bd2b97e1163"
/>

Initial dialog with no variable:
<img width="640" height="254" alt="image"
src="https://github.com/user-attachments/assets/b813b540-cae9-4c80-b2c0-4d965c022cb8"
/>
After you click add a few times:
<img width="640" height="254" alt="image"
src="https://github.com/user-attachments/assets/125cb978-90dc-4047-9694-b0ffc6eaa469"
/>

The state is updated as you change thing in the dialog.
I didn't complicated it to select the type of the variable, but this can
be added later of if there's a requirement to do so, I meant to keep it
simple for now.

 - [x] Briefly describe the changes in this PR.
- [x] Include before/after visuals or gifs if this PR includes visual
changes.
 - [x] Write tests for all new functionality.
 - [x] Add an entry to `CHANGELOG.md` under the `## main` section.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-09-14 11:13:52 +03:00

197 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from "react";
import { type WithTranslation, withTranslation } from "react-i18next";
import type {LayerSpecification, SourceSpecification} from "maplibre-gl";
import InputButton from "../InputButton";
import Modal from "./Modal";
import FieldType from "../FieldType";
import FieldId from "../FieldId";
import FieldSource from "../FieldSource";
import FieldSourceLayer from "../FieldSourceLayer";
import { NON_SOURCE_LAYERS } from "../../libs/non-source-layers";
type ModalAddInternalProps = {
layers: LayerSpecification[]
onLayersChange(layers: LayerSpecification[]): unknown
isOpen: boolean
onOpenToggle(): void
// A dict of source id's and the available source layers
sources: Record<string, SourceSpecification & {layers: string[]}>;
} & WithTranslation;
type ModalAddState = {
type: LayerSpecification["type"]
id: string
source?: string
"source-layer"?: string
error?: string | null
};
class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddState> {
addLayer = () => {
if (this.props.layers.some(l => l.id === this.state.id)) {
this.setState({ error: this.props.t("Layer ID already exists") });
return;
}
const changedLayers = this.props.layers.slice(0);
const layer: ModalAddState = {
id: this.state.id,
type: this.state.type,
};
if(this.state.type !== "background") {
layer.source = this.state.source;
if(!NON_SOURCE_LAYERS.includes(this.state.type) && this.state["source-layer"]) {
layer["source-layer"] = this.state["source-layer"];
}
}
changedLayers.push(layer as LayerSpecification);
this.setState({ error: null }, () => {
this.props.onLayersChange(changedLayers);
this.props.onOpenToggle();
});
};
constructor(props: ModalAddInternalProps) {
super(props);
const state: ModalAddState = {
type: "fill",
id: "",
error: null,
};
if(Object.keys(props.sources).length > 0) {
state.source = Object.keys(this.props.sources)[0];
const sourceLayers = this.props.sources[state.source].layers || [];
if (sourceLayers.length > 0) {
state["source-layer"] = sourceLayers[0];
}
}
this.state = state;
}
componentDidUpdate(_prevProps: ModalAddInternalProps, prevState: ModalAddState) {
// Check if source is valid for new type
const oldType = prevState.type;
const newType = this.state.type;
const availableSourcesOld = this.getSources(oldType);
const availableSourcesNew = this.getSources(newType);
if(
// Type has changed
oldType !== newType
&& prevState.source !== ""
// Was a valid source previously
&& availableSourcesOld.indexOf(prevState.source!) > -1
// And is not a valid source now
&& availableSourcesNew.indexOf(this.state.source!) < 0
) {
// Clear the source
this.setState({
source: ""
});
}
}
getLayersForSource(source: string) {
const sourceObj = this.props.sources[source] || {};
return sourceObj.layers || [];
}
getSources(type: LayerSpecification["type"]) {
switch(type) {
case "background":
return [];
case "hillshade":
case "color-relief":
return Object.entries(this.props.sources).filter(([_, v]) => v.type === "raster-dem").map(([k, _]) => k);
case "raster":
return Object.entries(this.props.sources).filter(([_, v]) => v.type === "raster").map(([k, _]) => k);
case "heatmap":
case "circle":
case "fill":
case "fill-extrusion":
case "line":
case "symbol":
return Object.entries(this.props.sources).filter(([_, v]) => v.type === "vector" || v.type === "geojson").map(([k, _]) => k);
default:
return [];
}
}
render() {
const t = this.props.t;
const sources = this.getSources(this.state.type);
const layers = this.getLayersForSource(this.state.source!);
let errorElement;
if (this.state.error) {
errorElement = (
<div className="maputnik-modal-error">
{this.state.error}
<a
href="#"
onClick={() => this.setState({ error: null })}
className="maputnik-modal-error-close"
>
×
</a>
</div>
);
}
return <Modal
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={t("Add Layer")}
data-wd-key="modal:add-layer"
className="maputnik-add-modal"
>
{errorElement}
<div className="maputnik-add-layer">
<FieldId
value={this.state.id}
wdKey="add-layer.layer-id"
onChange={(v: string) => {
this.setState({ id: v, error: null });
}}
/>
<FieldType
value={this.state.type}
wdKey="add-layer.layer-type"
onChange={(v: LayerSpecification["type"]) => this.setState({ type: v })}
/>
{this.state.type !== "background" &&
<FieldSource
sourceIds={sources}
wdKey="add-layer.layer-source-block"
value={this.state.source}
onChange={(v: string) => this.setState({ source: v })}
/>
}
{!NON_SOURCE_LAYERS.includes(this.state.type) &&
<FieldSourceLayer
sourceLayerIds={layers}
value={this.state["source-layer"]}
onChange={(v: string) => this.setState({ "source-layer": v })}
/>
}
<InputButton
className="maputnik-add-layer-button"
onClick={this.addLayer}
data-wd-key="add-layer"
>
{t("Add Layer")}
</InputButton>
</div>
</Modal>;
}
}
const ModalAdd = withTranslation()(ModalAddInternal);
export default ModalAdd;