diff --git a/app/coffee/modules/backlog/main.coffee b/app/coffee/modules/backlog/main.coffee
index c76c736f..681e1ddb 100644
--- a/app/coffee/modules/backlog/main.coffee
+++ b/app/coffee/modules/backlog/main.coffee
@@ -733,7 +733,6 @@ UsRolePointsSelectorDirective = ($rootscope, $template) ->
$el.on "click", ".role", (event) ->
event.preventDefault()
event.stopPropagation()
-
target = angular.element(event.currentTarget)
rolScope = target.scope()
$rootscope.$broadcast("uspoints:select", target.data("role-id"), target.text())
@@ -746,170 +745,107 @@ UsRolePointsSelectorDirective = ($rootscope, $template) ->
module.directive("tgUsRolePointsSelector", ["$rootScope", "$tgTemplate", UsRolePointsSelectorDirective])
-UsPointsDirective = ($repo, $tgTemplate) ->
+UsPointsDirective = ($tgEstimationsService, $repo, $tgTemplate) ->
rolesTemplate = $tgTemplate.get("common/estimation/us-points-roles-popover.html", true)
- pointsTemplate = $tgTemplate.get("common/estimation/us-estimation-points.html", true)
link = ($scope, $el, $attrs) ->
$ctrl = $el.controller()
-
- us = $scope.$eval($attrs.tgBacklogUsPoints)
-
updatingSelectedRoleId = null
selectedRoleId = null
- numberOfRoles = _.size(us.points)
+ filteringRoleId = null
+ estimationProcess = null
- # Preselect the role if we have only one
- if numberOfRoles == 1
- selectedRoleId = _.keys(us.points)[0]
+ $scope.$on "uspoints:select", (ctx, roleId, roleName) ->
+ us = $scope.$eval($attrs.tgBacklogUsPoints)
+ selectedRoleId = roleId
+ estimationProcess.render()
- roles = []
- updatePointsRoles = ->
- roles = _.map computableRoles, (role) ->
- pointId = us.points[role.id]
- pointObj = $scope.pointsById[pointId]
+ $scope.$on "uspoints:clear-selection", (ctx) ->
+ us = $scope.$eval($attrs.tgBacklogUsPoints)
+ selectedRoleId = null
+ estimationProcess.render()
- role = _.clone(role, true)
- role.points = if pointObj.value? then pointObj.value else "?"
- return role
+ $scope.$watch $attrs.tgBacklogUsPoints, (us) ->
+ if us
+ estimationProcess = $tgEstimationsService.create($el, us, $scope.project)
- computableRoles = _.filter($scope.project.roles, "computable")
- updatePointsRoles()
+ # Update roles
+ roles = estimationProcess.calculateRoles()
+ if roles.length == 0
+ $el.find(".icon-arrow-bottom").remove()
+ $el.find("a.us-points").addClass("not-clickable")
- if roles.length == 0
- $el.find(".icon-arrow-bottom").remove()
- $el.find("a.us-points").addClass("not-clickable")
+ else if roles.length == 1
+ # Preselect the role if we have only one
+ selectedRoleId = _.keys(us.points)[0]
- renderPointsSelector = (us, roleId) ->
- # Prepare data for rendering
- points = _.map $scope.project.points, (point) ->
- point = _.clone(point, true)
- point.selected = if us.points[roleId] == point.id then false else true
- return point
+ if estimationProcess.isEditable
+ bindClickElements()
- html = pointsTemplate({"points": points, "roleId": roleId})
+ estimationProcess.onSelectedPointForRole = (roleId, pointId) ->
+ @save(roleId, pointId).then ->
+ $ctrl.loadProjectStats()
- # Remove any prevous state
- $el.find(".popover").popover().close()
- $el.find(".pop-points-open").remove()
+ estimationProcess.render = () ->
+ totalPoints = @calculateTotalPoints()
+ if not selectedRoleId? or roles.length == 1
+ text = totalPoints
+ title = totalPoints
+ else
+ pointId = @us.points[selectedRoleId]
+ pointObj = @pointsById[pointId]
+ text = "#{pointObj.name} / #{totalPoints}"
+ title = "#{pointObj.name} / #{totalPoints}"
- # Render into DOM and show the new created element
- $el.append(html)
+ ctx = {
+ totalPoints: totalPoints
+ roles: @calculateRoles()
+ editable: @isEditable
+ text: text
+ title: title
+ }
+ mainTemplate = "common/estimation/us-estimation-total.html"
+ template = $tgTemplate.get(mainTemplate, true)
+ html = template(ctx)
+ @$el.html(html)
- # If not showing role selection let's move to the left
- if not $el.find(".pop-role:visible").css("left")?
- $el.find(".pop-points-open").css("left", "110px")
-
- $el.find(".pop-points-open").popover().open()
-
- renderRolesSelector = (us) ->
- updatePointsRoles()
+ estimationProcess.render()
+ renderRolesSelector = () ->
+ roles = estimationProcess.calculateRoles()
html = rolesTemplate({"roles": roles})
-
# Render into DOM and show the new created element
$el.append(html)
$el.find(".pop-role").popover().open(() -> $(this).remove())
- renderPoints = (us, roleId) ->
- dom = $el.find("a > span.points-value")
-
- if roleId == null or numberOfRoles == 1
- totalPoints = if us.total_points? then us.total_points else "?"
- dom.text(totalPoints)
- dom.parent().prop("title", totalPoints)
- else
- pointId = us.points[roleId]
- pointObj = $scope.pointsById[pointId]
- dom.html("#{pointObj.name} / #{us.total_points}")
- dom.parent().prop("title", "#{pointObj.name} / #{us.total_points}")
-
- calculateTotalPoints = ->
- values = _.map(us.points, (v, k) -> $scope.pointsById[v].value)
- values = _.filter(values, (num) -> num?)
-
- if values.length == 0
- return "?"
-
- return _.reduce(values, (acc, num) -> acc + num)
-
- $scope.$watch $attrs.tgBacklogUsPoints, (us) ->
- renderPoints(us, selectedRoleId) if us
-
- $scope.$on "uspoints:select", (ctx, roleId, roleName) ->
- us = $scope.$eval($attrs.tgBacklogUsPoints)
- renderPoints(us, roleId)
- selectedRoleId = roleId
-
- $scope.$on "uspoints:clear-selection", (ctx) ->
- us = $scope.$eval($attrs.tgBacklogUsPoints)
- renderPoints(us, null)
- selectedRoleId = null
-
- if roles.length > 0
+ bindClickElements = () ->
$el.on "click", "a.us-points span", (event) ->
event.preventDefault()
event.stopPropagation()
-
us = $scope.$eval($attrs.tgBacklogUsPoints)
updatingSelectedRoleId = selectedRoleId
-
if selectedRoleId?
- renderPointsSelector(us, selectedRoleId)
+ estimationProcess.renderPointsSelector(selectedRoleId)
else
- renderRolesSelector(us)
+ renderRolesSelector()
$el.on "click", ".role", (event) ->
event.preventDefault()
event.stopPropagation()
target = angular.element(event.currentTarget)
-
us = $scope.$eval($attrs.tgBacklogUsPoints)
-
updatingSelectedRoleId = target.data("role-id")
-
popRolesDom = $el.find(".pop-role")
popRolesDom.find("a").removeClass("active")
popRolesDom.find("a[data-role-id='#{updatingSelectedRoleId}']").addClass("active")
-
- renderPointsSelector(us, updatingSelectedRoleId)
-
- $el.on "click", ".point", (event) ->
- event.preventDefault()
- event.stopPropagation()
-
- target = angular.element(event.currentTarget)
- $el.find(".pop-points-open").hide()
- $el.find(".pop-role").hide()
-
- us = $scope.$eval($attrs.tgBacklogUsPoints)
-
- points = _.clone(us.points, true)
- points[updatingSelectedRoleId] = target.data("point-id")
-
- $scope.$apply ->
- us.points = points
- us.total_points = calculateTotalPoints(us)
-
- renderPoints(us, selectedRoleId)
-
- $repo.save(us).then ->
- # Little Hack for refresh.
- $repo.refresh(us).then ->
- $ctrl.loadProjectStats()
-
- bindOnce $scope, "project", (project) ->
- # If the user has not enough permissions the click events are unbinded
- if project.my_permissions.indexOf("modify_us") == -1
- $el.unbind("click")
- $el.find("a").addClass("not-clickable")
+ estimationProcess.renderPointsSelector(updatingSelectedRoleId)
$scope.$on "$destroy", ->
$el.off()
return {link: link}
-module.directive("tgBacklogUsPoints", ["$tgRepo", "$tgTemplate", UsPointsDirective])
+module.directive("tgBacklogUsPoints", ["$tgEstimationsService", "$tgRepo", "$tgTemplate", UsPointsDirective])
#############################################################################
## Burndown graph directive
diff --git a/app/coffee/modules/common/estimation.coffee b/app/coffee/modules/common/estimation.coffee
index 655d3daa..3b03a587 100644
--- a/app/coffee/modules/common/estimation.coffee
+++ b/app/coffee/modules/common/estimation.coffee
@@ -20,6 +20,7 @@
###
taiga = @.taiga
+groupBy = @.taiga.groupBy
module = angular.module("taigaCommon")
@@ -27,7 +28,53 @@ module = angular.module("taigaCommon")
## User story estimation directive (for Lightboxes)
#############################################################################
-LbUsEstimationDirective = ($rootScope, $repo, $confirm, $template) ->
+LbUsEstimationDirective = ($tgEstimationsService, $rootScope, $repo, $confirm, $template) ->
+ # Display the points of a US and you can edit it.
+ #
+ # Example:
+ # tg-lb-us-estimation-progress-bar(ng-model="us")
+ #
+ # Requirements:
+ # - Us object (ng-model)
+ # - scope.project object
+
+ link = ($scope, $el, $attrs, $model) ->
+ $scope.$watch $attrs.ngModel, (us) ->
+ if us
+ estimationProcess = $tgEstimationsService.create($el, us, $scope.project)
+ estimationProcess.onSelectedPointForRole = (roleId, pointId) ->
+ $scope.$apply ->
+ $model.$setViewValue(us)
+
+ estimationProcess.render = () ->
+ ctx = {
+ totalPoints: @calculateTotalPoints()
+ roles: @calculateRoles()
+ editable: @isEditable
+ }
+ mainTemplate = "common/estimation/us-estimation-points-per-role.html"
+ template = $template.get(mainTemplate, true)
+ html = template(ctx)
+ @$el.html(html)
+
+ estimationProcess.render()
+ $scope.$on "$destroy", ->
+ $el.off()
+
+ return {
+ link: link
+ restrict: "EA"
+ require: "ngModel"
+ }
+
+module.directive("tgLbUsEstimation", ["$tgEstimationsService", "$rootScope", "$tgRepo", "$tgConfirm", "$tgTemplate", LbUsEstimationDirective])
+
+
+#############################################################################
+## User story estimation directive
+#############################################################################
+
+UsEstimationDirective = ($tgEstimationsService, $rootScope, $repo, $confirm, $qqueue, $template) ->
# Display the points of a US and you can edit it.
#
# Example:
@@ -37,91 +84,26 @@ LbUsEstimationDirective = ($rootScope, $repo, $confirm, $template) ->
# - Us object (ng-model)
# - scope.project object
- mainTemplate = $template.get("common/estimation/us-estimation-points-per-role.html", true)
- pointsTemplate = $template.get("common/estimation/us-estimation-points.html", true)
-
link = ($scope, $el, $attrs, $model) ->
- render = (points) ->
- totalPoints = calculateTotalPoints(points) or 0
- computableRoles = _.filter($scope.project.roles, "computable")
+ $scope.$watch $attrs.ngModel, (us) ->
+ if us
+ estimationProcess = $tgEstimationsService.create($el, us, $scope.project)
+ estimationProcess.onSelectedPointForRole = (roleId, pointId) ->
+ @save(roleId, pointId).then ->
+ $rootScope.$broadcast("history:reload")
- roles = _.map computableRoles, (role) ->
- pointId = points[role.id]
- pointObj = $scope.pointsById[pointId]
+ estimationProcess.render = () ->
+ ctx = {
+ totalPoints: @calculateTotalPoints()
+ roles: @calculateRoles()
+ editable: @isEditable
+ }
+ mainTemplate = "common/estimation/us-estimation-points-per-role.html"
+ template = $template.get(mainTemplate, true)
+ html = template(ctx)
+ @$el.html(html)
- role = _.clone(role, true)
- role.points = if pointObj? and pointObj.name? then pointObj.name else "?"
- return role
-
- ctx = {
- totalPoints: totalPoints
- roles: roles
- editable: true
- }
- html = mainTemplate(ctx)
- $el.html(html)
-
- renderPoints = (target, usPoints, roleId) ->
- points = _.map $scope.project.points, (point) ->
- point = _.clone(point, true)
- point.selected = if usPoints[roleId] == point.id then false else true
- return point
-
- html = pointsTemplate({"points": points, roleId: roleId})
-
- # Remove any prevous state
- $el.find(".popover").popover().close()
- $el.find(".pop-points-open").remove()
-
- # If not showing role selection let's move to the left
- if not $el.find(".pop-role:visible").css("left")?
- $el.find(".pop-points-open").css("left", "110px")
-
- $el.find(".pop-points-open").remove()
-
- # Render into DOM and show the new created element
- $el.find(target).append(html)
-
- $el.find(".pop-points-open").popover().open(-> $(this).removeClass("active"))
- $el.find(".pop-points-open").show()
-
- calculateTotalPoints = (points) ->
- values = _.map(points, (v, k) -> $scope.pointsById[v]?.value or 0)
- if values.length == 0
- return "0"
- return _.reduce(values, (acc, num) -> acc + num)
-
- $el.on "click", ".total.clickable", (event) ->
- event.preventDefault()
- event.stopPropagation()
-
- target = angular.element(event.currentTarget)
- roleId = target.data("role-id")
-
- points = $model.$modelValue
- renderPoints(target, points, roleId)
-
- target.siblings().removeClass('active')
- target.addClass('active')
-
- $el.on "click", ".point", (event) ->
- event.preventDefault()
- event.stopPropagation()
-
- target = angular.element(event.currentTarget)
- roleId = target.data("role-id")
- pointId = target.data("point-id")
-
- $el.find(".popover").popover().close()
-
- points = _.clone($model.$modelValue, true)
- points[roleId] = pointId
-
- $scope.$apply ->
- $model.$setViewValue(points)
-
- $scope.$watch $attrs.ngModel, (points) ->
- render(points) if points
+ estimationProcess.render()
$scope.$on "$destroy", ->
$el.off()
@@ -132,81 +114,46 @@ LbUsEstimationDirective = ($rootScope, $repo, $confirm, $template) ->
require: "ngModel"
}
-module.directive("tgLbUsEstimation", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgTemplate", LbUsEstimationDirective])
+module.directive("tgUsEstimation", ["$tgEstimationsService", "$rootScope", "$tgRepo", "$tgConfirm", "$tgQqueue", "$tgTemplate",
+ UsEstimationDirective])
#############################################################################
-## User story estimation directive
+## Estimations service
#############################################################################
-UsEstimationDirective = ($rootScope, $repo, $confirm, $qqueue, $template) ->
- # Display the points of a US and you can edit it.
- #
- # Example:
- # tg-us-estimation-progress-bar(ng-model="us")
- #
- # Requirements:
- # - Us object (ng-model)
- # - scope.project object
-
- mainTemplate = $template.get("common/estimation/us-estimation-points-per-role.html", true)
+EstimationsService = ($template, $qqueue, $repo, $confirm, $q) ->
pointsTemplate = $template.get("common/estimation/us-estimation-points.html", true)
- link = ($scope, $el, $attrs, $model) ->
- isEditable = ->
- return $scope.project.my_permissions.indexOf("modify_us") != -1
+ class EstimationProcess
+ constructor: (@$el, @us, @project) ->
+ @isEditable = @project.my_permissions.indexOf("modify_us") != -1
+ @roles = @project.roles
+ @points = @project.points
+ @pointsById = groupBy(@points, (x) -> x.id)
+ @onSelectedPointForRole = (roleId, pointId) ->
+ @render = () ->
- render = (us) ->
- totalPoints = calculateTotalPoints(us.points) or "?"
- computableRoles = _.filter($scope.project.roles, "computable")
+ save: (roleId, pointId) ->
+ deferred = $q.defer()
+ $qqueue.add () =>
+ onSuccess = =>
+ deferred.resolve()
+ $confirm.notify("success")
- roles = _.map computableRoles, (role) ->
- pointId = us.points[role.id]
- pointObj = $scope.pointsById[pointId]
+ onError = =>
+ $confirm.notify("error")
+ @us.revert()
+ @render()
+ deferred.reject()
- role = _.clone(role, true)
- role.points = if pointObj? and pointObj.name? then pointObj.name else "?"
- return role
+ $repo.save(@us).then(onSuccess, onError)
- ctx = {
- totalPoints: totalPoints
- roles: roles
- editable: isEditable()
- }
- html = mainTemplate(ctx)
- $el.html(html)
+ return deferred.promise
- renderPoints = (target, us, roleId) ->
- points = _.map $scope.project.points, (point) ->
- point = _.clone(point, true)
- point.selected = if us.points[roleId] == point.id then false else true
- return point
+ calculateTotalPoints: () ->
+ values = _.map(@us.points, (v, k) => @pointsById[v]?.value)
- html = pointsTemplate({"points": points, roleId: roleId})
-
- # Remove any prevous state
- $el.find(".popover").popover().close()
- $el.find(".pop-points-open").remove()
-
- # If not showing role selection let's move to the left
- if not $el.find(".pop-role:visible").css("left")?
- $el.find(".pop-points-open").css("left", "110px")
-
- $el.find(".pop-points-open").remove()
-
- # Render into DOM and show the new created element
- $el.find(target).append(html)
-
- $el.find(".pop-points-open").popover().open ->
- $(this)
- .removeClass("active")
- .closest("li").removeClass("active")
-
-
- $el.find(".pop-points-open").show()
-
- calculateTotalPoints = (points) ->
- values = _.map(points, (v, k) -> $scope.pointsById[v]?.value)
if values.length == 0
return "0"
@@ -216,62 +163,74 @@ UsEstimationDirective = ($rootScope, $repo, $confirm, $qqueue, $template) ->
return _.reduce(notNullValues, (acc, num) -> acc + num)
- save = $qqueue.bindAdd (roleId, pointId) =>
- $el.find(".popover").popover().close()
+ calculateRoles: () ->
+ computableRoles = _.filter(@project.roles, "computable")
+ roles = _.map computableRoles, (role) =>
+ pointId = @us.points[role.id]
+ pointObj = @pointsById[pointId]
+ role = _.clone(role, true)
+ role.points = if pointObj? and pointObj.name? then pointObj.name else "?"
+ return role
- points = _.clone($model.$modelValue.points, true)
- points[roleId] = pointId
+ return roles
- us = $model.$modelValue.clone()
- us.points = points
- $model.$setViewValue(us)
+ bindClickEvents: =>
+ @$el.on "click", ".total.clickable", (event) =>
+ event.preventDefault()
+ event.stopPropagation()
+ target = angular.element(event.currentTarget)
+ roleId = target.data("role-id")
+ @renderPointsSelector(roleId, target)
+ target.siblings().removeClass('active')
+ target.addClass('active')
- onSuccess = ->
- $confirm.notify("success")
- $rootScope.$broadcast("history:reload")
- onError = ->
- $confirm.notify("error")
- us.revert()
- $model.$setViewValue(us)
+ @$el.on "click", ".point", (event) =>
+ event.preventDefault()
+ event.stopPropagation()
+ target = angular.element(event.currentTarget)
+ roleId = target.data("role-id")
+ pointId = target.data("point-id")
+ @$el.find(".popover").popover().close()
+ points = _.clone(@us.points, true)
+ points[roleId] = pointId
+ @us.points = points
+ @render()
+ @onSelectedPointForRole(roleId, pointId)
- $repo.save(us).then(onSuccess, onError)
+ renderPointsSelector: (roleId, target) ->
+ points = _.map @points, (point) =>
+ point = _.clone(point, true)
+ point.selected = if @us.points[roleId] == point.id then false else true
+ return point
- $el.on "click", ".total.clickable", (event) ->
- event.preventDefault()
- event.stopPropagation()
- return if not isEditable()
+ html = pointsTemplate({"points": points, roleId: roleId})
+ # Remove any previous state
+ @$el.find(".popover").popover().close()
+ @$el.find(".pop-points-open").remove()
+ # Render into DOM and show the new created element
+ if target?
+ @$el.find(target).append(html)
+ else
+ @$el.append(html)
- target = angular.element(event.currentTarget)
- roleId = target.data("role-id")
+ @$el.find(".pop-points-open").popover().open ->
+ $(this)
+ .removeClass("active")
+ .closest("li").removeClass("active")
- us = $model.$modelValue
- renderPoints(target, us, roleId)
+ @$el.find(".pop-points-open").show()
- target.siblings().removeClass('active')
- target.addClass('active')
+ create = ($el, us, project) ->
+ estimationProcess = new EstimationProcess($el, us, project)
+ if estimationProcess.isEditable
+ estimationProcess.bindClickEvents()
+ else
+ $el.unbind("click")
- $el.on "click", ".point", (event) ->
- event.preventDefault()
- event.stopPropagation()
- return if not isEditable()
-
- target = angular.element(event.currentTarget)
- roleId = target.data("role-id")
- pointId = target.data("point-id")
-
- save(roleId, pointId)
-
- $scope.$watch $attrs.ngModel, (us) ->
- render(us) if us
-
- $scope.$on "$destroy", ->
- $el.off()
+ return estimationProcess
return {
- link: link
- restrict: "EA"
- require: "ngModel"
+ create: create
}
-module.directive("tgUsEstimation", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgQqueue", "$tgTemplate",
- UsEstimationDirective])
+module.factory("$tgEstimationsService", ["$tgTemplate", "$tgQqueue", "$tgRepo", "$tgConfirm", "$q", EstimationsService])
diff --git a/app/partials/common/estimation/us-estimation-total.jade b/app/partials/common/estimation/us-estimation-total.jade
new file mode 100644
index 00000000..235494bb
--- /dev/null
+++ b/app/partials/common/estimation/us-estimation-total.jade
@@ -0,0 +1,5 @@
+a.us-points(href="", title!="<%= title %>", class!="<% if (!editable) { %>not-clickable<% } %>")
+ span.points-value <%= text %>
+ <% if (editable) { %>
+ span.icon.icon-arrow-bottom(tg-check-permission="modify_us")
+ <% } %>
diff --git a/app/partials/includes/components/backlog-row.jade b/app/partials/includes/components/backlog-row.jade
index a340e1ae..7fbe7dbe 100644
--- a/app/partials/includes/components/backlog-row.jade
+++ b/app/partials/includes/components/backlog-row.jade
@@ -20,7 +20,5 @@ div.row.us-item-row(ng-repeat="us in visibleUserstories track by us.id", tg-drag
div.points(tg-backlog-us-points="us")
a.us-points(href="", title="Points")
- span.points-value 0
- span.icon.icon-arrow-bottom(tg-check-permission="modify_us")
a.icon.icon-drag-v(tg-check-permission="modify_us", href="", title="Drag")
diff --git a/app/partials/includes/modules/lightbox-us-create-edit.jade b/app/partials/includes/modules/lightbox-us-create-edit.jade
index 0a6d73e7..deb5a41d 100644
--- a/app/partials/includes/modules/lightbox-us-create-edit.jade
+++ b/app/partials/includes/modules/lightbox-us-create-edit.jade
@@ -7,7 +7,7 @@ form
data-required="true", data-maxlength="500")
fieldset.estimation
- tg-lb-us-estimation(ng-model="us.points")
+ tg-lb-us-estimation(ng-model="us")
fieldset
select(name="status", ng-model="us.status", ng-options="s.id as s.name for s in usStatusList",
diff --git a/app/styles/modules/backlog/backlog-table.scss b/app/styles/modules/backlog/backlog-table.scss
index d5a1341e..c9dd613a 100644
--- a/app/styles/modules/backlog/backlog-table.scss
+++ b/app/styles/modules/backlog/backlog-table.scss
@@ -77,7 +77,7 @@
padding-right: 3rem;
}
.pop-points-open {
- @include popover(200px, 0, 260px, '', '');
+ @include popover(200px, 0, 30px, '', '');
li {
display: inline-block;
width: 23%;