Group plans reorder tasks (#8358)

* Added move route for group tasks

* Added group task reorder to front end

* Added syncing with group task order

* Fixed linting issues

* Added missing exec and abstracted move code

* Added unit test for moveTask
This commit is contained in:
Keith Holliday 2017-01-11 11:16:20 -07:00 committed by Matteo Pagliazzi
parent 2690caed35
commit 1590d955cd
9 changed files with 191 additions and 21 deletions

View file

@ -0,0 +1,49 @@
import {
generateUser,
generateGroup,
} from '../../../../../helpers/api-v3-integration.helper';
describe('POST group-tasks/:taskId/move/to/:position', () => {
let user, guild;
beforeEach(async () => {
user = await generateUser({balance: 1});
guild = await generateGroup(user, {type: 'guild'});
});
it('can move task to new position', async () => {
let tasks = await user.post(`/tasks/group/${guild._id}`, [
{type: 'habit', text: 'habit 1'},
{type: 'habit', text: 'habit 2'},
{type: 'daily', text: 'daily 1'},
{type: 'habit', text: 'habit 3'},
{type: 'habit', text: 'habit 4'},
{type: 'todo', text: 'todo 1'},
{type: 'habit', text: 'habit 5'},
]);
let taskToMove = tasks[1];
expect(taskToMove.text).to.equal('habit 2');
let newOrder = await user.post(`/group-tasks/${tasks[1]._id}/move/to/3`);
expect(newOrder[3]).to.equal(taskToMove._id);
expect(newOrder.length).to.equal(5);
});
it('can push to bottom', async () => {
let tasks = await user.post(`/tasks/group/${guild._id}`, [
{type: 'habit', text: 'habit 1'},
{type: 'habit', text: 'habit 2'},
{type: 'daily', text: 'daily 1'},
{type: 'habit', text: 'habit 3'},
{type: 'habit', text: 'habit 4'},
{type: 'todo', text: 'todo 1'},
{type: 'habit', text: 'habit 5'},
]);
let taskToMove = tasks[1];
expect(taskToMove.text).to.equal('habit 2');
let newOrder = await user.post(`/group-tasks/${tasks[1]._id}/move/to/-1`);
expect(newOrder[4]).to.equal(taskToMove._id);
expect(newOrder.length).to.equal(5);
});
});

View file

@ -2,6 +2,7 @@ import {
createTasks,
getTasks,
syncableAttrs,
moveTask,
} from '../../../../../website/server/libs/taskManager';
import i18n from '../../../../../website/common/script/i18n';
import {
@ -169,4 +170,12 @@ describe('taskManager', () => {
expect(syncableTask.notes).to.not.exist;
expect(syncableTask.updatedAt).to.not.exist;
});
it('moves tasks to a specified position', async() => {
let order = ['task-id-1', 'task-id-2'];
moveTask(order, 'task-id-2', 0);
expect(order).to.eql(['task-id-2', 'task-id-1']);
});
});

View file

