Skip to content

Commit

Permalink
user management + some future templates
Browse files Browse the repository at this point in the history
  • Loading branch information
fermion committed Jan 24, 2024
1 parent 59b5aab commit 005da31
Show file tree
Hide file tree
Showing 10 changed files with 315 additions and 23 deletions.
17 changes: 12 additions & 5 deletions app/commands/add_user_to_company.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

class AddUserToCompany

attr_accessor :user
attr_accessor :user, :membership
attr_reader :errors

class InvalidArguments < StandardError; end

def initialize(email:, name:, role: "member", company:)
@email = email
Expand All @@ -12,8 +15,10 @@ def initialize(email:, name:, role: "member", company:)
end

def call
find_or_create_user
add_user_to_company
User.transaction do
find_or_create_user
add_user_to_company
end

@user
end
Expand All @@ -25,10 +30,12 @@ def find_or_create_user
user.name = @name
user.current_company = @company
end

@user.save!
end

def add_user_to_company
@company.memberships.build(user: @user, role: @role, status: 'active')
@company.save!
@membership = @company.memberships.build(user: @user, role: @role, status: 'active')
@membership.save!
end
end
30 changes: 19 additions & 11 deletions app/components/settings/tabs_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
<div>
<div class="sm:hidden" data-controller="settings-tabs">
<label for="tabs" class="sr-only">Select a tab</label>
<select data-action="change->settings-tabs#handleChange" name="tabs" class="block w-full rounded-md border-gray-300 py-2 pl-3 pr-10 text-base focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm">
<%= settings_option "General", settings_path %>
<%= settings_option "User Management", settings_users_path %>
<%= settings_option "Billing", settings_billing_information_path %>
</select>
<div class="relative border-b border-gray-200 pb-5 sm:pb-0" data-controller="settings-tabs">
<div class="md:flex md:items-center md:justify-between">
<div class="mt-3 flex md:absolute md:right-0 md:top-3 md:mt-0">
<%= action_buttons %>
</div>
</div>
<div class="hidden sm:block">
<div class="border-b border-gray-200">
<nav class="-mb-px flex space-x-8" aria-label="Tabs">

<div class="mt-4">
<!-- Dropdown menu on small screens -->
<div class="sm:hidden">
<label for="current-tab" class="sr-only">Select a tab</label>
<select data-action="change->settings-tabs#handleChange" id="current-tab" name="current-tab" class="block w-full rounded-md border-0 py-1.5 pl-3 pr-10 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600">
<%= settings_option "General", settings_path %>
<%= settings_option "User Management", settings_users_path %>
<%= settings_option "Billing", settings_billing_information_path %>
</select>
</div>
<!-- Tabs at small breakpoint and up -->
<div class="hidden sm:block">
<nav class="-mb-px flex space-x-8">
<%= settings_link "General", settings_path %>
<%= settings_link "User Management", settings_users_path %>
<%= settings_link "Billing", settings_billing_information_path %>
Expand Down
18 changes: 13 additions & 5 deletions app/components/settings/tabs_component.rb
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
module Settings
class TabsComponent < ViewComponent::Base

LINK_CSS_CLASS = "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 whitespace-nowrap border-b-2 py-4 px-1 text-sm font-medium"
SELECTED_LINK_CSS_CLASS = "border-indigo-500 text-indigo-600 whitespace-nowrap border-b-2 py-4 px-1 text-sm font-medium"
LINK_CSS_CLASS = "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 whitespace-nowrap border-b-2 px-1 pb-4 text-sm font-medium"
SELECTED_LINK_CSS_CLASS = "border-indigo-500 text-indigo-600 whitespace-nowrap border-b-2 px-1 pb-4 text-sm font-medium"

renders_one :action_buttons

def initialize

end

def settings_link(text, path)
css_class = current_page?(path) ? SELECTED_LINK_CSS_CLASS : LINK_CSS_CLASS
css_class = current_or_starts_with?(path) ? SELECTED_LINK_CSS_CLASS : LINK_CSS_CLASS

options = {}
options[:"aria-current"] = "page" if current_page?(path)
options[:"aria-current"] = "page" if current_or_starts_with?(path)

link_to text, path, class: css_class, **options
end

def settings_option(text, path)
content_tag(:option, text, value: path, selected: current_page?(path))
content_tag(:option, text, value: path, selected: current_or_starts_with?(path))
end

private

def current_or_starts_with?(path)
current_page?(path)
end
end
end
24 changes: 24 additions & 0 deletions app/controllers/settings/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,35 @@ def update
end

def new
@user = User.new
end

def create
begin
@command = AddUserToCompany.new(
email: create_params[:email],
name: create_params[:name],
role: create_params[:role],
company: current_company
)

@command.call

flash[:notice] = 'User added successfully'
redirect_to settings_users_path
rescue ActiveRecord::RecordInvalid
@user = @command.user
@user.valid?
render :new
end
end

def destroy
end

private

def create_params
params.require(:user).permit(:email, :name, :role)
end
end
4 changes: 4 additions & 0 deletions app/views/settings/users/edit.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<div>
<h1 class="font-bold text-4xl">Settings::Users#edit</h1>
<p>Find me in app/views/settings/users/edit.html.erb</p>
</div>
6 changes: 5 additions & 1 deletion app/views/settings/users/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
<%= render(Settings::TabsComponent.new) %>
<%= render(Settings::TabsComponent.new) do |component| %>
<% component.with_action_buttons do %>
<%= link_to "New User", new_settings_user_path, class: "ml-3 inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" %>
<% end %>
<% end %>

