FEATURE: the ability to expand/collapse all admin sections (#26358)

By default, admin sections should be collapsed.
In addition, a button to expand/collapse all sections has been added.
This commit is contained in:
Krzysztof Kotlarek 2024-03-27 14:42:06 +11:00 committed by GitHub
parent 8e08a3b31f
commit 0932b146d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 235 additions and 66 deletions

View File

@ -0,0 +1,28 @@
import Component from "@glimmer/component";
import { service } from "@ember/service";
import { ADMIN_PANEL } from "discourse/lib/sidebar/panels";
import BackToForum from "./back-to-forum";
import Filter from "./filter";
import ToggleAllSections from "./toggle-all-sections";
export default class AdminHeader extends Component {
@service sidebarState;
get shouldDisplay() {
return this.sidebarState.isCurrentPanel(ADMIN_PANEL);
}
<template>
{{#if this.shouldDisplay}}
<div class="sidebar-admin-header">
<div class="sidebar-admin-header__row">
<BackToForum />
<ToggleAllSections @sections={{@sections}} />
</div>
<div class="sidebar-admin-header__row">
<Filter />
</div>
</div>
{{/if}}
</template>
}

View File

@ -9,6 +9,7 @@
@collapsable={{@collapsable}}
@displaySection={{this.section.displaySection}}
@hideSectionHeader={{this.section.hideSectionHeader}}
@collapsedByDefault={{this.section.collapsedByDefault}}
>
{{#each this.filteredLinks key="name" as |link|}}
<Sidebar::SectionLink

View File

@ -1,5 +1,4 @@
<Sidebar::BackToForum />
<Sidebar::Filter />
<Sidebar::AdminHeader />
{{#each this.sections as |sectionConfig|}}
<Sidebar::ApiSection
@sectionConfig={{sectionConfig}}

View File

@ -10,7 +10,7 @@ export default class BackToForum extends Component {
@service sidebarState;
get shouldDisplay() {
return this.sidebarState.currentPanel.key === ADMIN_PANEL;
return this.sidebarState.isCurrentPanel(ADMIN_PANEL);
}
get homepage() {

View File

@ -3,6 +3,7 @@
data-section-name={{@sectionName}}
class="sidebar-section-wrapper sidebar-section"
...attributes
{{did-insert this.setExpandedState}}
>
{{#unless @hideSectionHeader}}
<div class="sidebar-section-header-wrapper sidebar-row">

View File

@ -2,26 +2,41 @@ import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import { service } from "@ember/service";
import { bind } from "discourse-common/utils/decorators";
export default class SidebarSection extends Component {
@service keyValueStore;
@service sidebarState;
@tracked displaySectionContent;
@tracked collapsedSections = this.sidebarState.collapsedSections;
sidebarSectionContentID = `sidebar-section-content-${this.args.sectionName}`;
collapsedSidebarSectionKey = `sidebar-section-${this.args.sectionName}-collapsed`;
constructor() {
super(...arguments);
if (this.args.collapsable) {
this.displaySectionContent =
this.keyValueStore.getItem(this.collapsedSidebarSectionKey) ===
undefined
? true
: false;
} else {
this.displaySectionContent = true;
get isCollapsed() {
if (!this.args.collapsable) {
return false;
}
if (
this.keyValueStore.getItem(this.collapsedSidebarSectionKey) === undefined
) {
return this.args.collapsedByDefault;
}
return (
this.keyValueStore.getItem(this.collapsedSidebarSectionKey) === "true"
);
}
@bind
setExpandedState() {
if (this.isCollapsed) {
this.sidebarState.collapseSection(this.args.sectionName);
} else {
this.sidebarState.expandSection(this.args.sectionName);
}
}
get displaySectionContent() {
return !this.collapsedSections.includes(this.collapsedSidebarSectionKey);
}
willDestroy() {
@ -31,12 +46,10 @@ export default class SidebarSection extends Component {
@action
toggleSectionDisplay() {
this.displaySectionContent = !this.displaySectionContent;
if (this.displaySectionContent) {
this.keyValueStore.remove(this.collapsedSidebarSectionKey);
this.sidebarState.collapseSection(this.args.sectionName);
} else {
this.keyValueStore.setItem(this.collapsedSidebarSectionKey, true);
this.sidebarState.expandSection(this.args.sectionName);
}
// remove focus from the toggle, but only on click

View File

@ -0,0 +1,53 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import { service } from "@ember/service";
import DButton from "discourse/components/d-button";
import { ADMIN_NAV_MAP } from "discourse/lib/sidebar/admin-nav-map";
export default class ToggleAllSections extends Component {
@service sidebarState;
@service keyValueStore;
@tracked collapsedSections = this.sidebarState.collapsedSections;
get allSectionsExpanded() {
return ADMIN_NAV_MAP.every((adminNav) => {
return !this.collapsedSections.includes(
`sidebar-section-${this.sidebarState.currentPanel.key}-${adminNav.name}-collapsed`
);
});
}
get title() {
return this.allSectionsExpanded
? "admin.collapse_all_sections"
: "admin.expand_all_sections";
}
get icon() {
return this.allSectionsExpanded
? "discourse-chevron-collapse"
: "discourse-chevron-expand";
}
@action
toggleAllSections() {
const collapseOrExpand = this.allSectionsExpanded
? this.sidebarState.collapseSection.bind(this)
: this.sidebarState.expandSection.bind(this);
ADMIN_NAV_MAP.forEach((adminNav) => {
collapseOrExpand(
`${this.sidebarState.currentPanel.key}-${adminNav.name}`
);
});
}
<template>
<DButton
@action={{this.toggleAllSections}}
@icon={{this.icon}}
@title={{this.title}}
class="btn-transparent sidebar-toggle-all-sections"
/>
</template>
}

View File

@ -108,7 +108,7 @@ function defineAdminSection(
}
get name() {
return `admin-nav-section-${this.adminNavSectionData.name}`;
return `${ADMIN_PANEL}-${this.adminNavSectionData.name}`;
}
get title() {
@ -135,6 +135,10 @@ function defineAdminSection(
get displaySection() {
return true;
}
get collapsedByDefault() {
return this.adminNavSectionData.name !== "root";
}
};
return AdminNavSection;

View File

@ -38,6 +38,13 @@ export default class BaseCustomSidebarSection {
return true;
}
/**
* @returns {Boolean} Whether or not to collapse the entire section by default.
*/
get collapsedByDefault() {
return false;
}
_notImplemented() {
throw "not implemented";
}

View File

@ -1,5 +1,6 @@
import { tracked } from "@glimmer/tracking";
import Service from "@ember/service";
import { A } from "@ember/array";
import Service, { service } from "@ember/service";
import { disableImplicitInjections } from "discourse/lib/implicit-injections";
import {
currentPanelKey,
@ -13,11 +14,14 @@ import {
@disableImplicitInjections
export default class SidebarState extends Service {
@service keyValueStore;
@tracked currentPanelKey = currentPanelKey;
@tracked panels = panels;
@tracked mode = COMBINED_MODE;
@tracked displaySwitchPanelButtons = false;
@tracked filter = "";
@tracked collapsedSections = A([]);
previousState = {};
constructor() {
@ -63,6 +67,22 @@ export default class SidebarState extends Service {
};
}
collapseSection(sectionKey) {
const collapsedSidebarSectionKey = `sidebar-section-${sectionKey}-collapsed`;
this.keyValueStore.setItem(collapsedSidebarSectionKey, true);
this.collapsedSections.pushObject(collapsedSidebarSectionKey);
}
expandSection(sectionKey) {
const collapsedSidebarSectionKey = `sidebar-section-${sectionKey}-collapsed`;
this.keyValueStore.setItem(collapsedSidebarSectionKey, false);
this.collapsedSections.removeObject(collapsedSidebarSectionKey);
}
isCurrentPanel(panel) {
return this.currentPanel.key === panel;
}
restorePreviousState() {
const state = this.previousState[this.currentPanelKey];
if (!state) {

View File

@ -33,65 +33,54 @@ acceptance("Admin Sidebar - Sections", function (needs) {
await visit("/admin");
assert.ok(
exists(".sidebar-section[data-section-name='admin-nav-section-root']"),
exists(".sidebar-section[data-section-name='admin-root']"),
"root section is displayed"
);
assert.ok(
exists(".sidebar-section[data-section-name='admin-nav-section-account']"),
exists(".sidebar-section[data-section-name='admin-account']"),
"account section is displayed"
);
assert.ok(
exists(".sidebar-section[data-section-name='admin-nav-section-reports']"),
exists(".sidebar-section[data-section-name='admin-reports']"),
"reports section is displayed"
);
assert.ok(
exists(
".sidebar-section[data-section-name='admin-nav-section-community']"
),
exists(".sidebar-section[data-section-name='admin-community']"),
"community section is displayed"
);
assert.ok(
exists(
".sidebar-section[data-section-name='admin-nav-section-appearance']"
),
exists(".sidebar-section[data-section-name='admin-appearance']"),
"appearance section is displayed"
);
assert.ok(
exists(
".sidebar-section[data-section-name='admin-nav-section-email_settings']"
),
exists(".sidebar-section[data-section-name='admin-email_settings']"),
"email settings section is displayed"
);
assert.ok(
exists(
".sidebar-section[data-section-name='admin-nav-section-email_logs']"
),
exists(".sidebar-section[data-section-name='admin-email_logs']"),
"email logs settings section is displayed"
);
assert.ok(
exists(
".sidebar-section[data-section-name='admin-nav-section-security']"
),
exists(".sidebar-section[data-section-name='admin-security']"),
"security settings section is displayed"
);
assert.ok(
exists(".sidebar-section[data-section-name='admin-nav-section-plugins']"),
exists(".sidebar-section[data-section-name='admin-plugins']"),
"plugins section is displayed"
);
assert.ok(
exists(
".sidebar-section[data-section-name='admin-nav-section-advanced']"
),
exists(".sidebar-section[data-section-name='admin-advanced']"),
"advanced section is displayed"
);
});
test("enabled plugin admin routes have links added", async function (assert) {
await visit("/admin");
await click(".sidebar-toggle-all-sections");
assert.ok(
exists(
".sidebar-section[data-section-name='admin-nav-section-plugins'] .sidebar-section-link-wrapper[data-list-item-name=\"admin_installed_plugins\"]"
".sidebar-section[data-section-name='admin-plugins'] .sidebar-section-link-wrapper[data-list-item-name=\"admin_installed_plugins\"]"
),
"the admin plugin route is added to the plugins section"
);
@ -99,6 +88,7 @@ acceptance("Admin Sidebar - Sections", function (needs) {
test("Visit reports page", async function (assert) {
await visit("/admin");
await click(".sidebar-toggle-all-sections");
await click(".sidebar-section-link[data-link-name='admin_all_reports']");
assert.strictEqual(count(".admin-reports-list__report"), 1);
@ -170,28 +160,28 @@ acceptance("Admin Sidebar - Sections - Plugin API", function (needs) {
assert.ok(
exists(
".sidebar-section[data-section-name='admin-nav-section-root'] .sidebar-section-link-wrapper[data-list-item-name=\"admin_additional_root_test_section_link\"]"
".sidebar-section[data-section-name='admin-root'] .sidebar-section-link-wrapper[data-list-item-name=\"admin_additional_root_test_section_link\"]"
),
"link is appended to the root section"
);
assert.notOk(
exists(
".sidebar-section[data-section-name='admin-nav-section-root'] .sidebar-section-link-wrapper[data-list-item-name=\"admin_additional_root_test_section_link_no_route_or_href\"]"
".sidebar-section[data-section-name='admin-root'] .sidebar-section-link-wrapper[data-list-item-name=\"admin_additional_root_test_section_link_no_route_or_href\"]"
),
"invalid link that has no route or href is not appended to the root section"
);
assert.notOk(
exists(
".sidebar-section[data-section-name='admin-nav-section-root'] .sidebar-section-link-wrapper[data-list-item-name=\"admin_additional_root_test_section_link_no_label_or_text\"]"
".sidebar-section[data-section-name='admin-root'] .sidebar-section-link-wrapper[data-list-item-name=\"admin_additional_root_test_section_link_no_label_or_text\"]"
),
"invalid link that has no label or text is not appended to the root section"
);
assert.notOk(
exists(
".sidebar-section[data-section-name='admin-nav-section-root'] .sidebar-section-link-wrapper[data-list-item-name=\"admin_additional_root_test_section_link_invalid_label\"]"
".sidebar-section[data-section-name='admin-root'] .sidebar-section-link-wrapper[data-list-item-name=\"admin_additional_root_test_section_link_invalid_label\"]"
),
"invalid link with an invalid I18n key is not appended to the root section"
);

View File

@ -91,6 +91,10 @@
.badge-notification {
vertical-align: text-bottom;
}
.sidebar-filter {
width: calc(320px - 2 * var(--d-sidebar-row-horizontal-padding));
}
}
.search-menu .menu-panel {
@ -345,14 +349,12 @@
font-size: var(--font-down-1);
}
.sidebar-filter {
width: calc(100% - 2.35rem);
.sidebar-admin-header__row {
width: calc(320px - 2 * var(--d-sidebar-row-horizontal-padding));
}
.sidebar-sections {
&__back-to-forum {
margin: 0 var(--d-sidebar-row-horizontal-padding) 0.5em
var(--d-sidebar-row-horizontal-padding);
color: var(--d-sidebar-link-color);
display: flex;
align-items: center;

View File

@ -116,9 +116,8 @@
transition-delay: 0s;
}
&__back-to-forum {
margin: 0 var(--d-sidebar-row-horizontal-padding) 1em
var(--d-sidebar-row-horizontal-padding);
color: var(--d-sidebar-link-color);
display: flex;
align-items: center;
@ -310,8 +309,8 @@
}
.sidebar-filter {
margin: 0 var(--d-sidebar-row-horizontal-padding) 0.5em
var(--d-sidebar-row-horizontal-padding);
margin-top: 1em;
margin-bottom: 1em;
display: flex;
border: 1px solid var(--primary-400);
border-radius: var(--d-input-border-radius);
@ -319,7 +318,7 @@
justify-content: space-between;
background: var(--secondary);
width: calc(
var(--d-sidebar-width) - var(--d-sidebar-row-horizontal-padding) * 2
var(--d-sidebar-width) - 2 * var(--d-sidebar-row-horizontal-padding)
);
&:focus-within {
@ -336,7 +335,7 @@
&:focus-within {
outline: 0;
}
width: calc(100% - 2em);
width: 100%;
}
&__clear {
@ -346,6 +345,7 @@
background-color: var(--secondary);
}
}
.sidebar-no-results {
margin: 0.5em var(--d-sidebar-row-horizontal-padding) 0
var(--d-sidebar-row-horizontal-padding);
@ -359,3 +359,22 @@
.sidebar-section-wrapper + .sidebar-no-results {
display: none;
}
.sidebar-admin-header__row {
display: flex;
justify-content: space-between;
margin: 0 var(--d-sidebar-row-horizontal-padding) 0
var(--d-sidebar-row-horizontal-padding);
color: var(--d-sidebar-link-color);
width: calc(
var(--d-sidebar-width) - 2 * var(--d-sidebar-row-horizontal-padding) + 2px
);
}
.sidebar-toggle-all-sections.btn-transparent {
padding-right: 0;
color: var(--d-sidebar-link-color);
svg {
width: 0.75em;
}
}

View File

@ -4817,6 +4817,8 @@ en:
moderator: "Moderator"
back_to_forum: "Back to Forum"
filter_reports: Filter reports
expand_all_sections: "Expand all sections"
collapse_all_sections: "Collapse all sections"
tags:
remove_muted_tags_from_latest:

View File

@ -13,11 +13,13 @@ describe "Admin Revamp | Sidebar Navigation | Plugin Links", type: :system do
it "shows links to enabled plugin admin routes" do
visit("/admin")
sidebar.toggle_all_sections
expect(sidebar).to have_section_link("Chat", href: "/admin/plugins/chat")
end
it "does not duplicate links to enabled plugin admin routes when showing and hiding sidebar" do
visit("/admin")
sidebar.toggle_all_sections
expect(sidebar).to have_section_link("Chat", href: "/admin/plugins/chat", count: 1)
find(".header-sidebar-toggle").click
find(".header-sidebar-toggle").click
@ -68,12 +70,12 @@ describe "Admin Revamp | Sidebar Navigation | Plugin Links", type: :system do
admin.upsert_custom_fields(::Chat::LAST_CHAT_CHANNEL_ID => membership.chat_channel.id)
chat_page.prefers_full_page
visit("/admin")
expect(sidebar).to have_section("admin-nav-section-root")
expect(sidebar).to have_section("admin-root")
chat_page.open_from_header
expect(sidebar).to have_no_section("admin-nav-section-root")
expect(sidebar).to have_no_section("admin-root")
chat_page.minimize_full_page
expect(chat_page).to have_drawer
expect(sidebar).to have_section("admin-nav-section-root")
expect(sidebar).to have_section("admin-root")
end
end
end

View File

@ -14,15 +14,22 @@ describe "Admin Revamp | Sidebar Navigation", type: :system do
it "shows the sidebar when navigating to an admin route and hides it when leaving" do
visit("/latest")
expect(sidebar).to have_section("community")
expect(sidebar).to have_section("categories")
sidebar.click_link_in_section("community", "admin")
expect(page).to have_current_path("/admin")
expect(sidebar).to be_visible
expect(sidebar).to have_no_section("community")
expect(sidebar).to have_no_section("categories")
expect(page).to have_no_css(".admin-main-nav")
filter.click_back_to_forum
expect(page).to have_current_path("/latest")
expect(sidebar).to have_no_section("admin-nav-section-root")
expect(sidebar).to have_no_section("admin-root")
end
it "collapses sections by default" do
visit("/admin")
links = page.all(".sidebar-section-link-content-text")
expect(links.count).to eq(2)
expect(links.map(&:text)).to eq(["Dashboard", "All Site Settings"])
end
it "respects the user homepage preference for the Back to Forum link" do
@ -47,7 +54,7 @@ describe "Admin Revamp | Sidebar Navigation", type: :system do
filter.click_back_to_forum
expect(page).to have_current_path("/latest")
sidebar_dropdown.click
expect(sidebar).to have_no_section("admin-nav-section-root")
expect(sidebar).to have_no_section("admin-root")
end
end
@ -58,12 +65,13 @@ describe "Admin Revamp | Sidebar Navigation", type: :system do
visit("/latest")
sidebar.click_link_in_section("community", "admin")
expect(page).to have_current_path("/admin")
expect(sidebar).to have_no_section("admin-nav-section-root")
expect(sidebar).to have_no_section("admin-root")
end
end
it "allows links to be filtered" do
visit("/admin")
sidebar.toggle_all_sections
all_links_count = page.all(".sidebar-section-link-content-text").count
links = page.all(".sidebar-section-link-content-text")
@ -93,6 +101,21 @@ describe "Admin Revamp | Sidebar Navigation", type: :system do
expect(links.map(&:text)).to eq(["Appearance", "Preview Summary", "Server Setup"])
end
it "allows sections to be expanded" do
visit("/admin")
sidebar.toggle_all_sections
all_links_count = page.all(".sidebar-section-link-content-text").count
sidebar.toggle_all_sections
links = page.all(".sidebar-section-link-content-text")
expect(links.count).to eq(2)
expect(links.map(&:text)).to eq(["Dashboard", "All Site Settings"])
sidebar.toggle_all_sections
links = page.all(".sidebar-section-link-content-text")
expect(links.count).to eq(all_links_count)
end
it "accepts hidden keywords like installed plugin names for filter" do
Discourse.instance_variable_set(
"@plugins",
@ -100,6 +123,7 @@ describe "Admin Revamp | Sidebar Navigation", type: :system do
)
visit("/admin")
sidebar.toggle_all_sections
filter.filter("csp_extension")
links = page.all(".sidebar-section-link-content-text")
expect(links.count).to eq(1)

View File

@ -40,6 +40,10 @@ module PageObjects
def custom_section_modal_title
find("#discourse-modal-title")
end
def toggle_all_sections
find(".sidebar-toggle-all-sections").click
end
end
end
end