diff --git a/app/assets/javascripts/sitewide/dropdown_selector.js b/app/assets/javascripts/sitewide/dropdown_selector.js index 467fe8e0b..51cac3ab1 100644 --- a/app/assets/javascripts/sitewide/dropdown_selector.js +++ b/app/assets/javascripts/sitewide/dropdown_selector.js @@ -100,7 +100,7 @@ var dropdownSelector = (function() { // Save data to the field function updateCurrentData(container, data) { - container.find('.data-field').val(JSON.stringify(data)); + container.find('.data-field').val(JSON.stringify(data)).change(); } // Search filter for non-ajax data @@ -293,6 +293,7 @@ var dropdownSelector = (function() { var optionContainer; var perfectScroll; var dropdownContainer; + var toggleElement; if (selectElement.length === 0) return; @@ -316,6 +317,30 @@ var dropdownSelector = (function() { `).appendTo(dropdownContainer); + // Blank option + if (selectElement.data('blank')) { + $(``) + .appendTo(dropdownContainer.find('.dropdown-container')) + .click(() => { + dropdownContainer.find('.dropdown-group, .dropdown-option').removeClass('select'); + saveData(selectElement, dropdownContainer); + dropdownContainer.removeClass('open'); + }); + } + + if (selectElement.data('toggle-target')) { + dropdownContainer.find('.data-field').on('change', function() { + toggleElement = $(selectElement.data('toggle-target')); + if (getCurrentData(dropdownContainer).length > 0) { + toggleElement.removeClass('hidden'); + toggleElement.find('input, select').removeAttr('disabled'); + } else { + toggleElement.addClass('hidden'); + toggleElement.find('input, select').attr('disabled', true); + } + }); + } + // If we setup Select All we draw it and add correspond logic if (selectElement.data('select-all-button')) { $(``) diff --git a/app/assets/javascripts/users/settings/teams/invite_users_modal.js b/app/assets/javascripts/users/settings/teams/invite_users_modal.js index 1c2de5065..f89a5deff 100644 --- a/app/assets/javascripts/users/settings/teams/invite_users_modal.js +++ b/app/assets/javascripts/users/settings/teams/invite_users_modal.js @@ -26,6 +26,8 @@ var teamSelectorDropdown = modal.find('[data-role=team-selector-dropdown]'); var teamSelectorDropdown2 = $(); var emailsInput = modal.find('.emails-input'); + var teamsInput = modal.find('.teams-input'); + var roleInput = modal.find('.role-input'); var recaptchaErrorMsgDiv = modal.find('#recaptcha-error-msg'); var recaptchaErrorText = modal.find('#recaptcha-error-msg>span'); @@ -46,7 +48,7 @@ labelHTML: true, tagClass: 'users-dropdown-list', inputTagMode: true, - customDropdownIcon: () => { return ''; }, + customDropdownIcon: () => { return ''; }, onChange: () => { let values = dropdownSelector.getValues(emailsInput); if (values.length > 0) { @@ -114,6 +116,8 @@ modal.find('[data-action=invite]').off('click').on('click', function() { var data = { emails: dropdownSelector.getValues(emailsInput), + team_ids: dropdownSelector.getValues(teamsInput), + role: roleInput.val(), 'g-recaptcha-response': $('#recaptcha-invite-modal').val() }; @@ -121,24 +125,24 @@ switch (type) { case 'invite_to_team': - data.teamId = modal.attr('data-team-id'); + data.team_ids = [modal.attr('data-team-id')]; data.role = $(this).attr('data-team-role'); break; case 'invite_to_team_with_role': - data.teamId = modal.attr('data-team-id'); + data.team_ids = [modal.attr('data-team-id')]; data.role = modal.attr('data-team-role'); break; case 'invite': break; case 'invite_with_team_selector': if (teamSelectorCheckbox.is(':checked')) { - data.teamId = teamSelectorDropdown.val(); + data.team_ids = [teamSelectorDropdown.val()]; data.role = $(this).attr('data-team-role'); } break; case 'invite_with_team_selector_and_role': if (teamSelectorCheckbox.is(':checked')) { - data.teamId = teamSelectorDropdown.val(); + data.team_ids = [teamSelectorDropdown.val()]; data.role = modal.attr('data-team-role'); } break; @@ -181,6 +185,11 @@ script.src = 'https://www.google.com/recaptcha/api.js?hl=en'; $(script).insertAfter('#recaptcha-service'); // Remove 'data-invited="true"' status + + dropdownSelector.init(teamsInput, { + optionClass: 'checkbox-icon' + }); + modal.removeAttr('data-invited'); }).on('hide.bs.modal', function() { // 'Reset' modal state diff --git a/app/assets/stylesheets/shared/dropdown_selector.scss b/app/assets/stylesheets/shared/dropdown_selector.scss index f85df08d9..060a3805e 100644 --- a/app/assets/stylesheets/shared/dropdown_selector.scss +++ b/app/assets/stylesheets/shared/dropdown_selector.scss @@ -3,6 +3,19 @@ @import "constants"; +.select-container--with-search .dropdown-selector-container { + &.active, + &.open { + .right-icon.fa-search { + display: block; + } + + .right-icon.fa-caret-down { + display: none; + } + } +} + .dropdown-selector-container { display: inline-block; float: left; @@ -163,6 +176,18 @@ z-index: 5; } + .dropdown-blank { + border-radius: 0; + padding-left: 7px; + text-align: left; + width: 100%; + + &:hover { + background: $brand-primary; + color: $color-white; + } + } + .dropdown-option { align-items: center; cursor: pointer; diff --git a/app/assets/stylesheets/themes/scinote.scss b/app/assets/stylesheets/themes/scinote.scss index 0049e6adc..ab5d80afc 100644 --- a/app/assets/stylesheets/themes/scinote.scss +++ b/app/assets/stylesheets/themes/scinote.scss @@ -1319,6 +1319,7 @@ body > .loading-overlay { .modal-invite-users { .bootstrap-tagsinput { min-width: 450px; + width: 100%; } .results-container .alert { @@ -1342,6 +1343,10 @@ body > .loading-overlay { } } + .dropdown-selector-container { + margin-bottom: 20px; + } + .g-recaptcha { margin-top: 20px; } @@ -1486,3 +1491,15 @@ a.disabled-with-click-events { } } + +.form-select { + border: 1px solid $color-silver-chalice; + border-radius: 4px; + color: $color-emperor; + display: block; + font-size: 14px; + min-height: 36px; + outline: 0; + padding: 8px 42px 3px 3px; + width: 100%; +} diff --git a/app/controllers/users/invitations_controller.rb b/app/controllers/users/invitations_controller.rb index 44042ac48..af3b16aba 100644 --- a/app/controllers/users/invitations_controller.rb +++ b/app/controllers/users/invitations_controller.rb @@ -100,46 +100,49 @@ module Users user.delay.deliver_invitation end - if @team && user - user_team = UserTeam.find_by_user_id_and_team_id(user.id, @team.id) - if user_team - result[:status] = :user_exists_and_in_team - else - # Also generate user team relation - user_team = UserTeam.new( - user: user, - team: @team, - role: @role - ) - user_team.save + if @teams.any? && user + @teams.each do |team| + user_team = UserTeam.find_by(user_id: user.id, team_id: team.id) + if user_team + result[:status] = :user_exists_and_in_team + else + # Also generate user team relation + user_team = UserTeam.new( + user: user, + team: team, + role: @role || 'normal_user' + ) + user_team.save - generate_notification( - @user, - user, - user_team.team, - user_team.role_str - ) - Activities::CreateActivityService - .call(activity_type: :invite_user_to_team, - owner: current_user, - subject: @team, - team: @team, - message_items: { - team: @team.id, - user_invited: user.id, - role: user_team.role_str - }) + generate_notification( + @user, + user, + user_team.team, + user_team.role_str + ) - result[:status] = if result[:status] == :user_exists && !user.confirmed? - :user_exists_unconfirmed_invited_to_team - elsif result[:status] == :user_exists - :user_exists_invited_to_team - else - :user_created_invited_to_team - end + Activities::CreateActivityService + .call(activity_type: :invite_user_to_team, + owner: current_user, + subject: team, + team: team, + message_items: { + team: team.id, + user_invited: user.id, + role: user_team.role_str + }) + + result[:status] = if result[:status] == :user_exists && !user.confirmed? + :user_exists_unconfirmed_invited_to_team + elsif result[:status] == :user_exists + :user_exists_invited_to_team + else + :user_created_invited_to_team + end + end + + result[:user_teams] << user_team end - - result[:user_team] = user_team end @invite_results << result @@ -156,6 +159,17 @@ module Users end end + def invitable_teams + teams = current_user.teams + .select(:id, :name) + .joins(:user_teams) + .where('user_teams.role': UserTeam.roles[:admin]) + .distinct + .select { |team| can_invite_team_users?(team) } + + render json: teams.map { |t| { value: t.id, label: t.name } }.to_json + end + private def update_sanitized_params @@ -185,12 +199,13 @@ module Users def check_invite_users_permission @user = current_user @emails = params[:emails]&.map(&:downcase) - @team = Team.find_by_id(params['teamId']) + + @teams = Team.where(id: params[:team_ids]).select { |team| can_manage_team_users?(team) } + return render_403 if @teams.none? + @role = params['role'] - return render_403 if @team && @role.nil? # if we select team, we must select role return render_403 if @emails.blank? # We must have at least one email - return render_403 if @team && !can_manage_team_users?(@team) # if we select team, we must check permission return render_403 if @role && !UserTeam.roles.key?(@role) # if we select role, we must check that this role exist end end diff --git a/app/views/shared/_invite_users_modal.html.erb b/app/views/shared/_invite_users_modal.html.erb index e4a77c6df..c92171852 100644 --- a/app/views/shared/_invite_users_modal.html.erb +++ b/app/views/shared/_invite_users_modal.html.erb @@ -74,29 +74,38 @@ invite_to_team = type.in?(%w(invite_to_team invite_to_team_with_role))
-
- +

<%= t('invite_users.input_subtitle') %>

<% if current_user && type.in?(['invite_with_team_selector', 'invite_with_team_selector_and_role']) %> <% # Only allow inviting to teams where user is admin %> - <% teams = current_user.teams - .joins(:user_teams) - .where('user_teams.role': UserTeam.roles[:admin]) - .select { |team| can_invite_team_users?(team) } %> - <% if teams.any? %> -
-
- - <%= t('invite_users.invite_to_team_heading') %> +
+

+ <%= t('invite_users.select_team') %> +

+
+ + + - <%= select_tag('team-select', - options_for_select(teams.pluck(:name, :id)), - { class: 'form-control selectpicker', - 'data-role' => 'team-selector-dropdown', - disabled: 'disabled' }) %>
- <% end %> +
<% end %> <% if ENV['ENABLE_RECAPTCHA'] == 'true' %>