### # Copyright (C) 2014 Andrey Antukh # Copyright (C) 2014 Jesús Espino Garcia # Copyright (C) 2014 David Barragán Merino # # 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 . # # File: modules/taskboard.coffee ### taiga = @.taiga toggleText = @.taiga.toggleText mixOf = @.taiga.mixOf groupBy = @.taiga.groupBy bindOnce = @.taiga.bindOnce scopeDefer = @.taiga.scopeDefer timeout = @.taiga.timeout module = angular.module("taigaTaskboard") ############################################################################# ## Taskboard Controller ############################################################################# class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin) @.$inject = [ "$scope", "$rootScope", "$tgRepo", "$tgConfirm", "$tgResources", "$routeParams", "$q", "$appTitle", "$tgLocation", "$tgNavUrls" "$tgEvents" "tgLoader" ] constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @appTitle, @location, @navUrls, @events, tgLoader) -> _.bindAll(@) @scope.sectionName = "Taskboard" promise = @.loadInitialData() # On Success promise.then => @appTitle.set("Taskboard - " + @scope.project.name) tgLoader.pageLoaded() # On Error promise.then null, (xhr) => if xhr and xhr.status == 404 @location.path(@navUrls.resolve("not-found")) @location.replace() return @q.reject(xhr) # TODO: Reload entire taskboard after create/edit tasks seems # a big overhead. It should be optimized in near future. @scope.$on("taskform:bulk:success", => @.loadTaskboard()) @scope.$on("taskform:new:success", => @.loadTaskboard()) @scope.$on("taskform:edit:success", => @.loadTaskboard()) @scope.$on("taskboard:task:move", @.taskMove) @scope.$on "assigned-to:added", (ctx, userId, task) => task.assigned_to = userId promise = @repo.save(task) promise.then null, -> console.log "FAIL" # TODO initializeSubscription: -> routingKey = "changes.project.#{@scope.projectId}.tasks" @events.subscribe @scope, routingKey, (message) => @.loadTaskboard() loadProject: -> return @rs.projects.get(@scope.projectId).then (project) => @scope.project = project @scope.$emit('project:loaded', project) # Not used at this momment @scope.pointsList = _.sortBy(project.points, "order") # @scope.roleList = _.sortBy(project.roles, "order") @scope.pointsById = groupBy(project.points, (e) -> e.id) @scope.roleById = groupBy(project.roles, (e) -> e.id) @scope.taskStatusList = _.sortBy(project.task_statuses, "order") @scope.usStatusList = _.sortBy(project.us_statuses, "order") @scope.usStatusById = groupBy(project.us_statuses, (e) -> e.id) return project loadSprintStats: -> return @rs.sprints.stats(@scope.projectId, @scope.sprintId).then (stats) => totalPointsSum =_.reduce(_.values(stats.total_points), ((res, n) -> res + n), 0) completedPointsSum = _.reduce(_.values(stats.completed_points), ((res, n) -> res + n), 0) remainingPointsSum = totalPointsSum - completedPointsSum remainingTasks = stats.total_tasks - stats.completed_tasks @scope.stats = stats @scope.stats.totalPointsSum = totalPointsSum @scope.stats.completedPointsSum = completedPointsSum @scope.stats.remainingPointsSum = remainingPointsSum @scope.stats.remainingTasks = remainingTasks if stats.totalPointsSum @scope.stats.completedPercentage = Math.round(100 * stats.completedPointsSum / stats.totalPointsSum) else @scope.stats.completedPercentage = 0 @scope.stats.openTasks = stats.total_tasks - stats.completed_tasks return stats refreshTagsColors: -> return @rs.projects.tagsColors(@scope.projectId).then (tags_colors) => @scope.project.tags_colors = tags_colors loadSprint: -> return @rs.sprints.get(@scope.projectId, @scope.sprintId).then (sprint) => @scope.sprint = sprint @scope.userstories = sprint.user_stories return sprint loadTasks: -> return @rs.tasks.list(@scope.projectId, @scope.sprintId).then (tasks) => @scope.tasks = tasks @scope.usTasks = {} # Iterate over all userstories and # null userstory for unassigned tasks for us in _.union(@scope.userstories, [{id:null}]) @scope.usTasks[us.id] = {} for status in @scope.taskStatusList @scope.usTasks[us.id][status.id] = [] for task in @scope.tasks if @scope.usTasks[task.user_story]? and @scope.usTasks[task.user_story][task.status]? @scope.usTasks[task.user_story][task.status].push(task) return tasks loadTaskboard: -> return @q.all([ @.refreshTagsColors(), @.loadSprintStats(), @.loadSprint().then(=> @.loadTasks()) ]) loadInitialData: -> params = { pslug: @params.pslug sslug: @params.sslug } promise = @repo.resolve(params).then (data) => @scope.projectId = data.project @scope.sprintId = data.milestone @.initializeSubscription() return data return promise.then(=> @.loadProject()) .then(=> @.loadUsersAndRoles()) .then(=> @.loadTaskboard()) taskMove: (ctx, task, usId, statusId, order) -> # Remove task from old position r = @scope.usTasks[task.user_story][task.status].indexOf(task) @scope.usTasks[task.user_story][task.status].splice(r, 1) # Add task to new position @scope.usTasks[usId][statusId].splice(order, 0, task) task.user_story = usId task.status = statusId task.order = order promise = @repo.save(task) promise.then => @.loadSprintStats() promise.then null, => console.log "FAIL TASK SAVE" ## Template actions addNewTask: (type, us) -> switch type when "standard" then @rootscope.$broadcast("taskform:new", @scope.sprintId, us?.id) when "bulk" then @rootscope.$broadcast("taskform:bulk", @scope.sprintId, us?.id) editTaskAssignedTo: (task) -> @rootscope.$broadcast("assigned-to:add", task) module.controller("TaskboardController", TaskboardController) ############################################################################# ## TaskboardDirective ############################################################################# TaskboardDirective = ($rootscope) -> link = ($scope, $el, $attrs) -> $ctrl = $el.controller() $el.on "click", ".toggle-analytics-visibility", (event) -> event.preventDefault() target = angular.element(event.currentTarget) target.toggleClass('active'); #toggleText(target, ["Hide statistics", "Show statistics"]) # TODO: i18n $rootscope.$broadcast("taskboard:graph:toggle-visibility") tableBodyDom = $el.find(".taskboard-table-body") tableBodyDom.on "scroll", (event) -> target = angular.element(event.currentTarget) tableHeaderDom = $el.find(".taskboard-table-header .taskboard-table-inner") tableHeaderDom.css("left", -1 * target.scrollLeft()) $scope.$on "$destroy", -> $el.off() return {link: link} module.directive("tgTaskboard", ["$rootScope", TaskboardDirective]) ############################################################################# ## Taskboard Task Directive ############################################################################# TaskboardTaskDirective = ($rootscope) -> link = ($scope, $el, $attrs, $model) -> $el.disableSelection() if $scope.task.is_blocked $el.addClass('blocked') $el.find(".icon-edit").on "click", (event) -> if $el.find('.icon-edit').hasClass('noclick') return $scope.$apply -> $rootscope.$broadcast("taskform:edit", $scope.task) return {link:link} module.directive("tgTaskboardTask", ["$rootScope", TaskboardTaskDirective]) ############################################################################# ## Taskboard Task Row Size Fixer Directive ############################################################################# TaskboardRowWidthFixerDirective = -> link = ($scope, $el, $attrs) -> bindOnce $scope, "taskStatusList", (statuses) -> itemSize = 300 + (10 * statuses.length) size = (1 + statuses.length) * itemSize $el.css("width", "#{size}px") return {link: link} module.directive("tgTaskboardRowWidthFixer", TaskboardRowWidthFixerDirective) ############################################################################# ## Taskboard Table Height Fixer Directive ############################################################################# TaskboardTableHeightFixerDirective = -> mainPadding = 32 # px renderSize = ($el) -> elementOffset = $el.offset().top windowHeight = angular.element(window).height() columnHeight = windowHeight - elementOffset - mainPadding $el.css("height", "#{columnHeight}px") link = ($scope, $el, $attrs) -> timeout(500, -> renderSize($el)) return {link:link} module.directive("tgTaskboardTableHeightFixer", TaskboardTableHeightFixerDirective) ############################################################################# ## Taskboard User Directive ############################################################################# TaskboardUserDirective = ($log) -> template = _.template("""
class="not-clickable"<% } %>> <%- name %>
""") # TODO: i18n clickable = false link = ($scope, $el, $attrs, $model) -> if not $attrs.tgTaskboardUserAvatar? return $log.error "TaskboardUserDirective: no attr is defined" wtid = $scope.$watch $attrs.tgTaskboardUserAvatar, (v) -> if not $scope.usersById? $log.error "TaskboardUserDirective requires userById set in scope." wtid() else user = $scope.usersById[v] render(user) render = (user) -> if user is undefined ctx = {name: "Unassigned", imgurl: "/images/unnamed.png", clickable: clickable} else ctx = {name: user.full_name_display, imgurl: user.photo, clickable: clickable} html = template(ctx) $el.html(html) username_label = $el.parent().find("a.task-assigned") username_label.html(ctx.name) username_label.on "click", (event) -> if $el.find('a').hasClass('noclick') return us = $model.$modelValue $ctrl = $el.controller() $ctrl.editTaskAssignedTo(us) bindOnce $scope, "project", (project) -> if project.my_permissions.indexOf("modify_task") > -1 clickable = true $el.on "click", (event) => if $el.find('a').hasClass('noclick') return us = $model.$modelValue $ctrl = $el.controller() $ctrl.editTaskAssignedTo(us) return {link: link, require:"ngModel"} module.directive("tgTaskboardUserAvatar", ["$log", TaskboardUserDirective])