diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ba53a2d..9a528eec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/app/coffee/app.coffee b/app/coffee/app.coffee index d0a750f5..8d0b88de 100644 --- a/app/coffee/app.coffee +++ b/app/coffee/app.coffee @@ -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"}) diff --git a/app/coffee/modules/admin/lightboxes.coffee b/app/coffee/modules/admin/lightboxes.coffee index 8030262f..f28aba65 100644 --- a/app/coffee/modules/admin/lightboxes.coffee +++ b/app/coffee/modules/admin/lightboxes.coffee @@ -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]) diff --git a/app/coffee/modules/admin/memberships.coffee b/app/coffee/modules/admin/memberships.coffee index be2a7ad9..a85ae6e2 100644 --- a/app/coffee/modules/admin/memberships.coffee +++ b/app/coffee/modules/admin/memberships.coffee @@ -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) diff --git a/app/coffee/modules/admin/project-profile.coffee b/app/coffee/modules/admin/project-profile.coffee index 2d385c6e..8b0d64e4 100644 --- a/app/coffee/modules/admin/project-profile.coffee +++ b/app/coffee/modules/admin/project-profile.coffee @@ -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]) diff --git a/app/coffee/modules/common/confirm.coffee b/app/coffee/modules/common/confirm.coffee index 868920bc..713162d9 100644 --- a/app/coffee/modules/common/confirm.coffee +++ b/app/coffee/modules/common/confirm.coffee @@ -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 = $('').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 = $('').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 diff --git a/app/coffee/modules/resources.coffee b/app/coffee/modules/resources.coffee index a4a45eb9..c32a9f56 100644 --- a/app/coffee/modules/resources.coffee +++ b/app/coffee/modules/resources.coffee @@ -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" diff --git a/app/locales/taiga/locale-en.json b/app/locales/taiga/locale-en.json index 2283333c..936d704f 100644 --- a/app/locales/taiga/locale-en.json +++ b/app/locales/taiga/locale-en.json @@ -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": "{{owner}}, the current owner of the project {{project}} 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 {{maxProjects}} private projects and you already own {{currentProjects}} private projects", + "PUBLIC": "Remember, you can own {{maxProjects}} public projects and you already own {{currentProjects}} 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": { diff --git a/app/modules/projects/projects.service.coffee b/app/modules/projects/projects.service.coffee index 5aca481e..ab3973a3 100644 --- a/app/modules/projects/projects.service.coffee +++ b/app/modules/projects/projects.service.coffee @@ -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) diff --git a/app/modules/projects/projects.service.spec.coffee b/app/modules/projects/projects.service.spec.coffee index 6b49303b..1829e8a8 100644 --- a/app/modules/projects/projects.service.spec.coffee +++ b/app/modules/projects/projects.service.spec.coffee @@ -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() diff --git a/app/modules/projects/transfer/cant-own-project-explanation.directive.coffee b/app/modules/projects/transfer/cant-own-project-explanation.directive.coffee new file mode 100644 index 00000000..a6ea655c --- /dev/null +++ b/app/modules/projects/transfer/cant-own-project-explanation.directive.coffee @@ -0,0 +1,25 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: cant-own-project-explanation.directive.coffee +### + +CantOwnProjectExplanationDirective = () -> + return { + templateUrl: "projects/transfer/cant-own-project-explanation.html" + } + +angular.module("taigaProjects").directive("tgCantOwnProjectExplanation", CantOwnProjectExplanationDirective) diff --git a/app/modules/projects/transfer/cant-own-project-explanation.jade b/app/modules/projects/transfer/cant-own-project-explanation.jade new file mode 100644 index 00000000..6f6be740 --- /dev/null +++ b/app/modules/projects/transfer/cant-own-project-explanation.jade @@ -0,0 +1,3 @@ +p( + translate="ADMIN.PROJECT_TRANSFER.CANT_BE_OWNED" +) diff --git a/app/modules/projects/transfer/transfer-page.jade b/app/modules/projects/transfer/transfer-page.jade new file mode 100644 index 00000000..bb9dad96 --- /dev/null +++ b/app/modules/projects/transfer/transfer-page.jade @@ -0,0 +1,4 @@ +tg-transfer-project.transfer-project( + ng-if="vm.project" + project = "vm.project" +) diff --git a/app/modules/projects/transfer/transfer-project.controller.coffee b/app/modules/projects/transfer/transfer-project.controller.coffee new file mode 100644 index 00000000..41fbd976 --- /dev/null +++ b/app/modules/projects/transfer/transfer-project.controller.coffee @@ -0,0 +1,98 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: 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) diff --git a/app/modules/projects/transfer/transfer-project.directive.coffee b/app/modules/projects/transfer/transfer-project.directive.coffee new file mode 100644 index 00000000..d6b35f76 --- /dev/null +++ b/app/modules/projects/transfer/transfer-project.directive.coffee @@ -0,0 +1,33 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: 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) diff --git a/app/modules/projects/transfer/transfer-project.jade b/app/modules/projects/transfer/transfer-project.jade new file mode 100644 index 00000000..cc184317 --- /dev/null +++ b/app/modules/projects/transfer/transfer-project.jade @@ -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) diff --git a/app/modules/projects/transfer/transfer-project.scss b/app/modules/projects/transfer/transfer-project.scss new file mode 100644 index 00000000..99921eb5 --- /dev/null +++ b/app/modules/projects/transfer/transfer-project.scss @@ -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; + } + } + } +} diff --git a/app/modules/resources/projects-resource.service.coffee b/app/modules/resources/projects-resource.service.coffee index d18fc96e..226534ff 100644 --- a/app/modules/resources/projects-resource.service.coffee +++ b/app/modules/resources/projects-resource.service.coffee @@ -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} diff --git a/app/modules/services/current-user.service.coffee b/app/modules/services/current-user.service.coffee index 57b0db0f..60985801 100644 --- a/app/modules/services/current-user.service.coffee +++ b/app/modules/services/current-user.service.coffee @@ -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) diff --git a/app/modules/services/current-user.service.spec.coffee b/app/modules/services/current-user.service.spec.coffee index 36646f48..fba99a7f 100644 --- a/app/modules/services/current-user.service.spec.coffee +++ b/app/modules/services/current-user.service.spec.coffee @@ -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' + }) diff --git a/app/partials/admin/admin-project-change-owner.jade b/app/partials/admin/admin-project-change-owner.jade new file mode 100644 index 00000000..25c442ab --- /dev/null +++ b/app/partials/admin/admin-project-change-owner.jade @@ -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 }} diff --git a/app/partials/admin/admin-project-profile.jade b/app/partials/admin/admin-project-profile.jade index b85cec4f..36cf992d 100644 --- a/app/partials/admin/admin-project-profile.jade +++ b/app/partials/admin/admin-project-profile.jade @@ -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 }} diff --git a/app/partials/admin/admin-project-request-ownership.jade b/app/partials/admin/admin-project-request-ownership.jade new file mode 100644 index 00000000..796f9cc3 --- /dev/null +++ b/app/partials/admin/admin-project-request-ownership.jade @@ -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 }} \ No newline at end of file diff --git a/app/partials/common/lightbox/lightbox-assigned-to.jade b/app/partials/common/lightbox/lightbox-assigned-to.jade index b3947857..d97ab443 100644 --- a/app/partials/common/lightbox/lightbox-assigned-to.jade +++ b/app/partials/common/lightbox/lightbox-assigned-to.jade @@ -1,6 +1,6 @@ svg.close.icon.icon-close(href="", title="{{'COMMON.CLOSE' | translate}}") use(xlink:href="#icon-close") - + div.form h2.title(translate="LIGHTBOX.ASSIGNED_TO.SELECT") fieldset diff --git a/app/partials/common/lightbox/lightbox-change-owner.jade b/app/partials/common/lightbox/lightbox-change-owner.jade new file mode 100644 index 00000000..8fe0a128 --- /dev/null +++ b/app/partials/common/lightbox/lightbox-change-owner.jade @@ -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" + ) diff --git a/app/partials/common/lightbox/lightbox-request-ownership.jade b/app/partials/common/lightbox/lightbox-request-ownership.jade new file mode 100644 index 00000000..54247951 --- /dev/null +++ b/app/partials/common/lightbox/lightbox-request-ownership.jade @@ -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") diff --git a/app/styles/components/buttons.scss b/app/styles/components/buttons.scss index 1be18ede..66d0b13a 100755 --- a/app/styles/components/buttons.scss +++ b/app/styles/components/buttons.scss @@ -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; } } diff --git a/app/styles/components/notification-message.scss b/app/styles/components/notification-message.scss index 4a79a15d..33585da6 100644 --- a/app/styles/components/notification-message.scss +++ b/app/styles/components/notification-message.scss @@ -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); } } diff --git a/app/styles/components/user-list.scss b/app/styles/components/user-list.scss index 01771bc1..e39f8016 100644 --- a/app/styles/components/user-list.scss +++ b/app/styles/components/user-list.scss @@ -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; diff --git a/app/styles/core/animation.scss b/app/styles/core/animation.scss index 50bc216c..74d4cd56 100644 --- a/app/styles/core/animation.scss +++ b/app/styles/core/animation.scss @@ -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; diff --git a/app/styles/core/typography.scss b/app/styles/core/typography.scss index 18b984e4..85c74703 100755 --- a/app/styles/core/typography.scss +++ b/app/styles/core/typography.scss @@ -96,6 +96,10 @@ em { font-style: italic; } +small { + @extend %xsmall; +} + strong { font-weight: bold; } diff --git a/app/styles/dependencies/helpers.scss b/app/styles/dependencies/helpers.scss index d6d4d8aa..f9080da8 100644 --- a/app/styles/dependencies/helpers.scss +++ b/app/styles/dependencies/helpers.scss @@ -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 { diff --git a/app/styles/modules/admin/admin-project-profile.scss b/app/styles/modules/admin/admin-project-profile.scss index 50562431..c15a8f18 100644 --- a/app/styles/modules/admin/admin-project-profile.scss +++ b/app/styles/modules/admin/admin-project-profile.scss @@ -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; + } +} diff --git a/app/styles/modules/common/lightbox.scss b/app/styles/modules/common/lightbox.scss index f4d63c68..26bf2945 100644 --- a/app/styles/modules/common/lightbox.scss +++ b/app/styles/modules/common/lightbox.scss @@ -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; + } +} diff --git a/e2e/helpers/project-detail-helper.js b/e2e/helpers/project-detail-helper.js index 096dab63..efc676f5 100644 --- a/e2e/helpers/project-detail-helper.js +++ b/e2e/helpers/project-detail-helper.js @@ -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; +}; diff --git a/e2e/suites/admin/project/project-detail.e2e.js b/e2e/suites/admin/project/project-detail.e2e.js index 2ebb94a1..90e1a468 100644 --- a/e2e/suites/admin/project/project-detail.e2e.js +++ b/e2e/suites/admin/project/project-detail.e2e.js @@ -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); + }); }); diff --git a/e2e/utils/common.js b/e2e/utils/common.js index ae8846ec..9f2efdd7 100644 --- a/e2e/utils/common.js +++ b/e2e/utils/common.js @@ -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(); + } +}; diff --git a/e2e/utils/nav.js b/e2e/utils/nav.js index ba663509..b5733c56 100644 --- a/e2e/utils/nav.js +++ b/e2e/utils/nav.js @@ -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;