Merge branch 'main' into codemirror-upgrade

This commit is contained in:
HarelM
2025-09-16 21:07:10 +03:00
28 changed files with 1492 additions and 1138 deletions
+3
View File
@@ -5,6 +5,8 @@
- Add support for hillshade's color arrays and relief-color elevation expression
- Change layers icons to make them a bit more distinct
- Remove `@mdi` packages in favor of `react-icons`
- Add ability to control the projection of the map - either globe or mercator
- Add markdown support for doc related to the style-spec fields
- Added global state modal to allow editing the global state
- _...Add new stuff here..._
@@ -14,6 +16,7 @@
- Fix modal close button possition
- Fixed an issue with the generation of tranlations
- Fix missing spec info when clicking next to a property
- Fix Firefox open file that stopped working due to react upgrade
- _...Add new stuff here..._
## 3.0.0
+50 -21
View File
@@ -491,6 +491,20 @@ describe("layers", () => {
when.click("field-doc-button-Offset", 0);
then(get.elementByTestId("spec-field-doc")).shouldContainText("Offset distance");
});
it("should hide spec info when clicking a second time", () => {
when.modal.fillLayers({
type: "symbol",
layer: "example",
});
when.hover("spec-field-container:text-rotate");
then(get.elementByTestId("field-doc-button-Rotate")).shouldBeVisible();
when.click("field-doc-button-Rotate", 0);
when.wait(200);
when.click("field-doc-button-Rotate", 0);
then(get.elementByTestId("spec-field-doc")).shouldNotBeVisible();
});
});
describe("raster", () => {
@@ -684,33 +698,48 @@ describe("layers", () => {
});
});
describe("layereditor jsonlint should error", ()=>{
it("add", () => {
const id = when.modal.fillLayers({
type: "circle",
layer: "example",
describe("layers editor", () => {
describe("property fields", () => {
it("should show error", () => {
when.modal.fillLayers({
type: "circle",
layer: "invalid",
});
then(get.element(".maputnik-input-block--error .maputnik-input-block-label")).shouldHaveCss("color", "rgb(207, 74, 74)");
});
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "circle",
source: "example",
},
],
describe("jsonlint should error", ()=>{
it("add", () => {
const id = when.modal.fillLayers({
type: "circle",
layer: "example",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "circle",
source: "example",
},
],
});
const sourceText = get.elementByText('"source"');
sourceText.click();
sourceText.type("\"");
const error = get.element(".CodeMirror-lint-marker-error");
error.should("exist");
});
const sourceText = get.elementByText('"source"');
sourceText.click();
sourceText.type("\"");
const error = get.element(".CodeMirror-lint-marker-error");
error.should("exist");
});
});
describe("drag and drop", () => {
it("move layer should update local storage", () => {
when.modal.open();
+16
View File
@@ -26,6 +26,22 @@ export default class MaputnikCypressHelper {
this.helper.when.wait(200);
this.helper.get.elementByTestId(element).realMouseUp();
},
openFileByFixture: (fixture: string, buttonTestId: string, inputTestId: string) => {
cy.window().then((win) => {
const file = {
text: cy.stub().resolves(cy.fixture(fixture).then(JSON.stringify)),
};
const fileHandle = {
getFile: cy.stub().resolves(file),
};
if (!win.showOpenFilePicker) {
this.helper.get.elementByTestId(inputTestId).selectFile("cypress/fixtures/" + fixture, { force: true });
} else {
cy.stub(win, "showOpenFilePicker").resolves([fileHandle]);
this.helper.get.elementByTestId(buttonTestId).click();
}
});
},
...this.helper.when,
};
+3 -3
View File
@@ -109,9 +109,9 @@ export class MaputnikDriver {
this.helper.when.waitForResponse("example-style.json");
},
chooseExampleFile: () => {
this.helper.get
.bySelector("type", "file")
.selectFile("cypress/fixtures/example-style.json", { force: true });
this.helper.given.fixture("example-style.json", "example-style.json");
this.helper.when.openFileByFixture("example-style.json", "modal:open.file.button", "modal:open.file.input");
this.helper.when.wait(200);
},
setStyle: (
styleProperties: "geojson" | "raster" | "both" | "layer" | "rectangles" | "",
+45 -3
View File
@@ -18,10 +18,9 @@ describe("modals", () => {
then(get.elementByTestId("modal:open")).shouldNotExist();
});
it.skip("upload", () => {
// HM: I was not able to make the following choose file actually to select a file and close the modal...
it("upload", () => {
when.chooseExampleFile();
then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
then(get.fixture("example-style.json")).shouldEqualToStoredStyle();
});
describe("when click open url", () => {
@@ -236,6 +235,29 @@ describe("modals", () => {
).shouldInclude({ "maputnik:locationiq_access_token": apiKey });
});
it("style projection mercator", () => {
when.select("modal:settings.projection", "mercator");
then(
get.styleFromLocalStorage().then((style) => style.projection)
).shouldInclude({ type: "mercator" });
});
it("style projection globe", () => {
when.select("modal:settings.projection", "globe");
then(
get.styleFromLocalStorage().then((style) => style.projection)
).shouldInclude({ type: "globe" });
});
it("style projection vertical-perspective", () => {
when.select("modal:settings.projection", "vertical-perspective");
then(
get.styleFromLocalStorage().then((style) => style.projection)
).shouldInclude({ type: "vertical-perspective" });
});
it("style renderer", () => {
cy.on("uncaught:exception", () => false); // this is due to the fact that this is an invalid style for openlayers
when.select("modal:settings.maputnik:renderer", "ol");
@@ -311,6 +333,7 @@ describe("modals", () => {
it("add variable", () => {
when.click("global-state-add-variable");
when.wait(100);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
state: { key1: { default: "value" } },
});
@@ -321,6 +344,7 @@ describe("modals", () => {
when.click("global-state-add-variable");
when.click("global-state-add-variable");
when.click("global-state-add-variable");
when.wait(100);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
state: { key1: { default: "value" }, key2: { default: "value" }, key3: { default: "value" } },
});
@@ -331,6 +355,7 @@ describe("modals", () => {
when.click("global-state-add-variable");
when.click("global-state-add-variable");
when.click("global-state-remove-variable", 0);
when.wait(100);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
state: { key2: { default: "value" }, key3: { default: "value" } },
});
@@ -340,6 +365,7 @@ describe("modals", () => {
when.click("global-state-add-variable");
when.setValue("global-state-variable-key:0", "mykey");
when.typeKeys("{enter}");
when.wait(100);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
state: { mykey: { default: "value" } },
});
@@ -349,12 +375,28 @@ describe("modals", () => {
when.click("global-state-add-variable");
when.setValue("global-state-variable-value:0", "myvalue");
when.typeKeys("{enter}");
when.wait(100);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
state: { key1: { default: "myvalue" } },
});
});
});
describe("error panel", () => {
it("not visible when no errors", () => {
then(get.element("maputnik-message-panel-error")).shouldNotExist();
});
it("visible on style error", () => {
when.modal.open();
when.modal.fillLayers({
type: "circle",
layer: "invalid",
});
then(get.element(".maputnik-message-panel-error")).shouldBeVisible();
});
});
describe("Handle localStorage QuotaExceededError", () => {
it("handles quota exceeded error when opening style from URL", () => {
// Clear localStorage to start fresh
+1239 -1032
View File
File diff suppressed because it is too large Load Diff
+4 -5
View File
@@ -37,7 +37,7 @@
"buffer": "^6.0.3",
"classnames": "^2.5.1",
"codemirror": "^6.0.2",
"color": "^5.0.0",
"color": "^5.0.2",
"detect-browser": "^5.3.0",
"downshift": "^9.0.10",
"events": "^3.3.0",
@@ -67,9 +67,9 @@
"react-collapse": "^5.1.1",
"react-color": "^2.19.3",
"react-dom": "^19.1.1",
"react-file-reader-input": "^2.0.0",
"react-i18next": "^15.7.3",
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"reconnecting-websocket": "^4.4.0",
"slugify": "^1.6.6",
"string-hash": "^1.1.3",
@@ -112,13 +112,12 @@
"@types/lodash.isequal": "^4.5.8",
"@types/lodash.throttle": "^4.1.9",
"@types/randomcolor": "^0.5.9",
"@types/react": "^19.1.12",
"@types/react": "^19.1.13",
"@types/react-aria-menubutton": "^6.2.14",
"@types/react-aria-modal": "^5.0.0",
"@types/react-collapse": "^5.0.4",
"@types/react-color": "^3.0.13",
"@types/react-dom": "^19.1.9",
"@types/react-file-reader-input": "^2.0.4",
"@types/string-hash": "^1.1.3",
"@types/uuid": "^10.0.0",
"@types/wicg-file-system-access": "^2023.10.6",
@@ -140,7 +139,7 @@
"stylelint-config-recommended-scss": "^16.0.1",
"stylelint-scss": "^6.12.1",
"typescript": "^5.9.2",
"typescript-eslint": "^8.43.0",
"typescript-eslint": "^8.44.0",
"uuid": "^13.0.0",
"vite": "^7.1.5",
"vite-plugin-istanbul": "^7.1.0"
+3 -15
View File
@@ -36,7 +36,7 @@ import LayerWatcher from "../libs/layerwatcher";
import tokens from "../config/tokens.json";
import isEqual from "lodash.isequal";
import { type MapOptions } from "maplibre-gl";
import { type OnStyleChangedOpts, type StyleSpecificationWithId } from "../libs/definitions";
import { type MappedError, type OnStyleChangedOpts, type StyleSpecificationWithId } from "../libs/definitions";
// Buffer must be defined globally for @maplibre/maplibre-gl-style-spec validate() function to succeed.
window.Buffer = buffer.Buffer;
@@ -82,20 +82,8 @@ function updateRootSpec(spec: any, fieldName: string, newValues: any) {
};
}
type MappedErrors = {
message: string
parsed?: {
type: string
data: {
index: number
key: string
message: string
}
}
};
type AppState = {
errors: MappedErrors[],
errors: MappedError[],
infos: string[],
mapStyle: StyleSpecificationWithId,
dirtyMapStyle?: StyleSpecification,
@@ -383,7 +371,7 @@ export default class App extends React.Component<any, AppState> {
});
}
const mappedErrors = layerErrors.concat(errors).map(error => {
const mappedErrors: MappedError[] = layerErrors.concat(errors).map(error => {
// Special case: Duplicate layer id
const dupMatch = error.message.match(/layers\[(\d+)\]: (duplicate layer id "?(.*)"?, previously used)/);
if (dupMatch) {
+3 -2
View File
@@ -2,9 +2,10 @@ import React from "react";
import {formatLayerId} from "../libs/format";
import {type LayerSpecification, type StyleSpecification} from "maplibre-gl";
import { Trans, type WithTranslation, withTranslation } from "react-i18next";
import { type MappedError } from "../libs/definitions";
type AppMessagePanelInternalProps = {
errors?: unknown[]
errors?: MappedError[]
infos?: string[]
mapStyle?: StyleSpecification
onLayerSelect?(index: number): void;
@@ -19,7 +20,7 @@ class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalPro
render() {
const {t, selectedLayerIndex} = this.props;
const errors = this.props.errors?.map((error: any, idx) => {
const errors = this.props.errors?.map((error, idx) => {
let content;
if (error.parsed && error.parsed.type === "layer") {
const {parsed} = error;
+5 -4
View File
@@ -1,13 +1,13 @@
import React, {type PropsWithChildren, type SyntheticEvent} from "react";
import React, {type CSSProperties, type PropsWithChildren, type SyntheticEvent} from "react";
import classnames from "classnames";
import FieldDocLabel from "./FieldDocLabel";
import Doc from "./Doc";
type BlockProps = PropsWithChildren & {
export type BlockProps = PropsWithChildren & {
"data-wd-key"?: string
label?: string
action?: React.ReactElement
style?: object
style?: CSSProperties
onChange?(...args: unknown[]): unknown
fieldSpec?: object
wideMode?: boolean
@@ -65,7 +65,8 @@ export default class Block extends React.Component<BlockProps, BlockState> {
className={classnames({
"maputnik-input-block": true,
"maputnik-input-block--wide": this.props.wideMode,
"maputnik-action-block": this.props.action
"maputnik-action-block": this.props.action,
"maputnik-input-block--error": this.props.error
})}
onClick={this.onLabelClick}
>
+6 -1
View File
@@ -1,4 +1,5 @@
import React from "react";
import Markdown from "react-markdown";
const headers = {
js: "JS",
@@ -48,7 +49,11 @@ export default class Doc extends React.Component<DocProps> {
<>
{doc &&
<div className="SpecDoc">
<div className="SpecDoc__doc" data-wd-key='spec-field-doc'>{doc}</div>
<div className="SpecDoc__doc" data-wd-key='spec-field-doc'>
<Markdown components={{
a: ({node: _node, href, children, ...props}) => <a href={href} target="_blank" {...props}>{children}</a>,
}}>{doc}</Markdown>
</div>
{renderValues &&
<ul className="SpecDoc__values">
{Object.entries(values).map(([key, value]) => {
+2 -1
View File
@@ -6,6 +6,7 @@ import ZoomProperty from "./_ZoomProperty";
import ExpressionProperty from "./_ExpressionProperty";
import {function as styleFunction} from "@maplibre/maplibre-gl-style-spec";
import {findDefaultFromSpec} from "../libs/spec-helper";
import { type MappedLayerErrors } from "../libs/definitions";
function isLiteralExpression(value: any) {
@@ -119,7 +120,7 @@ type FieldFunctionProps = {
fieldName: string
fieldType: string
fieldSpec: any
errors?: {[key: string]: {message: string}}
errors?: MappedLayerErrors
value?: any
};
+4 -6
View File
@@ -1,6 +1,6 @@
import Block from "./Block";
import Block, { type BlockProps } from "./Block";
import InputSpec, { type FieldSpecType, type InputSpecProps } from "./InputSpec";
import Fieldset from "./Fieldset";
import Fieldset, { type FieldsetProps } from "./Fieldset";
function getElementFromType(fieldSpec: { type?: FieldSpecType, values?: unknown[] }): typeof Fieldset | typeof Block {
switch(fieldSpec.type) {
@@ -34,15 +34,13 @@ function getElementFromType(fieldSpec: { type?: FieldSpecType, values?: unknown[
}
}
export type FieldSpecProps = InputSpecProps & {
name?: string
};
export type FieldSpecProps = InputSpecProps & BlockProps & FieldsetProps;
const FieldSpec: React.FC<FieldSpecProps> = (props) => {
const TypeBlock = getElementFromType(props.fieldSpec!);
return (
<TypeBlock label={props.label} action={props.action} fieldSpec={props.fieldSpec}>
<TypeBlock label={props.label} action={props.action} fieldSpec={props.fieldSpec} error={props.error}>
<InputSpec {...props} />
</TypeBlock>
);
+7 -2
View File
@@ -1,12 +1,14 @@
import React, { type PropsWithChildren, type ReactElement } from "react";
import classnames from "classnames";
import FieldDocLabel from "./FieldDocLabel";
import Doc from "./Doc";
import generateUniqueId from "../libs/document-uid";
type FieldsetProps = PropsWithChildren & {
export type FieldsetProps = PropsWithChildren & {
label?: string,
fieldSpec?: { doc?: string },
action?: ReactElement,
error?: {message: string}
};
@@ -30,7 +32,10 @@ const Fieldset: React.FC<FieldsetProps> = (props) => {
</div>
)}
{!props.fieldSpec && (
<div className="maputnik-input-block-label">
<div className={classnames({
"maputnik-input-block-label": true,
"maputnik-input-block--error": props.error
})}>
{props.label}
</div>
)}
+2 -2
View File
@@ -14,7 +14,7 @@ import InputButton from "./InputButton";
import Doc from "./Doc";
import ExpressionProperty from "./_ExpressionProperty";
import { type WithTranslation, withTranslation } from "react-i18next";
import type { StyleSpecificationWithId } from "../libs/definitions";
import type { MappedLayerErrors, StyleSpecificationWithId } from "../libs/definitions";
function combiningFilter(props: FilterEditorInternalProps): LegacyFilterSpecification | ExpressionSpecification {
@@ -95,7 +95,7 @@ type FilterEditorInternalProps = {
/** Properties of the vector layer and the available fields */
properties?: {[key:string]: any}
filter?: any[]
errors?: {[key:string]: any}
errors?: MappedLayerErrors
onChange(value: LegacyFilterSpecification | ExpressionSpecification): unknown
} & WithTranslation;
-2
View File
@@ -31,7 +31,6 @@ export type InputSpecProps = {
/** Override the style of the field */
style?: object
"aria-label"?: string
error?: unknown[]
label?: string
action?: ReactElement
};
@@ -43,7 +42,6 @@ export default class InputSpec extends React.Component<InputSpecProps> {
childNodes() {
const commonProps = {
error: this.props.error,
fieldSpec: this.props.fieldSpec,
label: this.props.label,
action: this.props.action,
+3 -3
View File
@@ -22,7 +22,7 @@ import {formatLayerId} from "../libs/format";
import { type WithTranslation, withTranslation } from "react-i18next";
import { type TFunction } from "i18next";
import { NON_SOURCE_LAYERS } from "../libs/non-source-layers";
import { type OnMoveLayerCallback } from "../libs/definitions";
import { type MappedError, type MappedLayerErrors, type OnMoveLayerCallback } from "../libs/definitions";
type MaputnikLayoutGroup = {
id: string;
@@ -128,7 +128,7 @@ type LayerEditorInternalProps = {
isFirstLayer?: boolean
isLastLayer?: boolean
layerIndex: number
errors?: any[]
errors?: MappedError[]
} & WithTranslation;
type LayerEditorState = {
@@ -193,7 +193,7 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
}
const {errors, layerIndex} = this.props;
const errorData: {[key in LayerSpecification as string]: {message: string}} = {};
const errorData: MappedLayerErrors = {};
errors!.forEach(error => {
if (
error.parsed &&
+2 -2
View File
@@ -22,7 +22,7 @@ import type {LayerSpecification, SourceSpecification} from "maplibre-gl";
import generateUniqueId from "../libs/document-uid";
import { findClosestCommonPrefix, layerPrefix } from "../libs/layer";
import { type WithTranslation, withTranslation } from "react-i18next";
import { type OnMoveLayerCallback } from "../libs/definitions";
import { type MappedError, type OnMoveLayerCallback } from "../libs/definitions";
type LayerListContainerProps = {
layers: LayerSpecification[]
@@ -33,7 +33,7 @@ type LayerListContainerProps = {
onLayerCopy(...args: unknown[]): unknown
onLayerVisibilityToggle(...args: unknown[]): unknown
sources: Record<string, SourceSpecification & {layers: string[]}>;
errors: any[]
errors: MappedError[]
};
type LayerListContainerInternalProps = LayerListContainerProps & WithTranslation;
+2 -1
View File
@@ -2,6 +2,7 @@ import React from "react";
import FieldFunction from "./FieldFunction";
import type {LayerSpecification} from "maplibre-gl";
import { type MappedLayerErrors } from "../libs/definitions";
const iconProperties = ["background-pattern", "fill-pattern", "line-pattern", "fill-extrusion-pattern", "icon-image"];
@@ -36,7 +37,7 @@ type PropertyGroupProps = {
groupFields: string[]
onChange(...args: unknown[]): unknown
spec: any
errors?: {[key: string]: {message: string}}
errors?: MappedLayerErrors
};
export default class PropertyGroup extends React.Component<PropertyGroupProps> {
+2 -1
View File
@@ -16,6 +16,7 @@ import { type WithTranslation, withTranslation } from "react-i18next";
import labelFromFieldName from "../libs/label-from-field-name";
import DeleteStopButton from "./_DeleteStopButton";
import { type MappedLayerErrors } from "../libs/definitions";
@@ -47,7 +48,7 @@ type DataPropertyInternalProps = {
fieldType?: string
fieldSpec?: object
value?: DataPropertyValue
errors?: object
errors?: MappedLayerErrors
} & WithTranslation;
type DataPropertyState = {
+2 -1
View File
@@ -7,13 +7,14 @@ import InputButton from "./InputButton";
import labelFromFieldName from "../libs/label-from-field-name";
import FieldJson from "./FieldJson";
import type { StylePropertySpecification } from "maplibre-gl";
import { type MappedLayerErrors } from "../libs/definitions";
type ExpressionPropertyInternalProps = {
fieldName: string
fieldSpec?: StylePropertySpecification
value?: any
errors?: {[key: string]: {message: string}}
errors?: MappedLayerErrors
onDelete?(...args: unknown[]): unknown
onChange(value: object): void
onUndo?(...args: unknown[]): unknown
+2 -1
View File
@@ -15,6 +15,7 @@ import labelFromFieldName from "../libs/label-from-field-name";
import docUid from "../libs/document-uid";
import sortNumerically from "../libs/sort-numerically";
import { type MappedLayerErrors } from "../libs/definitions";
/**
@@ -59,7 +60,7 @@ type ZoomPropertyInternalProps = {
"property-type"?: string
"function-type"?: string
}
errors?: object
errors?: MappedLayerErrors
value?: ZoomWithStops
} & WithTranslation;
+9 -6
View File
@@ -1,7 +1,6 @@
import React, { type FormEvent } from "react";
import {MdFileUpload} from "react-icons/md";
import {MdAddCircleOutline} from "react-icons/md";
import FileReaderInput, { type Result } from "react-file-reader-input";
import { Trans, type WithTranslation, withTranslation } from "react-i18next";
import ModalLoading from "./ModalLoading";
@@ -171,8 +170,10 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
// it is not guaranteed that the File System Access API is available on all
// browsers. If the function is not available, a fallback behavior is used.
onFileChanged = async (_: any, files: Result[]) => {
const [, file] = files[0];
onFileChanged = (files: FileList | null) => {
if (!files) return;
if (files.length === 0) return;
const file = files[0];
const reader = new FileReader();
this.clearError();
@@ -246,13 +247,15 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
<div>
{typeof window.showOpenFilePicker === "function" ? (
<InputButton
data-wd-key="modal:open.file.button"
className="maputnik-big-button"
onClick={this.onOpenFile}><MdFileUpload/> {t("Open Style")}
</InputButton>
) : (
<FileReaderInput onChange={this.onFileChanged} tabIndex={-1} aria-label={t("Open Style")}>
<InputButton className="maputnik-upload-button"><MdFileUpload /> {t("Open Style")}</InputButton>
</FileReaderInput>
<label>
<a className="maputnik-button maputnik-upload-button" aria-label={t("Open Style")}><MdFileUpload /> {t("Open Style")}</a>
<input data-wd-key="modal:open.file.input" type="file" style={{ display: "none" }} onChange={(e) => this.onFileChanged(e.target.files)} />
</label>
)}
</div>
</section>
+54 -22
View File
@@ -1,6 +1,6 @@
import React from "react";
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import type {LightSpecification, StyleSpecification, TerrainSpecification, TransitionSpecification} from "maplibre-gl";
import type {LightSpecification, ProjectionSpecification, StyleSpecification, TerrainSpecification, TransitionSpecification} from "maplibre-gl";
import { type WithTranslation, withTranslation } from "react-i18next";
import FieldArray from "../FieldArray";
@@ -79,6 +79,24 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
});
}
changeProjectionType(value: any) {
const projection = {
...this.props.mapStyle.projection,
} as ProjectionSpecification;
if (value === undefined) {
delete projection.type;
}
else {
projection.type = value;
}
this.props.onStyleChanged({
...this.props.mapStyle,
projection,
});
}
changeStyleProperty(property: keyof StyleSpecification | "owner", value: any) {
const changedStyle = {
...this.props.mapStyle,
@@ -103,6 +121,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
const light = this.props.mapStyle.light || {};
const transition = this.props.mapStyle.transition || {};
const terrain = this.props.mapStyle.terrain || {} as TerrainSpecification;
const projection = this.props.mapStyle.projection || {} as ProjectionSpecification;
return <Modal
data-wd-key="modal:settings"
@@ -116,21 +135,21 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.$root.name}
data-wd-key="modal:settings.name"
value={this.props.mapStyle.name}
onChange={this.changeStyleProperty.bind(this, "name")}
onChange={(value) => this.changeStyleProperty("name", value)}
/>
<FieldString
label={t("Owner")}
fieldSpec={{doc: t("Owner ID of the style. Used by Mapbox or future style APIs.")}}
data-wd-key="modal:settings.owner"
value={(this.props.mapStyle as any).owner}
onChange={this.changeStyleProperty.bind(this, "owner")}
onChange={(value) => this.changeStyleProperty("owner", value)}
/>
<FieldUrl
fieldSpec={latest.$root.sprite}
label={t("Sprite URL")}
data-wd-key="modal:settings.sprite"
value={this.props.mapStyle.sprite as string}
onChange={this.changeStyleProperty.bind(this, "sprite")}
onChange={(value) => this.changeStyleProperty("sprite", value)}
/>
<FieldUrl
@@ -138,7 +157,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.$root.glyphs}
data-wd-key="modal:settings.glyphs"
value={this.props.mapStyle.glyphs as string}
onChange={this.changeStyleProperty.bind(this, "glyphs")}
onChange={(value) => this.changeStyleProperty("glyphs", value)}
/>
<FieldString
@@ -146,7 +165,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={fsa.maputnik.maptiler_access_token}
data-wd-key="modal:settings.maputnik:openmaptiles_access_token"
value={metadata["maputnik:openmaptiles_access_token"]}
onChange={onChangeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
onChange={(value) => onChangeMetadataProperty("maputnik:openmaptiles_access_token", value)}
/>
<FieldString
@@ -154,7 +173,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={fsa.maputnik.thunderforest_access_token}
data-wd-key="modal:settings.maputnik:thunderforest_access_token"
value={metadata["maputnik:thunderforest_access_token"]}
onChange={onChangeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
onChange={(value) => onChangeMetadataProperty("maputnik:thunderforest_access_token", value)}
/>
<FieldString
@@ -162,7 +181,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={fsa.maputnik.stadia_access_token}
data-wd-key="modal:settings.maputnik:stadia_access_token"
value={metadata["maputnik:stadia_access_token"]}
onChange={onChangeMetadataProperty.bind(this, "maputnik:stadia_access_token")}
onChange={(value) => onChangeMetadataProperty("maputnik:stadia_access_token", value)}
/>
<FieldString
@@ -170,7 +189,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={fsa.maputnik.locationiq_access_token}
data-wd-key="modal:settings.maputnik:locationiq_access_token"
value={metadata["maputnik:locationiq_access_token"]}
onChange={onChangeMetadataProperty.bind(this, "maputnik:locationiq_access_token")}
onChange={(value) => onChangeMetadataProperty("maputnik:locationiq_access_token", value)}
/>
<FieldArray
@@ -180,7 +199,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
type="number"
value={mapStyle.center || []}
default={[0, 0]}
onChange={this.changeStyleProperty.bind(this, "center")}
onChange={(value) => this.changeStyleProperty("center", value)}
/>
<FieldNumber
@@ -188,7 +207,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.$root.zoom}
value={mapStyle.zoom}
default={0}
onChange={this.changeStyleProperty.bind(this, "zoom")}
onChange={(value) => this.changeStyleProperty("zoom", value)}
/>
<FieldNumber
@@ -196,7 +215,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.$root.bearing}
value={mapStyle.bearing}
default={latest.$root.bearing.default}
onChange={this.changeStyleProperty.bind(this, "bearing")}
onChange={(value) => this.changeStyleProperty("bearing", value)}
/>
<FieldNumber
@@ -204,7 +223,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.$root.pitch}
value={mapStyle.pitch}
default={latest.$root.pitch.default}
onChange={this.changeStyleProperty.bind(this, "pitch")}
onChange={(value) => this.changeStyleProperty("pitch", value)}
/>
<FieldEnum
@@ -214,7 +233,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
value={light.anchor as string}
options={Object.keys(latest.light.anchor.values)}
default={latest.light.anchor.default}
onChange={this.changeLightProperty.bind(this, "anchor")}
onChange={(value) => this.changeLightProperty("anchor", value)}
/>
<FieldColor
@@ -222,7 +241,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.light.color}
value={light.color as string}
default={latest.light.color.default}
onChange={this.changeLightProperty.bind(this, "color")}
onChange={(value) => this.changeLightProperty("color", value)}
/>
<FieldNumber
@@ -230,7 +249,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.light.intensity}
value={light.intensity as number}
default={latest.light.intensity.default}
onChange={this.changeLightProperty.bind(this, "intensity")}
onChange={(value) => this.changeLightProperty("intensity", value)}
/>
<FieldArray
@@ -240,7 +259,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
length={latest.light.position.length}
value={light.position as number[]}
default={latest.light.position.default}
onChange={this.changeLightProperty.bind(this, "position")}
onChange={(value) => this.changeLightProperty("position", value)}
/>
<FieldString
@@ -248,7 +267,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.terrain.source}
data-wd-key="modal:settings.maputnik:terrain_source"
value={terrain.source}
onChange={this.changeTerrainProperty.bind(this, "source")}
onChange={(value) => this.changeTerrainProperty("source", value)}
/>
<FieldNumber
@@ -256,7 +275,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.terrain.exaggeration}
value={terrain.exaggeration}
default={latest.terrain.exaggeration.default}
onChange={this.changeTerrainProperty.bind(this, "exaggeration")}
onChange={(value) => this.changeTerrainProperty("exaggeration", value)}
/>
<FieldNumber
@@ -264,7 +283,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.transition.delay}
value={transition.delay}
default={latest.transition.delay.default}
onChange={this.changeTransitionProperty.bind(this, "delay")}
onChange={(value) => this.changeTransitionProperty("delay", value)}
/>
<FieldNumber
@@ -272,7 +291,20 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.transition.duration}
value={transition.duration}
default={latest.transition.duration.default}
onChange={this.changeTransitionProperty.bind(this, "duration")}
onChange={(value) => this.changeTransitionProperty("duration", value)}
/>
<FieldSelect
label={t("Projection")}
data-wd-key="modal:settings.projection"
options={[
["", "Undefined"],
["mercator", "Mercator"],
["globe", "Globe"],
["vertical-perspective", "Vertical Perspective"]
]}
value={projection?.type?.toString() || ""}
onChange={(value) => this.changeProjectionType(value)}
/>
<FieldSelect
@@ -284,7 +316,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
["ol", t("Open Layers (experimental)")],
]}
value={metadata["maputnik:renderer"] || "mlgljs"}
onChange={onChangeMetadataProperty.bind(this, "maputnik:renderer")}
onChange={(value) => onChangeMetadataProperty("maputnik:renderer", value)}
/>
</div>
</Modal>;
+16
View File
@@ -16,3 +16,19 @@ export interface IStyleStore {
getLatestStyle(): Promise<StyleSpecificationWithId>;
save(mapStyle: StyleSpecificationWithId): StyleSpecificationWithId;
}
export type MappedError = {
message: string
parsed?: {
type: "layer"
data: {
index: number
key: string
message: string
}
}
};
export type MappedLayerErrors = {
[key in LayerSpecification as string]: {message: string}
};
+6
View File
@@ -290,6 +290,12 @@
}
}
.maputnik-input-block--error {
.maputnik-input-block-label {
color: vars.$color-red;
}
}
.maputnik-expr-infobox {
font-size: vars.$font-size-6;
background: vars.$color-midgray;
+1 -1
View File
@@ -252,7 +252,7 @@
margin-top: vars.$margin-3;
}
.SpecDoc__values code {
.SpecDoc__values code, .SpecDoc__doc code {
background: vars.$color-midgray;
padding: 0.1em 0.3em;
border-radius: 2px;
+1 -1
View File
@@ -44,7 +44,7 @@
position: fixed;
bottom: 0;
right: 0;
z-index: 1;
z-index: 10;
width: vars.$layout-map-width;
background-color: vars.$color-black;
}