Add Data (User Details, Status, etc.) to the Member Index View Columns (#17033)

* Adds data to the member column in Member Index View

* Adds roles, statuses, and latest activity to Member Index views

* Adds the list of orgs a user belongs to in Member Index view

* Updates the image URL to link to each users member detail view

* Adds comment_suspended to user current_status and makes roles prettier

* Fixes bullet warning and encapsulates org logic in method

* Properly displays today and yesterday in words

* Refactors #current_organizations to use ternary

* Updates user.rb Member Index View-related methods

* Adjusts the styling of user roles and adds tooltip to roles

* Updates Member Index View to be mobile-friendly

* Updates userIndexView.spec.js

* Updates capitalization within userIndexView.spec.js

* Updates Member Index View per mobile designs and feedback

* Adds member status logic inline with SVGs

* Updates the status column for mobile layout

* Adjust current_role tooltips for Member Index View

* Adds organization name tooltip to member orgas

* Adds organization pics to Member Index View and cleans up helpers

* Addresses PR review feedback (Thanks, Suzanne!)

* Uses CSS in place of SVGs for member statuses

* Adds E2E test for and removes link to org page from Member Index View

* Addresses PR feedback comments by refactoring Member Index View code

* Removes leftover code in user.rb

* Update method that tooltips rely on

* Spells regular correctly :

* Adds a spec that tests Admin::UsersHelper#format_last_activity_timestamp

* Adjusts #cascading_high_level_roles logic and roles

* Adds a spec for #cascading_high_level_roles

* Adds a #last_activity spec to the User model

* Adjusts code a bit per PR feedback. Thanks, Ridhwana! :)

* Updates specs for Member Index View helpers

* Adds Timecop.freeze to help with timestamp failure in user_spec.rb
This commit is contained in:
Julianna Tetreault 2022-04-06 11:48:25 -06:00 committed by GitHub
parent 8c2b1276a4
commit ccf33c50a0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 195 additions and 20 deletions

View file

@ -0,0 +1,25 @@
module Admin
module UsersHelper
def format_last_activity_timestamp(timestamp)
return if timestamp.blank?
if timestamp.today?
"Today, #{timestamp.strftime('%d %b')}"
elsif timestamp.yesterday?
"Yesterday, #{timestamp.strftime('%d %b')}"
else
timestamp.strftime("%d %b, %Y")
end
end
def cascading_high_level_roles(user)
if user.super_admin?
"Super Admin"
elsif user.admin?
"Admin"
elsif user.single_resource_admin_for?(:any)
"Resource Admin"
end
end
end
end

View file

@ -554,6 +554,13 @@ class User < ApplicationRecord
Reaction.for_user(self)
end
def last_activity
return unless registered == true
[registered_at, last_comment_at, last_article_at, latest_article_updated_at, last_reacted_at, profile_updated_at,
last_moderation_notification, last_notification_activity].compact.max
end
protected
# Send emails asynchronously

View file

