mirror of
https://github.com/maputnik/editor.git
synced 2026-09-11 23:07:41 +00:00
Compare commits
1 Commits
main
..
95994dad16
| Author | SHA1 | Date | |
|---|---|---|---|
| 95994dad16 |
@@ -31,10 +31,6 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
groups:
|
||||
codeql-action:
|
||||
patterns:
|
||||
- "github/codeql-action*"
|
||||
cooldown:
|
||||
default-days: 3
|
||||
# no semver support for github-actions
|
||||
|
||||
@@ -6,10 +6,6 @@ on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
|
||||
build-node:
|
||||
|
||||
@@ -20,10 +20,6 @@ on:
|
||||
schedule:
|
||||
- cron: '17 0 * * 6'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
@@ -46,7 +42,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
|
||||
uses: github/codeql-action/init@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -57,7 +53,7 @@ jobs:
|
||||
# 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@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
|
||||
uses: github/codeql-action/autobuild@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -71,4 +67,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
|
||||
uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
|
||||
|
||||
@@ -2,17 +2,9 @@
|
||||
|
||||
### ✨ Features and improvements
|
||||
- Replace Cypress with Playwright for end-to-end tests and Vitest browser mode (Playwright provider) for component tests; drop the `@shellygo/cypress-test-utils` helper in favour of a `MaputnikDriver` page object
|
||||
- Upgrade to MapLibre GL JS 6 and Vite 8, and update the remaining dependencies
|
||||
- Serve the RTL text plugin from `@mapbox/mapbox-gl-rtl-text` instead of a pinned CDN URL, so its version is tracked in `package.json`
|
||||
- Exported HTML now loads MapLibre GL JS as an ES module, since v6 no longer ships a UMD bundle
|
||||
- _...Add new stuff here..._
|
||||
|
||||
### 🐞 Bug fixes
|
||||
- Prevent a crash when converting a function to an expression
|
||||
- Prevent a crash when deleting either stop from a function with two stops
|
||||
- Preserve expanded layer groups when deleting layers, including the first layer of a group
|
||||
- The map's data listener now fires on tile loads again, so source and vector layer field autocompletion is populated
|
||||
- The `maputnik` desktop binary now opens the default browser automatically on startup (opt out with `--no-browser`)
|
||||
- _...Add new stuff here..._
|
||||
|
||||
## 3.1.0
|
||||
|
||||
+8
-3
@@ -7,16 +7,21 @@ require (
|
||||
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.3
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/maputnik/desktop v1.0.7
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
|
||||
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
|
||||
golang.org/x/sys v0.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
|
||||
)
|
||||
|
||||
+9
-6
@@ -1,7 +1,9 @@
|
||||
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=
|
||||
@@ -18,14 +20,14 @@ github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH
|
||||
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.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
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/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
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=
|
||||
@@ -39,11 +41,12 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs
|
||||
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=
|
||||
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
||||
golang.org/x/sys v0.1.0/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=
|
||||
|
||||
+2
-26
@@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -10,7 +9,6 @@ import (
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/maputnik/desktop/filewatch"
|
||||
"github.com/pkg/browser"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
@@ -38,10 +36,6 @@ func main() {
|
||||
Name: "static",
|
||||
Usage: "Serve directory under /static/",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "no-browser",
|
||||
Usage: "Do not automatically open the default browser",
|
||||
},
|
||||
}
|
||||
|
||||
app.Action = func(c *cli.Context) error {
|
||||
@@ -77,26 +71,8 @@ func main() {
|
||||
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)
|
||||
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", c.Int("port")))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("http://localhost:%d", c.Int("port"))
|
||||
fmt.Printf("Exposing Maputnik on %s\n", url)
|
||||
|
||||
// Listener is already accepting connections, so this can't race http.Serve below.
|
||||
// xdg-open is known to hang on some headless Linux setups, so this runs in its own
|
||||
// goroutine to keep a stuck opener from stalling server startup.
|
||||
if !c.Bool("no-browser") {
|
||||
go func() {
|
||||
if err := browser.OpenURL(url); err != nil {
|
||||
fmt.Printf("Could not open browser automatically: %s\nPlease open %s manually.\n", err, url)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return http.Serve(listener, corsRouter)
|
||||
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,16 +0,0 @@
|
||||
{
|
||||
"version": 8,
|
||||
"name": "Grouped layers",
|
||||
"sources": {},
|
||||
"layers": [
|
||||
{ "id": "background", "type": "background" },
|
||||
{ "id": "park-a", "type": "background" },
|
||||
{ "id": "park-b", "type": "background" },
|
||||
{ "id": "park-c", "type": "background" },
|
||||
{ "id": "road", "type": "background" },
|
||||
{ "id": "park-d", "type": "background" },
|
||||
{ "id": "park-e", "type": "background" },
|
||||
{ "id": "water-a", "type": "background" },
|
||||
{ "id": "water-b", "type": "background" }
|
||||
]
|
||||
}
|
||||
@@ -289,20 +289,6 @@ describe("layer editor", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("should leave a plain editable value after deleting a stop", async () => {
|
||||
await when.setFunctionStopValue("circle-radius", "Output value", 1, "0");
|
||||
await when.deleteFunctionStop("circle-radius");
|
||||
await then(get.elementByTestId("spec-field:circle-radius")).shouldBeVisible();
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [{ id, paint: { "circle-radius": 0 } }],
|
||||
});
|
||||
|
||||
await when.setValue("spec-field-input:circle-radius", "7");
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [{ id, paint: { "circle-radius": 7 } }],
|
||||
});
|
||||
});
|
||||
|
||||
test("should set the base", async () => {
|
||||
await when.setFunctionBase("circle-radius", "2");
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
@@ -324,9 +310,8 @@ describe("layer editor", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("should convert to an expression without crashing", async () => {
|
||||
test("should convert to an expression", async () => {
|
||||
await when.makeExpression("circle-radius");
|
||||
await then(get.element("[data-wd-key='spec-field-container:circle-radius'] .maputnik-expression-editor")).shouldBeVisible();
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [{ id, paint: { "circle-radius": ["interpolate", ["linear"], ["zoom"], 6, 5, 10, 5] } }],
|
||||
});
|
||||
|
||||
+1
-78
@@ -1,4 +1,4 @@
|
||||
import { beforeEach, describe, expect, test } from "./utils/fixtures";
|
||||
import { beforeEach, describe, test } from "./utils/fixtures";
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
|
||||
describe("layers list", () => {
|
||||
@@ -47,14 +47,6 @@ describe("layers list", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("should title the visibility button with the action it performs", async () => {
|
||||
const key = "layer-list-item:" + id + ":toggle-visibility";
|
||||
// A visible layer is about to be hidden, and a hidden one to be shown.
|
||||
await expect(get.elementByTestId(key)).toHaveAttribute("title", "hide");
|
||||
await when.click(key);
|
||||
await expect(get.elementByTestId(key)).toHaveAttribute("title", "show");
|
||||
});
|
||||
|
||||
describe("when clicking hide", () => {
|
||||
beforeEach(async () => {
|
||||
await when.click("layer-list-item:" + id + ":toggle-visibility");
|
||||
@@ -306,75 +298,6 @@ describe("layers list", () => {
|
||||
});
|
||||
|
||||
describe("groups", () => {
|
||||
for (const control of ["menu", "trash"] as const) {
|
||||
describe(`after deletion using ${control}`, () => {
|
||||
beforeEach(async () => {
|
||||
await when.setStyle("grouped_layers");
|
||||
});
|
||||
|
||||
test("keeps all groups expanded when an earlier layer is deleted", async () => {
|
||||
await when.click("skip-target-layer-list");
|
||||
await then(get.elementByTestId("layer-list-item:park-b")).shouldBeVisible();
|
||||
await when.deleteLayer("background", control);
|
||||
await then(get.elementByTestId("layer-list-item:background")).shouldNotExist();
|
||||
await then(get.elementByTestId("layer-list-group:park-0")).shouldExist();
|
||||
await expect(get.elementByTestId("layer-list-group:water-6").getByRole("button")).toHaveAttribute("aria-expanded", "true");
|
||||
await then(get.elementByTestId("layer-list-item:park-b")).shouldBeVisible();
|
||||
await then(get.elementByTestId("layer-list-item:park-e")).shouldBeVisible();
|
||||
await then(get.elementByTestId("layer-list-item:water-b")).shouldBeVisible();
|
||||
});
|
||||
|
||||
test("preserves separate states for groups with the same prefix", async () => {
|
||||
await when.click("layer-list-group:park-1");
|
||||
await when.deleteLayer("background", control);
|
||||
await then(get.elementByTestId("layer-list-group:park-0")).shouldExist();
|
||||
await when.click("layer-list-item:road");
|
||||
await expect(get.elementByTestId("layer-list-group:park-0").getByRole("button")).toHaveAttribute("aria-expanded", "true");
|
||||
await expect(get.elementByTestId("layer-list-group:park-4").getByRole("button")).toHaveAttribute("aria-expanded", "false");
|
||||
await then(get.elementByTestId("layer-list-item:park-b")).shouldBeVisible();
|
||||
await then(get.elementByTestId("layer-list-item:park-e")).shouldNotBeVisible();
|
||||
});
|
||||
|
||||
test("keeps a group expanded when its first layer is deleted", async () => {
|
||||
await when.click("layer-list-group:park-1");
|
||||
await when.deleteLayer("park-a", control);
|
||||
await then(get.elementByTestId("layer-list-item:park-a")).shouldNotExist();
|
||||
await when.click("layer-list-item:road");
|
||||
await expect(get.elementByTestId("layer-list-group:park-1").getByRole("button")).toHaveAttribute("aria-expanded", "true");
|
||||
await then(get.elementByTestId("layer-list-item:park-c")).shouldBeVisible();
|
||||
});
|
||||
|
||||
for (const expandedGroup of ["park-1", "park-5"]) {
|
||||
test(`keeps a merged group expanded when ${expandedGroup} was expanded`, async () => {
|
||||
await when.click("layer-list-group:" + expandedGroup);
|
||||
await when.deleteLayer("road", control);
|
||||
await then(get.elementByTestId("layer-list-item:road")).shouldNotExist();
|
||||
await expect(get.elementByTestId("layer-list-group:park-1").getByRole("button")).toHaveAttribute("aria-expanded", "true");
|
||||
await then(get.elementByTestId("layer-list-item:park-b")).shouldBeVisible();
|
||||
await then(get.elementByTestId("layer-list-item:park-e")).shouldBeVisible();
|
||||
});
|
||||
}
|
||||
|
||||
test("keeps a merged group collapsed when both groups were collapsed", async () => {
|
||||
await when.deleteLayer("road", control);
|
||||
await then(get.elementByTestId("layer-list-item:road")).shouldNotExist();
|
||||
await when.click("layer-list-item:background");
|
||||
await expect(get.elementByTestId("layer-list-group:park-1").getByRole("button")).toHaveAttribute("aria-expanded", "false");
|
||||
await then(get.elementByTestId("layer-list-item:park-b")).shouldNotBeVisible();
|
||||
await then(get.elementByTestId("layer-list-item:park-e")).shouldNotBeVisible();
|
||||
});
|
||||
|
||||
test("preserves later groups after deleting an entire group", async () => {
|
||||
await when.click("skip-target-layer-list");
|
||||
await when.deleteLayer("park-a", control);
|
||||
await when.deleteLayer("park-b", control);
|
||||
await when.deleteLayer("park-c", control);
|
||||
await expect(get.elementByTestId("layer-list-group:water-4").getByRole("button")).toHaveAttribute("aria-expanded", "true");
|
||||
await then(get.elementByTestId("layer-list-item:water-b")).shouldBeVisible();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test("simple", async () => {
|
||||
await when.setStyle("geojson");
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ export class MaputnikDriver {
|
||||
"example-style-with-zoom-7-and-center-0-51.json",
|
||||
"example-style-with-zoom-5-and-center-50-50.json",
|
||||
"access-token-style.json",
|
||||
"grouped-layers-style.json",
|
||||
];
|
||||
for (const fixture of styleFixtures) {
|
||||
await this.helper.given.interceptAndMockResponse({
|
||||
@@ -64,18 +63,6 @@ export class MaputnikDriver {
|
||||
|
||||
modal: this.modalDriver.when,
|
||||
|
||||
deleteLayer: async (id: string, control: "menu" | "trash" = "menu") => {
|
||||
if (control === "trash") {
|
||||
await this.helper.when.hover("layer-list-item:" + id);
|
||||
await this.helper.when.click("layer-list-item:" + id + ":delete");
|
||||
return;
|
||||
}
|
||||
|
||||
await this.helper.when.click("layer-list-item:" + id);
|
||||
await this.helper.when.click("skip-target-layer-editor");
|
||||
await this.helper.when.click("menu-delete-layer");
|
||||
},
|
||||
|
||||
setStyle: async (
|
||||
styleProperties:
|
||||
| "geojson"
|
||||
@@ -86,7 +73,6 @@ export class MaputnikDriver {
|
||||
| "font"
|
||||
| "zoom_7_center_0_51"
|
||||
| "access_tokens"
|
||||
| "grouped_layers"
|
||||
| "",
|
||||
zoom?: number
|
||||
) => {
|
||||
@@ -99,7 +85,6 @@ export class MaputnikDriver {
|
||||
font: "example-style-with-fonts.json",
|
||||
zoom_7_center_0_51: "example-style-with-zoom-7-and-center-0-51.json",
|
||||
access_tokens: "access-token-style.json",
|
||||
grouped_layers: "grouped-layers-style.json",
|
||||
};
|
||||
|
||||
const url = new URL(baseUrl);
|
||||
|
||||
@@ -14,7 +14,6 @@ export default defineConfig({
|
||||
files: ["**/*.{js,jsx,ts,tsx}"],
|
||||
ignores: [
|
||||
"dist/**/*",
|
||||
"public/**/*",
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2024,
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { defineConfig } from "i18next-cli";
|
||||
|
||||
export default defineConfig({
|
||||
locales: ["az", "de", "fr", "he", "it", "ja", "ko", "tr", "zh"],
|
||||
locales: ["de", "fr", "he", "it", "ja", "ko", "tr", "zh"],
|
||||
extract: {
|
||||
input: ["src/**/*.{js,jsx,ts,tsx}"],
|
||||
output: "src/locales/{{language}}/{{namespace}}.json",
|
||||
|
||||
Generated
+1178
-871
File diff suppressed because it is too large
Load Diff
+29
-30
@@ -5,11 +5,10 @@
|
||||
"type": "module",
|
||||
"main": "''",
|
||||
"scripts": {
|
||||
"copy-rtl-text-plugin": "mkdir -p public && cp -p node_modules/@mapbox/mapbox-gl-rtl-text/dist/mapbox-gl-rtl-text.js public/",
|
||||
"start": "npm run copy-rtl-text-plugin && vite",
|
||||
"build": "npm run copy-rtl-text-plugin && tsc && vite build --mode=production",
|
||||
"build-desktop": "npm run copy-rtl-text-plugin && tsc && vite build --mode=desktop && cd desktop && make",
|
||||
"build-linux": "npm run copy-rtl-text-plugin && tsc && vite build --mode=desktop && cd desktop && make bin/linux/maputnik",
|
||||
"start": "vite",
|
||||
"build": "tsc && vite build --mode=production",
|
||||
"build-desktop": "tsc && vite build --mode=desktop && cd desktop && make",
|
||||
"build-linux": "tsc && vite build --mode=desktop && cd desktop && make bin/linux/maputnik",
|
||||
"i18n:extract": "npx i18next-cli extract",
|
||||
"lint": "eslint",
|
||||
"test": "playwright test",
|
||||
@@ -29,16 +28,16 @@
|
||||
"dependencies": {
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lint": "^6.9.7",
|
||||
"@codemirror/state": "^6.7.4",
|
||||
"@codemirror/state": "^6.7.1",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@codemirror/view": "^6.43.11",
|
||||
"@codemirror/view": "^6.43.8",
|
||||
"@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.9.0",
|
||||
"@maplibre/maplibre-gl-style-spec": "^26.4.2",
|
||||
"@maplibre/maplibre-gl-inspect": "^1.8.2",
|
||||
"@maplibre/maplibre-gl-style-spec": "^26.2.1",
|
||||
"array-move": "^4.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"classnames": "^2.5.1",
|
||||
@@ -48,7 +47,7 @@
|
||||
"downshift": "^9.4.0",
|
||||
"events": "^3.3.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"i18next": "^26.4.2",
|
||||
"i18next": "^26.3.6",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"i18next-resources-to-backend": "^1.2.3",
|
||||
"json-stringify-pretty-compact": "^4.0.0",
|
||||
@@ -60,20 +59,20 @@
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"maplibre-gl": "^6.8.0",
|
||||
"maplibre-gl": "^5.24.0",
|
||||
"maputnik-design": "github:maputnik/design#172b06c",
|
||||
"ol": "^10.10.0",
|
||||
"ol-mapbox-style": "^13.4.3",
|
||||
"ol-mapbox-style": "^13.4.2",
|
||||
"pmtiles": "^4.5.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^19.2.8",
|
||||
"react": "^19.2.7",
|
||||
"react-accessible-accordion": "^5.0.1",
|
||||
"react-aria-menubutton": "^8.0.0",
|
||||
"react-aria-modal": "^5.0.2",
|
||||
"react-collapse": "^5.1.1",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "^19.2.8",
|
||||
"react-i18next": "^17.0.13",
|
||||
"react-dom": "^19.2.7",
|
||||
"react-i18next": "^17.0.8",
|
||||
"react-icons": "^5.7.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"reconnecting-websocket": "^4.4.0",
|
||||
@@ -101,7 +100,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
||||
"@playwright/test": "^1.63.0",
|
||||
"@playwright/test": "^1.62.1",
|
||||
"@stylistic/eslint-plugin": "^5.10.0",
|
||||
"@types/codemirror": "^5.60.18",
|
||||
"@types/color": "^4.2.1",
|
||||
@@ -116,36 +115,36 @@
|
||||
"@types/lodash.isequal": "^4.5.8",
|
||||
"@types/lodash.throttle": "^4.1.9",
|
||||
"@types/randomcolor": "^0.5.9",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-aria-menubutton": "^6.2.14",
|
||||
"@types/react-aria-modal": "^5.0.0",
|
||||
"@types/react-collapse": "^5.0.4",
|
||||
"@types/react-color": "^3.0.13",
|
||||
"@types/react-dom": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/string-hash": "^1.1.3",
|
||||
"@types/wicg-file-system-access": "^2023.10.7",
|
||||
"@vitejs/plugin-react": "^6.1.1",
|
||||
"@vitest/coverage-v8": "^5.0.0",
|
||||
"@vitejs/plugin-react": "5.2",
|
||||
"@vitest/coverage-v8": "^4.1.10",
|
||||
"cors": "^2.8.6",
|
||||
"eslint": "^10.10.0",
|
||||
"eslint": "^10.8.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.6",
|
||||
"i18next-cli": "^1.73.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.3",
|
||||
"i18next-cli": "^1.69.0",
|
||||
"istanbul": "^0.4.5",
|
||||
"istanbul-lib-coverage": "^3.2.2",
|
||||
"nyc": "^18.0.0",
|
||||
"postcss": "^8.5.28",
|
||||
"postcss": "^8.5.26",
|
||||
"react-hot-loader": "^4.13.1",
|
||||
"sass": "^1.104.0",
|
||||
"stylelint": "^17.15.0",
|
||||
"sass": "^1.102.0",
|
||||
"stylelint": "^17.14.1",
|
||||
"stylelint-config-recommended-scss": "^17.0.1",
|
||||
"stylelint-scss": "^7.2.0",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.70.0",
|
||||
"uuid": "^14.0.2",
|
||||
"vite": "^8.2.2",
|
||||
"typescript-eslint": "^8.67.0",
|
||||
"uuid": "^14.0.1",
|
||||
"vite": "^8.2.1",
|
||||
"vite-plugin-istanbul": "^9.0.1",
|
||||
"vitest": "^5.0.0"
|
||||
"vitest": "^4.1.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,10 +129,16 @@ type FieldFunctionProps = {
|
||||
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
|
||||
*/
|
||||
export const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
|
||||
const [dataType, setDataType] = React.useState(
|
||||
getDataType(props.value, props.fieldSpec)
|
||||
);
|
||||
const [isEditing, setIsEditing] = React.useState(false);
|
||||
// Keep the expression editor mounted while typing, but otherwise select the
|
||||
// editor from the current value so a collapsed function never renders as stops.
|
||||
const dataType = isEditing ? "expression" : getDataType(props.value, props.fieldSpec);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isEditing) {
|
||||
setDataType(getDataType(props.value, props.fieldSpec));
|
||||
}
|
||||
}, [props.value, props.fieldSpec, isEditing]);
|
||||
|
||||
const getFieldFunctionType = (fieldSpec: any) => {
|
||||
if (fieldSpec.expression.interpolated) {
|
||||
@@ -167,7 +173,7 @@ export const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
|
||||
const deleteExpression = () => {
|
||||
const { fieldSpec, fieldName } = props;
|
||||
props.onChange(fieldName, fieldSpec.default);
|
||||
setIsEditing(false);
|
||||
setDataType("value");
|
||||
};
|
||||
|
||||
const deleteStop = (stopIdx: number) => {
|
||||
@@ -227,10 +233,10 @@ export const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
|
||||
type: "identity",
|
||||
property: value[1],
|
||||
});
|
||||
setIsEditing(false);
|
||||
setDataType("value");
|
||||
} else if (isLiteralExpression(value)) {
|
||||
props.onChange(fieldName, value[1]);
|
||||
setIsEditing(false);
|
||||
setDataType("value");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import Color from "color";
|
||||
import {ChromePicker, type ColorResult} from "react-color";
|
||||
import ChromePicker from "react-color/lib/components/chrome/Chrome";
|
||||
import {type ColorResult} from "react-color";
|
||||
import lodash from "lodash";
|
||||
|
||||
function formatColor(color: ColorResult): string {
|
||||
|
||||
@@ -106,13 +106,13 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
||||
});
|
||||
};
|
||||
|
||||
groupedLayers(layers = this.props.layers): (LayerSpecification & {key: string})[][] {
|
||||
groupedLayers(): (LayerSpecification & {key: string})[][] {
|
||||
const groups = [];
|
||||
const layerIdCount = new Map();
|
||||
|
||||
for (let i = 0; i < layers.length; i++) {
|
||||
const origLayer = layers[i];
|
||||
const previousLayer = layers[i-1];
|
||||
for (let i = 0; i < this.props.layers.length; i++) {
|
||||
const origLayer = this.props.layers[i];
|
||||
const previousLayer = this.props.layers[i-1];
|
||||
layerIdCount.set(origLayer.id,
|
||||
layerIdCount.has(origLayer.id) ? layerIdCount.get(origLayer.id) + 1 : 0
|
||||
);
|
||||
@@ -192,39 +192,7 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
||||
return propsChanged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preserves group state through surviving layer IDs when group indices change.
|
||||
* If deletion joins groups, the result stays expanded when either group was.
|
||||
*/
|
||||
private preserveLayerGroupState(previousLayers: LayerSpecification[]) {
|
||||
if (previousLayers === this.props.layers) return;
|
||||
|
||||
const collapsedByLayer = new Map<string, boolean>();
|
||||
let idx = 0;
|
||||
for (const layers of this.groupedLayers(previousLayers)) {
|
||||
const collapsed = this.isCollapsed(layerPrefix(layers[0].id), idx);
|
||||
for (const layer of layers) {
|
||||
collapsedByLayer.set(layer.id, collapsed);
|
||||
}
|
||||
idx += layers.length;
|
||||
}
|
||||
|
||||
const collapsedGroups: {[key: string]: boolean} = {};
|
||||
idx = 0;
|
||||
for (const layers of this.groupedLayers()) {
|
||||
const lookupKey = [layerPrefix(layers[0].id), idx].join("-");
|
||||
collapsedGroups[lookupKey] = layers.every(layer => collapsedByLayer.get(layer.id) !== false);
|
||||
idx += layers.length;
|
||||
}
|
||||
|
||||
if (!lodash.isEqual(collapsedGroups, this.state.collapsedGroups)) {
|
||||
this.setState({ collapsedGroups });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps: LayerListContainerProps) {
|
||||
this.preserveLayerGroupState(prevProps.layers);
|
||||
|
||||
if (prevProps.selectedLayerIndex !== this.props.selectedLayerIndex) {
|
||||
const selectedItemNode = this.selectedItemRef.current;
|
||||
if (selectedItemNode && selectedItemNode.node) {
|
||||
|
||||
@@ -32,8 +32,6 @@ const DraggableLabel: React.FC<DraggableLabelProps> = (props) => {
|
||||
|
||||
type IconActionProps = {
|
||||
action: string
|
||||
/** Tooltip text, for buttons whose action reads differently from their icon. */
|
||||
title?: string
|
||||
onClick(...args: unknown[]): unknown
|
||||
wdKey?: string
|
||||
classBlockName?: string
|
||||
@@ -64,7 +62,7 @@ class IconAction extends React.Component<IconActionProps> {
|
||||
|
||||
return <button
|
||||
tabIndex={-1}
|
||||
title={this.props.title ?? this.props.action}
|
||||
title={this.props.action}
|
||||
className={`maputnik-layer-list-icon-action ${classAdditions}`}
|
||||
data-wd-key={this.props.wdKey}
|
||||
onClick={this.props.onClick}
|
||||
@@ -113,12 +111,7 @@ export const LayerListItem = React.forwardRef<HTMLLIElement, LayerListItemProps>
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
};
|
||||
|
||||
// The icon and the CSS modifier describe the layer's current visibility,
|
||||
// while the tooltip has to describe what clicking the button does, which is
|
||||
// the opposite of it.
|
||||
const isVisible = visibility === "visible";
|
||||
const visibilityAction = isVisible ? "show" : "hide";
|
||||
const visibilityTitle = isVisible ? "hide" : "show";
|
||||
const visibilityAction = visibility === "visible" ? "show" : "hide";
|
||||
|
||||
// Cast ref to MutableRefObject since we know from the codebase that's what's always passed
|
||||
const refObject = ref as React.MutableRefObject<HTMLLIElement | null> | null;
|
||||
@@ -162,7 +155,6 @@ export const LayerListItem = React.forwardRef<HTMLLIElement, LayerListItemProps>
|
||||
<IconAction
|
||||
wdKey={"layer-list-item:" + props.layerId + ":toggle-visibility"}
|
||||
action={visibilityAction}
|
||||
title={visibilityTitle}
|
||||
classBlockName="visibility"
|
||||
classBlockModifier={visibilityAction}
|
||||
onClick={_e => onLayerVisibilityToggle!(props.layerIndex)}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from "react";
|
||||
import {createRoot} from "react-dom/client";
|
||||
import * as MapLibreGl from "maplibre-gl";
|
||||
import {type LayerSpecification, type LngLat, type Map, type MapOptions, type SourceSpecification, type StyleSpecification} from "maplibre-gl";
|
||||
import MapLibreGl, {type LayerSpecification, type LngLat, type Map, type MapOptions, type SourceSpecification, type StyleSpecification} from "maplibre-gl";
|
||||
import MaplibreInspect from "@maplibre/maplibre-gl-inspect";
|
||||
import colors from "@maplibre/maplibre-gl-inspect/lib/colors";
|
||||
import { FeatureLayerPopup as MapMaplibreGlLayerPopup } from "./MapMaplibreGlLayerPopup";
|
||||
@@ -11,7 +10,7 @@ import { ZoomControl } from "../libs/zoomcontrol";
|
||||
import { type HighlightedLayer, colorHighlightedLayer } from "../libs/highlight";
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
import "../maplibregl.css";
|
||||
import "../libs/maplibre-init";
|
||||
import "../libs/maplibre-rtl";
|
||||
import MaplibreGeocoder, { type MaplibreGeocoderApi, type MaplibreGeocoderApiConfig } from "@maplibre/maplibre-gl-geocoder";
|
||||
import "@maplibre/maplibre-gl-geocoder/dist/maplibre-gl-geocoder.css";
|
||||
import { withTranslation, type WithTranslation } from "react-i18next";
|
||||
@@ -239,7 +238,7 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
|
||||
});
|
||||
|
||||
map.on("data", e => {
|
||||
if(e.dataType !== "source" || !e.tile) return;
|
||||
if(e.dataType !== "tile") return;
|
||||
this.props.onDataChange!({
|
||||
map: this.state.map
|
||||
});
|
||||
|
||||
@@ -59,6 +59,7 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
|
||||
<meta charset="utf-8" />
|
||||
<title>${htmlTitle}</title>
|
||||
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
|
||||
<script src="https://unpkg.com/maplibre-gl@${MAPLIBRE_GL_VERSION}/dist/maplibre-gl.js"></script>
|
||||
<link href="https://unpkg.com/maplibre-gl@${MAPLIBRE_GL_VERSION}/dist/maplibre-gl.css" rel="stylesheet" />
|
||||
<style>
|
||||
body { margin: 0; padding: 0; }
|
||||
@@ -67,9 +68,8 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
|
||||
</head>
|
||||
<body>
|
||||
<div id="map"></div>
|
||||
<script type="module">
|
||||
import {Map} from 'https://unpkg.com/maplibre-gl@${MAPLIBRE_GL_VERSION}/dist/maplibre-gl.mjs';
|
||||
const map = new Map({
|
||||
<script>
|
||||
const map = new maplibregl.Map({
|
||||
container: 'map',
|
||||
style: ${tokenStyle},
|
||||
});
|
||||
|
||||
@@ -322,7 +322,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
||||
data-wd-key="modal:settings.maputnik:renderer"
|
||||
options={[
|
||||
["mlgljs", "MapLibreGL JS"],
|
||||
["ol", t("OpenLayers (experimental)")],
|
||||
["ol", t("Open Layers (experimental)")],
|
||||
]}
|
||||
value={metadata["maputnik:renderer"] || "mlgljs"}
|
||||
onChange={(value) => onChangeMetadataProperty("maputnik:renderer", value)}
|
||||
|
||||
@@ -4,7 +4,6 @@ import resourcesToBackend from "i18next-resources-to-backend";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
|
||||
export const supportedLanguages = {
|
||||
"az": "Azərbaycan",
|
||||
"de": "Deutsch",
|
||||
"en": "English",
|
||||
"fr": "Français",
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { setRTLTextPlugin, setWorkerUrl } from "maplibre-gl";
|
||||
import workerUrl from "maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url";
|
||||
|
||||
setWorkerUrl(workerUrl);
|
||||
setRTLTextPlugin(`${import.meta.env.BASE_URL}mapbox-gl-rtl-text.js`, false);
|
||||
@@ -0,0 +1,3 @@
|
||||
import MapLibreGl from "maplibre-gl";
|
||||
|
||||
MapLibreGl.setRTLTextPlugin("https://unpkg.com/@mapbox/mapbox-gl-rtl-text@0.2.3/mapbox-gl-rtl-text.min.js", false);
|
||||
@@ -32,6 +32,8 @@ export class ZoomControl {
|
||||
|
||||
addEventListeners (){
|
||||
this._map!.on("render", () => this.updateZoomLevel());
|
||||
this._map!.on("zoomIn", () => this.updateZoomLevel());
|
||||
this._map!.on("zoomOut", () => this.updateZoomLevel());
|
||||
}
|
||||
|
||||
onRemove() {
|
||||
|
||||
@@ -46,7 +46,6 @@ The following users can help you with the relevant languages:
|
||||
|
||||
| ISO Code | Language | User |
|
||||
|----------|--------------------|--------------------------------------------|
|
||||
| az | Azerbaijani | [@jamalkamaladdin](https://github.com/jamalkamaladdin) |
|
||||
| de | German | [@josxha](https://github.com/josxha) |
|
||||
| en | English | [@HarelM](https://github.com/HarelM) |
|
||||
| fr | French | [@lhapaipai](https://github.com/lhapaipai) |
|
||||
|
||||
@@ -1,204 +0,0 @@
|
||||
{
|
||||
"<0>Open in OSM</0>. Opens the current view on openstreetmap.org": "<0>OSM-də aç</0>. Cari görünüşü openstreetmap.org saytında açır",
|
||||
"Achromatopsia filter": "Axromatopsiya süzgəci",
|
||||
"Active Sources": "Aktiv Mənbələr",
|
||||
"Add a new source to your style. You can only choose the source type and id at creation time!": "Üslubunuza yeni mənbə əlavə edin. Mənbə növünü və ID-ni yalnız yaradılma zamanı seçə bilərsiniz!",
|
||||
"Add filter": "Süzgəc əlavə et",
|
||||
"Add Layer": "Qat Əlavə Et",
|
||||
"Add New Source": "Yeni Mənbə Əlavə Et",
|
||||
"Add one of the publicly available sources to your style.": "Üslubunuza ictimai mənbələrdən birini əlavə edin.",
|
||||
"Add Source": "Mənbə Əlavə Et",
|
||||
"Add stop": "Dayanacaq əlavə et",
|
||||
"Add value": "Dəyər əlavə et",
|
||||
"Add Variable": "Dəyişən Əlavə Et",
|
||||
"any filter matches": "istənilən süzgəc uyğun gəlirsə",
|
||||
"API key for Stadia Maps.": "Stadia Maps üçün API açarı.",
|
||||
"Base": "Baza",
|
||||
"Bearing": "İstiqamət",
|
||||
"Cancel": "Ləğv et",
|
||||
"Center": "Mərkəz",
|
||||
"Choose Public Source": "İctimai Mənbə Seç",
|
||||
"Choose the default Maputnik renderer for this style.": "Bu üslub üçün defolt Maputnik renderer-ini seçin.",
|
||||
"Click to close the editor": "Redaktoru bağlamaq üçün klikləyin",
|
||||
"Close": "Bağla",
|
||||
"Close modal": "Modalı bağla",
|
||||
"Close popup": "Popapı bağla",
|
||||
"Cluster": "Klaster",
|
||||
"Code Editor": "Kod Redaktoru",
|
||||
"Collapse": "Yığ",
|
||||
"Color accessibility": "Rəng əlçatanlığı",
|
||||
"Comment...": "Şərh...",
|
||||
"Comments": "Şərhlər",
|
||||
"Comments for the current layer. This is non-standard and not in the spec.": "Cari qat üçün şərhlər. Bu, standart deyil və spesifikasiyada yoxdur.",
|
||||
"Convert property into a elevation function": "Xassəni hündürlük funksiyasına çevir",
|
||||
"Convert property into a zoom function": "Xassəni yaxınlaşma funksiyasına çevir",
|
||||
"Convert property to data function": "Xassəni məlumat funksiyasına çevir",
|
||||
"Convert to expression": "İfadəyə çevir",
|
||||
"Coord bottom left": "Koordinat aşağı sol",
|
||||
"Coord bottom right": "Koordinat aşağı sağ",
|
||||
"Coord top left": "Koordinat yuxarı sol",
|
||||
"Coord top right": "Koordinat yuxarı sağ",
|
||||
"CORS policy won't allow fetching resources served over http from https, use a <1>https://</1> domain": "CORS siyasəti https-dən http üzərindən verilən resursların alınmasına icazə vermir, <1>https://</1> domenindən istifadə edin",
|
||||
"Create HTML": "HTML Yarat",
|
||||
"Data Sources": "Məlumat Mənbələri",
|
||||
"Data Sources modal": "Məlumat Mənbələri modalı",
|
||||
"Data value": "Məlumat dəyəri",
|
||||
"Debug": "Sazlama",
|
||||
"Debug modal": "Sazlama modalı",
|
||||
"Decrease the pitch by 10 degrees.": "Meyli 10 dərəcə azalt.",
|
||||
"Decrease the rotation by 15 degrees.": "Fırlanmanı 15 dərəcə azalt.",
|
||||
"Decrease the zoom level by 1.": "Yaxınlaşma səviyyəsini 1 azalt.",
|
||||
"Decrease the zoom level by 2.": "Yaxınlaşma səviyyəsini 2 azalt.",
|
||||
"Default": "Defolt",
|
||||
"Delete": "Sil",
|
||||
"Delete expression": "İfadəni sil",
|
||||
"Delete filter block": "Süzgəc blokunu sil",
|
||||
"Deuteranopia filter": "Deuteranopiya süzgəci",
|
||||
"Drag and drop a style JSON file here or click to browse": "Üslub JSON faylını buraya sürükləyib buraxın və ya seçmək üçün klikləyin",
|
||||
"Duplicate": "Dublikat et",
|
||||
"Encoding": "Kodlaşdırma",
|
||||
"Enter URL...": "URL daxil edin...",
|
||||
"every filter matches": "bütün süzgəclər uyğun gəlirsə",
|
||||
"Expand": "Genişlət",
|
||||
"Export modal": "İxrac modalı",
|
||||
"Filter": "Süzgəc",
|
||||
"Focus map": "Xəritəyə fokuslan",
|
||||
"Function": "Funksiya",
|
||||
"Gallery Styles": "Qalereya Üslubları",
|
||||
"General layout properties": "Ümumi düzülüş xassələri",
|
||||
"GeoJSON": "GeoJSON",
|
||||
"GeoJSON (JSON)": "GeoJSON (JSON)",
|
||||
"GeoJSON (URL)": "GeoJSON (URL)",
|
||||
"GeoJSON URL": "GeoJSON URL",
|
||||
"Global State": "Qlobal Vəziyyət",
|
||||
"Global State Variables": "Qlobal Vəziyyət Dəyişənləri",
|
||||
"Glyphs URL": "Şriftlər URL-i",
|
||||
"Help": "Kömək",
|
||||
"Hide": "Gizlət",
|
||||
"Icon layout properties": "İkon düzülüş xassələri",
|
||||
"Icon paint properties": "İkon boyama xassələri",
|
||||
"If the Map is in focused you can use the following shortcuts": "Xəritə fokusdadırsa aşağıdakı qısayollardan istifadə edə bilərsiniz",
|
||||
"Image": "Şəkil",
|
||||
"Image URL": "Şəkil URL-i",
|
||||
"Increase the pitch by 10 degrees.": "Meyli 10 dərəcə artır.",
|
||||
"Increase the rotation by 15 degrees.": "Fırlanmanı 15 dərəcə artır.",
|
||||
"Increase the zoom level by 1.": "Yaxınlaşma səviyyəsini 1 artır.",
|
||||
"Increase the zoom level by 2.": "Yaxınlaşma səviyyəsini 2 artır.",
|
||||
"Input a data property to base styles off of.": "Üslubların əsaslanacağı məlumat xassəsini daxil edin.",
|
||||
"Input value": "Giriş dəyəri",
|
||||
"Inspect": "Yoxla",
|
||||
"JSON Editor": "JSON Redaktoru",
|
||||
"Key": "Açar",
|
||||
"Layer": "Qat",
|
||||
"Layer editor": "Qat redaktoru",
|
||||
"Layer ID already exists": "Qat ID artıq mövcuddur",
|
||||
"Layers": "Qatlar",
|
||||
"Layers list": "Qatlar siyahısı",
|
||||
"Layout properties": "Düzülüş xassələri",
|
||||
"Learn More": "Ətraflı Öyrən",
|
||||
"Light anchor": "İşıq lövbəri",
|
||||
"Light color": "İşıq rəngi",
|
||||
"Light intensity": "İşıq intensivliyi",
|
||||
"Light position": "İşıq mövqeyi",
|
||||
"Links": "Keçidlər",
|
||||
"Load from a URL. Note that the URL must have <1>CORS enabled</1>.": "URL-dən yüklə. Qeyd edin ki, URL-də <1>CORS aktiv olmalıdır</1>.",
|
||||
"Load from URL": "URL-dən yüklə",
|
||||
"Loading": "Yüklənir",
|
||||
"Loading style": "Üslub yüklənir",
|
||||
"LocationIQ Access Token": "LocationIQ Giriş Tokeni",
|
||||
"Map": "Xəritə",
|
||||
"Map view": "Xəritə görünüşü",
|
||||
"MapTiler Access Token": "MapTiler Giriş Tokeni",
|
||||
"Maputnik on GitHub": "GitHub-da Maputnik",
|
||||
"Max Zoom": "Maks. Yaxınlaşma",
|
||||
"Min Zoom": "Min. Yaxınlaşma",
|
||||
"Move layer down": "Qatı aşağı köçür",
|
||||
"Move layer up": "Qatı yuxarı köçür",
|
||||
"Must provide protocol: <1>http://</1> or <3>https://</3>": "Protokol göstərilməlidir: <1>http://</1> və ya <3>https://</3>",
|
||||
"Must provide protocol: <1>https://</1>": "Protokol göstərilməlidir: <1>https://</1>",
|
||||
"Name": "Ad",
|
||||
"Nested filters are not supported.": "İç-içə süzgəclər dəstəklənmir.",
|
||||
"no filter matches": "heç bir süzgəc uyğun gəlmirsə",
|
||||
"No global state variables defined. Add variables to create reusable values in your style.": "Heç bir qlobal vəziyyət dəyişəni təyin edilməyib. Üslubunuzda təkrar istifadə oluna bilən dəyərlər yaratmaq üçün dəyişən əlavə edin.",
|
||||
"Open": "Aç",
|
||||
"Open a local JSON style from your computer.": "Kompüterinizdən yerli JSON üslubunu açın.",
|
||||
"OpenLayers (experimental)": "OpenLayers (eksperimental)",
|
||||
"Open local Style": "Yerli Üslubu Aç",
|
||||
"Open modal": "Modalı aç",
|
||||
"Open one of the publicly available styles to start from.": "Başlamaq üçün ictimai üslublardan birini açın.",
|
||||
"Open Style": "Üslubu Aç",
|
||||
"Options": "Seçimlər",
|
||||
"Output value": "Çıxış dəyəri",
|
||||
"Owner": "Sahib",
|
||||
"Owner ID of the style. Used by Mapbox or future style APIs.": "Üslubun sahib ID-si. Mapbox və ya gələcək üslub API-ləri tərəfindən istifadə olunur.",
|
||||
"Paint properties": "Boyama xassələri",
|
||||
"Pan down by 100 pixels.": "100 piksel aşağı sürüşdür.",
|
||||
"Pan left by 100 pixels.": "100 piksel sola sürüşdür.",
|
||||
"Pan right by 100 pixels.": "100 piksel sağa sürüşdür.",
|
||||
"Pan up by 100 pixels.": "100 piksel yuxarı sürüşdür.",
|
||||
"Pitch": "Meyl",
|
||||
"PMTiles URL": "PMTiles URL",
|
||||
"Press <1>ESC</1> to lose focus of any active elements, then press one of:": "İstənilən aktiv elementin fokusunu itirmək üçün <1>ESC</1> düyməsini basın, sonra bunlardan birini basın:",
|
||||
"Projection": "Proyeksiya",
|
||||
"Protanopia filter": "Protanopiya süzgəci",
|
||||
"Public access token for LocationIQ services.": "LocationIQ xidmətləri üçün ictimai giriş tokeni.",
|
||||
"Public access token for MapTiler Cloud.": "MapTiler Cloud üçün ictimai giriş tokeni.",
|
||||
"Public access token for Thunderforest services.": "Thunderforest xidmətləri üçün ictimai giriş tokeni.",
|
||||
"Raster (Tile URLs)": "Rastr (Kafel URL-ləri)",
|
||||
"Raster (TileJSON URL)": "Rastr (TileJSON URL)",
|
||||
"Raster DEM (TileJSON URL)": "Rastr DEM (TileJSON URL)",
|
||||
"Raster DEM (XYZ URLs)": "Rastr DEM (XYZ URL-ləri)",
|
||||
"Remove '{{sourceId}}' source": "'{{sourceId}}' mənbəsini sil",
|
||||
"Remove array item": "Massiv elementini sil",
|
||||
"Remove variable": "Dəyişəni sil",
|
||||
"Remove zoom level from stop": "Dayanacaqdan yaxınlaşma səviyyəsini sil",
|
||||
"Revert from expression": "İfadədən geri qaytar",
|
||||
"Save": "Yadda saxla",
|
||||
"Save as": "Fərqli yadda saxla",
|
||||
"Save Style": "Üslubu Yadda Saxla",
|
||||
"Save the JSON style to your computer.": "JSON üslubunu kompüterinizə yadda saxlayın.",
|
||||
"Scheme Type": "Sxem Növü",
|
||||
"Search": "Axtar",
|
||||
"Select a type of data scale (default is 'categorical').": "Məlumat şkalasının növünü seçin (defolt 'categorical'dır).",
|
||||
"Shortcuts": "Qısayollar",
|
||||
"Shortcuts menu": "Qısayollar menyusu",
|
||||
"Show": "Göstər",
|
||||
"Source": "Mənbə",
|
||||
"Source ID": "Mənbə ID",
|
||||
"Source Layer": "Mənbə Qatı",
|
||||
"Source Type": "Mənbə Növü",
|
||||
"Sources": "Mənbələr",
|
||||
"Sprite URL": "Sprite URL-i",
|
||||
"Stadia Maps API Key": "Stadia Maps API Açarı",
|
||||
"Stops": "Dayanacaqlar",
|
||||
"Style Renderer": "Üslub Renderer-i",
|
||||
"Style Settings": "Üslub Parametrləri",
|
||||
"Style Settings modal": "Üslub Parametrləri modalı",
|
||||
"Style URL": "Üslub URL-i",
|
||||
"Switch to filter editor.": "Süzgəc redaktoruna keç.",
|
||||
"switch to layer": "qata keç",
|
||||
"Terrain exaggeration": "Relyef şişirdilməsi",
|
||||
"Terrain source": "Relyef mənbəyi",
|
||||
"Text layout properties": "Mətn düzülüş xassələri",
|
||||
"Text paint properties": "Mətn boyama xassələri",
|
||||
"Thunderforest Access Token": "Thunderforest Giriş Tokeni",
|
||||
"Tile Size": "Kafel Ölçüsü",
|
||||
"Tile URL": "Kafel URL-i",
|
||||
"TileJSON URL": "TileJSON URL",
|
||||
"Toggle inspect": "Yoxlamanı aç/bağla",
|
||||
"Transition delay": "Keçid gecikməsi",
|
||||
"Transition duration": "Keçid müddəti",
|
||||
"Tritanopia filter": "Tritanopiya süzgəci",
|
||||
"Type": "Növ",
|
||||
"Unique ID that identifies the source and is used in the layer to reference the source.": "Mənbəni müəyyən edən və qatda mənbəyə istinad üçün istifadə olunan unikal ID.",
|
||||
"Upgrade to expression": "İfadəyə yüksəlt",
|
||||
"Value": "Dəyər",
|
||||
"Vector (PMTiles)": "Vektor (PMTiles)",
|
||||
"Vector (Tile URLs)": "Vektor (Kafel URL-ləri)",
|
||||
"Vector (TileJSON URL)": "Vektor (TileJSON URL)",
|
||||
"Video": "Video",
|
||||
"Video URL": "Video URL-i",
|
||||
"View": "Görünüş",
|
||||
"You've entered an old style filter.": "Köhnə üslub süzgəci daxil etmisiniz.",
|
||||
"Zoom": "Yaxınlaşma"
|
||||
}
|
||||
@@ -121,7 +121,7 @@
|
||||
"No global state variables defined. Add variables to create reusable values in your style.": "Keine globalen Zustandsvariablen definiert. Füge Variablen hinzu, um wiederverwendbare Werte in deinem Stil zu erstellen.",
|
||||
"Open": "Öffnen",
|
||||
"Open a local JSON style from your computer.": "Öffne einen lokalen JSON Stil von deinem Computer.",
|
||||
"OpenLayers (experimental)": "OpenLayers (experimentell)",
|
||||
"Open Layers (experimental)": "Ebenen öffnen (experimentell)",
|
||||
"Open local Style": "Lokalen Stil öffnen",
|
||||
"Open modal": "Modale Fenster öffnen",
|
||||
"Open one of the publicly available styles to start from.": "Öffne einen der öffentlich verfügbaren Stile, um zu starten.",
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
"No global state variables defined. Add variables to create reusable values in your style.": "Aucune variable d'état global définie. Ajoutez des variables pour créer des valeurs réutilisables dans votre style.",
|
||||
"Open": "Ouvrir",
|
||||
"Open a local JSON style from your computer.": "Ouvrir un style JSON local depuis votre ordinateur.",
|
||||
"OpenLayers (experimental)": "OpenLayers (expérimental)",
|
||||
"Open Layers (experimental)": "Open Layers (expérimental)",
|
||||
"Open local Style": "Ouvrir un style local",
|
||||
"Open modal": "Ouvrir (modale)",
|
||||
"Open one of the publicly available styles to start from.": "Ouvrez l'un des styles publics disponibles pour commencer.",
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
"No global state variables defined. Add variables to create reusable values in your style.": "לא הוגדרו משתני מצב גלובלי. הוסף משתנים כדי ליצור ערכים שניתן לעשות בהם שימוש חוזר בסטייל שלך.",
|
||||
"Open": "פתיחה",
|
||||
"Open a local JSON style from your computer.": "פתיחת סטייל JSON מקומי מהמחשב שלך.",
|
||||
"OpenLayers (experimental)": "OpenLayers (experimental)",
|
||||
"Open Layers (experimental)": "Open Layers (experimental)",
|
||||
"Open local Style": "פתיחת סטייל מקומי",
|
||||
"Open modal": "פתיחת חלונית",
|
||||
"Open one of the publicly available styles to start from.": "פתיחת אחד הסטייליםפ הציבוריים על מנת להתחיל מהם.",
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
"No global state variables defined. Add variables to create reusable values in your style.": "Nessuna variabile di stato globale definita. Aggiungi variabili per creare valori riutilizzabili nel tuo stile.",
|
||||
"Open": "Apri",
|
||||
"Open a local JSON style from your computer.": "Apri uno stile JSON dal tuo computer.",
|
||||
"OpenLayers (experimental)": "OpenLayers (sperimentale)",
|
||||
"Open Layers (experimental)": "Apri livelli (sperimentale)",
|
||||
"Open local Style": "Apri stile locale",
|
||||
"Open modal": "Apri finestra modale",
|
||||
"Open one of the publicly available styles to start from.": "Apro uno degli stili pubblici a partire da.",
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
"No global state variables defined. Add variables to create reusable values in your style.": "グローバルステート変数が定義されていません。スタイルで再利用可能な値を作成するには、変数を追加してください。",
|
||||
"Open": "開く",
|
||||
"Open a local JSON style from your computer.": "コンピュータからローカルJSONスタイルを開きます。",
|
||||
"OpenLayers (experimental)": "OpenLayers (実験的)",
|
||||
"Open Layers (experimental)": "Open Layers (実験的)",
|
||||
"Open local Style": "ローカルスタイルを開く",
|
||||
"Open modal": "モーダルを開く",
|
||||
"Open one of the publicly available styles to start from.": "公開スタイルを選んで開始しましょう。",
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
"No global state variables defined. Add variables to create reusable values in your style.": "전역 상태 변수가 정의되지 않았습니다. 스타일에서 재사용 가능한 값을 생성하려면 변수를 추가하세요.",
|
||||
"Open": "열기",
|
||||
"Open a local JSON style from your computer.": "컴퓨터에서 로컬 JSON 스타일을 엽니다.",
|
||||
"OpenLayers (experimental)": "OpenLayers (실험적)",
|
||||
"Open Layers (experimental)": "레이어 열기 (실험적)",
|
||||
"Open local Style": "로컬 스타일 열기",
|
||||
"Open modal": "모달 열기",
|
||||
"Open one of the publicly available styles to start from.": "공개 스타일 중 하나를 선택하여 시작하세요.",
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
"No global state variables defined. Add variables to create reusable values in your style.": "Genel durum değişkeni tanımlanmadı. Stilinizde yeniden kullanılabilir değerler oluşturmak için değişken ekleyin.",
|
||||
"Open": "Aç",
|
||||
"Open a local JSON style from your computer.": "Bilgisayarınızdan yerel bir JSON stili açın.",
|
||||
"OpenLayers (experimental)": "OpenLayers (deneysel)",
|
||||
"Open Layers (experimental)": "Open Layers (deneysel)",
|
||||
"Open local Style": "Yerel Stili Aç",
|
||||
"Open modal": "Modalı aç",
|
||||
"Open one of the publicly available styles to start from.": "Başlamak için herkese açık stillerden birini açın.",
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
"No global state variables defined. Add variables to create reusable values in your style.": "未定义全局状态变量。添加变量以在样式中创建可重用的值。",
|
||||
"Open": "打开",
|
||||
"Open a local JSON style from your computer.": "从您的计算机打开本地JSON样式。",
|
||||
"OpenLayers (experimental)": "OpenLayers(实验性)",
|
||||
"Open Layers (experimental)": "开放图层(实验性)",
|
||||
"Open local Style": "打开本地样式",
|
||||
"Open modal": "打开模态框",
|
||||
"Open one of the publicly available styles to start from.": "打开一个公开可用的样式开始。",
|
||||
|
||||
@@ -214,7 +214,7 @@
|
||||
|
||||
// SPACE HELPER
|
||||
.maputnik-space {
|
||||
flex-grow: 1;
|
||||
@include mixins.vendor-prefix(flex-grow, 1);
|
||||
}
|
||||
|
||||
// MESSAGE PANEL
|
||||
|
||||
@@ -101,7 +101,8 @@
|
||||
.maputnik-select {
|
||||
@extend .maputnik-input !optional; /* stylelint-disable-line */
|
||||
|
||||
appearance: none;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background: vars.$color-gray url("#{vars.$icon-down-arrow}") right center no-repeat;
|
||||
color: vars.$color-white;
|
||||
background-position: calc(100% - 2px) center;
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
line-height: 1.3;
|
||||
max-height: 50px;
|
||||
opacity: 1;
|
||||
-webkit-transition: opacity 600ms, visibility 600ms;
|
||||
transition: opacity 600ms, visibility 600ms;
|
||||
|
||||
&:focus-within {
|
||||
|
||||
+14
-2
@@ -1,9 +1,21 @@
|
||||
@mixin vendor-prefix($name, $argument) {
|
||||
-webkit-#{$name}: #{$argument};
|
||||
-ms-#{$name}: #{$argument};
|
||||
-moz-#{$name}: #{$argument};
|
||||
-o-#{$name}: #{$argument};
|
||||
#{$name}: #{$argument};
|
||||
}
|
||||
|
||||
@mixin flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
display: -ms-flexbox;
|
||||
|
||||
@include vendor-prefix(flex-direction, row);
|
||||
}
|
||||
|
||||
@mixin flex-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: -ms-flexbox;
|
||||
|
||||
@include vendor-prefix(flex-direction, column);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 6px;
|
||||
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
background-color: #666;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"types": ["geojson", "@types/wicg-file-system-access", "node", "vite/client"],
|
||||
"types": ["geojson", "@types/wicg-file-system-access", "node"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
|
||||
+2
-8
@@ -7,10 +7,7 @@ export default defineConfig(({ mode }) => ({
|
||||
port: 8888,
|
||||
},
|
||||
build: {
|
||||
sourcemap: true,
|
||||
rolldownOptions: {
|
||||
checks: { invalidAnnotation: false },
|
||||
},
|
||||
sourcemap: true
|
||||
},
|
||||
plugins: [
|
||||
react(),
|
||||
@@ -20,11 +17,8 @@ export default defineConfig(({ mode }) => ({
|
||||
forceBuildInstrument: true, // Instrument the source so e2e runs can collect coverage
|
||||
}),
|
||||
],
|
||||
optimizeDeps: {
|
||||
exclude: ["maplibre-gl/dist/maplibre-gl-worker.mjs"],
|
||||
},
|
||||
base: mode === "desktop" ? "/" : "/maputnik/",
|
||||
define: {
|
||||
global: "globalThis"
|
||||
global: "window"
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user