### # 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/kanban/main.coffee ### taiga = @.taiga mixOf = @.taiga.mixOf toggleText = @.taiga.toggleText scopeDefer = @.taiga.scopeDefer bindOnce = @.taiga.bindOnce groupBy = @.taiga.groupBy timeout = @.taiga.timeout module = angular.module("taigaKanban") # Vars defaultViewMode = "maximized" defaultViewModes = { maximized: { cardClass: "kanban-task-maximized" } minimized: { cardClass: "kanban-task-minimized" } } ############################################################################# ## Kanban Controller ############################################################################# class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.FiltersMixin) @.$inject = [ "$scope", "$rootScope", "$tgRepo", "$tgConfirm", "$tgResources", "$routeParams", "$q", "$tgLocation", "$appTitle", "$tgNavUrls", "$tgEvents", "tgLoader" ] constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @appTitle, @navUrls, @events, tgLoader) -> _.bindAll(@) @scope.sectionName = "Kanban" @scope.statusViewModes = {} promise = @.loadInitialData() # On Success promise.then => @appTitle.set("Kanban - " + @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) @scope.$on("usform:new:success", @.loadUserstories) @scope.$on("usform:bulk:success", @.loadUserstories) @scope.$on("usform:edit:success", @.loadUserstories) @scope.$on("assigned-to:added", @.onAssignedToChanged) @scope.$on("kanban:us:move", @.moveUs) # Template actions addNewUs: (type, statusId) -> switch type when "standard" then @rootscope.$broadcast("usform:new", @scope.projectId, statusId, @scope.usStatusList) when "bulk" then @rootscope.$broadcast("usform:bulk", @scope.projectId, statusId) changeUsAssignedTo: (us) -> @rootscope.$broadcast("assigned-to:add", us) # Scope Events Handlers onAssignedToChanged: (ctx, userid, us) -> us.assigned_to = userid promise = @repo.save(us) promise.then null, -> console.log "FAIL" # TODO # Load data methods loadProjectStats: -> return @rs.projects.stats(@scope.projectId).then (stats) => @scope.stats = stats if stats.total_points completedPercentage = Math.round(100 * stats.closed_points / stats.total_points) else completedPercentage = 0 @scope.stats.completedPercentage = "#{completedPercentage}%" return stats refreshTagsColors: -> return @rs.projects.tagsColors(@scope.projectId).then (tags_colors) => @scope.project.tags_colors = tags_colors loadUserstories: -> return @rs.userstories.listAll(@scope.projectId).then (userstories) => @scope.userstories = userstories @scope.usByStatus = _.groupBy(userstories, "status") for status in @scope.usStatusList if not @scope.usByStatus[status.id]? @scope.usByStatus[status.id] = [] @scope.usByStatus[status.id] = _.sortBy(@scope.usByStatus[status.id], "kanban_order") # The broadcast must be executed when the DOM has been fully reloaded. # We can't assure when this exactly happens so we need a defer scopeDefer @scope, => @scope.$broadcast("userstories:loaded") return userstories loadKanban: -> return @q.all([ @.refreshTagsColors(), @.loadProjectStats(), @.loadUserstories() ]) loadProject: -> return @rs.projects.get(@scope.projectId).then (project) => @scope.project = project @scope.points = _.sortBy(project.points, "order") @scope.pointsById = groupBy(project.points, (x) -> x.id) @scope.usStatusById = groupBy(project.us_statuses, (x) -> x.id) @scope.usStatusList = _.sortBy(project.us_statuses, "order") @.generateStatusViewModes() @scope.$emit("project:loaded", project) return project initializeSubscription: -> routingKey1 = "changes.project.#{@scope.projectId}.userstories" @events.subscribe @scope, routingKey1, (message) => @.loadUserstories() loadInitialData: -> # Resolve project slug promise = @repo.resolve({pslug: @params.pslug}).then (data) => @scope.projectId = data.project @.initializeSubscription() return data return promise.then(=> @.loadProject()) .then(=> @.loadUsersAndRoles()) .then(=> @.loadKanban()) .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") -> return _.map(uses, (x) -> {"us_id": x.id, "order": x[field]}) resortUserStories: (uses) -> items = [] for item, index in uses item.kanban_order = index if item.isModified() items.push(item) return items moveUs: (ctx, us, statusId, index) -> if us.status != statusId # Remove us from old status column r = @scope.usByStatus[us.status].indexOf(us) @scope.usByStatus[us.status].splice(r, 1) # Add us to new status column. @scope.usByStatus[statusId].splice(index, 0, us) us.status = statusId else r = @scope.usByStatus[statusId].indexOf(us) @scope.usByStatus[statusId].splice(r, 1) @scope.usByStatus[statusId].splice(index, 0, us) itemsToSave = @.resortUserStories(@scope.usByStatus[statusId]) @scope.usByStatus[statusId] = _.sortBy(@scope.usByStatus[statusId], "kanban_order") # Persist the userstory promise = @repo.save(us) # Rehash userstories order field # and persist in bulk all changes. promise = promise.then => itemsToSave = _.reject(itemsToSave, {"id": us.id}) data = @.prepareBulkUpdateData(itemsToSave) return @rs.userstories.bulkUpdateKanbanOrder(us.project, data).then => return itemsToSave return promise module.controller("KanbanController", KanbanController) ############################################################################# ## Kanban Directive ############################################################################# KanbanDirective = ($repo, $rootscope) -> link = ($scope, $el, $attrs) -> tableBodyDom = $el.find(".kanban-table-body") tableBodyDom.on "scroll", (event) -> target = angular.element(event.currentTarget) tableHeaderDom = $el.find(".kanban-table-header .kanban-table-inner") tableHeaderDom.css("left", -1 * target.scrollLeft()) return {link: link} module.directive("tgKanban", ["$tgRepo", "$rootScope", KanbanDirective]) ############################################################################# ## Kanban Row Size Fixer Directive ############################################################################# KanbanRowWidthFixerDirective = -> link = ($scope, $el, $attrs) -> bindOnce $scope, "usStatusList", (statuses) -> itemSize = 310 size = (statuses.length * itemSize) - 10 $el.css("width", "#{size}px") return {link: link} module.directive("tgKanbanRowWidthFixer", KanbanRowWidthFixerDirective) ############################################################################# ## Kanban Column Height Fixer Directive ############################################################################# KanbanColumnHeightFixerDirective = -> mainPadding = 32 # px scrollPadding = 0 # px renderSize = ($el) -> elementOffset = $el.parent().parent().offset().top windowHeight = angular.element(window).height() columnHeight = windowHeight - elementOffset - mainPadding - scrollPadding $el.css("height", "#{columnHeight}px") link = ($scope, $el, $attrs) -> timeout(500, -> renderSize($el)) return {link:link} module.directive("tgKanbanColumnHeightFixer", KanbanColumnHeightFixerDirective) ############################################################################# ## Kaban User Story Directive ############################################################################# KanbanUserstoryDirective = ($rootscope) -> link = ($scope, $el, $attrs, $model) -> $el.find(".icon-edit").on "click", (event) -> if $el.find(".icon-edit").hasClass("noclick") return $scope.$apply -> $rootscope.$broadcast("usform:edit", $model.$modelValue) if $scope.us.is_blocked $el.addClass("blocked") $el.disableSelection() return { templateUrl: "/partials/views/components/kanban-task.html" link: link require: "ngModel" } module.directive("tgKanbanUserstory", ["$rootScope", KanbanUserstoryDirective]) ############################################################################# ## Kaban WIP Limit Directive ############################################################################# KanbanWipLimitDirective = -> link = ($scope, $el, $attrs) -> $el.disableSelection() redrawWipLimit = -> $el.find(".kanban-wip-limit").remove() timeout 200, -> element = $el.find(".kanban-task")[$scope.status.wip_limit] if element angular.element(element).before("
") $scope.$on "redraw:wip", redrawWipLimit $scope.$on "kanban:us:move", redrawWipLimit $scope.$on "usform:new:success", redrawWipLimit $scope.$on "usform:bulk:success", redrawWipLimit return {link: link} module.directive("tgKanbanWipLimit", KanbanWipLimitDirective) ############################################################################# ## Kanban User Directive ############################################################################# KanbanUserDirective = ($log) -> template = _.template("""
class="not-clickable"<% } %>> <%- name %>
""") # TODO: i18n clickable = false link = ($scope, $el, $attrs, $model) -> if not $attrs.tgKanbanUserAvatar return $log.error "KanbanUserDirective: no attr is defined" wtid = $scope.$watch $attrs.tgKanbanUserAvatar, (v) -> if not $scope.usersById? $log.error "KanbanUserDirective 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.changeUsAssignedTo(us) bindOnce $scope, "project", (project) -> if project.my_permissions.indexOf("modify_us") > -1 clickable = true $el.on "click", (event) => if $el.find("a").hasClass("noclick") return us = $model.$modelValue $ctrl = $el.controller() $ctrl.changeUsAssignedTo(us) return {link: link, require:"ngModel"} module.directive("tgKanbanUserAvatar", ["$log", KanbanUserDirective])