mirror of
https://github.com/discourse/discourse.git
synced 2025-02-16 23:12:45 +08:00
FEATURE: filter admin sidebar (#25853)
Ability to filter admin sidebar. The filter can be cleared. In addition, it can be accessed with ctrl+/ shortcut
This commit is contained in:
parent
ffce2dd04f
commit
8b5204579c
|
@ -83,6 +83,10 @@ export default class KeyboardShortcutsHelp extends Component {
|
|||
keys2: [CTRL, ALT, "f"],
|
||||
keysDelimiter: PLUS,
|
||||
}),
|
||||
filter_sidebar: buildShortcut("application.filter_sidebar", {
|
||||
keys1: [META, "/"],
|
||||
keysDelimiter: PLUS,
|
||||
}),
|
||||
help: buildShortcut("application.help", { keys1: ["?"] }),
|
||||
dismiss_new: buildShortcut("application.dismiss_new", {
|
||||
keys1: ["x", "r"],
|
||||
|
|
|
@ -1,45 +1,46 @@
|
|||
<Sidebar::Section
|
||||
@sectionName={{this.section.name}}
|
||||
@headerLinkText={{this.section.text}}
|
||||
@headerLinkTitle={{this.section.title}}
|
||||
@headerActionsIcon={{this.section.actionsIcon}}
|
||||
@headerActions={{this.section.actions}}
|
||||
@willDestroy={{this.section.willDestroy}}
|
||||
@collapsable={{@collapsable}}
|
||||
@displaySection={{this.section.displaySection}}
|
||||
@hideSectionHeader={{this.section.hideSectionHeader}}
|
||||
>
|
||||
|
||||
{{#each this.section.links as |link|}}
|
||||
<Sidebar::SectionLink
|
||||
@linkName={{link.name}}
|
||||
@linkClass={{link.classNames}}
|
||||
@route={{link.route}}
|
||||
@model={{link.model}}
|
||||
@models={{link.models}}
|
||||
@href={{link.href}}
|
||||
@title={{link.title}}
|
||||
@contentCSSClass={{link.contentCSSClass}}
|
||||
@prefixColor={{link.prefixColor}}
|
||||
@prefixBadge={{link.prefixBadge}}
|
||||
@prefixType={{link.prefixType}}
|
||||
@prefixValue={{link.prefixValue}}
|
||||
@prefixCSSClass={{link.prefixCSSClass}}
|
||||
@suffixType={{link.suffixType}}
|
||||
@suffixValue={{link.suffixValue}}
|
||||
@suffixCSSClass={{link.suffixCSSClass}}
|
||||
@hoverType={{link.hoverType}}
|
||||
@hoverValue={{link.hoverValue}}
|
||||
@hoverAction={{link.hoverAction}}
|
||||
@hoverTitle={{link.hoverTitle}}
|
||||
@currentWhen={{link.currentWhen}}
|
||||
@didInsert={{link.didInsert}}
|
||||
@willDestroy={{link.willDestroy}}
|
||||
@content={{link.text}}
|
||||
@contentComponent={{component
|
||||
link.contentComponent
|
||||
status=link.contentComponentArgs
|
||||
}}
|
||||
/>
|
||||
{{/each}}
|
||||
</Sidebar::Section>
|
||||
{{#if this.shouldDisplay}}
|
||||
<Sidebar::Section
|
||||
@sectionName={{this.section.name}}
|
||||
@headerLinkText={{this.section.text}}
|
||||
@headerLinkTitle={{this.section.title}}
|
||||
@headerActionsIcon={{this.section.actionsIcon}}
|
||||
@headerActions={{this.section.actions}}
|
||||
@willDestroy={{this.section.willDestroy}}
|
||||
@collapsable={{@collapsable}}
|
||||
@displaySection={{this.section.displaySection}}
|
||||
@hideSectionHeader={{this.section.hideSectionHeader}}
|
||||
>
|
||||
{{#each this.filteredLinks key="name" as |link|}}
|
||||
<Sidebar::SectionLink
|
||||
@linkName={{link.name}}
|
||||
@linkClass={{link.classNames}}
|
||||
@route={{link.route}}
|
||||
@model={{link.model}}
|
||||
@models={{link.models}}
|
||||
@href={{link.href}}
|
||||
@title={{link.title}}
|
||||
@contentCSSClass={{link.contentCSSClass}}
|
||||
@prefixColor={{link.prefixColor}}
|
||||
@prefixBadge={{link.prefixBadge}}
|
||||
@prefixType={{link.prefixType}}
|
||||
@prefixValue={{link.prefixValue}}
|
||||
@prefixCSSClass={{link.prefixCSSClass}}
|
||||
@suffixType={{link.suffixType}}
|
||||
@suffixValue={{link.suffixValue}}
|
||||
@suffixCSSClass={{link.suffixCSSClass}}
|
||||
@hoverType={{link.hoverType}}
|
||||
@hoverValue={{link.hoverValue}}
|
||||
@hoverAction={{link.hoverAction}}
|
||||
@hoverTitle={{link.hoverTitle}}
|
||||
@currentWhen={{link.currentWhen}}
|
||||
@didInsert={{link.didInsert}}
|
||||
@willDestroy={{link.willDestroy}}
|
||||
@content={{link.text}}
|
||||
@contentComponent={{component
|
||||
link.contentComponent
|
||||
status=link.contentComponentArgs
|
||||
}}
|
||||
/>
|
||||
{{/each}}
|
||||
</Sidebar::Section>
|
||||
{{/if}}
|
|
@ -1,10 +1,34 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { getOwner, setOwner } from "@ember/application";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
export default class SidebarApiSection extends Component {
|
||||
@service sidebarState;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.section = new this.args.sectionConfig();
|
||||
setOwner(this.section, getOwner(this));
|
||||
}
|
||||
|
||||
get shouldDisplay() {
|
||||
if (!this.sidebarState.currentPanel.filterable) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
this.sidebarState.filter.length === 0 || this.filteredLinks.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
get filteredLinks() {
|
||||
if (!this.sidebarState.filter) {
|
||||
return this.section.links;
|
||||
}
|
||||
if (this.section.text.toLowerCase().match(this.sidebarState.filter)) {
|
||||
return this.section.links;
|
||||
}
|
||||
return this.section.links.filter((link) => {
|
||||
return link.text.toString().toLowerCase().match(this.sidebarState.filter);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<Sidebar::Filter />
|
||||
{{#each this.sections as |sectionConfig|}}
|
||||
<Sidebar::ApiSection
|
||||
@sectionConfig={{sectionConfig}}
|
||||
@collapsable={{@collapsable}}
|
||||
/>
|
||||
{{/each}}
|
||||
{{/each}}
|
||||
<Sidebar::FilterNoResults />
|
|
@ -0,0 +1,29 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { inject as service } from "@ember/service";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
|
||||
export default class FilterNoResulsts extends Component {
|
||||
@service sidebarState;
|
||||
|
||||
/**
|
||||
* Component is rendered when panel is filtreable
|
||||
* Visibility is additionally controlled by CSS rule `.sidebar-section-wrapper + .sidebar-no-results`
|
||||
*/
|
||||
get shouldDisplay() {
|
||||
return this.sidebarState.currentPanel.filterable;
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if this.shouldDisplay}}
|
||||
<div class="sidebar-no-results">
|
||||
<div class="sidebar-no-results__title">{{i18n
|
||||
"sidebar.no_results.title"
|
||||
}}</div>
|
||||
<div class="sidebar-no-results__description">{{i18n
|
||||
"sidebar.no_results.description"
|
||||
filter=this.sidebarState.filter
|
||||
}}</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { Input } from "@ember/component";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
|
||||
import { inject as service } from "@ember/service";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import dIcon from "discourse-common/helpers/d-icon";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
|
||||
export default class Filter extends Component {
|
||||
@service sidebarState;
|
||||
|
||||
get shouldDisplay() {
|
||||
return this.sidebarState.currentPanel.filterable;
|
||||
}
|
||||
|
||||
get displayClearFilter() {
|
||||
return this.sidebarState.filter.length > 0;
|
||||
}
|
||||
|
||||
@bind
|
||||
teardown() {
|
||||
this.sidebarState.clearFilter();
|
||||
}
|
||||
|
||||
@action
|
||||
setFilter(event) {
|
||||
this.sidebarState.filter = event.target.value.toLowerCase();
|
||||
}
|
||||
|
||||
@action
|
||||
clearFilter() {
|
||||
this.sidebarState.clearFilter();
|
||||
document.querySelector(".sidebar-filter__input").focus();
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if this.shouldDisplay}}
|
||||
<div class="sidebar-filter" {{willDestroy this.teardown}}>
|
||||
<Input
|
||||
class="sidebar-filter__input"
|
||||
placeholder={{i18n "sidebar.filter"}}
|
||||
@value={{this.sidebarState.filter}}
|
||||
{{on "input" this.setFilter}}
|
||||
/>
|
||||
{{#if this.displayClearFilter}}
|
||||
<DButton @action={{this.clearFilter}} class="sidebar-filter__clear">
|
||||
{{dIcon "times"}}
|
||||
</DButton>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
|
@ -41,6 +41,8 @@ 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 },
|
||||
|
@ -469,6 +471,15 @@ export default {
|
|||
composer.focusComposer(event);
|
||||
},
|
||||
|
||||
filterSidebar() {
|
||||
const filterInput = document.querySelector(".sidebar-filter__input");
|
||||
|
||||
if (filterInput) {
|
||||
this._scrollTo(0);
|
||||
filterInput.focus();
|
||||
}
|
||||
},
|
||||
|
||||
fullscreenComposer() {
|
||||
const composer = getOwner(this).lookup("service:composer");
|
||||
if (composer.get("model")) {
|
||||
|
|
|
@ -255,4 +255,8 @@ export default class AdminSidebarPanel extends BaseCustomSidebarPanel {
|
|||
return defineAdminSection(adminNavSectionData);
|
||||
});
|
||||
}
|
||||
|
||||
get filterable() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,6 +43,13 @@ export default class BaseCustomSidebarPanel {
|
|||
this.hidden || this.#notImplemented();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean} Controls whether the filter is shown
|
||||
*/
|
||||
get filterable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
#notImplemented() {
|
||||
throw "not implemented";
|
||||
}
|
||||
|
|
|
@ -17,10 +17,10 @@ export default class SidebarState extends Service {
|
|||
@tracked panels = panels;
|
||||
@tracked mode = COMBINED_MODE;
|
||||
@tracked displaySwitchPanelButtons = false;
|
||||
@tracked filter = "";
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
this.#reset();
|
||||
}
|
||||
|
||||
|
@ -64,4 +64,8 @@ export default class SidebarState extends Service {
|
|||
this.panels = panels;
|
||||
this.mode = COMBINED_MODE;
|
||||
}
|
||||
|
||||
clearFilter() {
|
||||
this.filter = "";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -344,6 +344,10 @@
|
|||
div.discourse-tags {
|
||||
font-size: var(--font-down-1);
|
||||
}
|
||||
|
||||
.sidebar-filter {
|
||||
width: calc(100% - 2.35rem);
|
||||
}
|
||||
}
|
||||
|
||||
// Panel / user-notification-list styles. **not** menu panel sizing styles
|
||||
|
|
|
@ -306,3 +306,54 @@
|
|||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-filter {
|
||||
margin: 0 var(--d-sidebar-row-horizontal-padding) 0.5em
|
||||
var(--d-sidebar-row-horizontal-padding);
|
||||
display: flex;
|
||||
border: 1px solid var(--primary-400);
|
||||
border-radius: var(--d-input-border-radius);
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: var(--secondary);
|
||||
width: calc(
|
||||
var(--d-sidebar-width) - var(--d-sidebar-row-horizontal-padding) * 2
|
||||
);
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--tertiary);
|
||||
outline: 2px solid var(--tertiary);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
&__input[type="text"] {
|
||||
border: 0;
|
||||
margin-bottom: 0;
|
||||
width: 50px;
|
||||
height: 2em;
|
||||
&:focus-within {
|
||||
outline: 0;
|
||||
}
|
||||
width: calc(100% - 2em);
|
||||
}
|
||||
|
||||
&__clear {
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
color: var(--primary-medium);
|
||||
background-color: var(--secondary);
|
||||
}
|
||||
}
|
||||
.sidebar-no-results {
|
||||
margin: 0.5em var(--d-sidebar-row-horizontal-padding) 0
|
||||
var(--d-sidebar-row-horizontal-padding);
|
||||
&__title {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
.sidebar-no-results {
|
||||
display: block;
|
||||
}
|
||||
.sidebar-section-wrapper + .sidebar-no-results {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -4243,6 +4243,7 @@ en:
|
|||
user_profile_menu: "%{shortcut} Open user menu"
|
||||
show_incoming_updated_topics: "%{shortcut} Show updated topics"
|
||||
search: "%{shortcut} Search"
|
||||
filter_sidebar: "%{shortcut} Filter sidebar"
|
||||
help: "%{shortcut} Open keyboard help"
|
||||
dismiss_new: "%{shortcut} Dismiss New"
|
||||
dismiss_topics: "%{shortcut} Dismiss Topics"
|
||||
|
@ -4677,6 +4678,11 @@ en:
|
|||
panels:
|
||||
forum:
|
||||
label: Forum
|
||||
filter: "Filter"
|
||||
clear_filter: "Clear filter"
|
||||
no_results:
|
||||
title: "No results"
|
||||
description: "We couldn’t find anything matching ‘%{filter}’"
|
||||
|
||||
welcome_topic_banner:
|
||||
title: "Create your Welcome Topic"
|
||||
|
|
|
@ -5,6 +5,7 @@ describe "Admin Revamp | 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.admin_sidebar_enabled_groups = Group::AUTO_GROUPS[:admins]
|
||||
|
@ -60,4 +61,34 @@ describe "Admin Revamp | Sidebar Navigation", type: :system do
|
|||
expect(sidebar).to have_no_section("admin-nav-section-root")
|
||||
end
|
||||
end
|
||||
|
||||
it "allows links to be filtered" do
|
||||
visit("/admin")
|
||||
all_links_count = page.all(".sidebar-section-link-content-text").count
|
||||
|
||||
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")
|
||||
|
||||
filter.filter("ie")
|
||||
links = page.all(".sidebar-section-link-content-text")
|
||||
expect(links.count).to eq(2)
|
||||
expect(links.map(&:text)).to eq(["Preview Summary", "User Fields"])
|
||||
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")
|
||||
|
||||
# When match section title, display all links
|
||||
filter.filter("Backups")
|
||||
links = page.all(".sidebar-section-link-content-text")
|
||||
expect(links.count).to eq(2)
|
||||
expect(links.map(&:text)).to eq(%w[Backups Logs])
|
||||
end
|
||||
end
|
||||
|
|
17
spec/system/page_objects/components/filter.rb
Normal file
17
spec/system/page_objects/components/filter.rb
Normal file
|
@ -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
|
Loading…
Reference in New Issue
Block a user