Merge pull request #1219 from taigaio/us/4663/suggest-members

Us/4663/suggest members
stable
David Barragán Merino 2017-02-07 11:41:41 +01:00 committed by GitHub
commit 79b70066e9
26 changed files with 1026 additions and 212 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
.lightbox-add-member {
.add-members-wrapper {
max-width: 900px;
width: 90%;
}
}

View File

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

View File

@ -0,0 +1,79 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: 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})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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