mirror of
https://github.com/maputnik/editor.git
synced 2026-09-01 18:07:37 +00:00
Compare commits
38 Commits
v3.0.0
..
13336bf2af
| Author | SHA1 | Date | |
|---|---|---|---|
| 13336bf2af | |||
| e74382d260 | |||
| fa2dfa7480 | |||
| 2213e74e2a | |||
| a94ee550f8 | |||
| 0ed6bb518f | |||
| c5a7005a3d | |||
| 055d35f4b8 | |||
| d4d37e5930 | |||
| b20c54d417 | |||
| f7f6df70b2 | |||
| 20c121d17a | |||
| 27190f683b | |||
| b4e31cc031 | |||
| 26bf99d91a | |||
| f5ab97d3a7 | |||
| 6aa48d66a2 | |||
| ff547fc3f7 | |||
| 138f685293 | |||
| aa8cb8f3d3 | |||
| c694237703 | |||
| ba201935a8 | |||
| 79b36efbdb | |||
| fb41a59bd8 | |||
| 7538e02e47 | |||
| d031fc67f5 | |||
| 7de266ca43 | |||
| ecfdd5f1ba | |||
| 0bb0ae87af | |||
| 7c8b25e82c | |||
| 666bc648d8 | |||
| f7a945abce | |||
| 085eda76a1 | |||
| 0dc3f4bf5d | |||
| efc507aecd | |||
| ef902910c8 | |||
| 6ed08c7416 | |||
| 9014e12ca6 |
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"packages": [],
|
||||||
|
"sandboxes": ["/"]
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-2
@@ -1,2 +1 @@
|
|||||||
github: [maplibre]
|
custom: "https://maputnik.github.io/donate"
|
||||||
open_collective: maplibre
|
|
||||||
|
|||||||
@@ -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) -->
|
<!-- 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.
|
|
||||||
@@ -1,17 +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
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: "npm" # See documentation for possible values
|
- package-ecosystem: "npm"
|
||||||
directory: "/" # Location of package manifests
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "daily"
|
||||||
open-pull-requests-limit: 20
|
open-pull-requests-limit: 2
|
||||||
versioning-strategy: increase
|
versioning-strategy: increase
|
||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "daily"
|
||||||
|
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.actor == 'dependabot[bot]' }}
|
|
||||||
steps:
|
|
||||||
- name: Dependabot metadata
|
|
||||||
id: metadata
|
|
||||||
uses: dependabot/fetch-metadata@v2.4.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}}
|
|
||||||
+75
-69
@@ -7,7 +7,17 @@ on:
|
|||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
|
|
||||||
jobs:
|
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 docker.pkg.github.com/maputnik/editor/editor:main .
|
||||||
|
|
||||||
|
# build the editor
|
||||||
build-node:
|
build-node:
|
||||||
name: "build on ${{ matrix.os }}"
|
name: "build on ${{ matrix.os }}"
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
@@ -17,17 +27,17 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v5
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npm run lint
|
- run: npm run lint
|
||||||
- run: npm run lint-css
|
- run: npm run lint-css
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -37,83 +47,79 @@ jobs:
|
|||||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v5
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- name: artifacts/maputnik
|
- run: npm run build-storybook
|
||||||
uses: actions/upload-artifact@v4
|
- name: artifacts/editor
|
||||||
with:
|
uses: actions/upload-artifact@v1
|
||||||
name: maputnik
|
with:
|
||||||
path: dist
|
name: editor
|
||||||
|
path: dist
|
||||||
|
- name: artifacts/storybook
|
||||||
|
uses: actions/upload-artifact@v1
|
||||||
|
with:
|
||||||
|
name: storybook
|
||||||
|
path: build/storybook
|
||||||
|
|
||||||
# Build and upload desktop CLI artifacts
|
# Build and upload desktop CLI artifacts
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: ^1.23.x
|
go-version: ^1.19.x
|
||||||
cache-dependency-path: desktop/go.sum
|
id: go
|
||||||
id: go
|
|
||||||
|
|
||||||
- name: Build desktop artifacts
|
- name: Check out code into the Go module directory
|
||||||
run: npm run build-desktop
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: maputnik/desktop
|
||||||
|
ref: master
|
||||||
|
path: ./src/github.com/maputnik/desktop/
|
||||||
|
|
||||||
- name: Artifacts/linux
|
- name: Make
|
||||||
uses: actions/upload-artifact@v4
|
run: cd src/github.com/maputnik/desktop/ && make
|
||||||
with:
|
|
||||||
name: maputnik-linux
|
|
||||||
path: ./desktop/bin/linux/
|
|
||||||
|
|
||||||
- name: Artifacts/darwin
|
- name: Artifacts/linux
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v1
|
||||||
with:
|
with:
|
||||||
name: maputnik-darwin
|
name: maputnik-linux
|
||||||
path: ./desktop/bin/darwin/
|
path: ./src/github.com/maputnik/desktop/bin/linux/
|
||||||
|
|
||||||
- name: Artifacts/windows
|
- name: Artifacts/darwin
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v1
|
||||||
with:
|
with:
|
||||||
name: maputnik-windows
|
name: maputnik-darwin
|
||||||
path: ./desktop/bin/windows/
|
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:
|
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
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- name: Cypress run
|
- name: Cypress run
|
||||||
uses: cypress-io/github-action@v6
|
uses: cypress-io/github-action@v6
|
||||||
with:
|
with:
|
||||||
build: npm run build
|
build: npm run build
|
||||||
start: npm run start
|
start: npm run start
|
||||||
browser: chrome
|
browser: ${{ matrix.browser }}
|
||||||
- name: Upload coverage reports to Codecov
|
- name: Upload coverage reports to Codecov
|
||||||
uses: codecov/codecov-action@v5
|
uses: codecov/codecov-action@v3
|
||||||
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@v5
|
|
||||||
- run: npm ci
|
|
||||||
- name: Cypress run
|
|
||||||
uses: cypress-io/github-action@v6
|
|
||||||
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@v5
|
|
||||||
with:
|
with:
|
||||||
files: ${{ github.workspace }}/.nyc_output/out.json
|
files: ${{ github.workspace }}/.nyc_output/out.json
|
||||||
verbose: true
|
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@v5
|
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@v3
|
|
||||||
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@v3
|
|
||||||
|
|
||||||
# ℹ️ 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@v3
|
|
||||||
@@ -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@v5
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
ref: main
|
|
||||||
|
|
||||||
- name: Use Node.js from nvmrc
|
|
||||||
uses: actions/setup-node@v5
|
|
||||||
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@v7
|
|
||||||
with:
|
|
||||||
commit-message: Bump version to ${{ inputs.version }}
|
|
||||||
branch: bump-version-to-${{ inputs.version }}
|
|
||||||
title: Bump version to ${{ inputs.version }}
|
|
||||||
@@ -3,49 +3,24 @@ name: deploy
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy-pages:
|
|
||||||
name: deploy/pages
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: ${{ github.event_name == 'push' }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
|
|
||||||
- name: Use Node.js from nvmrc
|
|
||||||
uses: actions/setup-node@v5
|
|
||||||
with:
|
|
||||||
node-version-file: '.nvmrc'
|
|
||||||
|
|
||||||
- name: Install
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
- name: Upload to GitHub Pages
|
|
||||||
uses: peaceiris/actions-gh-pages@v4
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
publish_dir: dist
|
|
||||||
|
|
||||||
# publish docker to GitHub registry
|
# publish docker to GitHub registry
|
||||||
deploy-docker:
|
deploy-docker:
|
||||||
name: deploy/docker
|
name: deploy/docker
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
if: ${{ github.event_name == 'push' }}
|
if: ${{ github.event_name == 'push' }}
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Login to GitHub Container Registry
|
- uses: actions/checkout@v4
|
||||||
uses: docker/login-action@v3
|
- run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u orangemug --password-stdin
|
||||||
with:
|
- run: docker build -t docker.pkg.github.com/maputnik/editor/editor:main .
|
||||||
registry: ghcr.io
|
- run: docker push docker.pkg.github.com/maputnik/editor/editor:main
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
- run: docker build -t ghcr.io/maplibre/maputnik:main .
|
|
||||||
- run: docker push ghcr.io/maplibre/maputnik:main
|
|
||||||
|
|||||||
@@ -1,101 +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
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
ref: main
|
|
||||||
|
|
||||||
- name: Use Node.js from nvmrc
|
|
||||||
uses: actions/setup-node@v5
|
|
||||||
with:
|
|
||||||
node-version-file: ".nvmrc"
|
|
||||||
|
|
||||||
- name: Check if version has been updated
|
|
||||||
id: check
|
|
||||||
uses: EndBug/version-check@v2
|
|
||||||
|
|
||||||
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@v5
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
ref: main
|
|
||||||
|
|
||||||
- name: Use Node.js from nvmrc
|
|
||||||
uses: actions/setup-node@v5
|
|
||||||
with:
|
|
||||||
node-version-file: ".nvmrc"
|
|
||||||
registry-url: "https://registry.npmjs.org"
|
|
||||||
|
|
||||||
- name: Set up Go for desktop build
|
|
||||||
uses: actions/setup-go@v6
|
|
||||||
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@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@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@v1
|
|
||||||
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
|
public
|
||||||
/errorShots
|
/errorShots
|
||||||
/old
|
/old
|
||||||
|
/build
|
||||||
/cypress/screenshots
|
/cypress/screenshots
|
||||||
/dist/
|
/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
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
const config = {
|
||||||
|
stories: ['../stories/**/*.stories.jsx'],
|
||||||
|
addons: ['@storybook/addon-actions', '@storybook/addon-links', '@storybook/addon-a11y/register', '@storybook/addon-storysource'],
|
||||||
|
framework: {
|
||||||
|
name: '@storybook/react-vite',
|
||||||
|
options: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { addons } from '@storybook/addons';
|
||||||
|
import { themes } from '@storybook/theming';
|
||||||
|
import theme from './maputnik.theme';
|
||||||
|
|
||||||
|
addons.setConfig({
|
||||||
|
theme: theme,
|
||||||
|
});
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { create } from '@storybook/theming/create';
|
||||||
|
|
||||||
|
export default create({
|
||||||
|
base: 'light',
|
||||||
|
|
||||||
|
brandTitle: 'Maputnik',
|
||||||
|
brandUrl: 'https://github.com/maputnik/editor',
|
||||||
|
});
|
||||||
@@ -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,54 +0,0 @@
|
|||||||
## main
|
|
||||||
|
|
||||||
### ✨ Features and improvements
|
|
||||||
- _...Add new stuff here..._
|
|
||||||
|
|
||||||
### 🐞 Bug fixes
|
|
||||||
|
|
||||||
- _...Add new stuff here..._
|
|
||||||
|
|
||||||
## 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
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
# Contributor Covenant
|
|
||||||
[](https://github.com/maplibre/maplibre/blob/main/CODE_OF_CONDUCT.md)
|
|
||||||
+10
-8
@@ -1,14 +1,16 @@
|
|||||||
FROM golang:1.23-alpine AS builder
|
FROM node:18 as builder
|
||||||
WORKDIR /maputnik
|
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
|
# Build maputnik
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm ci
|
RUN npm run build
|
||||||
RUN CGO_ENABLED=1 GOOS=linux npm run build-linux
|
|
||||||
|
|
||||||
FROM alpine:latest
|
#---------------------------------------------------------------------------
|
||||||
WORKDIR /app
|
# Create a clean nginx-alpine slim image with just the build results
|
||||||
COPY --from=builder /maputnik/desktop/bin/linux ./
|
FROM nginx:alpine-slim
|
||||||
ENTRYPOINT ["/app/maputnik"]
|
|
||||||
|
COPY --from=builder /maputnik/dist /usr/share/nginx/html/
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2015 Lukas Martinelli
|
Copyright (c) 2015 Lukas Martinelli
|
||||||
Copyright (c) 2024 MapLibre contributors
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
@@ -20,3 +19,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,
|
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
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<img width="200" alt="Maputnik logo" src="https://cdn.jsdelivr.net/gh/maputnik/design/logos/logo-color.png" />
|
<img width="200" alt="Maputnik logo" src="https://cdn.jsdelivr.net/gh/maputnik/design/logos/logo-color.png" />
|
||||||
|
|
||||||
# Maputnik
|
# Maputnik
|
||||||
[][github-action-ci]
|
[][github-action-ci]
|
||||||
[][license]
|
[][license]
|
||||||
|
|
||||||
[github-action-ci]: https://github.com/maplibre/maputnik/actions?query=workflow%3Aci
|
[github-action-ci]: https://github.com/maputnik/editor/actions?query=workflow%3Aci
|
||||||
[license]: https://tldrlegal.com/license/mit-license
|
[license]: https://tldrlegal.com/license/mit-license
|
||||||
|
|
||||||
A free and open visual editor for the [MapLibre GL styles](https://maplibre.org/maplibre-style-spec/)
|
A free and open visual editor for the [MapLibre GL styles](https://maplibre.org/maplibre-style-spec/)
|
||||||
@@ -13,40 +13,33 @@ targeted at developers and map designers.
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
- :link: Design your maps online at **<https://www.maplibre.org/maputnik/>** (all in local storage)
|
- :link: Design your maps online at **<https://maputnik.github.io/editor/>** (all in local storage)
|
||||||
- :link: Use the [Maputnik CLI](https://github.com/maplibre/maputnik/wiki/Maputnik-CLI) for local style development
|
- :link: Use the [Maputnik CLI](https://github.com/maputnik/editor/wiki/Maputnik-CLI) for local style development
|
||||||
- In a Docker, run this command and browse to http://localhost:8888, Ctrl+C to stop the server.
|
- In a Docker, run this command and browse to http://localhost:8888, Ctrl+C to stop the server.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -it --rm -p 8888:8000 ghcr.io/maplibre/maputnik:main
|
docker run -it --rm -p 8888:8888 maputnik/editor
|
||||||
```
|
```
|
||||||
|
|
||||||
To see the CLI options (for example file watching or style serving) run:
|
## Donations
|
||||||
```bash
|
Mapbox has built one of the best and most amazing OSS ecosystems. A key component to ensure its longevity and independence is an OSS map designer.
|
||||||
docker run -it --rm -p 8888:8000 ghcr.io/maplibre/maputnik:main --help
|
If you or your organisation has seen value from Maputnik, please consider donating at <https://maputnik.github.io/donate>
|
||||||
```
|
|
||||||
You might need to mount a volume (`-v`) to be able to use these options.
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
The documentation can be found in the [Wiki](https://github.com/maplibre/maputnik/wiki). You are welcome to collaborate!
|
The documentation can be found in the [Wiki](https://github.com/maputnik/editor/wiki). You are welcome to collaborate!
|
||||||
|
|
||||||
- :link: **Study the [Maputnik Wiki](https://github.com/maplibre/maputnik/wiki)**
|
- :link: **Study the [Maputnik Wiki](https://github.com/maputnik/editor/wiki)**
|
||||||
- :video_camera: Design a map from Scratch https://youtu.be/XoDh0gEnBQo
|
- :video_camera: Design a map from Scratch https://youtu.be/XoDh0gEnBQo
|
||||||
|
|
||||||
[](https://youtu.be/XoDh0gEnBQo)
|
[](https://youtu.be/XoDh0gEnBQo)
|
||||||
|
|
||||||
## Develop
|
## Develop
|
||||||
|
|
||||||
Maputnik is written in typescript and is using [React](https://github.com/facebook/react) and [MapLibre GL JS](https://maplibre.org/projects/maplibre-gl-js/).
|
Maputnik is written in ES6 and is using [React](https://github.com/facebook/react) and [MapLibre GL JS](https://maplibre.org/projects/maplibre-gl-js/).
|
||||||
|
|
||||||
We ensure building and developing Maputnik works with the [current active LTS Node.js version and above](https://github.com/nodejs/Release#release-schedule).
|
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.
|
|
||||||
|
|
||||||
Install the deps, start the dev server and open the web browser on `http://localhost:8888/`.
|
Install the deps, start the dev server and open the web browser on `http://localhost:8888/`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -74,14 +67,14 @@ Lint the JavaScript code.
|
|||||||
```
|
```
|
||||||
# run linter
|
# run linter
|
||||||
npm run lint
|
npm run lint
|
||||||
npm run lint-css
|
npm run lint-styles
|
||||||
npm run sort-styles
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
For E2E testing we use [Cypress](https://www.cypress.io/)
|
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*:
|
Now open a terminal and run the following using *chrome*:
|
||||||
|
|
||||||
@@ -101,23 +94,68 @@ You can also see the tests as they run or select which suites to run by executin
|
|||||||
npm run cy:open
|
npm run cy:open
|
||||||
```
|
```
|
||||||
|
|
||||||
## Release process
|
|
||||||
|
|
||||||
1. Review [`CHANGELOG.md`](/CHANGELOG.md)
|
## Related Projects
|
||||||
- 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.
|
|
||||||
|
|
||||||
|
- [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
|
## Sponsors
|
||||||
|
|
||||||
Thanks to the supporters of the **[Kickstarter campaign](https://www.kickstarter.com/projects/174808720/maputnik-visual-map-editor-for-mapbox-gl)**. This project would not be possible without these commercial and individual sponsors.
|
Thanks to the supporters of the **[Kickstarter campaign](https://www.kickstarter.com/projects/174808720/maputnik-visual-map-editor-for-mapbox-gl)**. This project would not be possible without these commercial and individual sponsors.
|
||||||
You can see this file's history for previous sponsors of the original Maputnik repo.
|
|
||||||
Read more about the MapLibre Sponsorship Program at https://maplibre.org/sponsors/.
|
### Gold
|
||||||
|
|
||||||
|
- [Wemap](https://getwemap.com/)
|
||||||
|
- [Orbicon Informatik](https://www.orbiconinformatik.dk/)
|
||||||
|
- [Terranodo](http://terranodo.io/)
|
||||||
|
|
||||||
|
<a href="https://getwemap.com/">
|
||||||
|
<img width="33%" alt="Wemap" style="display:inline" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/wemap.jpg" />
|
||||||
|
</a>
|
||||||
|
<a href="http://terranodo.io/">
|
||||||
|
<img width="33%" alt="Terranodo" style="display:inline" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/terranodo.png" />
|
||||||
|
</a>
|
||||||
|
<a href="https://www.orbiconinformatik.dk/">
|
||||||
|
<img width="32%" alt="Terranodo" style="display:inline" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/orbicon_informatik.png" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
### Silver
|
||||||
|
|
||||||
|
- [Klokan Technologies](https://www.klokantech.com/)
|
||||||
|
- [Geofabrik](http://www.geofabrik.de/)
|
||||||
|
- [Dreipol](https://www.dreipol.ch/)
|
||||||
|
|
||||||
|
<a href="https://www.klokantech.com/">
|
||||||
|
<img width="18%" alt="Klokan Technologies" style="display:inline-block" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/klokantech.png" />
|
||||||
|
</a>
|
||||||
|
<a href="http://www.geofabrik.de/">
|
||||||
|
<img width="18%" alt="Geofabrik" style="display:inline-block" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/geofabrik.png" />
|
||||||
|
</a>
|
||||||
|
<a href="https://www.dreipol.ch/">
|
||||||
|
<img width="18%" alt="Dreipol" style="display:inline-block" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/dreipol.png" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
### Individuals
|
||||||
|
|
||||||
|
**Influential Stakeholder**
|
||||||
|
|
||||||
|
Alan McConchie, Odi, Mats Norén, Uli [geOps](http://geops.ch/), Helge Fahrnberger ([Toursprung](http://www.toursprung.com/)), Kirusanth Poopalasingam
|
||||||
|
|
||||||
|
**Stakeholder**
|
||||||
|
|
||||||
|
Brian Flood, Vasile Coțovanu, Andreas Kalkbrenner, Christian Mäder, Gregor Wassmann, Lee Armstrong, Rafel, Jon Burgess, Lukas Lehmann, Joachim Ungar, Alois Ackermann, Zsolt Ero, Jordan Meek
|
||||||
|
|
||||||
|
**Supporter**
|
||||||
|
|
||||||
|
Sina Martinelli, Nicholas Doiron, Neil Cawse, Urs42, Benedikt Groß, Manuel Roth, Janko Mihelić, Moritz Stefaner, Sebastian Ahoi, Juerg Uhlmann, Tom Wider, Nadia Panchaud, Oliver Snowden, Stephan Heuel, Tobin Bradley, Adrian Herzog, Antti Lehto, Pascal Mages, Marc Gehling, Imre Samu, Lauri K., Visahavel Parthasarathy, Christophe Waterlot-Buisine, Max Galka, ubahnverleih, Wouter van Dam, Jakob Lobensteiner, Samuel Kurath, Brian Bancroft
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Maputnik is [licensed under MIT](LICENSE) and is Copyright (c) Lukas Martinelli and Maplibre contributors.
|
Maputnik is [licensed under MIT](LICENSE) and is Copyright (c) Lukas Martinelli and contributors.
|
||||||
As contributor please take extra care of not violating any Mapbox trademarks. Do not get inspired by other map studios and make your own decisions for a good style editor.
|
|
||||||
|
**Disclaimer** This is an independent style editor.
|
||||||
|
As contributor please take extra care of not violating any Mapbox trademarks. Do not get inspired by Mapbox Studio and make your own decisions for a good style editor.
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
For an up-to-date policy refer to
|
|
||||||
https://github.com/maplibre/maplibre/blob/main/SECURITY_POLICY.txt
|
|
||||||
@@ -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,7 +8,6 @@ export default defineConfig({
|
|||||||
exclude: "cypress/**/*.*",
|
exclude: "cypress/**/*.*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
e2e: {
|
e2e: {
|
||||||
setupNodeEvents(on, config) {
|
setupNodeEvents(on, config) {
|
||||||
// implement node event listeners here
|
// implement node event listeners here
|
||||||
@@ -21,11 +20,4 @@ export default defineConfig({
|
|||||||
openMode: 0,
|
openMode: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
component: {
|
|
||||||
devServer: {
|
|
||||||
framework: "react",
|
|
||||||
bundler: "vite",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { MaputnikDriver } from "./maputnik-driver";
|
import { MaputnikDriver } from "./maputnik-driver";
|
||||||
|
|
||||||
describe("accessibility", () => {
|
describe("accessibility", () => {
|
||||||
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
||||||
beforeAndAfter();
|
beforeAndAfter();
|
||||||
|
|
||||||
describe("skip links", () => {
|
describe("skip links", () => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { MaputnikDriver } from "./maputnik-driver";
|
import { MaputnikDriver } from "./maputnik-driver";
|
||||||
|
|
||||||
describe("history", () => {
|
describe("history", () => {
|
||||||
const { beforeAndAfter, when, get, then } = new MaputnikDriver();
|
let { beforeAndAfter, when, get, then } = new MaputnikDriver();
|
||||||
beforeAndAfter();
|
beforeAndAfter();
|
||||||
|
|
||||||
let undoKeyCombo: string;
|
let undoKeyCombo: string;
|
||||||
@@ -16,6 +16,7 @@ describe("history", () => {
|
|||||||
it("undo/redo", () => {
|
it("undo/redo", () => {
|
||||||
when.setStyle("geojson");
|
when.setStyle("geojson");
|
||||||
when.modal.open();
|
when.modal.open();
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
|
||||||
|
|
||||||
when.modal.fillLayers({
|
when.modal.fillLayers({
|
||||||
id: "step 1",
|
id: "step 1",
|
||||||
@@ -86,39 +87,4 @@ describe("history", () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not redo after undo and value change", () => {
|
|
||||||
when.setStyle("geojson");
|
|
||||||
when.modal.open();
|
|
||||||
when.modal.fillLayers({
|
|
||||||
id: "step 1",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.modal.open();
|
|
||||||
when.modal.fillLayers({
|
|
||||||
id: "step 2",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.typeKeys(undoKeyCombo);
|
|
||||||
when.typeKeys(undoKeyCombo);
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
|
|
||||||
|
|
||||||
when.modal.open();
|
|
||||||
when.modal.fillLayers({
|
|
||||||
id: "step 3",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.typeKeys(redoKeyCombo);
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "step 3",
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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";
|
import { MaputnikDriver } from "./maputnik-driver";
|
||||||
|
|
||||||
describe("keyboard", () => {
|
describe("keyboard", () => {
|
||||||
const { beforeAndAfter, given, when, get, then } = new MaputnikDriver();
|
let { beforeAndAfter, given, when, get, then } = new MaputnikDriver();
|
||||||
beforeAndAfter();
|
beforeAndAfter();
|
||||||
describe("shortcuts", () => {
|
describe("shortcuts", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
+18
-150
@@ -2,7 +2,7 @@ import { v1 as uuid } from "uuid";
|
|||||||
import { MaputnikDriver } from "./maputnik-driver";
|
import { MaputnikDriver } from "./maputnik-driver";
|
||||||
|
|
||||||
describe("layers", () => {
|
describe("layers", () => {
|
||||||
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
||||||
beforeAndAfter();
|
beforeAndAfter();
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
when.setStyle("both");
|
when.setStyle("both");
|
||||||
@@ -101,7 +101,7 @@ describe("layers", () => {
|
|||||||
});
|
});
|
||||||
describe("background", () => {
|
describe("background", () => {
|
||||||
it("add", () => {
|
it("add", () => {
|
||||||
const id = when.modal.fillLayers({
|
let id = when.modal.fillLayers({
|
||||||
type: "background",
|
type: "background",
|
||||||
});
|
});
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
@@ -117,7 +117,7 @@ describe("layers", () => {
|
|||||||
describe("modify", () => {
|
describe("modify", () => {
|
||||||
function createBackground() {
|
function createBackground() {
|
||||||
// Setup
|
// Setup
|
||||||
const id = uuid();
|
let id = uuid();
|
||||||
|
|
||||||
when.selectWithin("add-layer.layer-type", "background");
|
when.selectWithin("add-layer.layer-type", "background");
|
||||||
when.setValue("add-layer.layer-id.input", "background:" + id);
|
when.setValue("add-layer.layer-id.input", "background:" + id);
|
||||||
@@ -139,11 +139,11 @@ describe("layers", () => {
|
|||||||
describe("layer", () => {
|
describe("layer", () => {
|
||||||
it("expand/collapse");
|
it("expand/collapse");
|
||||||
it("id", () => {
|
it("id", () => {
|
||||||
const bgId = createBackground();
|
let bgId = createBackground();
|
||||||
|
|
||||||
when.click("layer-list-item:background:" + bgId);
|
when.click("layer-list-item:background:" + bgId);
|
||||||
|
|
||||||
const id = uuid();
|
let id = uuid();
|
||||||
when.setValue("layer-editor.layer-id.input", "foobar:" + id);
|
when.setValue("layer-editor.layer-id.input", "foobar:" + id);
|
||||||
when.click("min-zoom");
|
when.click("min-zoom");
|
||||||
|
|
||||||
@@ -219,7 +219,7 @@ describe("layers", () => {
|
|||||||
|
|
||||||
describe("comments", () => {
|
describe("comments", () => {
|
||||||
let bgId: string;
|
let bgId: string;
|
||||||
const comment = "42";
|
let comment = "42";
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
bgId = createBackground();
|
bgId = createBackground();
|
||||||
@@ -266,7 +266,7 @@ describe("layers", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
bgId = createBackground();
|
bgId = createBackground();
|
||||||
when.click("layer-list-item:background:" + bgId);
|
when.click("layer-list-item:background:" + bgId);
|
||||||
when.click("spec-field:background-color");
|
when.click("spec-field:background-color", 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should update style in local storage", () => {
|
it("should update style in local storage", () => {
|
||||||
@@ -280,25 +280,6 @@ describe("layers", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
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", () => {
|
describe("filter", () => {
|
||||||
@@ -320,11 +301,12 @@ describe("layers", () => {
|
|||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
it.skip("parse error", () => {
|
it.skip("parse error", () => {
|
||||||
const bgId = createBackground();
|
let bgId = createBackground();
|
||||||
|
|
||||||
when.click("layer-list-item:background:" + bgId);
|
when.click("layer-list-item:background:" + bgId);
|
||||||
|
|
||||||
const errorSelector = ".CodeMirror-lint-marker-error";
|
let errorSelector = ".CodeMirror-lint-marker-error";
|
||||||
|
// Not a good idea to check that something doesn't exist. infinite things don't exist.
|
||||||
then(get.elementByTestId(errorSelector)).shouldNotExist();
|
then(get.elementByTestId(errorSelector)).shouldNotExist();
|
||||||
|
|
||||||
when.click(".CodeMirror");
|
when.click(".CodeMirror");
|
||||||
@@ -339,7 +321,7 @@ describe("layers", () => {
|
|||||||
|
|
||||||
describe("fill", () => {
|
describe("fill", () => {
|
||||||
it("add", () => {
|
it("add", () => {
|
||||||
const id = when.modal.fillLayers({
|
let id = when.modal.fillLayers({
|
||||||
type: "fill",
|
type: "fill",
|
||||||
layer: "example",
|
layer: "example",
|
||||||
});
|
});
|
||||||
@@ -361,7 +343,7 @@ describe("layers", () => {
|
|||||||
|
|
||||||
describe("line", () => {
|
describe("line", () => {
|
||||||
it("add", () => {
|
it("add", () => {
|
||||||
const id = when.modal.fillLayers({
|
let id = when.modal.fillLayers({
|
||||||
type: "line",
|
type: "line",
|
||||||
layer: "example",
|
layer: "example",
|
||||||
});
|
});
|
||||||
@@ -378,63 +360,14 @@ describe("layers", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("groups", () => {
|
it("groups", () => {
|
||||||
when.modal.open();
|
// TODO
|
||||||
const id1 = when.modal.fillLayers({
|
// Click each of the layer groups.
|
||||||
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", () => {
|
describe("symbol", () => {
|
||||||
it("add", () => {
|
it("add", () => {
|
||||||
const id = when.modal.fillLayers({
|
let id = when.modal.fillLayers({
|
||||||
type: "symbol",
|
type: "symbol",
|
||||||
layer: "example",
|
layer: "example",
|
||||||
});
|
});
|
||||||
@@ -453,7 +386,7 @@ describe("layers", () => {
|
|||||||
|
|
||||||
describe("raster", () => {
|
describe("raster", () => {
|
||||||
it("add", () => {
|
it("add", () => {
|
||||||
const id = when.modal.fillLayers({
|
let id = when.modal.fillLayers({
|
||||||
type: "raster",
|
type: "raster",
|
||||||
layer: "raster",
|
layer: "raster",
|
||||||
});
|
});
|
||||||
@@ -472,7 +405,7 @@ describe("layers", () => {
|
|||||||
|
|
||||||
describe("circle", () => {
|
describe("circle", () => {
|
||||||
it("add", () => {
|
it("add", () => {
|
||||||
const id = when.modal.fillLayers({
|
let id = when.modal.fillLayers({
|
||||||
type: "circle",
|
type: "circle",
|
||||||
layer: "example",
|
layer: "example",
|
||||||
});
|
});
|
||||||
@@ -491,7 +424,7 @@ describe("layers", () => {
|
|||||||
|
|
||||||
describe("fill extrusion", () => {
|
describe("fill extrusion", () => {
|
||||||
it("add", () => {
|
it("add", () => {
|
||||||
const id = when.modal.fillLayers({
|
let id = when.modal.fillLayers({
|
||||||
type: "fill-extrusion",
|
type: "fill-extrusion",
|
||||||
layer: "example",
|
layer: "example",
|
||||||
});
|
});
|
||||||
@@ -543,69 +476,4 @@ describe("layers", () => {
|
|||||||
).shouldBeVisible();
|
).shouldBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("layereditor jsonlint should error", ()=>{
|
|
||||||
it("add", () => {
|
|
||||||
const id = when.modal.fillLayers({
|
|
||||||
type: "circle",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "circle",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const sourceText = get.elementByText('"source"');
|
|
||||||
|
|
||||||
sourceText.click();
|
|
||||||
sourceText.type("\"");
|
|
||||||
|
|
||||||
const error = get.element('.CodeMirror-lint-marker-error');
|
|
||||||
error.should('exist');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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.dragAndDrop(get.elementByTestId("layer-list-item:" + firstId), get.elementByTestId("layer-list-item:" + thirdId));
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: secondId,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: thirdId,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: firstId,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { MaputnikDriver } from "./maputnik-driver";
|
import { MaputnikDriver } from "./maputnik-driver";
|
||||||
|
|
||||||
describe("map", () => {
|
describe("map", () => {
|
||||||
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
||||||
beforeAndAfter();
|
beforeAndAfter();
|
||||||
describe("zoom level", () => {
|
describe("zoom level", () => {
|
||||||
it("via url", () => {
|
it("via url", () => {
|
||||||
const zoomLevel = 12.37;
|
let zoomLevel = 12.37;
|
||||||
when.setStyle("geojson", zoomLevel);
|
when.setStyle("geojson", zoomLevel);
|
||||||
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
|
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
|
||||||
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText(
|
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText(
|
||||||
@@ -14,7 +14,7 @@ describe("map", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("via map controls", () => {
|
it("via map controls", () => {
|
||||||
const zoomLevel = 12.37;
|
let zoomLevel = 12.37;
|
||||||
when.setStyle("geojson", zoomLevel);
|
when.setStyle("geojson", zoomLevel);
|
||||||
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
|
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
|
||||||
when.clickZoomIn();
|
when.clickZoomIn();
|
||||||
@@ -23,10 +23,4 @@ describe("map", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("search", () => {
|
|
||||||
it('should exist', () => {
|
|
||||||
then(get.searchControl()).shouldBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,24 +1,20 @@
|
|||||||
/// <reference types="cypress-plugin-tab" />
|
|
||||||
|
|
||||||
import { CypressHelper } from "@shellygo/cypress-test-utils";
|
import { CypressHelper } from "@shellygo/cypress-test-utils";
|
||||||
import { Assertable, then } from "@shellygo/cypress-test-utils/assertable";
|
import { Assertable, then } from "@shellygo/cypress-test-utils/assertable";
|
||||||
import MaputnikCypressHelper from "./maputnik-cypress-helper";
|
import MaputnikCypressHelper from "./cypress-wrapper-driver";
|
||||||
import ModalDriver from "./modal-driver";
|
import ModalDriver from "./modal-driver";
|
||||||
const baseUrl = "http://localhost:8888/";
|
const baseUrl = "http://localhost:8888/";
|
||||||
|
|
||||||
const styleFromWindow = (win: Window) => {
|
const styleFromWindow = (win: Window) => {
|
||||||
const styleId = win.localStorage.getItem("maputnik:latest_style");
|
const styleId = win.localStorage.getItem("maputnik:latest_style");
|
||||||
const styleItemKey = `maputnik:style:${styleId}`;
|
const styleItem = win.localStorage.getItem(`maputnik:style:${styleId}`);
|
||||||
const styleItem = win.localStorage.getItem(styleItemKey);
|
const obj = JSON.parse(styleItem || "");
|
||||||
if (!styleItem) throw new Error("Could not get styleItem from localStorage");
|
|
||||||
const obj = JSON.parse(styleItem);
|
|
||||||
return obj;
|
return obj;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class MaputnikAssertable<T> extends Assertable<T> {
|
export class MaputnikAssertable<T> extends Assertable<T> {
|
||||||
shouldEqualToStoredStyle = () =>
|
shouldEqualToStoredStyle = () =>
|
||||||
then(
|
then(
|
||||||
new CypressHelper().get.window().then((win: Window) => {
|
new CypressHelper().get.window().then((win) => {
|
||||||
const style = styleFromWindow(win);
|
const style = styleFromWindow(win);
|
||||||
then(this.chainable).shouldDeepNestedInclude(style);
|
then(this.chainable).shouldDeepNestedInclude(style);
|
||||||
})
|
})
|
||||||
@@ -94,8 +90,8 @@ export class MaputnikDriver {
|
|||||||
public when = {
|
public when = {
|
||||||
...this.helper.when,
|
...this.helper.when,
|
||||||
modal: this.modalDriver.when,
|
modal: this.modalDriver.when,
|
||||||
doWithin: (selector: string, fn: () => void) => {
|
within: (selector: string, fn: () => void) => {
|
||||||
this.helper.when.doWithin(fn, selector);
|
this.helper.when.within(fn, selector);
|
||||||
},
|
},
|
||||||
tab: () => this.helper.get.element("body").tab(),
|
tab: () => this.helper.get.element("body").tab(),
|
||||||
waitForExampleFileResponse: () => {
|
waitForExampleFileResponse: () => {
|
||||||
@@ -110,32 +106,30 @@ export class MaputnikDriver {
|
|||||||
styleProperties: "geojson" | "raster" | "both" | "layer" | "",
|
styleProperties: "geojson" | "raster" | "both" | "layer" | "",
|
||||||
zoom?: number
|
zoom?: number
|
||||||
) => {
|
) => {
|
||||||
const url = new URL(baseUrl);
|
let url = "?debug";
|
||||||
switch (styleProperties) {
|
switch (styleProperties) {
|
||||||
case "geojson":
|
case "geojson":
|
||||||
url.searchParams.set("style", baseUrl + "geojson-style.json");
|
url += `&style=${baseUrl}geojson-style.json`;
|
||||||
break;
|
break;
|
||||||
case "raster":
|
case "raster":
|
||||||
url.searchParams.set("style", baseUrl + "raster-style.json");
|
url += `&style=${baseUrl}raster-style.json`;
|
||||||
break;
|
break;
|
||||||
case "both":
|
case "both":
|
||||||
url.searchParams.set("style", baseUrl + "geojson-raster-style.json");
|
url += `&style=${baseUrl}geojson-raster-style.json`;
|
||||||
break;
|
break;
|
||||||
case "layer":
|
case "layer":
|
||||||
url.searchParams.set("style", baseUrl + "example-layer-style.json");
|
url += `&style=${baseUrl}/example-layer-style.json`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (zoom) {
|
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) {
|
if (styleProperties) {
|
||||||
this.helper.when.acceptConfirm();
|
this.helper.when.acceptConfirm();
|
||||||
}
|
}
|
||||||
// when methods should not include assertions
|
// when methods should not include assertions
|
||||||
const toolbarLink = this.helper.get.elementByTestId("toolbar:link")
|
this.helper.get.elementByTestId("toolbar:link").should("be.visible");
|
||||||
toolbarLink.scrollIntoView();
|
|
||||||
toolbarLink.should("be.visible");
|
|
||||||
},
|
},
|
||||||
|
|
||||||
typeKeys: (keys: string) => this.helper.get.element("body").type(keys),
|
typeKeys: (keys: string) => this.helper.get.element("body").type(keys),
|
||||||
@@ -145,7 +139,7 @@ export class MaputnikDriver {
|
|||||||
},
|
},
|
||||||
|
|
||||||
selectWithin: (selector: string, value: string) => {
|
selectWithin: (selector: string, value: string) => {
|
||||||
this.when.doWithin(selector, () => {
|
this.when.within(selector, () => {
|
||||||
this.helper.get.element("select").select(value);
|
this.helper.get.element("select").select(value);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -175,6 +169,13 @@ export class MaputnikDriver {
|
|||||||
styleFromLocalStorage: () =>
|
styleFromLocalStorage: () =>
|
||||||
this.helper.get.window().then((win) => styleFromWindow(win)),
|
this.helper.get.window().then((win) => styleFromWindow(win)),
|
||||||
|
|
||||||
|
maputnikStyleFromLocalStorageObj: () => {
|
||||||
|
const styleId = localStorage.getItem("maputnik:latest_style");
|
||||||
|
const styleItem = localStorage.getItem(`maputnik:style:${styleId}`);
|
||||||
|
const obj = JSON.parse(styleItem || "");
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
exampleFileUrl: () => {
|
exampleFileUrl: () => {
|
||||||
return baseUrl + "example-style.json";
|
return baseUrl + "example-style.json";
|
||||||
},
|
},
|
||||||
@@ -183,6 +184,5 @@ export class MaputnikDriver {
|
|||||||
skipTargetLayerEditor: () =>
|
skipTargetLayerEditor: () =>
|
||||||
this.helper.get.elementByTestId("skip-target-layer-editor"),
|
this.helper.get.elementByTestId("skip-target-layer-editor"),
|
||||||
canvas: () => this.helper.get.element("canvas"),
|
canvas: () => this.helper.get.element("canvas"),
|
||||||
searchControl: () => this.helper.get.element('.maplibregl-ctrl-geocoder')
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { v1 as uuid } from "uuid";
|
import { v1 as uuid } from "uuid";
|
||||||
import MaputnikCypressHelper from "./maputnik-cypress-helper";
|
import MaputnikCypressHelper from "./cypress-wrapper-driver";
|
||||||
|
|
||||||
export default class ModalDriver {
|
export default class ModalDriver {
|
||||||
private helper = new MaputnikCypressHelper();
|
private helper = new MaputnikCypressHelper();
|
||||||
@@ -8,8 +8,8 @@ export default class ModalDriver {
|
|||||||
fillLayers: (opts: { type: string; layer?: string; id?: string }) => {
|
fillLayers: (opts: { type: string; layer?: string; id?: string }) => {
|
||||||
// Having logic in test code is an anti pattern.
|
// Having logic in test code is an anti pattern.
|
||||||
// This should be splitted to multiple single responsibility functions
|
// This should be splitted to multiple single responsibility functions
|
||||||
const type = opts.type;
|
let type = opts.type;
|
||||||
const layer = opts.layer;
|
let layer = opts.layer;
|
||||||
let id;
|
let id;
|
||||||
if (opts.id) {
|
if (opts.id) {
|
||||||
id = opts.id;
|
id = opts.id;
|
||||||
@@ -20,8 +20,8 @@ export default class ModalDriver {
|
|||||||
this.helper.when.type("add-layer.layer-id.input", id);
|
this.helper.when.type("add-layer.layer-id.input", id);
|
||||||
|
|
||||||
if (layer) {
|
if (layer) {
|
||||||
this.helper.when.doWithin(() => {
|
this.helper.when.within(() => {
|
||||||
this.helper.get.element("input").clear().type(layer!);
|
this.helper.get.element("input").type(layer!);
|
||||||
}, "add-layer.layer-source-block");
|
}, "add-layer.layer-source-block");
|
||||||
}
|
}
|
||||||
this.helper.when.click("add-layer");
|
this.helper.when.click("add-layer");
|
||||||
|
|||||||
+12
-178
@@ -1,12 +1,11 @@
|
|||||||
import { MaputnikDriver } from "./maputnik-driver";
|
import { MaputnikDriver } from "./maputnik-driver";
|
||||||
import tokens from "../../src/config/tokens.json" with {type: "json"};
|
|
||||||
|
|
||||||
describe("modals", () => {
|
describe("modals", () => {
|
||||||
const { beforeAndAfter, when, get, given, then } = new MaputnikDriver();
|
let { beforeAndAfter, when, get, then } = new MaputnikDriver();
|
||||||
beforeAndAfter();
|
beforeAndAfter();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
when.setStyle("");
|
when.visit("/");
|
||||||
});
|
});
|
||||||
describe("open", () => {
|
describe("open", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -26,7 +25,7 @@ describe("modals", () => {
|
|||||||
|
|
||||||
describe("when click open url", () => {
|
describe("when click open url", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const styleFileUrl = get.exampleFileUrl();
|
let styleFileUrl = get.exampleFileUrl();
|
||||||
|
|
||||||
when.setValue("modal:open.url.input", styleFileUrl);
|
when.setValue("modal:open.url.input", styleFileUrl);
|
||||||
when.click("modal:open.url.button");
|
when.click("modal:open.url.button");
|
||||||
@@ -52,7 +51,7 @@ describe("modals", () => {
|
|||||||
when.click("nav:export");
|
when.click("nav:export");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("close", () => {
|
it.skip("close", () => {
|
||||||
when.modal.close("modal:export");
|
when.modal.close("modal:export");
|
||||||
then(get.elementByTestId("modal:export")).shouldNotExist();
|
then(get.elementByTestId("modal:export")).shouldNotExist();
|
||||||
});
|
});
|
||||||
@@ -62,60 +61,9 @@ describe("modals", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("sources", () => {
|
describe("sources", () => {
|
||||||
beforeEach(() => {
|
|
||||||
when.setStyle("layer");
|
|
||||||
when.click("nav:sources");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("active sources");
|
it("active sources");
|
||||||
it("public source");
|
it("public source");
|
||||||
|
it("add new 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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("inspect", () => {
|
describe("inspect", () => {
|
||||||
@@ -131,12 +79,12 @@ describe("modals", () => {
|
|||||||
when.click("nav:settings");
|
when.click("nav:settings");
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when click name filed spec information", () => {
|
describe("when click name", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
when.click("field-doc-button-Name");
|
when.click("field-doc-button-Name");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show the spec information", () => {
|
it("name", () => {
|
||||||
then(get.elementsText("spec-field-doc")).shouldInclude(
|
then(get.elementsText("spec-field-doc")).shouldInclude(
|
||||||
"name for the style"
|
"name for the style"
|
||||||
);
|
);
|
||||||
@@ -178,7 +126,7 @@ describe("modals", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("glyphs url", () => {
|
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.setValue("modal:settings.glyphs", glyphsUrl);
|
||||||
when.click("modal:settings.name");
|
when.click("modal:settings.name");
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
@@ -187,55 +135,31 @@ describe("modals", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("maptiler access token", () => {
|
it("maptiler access token", () => {
|
||||||
const apiKey = "testing123";
|
let apiKey = "testing123";
|
||||||
when.setValue(
|
when.setValue(
|
||||||
"modal:settings.maputnik:openmaptiles_access_token",
|
"modal:settings.maputnik:openmaptiles_access_token",
|
||||||
apiKey
|
apiKey
|
||||||
);
|
);
|
||||||
when.click("modal:settings.name");
|
when.click("modal:settings.name");
|
||||||
then(
|
then(
|
||||||
get.styleFromLocalStorage().then((style) => style.metadata)
|
get.styleFromLocalStorage().pipe((style) => style.metadata)
|
||||||
).shouldInclude({
|
).shouldInclude({
|
||||||
"maputnik:openmaptiles_access_token": apiKey,
|
"maputnik:openmaptiles_access_token": apiKey,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("thunderforest access token", () => {
|
it("thunderforest access token", () => {
|
||||||
const apiKey = "testing123";
|
let apiKey = "testing123";
|
||||||
when.setValue(
|
when.setValue(
|
||||||
"modal:settings.maputnik:thunderforest_access_token",
|
"modal:settings.maputnik:thunderforest_access_token",
|
||||||
apiKey
|
apiKey
|
||||||
);
|
);
|
||||||
when.click("modal:settings.name");
|
when.click("modal:settings.name");
|
||||||
then(
|
then(
|
||||||
get.styleFromLocalStorage().then((style) => style.metadata)
|
get.styleFromLocalStorage().pipe((style) => style.metadata)
|
||||||
).shouldInclude({ "maputnik:thunderforest_access_token": apiKey });
|
).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 renderer", () => {
|
it("style renderer", () => {
|
||||||
cy.on("uncaught:exception", () => false); // this is due to the fact that this is an invalid style for openlayers
|
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");
|
when.select("modal:settings.maputnik:renderer", "ol");
|
||||||
@@ -248,99 +172,9 @@ describe("modals", () => {
|
|||||||
metadata: { "maputnik:renderer": "ol" },
|
metadata: { "maputnik:renderer": "ol" },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
it("inlcude 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", () => {
|
describe("sources", () => {
|
||||||
it("toggle");
|
it("toggle");
|
||||||
});
|
});
|
||||||
|
|
||||||
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,114 +1,12 @@
|
|||||||
{
|
{
|
||||||
"id": "test-style",
|
"id": "test-style",
|
||||||
"version": 8,
|
"version": 8,
|
||||||
"name": "Test Style",
|
"name": "Test Style",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"maputnik:renderer": "mlgljs",
|
"maputnik:renderer": "mlgljs"
|
||||||
"data": [
|
},
|
||||||
0,
|
"sources": {},
|
||||||
1,
|
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
2,
|
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
3,
|
"layers": []
|
||||||
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": []
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,61 +0,0 @@
|
|||||||
import eslint from '@eslint/js';
|
|
||||||
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 tseslint.config({
|
|
||||||
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
|
|
||||||
},
|
|
||||||
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': ['error', 2],
|
|
||||||
'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',
|
|
||||||
|
|
||||||
},
|
|
||||||
linterOptions: {
|
|
||||||
reportUnusedDisableDirectives: true,
|
|
||||||
noInlineConfig: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
export default {
|
|
||||||
output: 'src/locales/$LOCALE/$NAMESPACE.json',
|
|
||||||
locales: [ 'de', 'fr', 'he', 'it','ja', 'zh' ],
|
|
||||||
|
|
||||||
// Because some keys are dynamically generated, i18next-parser can't detect them.
|
|
||||||
// We add these keys manually, so we don't want to remove them.
|
|
||||||
keepRemoved: true,
|
|
||||||
|
|
||||||
// We use plain English keys, so we disable key and namespace separators.
|
|
||||||
keySeparator: false,
|
|
||||||
namespaceSeparator: false,
|
|
||||||
|
|
||||||
defaultValue: (_locale, _ns, _key) => {
|
|
||||||
// The default value is a string that indicates that the string is not translated.
|
|
||||||
return '__STRING_NOT_TRANSLATED__';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Generated
+11867
-6207
File diff suppressed because it is too large
Load Diff
+75
-76
@@ -1,53 +1,43 @@
|
|||||||
{
|
{
|
||||||
"name": "maputnik",
|
"name": "maputnik",
|
||||||
"version": "3.0.0",
|
"version": "2.0.0-pre.2",
|
||||||
"description": "A MapLibre GL visual style editor",
|
"description": "A MapLibre GL visual style editor",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "''",
|
"main": "''",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"build": "tsc && vite build --mode=production",
|
"build": "tsc && vite build",
|
||||||
"build-desktop": "tsc && vite build --mode=desktop && cd desktop && make",
|
"lint": "eslint ./src ./cypress --ext ts,tsx,js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"build-linux": "tsc && vite build --mode=desktop && cd desktop && make bin/linux/maputnik",
|
|
||||||
"i18n:refresh": "i18next 'src/**/*.{ts,tsx,js,jsx}'",
|
|
||||||
"lint": "eslint",
|
|
||||||
"test": "cypress run",
|
"test": "cypress run",
|
||||||
"cy:open": "cypress open",
|
"cy:open": "cypress open",
|
||||||
"lint-css": "stylelint \"src/styles/*.scss\"",
|
"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"
|
"storybook": "storybook dev -h 0.0.0.0 -p 6006",
|
||||||
|
"build-storybook": "storybook build -o build/storybook"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/maplibre/maputnik"
|
"url": "https://github.com/maputnik/editor"
|
||||||
},
|
},
|
||||||
"author": "Lukas Martinelli",
|
"author": "Lukas Martinelli",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://github.com/maplibre/maputnik#readme",
|
"homepage": "https://github.com/maputnik/editor#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@mapbox/mapbox-gl-rtl-text": "^0.2.3",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@maplibre/maplibre-gl-style-spec": "^17.0.1",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@mdi/js": "^6.6.96",
|
||||||
"@mapbox/mapbox-gl-rtl-text": "^0.3.0",
|
"@mdi/react": "^1.5.0",
|
||||||
"@maplibre/maplibre-gl-geocoder": "^1.9.0",
|
"@typescript-eslint/eslint-plugin": "^6.16.0",
|
||||||
"@maplibre/maplibre-gl-inspect": "^1.7.1",
|
"@typescript-eslint/parser": "^6.16.0",
|
||||||
"@maplibre/maplibre-gl-style-spec": "^23.3.0",
|
|
||||||
"@mdi/js": "^7.4.47",
|
|
||||||
"@mdi/react": "^1.6.1",
|
|
||||||
"@prantlf/jsonlint": "^16.0.0",
|
|
||||||
"array-move": "^4.0.0",
|
"array-move": "^4.0.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.3.1",
|
||||||
"codemirror": "^5.65.20",
|
"codemirror": "^5.65.2",
|
||||||
"color": "^5.0.0",
|
"color": "^4.2.3",
|
||||||
"detect-browser": "^5.3.0",
|
"detect-browser": "^5.3.0",
|
||||||
"downshift": "^9.0.10",
|
|
||||||
"events": "^3.3.0",
|
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"i18next": "^25.5.2",
|
"json-stringify-pretty-compact": "^3.0.0",
|
||||||
"i18next-browser-languagedetector": "^8.2.0",
|
|
||||||
"i18next-resources-to-backend": "^1.2.1",
|
|
||||||
"json-stringify-pretty-compact": "^4.0.0",
|
|
||||||
"json-to-ast": "^2.1.0",
|
"json-to-ast": "^2.1.0",
|
||||||
|
"jsonlint": "github:josdejong/jsonlint#85a19d7",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lodash.capitalize": "^4.2.1",
|
"lodash.capitalize": "^4.2.1",
|
||||||
"lodash.clamp": "^4.0.3",
|
"lodash.clamp": "^4.0.3",
|
||||||
@@ -55,27 +45,30 @@
|
|||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"maplibre-gl": "^5.7.1",
|
"mapbox-gl-inspect": "^1.3.1",
|
||||||
|
"maplibre-gl": "^2.4.0",
|
||||||
"maputnik-design": "github:maputnik/design#172b06c",
|
"maputnik-design": "github:maputnik/design#172b06c",
|
||||||
"ol": "^10.6.1",
|
"ol": "^6.14.1",
|
||||||
"ol-mapbox-style": "^13.1.0",
|
"ol-mapbox-style": "^7.1.1",
|
||||||
"pmtiles": "^4.3.0",
|
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "^18.2.0",
|
"react": "^16.0.0",
|
||||||
"react-accessible-accordion": "^5.0.1",
|
"react-accessible-accordion": "^4.0.0",
|
||||||
"react-aria-menubutton": "^7.0.3",
|
"react-aria-menubutton": "^7.0.3",
|
||||||
"react-aria-modal": "^5.0.2",
|
"react-aria-modal": "^4.0.1",
|
||||||
|
"react-autobind": "^1.0.6",
|
||||||
|
"react-autocomplete": "^1.8.1",
|
||||||
"react-collapse": "^5.1.1",
|
"react-collapse": "^5.1.1",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^16.0.0",
|
||||||
"react-file-reader-input": "^2.0.0",
|
"react-file-reader-input": "^2.0.0",
|
||||||
"react-i18next": "^15.7.3",
|
|
||||||
"react-icon-base": "^2.1.2",
|
"react-icon-base": "^2.1.2",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^4.3.1",
|
||||||
|
"react-sortable-hoc": "^2.0.0",
|
||||||
"reconnecting-websocket": "^4.4.0",
|
"reconnecting-websocket": "^4.4.0",
|
||||||
"slugify": "^1.6.6",
|
"sass": "^1.50.0",
|
||||||
|
"slugify": "^1.6.5",
|
||||||
"string-hash": "^1.1.3",
|
"string-hash": "^1.1.3",
|
||||||
"url": "^0.11.4"
|
"url": "^0.11.0"
|
||||||
},
|
},
|
||||||
"jshintConfig": {
|
"jshintConfig": {
|
||||||
"esversion": 6
|
"esversion": 6
|
||||||
@@ -95,16 +88,24 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cypress/code-coverage": "^3.14.6",
|
"@cypress/code-coverage": "^3.12.15",
|
||||||
"@eslint/js": "^9.35.0",
|
|
||||||
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
||||||
"@rollup/plugin-replace": "^6.0.2",
|
"@rollup/plugin-replace": "^5.0.5",
|
||||||
"@shellygo/cypress-test-utils": "^6.0.1",
|
"@shellygo/cypress-test-utils": "^2.0.17",
|
||||||
"@types/codemirror": "^5.60.16",
|
"@storybook/addon-a11y": "^7.6.5",
|
||||||
"@types/color": "^4.2.0",
|
"@storybook/addon-actions": "^7.6.5",
|
||||||
"@types/cors": "^2.8.19",
|
"@storybook/addon-links": "^7.6.5",
|
||||||
|
"@storybook/addon-storysource": "^7.6.5",
|
||||||
|
"@storybook/addons": "^7.6.5",
|
||||||
|
"@storybook/builder-vite": "^7.6.5",
|
||||||
|
"@storybook/react": "^7.6.5",
|
||||||
|
"@storybook/react-vite": "^7.6.5",
|
||||||
|
"@storybook/theming": "^7.6.5",
|
||||||
|
"@types/codemirror": "^5.60.15",
|
||||||
|
"@types/color": "^3.0.6",
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/geojson": "^7946.0.16",
|
"@types/geojson": "^7946.0.13",
|
||||||
"@types/json-to-ast": "^2.1.4",
|
"@types/json-to-ast": "^2.1.4",
|
||||||
"@types/lodash.capitalize": "^4.2.9",
|
"@types/lodash.capitalize": "^4.2.9",
|
||||||
"@types/lodash.clamp": "^4.0.9",
|
"@types/lodash.clamp": "^4.0.9",
|
||||||
@@ -112,39 +113,37 @@
|
|||||||
"@types/lodash.get": "^4.4.9",
|
"@types/lodash.get": "^4.4.9",
|
||||||
"@types/lodash.isequal": "^4.5.8",
|
"@types/lodash.isequal": "^4.5.8",
|
||||||
"@types/lodash.throttle": "^4.1.9",
|
"@types/lodash.throttle": "^4.1.9",
|
||||||
"@types/randomcolor": "^0.5.9",
|
"@types/react": "^16.14.52",
|
||||||
"@types/react": "^18.2.67",
|
"@types/react-aria-menubutton": "^6.2.13",
|
||||||
"@types/react-aria-menubutton": "^6.2.14",
|
"@types/react-aria-modal": "^4.0.9",
|
||||||
"@types/react-aria-modal": "^5.0.0",
|
"@types/react-autocomplete": "^1.8.9",
|
||||||
"@types/react-collapse": "^5.0.4",
|
"@types/react-collapse": "^5.0.4",
|
||||||
"@types/react-color": "^3.0.13",
|
"@types/react-color": "^3.0.10",
|
||||||
"@types/react-dom": "^18.2.22",
|
"@types/react-dom": "^16.9.24",
|
||||||
"@types/react-file-reader-input": "^2.0.4",
|
"@types/react-file-reader-input": "^2.0.4",
|
||||||
"@types/react-icon-base": "^2.1.6",
|
"@types/react-icon-base": "^2.1.6",
|
||||||
"@types/string-hash": "^1.1.3",
|
"@types/string-hash": "^1.1.3",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^9.0.7",
|
||||||
"@types/wicg-file-system-access": "^2023.10.6",
|
"@vitejs/plugin-react": "^4.2.0",
|
||||||
"@vitejs/plugin-react": "^5.0.2",
|
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cypress": "^15.1.0",
|
"cypress": "^13.6.1",
|
||||||
"cypress-plugin-tab": "^1.0.5",
|
"eslint": "^8.56.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint-plugin-react": "^7.33.2",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-refresh": "^0.4.5",
|
||||||
"eslint-plugin-react-refresh": "^0.4.20",
|
"express": "^4.17.3",
|
||||||
"i18next-parser": "^9.3.0",
|
|
||||||
"istanbul": "^0.4.5",
|
"istanbul": "^0.4.5",
|
||||||
"istanbul-lib-coverage": "^3.2.2",
|
"istanbul-lib-coverage": "^3.2.0",
|
||||||
"postcss": "^8.5.6",
|
"mocha": "^9.2.2",
|
||||||
"react-hot-loader": "^4.13.1",
|
"postcss": "^8.4.12",
|
||||||
"sass": "^1.92.1",
|
"react-hot-loader": "^4.13.0",
|
||||||
"stylelint": "^16.24.0",
|
"storybook": "^7.6.5",
|
||||||
"stylelint-config-recommended-scss": "^16.0.0",
|
"stylelint": "^14.6.1",
|
||||||
"stylelint-scss": "^6.12.1",
|
"stylelint-config-recommended-scss": "^6.0.0",
|
||||||
"typescript": "^5.8.3",
|
"stylelint-scss": "^4.2.0",
|
||||||
"typescript-eslint": "^8.43.0",
|
"typescript": "^5.3.3",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^8.3.2",
|
||||||
"vite": "^7.1.5",
|
"vite": "^5.0.0",
|
||||||
"vite-plugin-istanbul": "^7.1.0"
|
"vite-plugin-istanbul": "^5.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+162
-131
@@ -1,3 +1,5 @@
|
|||||||
|
// @ts-ignore - this can be easily replaced with arrow functions
|
||||||
|
import autoBind from 'react-autobind';
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import cloneDeep from 'lodash.clonedeep'
|
import cloneDeep from 'lodash.clonedeep'
|
||||||
import clamp from 'lodash.clamp'
|
import clamp from 'lodash.clamp'
|
||||||
@@ -6,9 +8,8 @@ import get from 'lodash.get'
|
|||||||
import {unset} from 'lodash'
|
import {unset} from 'lodash'
|
||||||
import {arrayMoveMutable} from 'array-move'
|
import {arrayMoveMutable} from 'array-move'
|
||||||
import hash from "string-hash";
|
import hash from "string-hash";
|
||||||
import { PMTiles } from "pmtiles";
|
|
||||||
import {Map, LayerSpecification, StyleSpecification, ValidationError, SourceSpecification} from 'maplibre-gl'
|
import {Map, LayerSpecification, StyleSpecification, ValidationError, SourceSpecification} from 'maplibre-gl'
|
||||||
import {latest, validateStyleMin} from '@maplibre/maplibre-gl-style-spec'
|
import {latest, validate} from '@maplibre/maplibre-gl-style-spec'
|
||||||
|
|
||||||
import MapMaplibreGl from './MapMaplibreGl'
|
import MapMaplibreGl from './MapMaplibreGl'
|
||||||
import MapOpenLayers from './MapOpenLayers'
|
import MapOpenLayers from './MapOpenLayers'
|
||||||
@@ -23,18 +24,22 @@ import ModalExport from './ModalExport'
|
|||||||
import ModalSources from './ModalSources'
|
import ModalSources from './ModalSources'
|
||||||
import ModalOpen from './ModalOpen'
|
import ModalOpen from './ModalOpen'
|
||||||
import ModalShortcuts from './ModalShortcuts'
|
import ModalShortcuts from './ModalShortcuts'
|
||||||
|
import ModalSurvey from './ModalSurvey'
|
||||||
import ModalDebug from './ModalDebug'
|
import ModalDebug from './ModalDebug'
|
||||||
|
|
||||||
import {downloadGlyphsMetadata, downloadSpriteMetadata} from '../libs/metadata'
|
import {downloadGlyphsMetadata, downloadSpriteMetadata} from '../libs/metadata'
|
||||||
import style from '../libs/style'
|
import style from '../libs/style'
|
||||||
|
import { initialStyleUrl, loadStyleUrl, removeStyleQuerystring } from '../libs/urlopen'
|
||||||
import { undoMessages, redoMessages } from '../libs/diffmessage'
|
import { undoMessages, redoMessages } from '../libs/diffmessage'
|
||||||
import { createStyleStore, type IStyleStore } from '../libs/store/style-store-factory'
|
import { StyleStore } from '../libs/stylestore'
|
||||||
|
import { ApiStyleStore } from '../libs/apistore'
|
||||||
import { RevisionStore } from '../libs/revisions'
|
import { RevisionStore } from '../libs/revisions'
|
||||||
import LayerWatcher from '../libs/layerwatcher'
|
import LayerWatcher from '../libs/layerwatcher'
|
||||||
import tokens from '../config/tokens.json'
|
import tokens from '../config/tokens.json'
|
||||||
import isEqual from 'lodash.isequal'
|
import isEqual from 'lodash.isequal'
|
||||||
|
import Debug from '../libs/debug'
|
||||||
|
import { SortEnd } from 'react-sortable-hoc';
|
||||||
import { MapOptions } from 'maplibre-gl';
|
import { MapOptions } from 'maplibre-gl';
|
||||||
import { OnStyleChangedOpts, StyleSpecificationWithId } from '../libs/definitions'
|
|
||||||
|
|
||||||
// Buffer must be defined globally for @maplibre/maplibre-gl-style-spec validate() function to succeed.
|
// Buffer must be defined globally for @maplibre/maplibre-gl-style-spec validate() function to succeed.
|
||||||
window.Buffer = buffer.Buffer;
|
window.Buffer = buffer.Buffer;
|
||||||
@@ -43,7 +48,6 @@ function setFetchAccessToken(url: string, mapStyle: StyleSpecification) {
|
|||||||
const matchesTilehosting = url.match(/\.tilehosting\.com/);
|
const matchesTilehosting = url.match(/\.tilehosting\.com/);
|
||||||
const matchesMaptiler = url.match(/\.maptiler\.com/);
|
const matchesMaptiler = url.match(/\.maptiler\.com/);
|
||||||
const matchesThunderforest = url.match(/\.thunderforest\.com/);
|
const matchesThunderforest = url.match(/\.thunderforest\.com/);
|
||||||
const matchesLocationIQ = url.match(/\.locationiq\.com/);
|
|
||||||
if (matchesTilehosting || matchesMaptiler) {
|
if (matchesTilehosting || matchesMaptiler) {
|
||||||
const accessToken = style.getAccessToken("openmaptiles", mapStyle, {allowFallback: true})
|
const accessToken = style.getAccessToken("openmaptiles", mapStyle, {allowFallback: true})
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
@@ -56,12 +60,6 @@ function setFetchAccessToken(url: string, mapStyle: StyleSpecification) {
|
|||||||
return url.replace('{key}', accessToken)
|
return url.replace('{key}', accessToken)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (matchesLocationIQ) {
|
|
||||||
const accessToken = style.getAccessToken("locationiq", mapStyle, {allowFallback: true})
|
|
||||||
if (accessToken) {
|
|
||||||
return url.replace('{key}', accessToken)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
else {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
@@ -80,6 +78,12 @@ function updateRootSpec(spec: any, fieldName: string, newValues: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OnStyleChangedOpts = {
|
||||||
|
save?: boolean
|
||||||
|
addRevision?: boolean
|
||||||
|
initialLoad?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
type MappedErrors = {
|
type MappedErrors = {
|
||||||
message: string
|
message: string
|
||||||
parsed?: {
|
parsed?: {
|
||||||
@@ -95,11 +99,11 @@ type MappedErrors = {
|
|||||||
type AppState = {
|
type AppState = {
|
||||||
errors: MappedErrors[],
|
errors: MappedErrors[],
|
||||||
infos: string[],
|
infos: string[],
|
||||||
mapStyle: StyleSpecificationWithId,
|
mapStyle: StyleSpecification & {id: string},
|
||||||
dirtyMapStyle?: StyleSpecification,
|
dirtyMapStyle?: StyleSpecification,
|
||||||
selectedLayerIndex: number,
|
selectedLayerIndex: number,
|
||||||
selectedLayerOriginalId?: string,
|
selectedLayerOriginalId?: string,
|
||||||
sources: {[key: string]: SourceSpecification & {layers: string[]} },
|
sources: {[key: string]: SourceSpecification},
|
||||||
vectorLayers: {},
|
vectorLayers: {},
|
||||||
spec: any,
|
spec: any,
|
||||||
mapView: {
|
mapView: {
|
||||||
@@ -124,63 +128,34 @@ type AppState = {
|
|||||||
open: boolean
|
open: boolean
|
||||||
shortcuts: boolean
|
shortcuts: boolean
|
||||||
export: boolean
|
export: boolean
|
||||||
|
survey: boolean
|
||||||
debug: boolean
|
debug: boolean
|
||||||
}
|
}
|
||||||
fileHandle: FileSystemFileHandle | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class App extends React.Component<any, AppState> {
|
export default class App extends React.Component<any, AppState> {
|
||||||
revisionStore: RevisionStore;
|
revisionStore: RevisionStore;
|
||||||
styleStore: IStyleStore | null = null;
|
styleStore: StyleStore | ApiStyleStore;
|
||||||
layerWatcher: LayerWatcher;
|
layerWatcher: LayerWatcher;
|
||||||
|
shortcutEl: ModalShortcuts | null = null;
|
||||||
|
|
||||||
constructor(props: any) {
|
constructor(props: any) {
|
||||||
super(props)
|
super(props)
|
||||||
|
autoBind(this);
|
||||||
|
|
||||||
this.revisionStore = new RevisionStore();
|
this.revisionStore = new RevisionStore()
|
||||||
this.configureKeyboardShortcuts();
|
const params = new URLSearchParams(window.location.search.substring(1))
|
||||||
|
let port = params.get("localport")
|
||||||
this.state = {
|
if (port == null && (window.location.port !== "80" && window.location.port !== "443")) {
|
||||||
errors: [],
|
port = window.location.port
|
||||||
infos: [],
|
|
||||||
mapStyle: style.emptyStyle,
|
|
||||||
selectedLayerIndex: 0,
|
|
||||||
sources: {},
|
|
||||||
vectorLayers: {},
|
|
||||||
mapState: "map",
|
|
||||||
spec: latest,
|
|
||||||
mapView: {
|
|
||||||
zoom: 0,
|
|
||||||
center: {
|
|
||||||
lng: 0,
|
|
||||||
lat: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
isOpen: {
|
|
||||||
settings: false,
|
|
||||||
sources: false,
|
|
||||||
open: false,
|
|
||||||
shortcuts: false,
|
|
||||||
export: false,
|
|
||||||
debug: false,
|
|
||||||
},
|
|
||||||
maplibreGlDebugOptions: {
|
|
||||||
showTileBoundaries: false,
|
|
||||||
showCollisionBoxes: false,
|
|
||||||
showOverdrawInspector: false,
|
|
||||||
},
|
|
||||||
openlayersDebugOptions: {
|
|
||||||
debugToolbox: false,
|
|
||||||
},
|
|
||||||
fileHandle: null,
|
|
||||||
}
|
}
|
||||||
|
this.styleStore = new ApiStyleStore({
|
||||||
this.layerWatcher = new LayerWatcher({
|
onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, {save: false}),
|
||||||
onVectorLayersChange: v => this.setState({ vectorLayers: v })
|
port: port,
|
||||||
|
host: params.get("localhost")
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
configureKeyboardShortcuts = () => {
|
|
||||||
const shortcuts = [
|
const shortcuts = [
|
||||||
{
|
{
|
||||||
key: "?",
|
key: "?",
|
||||||
@@ -250,6 +225,74 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const styleUrl = initialStyleUrl()
|
||||||
|
if(styleUrl && window.confirm("Load style from URL: " + styleUrl + " and discard current changes?")) {
|
||||||
|
this.styleStore = new StyleStore()
|
||||||
|
loadStyleUrl(styleUrl, mapStyle => this.onStyleChanged(mapStyle))
|
||||||
|
removeStyleQuerystring()
|
||||||
|
} else {
|
||||||
|
if(styleUrl) {
|
||||||
|
removeStyleQuerystring()
|
||||||
|
}
|
||||||
|
this.styleStore.init(err => {
|
||||||
|
if(err) {
|
||||||
|
console.log('Falling back to local storage for storing styles')
|
||||||
|
this.styleStore = new StyleStore()
|
||||||
|
}
|
||||||
|
this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle, {initialLoad: true}))
|
||||||
|
|
||||||
|
if(Debug.enabled()) {
|
||||||
|
Debug.set("maputnik", "styleStore", this.styleStore);
|
||||||
|
Debug.set("maputnik", "revisionStore", this.revisionStore);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Debug.enabled()) {
|
||||||
|
Debug.set("maputnik", "revisionStore", this.revisionStore);
|
||||||
|
Debug.set("maputnik", "styleStore", this.styleStore);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
errors: [],
|
||||||
|
infos: [],
|
||||||
|
mapStyle: style.emptyStyle,
|
||||||
|
selectedLayerIndex: 0,
|
||||||
|
sources: {},
|
||||||
|
vectorLayers: {},
|
||||||
|
mapState: "map",
|
||||||
|
spec: latest,
|
||||||
|
mapView: {
|
||||||
|
zoom: 0,
|
||||||
|
center: {
|
||||||
|
lng: 0,
|
||||||
|
lat: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isOpen: {
|
||||||
|
settings: false,
|
||||||
|
sources: false,
|
||||||
|
open: false,
|
||||||
|
shortcuts: false,
|
||||||
|
export: false,
|
||||||
|
// TODO: Disabled for now, this should be opened on the Nth visit to the editor
|
||||||
|
survey: false,
|
||||||
|
debug: false,
|
||||||
|
},
|
||||||
|
maplibreGlDebugOptions: {
|
||||||
|
showTileBoundaries: false,
|
||||||
|
showCollisionBoxes: false,
|
||||||
|
showOverdrawInspector: false,
|
||||||
|
},
|
||||||
|
openlayersDebugOptions: {
|
||||||
|
debugToolbox: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
this.layerWatcher = new LayerWatcher({
|
||||||
|
onVectorLayersChange: v => this.setState({ vectorLayers: v })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyPress = (e: KeyboardEvent) => {
|
handleKeyPress = (e: KeyboardEvent) => {
|
||||||
@@ -275,8 +318,7 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
componentDidMount() {
|
||||||
this.styleStore = await createStyleStore((mapStyle, opts) => this.onStyleChanged(mapStyle, opts));
|
|
||||||
window.addEventListener("keydown", this.handleKeyPress);
|
window.addEventListener("keydown", this.handleKeyPress);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,8 +326,8 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
window.removeEventListener("keydown", this.handleKeyPress);
|
window.removeEventListener("keydown", this.handleKeyPress);
|
||||||
}
|
}
|
||||||
|
|
||||||
saveStyle(snapshotStyle: StyleSpecificationWithId) {
|
saveStyle(snapshotStyle: StyleSpecification & {id: string}) {
|
||||||
this.styleStore?.save(snapshotStyle)
|
this.styleStore.save(snapshotStyle)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFonts(urlTemplate: string) {
|
updateFonts(urlTemplate: string) {
|
||||||
@@ -322,11 +364,10 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
[property]: value
|
[property]: value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.onStyleChanged(changedStyle)
|
this.onStyleChanged(changedStyle)
|
||||||
}
|
}
|
||||||
|
|
||||||
onStyleChanged = (newStyle: StyleSpecificationWithId, opts: OnStyleChangedOpts={}): void => {
|
onStyleChanged = (newStyle: StyleSpecification & {id: string}, opts: OnStyleChangedOpts={}) => {
|
||||||
opts = {
|
opts = {
|
||||||
save: true,
|
save: true,
|
||||||
addRevision: true,
|
addRevision: true,
|
||||||
@@ -334,29 +375,13 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
...opts,
|
...opts,
|
||||||
};
|
};
|
||||||
|
|
||||||
// For the style object, find the urls that has "{key}" and insert the correct API keys
|
|
||||||
// Without this, going from e.g. MapTiler to OpenLayers and back will lose the maptlier key.
|
|
||||||
|
|
||||||
if (newStyle.glyphs && typeof newStyle.glyphs === 'string') {
|
|
||||||
newStyle.glyphs = setFetchAccessToken(newStyle.glyphs, newStyle);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newStyle.sprite && typeof newStyle.sprite === 'string') {
|
|
||||||
newStyle.sprite = setFetchAccessToken(newStyle.sprite, newStyle);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [_sourceId, source] of Object.entries(newStyle.sources)) {
|
|
||||||
if (source && 'url' in source && typeof source.url === 'string') {
|
|
||||||
source.url = setFetchAccessToken(source.url, newStyle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (opts.initialLoad) {
|
if (opts.initialLoad) {
|
||||||
this.getInitialStateFromUrl(newStyle);
|
this.getInitialStateFromUrl(newStyle);
|
||||||
}
|
}
|
||||||
|
|
||||||
const errors: ValidationError[] = validateStyleMin(newStyle) || [];
|
// This "any" can be removed in latest version of maplibre where maplibre re-exported types from style-spec
|
||||||
|
const errors = validate(newStyle as any, latest) || [];
|
||||||
|
|
||||||
// The validate function doesn't give us errors for duplicate error with
|
// The validate function doesn't give us errors for duplicate error with
|
||||||
// empty string for layer.id, manually deal with that here.
|
// empty string for layer.id, manually deal with that here.
|
||||||
const layerErrors: (Error | ValidationError)[] = [];
|
const layerErrors: (Error | ValidationError)[] = [];
|
||||||
@@ -435,7 +460,7 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
dirtyMapStyle = cloneDeep(newStyle);
|
dirtyMapStyle = cloneDeep(newStyle);
|
||||||
|
|
||||||
for (const error of errors) {
|
errors.forEach(error => {
|
||||||
const {message} = error;
|
const {message} = error;
|
||||||
if (message) {
|
if (message) {
|
||||||
try {
|
try {
|
||||||
@@ -445,10 +470,10 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
unset(dirtyMapStyle, unsetPath);
|
unset(dirtyMapStyle, unsetPath);
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.warn(message + " " + err);
|
console.warn(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
|
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
|
||||||
@@ -462,7 +487,7 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
this.revisionStore.addRevision(newStyle);
|
this.revisionStore.addRevision(newStyle);
|
||||||
}
|
}
|
||||||
if (opts.save) {
|
if (opts.save) {
|
||||||
this.saveStyle(newStyle);
|
this.saveStyle(newStyle as StyleSpecification & {id: string});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -495,7 +520,7 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onMoveLayer = (move: {oldIndex: number; newIndex: number}) => {
|
onMoveLayer = (move: SortEnd) => {
|
||||||
let { oldIndex, newIndex } = move;
|
let { oldIndex, newIndex } = move;
|
||||||
let layers = this.state.mapStyle.layers;
|
let layers = this.state.mapStyle.layers;
|
||||||
oldIndex = clamp(oldIndex, 0, layers.length-1);
|
oldIndex = clamp(oldIndex, 0, layers.length-1);
|
||||||
@@ -575,7 +600,7 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
}, this.setStateInUrl);
|
}, this.setStateInUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
setDefaultValues = (styleObj: StyleSpecificationWithId) => {
|
setDefaultValues = (styleObj: StyleSpecification & {id: string}) => {
|
||||||
const metadata: {[key: string]: string} = styleObj.metadata || {} as any
|
const metadata: {[key: string]: string} = styleObj.metadata || {} as any
|
||||||
if(metadata['maputnik:renderer'] === undefined) {
|
if(metadata['maputnik:renderer'] === undefined) {
|
||||||
const changedStyle = {
|
const changedStyle = {
|
||||||
@@ -591,28 +616,26 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
openStyle = (styleObj: StyleSpecificationWithId, fileHandle: FileSystemFileHandle | null) => {
|
openStyle = (styleObj: StyleSpecification & {id: string}) => {
|
||||||
this.setState({fileHandle: fileHandle});
|
|
||||||
styleObj = this.setDefaultValues(styleObj)
|
styleObj = this.setDefaultValues(styleObj)
|
||||||
this.onStyleChanged(styleObj)
|
this.onStyleChanged(styleObj)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchSources() {
|
fetchSources() {
|
||||||
const sourceList: {[key: string]: SourceSpecification & {layers: string[]}} = {};
|
const sourceList: {[key: string]: any} = {};
|
||||||
for(const key of Object.keys(this.state.mapStyle.sources)) {
|
|
||||||
const source = this.state.mapStyle.sources[key];
|
for(const [key, val] of Object.entries(this.state.mapStyle.sources)) {
|
||||||
if(source.type !== "vector" || !('url' in source)) {
|
if(
|
||||||
sourceList[key] = this.state.sources[key] || {...this.state.mapStyle.sources[key]};
|
!Object.prototype.hasOwnProperty.call(this.state.sources, key) &&
|
||||||
if (sourceList[key].layers === undefined) {
|
val.type === "vector" &&
|
||||||
sourceList[key].layers = [];
|
Object.prototype.hasOwnProperty.call(val, "url")
|
||||||
}
|
) {
|
||||||
} else {
|
|
||||||
sourceList[key] = {
|
sourceList[key] = {
|
||||||
type: source.type,
|
type: val.type,
|
||||||
layers: []
|
layers: []
|
||||||
};
|
};
|
||||||
|
|
||||||
let url = source.url;
|
let url = val.url;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
url = setFetchAccessToken(url!, this.state.mapStyle)
|
url = setFetchAccessToken(url!, this.state.mapStyle)
|
||||||
@@ -620,33 +643,41 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
console.warn("Failed to setFetchAccessToken: ", err);
|
console.warn("Failed to setFetchAccessToken: ", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
const setVectorLayers = (json:any) => {
|
fetch(url!, {
|
||||||
if(!Object.prototype.hasOwnProperty.call(json, "vector_layers")) {
|
mode: 'cors',
|
||||||
return;
|
})
|
||||||
}
|
.then(response => response.json())
|
||||||
|
.then(json => {
|
||||||
|
|
||||||
for(const layer of json.vector_layers) {
|
if(!Object.prototype.hasOwnProperty.call(json, "vector_layers")) {
|
||||||
sourceList[key].layers.push(layer.id)
|
return;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
// Create new objects before setState
|
||||||
if (url!.startsWith("pmtiles://")) {
|
const sources = Object.assign({}, {
|
||||||
const json = await (new PMTiles(url!.substring(10))).getTileJson("");
|
[key]: this.state.sources[key],
|
||||||
setVectorLayers(json);
|
});
|
||||||
} else {
|
|
||||||
const response = await fetch(url!, { mode: 'cors' });
|
for(const layer of json.vector_layers) {
|
||||||
const json = await response.json();
|
(sources[key] as any).layers.push(layer.id)
|
||||||
setVectorLayers(json);
|
}
|
||||||
}
|
|
||||||
} catch(err) {
|
console.debug("Updating source: "+key);
|
||||||
console.error(`Failed to process source for url: '${url}', ${err}`);
|
this.setState({
|
||||||
}
|
sources: sources
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Failed to process sources for '%s'", url, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sourceList[key] = this.state.sources[key] || this.state.mapStyle.sources[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!isEqual(this.state.sources, sourceList)) {
|
if(!isEqual(this.state.sources, sourceList)) {
|
||||||
console.debug("Setting sources", sourceList);
|
console.debug("Setting sources");
|
||||||
this.setState({
|
this.setState({
|
||||||
sources: sourceList
|
sources: sourceList
|
||||||
})
|
})
|
||||||
@@ -699,7 +730,6 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
onLayerSelect={this.onLayerSelect}
|
onLayerSelect={this.onLayerSelect}
|
||||||
/>
|
/>
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
mapElement = <MapMaplibreGl {...mapProps}
|
mapElement = <MapMaplibreGl {...mapProps}
|
||||||
onChange={this.onMapChange}
|
onChange={this.onMapChange}
|
||||||
options={this.state.maplibreGlDebugOptions}
|
options={this.state.maplibreGlDebugOptions}
|
||||||
@@ -753,7 +783,6 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
getInitialStateFromUrl = (mapStyle: StyleSpecification) => {
|
getInitialStateFromUrl = (mapStyle: StyleSpecification) => {
|
||||||
const url = new URL(location.href);
|
const url = new URL(location.href);
|
||||||
const modalParam = url.searchParams.get("modal");
|
const modalParam = url.searchParams.get("modal");
|
||||||
|
|
||||||
if (modalParam && modalParam !== "") {
|
if (modalParam && modalParam !== "") {
|
||||||
const modals = modalParam.split(",");
|
const modals = modalParam.split(",");
|
||||||
const modalObj: {[key: string]: boolean} = {};
|
const modalObj: {[key: string]: boolean} = {};
|
||||||
@@ -811,6 +840,10 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setModal(modalName: keyof AppState["isOpen"], value: boolean) {
|
setModal(modalName: keyof AppState["isOpen"], value: boolean) {
|
||||||
|
if(modalName === 'survey' && value === false) {
|
||||||
|
localStorage.setItem('survey', '');
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
isOpen: {
|
isOpen: {
|
||||||
...this.state.isOpen,
|
...this.state.isOpen,
|
||||||
@@ -823,10 +856,6 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
this.setModal(modalName, !this.state.isOpen[modalName]);
|
this.setModal(modalName, !this.state.isOpen[modalName]);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSetFileHandle = (fileHandle: FileSystemFileHandle | null) => {
|
|
||||||
this.setState({ fileHandle });
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeOpenlayersDebug = (key: keyof AppState["openlayersDebugOptions"], value: boolean) => {
|
onChangeOpenlayersDebug = (key: keyof AppState["openlayersDebugOptions"], value: boolean) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
openlayersDebugOptions: {
|
openlayersDebugOptions: {
|
||||||
@@ -914,6 +943,7 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
mapView={this.state.mapView}
|
mapView={this.state.mapView}
|
||||||
/>
|
/>
|
||||||
<ModalShortcuts
|
<ModalShortcuts
|
||||||
|
ref={(el) => this.shortcutEl = el}
|
||||||
isOpen={this.state.isOpen.shortcuts}
|
isOpen={this.state.isOpen.shortcuts}
|
||||||
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')}
|
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')}
|
||||||
/>
|
/>
|
||||||
@@ -929,14 +959,11 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
onStyleChanged={this.onStyleChanged}
|
onStyleChanged={this.onStyleChanged}
|
||||||
isOpen={this.state.isOpen.export}
|
isOpen={this.state.isOpen.export}
|
||||||
onOpenToggle={this.toggleModal.bind(this, 'export')}
|
onOpenToggle={this.toggleModal.bind(this, 'export')}
|
||||||
fileHandle={this.state.fileHandle}
|
|
||||||
onSetFileHandle={this.onSetFileHandle}
|
|
||||||
/>
|
/>
|
||||||
<ModalOpen
|
<ModalOpen
|
||||||
isOpen={this.state.isOpen.open}
|
isOpen={this.state.isOpen.open}
|
||||||
onStyleOpen={this.openStyle}
|
onStyleOpen={this.openStyle}
|
||||||
onOpenToggle={this.toggleModal.bind(this, 'open')}
|
onOpenToggle={this.toggleModal.bind(this, 'open')}
|
||||||
fileHandle={this.state.fileHandle}
|
|
||||||
/>
|
/>
|
||||||
<ModalSources
|
<ModalSources
|
||||||
mapStyle={this.state.mapStyle}
|
mapStyle={this.state.mapStyle}
|
||||||
@@ -944,6 +971,10 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
isOpen={this.state.isOpen.sources}
|
isOpen={this.state.isOpen.sources}
|
||||||
onOpenToggle={this.toggleModal.bind(this, 'sources')}
|
onOpenToggle={this.toggleModal.bind(this, 'sources')}
|
||||||
/>
|
/>
|
||||||
|
<ModalSurvey
|
||||||
|
isOpen={this.state.isOpen.survey}
|
||||||
|
onOpenToggle={this.toggleModal.bind(this, 'survey')}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
return <AppLayout
|
return <AppLayout
|
||||||
|
|||||||
@@ -1,45 +1,46 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
import ScrollContainer from './ScrollContainer'
|
import ScrollContainer from './ScrollContainer'
|
||||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
|
||||||
import { IconContext } from 'react-icons';
|
|
||||||
|
|
||||||
type AppLayoutInternalProps = {
|
type AppLayoutProps = {
|
||||||
toolbar: React.ReactElement
|
toolbar: React.ReactElement
|
||||||
layerList: React.ReactElement
|
layerList: React.ReactElement
|
||||||
layerEditor?: React.ReactElement
|
layerEditor?: React.ReactElement
|
||||||
map: React.ReactElement
|
map: React.ReactElement
|
||||||
bottom?: React.ReactElement
|
bottom?: React.ReactElement
|
||||||
modals?: React.ReactNode
|
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() {
|
render() {
|
||||||
document.body.dir = this.props.i18n.dir();
|
return <div className="maputnik-layout">
|
||||||
|
{this.props.toolbar}
|
||||||
return <IconContext.Provider value={{size: '14px'}}>
|
<div className="maputnik-layout-list">
|
||||||
<div className="maputnik-layout">
|
{this.props.layerList}
|
||||||
{this.props.toolbar}
|
|
||||||
<div className="maputnik-layout-main">
|
|
||||||
<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}
|
|
||||||
</div>
|
</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,24 +1,23 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {formatLayerId} from '../libs/format';
|
import {formatLayerId} from '../libs/format';
|
||||||
import {LayerSpecification, StyleSpecification} from 'maplibre-gl';
|
import {LayerSpecification, StyleSpecification} from 'maplibre-gl';
|
||||||
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
type AppMessagePanelInternalProps = {
|
type AppMessagePanelProps = {
|
||||||
errors?: unknown[]
|
errors?: unknown[]
|
||||||
infos?: string[]
|
infos?: unknown[]
|
||||||
mapStyle?: StyleSpecification
|
mapStyle?: StyleSpecification
|
||||||
onLayerSelect?(...args: unknown[]): unknown
|
onLayerSelect?(...args: unknown[]): unknown
|
||||||
currentLayer?: LayerSpecification
|
currentLayer?: LayerSpecification
|
||||||
selectedLayerIndex?: number
|
selectedLayerIndex?: number
|
||||||
} & WithTranslation;
|
};
|
||||||
|
|
||||||
class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalProps> {
|
export default class AppMessagePanel extends React.Component<AppMessagePanelProps> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
onLayerSelect: () => {},
|
onLayerSelect: () => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {t, selectedLayerIndex} = this.props;
|
const {selectedLayerIndex} = this.props;
|
||||||
const errors = this.props.errors?.map((error: any, idx) => {
|
const errors = this.props.errors?.map((error: any, idx) => {
|
||||||
let content;
|
let content;
|
||||||
if (error.parsed && error.parsed.type === "layer") {
|
if (error.parsed && error.parsed.type === "layer") {
|
||||||
@@ -26,9 +25,7 @@ class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalPro
|
|||||||
const layerId = this.props.mapStyle?.layers[parsed.data.index].id;
|
const layerId = this.props.mapStyle?.layers[parsed.data.index].id;
|
||||||
content = (
|
content = (
|
||||||
<>
|
<>
|
||||||
<Trans t={t}>
|
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
|
||||||
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
|
|
||||||
</Trans>
|
|
||||||
{selectedLayerIndex !== parsed.data.index &&
|
{selectedLayerIndex !== parsed.data.index &&
|
||||||
<>
|
<>
|
||||||
—
|
—
|
||||||
@@ -36,7 +33,7 @@ class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalPro
|
|||||||
className="maputnik-message-panel__switch-button"
|
className="maputnik-message-panel__switch-button"
|
||||||
onClick={() => this.props.onLayerSelect!(parsed.data.index)}
|
onClick={() => this.props.onLayerSelect!(parsed.data.index)}
|
||||||
>
|
>
|
||||||
{t("switch to layer")}
|
switch to layer
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
@@ -62,5 +59,3 @@ class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalPro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppMessagePanel = withTranslation()(AppMessagePanelInternal);
|
|
||||||
export default AppMessagePanel;
|
|
||||||
|
|||||||
@@ -2,21 +2,9 @@ import React from 'react'
|
|||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import {detect} from 'detect-browser';
|
import {detect} from 'detect-browser';
|
||||||
|
|
||||||
import {
|
import {MdFileDownload, MdOpenInBrowser, MdSettings, MdLayers, MdHelpOutline, MdFindInPage, MdAssignmentTurnedIn} from 'react-icons/md'
|
||||||
MdOpenInBrowser,
|
|
||||||
MdSettings,
|
|
||||||
MdLayers,
|
|
||||||
MdHelpOutline,
|
|
||||||
MdFindInPage,
|
|
||||||
MdLanguage,
|
|
||||||
MdSave
|
|
||||||
} from 'react-icons/md'
|
|
||||||
import pkgJson from '../../package.json'
|
import pkgJson from '../../package.json'
|
||||||
//@ts-ignore
|
|
||||||
import maputnikLogo from 'maputnik-design/logos/logo-color.svg?inline'
|
|
||||||
import { withTranslation, WithTranslation } from 'react-i18next';
|
|
||||||
import { supportedLanguages } from '../i18n';
|
|
||||||
import type { OnStyleChangedCallback } from '../libs/definitions';
|
|
||||||
|
|
||||||
// This is required because of <https://stackoverflow.com/a/49846426>, there isn't another way to detect support that I'm aware of.
|
// 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 browser = detect();
|
||||||
@@ -55,6 +43,29 @@ class ToolbarLink extends React.Component<ToolbarLinkProps> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ToolbarLinkHighlightedProps = {
|
||||||
|
className?: string
|
||||||
|
children?: React.ReactNode
|
||||||
|
href?: string
|
||||||
|
onToggleModal?(...args: unknown[]): unknown
|
||||||
|
};
|
||||||
|
|
||||||
|
class ToolbarLinkHighlighted extends React.Component<ToolbarLinkHighlightedProps> {
|
||||||
|
render() {
|
||||||
|
return <a
|
||||||
|
className={classnames('maputnik-toolbar-link', "maputnik-toolbar-link--highlighted", this.props.className)}
|
||||||
|
href={this.props.href}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
data-wd-key="toolbar:link-highlighted"
|
||||||
|
>
|
||||||
|
<span className="maputnik-toolbar-link-wrapper">
|
||||||
|
{this.props.children}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type ToolbarSelectProps = {
|
type ToolbarSelectProps = {
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
wdKey?: string
|
wdKey?: string
|
||||||
@@ -91,12 +102,12 @@ class ToolbarAction extends React.Component<ToolbarActionProps> {
|
|||||||
|
|
||||||
export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deuteranopia" | "filter-protanopia" | "filter-tritanopia";
|
export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deuteranopia" | "filter-protanopia" | "filter-tritanopia";
|
||||||
|
|
||||||
type AppToolbarInternalProps = {
|
type AppToolbarProps = {
|
||||||
mapStyle: object
|
mapStyle: object
|
||||||
inspectModeEnabled: boolean
|
inspectModeEnabled: boolean
|
||||||
onStyleChanged: OnStyleChangedCallback
|
onStyleChanged(...args: unknown[]): unknown
|
||||||
// A new style has been uploaded
|
// A new style has been uploaded
|
||||||
onStyleOpen: OnStyleChangedCallback
|
onStyleOpen(...args: unknown[]): unknown
|
||||||
// A dict of source id's and the available source layers
|
// A dict of source id's and the available source layers
|
||||||
sources: object
|
sources: object
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
@@ -104,9 +115,9 @@ type AppToolbarInternalProps = {
|
|||||||
onSetMapState(mapState: MapState): unknown
|
onSetMapState(mapState: MapState): unknown
|
||||||
mapState?: MapState
|
mapState?: MapState
|
||||||
renderer?: string
|
renderer?: string
|
||||||
} & WithTranslation;
|
};
|
||||||
|
|
||||||
class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
export default class AppToolbar extends React.Component<AppToolbarProps> {
|
||||||
state = {
|
state = {
|
||||||
isOpen: {
|
isOpen: {
|
||||||
settings: false,
|
settings: false,
|
||||||
@@ -121,10 +132,6 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
|||||||
this.props.onSetMapState(val);
|
this.props.onSetMapState(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLanguageChange(val: string) {
|
|
||||||
this.props.i18n.changeLanguage(val);
|
|
||||||
}
|
|
||||||
|
|
||||||
onSkip = (target: string) => {
|
onSkip = (target: string) => {
|
||||||
if (target === "map") {
|
if (target === "map") {
|
||||||
(document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus();
|
(document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus();
|
||||||
@@ -136,41 +143,40 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const t = this.props.t;
|
|
||||||
const views = [
|
const views = [
|
||||||
{
|
{
|
||||||
id: "map",
|
id: "map",
|
||||||
group: "general",
|
group: "general",
|
||||||
title: t("Map"),
|
title: "Map",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "inspect",
|
id: "inspect",
|
||||||
group: "general",
|
group: "general",
|
||||||
title: t("Inspect"),
|
title: "Inspect",
|
||||||
disabled: this.props.renderer === 'ol',
|
disabled: this.props.renderer === 'ol',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "filter-deuteranopia",
|
id: "filter-deuteranopia",
|
||||||
group: "color-accessibility",
|
group: "color-accessibility",
|
||||||
title: t("Deuteranopia filter"),
|
title: "Deuteranopia filter",
|
||||||
disabled: !colorAccessibilityFiltersEnabled,
|
disabled: !colorAccessibilityFiltersEnabled,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "filter-protanopia",
|
id: "filter-protanopia",
|
||||||
group: "color-accessibility",
|
group: "color-accessibility",
|
||||||
title: t("Protanopia filter"),
|
title: "Protanopia filter",
|
||||||
disabled: !colorAccessibilityFiltersEnabled,
|
disabled: !colorAccessibilityFiltersEnabled,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "filter-tritanopia",
|
id: "filter-tritanopia",
|
||||||
group: "color-accessibility",
|
group: "color-accessibility",
|
||||||
title: t("Tritanopia filter"),
|
title: "Tritanopia filter",
|
||||||
disabled: !colorAccessibilityFiltersEnabled,
|
disabled: !colorAccessibilityFiltersEnabled,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "filter-achromatopsia",
|
id: "filter-achromatopsia",
|
||||||
group: "color-accessibility",
|
group: "color-accessibility",
|
||||||
title: t("Achromatopsia filter"),
|
title: "Achromatopsia filter",
|
||||||
disabled: !colorAccessibilityFiltersEnabled,
|
disabled: !colorAccessibilityFiltersEnabled,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -190,29 +196,29 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
|||||||
className="maputnik-toolbar-skip"
|
className="maputnik-toolbar-skip"
|
||||||
onClick={_e => this.onSkip("layer-list")}
|
onClick={_e => this.onSkip("layer-list")}
|
||||||
>
|
>
|
||||||
{t("Layers list")}
|
Layers list
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
data-wd-key="root:skip:layer-editor"
|
data-wd-key="root:skip:layer-editor"
|
||||||
className="maputnik-toolbar-skip"
|
className="maputnik-toolbar-skip"
|
||||||
onClick={_e => this.onSkip("layer-editor")}
|
onClick={_e => this.onSkip("layer-editor")}
|
||||||
>
|
>
|
||||||
{t("Layer editor")}
|
Layer editor
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
data-wd-key="root:skip:map-view"
|
data-wd-key="root:skip:map-view"
|
||||||
className="maputnik-toolbar-skip"
|
className="maputnik-toolbar-skip"
|
||||||
onClick={_e => this.onSkip("map")}
|
onClick={_e => this.onSkip("map")}
|
||||||
>
|
>
|
||||||
{t("Map view")}
|
Map view
|
||||||
</button>
|
</button>
|
||||||
<a
|
<a
|
||||||
className="maputnik-toolbar-logo"
|
className="maputnik-toolbar-logo"
|
||||||
target="blank"
|
target="blank"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
href="https://github.com/maplibre/maputnik"
|
href="https://github.com/maputnik/editor"
|
||||||
>
|
>
|
||||||
<img src={maputnikLogo} alt={t("Maputnik on GitHub")} />
|
<img src="node_modules/maputnik-design/logos/logo-color.svg" />
|
||||||
<h1>
|
<h1>
|
||||||
<span className="maputnik-toolbar-name">{pkgJson.name}</span>
|
<span className="maputnik-toolbar-name">{pkgJson.name}</span>
|
||||||
<span className="maputnik-toolbar-version">v{pkgJson.version}</span>
|
<span className="maputnik-toolbar-version">v{pkgJson.version}</span>
|
||||||
@@ -222,24 +228,24 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
|||||||
<div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar">
|
<div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar">
|
||||||
<ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}>
|
<ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}>
|
||||||
<MdOpenInBrowser />
|
<MdOpenInBrowser />
|
||||||
<IconText>{t("Open")}</IconText>
|
<IconText>Open</IconText>
|
||||||
</ToolbarAction>
|
</ToolbarAction>
|
||||||
<ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, 'export')}>
|
<ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, 'export')}>
|
||||||
<MdSave />
|
<MdFileDownload />
|
||||||
<IconText>{t("Save")}</IconText>
|
<IconText>Export</IconText>
|
||||||
</ToolbarAction>
|
</ToolbarAction>
|
||||||
<ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, 'sources')}>
|
<ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, 'sources')}>
|
||||||
<MdLayers />
|
<MdLayers />
|
||||||
<IconText>{t("Data Sources")}</IconText>
|
<IconText>Data Sources</IconText>
|
||||||
</ToolbarAction>
|
</ToolbarAction>
|
||||||
<ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}>
|
<ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}>
|
||||||
<MdSettings />
|
<MdSettings />
|
||||||
<IconText>{t("Style Settings")}</IconText>
|
<IconText>Style Settings</IconText>
|
||||||
</ToolbarAction>
|
</ToolbarAction>
|
||||||
|
|
||||||
<ToolbarSelect wdKey="nav:inspect">
|
<ToolbarSelect wdKey="nav:inspect">
|
||||||
<MdFindInPage />
|
<MdFindInPage />
|
||||||
<IconText>{t("View")}
|
<label>View
|
||||||
<select
|
<select
|
||||||
className="maputnik-select"
|
className="maputnik-select"
|
||||||
data-wd-key="maputnik-select"
|
data-wd-key="maputnik-select"
|
||||||
@@ -253,7 +259,7 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
|||||||
</option>
|
</option>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<optgroup label={t("Color accessibility")}>
|
<optgroup label="Color accessibility">
|
||||||
{views.filter(v => v.group === "color-accessibility").map((item) => {
|
{views.filter(v => v.group === "color-accessibility").map((item) => {
|
||||||
return (
|
return (
|
||||||
<option key={item.id} value={item.id} disabled={item.disabled}>
|
<option key={item.id} value={item.id} disabled={item.disabled}>
|
||||||
@@ -263,38 +269,19 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
|||||||
})}
|
})}
|
||||||
</optgroup>
|
</optgroup>
|
||||||
</select>
|
</select>
|
||||||
</IconText>
|
</label>
|
||||||
</ToolbarSelect>
|
</ToolbarSelect>
|
||||||
|
|
||||||
<ToolbarSelect wdKey="nav:language">
|
<ToolbarLink href={"https://github.com/maputnik/editor/wiki"}>
|
||||||
<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>
|
|
||||||
</ToolbarSelect>
|
|
||||||
|
|
||||||
<ToolbarLink href={"https://github.com/maplibre/maputnik/wiki"}>
|
|
||||||
<MdHelpOutline />
|
<MdHelpOutline />
|
||||||
<IconText>{t("Help")}</IconText>
|
<IconText>Help</IconText>
|
||||||
</ToolbarLink>
|
</ToolbarLink>
|
||||||
|
<ToolbarLinkHighlighted href={"https://gregorywolanski.typeform.com/to/cPgaSY"}>
|
||||||
|
<MdAssignmentTurnedIn />
|
||||||
|
<IconText>Take the Maputnik Survey</IconText>
|
||||||
|
</ToolbarLinkHighlighted>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppToolbar = withTranslation()(AppToolbarInternal);
|
|
||||||
export default AppToolbar;
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React, {PropsWithChildren, SyntheticEvent} from 'react'
|
import React, {SyntheticEvent} from 'react'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import FieldDocLabel from './FieldDocLabel'
|
import FieldDocLabel from './FieldDocLabel'
|
||||||
import Doc from './Doc'
|
import Doc from './Doc'
|
||||||
|
|
||||||
|
|
||||||
type BlockProps = PropsWithChildren & {
|
type BlockProps = {
|
||||||
"data-wd-key"?: string
|
"data-wd-key"?: string
|
||||||
label?: string
|
label?: string
|
||||||
action?: React.ReactElement
|
action?: React.ReactElement
|
||||||
@@ -56,9 +56,7 @@ export default class Block extends React.Component<BlockProps, BlockState> {
|
|||||||
if (event.nativeEvent.target.nodeName !== "INPUT" && !contains) {
|
if (event.nativeEvent.target.nodeName !== "INPUT" && !contains) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
if (event.nativeEvent.target.nodeName !== "A") {
|
event.preventDefault();
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -102,3 +100,4 @@ export default class Block extends React.Component<BlockProps, BlockState> {
|
|||||||
</label>
|
</label>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,3 +31,4 @@ export default class Collapse extends React.Component<CollapseProps> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,3 +16,4 @@ export default class Collapser extends React.Component<CollapserProps> {
|
|||||||
return this.props.isCollapsed ? <MdArrowDropUp style={iconStyle}/> : <MdArrowDropDown style={iconStyle} />
|
return this.props.isCollapsed ? <MdArrowDropUp style={iconStyle}/> : <MdArrowDropDown style={iconStyle} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ type DocProps = {
|
|||||||
'sdk-support'?: {
|
'sdk-support'?: {
|
||||||
[key: string]: typeof headers
|
[key: string]: typeof headers
|
||||||
}
|
}
|
||||||
docUrl?: string,
|
|
||||||
docUrlLinkText?: string
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -27,13 +25,13 @@ export default class Doc extends React.Component<DocProps> {
|
|||||||
render () {
|
render () {
|
||||||
const {fieldSpec} = this.props;
|
const {fieldSpec} = this.props;
|
||||||
|
|
||||||
const {doc, values, docUrl, docUrlLinkText} = fieldSpec;
|
const {doc, values} = fieldSpec;
|
||||||
const sdkSupport = fieldSpec['sdk-support'];
|
const sdkSupport = fieldSpec['sdk-support'];
|
||||||
|
|
||||||
const renderValues = (
|
const renderValues = (
|
||||||
!!values &&
|
!!values &&
|
||||||
// HACK: Currently we merge additional values into the style spec, so this is required
|
// HACK: Currently we merge additional values into the style spec, so this is required
|
||||||
// See <https://github.com/maplibre/maputnik/blob/main/src/components/PropertyGroup.jsx#L16>
|
// See <https://github.com/maputnik/editor/blob/main/src/components/PropertyGroup.jsx#L16>
|
||||||
!Array.isArray(values)
|
!Array.isArray(values)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -87,11 +85,6 @@ export default class Doc extends React.Component<DocProps> {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{docUrl && docUrlLinkText &&
|
|
||||||
<div className="SpecDoc__learn-more">
|
|
||||||
<a href={docUrl} target="_blank" rel="noreferrer">{docUrlLinkText}</a>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react'
|
||||||
import InputArray, { FieldArrayProps as InputArrayProps } from './InputArray'
|
import InputArray, { FieldArrayProps as InputArrayProps } from './InputArray'
|
||||||
import Fieldset from './Fieldset'
|
import Fieldset from './Fieldset'
|
||||||
|
|
||||||
@@ -8,12 +9,11 @@ type FieldArrayProps = InputArrayProps & {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const FieldArray: React.FC<FieldArrayProps> = (props) => {
|
export default class FieldArray extends React.Component<FieldArrayProps> {
|
||||||
return (
|
render() {
|
||||||
<Fieldset label={props.label} fieldSpec={props.fieldSpec}>
|
return <Fieldset label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||||
<InputArray {...props} />
|
<InputArray {...this.props} />
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldArray;
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import InputAutocomplete, { InputAutocompleteProps } from './InputAutocomplete'
|
import InputAutocomplete, { InputAutocompleteProps } from './InputAutocomplete'
|
||||||
|
|
||||||
@@ -7,12 +8,11 @@ type FieldAutocompleteProps = InputAutocompleteProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const FieldAutocomplete: React.FC<FieldAutocompleteProps> = (props) => {
|
export default class FieldAutocomplete extends React.Component<FieldAutocompleteProps> {
|
||||||
return (
|
render() {
|
||||||
<Block label={props.label}>
|
return <Block label={this.props.label}>
|
||||||
<InputAutocomplete {...props} />
|
<InputAutocomplete {...this.props} />
|
||||||
</Block>
|
</Block>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldAutocomplete;
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import InputCheckbox, {InputCheckboxProps} from './InputCheckbox'
|
import InputCheckbox, {InputCheckboxProps} from './InputCheckbox'
|
||||||
|
|
||||||
@@ -7,12 +8,11 @@ type FieldCheckboxProps = InputCheckboxProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const FieldCheckbox: React.FC<FieldCheckboxProps> = (props) => {
|
export default class FieldCheckbox extends React.Component<FieldCheckboxProps> {
|
||||||
return (
|
render() {
|
||||||
<Block label={props.label}>
|
return <Block label={this.props.label}>
|
||||||
<InputCheckbox {...props} />
|
<InputCheckbox {...this.props} />
|
||||||
</Block>
|
</Block>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldCheckbox;
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import InputColor, {InputColorProps} from './InputColor'
|
import InputColor, {InputColorProps} from './InputColor'
|
||||||
|
|
||||||
@@ -10,12 +11,11 @@ type FieldColorProps = InputColorProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const FieldColor: React.FC<FieldColorProps> = (props) => {
|
export default class FieldColor extends React.Component<FieldColorProps> {
|
||||||
return (
|
render() {
|
||||||
<Block label={props.label} fieldSpec={props.fieldSpec}>
|
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||||
<InputColor {...props} />
|
<InputColor {...this.props} />
|
||||||
</Block>
|
</Block>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldColor;
|
|
||||||
|
|||||||
@@ -2,39 +2,32 @@ import React from 'react'
|
|||||||
|
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import InputString from './InputString'
|
import InputString from './InputString'
|
||||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
type FieldCommentInternalProps = {
|
type FieldCommentProps = {
|
||||||
value?: string
|
value?: string
|
||||||
onChange(value: string | undefined): unknown
|
onChange(value: string | undefined): unknown
|
||||||
error: {message: string}
|
error: {message: string}
|
||||||
} & WithTranslation;
|
};
|
||||||
|
|
||||||
const FieldCommentInternal: React.FC<FieldCommentInternalProps> = (props) => {
|
export default class FieldComment extends React.Component<FieldCommentProps> {
|
||||||
const t = props.t;
|
render() {
|
||||||
const fieldSpec = {
|
const fieldSpec = {
|
||||||
doc: t(
|
doc: "Comments for the current layer. This is non-standard and not in the spec."
|
||||||
"Comments for the current layer. This is non-standard and not in the spec."
|
};
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return <Block
|
||||||
<Block
|
label={"Comments"}
|
||||||
label={t("Comments")}
|
|
||||||
fieldSpec={fieldSpec}
|
fieldSpec={fieldSpec}
|
||||||
data-wd-key="layer-comment"
|
data-wd-key="layer-comment"
|
||||||
error={props.error}
|
error={this.props.error}
|
||||||
>
|
>
|
||||||
<InputString
|
<InputString
|
||||||
multi={true}
|
multi={true}
|
||||||
value={props.value}
|
value={this.props.value}
|
||||||
onChange={props.onChange}
|
onChange={this.props.onChange}
|
||||||
default={t("Comment...")}
|
default="Comment..."
|
||||||
data-wd-key="layer-comment.input"
|
data-wd-key="layer-comment.input"
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const FieldComment = withTranslation()(FieldCommentInternal);
|
|
||||||
export default FieldComment;
|
|
||||||
|
|||||||
@@ -2,52 +2,64 @@ import React from 'react'
|
|||||||
import {MdInfoOutline, MdHighlightOff} from 'react-icons/md'
|
import {MdInfoOutline, MdHighlightOff} from 'react-icons/md'
|
||||||
|
|
||||||
type FieldDocLabelProps = {
|
type FieldDocLabelProps = {
|
||||||
label: JSX.Element | string | undefined
|
label: object | string | undefined
|
||||||
fieldSpec?: {
|
fieldSpec?: {
|
||||||
doc?: string
|
doc?: string
|
||||||
}
|
}
|
||||||
onToggleDoc?(...args: unknown[]): unknown
|
onToggleDoc?(...args: unknown[]): unknown
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type FieldDocLabelState = {
|
||||||
|
open: boolean
|
||||||
|
};
|
||||||
|
|
||||||
const FieldDocLabel: React.FC<FieldDocLabelProps> = (props) => {
|
export default class FieldDocLabel extends React.Component<FieldDocLabelProps, FieldDocLabelState> {
|
||||||
const [open, setOpen] = React.useState(false);
|
constructor (props: FieldDocLabelProps) {
|
||||||
|
super(props);
|
||||||
const onToggleDoc = (state: boolean) => {
|
this.state = {
|
||||||
setOpen(state);
|
open: false,
|
||||||
if (props.onToggleDoc) {
|
|
||||||
props.onToggleDoc(state);
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const { label, fieldSpec } = props;
|
onToggleDoc = (open: boolean) => {
|
||||||
const { doc } = fieldSpec || {};
|
this.setState({
|
||||||
|
open,
|
||||||
|
}, () => {
|
||||||
|
if (this.props.onToggleDoc) {
|
||||||
|
this.props.onToggleDoc(this.state.open);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (doc) {
|
render() {
|
||||||
return (
|
const {label, fieldSpec} = this.props;
|
||||||
<label className="maputnik-doc-wrapper">
|
const {doc} = fieldSpec || {};
|
||||||
|
|
||||||
|
if (doc) {
|
||||||
|
return <label className="maputnik-doc-wrapper">
|
||||||
<div className="maputnik-doc-target">
|
<div className="maputnik-doc-target">
|
||||||
{label}
|
{label}
|
||||||
{'\xa0'}
|
{'\xa0'}
|
||||||
<button
|
<button
|
||||||
aria-label={open ? 'close property documentation' : 'open property documentation'}
|
aria-label={this.state.open ? "close property documentation" : "open property documentation"}
|
||||||
className={`maputnik-doc-button maputnik-doc-button--${open ? 'open' : 'closed'}`}
|
className={`maputnik-doc-button maputnik-doc-button--${this.state.open ? 'open' : 'closed'}`}
|
||||||
onClick={() => onToggleDoc(!open)}
|
onClick={() => this.onToggleDoc(!this.state.open)}
|
||||||
data-wd-key={'field-doc-button-' + label}
|
data-wd-key={'field-doc-button-'+label}
|
||||||
>
|
>
|
||||||
{open ? <MdHighlightOff /> : <MdInfoOutline />}
|
{this.state.open ? <MdHighlightOff /> : <MdInfoOutline />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
);
|
}
|
||||||
} else if (label) {
|
else if (label) {
|
||||||
return (
|
return <label className="maputnik-doc-wrapper">
|
||||||
<label className="maputnik-doc-wrapper">
|
<div className="maputnik-doc-target">
|
||||||
<div className="maputnik-doc-target">{label}</div>
|
{label}
|
||||||
|
</div>
|
||||||
</label>
|
</label>
|
||||||
);
|
}
|
||||||
|
else {
|
||||||
|
<div />
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return <div />;
|
}
|
||||||
};
|
|
||||||
|
|
||||||
export default FieldDocLabel;
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react'
|
||||||
import InputDynamicArray, {FieldDynamicArrayProps as InputDynamicArrayProps} from './InputDynamicArray'
|
import InputDynamicArray, {FieldDynamicArrayProps as InputDynamicArrayProps} from './InputDynamicArray'
|
||||||
import Fieldset from './Fieldset'
|
import Fieldset from './Fieldset'
|
||||||
|
|
||||||
@@ -5,12 +6,11 @@ type FieldDynamicArrayProps = InputDynamicArrayProps & {
|
|||||||
name?: string
|
name?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
const FieldDynamicArray: React.FC<FieldDynamicArrayProps> = (props) => {
|
export default class FieldDynamicArray extends React.Component<FieldDynamicArrayProps> {
|
||||||
return (
|
render() {
|
||||||
<Fieldset label={props.label}>
|
return <Fieldset label={this.props.label}>
|
||||||
<InputDynamicArray {...props} />
|
<InputDynamicArray {...this.props} />
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldDynamicArray;
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react'
|
||||||
import InputEnum, {InputEnumProps} from './InputEnum'
|
import InputEnum, {InputEnumProps} from './InputEnum'
|
||||||
import Fieldset from './Fieldset';
|
import Fieldset from './Fieldset';
|
||||||
|
|
||||||
@@ -10,12 +11,10 @@ type FieldEnumProps = InputEnumProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const FieldEnum: React.FC<FieldEnumProps> = (props) => {
|
export default class FieldEnum extends React.Component<FieldEnumProps> {
|
||||||
return (
|
render() {
|
||||||
<Fieldset label={props.label} fieldSpec={props.fieldSpec}>
|
return <Fieldset label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||||
<InputEnum {...props} />
|
<InputEnum {...this.props} />
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldEnum;
|
|
||||||
+247
-218
@@ -111,268 +111,297 @@ type FieldFunctionProps = {
|
|||||||
value?: any
|
value?: any
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type FieldFunctionState = {
|
||||||
|
dataType: string
|
||||||
|
isEditing: boolean
|
||||||
|
}
|
||||||
|
|
||||||
/** Supports displaying spec field for zoom function objects
|
/** Supports displaying spec field for zoom function objects
|
||||||
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
|
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
|
||||||
*/
|
*/
|
||||||
const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
|
export default class FieldFunction extends React.Component<FieldFunctionProps, FieldFunctionState> {
|
||||||
const [dataType, setDataType] = React.useState(
|
constructor (props: FieldFunctionProps) {
|
||||||
getDataType(props.value, props.fieldSpec)
|
super(props);
|
||||||
);
|
this.state = {
|
||||||
const [isEditing, setIsEditing] = React.useState(false);
|
dataType: getDataType(props.value, props.fieldSpec),
|
||||||
|
isEditing: false,
|
||||||
React.useEffect(() => {
|
|
||||||
if (!isEditing) {
|
|
||||||
setDataType(getDataType(props.value, props.fieldSpec));
|
|
||||||
}
|
}
|
||||||
}, [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) {
|
if (fieldSpec.expression.interpolated) {
|
||||||
return 'exponential';
|
return "exponential"
|
||||||
}
|
}
|
||||||
if (fieldSpec.type === 'number') {
|
if (fieldSpec.type === "number") {
|
||||||
return 'interval';
|
return "interval"
|
||||||
}
|
}
|
||||||
return 'categorical';
|
return "categorical"
|
||||||
};
|
}
|
||||||
|
|
||||||
const addStop = () => {
|
addStop = () => {
|
||||||
const stops = props.value.stops.slice(0);
|
const stops = this.props.value.stops.slice(0)
|
||||||
const lastStop = stops[stops.length - 1];
|
const lastStop = stops[stops.length - 1]
|
||||||
if (typeof lastStop[0] === 'object') {
|
if (typeof lastStop[0] === "object") {
|
||||||
stops.push([
|
stops.push([
|
||||||
{ zoom: lastStop[0].zoom + 1, value: lastStop[0].value },
|
{zoom: lastStop[0].zoom + 1, value: lastStop[0].value},
|
||||||
lastStop[1],
|
lastStop[1]
|
||||||
]);
|
])
|
||||||
} else {
|
}
|
||||||
stops.push([lastStop[0] + 1, lastStop[1]]);
|
else {
|
||||||
|
stops.push([lastStop[0] + 1, lastStop[1]])
|
||||||
}
|
}
|
||||||
|
|
||||||
const changedValue = {
|
const changedValue = {
|
||||||
...props.value,
|
...this.props.value,
|
||||||
stops: stops,
|
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 = () => {
|
deleteExpression = () => {
|
||||||
const { value } = props;
|
const {fieldSpec, fieldName} = this.props;
|
||||||
|
this.props.onChange(fieldName, fieldSpec.default);
|
||||||
|
this.setState({
|
||||||
|
dataType: "value",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let zoomFunc: any;
|
deleteStop = (stopIdx: number) => {
|
||||||
if (typeof value === 'object') {
|
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) {
|
if (value.stops) {
|
||||||
zoomFunc = {
|
zoomFunc = {
|
||||||
base: value.base,
|
base: value.base,
|
||||||
stops: value.stops.map((stop: Stop) => {
|
stops: value.stops.map((stop: Stop) => {
|
||||||
return [stop[0].zoom, stop[1] || findDefaultFromSpec(props.fieldSpec)];
|
return [stop[0].zoom, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
|
||||||
}),
|
})
|
||||||
};
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
zoomFunc = {
|
zoomFunc = {
|
||||||
base: value.base,
|
base: value.base,
|
||||||
stops: [
|
stops: [
|
||||||
[6, findDefaultFromSpec(props.fieldSpec)],
|
[6, findDefaultFromSpec(this.props.fieldSpec)],
|
||||||
[10, findDefaultFromSpec(props.fieldSpec)],
|
[10, findDefaultFromSpec(this.props.fieldSpec)]
|
||||||
],
|
]
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
zoomFunc = {
|
zoomFunc = {
|
||||||
stops: [
|
stops: [
|
||||||
[6, value || findDefaultFromSpec(props.fieldSpec)],
|
[6, value || findDefaultFromSpec(this.props.fieldSpec)],
|
||||||
[10, value || findDefaultFromSpec(props.fieldSpec)],
|
[10, value || findDefaultFromSpec(this.props.fieldSpec)]
|
||||||
],
|
]
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
props.onChange(props.fieldName, zoomFunc);
|
this.props.onChange(this.props.fieldName, zoomFunc)
|
||||||
};
|
}
|
||||||
|
|
||||||
const undoExpression = () => {
|
undoExpression = () => {
|
||||||
const { value, fieldName } = props;
|
const {value, fieldName} = this.props;
|
||||||
|
|
||||||
if (isGetExpression(value)) {
|
if (isGetExpression(value)) {
|
||||||
props.onChange(fieldName, {
|
this.props.onChange(fieldName, {
|
||||||
type: 'identity',
|
"type": "identity",
|
||||||
property: value[1],
|
"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 = () => {
|
canUndo = () => {
|
||||||
const { value, fieldSpec } = props;
|
const {value, fieldSpec} = this.props;
|
||||||
return (
|
return (
|
||||||
isGetExpression(value) ||
|
isGetExpression(value) ||
|
||||||
isLiteralExpression(value) ||
|
isLiteralExpression(value) ||
|
||||||
isPrimative(value) ||
|
isPrimative(value) ||
|
||||||
(Array.isArray(value) && fieldSpec.type === 'array')
|
(Array.isArray(value) && fieldSpec.type === "array")
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const makeExpression = () => {
|
|
||||||
const { value, fieldSpec } = props;
|
|
||||||
let expression;
|
|
||||||
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
const makeDataFunction = () => {
|
|
||||||
const functionType = getFieldFunctionType(props.fieldSpec);
|
|
||||||
const stopValue = functionType === 'categorical' ? '' : 0;
|
|
||||||
const { value } = props;
|
|
||||||
let dataFunc;
|
|
||||||
|
|
||||||
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 {
|
|
||||||
dataFunc = {
|
|
||||||
property: '',
|
|
||||||
type: functionType,
|
|
||||||
base: value.base,
|
|
||||||
stops: [
|
|
||||||
[{ zoom: 6, value: stopValue }, findDefaultFromSpec(props.fieldSpec)],
|
|
||||||
[{ zoom: 10, value: stopValue }, findDefaultFromSpec(props.fieldSpec)],
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} 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)],
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
props.onChange(props.fieldName, dataFunc);
|
|
||||||
};
|
|
||||||
|
|
||||||
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}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
makeExpression = () => {
|
||||||
<div className={propClass} data-wd-key={'spec-field-container:' + props.fieldName}>
|
const {value, fieldSpec} = this.props;
|
||||||
|
let expression;
|
||||||
|
|
||||||
|
if (typeof(value) === "object" && 'stops' in value) {
|
||||||
|
expression = styleFunction.convertFunction(value, fieldSpec);
|
||||||
|
}
|
||||||
|
else if (isIdentityProperty(value)) {
|
||||||
|
expression = ["get", value.property];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
expression = ["literal", value || this.props.fieldSpec.default];
|
||||||
|
}
|
||||||
|
this.props.onChange(this.props.fieldName, expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
makeDataFunction = () => {
|
||||||
|
const functionType = this.getFieldFunctionType(this.props.fieldSpec);
|
||||||
|
const stopValue = functionType === 'categorical' ? '' : 0;
|
||||||
|
const {value} = this.props;
|
||||||
|
let dataFunc;
|
||||||
|
|
||||||
|
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(this.props.fieldSpec)];
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dataFunc = {
|
||||||
|
property: "",
|
||||||
|
type: functionType,
|
||||||
|
base: value.base,
|
||||||
|
stops: [
|
||||||
|
[{zoom: 6, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)],
|
||||||
|
[{zoom: 10, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dataFunc = {
|
||||||
|
property: "",
|
||||||
|
type: functionType,
|
||||||
|
base: value.base,
|
||||||
|
stops: [
|
||||||
|
[{zoom: 6, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)],
|
||||||
|
[{zoom: 10, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.onChange(this.props.fieldName, dataFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
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:"+this.props.fieldName}>
|
||||||
{specField}
|
{specField}
|
||||||
</div>
|
</div>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldFunction;
|
|
||||||
|
|||||||
+12
-13
@@ -1,5 +1,6 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import InputString from './InputString'
|
import InputString from './InputString'
|
||||||
|
|
||||||
@@ -10,19 +11,17 @@ type FieldIdProps = {
|
|||||||
error?: {message: string}
|
error?: {message: string}
|
||||||
};
|
};
|
||||||
|
|
||||||
const FieldId: React.FC<FieldIdProps> = (props) => {
|
export default class FieldId extends React.Component<FieldIdProps> {
|
||||||
return (
|
render() {
|
||||||
<Block label="ID" fieldSpec={latest.layer.id}
|
return <Block label={"ID"} fieldSpec={latest.layer.id}
|
||||||
data-wd-key={props.wdKey}
|
data-wd-key={this.props.wdKey}
|
||||||
error={props.error}
|
error={this.props.error}
|
||||||
>
|
>
|
||||||
<InputString
|
<InputString
|
||||||
value={props.value}
|
value={this.props.value}
|
||||||
onInput={props.onChange}
|
onInput={this.props.onChange}
|
||||||
data-wd-key={props.wdKey + ".input"}
|
data-wd-key={this.props.wdKey + ".input"}
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldId;
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
|
import React from 'react'
|
||||||
import InputJson, {InputJsonProps} from './InputJson'
|
import InputJson, {InputJsonProps} from './InputJson'
|
||||||
|
|
||||||
|
|
||||||
type FieldJsonProps = InputJsonProps & {};
|
type FieldJsonProps = InputJsonProps & {};
|
||||||
|
|
||||||
|
|
||||||
const FieldJson: React.FC<FieldJsonProps> = (props) => {
|
export default class FieldJson extends React.Component<FieldJsonProps> {
|
||||||
return <InputJson {...props} />;
|
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 {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import InputNumber from './InputNumber'
|
import InputNumber from './InputNumber'
|
||||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
type FieldMaxZoomInternalProps = {
|
type FieldMaxZoomProps = {
|
||||||
value?: number
|
value?: number
|
||||||
onChange(value: number | undefined): unknown
|
onChange(value: number | undefined): unknown
|
||||||
error?: {message: string}
|
error?: {message: string}
|
||||||
} & WithTranslation;
|
};
|
||||||
|
|
||||||
const FieldMaxZoomInternal: React.FC<FieldMaxZoomInternalProps> = (props) => {
|
export default class FieldMaxZoom extends React.Component<FieldMaxZoomProps> {
|
||||||
const t = props.t;
|
render() {
|
||||||
return (
|
return <Block label={"Max Zoom"} fieldSpec={latest.layer.maxzoom}
|
||||||
<Block label={t('Max Zoom')} fieldSpec={latest.layer.maxzoom}
|
error={this.props.error}
|
||||||
error={props.error}
|
|
||||||
data-wd-key="max-zoom"
|
data-wd-key="max-zoom"
|
||||||
>
|
>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
allowRange={true}
|
allowRange={true}
|
||||||
value={props.value}
|
value={this.props.value}
|
||||||
onChange={props.onChange}
|
onChange={this.props.onChange}
|
||||||
min={latest.layer.maxzoom.minimum}
|
min={latest.layer.maxzoom.minimum}
|
||||||
max={latest.layer.maxzoom.maximum}
|
max={latest.layer.maxzoom.maximum}
|
||||||
default={latest.layer.maxzoom.maximum}
|
default={latest.layer.maxzoom.maximum}
|
||||||
data-wd-key="max-zoom.input"
|
data-wd-key="max-zoom.input"
|
||||||
/>
|
/>
|
||||||
</Block>
|
</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 {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import InputNumber from './InputNumber'
|
import InputNumber from './InputNumber'
|
||||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
type FieldMinZoomInternalProps = {
|
type FieldMinZoomProps = {
|
||||||
value?: number
|
value?: number
|
||||||
onChange(...args: unknown[]): unknown
|
onChange(...args: unknown[]): unknown
|
||||||
error?: {message: string}
|
error?: {message: string}
|
||||||
} & WithTranslation;
|
};
|
||||||
|
|
||||||
const FieldMinZoomInternal: React.FC<FieldMinZoomInternalProps> = (props) => {
|
export default class FieldMinZoom extends React.Component<FieldMinZoomProps> {
|
||||||
const t = props.t;
|
render() {
|
||||||
return (
|
return <Block label={"Min Zoom"} fieldSpec={latest.layer.minzoom}
|
||||||
<Block label={t('Min Zoom')} fieldSpec={latest.layer.minzoom}
|
error={this.props.error}
|
||||||
error={props.error}
|
|
||||||
data-wd-key="min-zoom"
|
data-wd-key="min-zoom"
|
||||||
>
|
>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
allowRange={true}
|
allowRange={true}
|
||||||
value={props.value}
|
value={this.props.value}
|
||||||
onChange={props.onChange}
|
onChange={this.props.onChange}
|
||||||
min={latest.layer.minzoom.minimum}
|
min={latest.layer.minzoom.minimum}
|
||||||
max={latest.layer.minzoom.maximum}
|
max={latest.layer.minzoom.maximum}
|
||||||
default={latest.layer.minzoom.minimum}
|
default={latest.layer.minzoom.minimum}
|
||||||
data-wd-key='min-zoom.input'
|
data-wd-key='min-zoom.input'
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const FieldMinZoom = withTranslation()(FieldMinZoomInternal);
|
|
||||||
export default FieldMinZoom;
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react'
|
||||||
import InputMultiInput, {InputMultiInputProps} from './InputMultiInput'
|
import InputMultiInput, {InputMultiInputProps} from './InputMultiInput'
|
||||||
import Fieldset from './Fieldset'
|
import Fieldset from './Fieldset'
|
||||||
|
|
||||||
@@ -7,12 +8,11 @@ type FieldMultiInputProps = InputMultiInputProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const FieldMultiInput: React.FC<FieldMultiInputProps> = (props) => {
|
export default class FieldMultiInput extends React.Component<FieldMultiInputProps> {
|
||||||
return (
|
render() {
|
||||||
<Fieldset label={props.label}>
|
return <Fieldset label={this.props.label}>
|
||||||
<InputMultiInput {...props} />
|
<InputMultiInput {...this.props} />
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldMultiInput;
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react'
|
||||||
import InputNumber, {InputNumberProps} from './InputNumber'
|
import InputNumber, {InputNumberProps} from './InputNumber'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
|
|
||||||
@@ -10,12 +11,10 @@ type FieldNumberProps = InputNumberProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const FieldNumber: React.FC<FieldNumberProps> = (props) => {
|
export default class FieldNumber extends React.Component<FieldNumberProps> {
|
||||||
return (
|
render() {
|
||||||
<Block label={props.label} fieldSpec={props.fieldSpec}>
|
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||||
<InputNumber {...props} />
|
<InputNumber {...this.props} />
|
||||||
</Block>
|
</Block>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldNumber;
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import InputSelect, {InputSelectProps} from './InputSelect'
|
import InputSelect, {InputSelectProps} from './InputSelect'
|
||||||
|
|
||||||
@@ -10,12 +11,12 @@ type FieldSelectProps = InputSelectProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const FieldSelect: React.FC<FieldSelectProps> = (props) => {
|
export default class FieldSelect extends React.Component<FieldSelectProps> {
|
||||||
return (
|
render() {
|
||||||
<Block label={props.label} fieldSpec={props.fieldSpec}>
|
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||||
<InputSelect {...props} />
|
<InputSelect {...this.props}/>
|
||||||
</Block>
|
</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 {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import InputAutocomplete from './InputAutocomplete'
|
import InputAutocomplete from './InputAutocomplete'
|
||||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
type FieldSourceInternalProps = {
|
type FieldSourceProps = {
|
||||||
value?: string
|
value?: string
|
||||||
wdKey?: string
|
wdKey?: string
|
||||||
onChange?(value: string| undefined): unknown
|
onChange?(value: string| undefined): unknown
|
||||||
sourceIds?: unknown[]
|
sourceIds?: unknown[]
|
||||||
error?: {message: string}
|
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);
|
render() {
|
||||||
export default FieldSource;
|
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>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,37 +3,35 @@ import React from 'react'
|
|||||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import InputAutocomplete from './InputAutocomplete'
|
import InputAutocomplete from './InputAutocomplete'
|
||||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
type FieldSourceLayerInternalProps = {
|
type FieldSourceLayerProps = {
|
||||||
value?: string
|
value?: string
|
||||||
onChange?(...args: unknown[]): unknown
|
onChange?(...args: unknown[]): unknown
|
||||||
sourceLayerIds?: unknown[]
|
sourceLayerIds?: unknown[]
|
||||||
|
isFixed?: boolean
|
||||||
error?: {message: string}
|
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 class FieldSourceLayer extends React.Component<FieldSourceLayerProps> {
|
||||||
export default FieldSourceLayer;
|
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,3 +1,4 @@
|
|||||||
|
import React from 'react'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import InputString, {InputStringProps} from './InputString'
|
import InputString, {InputStringProps} from './InputString'
|
||||||
|
|
||||||
@@ -9,12 +10,10 @@ type FieldStringProps = InputStringProps & {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const FieldString: React.FC<FieldStringProps> = (props) => {
|
export default class FieldString extends React.Component<FieldStringProps> {
|
||||||
return (
|
render() {
|
||||||
<Block label={props.label} fieldSpec={props.fieldSpec}>
|
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||||
<InputString {...props} />
|
<InputString {...this.props} />
|
||||||
</Block>
|
</Block>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldString;
|
|
||||||
|
|||||||
@@ -1,47 +1,52 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {v8} from '@maplibre/maplibre-gl-style-spec'
|
|
||||||
|
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import InputSelect from './InputSelect'
|
import InputSelect from './InputSelect'
|
||||||
import InputString from './InputString'
|
import InputString from './InputString'
|
||||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
|
||||||
import { startCase } from 'lodash'
|
|
||||||
|
|
||||||
type FieldTypeInternalProps = {
|
type FieldTypeProps = {
|
||||||
value: string
|
value: string
|
||||||
wdKey?: string
|
wdKey?: string
|
||||||
onChange(value: string): unknown
|
onChange(value: string): unknown
|
||||||
error?: {message: string}
|
error?: {message: string}
|
||||||
disabled?: boolean
|
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 class FieldType extends React.Component<FieldTypeProps> {
|
||||||
export default FieldType;
|
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>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react'
|
||||||
import InputUrl, {FieldUrlProps as InputUrlProps} from './InputUrl'
|
import InputUrl, {FieldUrlProps as InputUrlProps} from './InputUrl'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
|
|
||||||
@@ -10,12 +11,13 @@ type FieldUrlProps = InputUrlProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const FieldUrl: React.FC<FieldUrlProps> = (props) => {
|
export default class FieldUrl extends React.Component<FieldUrlProps> {
|
||||||
return (
|
render () {
|
||||||
<Block label={props.label} fieldSpec={props.fieldSpec}>
|
return (
|
||||||
<InputUrl {...props} />
|
<Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||||
</Block>
|
<InputUrl {...this.props} />
|
||||||
);
|
</Block>
|
||||||
};
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default FieldUrl;
|
|
||||||
|
|||||||
+54
-37
@@ -1,48 +1,65 @@
|
|||||||
import React, { PropsWithChildren, ReactElement } from 'react'
|
import React, { ReactElement } from 'react'
|
||||||
import FieldDocLabel from './FieldDocLabel'
|
import FieldDocLabel from './FieldDocLabel'
|
||||||
import Doc from './Doc'
|
import Doc from './Doc'
|
||||||
import generateUniqueId from '../libs/document-uid';
|
import generateUniqueId from '../libs/document-uid';
|
||||||
|
|
||||||
type FieldsetProps = PropsWithChildren & {
|
type FieldsetProps = {
|
||||||
label?: string,
|
label?: string,
|
||||||
fieldSpec?: { doc?: string },
|
fieldSpec?: { doc?: string },
|
||||||
action?: ReactElement,
|
action?: ReactElement,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type FieldsetState = {
|
||||||
const Fieldset: React.FC<FieldsetProps> = (props) => {
|
showDoc: boolean
|
||||||
const [showDoc, setShowDoc] = React.useState(false);
|
|
||||||
const labelId = React.useRef(generateUniqueId('fieldset_label_'));
|
|
||||||
|
|
||||||
const onToggleDoc = (val: boolean) => {
|
|
||||||
setShowDoc(val);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="maputnik-input-block" role="group" aria-labelledby={labelId.current}>
|
|
||||||
{props.fieldSpec && (
|
|
||||||
<div className="maputnik-input-block-label">
|
|
||||||
<FieldDocLabel
|
|
||||||
label={props.label}
|
|
||||||
onToggleDoc={onToggleDoc}
|
|
||||||
fieldSpec={props.fieldSpec}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!props.fieldSpec && (
|
|
||||||
<div className="maputnik-input-block-label">
|
|
||||||
{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>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Fieldset;
|
export default class Fieldset extends React.Component<FieldsetProps, FieldsetState> {
|
||||||
|
_labelId: string;
|
||||||
|
|
||||||
|
constructor (props: FieldsetProps) {
|
||||||
|
super(props);
|
||||||
|
this._labelId = generateUniqueId(`fieldset_label_`);
|
||||||
|
this.state = {
|
||||||
|
showDoc: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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={this.props.label}
|
||||||
|
onToggleDoc={this.onToggleDoc}
|
||||||
|
fieldSpec={this.props.fieldSpec}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{!this.props.fieldSpec &&
|
||||||
|
<div className="maputnik-input-block-label">
|
||||||
|
{this.props.label}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<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>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {mdiTableRowPlusAfter} from '@mdi/js';
|
import {mdiTableRowPlusAfter} from '@mdi/js';
|
||||||
import {isEqual} from 'lodash';
|
import {isEqual} from 'lodash';
|
||||||
import {ExpressionSpecification, LegacyFilterSpecification} from 'maplibre-gl'
|
import {ExpressionSpecification, LegacyFilterSpecification, StyleSpecification} from 'maplibre-gl'
|
||||||
import {latest, migrate, convertFilter} from '@maplibre/maplibre-gl-style-spec'
|
import {latest, migrate, convertFilter} from '@maplibre/maplibre-gl-style-spec'
|
||||||
import {mdiFunctionVariant} from '@mdi/js';
|
import {mdiFunctionVariant} from '@mdi/js';
|
||||||
|
|
||||||
@@ -13,11 +13,9 @@ import FilterEditorBlock from './FilterEditorBlock'
|
|||||||
import InputButton from './InputButton'
|
import InputButton from './InputButton'
|
||||||
import Doc from './Doc'
|
import Doc from './Doc'
|
||||||
import ExpressionProperty from './_ExpressionProperty';
|
import ExpressionProperty from './_ExpressionProperty';
|
||||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
|
||||||
import type { StyleSpecificationWithId } from '../libs/definitions';
|
|
||||||
|
|
||||||
|
|
||||||
function combiningFilter(props: FilterEditorInternalProps): LegacyFilterSpecification | ExpressionSpecification {
|
function combiningFilter(props: FilterEditorProps): LegacyFilterSpecification | ExpressionSpecification {
|
||||||
const filter = props.filter || ['all'];
|
const filter = props.filter || ['all'];
|
||||||
|
|
||||||
if (!Array.isArray(filter)) {
|
if (!Array.isArray(filter)) {
|
||||||
@@ -40,7 +38,7 @@ function migrateFilter(filter: LegacyFilterSpecification | ExpressionSpecificati
|
|||||||
return (migrate(createStyleFromFilter(filter) as any).layers[0] as any).filter;
|
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 {
|
return {
|
||||||
"id": "tmp",
|
"id": "tmp",
|
||||||
"version": 8,
|
"version": 8,
|
||||||
@@ -49,7 +47,7 @@ function createStyleFromFilter(filter: LegacyFilterSpecification | ExpressionSpe
|
|||||||
"sources": {
|
"sources": {
|
||||||
"tmp": {
|
"tmp": {
|
||||||
"type": "geojson",
|
"type": "geojson",
|
||||||
"data": ''
|
"data": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sprite": "",
|
"sprite": "",
|
||||||
@@ -91,13 +89,13 @@ function hasNestedCombiningFilter(filter: LegacyFilterSpecification | Expression
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
type FilterEditorInternalProps = {
|
type FilterEditorProps = {
|
||||||
/** Properties of the vector layer and the available fields */
|
/** Properties of the vector layer and the available fields */
|
||||||
properties?: {[key:string]: any}
|
properties?: {[key:string]: any}
|
||||||
filter?: any[]
|
filter?: any[]
|
||||||
errors?: {[key:string]: any}
|
errors?: {[key:string]: any}
|
||||||
onChange(value: LegacyFilterSpecification | ExpressionSpecification): unknown
|
onChange(value: LegacyFilterSpecification | ExpressionSpecification): unknown
|
||||||
} & WithTranslation;
|
};
|
||||||
|
|
||||||
type FilterEditorState = {
|
type FilterEditorState = {
|
||||||
showDoc: boolean
|
showDoc: boolean
|
||||||
@@ -105,12 +103,12 @@ type FilterEditorState = {
|
|||||||
valueIsSimpleFilter?: boolean
|
valueIsSimpleFilter?: boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
class FilterEditorInternal extends React.Component<FilterEditorInternalProps, FilterEditorState> {
|
export default class FilterEditor extends React.Component<FilterEditorProps, FilterEditorState> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
filter: ["all"],
|
filter: ["all"],
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor (props: FilterEditorInternalProps) {
|
constructor (props: FilterEditorProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
showDoc: false,
|
showDoc: false,
|
||||||
@@ -157,17 +155,17 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(props: Readonly<FilterEditorInternalProps>, state: FilterEditorState) {
|
static getDerivedStateFromProps(props: FilterEditorProps, currentState: FilterEditorState) {
|
||||||
const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props));
|
const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props));
|
||||||
|
|
||||||
// Upgrade but never downgrade
|
// Upgrade but never downgrade
|
||||||
if (!displaySimpleFilter && state.displaySimpleFilter === true) {
|
if (!displaySimpleFilter && currentState.displaySimpleFilter === true) {
|
||||||
return {
|
return {
|
||||||
displaySimpleFilter: false,
|
displaySimpleFilter: false,
|
||||||
valueIsSimpleFilter: false,
|
valueIsSimpleFilter: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else if (displaySimpleFilter && state.displaySimpleFilter === false) {
|
else if (displaySimpleFilter && currentState.displaySimpleFilter === false) {
|
||||||
return {
|
return {
|
||||||
valueIsSimpleFilter: true,
|
valueIsSimpleFilter: true,
|
||||||
}
|
}
|
||||||
@@ -180,7 +178,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {errors, t} = this.props;
|
const {errors} = this.props;
|
||||||
const {displaySimpleFilter} = this.state;
|
const {displaySimpleFilter} = this.state;
|
||||||
const fieldSpec={
|
const fieldSpec={
|
||||||
doc: latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."
|
doc: latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."
|
||||||
@@ -192,16 +190,16 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
|||||||
if (isNestedCombiningFilter) {
|
if (isNestedCombiningFilter) {
|
||||||
return <div className="maputnik-filter-editor-unsupported">
|
return <div className="maputnik-filter-editor-unsupported">
|
||||||
<p>
|
<p>
|
||||||
{t("Nested filters are not supported.")}
|
Nested filters are not supported.
|
||||||
</p>
|
</p>
|
||||||
<InputButton
|
<InputButton
|
||||||
onClick={this.makeExpression}
|
onClick={this.makeExpression}
|
||||||
title={t("Convert to expression")}
|
title="Convert to expression"
|
||||||
>
|
>
|
||||||
<svg style={{marginRight: "0.2em", width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
<svg style={{marginRight: "0.2em", width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
||||||
<path fill="currentColor" d={mdiFunctionVariant} />
|
<path fill="currentColor" d={mdiFunctionVariant} />
|
||||||
</svg>
|
</svg>
|
||||||
{t("Upgrade to expression")}
|
Upgrade to expression
|
||||||
</InputButton>
|
</InputButton>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -214,7 +212,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
|||||||
<div>
|
<div>
|
||||||
<InputButton
|
<InputButton
|
||||||
onClick={this.makeExpression}
|
onClick={this.makeExpression}
|
||||||
title={t("Convert to expression")}
|
title="Convert to expression"
|
||||||
className="maputnik-make-zoom-function"
|
className="maputnik-make-zoom-function"
|
||||||
>
|
>
|
||||||
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
||||||
@@ -249,17 +247,13 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
|||||||
<Block
|
<Block
|
||||||
key="top"
|
key="top"
|
||||||
fieldSpec={fieldSpec}
|
fieldSpec={fieldSpec}
|
||||||
label={t("Filter")}
|
label={"Filter"}
|
||||||
action={actions}
|
action={actions}
|
||||||
>
|
>
|
||||||
<InputSelect
|
<InputSelect
|
||||||
value={combiningOp}
|
value={combiningOp}
|
||||||
onChange={(v: [string, any]) => this.onFilterPartChanged(0, v)}
|
onChange={(v: [string, any]) => this.onFilterPartChanged(0, v)}
|
||||||
options={[
|
options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
|
||||||
["all", t("every filter matches")],
|
|
||||||
["none", t("no filter matches")],
|
|
||||||
["any", t("any filter matches")]
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
{editorBlocks}
|
{editorBlocks}
|
||||||
@@ -274,7 +268,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
|||||||
>
|
>
|
||||||
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
||||||
<path fill="currentColor" d={mdiTableRowPlusAfter} />
|
<path fill="currentColor" d={mdiTableRowPlusAfter} />
|
||||||
</svg> {t("Add filter")}
|
</svg> Add filter
|
||||||
</InputButton>
|
</InputButton>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -305,13 +299,12 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
|||||||
/>
|
/>
|
||||||
{this.state.valueIsSimpleFilter &&
|
{this.state.valueIsSimpleFilter &&
|
||||||
<div className="maputnik-expr-infobox">
|
<div className="maputnik-expr-infobox">
|
||||||
{t("You've entered an old style filter.")}
|
You've entered a old style filter,{' '}
|
||||||
{' '}
|
|
||||||
<button
|
<button
|
||||||
onClick={this.makeFilter}
|
onClick={this.makeFilter}
|
||||||
className="maputnik-expr-infobox__button"
|
className="maputnik-expr-infobox__button"
|
||||||
>
|
>
|
||||||
{t("Switch to filter editor.")}
|
switch to filter editor
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -320,6 +313,3 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const FilterEditor = withTranslation()(FilterEditorInternal);
|
|
||||||
export default FilterEditor;
|
|
||||||
|
|||||||
@@ -1,31 +1,27 @@
|
|||||||
import React, { PropsWithChildren } from 'react'
|
import React from 'react'
|
||||||
import InputButton from './InputButton'
|
import InputButton from './InputButton'
|
||||||
import {MdDelete} from 'react-icons/md'
|
import {MdDelete} from 'react-icons/md'
|
||||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
type FilterEditorBlockInternalProps = PropsWithChildren & {
|
type FilterEditorBlockProps = {
|
||||||
onDelete(...args: unknown[]): unknown
|
onDelete(...args: unknown[]): unknown
|
||||||
} & WithTranslation;
|
};
|
||||||
|
|
||||||
class FilterEditorBlockInternal extends React.Component<FilterEditorBlockInternalProps> {
|
export default class FilterEditorBlock extends React.Component<FilterEditorBlockProps> {
|
||||||
render() {
|
render() {
|
||||||
const t = this.props.t;
|
|
||||||
return <div className="maputnik-filter-editor-block">
|
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">
|
<div className="maputnik-filter-editor-block-action">
|
||||||
<InputButton
|
<InputButton
|
||||||
className="maputnik-icon-button"
|
className="maputnik-delete-filter"
|
||||||
onClick={this.props.onDelete}
|
onClick={this.props.onDelete}
|
||||||
title={t("Delete filter block")}
|
title="Delete filter block"
|
||||||
>
|
>
|
||||||
<MdDelete />
|
<MdDelete />
|
||||||
</InputButton>
|
</InputButton>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="maputnik-filter-editor-block-content">
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const FilterEditorBlock = withTranslation()(FilterEditorBlockInternal);
|
|
||||||
export default FilterEditorBlock;
|
|
||||||
|
|||||||
@@ -11,3 +11,5 @@ export default class IconBackground extends React.Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,3 +11,5 @@ export default class IconCircle extends React.Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,3 +11,5 @@ export default class IconFill extends React.Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -30,3 +30,4 @@ export default class IconLayer extends React.Component<IconLayerProps> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user