mirror of
https://github.com/maputnik/editor.git
synced 2026-08-03 03:37:25 +00:00
Compare commits
2 Commits
v3.1.0
..
285dca8d2c
| Author | SHA1 | Date | |
|---|---|---|---|
| 285dca8d2c | |||
| 2a3b6aa770 |
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2020": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react/jsx-runtime",
|
||||
"plugin:react-hooks/recommended",
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"dist"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"settings": {
|
||||
"react": { "version": "16.4" }
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"react-refresh"],
|
||||
"rules": {
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ "allowConstantExport": true }
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{ "argsIgnorePattern": "^_" }
|
||||
],
|
||||
"no-unused-vars": "off",
|
||||
"react/prop-types": ["off"],
|
||||
// Disable no-undef. It's covered by @typescript-eslint
|
||||
"no-undef": "off",
|
||||
"indent": ["error", 2],
|
||||
"no-var": ["error"]
|
||||
},
|
||||
"globals": {
|
||||
"global": "readonly"
|
||||
}
|
||||
}
|
||||
@@ -8,3 +8,4 @@ assignees: ''
|
||||
---
|
||||
|
||||
<!-- Thanks for reaching out! If you are having general Maputnik mapping questions, please asking them at https://gis.stackexchange.com/ using the 'maputnik' tag https://gis.stackexchange.com/questions/tagged/maputnik and read https://gis.stackexchange.com/help/how-to-ask before you do so (please keep in mind that you're asking there in a general GIS forum, not a dedicated support channel) -->
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
## Launch Checklist
|
||||
|
||||
<!-- Thanks for the PR! Feel free to add or remove items from the checklist. -->
|
||||
|
||||
|
||||
- [ ] Briefly describe the changes in this PR.
|
||||
- [ ] Link to related issues.
|
||||
- [ ] Include before/after visuals or gifs if this PR includes visual changes.
|
||||
- [ ] Write tests for all new functionality.
|
||||
- [ ] Add an entry to `CHANGELOG.md` under the `## main` section.
|
||||
+5
-30
@@ -1,39 +1,14 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 20
|
||||
open-pull-requests-limit: 2
|
||||
versioning-strategy: increase
|
||||
groups:
|
||||
vitest:
|
||||
patterns:
|
||||
- "*vitest*"
|
||||
react:
|
||||
patterns:
|
||||
- "*react*"
|
||||
cooldown:
|
||||
default-days: 5
|
||||
semver-major-days: 5
|
||||
semver-minor-days: 3
|
||||
semver-patch-days: 3
|
||||
include:
|
||||
- "*"
|
||||
exclude:
|
||||
- "@maplibre/*"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
cooldown:
|
||||
default-days: 3
|
||||
# no semver support for github-actions
|
||||
# => no specific configuration for this
|
||||
include:
|
||||
- "*"
|
||||
open-pull-requests-limit: 2
|
||||
versioning-strategy: increase
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
name: Automerge Dependabot
|
||||
|
||||
on: pull_request
|
||||
|
||||
permissions: write-all
|
||||
|
||||
jobs:
|
||||
dependabot:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }}
|
||||
steps:
|
||||
- name: Dependabot metadata
|
||||
id: metadata
|
||||
uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
|
||||
with:
|
||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
- name: Approve Dependabot PRs
|
||||
run: gh pr review --approve "$PR_URL"
|
||||
env:
|
||||
PR_URL: ${{github.event.pull_request.html_url}}
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
- name: Enable auto-merge for Dependabot PRs
|
||||
run: gh pr merge --auto --squash "$PR_URL"
|
||||
env:
|
||||
PR_URL: ${{github.event.pull_request.html_url}}
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
+71
-106
@@ -7,148 +7,113 @@ on:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
build-docker:
|
||||
name: build docker
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: docker build -t test-docker-image-build .
|
||||
|
||||
# build the editor
|
||||
build-node:
|
||||
name: "build on ${{ matrix.os }}"
|
||||
runs-on: ${{ matrix.os }}
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with: { persist-credentials: false }
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npm run lint
|
||||
- run: npm run lint-css
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npm run lint
|
||||
- run: npm run lint-css
|
||||
|
||||
|
||||
|
||||
build-artifacts:
|
||||
name: "build artifacts"
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with: { persist-credentials: false }
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- name: artifacts/maputnik
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: maputnik
|
||||
path: dist
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- name: artifacts/maputnik
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: maputnik
|
||||
path: dist
|
||||
|
||||
# Build and upload desktop CLI artifacts
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version: ^1.23.x
|
||||
cache-dependency-path: desktop/go.sum
|
||||
id: go
|
||||
# Build and upload desktop CLI artifacts
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ^1.19.x
|
||||
id: go
|
||||
|
||||
- name: Build desktop artifacts
|
||||
run: npm run build-desktop
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: maputnik/desktop
|
||||
ref: master
|
||||
path: ./src/github.com/maputnik/desktop/
|
||||
|
||||
- name: Artifacts/linux
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: maputnik-linux
|
||||
path: ./desktop/bin/linux/
|
||||
- name: Make
|
||||
run: cd src/github.com/maputnik/desktop/ && make
|
||||
|
||||
- name: Artifacts/darwin
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: maputnik-darwin
|
||||
path: ./desktop/bin/darwin/
|
||||
- name: Artifacts/linux
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: maputnik-linux
|
||||
path: ./src/github.com/maputnik/desktop/bin/linux/
|
||||
|
||||
- name: Artifacts/windows
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: maputnik-windows
|
||||
path: ./desktop/bin/windows/
|
||||
|
||||
unit-tests:
|
||||
name: "Unit tests"
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with: { persist-credentials: false }
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- run: npm ci
|
||||
- run: npm run test-unit-ci
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
with:
|
||||
files: ${{ github.workspace }}/coverage/coverage-final.json
|
||||
verbose: true
|
||||
- name: Artifacts/darwin
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: maputnik-darwin
|
||||
path: ./src/github.com/maputnik/desktop/bin/darwin/
|
||||
|
||||
- name: Artifacts/windows
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: maputnik-windows
|
||||
path: ./src/github.com/maputnik/desktop/bin/windows/
|
||||
|
||||
e2e-tests:
|
||||
name: "E2E tests using chrome"
|
||||
name: "E2E tests using ${{ matrix.browser }}"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
browser: [chrome, firefox]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with: { persist-credentials: false }
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
uses: actions/checkout@v4
|
||||
- run: npm ci
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@fa4a118725a8f001170d49631ea89e5d66fee626 # v7.4.1
|
||||
uses: cypress-io/github-action@v6
|
||||
with:
|
||||
build: npm run build
|
||||
start: npm run start
|
||||
browser: chrome
|
||||
browser: ${{ matrix.browser }}
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
with:
|
||||
files: ${{ github.workspace }}/.nyc_output/out.json
|
||||
verbose: true
|
||||
|
||||
e2e-tests-docker:
|
||||
name: "E2E tests using chrome and docker"
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with: { persist-credentials: false }
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- run: npm ci
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@fa4a118725a8f001170d49631ea89e5d66fee626 # v7.4.1
|
||||
with:
|
||||
build: docker build -t maputnik .
|
||||
start: docker run --rm --network host maputnik --port=8888
|
||||
browser: chrome
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: ${{ github.workspace }}/.nyc_output/out.json
|
||||
verbose: true
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main ]
|
||||
schedule:
|
||||
- cron: '17 0 * * 6'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
@@ -1,39 +0,0 @@
|
||||
name: Create bump version PR
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: Version to change to.
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
bump-version-pr:
|
||||
name: Bump version PR
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: main
|
||||
|
||||
- name: Use Node.js from nvmrc
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
- name: Bump version
|
||||
run: |
|
||||
npm version --commit-hooks false --git-tag-version false ${{ inputs.version }}
|
||||
./build/bump-version-changelog.js ${{ inputs.version }}
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||
with:
|
||||
commit-message: Bump version to ${{ inputs.version }}
|
||||
branch: bump-version-to-${{ inputs.version }}
|
||||
title: Bump version to ${{ inputs.version }}
|
||||
@@ -8,48 +8,44 @@ jobs:
|
||||
deploy-pages:
|
||||
name: deploy/pages
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with: { persist-credentials: false }
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js from nvmrc
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Install
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Upload to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4.1.0
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: dist
|
||||
|
||||
|
||||
# publish docker to GitHub registry
|
||||
deploy-docker:
|
||||
name: deploy/docker
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- run: docker build -t ghcr.io/maplibre/maputnik:main .
|
||||
- run: docker push ghcr.io/maplibre/maputnik:main
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: actions/checkout@v4
|
||||
- run: docker build -t ghcr.io/maplibre/maputnik:main .
|
||||
- run: docker push ghcr.io/maplibre/maputnik:main
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release-check:
|
||||
name: Check if version changed
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- 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@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
- name: Check if version has been updated
|
||||
id: check
|
||||
uses: EndBug/version-check@095362f3cd50f690c8fa0e6afeea81834bd8d320 # latest
|
||||
|
||||
outputs:
|
||||
publish: ${{ steps.check.outputs.changed }}
|
||||
|
||||
release:
|
||||
name: Release
|
||||
needs: release-check
|
||||
if: ${{ needs.release-check.outputs.publish == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: main
|
||||
|
||||
- name: Use Node.js from nvmrc
|
||||
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@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version: ^1.23.x
|
||||
cache-dependency-path: desktop/go.sum
|
||||
id: go
|
||||
|
||||
- name: Get version
|
||||
id: package-version
|
||||
uses: martinbeentjes/npm-get-version-action@3cf273023a0dda27efcd3164bdfb51908dd46a5b # v1.3.1
|
||||
|
||||
- name: Install
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
npm run build-desktop
|
||||
|
||||
- name: Tag commit and push
|
||||
id: tag_version
|
||||
uses: mathieudutour/github-tag-action@a22cf08638b34d5badda920f9daf6e72c477b07b # v6.2
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
custom_tag: ${{ steps.package-version.outputs.current-version }}
|
||||
|
||||
- name: Create Archives
|
||||
run: |
|
||||
zip -r "desktop-${{ steps.package-version.outputs.current-version }}" desktop/bin/
|
||||
|
||||
- name: Build Release Notes
|
||||
id: release_notes
|
||||
run: |
|
||||
RELEASE_NOTES_PATH="${PWD}/release_notes.txt"
|
||||
./build/release-notes.js > ${RELEASE_NOTES_PATH}
|
||||
echo "release_notes=${RELEASE_NOTES_PATH}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create GitHub Release
|
||||
id: create_regular_release
|
||||
uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag: ${{ steps.tag_version.outputs.new_tag }}
|
||||
name: ${{ steps.tag_version.outputs.new_tag }}
|
||||
bodyFile: ${{ steps.release_notes.outputs.release_notes }}
|
||||
artifacts: "desktop-${{ steps.package-version.outputs.current-version }}.zip"
|
||||
allowUpdates: true
|
||||
draft: false
|
||||
prerelease: false
|
||||
+1
-9
@@ -33,14 +33,6 @@ node_modules
|
||||
public
|
||||
/errorShots
|
||||
/old
|
||||
/build
|
||||
/cypress/screenshots
|
||||
/dist/
|
||||
/desktop/version.go
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Window metadata files
|
||||
/desktop/winres/winres.json
|
||||
/desktop/*.syso
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
|
||||
ci:
|
||||
autoupdate_schedule: monthly
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: check-executables-have-shebangs
|
||||
- id: check-json
|
||||
exclude: 'tsconfig(\.node)?\.json'
|
||||
- id: check-shebang-scripts-are-executable
|
||||
- id: check-symlinks
|
||||
- id: check-toml
|
||||
- id: check-yaml
|
||||
args: [ --allow-multiple-documents ]
|
||||
- id: destroyed-symlinks
|
||||
- id: end-of-file-fixer
|
||||
- id: mixed-line-ending
|
||||
args: [ --fix=lf ]
|
||||
- id: trailing-whitespace
|
||||
@@ -1,47 +0,0 @@
|
||||
Maputnik is a MapLibre style editor written using React and TypeScript.
|
||||
|
||||
To get started, install all npm packages:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
Verify code correctness by running ESLint:
|
||||
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
Or try fixing lint issues with:
|
||||
|
||||
```
|
||||
npm run lint -- --fix
|
||||
```
|
||||
|
||||
The project type checked and built with:
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
To run the tests make sure that xvfb is installed:
|
||||
|
||||
```
|
||||
apt install xvfb
|
||||
```
|
||||
|
||||
Run the development server in the background with Vite:
|
||||
|
||||
```
|
||||
nohup npm run start &
|
||||
```
|
||||
|
||||
Then start the Cypress tests with:
|
||||
|
||||
```
|
||||
xvfb-run -a npm run test
|
||||
```
|
||||
|
||||
## Pull Requests
|
||||
|
||||
- Pull requests should update `CHANGELOG.md` with a short description of the change.
|
||||
@@ -1,84 +0,0 @@
|
||||
## main
|
||||
|
||||
### ✨ Features and improvements
|
||||
- _...Add new stuff here..._
|
||||
|
||||
### 🐞 Bug fixes
|
||||
- _...Add new stuff here..._
|
||||
|
||||
## 3.1.0
|
||||
|
||||
### ✨ Features and improvements
|
||||
- Added translation to "Links" in debug modal
|
||||
- Add support for hillshade's color arrays and relief-color elevation expression
|
||||
- Change layers icons to make them a bit more distinct
|
||||
- Remove `@mdi` packages in favor of `react-icons`
|
||||
- Add ability to control the projection of the map - either globe or mercator
|
||||
- Add markdown support for doc related to the style-spec fields
|
||||
- Added global state modal to allow editing the global state
|
||||
- Added color highlight for problematic properties
|
||||
- Upgraded codemirror from version 5 to version 6
|
||||
- Add code editor to allow editing the entire style
|
||||
- Add support for sprite object in setting modal
|
||||
- Set the correct map view when opening a new style on an empty map
|
||||
- Allow root-relative urls in the stylefile
|
||||
|
||||
### 🐞 Bug fixes
|
||||
|
||||
- Improved the local style open modal and local file upload.
|
||||
- Fixed the Expression editor (for long expressions) being able to be float under other components further down
|
||||
- Fixed an issue when clicking on a popup and then clicking on the map again
|
||||
- Fix modal close button position
|
||||
- Fixed an issue with the generation of translations
|
||||
- Fix missing spec info when clicking next to a property
|
||||
- Fix Firefox open file that stopped working due to react upgrade
|
||||
- Fix issue with missing bottom error panel
|
||||
- Fixed headers in left panes (Layers list and Layer editor) to remain visible when scrolling
|
||||
- Fix error when using a source from localhost
|
||||
- Fix an issue with scrolling when using the code editor
|
||||
|
||||
## 3.0.0
|
||||
|
||||
### ✨ Features and improvements
|
||||
- Fix radio/delete filter buttons styling regression
|
||||
- Add german translation
|
||||
- Use same version number for web and desktop versions
|
||||
- Add scheme type options for vector/raster tile
|
||||
- Add `tileSize` field for raster and raster-dem tile sources
|
||||
- Update Protomaps Light gallery style to v4
|
||||
- Add support to edit local files on the file system if supported by the browser
|
||||
- Upgrade to MapLibre LG JS v5
|
||||
- Upgrade Vite 6 and Cypress 14 ([#970](https://github.com/maplibre/maputnik/pull/970))
|
||||
- Upgrade OpenLayers from v6 to v10
|
||||
- When loading a style into localStorage that causes a QuotaExceededError, purge localStorage and retry
|
||||
- Remove react-autobind dependency
|
||||
- Remove usage of legacy `childContextTypes` API
|
||||
- Refactor Field components to use arrow function syntax
|
||||
- Replace react-autocomplete with Downshift in the autocomplete component
|
||||
- Add LocationIQ as supported map provider with access token field and gallery style
|
||||
- Use maputnik go binary for the docker image to allow file watching
|
||||
- Revmove support for `debug` and `localport` url parameters
|
||||
- Replace react-sortable-hoc with dnd-kit to avoid react console warnings and also use a maintained library
|
||||
|
||||
### 🐞 Bug fixes
|
||||
|
||||
- Fix incorrect handing of network error response (#944)
|
||||
- Show an error when adding a layer with a duplicate ID
|
||||
- Replace deprecated `ReactDOM.render` usage with `createRoot` and drop the
|
||||
`DOMNodeRemoved` cleanup hack
|
||||
|
||||
## 2.1.1
|
||||
|
||||
### ✨ Features and improvements
|
||||
|
||||
- Add GitHub workflows for releasing new versions
|
||||
- Update desktop build to pull from this repo (#922)
|
||||
|
||||
## 2.0.0
|
||||
|
||||
- Update MapLibre to version 4 (#872)
|
||||
- Start continuous deployment of maputnik website
|
||||
|
||||
## 1.7.0
|
||||
|
||||
- See release notes at https://maputnik.github.io/blog/2020/04/23/release-v1.7.0
|
||||
+10
-8
@@ -1,14 +1,16 @@
|
||||
FROM golang:1.23-alpine AS builder
|
||||
FROM node:18 as builder
|
||||
WORKDIR /maputnik
|
||||
|
||||
RUN apk add --no-cache nodejs npm make git gcc g++ libc-dev
|
||||
# Only copy package.json to prevent npm install from running on every build
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm install
|
||||
|
||||
# Build maputnik
|
||||
COPY . .
|
||||
RUN npm ci
|
||||
RUN CGO_ENABLED=1 GOOS=linux npm run build-linux
|
||||
RUN npx vite build
|
||||
|
||||
FROM alpine:latest
|
||||
WORKDIR /app
|
||||
COPY --from=builder /maputnik/desktop/bin/linux ./
|
||||
ENTRYPOINT ["/app/maputnik"]
|
||||
#---------------------------------------------------------------------------
|
||||
# Create a clean nginx-alpine slim image with just the build results
|
||||
FROM nginx:alpine-slim
|
||||
|
||||
COPY --from=builder /maputnik/dist /usr/share/nginx/html/
|
||||
|
||||
@@ -20,3 +20,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
@@ -18,15 +18,9 @@ targeted at developers and map designers.
|
||||
- In a Docker, run this command and browse to http://localhost:8888, Ctrl+C to stop the server.
|
||||
|
||||
```bash
|
||||
docker run -it --rm -p 8888:8000 ghcr.io/maplibre/maputnik:main
|
||||
docker run -it --rm -p 8888:8888 ghcr.io/maplibre/maputnik:main
|
||||
```
|
||||
|
||||
To see the CLI options (for example file watching or style serving) run:
|
||||
```bash
|
||||
docker run -it --rm -p 8888:8000 ghcr.io/maplibre/maputnik:main --help
|
||||
```
|
||||
You might need to mount a volume (`-v`) to be able to use these options.
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation can be found in the [Wiki](https://github.com/maplibre/maputnik/wiki). You are welcome to collaborate!
|
||||
@@ -42,8 +36,6 @@ Maputnik is written in typescript and is using [React](https://github.com/facebo
|
||||
|
||||
We ensure building and developing Maputnik works with the [current active LTS Node.js version and above](https://github.com/nodejs/Release#release-schedule).
|
||||
|
||||
Check out our [Internationalization guide](./src/locales/README.md) for UI text related changes.
|
||||
|
||||
### Getting Involved
|
||||
Join the #maplibre or #maputnik slack channel at OSMUS: get an invite at https://slack.openstreetmap.us/ Read the the below guide in order to get familiar with how we do things around here.
|
||||
|
||||
@@ -74,14 +66,14 @@ Lint the JavaScript code.
|
||||
```
|
||||
# run linter
|
||||
npm run lint
|
||||
npm run lint-css
|
||||
npm run sort-styles
|
||||
npm run lint-styles
|
||||
```
|
||||
|
||||
|
||||
## Tests
|
||||
For E2E testing we use [Cypress](https://www.cypress.io/)
|
||||
|
||||
[Cypress](https://www.cypress.io/) doesn't start a server so you'll need to start one manually by running `npm run start`.
|
||||
[Cypress](https://www.cypress.io/) doesn't starts a server so you'll need to start one manually by running `npm run start`.
|
||||
|
||||
Now open a terminal and run the following using *chrome*:
|
||||
|
||||
@@ -101,15 +93,10 @@ You can also see the tests as they run or select which suites to run by executin
|
||||
npm run cy:open
|
||||
```
|
||||
|
||||
## Release process
|
||||
|
||||
1. Review [`CHANGELOG.md`](/CHANGELOG.md)
|
||||
- Double-check that all changes included in the release are appropriately documented.
|
||||
- To-be-released changes should be under the "main" header.
|
||||
- Commit any final changes to the changelog.
|
||||
2. Run [Create bump version PR](https://github.com/maplibre/maputnik/actions/workflows/create-bump-version-pr.yml) by manual workflow dispatch and set the version number in the input. This will create a PR that changes the changelog and `package.json` file to review and merge.
|
||||
3. Once merged, an automatic process will kick in and creates a GitHub release and uploads release assets.
|
||||
## Related Projects
|
||||
|
||||
- [maputnik-dev-server](https://github.com/nycplanning/labs-maputnik-dev-server) - An express.js server that allows for quickly loading the style from any mapboxGL map into mapuntnik.
|
||||
|
||||
## Sponsors
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
# Build Scripts
|
||||
|
||||
This folder holds common build scripts used by some of the Github workflows.
|
||||
|
||||
The scripts are borrowed from [maplibre/maplibre-gl-js](https://github.com/maplibre/maplibre-gl-js/tree/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build).
|
||||
|
||||
## Generate Release Notes
|
||||
|
||||
`bump-version-changelog.js` Used to update the changelog with the current notes, and set up a space for new notes
|
||||
|
||||
`release-notes.js` Used to generate release notes when releasing a new version
|
||||
@@ -1,29 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* This script updates the changelog.md file with the version given in the arguments
|
||||
* It replaces ## main with ## <version>
|
||||
* Removes _...Add new stuff here..._
|
||||
* And adds on top a ## main with add stuff here.
|
||||
*
|
||||
* Copied from maplibre/maplibre-gl-js
|
||||
* https://github.com/maplibre/maplibre-gl-js/blob/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build/release-notes.js
|
||||
*/
|
||||
|
||||
import * as fs from "fs";
|
||||
|
||||
const changelogPath = "CHANGELOG.md";
|
||||
let changelog = fs.readFileSync(changelogPath, "utf8");
|
||||
changelog = changelog.replace("## main", `## ${process.argv[2]}`);
|
||||
changelog = changelog.replaceAll("- _...Add new stuff here..._\n", "");
|
||||
changelog = `## main
|
||||
|
||||
### ✨ Features and improvements
|
||||
- _...Add new stuff here..._
|
||||
|
||||
### 🐞 Bug fixes
|
||||
- _...Add new stuff here..._
|
||||
|
||||
` + changelog;
|
||||
|
||||
fs.writeFileSync(changelogPath, changelog, "utf8");
|
||||
@@ -1,47 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Copied from maplibre/maplibre-gl-js
|
||||
// https://github.com/maplibre/maplibre-gl-js/blob/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build/release-notes.js
|
||||
|
||||
import * as fs from "fs";
|
||||
|
||||
const changelogPath = "CHANGELOG.md";
|
||||
const changelog = fs.readFileSync(changelogPath, "utf8");
|
||||
|
||||
/*
|
||||
Parse the raw changelog text and split it into individual releases.
|
||||
|
||||
This regular expression:
|
||||
- Matches lines starting with "## x.x.x".
|
||||
- Groups the version number.
|
||||
- Skips the (optional) release date.
|
||||
- Groups the changelog content.
|
||||
- Ends when another "## x.x.x" is found.
|
||||
*/
|
||||
const regex = /^## (\d+\.\d+\.\d+.*?)\n(.+?)(?=\n^## \d+\.\d+\.\d+.*?\n)/gms;
|
||||
|
||||
const releaseNotes = [];
|
||||
let match;
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
while (match = regex.exec(changelog)) {
|
||||
releaseNotes.push({
|
||||
"version": match[1],
|
||||
"changelog": match[2].trim(),
|
||||
});
|
||||
}
|
||||
|
||||
const latest = releaseNotes[0];
|
||||
const previous = releaseNotes[1];
|
||||
|
||||
// Print the release notes template.
|
||||
|
||||
let header = "Changes since previous version";
|
||||
if (previous) {
|
||||
header = `https://github.com/maplibre/maputnik
|
||||
[Changes](https://github.com/maplibre/maputnik/compare/v${previous.version}...v${latest.version}) since [Maputnik v${previous.version}](https://github.com/maplibre/maputnik/releases/tag/v${previous.version})`;
|
||||
}
|
||||
const templatedReleaseNotes = `${header}
|
||||
|
||||
${latest.changelog}`;
|
||||
|
||||
process.stdout.write(templatedReleaseNotes.trimEnd());
|
||||
@@ -8,34 +8,16 @@ export default defineConfig({
|
||||
exclude: "cypress/**/*.*",
|
||||
},
|
||||
},
|
||||
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
// implement node event listeners here
|
||||
require("@cypress/code-coverage/task")(on, config);
|
||||
on("before:browser:launch", (browser, launchOptions) => {
|
||||
if (browser.family !== "chromium") {
|
||||
return;
|
||||
}
|
||||
launchOptions.args.push("--disable-gpu");
|
||||
launchOptions.args.push("--enable-features=AllowSwiftShaderFallback,AllowSoftwareGLFallbackDueToCrashes");
|
||||
launchOptions.args.push("--enable-unsafe-swiftshader");
|
||||
return launchOptions;
|
||||
});
|
||||
return config;
|
||||
},
|
||||
baseUrl: "http://localhost:8888",
|
||||
scrollBehavior: "center",
|
||||
retries: {
|
||||
runMode: 2,
|
||||
openMode: 0,
|
||||
},
|
||||
},
|
||||
|
||||
component: {
|
||||
devServer: {
|
||||
framework: "react",
|
||||
bundler: "vite",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
|
||||
describe("accessibility", () => {
|
||||
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
||||
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
|
||||
describe("skip links", () => {
|
||||
@@ -18,10 +18,10 @@ describe("accessibility", () => {
|
||||
then(get.skipTargetLayerList()).shouldBeFocused();
|
||||
});
|
||||
|
||||
it("skip link to layer editor", () => {
|
||||
// This fails for some reason only in Chrome, but passes in firefox. Adding a skip here to allow merge and later on we'll decide if we want to fix this or not.
|
||||
it.skip("skip link to layer editor", () => {
|
||||
const selector = "root:skip:layer-editor";
|
||||
then(get.elementByTestId(selector)).shouldExist();
|
||||
then(get.elementByTestId("skip-target-layer-editor")).shouldExist();
|
||||
when.tab().tab();
|
||||
then(get.elementByTestId(selector)).shouldBeFocused();
|
||||
when.click(selector);
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
|
||||
describe("code editor", () => {
|
||||
const { beforeAndAfter, when, get, then } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
|
||||
it("open code editor", () => {
|
||||
when.click("nav:code-editor");
|
||||
then(get.element(".maputnik-code-editor")).shouldExist();
|
||||
});
|
||||
|
||||
it("closes code editor", () => {
|
||||
when.click("nav:code-editor");
|
||||
then(get.element(".maputnik-code-editor")).shouldExist();
|
||||
when.click("nav:code-editor");
|
||||
then(get.element(".maputnik-code-editor")).shouldNotExist();
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
|
||||
describe("history", () => {
|
||||
const { beforeAndAfter, when, get, then } = new MaputnikDriver();
|
||||
let { beforeAndAfter, when, get, then } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
|
||||
let undoKeyCombo: string;
|
||||
@@ -16,6 +16,7 @@ describe("history", () => {
|
||||
it("undo/redo", () => {
|
||||
when.setStyle("geojson");
|
||||
when.modal.open();
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
|
||||
|
||||
when.modal.fillLayers({
|
||||
id: "step 1",
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
|
||||
describe("i18n", () => {
|
||||
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
|
||||
describe("language detector", () => {
|
||||
it("English", () => {
|
||||
const url = "?lng=en";
|
||||
when.visit(url);
|
||||
then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("en");
|
||||
});
|
||||
|
||||
it("Japanese", () => {
|
||||
const url = "?lng=ja";
|
||||
when.visit(url);
|
||||
then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("ja");
|
||||
});
|
||||
});
|
||||
|
||||
describe("language switcher", () => {
|
||||
beforeEach(() => {
|
||||
when.setStyle("layer");
|
||||
});
|
||||
|
||||
it("the language switcher switches to Japanese", () => {
|
||||
const selector = "maputnik-lang-select";
|
||||
then(get.elementByTestId(selector)).shouldExist();
|
||||
when.select(selector, "ja");
|
||||
then(get.elementByTestId(selector)).shouldHaveValue("ja");
|
||||
|
||||
then(get.elementByTestId("nav:settings")).shouldHaveText("スタイル設定");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
|
||||
describe("keyboard", () => {
|
||||
const { beforeAndAfter, given, when, get, then } = new MaputnikDriver();
|
||||
let { beforeAndAfter, given, when, get, then } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
describe("shortcuts", () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -1,294 +0,0 @@
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
import { v1 as uuid } from "uuid";
|
||||
|
||||
describe("layer editor", () => {
|
||||
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
beforeEach(() => {
|
||||
when.setStyle("both");
|
||||
when.modal.open();
|
||||
});
|
||||
|
||||
function createBackground() {
|
||||
const id = uuid();
|
||||
|
||||
when.selectWithin("add-layer.layer-type", "background");
|
||||
when.setValue("add-layer.layer-id.input", "background:" + id);
|
||||
|
||||
when.click("add-layer");
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + id,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
return id;
|
||||
}
|
||||
|
||||
it("expand/collapse");
|
||||
it("id", () => {
|
||||
const bgId = createBackground();
|
||||
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
|
||||
const id = uuid();
|
||||
when.setValue("layer-editor.layer-id.input", "foobar:" + id);
|
||||
when.click("min-zoom");
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "foobar:" + id,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe("source", () => {
|
||||
it("should show error when the source is invalid", () => {
|
||||
when.modal.fillLayers({
|
||||
type: "circle",
|
||||
layer: "invalid",
|
||||
});
|
||||
then(get.element(".maputnik-input-block--error .maputnik-input-block-label")).shouldHaveCss("color", "rgb(207, 74, 74)");
|
||||
});
|
||||
});
|
||||
|
||||
describe("min-zoom", () => {
|
||||
let bgId: string;
|
||||
|
||||
beforeEach(() => {
|
||||
bgId = createBackground();
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.setValue("min-zoom.input-text", "1");
|
||||
when.click("layer-editor.layer-id");
|
||||
});
|
||||
|
||||
it("should update min-zoom in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
minzoom: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("when clicking next layer should update style on local storage", () => {
|
||||
when.type("min-zoom.input-text", "{backspace}");
|
||||
when.click("max-zoom.input-text");
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
minzoom: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("max-zoom", () => {
|
||||
let bgId: string;
|
||||
|
||||
beforeEach(() => {
|
||||
bgId = createBackground();
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.setValue("max-zoom.input-text", "1");
|
||||
when.click("layer-editor.layer-id");
|
||||
});
|
||||
|
||||
it("should update style in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
maxzoom: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("comments", () => {
|
||||
let bgId: string;
|
||||
const comment = "42";
|
||||
|
||||
beforeEach(() => {
|
||||
bgId = createBackground();
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.setValue("layer-comment.input", comment);
|
||||
when.click("layer-editor.layer-id");
|
||||
});
|
||||
|
||||
it("should update style in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
metadata: {
|
||||
"maputnik:comment": comment,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe("when unsetting", () => {
|
||||
beforeEach(() => {
|
||||
when.clear("layer-comment.input");
|
||||
when.click("min-zoom.input-text");
|
||||
});
|
||||
|
||||
it("should update style in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("color", () => {
|
||||
let bgId: string;
|
||||
beforeEach(() => {
|
||||
bgId = createBackground();
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.click("spec-field:background-color");
|
||||
});
|
||||
|
||||
it("should update style in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("opacity", () => {
|
||||
let bgId: string;
|
||||
beforeEach(() => {
|
||||
bgId = createBackground();
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.type("spec-field-input:background-opacity", "0.");
|
||||
});
|
||||
|
||||
it("should keep '.' in the input field", () => {
|
||||
then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue("0.");
|
||||
});
|
||||
|
||||
it("should revert to a valid value when focus out", () => {
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue("0");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
describe("filter", () => {
|
||||
it("expand/collapse");
|
||||
it("compound filter");
|
||||
});
|
||||
|
||||
describe("layout", () => {
|
||||
it("text-font", () => {
|
||||
when.setStyle("font");
|
||||
when.collapseGroupInLayerEditor();
|
||||
when.collapseGroupInLayerEditor(1);
|
||||
when.collapseGroupInLayerEditor(2);
|
||||
when.doWithin("spec-field:text-font", () => {
|
||||
get.element(".maputnik-autocomplete input").first().click();
|
||||
});
|
||||
then(get.element(".maputnik-autocomplete-menu-item")).shouldBeVisible();
|
||||
then(get.element(".maputnik-autocomplete-menu-item")).shouldHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("paint", () => {
|
||||
it("expand/collapse");
|
||||
it("color");
|
||||
it("pattern");
|
||||
it("opacity");
|
||||
});
|
||||
|
||||
describe("json-editor", () => {
|
||||
it("add", () => {
|
||||
const id = when.modal.fillLayers({
|
||||
type: "circle",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "circle",
|
||||
source: "example",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const sourceText = get.elementByText('"source"');
|
||||
|
||||
sourceText.click();
|
||||
sourceText.type("\"");
|
||||
|
||||
then(get.element(".cm-lint-marker-error")).shouldExist();
|
||||
});
|
||||
|
||||
|
||||
it("expand/collapse");
|
||||
it("modify");
|
||||
|
||||
it("parse error", () => {
|
||||
const bgId = createBackground();
|
||||
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.collapseGroupInLayerEditor();
|
||||
when.collapseGroupInLayerEditor(1);
|
||||
then(get.element(".cm-lint-marker-error")).shouldNotExist();
|
||||
|
||||
when.appendTextInJsonEditor(
|
||||
"\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013 {"
|
||||
);
|
||||
then(get.element(".cm-lint-marker-error")).shouldExist();
|
||||
});
|
||||
});
|
||||
|
||||
describe("sticky header", () => {
|
||||
it("should keep layer header visible when scrolling properties", () => {
|
||||
// Setup: Create a layer with many properties (e.g., symbol layer)
|
||||
when.modal.fillLayers({
|
||||
type: "symbol",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
when.wait(500);
|
||||
const header = get.elementByTestId("layer-editor.header");
|
||||
then(header).shouldBeVisible();
|
||||
|
||||
get.element(".maputnik-scroll-container").scrollTo("bottom", { ensureScrollable: false });
|
||||
when.wait(200);
|
||||
|
||||
then(header).shouldBeVisible();
|
||||
then(get.elementByTestId("skip-target-layer-editor")).shouldBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,541 +0,0 @@
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
|
||||
describe("layers list", () => {
|
||||
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
beforeEach(() => {
|
||||
when.setStyle("both");
|
||||
when.modal.open();
|
||||
});
|
||||
|
||||
describe("ops", () => {
|
||||
let id: string;
|
||||
beforeEach(() => {
|
||||
id = when.modal.fillLayers({
|
||||
type: "background",
|
||||
});
|
||||
});
|
||||
|
||||
it("should update layers in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe("when clicking delete", () => {
|
||||
beforeEach(() => {
|
||||
when.click("layer-list-item:" + id + ":delete");
|
||||
});
|
||||
it("should empty layers in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when clicking duplicate", () => {
|
||||
beforeEach(() => {
|
||||
when.click("layer-list-item:" + id + ":copy");
|
||||
});
|
||||
it("should add copy layer in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id + "-copy",
|
||||
type: "background",
|
||||
},
|
||||
{
|
||||
id: id,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when clicking hide", () => {
|
||||
beforeEach(() => {
|
||||
when.click("layer-list-item:" + id + ":toggle-visibility");
|
||||
});
|
||||
|
||||
it("should update visibility to none in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "background",
|
||||
layout: {
|
||||
visibility: "none",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe("when clicking show", () => {
|
||||
beforeEach(() => {
|
||||
when.click("layer-list-item:" + id + ":toggle-visibility");
|
||||
});
|
||||
|
||||
it("should update visibility to visible in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "background",
|
||||
layout: {
|
||||
visibility: "visible",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when selecting a layer", () => {
|
||||
let secondId: string;
|
||||
beforeEach(() => {
|
||||
when.modal.open();
|
||||
secondId = when.modal.fillLayers({
|
||||
id: "second-layer",
|
||||
type: "background",
|
||||
});
|
||||
});
|
||||
it("should show the selected layer in the editor", () => {
|
||||
when.realClick("layer-list-item:" + secondId);
|
||||
then(get.elementByTestId("layer-editor.layer-id.input")).shouldHaveValue(secondId);
|
||||
when.realClick("layer-list-item:" + id);
|
||||
then(get.elementByTestId("layer-editor.layer-id.input")).shouldHaveValue(id);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("background", () => {
|
||||
it("add", () => {
|
||||
const id = when.modal.fillLayers({
|
||||
type: "background",
|
||||
});
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe("modify", () => {});
|
||||
});
|
||||
|
||||
describe("fill", () => {
|
||||
it("add", () => {
|
||||
const id = when.modal.fillLayers({
|
||||
type: "fill",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "fill",
|
||||
source: "example",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Change source
|
||||
it("change source");
|
||||
});
|
||||
|
||||
describe("line", () => {
|
||||
it("add", () => {
|
||||
const id = when.modal.fillLayers({
|
||||
type: "line",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "line",
|
||||
source: "example",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("groups", () => {
|
||||
when.modal.open();
|
||||
const id1 = when.modal.fillLayers({
|
||||
id: "aa",
|
||||
type: "line",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
when.modal.open();
|
||||
const id2 = when.modal.fillLayers({
|
||||
id: "aa-2",
|
||||
type: "line",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
when.modal.open();
|
||||
const id3 = when.modal.fillLayers({
|
||||
id: "b",
|
||||
type: "line",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
then(get.elementByTestId("layer-list-item:" + id1)).shouldBeVisible();
|
||||
then(get.elementByTestId("layer-list-item:" + id2)).shouldNotBeVisible();
|
||||
then(get.elementByTestId("layer-list-item:" + id3)).shouldBeVisible();
|
||||
when.click("layer-list-group:aa-0");
|
||||
then(get.elementByTestId("layer-list-item:" + id1)).shouldBeVisible();
|
||||
then(get.elementByTestId("layer-list-item:" + id2)).shouldBeVisible();
|
||||
then(get.elementByTestId("layer-list-item:" + id3)).shouldBeVisible();
|
||||
when.click("layer-list-item:" + id2);
|
||||
when.click("skip-target-layer-editor");
|
||||
when.click("menu-move-layer-down");
|
||||
then(get.elementByTestId("layer-list-group:aa-0")).shouldNotExist();
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "aa",
|
||||
type: "line",
|
||||
source: "example",
|
||||
},
|
||||
{
|
||||
id: "b",
|
||||
type: "line",
|
||||
source: "example",
|
||||
},
|
||||
{
|
||||
id: "aa-2",
|
||||
type: "line",
|
||||
source: "example",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("symbol", () => {
|
||||
it("add", () => {
|
||||
const id = when.modal.fillLayers({
|
||||
type: "symbol",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "symbol",
|
||||
source: "example",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should show spec info when hovering and clicking single line property", () => {
|
||||
when.modal.fillLayers({
|
||||
type: "symbol",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
when.hover("spec-field-container:text-rotate");
|
||||
then(get.elementByTestId("field-doc-button-Rotate")).shouldBeVisible();
|
||||
when.click("field-doc-button-Rotate", 0);
|
||||
then(get.elementByTestId("spec-field-doc")).shouldContainText("Rotates the ");
|
||||
});
|
||||
|
||||
it("should show spec info when hovering and clicking multi line property", () => {
|
||||
when.modal.fillLayers({
|
||||
type: "symbol",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
when.hover("spec-field-container:text-offset");
|
||||
then(get.elementByTestId("field-doc-button-Offset")).shouldBeVisible();
|
||||
when.click("field-doc-button-Offset", 0);
|
||||
then(get.elementByTestId("spec-field-doc")).shouldContainText("Offset distance");
|
||||
});
|
||||
|
||||
it("should hide spec info when clicking a second time", () => {
|
||||
when.modal.fillLayers({
|
||||
type: "symbol",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
when.hover("spec-field-container:text-rotate");
|
||||
then(get.elementByTestId("field-doc-button-Rotate")).shouldBeVisible();
|
||||
when.click("field-doc-button-Rotate", 0);
|
||||
when.wait(200);
|
||||
when.click("field-doc-button-Rotate", 0);
|
||||
then(get.elementByTestId("spec-field-doc")).shouldNotBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe("raster", () => {
|
||||
it("add", () => {
|
||||
const id = when.modal.fillLayers({
|
||||
type: "raster",
|
||||
layer: "raster",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "raster",
|
||||
source: "raster",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("circle", () => {
|
||||
it("add", () => {
|
||||
const id = when.modal.fillLayers({
|
||||
type: "circle",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "circle",
|
||||
source: "example",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("fill extrusion", () => {
|
||||
it("add", () => {
|
||||
const id = when.modal.fillLayers({
|
||||
type: "fill-extrusion",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "fill-extrusion",
|
||||
source: "example",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("hillshade", () => {
|
||||
it("add", () => {
|
||||
const id = when.modal.fillLayers({
|
||||
type: "hillshade",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "hillshade",
|
||||
source: "example",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("set hillshade illumination direction array", () => {
|
||||
const id = when.modal.fillLayers({
|
||||
type: "hillshade",
|
||||
layer: "example",
|
||||
});
|
||||
when.collapseGroupInLayerEditor();
|
||||
when.collapseGroupInLayerEditor(1);
|
||||
when.setValueToPropertyArray("spec-field:hillshade-illumination-direction", "1");
|
||||
when.addValueToPropertyArray("spec-field:hillshade-illumination-direction", "2");
|
||||
when.addValueToPropertyArray("spec-field:hillshade-illumination-direction", "3");
|
||||
when.addValueToPropertyArray("spec-field:hillshade-illumination-direction", "4");
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "hillshade",
|
||||
source: "example",
|
||||
paint: {
|
||||
"hillshade-illumination-direction": [ 1, 2, 3, 4 ]
|
||||
}
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("set hillshade highlight color array", () => {
|
||||
const id = when.modal.fillLayers({
|
||||
type: "hillshade",
|
||||
layer: "example",
|
||||
});
|
||||
when.collapseGroupInLayerEditor();
|
||||
when.setValueToPropertyArray("spec-field:hillshade-highlight-color", "blue");
|
||||
when.addValueToPropertyArray("spec-field:hillshade-highlight-color", "#00ff00");
|
||||
when.addValueToPropertyArray("spec-field:hillshade-highlight-color", "rgba(255, 255, 0, 1)");
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "hillshade",
|
||||
source: "example",
|
||||
paint: {
|
||||
"hillshade-highlight-color": [ "blue", "#00ff00", "rgba(255, 255, 0, 1)" ]
|
||||
}
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("color-relief", () => {
|
||||
it("add", () => {
|
||||
const id = when.modal.fillLayers({
|
||||
type: "color-relief",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "color-relief",
|
||||
source: "example",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("adds elevation expression when clicking the elevation button", () => {
|
||||
when.modal.fillLayers({
|
||||
type: "color-relief",
|
||||
layer: "example",
|
||||
});
|
||||
when.collapseGroupInLayerEditor();
|
||||
when.click("make-elevation-function");
|
||||
then(get.element("[data-wd-key='spec-field-container:color-relief-color'] .cm-line")).shouldBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe("groups", () => {
|
||||
it("simple", () => {
|
||||
when.setStyle("geojson");
|
||||
|
||||
when.modal.open();
|
||||
when.modal.fillLayers({
|
||||
id: "foo",
|
||||
type: "background",
|
||||
});
|
||||
|
||||
when.modal.open();
|
||||
when.modal.fillLayers({
|
||||
id: "foo_bar",
|
||||
type: "background",
|
||||
});
|
||||
|
||||
when.modal.open();
|
||||
when.modal.fillLayers({
|
||||
id: "foo_bar_baz",
|
||||
type: "background",
|
||||
});
|
||||
|
||||
then(get.elementByTestId("layer-list-item:foo")).shouldBeVisible();
|
||||
then(get.elementByTestId("layer-list-item:foo_bar")).shouldNotBeVisible();
|
||||
then(
|
||||
get.elementByTestId("layer-list-item:foo_bar_baz")
|
||||
).shouldNotBeVisible();
|
||||
when.click("layer-list-group:foo-0");
|
||||
then(get.elementByTestId("layer-list-item:foo")).shouldBeVisible();
|
||||
then(get.elementByTestId("layer-list-item:foo_bar")).shouldBeVisible();
|
||||
then(
|
||||
get.elementByTestId("layer-list-item:foo_bar_baz")
|
||||
).shouldBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe("drag and drop", () => {
|
||||
it("move layer should update local storage", () => {
|
||||
when.modal.open();
|
||||
const firstId = when.modal.fillLayers({
|
||||
id: "a",
|
||||
type: "background",
|
||||
});
|
||||
when.modal.open();
|
||||
const secondId = when.modal.fillLayers({
|
||||
id: "b",
|
||||
type: "background",
|
||||
});
|
||||
when.modal.open();
|
||||
const thirdId = when.modal.fillLayers({
|
||||
id: "c",
|
||||
type: "background",
|
||||
});
|
||||
|
||||
when.dragAndDropWithWait("layer-list-item:" + firstId, "layer-list-item:" + thirdId);
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: secondId,
|
||||
type: "background",
|
||||
},
|
||||
{
|
||||
id: thirdId,
|
||||
type: "background",
|
||||
},
|
||||
{
|
||||
id: firstId,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("sticky header", () => {
|
||||
it("should keep header visible when scrolling layer list", () => {
|
||||
// Setup: Create multiple layers to enable scrolling
|
||||
for (let i = 0; i < 20; i++) {
|
||||
when.modal.open();
|
||||
when.modal.fillLayers({
|
||||
id: `layer-${i}`,
|
||||
type: "background",
|
||||
});
|
||||
}
|
||||
|
||||
when.wait(500);
|
||||
const header = get.elementByTestId("layer-list.header");
|
||||
then(header).shouldBeVisible();
|
||||
|
||||
// Scroll the layer list container (use ensureScrollable: false to avoid flakiness)
|
||||
get.elementByTestId("layer-list").scrollTo("bottom", { ensureScrollable: false });
|
||||
when.wait(200);
|
||||
then(header).shouldBeVisible();
|
||||
then(get.elementByTestId("layer-list:add-layer")).shouldBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,478 @@
|
||||
import { v1 as uuid } from "uuid";
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
|
||||
describe("layers", () => {
|
||||
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
beforeEach(() => {
|
||||
when.setStyle("both");
|
||||
when.modal.open();
|
||||
});
|
||||
|
||||
describe("ops", () => {
|
||||
let id: string;
|
||||
beforeEach(() => {
|
||||
id = when.modal.fillLayers({
|
||||
type: "background",
|
||||
});
|
||||
});
|
||||
|
||||
it("should update layers in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe("when clicking delete", () => {
|
||||
beforeEach(() => {
|
||||
when.click("layer-list-item:" + id + ":delete");
|
||||
});
|
||||
it("should empty layers in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when clicking duplicate", () => {
|
||||
beforeEach(() => {
|
||||
when.click("layer-list-item:" + id + ":copy");
|
||||
});
|
||||
it("should add copy layer in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id + "-copy",
|
||||
type: "background",
|
||||
},
|
||||
{
|
||||
id: id,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when clicking hide", () => {
|
||||
beforeEach(() => {
|
||||
when.click("layer-list-item:" + id + ":toggle-visibility");
|
||||
});
|
||||
|
||||
it("should update visibility to none in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "background",
|
||||
layout: {
|
||||
visibility: "none",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe("when clicking show", () => {
|
||||
beforeEach(() => {
|
||||
when.click("layer-list-item:" + id + ":toggle-visibility");
|
||||
});
|
||||
|
||||
it("should update visibility to visible in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "background",
|
||||
layout: {
|
||||
visibility: "visible",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("background", () => {
|
||||
it("add", () => {
|
||||
let id = when.modal.fillLayers({
|
||||
type: "background",
|
||||
});
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe("modify", () => {
|
||||
function createBackground() {
|
||||
// Setup
|
||||
let id = uuid();
|
||||
|
||||
when.selectWithin("add-layer.layer-type", "background");
|
||||
when.setValue("add-layer.layer-id.input", "background:" + id);
|
||||
|
||||
when.click("add-layer");
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + id,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
return id;
|
||||
}
|
||||
|
||||
// ====> THESE SHOULD BE FROM THE SPEC
|
||||
describe("layer", () => {
|
||||
it("expand/collapse");
|
||||
it("id", () => {
|
||||
let bgId = createBackground();
|
||||
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
|
||||
let id = uuid();
|
||||
when.setValue("layer-editor.layer-id.input", "foobar:" + id);
|
||||
when.click("min-zoom");
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "foobar:" + id,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe("min-zoom", () => {
|
||||
let bgId: string;
|
||||
|
||||
beforeEach(() => {
|
||||
bgId = createBackground();
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.setValue("min-zoom.input-text", "1");
|
||||
when.click("layer-editor.layer-id");
|
||||
});
|
||||
|
||||
it("should update min-zoom in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
minzoom: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("when clicking next layer should update style on local storage", () => {
|
||||
when.type("min-zoom.input-text", "{backspace}");
|
||||
when.click("max-zoom.input-text");
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
minzoom: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("max-zoom", () => {
|
||||
let bgId: string;
|
||||
|
||||
beforeEach(() => {
|
||||
bgId = createBackground();
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.setValue("max-zoom.input-text", "1");
|
||||
when.click("layer-editor.layer-id");
|
||||
});
|
||||
|
||||
it("should update style in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
maxzoom: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("comments", () => {
|
||||
let bgId: string;
|
||||
let comment = "42";
|
||||
|
||||
beforeEach(() => {
|
||||
bgId = createBackground();
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.setValue("layer-comment.input", comment);
|
||||
when.click("layer-editor.layer-id");
|
||||
});
|
||||
|
||||
it("should update style in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
metadata: {
|
||||
"maputnik:comment": comment,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe("when unsetting", () => {
|
||||
beforeEach(() => {
|
||||
when.clear("layer-comment.input");
|
||||
when.click("min-zoom.input-text");
|
||||
});
|
||||
|
||||
it("should update style in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("color", () => {
|
||||
let bgId: string;
|
||||
beforeEach(() => {
|
||||
bgId = createBackground();
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.click("spec-field:background-color");
|
||||
});
|
||||
|
||||
it("should update style in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("filter", () => {
|
||||
it("expand/collapse");
|
||||
it("compound filter");
|
||||
});
|
||||
|
||||
describe("paint", () => {
|
||||
it("expand/collapse");
|
||||
it("color");
|
||||
it("pattern");
|
||||
it("opacity");
|
||||
});
|
||||
// <=====
|
||||
|
||||
describe("json-editor", () => {
|
||||
it("expand/collapse");
|
||||
it("modify");
|
||||
|
||||
// TODO
|
||||
it.skip("parse error", () => {
|
||||
let bgId = createBackground();
|
||||
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
|
||||
let errorSelector = ".CodeMirror-lint-marker-error";
|
||||
then(get.elementByTestId(errorSelector)).shouldNotExist();
|
||||
|
||||
when.click(".CodeMirror");
|
||||
when.typeKeys(
|
||||
"\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013 {"
|
||||
);
|
||||
then(get.elementByTestId(errorSelector)).shouldExist();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("fill", () => {
|
||||
it("add", () => {
|
||||
let id = when.modal.fillLayers({
|
||||
type: "fill",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "fill",
|
||||
source: "example",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Change source
|
||||
it("change source");
|
||||
});
|
||||
|
||||
describe("line", () => {
|
||||
it("add", () => {
|
||||
let id = when.modal.fillLayers({
|
||||
type: "line",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "line",
|
||||
source: "example",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("groups", () => {
|
||||
// TODO
|
||||
// Click each of the layer groups.
|
||||
});
|
||||
});
|
||||
|
||||
describe("symbol", () => {
|
||||
it("add", () => {
|
||||
let id = when.modal.fillLayers({
|
||||
type: "symbol",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "symbol",
|
||||
source: "example",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("raster", () => {
|
||||
it("add", () => {
|
||||
let id = when.modal.fillLayers({
|
||||
type: "raster",
|
||||
layer: "raster",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "raster",
|
||||
source: "raster",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("circle", () => {
|
||||
it("add", () => {
|
||||
let id = when.modal.fillLayers({
|
||||
type: "circle",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "circle",
|
||||
source: "example",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("fill extrusion", () => {
|
||||
it("add", () => {
|
||||
let id = when.modal.fillLayers({
|
||||
type: "fill-extrusion",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "fill-extrusion",
|
||||
source: "example",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("groups", () => {
|
||||
it("simple", () => {
|
||||
when.setStyle("geojson");
|
||||
|
||||
when.modal.open();
|
||||
when.modal.fillLayers({
|
||||
id: "foo",
|
||||
type: "background",
|
||||
});
|
||||
|
||||
when.modal.open();
|
||||
when.modal.fillLayers({
|
||||
id: "foo_bar",
|
||||
type: "background",
|
||||
});
|
||||
|
||||
when.modal.open();
|
||||
when.modal.fillLayers({
|
||||
id: "foo_bar_baz",
|
||||
type: "background",
|
||||
});
|
||||
|
||||
then(get.elementByTestId("layer-list-item:foo")).shouldBeVisible();
|
||||
then(get.elementByTestId("layer-list-item:foo_bar")).shouldNotBeVisible();
|
||||
then(
|
||||
get.elementByTestId("layer-list-item:foo_bar_baz")
|
||||
).shouldNotBeVisible();
|
||||
when.click("layer-list-group:foo-0");
|
||||
then(get.elementByTestId("layer-list-item:foo")).shouldBeVisible();
|
||||
then(get.elementByTestId("layer-list-item:foo_bar")).shouldBeVisible();
|
||||
then(
|
||||
get.elementByTestId("layer-list-item:foo_bar_baz")
|
||||
).shouldBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
+5
-40
@@ -1,11 +1,11 @@
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
|
||||
describe("map", () => {
|
||||
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
||||
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
describe("zoom level", () => {
|
||||
it("via url", () => {
|
||||
const zoomLevel = 12.37;
|
||||
let zoomLevel = 12.37;
|
||||
when.setStyle("geojson", zoomLevel);
|
||||
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
|
||||
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText(
|
||||
@@ -14,7 +14,7 @@ describe("map", () => {
|
||||
});
|
||||
|
||||
it("via map controls", () => {
|
||||
const zoomLevel = 12.37;
|
||||
let zoomLevel = 12.37;
|
||||
when.setStyle("geojson", zoomLevel);
|
||||
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
|
||||
when.clickZoomIn();
|
||||
@@ -22,46 +22,11 @@ describe("map", () => {
|
||||
"Zoom: " + (zoomLevel + 1)
|
||||
);
|
||||
});
|
||||
|
||||
it("via style file definition", () => {
|
||||
when.setStyle("zoom_7_center_0_51");
|
||||
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
|
||||
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText(
|
||||
"Zoom: " + (7)
|
||||
);
|
||||
then(get.locationHash().should("contain", "#7/51/0"));
|
||||
|
||||
// opening another stylefile does not update the map view again
|
||||
// as discussed in https://github.com/maplibre/maputnik/issues/1546
|
||||
when.openASecondStyleWithDifferentZoomAndCenter();
|
||||
then(get.locationHash().should("contain", "#7/51/0"));
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("search", () => {
|
||||
it("should exist", () => {
|
||||
it('should exist', () => {
|
||||
then(get.searchControl()).shouldBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe("popup", () => {
|
||||
beforeEach(() => {
|
||||
when.setStyle("rectangles");
|
||||
then(get.locationHash().should("exist"));
|
||||
});
|
||||
it("should open on feature click", () => {
|
||||
when.clickCenter("maplibre:map");
|
||||
then(get.elementByTestId("feature-layer-popup")).shouldBeVisible();
|
||||
});
|
||||
|
||||
it("should open a second feature after closing popup", () => {
|
||||
when.clickCenter("maplibre:map");
|
||||
then(get.elementByTestId("feature-layer-popup")).shouldBeVisible();
|
||||
when.closePopup();
|
||||
then(get.elementByTestId("feature-layer-popup")).shouldNotExist();
|
||||
when.clickCenter("maplibre:map");
|
||||
then(get.elementByTestId("feature-layer-popup")).shouldBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/// <reference types="cypress-real-events" />
|
||||
import { CypressHelper } from "@shellygo/cypress-test-utils";
|
||||
import "cypress-real-events/support";
|
||||
|
||||
export default class MaputnikCypressHelper {
|
||||
private helper = new CypressHelper({ defaultDataAttribute: "data-wd-key" });
|
||||
@@ -10,45 +8,10 @@ export default class MaputnikCypressHelper {
|
||||
};
|
||||
|
||||
public get = {
|
||||
locationHash: (): Cypress.Chainable<string> => cy.location("hash"),
|
||||
...this.helper.get,
|
||||
};
|
||||
|
||||
public when = {
|
||||
dragAndDropWithWait: (element: string, targetElement: string) => {
|
||||
this.helper.get.elementByTestId(element).realMouseDown({ button: "left", position: "center" });
|
||||
this.helper.get.elementByTestId(element).realMouseMove(0, 10, { position: "center" });
|
||||
this.helper.get.elementByTestId(targetElement).realMouseMove(0, 0, { position: "center" });
|
||||
this.helper.when.wait(1);
|
||||
this.helper.get.elementByTestId(targetElement).realMouseUp();
|
||||
},
|
||||
clickCenter: (element: string) => {
|
||||
this.helper.get.elementByTestId(element).realMouseDown({ button: "left", position: "center" });
|
||||
this.helper.when.wait(200);
|
||||
this.helper.get.elementByTestId(element).realMouseUp();
|
||||
},
|
||||
openFileByFixture: (fixture: string, buttonTestId: string, inputTestId: string) => {
|
||||
cy.window().then((win) => {
|
||||
const file = {
|
||||
text: cy.stub().resolves(cy.fixture(fixture).then(JSON.stringify)),
|
||||
};
|
||||
const fileHandle = {
|
||||
getFile: cy.stub().resolves(file),
|
||||
};
|
||||
if (!win.showOpenFilePicker) {
|
||||
this.helper.get.elementByTestId(inputTestId).selectFile("cypress/fixtures/" + fixture, { force: true });
|
||||
} else {
|
||||
cy.stub(win, "showOpenFilePicker").resolves([fileHandle]);
|
||||
this.helper.get.elementByTestId(buttonTestId).click();
|
||||
}
|
||||
});
|
||||
},
|
||||
dropFileByFixture: (fixture: string, dropzoneTestId: string) => {
|
||||
this.helper.get.elementByTestId(dropzoneTestId).selectFile("cypress/fixtures/" + fixture, {
|
||||
action: "drag-drop",
|
||||
force: true,
|
||||
});
|
||||
},
|
||||
...this.helper.when,
|
||||
};
|
||||
|
||||
|
||||
+27
-117
@@ -1,5 +1,3 @@
|
||||
/// <reference types="cypress-plugin-tab" />
|
||||
|
||||
import { CypressHelper } from "@shellygo/cypress-test-utils";
|
||||
import { Assertable, then } from "@shellygo/cypress-test-utils/assertable";
|
||||
import MaputnikCypressHelper from "./maputnik-cypress-helper";
|
||||
@@ -8,17 +6,15 @@ const baseUrl = "http://localhost:8888/";
|
||||
|
||||
const styleFromWindow = (win: Window) => {
|
||||
const styleId = win.localStorage.getItem("maputnik:latest_style");
|
||||
const styleItemKey = `maputnik:style:${styleId}`;
|
||||
const styleItem = win.localStorage.getItem(styleItemKey);
|
||||
if (!styleItem) throw new Error("Could not get styleItem from localStorage");
|
||||
const obj = JSON.parse(styleItem);
|
||||
const styleItem = win.localStorage.getItem(`maputnik:style:${styleId}`);
|
||||
const obj = JSON.parse(styleItem || "");
|
||||
return obj;
|
||||
};
|
||||
|
||||
export class MaputnikAssertable<T> extends Assertable<T> {
|
||||
shouldEqualToStoredStyle = () =>
|
||||
then(
|
||||
new CypressHelper().get.window().then((win: Window) => {
|
||||
new CypressHelper().get.window().then((win) => {
|
||||
const style = styleFromWindow(win);
|
||||
then(this.chainable).shouldDeepNestedInclude(style);
|
||||
})
|
||||
@@ -78,34 +74,6 @@ export class MaputnikDriver {
|
||||
fixture: "geojson-raster-style.json",
|
||||
},
|
||||
});
|
||||
this.helper.given.interceptAndMockResponse({
|
||||
method: "GET",
|
||||
url: baseUrl + "rectangles-style.json",
|
||||
response: {
|
||||
fixture: "rectangles-style.json",
|
||||
},
|
||||
});
|
||||
this.helper.given.interceptAndMockResponse({
|
||||
method: "GET",
|
||||
url: baseUrl + "example-style-with-fonts.json",
|
||||
response: {
|
||||
fixture: "example-style-with-fonts.json",
|
||||
},
|
||||
});
|
||||
this.helper.given.interceptAndMockResponse({
|
||||
method: "GET",
|
||||
url: baseUrl + "example-style-with-zoom-7-and-center-0-51.json",
|
||||
response: {
|
||||
fixture: "example-style-with-zoom-7-and-center-0-51.json",
|
||||
},
|
||||
});
|
||||
this.helper.given.interceptAndMockResponse({
|
||||
method: "GET",
|
||||
url: baseUrl + "example-style-with-zoom-5-and-center-50-50.json",
|
||||
response: {
|
||||
fixture: "example-style-with-zoom-5-and-center-50-50.json",
|
||||
},
|
||||
});
|
||||
this.helper.given.interceptAndMockResponse({
|
||||
method: "GET",
|
||||
url: "*example.local/*",
|
||||
@@ -116,81 +84,52 @@ export class MaputnikDriver {
|
||||
url: "*example.com/*",
|
||||
response: [],
|
||||
});
|
||||
this.helper.given.interceptAndMockResponse({
|
||||
method: "GET",
|
||||
url: "https://www.glyph-server.com/*",
|
||||
response: ["Font 1", "Font 2", "Font 3"],
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
public when = {
|
||||
...this.helper.when,
|
||||
modal: this.modalDriver.when,
|
||||
doWithin: (selector: string, fn: () => void) => {
|
||||
this.helper.when.doWithin(fn, selector);
|
||||
within: (selector: string, fn: () => void) => {
|
||||
this.helper.when.within(fn, selector);
|
||||
},
|
||||
tab: () => this.helper.get.element("body").tab(),
|
||||
waitForExampleFileResponse: () => {
|
||||
this.helper.when.waitForResponse("example-style.json");
|
||||
},
|
||||
openASecondStyleWithDifferentZoomAndCenter: () => {
|
||||
cy.contains("button", "Open").click();
|
||||
cy.get('[data-wd-key="modal:open.url.input"]')
|
||||
.should("be.enabled")
|
||||
.clear()
|
||||
.type("http://localhost:8888/example-style-with-zoom-5-and-center-50-50.json{enter}");
|
||||
},
|
||||
chooseExampleFile: () => {
|
||||
this.helper.given.fixture("example-style.json", "example-style.json");
|
||||
this.helper.when.openFileByFixture("example-style.json", "modal:open.dropzone", "modal:open.file.input");
|
||||
this.helper.when.wait(200);
|
||||
},
|
||||
dropExampleFile: () => {
|
||||
this.helper.given.fixture("example-style.json", "example-style.json");
|
||||
this.helper.when.dropFileByFixture("example-style.json", "modal:open.dropzone");
|
||||
this.helper.when.wait(200);
|
||||
this.helper.get
|
||||
.bySelector("type", "file")
|
||||
.selectFile("cypress/fixtures/example-style.json", { force: true });
|
||||
},
|
||||
setStyle: (
|
||||
styleProperties: "geojson" | "raster" | "both" | "layer" | "rectangles" | "font" | "zoom_7_center_0_51" | "",
|
||||
styleProperties: "geojson" | "raster" | "both" | "layer" | "",
|
||||
zoom?: number
|
||||
) => {
|
||||
const url = new URL(baseUrl);
|
||||
let url = "?debug";
|
||||
switch (styleProperties) {
|
||||
case "geojson":
|
||||
url.searchParams.set("style", baseUrl + "geojson-style.json");
|
||||
break;
|
||||
case "raster":
|
||||
url.searchParams.set("style", baseUrl + "raster-style.json");
|
||||
break;
|
||||
case "both":
|
||||
url.searchParams.set("style", baseUrl + "geojson-raster-style.json");
|
||||
break;
|
||||
case "layer":
|
||||
url.searchParams.set("style", baseUrl + "example-layer-style.json");
|
||||
break;
|
||||
case "rectangles":
|
||||
url.searchParams.set("style", baseUrl + "rectangles-style.json");
|
||||
break;
|
||||
case "font":
|
||||
url.searchParams.set("style", baseUrl + "example-style-with-fonts.json");
|
||||
break;
|
||||
case "zoom_7_center_0_51":
|
||||
url.searchParams.set("style", baseUrl + "example-style-with-zoom-7-and-center-0-51.json");
|
||||
break;
|
||||
case "geojson":
|
||||
url += `&style=${baseUrl}geojson-style.json`;
|
||||
break;
|
||||
case "raster":
|
||||
url += `&style=${baseUrl}raster-style.json`;
|
||||
break;
|
||||
case "both":
|
||||
url += `&style=${baseUrl}geojson-raster-style.json`;
|
||||
break;
|
||||
case "layer":
|
||||
url += `&style=${baseUrl}/example-layer-style.json`;
|
||||
break;
|
||||
}
|
||||
|
||||
if (zoom) {
|
||||
url.hash = `${zoom}/41.3805/2.1635`;
|
||||
url += `#${zoom}/41.3805/2.1635`;
|
||||
}
|
||||
this.helper.when.visit(url.toString());
|
||||
this.helper.when.visit(baseUrl + url);
|
||||
if (styleProperties) {
|
||||
this.helper.when.acceptConfirm();
|
||||
}
|
||||
// when methods should not include assertions
|
||||
const toolbarLink = this.helper.get.elementByTestId("toolbar:link");
|
||||
toolbarLink.scrollIntoView();
|
||||
toolbarLink.should("be.visible");
|
||||
this.helper.get.elementByTestId("toolbar:link").should("be.visible");
|
||||
},
|
||||
|
||||
typeKeys: (keys: string) => this.helper.get.element("body").type(keys),
|
||||
@@ -200,7 +139,7 @@ export class MaputnikDriver {
|
||||
},
|
||||
|
||||
selectWithin: (selector: string, value: string) => {
|
||||
this.when.doWithin(selector, () => {
|
||||
this.when.within(selector, () => {
|
||||
this.helper.get.element("select").select(value);
|
||||
});
|
||||
},
|
||||
@@ -219,35 +158,6 @@ export class MaputnikDriver {
|
||||
.clear()
|
||||
.type(text, { parseSpecialCharSequences: false });
|
||||
},
|
||||
|
||||
setValueToPropertyArray: (selector: string, value: string) => {
|
||||
this.when.doWithin(selector, () => {
|
||||
this.helper.get.element(".maputnik-array-block-content input").last().type("{selectall}"+value, {force: true });
|
||||
});
|
||||
},
|
||||
|
||||
addValueToPropertyArray: (selector: string, value: string) => {
|
||||
this.when.doWithin(selector, () => {
|
||||
this.helper.get.element(".maputnik-array-add-value").click({ force: true });
|
||||
this.helper.get.element(".maputnik-array-block-content input").last().type("{selectall}"+value, {force: true });
|
||||
});
|
||||
},
|
||||
|
||||
closePopup: () => {
|
||||
this.helper.get.element(".maplibregl-popup-close-button").click();
|
||||
},
|
||||
|
||||
collapseGroupInLayerEditor: (index = 0) => {
|
||||
this.helper.get.element(".maputnik-layer-editor-group__button").eq(index).realClick();
|
||||
},
|
||||
|
||||
appendTextInJsonEditor: (text: string) => {
|
||||
this.helper.get.element(".cm-line").first().click().type(text, { parseSpecialCharSequences: false });
|
||||
},
|
||||
|
||||
setTextInJsonEditor: (text: string) => {
|
||||
this.helper.get.element(".cm-line").first().click().clear().type(text, { parseSpecialCharSequences: false });
|
||||
}
|
||||
};
|
||||
|
||||
public get = {
|
||||
@@ -267,6 +177,6 @@ export class MaputnikDriver {
|
||||
skipTargetLayerEditor: () =>
|
||||
this.helper.get.elementByTestId("skip-target-layer-editor"),
|
||||
canvas: () => this.helper.get.element("canvas"),
|
||||
searchControl: () => this.helper.get.element(".maplibregl-ctrl-geocoder")
|
||||
searchControl: () => this.helper.get.element('.maplibregl-ctrl-geocoder')
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ export default class ModalDriver {
|
||||
public when = {
|
||||
fillLayers: (opts: { type: string; layer?: string; id?: string }) => {
|
||||
// Having logic in test code is an anti pattern.
|
||||
// This should be split to multiple single responsibility functions
|
||||
const type = opts.type;
|
||||
const layer = opts.layer;
|
||||
// This should be splitted to multiple single responsibility functions
|
||||
let type = opts.type;
|
||||
let layer = opts.layer;
|
||||
let id;
|
||||
if (opts.id) {
|
||||
id = opts.id;
|
||||
@@ -20,8 +20,8 @@ export default class ModalDriver {
|
||||
this.helper.when.type("add-layer.layer-id.input", id);
|
||||
|
||||
if (layer) {
|
||||
this.helper.when.doWithin(() => {
|
||||
this.helper.get.element("input").clear().type(layer!);
|
||||
this.helper.when.within(() => {
|
||||
this.helper.get.element("input").type(layer!);
|
||||
}, "add-layer.layer-source-block");
|
||||
}
|
||||
this.helper.when.click("add-layer");
|
||||
|
||||
+12
-288
@@ -1,8 +1,7 @@
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
import tokens from "../../src/config/tokens.json" with {type: "json"};
|
||||
|
||||
describe("modals", () => {
|
||||
const { beforeAndAfter, when, get, given, then } = new MaputnikDriver();
|
||||
let { beforeAndAfter, when, get, then } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -18,19 +17,15 @@ describe("modals", () => {
|
||||
then(get.elementByTestId("modal:open")).shouldNotExist();
|
||||
});
|
||||
|
||||
it("upload", () => {
|
||||
it.skip("upload", () => {
|
||||
// HM: I was not able to make the following choose file actually to select a file and close the modal...
|
||||
when.chooseExampleFile();
|
||||
then(get.fixture("example-style.json")).shouldEqualToStoredStyle();
|
||||
});
|
||||
|
||||
it("upload via drag and drop", () => {
|
||||
when.dropExampleFile();
|
||||
then(get.fixture("example-style.json")).shouldEqualToStoredStyle();
|
||||
then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
|
||||
});
|
||||
|
||||
describe("when click open url", () => {
|
||||
beforeEach(() => {
|
||||
const styleFileUrl = get.exampleFileUrl();
|
||||
let styleFileUrl = get.exampleFileUrl();
|
||||
|
||||
when.setValue("modal:open.url.input", styleFileUrl);
|
||||
when.click("modal:open.url.button");
|
||||
@@ -66,60 +61,9 @@ describe("modals", () => {
|
||||
});
|
||||
|
||||
describe("sources", () => {
|
||||
beforeEach(() => {
|
||||
when.setStyle("layer");
|
||||
when.click("nav:sources");
|
||||
});
|
||||
|
||||
it("active sources");
|
||||
it("public source");
|
||||
|
||||
it("add new source", () => {
|
||||
const sourceId = "n1z2v3r";
|
||||
when.setValue("modal:sources.add.source_id", sourceId);
|
||||
when.select("modal:sources.add.source_type", "tile_vector");
|
||||
when.select("modal:sources.add.scheme_type", "tms");
|
||||
when.click("modal:sources.add.add_source");
|
||||
when.wait(200);
|
||||
then(
|
||||
get.styleFromLocalStorage().then((style) => style.sources[sourceId])
|
||||
).shouldInclude({
|
||||
scheme: "tms",
|
||||
});
|
||||
});
|
||||
|
||||
it("add new pmtiles source", () => {
|
||||
const sourceId = "pmtilestest";
|
||||
when.setValue("modal:sources.add.source_id", sourceId);
|
||||
when.select("modal:sources.add.source_type", "pmtiles_vector");
|
||||
when.setValue("modal:sources.add.source_url", "https://data.source.coop/protomaps/openstreetmap/v4.pmtiles");
|
||||
when.click("modal:sources.add.add_source");
|
||||
when.click("modal:sources.add.add_source");
|
||||
when.wait(200);
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
sources: {
|
||||
pmtilestest: {
|
||||
type: "vector",
|
||||
url: "pmtiles://https://data.source.coop/protomaps/openstreetmap/v4.pmtiles",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("add new raster source", () => {
|
||||
const sourceId = "rastertest";
|
||||
when.setValue("modal:sources.add.source_id", sourceId);
|
||||
when.select("modal:sources.add.source_type", "tile_raster");
|
||||
when.select("modal:sources.add.scheme_type", "xyz");
|
||||
when.setValue("modal:sources.add.tile_size", "128");
|
||||
when.click("modal:sources.add.add_source");
|
||||
when.wait(200);
|
||||
then(
|
||||
get.styleFromLocalStorage().then((style) => style.sources[sourceId])
|
||||
).shouldInclude({
|
||||
tileSize: 128,
|
||||
});
|
||||
});
|
||||
it("add new source");
|
||||
});
|
||||
|
||||
describe("inspect", () => {
|
||||
@@ -175,24 +119,14 @@ describe("modals", () => {
|
||||
});
|
||||
|
||||
it("sprite url", () => {
|
||||
when.setTextInJsonEditor("\"http://example.com\"");
|
||||
when.setValue("modal:settings.sprite", "http://example.com");
|
||||
when.click("modal:settings.name");
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
sprite: "http://example.com",
|
||||
});
|
||||
});
|
||||
|
||||
it("sprite object", () => {
|
||||
when.setTextInJsonEditor(JSON.stringify([{ id: "1", url: "2" }]));
|
||||
|
||||
when.click("modal:settings.name");
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
sprite: [{ id: "1", url: "2" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("glyphs url", () => {
|
||||
const glyphsUrl = "http://example.com/{fontstack}/{range}.pbf";
|
||||
let glyphsUrl = "http://example.com/{fontstack}/{range}.pbf";
|
||||
when.setValue("modal:settings.glyphs", glyphsUrl);
|
||||
when.click("modal:settings.name");
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
@@ -201,78 +135,31 @@ describe("modals", () => {
|
||||
});
|
||||
|
||||
it("maptiler access token", () => {
|
||||
const apiKey = "testing123";
|
||||
let apiKey = "testing123";
|
||||
when.setValue(
|
||||
"modal:settings.maputnik:openmaptiles_access_token",
|
||||
apiKey
|
||||
);
|
||||
when.click("modal:settings.name");
|
||||
then(
|
||||
get.styleFromLocalStorage().then((style) => style.metadata)
|
||||
get.styleFromLocalStorage().pipe((style) => style.metadata)
|
||||
).shouldInclude({
|
||||
"maputnik:openmaptiles_access_token": apiKey,
|
||||
});
|
||||
});
|
||||
|
||||
it("thunderforest access token", () => {
|
||||
const apiKey = "testing123";
|
||||
let apiKey = "testing123";
|
||||
when.setValue(
|
||||
"modal:settings.maputnik:thunderforest_access_token",
|
||||
apiKey
|
||||
);
|
||||
when.click("modal:settings.name");
|
||||
then(
|
||||
get.styleFromLocalStorage().then((style) => style.metadata)
|
||||
get.styleFromLocalStorage().pipe((style) => style.metadata)
|
||||
).shouldInclude({ "maputnik:thunderforest_access_token": apiKey });
|
||||
});
|
||||
|
||||
it("stadia access token", () => {
|
||||
const apiKey = "testing123";
|
||||
when.setValue(
|
||||
"modal:settings.maputnik:stadia_access_token",
|
||||
apiKey
|
||||
);
|
||||
when.click("modal:settings.name");
|
||||
then(
|
||||
get.styleFromLocalStorage().then((style) => style.metadata)
|
||||
).shouldInclude({ "maputnik:stadia_access_token": apiKey });
|
||||
});
|
||||
|
||||
it("locationiq access token", () => {
|
||||
const apiKey = "testing123";
|
||||
when.setValue(
|
||||
"modal:settings.maputnik:locationiq_access_token",
|
||||
apiKey
|
||||
);
|
||||
when.click("modal:settings.name");
|
||||
then(
|
||||
get.styleFromLocalStorage().then((style) => style.metadata)
|
||||
).shouldInclude({ "maputnik:locationiq_access_token": apiKey });
|
||||
});
|
||||
|
||||
it("style projection mercator", () => {
|
||||
when.select("modal:settings.projection", "mercator");
|
||||
then(
|
||||
get.styleFromLocalStorage().then((style) => style.projection)
|
||||
).shouldInclude({ type: "mercator" });
|
||||
});
|
||||
|
||||
it("style projection globe", () => {
|
||||
when.select("modal:settings.projection", "globe");
|
||||
then(
|
||||
get.styleFromLocalStorage().then((style) => style.projection)
|
||||
).shouldInclude({ type: "globe" });
|
||||
});
|
||||
|
||||
|
||||
it("style projection vertical-perspective", () => {
|
||||
when.select("modal:settings.projection", "vertical-perspective");
|
||||
then(
|
||||
get.styleFromLocalStorage().then((style) => style.projection)
|
||||
).shouldInclude({ type: "vertical-perspective" });
|
||||
|
||||
});
|
||||
|
||||
it("style renderer", () => {
|
||||
cy.on("uncaught:exception", () => false); // this is due to the fact that this is an invalid style for openlayers
|
||||
when.select("modal:settings.maputnik:renderer", "ol");
|
||||
@@ -285,172 +172,9 @@ describe("modals", () => {
|
||||
metadata: { "maputnik:renderer": "ol" },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
it("include API key when change renderer", () => {
|
||||
|
||||
when.click("modal:settings.close-modal");
|
||||
when.click("nav:open");
|
||||
|
||||
get.elementByAttribute("aria-label", "MapTiler Basic").should("exist").click();
|
||||
when.wait(1000);
|
||||
when.click("nav:settings");
|
||||
|
||||
when.select("modal:settings.maputnik:renderer", "mlgljs");
|
||||
then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual(
|
||||
"mlgljs"
|
||||
);
|
||||
|
||||
when.select("modal:settings.maputnik:renderer", "ol");
|
||||
then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual(
|
||||
"ol"
|
||||
);
|
||||
|
||||
given.intercept("https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key=*", "tileRequest", "GET");
|
||||
|
||||
when.select("modal:settings.maputnik:renderer", "mlgljs");
|
||||
then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual(
|
||||
"mlgljs"
|
||||
);
|
||||
|
||||
when.waitForResponse("tileRequest").its("request").its("url").should("include", `https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key=${tokens.openmaptiles}`);
|
||||
when.waitForResponse("tileRequest").its("request").its("url").should("include", `https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key=${tokens.openmaptiles}`);
|
||||
when.waitForResponse("tileRequest").its("request").its("url").should("include", `https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key=${tokens.openmaptiles}`);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("add layer", () => {
|
||||
beforeEach(() => {
|
||||
when.setStyle("layer");
|
||||
when.modal.open();
|
||||
});
|
||||
|
||||
it("shows duplicate id error", () => {
|
||||
when.setValue("add-layer.layer-id.input", "background");
|
||||
when.click("add-layer");
|
||||
then(get.elementByTestId("modal:add-layer")).shouldExist();
|
||||
then(get.element(".maputnik-modal-error")).shouldContainText(
|
||||
"Layer ID already exists"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sources", () => {
|
||||
it("toggle");
|
||||
});
|
||||
|
||||
describe("global state", () => {
|
||||
beforeEach(() => {
|
||||
when.click("nav:global-state");
|
||||
});
|
||||
|
||||
it("add variable", () => {
|
||||
when.wait(100);
|
||||
when.click("global-state-add-variable");
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
state: { key1: { default: "value" } },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("add multiple variables", () => {
|
||||
when.click("global-state-add-variable");
|
||||
when.click("global-state-add-variable");
|
||||
when.click("global-state-add-variable");
|
||||
when.wait(100);
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
state: { key1: { default: "value" }, key2: { default: "value" }, key3: { default: "value" } },
|
||||
});
|
||||
});
|
||||
|
||||
it("remove variable", () => {
|
||||
when.click("global-state-add-variable");
|
||||
when.click("global-state-add-variable");
|
||||
when.click("global-state-add-variable");
|
||||
when.click("global-state-remove-variable", 0);
|
||||
when.wait(100);
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
state: { key2: { default: "value" }, key3: { default: "value" } },
|
||||
});
|
||||
});
|
||||
|
||||
it("edit variable key", () => {
|
||||
when.click("global-state-add-variable");
|
||||
when.wait(100);
|
||||
when.setValue("global-state-variable-key:0", "mykey");
|
||||
when.typeKeys("{enter}");
|
||||
when.wait(100);
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
state: { mykey: { default: "value" } },
|
||||
});
|
||||
});
|
||||
|
||||
it("edit variable value", () => {
|
||||
when.click("global-state-add-variable");
|
||||
when.wait(100);
|
||||
when.setValue("global-state-variable-value:0", "myvalue");
|
||||
when.typeKeys("{enter}");
|
||||
when.wait(100);
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
state: { key1: { default: "myvalue" } },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("error panel", () => {
|
||||
it("not visible when no errors", () => {
|
||||
then(get.element("maputnik-message-panel-error")).shouldNotExist();
|
||||
});
|
||||
|
||||
it("visible on style error", () => {
|
||||
when.modal.open();
|
||||
when.modal.fillLayers({
|
||||
type: "circle",
|
||||
layer: "invalid",
|
||||
});
|
||||
then(get.element(".maputnik-message-panel-error")).shouldBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Handle localStorage QuotaExceededError", () => {
|
||||
it("handles quota exceeded error when opening style from URL", () => {
|
||||
// Clear localStorage to start fresh
|
||||
cy.clearLocalStorage();
|
||||
|
||||
// fill localStorage until we get a QuotaExceededError
|
||||
cy.window().then(win => {
|
||||
let chunkSize = 1000;
|
||||
const chunk = new Array(chunkSize).join("x");
|
||||
let index = 0;
|
||||
|
||||
// Keep adding until we hit the quota
|
||||
while (true) {
|
||||
try {
|
||||
const key = `maputnik:fill-${index++}`;
|
||||
win.localStorage.setItem(key, chunk);
|
||||
} catch (e: any) {
|
||||
// Verify it's a quota error
|
||||
if (e.name === "QuotaExceededError") {
|
||||
if (chunkSize <= 1) return;
|
||||
else {
|
||||
chunkSize /= 2;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
throw e; // Unexpected error
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Open the style via URL input
|
||||
when.click("nav:open");
|
||||
when.setValue("modal:open.url.input", get.exampleFileUrl());
|
||||
when.click("modal:open.url.button");
|
||||
|
||||
then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
|
||||
then(get.styleFromLocalStorage()).shouldExist();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,3 +15,4 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"id": "test-style",
|
||||
"version": 8,
|
||||
"name": "Test Style",
|
||||
"metadata": {
|
||||
"maputnik:renderer": "mlgljs"
|
||||
},
|
||||
"sources": {
|
||||
"example": {
|
||||
"type": "geojson",
|
||||
"data": {
|
||||
"type": "FeatureCollection",
|
||||
"features":[{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"name": "Dinagat Islands"
|
||||
},
|
||||
"geometry":{
|
||||
"type": "Point",
|
||||
"coordinates": [125.6, 10.1]
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
},
|
||||
"glyphs": "https://www.glyph-server.com/fonts/{fontstack}/{range}.pbf",
|
||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"layers": [
|
||||
{
|
||||
"id": "label",
|
||||
"type": "symbol",
|
||||
"source": "example",
|
||||
"layout": {
|
||||
"text-font": ["Font"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"id": "test-style",
|
||||
"center": [50,50],
|
||||
"zoom": 5,
|
||||
"version": 8,
|
||||
"name": "Test Style",
|
||||
"sources": {
|
||||
"rectangles": {
|
||||
"type": "geojson",
|
||||
"data": {
|
||||
"type": "FeatureCollection",
|
||||
"features": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"layers": []
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"id": "test-style",
|
||||
"center": [0,51],
|
||||
"zoom": 7,
|
||||
"version": 8,
|
||||
"name": "Test Style",
|
||||
"sources": {
|
||||
"rectangles": {
|
||||
"type": "geojson",
|
||||
"data": {
|
||||
"type": "FeatureCollection",
|
||||
"features": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"layers": []
|
||||
}
|
||||
@@ -1,114 +1,12 @@
|
||||
{
|
||||
"id": "test-style",
|
||||
"version": 8,
|
||||
"name": "Test Style",
|
||||
"metadata": {
|
||||
"maputnik:renderer": "mlgljs",
|
||||
"data": [
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
25,
|
||||
26,
|
||||
27,
|
||||
28,
|
||||
29,
|
||||
30,
|
||||
31,
|
||||
32,
|
||||
33,
|
||||
34,
|
||||
35,
|
||||
36,
|
||||
37,
|
||||
38,
|
||||
39,
|
||||
40,
|
||||
41,
|
||||
42,
|
||||
43,
|
||||
44,
|
||||
45,
|
||||
46,
|
||||
47,
|
||||
48,
|
||||
49,
|
||||
50,
|
||||
51,
|
||||
52,
|
||||
53,
|
||||
54,
|
||||
55,
|
||||
56,
|
||||
57,
|
||||
58,
|
||||
59,
|
||||
60,
|
||||
61,
|
||||
62,
|
||||
63,
|
||||
64,
|
||||
65,
|
||||
66,
|
||||
67,
|
||||
68,
|
||||
69,
|
||||
70,
|
||||
71,
|
||||
72,
|
||||
73,
|
||||
74,
|
||||
75,
|
||||
76,
|
||||
77,
|
||||
78,
|
||||
79,
|
||||
80,
|
||||
81,
|
||||
82,
|
||||
83,
|
||||
84,
|
||||
85,
|
||||
86,
|
||||
87,
|
||||
88,
|
||||
89,
|
||||
90,
|
||||
91,
|
||||
92,
|
||||
93,
|
||||
94,
|
||||
95,
|
||||
96,
|
||||
97,
|
||||
98,
|
||||
99
|
||||
]
|
||||
},
|
||||
"sources": {},
|
||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"layers": []
|
||||
"id": "test-style",
|
||||
"version": 8,
|
||||
"name": "Test Style",
|
||||
"metadata": {
|
||||
"maputnik:renderer": "mlgljs"
|
||||
},
|
||||
"sources": {},
|
||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"layers": []
|
||||
}
|
||||
|
||||
@@ -31,4 +31,4 @@
|
||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"layers": []
|
||||
}
|
||||
}
|
||||
@@ -26,4 +26,4 @@
|
||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"layers": []
|
||||
}
|
||||
}
|
||||
@@ -15,4 +15,4 @@
|
||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"layers": []
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
{
|
||||
"version": 8,
|
||||
"sources": {
|
||||
"rectangles": {
|
||||
"type": "geojson",
|
||||
"data": {
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {},
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[
|
||||
[
|
||||
-130.78125,
|
||||
-33.13755119234615
|
||||
],
|
||||
[
|
||||
-130.78125,
|
||||
63.548552232036414
|
||||
],
|
||||
[
|
||||
15.468749999999998,
|
||||
63.548552232036414
|
||||
],
|
||||
[
|
||||
15.468749999999998,
|
||||
-33.13755119234615
|
||||
],
|
||||
[
|
||||
-130.78125,
|
||||
-33.13755119234615
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {},
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[
|
||||
[
|
||||
-48.515625,
|
||||
-54.97761367069625
|
||||
],
|
||||
[
|
||||
-48.515625,
|
||||
36.5978891330702
|
||||
],
|
||||
[
|
||||
169.45312499999997,
|
||||
36.5978891330702
|
||||
],
|
||||
[
|
||||
169.45312499999997,
|
||||
-54.97761367069625
|
||||
],
|
||||
[
|
||||
-48.515625,
|
||||
-54.97761367069625
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"id": "background",
|
||||
"type": "background",
|
||||
"paint": {
|
||||
"background-color": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "rectangles",
|
||||
"type": "fill",
|
||||
"source": "rectangles",
|
||||
"paint": {
|
||||
"fill-opacity": 0.3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -34,4 +34,4 @@
|
||||
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -1,12 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>Components App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div data-cy-root></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,37 +0,0 @@
|
||||
// ***********************************************************
|
||||
// This example support/component.ts is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import "./commands";
|
||||
|
||||
import { mount } from "cypress/react";
|
||||
|
||||
// Augment the Cypress namespace to include type definitions for
|
||||
// your custom command.
|
||||
// Alternatively, can be defined in cypress/support/component.d.ts
|
||||
// with a <reference path="./component" /> at the top of your spec.
|
||||
declare global {
|
||||
/* eslint-disable @typescript-eslint/no-namespace */
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
mount: typeof mount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add("mount", mount);
|
||||
|
||||
// Example use:
|
||||
// cy.mount(<MyComponent />)
|
||||
@@ -1,31 +0,0 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
editor
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
# Binary version of pubilic/editor
|
||||
rice-box.go
|
||||
|
||||
# Built binary
|
||||
maputnik
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016 Maputnik
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,47 +0,0 @@
|
||||
SOURCEDIR=.
|
||||
SOURCES := $(shell find $(SOURCEDIR) -name '*.go')
|
||||
BINARY=maputnik
|
||||
VERSION := $(shell node -p "require('../package.json').version")
|
||||
GOBIN := $(or $(shell if [ -d /go/bin ]; then echo "/go/bin"; fi),$(HOME)/go/bin)
|
||||
|
||||
all: $(BINARY)
|
||||
|
||||
$(BINARY): $(GOBIN)/gox $(GOBIN)/go-winres $(SOURCES) version.go rice-box.go winres/winres.json
|
||||
$(GOBIN)/go-winres make --product-version=$(VERSION)
|
||||
$(GOBIN)/gox -osarch "windows/amd64 linux/amd64 darwin/amd64" -output "bin/{{.OS}}/${BINARY}"
|
||||
|
||||
bin/linux/$(BINARY): $(GOBIN)/gox $(GOBIN)/go-winres $(SOURCES) version.go rice-box.go winres/winres.json
|
||||
$(GOBIN)/go-winres make --product-version=$(VERSION)
|
||||
$(GOBIN)/gox -osarch "linux/amd64" -output "bin/{{.OS}}/${BINARY}"
|
||||
|
||||
winres/winres.json: winres/winres_template.json
|
||||
sed 's/{{.Version}}/$(VERSION)/g' winres/winres_template.json > $@
|
||||
|
||||
$(GOBIN)/go-winres:
|
||||
go install github.com/tc-hib/go-winres@latest
|
||||
|
||||
# Copy the current release into ./editor/maputnik so it can be
|
||||
# embedded in the binary
|
||||
editor/pull_release:
|
||||
mkdir -p editor
|
||||
cp -r ../dist/* editor
|
||||
|
||||
$(GOBIN)/gox:
|
||||
go install github.com/mitchellh/gox@v1.0.1
|
||||
|
||||
$(GOBIN)/rice:
|
||||
go install github.com/GeertJohan/go.rice/rice@v1.0.3
|
||||
|
||||
# Embed the current version number in the executable by writing version.go
|
||||
.PHONY: version.go
|
||||
version.go:
|
||||
@printf "// DO NOT EDIT: Autogenerated by Makefile\n" > version.go
|
||||
@printf "package main\n" >> version.go
|
||||
@printf "const Version = \"$(VERSION)\"\n" >> version.go
|
||||
|
||||
rice-box.go: $(GOBIN)/rice editor/pull_release
|
||||
$(GOBIN)/rice embed-go
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf editor && rm -f rice-box.go && rm -rf bin
|
||||
@@ -1,73 +0,0 @@
|
||||
# Maputnik Desktop [][github-action-ci]
|
||||
|
||||
---
|
||||
|
||||
A Golang based cross platform executable for integrating Maputnik locally.
|
||||
This binary packages up the JavaScript and CSS bundle produced by maputnik
|
||||
and embeds it in the program for easy distribution. It also allows
|
||||
exposing a local style file and work on it both in Maputnik and with your favorite
|
||||
editor.
|
||||
|
||||
Report issues on [maplibre/maputnik](https://github.com/maplibre/maputnik).
|
||||
|
||||
## Install
|
||||
|
||||
You can download a zip file containing desktop binaries for Linux, OSX and Windows from [the latest releases of **maplibre/maputnik**](https://github.com/maplibre/maputnik/releases/latest).
|
||||
|
||||
### Usage
|
||||
|
||||
Simply start up a web server and access the Maputnik editor GUI at `localhost:8000`.
|
||||
|
||||
```bash
|
||||
maputnik
|
||||
```
|
||||
|
||||
Expose a local style file to Maputnik allowing the web based editor
|
||||
to save to the local filesystem.
|
||||
|
||||
```bash
|
||||
maputnik --file basic-v9.json
|
||||
```
|
||||
|
||||
Watch the local style for changes and inform the editor via web socket.
|
||||
This makes it possible to edit the style with a local text editor and still
|
||||
use Maputnik.
|
||||
|
||||
```bash
|
||||
maputnik --watch --file basic-v9.json
|
||||
```
|
||||
|
||||
Choose a local port to listen on, instead of using the default port 8000.
|
||||
|
||||
```bash
|
||||
maputnik --port 8001
|
||||
```
|
||||
|
||||
Specify a path to a directory which, if it exists, will be served under http://localhost:8000/static/ .
|
||||
Could be used to serve sprites and glyphs.
|
||||
|
||||
```bash
|
||||
maputnik --static ./localFolder
|
||||
```
|
||||
|
||||
### API
|
||||
|
||||
`maputnik` exposes the configured styles via a HTTP API.
|
||||
|
||||
| Method | Description |
|
||||
| ------------------------ | ------------------------------------------------------ |
|
||||
| `GET /styles` | List the ID of all configured style files |
|
||||
| `GET /styles/{filename}` | Get contents of a single style file |
|
||||
| `PUT /styles/{filename}` | Update contents of a style file |
|
||||
| `WEBSOCKET /ws` | Listen to change events for the configured style files |
|
||||
|
||||
### Build
|
||||
|
||||
From the root of the [maplibre/maputnik](https://github.com/maplibre/maputnik) project, install the deps and run the desktop-build command.
|
||||
|
||||
```
|
||||
npm install
|
||||
npm run build-desktop
|
||||
```
|
||||
|
||||
You should now find the `maputnik` binary in your `desktop/bin` directory.
|
||||
@@ -1,81 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func StyleFileAccessor(filename string) styleFileAccessor {
|
||||
return styleFileAccessor{filename, styleId(filename)}
|
||||
}
|
||||
|
||||
func styleId(filename string) string {
|
||||
raw, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
var spec styleSpec
|
||||
err = json.Unmarshal(raw, &spec)
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
if spec.Id == "" {
|
||||
fmt.Println("No id in style")
|
||||
}
|
||||
return spec.Id
|
||||
}
|
||||
|
||||
type styleSpec struct {
|
||||
Id string `json:"id"`
|
||||
}
|
||||
|
||||
// Allows access to a single style file
|
||||
type styleFileAccessor struct {
|
||||
filename string
|
||||
id string
|
||||
}
|
||||
|
||||
func (fa styleFileAccessor) ListFiles(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
encoder := json.NewEncoder(w)
|
||||
encoder.Encode([]string{fa.id})
|
||||
}
|
||||
|
||||
func (fa styleFileAccessor) ReadFile(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
_ = vars["styleId"]
|
||||
|
||||
//TODO: Choose right file
|
||||
// right now we just return the single file we know of
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
raw, err := ioutil.ReadFile(fa.filename)
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
w.Write(raw)
|
||||
}
|
||||
|
||||
func (fa styleFileAccessor) SaveFile(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
_ = vars["styleId"]
|
||||
|
||||
//TODO: Save to right file
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
body, _ := ioutil.ReadAll(r.Body)
|
||||
var out bytes.Buffer
|
||||
json.Indent(&out, body, "", " ")
|
||||
|
||||
if err := ioutil.WriteFile(fa.filename, out.Bytes(), 0666); err != nil {
|
||||
log.Fatalf("Can not copy from request to file: %s", err.Error())
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package filewatch
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
|
||||
func writer(ws *websocket.Conn, filename string) {
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer watcher.Close()
|
||||
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case event := <-watcher.Events:
|
||||
if event.Op&fsnotify.Write == fsnotify.Write {
|
||||
log.Println("Modified file:", event.Name)
|
||||
var p []byte
|
||||
var err error
|
||||
|
||||
p, err = ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if p != nil {
|
||||
if err := ws.WriteMessage(websocket.TextMessage, p); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
case err := <-watcher.Errors:
|
||||
log.Println("Watch error:", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err = watcher.Add(filename); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
<-done
|
||||
}
|
||||
|
||||
func ServeWebsocketFileWatcher(filename string, w http.ResponseWriter, r *http.Request) {
|
||||
ws, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
if _, ok := err.(websocket.HandshakeError); !ok {
|
||||
log.Println(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
writer(ws, filename)
|
||||
defer ws.Close()
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
module maputnik/desktop
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/GeertJohan/go.rice v1.0.3
|
||||
github.com/fsnotify/fsnotify v1.6.0
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/maputnik/desktop v1.0.7
|
||||
github.com/urfave/cli v1.22.12
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/GeertJohan/go.incremental v1.0.0 // indirect
|
||||
github.com/akavel/rsrc v0.8.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/daaku/go.zipexe v1.0.2 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.1 // indirect
|
||||
github.com/jessevdk/go-flags v1.4.0 // indirect
|
||||
github.com/nkovacs/streamquote v1.0.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.0.1 // indirect
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956 // indirect
|
||||
)
|
||||
@@ -1,54 +0,0 @@
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg=
|
||||
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
|
||||
github.com/GeertJohan/go.rice v1.0.3 h1:k5viR+xGtIhF61125vCE1cmJ5957RQGXG6dmbaWZSmI=
|
||||
github.com/GeertJohan/go.rice v1.0.3/go.mod h1:XVdrU4pW00M4ikZed5q56tPf1v2KwnIKeIdc9CBYNt4=
|
||||
github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw=
|
||||
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/daaku/go.zipexe v1.0.2 h1:Zg55YLYTr7M9wjKn8SY/WcpuuEi+kR2u4E8RhvpyXmk=
|
||||
github.com/daaku/go.zipexe v1.0.2/go.mod h1:5xWogtqlYnfBXkSB1o9xysukNP9GTvaNkqzUZbt3Bw8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
|
||||
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
|
||||
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/maputnik/desktop v1.0.7 h1:rdFg7emIJOT3YsZpwqSChmWtMOvu+T4h6WwVQAZP9n4=
|
||||
github.com/maputnik/desktop v1.0.7/go.mod h1:wmDjHUztx9jOBz0I22589yWguAGdV/sEM57YANpN8oQ=
|
||||
github.com/nkovacs/streamquote v1.0.0 h1:PmVIV08Zlx2lZK5fFZlMZ04eHcDTIFJCv/5/0twVUow=
|
||||
github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/urfave/cli v1.22.12 h1:igJgVw1JdKH+trcLWLeLwZjU9fEfPesQ+9/e4MQ44S8=
|
||||
github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
|
||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956 h1:XeJjHH1KiLpKGb6lvMiksZ9l0fVUh+AmGcm0nOMEBOY=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -1,79 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"github.com/GeertJohan/go.rice"
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/maputnik/desktop/filewatch"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
app.Name = "maputnik"
|
||||
app.Usage = "Server for integrating Maputnik locally"
|
||||
app.Version = Version
|
||||
|
||||
app.Flags = []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "file, f",
|
||||
Usage: "Allow access to JSON style from web client",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "watch",
|
||||
Usage: "Notify web client about JSON style file changes",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "port",
|
||||
Value: 8000,
|
||||
Usage: "TCP port to listen on",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "static",
|
||||
Usage: "Serve directory under /static/",
|
||||
},
|
||||
}
|
||||
|
||||
app.Action = func(c *cli.Context) error {
|
||||
gui := http.FileServer(rice.MustFindBox("editor").HTTPBox())
|
||||
|
||||
router := mux.NewRouter().StrictSlash(true)
|
||||
|
||||
filename := c.String("file")
|
||||
if filename != "" {
|
||||
fmt.Printf("%s is accessible via Maputnik\n", filename)
|
||||
// Allow access to reading and writing file on the local system
|
||||
path, _ := filepath.Abs(filename)
|
||||
accessor := StyleFileAccessor(path)
|
||||
router.Path("/styles").Methods("GET").HandlerFunc(accessor.ListFiles)
|
||||
router.Path("/styles/{styleId}").Methods("GET").HandlerFunc(accessor.ReadFile)
|
||||
router.Path("/styles/{styleId}").Methods("PUT").HandlerFunc(accessor.SaveFile)
|
||||
|
||||
// Register websocket to notify we clients about file changes
|
||||
if c.Bool("watch") {
|
||||
router.Path("/ws").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
filewatch.ServeWebsocketFileWatcher(filename, w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
staticDir := c.String("static")
|
||||
if staticDir != "" {
|
||||
h := http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir)))
|
||||
router.PathPrefix("/static/").Handler(h)
|
||||
}
|
||||
|
||||
router.PathPrefix("/").Handler(http.StripPrefix("/", gui))
|
||||
loggedRouter := handlers.LoggingHandler(os.Stdout, router)
|
||||
corsRouter := handlers.CORS(handlers.AllowedHeaders([]string{"Content-Type"}), handlers.AllowedMethods([]string{"GET", "PUT"}), handlers.AllowedOrigins([]string{"*"}), handlers.AllowCredentials())(loggedRouter)
|
||||
|
||||
fmt.Printf("Exposing Maputnik on http://localhost:%d\n", c.Int("port"))
|
||||
return http.ListenAndServe(fmt.Sprintf(":%d", c.Int("port")), corsRouter)
|
||||
}
|
||||
|
||||
app.Run(os.Args)
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
{
|
||||
"RT_GROUP_ICON": {
|
||||
"APP": {
|
||||
"0000": [
|
||||
"../../src/img/maputnik.png"
|
||||
]
|
||||
}
|
||||
},
|
||||
"RT_MANIFEST": {
|
||||
"#1": {
|
||||
"0409": {
|
||||
"identity": {
|
||||
"name": "Maputnik",
|
||||
"version": "{{.Version}}"
|
||||
},
|
||||
"description": "A MapLibre GL visual style editor",
|
||||
"minimum-os": "win7",
|
||||
"execution-level": "as invoker",
|
||||
"ui-access": false,
|
||||
"auto-elevate": false,
|
||||
"dpi-awareness": "system",
|
||||
"disable-theming": false,
|
||||
"disable-window-filtering": false,
|
||||
"high-resolution-scrolling-aware": false,
|
||||
"ultra-high-resolution-scrolling-aware": false,
|
||||
"long-path-aware": false,
|
||||
"printer-driver-isolation": false,
|
||||
"gdi-scaling": false,
|
||||
"segment-heap": false,
|
||||
"use-common-controls-v6": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"RT_VERSION": {
|
||||
"#1": {
|
||||
"0000": {
|
||||
"fixed": {
|
||||
"file_version": "{{.Version}}",
|
||||
"product_version": "{{.Version}}"
|
||||
},
|
||||
"info": {
|
||||
"0409": {
|
||||
"Comments": "https://github.com/maplibre/maputnik",
|
||||
"CompanyName": "Maputnik",
|
||||
"FileDescription": "A MapLibre GL visual style editor",
|
||||
"FileVersion": "{{.Version}}",
|
||||
"InternalName": "Maputnik",
|
||||
"LegalCopyright": "MIT License",
|
||||
"LegalTrademarks": "",
|
||||
"OriginalFilename": "Maputnik.exe",
|
||||
"PrivateBuild": "",
|
||||
"ProductName": "Maputnik",
|
||||
"ProductVersion": "{{.Version}}",
|
||||
"SpecialBuild": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import eslint from "@eslint/js";
|
||||
import {defineConfig} from "eslint/config";
|
||||
import stylisticTs from "@stylistic/eslint-plugin";
|
||||
import tseslint from "typescript-eslint";
|
||||
import reactPlugin from "eslint-plugin-react";
|
||||
import reactHooksPlugin from "eslint-plugin-react-hooks";
|
||||
import reactRefreshPlugin from "eslint-plugin-react-refresh";
|
||||
|
||||
export default defineConfig({
|
||||
extends: [
|
||||
eslint.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
],
|
||||
files: ["**/*.{js,jsx,ts,tsx}"],
|
||||
ignores: [
|
||||
"dist/**/*",
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2024,
|
||||
sourceType: "module",
|
||||
globals: {
|
||||
global: "readonly"
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
react: { version: "18.2" }
|
||||
},
|
||||
plugins: {
|
||||
"react": reactPlugin,
|
||||
"react-hooks": reactHooksPlugin,
|
||||
"react-refresh": reactRefreshPlugin,
|
||||
"@stylistic": stylisticTs
|
||||
},
|
||||
rules: {
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ allowConstantExport: true }
|
||||
],
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
varsIgnorePattern: "^_",
|
||||
caughtErrors: "all",
|
||||
caughtErrorsIgnorePattern: "^_",
|
||||
argsIgnorePattern: "^_"
|
||||
}
|
||||
],
|
||||
"no-unused-vars": "off",
|
||||
"react/prop-types": "off",
|
||||
"no-undef": "off",
|
||||
"indent": "off",
|
||||
"@stylistic/indent": ["error", 2],
|
||||
"semi": "off",
|
||||
"@stylistic/semi": ["error", "always"],
|
||||
"quotes": "off",
|
||||
"@stylistic/quotes": ["error", "double", { avoidEscape: true }],
|
||||
"no-var": "error",
|
||||
"@typescript-eslint/no-non-null-asserted-optional-chain": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-empty-object-type": "off",
|
||||
"@typescript-eslint/consistent-type-imports": ["error", { "fixStyle": "inline-type-imports" }],
|
||||
|
||||
},
|
||||
linterOptions: {
|
||||
reportUnusedDisableDirectives: true,
|
||||
noInlineConfig: false
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -1,10 +0,0 @@
|
||||
import { defineConfig } from "i18next-cli";
|
||||
|
||||
export default defineConfig({
|
||||
locales: ["de", "fr", "he", "it", "ja", "ko", "tr", "zh"],
|
||||
extract: {
|
||||
input: ["src/**/*.{js,jsx,ts,tsx}"],
|
||||
output: "src/locales/{{language}}/{{namespace}}.json",
|
||||
defaultValue: "__STRING_NOT_TRANSLATED__"
|
||||
}
|
||||
});
|
||||
Generated
+8376
-9989
File diff suppressed because it is too large
Load Diff
+70
-85
@@ -1,22 +1,16 @@
|
||||
{
|
||||
"name": "maputnik",
|
||||
"version": "3.1.0",
|
||||
"version": "2.0.0",
|
||||
"description": "A MapLibre GL visual style editor",
|
||||
"type": "module",
|
||||
"main": "''",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"build": "tsc && vite build --mode=production",
|
||||
"build-desktop": "tsc && vite build --mode=desktop && cd desktop && make",
|
||||
"build-linux": "tsc && vite build --mode=desktop && cd desktop && make bin/linux/maputnik",
|
||||
"i18n:extract": "npx i18next-cli extract",
|
||||
"lint": "eslint",
|
||||
"build": "tsc && vite build --base=/maputnik/",
|
||||
"lint": "eslint ./src ./cypress --ext ts,tsx,js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||
"test": "cypress run",
|
||||
"test-unit": "vitest",
|
||||
"test-unit-ci": "vitest run --coverage --reporter=json",
|
||||
"cy:open": "cypress open",
|
||||
"lint-css": "stylelint \"src/styles/*.scss\"",
|
||||
"sort-styles": "jq 'sort_by(.id)' src/config/styles.json > tmp.json && mv tmp.json src/config/styles.json"
|
||||
"lint-css": "stylelint \"src/styles/*.scss\""
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -26,59 +20,54 @@
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/maplibre/maputnik#readme",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lint": "^6.9.7",
|
||||
"@codemirror/state": "^6.7.0",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@codemirror/view": "^6.43.4",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@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": "^25.0.1",
|
||||
"@mapbox/mapbox-gl-rtl-text": "^0.2.3",
|
||||
"@maplibre/maplibre-gl-geocoder": "^1.5.0",
|
||||
"@maplibre/maplibre-gl-style-spec": "^20.1.0",
|
||||
"@mdi/js": "^6.6.96",
|
||||
"@mdi/react": "^1.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.16.0",
|
||||
"@typescript-eslint/parser": "^6.16.0",
|
||||
"array-move": "^4.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"classnames": "^2.5.1",
|
||||
"codemirror": "^6.0.2",
|
||||
"color": "^5.0.3",
|
||||
"classnames": "^2.3.1",
|
||||
"codemirror": "^5.65.2",
|
||||
"color": "^4.2.3",
|
||||
"detect-browser": "^5.3.0",
|
||||
"downshift": "^9.4.0",
|
||||
"events": "^3.3.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"i18next": "^26.3.4",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"json-stringify-pretty-compact": "^4.0.0",
|
||||
"json-stringify-pretty-compact": "^3.0.0",
|
||||
"json-to-ast": "^2.1.0",
|
||||
"lodash": "^4.18.1",
|
||||
"jsonlint": "github:josdejong/jsonlint#85a19d7",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash.capitalize": "^4.2.1",
|
||||
"lodash.clamp": "^4.0.3",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"maplibre-gl": "^5.24.0",
|
||||
"mapbox-gl-inspect": "^1.3.1",
|
||||
"maplibre-gl": "^4.0.0",
|
||||
"maputnik-design": "github:maputnik/design#172b06c",
|
||||
"ol": "^10.9.0",
|
||||
"ol-mapbox-style": "^13.4.1",
|
||||
"pmtiles": "^4.4.1",
|
||||
"ol": "^6.14.1",
|
||||
"ol-mapbox-style": "^7.1.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^19.2.4",
|
||||
"react-accessible-accordion": "^5.0.1",
|
||||
"react-aria-menubutton": "^8.0.0",
|
||||
"react-aria-modal": "^5.0.2",
|
||||
"react": "^16.0.0",
|
||||
"react-accessible-accordion": "^4.0.0",
|
||||
"react-aria-menubutton": "^7.0.3",
|
||||
"react-aria-modal": "^4.0.1",
|
||||
"react-autobind": "^1.0.6",
|
||||
"react-autocomplete": "^1.8.1",
|
||||
"react-collapse": "^5.1.1",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-i18next": "^17.0.7",
|
||||
"react-icons": "^5.6.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-dom": "^16.0.0",
|
||||
"react-file-reader-input": "^2.0.0",
|
||||
"react-icon-base": "^2.1.2",
|
||||
"react-icons": "^4.3.1",
|
||||
"react-sortable-hoc": "^2.0.0",
|
||||
"reconnecting-websocket": "^4.4.0",
|
||||
"slugify": "^1.6.9",
|
||||
"sass": "^1.50.0",
|
||||
"slugify": "^1.6.5",
|
||||
"string-hash": "^1.1.3",
|
||||
"url": "^0.11.4"
|
||||
"url": "^0.11.0"
|
||||
},
|
||||
"jshintConfig": {
|
||||
"esversion": 6
|
||||
@@ -98,17 +87,15 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cypress/code-coverage": "^4.0.3",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@cypress/code-coverage": "^3.12.15",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
||||
"@rollup/plugin-replace": "^6.0.3",
|
||||
"@shellygo/cypress-test-utils": "^6.0.6",
|
||||
"@stylistic/eslint-plugin": "^5.10.0",
|
||||
"@types/codemirror": "^5.60.17",
|
||||
"@types/color": "^4.2.1",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@rollup/plugin-replace": "^5.0.5",
|
||||
"@shellygo/cypress-test-utils": "^2.0.17",
|
||||
"@types/codemirror": "^5.60.15",
|
||||
"@types/color": "^3.0.6",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/geojson": "^7946.0.16",
|
||||
"@types/geojson": "^7946.0.13",
|
||||
"@types/json-to-ast": "^2.1.4",
|
||||
"@types/lodash.capitalize": "^4.2.9",
|
||||
"@types/lodash.clamp": "^4.0.9",
|
||||
@@ -116,38 +103,36 @@
|
||||
"@types/lodash.get": "^4.4.9",
|
||||
"@types/lodash.isequal": "^4.5.8",
|
||||
"@types/lodash.throttle": "^4.1.9",
|
||||
"@types/randomcolor": "^0.5.9",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-aria-menubutton": "^6.2.14",
|
||||
"@types/react-aria-modal": "^5.0.0",
|
||||
"@types/react": "^16.14.52",
|
||||
"@types/react-aria-menubutton": "^6.2.13",
|
||||
"@types/react-aria-modal": "^4.0.9",
|
||||
"@types/react-autocomplete": "^1.8.9",
|
||||
"@types/react-collapse": "^5.0.4",
|
||||
"@types/react-color": "^3.0.13",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react-color": "^3.0.10",
|
||||
"@types/react-dom": "^16.9.24",
|
||||
"@types/react-file-reader-input": "^2.0.4",
|
||||
"@types/react-icon-base": "^2.1.6",
|
||||
"@types/string-hash": "^1.1.3",
|
||||
"@types/wicg-file-system-access": "^2023.10.7",
|
||||
"@vitejs/plugin-react": "^5.1.4",
|
||||
"@vitest/coverage-v8": "^4.1.9",
|
||||
"cors": "^2.8.6",
|
||||
"cypress": "^15.18.0",
|
||||
"cypress-plugin-tab": "^2.0.0",
|
||||
"eslint": "^10.6.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"i18next-cli": "^1.53.2",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"cors": "^2.8.5",
|
||||
"cypress": "^13.6.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"express": "^4.17.3",
|
||||
"istanbul": "^0.4.5",
|
||||
"istanbul-lib-coverage": "^3.2.2",
|
||||
"postcss": "^8.5.16",
|
||||
"react-hot-loader": "^4.13.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.62.1",
|
||||
"uuid": "^14.0.1",
|
||||
"vite": "^7.3.2",
|
||||
"vite-plugin-istanbul": "^9.0.1",
|
||||
"vitest": "^4.1.9"
|
||||
"istanbul-lib-coverage": "^3.2.0",
|
||||
"mocha": "^9.2.2",
|
||||
"postcss": "^8.4.12",
|
||||
"react-hot-loader": "^4.13.0",
|
||||
"stylelint": "^14.6.1",
|
||||
"stylelint-config-recommended-scss": "^6.0.0",
|
||||
"stylelint-scss": "^4.2.0",
|
||||
"typescript": "^5.3.3",
|
||||
"uuid": "^8.3.2",
|
||||
"vite": "^5.0.12",
|
||||
"vite-plugin-istanbul": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
+319
-324
File diff suppressed because it is too large
Load Diff
@@ -1,54 +1,46 @@
|
||||
import React from "react";
|
||||
import ScrollContainer from "./ScrollContainer";
|
||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||
import { IconContext } from "react-icons";
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ScrollContainer from './ScrollContainer'
|
||||
|
||||
type AppLayoutInternalProps = {
|
||||
type AppLayoutProps = {
|
||||
toolbar: React.ReactElement
|
||||
layerList: React.ReactElement
|
||||
layerEditor?: React.ReactElement
|
||||
codeEditor?: React.ReactElement
|
||||
map: React.ReactElement
|
||||
bottom?: React.ReactElement
|
||||
modals?: React.ReactNode
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
|
||||
class AppLayout extends React.Component<AppLayoutProps> {
|
||||
static childContextTypes = {
|
||||
reactIconBase: PropTypes.object
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
reactIconBase: { size: 14 }
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
document.body.dir = this.props.i18n.dir();
|
||||
|
||||
return <IconContext.Provider value={{size: "14px"}}>
|
||||
<div className="maputnik-layout">
|
||||
{this.props.toolbar}
|
||||
<div className="maputnik-layout-main">
|
||||
{this.props.codeEditor && <div className="maputnik-layout-code-editor">
|
||||
<ScrollContainer>
|
||||
{this.props.codeEditor}
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
}
|
||||
{!this.props.codeEditor && <>
|
||||
<div className="maputnik-layout-list">
|
||||
{this.props.layerList}
|
||||
</div>
|
||||
<div className="maputnik-layout-drawer">
|
||||
<ScrollContainer>
|
||||
{this.props.layerEditor}
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
</>}
|
||||
{this.props.map}
|
||||
</div>
|
||||
{this.props.bottom && <div className="maputnik-layout-bottom">
|
||||
{this.props.bottom}
|
||||
</div>
|
||||
}
|
||||
{this.props.modals}
|
||||
return <div className="maputnik-layout">
|
||||
{this.props.toolbar}
|
||||
<div className="maputnik-layout-list">
|
||||
{this.props.layerList}
|
||||
</div>
|
||||
</IconContext.Provider>;
|
||||
<div className="maputnik-layout-drawer">
|
||||
<ScrollContainer>
|
||||
{this.props.layerEditor}
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
{this.props.map}
|
||||
{this.props.bottom && <div className="maputnik-layout-bottom">
|
||||
{this.props.bottom}
|
||||
</div>
|
||||
}
|
||||
{this.props.modals}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
const AppLayout = withTranslation()(AppLayoutInternal);
|
||||
export default AppLayout;
|
||||
export default AppLayout
|
||||
|
||||
@@ -1,33 +1,31 @@
|
||||
import React from "react";
|
||||
import { formatLayerId } from "../libs/format";
|
||||
import { type LayerSpecification, type StyleSpecification } from "maplibre-gl";
|
||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||
import { type MappedError } from "../libs/definitions";
|
||||
import React from 'react'
|
||||
import {formatLayerId} from '../libs/format';
|
||||
import {LayerSpecification, StyleSpecification} from 'maplibre-gl';
|
||||
|
||||
type AppMessagePanelInternalProps = {
|
||||
errors?: MappedError[]
|
||||
infos?: string[]
|
||||
type AppMessagePanelProps = {
|
||||
errors?: unknown[]
|
||||
infos?: unknown[]
|
||||
mapStyle?: StyleSpecification
|
||||
onLayerSelect?(index: number): void;
|
||||
onLayerSelect?(...args: unknown[]): unknown
|
||||
currentLayer?: LayerSpecification
|
||||
selectedLayerIndex?: number
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalProps> {
|
||||
export default class AppMessagePanel extends React.Component<AppMessagePanelProps> {
|
||||
static defaultProps = {
|
||||
onLayerSelect: () => { },
|
||||
};
|
||||
onLayerSelect: () => {},
|
||||
}
|
||||
|
||||
render() {
|
||||
const { t, selectedLayerIndex } = this.props;
|
||||
const errors = this.props.errors?.map((error, idx) => {
|
||||
const {selectedLayerIndex} = this.props;
|
||||
const errors = this.props.errors?.map((error: any, idx) => {
|
||||
let content;
|
||||
if (error.parsed && error.parsed.type === "layer") {
|
||||
const { parsed } = error;
|
||||
const {parsed} = error;
|
||||
const layerId = this.props.mapStyle?.layers[parsed.data.index].id;
|
||||
content = (
|
||||
<>
|
||||
{t("Layer")} <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
|
||||
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
|
||||
{selectedLayerIndex !== parsed.data.index &&
|
||||
<>
|
||||
—
|
||||
@@ -35,7 +33,7 @@ class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalPro
|
||||
className="maputnik-message-panel__switch-button"
|
||||
onClick={() => this.props.onLayerSelect!(parsed.data.index)}
|
||||
>
|
||||
{t("switch to layer")}
|
||||
switch to layer
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
@@ -45,21 +43,19 @@ class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalPro
|
||||
else {
|
||||
content = error.message;
|
||||
}
|
||||
return <p key={"error-" + idx} className="maputnik-message-panel-error">
|
||||
return <p key={"error-"+idx} className="maputnik-message-panel-error">
|
||||
{content}
|
||||
</p>;
|
||||
});
|
||||
</p>
|
||||
})
|
||||
|
||||
const infos = this.props.infos?.map((m, i) => {
|
||||
return <p key={"info-" + i}>{m}</p>;
|
||||
});
|
||||
return <p key={"info-"+i}>{m}</p>
|
||||
})
|
||||
|
||||
return <div className="maputnik-message-panel">
|
||||
{errors}
|
||||
{infos}
|
||||
</div>;
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
const AppMessagePanel = withTranslation()(AppMessagePanelInternal);
|
||||
export default AppMessagePanel;
|
||||
|
||||
@@ -1,30 +1,16 @@
|
||||
import React from "react";
|
||||
import classnames from "classnames";
|
||||
import {detect} from "detect-browser";
|
||||
import React from 'react'
|
||||
import classnames from 'classnames'
|
||||
import {detect} from 'detect-browser';
|
||||
|
||||
import {
|
||||
MdOpenInBrowser,
|
||||
MdSettings,
|
||||
MdLayers,
|
||||
MdHelpOutline,
|
||||
MdFindInPage,
|
||||
MdLanguage,
|
||||
MdSave,
|
||||
MdPublic,
|
||||
MdCode
|
||||
} from "react-icons/md";
|
||||
import pkgJson from "../../package.json";
|
||||
import {MdFileDownload, MdOpenInBrowser, MdSettings, MdLayers, MdHelpOutline, MdFindInPage} from 'react-icons/md'
|
||||
import pkgJson from '../../package.json'
|
||||
//@ts-ignore
|
||||
import maputnikLogo from "maputnik-design/logos/logo-color.svg?inline";
|
||||
import { withTranslation, type WithTranslation } from "react-i18next";
|
||||
import { supportedLanguages } from "../i18n";
|
||||
import type { OnStyleChangedCallback } from "../libs/definitions";
|
||||
import maputnikLogo from 'maputnik-design/logos/logo-color.svg?inline'
|
||||
|
||||
// This is required because of <https://stackoverflow.com/a/49846426>, there isn't another way to detect support that I'm aware of.
|
||||
const browser = detect();
|
||||
const colorAccessibilityFiltersEnabled = ["chrome", "firefox"].indexOf(browser!.name) > -1;
|
||||
const colorAccessibilityFiltersEnabled = ['chrome', 'firefox'].indexOf(browser!.name) > -1;
|
||||
|
||||
export type ModalTypes = "settings" | "sources" | "open" | "shortcuts" | "export" | "debug" | "globalState" | "codeEditor";
|
||||
|
||||
type IconTextProps = {
|
||||
children?: React.ReactNode
|
||||
@@ -33,7 +19,7 @@ type IconTextProps = {
|
||||
|
||||
class IconText extends React.Component<IconTextProps> {
|
||||
render() {
|
||||
return <span className="maputnik-icon-text">{this.props.children}</span>;
|
||||
return <span className="maputnik-icon-text">{this.props.children}</span>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,19 +27,20 @@ type ToolbarLinkProps = {
|
||||
className?: string
|
||||
children?: React.ReactNode
|
||||
href?: string
|
||||
onToggleModal?(...args: unknown[]): unknown
|
||||
};
|
||||
|
||||
class ToolbarLink extends React.Component<ToolbarLinkProps> {
|
||||
render() {
|
||||
return <a
|
||||
className={classnames("maputnik-toolbar-link", this.props.className)}
|
||||
className={classnames('maputnik-toolbar-link', this.props.className)}
|
||||
href={this.props.href}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
data-wd-key="toolbar:link"
|
||||
>
|
||||
{this.props.children}
|
||||
</a>;
|
||||
</a>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +56,7 @@ class ToolbarSelect extends React.Component<ToolbarSelectProps> {
|
||||
data-wd-key={this.props.wdKey}
|
||||
>
|
||||
{this.props.children}
|
||||
</div>;
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,28 +74,28 @@ class ToolbarAction extends React.Component<ToolbarActionProps> {
|
||||
onClick={this.props.onClick}
|
||||
>
|
||||
{this.props.children}
|
||||
</button>;
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deuteranopia" | "filter-protanopia" | "filter-tritanopia";
|
||||
|
||||
type AppToolbarInternalProps = {
|
||||
type AppToolbarProps = {
|
||||
mapStyle: object
|
||||
inspectModeEnabled: boolean
|
||||
onStyleChanged: OnStyleChangedCallback
|
||||
onStyleChanged(...args: unknown[]): unknown
|
||||
// A new style has been uploaded
|
||||
onStyleOpen: OnStyleChangedCallback
|
||||
onStyleOpen(...args: unknown[]): unknown
|
||||
// A dict of source id's and the available source layers
|
||||
sources: object
|
||||
children?: React.ReactNode
|
||||
onToggleModal(modal: ModalTypes): void
|
||||
onToggleModal(...args: unknown[]): unknown
|
||||
onSetMapState(mapState: MapState): unknown
|
||||
mapState?: MapState
|
||||
renderer?: string
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
||||
export default class AppToolbar extends React.Component<AppToolbarProps> {
|
||||
state = {
|
||||
isOpen: {
|
||||
settings: false,
|
||||
@@ -117,16 +104,12 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
||||
add: false,
|
||||
export: false,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handleSelection(val: MapState) {
|
||||
this.props.onSetMapState(val);
|
||||
}
|
||||
|
||||
handleLanguageChange(val: string) {
|
||||
this.props.i18n.changeLanguage(val);
|
||||
}
|
||||
|
||||
onSkip = (target: string) => {
|
||||
if (target === "map") {
|
||||
(document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus();
|
||||
@@ -135,44 +118,43 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
||||
const el = document.querySelector("#skip-target-"+target) as HTMLButtonElement;
|
||||
el.focus();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const views = [
|
||||
{
|
||||
id: "map",
|
||||
group: "general",
|
||||
title: t("Map"),
|
||||
title: "Map",
|
||||
},
|
||||
{
|
||||
id: "inspect",
|
||||
group: "general",
|
||||
title: t("Inspect"),
|
||||
disabled: this.props.renderer === "ol",
|
||||
title: "Inspect",
|
||||
disabled: this.props.renderer === 'ol',
|
||||
},
|
||||
{
|
||||
id: "filter-deuteranopia",
|
||||
group: "color-accessibility",
|
||||
title: t("Deuteranopia filter"),
|
||||
title: "Deuteranopia filter",
|
||||
disabled: !colorAccessibilityFiltersEnabled,
|
||||
},
|
||||
{
|
||||
id: "filter-protanopia",
|
||||
group: "color-accessibility",
|
||||
title: t("Protanopia filter"),
|
||||
title: "Protanopia filter",
|
||||
disabled: !colorAccessibilityFiltersEnabled,
|
||||
},
|
||||
{
|
||||
id: "filter-tritanopia",
|
||||
group: "color-accessibility",
|
||||
title: t("Tritanopia filter"),
|
||||
title: "Tritanopia filter",
|
||||
disabled: !colorAccessibilityFiltersEnabled,
|
||||
},
|
||||
{
|
||||
id: "filter-achromatopsia",
|
||||
group: "color-accessibility",
|
||||
title: t("Achromatopsia filter"),
|
||||
title: "Achromatopsia filter",
|
||||
disabled: !colorAccessibilityFiltersEnabled,
|
||||
},
|
||||
];
|
||||
@@ -192,21 +174,21 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
||||
className="maputnik-toolbar-skip"
|
||||
onClick={_e => this.onSkip("layer-list")}
|
||||
>
|
||||
{t("Layers list")}
|
||||
Layers list
|
||||
</button>
|
||||
<button
|
||||
data-wd-key="root:skip:layer-editor"
|
||||
className="maputnik-toolbar-skip"
|
||||
onClick={_e => this.onSkip("layer-editor")}
|
||||
>
|
||||
{t("Layer editor")}
|
||||
Layer editor
|
||||
</button>
|
||||
<button
|
||||
data-wd-key="root:skip:map-view"
|
||||
className="maputnik-toolbar-skip"
|
||||
onClick={_e => this.onSkip("map")}
|
||||
>
|
||||
{t("Map view")}
|
||||
Map view
|
||||
</button>
|
||||
<a
|
||||
className="maputnik-toolbar-logo"
|
||||
@@ -214,7 +196,7 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
||||
rel="noreferrer noopener"
|
||||
href="https://github.com/maplibre/maputnik"
|
||||
>
|
||||
<img src={maputnikLogo} alt={t("Maputnik on GitHub")} />
|
||||
<img src={maputnikLogo} alt="Maputnik on GitHub" />
|
||||
<h1>
|
||||
<span className="maputnik-toolbar-name">{pkgJson.name}</span>
|
||||
<span className="maputnik-toolbar-version">v{pkgJson.version}</span>
|
||||
@@ -222,34 +204,26 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
||||
</a>
|
||||
</div>
|
||||
<div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar">
|
||||
<ToolbarAction wdKey="nav:open" onClick={() => this.props.onToggleModal("open")}>
|
||||
<ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}>
|
||||
<MdOpenInBrowser />
|
||||
<IconText>{t("Open")}</IconText>
|
||||
<IconText>Open</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:export" onClick={() => this.props.onToggleModal("export")}>
|
||||
<MdSave />
|
||||
<IconText>{t("Save")}</IconText>
|
||||
<ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, 'export')}>
|
||||
<MdFileDownload />
|
||||
<IconText>Export</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:code-editor" onClick={() => this.props.onToggleModal("codeEditor")}>
|
||||
<MdCode />
|
||||
<IconText>{t("Code Editor")}</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:sources" onClick={() => this.props.onToggleModal("sources")}>
|
||||
<ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, 'sources')}>
|
||||
<MdLayers />
|
||||
<IconText>{t("Data Sources")}</IconText>
|
||||
<IconText>Data Sources</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:settings" onClick={() => this.props.onToggleModal("settings")}>
|
||||
<ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}>
|
||||
<MdSettings />
|
||||
<IconText>{t("Style Settings")}</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:global-state" onClick={() => this.props.onToggleModal("globalState")}>
|
||||
<MdPublic />
|
||||
<IconText>{t("Global State")}</IconText>
|
||||
<IconText>Style Settings</IconText>
|
||||
</ToolbarAction>
|
||||
|
||||
<ToolbarSelect wdKey="nav:inspect">
|
||||
<MdFindInPage />
|
||||
<IconText>{t("View")}
|
||||
<label>View
|
||||
<select
|
||||
className="maputnik-select"
|
||||
data-wd-key="maputnik-select"
|
||||
@@ -263,7 +237,7 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
<optgroup label={t("Color accessibility")}>
|
||||
<optgroup label="Color accessibility">
|
||||
{views.filter(v => v.group === "color-accessibility").map((item) => {
|
||||
return (
|
||||
<option key={item.id} value={item.id} disabled={item.disabled}>
|
||||
@@ -273,38 +247,15 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
||||
})}
|
||||
</optgroup>
|
||||
</select>
|
||||
</IconText>
|
||||
</ToolbarSelect>
|
||||
|
||||
<ToolbarSelect wdKey="nav:language">
|
||||
<MdLanguage />
|
||||
<IconText>Language
|
||||
<select
|
||||
className="maputnik-select"
|
||||
data-wd-key="maputnik-lang-select"
|
||||
onChange={(e) => this.handleLanguageChange(e.target.value)}
|
||||
value={this.props.i18n.language}
|
||||
>
|
||||
{Object.entries(supportedLanguages).map(([code, name]) => {
|
||||
return (
|
||||
<option key={code} value={code}>
|
||||
{name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</IconText>
|
||||
</label>
|
||||
</ToolbarSelect>
|
||||
|
||||
<ToolbarLink href={"https://github.com/maplibre/maputnik/wiki"}>
|
||||
<MdHelpOutline />
|
||||
<IconText>{t("Help")}</IconText>
|
||||
<IconText>Help</IconText>
|
||||
</ToolbarLink>
|
||||
</div>
|
||||
</div>
|
||||
</nav>;
|
||||
</nav>
|
||||
}
|
||||
}
|
||||
|
||||
const AppToolbar = withTranslation()(AppToolbarInternal);
|
||||
export default AppToolbar;
|
||||
|
||||
+18
-19
@@ -1,13 +1,14 @@
|
||||
import React, {type CSSProperties, type PropsWithChildren, type SyntheticEvent} from "react";
|
||||
import classnames from "classnames";
|
||||
import FieldDocLabel from "./FieldDocLabel";
|
||||
import Doc from "./Doc";
|
||||
import React, {SyntheticEvent} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import FieldDocLabel from './FieldDocLabel'
|
||||
import Doc from './Doc'
|
||||
|
||||
export type BlockProps = PropsWithChildren & {
|
||||
|
||||
type BlockProps = {
|
||||
"data-wd-key"?: string
|
||||
label?: string
|
||||
action?: React.ReactElement
|
||||
style?: CSSProperties
|
||||
style?: object
|
||||
onChange?(...args: unknown[]): unknown
|
||||
fieldSpec?: object
|
||||
wideMode?: boolean
|
||||
@@ -26,13 +27,13 @@ export default class Block extends React.Component<BlockProps, BlockState> {
|
||||
super(props);
|
||||
this.state = {
|
||||
showDoc: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
onChange(e: React.BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) {
|
||||
const value = e.target.value;
|
||||
const value = e.target.value
|
||||
if (this.props.onChange) {
|
||||
return this.props.onChange(value === "" ? undefined : value);
|
||||
return this.props.onChange(value === "" ? undefined : value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +41,7 @@ export default class Block extends React.Component<BlockProps, BlockState> {
|
||||
this.setState({
|
||||
showDoc: val
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Some fields for example <InputColor/> bind click events inside the element
|
||||
@@ -55,10 +56,8 @@ export default class Block extends React.Component<BlockProps, BlockState> {
|
||||
if (event.nativeEvent.target.nodeName !== "INPUT" && !contains) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
if (event.nativeEvent.target.nodeName !== "A") {
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
render() {
|
||||
return <label style={this.props.style}
|
||||
@@ -66,8 +65,7 @@ export default class Block extends React.Component<BlockProps, BlockState> {
|
||||
className={classnames({
|
||||
"maputnik-input-block": true,
|
||||
"maputnik-input-block--wide": this.props.wideMode,
|
||||
"maputnik-action-block": this.props.action,
|
||||
"maputnik-input-block--error": this.props.error
|
||||
"maputnik-action-block": this.props.action
|
||||
})}
|
||||
onClick={this.onLabelClick}
|
||||
>
|
||||
@@ -88,17 +86,18 @@ export default class Block extends React.Component<BlockProps, BlockState> {
|
||||
<div className="maputnik-input-block-action">
|
||||
{this.props.action}
|
||||
</div>
|
||||
<div className="maputnik-input-block-content" ref={el => {this._blockEl = el;}}>
|
||||
<div className="maputnik-input-block-content" ref={el => this._blockEl = el}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
{this.props.fieldSpec &&
|
||||
<div
|
||||
className="maputnik-doc-inline"
|
||||
style={{display: this.state.showDoc ? "" : "none"}}
|
||||
style={{display: this.state.showDoc ? '' : 'none'}}
|
||||
>
|
||||
<Doc fieldSpec={this.props.fieldSpec} />
|
||||
</div>
|
||||
}
|
||||
</label>;
|
||||
</label>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import InputJson from "./InputJson";
|
||||
import React from "react";
|
||||
import { withTranslation, type WithTranslation } from "react-i18next";
|
||||
import { type StyleSpecification } from "maplibre-gl";
|
||||
import { type StyleSpecificationWithId } from "../libs/definitions";
|
||||
|
||||
export type CodeEditorProps = {
|
||||
value: StyleSpecification;
|
||||
onChange: (value: StyleSpecificationWithId) => void;
|
||||
onClose: () => void;
|
||||
} & WithTranslation;
|
||||
|
||||
const CodeEditorInternal: React.FC<CodeEditorProps> = (props) => {
|
||||
|
||||
return <>
|
||||
<button className="maputnik-button" onClick={props.onClose} aria-label={props.t("Close")} style={{ position: "sticky", top: "0", zIndex: 1 }}>{props.t("Click to close the editor")}</button>
|
||||
<InputJson
|
||||
lintType="style"
|
||||
value={props.value}
|
||||
onChange={props.onChange}
|
||||
className={"maputnik-code-editor"}
|
||||
withScroll={true}
|
||||
/>;
|
||||
</>;
|
||||
};
|
||||
|
||||
const CodeEditor = withTranslation()(CodeEditorInternal);
|
||||
|
||||
export default CodeEditor;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { Collapse as ReactCollapse } from "react-collapse";
|
||||
import {reducedMotionEnabled} from "../libs/accessibility";
|
||||
import React from 'react'
|
||||
import { Collapse as ReactCollapse } from 'react-collapse'
|
||||
import {reducedMotionEnabled} from '../libs/accessibility'
|
||||
|
||||
|
||||
type CollapseProps = {
|
||||
@@ -12,7 +12,7 @@ type CollapseProps = {
|
||||
export default class Collapse extends React.Component<CollapseProps> {
|
||||
static defaultProps = {
|
||||
isActive: true
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
if (reducedMotionEnabled()) {
|
||||
@@ -20,14 +20,15 @@ export default class Collapse extends React.Component<CollapseProps> {
|
||||
<div style={{display: this.props.isActive ? "block" : "none"}}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<ReactCollapse isOpened={this.props.isActive}>
|
||||
{this.props.children}
|
||||
</ReactCollapse>
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import {MdArrowDropDown, MdArrowDropUp} from "react-icons/md";
|
||||
import React from 'react'
|
||||
import {MdArrowDropDown, MdArrowDropUp} from 'react-icons/md'
|
||||
|
||||
type CollapserProps = {
|
||||
isCollapsed: boolean
|
||||
@@ -12,7 +12,8 @@ export default class Collapser extends React.Component<CollapserProps> {
|
||||
width: 20,
|
||||
height: 20,
|
||||
...this.props.style,
|
||||
};
|
||||
return this.props.isCollapsed ? <MdArrowDropUp style={iconStyle}/> : <MdArrowDropDown style={iconStyle} />;
|
||||
}
|
||||
return this.props.isCollapsed ? <MdArrowDropUp style={iconStyle}/> : <MdArrowDropDown style={iconStyle} />
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
-28
@@ -1,10 +1,10 @@
|
||||
import React from "react";
|
||||
import Markdown from "react-markdown";
|
||||
import React from 'react'
|
||||
|
||||
const headers = {
|
||||
js: "JS",
|
||||
android: "Android",
|
||||
ios: "iOS"
|
||||
ios: "iOS",
|
||||
macos: "macOS",
|
||||
};
|
||||
|
||||
type DocProps = {
|
||||
@@ -15,11 +15,9 @@ type DocProps = {
|
||||
doc?: string
|
||||
}
|
||||
}
|
||||
"sdk-support"?: {
|
||||
'sdk-support'?: {
|
||||
[key: string]: typeof headers
|
||||
}
|
||||
docUrl?: string,
|
||||
docUrlLinkText?: string
|
||||
}
|
||||
};
|
||||
|
||||
@@ -27,8 +25,8 @@ export default class Doc extends React.Component<DocProps> {
|
||||
render () {
|
||||
const {fieldSpec} = this.props;
|
||||
|
||||
const {doc, values, docUrl, docUrlLinkText} = fieldSpec;
|
||||
const sdkSupport = fieldSpec["sdk-support"];
|
||||
const {doc, values} = fieldSpec;
|
||||
const sdkSupport = fieldSpec['sdk-support'];
|
||||
|
||||
const renderValues = (
|
||||
!!values &&
|
||||
@@ -37,23 +35,11 @@ export default class Doc extends React.Component<DocProps> {
|
||||
!Array.isArray(values)
|
||||
);
|
||||
|
||||
const sdkSupportToJsx = (value: string) => {
|
||||
const supportValue = value.toLowerCase();
|
||||
if (supportValue.startsWith("https://")) {
|
||||
return <a href={supportValue} target="_blank" rel="noreferrer">{"#" + supportValue.split("/").pop()}</a>;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{doc &&
|
||||
<div className="SpecDoc">
|
||||
<div className="SpecDoc__doc" data-wd-key='spec-field-doc'>
|
||||
<Markdown components={{
|
||||
a: ({node: _node, href, children, ...props}) => <a href={href} target="_blank" {...props}>{children}</a>,
|
||||
}}>{doc}</Markdown>
|
||||
</div>
|
||||
<div className="SpecDoc__doc" data-wd-key='spec-field-doc'>{doc}</div>
|
||||
{renderValues &&
|
||||
<ul className="SpecDoc__values">
|
||||
{Object.entries(values).map(([key, value]) => {
|
||||
@@ -86,7 +72,7 @@ export default class Doc extends React.Component<DocProps> {
|
||||
<td>{key}</td>
|
||||
{Object.keys(headers).map((k) => {
|
||||
if (Object.prototype.hasOwnProperty.call(supportObj, k)) {
|
||||
return <td key={k}>{sdkSupportToJsx(supportObj[k as keyof typeof headers])}</td>;
|
||||
return <td key={k}>{supportObj[k as keyof typeof headers]}</td>;
|
||||
}
|
||||
else {
|
||||
return <td key={k}>no</td>;
|
||||
@@ -99,12 +85,7 @@ export default class Doc extends React.Component<DocProps> {
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
{docUrl && docUrlLinkText &&
|
||||
<div className="SpecDoc__learn-more">
|
||||
<a href={docUrl} target="_blank" rel="noreferrer">{docUrlLinkText}</a>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import InputArray, { type InputArrayProps } from "./InputArray";
|
||||
import Fieldset from "./Fieldset";
|
||||
import React from 'react'
|
||||
import InputArray, { FieldArrayProps as InputArrayProps } from './InputArray'
|
||||
import Fieldset from './Fieldset'
|
||||
|
||||
type FieldArrayProps = InputArrayProps & {
|
||||
name?: string
|
||||
@@ -8,12 +9,11 @@ type FieldArrayProps = InputArrayProps & {
|
||||
}
|
||||
};
|
||||
|
||||
const FieldArray: React.FC<FieldArrayProps> = (props) => {
|
||||
return (
|
||||
<Fieldset label={props.label} fieldSpec={props.fieldSpec}>
|
||||
<InputArray {...props} />
|
||||
export default class FieldArray extends React.Component<FieldArrayProps> {
|
||||
render() {
|
||||
return <Fieldset label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||
<InputArray {...this.props} />
|
||||
</Fieldset>
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default FieldArray;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Block from "./Block";
|
||||
import InputAutocomplete, { type InputAutocompleteProps } from "./InputAutocomplete";
|
||||
import React from 'react'
|
||||
import Block from './Block'
|
||||
import InputAutocomplete, { InputAutocompleteProps } from './InputAutocomplete'
|
||||
|
||||
|
||||
type FieldAutocompleteProps = InputAutocompleteProps & {
|
||||
@@ -7,12 +8,11 @@ type FieldAutocompleteProps = InputAutocompleteProps & {
|
||||
};
|
||||
|
||||
|
||||
const FieldAutocomplete: React.FC<FieldAutocompleteProps> = (props) => {
|
||||
return (
|
||||
<Block label={props.label}>
|
||||
<InputAutocomplete {...props} />
|
||||
export default class FieldAutocomplete extends React.Component<FieldAutocompleteProps> {
|
||||
render() {
|
||||
return <Block label={this.props.label}>
|
||||
<InputAutocomplete {...this.props} />
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default FieldAutocomplete;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Block from "./Block";
|
||||
import InputCheckbox, {type InputCheckboxProps} from "./InputCheckbox";
|
||||
import React from 'react'
|
||||
import Block from './Block'
|
||||
import InputCheckbox, {InputCheckboxProps} from './InputCheckbox'
|
||||
|
||||
|
||||
type FieldCheckboxProps = InputCheckboxProps & {
|
||||
@@ -7,12 +8,11 @@ type FieldCheckboxProps = InputCheckboxProps & {
|
||||
};
|
||||
|
||||
|
||||
const FieldCheckbox: React.FC<FieldCheckboxProps> = (props) => {
|
||||
return (
|
||||
<Block label={props.label}>
|
||||
<InputCheckbox {...props} />
|
||||
export default class FieldCheckbox extends React.Component<FieldCheckboxProps> {
|
||||
render() {
|
||||
return <Block label={this.props.label}>
|
||||
<InputCheckbox {...this.props} />
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default FieldCheckbox;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Block from "./Block";
|
||||
import InputColor, {type InputColorProps} from "./InputColor";
|
||||
import React from 'react'
|
||||
import Block from './Block'
|
||||
import InputColor, {InputColorProps} from './InputColor'
|
||||
|
||||
|
||||
type FieldColorProps = InputColorProps & {
|
||||
@@ -10,12 +11,11 @@ type FieldColorProps = InputColorProps & {
|
||||
};
|
||||
|
||||
|
||||
const FieldColor: React.FC<FieldColorProps> = (props) => {
|
||||
return (
|
||||
<Block label={props.label} fieldSpec={props.fieldSpec}>
|
||||
<InputColor {...props} />
|
||||
export default class FieldColor extends React.Component<FieldColorProps> {
|
||||
render() {
|
||||
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||
<InputColor {...this.props} />
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default FieldColor;
|
||||
|
||||
@@ -1,40 +1,33 @@
|
||||
import React from "react";
|
||||
import React from 'react'
|
||||
|
||||
import Block from "./Block";
|
||||
import InputString from "./InputString";
|
||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||
import Block from './Block'
|
||||
import InputString from './InputString'
|
||||
|
||||
type FieldCommentInternalProps = {
|
||||
type FieldCommentProps = {
|
||||
value?: string
|
||||
onChange(value: string | undefined): unknown
|
||||
error: {message: string}
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
const FieldCommentInternal: React.FC<FieldCommentInternalProps> = (props) => {
|
||||
const t = props.t;
|
||||
const fieldSpec = {
|
||||
doc: t(
|
||||
"Comments for the current layer. This is non-standard and not in the spec."
|
||||
),
|
||||
};
|
||||
export default class FieldComment extends React.Component<FieldCommentProps> {
|
||||
render() {
|
||||
const fieldSpec = {
|
||||
doc: "Comments for the current layer. This is non-standard and not in the spec."
|
||||
};
|
||||
|
||||
return (
|
||||
<Block
|
||||
label={t("Comments")}
|
||||
return <Block
|
||||
label={"Comments"}
|
||||
fieldSpec={fieldSpec}
|
||||
data-wd-key="layer-comment"
|
||||
error={props.error}
|
||||
error={this.props.error}
|
||||
>
|
||||
<InputString
|
||||
multi={true}
|
||||
value={props.value}
|
||||
onChange={props.onChange}
|
||||
default={t("Comment...")}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
default="Comment..."
|
||||
data-wd-key="layer-comment.input"
|
||||
/>
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
|
||||
const FieldComment = withTranslation()(FieldCommentInternal);
|
||||
export default FieldComment;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +1,65 @@
|
||||
import React, { type JSX } from "react";
|
||||
import {MdInfoOutline, MdHighlightOff} from "react-icons/md";
|
||||
import React from 'react'
|
||||
import {MdInfoOutline, MdHighlightOff} from 'react-icons/md'
|
||||
|
||||
type FieldDocLabelProps = {
|
||||
label: JSX.Element | string | undefined
|
||||
label: object | string | undefined
|
||||
fieldSpec?: {
|
||||
doc?: string
|
||||
}
|
||||
onToggleDoc?(...args: unknown[]): unknown
|
||||
};
|
||||
|
||||
type FieldDocLabelState = {
|
||||
open: boolean
|
||||
};
|
||||
|
||||
const FieldDocLabel: React.FC<FieldDocLabelProps> = (props) => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const onToggleDoc = (state: boolean) => {
|
||||
setOpen(state);
|
||||
if (props.onToggleDoc) {
|
||||
props.onToggleDoc(state);
|
||||
export default class FieldDocLabel extends React.Component<FieldDocLabelProps, FieldDocLabelState> {
|
||||
constructor (props: FieldDocLabelProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
open: false,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const { label, fieldSpec } = props;
|
||||
const { doc } = fieldSpec || {};
|
||||
onToggleDoc = (open: boolean) => {
|
||||
this.setState({
|
||||
open,
|
||||
}, () => {
|
||||
if (this.props.onToggleDoc) {
|
||||
this.props.onToggleDoc(this.state.open);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (doc) {
|
||||
return (
|
||||
<label className="maputnik-doc-wrapper">
|
||||
render() {
|
||||
const {label, fieldSpec} = this.props;
|
||||
const {doc} = fieldSpec || {};
|
||||
|
||||
if (doc) {
|
||||
return <label className="maputnik-doc-wrapper">
|
||||
<div className="maputnik-doc-target">
|
||||
{label}
|
||||
{"\xa0"}
|
||||
{'\xa0'}
|
||||
<button
|
||||
aria-label={open ? "close property documentation" : "open property documentation"}
|
||||
className={`maputnik-doc-button maputnik-doc-button--${open ? "open" : "closed"}`}
|
||||
onClick={() => onToggleDoc(!open)}
|
||||
data-wd-key={"field-doc-button-" + label}
|
||||
aria-label={this.state.open ? "close property documentation" : "open property documentation"}
|
||||
className={`maputnik-doc-button maputnik-doc-button--${this.state.open ? 'open' : 'closed'}`}
|
||||
onClick={() => this.onToggleDoc(!this.state.open)}
|
||||
data-wd-key={'field-doc-button-'+label}
|
||||
>
|
||||
{open ? <MdHighlightOff /> : <MdInfoOutline />}
|
||||
{this.state.open ? <MdHighlightOff /> : <MdInfoOutline />}
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
} else if (label) {
|
||||
return (
|
||||
<label className="maputnik-doc-wrapper">
|
||||
<div className="maputnik-doc-target">{label}</div>
|
||||
}
|
||||
else if (label) {
|
||||
return <label className="maputnik-doc-wrapper">
|
||||
<div className="maputnik-doc-target">
|
||||
{label}
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
else {
|
||||
<div />
|
||||
}
|
||||
}
|
||||
return <div />;
|
||||
};
|
||||
|
||||
export default FieldDocLabel;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import InputDynamicArray, {type InputDynamicArrayProps} from "./InputDynamicArray";
|
||||
import Fieldset from "./Fieldset";
|
||||
import React from 'react'
|
||||
import InputDynamicArray, {FieldDynamicArrayProps as InputDynamicArrayProps} from './InputDynamicArray'
|
||||
import Fieldset from './Fieldset'
|
||||
|
||||
type FieldDynamicArrayProps = InputDynamicArrayProps & {
|
||||
name?: string
|
||||
};
|
||||
|
||||
const FieldDynamicArray: React.FC<FieldDynamicArrayProps> = (props) => {
|
||||
return (
|
||||
<Fieldset label={props.label}>
|
||||
<InputDynamicArray {...props} />
|
||||
export default class FieldDynamicArray extends React.Component<FieldDynamicArrayProps> {
|
||||
render() {
|
||||
return <Fieldset label={this.props.label}>
|
||||
<InputDynamicArray {...this.props} />
|
||||
</Fieldset>
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default FieldDynamicArray;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import InputEnum, {type InputEnumProps} from "./InputEnum";
|
||||
import Fieldset from "./Fieldset";
|
||||
import React from 'react'
|
||||
import InputEnum, {InputEnumProps} from './InputEnum'
|
||||
import Fieldset from './Fieldset';
|
||||
|
||||
|
||||
type FieldEnumProps = InputEnumProps & {
|
||||
type FieldEnumProps = InputEnumProps & {
|
||||
label?: string;
|
||||
fieldSpec?: {
|
||||
doc: string
|
||||
@@ -10,12 +11,10 @@ type FieldEnumProps = InputEnumProps & {
|
||||
};
|
||||
|
||||
|
||||
const FieldEnum: React.FC<FieldEnumProps> = (props) => {
|
||||
return (
|
||||
<Fieldset label={props.label} fieldSpec={props.fieldSpec}>
|
||||
<InputEnum {...props} />
|
||||
export default class FieldEnum extends React.Component<FieldEnumProps> {
|
||||
render() {
|
||||
return <Fieldset label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||
<InputEnum {...this.props} />
|
||||
</Fieldset>
|
||||
);
|
||||
};
|
||||
|
||||
export default FieldEnum;
|
||||
}
|
||||
}
|
||||
+231
-230
@@ -1,12 +1,11 @@
|
||||
import React from "react";
|
||||
import React from 'react'
|
||||
|
||||
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";
|
||||
import SpecProperty from './_SpecProperty'
|
||||
import DataProperty, { 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';
|
||||
|
||||
|
||||
function isLiteralExpression(value: any) {
|
||||
@@ -23,9 +22,9 @@ function isGetExpression(value: any) {
|
||||
|
||||
function isZoomField(value: any) {
|
||||
return (
|
||||
typeof(value) === "object" &&
|
||||
typeof(value) === 'object' &&
|
||||
value.stops &&
|
||||
typeof(value.property) === "undefined" &&
|
||||
typeof(value.property) === 'undefined' &&
|
||||
Array.isArray(value.stops) &&
|
||||
value.stops.length > 1 &&
|
||||
value.stops.every((stop: Stop) => {
|
||||
@@ -39,7 +38,7 @@ function isZoomField(value: any) {
|
||||
|
||||
function isIdentityProperty(value: any) {
|
||||
return (
|
||||
typeof(value) === "object" &&
|
||||
typeof(value) === 'object' &&
|
||||
value.type === "identity" &&
|
||||
Object.prototype.hasOwnProperty.call(value, "property")
|
||||
);
|
||||
@@ -47,16 +46,16 @@ function isIdentityProperty(value: any) {
|
||||
|
||||
function isDataStopProperty(value: any) {
|
||||
return (
|
||||
typeof(value) === "object" &&
|
||||
typeof(value) === 'object' &&
|
||||
value.stops &&
|
||||
typeof(value.property) !== "undefined" &&
|
||||
typeof(value.property) !== 'undefined' &&
|
||||
value.stops.length > 1 &&
|
||||
Array.isArray(value.stops) &&
|
||||
value.stops.every((stop: Stop) => {
|
||||
return (
|
||||
Array.isArray(stop) &&
|
||||
stop.length === 2 &&
|
||||
typeof(stop[0]) === "object"
|
||||
typeof(stop[0]) === 'object'
|
||||
);
|
||||
})
|
||||
);
|
||||
@@ -91,18 +90,6 @@ function getDataType(value: any, fieldSpec={} as any) {
|
||||
else if (fieldSpec.type === "array" && isArrayOfPrimatives(value)) {
|
||||
return "value";
|
||||
}
|
||||
else if (fieldSpec.type === "numberArray" && isArrayOfPrimatives(value)) {
|
||||
return "value";
|
||||
}
|
||||
else if (fieldSpec.type === "colorArray") {
|
||||
return "value";
|
||||
}
|
||||
else if (fieldSpec.type === "padding") {
|
||||
return "value";
|
||||
}
|
||||
else if (fieldSpec.type === "variableAnchorOffsetCollection") {
|
||||
return "value";
|
||||
}
|
||||
else if (isZoomField(value)) {
|
||||
return "zoom_function";
|
||||
}
|
||||
@@ -120,287 +107,301 @@ type FieldFunctionProps = {
|
||||
fieldName: string
|
||||
fieldType: string
|
||||
fieldSpec: any
|
||||
errors?: MappedLayerErrors
|
||||
errors?: {[key: string]: {message: string}}
|
||||
value?: any
|
||||
};
|
||||
|
||||
type FieldFunctionState = {
|
||||
dataType: string
|
||||
isEditing: boolean
|
||||
}
|
||||
|
||||
/** Supports displaying spec field for zoom function objects
|
||||
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
|
||||
*/
|
||||
const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
|
||||
const [dataType, setDataType] = React.useState(
|
||||
getDataType(props.value, props.fieldSpec)
|
||||
);
|
||||
const [isEditing, setIsEditing] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isEditing) {
|
||||
setDataType(getDataType(props.value, props.fieldSpec));
|
||||
export default class FieldFunction extends React.Component<FieldFunctionProps, FieldFunctionState> {
|
||||
constructor (props: FieldFunctionProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
dataType: getDataType(props.value, props.fieldSpec),
|
||||
isEditing: false,
|
||||
}
|
||||
}, [props.value, props.fieldSpec, isEditing]);
|
||||
}
|
||||
|
||||
const getFieldFunctionType = (fieldSpec: any) => {
|
||||
static getDerivedStateFromProps(props: FieldFunctionProps, state: FieldFunctionState) {
|
||||
// Because otherwise when editing values we end up accidentally changing field type.
|
||||
if (state.isEditing) {
|
||||
return {};
|
||||
}
|
||||
else {
|
||||
return {
|
||||
isEditing: false,
|
||||
dataType: getDataType(props.value, props.fieldSpec)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
getFieldFunctionType(fieldSpec: any) {
|
||||
if (fieldSpec.expression.interpolated) {
|
||||
return "exponential";
|
||||
return "exponential"
|
||||
}
|
||||
if (fieldSpec.type === "number") {
|
||||
return "interval";
|
||||
return "interval"
|
||||
}
|
||||
return "categorical";
|
||||
};
|
||||
return "categorical"
|
||||
}
|
||||
|
||||
const addStop = () => {
|
||||
const stops = props.value.stops.slice(0);
|
||||
const lastStop = stops[stops.length - 1];
|
||||
addStop = () => {
|
||||
const stops = this.props.value.stops.slice(0)
|
||||
const lastStop = stops[stops.length - 1]
|
||||
if (typeof lastStop[0] === "object") {
|
||||
stops.push([
|
||||
{ zoom: lastStop[0].zoom + 1, value: lastStop[0].value },
|
||||
lastStop[1],
|
||||
]);
|
||||
} else {
|
||||
stops.push([lastStop[0] + 1, lastStop[1]]);
|
||||
{zoom: lastStop[0].zoom + 1, value: lastStop[0].value},
|
||||
lastStop[1]
|
||||
])
|
||||
}
|
||||
else {
|
||||
stops.push([lastStop[0] + 1, lastStop[1]])
|
||||
}
|
||||
|
||||
const changedValue = {
|
||||
...props.value,
|
||||
...this.props.value,
|
||||
stops: stops,
|
||||
};
|
||||
|
||||
props.onChange(props.fieldName, changedValue);
|
||||
};
|
||||
|
||||
const deleteExpression = () => {
|
||||
const { fieldSpec, fieldName } = props;
|
||||
props.onChange(fieldName, fieldSpec.default);
|
||||
setDataType("value");
|
||||
};
|
||||
|
||||
const deleteStop = (stopIdx: number) => {
|
||||
const stops = props.value.stops.slice(0);
|
||||
stops.splice(stopIdx, 1);
|
||||
|
||||
let changedValue: any = {
|
||||
...props.value,
|
||||
stops: stops,
|
||||
};
|
||||
|
||||
if (stops.length === 1) {
|
||||
changedValue = stops[0][1];
|
||||
}
|
||||
|
||||
props.onChange(props.fieldName, changedValue);
|
||||
};
|
||||
this.props.onChange(this.props.fieldName, changedValue)
|
||||
}
|
||||
|
||||
const makeZoomFunction = () => {
|
||||
const { value } = props;
|
||||
deleteExpression = () => {
|
||||
const {fieldSpec, fieldName} = this.props;
|
||||
this.props.onChange(fieldName, fieldSpec.default);
|
||||
this.setState({
|
||||
dataType: "value",
|
||||
});
|
||||
}
|
||||
|
||||
let zoomFunc: any;
|
||||
if (typeof value === "object") {
|
||||
deleteStop = (stopIdx: number) => {
|
||||
const stops = this.props.value.stops.slice(0)
|
||||
stops.splice(stopIdx, 1)
|
||||
|
||||
let changedValue = {
|
||||
...this.props.value,
|
||||
stops: stops,
|
||||
}
|
||||
|
||||
if(stops.length === 1) {
|
||||
changedValue = stops[0][1]
|
||||
}
|
||||
|
||||
this.props.onChange(this.props.fieldName, changedValue)
|
||||
}
|
||||
|
||||
makeZoomFunction = () => {
|
||||
const {value} = this.props;
|
||||
|
||||
let zoomFunc;
|
||||
if (typeof(value) === "object") {
|
||||
if (value.stops) {
|
||||
zoomFunc = {
|
||||
base: value.base,
|
||||
stops: value.stops.map((stop: Stop) => {
|
||||
return [stop[0].zoom, stop[1] || findDefaultFromSpec(props.fieldSpec)];
|
||||
}),
|
||||
};
|
||||
} else {
|
||||
return [stop[0].zoom, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
|
||||
})
|
||||
}
|
||||
}
|
||||
else {
|
||||
zoomFunc = {
|
||||
base: value.base,
|
||||
stops: [
|
||||
[6, findDefaultFromSpec(props.fieldSpec)],
|
||||
[10, findDefaultFromSpec(props.fieldSpec)],
|
||||
],
|
||||
};
|
||||
[6, findDefaultFromSpec(this.props.fieldSpec)],
|
||||
[10, findDefaultFromSpec(this.props.fieldSpec)]
|
||||
]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
zoomFunc = {
|
||||
stops: [
|
||||
[6, value || findDefaultFromSpec(props.fieldSpec)],
|
||||
[10, value || findDefaultFromSpec(props.fieldSpec)],
|
||||
],
|
||||
};
|
||||
[6, value || findDefaultFromSpec(this.props.fieldSpec)],
|
||||
[10, value || findDefaultFromSpec(this.props.fieldSpec)]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
props.onChange(props.fieldName, zoomFunc);
|
||||
};
|
||||
this.props.onChange(this.props.fieldName, zoomFunc)
|
||||
}
|
||||
|
||||
const undoExpression = () => {
|
||||
const { value, fieldName } = props;
|
||||
undoExpression = () => {
|
||||
const {value, fieldName} = this.props;
|
||||
|
||||
if (isGetExpression(value)) {
|
||||
props.onChange(fieldName, {
|
||||
type: "identity",
|
||||
property: value[1],
|
||||
this.props.onChange(fieldName, {
|
||||
"type": "identity",
|
||||
"property": value[1]
|
||||
});
|
||||
this.setState({
|
||||
dataType: "value",
|
||||
});
|
||||
setDataType("value");
|
||||
} else if (isLiteralExpression(value)) {
|
||||
props.onChange(fieldName, value[1]);
|
||||
setDataType("value");
|
||||
}
|
||||
};
|
||||
else if (isLiteralExpression(value)) {
|
||||
this.props.onChange(fieldName, value[1]);
|
||||
this.setState({
|
||||
dataType: "value",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const canUndo = () => {
|
||||
const { value, fieldSpec } = props;
|
||||
canUndo = () => {
|
||||
const {value, fieldSpec} = this.props;
|
||||
return (
|
||||
isGetExpression(value) ||
|
||||
isLiteralExpression(value) ||
|
||||
isPrimative(value) ||
|
||||
(Array.isArray(value) && fieldSpec.type === "array")
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const makeExpression = () => {
|
||||
const { value, fieldSpec } = props;
|
||||
makeExpression = () => {
|
||||
const {value, fieldSpec} = this.props;
|
||||
let expression;
|
||||
|
||||
if (typeof value === "object" && "stops" in value) {
|
||||
if (typeof(value) === "object" && 'stops' in value) {
|
||||
expression = styleFunction.convertFunction(value, fieldSpec);
|
||||
} else if (isIdentityProperty(value)) {
|
||||
expression = ["get", value.property];
|
||||
} else {
|
||||
expression = ["literal", value || props.fieldSpec.default];
|
||||
}
|
||||
props.onChange(props.fieldName, expression);
|
||||
};
|
||||
else if (isIdentityProperty(value)) {
|
||||
expression = ["get", value.property];
|
||||
}
|
||||
else {
|
||||
expression = ["literal", value || this.props.fieldSpec.default];
|
||||
}
|
||||
this.props.onChange(this.props.fieldName, expression);
|
||||
}
|
||||
|
||||
const makeDataFunction = () => {
|
||||
const functionType = getFieldFunctionType(props.fieldSpec);
|
||||
const stopValue = functionType === "categorical" ? "" : 0;
|
||||
const { value } = props;
|
||||
makeDataFunction = () => {
|
||||
const functionType = this.getFieldFunctionType(this.props.fieldSpec);
|
||||
const stopValue = functionType === 'categorical' ? '' : 0;
|
||||
const {value} = this.props;
|
||||
let dataFunc;
|
||||
|
||||
if (typeof value === "object") {
|
||||
if (typeof(value) === "object") {
|
||||
if (value.stops) {
|
||||
dataFunc = {
|
||||
property: "",
|
||||
type: functionType,
|
||||
base: value.base,
|
||||
stops: value.stops.map((stop: Stop) => {
|
||||
return [{ zoom: stop[0], value: stopValue }, stop[1] || findDefaultFromSpec(props.fieldSpec)];
|
||||
}),
|
||||
};
|
||||
} else {
|
||||
return [{zoom: stop[0], value: stopValue}, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
|
||||
})
|
||||
}
|
||||
}
|
||||
else {
|
||||
dataFunc = {
|
||||
property: "",
|
||||
type: functionType,
|
||||
base: value.base,
|
||||
stops: [
|
||||
[{ zoom: 6, value: stopValue }, findDefaultFromSpec(props.fieldSpec)],
|
||||
[{ zoom: 10, value: stopValue }, findDefaultFromSpec(props.fieldSpec)],
|
||||
],
|
||||
};
|
||||
[{zoom: 6, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)],
|
||||
[{zoom: 10, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)]
|
||||
]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
dataFunc = {
|
||||
property: "",
|
||||
type: functionType,
|
||||
base: value.base,
|
||||
stops: [
|
||||
[{ zoom: 6, value: stopValue }, props.value || findDefaultFromSpec(props.fieldSpec)],
|
||||
[{ zoom: 10, value: stopValue }, props.value || findDefaultFromSpec(props.fieldSpec)],
|
||||
],
|
||||
};
|
||||
[{zoom: 6, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)],
|
||||
[{zoom: 10, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
props.onChange(props.fieldName, dataFunc);
|
||||
};
|
||||
|
||||
const makeElevationFunction = () => {
|
||||
const expression = [
|
||||
"interpolate",
|
||||
["linear"],
|
||||
["elevation"],
|
||||
0,
|
||||
"black",
|
||||
2000,
|
||||
"white"
|
||||
];
|
||||
|
||||
props.onChange(props.fieldName, expression);
|
||||
};
|
||||
|
||||
const onMarkEditing = () => {
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
const onUnmarkEditing = () => {
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
const propClass =
|
||||
props.fieldSpec.default === props.value ? "maputnik-default-property" : "maputnik-modified-property";
|
||||
|
||||
let specField;
|
||||
|
||||
if (dataType === "expression") {
|
||||
specField = (
|
||||
<ExpressionProperty
|
||||
errors={props.errors}
|
||||
onChange={props.onChange.bind(null, props.fieldName)}
|
||||
canUndo={canUndo}
|
||||
onUndo={undoExpression}
|
||||
onDelete={deleteExpression}
|
||||
fieldType={props.fieldType}
|
||||
fieldName={props.fieldName}
|
||||
fieldSpec={props.fieldSpec}
|
||||
value={props.value}
|
||||
onFocus={onMarkEditing}
|
||||
onBlur={onUnmarkEditing}
|
||||
/>
|
||||
);
|
||||
} else if (dataType === "zoom_function") {
|
||||
specField = (
|
||||
<ZoomProperty
|
||||
errors={props.errors}
|
||||
onChange={props.onChange.bind(null)}
|
||||
fieldType={props.fieldType}
|
||||
fieldName={props.fieldName}
|
||||
fieldSpec={props.fieldSpec}
|
||||
value={props.value}
|
||||
onDeleteStop={deleteStop}
|
||||
onAddStop={addStop}
|
||||
onChangeToDataFunction={makeDataFunction}
|
||||
onExpressionClick={makeExpression}
|
||||
/>
|
||||
);
|
||||
} else if (dataType === "data_function") {
|
||||
specField = (
|
||||
<DataProperty
|
||||
errors={props.errors}
|
||||
onChange={props.onChange.bind(null)}
|
||||
fieldType={props.fieldType}
|
||||
fieldName={props.fieldName}
|
||||
fieldSpec={props.fieldSpec}
|
||||
value={props.value}
|
||||
onDeleteStop={deleteStop}
|
||||
onAddStop={addStop}
|
||||
onChangeToZoomFunction={makeZoomFunction}
|
||||
onExpressionClick={makeExpression}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
specField = (
|
||||
<SpecProperty
|
||||
errors={props.errors}
|
||||
onChange={props.onChange.bind(null)}
|
||||
fieldType={props.fieldType}
|
||||
fieldName={props.fieldName}
|
||||
fieldSpec={props.fieldSpec}
|
||||
value={props.value}
|
||||
onZoomClick={makeZoomFunction}
|
||||
onDataClick={makeDataFunction}
|
||||
onExpressionClick={makeExpression}
|
||||
onElevationClick={makeElevationFunction}
|
||||
/>
|
||||
);
|
||||
this.props.onChange(this.props.fieldName, dataFunc)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={propClass} data-wd-key={"spec-field-container:" + props.fieldName}>
|
||||
onMarkEditing = () => {
|
||||
this.setState({isEditing: true});
|
||||
}
|
||||
|
||||
onUnmarkEditing = () => {
|
||||
this.setState({isEditing: false});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {dataType} = this.state;
|
||||
const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property"
|
||||
let specField;
|
||||
|
||||
if (dataType === "expression") {
|
||||
specField = (
|
||||
<ExpressionProperty
|
||||
errors={this.props.errors}
|
||||
onChange={this.props.onChange.bind(this, this.props.fieldName)}
|
||||
canUndo={this.canUndo}
|
||||
onUndo={this.undoExpression}
|
||||
onDelete={this.deleteExpression}
|
||||
fieldType={this.props.fieldType}
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={this.props.value}
|
||||
onFocus={this.onMarkEditing}
|
||||
onBlur={this.onUnmarkEditing}
|
||||
/>
|
||||
);
|
||||
}
|
||||
else if (dataType === "zoom_function") {
|
||||
specField = (
|
||||
<ZoomProperty
|
||||
errors={this.props.errors}
|
||||
onChange={this.props.onChange.bind(this)}
|
||||
fieldType={this.props.fieldType}
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={this.props.value}
|
||||
onDeleteStop={this.deleteStop}
|
||||
onAddStop={this.addStop}
|
||||
onChangeToDataFunction={this.makeDataFunction}
|
||||
onExpressionClick={this.makeExpression}
|
||||
/>
|
||||
)
|
||||
}
|
||||
else if (dataType === "data_function") {
|
||||
// TODO: Rename to FieldFunction **this file** shouldn't be called that
|
||||
specField = (
|
||||
<DataProperty
|
||||
errors={this.props.errors}
|
||||
onChange={this.props.onChange.bind(this)}
|
||||
fieldType={this.props.fieldType}
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={this.props.value}
|
||||
onDeleteStop={this.deleteStop}
|
||||
onAddStop={this.addStop}
|
||||
onChangeToZoomFunction={this.makeZoomFunction}
|
||||
onExpressionClick={this.makeExpression}
|
||||
/>
|
||||
)
|
||||
}
|
||||
else {
|
||||
specField = (
|
||||
<SpecProperty
|
||||
errors={this.props.errors}
|
||||
onChange={this.props.onChange.bind(this)}
|
||||
fieldType={this.props.fieldType}
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={this.props.value}
|
||||
onZoomClick={this.makeZoomFunction}
|
||||
onDataClick={this.makeDataFunction}
|
||||
onExpressionClick={this.makeExpression}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return <div className={propClass} data-wd-key={"spec-field-container:"+this.props.fieldName}>
|
||||
{specField}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default FieldFunction;
|
||||
|
||||
+14
-15
@@ -1,7 +1,8 @@
|
||||
import React from 'react'
|
||||
|
||||
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
|
||||
import Block from "./Block";
|
||||
import InputString from "./InputString";
|
||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||
import Block from './Block'
|
||||
import InputString from './InputString'
|
||||
|
||||
type FieldIdProps = {
|
||||
value: string
|
||||
@@ -10,19 +11,17 @@ type FieldIdProps = {
|
||||
error?: {message: string}
|
||||
};
|
||||
|
||||
const FieldId: React.FC<FieldIdProps> = (props) => {
|
||||
return (
|
||||
<Block label="ID" fieldSpec={latest.layer.id}
|
||||
data-wd-key={props.wdKey}
|
||||
error={props.error}
|
||||
export default class FieldId extends React.Component<FieldIdProps> {
|
||||
render() {
|
||||
return <Block label={"ID"} fieldSpec={latest.layer.id}
|
||||
data-wd-key={this.props.wdKey}
|
||||
error={this.props.error}
|
||||
>
|
||||
<InputString
|
||||
value={props.value}
|
||||
onInput={props.onChange}
|
||||
data-wd-key={props.wdKey + ".input"}
|
||||
value={this.props.value}
|
||||
onInput={this.props.onChange}
|
||||
data-wd-key={this.props.wdKey + ".input"}
|
||||
/>
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
|
||||
export default FieldId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import InputJson, {type InputJsonProps} from "./InputJson";
|
||||
import React from 'react'
|
||||
import InputJson, {InputJsonProps} from './InputJson'
|
||||
|
||||
|
||||
type FieldJsonProps = InputJsonProps & {};
|
||||
|
||||
|
||||
const FieldJson: React.FC<FieldJsonProps> = (props) => {
|
||||
return <InputJson {...props} />;
|
||||
};
|
||||
export default class FieldJson extends React.Component<FieldJsonProps> {
|
||||
render() {
|
||||
return <InputJson {...this.props} />
|
||||
}
|
||||
}
|
||||
|
||||
export default FieldJson;
|
||||
|
||||
@@ -1,35 +1,30 @@
|
||||
import React from "react";
|
||||
import React from 'react'
|
||||
|
||||
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
|
||||
import Block from "./Block";
|
||||
import InputNumber from "./InputNumber";
|
||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||
import Block from './Block'
|
||||
import InputNumber from './InputNumber'
|
||||
|
||||
type FieldMaxZoomInternalProps = {
|
||||
type FieldMaxZoomProps = {
|
||||
value?: number
|
||||
onChange(value: number | undefined): unknown
|
||||
error?: {message: string}
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
const FieldMaxZoomInternal: React.FC<FieldMaxZoomInternalProps> = (props) => {
|
||||
const t = props.t;
|
||||
return (
|
||||
<Block label={t("Max Zoom")} fieldSpec={latest.layer.maxzoom}
|
||||
error={props.error}
|
||||
export default class FieldMaxZoom extends React.Component<FieldMaxZoomProps> {
|
||||
render() {
|
||||
return <Block label={"Max Zoom"} fieldSpec={latest.layer.maxzoom}
|
||||
error={this.props.error}
|
||||
data-wd-key="max-zoom"
|
||||
>
|
||||
<InputNumber
|
||||
allowRange={true}
|
||||
value={props.value}
|
||||
onChange={props.onChange}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
min={latest.layer.maxzoom.minimum}
|
||||
max={latest.layer.maxzoom.maximum}
|
||||
default={latest.layer.maxzoom.maximum}
|
||||
data-wd-key="max-zoom.input"
|
||||
/>
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
|
||||
const FieldMaxZoom = withTranslation()(FieldMaxZoomInternal);
|
||||
export default FieldMaxZoom;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,30 @@
|
||||
import React from "react";
|
||||
import React from 'react'
|
||||
|
||||
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
|
||||
import Block from "./Block";
|
||||
import InputNumber from "./InputNumber";
|
||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||
import Block from './Block'
|
||||
import InputNumber from './InputNumber'
|
||||
|
||||
type FieldMinZoomInternalProps = {
|
||||
type FieldMinZoomProps = {
|
||||
value?: number
|
||||
onChange(...args: unknown[]): unknown
|
||||
error?: {message: string}
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
const FieldMinZoomInternal: React.FC<FieldMinZoomInternalProps> = (props) => {
|
||||
const t = props.t;
|
||||
return (
|
||||
<Block label={t("Min Zoom")} fieldSpec={latest.layer.minzoom}
|
||||
error={props.error}
|
||||
export default class FieldMinZoom extends React.Component<FieldMinZoomProps> {
|
||||
render() {
|
||||
return <Block label={"Min Zoom"} fieldSpec={latest.layer.minzoom}
|
||||
error={this.props.error}
|
||||
data-wd-key="min-zoom"
|
||||
>
|
||||
<InputNumber
|
||||
allowRange={true}
|
||||
value={props.value}
|
||||
onChange={props.onChange}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
min={latest.layer.minzoom.minimum}
|
||||
max={latest.layer.minzoom.maximum}
|
||||
default={latest.layer.minzoom.minimum}
|
||||
data-wd-key='min-zoom.input'
|
||||
/>
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
|
||||
const FieldMinZoom = withTranslation()(FieldMinZoomInternal);
|
||||
export default FieldMinZoom;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import InputMultiInput, {type InputMultiInputProps} from "./InputMultiInput";
|
||||
import Fieldset from "./Fieldset";
|
||||
import React from 'react'
|
||||
import InputMultiInput, {InputMultiInputProps} from './InputMultiInput'
|
||||
import Fieldset from './Fieldset'
|
||||
|
||||
|
||||
type FieldMultiInputProps = InputMultiInputProps & {
|
||||
@@ -7,12 +8,11 @@ type FieldMultiInputProps = InputMultiInputProps & {
|
||||
};
|
||||
|
||||
|
||||
const FieldMultiInput: React.FC<FieldMultiInputProps> = (props) => {
|
||||
return (
|
||||
<Fieldset label={props.label}>
|
||||
<InputMultiInput {...props} />
|
||||
export default class FieldMultiInput extends React.Component<FieldMultiInputProps> {
|
||||
render() {
|
||||
return <Fieldset label={this.props.label}>
|
||||
<InputMultiInput {...this.props} />
|
||||
</Fieldset>
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default FieldMultiInput;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import InputNumber, {type InputNumberProps} from "./InputNumber";
|
||||
import Block from "./Block";
|
||||
import React from 'react'
|
||||
import InputNumber, {InputNumberProps} from './InputNumber'
|
||||
import Block from './Block'
|
||||
|
||||
|
||||
type FieldNumberProps = InputNumberProps & {
|
||||
@@ -10,12 +11,10 @@ type FieldNumberProps = InputNumberProps & {
|
||||
};
|
||||
|
||||
|
||||
const FieldNumber: React.FC<FieldNumberProps> = (props) => {
|
||||
return (
|
||||
<Block label={props.label} fieldSpec={props.fieldSpec}>
|
||||
<InputNumber {...props} />
|
||||
export default class FieldNumber extends React.Component<FieldNumberProps> {
|
||||
render() {
|
||||
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||
<InputNumber {...this.props} />
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
|
||||
export default FieldNumber;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Block from "./Block";
|
||||
import InputSelect, {type InputSelectProps} from "./InputSelect";
|
||||
import React from 'react'
|
||||
import Block from './Block'
|
||||
import InputSelect, {InputSelectProps} from './InputSelect'
|
||||
|
||||
|
||||
type FieldSelectProps = InputSelectProps & {
|
||||
@@ -10,12 +11,12 @@ type FieldSelectProps = InputSelectProps & {
|
||||
};
|
||||
|
||||
|
||||
const FieldSelect: React.FC<FieldSelectProps> = (props) => {
|
||||
return (
|
||||
<Block label={props.label} fieldSpec={props.fieldSpec}>
|
||||
<InputSelect {...props} />
|
||||
export default class FieldSelect extends React.Component<FieldSelectProps> {
|
||||
render() {
|
||||
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||
<InputSelect {...this.props}/>
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default FieldSelect;
|
||||
|
||||
@@ -1,42 +1,35 @@
|
||||
import React from "react";
|
||||
import React from 'react'
|
||||
|
||||
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
|
||||
import Block from "./Block";
|
||||
import InputAutocomplete from "./InputAutocomplete";
|
||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||
import Block from './Block'
|
||||
import InputAutocomplete from './InputAutocomplete'
|
||||
|
||||
type FieldSourceInternalProps = {
|
||||
type FieldSourceProps = {
|
||||
value?: string
|
||||
wdKey?: string
|
||||
onChange?(value: string| undefined): unknown
|
||||
sourceIds?: unknown[]
|
||||
error?: {message: string}
|
||||
} & WithTranslation;
|
||||
|
||||
const FieldSourceInternal: React.FC<FieldSourceInternalProps> = ({
|
||||
onChange = () => {},
|
||||
sourceIds = [],
|
||||
wdKey,
|
||||
value,
|
||||
error,
|
||||
t
|
||||
}) => {
|
||||
return (
|
||||
<Block
|
||||
label={t("Source")}
|
||||
fieldSpec={latest.layer.source}
|
||||
error={error}
|
||||
data-wd-key={wdKey}
|
||||
>
|
||||
<InputAutocomplete
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={sourceIds?.map((src) => [src, src])}
|
||||
/>
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
|
||||
export default class FieldSource extends React.Component<FieldSourceProps> {
|
||||
static defaultProps = {
|
||||
onChange: () => {},
|
||||
sourceIds: [],
|
||||
}
|
||||
|
||||
const FieldSource = withTranslation()(FieldSourceInternal);
|
||||
export default FieldSource;
|
||||
render() {
|
||||
return <Block
|
||||
label={"Source"}
|
||||
fieldSpec={latest.layer.source}
|
||||
error={this.props.error}
|
||||
data-wd-key={this.props.wdKey}
|
||||
>
|
||||
<InputAutocomplete
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
options={this.props.sourceIds?.map(src => [src, src])}
|
||||
/>
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,37 @@
|
||||
import React from "react";
|
||||
import React from 'react'
|
||||
|
||||
import {latest} from "@maplibre/maplibre-gl-style-spec";
|
||||
import Block from "./Block";
|
||||
import InputAutocomplete from "./InputAutocomplete";
|
||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||
import Block from './Block'
|
||||
import InputAutocomplete from './InputAutocomplete'
|
||||
|
||||
type FieldSourceLayerInternalProps = {
|
||||
type FieldSourceLayerProps = {
|
||||
value?: string
|
||||
onChange?(...args: unknown[]): unknown
|
||||
sourceLayerIds?: unknown[]
|
||||
isFixed?: boolean
|
||||
error?: {message: string}
|
||||
} & WithTranslation;
|
||||
|
||||
const FieldSourceLayerInternal: React.FC<FieldSourceLayerInternalProps> = ({
|
||||
onChange = () => {},
|
||||
sourceLayerIds = [],
|
||||
value,
|
||||
error,
|
||||
t
|
||||
}) => {
|
||||
return (
|
||||
<Block
|
||||
label={t("Source Layer")}
|
||||
fieldSpec={latest.layer["source-layer"]}
|
||||
data-wd-key="layer-source-layer"
|
||||
error={error}
|
||||
>
|
||||
<InputAutocomplete
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={sourceLayerIds?.map((l) => [l, l])}
|
||||
/>
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
|
||||
const FieldSourceLayer = withTranslation()(FieldSourceLayerInternal);
|
||||
export default FieldSourceLayer;
|
||||
export default class FieldSourceLayer extends React.Component<FieldSourceLayerProps> {
|
||||
static defaultProps = {
|
||||
onChange: () => {},
|
||||
sourceLayerIds: [],
|
||||
isFixed: false
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Block
|
||||
label={"Source Layer"}
|
||||
fieldSpec={latest.layer['source-layer']}
|
||||
data-wd-key="layer-source-layer"
|
||||
error={this.props.error}
|
||||
>
|
||||
<InputAutocomplete
|
||||
keepMenuWithinWindowBounds={!!this.props.isFixed}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
options={this.props.sourceLayerIds?.map(l => [l, l])}
|
||||
/>
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
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) {
|
||||
case "color":
|
||||
return Block;
|
||||
case "enum":
|
||||
return (Object.keys(fieldSpec.values!).length <= 3 ? Fieldset : Block);
|
||||
case "boolean":
|
||||
return Block;
|
||||
case "array":
|
||||
return Fieldset;
|
||||
case "resolvedImage":
|
||||
return Block;
|
||||
case "number":
|
||||
return Block;
|
||||
case "string":
|
||||
return Block;
|
||||
case "formatted":
|
||||
return Block;
|
||||
case "padding":
|
||||
return Block;
|
||||
case "numberArray":
|
||||
return Fieldset;
|
||||
case "colorArray":
|
||||
return Fieldset;
|
||||
case "variableAnchorOffsetCollection":
|
||||
return Fieldset;
|
||||
default:
|
||||
console.warn("No such type for: " + fieldSpec.type);
|
||||
return Block;
|
||||
}
|
||||
}
|
||||
|
||||
export type FieldSpecProps = InputSpecProps & BlockProps & FieldsetProps;
|
||||
|
||||
const FieldSpec: React.FC<FieldSpecProps> = (props) => {
|
||||
const TypeBlock = getElementFromType(props.fieldSpec!);
|
||||
|
||||
return (
|
||||
<TypeBlock label={props.label} action={props.action} fieldSpec={props.fieldSpec} error={props.error}>
|
||||
<InputSpec {...props} />
|
||||
</TypeBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default FieldSpec;
|
||||
@@ -1,5 +1,6 @@
|
||||
import Block from "./Block";
|
||||
import InputString, {type InputStringProps} from "./InputString";
|
||||
import React from 'react'
|
||||
import Block from './Block'
|
||||
import InputString, {InputStringProps} from './InputString'
|
||||
|
||||
type FieldStringProps = InputStringProps & {
|
||||
name?: string
|
||||
@@ -9,12 +10,10 @@ type FieldStringProps = InputStringProps & {
|
||||
}
|
||||
};
|
||||
|
||||
const FieldString: React.FC<FieldStringProps> = (props) => {
|
||||
return (
|
||||
<Block label={props.label} fieldSpec={props.fieldSpec}>
|
||||
<InputString {...props} />
|
||||
export default class FieldString extends React.Component<FieldStringProps> {
|
||||
render() {
|
||||
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||
<InputString {...this.props} />
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
|
||||
export default FieldString;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,52 @@
|
||||
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 { type WithTranslation, withTranslation } from "react-i18next";
|
||||
import { startCase } from "lodash";
|
||||
import React from 'react'
|
||||
|
||||
type FieldTypeInternalProps = {
|
||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||
import Block from './Block'
|
||||
import InputSelect from './InputSelect'
|
||||
import InputString from './InputString'
|
||||
|
||||
type FieldTypeProps = {
|
||||
value: string
|
||||
wdKey?: string
|
||||
onChange(value: string): unknown
|
||||
error?: {message: string}
|
||||
disabled?: boolean
|
||||
} & WithTranslation;
|
||||
|
||||
const FieldTypeInternal: React.FC<FieldTypeInternalProps> = ({
|
||||
t,
|
||||
value,
|
||||
wdKey,
|
||||
onChange,
|
||||
error,
|
||||
disabled = false
|
||||
}) => {
|
||||
const layerstypes: [string, string][] = Object.keys(v8.layer.type.values || {}).map(v => [v, startCase(v.replace(/-/g, " "))]);
|
||||
return (
|
||||
<Block label={t("Type")} fieldSpec={v8.layer.type}
|
||||
data-wd-key={wdKey}
|
||||
error={error}
|
||||
>
|
||||
{disabled && (
|
||||
<InputString value={value} disabled={true} />
|
||||
)}
|
||||
{!disabled && (
|
||||
<InputSelect
|
||||
options={layerstypes}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
data-wd-key={wdKey + ".select"}
|
||||
/>
|
||||
)}
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
|
||||
const FieldType = withTranslation()(FieldTypeInternal);
|
||||
export default FieldType;
|
||||
export default class FieldType extends React.Component<FieldTypeProps> {
|
||||
static defaultProps = {
|
||||
disabled: false,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Block label={"Type"} fieldSpec={latest.layer.type}
|
||||
data-wd-key={this.props.wdKey}
|
||||
error={this.props.error}
|
||||
>
|
||||
{this.props.disabled &&
|
||||
<InputString
|
||||
value={this.props.value}
|
||||
disabled={true}
|
||||
/>
|
||||
}
|
||||
{!this.props.disabled &&
|
||||
<InputSelect
|
||||
options={[
|
||||
['background', 'Background'],
|
||||
['fill', 'Fill'],
|
||||
['line', 'Line'],
|
||||
['symbol', 'Symbol'],
|
||||
['raster', 'Raster'],
|
||||
['circle', 'Circle'],
|
||||
['fill-extrusion', 'Fill Extrusion'],
|
||||
['hillshade', 'Hillshade'],
|
||||
['heatmap', 'Heatmap'],
|
||||
]}
|
||||
onChange={this.props.onChange}
|
||||
value={this.props.value}
|
||||
data-wd-key={this.props.wdKey + ".select"}
|
||||
/>
|
||||
}
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
+12
-10
@@ -1,5 +1,6 @@
|
||||
import InputUrl, {type FieldUrlProps as InputUrlProps} from "./InputUrl";
|
||||
import Block from "./Block";
|
||||
import React from 'react'
|
||||
import InputUrl, {FieldUrlProps as InputUrlProps} from './InputUrl'
|
||||
import Block from './Block'
|
||||
|
||||
|
||||
type FieldUrlProps = InputUrlProps & {
|
||||
@@ -10,12 +11,13 @@ type FieldUrlProps = InputUrlProps & {
|
||||
};
|
||||
|
||||
|
||||
const FieldUrl: React.FC<FieldUrlProps> = (props) => {
|
||||
return (
|
||||
<Block label={props.label} fieldSpec={props.fieldSpec}>
|
||||
<InputUrl {...props} />
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
export default class FieldUrl extends React.Component<FieldUrlProps> {
|
||||
render () {
|
||||
return (
|
||||
<Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||
<InputUrl {...this.props} />
|
||||
</Block>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FieldUrl;
|
||||
|
||||
+49
-37
@@ -1,53 +1,65 @@
|
||||
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 React, { ReactElement } from 'react'
|
||||
import FieldDocLabel from './FieldDocLabel'
|
||||
import Doc from './Doc'
|
||||
import generateUniqueId from '../libs/document-uid';
|
||||
|
||||
export type FieldsetProps = PropsWithChildren & {
|
||||
type FieldsetProps = {
|
||||
label?: string,
|
||||
fieldSpec?: { doc?: string },
|
||||
action?: ReactElement,
|
||||
error?: {message: string}
|
||||
};
|
||||
|
||||
type FieldsetState = {
|
||||
showDoc: boolean
|
||||
};
|
||||
|
||||
const Fieldset: React.FC<FieldsetProps> = (props) => {
|
||||
const [showDoc, setShowDoc] = React.useState(false);
|
||||
const labelId = React.useRef(generateUniqueId("fieldset_label_"));
|
||||
export default class Fieldset extends React.Component<FieldsetProps, FieldsetState> {
|
||||
_labelId: string;
|
||||
|
||||
const onToggleDoc = (val: boolean) => {
|
||||
setShowDoc(val);
|
||||
};
|
||||
constructor (props: FieldsetProps) {
|
||||
super(props);
|
||||
this._labelId = generateUniqueId(`fieldset_label_`);
|
||||
this.state = {
|
||||
showDoc: false,
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="maputnik-input-block" role="group" aria-labelledby={labelId.current}>
|
||||
{props.fieldSpec && (
|
||||
onToggleDoc = (val: boolean) => {
|
||||
this.setState({
|
||||
showDoc: val
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
return <div className="maputnik-input-block" role="group" aria-labelledby={this._labelId}>
|
||||
{this.props.fieldSpec &&
|
||||
<div className="maputnik-input-block-label">
|
||||
<FieldDocLabel
|
||||
label={props.label}
|
||||
onToggleDoc={onToggleDoc}
|
||||
fieldSpec={props.fieldSpec}
|
||||
label={this.props.label}
|
||||
onToggleDoc={this.onToggleDoc}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!props.fieldSpec && (
|
||||
<div className={classnames({
|
||||
"maputnik-input-block-label": true,
|
||||
"maputnik-input-block--error": props.error
|
||||
})}>
|
||||
{props.label}
|
||||
}
|
||||
{!this.props.fieldSpec &&
|
||||
<div className="maputnik-input-block-label">
|
||||
{this.props.label}
|
||||
</div>
|
||||
)}
|
||||
<div className="maputnik-input-block-action">{props.action}</div>
|
||||
<div className="maputnik-input-block-content">{props.children}</div>
|
||||
{props.fieldSpec && (
|
||||
<div className="maputnik-doc-inline" style={{ display: showDoc ? "" : "none" }}>
|
||||
<Doc fieldSpec={props.fieldSpec} />
|
||||
}
|
||||
<div className="maputnik-input-block-action">
|
||||
{this.props.action}
|
||||
</div>
|
||||
<div className="maputnik-input-block-content">
|
||||
{this.props.children}
|
||||
</div>
|
||||
{this.props.fieldSpec &&
|
||||
<div
|
||||
className="maputnik-doc-inline"
|
||||
style={{display: this.state.showDoc ? '' : 'none'}}
|
||||
>
|
||||
<Doc fieldSpec={this.props.fieldSpec} />
|
||||
</div>
|
||||
)}
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Fieldset;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
import React from "react";
|
||||
import { TbMathFunction } from "react-icons/tb";
|
||||
import { PiListPlusBold } from "react-icons/pi";
|
||||
import {isEqual} from "lodash";
|
||||
import {type ExpressionSpecification, type LegacyFilterSpecification} from "maplibre-gl";
|
||||
import {migrate, convertFilter} from "@maplibre/maplibre-gl-style-spec";
|
||||
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
|
||||
import React from 'react'
|
||||
import {mdiTableRowPlusAfter} from '@mdi/js';
|
||||
import {isEqual} from 'lodash';
|
||||
import {ExpressionSpecification, LegacyFilterSpecification, StyleSpecification} from 'maplibre-gl'
|
||||
import {latest, migrate, convertFilter} from '@maplibre/maplibre-gl-style-spec'
|
||||
import {mdiFunctionVariant} from '@mdi/js';
|
||||
|
||||
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 { type WithTranslation, withTranslation } from "react-i18next";
|
||||
import type { MappedLayerErrors, StyleSpecificationWithId } from "../libs/definitions";
|
||||
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';
|
||||
|
||||
|
||||
function combiningFilter(props: FilterEditorInternalProps): LegacyFilterSpecification | ExpressionSpecification {
|
||||
const filter = props.filter || ["all"];
|
||||
function combiningFilter(props: FilterEditorProps): LegacyFilterSpecification | ExpressionSpecification {
|
||||
const filter = props.filter || ['all'];
|
||||
|
||||
if (!Array.isArray(filter)) {
|
||||
return filter;
|
||||
@@ -29,7 +26,7 @@ function combiningFilter(props: FilterEditorInternalProps): LegacyFilterSpecific
|
||||
let filters = filter.slice(1);
|
||||
|
||||
if(combiningFilterOps.indexOf(combiningOp) < 0) {
|
||||
combiningOp = "all";
|
||||
combiningOp = 'all';
|
||||
filters = [filter.slice(0)];
|
||||
}
|
||||
|
||||
@@ -41,7 +38,7 @@ function migrateFilter(filter: LegacyFilterSpecification | ExpressionSpecificati
|
||||
return (migrate(createStyleFromFilter(filter) as any).layers[0] as any).filter;
|
||||
}
|
||||
|
||||
function createStyleFromFilter(filter: LegacyFilterSpecification | ExpressionSpecification): StyleSpecificationWithId {
|
||||
function createStyleFromFilter(filter: LegacyFilterSpecification | ExpressionSpecification): StyleSpecification & {id: string} {
|
||||
return {
|
||||
"id": "tmp",
|
||||
"version": 8,
|
||||
@@ -50,7 +47,7 @@ function createStyleFromFilter(filter: LegacyFilterSpecification | ExpressionSpe
|
||||
"sources": {
|
||||
"tmp": {
|
||||
"type": "geojson",
|
||||
"data": ""
|
||||
"data": ''
|
||||
}
|
||||
},
|
||||
"sprite": "",
|
||||
@@ -82,23 +79,23 @@ function checkIfSimpleFilter (filter: LegacyFilterSpecification | ExpressionSpec
|
||||
}
|
||||
|
||||
function hasCombiningFilter(filter: LegacyFilterSpecification | ExpressionSpecification) {
|
||||
return combiningFilterOps.indexOf(filter[0]) >= 0;
|
||||
return combiningFilterOps.indexOf(filter[0]) >= 0
|
||||
}
|
||||
|
||||
function hasNestedCombiningFilter(filter: LegacyFilterSpecification | ExpressionSpecification) {
|
||||
if(hasCombiningFilter(filter)) {
|
||||
return filter.slice(1).map(f => hasCombiningFilter(f as any)).filter(f => f == true).length > 0;
|
||||
return filter.slice(1).map(f => hasCombiningFilter(f as any)).filter(f => f == true).length > 0
|
||||
}
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
type FilterEditorInternalProps = {
|
||||
type FilterEditorProps = {
|
||||
/** Properties of the vector layer and the available fields */
|
||||
properties?: {[key:string]: any}
|
||||
filter?: any[]
|
||||
errors?: MappedLayerErrors
|
||||
onChange(value: LegacyFilterSpecification | ExpressionSpecification): void
|
||||
} & WithTranslation;
|
||||
errors?: {[key:string]: any}
|
||||
onChange(value: LegacyFilterSpecification | ExpressionSpecification): unknown
|
||||
};
|
||||
|
||||
type FilterEditorState = {
|
||||
showDoc: boolean
|
||||
@@ -106,12 +103,12 @@ type FilterEditorState = {
|
||||
valueIsSimpleFilter?: boolean
|
||||
};
|
||||
|
||||
class FilterEditorInternal extends React.Component<FilterEditorInternalProps, FilterEditorState> {
|
||||
export default class FilterEditor extends React.Component<FilterEditorProps, FilterEditorState> {
|
||||
static defaultProps = {
|
||||
filter: ["all"],
|
||||
};
|
||||
}
|
||||
|
||||
constructor (props: FilterEditorInternalProps) {
|
||||
constructor (props: FilterEditorProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showDoc: false,
|
||||
@@ -121,57 +118,57 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
||||
|
||||
// Convert filter to combining filter
|
||||
onFilterPartChanged(filterIdx: number, newPart: any[]) {
|
||||
const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification;
|
||||
newFilter[filterIdx] = newPart;
|
||||
this.props.onChange(newFilter);
|
||||
const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification
|
||||
newFilter[filterIdx] = newPart
|
||||
this.props.onChange(newFilter)
|
||||
}
|
||||
|
||||
deleteFilterItem(filterIdx: number) {
|
||||
const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification;
|
||||
newFilter.splice(filterIdx + 1, 1);
|
||||
this.props.onChange(newFilter);
|
||||
const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification
|
||||
newFilter.splice(filterIdx + 1, 1)
|
||||
this.props.onChange(newFilter)
|
||||
}
|
||||
|
||||
addFilterItem = () => {
|
||||
const newFilterItem = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification;
|
||||
(newFilterItem as any[]).push(["==", "name", ""]);
|
||||
this.props.onChange(newFilterItem);
|
||||
};
|
||||
const newFilterItem = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification
|
||||
(newFilterItem as any[]).push(['==', 'name', ''])
|
||||
this.props.onChange(newFilterItem)
|
||||
}
|
||||
|
||||
onToggleDoc = (val: boolean) => {
|
||||
this.setState({
|
||||
showDoc: val
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
makeFilter = () => {
|
||||
this.setState({
|
||||
displaySimpleFilter: true,
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
makeExpression = () => {
|
||||
const filter = combiningFilter(this.props);
|
||||
this.props.onChange(migrateFilter(filter));
|
||||
this.setState({
|
||||
displaySimpleFilter: false,
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: Readonly<FilterEditorInternalProps>, state: FilterEditorState) {
|
||||
static getDerivedStateFromProps(props: FilterEditorProps, currentState: FilterEditorState) {
|
||||
const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props));
|
||||
|
||||
// Upgrade but never downgrade
|
||||
if (!displaySimpleFilter && state.displaySimpleFilter === true) {
|
||||
if (!displaySimpleFilter && currentState.displaySimpleFilter === true) {
|
||||
return {
|
||||
displaySimpleFilter: false,
|
||||
valueIsSimpleFilter: false,
|
||||
};
|
||||
}
|
||||
else if (displaySimpleFilter && state.displaySimpleFilter === false) {
|
||||
else if (displaySimpleFilter && currentState.displaySimpleFilter === false) {
|
||||
return {
|
||||
valueIsSimpleFilter: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
else {
|
||||
return {
|
||||
@@ -181,7 +178,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
||||
}
|
||||
|
||||
render() {
|
||||
const {errors, t} = this.props;
|
||||
const {errors} = this.props;
|
||||
const {displaySimpleFilter} = this.state;
|
||||
const fieldSpec={
|
||||
doc: latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."
|
||||
@@ -193,16 +190,18 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
||||
if (isNestedCombiningFilter) {
|
||||
return <div className="maputnik-filter-editor-unsupported">
|
||||
<p>
|
||||
{t("Nested filters are not supported.")}
|
||||
Nested filters are not supported.
|
||||
</p>
|
||||
<InputButton
|
||||
onClick={this.makeExpression}
|
||||
title={t("Convert to expression")}
|
||||
title="Convert to expression"
|
||||
>
|
||||
<TbMathFunction />
|
||||
{t("Upgrade to expression")}
|
||||
<svg style={{marginRight: "0.2em", width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiFunctionVariant} />
|
||||
</svg>
|
||||
Upgrade to expression
|
||||
</InputButton>
|
||||
</div>;
|
||||
</div>
|
||||
}
|
||||
else if (displaySimpleFilter) {
|
||||
const filter = combiningFilter(this.props);
|
||||
@@ -213,10 +212,12 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
||||
<div>
|
||||
<InputButton
|
||||
onClick={this.makeExpression}
|
||||
title={t("Convert to expression")}
|
||||
title="Convert to expression"
|
||||
className="maputnik-make-zoom-function"
|
||||
>
|
||||
<TbMathFunction />
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiFunctionVariant} />
|
||||
</svg>
|
||||
</InputButton>
|
||||
</div>
|
||||
);
|
||||
@@ -238,7 +239,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
||||
}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
return (
|
||||
@@ -246,17 +247,13 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
||||
<Block
|
||||
key="top"
|
||||
fieldSpec={fieldSpec}
|
||||
label={t("Filter")}
|
||||
label={"Filter"}
|
||||
action={actions}
|
||||
>
|
||||
<InputSelect
|
||||
value={combiningOp}
|
||||
onChange={(v: [string, any]) => this.onFilterPartChanged(0, v)}
|
||||
options={[
|
||||
["all", t("every filter matches")],
|
||||
["none", t("no filter matches")],
|
||||
["any", t("any filter matches")]
|
||||
]}
|
||||
options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
|
||||
/>
|
||||
</Block>
|
||||
{editorBlocks}
|
||||
@@ -269,14 +266,15 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
||||
className="maputnik-add-filter"
|
||||
onClick={this.addFilterItem}
|
||||
>
|
||||
<PiListPlusBold style={{ verticalAlign: "text-bottom" }} />
|
||||
{t("Add filter")}
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiTableRowPlusAfter} />
|
||||
</svg> Add filter
|
||||
</InputButton>
|
||||
</div>
|
||||
<div
|
||||
key="doc"
|
||||
className="maputnik-doc-inline"
|
||||
style={{display: this.state.showDoc ? "" : "none"}}
|
||||
style={{display: this.state.showDoc ? '' : 'none'}}
|
||||
>
|
||||
<Doc fieldSpec={fieldSpec} />
|
||||
</div>
|
||||
@@ -294,19 +292,19 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
||||
this.props.onChange(defaultFilter);
|
||||
}}
|
||||
fieldName="filter"
|
||||
fieldSpec={fieldSpec}
|
||||
value={filter}
|
||||
errors={errors}
|
||||
onChange={this.props.onChange}
|
||||
/>
|
||||
{this.state.valueIsSimpleFilter &&
|
||||
<div className="maputnik-expr-infobox">
|
||||
{t("You've entered an old style filter.")}
|
||||
{" "}
|
||||
You've entered a old style filter,{' '}
|
||||
<button
|
||||
onClick={this.makeFilter}
|
||||
className="maputnik-expr-infobox__button"
|
||||
>
|
||||
{t("Switch to filter editor.")}
|
||||
switch to filter editor
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@@ -315,6 +313,3 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const FilterEditor = withTranslation()(FilterEditorInternal);
|
||||
export default FilterEditor;
|
||||
|
||||
@@ -1,31 +1,27 @@
|
||||
import React, { type PropsWithChildren } from "react";
|
||||
import InputButton from "./InputButton";
|
||||
import {MdDelete} from "react-icons/md";
|
||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||
import React from 'react'
|
||||
import InputButton from './InputButton'
|
||||
import {MdDelete} from 'react-icons/md'
|
||||
|
||||
type FilterEditorBlockInternalProps = PropsWithChildren & {
|
||||
type FilterEditorBlockProps = {
|
||||
onDelete(...args: unknown[]): unknown
|
||||
} & WithTranslation;
|
||||
};
|
||||
|
||||
class FilterEditorBlockInternal extends React.Component<FilterEditorBlockInternalProps> {
|
||||
export default class FilterEditorBlock extends React.Component<FilterEditorBlockProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <div className="maputnik-filter-editor-block">
|
||||
<div className="maputnik-filter-editor-block-content">
|
||||
{this.props.children}
|
||||
</div>
|
||||
<div className="maputnik-filter-editor-block-action">
|
||||
<InputButton
|
||||
className="maputnik-icon-button"
|
||||
className="maputnik-delete-filter"
|
||||
onClick={this.props.onDelete}
|
||||
title={t("Delete filter block")}
|
||||
title="Delete filter block"
|
||||
>
|
||||
<MdDelete />
|
||||
</InputButton>
|
||||
</div>
|
||||
</div>;
|
||||
<div className="maputnik-filter-editor-block-content">
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
const FilterEditorBlock = withTranslation()(FilterEditorBlockInternal);
|
||||
export default FilterEditorBlock;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react'
|
||||
import IconBase from 'react-icon-base'
|
||||
|
||||
|
||||
export default class IconBackground extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
||||
<path d="m 1.821019,10.255581 7.414535,5.020197 c 0.372277,0.25206 0.958697,0.239771 1.30985,-0.02745 L 17.539255,9.926162 C 17.89041,9.658941 17.873288,9.238006 17.501015,8.985946 L 10.08648,3.9657402 C 9.714204,3.7136802 9.127782,3.7259703 8.776627,3.9931918 L 1.782775,9.315365 c -0.3511551,0.267221 -0.3340331,0.688156 0.03824,0.940216 z" />
|
||||
</IconBase>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react'
|
||||
import IconBase from 'react-icon-base'
|
||||
|
||||
|
||||
export default class IconCircle extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
||||
<path transform="translate(2 2)" d="M7.5,0C11.6422,0,15,3.3578,15,7.5S11.6422,15,7.5,15 S0,11.6422,0,7.5S3.3578,0,7.5,0z M7.5,1.6666c-3.2217,0-5.8333,2.6117-5.8333,5.8334S4.2783,13.3334,7.5,13.3334 s5.8333-2.6117,5.8333-5.8334S10.7217,1.6666,7.5,1.6666z"></path>
|
||||
</IconBase>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react'
|
||||
import IconBase from 'react-icon-base'
|
||||
|
||||
|
||||
export default class IconFill extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
||||
<path d="M 2.84978,9.763512 9.462149,4.7316391 16.47225,9.478015 9.859886,14.509879 2.84978,9.763512 m -1.028761,0.492069 7.414535,5.020197 c 0.372277,0.25206 0.958697,0.239771 1.30985,-0.02745 L 17.539255,9.926162 C 17.89041,9.658941 17.873288,9.238006 17.501015,8.985946 L 10.08648,3.9657402 C 9.714204,3.7136802 9.127782,3.7259703 8.776627,3.9931918 L 1.782775,9.315365 c -0.3511551,0.267221 -0.3340331,0.688156 0.03824,0.940216 l 0,0 z" />
|
||||
</IconBase>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import React from "react";
|
||||
import React from 'react'
|
||||
|
||||
import type {CSSProperties} from "react";
|
||||
import { BsBack, BsDiamondFill, BsSunFill } from "react-icons/bs";
|
||||
import { MdBubbleChart, MdCircle, MdLocationPin, MdPhoto, MdPriorityHigh } from "react-icons/md";
|
||||
import { IoAnalyticsOutline } from "react-icons/io5";
|
||||
import { IoMdCube } from "react-icons/io";
|
||||
import { FaMountain } from "react-icons/fa";
|
||||
import IconLine from './IconLine'
|
||||
import IconFill from './IconFill'
|
||||
import IconSymbol from './IconSymbol'
|
||||
import IconBackground from './IconBackground'
|
||||
import IconCircle from './IconCircle'
|
||||
import IconMissing from './IconMissing'
|
||||
|
||||
type IconLayerProps = {
|
||||
type: string
|
||||
style?: CSSProperties
|
||||
style?: object
|
||||
className?: string
|
||||
};
|
||||
|
||||
const IconLayer: React.FC<IconLayerProps> = (props) => {
|
||||
const iconProps = { style: props.style };
|
||||
switch(props.type) {
|
||||
case "fill-extrusion": return <IoMdCube {...iconProps} />;
|
||||
case "raster": return <MdPhoto {...iconProps} />;
|
||||
case "hillshade": return <BsSunFill {...iconProps} />;
|
||||
case "color-relief": return <FaMountain {...iconProps} />;
|
||||
case "heatmap": return <MdBubbleChart {...iconProps} />;
|
||||
case "fill": return <BsDiamondFill {...iconProps} />;
|
||||
case "background": return <BsBack {...iconProps} />;
|
||||
case "line": return <IoAnalyticsOutline {...iconProps} />;
|
||||
case "symbol": return <MdLocationPin {...iconProps} />;
|
||||
case "circle": return <MdCircle {...iconProps} />;
|
||||
default: return <MdPriorityHigh {...iconProps} />;
|
||||
export default class IconLayer extends React.Component<IconLayerProps> {
|
||||
render() {
|
||||
const iconProps = { style: this.props.style }
|
||||
switch(this.props.type) {
|
||||
case 'fill-extrusion': return <IconBackground {...iconProps} />
|
||||
case 'raster': return <IconFill {...iconProps} />
|
||||
case 'hillshade': return <IconFill {...iconProps} />
|
||||
case 'heatmap': return <IconFill {...iconProps} />
|
||||
case 'fill': return <IconFill {...iconProps} />
|
||||
case 'background': return <IconBackground {...iconProps} />
|
||||
case 'line': return <IconLine {...iconProps} />
|
||||
case 'symbol': return <IconSymbol {...iconProps} />
|
||||
case 'circle': return <IconCircle {...iconProps} />
|
||||
default: return <IconMissing {...iconProps} />
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default IconLayer;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user