Refactor add-member lightbox and fix some error messages

stable
Xavier Julián 2016-03-14 17:02:06 +01:00 committed by David Barragán Merino
parent b6f3a7fe6d
commit acfacc6715
11 changed files with 267 additions and 202 deletions

View File

@ -27,153 +27,112 @@ debounce = @.taiga.debounce
module = angular.module("taigaKanban") module = angular.module("taigaKanban")
MAX_MEMBERSHIP_FIELDSETS = 4
############################################################################# #############################################################################
## Create Members Lightbox Directive ## Create Members Lightbox Directive
############################################################################# #############################################################################
CreateMembersDirective = ($rs, $rootScope, $confirm, $loading, lightboxService, $compile) -> class LightboxAddMembersController
extraTextTemplate = """ @.$inject = [
<fieldset class="extra-text"> "$scope",
<textarea ng-attr-placeholder="{{'LIGHTBOX.CREATE_MEMBER.PLACEHOLDER_INVITATION_TEXT' | translate}}" "lightboxService",
maxlength="255"></textarea> "tgLoader",
</fieldset> "$tgConfirm",
""" "$tgResources",
"$rootScope",
]
template = _.template(""" constructor: (@scope, @lightboxService, @tgLoader, @confirm, @rs, @rootScope) ->
<div class="add-member-wrapper"> @._defaultMaxInvites = 4
<fieldset> @._defaultRole = @.project.roles[0].id
<input tg-capslock type="email" placeholder="{{'LIGHTBOX.CREATE_MEMBER.PLACEHOLDER_TYPE_EMAIL' | translate}}" @.form = null
<% if(required) { %> data-required="true" <% } %> data-type="email" /> @.submitInvites = false
</fieldset> @.canAddUsers = true
<fieldset> @.memberInvites = []
<select <% if(required) { %> data-required="true" <% } %> data-required="true">
<% _.each(roleList, function(role) { %>
<option value="<%- role.id %>"><%- role.name %></option>
<% }); %>
</select>
<a class="add-fieldset" href="">
<svg class="icon icon-add">
<use xlink:href="#icon-add">
</svg>
</a>
</fieldset>
</div>
""")
link = ($scope, $el, $attrs) -> if @.project.max_memberships == null
createButton = (type) -> @.membersLimit = @._defaultMaxInvites
html = "<svg class='icon " + type + "'><use xlink:href='#" + type + "'></svg>"; else
return html pendingMembersCount = Math.max(@.project.max_memberships - @.project.total_memberships, 0)
@.membersLimit = Math.min(pendingMembersCount, @._defaultMaxInvites)
createFieldSet = (required = true)-> @.addSingleMember()
ctx = {roleList: $scope.project.roles, required: required}
return $compile(template(ctx))($scope)
resetForm = -> addSingleMember: () ->
$el.find("form textarea").remove() @.memberInvites.push({email:'', role_id: @._defaultRole})
$el.find("form .add-member-wrapper").remove()
invitations = $el.find(".add-member-forms") if @.memberInvites.length >= @.membersLimit
invitations.html($compile(extraTextTemplate)($scope)) @.canAddUsers = false
@.showWarningMessage = (!@.canAddUsers &&
@.project.total_memberships + @.memberInvites.length == @.project.max_memberships)
fieldSet = createFieldSet() removeSingleMember: (index) ->
invitations.prepend(fieldSet) @.memberInvites.splice(index, 1)
$scope.$on "membersform:new", -> @.canAddUsers = true
resetForm() @.showWarningMessage = @.membersLimit == 1
lightboxService.open($el)
$scope.$on "$destroy", -> submit: () ->
$el.off() # Need to reset the form constrains
@.form.initializeFields()
@.form.reset()
return if not @.form.validate()
$el.on "click", ".delete-fieldset", (event) -> @.submitInvites = true
event.preventDefault() promise = @rs.memberships.bulkCreateMemberships(
target = angular.element(event.currentTarget) @.project.id,
fieldSet = target.closest('.add-member-wrapper') @.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") _onErrorInvite: (response) ->
if lastActionButton.hasClass("delete-fieldset") @.submitInvites = false
lastActionButton.removeClass("delete-fieldset").addClass("add-fieldset") @.form.setErrors(response.data)
svg = createButton('icon-add') if response.data._error_message
lastActionButton.html(svg) @confirm.notify("error", response.data._error_message)
$el.on "click", ".add-fieldset", (event) -> module.controller("LbAddMembersController", LightboxAddMembersController)
event.preventDefault()
target = angular.element(event.currentTarget)
fieldSet = target.closest('.add-member-wrapper')
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 return {
$el.find(".add-member-wrapper fieldset:last > a") scope: {},
.removeClass("add-fieldset").addClass("delete-fieldset") bindToController: {
svg = createButton('icon-trash') project: '=',
$el.find(".add-member-wrapper fieldset:last > a").html(svg) },
controller: 'LbAddMembersController',
controllerAs: 'vm',
templateUrl: 'admin/lightbox-add-members.html',
link: link
}
submit = debounce 2000, (event) => module.directive("tgLbAddMembers", ["lightboxService", LightboxAddMembersDirective])
event.preventDefault()
currentLoading = $loading()
.target(submitButton)
.start()
onSuccess = (data) -> #############################################################################
currentLoading.finish() ## Warning message directive
lightboxService.close($el) #############################################################################
$confirm.notify("success")
$rootScope.$broadcast("membersform:new:success")
onError = (data) -> LightboxAddMembersWarningMessageDirective = () ->
currentLoading.finish() return {
lightboxService.close($el) templateUrl: "admin/lightbox-add-members-no-more=memberships-warning-message.html"
$confirm.notify("error") scope: {
$rootScope.$broadcast("membersform:new:error") project: "="
}
}
form = $el.find("form").checksley() module.directive("tgLightboxAddMembersWarningMessage", [LightboxAddMembersWarningMessageDirective])
#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])

View File

@ -49,10 +49,11 @@ class MembershipsController extends mixOf(taiga.Controller, taiga.PageMixin, tai
"tgAppMetaService", "tgAppMetaService",
"$translate", "$translate",
"$tgAuth" "$tgAuth"
"tgLightboxFactory"
] ]
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @navUrls, @analytics, constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @navUrls, @analytics,
@appMetaService, @translate, @tgAuth) -> @appMetaService, @translate, @auth, @lightboxFactory) ->
bindMethods(@) bindMethods(@)
@scope.project = {} @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}) title = @translate.instant("ADMIN.MEMBERSHIPS.PAGE_TITLE", {projectName: @scope.project.name})
description = @scope.project.description description = @scope.project.description
@appMetaService.setAll(title, description) @appMetaService.setAll(title, description)
@._checkUsersLimit()
promise.then null, @.onInitialDataError.bind(@) promise.then null, @.onInitialDataError.bind(@)
@ -79,8 +79,10 @@ class MembershipsController extends mixOf(taiga.Controller, taiga.PageMixin, tai
@scope.projectId = project.id @scope.projectId = project.id
@scope.project = project @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 return project
loadMembers: -> loadMembers: ->
@ -99,7 +101,7 @@ class MembershipsController extends mixOf(taiga.Controller, taiga.PageMixin, tai
return @.loadProject().then () => return @.loadProject().then () =>
return @q.all([ return @q.all([
@.loadMembers(), @.loadMembers(),
@tgAuth.refresh() @auth.refresh()
]) ])
getUrlFilters: -> getUrlFilters: ->
@ -107,16 +109,25 @@ class MembershipsController extends mixOf(taiga.Controller, taiga.PageMixin, tai
filters.page = 1 if not filters.page filters.page = 1 if not filters.page
return filters return filters
# Actions
addNewMembers: -> addNewMembers: ->
@rootscope.$broadcast("membersform:new") @lightboxFactory.create(
'tg-lb-add-members',
{
"class": "lightbox lightbox-add-member",
"project": "project"
},
{
"project": @scope.project
}
)
_checkUsersLimit: -> showLimitUsersWarningMessage: ->
@scope.canAddUsers = @.project.get('total_memberships') > @.project.get('max_memberships')
@.maxMembers = @.project.get('max_memberships')
limitUsersWarning: ->
title = @translate.instant("ADMIN.MEMBERSHIPS.LIMIT_USERS_WARNING") 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" icon = "/" + window._version + "/svg/icons/team-question.svg"
@confirm.success(title, message,icon) @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) template = $template.get("admin/admin-memberships-row-checkbox.html", true)
link = ($scope, $el, $attrs) -> 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) -> render = (member) ->
ctx = {inputId: "is-admin-#{member.id}"} ctx = {inputId: "is-admin-#{member.id}"}
@ -274,15 +297,6 @@ MembershipsRowAdminCheckboxDirective = ($log, $repo, $confirm, $template, $compi
$el.html(html) $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) => $el.on "click", ":checkbox", (event) =>
onSuccess = -> onSuccess = ->
$confirm.notify("success") $confirm.notify("success")
@ -296,8 +310,10 @@ MembershipsRowAdminCheckboxDirective = ($log, $repo, $confirm, $template, $compi
member.is_admin = target.prop("checked") member.is_admin = target.prop("checked")
$repo.save(member).then(onSuccess, onError) $repo.save(member).then(onSuccess, onError)
$scope.$on "$destroy", -> html = render(member)
$el.off()
if member.is_admin
$el.find(":checkbox").prop("checked", true)
return {link: link} return {link: link}
@ -469,4 +485,19 @@ MembershipsRowActionsDirective = ($log, $repo, $rs, $confirm, $compile, $transla
module.directive("tgMembershipsRowActions", ["$log", "$tgRepo", "$tgResources", "$tgConfirm", "$compile", 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])

View File

@ -409,8 +409,8 @@
"PAGE_TITLE": "Memberships - {{projectName}}", "PAGE_TITLE": "Memberships - {{projectName}}",
"ADD_BUTTON": "+ New member", "ADD_BUTTON": "+ New member",
"ADD_BUTTON_TITLE": "Add new member", "ADD_BUTTON_TITLE": "Add new member",
"LIMIT_USERS_WARNING": "Why can't I add more members", "LIMIT_USERS_WARNING_MESSAGE_FOR_ADMIN": "This project has reached its limit of allowed members <strong>({{members}})</strong>.",
"LIMIT_USERS_WARNING_MESSAGE": "Currently you can only have <strong>{{members}}</strong> members per project. If you want to add more members get in touch with the administrators" "LIMIT_USERS_WARNING_MESSAGE_FOR_OWNER": "This project has reached its limit of allowed members <strong>({{members}})</strong>. If you want to add more members please contact the administrators."
}, },
"PROJECT_EXPORT": { "PROJECT_EXPORT": {
"TITLE": "Export", "TITLE": "Export",
@ -918,7 +918,9 @@
}, },
"CREATE_MEMBER": { "CREATE_MEMBER": {
"PLACEHOLDER_INVITATION_TEXT": "(Optional) Add a personalized text to the invitation. Tell something lovely to your new members ;-)", "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 <strong>{{maxMembers}}</strong> members.<br> If you want to add more members contact the administrators.",
"LIMIT_USERS_WARNING_MESSAGE": "This project can't have more than <strong>{{maxMembers}}</strong> members."
}, },
"LEAVE_PROJECT_WARNING": { "LEAVE_PROJECT_WARNING": {
"TITLE": "You can not leave the project without owner", "TITLE": "You can not leave the project without owner",

View File

@ -1,4 +1,4 @@
.check .check.js-check
input(type="checkbox", id!="<%- inputId %>") input(type="checkbox", id!="<%- inputId %>")
div div
span.check-text.check-yes(translate="COMMON.YES") span.check-text.check-yes(translate="COMMON.YES")

View File

@ -1,35 +1,30 @@
doctype html doctype html
div.wrapper.memberships(ng-controller="MembershipsController as ctrl", div.wrapper.memberships(
ng-init="section='admin'; sectionName='ADMIN.MEMBERSHIPS.TITLE'", tg-memberships) ng-controller="MembershipsController as ctrl"
ng-init="section='admin'; sectionName='ADMIN.MEMBERSHIPS.TITLE'"
tg-memberships
)
tg-project-menu tg-project-menu
sidebar.menu-secondary.sidebar.settings-nav(tg-admin-navigation="memberships") sidebar.menu-secondary.sidebar.settings-nav(tg-admin-navigation="memberships")
include ../includes/modules/admin-menu include ../includes/modules/admin-menu
section.main.admin-membership section.main.admin-membership.admin-common
.header-with-actions .header-with-actions
header header
include ../includes/components/mainTitle include ../includes/components/mainTitle
tg-no-more-memberships-explanation(ng-if="canAddUsers == false", project="project")
.action-buttons .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( button.button-green(
translate="ADMIN.MEMBERSHIPS.ADD_BUTTON"
title="{{ ADMIN.MEMBERSHIPS.ADD_BUTTON_TITLE | translate }}", title="{{ ADMIN.MEMBERSHIPS.ADD_BUTTON_TITLE | translate }}",
ng-click="ctrl.addNewMembers()" ng-click="ctrl.addNewMembers()"
translate="ADMIN.MEMBERSHIPS.ADD_BUTTON" ng-disabled="canAddUsers == false"
ng-disabled="!canAddUsers"
) )
include ../includes/modules/admin/admin-membership-table include ../includes/modules/admin/admin-membership-table
div.paginator.memberships-paginator div.paginator.memberships-paginator
div.lightbox.lightbox-add-member(tg-lb-create-members)
include ../includes/modules/lightbox-add-member

View File

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

View File

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

View File

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

View File

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

View File

@ -88,12 +88,14 @@ body {
} }
.header-with-actions { .header-with-actions {
align-content: stretch;
align-items: center; align-items: center;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-between; justify-content: space-between;
margin-bottom: 1rem; margin-bottom: 1rem;
header {
flex: 1;
}
.action-buttons { .action-buttons {
flex-shrink: 0; flex-shrink: 0;
} }

View File

@ -142,54 +142,62 @@
.lightbox-add-member { .lightbox-add-member {
.add-member-wrapper { .add-member-wrapper {
max-width: 600px;
width: 90%;
}
.add-single-member {
align-items: center;
display: flex; display: flex;
justify-content: space-between;
margin-bottom: .5rem; margin-bottom: .5rem;
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
fieldset { fieldset {
position: relative; display: inline-block;
&:first-child { flex: 1;
flex-basis: 400px; margin: 0 .5rem 0 0;
flex-grow: 3;
}
&:last-child { &:last-child {
flex-basis: 200px; flex-basis: 30px;
flex-grow: 1; flex-grow: 0;
margin-left: .5rem; flex-shrink: 0;
} }
&:first-child {
flex-basis: 20%;
}
} }
} }
.extra-text {
margin-top: 1rem;
}
fieldset {
margin-bottom: 0;
}
select {
width: 80%;
}
.icon { .icon {
@include svg-size(1.25rem);
fill: $gray; fill: $gray;
margin-left: .5rem; margin-left: .5rem;
transition: fill .2s;
} }
.icon-add { .icon-add {
&:hover { &:hover {
fill: $primary-light; fill: $primary;
transition: fill .2s;
} }
} }
.icon-trash { .icon-trash {
fill: $red-light;
&:hover { &:hover {
fill: $red; fill: $red;
transition: fill .2s;
} }
} }
.button { .member-limit-warning {
margin-top: 1rem; @extend %small;
background: $red-light;
color: $white;
margin: 1rem 0;
padding: 1rem 2rem;
text-align: center;
} }
.help-text { .help-text {
@extend %small; @extend %small;
padding: .5rem 1rem; @extend %light;
margin-top: 1rem;
} }
.checksley-error-list { .checksley-error-list {
right: .5rem; right: .5rem;