FIX: remove sidebar filter (#32485)

Remove sidebar filter in favour of search.

In addition:
- Search was moved up just below Dashboard;
- Do not perform search when filter is empty;
- Display "no results".

![Screenshot 2025-04-28 at 12 37
38 pm](https://github.com/user-attachments/assets/d8cabbca-2a26-413b-abfc-5ae143ff8da6)
![Screenshot 2025-04-28 at 12 37
59 pm](https://github.com/user-attachments/assets/fbae4b8d-60a2-414e-bf40-86e8c50538e6)
![Screenshot 2025-04-28 at 12 38
12 pm](https://github.com/user-attachments/assets/e04371cd-392e-48eb-91c9-21a3d8fc44c0)
This commit is contained in:
Krzysztof Kotlarek
2025-04-28 15:57:29 +08:00
committed by GitHub
parent 84c8f51c54
commit e3b35ff2a5
16 changed files with 48 additions and 438 deletions
@@ -6,11 +6,13 @@ import didUpdate from "@ember/render-modifiers/modifiers/did-update";
import { service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import { TrackedObject } from "@ember-compat/tracked-built-ins";
import { and, not } from "truth-helpers";
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
import DButton from "discourse/components/d-button";
import icon from "discourse/helpers/d-icon";
import discourseDebounce from "discourse/lib/debounce";
import { INPUT_DELAY } from "discourse/lib/environment";
import { escapeExpression } from "discourse/lib/utilities";
import autoFocus from "discourse/modifiers/auto-focus";
import { i18n } from "discourse-i18n";
import AdminSearchFilters from "admin/components/admin-search-filters";
@@ -62,6 +64,12 @@ export default class AdminSearch extends Component {
return !this.adminSearchDataSource.isLoaded || this.loading;
}
get noResultsDescription() {
return i18n("admin.search.no_results", {
filter: escapeExpression(this.filter),
});
}
@action
toggleFilters() {
this.showFilters = !this.showFilters;
@@ -91,7 +99,9 @@ export default class AdminSearch extends Component {
changeSearchTerm(event) {
this.searchResults = [];
this.filter = event.target.value;
this.runSearch();
if (this.filter.length > 0) {
this.runSearch();
}
}
@action
@@ -219,6 +229,11 @@ export default class AdminSearch extends Component {
</a>
</div>
{{/each}}
{{#if (and (not this.searchResults) this.filter)}}
<p class="admin-search__no-results">
{{this.noResultsDescription}}
</p>
{{/if}}
</ConditionalLoadingSpinner>
</div>
</template>
@@ -1,35 +0,0 @@
import Component from "@glimmer/component";
import { service } from "@ember/service";
import { i18n } from "discourse-i18n";
export default class FilterNoResults extends Component {
@service sidebarState;
get shouldDisplay() {
return (
this.sidebarState.currentPanel.filterable &&
!!(this.args.sections?.length === 0)
);
}
get noResultsDescription() {
return this.sidebarState.currentPanel.filterNoResultsDescription(
this.sidebarState.filter
);
}
<template>
{{#if this.shouldDisplay}}
<div class="sidebar-no-results">
<h4 class="sidebar-no-results__title">{{i18n
"sidebar.no_results.title"
}}</h4>
{{#if this.noResultsDescription}}
<p class="sidebar-no-results__description">
{{this.noResultsDescription}}
</p>
{{/if}}
</div>
{{/if}}
</template>
}
@@ -1,82 +0,0 @@
import Component from "@glimmer/component";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { service } from "@ember/service";
import DButton from "discourse/components/d-button";
import { i18n } from "discourse-i18n";
export default class Filter extends Component {
@service sidebarState;
@service router;
@service currentUser;
willDestroy() {
super.willDestroy(...arguments);
this.sidebarState.clearFilter();
}
get shouldDisplay() {
return this.sidebarState.currentPanel.filterable;
}
get displayClearFilter() {
return this.sidebarState.filter.length > 0;
}
@action
setFilter(event) {
this.sidebarState.filter = event.target.value;
}
@action
handleEscape(event) {
if (event.key === "Escape") {
event.stopPropagation();
if (this.sidebarState.filter.length > 0) {
this.sidebarState.filter = "";
} else {
event.target.blur();
}
}
}
@action
clearFilter() {
this.sidebarState.clearFilter();
document.querySelector(".sidebar-filter__input").focus();
}
get filterPlaceholder() {
if (this.currentUser?.staff) {
return i18n("sidebar.filter_links");
}
return i18n("sidebar.filter");
}
<template>
{{#if this.shouldDisplay}}
<div class="sidebar-filter">
<div class="sidebar-filter__input-container">
<input
{{on "input" this.setFilter}}
{{on "keydown" this.handleEscape}}
value={{this.sidebarState.filter}}
placeholder={{this.filterPlaceholder}}
type="text"
enterkeyhint="done"
class="sidebar-filter__input"
/>
{{#if this.displayClearFilter}}
<DButton
@action={{this.clearFilter}}
@icon="xmark"
class="sidebar-filter__clear"
/>
{{/if}}
</div>
</div>
{{/if}}
</template>
}
@@ -1,8 +1,6 @@
import Component from "@glimmer/component";
import { service } from "@ember/service";
import BackToForum from "./back-to-forum";
import Filter from "./filter";
import FilterNoResults from "./filter-no-results";
import ToggleAllSections from "./toggle-all-sections";
export default class PanelHeader extends Component {
@@ -19,11 +17,7 @@ export default class PanelHeader extends Component {
<BackToForum />
<ToggleAllSections @sections={{@sections}} />
</div>
<div class="sidebar-panel-header__row">
<Filter />
</div>
</div>
<FilterNoResults @sections={{@sections}} />
{{/if}}
</template>
}
@@ -50,8 +50,6 @@ const DEFAULT_BINDINGS = {
"!": { postAction: "showFlags" },
"#": { handler: "goToPost", anonymous: true },
"/": { handler: "toggleSearch", anonymous: true },
"meta+/": { handler: "filterSidebar", anonymous: true },
[`${PLATFORM_KEY_MODIFIER}+/`]: { handler: "filterSidebar", anonymous: true },
"ctrl+alt+f": { handler: "toggleSearch", anonymous: true, global: true },
"=": { handler: "toggleHamburgerMenu", anonymous: true },
"?": { handler: "showHelpModal", anonymous: true },
@@ -495,14 +493,6 @@ export default {
composer.focusComposer(event);
},
filterSidebar() {
const filterInput = document.querySelector(".sidebar-filter__input");
if (filterInput) {
this._scrollTo(0);
}
},
fullscreenComposer() {
const composer = getOwner(this).lookup("service:composer");
if (composer.get("model")) {
@@ -12,14 +12,6 @@ export const ADMIN_NAV_MAP = [
icon: "house",
moderator: true,
},
{
name: "admin_users",
route: "adminUsers",
label: "admin.config.users.title",
description: "admin.config.users.header_description",
icon: "users",
moderator: true,
},
{
name: "admin_search",
route: "adminSearch",
@@ -28,6 +20,14 @@ export const ADMIN_NAV_MAP = [
icon: "magnifying-glass",
moderator: true,
},
{
name: "admin_users",
route: "adminUsers",
label: "admin.config.users.title",
description: "admin.config.users.header_description",
icon: "users",
moderator: true,
},
{
name: "admin_groups",
route: "adminGroups",
@@ -1,6 +1,5 @@
import { cached } from "@glimmer/tracking";
import { warn } from "@ember/debug";
import { htmlSafe } from "@ember/template";
import { configNavForPlugin } from "discourse/lib/admin-plugin-config-nav";
import { adminRouteValid } from "discourse/lib/admin-utilities";
import { getOwnerWithFallback } from "discourse/lib/get-owner";
@@ -11,7 +10,6 @@ import BaseCustomSidebarPanel from "discourse/lib/sidebar/base-custom-sidebar-pa
import BaseCustomSidebarSection from "discourse/lib/sidebar/base-custom-sidebar-section";
import BaseCustomSidebarSectionLink from "discourse/lib/sidebar/base-custom-sidebar-section-link";
import { ADMIN_PANEL } from "discourse/lib/sidebar/panels";
import { escapeExpression } from "discourse/lib/utilities";
import I18n, { i18n } from "discourse-i18n";
let additionalAdminSidebarSectionLinks = {};
@@ -409,21 +407,4 @@ export default class AdminSidebarPanel extends BaseCustomSidebarPanel {
);
});
}
get filterable() {
return true;
}
filterNoResultsDescription(filter) {
const escapedFilter = escapeExpression(filter);
return htmlSafe(
i18n("sidebar.no_results.description_admin_search", {
filter: escapedFilter,
admin_search_url: getURL(
`/admin/search?filter=${encodeURIComponent(filter)}`
),
})
);
}
}
@@ -50,13 +50,6 @@ export default class BaseCustomSidebarPanel {
return false;
}
/**
* @returns {boolean} Controls whether the filter is shown
*/
get filterable() {
return false;
}
get expandActiveSection() {
return false;
}
@@ -65,17 +58,6 @@ export default class BaseCustomSidebarPanel {
return false;
}
/**
* @param {string} filter filter applied
*
* @returns {string | SafeString} Description displayed when the applied filter has no results.
* Use `htmlSafe` from `from "@ember/template` to use HTML strings.
*/
// eslint-disable-next-line no-unused-vars
filterNoResultsDescription(filter) {
return null;
}
#notImplemented() {
throw "not implemented";
}
@@ -1,4 +1,4 @@
import { click, fillIn, triggerKeyEvent, visit } from "@ember/test-helpers";
import { click, fillIn, visit } from "@ember/test-helpers";
import { test } from "qunit";
import { AUTO_GROUPS } from "discourse/lib/constants";
import { withPluginApi } from "discourse/lib/plugin-api";
@@ -64,25 +64,6 @@ acceptance("Admin Sidebar - Sections", function (needs) {
.exists("advanced section is displayed");
});
test("filter sections and clear filter with ESC", async function (assert) {
await visit("/admin");
await fillIn(".sidebar-filter__input", "advanced");
assert
.dom(".sidebar-section[data-section-name='admin-plugins']")
.doesNotExist("plugins section is hidden");
assert
.dom(".sidebar-section[data-section-name='admin-advanced']")
.exists("advanced section is displayed");
await triggerKeyEvent(".sidebar-filter__input", "keydown", "Escape");
assert
.dom(".sidebar-section[data-section-name='admin-plugins']")
.exists("plugins section is displayed");
assert
.dom(".sidebar-section[data-section-name='admin-advanced']")
.exists("advanced section is displayed");
});
test("enabled plugin admin routes have links added", async function (assert) {
await visit("/admin");
await click(".sidebar-toggle-all-sections");
@@ -229,10 +229,6 @@
.badge-notification {
vertical-align: text-bottom;
}
.sidebar-filter {
width: 100%;
}
}
.search-menu .menu-panel {
@@ -351,68 +351,6 @@
}
}
.sidebar-filter {
margin-top: 1em;
margin-bottom: 1em;
border: 1px solid var(--primary-400);
border-radius: var(--d-input-border-radius);
background: var(--secondary);
width: calc(
var(--d-sidebar-width) - 2 * var(--d-sidebar-row-horizontal-padding)
);
&:focus-within {
border-color: var(--tertiary);
outline: 1px solid var(--tertiary);
outline-offset: -1px;
}
&__input-container {
position: relative;
display: flex;
align-items: center;
background: var(--secondary);
border-radius: var(--d-input-border-radius);
}
&__shortcut-hint {
background-color: rgba(var(--tertiary-rgb), 0.1);
padding: 0.25em 0.5em;
margin-right: 0.5em;
font-size: var(--font-down-3);
color: var(--primary-medium);
}
&__input[type="text"] {
border: 0;
background: none;
margin-bottom: 0;
height: 2em;
width: 100%;
&:focus-within {
outline: 0;
}
}
&__clear {
width: 2em;
height: 2em;
color: var(--primary-medium);
background-color: var(--secondary);
}
}
.sidebar-no-results {
display: block;
margin: 0.5em var(--d-sidebar-row-horizontal-padding) 0
var(--d-sidebar-row-horizontal-padding);
&__title {
font-weight: bold;
}
}
.sidebar-panel-header__row {
display: flex;
justify-content: space-between;
+1 -6
View File
@@ -5081,12 +5081,6 @@ en:
back_to_forum: "Back to Forum"
collapse_all_sections: "Collapse all sections"
expand_all_sections: "Expand all sections"
filter: "Filter..."
filter_links: "Filter links..."
clear_filter: "Clear filter"
no_results:
title: "No results"
description_admin_search: 'We couldnt find anything matching %{filter}.<br><br>Try <a class="sidebar-additional-filter-admin-search" href="%{admin_search_url}">searching the entire admin interface</a>.'
footer:
interface_color_selector:
light: "Light"
@@ -5605,6 +5599,7 @@ en:
modal_title: "Search anything in admin"
title: "Search"
instructions: "Type to search for pages, settings, reports, themes, components, and more..."
no_results: 'We couldnt find anything matching %{filter}.'
result_types:
page:
one: "Page"
+11
View File
@@ -64,4 +64,15 @@ describe "Admin Search", type: :system do
I18n.t("site_settings.min_topic_title_length"),
)
end
it "informs user about no results" do
visit "/admin"
open_search_modal
search_modal.search("very long search phrase")
expect(search_modal).to have_content(
"We couldnt find anything matching very long search phrase.",
)
end
end
+6 -145
View File
@@ -8,7 +8,6 @@ describe "Admin | Sidebar Navigation", type: :system do
let(:sidebar) { PageObjects::Components::NavigationMenu::Sidebar.new }
let(:sidebar_dropdown) { PageObjects::Components::SidebarHeaderDropdown.new }
let(:filter) { PageObjects::Components::Filter.new }
before do
SiteSetting.navigation_menu = "sidebar"
@@ -24,7 +23,7 @@ describe "Admin | Sidebar Navigation", type: :system do
expect(sidebar).to be_visible
expect(sidebar).to have_no_section("categories")
expect(page).to have_no_css(".admin-main-nav")
filter.click_back_to_forum
sidebar.click_back_to_forum
expect(page).to have_current_path("/")
expect(sidebar).to have_no_section("admin-root")
end
@@ -35,7 +34,7 @@ describe "Admin | Sidebar Navigation", type: :system do
it "navigates back to homepage correctly" do
visit("/discuss/admin")
filter.click_back_to_forum
sidebar.click_back_to_forum
expect(page).to have_current_path("/discuss/")
end
end
@@ -51,8 +50,8 @@ describe "Admin | Sidebar Navigation", type: :system do
expect(links.map(&:text)).to eq(
[
I18n.t("admin_js.admin.dashboard.title"),
I18n.t("admin_js.admin.config.users.title"),
I18n.t("admin_js.admin.config.search_everything.title"),
I18n.t("admin_js.admin.config.users.title"),
I18n.t("admin_js.admin.config.groups.title"),
I18n.t("admin_js.admin.config.site_settings.title"),
I18n.t("admin_js.admin.config.whats_new.title"),
@@ -71,119 +70,13 @@ describe "Admin | Sidebar Navigation", type: :system do
sidebar_dropdown.click
expect(sidebar).to have_no_section("community")
expect(page).to have_no_css(".admin-main-nav")
filter.click_back_to_forum
sidebar.click_back_to_forum
expect(page).to have_current_path("/")
sidebar_dropdown.click
expect(sidebar).to have_no_section("admin-root")
end
end
it "allows links to be filtered" do
visit("/admin")
sidebar.toggle_all_sections
expect(page).to have_selector(
".sidebar-section-link-content-text",
minimum: UNFILTERED_LINK_COUNT,
)
expect(page).to have_no_css(".sidebar-no-results")
all_links_count = page.all(".sidebar-section-link-content-text").count
filter.filter("ie")
links = page.all(".sidebar-section-link-content-text")
expect(links.map(&:text)).to eq(
[
I18n.t("admin_js.admin.config.content.title"),
I18n.t("admin_js.admin.config.user_fields.title"),
I18n.t("admin_js.admin.config.flags.title"),
I18n.t("admin_js.admin.config.email.title"),
],
)
expect(page).to have_no_css(".sidebar-no-results")
filter.filter("ieeee")
expect(page).to have_no_css(".sidebar-section-link-content-text")
expect(page).to have_css(".sidebar-no-results")
filter.clear
links = page.all(".sidebar-section-link-content-text")
expect(links.count).to eq(all_links_count)
expect(page).to have_no_css(".sidebar-no-results")
expect(page).to have_css(".sidebar-sections__back-to-forum")
# When match section title, display all links
filter.filter("Email")
links = page.all(".sidebar-section-link-content-text")
expect(links.map(&:text)).to eq(
[
I18n.t("admin_js.admin.config.email.title"),
I18n.t("admin_js.admin.config.email_appearance.title"),
I18n.t("admin_js.admin.config.email_logs.title"),
I18n.t("admin_js.admin.config.staff_action_logs.title"),
],
)
end
it "escapes the filtered expression for regex expressions" do
visit("/admin")
filter.filter(".*") # this shouldn't return any results if the expression was escaped
expect(page).to have_no_css(".sidebar-section-link-content-text")
expect(page).to have_css(".sidebar-no-results")
end
it "displays the no results description message correctly when the filter has no results" do
visit("/admin")
filter.filter("ieeee")
expect(page).to have_no_css(".sidebar-section-link-content-text")
expect(page).to have_css(".sidebar-no-results")
no_results_description = page.find(".sidebar-no-results__description")
expect(no_results_description.text).to eq(
"We couldnt find anything matching ieeee.\n\nTry searching the entire admin interface.",
)
end
it "temporarily expands section when filter" do
visit("/admin")
links = page.all(".sidebar-section-link-content-text")
expect(links.map(&:text)).to eq(
[
I18n.t("admin_js.admin.dashboard.title"),
I18n.t("admin_js.admin.config.users.title"),
I18n.t("admin_js.admin.config.search_everything.title"),
I18n.t("admin_js.admin.config.groups.title"),
I18n.t("admin_js.admin.config.site_settings.title"),
I18n.t("admin_js.admin.config.whats_new.title"),
],
)
filter.filter("ie")
links = page.all(".sidebar-section-link-content-text")
expect(links.map(&:text)).to eq(
[
I18n.t("admin_js.admin.config.content.title"),
I18n.t("admin_js.admin.config.user_fields.title"),
I18n.t("admin_js.admin.config.flags.title"),
I18n.t("admin_js.admin.config.email.title"),
],
)
filter.filter("")
links = page.all(".sidebar-section-link-content-text")
expect(links.map(&:text)).to eq(
[
I18n.t("admin_js.admin.dashboard.title"),
I18n.t("admin_js.admin.config.users.title"),
I18n.t("admin_js.admin.config.search_everything.title"),
I18n.t("admin_js.admin.config.groups.title"),
I18n.t("admin_js.admin.config.site_settings.title"),
I18n.t("admin_js.admin.config.whats_new.title"),
],
)
end
it "allows sections to be expanded" do
visit("/admin")
sidebar.toggle_all_sections
@@ -197,8 +90,8 @@ describe "Admin | Sidebar Navigation", type: :system do
expect(all(".sidebar-section-link-content-text").map(&:text)).to eq(
[
I18n.t("admin_js.admin.dashboard.title"),
I18n.t("admin_js.admin.config.users.title"),
I18n.t("admin_js.admin.config.search_everything.title"),
I18n.t("admin_js.admin.config.users.title"),
I18n.t("admin_js.admin.config.groups.title"),
I18n.t("admin_js.admin.config.site_settings.title"),
I18n.t("admin_js.admin.config.whats_new.title"),
@@ -212,38 +105,6 @@ describe "Admin | Sidebar Navigation", type: :system do
)
end
it "accepts hidden keywords like installed plugin names for filter" do
Discourse.instance_variable_set(
"@plugins",
Plugin::Instance.find_all("#{Rails.root}/spec/fixtures/plugins"),
)
visit("/admin")
sidebar.toggle_all_sections
filter.filter("csp_extension")
links = page.all(".sidebar-section-link-content-text")
expect(links.count).to eq(1)
expect(links.map(&:text)).to eq([I18n.t("admin_js.admin.config.plugins.title")])
end
it "accepts components and themes keywords for filter" do
Fabricate(:theme, name: "Air theme", component: false)
Fabricate(:theme, name: "Kanban", component: true)
visit("/admin")
sidebar.toggle_all_sections
filter.filter("air")
links = page.all(".sidebar-section-link-content-text")
expect(links.count).to eq(1)
expect(links.map(&:text)).to eq(["Themes and components"])
filter.filter("kanban")
links = page.all(".sidebar-section-link-content-text")
expect(links.count).to eq(1)
expect(links.map(&:text)).to eq(["Themes and components"])
end
it "highlights the 'Themes and components' link when the themes page is visited" do
visit("/admin/customize/themes")
expect(page).to have_css(
@@ -279,8 +140,8 @@ describe "Admin | Sidebar Navigation", type: :system do
expect(links.map(&:text)).to eq(
[
I18n.t("admin_js.admin.dashboard.title"),
I18n.t("admin_js.admin.config.users.title"),
I18n.t("admin_js.admin.config.search_everything.title"),
I18n.t("admin_js.admin.config.users.title"),
I18n.t("admin_js.admin.config.groups.title"),
I18n.t("admin_js.admin.config.whats_new.title"),
I18n.t("admin_js.admin.config.reports.title"),
@@ -1,22 +0,0 @@
# frozen_string_literal: true
module PageObjects
module Components
class Filter < PageObjects::Components::Base
def filter(text)
page.find(".sidebar-filter__input").fill_in(with: text)
self
end
def clear
page.find(".sidebar-filter__clear").click
self
end
def click_back_to_forum
page.find(".sidebar-sections__back-to-forum").click
self
end
end
end
end
@@ -64,6 +64,11 @@ module PageObjects
def toggle_section(name)
find("[data-section-name='admin-#{name.to_s.downcase}']").click
end
def click_back_to_forum
page.find(".sidebar-sections__back-to-forum").click
self
end
end
end
end