diff --git a/CHANGELOG.md b/CHANGELOG.md index 115d1776..37394547 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/app/coffee/modules/admin/project-values.coffee b/app/coffee/modules/admin/project-values.coffee index 8f70c0ce..188daba2 100644 --- a/app/coffee/modules/admin/project-values.coffee +++ b/app/coffee/modules/admin/project-values.coffee @@ -136,6 +136,7 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame) -> $scope.newValue = { "name": "" "is_closed": false + "is_archived": false } initializeNewValue() diff --git a/app/coffee/modules/kanban/main.coffee b/app/coffee/modules/kanban/main.coffee index 3e2dfbbd..f687b002 100644 --- a/app/coffee/modules/kanban/main.coffee +++ b/app/coffee/modules/kanban/main.coffee @@ -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 ############################################################################# diff --git a/app/coffee/modules/kanban/sortable.coffee b/app/coffee/modules/kanban/sortable.coffee index 0b641d01..ac86e649 100644 --- a/app/coffee/modules/kanban/sortable.coffee +++ b/app/coffee/modules/kanban/sortable.coffee @@ -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') diff --git a/app/partials/views/components/kanban-task.jade b/app/partials/views/components/kanban-task.jade index 628ab762..03739ccd 100644 --- a/app/partials/views/components/kanban-task.jade +++ b/app/partials/views/components/kanban-task.jade @@ -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") diff --git a/app/partials/views/modules/admin/project-us-status.jade b/app/partials/views/modules/admin/project-us-status.jade index 50c22641..a78f7607 100644 --- a/app/partials/views/modules/admin/project-us-status.jade +++ b/app/partials/views/modules/admin/project-us-status.jade @@ -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") diff --git a/app/partials/views/modules/kanban-table.jade b/app/partials/views/modules/kanban-table.jade index 3d006778..a3507eac 100644 --- a/app/partials/views/modules/kanban-table.jade +++ b/app/partials/views/modules/kanban-table.jade @@ -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") diff --git a/app/styles/components/kanban-task.scss b/app/styles/components/kanban-task.scss index 83dbe4b1..63778eb9 100644 --- a/app/styles/components/kanban-task.scss +++ b/app/styles/components/kanban-task.scss @@ -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); } diff --git a/app/styles/modules/common/colors-table.scss b/app/styles/modules/common/colors-table.scss index c188f54e..c1594d54 100644 --- a/app/styles/modules/common/colors-table.scss +++ b/app/styles/modules/common/colors-table.scss @@ -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; diff --git a/app/styles/modules/kanban/kanban-table.scss b/app/styles/modules/kanban/kanban-table.scss index ef5f1f52..5576acf6 100644 --- a/app/styles/modules/kanban/kanban-table.scss +++ b/app/styles/modules/kanban/kanban-table.scss @@ -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;