From d501d53fae9c6438c0fe65b35d55408f026ce11d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Fri, 31 Aug 2018 09:59:15 +0200 Subject: [PATCH] Reorder tasks withing a US --- app/coffee/modules/related-tasks.coffee | 11 +++- app/coffee/modules/resources/tasks.coffee | 10 ++- app/coffee/modules/userstories/detail.coffee | 62 +++++++++++++++--- .../tasks-sortable.directive.coffee | 64 +++++++++++++++++++ .../includes/modules/related-tasks.jade | 3 +- app/partials/task/related-task-row-edit.jade | 4 ++ app/partials/task/related-task-row.jade | 7 ++ app/styles/modules/common/related-tasks.scss | 19 ++++++ 8 files changed, 166 insertions(+), 14 deletions(-) create mode 100644 app/modules/components/tasks-sortable/tasks-sortable.directive.coffee 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 {