mirror of
https://github.com/maputnik/editor.git
synced 2026-07-11 00:17:28 +00:00
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>
This commit is contained in:
@@ -8,7 +8,7 @@ type ModalInternalProps = PropsWithChildren & {
|
||||
"data-wd-key"?: string
|
||||
isOpen: boolean
|
||||
title: string
|
||||
onOpenToggle(value: boolean): unknown
|
||||
onOpenToggle(): void
|
||||
underlayClickExits?: boolean
|
||||
className?: string
|
||||
} & WithTranslation;
|
||||
@@ -26,7 +26,7 @@ class ModalInternal extends React.Component<ModalInternalProps> {
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.props.onOpenToggle(false);
|
||||
this.props.onOpenToggle();
|
||||
}, 0);
|
||||
};
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ type ModalAddInternalProps = {
|
||||
layers: LayerSpecification[]
|
||||
onLayersChange(layers: LayerSpecification[]): unknown
|
||||
isOpen: boolean
|
||||
onOpenToggle(open: boolean): unknown
|
||||
onOpenToggle(): void
|
||||
// A dict of source id's and the available source layers
|
||||
sources: Record<string, SourceSpecification & {layers: string[]}>;
|
||||
} & WithTranslation;
|
||||
@@ -50,7 +50,7 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
|
||||
changedLayers.push(layer as LayerSpecification);
|
||||
this.setState({ error: null }, () => {
|
||||
this.props.onLayersChange(changedLayers);
|
||||
this.props.onOpenToggle(false);
|
||||
this.props.onOpenToggle();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ type ModalDebugInternalProps = {
|
||||
renderer: string
|
||||
onChangeMaplibreGlDebug(key: string, checked: boolean): unknown
|
||||
onChangeOpenlayersDebug(key: string, checked: boolean): unknown
|
||||
onOpenToggle(value: boolean): unknown
|
||||
onOpenToggle(): void
|
||||
maplibreGlDebugOptions?: object
|
||||
openlayersDebugOptions?: object
|
||||
mapView: {
|
||||
|
||||
@@ -22,7 +22,7 @@ type ModalExportInternalProps = {
|
||||
mapStyle: StyleSpecificationWithId
|
||||
onStyleChanged: OnStyleChangedCallback
|
||||
isOpen: boolean
|
||||
onOpenToggle(...args: unknown[]): unknown
|
||||
onOpenToggle(): void
|
||||
onSetFileHandle(fileHandle: FileSystemFileHandle | null): unknown
|
||||
fileHandle: FileSystemFileHandle | null
|
||||
} & WithTranslation;
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
import React from "react";
|
||||
import { withTranslation, type WithTranslation } from "react-i18next";
|
||||
import { MdDelete } from "react-icons/md";
|
||||
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
|
||||
|
||||
import Modal from "./Modal";
|
||||
import FieldString from "../FieldString";
|
||||
import InputButton from "../InputButton";
|
||||
import { PiListPlusBold } from "react-icons/pi";
|
||||
import { type StyleSpecificationWithId } from "../../libs/definitions";
|
||||
import { type SchemaSpecification } from "maplibre-gl";
|
||||
import Doc from "../Doc";
|
||||
|
||||
type ModalGlobalStateInternalProps = {
|
||||
mapStyle: StyleSpecificationWithId;
|
||||
isOpen: boolean;
|
||||
onStyleChanged(style: StyleSpecificationWithId): void;
|
||||
onOpenToggle(): void
|
||||
} & WithTranslation;
|
||||
|
||||
type GlobalStateVariable = {
|
||||
key: string;
|
||||
value: any;
|
||||
};
|
||||
|
||||
const ModalGlobalStateInternal: React.FC<ModalGlobalStateInternalProps> = (props) => {
|
||||
const getGlobalStateVariables = (): GlobalStateVariable[] => {
|
||||
const style = props.mapStyle;
|
||||
const globalState = style.state || {};
|
||||
|
||||
return Object.entries(globalState).map(([key, value]) => ({
|
||||
key,
|
||||
value: value.default
|
||||
}));
|
||||
};
|
||||
|
||||
const setGlobalStateVariables = (variables: GlobalStateVariable[]) => {
|
||||
const style = { ...props.mapStyle };
|
||||
|
||||
// Create the globalState object from the variables array
|
||||
const globalState: Record<string, SchemaSpecification> = {};
|
||||
for (const variable of variables) {
|
||||
if (variable.key.trim() !== "") {
|
||||
globalState[variable.key] = {
|
||||
default: variable.value
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
style.state = Object.keys(globalState).length > 0 ? globalState : undefined;
|
||||
|
||||
props.onStyleChanged(style);
|
||||
};
|
||||
|
||||
const onAddVariable = () => {
|
||||
const variables = getGlobalStateVariables();
|
||||
let index = 1;
|
||||
while (variables.find(v => v.key === `key${index}`)) {
|
||||
index++;
|
||||
}
|
||||
variables.push({ key: `key${index}`, value: "value" });
|
||||
setGlobalStateVariables(variables);
|
||||
};
|
||||
|
||||
const onRemoveVariable = (index: number) => {
|
||||
const variables = getGlobalStateVariables();
|
||||
variables.splice(index, 1);
|
||||
setGlobalStateVariables(variables);
|
||||
};
|
||||
|
||||
const onChangeVariableKey = (index: number, newKey: string) => {
|
||||
const variables = getGlobalStateVariables();
|
||||
variables[index].key = newKey || "";
|
||||
setGlobalStateVariables(variables);
|
||||
};
|
||||
|
||||
const onChangeVariableValue = (index: number, newValue: string) => {
|
||||
const variables = getGlobalStateVariables();
|
||||
|
||||
variables[index].value = newValue || "";
|
||||
setGlobalStateVariables(variables);
|
||||
};
|
||||
|
||||
const variables = getGlobalStateVariables();
|
||||
|
||||
const variableFields = variables.map((variable, index) => (
|
||||
<tr key={index}>
|
||||
<td>
|
||||
<FieldString
|
||||
label={props.t("Key")}
|
||||
value={variable.key}
|
||||
onChange={(value) => onChangeVariableKey(index, value || "")}
|
||||
data-wd-key={"global-state-variable-key:" + index}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<FieldString
|
||||
label={props.t("Value")}
|
||||
value={variable.value}
|
||||
onChange={(value) => onChangeVariableValue(index, value || "")}
|
||||
data-wd-key={"global-state-variable-value:" + index}
|
||||
/>
|
||||
</td>
|
||||
<td style={{ verticalAlign: "middle"}}>
|
||||
<InputButton
|
||||
onClick={() => onRemoveVariable(index)}
|
||||
title={props.t("Remove variable")}
|
||||
data-wd-key="global-state-remove-variable"
|
||||
>
|
||||
<MdDelete />
|
||||
</InputButton>
|
||||
</td>
|
||||
</tr>
|
||||
));
|
||||
|
||||
return (
|
||||
<Modal
|
||||
data-wd-key="modal:global-state"
|
||||
isOpen={props.isOpen}
|
||||
onOpenToggle={props.onOpenToggle}
|
||||
title={props.t("Global State Variables")}
|
||||
>
|
||||
|
||||
{variables.length === 0 &&
|
||||
<div>
|
||||
<p>{props.t("No global state variables defined. Add variables to create reusable values in your style.")}</p>
|
||||
<div key="doc" className="maputnik-doc-inline">
|
||||
<Doc fieldSpec={latest.$root.state} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{variables.length > 0 &&
|
||||
<table>
|
||||
<thead>
|
||||
</thead>
|
||||
<tbody>
|
||||
{variableFields}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
<div>
|
||||
<InputButton
|
||||
onClick={onAddVariable}
|
||||
data-wd-key="global-state-add-variable"
|
||||
>
|
||||
<PiListPlusBold />
|
||||
{props.t("Add Variable")}
|
||||
</InputButton>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const ModalGlobalState = withTranslation()(ModalGlobalStateInternal);
|
||||
export default ModalGlobalState;
|
||||
@@ -45,7 +45,7 @@ class PublicStyle extends React.Component<PublicStyleProps> {
|
||||
|
||||
type ModalOpenInternalProps = {
|
||||
isOpen: boolean
|
||||
onOpenToggle(...args: unknown[]): unknown
|
||||
onOpenToggle(): void
|
||||
onStyleOpen(...args: unknown[]): unknown
|
||||
fileHandle: FileSystemFileHandle | null
|
||||
} & WithTranslation;
|
||||
|
||||
@@ -19,7 +19,7 @@ type ModalSettingsInternalProps = {
|
||||
onStyleChanged: OnStyleChangedCallback
|
||||
onChangeMetadataProperty(...args: unknown[]): unknown
|
||||
isOpen: boolean
|
||||
onOpenToggle(...args: unknown[]): unknown
|
||||
onOpenToggle(): void
|
||||
} & WithTranslation;
|
||||
|
||||
class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps> {
|
||||
|
||||
@@ -6,7 +6,7 @@ import Modal from "./Modal";
|
||||
|
||||
type ModalShortcutsInternalProps = {
|
||||
isOpen: boolean
|
||||
onOpenToggle(...args: unknown[]): unknown
|
||||
onOpenToggle(): void
|
||||
} & WithTranslation;
|
||||
|
||||
|
||||
|
||||
@@ -273,7 +273,7 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
|
||||
type ModalSourcesInternalProps = {
|
||||
mapStyle: StyleSpecificationWithId
|
||||
isOpen: boolean
|
||||
onOpenToggle(...args: unknown[]): unknown
|
||||
onOpenToggle(): void
|
||||
onStyleChanged: OnStyleChangedCallback
|
||||
} & WithTranslation;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user