diff --git a/app/coffee/modules/related-tasks.coffee b/app/coffee/modules/related-tasks.coffee
index 5f6fa132..ec04baac 100644
--- a/app/coffee/modules/related-tasks.coffee
+++ b/app/coffee/modules/related-tasks.coffee
@@ -237,9 +237,8 @@ module.directive("tgRelatedTaskCreateButton", ["$tgRepo", "$compile", "$tgConfir
RelatedTasksDirective = ($repo, $rs, $rootscope) ->
link = ($scope, $el, $attrs) ->
loadTasks = ->
- return $rs.tasks.list($scope.projectId, null, $scope.usId).then (tasks) =>
- $scope.tasks = _.sortBy(tasks, (x) => [x.us_order, x.ref])
- return tasks
+ return $rs.tasks.list($scope.projectId, null, $scope.usId).then (result) ->
+ Immutable.fromJS(result.data)
_isVisible = ->
if $scope.project
@@ -251,6 +250,9 @@ RelatedTasksDirective = ($repo, $rs, $rootscope) ->
return $scope.project.my_permissions.indexOf("modify_task") != -1
return false
+ $scope.reorderTask = (task, newIndex) ->
+ $rootscope.$broadcast('task:reorder', task, newIndex)
+
$scope.showRelatedTasks = ->
return _isVisible() && ( _isEditable() || $scope.tasks?.length )
@@ -258,6 +260,9 @@ RelatedTasksDirective = ($repo, $rs, $rootscope) ->
loadTasks().then ->
$rootscope.$broadcast("related-tasks:update")
+ $scope.$on "related-tasks:reordered", ->
+ loadTasks()
+
$scope.$on "related-tasks:delete", ->
loadTasks().then ->
$rootscope.$broadcast("related-tasks:update")
diff --git a/app/coffee/modules/resources/tasks.coffee b/app/coffee/modules/resources/tasks.coffee
index 9070e0dd..39f449c9 100644
--- a/app/coffee/modules/resources/tasks.coffee
+++ b/app/coffee/modules/resources/tasks.coffee
@@ -57,7 +57,7 @@ resourceProvider = ($repo, $http, $urls, $storage) ->
return $repo.queryOneRaw("task-filters", null, params)
service.list = (projectId, sprintId=null, userStoryId=null, params) ->
- params = _.merge(params, {project: projectId})
+ params = _.merge(params, {project: projectId, order_by: 'us_order'})
params.milestone = sprintId if sprintId
params.user_story = userStoryId if userStoryId
service.storeQueryParams(projectId, params)
@@ -90,6 +90,14 @@ resourceProvider = ($repo, $http, $urls, $storage) ->
params = {project_id: projectId, bulk_tasks: data}
return $http.post(url, params)
+ service.reorder = (id, data, setOrders) ->
+ url = $urls.resolve("tasks") + "/#{id}"
+
+ options = {"headers": {"set-orders": JSON.stringify(setOrders)}}
+
+ return $http.patch(url, data, null, options)
+ .then (result) -> result.data
+
service.listValues = (projectId, type) ->
params = {"project": projectId}
return $repo.queryMany(type, params)
diff --git a/app/coffee/modules/userstories/detail.coffee b/app/coffee/modules/userstories/detail.coffee
index 56866510..ceca2a4f 100644
--- a/app/coffee/modules/userstories/detail.coffee
+++ b/app/coffee/modules/userstories/detail.coffee
@@ -101,6 +101,7 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
initializeEventHandlers: ->
@scope.$on "related-tasks:update", =>
+ @.loadTasks()
@scope.tasks = _.clone(@scope.tasks, false)
allClosed = _.every @scope.tasks, (task) -> return task.is_closed
@@ -110,6 +111,9 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
@scope.$on "attachment:create", =>
@analytics.trackEvent("attachment", "create", "create attachment on userstory", 1)
+ @scope.$on "task:reorder", (event, task, newIndex) =>
+ @.reorderTask(task, newIndex)
+
@scope.$on "comment:new", =>
@.loadUs()
@@ -233,15 +237,55 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
return @rs.userstories.unwatch(@scope.usId).then(onSuccess, onError)
onTribeInfo: ->
- publishTitle = @translate.instant("US.TRIBE.PUBLISH_MORE_INFO_TITLE")
- image = $('
')
- .attr({
- 'src': "/#{window._version}/images/monster-fight.png",
- 'alt': @translate.instant("US.TRIBE.PUBLISH_MORE_INFO_TITLE")
- })
- text = @translate.instant("US.TRIBE.PUBLISH_MORE_INFO_TEXT")
- publishDesc = $('
').append(image).append(text)
- @confirm.success(publishTitle, publishDesc)
+ publishTitle = @translate.instant("US.TRIBE.PUBLISH_MORE_INFO_TITLE")
+ image = $('
')
+ .attr({
+ 'src': "/#{window._version}/images/monster-fight.png",
+ 'alt': @translate.instant("US.TRIBE.PUBLISH_MORE_INFO_TITLE")
+ })
+ text = @translate.instant("US.TRIBE.PUBLISH_MORE_INFO_TEXT")
+ publishDesc = $('').append(image).append(text)
+ @confirm.success(publishTitle, publishDesc)
+
+ reorderTask: (task, newIndex) ->
+ orderList = {}
+ @scope.tasks.forEach (it) ->
+ orderList[it.id] = it.us_order
+
+ withoutMoved = @scope.tasks.filter (it) -> it.id != task.id
+ beforeDestination = withoutMoved.slice(0, newIndex)
+ afterDestination = withoutMoved.slice(newIndex)
+
+ previous = beforeDestination[beforeDestination.length - 1]
+ newOrder = if !previous then 0 else previous.us_order + 1
+
+ orderList[task.id] = newOrder
+
+ previousWithTheSameOrder = beforeDestination.filter (it) ->
+ it.us_order == previous.us_order
+
+ setOrders = _.fromPairs previousWithTheSameOrder.map((it) ->
+ [it.id, it.us_order]
+ )
+
+ afterDestination.forEach (it) -> orderList[it.id] = it.us_order + 1
+
+ @scope.tasks = _.map(@scope.tasks, (it) ->
+ it.us_order = orderList[it.id]
+ return it
+ )
+ @scope.tasks = _.sortBy(@scope.tasks, "us_order")
+
+ data = {
+ us_order: newOrder,
+ version: task.version
+ }
+
+ return @rs.tasks.reorder(task.id, data, setOrders).then (newTask) =>
+ @scope.tasks = _.map(@scope.tasks, (it) ->
+ return if it.id == newTask.id then newTask else it
+ )
+ @rootscope.$broadcast("related-tasks:reordered")
module.controller("UserStoryDetailController", UserStoryDetailController)
diff --git a/app/modules/components/tasks-sortable/tasks-sortable.directive.coffee b/app/modules/components/tasks-sortable/tasks-sortable.directive.coffee
new file mode 100644
index 00000000..f0b27ba8
--- /dev/null
+++ b/app/modules/components/tasks-sortable/tasks-sortable.directive.coffee
@@ -0,0 +1,64 @@
+###
+# Copyright (C) 2014-2018 Taiga Agile LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# File: tasks-sortable.directive.coffee
+###
+
+TasksSortableDirective = ($parse, projectService) ->
+ link = (scope, el, attrs) ->
+ return if not projectService.hasPermission("modify_task")
+
+ callback = $parse(attrs.tgTasksSortable)
+
+ drake = dragula([el[0]], {
+ copySortSource: false
+ copy: false
+ mirrorContainer: el[0]
+ moves: (item) ->
+ return $(item).is('div.single-related-task.js-related-task')
+ })
+
+ drake.on 'dragend', (item) ->
+ itemEl = $(item)
+
+ task = itemEl.scope().task
+ newIndex = itemEl.index()
+
+ scope.$apply () ->
+ callback(scope, {task: task, newIndex: newIndex})
+
+ scroll = autoScroll(window, {
+ margin: 20,
+ pixels: 30,
+ scrollWhenOutside: true,
+ autoScroll: () ->
+ return this.down && drake.dragging
+ })
+
+ scope.$on "$destroy", ->
+ el.off()
+ drake.destroy()
+
+ return {
+ link: link
+ }
+
+TasksSortableDirective.$inject = [
+ "$parse",
+ "tgProjectService"
+]
+
+angular.module("taigaComponents").directive("tgTasksSortable", TasksSortableDirective)
\ No newline at end of file
diff --git a/app/partials/includes/modules/related-tasks.jade b/app/partials/includes/modules/related-tasks.jade
index 84f09df7..b6fd5811 100644
--- a/app/partials/includes/modules/related-tasks.jade
+++ b/app/partials/includes/modules/related-tasks.jade
@@ -5,11 +5,12 @@ section.related-tasks(
.related-tasks-header
span.related-tasks-title(translate="COMMON.RELATED_TASKS")
div(tg-related-task-create-button)
- .related-tasks-body
+ .related-tasks-body(tg-tasks-sortable="reorderTask(task, newIndex)")
.row.single-related-task.js-related-task(
ng-repeat="task in tasks"
ng-class="{closed: task.is_closed, blocked: task.is_blocked, iocaine: task.is_iocaine}"
tg-related-task-row
+ tg-bind-scope
ng-model="task"
)
div(tg-related-task-create-form)
diff --git a/app/partials/task/related-task-row-edit.jade b/app/partials/task/related-task-row-edit.jade
index 893b7b27..a842cb04 100644
--- a/app/partials/task/related-task-row-edit.jade
+++ b/app/partials/task/related-task-row-edit.jade
@@ -1,3 +1,7 @@
+.task-reorder
+ tg-svg.icon-drag(
+ svg-icon="icon-drag"
+ )
.task-name
input(
type='text'
diff --git a/app/partials/task/related-task-row.jade b/app/partials/task/related-task-row.jade
index e2fab2e2..4e976f38 100644
--- a/app/partials/task/related-task-row.jade
+++ b/app/partials/task/related-task-row.jade
@@ -1,3 +1,10 @@
+.task-reorder
+ <% if(perms.modify_task) { %>
+ tg-svg.icon-drag(
+ svg-icon="icon-drag"
+ )
+ <% } %>
+
.task-name
a.clickable(
tg-nav="project-tasks-detail:project=project.slug,ref=task.ref")
diff --git a/app/styles/modules/common/related-tasks.scss b/app/styles/modules/common/related-tasks.scss
index a2762c97..215209df 100644
--- a/app/styles/modules/common/related-tasks.scss
+++ b/app/styles/modules/common/related-tasks.scss
@@ -65,6 +65,25 @@
width: 150px;
}
}
+ .single-related-task {
+ &:hover {
+ background: rgba($primary-light, .05);
+ .icon-drag {
+ opacity: 1;
+ }
+ }
+ .task-reorder {
+ display: flex;
+ margin-right: 1rem;
+ }
+ .icon-drag {
+ @include svg-size(.75rem);
+ cursor: move;
+ fill: $whitish;
+ opacity: 0;
+ transition: opacity .1s;
+ }
+ }
.related-task-create-form {
padding: 0;
&.active {