Epic detail page

stable
Alejandro Alonso 2016-09-08 15:16:03 +02:00 committed by David Barragán Merino
parent 1d22477410
commit a275d88506
9 changed files with 264 additions and 120 deletions

View File

@ -77,6 +77,28 @@ class EpicsService
.then () => .then () =>
@.fetchEpics() @.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) -> updateEpicStatus: (epic, statusId) ->
data = { data = {
status: statusId, status: statusId,

View File

@ -20,9 +20,9 @@
module = angular.module("taigaEpics") module = angular.module("taigaEpics")
class RelatedUserStoriesController class RelatedUserStoriesController
@.$inject = ["tgResources"] @.$inject = ["tgResources", "tgEpicsService"]
constructor: (@rs) -> constructor: (@rs, @epicsService) ->
@.sectionName = "Epics" @.sectionName = "Epics"
@.showCreateRelatedUserstoriesLightbox = false @.showCreateRelatedUserstoriesLightbox = false
@ -30,4 +30,8 @@ class RelatedUserStoriesController
@rs.userstories.listInEpic(@.epic.get('id')).then (data) => @rs.userstories.listInEpic(@.epic.get('id')).then (data) =>
@.userstories = data @.userstories = data
reorderRelatedUserstory: (us, newIndex) ->
@epicsService.reorderRelatedUserstory(@.epic, @.userstories, us, newIndex).then (userstories) =>
@.userstories = userstories
module.controller("RelatedUserStoriesCtrl", RelatedUserStoriesController) module.controller("RelatedUserStoriesCtrl", RelatedUserStoriesController)

View File

@ -0,0 +1,65 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -34,6 +34,7 @@ describe "RelatedUserStories", ->
_mockTgEpicsService = () -> _mockTgEpicsService = () ->
mocks.tgEpicsService = { mocks.tgEpicsService = {
reorderRelatedUserstory: sinon.stub()
} }
provide.value "tgEpicsService", mocks.tgEpicsService provide.value "tgEpicsService", mocks.tgEpicsService
@ -69,3 +70,37 @@ describe "RelatedUserStories", ->
ctrl.loadRelatedUserstories().then () -> ctrl.loadRelatedUserstories().then () ->
expect(ctrl.userstories).is.equal(userstories) expect(ctrl.userstories).is.equal(userstories)
done() 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()

View File

@ -10,14 +10,17 @@ section.related-userstories
load-related-userstories="vm.loadRelatedUserstories()" load-related-userstories="vm.loadRelatedUserstories()"
) )
.related-userstories-body .related-userstories-body(
div(tg-repeat="us in vm.userstories track by us.get('id')") tg-related-userstories-sortable="vm.reorderRelatedUserstory(us, newIndex)"
)
tg-related-userstory-row.row( 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')}" ng-class="{closed: us.get('is_closed'), blocked: us.get('is_blocked')}"
userstory="us" userstory="us"
epic="vm.epic" epic="vm.epic"
project="vm.project" project="vm.project"
load-related-userstories="vm.loadRelatedUserstories()" load-related-userstories="vm.loadRelatedUserstories()"
tg-bind-scope
) )
div(tg-related-userstories-create-form) div(tg-related-userstories-create-form)

View File

@ -36,112 +36,4 @@
.related-userstories-body { .related-userstories-body {
width: 100%; 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;
}
}
} }

View File

@ -1,3 +1,7 @@
tg-svg.icon-drag(
svg-icon="icon-drag"
)
.userstory-name .userstory-name
- var hash = "#"; - var hash = "#";
a( a(

View File

@ -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;
}
}
}

View File

@ -70,6 +70,13 @@ Resource = (urlsService, http) ->
return http.post(url, params) 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) -> service.bulkCreateRelatedUserStories = (epicId, projectId, bulk_userstories) ->
url = urlsService.resolve("epic-related-userstories-bulk-create", epicId) url = urlsService.resolve("epic-related-userstories-bulk-create", epicId)