@ -90,6 +90,14 @@ describe('Tasks Service', function() {
$httpBackend.flush();
});
it('calls group move task endpoint', function() {
var taskId = 1;
var position = 0;
$httpBackend.expectPOST('/api/v3/group-tasks/' + taskId + '/move/to/' + position).respond({});
tasks.moveGroupTask(taskId, position);
$httpBackend.flush();
});
it('calls add check list item endpoint', function() {
var taskId = 1;
$httpBackend.expectPOST(apiV3Prefix + '/' + taskId + '/checklist').respond({});

View file

@ -198,10 +198,28 @@ window.habitrpg = angular.module('habitrpg',
})
.then(function (response) {
var tasks = response.data.data;
tasks.forEach(function (element, index, array) {
if (!$scope.group[element.type + 's']) $scope.group[element.type + 's'] = [];
$scope.group[element.type + 's'].unshift(element);
});
// @TODO: This task ordering logic should be astracted and user everywhere group or user tasks are loaded
var groupedTasks = _(tasks)
.groupBy('type')
.forEach(function (tasksOfType, type) {
var order = $scope.group.tasksOrder[type + 's'];
var orderedTasks = new Array(tasksOfType.length);
var unorderedTasks = []; // what we want to add later
tasksOfType.forEach(function (task, index) {
var taskId = task._id;
var i = order[index] === taskId ? index : order.indexOf(taskId);
if (i === -1) {
unorderedTasks.unshift(task); // unshift because we want to display new on top
} else {
orderedTasks[i] = task;
}
});
// Remove empty values from the array and add any unordered task
$scope.group[type + 's'] = _.compact(orderedTasks).concat(unorderedTasks);
}).value();
$scope.group.approvals = [];
if (User.user._id === $scope.group.leader._id) {

View file

@ -6,10 +6,11 @@
.directive('hrpgSortTasks', hrpgSortTasks);
hrpgSortTasks.$inject = [
'User'
'User',
'Tasks',
];
function hrpgSortTasks(User) {
function hrpgSortTasks(User, Tasks) {
return function($scope, element, attrs, ngModel) {
$(element).sortable({
axis: "y",
@ -20,6 +21,13 @@
stop: function (event, ui) {
var task = angular.element(ui.item[0]).scope().task;
var startIndex = ui.item.data('startIndex');
// Check if task is a group original task
if (task.group.id && !task.userId) {
Tasks.moveGroupTask(task._id, ui.item.index());
return;
}
User.sortTask({
params: { id: task._id, taskType: task.type },
query: {

View file

@ -158,6 +158,13 @@ angular.module('habitrpg')
});
};
function moveGroupTask (taskId, position) {
return $http({
method: 'POST',
url: '/api/v3/group-tasks/' + taskId + '/move/to/' + position,
});
};
function addChecklistItem (taskId, checkListItem) {
return $http({
method: 'POST',
@ -408,5 +415,6 @@ angular.module('habitrpg')
getGroupApprovals: getGroupApprovals,
approve: approve,
moveGroupTask: moveGroupTask,
};
}]);

View file

@ -16,6 +16,7 @@ import {
import {
createTasks,
getTasks,
moveTask,
} from '../../libs/taskManager';
import common from '../../../common';
import Bluebird from 'bluebird';
@ -460,22 +461,8 @@ api.moveTask = {
if (!task) throw new NotFound(res.t('taskNotFound'));
if (task.type === 'todo' && task.completed) throw new BadRequest(res.t('cantMoveCompletedTodo'));
let order = user.tasksOrder[`${task.type}s`];
let currentIndex = order.indexOf(task._id);
// If for some reason the task isn't ordered (should never happen), push it in the new position
// if the task is moved to a non existing position
// or if the task is moved to position -1 (push to bottom)
// -> push task at end of list
if (!order[to] && to !== -1) {
order.push(task._id);
} else {
if (currentIndex !== -1) order.splice(currentIndex, 1);
if (to === -1) {
order.push(task._id);
} else {
order.splice(to, 0, task._id);
}
}
moveTask(order, task._id, to);
await user.save();
res.respond(200, order);

View file

@ -4,12 +4,14 @@ import * as Tasks from '../../../models/task';
import { model as Group } from '../../../models/group';
import { model as User } from '../../../models/user';
import {
BadRequest,
NotFound,
NotAuthorized,
} from '../../../libs/errors';
import {
createTasks,
getTasks,
moveTask,
} from '../../../libs/taskManager';
let requiredGroupFields = '_id leader tasksOrder name';
@ -80,6 +82,58 @@ api.getGroupTasks = {
},
};
/**
* @api {post} /api/v3/group/:groupId/tasks/:taskId/move/to/:position Move a group task to a specified position
* @apiDescription Moves a group task to a specified position
* @apiVersion 3.0.0
* @apiName GroupMoveTask
* @apiGroup Task
*
* @apiParam {String} taskId The task _id
* @apiParam {Number} position Query parameter - Where to move the task (-1 means push to bottom). First position is 0
*
* @apiSuccess {Array} data The new tasks order (group.tasksOrder.{task.type}s)
*/
api.groupMoveTask = {
method: 'POST',
url: '/group-tasks/:taskId/move/to/:position',
middlewares: [authWithHeaders()],
async handler (req, res) {
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty();
req.checkParams('position', res.t('positionRequired')).notEmpty().isNumeric();
let reqValidationErrors = req.validationErrors();
if (reqValidationErrors) throw reqValidationErrors;
let user = res.locals.user;
let taskId = req.params.taskId;
let task = await Tasks.Task.findOne({
_id: taskId,
}).exec();
let to = Number(req.params.position);
if (!task) {
throw new NotFound(res.t('taskNotFound'));
}
if (task.type === 'todo' && task.completed) throw new BadRequest(res.t('cantMoveCompletedTodo'));
let group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields});
if (!group) throw new NotFound(res.t('groupNotFound'));
if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
let order = group.tasksOrder[`${task.type}s`];
moveTask(order, task._id, to);
await group.save();
res.respond(200, order);
},
};
/**
* @api {post} /api/v3/tasks/:taskId/assign/:assignedUserId Assign a group task to a user
* @apiDescription Assigns a user to a group task

View file

@ -188,3 +188,32 @@ export function syncableAttrs (task) {
if (t.type !== 'reward') omitAttrs.push('value');
return _.omit(t, omitAttrs);
}
/**
* Moves a task to a specified position.
*
* @param order The list of ordered tasks
* @param taskId The Task._id of the task to move
* @param to A integer specifiying the index to move the task to
*
* @return Empty
*/
export function moveTask (order, taskId, to) {
let currentIndex = order.indexOf(taskId);
// If for some reason the task isn't ordered (should never happen), push it in the new position
// if the task is moved to a non existing position
// or if the task is moved to position -1 (push to bottom)
// -> push task at end of list
if (!order[to] && to !== -1) {
order.push(taskId);
return;
}
if (currentIndex !== -1) order.splice(currentIndex, 1);
if (to === -1) {
order.push(taskId);
} else {
order.splice(to, 0, taskId);
}
}