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(""" -
-
- data-required="true" <% } %> data-type="email" /> -
-
- - - - - - -
-
- """) + 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;