Add powerful related tasks management on user story detail

stable
Jesús Espino 2014-09-09 18:00:26 +02:00 committed by Andrey Antukh
parent d6455c9787
commit e3890c6d5d
7 changed files with 611 additions and 35 deletions

View File

@ -182,6 +182,7 @@ modules = [
"taigaAuth",
# Specific Modules
"taigaRelatedTasks",
"taigaBacklog",
"taigaTaskboard",
"taigaKanban"

View File

@ -101,6 +101,83 @@ UsStatusDirective = ($repo, popoverService) ->
module.directive("tgUsStatus", ["$tgRepo", UsStatusDirective])
RelatedTaskStatusDirective = ($repo, popoverService) ->
###
Print the status of a related task and a popover to change it.
- tg-related-task-status: The related task
- on-update: Method call after US is updated
Example:
div.status(tg-related-task-status="task" on-update="ctrl.loadSprintState()")
a.task-status(href="", title="Status Name")
NOTE: This directive need 'taskStatusById' and 'project'.
###
selectionTemplate = _.template("""
<ul class="popover pop-status">
<% _.forEach(statuses, function(status) { %>
<li>
<a href="" class="status" title="<%- status.name %>" data-status-id="<%- status.id %>">
<%- status.name %>
</a>
</li>
<% }); %>
</ul>""")
updateTaskStatus = ($el, task, taskStatusById) ->
taskStatusDomParent = $el.find(".us-status")
taskStatusDom = $el.find(".task-status .task-status-bind")
if taskStatusById[task.status]
taskStatusDom.text(taskStatusById[task.status].name)
taskStatusDomParent.css('color', taskStatusById[task.status].color)
link = ($scope, $el, $attrs) ->
$ctrl = $el.controller()
task = $scope.$eval($attrs.tgRelatedTaskStatus)
notAutoSave = $scope.$eval($attrs.notAutoSave)
autoSave = !notAutoSave
$el.on "click", ".task-status", (event) ->
event.preventDefault()
event.stopPropagation()
$el.find(".pop-status").popover().open()
# pop = $el.find(".pop-status")
# popoverService.open(pop)
$el.on "click", ".status", (event) ->
event.preventDefault()
event.stopPropagation()
target = angular.element(event.currentTarget)
task.status = target.data("status-id")
$el.find(".pop-status").popover().close()
updateTaskStatus($el, task, $scope.taskStatusById)
if autoSave
$scope.$apply () ->
$repo.save(task).then ->
$scope.$eval($attrs.onUpdate)
$scope.$emit("related-tasks:status-changed")
taiga.bindOnce $scope, "project", (project) ->
$el.append(selectionTemplate({ 'statuses': project.task_statuses }))
updateTaskStatus($el, task, $scope.taskStatusById)
# If the user has not enough permissions the click events are unbinded
if project.my_permissions.indexOf("modify_task") == -1
$el.unbind("click")
$el.find("a").addClass("not-clickable")
$scope.$on "$destroy", ->
$el.off()
return {link: link}
module.directive("tgRelatedTaskStatus", ["$tgRepo", RelatedTaskStatusDirective])
$.fn.popover = () ->
$el = @

View File

@ -0,0 +1,342 @@
###
# 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/related-tasks.coffee
###
taiga = @.taiga
trim = @.taiga.trim
module = angular.module("taigaRelatedTasks", [])
RelatedTaskRowDirective = ($repo, $compile, $confirm, $rootscope) ->
templateView = _.template("""
<div class="tasks">
<div class="task-name">
<span class="icon icon-iocaine"></span>
<a tg-nav="project-tasks-detail:project=project.slug,ref=task.ref" title="<%- task.ref %> <%- task.subject %>" class="clickable">
<span>#<%- task.ref %></span>
<span><%- task.subject %></span>
</a>
<div class="task-settings">
<% if(perms.modify_task) { %>
<a href="" title="Edit" class="icon icon-edit"></a>
<% } %>
<% if(perms.delete_task) { %>
<a href="" title="Delete" class="icon icon-delete delete-task"></a>
<% } %>
</div>
</div>
</div>
<div tg-related-task-status="task" ng-model="task" class="status">
<a href="" title="Status Name" class="task-status">
<span class="task-status-bind"></span>
<% if(perms.modify_task) { %>
<span class="icon icon-arrow-bottom"></span>
<% } %>
</a>
</div>
<div tg-related-task-assigned-to-inline-edition="task" class="assigned-to">
<div title="Assigned to" class="task-assignedto">
<figure class="avatar"></figure>
<% if(perms.modify_task) { %>
<span class="icon icon-arrow-bottom"></span>
<% } %>
</div>
</div>
""")
templateEdit = _.template("""
<div class="tasks">
<div class="task-name">
<input type="text" value="<%- task.subject %>" placeholder="Type the task subject" />
<div class="task-settings">
<a href="" title="Save" class="icon icon-floppy"></a>
<a href="" title="Cancel" class="icon icon-delete cancel-edit"></a>
</div>
</div>
</div>
<div tg-related-task-status="task" ng-model="task" class="status">
<a href="" title="Status Name" class="task-status">
<span class="task-status-bind"></span>
<span class="icon icon-arrow-bottom"></span>
</a>
</div>
<div tg-related-task-assigned-to-inline-edition="task" class="assigned-to">
<div title="Assigned to" class="task-assignedto">
<figure class="avatar"></figure>
<span class="icon icon-arrow-bottom"></span>
</div>
</div>
""")
link = ($scope, $el, $attrs, $model) ->
saveTask = (task) ->
task.subject = $el.find('input').val()
promise = $repo.save(task)
promise.then =>
$confirm.notify("success")
$rootscope.$broadcast("related-tasks:update")
promise.then null, =>
$confirm.notify("error")
renderEdit = (task) ->
$el.html($compile(templateEdit({task: task}))($scope))
$el.on "keyup", "input", (event) ->
if event.keyCode == 13
saveTask($model.$modelValue)
renderView($model.$modelValue)
else if event.keyCode == 27
renderView($model.$modelValue)
$el.on "click", ".icon-floppy", (event) ->
saveTask($model.$modelValue)
renderView($model.$modelValue)
$el.on "click", ".cancel-edit", (event) ->
renderView($model.$modelValue)
renderView = (task) ->
$el.off()
perms = {
modify_task: $scope.project.my_permissions.indexOf("modify_task") != -1
delete_task: $scope.project.my_permissions.indexOf("delete_task") != -1
}
$el.html($compile(templateView({task: task, perms: perms}))($scope))
$el.on "click", ".icon-edit", ->
renderEdit($model.$modelValue)
$el.find('input').focus().select()
$el.on "click", ".delete-task", (event) ->
#TODO: i18n
task = $model.$modelValue
title = "Delete Task"
subtitle = task.subject
$confirm.ask(title, subtitle).then ->
$repo.remove(task).then ->
$confirm.notify("success")
$scope.$emit("related-tasks:delete")
$scope.$watch $attrs.ngModel, (val) ->
return if not val
renderView(val)
$scope.$on "related-tasks:assigned-to-changed", ->
$rootscope.$broadcast("related-tasks:update")
$scope.$on "related-tasks:status-changed", ->
$rootscope.$broadcast("related-tasks:update")
$scope.$on "$destroy", ->
$el.off()
return {link:link, require:"ngModel"}
module.directive("tgRelatedTaskRow", ["$tgRepo", "$compile", "$tgConfirm", "$rootScope", RelatedTaskRowDirective])
RelatedTaskCreateFormDirective = ($repo, $compile, $confirm, $tgmodel) ->
template = _.template("""
<div class="tasks">
<div class="task-name">
<input type="text" placeholder="Type the new task subject" />
<div class="task-settings">
<a href="" title="Save" class="icon icon-floppy"></a>
<a href="" title="Cancel" class="icon icon-delete cancel-edit"></a>
</div>
</div>
</div>
<div tg-related-task-status="newTask" ng-model="newTask" class="status" not-auto-save="true">
<a href="" title="Status Name" class="task-status">
<span class="task-status-bind"></span>
<span class="icon icon-arrow-bottom"></span>
</a>
</div>
<div tg-related-task-assigned-to-inline-edition="newTask" class="assigned-to" not-auto-save="true">
<div title="Assigned to" class="task-assignedto">
<figure class="avatar"></figure>
<span class="icon icon-arrow-bottom"></span>
</div>
</div>
""")
newTask = {
subject: ""
assigned_to: null
}
link = ($scope, $el, $attrs) ->
createTask = (task) ->
task.subject = $el.find('input').val()
task.assigned_to = $scope.newTask.assigned_to
task.status = $scope.newTask.status
$scope.newTask.status = $scope.project.default_task_status
$scope.newTask.assigned_to = null
promise = $repo.create("tasks", task)
promise.then ->
$scope.$emit("related-tasks:add")
$confirm.notify("success")
promise.then null, ->
$confirm.notify("error")
return promise
render = ->
$el.off()
$el.html($compile(template())($scope))
$el.find('input').focus().select()
$el.on "keyup", "input", (event)->
if event.keyCode == 13
createTask(newTask).then ->
render()
else if event.keyCode == 27
$el.html("")
$el.on "click", ".icon-delete", (event)->
$el.html("")
$el.on "click", ".icon-floppy", (event)->
createTask(newTask).then ->
$el.html("")
$scope.$watch "us", (val) ->
return if not val
newTask["status"] = $scope.project.default_task_status
newTask["project"] = $scope.project.id
newTask["user_story"] = $scope.us.id
$scope.newTask = $tgmodel.make_model("tasks", newTask)
$el.html("")
$scope.$on "related-tasks:show-form", ->
render()
$scope.$on "$destroy", ->
$el.off()
return {link: link}
module.directive("tgRelatedTaskCreateForm", ["$tgRepo", "$compile", "$tgConfirm", "$tgModel", RelatedTaskCreateFormDirective])
RelatedTaskCreateButtonDirective = ($repo, $compile, $confirm, $tgmodel) ->
template = _.template("""
<div class="related-tasks-buttons">
<a class="button button-green">+ Add new task</a>
</div>
""")
link = ($scope, $el, $attrs) ->
$scope.$watch "project", (val) ->
return if not val
$el.off()
if $scope.project.my_permissions.indexOf("add_task") != -1
$el.html(template())
else
$el.html("")
$el.on "click", ".button", (event)->
$scope.$emit("related-tasks:add-new-clicked")
$scope.$on "$destroy", ->
$el.off()
return {link: link}
module.directive("tgRelatedTaskCreateButton", ["$tgRepo", "$compile", "$tgConfirm", "$tgModel", RelatedTaskCreateButtonDirective])
RelatedTasksDirective = ($repo, $rs, $rootscope) ->
link = ($scope, $el, $attrs) ->
loadTasks = ->
return $rs.tasks.list($scope.projectId, null, $scope.usId).then (tasks) =>
$scope.tasks = tasks
return tasks
$scope.$on "related-tasks:add", ->
loadTasks().then ->
$rootscope.$broadcast("related-tasks:update")
$scope.$on "related-tasks:delete", ->
loadTasks().then ->
$rootscope.$broadcast("related-tasks:update")
$scope.$on "related-tasks:add-new-clicked", ->
$scope.$broadcast("related-tasks:show-form")
$scope.$watch "us", (val) ->
return if not val
loadTasks()
$scope.$on "$destroy", ->
$el.off()
return {link: link}
module.directive("tgRelatedTasks", ["$tgRepo", "$tgResources", "$rootScope", RelatedTasksDirective])
RelatedTaskAssignedToInlineEditionDirective = ($repo, $rootscope, popoverService) ->
template = _.template("""
<img src="<%= imgurl %>" alt="<%- name %>"/>
<figcaption><%- name %></figcaption>
""")
link = ($scope, $el, $attrs) ->
updateRelatedTask = (task) ->
ctx = {name: "Unassigned", imgurl: "/images/unnamed.png"}
member = $scope.usersById[task.assigned_to]
if member
ctx.imgurl = member.photo
ctx.name = member.full_name_display
$el.find(".avatar").html(template(ctx))
$el.find(".task-assignedto").attr('title', ctx.name)
$ctrl = $el.controller()
task = $scope.$eval($attrs.tgRelatedTaskAssignedToInlineEdition)
notAutoSave = $scope.$eval($attrs.notAutoSave)
autoSave = !notAutoSave
updateRelatedTask(task)
$el.on "click", ".task-assignedto", (event) ->
$rootscope.$broadcast("assigned-to:add", task)
taiga.bindOnce $scope, "project", (project) ->
# If the user has not enough permissions the click events are unbinded
if project.my_permissions.indexOf("modify_task") == -1
$el.unbind("click")
$el.find("a").addClass("not-clickable")
$scope.$on "assigned-to:added", (ctx, userId, updatedRelatedTask) =>
if updatedRelatedTask.id == task.id
updatedRelatedTask.assigned_to = userId
if autoSave
$repo.save(updatedRelatedTask).then ->
$scope.$emit("related-tasks:assigned-to-changed")
updateRelatedTask(updatedRelatedTask)
$scope.$on "$destroy", ->
$el.off()
return {link: link}
module.directive("tgRelatedTaskAssignedToInlineEdition", ["$tgRepo", "$rootScope", RelatedTaskAssignedToInlineEditionDirective])

View File

@ -101,11 +101,6 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin,
return us
loadTasks: ->
return @rs.tasks.list(@scope.projectId, null, @scope.usId).then (tasks) =>
@scope.tasks = tasks
return tasks
loadInitialData: ->
params = {
pslug: @params.pslug
@ -120,8 +115,8 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin,
return promise.then(=> @.loadProject())
.then(=> @.loadUsersAndRoles())
.then(=> @q.all([@.loadUs(),
@.loadTasks(),
@.loadAttachments(@scope.usId)]))
block: ->
@rootscope.$broadcast("block", @scope.us)
@ -311,6 +306,11 @@ UsStatusDetailDirective = () ->
if us?
renderUsstatus(us)
$scope.$on "related-tasks:update", ->
us = $scope.$eval $attrs.ngModel
if us?
renderUsstatus(us)
if editable
$el.on "click", ".status-data", (event) ->
event.preventDefault()

View File

@ -49,3 +49,5 @@ block content
ng-class="{'active': us.client_requirement}") Client requirement
span.button.button-gray(href="", title="Team requirement",
ng-class="{'active': us.team_requirement}") Team requirement
div.lightbox.lightbox-select-user.hidden(tg-lb-assignedto)

View File

@ -1,7 +1,11 @@
section.related-tasks(ng-show="tasks")
h2 Related Tasks
ul.task-list
li.single-related-task(ng-repeat="task in tasks", ng-class="{closed: task.is_closed, blocked: task.is_blocked, iocaine: task.is_iocaine}")
span.icon.icon-iocaine(ng-show="task.is_iocaine")
a(href="", tg-bo-title="task.subject", tg-bo-bind="task.subject" tg-nav="project-tasks-detail:project=project.slug,ref=task.ref")
span.blocked-text(ng-show="task.is_blocked") (Blocked)
section.related-tasks(tg-related-tasks)
h2 Related tasks
div(tg-related-task-create-button)
div.related-tasks-header
.row.related-tasks-title
.tasks Task Name
.status Status
.assigned-to Assigned to
div.related-tasks-body
div.row.single-related-task(ng-repeat="task in tasks", ng-class="{closed: task.is_closed, blocked: task.is_blocked, iocaine: task.is_iocaine}", tg-related-task-row, ng-model="task")
div.row.single-related-task(tg-related-task-create-form)

View File

@ -1,38 +1,188 @@
.related-tasks {
ul {
list-style: disc inside;
margin-bottom: 2rem;
position: relative;
}
.related-tasks-header,
.related-tasks-body {
width: 100%;
.row {
@extend %small;
@include table-flex(center, center, flex, row, wrap, center);
border-bottom: 1px solid $gray-light;
padding: .5rem 0 .5rem .5rem;
text-align: left;
width: 100%;
}
li {
margin-bottom: .5rem;
&.iocaine {
list-style: none inside;
.row {
&:hover {
background: transparent;
}
&.blocked {
color: $red;
text-decoration: line-through;
.tasks {
@include table-flex-child(10, 78%, 0);
}
.status {
@include table-flex-child(0, 10%, 0);
}
.assigned-to {
@include table-flex-child(0, 10%, 0);
}
}
.status {
position: relative;
text-align: left;
.popover {
a {
color: $red;
text-align: left;
width: 100%;
}
.point {
text-align: center;
}
}
&.closed {
.icon {
color: $gray-light;
text-decoration: line-through;
a,
.icon-iocaine {
color: $gray-light;
margin-left: .2rem;
}
}
.pop-status {
@include popover(200px, 0, 40%, '', '');
padding-right: 1rem;
&.fix {
bottom: 0;
top: auto;
}
}
}
.related-tasks-header {
.related-tasks-title {
@extend %medium;
@extend %bold;
border-bottom: 2px solid $gray-light;
margin-top: 1rem;
}
}
.related-tasks-body {
.row {
position: relative;
&:hover {
.task-settings {
@include transition (all .2s ease-in);
opacity: 1;
}
}
&:last-child {
border-bottom: 0;
}
}
.task-name {
position: relative;
a {
color: $grayer;
display: inline-block;
max-width: 90%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
input {
margin-right: 1rem;
padding: 3px;
width: 85%;
}
}
.blocked,
.blocked:hover {
background: $red-light;
color: $white;
a {
color: $white !important;
&:hover {
color: $white;
}
}
.icon {
color: $white;
&:hover {
color: $white;
}
}
}
.icon-iocaine {
color: $green-taiga;
margin-right: .3rem;
position: relative;
right: .3rem;
display: none;
}
.blocked-text {
margin-left: .3rem;
.iocaine,
.iocaine:hover {
background: rgba($fresh-taiga, .3);
.icon-iocaine {
@extend %large;
display: inline-block;
margin-right: .5rem;
vertical-align: top;
}
}
.task-settings {
margin: 0 0 0 2rem;
opacity: 0;
position: absolute;
right: 0;
top: .1rem;
width: 10%;
a {
@include transition (all .2s ease-in);
@extend %large;
color: $gray-light;
&:hover {
@include transition (all .2s ease-in);
color: $grayer;
}
}
}
.assigned-to {
position: relative;
text-align: left;
}
.task-assignedto {
cursor: pointer;
position: relative;
&:hover {
.icon {
@include transition(opacity .3s linear);
opacity: 1;
}
}
figcaption {
max-width: 60%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.icon {
@include transition(opacity .3s linear);
opacity: 0;
position: absolute;
right: .5rem;
top: .5rem;
}
}
.avatar {
align-items: center;
display: flex;
img {
flex-basis: 35px;
}
figcaption {
margin-left: .5rem;
}
}
}
.related-tasks-buttons {
position: absolute;
right: 0;
top: 0;
.button {
cursor: pointer;
}
}