From c798961dcb948607d2abaf90817f431e4bc8937f Mon Sep 17 00:00:00 2001 From: Igor Dykhta Date: Tue, 10 Oct 2023 23:47:52 +0300 Subject: [PATCH] [feat] Introduced dnd-context factory to better override dnd properties (#2364) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Ihor Dykhta Co-authored-by: Giuseppe Macrì --- .gitignore | 1 + package.json | 2 +- src/components/package.json | 8 +- src/components/src/context.tsx | 1 + src/components/src/dnd-context.tsx | 151 +++++++++++++++++ src/components/src/dnd-layer-items.ts | 26 --- src/components/src/index.ts | 3 + src/components/src/kepler-gl.tsx | 158 +----------------- src/components/src/map-container.tsx | 5 +- .../side-panel/layer-panel/custom-palette.tsx | 6 +- .../src/side-panel/layer-panel/layer-list.tsx | 72 ++++---- .../side-panel/layer-panel/layer-panel.tsx | 1 - src/constants/src/default-settings.ts | 3 +- src/constants/src/dnd-layer-items.ts | 8 + src/constants/src/index.ts | 1 + src/reducers/src/layer-utils.ts | 19 +++ .../components/side-panel/layer-list.spec.js | 10 +- yarn.lock | 6 +- 18 files changed, 248 insertions(+), 233 deletions(-) create mode 100644 src/components/src/dnd-context.tsx delete mode 100644 src/components/src/dnd-layer-items.ts create mode 100644 src/constants/src/dnd-layer-items.ts diff --git a/.gitignore b/.gitignore index 5dfb8971c5..c30aa0683e 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ package-lock.json npm-debug.log .history/ +.yarn .pnp.* .yarn/* diff --git a/package.json b/package.json index 7e1d512b4d..d6893acaed 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "test-browser-drive": "NODE_ENV=test node ./test/browser-drive.js debug", "test-node-debug": "NODE_ENV=test node -r ./babel-register.js ./test/node.js", "test-browser-debug": "NODE_ENV=test node -r ./babel-register.js ./test/setup-browser-env.js ./test/js-dom.js", - "test-jest": "jest --watch", + "test-jest": "jest", "test-tape": "yarn test-node && yarn test-browser", "test": "yarn test-jest && yarn test-tape", "cover-tape": "nyc --reporter=json --report-dir=tape-coverage --reporter=html yarn test-tape", diff --git a/src/components/package.json b/src/components/package.json index 36fc05b8db..e02098e664 100644 --- a/src/components/package.json +++ b/src/components/package.json @@ -32,10 +32,10 @@ "dependencies": { "@deck.gl/core": "^8.9.12", "@deck.gl/react": "^8.9.12", - "@dnd-kit/core": "^6.0.5", - "@dnd-kit/modifiers": "^6.0.0", - "@dnd-kit/sortable": "^7.0.1", - "@dnd-kit/utilities": "^3.2.0", + "@dnd-kit/core": "^6.0.8", + "@dnd-kit/modifiers": "^6.0.1", + "@dnd-kit/sortable": "^7.0.2", + "@dnd-kit/utilities": "^3.2.1", "@kepler.gl/actions": "3.0.0-alpha.0", "@kepler.gl/cloud-providers": "3.0.0-alpha.0", "@kepler.gl/constants": "3.0.0-alpha.0", diff --git a/src/components/src/context.tsx b/src/components/src/context.tsx index a7facb1435..20c6b84452 100644 --- a/src/components/src/context.tsx +++ b/src/components/src/context.tsx @@ -27,6 +27,7 @@ const KeplerGlContext = createContext({ id: 'map' }); +// TODO: breakdown this file into multiple files export const FeatureFlagsContext = createContext({}); export type FeatureFlags = {[key: string]: string | boolean}; diff --git a/src/components/src/dnd-context.tsx b/src/components/src/dnd-context.tsx new file mode 100644 index 0000000000..6ba7e7c4b5 --- /dev/null +++ b/src/components/src/dnd-context.tsx @@ -0,0 +1,151 @@ +import React, {useCallback, useMemo, useState, PropsWithChildren} from 'react'; +import styled from 'styled-components'; +import {DndContext as DndKitContext, DragOverlay} from '@dnd-kit/core'; +import {useDispatch} from 'react-redux'; +import { + DND_EMPTY_MODIFIERS, + DND_MODIFIERS, + DROPPABLE_MAP_CONTAINER_TYPE, + SORTABLE_LAYER_TYPE, + SORTABLE_SIDE_PANEL_TYPE +} from '@kepler.gl/constants'; +import {layerConfigChange, reorderLayer, toggleLayerForMap} from '@kepler.gl/actions'; +import LayerPanelHeaderFactory from './side-panel/layer-panel/layer-panel-header'; +import {withState} from './injector'; +import {visStateLens} from '@kepler.gl/reducers'; +import {reorderLayerOrder} from '@kepler.gl/reducers'; +import {VisState} from '@kepler.gl/schemas'; +import {Layer} from '@kepler.gl/layers'; + +export type DndContextProps = PropsWithChildren<{ + visState: VisState; +}>; + +export type DndContextComponent = React.FC; + +export const DragItem = styled.div` + color: ${props => props.theme.textColorHl}; + border-radius: ${props => props.theme.radioButtonRadius}px; + padding: 5px 10px; + display: inline; +`; + +const nop = () => undefined; + +DndContextFactory.deps = [LayerPanelHeaderFactory]; + +function DndContextFactory( + LayerPanelHeader: ReturnType +): React.FC { + const LayerPanelOverlay = ({layer, datasets}) => { + const color = + layer.config.dataId && datasets[layer.config.dataId] + ? datasets[layer.config.dataId].color + : null; + return ( + + ); + }; + + const DndContext = ({children, visState}: DndContextProps) => { + const {datasets, layerOrder, layers, splitMaps} = visState; + + const [activeLayer, setActiveLayer]: [ + Layer | undefined, + (l: Layer | undefined) => void + ] = useState(); + + const dispatch = useDispatch(); + + const isSplit = useMemo(() => splitMaps?.length > 1, [splitMaps]); + const dndModifiers = useMemo(() => (isSplit ? DND_EMPTY_MODIFIERS : DND_MODIFIERS), [isSplit]); + const onDragStart = useCallback( + event => { + const {active} = event; + const newActiveLayer = layers.find(layer => layer.id === active.id); + if (newActiveLayer) { + setActiveLayer(newActiveLayer); + + if (newActiveLayer?.config.isConfigActive) { + dispatch(layerConfigChange(newActiveLayer, {isConfigActive: false})); + } + } + }, + [dispatch, layers] + ); + + const onDragEnd = useCallback( + event => { + const {active, over} = event; + + const {id: activeLayerId} = active; + const overType = over.data.current?.type; + + if (!overType) { + setActiveLayer(undefined); + return; + } + + switch (overType) { + // moving layers into maps + case DROPPABLE_MAP_CONTAINER_TYPE: + const mapIndex = over.data.current?.index ?? 0; + dispatch(toggleLayerForMap(mapIndex, activeLayerId)); + break; + // swaping layers + case SORTABLE_LAYER_TYPE: + const newLayerOrder = reorderLayerOrder(layerOrder, activeLayerId, over.id); + dispatch(reorderLayer(newLayerOrder)); + break; + // moving layers within side panel + case SORTABLE_SIDE_PANEL_TYPE: + // move layer to the end of the list + dispatch( + reorderLayer( + reorderLayerOrder(layerOrder, activeLayerId, layerOrder[layerOrder.length - 1]) + ) + ); + break; + default: + break; + } + + setActiveLayer(undefined); + }, + [dispatch, layerOrder] + ); + + return ( + + {children} + {activeLayer ? ( + + + + + + ) : null} + + ); + }; + + return withState([visStateLens], state => state)(DndContext) as React.FC; +} + +export default DndContextFactory; diff --git a/src/components/src/dnd-layer-items.ts b/src/components/src/dnd-layer-items.ts deleted file mode 100644 index e9afd5840a..0000000000 --- a/src/components/src/dnd-layer-items.ts +++ /dev/null @@ -1,26 +0,0 @@ -import styled from 'styled-components'; -import { - restrictToVerticalAxis, - restrictToWindowEdges, - restrictToParentElement -} from '@dnd-kit/modifiers'; -import {arrayMove} from '@dnd-kit/sortable'; - -export const DragItem = styled.div` - color: ${props => props.theme.textColorHl}; - border-radius: ${props => props.theme.radioButtonRadius}px; - padding: 5px 10px; - display: inline; -`; - -export const DND_MODIFIERS = [restrictToVerticalAxis, restrictToParentElement]; -export const DND_EMPTY_MODIFIERS = []; -export const DRAGOVERLAY_MODIFIERS = [restrictToWindowEdges]; -export const findDndContainerId = (id, items) => - id in items ? id : Object.keys(items).find(key => items[key].includes(id)); -export const getLayerOrderOnSort = (layerOrder, dndItems, activeLayerId, overLayerId) => { - const activeIndex = dndItems.indexOf(activeLayerId); - const overIndex = dndItems.indexOf(overLayerId); - - return activeIndex === overIndex ? layerOrder : arrayMove(layerOrder, activeIndex, overIndex); -}; diff --git a/src/components/src/index.ts b/src/components/src/index.ts index 6e5da81859..b2584e51e7 100644 --- a/src/components/src/index.ts +++ b/src/components/src/index.ts @@ -118,6 +118,8 @@ export {default as LayerGroupSelectorFactory} from './side-panel/map-style-panel export {default as MapStyleSelectorFactory} from './side-panel/map-style-panel/map-style-selector'; export {default as LayerGroupColorPickerFactory} from './side-panel/map-style-panel/map-layer-group-color-picker'; export {default as CustomPanelsFactory} from './side-panel/custom-panel'; +export {default as DndContextFactory} from './dnd-context'; + // // map factories export {default as MapPopoverFactory} from './map/map-popover'; export {default as MapPopoverContentFactory} from './map/map-popover-content'; @@ -294,6 +296,7 @@ export type {PanelMeta} from './side-panel/common/types'; export type {SideBarProps} from './side-panel/side-bar'; export type {FeatureActionPanelProps} from './editor/feature-action-panel'; export type {SingleColorPaletteProps} from './side-panel/layer-panel/single-color-palette'; +export type {DndContextProps, DndContextComponent} from './dnd-context'; export { Icons, diff --git a/src/components/src/kepler-gl.tsx b/src/components/src/kepler-gl.tsx index 7c8aae2795..13ad4ad648 100644 --- a/src/components/src/kepler-gl.tsx +++ b/src/components/src/kepler-gl.tsx @@ -28,9 +28,6 @@ import {IntlProvider} from 'react-intl'; import {messages} from '@kepler.gl/localization'; import {RootContext, FeatureFlagsContextProvider, FeatureFlags} from './context'; import {OnErrorCallBack, OnSuccessCallBack, Viewport} from '@kepler.gl/types'; -import {Layer} from '@kepler.gl/layers'; - -import {DndContext, DragOverlay, DragEndEvent, DragStartEvent} from '@dnd-kit/core'; import { MapStateActions, @@ -58,14 +55,6 @@ import { MISSING_MAPBOX_TOKEN } from '@kepler.gl/constants'; -import { - DragItem, - DND_EMPTY_MODIFIERS, - DRAGOVERLAY_MODIFIERS, - findDndContainerId, - getLayerOrderOnSort -} from './dnd-layer-items'; - import SidePanelFactory from './side-panel'; import MapContainerFactory from './map-container'; import MapsLayoutFactory from './maps-layout'; @@ -74,7 +63,7 @@ import ModalContainerFactory from './modal-container'; import PlotContainerFactory from './plot-container'; import NotificationPanelFactory from './notification-panel'; import GeoCoderPanelFactory from './geocoder-panel'; -import LayerPanelHeaderFactory from './side-panel/layer-panel/layer-panel-header'; +import DndContextFactory from './dnd-context'; import { filterObjectByPredicate, @@ -139,10 +128,6 @@ const BottomWidgetOuter = styled.div( }` ); -const nop = () => { - return; -}; - export const isViewportDisjointed = props => { return ( props.mapState.isSplit && @@ -347,17 +332,11 @@ type KeplerGLBasicProps = { topMapContainerProps?: object; bottomMapContainerProps?: object; - - onDragStart?: (event: DragStartEvent) => void; - onDragEnd?: (event: DragEndEvent) => void; }; type KeplerGLProps = KeplerGlState & KeplerGlActions & KeplerGLBasicProps; type KeplerGLCompState = { dimensions: {width: number; height: number} | null; - activeLayer?: Layer; - isDragging: boolean | null; - dndItems: {sortablelist: string[]; 0: []; 1: []}; }; KeplerGlFactory.deps = [ @@ -369,7 +348,7 @@ KeplerGlFactory.deps = [ SidePanelFactory, PlotContainerFactory, NotificationPanelFactory, - LayerPanelHeaderFactory + DndContextFactory ]; function KeplerGlFactory( @@ -381,7 +360,7 @@ function KeplerGlFactory( SidePanel: ReturnType, PlotContainer: ReturnType, NotificationPanel: ReturnType, - LayerPanelHeader: ReturnType + DndContext: ReturnType ): React.ComponentType KeplerGlState}> { /** @typedef {import('./kepler-gl').UnconnectedKeplerGlProps} KeplerGlProps */ /** @augments React.Component */ @@ -392,16 +371,12 @@ function KeplerGlFactory( static defaultProps = DEFAULT_KEPLER_GL_PROPS; state: KeplerGLCompState = { - dimensions: null, - activeLayer: undefined, - isDragging: null, - dndItems: {sortablelist: [], 0: [], 1: []} + dimensions: null }; componentDidMount() { this._validateMapboxToken(); this._loadMapStyle(); - this._updateDndItems(); if (typeof this.props.onKeplerGlInitialized === 'function') { this.props.onKeplerGlInitialized(); } @@ -410,15 +385,6 @@ function KeplerGlFactory( } } - componentDidUpdate(prevProps) { - if ( - this.props.visState.layerOrder !== prevProps.visState.layerOrder || - this.props.visState.layers !== prevProps.visState.layers - ) { - this._updateDndItems(); - } - } - componentWillUnmount() { if (this.root.current instanceof HTMLElement) { unobserveDimensions(this.root.current); @@ -492,87 +458,10 @@ function KeplerGlFactory( this.props.mapStyleActions.loadMapStyles(allStyles); }; - _updateDndItems = () => { - // update dndItems when layerOrder or layers change - this.setState((state, props) => { - const { - visState: {layerOrder} - } = props; - - return { - dndItems: { - ...state.dndItems, - sortablelist: layerOrder - } - }; - }); - }; - _deleteMapLabels = (containerId, layerId) => { - // delete dnditems in map panel this.props.visStateActions.toggleLayerForMap(containerId, layerId); }; - _handleDragStart = event => { - if (this.props.onDragStart) { - this.props.onDragStart(event); - return; - } - - const {active} = event; - const { - visState: {layers}, - visStateActions - } = this.props; - const activeLayer = layers.find(layer => layer.id === active.id); - this.setState({activeLayer}); - - if (activeLayer?.config.isConfigActive) { - visStateActions.layerConfigChange(activeLayer, {isConfigActive: false}); - } - }; - - _handleDragEnd = event => { - if (this.props.onDragEnd) { - this.props.onDragEnd(event); - return; - } - - const {active, over} = event; - - const { - visState: {layerOrder, splitMaps}, - visStateActions - } = this.props; - const {dndItems} = this.state; - - if (!dndItems) { - return; - } - - const {id: activeLayerId} = active; - const overId = over?.id; // isSplit ? overContainerId : overLayerId - const activeContainer = findDndContainerId(activeLayerId, dndItems); - const overContainer = findDndContainerId(overId, dndItems); - - if (!activeContainer || !overContainer) { - return; - } - - if (activeContainer === overContainer) { - // drag and drop in the same container: Sortablelist - // this sort action may happen in any modes, regardless of isSplit - visStateActions.reorderLayer( - getLayerOrderOnSort(layerOrder, dndItems[activeContainer], activeLayerId, overId) - ); - } else if (!splitMaps[overContainer].layers[activeLayerId]) { - // drag and drop in different containers: Sortablelist -> MapContainer - visStateActions.toggleLayerForMap(overContainer, activeLayerId); - } - - this.setState({activeLayer: undefined}); - }; - // eslint-disable-next-line complexity render() { const { @@ -589,11 +478,9 @@ function KeplerGlFactory( } = this.props; const dimensions = this.state.dimensions || {width, height}; - const activeLayer = this.state.activeLayer; const { splitMaps, // this will store support for split map view is necessary - interactionConfig, - datasets + interactionConfig } = visState; const isSplit = isSplitSelector(this.props); @@ -649,44 +536,11 @@ function KeplerGlFactory( ref={this.root} > - + {!uiState.readOnly && !readOnly && } {mapContainers} - {isSplit && ( - - {activeLayer !== undefined ? ( - - - - ) : null} - - )} {isExportingImage && } {/* 1 geocoder: single mode OR split mode and synced viewports */} diff --git a/src/components/src/map-container.tsx b/src/components/src/map-container.tsx index cfad940dc9..97c29f1653 100644 --- a/src/components/src/map-container.tsx +++ b/src/components/src/map-container.tsx @@ -74,7 +74,8 @@ import { THROTTLE_NOTIFICATION_TIME, DEFAULT_PICKING_RADIUS, NO_MAP_ID, - EMPTY_MAPBOX_STYLE + EMPTY_MAPBOX_STYLE, + DROPPABLE_MAP_CONTAINER_TYPE } from '@kepler.gl/constants'; // Contexts @@ -171,7 +172,7 @@ export const isSplitSelector = props => export const Droppable = ({containerId}) => { const {isOver, setNodeRef} = useDroppable({ id: containerId, - data: {type: 'map', index: containerId}, + data: {type: DROPPABLE_MAP_CONTAINER_TYPE, index: containerId}, disabled: !containerId }); diff --git a/src/components/src/side-panel/layer-panel/custom-palette.tsx b/src/components/src/side-panel/layer-panel/custom-palette.tsx index bbadab51bf..9070b15ee4 100644 --- a/src/components/src/side-panel/layer-panel/custom-palette.tsx +++ b/src/components/src/side-panel/layer-panel/custom-palette.tsx @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import React, {Component, createRef, MouseEventHandler, MouseEvent} from 'react'; +import React, {Component, createRef, MouseEventHandler, MouseEvent, PropsWithChildren} from 'react'; import classnames from 'classnames'; import styled, {css} from 'styled-components'; import { @@ -163,9 +163,9 @@ const WrappedSortableContainer = SortableContainer
{children}
); -type DragHandleProps = {children?: React.ReactNode; className: string}; +type DragHandleProps = PropsWithChildren<{className?: string; listeners?: unknown}>; -const DragHandle = SortableHandle(({className, children}) => ( +export const DragHandle = SortableHandle(({className, children}) => ( {children} )); diff --git a/src/components/src/side-panel/layer-panel/layer-list.tsx b/src/components/src/side-panel/layer-panel/layer-list.tsx index a1e19570ca..0a22472b32 100644 --- a/src/components/src/side-panel/layer-panel/layer-list.tsx +++ b/src/components/src/side-panel/layer-panel/layer-list.tsx @@ -21,16 +21,16 @@ import React, {useMemo} from 'react'; import styled from 'styled-components'; import classnames from 'classnames'; -import LayerPanelFactory from './layer-panel'; + import {Layer, LayerClassesType} from '@kepler.gl/layers'; import {Datasets} from '@kepler.gl/table'; import {UIStateActions, VisStateActions} from '@kepler.gl/actions'; -import {useDroppable} from '@dnd-kit/core'; import {useSortable, SortableContext, verticalListSortingStrategy} from '@dnd-kit/sortable'; import {CSS} from '@dnd-kit/utilities'; +import LayerPanelFactory from './layer-panel'; import {findById} from '@kepler.gl/utils'; -import {dataTestIds} from '@kepler.gl/constants'; +import {dataTestIds, SORTABLE_LAYER_TYPE, SORTABLE_SIDE_PANEL_TYPE} from '@kepler.gl/constants'; export type LayerListProps = { datasets: Datasets; @@ -47,6 +47,12 @@ export type LayerListFactoryDeps = [typeof LayerPanelFactory]; // make sure the element is always visible while is being dragged // item being dragged is appended in body, here to reset its global style +const Container = styled.div` + display: flex; + flex-direction: column; + gap: 8px; +`; + interface SortableStyledItemProps { transition?: string; transform?: string; @@ -85,16 +91,25 @@ LayerListFactory.deps = [LayerPanelFactory]; function LayerListFactory(LayerPanel: ReturnType) { // By wrapping layer panel using a sortable element we don't have to implement the drag and drop logic into the panel itself; // Developers can provide any layer panel implementation and it will still be sortable - const SortableItem = ({layer, idx, panelProps, layerActions}) => { + const SortableItem = ({layer, idx, panelProps, layerActions, disabled}) => { const {attributes, listeners, setNodeRef, isDragging, transform, transition} = useSortable({ - id: layer.id + id: layer.id, + data: { + type: SORTABLE_LAYER_TYPE, + parent: SORTABLE_SIDE_PANEL_TYPE + }, + disabled }); return ( ) { idx={idx} layer={layer} listeners={listeners} + isDraggable={!disabled} /> ); }; - const SortableList = ({containerId, sidePanelDndItems, children}) => { - const {setNodeRef} = useDroppable({id: containerId}); - return ( - -
{children}
-
- ); - }; - const LayerList: React.FC = props => { const { layers, @@ -188,9 +191,14 @@ function LayerListFactory(LayerPanel: ReturnType) { [datasets, openModal, layerTypeOptions] ); - return isSortable ? ( - <> - + return ( + + {/* warning: containerId should be similar to the first key in dndItems defined in kepler-gl.js*/} {layersToShow.map(layer => ( ) { idx={layers.findIndex(l => l?.id === layer.id)} panelProps={panelProps} layerActions={layerActions} + disabled={!isSortable} /> ))} - - - ) : ( - <> - {layersToShow.map(layer => ( - l?.id === layer.id)} - layer={layer} - isDraggable={false} - /> - ))} - + + ); }; return LayerList; diff --git a/src/components/src/side-panel/layer-panel/layer-panel.tsx b/src/components/src/side-panel/layer-panel/layer-panel.tsx index 9c123ec6cb..dcf98b93b7 100644 --- a/src/components/src/side-panel/layer-panel/layer-panel.tsx +++ b/src/components/src/side-panel/layer-panel/layer-panel.tsx @@ -68,7 +68,6 @@ type LayerPanelProps = { const PanelWrapper = styled.div<{active: boolean}>` font-size: 12px; border-radius: 1px; - margin-bottom: 8px; z-index: 1000; &.dragging { cursor: move; diff --git a/src/constants/src/default-settings.ts b/src/constants/src/default-settings.ts index f8c8184dcf..6cb3456cb2 100644 --- a/src/constants/src/default-settings.ts +++ b/src/constants/src/default-settings.ts @@ -1138,7 +1138,8 @@ export const dataTestIds: Record = { errorIcon: 'error-icon', successIcon: 'success-icon', checkmarkIcon: 'checkmark-icon', - sortableLayerItems: 'sortable-layer-items', + sortableLayerItem: 'sortable-layer-item', + staticLayerItem: 'static-layer-item', layerTitleEditor: 'layer__title__editor', removeLayerAction: 'remove-layer-action', layerPanel: 'layer-panel' diff --git a/src/constants/src/dnd-layer-items.ts b/src/constants/src/dnd-layer-items.ts new file mode 100644 index 0000000000..d9347cd4cb --- /dev/null +++ b/src/constants/src/dnd-layer-items.ts @@ -0,0 +1,8 @@ +import {Modifiers} from '@dnd-kit/core'; +import {restrictToVerticalAxis} from '@dnd-kit/modifiers'; + +export const DND_MODIFIERS: Modifiers = [restrictToVerticalAxis]; +export const DND_EMPTY_MODIFIERS: Modifiers = []; +export const SORTABLE_SIDE_PANEL_TYPE = 'root'; +export const DROPPABLE_MAP_CONTAINER_TYPE = 'map'; +export const SORTABLE_LAYER_TYPE = 'layer'; diff --git a/src/constants/src/index.ts b/src/constants/src/index.ts index f6ca3ab207..8a4c126d33 100644 --- a/src/constants/src/index.ts +++ b/src/constants/src/index.ts @@ -29,3 +29,4 @@ export {default as KeyEvent} from './keyevent'; export * from './tooltip'; export * from './user-feedbacks'; export * from './user-guides'; +export * from './dnd-layer-items'; diff --git a/src/reducers/src/layer-utils.ts b/src/reducers/src/layer-utils.ts index 325a239ba5..fe956569d7 100644 --- a/src/reducers/src/layer-utils.ts +++ b/src/reducers/src/layer-utils.ts @@ -18,6 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. +import {arrayMove} from '@dnd-kit/sortable'; import {GEOCODER_LAYER_ID} from '@kepler.gl/constants'; import {Layer as DeckLayer, LayerProps as DeckLayerProps} from '@deck.gl/core/typed'; import { @@ -440,3 +441,21 @@ export function computeDeckLayers( export function getLayerOrderFromLayers(layers: T[]): string[] { return layers.map(({id}) => id); } + +export function reorderLayerOrder( + layerOrder: VisState['layerOrder'], + originLayerId: string, + destinationLayerId: string +): VisState['layerOrder'] { + const activeIndex = layerOrder.indexOf(originLayerId); + const overIndex = layerOrder.indexOf(destinationLayerId); + + return arrayMove(layerOrder, activeIndex, overIndex); +} + +export function addLayerToLayerOrder( + layerOrder: VisState['layerOrder'], + layerId: string +): string[] { + return [layerId, ...layerOrder]; +} diff --git a/test/browser/components/side-panel/layer-list.spec.js b/test/browser/components/side-panel/layer-list.spec.js index 1b3cb58fe8..6dfb8ae27a 100644 --- a/test/browser/components/side-panel/layer-list.spec.js +++ b/test/browser/components/side-panel/layer-list.spec.js @@ -112,7 +112,7 @@ describe('Components -> SidePanel -> LayerPanel -> LayerList', () => { it('render sortable list', () => { renderWithTheme(); - expect(screen.getAllByTestId(dataTestIds.sortableLayerItems)).toHaveLength( + expect(screen.getAllByTestId(dataTestIds.sortableLayerItem)).toHaveLength( defaultProps.layers.length ); screen @@ -124,7 +124,13 @@ describe('Components -> SidePanel -> LayerPanel -> LayerList', () => { it('render non-sortable list', () => { renderWithTheme(); - expect(screen.queryByTestId(dataTestIds.sortableLayerItems)).not.toBeInTheDocument(); + // no sortable items + expect(screen.queryByTestId(dataTestIds.sortableLayerItem)).not.toBeInTheDocument(); + + // only static ones + expect(screen.getAllByTestId(dataTestIds.staticLayerItem)).toHaveLength( + defaultProps.layers.length + ); screen .getAllByTestId(dataTestIds.layerTitleEditor) .forEach((item, index) => expect(item.value).toBe(defaultProps.layers[index].config.label)); diff --git a/yarn.lock b/yarn.lock index ca203b484d..abfba15660 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1667,7 +1667,7 @@ dependencies: tslib "^2.0.0" -"@dnd-kit/core@^6.0.5": +"@dnd-kit/core@^6.0.8": version "6.0.8" resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-6.0.8.tgz#040ae13fea9787ee078e5f0361f3b49b07f3f005" integrity sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA== @@ -1676,7 +1676,7 @@ "@dnd-kit/utilities" "^3.2.1" tslib "^2.0.0" -"@dnd-kit/modifiers@^6.0.0": +"@dnd-kit/modifiers@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/@dnd-kit/modifiers/-/modifiers-6.0.1.tgz#9e39b25fd6e323659604cc74488fe044d33188c8" integrity sha512-rbxcsg3HhzlcMHVHWDuh9LCjpOVAgqbV78wLGI8tziXY3+qcMQ61qVXIvNKQFuhj75dSfD+o+PYZQ/NUk2A23A== @@ -1684,7 +1684,7 @@ "@dnd-kit/utilities" "^3.2.1" tslib "^2.0.0" -"@dnd-kit/sortable@^7.0.1": +"@dnd-kit/sortable@^7.0.2": version "7.0.2" resolved "https://registry.yarnpkg.com/@dnd-kit/sortable/-/sortable-7.0.2.tgz#791d550872457f3f3c843e00d159b640f982011c" integrity sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==