diff --git a/app/coffee/app.coffee b/app/coffee/app.coffee
index 7a7fad73..e3cc9698 100644
--- a/app/coffee/app.coffee
+++ b/app/coffee/app.coffee
@@ -182,6 +182,7 @@ modules = [
"taigaAuth",
# Specific Modules
+ "taigaRelatedTasks",
"taigaBacklog",
"taigaTaskboard",
"taigaKanban"
diff --git a/app/coffee/modules/common/popovers.coffee b/app/coffee/modules/common/popovers.coffee
index 55b85586..4abf6c95 100644
--- a/app/coffee/modules/common/popovers.coffee
+++ b/app/coffee/modules/common/popovers.coffee
@@ -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("""
+
""")
+
+ 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 = @
diff --git a/app/coffee/modules/related-tasks.coffee b/app/coffee/modules/related-tasks.coffee
new file mode 100644
index 00000000..38d5e57e
--- /dev/null
+++ b/app/coffee/modules/related-tasks.coffee
@@ -0,0 +1,342 @@
+###
+# 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/related-tasks.coffee
+###
+
+taiga = @.taiga
+trim = @.taiga.trim
+
+module = angular.module("taigaRelatedTasks", [])
+
+RelatedTaskRowDirective = ($repo, $compile, $confirm, $rootscope) ->
+ templateView = _.template("""
+
+
+
+
+
+ <% if(perms.modify_task) { %>
+
+ <% } %>
+
+
+ """)
+
+ templateEdit = _.template("""
+
+
+
+ """)
+
+ 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("""
+
+
+
+ """)
+
+ 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("""
+
+ """)
+
+ 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("""
+
+ <%- name %>
+ """)
+
+ 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])
diff --git a/app/coffee/modules/userstories/detail.coffee b/app/coffee/modules/userstories/detail.coffee
index a4f6c344..f2e1b618 100644
--- a/app/coffee/modules/userstories/detail.coffee
+++ b/app/coffee/modules/userstories/detail.coffee
@@ -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()
diff --git a/app/partials/us-detail.jade b/app/partials/us-detail.jade
index 812975a5..628fc529 100644
--- a/app/partials/us-detail.jade
+++ b/app/partials/us-detail.jade
@@ -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)
diff --git a/app/partials/views/modules/related-tasks.jade b/app/partials/views/modules/related-tasks.jade
index 5dbf3af9..29ee64fb 100644
--- a/app/partials/views/modules/related-tasks.jade
+++ b/app/partials/views/modules/related-tasks.jade
@@ -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)
diff --git a/app/styles/modules/common/related-tasks.scss b/app/styles/modules/common/related-tasks.scss
index da3750ac..42d679fe 100644
--- a/app/styles/modules/common/related-tasks.scss
+++ b/app/styles/modules/common/related-tasks.scss
@@ -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;
}
}