diff --git a/frontend/app/scripts/controllers/case/CaseTasksCtrl.js b/frontend/app/scripts/controllers/case/CaseTasksCtrl.js
index 2a295d2d5e..7618d83dfc 100755
--- a/frontend/app/scripts/controllers/case/CaseTasksCtrl.js
+++ b/frontend/app/scripts/controllers/case/CaseTasksCtrl.js
@@ -18,6 +18,11 @@
$scope.taskResponders = null;
$scope.collapseOptions = {};
+ $scope.selection = [];
+ $scope.menu = {
+ selectAll: false
+ };
+
this.$onInit = function() {
$scope.filtering = new FilteringSrv('task', 'task.list', {
version: 'v1',
@@ -48,7 +53,7 @@
};
$scope.load = function() {
- $scope.tasks = new PaginatedQuerySrv({
+ $scope.list = new PaginatedQuerySrv({
name: 'case-tasks',
root: $scope.caseId,
objectType: 'case_task',
@@ -71,11 +76,69 @@
extraData: ['shareCount', 'actionRequired'],
//extraData: ['isOwner', 'shareCount'],
onUpdate: function() {
- $scope.buildTaskGroups($scope.tasks.values);
+ $scope.buildTaskGroups($scope.list.values);
+ $scope.resetSelection();
}
});
};
+ $scope.resetSelection = function() {
+ if ($scope.menu.selectAll) {
+ $scope.selectAll();
+ } else {
+ $scope.selection = [];
+ $scope.menu.selectAll = false;
+ $scope.updateMenu();
+ }
+ };
+
+ $scope.updateMenu = function() {
+ // Handle flag/unflag menu items
+ var temp = _.uniq(_.pluck($scope.selection, 'flag'));
+ $scope.menu.unflag = temp.length === 1 && temp[0] === true;
+ $scope.menu.flag = temp.length === 1 && temp[0] === false;
+
+ // Handle close menu item
+ // temp = _.uniq(_.pluck($scope.selection, 'status'));
+ // $scope.menu.close = temp.length === 1 && temp[0] === 'Open';
+ // $scope.menu.reopen = temp.length === 1 && temp[0] === 'Resolved';
+
+ // $scope.menu.delete = $scope.selection.length > 0;
+ };
+
+ $scope.select = function(task) {
+ if (task.selected) {
+ $scope.selection.push(task);
+ } else {
+ $scope.selection = _.reject($scope.selection, function(item) {
+ return item._id === task._id;
+ });
+ }
+ $scope.updateMenu();
+ };
+
+ $scope.selectAll = function() {
+ var selected = $scope.menu.selectAll;
+
+ _.each($scope.list.values, function(item) {
+ // if(SecuritySrv.checkPermissions(['manageCase'], item.extraData.permissions)) {
+ item.selected = selected;
+ // }
+ });
+
+ if (selected) {
+ $scope.selection = _.filter($scope.list.values, function(item) {
+ return !!item.selected;
+ });
+ } else {
+ $scope.selection = [];
+ }
+
+ $scope.updateMenu();
+ };
+
+ // ######################@@@
+
$scope.toggleStats = function () {
$scope.filtering.toggleStats();
};
@@ -215,6 +278,18 @@
});
};
+ $scope.bulkFlag = function(flag) {
+ var ids = _.pluck($scope.selection, '_id');
+
+ return CaseTaskSrv.bulkUpdate(ids, {flag: flag})
+ .then(function(/*responses*/) {
+ NotificationSrv.log('Selected tasks have been updated successfully', 'success');
+ })
+ .catch(function(err) {
+ NotificationSrv.error('Bulk flag tasks', err.data, err.status);
+ });
+ }
+
// open task tab with its details
$scope.startTask = function(task) {
var taskId = task._id;
diff --git a/frontend/app/scripts/services/api/CaseTaskSrv.js b/frontend/app/scripts/services/api/CaseTaskSrv.js
index 6b3e96e8a4..1c8d6c3469 100644
--- a/frontend/app/scripts/services/api/CaseTaskSrv.js
+++ b/frontend/app/scripts/services/api/CaseTaskSrv.js
@@ -57,6 +57,10 @@
});
};
+ this.bulkUpdate = function(ids, update) {
+ return $http.patch('./api/v1/task/_bulk', _.extend({ids: ids}, update));
+ };
+
this.removeShare = function(id, share) {
return $http.delete('./api/task/'+id+'/shares', {
data: {
diff --git a/frontend/app/styles/case.css b/frontend/app/styles/case.css
index 3a71aae0e0..9ba0437f5f 100644
--- a/frontend/app/styles/case.css
+++ b/frontend/app/styles/case.css
@@ -80,7 +80,7 @@ table.case-list .case-tags .label,
font-size: 12px !important;
font-weight: normal;
}
-
+table.data-list .btn-icon,
table.case-list .btn-icon {
padding: 6px;
padding-top: 0;
diff --git a/frontend/app/views/partials/case/case.tasks.html b/frontend/app/views/partials/case/case.tasks.html
index 0811411f68..55553882aa 100755
--- a/frontend/app/views/partials/case/case.tasks.html
+++ b/frontend/app/views/partials/case/case.tasks.html
@@ -7,7 +7,7 @@
- Tasks List ({{tasks.values.length || 0}} of {{tasks.total}})
+ Tasks List ({{list.values.length || 0}} of {{list.total}})
@@ -17,7 +17,7 @@
-
+
No task found for this case.
@@ -52,17 +52,20 @@
-
+
-
+
-
+
- {{group.group || 'Not Specified'}} ({{group.tasks.length}} task(s))
+ {{group.group || 'Not Specified'}} ({{group.list.length}} task(s))
-
+
+
+
+ |
|
|
- Group |
+ Group |
Task |
|
Date |
@@ -204,6 +214,9 @@
+
+
+ |
-
-
+
+
|
@@ -270,28 +283,22 @@
|
-
-
- Delete
-
-
-
- Reopen
-
-
-
- Close
-
-
-
- Start
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
|
@@ -304,7 +311,7 @@
-
+
diff --git a/frontend/app/views/partials/case/tasks/toolbar.html b/frontend/app/views/partials/case/tasks/toolbar.html
index afddfc4cd2..ad5cd717c0 100644
--- a/frontend/app/views/partials/case/tasks/toolbar.html
+++ b/frontend/app/views/partials/case/tasks/toolbar.html
@@ -2,6 +2,31 @@
+
+
+
diff --git a/thehive/app/org/thp/thehive/controllers/v1/Router.scala b/thehive/app/org/thp/thehive/controllers/v1/Router.scala
index 5f609a94be..9cdc55f323 100644
--- a/thehive/app/org/thp/thehive/controllers/v1/Router.scala
+++ b/thehive/app/org/thp/thehive/controllers/v1/Router.scala
@@ -111,6 +111,7 @@ class Router @Inject() (
case GET(p"/task") => taskCtrl.list
case POST(p"/task") => taskCtrl.create
case GET(p"/task/$taskId") => taskCtrl.get(taskId)
+ case PATCH(p"/task/_bulk") => taskCtrl.bulkUpdate
case PATCH(p"/task/$taskId") => taskCtrl.update(taskId)
case GET(p"/task/$taskId/actionRequired") => taskCtrl.isActionRequired(taskId)
case PUT(p"/task/$taskId/actionRequired/$orgaId") => taskCtrl.actionRequired(taskId, orgaId, required = true)
diff --git a/thehive/app/org/thp/thehive/controllers/v1/TaskCtrl.scala b/thehive/app/org/thp/thehive/controllers/v1/TaskCtrl.scala
index 57f80452c5..35a8aae662 100644
--- a/thehive/app/org/thp/thehive/controllers/v1/TaskCtrl.scala
+++ b/thehive/app/org/thp/thehive/controllers/v1/TaskCtrl.scala
@@ -1,5 +1,6 @@
package org.thp.thehive.controllers.v1
+import org.thp.scalligraph._
import org.thp.scalligraph.EntityIdOrName
import org.thp.scalligraph.controllers.{Entrypoint, FieldsParser}
import org.thp.scalligraph.models.Database
@@ -126,6 +127,25 @@ class TaskCtrl @Inject() (
.map(_ => Results.NoContent)
}
+ def bulkUpdate: Action[AnyContent] =
+ entrypoint("bulk update")
+ .extract("input", FieldsParser.update("task", publicProperties))
+ .extract("ids", FieldsParser.seq[String].on("ids"))
+ .authTransaction(db) { implicit request => implicit graph =>
+ val properties: Seq[PropertyUpdater] = request.body("input")
+ val ids: Seq[String] = request.body("ids")
+ ids
+ .toTry { id =>
+ taskSrv
+ .update(
+ _.get(EntityIdOrName(id))
+ .can(Permissions.manageTask),
+ properties
+ )
+ }
+ .map(_ => Results.NoContent)
+ }
+
def isActionRequired(taskId: String): Action[AnyContent] =
entrypoint("is action required")
.authTransaction(db) { implicit request => implicit graph =>