@ -7,34 +7,59 @@
<!-- Small screen data view start -->
<div class="block m:hidden fs-s">
<h2 class="member-data-heading fs-s py-2 -mx-4 px-6 color-base-60">Members</h2>
<ul class="list-none mx-2">
<% @users.each do |user| %>
<h2 class="member-data-heading fs-s py-2 -mx-3 px-3 color-base-60">Members</h2>
<ul class="list-none">
<% @users.includes([:organization_memberships]).each do |user| %>
<li class="py-4">
<article>
<header class="flex">
<img class="radius-full mr-2" src="<%= user.profile_image_url_for(length: 50) %>" width="40" height="40" alt="" />
<div class="flex flex-col">
<h3 class="fs-base"><%= link_to user.name, admin_user_path(user) %></h3>
<span class="color-base-60">@<%= user.username %></span>
<a href="<%= admin_user_path(user.id) %>">
<img class="radius-full align-middle" src="<%= user.profile_image_url_for(length: 50) %>" width="40" height="40" alt="<%= user.name %>" />
</a>
<div class="pl-3 flex-1 fs-s"><h3 class="fs-base"><%= link_to user.name, admin_user_path(user), class: "color-base-80" %></h3>
<%= link_to "@#{user.username}", admin_user_path(user), class: "color-base-60" %>
</div>
</header>
<div class="flex justify-between py-2">
<div>standing info</div>
<div>role info</div>
<div>
<% if user.suspended? %>
<span class="c-indicator mr-2 inline-block" style="--bg: #DC2626"></span>Suspended
<% elsif user.warned? %>
<span class="c-indicator mr-2 inline-block" style="--bg: #F59E0B"></span>Warned
<% elsif user.comment_suspended? %>
<span class="c-indicator mr-2 inline-block" style="--bg: #DC2626"></span>Comment suspended
<% elsif user.trusted? %>
<span class="c-indicator mr-2 inline-block" style="--bg: #059669"></span>Trusted
<% else %>
<span class="c-indicator mr-2 inline-block" style="--bg: #A3A3A3"></span>Good standing
<% end %>
</div>
<div>
<span class="c-indicator ml-2 crayons-hover-tooltip" data-tooltip="<%= cascading_high_level_roles(user) %>">
<%= cascading_high_level_roles(user) %>
</span>
</div>
</div>
<div class="flex justify-between items-end">
<dl class="flex gap-2">
<div>
<dt class="color-base-60 fw-normal">Last activity</dt>
<dd>placeholder</dd>
<dd><%= format_last_activity_timestamp(user.last_activity) %></dd>
</div>
<div>
<dt class="color-base-60 fw-normal">Joined on</dt>
<dd><%= user.registered_at.strftime("%d %b, %Y") %></dd>
</div>
</dl>
<div>Orgs info</div>
<div>
<% if user.organization_memberships.any? %>
<% user.organization_memberships.includes([:organization]).each do |org_membership| %>
<span class="crayons-hover-tooltip" data-tooltip="<%= org_membership.organization.name %>">
<img src="<%= org_membership.organization.profile_image_url_for(length: 64) %>" width="32" height="32" alt="<%= org_membership.organization.name %>" class="c-link crayons-logo crayons-logo--l">
</span>
<% end %>
<% end %>
</div>
</div>
</article>
</li>
@ -54,9 +79,51 @@
</tr>
</thead>
<tbody class="crayons-card">
<% @users.each do |user| %>
<% @users.includes([:organization_memberships]).each do |user| %>
<tr>
<td><%= link_to user.name, admin_user_path(user) %></td>
<td>
<div class="flex s:items-start flex-col s:flex-row">
<a href="<%= admin_user_path(user.id) %>">
<img class="radius-full align-middle" src="<%= user.profile_image_url_for(length: 50) %>" width="40" height="40" alt="<%= user.name %>" />
</a>
<div class="pl-3 flex-1 fs-s"><h3 class="fs-base"><%= link_to user.name, admin_user_path(user), class: "color-base-80" %></h3>
<%= link_to "@#{user.username}", admin_user_path(user), class: "color-base-60" %>&nbsp;
<span class="c-indicator ml-2 crayons-hover-tooltip" data-tooltip="<%= cascading_high_level_roles(user) %>">
<%= cascading_high_level_roles(user) %>
</span>
</div>
</div>
</td>
<td>
<% if user.suspended? %>
<span class="c-indicator mr-2 inline-block" style="--bg: #DC2626"></span>Suspended
<% elsif user.warned? %>
<span class="c-indicator mr-2 inline-block" style="--bg: #F59E0B"></span>Warned
<% elsif user.comment_suspended? %>
<span class="c-indicator mr-2 inline-block" style="--bg: #DC2626"></span>Comment suspended
<% elsif user.trusted? %>
<span class="c-indicator mr-2 inline-block" style="--bg: #059669"></span>Trusted
<% else %>
<span class="c-indicator mr-2 inline-block" style="--bg: #A3A3A3"></span>Good standing
<% end %>
</td>
<td>
<%= format_last_activity_timestamp(user.last_activity) %>
<p class="fs-xs color-base-60">
<%= user.registered_at.strftime("%d %b, %Y") %>
</p>
</td>
<td>
<% if user.organization_memberships.any? %>
<% user.organization_memberships.includes([:organization]).each do |org_membership| %>
<span class="crayons-hover-tooltip" data-tooltip="<%= org_membership.organization.name %>">
<img src="<%= org_membership.organization.profile_image_url_for(length: 64) %>" width="32" height="32" alt="<%= org_membership.organization.name %>" class="c-link crayons-logo crayons-logo--l">
</span>
<% end %>
<% else %>
-
<% end %>
</td>
</tr>
<% end %>
</tbody>

View file

