Merge branch 'kanban-cards'

stable
Andrey Antukh 2014-09-18 12:08:37 +02:00
commit 4c354e1399
21 changed files with 469 additions and 111 deletions

View File

@ -593,6 +593,9 @@ BacklogDirective = ($repo, $rootscope) ->
return {link: link} return {link: link}
module.directive("tgBacklog", ["$tgRepo", "$rootScope", BacklogDirective])
############################################################################# #############################################################################
## User story points directive ## User story points directive
############################################################################# #############################################################################
@ -603,8 +606,10 @@ UsRolePointsSelectorDirective = ($rootscope) ->
<ul class="popover pop-role"> <ul class="popover pop-role">
<li><a class="clear-selection" href="" title="All">All</a></li> <li><a class="clear-selection" href="" title="All">All</a></li>
<% _.each(roles, function(role) { %> <% _.each(roles, function(role) { %>
<li><a href="" class="role" title="<%- role.name %>" <li>
data-role-id="<%- role.id %>"><%- role.name %></a></li> <a href="" class="role" title="<%- role.name %>"
data-role-id="<%- role.id %>"><%- role.name %></a>
</li>
<% }); %> <% }); %>
</ul> </ul>
""") """)
@ -616,7 +621,7 @@ UsRolePointsSelectorDirective = ($rootscope) ->
numberOfRoles = _.size(roles) numberOfRoles = _.size(roles)
if numberOfRoles > 1 if numberOfRoles > 1
$el.append(selectionTemplate({ 'roles': roles })) $el.append(selectionTemplate({"roles":roles}))
else else
$el.find(".icon-arrow-bottom").remove() $el.find(".icon-arrow-bottom").remove()
@ -655,15 +660,18 @@ UsRolePointsSelectorDirective = ($rootscope) ->
return {link: link} return {link: link}
module.directive("tgUsRolePointsSelector", ["$rootScope", UsRolePointsSelectorDirective])
UsPointsDirective = ($repo) -> UsPointsDirective = ($repo) ->
selectionTemplate = _.template(""" rolesTemplate = _.template("""
<ul class="popover pop-role"> <ul class="popover pop-role">
<% _.each(rolePoints, function(rolePointsElement) { %> <% _.each(roles, function(role) { %>
<li><a href="" class="role" title="<%- rolePointsElement.name %>" <li>
data-role-id="<%- rolePointsElement.id %>"> <a href="" class="role" title="<%- role.name %>"
<%- rolePointsElement.name %> data-role-id="<%- role.id %>">
(<%- rolePointsElement.points %>) <%- role.name %>
(<%- role.points %>)
</a> </a>
</li> </li>
<% }); %> <% }); %>
@ -673,8 +681,14 @@ UsPointsDirective = ($repo) ->
pointsTemplate = _.template(""" pointsTemplate = _.template("""
<ul class="popover pop-points-open"> <ul class="popover pop-points-open">
<% _.each(points, function(point) { %> <% _.each(points, function(point) { %>
<li><a href="" class="point" title="<%- point.name %>" <li>
<% if (point.selected) { %>
<a href="" class="point" title="<%- point.name %>"
data-point-id="<%- point.id %>"><%- point.name %></a> data-point-id="<%- point.id %>"><%- point.name %></a>
<% } else { %>
<a href="" class="point active" title="<%- point.name %>"
data-point-id="<%- point.id %>"><%- point.name %></a>
<% } %>
</li> </li>
<% }); %> <% }); %>
</ul> </ul>
@ -682,95 +696,116 @@ UsPointsDirective = ($repo) ->
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs) ->
$ctrl = $el.controller() $ctrl = $el.controller()
us = $scope.$eval($attrs.tgUsPoints)
us = $scope.$eval($attrs.tgBacklogUsPoints)
updatingSelectedRoleId = null updatingSelectedRoleId = null
selectedRoleId = null selectedRoleId = null
numberOfRoles = _.size(us.points) numberOfRoles = _.size(us.points)
# Preselect the rol if we have only one # Preselect the role if we have only one
if numberOfRoles == 1 if numberOfRoles == 1
selectedRoleId = _.keys(us.points)[0] selectedRoleId = _.keys(us.points)[0]
showPopPoints = () -> renderPointsSelector = (us, roleId) ->
$(".popover").popover().close() # 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
html = pointsTemplate({"points": points})
# Remove any prevous state
$el.find(".popover").popover().close()
$el.find(".pop-points-open").remove() $el.find(".pop-points-open").remove()
$el.append(pointsTemplate({ "points": $scope.project.points }))
dataPointId = us.points[updatingSelectedRoleId] # Render into DOM and show the new created element
$el.find(".pop-points-open a[data-point-id='#{dataPointId}']").addClass("active") $el.append(html)
# If not showing role selection let's move to the left # If not showing role selection let's move to the left
if not $el.find(".pop-role:visible").css('left')? if not $el.find(".pop-role:visible").css("left")?
$el.find(".pop-points-open").css('left', '110px') $el.find(".pop-points-open").css("left", "110px")
$el.find(".pop-points-open").show() $el.find(".pop-points-open").show()
showPopRoles = () -> renderRolesSelector = (us) ->
$el.find(".pop-role").remove() # Prepare data for rendering
rolePoints = _.clone(_.filter($scope.project.roles, "computable"), true) computableRoles = _.filter($scope.project.roles, "computable")
undefinedToQuestion = (val) -> roles = _.map computableRoles, (role) ->
return "?" if not val? pointId = us.points[role.id]
return val pointObj = $scope.pointsById[pointId]
_.map rolePoints, (v, k) -> role = _.clone(role, true)
v.points = undefinedToQuestion($scope.pointsById[us.points[v.id]]?.value) role.points = if pointObj.value? then pointObj.value else "?"
$el.append(selectionTemplate({ "rolePoints": rolePoints })) return role
html = rolesTemplate({"roles": roles})
# Render into DOM and show the new created element
$el.append(html)
$el.find(".pop-role").popover().open(() -> $(this).remove()) $el.find(".pop-role").popover().open(() -> $(this).remove())
updatePoints = (roleId) -> renderPoints = (us, roleId) ->
# Update the dom with the points dom = $el.find("a > span.points-value")
pointsDom = $el.find("a > span.points-value")
usTotalPoints = calculateTotalPoints(us) totalPoints = calculateTotalPoints(us)
us.total_points = usTotalPoints if roleId == null or numberOfRoles == 1
if not roleId? or numberOfRoles == 1 dom.text(us.total_points)
pointsDom.text(us.total_points)
else else
pointId = us.points[roleId] pointId = us.points[roleId]
points = $scope.pointsById[pointId] pointObj = $scope.pointsById[pointId]
pointsDom.html("#{points.name} / <span>#{us.total_points}</span>") dom.html("#{pointObj.name} / <span>#{us.total_points}</span>")
calculateTotalPoints = -> calculateTotalPoints = ->
values = _.map(us.points, (v, k) -> $scope.pointsById[v].value) values = _.map(us.points, (v, k) -> $scope.pointsById[v].value)
values = _.filter(values, (num) -> num?) values = _.filter(values, (num) -> num?)
if values.length == 0 if values.length == 0
return "?" return "?"
return _.reduce(values, (acc, num) -> acc + num) return _.reduce(values, (acc, num) -> acc + num)
updatePoints(null) $scope.$watch $attrs.tgBacklogUsPoints, (us) ->
renderPoints(us, selectedRoleId) if us
$scope.$on "uspoints:select", (ctx, roleId, roleName) -> $scope.$on "uspoints:select", (ctx, roleId, roleName) ->
updatePoints(roleId) us = $scope.$eval($attrs.tgBacklogUsPoints)
renderPoints(us, roleId)
selectedRoleId = roleId selectedRoleId = roleId
$scope.$on "uspoints:clear-selection", (ctx) -> $scope.$on "uspoints:clear-selection", (ctx) ->
updatePoints(null) us = $scope.$eval($attrs.tgBacklogUsPoints)
renderPoints(us, null)
selectedRoleId = null selectedRoleId = null
$el.on "click", "a.us-points span", (event) -> $el.on "click", "a.us-points span", (event) ->
event.preventDefault() event.preventDefault()
target = angular.element(event.target)
event.stopPropagation() event.stopPropagation()
if selectedRoleId? us = $scope.$eval($attrs.tgBacklogUsPoints)
updatingSelectedRoleId = selectedRoleId updatingSelectedRoleId = selectedRoleId
showPopPoints()
if selectedRoleId?
renderPointsSelector(us, selectedRoleId)
else else
showPopRoles() renderRolesSelector(us)
$el.on "click", ".role", (event) -> $el.on "click", ".role", (event) ->
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
target = angular.element(event.currentTarget) target = angular.element(event.currentTarget)
us = $scope.$eval($attrs.tgBacklogUsPoints)
updatingSelectedRoleId = target.data("role-id") updatingSelectedRoleId = target.data("role-id")
popRolesDom = $el.find(".pop-role") popRolesDom = $el.find(".pop-role")
popRolesDom.find("a").removeClass("active") popRolesDom.find("a").removeClass("active")
popRolesDom.find("a[data-role-id='#{updatingSelectedRoleId}']").addClass("active") popRolesDom.find("a[data-role-id='#{updatingSelectedRoleId}']").addClass("active")
showPopPoints()
renderPointsSelector(us, updatingSelectedRoleId)
$el.on "click", ".point", (event) -> $el.on "click", ".point", (event) ->
event.preventDefault() event.preventDefault()
@ -780,15 +815,16 @@ UsPointsDirective = ($repo) ->
$el.find(".pop-points-open").hide() $el.find(".pop-points-open").hide()
$el.find(".pop-role").hide() $el.find(".pop-role").hide()
$scope.$apply () -> us = $scope.$eval($attrs.tgBacklogUsPoints)
usPoints = _.clone(us.points, true)
usPoints[updatingSelectedRoleId] = target.data("point-id")
us.points = usPoints
usTotalPoints = calculateTotalPoints(us) points = _.clone(us.points, true)
us.total_points = usTotalPoints points[updatingSelectedRoleId] = target.data("point-id")
updatePoints(selectedRoleId) $scope.$apply ->
us.points = points
us.total_points = calculateTotalPoints(us)
renderPoints(us, selectedRoleId)
$repo.save(us).then -> $repo.save(us).then ->
# Little Hack for refresh. # Little Hack for refresh.
@ -806,6 +842,7 @@ UsPointsDirective = ($repo) ->
return {link: link} return {link: link}
module.directive("tgBacklogUsPoints", ["$tgRepo", UsPointsDirective])
############################################################################# #############################################################################
## Burndown graph directive ## Burndown graph directive
@ -837,14 +874,14 @@ tgBacklogGraphDirective = ->
lines: lines:
fillColor : "rgba(102,153,51,0.3)" fillColor : "rgba(102,153,51,0.3)"
}) })
team_increment_line = _.map(dataToDraw.milestones, (ml) -> -ml['team-increment']) team_increment_line = _.map(dataToDraw.milestones, (ml) -> -ml["team-increment"])
data.push({ data.push({
data: _.zip(milestonesRange, team_increment_line) data: _.zip(milestonesRange, team_increment_line)
lines: lines:
fillColor : "rgba(153,51,51,0.3)" fillColor : "rgba(153,51,51,0.3)"
}) })
client_increment_line = _.map dataToDraw.milestones, (ml) -> client_increment_line = _.map dataToDraw.milestones, (ml) ->
-ml['team-increment'] - ml['client-increment'] -ml["team-increment"] - ml["client-increment"]
data.push({ data.push({
data: _.zip(milestonesRange, client_increment_line) data: _.zip(milestonesRange, client_increment_line)
lines: lines:
@ -862,14 +899,14 @@ tgBacklogGraphDirective = ->
options = { options = {
grid: { grid: {
borderWidth: { top: 0, right: 1, left:0, bottom: 0 } borderWidth: { top: 0, right: 1, left:0, bottom: 0 }
borderColor: '#ccc' borderColor: "#ccc"
} }
xaxis: { xaxis: {
ticks: dataToDraw.milestones.length ticks: dataToDraw.milestones.length
axisLabel: "Sprints" axisLabel: "Sprints"
axisLabelUseCanvas: true axisLabelUseCanvas: true
axisLabelFontSizePixels: 14 axisLabelFontSizePixels: 14
axisLabelFontFamily: 'Verdana, Arial, Helvetica, Tahoma, sans-serif' axisLabelFontFamily: "Verdana, Arial, Helvetica, Tahoma, sans-serif"
axisLabelPadding: 15 axisLabelPadding: 15
tickFormatter: (val, axis) -> "" tickFormatter: (val, axis) -> ""
} }
@ -895,7 +932,7 @@ tgBacklogGraphDirective = ->
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs) ->
element = angular.element($el) element = angular.element($el)
$scope.$watch 'stats', (value) -> $scope.$watch "stats", (value) ->
if $scope.stats? if $scope.stats?
redrawChart(element, $scope.stats) redrawChart(element, $scope.stats)
@ -908,7 +945,4 @@ tgBacklogGraphDirective = ->
return {link: link} return {link: link}
module.directive("tgBacklog", ["$tgRepo", "$rootScope", BacklogDirective])
module.directive("tgUsPoints", ["$tgRepo", UsPointsDirective])
module.directive("tgUsRolePointsSelector", ["$rootScope", UsRolePointsSelectorDirective])
module.directive("tgGmBacklogGraph", tgBacklogGraphDirective) module.directive("tgGmBacklogGraph", tgBacklogGraphDirective)

View File

@ -198,16 +198,17 @@ CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService,
isNew = true isNew = true
$scope.$on "usform:new", (ctx, projectId, status, statusList) -> $scope.$on "usform:new", (ctx, projectId, status, statusList) ->
isNew = true
$scope.usStatusList = statusList $scope.usStatusList = statusList
$scope.us = { $scope.us = {
project: projectId project: projectId
points : {}
status: status status: status
is_archived: false is_archived: false
tags: [] tags: []
} }
isNew = true
# Update texts for creation # Update texts for creation
$el.find(".button-green span").html("Create") #TODO: i18n $el.find(".button-green span").html("Create") #TODO: i18n
$el.find(".title").html("New user story ") #TODO: i18n $el.find(".title").html("New user story ") #TODO: i18n
@ -222,6 +223,7 @@ CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService,
$scope.$on "usform:edit", (ctx, us) -> $scope.$on "usform:edit", (ctx, us) ->
$scope.us = us $scope.us = us
isNew = false isNew = false
# Update texts for edition # Update texts for edition
$el.find(".button-green span").html("Save") #TODO: i18n $el.find(".button-green span").html("Save") #TODO: i18n
$el.find(".title").html("Edit user story ") #TODO: i18n $el.find(".title").html("Edit user story ") #TODO: i18n
@ -254,6 +256,7 @@ CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService,
return return
$loading.start(target) $loading.start(target)
if isNew if isNew
promise = $repo.create("userstories", $scope.us) promise = $repo.create("userstories", $scope.us)
broadcastEvent = "usform:new:success" broadcastEvent = "usform:new:success"

View File

@ -101,6 +101,10 @@ UsStatusDirective = ($repo, popoverService) ->
module.directive("tgUsStatus", ["$tgRepo", UsStatusDirective]) module.directive("tgUsStatus", ["$tgRepo", UsStatusDirective])
#############################################################################
## Related Task Status Directive
#############################################################################
RelatedTaskStatusDirective = ($repo, popoverService) -> RelatedTaskStatusDirective = ($repo, popoverService) ->
### ###
Print the status of a related task and a popover to change it. Print the status of a related task and a popover to change it.
@ -178,6 +182,10 @@ RelatedTaskStatusDirective = ($repo, popoverService) ->
module.directive("tgRelatedTaskStatus", ["$tgRepo", RelatedTaskStatusDirective]) module.directive("tgRelatedTaskStatus", ["$tgRepo", RelatedTaskStatusDirective])
#############################################################################
## jQuery plugin for Popover
#############################################################################
$.fn.popover = () -> $.fn.popover = () ->
$el = @ $el = @

View File

@ -30,6 +30,19 @@ timeout = @.taiga.timeout
module = angular.module("taigaKanban") module = angular.module("taigaKanban")
# Vars
defaultViewMode = "maximized"
defaultViewModes = {
maximized: {
cardClass: "kanban-task-maximized"
}
minimized: {
cardClass: "kanban-task-minimized"
}
}
############################################################################# #############################################################################
## Kanban Controller ## Kanban Controller
############################################################################# #############################################################################
@ -51,6 +64,7 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @appTitle, tgLoader) -> constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @appTitle, tgLoader) ->
_.bindAll(@) _.bindAll(@)
@scope.sectionName = "Kanban" @scope.sectionName = "Kanban"
@scope.statusViewModes = {}
promise = @.loadInitialData() promise = @.loadInitialData()
@ -147,6 +161,8 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
@scope.usStatusById = groupBy(project.us_statuses, (x) -> x.id) @scope.usStatusById = groupBy(project.us_statuses, (x) -> x.id)
@scope.usStatusList = _.sortBy(project.us_statuses, "order") @scope.usStatusList = _.sortBy(project.us_statuses, "order")
@.generateStatusViewModes()
@scope.$emit("project:loaded", project) @scope.$emit("project:loaded", project)
return project return project
@ -161,6 +177,31 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
.then(=> @.loadKanban()) .then(=> @.loadKanban())
.then(=> @scope.$broadcast("redraw:wip")) .then(=> @scope.$broadcast("redraw:wip"))
## View Mode methods
generateStatusViewModes: ->
storedStatusViewModes = @rs.kanban.getStatusViewModes(@scope.projectId)
@scope.statusViewModes = {}
for status in @scope.usStatusList
mode = storedStatusViewModes[status.id]
@scope.statusViewModes[status.id] = if _.has(defaultViewModes, mode) then mode else defaultViewMode
@.storeStatusViewModes()
storeStatusViewModes: ->
@rs.kanban.storeStatusViewModes(@scope.projectId, @scope.statusViewModes)
updateStatusViewMode: (statusId, newViewMode) ->
@scope.statusViewModes[statusId] = newViewMode
@.storeStatusViewModes()
getCardClass: (statusId)->
mode = @scope.statusViewModes[statusId] or defaultViewMode
return defaultViewModes[mode].cardClass or defaultViewModes[defaultViewMode].cardClass
# Utils methods
prepareBulkUpdateData: (uses, field="kanban_order") -> prepareBulkUpdateData: (uses, field="kanban_order") ->
return _.map(uses, (x) -> {"us_id": x.id, "order": x[field]}) return _.map(uses, (x) -> {"us_id": x.id, "order": x[field]})

View File

@ -133,5 +133,6 @@ module.run([
"$tgAttachmentsResourcesProvider", "$tgAttachmentsResourcesProvider",
"$tgMdRenderResourcesProvider", "$tgMdRenderResourcesProvider",
"$tgHistoryResourcesProvider", "$tgHistoryResourcesProvider",
"$tgKanbanResourcesProvider",
initResources initResources
]) ])

View File

@ -0,0 +1,46 @@
###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: modules/resources/kanban.coffee
###
taiga = @.taiga
generateHash = taiga.generateHash
resourceProvider = ($storage) ->
service = {}
hashSuffixStatusViewModes = "kanban-statusviewmodels"
service.storeStatusViewModes = (projectId, params) ->
ns = "#{projectId}:#{hashSuffixStatusViewModes}"
hash = generateHash([projectId, ns])
$storage.set(hash, params)
service.getStatusViewModes = (projectId) ->
ns = "#{projectId}:#{hashSuffixStatusViewModes}"
hash = generateHash([projectId, ns])
return $storage.get(hash) or {}
return (instance) ->
instance.kanban = service
module = angular.module("taigaResources")
module.factory("$tgKanbanResourcesProvider", ["$tgStorage", resourceProvider])

View File

@ -149,8 +149,6 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
module.controller("UserStoryDetailController", UserStoryDetailController) module.controller("UserStoryDetailController", UserStoryDetailController)
############################################################################# #############################################################################
## User story Main Directive ## User story Main Directive
############################################################################# #############################################################################
@ -188,8 +186,8 @@ UsDirective = ($tgrepo, $log, $location, $confirm, $navUrls, $loading) ->
return {link:link} return {link:link}
module.directive("tgUsDetail", ["$tgRepo", "$log", "$tgLocation", "$tgConfirm", "$tgNavUrls", "$tgLoading", UsDirective]) module.directive("tgUsDetail", ["$tgRepo", "$log", "$tgLocation", "$tgConfirm",
"$tgNavUrls", "$tgLoading", UsDirective])
############################################################################# #############################################################################
## User story status directive ## User story status directive
@ -365,3 +363,129 @@ UsStatusDetailDirective = () ->
return {link:link, require:"ngModel"} return {link:link, require:"ngModel"}
module.directive("tgUsStatusDetail", UsStatusDetailDirective) module.directive("tgUsStatusDetail", UsStatusDetailDirective)
#############################################################################
## User story estimation directive
#############################################################################
UsEstimationDirective = ($log) ->
mainTemplate = _.template("""
<ul class="points-per-role">
<li class="total">
<span class="points"><%- totalPoints %></span>
<span class="role">total</span>
</li>
<% _.each(roles, function(role) { %>
<li class="total clickable" data-role-id="<%- role.id %>">
<span class="points"><%- role.points %></span>
<span class="role"><%- role.name %></span></li>
<% }); %>
</ul>
""")
pointsTemplate = _.template("""
<ul class="popover pop-points-open">
<% _.each(points, function(point) { %>
<li>
<% if (point.selected) { %>
<a href="" class="point" title="<%- point.name %>"
data-point-id="<%- point.id %>" data-role-id="<%- roleId %>"><%- point.name %></a>
<% } else { %>
<a href="" class="point active" title="<%- point.name %>"
data-point-id="<%- point.id %>" data-role-id="<%- roleId %>"><%- point.name %></a>
<% } %>
</li>
<% }); %>
</ul>
""")
link = ($scope, $el, $attrs) ->
render = (us) ->
totalPoints = us.total_points or 0
computableRoles = _.filter($scope.project.roles, "computable")
roles = _.map computableRoles, (role) ->
pointId = us.points[role.id]
pointObj = $scope.pointsById[pointId]
role = _.clone(role, true)
role.points = if pointObj? and pointObj.name? then pointObj.name else "?"
return role
html = mainTemplate({totalPoints: totalPoints, roles: roles})
$el.html(html)
renderPoints = (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
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(".points-per-role").append(html)
$el.find(".pop-points-open").popover().open(-> $(this).removeClass("active"))
$el.find(".pop-points-open").show()
calculateTotalPoints = (us) ->
values = _.map(us.points, (v, k) -> $scope.pointsById[v]?.value or 0)
if values.length == 0
return "0"
return _.reduce(values, (acc, num) -> acc + num)
$scope.$watch $attrs.ngModel, (us) ->
render(us) if us
$scope.$on "$destroy", ->
$el.off()
$el.on "click", ".total.clickable", (event) ->
event.preventDefault()
event.stopPropagation()
target = angular.element(event.currentTarget)
roleId = target.data("role-id")
us = $scope.$eval($attrs.ngModel)
renderPoints(us, roleId)
target.siblings().removeClass('active')
target.addClass('active')
$el.on "click", ".point", (event) ->
event.preventDefault()
event.stopPropagation()
us = $scope.$eval($attrs.ngModel)
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
$scope.$apply ->
us.points = points
us.total_points = calculateTotalPoints(us)
render(us)
return {
link: link
restrict: "EA"
}
module.directive("tgUsEstimation", UsEstimationDirective)

Binary file not shown.

View File

@ -42,4 +42,6 @@
<glyph unicode="&#72;" d="M128 384l64 0 0-64-64 0z m0-96l64 0 0-64-64 0z m0-96l64 0 0-64-64 0z m128 0l128 0 0-64-128 0z m224 320l-448 0c-18 0-32-14-32-32l0-448c0-18 14-32 32-32l448 0c18 0 32 14 32 32l0 448c0 18-14 32-32 32z m-32-448l-384 0 0 384 384 0z m-192 224l128 0 0-64-128 0z m0 96l128 0 0-64-128 0z"/> <glyph unicode="&#72;" d="M128 384l64 0 0-64-64 0z m0-96l64 0 0-64-64 0z m0-96l64 0 0-64-64 0z m128 0l128 0 0-64-128 0z m224 320l-448 0c-18 0-32-14-32-32l0-448c0-18 14-32 32-32l448 0c18 0 32 14 32 32l0 448c0 18-14 32-32 32z m-32-448l-384 0 0 384 384 0z m-192 224l128 0 0-64-128 0z m0 96l128 0 0-64-128 0z"/>
<glyph unicode="&#73;" d="M184 20c0 0 0 54 0 54 0 0 144 0 144 0 0 0 0-54 0-54-24-14-48-21-73-20-23-1-47 6-71 20m141 84c0 0-138 0-138 0 0 25-6 49-19 72-12 23-25 43-40 58-14 15-26 34-37 57-11 23-16 47-14 71 3 41 19 77 48 106 30 29 73 44 130 44 58 0 102-15 131-44 29-29 45-65 49-106 1-20-2-39-9-57-6-18-15-35-26-50-11-14-22-29-33-43-12-14-21-31-30-49-8-19-12-38-12-59m-193 254c-2-1-2-4 0-10 1-5 1-9 1-10-1-1 0-5 2-10 3-5 4-8 3-9 0-1 1-4 4-9 3-5 5-9 6-10 1-1 3-5 7-10 3-5 6-8 7-9 1-2 3-5 7-11 5-6 7-10 9-12 30-42 49-78 57-108 0 0 42 0 42 0 8 32 27 68 57 108 2 2 6 8 13 18 7 10 12 16 13 18 1 3 4 8 9 15 4 8 7 13 8 17 1 4 2 9 3 14 1 6 1 12 0 18-5 67-47 101-125 101-77 0-118-34-123-101"/> <glyph unicode="&#73;" d="M184 20c0 0 0 54 0 54 0 0 144 0 144 0 0 0 0-54 0-54-24-14-48-21-73-20-23-1-47 6-71 20m141 84c0 0-138 0-138 0 0 25-6 49-19 72-12 23-25 43-40 58-14 15-26 34-37 57-11 23-16 47-14 71 3 41 19 77 48 106 30 29 73 44 130 44 58 0 102-15 131-44 29-29 45-65 49-106 1-20-2-39-9-57-6-18-15-35-26-50-11-14-22-29-33-43-12-14-21-31-30-49-8-19-12-38-12-59m-193 254c-2-1-2-4 0-10 1-5 1-9 1-10-1-1 0-5 2-10 3-5 4-8 3-9 0-1 1-4 4-9 3-5 5-9 6-10 1-1 3-5 7-10 3-5 6-8 7-9 1-2 3-5 7-11 5-6 7-10 9-12 30-42 49-78 57-108 0 0 42 0 42 0 8 32 27 68 57 108 2 2 6 8 13 18 7 10 12 16 13 18 1 3 4 8 9 15 4 8 7 13 8 17 1 4 2 9 3 14 1 6 1 12 0 18-5 67-47 101-125 101-77 0-118-34-123-101"/>
<glyph unicode="&#69;" d="M480 224l-64 0c-18 0-32 14-32 32 0 18 14 32 32 32l64 0c18 0 32-14 32-32 0-18-14-32-32-32z m-88 122c-13-12-33-12-45 0-13 13-13 33 0 46l45 45c12 12 33 12 45 0 13-13 13-33 0-45z m-136-346c-18 0-32 14-32 32l0 64c0 18 14 32 32 32 18 0 32-14 32-32l0-64c0-18-14-32-32-32z m0 384c-18 0-32 14-32 32l0 64c0 18 14 32 32 32 18 0 32-14 32-32l0-64c0-18-14-32-32-32z m-136-309c-12-13-32-13-45 0-12 12-12 33 0 45l45 45c13 13 33 13 46 0 12-12 12-32 0-45z m0 271l-45 46c-12 12-12 32 0 45 13 12 33 12 45 0l46-45c12-13 12-33 0-46-13-12-33-12-46 0z m8-90c0-18-14-32-32-32l-64 0c-18 0-32 14-32 32 0 18 14 32 32 32l64 0c18 0 32-14 32-32z m264-91l45-45c13-12 13-33 0-45-12-13-33-13-45 0l-45 45c-13 13-13 33 0 45 12 13 32 13 45 0z"/> <glyph unicode="&#69;" d="M480 224l-64 0c-18 0-32 14-32 32 0 18 14 32 32 32l64 0c18 0 32-14 32-32 0-18-14-32-32-32z m-88 122c-13-12-33-12-45 0-13 13-13 33 0 46l45 45c12 12 33 12 45 0 13-13 13-33 0-45z m-136-346c-18 0-32 14-32 32l0 64c0 18 14 32 32 32 18 0 32-14 32-32l0-64c0-18-14-32-32-32z m0 384c-18 0-32 14-32 32l0 64c0 18 14 32 32 32 18 0 32-14 32-32l0-64c0-18-14-32-32-32z m-136-309c-12-13-32-13-45 0-12 12-12 33 0 45l45 45c13 13 33 13 46 0 12-12 12-32 0-45z m0 271l-45 46c-12 12-12 32 0 45 13 12 33 12 45 0l46-45c12-13 12-33 0-46-13-12-33-12-46 0z m8-90c0-18-14-32-32-32l-64 0c-18 0-32 14-32 32 0 18 14 32 32 32l64 0c18 0 32-14 32-32z m264-91l45-45c13-12 13-33 0-45-12-13-33-13-45 0l-45 45c-13 13-13 33 0 45 12 13 32 13 45 0z"/>
<glyph unicode="&#74;" d="M184 54l24 145c1 3 0 5-2 7 0 0 0 0 0 0-2 2-5 3-7 3l-145-25c-3 0-5-2-6-5-1-3 0-7 2-9l28-28-76-76c-3-3-3-8 0-11l53-53c3-3 8-3 11 0l76 76 28-28c3-2 6-3 9-2 3 1 5 3 5 6z m144 404l-24-145c-1-3 0-5 2-7 0 0 0 0 0 0 2-2 5-3 7-3l145 25c3 0 5 2 6 5 1 3 0 7-2 9l-28 28 76 76c3 3 3 8 0 11l-53 53c-3 3-8 3-11 0l-76-76-28 28c-3 2-6 3-9 2-3-1-5-3-5-6z m130-274l-145 24c-3 1-5 0-7-2 0 0 0 0 0 0-2-2-3-5-3-7l25-145c0-3 2-5 5-6 3-1 7 0 9 2l28 28 76-76c3-3 8-3 11 0l53 53c3 3 3 8 0 11l-76 76 28 28c2 3 3 6 2 9-1 3-3 5-6 5z m-404 144l145-24c3-1 5 0 7 2 0 0 0 0 0 0 2 2 3 5 3 7l-25 145c0 3-2 5-5 6-3 1-6 0-9-2l-28-28-76 76c-3 3-8 3-11 0l-53-53c-3-3-3-8 0-11l76-76-28-28c-2-3-3-6-2-9 1-3 3-5 6-5z"/>
<glyph unicode="&#75;" d="M24 154l-24-144c0-3 1-6 2-8 0 0 0 0 0 0 2-1 5-2 8-2l144 25c3 0 6 2 6 5 1 3 1 6-2 8l-28 29 76 75c3 4 3 9 0 12l-52 52c-3 4-9 4-12 0l-76-75-28 28c-2 2-5 3-8 2-3-1-5-4-6-7z m464 204l24 144c0 3-1 6-2 8 0 0 0 0 0 0-2 1-5 2-8 2l-144-25c-3 0-6-2-6-5-1-3-1-6 2-8l28-29-76-75c-3-4-3-9 0-12l52-52c3-4 9-4 12 0l76 75 28-28c2-2 5-3 8-2 3 1 5 4 6 7z m-130-334l144-24c3 0 6 1 8 2 0 0 0 0 0 0 1 2 2 5 2 8l-25 144c0 3-2 6-5 6-3 1-6 1-8-2l-29-28-75 76c-4 3-9 3-12 0l-52-52c-4-3-4-9 0-12l75-76-28-28c-2-2-3-5-2-8 1-3 4-5 7-6z m-204 464l-144 24c-3 0-6-1-8-2 0 0 0 0 0 0-1-2-2-5-2-8l25-144c0-3 2-6 5-6 3-1 6-1 8 2l29 28 75-76c4-3 9-3 12 0l52 52c4 3 4 9 0 12l-75 76 28 28c2 2 3 5 2 8-1 3-4 5-7 6z"/>
</font></defs></svg> </font></defs></svg>

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Binary file not shown.

View File

@ -20,7 +20,7 @@ block content
//-include views/modules/list-filters-kanban //-include views/modules/list-filters-kanban
include views/modules/kanban-table include views/modules/kanban-table
div.lightbox.lightbox-generic-form(tg-lb-create-edit-userstory) div.lightbox.lightbox-generic-form.lb-create-edit-userstory(tg-lb-create-edit-userstory)
include views/modules/lightbox-us-create-edit include views/modules/lightbox-us-create-edit
div.lightbox.lightbox-generic-bulk(tg-lb-create-bulk-userstories) div.lightbox.lightbox-generic-bulk(tg-lb-create-bulk-userstories)

View File

@ -18,7 +18,7 @@ div.row.us-item-row(ng-repeat="us in visibleUserstories|orderBy:order track by u
span.us-status-bind span.us-status-bind
span.icon.icon-arrow-bottom(tg-check-permission="modify_us") span.icon.icon-arrow-bottom(tg-check-permission="modify_us")
div.points(tg-us-points="us") div.points(tg-backlog-us-points="us")
a.us-points(href="", title="Points") a.us-points(href="", title="Points")
span.points-value 0 span.points-value 0
span.icon.icon-arrow-bottom(tg-check-permission="modify_us") span.icon.icon-arrow-bottom(tg-check-permission="modify_us")

View File

@ -4,9 +4,9 @@ div.kanban-task-inner
div.task-text div.task-text
a.task-assigned(href="", title="Assign User Story") a.task-assigned(href="", title="Assign User Story")
span.task-num(tg-bo-ref="us.ref") span.task-num(tg-bo-ref="us.ref")
a.task-name(href="", title="", tg-bind-html="us.subject", a.task-name(href="", tg-bo-title="us.subject", tg-bind-html="us.subject",
tg-nav="project-userstories-detail:project=project.slug,ref=us.ref") tg-nav="project-userstories-detail:project=project.slug,ref=us.ref")
p.task-points a.task-points(href="", title="Total Us points")
span(tg-bind-html="us.total_points") -- span(tg-bind-html="us.total_points") --
span points span points
a.icon.icon-edit(tg-check-permission="modify_us", href="", title="Edit") a.icon.icon-edit(tg-check-permission="modify_us", href="", title="Edit")

View File

@ -4,8 +4,21 @@ div.kanban-table
h2.task-colum_name(ng-repeat="s in usStatusList track by s.id", h2.task-colum_name(ng-repeat="s in usStatusList track by s.id",
ng-style="{'border-top-color':s.color}") ng-style="{'border-top-color':s.color}")
span(tg-bo-bind="s.name") span(tg-bo-bind="s.name")
a.icon.icon-plus(tg-check-permission="add_us", href="", title="Add New task", ng-click="ctrl.addNewUs('standard', s.id)")
a.icon.icon-bulk(tg-check-permission="add_us", href="", title="Add New bulk", ng-click="ctrl.addNewUs('bulk', s.id)") a.icon.icon-minimize(href="", title="Minimize",
ng-if="statusViewModes[s.id] == 'maximized'",
ng-click="ctrl.updateStatusViewMode(s.id, 'minimized')")
a.icon.icon-maximize(href="", title="Maximize",
ng-if="statusViewModes[s.id] == 'minimized'",
ng-click="ctrl.updateStatusViewMode(s.id, 'maximized')")
a.icon.icon-plus(href="", title="Add New task",
ng-click="ctrl.addNewUs('standard', s.id)",
tg-check-permission="add_us")
a.icon.icon-bulk(href="", title="Add New bulk",
ng-click="ctrl.addNewUs('bulk', s.id)",
tg-check-permission="add_us")
div.kanban-table-body div.kanban-table-body
div.kanban-table-inner(tg-kanban-row-width-fixer) div.kanban-table-inner(tg-kanban-row-width-fixer)
@ -14,4 +27,5 @@ div.kanban-table
tg-kanban-wip-limit, tg-kanban-wip-limit,
tg-kanban-column-height-fixer) tg-kanban-column-height-fixer)
div.kanban-task(ng-repeat="us in usByStatus[status.id] track by us.id", div.kanban-task(ng-repeat="us in usByStatus[status.id] track by us.id",
tg-kanban-userstory, ng-model="us") tg-kanban-userstory, ng-model="us",
ng-class="ctrl.getCardClass(status.id)")

View File

@ -5,9 +5,15 @@ form
fieldset fieldset
input(type="text", name="subject", ng-model="us.subject", tg-i18n="placeholder:common.subject", input(type="text", name="subject", ng-model="us.subject", tg-i18n="placeholder:common.subject",
data-required="true", data-maxlength="500") data-required="true", data-maxlength="500")
fieldset.estimation
tg-us-estimation(ng-model="us")
//- Render by tg-lb-create-edit-userstory
fieldset fieldset
select(name="status", ng-model="us.status", ng-options="s.id as s.name for s in usStatusList", select(name="status", ng-model="us.status", ng-options="s.id as s.name for s in usStatusList",
tg-i18n="placeholder:common.status") tg-i18n="placeholder:common.status")
fieldset fieldset
div(tg-tag-line, editable="true", ng-model="us.tags") div(tg-tag-line, editable="true", ng-model="us.tags")

View File

@ -28,20 +28,18 @@
} }
.kanban-tagline { .kanban-tagline {
@include table-flex(); @include table-flex();
background: $gray-light; //Fallback background: $postit-hover; //Fallback
height: .3rem; //height: .3rem;
} }
.kanban-tag { .kanban-tag {
@include table-flex-child(1, 0, 0, 0); @include table-flex-child(1, 0, 0, 0);
background: $postit-hover; //Fallback background: $postit-hover; //Fallback
height: .3rem; //height: .3rem;
} }
.kanban-task-inner { .kanban-task-inner {
@include table-flex(); @include table-flex();
padding: 1rem 1rem 2rem;
} }
.avatar { .avatar {
@include table-flex-child($flex-basis: 50px);
a { a {
@extend %small; @extend %small;
text-align: center; text-align: center;
@ -69,37 +67,24 @@
color: $postit-dark-hover; color: $postit-dark-hover;
display: block; display: block;
} }
.task-text {
@include table-flex-child($flex-grow: 10, $flex-basis: 50px);
@extend %small;
padding: 0 .5rem 0 .8rem;
word-wrap: break-word;
}
.task-num { .task-num {
color: $grayer; color: $grayer;
margin-right: .5em; margin-right: .3rem;
} }
.task-name { .task-name {
@extend %bold; @extend %bold;
color: $grayer; color: $grayer;
word-wrap: break-word;
}
.task-text {
@include table-flex-child($flex-grow: 10, $flex-basis: 50px);
@extend %small;
padding: 0 .5rem 0 1rem;
word-wrap: break-word;
}
.task-points {
@extend %small;
color: darken($postit-hover, 15%);
margin: 0;
span {
display: inline-block;
&:first-child {
padding-right: .2rem;
}
}
} }
.icon-edit, .icon-edit,
.icon-drag-h { .icon-drag-h {
@include transition(opacity .2s linear); @include transition(opacity .2s linear);
@extend %large; @extend %large;
bottom: .2rem;
color: $postit-hover; color: $postit-hover;
opacity: 0; opacity: 0;
position: absolute; position: absolute;
@ -108,12 +93,86 @@
color: darken($postit-hover, 15%); color: darken($postit-hover, 15%);
} }
} }
}
.kanban-task-maximized {
.kanban-task-inner {
padding: 1rem 1rem 2rem;
}
.avatar {
@include table-flex-child($flex-basis: 50px);
}
.task-name {
word-wrap: break-word;
}
.icon-edit { .icon-edit {
bottom: .2rem;
right: .5rem; right: .5rem;
} }
.icon-drag-h { .icon-drag-h {
@extend %xlarge; @extend %xlarge;
bottom: .2rem;
cursor: move; cursor: move;
right: 45%; right: 45%;
} }
.task-points {
@extend %small;
color: darken($postit-hover, 15%);
display: block;
margin: 0;
span {
display: inline-block;
&:first-child {
padding-right: .2rem;
}
}
}
.kanban-tagline {
height: .3rem;
}
.kanban-tag {
height: .3rem;
}
}
.kanban-task-minimized {
.kanban-task-inner {
padding: .3rem;
}
.avatar {
@include table-flex-child($flex-basis: 40px);
}
.task-num {
vertical-align: top;
}
.task-name {
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 70%;
}
.task-points {
display: none;
}
.icon-edit,
.icon-drag-h {
top: 1.4rem;
}
.icon-edit {
bottom: .2rem;
right: 1rem;
}
.icon-drag-h {
@extend %medium;
@include transform(rotate(90deg));
cursor: move;
right: .1rem;
}
.kanban-tagline {
height: .2rem;
}
.kanban-tag {
height: .2rem;
}
} }

