Compare commits

..

42 Commits

Author SHA1 Message Date
HarelM 1a5c879c75 Improve tests. 2026-07-11 15:33:31 +03:00
HarelM b16776fed3 Merge branch 'main' into increase-coverage 2026-07-10 21:40:37 +03:00
HarelM d039dc73a5 Increase coverage to about 80% 2026-07-10 21:11:41 +03:00
HarelM 70a2db580f replace for ;; with while true. 2026-07-09 16:31:27 +03:00
HarelM b76d9cbe29 Remove circular dependency. 2026-07-09 15:38:41 +03:00
HarelM dd04c14d96 Remove maputnik assertable 2026-07-09 15:28:36 +03:00
HarelM 6c31add041 Reduce export surface 2026-07-09 15:22:41 +03:00
HarelM 60ed80651c Improve abstraction 2026-07-09 15:05:18 +03:00
HarelM c642ec5325 Remove comments 2026-07-09 15:05:18 +03:00
pre-commit-ci[bot] 0c98378243 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-09 11:44:08 +00:00
HarelM 95ba3c4428 Remove cypress related comments. 2026-07-09 14:43:58 +03:00
HarelM b112c6a2bf Split dirver and helper 2026-07-09 14:40:06 +03:00
HarelM a251faf8f7 improve abstraction 2026-07-09 14:27:33 +03:00
HarelM 8580499ec3 Fix incorrect quotes 2026-07-09 14:19:47 +03:00
HarelM 56a9840def Remove unused page 2026-07-09 14:19:23 +03:00
HarelM 3d1919738b Improve test readability 2026-07-09 14:16:12 +03:00
HarelM cc317c20a1 Move fill local storage to driver 2026-07-09 14:11:49 +03:00
HarelM a9c3da0f40 Remove hack from modal driver 2026-07-09 13:59:55 +03:00
HarelM dc5d6dd77d Remove unneeded comments. 2026-07-09 13:50:16 +03:00
HarelM 95d87ad7ef Remove export for a method that isn't used outside the file. 2026-07-09 13:41:51 +03:00
HarelM 09efbb5cd6 Fix utils path 2026-07-09 13:40:45 +03:00
HarelM 4f046cb83e Fix build 2026-07-09 13:38:54 +03:00
HarelM fb00dea285 Move playwrite stuff to utils folder 2026-07-09 13:33:29 +03:00
HarelM c00395ee11 Remove coverage file and have everything inside the fixtures.ts file. 2026-07-09 13:29:42 +03:00
pre-commit-ci[bot] 5092a924aa [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-09 10:27:14 +00:00
HarelM 7d80dccb81 Remote test related entries. 2026-07-09 13:26:24 +03:00
HarelM a93fe4d78b Fix test for regexp 2026-07-09 12:35:29 +03:00
Harel M 603a30cba8 Potential fix for pull request finding 'CodeQL / Incomplete regular expression for hostnames'
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-07-09 12:19:45 +03:00
HarelM 6f55fda7d4 Remove docker coverage upload. 2026-07-09 11:47:40 +03:00
HarelM 8f0ee898ff Merge branch 'main' into replace-cypress-with-playwright 2026-07-09 11:43:28 +03:00
HarelM 3896c4c187 Merge branch 'relocate-e2e-tests' into replace-cypress-with-playwright 2026-07-08 22:44:19 +03:00
HarelM c0f1a72d80 Fix failing unit tests in CI 2026-07-08 22:43:34 +03:00
HarelM ac9186e7f8 test: instantiate MaputnikDriver once per describe block
Make the driver page-lazy (it resolves the running test's page on demand via
an auto fixture) so it can be created a single time at describe scope and
reused across the block's tests, matching the pre-migration ergonomics:

  const { given, get, when, then } = new MaputnikDriver();

instead of pulling `driver` out of a fixture and destructuring it in every
test. Coverage collection and dialog handling move into the auto fixture.

Also inject a bare invalid token ("zzz") in the json-editor parse-error test:
CodeMirror auto-closes brackets/quotes, so " {" no longer reliably breaks the
JSON.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 17:48:44 +03:00
Harel M 8c86607100 Add alias for 'it' as 'test' in layers list spec 2026-07-08 15:35:34 +03:00
HarelM 7c5e5358cb Merge branch 'relocate-e2e-tests' into replace-cypress-with-playwright 2026-07-08 15:17:09 +03:00
HarelM 18b34d3ecd Fix lint 2026-07-08 15:16:28 +03:00
HarelM 08ebca266c Merge branch 'relocate-e2e-tests' into replace-cypress-with-playwright 2026-07-08 15:15:39 +03:00
HarelM 8344a30f5d Move things back to where this will still work. 2026-07-08 15:05:35 +03:00
Harel M 4249e9beb9 Merge branch 'main' into relocate-e2e-tests 2026-07-08 14:32:25 +03:00
HarelM c3dd253737 test: migrate e2e to Playwright and component test to Vitest browser mode
Replace Cypress with Playwright for the end-to-end suite and drop
@shellygo/cypress-test-utils in favour of a hand-written MaputnikDriver
page object that keeps the fluent then(...).shouldX() assertion style
(now async). The InputAutocomplete component test moves to Vitest browser
mode using the Playwright provider.

- e2e/maputnik-driver.ts: driver + MaputnikAssertable over Playwright
- e2e/{fixtures,coverage,global-setup,global-teardown}.ts: test fixture,
  istanbul coverage collection, and nyc report generation
- playwright.config.ts / vitest.config.ts
- Code coverage preserved: dev server is istanbul-instrumented, per-test
  window.__coverage__ is merged via nyc into coverage/coverage-final.json
- CI: Cypress jobs replaced with Playwright; docker e2e runs against the
  container via E2E_NO_WEBSERVER
- Remove Cypress deps, config and support files; update docs and .nycrc

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 12:42:32 +03:00
HarelM f7b48139a4 test: adopt Playwright test()/test.describe() naming in e2e specs
Mechanical rename of it() -> test() and describe() -> test.describe() so the
subsequent Playwright migration diff only touches test bodies, not the
suite/case wrappers.
2026-07-08 12:42:13 +03:00
HarelM 8af1cfd5f8 test: relocate Cypress e2e suite to e2e/ ahead of Playwright migration
Pure file moves (no content changes) so git records them as renames and
history/blame is preserved through the migration that follows:
  cypress/e2e/*.cy.ts        -> e2e/*.spec.ts
  cypress/e2e/*-driver.ts    -> e2e/*-driver.ts
  cypress/fixtures/          -> e2e/fixtures/
  cypress.config.ts          -> playwright.config.ts
  InputAutocomplete.cy.tsx   -> InputAutocomplete.browser.test.tsx

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 12:24:03 +03:00
119 changed files with 978 additions and 1455 deletions
+11 -11
View File
@@ -21,9 +21,9 @@ jobs:
os: [ ubuntu-latest, windows-latest, macos-latest ]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: { persist-credentials: false }
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: '.nvmrc'
- run: npm ci
@@ -41,9 +41,9 @@ jobs:
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: { persist-credentials: false }
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: '.nvmrc'
- run: npm ci
@@ -56,7 +56,7 @@ jobs:
# Build and upload desktop CLI artifacts
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version: ^1.23.x
cache-dependency-path: desktop/go.sum
@@ -90,9 +90,9 @@ jobs:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: { persist-credentials: false }
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: '.nvmrc'
- run: npm ci
@@ -111,9 +111,9 @@ jobs:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: { persist-credentials: false }
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: '.nvmrc'
- run: npm ci
@@ -139,9 +139,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: { persist-credentials: false }
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: '.nvmrc'
- run: npm ci
+3 -3
View File
@@ -38,11 +38,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -67,4 +67,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@e0647621c2984b5ed2f768cb892365bf2a616ad1 # v4.37.2
uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
+2 -2
View File
@@ -16,13 +16,13 @@ jobs:
run:
shell: bash
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
ref: main
- name: Use Node.js from nvmrc
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: ".nvmrc"
+3 -3
View File
@@ -12,11 +12,11 @@ jobs:
contents: write
if: ${{ github.event_name == 'push' }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: { persist-credentials: false }
- name: Use Node.js from nvmrc
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: '.nvmrc'
@@ -50,6 +50,6 @@ jobs:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- run: docker build -t ghcr.io/maplibre/maputnik:main .
- run: docker push ghcr.io/maplibre/maputnik:main
+5 -5
View File
@@ -15,14 +15,14 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
ref: main
persist-credentials: false
- name: Use Node.js from nvmrc
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: ".nvmrc"
@@ -42,19 +42,19 @@ jobs:
run:
shell: bash
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
ref: main
- name: Use Node.js from nvmrc
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: ".nvmrc"
registry-url: "https://registry.npmjs.org"
- name: Set up Go for desktop build
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version: ^1.23.x
cache-dependency-path: desktop/go.sum
+1 -104
View File
@@ -36,7 +36,7 @@ Then run the end-to-end tests (Playwright starts the dev server automatically):
npm run test
```
Run the unit tests with Vitest:
Run the unit and component tests with Vitest:
```
npm run test-unit
@@ -45,106 +45,3 @@ npm run test-unit
## Pull Requests
- Pull requests should update `CHANGELOG.md` with a short description of the change.
## Testing
### Prefer end-to-end tests
Most of this codebase is React components, and they are only reachable from an
end-to-end test. E2E coverage is the primary signal.
Reach for a unit test only for pure logic that e2e cannot cheaply reach (parsers,
sorting, watchers, stores). Before writing one, check whether e2e already covers
the file — a unit test that duplicates existing e2e coverage adds test code and
almost no coverage:
```
npx nyc report --reporter=text --include="src/libs/style.ts"
```
Do **not** merge the Vitest (v8) and e2e (istanbul) coverage reports locally. They
produce conflicting statement maps for the same files and the combined percentage
is meaningless. Codecov merges the two uploads server-side; that is the number to
trust. Locally, read them separately:
- e2e: `npx playwright test` then `npx nyc report --reporter=text-summary` (reads `.nyc_output/`)
- unit: `npx vitest run --coverage` (writes `coverage/`)
### E2E layering
Three layers, and the boundaries matter:
- `e2e/playwright-helper.ts` — generic, app-agnostic browser actions. **The only
file allowed to import `@playwright/test`** (besides `e2e/utils/fixtures.ts`).
- `e2e/maputnik-driver.ts` — domain actions (layers, filters, functions, the
style). Knows nothing about `page` or Playwright.
- `e2e/modal-driver.ts` — actions scoped to a modal, exposed as `when.modal.*`.
Specs get a driver at describe scope and assert fluently:
```ts
describe("layer editor", () => {
const { given, get, when, then } = new MaputnikDriver();
...
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [{ id, type: "fill" }] });
});
```
New UI interactions belong in a driver, not inline in a spec.
### Writing assertions
- `shouldDeepNestedInclude` is a recursive partial match (`toMatchObject`): nested
objects are matched as subsets, arrays and primitives must match exactly
(including array length).
- Assert against the whole style, not an extracted slice. Avoid
`get.styleFromLocalStorage().then(style => style.layers.find(...))` — it moves
test logic into the test. Compare the real object instead.
- `Query.then()` is lazy and returns a new `Query`, **not** a Promise. `await
get.styleFromLocalStorage()` hangs forever. Use `.get()` to await it directly,
or pass the Query to `then(...)`.
### One behaviour per test
If a test needs comments narrating "and now this…", it is several tests. Split it,
and hoist the shared setup into a nested `describe` + `beforeEach`.
### Test ids
Test ids use the `data-wd-key` attribute and are read via `get.elementByTestId`.
The `Input*` components already accept `data-wd-key` and render it on the real
`<input>`; the `Field*` wrappers forward it through their `{...props}` spread. So
passing `data-wd-key` to a `Field*` component is usually enough. Do **not** also
add it to `Block`/`Fieldset` — the id then matches two elements and locators fail
in strict mode.
Note `InputNumber` renders `<key>-text` and `<key>-range` when `allowRange` is set,
and `<key>` otherwise.
### Input commit semantics (common source of "the value didn't save")
- `InputString` only fires its `onChange` on **blur** or **Enter**. Typing alone
fires `onInput`. A driver that calls `fill()` must then call `blur()`, or the
value never reaches the style.
- `InputNumber` commits on every change; no blur needed.
- The autocomplete inputs (layer source, add-layer source) are controlled
downshift comboboxes. Keystroke typing is dropped/reordered — `{selectall}` then
typing `raster` yields `"exampleaster"`. Use `fill()`, which dispatches a single
input event, then pick from the filtered menu.
- CodeMirror auto-closes brackets and quotes, and types over its own closers, so
inserting a well-formed JSON fragment stays well-formed. To break JSON on
purpose, insert a bare word.
### Fixtures
Style fixtures live in `e2e/fixtures/`. A new one must be registered in two places
in `maputnik-driver.ts`: the list in `given.setupMockBackedResponses` and the
`styleFileByKey` map in `when.setStyle`.
### Verify a new test can fail
A test that passes for the wrong reason is worse than no test. After writing one,
mutate the expected value and confirm it fails. This has caught real mistakes
(e.g. a driver that never committed its input, so the assertion was matching a
value written by the *previous* step).
-60
View File
@@ -1,60 +0,0 @@
import { test, expect, describe, beforeEach } from "./utils/fixtures";
import { MaputnikDriver } from "./maputnik-driver";
import tokens from "../src/config/tokens.json" with { type: "json" };
describe("access tokens", () => {
const { given, when } = new MaputnikDriver();
const tileJson = {
tilejson: "2.2.0",
tiles: ["https://example.local/{z}/{x}/{y}.pbf"],
minzoom: 0,
maxzoom: 14,
};
beforeEach(async () => {
await given.setupMockBackedResponses();
});
test("uses the thunderforest token for a thunderforest source", async () => {
await given.interceptAndMockResponse({
method: "GET",
url: /tile\.thunderforest\.com\/.*/,
response: tileJson,
alias: "thunderforest",
});
await when.setStyle("access_tokens");
const request = await when.waitForResponse("thunderforest");
expect(request.url()).toContain(`apikey=${tokens.thunderforest}`);
});
test("uses the locationiq token for a locationiq source", async () => {
await given.interceptAndMockResponse({
method: "GET",
url: /tiles\.locationiq\.com\/.*/,
response: tileJson,
alias: "locationiq",
});
await when.setStyle("access_tokens");
const request = await when.waitForResponse("locationiq");
expect(request.url()).toContain(`key=${tokens.locationiq}`);
});
test("appends the stadia token as a query parameter", async () => {
await given.interceptAndMockResponse({
method: "GET",
url: /tiles\.stadiamaps\.com\/.*/,
response: tileJson,
alias: "stadia",
});
await when.setStyle("access_tokens");
const request = await when.waitForResponse("stadia");
expect(request.url()).toContain("?api_key=stadia-test-token");
});
});
-22
View File
@@ -1,22 +0,0 @@
{
"version": 8,
"name": "Access token style",
"metadata": {
"maputnik:stadia_access_token": "stadia-test-token"
},
"sources": {
"thunderforest_transport": {
"type": "vector",
"url": "https://tile.thunderforest.com/thunderforest.transport-v2.json?apikey={key}"
},
"stadia_outdoors": {
"type": "vector",
"url": "https://tiles.stadiamaps.com/data/openmaptiles.json"
},
"locationiq": {
"type": "vector",
"url": "https://tiles.locationiq.com/v3/pbf/tiles.json?key={key}"
}
},
"layers": []
}
+72 -317
View File
@@ -25,18 +25,7 @@ describe("layer editor", () => {
return id;
}
test("expand/collapse", async () => {
const bgId = await createBackground();
await when.click("layer-list-item:background:" + bgId);
await then(get.elementByTestId("layer-editor.layer-id.input")).shouldBeVisible();
await when.toggleGroupInLayerEditor("Layer");
await then(get.elementByTestId("layer-editor.layer-id.input")).shouldNotBeVisible();
await when.toggleGroupInLayerEditor("Layer");
await then(get.elementByTestId("layer-editor.layer-id.input")).shouldBeVisible();
});
test.skip("expand/collapse", () => {});
test("id", async () => {
const bgId = await createBackground();
@@ -89,6 +78,7 @@ describe("layer editor", () => {
});
test("the range slider adjusts min-zoom", async () => {
// The slider starts at 1 (set via the text input above); one step right lands on 2.
await when.focus("min-zoom.input-range");
await when.typeKeys("{rightarrow}");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
@@ -192,285 +182,103 @@ describe("layer editor", () => {
});
describe("filter", () => {
let id: string;
beforeEach(async () => {
id = await when.modal.fillLayers({ type: "fill", layer: "example" });
test("compound filter", async () => {
const id = await when.modal.fillLayers({ type: "fill", layer: "example" });
await when.addFilter();
});
test("should add a filter item", async () => {
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, type: "fill", source: "example", filter: ["all", ["==", "name", ""]] }],
});
});
test("should change the filter operator", async () => {
// Changing the operator updates the compound filter.
await when.selectFilterOperator("!=");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, filter: ["all", ["!=", "name", ""]] }],
layers: [{ id, type: "fill", source: "example", filter: ["all", ["!=", "name", ""]] }],
});
});
test("should extend the compound filter with a second item", async () => {
// A second filter item extends the compound filter.
await when.addFilter();
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, filter: ["all", ["==", "name", ""], ["==", "name", ""]] }],
});
});
test("should change the combining operator", async () => {
await when.selectFilterCombiningOperator("any");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, filter: ["any", ["==", "name", ""]] }],
});
});
test("should delete a filter item", async () => {
await when.addFilter();
await when.deleteFilterItem();
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, filter: ["all", ["==", "name", ""]] }],
});
});
describe("when converted to an expression", () => {
beforeEach(async () => {
await when.convertFilterToExpression();
});
test("should migrate the filter to an expression", async () => {
// A single-item "all" collapses to the bare comparison when migrated.
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, filter: ["==", ["get", "name"], ""] }],
});
});
test("should restore the default filter when the expression is deleted", async () => {
await when.deleteFilterExpression();
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, filter: ["all"] }],
});
layers: [{ id, type: "fill", source: "example", filter: ["all", ["!=", "name", ""], ["==", "name", ""]] }],
});
});
});
describe("functions", () => {
let id: string;
describe("zoom function", () => {
beforeEach(async () => {
id = await when.modal.fillLayers({ type: "circle", layer: "example" });
await when.makeZoomFunction("circle-radius");
test("convert a property to a zoom function and add a stop", async () => {
const id = await when.modal.fillLayers({ type: "circle", layer: "example" });
await when.makeZoomFunction("circle-radius");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, type: "circle", source: "example", paint: { "circle-radius": { stops: [[6, 5], [10, 5]] } } }],
});
test("should convert the property to a zoom function", async () => {
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{ id, type: "circle", source: "example", paint: { "circle-radius": { stops: [[6, 5], [10, 5]] } } },
],
});
await when.addFunctionStop("circle-radius");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, type: "circle", source: "example", paint: { "circle-radius": { stops: [[6, 5], [10, 5], [11, 5]] } } }],
});
test("should add a stop", async () => {
await when.addFunctionStop("circle-radius");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, paint: { "circle-radius": { stops: [[6, 5], [10, 5], [11, 5]] } } }],
});
});
test("should delete the first stop", async () => {
// A function needs more than two stops, otherwise deleting one collapses
// it back into a plain value.
await when.addFunctionStop("circle-radius");
await when.deleteFunctionStop("circle-radius");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, paint: { "circle-radius": { stops: [[10, 5], [11, 5]] } } }],
});
});
test("should set the base", async () => {
await when.setFunctionBase("circle-radius", "2");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, paint: { "circle-radius": { base: 2, stops: [[6, 5], [10, 5]] } } }],
});
});
test("should edit the zoom of a stop", async () => {
await when.setFunctionStopValue("circle-radius", "Zoom", 0, "3");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, paint: { "circle-radius": { stops: [[3, 5], [10, 5]] } } }],
});
});
test("should edit the output value of a stop", async () => {
await when.setFunctionStopValue("circle-radius", "Output value", 0, "9");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, paint: { "circle-radius": { stops: [[6, 9], [10, 5]] } } }],
});
});
test("should convert to an expression", async () => {
await when.makeExpression("circle-radius");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, paint: { "circle-radius": ["interpolate", ["linear"], ["zoom"], 6, 5, 10, 5] } }],
});
});
describe("when converted to a data function", () => {
beforeEach(async () => {
// Any non-interpolate scale turns the zoom function into a data one.
await when.selectFunctionType("circle-radius", "categorical");
});
test("should carry the stops over as zoom/value pairs", async () => {
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id,
paint: {
"circle-radius": {
property: "",
type: "exponential",
stops: [[{ zoom: 6, value: 0 }, 5], [{ zoom: 10, value: 0 }, 5]],
},
},
},
],
});
});
test("should convert back to a zoom function", async () => {
await when.selectFunctionType("circle-radius", "interpolate");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, paint: { "circle-radius": { stops: [[6, 5], [10, 5]] } } }],
});
});
// Deleting the first stop leaves the rest.
await when.deleteFunctionStop("circle-radius");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, type: "circle", source: "example", paint: { "circle-radius": { stops: [[10, 5], [11, 5]] } } }],
});
});
describe("data function", () => {
beforeEach(async () => {
id = await when.modal.fillLayers({ type: "circle", layer: "example" });
await when.setValue("spec-field-input:circle-blur", "1");
await when.makeDataFunction("circle-blur");
});
test("should convert the property to a data function", async () => {
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id,
type: "circle",
source: "example",
paint: {
"circle-blur": {
property: "",
type: "exponential",
stops: [[{ zoom: 6, value: 0 }, 1], [{ zoom: 10, value: 0 }, 1]],
},
test("convert a property to a data function and edit stops", async () => {
const id = await when.modal.fillLayers({ type: "circle", layer: "example" });
// The property needs a value before it can be turned into a data function.
await when.setValue("spec-field-input:circle-blur", "1");
await when.makeDataFunction("circle-blur");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id,
type: "circle",
source: "example",
paint: {
"circle-blur": {
property: "",
type: "exponential",
stops: [[{ zoom: 6, value: 0 }, 1], [{ zoom: 10, value: 0 }, 1]],
},
},
],
});
},
],
});
test("should add a stop", async () => {
await when.addFunctionStop("circle-blur");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id,
paint: {
"circle-blur": {
stops: [[{ zoom: 6, value: 0 }, 1], [{ zoom: 10, value: 0 }, 1], [{ zoom: 11, value: 0 }, 1]],
},
await when.addFunctionStop("circle-blur");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id,
type: "circle",
source: "example",
paint: {
"circle-blur": {
property: "",
type: "exponential",
stops: [[{ zoom: 6, value: 0 }, 1], [{ zoom: 10, value: 0 }, 1], [{ zoom: 11, value: 0 }, 1]],
},
},
],
});
},
],
});
test("should delete the first stop", async () => {
await when.addFunctionStop("circle-blur");
await when.deleteFunctionStop("circle-blur");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id,
paint: {
"circle-blur": {
stops: [[{ zoom: 10, value: 0 }, 1], [{ zoom: 11, value: 0 }, 1]],
},
await when.deleteFunctionStop("circle-blur");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id,
type: "circle",
source: "example",
paint: {
"circle-blur": {
property: "",
type: "exponential",
stops: [[{ zoom: 10, value: 0 }, 1], [{ zoom: 11, value: 0 }, 1]],
},
},
],
});
});
test("should set the property", async () => {
await when.setFunctionProperty("circle-blur", "myprop");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, paint: { "circle-blur": { property: "myprop" } } }],
});
});
test("should set the default", async () => {
await when.setFunctionDefault("circle-blur", "0.5");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, paint: { "circle-blur": { default: 0.5 } } }],
});
});
test("should edit the input value of a stop", async () => {
await when.setFunctionStopValue("circle-blur", "Input value", 0, "7");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id,
paint: {
"circle-blur": {
stops: [[{ zoom: 6, value: 7 }, 1], [{ zoom: 10, value: 0 }, 1]],
},
},
},
],
});
});
test("should change the function type", async () => {
await when.selectFunctionType("circle-blur", "categorical");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, paint: { "circle-blur": { type: "categorical" } } }],
});
});
});
describe("expression", () => {
beforeEach(async () => {
id = await when.modal.fillLayers({ type: "circle", layer: "example" });
await when.setValue("spec-field-input:circle-blur", "1");
await when.makeExpression("circle-blur");
});
test("should wrap the property value in a literal expression", async () => {
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, paint: { "circle-blur": ["literal", 1] } }],
});
});
test("should restore the plain value when reverted", async () => {
await when.undoExpression("circle-blur");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, paint: { "circle-blur": 1 } }],
});
});
test("should fall back to the spec default when deleted", async () => {
await when.deleteExpression("circle-blur");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, paint: { "circle-blur": 0 } }],
});
},
],
});
});
});
@@ -488,43 +296,10 @@ describe("layer editor", () => {
});
describe("paint", () => {
let id: string;
beforeEach(async () => {
id = await when.modal.fillLayers({ type: "fill", layer: "example" });
});
test("expand/collapse", async () => {
await then(get.elementByTestId("spec-field:fill-color")).shouldBeVisible();
await when.toggleGroupInLayerEditor("Paint properties");
await then(get.elementByTestId("spec-field:fill-color")).shouldNotBeVisible();
await when.toggleGroupInLayerEditor("Paint properties");
await then(get.elementByTestId("spec-field:fill-color")).shouldBeVisible();
});
test("color", async () => {
await when.setColorValue("fill-color", "#ff0000");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, type: "fill", source: "example", paint: { "fill-color": "#ff0000" } }],
});
});
test("pattern", async () => {
await when.setStringValue("fill-pattern", "some-pattern");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, type: "fill", source: "example", paint: { "fill-pattern": "some-pattern" } }],
});
});
test("opacity", async () => {
await when.setValue("spec-field-input:fill-opacity", "0.4");
await when.click("layer-editor.layer-id");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, type: "fill", source: "example", paint: { "fill-opacity": 0.4 } }],
});
});
test.skip("expand/collapse", () => {});
test.skip("color", () => {});
test.skip("pattern", () => {});
test.skip("opacity", () => {});
});
describe("json-editor", () => {
@@ -544,28 +319,8 @@ describe("layer editor", () => {
await then(get.element(".cm-lint-marker-error")).shouldExist();
});
test("expand/collapse", async () => {
const bgId = await createBackground();
await when.click("layer-list-item:background:" + bgId);
await then(get.element(".cm-content")).shouldBeVisible();
await when.toggleGroupInLayerEditor("JSON Editor");
await then(get.element(".cm-content")).shouldNotBeVisible();
await when.toggleGroupInLayerEditor("JSON Editor");
await then(get.element(".cm-content")).shouldBeVisible();
});
test("modify", async () => {
const bgId = await createBackground();
await when.click("layer-list-item:background:" + bgId);
await when.appendToJsonEditorLine('"background"', ',\n"minzoom": 5');
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id: "background:" + bgId, type: "background", minzoom: 5 }],
});
});
test.skip("expand/collapse", () => {});
test.skip("modify", () => {});
test("parse error", async () => {
const bgId = await createBackground();
+3 -16
View File
@@ -97,15 +97,7 @@ describe("layers list", () => {
});
});
test("modify", async () => {
const id = await when.modal.fillLayers({ type: "background" });
await when.click("layer-list-item:" + id);
await when.setValue("spec-field-input:background-opacity", "0.4");
await when.click("layer-editor.layer-id");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, type: "background", paint: { "background-opacity": 0.4 } }],
});
});
test.skip("modify", () => {});
});
describe("fill", () => {
@@ -116,13 +108,8 @@ describe("layers list", () => {
});
});
test("change source", async () => {
const id = await when.modal.fillLayers({ type: "fill", layer: "example" });
await when.changeLayerSource("raster");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [{ id, type: "fill", source: "raster" }],
});
});
// TODO: Change source
test.skip("change source", () => {});
});
describe("line", () => {
+8 -124
View File
@@ -38,7 +38,6 @@ export class MaputnikDriver {
"example-style-with-fonts.json",
"example-style-with-zoom-7-and-center-0-51.json",
"example-style-with-zoom-5-and-center-50-50.json",
"access-token-style.json",
];
for (const fixture of styleFixtures) {
await this.helper.given.interceptAndMockResponse({
@@ -72,7 +71,6 @@ export class MaputnikDriver {
| "rectangles"
| "font"
| "zoom_7_center_0_51"
| "access_tokens"
| "",
zoom?: number
) => {
@@ -84,7 +82,6 @@ export class MaputnikDriver {
rectangles: "rectangles-style.json",
font: "example-style-with-fonts.json",
zoom_7_center_0_51: "example-style-with-zoom-7-and-center-0-51.json",
access_tokens: "access-token-style.json",
};
const url = new URL(baseUrl);
@@ -110,13 +107,7 @@ export class MaputnikDriver {
},
chooseExampleFile: async () => {
await this.helper.when.openFileByFixture("example-style.json", "modal:open.dropzone");
await this.helper.when.wait(200);
},
/** Picks the example style through the browser's native file chooser. */
chooseExampleFileFromPicker: async () => {
await this.helper.when.chooseFileFromPicker("example-style.json", "modal:open.dropzone");
await this.helper.when.openFileByFixture("example-style.json", "modal:open.dropzone", "modal:open.file.input");
await this.helper.when.wait(200);
},
@@ -137,22 +128,6 @@ export class MaputnikDriver {
await this.helper.get.element(".maputnik-layer-editor-group__button").nth(index).click();
},
/** Expands/collapses a layer-editor group by its title, e.g. "Paint properties". */
toggleGroupInLayerEditor: async (title: string) => {
await this.helper.when.click("layer-editor-group:" + title);
},
/**
* Picks a source for the selected layer from the source autocomplete.
* The autocomplete is a controlled (downshift) input, so the value has to be
* filled rather than typed key by key, then chosen from the filtered menu.
*/
changeLayerSource: async (sourceId: string) => {
const input = this.helper.get.elementByTestId("layer-editor.layer-source").locator("input");
await input.fill(sourceId);
await this.helper.get.element(".maputnik-autocomplete-menu-item").first().click();
},
appendTextInJsonEditor: async (text: string) => {
await this.helper.get.element(".cm-line").first().click();
// Move to the very start of the document so the inserted text breaks the
@@ -205,67 +180,6 @@ export class MaputnikDriver {
await container.locator(".maputnik-delete-stop").first().click({ force: true });
},
/** Turns the property into a raw style expression. */
makeExpression: async (fieldName: string) => {
const container = this.helper.get.elementByTestId("spec-field-container:" + fieldName);
await container.scrollIntoViewIfNeeded();
// In the plain spec field the expression button shares the zoom-function
// class and comes first; inside a function editor it has its own test id.
const inFunctionEditor = container.locator("[data-wd-key='convert-to-expression']");
const button =
(await inFunctionEditor.count()) > 0
? inFunctionEditor
: container.locator(".maputnik-make-zoom-function").first();
await button.click({ force: true });
},
/** Reverts an expression back to a plain value. */
undoExpression: async (fieldName: string) => {
const container = this.helper.get.elementByTestId("spec-field-container:" + fieldName);
await container.locator("[data-wd-key='undo-expression']").click({ force: true });
},
/** Removes an expression, restoring the property's spec default. */
deleteExpression: async (fieldName: string) => {
const container = this.helper.get.elementByTestId("spec-field-container:" + fieldName);
await container.locator("[data-wd-key='delete-expression']").click({ force: true });
},
/** Picks the function scale (categorical/interval/exponential/identity/interpolate). */
selectFunctionType: async (fieldName: string, type: string) => {
const container = this.helper.get.elementByTestId("spec-field-container:" + fieldName);
await container.locator("[data-wd-key='function-type'] select").selectOption(type);
},
/** Sets the "Base" input of a zoom/data function. */
setFunctionBase: async (fieldName: string, value: string) => {
const container = this.helper.get.elementByTestId("spec-field-container:" + fieldName);
await container.locator("[data-wd-key='function-base'] input").fill(value);
},
/**
* Sets the data property a data function keys off of. This is an InputString,
* which only commits its value on blur, so typing alone is not enough.
*/
setFunctionProperty: async (fieldName: string, value: string) => {
const container = this.helper.get.elementByTestId("spec-field-container:" + fieldName);
const input = container.locator("[data-wd-key='function-property'] input");
await input.fill(value);
await input.blur();
},
/** Sets the fallback value used when a feature has no matching stop. */
setFunctionDefault: async (fieldName: string, value: string) => {
const container = this.helper.get.elementByTestId("spec-field-container:" + fieldName);
await container.locator("[data-wd-key='function-default'] input").fill(value);
},
/** Edits one cell of a function's stop table ("Zoom", "Input value" or "Output value"). */
setFunctionStopValue: async (fieldName: string, column: string, index: number, value: string) => {
const container = this.helper.get.elementByTestId("spec-field-container:" + fieldName);
await container.locator(`[aria-label="${column}"]`).nth(index).fill(value);
},
addFilter: async () => {
const button = this.helper.get.elementByTestId("layer-filter-button");
await button.scrollIntoViewIfNeeded();
@@ -276,29 +190,8 @@ export class MaputnikDriver {
await this.helper.get.element(".maputnik-filter-editor-operator select").first().selectOption(value);
},
/** Chooses how the filter items combine: all / none / any. */
selectFilterCombiningOperator: async (value: string) => {
await this.helper.when.selectWithin("filter-combining-operator", value);
},
deleteFilterItem: async (index = 0) => {
await this.helper.get
.element(".maputnik-filter-editor-block-action .maputnik-icon-button")
.nth(index)
.click();
},
/** Converts the simple filter editor into a raw expression editor. */
convertFilterToExpression: async () => {
await this.helper.when.click("filter-convert-to-expression");
},
/**
* Deletes the filter expression, restoring the simple filter editor.
* The filter group precedes the paint group, so its button comes first.
*/
deleteFilterExpression: async () => {
await this.helper.get.element("[data-wd-key='delete-expression']").first().click();
deleteFirstActiveSource: async () => {
await this.helper.get.element(".maputnik-active-source-type-editor-header-delete").first().click();
},
setColorValue: async (fieldName: string, value: string) => {
@@ -306,22 +199,13 @@ export class MaputnikDriver {
await input.fill(value);
},
/** Sets a plain string spec field (e.g. a pattern), which has no dedicated input test id. */
setStringValue: async (fieldName: string, value: string) => {
const input = this.helper.get.elementByTestId("spec-field:" + fieldName).locator("input.maputnik-string");
await input.fill(value);
await input.blur();
exportCreateHtml: async () => {
await this.helper.get.element(".maputnik-modal-export-buttons button").last().click();
},
/**
* Appends text to the end of the JSON editor line holding `lineText`.
* CodeMirror types over its own auto-inserted closing quotes/brackets, so a
* well-formed fragment stays well-formed.
*/
appendToJsonEditorLine: async (lineText: string, text: string) => {
await this.helper.when.clickByText(lineText);
await this.helper.when.typeKeys("{end}");
await this.helper.when.typeText(text);
exportSaveStyle: async () => {
await this.helper.stubSaveFilePicker();
await this.helper.get.element(".maputnik-modal-export-buttons button").first().click();
},
waitForExampleFileResponse: () => this.helper.when.waitForResponse("example-style.json"),
-40
View File
@@ -34,45 +34,5 @@ export class ModalDriver {
close: async (key: string) => {
await this.helper.when.click(key + ".close-modal");
},
/**
* Adds a source of the given type from the sources modal, keeping whatever
* defaults that type's editor prefills.
*/
addSource: async (sourceId: string, sourceType: string) => {
const { when } = this.helper;
await when.setValue("modal:sources.add.source_id", sourceId);
await when.select("modal:sources.add.source_type", sourceType);
await when.click("modal:sources.add.add_source");
await when.wait(200);
},
/** Adds one of the predefined public sources listed in the sources modal. */
addPublicSource: async (index = 0) => {
await this.helper.get.element(".maputnik-public-source-select").nth(index).click();
},
deleteFirstActiveSource: async () => {
await this.helper.get.element(".maputnik-active-source-type-editor-header-delete").first().click();
},
/** Fills one number box of a coordinate pair in the image/video source editor. */
setCoordinateValue: async (index: number, value: string) => {
const input = this.helper.get
.elementByTestId("modal:sources")
.locator(".maputnik-array input")
.nth(index);
await input.fill(value);
await input.blur();
},
exportCreateHtml: async () => {
await this.helper.get.element(".maputnik-modal-export-buttons button").last().click();
},
exportSaveStyle: async () => {
await this.helper.stubSaveFilePicker();
await this.helper.get.element(".maputnik-modal-export-buttons button").first().click();
},
};
}
+9 -164
View File
@@ -43,16 +43,6 @@ describe("modals", () => {
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude(get.responseBody("example-style.json"));
});
});
describe("without the File System Access API", () => {
test("upload via the file chooser", async () => {
await given.noFileSystemAccessApi();
await when.setStyle("");
await when.click("nav:open");
await when.chooseExampleFileFromPicker();
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude(get.fixture("example-style.json"));
});
});
});
describe("shortcuts", () => {
@@ -76,11 +66,11 @@ describe("modals", () => {
test("download HTML and save the style", async () => {
// Generate the standalone HTML export (triggers a file download).
await when.modal.exportCreateHtml();
await when.exportCreateHtml();
await then(get.elementByTestId("modal:export")).shouldExist();
// Saving the style closes the export modal.
await when.modal.exportSaveStyle();
await when.exportSaveStyle();
await then(get.elementByTestId("modal:export")).shouldNotExist();
});
});
@@ -92,26 +82,17 @@ describe("modals", () => {
});
test("active sources are listed and can be deleted", async () => {
// The "both" style ships with active sources; reopen the modal against it.
await when.setStyle("both");
await when.click("nav:sources");
const before = Object.keys(get.fixture("geojson-raster-style.json").sources).length;
await when.modal.deleteFirstActiveSource();
await when.deleteFirstActiveSource();
await then(
get.styleFromLocalStorage().then((style) => Object.keys(style.sources).length)
).shouldEqual(before - 1);
});
test("public source", async () => {
await when.modal.addPublicSource();
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
sources: {
openmaptiles: {
type: "vector",
url: `https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key=${tokens.openmaptiles}`,
},
},
});
});
test.skip("public source", () => {});
test("add new source", async () => {
const sourceId = "n1z2v3r";
@@ -158,108 +139,6 @@ describe("modals", () => {
sources: { [sourceId]: { tileSize: 128 } },
});
});
test("add new geojson url source", async () => {
await when.modal.addSource("geojsonurl", "geojson_url");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
sources: {
geojsonurl: { type: "geojson", data: "http://localhost:3000/geojson.json" },
},
});
});
test("add new geojson json source", async () => {
await when.modal.addSource("geojsonjson", "geojson_json");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
sources: {
geojsonjson: { type: "geojson", cluster: false, data: "" },
},
});
});
test("add new tilejson vector source", async () => {
await when.modal.addSource("tilejsonvector", "tilejson_vector");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
sources: {
tilejsonvector: { type: "vector", url: "http://localhost:3000/tilejson.json" },
},
});
});
test("add new tilejson raster source", async () => {
await when.modal.addSource("tilejsonraster", "tilejson_raster");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
sources: {
tilejsonraster: { type: "raster", url: "http://localhost:3000/tilejson.json" },
},
});
});
test("add new tilejson raster-dem source", async () => {
await when.modal.addSource("tilejsonrasterdem", "tilejson_raster-dem");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
sources: {
tilejsonrasterdem: { type: "raster-dem", url: "http://localhost:3000/tilejson.json" },
},
});
});
test("add new tile xyz raster-dem source", async () => {
await when.modal.addSource("tilexyzrasterdem", "tilexyz_raster-dem");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
sources: {
tilexyzrasterdem: {
type: "raster-dem",
tiles: ["http://localhost:3000/{x}/{y}/{z}.png"],
minzoom: 0,
maxzoom: 14,
tileSize: 512,
},
},
});
});
test("add new image source", async () => {
await when.modal.addSource("imagesource", "image");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
sources: {
imagesource: {
type: "image",
url: "http://localhost:3000/image.png",
coordinates: [[0, 0], [0, 0], [0, 0], [0, 0]],
},
},
});
});
test("add new video source", async () => {
await when.modal.addSource("videosource", "video");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
sources: {
videosource: {
type: "video",
urls: ["http://localhost:3000/movie.mp4"],
coordinates: [[0, 0], [0, 0], [0, 0], [0, 0]],
},
},
});
});
test("edit the corner coordinates of an image source", async () => {
const sourceId = "imagecoords";
await when.setValue("modal:sources.add.source_id", sourceId);
await when.select("modal:sources.add.source_type", "image");
// The first corner is the first two number boxes of the coordinate arrays.
await when.modal.setCoordinateValue(0, "1");
await when.modal.setCoordinateValue(1, "2");
await when.click("modal:sources.add.add_source");
await when.wait(200);
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
sources: {
[sourceId]: { type: "image", coordinates: [[1, 2], [0, 0], [0, 0], [0, 0]] },
},
});
});
});
describe("inspect", () => {
@@ -374,44 +253,6 @@ describe("modals", () => {
});
});
test("map view defaults", async () => {
await when.setValue("modal:settings.zoom", "4");
await when.setValue("modal:settings.bearing", "12");
await when.setValue("modal:settings.pitch", "30");
await when.click("modal:settings.name");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
zoom: 4,
bearing: 12,
pitch: 30,
});
});
test("light intensity", async () => {
await when.setValue("modal:settings.light-intensity", "0.7");
await when.click("modal:settings.name");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
light: { intensity: 0.7 },
});
});
test("terrain source and exaggeration", async () => {
await when.setValue("modal:settings.maputnik:terrain_source", "terrain");
await when.setValue("modal:settings.terrain-exaggeration", "1.5");
await when.click("modal:settings.name");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
terrain: { source: "terrain", exaggeration: 1.5 },
});
});
test("transition delay and duration", async () => {
await when.setValue("modal:settings.transition-delay", "100");
await when.setValue("modal:settings.transition-duration", "500");
await when.click("modal:settings.name");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
transition: { delay: 100, duration: 500 },
});
});
test("style projection mercator", async () => {
await when.select("modal:settings.projection", "mercator");
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
@@ -487,6 +328,10 @@ describe("modals", () => {
});
});
describe("sources placeholder", () => {
test.skip("toggle", () => {});
});
describe("global state", () => {
beforeEach(async () => {
await when.click("nav:global-state");
+16 -44
View File
@@ -145,7 +145,6 @@ async function typeSequence(page: Page, text: string): Promise<void> {
del: "Delete",
tab: "Tab",
home: "Home",
end: "End",
rightarrow: "ArrowRight",
};
@@ -218,18 +217,6 @@ export class PlaywrightHelper {
public then = <T>(target: T): Assertable<T> => new Assertable(target);
public given = {
/**
* Removes the File System Access API so the app falls back to a plain
* <input type="file">, the way Firefox and Safari behave. Must be called
* before the page under test is loaded.
*/
noFileSystemAccessApi: async () => {
await this.page.addInitScript(() => {
delete (window as any).showOpenFilePicker;
delete (window as any).showSaveFilePicker;
});
},
intercept: async (pattern: RegExp, alias: string, _method = "GET") => {
this.recordedRequests.set(alias, []);
await this.page.route(pattern, (route) => {
@@ -357,38 +344,23 @@ export class PlaywrightHelper {
await this.page.mouse.up();
},
/**
* Opens a fixture through the File System Access API, which raises no
* "filechooser" event and so has to be stubbed. For the <input type="file">
* fallback that browsers without the API use, see chooseFileFromPicker.
*/
openFileByFixture: async (fixture: string, buttonTestId: string) => {
openFileByFixture: async (fixture: string, buttonTestId: string, inputTestId: string) => {
const content = JSON.stringify(this.readFixture(fixture));
await this.page.evaluate((fileContent) => {
(window as any).showOpenFilePicker = async () => [
{ getFile: async () => ({ text: async () => fileContent }) },
];
}, content);
await this.testId(buttonTestId).click();
},
/**
* Clicks a control that opens the browser's native file chooser and answers
* it with a fixture. Only works on the <input type="file"> path — the File
* System Access API does not raise a "filechooser" event, so pair this with
* given.noFileSystemAccessApi().
*/
chooseFileFromPicker: async (fixture: string, triggerTestId: string) => {
const content = JSON.stringify(this.readFixture(fixture));
const [fileChooser] = await Promise.all([
this.page.waitForEvent("filechooser"),
this.testId(triggerTestId).click(),
]);
await fileChooser.setFiles({
name: fixture,
mimeType: "application/json",
buffer: Buffer.from(content),
});
const hasPicker = await this.page.evaluate(() => "showOpenFilePicker" in window);
if (hasPicker) {
await this.page.evaluate((fileContent) => {
(window as any).showOpenFilePicker = async () => [
{ getFile: async () => ({ text: async () => fileContent }) },
];
}, content);
await this.testId(buttonTestId).click();
} else {
await this.testId(inputTestId).setInputFiles({
name: fixture,
mimeType: "application/json",
buffer: Buffer.from(content),
});
}
},
dropFileByFixture: async (fixture: string, dropzoneTestId: string) => {
+1 -3
View File
@@ -34,9 +34,7 @@ export default defineConfig({
rules: {
"react-refresh/only-export-components": [
"warn",
// Many components are exported as withTranslation()(Component); without
// this the rule cannot tell the HOC's result is still a component.
{ allowConstantExport: true, extraHOCs: ["withTranslation"] }
{ allowConstantExport: true }
],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": [
+171 -171
View File
@@ -20,7 +20,7 @@
"@mapbox/mapbox-gl-rtl-text": "^0.4.0",
"@maplibre/maplibre-gl-geocoder": "^1.9.4",
"@maplibre/maplibre-gl-inspect": "^1.8.2",
"@maplibre/maplibre-gl-style-spec": "^26.2.1",
"@maplibre/maplibre-gl-style-spec": "^25.0.2",
"array-move": "^4.0.0",
"buffer": "^6.0.3",
"classnames": "^2.5.1",
@@ -30,7 +30,7 @@
"downshift": "^9.4.0",
"events": "^3.3.0",
"file-saver": "^2.0.5",
"i18next": "^26.3.6",
"i18next": "^26.3.4",
"i18next-browser-languagedetector": "^8.2.1",
"i18next-resources-to-backend": "^1.2.1",
"json-stringify-pretty-compact": "^4.0.0",
@@ -92,22 +92,22 @@
"@vitejs/plugin-react": "5.2",
"@vitest/coverage-v8": "^4.1.10",
"cors": "^2.8.6",
"eslint": "^10.7.0",
"eslint": "^10.6.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.3",
"i18next-cli": "^1.66.2",
"i18next-cli": "^1.65.0",
"istanbul": "^0.4.5",
"istanbul-lib-coverage": "^3.2.2",
"nyc": "^18.0.0",
"postcss": "^8.5.21",
"postcss": "^8.5.16",
"react-hot-loader": "^4.13.1",
"sass": "^1.101.3",
"stylelint": "^17.14.1",
"sass": "^1.101.0",
"stylelint": "^17.14.0",
"stylelint-config-recommended-scss": "^17.0.1",
"stylelint-scss": "^7.2.0",
"typescript": "^6.0.3",
"typescript-eslint": "^8.65.0",
"typescript-eslint": "^8.62.1",
"uuid": "^14.0.1",
"vite": "^7.3.2",
"vite-plugin-istanbul": "^9.0.1",
@@ -436,13 +436,13 @@
}
},
"node_modules/@cacheable/memory": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz",
"integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==",
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.9.tgz",
"integrity": "sha512-HdMx6DoGywB30vacDbBsITbIX4pgFqj1zsrV58jZBUw3klzkNoXhj7qOqAgledhxG7YZI5rBSJg7Zp8/VG0DuA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@cacheable/utils": "^2.5.0",
"@cacheable/utils": "^2.4.1",
"@keyv/bigmap": "^1.3.1",
"hookified": "^1.15.1",
"keyv": "^5.6.0"
@@ -459,9 +459,9 @@
}
},
"node_modules/@cacheable/utils": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz",
"integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==",
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.4.1.tgz",
"integrity": "sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -647,9 +647,9 @@
}
},
"node_modules/@csstools/css-syntax-patches-for-csstree": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz",
"integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==",
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz",
"integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==",
"dev": true,
"funding": [
{
@@ -2179,9 +2179,9 @@
}
},
"node_modules/@maplibre/maplibre-gl-style-spec": {
"version": "26.2.1",
"resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-26.2.1.tgz",
"integrity": "sha512-QFKCXkOeSzOr8jF75jm6kySOg+dUvOehPhRi68gcOYPHb7U5JloUq0dJW0Y5/fZV8ygfT0Vp2RWodvq+fyxFWA==",
"version": "25.0.2",
"resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-25.0.2.tgz",
"integrity": "sha512-hTNlIVG1gaLKz6UvOV23PYcT8wVS69LLMK9jbGT2hDWzs6hlTbMejeMyQsAF0rebj9czVp77Vhp4KRaUHHUp9g==",
"license": "ISC",
"dependencies": {
"@mapbox/jsonlint-lines-primitives": "^2.0.3",
@@ -3741,17 +3741,17 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz",
"integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==",
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.1.tgz",
"integrity": "sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.65.0",
"@typescript-eslint/type-utils": "8.65.0",
"@typescript-eslint/utils": "8.65.0",
"@typescript-eslint/visitor-keys": "8.65.0",
"@typescript-eslint/scope-manager": "8.62.1",
"@typescript-eslint/type-utils": "8.62.1",
"@typescript-eslint/utils": "8.62.1",
"@typescript-eslint/visitor-keys": "8.62.1",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -3764,15 +3764,15 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.65.0",
"@typescript-eslint/parser": "^8.62.1",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz",
"integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3780,16 +3780,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz",
"integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==",
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.1.tgz",
"integrity": "sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.65.0",
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/typescript-estree": "8.65.0",
"@typescript-eslint/visitor-keys": "8.65.0",
"@typescript-eslint/scope-manager": "8.62.1",
"@typescript-eslint/types": "8.62.1",
"@typescript-eslint/typescript-estree": "8.62.1",
"@typescript-eslint/visitor-keys": "8.62.1",
"debug": "^4.4.3"
},
"engines": {
@@ -3805,14 +3805,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz",
"integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==",
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.1.tgz",
"integrity": "sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.65.0",
"@typescript-eslint/types": "^8.65.0",
"@typescript-eslint/tsconfig-utils": "^8.62.1",
"@typescript-eslint/types": "^8.62.1",
"debug": "^4.4.3"
},
"engines": {
@@ -3827,14 +3827,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz",
"integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==",
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.1.tgz",
"integrity": "sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/visitor-keys": "8.65.0"
"@typescript-eslint/types": "8.62.1",
"@typescript-eslint/visitor-keys": "8.62.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3845,9 +3845,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz",
"integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==",
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.1.tgz",
"integrity": "sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3862,15 +3862,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz",
"integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==",
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.62.1.tgz",
"integrity": "sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/typescript-estree": "8.65.0",
"@typescript-eslint/utils": "8.65.0",
"@typescript-eslint/types": "8.62.1",
"@typescript-eslint/typescript-estree": "8.62.1",
"@typescript-eslint/utils": "8.62.1",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -3887,9 +3887,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz",
"integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==",
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.1.tgz",
"integrity": "sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3901,16 +3901,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz",
"integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==",
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.1.tgz",
"integrity": "sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.65.0",
"@typescript-eslint/tsconfig-utils": "8.65.0",
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/visitor-keys": "8.65.0",
"@typescript-eslint/project-service": "8.62.1",
"@typescript-eslint/tsconfig-utils": "8.62.1",
"@typescript-eslint/types": "8.62.1",
"@typescript-eslint/visitor-keys": "8.62.1",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -3939,16 +3939,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "20 || >=22"
"node": "18 || 20 || >=22"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
@@ -3968,16 +3968,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz",
"integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==",
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.1.tgz",
"integrity": "sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.65.0",
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/typescript-estree": "8.65.0"
"@typescript-eslint/scope-manager": "8.62.1",
"@typescript-eslint/types": "8.62.1",
"@typescript-eslint/typescript-estree": "8.62.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3992,13 +3992,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz",
"integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==",
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.1.tgz",
"integrity": "sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/types": "8.62.1",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -4693,14 +4693,14 @@
}
},
"node_modules/cacheable": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz",
"integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==",
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.5.tgz",
"integrity": "sha512-EQfaKe09tl615iNvq/TBRWTFf1AKJNXYQSsMx0Z3EI0nA+pVsVPS8wJhnRlkbdacKPh1d0qVIhwTc2zsQNFEEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@cacheable/memory": "^2.2.0",
"@cacheable/utils": "^2.5.0",
"@cacheable/memory": "^2.0.8",
"@cacheable/utils": "^2.4.1",
"hookified": "^1.15.0",
"keyv": "^5.6.0",
"qified": "^0.10.1"
@@ -5857,9 +5857,9 @@
}
},
"node_modules/eslint": {
"version": "10.7.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.7.0.tgz",
"integrity": "sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==",
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.6.0.tgz",
"integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==",
"dev": true,
"license": "MIT",
"workspaces": [
@@ -6282,9 +6282,9 @@
}
},
"node_modules/fast-uri": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz",
"integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
"dev": true,
"funding": [
{
@@ -6867,9 +6867,9 @@
}
},
"node_modules/globby": {
"version": "16.2.2",
"resolved": "https://registry.npmjs.org/globby/-/globby-16.2.2.tgz",
"integrity": "sha512-NLvV9ubZ6NDsJaOpKPy3cQeJpKi9DcWiyCiFUpJPA0YihRqiE6RWaLUmgNNPr8MgPpLZjnBjSmou7uZBRJv9wA==",
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/globby/-/globby-16.2.0.tgz",
"integrity": "sha512-QrJia2qDf5BB/V6HYlDTs0I0lBahyjLzpGQg3KT7FnCdTonAyPy2RtY802m2k4ALx6Dp752f82WsOczEVr3l6Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6888,15 +6888,41 @@
}
},
"node_modules/globby/node_modules/ignore": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz",
"integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/globby/node_modules/is-path-inside": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz",
"integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/globby/node_modules/slash": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
"integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/globjoin": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz",
@@ -7204,9 +7230,9 @@
}
},
"node_modules/i18next": {
"version": "26.3.6",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.6.tgz",
"integrity": "sha512-Bu5Z2nAXgfVyM8xvW3jk9EKRIuX37PudsrBViThNFx7CR7aaYTpP01cxNB/E4c4UUzTDiAZRstEhsRfPOL/8xA==",
"version": "26.3.4",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.4.tgz",
"integrity": "sha512-pa7m0d7pBDqGHZxljT+WPFeyFgQ7P7SciPPo1tTqYuO0z4sqADYhwnBESmmGp/wEof1inwdls/k8ZgTg8rxFHA==",
"funding": [
{
"type": "individual",
@@ -7223,7 +7249,7 @@
],
"license": "MIT",
"peerDependencies": {
"typescript": "^5 || ^6 || ^7"
"typescript": "^5 || ^6"
},
"peerDependenciesMeta": {
"typescript": {
@@ -7241,9 +7267,9 @@
}
},
"node_modules/i18next-cli": {
"version": "1.66.2",
"resolved": "https://registry.npmjs.org/i18next-cli/-/i18next-cli-1.66.2.tgz",
"integrity": "sha512-mI9MSOt/sal4QehCx/fsGfMg5K+PG8le6POeaKj3FlAqrpSZIpH0kf0j5kvVE8Sk2cSfHCjAfmoKWiXmvNNwLw==",
"version": "1.65.0",
"resolved": "https://registry.npmjs.org/i18next-cli/-/i18next-cli-1.65.0.tgz",
"integrity": "sha512-sak+2Ry4P7wtl7xMAZg2sWG2vup1lRHFBKA7h5IeEqFUog51QEgeYUhHxd2x85+MvS4BhVOZZs833clnd1WgYA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -7577,9 +7603,9 @@
}
},
"node_modules/immutable": {
"version": "5.1.9",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz",
"integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==",
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz",
"integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==",
"dev": true,
"license": "MIT"
},
@@ -8018,19 +8044,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-path-inside": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz",
"integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-plain-obj": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
@@ -9713,9 +9726,9 @@
}
},
"node_modules/nanoid": {
"version": "3.3.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"dev": true,
"funding": [
{
@@ -10742,9 +10755,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.21",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.21.tgz",
"integrity": "sha512-v4sDNP3fdNiWMfabO7OwOQdOX8TiQSztKyT1Wj0w+j7LDallJThJRBBBmzVGyYj0crMh7jlV4zepPkiNu9UwDQ==",
"version": "8.5.16",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
"integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
"dev": true,
"funding": [
{
@@ -10762,7 +10775,7 @@
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.16",
"nanoid": "^3.3.12",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -11725,9 +11738,9 @@
"license": "MIT"
},
"node_modules/sass": {
"version": "1.101.3",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.101.3.tgz",
"integrity": "sha512-Z1lLHhtAII+dyLNIQB6JQTZMy7sDxk3f5NzbINRc9ks1P0HCGvSuKev0wUhULFpLSaHBIMZrcTs9WDQUZerrgA==",
"version": "1.101.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.101.0.tgz",
"integrity": "sha512-OL3GoQyoUdDt843DpVmDO6y2k1sc5IhUDSpu8XucEI+35neq5QivZ1iuegnpraEVTJXlQGK1gl27zKcTLEPbQw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -11936,19 +11949,6 @@
"dev": true,
"license": "ISC"
},
"node_modules/slash": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
"integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/slugify": {
"version": "1.6.9",
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.9.tgz",
@@ -12272,9 +12272,9 @@
}
},
"node_modules/stylelint": {
"version": "17.14.1",
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.14.1.tgz",
"integrity": "sha512-xVQwyiuxALUBNB2fBe0tmNemg9KqLtdj3T64mioFDar79B2cU8LIyz+3KL6LdiHs9NkeNfwxpKSaIVOY8f112g==",
"version": "17.14.0",
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.14.0.tgz",
"integrity": "sha512-8xkHPpdqYryeIsOgfsYTmr6cIeC4nLYWk5S8BPxpodq8mIuepggkMljsHewWfuAjj/+qpRKou2QerhjMH3iasg==",
"dev": true,
"funding": [
{
@@ -12290,7 +12290,7 @@
"dependencies": {
"@csstools/css-calc": "^3.2.1",
"@csstools/css-parser-algorithms": "^4.0.0",
"@csstools/css-syntax-patches-for-csstree": "^1.1.6",
"@csstools/css-syntax-patches-for-csstree": "^1.1.5",
"@csstools/css-tokenizer": "^4.0.0",
"@csstools/media-query-list-parser": "^5.0.0",
"@csstools/selector-resolve-nested": "^4.0.0",
@@ -12302,9 +12302,9 @@
"debug": "^4.4.3",
"fast-glob": "^3.3.3",
"fastest-levenshtein": "^1.0.16",
"file-entry-cache": "^11.1.5",
"file-entry-cache": "^11.1.3",
"global-modules": "^2.0.0",
"globby": "^16.2.1",
"globby": "^16.2.0",
"globjoin": "^0.1.4",
"html-tags": "^5.1.0",
"ignore": "^7.0.5",
@@ -12314,12 +12314,12 @@
"micromatch": "^4.0.8",
"normalize-path": "^3.0.0",
"picocolors": "^1.1.1",
"postcss": "^8.5.16",
"postcss": "^8.5.15",
"postcss-safe-parser": "^7.0.1",
"postcss-selector-parser": "^7.1.4",
"postcss-value-parser": "^4.2.0",
"string-width": "^8.2.1",
"supports-hyperlinks": "^4.5.0",
"supports-hyperlinks": "^4.4.0",
"svg-tags": "^1.0.0",
"table": "^6.9.0",
"write-file-atomic": "^7.0.1"
@@ -12418,23 +12418,23 @@
}
},
"node_modules/stylelint/node_modules/file-entry-cache": {
"version": "11.1.5",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz",
"integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==",
"version": "11.1.3",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.3.tgz",
"integrity": "sha512-oMbq0PD6VIiIwMF6LIa7MEwd/l9huKwmqRKXqmrkqIZv8CvRbfowL+L0ryAl8h//HfAS0zS+4SbYoRyAoA6BJA==",
"dev": true,
"license": "MIT",
"dependencies": {
"flat-cache": "^6.1.23"
"flat-cache": "^6.1.22"
}
},
"node_modules/stylelint/node_modules/flat-cache": {
"version": "6.1.23",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz",
"integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==",
"version": "6.1.22",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.22.tgz",
"integrity": "sha512-N2dnzVJIphnNsjHcrxGW7DePckJ6haPrSFqpsBUhHYgwtKGVq4JrBGielEGD2fCVnsGm1zlBVZ8wGhkyuetgug==",
"dev": true,
"license": "MIT",
"dependencies": {
"cacheable": "^2.5.0",
"cacheable": "^2.3.4",
"flatted": "^3.4.2",
"hookified": "^1.15.0"
}
@@ -12547,9 +12547,9 @@
}
},
"node_modules/supports-hyperlinks": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-4.5.0.tgz",
"integrity": "sha512-ZW2OvfeCXrNTbLakPUzjQG922EeGCOteFSVoek5DKStTh898wf7zgtuFlzQN8HfZCxC3Eh02yJVrRW51hADf+w==",
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-4.4.0.tgz",
"integrity": "sha512-UKbpT93hN5Nr9go5UY7bopIB9YQlMz9nm/ct4IXt/irb5YRkn9WaqrOBJGZ5Pwvsd5FQzSVeYlGdXoCAPQZrPg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -12985,16 +12985,16 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.65.0.tgz",
"integrity": "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==",
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.62.1.tgz",
"integrity": "sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.65.0",
"@typescript-eslint/parser": "8.65.0",
"@typescript-eslint/typescript-estree": "8.65.0",
"@typescript-eslint/utils": "8.65.0"
"@typescript-eslint/eslint-plugin": "8.62.1",
"@typescript-eslint/parser": "8.62.1",
"@typescript-eslint/typescript-estree": "8.62.1",
"@typescript-eslint/utils": "8.62.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+8 -8
View File
@@ -37,7 +37,7 @@
"@mapbox/mapbox-gl-rtl-text": "^0.4.0",
"@maplibre/maplibre-gl-geocoder": "^1.9.4",
"@maplibre/maplibre-gl-inspect": "^1.8.2",
"@maplibre/maplibre-gl-style-spec": "^26.2.1",
"@maplibre/maplibre-gl-style-spec": "^25.0.2",
"array-move": "^4.0.0",
"buffer": "^6.0.3",
"classnames": "^2.5.1",
@@ -47,7 +47,7 @@
"downshift": "^9.4.0",
"events": "^3.3.0",
"file-saver": "^2.0.5",
"i18next": "^26.3.6",
"i18next": "^26.3.4",
"i18next-browser-languagedetector": "^8.2.1",
"i18next-resources-to-backend": "^1.2.1",
"json-stringify-pretty-compact": "^4.0.0",
@@ -126,22 +126,22 @@
"@vitejs/plugin-react": "5.2",
"@vitest/coverage-v8": "^4.1.10",
"cors": "^2.8.6",
"eslint": "^10.7.0",
"eslint": "^10.6.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.3",
"i18next-cli": "^1.66.2",
"i18next-cli": "^1.65.0",
"istanbul": "^0.4.5",
"istanbul-lib-coverage": "^3.2.2",
"nyc": "^18.0.0",
"postcss": "^8.5.21",
"postcss": "^8.5.16",
"react-hot-loader": "^4.13.1",
"sass": "^1.101.3",
"stylelint": "^17.14.1",
"sass": "^1.101.0",
"stylelint": "^17.14.0",
"stylelint-config-recommended-scss": "^17.0.1",
"stylelint-scss": "^7.2.0",
"typescript": "^6.0.3",
"typescript-eslint": "^8.65.0",
"typescript-eslint": "^8.62.1",
"uuid": "^14.0.1",
"vite": "^7.3.2",
"vite-plugin-istanbul": "^9.0.1",
+23 -23
View File
@@ -11,29 +11,29 @@ import {type Map, type LayerSpecification, type StyleSpecification, type Validat
import {validateStyleMin} from "@maplibre/maplibre-gl-style-spec";
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import { MapMaplibreGl } from "./MapMaplibreGl";
import { MapOpenLayers } from "./MapOpenLayers";
import { CodeEditor } from "./CodeEditor";
import { LayerList } from "./LayerList";
import { LayerEditor } from "./LayerEditor";
import { AppToolbar, type MapState } from "./AppToolbar";
import { AppLayout } from "./AppLayout";
import { AppMessagePanel as MessagePanel } from "./AppMessagePanel";
import MapMaplibreGl from "./MapMaplibreGl";
import MapOpenLayers from "./MapOpenLayers";
import CodeEditor from "./CodeEditor";
import LayerList from "./LayerList";
import LayerEditor from "./LayerEditor";
import AppToolbar, { type MapState } from "./AppToolbar";
import AppLayout from "./AppLayout";
import MessagePanel from "./AppMessagePanel";
import { ModalSettings } from "./modals/ModalSettings";
import { ModalExport } from "./modals/ModalExport";
import { ModalSources } from "./modals/ModalSources";
import { ModalOpen } from "./modals/ModalOpen";
import { ModalShortcuts } from "./modals/ModalShortcuts";
import { ModalDebug } from "./modals/ModalDebug";
import { ModalGlobalState } from "./modals/ModalGlobalState";
import ModalSettings from "./modals/ModalSettings";
import ModalExport from "./modals/ModalExport";
import ModalSources from "./modals/ModalSources";
import ModalOpen from "./modals/ModalOpen";
import ModalShortcuts from "./modals/ModalShortcuts";
import ModalDebug from "./modals/ModalDebug";
import ModalGlobalState from "./modals/ModalGlobalState";
import {downloadGlyphsMetadata, downloadSpriteMetadata} from "../libs/metadata";
import { emptyStyle, getAccessToken, replaceAccessTokens } from "../libs/style";
import style from "../libs/style";
import { undoMessages, redoMessages } from "../libs/diffmessage";
import { createStyleStore, type IStyleStore } from "../libs/store/style-store-factory";
import { RevisionStore } from "../libs/revisions";
import { LayerWatcher } from "../libs/layerwatcher";
import LayerWatcher from "../libs/layerwatcher";
import tokens from "../config/tokens.json";
import isEqual from "lodash.isequal";
import { type MapOptions } from "maplibre-gl";
@@ -48,19 +48,19 @@ function setFetchAccessToken(url: string, mapStyle: StyleSpecification) {
const matchesThunderforest = url.match(/\.thunderforest\.com/);
const matchesLocationIQ = url.match(/\.locationiq\.com/);
if (matchesTilehosting || matchesMaptiler) {
const accessToken = getAccessToken("openmaptiles", mapStyle, {allowFallback: true});
const accessToken = style.getAccessToken("openmaptiles", mapStyle, {allowFallback: true});
if (accessToken) {
return url.replace("{key}", accessToken);
}
}
else if (matchesThunderforest) {
const accessToken = getAccessToken("thunderforest", mapStyle, {allowFallback: true});
const accessToken = style.getAccessToken("thunderforest", mapStyle, {allowFallback: true});
if (accessToken) {
return url.replace("{key}", accessToken);
}
}
else if (matchesLocationIQ) {
const accessToken = getAccessToken("locationiq", mapStyle, {allowFallback: true});
const accessToken = style.getAccessToken("locationiq", mapStyle, {allowFallback: true});
if (accessToken) {
return url.replace("{key}", accessToken);
}
@@ -123,7 +123,7 @@ type AppState = {
fileHandle: FileSystemFileHandle | null
};
export class App extends React.Component<any, AppState> {
export default class App extends React.Component<any, AppState> {
revisionStore: RevisionStore;
styleStore: IStyleStore | null = null;
layerWatcher: LayerWatcher;
@@ -137,7 +137,7 @@ export class App extends React.Component<any, AppState> {
this.state = {
errors: [],
infos: [],
mapStyle: emptyStyle,
mapStyle: style.emptyStyle,
selectedLayerIndex: 0,
sources: {},
vectorLayers: {},
@@ -698,7 +698,7 @@ export class App extends React.Component<any, AppState> {
mapStyle: (dirtyMapStyle || mapStyle),
mapView: this.state.mapView,
replaceAccessTokens: (mapStyle: StyleSpecification) => {
return replaceAccessTokens(mapStyle, {
return style.replaceAccessTokens(mapStyle, {
allowFallback: true
});
},
+3 -2
View File
@@ -1,5 +1,5 @@
import React from "react";
import { ScrollContainer } from "./ScrollContainer";
import ScrollContainer from "./ScrollContainer";
import { type WithTranslation, withTranslation } from "react-i18next";
import { IconContext } from "react-icons";
@@ -50,4 +50,5 @@ class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
}
}
export const AppLayout = withTranslation()(AppLayoutInternal);
const AppLayout = withTranslation()(AppLayoutInternal);
export default AppLayout;
+2 -1
View File
@@ -61,4 +61,5 @@ class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalPro
}
}
export const AppMessagePanel = withTranslation()(AppMessagePanelInternal);
const AppMessagePanel = withTranslation()(AppMessagePanelInternal);
export default AppMessagePanel;
+2 -1
View File
@@ -306,4 +306,5 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
}
}
export const AppToolbar = withTranslation()(AppToolbarInternal);
const AppToolbar = withTranslation()(AppToolbarInternal);
export default AppToolbar;
+3 -3
View File
@@ -1,7 +1,7 @@
import React, {type CSSProperties, type PropsWithChildren, type SyntheticEvent} from "react";
import classnames from "classnames";
import { FieldDocLabel } from "./FieldDocLabel";
import { Doc } from "./Doc";
import FieldDocLabel from "./FieldDocLabel";
import Doc from "./Doc";
export type BlockProps = PropsWithChildren & {
"data-wd-key"?: string
@@ -19,7 +19,7 @@ type BlockState = {
};
/** Wrap a component with a label */
export class Block extends React.Component<BlockProps, BlockState> {
export default class Block extends React.Component<BlockProps, BlockState> {
_blockEl: HTMLDivElement | null = null;
constructor (props: BlockProps) {
+4 -2
View File
@@ -1,4 +1,4 @@
import { InputJson } from "./InputJson";
import InputJson from "./InputJson";
import React from "react";
import { withTranslation, type WithTranslation } from "react-i18next";
import { type StyleSpecification } from "maplibre-gl";
@@ -24,4 +24,6 @@ const CodeEditorInternal: React.FC<CodeEditorProps> = (props) => {
</>;
};
export const CodeEditor = withTranslation()(CodeEditorInternal);
const CodeEditor = withTranslation()(CodeEditorInternal);
export default CodeEditor;
+1 -1
View File
@@ -9,7 +9,7 @@ type CollapseProps = {
};
export class Collapse extends React.Component<CollapseProps> {
export default class Collapse extends React.Component<CollapseProps> {
static defaultProps = {
isActive: true
};
+1 -1
View File
@@ -6,7 +6,7 @@ type CollapserProps = {
style?: object
};
export class Collapser extends React.Component<CollapserProps> {
export default class Collapser extends React.Component<CollapserProps> {
render() {
const iconStyle = {
width: 20,
+1 -1
View File
@@ -23,7 +23,7 @@ type DocProps = {
}
};
export class Doc extends React.Component<DocProps> {
export default class Doc extends React.Component<DocProps> {
render () {
const {fieldSpec} = this.props;
+5 -3
View File
@@ -1,5 +1,5 @@
import { InputArray, type InputArrayProps } from "./InputArray";
import { Fieldset } from "./Fieldset";
import InputArray, { type InputArrayProps } from "./InputArray";
import Fieldset from "./Fieldset";
type FieldArrayProps = InputArrayProps & {
name?: string
@@ -8,10 +8,12 @@ type FieldArrayProps = InputArrayProps & {
}
};
export const FieldArray: React.FC<FieldArrayProps> = (props) => {
const FieldArray: React.FC<FieldArrayProps> = (props) => {
return (
<Fieldset label={props.label} fieldSpec={props.fieldSpec}>
<InputArray {...props} />
</Fieldset>
);
};
export default FieldArray;
+5 -3
View File
@@ -1,5 +1,5 @@
import { Block } from "./Block";
import { InputAutocomplete, type InputAutocompleteProps } from "./InputAutocomplete";
import Block from "./Block";
import InputAutocomplete, { type InputAutocompleteProps } from "./InputAutocomplete";
type FieldAutocompleteProps = InputAutocompleteProps & {
@@ -7,10 +7,12 @@ type FieldAutocompleteProps = InputAutocompleteProps & {
};
export const FieldAutocomplete: React.FC<FieldAutocompleteProps> = (props) => {
const FieldAutocomplete: React.FC<FieldAutocompleteProps> = (props) => {
return (
<Block label={props.label}>
<InputAutocomplete {...props} />
</Block>
);
};
export default FieldAutocomplete;
+5 -3
View File
@@ -1,5 +1,5 @@
import { Block } from "./Block";
import { InputCheckbox, type InputCheckboxProps } from "./InputCheckbox";
import Block from "./Block";
import InputCheckbox, {type InputCheckboxProps} from "./InputCheckbox";
type FieldCheckboxProps = InputCheckboxProps & {
@@ -7,10 +7,12 @@ type FieldCheckboxProps = InputCheckboxProps & {
};
export const FieldCheckbox: React.FC<FieldCheckboxProps> = (props) => {
const FieldCheckbox: React.FC<FieldCheckboxProps> = (props) => {
return (
<Block label={props.label}>
<InputCheckbox {...props} />
</Block>
);
};
export default FieldCheckbox;
+5 -3
View File
@@ -1,5 +1,5 @@
import { Block } from "./Block";
import { InputColor, type InputColorProps } from "./InputColor";
import Block from "./Block";
import InputColor, {type InputColorProps} from "./InputColor";
type FieldColorProps = InputColorProps & {
@@ -10,10 +10,12 @@ type FieldColorProps = InputColorProps & {
};
export const FieldColor: React.FC<FieldColorProps> = (props) => {
const FieldColor: React.FC<FieldColorProps> = (props) => {
return (
<Block label={props.label} fieldSpec={props.fieldSpec}>
<InputColor {...props} />
</Block>
);
};
export default FieldColor;
+4 -3
View File
@@ -1,7 +1,7 @@
import React from "react";
import { Block } from "./Block";
import { InputString } from "./InputString";
import Block from "./Block";
import InputString from "./InputString";
import { type WithTranslation, withTranslation } from "react-i18next";
type FieldCommentInternalProps = {
@@ -36,4 +36,5 @@ const FieldCommentInternal: React.FC<FieldCommentInternalProps> = (props) => {
);
};
export const FieldComment = withTranslation()(FieldCommentInternal);
const FieldComment = withTranslation()(FieldCommentInternal);
export default FieldComment;
+3 -1
View File
@@ -10,7 +10,7 @@ type FieldDocLabelProps = {
};
export const FieldDocLabel: React.FC<FieldDocLabelProps> = (props) => {
const FieldDocLabel: React.FC<FieldDocLabelProps> = (props) => {
const [open, setOpen] = React.useState(false);
const onToggleDoc = (state: boolean) => {
@@ -49,3 +49,5 @@ export const FieldDocLabel: React.FC<FieldDocLabelProps> = (props) => {
}
return <div />;
};
export default FieldDocLabel;
+6 -7
View File
@@ -1,17 +1,16 @@
import { InputDynamicArray, type InputDynamicArrayProps } from "./InputDynamicArray";
import { Fieldset } from "./Fieldset";
import InputDynamicArray, {type InputDynamicArrayProps} from "./InputDynamicArray";
import Fieldset from "./Fieldset";
type FieldDynamicArrayProps = InputDynamicArrayProps & {
name?: string;
fieldSpec?: {
value?: string;
}
name?: string
};
export const FieldDynamicArray: React.FC<FieldDynamicArrayProps> = (props) => {
const FieldDynamicArray: React.FC<FieldDynamicArrayProps> = (props) => {
return (
<Fieldset label={props.label}>
<InputDynamicArray {...props} />
</Fieldset>
);
};
export default FieldDynamicArray;
+5 -3
View File
@@ -1,5 +1,5 @@
import { InputEnum, type InputEnumProps } from "./InputEnum";
import { Fieldset } from "./Fieldset";
import InputEnum, {type InputEnumProps} from "./InputEnum";
import Fieldset from "./Fieldset";
type FieldEnumProps = InputEnumProps & {
@@ -10,10 +10,12 @@ type FieldEnumProps = InputEnumProps & {
};
export const FieldEnum: React.FC<FieldEnumProps> = (props) => {
const FieldEnum: React.FC<FieldEnumProps> = (props) => {
return (
<Fieldset label={props.label} fieldSpec={props.fieldSpec}>
<InputEnum {...props} />
</Fieldset>
);
};
export default FieldEnum;
+7 -5
View File
@@ -1,9 +1,9 @@
import React from "react";
import { SpecProperty } from "./SpecProperty";
import { DataProperty, type Stop } from "./DataProperty";
import { ZoomProperty } from "./ZoomProperty";
import { ExpressionProperty } from "./ExpressionProperty";
import SpecProperty from "./_SpecProperty";
import DataProperty, { type Stop } from "./_DataProperty";
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";
@@ -128,7 +128,7 @@ type FieldFunctionProps = {
/** Supports displaying spec field for zoom function objects
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
*/
export const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
const [dataType, setDataType] = React.useState(
getDataType(props.value, props.fieldSpec)
);
@@ -402,3 +402,5 @@ export const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
</div>
);
};
export default FieldFunction;
+5 -3
View File
@@ -1,7 +1,7 @@
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import { Block } from "./Block";
import { InputString } from "./InputString";
import Block from "./Block";
import InputString from "./InputString";
type FieldIdProps = {
value: string
@@ -10,7 +10,7 @@ type FieldIdProps = {
error?: {message: string}
};
export const FieldId: React.FC<FieldIdProps> = (props) => {
const FieldId: React.FC<FieldIdProps> = (props) => {
return (
<Block label="ID" fieldSpec={latest.layer.id}
data-wd-key={props.wdKey}
@@ -24,3 +24,5 @@ export const FieldId: React.FC<FieldIdProps> = (props) => {
</Block>
);
};
export default FieldId;
+4 -2
View File
@@ -1,9 +1,11 @@
import { InputJson, type InputJsonProps } from "./InputJson";
import InputJson, {type InputJsonProps} from "./InputJson";
type FieldJsonProps = InputJsonProps & {};
export const FieldJson: React.FC<FieldJsonProps> = (props) => {
const FieldJson: React.FC<FieldJsonProps> = (props) => {
return <InputJson {...props} />;
};
export default FieldJson;
+4 -3
View File
@@ -1,8 +1,8 @@
import React from "react";
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import { Block } from "./Block";
import { InputNumber } from "./InputNumber";
import Block from "./Block";
import InputNumber from "./InputNumber";
import { type WithTranslation, withTranslation } from "react-i18next";
type FieldMaxZoomInternalProps = {
@@ -31,4 +31,5 @@ const FieldMaxZoomInternal: React.FC<FieldMaxZoomInternalProps> = (props) => {
);
};
export const FieldMaxZoom = withTranslation()(FieldMaxZoomInternal);
const FieldMaxZoom = withTranslation()(FieldMaxZoomInternal);
export default FieldMaxZoom;
+4 -3
View File
@@ -1,8 +1,8 @@
import React from "react";
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import { Block } from "./Block";
import { InputNumber } from "./InputNumber";
import Block from "./Block";
import InputNumber from "./InputNumber";
import { type WithTranslation, withTranslation } from "react-i18next";
type FieldMinZoomInternalProps = {
@@ -31,4 +31,5 @@ const FieldMinZoomInternal: React.FC<FieldMinZoomInternalProps> = (props) => {
);
};
export const FieldMinZoom = withTranslation()(FieldMinZoomInternal);
const FieldMinZoom = withTranslation()(FieldMinZoomInternal);
export default FieldMinZoom;
+5 -3
View File
@@ -1,5 +1,5 @@
import { InputMultiInput, type InputMultiInputProps } from "./InputMultiInput";
import { Fieldset } from "./Fieldset";
import InputMultiInput, {type InputMultiInputProps} from "./InputMultiInput";
import Fieldset from "./Fieldset";
type FieldMultiInputProps = InputMultiInputProps & {
@@ -7,10 +7,12 @@ type FieldMultiInputProps = InputMultiInputProps & {
};
export const FieldMultiInput: React.FC<FieldMultiInputProps> = (props) => {
const FieldMultiInput: React.FC<FieldMultiInputProps> = (props) => {
return (
<Fieldset label={props.label}>
<InputMultiInput {...props} />
</Fieldset>
);
};
export default FieldMultiInput;
+5 -3
View File
@@ -1,5 +1,5 @@
import { InputNumber, type InputNumberProps } from "./InputNumber";
import { Block } from "./Block";
import InputNumber, {type InputNumberProps} from "./InputNumber";
import Block from "./Block";
type FieldNumberProps = InputNumberProps & {
@@ -10,10 +10,12 @@ type FieldNumberProps = InputNumberProps & {
};
export const FieldNumber: React.FC<FieldNumberProps> = (props) => {
const FieldNumber: React.FC<FieldNumberProps> = (props) => {
return (
<Block label={props.label} fieldSpec={props.fieldSpec}>
<InputNumber {...props} />
</Block>
);
};
export default FieldNumber;
+5 -3
View File
@@ -1,5 +1,5 @@
import { Block } from "./Block";
import { InputSelect, type InputSelectProps } from "./InputSelect";
import Block from "./Block";
import InputSelect, {type InputSelectProps} from "./InputSelect";
type FieldSelectProps = InputSelectProps & {
@@ -10,10 +10,12 @@ type FieldSelectProps = InputSelectProps & {
};
export const FieldSelect: React.FC<FieldSelectProps> = (props) => {
const FieldSelect: React.FC<FieldSelectProps> = (props) => {
return (
<Block label={props.label} fieldSpec={props.fieldSpec}>
<InputSelect {...props} />
</Block>
);
};
export default FieldSelect;
+4 -3
View File
@@ -1,8 +1,8 @@
import React from "react";
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import { Block } from "./Block";
import { InputAutocomplete } from "./InputAutocomplete";
import Block from "./Block";
import InputAutocomplete from "./InputAutocomplete";
import { type WithTranslation, withTranslation } from "react-i18next";
type FieldSourceInternalProps = {
@@ -38,4 +38,5 @@ const FieldSourceInternal: React.FC<FieldSourceInternalProps> = ({
};
export const FieldSource = withTranslation()(FieldSourceInternal);
const FieldSource = withTranslation()(FieldSourceInternal);
export default FieldSource;
+4 -3
View File
@@ -1,8 +1,8 @@
import React from "react";
import {latest} from "@maplibre/maplibre-gl-style-spec";
import { Block } from "./Block";
import { InputAutocomplete } from "./InputAutocomplete";
import Block from "./Block";
import InputAutocomplete from "./InputAutocomplete";
import { type WithTranslation, withTranslation } from "react-i18next";
type FieldSourceLayerInternalProps = {
@@ -35,4 +35,5 @@ const FieldSourceLayerInternal: React.FC<FieldSourceLayerInternalProps> = ({
);
};
export const FieldSourceLayer = withTranslation()(FieldSourceLayerInternal);
const FieldSourceLayer = withTranslation()(FieldSourceLayerInternal);
export default FieldSourceLayer;
+6 -4
View File
@@ -1,6 +1,6 @@
import { Block, type BlockProps } from "./Block";
import { InputSpec, type FieldSpecType, type InputSpecProps } from "./InputSpec";
import { Fieldset, type FieldsetProps } from "./Fieldset";
import Block, { type BlockProps } from "./Block";
import InputSpec, { type FieldSpecType, type InputSpecProps } from "./InputSpec";
import Fieldset, { type FieldsetProps } from "./Fieldset";
function getElementFromType(fieldSpec: { type?: FieldSpecType, values?: unknown[] }): typeof Fieldset | typeof Block {
switch(fieldSpec.type) {
@@ -36,7 +36,7 @@ function getElementFromType(fieldSpec: { type?: FieldSpecType, values?: unknown[
export type FieldSpecProps = InputSpecProps & BlockProps & FieldsetProps;
export const FieldSpec: React.FC<FieldSpecProps> = (props) => {
const FieldSpec: React.FC<FieldSpecProps> = (props) => {
const TypeBlock = getElementFromType(props.fieldSpec!);
return (
@@ -45,3 +45,5 @@ export const FieldSpec: React.FC<FieldSpecProps> = (props) => {
</TypeBlock>
);
};
export default FieldSpec;
+5 -3
View File
@@ -1,5 +1,5 @@
import { Block } from "./Block";
import { InputString, type InputStringProps } from "./InputString";
import Block from "./Block";
import InputString, {type InputStringProps} from "./InputString";
type FieldStringProps = InputStringProps & {
name?: string
@@ -9,10 +9,12 @@ type FieldStringProps = InputStringProps & {
}
};
export const FieldString: React.FC<FieldStringProps> = (props) => {
const FieldString: React.FC<FieldStringProps> = (props) => {
return (
<Block label={props.label} fieldSpec={props.fieldSpec}>
<InputString {...props} />
</Block>
);
};
export default FieldString;
+5 -4
View File
@@ -1,8 +1,8 @@
import React from "react";
import {v8} from "@maplibre/maplibre-gl-style-spec";
import { Block } from "./Block";
import { InputSelect } from "./InputSelect";
import { InputString } from "./InputString";
import Block from "./Block";
import InputSelect from "./InputSelect";
import InputString from "./InputString";
import { type WithTranslation, withTranslation } from "react-i18next";
import { startCase } from "lodash";
@@ -43,4 +43,5 @@ const FieldTypeInternal: React.FC<FieldTypeInternalProps> = ({
);
};
export const FieldType = withTranslation()(FieldTypeInternal);
const FieldType = withTranslation()(FieldTypeInternal);
export default FieldType;
+5 -3
View File
@@ -1,5 +1,5 @@
import { InputUrl, type FieldUrlProps as InputUrlProps } from "./InputUrl";
import { Block } from "./Block";
import InputUrl, {type FieldUrlProps as InputUrlProps} from "./InputUrl";
import Block from "./Block";
type FieldUrlProps = InputUrlProps & {
@@ -10,10 +10,12 @@ type FieldUrlProps = InputUrlProps & {
};
export const FieldUrl: React.FC<FieldUrlProps> = (props) => {
const FieldUrl: React.FC<FieldUrlProps> = (props) => {
return (
<Block label={props.label} fieldSpec={props.fieldSpec}>
<InputUrl {...props} />
</Block>
);
};
export default FieldUrl;
+6 -4
View File
@@ -1,8 +1,8 @@
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";
import FieldDocLabel from "./FieldDocLabel";
import Doc from "./Doc";
import generateUniqueId from "../libs/document-uid";
export type FieldsetProps = PropsWithChildren & {
label?: string,
@@ -12,7 +12,7 @@ export type FieldsetProps = PropsWithChildren & {
};
export const Fieldset: React.FC<FieldsetProps> = (props) => {
const Fieldset: React.FC<FieldsetProps> = (props) => {
const [showDoc, setShowDoc] = React.useState(false);
const labelId = React.useRef(generateUniqueId("fieldset_label_"));
@@ -49,3 +49,5 @@ export const Fieldset: React.FC<FieldsetProps> = (props) => {
</div>
);
};
export default Fieldset;
+9 -10
View File
@@ -7,13 +7,13 @@ import {migrate, convertFilter} from "@maplibre/maplibre-gl-style-spec";
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import {combiningFilterOps} from "../libs/filterops";
import { InputSelect } from "./InputSelect";
import { Block } from "./Block";
import { SingleFilterEditor } from "./SingleFilterEditor";
import { FilterEditorBlock } from "./FilterEditorBlock";
import { InputButton } from "./InputButton";
import { Doc } from "./Doc";
import { ExpressionProperty } from "./ExpressionProperty";
import InputSelect from "./InputSelect";
import Block from "./Block";
import SingleFilterEditor from "./SingleFilterEditor";
import FilterEditorBlock from "./FilterEditorBlock";
import InputButton from "./InputButton";
import Doc from "./Doc";
import ExpressionProperty from "./_ExpressionProperty";
import { type WithTranslation, withTranslation } from "react-i18next";
import type { MappedLayerErrors, StyleSpecificationWithId } from "../libs/definitions";
@@ -215,7 +215,6 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
onClick={this.makeExpression}
title={t("Convert to expression")}
className="maputnik-make-zoom-function"
data-wd-key="filter-convert-to-expression"
>
<TbMathFunction />
</InputButton>
@@ -249,7 +248,6 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
fieldSpec={fieldSpec}
label={t("Filter")}
action={actions}
data-wd-key="filter-combining-operator"
>
<InputSelect
value={combiningOp}
@@ -318,4 +316,5 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
}
}
export const FilterEditor = withTranslation()(FilterEditorInternal);
const FilterEditor = withTranslation()(FilterEditorInternal);
export default FilterEditor;
+3 -2
View File
@@ -1,5 +1,5 @@
import React, { type PropsWithChildren } from "react";
import { InputButton } from "./InputButton";
import InputButton from "./InputButton";
import {MdDelete} from "react-icons/md";
import { type WithTranslation, withTranslation } from "react-i18next";
@@ -27,4 +27,5 @@ class FilterEditorBlockInternal extends React.Component<FilterEditorBlockInterna
}
}
export const FilterEditorBlock = withTranslation()(FilterEditorBlockInternal);
const FilterEditorBlock = withTranslation()(FilterEditorBlockInternal);
export default FilterEditorBlock;
+3 -1
View File
@@ -13,7 +13,7 @@ type IconLayerProps = {
className?: string
};
export const IconLayer: React.FC<IconLayerProps> = (props) => {
const IconLayer: React.FC<IconLayerProps> = (props) => {
const iconProps = { style: props.style };
switch(props.type) {
case "fill-extrusion": return <IoMdCube {...iconProps} />;
@@ -29,3 +29,5 @@ export const IconLayer: React.FC<IconLayerProps> = (props) => {
default: return <MdPriorityHigh {...iconProps} />;
}
};
export default IconLayer;
+3 -3
View File
@@ -1,6 +1,6 @@
import React from "react";
import { InputString } from "./InputString";
import { InputNumber } from "./InputNumber";
import InputString from "./InputString";
import InputNumber from "./InputNumber";
export type InputArrayProps = {
value: (string | number | undefined)[]
@@ -17,7 +17,7 @@ type InputArrayState = {
initialPropsValue: unknown[]
};
export class InputArray extends React.Component<InputArrayProps, InputArrayState> {
export default class InputArray extends React.Component<InputArrayProps, InputArrayState> {
static defaultProps = {
value: [],
default: [],
+1 -1
View File
@@ -11,7 +11,7 @@ export type InputAutocompleteProps = {
"aria-label"?: string
};
export function InputAutocomplete({
export default function InputAutocomplete({
value,
options = [],
onChange = () => {},
+1 -1
View File
@@ -14,7 +14,7 @@ type InputButtonProps = {
title?: string
};
export class InputButton extends React.Component<InputButtonProps> {
export default class InputButton extends React.Component<InputButtonProps> {
render() {
return <button
id={this.props.id}
+1 -1
View File
@@ -6,7 +6,7 @@ export type InputCheckboxProps = {
onChange(...args: unknown[]): unknown
};
export class InputCheckbox extends React.Component<InputCheckboxProps> {
export default class InputCheckbox extends React.Component<InputCheckboxProps> {
static defaultProps = {
value: false,
};
+1 -1
View File
@@ -20,7 +20,7 @@ export type InputColorProps = {
};
/*** Number fields with support for min, max and units and documentation*/
export class InputColor extends React.Component<InputColorProps> {
export default class InputColor extends React.Component<InputColorProps> {
state = {
pickerOpened: false
};
+10 -8
View File
@@ -3,13 +3,13 @@ import capitalize from "lodash.capitalize";
import {MdDelete} from "react-icons/md";
import { type WithTranslation, withTranslation } from "react-i18next";
import { InputString } from "./InputString";
import { InputNumber } from "./InputNumber";
import { InputButton } from "./InputButton";
import { FieldDocLabel } from "./FieldDocLabel";
import { InputEnum } from "./InputEnum";
import { InputUrl } from "./InputUrl";
import { InputColor } from "./InputColor";
import InputString from "./InputString";
import InputNumber from "./InputNumber";
import InputButton from "./InputButton";
import FieldDocLabel from "./FieldDocLabel";
import InputEnum from "./InputEnum";
import InputUrl from "./InputUrl";
import InputColor from "./InputColor";
export type InputDynamicArrayProps = {
@@ -141,7 +141,9 @@ class InputDynamicArrayInternal extends React.Component<InputDynamicArrayInterna
}
}
export const InputDynamicArray = withTranslation()(InputDynamicArrayInternal);
const InputDynamicArray = withTranslation()(InputDynamicArrayInternal);
export default InputDynamicArray;
type DeleteValueInputButtonProps = {
onClick?(...args: unknown[]): unknown
} & WithTranslation;
+3 -3
View File
@@ -1,6 +1,6 @@
import React from "react";
import { InputSelect } from "./InputSelect";
import { InputMultiInput } from "./InputMultiInput";
import InputSelect from "./InputSelect";
import InputMultiInput from "./InputMultiInput";
function optionsLabelLength(options: any[]) {
@@ -25,7 +25,7 @@ export type InputEnumProps = {
};
export class InputEnum extends React.Component<InputEnumProps> {
export default class InputEnum extends React.Component<InputEnumProps> {
render() {
const {options, value, onChange, name, label} = this.props;
+2 -2
View File
@@ -1,5 +1,5 @@
import React from "react";
import { InputAutocomplete } from "./InputAutocomplete";
import InputAutocomplete from "./InputAutocomplete";
export type InputFontProps = {
name: string
@@ -11,7 +11,7 @@ export type InputFontProps = {
"aria-label"?: string
};
export class InputFont extends React.Component<InputFontProps> {
export default class InputFont extends React.Component<InputFontProps> {
static defaultProps = {
fonts: []
};
+2 -1
View File
@@ -135,4 +135,5 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
}
}
export const InputJson = withTranslation()(InputJsonInternal);
const InputJson = withTranslation()(InputJsonInternal);
export default InputJson;
+1 -1
View File
@@ -9,7 +9,7 @@ export type InputMultiInputProps = {
"aria-label"?: string
};
export class InputMultiInput extends React.Component<InputMultiInputProps> {
export default class InputMultiInput extends React.Component<InputMultiInputProps> {
render() {
let options = this.props.options;
if(options.length > 0 && !Array.isArray(options[0])) {
+2 -2
View File
@@ -1,5 +1,5 @@
import React, { type BaseSyntheticEvent } from "react";
import { generateUniqueId } from "../libs/document-uid";
import generateUniqueId from "../libs/document-uid";
export type InputNumberProps = {
value?: number
@@ -25,7 +25,7 @@ type InputNumberState = {
dirtyValue?: number | string | undefined
};
export class InputNumber extends React.Component<InputNumberProps, InputNumberState> {
export default class InputNumber extends React.Component<InputNumberProps, InputNumberState> {
static defaultProps = {
rangeStep: 1
};
+1 -1
View File
@@ -10,7 +10,7 @@ export type InputSelectProps = {
"aria-label"?: string
};
export class InputSelect extends React.Component<InputSelectProps> {
export default class InputSelect extends React.Component<InputSelectProps> {
render() {
let options = this.props.options;
if(options.length > 0 && !Array.isArray(options[0])) {
+10 -10
View File
@@ -1,14 +1,14 @@
import React, { type ReactElement } from "react";
import { InputColor, type InputColorProps } from "./InputColor";
import { InputNumber, type InputNumberProps } from "./InputNumber";
import { InputCheckbox, type InputCheckboxProps } from "./InputCheckbox";
import { InputString, type InputStringProps } from "./InputString";
import { InputArray, type InputArrayProps } from "./InputArray";
import { InputDynamicArray, type InputDynamicArrayProps } from "./InputDynamicArray";
import { InputFont, type InputFontProps } from "./InputFont";
import { InputAutocomplete, type InputAutocompleteProps } from "./InputAutocomplete";
import { InputEnum, type InputEnumProps } from "./InputEnum";
import InputColor, { type InputColorProps } from "./InputColor";
import InputNumber, { type InputNumberProps } from "./InputNumber";
import InputCheckbox, { type InputCheckboxProps } from "./InputCheckbox";
import InputString, { type InputStringProps } from "./InputString";
import InputArray, { type InputArrayProps } from "./InputArray";
import InputDynamicArray, { type InputDynamicArrayProps } from "./InputDynamicArray";
import InputFont, { type InputFontProps } from "./InputFont";
import InputAutocomplete, { type InputAutocompleteProps } from "./InputAutocomplete";
import InputEnum, { type InputEnumProps } from "./InputEnum";
import capitalize from "lodash.capitalize";
const iconProperties = ["background-pattern", "fill-pattern", "line-pattern", "fill-extrusion-pattern", "icon-image"];
@@ -38,7 +38,7 @@ export type InputSpecProps = {
/** Display any field from the Maplibre GL style spec and
* choose the correct field component based on the @{fieldSpec}
* to display @{value}. */
export class InputSpec extends React.Component<InputSpecProps> {
export default class InputSpec extends React.Component<InputSpecProps> {
childNodes() {
const commonProps = {
+1 -1
View File
@@ -20,7 +20,7 @@ type InputStringState = {
value?: string
};
export class InputString extends React.Component<InputStringProps, InputStringState> {
export default class InputString extends React.Component<InputStringProps, InputStringState> {
static defaultProps = {
onInput: () => {},
};
+4 -3
View File
@@ -1,6 +1,6 @@
import React, { type JSX } from "react";
import { InputString } from "./InputString";
import { SmallError } from "./SmallError";
import InputString from "./InputString";
import SmallError from "./SmallError";
import { Trans, type WithTranslation, withTranslation } from "react-i18next";
import { type TFunction } from "i18next";
import { ErrorType, validate } from "../libs/urlopen";
@@ -91,4 +91,5 @@ class InputUrlInternal extends React.Component<InputUrlInternalProps, InputUrlSt
}
}
export const InputUrl = withTranslation()(InputUrlInternal);
const InputUrl = withTranslation()(InputUrlInternal);
export default InputUrl;
+17 -17
View File
@@ -6,17 +6,17 @@ import { IconContext } from "react-icons";
import { type BackgroundLayerSpecification, type LayerSpecification, type SourceSpecification } from "maplibre-gl";
import { v8 } from "@maplibre/maplibre-gl-style-spec";
import { FieldJson } from "./FieldJson";
import { FilterEditor } from "./FilterEditor";
import { PropertyGroup } from "./PropertyGroup";
import { LayerEditorGroup } from "./LayerEditorGroup";
import { FieldType } from "./FieldType";
import { FieldId } from "./FieldId";
import { FieldMinZoom } from "./FieldMinZoom";
import { FieldMaxZoom } from "./FieldMaxZoom";
import { FieldComment } from "./FieldComment";
import { FieldSource } from "./FieldSource";
import { FieldSourceLayer } from "./FieldSourceLayer";
import FieldJson from "./FieldJson";
import FilterEditor from "./FilterEditor";
import PropertyGroup from "./PropertyGroup";
import LayerEditorGroup from "./LayerEditorGroup";
import FieldType from "./FieldType";
import FieldId from "./FieldId";
import FieldMinZoom from "./FieldMinZoom";
import FieldMaxZoom from "./FieldMaxZoom";
import FieldComment from "./FieldComment";
import FieldSource from "./FieldSource";
import FieldSourceLayer from "./FieldSourceLayer";
import { changeType, changeProperty } from "../libs/layer";
import { formatLayerId } from "../libs/format";
import { type WithTranslation, withTranslation } from "react-i18next";
@@ -74,20 +74,20 @@ function getLayoutForType(type: LayerSpecification["type"], t: TFunction): Maput
return getLayoutForSymbolType(t);
}
const groups: MaputnikLayoutGroup[] = [];
if (Object.keys(v8["paint_" + type as keyof typeof v8]).length > 0) {
if (Object.keys(v8["paint_" + type]).length > 0) {
groups.push({
title: t("Paint properties"),
id: "Paint_properties",
type: "properties",
fields: Object.keys(v8["paint_" + type as keyof typeof v8]),
fields: Object.keys(v8["paint_" + type]),
});
}
if (Object.keys(v8["layout_" + type as keyof typeof v8]).length > 0) {
if (Object.keys(v8["layout_" + type]).length > 0) {
groups.push({
title: t("Layout properties"),
id: "Layout_properties",
type: "properties",
fields: Object.keys(v8["layout_" + type as keyof typeof v8])
fields: Object.keys(v8["layout_" + type])
});
}
return groups;
@@ -230,7 +230,6 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
)}
/>
{this.props.layer.type !== "background" && <FieldSource
wdKey="layer-editor.layer-source"
error={errorData.source}
sourceIds={Object.keys(this.props.sources!)}
value={this.props.layer.source}
@@ -420,4 +419,5 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
}
}
export const LayerEditor = withTranslation()(LayerEditorInternal);
const LayerEditor = withTranslation()(LayerEditorInternal);
export default LayerEditor;
+1 -1
View File
@@ -18,7 +18,7 @@ type LayerEditorGroupProps = {
};
export class LayerEditorGroup extends React.Component<LayerEditorGroupProps> {
export default class LayerEditorGroup extends React.Component<LayerEditorGroupProps> {
render() {
return <AccordionItem uuid={this.props.id}>
<AccordionItemHeading className="maputnik-layer-editor-group"
+7 -5
View File
@@ -14,12 +14,12 @@ import {
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { LayerListGroup } from "./LayerListGroup";
import { LayerListItem } from "./LayerListItem";
import { ModalAdd } from "./modals/ModalAdd";
import LayerListGroup from "./LayerListGroup";
import LayerListItem from "./LayerListItem";
import ModalAdd from "./modals/ModalAdd";
import type {LayerSpecification, SourceSpecification} from "maplibre-gl";
import { generateUniqueId } from "../libs/document-uid";
import generateUniqueId from "../libs/document-uid";
import { findClosestCommonPrefix, layerPrefix } from "../libs/layer";
import { type WithTranslation, withTranslation } from "react-i18next";
import { type MappedError, type OnMoveLayerCallback } from "../libs/definitions";
@@ -336,7 +336,7 @@ type LayerListProps = LayerListContainerProps & {
onMoveLayer: OnMoveLayerCallback
};
export const LayerList: React.FC<LayerListProps> = (props) => {
const LayerList: React.FC<LayerListProps> = (props) => {
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } }));
const handleDragEnd = (event: DragEndEvent) => {
@@ -361,3 +361,5 @@ export const LayerList: React.FC<LayerListProps> = (props) => {
</DndContext>
);
};
export default LayerList;
+2 -2
View File
@@ -1,5 +1,5 @@
import React from "react";
import { Collapser } from "./Collapser";
import Collapser from "./Collapser";
type LayerListGroupProps = {
title: string
@@ -9,7 +9,7 @@ type LayerListGroupProps = {
"aria-controls"?: string
};
export class LayerListGroup extends React.Component<LayerListGroupProps> {
export default class LayerListGroup extends React.Component<LayerListGroupProps> {
render() {
return <li className="maputnik-layer-list-group">
<div className="maputnik-layer-list-group-header"
+4 -2
View File
@@ -5,7 +5,7 @@ import { IconContext } from "react-icons";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { IconLayer } from "./IconLayer";
import IconLayer from "./IconLayer";
import type { VisibilitySpecification } from "maplibre-gl";
@@ -87,7 +87,7 @@ type LayerListItemProps = {
onLayerVisibilityToggle?(...args: unknown[]): unknown
};
export const LayerListItem = React.forwardRef<HTMLLIElement, LayerListItemProps>((props, ref) => {
const LayerListItem = React.forwardRef<HTMLLIElement, LayerListItemProps>((props, ref) => {
const {
isSelected = false,
visibility = "visible",
@@ -162,3 +162,5 @@ export const LayerListItem = React.forwardRef<HTMLLIElement, LayerListItemProps>
</li>
</IconContext.Provider>;
});
export default LayerListItem;
+5 -4
View File
@@ -3,10 +3,10 @@ import {createRoot} from "react-dom/client";
import MapLibreGl, {type LayerSpecification, type LngLat, type Map, type MapOptions, type SourceSpecification, type StyleSpecification} from "maplibre-gl";
import MaplibreInspect from "@maplibre/maplibre-gl-inspect";
import colors from "@maplibre/maplibre-gl-inspect/lib/colors";
import { FeatureLayerPopup as MapMaplibreGlLayerPopup } from "./MapMaplibreGlLayerPopup";
import { FeaturePropertyPopup as MapMaplibreGlFeaturePropertyPopup, type InspectFeature } from "./MapMaplibreGlFeaturePropertyPopup";
import MapMaplibreGlLayerPopup from "./MapMaplibreGlLayerPopup";
import MapMaplibreGlFeaturePropertyPopup, { type InspectFeature } from "./MapMaplibreGlFeaturePropertyPopup";
import Color from "color";
import { ZoomControl } from "../libs/zoomcontrol";
import ZoomControl from "../libs/zoomcontrol";
import { type HighlightedLayer, colorHighlightedLayer } from "../libs/highlight";
import "maplibre-gl/dist/maplibre-gl.css";
import "../maplibregl.css";
@@ -322,4 +322,5 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
}
}
export const MapMaplibreGl = withTranslation()(MapMaplibreGlInternal);
const MapMaplibreGl = withTranslation()(MapMaplibreGlInternal);
export default MapMaplibreGl;
@@ -63,7 +63,7 @@ type FeaturePropertyPopupProps = {
features: InspectFeature[]
};
export class FeaturePropertyPopup extends React.Component<FeaturePropertyPopupProps> {
class FeaturePropertyPopup extends React.Component<FeaturePropertyPopupProps> {
render() {
const features = removeDuplicatedFeatures(this.props.features);
return <div className="maputnik-feature-property-popup" dir="ltr" data-wd-key="feature-property-popup">
@@ -75,3 +75,6 @@ export class FeaturePropertyPopup extends React.Component<FeaturePropertyPopupPr
</div>;
}
}
export default FeaturePropertyPopup;
+5 -2
View File
@@ -1,5 +1,5 @@
import React from "react";
import { IconLayer } from "./IconLayer";
import IconLayer from "./IconLayer";
import type {InspectFeature} from "./MapMaplibreGlFeaturePropertyPopup";
function groupFeaturesBySourceLayer(features: InspectFeature[]) {
@@ -32,7 +32,7 @@ type FeatureLayerPopupProps = {
zoom?: number
};
export class FeatureLayerPopup extends React.Component<FeatureLayerPopupProps> {
class FeatureLayerPopup extends React.Component<FeatureLayerPopupProps> {
_getFeatureColor(feature: InspectFeature, _zoom?: number) {
// Guard because openlayers won't have this
if (!feature.layer.paint) {
@@ -109,3 +109,6 @@ export class FeatureLayerPopup extends React.Component<FeatureLayerPopupProps> {
</div>;
}
}
export default FeatureLayerPopup;
+3 -2
View File
@@ -2,7 +2,7 @@ import React from "react";
import {throttle} from "lodash";
import { type WithTranslation, withTranslation } from "react-i18next";
import { FeatureLayerPopup as MapMaplibreGlLayerPopup } from "./MapMaplibreGlLayerPopup";
import MapMaplibreGlLayerPopup from "./MapMaplibreGlLayerPopup";
import "ol/ol.css";
//@ts-ignore
@@ -204,4 +204,5 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
}
}
export const MapOpenLayers = withTranslation()(MapOpenLayersInternal);
const MapOpenLayers = withTranslation()(MapOpenLayersInternal);
export default MapOpenLayers;
+2 -2
View File
@@ -1,6 +1,6 @@
import React from "react";
import { FieldFunction } from "./FieldFunction";
import FieldFunction from "./FieldFunction";
import type {LayerSpecification} from "maplibre-gl";
import { type MappedLayerErrors } from "../libs/definitions";
@@ -40,7 +40,7 @@ type PropertyGroupProps = {
errors?: MappedLayerErrors
};
export class PropertyGroup extends React.Component<PropertyGroupProps> {
export default class PropertyGroup extends React.Component<PropertyGroupProps> {
onPropertyChange = (property: string, newValue: any) => {
const group = getGroupName(this.props.spec, this.props.layer.type, property);
this.props.onChange(group ,property, newValue);
+1 -1
View File
@@ -4,7 +4,7 @@ type ScrollContainerProps = {
children?: React.ReactNode
};
export class ScrollContainer extends React.Component<ScrollContainerProps> {
export default class ScrollContainer extends React.Component<ScrollContainerProps> {
render() {
return <div className="maputnik-scroll-container">
{this.props.children}
+4 -4
View File
@@ -1,9 +1,9 @@
import React from "react";
import {otherFilterOps} from "../libs/filterops";
import { InputString } from "./InputString";
import { InputAutocomplete } from "./InputAutocomplete";
import { InputSelect } from "./InputSelect";
import InputString from "./InputString";
import InputAutocomplete from "./InputAutocomplete";
import InputSelect from "./InputSelect";
function tryParseInt(v: string | number) {
if (v === "") return v;
@@ -40,7 +40,7 @@ type SingleFilterEditorProps = {
properties?: {[key: string]: string}
};
export class SingleFilterEditor extends React.Component<SingleFilterEditorProps> {
export default class SingleFilterEditor extends React.Component<SingleFilterEditorProps> {
static defaultProps = {
properties: {},
};
+2 -1
View File
@@ -18,4 +18,5 @@ class SmallErrorInternal extends React.Component<SmallErrorInternalProps> {
}
}
export const SmallError = withTranslation()(SmallErrorInternal);
const SmallError = withTranslation()(SmallErrorInternal);
export default SmallError;
@@ -3,19 +3,19 @@ import {PiListPlusBold} from "react-icons/pi";
import {TbMathFunction} from "react-icons/tb";
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import { InputButton } from "./InputButton";
import { InputSpec } from "./InputSpec";
import { InputNumber } from "./InputNumber";
import { InputString } from "./InputString";
import { InputSelect } from "./InputSelect";
import { Block } from "./Block";
import { generateUniqueId as docUid } from "../libs/document-uid";
import { sortNumerically } from "../libs/sort-numerically";
import InputButton from "./InputButton";
import InputSpec from "./InputSpec";
import InputNumber from "./InputNumber";
import InputString from "./InputString";
import InputSelect from "./InputSelect";
import Block from "./Block";
import docUid from "../libs/document-uid";
import sortNumerically from "../libs/sort-numerically";
import {findDefaultFromSpec} from "../libs/spec-helper";
import { type WithTranslation, withTranslation } from "react-i18next";
import { labelFromFieldName } from "../libs/label-from-field-name";
import { DeleteStopButton } from "./DeleteStopButton";
import labelFromFieldName from "../libs/label-from-field-name";
import DeleteStopButton from "./_DeleteStopButton";
import { type MappedLayerErrors } from "../libs/definitions";
@@ -289,7 +289,6 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
<Block
label={t("Function")}
key="function"
data-wd-key="function-type"
>
<div className="maputnik-data-spec-property-input">
<InputSelect
@@ -304,7 +303,6 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
<Block
label={t("Base")}
key="base"
data-wd-key="function-base"
>
<div className="maputnik-data-spec-property-input">
<InputSpec
@@ -319,7 +317,6 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
<Block
label={"Property"}
key="property"
data-wd-key="function-property"
>
<div className="maputnik-data-spec-property-input">
<InputString
@@ -333,7 +330,6 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
<Block
label={t("Default")}
key="default"
data-wd-key="function-default"
>
<InputSpec
fieldName={this.props.fieldName}
@@ -372,7 +368,6 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
}
<InputButton
className="maputnik-add-stop"
data-wd-key="convert-to-expression"
onClick={this.props.onExpressionClick?.bind(this)}
>
<TbMathFunction style={{ verticalAlign: "text-bottom" }} />
@@ -385,4 +380,5 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
}
}
export const DataProperty = withTranslation()(DataPropertyInternal);
const DataProperty = withTranslation()(DataPropertyInternal);
export default DataProperty;
@@ -1,6 +1,6 @@
import React from "react";
import { InputButton } from "./InputButton";
import InputButton from "./InputButton";
import {MdDelete} from "react-icons/md";
import { type WithTranslation, withTranslation } from "react-i18next";
@@ -23,4 +23,5 @@ class DeleteStopButtonInternal extends React.Component<DeleteStopButtonInternalP
}
}
export const DeleteStopButton = withTranslation()(DeleteStopButtonInternal);
const DeleteStopButton = withTranslation()(DeleteStopButtonInternal);
export default DeleteStopButton;
@@ -2,10 +2,10 @@ import React from "react";
import {MdDelete, MdUndo} from "react-icons/md";
import { type WithTranslation, withTranslation } from "react-i18next";
import { Block } from "./Block";
import { InputButton } from "./InputButton";
import { labelFromFieldName } from "../libs/label-from-field-name";
import { FieldJson } from "./FieldJson";
import Block from "./Block";
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";
@@ -50,7 +50,6 @@ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInter
onClick={this.props.onUndo}
disabled={undoDisabled}
className="maputnik-delete-stop"
data-wd-key="undo-expression"
title={t("Revert from expression")}
>
<MdUndo />
@@ -60,7 +59,6 @@ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInter
key="delete_action"
onClick={this.props.onDelete}
className="maputnik-delete-stop"
data-wd-key="delete-expression"
title={t("Delete expression")}
>
<MdDelete />
@@ -92,4 +90,5 @@ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInter
}
}
export const ExpressionProperty = withTranslation()(ExpressionPropertyInternal);
const ExpressionProperty = withTranslation()(ExpressionPropertyInternal);
export default ExpressionProperty;
@@ -1,6 +1,6 @@
import React from "react";
import { InputButton } from "./InputButton";
import InputButton from "./InputButton";
import {MdFunctions, MdInsertChart} from "react-icons/md";
import { TbMathFunction } from "react-icons/tb";
import { type WithTranslation, withTranslation } from "react-i18next";
@@ -67,4 +67,5 @@ class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsI
}
}
export const FunctionInputButtons = withTranslation()(FunctionInputButtonsInternal);
const FunctionInputButtons = withTranslation()(FunctionInputButtonsInternal);
export default FunctionInputButtons;
@@ -1,9 +1,9 @@
import React from "react";
import { FieldSpec, type FieldSpecProps } from "./FieldSpec";
import { FunctionInputButtons as FunctionButtons } from "./FunctionButtons";
import FieldSpec, {type FieldSpecProps} from "./FieldSpec";
import FunctionButtons from "./_FunctionButtons";
import { labelFromFieldName } from "../libs/label-from-field-name";
import labelFromFieldName from "../libs/label-from-field-name";
type SpecPropertyProps = FieldSpecProps & {
@@ -19,7 +19,7 @@ type SpecPropertyProps = FieldSpecProps & {
};
export class SpecProperty extends React.Component<SpecPropertyProps> {
export default class SpecProperty extends React.Component<SpecPropertyProps> {
static defaultProps = {
errors: {},
};
@@ -4,17 +4,17 @@ import { TbMathFunction } from "react-icons/tb";
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import { type WithTranslation, withTranslation } from "react-i18next";
import { InputButton } from "./InputButton";
import { InputSpec } from "./InputSpec";
import { InputNumber } from "./InputNumber";
import { InputSelect } from "./InputSelect";
import { Block } from "./Block";
import InputButton from "./InputButton";
import InputSpec from "./InputSpec";
import InputNumber from "./InputNumber";
import InputSelect from "./InputSelect";
import Block from "./Block";
import { DeleteStopButton } from "./DeleteStopButton";
import { labelFromFieldName } from "../libs/label-from-field-name";
import DeleteStopButton from "./_DeleteStopButton";
import labelFromFieldName from "../libs/label-from-field-name";
import { generateUniqueId as docUid } from "../libs/document-uid";
import { sortNumerically } from "../libs/sort-numerically";
import docUid from "../libs/document-uid";
import sortNumerically from "../libs/sort-numerically";
import { type MappedLayerErrors } from "../libs/definitions";
@@ -194,7 +194,6 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
<div className="maputnik-data-fieldset-inner">
<Block
label={t("Function")}
data-wd-key="function-type"
>
<div className="maputnik-data-spec-property-input">
<InputSelect
@@ -207,7 +206,6 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
</Block>
<Block
label={t("Base")}
data-wd-key="function-base"
>
<div className="maputnik-data-spec-property-input">
<InputSpec
@@ -242,7 +240,6 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
</InputButton>
<InputButton
className="maputnik-add-stop"
data-wd-key="convert-to-expression"
onClick={this.props.onExpressionClick?.bind(this)}
>
<TbMathFunction style={{ verticalAlign: "text-bottom" }} />
@@ -267,4 +264,5 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
}
}
export const ZoomProperty = withTranslation()(ZoomPropertyInternal);
const ZoomProperty = withTranslation()(ZoomPropertyInternal);
export default ZoomProperty;
+2 -1
View File
@@ -67,4 +67,5 @@ class ModalInternal extends React.Component<ModalInternalProps> {
}
}
export const Modal = withTranslation()(ModalInternal);
const Modal = withTranslation()(ModalInternal);
export default Modal;
+8 -7
View File
@@ -2,12 +2,12 @@ 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 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 = {
@@ -192,4 +192,5 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
}
}
export const ModalAdd = withTranslation()(ModalAddInternal);
const ModalAdd = withTranslation()(ModalAddInternal);
export default ModalAdd;
+3 -2
View File
@@ -1,7 +1,7 @@
import React from "react";
import { Trans, type WithTranslation, withTranslation } from "react-i18next";
import { Modal } from "./Modal";
import Modal from "./Modal";
type ModalDebugInternalProps = {
@@ -79,4 +79,5 @@ class ModalDebugInternal extends React.Component<ModalDebugInternalProps> {
}
}
export const ModalDebug = withTranslation()(ModalDebugInternal);
const ModalDebug = withTranslation()(ModalDebugInternal);
export default ModalDebug;
+9 -8
View File
@@ -6,11 +6,11 @@ import {format} from "@maplibre/maplibre-gl-style-spec";
import {MdMap, MdSave} from "react-icons/md";
import {type WithTranslation, withTranslation} from "react-i18next";
import { FieldString } from "../FieldString";
import { InputButton } from "../InputButton";
import { Modal } from "./Modal";
import { replaceAccessTokens, stripAccessTokens } from "../../libs/style";
import { spec as fieldSpecAdditional } from "../../libs/field-spec-additional";
import FieldString from "../FieldString";
import InputButton from "../InputButton";
import Modal from "./Modal";
import style from "../../libs/style";
import fieldSpecAdditional from "../../libs/field-spec-additional";
import type {OnStyleChangedCallback, StyleSpecificationWithId} from "../../libs/definitions";
@@ -32,8 +32,8 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
tokenizedStyle() {
return format(
stripAccessTokens(
replaceAccessTokens(this.props.mapStyle)
style.stripAccessTokens(
style.replaceAccessTokens(this.props.mapStyle)
)
);
}
@@ -217,4 +217,5 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
}
}
export const ModalExport = withTranslation()(ModalExportInternal);
const ModalExport = withTranslation()(ModalExportInternal);
export default ModalExport;
+6 -5
View File
@@ -3,13 +3,13 @@ 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 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";
import Doc from "../Doc";
type ModalGlobalStateInternalProps = {
mapStyle: StyleSpecificationWithId;
@@ -151,4 +151,5 @@ const ModalGlobalStateInternal: React.FC<ModalGlobalStateInternalProps> = (props
);
};
export const ModalGlobalState = withTranslation()(ModalGlobalStateInternal);
const ModalGlobalState = withTranslation()(ModalGlobalStateInternal);
export default ModalGlobalState;
+4 -3
View File
@@ -1,8 +1,8 @@
import React from "react";
import { type WithTranslation, withTranslation } from "react-i18next";
import { InputButton } from "../InputButton";
import { Modal } from "./Modal";
import InputButton from "../InputButton";
import Modal from "./Modal";
type ModalLoadingInternalProps = {
@@ -35,4 +35,5 @@ class ModalLoadingInternal extends React.Component<ModalLoadingInternalProps> {
}
}
export const ModalLoading = withTranslation()(ModalLoadingInternal);
const ModalLoading = withTranslation()(ModalLoadingInternal);
export default ModalLoading;
+10 -9
View File
@@ -3,12 +3,12 @@ import { MdFileUpload } from "react-icons/md";
import { MdAddCircleOutline } from "react-icons/md";
import { Trans, type WithTranslation, withTranslation } from "react-i18next";
import { ModalLoading } from "./ModalLoading";
import { Modal } from "./Modal";
import { InputButton } from "../InputButton";
import { InputUrl } from "../InputUrl";
import ModalLoading from "./ModalLoading";
import Modal from "./Modal";
import InputButton from "../InputButton";
import InputUrl from "../InputUrl";
import { ensureStyleValidity } from "../../libs/style";
import style from "../../libs/style";
import publicStyles from "../../config/styles.json";
type PublicStyleProps = {
@@ -109,7 +109,7 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
activeRequestUrl: null
});
const mapStyle = ensureStyleValidity(body);
const mapStyle = style.ensureStyleValidity(body);
console.log("Loaded style ", mapStyle.id);
this.props.onStyleOpen(mapStyle);
this.onOpenToggle();
@@ -165,7 +165,7 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
});
return;
}
mapStyle = ensureStyleValidity(mapStyle);
mapStyle = style.ensureStyleValidity(mapStyle);
this.props.onStyleOpen(mapStyle, fileHandle);
this.onOpenToggle();
@@ -193,7 +193,7 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
});
return;
}
mapStyle = ensureStyleValidity(mapStyle);
mapStyle = style.ensureStyleValidity(mapStyle);
this.props.onStyleOpen(mapStyle);
this.onOpenToggle();
};
@@ -364,4 +364,5 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
}
}
export const ModalOpen = withTranslation()(ModalOpenInternal);
const ModalOpen = withTranslation()(ModalOpenInternal);
export default ModalOpen;
+13 -19
View File
@@ -3,17 +3,17 @@ import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import type {LightSpecification, ProjectionSpecification, StyleSpecification, TerrainSpecification, TransitionSpecification} from "maplibre-gl";
import { type WithTranslation, withTranslation } from "react-i18next";
import { FieldArray } from "../FieldArray";
import { FieldNumber } from "../FieldNumber";
import { FieldString } from "../FieldString";
import { FieldUrl } from "../FieldUrl";
import { FieldSelect } from "../FieldSelect";
import { FieldEnum } from "../FieldEnum";
import { FieldColor } from "../FieldColor";
import { Modal } from "./Modal";
import { FieldJson } from "../FieldJson";
import { Block } from "../Block";
import { spec as fieldSpecAdditional } from "../../libs/field-spec-additional";
import FieldArray from "../FieldArray";
import FieldNumber from "../FieldNumber";
import FieldString from "../FieldString";
import FieldUrl from "../FieldUrl";
import FieldSelect from "../FieldSelect";
import FieldEnum from "../FieldEnum";
import FieldColor from "../FieldColor";
import Modal from "./Modal";
import FieldJson from "../FieldJson";
import Block from "../Block";
import fieldSpecAdditional from "../../libs/field-spec-additional";
import type {OnStyleChangedCallback, StyleSpecificationWithId} from "../../libs/definitions";
type ModalSettingsInternalProps = {
@@ -206,7 +206,6 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
<FieldNumber
label={t("Zoom")}
data-wd-key="modal:settings.zoom"
fieldSpec={latest.$root.zoom}
value={mapStyle.zoom}
default={0}
@@ -215,7 +214,6 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
<FieldNumber
label={t("Bearing")}
data-wd-key="modal:settings.bearing"
fieldSpec={latest.$root.bearing}
value={mapStyle.bearing}
default={latest.$root.bearing.default}
@@ -224,7 +222,6 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
<FieldNumber
label={t("Pitch")}
data-wd-key="modal:settings.pitch"
fieldSpec={latest.$root.pitch}
value={mapStyle.pitch}
default={latest.$root.pitch.default}
@@ -251,7 +248,6 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
<FieldNumber
label={t("Light intensity")}
data-wd-key="modal:settings.light-intensity"
fieldSpec={latest.light.intensity}
value={light.intensity as number}
default={latest.light.intensity.default}
@@ -278,7 +274,6 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
<FieldNumber
label={t("Terrain exaggeration")}
data-wd-key="modal:settings.terrain-exaggeration"
fieldSpec={latest.terrain.exaggeration}
value={terrain.exaggeration}
default={latest.terrain.exaggeration.default}
@@ -287,7 +282,6 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
<FieldNumber
label={t("Transition delay")}
data-wd-key="modal:settings.transition-delay"
fieldSpec={latest.transition.delay}
value={transition.delay}
default={latest.transition.delay.default}
@@ -296,7 +290,6 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
<FieldNumber
label={t("Transition duration")}
data-wd-key="modal:settings.transition-duration"
fieldSpec={latest.transition.duration}
value={transition.duration}
default={latest.transition.duration.default}
@@ -332,4 +325,5 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
}
}
export const ModalSettings = withTranslation()(ModalSettingsInternal);
const ModalSettings = withTranslation()(ModalSettingsInternal);
export default ModalSettings;
+3 -2
View File
@@ -1,7 +1,7 @@
import React from "react";
import { Trans, type WithTranslation, withTranslation } from "react-i18next";
import { Modal } from "./Modal";
import Modal from "./Modal";
type ModalShortcutsInternalProps = {
@@ -134,4 +134,5 @@ class ModalShortcutsInternal extends React.Component<ModalShortcutsInternalProps
}
}
export const ModalShortcuts = withTranslation()(ModalShortcutsInternal);
const ModalShortcuts = withTranslation()(ModalShortcutsInternal);
export default ModalShortcuts;
+9 -8
View File
@@ -4,13 +4,13 @@ import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import type {GeoJSONSourceSpecification, RasterDEMSourceSpecification, RasterSourceSpecification, SourceSpecification, VectorSourceSpecification} from "maplibre-gl";
import { type WithTranslation, withTranslation } from "react-i18next";
import { Modal } from "./Modal";
import { InputButton } from "../InputButton";
import { FieldString } from "../FieldString";
import { FieldSelect } from "../FieldSelect";
import { ModalSourcesTypeEditor, type EditorMode } from "./ModalSourcesTypeEditor";
import Modal from "./Modal";
import InputButton from "../InputButton";
import FieldString from "../FieldString";
import FieldSelect from "../FieldSelect";
import ModalSourcesTypeEditor, { type EditorMode } from "./ModalSourcesTypeEditor";
import { generateId } from "../../libs/style";
import style from "../../libs/style";
import { deleteSource, addSource, changeSource } from "../../libs/source";
import publicSources from "../../config/tilesets.json";
import { type OnStyleChangedCallback, type StyleSpecificationWithId } from "../../libs/definitions";
@@ -121,7 +121,7 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
super(props);
this.state = {
mode: "tilejson_vector",
sourceId: generateId(),
sourceId: style.generateId(),
source: this.defaultSource("tilejson_vector"),
};
}
@@ -343,4 +343,5 @@ class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {
}
}
export const ModalSources = withTranslation()(ModalSourcesInternal);
const ModalSources = withTranslation()(ModalSourcesInternal);
export default ModalSources;
@@ -3,14 +3,14 @@ import {latest} from "@maplibre/maplibre-gl-style-spec";
import { type WithTranslation, withTranslation } from "react-i18next";
import { type TFunction } from "i18next";
import { Block } from "../Block";
import { FieldUrl } from "../FieldUrl";
import { FieldNumber } from "../FieldNumber";
import { FieldSelect } from "../FieldSelect";
import { FieldDynamicArray } from "../FieldDynamicArray";
import { FieldArray } from "../FieldArray";
import { FieldJson } from "../FieldJson";
import { FieldCheckbox } from "../FieldCheckbox";
import Block from "../Block";
import FieldUrl from "../FieldUrl";
import FieldNumber from "../FieldNumber";
import FieldSelect from "../FieldSelect";
import FieldDynamicArray from "../FieldDynamicArray";
import FieldArray from "../FieldArray";
import FieldJson from "../FieldJson";
import FieldCheckbox from "../FieldCheckbox";
export type EditorMode = "video" | "image" | "tilejson_vector" | "tile_raster" | "tilejson_raster" | "tilexyz_raster-dem" | "tilejson_raster-dem" | "pmtiles_vector" | "tile_vector" | "geojson_url" | "geojson_json" | null;
@@ -375,4 +375,5 @@ class ModalSourcesTypeEditorInternal extends React.Component<ModalSourcesTypeEdi
}
}
export const ModalSourcesTypeEditor = withTranslation()(ModalSourcesTypeEditorInternal);
const ModalSourcesTypeEditor = withTranslation()(ModalSourcesTypeEditorInternal);
export default ModalSourcesTypeEditor;
+1 -1
View File
@@ -42,4 +42,4 @@ i18n
}
});
export { i18n };
export default i18n;
+1 -1
View File
@@ -4,7 +4,7 @@ import { createRoot } from "react-dom/client";
import "./favicon.ico";
import "./styles/index.scss";
import "./i18n";
import { App } from "./components/App";
import App from "./components/App";
const root = createRoot(document.querySelector("#app"));
root.render(
+1 -1
View File
@@ -119,7 +119,7 @@ function createMaplibreExpressionLinter(spec?: StylePropertySpecification) {
const text = view.state.doc.toString();
const parsedJson = JSON.parse(text);
const ast = jsonToAst(text);
const out = expression.createExpression(parsedJson, "expression", spec);
const out = expression.createExpression(parsedJson, spec);
if (out?.result !== "error") {
return [];
}

Some files were not shown because too many files have changed in this diff Show More