mirror of
https://github.com/maputnik/editor.git
synced 2026-06-19 13:47:27 +00:00
Replacing react-sortable-hoc with dnd-kit (#1359)
## Launch Checklist This PR replace react-sortable-hoc which is unmaintained with dnd-kit - Resolves #1016 - Replaces the following PR: #1259 I've tested it locally to make sure it does what it should. I'll see if I can add a test... - [x] Briefly describe the changes in this PR. - [x] Link to related issues. - [x] Write tests for all new functionality. - [x] Add an entry to `CHANGELOG.md` under the `## main` section. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
@@ -18,6 +18,7 @@
|
|||||||
- Replace react-autocomplete with Downshift in the autocomplete component
|
- Replace react-autocomplete with Downshift in the autocomplete component
|
||||||
- Add LocationIQ as supported map provider with access token field and gallery style
|
- Add LocationIQ as supported map provider with access token field and gallery style
|
||||||
- Revmove support for `debug` and `localport` url parameters
|
- 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
|
||||||
- _...Add new stuff here..._
|
- _...Add new stuff here..._
|
||||||
|
|
||||||
### 🐞 Bug fixes
|
### 🐞 Bug fixes
|
||||||
|
|||||||
@@ -378,8 +378,57 @@ describe("layers", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("groups", () => {
|
it("groups", () => {
|
||||||
// TODO
|
when.modal.open();
|
||||||
// Click each of the layer groups.
|
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",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -495,9 +544,7 @@ describe("layers", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe("layereditor jsonlint should error", ()=>{
|
describe("layereditor jsonlint should error", ()=>{
|
||||||
|
|
||||||
it("add", () => {
|
it("add", () => {
|
||||||
const id = when.modal.fillLayers({
|
const id = when.modal.fillLayers({
|
||||||
type: "circle",
|
type: "circle",
|
||||||
@@ -523,4 +570,42 @@ describe("layers", () => {
|
|||||||
error.should('exist');
|
error.should('exist');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("drag and drop", () => {
|
||||||
|
it("move layer should update local storage", () => {
|
||||||
|
when.modal.open();
|
||||||
|
const firstId = when.modal.fillLayers({
|
||||||
|
id: "a",
|
||||||
|
type: "background",
|
||||||
|
});
|
||||||
|
when.modal.open();
|
||||||
|
const secondId = when.modal.fillLayers({
|
||||||
|
id: "b",
|
||||||
|
type: "background",
|
||||||
|
});
|
||||||
|
when.modal.open();
|
||||||
|
const thirdId = when.modal.fillLayers({
|
||||||
|
id: "c",
|
||||||
|
type: "background",
|
||||||
|
});
|
||||||
|
when.dragAndDrop(get.elementByTestId("layer-list-item:" + firstId), get.elementByTestId("layer-list-item:" + thirdId));
|
||||||
|
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: secondId,
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: thirdId,
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: firstId,
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -94,8 +94,8 @@ export class MaputnikDriver {
|
|||||||
public when = {
|
public when = {
|
||||||
...this.helper.when,
|
...this.helper.when,
|
||||||
modal: this.modalDriver.when,
|
modal: this.modalDriver.when,
|
||||||
within: (selector: string, fn: () => void) => {
|
doWithin: (selector: string, fn: () => void) => {
|
||||||
this.helper.when.within(fn, selector);
|
this.helper.when.doWithin(fn, selector);
|
||||||
},
|
},
|
||||||
tab: () => this.helper.get.element("body").tab(),
|
tab: () => this.helper.get.element("body").tab(),
|
||||||
waitForExampleFileResponse: () => {
|
waitForExampleFileResponse: () => {
|
||||||
@@ -145,7 +145,7 @@ export class MaputnikDriver {
|
|||||||
},
|
},
|
||||||
|
|
||||||
selectWithin: (selector: string, value: string) => {
|
selectWithin: (selector: string, value: string) => {
|
||||||
this.when.within(selector, () => {
|
this.when.doWithin(selector, () => {
|
||||||
this.helper.get.element("select").select(value);
|
this.helper.get.element("select").select(value);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ export default class ModalDriver {
|
|||||||
this.helper.when.type("add-layer.layer-id.input", id);
|
this.helper.when.type("add-layer.layer-id.input", id);
|
||||||
|
|
||||||
if (layer) {
|
if (layer) {
|
||||||
this.helper.when.within(() => {
|
this.helper.when.doWithin(() => {
|
||||||
this.helper.get.element("input").type(layer!);
|
this.helper.get.element("input").clear().type(layer!);
|
||||||
}, "add-layer.layer-source-block");
|
}, "add-layer.layer-source-block");
|
||||||
}
|
}
|
||||||
this.helper.when.click("add-layer");
|
this.helper.when.click("add-layer");
|
||||||
|
|||||||
Generated
+57
-24
@@ -9,6 +9,9 @@
|
|||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@mapbox/mapbox-gl-rtl-text": "^0.3.0",
|
"@mapbox/mapbox-gl-rtl-text": "^0.3.0",
|
||||||
"@maplibre/maplibre-gl-geocoder": "^1.9.0",
|
"@maplibre/maplibre-gl-geocoder": "^1.9.0",
|
||||||
"@maplibre/maplibre-gl-inspect": "^1.7.1",
|
"@maplibre/maplibre-gl-inspect": "^1.7.1",
|
||||||
@@ -54,7 +57,6 @@
|
|||||||
"react-i18next": "^15.7.3",
|
"react-i18next": "^15.7.3",
|
||||||
"react-icon-base": "^2.1.2",
|
"react-icon-base": "^2.1.2",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-sortable-hoc": "^2.0.0",
|
|
||||||
"reconnecting-websocket": "^4.4.0",
|
"reconnecting-websocket": "^4.4.0",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"string-hash": "^1.1.3",
|
"string-hash": "^1.1.3",
|
||||||
@@ -698,6 +700,59 @@
|
|||||||
"ms": "^2.1.1"
|
"ms": "^2.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@dnd-kit/accessibility": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/core": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit/accessibility": "^3.1.1",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/sortable": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.0",
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/utilities": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@dual-bundle/import-meta-resolve": {
|
"node_modules/@dual-bundle/import-meta-resolve": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz",
|
||||||
@@ -3779,6 +3834,7 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/array-move/-/array-move-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/array-move/-/array-move-4.0.0.tgz",
|
||||||
"integrity": "sha512-+RY54S8OuVvg94THpneQvFRmqWdAHeqtMzgMW6JNurHxe8rsS07cHQdfGkXnTUXiBcyZ0j3SiDIxxj0RPiqCkQ==",
|
"integrity": "sha512-+RY54S8OuVvg94THpneQvFRmqWdAHeqtMzgMW6JNurHxe8rsS07cHQdfGkXnTUXiBcyZ0j3SiDIxxj0RPiqCkQ==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
},
|
},
|
||||||
@@ -7569,14 +7625,6 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/invariant": {
|
|
||||||
"version": "2.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
|
||||||
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
|
|
||||||
"dependencies": {
|
|
||||||
"loose-envify": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/is-array-buffer": {
|
"node_modules/is-array-buffer": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||||
@@ -10899,21 +10947,6 @@
|
|||||||
"react-dom": ">=16.8"
|
"react-dom": ">=16.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-sortable-hoc": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-sortable-hoc/-/react-sortable-hoc-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-JZUw7hBsAHXK7PTyErJyI7SopSBFRcFHDjWW5SWjcugY0i6iH7f+eJkY8cJmGMlZ1C9xz1J3Vjz0plFpavVeRg==",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.2.0",
|
|
||||||
"invariant": "^2.2.4",
|
|
||||||
"prop-types": "^15.5.7"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"prop-types": "^15.5.7",
|
|
||||||
"react": "^16.3.0 || ^17.0.0",
|
|
||||||
"react-dom": "^16.3.0 || ^17.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/reactcss": {
|
"node_modules/reactcss": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz",
|
||||||
|
|||||||
+4
-2
@@ -23,6 +23,9 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://github.com/maplibre/maputnik#readme",
|
"homepage": "https://github.com/maplibre/maputnik#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@mapbox/mapbox-gl-rtl-text": "^0.3.0",
|
"@mapbox/mapbox-gl-rtl-text": "^0.3.0",
|
||||||
"@maplibre/maplibre-gl-geocoder": "^1.9.0",
|
"@maplibre/maplibre-gl-geocoder": "^1.9.0",
|
||||||
"@maplibre/maplibre-gl-inspect": "^1.7.1",
|
"@maplibre/maplibre-gl-inspect": "^1.7.1",
|
||||||
@@ -36,6 +39,7 @@
|
|||||||
"codemirror": "^5.65.20",
|
"codemirror": "^5.65.20",
|
||||||
"color": "^5.0.0",
|
"color": "^5.0.0",
|
||||||
"detect-browser": "^5.3.0",
|
"detect-browser": "^5.3.0",
|
||||||
|
"downshift": "^9.0.10",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"i18next": "^25.5.2",
|
"i18next": "^25.5.2",
|
||||||
@@ -60,7 +64,6 @@
|
|||||||
"react-accessible-accordion": "^5.0.1",
|
"react-accessible-accordion": "^5.0.1",
|
||||||
"react-aria-menubutton": "^7.0.3",
|
"react-aria-menubutton": "^7.0.3",
|
||||||
"react-aria-modal": "^5.0.2",
|
"react-aria-modal": "^5.0.2",
|
||||||
"downshift": "^9.0.10",
|
|
||||||
"react-collapse": "^5.1.1",
|
"react-collapse": "^5.1.1",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
@@ -68,7 +71,6 @@
|
|||||||
"react-i18next": "^15.7.3",
|
"react-i18next": "^15.7.3",
|
||||||
"react-icon-base": "^2.1.2",
|
"react-icon-base": "^2.1.2",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-sortable-hoc": "^2.0.0",
|
|
||||||
"reconnecting-websocket": "^4.4.0",
|
"reconnecting-websocket": "^4.4.0",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"string-hash": "^1.1.3",
|
"string-hash": "^1.1.3",
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ import { RevisionStore } from '../libs/revisions'
|
|||||||
import LayerWatcher from '../libs/layerwatcher'
|
import LayerWatcher from '../libs/layerwatcher'
|
||||||
import tokens from '../config/tokens.json'
|
import tokens from '../config/tokens.json'
|
||||||
import isEqual from 'lodash.isequal'
|
import isEqual from 'lodash.isequal'
|
||||||
import { SortEnd } from 'react-sortable-hoc';
|
|
||||||
import { MapOptions } from 'maplibre-gl';
|
import { MapOptions } from 'maplibre-gl';
|
||||||
import { OnStyleChangedOpts, StyleSpecificationWithId } from '../libs/definitions'
|
import { OnStyleChangedOpts, StyleSpecificationWithId } from '../libs/definitions'
|
||||||
|
|
||||||
@@ -436,7 +435,7 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
dirtyMapStyle = cloneDeep(newStyle);
|
dirtyMapStyle = cloneDeep(newStyle);
|
||||||
|
|
||||||
errors.forEach(error => {
|
for (const error of errors) {
|
||||||
const {message} = error;
|
const {message} = error;
|
||||||
if (message) {
|
if (message) {
|
||||||
try {
|
try {
|
||||||
@@ -446,10 +445,10 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
unset(dirtyMapStyle, unsetPath);
|
unset(dirtyMapStyle, unsetPath);
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.warn(err);
|
console.warn(message + " " + err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
|
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
|
||||||
@@ -496,7 +495,7 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onMoveLayer = (move: SortEnd) => {
|
onMoveLayer = (move: {oldIndex: number; newIndex: number}) => {
|
||||||
let { oldIndex, newIndex } = move;
|
let { oldIndex, newIndex } = move;
|
||||||
let layers = this.state.mapStyle.layers;
|
let layers = this.state.mapStyle.layers;
|
||||||
oldIndex = clamp(oldIndex, 0, layers.length-1);
|
oldIndex = clamp(oldIndex, 0, layers.length-1);
|
||||||
|
|||||||
@@ -13,28 +13,30 @@ type FieldSourceInternalProps = {
|
|||||||
error?: {message: string}
|
error?: {message: string}
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
const FieldSourceInternal: React.FC<FieldSourceInternalProps> = (props) => {
|
const FieldSourceInternal: React.FC<FieldSourceInternalProps> = ({
|
||||||
const t = props.t;
|
onChange = () => {},
|
||||||
|
sourceIds = [],
|
||||||
|
wdKey,
|
||||||
|
value,
|
||||||
|
error,
|
||||||
|
t
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Block
|
<Block
|
||||||
label={t('Source')}
|
label={t('Source')}
|
||||||
fieldSpec={latest.layer.source}
|
fieldSpec={latest.layer.source}
|
||||||
error={props.error}
|
error={error}
|
||||||
data-wd-key={props.wdKey}
|
data-wd-key={wdKey}
|
||||||
>
|
>
|
||||||
<InputAutocomplete
|
<InputAutocomplete
|
||||||
value={props.value}
|
value={value}
|
||||||
onChange={props.onChange}
|
onChange={onChange}
|
||||||
options={props.sourceIds?.map((src) => [src, src])}
|
options={sourceIds?.map((src) => [src, src])}
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
FieldSourceInternal.defaultProps = {
|
|
||||||
onChange: () => {},
|
|
||||||
sourceIds: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const FieldSource = withTranslation()(FieldSourceInternal);
|
const FieldSource = withTranslation()(FieldSourceInternal);
|
||||||
export default FieldSource;
|
export default FieldSource;
|
||||||
|
|||||||
@@ -9,33 +9,31 @@ type FieldSourceLayerInternalProps = {
|
|||||||
value?: string
|
value?: string
|
||||||
onChange?(...args: unknown[]): unknown
|
onChange?(...args: unknown[]): unknown
|
||||||
sourceLayerIds?: unknown[]
|
sourceLayerIds?: unknown[]
|
||||||
isFixed?: boolean
|
|
||||||
error?: {message: string}
|
error?: {message: string}
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
const FieldSourceLayerInternal: React.FC<FieldSourceLayerInternalProps> = (props) => {
|
const FieldSourceLayerInternal: React.FC<FieldSourceLayerInternalProps> = ({
|
||||||
const t = props.t;
|
onChange = () => {},
|
||||||
|
sourceLayerIds = [],
|
||||||
|
value,
|
||||||
|
error,
|
||||||
|
t
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Block
|
<Block
|
||||||
label={t('Source Layer')}
|
label={t('Source Layer')}
|
||||||
fieldSpec={latest.layer['source-layer']}
|
fieldSpec={latest.layer['source-layer']}
|
||||||
data-wd-key="layer-source-layer"
|
data-wd-key="layer-source-layer"
|
||||||
error={props.error}
|
error={error}
|
||||||
>
|
>
|
||||||
<InputAutocomplete
|
<InputAutocomplete
|
||||||
value={props.value}
|
value={value}
|
||||||
onChange={props.onChange}
|
onChange={onChange}
|
||||||
options={props.sourceLayerIds?.map((l) => [l, l])}
|
options={sourceLayerIds?.map((l) => [l, l])}
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
FieldSourceLayerInternal.defaultProps = {
|
|
||||||
onChange: () => {},
|
|
||||||
sourceLayerIds: [],
|
|
||||||
isFixed: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const FieldSourceLayer = withTranslation()(FieldSourceLayerInternal);
|
const FieldSourceLayer = withTranslation()(FieldSourceLayerInternal);
|
||||||
export default FieldSourceLayer;
|
export default FieldSourceLayer;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {formatLayerId} from '../libs/format';
|
|||||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||||
import { TFunction } from 'i18next';
|
import { TFunction } from 'i18next';
|
||||||
import { NON_SOURCE_LAYERS } from '../libs/non-source-layers';
|
import { NON_SOURCE_LAYERS } from '../libs/non-source-layers';
|
||||||
|
import { OnMoveLayerCallback } from '../libs/definitions';
|
||||||
|
|
||||||
type MaputnikLayoutGroup = {
|
type MaputnikLayoutGroup = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -120,7 +121,7 @@ type LayerEditorInternalProps = {
|
|||||||
spec: object
|
spec: object
|
||||||
onLayerChanged(...args: unknown[]): unknown
|
onLayerChanged(...args: unknown[]): unknown
|
||||||
onLayerIdChange(...args: unknown[]): unknown
|
onLayerIdChange(...args: unknown[]): unknown
|
||||||
onMoveLayer(...args: unknown[]): unknown
|
onMoveLayer: OnMoveLayerCallback
|
||||||
onLayerDestroy(...args: unknown[]): unknown
|
onLayerDestroy(...args: unknown[]): unknown
|
||||||
onLayerCopy(...args: unknown[]): unknown
|
onLayerCopy(...args: unknown[]): unknown
|
||||||
onLayerVisibilityToggle(...args: unknown[]): unknown
|
onLayerVisibilityToggle(...args: unknown[]): unknown
|
||||||
@@ -322,30 +323,38 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
|||||||
|
|
||||||
const layout = this.props.layer.layout || {}
|
const layout = this.props.layer.layout || {}
|
||||||
|
|
||||||
const items: {[key: string]: {text: string, handler: () => void, disabled?: boolean}} = {
|
const items: {[key: string]: {
|
||||||
|
text: string,
|
||||||
|
handler: () => void,
|
||||||
|
disabled?: boolean,
|
||||||
|
wdKey?: string
|
||||||
|
}} = {
|
||||||
delete: {
|
delete: {
|
||||||
text: t("Delete"),
|
text: t("Delete"),
|
||||||
handler: () => this.props.onLayerDestroy(this.props.layerIndex)
|
handler: () => this.props.onLayerDestroy(this.props.layerIndex),
|
||||||
|
wdKey: "menu-delete-layer"
|
||||||
},
|
},
|
||||||
duplicate: {
|
duplicate: {
|
||||||
text: t("Duplicate"),
|
text: t("Duplicate"),
|
||||||
handler: () => this.props.onLayerCopy(this.props.layerIndex)
|
handler: () => this.props.onLayerCopy(this.props.layerIndex),
|
||||||
|
wdKey: "menu-duplicate-layer"
|
||||||
},
|
},
|
||||||
hide: {
|
hide: {
|
||||||
text: (layout.visibility === "none") ? t("Show") : t("Hide"),
|
text: (layout.visibility === "none") ? t("Show") : t("Hide"),
|
||||||
handler: () => this.props.onLayerVisibilityToggle(this.props.layerIndex)
|
handler: () => this.props.onLayerVisibilityToggle(this.props.layerIndex),
|
||||||
|
wdKey: "menu-hide-layer"
|
||||||
},
|
},
|
||||||
moveLayerUp: {
|
moveLayerUp: {
|
||||||
text: t("Move layer up"),
|
text: t("Move layer up"),
|
||||||
// Not actually used...
|
|
||||||
disabled: this.props.isFirstLayer,
|
disabled: this.props.isFirstLayer,
|
||||||
handler: () => this.moveLayer(-1)
|
handler: () => this.moveLayer(-1),
|
||||||
|
wdKey: "menu-move-layer-up"
|
||||||
},
|
},
|
||||||
moveLayerDown: {
|
moveLayerDown: {
|
||||||
text: t("Move layer down"),
|
text: t("Move layer down"),
|
||||||
// Not actually used...
|
|
||||||
disabled: this.props.isLastLayer,
|
disabled: this.props.isLastLayer,
|
||||||
handler: () => this.moveLayer(+1)
|
handler: () => this.moveLayer(+1),
|
||||||
|
wdKey: "menu-move-layer-down"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,7 +391,7 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
|||||||
{Object.keys(items).map((id) => {
|
{Object.keys(items).map((id) => {
|
||||||
const item = items[id];
|
const item = items[id];
|
||||||
return <li key={id}>
|
return <li key={id}>
|
||||||
<MenuItem value={id} className='more-menu__menu__item'>
|
<MenuItem value={id} className='more-menu__menu__item' data-wd-key={item.wdKey}>
|
||||||
{item.text}
|
{item.text}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,16 +1,28 @@
|
|||||||
import React, {type JSX} from 'react'
|
import React, {type JSX} from 'react'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import lodash from 'lodash';
|
import lodash from 'lodash';
|
||||||
|
import {
|
||||||
|
DndContext,
|
||||||
|
PointerSensor,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
closestCenter,
|
||||||
|
DragEndEvent,
|
||||||
|
} from '@dnd-kit/core';
|
||||||
|
import {
|
||||||
|
SortableContext,
|
||||||
|
verticalListSortingStrategy,
|
||||||
|
} from '@dnd-kit/sortable';
|
||||||
|
|
||||||
import LayerListGroup from './LayerListGroup'
|
import LayerListGroup from './LayerListGroup'
|
||||||
import LayerListItem from './LayerListItem'
|
import LayerListItem from './LayerListItem'
|
||||||
import ModalAdd from './ModalAdd'
|
import ModalAdd from './ModalAdd'
|
||||||
|
|
||||||
import {SortEndHandler, SortableContainer} from 'react-sortable-hoc';
|
|
||||||
import type {LayerSpecification, SourceSpecification} from 'maplibre-gl';
|
import type {LayerSpecification, SourceSpecification} from 'maplibre-gl';
|
||||||
import generateUniqueId from '../libs/document-uid';
|
import generateUniqueId from '../libs/document-uid';
|
||||||
import { findClosestCommonPrefix, layerPrefix } from '../libs/layer';
|
import { findClosestCommonPrefix, layerPrefix } from '../libs/layer';
|
||||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||||
|
import { OnMoveLayerCallback } from '../libs/definitions';
|
||||||
|
|
||||||
type LayerListContainerProps = {
|
type LayerListContainerProps = {
|
||||||
layers: LayerSpecification[]
|
layers: LayerSpecification[]
|
||||||
@@ -242,7 +254,6 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
|||||||
'maputnik-layer-list-item-group-last': idxInGroup == layers.length - 1 && layers.length > 1,
|
'maputnik-layer-list-item-group-last': idxInGroup == layers.length - 1 && layers.length > 1,
|
||||||
'maputnik-layer-list-item--error': !!layerError
|
'maputnik-layer-list-item--error': !!layerError
|
||||||
})}
|
})}
|
||||||
index={idx}
|
|
||||||
key={layer.key}
|
key={layer.key}
|
||||||
id={layer.key}
|
id={layer.key}
|
||||||
layerId={layer.id}
|
layerId={layer.id}
|
||||||
@@ -319,20 +330,35 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
|||||||
}
|
}
|
||||||
|
|
||||||
const LayerListContainer = withTranslation()(LayerListContainerInternal);
|
const LayerListContainer = withTranslation()(LayerListContainerInternal);
|
||||||
const LayerListContainerSortable = SortableContainer((props: LayerListContainerProps) => <LayerListContainer {...props} />)
|
|
||||||
|
|
||||||
type LayerListProps = LayerListContainerProps & {
|
type LayerListProps = LayerListContainerProps & {
|
||||||
onMoveLayer: SortEndHandler
|
onMoveLayer: OnMoveLayerCallback
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class LayerList extends React.Component<LayerListProps> {
|
const LayerList: React.FC<LayerListProps> = (props) => {
|
||||||
render() {
|
const sensors = useSensors(useSensor(PointerSensor));
|
||||||
return <LayerListContainerSortable
|
|
||||||
{...this.props}
|
const handleDragEnd = (event: DragEndEvent) => {
|
||||||
helperClass='sortableHelper'
|
const {active, over} = event;
|
||||||
onSortEnd={this.props.onMoveLayer.bind(this)}
|
if (!over) return;
|
||||||
useDragHandle={true}
|
|
||||||
shouldCancelStart={() => false}
|
const oldIndex = props.layers.findIndex(layer => layer.id === active.id);
|
||||||
/>
|
const newIndex = props.layers.findIndex(layer => layer.id === over.id);
|
||||||
}
|
|
||||||
|
if (oldIndex !== -1 && newIndex !== -1 && oldIndex !== newIndex) {
|
||||||
|
props.onMoveLayer({oldIndex, newIndex});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const layerIds = props.layers.map(layer => layer.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||||
|
<SortableContext items={layerIds} strategy={verticalListSortingStrategy}>
|
||||||
|
<LayerListContainer {...props} />
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default LayerList;
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
|
|
||||||
import {MdContentCopy, MdVisibility, MdVisibilityOff, MdDelete} from 'react-icons/md'
|
import {MdContentCopy, MdVisibility, MdVisibilityOff, MdDelete} from 'react-icons/md'
|
||||||
import { IconContext } from 'react-icons'
|
import { IconContext } from 'react-icons'
|
||||||
|
import {useSortable} from '@dnd-kit/sortable'
|
||||||
|
import {CSS} from '@dnd-kit/utilities'
|
||||||
|
|
||||||
import IconLayer from './IconLayer'
|
import IconLayer from './IconLayer'
|
||||||
import {SortableElement, SortableHandle} from 'react-sortable-hoc'
|
|
||||||
|
|
||||||
|
|
||||||
type DraggableLabelProps = {
|
type DraggableLabelProps = {
|
||||||
layerId: string
|
layerId: string
|
||||||
layerType: string
|
layerType: string
|
||||||
|
dragAttributes?: React.HTMLAttributes<HTMLElement>
|
||||||
|
dragListeners?: React.HTMLAttributes<HTMLElement>
|
||||||
};
|
};
|
||||||
|
|
||||||
const DraggableLabel = SortableHandle((props: DraggableLabelProps) => {
|
const DraggableLabel: React.FC<DraggableLabelProps> = (props) => {
|
||||||
return <div className="maputnik-layer-list-item-handle">
|
const {dragAttributes, dragListeners} = props;
|
||||||
|
return <div className="maputnik-layer-list-item-handle" {...dragAttributes} {...dragListeners}>
|
||||||
<IconLayer
|
<IconLayer
|
||||||
className="layer-handle__icon"
|
className="layer-handle__icon"
|
||||||
type={props.layerType}
|
type={props.layerType}
|
||||||
@@ -23,7 +26,7 @@ const DraggableLabel = SortableHandle((props: DraggableLabelProps) => {
|
|||||||
{props.layerId}
|
{props.layerId}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
});
|
};
|
||||||
|
|
||||||
type IconActionProps = {
|
type IconActionProps = {
|
||||||
action: string
|
action: string
|
||||||
@@ -82,55 +85,80 @@ type LayerListItemProps = {
|
|||||||
onLayerVisibilityToggle?(...args: unknown[]): unknown
|
onLayerVisibilityToggle?(...args: unknown[]): unknown
|
||||||
};
|
};
|
||||||
|
|
||||||
class LayerListItem extends React.Component<LayerListItemProps> {
|
const LayerListItem = React.forwardRef<HTMLLIElement, LayerListItemProps>((props, ref) => {
|
||||||
static defaultProps = {
|
const {
|
||||||
isSelected: false,
|
isSelected = false,
|
||||||
visibility: 'visible',
|
visibility = 'visible',
|
||||||
onLayerCopy: () => {},
|
onLayerCopy = () => {},
|
||||||
onLayerDestroy: () => {},
|
onLayerDestroy = () => {},
|
||||||
onLayerVisibilityToggle: () => {},
|
onLayerVisibilityToggle = () => {},
|
||||||
}
|
} = props;
|
||||||
|
|
||||||
render() {
|
const {
|
||||||
const visibilityAction = this.props.visibility === 'visible' ? 'show' : 'hide';
|
attributes,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging,
|
||||||
|
} = useSortable({id: props.layerId});
|
||||||
|
|
||||||
return <IconContext.Provider value={{size: '14px'}}>
|
const style = {
|
||||||
<li
|
transform: CSS.Transform.toString(transform),
|
||||||
id={this.props.id}
|
transition,
|
||||||
key={this.props.layerId}
|
opacity: isDragging ? 0.5 : 1,
|
||||||
onClick={_e => this.props.onLayerSelect(this.props.layerIndex)}
|
};
|
||||||
data-wd-key={"layer-list-item:"+this.props.layerId}
|
|
||||||
className={classnames({
|
|
||||||
"maputnik-layer-list-item": true,
|
|
||||||
"maputnik-layer-list-item-selected": this.props.isSelected,
|
|
||||||
[this.props.className!]: true,
|
|
||||||
})}>
|
|
||||||
<DraggableLabel {...this.props} />
|
|
||||||
<span style={{flexGrow: 1}} />
|
|
||||||
<IconAction
|
|
||||||
wdKey={"layer-list-item:"+this.props.layerId+":delete"}
|
|
||||||
action={'delete'}
|
|
||||||
classBlockName="delete"
|
|
||||||
onClick={_e => this.props.onLayerDestroy!(this.props.layerIndex)}
|
|
||||||
/>
|
|
||||||
<IconAction
|
|
||||||
wdKey={"layer-list-item:"+this.props.layerId+":copy"}
|
|
||||||
action={'duplicate'}
|
|
||||||
classBlockName="duplicate"
|
|
||||||
onClick={_e => this.props.onLayerCopy!(this.props.layerIndex)}
|
|
||||||
/>
|
|
||||||
<IconAction
|
|
||||||
wdKey={"layer-list-item:"+this.props.layerId+":toggle-visibility"}
|
|
||||||
action={visibilityAction}
|
|
||||||
classBlockName="visibility"
|
|
||||||
classBlockModifier={visibilityAction}
|
|
||||||
onClick={_e => this.props.onLayerVisibilityToggle!(this.props.layerIndex)}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
</IconContext.Provider>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const LayerListItemSortable = SortableElement<LayerListItemProps>((props: LayerListItemProps) => <LayerListItem {...props} />);
|
const visibilityAction = visibility === 'visible' ? 'show' : 'hide';
|
||||||
|
|
||||||
export default LayerListItemSortable;
|
// 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;
|
||||||
|
|
||||||
|
return <IconContext.Provider value={{size: '14px'}}>
|
||||||
|
<li
|
||||||
|
ref={(node) => {
|
||||||
|
setNodeRef(node);
|
||||||
|
if (refObject) {
|
||||||
|
refObject.current = node;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={style}
|
||||||
|
id={props.id}
|
||||||
|
onClick={_e => props.onLayerSelect(props.layerIndex)}
|
||||||
|
data-wd-key={"layer-list-item:" + props.layerId}
|
||||||
|
className={classnames({
|
||||||
|
"maputnik-layer-list-item": true,
|
||||||
|
"maputnik-layer-list-item-selected": isSelected,
|
||||||
|
[props.className!]: true,
|
||||||
|
})}>
|
||||||
|
<DraggableLabel
|
||||||
|
layerId={props.layerId}
|
||||||
|
layerType={props.layerType}
|
||||||
|
dragAttributes={attributes}
|
||||||
|
dragListeners={listeners}
|
||||||
|
/>
|
||||||
|
<span style={{flexGrow: 1}} />
|
||||||
|
<IconAction
|
||||||
|
wdKey={"layer-list-item:" + props.layerId+":delete"}
|
||||||
|
action={'delete'}
|
||||||
|
classBlockName="delete"
|
||||||
|
onClick={_e => onLayerDestroy!(props.layerIndex)}
|
||||||
|
/>
|
||||||
|
<IconAction
|
||||||
|
wdKey={"layer-list-item:" + props.layerId+":copy"}
|
||||||
|
action={'duplicate'}
|
||||||
|
classBlockName="duplicate"
|
||||||
|
onClick={_e => onLayerCopy!(props.layerIndex)}
|
||||||
|
/>
|
||||||
|
<IconAction
|
||||||
|
wdKey={"layer-list-item:"+props.layerId+":toggle-visibility"}
|
||||||
|
action={visibilityAction}
|
||||||
|
classBlockName="visibility"
|
||||||
|
classBlockModifier={visibilityAction}
|
||||||
|
onClick={_e => onLayerVisibilityToggle!(props.layerIndex)}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</IconContext.Provider>
|
||||||
|
});
|
||||||
|
|
||||||
|
export default LayerListItem;
|
||||||
|
|||||||
@@ -175,7 +175,6 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
|
|||||||
}
|
}
|
||||||
{!NON_SOURCE_LAYERS.includes(this.state.type) &&
|
{!NON_SOURCE_LAYERS.includes(this.state.type) &&
|
||||||
<FieldSourceLayer
|
<FieldSourceLayer
|
||||||
isFixed={true}
|
|
||||||
sourceLayerIds={layers}
|
sourceLayerIds={layers}
|
||||||
value={this.state['source-layer']}
|
value={this.state['source-layer']}
|
||||||
onChange={(v: string) => this.setState({ 'source-layer': v })}
|
onChange={(v: string) => this.setState({ 'source-layer': v })}
|
||||||
|
|||||||
Vendored
+2
@@ -10,6 +10,8 @@ export type OnStyleChangedOpts = {
|
|||||||
|
|
||||||
export type OnStyleChangedCallback = (newStyle: StyleSpecificationWithId, opts: OnStyleChangedOpts={}) => void;
|
export type OnStyleChangedCallback = (newStyle: StyleSpecificationWithId, opts: OnStyleChangedOpts={}) => void;
|
||||||
|
|
||||||
|
export type OnMoveLayerCallback = (move: {oldIndex: number; newIndex: number}) => void;
|
||||||
|
|
||||||
export interface IStyleStore {
|
export interface IStyleStore {
|
||||||
getLatestStyle(): Promise<StyleSpecificationWithId>;
|
getLatestStyle(): Promise<StyleSpecificationWithId>;
|
||||||
save(mapStyle: StyleSpecificationWithId): StyleSpecificationWithId;
|
save(mapStyle: StyleSpecificationWithId): StyleSpecificationWithId;
|
||||||
|
|||||||
Reference in New Issue
Block a user