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

View File

@ -54,6 +54,7 @@ urls = {
"project": "/project/:project",
"project-backlog": "/project/:project/backlog",
"project-taskboard": "/project/:project/taskboard/:sprint",
"project-kanban": "/project/:project/kanban",
"project-issues": "/project/:project/issues",
"project-search": "/project/:project/search",
"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>
</li>
<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>
</a>
</li>

View File

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

View File

@ -12,4 +12,4 @@ div.kanban-task-inner
a(href="", title="Change assignation") Username
a.icon.icon-edit(href="", title="Edit", ng-click="ctrl.editTask(task)")
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-header
div.kanban-table-inner
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")
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
h2.task-colum_name(ng-repeat="s in usStatusList track by s.id")
span(tg-bo-html="s.name")
a.icon.icon-plus(href="", title="Add New task")
div.kanban-table-body
div.kanban-table-inner
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
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
div.taskboard_task-playground.task-column(ng-repeat="s in usStatusList track by s.id",
tg-kanban-sortable)
div.kanban-task(ng-repeat="us in usByStatus[s.id] track by us.id")
include ../components/kanban-task

View File

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