diff --git a/app/modules/epics/epics.service.coffee b/app/modules/epics/epics.service.coffee index 09eb9a96..27e8f0b2 100644 --- a/app/modules/epics/epics.service.coffee +++ b/app/modules/epics/epics.service.coffee @@ -77,6 +77,28 @@ class EpicsService .then () => @.fetchEpics() + reorderRelatedUserstory: (epic, epicUserstories, userstory, newIndex) -> + withoutMoved = epicUserstories.filter (it) => it.get('id') != userstory.get('id') + beforeDestination = withoutMoved.slice(0, newIndex) + + previous = beforeDestination.last() + newOrder = if !previous then 0 else previous.get('epic_order') + 1 + + previousWithTheSameOrder = beforeDestination.filter (it) => + it.get('epic_order') == previous.get('epic_order') + + setOrders = Immutable.OrderedMap previousWithTheSameOrder.map (it) => + [it.get('id'), it.get('epic_order')] + + data = { + order: newOrder + } + epicId = epic.get('id') + userstoryId = userstory.get('id') + return @resources.epics.reorderRelatedUserstory(epicId, userstoryId, data, setOrders) + .then () => + return @.listRelatedUserStories(epic) + updateEpicStatus: (epic, statusId) -> data = { status: statusId, diff --git a/app/modules/epics/related-userstories/related-userstories-controller.coffee b/app/modules/epics/related-userstories/related-userstories-controller.coffee index 8042fa8f..4b9be953 100644 --- a/app/modules/epics/related-userstories/related-userstories-controller.coffee +++ b/app/modules/epics/related-userstories/related-userstories-controller.coffee @@ -20,9 +20,9 @@ module = angular.module("taigaEpics") class RelatedUserStoriesController - @.$inject = ["tgResources"] + @.$inject = ["tgResources", "tgEpicsService"] - constructor: (@rs) -> + constructor: (@rs, @epicsService) -> @.sectionName = "Epics" @.showCreateRelatedUserstoriesLightbox = false @@ -30,4 +30,8 @@ class RelatedUserStoriesController @rs.userstories.listInEpic(@.epic.get('id')).then (data) => @.userstories = data + reorderRelatedUserstory: (us, newIndex) -> + @epicsService.reorderRelatedUserstory(@.epic, @.userstories, us, newIndex).then (userstories) => + @.userstories = userstories + module.controller("RelatedUserStoriesCtrl", RelatedUserStoriesController) diff --git a/app/modules/epics/related-userstories/related-userstories-sortable/related-userstories-sortable.directive.coffee b/app/modules/epics/related-userstories/related-userstories-sortable/related-userstories-sortable.directive.coffee new file mode 100644 index 00000000..1989e7d5 --- /dev/null +++ b/app/modules/epics/related-userstories/related-userstories-sortable/related-userstories-sortable.directive.coffee @@ -0,0 +1,65 @@ +### +# Copyright (C) 2014-2016 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: related-userstories-sortable.directive.coffee +### + +module = angular.module('taigaEpics') + +RelatedUserstoriesSortableDirective = ($parse, projectService) -> + link = (scope, el, attrs) -> + return if not projectService.hasPermission("modify_epic") + + callback = $parse(attrs.tgRelatedUserstoriesSortable) + + drake = dragula([el[0]], { + copySortSource: false + copy: false + mirrorContainer: el[0] + moves: (item) -> + return $(item).is('tg-related-userstory-row') + }) + + drake.on 'dragend', (item) -> + itemEl = $(item) + us = itemEl.scope().us + newIndex = itemEl.index() + + scope.$apply () -> + callback(scope, {us: us, 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 + } + +RelatedUserstoriesSortableDirective.$inject = [ + "$parse", + "tgProjectService" +] + +module.directive("tgRelatedUserstoriesSortable", RelatedUserstoriesSortableDirective) diff --git a/app/modules/epics/related-userstories/related-userstories.controller.spec.coffee b/app/modules/epics/related-userstories/related-userstories.controller.spec.coffee index 3498e404..2543b085 100644 --- a/app/modules/epics/related-userstories/related-userstories.controller.spec.coffee +++ b/app/modules/epics/related-userstories/related-userstories.controller.spec.coffee @@ -34,6 +34,7 @@ describe "RelatedUserStories", -> _mockTgEpicsService = () -> mocks.tgEpicsService = { + reorderRelatedUserstory: sinon.stub() } provide.value "tgEpicsService", mocks.tgEpicsService @@ -69,3 +70,37 @@ describe "RelatedUserStories", -> ctrl.loadRelatedUserstories().then () -> expect(ctrl.userstories).is.equal(userstories) done() + + it "reorderRelatedUserstory", (done) -> + ctrl = controller "RelatedUserStoriesCtrl" + userstories = Immutable.fromJS([ + { + id: 1 + }, + { + id: 2 + } + ]) + + reorderedUserstories = Immutable.fromJS([ + { + id: 2 + }, + { + id: 1 + } + ]) + + ctrl.epic = Immutable.fromJS({ + id: 66 + }) + + + promise = mocks.tgEpicsService.reorderRelatedUserstory + .withArgs(ctrl.epic, ctrl.userstories, userstories.get(1), 0) + .promise() + .resolve(reorderedUserstories) + + ctrl.reorderRelatedUserstory(userstories.get(1), 0).then () -> + expect(ctrl.userstories).is.equal(reorderedUserstories) + done() diff --git a/app/modules/epics/related-userstories/related-userstories.jade b/app/modules/epics/related-userstories/related-userstories.jade index ecf642de..35a848b7 100644 --- a/app/modules/epics/related-userstories/related-userstories.jade +++ b/app/modules/epics/related-userstories/related-userstories.jade @@ -10,14 +10,17 @@ section.related-userstories load-related-userstories="vm.loadRelatedUserstories()" ) - .related-userstories-body - div(tg-repeat="us in vm.userstories track by us.get('id')") - tg-related-userstory-row.row( - ng-class="{closed: us.get('is_closed'), blocked: us.get('is_blocked')}" - userstory="us" - epic="vm.epic" - project="vm.project" - load-related-userstories="vm.loadRelatedUserstories()" - ) + .related-userstories-body( + tg-related-userstories-sortable="vm.reorderRelatedUserstory(us, newIndex)" + ) + tg-related-userstory-row.row( + tg-repeat="us in vm.userstories track by us.get('id')" + ng-class="{closed: us.get('is_closed'), blocked: us.get('is_blocked')}" + userstory="us" + epic="vm.epic" + project="vm.project" + load-related-userstories="vm.loadRelatedUserstories()" + tg-bind-scope + ) - div(tg-related-userstories-create-form) + div(tg-related-userstories-create-form) diff --git a/app/modules/epics/related-userstories/related-userstories.scss b/app/modules/epics/related-userstories/related-userstories.scss index 62bc0b46..67ba81a0 100644 --- a/app/modules/epics/related-userstories/related-userstories.scss +++ b/app/modules/epics/related-userstories/related-userstories.scss @@ -36,112 +36,4 @@ .related-userstories-body { width: 100%; - .row { - @include font-size(small); - align-items: center; - border-bottom: 1px solid $whitish; - display: flex; - padding: .5rem 0 .5rem .5rem; - &:hover { - .userstory-settings { - opacity: 1; - transition: all .2s ease-in; - } - } - .userstory-name { - flex: 1; - } - .userstory-settings { - flex-shrink: 0; - width: 60px; - } - .status { - flex-shrink: 0; - width: 125px; - } - .assigned-to-column { - flex-shrink: 0; - width: 150px; - img { - flex-basis: 35px; - // width & height they are only required for IE - height: 35px; - width: 35px; - } - } - .project { - flex-basis: 100px; - img { - width: 40px; - } - } - } - - .userstory-name { - display: flex; - margin-right: 1rem; - - span { - margin-right: .25rem; - } - } - .status { - position: relative; - } - .closed { - border-left: 10px solid $whitish; - color: $whitish; - a, - svg { - fill: $whitish; - } - .userstory-name a { - color: $whitish; - text-decoration: line-through; - - } - } - .blocked { - background: rgba($red-light, .2); - border-left: 10px solid $red-light; - } - .userstory-settings { - align-items: center; - display: flex; - opacity: 0; - svg { - @include svg-size(1.1rem); - fill: $gray-light; - margin-right: .5rem; - transition: fill .2s ease-in; - &:hover { - fill: $gray; - } - } - a { - &:hover { - cursor: pointer; - } - } - } - .delete-userstory { - &:hover { - .icon-trash { - fill: $red-light; - } - } - } - .avatar { - align-items: center; - display: flex; - img { - flex-basis: 35px; - // width & height they are only required for IE - height: 35px; - width: 35px; - } - figcaption { - margin-left: .5rem; - } - } } diff --git a/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.jade b/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.jade index 7c7b8a41..c7790332 100644 --- a/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.jade +++ b/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.jade @@ -1,3 +1,7 @@ +tg-svg.icon-drag( + svg-icon="icon-drag" +) + .userstory-name - var hash = "#"; a( diff --git a/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.scss b/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.scss new file mode 100644 index 00000000..64b82340 --- /dev/null +++ b/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.scss @@ -0,0 +1,112 @@ +tg-related-userstory-row { + @include font-size(small); + align-items: center; + border-bottom: 1px solid $whitish; + display: flex; + padding: .5rem 0 .5rem .5rem; + &:hover { + background: rgba($primary-light, .05); + .userstory-settings { + opacity: 1; + transition: all .2s ease-in; + } + .icon-drag { + opacity: 1; + } + } + .icon-drag { + @include svg-size(.75rem); + cursor: move; + fill: $whitish; + opacity: 0; + transition: opacity .1s; + } + .status { + flex-shrink: 0; + position: relative; + width: 125px; + } + .assigned-to-column { + flex-shrink: 0; + width: 150px; + img { + flex-basis: 35px; + // width & height they are only required for IE + height: 35px; + width: 35px; + } + } + .project { + flex-basis: 100px; + img { + width: 40px; + } + } + .userstory-name { + display: flex; + flex: 1; + margin-right: 1rem; + + span { + margin-right: .25rem; + } + } + .closed { + border-left: 10px solid $whitish; + color: $whitish; + a, + svg { + fill: $whitish; + } + .userstory-name a { + color: $whitish; + text-decoration: line-through; + + } + } + .blocked { + background: rgba($red-light, .2); + border-left: 10px solid $red-light; + } + .userstory-settings { + align-items: center; + display: flex; + flex-shrink: 0; + opacity: 0; + width: 60px; + svg { + @include svg-size(1.1rem); + fill: $gray-light; + margin-right: .5rem; + transition: fill .2s ease-in; + &:hover { + fill: $gray; + } + } + a { + &:hover { + cursor: pointer; + } + } + } + .delete-userstory { + &:hover { + .icon-trash { + fill: $red-light; + } + } + } + .avatar { + align-items: center; + display: flex; + img { + flex-basis: 35px; + // width & height they are only required for IE + height: 35px; + width: 35px; + } + figcaption { + margin-left: .5rem; + } + } +} diff --git a/app/modules/resources/epics-resource.service.coffee b/app/modules/resources/epics-resource.service.coffee index a63029c9..293830b9 100644 --- a/app/modules/resources/epics-resource.service.coffee +++ b/app/modules/resources/epics-resource.service.coffee @@ -70,6 +70,13 @@ Resource = (urlsService, http) -> return http.post(url, params) + service.reorderRelatedUserstory = (epicId, userstoryId, data, setOrders) -> + url = urlsService.resolve("epic-related-userstories", epicId) + "/#{userstoryId}" + + options = {"headers": {"set-orders": JSON.stringify(setOrders)}} + + return http.patch(url, data, null, options) + service.bulkCreateRelatedUserStories = (epicId, projectId, bulk_userstories) -> url = urlsService.resolve("epic-related-userstories-bulk-create", epicId)