commit
763e5d0879
|
@ -4,6 +4,7 @@
|
|||
|
||||
### Features
|
||||
- Not showing closed milestones by default in backlog view.
|
||||
- In kanban view an archived user story status doesn't show his content by default.
|
||||
|
||||
### Misc
|
||||
- Lots of small and not so small bugfixes.
|
||||
|
|
|
@ -136,6 +136,7 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame) ->
|
|||
$scope.newValue = {
|
||||
"name": ""
|
||||
"is_closed": false
|
||||
"is_archived": false
|
||||
}
|
||||
|
||||
initializeNewValue()
|
||||
|
|
|
@ -73,6 +73,7 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
|||
@scope.sectionName = "Kanban"
|
||||
@scope.statusViewModes = {}
|
||||
@.initializeEventHandlers()
|
||||
|
||||
promise = @.loadInitialData()
|
||||
|
||||
# On Success
|
||||
|
@ -101,6 +102,8 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
|||
|
||||
@scope.$on("assigned-to:added", @.onAssignedToChanged)
|
||||
@scope.$on("kanban:us:move", @.moveUs)
|
||||
@scope.$on("kanban:show-userstories-for-status", @.loadUserStoriesForStatus)
|
||||
@scope.$on("kanban:hide-userstories-for-status", @.hideUserStoriesForStatus)
|
||||
|
||||
# Template actions
|
||||
|
||||
|
@ -127,22 +130,44 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
|||
@scope.project.tags_colors = tags_colors
|
||||
|
||||
loadUserstories: ->
|
||||
return @rs.userstories.listAll(@scope.projectId).then (userstories) =>
|
||||
@scope.userstories = userstories
|
||||
@scope.usByStatus = _.groupBy(userstories, "status")
|
||||
params = {
|
||||
status__is_archived: false
|
||||
}
|
||||
|
||||
return @rs.userstories.listAll(@scope.projectId, params).then (userstories) =>
|
||||
@scope.userstories = userstories
|
||||
|
||||
usByStatus = _.groupBy(userstories, "status")
|
||||
for status in @scope.usStatusList
|
||||
if not @scope.usByStatus[status.id]?
|
||||
@scope.usByStatus[status.id] = []
|
||||
@scope.usByStatus[status.id] = _.sortBy(@scope.usByStatus[status.id], "kanban_order")
|
||||
if not usByStatus[status.id]?
|
||||
usByStatus[status.id] = []
|
||||
|
||||
# Must preserve the archived columns if loaded
|
||||
if status.is_archived and @scope.usByStatus?
|
||||
usByStatus[status.id] = @scope.usByStatus[status.id]
|
||||
|
||||
usByStatus[status.id] = _.sortBy(usByStatus[status.id], "kanban_order")
|
||||
|
||||
@scope.usByStatus = usByStatus
|
||||
|
||||
# The broadcast must be executed when the DOM has been fully reloaded.
|
||||
# We can't assure when this exactly happens so we need a defer
|
||||
scopeDefer @scope, =>
|
||||
@scope.$broadcast("userstories:loaded")
|
||||
@scope.$broadcast("userstories:loaded", userstories)
|
||||
|
||||
return userstories
|
||||
|
||||
loadUserStoriesForStatus: (ctx, statusId) ->
|
||||
params = { status: statusId }
|
||||
return @rs.userstories.listAll(@scope.projectId, params).then (userstories) =>
|
||||
@scope.usByStatus[statusId] = _.sortBy(userstories, "kanban_order")
|
||||
@scope.$broadcast("kanban:shown-userstories-for-status", statusId, userstories)
|
||||
return userstories
|
||||
|
||||
hideUserStoriesForStatus: (ctx, statusId) ->
|
||||
@scope.usByStatus[statusId] = []
|
||||
@scope.$broadcast("kanban:hidden-userstories-for-status", statusId)
|
||||
|
||||
loadKanban: ->
|
||||
return @q.all([
|
||||
@.refreshTagsColors(),
|
||||
|
@ -153,6 +178,7 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
|||
return @rs.projects.getBySlug(@params.pslug).then (project) =>
|
||||
@scope.projectId = project.id
|
||||
@scope.project = project
|
||||
@scope.projectId = project.id
|
||||
@scope.points = _.sortBy(project.points, "order")
|
||||
@scope.pointsById = groupBy(project.points, (x) -> x.id)
|
||||
@scope.usStatusById = groupBy(project.us_statuses, (x) -> x.id)
|
||||
|
@ -213,22 +239,22 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
|||
|
||||
return items
|
||||
|
||||
moveUs: (ctx, us, statusId, index) ->
|
||||
if us.status != statusId
|
||||
moveUs: (ctx, us, oldStatusId, newStatusId, index) ->
|
||||
if oldStatusId != newStatusId
|
||||
# Remove us from old status column
|
||||
r = @scope.usByStatus[us.status].indexOf(us)
|
||||
@scope.usByStatus[us.status].splice(r, 1)
|
||||
r = @scope.usByStatus[oldStatusId].indexOf(us)
|
||||
@scope.usByStatus[oldStatusId].splice(r, 1)
|
||||
|
||||
# Add us to new status column.
|
||||
@scope.usByStatus[statusId].splice(index, 0, us)
|
||||
us.status = statusId
|
||||
@scope.usByStatus[newStatusId].splice(index, 0, us)
|
||||
us.status = newStatusId
|
||||
else
|
||||
r = @scope.usByStatus[statusId].indexOf(us)
|
||||
@scope.usByStatus[statusId].splice(r, 1)
|
||||
@scope.usByStatus[statusId].splice(index, 0, us)
|
||||
r = @scope.usByStatus[newStatusId].indexOf(us)
|
||||
@scope.usByStatus[newStatusId].splice(r, 1)
|
||||
@scope.usByStatus[newStatusId].splice(index, 0, us)
|
||||
|
||||
itemsToSave = @.resortUserStories(@scope.usByStatus[statusId])
|
||||
@scope.usByStatus[statusId] = _.sortBy(@scope.usByStatus[statusId], "kanban_order")
|
||||
itemsToSave = @.resortUserStories(@scope.usByStatus[newStatusId])
|
||||
@scope.usByStatus[newStatusId] = _.sortBy(@scope.usByStatus[newStatusId], "kanban_order")
|
||||
|
||||
# Persist the userstory
|
||||
promise = @repo.save(us)
|
||||
|
@ -296,6 +322,104 @@ KanbanColumnHeightFixerDirective = ->
|
|||
|
||||
module.directive("tgKanbanColumnHeightFixer", KanbanColumnHeightFixerDirective)
|
||||
|
||||
|
||||
#############################################################################
|
||||
## Kanban Archived Status Column Header Control
|
||||
#############################################################################
|
||||
|
||||
KanbanArchivedStatusHeaderDirective = ($rootscope) ->
|
||||
#TODO: i18N
|
||||
showArchivedText = "Show archived"
|
||||
hideArchivedText = "Hide archived"
|
||||
|
||||
link = ($scope, $el, $attrs) ->
|
||||
status = $scope.$eval($attrs.tgKanbanArchivedStatusHeader)
|
||||
hidden = true
|
||||
|
||||
$scope.class = "icon icon-open-eye"
|
||||
$scope.title = showArchivedText
|
||||
|
||||
$el.on "click", (event) ->
|
||||
hidden = not hidden
|
||||
|
||||
$scope.$apply ->
|
||||
if hidden
|
||||
$scope.class = "icon icon-open-eye"
|
||||
$scope.title = showArchivedText
|
||||
$rootscope.$broadcast("kanban:hide-userstories-for-status", status.id)
|
||||
|
||||
else
|
||||
$scope.class = "icon icon-closed-eye"
|
||||
$scope.title = hideArchivedText
|
||||
$rootscope.$broadcast("kanban:show-userstories-for-status", status.id)
|
||||
|
||||
$scope.$on "$destroy", ->
|
||||
$el.off()
|
||||
|
||||
return {link:link}
|
||||
|
||||
module.directive("tgKanbanArchivedStatusHeader", [ "$rootScope", KanbanArchivedStatusHeaderDirective])
|
||||
|
||||
|
||||
#############################################################################
|
||||
## Kanban Archived Status Column Intro Directive
|
||||
#############################################################################
|
||||
|
||||
KanbanArchivedStatusIntroDirective = ->
|
||||
# TODO: i18n
|
||||
hiddenUserStoriexText = "The user stories in this status are hidden by default"
|
||||
userStories = []
|
||||
|
||||
link = ($scope, $el, $attrs) ->
|
||||
status = $scope.$eval($attrs.tgKanbanArchivedStatusIntro)
|
||||
$el.text(hiddenUserStoriexText)
|
||||
|
||||
updateIntroText = ->
|
||||
if userStories.length > 0
|
||||
$el.text("")
|
||||
else
|
||||
$el.text(hiddenUserStoriexText)
|
||||
|
||||
$scope.$on "kanban:us:move", (ctx, itemUs, oldStatusId, newStatusId, itemIndex) ->
|
||||
# The destination columnd is this one
|
||||
if status.id == newStatusId
|
||||
# Reorder
|
||||
if status.id == oldStatusId
|
||||
r = userStories.indexOf(itemUs)
|
||||
userStories.splice(r, 1)
|
||||
userStories.splice(itemIndex, 0, itemUs)
|
||||
|
||||
# Archiving user story
|
||||
else
|
||||
itemUs.isArchived = true
|
||||
userStories.splice(itemIndex, 0, itemUs)
|
||||
|
||||
# Unarchiving user story
|
||||
else if status.id == oldStatusId
|
||||
itemUs.isArchived = false
|
||||
r = userStories.indexOf(itemUs)
|
||||
userStories.splice(r, 1)
|
||||
|
||||
updateIntroText()
|
||||
|
||||
$scope.$on "kanban:shown-userstories-for-status", (ctx, statusId, userStoriesLoaded) ->
|
||||
if statusId == status.id
|
||||
userStories = _.filter(userStoriesLoaded, (us) -> us.status == status.id)
|
||||
updateIntroText()
|
||||
|
||||
$scope.$on "kanban:hidden-userstories-for-status", (ctx, statusId) ->
|
||||
if statusId == status.id
|
||||
userStories = []
|
||||
updateIntroText()
|
||||
|
||||
$scope.$on "$destroy", ->
|
||||
$el.off()
|
||||
|
||||
return {link:link}
|
||||
|
||||
module.directive("tgKanbanArchivedStatusIntro", KanbanArchivedStatusIntroDirective)
|
||||
|
||||
|
||||
#############################################################################
|
||||
## Kanban User Story Directive
|
||||
#############################################################################
|
||||
|
|
|
@ -73,7 +73,7 @@ KanbanSortableDirective = ($repo, $rs, $rootscope) ->
|
|||
deleteElement(itemEl)
|
||||
|
||||
$scope.$apply ->
|
||||
$rootscope.$broadcast("kanban:us:move", itemUs, newStatusId, itemIndex)
|
||||
$rootscope.$broadcast("kanban:us:move", itemUs, itemUs.status, newStatusId, itemIndex)
|
||||
|
||||
ui.item.find('a').removeClass('noclick')
|
||||
|
||||
|
|
|
@ -1,14 +1,23 @@
|
|||
div.kanban-tagline(tg-colorize-tags="us.tags", tg-colorize-tags-type="kanban")
|
||||
div.kanban-task-inner
|
||||
div(tg-kanban-user-avatar="us.assigned_to", ng-model="us")
|
||||
div.task-text
|
||||
div.kanban-tagline(tg-colorize-tags="us.tags", tg-colorize-tags-type="kanban", ng-hide="us.isArchived")
|
||||
div.kanban-task-inner(ng-class="{'task-archived': us.isArchived}")
|
||||
div(tg-kanban-user-avatar="us.assigned_to", ng-model="us", ng-hide="us.isArchived")
|
||||
div.task-text(ng-hide="us.isArchived")
|
||||
a.task-assigned(href="", title="Assign User Story")
|
||||
span.task-num(tg-bo-ref="us.ref")
|
||||
a.task-name(href="", title="See user story detail", ng-bind="us.subject",
|
||||
tg-nav="project-userstories-detail:project=project.slug,ref=us.ref")
|
||||
|
||||
p.task-points(href="", title="Total Us points")
|
||||
span(ng-if="us.total_points !== null", ng-bind="us.total_points")
|
||||
span(ng-if="us.total_points !== null") points
|
||||
span(ng-if="us.total_points === null") Not estimated
|
||||
a.icon.icon-edit(tg-check-permission="modify_us", href="", title="Edit")
|
||||
a.icon.icon-drag-h(tg-check-permission="modify_us", href="", title="Drag&Drop")
|
||||
|
||||
div.task-archived-text(ng-show="us.isArchived")
|
||||
p You have archived
|
||||
p
|
||||
span.task-num(tg-bo-ref="us.ref")
|
||||
span.task-name(ng-bind="us.subject")
|
||||
p Drag & drop again to undo
|
||||
|
||||
a.icon.icon-edit(tg-check-permission="modify_us", href="", title="Edit", ng-hide="us.isArchived")
|
||||
a.icon.icon-drag-h(tg-check-permission="modify_us", href="", title="Drag&Drop", ng-hide="us.isArchived")
|
||||
|
|
|
@ -5,6 +5,7 @@ section.colors-table
|
|||
div.status-name Name
|
||||
div.status-slug Slug
|
||||
div.is-closed-column Is closed?
|
||||
div.is-archived-column Is archived?
|
||||
div.status-wip-limit WIP Limit
|
||||
div.options-column
|
||||
|
||||
|
@ -26,6 +27,9 @@ section.colors-table
|
|||
div.is-closed-column
|
||||
div.icon.icon-check-square(ng-show="value.is_closed")
|
||||
|
||||
div.is-archived-column
|
||||
div.icon.icon-check-square(ng-show="value.is_archived")
|
||||
|
||||
div.status-wip-limit
|
||||
span {{ value.wip_limit }}
|
||||
|
||||
|
@ -45,6 +49,11 @@ section.colors-table
|
|||
div.is-closed-column
|
||||
select(name="is_closed", ng-model="value.is_closed", data-required="true",
|
||||
ng-options="e.id as e.name for e in [{'id':true, 'name':'Yes'},{'id':false, 'name': 'No'}]")
|
||||
|
||||
div.is-archived-column
|
||||
select(name="is_archived", ng-model="value.is_archived", data-required="true",
|
||||
ng-options="e.id as e.name for e in [{'id':true, 'name':'Yes'},{'id':false, 'name': 'No'}]")
|
||||
|
||||
div.status-wip-limit
|
||||
input(name="wip_limit", type="number", placeholder="WIP Limit",
|
||||
ng-model="value.wip_limit", data-type="digits")
|
||||
|
@ -67,6 +76,10 @@ section.colors-table
|
|||
select(name="is_closed", ng-model="newValue.is_closed", data-required="true",
|
||||
ng-options="e.id as e.name for e in [{'id':true, 'name':'Yes'},{'id':false, 'name': 'No'}]")
|
||||
|
||||
div.is-archived-column
|
||||
select(name="is_archived", ng-model="newValue.is_archived", data-required="true",
|
||||
ng-options="e.id as e.name for e in [{'id':true, 'name':'Yes'},{'id':false, 'name': 'No'}]")
|
||||
|
||||
div.status-wip-limit
|
||||
input(name="wip_limit", type="number", placeholder="WIP Limit",
|
||||
ng-model="newValue.wip_limit", data-type="digits")
|
||||
|
|
|
@ -5,11 +5,9 @@ div.kanban-table(tg-kanban-squish-column)
|
|||
span(tg-bo-bind="s.name")
|
||||
div.options
|
||||
|
||||
|
||||
a.icon.icon-vfold.hfold(href="", ng-click='foldStatus(s)' title="Fold Column", ng-class='{hidden:folds[s.id]}')
|
||||
a.icon.icon-vunfold.hunfold(href="", ng-click='foldStatus(s)', title="Unfold Column", ng-class='{hidden:!folds[s.id]}')
|
||||
|
||||
|
||||
a.icon.icon-vfold(href="", title="Fold Cards",
|
||||
ng-class="{hidden:statusViewModes[s.id] == 'minimized'}",
|
||||
ng-click="ctrl.updateStatusViewMode(s.id, 'minimized')")
|
||||
|
@ -19,18 +17,30 @@ div.kanban-table(tg-kanban-squish-column)
|
|||
|
||||
a.icon.icon-plus(href="", title="Add New User Story",
|
||||
ng-click="ctrl.addNewUs('standard', s.id)",
|
||||
tg-check-permission="add_us")
|
||||
tg-check-permission="add_us",
|
||||
ng-hide="s.is_archived")
|
||||
|
||||
a.icon.icon-bulk(href="", title="Add New bulk",
|
||||
ng-click="ctrl.addNewUs('bulk', s.id)",
|
||||
tg-check-permission="add_us")
|
||||
tg-check-permission="add_us",
|
||||
ng-hide="s.is_archived")
|
||||
|
||||
a(href="",
|
||||
ng-attr-title="{{title}}",
|
||||
ng-class="class"
|
||||
ng-if="s.is_archived",
|
||||
tg-kanban-archived-status-header="s")
|
||||
|
||||
div.kanban-table-body
|
||||
div.kanban-table-inner(tg-kanban-row-width-fixer)
|
||||
div.kanban-uses-box.task-column(ng-class='{vfold:folds[s.id]}', ng-repeat="s in usStatusList track by s.id",
|
||||
div.kanban-uses-box.task-column(ng-class='{vfold:folds[s.id]}',
|
||||
ng-repeat="s in usStatusList track by s.id",
|
||||
tg-kanban-sortable,
|
||||
tg-kanban-wip-limit="s.wip_limit",
|
||||
tg-kanban-column-height-fixer)
|
||||
|
||||
div.kanban-task(ng-repeat="us in usByStatus[s.id] track by us.id",
|
||||
tg-kanban-userstory, ng-model="us",
|
||||
ng-class="ctrl.getCardClass(s.id)")
|
||||
|
||||
div.kanban-column-intro(ng-if="s.is_archived", tg-kanban-archived-status-intro="s")
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
margin: .2rem;
|
||||
position: relative;
|
||||
&:last-child {
|
||||
margin: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
&:hover {
|
||||
.icon-edit,
|
||||
|
@ -94,10 +94,37 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.kanban-task-maximized {
|
||||
.kanban-task-inner {
|
||||
padding: 1rem 1rem 2rem;
|
||||
}
|
||||
.task-archived {
|
||||
background: darken($whitish, 5%);
|
||||
padding: .5rem;
|
||||
text-align: left;
|
||||
transition: background .3s linear;
|
||||
&:hover {
|
||||
background: darken($whitish, 8%);
|
||||
transition: background .3s linear;
|
||||
}
|
||||
.task-archived-text {
|
||||
flex: 1;
|
||||
}
|
||||
span {
|
||||
color: $gray-light;
|
||||
}
|
||||
p {
|
||||
@extend %small;
|
||||
color: $gray-light;
|
||||
margin: 0;
|
||||
&:last-child {
|
||||
color: $gray;
|
||||
margin: .5rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
.avatar {
|
||||
@include table-flex-child($flex-basis: 50px);
|
||||
}
|
||||
|
@ -134,6 +161,32 @@
|
|||
.kanban-task-inner {
|
||||
padding: 0 .3rem;
|
||||
}
|
||||
.task-archived {
|
||||
@extend %small;
|
||||
background: darken($whitish, 5%);
|
||||
padding: .3rem;
|
||||
text-align: left;
|
||||
.task-archived-text {
|
||||
flex: 1;
|
||||
}
|
||||
span {
|
||||
color: $gray-light;
|
||||
}
|
||||
.task-name {
|
||||
display: inline-block;
|
||||
max-width: 70%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
p {
|
||||
color: $gray-light;
|
||||
margin: 0;
|
||||
&:last-child {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.avatar {
|
||||
@include table-flex-child($flex-basis: 40px);
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
max-width: 100px;
|
||||
position: relative;
|
||||
}
|
||||
.is-archived-column,
|
||||
.is-closed-column,
|
||||
.options-column,
|
||||
.status-wip-limit {
|
||||
|
@ -59,6 +60,11 @@
|
|||
opacity: 0;
|
||||
text-align: right;
|
||||
}
|
||||
.is-archived-column {
|
||||
max-width: 130px;
|
||||
padding: 0 0 0 10px;
|
||||
text-align: center;
|
||||
}
|
||||
.is-closed-column {
|
||||
max-width: 130px;
|
||||
text-align: center;
|
||||
|
|
|
@ -22,6 +22,8 @@ $column-margin: 0 10px 0 0;
|
|||
.icon-bulk,
|
||||
.icon-vfold,
|
||||
.icon-vunfold,
|
||||
.icon-open-eye,
|
||||
.icon-closed-eye,
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
|
@ -96,6 +98,15 @@ $column-margin: 0 10px 0 0;
|
|||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
.kanban-column-intro {
|
||||
@extend %bold;
|
||||
@extend %small;
|
||||
color: $gray-light;
|
||||
margin: 1rem 2rem;
|
||||
&.active {
|
||||
color: $blackish;
|
||||
}
|
||||
}
|
||||
.kanban-wip-limit {
|
||||
background: $red;
|
||||
border-radius: 2px;
|
||||
|
|
Loading…
Reference in New Issue