Epic detail

stable
Alejandro Alonso 2016-08-26 12:11:12 +02:00 committed by David Barragán Merino
parent f8dd7408d2
commit 3d858cf82a
45 changed files with 2214 additions and 153 deletions

View File

@ -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",

View File

@ -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])

View File

@ -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", [])

View File

@ -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])

View File

@ -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"

View File

@ -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])

View File

@ -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}}",

View File

@ -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')}}

View File

@ -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"

View File

@ -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

View File

@ -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]

View File

@ -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(

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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)

View File

@ -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"
)

View File

@ -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;
}
}
}

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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;
}
}
}

View File

@ -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)

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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}

View File

@ -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")

View File

@ -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)

View File

@ -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 {

View File

@ -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;

View File

@ -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;
};

View File

@ -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;
}

View File

@ -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();

View File

@ -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");

View File

@ -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);

View File

@ -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);
});
}

View File

@ -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);
}); });
}); });
}); });
}); });

View File

@ -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');

View File

@ -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);
});
}) })

View File

@ -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
*/

View File

@ -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);

View File

@ -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;

View File

@ -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",

View File

@ -12,6 +12,7 @@ var suites = [
'wiki', 'wiki',
'admin', 'admin',
'issues', 'issues',
'epics',
'tasks', 'tasks',
'userProfile', 'userProfile',
'userStories', 'userStories',