commit
a8caf6dae0
|
@ -27,6 +27,8 @@ generateHash = taiga.generateHash
|
|||
resourceProvider = ($repo, $http, $urls, $storage) ->
|
||||
service = {}
|
||||
hashSuffix = "tasks-queryparams"
|
||||
hashSuffixStatusColumnModes = "tasks-statuscolumnmodels"
|
||||
hashSuffixUsRowModes = "tasks-usrowmodels"
|
||||
|
||||
service.get = (projectId, taskId) ->
|
||||
params = service.getQueryParams(projectId)
|
||||
|
@ -65,6 +67,28 @@ resourceProvider = ($repo, $http, $urls, $storage) ->
|
|||
hash = generateHash([projectId, ns])
|
||||
return $storage.get(hash) or {}
|
||||
|
||||
service.storeStatusColumnModes = (projectId, params) ->
|
||||
ns = "#{projectId}:#{hashSuffixStatusColumnModes}"
|
||||
hash = generateHash([projectId, ns])
|
||||
$storage.set(hash, params)
|
||||
|
||||
service.getStatusColumnModes = (projectId) ->
|
||||
ns = "#{projectId}:#{hashSuffixStatusColumnModes}"
|
||||
hash = generateHash([projectId, ns])
|
||||
return $storage.get(hash) or {}
|
||||
|
||||
service.storeUsRowModes = (projectId, sprintId, params) ->
|
||||
ns = "#{projectId}:#{hashSuffixUsRowModes}"
|
||||
hash = generateHash([projectId, sprintId, ns])
|
||||
|
||||
$storage.set(hash, params)
|
||||
|
||||
service.getUsRowModes = (projectId, sprintId) ->
|
||||
ns = "#{projectId}:#{hashSuffixUsRowModes}"
|
||||
hash = generateHash([projectId, sprintId, ns])
|
||||
|
||||
return $storage.get(hash) or {}
|
||||
|
||||
return (instance) ->
|
||||
instance.tasks = service
|
||||
|
||||
|
|
|
@ -103,7 +103,6 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin)
|
|||
loadProject: ->
|
||||
return @rs.projects.get(@scope.projectId).then (project) =>
|
||||
@scope.project = project
|
||||
@scope.$emit('project:loaded', project)
|
||||
# Not used at this momment
|
||||
@scope.pointsList = _.sortBy(project.points, "order")
|
||||
# @scope.roleList = _.sortBy(project.roles, "order")
|
||||
|
@ -112,6 +111,9 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin)
|
|||
@scope.taskStatusList = _.sortBy(project.task_statuses, "order")
|
||||
@scope.usStatusList = _.sortBy(project.us_statuses, "order")
|
||||
@scope.usStatusById = groupBy(project.us_statuses, (e) -> e.id)
|
||||
|
||||
@scope.$emit('project:loaded', project)
|
||||
|
||||
return project
|
||||
|
||||
loadSprintStats: ->
|
||||
|
@ -218,6 +220,8 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin)
|
|||
|
||||
promise = @repo.save(task)
|
||||
|
||||
@rootscope.$broadcast("sprint:task:moved", task)
|
||||
|
||||
promise.then =>
|
||||
@.refreshTasksOrder(tasks)
|
||||
@.loadSprintStats()
|
||||
|
@ -291,22 +295,6 @@ TaskboardTaskDirective = ($rootscope) ->
|
|||
|
||||
module.directive("tgTaskboardTask", ["$rootScope", TaskboardTaskDirective])
|
||||
|
||||
|
||||
#############################################################################
|
||||
## Taskboard Task Row Size Fixer Directive
|
||||
#############################################################################
|
||||
|
||||
TaskboardRowWidthFixerDirective = ->
|
||||
link = ($scope, $el, $attrs) ->
|
||||
bindOnce $scope, "taskStatusList", (statuses) ->
|
||||
itemSize = 300 + (10 * statuses.length)
|
||||
size = (1 + statuses.length) * itemSize
|
||||
$el.css("width", "#{size}px")
|
||||
|
||||
return {link: link}
|
||||
|
||||
module.directive("tgTaskboardRowWidthFixer", TaskboardRowWidthFixerDirective)
|
||||
|
||||
#############################################################################
|
||||
## Taskboard Table Height Fixer Directive
|
||||
#############################################################################
|
||||
|
@ -331,64 +319,156 @@ TaskboardTableHeightFixerDirective = ->
|
|||
|
||||
module.directive("tgTaskboardTableHeightFixer", TaskboardTableHeightFixerDirective)
|
||||
|
||||
#############################################################################
|
||||
## Taskboard Squish Column Directive
|
||||
#############################################################################
|
||||
|
||||
TaskboardSquishColumnDirective = (rs) ->
|
||||
avatarWidth = 40
|
||||
|
||||
link = ($scope, $el, $attrs) ->
|
||||
$scope.$on "sprint:task:moved", () =>
|
||||
recalculateTaskboardWidth()
|
||||
|
||||
bindOnce $scope, "usTasks", (project) ->
|
||||
$scope.statusesFolded = rs.tasks.getStatusColumnModes($scope.project.id)
|
||||
$scope.usFolded = rs.tasks.getUsRowModes($scope.project.id, $scope.sprintId)
|
||||
|
||||
recalculateTaskboardWidth()
|
||||
|
||||
$scope.foldStatus = (status) ->
|
||||
$scope.statusesFolded[status.id] = !!!$scope.statusesFolded[status.id]
|
||||
rs.tasks.storeStatusColumnModes($scope.projectId, $scope.statusesFolded)
|
||||
|
||||
recalculateTaskboardWidth()
|
||||
|
||||
$scope.foldUs = (us) ->
|
||||
if !us
|
||||
$scope.usFolded["unassigned"] = !!!$scope.usFolded["unassigned"]
|
||||
else
|
||||
$scope.usFolded[us.id] = !!!$scope.usFolded[us.id]
|
||||
|
||||
rs.tasks.storeUsRowModes($scope.projectId, $scope.sprintId, $scope.usFolded)
|
||||
|
||||
recalculateTaskboardWidth()
|
||||
|
||||
getCeilWidth = (usId, statusId) =>
|
||||
tasks = $scope.usTasks[usId][statusId].length
|
||||
|
||||
if $scope.statusesFolded[statusId]
|
||||
if tasks and $scope.usFolded[usId]
|
||||
tasksMatrixSize = Math.round(Math.sqrt(tasks))
|
||||
width = avatarWidth * tasksMatrixSize
|
||||
else
|
||||
width = avatarWidth
|
||||
|
||||
return width
|
||||
|
||||
return 0
|
||||
|
||||
setStatusColumnWidth = (statusId, width) =>
|
||||
column = $el.find(".squish-status-#{statusId}")
|
||||
|
||||
if width
|
||||
column.css('max-width', width)
|
||||
else
|
||||
column.removeAttr("style")
|
||||
|
||||
refreshTaskboardTableWidth = () =>
|
||||
columnWidths = []
|
||||
|
||||
columns = $el.find(".task-colum-name")
|
||||
|
||||
columnWidths = _.map columns, (column) ->
|
||||
return $(column).outerWidth(true)
|
||||
|
||||
totalWidth = _.reduce columnWidths, (total, width) ->
|
||||
return total + width
|
||||
|
||||
$el.find('.taskboard-table-inner').css("width", totalWidth)
|
||||
|
||||
recalculateStatusColumnWidth = (statusId) =>
|
||||
statusFoldedWidth = 0
|
||||
|
||||
_.forEach $scope.userstories, (us) ->
|
||||
width = getCeilWidth(us.id, statusId)
|
||||
|
||||
statusFoldedWidth = width if width > statusFoldedWidth
|
||||
|
||||
setStatusColumnWidth(statusId, statusFoldedWidth)
|
||||
|
||||
recalculateTaskboardWidth = () =>
|
||||
_.forEach $scope.taskStatusList, (status) ->
|
||||
recalculateStatusColumnWidth(status.id)
|
||||
|
||||
refreshTaskboardTableWidth()
|
||||
|
||||
return
|
||||
|
||||
return {link: link}
|
||||
|
||||
module.directive("tgTaskboardSquishColumn", ["$tgResources", TaskboardSquishColumnDirective])
|
||||
|
||||
#############################################################################
|
||||
## Taskboard User Directive
|
||||
#############################################################################
|
||||
|
||||
TaskboardUserDirective = ($log) ->
|
||||
template = _.template("""
|
||||
<figure class="avatar">
|
||||
<a href="#" title="Assign task" <% if (!clickable) {%>class="not-clickable"<% } %>>
|
||||
<img src="<%- imgurl %>" alt="<%- name %>">
|
||||
template = """
|
||||
<figure class="avatar avatar-assigned-to">
|
||||
<a href="#" title="Assign task" ng-class="{'not-clickable': !clickable}">
|
||||
<img ng-src="{{imgurl}}">
|
||||
</a>
|
||||
</figure>
|
||||
""") # TODO: i18n
|
||||
|
||||
<figure class="avatar avatar-task-link">
|
||||
<a tg-nav="project-tasks-detail:project=project.slug,ref=task.ref" ng-attr-title="{{task.subject}}">
|
||||
<img ng-src="{{imgurl}}">
|
||||
</a>
|
||||
</figure>
|
||||
""" # TODO: i18n
|
||||
|
||||
clickable = false
|
||||
|
||||
link = ($scope, $el, $attrs, $model) ->
|
||||
if not $attrs.tgTaskboardUserAvatar?
|
||||
return $log.error "TaskboardUserDirective: no attr is defined"
|
||||
link = ($scope, $el, $attrs) ->
|
||||
username_label = $el.parent().find("a.task-assigned")
|
||||
username_label.on "click", (event) ->
|
||||
if $el.find('a').hasClass('noclick')
|
||||
return
|
||||
|
||||
wtid = $scope.$watch $attrs.tgTaskboardUserAvatar, (v) ->
|
||||
if not $scope.usersById?
|
||||
$log.error "TaskboardUserDirective requires userById set in scope."
|
||||
wtid()
|
||||
else
|
||||
user = $scope.usersById[v]
|
||||
render(user)
|
||||
$ctrl = $el.controller()
|
||||
$ctrl.editTaskAssignedTo($scope.task)
|
||||
|
||||
$scope.$watch 'task.assigned_to', (assigned_to) ->
|
||||
user = $scope.usersById[assigned_to]
|
||||
|
||||
render = (user) ->
|
||||
if user is undefined
|
||||
ctx = {name: "Unassigned", imgurl: "/images/unnamed.png", clickable: clickable}
|
||||
_.assign($scope, {name: "Unassigned", imgurl: "/images/unnamed.png", clickable: clickable})
|
||||
else
|
||||
ctx = {name: user.full_name_display, imgurl: user.photo, clickable: clickable}
|
||||
_.assign($scope, {name: user.full_name_display, imgurl: user.photo, clickable: clickable})
|
||||
|
||||
html = template(ctx)
|
||||
$el.html(html)
|
||||
username_label = $el.parent().find("a.task-assigned")
|
||||
username_label.html(ctx.name)
|
||||
username_label.on "click", (event) ->
|
||||
if $el.find('a').hasClass('noclick')
|
||||
return
|
||||
username_label.text($scope.name)
|
||||
|
||||
us = $model.$modelValue
|
||||
$ctrl = $el.controller()
|
||||
$ctrl.editTaskAssignedTo(us)
|
||||
|
||||
bindOnce $scope, "project", (project) ->
|
||||
if project.my_permissions.indexOf("modify_task") > -1
|
||||
clickable = true
|
||||
$el.on "click", (event) =>
|
||||
$el.find(".avatar-assigned-to").on "click", (event) =>
|
||||
if $el.find('a').hasClass('noclick')
|
||||
return
|
||||
|
||||
us = $model.$modelValue
|
||||
$ctrl = $el.controller()
|
||||
$ctrl.editTaskAssignedTo(us)
|
||||
$ctrl.editTaskAssignedTo($scope.task)
|
||||
|
||||
return {link: link, require:"ngModel"}
|
||||
return {
|
||||
link: link,
|
||||
template: template,
|
||||
scope: {
|
||||
"usersById": "=users",
|
||||
"project": "=",
|
||||
"task": "=",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.directive("tgTaskboardUserAvatar", ["$log", TaskboardUserDirective])
|
||||
|
|
|
@ -11,7 +11,7 @@ block content
|
|||
span(tg-bo-bind="project.name", class="project-name-short")
|
||||
span.green(tg-bo-bind="sprint.name")
|
||||
span.date(tg-date-range="sprint.estimated_start,sprint.estimated_finish")
|
||||
include views/components/sprint-summary
|
||||
//- include views/components/sprint-summary
|
||||
|
||||
div.graphics-container
|
||||
div.burndown(tg-sprint-graph)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
div.taskboard-tagline(tg-colorize-tags="task.tags", tg-colorize-tags-type="taskboard")
|
||||
div.taskboard-task-inner
|
||||
div.taskboard-user-avatar(tg-taskboard-user-avatar="task.assigned_to", ng-model="task",
|
||||
ng-class="{iocaine: task.is_iocaine}")
|
||||
div.taskboard-user-avatar(tg-taskboard-user-avatar, users="usersById", task="task", project="project", ng-class="{iocaine: task.is_iocaine}")
|
||||
span.icon.icon-iocaine(ng-if="task.is_iocaine", title="Feeling a bit overwhelmed by a task? Make sure others know about it by clicking on Iocaine when editing a task. It's possible to become immune to this (fictional) deadly poison by consuming small amounts over time just as it's possible to get better at what you do by occasionally taking on extra challenges!")
|
||||
p.taskboard-text
|
||||
a.task-assigned(href="", title="Assign task")
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
div.taskboard-table
|
||||
div.taskboard-table(tg-taskboard-squish-column)
|
||||
div.taskboard-table-header
|
||||
div.taskboard-table-inner(tg-taskboard-row-width-fixer)
|
||||
div.taskboard-table-inner
|
||||
h2.task-colum-name "User story"
|
||||
h2.task-colum-name(ng-repeat="s in taskStatusList track by s.id",
|
||||
ng-style="{'border-top-color':s.color}")
|
||||
h2.task-colum-name(ng-repeat="s in taskStatusList track by s.id", ng-style="{'border-top-color':s.color}", ng-class="{'column-fold':statusesFolded[s.id]}", class="squish-status-{{s.id}}", tg-bo-title="s.name")
|
||||
span(tg-bo-bind="s.name")
|
||||
a.icon.icon-vfold.hfold(href="", ng-click='foldStatus(s)', title="Fold Column", ng-class='{hidden:statusesFolded[s.id]}')
|
||||
a.icon.icon-vunfold.hunfold(href="", title="Unfold Column", ng-click='foldStatus(s)', ng-class='{hidden:!statusesFolded[s.id]}')
|
||||
|
||||
div.taskboard-table-body(tg-taskboard-table-height-fixer)
|
||||
div.taskboard-table-inner(tg-taskboard-row-width-fixer)
|
||||
div.task-row(ng-repeat="us in userstories track by us.id", ng-class="{blocked: us.is_blocked}")
|
||||
div.taskboard-table-inner
|
||||
div.task-row(ng-repeat="us in userstories track by us.id", ng-class="{blocked: us.is_blocked, 'row-fold':usFolded[us.id]}")
|
||||
div.taskboard-userstory-box.task-column(tg-bo-title="us.blocked_note")
|
||||
a.icon.icon-vfold.vfold(href="", title="Fold Row", ng-click='foldUs(us)', ng-class='{hidden:usFolded[us.id]}')
|
||||
a.icon.icon-vunfold.vunfold(href="", title="Unfold Row", ng-click='foldUs(us)', ng-class='{hidden:!usFolded[us.id]}')
|
||||
h3.us-title
|
||||
a(href="", tg-nav="project-userstories-detail:project=project.slug,ref=us.ref",
|
||||
tg-bo-title="'#' + us.ref + ' ' + us.subject")
|
||||
|
@ -18,22 +21,20 @@ div.taskboard-table
|
|||
p.points-value
|
||||
span(ng-bind="us.total_points")
|
||||
span points
|
||||
include ../components/addnewtask.jade
|
||||
|
||||
div.taskboard-tasks-box.task-column(ng-repeat="st in taskStatusList track by st.id",
|
||||
tg-taskboard-sortable)
|
||||
include ../components/addnewtask
|
||||
div.taskboard-tasks-box.task-column(ng-repeat="st in taskStatusList track by st.id", tg-taskboard-sortable, class="squish-status-{{st.id}}", ng-class="{'column-fold':statusesFolded[st.id]}")
|
||||
div.taskboard-task(ng-repeat="task in usTasks[us.id][st.id] track by task.id",
|
||||
tg-taskboard-task)
|
||||
include ../components/taskboard-task
|
||||
|
||||
div.task-row(ng-init="us = null")
|
||||
div.task-row(ng-init="us = null", ng-class="{'row-fold':usFolded['unassigned']}")
|
||||
div.taskboard-userstory-box.task-column
|
||||
a.icon.icon-vfold.vfold(href="", title="Fold Row", ng-click='foldUs()', ng-class="{hidden:usFolded['unassigned']}")
|
||||
a.icon.icon-vunfold.vunfold(href="", title="Unfold Row", ng-click='foldUs()', ng-class="{hidden:!usFolded['unassigned']}")
|
||||
h3.us-title
|
||||
span Unassigned tasks
|
||||
include ../components/addnewtask.jade
|
||||
|
||||
div.taskboard-tasks-box.task-column(ng-repeat="st in taskStatusList track by st.id",
|
||||
tg-taskboard-sortable)
|
||||
div.taskboard-tasks-box.task-column(ng-repeat="st in taskStatusList track by st.id", tg-taskboard-sortable, class="squish-status-{{st.id}}", ng-class="{'column-fold':statusesFolded[st.id]}")
|
||||
div.taskboard-task(ng-repeat="task in usTasks[null][st.id] track by task.id",
|
||||
tg-taskboard-task)
|
||||
include ../components/taskboard-task
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
.taskboard-task {
|
||||
@include transition (all .4s linear);
|
||||
background: $postit;
|
||||
border: 1px solid $postit-hover;
|
||||
box-shadow: none;
|
||||
|
@ -45,7 +44,6 @@
|
|||
}
|
||||
.taskboard-task-inner {
|
||||
@include table-flex();
|
||||
min-height: 7rem;
|
||||
padding: .5rem;
|
||||
}
|
||||
.taskboard-user-avatar {
|
||||
|
|
|
@ -1,10 +1,46 @@
|
|||
//Table basic shared vars
|
||||
|
||||
$column-width: 300px;
|
||||
$column-flex: 1;
|
||||
$column-flex: 0;
|
||||
$column-shrink: 0;
|
||||
$column-margin: 0 10px 0 0;
|
||||
|
||||
%fold {
|
||||
.taskboard-task {
|
||||
background: none;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
min-height: 0;
|
||||
.taskboard-task-inner {
|
||||
padding: .2rem;
|
||||
}
|
||||
.taskboard-tagline,
|
||||
.taskboard-text {
|
||||
display: none;
|
||||
}
|
||||
.avatar {
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
}
|
||||
.icon {
|
||||
display: none;
|
||||
}
|
||||
&.ui-sortable-helper {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
&.task-column,
|
||||
.task-column {
|
||||
@include table-flex(flex-start);
|
||||
@include flex-direction(row);
|
||||
}
|
||||
.avatar-task-link {
|
||||
display: block;
|
||||
}
|
||||
.avatar-assigned-to {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.taskboard-table {
|
||||
overflow: hidden;
|
||||
|
@ -21,18 +57,49 @@ $column-margin: 0 10px 0 0;
|
|||
position: absolute;
|
||||
}
|
||||
.task-colum-name {
|
||||
@extend %large;
|
||||
@include table-flex-child($column-flex, $column-width, $column-shrink, $column-width);
|
||||
@include table-flex();
|
||||
@include justify-content(space-between);
|
||||
@extend %large;
|
||||
background: $whitish;
|
||||
border-top: 3px solid $gray-light;
|
||||
margin: $column-margin;
|
||||
padding: .5rem 0;
|
||||
max-width: $column-width;
|
||||
padding: .5rem 1rem;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
width: $column-width;
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
.icon {
|
||||
@extend %medium;
|
||||
@include transition(color .2s linear);
|
||||
color: $gray-light;
|
||||
margin-right: .3rem;
|
||||
&:hover {
|
||||
color: $green-taiga;
|
||||
}
|
||||
&.hfold,
|
||||
&.hunfold {
|
||||
@include transform(rotate(90deg));
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
&.column-fold {
|
||||
@include align-items(center);
|
||||
@include justify-content(center);
|
||||
padding: .3rem 0;
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
.icon {
|
||||
&.hfold,
|
||||
&.hunfold {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,10 +111,22 @@ $column-margin: 0 10px 0 0;
|
|||
.task-column {
|
||||
@include table-flex-child($column-flex, $column-width, $column-shrink, $column-width);
|
||||
margin: $column-margin;
|
||||
max-width: $column-width;
|
||||
width: $column-width;
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
.row-fold {
|
||||
@extend %fold;
|
||||
}
|
||||
.column-fold {
|
||||
@extend %fold;
|
||||
.taskboard-task {
|
||||
max-width: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
.task-row {
|
||||
@include table-flex();
|
||||
margin-bottom: .5rem;
|
||||
|
@ -74,32 +153,54 @@ $column-margin: 0 10px 0 0;
|
|||
}
|
||||
}
|
||||
.taskboard-tasks-box {
|
||||
//@include filter(saturate(20%));
|
||||
background: rgba($red, .1);
|
||||
}
|
||||
}
|
||||
&.row-fold {
|
||||
min-height: 0;
|
||||
.taskboard-userstory-box {
|
||||
.us-title {
|
||||
@include ellipsis(100%);
|
||||
}
|
||||
.points-value,
|
||||
.icon-plus,
|
||||
.icon-bulk {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.taskboard-tasks-box {
|
||||
background: $whitish;
|
||||
//background: $very-light-gray;
|
||||
}
|
||||
|
||||
.taskboard-userstory-box {
|
||||
padding: .5rem;
|
||||
padding: .5rem .5rem .5rem 1.5rem;
|
||||
.icon {
|
||||
@include transition(color .2s linear);
|
||||
color: $gray-light;
|
||||
position: absolute;
|
||||
right: .5rem;
|
||||
top: 1rem;
|
||||
top: .7rem;
|
||||
&:hover {
|
||||
color: $green-taiga;
|
||||
}
|
||||
&.icon-plus {
|
||||
right: 2rem;
|
||||
}
|
||||
&.icon-vfold,
|
||||
&.icon-vunfold {
|
||||
left: 0;
|
||||
right: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
.avatar-task-link {
|
||||
display: none;
|
||||
}
|
||||
.avatar-assigned-to {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.taskboard-userstory-box {
|
||||
|
|
Loading…
Reference in New Issue