Merge pull request #247 from taigaio/archived-userstory-status

Archived userstory status
stable
Jesús Espino 2015-01-15 10:20:42 +01:00
commit 763e5d0879
10 changed files with 259 additions and 31 deletions

View File

@ -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.

View File

@ -136,6 +136,7 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame) ->
$scope.newValue = {
"name": ""
"is_closed": false
"is_archived": false
}
initializeNewValue()

View File

@ -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
#############################################################################

View File

@ -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')

View File

@ -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")

View File

@ -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")

View File

@ -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")

View File

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

View File

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

View File

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