diff --git a/app/coffee/modules/resources/tasks.coffee b/app/coffee/modules/resources/tasks.coffee index 07cacb18..3e29f612 100644 --- a/app/coffee/modules/resources/tasks.coffee +++ b/app/coffee/modules/resources/tasks.coffee @@ -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 diff --git a/app/coffee/modules/taskboard/main.coffee b/app/coffee/modules/taskboard/main.coffee index e25fe79f..15e5cf7d 100644 --- a/app/coffee/modules/taskboard/main.coffee +++ b/app/coffee/modules/taskboard/main.coffee @@ -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(""" -
- class="not-clickable"<% } %>> - <%- name %> + template = """ +
+ +
- """) # TODO: i18n + + + """ # 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]) diff --git a/app/partials/taskboard.jade b/app/partials/taskboard.jade index 9cdf3e1e..a9a191c0 100644 --- a/app/partials/taskboard.jade +++ b/app/partials/taskboard.jade @@ -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) diff --git a/app/partials/views/components/taskboard-task.jade b/app/partials/views/components/taskboard-task.jade index 9fd01538..29311f7c 100644 --- a/app/partials/views/components/taskboard-task.jade +++ b/app/partials/views/components/taskboard-task.jade @@ -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") diff --git a/app/partials/views/modules/taskboard-table.jade b/app/partials/views/modules/taskboard-table.jade index d346793f..0454497e 100644 --- a/app/partials/views/modules/taskboard-table.jade +++ b/app/partials/views/modules/taskboard-table.jade @@ -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 diff --git a/app/styles/components/taskboard-task.scss b/app/styles/components/taskboard-task.scss index 7b9fe6e0..a306cf3e 100644 --- a/app/styles/components/taskboard-task.scss +++ b/app/styles/components/taskboard-task.scss @@ -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 { diff --git a/app/styles/modules/backlog/taskboard-table.scss b/app/styles/modules/backlog/taskboard-table.scss index 1d028e95..c2a35a58 100644 --- a/app/styles/modules/backlog/taskboard-table.scss +++ b/app/styles/modules/backlog/taskboard-table.scss @@ -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 {