diff --git a/app/coffee/modules/admin/lightboxes.coffee b/app/coffee/modules/admin/lightboxes.coffee
index 5a4ff7cd..8030262f 100644
--- a/app/coffee/modules/admin/lightboxes.coffee
+++ b/app/coffee/modules/admin/lightboxes.coffee
@@ -27,153 +27,112 @@ debounce = @.taiga.debounce
module = angular.module("taigaKanban")
-MAX_MEMBERSHIP_FIELDSETS = 4
-
#############################################################################
## Create Members Lightbox Directive
#############################################################################
-CreateMembersDirective = ($rs, $rootScope, $confirm, $loading, lightboxService, $compile) ->
- extraTextTemplate = """
-
- """
+class LightboxAddMembersController
+ @.$inject = [
+ "$scope",
+ "lightboxService",
+ "tgLoader",
+ "$tgConfirm",
+ "$tgResources",
+ "$rootScope",
+ ]
- template = _.template("""
-
-
-
-
- """)
+ constructor: (@scope, @lightboxService, @tgLoader, @confirm, @rs, @rootScope) ->
+ @._defaultMaxInvites = 4
+ @._defaultRole = @.project.roles[0].id
+ @.form = null
+ @.submitInvites = false
+ @.canAddUsers = true
+ @.memberInvites = []
- link = ($scope, $el, $attrs) ->
- createButton = (type) ->
- html = "";
- return html
+ if @.project.max_memberships == null
+ @.membersLimit = @._defaultMaxInvites
+ else
+ pendingMembersCount = Math.max(@.project.max_memberships - @.project.total_memberships, 0)
+ @.membersLimit = Math.min(pendingMembersCount, @._defaultMaxInvites)
- createFieldSet = (required = true)->
- ctx = {roleList: $scope.project.roles, required: required}
- return $compile(template(ctx))($scope)
+ @.addSingleMember()
- resetForm = ->
- $el.find("form textarea").remove()
- $el.find("form .add-member-wrapper").remove()
+ addSingleMember: () ->
+ @.memberInvites.push({email:'', role_id: @._defaultRole})
- invitations = $el.find(".add-member-forms")
- invitations.html($compile(extraTextTemplate)($scope))
+ if @.memberInvites.length >= @.membersLimit
+ @.canAddUsers = false
+ @.showWarningMessage = (!@.canAddUsers &&
+ @.project.total_memberships + @.memberInvites.length == @.project.max_memberships)
- fieldSet = createFieldSet()
- invitations.prepend(fieldSet)
+ removeSingleMember: (index) ->
+ @.memberInvites.splice(index, 1)
- $scope.$on "membersform:new", ->
- resetForm()
- lightboxService.open($el)
+ @.canAddUsers = true
+ @.showWarningMessage = @.membersLimit == 1
- $scope.$on "$destroy", ->
- $el.off()
+ submit: () ->
+ # Need to reset the form constrains
+ @.form.initializeFields()
+ @.form.reset()
+ return if not @.form.validate()
- $el.on "click", ".delete-fieldset", (event) ->
- event.preventDefault()
- target = angular.element(event.currentTarget)
- fieldSet = target.closest('.add-member-wrapper')
+ @.submitInvites = true
+ promise = @rs.memberships.bulkCreateMemberships(
+ @.project.id,
+ @.memberInvites,
+ @.invitationText
+ )
+ promise.then(
+ @._onSuccessInvite.bind(this),
+ @._onErrorInvite.bind(this)
+ )
- fieldSet.remove()
+ _onSuccessInvite: () ->
+ @.submitInvites = false
+ @rootScope.$broadcast("membersform:new:success")
+ @lightboxService.closeAll()
+ @confirm.notify("success")
- lastActionButton = $el.find(".add-member-wrapper fieldset:last > a")
- if lastActionButton.hasClass("delete-fieldset")
- lastActionButton.removeClass("delete-fieldset").addClass("add-fieldset")
- svg = createButton('icon-add')
- lastActionButton.html(svg)
+ _onErrorInvite: (response) ->
+ @.submitInvites = false
+ @.form.setErrors(response.data)
+ if response.data._error_message
+ @confirm.notify("error", response.data._error_message)
- $el.on "click", ".add-fieldset", (event) ->
- event.preventDefault()
- target = angular.element(event.currentTarget)
- fieldSet = target.closest('.add-member-wrapper')
+module.controller("LbAddMembersController", LightboxAddMembersController)
- target.removeClass("add-fieldset").addClass("delete-fieldset")
- svg = createButton('icon-trash')
- target.html(svg)
- newFieldSet = createFieldSet(false)
- fieldSet.after(newFieldSet)
- $scope.$digest() # To compile newFieldSet and translate text
+LightboxAddMembersDirective = (lightboxService) ->
+ link = (scope, el, attrs, ctrl) ->
+ lightboxService.open(el)
+ ctrl.form = el.find("form").checksley()
- if $el.find(".add-member-wrapper").length == MAX_MEMBERSHIP_FIELDSETS
- $el.find(".add-member-wrapper fieldset:last > a")
- .removeClass("add-fieldset").addClass("delete-fieldset")
- svg = createButton('icon-trash')
- $el.find(".add-member-wrapper fieldset:last > a").html(svg)
+ return {
+ scope: {},
+ bindToController: {
+ project: '=',
+ },
+ controller: 'LbAddMembersController',
+ controllerAs: 'vm',
+ templateUrl: 'admin/lightbox-add-members.html',
+ link: link
+ }
- submit = debounce 2000, (event) =>
- event.preventDefault()
+module.directive("tgLbAddMembers", ["lightboxService", LightboxAddMembersDirective])
- currentLoading = $loading()
- .target(submitButton)
- .start()
- onSuccess = (data) ->
- currentLoading.finish()
- lightboxService.close($el)
- $confirm.notify("success")
- $rootScope.$broadcast("membersform:new:success")
+#############################################################################
+## Warning message directive
+#############################################################################
- onError = (data) ->
- currentLoading.finish()
- lightboxService.close($el)
- $confirm.notify("error")
- $rootScope.$broadcast("membersform:new:error")
+LightboxAddMembersWarningMessageDirective = () ->
+ return {
+ templateUrl: "admin/lightbox-add-members-no-more=memberships-warning-message.html"
+ scope: {
+ project: "="
+ }
+ }
- form = $el.find("form").checksley()
-
- #checksley find new fields
- form.destroy()
- form.initialize()
- if not form.validate()
- return
-
- memberWrappers = $el.find("form .add-member-wrapper")
- memberWrappers = _.filter memberWrappers, (mw) ->
- angular.element(mw).find("input").hasClass('checksley-ok')
-
- invitations = _.map memberWrappers, (mw) ->
- memberWrapper = angular.element(mw)
- email = memberWrapper.find("input")
- role = memberWrapper.find("select")
-
- return {
- email: email.val()
- role_id: role.val()
- }
-
- if invitations.length
- invitation_extra_text = $el.find("form textarea").val()
-
- promise = $rs.memberships.bulkCreateMemberships($scope.project.id,
- invitations, invitation_extra_text)
- promise.then(onSuccess, onError)
-
- submitButton = $el.find(".submit-button")
-
- $el.on "submit", "form", submit
-
- return {link: link}
-
-module.directive("tgLbCreateMembers", ["$tgResources", "$rootScope", "$tgConfirm", "$tgLoading",
- "lightboxService", "$compile", CreateMembersDirective])
+module.directive("tgLightboxAddMembersWarningMessage", [LightboxAddMembersWarningMessageDirective])
diff --git a/app/coffee/modules/admin/memberships.coffee b/app/coffee/modules/admin/memberships.coffee
index e36d8f92..be2a7ad9 100644
--- a/app/coffee/modules/admin/memberships.coffee
+++ b/app/coffee/modules/admin/memberships.coffee
@@ -49,10 +49,11 @@ class MembershipsController extends mixOf(taiga.Controller, taiga.PageMixin, tai
"tgAppMetaService",
"$translate",
"$tgAuth"
+ "tgLightboxFactory"
]
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @navUrls, @analytics,
- @appMetaService, @translate, @tgAuth) ->
+ @appMetaService, @translate, @auth, @lightboxFactory) ->
bindMethods(@)
@scope.project = {}
@@ -64,7 +65,6 @@ class MembershipsController extends mixOf(taiga.Controller, taiga.PageMixin, tai
title = @translate.instant("ADMIN.MEMBERSHIPS.PAGE_TITLE", {projectName: @scope.project.name})
description = @scope.project.description
@appMetaService.setAll(title, description)
- @._checkUsersLimit()
promise.then null, @.onInitialDataError.bind(@)
@@ -79,8 +79,10 @@ class MembershipsController extends mixOf(taiga.Controller, taiga.PageMixin, tai
@scope.projectId = project.id
@scope.project = project
- @scope.$emit('project:loaded', project)
+ @scope.canAddUsers = project.max_memberships == null || project.max_memberships > project.total_memberships
+
+ @scope.$emit('project:loaded', project)
return project
loadMembers: ->
@@ -99,7 +101,7 @@ class MembershipsController extends mixOf(taiga.Controller, taiga.PageMixin, tai
return @.loadProject().then () =>
return @q.all([
@.loadMembers(),
- @tgAuth.refresh()
+ @auth.refresh()
])
getUrlFilters: ->
@@ -107,16 +109,25 @@ class MembershipsController extends mixOf(taiga.Controller, taiga.PageMixin, tai
filters.page = 1 if not filters.page
return filters
+ # Actions
+
addNewMembers: ->
- @rootscope.$broadcast("membersform:new")
+ @lightboxFactory.create(
+ 'tg-lb-add-members',
+ {
+ "class": "lightbox lightbox-add-member",
+ "project": "project"
+ },
+ {
+ "project": @scope.project
+ }
+ )
- _checkUsersLimit: ->
- @scope.canAddUsers = @.project.get('total_memberships') > @.project.get('max_memberships')
- @.maxMembers = @.project.get('max_memberships')
-
- limitUsersWarning: ->
+ showLimitUsersWarningMessage: ->
title = @translate.instant("ADMIN.MEMBERSHIPS.LIMIT_USERS_WARNING")
- message = @translate.instant("ADMIN.MEMBERSHIPS.LIMIT_USERS_WARNING_MESSAGE", {members: @.maxMembers})
+ message = @translate.instant("ADMIN.MEMBERSHIPS.LIMIT_USERS_WARNING_MESSAGE", {
+ members: @scope.project.max_memberships
+ })
icon = "/" + window._version + "/svg/icons/team-question.svg"
@confirm.success(title, message,icon)
@@ -266,6 +277,18 @@ MembershipsRowAdminCheckboxDirective = ($log, $repo, $confirm, $template, $compi
template = $template.get("admin/admin-memberships-row-checkbox.html", true)
link = ($scope, $el, $attrs) ->
+ $scope.$on "$destroy", ->
+ $el.off()
+
+ if not $attrs.tgMembershipsRowAdminCheckbox?
+ return $log.error "MembershipsRowAdminCheckboxDirective: the directive need a member"
+
+ member = $scope.$eval($attrs.tgMembershipsRowAdminCheckbox)
+
+ if member.is_owner
+ $el.find(".js-check").remove()
+ return
+
render = (member) ->
ctx = {inputId: "is-admin-#{member.id}"}
@@ -274,15 +297,6 @@ MembershipsRowAdminCheckboxDirective = ($log, $repo, $confirm, $template, $compi
$el.html(html)
- if not $attrs.tgMembershipsRowAdminCheckbox?
- return $log.error "MembershipsRowAdminCheckboxDirective: the directive need a member"
-
- member = $scope.$eval($attrs.tgMembershipsRowAdminCheckbox)
- html = render(member)
-
- if member.is_admin
- $el.find(":checkbox").prop("checked", true)
-
$el.on "click", ":checkbox", (event) =>
onSuccess = ->
$confirm.notify("success")
@@ -296,8 +310,10 @@ MembershipsRowAdminCheckboxDirective = ($log, $repo, $confirm, $template, $compi
member.is_admin = target.prop("checked")
$repo.save(member).then(onSuccess, onError)
- $scope.$on "$destroy", ->
- $el.off()
+ html = render(member)
+
+ if member.is_admin
+ $el.find(":checkbox").prop("checked", true)
return {link: link}
@@ -469,4 +485,19 @@ MembershipsRowActionsDirective = ($log, $repo, $rs, $confirm, $compile, $transla
module.directive("tgMembershipsRowActions", ["$log", "$tgRepo", "$tgResources", "$tgConfirm", "$compile",
- "$translate", "tgCurrentUserService", "tgLightboxFactory", MembershipsRowActionsDirective])
+ "$translate", MembershipsRowActionsDirective])
+
+
+#############################################################################
+## No more memberships explanation directive
+#############################################################################
+
+NoMoreMembershipsExplanationDirective = () ->
+ return {
+ templateUrl: "admin/no-more-memberships-explanation.html"
+ scope: {
+ project: "="
+ }
+ }
+
+module.directive("tgNoMoreMembershipsExplanation", [NoMoreMembershipsExplanationDirective])
diff --git a/app/locales/taiga/locale-en.json b/app/locales/taiga/locale-en.json
index 8bd9721b..2283333c 100644
--- a/app/locales/taiga/locale-en.json
+++ b/app/locales/taiga/locale-en.json
@@ -409,8 +409,8 @@
"PAGE_TITLE": "Memberships - {{projectName}}",
"ADD_BUTTON": "+ New member",
"ADD_BUTTON_TITLE": "Add new member",
- "LIMIT_USERS_WARNING": "Why can't I add more members",
- "LIMIT_USERS_WARNING_MESSAGE": "Currently you can only have {{members}} members per project. If you want to add more members get in touch with the administrators"
+ "LIMIT_USERS_WARNING_MESSAGE_FOR_ADMIN": "This project has reached its limit of allowed members ({{members}}).",
+ "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "This project has reached its limit of allowed members ({{members}}). If you want to add more members please contact the administrators."
},
"PROJECT_EXPORT": {
"TITLE": "Export",
@@ -918,7 +918,9 @@
},
"CREATE_MEMBER": {
"PLACEHOLDER_INVITATION_TEXT": "(Optional) Add a personalized text to the invitation. Tell something lovely to your new members ;-)",
- "PLACEHOLDER_TYPE_EMAIL": "Type an Email"
+ "PLACEHOLDER_TYPE_EMAIL": "Type an Email",
+ "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "This project can't have more than {{maxMembers}} members.
If you want to add more members contact the administrators.",
+ "LIMIT_USERS_WARNING_MESSAGE": "This project can't have more than {{maxMembers}} members."
},
"LEAVE_PROJECT_WARNING": {
"TITLE": "You can not leave the project without owner",
diff --git a/app/partials/admin/admin-memberships-row-checkbox.jade b/app/partials/admin/admin-memberships-row-checkbox.jade
index efa25b23..828dd28e 100644
--- a/app/partials/admin/admin-memberships-row-checkbox.jade
+++ b/app/partials/admin/admin-memberships-row-checkbox.jade
@@ -1,4 +1,4 @@
-.check
+.check.js-check
input(type="checkbox", id!="<%- inputId %>")
div
span.check-text.check-yes(translate="COMMON.YES")
diff --git a/app/partials/admin/admin-memberships.jade b/app/partials/admin/admin-memberships.jade
index b97c5451..46064518 100644
--- a/app/partials/admin/admin-memberships.jade
+++ b/app/partials/admin/admin-memberships.jade
@@ -1,35 +1,30 @@
doctype html
-div.wrapper.memberships(ng-controller="MembershipsController as ctrl",
- ng-init="section='admin'; sectionName='ADMIN.MEMBERSHIPS.TITLE'", tg-memberships)
+div.wrapper.memberships(
+ ng-controller="MembershipsController as ctrl"
+ ng-init="section='admin'; sectionName='ADMIN.MEMBERSHIPS.TITLE'"
+ tg-memberships
+)
tg-project-menu
sidebar.menu-secondary.sidebar.settings-nav(tg-admin-navigation="memberships")
include ../includes/modules/admin-menu
- section.main.admin-membership
+ section.main.admin-membership.admin-common
.header-with-actions
header
include ../includes/components/mainTitle
+ tg-no-more-memberships-explanation(ng-if="canAddUsers == false", project="project")
.action-buttons
- a.limit-users-warning(
- ng-if="!canAddUsers"
- translate="ADMIN.MEMBERSHIPS.LIMIT_USERS_WARNING"
- translate-values="{members: ctrl.maxMembers}"
- href=""
- ng-click="ctrl.limitUsersWarning()"
- )
button.button-green(
+ translate="ADMIN.MEMBERSHIPS.ADD_BUTTON"
title="{{ ADMIN.MEMBERSHIPS.ADD_BUTTON_TITLE | translate }}",
ng-click="ctrl.addNewMembers()"
- translate="ADMIN.MEMBERSHIPS.ADD_BUTTON"
- ng-disabled="!canAddUsers"
+ ng-disabled="canAddUsers == false"
)
+
include ../includes/modules/admin/admin-membership-table
div.paginator.memberships-paginator
-
- div.lightbox.lightbox-add-member(tg-lb-create-members)
- include ../includes/modules/lightbox-add-member
diff --git a/app/partials/admin/lightbox-add-members-no-more=memberships-warning-message.jade b/app/partials/admin/lightbox-add-members-no-more=memberships-warning-message.jade
new file mode 100644
index 00000000..304e5f09
--- /dev/null
+++ b/app/partials/admin/lightbox-add-members-no-more=memberships-warning-message.jade
@@ -0,0 +1,11 @@
+p.member-limit-warning(
+ ng-if="project.i_am_owner == true"
+ translate="LIGHTBOX.CREATE_MEMBER.LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER"
+ translate-values="{maxMembers: project.max_memberships}"
+)
+
+p.member-limit-warning(
+ ng-if="project.i_am_owner == false"
+ translate="LIGHTBOX.CREATE_MEMBER.LIMIT_USERS_WARNING_MESSAGE"
+ translate-values="{maxMembers: project.max_memberships}"
+)
diff --git a/app/partials/admin/lightbox-add-members.jade b/app/partials/admin/lightbox-add-members.jade
new file mode 100644
index 00000000..2cd98ac7
--- /dev/null
+++ b/app/partials/admin/lightbox-add-members.jade
@@ -0,0 +1,58 @@
+a.close(
+ href=""
+ title="close"
+)
+ svg.icon.icon-close
+ use(xlink:href="#icon-close")
+.add-member-wrapper
+ h2.title(translate="LIGHTBOX.ADD_MEMBER.TITLE")
+ form.add-member-form(ng-submit="vm.submit()")
+ .add-single-member(ng-repeat="member in vm.memberInvites")
+ fieldset
+ input(
+ type="email"
+ required
+ placeholder="{{'LIGHTBOX.CREATE_MEMBER.PLACEHOLDER_TYPE_EMAIL' | translate}}"
+ data-required="true"
+ data-type="email"
+ ng-model="member.email"
+ )
+ fieldset
+ select(
+ ng-if="vm.project"
+ ng-model="member.role_id"
+ ng-options="role.id as role.name for role in vm.project.roles"
+ )
+ fieldset
+ a.add-fieldset.ng-animate-disabled(
+ href=""
+ ng-click="vm.addSingleMember()"
+ ng-if="$last && vm.canAddUsers"
+ )
+ svg.icon.icon-add
+ use(xlink:href="#icon-add")
+ a.remove-fieldset.ng-animate-disabled(
+ href=""
+ ng-click="vm.removeSingleMember($index)"
+ ng-if="!$last || ($last && !vm.canAddUsers && vm.membersLimit > 1)"
+ )
+ svg.icon.icon-trash
+ use(xlink:href="#icon-trash")
+
+ tg-lightbox-add-members-warning-message(ng-if="vm.showWarningMessage", project="vm.project")
+
+ fieldset.invitation-text
+ textarea(
+ ng-attr-placeholder="{{'LIGHTBOX.CREATE_MEMBER.PLACEHOLDER_INVITATION_TEXT' | translate}}"
+ maxlength="255"
+ ng-model="vm.invitationText"
+ )
+
+ button.button-green.submit-button(
+ type="submit"
+ title="{{'COMMON.CREATE' | translate}}"
+ translate="COMMON.CREATE"
+ tg-loading="vm.submitInvites"
+ )
+
+ p.help-text(translate="LIGHTBOX.ADD_MEMBER.HELP_TEXT")
diff --git a/app/partials/admin/no-more-memberships-explanation.jade b/app/partials/admin/no-more-memberships-explanation.jade
new file mode 100644
index 00000000..704fa69e
--- /dev/null
+++ b/app/partials/admin/no-more-memberships-explanation.jade
@@ -0,0 +1,11 @@
+p.admin-subtitle(
+ ng-if="project.i_am_owner == true"
+ translate="ADMIN.MEMBERSHIPS.LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER"
+ translate-values="{members: project.total_memberships}"
+)
+
+p.admin-subtitle(
+ ng-if="project.i_am_owner == false"
+ translate="ADMIN.MEMBERSHIPS.LIMIT_USERS_WARNING_MESSAGE_FOR_ADMIN"
+ translate-values="{members: project.total_memberships}"
+)
diff --git a/app/partials/includes/modules/lightbox-add-member.jade b/app/partials/includes/modules/lightbox-add-member.jade
deleted file mode 100644
index 31aa886b..00000000
--- a/app/partials/includes/modules/lightbox-add-member.jade
+++ /dev/null
@@ -1,12 +0,0 @@
-a.close(href="", title="close")
- svg.icon.icon-close
- use(xlink:href="#icon-close")
-form
- h2.title(translate="LIGHTBOX.ADD_MEMBER.TITLE")
-
- //- Form is set in a directive
- .add-member-forms
-
- button.button-green.submit-button(type="submit", title="{{'COMMON.CREATE' | translate}}", translate="COMMON.CREATE")
-
- p.help-text(translate="LIGHTBOX.ADD_MEMBER.HELP_TEXT")
diff --git a/app/styles/core/base.scss b/app/styles/core/base.scss
index fdec382a..a531fad6 100644
--- a/app/styles/core/base.scss
+++ b/app/styles/core/base.scss
@@ -88,12 +88,14 @@ body {
}
.header-with-actions {
- align-content: stretch;
align-items: center;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin-bottom: 1rem;
+ header {
+ flex: 1;
+ }
.action-buttons {
flex-shrink: 0;
}
diff --git a/app/styles/modules/common/lightbox.scss b/app/styles/modules/common/lightbox.scss
index a3b0d9ac..f4d63c68 100644
--- a/app/styles/modules/common/lightbox.scss
+++ b/app/styles/modules/common/lightbox.scss
@@ -142,54 +142,62 @@
.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 {
- position: relative;
- &:first-child {
- flex-basis: 400px;
- flex-grow: 3;
- }
+ display: inline-block;
+ flex: 1;
+ margin: 0 .5rem 0 0;
&:last-child {
- flex-basis: 200px;
- flex-grow: 1;
- margin-left: .5rem;
+ flex-basis: 30px;
+ flex-grow: 0;
+ flex-shrink: 0;
}
+ &:first-child {
+ flex-basis: 20%;
+ }
+
}
}
- .extra-text {
- margin-top: 1rem;
- }
- fieldset {
- margin-bottom: 0;
- }
- select {
- width: 80%;
- }
.icon {
+ @include svg-size(1.25rem);
fill: $gray;
margin-left: .5rem;
- transition: fill .2s;
}
.icon-add {
&:hover {
- fill: $primary-light;
+ fill: $primary;
+ transition: fill .2s;
}
}
.icon-trash {
+ fill: $red-light;
&:hover {
fill: $red;
+ transition: fill .2s;
}
}
- .button {
- margin-top: 1rem;
+ .member-limit-warning {
+ @extend %small;
+ background: $red-light;
+ color: $white;
+ margin: 1rem 0;
+ padding: 1rem 2rem;
+ text-align: center;
}
.help-text {
@extend %small;
- padding: .5rem 1rem;
+ @extend %light;
+ margin-top: 1rem;
}
.checksley-error-list {
right: .5rem;