Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions app/controllers/partner_users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@

class PartnerUsersController < ApplicationController
before_action :authorize_admin
before_action :set_partner, only: %i[index create destroy resend_invitation]
before_action :set_partner, only: %i[index lookup create destroy resend_invitation]

def index
@users = @partner.users
@user = User.new(name: "")
end

def lookup
user = User.find_by("LOWER(email) = ?", params[:email].to_s.downcase)
render json: {exists: user.present?, has_name: user&.name.present?}
end

def create
@user = UserInviteService.invite(
email: user_params[:email],
Expand Down Expand Up @@ -64,7 +69,7 @@ def reset_password
private

def set_partner
@partner = Partner.find(params[:partner_id])
@partner = current_organization.partners.find(params[:partner_id])
end

def user_params
Expand Down
47 changes: 47 additions & 0 deletions app/javascript/controllers/existing_user_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static targets = ["email", "name", "message"]
static values = { lookupUrl: String }

disconnect() {
this.abortController?.abort()
}

async lookup() {
this.abortController?.abort()
this.abortController = new AbortController()

const email = this.emailTarget.value.trim()

if (email === "") {
this.resetNameField()
return
}

try {
const response = await fetch(`${this.lookupUrlValue}?email=${encodeURIComponent(email)}`, {
headers: { "Accept": "application/json" },
signal: this.abortController.signal
})
if (!response.ok || this.emailTarget.value.trim() !== email) return

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for debouncing purposes? Wouldn't it be better to cancel the existing fetch if we detect that the name has changed when a fetch is in progress?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need this check now that we're aborting?


const user = await response.json()
if (!user.exists) {
this.resetNameField()
} else {
this.nameTarget.disabled = false
this.messageTarget.textContent = user.has_name
? "This user already exists. Their current profile name will be kept."
: "This user already exists. The submitted name will be used because their profile has no name yet."
}
} catch (error) {
if (error.name !== "AbortError") this.messageTarget.textContent = ""
}
}

resetNameField() {
this.nameTarget.disabled = false
this.messageTarget.textContent = ""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to clear out the name field when an error happens? Why?

}
}
3 changes: 2 additions & 1 deletion app/services/user_invite_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ def self.invite(email:, resource:, name: nil, roles: [], force: false)
roles.append(Role::ORG_USER)
end

user = User.find_by(email: email)
user = User.find_by("LOWER(email) = ?", email.to_s.downcase)

# return if user already has all the roles we're trying to add
if !force && user && roles.all? { |role| user.has_role?(role, resource) }
raise "User already has the requested role!"
end

if user
user.update!(name: name) if user.name.blank? && name.present?
add_roles(user, resource: resource, roles: roles)
if force
user.invite!
Expand Down
21 changes: 18 additions & 3 deletions app/views/partner_users/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,28 @@
<h3 class="card-title">Invite New User</h3>
</div>

<%= simple_form_for user, url: partner_users_path(partner) do |f| %>
<%= simple_form_for user,
url: partner_users_path(partner),
html: {
data: {
controller: "existing-user",
existing_user_lookup_url_value: lookup_partner_users_path(partner)
}
} do |f| %>
<div class="card-body">
<div class="form-group">
<%= f.input :name, label: "Name", placeholder: "Name", required: true %>
<%= f.input :name, label: "Name", placeholder: "Name", required: true,
input_html: {data: {existing_user_target: "name"}} %>
</div>
<div class="form-group">
<%= f.input :email, label: "Email", placeholder: "Email", required: true %>
<%= f.input :email, label: "Email", placeholder: "Email", required: true,
input_html: {
data: {
existing_user_target: "email",
action: "blur->existing-user#lookup"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be on blur or on change?

}
} %>
<small class="form-text text-muted" data-existing-user-target="message" role="status" aria-live="polite" aria-atomic="true"></small>
</div>
</div>

Expand Down
3 changes: 3 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,9 @@ def set_up_flipper

