Skip to content

Commit

Permalink
Merge pull request #14160 from opf/implementation/50730-show-warning-…
Browse files Browse the repository at this point in the history
…if-you-cannot-add-new-users-due-to-user-limit

[50730] Show warning if you cannot add new users due to user limit
  • Loading branch information
aaron-contreras authored Nov 20, 2023
2 parents 875ecc4 + 557d4b7 commit 743ae22
Show file tree
Hide file tree
Showing 14 changed files with 225 additions and 47 deletions.
1 change: 1 addition & 0 deletions app/components/_index.sass
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
@import "work_packages/share/modal_body_component"
@import "work_packages/share/invite_user_form_component"
Original file line number Diff line number Diff line change
@@ -1,24 +1,48 @@
<%=
component_wrapper do
component_wrapper(data: { controller: "user-limit",
'user-limit-open-seats-value': OpenProject::Enterprise.open_seats_count,
'application-target': "dynamic" }) do
if sharing_manageable?
primer_form_with(
model: new_share,
url: work_package_shares_path(@work_package)
) do |form|
flex_layout do |new_user_row|
new_user_row.with_column(flex: :auto, mr: 2) do
grid_layout('invite-user-form',
tag: :div,
data: { 'user-limit-target': 'inviteUserForm' }) do |invite_form|
invite_form.with_area('invitee') do
render(WorkPackages::Share::Invitee.new(form))
end

new_user_row.with_column(mr: 2) do
invite_form.with_area('permission') do
render(WorkPackages::Share::PermissionButtonComponent.new(share: new_share,
form_arguments: { builder: form, name: "role_id" },
data: { 'test-selector': 'op-share-wp-invite-role' }))
end

new_user_row.with_column do
invite_form.with_area('submit') do
render(Primer::Beta::Button.new(scheme: :primary, type: :submit)) { I18n.t('work_package.sharing.share') }
end

if OpenProject::Enterprise.user_limit.present?
invite_form.with_area('userLimit',
data: {
'user-limit-target': 'limitWarning',
'test-selector': 'op-share-wp-user-limit'
},
display: :none) do
flex_layout do |user_limit_row|
user_limit_row.with_column(mr: 2) do
render(Primer::Beta::Octicon.new(icon: :'alert-fill', color: :danger))
end

user_limit_row.with_column do
render(Primer::Beta::Text.new(color: :danger)) { I18n.t("work_package.sharing.warning_user_limit_reached#{'_admin' if User.current.admin?}",
upgrade_url: OpenProject::Enterprise.upgrade_url).html_safe }
end
end
end
end
end
end
else
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.invite-user-form
display: grid
grid-template-columns: 1fr auto auto
grid-template-areas: "invitee permission submit" "userLimit userLimit userLimit"
grid-column-gap: 0.5rem
14 changes: 10 additions & 4 deletions app/controllers/concerns/accounts/user_limits.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,16 @@ def show_user_limit_error!
end

def user_limit_warning
warning = I18n.t(
:warning_user_limit_reached,
upgrade_url: OpenProject::Enterprise.upgrade_url
)
warning = if current_user.admin?
I18n.t(
:warning_user_limit_reached_admin,
upgrade_url: OpenProject::Enterprise.upgrade_url
)
else
I18n.t(
:warning_user_limit_reached
)
end

warning.html_safe
end
Expand Down
1 change: 1 addition & 0 deletions app/forms/work_packages/share/invitee.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class Invitee < ApplicationForm
name: :user_id,
label: I18n.t('work_package.sharing.label_search'),
visually_hide_label: true,
data: { 'work-packages--share--user-limit-target': 'autocompleter' },
autocomplete_options: {
id: "op-share-wp-invite-autocomplete",
placeholder: I18n.t('work_package.sharing.label_search_placeholder'),
Expand Down
24 changes: 17 additions & 7 deletions app/views/members/_member_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,23 @@ See COPYRIGHT and LICENSE files for more details.
method: :post,
html: { id: "members_add_form",
class: "form -vertical -bordered -medium-compressed"},
data: { 'members-form-target': 'addMemberForm' }) do |f| %>
data: {
'members-form-target': 'addMemberForm',
controller: "user-limit",
'user-limit-open-seats-value': OpenProject::Enterprise.open_seats_count,
'user-limit-member-autocompleter-value': true,
'application-target': "dynamic",
}) do |f| %>
<a title="<%= t('js.close_form_title') %>"
class="hide-member-form-button form--close icon-context icon-close"
data-action="members-form#hideAddMemberForm"></a>
<div id="new-member-message"></div>
<div class="grid-block">
<div class="grid-content medium-5 small-12 collapse -flex">
<div class="form--field">
<div
class="form--field"
data-user-limit-target="inviteUserForm"
>
<%
user_id_title = I18n.t(:label_principal_search)

