Suggest/invite members
parent
d41eaa07cc
commit
160bff7783
13
.travis.yml
13
.travis.yml
|
@ -1,12 +1,17 @@
|
||||||
language: node_js
|
language: node_js
|
||||||
|
dist: trusty
|
||||||
node_js:
|
node_js:
|
||||||
- "node"
|
- "node"
|
||||||
before_install:
|
before_install:
|
||||||
- export CHROME_BIN=chromium-browser
|
- sudo apt-get update
|
||||||
- export DISPLAY=:99.0
|
- sudo apt-get install -y libappindicator1 fonts-liberation
|
||||||
- sh -e /etc/init.d/xvfb start
|
- wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
||||||
- travis_retry npm install -g gulp
|
- sudo dpkg -i google-chrome*.deb
|
||||||
install:
|
install:
|
||||||
- travis_retry npm install
|
- travis_retry npm install
|
||||||
before_script:
|
before_script:
|
||||||
|
- export CHROME_BIN=/usr/bin/google-chrome
|
||||||
|
- export DISPLAY=:99.0
|
||||||
|
- sh -e /etc/init.d/xvfb start
|
||||||
|
- travis_retry npm install -g gulp
|
||||||
- gulp deploy
|
- gulp deploy
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
- Add rich text custom fields (with a wysiwyg editor like descreption or comments).
|
- Add rich text custom fields (with a wysiwyg editor like descreption or comments).
|
||||||
- Add thumbnails and preview for PSD files.
|
- Add thumbnails and preview for PSD files.
|
||||||
- Add thumbnails and preview for SVG files.
|
- Add thumbnails and preview for SVG files.
|
||||||
|
- Improve add-members form: Now users can select between their contacts or type an email.
|
||||||
- i18n:
|
- i18n:
|
||||||
- Add japanese (ja) translation.
|
- Add japanese (ja) translation.
|
||||||
- Add korean (ko) translation.
|
- Add korean (ko) translation.
|
||||||
|
|
|
@ -27,112 +27,6 @@ debounce = @.taiga.debounce
|
||||||
|
|
||||||
module = angular.module("taigaKanban")
|
module = angular.module("taigaKanban")
|
||||||
|
|
||||||
#############################################################################
|
|
||||||
## Create Members Lightbox Directive
|
|
||||||
#############################################################################
|
|
||||||
|
|
||||||
class LightboxAddMembersController
|
|
||||||
@.$inject = [
|
|
||||||
"$scope",
|
|
||||||
"lightboxService",
|
|
||||||
"tgLoader",
|
|
||||||
"$tgConfirm",
|
|
||||||
"$tgResources",
|
|
||||||
"$rootScope",
|
|
||||||
]
|
|
||||||
|
|
||||||
constructor: (@scope, @lightboxService, @tgLoader, @confirm, @rs, @rootScope) ->
|
|
||||||
@._defaultMaxInvites = 4
|
|
||||||
@._defaultRole = @.project.roles[0].id
|
|
||||||
@.form = null
|
|
||||||
@.submitInvites = false
|
|
||||||
@.canAddUsers = true
|
|
||||||
@.memberInvites = []
|
|
||||||
|
|
||||||
if @.project.max_memberships == null
|
|
||||||
@.membersLimit = @._defaultMaxInvites
|
|
||||||
else
|
|
||||||
pendingMembersCount = Math.max(@.project.max_memberships - @.project.total_memberships, 0)
|
|
||||||
@.membersLimit = Math.min(pendingMembersCount, @._defaultMaxInvites)
|
|
||||||
|
|
||||||
@.addSingleMember()
|
|
||||||
|
|
||||||
addSingleMember: () ->
|
|
||||||
@.memberInvites.push({email:'', role_id: @._defaultRole})
|
|
||||||
|
|
||||||
if @.memberInvites.length >= @.membersLimit
|
|
||||||
@.canAddUsers = false
|
|
||||||
@.showWarningMessage = (!@.canAddUsers &&
|
|
||||||
@.project.total_memberships + @.memberInvites.length == @.project.max_memberships)
|
|
||||||
|
|
||||||
removeSingleMember: (index) ->
|
|
||||||
@.memberInvites.splice(index, 1)
|
|
||||||
|
|
||||||
@.canAddUsers = true
|
|
||||||
@.showWarningMessage = @.membersLimit == 1
|
|
||||||
|
|
||||||
submit: () ->
|
|
||||||
# Need to reset the form constrains
|
|
||||||
@.form.initializeFields()
|
|
||||||
@.form.reset()
|
|
||||||
return if not @.form.validate()
|
|
||||||
|
|
||||||
@.memberInvites = _.filter(@.memberInvites, (invites) ->
|
|
||||||
invites.email != "")
|
|
||||||
|
|
||||||
@.submitInvites = true
|
|
||||||
promise = @rs.memberships.bulkCreateMemberships(
|
|
||||||
@.project.id,
|
|
||||||
@.memberInvites,
|
|
||||||
@.invitationText
|
|
||||||
)
|
|
||||||
promise.then(
|
|
||||||
@._onSuccessInvite.bind(this),
|
|
||||||
@._onErrorInvite.bind(this)
|
|
||||||
)
|
|
||||||
|
|
||||||
_onSuccessInvite: () ->
|
|
||||||
@.submitInvites = false
|
|
||||||
@rootScope.$broadcast("membersform:new:success")
|
|
||||||
@lightboxService.closeAll()
|
|
||||||
@confirm.notify("success")
|
|
||||||
|
|
||||||
_onErrorInvite: (response) ->
|
|
||||||
@.submitInvites = false
|
|
||||||
errors = {}
|
|
||||||
_.each response.data.bulk_memberships, (value, index) =>
|
|
||||||
if value.email
|
|
||||||
errors["email-#{index}"] = value.email[0]
|
|
||||||
if value.role
|
|
||||||
errors["role-#{index}"] = value.role[0]
|
|
||||||
|
|
||||||
@.form.setErrors(errors)
|
|
||||||
if response.data._error_message
|
|
||||||
@confirm.notify("error", response.data._error_message)
|
|
||||||
|
|
||||||
module.controller("LbAddMembersController", LightboxAddMembersController)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
LightboxAddMembersDirective = (lightboxService) ->
|
|
||||||
link = (scope, el, attrs, ctrl) ->
|
|
||||||
lightboxService.open(el)
|
|
||||||
ctrl.form = el.find("form").checksley()
|
|
||||||
|
|
||||||
return {
|
|
||||||
scope: {},
|
|
||||||
bindToController: {
|
|
||||||
project: '=',
|
|
||||||
},
|
|
||||||
controller: 'LbAddMembersController',
|
|
||||||
controllerAs: 'vm',
|
|
||||||
templateUrl: 'admin/lightbox-add-members.html',
|
|
||||||
link: link
|
|
||||||
}
|
|
||||||
|
|
||||||
module.directive("tgLbAddMembers", ["lightboxService", LightboxAddMembersDirective])
|
|
||||||
|
|
||||||
|
|
||||||
#############################################################################
|
#############################################################################
|
||||||
## Warning message directive
|
## Warning message directive
|
||||||
#############################################################################
|
#############################################################################
|
||||||
|
|
|
@ -222,6 +222,8 @@ _.mixin
|
||||||
isImage = (name) ->
|
isImage = (name) ->
|
||||||
return name.match(/\.(jpe?g|png|gif|gifv|webm|svg|psd)/i) != null
|
return name.match(/\.(jpe?g|png|gif|gifv|webm|svg|psd)/i) != null
|
||||||
|
|
||||||
|
isEmail = (name) ->
|
||||||
|
return name? and name.match(/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/) != null
|
||||||
|
|
||||||
isPdf = (name) ->
|
isPdf = (name) ->
|
||||||
return name.match(/\.(pdf)/i) != null
|
return name.match(/\.(pdf)/i) != null
|
||||||
|
@ -286,6 +288,7 @@ taiga.stripTags = stripTags
|
||||||
taiga.replaceTags = replaceTags
|
taiga.replaceTags = replaceTags
|
||||||
taiga.defineImmutableProperty = defineImmutableProperty
|
taiga.defineImmutableProperty = defineImmutableProperty
|
||||||
taiga.isImage = isImage
|
taiga.isImage = isImage
|
||||||
|
taiga.isEmail = isEmail
|
||||||
taiga.isPdf = isPdf
|
taiga.isPdf = isPdf
|
||||||
taiga.patch = patch
|
taiga.patch = patch
|
||||||
taiga.getRandomDefaultColor = getRandomDefaultColor
|
taiga.getRandomDefaultColor = getRandomDefaultColor
|
||||||
|
|
|
@ -990,6 +990,12 @@
|
||||||
},
|
},
|
||||||
"ADD_MEMBER": {
|
"ADD_MEMBER": {
|
||||||
"TITLE": "New Member",
|
"TITLE": "New Member",
|
||||||
|
"PLACEHOLDER": "Filter users or write an email to invite",
|
||||||
|
"ADD_EMAIL": "Add email",
|
||||||
|
"REMOVE": "Remove",
|
||||||
|
"INVITE": "Invite",
|
||||||
|
"CHOOSE_ROLE": "Choose a role",
|
||||||
|
"PLACEHOLDER_INVITATION_TEXT": "(Optional) Add a personalized text to the invitation. Tell something lovely to your new members ;-)",
|
||||||
"HELP_TEXT": "If users are already registered on Taiga, they will be added automatically. Otherwise they will receive an invitation."
|
"HELP_TEXT": "If users are already registered on Taiga, they will be added automatically. Otherwise they will receive an invitation."
|
||||||
},
|
},
|
||||||
"CREATE_ISSUE": {
|
"CREATE_ISSUE": {
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# File: add-members.controller.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
taiga = @.taiga
|
||||||
|
|
||||||
|
|
||||||
|
class InviteMembersFormController
|
||||||
|
@.$inject = [
|
||||||
|
"tgProjectService",
|
||||||
|
"$tgResources",
|
||||||
|
"lightboxService",
|
||||||
|
"$tgConfirm",
|
||||||
|
"$rootScope"
|
||||||
|
]
|
||||||
|
|
||||||
|
constructor: (@projectService, @rs, @lightboxService, @confirm, @rootScope) ->
|
||||||
|
@.project = @projectService.project
|
||||||
|
@.roles = @projectService.project.get('roles')
|
||||||
|
@.rolesValues = {}
|
||||||
|
@.loading = false
|
||||||
|
@.defaultMaxInvites = 4
|
||||||
|
|
||||||
|
_areRolesValidated: () ->
|
||||||
|
Object.defineProperty @, 'areRolesValidated', {
|
||||||
|
get: () =>
|
||||||
|
roleIds = _.filter Object.values(@.rolesValues), (it) -> return it
|
||||||
|
return roleIds.length == @.contactsToInvite.size + @.emailsToInvite.size
|
||||||
|
}
|
||||||
|
|
||||||
|
_checkLimitMemberships: () ->
|
||||||
|
if @.project.get('max_memberships') == null
|
||||||
|
@.membersLimit = @.defaultMaxInvites
|
||||||
|
else
|
||||||
|
pendingMembersCount = Math.max(@.project.get('max_memberships') - @.project.get('total_memberships'), 0)
|
||||||
|
@.membersLimit = Math.min(pendingMembersCount, @.defaultMaxInvites)
|
||||||
|
|
||||||
|
@.showWarningMessage = @.membersLimit < @.defaultMaxInvites
|
||||||
|
|
||||||
|
sendInvites: () ->
|
||||||
|
@.setInvitedContacts = []
|
||||||
|
_.forEach(@.rolesValues, (key, value) =>
|
||||||
|
@.setInvitedContacts.push({
|
||||||
|
'role_id': key
|
||||||
|
'username': value
|
||||||
|
})
|
||||||
|
)
|
||||||
|
@.loading = true
|
||||||
|
@rs.memberships.bulkCreateMemberships(
|
||||||
|
@.project.get('id'),
|
||||||
|
@.setInvitedContacts,
|
||||||
|
@.inviteContactsMessage
|
||||||
|
)
|
||||||
|
.then (response) => # On success
|
||||||
|
@.loading = false
|
||||||
|
@lightboxService.closeAll()
|
||||||
|
@rootScope.$broadcast("membersform:new:success")
|
||||||
|
@confirm.notify('success')
|
||||||
|
.catch (response) => # On error
|
||||||
|
@.loading = false
|
||||||
|
if response.data._error_message
|
||||||
|
@confirm.notify("error", response.data._error_message)
|
||||||
|
|
||||||
|
|
||||||
|
angular.module("taigaAdmin").controller("InviteMembersFormCtrl", InviteMembersFormController)
|
|
@ -0,0 +1,134 @@
|
||||||
|
###
|
||||||
|
# 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: invite-members-form.controller.spec.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
describe "InviteMembersFormController", ->
|
||||||
|
inviteMembersFormCtrl = null
|
||||||
|
provide = null
|
||||||
|
controller = null
|
||||||
|
mocks = {}
|
||||||
|
|
||||||
|
_mockProjectService = () ->
|
||||||
|
mocks.projectService = {
|
||||||
|
project: sinon.stub()
|
||||||
|
}
|
||||||
|
|
||||||
|
provide.value "tgProjectService", mocks.projectService
|
||||||
|
|
||||||
|
_mockTgResources = () ->
|
||||||
|
mocks.tgResources = {
|
||||||
|
memberships: {
|
||||||
|
bulkCreateMemberships: sinon.stub()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provide.value "$tgResources", mocks.tgResources
|
||||||
|
|
||||||
|
_mockLightboxService = () ->
|
||||||
|
mocks.lightboxService = {
|
||||||
|
closeAll: sinon.stub()
|
||||||
|
}
|
||||||
|
|
||||||
|
provide.value "lightboxService", mocks.lightboxService
|
||||||
|
|
||||||
|
_mockTgConfirm = () ->
|
||||||
|
mocks.tgConfirm = {
|
||||||
|
notify: sinon.stub()
|
||||||
|
}
|
||||||
|
|
||||||
|
provide.value "$tgConfirm", mocks.tgConfirm
|
||||||
|
|
||||||
|
_mockRootScope = ->
|
||||||
|
mocks.rootScope = {
|
||||||
|
$broadcast: sinon.stub()
|
||||||
|
}
|
||||||
|
|
||||||
|
provide.value("$rootScope", mocks.rootScope)
|
||||||
|
|
||||||
|
_mocks = () ->
|
||||||
|
module ($provide) ->
|
||||||
|
provide = $provide
|
||||||
|
_mockProjectService()
|
||||||
|
_mockTgResources()
|
||||||
|
_mockLightboxService()
|
||||||
|
_mockTgConfirm()
|
||||||
|
_mockRootScope()
|
||||||
|
return null
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
module "taigaAdmin"
|
||||||
|
|
||||||
|
_mocks()
|
||||||
|
|
||||||
|
inject ($controller) ->
|
||||||
|
controller = $controller
|
||||||
|
|
||||||
|
mocks.projectService.project = Immutable.fromJS([{
|
||||||
|
'roles': 'role1'
|
||||||
|
}])
|
||||||
|
|
||||||
|
it "check limit memberships - no limit", () ->
|
||||||
|
inviteMembersFormCtrl = controller "InviteMembersFormCtrl"
|
||||||
|
|
||||||
|
inviteMembersFormCtrl.project = Immutable.fromJS({
|
||||||
|
'max_memberships': null,
|
||||||
|
})
|
||||||
|
|
||||||
|
inviteMembersFormCtrl.defaultMaxInvites = 4
|
||||||
|
|
||||||
|
inviteMembersFormCtrl._checkLimitMemberships()
|
||||||
|
expect(inviteMembersFormCtrl.membersLimit).to.be.equal(4)
|
||||||
|
expect(inviteMembersFormCtrl.showWarningMessage).to.be.false
|
||||||
|
|
||||||
|
it "check limit memberships", () ->
|
||||||
|
inviteMembersFormCtrl = controller "InviteMembersFormCtrl"
|
||||||
|
|
||||||
|
inviteMembersFormCtrl.project = Immutable.fromJS({
|
||||||
|
'max_memberships': 15,
|
||||||
|
'total_memberships': 13
|
||||||
|
})
|
||||||
|
inviteMembersFormCtrl.defaultMaxInvites = 4
|
||||||
|
|
||||||
|
inviteMembersFormCtrl._checkLimitMemberships()
|
||||||
|
expect(inviteMembersFormCtrl.membersLimit).to.be.equal(2)
|
||||||
|
expect(inviteMembersFormCtrl.showWarningMessage).to.be.true
|
||||||
|
|
||||||
|
|
||||||
|
it "send invites", (done) ->
|
||||||
|
inviteMembersFormCtrl = controller "InviteMembersFormCtrl"
|
||||||
|
inviteMembersFormCtrl.project = Immutable.fromJS(
|
||||||
|
{'id': 1}
|
||||||
|
)
|
||||||
|
inviteMembersFormCtrl.rolesValues = {'user1': 1}
|
||||||
|
inviteMembersFormCtrl.inviteContactsMessage = 'Message'
|
||||||
|
inviteMembersFormCtrl.loading = true
|
||||||
|
|
||||||
|
promise = mocks.tgResources.memberships.bulkCreateMemberships.withArgs(
|
||||||
|
1,
|
||||||
|
[{
|
||||||
|
'role_id': 1
|
||||||
|
'username': 'user1'
|
||||||
|
}],
|
||||||
|
'Message'
|
||||||
|
).promise().resolve()
|
||||||
|
|
||||||
|
inviteMembersFormCtrl.sendInvites().then () ->
|
||||||
|
expect(inviteMembersFormCtrl.loading).to.be.false
|
||||||
|
expect(mocks.rootScope.$broadcast).to.have.been.calledWith("membersform:new:success")
|
||||||
|
expect(mocks.tgConfirm.notify).to.have.been.calledWith("success")
|
||||||
|
done()
|
|
@ -0,0 +1,41 @@
|
||||||
|
###
|
||||||
|
# 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: invite-members.directive.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
InviteMembersFormDirective = () ->
|
||||||
|
link = (scope, el, attrs, ctrl) ->
|
||||||
|
ctrl._areRolesValidated()
|
||||||
|
ctrl._checkLimitMemberships()
|
||||||
|
|
||||||
|
return {
|
||||||
|
scope: {},
|
||||||
|
templateUrl:"invite-members/invite-members-form/invite-members-form.html",
|
||||||
|
controller: "InviteMembersFormCtrl",
|
||||||
|
controllerAs: "vm",
|
||||||
|
bindToController: {
|
||||||
|
contactsToInvite: '<',
|
||||||
|
emailsToInvite: '=',
|
||||||
|
onDisplayContactList: '&',
|
||||||
|
onRemoveInvitedContact: '&',
|
||||||
|
onRemoveInvitedEmail: '&',
|
||||||
|
onSendInvites: '&'
|
||||||
|
},
|
||||||
|
link: link
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module("taigaAdmin").directive("tgInviteMembersForm", InviteMembersFormDirective)
|
|
@ -0,0 +1,69 @@
|
||||||
|
form.invite-members-form(ng-submit="vm.sendInvites(vm.inviteContacts)")
|
||||||
|
ul.invite-members-form-list
|
||||||
|
li.invite-members-single.e2e-invite-members-single(
|
||||||
|
ng-repeat="contact in vm.contactsToInvite | toMutable track by contact.id"
|
||||||
|
)
|
||||||
|
.invite-members-single-data
|
||||||
|
img.invite-members-single-avatar(
|
||||||
|
tg-avatar="contact"
|
||||||
|
alt="{{contact.full_name}}"
|
||||||
|
)
|
||||||
|
span.invite-members-single-name {{contact.full_name}}
|
||||||
|
a.invite-members-single-remove.e2e-invite-members-single-remove(
|
||||||
|
href=""
|
||||||
|
ng-click="vm.onRemoveInvitedContact({contact: contact})"
|
||||||
|
translate="LIGHTBOX.ADD_MEMBER.REMOVE"
|
||||||
|
)
|
||||||
|
select.invite-members-single-role.e2e-invite-members-single-role(
|
||||||
|
ng-model="vm.rolesValues[contact.username]"
|
||||||
|
id="add-member-suggest-role-dropdown"
|
||||||
|
ng-options="role.id as role.name for role in vm.roles | toMutable track by role.id"
|
||||||
|
required
|
||||||
|
)
|
||||||
|
option(
|
||||||
|
value=""
|
||||||
|
selected="selected"
|
||||||
|
translate="LIGHTBOX.ADD_MEMBER.CHOOSE_ROLE"
|
||||||
|
)
|
||||||
|
li.invite-members-single.e2e-invite-members-single(
|
||||||
|
ng-repeat="userMail in vm.emailsToInvite | toMutable"
|
||||||
|
)
|
||||||
|
.invite-members-single-data
|
||||||
|
span.invite-members-single-email {{userMail.email}}
|
||||||
|
a.invite-members-single-remove.e2e-invite-members-single-remove(
|
||||||
|
href=""
|
||||||
|
ng-click="vm.onRemoveInvitedEmail({email: userMail})"
|
||||||
|
translate="LIGHTBOX.ADD_MEMBER.REMOVE"
|
||||||
|
)
|
||||||
|
select.invite-members-single-role.e2e-invite-members-single-role(
|
||||||
|
ng-model="vm.rolesValues[userMail.email]"
|
||||||
|
id="add-email-suggest-role-dropdown"
|
||||||
|
ng-options="role.id as role.name for role in vm.roles | toMutable track by role.id"
|
||||||
|
required
|
||||||
|
)
|
||||||
|
option(
|
||||||
|
value=""
|
||||||
|
translate="LIGHTBOX.ADD_MEMBER.CHOOSE_ROLE"
|
||||||
|
)
|
||||||
|
.invite-members-single-new.e2e-invite-members-single-new(
|
||||||
|
ng-if="vm.contactsToInvite.size + vm.emailsToInvite.size < vm.membersLimit"
|
||||||
|
)
|
||||||
|
tg-svg.invite-members-single-new-btn(
|
||||||
|
svg-icon="icon-add"
|
||||||
|
ng-click="vm.onDisplayContactList()"
|
||||||
|
)
|
||||||
|
tg-lightbox-add-members-warning-message(
|
||||||
|
ng-if="vm.showWarningMessage"
|
||||||
|
project="vm.project"
|
||||||
|
)
|
||||||
|
textarea.invite-members-single-msg(
|
||||||
|
ng-model="vm.inviteContactsMessage"
|
||||||
|
placeholder="{{'LIGHTBOX.ADD_MEMBER.PLACEHOLDER_INVITATION_TEXT' | translate}}"
|
||||||
|
)
|
||||||
|
button.button-green.invite-members-single-send.e2e-invite-members-single-send(
|
||||||
|
type="submit"
|
||||||
|
translate="LIGHTBOX.ADD_MEMBER.INVITE"
|
||||||
|
ng-disabled="!vm.areRolesValidated"
|
||||||
|
tg-loading="vm.loading"
|
||||||
|
)
|
||||||
|
p.invite-members-single-help(translate="LIGHTBOX.ADD_MEMBER.HELP_TEXT")
|
|
@ -0,0 +1,67 @@
|
||||||
|
.invite-members-form {
|
||||||
|
border-top: 1px solid $whitish;
|
||||||
|
margin: 0 5rem;
|
||||||
|
.invite-members-form-list {
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.invite-members-single {
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid $whitish;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.invite-members-single-data {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.invite-members-single-avatar {
|
||||||
|
height: 4rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
width: 4rem;
|
||||||
|
}
|
||||||
|
.invite-members-single-remove {
|
||||||
|
color: $red-light;
|
||||||
|
margin-left: 1rem;
|
||||||
|
transition: color .2s;
|
||||||
|
&:hover {
|
||||||
|
color: $red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.invite-members-single-role {
|
||||||
|
flex-basis: 40%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.invite-members-single-new {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem 0;
|
||||||
|
.invite-members-single-new-btn {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.icon-add {
|
||||||
|
@include svg-size(2rem);
|
||||||
|
fill: $grayer;
|
||||||
|
transition: fill .2s;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
.icon-add {
|
||||||
|
fill: $primary-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.invite-members-single-send {
|
||||||
|
@include font-size(large);
|
||||||
|
display: block;
|
||||||
|
margin: 1.5rem 0 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.invite-members-single-help {
|
||||||
|
@include font-size(small);
|
||||||
|
@include font-type(light);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# File: add-members.controller.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
taiga = @.taiga
|
||||||
|
|
||||||
|
class AddMembersController
|
||||||
|
@.$inject = [
|
||||||
|
"tgUserService",
|
||||||
|
"tgCurrentUserService",
|
||||||
|
"tgProjectService",
|
||||||
|
]
|
||||||
|
|
||||||
|
constructor: (@userService, @currentUserService, @projectService) ->
|
||||||
|
@.contactsToInvite = Immutable.List()
|
||||||
|
@.emailsToInvite = Immutable.List()
|
||||||
|
@.displayContactList = false
|
||||||
|
|
||||||
|
_getContacts: () ->
|
||||||
|
userId = @currentUserService.getUser().get("id")
|
||||||
|
excludeProjectId = @projectService.project.get("id")
|
||||||
|
|
||||||
|
@userService.getContacts(userId, excludeProjectId).then (contacts) =>
|
||||||
|
@.contacts = contacts
|
||||||
|
|
||||||
|
_filterContacts: (invited) ->
|
||||||
|
@.contacts = @.contacts.filter( (contact) =>
|
||||||
|
contact.get('id') != invited.get('id')
|
||||||
|
)
|
||||||
|
|
||||||
|
inviteSuggested: (contact) ->
|
||||||
|
@.contactsToInvite = @.contactsToInvite.push(contact)
|
||||||
|
@._filterContacts(contact)
|
||||||
|
@.displayContactList = true
|
||||||
|
|
||||||
|
removeContact: (invited) ->
|
||||||
|
@.contactsToInvite = @.contactsToInvite.filter( (contact) =>
|
||||||
|
return contact.get('id') != invited.id
|
||||||
|
)
|
||||||
|
invited = Immutable.fromJS(invited)
|
||||||
|
@.contacts = @.contacts.push(invited)
|
||||||
|
@.testEmptyContacts()
|
||||||
|
|
||||||
|
inviteEmail: (email) ->
|
||||||
|
emailData = Immutable.Map({'email': email})
|
||||||
|
@.emailsToInvite = @.emailsToInvite.push(emailData)
|
||||||
|
@.displayContactList = true
|
||||||
|
|
||||||
|
removeEmail: (invited) ->
|
||||||
|
@.emailsToInvite = @.emailsToInvite.filter( (email) =>
|
||||||
|
return email.get('email') != invited.email
|
||||||
|
)
|
||||||
|
@.testEmptyContacts()
|
||||||
|
|
||||||
|
testEmptyContacts: () ->
|
||||||
|
if @.emailsToInvite.size + @.contactsToInvite.size == 0
|
||||||
|
@.displayContactList = false
|
||||||
|
|
||||||
|
angular.module("taigaAdmin").controller("AddMembersCtrl", AddMembersController)
|
|
@ -0,0 +1,180 @@
|
||||||
|
###
|
||||||
|
# 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: lightbox-add-members.controller.spec.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
describe "AddMembersController", ->
|
||||||
|
addMembersCtrl = null
|
||||||
|
provide = null
|
||||||
|
controller = null
|
||||||
|
mocks = {}
|
||||||
|
|
||||||
|
_mockUserService = () ->
|
||||||
|
mocks.userService = {
|
||||||
|
getContacts: sinon.stub()
|
||||||
|
}
|
||||||
|
|
||||||
|
provide.value "tgUserService", mocks.userService
|
||||||
|
|
||||||
|
_mockCurrentUser = () ->
|
||||||
|
mocks.currentUser = {
|
||||||
|
getUser: sinon.stub()
|
||||||
|
}
|
||||||
|
|
||||||
|
provide.value "tgCurrentUserService", mocks.currentUser
|
||||||
|
|
||||||
|
_mockProjectService = () ->
|
||||||
|
mocks.projectService = {
|
||||||
|
project: sinon.stub()
|
||||||
|
}
|
||||||
|
|
||||||
|
provide.value "tgProjectService", mocks.projectService
|
||||||
|
|
||||||
|
_mocks = () ->
|
||||||
|
module ($provide) ->
|
||||||
|
provide = $provide
|
||||||
|
_mockCurrentUser()
|
||||||
|
_mockUserService()
|
||||||
|
_mockProjectService()
|
||||||
|
return null
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
module "taigaAdmin"
|
||||||
|
|
||||||
|
_mocks()
|
||||||
|
|
||||||
|
inject ($controller) ->
|
||||||
|
controller = $controller
|
||||||
|
|
||||||
|
|
||||||
|
it "get user contacts", (done) ->
|
||||||
|
|
||||||
|
userId = 1
|
||||||
|
excludeProjectId = 1
|
||||||
|
|
||||||
|
mocks.currentUser.getUser.returns(Immutable.fromJS({
|
||||||
|
id: userId
|
||||||
|
}))
|
||||||
|
mocks.projectService.project = Immutable.fromJS({
|
||||||
|
id: excludeProjectId
|
||||||
|
})
|
||||||
|
|
||||||
|
contacts = Immutable.fromJS({
|
||||||
|
username: "username",
|
||||||
|
full_name_display: "full-name-display",
|
||||||
|
bio: "bio"
|
||||||
|
})
|
||||||
|
|
||||||
|
mocks.userService.getContacts.withArgs(userId, excludeProjectId).promise().resolve(contacts)
|
||||||
|
|
||||||
|
addMembersCtrl = controller "AddMembersCtrl"
|
||||||
|
|
||||||
|
addMembersCtrl._getContacts().then () ->
|
||||||
|
expect(addMembersCtrl.contacts).to.be.equal(contacts)
|
||||||
|
done()
|
||||||
|
|
||||||
|
it "filterContacts", () ->
|
||||||
|
|
||||||
|
addMembersCtrl = controller "AddMembersCtrl"
|
||||||
|
addMembersCtrl.contacts = Immutable.fromJS([
|
||||||
|
{id: 1}
|
||||||
|
{id: 2}
|
||||||
|
])
|
||||||
|
invited = Immutable.fromJS({id: 1})
|
||||||
|
|
||||||
|
addMembersCtrl._filterContacts(invited)
|
||||||
|
|
||||||
|
expect(addMembersCtrl.contacts.size).to.be.equal(1)
|
||||||
|
|
||||||
|
it "invite suggested", () ->
|
||||||
|
addMembersCtrl = controller "AddMembersCtrl"
|
||||||
|
addMembersCtrl.contactsToInvite = Immutable.List()
|
||||||
|
addMembersCtrl.displayContactList = false
|
||||||
|
|
||||||
|
contact = Immutable.fromJS({id: 1})
|
||||||
|
|
||||||
|
addMembersCtrl._filterContacts = sinon.stub()
|
||||||
|
|
||||||
|
addMembersCtrl.inviteSuggested(contact)
|
||||||
|
expect(addMembersCtrl.contactsToInvite.size).to.be.equal(1)
|
||||||
|
expect(addMembersCtrl._filterContacts).to.be.calledWith(contact)
|
||||||
|
expect(addMembersCtrl.displayContactList).to.be.true
|
||||||
|
|
||||||
|
it "remove contact", () ->
|
||||||
|
addMembersCtrl = controller "AddMembersCtrl"
|
||||||
|
addMembersCtrl.contactsToInvite = Immutable.fromJS([
|
||||||
|
{id: 1}
|
||||||
|
{id: 2}
|
||||||
|
])
|
||||||
|
invited = {id: 1}
|
||||||
|
addMembersCtrl.contacts = Immutable.fromJS([])
|
||||||
|
|
||||||
|
addMembersCtrl.testEmptyContacts = sinon.stub()
|
||||||
|
|
||||||
|
addMembersCtrl.removeContact(invited)
|
||||||
|
expect(addMembersCtrl.contactsToInvite.size).to.be.equal(1)
|
||||||
|
expect(addMembersCtrl.contacts.size).to.be.equal(1)
|
||||||
|
expect(addMembersCtrl.testEmptyContacts).to.be.called
|
||||||
|
|
||||||
|
it "invite email", () ->
|
||||||
|
addMembersCtrl = controller "AddMembersCtrl"
|
||||||
|
email = 'email@example.com'
|
||||||
|
emailData = Immutable.Map({'email': email})
|
||||||
|
addMembersCtrl.displayContactList = false
|
||||||
|
|
||||||
|
addMembersCtrl.emailsToInvite = Immutable.fromJS([])
|
||||||
|
|
||||||
|
addMembersCtrl.inviteEmail(email)
|
||||||
|
expect(emailData.get('email')).to.be.equal(email)
|
||||||
|
expect(addMembersCtrl.emailsToInvite.size).to.be.equal(1)
|
||||||
|
expect(addMembersCtrl.displayContactList).to.be.true
|
||||||
|
|
||||||
|
it "remove email", () ->
|
||||||
|
addMembersCtrl = controller "AddMembersCtrl"
|
||||||
|
invited = {email: 'email@example.com'}
|
||||||
|
addMembersCtrl.emailsToInvite = Immutable.fromJS([
|
||||||
|
{'email': 'email@example.com'}
|
||||||
|
{'email': 'email@example2.com'}
|
||||||
|
])
|
||||||
|
|
||||||
|
addMembersCtrl.testEmptyContacts = sinon.stub()
|
||||||
|
|
||||||
|
addMembersCtrl.removeEmail(invited)
|
||||||
|
expect(addMembersCtrl.emailsToInvite.size).to.be.equal(1)
|
||||||
|
expect(addMembersCtrl.testEmptyContacts).to.be.called
|
||||||
|
|
||||||
|
it "test empty contacts - not empty", () ->
|
||||||
|
addMembersCtrl = controller "AddMembersCtrl"
|
||||||
|
addMembersCtrl.displayContactList = true
|
||||||
|
addMembersCtrl.emailsToInvite = Immutable.fromJS([
|
||||||
|
{'email': 'email@example.com'}
|
||||||
|
{'email': 'email@example2.com'}
|
||||||
|
])
|
||||||
|
addMembersCtrl.contactsToInvite = Immutable.fromJS([
|
||||||
|
{'id': 1}
|
||||||
|
{'id': 1}
|
||||||
|
])
|
||||||
|
addMembersCtrl.testEmptyContacts()
|
||||||
|
expect(addMembersCtrl.displayContactList).to.be.true
|
||||||
|
|
||||||
|
it "test empty contacts - empty", () ->
|
||||||
|
addMembersCtrl = controller "AddMembersCtrl"
|
||||||
|
addMembersCtrl.displayContactList = true
|
||||||
|
addMembersCtrl.emailsToInvite = Immutable.fromJS([])
|
||||||
|
addMembersCtrl.contactsToInvite = Immutable.fromJS([])
|
||||||
|
addMembersCtrl.testEmptyContacts()
|
||||||
|
expect(addMembersCtrl.displayContactList).to.be.false
|
|
@ -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: add-member.directive.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
LightboxAddMembersDirective = (lightboxService) ->
|
||||||
|
link = (scope, el, attrs, ctrl) ->
|
||||||
|
lightboxService.open(el)
|
||||||
|
ctrl._getContacts()
|
||||||
|
|
||||||
|
return {
|
||||||
|
scope: {},
|
||||||
|
templateUrl:"invite-members/lightbox-add-members.html",
|
||||||
|
controller: "AddMembersCtrl",
|
||||||
|
controllerAs: "vm",
|
||||||
|
link: link
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module("taigaAdmin").directive("tgLbAddMembers", ["lightboxService", LightboxAddMembersDirective])
|
|
@ -0,0 +1,18 @@
|
||||||
|
tg-lightbox-close
|
||||||
|
.add-members-wrapper
|
||||||
|
h2.title(translate="LIGHTBOX.ADD_MEMBER.TITLE")
|
||||||
|
tg-suggest-add-members(
|
||||||
|
ng-show="!vm.displayContactList"
|
||||||
|
contacts="vm.contacts"
|
||||||
|
on-invite-suggested="vm.inviteSuggested(contact)"
|
||||||
|
on-invite-email="vm.inviteEmail(email)"
|
||||||
|
)
|
||||||
|
tg-invite-members-form(
|
||||||
|
ng-show="vm.displayContactList"
|
||||||
|
on-display-contact-list="vm.displayContactList = false"
|
||||||
|
contacts-to-invite="vm.contactsToInvite"
|
||||||
|
emails-to-invite="vm.emailsToInvite"
|
||||||
|
on-remove-invited-contact="vm.removeContact(contact)"
|
||||||
|
on-remove-invited-email="vm.removeEmail(email)"
|
||||||
|
on-send-invites="vm.submit(invites)"
|
||||||
|
)
|
|
@ -0,0 +1,6 @@
|
||||||
|
.lightbox-add-member {
|
||||||
|
.add-members-wrapper {
|
||||||
|
max-width: 900px;
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# File: suggest-add-members.controller.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
taiga = @.taiga
|
||||||
|
|
||||||
|
class SuggestAddMembersController
|
||||||
|
@.$inject = []
|
||||||
|
|
||||||
|
constructor: () ->
|
||||||
|
@.contactQuery = ""
|
||||||
|
|
||||||
|
isEmail: () ->
|
||||||
|
return taiga.isEmail(@.contactQuery)
|
||||||
|
|
||||||
|
filterContacts: () ->
|
||||||
|
@.filteredContacts = @.contacts.filter( (contact) =>
|
||||||
|
contact.get('full_name_display').toLowerCase().includes(@.contactQuery.toLowerCase()) || contact.get('username').toLowerCase().includes(@.contactQuery.toLowerCase());
|
||||||
|
)
|
||||||
|
|
||||||
|
setInvited: (contact) ->
|
||||||
|
@.onInviteSuggested({'contact': contact})
|
||||||
|
|
||||||
|
angular.module("taigaAdmin").controller("SuggestAddMembersCtrl", SuggestAddMembersController)
|
|
@ -0,0 +1,79 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# File: suggest-add-members.controller.spec.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
describe "SuggestAddMembersController", ->
|
||||||
|
suggestAddMembersCtrl = null
|
||||||
|
provide = null
|
||||||
|
controller = null
|
||||||
|
mocks = {}
|
||||||
|
|
||||||
|
_mocks = () ->
|
||||||
|
module ($provide) ->
|
||||||
|
provide = $provide
|
||||||
|
return null
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
module "taigaAdmin"
|
||||||
|
|
||||||
|
_mocks()
|
||||||
|
|
||||||
|
inject ($controller) ->
|
||||||
|
controller = $controller
|
||||||
|
|
||||||
|
it "is email - wrong", () ->
|
||||||
|
suggestAddMembersCtrl = controller "SuggestAddMembersCtrl"
|
||||||
|
suggestAddMembersCtrl.contactQuery = 'lololo'
|
||||||
|
|
||||||
|
result = suggestAddMembersCtrl.isEmail()
|
||||||
|
expect(result).to.be.false
|
||||||
|
|
||||||
|
it "is email - true", () ->
|
||||||
|
suggestAddMembersCtrl = controller "SuggestAddMembersCtrl"
|
||||||
|
suggestAddMembersCtrl.contactQuery = 'lololo@lolo.com'
|
||||||
|
|
||||||
|
result = suggestAddMembersCtrl.isEmail()
|
||||||
|
expect(result).to.be.true
|
||||||
|
|
||||||
|
it "filter contacts", () ->
|
||||||
|
suggestAddMembersCtrl = controller "SuggestAddMembersCtrl"
|
||||||
|
suggestAddMembersCtrl.contacts = Immutable.fromJS([
|
||||||
|
{
|
||||||
|
full_name_display: 'Abel Sonofadan'
|
||||||
|
username: 'abel'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
full_name_display: 'Cain Sonofadan'
|
||||||
|
username: 'cain'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
suggestAddMembersCtrl.contactQuery = 'Cain Sonofadan'
|
||||||
|
|
||||||
|
suggestAddMembersCtrl.filterContacts()
|
||||||
|
expect(suggestAddMembersCtrl.filteredContacts.size).to.be.equal(1)
|
||||||
|
|
||||||
|
it "set invited", () ->
|
||||||
|
suggestAddMembersCtrl = controller "SuggestAddMembersCtrl"
|
||||||
|
|
||||||
|
contact = 'contact'
|
||||||
|
|
||||||
|
suggestAddMembersCtrl.onInviteSuggested = sinon.stub()
|
||||||
|
|
||||||
|
suggestAddMembersCtrl.setInvited(contact)
|
||||||
|
expect(suggestAddMembersCtrl.onInviteSuggested).has.been.calledWith({'contact': contact})
|
|
@ -0,0 +1,34 @@
|
||||||
|
###
|
||||||
|
# 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: suggest-add-member.directive.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
SuggestAddMembersDirective = (lightboxService) ->
|
||||||
|
return {
|
||||||
|
scope: {},
|
||||||
|
templateUrl:"invite-members/suggest-add-members/suggest-add-members.html",
|
||||||
|
controller: "SuggestAddMembersCtrl",
|
||||||
|
controllerAs: "vm",
|
||||||
|
bindToController: {
|
||||||
|
contacts: '=',
|
||||||
|
filteredContacts: '<contacts',
|
||||||
|
onInviteSuggested: '&',
|
||||||
|
onInviteEmail: '&'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module("taigaAdmin").directive("tgSuggestAddMembers", SuggestAddMembersDirective)
|
|
@ -0,0 +1,31 @@
|
||||||
|
.add-member-suggest
|
||||||
|
form.add-member-suggest-filter
|
||||||
|
input.add-member-suggest-filter-input(
|
||||||
|
type="text"
|
||||||
|
ng-model="vm.contactQuery"
|
||||||
|
placeholder="{{'LIGHTBOX.ADD_MEMBER.PLACEHOLDER' | translate}}"
|
||||||
|
ng-keyup="vm.filterContacts()"
|
||||||
|
)
|
||||||
|
|
||||||
|
span.add-member-suggest-filter-hint(
|
||||||
|
ng-if="!vm.filteredContacts.size"
|
||||||
|
ng-class="{'to-send': vm.isEmail()}"
|
||||||
|
translate="LIGHTBOX.ADD_MEMBER.ADD_EMAIL"
|
||||||
|
)
|
||||||
|
|
||||||
|
button.add-member-suggest-filter-addmail.e2e-add-member-suggest-filter-addmail(
|
||||||
|
ng-click="vm.onInviteEmail({email: vm.contactQuery})"
|
||||||
|
ng-if="vm.isEmail()"
|
||||||
|
)
|
||||||
|
tg-svg(svg-icon="icon-add-user")
|
||||||
|
|
||||||
|
ul.add-member-suggest-list
|
||||||
|
li.add-member-suggest-single.e2e-add-member-suggest-single(
|
||||||
|
tg-repeat="contact in vm.filteredContacts"
|
||||||
|
ng-click="vm.setInvited(contact)"
|
||||||
|
)
|
||||||
|
img.add-member-suggest-avatar(
|
||||||
|
tg-avatar="contact"
|
||||||
|
alt="{{contact.get('full_name_display')}}"
|
||||||
|
)
|
||||||
|
span.add-member-suggest-name {{contact.get('full_name_display')}}
|
|
@ -0,0 +1,78 @@
|
||||||
|
.add-member-suggest {
|
||||||
|
.add-member-suggest-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin: 2rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-member-suggest-filter {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
padding: 0 15rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-member-suggest-filter-input {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-member-suggest-filter-hint {
|
||||||
|
@include font-size(xsmall);
|
||||||
|
color: $gray-light;
|
||||||
|
position: absolute;
|
||||||
|
right: 16rem;
|
||||||
|
top: .5rem;
|
||||||
|
&.to-send {
|
||||||
|
right: 19rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-member-suggest-filter-addmail {
|
||||||
|
background: $grayer;
|
||||||
|
border-radius: .25rem;
|
||||||
|
padding: .5rem .75rem;
|
||||||
|
transition: background .2s linear;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $blackish;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
@include svg-size(1.3rem);
|
||||||
|
fill: $white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-member-suggest-single {
|
||||||
|
align-items: center;
|
||||||
|
background: $white;
|
||||||
|
border-bottom: 1px solid $whitish;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-basis: calc(25% - 1rem);
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 1rem;
|
||||||
|
padding: .2rem;
|
||||||
|
transition: .2s linear;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba($primary-light, .1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(4n) {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-member-suggest-avatar {
|
||||||
|
height: 5rem;
|
||||||
|
margin: .5rem;
|
||||||
|
width: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-member-suggest-name {
|
||||||
|
@include font-type(light);
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,16 +50,19 @@ Resource = (urlsService, http, paginateResponseService) ->
|
||||||
.then (result) ->
|
.then (result) ->
|
||||||
return Immutable.fromJS(result.data)
|
return Immutable.fromJS(result.data)
|
||||||
|
|
||||||
service.getContacts = (userId) ->
|
service.getContacts = (userId, excludeProjectId) ->
|
||||||
url = urlsService.resolve("user-contacts", userId)
|
url = urlsService.resolve("user-contacts", userId)
|
||||||
|
|
||||||
|
params = {}
|
||||||
|
params.exclude_project = excludeProjectId if excludeProjectId?
|
||||||
|
|
||||||
httpOptions = {
|
httpOptions = {
|
||||||
headers: {
|
headers: {
|
||||||
"x-disable-pagination": "1"
|
"x-disable-pagination": "1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return http.get(url, {}, httpOptions)
|
return http.get(url, params, httpOptions)
|
||||||
.then (result) ->
|
.then (result) ->
|
||||||
return Immutable.fromJS(result.data)
|
return Immutable.fromJS(result.data)
|
||||||
|
|
||||||
|
|
|
@ -30,8 +30,8 @@ class UserService extends taiga.Service
|
||||||
getUserByUserName: (username) ->
|
getUserByUserName: (username) ->
|
||||||
return @rs.users.getUserByUsername(username)
|
return @rs.users.getUserByUsername(username)
|
||||||
|
|
||||||
getContacts: (userId) ->
|
getContacts: (userId, excludeProjectId) ->
|
||||||
return @rs.users.getContacts(userId)
|
return @rs.users.getContacts(userId, excludeProjectId)
|
||||||
|
|
||||||
getLiked: (userId, pageNumber, objectType, textQuery) ->
|
getLiked: (userId, pageNumber, objectType, textQuery) ->
|
||||||
return @rs.users.getLiked(userId, pageNumber, objectType, textQuery)
|
return @rs.users.getLiked(userId, pageNumber, objectType, textQuery)
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
p.member-limit-warning(
|
p.member-limit-warning(
|
||||||
ng-if="project.i_am_owner == true"
|
ng-if="project.get('i_am_owner') == true"
|
||||||
translate="LIGHTBOX.CREATE_MEMBER.LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER"
|
translate="LIGHTBOX.CREATE_MEMBER.LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER"
|
||||||
translate-values="{maxMembers: project.max_memberships}"
|
translate-values="{maxMembers: project.get('max_memberships')}"
|
||||||
)
|
)
|
||||||
|
|
||||||
p.member-limit-warning(
|
p.member-limit-warning(
|
||||||
ng-if="project.i_am_owner == false"
|
ng-if="project.get('i_am_owner') == false"
|
||||||
translate="LIGHTBOX.CREATE_MEMBER.LIMIT_USERS_WARNING_MESSAGE"
|
translate="LIGHTBOX.CREATE_MEMBER.LIMIT_USERS_WARNING_MESSAGE"
|
||||||
translate-values="{maxMembers: project.max_memberships}"
|
translate-values="{maxMembers: project.get('max_memberships')}"
|
||||||
)
|
)
|
||||||
|
|
|
@ -143,83 +143,6 @@
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.lightbox-add-member {
|
|
||||||
.add-member-wrapper {
|
|
||||||
max-width: 600px;
|
|
||||||
width: 90%;
|
|
||||||
}
|
|
||||||
.add-single-member {
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: .5rem;
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
fieldset {
|
|
||||||
display: inline-block;
|
|
||||||
flex: 1;
|
|
||||||
margin: 0 .5rem 0 0;
|
|
||||||
&:last-child {
|
|
||||||
flex-basis: 30px;
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
&:first-child {
|
|
||||||
flex-basis: 20%;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.icon {
|
|
||||||
@include svg-size(1.25rem);
|
|
||||||
fill: $gray;
|
|
||||||
margin-left: .5rem;
|
|
||||||
}
|
|
||||||
.icon-add {
|
|
||||||
&:hover {
|
|
||||||
fill: $primary;
|
|
||||||
transition: fill .2s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.icon-trash {
|
|
||||||
fill: $red-light;
|
|
||||||
&:hover {
|
|
||||||
fill: $red;
|
|
||||||
transition: fill .2s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.member-limit-warning {
|
|
||||||
@include font-size(small);
|
|
||||||
background: $mass-white;
|
|
||||||
color: $grayer;
|
|
||||||
margin: 1rem 0;
|
|
||||||
padding: 1rem 2rem;
|
|
||||||
text-align: center;
|
|
||||||
a {
|
|
||||||
color: $primary;
|
|
||||||
&:hover {
|
|
||||||
color: $primary-light;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.help-text {
|
|
||||||
@include font-size(small);
|
|
||||||
@include font-type(light);
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
.checksley-error-list {
|
|
||||||
right: .5rem;
|
|
||||||
li {
|
|
||||||
display: none;
|
|
||||||
&:first-child {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.lightbox-sprint-add-edit {
|
.lightbox-sprint-add-edit {
|
||||||
form {
|
form {
|
||||||
flex-basis: 600px;
|
flex-basis: 600px;
|
||||||
|
|
|
@ -18,17 +18,28 @@ helper.getNewMemberLightbox = function() {
|
||||||
return utils.lightbox.close(el);
|
return utils.lightbox.close(el);
|
||||||
},
|
},
|
||||||
newEmail: function(email) {
|
newEmail: function(email) {
|
||||||
el.$$('input').last().sendKeys(email);
|
el.$$('input').clear();
|
||||||
el.$('.add-fieldset').click();
|
el.$$('input').sendKeys(email);
|
||||||
|
el.$('.e2e-add-member-suggest-filter-addmail').click();
|
||||||
},
|
},
|
||||||
getRows: function() {
|
addSuggested: function(index) {
|
||||||
return el.$$('.add-single-member');
|
el.$$('.e2e-add-member-suggest-single').get(index).click();
|
||||||
},
|
},
|
||||||
deleteRow: function(index) {
|
addNew: function() {
|
||||||
el.$$('.remove-fieldset').get(index).click();
|
return el.$$('.e2e-invite-members-single-new').click();
|
||||||
|
},
|
||||||
|
setRole: function(index) {
|
||||||
|
let select = el.$$('.e2e-invite-members-single-role').get(index);
|
||||||
|
select.$('option:last-child').click();
|
||||||
|
},
|
||||||
|
getInviteds: function() {
|
||||||
|
return el.$$('.e2e-invite-members-single')
|
||||||
|
},
|
||||||
|
deleteInvited: function(index) {
|
||||||
|
el.$$('.e2e-invite-members-single-remove').get(index).click();
|
||||||
},
|
},
|
||||||
submit: function() {
|
submit: function() {
|
||||||
return el.$('.submit-button').click();
|
return el.$('.e2e-invite-members-single-send').click();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -28,25 +28,30 @@ describe('admin - members', function() {
|
||||||
adminMembershipsHelper.openNewMemberLightbox();
|
adminMembershipsHelper.openNewMemberLightbox();
|
||||||
|
|
||||||
await newMemberLightbox.waitOpen();
|
await newMemberLightbox.waitOpen();
|
||||||
utils.common.takeScreenshot('memberships', 'new-member');
|
utils.common.takeScreenshot('memberships', 'add-new-member');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('add members row', async function() {
|
it('add contacts', async function() {
|
||||||
|
newMemberLightbox.addSuggested(0);
|
||||||
|
newMemberLightbox.addNew();
|
||||||
newMemberLightbox.newEmail('xxx' + new Date().getTime() + '@xx.es');
|
newMemberLightbox.newEmail('xxx' + new Date().getTime() + '@xx.es');
|
||||||
|
newMemberLightbox.addNew();
|
||||||
newMemberLightbox.newEmail('xxx' + new Date().getTime() + '@xx.es');
|
newMemberLightbox.newEmail('xxx' + new Date().getTime() + '@xx.es');
|
||||||
newMemberLightbox.newEmail('xxx' + new Date().getTime() + '@xx.es');
|
utils.common.takeScreenshot('memberships', 'add-new-member-form');
|
||||||
|
|
||||||
let membersRows = await newMemberLightbox.getRows().count();
|
|
||||||
|
|
||||||
expect(membersRows).to.be.equal(3 + 1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('delete members row', async function() {
|
it('delete members row', async function() {
|
||||||
newMemberLightbox.deleteRow(2);
|
newMemberLightbox.deleteInvited(2);
|
||||||
|
|
||||||
let membersRows = await newMemberLightbox.getRows().count();
|
let invitedRows = await newMemberLightbox.getInviteds().count();
|
||||||
|
|
||||||
expect(membersRows).to.be.equal(2 + 1);
|
expect(invitedRows).to.be.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('set roles', async function() {
|
||||||
|
newMemberLightbox.setRole(0);
|
||||||
|
newMemberLightbox.setRole(1);
|
||||||
|
utils.common.takeScreenshot('memberships', 'add-new-member-form-active');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('submit', async function() {
|
it('submit', async function() {
|
||||||
|
|
Loading…
Reference in New Issue