-
Notifications
You must be signed in to change notification settings - Fork 14
add task reassignment functionality #195
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
1708e14
a981977
c1e3c07
8213591
8c4b268
2d5b3bf
556ba52
aa1173c
d810790
3e50e22
2faad7e
744c64c
3f0d3b4
3db4550
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,8 @@ | ||
| 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 generateId from '../../../../utils/generateId'; | ||
| import { showAssignee, isRootTaskInPursuance } from '../../../../utils/tasks'; | ||
| import { OverlayTrigger, Tooltip } from 'react-bootstrap'; | ||
|
|
@@ -19,9 +21,65 @@ import { | |
| removeTaskFormFromHierarchy, | ||
| startSuggestions, | ||
| rpShowTaskDetailsOrCollapse, | ||
| patchTask | ||
| patchTask, | ||
| moveTask | ||
| } from '../../../../actions'; | ||
|
|
||
| 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(props, monitor) { | ||
| const { taskMap, taskData } = props; | ||
| const source = monitor.getItem(); | ||
| // recursively checks if the source is a descendant of the target | ||
| const isParent = (map, target, source) => { | ||
| if (!target.parent_task_gid) { | ||
| return false; | ||
| } | ||
| return (target.gid === source.gid) || isParent(map, map[target.parent_task_gid], source); | ||
| } | ||
| return !isParent(taskMap, taskData, source); | ||
| }, | ||
| drop(props, monitor, component) { | ||
| const { taskData, patchTask, moveTask } = props; | ||
| const { gid, parent_task_gid } = monitor.getItem(); | ||
| const oldParent = parent_task_gid; | ||
| moveTask(oldParent, taskData.gid, gid) | ||
| patchTask({ | ||
| gid: gid, | ||
| parent_task_gid: taskData.gid | ||
| }).catch(res => { | ||
| const { action: { type } } = res; | ||
| if ( type !== 'PATCH_TASK_FULFILLED') { | ||
| moveTask(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); | ||
|
|
@@ -155,7 +213,7 @@ class RawTask extends Component { | |
| } | ||
|
|
||
| render() { | ||
| const { pursuances, taskData, currentPursuanceId } = this.props; | ||
| const { pursuances, taskData, currentPursuanceId, connectDragSource, connectDropTarget, canDrop, isOver } = this.props; | ||
| const { showChildren } = this.state; | ||
| const task = taskData; | ||
| const { placeholder, assignedTo } = | ||
|
|
@@ -167,7 +225,8 @@ class RawTask extends Component { | |
| <div className="toggle-ctn"> | ||
| {this.getTaskIcon(task, showChildren)} | ||
| </div> | ||
| <div className="task-row-ctn"> | ||
| {connectDropTarget(connectDragSource( | ||
| <div className={'task-row-ctn ' + (canDrop && isOver ? 'highlight-task' : '')}> | ||
| <div className="task-title" onClick={this.selectTaskInHierarchy}> | ||
| {this.showTitle(task)} | ||
| </div> | ||
|
|
@@ -208,6 +267,7 @@ class RawTask extends Component { | |
| patchTask={this.props.patchTask} | ||
| /> | ||
| </div> | ||
| ))} | ||
| </div> | ||
| { | ||
| task.subtask_gids && task.subtask_gids.length > 0 && | ||
|
|
@@ -223,15 +283,23 @@ class RawTask extends Component { | |
| } | ||
| } | ||
|
|
||
| const Task = withRouter(connect( | ||
| ({ pursuances, user, users, currentPursuanceId, autoComplete, rightPanel }) => | ||
| ({ pursuances, user, users, currentPursuanceId, autoComplete, rightPanel }), { | ||
| addTaskFormToHierarchy, | ||
| removeTaskFormFromHierarchy, | ||
| startSuggestions, | ||
| rpShowTaskDetailsOrCollapse, | ||
| patchTask | ||
| })(RawTask)); | ||
| const enhance = compose( | ||
| withRouter, | ||
| connect( | ||
| ({ pursuances, user, users, currentPursuanceId, autoComplete, rightPanel }) => | ||
| ({ pursuances, user, users, currentPursuanceId, autoComplete, rightPanel }), { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the most readable way I've seen to do this; sweet 👍 . |
||
| addTaskFormToHierarchy, | ||
| removeTaskFormFromHierarchy, | ||
| startSuggestions, | ||
| rpShowTaskDetailsOrCollapse, | ||
| patchTask, | ||
| moveTask | ||
| }), | ||
| // placed after connect to make dispatch available. | ||
| DragSource('TASK', taskSource, collect), | ||
| DropTarget('TASK', taskTarget, collectTarget), | ||
| ) | ||
| const Task = enhance(RawTask); | ||
|
|
||
| // Why RawTask _and_ Task? Because Task.mapSubTasks() recursively | ||
| // renders Task components which weren't wrapped in a Redux connect() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -88,6 +88,7 @@ export default function(state = initialState, action) { | |
| const patchedTask = action.payload; | ||
| patchedTask.subtask_gids = | ||
| state.taskMap[patchedTask.gid].subtask_gids || []; | ||
|
|
||
| return Object.assign({}, state, { | ||
| taskMap: Object.assign({}, state.taskMap, { | ||
| [patchedTask.gid]: patchedTask | ||
|
|
@@ -163,6 +164,42 @@ export default function(state = initialState, action) { | |
| }); | ||
| } | ||
|
|
||
| case 'MOVE_TASK': | ||
| const { oldParentGid, newParentGid, taskGid } = action; | ||
| const newMap = Object.assign({}, state.taskMap); | ||
| const newParentTask = newMap[newParentGid]; | ||
| const oldParentTask = newMap[oldParentGid]; | ||
| const oldParentSubtaskGids = oldParentTask.subtask_gids.filter( | ||
| gid => gid !== taskGid | ||
| ); | ||
| const newSubtaskGids = [...newParentTask.subtask_gids, taskGid]; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So that what redux shows right after the drag-and-drop occurs is the same as what the user will see after a refresh, technically the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure :-) |
||
| const newSubtasks = newSubtaskGids.filter( | ||
| (gid, idx) => newSubtaskGids.indexOf(gid) === idx | ||
| ); | ||
|
|
||
| newSubtasks.sort(function(gid1, gid2) { | ||
| const t1Date = new Date(newMap[gid1].created); | ||
| const t2Date = new Date(newMap[gid2].created); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @colinfruit Efficiency idea: so that there's no need to parse any task's
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice, created_parsed a field? Or is it on the todo list?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a field that we'd be creating just to make this more efficient.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The point is that, the second time that one of these tasks's |
||
| if (t1Date === t2Date) { | ||
| return ( gid1 < gid2) ? -1 : ( gid1 > gid2 ) ? 1 : 0; | ||
| } else { | ||
| return (t1Date > t2Date) ? 1 : -1; | ||
| } | ||
| }); | ||
|
|
||
| return Object.assign({}, state, { | ||
| taskMap: Object.assign(newMap, { | ||
| [oldParentGid]: { | ||
| ...oldParentTask, | ||
| subtask_gids: oldParentSubtaskGids | ||
| }, | ||
| [newParentGid]: { | ||
| ...newParentTask, | ||
| subtask_gids: newSubtasks | ||
| } | ||
| }) | ||
| }); | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hell yeah! 🎉 |
||
| default: | ||
| return state; | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@colinfruit Adding
console.log("canDrop called at", new Date())here shows thatcanDropis called hundreds of times even if thedivthat the user's mouse is hovering over doesn't change.Think there's some way to make this more efficient? It works great but it's very slow and I'm trying to figure out why.