Expand Down Expand Up @@ -90,13 +99,14 @@ See COPYRIGHT and LICENSE files for more details.
</div>
</div>
</div>
<% if OpenProject::Enterprise.user_limit_reached? %>
<div class="op-toast -warning icon-warning"
data-members-form-target="limitWarning"
style="display: none;">

<% if OpenProject::Enterprise.user_limit.present? %>
<div class="op-toast -warning icon-warning d-none"
data-user-limit-target="limitWarning">
<div class="op-toast--content">
<p><%= I18n.t(:warning_user_limit_reached, upgrade_url: OpenProject::Enterprise.upgrade_path).html_safe %></p>
<p><%= I18n.t("warning_user_limit_reached#{'_admin' if current_user.admin?}", upgrade_url: OpenProject::Enterprise.upgrade_url).html_safe %></p>
</div>
</div>
<% end %>

<% end %>
11 changes: 9 additions & 2 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3200,8 +3200,9 @@ en:
The activation email has expired. We sent you a new one to %{email}.
Please click the link inside of it to activate your account.
warning_user_limit_reached: >
User limit reached. You cannot activate any more users.
Please <a href="%{upgrade_url}">upgrade your plan</a> or block members to allow for additional users.
Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this instance.
warning_user_limit_reached_admin: >
Adding additional users will exceed the current limit. Please <a href="%{upgrade_url}">upgrade your plan</a> to be able to ensure external users are able to access this instance.
warning_user_limit_reached_instructions: >
You reached your user limit (%{current}/%{max} active users).
Please contact [email protected] to upgrade your Enterprise edition plan and add additional users.
Expand Down Expand Up @@ -3265,6 +3266,12 @@ en:
share: "Share"
text_empty_state_description: "The work package has not been shared with anyone yet."
text_empty_state_header: "No shared users"
text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package."
text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please <a href="%{upgrade_url}">upgrade your plan</a> to be able to add more users.'
warning_user_limit_reached: >
Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this work package.
warning_user_limit_reached_admin: >
Adding additional users will exceed the current limit. Please <a href="%{upgrade_url}">upgrade your plan</a> to be able to ensure external users are able to access this work package.
working_days:
info: >
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,6 @@
*/

import { Controller } from '@hotwired/stimulus';
import {
IUserAutocompleteItem,
} from 'core-app/shared/components/autocompleter/user-autocompleter/user-autocompleter.component';

