Merge pull request #164 from taigaio/us/982/taskboard-refactor

Us/982/taskboard refactor
stable
David Barragán Merino 2014-12-03 12:41:55 +01:00
commit a8caf6dae0
7 changed files with 279 additions and 76 deletions

View File

@ -27,6 +27,8 @@ generateHash = taiga.generateHash
resourceProvider = ($repo, $http, $urls, $storage) -> resourceProvider = ($repo, $http, $urls, $storage) ->
service = {} service = {}
hashSuffix = "tasks-queryparams" hashSuffix = "tasks-queryparams"
hashSuffixStatusColumnModes = "tasks-statuscolumnmodels"
hashSuffixUsRowModes = "tasks-usrowmodels"
service.get = (projectId, taskId) -> service.get = (projectId, taskId) ->
params = service.getQueryParams(projectId) params = service.getQueryParams(projectId)
@ -65,6 +67,28 @@ resourceProvider = ($repo, $http, $urls, $storage) ->
hash = generateHash([projectId, ns]) hash = generateHash([projectId, ns])
return $storage.get(hash) or {} 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) -> return (instance) ->
instance.tasks = service instance.tasks = service

View File

@ -103,7 +103,6 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin)
loadProject: -> loadProject: ->
return @rs.projects.get(@scope.projectId).then (project) => return @rs.projects.get(@scope.projectId).then (project) =>
@scope.project = project @scope.project = project
@scope.$emit('project:loaded', project)
# Not used at this momment # Not used at this momment
@scope.pointsList = _.sortBy(project.points, "order") @scope.pointsList = _.sortBy(project.points, "order")
# @scope.roleList = _.sortBy(project.roles, "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.taskStatusList = _.sortBy(project.task_statuses, "order")
@scope.usStatusList = _.sortBy(project.us_statuses, "order") @scope.usStatusList = _.sortBy(project.us_statuses, "order")
@scope.usStatusById = groupBy(project.us_statuses, (e) -> e.id) @scope.usStatusById = groupBy(project.us_statuses, (e) -> e.id)
@scope.$emit('project:loaded', project)
return project return project
loadSprintStats: -> loadSprintStats: ->
@ -218,6 +220,8 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin)
promise = @repo.save(task) promise = @repo.save(task)
@rootscope.$broadcast("sprint:task:moved", task)
promise.then => promise.then =>
@.refreshTasksOrder(tasks) @.refreshTasksOrder(tasks)
@.loadSprintStats() @.loadSprintStats()
@ -291,22 +295,6 @@ TaskboardTaskDirective = ($rootscope) ->
module.directive("tgTaskboardTask", ["$rootScope", TaskboardTaskDirective]) 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 ## Taskboard Table Height Fixer Directive
############################################################################# #############################################################################
@ -331,64 +319,156 @@ TaskboardTableHeightFixerDirective = ->
module.directive("tgTaskboardTableHeightFixer", 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 ## Taskboard User Directive
############################################################################# #############################################################################
TaskboardUserDirective = ($log) -> TaskboardUserDirective = ($log) ->
template = _.template(""" template = """
<figure class="avatar"> <figure class="avatar avatar-assigned-to">
<a href="#" title="Assign task" <% if (!clickable) {%>class="not-clickable"<% } %>> <a href="#" title="Assign task" ng-class="{'not-clickable': !clickable}">
<img src="<%- imgurl %>" alt="<%- name %>"> <img ng-src="{{imgurl}}">
</a> </a>
</figure> </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 clickable = false
link = ($scope, $el, $attrs, $model) -> link = ($scope, $el, $attrs) ->
if not $attrs.tgTaskboardUserAvatar?
return $log.error "TaskboardUserDirective: no attr is defined"
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)
render = (user) ->
if user is undefined
ctx = {name: "Unassigned", imgurl: "/images/unnamed.png", clickable: clickable}
else
ctx = {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 = $el.parent().find("a.task-assigned")
username_label.html(ctx.name)
username_label.on "click", (event) -> username_label.on "click", (event) ->
if $el.find('a').hasClass('noclick') if $el.find('a').hasClass('noclick')
return return
us = $model.$modelValue
$ctrl = $el.controller() $ctrl = $el.controller()
$ctrl.editTaskAssignedTo(us) $ctrl.editTaskAssignedTo($scope.task)
$scope.$watch 'task.assigned_to', (assigned_to) ->
user = $scope.usersById[assigned_to]
if user is undefined
_.assign($scope, {name: "Unassigned", imgurl: "/images/unnamed.png", clickable: clickable})
else
_.assign($scope, {name: user.full_name_display, imgurl: user.photo, clickable: clickable})
username_label.text($scope.name)
bindOnce $scope, "project", (project) -> bindOnce $scope, "project", (project) ->
if project.my_permissions.indexOf("modify_task") > -1 if project.my_permissions.indexOf("modify_task") > -1
clickable = true clickable = true
$el.on "click", (event) => $el.find(".avatar-assigned-to").on "click", (event) =>
if $el.find('a').hasClass('noclick') if $el.find('a').hasClass('noclick')
return return
us = $model.$modelValue
$ctrl = $el.controller() $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]) module.directive("tgTaskboardUserAvatar", ["$log", TaskboardUserDirective])

