mirror of
https://github.com/maputnik/editor.git
synced 2026-08-03 03:37:25 +00:00
Compare commits
2 Commits
v3.0.0
..
285dca8d2c
| Author | SHA1 | Date | |
|---|---|---|---|
| 285dca8d2c | |||
| 2a3b6aa770 |
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es2020": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:react/jsx-runtime",
|
||||||
|
"plugin:react-hooks/recommended",
|
||||||
|
],
|
||||||
|
"ignorePatterns": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": "latest",
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"react": { "version": "16.4" }
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"@typescript-eslint",
|
||||||
|
"react-refresh"],
|
||||||
|
"rules": {
|
||||||
|
"react-refresh/only-export-components": [
|
||||||
|
"warn",
|
||||||
|
{ "allowConstantExport": true }
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"warn",
|
||||||
|
{ "argsIgnorePattern": "^_" }
|
||||||
|
],
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"react/prop-types": ["off"],
|
||||||
|
// Disable no-undef. It's covered by @typescript-eslint
|
||||||
|
"no-undef": "off",
|
||||||
|
"indent": ["error", 2],
|
||||||
|
"no-var": ["error"]
|
||||||
|
},
|
||||||
|
"globals": {
|
||||||
|
"global": "readonly"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,3 +8,4 @@ assignees: ''
|
|||||||
---
|
---
|
||||||
|
|
||||||
<!-- Thanks for reaching out! If you are having general Maputnik mapping questions, please asking them at https://gis.stackexchange.com/ using the 'maputnik' tag https://gis.stackexchange.com/questions/tagged/maputnik and read https://gis.stackexchange.com/help/how-to-ask before you do so (please keep in mind that you're asking there in a general GIS forum, not a dedicated support channel) -->
|
<!-- 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}}
|
|
||||||
+69
-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 test-docker-image-build .
|
||||||
|
|
||||||
|
# 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,73 @@ 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
|
- name: artifacts/maputnik
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v1
|
||||||
with:
|
with:
|
||||||
name: maputnik
|
name: maputnik
|
||||||
path: dist
|
path: dist
|
||||||
|
|
||||||
# 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 }}
|
|
||||||
@@ -10,10 +10,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event_name == 'push' }}
|
if: ${{ github.event_name == 'push' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Use Node.js from nvmrc
|
- name: Use Node.js from nvmrc
|
||||||
uses: actions/setup-node@v5
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Upload to GitHub Pages
|
- name: Upload to GitHub Pages
|
||||||
uses: peaceiris/actions-gh-pages@v4
|
uses: peaceiris/actions-gh-pages@v3
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
publish_dir: dist
|
publish_dir: dist
|
||||||
@@ -40,12 +40,12 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
- run: docker build -t ghcr.io/maplibre/maputnik:main .
|
- run: docker build -t ghcr.io/maplibre/maputnik:main .
|
||||||
- run: docker push 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
|
|
||||||
@@ -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
|
|
||||||
+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 npx vite 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/
|
||||||
|
|||||||
@@ -20,3 +20,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -18,15 +18,9 @@ targeted at developers and map designers.
|
|||||||
- In a Docker, run this command and browse to http://localhost:8888, Ctrl+C to stop the server.
|
- 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 ghcr.io/maplibre/maputnik:main
|
||||||
```
|
```
|
||||||
|
|
||||||
To see the CLI options (for example file watching or style serving) run:
|
|
||||||
```bash
|
|
||||||
docker run -it --rm -p 8888:8000 ghcr.io/maplibre/maputnik:main --help
|
|
||||||
```
|
|
||||||
You might need to mount a volume (`-v`) to be able to use these options.
|
|
||||||
|
|
||||||
## Documentation
|
## 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/maplibre/maputnik/wiki). You are welcome to collaborate!
|
||||||
@@ -42,8 +36,6 @@ Maputnik is written in typescript and is using [React](https://github.com/facebo
|
|||||||
|
|
||||||
We ensure building and developing Maputnik works with the [current active LTS Node.js version and above](https://github.com/nodejs/Release#release-schedule).
|
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
|
### 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.
|
Join the #maplibre or #maputnik slack channel at OSMUS: get an invite at https://slack.openstreetmap.us/ Read the the below guide in order to get familiar with how we do things around here.
|
||||||
|
|
||||||
@@ -74,14 +66,14 @@ Lint the JavaScript code.
|
|||||||
```
|
```
|
||||||
# run linter
|
# 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,15 +93,10 @@ 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
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
+16
-149
@@ -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();
|
||||||
@@ -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,11 @@ 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";
|
||||||
then(get.elementByTestId(errorSelector)).shouldNotExist();
|
then(get.elementByTestId(errorSelector)).shouldNotExist();
|
||||||
|
|
||||||
when.click(".CodeMirror");
|
when.click(".CodeMirror");
|
||||||
@@ -339,7 +320,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 +342,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 +359,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 +385,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 +404,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 +423,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 +475,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();
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
/// <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 "./maputnik-cypress-helper";
|
||||||
@@ -8,17 +6,15 @@ 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);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
+8
-174
@@ -1,8 +1,7 @@
|
|||||||
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(() => {
|
||||||
@@ -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");
|
||||||
@@ -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", () => {
|
||||||
@@ -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__';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generated
+7801
-6813
File diff suppressed because it is too large
Load Diff
+63
-75
@@ -1,20 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "maputnik",
|
"name": "maputnik",
|
||||||
"version": "3.0.0",
|
"version": "2.0.0",
|
||||||
"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 --base=/maputnik/",
|
||||||
"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"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -24,30 +20,23 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://github.com/maplibre/maputnik#readme",
|
"homepage": "https://github.com/maplibre/maputnik#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-geocoder": "^1.5.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@maplibre/maplibre-gl-style-spec": "^20.1.0",
|
||||||
"@mapbox/mapbox-gl-rtl-text": "^0.3.0",
|
"@mdi/js": "^6.6.96",
|
||||||
"@maplibre/maplibre-gl-geocoder": "^1.9.0",
|
"@mdi/react": "^1.5.0",
|
||||||
"@maplibre/maplibre-gl-inspect": "^1.7.1",
|
"@typescript-eslint/eslint-plugin": "^6.16.0",
|
||||||
"@maplibre/maplibre-gl-style-spec": "^23.3.0",
|
"@typescript-eslint/parser": "^6.16.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 +44,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": "^4.0.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 +87,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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",
|
"@types/codemirror": "^5.60.15",
|
||||||
"@types/color": "^4.2.0",
|
"@types/color": "^3.0.6",
|
||||||
"@types/cors": "^2.8.19",
|
"@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 +103,36 @@
|
|||||||
"@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",
|
"stylelint": "^14.6.1",
|
||||||
"stylelint-config-recommended-scss": "^16.0.0",
|
"stylelint-config-recommended-scss": "^6.0.0",
|
||||||
"stylelint-scss": "^6.12.1",
|
"stylelint-scss": "^4.2.0",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.3.3",
|
||||||
"typescript-eslint": "^8.43.0",
|
"uuid": "^8.3.2",
|
||||||
"uuid": "^13.0.0",
|
"vite": "^5.0.12",
|
||||||
"vite": "^7.1.5",
|
"vite-plugin-istanbul": "^5.0.0"
|
||||||
"vite-plugin-istanbul": "^7.1.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+159
-129
@@ -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,7 +8,6 @@ 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, validateStyleMin} from '@maplibre/maplibre-gl-style-spec'
|
||||||
|
|
||||||
@@ -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,12 @@ 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) || [];
|
const errors: ValidationError[] = validateStyleMin(newStyle) || [];
|
||||||
|
|
||||||
// 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 +459,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 +469,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 +486,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 +519,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 +599,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 +615,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 +642,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 +729,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 +782,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 +839,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 +855,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 +942,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 +958,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 +970,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,10 @@ 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} 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
|
//@ts-ignore
|
||||||
import maputnikLogo from 'maputnik-design/logos/logo-color.svg?inline'
|
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();
|
||||||
@@ -91,12 +80,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 +93,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 +110,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 +121,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,21 +174,21 @@ 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"
|
||||||
@@ -212,7 +196,7 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
|||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
href="https://github.com/maplibre/maputnik"
|
href="https://github.com/maplibre/maputnik"
|
||||||
>
|
>
|
||||||
<img src={maputnikLogo} alt={t("Maputnik on GitHub")} />
|
<img src={maputnikLogo} alt="Maputnik on GitHub" />
|
||||||
<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 +206,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 +237,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 +247,15 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
|||||||
})}
|
})}
|
||||||
</optgroup>
|
</optgroup>
|
||||||
</select>
|
</select>
|
||||||
</IconText>
|
</label>
|
||||||
</ToolbarSelect>
|
|
||||||
|
|
||||||
<ToolbarSelect wdKey="nav:language">
|
|
||||||
<MdLanguage />
|
|
||||||
<IconText>Language
|
|
||||||
<select
|
|
||||||
className="maputnik-select"
|
|
||||||
data-wd-key="maputnik-lang-select"
|
|
||||||
onChange={(e) => this.handleLanguageChange(e.target.value)}
|
|
||||||
value={this.props.i18n.language}
|
|
||||||
>
|
|
||||||
{Object.entries(supportedLanguages).map(([code, name]) => {
|
|
||||||
return (
|
|
||||||
<option key={code} value={code}>
|
|
||||||
{name}
|
|
||||||
</option>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</select>
|
|
||||||
</IconText>
|
|
||||||
</ToolbarSelect>
|
</ToolbarSelect>
|
||||||
|
|
||||||
<ToolbarLink href={"https://github.com/maplibre/maputnik/wiki"}>
|
<ToolbarLink href={"https://github.com/maplibre/maputnik/wiki"}>
|
||||||
<MdHelpOutline />
|
<MdHelpOutline />
|
||||||
<IconText>{t("Help")}</IconText>
|
<IconText>Help</IconText>
|
||||||
</ToolbarLink>
|
</ToolbarLink>
|
||||||
</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,7 +25,7 @@ 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 = (
|
||||||
@@ -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-container:"+this.props.fieldName}>
|
||||||
{specField}
|
{specField}
|
||||||
</div>
|
</div>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldFunction;
|
|
||||||
|
|||||||
+11
-12
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
@@ -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;
|
|
||||||
|
|||||||
@@ -3,33 +3,28 @@ import React from 'react'
|
|||||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||||
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;
|
|
||||||
|
|||||||
@@ -3,33 +3,28 @@ import React from 'react'
|
|||||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||||
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;
|
|
||||||
|
|||||||
@@ -3,40 +3,33 @@ import React from 'react'
|
|||||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||||
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/dist/latest.json'
|
||||||
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,
|
||||||
@@ -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> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,3 +11,5 @@ export default class IconLine extends React.Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,3 +13,6 @@ export default class IconSymbol extends React.Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export default class FieldArray extends React.Component<FieldArrayProps, FieldAr
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(props: Readonly<FieldArrayProps>, state: FieldArrayState) {
|
static getDerivedStateFromProps(props: FieldArrayProps, state: FieldArrayState) {
|
||||||
const value: any[] = [];
|
const value: any[] = [];
|
||||||
const initialPropsValue = state.initialPropsValue.slice(0);
|
const initialPropsValue = state.initialPropsValue.slice(0);
|
||||||
|
|
||||||
@@ -115,3 +115,4 @@ export default class FieldArray extends React.Component<FieldArrayProps, FieldAr
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import InputAutocomplete from './InputAutocomplete'
|
|
||||||
import { mount } from 'cypress/react'
|
|
||||||
|
|
||||||
const fruits = ['apple', 'banana', 'cherry'];
|
|
||||||
|
|
||||||
describe('<InputAutocomplete />', () => {
|
|
||||||
it('filters options when typing', () => {
|
|
||||||
mount(
|
|
||||||
<InputAutocomplete aria-label="Fruit" options={fruits.map(f => [f, f])} />
|
|
||||||
);
|
|
||||||
cy.get('input').focus();
|
|
||||||
cy.get('.maputnik-autocomplete-menu-item').should('have.length', 3);
|
|
||||||
cy.get('input').type('ch');
|
|
||||||
cy.get('.maputnik-autocomplete-menu-item').should('have.length', 1).and('contain', 'cherry');
|
|
||||||
cy.get('.maputnik-autocomplete-menu-item').click();
|
|
||||||
cy.get('input').should('have.value', 'cherry');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,116 +1,102 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import {useCombobox} from 'downshift'
|
import Autocomplete from 'react-autocomplete'
|
||||||
|
|
||||||
const MAX_HEIGHT = 140
|
|
||||||
|
const MAX_HEIGHT = 140;
|
||||||
|
|
||||||
export type InputAutocompleteProps = {
|
export type InputAutocompleteProps = {
|
||||||
value?: string
|
value?: string
|
||||||
options?: any[]
|
options: any[]
|
||||||
onChange?(value: string | undefined): unknown
|
onChange(value: string | undefined): unknown
|
||||||
|
keepMenuWithinWindowBounds?: boolean
|
||||||
'aria-label'?: string
|
'aria-label'?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function InputAutocomplete({
|
export default class InputAutocomplete extends React.Component<InputAutocompleteProps> {
|
||||||
value,
|
state = {
|
||||||
options = [],
|
maxHeight: MAX_HEIGHT
|
||||||
onChange = () => {},
|
}
|
||||||
'aria-label': ariaLabel,
|
|
||||||
}: InputAutocompleteProps) {
|
|
||||||
const [input, setInput] = React.useState(value || '')
|
|
||||||
const menuRef = React.useRef<HTMLDivElement>(null)
|
|
||||||
const [maxHeight, setMaxHeight] = React.useState(MAX_HEIGHT)
|
|
||||||
|
|
||||||
const filteredItems = React.useMemo(() => {
|
autocompleteMenuEl: HTMLDivElement | null = null;
|
||||||
const lv = input.toLowerCase()
|
|
||||||
return options.filter((item) => item[0].toLowerCase().includes(lv))
|
|
||||||
}, [options, input])
|
|
||||||
|
|
||||||
const calcMaxHeight = React.useCallback(() => {
|
static defaultProps = {
|
||||||
if (menuRef.current) {
|
onChange: () => {},
|
||||||
const space = window.innerHeight - menuRef.current.getBoundingClientRect().top
|
options: [],
|
||||||
setMaxHeight(Math.min(space, MAX_HEIGHT))
|
}
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const {
|
calcMaxHeight() {
|
||||||
isOpen,
|
if(this.props.keepMenuWithinWindowBounds) {
|
||||||
getMenuProps,
|
const maxHeight = window.innerHeight - this.autocompleteMenuEl!.getBoundingClientRect().top;
|
||||||
getInputProps,
|
const limitedMaxHeight = Math.min(maxHeight, MAX_HEIGHT);
|
||||||
getItemProps,
|
|
||||||
highlightedIndex,
|
if(limitedMaxHeight != this.state.maxHeight) {
|
||||||
openMenu,
|
this.setState({
|
||||||
} = useCombobox({
|
maxHeight: limitedMaxHeight
|
||||||
items: filteredItems,
|
})
|
||||||
inputValue: input,
|
|
||||||
itemToString: (item) => (item ? item[0] : ''),
|
|
||||||
stateReducer: (_state, action) => {
|
|
||||||
if (action.type === useCombobox.stateChangeTypes.InputClick) {
|
|
||||||
return {...action.changes, isOpen: true}
|
|
||||||
}
|
}
|
||||||
return action.changes
|
|
||||||
},
|
|
||||||
onSelectedItemChange: ({selectedItem}) => {
|
|
||||||
const v = selectedItem ? selectedItem[0] : ''
|
|
||||||
setInput(v)
|
|
||||||
onChange(selectedItem ? selectedItem[0] : undefined)
|
|
||||||
},
|
|
||||||
onInputValueChange: ({inputValue: v}) => {
|
|
||||||
if (typeof v === 'string') {
|
|
||||||
setInput(v)
|
|
||||||
onChange(v === '' ? undefined : v)
|
|
||||||
openMenu()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (isOpen) {
|
|
||||||
calcMaxHeight()
|
|
||||||
}
|
}
|
||||||
}, [isOpen, calcMaxHeight])
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
componentDidMount() {
|
||||||
window.addEventListener('resize', calcMaxHeight)
|
this.calcMaxHeight();
|
||||||
return () => window.removeEventListener('resize', calcMaxHeight)
|
}
|
||||||
}, [calcMaxHeight])
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
componentDidUpdate() {
|
||||||
setInput(value || '')
|
this.calcMaxHeight();
|
||||||
}, [value])
|
}
|
||||||
|
|
||||||
return (
|
onChange(v: string) {
|
||||||
<div className="maputnik-autocomplete">
|
this.props.onChange(v === "" ? undefined : v);
|
||||||
<input
|
}
|
||||||
{...getInputProps({
|
|
||||||
'aria-label': ariaLabel,
|
render() {
|
||||||
className: 'maputnik-string',
|
return <div
|
||||||
spellCheck: false,
|
ref={(el) => {
|
||||||
onFocus: () => openMenu(),
|
this.autocompleteMenuEl = el;
|
||||||
})}
|
}}
|
||||||
|
>
|
||||||
|
<Autocomplete
|
||||||
|
menuStyle={{
|
||||||
|
position: "fixed",
|
||||||
|
overflow: "auto",
|
||||||
|
maxHeight: this.state.maxHeight,
|
||||||
|
zIndex: '998'
|
||||||
|
}}
|
||||||
|
wrapperProps={{
|
||||||
|
className: "maputnik-autocomplete",
|
||||||
|
style: {}
|
||||||
|
}}
|
||||||
|
inputProps={{
|
||||||
|
'aria-label': this.props['aria-label'],
|
||||||
|
className: "maputnik-string",
|
||||||
|
spellCheck: false
|
||||||
|
}}
|
||||||
|
value={this.props.value}
|
||||||
|
items={this.props.options}
|
||||||
|
getItemValue={(item) => item[0]}
|
||||||
|
onSelect={v => this.onChange(v)}
|
||||||
|
onChange={(_e, v) => this.onChange(v)}
|
||||||
|
shouldItemRender={(item, value="") => {
|
||||||
|
if (typeof(value) === "string") {
|
||||||
|
return item[0].toLowerCase().indexOf(value.toLowerCase()) > -1
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}}
|
||||||
|
renderItem={(item, isHighlighted) => (
|
||||||
|
<div
|
||||||
|
key={item[0]}
|
||||||
|
className={classnames({
|
||||||
|
"maputnik-autocomplete-menu-item": true,
|
||||||
|
"maputnik-autocomplete-menu-item-selected": isHighlighted,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{item[1]}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<div
|
|
||||||
{...getMenuProps({}, {suppressRefError: true})}
|
|
||||||
ref={menuRef}
|
|
||||||
style={{position: 'fixed', overflow: 'auto', maxHeight, zIndex: 998}}
|
|
||||||
className="maputnik-autocomplete-menu"
|
|
||||||
>
|
|
||||||
{isOpen &&
|
|
||||||
filteredItems.map((item, index) => (
|
|
||||||
<div
|
|
||||||
key={item[0]}
|
|
||||||
{...getItemProps({
|
|
||||||
item,
|
|
||||||
index,
|
|
||||||
className: classnames('maputnik-autocomplete-menu-item', {
|
|
||||||
'maputnik-autocomplete-menu-item-selected': highlightedIndex === index,
|
|
||||||
}),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{item[1]}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -133,3 +133,4 @@ export default class InputColor extends React.Component<InputColorProps> {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import capitalize from 'lodash.capitalize'
|
import capitalize from 'lodash.capitalize'
|
||||||
import {MdDelete} from 'react-icons/md'
|
import {MdDelete} from 'react-icons/md'
|
||||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
import InputString from './InputString'
|
import InputString from './InputString'
|
||||||
import InputNumber from './InputNumber'
|
import InputNumber from './InputNumber'
|
||||||
@@ -22,11 +21,10 @@ export type FieldDynamicArrayProps = {
|
|||||||
}
|
}
|
||||||
'aria-label'?: string
|
'aria-label'?: string
|
||||||
label: string
|
label: string
|
||||||
}
|
};
|
||||||
|
|
||||||
type FieldDynamicArrayInternalProps = FieldDynamicArrayProps & WithTranslation;
|
|
||||||
|
|
||||||
class FieldDynamicArrayInternal extends React.Component<FieldDynamicArrayInternalProps> {
|
export default class FieldDynamicArray extends React.Component<FieldDynamicArrayProps> {
|
||||||
changeValue(idx: number, newValue: string | number | undefined) {
|
changeValue(idx: number, newValue: string | number | undefined) {
|
||||||
const values = this.values.slice(0)
|
const values = this.values.slice(0)
|
||||||
values[idx] = newValue
|
values[idx] = newValue
|
||||||
@@ -64,13 +62,8 @@ class FieldDynamicArrayInternal extends React.Component<FieldDynamicArrayInterna
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const t = this.props.t;
|
|
||||||
const i18nProps = { t, i18n: this.props.i18n, tReady: this.props.tReady };
|
|
||||||
const inputs = this.values.map((v, i) => {
|
const inputs = this.values.map((v, i) => {
|
||||||
const deleteValueBtn= <DeleteValueInputButton
|
const deleteValueBtn= <DeleteValueInputButton onClick={this.deleteValue.bind(this, i)} />
|
||||||
onClick={this.deleteValue.bind(this, i)}
|
|
||||||
{...i18nProps}
|
|
||||||
/>;
|
|
||||||
let input;
|
let input;
|
||||||
if(this.props.type === 'url') {
|
if(this.props.type === 'url') {
|
||||||
input = <InputUrl
|
input = <InputUrl
|
||||||
@@ -124,27 +117,23 @@ class FieldDynamicArrayInternal extends React.Component<FieldDynamicArrayInterna
|
|||||||
className="maputnik-array-add-value"
|
className="maputnik-array-add-value"
|
||||||
onClick={this.addValue}
|
onClick={this.addValue}
|
||||||
>
|
>
|
||||||
{t("Add value")}
|
Add value
|
||||||
</InputButton>
|
</InputButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const FieldDynamicArray = withTranslation()(FieldDynamicArrayInternal);
|
|
||||||
export default FieldDynamicArray;
|
|
||||||
|
|
||||||
type DeleteValueInputButtonProps = {
|
type DeleteValueInputButtonProps = {
|
||||||
onClick?(...args: unknown[]): unknown
|
onClick?(...args: unknown[]): unknown
|
||||||
} & WithTranslation;
|
};
|
||||||
|
|
||||||
class DeleteValueInputButton extends React.Component<DeleteValueInputButtonProps> {
|
class DeleteValueInputButton extends React.Component<DeleteValueInputButtonProps> {
|
||||||
render() {
|
render() {
|
||||||
const t = this.props.t;
|
|
||||||
return <InputButton
|
return <InputButton
|
||||||
className="maputnik-delete-stop"
|
className="maputnik-delete-stop"
|
||||||
onClick={this.props.onClick}
|
onClick={this.props.onClick}
|
||||||
title={t("Remove array item")}
|
title="Remove array item"
|
||||||
>
|
>
|
||||||
<FieldDocLabel
|
<FieldDocLabel
|
||||||
label={<MdDelete />}
|
label={<MdDelete />}
|
||||||
@@ -152,3 +141,4 @@ class DeleteValueInputButton extends React.Component<DeleteValueInputButtonProps
|
|||||||
</InputButton>
|
</InputButton>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,3 +47,4 @@ export default class InputEnum extends React.Component<InputEnumProps> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import CodeMirror, { ModeSpec } from 'codemirror';
|
import CodeMirror, { ModeSpec } from 'codemirror';
|
||||||
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
import 'codemirror/mode/javascript/javascript'
|
import 'codemirror/mode/javascript/javascript'
|
||||||
import 'codemirror/addon/lint/lint'
|
import 'codemirror/addon/lint/lint'
|
||||||
@@ -28,7 +27,6 @@ export type InputJsonProps = {
|
|||||||
mode?: ModeSpec<any>
|
mode?: ModeSpec<any>
|
||||||
lint?: boolean | object
|
lint?: boolean | object
|
||||||
};
|
};
|
||||||
type InputJsonInternalProps = InputJsonProps & WithTranslation;
|
|
||||||
|
|
||||||
type InputJsonState = {
|
type InputJsonState = {
|
||||||
isEditing: boolean
|
isEditing: boolean
|
||||||
@@ -36,7 +34,7 @@ type InputJsonState = {
|
|||||||
prevValue: string
|
prevValue: string
|
||||||
};
|
};
|
||||||
|
|
||||||
class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJsonState> {
|
export default class InputJson extends React.Component<InputJsonProps, InputJsonState> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
lineNumbers: true,
|
lineNumbers: true,
|
||||||
lineWrapping: false,
|
lineWrapping: false,
|
||||||
@@ -54,7 +52,7 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
|
|||||||
_el: HTMLDivElement | null = null;
|
_el: HTMLDivElement | null = null;
|
||||||
_cancelNextChange: boolean = false;
|
_cancelNextChange: boolean = false;
|
||||||
|
|
||||||
constructor(props: InputJsonInternalProps) {
|
constructor(props: InputJsonProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this._keyEvent = "keyboard";
|
this._keyEvent = "keyboard";
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -158,7 +156,6 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const t = this.props.t;
|
|
||||||
const {showMessage} = this.state;
|
const {showMessage} = this.state;
|
||||||
const style = {} as {maxHeight?: number};
|
const style = {} as {maxHeight?: number};
|
||||||
if (this.props.maxHeight) {
|
if (this.props.maxHeight) {
|
||||||
@@ -167,9 +164,7 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
|
|||||||
|
|
||||||
return <div className="JSONEditor" onPointerDown={this.onPointerDown} aria-hidden="true">
|
return <div className="JSONEditor" onPointerDown={this.onPointerDown} aria-hidden="true">
|
||||||
<div className={classnames("JSONEditor__message", {"JSONEditor__message--on": showMessage})}>
|
<div className={classnames("JSONEditor__message", {"JSONEditor__message--on": showMessage})}>
|
||||||
<Trans t={t}>
|
Press <kbd>ESC</kbd> to lose focus
|
||||||
Press <kbd>ESC</kbd> to lose focus
|
|
||||||
</Trans>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={classnames("codemirror-container", this.props.className)}
|
className={classnames("codemirror-container", this.props.className)}
|
||||||
@@ -179,6 +174,3 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const InputJson = withTranslation()(InputJsonInternal);
|
|
||||||
export default InputJson;
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default class InputMultiInput extends React.Component<InputMultiInputProp
|
|||||||
const radios = options.map(([val, label])=> {
|
const radios = options.map(([val, label])=> {
|
||||||
return <label
|
return <label
|
||||||
key={val}
|
key={val}
|
||||||
className={classnames("maputnik-button", "maputnik-radio-as-button", {"maputnik-button-selected": val === selectedValue})}
|
className={classnames("maputnik-radio-as-button", {"maputnik-button-selected": val === selectedValue})}
|
||||||
>
|
>
|
||||||
<input type="radio"
|
<input type="radio"
|
||||||
name={this.props.name}
|
name={this.props.name}
|
||||||
@@ -37,3 +37,5 @@ export default class InputMultiInput extends React.Component<InputMultiInputProp
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -19,10 +19,7 @@ type InputNumberState = {
|
|||||||
editing: boolean
|
editing: boolean
|
||||||
editingRange?: boolean
|
editingRange?: boolean
|
||||||
value?: number
|
value?: number
|
||||||
/**
|
dirtyValue?: number
|
||||||
* This is the value that is currently being edited. It can be an invalid value.
|
|
||||||
*/
|
|
||||||
dirtyValue?: number | string | undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class InputNumber extends React.Component<InputNumberProps, InputNumberState> {
|
export default class InputNumber extends React.Component<InputNumberProps, InputNumberState> {
|
||||||
@@ -41,7 +38,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(props: Readonly<InputNumberProps>, state: InputNumberState) {
|
static getDerivedStateFromProps(props: InputNumberProps, state: InputNumberState) {
|
||||||
if (!state.editing && props.value !== state.value) {
|
if (!state.editing && props.value !== state.value) {
|
||||||
return {
|
return {
|
||||||
value: props.value,
|
value: props.value,
|
||||||
@@ -69,7 +66,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
dirtyValue: newValue === "" ? undefined : newValue,
|
dirtyValue: newValue === "" ? undefined : value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +125,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
|
|||||||
// for example we might go from 13 to 13.23, however because we know
|
// for example we might go from 13 to 13.23, however because we know
|
||||||
// that came from a keyboard event we always want to increase by a
|
// that came from a keyboard event we always want to increase by a
|
||||||
// single step value.
|
// single step value.
|
||||||
if (value < +this.state.dirtyValue!) {
|
if (value < this.state.dirtyValue!) {
|
||||||
value = this.state.value! - step;
|
value = this.state.value! - step;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -244,3 +241,5 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -30,3 +30,5 @@ export default class InputSelect extends React.Component<InputSelectProps> {
|
|||||||
</select>
|
</select>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ export default class SpecField extends React.Component<SpecFieldProps> {
|
|||||||
value: this.props.value,
|
value: this.props.value,
|
||||||
default: this.props.fieldSpec?.default,
|
default: this.props.fieldSpec?.default,
|
||||||
name: this.props.fieldName,
|
name: this.props.fieldName,
|
||||||
"data-wd-key": "spec-field-input:" + this.props.fieldName,
|
|
||||||
onChange: (newValue: number | undefined | (string | number | undefined)[]) => this.props.onChange!(this.props.fieldName, newValue),
|
onChange: (newValue: number | undefined | (string | number | undefined)[]) => this.props.onChange!(this.props.fieldName, newValue),
|
||||||
'aria-label': this.props['aria-label'],
|
'aria-label': this.props['aria-label'],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export default class InputString extends React.Component<InputStringProps, Input
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(props: Readonly<InputStringProps>, state: InputStringState) {
|
static getDerivedStateFromProps(props: InputStringProps, state: InputStringState) {
|
||||||
if (!state.editing) {
|
if (!state.editing) {
|
||||||
return {
|
return {
|
||||||
value: props.value
|
value: props.value
|
||||||
@@ -97,3 +97,5 @@ export default class InputString extends React.Component<InputStringProps, Input
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user