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:
Harel M
2025-09-08 22:08:32 +03:00
committed by GitHub
parent 03f3c5c032
commit 55a487d0c8
14 changed files with 326 additions and 142 deletions

View File

@@ -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

View File

@@ -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",
},
],
});
});
});
});

View File

@@ -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);
});
},

View File

@@ -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");

81
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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<any, AppState> {
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<any, AppState> {
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<any, AppState> {
})
}
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);

View File

@@ -13,28 +13,30 @@ type FieldSourceInternalProps = {
error?: {message: string}
} & WithTranslation;
const FieldSourceInternal: React.FC<FieldSourceInternalProps> = (props) => {
const t = props.t;
const FieldSourceInternal: React.FC<FieldSourceInternalProps> = ({
onChange = () => {},
sourceIds = [],
wdKey,
value,
error,
t
}) => {
return (
<Block
label={t('Source')}
fieldSpec={latest.layer.source}
error={props.error}
data-wd-key={props.wdKey}
error={error}
data-wd-key={wdKey}
>
<InputAutocomplete
value={props.value}
onChange={props.onChange}
options={props.sourceIds?.map((src) => [src, src])}
value={value}
onChange={onChange}
options={sourceIds?.map((src) => [src, src])}
/>
</Block>
);
};
FieldSourceInternal.defaultProps = {
onChange: () => {},
sourceIds: [],
};
const FieldSource = withTranslation()(FieldSourceInternal);
export default FieldSource;

View File

@@ -9,33 +9,31 @@ type FieldSourceLayerInternalProps = {
value?: string
onChange?(...args: unknown[]): unknown
sourceLayerIds?: unknown[]
isFixed?: boolean
error?: {message: string}
} & WithTranslation;
const FieldSourceLayerInternal: React.FC<FieldSourceLayerInternalProps> = (props) => {
const t = props.t;
const FieldSourceLayerInternal: React.FC<FieldSourceLayerInternalProps> = ({
onChange = () => {},
sourceLayerIds = [],
value,
error,
t
}) => {
return (
<Block
label={t('Source Layer')}
fieldSpec={latest.layer['source-layer']}
data-wd-key="layer-source-layer"
error={props.error}
error={error}
>
<InputAutocomplete
value={props.value}
onChange={props.onChange}
options={props.sourceLayerIds?.map((l) => [l, l])}
value={value}
onChange={onChange}
options={sourceLayerIds?.map((l) => [l, l])}
/>
</Block>
);
};
FieldSourceLayerInternal.defaultProps = {
onChange: () => {},
sourceLayerIds: [],
isFixed: false,
};
const FieldSourceLayer = withTranslation()(FieldSourceLayerInternal);
export default FieldSourceLayer;

View File

@@ -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<LayerEditorInternalProps, Laye
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: {
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<LayerEditorInternalProps, Laye
{Object.keys(items).map((id) => {
const item = items[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}
</MenuItem>
</li>

View File

@@ -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<LayerListContainerInter
'maputnik-layer-list-item-group-last': idxInGroup == layers.length - 1 && layers.length > 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<LayerListContainerInter
}
const LayerListContainer = withTranslation()(LayerListContainerInternal);
const LayerListContainerSortable = SortableContainer((props: LayerListContainerProps) => <LayerListContainer {...props} />)
type LayerListProps = LayerListContainerProps & {
onMoveLayer: SortEndHandler
onMoveLayer: OnMoveLayerCallback
};
export default class LayerList extends React.Component<LayerListProps> {
render() {
return <LayerListContainerSortable
{...this.props}
helperClass='sortableHelper'
onSortEnd={this.props.onMoveLayer.bind(this)}
useDragHandle={true}
shouldCancelStart={() => false}
/>
}
const LayerList: React.FC<LayerListProps> = (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 (
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={layerIds} strategy={verticalListSortingStrategy}>
<LayerListContainer {...props} />
</SortableContext>
</DndContext>
);
}
export default LayerList;

View File

@@ -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<HTMLElement>
dragListeners?: React.HTMLAttributes<HTMLElement>
};
const DraggableLabel = SortableHandle((props: DraggableLabelProps) => {
return <div className="maputnik-layer-list-item-handle">
const DraggableLabel: React.FC<DraggableLabelProps> = (props) => {
const {dragAttributes, dragListeners} = props;
return <div className="maputnik-layer-list-item-handle" {...dragAttributes} {...dragListeners}>
<IconLayer
className="layer-handle__icon"
type={props.layerType}
@@ -23,7 +26,7 @@ const DraggableLabel = SortableHandle((props: DraggableLabelProps) => {
{props.layerId}
</button>
</div>
});
};
type IconActionProps = {
action: string
@@ -82,55 +85,80 @@ type LayerListItemProps = {
onLayerVisibilityToggle?(...args: unknown[]): unknown
};
class LayerListItem extends React.Component<LayerListItemProps> {
static defaultProps = {
isSelected: false,
visibility: 'visible',
onLayerCopy: () => {},
onLayerDestroy: () => {},
onLayerVisibilityToggle: () => {},
}
const LayerListItem = React.forwardRef<HTMLLIElement, LayerListItemProps>((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 <IconContext.Provider value={{size: '14px'}}>
<li
id={this.props.id}
key={this.props.layerId}
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 style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
};
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;

View File

@@ -175,7 +175,6 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
}
{!NON_SOURCE_LAYERS.includes(this.state.type) &&
<FieldSourceLayer
isFixed={true}
sourceLayerIds={layers}
value={this.state['source-layer']}
onChange={(v: string) => this.setState({ 'source-layer': v })}

View File

@@ -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<StyleSpecificationWithId>;
save(mapStyle: StyleSpecificationWithId): StyleSpecificationWithId;