From 55a487d0c8dcc0c43efd832323049b39c7eff7e2 Mon Sep 17 00:00:00 2001 From: Harel M Date: Mon, 8 Sep 2025 22:08:32 +0300 Subject: [PATCH] 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> --- CHANGELOG.md | 1 + cypress/e2e/layers.cy.ts | 93 ++++++++++++++++++- cypress/e2e/maputnik-driver.ts | 6 +- cypress/e2e/modal-driver.ts | 4 +- package-lock.json | 81 ++++++++++++----- package.json | 6 +- src/components/App.tsx | 9 +- src/components/FieldSource.tsx | 24 ++--- src/components/FieldSourceLayer.tsx | 24 +++-- src/components/LayerEditor.tsx | 29 +++--- src/components/LayerList.tsx | 54 ++++++++--- src/components/LayerListItem.tsx | 134 +++++++++++++++++----------- src/components/ModalAdd.tsx | 1 - src/libs/definitions.d.ts | 2 + 14 files changed, 326 insertions(+), 142 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d74ccd0..dd223a3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - Replace react-autocomplete with Downshift in the autocomplete component - Add LocationIQ as supported map provider with access token field and gallery style - 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..._ ### 🐞 Bug fixes diff --git a/cypress/e2e/layers.cy.ts b/cypress/e2e/layers.cy.ts index 5de3880b..a3d47074 100644 --- a/cypress/e2e/layers.cy.ts +++ b/cypress/e2e/layers.cy.ts @@ -378,8 +378,57 @@ describe("layers", () => { }); it("groups", () => { - // TODO - // Click each of the layer groups. + when.modal.open(); + const id1 = when.modal.fillLayers({ + id: "aa", + type: "line", + layer: "example", + }); + + when.modal.open(); + const id2 = when.modal.fillLayers({ + id: "aa-2", + type: "line", + layer: "example", + }); + + when.modal.open(); + const id3 = when.modal.fillLayers({ + id: "b", + type: "line", + layer: "example", + }); + + then(get.elementByTestId("layer-list-item:" + id1)).shouldBeVisible(); + then(get.elementByTestId("layer-list-item:" + id2)).shouldNotBeVisible(); + then(get.elementByTestId("layer-list-item:" + id3)).shouldBeVisible(); + when.click("layer-list-group:aa-0"); + then(get.elementByTestId("layer-list-item:" + id1)).shouldBeVisible(); + then(get.elementByTestId("layer-list-item:" + id2)).shouldBeVisible(); + then(get.elementByTestId("layer-list-item:" + id3)).shouldBeVisible(); + when.click("layer-list-item:" + id2); + when.click("skip-target-layer-editor"); + when.click("menu-move-layer-down"); + then(get.elementByTestId("layer-list-group:aa-0")).shouldNotExist(); + then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ + layers: [ + { + id: "aa", + type: "line", + source: "example", + }, + { + id: "b", + type: "line", + source: "example", + }, + { + id: "aa-2", + type: "line", + source: "example", + }, + ], + }); }); }); @@ -495,9 +544,7 @@ describe("layers", () => { }); }); - describe("layereditor jsonlint should error", ()=>{ - it("add", () => { const id = when.modal.fillLayers({ type: "circle", @@ -523,4 +570,42 @@ describe("layers", () => { 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", + }, + ], + }); + }); + }); }); diff --git a/cypress/e2e/maputnik-driver.ts b/cypress/e2e/maputnik-driver.ts index 5af27794..5c885815 100644 --- a/cypress/e2e/maputnik-driver.ts +++ b/cypress/e2e/maputnik-driver.ts @@ -94,8 +94,8 @@ export class MaputnikDriver { public when = { ...this.helper.when, modal: this.modalDriver.when, - within: (selector: string, fn: () => void) => { - this.helper.when.within(fn, selector); + doWithin: (selector: string, fn: () => void) => { + this.helper.when.doWithin(fn, selector); }, tab: () => this.helper.get.element("body").tab(), waitForExampleFileResponse: () => { @@ -145,7 +145,7 @@ export class MaputnikDriver { }, selectWithin: (selector: string, value: string) => { - this.when.within(selector, () => { + this.when.doWithin(selector, () => { this.helper.get.element("select").select(value); }); }, diff --git a/cypress/e2e/modal-driver.ts b/cypress/e2e/modal-driver.ts index 8d3b90a2..a29ad234 100644 --- a/cypress/e2e/modal-driver.ts +++ b/cypress/e2e/modal-driver.ts @@ -20,8 +20,8 @@ export default class ModalDriver { this.helper.when.type("add-layer.layer-id.input", id); if (layer) { - this.helper.when.within(() => { - this.helper.get.element("input").type(layer!); + this.helper.when.doWithin(() => { + this.helper.get.element("input").clear().type(layer!); }, "add-layer.layer-source-block"); } this.helper.when.click("add-layer"); diff --git a/package-lock.json b/package-lock.json index 313f6065..026ac89a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,9 @@ "version": "2.1.1", "license": "MIT", "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", "@maplibre/maplibre-gl-geocoder": "^1.9.0", "@maplibre/maplibre-gl-inspect": "^1.7.1", @@ -54,7 +57,6 @@ "react-i18next": "^15.7.3", "react-icon-base": "^2.1.2", "react-icons": "^5.5.0", - "react-sortable-hoc": "^2.0.0", "reconnecting-websocket": "^4.4.0", "slugify": "^1.6.6", "string-hash": "^1.1.3", @@ -698,6 +700,59 @@ "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": { "version": "4.1.0", "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", "resolved": "https://registry.npmjs.org/array-move/-/array-move-4.0.0.tgz", "integrity": "sha512-+RY54S8OuVvg94THpneQvFRmqWdAHeqtMzgMW6JNurHxe8rsS07cHQdfGkXnTUXiBcyZ0j3SiDIxxj0RPiqCkQ==", + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -7569,14 +7625,6 @@ "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": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -10899,21 +10947,6 @@ "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": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", diff --git a/package.json b/package.json index be5ee6ee..83f0a971 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,9 @@ "license": "MIT", "homepage": "https://github.com/maplibre/maputnik#readme", "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", "@maplibre/maplibre-gl-geocoder": "^1.9.0", "@maplibre/maplibre-gl-inspect": "^1.7.1", @@ -36,6 +39,7 @@ "codemirror": "^5.65.20", "color": "^5.0.0", "detect-browser": "^5.3.0", + "downshift": "^9.0.10", "events": "^3.3.0", "file-saver": "^2.0.5", "i18next": "^25.5.2", @@ -60,7 +64,6 @@ "react-accessible-accordion": "^5.0.1", "react-aria-menubutton": "^7.0.3", "react-aria-modal": "^5.0.2", - "downshift": "^9.0.10", "react-collapse": "^5.1.1", "react-color": "^2.19.3", "react-dom": "^18.2.0", @@ -68,7 +71,6 @@ "react-i18next": "^15.7.3", "react-icon-base": "^2.1.2", "react-icons": "^5.5.0", - "react-sortable-hoc": "^2.0.0", "reconnecting-websocket": "^4.4.0", "slugify": "^1.6.6", "string-hash": "^1.1.3", diff --git a/src/components/App.tsx b/src/components/App.tsx index 4c74d85d..992d9b6d 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -33,7 +33,6 @@ import { RevisionStore } from '../libs/revisions' import LayerWatcher from '../libs/layerwatcher' import tokens from '../config/tokens.json' import isEqual from 'lodash.isequal' -import { SortEnd } from 'react-sortable-hoc'; import { MapOptions } from 'maplibre-gl'; import { OnStyleChangedOpts, StyleSpecificationWithId } from '../libs/definitions' @@ -436,7 +435,7 @@ export default class App extends React.Component { if (errors.length > 0) { dirtyMapStyle = cloneDeep(newStyle); - errors.forEach(error => { + for (const error of errors) { const {message} = error; if (message) { try { @@ -446,10 +445,10 @@ export default class App extends React.Component { unset(dirtyMapStyle, unsetPath); } catch (err) { - console.warn(err); + console.warn(message + " " + err); } } - }); + } } if(newStyle.glyphs !== this.state.mapStyle.glyphs) { @@ -496,7 +495,7 @@ export default class App extends React.Component { }) } - onMoveLayer = (move: SortEnd) => { + onMoveLayer = (move: {oldIndex: number; newIndex: number}) => { let { oldIndex, newIndex } = move; let layers = this.state.mapStyle.layers; oldIndex = clamp(oldIndex, 0, layers.length-1); diff --git a/src/components/FieldSource.tsx b/src/components/FieldSource.tsx index e08ca16d..3303e1f4 100644 --- a/src/components/FieldSource.tsx +++ b/src/components/FieldSource.tsx @@ -13,28 +13,30 @@ type FieldSourceInternalProps = { error?: {message: string} } & WithTranslation; -const FieldSourceInternal: React.FC = (props) => { - const t = props.t; +const FieldSourceInternal: React.FC = ({ + onChange = () => {}, + sourceIds = [], + wdKey, + value, + error, + t +}) => { return ( [src, src])} + value={value} + onChange={onChange} + options={sourceIds?.map((src) => [src, src])} /> ); }; -FieldSourceInternal.defaultProps = { - onChange: () => {}, - sourceIds: [], -}; const FieldSource = withTranslation()(FieldSourceInternal); export default FieldSource; diff --git a/src/components/FieldSourceLayer.tsx b/src/components/FieldSourceLayer.tsx index 000dcfa1..e13057ec 100644 --- a/src/components/FieldSourceLayer.tsx +++ b/src/components/FieldSourceLayer.tsx @@ -9,33 +9,31 @@ type FieldSourceLayerInternalProps = { value?: string onChange?(...args: unknown[]): unknown sourceLayerIds?: unknown[] - isFixed?: boolean error?: {message: string} } & WithTranslation; -const FieldSourceLayerInternal: React.FC = (props) => { - const t = props.t; +const FieldSourceLayerInternal: React.FC = ({ + onChange = () => {}, + sourceLayerIds = [], + value, + error, + t +}) => { return ( [l, l])} + value={value} + onChange={onChange} + options={sourceLayerIds?.map((l) => [l, l])} /> ); }; -FieldSourceLayerInternal.defaultProps = { - onChange: () => {}, - sourceLayerIds: [], - isFixed: false, -}; - const FieldSourceLayer = withTranslation()(FieldSourceLayerInternal); export default FieldSourceLayer; diff --git a/src/components/LayerEditor.tsx b/src/components/LayerEditor.tsx index 9dfe1d5d..aa5484e5 100644 --- a/src/components/LayerEditor.tsx +++ b/src/components/LayerEditor.tsx @@ -22,6 +22,7 @@ import {formatLayerId} from '../libs/format'; import { WithTranslation, withTranslation } from 'react-i18next'; import { TFunction } from 'i18next'; import { NON_SOURCE_LAYERS } from '../libs/non-source-layers'; +import { OnMoveLayerCallback } from '../libs/definitions'; type MaputnikLayoutGroup = { id: string; @@ -120,7 +121,7 @@ type LayerEditorInternalProps = { spec: object onLayerChanged(...args: unknown[]): unknown onLayerIdChange(...args: unknown[]): unknown - onMoveLayer(...args: unknown[]): unknown + onMoveLayer: OnMoveLayerCallback onLayerDestroy(...args: unknown[]): unknown onLayerCopy(...args: unknown[]): unknown onLayerVisibilityToggle(...args: unknown[]): unknown @@ -322,30 +323,38 @@ class LayerEditorInternal extends React.Component void, disabled?: boolean}} = { + const items: {[key: string]: { + text: string, + handler: () => void, + disabled?: boolean, + wdKey?: string + }} = { delete: { text: t("Delete"), - handler: () => this.props.onLayerDestroy(this.props.layerIndex) + handler: () => this.props.onLayerDestroy(this.props.layerIndex), + wdKey: "menu-delete-layer" }, duplicate: { text: t("Duplicate"), - handler: () => this.props.onLayerCopy(this.props.layerIndex) + handler: () => this.props.onLayerCopy(this.props.layerIndex), + wdKey: "menu-duplicate-layer" }, 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: { text: t("Move layer up"), - // Not actually used... disabled: this.props.isFirstLayer, - handler: () => this.moveLayer(-1) + handler: () => this.moveLayer(-1), + wdKey: "menu-move-layer-up" }, moveLayerDown: { text: t("Move layer down"), - // Not actually used... 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 { const item = items[id]; return
  • - + {item.text}
  • diff --git a/src/components/LayerList.tsx b/src/components/LayerList.tsx index d2151360..3de16c58 100644 --- a/src/components/LayerList.tsx +++ b/src/components/LayerList.tsx @@ -1,16 +1,28 @@ import React, {type JSX} from 'react' import classnames from 'classnames' 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 LayerListItem from './LayerListItem' import ModalAdd from './ModalAdd' -import {SortEndHandler, SortableContainer} from 'react-sortable-hoc'; import type {LayerSpecification, SourceSpecification} from 'maplibre-gl'; import generateUniqueId from '../libs/document-uid'; import { findClosestCommonPrefix, layerPrefix } from '../libs/layer'; import { WithTranslation, withTranslation } from 'react-i18next'; +import { OnMoveLayerCallback } from '../libs/definitions'; type LayerListContainerProps = { layers: LayerSpecification[] @@ -242,7 +254,6 @@ class LayerListContainerInternal extends React.Component 1, 'maputnik-layer-list-item--error': !!layerError })} - index={idx} key={layer.key} id={layer.key} layerId={layer.id} @@ -319,20 +330,35 @@ class LayerListContainerInternal extends React.Component ) type LayerListProps = LayerListContainerProps & { - onMoveLayer: SortEndHandler + onMoveLayer: OnMoveLayerCallback }; -export default class LayerList extends React.Component { - render() { - return false} - /> - } +const LayerList: React.FC = (props) => { + const sensors = useSensors(useSensor(PointerSensor)); + + const handleDragEnd = (event: DragEndEvent) => { + const {active, over} = event; + if (!over) return; + + 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 ( + + + + + + ); } + +export default LayerList; diff --git a/src/components/LayerListItem.tsx b/src/components/LayerListItem.tsx index 04f16c77..3bbb38a5 100644 --- a/src/components/LayerListItem.tsx +++ b/src/components/LayerListItem.tsx @@ -1,20 +1,23 @@ import React from 'react' import classnames from 'classnames' - import {MdContentCopy, MdVisibility, MdVisibilityOff, MdDelete} from 'react-icons/md' import { IconContext } from 'react-icons' +import {useSortable} from '@dnd-kit/sortable' +import {CSS} from '@dnd-kit/utilities' import IconLayer from './IconLayer' -import {SortableElement, SortableHandle} from 'react-sortable-hoc' type DraggableLabelProps = { layerId: string layerType: string + dragAttributes?: React.HTMLAttributes + dragListeners?: React.HTMLAttributes }; -const DraggableLabel = SortableHandle((props: DraggableLabelProps) => { - return
    +const DraggableLabel: React.FC = (props) => { + const {dragAttributes, dragListeners} = props; + return
    { {props.layerId}
    -}); +}; type IconActionProps = { action: string @@ -82,55 +85,80 @@ type LayerListItemProps = { onLayerVisibilityToggle?(...args: unknown[]): unknown }; -class LayerListItem extends React.Component { - static defaultProps = { - isSelected: false, - visibility: 'visible', - onLayerCopy: () => {}, - onLayerDestroy: () => {}, - onLayerVisibilityToggle: () => {}, - } +const LayerListItem = React.forwardRef((props, ref) => { + const { + isSelected = false, + visibility = 'visible', + onLayerCopy = () => {}, + onLayerDestroy = () => {}, + onLayerVisibilityToggle = () => {}, + } = props; - render() { - const visibilityAction = this.props.visibility === 'visible' ? 'show' : 'hide'; + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({id: props.layerId}); - return -
  • 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, - })}> - - - this.props.onLayerDestroy!(this.props.layerIndex)} - /> - this.props.onLayerCopy!(this.props.layerIndex)} - /> - this.props.onLayerVisibilityToggle!(this.props.layerIndex)} - /> -
  • -
    - } -} + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + }; -const LayerListItemSortable = SortableElement((props: LayerListItemProps) => ); + 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 | null; + + return +
  • { + 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, + })}> + + + onLayerDestroy!(props.layerIndex)} + /> + onLayerCopy!(props.layerIndex)} + /> + onLayerVisibilityToggle!(props.layerIndex)} + /> +
  • +
    +}); + +export default LayerListItem; diff --git a/src/components/ModalAdd.tsx b/src/components/ModalAdd.tsx index 2a19f1c1..b80751fd 100644 --- a/src/components/ModalAdd.tsx +++ b/src/components/ModalAdd.tsx @@ -175,7 +175,6 @@ class ModalAddInternal extends React.Component this.setState({ 'source-layer': v })} diff --git a/src/libs/definitions.d.ts b/src/libs/definitions.d.ts index d2725031..b742e1de 100644 --- a/src/libs/definitions.d.ts +++ b/src/libs/definitions.d.ts @@ -10,6 +10,8 @@ export type OnStyleChangedOpts = { export type OnStyleChangedCallback = (newStyle: StyleSpecificationWithId, opts: OnStyleChangedOpts={}) => void; +export type OnMoveLayerCallback = (move: {oldIndex: number; newIndex: number}) => void; + export interface IStyleStore { getLatestStyle(): Promise; save(mapStyle: StyleSpecificationWithId): StyleSpecificationWithId;