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;