Skip to content
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

Task Hierarchy: drag-and-drop task from one part of the hierarchy to another #37

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 7 additions & 0 deletions src/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ import {
deleteMembershipReq,
} from '../api/memberships';

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

Choose a reason for hiding this comment

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

How about MOVE_TASK_IN_HIERARCHY? Soon we may be reordering task lists in My Tasks

oldParentGid,
newParentGid,
taskGid
})

export const updateFormField = (formId, fieldId, value) => ({
type: 'TASK_FIELD_UPDATE',
formId,
Expand Down
21 changes: 12 additions & 9 deletions src/api/tasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`
);
}
}
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';
Copy link
Member

Choose a reason for hiding this comment

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

Let's make sure there's no negative privacy repercussions here. Is HTML5 the only back end option that exists for this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Well, the only other one is the TouchBackend which doesn't seem to work using the mouse (one could say, obviously).

The thing is HTML5 backend doesn't work on touch devices (again, obviously).

I've checked the TouchBackend on iPhone6 (browserstack) and drag and drop seems to work ... sometimes. I guess it's and IOS issue: https://github.com/yahoo/react-dnd-touch-backend/issues/78. Although Android (Pixel 3) seemed to work even worse (I managed to delete some tasks trying to drag and drop 👎)

Anyway, if we're still up for it we could use https://louisbrunner.github.io/dnd-multi-backend/packages/react-dnd-multi-backend/
Quote: "HTML5toTouch starts by using the React DnD HTML5 Backend, but switches to the React DnD Touch Backend if a touch event is triggered. You application can smoothly use the nice HTML5 compatible backend and fallback on the Touch one on mobile devices!"

But as far as your original question goes re. privacy repercussions. No, I don't think we have any other backend option with react-dnd for desktop. Is HTML5 a privacy problem?

import { setCurrentPursuance } from '../../../actions';
import PursuanceMenu from './PursuanceMenu';
import MyTasksView from './views/MyTasksView';
Expand Down Expand Up @@ -50,7 +52,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 @@ -15,6 +15,10 @@
border-color: #fff;
}

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

.toggle-ctn {
padding-right: 6px;
}
Expand Down
173 changes: 122 additions & 51 deletions src/components/Content/TaskHierarchy/Task/Task.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -20,7 +23,8 @@ import {
removeTaskFormFromHierarchy,
startSuggestions,
rpShowTaskDetailsOrCollapse,
patchTask
patchTask,
moveTask
} from '../../../../actions';

// task list uses the default vertical/wide spread confetti
Expand All @@ -32,6 +36,63 @@ 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, 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 @@ -186,7 +247,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) {
Expand All @@ -208,55 +269,57 @@ class RawTask extends Component {
{this.getTaskIcon(task, showChildren)}
</div>
)}
<div className="task-row-ctn">
<div className="task-title" onClick={this.selectTaskInHierarchy}>
{this.showTitle(task)}
</div>
<div className="task-title-buffer" onClick={this.selectTaskInHierarchy}>
</div>
<div className={"task-icons-ctn " + (isInTaskList ? 'in-task-list-narrow' : '')}>
{!isInTaskList && (
{connectDropTarget(connectDragSource(
<div className={'task-row-ctn ' + (canDrop && isOver ? 'highlight-task' : '')}>
<div className="task-title" onClick={this.selectTaskInHierarchy}>
{this.showTitle(task)}
</div>
<div className="task-title-buffer" onClick={this.selectTaskInHierarchy}>
</div>
<div className={"task-icons-ctn " + (isInTaskList ? 'in-task-list-narrow' : '')}>
{!isInTaskList && (
<OverlayTrigger
placement="bottom"
overlay={this.getTooltip('hands-down')}
>
<div id={'create-subtask-' + task.gid} className="icon-ctn create-subtask" onClick={this.toggleNewForm}>
<TiFlowChildren size={20} />
</div>
</OverlayTrigger>
)}
<OverlayTrigger
placement="bottom"
overlay={this.getTooltip('hands-down')}
overlay={this.getTooltip('chat')}
>
<div id={'create-subtask-' + task.gid} className="icon-ctn create-subtask" onClick={this.toggleNewForm}>
<TiFlowChildren size={20} />
<div id={'discuss-task-' + task.gid} className="icon-ctn discuss-task hide-small" onClick={this.redirectToDiscuss}>
<FaCommentsO size={20} />
</div>
</OverlayTrigger>
</div>
{!isInTaskList && (
<TaskStatus
gid={task.gid}
status={task.status}
patchTask={this.props.patchTask}
showCelebration={task.celebration === 'show' && !rightPanel.show}
confettiConfig={confettiConfig}
/>
)}
<OverlayTrigger
placement="bottom"
overlay={this.getTooltip('chat')}
>
<div id={'discuss-task-' + task.gid} className="icon-ctn discuss-task hide-small" onClick={this.redirectToDiscuss}>
<FaCommentsO size={20} />
</div>
</OverlayTrigger>
</div>
{!isInTaskList && (
<TaskStatus
gid={task.gid}
status={task.status}
<div className="task-assigned-to hide-small">
<TaskAssigner
taskGid={task.gid}
placeholder={placeholder}
assignedTo={assignedTo}
/>
</div>
<TaskDueDate
id={task.gid}
taskData={task}
autoFocus={true}
patchTask={this.props.patchTask}
showCelebration={task.celebration === 'show' && !rightPanel.show}
confettiConfig={confettiConfig}
/>
)}
<div className="task-assigned-to hide-small">
<TaskAssigner
taskGid={task.gid}
placeholder={placeholder}
assignedTo={assignedTo}
/>
</div>
<TaskDueDate
id={task.gid}
taskData={task}
autoFocus={true}
patchTask={this.props.patchTask}
/>
</div>
))}
</div>
{
task.subtask_gids && task.subtask_gids.length > 0 &&
Expand All @@ -272,15 +335,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 }), {
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
39 changes: 39 additions & 0 deletions src/reducers/tasksReducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,45 @@ 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];
const newSubtasks = newSubtaskGids.filter(
(gid, idx) => newSubtaskGids.indexOf(gid) === idx
Copy link
Member

Choose a reason for hiding this comment

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

Every subtask gid has the same index as itself... or am I reading this wrong?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Reading well ... I'll remove this bit.

);

newSubtasks.sort(function(gid1, gid2) {
newMap[gid1].created_parsed = newMap[gid1].created_parsed || new Date(newMap[gid1].created);
newMap[gid2].created_parsed = newMap[gid2].created_parsed || new Date(newMap[gid2].created);
const t1Date = newMap[gid1].created_parsed;
const t2Date = newMap[gid2].created_parsed;

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

default:
return state;
}
Expand Down