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