Initial kanban code.
parent
b172b07ed9
commit
a8f9bd0303
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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", [])
|
|
@ -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])
|
|
@ -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
|
||||
])
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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") --
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue