diff --git a/app/assets/javascripts/discourse/app/components/sidebar/filter-no-results.gjs b/app/assets/javascripts/discourse/app/components/sidebar/filter-no-results.gjs new file mode 100644 index 0000000000000..d46c208e41eff --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/sidebar/filter-no-results.gjs @@ -0,0 +1,35 @@ +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 + ); + } + + +} diff --git a/app/assets/javascripts/discourse/app/components/sidebar/filter.gjs b/app/assets/javascripts/discourse/app/components/sidebar/filter.gjs new file mode 100644 index 0000000000000..d783f2d7835b9 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/sidebar/filter.gjs @@ -0,0 +1,61 @@ +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 + clearFilter() { + this.sidebarState.clearFilter(); + document.querySelector(".sidebar-filter__input").focus(); + } + + +} diff --git a/app/assets/javascripts/discourse/app/components/sidebar/panel-header.gjs b/app/assets/javascripts/discourse/app/components/sidebar/panel-header.gjs index 02d6c2411019b..8f8943196901d 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/panel-header.gjs +++ b/app/assets/javascripts/discourse/app/components/sidebar/panel-header.gjs @@ -2,10 +2,13 @@ import Component from "@glimmer/component"; import { service } from "@ember/service"; import BackToForum from "discourse/components/sidebar/back-to-forum"; import Search from "discourse/components/sidebar/search"; +import Filter from "./filter"; +import FilterNoResults from "./filter-no-results"; import ToggleAllSections from "./toggle-all-sections"; export default class PanelHeader extends Component { @service sidebarState; + @service currentUser; get shouldDisplay() { return this.sidebarState.currentPanel.displayHeader; @@ -20,7 +23,9 @@ export default class PanelHeader extends Component { + {{/if}} diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/admin-sidebar.js b/app/assets/javascripts/discourse/app/lib/sidebar/admin-sidebar.js index 936cf4284e489..1214c5cfbf4f6 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/admin-sidebar.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/admin-sidebar.js @@ -10,6 +10,7 @@ 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,7 +410,25 @@ export default class AdminSidebarPanel extends BaseCustomSidebarPanel { } get searchable() { - return true; + const currentUser = getOwnerWithFallback(this).lookup( + "service:current-user" + ); + return currentUser.admin; + } + + get filterable() { + const currentUser = getOwnerWithFallback(this).lookup( + "service:current-user" + ); + return !currentUser.admin && currentUser.moderator; + } + + filterNoResultsDescription(filter) { + const escapedFilter = escapeExpression(filter); + + i18n("sidebar.no_results.description_admin_search", { + filter: escapedFilter, + }); } get onSearchClick() { diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/base-custom-sidebar-panel.js b/app/assets/javascripts/discourse/app/lib/sidebar/base-custom-sidebar-panel.js index e415fb04cd4a1..04bd0d6f6bcf6 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/base-custom-sidebar-panel.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/base-custom-sidebar-panel.js @@ -55,7 +55,27 @@ export default class BaseCustomSidebarPanel { } /** - * @returns {boolean} Controls whether the search is shown + * @returns {boolean} Controls whether the filter is shown. + * Filter allows to remove sidebar links which does not match the filter phrase. + */ + get filterable() { + 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; + } + + /** + * @returns {boolean} Controls whether the search is shown. + * Displays modal on click allowing searching for admin pages, site settings, themes, components and reports. */ get searchable() { return false; diff --git a/app/assets/stylesheets/common/base/menu-panel.scss b/app/assets/stylesheets/common/base/menu-panel.scss index 2092b779fd9af..cef39f77a29d8 100644 --- a/app/assets/stylesheets/common/base/menu-panel.scss +++ b/app/assets/stylesheets/common/base/menu-panel.scss @@ -233,6 +233,10 @@ vertical-align: text-bottom; } + .sidebar-filter { + width: 100%; + } + .sidebar-search { width: 100%; } diff --git a/app/assets/stylesheets/common/base/sidebar.scss b/app/assets/stylesheets/common/base/sidebar.scss index 571ab10c5066e..0f3f12989bfaa 100644 --- a/app/assets/stylesheets/common/base/sidebar.scss +++ b/app/assets/stylesheets/common/base/sidebar.scss @@ -353,6 +353,68 @@ } } +.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: rgb(var(--tertiary-rgb) / 10%); + 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-search { width: 100%; diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 25886d02b1713..78edbfd6ec020 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -5096,6 +5096,11 @@ en: forum: label: Forum back_to_forum: "Back to Forum" + filter_links: "Filter links..." + clear_filter: "Clear filter" + no_results: + title: "No results" + description_admin_search: 'We couldn’t find anything matching ‘%{filter}’.' collapse_all_sections: "Collapse all sections" expand_all_sections: "Expand all sections" search: "Search" diff --git a/spec/system/admin_sidebar_navigation_spec.rb b/spec/system/admin_sidebar_navigation_spec.rb index 7f8bde374d00d..86cbede331025 100644 --- a/spec/system/admin_sidebar_navigation_spec.rb +++ b/spec/system/admin_sidebar_navigation_spec.rb @@ -8,6 +8,7 @@ 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" @@ -158,5 +159,10 @@ I18n.t("admin_js.admin.config.staff_action_logs.title"), ], ) + + filter.filter("watched") + links = page.all(".sidebar-section-link-content-text") + expect(links.count).to eq(1) + expect(links.map(&:text)).to eq(["Watched words"]) end end diff --git a/spec/system/page_objects/components/filter.rb b/spec/system/page_objects/components/filter.rb new file mode 100644 index 0000000000000..3bcdd671e4a77 --- /dev/null +++ b/spec/system/page_objects/components/filter.rb @@ -0,0 +1,17 @@ +# 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 + end + end +end