View File

@ -247,3 +247,9 @@ a:visited {
.icon-spinner:before { .icon-spinner:before {
content: 'E'; content: 'E';
} }
.icon-minimize:before {
content: 'J';
}
.icon-maximize:before {
content: 'K';
}

View File

@ -160,8 +160,10 @@
} }
.points-per-role { .points-per-role {
@include table-flex();
position: relative; position: relative;
> li { > li {
@include table-flex-child(1, 18%, 0);
@include transition(color .3s linear); @include transition(color .3s linear);
border-right: 1px solid rgba($grayer, .3); border-right: 1px solid rgba($grayer, .3);
color: rgba($grayer, .3); color: rgba($grayer, .3);
@ -169,14 +171,13 @@
margin: .5rem .1rem; margin: .5rem .1rem;
position: relative; position: relative;
text-align: center; text-align: center;
width: 18%;
&.active { &.active {
color: rgba($green-taiga, 1); color: rgba($green-taiga, 1);
} }
&:first-child { &:first-child {
opacity: 1; opacity: 1;
} }
&:nth-child(5n) { &:last-child {
border: 0; border: 0;
} }
} }

View File

@ -501,3 +501,12 @@
width: 600px; width: 600px;
} }
} }
.lb-create-edit-userstory {
.points-per-role {
margin-bottom: 1rem;
li {
margin: .5rem .1rem;
}
}
}

View File

@ -44,6 +44,11 @@ $column-margin: 0 10px 0 0;
&.icon-plus { &.icon-plus {
right: 2rem; right: 2rem;
} }
&.icon-maximize,
&.icon-minimize {
left: .5rem;
right: inherit;
}
} }
} }
} }
@ -51,7 +56,6 @@ $column-margin: 0 10px 0 0;
.kanban-table-body { .kanban-table-body {
@include table-flex(); @include table-flex();
@extend %medium; @extend %medium;
//height: 700px;
overflow: hidden; overflow: hidden;
overflow-x: auto; overflow-x: auto;
width: 100%; width: 100%;