<div class="bg-white p-4 mt-4">
<ul role="list" class="divide-y divide-gray-100">
Expand Down
81 changes: 81 additions & 0 deletions app/views/settings/users/new.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<div class="border-b border-gray-200 pb-5">
<%= link_to settings_users_path, class: "ml-3 inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" do %>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" />
</svg>
<span class="ml-2 pr-2">Back</span>
<% end %>
</div>

<div class="bg-white p-4 mt-4">
<%= form_for @user, url: settings_users_path, data: {turbo: false} do |f| %>
<div class="space-y-12">
<div class="pb-12">
<h2 class="text-base font-semibold leading-7 text-gray-900">Add a new user</h2>

<% if @user.errors.any? %>
<div class="rounded-md bg-red-50 p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">There were <%= pluralize(@user.errors.count, "error") %> with adding the user to your company</h3>
<div class="mt-2 text-sm text-red-700">
<ul role="list" class="list-disc space-y-1 pl-5">
<% @user.errors.full_messages.each do |error| %>
<li><%= error %></li>
<% end %>
</ul>
</div>
</div>
</div>
</div>
<% end %>

<div class="mt-10 grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<div class="sm:col-span-3">
<%= f.label :name, "Full name", class: "block text-sm font-medium leading-6 text-gray-900" %>
<div class="mt-2">
<%= f.text_field :name, class: "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" %>
</div>
</div>

<div class="sm:col-span-3">
<%= f.label :email, "E-mail address", class: "block text-sm font-medium leading-6 text-gray-900" %>
<div class="mt-2">
<%= f.email_field :email, class: "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" %>
</div>
</div>

<div class="sm:col-span-3">
<%= label_tag "user_role", "What role should this person have?", class: "block text-sm font-medium leading-6 text-gray-900" %>
<div class="mt-2">
<select id="user_role" name="user[role]" class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6">
<% user_role_param = @user.memberships.first&.role %>
<option value="<%= Membership::MEMBER %>"<%= " selected" if user_role_param.blank? || user_role_param == Membership::MEMBER %>>Member</option>
<option value="<%= Membership::ADMIN %>"<%= " selected" if user_role_param == Membership::ADMIN %>>Admin</option>
<option value="<%= Membership::OWNER %>"<%= " selected" if user_role_param == Membership::OWNER %>>Owner</option>
</select>
</div>
</div>
</div>
</div>

<div class="border-b border-gray-900/10 pb-12">
<h2 class="text-base font-semibold leading-7 text-gray-900">Note</h2>
<p class="mt-1 text-sm leading-6 text-gray-600">
This user will receive an email with a link letting them know they have access to StaffPlan. Your account will be charged for this user.
</p>
</div>
</div>

<div class="mt-6 flex items-center justify-end gap-x-6">
<%= link_to "Cancel", settings_users_path, class: "text-sm font-semibold leading-6 text-gray-900" %>
<%= submit_tag "Create", data: {disable_with: "Please wait..."}, class: "rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" %>
</div>
<% end %>

</div>
4 changes: 4 additions & 0 deletions app/views/settings/users/show.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<div>
<h1 class="font-bold text-4xl">Settings::Users#show</h1>
<p>Find me in app/views/settings/users/show.html.erb</p>
</div>
84 changes: 83 additions & 1 deletion spec/commands/add_user_to_company_spec.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,72 @@
require 'rails_helper'

RSpec.describe "AddUserToCompany" do
context "validations" do
it "is invalid without a name" do
company = create(:company)
command = nil

command = AddUserToCompany.new(
email: Faker::Internet.email,
name: nil,
company: company
)

expect { command.call }.to raise_error(ActiveRecord::RecordInvalid)

user = command.user
expect(user.errors.count).to eq(1)
expect(user.errors[:name]).to eq(["can't be blank"])
end

it "is invalid without an email" do
company = create(:company)

command = AddUserToCompany.new(
email: nil,
name: Faker::Name.name,
company: company
)

expect { command.call }.to raise_error(ActiveRecord::RecordInvalid)
user = command.user

expect(user.errors.count).to eq(2)
expect(user.errors[:email]).to eq(["can't be blank", "is invalid"])
end

it "is invalid without a role" do
company = create(:company)

command = AddUserToCompany.new(
email: Faker::Internet.email,
name: Faker::Name.name,
role: nil,
company: company
)

expect { command.call }.to raise_error(ActiveRecord::RecordInvalid)
membership = command.membership

expect(membership.errors.count).to eq(2)
expect(membership.errors[:role]).to eq(["can't be blank", "is not included in the list"])
end

it "is invalid without a company" do
command = AddUserToCompany.new(
email: Faker::Internet.email,
name: Faker::Name.name,
company: nil
)

expect { command.call }.to raise_error(ActiveRecord::RecordInvalid)
user = command.user

expect(user.errors.count).to eq(2)
expect(user.errors[:current_company]).to eq(["must exist", "can't be blank"])
end
end

context "when given an existing user's email address" do
it "adds the user to the company" do
company = create(:company)
Expand Down Expand Up @@ -63,7 +129,23 @@
expect(company.users).to include user
end

it "sets the company as the user's current company" do
it "does not set the current_company for existing users" do
company = create(:company)
user = create(:user)
current_company = user.current_company
expect(current_company).to eq(user.companies.first)

AddUserToCompany.new(
email: user.email,
name: user.name,
company: company
).call

expect(user.companies.count).to eq(2)
expect(user.reload.current_company).to eq(current_company)
end

it "sets the company as the user's current company for new users" do
company = create(:company)
email = Faker::Internet.email

Expand Down
Loading

0 comments on commit 005da31

Please sign in to comment.