resources :partners do
resources :users, only: [:index, :create, :destroy], controller: 'partner_users' do
collection do
get :lookup
end
member do
post :resend_invitation
post :reset_password
Expand Down
39 changes: 38 additions & 1 deletion spec/requests/partner_users_requests_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# spec/requests/partner_users_controller_spec.rb

RSpec.describe PartnerUsersController, type: :request do
let!(:partner) { create(:partner) } # Assuming you have a factory for creating partners
let!(:partner) { create(:partner, organization: organization) }
let(:organization) { create(:organization) }
let(:user) { create(:user, organization: organization) }
let(:org_admin) { create(:organization_admin, organization: organization) }
Expand Down Expand Up @@ -36,6 +36,43 @@
end
end

describe "GET #lookup" do
before do
sign_in(org_admin)
end

it "reports when a user already exists" do
existing_user = create(:user, name: nil, email: "existing@example.com")

get lookup_partner_users_path(
default_params.merge(partner_id: partner, email: existing_user.email.upcase)
)

expect(response).to have_http_status(:ok)
expect(response.parsed_body).to eq("exists" => true, "has_name" => false)
end

it "reports when an existing user has a name" do
existing_user = create(:user, name: "Existing Name", email: "named@example.com")

get lookup_partner_users_path(
default_params.merge(partner_id: partner, email: existing_user.email)
)

expect(response).to have_http_status(:ok)
expect(response.parsed_body).to eq("exists" => true, "has_name" => true)
end

it "reports when a user does not exist" do
get lookup_partner_users_path(
default_params.merge(partner_id: partner, email: "missing@example.com")
)

expect(response).to have_http_status(:ok)
expect(response.parsed_body).to eq("exists" => false, "has_name" => false)
end
end

describe "POST #create" do
let(:valid_user_params) do
{
Expand Down
31 changes: 30 additions & 1 deletion spec/services/user_invite_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,42 @@
end

it "should add roles to existing user" do
described_class.invite(email: "email@email.com",
described_class.invite(email: "EMAIL@EMAIL.COM",
roles: [Role::ORG_USER, Role::ORG_ADMIN],
resource: organization)
expect(user).to have_role(Role::ORG_USER, organization)
expect(user).to have_role(Role::ORG_ADMIN, organization)
expect(user).not_to have_role(Role::PARTNER, :any)
end

it "should find an existing user case-insensitively" do
uppercase_email = user.email.upcase
target_partner = partner
expect(User.find_by("LOWER(email) = ?", uppercase_email.downcase)).to eq(user)

expect {
described_class.invite(
email: uppercase_email,
roles: [Role::PARTNER],
resource: target_partner
)
}.not_to change(User, :count)

expect(user.reload).to have_role(Role::PARTNER, target_partner)
end

it "should add a submitted name when the existing user has no name" do
user.update!(name: nil)

described_class.invite(
name: "Existing User",
email: user.email,
roles: [Role::PARTNER],
resource: partner
)

expect(user.reload.name).to eq("Existing User")
end
end

context "with a new user" do
Expand Down
23 changes: 23 additions & 0 deletions spec/system/partner_system_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,29 @@
expect(page).to have_content(partner_user.name)
expect(page).to have_content(partner_user.email)
end

it 'checks an existing user when leaving the email field', js: true do
existing_user = create(:user, name: nil, email: "existing@example.com")
visit subject

fill_in "Email", with: existing_user.email
find_field("Email").send_keys(:tab)

expect(page).to have_content("This user already exists. The submitted name will be used because their profile has no name yet.")
expect(page).to have_field("Name", disabled: false)
end

it 'keeps the name of an existing named user', js: true do
existing_user = create(:user, name: "Existing Name", email: "named@example.com")
visit subject

fill_in "Name", with: "Replacement Name"
fill_in "Email", with: existing_user.email
find_field("Email").send_keys(:tab)

expect(page).to have_content("This user already exists. Their current profile name will be kept.")
expect(page).to have_field("Name", with: "Replacement Name", disabled: false)
end
end

context "when partner has :awaiting_review status" do
Expand Down