diff --git a/app/coffee/modules/common/confirm.coffee b/app/coffee/modules/common/confirm.coffee index 607741ef..c0a6e05f 100644 --- a/app/coffee/modules/common/confirm.coffee +++ b/app/coffee/modules/common/confirm.coffee @@ -163,7 +163,7 @@ class ConfirmService extends taiga.Service return defered.promise - success: (title, message, icon) -> + success: (title, message, icon, action) -> defered = @q.defer() el = angular.element(".lightbox-generic-success") @@ -193,6 +193,9 @@ class ConfirmService extends taiga.Service # Render content el.find(".title").html(title) if title el.find(".message").html(message) if message + if action + el.find(".button-green").html(action) + el.find(".button-green").attr('title', action) # Assign event handlers el.on "click.confirm-dialog", ".button-green", (event) => diff --git a/app/coffee/modules/resources.coffee b/app/coffee/modules/resources.coffee index de2fa2e5..9c4b97f4 100644 --- a/app/coffee/modules/resources.coffee +++ b/app/coffee/modules/resources.coffee @@ -116,7 +116,6 @@ urls = { "bulk-update-us-milestone": "/userstories/bulk_update_milestone" "bulk-update-us-miles-order": "/userstories/bulk_update_sprint_order" "bulk-update-us-kanban-order": "/userstories/bulk_update_kanban_order" - "bulk-update-us-milestone": "/userstories/bulk_update_milestone" "userstories-filters": "/userstories/filters_data" "userstory-upvote": "/userstories/%s/upvote" "userstory-downvote": "/userstories/%s/downvote" @@ -127,6 +126,7 @@ urls = { "tasks": "/tasks" "bulk-create-tasks": "/tasks/bulk_create" "bulk-update-task-taskboard-order": "/tasks/bulk_update_taskboard_order" + "bulk-update-task-milestone": "/tasks/bulk_update_milestone" "task-upvote": "/tasks/%s/upvote" "task-downvote": "/tasks/%s/downvote" "task-watch": "/tasks/%s/watch" @@ -136,6 +136,7 @@ urls = { # Issues "issues": "/issues" "bulk-create-issues": "/issues/bulk_create" + "bulk-update-issue-milestone": "/issues/bulk_update_milestone" "issues-filters": "/issues/filters_data" "issue-upvote": "/issues/%s/upvote" "issue-downvote": "/issues/%s/downvote" diff --git a/app/coffee/modules/resources/issues.coffee b/app/coffee/modules/resources/issues.coffee index 50bc80bb..88f5b6a3 100644 --- a/app/coffee/modules/resources/issues.coffee +++ b/app/coffee/modules/resources/issues.coffee @@ -99,6 +99,11 @@ resourceProvider = ($repo, $http, $urls, $storage, $q) -> hash = generateHash([projectId, ns]) return $storage.get(hash) or {} + service.bulkUpdateMilestone = (projectId, milestoneId, data) -> + url = $urls.resolve("bulk-update-issue-milestone") + params = {project_id: projectId, milestone_id: milestoneId, bulk_issues: data} + return $http.post(url, params) + return (instance) -> instance.issues = service diff --git a/app/coffee/modules/resources/tasks.coffee b/app/coffee/modules/resources/tasks.coffee index 0de2558c..0344a2cc 100644 --- a/app/coffee/modules/resources/tasks.coffee +++ b/app/coffee/modules/resources/tasks.coffee @@ -85,6 +85,11 @@ resourceProvider = ($repo, $http, $urls, $storage) -> params = {project_id: projectId, bulk_tasks: data} return $http.post(url, params) + service.bulkUpdateMilestone = (projectId, milestoneId, data) -> + url = $urls.resolve("bulk-update-task-milestone") + params = {project_id: projectId, milestone_id: milestoneId, bulk_tasks: data} + return $http.post(url, params) + service.reorder = (id, data, setOrders) -> url = $urls.resolve("tasks") + "/#{id}" diff --git a/app/coffee/modules/taskboard/main.coffee b/app/coffee/modules/taskboard/main.coffee index ea240ebd..dd7290ab 100644 --- a/app/coffee/modules/taskboard/main.coffee +++ b/app/coffee/modules/taskboard/main.coffee @@ -332,6 +332,13 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga @scope.$on("taskboard:task:move", @.taskMove) @scope.$on("assigned-to:added", @.onAssignedToChanged) + @scope.$on "taskboard:items:move", (event, itemsMoved) => + if itemsMoved.uss + @.firstLoad() + else + @.loadTasks() if itemsMoved.tasks + @.loadIssues() if itemsMoved.issues + onAssignedToChanged: (ctx, userid, model) -> if model.getName() == 'tasks' model.assigned_to = userid @@ -430,6 +437,7 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga return @rs.issues.listInProject(@scope.projectId, @scope.sprintId, params).then (issues) => @taskboardIssuesService.init(@scope.project, @scope.usersById, @scope.issueStatusById) @taskboardIssuesService.set(issues) + @scope.taskBoardLoading = false loadTasks: -> params = {} @@ -577,6 +585,7 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga @confirm.notify("error") taskMove: (ctx, task, oldStatusId, usId, statusId, order) -> + @scope.movingTask = true task = @taskboardTasksService.getTaskModel(task.get('id')) moveUpdateData = @taskboardTasksService.move(task.id, usId, statusId, order) @@ -593,6 +602,10 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga } promise = @repo.save(task, true, params, options, true).then (result) => + if result[0].user_story + @.reloadUserStory(result[0].user_story) + + @scope.movingTask = false headers = result[1] if headers && headers['taiga-info-order-updated'] @@ -604,6 +617,9 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga if @.isFilterDataTypeSelected('status') @.loadTasks() + reloadUserStory: (userStoryId) -> + @rs.userstories.get(@scope.project.id, userStoryId).then (us) => + @scope.userstories = _.map(@scope.userstories, (x) -> if x.id == us.id then us else x) ## Template actions addNewTask: (type, us) -> diff --git a/app/locales/taiga/locale-en.json b/app/locales/taiga/locale-en.json index 2bd9067e..be4f58dd 100644 --- a/app/locales/taiga/locale-en.json +++ b/app/locales/taiga/locale-en.json @@ -3,6 +3,7 @@ "YES": "Yes", "NO": "No", "OR": "or", + "I_GET_IT": "OK, I get it", "LOADING": "Loading...", "DATE": "DD MMM YYYY", "DATETIME": "DD MMM YYYY HH:mm", @@ -1392,6 +1393,19 @@ "OPTIMAL": "Optimal pending points for day {{formattedDate}} should be {{roundedValue}}", "REAL": "Real pending points for day {{formattedDate}} is {{roundedValue}}", "DATE": "DD MMMM YYYY" + }, + "MOVE_TO_SPRINT": { + "TITLE_ACTION_MOVE_UNFINISHED": "Move unfinished items to another sprint", + "TITLE_MOVE_UNFINISHED": "Move unfinished items to another open sprint", + "MOVE_TO_OPEN_SPRINT": "Move to open sprint", + "SELECT_DESTINATION_PLACEHOLDER": "Select destination", + "UNFINISHED_USER_STORIES_COUNT": "{total, plural, one{# unfinished user story} other{# unfinished user stories}}", + "UNFINISHED_UNASSIGNED_TASKS_COUNT": "{total, plural, one{# unfinished unnasigned task} other{# unfinished unnasigned tasks}}", + "UNFINISHED_ISSUES_COUNT": "{total, plural, one{# unfinished issue} other{# unfinished unnasigned issue}}", + "WARNING_ISSUES_NOT_MOVED_TITLE": "You just moved all user stories and taks, and the sprint'll be closed", + "WARNING_ISSUES_NOT_MOVED": "The issues'll remain in the sprint and don't be removed", + "WARNING_SPRINT_STILL_OPEN_TITLE": "{total, plural, one{You just moved # item!} other{You just moved # items!}}", + "WARNING_SPRINT_STILL_OPEN": "Please note that the sprint {{sprintName}} appears as open as long as still contains open items." } }, "TASK": { diff --git a/app/modules/components/move-to-sprint/move-to-sprint-lb/move-to-sprint-lb.controller.coffee b/app/modules/components/move-to-sprint/move-to-sprint-lb/move-to-sprint-lb.controller.coffee new file mode 100644 index 00000000..5dac4e80 --- /dev/null +++ b/app/modules/components/move-to-sprint/move-to-sprint-lb/move-to-sprint-lb.controller.coffee @@ -0,0 +1,145 @@ +### +# Copyright (C) 2014-2018 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: components/move-to-sprint/move-to-sprint-lb/move-to-sprint-lb.controller.coffee +### + +module = angular.module("taigaComponents") + +class MoveToSprintLightboxController + @.$inject = [ + '$rootScope' + '$scope' + '$tgResources' + 'tgProjectService' + '$translate' + 'lightboxService' + '$tgConfirm' + ] + + constructor: ( + @rootScope + @scope + @rs + @projectService + @translate + @lightboxService + @confirm + ) -> + @.projectId = @projectService.project.get('id') + @.loading = false + @.someSelected = false + @.selectedSprintId = null + @.typesSelected = { + uss: false + tasks: false + issues: false + } + @.itemsToMove = {} + @._loadSprints() + + @scope.$watch "vm.openItems", (openItems) => + return if !openItems + @._init(openItems) + + _init: (openItems) -> + @.hasManyItemTypes = _.size(@.openItems) > 1 + + @.ussCount = parseInt(openItems.uss?.length) + @.updateSelected('uss', @.ussCount > 0) + + @.tasksCount = parseInt(openItems.tasks?.length) + @.updateSelected('tasks', @.tasksCount > 0) + + @.issuesCount = parseInt(openItems.issues?.length) + @.updateSelected('issues', @.issuesCount > 0) + + _loadSprints: () -> + @rs.sprints.list(@.projectId, {closed: false}).then (data) => + @.sprints = data.milestones + + updateSelected: (itemType, value) -> + @.typesSelected[itemType] = value + @.someSelected = _.some(@.typesSelected) + + if value is true + @.itemsToMove[itemType] = @.openItems[itemType] + else if @.itemsToMove[itemType] + delete @.itemsToMove[itemType] + + submit: () -> + itemsNotMoved = {} + _.map @.openItems, (itemsList, itemsType) => + if not @.itemsToMove[itemsType] + itemsNotMoved[itemsType] = true + + @.loading = true + + @moveItems().then () => + @rootScope.$broadcast("taskboard:items:move", @.typesSelected) + @lightboxService.closeAll() + @.loading = false + if _.size(itemsNotMoved) > 0 + @.displayWarning(itemsNotMoved) + + moveItems: () -> + promises = [] + if @.itemsToMove.uss + promises.push( + @rs.userstories.bulkUpdateMilestone( + @.projectId + @.selectedSprintId + @.itemsToMove.uss + ) + ) + if @.itemsToMove.tasks + promises.push( + @rs.tasks.bulkUpdateMilestone( + @.projectId + @.selectedSprintId + @.itemsToMove.tasks + ) + ) + if @.itemsToMove.issues + promises.push( + @rs.issues.bulkUpdateMilestone( + @.projectId + @.selectedSprintId + @.itemsToMove.issues + ) + ) + return Promise.all(promises) + + displayWarning: (itemsNotMoved) -> + action = @translate.instant('COMMON.I_GET_IT') + if _.size(itemsNotMoved) == 1 and itemsNotMoved.issues is true + title = @translate.instant('TASKBOARD.MOVE_TO_SPRINT.WARNING_ISSUES_NOT_MOVED_TITLE') + desc = @translate.instant('TASKBOARD.MOVE_TO_SPRINT.WARNING_ISSUES_NOT_MOVED') + else + totalItemsMoved = 0 + _.map @.itemsToMove, (itemsList, itemsType) -> totalItemsMoved += itemsList.length + title = @translate.instant( + 'TASKBOARD.MOVE_TO_SPRINT.WARNING_SPRINT_STILL_OPEN_TITLE' + { total: totalItemsMoved } + 'messageformat' + ) + desc = @translate.instant( + 'TASKBOARD.MOVE_TO_SPRINT.WARNING_SPRINT_STILL_OPEN' + { sprintName: @.sprint?.name } + ) + @confirm.success(title, desc, null, action) + +module.controller("MoveToSprintLbCtrl", MoveToSprintLightboxController) diff --git a/app/modules/components/move-to-sprint/move-to-sprint-lb/move-to-sprint-lb.directive.coffee b/app/modules/components/move-to-sprint/move-to-sprint-lb/move-to-sprint-lb.directive.coffee new file mode 100644 index 00000000..932250eb --- /dev/null +++ b/app/modules/components/move-to-sprint/move-to-sprint-lb/move-to-sprint-lb.directive.coffee @@ -0,0 +1,42 @@ +### +# Copyright (C) 2014-2018 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: components/move-to-sprint/move-to-sprint-lb/move-to-sprint-lb.directive.coffee +### + +module = angular.module("taigaComponents") + +moveToSprintLightboxDirective = (lightboxService) -> + link = (scope, el, attrs, ctrl) -> + lightboxService.open(el) + + return { + scope: {} + bindToController: { + openItems: "=" + sprint: "=" + }, + templateUrl: "components/move-to-sprint/move-to-sprint-lb/move-to-sprint-lb.html" + controller: "MoveToSprintLbCtrl" + controllerAs: "vm" + link: link + } + +moveToSprintLightboxDirective.$inject = [ + "lightboxService" +] + +module.directive("tgLbMoveToSprint", moveToSprintLightboxDirective) diff --git a/app/modules/components/move-to-sprint/move-to-sprint-lb/move-to-sprint-lb.jade b/app/modules/components/move-to-sprint/move-to-sprint-lb/move-to-sprint-lb.jade new file mode 100644 index 00000000..823e073e --- /dev/null +++ b/app/modules/components/move-to-sprint/move-to-sprint-lb/move-to-sprint-lb.jade @@ -0,0 +1,67 @@ +tg-lightbox-close + +.move-to-sprint-container + .move-to-sprint-header + h2.title {{ 'TASKBOARD.MOVE_TO_SPRINT.TITLE_ACTION_MOVE_UNFINISHED'|translate }} + + ul + li.choice(ng-if="vm.ussCount") + span(ng-bind-html="'TASKBOARD.MOVE_TO_SPRINT.UNFINISHED_USER_STORIES_COUNT'|translate:{ total: vm.ussCount || 0 }:'messageformat'") + .check.js-check(ng-if="vm.hasManyItemTypes") + input( + type="checkbox" + ng-checked="vm.typesSelected['uss']" + ng-model="vm.typesSelected['uss']" + ng-change="vm.updateSelected('uss', vm.typesSelected['uss'])" + ) + div + span.check-text.check-yes(translate="COMMON.YES") + span.check-text.check-no(translate="COMMON.NO") + + li.choice(ng-if="vm.tasksCount") + span(ng-bind-html="'TASKBOARD.MOVE_TO_SPRINT.UNFINISHED_UNASSIGNED_TASKS_COUNT'|translate:{ total: vm.tasksCount || 0 }:'messageformat'") + .check.js-check(ng-if="vm.hasManyItemTypes") + input( + type="checkbox" + ng-checked="vm.typesSelected['tasks']" + ng-model="vm.typesSelected['tasks']" + ng-change="vm.updateSelected('tasks', vm.typesSelected['tasks'])" + ) + div + span.check-text.check-yes(translate="COMMON.YES") + span.check-text.check-no(translate="COMMON.NO") + + li.choice(ng-if="vm.issuesCount") + span(ng-bind-html="'TASKBOARD.MOVE_TO_SPRINT.UNFINISHED_ISSUES_COUNT'|translate:{ total: vm.issuesCount || 0 }:'messageformat'") + .check.js-check(ng-if="vm.hasManyItemTypes") + input( + type="checkbox" + ng-checked="vm.typesSelected['issues']" + ng-model="vm.typesSelected['issues']" + ng-change="vm.updateSelected('issues', vm.typesSelected['issues'])" + ) + div + span.check-text.check-yes(translate="COMMON.YES") + span.check-text.check-no(translate="COMMON.NO") + + .move-to-sprint-controls + fieldset + label {{ 'TASKBOARD.MOVE_TO_SPRINT.MOVE_TO_OPEN_SPRINT'|translate }} + select.sprint-select( + ng-model="vm.selectedSprintId" + ng-options="s.id as s.name for s in vm.sprints|filter: { id: '!' + vm.sprint.id }" + id="sprint-selector-dropdown" + autofocus + ) + option( + value="" + disabled + selected + translate="TASKBOARD.MOVE_TO_SPRINT.SELECT_DESTINATION_PLACEHOLDER" + ) + button.button-green.move-button( + href="" + ng-click="vm.submit()" + translate="COMMON.SAVE" + ng-disabled="!(vm.selectedSprintId && vm.someSelected)" + ) diff --git a/app/modules/components/move-to-sprint/move-to-sprint-lb/move-to-sprint-lb.scss b/app/modules/components/move-to-sprint/move-to-sprint-lb/move-to-sprint-lb.scss new file mode 100644 index 00000000..95654969 --- /dev/null +++ b/app/modules/components/move-to-sprint/move-to-sprint-lb/move-to-sprint-lb.scss @@ -0,0 +1,32 @@ +.lightbox-move-to-sprint .move-to-sprint-container { + display: flex; + flex-direction: column; + max-width: 550px; + width: 100%; + .move-to-sprint-header { + margin: 0 auto; + max-width: 400px; + text-align: center; + ul { + display: inline-block; + margin: .5em auto 2.5em; + width: auto; + } + li { + display: flex; + justify-content: space-between; + margin-top: 1em; + } + .check { + margin-left: 4em; + } + } + .move-to-sprint-controls { + .sprint-select { + margin-top: .5em; + } + .move-button { + width: 100%; + } + } +} diff --git a/app/modules/components/move-to-sprint/move-to-sprint.controller.coffee b/app/modules/components/move-to-sprint/move-to-sprint.controller.coffee new file mode 100644 index 00000000..60d98b89 --- /dev/null +++ b/app/modules/components/move-to-sprint/move-to-sprint.controller.coffee @@ -0,0 +1,90 @@ +### +# Copyright (C) 2014-2018 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: components/move-to-sprint/move-to-sprint-controller.coffee +### + +taiga = @.taiga + +class MoveToSprintController + @.$inject = [ + '$scope' + 'tgLightboxFactory' + ] + + constructor: (@scope, @lightboxFactory) -> + @.hasOpenItems = false + @.disabled = false + @.openItems = { + uss: [] + tasks: [] + issues: [] + } + + @scope.$watch "vm.uss", () => @getOpenUss() + @scope.$watch "vm.unnasignedTasks", () => @getOpenUnassignedTasks() + @scope.$watch "vm.issues", () => @getOpenIssues() + + checkOpenItems: () -> + return _.some(Object.keys(@.openItems), (x) => @.openItems[x].length > 0) + + openLightbox: () -> + if @.disabled is not true && @.hasOpenItems + openItems = {} + _.map @.openItems, (itemsList, itemsType) -> + if itemsList.length + openItems[itemsType] = itemsList + + @lightboxFactory.create('tg-lb-move-to-sprint', { + "class": "lightbox lightbox-move-to-sprint" + "sprint": "sprint" + "open-items": "openItems" + }, { + sprint: @.sprint + openItems: openItems + }) + + getOpenUss: () -> + return if !@.uss + @.openItems.uss = [] + @.uss.map (us) => + if us.is_closed is false + @.openItems.uss.push({ + us_id: us.id + order: us.sprint_order + }) + @.hasOpenItems = @checkOpenItems() + + getOpenUnassignedTasks: () -> + return if !@.unnasignedTasks + @.openItems.tasks = [] + @.unnasignedTasks.map (column) => column.map (task) => + if task.get('model').get('is_closed') is false + @.openItems.tasks.push({ + task_id: task.get('model').get('id') + order: task.get('model').get('taskboard_order') + }) + @.hasOpenItems = @checkOpenItems() + + getOpenIssues: () -> + return if !@.issues + @.openItems.issues = [] + @.issues.map (issue) => + if issue.get('status').get('is_closed') is false + @.openItems.issues.push({ issue_id: issue.get('id') }) + @.hasOpenItems = @checkOpenItems() + +angular.module('taigaComponents').controller('MoveToSprintCtrl', MoveToSprintController) diff --git a/app/modules/components/move-to-sprint/move-to-sprint.controller.spec.coffee b/app/modules/components/move-to-sprint/move-to-sprint.controller.spec.coffee new file mode 100644 index 00000000..6ee5ec89 --- /dev/null +++ b/app/modules/components/move-to-sprint/move-to-sprint.controller.spec.coffee @@ -0,0 +1,114 @@ +### +# Copyright (C) 2014-2018 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: components/move-to-sprint/move-to-sprint-controller.spec.coffee +### + +describe "MoveToSprint", -> + provide = null + $controller = null + scope = null + ctrl = null + mocks = {} + + _mockTgLightboxFactory = () -> + mocks.tgLightboxFactory = { + create: sinon.stub() + } + + provide.value "tgLightboxFactory", mocks.tgLightboxFactory + + _mocks = () -> + module ($provide) -> + provide = $provide + _mockTgLightboxFactory() + return null + + _inject = -> + inject (_$controller_, $rootScope) -> + $controller = _$controller_ + scope = $rootScope.$new() + + _setup = -> + _mocks() + _inject() + + beforeEach -> + module "taigaComponents" + _setup() + + ctrl = $controller("MoveToSprintCtrl", { + $scope: scope + }, { + uss: null, + unnasignedTasks: null, + issues: null, + disabled: false + }) + + describe "button", -> + it "is disabled by default", () -> + expect(ctrl.hasOpenItems).to.be.false + + it "is enabled when milestone has open user stories", () -> + ctrl.uss = [ + { id: 1, is_closed: true, sprint_order: 5 } + { id: 2, is_closed: false, sprint_order: 6 } + { id: 3, is_closed: false, sprint_order: 7 } + ] + ctrl.getOpenUss() + expect(ctrl.hasOpenItems).to.be.true + expect(ctrl.openItems.uss).to.be.eql([ + { us_id: 2, order: 6 } + { us_id: 3, order: 7 } + ]) + + it "is enabled when milestone has open unassigned tasks", () -> + ctrl.unnasignedTasks = Immutable.fromJS([ + [ + { model: { id: 1, is_closed: true, taskboard_order: 5 } } + { model: { id: 2, is_closed: false, taskboard_order: 6 } } + ], + [{ model: { id: 3, is_closed: false, taskboard_order: 7 } }] + ]) + ctrl.getOpenUnassignedTasks() + expect(ctrl.hasOpenItems).to.be.true + expect(ctrl.openItems.tasks).to.be.eql([ + { task_id: 2, order: 6 } + { task_id: 3, order: 7 } + ]) + + it "is enabled when milestone has open issues", () -> + ctrl.issues = Immutable.fromJS([ + { id: 1, status: { is_closed: true } } + { id: 2, status: { is_closed: false } } + ]) + ctrl.getOpenIssues() + expect(ctrl.hasOpenItems).to.be.true + expect(ctrl.openItems.issues).to.be.eql([{ issue_id: 2 }]) + + describe "lightbox", -> + it "is opened on button click if has open items", () -> + ctrl.issues = Immutable.fromJS([ + { id: 1, status: { is_closed: false } } + ]) + ctrl.getOpenIssues() + ctrl.openLightbox() + expect(mocks.tgLightboxFactory.create).have.been.called + + it "is not opened on button click if has no open items", () -> + ctrl.openLightbox() + expect(mocks.tgLightboxFactory.create).not.have.been.called diff --git a/app/modules/components/move-to-sprint/move-to-sprint.directive.coffee b/app/modules/components/move-to-sprint/move-to-sprint.directive.coffee new file mode 100644 index 00000000..2dde5ded --- /dev/null +++ b/app/modules/components/move-to-sprint/move-to-sprint.directive.coffee @@ -0,0 +1,41 @@ +### +# Copyright (C) 2014-2018 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: components/move-to-sprint/move-to-sprint.directive.coffee +### + +module = angular.module("taigaComponents") + +moveToSprintDirective = (taskboardTasksService) -> + return { + controller: "MoveToSprintCtrl" + controllerAs: "vm" + bindToController: true + templateUrl: 'components/move-to-sprint/move-to-sprint.html' + scope: { + sprint: '=' + uss: '=' + unnasignedTasks: '=' + issues: '=' + disabled: '=' + } + } + +moveToSprintDirective.$inject = [ + 'tgTaskboardTasks' +] + +module.directive('tgMoveToSprint', [moveToSprintDirective]) \ No newline at end of file diff --git a/app/modules/components/move-to-sprint/move-to-sprint.jade b/app/modules/components/move-to-sprint/move-to-sprint.jade new file mode 100644 index 00000000..87489f3f --- /dev/null +++ b/app/modules/components/move-to-sprint/move-to-sprint.jade @@ -0,0 +1,6 @@ +a.move-to-sprint-button.is-editable( + title="{{ 'TASKBOARD.MOVE_TO_SPRINT.TITLE_ACTION_MOVE_UNFINISHED' | translate }}" + ng-class="{'disabled': !vm.hasOpenItems || vm.disabled}" + ng-click="vm.openLightbox()" +) + tg-svg(svg-icon="icon-move") diff --git a/app/modules/components/move-to-sprint/move-to-sprint.scss b/app/modules/components/move-to-sprint/move-to-sprint.scss new file mode 100644 index 00000000..a4e31c76 --- /dev/null +++ b/app/modules/components/move-to-sprint/move-to-sprint.scss @@ -0,0 +1,15 @@ +.move-to-sprint-button { + color: $white; + &:not(.disabled) { + cursor: pointer; + &:hover { + color: $primary-light; + } + } + &.disabled { + opacity: .5; + &:hover { + color: $white; + } + } +} diff --git a/app/partials/includes/components/sprint-summary.jade b/app/partials/includes/components/sprint-summary.jade index b4f5f6ac..7b41edd4 100644 --- a/app/partials/includes/components/sprint-summary.jade +++ b/app/partials/includes/components/sprint-summary.jade @@ -23,6 +23,16 @@ div.summary.large-summary span.number(ng-bind="stats.completed_tasks|default:'--'") span.description(translate="BACKLOG.SPRINT_SUMMARY.CLOSED_TASKS") + div.summary-stats.summary-move-unfinished + tg-move-to-sprint( + sprint="sprint" + uss="userstories" + unnasigned-tasks="usTasks.get('null')" + issues="milestoneIssues" + disabled="movingTask" + ) + span.taskBoardLoading + div.summary-stats.summary-iocaine(title="{{'COMMON.IOCAINE_TEXT' | translate}}") tg-svg(svg-icon="icon-iocaine") span.number(ng-bind="stats.iocaine_doses|default:'--'") diff --git a/app/styles/components/summary.scss b/app/styles/components/summary.scss index 8033c81a..5aea761a 100644 --- a/app/styles/components/summary.scss +++ b/app/styles/components/summary.scss @@ -149,7 +149,7 @@ $summary-background: $grayer; margin: 0; } &.summary-completed-points, - &.summary-closed-tasks { + &.summary-move-unfinished { border-right: 1px solid $blackish; margin-right: 0; padding-right: 1rem;