From 7c573b7d8e9fced8b97c86f6c8167a424f10ab4d Mon Sep 17 00:00:00 2001 From: Jen Jones Arnesen Date: Wed, 29 Jan 2020 21:43:37 -0800 Subject: [PATCH] feat: add dnd from dimensions to specific position in layout axis (#575) In order to drag/drop a dimension item from the dimension panel to a specific position in a layout axis, both the dimension panel and the layout components need to share the same dnd library (react-beautiful-dnd "rbd"). * The rbd context has been moved up in the hierarchy to contain both the dimension panel and layout as children, so they can drag/drop to each other * DndContext component sets up the rbd context and handles the drop events * Created custom dnd dimension panel/list/item components to properly configure rbd * DimensionsPanel is simpler, as all the item properties have been moved to the DndDimensionList component * DndDimensionItem sets up a clone item that will stay in the Dimension Panel while the original item is being dragged (due to rbd not supporting this "toolbox" behaviour out-of-the-box) * styles for the dnd dimension list and panel are copied from the shared components in @dhis2/analytics * use css modules Implements [DHIS2-8121] --- packages/app/i18n/en.pot | 3 + packages/app/src/components/App.js | 31 +++-- .../AddToLayoutButton/AddToLayoutButton.js | 2 +- .../DimensionsPanel/DimensionsPanel.js | 71 ++-------- .../DimensionsPanel/DndDimensionItem.js | 76 ++++++++++ .../DimensionsPanel/DndDimensionList.js | 107 ++++++++++++++ .../DimensionsPanel/DndDimensionsPanel.js | 44 ++++++ .../__tests__/DimensionsPanel.spec.js | 9 +- .../styles/DndDimensionItem.module.css | 12 ++ .../styles/DndDimensionList.module.css | 14 ++ .../styles/DndDimensionsPanel.module.css | 8 ++ packages/app/src/components/DndContext.js | 131 ++++++++++++++++++ .../Layout/DefaultLayout/DefaultAxis.js | 64 +-------- packages/app/src/components/Layout/Layout.js | 52 +------ packages/app/src/components/Layout/Menu.js | 13 +- .../app/src/modules/__tests__/layout.spec.js | 35 +++++ packages/app/src/modules/layout.js | 6 +- .../app/src/reducers/__tests__/ui.spec.js | 33 ++++- packages/app/src/reducers/ui.js | 32 +++-- 19 files changed, 529 insertions(+), 214 deletions(-) create mode 100644 packages/app/src/components/DimensionsPanel/DndDimensionItem.js create mode 100644 packages/app/src/components/DimensionsPanel/DndDimensionList.js create mode 100644 packages/app/src/components/DimensionsPanel/DndDimensionsPanel.js create mode 100644 packages/app/src/components/DimensionsPanel/styles/DndDimensionItem.module.css create mode 100644 packages/app/src/components/DimensionsPanel/styles/DndDimensionList.module.css create mode 100644 packages/app/src/components/DimensionsPanel/styles/DndDimensionsPanel.module.css create mode 100644 packages/app/src/components/DndContext.js create mode 100644 packages/app/src/modules/__tests__/layout.spec.js diff --git a/packages/app/i18n/en.pot b/packages/app/i18n/en.pot index 002891bb23..5611df459b 100644 --- a/packages/app/i18n/en.pot +++ b/packages/app/i18n/en.pot @@ -44,6 +44,9 @@ msgid "" "items. Only the first {{maxNumber}} items will be used and saved." msgstr "" +msgid "Search dimensions" +msgstr "" + msgid "Download" msgstr "" diff --git a/packages/app/src/components/App.js b/packages/app/src/components/App.js index c089494293..550b77876e 100644 --- a/packages/app/src/components/App.js +++ b/packages/app/src/components/App.js @@ -3,6 +3,7 @@ import { connect } from 'react-redux' import PropTypes from 'prop-types' import i18n from '@dhis2/d2-i18n' +import DndContext from './DndContext' import Snackbar from '../components/Snackbar/Snackbar' import MenuBar from './MenuBar/MenuBar' import TitleBar from './TitleBar/TitleBar' @@ -169,22 +170,24 @@ export class App extends Component {
-
- -
-
-
- -
-
- + +
+
-
- {this.state.initialLoadIsComplete && ( - - )} +
+
+ +
+
+ +
+
+ {this.state.initialLoadIsComplete && ( + + )} +
-
+
{this.props.ui.rightSidebarOpen && this.props.current && (
{ this.props.onAddDimension({ - [this.props.dialogId]: axisId, + [this.props.dialogId]: { axisId }, }) this.props.onClick() diff --git a/packages/app/src/components/DimensionsPanel/DimensionsPanel.js b/packages/app/src/components/DimensionsPanel/DimensionsPanel.js index 4c911f9dc3..5285205547 100644 --- a/packages/app/src/components/DimensionsPanel/DimensionsPanel.js +++ b/packages/app/src/components/DimensionsPanel/DimensionsPanel.js @@ -1,19 +1,14 @@ import React, { Component } from 'react' import { connect } from 'react-redux' import { - DimensionsPanel, DimensionMenu, - getDisallowedDimensions, - getAllLockedDimensionIds, DIMENSION_ID_ASSIGNED_CATEGORIES, } from '@dhis2/analytics' import PropTypes from 'prop-types' import DialogManager from './Dialogs/DialogManager' -import { SOURCE_DIMENSIONS } from '../../modules/layout' -import { setDataTransfer } from '../../modules/dnd' +import DndDimensionsPanel from './DndDimensionsPanel' import * as fromReducers from '../../reducers' -import * as fromActions from '../../actions' import { styles } from './styles/DimensionsPanel.style' import { AXIS_SETUP_DIALOG_ID } from '../AxisSetup/AxisSetup' @@ -22,8 +17,6 @@ import { acAddUiLayoutDimensions, acRemoveUiLayoutDimensions, } from '../../actions/ui' -import { sGetUiType } from '../../reducers/ui' -import { createSelector } from 'reselect' export class Dimensions extends Component { state = { @@ -31,50 +24,29 @@ export class Dimensions extends Component { dimensionId: null, } - onDimensionOptionsClick = (event, id) => { + openOptionsMenuForDimension = (event, id) => { event.stopPropagation() - // set anchor for options menu - // open menu this.setState({ dimensionMenuAnchorEl: event.currentTarget, dimensionId: id, }) } - onDimensionOptionsClose = () => + closeOptionsMenuForDimension = () => this.setState({ dimensionMenuAnchorEl: null, dimensionId: null, }) - onDimensionDragStart = e => { - setDataTransfer(e, SOURCE_DIMENSIONS) - } - - disabledDimension = dimensionId => - this.props.disallowedDimensions.includes(dimensionId) - - lockedDimension = dimensionId => - this.props.lockedDimensions.includes(dimensionId) - getNumberOfDimensionItems = () => (this.props.itemsByDimension[this.state.dimensionId] || []).length render() { return (
- - this.props.recommendedIds.includes(dimensionId) - } - onDimensionOptionsClick={this.onDimensionOptionsClick} - onDimensionDragStart={this.onDimensionDragStart} - onDimensionClick={this.props.onDimensionClick} +
@@ -104,40 +76,22 @@ export class Dimensions extends Component { } } -const getDisallowedDimensionsMemo = createSelector([sGetUiType], type => - getDisallowedDimensions(type) -) - -const getLockedDimensionsMemo = createSelector([sGetUiType], type => - getAllLockedDimensionIds(type) -) - Dimensions.propTypes = { assignedCategoriesItemHandler: PropTypes.func, axisItemHandler: PropTypes.func, - dimensions: PropTypes.object, - disallowedDimensions: PropTypes.array, dualAxisItemHandler: PropTypes.func, getCurrentAxisId: PropTypes.func, itemsByDimension: PropTypes.object, layoutHasAssignedCategories: PropTypes.bool, - lockedDimensions: PropTypes.array, - recommendedIds: PropTypes.array, removeItemHandler: PropTypes.func, - selectedIds: PropTypes.array, ui: PropTypes.object, - onDimensionClick: PropTypes.func, } const mapStateToProps = state => ({ ui: fromReducers.fromUi.sGetUi(state), dimensions: fromReducers.fromDimensions.sGetDimensions(state), - selectedIds: fromReducers.fromUi.sGetDimensionIdsFromLayout(state), - recommendedIds: fromReducers.fromRecommendedIds.sGetRecommendedIds(state), layout: fromReducers.fromUi.sGetUiLayout(state), itemsByDimension: fromReducers.fromUi.sGetUiItems(state), - disallowedDimensions: getDisallowedDimensionsMemo(state), - lockedDimensions: getLockedDimensionsMemo(state), layoutHasAssignedCategories: fromReducers.fromUi.sLayoutHasAssignedCategories( state ), @@ -146,12 +100,10 @@ const mapStateToProps = state => ({ }) const mapDispatchToProps = dispatch => ({ - onDimensionClick: id => - dispatch(fromActions.fromUi.acSetUiActiveModalDialog(id)), dualAxisItemHandler: () => dispatch(acSetUiActiveModalDialog(AXIS_SETUP_DIALOG_ID)), - axisItemHandler: (dimensionId, targetAxisId, numberOfDimensionItems) => { - dispatch(acAddUiLayoutDimensions({ [dimensionId]: targetAxisId })) + axisItemHandler: (dimensionId, axisId, numberOfDimensionItems) => { + dispatch(acAddUiLayoutDimensions({ [dimensionId]: { axisId } })) if (numberOfDimensionItems > 0) { dispatch(acSetUiActiveModalDialog(dimensionId)) @@ -160,15 +112,12 @@ const mapDispatchToProps = dispatch => ({ removeItemHandler: dimensionId => { dispatch(acRemoveUiLayoutDimensions(dimensionId)) }, - assignedCategoriesItemHandler: ( - layoutHasAssignedCategories, - destination - ) => { + assignedCategoriesItemHandler: (layoutHasAssignedCategories, axisId) => { dispatch( layoutHasAssignedCategories ? acRemoveUiLayoutDimensions(DIMENSION_ID_ASSIGNED_CATEGORIES) : acAddUiLayoutDimensions({ - [DIMENSION_ID_ASSIGNED_CATEGORIES]: destination, + [DIMENSION_ID_ASSIGNED_CATEGORIES]: { axisId }, }) ) }, diff --git a/packages/app/src/components/DimensionsPanel/DndDimensionItem.js b/packages/app/src/components/DimensionsPanel/DndDimensionItem.js new file mode 100644 index 0000000000..621533e07b --- /dev/null +++ b/packages/app/src/components/DimensionsPanel/DndDimensionItem.js @@ -0,0 +1,76 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import { Draggable } from 'react-beautiful-dnd' +import { DimensionItem } from '@dhis2/analytics' + +import styles from './styles/DndDimensionItem.module.css' + +export class DndDimensionItem extends Component { + render = () => { + const { + id, + index, + name, + isSelected, + isLocked, + isDeactivated, + isRecommended, + onClick, + onOptionsClick, + } = this.props + + const itemCommonProps = { + name, + isSelected, + isLocked, + isDeactivated, + isRecommended, + } + + return ( + + {(provided, snapshot) => ( + <> + + {snapshot.isDragging && ( + + )} + + )} + + ) + } +} + +DndDimensionItem.propTypes = { + id: PropTypes.string, + index: PropTypes.number, + isDeactivated: PropTypes.bool, + isLocked: PropTypes.bool, + isRecommended: PropTypes.bool, + isSelected: PropTypes.bool, + name: PropTypes.string, + onClick: PropTypes.func, + onOptionsClick: PropTypes.func, +} + +export default DndDimensionItem diff --git a/packages/app/src/components/DimensionsPanel/DndDimensionList.js b/packages/app/src/components/DimensionsPanel/DndDimensionList.js new file mode 100644 index 0000000000..4ace8a0674 --- /dev/null +++ b/packages/app/src/components/DimensionsPanel/DndDimensionList.js @@ -0,0 +1,107 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import { Droppable } from 'react-beautiful-dnd' +import { createSelector } from 'reselect' +import { + getDisallowedDimensions, + getAllLockedDimensionIds, +} from '@dhis2/analytics' + +import DndDimensionItem from './DndDimensionItem' +import * as fromReducers from '../../reducers' +import { acSetUiActiveModalDialog } from '../../actions/ui' +import { SOURCE_DIMENSIONS } from '../../modules/layout' + +import styles from './styles/DndDimensionList.module.css' + +export class DndDimensionList extends Component { + nameContainsFilterText = dimension => + dimension.name + .toLowerCase() + .includes(this.props.filterText.toLowerCase()) + + isSelected = id => this.props.selectedIds.includes(id) + isDisabledDimension = id => this.props.disallowedDimensions.includes(id) + isLockedDimension = id => this.props.lockedDimensions.includes(id) + isRecommendedDimension = id => this.props.recommendedIds.includes(id) + + renderItem = ({ id, name }, index) => { + const itemProps = { + id, + name, + index, + isSelected: this.isSelected(id), + isLocked: this.isLockedDimension(id), + isDeactivated: this.isDisabledDimension(id), + isRecommended: this.isRecommendedDimension(id), + onClick: this.props.onDimensionClick, + onOptionsClick: this.props.onDimensionOptionsClick, + } + + return ( + + ) + } + + render() { + const dimensionsList = this.props.dimensions + .filter(this.nameContainsFilterText) + .map(this.renderItem) + + return ( + + {provided => ( +
+
    {dimensionsList}
+ {provided.placeholder} +
+ )} +
+ ) + } +} + +DndDimensionList.propTypes = { + dimensions: PropTypes.array, + disallowedDimensions: PropTypes.array, + filterText: PropTypes.string, + lockedDimensions: PropTypes.array, + recommendedIds: PropTypes.array, + selectedIds: PropTypes.array, + onDimensionClick: PropTypes.func, + onDimensionOptionsClick: PropTypes.func, +} + +const getDisallowedDimensionsMemo = createSelector( + [fromReducers.fromUi.sGetUiType], + type => getDisallowedDimensions(type) +) + +const getisLockedDimensionsMemo = createSelector( + [fromReducers.fromUi.sGetUiType], + type => getAllLockedDimensionIds(type) +) + +const mapStateToProps = state => ({ + dimensions: Object.values( + fromReducers.fromDimensions.sGetDimensions(state) + ), + selectedIds: fromReducers.fromUi.sGetDimensionIdsFromLayout(state), + recommendedIds: fromReducers.fromRecommendedIds.sGetRecommendedIds(state), + disallowedDimensions: getDisallowedDimensionsMemo(state), + lockedDimensions: getisLockedDimensionsMemo(state), +}) + +const mapDispatchToProps = dispatch => ({ + onDimensionClick: id => dispatch(acSetUiActiveModalDialog(id)), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(DndDimensionList) diff --git a/packages/app/src/components/DimensionsPanel/DndDimensionsPanel.js b/packages/app/src/components/DimensionsPanel/DndDimensionsPanel.js new file mode 100644 index 0000000000..b307732508 --- /dev/null +++ b/packages/app/src/components/DimensionsPanel/DndDimensionsPanel.js @@ -0,0 +1,44 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import i18n from '@dhis2/d2-i18n' +import { DimensionFilter } from '@dhis2/analytics' + +import DndDimensionList from './DndDimensionList' + +import styles from './styles/DndDimensionsPanel.module.css' + +export class DndDimensionsPanel extends Component { + state = { filterText: '' } + + onClearFilter = () => { + this.setState({ filterText: '' }) + } + + onFilterTextChange = filterText => { + this.setState({ filterText }) + } + + render() { + return ( +
+ + +
+ ) + } +} + +DndDimensionsPanel.propTypes = { + onDimensionOptionsClick: PropTypes.func, +} + +export default DndDimensionsPanel diff --git a/packages/app/src/components/DimensionsPanel/__tests__/DimensionsPanel.spec.js b/packages/app/src/components/DimensionsPanel/__tests__/DimensionsPanel.spec.js index c46c3e4626..ea003e62a4 100644 --- a/packages/app/src/components/DimensionsPanel/__tests__/DimensionsPanel.spec.js +++ b/packages/app/src/components/DimensionsPanel/__tests__/DimensionsPanel.spec.js @@ -1,10 +1,10 @@ import React from 'react' import { shallow } from 'enzyme' -import { DimensionsPanel } from '@dhis2/analytics' +import { DndDimensionsPanel } from '../DndDimensionsPanel' import { Dimensions } from '../DimensionsPanel' -describe('The Dimensions component ', () => { +describe('Dimensions component ', () => { let shallowDimensions let props const dimensionsComponent = () => { @@ -17,7 +17,6 @@ describe('The Dimensions component ', () => { beforeEach(() => { shallowDimensions = undefined props = { - dimensions: {}, ui: { layout: {}, type: '', @@ -42,9 +41,9 @@ describe('The Dimensions component ', () => { expect(wrappingDiv.children()).toEqual(dimensionsComponent().children()) }) - it('renders a DimensionsPanel with the correct prop', () => { + it('renders a DndDimensionsPanel', () => { const dimensionsComp = dimensionsComponent() - expect(dimensionsComp.find(DimensionsPanel).length).toEqual(1) + expect(dimensionsComp.find(DndDimensionsPanel).length).toEqual(1) }) }) diff --git a/packages/app/src/components/DimensionsPanel/styles/DndDimensionItem.module.css b/packages/app/src/components/DimensionsPanel/styles/DndDimensionItem.module.css new file mode 100644 index 0000000000..0ae984b5f2 --- /dev/null +++ b/packages/app/src/components/DimensionsPanel/styles/DndDimensionItem.module.css @@ -0,0 +1,12 @@ +/* +This keeps the dimension item clone from causing +a ripple effect in the Dimension list during drag +*/ +.dimensionItemClone ~ li { + transform: none !important; +} + +.dragging { + background-color: #e8edf2; + border-radius: 2px; +} diff --git a/packages/app/src/components/DimensionsPanel/styles/DndDimensionList.module.css b/packages/app/src/components/DimensionsPanel/styles/DndDimensionList.module.css new file mode 100644 index 0000000000..f1b96bba81 --- /dev/null +++ b/packages/app/src/components/DimensionsPanel/styles/DndDimensionList.module.css @@ -0,0 +1,14 @@ +.container { + position: relative; + flex: 1 1 0%; + min-height: 30vh; +} + +.list { + position: absolute; + width: 100%; + height: 100%; + overflow: auto; + margin-top: 0px; + padding: 0; +} diff --git a/packages/app/src/components/DimensionsPanel/styles/DndDimensionsPanel.module.css b/packages/app/src/components/DimensionsPanel/styles/DndDimensionsPanel.module.css new file mode 100644 index 0000000000..1f44e75119 --- /dev/null +++ b/packages/app/src/components/DimensionsPanel/styles/DndDimensionsPanel.module.css @@ -0,0 +1,8 @@ +.container { + height: 100%; + display: flex; + flex-direction: column; + background-color: #f4f6f8; + padding: 8px; + overflow: hidden; +} diff --git a/packages/app/src/components/DndContext.js b/packages/app/src/components/DndContext.js new file mode 100644 index 0000000000..b4838d9cb8 --- /dev/null +++ b/packages/app/src/components/DndContext.js @@ -0,0 +1,131 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' +import PropTypes from 'prop-types' +import { DragDropContext } from 'react-beautiful-dnd' +import { + canDimensionBeAddedToAxis, + getPredefinedDimensionProp, + DIMENSION_PROP_NO_ITEMS, +} from '@dhis2/analytics' + +import { sGetUiLayout, sGetUiItems, sGetUiType } from '../reducers/ui' +import { + acAddUiLayoutDimensions, + acSetUiActiveModalDialog, + acSetUiLayout, +} from '../actions/ui' +import { SOURCE_DIMENSIONS } from '../modules/layout' + +class DndContext extends Component { + rearrangeLayoutDimensions = ({ + sourceAxisId, + sourceIndex, + destinationAxisId, + destinationIndex, + }) => { + const layout = this.props.layout + + const sourceList = Array.from(layout[sourceAxisId]) + const [moved] = sourceList.splice(sourceIndex, 1) + + if (sourceAxisId === destinationAxisId) { + sourceList.splice(destinationIndex, 0, moved) + + this.props.onReorderDimensions({ + ...layout, + [sourceAxisId]: sourceList, + }) + } else { + if ( + canDimensionBeAddedToAxis( + this.props.type, + layout[destinationAxisId], + destinationAxisId + ) + ) { + this.props.onAddDimensions({ + [moved]: { + axisId: destinationAxisId, + index: destinationIndex, + }, + }) + } + } + } + + addDimensionToLayout = ({ axisId, index, dimensionId }) => { + const { layout, type } = this.props + + if (!canDimensionBeAddedToAxis(type, layout[axisId], axisId)) { + return + } + + this.props.onAddDimensions({ [dimensionId]: { axisId, index } }) + + const items = this.props.itemsByDimension[dimensionId] + const hasNoItems = Boolean(!items || !items.length) + + if ( + hasNoItems && + !getPredefinedDimensionProp(dimensionId, DIMENSION_PROP_NO_ITEMS) + ) { + this.props.onDropWithoutItems(dimensionId) + } + } + + onDragEnd = result => { + const { source, destination, draggableId } = result + + if (!destination) { + return + } + + if (source.droppableId === SOURCE_DIMENSIONS) { + this.addDimensionToLayout({ + axisId: destination.droppableId, + index: destination.index, + dimensionId: draggableId, + }) + } else { + this.rearrangeLayoutDimensions({ + sourceAxisId: source.droppableId, + sourceIndex: source.index, + destinationAxisId: destination.droppableId, + destinationIndex: destination.index, + }) + } + } + + render() { + return ( + + {this.props.children} + + ) + } +} + +DndContext.propTypes = { + children: PropTypes.node, + itemsByDimension: PropTypes.object, + layout: PropTypes.object, + type: PropTypes.string, + onAddDimensions: PropTypes.func, + onDropWithoutItems: PropTypes.func, + onReorderDimensions: PropTypes.func, +} + +const mapStateToProps = state => ({ + layout: sGetUiLayout(state), + type: sGetUiType(state), + itemsByDimension: sGetUiItems(state), +}) + +const mapDispatchToProps = dispatch => ({ + onAddDimensions: map => dispatch(acAddUiLayoutDimensions(map)), + onDropWithoutItems: dimensionId => + dispatch(acSetUiActiveModalDialog(dimensionId)), + onReorderDimensions: layout => dispatch(acSetUiLayout(layout)), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(DndContext) diff --git a/packages/app/src/components/Layout/DefaultLayout/DefaultAxis.js b/packages/app/src/components/Layout/DefaultLayout/DefaultAxis.js index 3cb7b8b1f0..ec86646ddb 100644 --- a/packages/app/src/components/Layout/DefaultLayout/DefaultAxis.js +++ b/packages/app/src/components/Layout/DefaultLayout/DefaultAxis.js @@ -2,28 +2,12 @@ import React from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' import { Droppable, Draggable } from 'react-beautiful-dnd' -import { - getAxisName, - isDimensionLocked, - canDimensionBeAddedToAxis, - getPredefinedDimensionProp, - DIMENSION_PROP_NO_ITEMS, -} from '@dhis2/analytics' +import { getAxisName, isDimensionLocked } from '@dhis2/analytics' import { withStyles } from '@material-ui/core' import Chip from '../Chip' -import { - sGetUi, - sGetUiLayout, - sGetUiItems, - sGetUiType, -} from '../../../reducers/ui' -import { decodeDataTransfer } from '../../../modules/dnd' -import { - acAddUiLayoutDimensions, - acSetUiActiveModalDialog, -} from '../../../actions/ui' -import { SOURCE_DIMENSIONS } from '../../../modules/layout' +import { sGetUi, sGetUiLayout, sGetUiType } from '../../../reducers/ui' +import { acSetUiActiveModalDialog } from '../../../actions/ui' import styles from './styles/DefaultAxis.style' class Axis extends React.Component { @@ -31,40 +15,6 @@ class Axis extends React.Component { e.preventDefault() } - onDrop = e => { - e.preventDefault() - - const { - type, - layout, - axisId, - itemsByDimension, - onAddDimension, - onDropWithoutItems, - } = this.props - const { dimensionId, source } = decodeDataTransfer(e) - - if (canDimensionBeAddedToAxis(type, layout[axisId], axisId)) { - onAddDimension({ - [dimensionId]: axisId, - }) - - const items = itemsByDimension[dimensionId] - const hasNoItems = Boolean(!items || !items.length) - - if ( - source === SOURCE_DIMENSIONS && - hasNoItems && - !getPredefinedDimensionProp( - dimensionId, - DIMENSION_PROP_NO_ITEMS - ) - ) { - onDropWithoutItems(dimensionId) - } - } - } - render() { const { axisId, axis, style, type, getOpenHandler } = this.props @@ -73,7 +23,6 @@ class Axis extends React.Component { id={axisId} style={{ ...styles.axisContainer, ...style }} onDragOver={this.onDragOver} - onDrop={this.onDrop} >
{this.props.label || getAxisName(axisId)} @@ -132,14 +81,11 @@ Axis.propTypes = { getMoveHandler: PropTypes.func, getOpenHandler: PropTypes.func, getRemoveHandler: PropTypes.func, - itemsByDimension: PropTypes.object, label: PropTypes.string, layout: PropTypes.object, style: PropTypes.object, type: PropTypes.string, ui: PropTypes.object, - onAddDimension: PropTypes.func, - onDropWithoutItems: PropTypes.func, onOpenAxisSetup: PropTypes.func, } @@ -147,13 +93,9 @@ const mapStateToProps = state => ({ ui: sGetUi(state), type: sGetUiType(state), layout: sGetUiLayout(state), - itemsByDimension: sGetUiItems(state), }) const mapDispatchToProps = dispatch => ({ - onAddDimension: map => dispatch(acAddUiLayoutDimensions(map)), - onDropWithoutItems: dimensionId => - dispatch(acSetUiActiveModalDialog(dimensionId)), getOpenHandler: dimensionId => () => dispatch(acSetUiActiveModalDialog(dimensionId)), }) diff --git a/packages/app/src/components/Layout/Layout.js b/packages/app/src/components/Layout/Layout.js index 9a15005bfb..e901ab9f28 100644 --- a/packages/app/src/components/Layout/Layout.js +++ b/packages/app/src/components/Layout/Layout.js @@ -1,22 +1,19 @@ import React from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' -import { DragDropContext } from 'react-beautiful-dnd' import { LAYOUT_TYPE_DEFAULT, LAYOUT_TYPE_PIE, LAYOUT_TYPE_YEAR_OVER_YEAR, LAYOUT_TYPE_PIVOT_TABLE, getLayoutTypeByVisType, - canDimensionBeAddedToAxis, } from '@dhis2/analytics' import DefaultLayout from './DefaultLayout/DefaultLayout' import YearOverYearLayout from './YearOverYearLayout/YearOverYearLayout' import PieLayout from './PieLayout/PieLayout' +import { sGetUiType } from '../../reducers/ui' import PivotTableLayout from './PivotTableLayout/PivotTableLayout' -import { sGetUiLayout, sGetUiType } from '../../reducers/ui' -import { acAddUiLayoutDimensions, acSetUiLayout } from '../../actions/ui' const componentMap = { [LAYOUT_TYPE_DEFAULT]: DefaultLayout, @@ -25,60 +22,19 @@ const componentMap = { [LAYOUT_TYPE_PIVOT_TABLE]: PivotTableLayout, } -const Layout = props => { - const { visType, layout } = props - +const Layout = ({ visType }) => { const layoutType = getLayoutTypeByVisType(visType) const LayoutComponent = componentMap[layoutType] - const onDragEnd = result => { - const { source, destination } = result - - if (!destination) { - return - } - - const sourceList = Array.from(layout[source.droppableId]) - const [moved] = sourceList.splice(source.index, 1) - - if (source.droppableId === destination.droppableId) { - sourceList.splice(destination.index, 0, moved) - - props.onReorderDimensions({ - ...layout, - [source.droppableId]: sourceList, - }) - } else { - const axisId = destination.droppableId - - if (canDimensionBeAddedToAxis(visType, layout[axisId], axisId)) { - props.onAddDimensions({ [moved]: destination.droppableId }) - } - } - } - - return ( - - - - ) + return } Layout.propTypes = { - layout: PropTypes.object, visType: PropTypes.string, - onAddDimensions: PropTypes.func, - onReorderDimensions: PropTypes.func, } const mapStateToProps = state => ({ - layout: sGetUiLayout(state), visType: sGetUiType(state), }) -const mapDispatchToProps = dispatch => ({ - onReorderDimensions: layout => dispatch(acSetUiLayout(layout)), - onAddDimensions: map => dispatch(acAddUiLayoutDimensions(map)), -}) - -export default connect(mapStateToProps, mapDispatchToProps)(Layout) +export default connect(mapStateToProps, null)(Layout) diff --git a/packages/app/src/components/Layout/Menu.js b/packages/app/src/components/Layout/Menu.js index d92e142ba7..1151c71d69 100644 --- a/packages/app/src/components/Layout/Menu.js +++ b/packages/app/src/components/Layout/Menu.js @@ -94,21 +94,20 @@ const mapStateToProps = state => { const mapDispatchToProps = dispatch => ({ dualAxisItemHandler: () => dispatch(acSetUiActiveModalDialog(AXIS_SETUP_DIALOG_ID)), - axisItemHandler: (dimensionId, targetAxisId) => { - dispatch(acAddUiLayoutDimensions({ [dimensionId]: targetAxisId })) + axisItemHandler: (dimensionId, axisId) => { + dispatch(acAddUiLayoutDimensions({ [dimensionId]: { axisId } })) }, removeItemHandler: dimensionId => { dispatch(acRemoveUiLayoutDimensions(dimensionId)) }, - assignedCategoriesItemHandler: ( - layoutHasAssignedCategories, - destination - ) => { + assignedCategoriesItemHandler: (layoutHasAssignedCategories, axisId) => { dispatch( layoutHasAssignedCategories ? acRemoveUiLayoutDimensions(DIMENSION_ID_ASSIGNED_CATEGORIES) : acAddUiLayoutDimensions({ - [DIMENSION_ID_ASSIGNED_CATEGORIES]: destination, + [DIMENSION_ID_ASSIGNED_CATEGORIES]: { + axisId, + }, }) ) }, diff --git a/packages/app/src/modules/__tests__/layout.spec.js b/packages/app/src/modules/__tests__/layout.spec.js new file mode 100644 index 0000000000..2f59b5c06c --- /dev/null +++ b/packages/app/src/modules/__tests__/layout.spec.js @@ -0,0 +1,35 @@ +import { getRetransfer } from '../layout' + +const layout = { + columns: ['dx'], + rows: ['pe'], + filters: ['ou', 'abc'], +} + +describe('layout', () => { + describe('getRetransfer', () => { + it('transfers "pe" to filters axis when new dimension added', () => { + const transfer = { + xyz: { axisId: 'rows' }, + } + + expect(getRetransfer(layout, transfer, 'COLUMN')).toMatchObject({ + pe: { + axisId: 'filters', + }, + }) + }) + + it('transfers "pe" to filters axis when filter dimension moved to rows', () => { + const transfer = { + abc: { axisId: 'rows' }, + } + + expect(getRetransfer(layout, transfer, 'COLUMN')).toMatchObject({ + pe: { + axisId: 'filters', + }, + }) + }) + }) +}) diff --git a/packages/app/src/modules/layout.js b/packages/app/src/modules/layout.js index b8cbafcd08..b757b20e26 100644 --- a/packages/app/src/modules/layout.js +++ b/packages/app/src/modules/layout.js @@ -66,7 +66,7 @@ export const getRetransfer = (layout, transfer, visType) => { dimensionIds.forEach(id => { const sourceAxis = inverseLayout[id] || null - const destinationAxisId = transfer[id] + const destinationAxisId = transfer[id].axisId const dimensionsAtDestination = layout[destinationAxisId] || [] if ( @@ -83,11 +83,13 @@ export const getRetransfer = (layout, transfer, visType) => { ) if (transferableDimension) { - retransfer[transferableDimension] = sourceAxis + const axisId = sourceAxis ? sourceAxis : getAvailableAxes(visType).find( axis => !getAxisMaxNumberOfDimensions(visType, axis) ) + + retransfer[transferableDimension] = { axisId } } } }) diff --git a/packages/app/src/reducers/__tests__/ui.spec.js b/packages/app/src/reducers/__tests__/ui.spec.js index a9c710390b..8de43da030 100644 --- a/packages/app/src/reducers/__tests__/ui.spec.js +++ b/packages/app/src/reducers/__tests__/ui.spec.js @@ -165,7 +165,7 @@ describe('reducer: ui', () => { const actualState = reducer(state, { type: ui.ADD_UI_LAYOUT_DIMENSIONS, value: { - [DIMENSION_ID_DATA]: AXIS_ID_ROWS, + [DIMENSION_ID_DATA]: { axisId: AXIS_ID_ROWS }, }, }) @@ -194,7 +194,7 @@ describe('reducer: ui', () => { const actualState = reducer(state, { type: ui.ADD_UI_LAYOUT_DIMENSIONS, value: { - [otherId]: AXIS_ID_COLUMNS, + [otherId]: { axisId: AXIS_ID_COLUMNS }, }, }) @@ -210,6 +210,35 @@ describe('reducer: ui', () => { expect(actualState).toEqual(expectedState) }) + it(`${ui.ADD_UI_LAYOUT_DIMENSIONS}: should add layout dimensions at desired position`, () => { + const state = { + type: VIS_TYPE_COLUMN, + layout: { + columns: [DIMENSION_ID_DATA], + rows: [DIMENSION_ID_PERIOD], + filters: [DIMENSION_ID_ORGUNIT], + }, + } + + const actualState = reducer(state, { + type: ui.ADD_UI_LAYOUT_DIMENSIONS, + value: { + [otherId]: { axisId: AXIS_ID_FILTERS, index: 0 }, + }, + }) + + const expectedState = { + ...state, + layout: { + columns: [DIMENSION_ID_DATA], + rows: [DIMENSION_ID_PERIOD], + filters: [otherId, DIMENSION_ID_ORGUNIT], + }, + } + + expect(actualState).toEqual(expectedState) + }) + it(`${ui.REMOVE_UI_LAYOUT_DIMENSIONS}: should remove a single dimension`, () => { const state = { layout: { diff --git a/packages/app/src/reducers/ui.js b/packages/app/src/reducers/ui.js index b7425baf7a..4a9721c5b7 100644 --- a/packages/app/src/reducers/ui.js +++ b/packages/app/src/reducers/ui.js @@ -105,20 +105,26 @@ export default (state = DEFAULT_UI, action) => { let newLayout = state.layout // Add dimension ids to destination (axisId === null means remove from layout) - Object.entries(transfers).forEach(([dimensionId, axisId]) => { - if ( - newLayout[axisId] && - canDimensionBeAddedToAxis( - state.type, - newLayout[axisId], - axisId - ) - ) { - // Filter out transferred dimension id (remove from source) - newLayout = getFilteredLayout(newLayout, [dimensionId]) - newLayout[axisId].push(dimensionId) + Object.entries(transfers).forEach( + ([dimensionId, { axisId, index }]) => { + if ( + newLayout[axisId] && + canDimensionBeAddedToAxis( + state.type, + newLayout[axisId], + axisId + ) + ) { + // Filter out transferred dimension id (remove from source) + newLayout = getFilteredLayout(newLayout, [dimensionId]) + if (index === null || index === undefined) { + newLayout[axisId].push(dimensionId) + } else { + newLayout[axisId].splice(index, 0, dimensionId) + } + } } - }) + ) return { ...state,