export default class MembersFormController extends Controller {
static targets = [
Expand All @@ -43,7 +40,6 @@ export default class MembersFormController extends Controller {
'addMemberButton',
'membershipEditForm',
'errorExplanation',
'limitWarning',
];

declare readonly filterContainerTarget:HTMLElement;
Expand All @@ -62,12 +58,7 @@ export default class MembersFormController extends Controller {

declare readonly hasErrorExplanationTarget:HTMLElement;

declare readonly limitWarningTarget:HTMLElement;

declare readonly hasLimitWarningTarget:HTMLElement;

private autocompleter:HTMLElement;
private autocompleterListener = this.triggerLimitWarningIfReached.bind(this);

connect() {
// Show/Hide content when page is loaded
Expand All @@ -80,7 +71,6 @@ export default class MembersFormController extends Controller {
}

this.autocompleter = this.addMemberFormTarget.querySelector('opce-members-autocompleter') as HTMLElement;
this.autocompleter.addEventListener('valueChange', this.autocompleterListener);

if (this.hasErrorExplanationTarget && this.errorExplanationTarget.textContent !== '') {
this.showAddMemberForm();
Expand All @@ -91,10 +81,6 @@ export default class MembersFormController extends Controller {
}
}

disconnect() {
this.autocompleter.removeEventListener('valueChange', this.autocompleterListener);
}

hideFilter() {
this.filterContainerTarget.classList.add('collapsed');
}
Expand All @@ -119,18 +105,6 @@ export default class MembersFormController extends Controller {
this.focusAutocompleter();
}

triggerLimitWarningIfReached(evt:CustomEvent) {
const values = evt.detail as IUserAutocompleteItem[];

if (this.hasLimitWarningTarget) {
if (values.find(({ id }) => typeof (id) === 'string' && id.includes('@'))) {
this.limitWarningTarget.style.display = 'block';
} else {
this.limitWarningTarget.style.display = 'none';
}
}
}

toggleMemberFilter() {
if (window.OpenProject.guardedLocalStorage('showFilter') === 'true') {
window.OpenProject.guardedLocalStorage('showFilter', 'false');
Expand Down
85 changes: 85 additions & 0 deletions frontend/src/stimulus/controllers/dynamic/user-limit.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* -- copyright
* OpenProject is an open source project management software.
* Copyright (C) 2023 the OpenProject GmbH
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 3.
*
* OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
* Copyright (C) 2006-2013 Jean-Philippe Lang
* Copyright (C) 2010-2013 the ChiliProject Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* See COPYRIGHT and LICENSE files for more details.
* ++
*/

import { Controller } from '@hotwired/stimulus';
import {
IUserAutocompleteItem,
} from 'core-app/shared/components/autocompleter/user-autocompleter/user-autocompleter.component';

export default class UserLimitController extends Controller {
static targets = [
'limitWarning',
'inviteUserForm',
];

static values = {
openSeats: Number,
// Special case, that the autocompleter is a members-autocompleter, instead of the normal user-autocompleter
memberAutocompleter: Boolean,
};

declare readonly limitWarningTarget:HTMLElement;
declare readonly hasLimitWarningTarget:HTMLElement;
declare readonly inviteUserFormTarget:HTMLElement;

declare readonly openSeatsValue:number;
declare readonly hasOpenSeatsValue:number;
declare readonly memberAutocompleterValue:boolean;

private autocompleter:HTMLElement;
private autocompleterListener = this.triggerLimitWarningIfReached.bind(this);

connect() {
if (this.memberAutocompleterValue) {
this.autocompleter = this.inviteUserFormTarget.querySelector('opce-members-autocompleter') as HTMLElement;
} else {
this.autocompleter = this.inviteUserFormTarget.querySelector('opce-user-autocompleter') as HTMLElement;
}

this.autocompleter.addEventListener('change', this.autocompleterListener);
}

disconnect() {
this.autocompleter.removeEventListener('change', this.autocompleterListener);
}

triggerLimitWarningIfReached(evt:CustomEvent) {
const values = evt.detail as IUserAutocompleteItem[];

if (this.hasLimitWarningTarget && this.hasOpenSeatsValue) {
const numberOfNewUsers = values.filter(({ id }) => typeof (id) === 'string' && id.includes('@')).length;
if (numberOfNewUsers > 0 && numberOfNewUsers > this.openSeatsValue) {
this.limitWarningTarget.classList.remove('d-none');
} else {
this.limitWarningTarget.classList.add('d-none');
}
}
}
}
4 changes: 4 additions & 0 deletions lib/open_project/enterprise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ def active_user_count
User.human.active.count
end

def open_seats_count
user_limit - active_user_count if user_limit
end

##
# Indicates if there are more active users than the support token allows for.
#
Expand Down
6 changes: 3 additions & 3 deletions spec/controllers/users_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
end

it "shows a user limit error" do
expect(flash[:error]).to match /user limit reached/i
expect(flash[:error]).to match /Adding additional users will exceed the current limit/i
end

it "redirects back to the user index" do
Expand All @@ -96,7 +96,7 @@
end

it "shows a user limit warning" do
expect(flash[:warning]).to match /user limit reached/i
expect(flash[:warning]).to match /Adding additional users will exceed the current limit/i
end

it "shows the new user page" do
Expand Down Expand Up @@ -451,7 +451,7 @@
let(:user_limit_reached) { true }

it "shows the user limit reached error and recommends to upgrade" do
expect(flash[:error]).to match /user limit reached.*upgrade/i
expect(flash[:error]).to match /Adding additional users will exceed the current limit.*upgrade/i
end

it "does not activate the user" do
Expand Down
26 changes: 26 additions & 0 deletions spec/features/members/invitation_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,32 @@
members_page.visit!
expect(members_page).to have_user 'finkelstein @openproject.com'
end

context 'with an instance with a user limit (regression)' do
before do
allow(OpenProject::Enterprise).to receive_messages(
user_limit: 10,
open_seats_count: 1
)
end

it 'shows a warning when the limit is reached' do
members_page.visit!
click_button 'Add member'

members_page.search_and_select_principal! '[email protected]',
'Send invite to [email protected]'

expect(members_page).not_to have_text sanitize_string(I18n.t(:warning_user_limit_reached)),
normalize_ws: true

members_page.search_and_select_principal! '[email protected]',
'Send invite to [email protected]'

expect(members_page).to have_text sanitize_string(I18n.t(:warning_user_limit_reached)),
normalize_ws: true
end
end
end

context 'with a registered user' do
Expand Down
Loading

0 comments on commit 743ae22

Please sign in to comment.