@ -28,7 +28,7 @@ describe('User index view', () => {
cy.findByRole('button', { name: 'Search' }).click();
// Coreect search result should appear
// Correct search result should appear
cy.findByRole('heading', { name: 'Admin McAdmin' }).should('exist');
});
@ -41,11 +41,9 @@ describe('User index view', () => {
cy.findByRole('combobox', { name: 'User role' }).select('super_admin');
cy.findByRole('button', { name: 'Filter' }).click();
// Search results should include these two results
// Filter results should include these two results
cy.findByRole('heading', { name: 'Admin McAdmin' }).should('exist');
cy.findByRole('heading', { name: 'Apple Auth Admin User' }).should(
'exist',
);
cy.findAllByText('Apple Auth Admin User').should('exist');
});
it('Prevents both search and filter widgets being visible at the same time', () => {
@ -65,15 +63,20 @@ describe('User index view', () => {
.should('have.attr', 'aria-expanded', 'true');
cy.findByRole('button', { name: 'Search' }).should('exist');
// verify the filter options have now closed
// Verify the filter options have now closed
cy.get('@filterButton').should('have.attr', 'aria-expanded', 'false');
cy.findByRole('combobox').should('not.exist');
// now re-click filter options and check search options have closed
// Now re-click filter options and check search options have closed
cy.get('@filterButton').click();
cy.get('@searchButton').should('have.attr', 'aria-expanded', 'false');
cy.findByRole('button', { name: 'Search' }).should('not.exist');
});
it(`Clicks through to the Member Detail View`, () => {
cy.findAllByRole('link', { name: 'Admin McAdmin' }).first().click();
cy.url().should('contain', '/admin/users/1');
});
});
});
@ -97,6 +100,9 @@ describe('User index view', () => {
cy.findByRole('button', { name: 'Search' }).click();
// Correct search result should appear
cy.findByRole('heading', { name: 'Admin McAdmin' }).should('exist');
// The table headers consistitute a row, plus one result
cy.findAllByRole('row').should('have.length', 2);
});
@ -105,9 +111,18 @@ describe('User index view', () => {
cy.findByRole('combobox', { name: 'User role' }).select('super_admin');
cy.findByRole('button', { name: 'Filter' }).click();
// Filter results should include these two results
cy.findByRole('heading', { name: 'Admin McAdmin' }).should('exist');
cy.findAllByText('Apple Auth Admin User').should('exist');
// Table header, 'normal' admin and apple auth admin
cy.findAllByRole('row').should('have.length', 3);
});
it(`Clicks through to the Member Detail View`, () => {
cy.findAllByRole('link', { name: 'Admin McAdmin' }).first().click();
cy.url().should('contain', '/admin/users/1');
});
});
});
});

View file

@ -0,0 +1,52 @@
require "rails_helper"
describe Admin::UsersHelper do
describe "#format_last_activity_timestamp" do
it "renders the proper 'Last activity' date for a user that was active today" do
timestamp = Time.zone.today
date = timestamp.strftime("%d %b")
formatted_date = helper.format_last_activity_timestamp(timestamp)
expect(formatted_date).to eq "Today, #{date}"
end
it "renders the proper 'Last activity' date for a user that was active yesterday" do
timestamp = Date.yesterday
date = timestamp.strftime("%d %b")
formatted_date = helper.format_last_activity_timestamp(timestamp)
expect(formatted_date).to eq "Yesterday, #{date}"
end
it "renders the proper 'Last activity' date for a user that was active recently" do
timestamp = 11.days.ago
date = timestamp.strftime("%d %b, %Y")
formatted_date = helper.format_last_activity_timestamp(timestamp)
expect(formatted_date).to eq date.to_s
end
end
describe "#cascading_high_level_roles" do
it "renders the proper role for a Super Admin" do
super_admin = create(:user, :super_admin)
role = helper.cascading_high_level_roles(super_admin)
expect(role).to eq "Super Admin"
end
it "renders the proper role for an Admin" do
admin = create(:user, :admin)
role = helper.cascading_high_level_roles(admin)
expect(role).to eq "Admin"
end
it "renders the proper role for a Resource Admin" do
resource_admin = create(:user, :single_resource_admin)
role = helper.cascading_high_level_roles(resource_admin)
expect(role).to eq "Resource Admin"
end
it "renders the proper role for a user that isn't a Super Admin, Admin, or Resource Admin" do
user = create(:user)
role = helper.cascading_high_level_roles(user)
expect(role).to be_nil
end
end
end

View file

@ -798,4 +798,13 @@ RSpec.describe User, type: :model do
expect(user.profile).to respond_to(:location)
end
end
describe "#last_activity" do
it "determines a user's last activity" do
Timecop.freeze do
user = create(:user, last_comment_at: 1.minute.ago)
expect(user.last_activity).to eq(Time.zone.now)
end
end
end
end