mirror of
https://github.com/maputnik/editor.git
synced 2026-09-01 18:07:37 +00:00
Compare commits
2 Commits
v3.1.0
..
a8a221ba15
| Author | SHA1 | Date | |
|---|---|---|---|
| a8a221ba15 | |||
| d6bf0a7a6c |
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
"@babel/preset-env",
|
||||||
|
"@babel/preset-react"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"static-fs",
|
||||||
|
"react-hot-loader/babel",
|
||||||
|
"@babel/plugin-proposal-class-properties",
|
||||||
|
"@babel/transform-runtime"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"test": {
|
||||||
|
"plugins": [
|
||||||
|
["istanbul", {
|
||||||
|
"exclude": ["node_modules/**", "test/**"]
|
||||||
|
}]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"packages": [],
|
||||||
|
"sandboxes": ["/"]
|
||||||
|
}
|
||||||
+1
-2
@@ -1,2 +1 @@
|
|||||||
github: [maplibre]
|
custom: "https://maputnik.github.io/donate"
|
||||||
open_collective: maplibre
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ assignees: ''
|
|||||||
|
|
||||||
<!-- Thanks for your feedback! Please complete the following information: -->
|
<!-- Thanks for your feedback! Please complete the following information: -->
|
||||||
|
|
||||||
**Maputnik version**:<!-- e.g v1.7.0, main -->
|
**Maputnik version**:<!-- e.g v1.7.0, master -->
|
||||||
**Browser**:
|
**Browser**:
|
||||||
**OS**:<!-- (Windows, macOS, Linux) -->
|
**OS**:<!-- (Windows, macOS, Linux) -->
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
|
||||||
+5
-30
@@ -1,39 +1,14 @@
|
|||||||
# To get started with Dependabot version updates, you'll need to specify which
|
|
||||||
# package ecosystems to update and where the package manifests are located.
|
|
||||||
# Please see the documentation for all configuration options:
|
|
||||||
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
|
||||||
|
|
||||||
version: 2
|
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
|
||||||
groups:
|
|
||||||
vitest:
|
|
||||||
patterns:
|
|
||||||
- "*vitest*"
|
|
||||||
react:
|
|
||||||
patterns:
|
|
||||||
- "*react*"
|
|
||||||
cooldown:
|
|
||||||
default-days: 5
|
|
||||||
semver-major-days: 5
|
|
||||||
semver-minor-days: 3
|
|
||||||
semver-patch-days: 3
|
|
||||||
include:
|
|
||||||
- "*"
|
|
||||||
exclude:
|
|
||||||
- "@maplibre/*"
|
|
||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "daily"
|
||||||
cooldown:
|
open-pull-requests-limit: 2
|
||||||
default-days: 3
|
versioning-strategy: increase
|
||||||
# no semver support for github-actions
|
|
||||||
# => no specific configuration for this
|
|
||||||
include:
|
|
||||||
- "*"
|
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
name: Automerge Dependabot
|
|
||||||
|
|
||||||
on: pull_request
|
|
||||||
|
|
||||||
permissions: write-all
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
dependabot:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }}
|
|
||||||
steps:
|
|
||||||
- name: Dependabot metadata
|
|
||||||
id: metadata
|
|
||||||
uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
|
|
||||||
with:
|
|
||||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
|
||||||
- name: Approve Dependabot PRs
|
|
||||||
run: gh pr review --approve "$PR_URL"
|
|
||||||
env:
|
|
||||||
PR_URL: ${{github.event.pull_request.html_url}}
|
|
||||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
|
||||||
- name: Enable auto-merge for Dependabot PRs
|
|
||||||
run: gh pr merge --auto --squash "$PR_URL"
|
|
||||||
env:
|
|
||||||
PR_URL: ${{github.event.pull_request.html_url}}
|
|
||||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
|
||||||
+125
-118
@@ -2,153 +2,160 @@ name: ci
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main ]
|
branches: [ master ]
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [ master ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
build-node:
|
# post a comment linking to codesandbox with the current branch
|
||||||
name: "build on ${{ matrix.os }}"
|
# meta-demo-comment:
|
||||||
|
# name: meta/demo-comment
|
||||||
|
# runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
# if: ${{ github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
|
# steps:
|
||||||
|
# - uses: unsplash/comment-on-pr@v1.2.0
|
||||||
|
# env:
|
||||||
|
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# with:
|
||||||
|
# msg: "Demo: <https://codesandbox.io/embed/github/${{ github.repository }}/tree/${{ github.head_ref }}?view=preview>"
|
||||||
|
|
||||||
|
build-docker:
|
||||||
|
name: build/docker
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
os: [ubuntu-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@v4
|
||||||
with: { persist-credentials: false }
|
- run: docker build -t docker.pkg.github.com/maputnik/editor/editor:master .
|
||||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
||||||
with:
|
|
||||||
node-version-file: '.nvmrc'
|
|
||||||
- run: npm ci
|
|
||||||
- run: npm run build
|
|
||||||
- run: npm run lint
|
|
||||||
- run: npm run lint-css
|
|
||||||
|
|
||||||
|
# build the editor
|
||||||
|
build-node:
|
||||||
|
name: "build/node@${{ matrix.node-version }} (${{ matrix.os }})"
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
|
node-version: [18.x]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
- uses: actions/cache@v1
|
||||||
|
with:
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-node-
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run build
|
||||||
|
|
||||||
|
|
||||||
build-artifacts:
|
build-artifacts:
|
||||||
name: "build artifacts"
|
name: "build/artifacts (${{ matrix.os }})"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.os }}
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
node-version: [18.x]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@v4
|
||||||
with: { persist-credentials: false }
|
- uses: actions/setup-node@v1
|
||||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
with:
|
||||||
with:
|
node-version: ${{ matrix.node-version }}
|
||||||
node-version-file: '.nvmrc'
|
- uses: actions/cache@v1
|
||||||
- run: npm ci
|
with:
|
||||||
- run: npm run build
|
path: ~/.npm
|
||||||
- name: artifacts/maputnik
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
restore-keys: |
|
||||||
with:
|
${{ runner.os }}-node-
|
||||||
name: maputnik
|
- run: npm ci
|
||||||
path: dist
|
- run: npm run build
|
||||||
|
- run: npm run build-storybook
|
||||||
|
- name: artifacts/editor
|
||||||
|
uses: actions/upload-artifact@v1
|
||||||
|
with:
|
||||||
|
name: editor
|
||||||
|
path: build/build
|
||||||
|
- run: npm run profiling-build
|
||||||
|
- name: artifacts/editor-profiling
|
||||||
|
uses: actions/upload-artifact@v1
|
||||||
|
with:
|
||||||
|
name: editor-profiling
|
||||||
|
path: build/profiling
|
||||||
|
- name: artifacts/storybook
|
||||||
|
uses: actions/upload-artifact@v1
|
||||||
|
with:
|
||||||
|
name: storybook
|
||||||
|
path: build/storybook
|
||||||
|
|
||||||
# Build and upload desktop CLI artifacts
|
# Build and upload desktop CLI artifacts
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
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/
|
||||||
|
|
||||||
unit-tests:
|
- name: Artifacts/windows
|
||||||
name: "Unit tests"
|
uses: actions/upload-artifact@v1
|
||||||
runs-on: ubuntu-latest
|
with:
|
||||||
permissions:
|
name: maputnik-windows
|
||||||
contents: read
|
path: ./src/github.com/maputnik/desktop/bin/windows/
|
||||||
|
|
||||||
|
cypress-run:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
browser: [chrome, firefox]
|
||||||
|
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4
|
||||||
with: { persist-credentials: false }
|
|
||||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
||||||
with:
|
|
||||||
node-version-file: '.nvmrc'
|
|
||||||
- run: npm ci
|
|
||||||
- run: npm run test-unit-ci
|
|
||||||
- name: Upload coverage reports to Codecov
|
|
||||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
|
||||||
with:
|
|
||||||
files: ${{ github.workspace }}/coverage/coverage-final.json
|
|
||||||
verbose: true
|
|
||||||
|
|
||||||
e2e-tests:
|
|
||||||
name: "E2E tests using chrome"
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with: { persist-credentials: false }
|
|
||||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
||||||
with:
|
|
||||||
node-version-file: '.nvmrc'
|
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- name: Cypress run
|
- name: Cypress run
|
||||||
uses: cypress-io/github-action@fa4a118725a8f001170d49631ea89e5d66fee626 # v7.4.1
|
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
|
|
||||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
|
||||||
with:
|
|
||||||
files: ${{ github.workspace }}/.nyc_output/out.json
|
|
||||||
verbose: true
|
|
||||||
|
|
||||||
e2e-tests-docker:
|
|
||||||
name: "E2E tests using chrome and docker"
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with: { persist-credentials: false }
|
|
||||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
||||||
with:
|
|
||||||
node-version-file: '.nvmrc'
|
|
||||||
- run: npm ci
|
|
||||||
- name: Cypress run
|
|
||||||
uses: cypress-io/github-action@fa4a118725a8f001170d49631ea89e5d66fee626 # v7.4.1
|
|
||||||
with:
|
|
||||||
build: docker build -t maputnik .
|
|
||||||
start: docker run --rm --network host maputnik --port=8888
|
|
||||||
browser: chrome
|
|
||||||
- name: Upload coverage reports to Codecov
|
|
||||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
|
||||||
with:
|
|
||||||
files: ${{ github.workspace }}/.nyc_output/out.json
|
|
||||||
verbose: true
|
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
# For most projects, this workflow file will not need changing; you simply need
|
|
||||||
# to commit it to your repository.
|
|
||||||
#
|
|
||||||
# You may wish to alter this file to override the set of languages analyzed,
|
|
||||||
# or to provide custom queries or build logic.
|
|
||||||
#
|
|
||||||
# ******** NOTE ********
|
|
||||||
# We have attempted to detect the languages in your repository. Please check
|
|
||||||
# the `language` matrix defined below to confirm you have the correct set of
|
|
||||||
# supported CodeQL languages.
|
|
||||||
#
|
|
||||||
name: "CodeQL"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ main ]
|
|
||||||
pull_request:
|
|
||||||
# The branches below must be a subset of the branches above
|
|
||||||
branches: [ main ]
|
|
||||||
schedule:
|
|
||||||
- cron: '17 0 * * 6'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
analyze:
|
|
||||||
name: Analyze
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
contents: read
|
|
||||||
security-events: write
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
language: [ 'javascript' ]
|
|
||||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
|
||||||
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
|
||||||
with:
|
|
||||||
languages: ${{ matrix.language }}
|
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
|
||||||
# By default, queries listed here will override any specified in a config file.
|
|
||||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
|
||||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
|
||||||
- name: Autobuild
|
|
||||||
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
|
||||||
# 📚 https://git.io/JvXDl
|
|
||||||
|
|
||||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
|
||||||
# and modify them (or add more) to build your code if your project
|
|
||||||
# uses a compiled language
|
|
||||||
|
|
||||||
#- run: |
|
|
||||||
# make bootstrap
|
|
||||||
# make release
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
name: Create bump version PR
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
version:
|
|
||||||
description: Version to change to.
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
bump-version-pr:
|
|
||||||
name: Bump version PR
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
ref: main
|
|
||||||
|
|
||||||
- name: Use Node.js from nvmrc
|
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
||||||
with:
|
|
||||||
node-version-file: ".nvmrc"
|
|
||||||
|
|
||||||
- name: Bump version
|
|
||||||
run: |
|
|
||||||
npm version --commit-hooks false --git-tag-version false ${{ inputs.version }}
|
|
||||||
./build/bump-version-changelog.js ${{ inputs.version }}
|
|
||||||
|
|
||||||
- name: Create Pull Request
|
|
||||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
|
||||||
with:
|
|
||||||
commit-message: Bump version to ${{ inputs.version }}
|
|
||||||
branch: bump-version-to-${{ inputs.version }}
|
|
||||||
title: Bump version to ${{ inputs.version }}
|
|
||||||
@@ -2,54 +2,26 @@ name: deploy
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [ master ]
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy-pages:
|
|
||||||
name: deploy/pages
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
if: ${{ github.event_name == 'push' }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with: { persist-credentials: false }
|
|
||||||
|
|
||||||
- name: Use Node.js from nvmrc
|
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
||||||
with:
|
|
||||||
node-version-file: '.nvmrc'
|
|
||||||
|
|
||||||
- name: Install
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
- name: Upload to GitHub Pages
|
|
||||||
uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4.1.0
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
publish_dir: dist
|
|
||||||
|
|
||||||
# publish docker to GitHub registry
|
# publish docker to GitHub registry
|
||||||
deploy-docker:
|
deploy-docker:
|
||||||
name: deploy/docker
|
name: deploy/docker
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
if: ${{ github.event_name == 'push' }}
|
if: ${{ github.event_name == 'push' }}
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Login to GitHub Container Registry
|
- uses: actions/checkout@v4
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
- run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u orangemug --password-stdin
|
||||||
with:
|
- run: docker build -t docker.pkg.github.com/maputnik/editor/editor:master .
|
||||||
registry: ghcr.io
|
- run: docker push docker.pkg.github.com/maputnik/editor/editor:master
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
- run: docker build -t ghcr.io/maplibre/maputnik:main .
|
|
||||||
- run: docker push ghcr.io/maplibre/maputnik:main
|
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
name: Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ main ]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release-check:
|
|
||||||
name: Check if version changed
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
ref: main
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Use Node.js from nvmrc
|
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
||||||
with:
|
|
||||||
node-version-file: ".nvmrc"
|
|
||||||
|
|
||||||
- name: Check if version has been updated
|
|
||||||
id: check
|
|
||||||
uses: EndBug/version-check@095362f3cd50f690c8fa0e6afeea81834bd8d320 # latest
|
|
||||||
|
|
||||||
outputs:
|
|
||||||
publish: ${{ steps.check.outputs.changed }}
|
|
||||||
|
|
||||||
release:
|
|
||||||
name: Release
|
|
||||||
needs: release-check
|
|
||||||
if: ${{ needs.release-check.outputs.publish == 'true' }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
ref: main
|
|
||||||
|
|
||||||
- name: Use Node.js from nvmrc
|
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
||||||
with:
|
|
||||||
node-version-file: ".nvmrc"
|
|
||||||
registry-url: "https://registry.npmjs.org"
|
|
||||||
|
|
||||||
- name: Set up Go for desktop build
|
|
||||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
|
||||||
with:
|
|
||||||
go-version: ^1.23.x
|
|
||||||
cache-dependency-path: desktop/go.sum
|
|
||||||
id: go
|
|
||||||
|
|
||||||
- name: Get version
|
|
||||||
id: package-version
|
|
||||||
uses: martinbeentjes/npm-get-version-action@3cf273023a0dda27efcd3164bdfb51908dd46a5b # v1.3.1
|
|
||||||
|
|
||||||
- name: Install
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: |
|
|
||||||
npm run build-desktop
|
|
||||||
|
|
||||||
- name: Tag commit and push
|
|
||||||
id: tag_version
|
|
||||||
uses: mathieudutour/github-tag-action@a22cf08638b34d5badda920f9daf6e72c477b07b # v6.2
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
custom_tag: ${{ steps.package-version.outputs.current-version }}
|
|
||||||
|
|
||||||
- name: Create Archives
|
|
||||||
run: |
|
|
||||||
zip -r "desktop-${{ steps.package-version.outputs.current-version }}" desktop/bin/
|
|
||||||
|
|
||||||
- name: Build Release Notes
|
|
||||||
id: release_notes
|
|
||||||
run: |
|
|
||||||
RELEASE_NOTES_PATH="${PWD}/release_notes.txt"
|
|
||||||
./build/release-notes.js > ${RELEASE_NOTES_PATH}
|
|
||||||
echo "release_notes=${RELEASE_NOTES_PATH}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Create GitHub Release
|
|
||||||
id: create_regular_release
|
|
||||||
uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
tag: ${{ steps.tag_version.outputs.new_tag }}
|
|
||||||
name: ${{ steps.tag_version.outputs.new_tag }}
|
|
||||||
bodyFile: ${{ steps.release_notes.outputs.release_notes }}
|
|
||||||
artifacts: "desktop-${{ steps.package-version.outputs.current-version }}.zip"
|
|
||||||
allowUpdates: true
|
|
||||||
draft: false
|
|
||||||
prerelease: false
|
|
||||||
+1
-11
@@ -14,7 +14,6 @@ lib-cov
|
|||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
# Coverage directory used by tools like istanbul
|
||||||
coverage
|
coverage
|
||||||
.nyc_output
|
|
||||||
|
|
||||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
.grunt
|
.grunt
|
||||||
@@ -33,14 +32,5 @@ node_modules
|
|||||||
public
|
public
|
||||||
/errorShots
|
/errorShots
|
||||||
/old
|
/old
|
||||||
|
/build
|
||||||
/cypress/screenshots
|
/cypress/screenshots
|
||||||
/dist/
|
|
||||||
/desktop/version.go
|
|
||||||
|
|
||||||
# IDE
|
|
||||||
.vscode/
|
|
||||||
.idea/
|
|
||||||
|
|
||||||
# Window metadata files
|
|
||||||
/desktop/winres/winres.json
|
|
||||||
/desktop/*.syso
|
|
||||||
|
|||||||
-18
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"all": true,
|
|
||||||
"extends": "@istanbuljs/nyc-config-typescript",
|
|
||||||
"check-coverage": false,
|
|
||||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
|
||||||
"exclude": [
|
|
||||||
"cypress/**/*.*",
|
|
||||||
"**/*.d.ts",
|
|
||||||
"**/*.cy.tsx",
|
|
||||||
"**/*.cy.ts",
|
|
||||||
"./coverage/**",
|
|
||||||
"./cypress/**",
|
|
||||||
"./dist/**",
|
|
||||||
"node_modules"
|
|
||||||
],
|
|
||||||
"report-dir": "coverage",
|
|
||||||
"reporter": ["json", "lcov", "json-summary"]
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# See https://pre-commit.com for more information
|
|
||||||
# See https://pre-commit.com/hooks.html for more hooks
|
|
||||||
|
|
||||||
ci:
|
|
||||||
autoupdate_schedule: monthly
|
|
||||||
|
|
||||||
repos:
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
||||||
rev: v6.0.0
|
|
||||||
hooks:
|
|
||||||
- id: check-added-large-files
|
|
||||||
- id: check-executables-have-shebangs
|
|
||||||
- id: check-json
|
|
||||||
exclude: 'tsconfig(\.node)?\.json'
|
|
||||||
- id: check-shebang-scripts-are-executable
|
|
||||||
- id: check-symlinks
|
|
||||||
- id: check-toml
|
|
||||||
- id: check-yaml
|
|
||||||
args: [ --allow-multiple-documents ]
|
|
||||||
- id: destroyed-symlinks
|
|
||||||
- id: end-of-file-fixer
|
|
||||||
- id: mixed-line-ending
|
|
||||||
args: [ --fix=lf ]
|
|
||||||
- id: trailing-whitespace
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
const rules = require('../config/webpack.rules');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
stories: ['../stories/**/*.stories.js'],
|
||||||
|
addons: [
|
||||||
|
'@storybook/addon-actions',
|
||||||
|
'@storybook/addon-links',
|
||||||
|
'@storybook/addon-a11y/register',
|
||||||
|
'@storybook/addon-storysource',
|
||||||
|
],
|
||||||
|
webpackFinal: async config => {
|
||||||
|
// do mutation to the config
|
||||||
|
console.log("config.module", config.module);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
...rules,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { addons } from '@storybook/addons';
|
||||||
|
import { themes } from '@storybook/theming';
|
||||||
|
import theme from './maputnik.theme';
|
||||||
|
|
||||||
|
addons.setConfig({
|
||||||
|
theme: theme,
|
||||||
|
});
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { create } from '@storybook/theming/create';
|
||||||
|
|
||||||
|
export default create({
|
||||||
|
base: 'light',
|
||||||
|
|
||||||
|
brandTitle: 'Maputnik',
|
||||||
|
brandUrl: 'https://github.com/maputnik/editor',
|
||||||
|
});
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
Maputnik is a MapLibre style editor written using React and TypeScript.
|
|
||||||
|
|
||||||
To get started, install all npm packages:
|
|
||||||
|
|
||||||
```
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
Verify code correctness by running ESLint:
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run lint
|
|
||||||
```
|
|
||||||
|
|
||||||
Or try fixing lint issues with:
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run lint -- --fix
|
|
||||||
```
|
|
||||||
|
|
||||||
The project type checked and built with:
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
To run the tests make sure that xvfb is installed:
|
|
||||||
|
|
||||||
```
|
|
||||||
apt install xvfb
|
|
||||||
```
|
|
||||||
|
|
||||||
Run the development server in the background with Vite:
|
|
||||||
|
|
||||||
```
|
|
||||||
nohup npm run start &
|
|
||||||
```
|
|
||||||
|
|
||||||
Then start the Cypress tests with:
|
|
||||||
|
|
||||||
```
|
|
||||||
xvfb-run -a npm run test
|
|
||||||
```
|
|
||||||
|
|
||||||
## Pull Requests
|
|
||||||
|
|
||||||
- Pull requests should update `CHANGELOG.md` with a short description of the change.
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
## main
|
|
||||||
|
|
||||||
### ✨ Features and improvements
|
|
||||||
- _...Add new stuff here..._
|
|
||||||
|
|
||||||
### 🐞 Bug fixes
|
|
||||||
- _...Add new stuff here..._
|
|
||||||
|
|
||||||
## 3.1.0
|
|
||||||
|
|
||||||
### ✨ Features and improvements
|
|
||||||
- Added translation to "Links" in debug modal
|
|
||||||
- Add support for hillshade's color arrays and relief-color elevation expression
|
|
||||||
- Change layers icons to make them a bit more distinct
|
|
||||||
- Remove `@mdi` packages in favor of `react-icons`
|
|
||||||
- Add ability to control the projection of the map - either globe or mercator
|
|
||||||
- Add markdown support for doc related to the style-spec fields
|
|
||||||
- Added global state modal to allow editing the global state
|
|
||||||
- Added color highlight for problematic properties
|
|
||||||
- Upgraded codemirror from version 5 to version 6
|
|
||||||
- Add code editor to allow editing the entire style
|
|
||||||
- Add support for sprite object in setting modal
|
|
||||||
- Set the correct map view when opening a new style on an empty map
|
|
||||||
- Allow root-relative urls in the stylefile
|
|
||||||
|
|
||||||
### 🐞 Bug fixes
|
|
||||||
|
|
||||||
- Improved the local style open modal and local file upload.
|
|
||||||
- Fixed the Expression editor (for long expressions) being able to be float under other components further down
|
|
||||||
- Fixed an issue when clicking on a popup and then clicking on the map again
|
|
||||||
- Fix modal close button position
|
|
||||||
- Fixed an issue with the generation of translations
|
|
||||||
- Fix missing spec info when clicking next to a property
|
|
||||||
- Fix Firefox open file that stopped working due to react upgrade
|
|
||||||
- Fix issue with missing bottom error panel
|
|
||||||
- Fixed headers in left panes (Layers list and Layer editor) to remain visible when scrolling
|
|
||||||
- Fix error when using a source from localhost
|
|
||||||
- Fix an issue with scrolling when using the code editor
|
|
||||||
|
|
||||||
## 3.0.0
|
|
||||||
|
|
||||||
### ✨ Features and improvements
|
|
||||||
- Fix radio/delete filter buttons styling regression
|
|
||||||
- Add german translation
|
|
||||||
- Use same version number for web and desktop versions
|
|
||||||
- Add scheme type options for vector/raster tile
|
|
||||||
- Add `tileSize` field for raster and raster-dem tile sources
|
|
||||||
- Update Protomaps Light gallery style to v4
|
|
||||||
- Add support to edit local files on the file system if supported by the browser
|
|
||||||
- Upgrade to MapLibre LG JS v5
|
|
||||||
- Upgrade Vite 6 and Cypress 14 ([#970](https://github.com/maplibre/maputnik/pull/970))
|
|
||||||
- Upgrade OpenLayers from v6 to v10
|
|
||||||
- When loading a style into localStorage that causes a QuotaExceededError, purge localStorage and retry
|
|
||||||
- Remove react-autobind dependency
|
|
||||||
- Remove usage of legacy `childContextTypes` API
|
|
||||||
- Refactor Field components to use arrow function syntax
|
|
||||||
- Replace react-autocomplete with Downshift in the autocomplete component
|
|
||||||
- Add LocationIQ as supported map provider with access token field and gallery style
|
|
||||||
- Use maputnik go binary for the docker image to allow file watching
|
|
||||||
- Revmove support for `debug` and `localport` url parameters
|
|
||||||
- Replace react-sortable-hoc with dnd-kit to avoid react console warnings and also use a maintained library
|
|
||||||
|
|
||||||
### 🐞 Bug fixes
|
|
||||||
|
|
||||||
- Fix incorrect handing of network error response (#944)
|
|
||||||
- Show an error when adding a layer with a duplicate ID
|
|
||||||
- Replace deprecated `ReactDOM.render` usage with `createRoot` and drop the
|
|
||||||
`DOMNodeRemoved` cleanup hack
|
|
||||||
|
|
||||||
## 2.1.1
|
|
||||||
|
|
||||||
### ✨ Features and improvements
|
|
||||||
|
|
||||||
- Add GitHub workflows for releasing new versions
|
|
||||||
- Update desktop build to pull from this repo (#922)
|
|
||||||
|
|
||||||
## 2.0.0
|
|
||||||
|
|
||||||
- Update MapLibre to version 4 (#872)
|
|
||||||
- Start continuous deployment of maputnik website
|
|
||||||
|
|
||||||
## 1.7.0
|
|
||||||
|
|
||||||
- See release notes at https://maputnik.github.io/blog/2020/04/23/release-v1.7.0
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
# Contributor Covenant
|
|
||||||
[](https://github.com/maplibre/maplibre/blob/main/CODE_OF_CONDUCT.md)
|
|
||||||
+16
-8
@@ -1,14 +1,22 @@
|
|||||||
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
|
||||||
|
# TODO: we should also do a npm run test here (needs more dependencies)
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm ci
|
RUN npm run build
|
||||||
RUN CGO_ENABLED=1 GOOS=linux npm run build-linux
|
|
||||||
|
|
||||||
FROM alpine:latest
|
#---------------------------------------------------------------------------
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=builder /maputnik/desktop/bin/linux ./
|
# Create a clean python-based image with just the build results
|
||||||
ENTRYPOINT ["/app/maputnik"]
|
FROM python:3-slim
|
||||||
|
WORKDIR /maputnik
|
||||||
|
|
||||||
|
COPY --from=builder /maputnik/build/build .
|
||||||
|
|
||||||
|
EXPOSE 8888
|
||||||
|
CMD python -m http.server 8888
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2015 Lukas Martinelli
|
Copyright (c) 2015 Lukas Martinelli
|
||||||
Copyright (c) 2024 MapLibre contributors
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
@@ -20,3 +19,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<img width="200" alt="Maputnik logo" src="https://cdn.jsdelivr.net/gh/maputnik/design/logos/logo-color.png" />
|
<img width="200" alt="Maputnik logo" src="https://cdn.jsdelivr.net/gh/maputnik/design/logos/logo-color.png" />
|
||||||
|
|
||||||
# Maputnik
|
# Maputnik
|
||||||
[][github-action-ci]
|
[][github-action-ci]
|
||||||
[][license]
|
[][license]
|
||||||
|
|
||||||
[github-action-ci]: https://github.com/maplibre/maputnik/actions?query=workflow%3Aci
|
[github-action-ci]: https://github.com/maputnik/editor/actions?query=workflow%3Aci
|
||||||
[license]: https://tldrlegal.com/license/mit-license
|
[license]: https://tldrlegal.com/license/mit-license
|
||||||
|
|
||||||
A free and open visual editor for the [MapLibre GL styles](https://maplibre.org/maplibre-style-spec/)
|
A free and open visual editor for the [MapLibre GL styles](https://maplibre.org/maplibre-style-spec/)
|
||||||
@@ -13,40 +13,33 @@ targeted at developers and map designers.
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
- :link: Design your maps online at **<https://www.maplibre.org/maputnik/>** (all in local storage)
|
- :link: Design your maps online at **<https://maputnik.github.io/editor/>** (all in local storage)
|
||||||
- :link: Use the [Maputnik CLI](https://github.com/maplibre/maputnik/wiki/Maputnik-CLI) for local style development
|
- :link: Use the [Maputnik CLI](https://github.com/maputnik/editor/wiki/Maputnik-CLI) for local style development
|
||||||
- In a Docker, run this command and browse to http://localhost:8888, Ctrl+C to stop the server.
|
- In a Docker, run this command and browse to http://localhost:8888, Ctrl+C to stop the server.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -it --rm -p 8888:8000 ghcr.io/maplibre/maputnik:main
|
docker run -it --rm -p 8888:8888 maputnik/editor
|
||||||
```
|
```
|
||||||
|
|
||||||
To see the CLI options (for example file watching or style serving) run:
|
## Donations
|
||||||
```bash
|
Mapbox has built one of the best and most amazing OSS ecosystems. A key component to ensure its longevity and independence is an OSS map designer.
|
||||||
docker run -it --rm -p 8888:8000 ghcr.io/maplibre/maputnik:main --help
|
If you or your organisation has seen value from Maputnik, please consider donating at <https://maputnik.github.io/donate>
|
||||||
```
|
|
||||||
You might need to mount a volume (`-v`) to be able to use these options.
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
The documentation can be found in the [Wiki](https://github.com/maplibre/maputnik/wiki). You are welcome to collaborate!
|
The documentation can be found in the [Wiki](https://github.com/maputnik/editor/wiki). You are welcome to collaborate!
|
||||||
|
|
||||||
- :link: **Study the [Maputnik Wiki](https://github.com/maplibre/maputnik/wiki)**
|
- :link: **Study the [Maputnik Wiki](https://github.com/maputnik/editor/wiki)**
|
||||||
- :video_camera: Design a map from Scratch https://youtu.be/XoDh0gEnBQo
|
- :video_camera: Design a map from Scratch https://youtu.be/XoDh0gEnBQo
|
||||||
|
|
||||||
[](https://youtu.be/XoDh0gEnBQo)
|
[](https://youtu.be/XoDh0gEnBQo)
|
||||||
|
|
||||||
## Develop
|
## Develop
|
||||||
|
|
||||||
Maputnik is written in typescript and is using [React](https://github.com/facebook/react) and [MapLibre GL JS](https://maplibre.org/projects/maplibre-gl-js/).
|
Maputnik is written in ES6 and is using [React](https://github.com/facebook/react) and [MapLibre GL JS](https://maplibre.org/projects/maplibre-gl-js/).
|
||||||
|
|
||||||
We ensure building and developing Maputnik works with the [current active LTS Node.js version and above](https://github.com/nodejs/Release#release-schedule).
|
We ensure building and developing Maputnik works with the [current active LTS Node.js version and above](https://github.com/nodejs/Release#release-schedule).
|
||||||
|
|
||||||
Check out our [Internationalization guide](./src/locales/README.md) for UI text related changes.
|
|
||||||
|
|
||||||
### Getting Involved
|
|
||||||
Join the #maplibre or #maputnik slack channel at OSMUS: get an invite at https://slack.openstreetmap.us/ Read the the below guide in order to get familiar with how we do things around here.
|
|
||||||
|
|
||||||
Install the deps, start the dev server and open the web browser on `http://localhost:8888/`.
|
Install the deps, start the dev server and open the web browser on `http://localhost:8888/`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -56,14 +49,18 @@ npm install
|
|||||||
npm run start
|
npm run start
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want Maputnik to be accessible externally use the [`--host` option](https://vitejs.dev/config/server-options.html#server-host):
|
If you want Maputnik to be accessible externally use the [`--host` option](https://webpack.js.org/configuration/dev-server/#devserverhost):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# start externally accessible dev server
|
# start externally accessible dev server
|
||||||
npm run start -- --host 0.0.0.0
|
npm run start -- --host 0.0.0.0
|
||||||
```
|
```
|
||||||
|
|
||||||
The build process will watch for changes to the filesystem, rebuild and autoreload the editor.
|
The build process will watch for changes to the filesystem, rebuild and autoreload the editor. However note this from the [webpack-dev-server docs](https://webpack.js.org/configuration/dev-server/):
|
||||||
|
|
||||||
|
> webpack uses the file system to get notified of file changes. In some cases this does not work. For example, when using Network File System (NFS). Vagrant also has a lot of problems with this. ([snippet source](https://webpack.js.org/configuration/dev-server/#devserverwatchoptions-))
|
||||||
|
|
||||||
|
To enable polling add `export WEBPACK_DEV_SERVER_POLLING=1` to your environment.
|
||||||
|
|
||||||
```
|
```
|
||||||
npm run build
|
npm run build
|
||||||
@@ -74,14 +71,14 @@ Lint the JavaScript code.
|
|||||||
```
|
```
|
||||||
# run linter
|
# run linter
|
||||||
npm run lint
|
npm run lint
|
||||||
npm run lint-css
|
npm run lint-styles
|
||||||
npm run sort-styles
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
For E2E testing we use [Cypress](https://www.cypress.io/)
|
For E2E testing we use [Cypress](https://www.cypress.io/)
|
||||||
|
|
||||||
[Cypress](https://www.cypress.io/) doesn't start a server so you'll need to start one manually by running `npm run start`.
|
[Cypress](https://www.cypress.io/) doesn't starts a server so you'll need to start one manually by running `npm run start`.
|
||||||
|
|
||||||
Now open a terminal and run the following using *chrome*:
|
Now open a terminal and run the following using *chrome*:
|
||||||
|
|
||||||
@@ -101,23 +98,68 @@ You can also see the tests as they run or select which suites to run by executin
|
|||||||
npm run cy:open
|
npm run cy:open
|
||||||
```
|
```
|
||||||
|
|
||||||
## Release process
|
|
||||||
|
|
||||||
1. Review [`CHANGELOG.md`](/CHANGELOG.md)
|
## Related Projects
|
||||||
- Double-check that all changes included in the release are appropriately documented.
|
|
||||||
- To-be-released changes should be under the "main" header.
|
|
||||||
- Commit any final changes to the changelog.
|
|
||||||
2. Run [Create bump version PR](https://github.com/maplibre/maputnik/actions/workflows/create-bump-version-pr.yml) by manual workflow dispatch and set the version number in the input. This will create a PR that changes the changelog and `package.json` file to review and merge.
|
|
||||||
3. Once merged, an automatic process will kick in and creates a GitHub release and uploads release assets.
|
|
||||||
|
|
||||||
|
- [maputnik-dev-server](https://github.com/nycplanning/labs-maputnik-dev-server) - An express.js server that allows for quickly loading the style from any mapboxGL map into mapuntnik.
|
||||||
|
|
||||||
## Sponsors
|
## Sponsors
|
||||||
|
|
||||||
Thanks to the supporters of the **[Kickstarter campaign](https://www.kickstarter.com/projects/174808720/maputnik-visual-map-editor-for-mapbox-gl)**. This project would not be possible without these commercial and individual sponsors.
|
Thanks to the supporters of the **[Kickstarter campaign](https://www.kickstarter.com/projects/174808720/maputnik-visual-map-editor-for-mapbox-gl)**. This project would not be possible without these commercial and individual sponsors.
|
||||||
You can see this file's history for previous sponsors of the original Maputnik repo.
|
|
||||||
Read more about the MapLibre Sponsorship Program at https://maplibre.org/sponsors/.
|
### Gold
|
||||||
|
|
||||||
|
- [Wemap](https://getwemap.com/)
|
||||||
|
- [Orbicon Informatik](https://www.orbiconinformatik.dk/)
|
||||||
|
- [Terranodo](http://terranodo.io/)
|
||||||
|
|
||||||
|
<a href="https://getwemap.com/">
|
||||||
|
<img width="33%" alt="Wemap" style="display:inline" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/wemap.jpg" />
|
||||||
|
</a>
|
||||||
|
<a href="http://terranodo.io/">
|
||||||
|
<img width="33%" alt="Terranodo" style="display:inline" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/terranodo.png" />
|
||||||
|
</a>
|
||||||
|
<a href="https://www.orbiconinformatik.dk/">
|
||||||
|
<img width="32%" alt="Terranodo" style="display:inline" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/orbicon_informatik.png" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
### Silver
|
||||||
|
|
||||||
|
- [Klokan Technologies](https://www.klokantech.com/)
|
||||||
|
- [Geofabrik](http://www.geofabrik.de/)
|
||||||
|
- [Dreipol](https://www.dreipol.ch/)
|
||||||
|
|
||||||
|
<a href="https://www.klokantech.com/">
|
||||||
|
<img width="18%" alt="Klokan Technologies" style="display:inline-block" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/klokantech.png" />
|
||||||
|
</a>
|
||||||
|
<a href="http://www.geofabrik.de/">
|
||||||
|
<img width="18%" alt="Geofabrik" style="display:inline-block" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/geofabrik.png" />
|
||||||
|
</a>
|
||||||
|
<a href="https://www.dreipol.ch/">
|
||||||
|
<img width="18%" alt="Dreipol" style="display:inline-block" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/dreipol.png" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
### Individuals
|
||||||
|
|
||||||
|
**Influential Stakeholder**
|
||||||
|
|
||||||
|
Alan McConchie, Odi, Mats Norén, Uli [geOps](http://geops.ch/), Helge Fahrnberger ([Toursprung](http://www.toursprung.com/)), Kirusanth Poopalasingam
|
||||||
|
|
||||||
|
**Stakeholder**
|
||||||
|
|
||||||
|
Brian Flood, Vasile Coțovanu, Andreas Kalkbrenner, Christian Mäder, Gregor Wassmann, Lee Armstrong, Rafel, Jon Burgess, Lukas Lehmann, Joachim Ungar, Alois Ackermann, Zsolt Ero, Jordan Meek
|
||||||
|
|
||||||
|
**Supporter**
|
||||||
|
|
||||||
|
Sina Martinelli, Nicholas Doiron, Neil Cawse, Urs42, Benedikt Groß, Manuel Roth, Janko Mihelić, Moritz Stefaner, Sebastian Ahoi, Juerg Uhlmann, Tom Wider, Nadia Panchaud, Oliver Snowden, Stephan Heuel, Tobin Bradley, Adrian Herzog, Antti Lehto, Pascal Mages, Marc Gehling, Imre Samu, Lauri K., Visahavel Parthasarathy, Christophe Waterlot-Buisine, Max Galka, ubahnverleih, Wouter van Dam, Jakob Lobensteiner, Samuel Kurath, Brian Bancroft
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Maputnik is [licensed under MIT](LICENSE) and is Copyright (c) Lukas Martinelli and Maplibre contributors.
|
Maputnik is [licensed under MIT](LICENSE) and is Copyright (c) Lukas Martinelli and contributors.
|
||||||
As contributor please take extra care of not violating any Mapbox trademarks. Do not get inspired by other map studios and make your own decisions for a good style editor.
|
|
||||||
|
**Disclaimer** This is an independent style editor.
|
||||||
|
As contributor please take extra care of not violating any Mapbox trademarks. Do not get inspired by Mapbox Studio and make your own decisions for a good style editor.
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
For an up-to-date policy refer to
|
|
||||||
https://github.com/maplibre/maplibre/blob/main/SECURITY_POLICY.txt
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# Build Scripts
|
|
||||||
|
|
||||||
This folder holds common build scripts used by some of the Github workflows.
|
|
||||||
|
|
||||||
The scripts are borrowed from [maplibre/maplibre-gl-js](https://github.com/maplibre/maplibre-gl-js/tree/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build).
|
|
||||||
|
|
||||||
## Generate Release Notes
|
|
||||||
|
|
||||||
`bump-version-changelog.js` Used to update the changelog with the current notes, and set up a space for new notes
|
|
||||||
|
|
||||||
`release-notes.js` Used to generate release notes when releasing a new version
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This script updates the changelog.md file with the version given in the arguments
|
|
||||||
* It replaces ## main with ## <version>
|
|
||||||
* Removes _...Add new stuff here..._
|
|
||||||
* And adds on top a ## main with add stuff here.
|
|
||||||
*
|
|
||||||
* Copied from maplibre/maplibre-gl-js
|
|
||||||
* https://github.com/maplibre/maplibre-gl-js/blob/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build/release-notes.js
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as fs from "fs";
|
|
||||||
|
|
||||||
const changelogPath = "CHANGELOG.md";
|
|
||||||
let changelog = fs.readFileSync(changelogPath, "utf8");
|
|
||||||
changelog = changelog.replace("## main", `## ${process.argv[2]}`);
|
|
||||||
changelog = changelog.replaceAll("- _...Add new stuff here..._\n", "");
|
|
||||||
changelog = `## main
|
|
||||||
|
|
||||||
### ✨ Features and improvements
|
|
||||||
- _...Add new stuff here..._
|
|
||||||
|
|
||||||
### 🐞 Bug fixes
|
|
||||||
- _...Add new stuff here..._
|
|
||||||
|
|
||||||
` + changelog;
|
|
||||||
|
|
||||||
fs.writeFileSync(changelogPath, changelog, "utf8");
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
// Copied from maplibre/maplibre-gl-js
|
|
||||||
// https://github.com/maplibre/maplibre-gl-js/blob/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build/release-notes.js
|
|
||||||
|
|
||||||
import * as fs from "fs";
|
|
||||||
|
|
||||||
const changelogPath = "CHANGELOG.md";
|
|
||||||
const changelog = fs.readFileSync(changelogPath, "utf8");
|
|
||||||
|
|
||||||
/*
|
|
||||||
Parse the raw changelog text and split it into individual releases.
|
|
||||||
|
|
||||||
This regular expression:
|
|
||||||
- Matches lines starting with "## x.x.x".
|
|
||||||
- Groups the version number.
|
|
||||||
- Skips the (optional) release date.
|
|
||||||
- Groups the changelog content.
|
|
||||||
- Ends when another "## x.x.x" is found.
|
|
||||||
*/
|
|
||||||
const regex = /^## (\d+\.\d+\.\d+.*?)\n(.+?)(?=\n^## \d+\.\d+\.\d+.*?\n)/gms;
|
|
||||||
|
|
||||||
const releaseNotes = [];
|
|
||||||
let match;
|
|
||||||
// eslint-disable-next-line no-cond-assign
|
|
||||||
while (match = regex.exec(changelog)) {
|
|
||||||
releaseNotes.push({
|
|
||||||
"version": match[1],
|
|
||||||
"changelog": match[2].trim(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const latest = releaseNotes[0];
|
|
||||||
const previous = releaseNotes[1];
|
|
||||||
|
|
||||||
// Print the release notes template.
|
|
||||||
|
|
||||||
let header = "Changes since previous version";
|
|
||||||
if (previous) {
|
|
||||||
header = `https://github.com/maplibre/maputnik
|
|
||||||
[Changes](https://github.com/maplibre/maputnik/compare/v${previous.version}...v${latest.version}) since [Maputnik v${previous.version}](https://github.com/maplibre/maputnik/releases/tag/v${previous.version})`;
|
|
||||||
}
|
|
||||||
const templatedReleaseNotes = `${header}
|
|
||||||
|
|
||||||
${latest.changelog}`;
|
|
||||||
|
|
||||||
process.stdout.write(templatedReleaseNotes.trimEnd());
|
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
"use strict";
|
||||||
|
var path = require('path');
|
||||||
|
var rules = require('./webpack.rules');
|
||||||
|
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
var HtmlWebpackInlineSVGPlugin = require('html-webpack-inline-svg-plugin');
|
||||||
|
var CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||||
|
|
||||||
|
const HOST = process.env.HOST || "127.0.0.1";
|
||||||
|
const PORT = process.env.PORT || "8888";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
target: 'web',
|
||||||
|
mode: 'development',
|
||||||
|
entry: [
|
||||||
|
`webpack-dev-server/client?http://${HOST}:${PORT}`,
|
||||||
|
`webpack/hot/only-dev-server`,
|
||||||
|
`./src/index.jsx` // Your appʼs entry point
|
||||||
|
],
|
||||||
|
devtool: process.env.WEBPACK_DEVTOOL || 'cheap-module-source-map',
|
||||||
|
output: {
|
||||||
|
path: path.join(__dirname, '..', 'public'),
|
||||||
|
filename: 'bundle.js'
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.js', '.jsx']
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: rules
|
||||||
|
},
|
||||||
|
node: {
|
||||||
|
fs: "empty",
|
||||||
|
net: 'empty',
|
||||||
|
tls: 'empty'
|
||||||
|
},
|
||||||
|
devServer: {
|
||||||
|
// enable HMR
|
||||||
|
hot: true,
|
||||||
|
// serve index.html in place of 404 responses to allow HTML5 history
|
||||||
|
historyApiFallback: true,
|
||||||
|
port: PORT,
|
||||||
|
host: HOST,
|
||||||
|
watchFiles: {
|
||||||
|
options: {
|
||||||
|
// Disabled polling by default as it causes lots of CPU usage and hence drains laptop batteries. To enable polling add WEBPACK_DEV_SERVER_POLLING to your environment
|
||||||
|
// See <https://webpack.js.org/configuration/watch/#watchoptions-poll> for details
|
||||||
|
usePolling: (!!process.env.WEBPACK_DEV_SERVER_POLLING ? true : false),
|
||||||
|
watch: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
noEmitOnErrors: true,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
title: 'Maputnik',
|
||||||
|
template: './src/template.html'
|
||||||
|
}),
|
||||||
|
new HtmlWebpackInlineSVGPlugin({
|
||||||
|
runPreEmit: true,
|
||||||
|
}),
|
||||||
|
new CopyWebpackPlugin({
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
from: './src/manifest.json',
|
||||||
|
to: 'manifest.json'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
]
|
||||||
|
};
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
var webpack = require('webpack');
|
||||||
|
var path = require('path');
|
||||||
|
var rules = require('./webpack.rules');
|
||||||
|
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
var HtmlWebpackInlineSVGPlugin = require('html-webpack-inline-svg-plugin');
|
||||||
|
var WebpackCleanupPlugin = require('webpack-cleanup-plugin');
|
||||||
|
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||||
|
var CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: {
|
||||||
|
app: './src/index.jsx',
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: path.join(__dirname, '..', 'build', 'build'),
|
||||||
|
filename: '[name].[contenthash].js',
|
||||||
|
chunkFilename: '[contenthash].js'
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.js', '.jsx']
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: rules
|
||||||
|
},
|
||||||
|
node: {
|
||||||
|
fs: "empty",
|
||||||
|
net: 'empty',
|
||||||
|
tls: 'empty'
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.NoEmitOnErrorsPlugin(),
|
||||||
|
new WebpackCleanupPlugin(),
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.env': {
|
||||||
|
NODE_ENV: '"production"'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
template: './src/template.html',
|
||||||
|
title: 'Maputnik'
|
||||||
|
}),
|
||||||
|
new HtmlWebpackInlineSVGPlugin({
|
||||||
|
runPreEmit: true,
|
||||||
|
}),
|
||||||
|
new CopyWebpackPlugin({
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
from: './src/manifest.json',
|
||||||
|
to: 'manifest.json'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
new BundleAnalyzerPlugin({
|
||||||
|
analyzerMode: 'static',
|
||||||
|
defaultSizes: 'gzip',
|
||||||
|
openAnalyzer: false,
|
||||||
|
generateStatsFile: true,
|
||||||
|
reportFilename: 'bundle-stats.html',
|
||||||
|
statsFilename: 'bundle-stats.json',
|
||||||
|
})
|
||||||
|
]
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
const webpackProdConfig = require('./webpack.production.config');
|
||||||
|
var path = require('path');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
...webpackProdConfig,
|
||||||
|
output: {
|
||||||
|
...webpackProdConfig.output,
|
||||||
|
path: path.join(__dirname, '..', 'build', 'profiling'),
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
...webpackProdConfig.resolve,
|
||||||
|
alias: {
|
||||||
|
...webpackProdConfig.resolve.alias,
|
||||||
|
'react-dom$': 'react-dom/profiling',
|
||||||
|
'scheduler/tracing': 'scheduler/tracing-profiling',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
module.exports = [
|
||||||
|
{
|
||||||
|
test: /\.jsx?$/,
|
||||||
|
exclude: [
|
||||||
|
path.resolve(__dirname, '../node_modules')
|
||||||
|
],
|
||||||
|
use: 'babel-loader'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(eot|ttf|woff|woff2)$/,
|
||||||
|
use: 'file-loader?name=fonts/[name].[ext]'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.ico$/,
|
||||||
|
use: 'file-loader?name=[name].[ext]'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(gif|jpg|png)$/,
|
||||||
|
use: 'file-loader?name=img/[name].[ext]'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.svg$/,
|
||||||
|
use: [
|
||||||
|
'svg-inline-loader'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /[\/\\](node_modules|global|src)[\/\\].*\.scss$/,
|
||||||
|
use: [
|
||||||
|
'style-loader',
|
||||||
|
"css-loader",
|
||||||
|
"sass-loader"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /[\/\\](node_modules|global|src)[\/\\].*\.css$/,
|
||||||
|
use: [
|
||||||
|
'style-loader',
|
||||||
|
'css-loader'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
@@ -1,41 +1,9 @@
|
|||||||
import { defineConfig } from "cypress";
|
import { defineConfig } from "cypress";
|
||||||
import { createRequire } from "module";
|
|
||||||
const require = createRequire(import.meta.url);
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
env: {
|
|
||||||
codeCoverage: {
|
|
||||||
exclude: "cypress/**/*.*",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
e2e: {
|
e2e: {
|
||||||
setupNodeEvents(on, config) {
|
setupNodeEvents(on, config) {
|
||||||
// implement node event listeners here
|
// implement node event listeners here
|
||||||
require("@cypress/code-coverage/task")(on, config);
|
|
||||||
on("before:browser:launch", (browser, launchOptions) => {
|
|
||||||
if (browser.family !== "chromium") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
launchOptions.args.push("--disable-gpu");
|
|
||||||
launchOptions.args.push("--enable-features=AllowSwiftShaderFallback,AllowSoftwareGLFallbackDueToCrashes");
|
|
||||||
launchOptions.args.push("--enable-unsafe-swiftshader");
|
|
||||||
return launchOptions;
|
|
||||||
});
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
baseUrl: "http://localhost:8888",
|
|
||||||
scrollBehavior: "center",
|
|
||||||
retries: {
|
|
||||||
runMode: 2,
|
|
||||||
openMode: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
component: {
|
|
||||||
devServer: {
|
|
||||||
framework: "react",
|
|
||||||
bundler: "vite",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,40 +1,41 @@
|
|||||||
import { MaputnikDriver } from "./maputnik-driver";
|
import driver from "./driver";
|
||||||
|
|
||||||
describe("accessibility", () => {
|
describe("accessibility", () => {
|
||||||
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
// skipped due to the following issue with cypress: https://github.com/cypress-io/cypress/issues/299
|
||||||
beforeAndAfter();
|
describe.skip("skip links", () => {
|
||||||
|
|
||||||
describe("skip links", () => {
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
when.setStyle("layer");
|
driver.beforeEach();
|
||||||
|
driver.setStyle("layer");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("skip link to layer list", () => {
|
it("skip link to layer list", () => {
|
||||||
const selector = "root:skip:layer-list";
|
const selector = driver.getDataAttribute("root:skip:layer-list");
|
||||||
then(get.elementByTestId(selector)).shouldExist();
|
driver.isExists(selector);
|
||||||
when.tab();
|
driver.typeKeys('{tab}');
|
||||||
then(get.elementByTestId(selector)).shouldBeFocused();
|
driver.isFocused(selector);
|
||||||
when.click(selector);
|
driver.click(selector);
|
||||||
then(get.skipTargetLayerList()).shouldBeFocused();
|
|
||||||
|
driver.isFocused("#skip-target-layer-list");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("skip link to layer editor", () => {
|
it("skip link to layer editor", () => {
|
||||||
const selector = "root:skip:layer-editor";
|
const selector = driver.getDataAttribute("root:skip:layer-editor");
|
||||||
then(get.elementByTestId(selector)).shouldExist();
|
driver.isExists(selector);
|
||||||
then(get.elementByTestId("skip-target-layer-editor")).shouldExist();
|
driver.typeKeys('{tab}{tab}');
|
||||||
when.tab().tab();
|
driver.isFocused(selector);
|
||||||
then(get.elementByTestId(selector)).shouldBeFocused();
|
driver.click(selector);
|
||||||
when.click(selector);
|
|
||||||
then(get.skipTargetLayerEditor()).shouldBeFocused();
|
driver.isFocused("#skip-target-layer-editor");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("skip link to map view", () => {
|
it("skip link to map view", () => {
|
||||||
const selector = "root:skip:map-view";
|
const selector = driver.getDataAttribute("root:skip:map-view");
|
||||||
then(get.elementByTestId(selector)).shouldExist();
|
driver.isExists(selector);
|
||||||
when.tab().tab().tab();
|
driver.typeKeys('{tab}{tab}{tab}');
|
||||||
then(get.elementByTestId(selector)).shouldBeFocused();
|
driver.isFocused(selector);
|
||||||
when.click(selector);
|
driver.click(selector);
|
||||||
then(get.canvas()).shouldBeFocused();
|
|
||||||
|
driver.isFocused(".maplibregl-canvas");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
})
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import { MaputnikDriver } from "./maputnik-driver";
|
|
||||||
|
|
||||||
describe("code editor", () => {
|
|
||||||
const { beforeAndAfter, when, get, then } = new MaputnikDriver();
|
|
||||||
beforeAndAfter();
|
|
||||||
|
|
||||||
it("open code editor", () => {
|
|
||||||
when.click("nav:code-editor");
|
|
||||||
then(get.element(".maputnik-code-editor")).shouldExist();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("closes code editor", () => {
|
|
||||||
when.click("nav:code-editor");
|
|
||||||
then(get.element(".maputnik-code-editor")).shouldExist();
|
|
||||||
when.click("nav:code-editor");
|
|
||||||
then(get.element(".maputnik-code-editor")).shouldNotExist();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import {v1 as uuid} from "uuid";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
isMac() {
|
||||||
|
return Cypress.platform === "darwin";
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeEach() {
|
||||||
|
this.setupInterception();
|
||||||
|
this.setStyle('both');
|
||||||
|
},
|
||||||
|
|
||||||
|
setupInterception() {
|
||||||
|
cy.intercept('GET', 'http://localhost:8888/example-style.json', { fixture: 'example-style.json' }).as('example-style.json');
|
||||||
|
cy.intercept('GET', 'http://localhost:8888/example-layer-style.json', { fixture: 'example-layer-style.json' });
|
||||||
|
cy.intercept('GET', 'http://localhost:8888/geojson-style.json', { fixture: 'geojson-style.json' });
|
||||||
|
cy.intercept('GET', 'http://localhost:8888/raster-style.json', { fixture: 'raster-style.json' });
|
||||||
|
cy.intercept('GET', 'http://localhost:8888/geojson-raster-style.json', { fixture: 'geojson-raster-style.json' });
|
||||||
|
cy.intercept({method: 'GET', url: '*example.local/*' }, []);
|
||||||
|
cy.intercept({method: 'GET', url: '*example.com/*' }, []);
|
||||||
|
},
|
||||||
|
|
||||||
|
setStyle(styleProperties: 'geojson' | 'raster' | 'both' | 'layer' | '', zoom? : number) {
|
||||||
|
let url = "?debug";
|
||||||
|
switch (styleProperties) {
|
||||||
|
case "geojson":
|
||||||
|
url += "&style=http://localhost:8888/geojson-style.json";
|
||||||
|
break;
|
||||||
|
case "raster":
|
||||||
|
url += "&style=http://localhost:8888/raster-style.json";
|
||||||
|
break;
|
||||||
|
case "both":
|
||||||
|
url += "&style=http://localhost:8888/geojson-raster-style.json";
|
||||||
|
break;
|
||||||
|
case "layer":
|
||||||
|
url += "&style=http://localhost:8888/example-layer-style.json";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (zoom) {
|
||||||
|
url += "#" + zoom + "/41.3805/2.1635";
|
||||||
|
}
|
||||||
|
cy.visit("http://localhost:8888/" + url);
|
||||||
|
if (styleProperties) {
|
||||||
|
cy.on('window:confirm', () => true)
|
||||||
|
}
|
||||||
|
cy.get(".maputnik-toolbar-link").should("be.visible");
|
||||||
|
},
|
||||||
|
|
||||||
|
getDataAttribute(key: string, selector?: string) {
|
||||||
|
return `*[data-wd-key='${key}'] ${selector || ''}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
closeModal(key: string) {
|
||||||
|
const selector = this.getDataAttribute(key);
|
||||||
|
|
||||||
|
this.isDisplayedInViewport(selector);
|
||||||
|
|
||||||
|
this.click(this.getDataAttribute(key + ".close-modal"));
|
||||||
|
|
||||||
|
this.doesNotExists(selector);
|
||||||
|
},
|
||||||
|
|
||||||
|
openLayersModal() {
|
||||||
|
cy.get(this.getDataAttribute('layer-list:add-layer')).click();
|
||||||
|
|
||||||
|
cy.get(this.getDataAttribute('modal:add-layer')).should('exist');
|
||||||
|
cy.get(this.getDataAttribute('modal:add-layer')).should('be.visible');
|
||||||
|
},
|
||||||
|
|
||||||
|
getStyleFromWindow(win: Window) {
|
||||||
|
const styleId = win.localStorage.getItem("maputnik:latest_style");
|
||||||
|
const styleItem = win.localStorage.getItem(`maputnik:style:${styleId}`)
|
||||||
|
const obj = JSON.parse(styleItem || "");
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
isStyleStoreEqual(getter: (obj:any) => any, styleObj: any) {
|
||||||
|
cy.window().then((win: any) => {
|
||||||
|
const obj = this.getStyleFromWindow(win);
|
||||||
|
assert.deepEqual(getter(obj), styleObj);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
isStyleStoreEqualToExampleFileData() {
|
||||||
|
cy.window().then((win: any) => {
|
||||||
|
const obj = this.getStyleFromWindow(win);
|
||||||
|
cy.fixture('example-style.json').should('deep.equal', obj);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
fillLayersModal(opts: any) {
|
||||||
|
var type = opts.type;
|
||||||
|
var layer = opts.layer;
|
||||||
|
var id;
|
||||||
|
if(opts.id) {
|
||||||
|
id = opts.id
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
id = `${type}:${uuid()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
cy.get(this.getDataAttribute('add-layer.layer-type', "select")).select(type);
|
||||||
|
cy.get(this.getDataAttribute("add-layer.layer-id", "input")).type(id);
|
||||||
|
if(layer) {
|
||||||
|
cy.get(this.getDataAttribute("add-layer.layer-source-block", "input")).type(layer);
|
||||||
|
}
|
||||||
|
cy.get(this.getDataAttribute("add-layer")).click();
|
||||||
|
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
|
||||||
|
typeKeys(keys: string) {
|
||||||
|
cy.get('body').type(keys);
|
||||||
|
},
|
||||||
|
|
||||||
|
click(selector: string) {
|
||||||
|
cy.get(selector).click();
|
||||||
|
},
|
||||||
|
|
||||||
|
select(selector: string, value: string) {
|
||||||
|
cy.get(selector).select(value);
|
||||||
|
},
|
||||||
|
|
||||||
|
isSelected(selector: string, value: string) {
|
||||||
|
cy.get(selector).find(`option[value="${value}"]`).should("be.selected");
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
focus(selector: string) {
|
||||||
|
cy.get(selector).focus();
|
||||||
|
},
|
||||||
|
|
||||||
|
isFocused(selector: string) {
|
||||||
|
cy.get(selector).should('have.focus');
|
||||||
|
},
|
||||||
|
|
||||||
|
isDisplayedInViewport(selector: string) {
|
||||||
|
cy.get(selector).should('be.visible');
|
||||||
|
},
|
||||||
|
|
||||||
|
isNotDisplayedInViewport(selector: string) {
|
||||||
|
cy.get(selector).should('not.be.visible');
|
||||||
|
},
|
||||||
|
|
||||||
|
setValue(selector: string, text: string) {
|
||||||
|
cy.get(selector).clear().type(text, {parseSpecialCharSequences: false});
|
||||||
|
},
|
||||||
|
|
||||||
|
isExists(selector: string) {
|
||||||
|
cy.get(selector).should('exist');
|
||||||
|
},
|
||||||
|
|
||||||
|
doesNotExists(selector: string) {
|
||||||
|
cy.get(selector).should('not.exist');
|
||||||
|
},
|
||||||
|
|
||||||
|
chooseExampleFile() {
|
||||||
|
cy.get("input[type='file']").selectFile('cypress/fixtures/example-style.json', {force: true});
|
||||||
|
},
|
||||||
|
|
||||||
|
getExampleFileUrl() {
|
||||||
|
return "http://localhost:8888/example-style.json";
|
||||||
|
},
|
||||||
|
|
||||||
|
waitForExampleFileRequset() {
|
||||||
|
cy.wait('@example-style.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
+61
-105
@@ -1,124 +1,80 @@
|
|||||||
import { MaputnikDriver } from "./maputnik-driver";
|
import driver from "./driver";
|
||||||
|
|
||||||
describe("history", () => {
|
describe("history", () => {
|
||||||
const { beforeAndAfter, when, get, then } = new MaputnikDriver();
|
|
||||||
beforeAndAfter();
|
|
||||||
|
|
||||||
let undoKeyCombo: string;
|
let undoKeyCombo: string;
|
||||||
let redoKeyCombo: string;
|
let redoKeyCombo: string;
|
||||||
|
|
||||||
before(() => {
|
before(() => {
|
||||||
const isMac = get.isMac();
|
const isMac = driver.isMac();
|
||||||
undoKeyCombo = isMac ? "{meta}z" : "{ctrl}z";
|
undoKeyCombo = isMac ? '{meta}z' : '{ctrl}z';
|
||||||
redoKeyCombo = isMac ? "{meta}{shift}z" : "{ctrl}y";
|
redoKeyCombo = isMac ? '{meta}{shift}z' : '{ctrl}y';
|
||||||
|
driver.beforeEach();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("undo/redo", () => {
|
it("undo/redo", () => {
|
||||||
when.setStyle("geojson");
|
driver.setStyle('geojson');
|
||||||
when.modal.open();
|
driver.openLayersModal();
|
||||||
|
|
||||||
when.modal.fillLayers({
|
driver.isStyleStoreEqual((a: any) => a.layers, []);
|
||||||
|
|
||||||
|
driver.fillLayersModal({
|
||||||
id: "step 1",
|
id: "step 1",
|
||||||
type: "background",
|
type: "background"
|
||||||
});
|
})
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "step 1",
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
when.modal.open();
|
driver.isStyleStoreEqual((a: any) => a.layers, [
|
||||||
when.modal.fillLayers({
|
{
|
||||||
|
"id": "step 1",
|
||||||
|
"type": 'background'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
driver.openLayersModal();
|
||||||
|
driver.fillLayersModal({
|
||||||
id: "step 2",
|
id: "step 2",
|
||||||
type: "background",
|
type: "background"
|
||||||
});
|
})
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
driver.isStyleStoreEqual((a: any) => a.layers, [
|
||||||
layers: [
|
{
|
||||||
{
|
"id": "step 1",
|
||||||
id: "step 1",
|
"type": 'background'
|
||||||
type: "background",
|
},
|
||||||
},
|
{
|
||||||
{
|
"id": "step 2",
|
||||||
id: "step 2",
|
"type": 'background'
|
||||||
type: "background",
|
}
|
||||||
},
|
]);
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
when.typeKeys(undoKeyCombo);
|
driver.typeKeys(undoKeyCombo);
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
driver.isStyleStoreEqual((a: any) => a.layers, [
|
||||||
layers: [
|
{
|
||||||
{
|
"id": "step 1",
|
||||||
id: "step 1",
|
"type": 'background'
|
||||||
type: "background",
|
}
|
||||||
},
|
]);
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
when.typeKeys(undoKeyCombo);
|
driver.typeKeys(undoKeyCombo)
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
|
driver.isStyleStoreEqual((a: any) => a.layers, []);
|
||||||
|
|
||||||
when.typeKeys(redoKeyCombo);
|
driver.typeKeys(redoKeyCombo)
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
driver.isStyleStoreEqual((a: any) => a.layers, [
|
||||||
layers: [
|
{
|
||||||
{
|
"id": "step 1",
|
||||||
id: "step 1",
|
"type": 'background'
|
||||||
type: "background",
|
}
|
||||||
},
|
]);
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
when.typeKeys(redoKeyCombo);
|
driver.typeKeys(redoKeyCombo)
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
driver.isStyleStoreEqual((a: any) => a.layers, [
|
||||||
layers: [
|
{
|
||||||
{
|
"id": "step 1",
|
||||||
id: "step 1",
|
"type": 'background'
|
||||||
type: "background",
|
},
|
||||||
},
|
{
|
||||||
{
|
"id": "step 2",
|
||||||
id: "step 2",
|
"type": 'background'
|
||||||
type: "background",
|
}
|
||||||
},
|
]);
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
})
|
||||||
it("should not redo after undo and value change", () => {
|
|
||||||
when.setStyle("geojson");
|
|
||||||
when.modal.open();
|
|
||||||
when.modal.fillLayers({
|
|
||||||
id: "step 1",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.modal.open();
|
|
||||||
when.modal.fillLayers({
|
|
||||||
id: "step 2",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.typeKeys(undoKeyCombo);
|
|
||||||
when.typeKeys(undoKeyCombo);
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
|
|
||||||
|
|
||||||
when.modal.open();
|
|
||||||
when.modal.fillLayers({
|
|
||||||
id: "step 3",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.typeKeys(redoKeyCombo);
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "step 3",
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import { MaputnikDriver } from "./maputnik-driver";
|
|
||||||
|
|
||||||
describe("i18n", () => {
|
|
||||||
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
|
||||||
beforeAndAfter();
|
|
||||||
|
|
||||||
describe("language detector", () => {
|
|
||||||
it("English", () => {
|
|
||||||
const url = "?lng=en";
|
|
||||||
when.visit(url);
|
|
||||||
then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("en");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Japanese", () => {
|
|
||||||
const url = "?lng=ja";
|
|
||||||
when.visit(url);
|
|
||||||
then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("ja");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("language switcher", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.setStyle("layer");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("the language switcher switches to Japanese", () => {
|
|
||||||
const selector = "maputnik-lang-select";
|
|
||||||
then(get.elementByTestId(selector)).shouldExist();
|
|
||||||
when.select(selector, "ja");
|
|
||||||
then(get.elementByTestId(selector)).shouldHaveValue("ja");
|
|
||||||
|
|
||||||
then(get.elementByTestId("nav:settings")).shouldHaveText("スタイル設定");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
+27
-27
@@ -1,60 +1,60 @@
|
|||||||
import { MaputnikDriver } from "./maputnik-driver";
|
import driver from "./driver";
|
||||||
|
|
||||||
describe("keyboard", () => {
|
describe("keyboard", () => {
|
||||||
const { beforeAndAfter, given, when, get, then } = new MaputnikDriver();
|
|
||||||
beforeAndAfter();
|
|
||||||
describe("shortcuts", () => {
|
describe("shortcuts", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
given.setupMockBackedResponses();
|
driver.setupInterception();
|
||||||
when.setStyle("");
|
driver.setStyle('');
|
||||||
});
|
})
|
||||||
|
|
||||||
it("ESC should unfocus", () => {
|
it("ESC should unfocus", () => {
|
||||||
const targetSelector = "maputnik-select";
|
const targetSelector = driver.getDataAttribute("nav:inspect") + " select";
|
||||||
when.focus(targetSelector);
|
driver.focus(targetSelector);
|
||||||
then(get.elementByTestId(targetSelector)).shouldBeFocused();
|
driver.isFocused(targetSelector);
|
||||||
when.typeKeys("{esc}");
|
|
||||||
then(get.elementByTestId(targetSelector)).shouldNotBeFocused();
|
//driver.typeKeys("{esc}");
|
||||||
|
//driver.isFocused('body');
|
||||||
});
|
});
|
||||||
|
|
||||||
it("'?' should show shortcuts modal", () => {
|
it("'?' should show shortcuts modal", () => {
|
||||||
when.typeKeys("?");
|
driver.typeKeys("?");
|
||||||
then(get.elementByTestId("modal:shortcuts")).shouldBeVisible();
|
driver.isDisplayedInViewport(driver.getDataAttribute("modal:shortcuts"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("'o' should show open modal", () => {
|
it("'o' should show open modal", () => {
|
||||||
when.typeKeys("o");
|
driver.typeKeys("o");
|
||||||
then(get.elementByTestId("modal:open")).shouldBeVisible();
|
driver.isDisplayedInViewport(driver.getDataAttribute("modal:open"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("'e' should show export modal", () => {
|
it("'e' should show export modal", () => {
|
||||||
when.typeKeys("e");
|
driver.typeKeys("e");
|
||||||
then(get.elementByTestId("modal:export")).shouldBeVisible();
|
driver.isDisplayedInViewport(driver.getDataAttribute("modal:export"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("'d' should show sources modal", () => {
|
it("'d' should show sources modal", () => {
|
||||||
when.typeKeys("d");
|
driver.typeKeys("d");
|
||||||
then(get.elementByTestId("modal:sources")).shouldBeVisible();
|
driver.isDisplayedInViewport(driver.getDataAttribute("modal:sources"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("'s' should show settings modal", () => {
|
it("'s' should show settings modal", () => {
|
||||||
when.typeKeys("s");
|
driver.typeKeys("s");
|
||||||
then(get.elementByTestId("modal:settings")).shouldBeVisible();
|
driver.isDisplayedInViewport(driver.getDataAttribute("modal:settings"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("'i' should change map to inspect mode", () => {
|
it("'i' should change map to inspect mode", () => {
|
||||||
when.typeKeys("i");
|
driver.typeKeys("i");
|
||||||
then(get.inputValue("maputnik-select")).shouldEqual("inspect");
|
driver.isSelected(driver.getDataAttribute("nav:inspect"), "inspect");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("'m' should focus map", () => {
|
it("'m' should focus map", () => {
|
||||||
when.typeKeys("m");
|
driver.typeKeys("m");
|
||||||
then(get.canvas()).shouldBeFocused();
|
driver.isFocused(".maplibregl-canvas");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("'!' should show debug modal", () => {
|
it("'!' should show debug modal", () => {
|
||||||
when.typeKeys("!");
|
driver.typeKeys("!");
|
||||||
then(get.elementByTestId("modal:debug")).shouldBeVisible();
|
driver.isDisplayedInViewport(driver.getDataAttribute("modal:debug"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,294 +0,0 @@
|
|||||||
import { MaputnikDriver } from "./maputnik-driver";
|
|
||||||
import { v1 as uuid } from "uuid";
|
|
||||||
|
|
||||||
describe("layer editor", () => {
|
|
||||||
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
|
||||||
beforeAndAfter();
|
|
||||||
beforeEach(() => {
|
|
||||||
when.setStyle("both");
|
|
||||||
when.modal.open();
|
|
||||||
});
|
|
||||||
|
|
||||||
function createBackground() {
|
|
||||||
const id = uuid();
|
|
||||||
|
|
||||||
when.selectWithin("add-layer.layer-type", "background");
|
|
||||||
when.setValue("add-layer.layer-id.input", "background:" + id);
|
|
||||||
|
|
||||||
when.click("add-layer");
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "background:" + id,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
it("expand/collapse");
|
|
||||||
it("id", () => {
|
|
||||||
const bgId = createBackground();
|
|
||||||
|
|
||||||
when.click("layer-list-item:background:" + bgId);
|
|
||||||
|
|
||||||
const id = uuid();
|
|
||||||
when.setValue("layer-editor.layer-id.input", "foobar:" + id);
|
|
||||||
when.click("min-zoom");
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "foobar:" + id,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("source", () => {
|
|
||||||
it("should show error when the source is invalid", () => {
|
|
||||||
when.modal.fillLayers({
|
|
||||||
type: "circle",
|
|
||||||
layer: "invalid",
|
|
||||||
});
|
|
||||||
then(get.element(".maputnik-input-block--error .maputnik-input-block-label")).shouldHaveCss("color", "rgb(207, 74, 74)");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("min-zoom", () => {
|
|
||||||
let bgId: string;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
bgId = createBackground();
|
|
||||||
when.click("layer-list-item:background:" + bgId);
|
|
||||||
when.setValue("min-zoom.input-text", "1");
|
|
||||||
when.click("layer-editor.layer-id");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should update min-zoom in local storage", () => {
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "background:" + bgId,
|
|
||||||
type: "background",
|
|
||||||
minzoom: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("when clicking next layer should update style on local storage", () => {
|
|
||||||
when.type("min-zoom.input-text", "{backspace}");
|
|
||||||
when.click("max-zoom.input-text");
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "background:" + bgId,
|
|
||||||
type: "background",
|
|
||||||
minzoom: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("max-zoom", () => {
|
|
||||||
let bgId: string;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
bgId = createBackground();
|
|
||||||
when.click("layer-list-item:background:" + bgId);
|
|
||||||
when.setValue("max-zoom.input-text", "1");
|
|
||||||
when.click("layer-editor.layer-id");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should update style in local storage", () => {
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "background:" + bgId,
|
|
||||||
type: "background",
|
|
||||||
maxzoom: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("comments", () => {
|
|
||||||
let bgId: string;
|
|
||||||
const comment = "42";
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
bgId = createBackground();
|
|
||||||
when.click("layer-list-item:background:" + bgId);
|
|
||||||
when.setValue("layer-comment.input", comment);
|
|
||||||
when.click("layer-editor.layer-id");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should update style in local storage", () => {
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "background:" + bgId,
|
|
||||||
type: "background",
|
|
||||||
metadata: {
|
|
||||||
"maputnik:comment": comment,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when unsetting", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.clear("layer-comment.input");
|
|
||||||
when.click("min-zoom.input-text");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should update style in local storage", () => {
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "background:" + bgId,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("color", () => {
|
|
||||||
let bgId: string;
|
|
||||||
beforeEach(() => {
|
|
||||||
bgId = createBackground();
|
|
||||||
when.click("layer-list-item:background:" + bgId);
|
|
||||||
when.click("spec-field:background-color");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should update style in local storage", () => {
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "background:" + bgId,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("opacity", () => {
|
|
||||||
let bgId: string;
|
|
||||||
beforeEach(() => {
|
|
||||||
bgId = createBackground();
|
|
||||||
when.click("layer-list-item:background:" + bgId);
|
|
||||||
when.type("spec-field-input:background-opacity", "0.");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should keep '.' in the input field", () => {
|
|
||||||
then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue("0.");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should revert to a valid value when focus out", () => {
|
|
||||||
when.click("layer-list-item:background:" + bgId);
|
|
||||||
then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue("0");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
describe("filter", () => {
|
|
||||||
it("expand/collapse");
|
|
||||||
it("compound filter");
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("layout", () => {
|
|
||||||
it("text-font", () => {
|
|
||||||
when.setStyle("font");
|
|
||||||
when.collapseGroupInLayerEditor();
|
|
||||||
when.collapseGroupInLayerEditor(1);
|
|
||||||
when.collapseGroupInLayerEditor(2);
|
|
||||||
when.doWithin("spec-field:text-font", () => {
|
|
||||||
get.element(".maputnik-autocomplete input").first().click();
|
|
||||||
});
|
|
||||||
then(get.element(".maputnik-autocomplete-menu-item")).shouldBeVisible();
|
|
||||||
then(get.element(".maputnik-autocomplete-menu-item")).shouldHaveLength(3);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("paint", () => {
|
|
||||||
it("expand/collapse");
|
|
||||||
it("color");
|
|
||||||
it("pattern");
|
|
||||||
it("opacity");
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("json-editor", () => {
|
|
||||||
it("add", () => {
|
|
||||||
const id = when.modal.fillLayers({
|
|
||||||
type: "circle",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "circle",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const sourceText = get.elementByText('"source"');
|
|
||||||
|
|
||||||
sourceText.click();
|
|
||||||
sourceText.type("\"");
|
|
||||||
|
|
||||||
then(get.element(".cm-lint-marker-error")).shouldExist();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it("expand/collapse");
|
|
||||||
it("modify");
|
|
||||||
|
|
||||||
it("parse error", () => {
|
|
||||||
const bgId = createBackground();
|
|
||||||
|
|
||||||
when.click("layer-list-item:background:" + bgId);
|
|
||||||
when.collapseGroupInLayerEditor();
|
|
||||||
when.collapseGroupInLayerEditor(1);
|
|
||||||
then(get.element(".cm-lint-marker-error")).shouldNotExist();
|
|
||||||
|
|
||||||
when.appendTextInJsonEditor(
|
|
||||||
"\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013 {"
|
|
||||||
);
|
|
||||||
then(get.element(".cm-lint-marker-error")).shouldExist();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("sticky header", () => {
|
|
||||||
it("should keep layer header visible when scrolling properties", () => {
|
|
||||||
// Setup: Create a layer with many properties (e.g., symbol layer)
|
|
||||||
when.modal.fillLayers({
|
|
||||||
type: "symbol",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.wait(500);
|
|
||||||
const header = get.elementByTestId("layer-editor.header");
|
|
||||||
then(header).shouldBeVisible();
|
|
||||||
|
|
||||||
get.element(".maputnik-scroll-container").scrollTo("bottom", { ensureScrollable: false });
|
|
||||||
when.wait(200);
|
|
||||||
|
|
||||||
then(header).shouldBeVisible();
|
|
||||||
then(get.elementByTestId("skip-target-layer-editor")).shouldBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,541 +0,0 @@
|
|||||||
import { MaputnikDriver } from "./maputnik-driver";
|
|
||||||
|
|
||||||
describe("layers list", () => {
|
|
||||||
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
|
||||||
beforeAndAfter();
|
|
||||||
beforeEach(() => {
|
|
||||||
when.setStyle("both");
|
|
||||||
when.modal.open();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("ops", () => {
|
|
||||||
let id: string;
|
|
||||||
beforeEach(() => {
|
|
||||||
id = when.modal.fillLayers({
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should update layers in local storage", () => {
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when clicking delete", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.click("layer-list-item:" + id + ":delete");
|
|
||||||
});
|
|
||||||
it("should empty layers in local storage", () => {
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when clicking duplicate", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.click("layer-list-item:" + id + ":copy");
|
|
||||||
});
|
|
||||||
it("should add copy layer in local storage", () => {
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id + "-copy",
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when clicking hide", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.click("layer-list-item:" + id + ":toggle-visibility");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should update visibility to none in local storage", () => {
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "background",
|
|
||||||
layout: {
|
|
||||||
visibility: "none",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when clicking show", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.click("layer-list-item:" + id + ":toggle-visibility");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should update visibility to visible in local storage", () => {
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "background",
|
|
||||||
layout: {
|
|
||||||
visibility: "visible",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when selecting a layer", () => {
|
|
||||||
let secondId: string;
|
|
||||||
beforeEach(() => {
|
|
||||||
when.modal.open();
|
|
||||||
secondId = when.modal.fillLayers({
|
|
||||||
id: "second-layer",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it("should show the selected layer in the editor", () => {
|
|
||||||
when.realClick("layer-list-item:" + secondId);
|
|
||||||
then(get.elementByTestId("layer-editor.layer-id.input")).shouldHaveValue(secondId);
|
|
||||||
when.realClick("layer-list-item:" + id);
|
|
||||||
then(get.elementByTestId("layer-editor.layer-id.input")).shouldHaveValue(id);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("background", () => {
|
|
||||||
it("add", () => {
|
|
||||||
const id = when.modal.fillLayers({
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("modify", () => {});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("fill", () => {
|
|
||||||
it("add", () => {
|
|
||||||
const id = when.modal.fillLayers({
|
|
||||||
type: "fill",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "fill",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Change source
|
|
||||||
it("change source");
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("line", () => {
|
|
||||||
it("add", () => {
|
|
||||||
const id = when.modal.fillLayers({
|
|
||||||
type: "line",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "line",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("groups", () => {
|
|
||||||
when.modal.open();
|
|
||||||
const id1 = when.modal.fillLayers({
|
|
||||||
id: "aa",
|
|
||||||
type: "line",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.modal.open();
|
|
||||||
const id2 = when.modal.fillLayers({
|
|
||||||
id: "aa-2",
|
|
||||||
type: "line",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.modal.open();
|
|
||||||
const id3 = when.modal.fillLayers({
|
|
||||||
id: "b",
|
|
||||||
type: "line",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.elementByTestId("layer-list-item:" + id1)).shouldBeVisible();
|
|
||||||
then(get.elementByTestId("layer-list-item:" + id2)).shouldNotBeVisible();
|
|
||||||
then(get.elementByTestId("layer-list-item:" + id3)).shouldBeVisible();
|
|
||||||
when.click("layer-list-group:aa-0");
|
|
||||||
then(get.elementByTestId("layer-list-item:" + id1)).shouldBeVisible();
|
|
||||||
then(get.elementByTestId("layer-list-item:" + id2)).shouldBeVisible();
|
|
||||||
then(get.elementByTestId("layer-list-item:" + id3)).shouldBeVisible();
|
|
||||||
when.click("layer-list-item:" + id2);
|
|
||||||
when.click("skip-target-layer-editor");
|
|
||||||
when.click("menu-move-layer-down");
|
|
||||||
then(get.elementByTestId("layer-list-group:aa-0")).shouldNotExist();
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "aa",
|
|
||||||
type: "line",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "b",
|
|
||||||
type: "line",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "aa-2",
|
|
||||||
type: "line",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("symbol", () => {
|
|
||||||
it("add", () => {
|
|
||||||
const id = when.modal.fillLayers({
|
|
||||||
type: "symbol",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "symbol",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should show spec info when hovering and clicking single line property", () => {
|
|
||||||
when.modal.fillLayers({
|
|
||||||
type: "symbol",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.hover("spec-field-container:text-rotate");
|
|
||||||
then(get.elementByTestId("field-doc-button-Rotate")).shouldBeVisible();
|
|
||||||
when.click("field-doc-button-Rotate", 0);
|
|
||||||
then(get.elementByTestId("spec-field-doc")).shouldContainText("Rotates the ");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should show spec info when hovering and clicking multi line property", () => {
|
|
||||||
when.modal.fillLayers({
|
|
||||||
type: "symbol",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.hover("spec-field-container:text-offset");
|
|
||||||
then(get.elementByTestId("field-doc-button-Offset")).shouldBeVisible();
|
|
||||||
when.click("field-doc-button-Offset", 0);
|
|
||||||
then(get.elementByTestId("spec-field-doc")).shouldContainText("Offset distance");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should hide spec info when clicking a second time", () => {
|
|
||||||
when.modal.fillLayers({
|
|
||||||
type: "symbol",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.hover("spec-field-container:text-rotate");
|
|
||||||
then(get.elementByTestId("field-doc-button-Rotate")).shouldBeVisible();
|
|
||||||
when.click("field-doc-button-Rotate", 0);
|
|
||||||
when.wait(200);
|
|
||||||
when.click("field-doc-button-Rotate", 0);
|
|
||||||
then(get.elementByTestId("spec-field-doc")).shouldNotBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("raster", () => {
|
|
||||||
it("add", () => {
|
|
||||||
const id = when.modal.fillLayers({
|
|
||||||
type: "raster",
|
|
||||||
layer: "raster",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "raster",
|
|
||||||
source: "raster",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("circle", () => {
|
|
||||||
it("add", () => {
|
|
||||||
const id = when.modal.fillLayers({
|
|
||||||
type: "circle",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "circle",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("fill extrusion", () => {
|
|
||||||
it("add", () => {
|
|
||||||
const id = when.modal.fillLayers({
|
|
||||||
type: "fill-extrusion",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "fill-extrusion",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("hillshade", () => {
|
|
||||||
it("add", () => {
|
|
||||||
const id = when.modal.fillLayers({
|
|
||||||
type: "hillshade",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "hillshade",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("set hillshade illumination direction array", () => {
|
|
||||||
const id = when.modal.fillLayers({
|
|
||||||
type: "hillshade",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
when.collapseGroupInLayerEditor();
|
|
||||||
when.collapseGroupInLayerEditor(1);
|
|
||||||
when.setValueToPropertyArray("spec-field:hillshade-illumination-direction", "1");
|
|
||||||
when.addValueToPropertyArray("spec-field:hillshade-illumination-direction", "2");
|
|
||||||
when.addValueToPropertyArray("spec-field:hillshade-illumination-direction", "3");
|
|
||||||
when.addValueToPropertyArray("spec-field:hillshade-illumination-direction", "4");
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "hillshade",
|
|
||||||
source: "example",
|
|
||||||
paint: {
|
|
||||||
"hillshade-illumination-direction": [ 1, 2, 3, 4 ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("set hillshade highlight color array", () => {
|
|
||||||
const id = when.modal.fillLayers({
|
|
||||||
type: "hillshade",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
when.collapseGroupInLayerEditor();
|
|
||||||
when.setValueToPropertyArray("spec-field:hillshade-highlight-color", "blue");
|
|
||||||
when.addValueToPropertyArray("spec-field:hillshade-highlight-color", "#00ff00");
|
|
||||||
when.addValueToPropertyArray("spec-field:hillshade-highlight-color", "rgba(255, 255, 0, 1)");
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "hillshade",
|
|
||||||
source: "example",
|
|
||||||
paint: {
|
|
||||||
"hillshade-highlight-color": [ "blue", "#00ff00", "rgba(255, 255, 0, 1)" ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("color-relief", () => {
|
|
||||||
it("add", () => {
|
|
||||||
const id = when.modal.fillLayers({
|
|
||||||
type: "color-relief",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "color-relief",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("adds elevation expression when clicking the elevation button", () => {
|
|
||||||
when.modal.fillLayers({
|
|
||||||
type: "color-relief",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
when.collapseGroupInLayerEditor();
|
|
||||||
when.click("make-elevation-function");
|
|
||||||
then(get.element("[data-wd-key='spec-field-container:color-relief-color'] .cm-line")).shouldBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("groups", () => {
|
|
||||||
it("simple", () => {
|
|
||||||
when.setStyle("geojson");
|
|
||||||
|
|
||||||
when.modal.open();
|
|
||||||
when.modal.fillLayers({
|
|
||||||
id: "foo",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.modal.open();
|
|
||||||
when.modal.fillLayers({
|
|
||||||
id: "foo_bar",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.modal.open();
|
|
||||||
when.modal.fillLayers({
|
|
||||||
id: "foo_bar_baz",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.elementByTestId("layer-list-item:foo")).shouldBeVisible();
|
|
||||||
then(get.elementByTestId("layer-list-item:foo_bar")).shouldNotBeVisible();
|
|
||||||
then(
|
|
||||||
get.elementByTestId("layer-list-item:foo_bar_baz")
|
|
||||||
).shouldNotBeVisible();
|
|
||||||
when.click("layer-list-group:foo-0");
|
|
||||||
then(get.elementByTestId("layer-list-item:foo")).shouldBeVisible();
|
|
||||||
then(get.elementByTestId("layer-list-item:foo_bar")).shouldBeVisible();
|
|
||||||
then(
|
|
||||||
get.elementByTestId("layer-list-item:foo_bar_baz")
|
|
||||||
).shouldBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("drag and drop", () => {
|
|
||||||
it("move layer should update local storage", () => {
|
|
||||||
when.modal.open();
|
|
||||||
const firstId = when.modal.fillLayers({
|
|
||||||
id: "a",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
when.modal.open();
|
|
||||||
const secondId = when.modal.fillLayers({
|
|
||||||
id: "b",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
when.modal.open();
|
|
||||||
const thirdId = when.modal.fillLayers({
|
|
||||||
id: "c",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.dragAndDropWithWait("layer-list-item:" + firstId, "layer-list-item:" + thirdId);
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: secondId,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: thirdId,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: firstId,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("sticky header", () => {
|
|
||||||
it("should keep header visible when scrolling layer list", () => {
|
|
||||||
// Setup: Create multiple layers to enable scrolling
|
|
||||||
for (let i = 0; i < 20; i++) {
|
|
||||||
when.modal.open();
|
|
||||||
when.modal.fillLayers({
|
|
||||||
id: `layer-${i}`,
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
when.wait(500);
|
|
||||||
const header = get.elementByTestId("layer-list.header");
|
|
||||||
then(header).shouldBeVisible();
|
|
||||||
|
|
||||||
// Scroll the layer list container (use ensureScrollable: false to avoid flakiness)
|
|
||||||
get.elementByTestId("layer-list").scrollTo("bottom", { ensureScrollable: false });
|
|
||||||
when.wait(200);
|
|
||||||
then(header).shouldBeVisible();
|
|
||||||
then(get.elementByTestId("layer-list:add-layer")).shouldBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,427 @@
|
|||||||
|
var assert = require("assert");
|
||||||
|
import driver from "./driver";
|
||||||
|
import { v1 as uuid } from 'uuid';
|
||||||
|
|
||||||
|
describe("layers", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
driver.beforeEach();
|
||||||
|
driver.setStyle('both');
|
||||||
|
driver.openLayersModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ops", () => {
|
||||||
|
it("delete", () => {
|
||||||
|
var id = driver.fillLayersModal({
|
||||||
|
type: "background"
|
||||||
|
})
|
||||||
|
|
||||||
|
driver.isStyleStoreEqual((a: any) => a.layers, [
|
||||||
|
{
|
||||||
|
"id": id,
|
||||||
|
"type": 'background'
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
driver.click(driver.getDataAttribute("layer-list-item:"+id+":delete", ""))
|
||||||
|
|
||||||
|
driver.isStyleStoreEqual((a: any) => a.layers, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("duplicate", () => {
|
||||||
|
var styleObj;
|
||||||
|
var id = driver.fillLayersModal({
|
||||||
|
type: "background"
|
||||||
|
})
|
||||||
|
|
||||||
|
driver.isStyleStoreEqual((a: any) => a.layers, [
|
||||||
|
{
|
||||||
|
"id": id,
|
||||||
|
"type": 'background'
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
driver.click(driver.getDataAttribute("layer-list-item:"+id+":copy", ""));
|
||||||
|
|
||||||
|
driver.isStyleStoreEqual((a: any) => a.layers, [
|
||||||
|
{
|
||||||
|
"id": id+"-copy",
|
||||||
|
"type": "background"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": id,
|
||||||
|
"type": "background"
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hide", () => {
|
||||||
|
var styleObj;
|
||||||
|
var id = driver.fillLayersModal({
|
||||||
|
type: "background"
|
||||||
|
})
|
||||||
|
|
||||||
|
driver.isStyleStoreEqual((a: any) => a.layers, [
|
||||||
|
{
|
||||||
|
"id": id,
|
||||||
|
"type": 'background'
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
driver.click(driver.getDataAttribute("layer-list-item:"+id+":toggle-visibility", ""));
|
||||||
|
|
||||||
|
driver.isStyleStoreEqual((a: any) => a.layers, [
|
||||||
|
{
|
||||||
|
"id": id,
|
||||||
|
"type": "background",
|
||||||
|
"layout": {
|
||||||
|
"visibility": "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
driver.click(driver.getDataAttribute("layer-list-item:"+id+":toggle-visibility", ""));
|
||||||
|
|
||||||
|
driver.isStyleStoreEqual((a: any) => a.layers, [
|
||||||
|
{
|
||||||
|
"id": id,
|
||||||
|
"type": "background",
|
||||||
|
"layout": {
|
||||||
|
"visibility": "visible"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
describe('background', () => {
|
||||||
|
|
||||||
|
it("add", () => {
|
||||||
|
var id = driver.fillLayersModal({
|
||||||
|
type: "background"
|
||||||
|
})
|
||||||
|
|
||||||
|
driver.isStyleStoreEqual((a: any) => a.layers, [
|
||||||
|
{
|
||||||
|
"id": id,
|
||||||
|
"type": 'background'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("modify", () => {
|
||||||
|
function createBackground() {
|
||||||
|
// Setup
|
||||||
|
var id = uuid();
|
||||||
|
|
||||||
|
driver.select(driver.getDataAttribute("add-layer.layer-type", "select"), "background");
|
||||||
|
driver.setValue(driver.getDataAttribute("add-layer.layer-id", "input"), "background:"+id);
|
||||||
|
|
||||||
|
driver.click(driver.getDataAttribute("add-layer"));
|
||||||
|
|
||||||
|
driver.isStyleStoreEqual((a: any) => a.layers, [
|
||||||
|
{
|
||||||
|
"id": 'background:'+id,
|
||||||
|
"type": 'background'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====> THESE SHOULD BE FROM THE SPEC
|
||||||
|
describe("layer", () => {
|
||||||
|
it("expand/collapse");
|
||||||
|
it("id", () => {
|
||||||
|
var bgId = createBackground();
|
||||||
|
|
||||||
|
driver.click(driver.getDataAttribute("layer-list-item:background:"+bgId));
|
||||||
|
|
||||||
|
var id = uuid();
|
||||||
|
driver.setValue(driver.getDataAttribute("layer-editor.layer-id", "input"), "foobar:"+id)
|
||||||
|
driver.click(driver.getDataAttribute("min-zoom"));
|
||||||
|
|
||||||
|
driver.isStyleStoreEqual((a: any) => a.layers, [
|
||||||
|
{
|
||||||
|
"id": 'foobar:'+id,
|
||||||
|
"type": 'background'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("min-zoom", () => {
|
||||||
|
var bgId = createBackground();
|
||||||
|
|
||||||
|
driver.click(driver.getDataAttribute("layer-list-item:background:"+bgId));
|
||||||
|
driver.setValue(driver.getDataAttribute("min-zoom", 'input[type="text"]'), "1");
|
||||||
|
|
||||||
|
driver.click(driver.getDataAttribute("layer-editor.layer-id", "input"));
|
||||||
|
|
||||||
|
driver.isStyleStoreEqual((a: any) => a.layers, [
|
||||||
|
{
|
||||||
|
"id": 'background:'+bgId,
|
||||||
|
"type": 'background',
|
||||||
|
"minzoom": 1
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// AND RESET!
|
||||||
|
// driver.setValue(driver.getDataAttribute("min-zoom", "input"), "")
|
||||||
|
// driver.click(driver.getDataAttribute("max-zoom", "input"));
|
||||||
|
|
||||||
|
// driver.isStyleStoreEqual((a: any) => a.layers, [
|
||||||
|
// {
|
||||||
|
// "id": 'background:'+bgId,
|
||||||
|
// "type": 'background'
|
||||||
|
// }
|
||||||
|
// ]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("max-zoom", () => {
|
||||||
|
var bgId = createBackground();
|
||||||
|
|
||||||
|
driver.click(driver.getDataAttribute("layer-list-item:background:"+bgId));
|
||||||
|
driver.setValue(driver.getDataAttribute("max-zoom", 'input[type="text"]'), "1")
|
||||||
|
|
||||||
|
driver.click(driver.getDataAttribute("layer-editor.layer-id", "input"));
|
||||||
|
|
||||||
|
driver.isStyleStoreEqual((a: any) => a.layers, [
|
||||||
|
{
|
||||||
|
"id": 'background:'+bgId,
|
||||||
|
"type": 'background',
|
||||||
|
"maxzoom": 1
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("comments", () => {
|
||||||
|
var bgId = createBackground();
|
||||||
|
var id = uuid();
|
||||||
|
|
||||||
|
driver.click(driver.getDataAttribute("layer-list-item:background:"+bgId));
|
||||||
|
driver.setValue(driver.getDataAttribute("layer-comment", "textarea"), id);
|
||||||
|
|
||||||
|
driver.click(driver.getDataAttribute("layer-editor.layer-id", "input"));
|
||||||
|
|
||||||
|
driver.isStyleStoreEqual((a: any) => a.layers, [
|
||||||
|
{
|
||||||
|
"id": 'background:'+bgId,
|
||||||
|
"type": 'background',
|
||||||
|
metadata: {
|
||||||
|
'maputnik:comment': id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Unset it again.
|
||||||
|
// TODO: This fails
|
||||||
|
// driver.setValue(driver.getDataAttribute("layer-comment", "textarea"), "");
|
||||||
|
// driver.click(driver.getDataAttribute("min-zoom", "input"));
|
||||||
|
|
||||||
|
// driver.isStyleStoreEqual((a: any) => a.layers, [
|
||||||
|
// {
|
||||||
|
// "id": 'background:'+bgId,
|
||||||
|
// "type": 'background'
|
||||||
|
// }
|
||||||
|
// ]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("color", () => {
|
||||||
|
var bgId = createBackground();
|
||||||
|
|
||||||
|
driver.click(driver.getDataAttribute("layer-list-item:background:"+bgId));
|
||||||
|
|
||||||
|
driver.click(driver.getDataAttribute("spec-field:background-color", "input"));
|
||||||
|
|
||||||
|
driver.isStyleStoreEqual((a: any) => a.layers, [
|
||||||
|
{
|
||||||
|
"id": 'background:'+bgId,
|
||||||
|
"type": 'background'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("filter", () => {
|
||||||
|
it("expand/collapse");
|
||||||
|
it("compound filter");
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("paint", () => {
|
||||||
|
it("expand/collapse");
|
||||||
|
it("color");
|
||||||
|
it("pattern");
|
||||||
|
it("opacity");
|
||||||
|
})
|
||||||
|
// <=====
|
||||||
|
|
||||||
|
describe("json-editor", () => {
|
||||||
|
it("expand/collapse");
|
||||||
|
it("modify");
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
it.skip("parse error", () => {
|
||||||
|
var bgId = createBackground();
|
||||||
|
|
||||||
|
driver.click(driver.getDataAttribute("layer-list-item:background:"+bgId));
|
||||||
|
|
||||||
|
var errorSelector = ".CodeMirror-lint-marker-error";
|
||||||
|
driver.doesNotExists(errorSelector);
|
||||||
|
|
||||||
|
driver.click(".CodeMirror");
|
||||||
|
driver.typeKeys("\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013 {");
|
||||||
|
driver.isExists(errorSelector);
|
||||||
|
|
||||||
|
driver.click(driver.getDataAttribute("layer-editor.layer-id"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fill', () => {
|
||||||
|
it("add", () => {
|
||||||
|
|
||||||
|
var id = driver.fillLayersModal({
|
||||||
|
type: "fill",
|
||||||
|
layer: "example"
|
||||||
|
});
|
||||||
|
|
||||||
|
driver.isStyleStoreEqual((a: any) => a.layers, [
|
||||||
|
{
|
||||||
|
"id": id,
|
||||||
|
"type": 'fill',
|
||||||
|
"source": "example"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: Change source
|
||||||
|
it("change source")
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('line', () => {
|
||||||
|
it("add", () => {
|
||||||
|
var id = driver.fillLayersModal({
|
||||||
|
type: "line",
|
||||||
|
layer: "example"
|
||||||
|
});
|
||||||
|
|
||||||
|
driver.isStyleStoreEqual((a: any) => a.layers, [
|
||||||
|
{
|
||||||
|
"id": id,
|
||||||
|
"type": "line",
|
||||||
|
"source": "example",
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("groups", () => {
|
||||||
|
// TODO
|
||||||
|
// Click each of the layer groups.
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('symbol', () => {
|
||||||
|
it("add", () => {
|
||||||
|
var id = driver.fillLayersModal({
|
||||||
|
type: "symbol",
|
||||||
|
layer: "example"
|
||||||
|
});
|
||||||
|
|
||||||
|
driver.isStyleStoreEqual((a: any) => a.layers, [
|
||||||
|
{
|
||||||
|
"id": id,
|
||||||
|
"type": "symbol",
|
||||||
|
"source": "example",
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('raster', () => {
|
||||||
|
it("add", () => {
|
||||||
|
var id = driver.fillLayersModal({
|
||||||
|
type: "raster",
|
||||||
|
layer: "raster"
|
||||||
|
});
|
||||||
|
|
||||||
|
driver.isStyleStoreEqual((a: any) => a.layers, [
|
||||||
|
{
|
||||||
|
"id": id,
|
||||||
|
"type": "raster",
|
||||||
|
"source": "raster",
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('circle', () => {
|
||||||
|
it("add", () => {
|
||||||
|
var id = driver.fillLayersModal({
|
||||||
|
type: "circle",
|
||||||
|
layer: "example"
|
||||||
|
});
|
||||||
|
|
||||||
|
driver.isStyleStoreEqual((a: any) => a.layers, [
|
||||||
|
{
|
||||||
|
"id": id,
|
||||||
|
"type": "circle",
|
||||||
|
"source": "example",
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fill extrusion', () => {
|
||||||
|
it("add", () => {
|
||||||
|
var id = driver.fillLayersModal({
|
||||||
|
type: "fill-extrusion",
|
||||||
|
layer: "example"
|
||||||
|
});
|
||||||
|
|
||||||
|
driver.isStyleStoreEqual((a: any) => a.layers, [
|
||||||
|
{
|
||||||
|
"id": id,
|
||||||
|
"type": 'fill-extrusion',
|
||||||
|
"source": "example"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe("groups", () => {
|
||||||
|
it("simple", () => {
|
||||||
|
driver.setStyle("geojson");
|
||||||
|
|
||||||
|
driver.openLayersModal();
|
||||||
|
driver.fillLayersModal({
|
||||||
|
id: "foo",
|
||||||
|
type: "background"
|
||||||
|
})
|
||||||
|
|
||||||
|
driver.openLayersModal();
|
||||||
|
driver.fillLayersModal({
|
||||||
|
id: "foo_bar",
|
||||||
|
type: "background"
|
||||||
|
})
|
||||||
|
|
||||||
|
driver.openLayersModal();
|
||||||
|
driver.fillLayersModal({
|
||||||
|
id: "foo_bar_baz",
|
||||||
|
type: "background"
|
||||||
|
})
|
||||||
|
|
||||||
|
driver.isDisplayedInViewport(driver.getDataAttribute("layer-list-item:foo"));
|
||||||
|
driver.isNotDisplayedInViewport(driver.getDataAttribute("layer-list-item:foo_bar"));
|
||||||
|
driver.isNotDisplayedInViewport(driver.getDataAttribute("layer-list-item:foo_bar_baz"));
|
||||||
|
|
||||||
|
driver.click(driver.getDataAttribute("layer-list-group:foo-0"));
|
||||||
|
|
||||||
|
driver.isDisplayedInViewport(driver.getDataAttribute("layer-list-item:foo"));
|
||||||
|
driver.isDisplayedInViewport(driver.getDataAttribute("layer-list-item:foo_bar"));
|
||||||
|
driver.isDisplayedInViewport(driver.getDataAttribute("layer-list-item:foo_bar_baz"));
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
+23
-65
@@ -1,67 +1,25 @@
|
|||||||
import { MaputnikDriver } from "./maputnik-driver";
|
import driver from "./driver";
|
||||||
|
|
||||||
describe("map", () => {
|
describe("map", () => {
|
||||||
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
describe("zoom level", () => {
|
||||||
beforeAndAfter();
|
beforeEach(() => {
|
||||||
describe("zoom level", () => {
|
driver.beforeEach();
|
||||||
it("via url", () => {
|
});
|
||||||
const zoomLevel = 12.37;
|
it("via url", () => {
|
||||||
when.setStyle("geojson", zoomLevel);
|
var zoomLevel = 12.37;
|
||||||
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
|
driver.setStyle("geojson", zoomLevel);
|
||||||
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText(
|
driver.isDisplayedInViewport(".maplibregl-ctrl-zoom");
|
||||||
"Zoom: " + zoomLevel
|
// HM TODO
|
||||||
);
|
//driver.getText(".maplibregl-ctrl-zoom") === "Zoom "+(zoomLevel);
|
||||||
});
|
})
|
||||||
|
it("via map controls", () => {
|
||||||
it("via map controls", () => {
|
var zoomLevel = 12.37;
|
||||||
const zoomLevel = 12.37;
|
driver.setStyle("geojson", zoomLevel);
|
||||||
when.setStyle("geojson", zoomLevel);
|
|
||||||
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
|
driver.click(".maplibregl-ctrl-zoom-in");
|
||||||
when.clickZoomIn();
|
driver.isDisplayedInViewport(".maplibregl-ctrl-zoom");
|
||||||
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText(
|
// HM TODO
|
||||||
"Zoom: " + (zoomLevel + 1)
|
//driver.getText(".maplibregl-ctrl-zoom") === "Zoom "+(zoomLevel + 1);
|
||||||
);
|
})
|
||||||
});
|
})
|
||||||
|
})
|
||||||
it("via style file definition", () => {
|
|
||||||
when.setStyle("zoom_7_center_0_51");
|
|
||||||
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
|
|
||||||
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText(
|
|
||||||
"Zoom: " + (7)
|
|
||||||
);
|
|
||||||
then(get.locationHash().should("contain", "#7/51/0"));
|
|
||||||
|
|
||||||
// opening another stylefile does not update the map view again
|
|
||||||
// as discussed in https://github.com/maplibre/maputnik/issues/1546
|
|
||||||
when.openASecondStyleWithDifferentZoomAndCenter();
|
|
||||||
then(get.locationHash().should("contain", "#7/51/0"));
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("search", () => {
|
|
||||||
it("should exist", () => {
|
|
||||||
then(get.searchControl()).shouldBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("popup", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.setStyle("rectangles");
|
|
||||||
then(get.locationHash().should("exist"));
|
|
||||||
});
|
|
||||||
it("should open on feature click", () => {
|
|
||||||
when.clickCenter("maplibre:map");
|
|
||||||
then(get.elementByTestId("feature-layer-popup")).shouldBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should open a second feature after closing popup", () => {
|
|
||||||
when.clickCenter("maplibre:map");
|
|
||||||
then(get.elementByTestId("feature-layer-popup")).shouldBeVisible();
|
|
||||||
when.closePopup();
|
|
||||||
then(get.elementByTestId("feature-layer-popup")).shouldNotExist();
|
|
||||||
when.clickCenter("maplibre:map");
|
|
||||||
then(get.elementByTestId("feature-layer-popup")).shouldBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
/// <reference types="cypress-real-events" />
|
|
||||||
import { CypressHelper } from "@shellygo/cypress-test-utils";
|
|
||||||
import "cypress-real-events/support";
|
|
||||||
|
|
||||||
export default class MaputnikCypressHelper {
|
|
||||||
private helper = new CypressHelper({ defaultDataAttribute: "data-wd-key" });
|
|
||||||
|
|
||||||
public given = {
|
|
||||||
...this.helper.given,
|
|
||||||
};
|
|
||||||
|
|
||||||
public get = {
|
|
||||||
locationHash: (): Cypress.Chainable<string> => cy.location("hash"),
|
|
||||||
...this.helper.get,
|
|
||||||
};
|
|
||||||
|
|
||||||
public when = {
|
|
||||||
dragAndDropWithWait: (element: string, targetElement: string) => {
|
|
||||||
this.helper.get.elementByTestId(element).realMouseDown({ button: "left", position: "center" });
|
|
||||||
this.helper.get.elementByTestId(element).realMouseMove(0, 10, { position: "center" });
|
|
||||||
this.helper.get.elementByTestId(targetElement).realMouseMove(0, 0, { position: "center" });
|
|
||||||
this.helper.when.wait(1);
|
|
||||||
this.helper.get.elementByTestId(targetElement).realMouseUp();
|
|
||||||
},
|
|
||||||
clickCenter: (element: string) => {
|
|
||||||
this.helper.get.elementByTestId(element).realMouseDown({ button: "left", position: "center" });
|
|
||||||
this.helper.when.wait(200);
|
|
||||||
this.helper.get.elementByTestId(element).realMouseUp();
|
|
||||||
},
|
|
||||||
openFileByFixture: (fixture: string, buttonTestId: string, inputTestId: string) => {
|
|
||||||
cy.window().then((win) => {
|
|
||||||
const file = {
|
|
||||||
text: cy.stub().resolves(cy.fixture(fixture).then(JSON.stringify)),
|
|
||||||
};
|
|
||||||
const fileHandle = {
|
|
||||||
getFile: cy.stub().resolves(file),
|
|
||||||
};
|
|
||||||
if (!win.showOpenFilePicker) {
|
|
||||||
this.helper.get.elementByTestId(inputTestId).selectFile("cypress/fixtures/" + fixture, { force: true });
|
|
||||||
} else {
|
|
||||||
cy.stub(win, "showOpenFilePicker").resolves([fileHandle]);
|
|
||||||
this.helper.get.elementByTestId(buttonTestId).click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
dropFileByFixture: (fixture: string, dropzoneTestId: string) => {
|
|
||||||
this.helper.get.elementByTestId(dropzoneTestId).selectFile("cypress/fixtures/" + fixture, {
|
|
||||||
action: "drag-drop",
|
|
||||||
force: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
...this.helper.when,
|
|
||||||
};
|
|
||||||
|
|
||||||
public beforeAndAfter = this.helper.beforeAndAfter;
|
|
||||||
}
|
|
||||||
@@ -1,272 +0,0 @@
|
|||||||
/// <reference types="cypress-plugin-tab" />
|
|
||||||
|
|
||||||
import { CypressHelper } from "@shellygo/cypress-test-utils";
|
|
||||||
import { Assertable, then } from "@shellygo/cypress-test-utils/assertable";
|
|
||||||
import MaputnikCypressHelper from "./maputnik-cypress-helper";
|
|
||||||
import ModalDriver from "./modal-driver";
|
|
||||||
const baseUrl = "http://localhost:8888/";
|
|
||||||
|
|
||||||
const styleFromWindow = (win: Window) => {
|
|
||||||
const styleId = win.localStorage.getItem("maputnik:latest_style");
|
|
||||||
const styleItemKey = `maputnik:style:${styleId}`;
|
|
||||||
const styleItem = win.localStorage.getItem(styleItemKey);
|
|
||||||
if (!styleItem) throw new Error("Could not get styleItem from localStorage");
|
|
||||||
const obj = JSON.parse(styleItem);
|
|
||||||
return obj;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class MaputnikAssertable<T> extends Assertable<T> {
|
|
||||||
shouldEqualToStoredStyle = () =>
|
|
||||||
then(
|
|
||||||
new CypressHelper().get.window().then((win: Window) => {
|
|
||||||
const style = styleFromWindow(win);
|
|
||||||
then(this.chainable).shouldDeepNestedInclude(style);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MaputnikDriver {
|
|
||||||
private helper = new MaputnikCypressHelper();
|
|
||||||
private modalDriver = new ModalDriver();
|
|
||||||
|
|
||||||
public beforeAndAfter = () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
this.given.setupMockBackedResponses();
|
|
||||||
this.when.setStyle("both");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
public then = (chainable: Cypress.Chainable<any>) =>
|
|
||||||
new MaputnikAssertable(chainable);
|
|
||||||
|
|
||||||
public given = {
|
|
||||||
...this.helper.given,
|
|
||||||
setupMockBackedResponses: () => {
|
|
||||||
this.helper.given.interceptAndMockResponse({
|
|
||||||
method: "GET",
|
|
||||||
url: baseUrl + "example-style.json",
|
|
||||||
response: {
|
|
||||||
fixture: "example-style.json",
|
|
||||||
},
|
|
||||||
alias: "example-style.json",
|
|
||||||
});
|
|
||||||
this.helper.given.interceptAndMockResponse({
|
|
||||||
method: "GET",
|
|
||||||
url: baseUrl + "example-layer-style.json",
|
|
||||||
response: {
|
|
||||||
fixture: "example-layer-style.json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.helper.given.interceptAndMockResponse({
|
|
||||||
method: "GET",
|
|
||||||
url: baseUrl + "geojson-style.json",
|
|
||||||
response: {
|
|
||||||
fixture: "geojson-style.json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.helper.given.interceptAndMockResponse({
|
|
||||||
method: "GET",
|
|
||||||
url: baseUrl + "raster-style.json",
|
|
||||||
response: {
|
|
||||||
fixture: "raster-style.json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.helper.given.interceptAndMockResponse({
|
|
||||||
method: "GET",
|
|
||||||
url: baseUrl + "geojson-raster-style.json",
|
|
||||||
response: {
|
|
||||||
fixture: "geojson-raster-style.json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.helper.given.interceptAndMockResponse({
|
|
||||||
method: "GET",
|
|
||||||
url: baseUrl + "rectangles-style.json",
|
|
||||||
response: {
|
|
||||||
fixture: "rectangles-style.json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.helper.given.interceptAndMockResponse({
|
|
||||||
method: "GET",
|
|
||||||
url: baseUrl + "example-style-with-fonts.json",
|
|
||||||
response: {
|
|
||||||
fixture: "example-style-with-fonts.json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.helper.given.interceptAndMockResponse({
|
|
||||||
method: "GET",
|
|
||||||
url: baseUrl + "example-style-with-zoom-7-and-center-0-51.json",
|
|
||||||
response: {
|
|
||||||
fixture: "example-style-with-zoom-7-and-center-0-51.json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.helper.given.interceptAndMockResponse({
|
|
||||||
method: "GET",
|
|
||||||
url: baseUrl + "example-style-with-zoom-5-and-center-50-50.json",
|
|
||||||
response: {
|
|
||||||
fixture: "example-style-with-zoom-5-and-center-50-50.json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.helper.given.interceptAndMockResponse({
|
|
||||||
method: "GET",
|
|
||||||
url: "*example.local/*",
|
|
||||||
response: [],
|
|
||||||
});
|
|
||||||
this.helper.given.interceptAndMockResponse({
|
|
||||||
method: "GET",
|
|
||||||
url: "*example.com/*",
|
|
||||||
response: [],
|
|
||||||
});
|
|
||||||
this.helper.given.interceptAndMockResponse({
|
|
||||||
method: "GET",
|
|
||||||
url: "https://www.glyph-server.com/*",
|
|
||||||
response: ["Font 1", "Font 2", "Font 3"],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
public when = {
|
|
||||||
...this.helper.when,
|
|
||||||
modal: this.modalDriver.when,
|
|
||||||
doWithin: (selector: string, fn: () => void) => {
|
|
||||||
this.helper.when.doWithin(fn, selector);
|
|
||||||
},
|
|
||||||
tab: () => this.helper.get.element("body").tab(),
|
|
||||||
waitForExampleFileResponse: () => {
|
|
||||||
this.helper.when.waitForResponse("example-style.json");
|
|
||||||
},
|
|
||||||
openASecondStyleWithDifferentZoomAndCenter: () => {
|
|
||||||
cy.contains("button", "Open").click();
|
|
||||||
cy.get('[data-wd-key="modal:open.url.input"]')
|
|
||||||
.should("be.enabled")
|
|
||||||
.clear()
|
|
||||||
.type("http://localhost:8888/example-style-with-zoom-5-and-center-50-50.json{enter}");
|
|
||||||
},
|
|
||||||
chooseExampleFile: () => {
|
|
||||||
this.helper.given.fixture("example-style.json", "example-style.json");
|
|
||||||
this.helper.when.openFileByFixture("example-style.json", "modal:open.dropzone", "modal:open.file.input");
|
|
||||||
this.helper.when.wait(200);
|
|
||||||
},
|
|
||||||
dropExampleFile: () => {
|
|
||||||
this.helper.given.fixture("example-style.json", "example-style.json");
|
|
||||||
this.helper.when.dropFileByFixture("example-style.json", "modal:open.dropzone");
|
|
||||||
this.helper.when.wait(200);
|
|
||||||
},
|
|
||||||
setStyle: (
|
|
||||||
styleProperties: "geojson" | "raster" | "both" | "layer" | "rectangles" | "font" | "zoom_7_center_0_51" | "",
|
|
||||||
zoom?: number
|
|
||||||
) => {
|
|
||||||
const url = new URL(baseUrl);
|
|
||||||
switch (styleProperties) {
|
|
||||||
case "geojson":
|
|
||||||
url.searchParams.set("style", baseUrl + "geojson-style.json");
|
|
||||||
break;
|
|
||||||
case "raster":
|
|
||||||
url.searchParams.set("style", baseUrl + "raster-style.json");
|
|
||||||
break;
|
|
||||||
case "both":
|
|
||||||
url.searchParams.set("style", baseUrl + "geojson-raster-style.json");
|
|
||||||
break;
|
|
||||||
case "layer":
|
|
||||||
url.searchParams.set("style", baseUrl + "example-layer-style.json");
|
|
||||||
break;
|
|
||||||
case "rectangles":
|
|
||||||
url.searchParams.set("style", baseUrl + "rectangles-style.json");
|
|
||||||
break;
|
|
||||||
case "font":
|
|
||||||
url.searchParams.set("style", baseUrl + "example-style-with-fonts.json");
|
|
||||||
break;
|
|
||||||
case "zoom_7_center_0_51":
|
|
||||||
url.searchParams.set("style", baseUrl + "example-style-with-zoom-7-and-center-0-51.json");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (zoom) {
|
|
||||||
url.hash = `${zoom}/41.3805/2.1635`;
|
|
||||||
}
|
|
||||||
this.helper.when.visit(url.toString());
|
|
||||||
if (styleProperties) {
|
|
||||||
this.helper.when.acceptConfirm();
|
|
||||||
}
|
|
||||||
// when methods should not include assertions
|
|
||||||
const toolbarLink = this.helper.get.elementByTestId("toolbar:link");
|
|
||||||
toolbarLink.scrollIntoView();
|
|
||||||
toolbarLink.should("be.visible");
|
|
||||||
},
|
|
||||||
|
|
||||||
typeKeys: (keys: string) => this.helper.get.element("body").type(keys),
|
|
||||||
|
|
||||||
clickZoomIn: () => {
|
|
||||||
this.helper.get.element(".maplibregl-ctrl-zoom-in").click();
|
|
||||||
},
|
|
||||||
|
|
||||||
selectWithin: (selector: string, value: string) => {
|
|
||||||
this.when.doWithin(selector, () => {
|
|
||||||
this.helper.get.element("select").select(value);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
select: (selector: string, value: string) => {
|
|
||||||
this.helper.get.elementByTestId(selector).select(value);
|
|
||||||
},
|
|
||||||
|
|
||||||
focus: (selector: string) => {
|
|
||||||
this.helper.when.focus(selector);
|
|
||||||
},
|
|
||||||
|
|
||||||
setValue: (selector: string, text: string) => {
|
|
||||||
this.helper.get
|
|
||||||
.elementByTestId(selector)
|
|
||||||
.clear()
|
|
||||||
.type(text, { parseSpecialCharSequences: false });
|
|
||||||
},
|
|
||||||
|
|
||||||
setValueToPropertyArray: (selector: string, value: string) => {
|
|
||||||
this.when.doWithin(selector, () => {
|
|
||||||
this.helper.get.element(".maputnik-array-block-content input").last().type("{selectall}"+value, {force: true });
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
addValueToPropertyArray: (selector: string, value: string) => {
|
|
||||||
this.when.doWithin(selector, () => {
|
|
||||||
this.helper.get.element(".maputnik-array-add-value").click({ force: true });
|
|
||||||
this.helper.get.element(".maputnik-array-block-content input").last().type("{selectall}"+value, {force: true });
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
closePopup: () => {
|
|
||||||
this.helper.get.element(".maplibregl-popup-close-button").click();
|
|
||||||
},
|
|
||||||
|
|
||||||
collapseGroupInLayerEditor: (index = 0) => {
|
|
||||||
this.helper.get.element(".maputnik-layer-editor-group__button").eq(index).realClick();
|
|
||||||
},
|
|
||||||
|
|
||||||
appendTextInJsonEditor: (text: string) => {
|
|
||||||
this.helper.get.element(".cm-line").first().click().type(text, { parseSpecialCharSequences: false });
|
|
||||||
},
|
|
||||||
|
|
||||||
setTextInJsonEditor: (text: string) => {
|
|
||||||
this.helper.get.element(".cm-line").first().click().clear().type(text, { parseSpecialCharSequences: false });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public get = {
|
|
||||||
...this.helper.get,
|
|
||||||
isMac: () => {
|
|
||||||
return Cypress.platform === "darwin";
|
|
||||||
},
|
|
||||||
|
|
||||||
styleFromLocalStorage: () =>
|
|
||||||
this.helper.get.window().then((win) => styleFromWindow(win)),
|
|
||||||
|
|
||||||
exampleFileUrl: () => {
|
|
||||||
return baseUrl + "example-style.json";
|
|
||||||
},
|
|
||||||
skipTargetLayerList: () =>
|
|
||||||
this.helper.get.elementByTestId("skip-target-layer-list"),
|
|
||||||
skipTargetLayerEditor: () =>
|
|
||||||
this.helper.get.elementByTestId("skip-target-layer-editor"),
|
|
||||||
canvas: () => this.helper.get.element("canvas"),
|
|
||||||
searchControl: () => this.helper.get.element(".maplibregl-ctrl-geocoder")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { v1 as uuid } from "uuid";
|
|
||||||
import MaputnikCypressHelper from "./maputnik-cypress-helper";
|
|
||||||
|
|
||||||
export default class ModalDriver {
|
|
||||||
private helper = new MaputnikCypressHelper();
|
|
||||||
|
|
||||||
public when = {
|
|
||||||
fillLayers: (opts: { type: string; layer?: string; id?: string }) => {
|
|
||||||
// Having logic in test code is an anti pattern.
|
|
||||||
// This should be split to multiple single responsibility functions
|
|
||||||
const type = opts.type;
|
|
||||||
const layer = opts.layer;
|
|
||||||
let id;
|
|
||||||
if (opts.id) {
|
|
||||||
id = opts.id;
|
|
||||||
} else {
|
|
||||||
id = `${type}:${uuid()}`;
|
|
||||||
}
|
|
||||||
this.helper.when.selectOption("add-layer.layer-type.select", type);
|
|
||||||
this.helper.when.type("add-layer.layer-id.input", id);
|
|
||||||
|
|
||||||
if (layer) {
|
|
||||||
this.helper.when.doWithin(() => {
|
|
||||||
this.helper.get.element("input").clear().type(layer!);
|
|
||||||
}, "add-layer.layer-source-block");
|
|
||||||
}
|
|
||||||
this.helper.when.click("add-layer");
|
|
||||||
|
|
||||||
return id;
|
|
||||||
},
|
|
||||||
|
|
||||||
open: () => {
|
|
||||||
this.helper.when.click("layer-list:add-layer");
|
|
||||||
},
|
|
||||||
|
|
||||||
close: (key: string) => {
|
|
||||||
this.helper.when.click(key + ".close-modal");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
+82
-401
@@ -1,456 +1,137 @@
|
|||||||
import { MaputnikDriver } from "./maputnik-driver";
|
import driver from "./driver";
|
||||||
import tokens from "../../src/config/tokens.json" with {type: "json"};
|
|
||||||
|
|
||||||
describe("modals", () => {
|
describe("modals", () => {
|
||||||
const { beforeAndAfter, when, get, given, then } = new MaputnikDriver();
|
|
||||||
beforeAndAfter();
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
when.setStyle("");
|
driver.beforeEach();
|
||||||
|
driver.setStyle('');
|
||||||
});
|
});
|
||||||
describe("open", () => {
|
describe("open", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
when.click("nav:open");
|
driver.click(driver.getDataAttribute("nav:open"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("close", () => {
|
it("close", () => {
|
||||||
when.modal.close("modal:open");
|
driver.closeModal("modal:open");
|
||||||
then(get.elementByTestId("modal:open")).shouldNotExist();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("upload", () => {
|
it.skip("upload", () => {
|
||||||
when.chooseExampleFile();
|
// HM: I was not able to make the following choose file actually to select a file and close the modal...
|
||||||
then(get.fixture("example-style.json")).shouldEqualToStoredStyle();
|
driver.chooseExampleFile();
|
||||||
|
|
||||||
|
driver.isStyleStoreEqualToExampleFileData();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("upload via drag and drop", () => {
|
it("load from url", () => {
|
||||||
when.dropExampleFile();
|
var styleFileUrl = driver.getExampleFileUrl();
|
||||||
then(get.fixture("example-style.json")).shouldEqualToStoredStyle();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when click open url", () => {
|
driver.setValue(driver.getDataAttribute("modal:open.url.input"), styleFileUrl);
|
||||||
beforeEach(() => {
|
driver.click(driver.getDataAttribute("modal:open.url.button"))
|
||||||
const styleFileUrl = get.exampleFileUrl();
|
driver.waitForExampleFileRequset();
|
||||||
|
|
||||||
when.setValue("modal:open.url.input", styleFileUrl);
|
driver.isStyleStoreEqualToExampleFileData();
|
||||||
when.click("modal:open.url.button");
|
|
||||||
when.wait(200);
|
|
||||||
});
|
|
||||||
it("load from url", () => {
|
|
||||||
then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
})
|
||||||
|
|
||||||
describe("shortcuts", () => {
|
describe("shortcuts", () => {
|
||||||
it("open/close", () => {
|
it("open/close", () => {
|
||||||
when.setStyle("");
|
driver.setStyle('');
|
||||||
when.typeKeys("?");
|
|
||||||
when.modal.close("modal:shortcuts");
|
driver.typeKeys("?");
|
||||||
then(get.elementByTestId("modal:shortcuts")).shouldNotExist();
|
|
||||||
|
driver.isDisplayedInViewport(driver.getDataAttribute("modal:shortcuts"));
|
||||||
|
|
||||||
|
driver.closeModal("modal:shortcuts");
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("export", () => {
|
describe("export", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
when.click("nav:export");
|
driver.click(driver.getDataAttribute("nav:export"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("close", () => {
|
it("close", () => {
|
||||||
when.modal.close("modal:export");
|
driver.closeModal("modal:export");
|
||||||
then(get.elementByTestId("modal:export")).shouldNotExist();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Work out how to download a file and check the contents
|
// TODO: Work out how to download a file and check the contents
|
||||||
it("download");
|
it("download")
|
||||||
});
|
|
||||||
|
})
|
||||||
|
|
||||||
describe("sources", () => {
|
describe("sources", () => {
|
||||||
beforeEach(() => {
|
it("active sources")
|
||||||
when.setStyle("layer");
|
it("public source")
|
||||||
when.click("nav:sources");
|
it("add new source")
|
||||||
});
|
})
|
||||||
|
|
||||||
it("active sources");
|
|
||||||
it("public source");
|
|
||||||
|
|
||||||
it("add new source", () => {
|
|
||||||
const sourceId = "n1z2v3r";
|
|
||||||
when.setValue("modal:sources.add.source_id", sourceId);
|
|
||||||
when.select("modal:sources.add.source_type", "tile_vector");
|
|
||||||
when.select("modal:sources.add.scheme_type", "tms");
|
|
||||||
when.click("modal:sources.add.add_source");
|
|
||||||
when.wait(200);
|
|
||||||
then(
|
|
||||||
get.styleFromLocalStorage().then((style) => style.sources[sourceId])
|
|
||||||
).shouldInclude({
|
|
||||||
scheme: "tms",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("add new pmtiles source", () => {
|
|
||||||
const sourceId = "pmtilestest";
|
|
||||||
when.setValue("modal:sources.add.source_id", sourceId);
|
|
||||||
when.select("modal:sources.add.source_type", "pmtiles_vector");
|
|
||||||
when.setValue("modal:sources.add.source_url", "https://data.source.coop/protomaps/openstreetmap/v4.pmtiles");
|
|
||||||
when.click("modal:sources.add.add_source");
|
|
||||||
when.click("modal:sources.add.add_source");
|
|
||||||
when.wait(200);
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
sources: {
|
|
||||||
pmtilestest: {
|
|
||||||
type: "vector",
|
|
||||||
url: "pmtiles://https://data.source.coop/protomaps/openstreetmap/v4.pmtiles",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("add new raster source", () => {
|
|
||||||
const sourceId = "rastertest";
|
|
||||||
when.setValue("modal:sources.add.source_id", sourceId);
|
|
||||||
when.select("modal:sources.add.source_type", "tile_raster");
|
|
||||||
when.select("modal:sources.add.scheme_type", "xyz");
|
|
||||||
when.setValue("modal:sources.add.tile_size", "128");
|
|
||||||
when.click("modal:sources.add.add_source");
|
|
||||||
when.wait(200);
|
|
||||||
then(
|
|
||||||
get.styleFromLocalStorage().then((style) => style.sources[sourceId])
|
|
||||||
).shouldInclude({
|
|
||||||
tileSize: 128,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("inspect", () => {
|
describe("inspect", () => {
|
||||||
it("toggle", () => {
|
it("toggle", () => {
|
||||||
// There is no assertion in this test
|
driver.setStyle('geojson');
|
||||||
when.setStyle("geojson");
|
|
||||||
when.select("maputnik-select", "inspect");
|
driver.select(driver.getDataAttribute("nav:inspect", "select"), "inspect");
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|
||||||
describe("style settings", () => {
|
describe("style settings", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
when.click("nav:settings");
|
driver.click(driver.getDataAttribute("nav:settings"));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when click name filed spec information", () => {
|
it("name", () => {
|
||||||
beforeEach(() => {
|
driver.setValue(driver.getDataAttribute("modal:settings.name"), "foobar");
|
||||||
when.click("field-doc-button-Name");
|
driver.click(driver.getDataAttribute("modal:settings.owner"));
|
||||||
});
|
|
||||||
|
|
||||||
it("should show the spec information", () => {
|
driver.isStyleStoreEqual((obj) => obj.name, "foobar");
|
||||||
then(get.elementsText("spec-field-doc")).shouldInclude(
|
})
|
||||||
"name for the style"
|
it("owner", () => {
|
||||||
);
|
driver.setValue(driver.getDataAttribute("modal:settings.owner"), "foobar")
|
||||||
});
|
driver.click(driver.getDataAttribute("modal:settings.name"));
|
||||||
});
|
|
||||||
|
|
||||||
describe("when set name and click owner", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.setValue("modal:settings.name", "foobar");
|
|
||||||
when.click("modal:settings.owner");
|
|
||||||
when.wait(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("show name specifications", () => {
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
name: "foobar",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when set owner and click name", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.setValue("modal:settings.owner", "foobar");
|
|
||||||
when.click("modal:settings.name");
|
|
||||||
when.wait(200);
|
|
||||||
});
|
|
||||||
it("should update owner in local storage", () => {
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
owner: "foobar",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
driver.isStyleStoreEqual((obj) => obj.owner, "foobar");
|
||||||
|
})
|
||||||
it("sprite url", () => {
|
it("sprite url", () => {
|
||||||
when.setTextInJsonEditor("\"http://example.com\"");
|
driver.setValue(driver.getDataAttribute("modal:settings.sprite"), "http://example.com")
|
||||||
when.click("modal:settings.name");
|
driver.click(driver.getDataAttribute("modal:settings.name"));
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
sprite: "http://example.com",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sprite object", () => {
|
|
||||||
when.setTextInJsonEditor(JSON.stringify([{ id: "1", url: "2" }]));
|
|
||||||
|
|
||||||
when.click("modal:settings.name");
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
sprite: [{ id: "1", url: "2" }],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
driver.isStyleStoreEqual((obj) => obj.sprite, "http://example.com");
|
||||||
|
})
|
||||||
it("glyphs url", () => {
|
it("glyphs url", () => {
|
||||||
const glyphsUrl = "http://example.com/{fontstack}/{range}.pbf";
|
var glyphsUrl = "http://example.com/{fontstack}/{range}.pbf"
|
||||||
when.setValue("modal:settings.glyphs", glyphsUrl);
|
driver.setValue(driver.getDataAttribute("modal:settings.glyphs"), glyphsUrl);
|
||||||
when.click("modal:settings.name");
|
driver.click(driver.getDataAttribute("modal:settings.name"));
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
glyphs: glyphsUrl,
|
driver.isStyleStoreEqual((obj) => obj.glyphs, glyphsUrl);
|
||||||
});
|
})
|
||||||
});
|
|
||||||
|
|
||||||
it("maptiler access token", () => {
|
it("maptiler access token", () => {
|
||||||
const apiKey = "testing123";
|
var apiKey = "testing123";
|
||||||
when.setValue(
|
driver.setValue(driver.getDataAttribute("modal:settings.maputnik:openmaptiles_access_token"), apiKey);
|
||||||
"modal:settings.maputnik:openmaptiles_access_token",
|
driver.click(driver.getDataAttribute("modal:settings.name"));
|
||||||
apiKey
|
|
||||||
);
|
driver.isStyleStoreEqual((obj) => obj.metadata["maputnik:openmaptiles_access_token"], apiKey);
|
||||||
when.click("modal:settings.name");
|
})
|
||||||
then(
|
|
||||||
get.styleFromLocalStorage().then((style) => style.metadata)
|
|
||||||
).shouldInclude({
|
|
||||||
"maputnik:openmaptiles_access_token": apiKey,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("thunderforest access token", () => {
|
it("thunderforest access token", () => {
|
||||||
const apiKey = "testing123";
|
var apiKey = "testing123";
|
||||||
when.setValue(
|
driver.setValue(driver.getDataAttribute("modal:settings.maputnik:thunderforest_access_token"), apiKey);
|
||||||
"modal:settings.maputnik:thunderforest_access_token",
|
driver.click(driver.getDataAttribute("modal:settings.name"));
|
||||||
apiKey
|
|
||||||
);
|
|
||||||
when.click("modal:settings.name");
|
|
||||||
then(
|
|
||||||
get.styleFromLocalStorage().then((style) => style.metadata)
|
|
||||||
).shouldInclude({ "maputnik:thunderforest_access_token": apiKey });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("stadia access token", () => {
|
driver.isStyleStoreEqual((obj) => obj.metadata["maputnik:thunderforest_access_token"], apiKey);
|
||||||
const apiKey = "testing123";
|
})
|
||||||
when.setValue(
|
|
||||||
"modal:settings.maputnik:stadia_access_token",
|
|
||||||
apiKey
|
|
||||||
);
|
|
||||||
when.click("modal:settings.name");
|
|
||||||
then(
|
|
||||||
get.styleFromLocalStorage().then((style) => style.metadata)
|
|
||||||
).shouldInclude({ "maputnik:stadia_access_token": apiKey });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("locationiq access token", () => {
|
|
||||||
const apiKey = "testing123";
|
|
||||||
when.setValue(
|
|
||||||
"modal:settings.maputnik:locationiq_access_token",
|
|
||||||
apiKey
|
|
||||||
);
|
|
||||||
when.click("modal:settings.name");
|
|
||||||
then(
|
|
||||||
get.styleFromLocalStorage().then((style) => style.metadata)
|
|
||||||
).shouldInclude({ "maputnik:locationiq_access_token": apiKey });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("style projection mercator", () => {
|
|
||||||
when.select("modal:settings.projection", "mercator");
|
|
||||||
then(
|
|
||||||
get.styleFromLocalStorage().then((style) => style.projection)
|
|
||||||
).shouldInclude({ type: "mercator" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("style projection globe", () => {
|
|
||||||
when.select("modal:settings.projection", "globe");
|
|
||||||
then(
|
|
||||||
get.styleFromLocalStorage().then((style) => style.projection)
|
|
||||||
).shouldInclude({ type: "globe" });
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it("style projection vertical-perspective", () => {
|
|
||||||
when.select("modal:settings.projection", "vertical-perspective");
|
|
||||||
then(
|
|
||||||
get.styleFromLocalStorage().then((style) => style.projection)
|
|
||||||
).shouldInclude({ type: "vertical-perspective" });
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
it("style renderer", () => {
|
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");
|
driver.select(driver.getDataAttribute("modal:settings.maputnik:renderer"), "ol");
|
||||||
then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual(
|
driver.isSelected(driver.getDataAttribute("modal:settings.maputnik:renderer"), "ol");
|
||||||
"ol"
|
|
||||||
);
|
driver.click(driver.getDataAttribute("modal:settings.name"));
|
||||||
|
|
||||||
when.click("modal:settings.name");
|
driver.isStyleStoreEqual((obj) => obj.metadata["maputnik:renderer"], "ol");
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
})
|
||||||
metadata: { "maputnik:renderer": "ol" },
|
})
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
it("include API key when change renderer", () => {
|
|
||||||
|
|
||||||
when.click("modal:settings.close-modal");
|
|
||||||
when.click("nav:open");
|
|
||||||
|
|
||||||
get.elementByAttribute("aria-label", "MapTiler Basic").should("exist").click();
|
|
||||||
when.wait(1000);
|
|
||||||
when.click("nav:settings");
|
|
||||||
|
|
||||||
when.select("modal:settings.maputnik:renderer", "mlgljs");
|
|
||||||
then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual(
|
|
||||||
"mlgljs"
|
|
||||||
);
|
|
||||||
|
|
||||||
when.select("modal:settings.maputnik:renderer", "ol");
|
|
||||||
then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual(
|
|
||||||
"ol"
|
|
||||||
);
|
|
||||||
|
|
||||||
given.intercept("https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key=*", "tileRequest", "GET");
|
|
||||||
|
|
||||||
when.select("modal:settings.maputnik:renderer", "mlgljs");
|
|
||||||
then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual(
|
|
||||||
"mlgljs"
|
|
||||||
);
|
|
||||||
|
|
||||||
when.waitForResponse("tileRequest").its("request").its("url").should("include", `https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key=${tokens.openmaptiles}`);
|
|
||||||
when.waitForResponse("tileRequest").its("request").its("url").should("include", `https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key=${tokens.openmaptiles}`);
|
|
||||||
when.waitForResponse("tileRequest").its("request").its("url").should("include", `https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key=${tokens.openmaptiles}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("add layer", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.setStyle("layer");
|
|
||||||
when.modal.open();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows duplicate id error", () => {
|
|
||||||
when.setValue("add-layer.layer-id.input", "background");
|
|
||||||
when.click("add-layer");
|
|
||||||
then(get.elementByTestId("modal:add-layer")).shouldExist();
|
|
||||||
then(get.element(".maputnik-modal-error")).shouldContainText(
|
|
||||||
"Layer ID already exists"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("sources", () => {
|
describe("sources", () => {
|
||||||
it("toggle");
|
it("toggle")
|
||||||
});
|
})
|
||||||
|
})
|
||||||
describe("global state", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.click("nav:global-state");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("add variable", () => {
|
|
||||||
when.wait(100);
|
|
||||||
when.click("global-state-add-variable");
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
state: { key1: { default: "value" } },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it("add multiple variables", () => {
|
|
||||||
when.click("global-state-add-variable");
|
|
||||||
when.click("global-state-add-variable");
|
|
||||||
when.click("global-state-add-variable");
|
|
||||||
when.wait(100);
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
state: { key1: { default: "value" }, key2: { default: "value" }, key3: { default: "value" } },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("remove variable", () => {
|
|
||||||
when.click("global-state-add-variable");
|
|
||||||
when.click("global-state-add-variable");
|
|
||||||
when.click("global-state-add-variable");
|
|
||||||
when.click("global-state-remove-variable", 0);
|
|
||||||
when.wait(100);
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
state: { key2: { default: "value" }, key3: { default: "value" } },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("edit variable key", () => {
|
|
||||||
when.click("global-state-add-variable");
|
|
||||||
when.wait(100);
|
|
||||||
when.setValue("global-state-variable-key:0", "mykey");
|
|
||||||
when.typeKeys("{enter}");
|
|
||||||
when.wait(100);
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
state: { mykey: { default: "value" } },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("edit variable value", () => {
|
|
||||||
when.click("global-state-add-variable");
|
|
||||||
when.wait(100);
|
|
||||||
when.setValue("global-state-variable-value:0", "myvalue");
|
|
||||||
when.typeKeys("{enter}");
|
|
||||||
when.wait(100);
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
state: { key1: { default: "myvalue" } },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("error panel", () => {
|
|
||||||
it("not visible when no errors", () => {
|
|
||||||
then(get.element("maputnik-message-panel-error")).shouldNotExist();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("visible on style error", () => {
|
|
||||||
when.modal.open();
|
|
||||||
when.modal.fillLayers({
|
|
||||||
type: "circle",
|
|
||||||
layer: "invalid",
|
|
||||||
});
|
|
||||||
then(get.element(".maputnik-message-panel-error")).shouldBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Handle localStorage QuotaExceededError", () => {
|
|
||||||
it("handles quota exceeded error when opening style from URL", () => {
|
|
||||||
// Clear localStorage to start fresh
|
|
||||||
cy.clearLocalStorage();
|
|
||||||
|
|
||||||
// fill localStorage until we get a QuotaExceededError
|
|
||||||
cy.window().then(win => {
|
|
||||||
let chunkSize = 1000;
|
|
||||||
const chunk = new Array(chunkSize).join("x");
|
|
||||||
let index = 0;
|
|
||||||
|
|
||||||
// Keep adding until we hit the quota
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
const key = `maputnik:fill-${index++}`;
|
|
||||||
win.localStorage.setItem(key, chunk);
|
|
||||||
} catch (e: any) {
|
|
||||||
// Verify it's a quota error
|
|
||||||
if (e.name === "QuotaExceededError") {
|
|
||||||
if (chunkSize <= 1) return;
|
|
||||||
else {
|
|
||||||
chunkSize /= 2;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw e; // Unexpected error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Open the style via URL input
|
|
||||||
when.click("nav:open");
|
|
||||||
when.setValue("modal:open.url.input", get.exampleFileUrl());
|
|
||||||
when.click("modal:open.url.button");
|
|
||||||
|
|
||||||
then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
|
|
||||||
then(get.styleFromLocalStorage()).shouldExist();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -15,3 +15,4 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "test-style",
|
|
||||||
"version": 8,
|
|
||||||
"name": "Test Style",
|
|
||||||
"metadata": {
|
|
||||||
"maputnik:renderer": "mlgljs"
|
|
||||||
},
|
|
||||||
"sources": {
|
|
||||||
"example": {
|
|
||||||
"type": "geojson",
|
|
||||||
"data": {
|
|
||||||
"type": "FeatureCollection",
|
|
||||||
"features":[{
|
|
||||||
"type": "Feature",
|
|
||||||
"properties": {
|
|
||||||
"name": "Dinagat Islands"
|
|
||||||
},
|
|
||||||
"geometry":{
|
|
||||||
"type": "Point",
|
|
||||||
"coordinates": [125.6, 10.1]
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"glyphs": "https://www.glyph-server.com/fonts/{fontstack}/{range}.pbf",
|
|
||||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
|
||||||
"layers": [
|
|
||||||
{
|
|
||||||
"id": "label",
|
|
||||||
"type": "symbol",
|
|
||||||
"source": "example",
|
|
||||||
"layout": {
|
|
||||||
"text-font": ["Font"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "test-style",
|
|
||||||
"center": [50,50],
|
|
||||||
"zoom": 5,
|
|
||||||
"version": 8,
|
|
||||||
"name": "Test Style",
|
|
||||||
"sources": {
|
|
||||||
"rectangles": {
|
|
||||||
"type": "geojson",
|
|
||||||
"data": {
|
|
||||||
"type": "FeatureCollection",
|
|
||||||
"features": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
|
||||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
|
||||||
"layers": []
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "test-style",
|
|
||||||
"center": [0,51],
|
|
||||||
"zoom": 7,
|
|
||||||
"version": 8,
|
|
||||||
"name": "Test Style",
|
|
||||||
"sources": {
|
|
||||||
"rectangles": {
|
|
||||||
"type": "geojson",
|
|
||||||
"data": {
|
|
||||||
"type": "FeatureCollection",
|
|
||||||
"features": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"layers": []
|
|
||||||
}
|
|
||||||
@@ -1,114 +1,12 @@
|
|||||||
{
|
{
|
||||||
"id": "test-style",
|
"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": []
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,4 +31,4 @@
|
|||||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
"layers": []
|
"layers": []
|
||||||
}
|
}
|
||||||
@@ -26,4 +26,4 @@
|
|||||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
"layers": []
|
"layers": []
|
||||||
}
|
}
|
||||||
@@ -15,4 +15,4 @@
|
|||||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
"layers": []
|
"layers": []
|
||||||
}
|
}
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 8,
|
|
||||||
"sources": {
|
|
||||||
"rectangles": {
|
|
||||||
"type": "geojson",
|
|
||||||
"data": {
|
|
||||||
"type": "FeatureCollection",
|
|
||||||
"features": [
|
|
||||||
{
|
|
||||||
"type": "Feature",
|
|
||||||
"properties": {},
|
|
||||||
"geometry": {
|
|
||||||
"type": "Polygon",
|
|
||||||
"coordinates": [
|
|
||||||
[
|
|
||||||
[
|
|
||||||
-130.78125,
|
|
||||||
-33.13755119234615
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-130.78125,
|
|
||||||
63.548552232036414
|
|
||||||
],
|
|
||||||
[
|
|
||||||
15.468749999999998,
|
|
||||||
63.548552232036414
|
|
||||||
],
|
|
||||||
[
|
|
||||||
15.468749999999998,
|
|
||||||
-33.13755119234615
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-130.78125,
|
|
||||||
-33.13755119234615
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "Feature",
|
|
||||||
"properties": {},
|
|
||||||
"geometry": {
|
|
||||||
"type": "Polygon",
|
|
||||||
"coordinates": [
|
|
||||||
[
|
|
||||||
[
|
|
||||||
-48.515625,
|
|
||||||
-54.97761367069625
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-48.515625,
|
|
||||||
36.5978891330702
|
|
||||||
],
|
|
||||||
[
|
|
||||||
169.45312499999997,
|
|
||||||
36.5978891330702
|
|
||||||
],
|
|
||||||
[
|
|
||||||
169.45312499999997,
|
|
||||||
-54.97761367069625
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-48.515625,
|
|
||||||
-54.97761367069625
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"layers": [
|
|
||||||
{
|
|
||||||
"id": "background",
|
|
||||||
"type": "background",
|
|
||||||
"paint": {
|
|
||||||
"background-color": "white"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "rectangles",
|
|
||||||
"type": "fill",
|
|
||||||
"source": "rectangles",
|
|
||||||
"paint": {
|
|
||||||
"fill-opacity": 0.3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -34,4 +34,4 @@
|
|||||||
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
|
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
||||||
<title>Components App</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div data-cy-root></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
// ***********************************************************
|
|
||||||
// This example support/component.ts is processed and
|
|
||||||
// loaded automatically before your test files.
|
|
||||||
//
|
|
||||||
// This is a great place to put global configuration and
|
|
||||||
// behavior that modifies Cypress.
|
|
||||||
//
|
|
||||||
// You can change the location of this file or turn off
|
|
||||||
// automatically serving support files with the
|
|
||||||
// 'supportFile' configuration option.
|
|
||||||
//
|
|
||||||
// You can read more here:
|
|
||||||
// https://on.cypress.io/configuration
|
|
||||||
// ***********************************************************
|
|
||||||
|
|
||||||
// Import commands.js using ES2015 syntax:
|
|
||||||
import "./commands";
|
|
||||||
|
|
||||||
import { mount } from "cypress/react";
|
|
||||||
|
|
||||||
// Augment the Cypress namespace to include type definitions for
|
|
||||||
// your custom command.
|
|
||||||
// Alternatively, can be defined in cypress/support/component.d.ts
|
|
||||||
// with a <reference path="./component" /> at the top of your spec.
|
|
||||||
declare global {
|
|
||||||
/* eslint-disable @typescript-eslint/no-namespace */
|
|
||||||
namespace Cypress {
|
|
||||||
interface Chainable {
|
|
||||||
mount: typeof mount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Cypress.Commands.add("mount", mount);
|
|
||||||
|
|
||||||
// Example use:
|
|
||||||
// cy.mount(<MyComponent />)
|
|
||||||
@@ -14,9 +14,7 @@
|
|||||||
// ***********************************************************
|
// ***********************************************************
|
||||||
|
|
||||||
// Import commands.js using ES2015 syntax:
|
// Import commands.js using ES2015 syntax:
|
||||||
import "@cypress/code-coverage/support";
|
import './commands'
|
||||||
import "cypress-plugin-tab";
|
|
||||||
import "./commands";
|
|
||||||
|
|
||||||
// Alternatively you can use CommonJS syntax:
|
// Alternatively you can use CommonJS syntax:
|
||||||
// require('./commands')
|
// require('./commands')
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
|
||||||
*.o
|
|
||||||
*.a
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Folders
|
|
||||||
_obj
|
|
||||||
_test
|
|
||||||
editor
|
|
||||||
|
|
||||||
# Architecture specific extensions/prefixes
|
|
||||||
*.[568vq]
|
|
||||||
[568vq].out
|
|
||||||
|
|
||||||
*.cgo1.go
|
|
||||||
*.cgo2.c
|
|
||||||
_cgo_defun.c
|
|
||||||
_cgo_gotypes.go
|
|
||||||
_cgo_export.*
|
|
||||||
|
|
||||||
_testmain.go
|
|
||||||
|
|
||||||
*.exe
|
|
||||||
*.test
|
|
||||||
*.prof
|
|
||||||
|
|
||||||
# Binary version of pubilic/editor
|
|
||||||
rice-box.go
|
|
||||||
|
|
||||||
# Built binary
|
|
||||||
maputnik
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2016 Maputnik
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
SOURCEDIR=.
|
|
||||||
SOURCES := $(shell find $(SOURCEDIR) -name '*.go')
|
|
||||||
BINARY=maputnik
|
|
||||||
VERSION := $(shell node -p "require('../package.json').version")
|
|
||||||
GOBIN := $(or $(shell if [ -d /go/bin ]; then echo "/go/bin"; fi),$(HOME)/go/bin)
|
|
||||||
|
|
||||||
all: $(BINARY)
|
|
||||||
|
|
||||||
$(BINARY): $(GOBIN)/gox $(GOBIN)/go-winres $(SOURCES) version.go rice-box.go winres/winres.json
|
|
||||||
$(GOBIN)/go-winres make --product-version=$(VERSION)
|
|
||||||
$(GOBIN)/gox -osarch "windows/amd64 linux/amd64 darwin/amd64" -output "bin/{{.OS}}/${BINARY}"
|
|
||||||
|
|
||||||
bin/linux/$(BINARY): $(GOBIN)/gox $(GOBIN)/go-winres $(SOURCES) version.go rice-box.go winres/winres.json
|
|
||||||
$(GOBIN)/go-winres make --product-version=$(VERSION)
|
|
||||||
$(GOBIN)/gox -osarch "linux/amd64" -output "bin/{{.OS}}/${BINARY}"
|
|
||||||
|
|
||||||
winres/winres.json: winres/winres_template.json
|
|
||||||
sed 's/{{.Version}}/$(VERSION)/g' winres/winres_template.json > $@
|
|
||||||
|
|
||||||
$(GOBIN)/go-winres:
|
|
||||||
go install github.com/tc-hib/go-winres@latest
|
|
||||||
|
|
||||||
# Copy the current release into ./editor/maputnik so it can be
|
|
||||||
# embedded in the binary
|
|
||||||
editor/pull_release:
|
|
||||||
mkdir -p editor
|
|
||||||
cp -r ../dist/* editor
|
|
||||||
|
|
||||||
$(GOBIN)/gox:
|
|
||||||
go install github.com/mitchellh/gox@v1.0.1
|
|
||||||
|
|
||||||
$(GOBIN)/rice:
|
|
||||||
go install github.com/GeertJohan/go.rice/rice@v1.0.3
|
|
||||||
|
|
||||||
# Embed the current version number in the executable by writing version.go
|
|
||||||
.PHONY: version.go
|
|
||||||
version.go:
|
|
||||||
@printf "// DO NOT EDIT: Autogenerated by Makefile\n" > version.go
|
|
||||||
@printf "package main\n" >> version.go
|
|
||||||
@printf "const Version = \"$(VERSION)\"\n" >> version.go
|
|
||||||
|
|
||||||
rice-box.go: $(GOBIN)/rice editor/pull_release
|
|
||||||
$(GOBIN)/rice embed-go
|
|
||||||
|
|
||||||
.PHONY: clean
|
|
||||||
clean:
|
|
||||||
rm -rf editor && rm -f rice-box.go && rm -rf bin
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
# Maputnik Desktop [][github-action-ci]
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
A Golang based cross platform executable for integrating Maputnik locally.
|
|
||||||
This binary packages up the JavaScript and CSS bundle produced by maputnik
|
|
||||||
and embeds it in the program for easy distribution. It also allows
|
|
||||||
exposing a local style file and work on it both in Maputnik and with your favorite
|
|
||||||
editor.
|
|
||||||
|
|
||||||
Report issues on [maplibre/maputnik](https://github.com/maplibre/maputnik).
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
You can download a zip file containing desktop binaries for Linux, OSX and Windows from [the latest releases of **maplibre/maputnik**](https://github.com/maplibre/maputnik/releases/latest).
|
|
||||||
|
|
||||||
### Usage
|
|
||||||
|
|
||||||
Simply start up a web server and access the Maputnik editor GUI at `localhost:8000`.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
maputnik
|
|
||||||
```
|
|
||||||
|
|
||||||
Expose a local style file to Maputnik allowing the web based editor
|
|
||||||
to save to the local filesystem.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
maputnik --file basic-v9.json
|
|
||||||
```
|
|
||||||
|
|
||||||
Watch the local style for changes and inform the editor via web socket.
|
|
||||||
This makes it possible to edit the style with a local text editor and still
|
|
||||||
use Maputnik.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
maputnik --watch --file basic-v9.json
|
|
||||||
```
|
|
||||||
|
|
||||||
Choose a local port to listen on, instead of using the default port 8000.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
maputnik --port 8001
|
|
||||||
```
|
|
||||||
|
|
||||||
Specify a path to a directory which, if it exists, will be served under http://localhost:8000/static/ .
|
|
||||||
Could be used to serve sprites and glyphs.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
maputnik --static ./localFolder
|
|
||||||
```
|
|
||||||
|
|
||||||
### API
|
|
||||||
|
|
||||||
`maputnik` exposes the configured styles via a HTTP API.
|
|
||||||
|
|
||||||
| Method | Description |
|
|
||||||
| ------------------------ | ------------------------------------------------------ |
|
|
||||||
| `GET /styles` | List the ID of all configured style files |
|
|
||||||
| `GET /styles/{filename}` | Get contents of a single style file |
|
|
||||||
| `PUT /styles/{filename}` | Update contents of a style file |
|
|
||||||
| `WEBSOCKET /ws` | Listen to change events for the configured style files |
|
|
||||||
|
|
||||||
### Build
|
|
||||||
|
|
||||||
From the root of the [maplibre/maputnik](https://github.com/maplibre/maputnik) project, install the deps and run the desktop-build command.
|
|
||||||
|
|
||||||
```
|
|
||||||
npm install
|
|
||||||
npm run build-desktop
|
|
||||||
```
|
|
||||||
|
|
||||||
You should now find the `maputnik` binary in your `desktop/bin` directory.
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
|
||||||
|
|
||||||
func StyleFileAccessor(filename string) styleFileAccessor {
|
|
||||||
return styleFileAccessor{filename, styleId(filename)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func styleId(filename string) string {
|
|
||||||
raw, err := ioutil.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
log.Panicln(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var spec styleSpec
|
|
||||||
err = json.Unmarshal(raw, &spec)
|
|
||||||
if err != nil {
|
|
||||||
log.Panicln(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if spec.Id == "" {
|
|
||||||
fmt.Println("No id in style")
|
|
||||||
}
|
|
||||||
return spec.Id
|
|
||||||
}
|
|
||||||
|
|
||||||
type styleSpec struct {
|
|
||||||
Id string `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allows access to a single style file
|
|
||||||
type styleFileAccessor struct {
|
|
||||||
filename string
|
|
||||||
id string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fa styleFileAccessor) ListFiles(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
encoder := json.NewEncoder(w)
|
|
||||||
encoder.Encode([]string{fa.id})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fa styleFileAccessor) ReadFile(w http.ResponseWriter, r *http.Request) {
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
_ = vars["styleId"]
|
|
||||||
|
|
||||||
//TODO: Choose right file
|
|
||||||
// right now we just return the single file we know of
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
raw, err := ioutil.ReadFile(fa.filename)
|
|
||||||
if err != nil {
|
|
||||||
log.Panicln(err)
|
|
||||||
}
|
|
||||||
w.Write(raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fa styleFileAccessor) SaveFile(w http.ResponseWriter, r *http.Request) {
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
_ = vars["styleId"]
|
|
||||||
|
|
||||||
//TODO: Save to right file
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
body, _ := ioutil.ReadAll(r.Body)
|
|
||||||
var out bytes.Buffer
|
|
||||||
json.Indent(&out, body, "", " ")
|
|
||||||
|
|
||||||
if err := ioutil.WriteFile(fa.filename, out.Bytes(), 0666); err != nil {
|
|
||||||
log.Fatalf("Can not copy from request to file: %s", err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
package filewatch
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/fsnotify/fsnotify"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
)
|
|
||||||
|
|
||||||
var upgrader = websocket.Upgrader{
|
|
||||||
ReadBufferSize: 1024,
|
|
||||||
WriteBufferSize: 1024,
|
|
||||||
CheckOrigin: func(r *http.Request) bool { return true },
|
|
||||||
}
|
|
||||||
|
|
||||||
func writer(ws *websocket.Conn, filename string) {
|
|
||||||
watcher, err := fsnotify.NewWatcher()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
defer watcher.Close()
|
|
||||||
|
|
||||||
done := make(chan bool)
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case event := <-watcher.Events:
|
|
||||||
if event.Op&fsnotify.Write == fsnotify.Write {
|
|
||||||
log.Println("Modified file:", event.Name)
|
|
||||||
var p []byte
|
|
||||||
var err error
|
|
||||||
|
|
||||||
p, err = ioutil.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if p != nil {
|
|
||||||
if err := ws.WriteMessage(websocket.TextMessage, p); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case err := <-watcher.Errors:
|
|
||||||
log.Println("Watch error:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err = watcher.Add(filename); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
<-done
|
|
||||||
}
|
|
||||||
|
|
||||||
func ServeWebsocketFileWatcher(filename string, w http.ResponseWriter, r *http.Request) {
|
|
||||||
ws, err := upgrader.Upgrade(w, r, nil)
|
|
||||||
if err != nil {
|
|
||||||
if _, ok := err.(websocket.HandshakeError); !ok {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
writer(ws, filename)
|
|
||||||
defer ws.Close()
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
module maputnik/desktop
|
|
||||||
|
|
||||||
go 1.19
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/GeertJohan/go.rice v1.0.3
|
|
||||||
github.com/fsnotify/fsnotify v1.6.0
|
|
||||||
github.com/gorilla/handlers v1.5.1
|
|
||||||
github.com/gorilla/mux v1.8.0
|
|
||||||
github.com/gorilla/websocket v1.5.0
|
|
||||||
github.com/maputnik/desktop v1.0.7
|
|
||||||
github.com/urfave/cli v1.22.12
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/GeertJohan/go.incremental v1.0.0 // indirect
|
|
||||||
github.com/akavel/rsrc v0.8.0 // indirect
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
|
||||||
github.com/daaku/go.zipexe v1.0.2 // indirect
|
|
||||||
github.com/felixge/httpsnoop v1.0.1 // indirect
|
|
||||||
github.com/jessevdk/go-flags v1.4.0 // indirect
|
|
||||||
github.com/nkovacs/streamquote v1.0.0 // indirect
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
|
||||||
github.com/valyala/fasttemplate v1.0.1 // indirect
|
|
||||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956 // indirect
|
|
||||||
)
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
|
||||||
github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg=
|
|
||||||
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
|
|
||||||
github.com/GeertJohan/go.rice v1.0.3 h1:k5viR+xGtIhF61125vCE1cmJ5957RQGXG6dmbaWZSmI=
|
|
||||||
github.com/GeertJohan/go.rice v1.0.3/go.mod h1:XVdrU4pW00M4ikZed5q56tPf1v2KwnIKeIdc9CBYNt4=
|
|
||||||
github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw=
|
|
||||||
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
|
||||||
github.com/daaku/go.zipexe v1.0.2 h1:Zg55YLYTr7M9wjKn8SY/WcpuuEi+kR2u4E8RhvpyXmk=
|
|
||||||
github.com/daaku/go.zipexe v1.0.2/go.mod h1:5xWogtqlYnfBXkSB1o9xysukNP9GTvaNkqzUZbt3Bw8=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
|
|
||||||
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
|
||||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
|
||||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
|
||||||
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
|
|
||||||
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
|
|
||||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
|
||||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
|
||||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
|
||||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
|
||||||
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
|
|
||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
|
||||||
github.com/maputnik/desktop v1.0.7 h1:rdFg7emIJOT3YsZpwqSChmWtMOvu+T4h6WwVQAZP9n4=
|
|
||||||
github.com/maputnik/desktop v1.0.7/go.mod h1:wmDjHUztx9jOBz0I22589yWguAGdV/sEM57YANpN8oQ=
|
|
||||||
github.com/nkovacs/streamquote v1.0.0 h1:PmVIV08Zlx2lZK5fFZlMZ04eHcDTIFJCv/5/0twVUow=
|
|
||||||
github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
|
||||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
|
||||||
github.com/urfave/cli v1.22.12 h1:igJgVw1JdKH+trcLWLeLwZjU9fEfPesQ+9/e4MQ44S8=
|
|
||||||
github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
|
||||||
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
|
|
||||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
|
||||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956 h1:XeJjHH1KiLpKGb6lvMiksZ9l0fVUh+AmGcm0nOMEBOY=
|
|
||||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"github.com/GeertJohan/go.rice"
|
|
||||||
"github.com/gorilla/handlers"
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/maputnik/desktop/filewatch"
|
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
app := cli.NewApp()
|
|
||||||
app.Name = "maputnik"
|
|
||||||
app.Usage = "Server for integrating Maputnik locally"
|
|
||||||
app.Version = Version
|
|
||||||
|
|
||||||
app.Flags = []cli.Flag{
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "file, f",
|
|
||||||
Usage: "Allow access to JSON style from web client",
|
|
||||||
},
|
|
||||||
&cli.BoolFlag{
|
|
||||||
Name: "watch",
|
|
||||||
Usage: "Notify web client about JSON style file changes",
|
|
||||||
},
|
|
||||||
&cli.IntFlag{
|
|
||||||
Name: "port",
|
|
||||||
Value: 8000,
|
|
||||||
Usage: "TCP port to listen on",
|
|
||||||
},
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "static",
|
|
||||||
Usage: "Serve directory under /static/",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
app.Action = func(c *cli.Context) error {
|
|
||||||
gui := http.FileServer(rice.MustFindBox("editor").HTTPBox())
|
|
||||||
|
|
||||||
router := mux.NewRouter().StrictSlash(true)
|
|
||||||
|
|
||||||
filename := c.String("file")
|
|
||||||
if filename != "" {
|
|
||||||
fmt.Printf("%s is accessible via Maputnik\n", filename)
|
|
||||||
// Allow access to reading and writing file on the local system
|
|
||||||
path, _ := filepath.Abs(filename)
|
|
||||||
accessor := StyleFileAccessor(path)
|
|
||||||
router.Path("/styles").Methods("GET").HandlerFunc(accessor.ListFiles)
|
|
||||||
router.Path("/styles/{styleId}").Methods("GET").HandlerFunc(accessor.ReadFile)
|
|
||||||
router.Path("/styles/{styleId}").Methods("PUT").HandlerFunc(accessor.SaveFile)
|
|
||||||
|
|
||||||
// Register websocket to notify we clients about file changes
|
|
||||||
if c.Bool("watch") {
|
|
||||||
router.Path("/ws").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
filewatch.ServeWebsocketFileWatcher(filename, w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
staticDir := c.String("static")
|
|
||||||
if staticDir != "" {
|
|
||||||
h := http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir)))
|
|
||||||
router.PathPrefix("/static/").Handler(h)
|
|
||||||
}
|
|
||||||
|
|
||||||
router.PathPrefix("/").Handler(http.StripPrefix("/", gui))
|
|
||||||
loggedRouter := handlers.LoggingHandler(os.Stdout, router)
|
|
||||||
corsRouter := handlers.CORS(handlers.AllowedHeaders([]string{"Content-Type"}), handlers.AllowedMethods([]string{"GET", "PUT"}), handlers.AllowedOrigins([]string{"*"}), handlers.AllowCredentials())(loggedRouter)
|
|
||||||
|
|
||||||
fmt.Printf("Exposing Maputnik on http://localhost:%d\n", c.Int("port"))
|
|
||||||
return http.ListenAndServe(fmt.Sprintf(":%d", c.Int("port")), corsRouter)
|
|
||||||
}
|
|
||||||
|
|
||||||
app.Run(os.Args)
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
{
|
|
||||||
"RT_GROUP_ICON": {
|
|
||||||
"APP": {
|
|
||||||
"0000": [
|
|
||||||
"../../src/img/maputnik.png"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"RT_MANIFEST": {
|
|
||||||
"#1": {
|
|
||||||
"0409": {
|
|
||||||
"identity": {
|
|
||||||
"name": "Maputnik",
|
|
||||||
"version": "{{.Version}}"
|
|
||||||
},
|
|
||||||
"description": "A MapLibre GL visual style editor",
|
|
||||||
"minimum-os": "win7",
|
|
||||||
"execution-level": "as invoker",
|
|
||||||
"ui-access": false,
|
|
||||||
"auto-elevate": false,
|
|
||||||
"dpi-awareness": "system",
|
|
||||||
"disable-theming": false,
|
|
||||||
"disable-window-filtering": false,
|
|
||||||
"high-resolution-scrolling-aware": false,
|
|
||||||
"ultra-high-resolution-scrolling-aware": false,
|
|
||||||
"long-path-aware": false,
|
|
||||||
"printer-driver-isolation": false,
|
|
||||||
"gdi-scaling": false,
|
|
||||||
"segment-heap": false,
|
|
||||||
"use-common-controls-v6": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"RT_VERSION": {
|
|
||||||
"#1": {
|
|
||||||
"0000": {
|
|
||||||
"fixed": {
|
|
||||||
"file_version": "{{.Version}}",
|
|
||||||
"product_version": "{{.Version}}"
|
|
||||||
},
|
|
||||||
"info": {
|
|
||||||
"0409": {
|
|
||||||
"Comments": "https://github.com/maplibre/maputnik",
|
|
||||||
"CompanyName": "Maputnik",
|
|
||||||
"FileDescription": "A MapLibre GL visual style editor",
|
|
||||||
"FileVersion": "{{.Version}}",
|
|
||||||
"InternalName": "Maputnik",
|
|
||||||
"LegalCopyright": "MIT License",
|
|
||||||
"LegalTrademarks": "",
|
|
||||||
"OriginalFilename": "Maputnik.exe",
|
|
||||||
"PrivateBuild": "",
|
|
||||||
"ProductName": "Maputnik",
|
|
||||||
"ProductVersion": "{{.Version}}",
|
|
||||||
"SpecialBuild": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import eslint from "@eslint/js";
|
|
||||||
import {defineConfig} from "eslint/config";
|
|
||||||
import stylisticTs from "@stylistic/eslint-plugin";
|
|
||||||
import tseslint from "typescript-eslint";
|
|
||||||
import reactPlugin from "eslint-plugin-react";
|
|
||||||
import reactHooksPlugin from "eslint-plugin-react-hooks";
|
|
||||||
import reactRefreshPlugin from "eslint-plugin-react-refresh";
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
extends: [
|
|
||||||
eslint.configs.recommended,
|
|
||||||
tseslint.configs.recommended,
|
|
||||||
],
|
|
||||||
files: ["**/*.{js,jsx,ts,tsx}"],
|
|
||||||
ignores: [
|
|
||||||
"dist/**/*",
|
|
||||||
],
|
|
||||||
languageOptions: {
|
|
||||||
ecmaVersion: 2024,
|
|
||||||
sourceType: "module",
|
|
||||||
globals: {
|
|
||||||
global: "readonly"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
settings: {
|
|
||||||
react: { version: "18.2" }
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
"react": reactPlugin,
|
|
||||||
"react-hooks": reactHooksPlugin,
|
|
||||||
"react-refresh": reactRefreshPlugin,
|
|
||||||
"@stylistic": stylisticTs
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
"react-refresh/only-export-components": [
|
|
||||||
"warn",
|
|
||||||
{ allowConstantExport: true }
|
|
||||||
],
|
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
|
||||||
"@typescript-eslint/no-unused-vars": [
|
|
||||||
"warn",
|
|
||||||
{
|
|
||||||
varsIgnorePattern: "^_",
|
|
||||||
caughtErrors: "all",
|
|
||||||
caughtErrorsIgnorePattern: "^_",
|
|
||||||
argsIgnorePattern: "^_"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-unused-vars": "off",
|
|
||||||
"react/prop-types": "off",
|
|
||||||
"no-undef": "off",
|
|
||||||
"indent": "off",
|
|
||||||
"@stylistic/indent": ["error", 2],
|
|
||||||
"semi": "off",
|
|
||||||
"@stylistic/semi": ["error", "always"],
|
|
||||||
"quotes": "off",
|
|
||||||
"@stylistic/quotes": ["error", "double", { avoidEscape: true }],
|
|
||||||
"no-var": "error",
|
|
||||||
"@typescript-eslint/no-non-null-asserted-optional-chain": "off",
|
|
||||||
"@typescript-eslint/ban-ts-comment": "off",
|
|
||||||
"@typescript-eslint/no-empty-object-type": "off",
|
|
||||||
"@typescript-eslint/consistent-type-imports": ["error", { "fixStyle": "inline-type-imports" }],
|
|
||||||
|
|
||||||
},
|
|
||||||
linterOptions: {
|
|
||||||
reportUnusedDisableDirectives: true,
|
|
||||||
noInlineConfig: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { defineConfig } from "i18next-cli";
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
locales: ["de", "fr", "he", "it", "ja", "ko", "tr", "zh"],
|
|
||||||
extract: {
|
|
||||||
input: ["src/**/*.{js,jsx,ts,tsx}"],
|
|
||||||
output: "src/locales/{{language}}/{{namespace}}.json",
|
|
||||||
defaultValue: "__STRING_NOT_TRANSLATED__"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Generated
+37743
-11531
File diff suppressed because it is too large
Load Diff
+118
-99
@@ -1,84 +1,76 @@
|
|||||||
{
|
{
|
||||||
"name": "maputnik",
|
"name": "maputnik",
|
||||||
"version": "3.1.0",
|
"version": "2.0.0-pre.1",
|
||||||
"description": "A MapLibre GL visual style editor",
|
"description": "A MapLibre GL visual style editor",
|
||||||
"type": "module",
|
|
||||||
"main": "''",
|
"main": "''",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"stats": "cross-env NODE_OPTIONS=--openssl-legacy-provider webpack --config config/webpack.production.config.js --progress=profile --json > stats.json",
|
||||||
"build": "tsc && vite build --mode=production",
|
"build": "cross-env NODE_OPTIONS=--openssl-legacy-provider webpack --config config/webpack.production.config.js --progress=profile --color",
|
||||||
"build-desktop": "tsc && vite build --mode=desktop && cd desktop && make",
|
"profiling-build": "cross-env NODE_OPTIONS=--openssl-legacy-provider webpack --config config/webpack.profiling.config.js --progress=profile --color",
|
||||||
"build-linux": "tsc && vite build --mode=desktop && cd desktop && make bin/linux/maputnik",
|
|
||||||
"i18n:extract": "npx i18next-cli extract",
|
|
||||||
"lint": "eslint",
|
|
||||||
"test": "cypress run",
|
"test": "cypress run",
|
||||||
"test-unit": "vitest",
|
|
||||||
"test-unit-ci": "vitest run --coverage --reporter=json",
|
|
||||||
"cy:open": "cypress open",
|
"cy:open": "cypress open",
|
||||||
|
"start": "cross-env NODE_OPTIONS=--openssl-legacy-provider webpack-dev-server --progress=profile --color --config config/webpack.config.js",
|
||||||
|
"start-prod": "cross-env NODE_OPTIONS=--openssl-legacy-provider webpack-dev-server --progress=profile --color --config config/webpack.production.config.js",
|
||||||
|
"start-sandbox": "cross-env NODE_OPTIONS=--openssl-legacy-provider webpack-dev-server --disable-host-check --host 0.0.0.0 --progress=profile --color --config config/webpack.production.config.js",
|
||||||
|
"lint-js": "eslint --ext js --ext jsx src test",
|
||||||
"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"
|
"lint": "npm run lint-js && npm run lint-css",
|
||||||
|
"storybook": "cross-env NODE_OPTIONS=--openssl-legacy-provider start-storybook -h 0.0.0.0 -p 6006",
|
||||||
|
"build-storybook": "cross-env NODE_OPTIONS=--openssl-legacy-provider build-storybook -o build/storybook"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/maplibre/maputnik"
|
"url": "https://github.com/maputnik/editor"
|
||||||
},
|
},
|
||||||
"author": "Lukas Martinelli",
|
"author": "Lukas Martinelli",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://github.com/maplibre/maputnik#readme",
|
"homepage": "https://github.com/maputnik/editor#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/lang-json": "^6.0.2",
|
"@babel/runtime": "^7.17.9",
|
||||||
"@codemirror/lint": "^6.9.7",
|
"@mapbox/mapbox-gl-rtl-text": "^0.2.3",
|
||||||
"@codemirror/state": "^6.7.0",
|
"@maplibre/maplibre-gl-style-spec": "^17.0.1",
|
||||||
"@codemirror/theme-one-dark": "^6.1.3",
|
"@mdi/react": "^1.5.0",
|
||||||
"@codemirror/view": "^6.43.4",
|
|
||||||
"@dnd-kit/core": "^6.3.1",
|
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
|
||||||
"@mapbox/mapbox-gl-rtl-text": "^0.4.0",
|
|
||||||
"@maplibre/maplibre-gl-geocoder": "^1.9.4",
|
|
||||||
"@maplibre/maplibre-gl-inspect": "^1.8.2",
|
|
||||||
"@maplibre/maplibre-gl-style-spec": "^25.0.1",
|
|
||||||
"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": "^6.0.2",
|
"codemirror": "^5.65.2",
|
||||||
"color": "^5.0.3",
|
"color": "^4.2.3",
|
||||||
"detect-browser": "^5.3.0",
|
"detect-browser": "^5.3.0",
|
||||||
"downshift": "^9.4.0",
|
|
||||||
"events": "^3.3.0",
|
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"i18next": "^26.3.4",
|
"json-stringify-pretty-compact": "^3.0.0",
|
||||||
"i18next-browser-languagedetector": "^8.2.1",
|
|
||||||
"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",
|
||||||
"lodash": "^4.18.1",
|
"jsonlint": "github:josdejong/jsonlint#85a19d7",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"lodash.capitalize": "^4.2.1",
|
"lodash.capitalize": "^4.2.1",
|
||||||
"lodash.clamp": "^4.0.3",
|
"lodash.clamp": "^4.0.3",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
"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.24.0",
|
"mapbox-gl-inspect": "^1.3.1",
|
||||||
|
"maplibre-gl": "^2.4.0",
|
||||||
"maputnik-design": "github:maputnik/design#172b06c",
|
"maputnik-design": "github:maputnik/design#172b06c",
|
||||||
"ol": "^10.9.0",
|
"ol": "^6.14.1",
|
||||||
"ol-mapbox-style": "^13.4.1",
|
"ol-mapbox-style": "^7.1.1",
|
||||||
"pmtiles": "^4.4.1",
|
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "^19.2.4",
|
"react": "^16.0.0",
|
||||||
"react-accessible-accordion": "^5.0.1",
|
"react-accessible-accordion": "^4.0.0",
|
||||||
"react-aria-menubutton": "^8.0.0",
|
"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": "^19.2.4",
|
"react-dom": "^16.0.0",
|
||||||
"react-i18next": "^17.0.7",
|
"react-file-reader-input": "^2.0.0",
|
||||||
"react-icons": "^5.6.0",
|
"react-icon-base": "^2.1.2",
|
||||||
"react-markdown": "^10.1.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.9",
|
"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
|
||||||
@@ -97,57 +89,84 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"plugins": [
|
||||||
|
"react"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"plugin:react/recommended"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"node": true,
|
||||||
|
"es6": true
|
||||||
|
},
|
||||||
|
"parser": "@babel/eslint-parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 6,
|
||||||
|
"sourceType": "module",
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"impliedStrict": true,
|
||||||
|
"experimentalObjectRestSpread": true,
|
||||||
|
"jsx": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"react": {
|
||||||
|
"version": "detect"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cypress/code-coverage": "^4.0.3",
|
"@babel/core": "^7.17.9",
|
||||||
"@eslint/js": "^10.0.1",
|
"@babel/eslint-parser": "^7.19.1",
|
||||||
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
"@babel/plugin-proposal-class-properties": "^7.16.7",
|
||||||
"@rollup/plugin-replace": "^6.0.3",
|
"@babel/plugin-transform-runtime": "^7.17.0",
|
||||||
"@shellygo/cypress-test-utils": "^6.0.6",
|
"@babel/preset-env": "^7.16.11",
|
||||||
"@stylistic/eslint-plugin": "^5.10.0",
|
"@babel/preset-flow": "^7.16.7",
|
||||||
"@types/codemirror": "^5.60.17",
|
"@babel/preset-react": "^7.16.7",
|
||||||
"@types/color": "^4.2.1",
|
"@mdi/js": "^6.6.96",
|
||||||
"@types/cors": "^2.8.19",
|
"@storybook/addon-a11y": "^6.4.20",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@storybook/addon-actions": "^6.4.20",
|
||||||
"@types/geojson": "^7946.0.16",
|
"@storybook/addon-links": "^6.4.20",
|
||||||
"@types/json-to-ast": "^2.1.4",
|
"@storybook/addon-storysource": "^6.4.20",
|
||||||
"@types/lodash.capitalize": "^4.2.9",
|
"@storybook/addons": "^6.4.20",
|
||||||
"@types/lodash.clamp": "^4.0.9",
|
"@storybook/react": "^6.4.20",
|
||||||
"@types/lodash.clonedeep": "^4.5.9",
|
"@storybook/theming": "^6.4.20",
|
||||||
"@types/lodash.get": "^4.4.9",
|
"@types/cors": "^2.8.17",
|
||||||
"@types/lodash.isequal": "^4.5.8",
|
"@types/uuid": "^9.0.7",
|
||||||
"@types/lodash.throttle": "^4.1.9",
|
"babel-loader": "^8.2.4",
|
||||||
"@types/randomcolor": "^0.5.9",
|
"babel-plugin-istanbul": "^6.1.1",
|
||||||
"@types/react": "^19.2.14",
|
"babel-plugin-static-fs": "^3.0.0",
|
||||||
"@types/react-aria-menubutton": "^6.2.14",
|
"copy-webpack-plugin": "^6.4.1",
|
||||||
"@types/react-aria-modal": "^5.0.0",
|
"cors": "^2.8.5",
|
||||||
"@types/react-collapse": "^5.0.4",
|
"cross-env": "^7.0.3",
|
||||||
"@types/react-color": "^3.0.13",
|
"css-loader": "^5.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"cypress": "^13.6.1",
|
||||||
"@types/string-hash": "^1.1.3",
|
"eslint": "^8.12.0",
|
||||||
"@types/wicg-file-system-access": "^2023.10.7",
|
"eslint-plugin-react": "^7.29.4",
|
||||||
"@vitejs/plugin-react": "^5.1.4",
|
"express": "^4.17.3",
|
||||||
"@vitest/coverage-v8": "^4.1.9",
|
"html-webpack-inline-svg-plugin": "^2.3.0",
|
||||||
"cors": "^2.8.6",
|
"html-webpack-plugin": "^4.5.2",
|
||||||
"cypress": "^15.18.0",
|
|
||||||
"cypress-plugin-tab": "^2.0.0",
|
|
||||||
"eslint": "^10.6.0",
|
|
||||||
"eslint-plugin-react": "^7.37.5",
|
|
||||||
"eslint-plugin-react-hooks": "^7.1.1",
|
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
|
||||||
"i18next-cli": "^1.53.2",
|
|
||||||
"istanbul": "^0.4.5",
|
"istanbul": "^0.4.5",
|
||||||
"istanbul-lib-coverage": "^3.2.2",
|
"istanbul-lib-coverage": "^3.2.0",
|
||||||
"postcss": "^8.5.16",
|
"mkdirp": "^1.0.4",
|
||||||
"react-hot-loader": "^4.13.1",
|
"mocha": "^9.2.2",
|
||||||
"sass": "^1.101.0",
|
"postcss": "^8.4.12",
|
||||||
"stylelint": "^17.14.0",
|
"react-hot-loader": "^4.13.0",
|
||||||
"stylelint-config-recommended-scss": "^17.0.1",
|
"sass-loader": "^10.2.1",
|
||||||
"stylelint-scss": "^7.2.0",
|
"style-loader": "^2.0.0",
|
||||||
"typescript": "^6.0.3",
|
"stylelint": "^14.6.1",
|
||||||
"typescript-eslint": "^8.62.1",
|
"stylelint-config-recommended-scss": "^6.0.0",
|
||||||
"uuid": "^14.0.1",
|
"stylelint-scss": "^4.2.0",
|
||||||
"vite": "^7.3.2",
|
"svg-inline-loader": "^0.8.2",
|
||||||
"vite-plugin-istanbul": "^9.0.1",
|
"transform-loader": "^0.2.4",
|
||||||
"vitest": "^4.1.9"
|
"typescript": "^4.9.5",
|
||||||
|
"uuid": "^8.3.2",
|
||||||
|
"webpack": "^4.46.0",
|
||||||
|
"webpack-bundle-analyzer": "^4.5.0",
|
||||||
|
"webpack-cleanup-plugin": "^0.5.1",
|
||||||
|
"webpack-cli": "^4.9.2",
|
||||||
|
"webpack-dev-server": "^4.8.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"container": {
|
||||||
|
"startScript": "start-sandbox"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import ScrollContainer from './ScrollContainer'
|
||||||
|
|
||||||
|
class AppLayout extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
toolbar: PropTypes.element.isRequired,
|
||||||
|
layerList: PropTypes.element.isRequired,
|
||||||
|
layerEditor: PropTypes.element,
|
||||||
|
map: PropTypes.element.isRequired,
|
||||||
|
bottom: PropTypes.element,
|
||||||
|
modals: PropTypes.node,
|
||||||
|
}
|
||||||
|
|
||||||
|
static childContextTypes = {
|
||||||
|
reactIconBase: PropTypes.object
|
||||||
|
}
|
||||||
|
|
||||||
|
getChildContext() {
|
||||||
|
return {
|
||||||
|
reactIconBase: { size: 14 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <div className="maputnik-layout">
|
||||||
|
{this.props.toolbar}
|
||||||
|
<div className="maputnik-layout-list">
|
||||||
|
{this.props.layerList}
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppLayout
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import ScrollContainer from "./ScrollContainer";
|
|
||||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
|
||||||
import { IconContext } from "react-icons";
|
|
||||||
|
|
||||||
type AppLayoutInternalProps = {
|
|
||||||
toolbar: React.ReactElement
|
|
||||||
layerList: React.ReactElement
|
|
||||||
layerEditor?: React.ReactElement
|
|
||||||
codeEditor?: React.ReactElement
|
|
||||||
map: React.ReactElement
|
|
||||||
bottom?: React.ReactElement
|
|
||||||
modals?: React.ReactNode
|
|
||||||
} & WithTranslation;
|
|
||||||
|
|
||||||
class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
|
|
||||||
|
|
||||||
render() {
|
|
||||||
document.body.dir = this.props.i18n.dir();
|
|
||||||
|
|
||||||
return <IconContext.Provider value={{size: "14px"}}>
|
|
||||||
<div className="maputnik-layout">
|
|
||||||
{this.props.toolbar}
|
|
||||||
<div className="maputnik-layout-main">
|
|
||||||
{this.props.codeEditor && <div className="maputnik-layout-code-editor">
|
|
||||||
<ScrollContainer>
|
|
||||||
{this.props.codeEditor}
|
|
||||||
</ScrollContainer>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
{!this.props.codeEditor && <>
|
|
||||||
<div className="maputnik-layout-list">
|
|
||||||
{this.props.layerList}
|
|
||||||
</div>
|
|
||||||
<div className="maputnik-layout-drawer">
|
|
||||||
<ScrollContainer>
|
|
||||||
{this.props.layerEditor}
|
|
||||||
</ScrollContainer>
|
|
||||||
</div>
|
|
||||||
</>}
|
|
||||||
{this.props.map}
|
|
||||||
</div>
|
|
||||||
{this.props.bottom && <div className="maputnik-layout-bottom">
|
|
||||||
{this.props.bottom}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
{this.props.modals}
|
|
||||||
</div>
|
|
||||||
</IconContext.Provider>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const AppLayout = withTranslation()(AppLayoutInternal);
|
|
||||||
export default AppLayout;
|
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import {formatLayerId} from '../util/format';
|
||||||
|
|
||||||
|
export default class AppMessagePanel extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
errors: PropTypes.array,
|
||||||
|
infos: PropTypes.array,
|
||||||
|
mapStyle: PropTypes.object,
|
||||||
|
onLayerSelect: PropTypes.func,
|
||||||
|
currentLayer: PropTypes.object,
|
||||||
|
selectedLayerIndex: PropTypes.number,
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
onLayerSelect: () => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {selectedLayerIndex} = this.props;
|
||||||
|
const errors = this.props.errors.map((error, idx) => {
|
||||||
|
let content;
|
||||||
|
if (error.parsed && error.parsed.type === "layer") {
|
||||||
|
const {parsed} = error;
|
||||||
|
const {mapStyle, currentLayer} = this.props;
|
||||||
|
const layerId = mapStyle.layers[parsed.data.index].id;
|
||||||
|
content = (
|
||||||
|
<>
|
||||||
|
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
|
||||||
|
{selectedLayerIndex !== parsed.data.index &&
|
||||||
|
<>
|
||||||
|
—
|
||||||
|
<button
|
||||||
|
className="maputnik-message-panel__switch-button"
|
||||||
|
onClick={() => this.props.onLayerSelect(parsed.data.index)}
|
||||||
|
>
|
||||||
|
switch to layer
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
content = error.message;
|
||||||
|
}
|
||||||
|
return <p key={"error-"+idx} className="maputnik-message-panel-error">
|
||||||
|
{content}
|
||||||
|
</p>
|
||||||
|
})
|
||||||
|
|
||||||
|
const infos = this.props.infos.map((m, i) => {
|
||||||
|
return <p key={"info-"+i}>{m}</p>
|
||||||
|
})
|
||||||
|
|
||||||
|
return <div className="maputnik-message-panel">
|
||||||
|
{errors}
|
||||||
|
{infos}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { formatLayerId } from "../libs/format";
|
|
||||||
import { type LayerSpecification, type StyleSpecification } from "maplibre-gl";
|
|
||||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
|
||||||
import { type MappedError } from "../libs/definitions";
|
|
||||||
|
|
||||||
type AppMessagePanelInternalProps = {
|
|
||||||
errors?: MappedError[]
|
|
||||||
infos?: string[]
|
|
||||||
mapStyle?: StyleSpecification
|
|
||||||
onLayerSelect?(index: number): void;
|
|
||||||
currentLayer?: LayerSpecification
|
|
||||||
selectedLayerIndex?: number
|
|
||||||
} & WithTranslation;
|
|
||||||
|
|
||||||
class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalProps> {
|
|
||||||
static defaultProps = {
|
|
||||||
onLayerSelect: () => { },
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { t, selectedLayerIndex } = this.props;
|
|
||||||
const errors = this.props.errors?.map((error, idx) => {
|
|
||||||
let content;
|
|
||||||
if (error.parsed && error.parsed.type === "layer") {
|
|
||||||
const { parsed } = error;
|
|
||||||
const layerId = this.props.mapStyle?.layers[parsed.data.index].id;
|
|
||||||
content = (
|
|
||||||
<>
|
|
||||||
{t("Layer")} <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
|
|
||||||
{selectedLayerIndex !== parsed.data.index &&
|
|
||||||
<>
|
|
||||||
—
|
|
||||||
<button
|
|
||||||
className="maputnik-message-panel__switch-button"
|
|
||||||
onClick={() => this.props.onLayerSelect!(parsed.data.index)}
|
|
||||||
>
|
|
||||||
{t("switch to layer")}
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
content = error.message;
|
|
||||||
}
|
|
||||||
return <p key={"error-" + idx} className="maputnik-message-panel-error">
|
|
||||||
{content}
|
|
||||||
</p>;
|
|
||||||
});
|
|
||||||
|
|
||||||
const infos = this.props.infos?.map((m, i) => {
|
|
||||||
return <p key={"info-" + i}>{m}</p>;
|
|
||||||
});
|
|
||||||
|
|
||||||
return <div className="maputnik-message-panel">
|
|
||||||
{errors}
|
|
||||||
{infos}
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const AppMessagePanel = withTranslation()(AppMessagePanelInternal);
|
|
||||||
export default AppMessagePanel;
|
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
import {detect} from 'detect-browser';
|
||||||
|
|
||||||
|
import {MdFileDownload, MdOpenInBrowser, MdSettings, MdLayers, MdHelpOutline, MdFindInPage, MdAssignmentTurnedIn} from 'react-icons/md'
|
||||||
|
|
||||||
|
|
||||||
|
import logoImage from 'maputnik-design/logos/logo-color.svg'
|
||||||
|
import pkgJson from '../../package.json'
|
||||||
|
|
||||||
|
|
||||||
|
// This is required because of <https://stackoverflow.com/a/49846426>, there isn't another way to detect support that I'm aware of.
|
||||||
|
const browser = detect();
|
||||||
|
const colorAccessibilityFiltersEnabled = ['chrome', 'firefox'].indexOf(browser.name) > -1;
|
||||||
|
|
||||||
|
|
||||||
|
class IconText extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <span className="maputnik-icon-text">{this.props.children}</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ToolbarLink extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
children: PropTypes.node,
|
||||||
|
href: PropTypes.string,
|
||||||
|
onToggleModal: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <a
|
||||||
|
className={classnames('maputnik-toolbar-link', this.props.className)}
|
||||||
|
href={this.props.href}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{this.props.children}
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ToolbarLinkHighlighted extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
children: PropTypes.node,
|
||||||
|
href: PropTypes.string,
|
||||||
|
onToggleModal: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <a
|
||||||
|
className={classnames('maputnik-toolbar-link', "maputnik-toolbar-link--highlighted", this.props.className)}
|
||||||
|
href={this.props.href}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<span className="maputnik-toolbar-link-wrapper">
|
||||||
|
{this.props.children}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ToolbarSelect extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
wdKey: PropTypes.string
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <div
|
||||||
|
className='maputnik-toolbar-select'
|
||||||
|
data-wd-key={this.props.wdKey}
|
||||||
|
>
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ToolbarAction extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
wdKey: PropTypes.string
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <button
|
||||||
|
className='maputnik-toolbar-action'
|
||||||
|
data-wd-key={this.props.wdKey}
|
||||||
|
onClick={this.props.onClick}
|
||||||
|
>
|
||||||
|
{this.props.children}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class AppToolbar extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
mapStyle: PropTypes.object.isRequired,
|
||||||
|
inspectModeEnabled: PropTypes.bool.isRequired,
|
||||||
|
onStyleChanged: PropTypes.func.isRequired,
|
||||||
|
// A new style has been uploaded
|
||||||
|
onStyleOpen: PropTypes.func.isRequired,
|
||||||
|
// A dict of source id's and the available source layers
|
||||||
|
sources: PropTypes.object.isRequired,
|
||||||
|
children: PropTypes.node,
|
||||||
|
onToggleModal: PropTypes.func,
|
||||||
|
onSetMapState: PropTypes.func,
|
||||||
|
mapState: PropTypes.string,
|
||||||
|
renderer: PropTypes.string,
|
||||||
|
}
|
||||||
|
|
||||||
|
state = {
|
||||||
|
isOpen: {
|
||||||
|
settings: false,
|
||||||
|
sources: false,
|
||||||
|
open: false,
|
||||||
|
add: false,
|
||||||
|
export: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSelection(val) {
|
||||||
|
this.props.onSetMapState(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSkip = (target) => {
|
||||||
|
if (target === "map") {
|
||||||
|
document.querySelector(".maplibregl-canvas").focus();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const el = document.querySelector("#skip-target-"+target);
|
||||||
|
el.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const views = [
|
||||||
|
{
|
||||||
|
id: "map",
|
||||||
|
group: "general",
|
||||||
|
title: "Map",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "inspect",
|
||||||
|
group: "general",
|
||||||
|
title: "Inspect",
|
||||||
|
disabled: this.props.renderer === 'ol',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "filter-deuteranopia",
|
||||||
|
group: "color-accessibility",
|
||||||
|
title: "Deuteranopia filter",
|
||||||
|
disabled: !colorAccessibilityFiltersEnabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "filter-protanopia",
|
||||||
|
group: "color-accessibility",
|
||||||
|
title: "Protanopia filter",
|
||||||
|
disabled: !colorAccessibilityFiltersEnabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "filter-tritanopia",
|
||||||
|
group: "color-accessibility",
|
||||||
|
title: "Tritanopia filter",
|
||||||
|
disabled: !colorAccessibilityFiltersEnabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "filter-achromatopsia",
|
||||||
|
group: "color-accessibility",
|
||||||
|
title: "Achromatopsia filter",
|
||||||
|
disabled: !colorAccessibilityFiltersEnabled,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const currentView = views.find((view) => {
|
||||||
|
return view.id === this.props.mapState;
|
||||||
|
});
|
||||||
|
|
||||||
|
return <nav className='maputnik-toolbar'>
|
||||||
|
<div className="maputnik-toolbar__inner">
|
||||||
|
<div
|
||||||
|
className="maputnik-toolbar-logo-container"
|
||||||
|
>
|
||||||
|
{/* Keyboard accessible quick links */}
|
||||||
|
<button
|
||||||
|
data-wd-key="root:skip:layer-list"
|
||||||
|
className="maputnik-toolbar-skip"
|
||||||
|
onClick={e => this.onSkip("layer-list")}
|
||||||
|
>
|
||||||
|
Layers list
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
data-wd-key="root:skip:layer-editor"
|
||||||
|
className="maputnik-toolbar-skip"
|
||||||
|
onClick={e => this.onSkip("layer-editor")}
|
||||||
|
>
|
||||||
|
Layer editor
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
data-wd-key="root:skip:map-view"
|
||||||
|
className="maputnik-toolbar-skip"
|
||||||
|
onClick={e => this.onSkip("map")}
|
||||||
|
>
|
||||||
|
Map view
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
className="maputnik-toolbar-logo"
|
||||||
|
target="blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
href="https://github.com/maputnik/editor"
|
||||||
|
>
|
||||||
|
<span dangerouslySetInnerHTML={{__html: logoImage}} />
|
||||||
|
<h1>
|
||||||
|
<span className="maputnik-toolbar-name">{pkgJson.name}</span>
|
||||||
|
<span className="maputnik-toolbar-version">v{pkgJson.version}</span>
|
||||||
|
</h1>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar">
|
||||||
|
<ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}>
|
||||||
|
<MdOpenInBrowser />
|
||||||
|
<IconText>Open</IconText>
|
||||||
|
</ToolbarAction>
|
||||||
|
<ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, 'export')}>
|
||||||
|
<MdFileDownload />
|
||||||
|
<IconText>Export</IconText>
|
||||||
|
</ToolbarAction>
|
||||||
|
<ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, 'sources')}>
|
||||||
|
<MdLayers />
|
||||||
|
<IconText>Data Sources</IconText>
|
||||||
|
</ToolbarAction>
|
||||||
|
<ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}>
|
||||||
|
<MdSettings />
|
||||||
|
<IconText>Style Settings</IconText>
|
||||||
|
</ToolbarAction>
|
||||||
|
|
||||||
|
<ToolbarSelect wdKey="nav:inspect">
|
||||||
|
<MdFindInPage />
|
||||||
|
<label>View
|
||||||
|
<select
|
||||||
|
className="maputnik-select"
|
||||||
|
onChange={(e) => this.handleSelection(e.target.value)}
|
||||||
|
value={currentView.id}
|
||||||
|
>
|
||||||
|
{views.filter(v => v.group === "general").map((item) => {
|
||||||
|
return (
|
||||||
|
<option key={item.id} value={item.id} disabled={item.disabled}>
|
||||||
|
{item.title}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<optgroup label="Color accessibility">
|
||||||
|
{views.filter(v => v.group === "color-accessibility").map((item) => {
|
||||||
|
return (
|
||||||
|
<option key={item.id} value={item.id} disabled={item.disabled}>
|
||||||
|
{item.title}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</ToolbarSelect>
|
||||||
|
|
||||||
|
<ToolbarLink href={"https://github.com/maputnik/editor/wiki"}>
|
||||||
|
<MdHelpOutline />
|
||||||
|
<IconText>Help</IconText>
|
||||||
|
</ToolbarLink>
|
||||||
|
<ToolbarLinkHighlighted href={"https://gregorywolanski.typeform.com/to/cPgaSY"}>
|
||||||
|
<MdAssignmentTurnedIn />
|
||||||
|
<IconText>Take the Maputnik Survey</IconText>
|
||||||
|
</ToolbarLinkHighlighted>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import classnames from "classnames";
|
|
||||||
import {detect} from "detect-browser";
|
|
||||||
|
|
||||||
import {
|
|
||||||
MdOpenInBrowser,
|
|
||||||
MdSettings,
|
|
||||||
MdLayers,
|
|
||||||
MdHelpOutline,
|
|
||||||
MdFindInPage,
|
|
||||||
MdLanguage,
|
|
||||||
MdSave,
|
|
||||||
MdPublic,
|
|
||||||
MdCode
|
|
||||||
} from "react-icons/md";
|
|
||||||
import pkgJson from "../../package.json";
|
|
||||||
//@ts-ignore
|
|
||||||
import maputnikLogo from "maputnik-design/logos/logo-color.svg?inline";
|
|
||||||
import { withTranslation, type WithTranslation } from "react-i18next";
|
|
||||||
import { supportedLanguages } from "../i18n";
|
|
||||||
import type { OnStyleChangedCallback } from "../libs/definitions";
|
|
||||||
|
|
||||||
// This is required because of <https://stackoverflow.com/a/49846426>, there isn't another way to detect support that I'm aware of.
|
|
||||||
const browser = detect();
|
|
||||||
const colorAccessibilityFiltersEnabled = ["chrome", "firefox"].indexOf(browser!.name) > -1;
|
|
||||||
|
|
||||||
export type ModalTypes = "settings" | "sources" | "open" | "shortcuts" | "export" | "debug" | "globalState" | "codeEditor";
|
|
||||||
|
|
||||||
type IconTextProps = {
|
|
||||||
children?: React.ReactNode
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
class IconText extends React.Component<IconTextProps> {
|
|
||||||
render() {
|
|
||||||
return <span className="maputnik-icon-text">{this.props.children}</span>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ToolbarLinkProps = {
|
|
||||||
className?: string
|
|
||||||
children?: React.ReactNode
|
|
||||||
href?: string
|
|
||||||
};
|
|
||||||
|
|
||||||
class ToolbarLink extends React.Component<ToolbarLinkProps> {
|
|
||||||
render() {
|
|
||||||
return <a
|
|
||||||
className={classnames("maputnik-toolbar-link", this.props.className)}
|
|
||||||
href={this.props.href}
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
data-wd-key="toolbar:link"
|
|
||||||
>
|
|
||||||
{this.props.children}
|
|
||||||
</a>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ToolbarSelectProps = {
|
|
||||||
children?: React.ReactNode
|
|
||||||
wdKey?: string
|
|
||||||
};
|
|
||||||
|
|
||||||
class ToolbarSelect extends React.Component<ToolbarSelectProps> {
|
|
||||||
render() {
|
|
||||||
return <div
|
|
||||||
className='maputnik-toolbar-select'
|
|
||||||
data-wd-key={this.props.wdKey}
|
|
||||||
>
|
|
||||||
{this.props.children}
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ToolbarActionProps = {
|
|
||||||
children?: React.ReactNode
|
|
||||||
onClick?(...args: unknown[]): unknown
|
|
||||||
wdKey?: string
|
|
||||||
};
|
|
||||||
|
|
||||||
class ToolbarAction extends React.Component<ToolbarActionProps> {
|
|
||||||
render() {
|
|
||||||
return <button
|
|
||||||
className='maputnik-toolbar-action'
|
|
||||||
data-wd-key={this.props.wdKey}
|
|
||||||
onClick={this.props.onClick}
|
|
||||||
>
|
|
||||||
{this.props.children}
|
|
||||||
</button>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deuteranopia" | "filter-protanopia" | "filter-tritanopia";
|
|
||||||
|
|
||||||
type AppToolbarInternalProps = {
|
|
||||||
mapStyle: object
|
|
||||||
inspectModeEnabled: boolean
|
|
||||||
onStyleChanged: OnStyleChangedCallback
|
|
||||||
// A new style has been uploaded
|
|
||||||
onStyleOpen: OnStyleChangedCallback
|
|
||||||
// A dict of source id's and the available source layers
|
|
||||||
sources: object
|
|
||||||
children?: React.ReactNode
|
|
||||||
onToggleModal(modal: ModalTypes): void
|
|
||||||
onSetMapState(mapState: MapState): unknown
|
|
||||||
mapState?: MapState
|
|
||||||
renderer?: string
|
|
||||||
} & WithTranslation;
|
|
||||||
|
|
||||||
class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
|
||||||
state = {
|
|
||||||
isOpen: {
|
|
||||||
settings: false,
|
|
||||||
sources: false,
|
|
||||||
open: false,
|
|
||||||
add: false,
|
|
||||||
export: false,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleSelection(val: MapState) {
|
|
||||||
this.props.onSetMapState(val);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleLanguageChange(val: string) {
|
|
||||||
this.props.i18n.changeLanguage(val);
|
|
||||||
}
|
|
||||||
|
|
||||||
onSkip = (target: string) => {
|
|
||||||
if (target === "map") {
|
|
||||||
(document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const el = document.querySelector("#skip-target-"+target) as HTMLButtonElement;
|
|
||||||
el.focus();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const t = this.props.t;
|
|
||||||
const views = [
|
|
||||||
{
|
|
||||||
id: "map",
|
|
||||||
group: "general",
|
|
||||||
title: t("Map"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "inspect",
|
|
||||||
group: "general",
|
|
||||||
title: t("Inspect"),
|
|
||||||
disabled: this.props.renderer === "ol",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "filter-deuteranopia",
|
|
||||||
group: "color-accessibility",
|
|
||||||
title: t("Deuteranopia filter"),
|
|
||||||
disabled: !colorAccessibilityFiltersEnabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "filter-protanopia",
|
|
||||||
group: "color-accessibility",
|
|
||||||
title: t("Protanopia filter"),
|
|
||||||
disabled: !colorAccessibilityFiltersEnabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "filter-tritanopia",
|
|
||||||
group: "color-accessibility",
|
|
||||||
title: t("Tritanopia filter"),
|
|
||||||
disabled: !colorAccessibilityFiltersEnabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "filter-achromatopsia",
|
|
||||||
group: "color-accessibility",
|
|
||||||
title: t("Achromatopsia filter"),
|
|
||||||
disabled: !colorAccessibilityFiltersEnabled,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const currentView = views.find((view) => {
|
|
||||||
return view.id === this.props.mapState;
|
|
||||||
});
|
|
||||||
|
|
||||||
return <nav className='maputnik-toolbar'>
|
|
||||||
<div className="maputnik-toolbar__inner">
|
|
||||||
<div
|
|
||||||
className="maputnik-toolbar-logo-container"
|
|
||||||
>
|
|
||||||
{/* Keyboard accessible quick links */}
|
|
||||||
<button
|
|
||||||
data-wd-key="root:skip:layer-list"
|
|
||||||
className="maputnik-toolbar-skip"
|
|
||||||
onClick={_e => this.onSkip("layer-list")}
|
|
||||||
>
|
|
||||||
{t("Layers list")}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
data-wd-key="root:skip:layer-editor"
|
|
||||||
className="maputnik-toolbar-skip"
|
|
||||||
onClick={_e => this.onSkip("layer-editor")}
|
|
||||||
>
|
|
||||||
{t("Layer editor")}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
data-wd-key="root:skip:map-view"
|
|
||||||
className="maputnik-toolbar-skip"
|
|
||||||
onClick={_e => this.onSkip("map")}
|
|
||||||
>
|
|
||||||
{t("Map view")}
|
|
||||||
</button>
|
|
||||||
<a
|
|
||||||
className="maputnik-toolbar-logo"
|
|
||||||
target="blank"
|
|
||||||
rel="noreferrer noopener"
|
|
||||||
href="https://github.com/maplibre/maputnik"
|
|
||||||
>
|
|
||||||
<img src={maputnikLogo} alt={t("Maputnik on GitHub")} />
|
|
||||||
<h1>
|
|
||||||
<span className="maputnik-toolbar-name">{pkgJson.name}</span>
|
|
||||||
<span className="maputnik-toolbar-version">v{pkgJson.version}</span>
|
|
||||||
</h1>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar">
|
|
||||||
<ToolbarAction wdKey="nav:open" onClick={() => this.props.onToggleModal("open")}>
|
|
||||||
<MdOpenInBrowser />
|
|
||||||
<IconText>{t("Open")}</IconText>
|
|
||||||
</ToolbarAction>
|
|
||||||
<ToolbarAction wdKey="nav:export" onClick={() => this.props.onToggleModal("export")}>
|
|
||||||
<MdSave />
|
|
||||||
<IconText>{t("Save")}</IconText>
|
|
||||||
</ToolbarAction>
|
|
||||||
<ToolbarAction wdKey="nav:code-editor" onClick={() => this.props.onToggleModal("codeEditor")}>
|
|
||||||
<MdCode />
|
|
||||||
<IconText>{t("Code Editor")}</IconText>
|
|
||||||
</ToolbarAction>
|
|
||||||
<ToolbarAction wdKey="nav:sources" onClick={() => this.props.onToggleModal("sources")}>
|
|
||||||
<MdLayers />
|
|
||||||
<IconText>{t("Data Sources")}</IconText>
|
|
||||||
</ToolbarAction>
|
|
||||||
<ToolbarAction wdKey="nav:settings" onClick={() => this.props.onToggleModal("settings")}>
|
|
||||||
<MdSettings />
|
|
||||||
<IconText>{t("Style Settings")}</IconText>
|
|
||||||
</ToolbarAction>
|
|
||||||
<ToolbarAction wdKey="nav:global-state" onClick={() => this.props.onToggleModal("globalState")}>
|
|
||||||
<MdPublic />
|
|
||||||
<IconText>{t("Global State")}</IconText>
|
|
||||||
</ToolbarAction>
|
|
||||||
|
|
||||||
<ToolbarSelect wdKey="nav:inspect">
|
|
||||||
<MdFindInPage />
|
|
||||||
<IconText>{t("View")}
|
|
||||||
<select
|
|
||||||
className="maputnik-select"
|
|
||||||
data-wd-key="maputnik-select"
|
|
||||||
onChange={(e) => this.handleSelection(e.target.value as MapState)}
|
|
||||||
value={currentView?.id}
|
|
||||||
>
|
|
||||||
{views.filter(v => v.group === "general").map((item) => {
|
|
||||||
return (
|
|
||||||
<option key={item.id} value={item.id} disabled={item.disabled} data-wd-key={item.id}>
|
|
||||||
{item.title}
|
|
||||||
</option>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<optgroup label={t("Color accessibility")}>
|
|
||||||
{views.filter(v => v.group === "color-accessibility").map((item) => {
|
|
||||||
return (
|
|
||||||
<option key={item.id} value={item.id} disabled={item.disabled}>
|
|
||||||
{item.title}
|
|
||||||
</option>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</optgroup>
|
|
||||||
</select>
|
|
||||||
</IconText>
|
|
||||||
</ToolbarSelect>
|
|
||||||
|
|
||||||
<ToolbarSelect wdKey="nav:language">
|
|
||||||
<MdLanguage />
|
|
||||||
<IconText>Language
|
|
||||||
<select
|
|
||||||
className="maputnik-select"
|
|
||||||
data-wd-key="maputnik-lang-select"
|
|
||||||
onChange={(e) => this.handleLanguageChange(e.target.value)}
|
|
||||||
value={this.props.i18n.language}
|
|
||||||
>
|
|
||||||
{Object.entries(supportedLanguages).map(([code, name]) => {
|
|
||||||
return (
|
|
||||||
<option key={code} value={code}>
|
|
||||||
{name}
|
|
||||||
</option>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</select>
|
|
||||||
</IconText>
|
|
||||||
</ToolbarSelect>
|
|
||||||
|
|
||||||
<ToolbarLink href={"https://github.com/maplibre/maputnik/wiki"}>
|
|
||||||
<MdHelpOutline />
|
|
||||||
<IconText>{t("Help")}</IconText>
|
|
||||||
</ToolbarLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const AppToolbar = withTranslation()(AppToolbarInternal);
|
|
||||||
export default AppToolbar;
|
|
||||||
@@ -1,46 +1,44 @@
|
|||||||
import React, {type CSSProperties, type PropsWithChildren, type SyntheticEvent} from "react";
|
import React from 'react'
|
||||||
import classnames from "classnames";
|
import PropTypes from 'prop-types'
|
||||||
import FieldDocLabel from "./FieldDocLabel";
|
import classnames from 'classnames'
|
||||||
import Doc from "./Doc";
|
import FieldDocLabel from './FieldDocLabel'
|
||||||
|
import Doc from './Doc'
|
||||||
|
|
||||||
export type BlockProps = PropsWithChildren & {
|
|
||||||
"data-wd-key"?: string
|
|
||||||
label?: string
|
|
||||||
action?: React.ReactElement
|
|
||||||
style?: CSSProperties
|
|
||||||
onChange?(...args: unknown[]): unknown
|
|
||||||
fieldSpec?: object
|
|
||||||
wideMode?: boolean
|
|
||||||
error?: {message: string}
|
|
||||||
};
|
|
||||||
|
|
||||||
type BlockState = {
|
|
||||||
showDoc: boolean
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Wrap a component with a label */
|
/** Wrap a component with a label */
|
||||||
export default class Block extends React.Component<BlockProps, BlockState> {
|
export default class Block extends React.Component {
|
||||||
_blockEl: HTMLDivElement | null = null;
|
static propTypes = {
|
||||||
|
"data-wd-key": PropTypes.string,
|
||||||
|
label: PropTypes.oneOfType([
|
||||||
|
PropTypes.string,
|
||||||
|
PropTypes.element,
|
||||||
|
]),
|
||||||
|
action: PropTypes.element,
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
style: PropTypes.object,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
fieldSpec: PropTypes.object,
|
||||||
|
wideMode: PropTypes.bool,
|
||||||
|
error: PropTypes.array,
|
||||||
|
}
|
||||||
|
|
||||||
constructor (props: BlockProps) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
showDoc: false,
|
showDoc: false,
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(e: React.BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) {
|
|
||||||
const value = e.target.value;
|
|
||||||
if (this.props.onChange) {
|
|
||||||
return this.props.onChange(value === "" ? undefined : value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onToggleDoc = (val: boolean) => {
|
onChange(e) {
|
||||||
|
const value = e.target.value
|
||||||
|
return this.props.onChange(value === "" ? undefined : value)
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggleDoc = (val) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
showDoc: val
|
showDoc: val
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Some fields for example <InputColor/> bind click events inside the element
|
* Some fields for example <InputColor/> bind click events inside the element
|
||||||
@@ -48,26 +46,26 @@ export default class Block extends React.Component<BlockProps, BlockState> {
|
|||||||
* causing the picker to reopen. This causes a scenario where the picker can
|
* causing the picker to reopen. This causes a scenario where the picker can
|
||||||
* never be closed once open.
|
* never be closed once open.
|
||||||
*/
|
*/
|
||||||
onLabelClick = (event: SyntheticEvent<any, any>) => {
|
onLabelClick = (event) => {
|
||||||
const el = event.nativeEvent.target;
|
const el = event.nativeEvent.target;
|
||||||
const contains = this._blockEl?.contains(el);
|
const nativeEvent = event.nativeEvent;
|
||||||
|
const contains = this._blockEl.contains(el);
|
||||||
|
|
||||||
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() {
|
||||||
|
const errors = [].concat(this.props.error || []);
|
||||||
|
|
||||||
return <label style={this.props.style}
|
return <label style={this.props.style}
|
||||||
data-wd-key={this.props["data-wd-key"]}
|
data-wd-key={this.props["data-wd-key"]}
|
||||||
className={classnames({
|
className={classnames({
|
||||||
"maputnik-input-block": true,
|
"maputnik-input-block": true,
|
||||||
"maputnik-input-block--wide": this.props.wideMode,
|
"maputnik-input-block--wide": this.props.wideMode,
|
||||||
"maputnik-action-block": this.props.action,
|
"maputnik-action-block": this.props.action
|
||||||
"maputnik-input-block--error": this.props.error
|
|
||||||
})}
|
})}
|
||||||
onClick={this.onLabelClick}
|
onClick={this.onLabelClick}
|
||||||
>
|
>
|
||||||
@@ -88,17 +86,18 @@ export default class Block extends React.Component<BlockProps, BlockState> {
|
|||||||
<div className="maputnik-input-block-action">
|
<div className="maputnik-input-block-action">
|
||||||
{this.props.action}
|
{this.props.action}
|
||||||
</div>
|
</div>
|
||||||
<div className="maputnik-input-block-content" ref={el => {this._blockEl = el;}}>
|
<div className="maputnik-input-block-content" ref={el => this._blockEl = el}>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</div>
|
</div>
|
||||||
{this.props.fieldSpec &&
|
{this.props.fieldSpec &&
|
||||||
<div
|
<div
|
||||||
className="maputnik-doc-inline"
|
className="maputnik-doc-inline"
|
||||||
style={{display: this.state.showDoc ? "" : "none"}}
|
style={{display: this.state.showDoc ? '' : 'none'}}
|
||||||
>
|
>
|
||||||
<Doc fieldSpec={this.props.fieldSpec} />
|
<Doc fieldSpec={this.props.fieldSpec} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</label>;
|
</label>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import InputJson from "./InputJson";
|
|
||||||
import React from "react";
|
|
||||||
import { withTranslation, type WithTranslation } from "react-i18next";
|
|
||||||
import { type StyleSpecification } from "maplibre-gl";
|
|
||||||
import { type StyleSpecificationWithId } from "../libs/definitions";
|
|
||||||
|
|
||||||
export type CodeEditorProps = {
|
|
||||||
value: StyleSpecification;
|
|
||||||
onChange: (value: StyleSpecificationWithId) => void;
|
|
||||||
onClose: () => void;
|
|
||||||
} & WithTranslation;
|
|
||||||
|
|
||||||
const CodeEditorInternal: React.FC<CodeEditorProps> = (props) => {
|
|
||||||
|
|
||||||
return <>
|
|
||||||
<button className="maputnik-button" onClick={props.onClose} aria-label={props.t("Close")} style={{ position: "sticky", top: "0", zIndex: 1 }}>{props.t("Click to close the editor")}</button>
|
|
||||||
<InputJson
|
|
||||||
lintType="style"
|
|
||||||
value={props.value}
|
|
||||||
onChange={props.onChange}
|
|
||||||
className={"maputnik-code-editor"}
|
|
||||||
withScroll={true}
|
|
||||||
/>;
|
|
||||||
</>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const CodeEditor = withTranslation()(CodeEditorInternal);
|
|
||||||
|
|
||||||
export default CodeEditor;
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { Collapse as ReactCollapse } from 'react-collapse'
|
||||||
|
import accessibility from '../../libs/accessibility'
|
||||||
|
|
||||||
|
|
||||||
|
export default class Collapse extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
isActive: PropTypes.bool.isRequired,
|
||||||
|
children: PropTypes.element.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
isActive: true
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (accessibility.reducedMotionEnabled()) {
|
||||||
|
return (
|
||||||
|
<div style={{display: this.props.isActive ? "block" : "none"}}>
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return (
|
||||||
|
<ReactCollapse isOpened={this.props.isActive}>
|
||||||
|
{this.props.children}
|
||||||
|
</ReactCollapse>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Collapse as ReactCollapse } from "react-collapse";
|
|
||||||
import {reducedMotionEnabled} from "../libs/accessibility";
|
|
||||||
|
|
||||||
|
|
||||||
type CollapseProps = {
|
|
||||||
isActive: boolean
|
|
||||||
children: React.ReactElement
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export default class Collapse extends React.Component<CollapseProps> {
|
|
||||||
static defaultProps = {
|
|
||||||
isActive: true
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (reducedMotionEnabled()) {
|
|
||||||
return (
|
|
||||||
<div style={{display: this.props.isActive ? "block" : "none"}}>
|
|
||||||
{this.props.children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return (
|
|
||||||
<ReactCollapse isOpened={this.props.isActive}>
|
|
||||||
{this.props.children}
|
|
||||||
</ReactCollapse>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import {MdArrowDropDown, MdArrowDropUp} from 'react-icons/md'
|
||||||
|
|
||||||
|
export default class Collapser extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
isCollapsed: PropTypes.bool.isRequired,
|
||||||
|
style: PropTypes.object,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const iconStyle = {
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
...this.props.style,
|
||||||
|
}
|
||||||
|
return this.props.isCollapsed ? <MdArrowDropUp style={iconStyle}/> : <MdArrowDropDown style={iconStyle} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import {MdArrowDropDown, MdArrowDropUp} from "react-icons/md";
|
|
||||||
|
|
||||||
type CollapserProps = {
|
|
||||||
isCollapsed: boolean
|
|
||||||
style?: object
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class Collapser extends React.Component<CollapserProps> {
|
|
||||||
render() {
|
|
||||||
const iconStyle = {
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
...this.props.style,
|
|
||||||
};
|
|
||||||
return this.props.isCollapsed ? <MdArrowDropUp style={iconStyle}/> : <MdArrowDropDown style={iconStyle} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
export default class Doc extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
fieldSpec: PropTypes.object.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const {fieldSpec} = this.props;
|
||||||
|
|
||||||
|
const {doc, values} = fieldSpec;
|
||||||
|
const sdkSupport = fieldSpec['sdk-support'];
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
js: "JS",
|
||||||
|
android: "Android",
|
||||||
|
ios: "iOS",
|
||||||
|
macos: "macOS",
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderValues = (
|
||||||
|
!!values &&
|
||||||
|
// HACK: Currently we merge additional values into the stylespec, so this is required
|
||||||
|
// See <https://github.com/maputnik/editor/blob/master/src/components/fields/PropertyGroup.jsx#L16>
|
||||||
|
!Array.isArray(values)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{doc &&
|
||||||
|
<div className="SpecDoc">
|
||||||
|
<div className="SpecDoc__doc">{doc}</div>
|
||||||
|
{renderValues &&
|
||||||
|
<ul className="SpecDoc__values">
|
||||||
|
{Object.entries(values).map(([key, value]) => {
|
||||||
|
return (
|
||||||
|
<li key={key}>
|
||||||
|
<code>{JSON.stringify(key)}</code>
|
||||||
|
<div>{value.doc}</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{sdkSupport &&
|
||||||
|
<div className="SpecDoc__sdk-support">
|
||||||
|
<table className="SpecDoc__sdk-support__table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
{Object.values(headers).map(header => {
|
||||||
|
return <th key={header}>{header}</th>;
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Object.entries(sdkSupport).map(([key, supportObj]) => {
|
||||||
|
return (
|
||||||
|
<tr key={key}>
|
||||||
|
<td>{key}</td>
|
||||||
|
{Object.keys(headers).map(k => {
|
||||||
|
const value = supportObj[k];
|
||||||
|
if (supportObj.hasOwnProperty(k)) {
|
||||||
|
return <td key={k}>{supportObj[k]}</td>;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return <td key={k}>no</td>;
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import Markdown from "react-markdown";
|
|
||||||
|
|
||||||
const headers = {
|
|
||||||
js: "JS",
|
|
||||||
android: "Android",
|
|
||||||
ios: "iOS"
|
|
||||||
};
|
|
||||||
|
|
||||||
type DocProps = {
|
|
||||||
fieldSpec: {
|
|
||||||
doc?: string
|
|
||||||
values?: {
|
|
||||||
[key: string]: {
|
|
||||||
doc?: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"sdk-support"?: {
|
|
||||||
[key: string]: typeof headers
|
|
||||||
}
|
|
||||||
docUrl?: string,
|
|
||||||
docUrlLinkText?: string
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class Doc extends React.Component<DocProps> {
|
|
||||||
render () {
|
|
||||||
const {fieldSpec} = this.props;
|
|
||||||
|
|
||||||
const {doc, values, docUrl, docUrlLinkText} = fieldSpec;
|
|
||||||
const sdkSupport = fieldSpec["sdk-support"];
|
|
||||||
|
|
||||||
const renderValues = (
|
|
||||||
!!values &&
|
|
||||||
// HACK: Currently we merge additional values into the style spec, so this is required
|
|
||||||
// See <https://github.com/maplibre/maputnik/blob/main/src/components/PropertyGroup.jsx#L16>
|
|
||||||
!Array.isArray(values)
|
|
||||||
);
|
|
||||||
|
|
||||||
const sdkSupportToJsx = (value: string) => {
|
|
||||||
const supportValue = value.toLowerCase();
|
|
||||||
if (supportValue.startsWith("https://")) {
|
|
||||||
return <a href={supportValue} target="_blank" rel="noreferrer">{"#" + supportValue.split("/").pop()}</a>;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{doc &&
|
|
||||||
<div className="SpecDoc">
|
|
||||||
<div className="SpecDoc__doc" data-wd-key='spec-field-doc'>
|
|
||||||
<Markdown components={{
|
|
||||||
a: ({node: _node, href, children, ...props}) => <a href={href} target="_blank" {...props}>{children}</a>,
|
|
||||||
}}>{doc}</Markdown>
|
|
||||||
</div>
|
|
||||||
{renderValues &&
|
|
||||||
<ul className="SpecDoc__values">
|
|
||||||
{Object.entries(values).map(([key, value]) => {
|
|
||||||
return (
|
|
||||||
<li key={key}>
|
|
||||||
<code>{JSON.stringify(key)}</code>
|
|
||||||
<div>{value.doc}</div>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
{sdkSupport &&
|
|
||||||
<div className="SpecDoc__sdk-support">
|
|
||||||
<table className="SpecDoc__sdk-support__table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
{Object.values(headers).map(header => {
|
|
||||||
return <th key={header}>{header}</th>;
|
|
||||||
})}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Object.entries(sdkSupport).map(([key, supportObj]) => {
|
|
||||||
return (
|
|
||||||
<tr key={key}>
|
|
||||||
<td>{key}</td>
|
|
||||||
{Object.keys(headers).map((k) => {
|
|
||||||
if (Object.prototype.hasOwnProperty.call(supportObj, k)) {
|
|
||||||
return <td key={k}>{sdkSupportToJsx(supportObj[k as keyof typeof headers])}</td>;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return <td key={k}>no</td>;
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
{docUrl && docUrlLinkText &&
|
|
||||||
<div className="SpecDoc__learn-more">
|
|
||||||
<a href={docUrl} target="_blank" rel="noreferrer">{docUrlLinkText}</a>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import Block from './Block'
|
||||||
|
import InputArray from './InputArray'
|
||||||
|
import Fieldset from './Fieldset'
|
||||||
|
|
||||||
|
export default class FieldArray extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
...InputArray.propTypes,
|
||||||
|
name: PropTypes.string,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {props} = this;
|
||||||
|
|
||||||
|
return <Fieldset label={props.label}>
|
||||||
|
<InputArray {...props} />
|
||||||
|
</Fieldset>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user