Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"react-big-calendar": "^0.19.0",
"react-bootstrap": "^0.31.3",
"react-datepicker": "^0.56.0",
"react-dnd": "^3.0.2",
"react-dnd-html5-backend": "^3.0.2",
"react-dom": "^15.6.1",
"react-icons": "^2.2.7",
"react-markdown": "^2.5.0",
Expand Down
7 changes: 7 additions & 0 deletions src/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ export const setTaskFormParentGid = (formId, newParentGid, oldParentGid) => ({
oldParentGid
});

export const moveTask = (oldParentGid, newParentGid, taskGid) => ({
type: 'MOVE_TASK',
oldParentGid,
newParentGid,
taskGid
})

export const getUsers = () => ({ type: 'GET_USERS', payload: getUsersReq() });

export const getPursuancesByIds = pursuanceIds => ({
Expand Down
17 changes: 10 additions & 7 deletions src/api/tasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,20 @@ 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}` +
console.log(`Task ${t2.gid} ("${t2.title}")'s parent ${t2.parent_task_gid}` +
` not found in taskMap`);
}
}
Expand Down
6 changes: 4 additions & 2 deletions src/components/Content/Pursuance/PursuancePage.js
Original file line number Diff line number Diff line change
@@ -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 TaskListView from './views/TaskListView';
Expand Down Expand Up @@ -41,7 +43,7 @@ class PursuancePage extends Component {
}
}

export default connect(({currentPursuanceId}) =>
export default DragDropContext(HTML5Backend)(connect(({currentPursuanceId}) =>
({ currentPursuanceId }), {
setCurrentPursuance
})(PursuancePage);
})(PursuancePage));
4 changes: 4 additions & 0 deletions src/components/Content/TaskHierarchy/Task/Task.css
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@
border-color: #fff;
}

.highlight-task {
background-color: #50b3fe;
}

@media (max-width: 1279px) {
#task-hierarchy .task-row-ctn {
padding: 0;
Expand Down
92 changes: 80 additions & 12 deletions src/components/Content/TaskHierarchy/Task/Task.js
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';
Expand All @@ -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) {
Copy link
Contributor

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 that canDrop is called hundreds of times even if the div that 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.

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);
Expand Down Expand Up @@ -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 } =
Expand All @@ -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>
Expand Down Expand Up @@ -208,6 +267,7 @@ class RawTask extends Component {
patchTask={this.props.patchTask}
/>
</div>
))}
</div>
{
task.subtask_gids && task.subtask_gids.length > 0 &&
Expand All @@ -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 }), {
Copy link
Contributor

Choose a reason for hiding this comment

The 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()
Expand Down
37 changes: 37 additions & 0 deletions src/reducers/tasksReducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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];
Copy link
Contributor

Choose a reason for hiding this comment

The 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 subtask_gids should be ordered by created and then by id. Think it'd be too slow to do that sorting here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@colinfruit Efficiency idea:

const t1 = newMap[gid1];
const t2 = newMap[gid2];
t1.created_parsed = t1.created_parsed || new Date(t1.created);
t2.created_parsed = t2.created_parsed || new Date(t2.created);

so that there's no need to parse any task's created timestamp twice.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice, created_parsed a field? Or is it on the todo list?

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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 created timestamp goes to be parsed, it'll reuse created_parsed rather than parsing the created field again and creating a Date object out of it.

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
}
})
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hell yeah! 🎉

default:
return state;
}
Expand Down