Epic detail page
parent
1d22477410
commit
a275d88506
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 = () ->
|
_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()
|
||||||
|
|
|
@ -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(
|
)
|
||||||
ng-class="{closed: us.get('is_closed'), blocked: us.get('is_blocked')}"
|
tg-related-userstory-row.row(
|
||||||
userstory="us"
|
tg-repeat="us in vm.userstories track by us.get('id')"
|
||||||
epic="vm.epic"
|
ng-class="{closed: us.get('is_closed'), blocked: us.get('is_blocked')}"
|
||||||
project="vm.project"
|
userstory="us"
|
||||||
load-related-userstories="vm.loadRelatedUserstories()"
|
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 {
|
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
tg-svg.icon-drag(
|
||||||
|
svg-icon="icon-drag"
|
||||||
|
)
|
||||||
|
|
||||||
.userstory-name
|
.userstory-name
|
||||||
- var hash = "#";
|
- var hash = "#";
|
||||||
a(
|
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)
|
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)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue