diff --git a/app/coffee/app.coffee b/app/coffee/app.coffee
index 97c3efbd..fb81877e 100644
--- a/app/coffee/app.coffee
+++ b/app/coffee/app.coffee
@@ -46,7 +46,7 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
$animateProvider.classNameFilter(/^(?:(?!ng-animate-disabled).)*$/)
- # wait until the trasnlation is ready to resolve the page
+ # wait until the translation is ready to resolve the page
originalWhen = $routeProvider.when
$routeProvider.when = (path, route) ->
@@ -162,6 +162,15 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
}
)
+ # Epics
+ $routeProvider.when("/project/:pslug/epic/:epicref",
+ {
+ templateUrl: "epic/epic-detail.html",
+ loader: true,
+ section: "epics"
+ }
+ )
+
$routeProvider.when("/project/:pslug/backlog",
{
templateUrl: "backlog/backlog.html",
@@ -793,6 +802,7 @@ modules = [
"taigaPlugins",
"taigaIntegrations",
"taigaComponents",
+
# new modules
"taigaProfile",
"taigaHome",
@@ -801,7 +811,7 @@ modules = [
"taigaDiscover",
"taigaHistory",
"taigaWikiHistory",
- 'taigaEpics',
+ "taigaEpics",
# template cache
"templates",
diff --git a/app/coffee/modules/common/filters.coffee b/app/coffee/modules/common/filters.coffee
index 7590b45c..17696c55 100644
--- a/app/coffee/modules/common/filters.coffee
+++ b/app/coffee/modules/common/filters.coffee
@@ -74,3 +74,29 @@ sizeFormat = =>
return @.taiga.sizeFormat
module.filter("sizeFormat", sizeFormat)
+
+
+toMutableFilter = ->
+ toMutable = (js) ->
+ return js.toJS()
+
+ memoizedMutable = _.memoize(toMutable)
+
+ return (input) ->
+ if input instanceof Immutable.List
+ return memoizedMutable(input)
+
+ return input
+
+module.filter("toMutable", toMutableFilter)
+
+
+byRefFilter = ($filterFilter)->
+ return (userstories, filter) ->
+ if filter?.startsWith("#")
+ cleanRef= filter.substr(1)
+ return _.filter(userstories, (us) => String(us.ref).startsWith(cleanRef))
+
+ return $filterFilter(userstories, filter)
+
+module.filter("byRef", ["filterFilter", byRefFilter])
diff --git a/app/coffee/modules/epics.coffee b/app/coffee/modules/epics.coffee
index 15941253..743e70d4 100644
--- a/app/coffee/modules/epics.coffee
+++ b/app/coffee/modules/epics.coffee
@@ -19,7 +19,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
#
-# File: modules/projects.coffee
+# File: modules/epics.coffee
###
module = angular.module("taigaEpics", [])
diff --git a/app/coffee/modules/epics/detail.coffee b/app/coffee/modules/epics/detail.coffee
new file mode 100644
index 00000000..f5a35849
--- /dev/null
+++ b/app/coffee/modules/epics/detail.coffee
@@ -0,0 +1,337 @@
+###
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino Garcia
+# Copyright (C) 2014-2016 David Barragán Merino
+# Copyright (C) 2014-2016 Alejandro Alonso
+# Copyright (C) 2014-2016 Juan Francisco Alcántara
+# Copyright (C) 2014-2016 Xavi Julian
+#
+# 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: modules/epics/detail.coffee
+###
+
+taiga = @.taiga
+
+mixOf = @.taiga.mixOf
+toString = @.taiga.toString
+joinStr = @.taiga.joinStr
+groupBy = @.taiga.groupBy
+bindOnce = @.taiga.bindOnce
+bindMethods = @.taiga.bindMethods
+
+module = angular.module("taigaEpics")
+
+#############################################################################
+## Epic Detail Controller
+#############################################################################
+
+class EpicDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
+ @.$inject = [
+ "$scope",
+ "$rootScope",
+ "$tgRepo",
+ "$tgConfirm",
+ "$tgResources",
+ "tgResources"
+ "$routeParams",
+ "$q",
+ "$tgLocation",
+ "$log",
+ "tgAppMetaService",
+ "$tgAnalytics",
+ "$tgNavUrls",
+ "$translate",
+ "$tgQueueModelTransformation",
+ "tgErrorHandlingService"
+ ]
+
+ constructor: (@scope, @rootscope, @repo, @confirm, @rs, @rs2, @params, @q, @location,
+ @log, @appMetaService, @analytics, @navUrls, @translate, @modelTransform, @errorHandlingService) ->
+ bindMethods(@)
+
+ @scope.epicRef = @params.epicref
+ @scope.sectionName = @translate.instant("EPIC.SECTION_NAME")
+ @.initializeEventHandlers()
+
+ promise = @.loadInitialData()
+
+ # On Success
+ promise.then =>
+ @._setMeta()
+ @.initializeOnDeleteGoToUrl()
+
+ # On Error
+ promise.then null, @.onInitialDataError.bind(@)
+
+ _setMeta: ->
+ title = @translate.instant("EPIC.PAGE_TITLE", {
+ epicRef: "##{@scope.epic.ref}"
+ epicSubject: @scope.epic.subject
+ projectName: @scope.project.name
+ })
+ description = @translate.instant("EPIC.PAGE_DESCRIPTION", {
+ epicStatus: @scope.statusById[@scope.epic.status]?.name or "--"
+ epicDescription: angular.element(@scope.epic.description_html or "").text()
+ })
+ @appMetaService.setAll(title, description)
+
+ initializeEventHandlers: ->
+ @scope.$on "attachment:create", =>
+ @analytics.trackEvent("attachment", "create", "create attachment on epic", 1)
+
+ @scope.$on "comment:new", =>
+ @.loadEpic()
+
+ @scope.$on "custom-attributes-values:edit", =>
+ @rootscope.$broadcast("object:updated")
+
+ initializeOnDeleteGoToUrl: ->
+ ctx = {project: @scope.project.slug}
+ @scope.onDeleteGoToUrl = @navUrls.resolve("project-epics", ctx)
+
+ loadProject: ->
+ return @rs.projects.getBySlug(@params.pslug).then (project) =>
+ @scope.projectId = project.id
+ @scope.project = project
+ @scope.immutableProject = Immutable.fromJS(project._attrs)
+ @scope.$emit('project:loaded', project)
+ @scope.statusList = project.epic_statuses
+ @scope.statusById = groupBy(project.epic_statuses, (x) -> x.id)
+ return project
+
+ loadEpic: ->
+ return @rs.epics.getByRef(@scope.projectId, @params.epicref).then (epic) =>
+ @scope.epic = epic
+ @scope.immutableEpic = Immutable.fromJS(epic._attrs)
+ @scope.epicId = epic.id
+ @scope.commentModel = epic
+
+ @modelTransform.setObject(@scope, 'epic')
+
+ if @scope.epic.neighbors.previous?.ref?
+ ctx = {
+ project: @scope.project.slug
+ ref: @scope.epic.neighbors.previous.ref
+ }
+ @scope.previousUrl = @navUrls.resolve("project-epics-detail", ctx)
+
+ if @scope.epic.neighbors.next?.ref?
+ ctx = {
+ project: @scope.project.slug
+ ref: @scope.epic.neighbors.next.ref
+ }
+ @scope.nextUrl = @navUrls.resolve("project-epics-detail", ctx)
+
+ loadUserstories: ->
+ return @rs2.userstories.listInEpic(@scope.epicId).then (data) =>
+ @scope.userstories = data
+
+ loadInitialData: ->
+ promise = @.loadProject()
+ return promise.then (project) =>
+ @.fillUsersAndRoles(project.members, project.roles)
+ @.loadEpic().then(=> @.loadUserstories())
+
+ ###
+ # Note: This methods (onUpvote() and onDownvote()) are related to tg-vote-button.
+ # See app/modules/components/vote-button for more info
+ ###
+ onUpvote: ->
+ onSuccess = =>
+ @.loadEpic()
+ @rootscope.$broadcast("object:updated")
+ onError = =>
+ @confirm.notify("error")
+
+ return @rs.epics.upvote(@scope.epicId).then(onSuccess, onError)
+
+ onDownvote: ->
+ onSuccess = =>
+ @.loadEpic()
+ @rootscope.$broadcast("object:updated")
+ onError = =>
+ @confirm.notify("error")
+
+ return @rs.epics.downvote(@scope.epicId).then(onSuccess, onError)
+
+ ###
+ # Note: This methods (onWatch() and onUnwatch()) are related to tg-watch-button.
+ # See app/modules/components/watch-button for more info
+ ###
+ onWatch: ->
+ onSuccess = =>
+ @.loadEpic()
+ @rootscope.$broadcast("object:updated")
+ onError = =>
+ @confirm.notify("error")
+
+ return @rs.epics.watch(@scope.epicId).then(onSuccess, onError)
+
+ onUnwatch: ->
+ onSuccess = =>
+ @.loadEpic()
+ @rootscope.$broadcast("object:updated")
+ onError = =>
+ @confirm.notify("error")
+
+ return @rs.epics.unwatch(@scope.epicId).then(onSuccess, onError)
+
+ onSelectColor: (color) ->
+ onSelectColorSuccess = () =>
+ @rootscope.$broadcast("object:updated")
+ @confirm.notify('success')
+
+ onSelectColorError = () =>
+ @confirm.notify('error')
+
+ transform = @modelTransform.save (epic) ->
+ epic.color = color
+ return epic
+
+ return transform.then(onSelectColorSuccess, onSelectColorError)
+
+module.controller("EpicDetailController", EpicDetailController)
+
+
+#############################################################################
+## Epic status display directive
+#############################################################################
+
+EpicStatusDisplayDirective = ($template, $compile) ->
+ # Display if an epic is open or closed and its status.
+ #
+ # Example:
+ # tg-epic-status-display(ng-model="epic")
+ #
+ # Requirements:
+ # - Epic object (ng-model)
+ # - scope.statusById object
+
+ template = $template.get("common/components/status-display.html", true)
+
+ link = ($scope, $el, $attrs) ->
+ render = (epic) ->
+ status = $scope.statusById[epic.status]
+
+ html = template({
+ is_closed: status.is_closed
+ status: status
+ })
+
+ html = $compile(html)($scope)
+ $el.html(html)
+
+ $scope.$watch $attrs.ngModel, (epic) ->
+ render(epic) if epic?
+
+ $scope.$on "$destroy", ->
+ $el.off()
+
+ return {
+ link: link
+ restrict: "EA"
+ require: "ngModel"
+ }
+
+module.directive("tgEpicStatusDisplay", ["$tgTemplate", "$compile", EpicStatusDisplayDirective])
+
+
+#############################################################################
+## Epic status button directive
+#############################################################################
+
+EpicStatusButtonDirective = ($rootScope, $repo, $confirm, $loading, $modelTransform, $compile, $translate, $template) ->
+ # Display the status of epic and you can edit it.
+ #
+ # Example:
+ # tg-epic-status-button(ng-model="epic")
+ #
+ # Requirements:
+ # - Epic object (ng-model)
+ # - scope.statusById object
+ # - $scope.project.my_permissions
+
+ template = $template.get("common/components/status-button.html", true)
+
+ link = ($scope, $el, $attrs, $model) ->
+ isEditable = ->
+ return $scope.project.my_permissions.indexOf("modify_epic") != -1
+
+ render = (epic) =>
+ status = $scope.statusById[epic.status]
+
+ html = $compile(template({
+ status: status
+ statuses: $scope.statusList
+ editable: isEditable()
+ }))($scope)
+
+ $el.html(html)
+
+ save = (status) ->
+ currentLoading = $loading()
+ .target($el)
+ .start()
+
+ transform = $modelTransform.save (epic) ->
+ epic.status = status
+
+ return epic
+
+ onSuccess = ->
+ $rootScope.$broadcast("object:updated")
+ currentLoading.finish()
+
+ onError = ->
+ $confirm.notify("error")
+ currentLoading.finish()
+
+ transform.then(onSuccess, onError)
+
+ $el.on "click", ".js-edit-status", (event) ->
+ event.preventDefault()
+ event.stopPropagation()
+ return if not isEditable()
+
+ $el.find(".pop-status").popover().open()
+
+ $el.on "click", ".status", (event) ->
+ event.preventDefault()
+ event.stopPropagation()
+ return if not isEditable()
+
+ target = angular.element(event.currentTarget)
+
+ $.fn.popover().closeAll()
+
+ save(target.data("status-id"))
+
+ $scope.$watch () ->
+ return $model.$modelValue?.status
+ , () ->
+ epic = $model.$modelValue
+ render(epic) if epic
+
+ $scope.$on "$destroy", ->
+ $el.off()
+
+ return {
+ link: link
+ restrict: "EA"
+ require: "ngModel"
+ }
+
+module.directive("tgEpicStatusButton", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading", "$tgQueueModelTransformation",
+ "$compile", "$translate", "$tgTemplate", EpicStatusButtonDirective])
diff --git a/app/coffee/modules/resources.coffee b/app/coffee/modules/resources.coffee
index 9085f7da..20b9fa9a 100644
--- a/app/coffee/modules/resources.coffee
+++ b/app/coffee/modules/resources.coffee
@@ -95,6 +95,12 @@ urls = {
# Epics
"epics": "/epics"
+ "epic-upvote": "/epics/%s/upvote"
+ "epic-downvote": "/epics/%s/downvote"
+ "epic-watch": "/epics/%s/watch"
+ "epic-unwatch": "/epics/%s/unwatch"
+ "epic-related-userstories": "/epics/%s/related_userstories"
+ "epic-related-userstories-bulk-create": "/epics/%s/related_userstories/bulk_create"
# User stories
"userstories": "/userstories"
@@ -134,12 +140,14 @@ urls = {
"wiki-links": "/wiki-links"
# History
+ "history/epic": "/history/epic"
"history/us": "/history/userstory"
"history/issue": "/history/issue"
"history/task": "/history/task"
"history/wiki": "/history/wiki/%s"
# Attachments
+ "attachments/epic": "/epics/attachments"
"attachments/us": "/userstories/attachments"
"attachments/issue": "/issues/attachments"
"attachments/task": "/tasks/attachments"
diff --git a/app/coffee/modules/resources/epics.coffee b/app/coffee/modules/resources/epics.coffee
index 9793f485..480395ce 100644
--- a/app/coffee/modules/resources/epics.coffee
+++ b/app/coffee/modules/resources/epics.coffee
@@ -28,10 +28,16 @@ taiga = @.taiga
generateHash = taiga.generateHash
-resourceProvider = ($repo, $storage) ->
+resourceProvider = ($repo, $http, $urls, $storage) ->
service = {}
hashSuffix = "epics-queryparams"
+ service.getByRef = (projectId, ref) ->
+ params = service.getQueryParams(projectId)
+ params.project = projectId
+ params.ref = ref
+ return $repo.queryOne("epics", "by_ref", params)
+
service.listValues = (projectId, type) ->
params = {"project": projectId}
service.storeQueryParams(projectId, params)
@@ -47,9 +53,25 @@ resourceProvider = ($repo, $storage) ->
hash = generateHash([projectId, ns])
return $storage.get(hash) or {}
+ service.upvote = (epicId) ->
+ url = $urls.resolve("epic-upvote", epicId)
+ return $http.post(url)
+
+ service.downvote = (epicId) ->
+ url = $urls.resolve("epic-downvote", epicId)
+ return $http.post(url)
+
+ service.watch = (epicId) ->
+ url = $urls.resolve("epic-watch", epicId)
+ return $http.post(url)
+
+ service.unwatch = (epicId) ->
+ url = $urls.resolve("epic-unwatch", epicId)
+ return $http.post(url)
+
return (instance) ->
instance.epics = service
module = angular.module("taigaResources")
-module.factory("$tgEpicsResourcesProvider", ["$tgRepo", "$tgStorage", resourceProvider])
+module.factory("$tgEpicsResourcesProvider", ["$tgRepo","$tgHttp", "$tgUrls", "$tgStorage", resourceProvider])
diff --git a/app/locales/taiga/locale-en.json b/app/locales/taiga/locale-en.json
index 3403ff03..cd73c545 100644
--- a/app/locales/taiga/locale-en.json
+++ b/app/locales/taiga/locale-en.json
@@ -47,6 +47,7 @@
"CAPSLOCK_WARNING": "Be careful! You are using capital letters in an input field that is case sensitive.",
"CONFIRM_CLOSE_EDIT_MODE_TITLE": "Are you sure you want to close the edit mode?",
"CONFIRM_CLOSE_EDIT_MODE_MESSAGE": "Remember that if you close the edit mode without saving all the changes will be lost",
+ "RELATED_USERSTORIES": "Related user stories",
"CARD": {
"ASSIGN_TO": "Assign To",
"EDIT": "Edit card"
@@ -1061,6 +1062,26 @@
"BUTTON": "Ask this project member to become the new project owner"
}
},
+ "EPIC": {
+ "PAGE_TITLE": "{{epicSubject}} - Epic {{epicRef}} - {{projectName}}",
+ "PAGE_DESCRIPTION": "Status: {{epicStatus }}. Description: {{epicDescription}}",
+ "SECTION_NAME": "Epic",
+ "TITLE_LIGHTBOX_DELETE_RELATED_USERSTORY": "Delete related userstory...",
+ "MSG_LIGHTBOX_DELETE_RELATED_USERSTORY": "the related userstory '{{subject}}'",
+ "ERROR_DELETE_RELATED_USERSTORY": "We have not been able to delete: {{errorMessage}}",
+ "CREATE_RELATED_USERSTORIES": "Create a relationship with a user story",
+ "RELATED_WITH": "Related with",
+ "NEW_USERSTORY": "New user story",
+ "EXISTING_USERSTORY": "Existing user story",
+ "CHOOSE_PROJECT_FOR_CREATION": "Whats' the project?",
+ "SUBJECT": "Subject",
+ "SUBJECT_BULK_MODE": "Subject (bulk insert)",
+ "CHOOSE_PROJECT_FROM": "What's the project?",
+ "CHOOSE_USERSTORY": "What's the user story?",
+ "FILTER_USERSTORIES": "Filter user stories",
+ "LIGHTBOX_TITLE_BLOKING_EPIC": "Blocking epic",
+ "ACTION_DELETE": "Delete epic"
+ },
"US": {
"PAGE_TITLE": "{{userStorySubject}} - User Story {{userStoryRef}} - {{projectName}}",
"PAGE_DESCRIPTION": "Status: {{userStoryStatus }}. Completed {{userStoryProgressPercentage}}% ({{userStoryClosedTasks}} of {{userStoryTotalTasks}} tasks closed). Points: {{userStoryPoints}}. Description: {{userStoryDescription}}",
diff --git a/app/modules/components/belong-to-epics/belong-to-epics-text.jade b/app/modules/components/belong-to-epics/belong-to-epics-text.jade
index f8b935d0..db9614b9 100644
--- a/app/modules/components/belong-to-epics/belong-to-epics-text.jade
+++ b/app/modules/components/belong-to-epics/belong-to-epics-text.jade
@@ -6,5 +6,5 @@ span.belong-to-epic-text-wrapper(tg-repeat="epic in epics track by epic.get('id'
)
a.belong-to-epic-text(
href=""
- tg-nav="project-epics-detail:project=vm.project.get('slug')"
+ tg-nav="project-epics-detail:project=epic.getIn(['project', 'slug']),ref=epic.get('ref')"
) #{hash}{{epic.get('id')}} {{epic.get('subject')}}
diff --git a/app/modules/components/belong-to-epics/belong-to-epics.directive.coffee b/app/modules/components/belong-to-epics/belong-to-epics.directive.coffee
index 1e7d8fa7..08d4ac43 100644
--- a/app/modules/components/belong-to-epics/belong-to-epics.directive.coffee
+++ b/app/modules/components/belong-to-epics/belong-to-epics.directive.coffee
@@ -25,9 +25,6 @@ BelongToEpicsDirective = () ->
if scope.epics && !scope.epics.isIterable
scope.epics = Immutable.fromJS(scope.epics)
- if scope.project && !scope.project.isIterable
- scope.project = Immutable.fromJS(scope.project)
-
scope.getTemplateUrl = () ->
if attrs.format
return "components/belong-to-epics/belong-to-epics-" + attrs.format + ".html"
diff --git a/app/modules/components/detail/header/detail-header.jade b/app/modules/components/detail/header/detail-header.jade
index 82615b20..317065c0 100644
--- a/app/modules/components/detail/header/detail-header.jade
+++ b/app/modules/components/detail/header/detail-header.jade
@@ -41,7 +41,6 @@
ng-if="::vm.item.epics"
epics="::vm.item.epics"
format="text"
- project="project"
)
//- Task belongs to US
@@ -60,7 +59,6 @@
ng-if="::vm.item.user_story_extra_info.epics"
epics="::vm.item.user_story_extra_info.epics"
format="pill"
- project="vm.project"
)
//- User Stories generated from issue
diff --git a/app/modules/components/watch-button/watch-button.controller.coffee b/app/modules/components/watch-button/watch-button.controller.coffee
index 99514424..e7cbae9c 100644
--- a/app/modules/components/watch-button/watch-button.controller.coffee
+++ b/app/modules/components/watch-button/watch-button.controller.coffee
@@ -45,7 +45,8 @@ class WatchButtonController
perms = {
userstories: 'modify_us',
issues: 'modify_issue',
- tasks: 'modify_task'
+ tasks: 'modify_task',
+ epics: 'modify_epic'
}
return perms[name]
diff --git a/app/modules/epics/dashboard/epic-row/epic-row.jade b/app/modules/epics/dashboard/epic-row/epic-row.jade
index 452d928a..2dc410b5 100644
--- a/app/modules/epics/dashboard/epic-row/epic-row.jade
+++ b/app/modules/epics/dashboard/epic-row/epic-row.jade
@@ -15,7 +15,7 @@
.name(ng-if="vm.column.name")
- var hash = "#";
a(
- tg-nav="project-epics-detail:project=vm.project.get('slug')"
+ tg-nav="project-epics-detail:project=vm.project.slug,ref=vm.epic.get('ref')"
ng-attr-title="{{::vm.epic.get('subject')}}"
) #{hash}{{::vm.epic.get('ref')}} {{::vm.epic.get('subject')}}
span.epic-pill(
diff --git a/app/modules/epics/related-userstories/related-userstories-controller.coffee b/app/modules/epics/related-userstories/related-userstories-controller.coffee
new file mode 100644
index 00000000..8042fa8f
--- /dev/null
+++ b/app/modules/epics/related-userstories/related-userstories-controller.coffee
@@ -0,0 +1,33 @@
+###
+# 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: related-userstories.controller.coffee
+###
+
+module = angular.module("taigaEpics")
+
+class RelatedUserStoriesController
+ @.$inject = ["tgResources"]
+
+ constructor: (@rs) ->
+ @.sectionName = "Epics"
+ @.showCreateRelatedUserstoriesLightbox = false
+
+ loadRelatedUserstories: () ->
+ @rs.userstories.listInEpic(@.epic.get('id')).then (data) =>
+ @.userstories = data
+
+module.controller("RelatedUserStoriesCtrl", RelatedUserStoriesController)
diff --git a/app/modules/epics/related-userstories/related-userstories-create/related-userstories-create.controller.coffee b/app/modules/epics/related-userstories/related-userstories-create/related-userstories-create.controller.coffee
new file mode 100644
index 00000000..8b51a2c2
--- /dev/null
+++ b/app/modules/epics/related-userstories/related-userstories-create/related-userstories-create.controller.coffee
@@ -0,0 +1,92 @@
+###
+# 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: related-userstory-create.controller.coffee
+###
+
+module = angular.module("taigaEpics")
+
+class RelatedUserstoriesCreateController
+ @.$inject = [
+ "tgCurrentUserService",
+ "tgResources",
+ "$tgConfirm",
+ "$tgAnalytics"
+ ]
+
+ constructor: (@currentUserService, @rs, @confirm, @analytics) ->
+ @.projects = @currentUserService.projects.get("all")
+ @.projectUserstories = Immutable.List()
+ @.loading = false
+
+ selectProject: (selectedProjectId, onSelectedProject) ->
+ @rs.userstories.listAllInProject(selectedProjectId).then (data) =>
+ excludeIds = @.epicUserstories.map((us) -> us.get('id'))
+ filteredData = data.filter((us) -> excludeIds.indexOf(us.get('id')) == -1)
+ @.projectUserstories = filteredData
+ if onSelectedProject
+ onSelectedProject()
+
+ saveRelatedUserStory: (selectedUserstoryId, onSavedRelatedUserstory) ->
+ # This method assumes the following methods are binded to the controller:
+ # - validateExistingUserstoryForm
+ # - setExistingUserstoryFormErrors
+ # - loadRelatedUserstories
+ return if not @.validateExistingUserstoryForm()
+
+ @.loading = true
+
+ onError = (data) =>
+ @.loading = false
+ @confirm.notify("error")
+ @.setExistingUserstoryFormErrors(data)
+
+ onSuccess = () =>
+ @analytics.trackEvent("epic related user story", "create", "create related user story on epic", 1)
+ @.loading = false
+ if onSavedRelatedUserstory
+ onSavedRelatedUserstory()
+ @.loadRelatedUserstories()
+
+ epicId = @.epic.get('id')
+ @rs.epics.addRelatedUserstory(epicId, selectedUserstoryId).then(onSuccess, onError)
+
+ bulkCreateRelatedUserStories: (selectedProjectId, userstoriesText, onCreatedRelatedUserstory) ->
+ # This method assumes the following methods are binded to the controller:
+ # - validateNewUserstoryForm
+ # - setNewUserstoryFormErrors
+ # - loadRelatedUserstories
+ return if not @.validateNewUserstoryForm()
+
+ @.loading = true
+
+ onError = (data) =>
+ @.loading = false
+ @confirm.notify("error")
+ @.setNewUserstoryFormErrors(data)
+
+ onSuccess = () =>
+ @analytics.trackEvent("epic related user story", "create", "create related user story on epic", 1)
+ @.loading = false
+ if onCreatedRelatedUserstory
+ onCreatedRelatedUserstory()
+ @.loadRelatedUserstories()
+
+ epicId = @.epic.get('id')
+ @rs.epics.bulkCreateRelatedUserStories(epicId, selectedProjectId, userstoriesText).then(onSuccess, onError)
+
+
+module.controller("RelatedUserstoriesCreateCtrl", RelatedUserstoriesCreateController)
diff --git a/app/modules/epics/related-userstories/related-userstories-create/related-userstories-create.controller.spec.coffee b/app/modules/epics/related-userstories/related-userstories-create/related-userstories-create.controller.spec.coffee
new file mode 100644
index 00000000..f3bc84b1
--- /dev/null
+++ b/app/modules/epics/related-userstories/related-userstories-create/related-userstories-create.controller.spec.coffee
@@ -0,0 +1,185 @@
+###
+# 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: related-userstories-create.controller.spec.coffee
+###
+
+describe "RelatedUserstoriesCreate", ->
+ RelatedUserstoriesCreateCtrl = null
+ provide = null
+ controller = null
+ mocks = {}
+
+ _mockTgCurrentUserService = () ->
+ mocks.tgCurrentUserService = {
+ projects: {
+ get: sinon.stub()
+ }
+ }
+
+ provide.value "tgCurrentUserService", mocks.tgCurrentUserService
+
+ _mockTgConfirm = () ->
+ mocks.tgConfirm = {
+ askOnDelete: sinon.stub()
+ notify: sinon.stub()
+ }
+
+ provide.value "$tgConfirm", mocks.tgConfirm
+
+
+ _mockTgResources = () ->
+ mocks.tgResources = {
+ userstories: {
+ listAllInProject: sinon.stub()
+ }
+ epics: {
+ deleteRelatedUserstory: sinon.stub()
+ addRelatedUserstory: sinon.stub()
+ bulkCreateRelatedUserStories: sinon.stub()
+ }
+ }
+
+ provide.value "tgResources", mocks.tgResources
+
+ _mockTgAnalytics = () ->
+ mocks.tgAnalytics = {
+ trackEvent: sinon.stub()
+ }
+
+ provide.value "$tgAnalytics", mocks.tgAnalytics
+
+ _mocks = () ->
+ module ($provide) ->
+ provide = $provide
+ _mockTgCurrentUserService()
+ _mockTgConfirm()
+ _mockTgResources()
+ _mockTgAnalytics()
+ return null
+
+ beforeEach ->
+ module "taigaEpics"
+
+ _mocks()
+
+ inject ($controller) ->
+ controller = $controller
+
+ RelatedUserstoriesCreateCtrl = controller "RelatedUserstoriesCreateCtrl"
+
+ it "select project", (done) ->
+ # This test tries to reproduce a project containing userstories 11 and 12 where 11
+ # is yet related to the epic
+ RelatedUserstoriesCreateCtrl.epicUserstories = Immutable.fromJS([
+ {
+ id: 11
+ }
+ ])
+
+ onSelectedProjectCallback = sinon.stub()
+ userstories = Immutable.fromJS([
+ {
+ id: 11
+ },
+ {
+
+ id: 12
+ }
+ ])
+ filteredUserstories = Immutable.fromJS([
+ {
+
+ id: 12
+ }
+ ])
+
+ promise = mocks.tgResources.userstories.listAllInProject.withArgs(1).promise().resolve(userstories)
+ RelatedUserstoriesCreateCtrl.selectProject(1, onSelectedProjectCallback).then () ->
+ expect(RelatedUserstoriesCreateCtrl.projectUserstories.toJS()).to.eql(filteredUserstories.toJS())
+ done()
+
+ it "save related user story success", (done) ->
+ RelatedUserstoriesCreateCtrl.validateExistingUserstoryForm = sinon.stub()
+ RelatedUserstoriesCreateCtrl.validateExistingUserstoryForm.returns(true)
+ onSavedRelatedUserstoryCallback = sinon.stub()
+ onSavedRelatedUserstoryCallback.returns(true)
+ RelatedUserstoriesCreateCtrl.loadRelatedUserstories = sinon.stub()
+ RelatedUserstoriesCreateCtrl.epic = Immutable.fromJS({
+ id: 1
+ })
+ promise = mocks.tgResources.epics.addRelatedUserstory.withArgs(1, 11).promise().resolve(true)
+ RelatedUserstoriesCreateCtrl.saveRelatedUserStory(11, onSavedRelatedUserstoryCallback).then () ->
+ expect(RelatedUserstoriesCreateCtrl.validateExistingUserstoryForm).have.been.calledOnce
+ expect(onSavedRelatedUserstoryCallback).have.been.calledOnce
+ expect(mocks.tgResources.epics.addRelatedUserstory).have.been.calledWith(1, 11)
+ expect(mocks.tgAnalytics.trackEvent).have.been.calledWith("epic related user story", "create", "create related user story on epic", 1)
+ expect(RelatedUserstoriesCreateCtrl.loadRelatedUserstories).have.been.calledOnce
+ done()
+
+ it "save related user story error", (done) ->
+ RelatedUserstoriesCreateCtrl.validateExistingUserstoryForm = sinon.stub()
+ RelatedUserstoriesCreateCtrl.validateExistingUserstoryForm.returns(true)
+ onSavedRelatedUserstoryCallback = sinon.stub()
+ RelatedUserstoriesCreateCtrl.setExistingUserstoryFormErrors = sinon.stub()
+ RelatedUserstoriesCreateCtrl.setExistingUserstoryFormErrors.returns({})
+ RelatedUserstoriesCreateCtrl.epic = Immutable.fromJS({
+ id: 1
+ })
+ promise = mocks.tgResources.epics.addRelatedUserstory.withArgs(1, 11).promise().reject(new Error("error"))
+ RelatedUserstoriesCreateCtrl.saveRelatedUserStory(11, onSavedRelatedUserstoryCallback).then () ->
+ expect(RelatedUserstoriesCreateCtrl.validateExistingUserstoryForm).have.been.calledOnce
+ expect(onSavedRelatedUserstoryCallback).to.not.have.been.called
+ expect(mocks.tgResources.epics.addRelatedUserstory).have.been.calledWith(1, 11)
+ expect(mocks.tgConfirm.notify).have.been.calledWith("error")
+ expect(RelatedUserstoriesCreateCtrl.setExistingUserstoryFormErrors).have.been.calledOnce
+ done()
+
+ it "bulk create related user stories success", (done) ->
+ RelatedUserstoriesCreateCtrl.validateNewUserstoryForm = sinon.stub()
+ RelatedUserstoriesCreateCtrl.validateNewUserstoryForm.returns(true)
+ onCreatedRelatedUserstoryCallback = sinon.stub()
+ onCreatedRelatedUserstoryCallback.returns(true)
+ RelatedUserstoriesCreateCtrl.loadRelatedUserstories = sinon.stub()
+ RelatedUserstoriesCreateCtrl.epic = Immutable.fromJS({
+ id: 1
+ })
+ promise = mocks.tgResources.epics.bulkCreateRelatedUserStories.withArgs(1, 22, 'a\nb').promise().resolve(true)
+ RelatedUserstoriesCreateCtrl.bulkCreateRelatedUserStories(22, 'a\nb', onCreatedRelatedUserstoryCallback).then () ->
+ expect(RelatedUserstoriesCreateCtrl.validateNewUserstoryForm).have.been.calledOnce
+ expect(onCreatedRelatedUserstoryCallback).have.been.calledOnce
+ expect(mocks.tgResources.epics.bulkCreateRelatedUserStories).have.been.calledWith(1, 22, 'a\nb')
+ expect(mocks.tgAnalytics.trackEvent).have.been.calledWith("epic related user story", "create", "create related user story on epic", 1)
+ expect(RelatedUserstoriesCreateCtrl.loadRelatedUserstories).have.been.calledOnce
+ done()
+
+ it "bulk create related user stories error", (done) ->
+ RelatedUserstoriesCreateCtrl.validateNewUserstoryForm = sinon.stub()
+ RelatedUserstoriesCreateCtrl.validateNewUserstoryForm.returns(true)
+ onCreatedRelatedUserstoryCallback = sinon.stub()
+ RelatedUserstoriesCreateCtrl.setNewUserstoryFormErrors = sinon.stub()
+ RelatedUserstoriesCreateCtrl.setNewUserstoryFormErrors.returns({})
+ RelatedUserstoriesCreateCtrl.epic = Immutable.fromJS({
+ id: 1
+ })
+ promise = mocks.tgResources.epics.bulkCreateRelatedUserStories.withArgs(1, 22, 'a\nb').promise().reject(new Error("error"))
+ RelatedUserstoriesCreateCtrl.bulkCreateRelatedUserStories(22, 'a\nb', onCreatedRelatedUserstoryCallback).then () ->
+ expect(RelatedUserstoriesCreateCtrl.validateNewUserstoryForm).have.been.calledOnce
+ expect(onCreatedRelatedUserstoryCallback).to.not.have.been.called
+ expect(mocks.tgResources.epics.bulkCreateRelatedUserStories).have.been.calledWith(1, 22, 'a\nb')
+ expect(mocks.tgConfirm.notify).have.been.calledWith("error")
+ expect(RelatedUserstoriesCreateCtrl.setNewUserstoryFormErrors).have.been.calledOnce
+ done()
diff --git a/app/modules/epics/related-userstories/related-userstories-create/related-userstories-create.directive.coffee b/app/modules/epics/related-userstories/related-userstories-create/related-userstories-create.directive.coffee
new file mode 100644
index 00000000..9ecd4a03
--- /dev/null
+++ b/app/modules/epics/related-userstories/related-userstories-create/related-userstories-create.directive.coffee
@@ -0,0 +1,79 @@
+###
+# 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: related-userstory-create.directive.coffee
+###
+
+module = angular.module('taigaEpics')
+
+RelatedUserstoriesCreateDirective = (@lightboxService) ->
+ link = (scope, el, attrs, ctrl) ->
+ newUserstoryForm = el.find(".new-user-story-form").checksley()
+ existingUserstoryForm = el.find(".existing-user-story-form").checksley()
+
+ ctrl.validateNewUserstoryForm = =>
+ return newUserstoryForm.validate()
+
+ ctrl.setNewUserstoryFormErrors = (errors) =>
+ newUserstoryForm.setErrors(errors)
+
+ ctrl.validateExistingUserstoryForm = =>
+ return existingUserstoryForm.validate()
+
+ ctrl.setExistingUserstoryFormErrors = (errors) =>
+ existingUserstoryForm.setErrors(errors)
+
+ scope.showLightbox = (selectedProjectId) ->
+ scope.selectProject(selectedProjectId).then () =>
+ lightboxService.open(el.find(".lightbox-create-related-user-stories"))
+
+ scope.closeLightbox = () ->
+ scope.selectedUserstory = null
+ scope.searchUserstory = ""
+ scope.relatedUserstoriesText = ""
+ lightboxService.close(el.find(".lightbox-create-related-user-stories"))
+
+ scope.$watch 'vm.project', (project) ->
+ if project?
+ scope.selectedProject = project.get('id')
+
+ scope.selectProject = (selectedProjectId) ->
+ scope.selectedUserstory = null
+ scope.searchUserstory = ""
+ ctrl.selectProject(selectedProjectId)
+
+ scope.onUpdateSearchUserstory = () ->
+ scope.selectedUserstory = null
+
+ return {
+ link: link,
+ templateUrl:"epics/related-userstories/related-userstories-create/related-userstories-create.html",
+ controller: "RelatedUserstoriesCreateCtrl",
+ controllerAs: "vm",
+ bindToController: true,
+ scope: {
+ showCreateRelatedUserstoriesLightbox: "="
+ project: "="
+ epic: "="
+ epicUserstories: "="
+ loadRelatedUserstories:"&"
+ }
+
+ }
+
+RelatedUserstoriesCreateDirective.$inject = ["lightboxService",]
+
+module.directive("tgRelatedUserstoriesCreate", RelatedUserstoriesCreateDirective)
diff --git a/app/modules/epics/related-userstories/related-userstories-create/related-userstories-create.jade b/app/modules/epics/related-userstories/related-userstories-create/related-userstories-create.jade
new file mode 100644
index 00000000..468acbbb
--- /dev/null
+++ b/app/modules/epics/related-userstories/related-userstories-create/related-userstories-create.jade
@@ -0,0 +1,153 @@
+a.add-button.e2e-add-userstory-button(
+ href=""
+ ng-click="showLightbox(vm.project.get('id'))"
+)
+ tg-svg(svg-icon="icon-add")
+
+div.lightbox.lightbox-create-related-user-stories
+ tg-lightbox-close
+
+ div.form
+ h2.title(translate="EPIC.CREATE_RELATED_USERSTORIES")
+
+ .related-with-selector-title
+ legend(translate="EPIC.RELATED_WITH")
+
+ .related-with-selector
+ fieldset
+ input(
+ type="radio"
+ name="related-with-selector"
+ id="new-user-story"
+ value="new-user-story"
+ ng-model="relatedWithSelector"
+ ng-init="relatedWithSelector='new-user-story'"
+ )
+ label.e2e-new-userstory-label(for="new-user-story")
+ span.name {{ 'EPIC.NEW_USERSTORY' | translate}}
+
+ fieldset
+ input(
+ type="radio"
+ name="related-with-selector"
+ id="existing-user-story"
+ value="existing-user-story"
+ ng-model="relatedWithSelector"
+ )
+ label.e2e-existing-user-story-label(for="existing-user-story")
+ span.name {{ 'EPIC.EXISTING_USERSTORY' | translate}}
+
+ .project-selector-title
+ legend(
+ ng-if="relatedWithSelector=='new-user-story'"
+ translate="EPIC.CHOOSE_PROJECT_FOR_CREATION"
+ )
+
+ legend(
+ ng-if="relatedWithSelector=='existing-user-story'"
+ translate="EPIC.CHOOSE_PROJECT_FROM"
+ )
+
+ .project-selector()
+ select(
+ ng-model="selectedProject"
+ ng-change="selectProject(selectedProject)"
+ data-required="true"
+ required
+ ng-options="p.id as p.name for p in vm.projects | toMutable"
+ )
+
+ div(ng-show="relatedWithSelector=='new-user-story'")
+ .new-user-story-selector
+ .new-user-story-title
+ legend(
+ ng-show="creationMode=='single-new-user-story'"
+ translate="EPIC.SUBJECT"
+ )
+
+ legend(
+ ng-show="creationMode=='bulk-new-user-stories'"
+ translate="EPIC.SUBJECT_BULK_MODE"
+ )
+ .new-user-story-options
+ fieldset
+ input(
+ type="radio"
+ name="new-user-story-selector"
+ id="single-new-user-story"
+ value="single-new-user-story"
+ ng-model="creationMode"
+ ng-init="creationMode='single-new-user-story'"
+ )
+ label.e2e-single-creation-label(for="single-new-user-story")
+ tg-svg(svg-icon="icon-add")
+
+ fieldset
+ input(
+ type="radio"
+ name="new-user-story-selector"
+ id="bulk-new-user-stories"
+ value="bulk-new-user-stories"
+ ng-model="creationMode"
+ )
+ label.e2e-bulk-creation-label(for="bulk-new-user-stories")
+ tg-svg(svg-icon="icon-bulk")
+
+ form.new-user-story-form
+ .single-creation(ng-show="creationMode=='single-new-user-story'")
+ input.e2e-new-userstory-input-text(
+ type="text"
+ ng-model="relatedUserstoriesText"
+ data-required="true"
+ )
+
+ .bulk-creation(ng-show="creationMode=='bulk-new-user-stories'")
+ textarea.e2e-new-userstories-input-textarea(
+ ng-model="relatedUserstoriesText"
+ data-required="true"
+ )
+
+ a.button-green.e2e-create-userstory-button(
+ href=""
+ ng-click="vm.bulkCreateRelatedUserStories(selectedProject, relatedUserstoriesText, closeLightbox)"
+ tg-loading="vm.loading"
+ )
+ span(
+ translate="COMMON.SAVE"
+ )
+
+ .existing-user-story(ng-show="relatedWithSelector=='existing-user-story'")
+ .existing-user-story-title
+ legend(translate="EPIC.CHOOSE_USERSTORY")
+
+ input.userstory.e2e-filter-userstories-input(
+ type="text"
+ placeholder="{{'EPIC.FILTER_USERSTORIES' | translate}}"
+ ng-model="searchUserstory"
+ ng-change="onUpdateSearchUserstory()"
+ )
+
+ form.existing-user-story-form
+ select.userstory.e2e-userstories-select(
+ size="5"
+ ng-model="selectedUserstory"
+ required
+ data-required="true"
+ )
+ - var hash = "#";
+ option.hidden(
+ value=""
+ )
+ option(
+ ng-repeat="us in vm.projectUserstories | toMutable | byRef:searchUserstory track by us.id"
+ value="{{ ::us.id }}"
+ ) #{hash}{{::us.ref}} {{::us.subject}}
+
+ a.button-green.e2e-select-related-userstory-button(
+ href=""
+ ng-click="vm.saveRelatedUserStory(selectedUserstory, closeLightbox)"
+ tg-loading="vm.loading"
+ )
+ span(
+ translate="COMMON.SAVE"
+ )
diff --git a/app/modules/epics/related-userstories/related-userstories-create/related-userstories-create.scss b/app/modules/epics/related-userstories/related-userstories-create/related-userstories-create.scss
new file mode 100644
index 00000000..82412585
--- /dev/null
+++ b/app/modules/epics/related-userstories/related-userstories-create/related-userstories-create.scss
@@ -0,0 +1,78 @@
+.lightbox-create-related-user-stories {
+ .related-with-selector-title,
+ .project-selector-title,
+ .new-user-story-title,
+ .existing-user-story-title {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 1rem;
+ }
+ .related-with-selector,
+ .new-user-story-selector {
+ display: flex;
+ input {
+ display: none;
+ }
+ fieldset {
+ &:first-child {
+ margin-right: .5rem;
+ }
+ }
+ }
+ .project-selector,
+ .single-creation {
+ margin-bottom: 1rem;
+ }
+ input {
+ &:checked+label {
+ background: $primary-light;
+ color: $white;
+ transition: background .2s ease-in;
+ &:hover {
+ background: $primary-light;
+ }
+ }
+ +label {
+ background: rgba($whitish, .7);
+ cursor: pointer;
+ display: block;
+ padding: 2rem 1rem;
+ text-align: center;
+ transition: background .2s ease-in;
+ &:hover {
+ background: rgba($primary-light, .3);
+ transition: background .2s ease-in;
+ }
+ .icon {
+ fill: currentColor;
+ margin-top: .25rem;
+ vertical-align: text-top;
+ }
+ .name {
+ @include font-size(large);
+ text-transform: uppercase;
+ }
+ }
+ }
+ .new-user-story-selector {
+ display: flex;
+ justify-content: space-between;
+ .new-user-story-options {
+ display: flex;
+ }
+ fieldset {
+ width: auto;
+ }
+ label {
+ height: 1.5rem;
+ padding: 0;
+ width: 1.5rem;
+ }
+ }
+
+ .existing-user-story {
+ .button-green {
+ margin-top: 1rem;
+ }
+ }
+}
diff --git a/app/modules/epics/related-userstories/related-userstories.controller.spec.coffee b/app/modules/epics/related-userstories/related-userstories.controller.spec.coffee
new file mode 100644
index 00000000..9162c935
--- /dev/null
+++ b/app/modules/epics/related-userstories/related-userstories.controller.spec.coffee
@@ -0,0 +1,66 @@
+###
+# 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: related-userstories.controller.spec.coffee
+###
+
+describe "RelatedUserStories", ->
+ RelatedUserStoriesCtrl = null
+ provide = null
+ controller = null
+ mocks = {}
+
+ _mockTgResources = () ->
+ mocks.tgResources = {
+ userstories: {
+ listInEpic: sinon.stub()
+ }
+ }
+
+ provide.value "tgResources", mocks.tgResources
+
+ _mocks = () ->
+ module ($provide) ->
+ provide = $provide
+ _mockTgResources()
+
+ return null
+
+ beforeEach ->
+ module "taigaEpics"
+
+ _mocks()
+
+ inject ($controller) ->
+ controller = $controller
+
+ RelatedUserStoriesCtrl = controller "RelatedUserStoriesCtrl"
+
+ it "load related userstories", (done) ->
+ userstories = Immutable.fromJS([
+ {
+ id: 1
+ }
+ ])
+
+ RelatedUserStoriesCtrl.epic = Immutable.fromJS({
+ id: 66
+ })
+
+ promise = mocks.tgResources.userstories.listInEpic.withArgs(66).promise().resolve(userstories)
+ RelatedUserStoriesCtrl.loadRelatedUserstories().then () ->
+ expect(RelatedUserStoriesCtrl.userstories).is.equal(userstories)
+ done()
diff --git a/app/modules/epics/related-userstories/related-userstories.directive.coffee b/app/modules/epics/related-userstories/related-userstories.directive.coffee
new file mode 100644
index 00000000..e3db9be8
--- /dev/null
+++ b/app/modules/epics/related-userstories/related-userstories.directive.coffee
@@ -0,0 +1,37 @@
+###
+# 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: related-userstories.directive.coffee
+###
+
+module = angular.module('taigaEpics')
+
+RelatedUserStoriesDirective = () ->
+ return {
+ templateUrl:"epics/related-userstories/related-userstories.html",
+ controller: "RelatedUserStoriesCtrl",
+ controllerAs: "vm",
+ bindToController: true,
+ scope: {
+ userstories: '=',
+ project: '='
+ epic: '='
+ }
+ }
+
+RelatedUserStoriesDirective.$inject = []
+
+module.directive("tgRelatedUserstories", RelatedUserStoriesDirective)
diff --git a/app/modules/epics/related-userstories/related-userstories.jade b/app/modules/epics/related-userstories/related-userstories.jade
new file mode 100644
index 00000000..ecf642de
--- /dev/null
+++ b/app/modules/epics/related-userstories/related-userstories.jade
@@ -0,0 +1,23 @@
+section.related-userstories
+ .related-userstories-header
+ span.related-userstories-title(translate="COMMON.RELATED_USERSTORIES")
+ tg-related-userstories-create(
+ tg-check-permission="modify_epic"
+ show-create-related-userstories-lightbox="vm.showCreateRelatedUserstoriesLightbox"
+ project="vm.project"
+ epic="vm.epic"
+ epic-userstories="vm.userstories"
+ load-related-userstories="vm.loadRelatedUserstories()"
+ )
+
+ .related-userstories-body
+ div(tg-repeat="us in vm.userstories track by us.get('id')")
+ tg-related-userstory-row.row(
+ ng-class="{closed: us.get('is_closed'), blocked: us.get('is_blocked')}"
+ userstory="us"
+ epic="vm.epic"
+ project="vm.project"
+ load-related-userstories="vm.loadRelatedUserstories()"
+ )
+
+ div(tg-related-userstories-create-form)
diff --git a/app/modules/epics/related-userstories/related-userstories.scss b/app/modules/epics/related-userstories/related-userstories.scss
new file mode 100644
index 00000000..62bc0b46
--- /dev/null
+++ b/app/modules/epics/related-userstories/related-userstories.scss
@@ -0,0 +1,147 @@
+.related-userstories {
+ margin-bottom: 2rem;
+ position: relative;
+}
+
+.related-userstories-header {
+ align-content: center;
+ align-items: center;
+ background: $mass-white;
+ display: flex;
+ justify-content: space-between;
+ min-height: 36px;
+ .related-userstories-title {
+ @include font-size(medium);
+ @include font-type(bold);
+ margin-left: 1rem;
+ }
+ .add-button {
+ background: $grayer;
+ border: 0;
+ display: inline-block;
+ padding: .5rem;
+ transition: background .25s;
+ &:hover,
+ &.is-active {
+ background: $primary-light;
+ }
+ svg {
+ fill: $white;
+ height: 1.25rem;
+ margin-bottom: -.2rem;
+ width: 1.25rem;
+ }
+ }
+}
+
+.related-userstories-body {
+ width: 100%;
+ .row {
+ @include font-size(small);
+ align-items: center;
+ border-bottom: 1px solid $whitish;
+ display: flex;
+ padding: .5rem 0 .5rem .5rem;
+ &:hover {
+ .userstory-settings {
+ opacity: 1;
+ transition: all .2s ease-in;
+ }
+ }
+ .userstory-name {
+ flex: 1;
+ }
+ .userstory-settings {
+ flex-shrink: 0;
+ width: 60px;
+ }
+ .status {
+ flex-shrink: 0;
+ width: 125px;
+ }
+ .assigned-to-column {
+ flex-shrink: 0;
+ width: 150px;
+ img {
+ flex-basis: 35px;
+ // width & height they are only required for IE
+ height: 35px;
+ width: 35px;
+ }
+ }
+ .project {
+ flex-basis: 100px;
+ img {
+ width: 40px;
+ }
+ }
+ }
+
+ .userstory-name {
+ display: flex;
+ margin-right: 1rem;
+
+ span {
+ margin-right: .25rem;
+ }
+ }
+ .status {
+ position: relative;
+ }
+ .closed {
+ border-left: 10px solid $whitish;
+ color: $whitish;
+ a,
+ svg {
+ fill: $whitish;
+ }
+ .userstory-name a {
+ color: $whitish;
+ text-decoration: line-through;
+
+ }
+ }
+ .blocked {
+ background: rgba($red-light, .2);
+ border-left: 10px solid $red-light;
+ }
+ .userstory-settings {
+ align-items: center;
+ display: flex;
+ opacity: 0;
+ svg {
+ @include svg-size(1.1rem);
+ fill: $gray-light;
+ margin-right: .5rem;
+ transition: fill .2s ease-in;
+ &:hover {
+ fill: $gray;
+ }
+ }
+ a {
+ &:hover {
+ cursor: pointer;
+ }
+ }
+ }
+ .delete-userstory {
+ &:hover {
+ .icon-trash {
+ fill: $red-light;
+ }
+ }
+ }
+ .avatar {
+ align-items: center;
+ display: flex;
+ img {
+ flex-basis: 35px;
+ // width & height they are only required for IE
+ height: 35px;
+ width: 35px;
+ }
+ figcaption {
+ margin-left: .5rem;
+ }
+ }
+}
diff --git a/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.controller.coffee b/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.controller.coffee
new file mode 100644
index 00000000..ef58ab9b
--- /dev/null
+++ b/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.controller.coffee
@@ -0,0 +1,63 @@
+###
+# 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: reñated-userstory-row.controller.coffee
+###
+
+module = angular.module("taigaEpics")
+
+class RelatedUserstoryRowController
+ @.$inject = [
+ "tgAvatarService",
+ "$translate",
+ "$tgConfirm",
+ "tgResources"
+ ]
+
+ constructor: (@avatarService, @translate, @confirm, @rs) ->
+
+ setAvatarData: () ->
+ member = @.userstory.get('assigned_to_extra_info')
+ @.avatar = @avatarService.getAvatar(member)
+
+ getAssignedToFullNameDisplay: () ->
+ if @.userstory.get('assigned_to')
+ return @.userstory.getIn(['assigned_to_extra_info', 'full_name_display'])
+
+ return @translate.instant("COMMON.ASSIGNED_TO.NOT_ASSIGNED")
+
+ onDeleteRelatedUserstory: () ->
+ title = @translate.instant('EPIC.TITLE_LIGHTBOX_DELETE_RELATED_USERSTORY')
+ message = @translate.instant('EPIC.MSG_LIGHTBOX_DELETE_RELATED_USERSTORY', {
+ subject: @.userstory.get('subject')
+ })
+
+ return @confirm.askOnDelete(title, message)
+ .then (askResponse) =>
+ onError = () =>
+ message = @translate.instant('EPIC.ERROR_DELETE_RELATED_USERSTORY', {errorMessage: message})
+ @confirm.notify("error", null, message)
+ askResponse.finish(false)
+
+ onSuccess = () =>
+ @.loadRelatedUserstories()
+ askResponse.finish()
+
+ epicId = @.epic.get('id')
+ userstoryId = @.userstory.get('id')
+ @rs.epics.deleteRelatedUserstory(epicId, userstoryId).then(onSuccess, onError)
+
+module.controller("RelatedUserstoryRowCtrl", RelatedUserstoryRowController)
diff --git a/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.controller.spec.coffee b/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.controller.spec.coffee
new file mode 100644
index 00000000..c300b372
--- /dev/null
+++ b/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.controller.spec.coffee
@@ -0,0 +1,169 @@
+###
+# 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: related-userstory-row.controller.spec.coffee
+###
+
+describe "RelatedUserstoryRow", ->
+ RelatedUserstoryRowCtrl = null
+ provide = null
+ controller = null
+ mocks = {}
+
+ _mockTgConfirm = () ->
+ mocks.tgConfirm = {
+ askOnDelete: sinon.stub()
+ notify: sinon.stub()
+ }
+
+ provide.value "$tgConfirm", mocks.tgConfirm
+
+ _mockTgAvatarService = () ->
+ mocks.tgAvatarService = {
+ getAvatar: sinon.stub()
+ }
+
+ provide.value "tgAvatarService", mocks.tgAvatarService
+
+ _mockTranslate = () ->
+ mocks.translate = {
+ instant: sinon.stub()
+ }
+
+ provide.value "$translate", mocks.translate
+
+ _mockTgResources = () ->
+ mocks.tgResources = {
+ epics: {
+ deleteRelatedUserstory: sinon.stub()
+ }
+ }
+
+ provide.value "tgResources", mocks.tgResources
+
+ _mocks = () ->
+ module ($provide) ->
+ provide = $provide
+ _mockTgConfirm()
+ _mockTgAvatarService()
+ _mockTranslate()
+ _mockTgResources()
+
+ return null
+
+ beforeEach ->
+ module "taigaEpics"
+
+ _mocks()
+
+ inject ($controller) ->
+ controller = $controller
+
+ RelatedUserstoryRowCtrl = controller "RelatedUserstoryRowCtrl"
+
+ it "set avatar data", (done) ->
+ RelatedUserstoryRowCtrl.userstory = Immutable.fromJS({
+ assigned_to_extra_info: {
+ id: 3
+ }
+ })
+ member = RelatedUserstoryRowCtrl.userstory.get("assigned_to_extra_info")
+ avatar = {
+ url: "http://taiga.io"
+ bg: "#AAAAAA"
+ }
+ mocks.tgAvatarService.getAvatar.withArgs(member).returns(avatar)
+ RelatedUserstoryRowCtrl.setAvatarData()
+ expect(mocks.tgAvatarService.getAvatar).have.been.calledWith(member)
+ expect(RelatedUserstoryRowCtrl.avatar).is.equal(avatar)
+ done()
+
+ it "get assigned to full name display for existing user", (done) ->
+ RelatedUserstoryRowCtrl.userstory = Immutable.fromJS({
+ assigned_to: 1
+ assigned_to_extra_info: {
+ full_name_display: "Beta tester"
+ }
+ })
+
+ expect(RelatedUserstoryRowCtrl.getAssignedToFullNameDisplay()).is.equal("Beta tester")
+ done()
+
+ it "get assigned to full name display for unassigned user story", (done) ->
+ RelatedUserstoryRowCtrl.userstory = Immutable.fromJS({
+ assigned_to: null
+ })
+ mocks.translate.instant.withArgs("COMMON.ASSIGNED_TO.NOT_ASSIGNED").returns("Unassigned")
+ expect(RelatedUserstoryRowCtrl.getAssignedToFullNameDisplay()).is.equal("Unassigned")
+ done()
+
+ it "delete related userstory success", (done) ->
+ RelatedUserstoryRowCtrl.epic = Immutable.fromJS({
+ id: 123
+ })
+ RelatedUserstoryRowCtrl.userstory = Immutable.fromJS({
+ subject: "Deleting"
+ id: 124
+ })
+
+ RelatedUserstoryRowCtrl.loadRelatedUserstories = sinon.stub()
+
+ askResponse = {
+ finish: sinon.spy()
+ }
+
+ mocks.translate.instant.withArgs("EPIC.TITLE_LIGHTBOX_DELETE_RELATED_USERSTORY").returns("title")
+ mocks.translate.instant.withArgs("EPIC.MSG_LIGHTBOX_DELETE_RELATED_USERSTORY", {subject: "Deleting"}).returns("message")
+
+ mocks.tgConfirm.askOnDelete = sinon.stub()
+ mocks.tgConfirm.askOnDelete.withArgs("title", "message").promise().resolve(askResponse)
+
+ promise = mocks.tgResources.epics.deleteRelatedUserstory.withArgs(123, 124).promise().resolve(true)
+ RelatedUserstoryRowCtrl.onDeleteRelatedUserstory().then () ->
+ expect(mocks.tgResources.epics.deleteRelatedUserstory).have.been.calledWith(123, 124)
+ expect(RelatedUserstoryRowCtrl.loadRelatedUserstories).have.been.calledOnce
+ expect(askResponse.finish).have.been.calledOnce
+ done()
+
+ it "delete related userstory error", (done) ->
+ RelatedUserstoryRowCtrl.epic = Immutable.fromJS({
+ id: 123
+ })
+ RelatedUserstoryRowCtrl.userstory = Immutable.fromJS({
+ subject: "Deleting"
+ id: 124
+ })
+
+ RelatedUserstoryRowCtrl.loadRelatedUserstories = sinon.stub()
+
+ askResponse = {
+ finish: sinon.spy()
+ }
+
+ mocks.translate.instant.withArgs("EPIC.TITLE_LIGHTBOX_DELETE_RELATED_USERSTORY").returns("title")
+ mocks.translate.instant.withArgs("EPIC.MSG_LIGHTBOX_DELETE_RELATED_USERSTORY", {subject: "Deleting"}).returns("message")
+ mocks.translate.instant.withArgs("EPIC.ERROR_DELETE_RELATED_USERSTORY", {errorMessage: "message"}).returns("error message")
+
+ mocks.tgConfirm.askOnDelete = sinon.stub()
+ mocks.tgConfirm.askOnDelete.withArgs("title", "message").promise().resolve(askResponse)
+
+ promise = mocks.tgResources.epics.deleteRelatedUserstory.withArgs(123, 124).promise().reject(new Error("error"))
+ RelatedUserstoryRowCtrl.onDeleteRelatedUserstory().then () ->
+ expect(mocks.tgResources.epics.deleteRelatedUserstory).have.been.calledWith(123, 124)
+ expect(RelatedUserstoryRowCtrl.loadRelatedUserstories).to.not.have.been.called
+ expect(askResponse.finish).have.been.calledWith(false)
+ expect(mocks.tgConfirm.notify).have.been.calledWith("error", null, "error message")
+ done()
diff --git a/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.directive.coffee b/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.directive.coffee
new file mode 100644
index 00000000..02ea4ebd
--- /dev/null
+++ b/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.directive.coffee
@@ -0,0 +1,42 @@
+###
+# 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: related-userstory-row.directive.coffee
+###
+
+module = angular.module('taigaEpics')
+
+RelatedUserstoryRowDirective = () ->
+ link = (scope, el, attrs, ctrl) ->
+ ctrl.setAvatarData()
+
+ return {
+ link: link,
+ templateUrl:"epics/related-userstories/related-userstory-row/related-userstory-row.html",
+ controller: "RelatedUserstoryRowCtrl",
+ controllerAs: "vm",
+ bindToController: true,
+ scope: {
+ userstory: '='
+ epic: '='
+ project: '='
+ loadRelatedUserstories:"&"
+ }
+ }
+
+RelatedUserstoryRowDirective.$inject = []
+
+module.directive("tgRelatedUserstoryRow", RelatedUserstoryRowDirective)
diff --git a/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.jade b/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.jade
new file mode 100644
index 00000000..7c7b8a41
--- /dev/null
+++ b/app/modules/epics/related-userstories/related-userstory-row/related-userstory-row.jade
@@ -0,0 +1,44 @@
+.userstory-name
+ - var hash = "#";
+ a(
+ tg-nav="project-userstories-detail:project=vm.userstory.getIn(['project_extra_info', 'slug']),ref=vm.userstory.get('ref')"
+ ng-attr-title="{{vm.userstory.get('subject')}}"
+ ) #{hash}{{vm.userstory.get('ref')}} {{vm.userstory.get('subject')}}
+
+ tg-belong-to-epics(
+ format="pill"
+ ng-if="vm.userstory.get('epics')"
+ epics="vm.userstory.get('epics')"
+ )
+
+.userstory-settings
+ a.delete-userstory.e2e-delete-userstory(
+ tg-check-permission="modify_epic"
+ title="{{'COMMON.DELETE' | translate}}"
+ href=""
+ ng-click="vm.onDeleteRelatedUserstory()"
+ )
+ tg-svg(svg-icon="icon-trash")
+
+.project(
+ tg-nav="project:project=vm.userstory.getIn(['project_extra_info', 'slug'])"
+)
+ img(
+ tg-project-logo-small-src="::vm.userstory.get('project_extra_info')"
+ alt="{{::vm.userstory.getIn(['project_extra_info', 'name'])}}"
+ )
+
+.status
+ span.userstory-status(ng-style="{'color': vm.userstory.getIn(['status_extra_info', 'color'])}") {{vm.userstory.getIn(['status_extra_info', 'name'])}}
+
+.assigned-to-column
+ figure.avatar
+ img(
+ style="background-color: {{ vm.avatar.bg }}"
+ src="{{ vm.avatar.url }}"
+ alt="{{ vm.avatar.full_name_display }}"
+ )
+
+ figcaption {{ vm.getAssignedToFullNameDisplay() }}
+
+div(tg-related-userstories-create-form)
diff --git a/app/modules/resources/epics-resource.service.coffee b/app/modules/resources/epics-resource.service.coffee
index 21129745..82d48c11 100644
--- a/app/modules/resources/epics-resource.service.coffee
+++ b/app/modules/resources/epics-resource.service.coffee
@@ -51,6 +51,31 @@ Resource = (urlsService, http) ->
return http.post(url, params)
+ service.addRelatedUserstory = (epicId, userstoryId) ->
+ url = urlsService.resolve("epic-related-userstories", epicId)
+
+ params = {
+ user_story: userstoryId
+ epic: epicId
+ }
+
+ return http.post(url, params)
+
+ service.bulkCreateRelatedUserStories = (epicId, projectId, bulk_userstories) ->
+ url = urlsService.resolve("epic-related-userstories-bulk-create", epicId)
+
+ params = {
+ bulk_userstories: bulk_userstories,
+ project_id: projectId
+ }
+
+ return http.post(url, params)
+
+ service.deleteRelatedUserstory = (epicId, userstoryId) ->
+ url = urlsService.resolve("epic-related-userstories", epicId) + "/#{userstoryId}"
+
+ return http.delete(url)
+
return () ->
return {"epics": service}
diff --git a/app/modules/resources/userstories-resource.service.coffee b/app/modules/resources/userstories-resource.service.coffee
index 7b7f9b90..d410036e 100644
--- a/app/modules/resources/userstories-resource.service.coffee
+++ b/app/modules/resources/userstories-resource.service.coffee
@@ -33,6 +33,22 @@ Resource = (urlsService, http) ->
.then (result) ->
return Immutable.fromJS(result.data)
+ service.listAllInProject = (projectId) ->
+ url = urlsService.resolve("userstories")
+
+ httpOptions = {
+ headers: {
+ "x-disable-pagination": "1"
+ }
+ }
+
+ params = {
+ project: projectId
+ }
+ return http.get(url, params, httpOptions)
+ .then (result) ->
+ return Immutable.fromJS(result.data)
+
service.listInEpic = (epicIid) ->
url = urlsService.resolve("userstories")
diff --git a/app/partials/epic/epic-detail.jade b/app/partials/epic/epic-detail.jade
new file mode 100644
index 00000000..b03ec020
--- /dev/null
+++ b/app/partials/epic/epic-detail.jade
@@ -0,0 +1,127 @@
+doctype html
+
+div.wrapper(
+ ng-controller="EpicDetailController as ctrl",
+ ng-init="section='epics'"
+)
+ tg-project-menu
+
+ div.main.us-detail
+ div.us-detail-header.header-with-actions
+ include ../includes/components/mainTitle
+
+ section.us-story-main-data
+ header
+ tg-vote-button.upvote-btn(
+ item="epic"
+ on-upvote="ctrl.onUpvote"
+ on-downvote="ctrl.onDownvote"
+ )
+
+ .detail-header-container
+ tg-color-selector(
+ color="epic.color",
+ on-select-color="ctrl.onSelectColor(color)"
+ )
+ tg-detail-header(
+ item="epic"
+ project="project"
+ required-perm="modify_epic"
+ ng-class="{blocked: epic.is_blocked}"
+ ng-if="project && epic"
+ format="text"
+ )
+ .subheader
+ tg-tag-line.tags-block(
+ ng-if="epic && project"
+ project="project"
+ item="epic"
+ permissions="modify_epic"
+ )
+ tg-created-by-display.ticket-created-by(ng-model="epic")
+
+ section.duty-content(
+ tg-editable-description
+ tg-editable-wysiwyg
+ ng-model="epic"
+ required-perm="modify_epic"
+ )
+
+ // Custom Fields
+ tg-custom-attributes-values(
+ ng-model="epic"
+ type="epic"
+ project="project"
+ required-edition-perm="modify_epic"
+ )
+
+ tg-related-userstories(
+ project="immutableProject"
+ userstories="userstories"
+ epic="immutableEpic"
+ )
+
+ tg-attachments-full(
+ obj-id="epic.id"
+ type="epic",
+ project-id="projectId"
+ edit-permission = "modify_epic"
+ )
+
+ tg-history-section(
+ ng-if="epic"
+ type="epic"
+ name="epic"
+ id="epic.id"
+ project-id="projectId"
+ )
+
+ sidebar.menu-secondary.sidebar.ticket-data
+
+ .ticket-header
+ span.ticket-title(
+ tg-epic-status-display
+ ng-model="epic"
+ )
+ span.detail-status(
+ tg-epic-status-button
+ ng-model="epic"
+ )
+
+ section.ticket-assigned-to(
+ tg-assigned-to
+ ng-model="epic"
+ required-perm="modify_epic"
+ )
+
+ section.ticket-watch-buttons
+ div.ticket-watch(
+ tg-watch-button
+ item="epic"
+ data-environment="ticket"
+ on-watch="ctrl.onWatch"
+ on-unwatch="ctrl.onUnwatch"
+ )
+ div.ticket-watchers(
+ tg-watchers
+ ng-model="epic"
+ required-perm="modify_epic"
+ )
+
+ section.ticket-detail-settings
+ tg-us-team-requirement-button(ng-model="epic")
+ tg-us-client-requirement-button(ng-model="epic")
+ tg-block-button(
+ tg-check-permission="modify_epic",
+ ng-model="epic"
+ )
+ tg-delete-button(
+ tg-check-permission="delete_epic",
+ on-delete-title="{{'EPIC.ACTION_DELETE' | translate}}",
+ on-delete-go-to-url="onDeleteGoToUrl",
+ ng-model="epic"
+ )
+
+ div.lightbox.lightbox-block(tg-lb-block, ng-model="epic", title="EPIC.LIGHTBOX_TITLE_BLOKING_EPIC")
+ div.lightbox.lightbox-select-user(tg-lb-assignedto)
+ div.lightbox.lightbox-select-user(tg-lb-watchers)
diff --git a/app/styles/modules/common/wizard.scss b/app/styles/modules/common/wizard.scss
index f0482026..5bcae75f 100644
--- a/app/styles/modules/common/wizard.scss
+++ b/app/styles/modules/common/wizard.scss
@@ -57,7 +57,6 @@
.icon {
@include svg-size(1.5rem);
fill: currentColor;
- margin-right: 1rem;
vertical-align: text-top;
}
.template-name {
diff --git a/conf.e2e.js b/conf.e2e.js
index d7ca0a01..cd421c09 100644
--- a/conf.e2e.js
+++ b/conf.e2e.js
@@ -53,55 +53,55 @@ var config = {
onPrepare: function() {
// disable by default because performance problems on IE
// track mouse movements
- // var trackMouse = function() {
- // angular.module('trackMouse', []).run(function($document) {
+ var trackMouse = function() {
+ angular.module('trackMouse', []).run(function($document) {
- // function addDot(ev) {
- // var color = 'black',
- // size = 6;
+ function addDot(ev) {
+ var color = 'black',
+ size = 6;
- // switch (ev.type) {
- // case 'click':
- // color = 'red';
- // break;
- // case 'dblclick':
- // color = 'blue';
- // break;
- // case 'mousemove':
- // color = 'green';
- // break;
- // }
+ switch (ev.type) {
+ case 'click':
+ color = 'red';
+ break;
+ case 'dblclick':
+ color = 'blue';
+ break;
+ case 'mousemove':
+ color = 'green';
+ break;
+ }
- // var dotEl = $('')
- // .css({
- // position: 'fixed',
- // height: size + 'px',
- // width: size + 'px',
- // 'background-color': color,
- // top: ev.clientY,
- // left: ev.clientX,
+ var dotEl = $('')
+ .css({
+ position: 'fixed',
+ height: size + 'px',
+ width: size + 'px',
+ 'background-color': color,
+ top: ev.clientY,
+ left: ev.clientX,
- // 'z-index': 9999,
+ 'z-index': 9999,
- // // make sure this dot won't interfere with the mouse events of other elements
- // 'pointer-events': 'none'
- // })
- // .appendTo('body');
+ // make sure this dot won't interfere with the mouse events of other elements
+ 'pointer-events': 'none'
+ })
+ .appendTo('body');
- // setTimeout(function() {
- // dotEl.remove();
- // }, 1000);
- // }
+ setTimeout(function() {
+ dotEl.remove();
+ }, 1000);
+ }
- // $document.on({
- // click: addDot,
- // dblclick: addDot,
- // mousemove: addDot
- // });
+ $document.on({
+ click: addDot,
+ dblclick: addDot,
+ mousemove: addDot
+ });
- // });
- // };
- // browser.addMockModule('trackMouse', trackMouse);
+ });
+ };
+ browser.addMockModule('trackMouse', trackMouse);
browser.params.glob.back = argv.back;
diff --git a/e2e/helpers/detail-helper.js b/e2e/helpers/detail-helper.js
index 78daae62..a315edbb 100644
--- a/e2e/helpers/detail-helper.js
+++ b/e2e/helpers/detail-helper.js
@@ -86,7 +86,7 @@ helper.tags = function() {
for (let tag of tags){
htmlChanges = await utils.common.outerHtmlChanges(el.$(".tags-container"));
el.$('.e2e-add-tag-input').sendKeys(tag);
- await browser.actions().sendKeys(protractor.Key.ENTER).perform();
+ el.$('.save').click();
await htmlChanges();
}
}
@@ -542,3 +542,43 @@ helper.watchersLightbox = function() {
return obj;
};
+
+helper.teamRequirement = function() {
+ let el = $('tg-us-team-requirement-button');
+
+ let obj = {
+ el: el,
+
+ toggleStatus: async function(){
+ await el.$("label").click();
+ await browser.waitForAngular();
+ },
+
+ isRequired: async function() {
+ let classes = await el.$("label").getAttribute('class');
+ return classes.includes("active");
+ }
+ };
+
+ return obj;
+};
+
+helper.clientRequirement = function() {
+ let el = $('tg-us-client-requirement-button');
+
+ let obj = {
+ el: el,
+
+ toggleStatus: async function(){
+ await el.$("label").click();
+ await browser.waitForAngular();
+ },
+
+ isRequired: async function() {
+ let classes = await el.$("label").getAttribute('class');
+ return classes.includes("active");
+ }
+ };
+
+ return obj;
+};
diff --git a/e2e/helpers/epic-detail-helper.js b/e2e/helpers/epic-detail-helper.js
new file mode 100644
index 00000000..84368661
--- /dev/null
+++ b/e2e/helpers/epic-detail-helper.js
@@ -0,0 +1,76 @@
+var utils = require('../utils');
+var commonHelper = require('./common-helper');
+
+var helper = module.exports;
+
+
+helper.colorEditor = function() {
+ let el = $('tg-color-selector');
+
+ let obj = {
+ el: el,
+
+ open: async function(){
+ await el.$(".e2e-open-color-selector").click();
+ },
+
+ selectFirstColor: async function() {
+ let color = el.$$(".color-selector-option").first();
+ color.click();
+ await browser.waitForAngular();
+ },
+
+ selectLastColor: async function() {
+ let color = el.$$(".color-selector-option").last();
+ color.click();
+ await browser.waitForAngular();
+ }
+ };
+
+ return obj;
+};
+
+helper.relatedUserstories = function() {
+ let el = $('tg-related-userstories');
+
+ let obj = {
+ el: el,
+
+ createNewUserStory: async function(subject) {
+ el.$(".e2e-add-userstory-button").click();
+ el.$(".e2e-new-userstory-label").click();
+ el.$(".e2e-single-creation-label").click();
+ el.$(".e2e-new-userstory-input-text").sendKeys(subject);
+ el.$(".e2e-create-userstory-button").click();
+ await browser.waitForAngular();
+ },
+
+ createNewUserStories: async function(subject) {
+ el.$(".e2e-add-userstory-button").click();
+ el.$(".e2e-new-userstory-label").click();
+ el.$(".e2e-bulk-creation-label").click();
+ el.$(".e2e-new-userstories-input-textarea").sendKeys(subject);
+ el.$(".e2e-create-userstory-button").click();
+ await browser.waitForAngular();
+ },
+
+ selectFirstRelatedUserstory: async function() {
+ el.$(".e2e-add-userstory-button").click();
+ el.$(".e2e-existing-user-story-label").click();
+ el.$(".e2e-filter-userstories-input").click().sendKeys("#1");
+ await browser.waitForAngular();
+ el.$$(".e2e-userstories-select option").get(1).click()
+ el.$(".e2e-select-related-userstory-button").click();
+ await browser.waitForAngular();
+ },
+
+ deleteFirstRelatedUserstory: async function() {
+ let relatedUSRow = el.$$("tg-related-userstory-row").first();
+ browser.actions().mouseMove(relatedUSRow).perform();
+ relatedUSRow.$(".e2e-delete-userstory").click();
+ await utils.lightbox.confirm.ok();
+ }
+ };
+
+ return obj;
+}
diff --git a/e2e/helpers/epics-helper.js b/e2e/helpers/epics-dashboard-helper.js
similarity index 94%
rename from e2e/helpers/epics-helper.js
rename to e2e/helpers/epics-dashboard-helper.js
index 305f7a27..787acd1c 100644
--- a/e2e/helpers/epics-helper.js
+++ b/e2e/helpers/epics-dashboard-helper.js
@@ -44,33 +44,38 @@ helper.epic = function() {
resetAssignedTo: async function() {
el.get(0).$('.e2e-assigned-to-image').click();
$$('.e2e-assigned-to-selector').get(0).click();
+ await browser.waitForAngular();
},
editAssignedTo: async function() {
el.get(0).$('.e2e-assigned-to-image').click();
utils.common.takeScreenshot("epics", "epics-edit-assigned");
$$('.e2e-assigned-to-selector').last().click();
+ await browser.waitForAngular();
},
removeAssignedTo: async function() {
el.get(0).$('.e2e-assigned-to-image').click();
- $$('.e2e-unassign').click();
+ $('.e2e-unassign').click();
+ await browser.waitForAngular();
return el.get(0).$('.e2e-assigned-to-image').getAttribute("alt");
},
- resetStatus: function() {
+ resetStatus: async function() {
el.get(0).$('.e2e-epic-status').click();
el.get(0).$$('.e2e-edit-epic-status').get(0).click();
+ await browser.waitForAngular();
},
getStatus: function() {
return el.get(0).$('.e2e-epic-status').getText();
},
- editStatus: function() {
+ editStatus: async function() {
el.get(0).$('.e2e-epic-status').click();
utils.common.takeScreenshot("epics", "epics-edit-status");
el.get(0).$$('.e2e-edit-epic-status').last().click();
+ await browser.waitForAngular();
},
getColumns: function() {
return $$('.e2e-epics-table-header > div').count();
},
- removeColumns: function() {
+ removeColumns: async function() {
$('.e2e-epics-column-button').click();
utils.common.takeScreenshot("epics", "epics-edit-columns");
$$('.e2e-epics-column-dropdown .check').first().click();
diff --git a/e2e/helpers/index.js b/e2e/helpers/index.js
index bc497ffc..307b9daf 100644
--- a/e2e/helpers/index.js
+++ b/e2e/helpers/index.js
@@ -13,3 +13,5 @@ module.exports.adminPermissions = require("./admin-permissions");
module.exports.adminIntegrations = require("./admin-integrations");
module.exports.issues = require("./issues-helper");
module.exports.createProject = require("./create-project-helper");
+module.exports.epicsDashboard = require("./epics-dashboard-helper");
+module.exports.epicDetail = require("./epic-detail-helper");
diff --git a/e2e/helpers/us-detail-helper.js b/e2e/helpers/us-detail-helper.js
index c41f53af..b419d433 100644
--- a/e2e/helpers/us-detail-helper.js
+++ b/e2e/helpers/us-detail-helper.js
@@ -3,45 +3,6 @@ var commonHelper = require('./common-helper');
var helper = module.exports;
-helper.teamRequirement = function() {
- let el = $('tg-us-team-requirement-button');
-
- let obj = {
- el: el,
-
- toggleStatus: async function(){
- await el.$("label").click();
- await browser.waitForAngular();
- },
-
- isRequired: async function() {
- let classes = await el.$("label").getAttribute('class');
- return classes.includes("active");
- }
- };
-
- return obj;
-};
-
-helper.clientRequirement = function() {
- let el = $('tg-us-client-requirement-button');
-
- let obj = {
- el: el,
-
- toggleStatus: async function(){
- await el.$("label").click();
- await browser.waitForAngular();
- },
-
- isRequired: async function() {
- let classes = await el.$("label").getAttribute('class');
- return classes.includes("active");
- }
- };
-
- return obj;
-};
helper.relatedTaskForm = async function(form, name, status, assigned_to) {
await form.$('input').sendKeys(name);
diff --git a/e2e/shared/detail.js b/e2e/shared/detail.js
index 3024a595..f8694cbd 100644
--- a/e2e/shared/detail.js
+++ b/e2e/shared/detail.js
@@ -274,12 +274,12 @@ shared.blockTesting = async function() {
let descriptionText = await $('.block-description').getText();
expect(descriptionText).to.be.equal('This is a testing block reason');
- let isDisplayed = $('.block-description').isDisplayed();
+ let isDisplayed = $('.block-desc-container').isDisplayed();
expect(isDisplayed).to.be.equal.true;
blockHelper.unblock();
- isDisplayed = $('.block-description').isDisplayed();
+ isDisplayed = $('.block-desc-container').isDisplayed();
expect(isDisplayed).to.be.equal.false;
await notifications.success.close();
@@ -548,3 +548,37 @@ shared.customFields = function(typeIndex) {
expect(fieldText).to.be.equal('test text2 edit');
});
};
+
+shared.teamRequirementTesting = function() {
+ it('team requirement edition', async function() {
+ let requirementHelper = detailHelper.teamRequirement();
+ let isRequired = await requirementHelper.isRequired();
+
+ // Toggle
+ requirementHelper.toggleStatus();
+ let newIsRequired = await requirementHelper.isRequired();
+ expect(isRequired).to.be.not.equal(newIsRequired);
+
+ // Toggle again
+ requirementHelper.toggleStatus();
+ newIsRequired = await requirementHelper.isRequired();
+ expect(isRequired).to.be.equal(newIsRequired);
+ });
+}
+
+shared.clientRequirementTesting = function () {
+ it('client requirement edition', async function() {
+ let requirementHelper = detailHelper.clientRequirement();
+ let isRequired = await requirementHelper.isRequired();
+
+ // Toggle
+ requirementHelper.toggleStatus();
+ let newIsRequired = await requirementHelper.isRequired();
+ expect(isRequired).to.be.not.equal(newIsRequired);
+
+ // Toggle again
+ requirementHelper.toggleStatus();
+ newIsRequired = await requirementHelper.isRequired();
+ expect(isRequired).to.be.equal(newIsRequired);
+ });
+}
diff --git a/e2e/suites/admin/attributes/custom-fields.e2e.js b/e2e/suites/admin/attributes/custom-fields.e2e.js
index 0a61db8c..aef42ac7 100644
--- a/e2e/suites/admin/attributes/custom-fields.e2e.js
+++ b/e2e/suites/admin/attributes/custom-fields.e2e.js
@@ -16,8 +16,64 @@ describe('custom-fields', function() {
});
describe('create custom fields', function() {
+ describe('epics', function() {
+ let typeIndex = 0;
+
+ it('create', async function() {
+ let oldCountCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count();
+
+ await customFieldsHelper.create(typeIndex, 'test1-text', 'desc1', 1);
+
+ // debounce :(
+ await utils.notifications.success.open();
+ await browser.sleep(2000);
+
+ await customFieldsHelper.create(typeIndex, 'test1-multi', 'desc1', 3);
+
+ // debounce :(
+ await utils.notifications.success.open();
+ await browser.sleep(2000);
+
+ let countCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count();
+
+ expect(countCustomFields).to.be.equal(oldCountCustomFields + 2);
+ });
+
+ it('edit', async function() {
+ customFieldsHelper.edit(typeIndex, 0, 'edit', 'desc2', 2);
+
+ let open = await utils.notifications.success.open();
+
+ expect(open).to.be.true;
+
+ await utils.notifications.success.close();
+ });
+
+ it('drag', async function() {
+ let nameOld = await customFieldsHelper.getName(typeIndex, 0);
+
+ await customFieldsHelper.drag(typeIndex, 0, 1);
+
+ let nameNew = await customFieldsHelper.getName(typeIndex, 1);
+
+ expect(nameNew).to.be.equal(nameOld);
+ });
+
+ it('delete', async function() {
+ let oldCountCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count();
+
+ await customFieldsHelper.delete(typeIndex, 0);
+
+ await browser.wait(async function() {
+ let countCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count();
+
+ return countCustomFields === oldCountCustomFields - 1;
+ }, 4000);
+ });
+ });
+
describe('userstories', function() {
- let typeIndex = 0;
+ let typeIndex = 1;
it('create', async function() {
let oldCountCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count();
@@ -73,7 +129,7 @@ describe('custom-fields', function() {
});
describe('tasks', function() {
- let typeIndex = 1;
+ let typeIndex = 2;
it('create', async function() {
let oldCountCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count();
@@ -126,7 +182,7 @@ describe('custom-fields', function() {
});
describe('issues', function() {
- let typeIndex = 2;
+ let typeIndex = 3;
it('create', async function() {
let oldCountCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count();
@@ -180,5 +236,6 @@ describe('custom-fields', function() {
}, 4000);
});
});
+
});
});
diff --git a/e2e/suites/admin/members.e2e.js b/e2e/suites/admin/members.e2e.js
index 2cbf6904..5178b139 100644
--- a/e2e/suites/admin/members.e2e.js
+++ b/e2e/suites/admin/members.e2e.js
@@ -8,7 +8,7 @@ var chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised);
var expect = chai.expect;
-describe.only('admin - members', function() {
+describe('admin - members', function() {
before(async function(){
browser.get(browser.params.glob.host + 'project/project-0/admin/memberships');
diff --git a/e2e/suites/epics/epic-dashboard.e2e.js b/e2e/suites/epics/epic-dashboard.e2e.js
index 370cc6af..9eb5d606 100644
--- a/e2e/suites/epics/epic-dashboard.e2e.js
+++ b/e2e/suites/epics/epic-dashboard.e2e.js
@@ -1,5 +1,5 @@
var utils = require('../../utils');
-var epicsHelper = require('../../helpers/epics-helper');
+var epicsDashboardHelper = require('../../helpers').epicsDashboard;
var chai = require('chai');
var chaiAsPromised = require('chai-as-promised');
@@ -8,7 +8,7 @@ chai.use(chaiAsPromised);
var expect = chai.expect;
describe('Epics Dashboard', function(){
- let usUrl = '';
+ let epicsUrl = '';
before(async function(){
await utils.nav
@@ -17,7 +17,7 @@ describe('Epics Dashboard', function(){
.epics()
.go();
- usUrl = await browser.getCurrentUrl();
+ epicsUrl = await browser.getCurrentUrl();
});
it('screenshot', async function() {
@@ -25,13 +25,23 @@ describe('Epics Dashboard', function(){
});
it('display child stories', async function() {
- let epic = epicsHelper.epic();
+ let epic = epicsDashboardHelper.epic();
let childStoriesNum = await epic.displayUserStoriesinEpic();
expect(childStoriesNum).to.be.above(0);
});
+ it('create Epic', async function() {
+ let date = Date.now();
+ let description = Math.random().toString(36).substring(7);
+ let epic = epicsDashboardHelper.epic();
+ let currentEpicsNum = await epic.getEpics();
+ await epic.createEpic(date, description);
+ let newEpicsNum = await epic.getEpics();
+ expect(newEpicsNum).to.be.above(currentEpicsNum);
+ });
+
it('change epic assigned from dashboard', async function() {
- let epic = epicsHelper.epic();
+ let epic = epicsDashboardHelper.epic();
await epic.resetAssignedTo();
let currentAssigned = await epic.getAssignedTo();
await epic.editAssignedTo();
@@ -40,15 +50,14 @@ describe('Epics Dashboard', function(){
});
it('remove assigned from dashboard', async function() {
- let epic = epicsHelper.epic();
+ let epic = epicsDashboardHelper.epic();
await epic.resetAssignedTo();
let unAssigned = await epic.removeAssignedTo();
- console.log(unAssigned);
expect(unAssigned).to.be.equal('Unassigned');
});
it('change status from dashboard', async function() {
- let epic = epicsHelper.epic();
+ let epic = epicsDashboardHelper.epic();
await epic.resetStatus();
let currentStatus = await epic.getStatus();
await epic.editStatus();
@@ -57,22 +66,11 @@ describe('Epics Dashboard', function(){
});
it('remove columns from dashboard', async function() {
- let epic = epicsHelper.epic();
+ let epic = epicsDashboardHelper.epic();
let currentColumns = await epic.getColumns();
await epic.removeColumns();
let newColumns = await epic.getColumns();
expect(currentColumns).to.be.above(newColumns);
});
- it.only('create Epic', async function() {
- let date = Date.now();
- let description = Math.random().toString(36).substring(7);
- let epic = epicsHelper.epic();
- let currentEpicsNum = await epic.getEpics();
- await epic.createEpic(date, description);
- let newEpicsNum = await epic.getEpics();
- console.log(currentEpicsNum, newEpicsNum);
- expect(newEpicsNum).to.be.above(currentEpicsNum);
- });
-
})
diff --git a/e2e/suites/epics/epic-detail.e2e.js b/e2e/suites/epics/epic-detail.e2e.js
new file mode 100644
index 00000000..66c5e34a
--- /dev/null
+++ b/e2e/suites/epics/epic-detail.e2e.js
@@ -0,0 +1,100 @@
+var utils = require('../../utils');
+var sharedDetail = require('../../shared/detail');
+var epicDetailHelper = require('../../helpers').epicDetail;
+
+var chai = require('chai');
+var chaiAsPromised = require('chai-as-promised');
+
+chai.use(chaiAsPromised);
+var expect = chai.expect;
+
+describe('Epic detail', async function(){
+ let epicUrl = '';
+
+ before(async function(){
+ await utils.nav
+ .init()
+ .project('Project Example 0')
+ .epics()
+ .epic(0)
+ .go();
+
+ epicUrl = await browser.getCurrentUrl();
+ });
+
+ it('screenshot', async function() {
+ await utils.common.takeScreenshot("epics", "detail");
+ });
+
+ it('color edition', async function() {
+ let colorEditor = epicDetailHelper.colorEditor();
+ await colorEditor.open();
+ await colorEditor.selectFirstColor();
+ await colorEditor.open();
+ await colorEditor.selectLastColor();
+ await utils.common.takeScreenshot("epics", "detail color updated");
+ });
+
+ it('title edition', sharedDetail.titleTesting);
+
+ it('tags edition', sharedDetail.tagsTesting);
+
+ describe('description', sharedDetail.descriptionTesting);
+
+ describe('related userstories', function() {
+ let relatedUserstories = epicDetailHelper.relatedUserstories();
+ it('create new user story', async function(){
+ await relatedUserstories.createNewUserStory("Testing subject");
+ });
+
+ it('create new user stories in bulk', async function(){
+ await relatedUserstories.createNewUserStories("Testing subject1\nTesting subject 2");
+ });
+
+ it('add related userstory', async function(){
+ await relatedUserstories.selectFirstRelatedUserstory();
+ });
+
+ it('delete related userstory', async function(){
+ await relatedUserstories.deleteFirstRelatedUserstory();
+ })
+ });
+
+ it('status edition', sharedDetail.statusTesting.bind(this, 'Ready', 'In progress'));
+
+ describe('assigned to edition', sharedDetail.assignedToTesting);
+
+ describe('watchers edition', sharedDetail.watchersTesting);
+
+ it('history', sharedDetail.historyTesting.bind(this, "epics"));
+
+ it('block', sharedDetail.blockTesting);
+
+ describe('team requirement edition', sharedDetail.teamRequirementTesting);
+
+ describe('client requirement edition', sharedDetail.clientRequirementTesting);
+
+ it('attachments', sharedDetail.attachmentTesting);
+
+ describe('custom-fields', sharedDetail.customFields.bind(this, 0));
+
+ it('screenshot', async function() {
+ await utils.common.takeScreenshot("epics", "detail updated");
+ });
+
+ describe('delete & redirect', function() {
+ it('delete', sharedDetail.deleteTesting);
+
+ it('redirected', async function (){
+ let url = await browser.getCurrentUrl();
+ expect(url).not.to.be.equal(epicUrl);
+ });
+ });
+
+});
+
+
+/*
+TODO:
+# Related user stories
+*/
diff --git a/e2e/suites/user-stories/user-story-detail.e2e.js b/e2e/suites/user-stories/user-story-detail.e2e.js
index 9727efa6..bbc52509 100644
--- a/e2e/suites/user-stories/user-story-detail.e2e.js
+++ b/e2e/suites/user-stories/user-story-detail.e2e.js
@@ -36,35 +36,9 @@ describe('User story detail', function(){
describe('assigned to edition', sharedDetail.assignedToTesting);
- it('team requirement edition', async function() {
- let requirementHelper = usDetailHelper.teamRequirement();
- let isRequired = await requirementHelper.isRequired();
+ describe('team requirement edition', sharedDetail.teamRequirementTesting);
- // Toggle
- requirementHelper.toggleStatus();
- let newIsRequired = await requirementHelper.isRequired();
- expect(isRequired).to.be.not.equal(newIsRequired);
-
- // Toggle again
- requirementHelper.toggleStatus();
- newIsRequired = await requirementHelper.isRequired();
- expect(isRequired).to.be.equal(newIsRequired);
- });
-
- it('client requirement edition', async function() {
- let requirementHelper = usDetailHelper.clientRequirement();
- let isRequired = await requirementHelper.isRequired();
-
- // Toggle
- requirementHelper.toggleStatus();
- let newIsRequired = await requirementHelper.isRequired();
- expect(isRequired).to.be.not.equal(newIsRequired);
-
- // Toggle again
- requirementHelper.toggleStatus();
- newIsRequired = await requirementHelper.isRequired();
- expect(isRequired).to.be.equal(newIsRequired);
- });
+ describe('client requirement edition', sharedDetail.clientRequirementTesting);
describe('watchers edition', sharedDetail.watchersTesting);
diff --git a/e2e/utils/nav.js b/e2e/utils/nav.js
index 17710dbe..b3c32daa 100644
--- a/e2e/utils/nav.js
+++ b/e2e/utils/nav.js
@@ -46,11 +46,21 @@ var actions = {
return common.waitLoader();
},
+
epics: async function() {
await common.link($('#nav-epics a'));
return common.waitLoader();
},
+
+ epic: async function(index) {
+ let epic = $$('.e2e-epic-row .name a').get(index);
+
+ await common.link(epic);
+
+ return common.waitLoader();
+ },
+
backlog: async function() {
await common.link($$('#nav-backlog a').first());
@@ -110,6 +120,10 @@ var nav = {
this.actions.push(actions.epics.bind(null, index));
return this;
},
+ epic: function(index) {
+ this.actions.push(actions.epic.bind(null, index));
+ return this;
+ },
backlog: function(index) {
this.actions.push(actions.backlog.bind(null, index));
return this;
diff --git a/gulpfile.js b/gulpfile.js
index b69e5c92..3fbc02a5 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -130,6 +130,7 @@ paths.coffee_order = [
paths.app + "coffee/modules/backlog/*.coffee",
paths.app + "coffee/modules/taskboard/*.coffee",
paths.app + "coffee/modules/kanban/*.coffee",
+ paths.app + "coffee/modules/epics/*.coffee",
paths.app + "coffee/modules/issues/*.coffee",
paths.app + "coffee/modules/userstories/*.coffee",
paths.app + "coffee/modules/tasks/*.coffee",
diff --git a/run-e2e.js b/run-e2e.js
index 77d8e48b..56d78341 100644
--- a/run-e2e.js
+++ b/run-e2e.js
@@ -12,6 +12,7 @@ var suites = [
'wiki',
'admin',
'issues',
+ 'epics',
'tasks',
'userProfile',
'userStories',