mirror of
https://github.com/maputnik/editor.git
synced 2026-08-19 19:47:28 +00:00
Merge branch 'main' into codemirror-upgrade
This commit is contained in:
@@ -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
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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" | "",
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+1239
-1032
File diff suppressed because it is too large
Load Diff
+4
-5
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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]) => {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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,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> {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>;
|
||||
|
||||
Vendored
+16
@@ -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}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user