diff --git a/package.json b/package.json index 76ad794..bd446d0 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "private": true, "dependencies": { "glamor": "^2.20.40", + "lodash": "^4.17.11", "prismjs": "^1.15.0", "react": "^15.6.1", "react-animated-number": "^0.4.3", diff --git a/src/actions/index.js b/src/actions/index.js index a43724c..1c10673 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -24,6 +24,13 @@ import { deleteMembershipReq, } from '../api/memberships'; +export const moveTaskInHierarchy = (oldParentGid, newParentGid, taskGid) => ({ + type: 'MOVE_TASK_IN_HIERARCHY', + oldParentGid, + newParentGid, + taskGid +}) + export const updateFormField = (formId, fieldId, value) => ({ type: 'TASK_FIELD_UPDATE', formId, diff --git a/src/api/tasks.js b/src/api/tasks.js index 9fff488..9b4a89c 100644 --- a/src/api/tasks.js +++ b/src/api/tasks.js @@ -77,19 +77,22 @@ const buildTaskHierarchy = (tasks, pursuanceId) => { const taskMap = {}; const rootTaskGids = []; for (let i = 0; i < tasks.length; i++) { - const t = tasks[i]; - taskMap[t.gid] = Object.assign(t, { subtask_gids: [] }); + const t1 = tasks[i]; + taskMap[t1.gid] = Object.assign(t1, { subtask_gids: [] }); + } + + for (let i = 0; i < tasks.length; i++) { + const t2 = tasks[i]; - if (isRootTaskInPursuance(t, pursuanceId)) { - rootTaskGids.push(t.gid); + if (isRootTaskInPursuance(t2, pursuanceId)) { + rootTaskGids.push(t2.gid); } else { // Add t to its parent's subtasks (if its parent is in taskMap) - if (taskMap[t.parent_task_gid]) { - taskMap[t.parent_task_gid].subtask_gids.push(t.gid); + if (taskMap[t2.parent_task_gid]) { + taskMap[t2.parent_task_gid].subtask_gids.push(t2.gid); } else { - console.log( - `Task ${t.gid} ("${t.title}")'s parent ${t.parent_task_gid}` + - ` not found in taskMap` + console.log(`Task ${t2.gid} ("${t2.title}")'s parent ${t2.parent_task_gid}` + + ` not found in taskMap` ); } } diff --git a/src/components/Content/Pursuance/PursuancePage.js b/src/components/Content/Pursuance/PursuancePage.js index 2ecc458..88b047e 100644 --- a/src/components/Content/Pursuance/PursuancePage.js +++ b/src/components/Content/Pursuance/PursuancePage.js @@ -1,6 +1,8 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; +import { DragDropContext } from 'react-dnd'; +import HTML5Backend from 'react-dnd-html5-backend'; import { setCurrentPursuance } from '../../../actions'; import PursuanceMenu from './PursuanceMenu'; import MyTasksView from './views/MyTasksView'; @@ -50,7 +52,7 @@ class PursuancePage extends Component { } } -export default connect(({currentPursuanceId}) => +export default DragDropContext(HTML5Backend)(connect(({currentPursuanceId}) => ({ currentPursuanceId }), { setCurrentPursuance -})(PursuancePage); +})(PursuancePage)); diff --git a/src/components/Content/TaskHierarchy/Task/Task.css b/src/components/Content/TaskHierarchy/Task/Task.css index f140dc7..8f7f773 100644 --- a/src/components/Content/TaskHierarchy/Task/Task.css +++ b/src/components/Content/TaskHierarchy/Task/Task.css @@ -15,6 +15,10 @@ border-color: #fff; } +.highlight-task { + background-color: #50b3fe; +} + .toggle-ctn { padding-right: 6px; } diff --git a/src/components/Content/TaskHierarchy/Task/Task.js b/src/components/Content/TaskHierarchy/Task/Task.js index ed9f5f5..df2fc15 100644 --- a/src/components/Content/TaskHierarchy/Task/Task.js +++ b/src/components/Content/TaskHierarchy/Task/Task.js @@ -1,6 +1,9 @@ import React, { Component } from 'react'; +import { compose } from 'redux'; import { connect } from 'react-redux'; import { withRouter } from 'react-router-dom'; +import { DragSource, DropTarget } from 'react-dnd'; +import _ from 'lodash'; import generateId from '../../../../utils/generateId'; import { showAssignee, isRootTaskInPursuance } from '../../../../utils/tasks'; import { OverlayTrigger, Tooltip } from 'react-bootstrap'; @@ -20,7 +23,8 @@ import { removeTaskFormFromHierarchy, startSuggestions, rpShowTaskDetailsOrCollapse, - patchTask + patchTask, + moveTaskInHierarchy } from '../../../../actions'; // task list uses the default vertical/wide spread confetti @@ -32,6 +36,64 @@ const confettiConfig = { decay: 0.84 }; + +const taskSource = { + beginDrag(props, monitor, component) { + const { taskData } = props; + return taskData; + }, + canDrag(props, monitor) { + const { taskData } = props; + return !!taskData.parent_task_gid; + } +}; + +const taskTarget = { + canDrop: _.debounce((props, monitor) => { + const { taskMap, taskData } = props; + const source = monitor.getItem(); + + if (source) { + // recursively checks if the source is a descendant of the target + const isParent = (map, target, source) => { + if (!target || !target.parent_task_gid) return false; + return (target.gid === source.gid) || isParent(map, map[target.parent_task_gid], source); + } + return !isParent(taskMap, taskData, source); + } + }, 15), + drop(props, monitor, component) { + const { taskData, patchTask, moveTaskInHierarchy } = props; + const { gid, parent_task_gid } = monitor.getItem(); + const oldParent = parent_task_gid; + moveTaskInHierarchy(oldParent, taskData.gid, gid); + + patchTask({ + gid: gid, + parent_task_gid: taskData.gid + }).catch(res => { + const { action: { type } } = res; + if ( type !== 'PATCH_TASK_FULFILLED') { + moveTaskInHierarchy(taskData.gid, oldParent, gid); + } + }); + } +} + +function collectTarget(connect, monitor) { + return { + connectDropTarget: connect.dropTarget(), + canDrop: monitor.canDrop(), + isOver: monitor.isOver() + } +} + +function collect(connect, monitor) { + return { + connectDragSource: connect.dragSource() + }; +} + class RawTask extends Component { constructor(props) { super(props); @@ -186,7 +248,7 @@ class RawTask extends Component { } render() { - const { pursuances, taskData, currentPursuanceId, rightPanel, isInTaskList } = this.props; + const { pursuances, taskData, currentPursuanceId, rightPanel, isInTaskList, connectDragSource, connectDropTarget, canDrop, isOver } = this.props; const { showChildren } = this.state; const task = taskData; if (!task) { @@ -208,55 +270,57 @@ class RawTask extends Component { {this.getTaskIcon(task, showChildren)} )} -