Skip to content

Commit

Permalink
[FS-6] Create companies admin panel (#21)
Browse files Browse the repository at this point in the history
* [FS-6] Add company policy

* [FS-6] Add companies helper

* [FS-6] Configure navigation properly

* [FS-6] Add companies admin

* [FS-6] Do assets precompile before running tests in CI
  • Loading branch information
beetlegius-jt authored Jan 21, 2025
1 parent bb9531f commit bf01b08
Show file tree
Hide file tree
Showing 22 changed files with 295 additions and 15 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,10 @@ jobs:
RAILS_ENV: test
DATABASE_URL: postgres://postgres:postgres@localhost:5432
# REDIS_URL: redis://localhost:6379/0
run: bin/rails db:test:prepare && bin/rspec
run: |
bin/rails db:test:prepare
bin/rails assets:precompile
bin/rspec
- name: Keep screenshots from failed system tests
uses: actions/upload-artifact@v4
Expand Down
4 changes: 3 additions & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Omakase Ruby styling for Rails
inherit_gem: { rubocop-rails-omakase: rubocop.yml }
inherit_gem:
rubocop-rails-omakase: rubocop.yml
pundit: config/rubocop-rspec.yml

# Overwrite or add rules to create your own house style
#
Expand Down
1 change: 1 addition & 0 deletions app/controllers/admin/base_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module Admin
class BaseController < ApplicationController
include HasCompany
include ErrorHandler

before_action :authenticate_user!
after_action :verify_authorized
Expand Down
32 changes: 32 additions & 0 deletions app/controllers/admin/companies_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
module Admin
class CompaniesController < BaseController
before_action :set_company

def show
end

def edit
end

def update
@company.update!(company_params)

flash.now.notice = notice_message

respond_to do |format|
format.turbo_stream { render_flash }
format.html { redirect_to edit_admin_company_path, notice: notice_message }
end
end

private

def set_company
@company = authorize Current.company
end

def company_params
params.require(:company).permit(:name, :subdomain, :utc_offset, :logo)
end
end
end
14 changes: 14 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,18 @@ class ApplicationController < ActionController::Base

# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
allow_browser versions: :modern

private

def render_flash
render turbo_stream: turbo_stream.update(:flash, partial: "layouts/flash")
end

def notice_message(key = action_name)
t(key, scope: :notice)
end

def alert_message(key = action_name)
t(key, scope: :alert)
end
end
16 changes: 16 additions & 0 deletions app/controllers/concerns/error_handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module ErrorHandler
extend ActiveSupport::Concern

included do
rescue_from ActiveRecord::RecordInvalid, with: :handle_record_invalid

def handle_record_invalid(exception)
flash.now.alert = exception.message

respond_to do |format|
format.turbo_stream { render_flash }
format.html { render :edit }
end
end
end
end
7 changes: 7 additions & 0 deletions app/helpers/companies_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module CompaniesHelper
def options_for_utc_offset
Company::UTC_OFFSETS.map do |utc_offset|
[ ActiveSupport::TimeZone.seconds_to_utc_offset(utc_offset), utc_offset ]
end
end
end
22 changes: 22 additions & 0 deletions app/policies/company_policy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
class CompanyPolicy < ApplicationPolicy
# NOTE: Up to Pundit v2.3.1, the inheritance was declared as
# `Scope < Scope` rather than `Scope < ApplicationPolicy::Scope`.
# In most cases the behavior will be identical, but if updating existing
# code, beware of possible changes to the ancestors:
# https://gist.github.com/Burgestrand/4b4bc22f31c8a95c425fc0e30d7ef1f5

def show?
user&.company == Current.company
end

def update?
user&.company == Current.company
end

class Scope < ApplicationPolicy::Scope
# NOTE: Be explicit about which records you allow access to!
# def resolve
# scope.all
# end
end
end
7 changes: 7 additions & 0 deletions app/views/admin/base/_navigation_brand.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<%= link_to admin_root_path, class: "navbar-brand" do %>
<% if Current.company.logo.attached? %>
<%= image_tag Current.company.logo.variant(:thumb), class: "img-fluid" %>
<% else %>
<%= Current.company.name %>
<% end %>
<% end %>
1 change: 1 addition & 0 deletions app/views/admin/base/_navigation_links.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= nav_item t(".edit_company"), edit_admin_company_path if Current.user.company? %>
33 changes: 33 additions & 0 deletions app/views/admin/companies/_form.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<div class="card">
<div class="card-header">
<%= t '.title' %>
</div>

<%= form_with(model: company, url: admin_company_path) do |form| %>
<div class="card-body">
<div class="mb-3">
<%= form.label :name, class: 'form-label' %>
<%= form.text_field :name, autofocus: true, class: 'form-control' %>
</div>

<div class="mb-3">
<%= form.label :subdomain, class: 'form-label' %>
<%= form.text_field :subdomain, class: 'form-control' %>
</div>

<div class="mb-3">
<%= form.label :utc_offset, class: 'form-label' %>
<%= form.select :utc_offset, options_for_select(options_for_utc_offset, form.object.utc_offset), {}, class: 'form-control' %>
</div>

<div class="mb-3">
<%= form.label :logo, class: 'form-label' %>
<%= form.file_field :logo, class: 'form-control' %>
</div>
</div>

<div class="card-footer">
<%= form.submit class: 'btn btn-primary' %>
</div>
<% end %>
</div>
1 change: 1 addition & 0 deletions app/views/admin/companies/edit.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render "form", company: @company %>
1 change: 1 addition & 0 deletions app/views/admin/companies/show.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1><%= Current.company.name %> %>
11 changes: 11 additions & 0 deletions app/views/application/_navigation_brand.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<span class="navbar-brand">
<% if Current.company.present? %>
<% if Current.company.logo.attached? %>
<%= image_tag Current.company.logo.variant(:thumb), class: "img-fluid" %>
<% else %>
<%= Current.company.name %>
<% end %>
<% else %>
Fit Shift
<% end %>
</span>
16 changes: 3 additions & 13 deletions app/views/layouts/_navigation.html.erb
Original file line number Diff line number Diff line change
@@ -1,27 +1,17 @@
<nav class="navbar navbar-expand-lg navbar-light py-4">
<div class="container-fluid">
<%= link_to root_path, class: "navbar-brand" do %>
<% if Current.company.present?%>
<% if Current.company.logo.attached? %>
<%= image_tag Current.company.logo.variant(:thumb), class: "img-fluid" %>
<% else %>
<%= Current.company.name %>
<% end %>
<% else %>
Fit Shift
<% end%>
<% end %>
<%= render "navigation_brand" %>

<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#nav">
<span class="navbar-toggler-icon"></span>
</button>

<div class="collapse navbar-collapse" id="nav">
<ul class="d-none d-lg-flex navbar-nav mt-3 mt-lg-0 mb-3 mb-lg-0">
<ul id="web-nav" class="d-none d-lg-flex navbar-nav mt-3 mt-lg-0 mb-3 mb-lg-0">
<%= render "navigation_links" %>
<%= render "layouts/user_navigation_links" %>
</ul>
<ul class="navbar-nav mt-3 mt-lg-0 mb-3 mb-lg-0 d-lg-none">
<ul id="mobile-nav" class="navbar-nav mt-3 mt-lg-0 mb-3 mb-lg-0 d-lg-none">
<%= render "navigation_links" %>
<%= render "layouts/user_navigation_links" %>
</ul>
Expand Down
20 changes: 20 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,29 @@
# enabled: "ON"

en:
activerecord:
attributes:
company:
name: Company name
subdomain: Company subdomain
utc_offset: UTC offset
logo: Logo
admin:
companies:
form:
title: Configure your company
base:
navigation_links:
edit_company: Configuration
application:
navigation_links:
admin: Admin panel
helpers:
submit:
update: Save changes
notice:
update: The changes were saved
layouts:
user_navigation_links:
sign_in: Sign in
sign_out: Sign out
3 changes: 3 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

constraints CompanyConstraint.new do
namespace :admin do
resource :company, only: [ :edit, :update ]

root to: "companies#show"
end
end

Expand Down
18 changes: 18 additions & 0 deletions spec/helpers/companies_helper_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
require 'rails_helper'

RSpec.describe CompaniesHelper, type: :helper do
describe 'options_for_utc_offset' do
let(:utc_offset) { "whatever" }
let(:seconds_to_utc_offset) { "text" }

before do
stub_const("Company::UTC_OFFSETS", [ utc_offset ])

allow(ActiveSupport::TimeZone).to receive(:seconds_to_utc_offset).with(utc_offset).and_return(seconds_to_utc_offset)
end

subject { helper.options_for_utc_offset }

it { is_expected.to eq([ [ seconds_to_utc_offset, utc_offset ] ]) }
end
end
35 changes: 35 additions & 0 deletions spec/policies/company_policy_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
require 'rails_helper'

RSpec.describe CompanyPolicy, type: :policy do
let(:company) { build(:company) }

subject { described_class }

before { Current.company = company }

permissions :show?, :update? do
context "when the user is not logged in" do
let(:user) { nil }

it { is_expected.not_to permit(user, company) }
end

context "when the user is an admin" do
let(:user) { build(:user, :admin) }

it { is_expected.not_to permit(user, company) }
end

context "when the user is a customer" do
let(:user) { build(:user, :customer) }

it { is_expected.not_to permit(user, company) }
end

context "when the user is a company" do
let(:user) { build(:user, :company, owner: company) }

it { is_expected.to permit(user, company) }
end
end
end
2 changes: 2 additions & 0 deletions spec/rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@
# return unless Rails.env.test?
require 'rspec/rails'
# Add additional requires below this line. Rails is not loaded until this point!
require 'support/devise'
require 'support/factory_bot'
require 'support/shoulda'
require 'support/system_tests'
require 'capybara/rspec'
require 'pundit/rspec'

# Requires supporting ruby files with custom matchers and macros, etc, in
# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
Expand Down
9 changes: 9 additions & 0 deletions spec/support/devise.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
RSpec.configure do |config|
# config.include Devise::Test::IntegrationHelpers, type: :request
config.include Devise::Test::IntegrationHelpers, type: :system

# TODO: Remove when Devise fixes https://github.com/heartcombo/devise/issues/5705
config.before(:each, type: :system) do
Rails.application.reload_routes_unless_loaded
end
end
52 changes: 52 additions & 0 deletions spec/system/admin/admin_companies_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
require 'rails_helper'

RSpec.describe "Admin::Companies", type: :system do
let(:subdomain) { "something" }
let(:company) { create(:company, subdomain:) }
let(:user) { create(:user, :company, owner: company) }

before { sign_in(user) }

it "can update the company configuration" do
visit admin_root_url(subdomain:)

within("#web-nav") do
find("a[href='#{edit_admin_company_path}']").click
end

new_name = "whatever name"
new_subdomain = "whatever-subdomain"

fill_in :company_name, with: new_name
fill_in :company_subdomain, with: new_subdomain

click_button I18n.t(:update, scope: "helpers.submit")

expect(page).to have_current_path(edit_admin_company_path)
expect(page).to have_content I18n.t(:update, scope: :notice)

company.reload

expect(company.name).to eq(new_name)
expect(company.subdomain).to eq(new_subdomain)
end

it "shows errors when attempting to update a company with invalid attributes" do
visit admin_root_url(subdomain:)

within("#web-nav") do
find("a[href='#{edit_admin_company_path}']").click
end

fill_in :company_name, with: nil

click_button I18n.t(:update, scope: "helpers.submit")

message = "Validation failed: Company name can't be blank"

expect(page).to have_current_path(admin_company_path)
expect(page).to have_content(message)

expect(company.reload.name).to be_present
end
end

0 comments on commit bf01b08

Please sign in to comment.