diff --git a/components/higher-order/with-dnd/README.md b/components/higher-order/with-dnd/README.md new file mode 100644 index 0000000000000..a6547ced68e86 --- /dev/null +++ b/components/higher-order/with-dnd/README.md @@ -0,0 +1,3 @@ +# withDragAndDrop + +// @todo :clk:doc diff --git a/components/higher-order/with-dnd/index.js b/components/higher-order/with-dnd/index.js new file mode 100644 index 0000000000000..44c72e555382f --- /dev/null +++ b/components/higher-order/with-dnd/index.js @@ -0,0 +1,43 @@ +/** + * External dependencies + */ +import { DragDropContext, DragSource, DropTarget } from 'react-dnd'; +import MixedHTML5Backend from 'react-dnd-html5-mixed-backend'; +// import HTML5Backend from 'react-dnd-html5-backend'; + +/** + * A wrapper around react-dnd DragDropContext higher order component. + * @param { Function } WrappedComponent Component to be wrapped with drag & drop context. + * @return { Function } A component wrapped with the react-dnd HOC. + */ +export default function withDragAndDropContext( WrappedComponent ) { + return DragDropContext( MixedHTML5Backend )( WrappedComponent ); +} + +/** + * @todo :clk:doc + * A wrapper around react-dnd DragSource higher order component. + * @param {[type]} itemType [description] + * @param {[type]} sourceSpec [description] + * @param {[type]} sourceCollect [description] + * @return {[type]} [description] + */ +export function withDragSource( itemType, sourceSpec, sourceCollect ) { + return ( WrappedComponent ) => { + return DragSource( itemType, sourceSpec, sourceCollect )( WrappedComponent ); + }; +} + +/** + * @todo :clk:doc + * A wrapper around react-dnd DropTarget higher order component. + * @param {[type]} itemType [description] + * @param {[type]} targetSpec [description] + * @param {[type]} targetCollect [description] + * @return {[type]} [description] + */ +export function withDropTarget( itemType, targetSpec, targetCollect ) { + return ( WrappedComponent ) => { + return DropTarget( itemType, targetSpec, targetCollect )( WrappedComponent ); + }; +} diff --git a/components/higher-order/with-dnd/test/index.js b/components/higher-order/with-dnd/test/index.js new file mode 100644 index 0000000000000..9b167f4254b6e --- /dev/null +++ b/components/higher-order/with-dnd/test/index.js @@ -0,0 +1 @@ +// @todo :clk:tests diff --git a/components/index.js b/components/index.js index dc63a3eabae28..d86f2b27d36b0 100644 --- a/components/index.js +++ b/components/index.js @@ -45,3 +45,4 @@ export { default as withFocusReturn } from './higher-order/with-focus-return'; export { default as withInstanceId } from './higher-order/with-instance-id'; export { default as withSpokenMessages } from './higher-order/with-spoken-messages'; export { default as withState } from './higher-order/with-state'; +export { default as withDragAndDropContext, withDragSource, withDropTarget } from './higher-order/with-dnd'; diff --git a/editor/actions.js b/editor/actions.js index 5a7733fc3cbf6..07c7a5bbe846e 100644 --- a/editor/actions.js +++ b/editor/actions.js @@ -177,6 +177,20 @@ export function replaceBlock( uid, block ) { return replaceBlocks( uid, block ); } +/** + * @todo :clk:doc + * @param {[type]} uid [description] + * @param {[type]} index [description] + * @return {[type]} [description] + */ +export function moveBlockToIndex( uid, index ) { + return { + type: 'MOVE_BLOCK_TO_INDEX', + uid, + index, + }; +} + export function insertBlock( block, position ) { return insertBlocks( [ block ], position ); } @@ -599,3 +613,25 @@ export function appendDefaultBlock() { type: 'APPEND_DEFAULT_BLOCK', }; } + +/** + * @todo :clk:doc + * [startReordering description] + * @return {[type]} [description] + */ +export function startReordering( ) { + return { + type: 'START_DRAG_AND_DROP', + }; +} + +/** + * @todo :clk:doc + * [stopReordering description] + * @return {[type]} [description] + */ +export function stopReordering( ) { + return { + type: 'STOP_DRAG_AND_DROP', + }; +} diff --git a/editor/components/block-list/block.js b/editor/components/block-list/block.js index 50fb5b47a2ae9..8ef8f89c3369b 100644 --- a/editor/components/block-list/block.js +++ b/editor/components/block-list/block.js @@ -24,6 +24,7 @@ import { __, sprintf } from '@wordpress/i18n'; /** * Internal dependencies */ +import { DragAndDropSource, DragAndDropTarget } from '../draggable'; import BlockMover from '../block-mover'; import BlockDropZone from '../block-drop-zone'; import BlockSettingsMenu from '../block-settings-menu'; @@ -46,6 +47,7 @@ import { stopTyping, updateBlockAttributes, toggleSelection, + moveBlockToIndex, } from '../../actions'; import { getBlock, @@ -62,7 +64,9 @@ import { isSelectionEnabled, isTyping, getBlockMode, + isReorderingInProgress, } from '../../selectors'; +import { DRAGGABLE_BLOCK } from '../../constants'; const { BACKSPACE, ESCAPE, DELETE, ENTER, UP, RIGHT, DOWN, LEFT } = keycodes; @@ -349,7 +353,7 @@ export class BlockListBlock extends Component { } render() { - const { block, order, mode, showContextualToolbar, isLocked } = this.props; + const { block, order, mode, showContextualToolbar, isLocked, isReorderingInProgress } = this.props; const { name: blockName, isValid } = block; const blockType = getBlockType( blockName ); // translators: %s: Type of block (i.e. Text, Image etc) @@ -397,7 +401,32 @@ export class BlockListBlock extends Component { onClick={ this.onClick } { ...wrapperProps } > - + + + { true && + +
DRAG
+
+ } + + { true && + +
DROP HERE
+
+ } + + { ! this.props.isReorderingInProgress && } + { ( showUI || isHovered ) && } { ( showUI || isHovered ) && } { showUI && isValid && showContextualToolbar && } @@ -447,6 +476,7 @@ export class BlockListBlock extends Component { { !! error && } + ); /* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */ @@ -467,6 +497,7 @@ const mapStateToProps = ( state, { uid } ) => ( { meta: getEditedPostAttribute( state, 'meta' ), mode: getBlockMode( state, uid ), isSelectionEnabled: isSelectionEnabled( state ), + isReorderingInProgress: isReorderingInProgress( state ), } ); const mapDispatchToProps = ( dispatch, ownProps ) => ( { @@ -477,6 +508,7 @@ const mapDispatchToProps = ( dispatch, ownProps ) => ( { onSelect() { dispatch( selectBlock( ownProps.uid ) ); }, + onDeselect() { dispatch( clearSelectedBlock() ); }, @@ -496,6 +528,7 @@ const mapDispatchToProps = ( dispatch, ownProps ) => ( { uid: ownProps.uid, } ); }, + onMouseLeave() { dispatch( { type: 'TOGGLE_BLOCK_HOVERED', @@ -527,9 +560,14 @@ const mapDispatchToProps = ( dispatch, ownProps ) => ( { onMetaChange( meta ) { dispatch( editPost( { meta } ) ); }, + toggleSelection( selectionEnabled ) { dispatch( toggleSelection( selectionEnabled ) ); }, + + onMoveBlockToIndex( uid, index ) { + dispatch( moveBlockToIndex( uid, index ) ); + }, } ); BlockListBlock.className = 'editor-block-list__block-edit'; diff --git a/editor/components/block-list/index.js b/editor/components/block-list/index.js index ba96280b555e1..e5b32b94691e0 100644 --- a/editor/components/block-list/index.js +++ b/editor/components/block-list/index.js @@ -17,8 +17,9 @@ import 'element-closest'; /** * WordPress dependencies */ -import { Component } from '@wordpress/element'; +import { Component, compose } from '@wordpress/element'; import { serialize } from '@wordpress/blocks'; +import { withDragAndDropContext } from '@wordpress/components'; /** * Internal dependencies @@ -230,31 +231,34 @@ class BlockList extends Component { } } -export default connect( - ( state ) => ( { - blocks: getBlockUids( state ), - selectionStart: getMultiSelectedBlocksStartUid( state ), - selectionEnd: getMultiSelectedBlocksEndUid( state ), - multiSelectedBlocks: getMultiSelectedBlocks( state ), - multiSelectedBlockUids: getMultiSelectedBlockUids( state ), - selectedBlock: getSelectedBlock( state ), - isSelectionEnabled: isSelectionEnabled( state ), - } ), - ( dispatch ) => ( { - onStartMultiSelect() { - dispatch( startMultiSelect() ); - }, - onStopMultiSelect() { - dispatch( stopMultiSelect() ); - }, - onMultiSelect( start, end ) { - dispatch( multiSelect( start, end ) ); - }, - onSelect( uid ) { - dispatch( selectBlock( uid ) ); - }, - onRemove( uids ) { - dispatch( { type: 'REMOVE_BLOCKS', uids } ); - }, - } ) +export default compose( + withDragAndDropContext, + connect( + ( state ) => ( { + blocks: getBlockUids( state ), + selectionStart: getMultiSelectedBlocksStartUid( state ), + selectionEnd: getMultiSelectedBlocksEndUid( state ), + multiSelectedBlocks: getMultiSelectedBlocks( state ), + multiSelectedBlockUids: getMultiSelectedBlockUids( state ), + selectedBlock: getSelectedBlock( state ), + isSelectionEnabled: isSelectionEnabled( state ), + } ), + ( dispatch ) => ( { + onStartMultiSelect() { + dispatch( startMultiSelect() ); + }, + onStopMultiSelect() { + dispatch( stopMultiSelect() ); + }, + onMultiSelect( start, end ) { + dispatch( multiSelect( start, end ) ); + }, + onSelect( uid ) { + dispatch( selectBlock( uid ) ); + }, + onRemove( uids ) { + dispatch( { type: 'REMOVE_BLOCKS', uids } ); + }, + } ) + ) )( BlockList ); diff --git a/editor/components/draggable/drag-source.js b/editor/components/draggable/drag-source.js new file mode 100644 index 0000000000000..b39354afd8ab4 --- /dev/null +++ b/editor/components/draggable/drag-source.js @@ -0,0 +1,87 @@ +/** + * External dependencies + */ +import { connect } from 'react-redux'; +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { Component, compose } from '@wordpress/element'; +import { withDragSource } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import './style.scss'; +import { startReordering, stopReordering } from '../../actions'; + + +/** + * @todo :clk:doc + * Collector - properties to inject in the wrapped component + * @param {[type]} connect [description] + * @param {[type]} monitor [description] + * @return {[type]} [description] + */ +function sourceCollect( connect, monitor ) { + return { + connectDragSource: connect.dragSource(), + isDragging: monitor.isDragging() + }; +} + +/** + * @todo :clk:doc + * [sourceSpec description] + * @type {Object} + */ +const sourceSpec = { + beginDrag( props ) { + props.startReordering(); + + return { + dragSourceUid: props.dragSourceUid, + index: props.index, + }; + }, + + // every drag is [guaranteed] to fire this endDrag callback + endDrag( props ) { + setTimeout( props.stopReordering, 500 ); + // props.hideDropIndicator(); + }, + +}; + +class DragSource extends Component { + constructor( props ) { + super( props ); + } + + render() { + const classes = classnames( 'draggable-drag-source'); + + return this.props.connectDragSource( +
+ { this.props.children } +
+ ); + } +} + +const mapDispatchToProps = ( dispatch ) => ( { + startReordering() { + dispatch( startReordering() ); + }, + + stopReordering() { + dispatch( stopReordering() ); + }, + +} ); + +export default compose( + connect( undefined, mapDispatchToProps ), + withDragSource( props => props.draggableType, sourceSpec, sourceCollect ), +)( DragSource ); diff --git a/editor/components/draggable/drop-target.js b/editor/components/draggable/drop-target.js new file mode 100644 index 0000000000000..42765140b7f57 --- /dev/null +++ b/editor/components/draggable/drop-target.js @@ -0,0 +1,124 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; +import { findDOMNode } from 'react-dom'; + +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; +import { withDropTarget } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import './style.scss'; + + +/** + * @todo :clk:doc + * [targetCollect description] + * @param {[type]} connect [description] + * @param {[type]} monitor [description] + * @return {[type]} [description] + */ +function targetCollect( connect, monitor ) { + return { + connectDropTarget: connect.dropTarget(), + isOver: monitor.isOver() + }; +} + +/** + * @todo :clk:doc + * [targetSpec description] + * @type {Object} + */ +const targetSpec = { + drop( props, monitor, component ) { + const dragIndex = monitor.getItem().index; // index of dragged item + const hoverIndex = props.index; // index of hovered over item + + console.log(`Drag index: ${dragIndex}`); + console.log(`Drop index: ${hoverIndex}`); + + if ( dragIndex === hoverIndex ) { + return; + } + + props.reIndexCallback( monitor.getItem().dragSourceUid, hoverIndex ); + return; + + // monitor.getItem().index = hoverIndex; + + // Determine rectangle on screen + const hoverBoundingRect = findDOMNode( component ).getBoundingClientRect(); + + // Get vertical middle + const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top ) / 2; + + // Determine mouse position + const clientOffset = monitor.getClientOffset(); + + // Get pixels to the top + const hoverClientY = clientOffset.y - hoverBoundingRect.top; + + + // Dragging downwards + if ( dragIndex < hoverIndex ) { + let dropIndex; + + if ( hoverClientY < hoverMiddleY ) { + dropIndex = hoverIndex - 1; + } else { + dropIndex = hoverIndex; + } + + if ( dragIndex !== dropIndex ) { + // props.moveBlock( dragIndex, dropIndex ); + props.reIndexCallback( monitor.getItem().dragSourceUid, dropIndex ); + } + // monitor.getItem().index = dropIndex; + } + + // Dragging upwards + if ( dragIndex > hoverIndex ) { + let dropIndex; + + if ( hoverClientY > hoverMiddleY ) { + dropIndex = hoverIndex + 1; + } else { + dropIndex = hoverIndex; + } + + if ( dragIndex !== dropIndex ) { + // props.moveBlock( dragIndex, dropIndex ); + props.reIndexCallback( monitor.getItem().dragSourceUid, dropIndex ); + } + // monitor.getItem().index = dropIndex; + } + + }, + hover( props, monitor, component ) { + + } +} + +class DropTarget extends Component { + constructor( props ) { + super( props ); + } + + render() { + const classes = classnames( 'draggable-drop-target'); + + return this.props.connectDropTarget( +
+ { this.props.children } +
+ ); + } +} + +export default withDropTarget( props => props.draggableType, targetSpec, targetCollect )( DropTarget ); diff --git a/editor/components/draggable/index.js b/editor/components/draggable/index.js new file mode 100644 index 0000000000000..a89e3d96a3092 --- /dev/null +++ b/editor/components/draggable/index.js @@ -0,0 +1,2 @@ +export { default as DragAndDropSource } from './drag-source.js'; +export { default as DragAndDropTarget } from './drop-target.js'; diff --git a/editor/components/draggable/style.scss b/editor/components/draggable/style.scss new file mode 100644 index 0000000000000..c6ca4c6fb235b --- /dev/null +++ b/editor/components/draggable/style.scss @@ -0,0 +1,12 @@ + +.draggable-drag-source { + border: 1px solid gray; + padding: 5px; + margin: 5px; +} + +.draggable-drop-target { + border: 1px solid gray; + padding: 5px; + margin: 5px; +} diff --git a/editor/components/draggable/test/index.js b/editor/components/draggable/test/index.js new file mode 100644 index 0000000000000..772624a40f685 --- /dev/null +++ b/editor/components/draggable/test/index.js @@ -0,0 +1 @@ +// @todo:clk:tests diff --git a/editor/constants.js b/editor/constants.js index 7d682b919c680..ec11afc9cd57e 100644 --- a/editor/constants.js +++ b/editor/constants.js @@ -9,3 +9,5 @@ export const BREAK_LARGE = parseInt( breakpointsScssVariables.breakLarge ); export const BREAK_MEDIUM = parseInt( breakpointsScssVariables.breakMedium ); export const BREAK_SMALL = parseInt( breakpointsScssVariables.breakSmall ); export const BREAK_MOBILE = parseInt( breakpointsScssVariables.breakMobile ); +export const DRAGGABLE_BLOCK = 'DRAGGABLE_BLOCK'; +export const DRAGGABLE_IMAGE = 'DRAGGABLE_IMAGE'; diff --git a/editor/reducer.js b/editor/reducer.js index f7b02ff028e65..a239ce1f85cf9 100644 --- a/editor/reducer.js +++ b/editor/reducer.js @@ -236,6 +236,30 @@ export const editor = flow( [ ]; } + case 'MOVE_BLOCK_TO_INDEX': { + if ( ! Number.isInteger( action.index ) || ! state.length ) { + return state; + } + + const blockIndex = state.indexOf( action.uid ); + + if ( blockIndex === -1 || blockIndex === action.index ) { + return state; + } + + if ( action.index < 0 ) { + action.index = 0; + } else if ( action.index >= state.length ) { + action.index = state.length - 1; + } + + const _state = [ ...state ]; + + _state.splice( action.index, 0, _state.splice( blockIndex, 1 )[ 0 ] ); + + return _state; + } + case 'MOVE_BLOCKS_UP': { const firstUid = first( action.uids ); const lastUid = last( action.uids ); @@ -782,6 +806,23 @@ export const reusableBlocks = combineReducers( { }, } ); +/** + * @todo :clk:doc + * Reducer returning drag and drop in progress status. + * @return {Boolean} Updated state + */ +export function isReorderingInProgress( state = false, action ) { + switch ( action.type ) { + case 'START_DRAG_AND_DROP': + return true; + + case 'STOP_DRAG_AND_DROP': + return false; + } + + return state; +} + export default optimist( combineReducers( { editor, currentPost, @@ -797,4 +838,5 @@ export default optimist( combineReducers( { metaBoxes, browser, reusableBlocks, + isReorderingInProgress, } ) ); diff --git a/editor/selectors.js b/editor/selectors.js index 565452ba2c184..2d1b53faabd4c 100644 --- a/editor/selectors.js +++ b/editor/selectors.js @@ -1147,3 +1147,12 @@ export function isPublishingPost( state ) { // considered published return !! stateBeforeRequest && ! isCurrentPostPublished( stateBeforeRequest ); } + +/** + * @todo :clk:doc + * @param {[type]} state [description] + * @return {Boolean} [description] + */ +export function isReorderingInProgress( state ) { + return state.isReorderingInProgress; +} diff --git a/editor/test/reducer.js b/editor/test/reducer.js index 08426a96d4d4f..8da7ca12247cd 100644 --- a/editor/test/reducer.js +++ b/editor/test/reducer.js @@ -403,6 +403,84 @@ describe( 'state', () => { expect( state.present.blockOrder ).toEqual( [ 'kumquat', 'persimmon', 'loquat' ] ); } ); + it( 'should move block to lower index', () => { + const original = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + uid: 'chicken', + name: 'core/test-block', + attributes: {}, + }, { + uid: 'ribs', + name: 'core/test-block', + attributes: {}, + }, { + uid: 'veggies', + name: 'core/test-block', + attributes: {}, + } ], + } ); + const state = editor( original, { + type: 'MOVE_BLOCK_TO_INDEX', + uid: 'ribs', + index: 0 + } ); + + expect( state.present.blockOrder ).toEqual( [ 'ribs', 'chicken', 'veggies', ] ); + } ); + + it( 'should move block to higher index', () => { + const original = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + uid: 'chicken', + name: 'core/test-block', + attributes: {}, + }, { + uid: 'ribs', + name: 'core/test-block', + attributes: {}, + }, { + uid: 'veggies', + name: 'core/test-block', + attributes: {}, + } ], + } ); + const state = editor( original, { + type: 'MOVE_BLOCK_TO_INDEX', + uid: 'ribs', + index: 2 + } ); + + expect( state.present.blockOrder ).toEqual( [ 'chicken', 'veggies', 'ribs', ] ); + } ); + + it( 'should not move block if passed same index', () => { + const original = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + uid: 'chicken', + name: 'core/test-block', + attributes: {}, + }, { + uid: 'ribs', + name: 'core/test-block', + attributes: {}, + }, { + uid: 'veggies', + name: 'core/test-block', + attributes: {}, + } ], + } ); + const state = editor( original, { + type: 'MOVE_BLOCK_TO_INDEX', + uid: 'ribs', + index: 1 + } ); + + expect( state.present.blockOrder ).toEqual( [ 'chicken', 'ribs', 'veggies', ] ); + } ); + describe( 'edits()', () => { it( 'should save newly edited properties', () => { const original = editor( undefined, { diff --git a/package-lock.json b/package-lock.json index 0125e8325c709..55d2a9d0e2b6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2800,6 +2800,22 @@ "integrity": "sha1-44Mx8IRLukm5qctxx3FYWqsbxlo=", "dev": true }, + "disposables": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/disposables/-/disposables-1.0.1.tgz", + "integrity": "sha1-BkcnoltU9QK9griaot+4358bOeM=" + }, + "dnd-core": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-2.5.4.tgz", + "integrity": "sha512-BcI782MfTm3wCxeIS5c7tAutyTwEIANtuu3W6/xkoJRwiqhRXKX3BbGlycUxxyzMsKdvvoavxgrC3EMPFNYL9A==", + "requires": { + "asap": "2.0.6", + "invariant": "2.2.2", + "lodash": "4.17.4", + "redux": "3.7.2" + } + }, "doctrine": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.0.tgz", @@ -9297,6 +9313,49 @@ } } }, + "react-dnd": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-2.5.4.tgz", + "integrity": "sha512-y9YmnusURc+3KPgvhYKvZ9oCucj51MSZWODyaeV0KFU0cquzA7dCD1g/OIYUKtNoZ+MXtacDngkdud2TklMSjw==", + "requires": { + "disposables": "1.0.1", + "dnd-core": "2.5.4", + "hoist-non-react-statics": "2.3.1", + "invariant": "2.2.2", + "lodash": "4.17.4", + "prop-types": "15.5.10" + }, + "dependencies": { + "hoist-non-react-statics": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz", + "integrity": "sha1-ND24TGAYxlB3iJgkATWhQg7iLOA=" + } + } + }, + "react-dnd-html5-backend": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-2.5.4.tgz", + "integrity": "sha512-jDqAkm/hI8Tl4HcsbhkBgB6HgpJR1e+ML1SbfxaegXYiuMxEVQm0FOwEH5WxUoo6fmIG4N+H0rSm59POuZOCaA==", + "requires": { + "lodash": "4.17.4" + } + }, + "react-dnd-html5-mixed-backend": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/react-dnd-html5-mixed-backend/-/react-dnd-html5-mixed-backend-2.0.1.tgz", + "integrity": "sha1-z1oFCA8V6b/qA+iKwP4yb5WNDjE=", + "requires": { + "lodash": "3.10.1" + }, + "dependencies": { + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" + } + } + }, "react-dom": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.2.0.tgz", diff --git a/package.json b/package.json index 58eb251a480e8..0895d0e6b2c8c 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,9 @@ "react-click-outside": "2.3.1", "react-color": "2.13.4", "react-datepicker": "0.61.0", + "react-dnd": "2.5.4", + "react-dnd-html5-backend": "2.5.4", + "react-dnd-html5-mixed-backend": "2.0.1", "react-dom": "16.2.0", "react-redux": "5.0.6", "redux": "3.7.2",