Epic detail page
parent
1d22477410
commit
a275d88506
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
tg-svg.icon-drag(
|
||||
svg-icon="icon-drag"
|
||||
)
|
||||
|
||||
.userstory-name
|
||||
- var hash = "#";
|
||||
a(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in New Issue