Initial kanban code.

stable
Andrey Antukh 2014-07-21 16:46:02 +02:00
parent b172b07ed9
commit a8f9bd0303
10 changed files with 410 additions and 55 deletions

View File

@ -26,6 +26,7 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide) ->
$routeProvider.when("/project/:pslug/backlog", {templateUrl: "/partials/backlog.html"}) $routeProvider.when("/project/:pslug/backlog", {templateUrl: "/partials/backlog.html"})
$routeProvider.when("/project/:pslug/taskboard/:id", {templateUrl: "/partials/taskboard.html"}) $routeProvider.when("/project/:pslug/taskboard/:id", {templateUrl: "/partials/taskboard.html"})
$routeProvider.when("/project/:pslug/search", {templateUrl: "/partials/search.html"}) $routeProvider.when("/project/:pslug/search", {templateUrl: "/partials/search.html"})
$routeProvider.when("/project/:pslug/kanban", {templateUrl: "/partials/kanban.html"})
# User stories # User stories
$routeProvider.when("/project/:pslug/us/:usref", $routeProvider.when("/project/:pslug/us/:usref",
@ -121,6 +122,7 @@ modules = [
# Specific Modules # Specific Modules
"taigaBacklog", "taigaBacklog",
"taigaTaskboard", "taigaTaskboard",
"taigaKanban"
"taigaIssues", "taigaIssues",
"taigaUserStories", "taigaUserStories",
"taigaTasks", "taigaTasks",

View File

@ -54,6 +54,7 @@ urls = {
"project": "/project/:project", "project": "/project/:project",
"project-backlog": "/project/:project/backlog", "project-backlog": "/project/:project/backlog",
"project-taskboard": "/project/:project/taskboard/:sprint", "project-taskboard": "/project/:project/taskboard/:sprint",
"project-kanban": "/project/:project/kanban",
"project-issues": "/project/:project/issues", "project-issues": "/project/:project/issues",
"project-search": "/project/:project/search", "project-search": "/project/:project/search",
"project-issues-detail": "/project/:project/issues/:ref", "project-issues-detail": "/project/:project/issues/:ref",

View File

@ -0,0 +1,22 @@
###
# 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/kanban.coffee
###
module = angular.module("taigaKanban", [])

View File

@ -0,0 +1,273 @@
###
# 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/kanban/main.coffee
###
taiga = @.taiga
mixOf = @.taiga.mixOf
toggleText = @.taiga.toggleText
scopeDefer = @.taiga.scopeDefer
bindOnce = @.taiga.bindOnce
groupBy = @.taiga.groupBy
module = angular.module("taigaKanban")
#############################################################################
## Kanban Controller
#############################################################################
class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.FiltersMixin)
@.$inject = [
"$scope",
"$rootScope",
"$tgRepo",
"$tgConfirm",
"$tgResources",
"$routeParams",
"$q",
"$tgLocation"
]
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location) ->
_.bindAll(@)
@scope.sectionName = "Kanban"
promise = @.loadInitialData()
promise.then null, =>
console.log "FAIL"
# @scope.$on("usform:bulk:success", @.loadUserstories)
# @scope.$on("sprintform:create:success", @.loadSprints)
# @scope.$on("sprintform:create:success", @.loadProjectStats)
# @scope.$on("sprintform:remove:success", @.loadSprints)
# @scope.$on("sprintform:remove:success", @.loadProjectStats)
# @scope.$on("usform:new:success", @.loadUserstories)
# @scope.$on("usform:edit:success", @.loadUserstories)
# @scope.$on("sprint:us:move", @.moveUs)
# @scope.$on("sprint:us:moved", @.loadSprints)
# @scope.$on("sprint:us:moved", @.loadProjectStats)
loadProjectStats: ->
return @rs.projects.stats(@scope.projectId).then (stats) =>
@scope.stats = stats
completedPercentage = Math.round(100 * stats.closed_points / stats.total_points)
@scope.stats.completedPercentage = "#{completedPercentage}%"
return stats
loadUserstories: ->
return @rs.userstories.listUnassigned(@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] = []
# 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([
@.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, "id")
return project
loadInitialData: ->
# Resolve project slug
promise = @repo.resolve({pslug: @params.pslug}).then (data) =>
@scope.projectId = data.project
return data
return promise.then(=> @.loadProject())
.then(=> @.loadUsersAndRoles())
.then(=> @.loadKanban())
prepareBulkUpdateData: (uses) ->
return _.map(uses, (x) -> [x.id, x.order])
resortUserStories: (uses) ->
items = []
for item, index in uses
item.order = index
if item.isModified()
items.push(item)
return items
moveUs: (ctx, us, newUsIndex, newSprintId) ->
oldSprintId = us.milestone
# In the same sprint or in the backlog
if newSprintId == oldSprintId
items = null
userstories = null
if newSprintId == null
userstories = @scope.userstories
else
userstories = @scope.sprintsById[newSprintId].user_stories
@scope.$apply ->
r = userstories.indexOf(us)
userstories.splice(r, 1)
userstories.splice(newUsIndex, 0, us)
# Rehash userstories order field
items = @.resortUserStories(userstories)
data = @.prepareBulkUpdateData(items)
# Persist in bulk all affected
# userstories with order change
promise = @rs.userstories.bulkUpdateOrder(us.project, data).then =>
@rootscope.$broadcast("sprint:us:moved", us, oldSprintId, newSprintId)
promise.then null, ->
console.log "FAIL"
return promise
# From sprint to backlog
if newSprintId == null
us.milestone = null
@scope.$apply =>
# Add new us to backlog userstories list
@scope.userstories.splice(newUsIndex, 0, us)
@scope.visibleUserstories.splice(newUsIndex, 0, us)
# Execute the prefiltering of user stories
@.filterVisibleUserstories()
# Remove the us from the sprint list.
sprint = @scope.sprintsById[oldSprintId]
r = sprint.user_stories.indexOf(us)
sprint.user_stories.splice(r, 1)
# Persist the milestone change of userstory
promise = @repo.save(us)
# Rehash userstories order field
# and persist in bulk all changes.
promise = promise.then =>
items = @.resortUserStories(@scope.userstories)
data = @.prepareBulkUpdateData(items)
promise = @rs.userstories.bulkUpdateOrder(us.project, data).then =>
@rootscope.$broadcast("sprint:us:moved", us, oldSprintId, newSprintId)
promise.then null, ->
# TODO
console.log "FAIL"
return promise
# From backlog to sprint
newSprint = @scope.sprintsById[newSprintId]
if us.milestone == null
us.milestone = newSprintId
@scope.$apply =>
# Add moving us to sprint user stories list
newSprint.user_stories.splice(newUsIndex, 0, us)
# Remove moving us from backlog userstories lists.
r = @scope.visibleUserstories.indexOf(us)
@scope.visibleUserstories.splice(r, 1)
r = @scope.userstories.indexOf(us)
@scope.userstories.splice(r, 1)
# From sprint to sprint
else
us.milestone = newSprintId
@scope.$apply =>
# Add new us to backlog userstories list
newSprint.user_stories.splice(newUsIndex, 0, us)
# Remove the us from the sprint list.
oldSprint = @scope.sprintsById[oldSprintId]
r = oldSprint.user_stories.indexOf(us)
oldSprint.user_stories.splice(r, 1)
# Persist the milestone change of userstory
promise = @repo.save(us)
# Rehash userstories order field
# and persist in bulk all changes.
promise = promise.then =>
items = @.resortUserStories(newSprint.user_stories)
data = @.prepareBulkUpdateData(items)
promise = @rs.userstories.bulkUpdateOrder(us.project, data).then =>
@rootscope.$broadcast("sprint:us:moved", us, oldSprintId, newSprintId)
promise.then null, ->
# TODO
console.log "FAIL"
return promise
## Template actions
# editUserStory: (us) ->
# @rootscope.$broadcast("usform:edit", us)
# deleteUserStory: (us) ->
# #TODO: i18n
# title = "Delete User Story"
# subtitle = us.subject
# @confirm.ask(title, subtitle).then =>
# # We modify the userstories in scope so the user doesn't see the removed US for a while
# @scope.userstories = _.without(@scope.userstories, us);
# @filterVisibleUserstories()
# @.repo.remove(us).then =>
# @.loadBacklog()
# addNewUs: (type) ->
# switch type
# when "standard" then @rootscope.$broadcast("usform:new")
# when "bulk" then @rootscope.$broadcast("usform:bulk")
module.controller("KanbanController", KanbanController)
#############################################################################
## Kanban Directive
#############################################################################
KanbanDirective = ($repo, $rootscope) ->
link = ($scope, $el, $attrs) ->
return {link: link}
module.directive("tgKanban", ["$tgRepo", "$rootScope", KanbanDirective])

View File

@ -0,0 +1,94 @@
###
# 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/kanban/sortable.coffee
###
taiga = @.taiga
mixOf = @.taiga.mixOf
toggleText = @.taiga.toggleText
scopeDefer = @.taiga.scopeDefer
bindOnce = @.taiga.bindOnce
groupBy = @.taiga.groupBy
module = angular.module("taigaKanban")
#############################################################################
## Sortable Directive
#############################################################################
KanbanSortableDirective = ($repo, $rs, $rootscope) ->
#########################
## Drag & Drop Link
#########################
link = ($scope, $el, $attrs) ->
oldParentScope = null
newParentScope = null
itemEl = null
tdom = $el
deleteElement = (itemEl) ->
# Completelly remove item and its scope from dom
itemEl.scope().$destroy()
itemEl.off()
itemEl.remove()
tdom.sortable({
handle: ".icon-drag-h",
dropOnEmpty: true
connectWith: ".taskboard_task-playground"
revert: 400
})
tdom.on "sortstop", (event, ui) ->
# parentEl = ui.item.parent()
# itemEl = ui.item
# itemTask = itemEl.scope().task
# itemIndex = itemEl.index()
# newParentScope = parentEl.scope()
# oldUsId = if oldParentScope.us then oldParentScope.us.id else null
# oldStatusId = oldParentScope.st.id
# newUsId = if newParentScope.us then newParentScope.us.id else null
# newStatusId = newParentScope.st.id
# if newStatusId != oldStatusId or newUsId != oldUsId
# deleteElement(itemEl)
# $scope.$apply ->
# $rootscope.$broadcast("taskboard:task:move", itemTask, newUsId, newStatusId, itemIndex)
tdom.on "sortstart", (event, ui) ->
oldParentScope = ui.item.parent().scope()
$scope.$on "$destroy", ->
$el.off()
return {link: link}
module.directive("tgKanbanSortable", [
"$tgRepo",
"$tgResources",
"$rootScope",
KanbanSortableDirective
])

View File

@ -82,7 +82,7 @@ ProjectMenuDirective = ($log, $compile, $rootscope) ->
</a> </a>
</li> </li>
<li id="nav-kanban"> <li id="nav-kanban">
<a href="" title="Kanban"> <a href="" title="Kanban" tg-nav="project-kanban:project=project.slug">
<span class="icon icon-kanban"></span><span class="item">Kanban</span> <span class="icon icon-kanban"></span><span class="item">Kanban</span>
</a> </a>
</li> </li>

View File

@ -1,23 +1,23 @@
extends layout extends dummy-layout
block head block head
title Taiga Project management web application with scrum in mind! title Taiga Project management web application with scrum in mind!
block content block content
div.wrapper div.wrapper(tg-kanban, ng-controller="KanbanController as ctrl")
section.main.kanban section.main.kanban
div.kanban-detail-header div.kanban-detail-header
h1 h1
span ProjectName span(tg-bo-html="project.name")
span.green Sprint Name // span.green Sprint Name
span.date 02/10/2014-15/10/2014 // span.date 02/10/2014-15/10/2014
div.kanban-settings div.kanban-settings
a.button.button-trans(href="", title="Filter") // a.button.button-trans(href="", title="Filter")
span.icon.icon-filter // span.icon.icon-filter
span Filters // span Filters
a.button.button-gray(href="", title="Filter") a.button.button-gray(href="", title="Filter")
span Show Statistics span Show Statistics
//-include views/components/large-summary //-include views/components/large-summary
include views/modules/burndown //-include views/modules/burndown
//-include views/modules/list-filters-kanban //-include views/modules/list-filters-kanban
include views/modules/kanban-table include views/modules/kanban-table

View File

@ -12,4 +12,4 @@ div.kanban-task-inner
a(href="", title="Change assignation") Username a(href="", title="Change assignation") Username
a.icon.icon-edit(href="", title="Edit", ng-click="ctrl.editTask(task)") a.icon.icon-edit(href="", title="Edit", ng-click="ctrl.editTask(task)")
a.icon.icon-drag-h(href="", title="Drag&Drop") a.icon.icon-drag-h(href="", title="Drag&Drop")
a.task-points(href="", title="task points") 8 a.task-points(href="", title="task points", tg-bo-html="us.total_points") --

View File

@ -1,51 +1,13 @@
div.kanban-table div.kanban-table
div.kanban-table-header div.kanban-table-header
div.kanban-table-inner div.kanban-table-inner
h2.task-colum_name h2.task-colum_name(ng-repeat="s in usStatusList track by s.id")
span Task span(tg-bo-html="s.name")
a.icon.icon-plus(href="", title="Add New task")
h2.task-colum_name
span Open
a.icon.icon-plus(href="", title="Add New task")
h2.task-colum_name
span Ready for test
a.icon.icon-plus(href="", title="Add New task")
h2.task-colum_name
span Closed
a.icon.icon-plus(href="", title="Add New task")
h2.task-colum_name
span Task
a.icon.icon-plus(href="", title="Add New task")
h2.task-colum_name
span Open
a.icon.icon-plus(href="", title="Add New task")
h2.task-colum_name
span Ready for test
a.icon.icon-plus(href="", title="Add New task")
h2.task-colum_name
span Closed
a.icon.icon-plus(href="", title="Add New task") a.icon.icon-plus(href="", title="Add New task")
div.kanban-table-body div.kanban-table-body
div.kanban-table-inner div.kanban-table-inner
div.taskboard_task-playground.task-column div.taskboard_task-playground.task-column(ng-repeat="s in usStatusList track by s.id",
div.kanban-task tg-kanban-sortable)
include ../components/kanban-task div.kanban-task(ng-repeat="us in usByStatus[s.id] track by us.id")
div.taskboard_task-playground.task-column
div.kanban-task
include ../components/kanban-task
div.taskboard_task-playground.task-column
div.taskboard_task-playground.task-column
div.kanban-task
include ../components/kanban-task
div.taskboard_task-playground.task-column
div.kanban-task
include ../components/kanban-task
div.taskboard_task-playground.task-column
div.kanban-task
include ../components/kanban-task
div.taskboard_task-playground.task-column
div.taskboard_task-playground.task-column
div.kanban-task
include ../components/kanban-task include ../components/kanban-task

View File

@ -42,6 +42,7 @@ paths = {
"app/coffee/modules/common/*.coffee", "app/coffee/modules/common/*.coffee",
"app/coffee/modules/backlog/*.coffee", "app/coffee/modules/backlog/*.coffee",
"app/coffee/modules/taskboard/*.coffee", "app/coffee/modules/taskboard/*.coffee",
"app/coffee/modules/kanban/*.coffee",
"app/coffee/modules/issues/*.coffee", "app/coffee/modules/issues/*.coffee",
"app/coffee/modules/userstories/*.coffee", "app/coffee/modules/userstories/*.coffee",
"app/coffee/modules/tasks/*.coffee", "app/coffee/modules/tasks/*.coffee",