UX: admins users page follows admin ux guideline (#29873)

Conversion of `/admin/users` page to follow admin UX guidelines.

In addition, add the username to the title on the user admin page.
This commit is contained in:
Krzysztof Kotlarek 2024-12-02 10:11:23 +11:00 committed by GitHub
parent 976be2abcd
commit 6d4c6ee154
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 225 additions and 97 deletions

View File

@ -6,6 +6,10 @@ export default class AdminUserIndexRoute extends DiscourseRoute {
return this.modelFor("adminUser"); return this.modelFor("adminUser");
} }
titleToken() {
return this.currentModel.username;
}
afterModel(model) { afterModel(model) {
if (this.currentUser.admin) { if (this.currentUser.admin) {
return Group.findAll().then((groups) => { return Group.findAll().then((groups) => {

View File

@ -1,26 +1,27 @@
<PluginOutlet @name="admin-users-list-show-before" /> <AdminPageSubheader @titleLabelTranslated={{this.title}}>
<:actions as |actions|>
<div class="admin-title">
<h2>{{this.title}}</h2>
{{#if this.canCheckEmails}} {{#if this.canCheckEmails}}
{{#if this.showEmails}} {{#if this.showEmails}}
<DButton <actions.Default
@action={{this.toggleEmailVisibility}} @action={{this.toggleEmailVisibility}}
@label="admin.users.hide_emails" @label="admin.users.hide_emails"
class="hide-emails btn-default" class="admin-users__subheader-hide-emails"
/> />
{{else}} {{else}}
<DButton <actions.Default
@action={{this.toggleEmailVisibility}} @action={{this.toggleEmailVisibility}}
@label="admin.users.show_emails" @label="admin.users.show_emails"
class="show-emails btn-default" class="admin-users__subheader-show-emails"
/> />
{{/if}} {{/if}}
{{/if}} {{/if}}
</div> </:actions>
</AdminPageSubheader>
<PluginOutlet @name="admin-users-list-show-before" />
<div class="admin-users-list__controls"> <div class="admin-users-list__controls">
<div class="username"> <div class="admin-users-list__search">
<input <input
type="text" type="text"
dir="auto" dir="auto"
@ -64,7 +65,7 @@
> >
{{#if this.users}} {{#if this.users}}
<ResponsiveTable <ResponsiveTable
@className="users-list" @className={{concat-class "users-list" this.query}}
@aria-label={{this.title}} @aria-label={{this.title}}
@style={{html-safe @style={{html-safe
(concat (concat

View File

@ -1,70 +1,82 @@
<div class="admin-controls"> <div class="admin-users admin-config-page">
<nav> <AdminPageHeader
<ul class="nav nav-pills"> @titleLabel="admin.users.title"
@descriptionLabel="admin.users.description"
@learnMoreUrl="https://meta.discourse.org/t/accessing-a-user-s-admin-page/311859"
>
<:breadcrumbs>
<DBreadcrumbsItem
@path="/admin/users/list"
@label={{i18n "admin.permalink.title"}}
/>
</:breadcrumbs>
<:actions as |actions|>
{{#if this.currentUser.can_invite_to_forum}}
<actions.Primary
@action={{route-action "sendInvites"}}
@title="admin.invite.button_title"
@label="admin.invite.button_text"
class="admin-users__header-send-invites"
/>
{{/if}}
{{#if this.currentUser.admin}}
<actions.Primary
@action={{route-action "exportUsers"}}
@title="admin.export_csv.button_title.user"
@label="admin.export_csv.button_text"
class="admin-users__header-export-users"
/>
{{/if}}
</:actions>
<:tabs>
<NavItem <NavItem
@route="adminUsersList.show" @route="adminUsersList.show"
@routeParam="active" @routeParam="active"
@label="admin.users.nav.active" @label="admin.users.nav.active"
class="active-users" class="admin-users-tabs__active"
/> />
<NavItem <NavItem
@route="adminUsersList.show" @route="adminUsersList.show"
@routeParam="new" @routeParam="new"
@label="admin.users.nav.new" @label="admin.users.nav.new"
class="new-users" class="admin-users-tabs__new"
/> />
<NavItem <NavItem
@route="adminUsersList.show" @route="adminUsersList.show"
@routeParam="staff" @routeParam="staff"
@label="admin.users.nav.staff" @label="admin.users.nav.staff"
class="staff-users" class="admin-users-tabs__staff"
/> />
<NavItem <NavItem
@route="adminUsersList.show" @route="adminUsersList.show"
@routeParam="suspended" @routeParam="suspended"
@label="admin.users.nav.suspended" @label="admin.users.nav.suspended"
class="suspended-users" class="admin-users-tabs__suspended"
/> />
<NavItem <NavItem
@route="adminUsersList.show" @route="adminUsersList.show"
@routeParam="silenced" @routeParam="silenced"
@label="admin.users.nav.silenced" @label="admin.users.nav.silenced"
class="silenced-users" class="admin-users-tabs__silenced"
/> />
<NavItem <NavItem
@route="adminUsersList.show" @route="adminUsersList.show"
@routeParam="staged" @routeParam="staged"
@label="admin.users.nav.staged" @label="admin.users.nav.staged"
class="staged-users" class="admin-users-tabs__staged"
/> />
<NavItem @route="groups" @label="groups.index.title" class="groups" /> <NavItem
<PluginOutlet @name="admin-users-list-nav-after" /> @route="groups"
@label="groups.index.title"
<li class="admin-actions"> class="admin-users-tabs__groups"
{{#if this.currentUser.can_invite_to_forum}}
<DButton
@action={{route-action "sendInvites"}}
@title="admin.invite.button_title"
@icon="user-plus"
@label="admin.invite.button_text"
class="btn-flat"
/> />
{{/if}} </:tabs>
</AdminPageHeader>
{{#if this.currentUser.admin}} <div class="admin-container admin-config-page__main-area">
<DButton </div>
@action={{route-action "exportUsers"}}
@title="admin.export_csv.button_title.user"
@icon="download"
@label="admin.export_csv.button_text"
class="btn-flat"
/>
{{/if}}
</li>
</ul>
</nav>
</div> </div>
<div class="admin-container"> <div class="admin-container admin-config-page__main-area">
{{outlet}} {{outlet}}
</div> </div>

View File

@ -18,7 +18,7 @@ acceptance("Admin - Users List", function (needs) {
test("searching users with no matches", async function (assert) { test("searching users with no matches", async function (assert) {
await visit("/admin/users/list/active"); await visit("/admin/users/list/active");
await fillIn(".admin-users-list__controls .username input", "doesntexist"); await fillIn(".admin-users-list__search input", "doesntexist");
assert.dom(".users-list-container").hasText(i18n("search.no_results")); assert.dom(".users-list-container").hasText(i18n("search.no_results"));
}); });
@ -50,13 +50,13 @@ acceptance("Admin - Users List", function (needs) {
assert.dom(".users-list .user").exists(); assert.dom(".users-list .user").exists();
await click(".show-emails"); await click(".admin-users__subheader-show-emails");
assert assert
.dom(".users-list .user:nth-child(1) .email") .dom(".users-list .user:nth-child(1) .email")
.hasText("<small>eviltrout@example.com</small>", "shows the emails"); .hasText("<small>eviltrout@example.com</small>", "shows the emails");
await click(".hide-emails"); await click(".admin-users__subheader-hide-emails");
assert assert
.dom(".users-list .user:nth-child(1) .email .directory-table__value") .dom(".users-list .user:nth-child(1) .email .directory-table__value")
@ -71,28 +71,28 @@ acceptance("Admin - Users List", function (needs) {
await visit("/admin/users/list/active"); await visit("/admin/users/list/active");
assert.dom(".admin-title h2").hasText(activeTitle); assert.dom(".admin-page-subheader__title").hasText(activeTitle);
assert assert
.dom(".users-list .user:nth-child(1) .username") .dom(".users-list .user:nth-child(1) .username")
.includesText(activeUser); .includesText(activeUser);
await click('a[href="/admin/users/list/new"]'); await click('a[href="/admin/users/list/new"]');
assert.dom(".admin-title h2").hasText(suspectTitle); assert.dom(".admin-page-subheader__title").hasText(suspectTitle);
assert assert
.dom(".users-list .user:nth-child(1) .username") .dom(".users-list .user:nth-child(1) .username")
.includesText(suspectUser); .includesText(suspectUser);
await click(".users-list .sortable:nth-child(4)"); await click(".users-list .sortable:nth-child(4)");
assert.dom(".admin-title h2").hasText(suspectTitle); assert.dom(".admin-page-subheader__title").hasText(suspectTitle);
assert assert
.dom(".users-list .user:nth-child(1) .username") .dom(".users-list .user:nth-child(1) .username")
.includesText(suspectUser); .includesText(suspectUser);
await click('a[href="/admin/users/list/active"]'); await click('a[href="/admin/users/list/active"]');
assert.dom(".admin-title h2").hasText(activeTitle); assert.dom(".admin-page-subheader__title").hasText(activeTitle);
assert assert
.dom(".users-list .user:nth-child(1) .username") .dom(".users-list .user:nth-child(1) .username")
.includesText(activeUser); .includesText(activeUser);

View File

@ -423,6 +423,20 @@ $mobile-breakpoint: 700px;
color: var(--primary-medium); color: var(--primary-medium);
} }
} }
.admin-users-list {
&__search {
@media screen and (max-width: 500px) {
width: 100%;
}
input {
min-width: 15em;
@media screen and (max-width: 500px) {
box-sizing: border-box;
width: 100%;
}
}
}
}
.ip-lookup { .ip-lookup {
position: relative; position: relative;

View File

@ -6642,6 +6642,7 @@ en:
users: users:
title: "Users" title: "Users"
description: "View and manage users."
create: "Add Admin User" create: "Add Admin User"
last_emailed: "Last Emailed" last_emailed: "Last Emailed"
not_found: "Sorry, that username doesn't exist in our system." not_found: "Sorry, that username doesn't exist in our system."

View File

@ -44,6 +44,11 @@ describe "Admin User Page", type: :system do
expect(admin_user_page).to have_silence_button expect(admin_user_page).to have_silence_button
end end
it "displays username in the title" do
expect(page).to have_css(".display-row.username")
expect(page.title).to eq("#{user.username} - Admin - Discourse")
end
describe "the suspend user modal" do describe "the suspend user modal" do
it "displays the list of users who share the same IP but are not mods or admins" do it "displays the list of users who share the same IP but are not mods or admins" do
admin_user_page.click_suspend_button admin_user_page.click_suspend_button

View File

@ -3,7 +3,9 @@
describe "Admin Users Page", type: :system do describe "Admin Users Page", type: :system do
fab!(:current_user) { Fabricate(:admin) } fab!(:current_user) { Fabricate(:admin) }
fab!(:another_admin) { Fabricate(:admin) } fab!(:another_admin) { Fabricate(:admin) }
fab!(:users) { Fabricate.times(3, :user) } fab!(:user_1) { Fabricate(:user) }
fab!(:user_2) { Fabricate(:user) }
fab!(:user_3) { Fabricate(:user) }
let(:admin_users_page) { PageObjects::Pages::AdminUsers.new } let(:admin_users_page) { PageObjects::Pages::AdminUsers.new }
@ -19,7 +21,7 @@ describe "Admin Users Page", type: :system do
expect(admin_users_page.user_row(current_user.id).bulk_select_checkbox.disabled?).to eq(true) expect(admin_users_page.user_row(current_user.id).bulk_select_checkbox.disabled?).to eq(true)
expect(admin_users_page.user_row(another_admin.id).bulk_select_checkbox.disabled?).to eq(true) expect(admin_users_page.user_row(another_admin.id).bulk_select_checkbox.disabled?).to eq(true)
expect(admin_users_page.user_row(users[0].id).bulk_select_checkbox.disabled?).to eq(false) expect(admin_users_page.user_row(user_1.id).bulk_select_checkbox.disabled?).to eq(false)
admin_users_page.user_row(another_admin.id).bulk_select_checkbox.hover admin_users_page.user_row(another_admin.id).bulk_select_checkbox.hover
expect(PageObjects::Components::Tooltips.new("bulk-delete-unavailable-reason")).to be_present( expect(PageObjects::Components::Tooltips.new("bulk-delete-unavailable-reason")).to be_present(
@ -30,25 +32,25 @@ describe "Admin Users Page", type: :system do
it "has a button that toggles the bulk select checkboxes" do it "has a button that toggles the bulk select checkboxes" do
admin_users_page.visit admin_users_page.visit
expect(admin_users_page).to have_users(users.map(&:id)) expect(admin_users_page).to have_users([user_1.id, user_2.id, user_3.id])
expect(admin_users_page.user_row(users[0].id)).to have_no_bulk_select_checkbox expect(admin_users_page.user_row(user_1.id)).to have_no_bulk_select_checkbox
expect(admin_users_page.user_row(users[1].id)).to have_no_bulk_select_checkbox expect(admin_users_page.user_row(user_2.id)).to have_no_bulk_select_checkbox
expect(admin_users_page.user_row(users[2].id)).to have_no_bulk_select_checkbox expect(admin_users_page.user_row(user_3.id)).to have_no_bulk_select_checkbox
admin_users_page.bulk_select_button.click admin_users_page.bulk_select_button.click
expect(admin_users_page.user_row(users[0].id)).to have_bulk_select_checkbox expect(admin_users_page.user_row(user_1.id)).to have_bulk_select_checkbox
expect(admin_users_page.user_row(users[1].id)).to have_bulk_select_checkbox expect(admin_users_page.user_row(user_2.id)).to have_bulk_select_checkbox
expect(admin_users_page.user_row(users[2].id)).to have_bulk_select_checkbox expect(admin_users_page.user_row(user_3.id)).to have_bulk_select_checkbox
expect(admin_users_page).to have_no_bulk_actions_dropdown expect(admin_users_page).to have_no_bulk_actions_dropdown
admin_users_page.user_row(users[0].id).bulk_select_checkbox.click admin_users_page.user_row(user_1.id).bulk_select_checkbox.click
expect(admin_users_page).to have_bulk_actions_dropdown expect(admin_users_page).to have_bulk_actions_dropdown
admin_users_page.user_row(users[1].id).bulk_select_checkbox.click admin_users_page.user_row(user_2.id).bulk_select_checkbox.click
admin_users_page.bulk_actions_dropdown.expand admin_users_page.bulk_actions_dropdown.expand
admin_users_page.bulk_actions_dropdown.option(".bulk-delete").click admin_users_page.bulk_actions_dropdown.option(".bulk-delete").click
@ -64,38 +66,37 @@ describe "Admin Users Page", type: :system do
confirmation_modal.confirm_button.click confirmation_modal.confirm_button.click
expect(confirmation_modal).to have_successful_log_entry_for_user( expect(confirmation_modal).to have_successful_log_entry_for_user(
user: users[0], user: user_1,
position: 1, position: 1,
total: 2, total: 2,
) )
expect(confirmation_modal).to have_successful_log_entry_for_user( expect(confirmation_modal).to have_successful_log_entry_for_user(
user: users[1], user: user_2,
position: 2, position: 2,
total: 2, total: 2,
) )
expect(confirmation_modal).to have_no_error_log_entries expect(confirmation_modal).to have_no_error_log_entries
confirmation_modal.close confirmation_modal.close
deleted_ids = users[0..1].map(&:id) expect(admin_users_page).to have_no_users([user_1.id, user_2.id])
expect(admin_users_page).to have_no_users(deleted_ids) expect(User.where(id: [user_1.id, user_2.id]).count).to eq(0)
expect(User.where(id: deleted_ids).count).to eq(0)
end end
it "remembers selected users when the user list refreshes due to search" do it "remembers selected users when the user list refreshes due to search" do
admin_users_page.visit admin_users_page.visit
admin_users_page.bulk_select_button.click admin_users_page.bulk_select_button.click
admin_users_page.search_input.fill_in(with: users[0].username) admin_users_page.search_input.fill_in(with: user_1.username)
admin_users_page.user_row(users[0].id).bulk_select_checkbox.click admin_users_page.user_row(user_1.id).bulk_select_checkbox.click
admin_users_page.search_input.fill_in(with: users[1].username) admin_users_page.search_input.fill_in(with: user_2.username)
admin_users_page.user_row(users[1].id).bulk_select_checkbox.click admin_users_page.user_row(user_2.id).bulk_select_checkbox.click
admin_users_page.search_input.fill_in(with: "") admin_users_page.search_input.fill_in(with: "")
expect(admin_users_page).to have_users(users.map(&:id)) expect(admin_users_page).to have_users([user_1.id, user_2.id, user_3.id])
expect(admin_users_page.user_row(users[0].id).bulk_select_checkbox).to be_checked expect(admin_users_page.user_row(user_1.id).bulk_select_checkbox).to be_checked
expect(admin_users_page.user_row(users[1].id).bulk_select_checkbox).to be_checked expect(admin_users_page.user_row(user_2.id).bulk_select_checkbox).to be_checked
expect(admin_users_page.user_row(users[2].id).bulk_select_checkbox).not_to be_checked expect(admin_users_page.user_row(user_3.id).bulk_select_checkbox).not_to be_checked
admin_users_page.bulk_actions_dropdown.expand admin_users_page.bulk_actions_dropdown.expand
admin_users_page.bulk_actions_dropdown.option(".bulk-delete").click admin_users_page.bulk_actions_dropdown.option(".bulk-delete").click
@ -104,39 +105,91 @@ describe "Admin Users Page", type: :system do
confirmation_modal.fill_in_confirmation_phase(user_count: 2) confirmation_modal.fill_in_confirmation_phase(user_count: 2)
confirmation_modal.confirm_button.click confirmation_modal.confirm_button.click
expect(confirmation_modal).to have_successful_log_entry_for_user( expect(confirmation_modal).to have_successful_log_entry_for_user(
user: users[0], user: user_1,
position: 1, position: 1,
total: 2, total: 2,
) )
expect(confirmation_modal).to have_successful_log_entry_for_user( expect(confirmation_modal).to have_successful_log_entry_for_user(
user: users[1], user: user_2,
position: 2, position: 2,
total: 2, total: 2,
) )
confirmation_modal.close confirmation_modal.close
deleted_ids = users[0..1].map(&:id) expect(admin_users_page).to have_no_users([user_1.id, user_2.id])
expect(admin_users_page).to have_no_users(deleted_ids) expect(User.where(id: [user_1.id, user_2.id]).count).to eq(0)
expect(User.where(id: deleted_ids).count).to eq(0)
end end
it "displays an error message if bulk delete fails" do it "displays an error message if bulk delete fails" do
admin_users_page.visit admin_users_page.visit
admin_users_page.bulk_select_button.click admin_users_page.bulk_select_button.click
admin_users_page.user_row(users[0].id).bulk_select_checkbox.click admin_users_page.user_row(user_1.id).bulk_select_checkbox.click
admin_users_page.bulk_actions_dropdown.expand admin_users_page.bulk_actions_dropdown.expand
admin_users_page.bulk_actions_dropdown.option(".bulk-delete").click admin_users_page.bulk_actions_dropdown.option(".bulk-delete").click
confirmation_modal.fill_in_confirmation_phase(user_count: 1) confirmation_modal.fill_in_confirmation_phase(user_count: 1)
users[0].update!(admin: true) user_1.update!(admin: true)
confirmation_modal.confirm_button.click confirmation_modal.confirm_button.click
expect(confirmation_modal).to have_error_log_entry( expect(confirmation_modal).to have_error_log_entry(
I18n.t("js.generic_error_with_reason", error: I18n.t("user.cannot_bulk_delete")), I18n.t("js.generic_error_with_reason", error: I18n.t("user.cannot_bulk_delete")),
) )
confirmation_modal.close confirmation_modal.close
expect(admin_users_page).to have_users([users[0].id]) expect(admin_users_page).to have_users([user_1.id])
end
end
context "when visiting an admin's page" do
it "shows list of active users" do
admin_users_page.visit
expect(admin_users_page).to have_active_tab("active")
expect(page).to have_css(".directory-table__cell.username")
expect(admin_users_page).to have_users(
[current_user.id, another_admin.id, user_1.id, user_2.id, user_3.id],
)
end
it "shows list of suspended users" do
admin_users_page.visit
admin_users_page.click_tab("suspended")
expect(admin_users_page).to have_active_tab("suspended")
expect(admin_users_page).to have_none_users
end
it "shows list of silenced users" do
admin_users_page.visit
user_1.update!(silenced_till: 1.day.from_now)
admin_users_page.click_tab("silenced")
expect(admin_users_page).to have_active_tab("silenced")
expect(page).to have_css(".users-list.silenced")
expect(admin_users_page).to have_users([user_1.id])
end
it "shows emails" do
admin_users_page.visit
expect(admin_users_page).to have_no_emails
admin_users_page.click_show_emails
expect(admin_users_page).to have_emails
end
it "redirects to groups page" do
admin_users_page.visit
admin_users_page.click_tab("groups")
expect(page).to have_current_path("/g")
end
it "redirect to invites page" do
admin_users_page.visit
admin_users_page.click_send_invites
expect(page).to have_current_path("/u/#{current_user.username}/invited/pending")
end
it "allows to export users" do
admin_users_page.visit
admin_users_page.click_export
expect(page).to have_css(".dialog-body")
expect(page).to have_content(I18n.t("admin_js.admin.export_csv.success"))
end end
end end
end end

View File

@ -32,7 +32,7 @@ module PageObjects
end end
def search_input def search_input
find(".admin-users-list__controls .username input") find(".admin-users-list__search input")
end end
def user_row(id) def user_row(id)
@ -62,6 +62,44 @@ module PageObjects
def has_no_bulk_actions_dropdown? def has_no_bulk_actions_dropdown?
has_no_css?(".bulk-select-admin-users-dropdown-trigger") has_no_css?(".bulk-select-admin-users-dropdown-trigger")
end end
def has_usernames?(usernames)
expect(all(".directory-table__cell.username").map(&:text)).to eq(usernames)
end
def has_none_users?
has_content?(I18n.t("js.search.no_results"))
end
def click_tab(tab)
has_css?(".admin-users-tabs__#{tab}")
find(".admin-users-tabs__#{tab}").click
end
def has_active_tab?(tab)
has_css?(".admin-users-tabs__#{tab} .active")
has_no_css?(".loading-container .visible")
end
def has_no_emails?
has_no_css?(".directory-table__column-header--email")
end
def has_emails?
has_css?(".directory-table__column-header--email")
end
def click_show_emails
find(".admin-users__subheader-show-emails").click
end
def click_send_invites
find(".admin-users__header-send-invites").click
end
def click_export
find(".admin-users__header-export-users").click
end
end end
end end
end end