Skip to content

Commit

Permalink
feat: add dnd from dimensions to specific position in layout axis (#575)
Browse files Browse the repository at this point in the history
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]
  • Loading branch information
jenniferarnesen committed Jan 30, 2020
1 parent 3d249a6 commit 7c573b7
Show file tree
Hide file tree
Showing 19 changed files with 529 additions and 214 deletions.
3 changes: 3 additions & 0 deletions packages/app/i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ msgid ""
"items. Only the first {{maxNumber}} items will be used and saved."
msgstr ""

msgid "Search dimensions"
msgstr ""

msgid "Download"
msgstr ""

Expand Down
31 changes: 17 additions & 14 deletions packages/app/src/components/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -169,22 +170,24 @@ export class App extends Component {
</div>
</div>
<div className="section-main flex-grow-1 flex-ct">
<div className="main-left">
<DimensionsPanel />
</div>
<div className="main-center flex-grow-1 flex-basis-0 flex-ct flex-dir-col">
<div className="main-center-layout">
<Layout />
</div>
<div className="main-center-titlebar">
<TitleBar />
<DndContext>
<div className="main-left">
<DimensionsPanel />
</div>
<div className="main-center-canvas flex-grow-1">
{this.state.initialLoadIsComplete && (
<Visualization />
)}
<div className="main-center flex-grow-1 flex-basis-0 flex-ct flex-dir-col">
<div className="main-center-layout">
<Layout />
</div>
<div className="main-center-titlebar">
<TitleBar />
</div>
<div className="main-center-canvas flex-grow-1">
{this.state.initialLoadIsComplete && (
<Visualization />
)}
</div>
</div>
</div>
</DndContext>
{this.props.ui.rightSidebarOpen && this.props.current && (
<div className="main-right">
<Interpretations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class AddToLayoutButton extends Component {

onUpdate = axisId => {
this.props.onAddDimension({
[this.props.dialogId]: axisId,
[this.props.dialogId]: { axisId },
})

this.props.onClick()
Expand Down
71 changes: 10 additions & 61 deletions packages/app/src/components/DimensionsPanel/DimensionsPanel.js
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -22,59 +17,36 @@ import {
acAddUiLayoutDimensions,
acRemoveUiLayoutDimensions,
} from '../../actions/ui'
import { sGetUiType } from '../../reducers/ui'
import { createSelector } from 'reselect'

export class Dimensions extends Component {
state = {
dimensionMenuAnchorEl: null,
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 (
<div style={styles.divContainer}>
<DimensionsPanel
dimensions={Object.values(this.props.dimensions)}
selectedIds={this.props.selectedIds}
disabledDimension={this.disabledDimension}
lockedDimension={this.lockedDimension}
recommendedDimension={dimensionId =>
this.props.recommendedIds.includes(dimensionId)
}
onDimensionOptionsClick={this.onDimensionOptionsClick}
onDimensionDragStart={this.onDimensionDragStart}
onDimensionClick={this.props.onDimensionClick}
<DndDimensionsPanel
onDimensionOptionsClick={this.openOptionsMenuForDimension}
/>
<DimensionMenu
dimensionId={this.state.dimensionId}
Expand All @@ -96,48 +68,30 @@ export class Dimensions extends Component {
axisItemHandler={this.props.axisItemHandler}
removeItemHandler={this.props.removeItemHandler}
anchorEl={this.state.dimensionMenuAnchorEl}
onClose={this.onDimensionOptionsClose}
onClose={this.closeOptionsMenuForDimension}
/>
<DialogManager />
</div>
)
}
}

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
),
Expand All @@ -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))
Expand All @@ -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 },
})
)
},
Expand Down
76 changes: 76 additions & 0 deletions packages/app/src/components/DimensionsPanel/DndDimensionItem.js
Original file line number Diff line number Diff line change
@@ -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 (
<Draggable
draggableId={id}
index={index}
isDragDisabled={isSelected || isDeactivated || isLocked}
>
{(provided, snapshot) => (
<>
<DimensionItem
innerRef={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
id={id}
className={
snapshot.isDragging ? styles.dragging : null
}
onClick={onClick}
onOptionsClick={onOptionsClick}
{...itemCommonProps}
/>
{snapshot.isDragging && (
<DimensionItem
id={`dimension-item-clone-${id}`}
className={styles.dimensionItemClone}
{...itemCommonProps}
/>
)}
</>
)}
</Draggable>
)
}
}

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
Loading

0 comments on commit 7c573b7

Please sign in to comment.