Browse Source
* 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 commentspull/124/head
Daniel Baark
3 years ago
committed by
GitHub
26 changed files with 1078 additions and 661 deletions
@ -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> |
@ -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 |
@ -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 |
@ -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 |
@ -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; |
||||||
|
} |
||||||
|
} |
@ -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" %> |
@ -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> |
@ -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 %> |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
Loading…
Reference in new issue