From db6053463fb0aafc452bbba4663aae122635e812 Mon Sep 17 00:00:00 2001 From: 4xdk Date: Sat, 16 Feb 2019 10:42:42 +0000 Subject: [PATCH 1/4] Port over changes from abandoned PR --- src/actions/index.js | 7 + src/api/tasks.js | 21 ++- .../Content/Pursuance/PursuancePage.js | 6 +- .../Content/TaskHierarchy/Task/Task.css | 4 + .../Content/TaskHierarchy/Task/Task.js | 169 ++++++++++++------ src/reducers/tasksReducer.js | 37 ++++ 6 files changed, 182 insertions(+), 62 deletions(-) diff --git a/src/actions/index.js b/src/actions/index.js index a43724c..40ee222 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -24,6 +24,13 @@ import { deleteMembershipReq, } from '../api/memberships'; +export const moveTask = (oldParentGid, newParentGid, taskGid) => ({ + type: 'MOVE_TASK', + 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..7c4b4f5 100644 --- a/src/components/Content/TaskHierarchy/Task/Task.js +++ b/src/components/Content/TaskHierarchy/Task/Task.js @@ -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'; @@ -20,7 +22,8 @@ import { removeTaskFormFromHierarchy, startSuggestions, rpShowTaskDetailsOrCollapse, - patchTask + patchTask, + moveTask } from '../../../../actions'; // task list uses the default vertical/wide spread confetti @@ -32,6 +35,60 @@ 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(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 || !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); @@ -186,7 +243,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 +265,57 @@ class RawTask extends Component { {this.getTaskIcon(task, showChildren)} )} -
-
- {this.showTitle(task)} -
-
-
-
- {!isInTaskList && ( + {connectDropTarget(connectDragSource( +
+
+ {this.showTitle(task)} +
+
+
+
+ {!isInTaskList && ( + +
+ +
+
+ )} -
- +
+
+
+ {!isInTaskList && ( + )} - -
- -
-
-
- {!isInTaskList && ( - + +
+ - )} -
-
- -
+ ))}
{ task.subtask_gids && task.subtask_gids.length > 0 && @@ -272,15 +331,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() diff --git a/src/reducers/tasksReducer.js b/src/reducers/tasksReducer.js index 3a6b509..2a2d3d6 100644 --- a/src/reducers/tasksReducer.js +++ b/src/reducers/tasksReducer.js @@ -195,6 +195,43 @@ 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 + ); + + newSubtasks.sort(function(gid1, gid2) { + const t1Date = new Date(newMap[gid1].created); + const t2Date = new Date(newMap[gid2].created); + + 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; } From ffaefa8b4de4e0ab538ad1718e7f0465da004b1e Mon Sep 17 00:00:00 2001 From: 4xdk Date: Sun, 24 Feb 2019 16:10:36 +0000 Subject: [PATCH 2/4] Add lodash, use debounce to reduce number of canDrop calls, cache parsed task creation date on sorting --- package.json | 1 + .../Content/TaskHierarchy/Task/Task.js | 18 +++++++++++------- src/reducers/tasksReducer.js | 7 +++++-- 3 files changed, 17 insertions(+), 9 deletions(-) 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/components/Content/TaskHierarchy/Task/Task.js b/src/components/Content/TaskHierarchy/Task/Task.js index 7c4b4f5..641147e 100644 --- a/src/components/Content/TaskHierarchy/Task/Task.js +++ b/src/components/Content/TaskHierarchy/Task/Task.js @@ -3,6 +3,7 @@ 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'; @@ -48,16 +49,19 @@ const taskSource = { }; const taskTarget = { - canDrop(props, monitor) { + canDrop: _.debounce((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 || !target.parent_task_gid) return false; - return (target.gid === source.gid) || isParent(map, map[target.parent_task_gid], source); + + 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); } - return !isParent(taskMap, taskData, source); - }, + }, 15), drop(props, monitor, component) { const { taskData, patchTask, moveTask } = props; const { gid, parent_task_gid } = monitor.getItem(); diff --git a/src/reducers/tasksReducer.js b/src/reducers/tasksReducer.js index 2a2d3d6..132e4f2 100644 --- a/src/reducers/tasksReducer.js +++ b/src/reducers/tasksReducer.js @@ -208,9 +208,12 @@ export default function(state = initialState, action) { (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); + 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; From 0ebab0321e6230e5becd32bc8bcb35d3f7b48618 Mon Sep 17 00:00:00 2001 From: 4xdk Date: Sun, 24 Feb 2019 19:28:24 +0000 Subject: [PATCH 3/4] remove empty line --- src/reducers/tasksReducer.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/reducers/tasksReducer.js b/src/reducers/tasksReducer.js index 132e4f2..e4fdba6 100644 --- a/src/reducers/tasksReducer.js +++ b/src/reducers/tasksReducer.js @@ -207,7 +207,6 @@ export default function(state = initialState, action) { const newSubtasks = newSubtaskGids.filter( (gid, idx) => newSubtaskGids.indexOf(gid) === idx ); - newSubtasks.sort(function(gid1, gid2) { newMap[gid1].created_parsed = newMap[gid1].created_parsed || new Date(newMap[gid1].created); From fb5f1e10dd3739ca48e5f302fad8db360be8298a Mon Sep 17 00:00:00 2001 From: 4xdk Date: Sun, 3 Mar 2019 10:12:20 +0000 Subject: [PATCH 4/4] change move_task to move_task_in_hierarchy, remove some useless code --- src/actions/index.js | 4 ++-- src/components/Content/TaskHierarchy/Task/Task.js | 11 ++++++----- src/reducers/tasksReducer.js | 7 ++----- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/actions/index.js b/src/actions/index.js index 40ee222..1c10673 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -24,8 +24,8 @@ import { deleteMembershipReq, } from '../api/memberships'; -export const moveTask = (oldParentGid, newParentGid, taskGid) => ({ - type: 'MOVE_TASK', +export const moveTaskInHierarchy = (oldParentGid, newParentGid, taskGid) => ({ + type: 'MOVE_TASK_IN_HIERARCHY', oldParentGid, newParentGid, taskGid diff --git a/src/components/Content/TaskHierarchy/Task/Task.js b/src/components/Content/TaskHierarchy/Task/Task.js index 641147e..df2fc15 100644 --- a/src/components/Content/TaskHierarchy/Task/Task.js +++ b/src/components/Content/TaskHierarchy/Task/Task.js @@ -24,7 +24,7 @@ import { startSuggestions, rpShowTaskDetailsOrCollapse, patchTask, - moveTask + moveTaskInHierarchy } from '../../../../actions'; // task list uses the default vertical/wide spread confetti @@ -63,17 +63,18 @@ const taskTarget = { } }, 15), drop(props, monitor, component) { - const { taskData, patchTask, moveTask } = props; + const { taskData, patchTask, moveTaskInHierarchy } = props; const { gid, parent_task_gid } = monitor.getItem(); const oldParent = parent_task_gid; - moveTask(oldParent, taskData.gid, 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') { - moveTask(taskData.gid, oldParent, gid); + moveTaskInHierarchy(taskData.gid, oldParent, gid); } }); } @@ -345,7 +346,7 @@ const enhance = compose( startSuggestions, rpShowTaskDetailsOrCollapse, patchTask, - moveTask + moveTaskInHierarchy }), // placed after connect to make dispatch available. DragSource('TASK', taskSource, collect), diff --git a/src/reducers/tasksReducer.js b/src/reducers/tasksReducer.js index e4fdba6..f2a60b1 100644 --- a/src/reducers/tasksReducer.js +++ b/src/reducers/tasksReducer.js @@ -195,7 +195,7 @@ export default function(state = initialState, action) { }); } - case 'MOVE_TASK': + case 'MOVE_TASK_IN_HIERARCHY': const { oldParentGid, newParentGid, taskGid } = action; const newMap = Object.assign({}, state.taskMap); const newParentTask = newMap[newParentGid]; @@ -203,11 +203,8 @@ export default function(state = initialState, action) { 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 - ); + const newSubtasks = [...newParentTask.subtask_gids, taskGid]; 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);