diff --git a/app/locales/taiga/locale-en.json b/app/locales/taiga/locale-en.json index cfd6397d..012bb8a5 100644 --- a/app/locales/taiga/locale-en.json +++ b/app/locales/taiga/locale-en.json @@ -393,6 +393,7 @@ }, "EPICS": { "TITLE": "EPICS", + "EPIC": "EPIC", "DASHBOARD": { "ADD": "+ ADD EPIC", "UNASSIGNED": "Unassigned" diff --git a/app/modules/epics/dashboard/epic-row/epic-row.controller.coffee b/app/modules/epics/dashboard/epic-row/epic-row.controller.coffee index f1719faf..cb1e5989 100644 --- a/app/modules/epics/dashboard/epic-row/epic-row.controller.coffee +++ b/app/modules/epics/dashboard/epic-row/epic-row.controller.coffee @@ -26,6 +26,7 @@ class EpicRowController ] constructor: (@rs, @confirm) -> + @.displayUserStories = false @._calculateProgressBar() _calculateProgressBar: () -> @@ -50,4 +51,21 @@ class EpicRowController return @rs.epics.patch(id, patch).then(onSuccess, onError) + requestUserStories: (epic) -> + if @.displayUserStories == false + id = @.epic.get('id') + + onSuccess = (data) => + @.epicStories = data + console.log @.epicStories.toJS() + @.displayUserStories = true + @confirm.notify('success') + + onError = (data) => + @confirm.notify('error') + + return @rs.userstories.listInEpics(id).then(onSuccess, onError) + else + @.displayUserStories = false + module.controller("EpicRowCtrl", EpicRowController) diff --git a/app/modules/epics/dashboard/epic-row/epic-row.jade b/app/modules/epics/dashboard/epic-row/epic-row.jade index d260d601..1eadbc19 100644 --- a/app/modules/epics/dashboard/epic-row/epic-row.jade +++ b/app/modules/epics/dashboard/epic-row/epic-row.jade @@ -1,40 +1,49 @@ .epic-row( - ng-class="{'is-blocked': vm.epic.get('is_blocked'), 'is-closed': vm.epic.get('is_closed')}" + ng-class="{'is-blocked': vm.epic.get('is_blocked'), 'is-closed': vm.epic.get('is_closed'), 'unfold': vm.displayUserStories}" + ng-click="vm.requestUserStories(vm.epic)" ) - tg-svg( + tg-svg.icon-drag( svg-icon="icon-drag" ) .vote( ng-if="vm.column.votes" ng-class="{'is-voter': vm.epic.get('is_voter')}" - ) + ) tg-svg(svg-icon='icon-upvote') span {{::vm.epic.get('total_voters')}} - - .name(ng-if="vm.column.name") + + .name(ng-if="vm.column.name") + - var hash = "#"; a( tg-nav="project-epic-detail:project=vm.project.get('slug')" ng-attr-title="{{::vm.epic.get('subject')}}" - ) {{::vm.epic.get('subject')}} - - .project(ng-if="vm.column.project") {{::vm.epic.get('project')}} + ) #{hash}{{::vm.epic.get('ref')}} {{::vm.epic.get('subject')}} + span.epic-pill( + ng-style="::{'background-color': vm.epic.get('color')}" + translate="EPICS.EPIC" + ) + tg-svg( + svg-icon="icon-arrow-down" + ng-if="vm.epic.getIn(['user_stories_counts', 'opened']) || vm.epic.getIn(['user_stories_counts', 'closed'])" + ) + + .project(ng-if="vm.column.project") .sprint( ng-if="vm.column.sprint" - translate="EPICS.TABLE.SPRINT" ) .assigned( ng-if="vm.column.assigned && vm.epic.get('assigned_to')" - ) + ) img( ng-if="vm.epic.getIn(['assigned_to_extra_info', 'photo'])" ng-src="{{vm.epic.getIn(['assigned_to_extra_info', 'photo'])}}" alt="{{::vm.epic.getIn(['assigned_to_extra_info', 'full_name_display'])}}" - ) + ) img( ng-if="!vm.epic.getIn(['assigned_to_extra_info', 'photo'])" ng-src="https://www.gravatar.com/avatar/{{vm.epic.getIn(['assigned_to_extra_info', 'gravatar_id'])}}" alt="{{::vm.epic.getIn(['assigned_to_extra_info', 'full_name_display'])}}" - ) + ) .assigned( ng-if="vm.column.assigned && !vm.epic.get('assigned_to')" ng-class="{'is-unassigned': !vm.epic.get('assigned_to')}" @@ -56,7 +65,7 @@ tg-svg( svg-icon="icon-arrow-down" ) - + ul.epic-statuses(ng-show="displayStatusList") li( ng-repeat="status in vm.project.epic_statuses | orderBy:'order'" @@ -68,3 +77,12 @@ ng-if="::vm.percentage" ng-attr-width="::vm.percentage" ) +.epic-stories-wrapper(ng-if="vm.displayUserStories && vm.epicStories") + + .epic-story(tg-repeat="story in vm.epicStories track by story.get('id')") + tg-story-row( + epic="vm.epic" + story="story" + project="vm.project" + column="vm.column" + ) diff --git a/app/modules/epics/dashboard/epic-row/epic-row.scss b/app/modules/epics/dashboard/epic-row/epic-row.scss index a3d952e0..7760f8e0 100644 --- a/app/modules/epics/dashboard/epic-row/epic-row.scss +++ b/app/modules/epics/dashboard/epic-row/epic-row.scss @@ -1,9 +1,12 @@ +@import '../../../../styles/dependencies/mixins/epics-dashboard'; + .epic-row { + @include epics-table; @include font-size(small); align-items: center; background: $white; border-bottom: 1px solid $whitish; - cursor: move; + cursor: pointer; display: flex; transition: background .2s; &:hover { @@ -21,6 +24,19 @@ text-decoration: line-through; } } + &.unfold { + .name { + .icon { + transform: rotate(0deg); + } + } + } + .name { + .icon { + transform: rotate(180deg); + transition: all .2s; + } + } .icon-drag { @include svg-size(.75rem); cursor: move; @@ -28,17 +44,26 @@ opacity: 0; transition: opacity .1s; } + .epic-pill { + @include font-type(light); + @include font-size(xsmall); + background: $grayer; + border-radius: .25rem; + color: $white; + margin: 0 .5rem; + padding: .1rem .25rem; + } .status { cursor: pointer; position: relative; button { background: none; } - .icon { - @include svg-size(.7rem); - fill: $gray-light; - margin-left: .1rem; - } + } + .icon-arrow-down { + @include svg-size(.7rem); + fill: $gray-light; + margin-left: .1rem; } .progress-bar, .progress-status { diff --git a/app/modules/epics/dashboard/epics-table/epics-table.scss b/app/modules/epics/dashboard/epics-table/epics-table.scss index 5e43af45..74e50c1a 100644 --- a/app/modules/epics/dashboard/epics-table/epics-table.scss +++ b/app/modules/epics/dashboard/epics-table/epics-table.scss @@ -1,58 +1,11 @@ +@import '../../../../styles/dependencies/mixins/epics-dashboard'; + .epics-table { margin-top: 2rem; } -.epics-table-header, -.epic-row { - .assigned { - padding: .5rem; - } - .project, - .vote, - .status, - .sprint, - .name, - .progress { - padding: 1rem .5rem; - } - - .assigned, - .project, - .vote { - flex-basis: 100px; - flex-grow: 0; - flex-shrink: 0; - flex-wrap: wrap; - text-align: center; - } - .status, - .sprint { - flex-basis: 150px; - flex-grow: 0; - flex-shrink: 0; - flex-wrap: wrap; - max-width: 150px; - } - .name, - .progress { - flex-basis: 20vw; - flex-grow: 1; - flex-shrink: 2; - max-width: 40vw; - } - .name, - .sprint { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - width: 90%; - } - .progress { - position: relative; - } -} - .epics-table-header { + @include epics-table; @include font-type(bold); border-bottom: 1px solid $gray-light; display: flex; diff --git a/app/modules/epics/dashboard/story-row/story-row.controller.coffee b/app/modules/epics/dashboard/story-row/story-row.controller.coffee new file mode 100644 index 00000000..42990d67 --- /dev/null +++ b/app/modules/epics/dashboard/story-row/story-row.controller.coffee @@ -0,0 +1,35 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: epics-table.controller.coffee +### + +module = angular.module("taigaEpics") + +class StoryRowController + @.$inject = [] + + constructor: () -> + @._calculateProgressBar() + + _calculateProgressBar: () -> + tasks = @.story.get('tasks').toJS() + totalTasks = @.story.get('tasks').size + areTasksCompleted = _.map(tasks, 'is_closed') + totalTasksCompleted = _.pull(areTasksCompleted, false).length + @.percentage = totalTasksCompleted * 100 / totalTasks + +module.controller("StoryRowCtrl", StoryRowController) diff --git a/app/modules/epics/dashboard/story-row/story-row.directive.coffee b/app/modules/epics/dashboard/story-row/story-row.directive.coffee new file mode 100644 index 00000000..338f676f --- /dev/null +++ b/app/modules/epics/dashboard/story-row/story-row.directive.coffee @@ -0,0 +1,39 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: epics-table.directive.coffee +### + +module = angular.module('taigaEpics') + +StoryRowDirective = () -> + + return { + templateUrl:"epics/dashboard/story-row/story-row.html", + controller: "StoryRowCtrl", + controllerAs: "vm", + bindToController: true, + scope: { + epic: '=', + story: '=', + project: '=', + column: '=' + } + } + +StoryRowDirective.$inject = [] + +module.directive("tgStoryRow", StoryRowDirective) diff --git a/app/modules/epics/dashboard/story-row/story-row.jade b/app/modules/epics/dashboard/story-row/story-row.jade new file mode 100644 index 00000000..39a8dda4 --- /dev/null +++ b/app/modules/epics/dashboard/story-row/story-row.jade @@ -0,0 +1,54 @@ +.story-row( + ng-class="{'is-blocked': vm.story.is_blocked, 'is-closed': vm.story.is_closed}" +) + tg-svg.icon-drag( + svg-icon="icon-drag" + ) + .vote( + ng-if="vm.column.votes" + ng-class="{'is-voter': vm.story.get('is_voter')}" + ) + tg-svg(svg-icon='icon-upvote') + span {{::vm.story.get('total_voters')}} + + .name(ng-if="vm.column.name") + - var hash = "#"; + a( + tg-nav="project-userstories-detail:project=vm.project.slug,ref=vm.story.get('ref')" + ng-attr-title="{{::vm.story.get('subject')}}" + ) #{hash}{{::vm.story.get('ref')}} {{::vm.story.get('subject')}} + .story-pill(ng-style="::{'background-color': vm.epic.get('color')}") + .project( + ng-if="vm.column.project" + tg-nav="project:project=vm.story.getIn(['project_extra_info', 'slug'])" + ) + img( + tg-project-logo-small-src="::vm.story.get('project_extra_info')" + alt="{{::vm.story.getIn(['project_extra_info', 'name'])}}" + ) + .sprint(ng-if="vm.column.sprint") {{::vm.story.get('milestone_name')}} + .assigned( + ng-if="vm.column.assigned && vm.story.get('assigned_to')" + ) + img( + ng-if="vm.story.getIn(['assigned_to_extra_info', 'photo'])" + ng-src="{{vm.story.getIn(['assigned_to_extra_info', 'photo'])}}" + alt="{{::vm.story.getIn(['assigned_to_extra_info', 'full_name_display'])}}" + ) + img( + ng-if="!vm.story.getIn(['assigned_to_extra_info', 'photo'])" + ng-src="https://www.gravatar.com/avatar/{{vm.story.getIn(['assigned_to_extra_info', 'gravatar_id'])}}" + alt="{{::vm.story.getIn(['assigned_to_extra_info', 'full_name_display'])}}" + ) + .assigned( + ng-if="vm.column.assigned && !vm.story.get('assigned_to')" + ng-class="{'is-unassigned': !vm.story.get('assigned_to')}" + translate="EPICS.DASHBOARD.UNASSIGNED" + ) + .status(ng-if="vm.column.status") {{vm.story.getIn(['status_extra_info', 'name'])}} + .progress(ng-if="vm.column.progress") + .progress-bar + .progress-status( + ng-if="::vm.percentage" + ng-attr-width="::vm.percentage" + ) diff --git a/app/modules/epics/dashboard/story-row/story-row.scss b/app/modules/epics/dashboard/story-row/story-row.scss new file mode 100644 index 00000000..0098b6fd --- /dev/null +++ b/app/modules/epics/dashboard/story-row/story-row.scss @@ -0,0 +1,80 @@ +@import '../../../../styles/dependencies/mixins/epics-dashboard'; + +.story-row { + @include font-size(small); + @include epics-table; + align-items: center; + background: $white; + border-bottom: 1px solid $whitish; + cursor: pointer; + display: flex; + margin-left: 2rem; + transition: background .2s; + &:hover { + background: rgba($primary-light, .05); + .icon-drag { + opacity: 1; + } + } + &.is-blocked { + background: rgba($red-light, .5); + } + &.is-closed { + .name { + color: $gray-light; + text-decoration: line-through; + } + } + .icon-drag { + @include svg-size(.75rem); + cursor: move; + fill: $whitish; + opacity: 0; + transition: opacity .1s; + } + .name { + flex-basis: 18vw; + } + .story-pill { + background: $grayer; + border-radius: 50%; + display: inline-block; + height: .5rem; + margin-left: .25rem; + width: .5rem; + } + .progress-bar, + .progress-status { + height: 1.5rem; + left: 0; + position: absolute; + top: .25rem; + } + .progress-bar { + background: $mass-white; + max-width: 40vw; + width: 100%; + } + .progress-status { + background: $primary-light; + width: 10vw; + } + .vote { + color: $gray; + } + .project, + .assigned { + img { + width: 40px; + } + } + .icon-upvote { + @include svg-size(.75rem); + fill: $gray; + margin-right: .25rem; + vertical-align: middle; + } + .is-unassigned { + color: $gray-light; + } +} diff --git a/app/modules/resources/userstories-resource.service.coffee b/app/modules/resources/userstories-resource.service.coffee index ce2b7cd4..f5b13f79 100644 --- a/app/modules/resources/userstories-resource.service.coffee +++ b/app/modules/resources/userstories-resource.service.coffee @@ -33,6 +33,18 @@ Resource = (urlsService, http) -> .then (result) -> return Immutable.fromJS(result.data) + service.listInEpics = (ids) -> + url = urlsService.resolve("userstories") + + params = { + 'epics': ids, + 'include_tasks': true + } + + return http.get(url, params) + .then (result) -> + return Immutable.fromJS(result.data) + return () -> return {"userstories": service} diff --git a/app/styles/dependencies/mixins/epics-dashboard.scss b/app/styles/dependencies/mixins/epics-dashboard.scss new file mode 100644 index 00000000..b472653d --- /dev/null +++ b/app/styles/dependencies/mixins/epics-dashboard.scss @@ -0,0 +1,57 @@ +@mixin epics-table { + .assigned { + padding: .5rem; + } + .project, + .vote, + .status, + .sprint, + .name, + .progress { + padding: 1rem .5rem; + } + .vote { + flex-basis: 60px; + flex-grow: 0; + flex-shrink: 0; + flex-wrap: wrap; + text-align: center; + } + .assigned, + .project { + flex-basis: 100px; + flex-grow: 0; + flex-shrink: 0; + flex-wrap: wrap; + text-align: center; + } + .status, + .sprint { + flex-basis: 150px; + flex-grow: 0; + flex-shrink: 0; + flex-wrap: wrap; + max-width: 150px; + text-align: center; + } + .name, + .progress { + flex-basis: 20vw; + flex-grow: 1; + flex-shrink: 1; + max-width: 40vw; + } + .progress { + flex-shrink: 3; + } + .name, + .sprint { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 90%; + } + .progress { + position: relative; + } +}