Epic detail
parent
f8dd7408d2
commit
3d858cf82a
|
@ -46,7 +46,7 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
|
|||
|
||||
$animateProvider.classNameFilter(/^(?:(?!ng-animate-disabled).)*$/)
|
||||
|
||||
# wait until the trasnlation is ready to resolve the page
|
||||
# wait until the translation is ready to resolve the page
|
||||
originalWhen = $routeProvider.when
|
||||
|
||||
$routeProvider.when = (path, route) ->
|
||||
|
@ -162,6 +162,15 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
|
|||
}
|
||||
)
|
||||
|
||||
# Epics
|
||||
$routeProvider.when("/project/:pslug/epic/:epicref",
|
||||
{
|
||||
templateUrl: "epic/epic-detail.html",
|
||||
loader: true,
|
||||
section: "epics"
|
||||
}
|
||||
)
|
||||
|
||||
$routeProvider.when("/project/:pslug/backlog",
|
||||
{
|
||||
templateUrl: "backlog/backlog.html",
|
||||
|
@ -793,6 +802,7 @@ modules = [
|
|||
"taigaPlugins",
|
||||
"taigaIntegrations",
|
||||
"taigaComponents",
|
||||
|
||||
# new modules
|
||||
"taigaProfile",
|
||||
"taigaHome",
|
||||
|
@ -801,7 +811,7 @@ modules = [
|
|||
"taigaDiscover",
|
||||
"taigaHistory",
|
||||
"taigaWikiHistory",
|
||||
'taigaEpics',
|
||||
"taigaEpics",
|
||||
|
||||
# template cache
|
||||
"templates",
|
||||
|
|
|
@ -74,3 +74,29 @@ sizeFormat = =>
|
|||
return @.taiga.sizeFormat
|
||||
|
||||
module.filter("sizeFormat", sizeFormat)
|
||||
|
||||
|
||||
toMutableFilter = ->
|
||||
toMutable = (js) ->
|
||||
return js.toJS()
|
||||
|
||||
memoizedMutable = _.memoize(toMutable)
|
||||
|
||||
return (input) ->
|
||||
if input instanceof Immutable.List
|
||||
return memoizedMutable(input)
|
||||
|
||||
return input
|
||||
|
||||
module.filter("toMutable", toMutableFilter)
|
||||
|
||||
|
||||
byRefFilter = ($filterFilter)->
|
||||
return (userstories, filter) ->
|
||||
if filter?.startsWith("#")
|
||||
cleanRef= filter.substr(1)
|
||||
return _.filter(userstories, (us) => String(us.ref).startsWith(cleanRef))
|
||||
|
||||
return $filterFilter(userstories, filter)
|
||||
|
||||
module.filter("byRef", ["filterFilter", byRefFilter])
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
# 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/projects.coffee
|
||||
# File: modules/epics.coffee
|
||||
###
|
||||
|
||||
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"
|
||||
"epic-upvote": "/epics/%s/upvote"
|
||||
"epic-downvote": "/epics/%s/downvote"
|
||||
"epic-watch": "/epics/%s/watch"
|
||||
"epic-unwatch": "/epics/%s/unwatch"
|
||||
"epic-related-userstories": "/epics/%s/related_userstories"
|
||||
"epic-related-userstories-bulk-create": "/epics/%s/related_userstories/bulk_create"
|
||||
|
||||
# User stories
|
||||
"userstories": "/userstories"
|
||||
|
@ -134,12 +140,14 @@ urls = {
|
|||
"wiki-links": "/wiki-links"
|
||||
|
||||
# History
|
||||
"history/epic": "/history/epic"
|
||||
"history/us": "/history/userstory"
|
||||
"history/issue": "/history/issue"
|
||||
"history/task": "/history/task"
|
||||
"history/wiki": "/history/wiki/%s"
|
||||
|
||||
# Attachments
|
||||
"attachments/epic": "/epics/attachments"
|
||||
"attachments/us": "/userstories/attachments"
|
||||
"attachments/issue": "/issues/attachments"
|
||||
"attachments/task": "/tasks/attachments"
|
||||
|
|
|
@ -28,10 +28,16 @@ taiga = @.taiga
|
|||
generateHash = taiga.generateHash
|
||||
|
||||
|
||||
resourceProvider = ($repo, $storage) ->
|
||||
resourceProvider = ($repo, $http, $urls, $storage) ->
|
||||
service = {}
|
||||
hashSuffix = "epics-queryparams"
|
||||
|
||||
service.getByRef = (projectId, ref) ->
|
||||
params = service.getQueryParams(projectId)
|
||||
params.project = projectId
|
||||
params.ref = ref
|
||||
return $repo.queryOne("epics", "by_ref", params)
|
||||
|
||||
service.listValues = (projectId, type) ->
|
||||
params = {"project": projectId}
|
||||
service.storeQueryParams(projectId, params)
|
||||
|
@ -47,9 +53,25 @@ resourceProvider = ($repo, $storage) ->
|
|||
hash = generateHash([projectId, ns])
|
||||
return $storage.get(hash) or {}
|
||||
|
||||
service.upvote = (epicId) ->
|
||||
url = $urls.resolve("epic-upvote", epicId)
|
||||
return $http.post(url)
|
||||
|
||||
service.downvote = (epicId) ->
|
||||
url = $urls.resolve("epic-downvote", epicId)
|
||||
return $http.post(url)
|
||||
|
||||
service.watch = (epicId) ->
|
||||
url = $urls.resolve("epic-watch", epicId)
|
||||
return $http.post(url)
|
||||
|
||||
service.unwatch = (epicId) ->
|
||||
url = $urls.resolve("epic-unwatch", epicId)
|
||||
return $http.post(url)
|
||||
|
||||
return (instance) ->
|
||||
instance.epics = service
|
||||
|
||||
|
||||
module = angular.module("taigaResources")
|
||||
module.factory("$tgEpicsResourcesProvider", ["$tgRepo", "$tgStorage", resourceProvider])
|
||||
module.factory("$tgEpicsResourcesProvider", ["$tgRepo","$tgHttp", "$tgUrls", "$tgStorage", resourceProvider])
|
||||
|
|
|
@ -47,6 +47,7 @@
|
|||
"CAPSLOCK_WARNING": "Be careful! You are using capital letters in an input field that is case sensitive.",
|
||||
"CONFIRM_CLOSE_EDIT_MODE_TITLE": "Are you sure you want to close the edit mode?",
|
||||
"CONFIRM_CLOSE_EDIT_MODE_MESSAGE": "Remember that if you close the edit mode without saving all the changes will be lost",
|
||||
"RELATED_USERSTORIES": "Related user stories",
|
||||
"CARD": {
|
||||
"ASSIGN_TO": "Assign To",
|
||||
"EDIT": "Edit card"
|
||||
|
@ -1061,6 +1062,26 @@
|
|||
"BUTTON": "Ask this project member to become the new project owner"
|
||||
}
|
||||
},
|
||||
"EPIC": {
|
||||
"PAGE_TITLE": "{{epicSubject}} - Epic {{epicRef}} - {{projectName}}",
|
||||
"PAGE_DESCRIPTION": "Status: {{epicStatus }}. Description: {{epicDescription}}",
|
||||
"SECTION_NAME": "Epic",
|
||||
"TITLE_LIGHTBOX_DELETE_RELATED_USERSTORY": "Delete related userstory...",
|
||||
"MSG_LIGHTBOX_DELETE_RELATED_USERSTORY": "the related userstory '{{subject}}'",
|
||||
"ERROR_DELETE_RELATED_USERSTORY": "We have not been able to delete: {{errorMessage}}",
|
||||
"CREATE_RELATED_USERSTORIES": "Create a relationship with a user story",
|
||||
"RELATED_WITH": "Related with",
|
||||
"NEW_USERSTORY": "New user story",
|
||||
"EXISTING_USERSTORY": "Existing user story",
|
||||
"CHOOSE_PROJECT_FOR_CREATION": "Whats' the project?",
|
||||
"SUBJECT": "Subject",
|
||||
"SUBJECT_BULK_MODE": "Subject (bulk insert)",
|
||||
"CHOOSE_PROJECT_FROM": "What's the project?",
|
||||
"CHOOSE_USERSTORY": "What's the user story?",
|
||||
"FILTER_USERSTORIES": "Filter user stories",
|
||||
"LIGHTBOX_TITLE_BLOKING_EPIC": "Blocking epic",
|
||||
"ACTION_DELETE": "Delete epic"
|
||||
},
|
||||
"US": {
|
||||
"PAGE_TITLE": "{{userStorySubject}} - User Story {{userStoryRef}} - {{projectName}}",
|
||||
"PAGE_DESCRIPTION": "Status: {{userStoryStatus }}. Completed {{userStoryProgressPercentage}}% ({{userStoryClosedTasks}} of {{userStoryTotalTasks}} tasks closed). Points: {{userStoryPoints}}. Description: {{userStoryDescription}}",
|
||||
|
|
|
@ -6,5 +6,5 @@ span.belong-to-epic-text-wrapper(tg-repeat="epic in epics track by epic.get('id'
|
|||
)
|
||||
a.belong-to-epic-text(
|
||||
href=""
|
||||
tg-nav="project-epics-detail:project=vm.project.get('slug')"
|
||||
tg-nav="project-epics-detail:project=epic.getIn(['project', 'slug']),ref=epic.get('ref')"
|
||||
) #{hash}{{epic.get('id')}} {{epic.get('subject')}}
|
||||
|
|
|
@ -25,9 +25,6 @@ BelongToEpicsDirective = () ->
|
|||
if scope.epics && !scope.epics.isIterable
|
||||
scope.epics = Immutable.fromJS(scope.epics)
|
||||
|
||||
if scope.project && !scope.project.isIterable
|
||||
scope.project = Immutable.fromJS(scope.project)
|
||||
|
||||
scope.getTemplateUrl = () ->
|
||||
if attrs.format
|
||||
return "components/belong-to-epics/belong-to-epics-" + attrs.format + ".html"
|
||||
|
|
|
@ -41,7 +41,6 @@
|
|||
ng-if="::vm.item.epics"
|
||||
epics="::vm.item.epics"
|
||||
format="text"
|
||||
project="project"
|
||||
)
|
||||
|
||||
//- Task belongs to US
|
||||
|
@ -60,7 +59,6 @@
|
|||
ng-if="::vm.item.user_story_extra_info.epics"
|
||||
epics="::vm.item.user_story_extra_info.epics"
|
||||
format="pill"
|
||||
project="vm.project"
|
||||
)
|
||||
|
||||
//- User Stories generated from issue
|
||||
|
|
|
@ -45,7 +45,8 @@ class WatchButtonController
|
|||
perms = {
|
||||
userstories: 'modify_us',
|
||||
issues: 'modify_issue',
|
||||
tasks: 'modify_task'
|
||||
tasks: 'modify_task',
|
||||
epics: 'modify_epic'
|
||||
}
|
||||
|
||||
return perms[name]
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
.name(ng-if="vm.column.name")
|
||||
- var hash = "#";
|
||||
a(
|
||||
tg-nav="project-epics-detail:project=vm.project.get('slug')"
|
||||
tg-nav="project-epics-detail:project=vm.project.slug,ref=vm.epic.get('ref')"
|
||||
ng-attr-title="{{::vm.epic.get('subject')}}"
|
||||
) #{hash}{{::vm.epic.get('ref')}} {{::vm.epic.get('subject')}}
|
||||
span.epic-pill(
|
||||
|
|
|
@ -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)
|
||||
|
||||
service.addRelatedUserstory = (epicId, userstoryId) ->
|
||||
url = urlsService.resolve("epic-related-userstories", epicId)
|
||||
|
||||
params = {
|
||||
user_story: userstoryId
|
||||
epic: epicId
|
||||
}
|
||||
|
||||
return http.post(url, params)
|
||||
|
||||
service.bulkCreateRelatedUserStories = (epicId, projectId, bulk_userstories) ->
|
||||
url = urlsService.resolve("epic-related-userstories-bulk-create", epicId)
|
||||
|
||||
params = {
|
||||
bulk_userstories: bulk_userstories,
|
||||
project_id: projectId
|
||||
}
|
||||
|
||||
return http.post(url, params)
|
||||
|
||||
service.deleteRelatedUserstory = (epicId, userstoryId) ->
|
||||
url = urlsService.resolve("epic-related-userstories", epicId) + "/#{userstoryId}"
|
||||
|
||||
return http.delete(url)
|
||||
|
||||
return () ->
|
||||
return {"epics": service}
|
||||
|
||||
|
|
|
@ -33,6 +33,22 @@ Resource = (urlsService, http) ->
|
|||
.then (result) ->
|
||||
return Immutable.fromJS(result.data)
|
||||
|
||||
service.listAllInProject = (projectId) ->
|
||||
url = urlsService.resolve("userstories")
|
||||
|
||||
httpOptions = {
|
||||
headers: {
|
||||
"x-disable-pagination": "1"
|
||||
}
|
||||
}
|
||||
|
||||
params = {
|
||||
project: projectId
|
||||
}
|
||||
return http.get(url, params, httpOptions)
|
||||
.then (result) ->
|
||||
return Immutable.fromJS(result.data)
|
||||
|
||||
service.listInEpic = (epicIid) ->
|
||||
url = urlsService.resolve("userstories")
|
||||
|
||||
|
|
|
@ -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 {
|
||||
@include svg-size(1.5rem);
|
||||
fill: currentColor;
|
||||
margin-right: 1rem;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
.template-name {
|
||||
|
|
82
conf.e2e.js
82
conf.e2e.js
|
@ -53,55 +53,55 @@ var config = {
|
|||
onPrepare: function() {
|
||||
// disable by default because performance problems on IE
|
||||
// track mouse movements
|
||||
// var trackMouse = function() {
|
||||
// angular.module('trackMouse', []).run(function($document) {
|
||||
var trackMouse = function() {
|
||||
angular.module('trackMouse', []).run(function($document) {
|
||||
|
||||
// function addDot(ev) {
|
||||
// var color = 'black',
|
||||
// size = 6;
|
||||
function addDot(ev) {
|
||||
var color = 'black',
|
||||
size = 6;
|
||||
|
||||
// switch (ev.type) {
|
||||
// case 'click':
|
||||
// color = 'red';
|
||||
// break;
|
||||
// case 'dblclick':
|
||||
// color = 'blue';
|
||||
// break;
|
||||
// case 'mousemove':
|
||||
// color = 'green';
|
||||
// break;
|
||||
// }
|
||||
switch (ev.type) {
|
||||
case 'click':
|
||||
color = 'red';
|
||||
break;
|
||||
case 'dblclick':
|
||||
color = 'blue';
|
||||
break;
|
||||
case 'mousemove':
|
||||
color = 'green';
|
||||
break;
|
||||
}
|
||||
|
||||
// var dotEl = $('<div></div>')
|
||||
// .css({
|
||||
// position: 'fixed',
|
||||
// height: size + 'px',
|
||||
// width: size + 'px',
|
||||
// 'background-color': color,
|
||||
// top: ev.clientY,
|
||||
// left: ev.clientX,
|
||||
var dotEl = $('<div></div>')
|
||||
.css({
|
||||
position: 'fixed',
|
||||
height: size + 'px',
|
||||
width: size + 'px',
|
||||
'background-color': color,
|
||||
top: ev.clientY,
|
||||
left: ev.clientX,
|
||||
|
||||
// 'z-index': 9999,
|
||||
'z-index': 9999,
|
||||
|
||||
// // make sure this dot won't interfere with the mouse events of other elements
|
||||
// 'pointer-events': 'none'
|
||||
// })
|
||||
// .appendTo('body');
|
||||
// make sure this dot won't interfere with the mouse events of other elements
|
||||
'pointer-events': 'none'
|
||||
})
|
||||
.appendTo('body');
|
||||
|
||||
// setTimeout(function() {
|
||||
// dotEl.remove();
|
||||
// }, 1000);
|
||||
// }
|
||||
setTimeout(function() {
|
||||
dotEl.remove();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// $document.on({
|
||||
// click: addDot,
|
||||
// dblclick: addDot,
|
||||
// mousemove: addDot
|
||||
// });
|
||||
$document.on({
|
||||
click: addDot,
|
||||
dblclick: addDot,
|
||||
mousemove: addDot
|
||||
});
|
||||
|
||||
// });
|
||||
// };
|
||||
// browser.addMockModule('trackMouse', trackMouse);
|
||||
});
|
||||
};
|
||||
browser.addMockModule('trackMouse', trackMouse);
|
||||
|
||||
browser.params.glob.back = argv.back;
|
||||
|
||||
|
|
|
@ -86,7 +86,7 @@ helper.tags = function() {
|
|||
for (let tag of tags){
|
||||
htmlChanges = await utils.common.outerHtmlChanges(el.$(".tags-container"));
|
||||
el.$('.e2e-add-tag-input').sendKeys(tag);
|
||||
await browser.actions().sendKeys(protractor.Key.ENTER).perform();
|
||||
el.$('.save').click();
|
||||
await htmlChanges();
|
||||
}
|
||||
}
|
||||
|
@ -542,3 +542,43 @@ helper.watchersLightbox = function() {
|
|||
|
||||
return obj;
|
||||
};
|
||||
|
||||
helper.teamRequirement = function() {
|
||||
let el = $('tg-us-team-requirement-button');
|
||||
|
||||
let obj = {
|
||||
el: el,
|
||||
|
||||
toggleStatus: async function(){
|
||||
await el.$("label").click();
|
||||
await browser.waitForAngular();
|
||||
},
|
||||
|
||||
isRequired: async function() {
|
||||
let classes = await el.$("label").getAttribute('class');
|
||||
return classes.includes("active");
|
||||
}
|
||||
};
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
helper.clientRequirement = function() {
|
||||
let el = $('tg-us-client-requirement-button');
|
||||
|
||||
let obj = {
|
||||
el: el,
|
||||
|
||||
toggleStatus: async function(){
|
||||
await el.$("label").click();
|
||||
await browser.waitForAngular();
|
||||
},
|
||||
|
||||
isRequired: async function() {
|
||||
let classes = await el.$("label").getAttribute('class');
|
||||
return classes.includes("active");
|
||||
}
|
||||
};
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
|
|
@ -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() {
|
||||
el.get(0).$('.e2e-assigned-to-image').click();
|
||||
$$('.e2e-assigned-to-selector').get(0).click();
|
||||
await browser.waitForAngular();
|
||||
},
|
||||
editAssignedTo: async function() {
|
||||
el.get(0).$('.e2e-assigned-to-image').click();
|
||||
utils.common.takeScreenshot("epics", "epics-edit-assigned");
|
||||
$$('.e2e-assigned-to-selector').last().click();
|
||||
await browser.waitForAngular();
|
||||
},
|
||||
removeAssignedTo: async function() {
|
||||
el.get(0).$('.e2e-assigned-to-image').click();
|
||||
$$('.e2e-unassign').click();
|
||||
$('.e2e-unassign').click();
|
||||
await browser.waitForAngular();
|
||||
return el.get(0).$('.e2e-assigned-to-image').getAttribute("alt");
|
||||
},
|
||||
resetStatus: function() {
|
||||
resetStatus: async function() {
|
||||
el.get(0).$('.e2e-epic-status').click();
|
||||
el.get(0).$$('.e2e-edit-epic-status').get(0).click();
|
||||
await browser.waitForAngular();
|
||||
},
|
||||
getStatus: function() {
|
||||
return el.get(0).$('.e2e-epic-status').getText();
|
||||
},
|
||||
editStatus: function() {
|
||||
editStatus: async function() {
|
||||
el.get(0).$('.e2e-epic-status').click();
|
||||
utils.common.takeScreenshot("epics", "epics-edit-status");
|
||||
el.get(0).$$('.e2e-edit-epic-status').last().click();
|
||||
await browser.waitForAngular();
|
||||
},
|
||||
getColumns: function() {
|
||||
return $$('.e2e-epics-table-header > div').count();
|
||||
},
|
||||
removeColumns: function() {
|
||||
removeColumns: async function() {
|
||||
$('.e2e-epics-column-button').click();
|
||||
utils.common.takeScreenshot("epics", "epics-edit-columns");
|
||||
$$('.e2e-epics-column-dropdown .check').first().click();
|
|
@ -13,3 +13,5 @@ module.exports.adminPermissions = require("./admin-permissions");
|
|||
module.exports.adminIntegrations = require("./admin-integrations");
|
||||
module.exports.issues = require("./issues-helper");
|
||||
module.exports.createProject = require("./create-project-helper");
|
||||
module.exports.epicsDashboard = require("./epics-dashboard-helper");
|
||||
module.exports.epicDetail = require("./epic-detail-helper");
|
||||
|
|
|
@ -3,45 +3,6 @@ var commonHelper = require('./common-helper');
|
|||
|
||||
var helper = module.exports;
|
||||
|
||||
helper.teamRequirement = function() {
|
||||
let el = $('tg-us-team-requirement-button');
|
||||
|
||||
let obj = {
|
||||
el: el,
|
||||
|
||||
toggleStatus: async function(){
|
||||
await el.$("label").click();
|
||||
await browser.waitForAngular();
|
||||
},
|
||||
|
||||
isRequired: async function() {
|
||||
let classes = await el.$("label").getAttribute('class');
|
||||
return classes.includes("active");
|
||||
}
|
||||
};
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
helper.clientRequirement = function() {
|
||||
let el = $('tg-us-client-requirement-button');
|
||||
|
||||
let obj = {
|
||||
el: el,
|
||||
|
||||
toggleStatus: async function(){
|
||||
await el.$("label").click();
|
||||
await browser.waitForAngular();
|
||||
},
|
||||
|
||||
isRequired: async function() {
|
||||
let classes = await el.$("label").getAttribute('class');
|
||||
return classes.includes("active");
|
||||
}
|
||||
};
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
helper.relatedTaskForm = async function(form, name, status, assigned_to) {
|
||||
await form.$('input').sendKeys(name);
|
||||
|
|
|
@ -274,12 +274,12 @@ shared.blockTesting = async function() {
|
|||
let descriptionText = await $('.block-description').getText();
|
||||
expect(descriptionText).to.be.equal('This is a testing block reason');
|
||||
|
||||
let isDisplayed = $('.block-description').isDisplayed();
|
||||
let isDisplayed = $('.block-desc-container').isDisplayed();
|
||||
expect(isDisplayed).to.be.equal.true;
|
||||
|
||||
blockHelper.unblock();
|
||||
|
||||
isDisplayed = $('.block-description').isDisplayed();
|
||||
isDisplayed = $('.block-desc-container').isDisplayed();
|
||||
expect(isDisplayed).to.be.equal.false;
|
||||
|
||||
await notifications.success.close();
|
||||
|
@ -548,3 +548,37 @@ shared.customFields = function(typeIndex) {
|
|||
expect(fieldText).to.be.equal('test text2 edit');
|
||||
});
|
||||
};
|
||||
|
||||
shared.teamRequirementTesting = function() {
|
||||
it('team requirement edition', async function() {
|
||||
let requirementHelper = detailHelper.teamRequirement();
|
||||
let isRequired = await requirementHelper.isRequired();
|
||||
|
||||
// Toggle
|
||||
requirementHelper.toggleStatus();
|
||||
let newIsRequired = await requirementHelper.isRequired();
|
||||
expect(isRequired).to.be.not.equal(newIsRequired);
|
||||
|
||||
// Toggle again
|
||||
requirementHelper.toggleStatus();
|
||||
newIsRequired = await requirementHelper.isRequired();
|
||||
expect(isRequired).to.be.equal(newIsRequired);
|
||||
});
|
||||
}
|
||||
|
||||
shared.clientRequirementTesting = function () {
|
||||
it('client requirement edition', async function() {
|
||||
let requirementHelper = detailHelper.clientRequirement();
|
||||
let isRequired = await requirementHelper.isRequired();
|
||||
|
||||
// Toggle
|
||||
requirementHelper.toggleStatus();
|
||||
let newIsRequired = await requirementHelper.isRequired();
|
||||
expect(isRequired).to.be.not.equal(newIsRequired);
|
||||
|
||||
// Toggle again
|
||||
requirementHelper.toggleStatus();
|
||||
newIsRequired = await requirementHelper.isRequired();
|
||||
expect(isRequired).to.be.equal(newIsRequired);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -16,8 +16,64 @@ describe('custom-fields', function() {
|
|||
});
|
||||
|
||||
describe('create custom fields', function() {
|
||||
describe('epics', function() {
|
||||
let typeIndex = 0;
|
||||
|
||||
it('create', async function() {
|
||||
let oldCountCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count();
|
||||
|
||||
await customFieldsHelper.create(typeIndex, 'test1-text', 'desc1', 1);
|
||||
|
||||
// debounce :(
|
||||
await utils.notifications.success.open();
|
||||
await browser.sleep(2000);
|
||||
|
||||
await customFieldsHelper.create(typeIndex, 'test1-multi', 'desc1', 3);
|
||||
|
||||
// debounce :(
|
||||
await utils.notifications.success.open();
|
||||
await browser.sleep(2000);
|
||||
|
||||
let countCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count();
|
||||
|
||||
expect(countCustomFields).to.be.equal(oldCountCustomFields + 2);
|
||||
});
|
||||
|
||||
it('edit', async function() {
|
||||
customFieldsHelper.edit(typeIndex, 0, 'edit', 'desc2', 2);
|
||||
|
||||
let open = await utils.notifications.success.open();
|
||||
|
||||
expect(open).to.be.true;
|
||||
|
||||
await utils.notifications.success.close();
|
||||
});
|
||||
|
||||
it('drag', async function() {
|
||||
let nameOld = await customFieldsHelper.getName(typeIndex, 0);
|
||||
|
||||
await customFieldsHelper.drag(typeIndex, 0, 1);
|
||||
|
||||
let nameNew = await customFieldsHelper.getName(typeIndex, 1);
|
||||
|
||||
expect(nameNew).to.be.equal(nameOld);
|
||||
});
|
||||
|
||||
it('delete', async function() {
|
||||
let oldCountCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count();
|
||||
|
||||
await customFieldsHelper.delete(typeIndex, 0);
|
||||
|
||||
await browser.wait(async function() {
|
||||
let countCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count();
|
||||
|
||||
return countCustomFields === oldCountCustomFields - 1;
|
||||
}, 4000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('userstories', function() {
|
||||
let typeIndex = 0;
|
||||
let typeIndex = 1;
|
||||
|
||||
it('create', async function() {
|
||||
let oldCountCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count();
|
||||
|
@ -73,7 +129,7 @@ describe('custom-fields', function() {
|
|||
});
|
||||
|
||||
describe('tasks', function() {
|
||||
let typeIndex = 1;
|
||||
let typeIndex = 2;
|
||||
|
||||
it('create', async function() {
|
||||
let oldCountCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count();
|
||||
|
@ -126,7 +182,7 @@ describe('custom-fields', function() {
|
|||
});
|
||||
|
||||
describe('issues', function() {
|
||||
let typeIndex = 2;
|
||||
let typeIndex = 3;
|
||||
|
||||
it('create', async function() {
|
||||
let oldCountCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count();
|
||||
|
@ -180,5 +236,6 @@ describe('custom-fields', function() {
|
|||
}, 4000);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,7 +8,7 @@ var chaiAsPromised = require('chai-as-promised');
|
|||
chai.use(chaiAsPromised);
|
||||
var expect = chai.expect;
|
||||
|
||||
describe.only('admin - members', function() {
|
||||
describe('admin - members', function() {
|
||||
before(async function(){
|
||||
browser.get(browser.params.glob.host + 'project/project-0/admin/memberships');
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
var utils = require('../../utils');
|
||||
var epicsHelper = require('../../helpers/epics-helper');
|
||||
var epicsDashboardHelper = require('../../helpers').epicsDashboard;
|
||||
|
||||
var chai = require('chai');
|
||||
var chaiAsPromised = require('chai-as-promised');
|
||||
|
@ -8,7 +8,7 @@ chai.use(chaiAsPromised);
|
|||
var expect = chai.expect;
|
||||
|
||||
describe('Epics Dashboard', function(){
|
||||
let usUrl = '';
|
||||
let epicsUrl = '';
|
||||
|
||||
before(async function(){
|
||||
await utils.nav
|
||||
|
@ -17,7 +17,7 @@ describe('Epics Dashboard', function(){
|
|||
.epics()
|
||||
.go();
|
||||
|
||||
usUrl = await browser.getCurrentUrl();
|
||||
epicsUrl = await browser.getCurrentUrl();
|
||||
});
|
||||
|
||||
it('screenshot', async function() {
|
||||
|
@ -25,13 +25,23 @@ describe('Epics Dashboard', function(){
|
|||
});
|
||||
|
||||
it('display child stories', async function() {
|
||||
let epic = epicsHelper.epic();
|
||||
let epic = epicsDashboardHelper.epic();
|
||||
let childStoriesNum = await epic.displayUserStoriesinEpic();
|
||||
expect(childStoriesNum).to.be.above(0);
|
||||
});
|
||||
|
||||
it('create Epic', async function() {
|
||||
let date = Date.now();
|
||||
let description = Math.random().toString(36).substring(7);
|
||||
let epic = epicsDashboardHelper.epic();
|
||||
let currentEpicsNum = await epic.getEpics();
|
||||
await epic.createEpic(date, description);
|
||||
let newEpicsNum = await epic.getEpics();
|
||||
expect(newEpicsNum).to.be.above(currentEpicsNum);
|
||||
});
|
||||
|
||||
it('change epic assigned from dashboard', async function() {
|
||||
let epic = epicsHelper.epic();
|
||||
let epic = epicsDashboardHelper.epic();
|
||||
await epic.resetAssignedTo();
|
||||
let currentAssigned = await epic.getAssignedTo();
|
||||
await epic.editAssignedTo();
|
||||
|
@ -40,15 +50,14 @@ describe('Epics Dashboard', function(){
|
|||
});
|
||||
|
||||
it('remove assigned from dashboard', async function() {
|
||||
let epic = epicsHelper.epic();
|
||||
let epic = epicsDashboardHelper.epic();
|
||||
await epic.resetAssignedTo();
|
||||
let unAssigned = await epic.removeAssignedTo();
|
||||
console.log(unAssigned);
|
||||
expect(unAssigned).to.be.equal('Unassigned');
|
||||
});
|
||||
|
||||
it('change status from dashboard', async function() {
|
||||
let epic = epicsHelper.epic();
|
||||
let epic = epicsDashboardHelper.epic();
|
||||
await epic.resetStatus();
|
||||
let currentStatus = await epic.getStatus();
|
||||
await epic.editStatus();
|
||||
|
@ -57,22 +66,11 @@ describe('Epics Dashboard', function(){
|
|||
});
|
||||
|
||||
it('remove columns from dashboard', async function() {
|
||||
let epic = epicsHelper.epic();
|
||||
let epic = epicsDashboardHelper.epic();
|
||||
let currentColumns = await epic.getColumns();
|
||||
await epic.removeColumns();
|
||||
let newColumns = await epic.getColumns();
|
||||
expect(currentColumns).to.be.above(newColumns);
|
||||
});
|
||||
|
||||
it.only('create Epic', async function() {
|
||||
let date = Date.now();
|
||||
let description = Math.random().toString(36).substring(7);
|
||||
let epic = epicsHelper.epic();
|
||||
let currentEpicsNum = await epic.getEpics();
|
||||
await epic.createEpic(date, description);
|
||||
let newEpicsNum = await epic.getEpics();
|
||||
console.log(currentEpicsNum, newEpicsNum);
|
||||
expect(newEpicsNum).to.be.above(currentEpicsNum);
|
||||
});
|
||||
|
||||
})
|
||||
|
|
|
@ -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);
|
||||
|
||||
it('team requirement edition', async function() {
|
||||
let requirementHelper = usDetailHelper.teamRequirement();
|
||||
let isRequired = await requirementHelper.isRequired();
|
||||
describe('team requirement edition', sharedDetail.teamRequirementTesting);
|
||||
|
||||
// Toggle
|
||||
requirementHelper.toggleStatus();
|
||||
let newIsRequired = await requirementHelper.isRequired();
|
||||
expect(isRequired).to.be.not.equal(newIsRequired);
|
||||
|
||||
// Toggle again
|
||||
requirementHelper.toggleStatus();
|
||||
newIsRequired = await requirementHelper.isRequired();
|
||||
expect(isRequired).to.be.equal(newIsRequired);
|
||||
});
|
||||
|
||||
it('client requirement edition', async function() {
|
||||
let requirementHelper = usDetailHelper.clientRequirement();
|
||||
let isRequired = await requirementHelper.isRequired();
|
||||
|
||||
// Toggle
|
||||
requirementHelper.toggleStatus();
|
||||
let newIsRequired = await requirementHelper.isRequired();
|
||||
expect(isRequired).to.be.not.equal(newIsRequired);
|
||||
|
||||
// Toggle again
|
||||
requirementHelper.toggleStatus();
|
||||
newIsRequired = await requirementHelper.isRequired();
|
||||
expect(isRequired).to.be.equal(newIsRequired);
|
||||
});
|
||||
describe('client requirement edition', sharedDetail.clientRequirementTesting);
|
||||
|
||||
describe('watchers edition', sharedDetail.watchersTesting);
|
||||
|
||||
|
|
|
@ -46,11 +46,21 @@ var actions = {
|
|||
|
||||
return common.waitLoader();
|
||||
},
|
||||
|
||||
epics: async function() {
|
||||
await common.link($('#nav-epics a'));
|
||||
|
||||
return common.waitLoader();
|
||||
},
|
||||
|
||||
epic: async function(index) {
|
||||
let epic = $$('.e2e-epic-row .name a').get(index);
|
||||
|
||||
await common.link(epic);
|
||||
|
||||
return common.waitLoader();
|
||||
},
|
||||
|
||||
backlog: async function() {
|
||||
await common.link($$('#nav-backlog a').first());
|
||||
|
||||
|
@ -110,6 +120,10 @@ var nav = {
|
|||
this.actions.push(actions.epics.bind(null, index));
|
||||
return this;
|
||||
},
|
||||
epic: function(index) {
|
||||
this.actions.push(actions.epic.bind(null, index));
|
||||
return this;
|
||||
},
|
||||
backlog: function(index) {
|
||||
this.actions.push(actions.backlog.bind(null, index));
|
||||
return this;
|
||||
|
|
|
@ -130,6 +130,7 @@ paths.coffee_order = [
|
|||
paths.app + "coffee/modules/backlog/*.coffee",
|
||||
paths.app + "coffee/modules/taskboard/*.coffee",
|
||||
paths.app + "coffee/modules/kanban/*.coffee",
|
||||
paths.app + "coffee/modules/epics/*.coffee",
|
||||
paths.app + "coffee/modules/issues/*.coffee",
|
||||
paths.app + "coffee/modules/userstories/*.coffee",
|
||||
paths.app + "coffee/modules/tasks/*.coffee",
|
||||
|
|
|
@ -12,6 +12,7 @@ var suites = [
|
|||
'wiki',
|
||||
'admin',
|
||||
'issues',
|
||||
'epics',
|
||||
'tasks',
|
||||
'userProfile',
|
||||
'userStories',
|
||||
|
|
Loading…
Reference in New Issue