Browse Source

CLDC-673: Organisation Page frontend (#122)

* Add basic info read page

* Header

* Better org seed

* Use gem component

* Tabs

* Users

* Users is a table rather than a summary list

* Table rows

* User is trackable

* Date format

* View component init

* Add pages and routes to render

* Partials

* Match the prototype design

* Nested layout is nicer

* Except of course they're not the same width

* Add button with wrong link

* Add user routes

* Don't move account route for now

* Add component test

* Rubocop

* Request spec

* Test views

* Add a feature spec for tab switching

* Move styling methods into helper

* PR suggestions

* PR comments
pull/124/head
Daniel Baark 3 years ago committed by GitHub
parent
commit
c2a0ccc929
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      Gemfile
  2. 1
      Gemfile.lock
  3. 15
      app/components/tab_navigation_component.html.erb
  4. 14
      app/components/tab_navigation_component.rb
  5. 14
      app/controllers/organisations_controller.rb
  6. 12
      app/helpers/user_table_helper.rb
  7. 76
      app/javascript/styles/_tab-navigation.scss
  8. 6
      app/javascript/styles/application.scss
  9. 17
      app/models/organisation.rb
  10. 5
      app/models/user.rb
  11. 3
      app/views/layouts/application.html.erb
  12. 23
      app/views/layouts/organisations.html.erb
  13. 16
      app/views/organisations/show.html.erb
  14. 23
      app/views/organisations/users.html.erb
  15. 14
      config/routes.rb
  16. 12
      db/migrate/20211130144840_add_user_last_logged_in.rb
  17. 7
      db/schema.rb
  18. 11
      db/seeds.rb
  19. 2
      package.json
  20. 27
      spec/components/tab_navigation_component_spec.rb
  21. 2
      spec/factories/user.rb
  22. 29
      spec/features/organisation_spec.rb
  23. 19
      spec/helpers/user_table_helper_spec.rb
  24. 3
      spec/rails_helper.rb
  25. 59
      spec/requests/organisations_controller_spec.rb
  26. 1328
      yarn.lock

1
Gemfile

@ -35,6 +35,7 @@ gem "json-schema"
gem "devise" gem "devise"
gem "turbo-rails", "~> 0.8" gem "turbo-rails", "~> 0.8"
gem "uk_postcode" gem "uk_postcode"
gem "view_component"
group :development, :test do group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console # Call 'byebug' anywhere in the code to stop execution and get a debugger console

1
Gemfile.lock

@ -438,6 +438,7 @@ DEPENDENCIES
turbo-rails (~> 0.8) turbo-rails (~> 0.8)
tzinfo-data tzinfo-data
uk_postcode uk_postcode
view_component
web-console (>= 4.1.0) web-console (>= 4.1.0)
webpacker (~> 5.0) webpacker (~> 5.0)

15
app/components/tab_navigation_component.html.erb

@ -0,0 +1,15 @@
<nav class="app-tab-navigation" aria-label="sub menu">
<ul class="app-tab-navigation__list">
<% items.each do |item| %>
<% if item.fetch(:current, false) || current_page?(strip_query(item.fetch(:url))) %>
<li class="app-tab-navigation__item app-tab-navigation__item--current">
<%= govuk_link_to item[:name], item[:url], class: 'app-tab-navigation__link', aria: { current: 'page' } %>
</li>
<% else %>
<li class="app-tab-navigation__item">
<%= govuk_link_to item[:name], item[:url], class: 'app-tab-navigation__link' %>
</li>
<% end %>
<% end %>
</ul>
</nav>

14
app/components/tab_navigation_component.rb

@ -0,0 +1,14 @@
class TabNavigationComponent < ViewComponent::Base
attr_reader :items
def initialize(items:)
@items = items
super
end
def strip_query(url)
url = Addressable::URI.parse(url)
url.query_values = nil
url.to_s
end
end

14
app/controllers/organisations_controller.rb

@ -0,0 +1,14 @@
class OrganisationsController < ApplicationController
before_action :authenticate_user!
before_action :find_organisation
def users
render "users"
end
private
def find_organisation
@organisation = Organisation.find(params[:id])
end
end

12
app/helpers/user_table_helper.rb

@ -0,0 +1,12 @@
module UserTableHelper
include GovukLinkHelper
def user_cell(user)
[govuk_link_to(user.name, user), user.email].join("\n")
end
def org_cell(user)
role = "<span class='app-!-colour-muted'>#{user.role}</span>"
[user.organisation.name, role].join("\n")
end
end

76
app/javascript/styles/_tab-navigation.scss

@ -0,0 +1,76 @@
.app-tab-navigation {
@include govuk-font(19, $weight: bold);
@include govuk-responsive-margin(6, "bottom");
}
.app-tab-navigation__list {
@include govuk-clearfix;
left: govuk-spacing(-3);
list-style: none;
margin: 0;
padding: 0;
position: relative;
right: govuk-spacing(-3);
width: calc(100% + #{govuk-spacing(6)});
@include govuk-media-query($from: tablet) {
box-shadow: inset 0 -1px 0 $govuk-border-colour;
}
}
.app-tab-navigation__item {
box-sizing: border-box;
display: block;
line-height: 40px;
height: 40px;
padding: 0 govuk-spacing(3);
@include govuk-media-query($from: tablet) {
box-shadow: none;
display: block;
float: left;
line-height: 50px;
height: 50px;
padding: 0 govuk-spacing(3);
position: relative;
}
}
.app-tab-navigation__item--current {
@include govuk-media-query($until: tablet) {
border-left: 4px solid $govuk-link-colour;
padding-left: 11px;
}
@include govuk-media-query($from: tablet) {
border-bottom: 4px solid $govuk-link-colour;
padding-left: govuk-spacing(3);
}
}
.app-tab-navigation__link {
@include govuk-link-common;
@include govuk-link-style-no-visited-state;
@include govuk-link-style-no-underline;
@include govuk-typography-weight-bold;
&:not(:focus):hover {
color: $govuk-link-colour;
}
// Extend the touch area of the link to the list
&:after {
bottom: 0;
content: "";
left: 0;
position: absolute;
right: 0;
top: 0;
}
}
.app-tab-navigation__item--current .app-tab-navigation__link {
&:hover {
text-decoration: none;
}
}

6
app/javascript/styles/application.scss

@ -12,6 +12,7 @@ $govuk-image-url-function: frontend-image-url;
@import "~govuk-frontend/govuk/all"; @import "~govuk-frontend/govuk/all";
@import '_task-list'; @import '_task-list';
@import '_tab-navigation';
$govuk-global-styles: true; $govuk-global-styles: true;
@ -19,3 +20,8 @@ $govuk-global-styles: true;
// display: block; // display: block;
// border: 1px solid blue // border: 1px solid blue
// } // }
//overrides
.app-\!-colour-muted {
color: $govuk-secondary-text-colour !important;
}

17
app/models/organisation.rb

@ -14,4 +14,21 @@ class Organisation < ApplicationRecord
def not_completed_case_logs def not_completed_case_logs
case_logs.not_completed case_logs.not_completed
end end
def address_string
%i[address_line1 address_line2 postcode].map { |field| public_send(field) }.join("\n")
end
def display_attributes
{
name: name,
address: address_string,
telephone_number: phone,
type: org_type,
local_authorities_operated_in: local_authorities,
holds_own_stock: holds_own_stock,
other_stock_owners: other_stock_owners,
managing_agents: managing_agents,
}
end
end end

5
app/models/user.rb

@ -1,7 +1,8 @@
class User < ApplicationRecord class User < ApplicationRecord
# Include default devise modules. Others available are: # Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable # :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :recoverable, :rememberable, :validatable devise :database_authenticatable, :recoverable, :rememberable, :validatable,
:trackable
belongs_to :organisation belongs_to :organisation
has_many :owned_case_logs, through: :organisation has_many :owned_case_logs, through: :organisation

3
app/views/layouts/application.html.erb

@ -39,6 +39,7 @@
if current_user.nil? if current_user.nil?
component.navigation_item(text: 'Case logs', href: '/case_logs') component.navigation_item(text: 'Case logs', href: '/case_logs')
elsif elsif
component.navigation_item(text: 'Your organisation', href: "/organisations/#{current_user.organisation.id}")
component.navigation_item(text: 'Your account', href: '/users/account') component.navigation_item(text: 'Your account', href: '/users/account')
component.navigation_item(text: 'Sign out', href: destroy_user_session_path, options: {:method => :delete}) component.navigation_item(text: 'Sign out', href: destroy_user_session_path, options: {:method => :delete})
end end
@ -65,7 +66,7 @@
end end
%> %>
<% end %> <% end %>
<%= yield %> <%= content_for?(:content) ? yield(:content) : yield %>
</main> </main>
</div> </div>

23
app/views/layouts/organisations.html.erb

@ -0,0 +1,23 @@
<% content_for :before_content do %>
<%= govuk_back_link(
text: 'Back',
href: :back,
) %>
<% end %>
<% content_for :content do %>
<h1 class="govuk-heading-l">
Your organisation
</h1>
<%= render TabNavigationComponent.new(items: [
{ name: t('Details'), url: details_organisation_path(@organisation) },
{ name: t('Users'), url: users_organisation_path(@organisation) },
]) %>
<h2 class="govuk-visually-hidden"><%= content_for(:tab_title) %></h2>
<%= content_for?(:organisations_content) ? yield(:organisations_content) : yield %>
<% end %>
<%= render template: "layouts/application" %>

16
app/views/organisations/show.html.erb

@ -0,0 +1,16 @@
<% content_for :tab_title do %>
<%= "Details" %>
<% end %>
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds-from-desktop">
<%= govuk_summary_list do |summary_list| %>
<% @organisation.display_attributes.each do |attr, val| %>
<%= summary_list.row do |row|
row.key { attr.to_s.humanize }
row.value { simple_format(val.to_s, {}, wrapper_tag: "div") }
end %>
<% end %>
<% end %>
</div>
</div>

23
app/views/organisations/users.html.erb

@ -0,0 +1,23 @@
<% content_for :tab_title do %>
<%= "Users" %>
<% end %>
<%= govuk_button_link_to "Invite user", new_user_path, method: :post %>
<%= govuk_table do |table| %>
<%= table.head do |head| %>
<%= head.row do |row|
row.cell(header: true, text: "Name and email adress")
row.cell(header: true, text: "Organisation and role")
row.cell(header: true, text: "Last logged in")
end %>
<% end %>
<% @organisation.users.each do |user| %>
<%= table.body do |body| %>
<%= body.row do |row|
row.cell(text: simple_format(user_cell(user), {}, wrapper_tag: "div"))
row.cell(text: simple_format(org_cell(user), {}, wrapper_tag: "div"))
row.cell(text: user.last_sign_in_at&.strftime("%d %b %Y") )
end %>
<% end %>
<% end %>
<% end %>

14
config/routes.rb

@ -13,11 +13,23 @@ Rails.application.routes.draw do
root to: "test#index" root to: "test#index"
get "about", to: "about#index" get "about", to: "about#index"
get "/users/account", to: "users/account#index" get "/users/account", to: "users/account#index"
get "/users/account/personal_details", to: "users/account#personal_details"
form_handler = FormHandler.instance form_handler = FormHandler.instance
form = form_handler.get_form("2021_2022") form = form_handler.get_form("2021_2022")
resources :users do
collection do
get "account/personal_details", to: "users/account#personal_details"
end
end
resources :organisations do
member do
get "details", to: "organisations#show"
get "users", to: "organisations#users"
end
end
resources :case_logs do resources :case_logs do
collection do collection do
post "/bulk_upload", to: "bulk_upload#bulk_upload" post "/bulk_upload", to: "bulk_upload#bulk_upload"

12
db/migrate/20211130144840_add_user_last_logged_in.rb

@ -0,0 +1,12 @@
class AddUserLastLoggedIn < ActiveRecord::Migration[6.1]
def change
change_table :users, bulk: true do |t|
## Trackable
t.integer :sign_in_count, default: 0, null: false
t.datetime :current_sign_in_at
t.datetime :last_sign_in_at
t.string :current_sign_in_ip
t.string :last_sign_in_ip
end
end
end

7
db/schema.rb

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2021_11_30_090246) do ActiveRecord::Schema.define(version: 2021_11_30_144840) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -200,6 +200,11 @@ ActiveRecord::Schema.define(version: 2021_11_30_090246) do
t.string "name" t.string "name"
t.string "role" t.string "role"
t.bigint "organisation_id" t.bigint "organisation_id"
t.integer "sign_in_count", default: 0, null: false
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.index ["email"], name: "index_users_on_email", unique: true t.index ["email"], name: "index_users_on_email", unique: true
t.index ["organisation_id"], name: "index_users_on_organisation_id" t.index ["organisation_id"], name: "index_users_on_organisation_id"
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true

11
db/seeds.rb

@ -6,6 +6,15 @@
# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }])
# Character.create(name: 'Luke', movie: movies.first) # Character.create(name: 'Luke', movie: movies.first)
org = Organisation.create!(name: "DLUHC", address_line1: "2 Marsham Street", address_line2: "London", postcode: "SW1P 4DF") org = Organisation.create!(
name: "DLUHC",
address_line1: "2 Marsham Street",
address_line2: "London",
postcode: "SW1P 4DF",
local_authorities: "None",
holds_own_stock: false,
other_stock_owners: "None",
managing_agents: "None",
)
User.create!(email: "test@example.com", password: "password", organisation: org) User.create!(email: "test@example.com", password: "password", organisation: org)
AdminUser.create!(email: "admin@example.com", password: "password") AdminUser.create!(email: "admin@example.com", password: "password")

2
package.json

@ -16,7 +16,7 @@
"@rails/webpacker": "5.4.0", "@rails/webpacker": "5.4.0",
"chart.js": "^3.6.0", "chart.js": "^3.6.0",
"chartkick": "^4.1.0", "chartkick": "^4.1.0",
"govuk-frontend": "^3.13.0", "govuk-frontend": "^3.14.0",
"stimulus": "^3.0.0", "stimulus": "^3.0.0",
"webpack": "^4.46.0", "webpack": "^4.46.0",
"webpack-cli": "^3.3.12" "webpack-cli": "^3.3.12"

27
spec/components/tab_navigation_component_spec.rb

@ -0,0 +1,27 @@
require "rails_helper"
RSpec.describe TabNavigationComponent, type: :component do
let(:items) do
[{ name: "Application", url: "#", current: true },
{ name: "Notes", url: "#" },
{ name: "Timeline", url: "#" }]
end
context "nav tabs appearing as selected" do
it "when the item is 'current' then that tab is selected" do
result = render_inline(described_class.new(items: items))
expect(result.css('.app-tab-navigation__link[aria-current="page"]').text).to include("Application")
end
end
context "rendering tabs" do
it "renders all of the nav tabs specified in the items hash passed to it" do
result = render_inline(described_class.new(items: items))
expect(result.text).to include("Application")
expect(result.text).to include("Notes")
expect(result.text).to include("Timeline")
end
end
end

2
spec/factories/user.rb

@ -1,8 +1,10 @@
FactoryBot.define do FactoryBot.define do
factory :user do factory :user do
sequence(:email) { |i| "test#{i}@example.com" } sequence(:email) { |i| "test#{i}@example.com" }
name { "Danny Rojas" }
password { "pAssword1" } password { "pAssword1" }
organisation organisation
role { "Data Provider" }
created_at { Time.zone.now } created_at { Time.zone.now }
updated_at { Time.zone.now } updated_at { Time.zone.now }
end end

29
spec/features/organisation_spec.rb

@ -0,0 +1,29 @@
require "rails_helper"
require_relative "form/helpers"
RSpec.describe "User Features" do
include Helpers
let!(:user) { FactoryBot.create(:user) }
let(:organisation) { user.organisation }
let(:org_id) { organisation.id }
before do
sign_in user
end
context "Organisation page" do
it "default to organisation details" do
visit("/case_logs")
click_link("Your organisation")
expect(page).to have_content(user.organisation.name)
end
it "can switch tabs" do
visit("/organisations/#{org_id}")
click_link("Users")
expect(page).to have_current_path("/organisations/#{org_id}/users")
click_link("Details")
expect(page).to have_current_path("/organisations/#{org_id}/details")
end
end
end

19
spec/helpers/user_table_helper_spec.rb

@ -0,0 +1,19 @@
require "rails_helper"
RSpec.describe UserTableHelper do
let(:user) { FactoryBot.build(:user) }
describe "#user_cell" do
it "returns user link and email separated by a newline character" do
expected_html = "<a class=\"govuk-link\" href=\"/users\">Danny Rojas</a>\n#{user.email}"
expect(user_cell(user)).to match(expected_html)
end
end
describe "#org_cell" do
it "returns the users org name and role separated by a newline character" do
expected_html = "DLUHC\n<span class='app-!-colour-muted'>Data Provider</span>"
expect(org_cell(user)).to match(expected_html)
end
end
end

3
spec/rails_helper.rb

@ -7,6 +7,7 @@ abort("The Rails environment is running in production mode!") if Rails.env.produ
require "rspec/rails" require "rspec/rails"
require "capybara/rspec" require "capybara/rspec"
require "selenium-webdriver" require "selenium-webdriver"
require "view_component/test_helpers"
Capybara.register_driver :headless do |app| Capybara.register_driver :headless do |app|
options = Selenium::WebDriver::Firefox::Options.new options = Selenium::WebDriver::Firefox::Options.new
@ -80,4 +81,6 @@ RSpec.configure do |config|
config.include Devise::Test::ControllerHelpers, type: :controller config.include Devise::Test::ControllerHelpers, type: :controller
config.include Devise::Test::IntegrationHelpers, type: :request config.include Devise::Test::IntegrationHelpers, type: :request
config.include ViewComponent::TestHelpers, type: :component
config.include Capybara::RSpecMatchers, type: :component
end end

59
spec/requests/organisations_controller_spec.rb

@ -0,0 +1,59 @@
require "rails_helper"
RSpec.describe OrganisationsController, type: :request do
let(:user) { FactoryBot.create(:user) }
let(:organisation) { user.organisation }
let(:headers) { { "Accept" => "text/html" } }
context "details tab" do
before do
sign_in user
get "/organisations/#{organisation.id}", headers: headers, params: {}
end
it "shows the tab navigation" do
expected_html = "<nav class=\"app-tab-navigation\""
expect(response.body).to include(expected_html)
end
it "shows a summary list of org details" do
expected_html = "<dl class=\"govuk-summary-list\""
expect(response.body).to include(expected_html)
expect(response.body).to include(organisation.name)
end
it "has a hidden header title" do
expected_html = "<h2 class=\"govuk-visually-hidden\"> Details"
expect(response.body).to include(expected_html)
end
end
context "users tab" do
before do
sign_in user
get "/organisations/#{organisation.id}/users", headers: headers, params: {}
end
it "shows the tab navigation" do
expected_html = "<nav class=\"app-tab-navigation\""
expect(response.body).to include(expected_html)
end
it "shows a new user button" do
expected_html = "<a class=\"govuk-button\""
expect(response.body).to include(expected_html)
expect(response.body).to include("Invite user")
end
it "shows a table of users" do
expected_html = "<table class=\"govuk-table\""
expect(response.body).to include(expected_html)
expect(response.body).to include(user.email)
end
it "has a hidden header title" do
expected_html = "<h2 class=\"govuk-visually-hidden\"> Users"
expect(response.body).to include(expected_html)
end
end
end

1328
yarn.lock

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save