Epic detail
parent
f8dd7408d2
commit
3d858cf82a
|
@ -46,7 +46,7 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
|
||||||
|
|
||||||
$animateProvider.classNameFilter(/^(?:(?!ng-animate-disabled).)*$/)
|
$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
|
originalWhen = $routeProvider.when
|
||||||
|
|
||||||
$routeProvider.when = (path, route) ->
|
$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",
|
$routeProvider.when("/project/:pslug/backlog",
|
||||||
{
|
{
|
||||||
templateUrl: "backlog/backlog.html",
|
templateUrl: "backlog/backlog.html",
|
||||||
|
@ -793,6 +802,7 @@ modules = [
|
||||||
"taigaPlugins",
|
"taigaPlugins",
|
||||||
"taigaIntegrations",
|
"taigaIntegrations",
|
||||||
"taigaComponents",
|
"taigaComponents",
|
||||||
|
|
||||||
# new modules
|
# new modules
|
||||||
"taigaProfile",
|
"taigaProfile",
|
||||||
"taigaHome",
|
"taigaHome",
|
||||||
|
@ -801,7 +811,7 @@ modules = [
|
||||||
"taigaDiscover",
|
"taigaDiscover",
|
||||||
"taigaHistory",
|
"taigaHistory",
|
||||||
"taigaWikiHistory",
|
"taigaWikiHistory",
|
||||||
'taigaEpics',
|
"taigaEpics",
|
||||||
|
|
||||||
# template cache
|
# template cache
|
||||||
"templates",
|
"templates",
|
||||||
|
|
|
@ -74,3 +74,29 @@ sizeFormat = =>
|
||||||
return @.taiga.sizeFormat
|
return @.taiga.sizeFormat
|
||||||
|
|
||||||
module.filter("sizeFormat", 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])
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
# File: modules/projects.coffee
|
# File: modules/epics.coffee
|
||||||
###
|
###
|
||||||
|
|
||||||
module = angular.module("taigaEpics", [])
|
module = angular.module("taigaEpics", [])
|
||||||
|
|
|
@ -0,0 +1,337 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
||||||
|
# Copyright (C) 2014-2016 Jesús Espino Garcia <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2016 David Barragán Merino <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||||
|
# Copyright (C) 2014-2016 Juan Francisco Alcántara <juanfran.alcantara@kaleidos.net>
|
||||||
|
# Copyright (C) 2014-2016 Xavi Julian <xavier.julian@kaleidos.net>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# 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])
|
|
@ -95,6 +95,12 @@ urls = {
|
||||||
|
|
||||||
# Epics
|
# Epics
|
||||||
"epics": "/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
|
# User stories
|
||||||
"userstories": "/userstories"
|
"userstories": "/userstories"
|
||||||
|
@ -134,12 +140,14 @@ urls = {
|
||||||
"wiki-links": "/wiki-links"
|
"wiki-links": "/wiki-links"
|
||||||
|
|
||||||
# History
|
# History
|
||||||
|
"history/epic": "/history/epic"
|
||||||
"history/us": "/history/userstory"
|
"history/us": "/history/userstory"
|
||||||
"history/issue": "/history/issue"
|
"history/issue": "/history/issue"
|
||||||
"history/task": "/history/task"
|
"history/task": "/history/task"
|
||||||
"history/wiki": "/history/wiki/%s"
|
"history/wiki": "/history/wiki/%s"
|
||||||
|
|
||||||
# Attachments
|
# Attachments
|
||||||
|
"attachments/epic": "/epics/attachments"
|
||||||
"attachments/us": "/userstories/attachments"
|
"attachments/us": "/userstories/attachments"
|
||||||
"attachments/issue": "/issues/attachments"
|
"attachments/issue": "/issues/attachments"
|
||||||
"attachments/task": "/tasks/attachments"
|
"attachments/task": "/tasks/attachments"
|
||||||
|
|
|
@ -28,10 +28,16 @@ taiga = @.taiga
|
||||||
generateHash = taiga.generateHash
|
generateHash = taiga.generateHash
|
||||||
|
|
||||||
|
|
||||||
resourceProvider = ($repo, $storage) ->
|
resourceProvider = ($repo, $http, $urls, $storage) ->
|
||||||
service = {}
|
service = {}
|
||||||
hashSuffix = "epics-queryparams"
|
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) ->
|
service.listValues = (projectId, type) ->
|
||||||
params = {"project": projectId}
|
params = {"project": projectId}
|
||||||
service.storeQueryParams(projectId, params)
|
service.storeQueryParams(projectId, params)
|
||||||
|
@ -47,9 +53,25 @@ resourceProvider = ($repo, $storage) ->
|
||||||
hash = generateHash([projectId, ns])
|
hash = generateHash([projectId, ns])
|
||||||
return $storage.get(hash) or {}
|
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) ->
|
return (instance) ->
|
||||||
instance.epics = service
|
instance.epics = service
|
||||||
|
|
||||||
|
|
||||||
module = angular.module("taigaResources")
|
module = angular.module("taigaResources")
|
||||||
module.factory("$tgEpicsResourcesProvider", ["$tgRepo", "$tgStorage", resourceProvider])
|
module.factory("$tgEpicsResourcesProvider", ["$tgRepo","$tgHttp", "$tgUrls", "$tgStorage", resourceProvider])
|
||||||
|
|
|
@ -47,6 +47,7 @@
|
||||||
"CAPSLOCK_WARNING": "Be careful! You are using capital letters in an input field that is case sensitive.",
|
"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_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",
|
"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": {
|
"CARD": {
|
||||||
"ASSIGN_TO": "Assign To",
|
"ASSIGN_TO": "Assign To",
|
||||||
"EDIT": "Edit card"
|
"EDIT": "Edit card"
|
||||||
|
@ -1061,6 +1062,26 @@
|
||||||
"BUTTON": "Ask this project member to become the new project owner"
|
"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": {
|
"US": {
|
||||||
"PAGE_TITLE": "{{userStorySubject}} - User Story {{userStoryRef}} - {{projectName}}",
|
"PAGE_TITLE": "{{userStorySubject}} - User Story {{userStoryRef}} - {{projectName}}",
|
||||||
"PAGE_DESCRIPTION": "Status: {{userStoryStatus }}. Completed {{userStoryProgressPercentage}}% ({{userStoryClosedTasks}} of {{userStoryTotalTasks}} tasks closed). Points: {{userStoryPoints}}. Description: {{userStoryDescription}}",
|
"PAGE_DESCRIPTION": "Status: {{userStoryStatus }}. Completed {{userStoryProgressPercentage}}% ({{userStoryClosedTasks}} of {{userStoryTotalTasks}} tasks closed). Points: {{userStoryPoints}}. Description: {{userStoryDescription}}",
|
||||||
|
|
|
@ -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(
|
a.belong-to-epic-text(
|
||||||
href=""
|
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')}}
|
) #{hash}{{epic.get('id')}} {{epic.get('subject')}}
|
||||||
|
|
|
@ -25,9 +25,6 @@ BelongToEpicsDirective = () ->
|
||||||
if scope.epics && !scope.epics.isIterable
|
if scope.epics && !scope.epics.isIterable
|
||||||
scope.epics = Immutable.fromJS(scope.epics)
|
scope.epics = Immutable.fromJS(scope.epics)
|
||||||
|
|
||||||
if scope.project && !scope.project.isIterable
|
|
||||||
scope.project = Immutable.fromJS(scope.project)
|
|
||||||
|
|
||||||
scope.getTemplateUrl = () ->
|
scope.getTemplateUrl = () ->
|
||||||
if attrs.format
|
if attrs.format
|
||||||
return "components/belong-to-epics/belong-to-epics-" + attrs.format + ".html"
|
return "components/belong-to-epics/belong-to-epics-" + attrs.format + ".html"
|
||||||
|
|
|
@ -41,7 +41,6 @@
|
||||||
ng-if="::vm.item.epics"
|
ng-if="::vm.item.epics"
|
||||||
epics="::vm.item.epics"
|
epics="::vm.item.epics"
|
||||||
format="text"
|
format="text"
|
||||||
project="project"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//- Task belongs to US
|
//- Task belongs to US
|
||||||
|
@ -60,7 +59,6 @@
|
||||||
ng-if="::vm.item.user_story_extra_info.epics"
|
ng-if="::vm.item.user_story_extra_info.epics"
|
||||||
epics="::vm.item.user_story_extra_info.epics"
|
epics="::vm.item.user_story_extra_info.epics"
|
||||||
format="pill"
|
format="pill"
|
||||||
project="vm.project"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//- User Stories generated from issue
|
//- User Stories generated from issue
|
||||||
|
|
|
@ -45,7 +45,8 @@ class WatchButtonController
|
||||||
perms = {
|
perms = {
|
||||||
userstories: 'modify_us',
|
userstories: 'modify_us',
|
||||||
issues: 'modify_issue',
|
issues: 'modify_issue',
|
||||||
tasks: 'modify_task'
|
tasks: 'modify_task',
|
||||||
|
epics: 'modify_epic'
|
||||||
}
|
}
|
||||||
|
|
||||||
return perms[name]
|
return perms[name]
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
.name(ng-if="vm.column.name")
|
.name(ng-if="vm.column.name")
|
||||||
- var hash = "#";
|
- var hash = "#";
|
||||||
a(
|
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')}}"
|
ng-attr-title="{{::vm.epic.get('subject')}}"
|
||||||
) #{hash}{{::vm.epic.get('ref')}} {{::vm.epic.get('subject')}}
|
) #{hash}{{::vm.epic.get('ref')}} {{::vm.epic.get('subject')}}
|
||||||
span.epic-pill(
|
span.epic-pill(
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# 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)
|
|
@ -0,0 +1,92 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# 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)
|
|
@ -0,0 +1,185 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# 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()
|
|
@ -0,0 +1,79 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# 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)
|
|
@ -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"
|
||||||
|
)
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# 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()
|
|
@ -0,0 +1,37 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# 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)
|
|
@ -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)
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# 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)
|
|
@ -0,0 +1,169 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# 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()
|
|
@ -0,0 +1,42 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# 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)
|
|
@ -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)
|
|
@ -51,6 +51,31 @@ Resource = (urlsService, http) ->
|
||||||
|
|
||||||
return http.post(url, params)
|
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 () ->
|
||||||
return {"epics": service}
|
return {"epics": service}
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,22 @@ Resource = (urlsService, http) ->
|
||||||
.then (result) ->
|
.then (result) ->
|
||||||
return Immutable.fromJS(result.data)
|
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) ->
|
service.listInEpic = (epicIid) ->
|
||||||
url = urlsService.resolve("userstories")
|
url = urlsService.resolve("userstories")
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
@ -57,7 +57,6 @@
|
||||||
.icon {
|
.icon {
|
||||||
@include svg-size(1.5rem);
|
@include svg-size(1.5rem);
|
||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
margin-right: 1rem;
|
|
||||||
vertical-align: text-top;
|
vertical-align: text-top;
|
||||||
}
|
}
|
||||||
.template-name {
|
.template-name {
|
||||||
|
|
82
conf.e2e.js
82
conf.e2e.js
|
@ -53,55 +53,55 @@ var config = {
|
||||||
onPrepare: function() {
|
onPrepare: function() {
|
||||||
// disable by default because performance problems on IE
|
// disable by default because performance problems on IE
|
||||||
// track mouse movements
|
// track mouse movements
|
||||||
// var trackMouse = function() {
|
var trackMouse = function() {
|
||||||
// angular.module('trackMouse', []).run(function($document) {
|
angular.module('trackMouse', []).run(function($document) {
|
||||||
|
|
||||||
// function addDot(ev) {
|
function addDot(ev) {
|
||||||
// var color = 'black',
|
var color = 'black',
|
||||||
// size = 6;
|
size = 6;
|
||||||
|
|
||||||
// switch (ev.type) {
|
switch (ev.type) {
|
||||||
// case 'click':
|
case 'click':
|
||||||
// color = 'red';
|
color = 'red';
|
||||||
// break;
|
break;
|
||||||
// case 'dblclick':
|
case 'dblclick':
|
||||||
// color = 'blue';
|
color = 'blue';
|
||||||
// break;
|
break;
|
||||||
// case 'mousemove':
|
case 'mousemove':
|
||||||
// color = 'green';
|
color = 'green';
|
||||||
// break;
|
break;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// var dotEl = $('<div></div>')
|
var dotEl = $('<div></div>')
|
||||||
// .css({
|
.css({
|
||||||
// position: 'fixed',
|
position: 'fixed',
|
||||||
// height: size + 'px',
|
height: size + 'px',
|
||||||
// width: size + 'px',
|
width: size + 'px',
|
||||||
// 'background-color': color,
|
'background-color': color,
|
||||||
// top: ev.clientY,
|
top: ev.clientY,
|
||||||
// left: ev.clientX,
|
left: ev.clientX,
|
||||||
|
|
||||||
// 'z-index': 9999,
|
'z-index': 9999,
|
||||||
|
|
||||||
// // make sure this dot won't interfere with the mouse events of other elements
|
// make sure this dot won't interfere with the mouse events of other elements
|
||||||
// 'pointer-events': 'none'
|
'pointer-events': 'none'
|
||||||
// })
|
})
|
||||||
// .appendTo('body');
|
.appendTo('body');
|
||||||
|
|
||||||
// setTimeout(function() {
|
setTimeout(function() {
|
||||||
// dotEl.remove();
|
dotEl.remove();
|
||||||
// }, 1000);
|
}, 1000);
|
||||||
// }
|
}
|
||||||
|
|
||||||
// $document.on({
|
$document.on({
|
||||||
// click: addDot,
|
click: addDot,
|
||||||
// dblclick: addDot,
|
dblclick: addDot,
|
||||||
// mousemove: addDot
|
mousemove: addDot
|
||||||
// });
|
});
|
||||||
|
|
||||||
// });
|
});
|
||||||
// };
|
};
|
||||||
// browser.addMockModule('trackMouse', trackMouse);
|
browser.addMockModule('trackMouse', trackMouse);
|
||||||
|
|
||||||
browser.params.glob.back = argv.back;
|
browser.params.glob.back = argv.back;
|
||||||
|
|
||||||
|
|
|
@ -86,7 +86,7 @@ helper.tags = function() {
|
||||||
for (let tag of tags){
|
for (let tag of tags){
|
||||||
htmlChanges = await utils.common.outerHtmlChanges(el.$(".tags-container"));
|
htmlChanges = await utils.common.outerHtmlChanges(el.$(".tags-container"));
|
||||||
el.$('.e2e-add-tag-input').sendKeys(tag);
|
el.$('.e2e-add-tag-input').sendKeys(tag);
|
||||||
await browser.actions().sendKeys(protractor.Key.ENTER).perform();
|
el.$('.save').click();
|
||||||
await htmlChanges();
|
await htmlChanges();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -542,3 +542,43 @@ helper.watchersLightbox = function() {
|
||||||
|
|
||||||
return obj;
|
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;
|
||||||
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -44,33 +44,38 @@ helper.epic = function() {
|
||||||
resetAssignedTo: async function() {
|
resetAssignedTo: async function() {
|
||||||
el.get(0).$('.e2e-assigned-to-image').click();
|
el.get(0).$('.e2e-assigned-to-image').click();
|
||||||
$$('.e2e-assigned-to-selector').get(0).click();
|
$$('.e2e-assigned-to-selector').get(0).click();
|
||||||
|
await browser.waitForAngular();
|
||||||
},
|
},
|
||||||
editAssignedTo: async function() {
|
editAssignedTo: async function() {
|
||||||
el.get(0).$('.e2e-assigned-to-image').click();
|
el.get(0).$('.e2e-assigned-to-image').click();
|
||||||
utils.common.takeScreenshot("epics", "epics-edit-assigned");
|
utils.common.takeScreenshot("epics", "epics-edit-assigned");
|
||||||
$$('.e2e-assigned-to-selector').last().click();
|
$$('.e2e-assigned-to-selector').last().click();
|
||||||
|
await browser.waitForAngular();
|
||||||
},
|
},
|
||||||
removeAssignedTo: async function() {
|
removeAssignedTo: async function() {
|
||||||
el.get(0).$('.e2e-assigned-to-image').click();
|
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");
|
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-epic-status').click();
|
||||||
el.get(0).$$('.e2e-edit-epic-status').get(0).click();
|
el.get(0).$$('.e2e-edit-epic-status').get(0).click();
|
||||||
|
await browser.waitForAngular();
|
||||||
},
|
},
|
||||||
getStatus: function() {
|
getStatus: function() {
|
||||||
return el.get(0).$('.e2e-epic-status').getText();
|
return el.get(0).$('.e2e-epic-status').getText();
|
||||||
},
|
},
|
||||||
editStatus: function() {
|
editStatus: async function() {
|
||||||
el.get(0).$('.e2e-epic-status').click();
|
el.get(0).$('.e2e-epic-status').click();
|
||||||
utils.common.takeScreenshot("epics", "epics-edit-status");
|
utils.common.takeScreenshot("epics", "epics-edit-status");
|
||||||
el.get(0).$$('.e2e-edit-epic-status').last().click();
|
el.get(0).$$('.e2e-edit-epic-status').last().click();
|
||||||
|
await browser.waitForAngular();
|
||||||
},
|
},
|
||||||
getColumns: function() {
|
getColumns: function() {
|
||||||
return $$('.e2e-epics-table-header > div').count();
|
return $$('.e2e-epics-table-header > div').count();
|
||||||
},
|
},
|
||||||
removeColumns: function() {
|
removeColumns: async function() {
|
||||||
$('.e2e-epics-column-button').click();
|
$('.e2e-epics-column-button').click();
|
||||||
utils.common.takeScreenshot("epics", "epics-edit-columns");
|
utils.common.takeScreenshot("epics", "epics-edit-columns");
|
||||||
$$('.e2e-epics-column-dropdown .check').first().click();
|
$$('.e2e-epics-column-dropdown .check').first().click();
|
|
@ -13,3 +13,5 @@ module.exports.adminPermissions = require("./admin-permissions");
|
||||||
module.exports.adminIntegrations = require("./admin-integrations");
|
module.exports.adminIntegrations = require("./admin-integrations");
|
||||||
module.exports.issues = require("./issues-helper");
|
module.exports.issues = require("./issues-helper");
|
||||||
module.exports.createProject = require("./create-project-helper");
|
module.exports.createProject = require("./create-project-helper");
|
||||||
|
module.exports.epicsDashboard = require("./epics-dashboard-helper");
|
||||||
|
module.exports.epicDetail = require("./epic-detail-helper");
|
||||||
|
|
|
@ -3,45 +3,6 @@ var commonHelper = require('./common-helper');
|
||||||
|
|
||||||
var helper = module.exports;
|
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) {
|
helper.relatedTaskForm = async function(form, name, status, assigned_to) {
|
||||||
await form.$('input').sendKeys(name);
|
await form.$('input').sendKeys(name);
|
||||||
|
|
|
@ -274,12 +274,12 @@ shared.blockTesting = async function() {
|
||||||
let descriptionText = await $('.block-description').getText();
|
let descriptionText = await $('.block-description').getText();
|
||||||
expect(descriptionText).to.be.equal('This is a testing block reason');
|
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;
|
expect(isDisplayed).to.be.equal.true;
|
||||||
|
|
||||||
blockHelper.unblock();
|
blockHelper.unblock();
|
||||||
|
|
||||||
isDisplayed = $('.block-description').isDisplayed();
|
isDisplayed = $('.block-desc-container').isDisplayed();
|
||||||
expect(isDisplayed).to.be.equal.false;
|
expect(isDisplayed).to.be.equal.false;
|
||||||
|
|
||||||
await notifications.success.close();
|
await notifications.success.close();
|
||||||
|
@ -548,3 +548,37 @@ shared.customFields = function(typeIndex) {
|
||||||
expect(fieldText).to.be.equal('test text2 edit');
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -16,8 +16,64 @@ describe('custom-fields', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('create 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() {
|
describe('userstories', function() {
|
||||||
let typeIndex = 0;
|
let typeIndex = 1;
|
||||||
|
|
||||||
it('create', async function() {
|
it('create', async function() {
|
||||||
let oldCountCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count();
|
let oldCountCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count();
|
||||||
|
@ -73,7 +129,7 @@ describe('custom-fields', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('tasks', function() {
|
describe('tasks', function() {
|
||||||
let typeIndex = 1;
|
let typeIndex = 2;
|
||||||
|
|
||||||
it('create', async function() {
|
it('create', async function() {
|
||||||
let oldCountCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count();
|
let oldCountCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count();
|
||||||
|
@ -126,7 +182,7 @@ describe('custom-fields', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('issues', function() {
|
describe('issues', function() {
|
||||||
let typeIndex = 2;
|
let typeIndex = 3;
|
||||||
|
|
||||||
it('create', async function() {
|
it('create', async function() {
|
||||||
let oldCountCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count();
|
let oldCountCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count();
|
||||||
|
@ -180,5 +236,6 @@ describe('custom-fields', function() {
|
||||||
}, 4000);
|
}, 4000);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,7 +8,7 @@ var chaiAsPromised = require('chai-as-promised');
|
||||||
chai.use(chaiAsPromised);
|
chai.use(chaiAsPromised);
|
||||||
var expect = chai.expect;
|
var expect = chai.expect;
|
||||||
|
|
||||||
describe.only('admin - members', function() {
|
describe('admin - members', function() {
|
||||||
before(async function(){
|
before(async function(){
|
||||||
browser.get(browser.params.glob.host + 'project/project-0/admin/memberships');
|
browser.get(browser.params.glob.host + 'project/project-0/admin/memberships');
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
var utils = require('../../utils');
|
var utils = require('../../utils');
|
||||||
var epicsHelper = require('../../helpers/epics-helper');
|
var epicsDashboardHelper = require('../../helpers').epicsDashboard;
|
||||||
|
|
||||||
var chai = require('chai');
|
var chai = require('chai');
|
||||||
var chaiAsPromised = require('chai-as-promised');
|
var chaiAsPromised = require('chai-as-promised');
|
||||||
|
@ -8,7 +8,7 @@ chai.use(chaiAsPromised);
|
||||||
var expect = chai.expect;
|
var expect = chai.expect;
|
||||||
|
|
||||||
describe('Epics Dashboard', function(){
|
describe('Epics Dashboard', function(){
|
||||||
let usUrl = '';
|
let epicsUrl = '';
|
||||||
|
|
||||||
before(async function(){
|
before(async function(){
|
||||||
await utils.nav
|
await utils.nav
|
||||||
|
@ -17,7 +17,7 @@ describe('Epics Dashboard', function(){
|
||||||
.epics()
|
.epics()
|
||||||
.go();
|
.go();
|
||||||
|
|
||||||
usUrl = await browser.getCurrentUrl();
|
epicsUrl = await browser.getCurrentUrl();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('screenshot', async function() {
|
it('screenshot', async function() {
|
||||||
|
@ -25,13 +25,23 @@ describe('Epics Dashboard', function(){
|
||||||
});
|
});
|
||||||
|
|
||||||
it('display child stories', async function() {
|
it('display child stories', async function() {
|
||||||
let epic = epicsHelper.epic();
|
let epic = epicsDashboardHelper.epic();
|
||||||
let childStoriesNum = await epic.displayUserStoriesinEpic();
|
let childStoriesNum = await epic.displayUserStoriesinEpic();
|
||||||
expect(childStoriesNum).to.be.above(0);
|
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() {
|
it('change epic assigned from dashboard', async function() {
|
||||||
let epic = epicsHelper.epic();
|
let epic = epicsDashboardHelper.epic();
|
||||||
await epic.resetAssignedTo();
|
await epic.resetAssignedTo();
|
||||||
let currentAssigned = await epic.getAssignedTo();
|
let currentAssigned = await epic.getAssignedTo();
|
||||||
await epic.editAssignedTo();
|
await epic.editAssignedTo();
|
||||||
|
@ -40,15 +50,14 @@ describe('Epics Dashboard', function(){
|
||||||
});
|
});
|
||||||
|
|
||||||
it('remove assigned from dashboard', async function() {
|
it('remove assigned from dashboard', async function() {
|
||||||
let epic = epicsHelper.epic();
|
let epic = epicsDashboardHelper.epic();
|
||||||
await epic.resetAssignedTo();
|
await epic.resetAssignedTo();
|
||||||
let unAssigned = await epic.removeAssignedTo();
|
let unAssigned = await epic.removeAssignedTo();
|
||||||
console.log(unAssigned);
|
|
||||||
expect(unAssigned).to.be.equal('Unassigned');
|
expect(unAssigned).to.be.equal('Unassigned');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('change status from dashboard', async function() {
|
it('change status from dashboard', async function() {
|
||||||
let epic = epicsHelper.epic();
|
let epic = epicsDashboardHelper.epic();
|
||||||
await epic.resetStatus();
|
await epic.resetStatus();
|
||||||
let currentStatus = await epic.getStatus();
|
let currentStatus = await epic.getStatus();
|
||||||
await epic.editStatus();
|
await epic.editStatus();
|
||||||
|
@ -57,22 +66,11 @@ describe('Epics Dashboard', function(){
|
||||||
});
|
});
|
||||||
|
|
||||||
it('remove columns from dashboard', async function() {
|
it('remove columns from dashboard', async function() {
|
||||||
let epic = epicsHelper.epic();
|
let epic = epicsDashboardHelper.epic();
|
||||||
let currentColumns = await epic.getColumns();
|
let currentColumns = await epic.getColumns();
|
||||||
await epic.removeColumns();
|
await epic.removeColumns();
|
||||||
let newColumns = await epic.getColumns();
|
let newColumns = await epic.getColumns();
|
||||||
expect(currentColumns).to.be.above(newColumns);
|
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
|
*/
|
|
@ -36,35 +36,9 @@ describe('User story detail', function(){
|
||||||
|
|
||||||
describe('assigned to edition', sharedDetail.assignedToTesting);
|
describe('assigned to edition', sharedDetail.assignedToTesting);
|
||||||
|
|
||||||
it('team requirement edition', async function() {
|
describe('team requirement edition', sharedDetail.teamRequirementTesting);
|
||||||
let requirementHelper = usDetailHelper.teamRequirement();
|
|
||||||
let isRequired = await requirementHelper.isRequired();
|
|
||||||
|
|
||||||
// Toggle
|
describe('client requirement edition', sharedDetail.clientRequirementTesting);
|
||||||
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('watchers edition', sharedDetail.watchersTesting);
|
describe('watchers edition', sharedDetail.watchersTesting);
|
||||||
|
|
||||||
|
|
|
@ -46,11 +46,21 @@ var actions = {
|
||||||
|
|
||||||
return common.waitLoader();
|
return common.waitLoader();
|
||||||
},
|
},
|
||||||
|
|
||||||
epics: async function() {
|
epics: async function() {
|
||||||
await common.link($('#nav-epics a'));
|
await common.link($('#nav-epics a'));
|
||||||
|
|
||||||
return common.waitLoader();
|
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() {
|
backlog: async function() {
|
||||||
await common.link($$('#nav-backlog a').first());
|
await common.link($$('#nav-backlog a').first());
|
||||||
|
|
||||||
|
@ -110,6 +120,10 @@ var nav = {
|
||||||
this.actions.push(actions.epics.bind(null, index));
|
this.actions.push(actions.epics.bind(null, index));
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
|
epic: function(index) {
|
||||||
|
this.actions.push(actions.epic.bind(null, index));
|
||||||
|
return this;
|
||||||
|
},
|
||||||
backlog: function(index) {
|
backlog: function(index) {
|
||||||
this.actions.push(actions.backlog.bind(null, index));
|
this.actions.push(actions.backlog.bind(null, index));
|
||||||
return this;
|
return this;
|
||||||
|
|
|
@ -130,6 +130,7 @@ paths.coffee_order = [
|
||||||
paths.app + "coffee/modules/backlog/*.coffee",
|
paths.app + "coffee/modules/backlog/*.coffee",
|
||||||
paths.app + "coffee/modules/taskboard/*.coffee",
|
paths.app + "coffee/modules/taskboard/*.coffee",
|
||||||
paths.app + "coffee/modules/kanban/*.coffee",
|
paths.app + "coffee/modules/kanban/*.coffee",
|
||||||
|
paths.app + "coffee/modules/epics/*.coffee",
|
||||||
paths.app + "coffee/modules/issues/*.coffee",
|
paths.app + "coffee/modules/issues/*.coffee",
|
||||||
paths.app + "coffee/modules/userstories/*.coffee",
|
paths.app + "coffee/modules/userstories/*.coffee",
|
||||||
paths.app + "coffee/modules/tasks/*.coffee",
|
paths.app + "coffee/modules/tasks/*.coffee",
|
||||||
|
|
|
@ -12,6 +12,7 @@ var suites = [
|
||||||
'wiki',
|
'wiki',
|
||||||
'admin',
|
'admin',
|
||||||
'issues',
|
'issues',
|
||||||
|
'epics',
|
||||||
'tasks',
|
'tasks',
|
||||||
'userProfile',
|
'userProfile',
|
||||||
'userStories',
|
'userStories',
|
||||||
|
|
Loading…
Reference in New Issue