View File

@ -11,7 +11,7 @@ block content
span(tg-bo-bind="project.name", class="project-name-short") span(tg-bo-bind="project.name", class="project-name-short")
span.green(tg-bo-bind="sprint.name") span.green(tg-bo-bind="sprint.name")
span.date(tg-date-range="sprint.estimated_start,sprint.estimated_finish") 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.graphics-container
div.burndown(tg-sprint-graph) div.burndown(tg-sprint-graph)

View File

@ -1,7 +1,6 @@
div.taskboard-tagline(tg-colorize-tags="task.tags", tg-colorize-tags-type="taskboard") div.taskboard-tagline(tg-colorize-tags="task.tags", tg-colorize-tags-type="taskboard")
div.taskboard-task-inner div.taskboard-task-inner
div.taskboard-user-avatar(tg-taskboard-user-avatar="task.assigned_to", ng-model="task", div.taskboard-user-avatar(tg-taskboard-user-avatar, users="usersById", task="task", project="project", ng-class="{iocaine: task.is_iocaine}")
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!") 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 p.taskboard-text
a.task-assigned(href="", title="Assign task") a.task-assigned(href="", title="Assign task")

View File

@ -1,15 +1,18 @@
div.taskboard-table div.taskboard-table(tg-taskboard-squish-column)
div.taskboard-table-header 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 "User story"
h2.task-colum-name(ng-repeat="s in taskStatusList track by s.id", 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")
ng-style="{'border-top-color':s.color}")
span(tg-bo-bind="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-body(tg-taskboard-table-height-fixer)
div.taskboard-table-inner(tg-taskboard-row-width-fixer) div.taskboard-table-inner
div.task-row(ng-repeat="us in userstories track by us.id", ng-class="{blocked: us.is_blocked}") 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") 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 h3.us-title
a(href="", tg-nav="project-userstories-detail:project=project.slug,ref=us.ref", a(href="", tg-nav="project-userstories-detail:project=project.slug,ref=us.ref",
tg-bo-title="'#' + us.ref + ' ' + us.subject") tg-bo-title="'#' + us.ref + ' ' + us.subject")
@ -18,22 +21,20 @@ div.taskboard-table
p.points-value p.points-value
span(ng-bind="us.total_points") span(ng-bind="us.total_points")
span points span points
include ../components/addnewtask.jade 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-tasks-box.task-column(ng-repeat="st in taskStatusList track by st.id",
tg-taskboard-sortable)
div.taskboard-task(ng-repeat="task in usTasks[us.id][st.id] track by task.id", div.taskboard-task(ng-repeat="task in usTasks[us.id][st.id] track by task.id",
tg-taskboard-task) tg-taskboard-task)
include ../components/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 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 h3.us-title
span Unassigned tasks span Unassigned tasks
include ../components/addnewtask.jade include ../components/addnewtask.jade
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-tasks-box.task-column(ng-repeat="st in taskStatusList track by st.id",
tg-taskboard-sortable)
div.taskboard-task(ng-repeat="task in usTasks[null][st.id] track by task.id", div.taskboard-task(ng-repeat="task in usTasks[null][st.id] track by task.id",
tg-taskboard-task) tg-taskboard-task)
include ../components/taskboard-task include ../components/taskboard-task

View File

@ -1,5 +1,4 @@
.taskboard-task { .taskboard-task {
@include transition (all .4s linear);
background: $postit; background: $postit;
border: 1px solid $postit-hover; border: 1px solid $postit-hover;
box-shadow: none; box-shadow: none;
@ -45,7 +44,6 @@
} }
.taskboard-task-inner { .taskboard-task-inner {
@include table-flex(); @include table-flex();
min-height: 7rem;
padding: .5rem; padding: .5rem;
} }
.taskboard-user-avatar { .taskboard-user-avatar {

View File

@ -1,10 +1,46 @@
//Table basic shared vars //Table basic shared vars
$column-width: 300px; $column-width: 300px;
$column-flex: 1; $column-flex: 0;
$column-shrink: 0; $column-shrink: 0;
$column-margin: 0 10px 0 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 { .taskboard-table {
overflow: hidden; overflow: hidden;
@ -21,18 +57,49 @@ $column-margin: 0 10px 0 0;
position: absolute; position: absolute;
} }
.task-colum-name { .task-colum-name {
@extend %large;
@include table-flex-child($column-flex, $column-width, $column-shrink, $column-width); @include table-flex-child($column-flex, $column-width, $column-shrink, $column-width);
@include table-flex();
@include justify-content(space-between);
@extend %large;
background: $whitish; background: $whitish;
border-top: 3px solid $gray-light; border-top: 3px solid $gray-light;
margin: $column-margin; margin: $column-margin;
padding: .5rem 0; max-width: $column-width;
padding: .5rem 1rem;
position: relative; position: relative;
text-align: center;
text-transform: uppercase; text-transform: uppercase;
width: $column-width;
&:last-child { &:last-child {
margin-right: 0; 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 { .task-column {
@include table-flex-child($column-flex, $column-width, $column-shrink, $column-width); @include table-flex-child($column-flex, $column-width, $column-shrink, $column-width);
margin: $column-margin; margin: $column-margin;
max-width: $column-width;
width: $column-width;
&:last-child { &:last-child {
margin-right: 0; margin-right: 0;
} }
} }
.row-fold {
@extend %fold;
}
.column-fold {
@extend %fold;
.taskboard-task {
max-width: 40px;
width: 40px;
}
}
.task-row { .task-row {
@include table-flex(); @include table-flex();
margin-bottom: .5rem; margin-bottom: .5rem;
@ -74,32 +153,54 @@ $column-margin: 0 10px 0 0;
} }
} }
.taskboard-tasks-box { .taskboard-tasks-box {
//@include filter(saturate(20%));
background: rgba($red, .1); 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 { .taskboard-tasks-box {
background: $whitish; background: $whitish;
//background: $very-light-gray;
} }
.taskboard-userstory-box { .taskboard-userstory-box {
padding: .5rem; padding: .5rem .5rem .5rem 1.5rem;
.icon { .icon {
@include transition(color .2s linear); @include transition(color .2s linear);
color: $gray-light; color: $gray-light;
position: absolute; position: absolute;
right: .5rem; right: .5rem;
top: 1rem; top: .7rem;
&:hover { &:hover {
color: $green-taiga; color: $green-taiga;
} }
&.icon-plus { &.icon-plus {
right: 2rem; right: 2rem;
} }
&.icon-vfold,
&.icon-vunfold {
left: 0;
right: inherit;
} }
} }
}
.avatar-task-link {
display: none;
}
.avatar-assigned-to {
display: block;
}
} }
.taskboard-userstory-box { .taskboard-userstory-box {