US #49 (taiga-subscriptions): Transfer project ownership
parent
f3d93e7a0f
commit
2e4a63fe75
|
@ -11,6 +11,7 @@
|
|||
- Add badge to project owners
|
||||
- Limit of user per project.
|
||||
- Redesign of the create project wizard
|
||||
- Transfer project ownership
|
||||
|
||||
### Misc
|
||||
- Lots of small and not so small bugfixes.
|
||||
|
|
|
@ -333,6 +333,16 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
|
|||
$routeProvider.when("/project/:pslug/admin/contrib/:plugin",
|
||||
{templateUrl: "contrib/main.html"})
|
||||
|
||||
# Transfer project
|
||||
$routeProvider.when("/project/:pslug/transfer/:token",
|
||||
{
|
||||
templateUrl: "projects/transfer/transfer-page.html",
|
||||
loader: true,
|
||||
controller: "Project",
|
||||
controllerAs: "vm"
|
||||
}
|
||||
)
|
||||
|
||||
# User settings
|
||||
$routeProvider.when("/user-settings/user-profile",
|
||||
{templateUrl: "user/user-profile.html"})
|
||||
|
|
|
@ -136,3 +136,144 @@ LightboxAddMembersWarningMessageDirective = () ->
|
|||
}
|
||||
|
||||
module.directive("tgLightboxAddMembersWarningMessage", [LightboxAddMembersWarningMessageDirective])
|
||||
|
||||
|
||||
#############################################################################
|
||||
## Transfer project ownership
|
||||
#############################################################################
|
||||
|
||||
LbRequestOwnershipDirective = (lightboxService, rs, confirmService, $translate) ->
|
||||
return {
|
||||
link: (scope, el) ->
|
||||
lightboxService.open(el)
|
||||
|
||||
scope.request = () ->
|
||||
scope.loading = true
|
||||
|
||||
rs.projects.transferRequest(scope.projectId).then () ->
|
||||
scope.loading = false
|
||||
|
||||
lightboxService.close(el)
|
||||
|
||||
confirmService.notify("success", $translate.instant("ADMIN.PROJECT_PROFILE.REQUEST_OWNERSHIP_SUCCESS"))
|
||||
|
||||
templateUrl: "common/lightbox/lightbox-request-ownership.html"
|
||||
}
|
||||
|
||||
module.directive('tgLbRequestOwnership', [
|
||||
"lightboxService",
|
||||
"tgResources",
|
||||
"$tgConfirm",
|
||||
"$translate",
|
||||
LbRequestOwnershipDirective])
|
||||
|
||||
class ChangeOwnerLightboxController
|
||||
constructor: (@rs, @lightboxService, @confirm, @translate) ->
|
||||
@.users = []
|
||||
@.q = ""
|
||||
@.commentOpen = false
|
||||
|
||||
limit: 3
|
||||
|
||||
normalizeString: (normalizedString) ->
|
||||
normalizedString = normalizedString.replace("Á", "A").replace("Ä", "A").replace("À", "A")
|
||||
normalizedString = normalizedString.replace("É", "E").replace("Ë", "E").replace("È", "E")
|
||||
normalizedString = normalizedString.replace("Í", "I").replace("Ï", "I").replace("Ì", "I")
|
||||
normalizedString = normalizedString.replace("Ó", "O").replace("Ö", "O").replace("Ò", "O")
|
||||
normalizedString = normalizedString.replace("Ú", "U").replace("Ü", "U").replace("Ù", "U")
|
||||
return normalizedString
|
||||
|
||||
filterUsers: (user) ->
|
||||
username = user.full_name_display.toUpperCase()
|
||||
username = @.normalizeString(username)
|
||||
text = @.q.toUpperCase()
|
||||
text = @.normalizeString(text)
|
||||
|
||||
return _.includes(username, text)
|
||||
|
||||
getUsers: () ->
|
||||
if !@.users.length && !@.q.length
|
||||
users = @.activeUsers
|
||||
else
|
||||
users = @.users
|
||||
|
||||
users = users.slice(0, @.limit)
|
||||
users = _.reject(users, {"selected": true})
|
||||
|
||||
return _.reject(users, {"id": @.currentOwnerId})
|
||||
|
||||
userSearch: () ->
|
||||
@.users = @.activeUsers
|
||||
|
||||
@.selected = _.find(@.users, {"selected": true})
|
||||
|
||||
@.users = _.filter(@.users, @.filterUsers.bind(this)) if @.q
|
||||
|
||||
selectUser: (user) ->
|
||||
@.activeUsers = _.map @.activeUsers, (user) ->
|
||||
user.selected = false
|
||||
|
||||
return user
|
||||
|
||||
user.selected = true
|
||||
|
||||
@.userSearch()
|
||||
|
||||
submit: () ->
|
||||
@.loading = true
|
||||
@rs.projects.transferStart(@.projectId, @.selected.id, @.comment)
|
||||
.then () =>
|
||||
@.loading = false
|
||||
@lightboxService.closeAll()
|
||||
|
||||
title = @translate.instant("ADMIN.PROJECT_PROFILE.CHANGE_OWNER_SUCCESS_TITLE")
|
||||
desc = @translate.instant("ADMIN.PROJECT_PROFILE.CHANGE_OWNER_SUCCESS_DESC")
|
||||
|
||||
@confirm.success(title, desc, {
|
||||
type: "svg",
|
||||
name: "icon-speak-up"
|
||||
})
|
||||
|
||||
ChangeOwnerLightboxController.$inject = [
|
||||
"tgResources",
|
||||
"lightboxService",
|
||||
"$tgConfirm",
|
||||
"$translate"
|
||||
]
|
||||
|
||||
module.controller('ChangeOwnerLightbox', ChangeOwnerLightboxController)
|
||||
|
||||
ChangeOwnerLightboxDirective = (lightboxService, lightboxKeyboardNavigationService, $template, $compile) ->
|
||||
link = (scope, el) ->
|
||||
lightboxService.open(el)
|
||||
|
||||
return {
|
||||
scope: true,
|
||||
controller: "ChangeOwnerLightbox",
|
||||
controllerAs: "vm",
|
||||
bindToController: {
|
||||
currentOwnerId: "=",
|
||||
projectId: "=",
|
||||
activeUsers: "="
|
||||
},
|
||||
templateUrl: "common/lightbox/lightbox-change-owner.html"
|
||||
link:link
|
||||
}
|
||||
|
||||
|
||||
module.directive("tgLbChangeOwner", ["lightboxService", "lightboxKeyboardNavigationService", "$tgTemplate", "$compile", ChangeOwnerLightboxDirective])
|
||||
|
||||
TransferProjectStartSuccessDirective = (lightboxService) ->
|
||||
link = (scope, el) ->
|
||||
scope.close = () ->
|
||||
lightboxService.close(el)
|
||||
|
||||
lightboxService.open(el)
|
||||
|
||||
return {
|
||||
templateUrl: "common/lightbox/lightbox-transfer-project-start-success.html"
|
||||
link:link
|
||||
}
|
||||
|
||||
|
||||
module.directive("tgLbTransferProjectStartSuccess", ["lightboxService", TransferProjectStartSuccessDirective])
|
||||
|
|
|
@ -129,7 +129,10 @@ class MembershipsController extends mixOf(taiga.Controller, taiga.PageMixin, tai
|
|||
members: @scope.project.max_memberships
|
||||
})
|
||||
icon = "/" + window._version + "/svg/icons/team-question.svg"
|
||||
@confirm.success(title, message,icon)
|
||||
@confirm.success(title, message, {
|
||||
name: icon,
|
||||
type: "img"
|
||||
})
|
||||
|
||||
module.controller("MembershipsController", MembershipsController)
|
||||
|
||||
|
|
|
@ -69,6 +69,8 @@ class ProjectProfileController extends mixOf(taiga.Controller, taiga.PageMixin)
|
|||
description = @scope.project.description
|
||||
@appMetaService.setAll(title, description)
|
||||
|
||||
@.fillUsersAndRoles(@scope.project.members, @scope.project.roles)
|
||||
|
||||
promise.then null, @.onInitialDataError.bind(@)
|
||||
|
||||
@scope.$on "project:loaded", =>
|
||||
|
@ -535,3 +537,47 @@ AdminProjectRestrictionsDirective = () ->
|
|||
}
|
||||
|
||||
module.directive('tgAdminProjectRestrictions', [AdminProjectRestrictionsDirective])
|
||||
|
||||
AdminProjectRequestOwnershipDirective = (lightboxFactory) ->
|
||||
return {
|
||||
link: (scope) ->
|
||||
scope.requestOwnership = () ->
|
||||
lightboxFactory.create("tg-lb-request-ownership", {
|
||||
"class": "lightbox lightbox-request-ownership"
|
||||
}, {
|
||||
projectId: scope.projectId
|
||||
})
|
||||
|
||||
scope: {
|
||||
"projectId": "=",
|
||||
"owner": "="
|
||||
},
|
||||
templateUrl: "admin/admin-project-request-ownership.html"
|
||||
}
|
||||
|
||||
module.directive('tgAdminProjectRequestOwnership', ["tgLightboxFactory", AdminProjectRequestOwnershipDirective])
|
||||
|
||||
AdminProjectChangeOwnerDirective = (lightboxFactory) ->
|
||||
return {
|
||||
link: (scope) ->
|
||||
scope.changeOwner = () ->
|
||||
lightboxFactory.create("tg-lb-change-owner", {
|
||||
"class": "lightbox lightbox-select-user",
|
||||
"project-id": "projectId",
|
||||
"active-users": "activeUsers",
|
||||
"current-owner-id": "currentOwnerId"
|
||||
}, {
|
||||
projectId: scope.projectId,
|
||||
activeUsers: scope.activeUsers,
|
||||
currentOwnerId: scope.owner.id
|
||||
})
|
||||
|
||||
scope: {
|
||||
"activeUsers": "="
|
||||
"projectId": "="
|
||||
"owner": "="
|
||||
},
|
||||
templateUrl: "admin/admin-project-change-owner.html"
|
||||
}
|
||||
|
||||
module.directive('tgAdminProjectChangeOwner', ["tgLightboxFactory", AdminProjectChangeOwnerDirective])
|
||||
|
|
|
@ -167,8 +167,20 @@ class ConfirmService extends taiga.Service
|
|||
el = angular.element(".lightbox-generic-success")
|
||||
|
||||
el.find("img").remove()
|
||||
el.find("svg").remove()
|
||||
|
||||
if icon.type == "img"
|
||||
detailImage = $('<img>').addClass('lb-icon').attr('src', icon.name)
|
||||
else if icon.type == "svg"
|
||||
useSVG = document.createElementNS('http://www.w3.org/2000/svg', 'use')
|
||||
useSVG.setAttributeNS('http://www.w3.org/1999/xlink','href', '#' + icon.name)
|
||||
|
||||
detailImage = document.createElementNS("http://www.w3.org/2000/svg", "svg")
|
||||
detailImage.classList.add("icon")
|
||||
detailImage.classList.add("lb-icon")
|
||||
detailImage.classList.add(icon.name)
|
||||
detailImage.appendChild(useSVG)
|
||||
|
||||
detailImage = $('<img>').addClass('lb-icon').attr('src', icon)
|
||||
if detailImage
|
||||
el.find('section').prepend(detailImage)
|
||||
|
||||
|
@ -254,6 +266,7 @@ class ConfirmService extends taiga.Service
|
|||
body.find(selector)
|
||||
.removeClass('active')
|
||||
.addClass('inactive')
|
||||
.one 'animationend', () -> $(this).removeClass('inactive')
|
||||
|
||||
delete @.tsem
|
||||
|
||||
|
|
|
@ -74,6 +74,11 @@ urls = {
|
|||
"project-unlike": "/projects/%s/unlike"
|
||||
"project-watch": "/projects/%s/watch"
|
||||
"project-unwatch": "/projects/%s/unwatch"
|
||||
"project-transfer-validate-token": "/projects/%s/transfer_validate_token"
|
||||
"project-transfer-accept": "/projects/%s/transfer_accept"
|
||||
"project-transfer-reject": "/projects/%s/transfer_reject"
|
||||
"project-transfer-request": "/projects/%s/transfer_request"
|
||||
"project-transfer-start": "/projects/%s/transfer_start"
|
||||
|
||||
# Project Values - Choises
|
||||
"userstory-statuses": "/userstory-statuses"
|
||||
|
|
|
@ -472,7 +472,16 @@
|
|||
"MAX_PRIVATE_PROJECTS": "You've reached the maximum number of private projects",
|
||||
"MAX_PRIVATE_PROJECTS_MEMBERS": "The project exceeds the maximum members number in private projects",
|
||||
"MAX_PUBLIC_PROJECTS": "You've reached the maximum number of public projects",
|
||||
"MAX_PUBLIC_PROJECTS_MEMBERS": "The project exceeds the maximum members number in public projects"
|
||||
"MAX_PUBLIC_PROJECTS_MEMBERS": "The project exceeds the maximum members number in public projects",
|
||||
"PROJECT_OWNER": "Project owner",
|
||||
"REQUEST_OWNERSHIP": "Request ownership",
|
||||
"REQUEST_OWNERSHIP_CONFIRMATION_TITLE": "Do you want to be the project owner?",
|
||||
"REQUEST_OWNERSHIP_DESC": "Ask the owner {{name}} to transfert to you the project ownership.",
|
||||
"REQUEST_OWNERSHIP_BUTTON": "Request",
|
||||
"REQUEST_OWNERSHIP_SUCCESS": "We'll notify the project owner",
|
||||
"CHANGE_OWNER": "Change owner",
|
||||
"CHANGE_OWNER_SUCCESS_TITLE": "Ok, your request has been sent",
|
||||
"CHANGE_OWNER_SUCCESS_DESC": "We will notify you by email whether it accepts or rejects the ownership of the project."
|
||||
},
|
||||
"REPORTS": {
|
||||
"TITLE": "Reports",
|
||||
|
@ -680,6 +689,24 @@
|
|||
},
|
||||
"SUBMENU_THIDPARTIES": {
|
||||
"TITLE": "Services"
|
||||
},
|
||||
"PROJECT_TRANSFER": {
|
||||
"DO_YOU_ACCEPT_PROJECT_OWNERNSHIP": "Do you want to be the new project owner?",
|
||||
"PRIVATE": "Private",
|
||||
"ACCEPTED_PROJECT_OWNERNSHIP": "OK. Now you are the new owner of the project.",
|
||||
"REJECTED_PROJECT_OWNERNSHIP": "OK. We will contact with the owner",
|
||||
"ACCEPT": "Accept",
|
||||
"REJECT": "Reject",
|
||||
"PROPOSE_OWNERSHIP": "<strong>{{owner}}</strong>, the current owner of the project <strong>{{project}}</strong> wants you to be the new owner of the project.",
|
||||
"ADD_COMMENT_QUESTION": "Do you want to add a comment for the owner?",
|
||||
"ADD_COMMENT": "Do you want to add a comment for the owner?",
|
||||
"UNLIMITED_PROJECTS": "Unlimited",
|
||||
"OWNER_MESSAGE": {
|
||||
"PRIVATE": "Remember, you can own up to <strong>{{maxProjects}}</strong> private projects and you already own <strong>{{currentProjects}}</strong> private projects",
|
||||
"PUBLIC": "Remember, you can own <strong>{{maxProjects}}</strong> public projects and you already own <strong>{{currentProjects}}</strong> public projects"
|
||||
},
|
||||
"CANT_BE_OWNED": "Right now you can't be the owner of a project with this characteristics. To be the owner of this project you should contact the admin staff and change your account conditions.",
|
||||
"CHANGE_MY_PLAN": "Change my plan"
|
||||
}
|
||||
},
|
||||
"USER": {
|
||||
|
@ -932,6 +959,11 @@
|
|||
"DESC": "You can't delete the project owner, you must request a new owner before deleting the user.",
|
||||
"BUTTON": "Request change project owner"
|
||||
}
|
||||
},
|
||||
"CHANGE_OWNER": {
|
||||
"TITLE": "Who do you want to be the new owner?",
|
||||
"ADD_COMMENT": "Add comment",
|
||||
"BUTTON": "Ask this teammate to be the owner"
|
||||
}
|
||||
},
|
||||
"US": {
|
||||
|
|
|
@ -63,4 +63,14 @@ class ProjectsService extends taiga.Service
|
|||
bulkUpdateProjectsOrder: (sortData) ->
|
||||
return @rs.projects.bulkUpdateOrder(sortData)
|
||||
|
||||
transferValidateToken: (projectId, token) ->
|
||||
return @rs.projects.transferValidateToken(projectId, token)
|
||||
|
||||
transferAccept: (projectId, token, reason) ->
|
||||
return @rs.projects.transferAccept(projectId, token, reason)
|
||||
|
||||
transferReject: (projectId, token, reason) ->
|
||||
return @rs.projects.transferReject(projectId, token, reason)
|
||||
|
||||
|
||||
angular.module("taigaProjects").service("tgProjectsService", ProjectsService)
|
||||
|
|
|
@ -163,3 +163,16 @@ describe "tgProjectsService", ->
|
|||
])
|
||||
|
||||
done()
|
||||
|
||||
it "validateTransferToken", (done) ->
|
||||
projectId = 3
|
||||
|
||||
tokenValidation = Immutable.fromJS({})
|
||||
|
||||
mocks.resources.projects = {}
|
||||
mocks.resources.projects.transferValidateToken = sinon.stub()
|
||||
mocks.resources.projects.transferValidateToken.withArgs(projectId).promise().resolve(tokenValidation)
|
||||
|
||||
projectsService.transferValidateToken(projectId).then (projects) ->
|
||||
expect(projects.toJS()).to.be.eql({})
|
||||
done()
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
###
|
||||
# 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: cant-own-project-explanation.directive.coffee
|
||||
###
|
||||
|
||||
CantOwnProjectExplanationDirective = () ->
|
||||
return {
|
||||
templateUrl: "projects/transfer/cant-own-project-explanation.html"
|
||||
}
|
||||
|
||||
angular.module("taigaProjects").directive("tgCantOwnProjectExplanation", CantOwnProjectExplanationDirective)
|
|
@ -0,0 +1,3 @@
|
|||
p(
|
||||
translate="ADMIN.PROJECT_TRANSFER.CANT_BE_OWNED"
|
||||
)
|
|
@ -0,0 +1,4 @@
|
|||
tg-transfer-project.transfer-project(
|
||||
ng-if="vm.project"
|
||||
project = "vm.project"
|
||||
)
|
|
@ -0,0 +1,98 @@
|
|||
###
|
||||
# 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: transfer-project.directive.coffee
|
||||
###
|
||||
|
||||
module = angular.module('taigaProjects')
|
||||
|
||||
class TransferProject
|
||||
@.$inject = [
|
||||
"$routeParams",
|
||||
"tgProjectsService"
|
||||
"$location",
|
||||
"$tgAuth",
|
||||
"tgCurrentUserService",
|
||||
"$tgNavUrls",
|
||||
"$translate",
|
||||
"$tgConfirm"
|
||||
]
|
||||
|
||||
constructor: (@routeParams, @projectService, @location, @authService, @currentUserService, @navUrls, @translate, @confirmService) ->
|
||||
@.projectId = @.project.get("id")
|
||||
@.token = @routeParams.token
|
||||
@._refreshUserData()
|
||||
@.showAddComment = false
|
||||
|
||||
_validateToken: () ->
|
||||
@projectService.transferValidateToken(@.projectId, @.token).error (data, status) =>
|
||||
@location.path(@navUrls.resolve("not-found"))
|
||||
|
||||
_refreshUserData: () ->
|
||||
@authService.refresh().then () =>
|
||||
@._validateToken()
|
||||
@._setProjectData()
|
||||
@._checkOwnerData()
|
||||
|
||||
_setProjectData: () ->
|
||||
@.canBeOwnedByUser = @currentUserService.canOwnProject(@.project)
|
||||
|
||||
_checkOwnerData: () ->
|
||||
currentUser = @currentUserService.getUser()
|
||||
if(@.project.get('is_private'))
|
||||
@.ownerMessage = 'ADMIN.PROJECT_TRANSFER.OWNER_MESSAGE.PRIVATE'
|
||||
@.maxProjects = currentUser.get('max_private_projects')
|
||||
if @.maxProjects == null
|
||||
@.maxProjects = @translate.instant('ADMIN.PROJECT_TRANSFER.UNLIMITED_PROJECTS')
|
||||
@.currentProjects = currentUser.get('total_private_projects')
|
||||
maxMemberships = currentUser.get('max_memberships_private_projects')
|
||||
|
||||
else
|
||||
@.ownerMessage = 'ADMIN.PROJECT_TRANSFER.OWNER_MESSAGE.PUBLIC'
|
||||
@.maxProjects = currentUser.get('max_public_projects')
|
||||
if @.maxProjects == null
|
||||
@.maxProjects = @translate.instant('ADMIN.PROJECT_TRANSFER.UNLIMITED_PROJECTS')
|
||||
@.currentProjects = currentUser.get('total_public_projects')
|
||||
maxMemberships = currentUser.get('max_memberships_public_projects')
|
||||
|
||||
@.validNumberOfMemberships = maxMemberships == null || @.project.get('total_memberships') <= maxMemberships
|
||||
|
||||
transferAccept: (token, reason) ->
|
||||
@projectService.transferAccept(@.project.get("id"), token, reason).success () =>
|
||||
newUrl = @navUrls.resolve("project-admin-project-profile-details", {
|
||||
project: @.project.get("slug")
|
||||
})
|
||||
@location.path(newUrl)
|
||||
@confirmService.notify("success", @translate.instant("ADMIN.PROJECT_TRANSFER.ACCEPTED_PROJECT_OWNERNSHIP"), '', 5000)
|
||||
|
||||
transferReject: (token, reason) ->
|
||||
@projectService.transferReject(@.project.get("id"), token, reason).success () =>
|
||||
newUrl = @navUrls.resolve("project-admin-project-profile-details", {
|
||||
project: @project.get("slug")
|
||||
})
|
||||
@location.path(newUrl)
|
||||
@confirmService.notify("success", @translate.instant("ADMIN.PROJECT_TRANSFER.REJECTED_PROJECT_OWNERNSHIP"), '', 5000)
|
||||
|
||||
addComment: () ->
|
||||
@.showAddComment = true
|
||||
|
||||
hideComment: () ->
|
||||
@.showAddComment = false
|
||||
@.reason = ''
|
||||
|
||||
|
||||
|
||||
module.controller("TransferProjectController", TransferProject)
|
|
@ -0,0 +1,33 @@
|
|||
###
|
||||
# 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: transfer-project.directive.coffee
|
||||
###
|
||||
|
||||
module = angular.module('taigaProjects')
|
||||
|
||||
TransferProjectDirective = () ->
|
||||
return {
|
||||
scope: {},
|
||||
bindToController: {
|
||||
project: "="
|
||||
},
|
||||
templateUrl: "projects/transfer/transfer-project.html",
|
||||
controller: 'TransferProjectController',
|
||||
controllerAs: 'vm'
|
||||
}
|
||||
|
||||
module.directive('tgTransferProject', TransferProjectDirective)
|
|
@ -0,0 +1,69 @@
|
|||
.transfer-project-wrapper
|
||||
h2.transfer-title(translate="ADMIN.PROJECT_TRANSFER.DO_YOU_ACCEPT_PROJECT_OWNERNSHIP")
|
||||
.transfer-project-detail
|
||||
img.transfer-project-image(
|
||||
tg-project-logo-small-src="vm.project"
|
||||
alt="{{vm.project.get('name')}}"
|
||||
)
|
||||
.transfer-project-data
|
||||
h3.transfer-project-title {{::vm.project.get("name")}}
|
||||
.transfer-project-statistics
|
||||
span.transfer-project-private(ng-if="vm.project.get('is_private')")
|
||||
svg.icon.icon-lock
|
||||
use(xlink:href="#icon-lock")
|
||||
span(translate="ADMIN.PROJECT_TRANSFER.PRIVATE")
|
||||
span.transfer-project-members
|
||||
svg.icon.icon-team
|
||||
use(xlink:href="#icon-team")
|
||||
span {{::vm.project.get("members").size}}
|
||||
|
||||
p(
|
||||
translate="ADMIN.PROJECT_TRANSFER.PROPOSE_OWNERSHIP"
|
||||
translate-values="{owner: vm.project.getIn(['owner', 'full_name_display']), project: vm.project.get('name')}"
|
||||
)
|
||||
|
||||
div(ng-if="vm.canBeOwnedByUser.valid")
|
||||
|
||||
p(
|
||||
translate="{{vm.ownerMessage}}"
|
||||
translate-values="{maxProjects: vm.maxProjects, currentProjects: vm.currentProjects}"
|
||||
)
|
||||
|
||||
a.transfer-project-comment-link.ng-animate-disabled(
|
||||
href=""
|
||||
ng-click="vm.addComment()"
|
||||
ng-if="!vm.showAddComment"
|
||||
translate="ADMIN.PROJECT_TRANSFER.ADD_COMMENT_QUESTION"
|
||||
)
|
||||
|
||||
fieldset.transfer-project-comment-form(
|
||||
ng-if="vm.showAddComment"
|
||||
ng-class="{'open': vm.showAddComment}"
|
||||
)
|
||||
.transfer-project-comment-header
|
||||
label.transfer-project-comment-label(
|
||||
translate="ADMIN.PROJECT_TRANSFER.ADD_COMMENT"
|
||||
)
|
||||
svg.icon.icon-close(ng-click="vm.hideComment()")
|
||||
use(xlink:href="#icon-close")
|
||||
textarea.transfer-project-comment(
|
||||
name="reason"
|
||||
ng-model="vm.reason"
|
||||
)
|
||||
|
||||
.transfer-project-options
|
||||
a.button.button-gray(
|
||||
ng-click="vm.transferReject(vm.token, vm.reason)"
|
||||
href="#"
|
||||
title="{{'ADMIN.PROJECT_TRANSFER.REJECT' | translate}}"
|
||||
translate="ADMIN.PROJECT_TRANSFER.REJECT"
|
||||
)
|
||||
|
||||
a.button.button-green(
|
||||
ng-click="vm.transferAccept(vm.token, vm.reason)"
|
||||
href="#"
|
||||
title="{{'ADMIN.PROJECT_TRANSFER.ACCEPT' | translate}}"
|
||||
translate="ADMIN.PROJECT_TRANSFER.ACCEPT"
|
||||
)
|
||||
|
||||
div(ng-if="!vm.canBeOwnedByUser.valid", tg-cant-own-project-explanation)
|
|
@ -0,0 +1,93 @@
|
|||
.transfer-project-wrapper {
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.transfer-project {
|
||||
align-items: center;
|
||||
background: url('../images/discover.png') bottom center repeat-x;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-height: calc(100vh - 40px);
|
||||
.transfer-title {
|
||||
@extend %light;
|
||||
}
|
||||
&-detail {
|
||||
align-items: center;
|
||||
border-bottom: 1px solid $whitish;
|
||||
border-top: 1px solid $whitish;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
margin: 1rem 0 3rem;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
&-image {
|
||||
margin-right: 1rem;
|
||||
width: 4rem;
|
||||
}
|
||||
&-title {
|
||||
@extend %light;
|
||||
@extend %larger;
|
||||
margin-bottom: .25rem;
|
||||
}
|
||||
&-statistics {
|
||||
span {
|
||||
color: $gray-light;
|
||||
margin-right: .5rem;
|
||||
}
|
||||
svg {
|
||||
fill: $gray-light;
|
||||
margin-right: .25rem;
|
||||
}
|
||||
}
|
||||
&-private {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
&-comment-link {
|
||||
color: $primary;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
margin-bottom: 1rem;
|
||||
&:hover {
|
||||
color: $primary-light;
|
||||
}
|
||||
}
|
||||
&-comment-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.icon-close {
|
||||
cursor: pointer;
|
||||
fill: $gray-light;
|
||||
&:hover {
|
||||
fill: $red-light;
|
||||
transition: fill .2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
&-comment-form {
|
||||
&.ng-enter {
|
||||
animation: dropdownFade .2s;
|
||||
}
|
||||
}
|
||||
&-comment-label {
|
||||
display: block;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
&-comment {
|
||||
margin-bottom: 1rem;
|
||||
min-height: 6rem;
|
||||
}
|
||||
&-options {
|
||||
display: flex;
|
||||
a {
|
||||
@extend %large;
|
||||
display: block;
|
||||
flex: 1;
|
||||
padding: .75rem;
|
||||
&:first-child {
|
||||
margin-right: .5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -108,6 +108,42 @@ Resource = (urlsService, http, paginateResponseService) ->
|
|||
url = urlsService.resolve("project-unwatch", projectId)
|
||||
return http.post(url)
|
||||
|
||||
service.transferValidateToken = (projectId, token) ->
|
||||
data = {
|
||||
token: token
|
||||
}
|
||||
url = urlsService.resolve("project-transfer-validate-token", projectId)
|
||||
return http.post(url, data)
|
||||
|
||||
service.transferAccept = (projectId, token, reason) ->
|
||||
data = {
|
||||
token: token
|
||||
reason: reason
|
||||
}
|
||||
url = urlsService.resolve("project-transfer-accept", projectId)
|
||||
return http.post(url, data)
|
||||
|
||||
service.transferReject = (projectId, token, reason) ->
|
||||
data = {
|
||||
token: token
|
||||
reason: reason
|
||||
}
|
||||
url = urlsService.resolve("project-transfer-reject", projectId)
|
||||
return http.post(url, data)
|
||||
|
||||
service.transferRequest = (projectId) ->
|
||||
url = urlsService.resolve("project-transfer-request", projectId)
|
||||
return http.post(url)
|
||||
|
||||
service.transferStart = (projectId, userId, reason) ->
|
||||
data = {
|
||||
user: userId,
|
||||
reason: reason
|
||||
}
|
||||
|
||||
url = urlsService.resolve("project-transfer-start", projectId)
|
||||
return http.post(url, data)
|
||||
|
||||
return () ->
|
||||
return {"projects": service}
|
||||
|
||||
|
|
|
@ -120,8 +120,7 @@ class CurrentUserService
|
|||
|
||||
canCreatePrivateProjects: () ->
|
||||
user = @.getUser()
|
||||
|
||||
if user.get('max_private_projects') != null && user.get('max_private_projects') <= user.get('total_private_projects')
|
||||
if user.get('max_private_projects') != null && user.get('total_private_projects') >= user.get('max_private_projects')
|
||||
return {valid: false, reason: 'max_private_projects', type: 'private_project'}
|
||||
|
||||
return {valid: true}
|
||||
|
@ -129,9 +128,27 @@ class CurrentUserService
|
|||
canCreatePublicProjects: () ->
|
||||
user = @.getUser()
|
||||
|
||||
if user.get('max_public_projects') != null && user.get('max_public_projects') <= user.get('total_public_projects')
|
||||
if user.get('max_public_projects') != null && user.get('total_public_projects') >= user.get('max_public_projects')
|
||||
return {valid: false, reason: 'max_public_projects', type: 'public_project'}
|
||||
|
||||
return {valid: true}
|
||||
|
||||
canOwnProject: (project) ->
|
||||
user = @.getUser()
|
||||
if project.get('is_private')
|
||||
result = @.canCreatePrivateProjects()
|
||||
return result if !result.valid
|
||||
|
||||
if user.get('max_memberships_private_projects') != null && project.get('total_memberships') > user.get('max_memberships_private_projects')
|
||||
return {valid: false, reason: 'max_members_private_projects', type: 'private_project'}
|
||||
|
||||
else
|
||||
result = @.canCreatePublicProjects()
|
||||
return result if !result.valid
|
||||
|
||||
if user.get('max_memberships_public_projects') != null && project.get('total_memberships') > user.get('max_memberships_public_projects')
|
||||
return {valid: false, reason: 'max_members_public_projects', type: 'public_project'}
|
||||
|
||||
return {valid: true}
|
||||
|
||||
angular.module("taigaCommon").service("tgCurrentUserService", CurrentUserService)
|
||||
|
|
|
@ -273,3 +273,157 @@ describe "tgCurrentUserService", ->
|
|||
expect(result).to.be.eql({
|
||||
valid: true
|
||||
})
|
||||
|
||||
it "the user can own public project", () ->
|
||||
user = Immutable.fromJS({
|
||||
id: 1,
|
||||
name: "fake1",
|
||||
max_public_projects: 10,
|
||||
total_public_projects: 1,
|
||||
max_memberships_public_projects: 20
|
||||
})
|
||||
|
||||
currentUserService._user = user
|
||||
|
||||
project = Immutable.fromJS({
|
||||
id: 2,
|
||||
name: "fake2",
|
||||
total_memberships: 5,
|
||||
is_private: false
|
||||
})
|
||||
|
||||
result = currentUserService.canOwnProject(project)
|
||||
|
||||
expect(result).to.be.eql({
|
||||
valid: true
|
||||
})
|
||||
|
||||
it "the user can't own public project because of max projects", () ->
|
||||
user = Immutable.fromJS({
|
||||
id: 1,
|
||||
name: "fake1",
|
||||
max_public_projects: 1,
|
||||
total_public_projects: 1,
|
||||
max_memberships_public_projects: 20
|
||||
})
|
||||
|
||||
currentUserService._user = user
|
||||
|
||||
project = Immutable.fromJS({
|
||||
id: 2,
|
||||
name: "fake2",
|
||||
total_memberships: 5,
|
||||
is_private: false
|
||||
})
|
||||
|
||||
result = currentUserService.canOwnProject(project)
|
||||
|
||||
expect(result).to.be.eql({
|
||||
valid: false
|
||||
reason: 'max_public_projects'
|
||||
type: 'public_project'
|
||||
})
|
||||
|
||||
|
||||
it "the user can't own public project because of max memberships", () ->
|
||||
user = Immutable.fromJS({
|
||||
id: 1,
|
||||
name: "fake1",
|
||||
max_public_projects: 5,
|
||||
total_public_projects: 1,
|
||||
max_memberships_public_projects: 4
|
||||
})
|
||||
|
||||
currentUserService._user = user
|
||||
|
||||
project = Immutable.fromJS({
|
||||
id: 2,
|
||||
name: "fake2",
|
||||
total_memberships: 5,
|
||||
is_private: false
|
||||
})
|
||||
|
||||
result = currentUserService.canOwnProject(project)
|
||||
|
||||
expect(result).to.be.eql({
|
||||
valid: false
|
||||
reason: 'max_members_public_projects'
|
||||
type: 'public_project'
|
||||
})
|
||||
|
||||
it "the user can own private project", () ->
|
||||
user = Immutable.fromJS({
|
||||
id: 1,
|
||||
name: "fake1",
|
||||
max_private_projects: 10,
|
||||
total_private_projects: 1,
|
||||
max_memberships_private_projects: 20
|
||||
})
|
||||
|
||||
currentUserService._user = user
|
||||
|
||||
project = Immutable.fromJS({
|
||||
id: 2,
|
||||
name: "fake2",
|
||||
total_memberships: 5,
|
||||
is_private: true
|
||||
})
|
||||
|
||||
result = currentUserService.canOwnProject(project)
|
||||
|
||||
expect(result).to.be.eql({
|
||||
valid: true
|
||||
})
|
||||
|
||||
it "the user can't own private project because of max projects", () ->
|
||||
user = Immutable.fromJS({
|
||||
id: 1,
|
||||
name: "fake1",
|
||||
max_private_projects: 1,
|
||||
total_private_projects: 1,
|
||||
max_memberships_private_projects: 20
|
||||
})
|
||||
|
||||
currentUserService._user = user
|
||||
|
||||
project = Immutable.fromJS({
|
||||
id: 2,
|
||||
name: "fake2",
|
||||
total_memberships: 5,
|
||||
is_private: true
|
||||
})
|
||||
|
||||
result = currentUserService.canOwnProject(project)
|
||||
|
||||
expect(result).to.be.eql({
|
||||
valid: false
|
||||
reason: 'max_private_projects'
|
||||
type: 'private_project'
|
||||
})
|
||||
|
||||
|
||||
it "the user can't own private project because of max memberships", () ->
|
||||
user = Immutable.fromJS({
|
||||
id: 1,
|
||||
name: "fake1",
|
||||
max_private_projects: 10,
|
||||
total_private_projects: 1,
|
||||
max_memberships_private_projects: 4
|
||||
})
|
||||
|
||||
currentUserService._user = user
|
||||
|
||||
project = Immutable.fromJS({
|
||||
id: 2,
|
||||
name: "fake2",
|
||||
total_memberships: 5,
|
||||
is_private: true
|
||||
})
|
||||
|
||||
result = currentUserService.canOwnProject(project)
|
||||
|
||||
expect(result).to.be.eql({
|
||||
valid: false
|
||||
reason: 'max_members_private_projects'
|
||||
type: 'private_project'
|
||||
})
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
.owner-avatar
|
||||
img(ng-src="{{::owner.photo || '/#{v}/images/user-noimage.png'}}", alt="{{::owner.full_name_display}}")
|
||||
|
||||
.owner-info
|
||||
.owner-info-title {{ 'ADMIN.PROJECT_PROFILE.PROJECT_OWNER' | translate }}
|
||||
.owner-name {{::owner.full_name_display}}
|
||||
|
||||
a.request(href="", ng-click="changeOwner()") {{ 'ADMIN.PROJECT_PROFILE.CHANGE_OWNER' | translate }}
|
|
@ -78,6 +78,19 @@ div.wrapper(
|
|||
ng-model="project.tags"
|
||||
)
|
||||
|
||||
fieldset(ng-if="project.owner.id != user.id")
|
||||
tg-admin-project-request-ownership.admin-project-profile-owner-actions(
|
||||
owner="project.owner",
|
||||
project-id="project.id"
|
||||
)
|
||||
|
||||
fieldset(ng-if="project.owner.id == user.id")
|
||||
tg-admin-project-change-owner.admin-project-profile-owner-actions(
|
||||
owner="project.owner",
|
||||
project-id="project.id"
|
||||
active-users="activeUsers"
|
||||
)
|
||||
|
||||
fieldset.looking-for-people
|
||||
.looking-for-people-selector
|
||||
span {{ 'ADMIN.PROJECT_PROFILE.RECRUITING' | translate }}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
.owner-avatar
|
||||
img(ng-src="{{::owner.photo || '/#{v}/images/user-noimage.png'}}", alt="{{::owner.full_name_display}}")
|
||||
|
||||
.owner-info
|
||||
.title {{ 'ADMIN.PROJECT_PROFILE.PROJECT_OWNER' | translate }}
|
||||
.owner-name {{::owner.full_name_display}}
|
||||
|
||||
a.request(href="", ng-click="requestOwnership()") {{ 'ADMIN.PROJECT_PROFILE.REQUEST_OWNERSHIP' | translate }}
|
|
@ -0,0 +1,72 @@
|
|||
svg.close.icon.icon-close(href="", title="{{'COMMON.CLOSE' | translate}}")
|
||||
use(xlink:href="#icon-close")
|
||||
|
||||
.form
|
||||
h2.title(translate="LIGHTBOX.CHANGE_OWNER.TITLE")
|
||||
fieldset
|
||||
input(
|
||||
type="text",
|
||||
data-maxlength="500",
|
||||
placeholder="{{'LIGHTBOX.ASSIGNED_TO.SEARCH' | translate}}",
|
||||
ng-model="vm.q",
|
||||
ng-change="vm.userSearch()"
|
||||
)
|
||||
|
||||
.assigned-to-list
|
||||
.user-list-single.is-active(ng-if="vm.selected")
|
||||
.user-list-avatar
|
||||
a(
|
||||
href="#"
|
||||
title="{{'COMMON.ASSIGNED_TO.TITLE' | translate}}"
|
||||
)
|
||||
img(ng-src="{{vm.selected.photo}}")
|
||||
a.user-list-name(
|
||||
href=""
|
||||
title="{{vm.selected.full_name_display}}"
|
||||
) {{vm.selected.full_name_display}}
|
||||
|
||||
.user-list-single.ng-animate-disabled(
|
||||
ng-repeat="user in vm.getUsers()",
|
||||
ng-click="vm.selectUser(user)"
|
||||
)
|
||||
.user-list-avatar
|
||||
a(
|
||||
href="#"
|
||||
title="{{'COMMON.ASSIGNED_TO.TITLE' | translate}}"
|
||||
)
|
||||
img(ng-src="{{user.photo}}")
|
||||
a.user-list-name(
|
||||
href=""
|
||||
title="{{user.full_name_display}}"
|
||||
) {{user.full_name_display}}
|
||||
|
||||
.more-watchers(ng-if="!vm.q.length")
|
||||
span(translate="COMMON.ASSIGNED_TO.TOO_MANY")
|
||||
|
||||
.add-comment
|
||||
a(
|
||||
href="",
|
||||
class="ng-animate-disabled"
|
||||
ng-if="!vm.commentOpen",
|
||||
ng-click="vm.commentOpen = true"
|
||||
translate="LIGHTBOX.CHANGE_OWNER.ADD_COMMENT"
|
||||
)
|
||||
|
||||
fieldset(ng-if="vm.commentOpen")
|
||||
svg.icon.icon-close(
|
||||
ng-click="vm.commentOpen = false"
|
||||
href="",
|
||||
title="{{'COMMON.CLOSE' | translate}}"
|
||||
)
|
||||
use(xlink:href="#icon-close")
|
||||
label(translate="LIGHTBOX.CHANGE_OWNER.ADD_COMMENT")
|
||||
textarea(ng-model="vm.comment")
|
||||
|
||||
button.button-green.submit-button(
|
||||
tg-loading="vm.loading",
|
||||
ng-click="vm.submit()",
|
||||
ng-disabled="!vm.selected",
|
||||
type="submit",
|
||||
title="{{'LIGHTBOX.CHANGE_OWNER.BUTTON' | translate}}",
|
||||
translate="LIGHTBOX.CHANGE_OWNER.BUTTON"
|
||||
)
|
|
@ -0,0 +1,14 @@
|
|||
a.close(href="", title="{{'COMMON.CLOSE' | translate}}")
|
||||
svg.icon.icon-close
|
||||
use(xlink:href="#icon-close")
|
||||
|
||||
.content
|
||||
h2.title(translate="ADMIN.PROJECT_PROFILE.REQUEST_OWNERSHIP_CONFIRMATION_TITLE")
|
||||
p(translate="ADMIN.PROJECT_PROFILE.REQUEST_OWNERSHIP_DESC")
|
||||
|
||||
a.button-green(
|
||||
href="",
|
||||
ng-click="request()",
|
||||
tg-loading="loading"
|
||||
)
|
||||
span(translate="ADMIN.PROJECT_PROFILE.REQUEST_OWNERSHIP_BUTTON")
|
|
@ -33,13 +33,13 @@
|
|||
}
|
||||
&.disabled,
|
||||
&[disabled] {
|
||||
background: lighten($whitish, 10%);
|
||||
background: $whitish;
|
||||
box-shadow: none;
|
||||
color: $gray-light;
|
||||
cursor: not-allowed;
|
||||
opacity: .65;
|
||||
&:hover {
|
||||
background: lighten($whitish, 10%);
|
||||
background: $whitish;
|
||||
color: $gray-light;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,22 @@
|
|||
.notification-message-success {
|
||||
background: rgba($primary-light, .95);
|
||||
box-shadow: 0 25px 10px -15px rgba($black, .05);
|
||||
opacity: 1;
|
||||
right: -370px;
|
||||
top: 2%;
|
||||
transition: opacity .2s ease-in;
|
||||
width: 370px;
|
||||
&.active {
|
||||
animation: animSlide 2000ms linear both;
|
||||
animation: animSlide 2000ms;
|
||||
animation-fill-mode: forwards;
|
||||
animation-iteration-count: 1;
|
||||
opacity: 1;
|
||||
}
|
||||
&.inactive {
|
||||
animation: animSlideOut .5s;
|
||||
opacity: 0;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
@ -40,8 +47,11 @@
|
|||
20% { transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -413.214, 0, 0, 1); }
|
||||
27.23% { transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -403.135, 0, 0, 1); }
|
||||
38.34% { transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -399.585, 0, 0, 1); }
|
||||
60.56% { transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -400.01, 0, 0, 1); }
|
||||
82.78% { opacity: 1; transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -400, 0, 0, 1); }
|
||||
100% { transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -400, 0, 0, 1); }
|
||||
}
|
||||
|
||||
@keyframes animSlideOut {
|
||||
0% { opacity: 1; transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -400, 0, 0, 1); }
|
||||
100% { opacity: 0; transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -400, 0, 0, 1); }
|
||||
}
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
.user-list-single {
|
||||
&:hover,
|
||||
&.selected {
|
||||
background: lighten($primary, 58%);
|
||||
background: rgba(lighten($primary-light, 30%), .3);
|
||||
cursor: pointer;
|
||||
}
|
||||
&:hover {
|
||||
|
@ -41,7 +41,7 @@
|
|||
transition-delay: .2s;
|
||||
}
|
||||
&.is-active {
|
||||
background: lighten($primary, 55%);
|
||||
background: rgba(lighten($primary-light, 30%), .3);
|
||||
cursor: pointer;
|
||||
margin-bottom: 1rem;
|
||||
position: relative;
|
||||
|
|
|
@ -59,6 +59,21 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Drop Down element animations
|
||||
|
||||
@keyframes dropdownFade {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(-.25rem);
|
||||
}
|
||||
60% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
85% {
|
||||
opacity: 1;
|
||||
|
|
|
@ -96,6 +96,10 @@ em {
|
|||
font-style: italic;
|
||||
}
|
||||
|
||||
small {
|
||||
@extend %xsmall;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
top: 0;
|
||||
z-index: 99910;
|
||||
.close {
|
||||
@include svg-size(2rem);
|
||||
cursor: pointer;
|
||||
fill: $gray;
|
||||
position: absolute;
|
||||
|
@ -53,6 +54,7 @@
|
|||
.lb-icon {
|
||||
@include svg-size(6rem);
|
||||
display: block;
|
||||
fill: $whitish;
|
||||
margin: 1rem auto;
|
||||
}
|
||||
.title {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
@import '../dependencies/mixins/profile-form';
|
||||
|
||||
.project-details {
|
||||
@include profile-form;
|
||||
.looking-for-people {
|
||||
|
@ -31,7 +30,6 @@
|
|||
animation-delay: .1s;
|
||||
}
|
||||
}
|
||||
|
||||
.delete-project {
|
||||
@extend %xsmall;
|
||||
display: block;
|
||||
|
@ -52,9 +50,7 @@
|
|||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.project-privacy-settings {
|
||||
display: flex;
|
||||
margin-bottom: .5rem;
|
||||
|
@ -117,7 +113,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
tg-admin-project-restrictions {
|
||||
p {
|
||||
@extend %xsmall;
|
||||
|
@ -147,3 +142,36 @@ tg-admin-project-restrictions {
|
|||
}
|
||||
}
|
||||
}
|
||||
.admin-project-profile-owner-actions {
|
||||
align-items: center;
|
||||
border-top: 1px solid $whitish;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-top: 1rem;
|
||||
a {
|
||||
color: $primary;
|
||||
&:hover {
|
||||
color: $primary-light;
|
||||
transition: color .2s;
|
||||
}
|
||||
}
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
.owner-info {
|
||||
flex: 1;
|
||||
padding-left: .5rem;
|
||||
}
|
||||
.owner-info-title {
|
||||
color: $gray-light;
|
||||
}
|
||||
.owner-name {
|
||||
@extend %bold;
|
||||
}
|
||||
.owner-avatar {
|
||||
width: 2.5rem;
|
||||
}
|
||||
.request {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -504,7 +504,7 @@
|
|||
.user-list-single {
|
||||
&:hover,
|
||||
&.selected {
|
||||
background: lighten($primary, 58%);
|
||||
background: rgba(lighten($primary-light, 30%), .3);
|
||||
cursor: pointer;
|
||||
}
|
||||
&:hover {
|
||||
|
@ -518,6 +518,33 @@
|
|||
padding: .5rem;
|
||||
text-align: center;
|
||||
}
|
||||
.submit-button {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.add-comment {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
.icon-close {
|
||||
cursor: pointer;
|
||||
fill: $gray;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transition: fill .2s;
|
||||
&:hover {
|
||||
fill: $red-light;
|
||||
}
|
||||
svg {
|
||||
@include svg-size(2rem);
|
||||
}
|
||||
}
|
||||
textarea {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
a {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lb-create-edit-userstory {
|
||||
|
@ -573,3 +600,10 @@
|
|||
width: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
.lightbox-request-ownership {
|
||||
text-align: center;
|
||||
.content {
|
||||
width: 500px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,3 +25,52 @@ helper.editLogo = function() {
|
|||
helper.getLogoSrc = function() {
|
||||
return $('.image-container .image');
|
||||
};
|
||||
|
||||
helper.requestOwnershipLb = function() {
|
||||
return $('div[tg-lb-request-ownership]');
|
||||
};
|
||||
|
||||
helper.requestOwnership = function() {
|
||||
$('tg-admin-project-request-ownership .request').click();
|
||||
};
|
||||
|
||||
helper.changeOwner = function() {
|
||||
$('tg-admin-project-change-owner .request').click();
|
||||
};
|
||||
|
||||
helper.acceptRequestOwnership = function() {
|
||||
helper.requestOwnershipLb().$('.button-green').click();
|
||||
};
|
||||
|
||||
helper.changeOwnerSuccessLb = function() {
|
||||
return $('.lightbox-generic-success');
|
||||
};
|
||||
|
||||
helper.getChangeOwnerLb = function() {
|
||||
let el = $('div[tg-lb-change-owner]');
|
||||
|
||||
let obj = {
|
||||
el: el,
|
||||
waitOpen: function() {
|
||||
return utils.lightbox.open(el);
|
||||
},
|
||||
waitClose: function() {
|
||||
return utils.lightbox.close(el);
|
||||
},
|
||||
search: function(q) {
|
||||
el.$$('input').get(0).sendKeys(q);
|
||||
},
|
||||
select: function(index) {
|
||||
el.$$('.user-list-single').get(index).click();
|
||||
},
|
||||
addComment: function(text) {
|
||||
el.$('.add-comment a').click();
|
||||
el.$('textarea').sendKeys(text);
|
||||
},
|
||||
send: function() {
|
||||
el.$('.submit-button').click();
|
||||
}
|
||||
};
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
|
|
@ -78,4 +78,43 @@ describe('project detail', function() {
|
|||
|
||||
expect(src).to.contains('upload-image-test.png');
|
||||
});
|
||||
|
||||
it('request ownership', async function() {
|
||||
adminHelper.requestOwnership();
|
||||
|
||||
await utils.lightbox.open(adminHelper.requestOwnershipLb());
|
||||
|
||||
expect(utils.notifications.success.open()).to.be.eventually.true;
|
||||
});
|
||||
|
||||
it('change ownership', async function() {
|
||||
await utils.common.createProject(['user5@taigaio.demo']);
|
||||
|
||||
await utils.nav
|
||||
.init()
|
||||
.admin()
|
||||
.go();
|
||||
|
||||
adminHelper.changeOwner();
|
||||
|
||||
let lb = adminHelper.getChangeOwnerLb();
|
||||
|
||||
await lb.waitOpen();
|
||||
|
||||
lb.search('Alicia Flores');
|
||||
lb.select(0);
|
||||
lb.addComment('text');
|
||||
|
||||
utils.common.takeScreenshot('admin', 'project-transfer-lb');
|
||||
|
||||
lb.send();
|
||||
|
||||
let changeOwnerSuccessLb = adminHelper.changeOwnerSuccessLb();
|
||||
|
||||
await utils.lightbox.open(changeOwnerSuccessLb);
|
||||
|
||||
changeOwnerSuccessLb.$('.button-green').click();
|
||||
|
||||
await utils.lightbox.close(changeOwnerSuccessLb);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -435,3 +435,50 @@ common.closeJoyride = async function() {
|
|||
await browser.sleep(600);
|
||||
}
|
||||
};
|
||||
|
||||
common.createProject = async function(members = []) {
|
||||
var createProject = require('../helpers').createProject;
|
||||
var notifications = require('./notifications');
|
||||
|
||||
browser.get(browser.params.glob.host + 'projects/');
|
||||
await common.waitLoader();
|
||||
|
||||
let lb = createProject.createProjectLightbox();
|
||||
|
||||
createProject.openWizard();
|
||||
|
||||
await lb.waitOpen();
|
||||
|
||||
lb.name().sendKeys('aaa');
|
||||
|
||||
lb.description().sendKeys('bbb');
|
||||
|
||||
await lb.submit();
|
||||
|
||||
await notifications.success.open();
|
||||
await notifications.success.close();
|
||||
|
||||
if (members.length) {
|
||||
var adminMembershipsHelper = require('../helpers').adminMemberships;
|
||||
|
||||
let url = await browser.getCurrentUrl();
|
||||
url = url.split('/');
|
||||
url = browser.params.glob.host + '/project/' + url[4] + '/admin/memberships';
|
||||
|
||||
browser.get(url);
|
||||
await common.waitLoader();
|
||||
|
||||
let newMemberLightbox = adminMembershipsHelper.getNewMemberLightbox();
|
||||
adminMembershipsHelper.openNewMemberLightbox();
|
||||
|
||||
await newMemberLightbox.waitOpen();
|
||||
|
||||
for(var i = 0; i < members.length; i++) {
|
||||
newMemberLightbox.newEmail(members[i]);
|
||||
}
|
||||
|
||||
newMemberLightbox.submit();
|
||||
|
||||
await newMemberLightbox.waitClose();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -46,6 +46,11 @@ var actions = {
|
|||
browser.get(browser.params.glob.host);
|
||||
return common.waitLoader();
|
||||
},
|
||||
admin: async function() {
|
||||
await common.link($('#nav-admin a'));
|
||||
|
||||
return common.waitLoader();
|
||||
},
|
||||
taskboard: async function(index) {
|
||||
let link = $$('.sprints .button-gray').get(index);
|
||||
|
||||
|
@ -92,6 +97,10 @@ var nav = {
|
|||
this.actions.push(actions.home.bind(null));
|
||||
return this;
|
||||
},
|
||||
admin: function() {
|
||||
this.actions.push(actions.admin.bind(null));
|
||||
return this;
|
||||
},
|
||||
taskboard: function(index) {
|
||||
this.actions.push(actions.taskboard.bind(null, index));
|
||||
return this;
|
||||
|
|
Loading…
Reference in New Issue