Files
editor/src/components/ModalAdd.tsx
Harel M 55a487d0c8 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>
2025-09-08 22:08:32 +03:00

197 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from 'react'
import InputButton from './InputButton'
import Modal from './Modal'
import FieldType from './FieldType'
import FieldId from './FieldId'
import FieldSource from './FieldSource'
import FieldSourceLayer from './FieldSourceLayer'
import type {LayerSpecification, SourceSpecification} from 'maplibre-gl'
import { WithTranslation, withTranslation } from 'react-i18next';
import { NON_SOURCE_LAYERS } from '../libs/non-source-layers'
type ModalAddInternalProps = {
layers: LayerSpecification[]
onLayersChange(layers: LayerSpecification[]): unknown
isOpen: boolean
onOpenToggle(open: boolean): unknown
// A dict of source id's and the available source layers
sources: Record<string, SourceSpecification & {layers: string[]}>;
} & WithTranslation;
type ModalAddState = {
type: LayerSpecification["type"]
id: string
source?: string
'source-layer'?: string
error?: string | null
};
class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddState> {
addLayer = () => {
if (this.props.layers.some(l => l.id === this.state.id)) {
this.setState({ error: this.props.t('Layer ID already exists') })
return
}
const changedLayers = this.props.layers.slice(0)
const layer: ModalAddState = {
id: this.state.id,
type: this.state.type,
}
if(this.state.type !== 'background') {
layer.source = this.state.source
if(!NON_SOURCE_LAYERS.includes(this.state.type) && this.state['source-layer']) {
layer['source-layer'] = this.state['source-layer']
}
}
changedLayers.push(layer as LayerSpecification)
this.setState({ error: null }, () => {
this.props.onLayersChange(changedLayers)
this.props.onOpenToggle(false)
})
}
constructor(props: ModalAddInternalProps) {
super(props)
const state: ModalAddState = {
type: 'fill',
id: '',
error: null,
}
if(Object.keys(props.sources).length > 0) {
state.source = Object.keys(this.props.sources)[0];
const sourceLayers = this.props.sources[state.source].layers || []
if (sourceLayers.length > 0) {
state['source-layer'] = sourceLayers[0];
}
}
this.state = state;
}
componentDidUpdate(_prevProps: ModalAddInternalProps, prevState: ModalAddState) {
// Check if source is valid for new type
const oldType = prevState.type;
const newType = this.state.type;
const availableSourcesOld = this.getSources(oldType);
const availableSourcesNew = this.getSources(newType);
if(
// Type has changed
oldType !== newType
&& prevState.source !== ""
// Was a valid source previously
&& availableSourcesOld.indexOf(prevState.source!) > -1
// And is not a valid source now
&& availableSourcesNew.indexOf(this.state.source!) < 0
) {
// Clear the source
this.setState({
source: ""
});
}
}
getLayersForSource(source: string) {
const sourceObj = this.props.sources[source] || {};
return sourceObj.layers || [];
}
getSources(type: LayerSpecification["type"]) {
switch(type) {
case 'background':
return [];
case 'hillshade':
case 'color-relief':
return Object.entries(this.props.sources).filter(([_, v]) => v.type === 'raster-dem').map(([k, _]) => k);
case 'raster':
return Object.entries(this.props.sources).filter(([_, v]) => v.type === 'raster').map(([k, _]) => k);
case 'heatmap':
case 'circle':
case 'fill':
case 'fill-extrusion':
case 'line':
case 'symbol':
return Object.entries(this.props.sources).filter(([_, v]) => v.type === 'vector' || v.type === 'geojson').map(([k, _]) => k);
default:
return [];
}
}
render() {
const t = this.props.t;
const sources = this.getSources(this.state.type);
const layers = this.getLayersForSource(this.state.source!);
let errorElement;
if (this.state.error) {
errorElement = (
<div className="maputnik-modal-error">
{this.state.error}
<a
href="#"
onClick={() => this.setState({ error: null })}
className="maputnik-modal-error-close"
>
×
</a>
</div>
);
}
return <Modal
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={t('Add Layer')}
data-wd-key="modal:add-layer"
className="maputnik-add-modal"
>
{errorElement}
<div className="maputnik-add-layer">
<FieldId
value={this.state.id}
wdKey="add-layer.layer-id"
onChange={(v: string) => {
this.setState({ id: v, error: null })
}}
/>
<FieldType
value={this.state.type}
wdKey="add-layer.layer-type"
onChange={(v: LayerSpecification["type"]) => this.setState({ type: v })}
/>
{this.state.type !== 'background' &&
<FieldSource
sourceIds={sources}
wdKey="add-layer.layer-source-block"
value={this.state.source}
onChange={(v: string) => this.setState({ source: v })}
/>
}
{!NON_SOURCE_LAYERS.includes(this.state.type) &&
<FieldSourceLayer
sourceLayerIds={layers}
value={this.state['source-layer']}
onChange={(v: string) => this.setState({ 'source-layer': v })}
/>
}
<InputButton
className="maputnik-add-layer-button"
onClick={this.addLayer}
data-wd-key="add-layer"
>
{t("Add Layer")}
</InputButton>
</div>
</Modal>
}
}
const ModalAdd = withTranslation()(ModalAddInternal);
export default ModalAdd;