mirror of
https://github.com/discourse/discourse.git
synced 2025-02-21 18:50:55 +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"],
|
keys2: [CTRL, ALT, "f"],
|
||||||
keysDelimiter: PLUS,
|
keysDelimiter: PLUS,
|
||||||
}),
|
}),
|
||||||
|
filter_sidebar: buildShortcut("application.filter_sidebar", {
|
||||||
|
keys1: [META, "/"],
|
||||||
|
keysDelimiter: PLUS,
|
||||||
|
}),
|
||||||
help: buildShortcut("application.help", { keys1: ["?"] }),
|
help: buildShortcut("application.help", { keys1: ["?"] }),
|
||||||
dismiss_new: buildShortcut("application.dismiss_new", {
|
dismiss_new: buildShortcut("application.dismiss_new", {
|
||||||
keys1: ["x", "r"],
|
keys1: ["x", "r"],
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
{{#if this.shouldDisplay}}
|
||||||
<Sidebar::Section
|
<Sidebar::Section
|
||||||
@sectionName={{this.section.name}}
|
@sectionName={{this.section.name}}
|
||||||
@headerLinkText={{this.section.text}}
|
@headerLinkText={{this.section.text}}
|
||||||
@ -9,8 +10,7 @@
|
|||||||
@displaySection={{this.section.displaySection}}
|
@displaySection={{this.section.displaySection}}
|
||||||
@hideSectionHeader={{this.section.hideSectionHeader}}
|
@hideSectionHeader={{this.section.hideSectionHeader}}
|
||||||
>
|
>
|
||||||
|
{{#each this.filteredLinks key="name" as |link|}}
|
||||||
{{#each this.section.links as |link|}}
|
|
||||||
<Sidebar::SectionLink
|
<Sidebar::SectionLink
|
||||||
@linkName={{link.name}}
|
@linkName={{link.name}}
|
||||||
@linkClass={{link.classNames}}
|
@linkClass={{link.classNames}}
|
||||||
@ -43,3 +43,4 @@
|
|||||||
/>
|
/>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</Sidebar::Section>
|
</Sidebar::Section>
|
||||||
|
{{/if}}
|
@ -1,10 +1,34 @@
|
|||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import { getOwner, setOwner } from "@ember/application";
|
import { getOwner, setOwner } from "@ember/application";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
|
||||||
export default class SidebarApiSection extends Component {
|
export default class SidebarApiSection extends Component {
|
||||||
|
@service sidebarState;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(...arguments);
|
super(...arguments);
|
||||||
this.section = new this.args.sectionConfig();
|
this.section = new this.args.sectionConfig();
|
||||||
setOwner(this.section, getOwner(this));
|
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|}}
|
{{#each this.sections as |sectionConfig|}}
|
||||||
<Sidebar::ApiSection
|
<Sidebar::ApiSection
|
||||||
@sectionConfig={{sectionConfig}}
|
@sectionConfig={{sectionConfig}}
|
||||||
@collapsable={{@collapsable}}
|
@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" },
|
"!": { postAction: "showFlags" },
|
||||||
"#": { handler: "goToPost", anonymous: true },
|
"#": { handler: "goToPost", anonymous: true },
|
||||||
"/": { handler: "toggleSearch", 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 },
|
"ctrl+alt+f": { handler: "toggleSearch", anonymous: true, global: true },
|
||||||
"=": { handler: "toggleHamburgerMenu", anonymous: true },
|
"=": { handler: "toggleHamburgerMenu", anonymous: true },
|
||||||
"?": { handler: "showHelpModal", anonymous: true },
|
"?": { handler: "showHelpModal", anonymous: true },
|
||||||
@ -469,6 +471,15 @@ export default {
|
|||||||
composer.focusComposer(event);
|
composer.focusComposer(event);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
filterSidebar() {
|
||||||
|
const filterInput = document.querySelector(".sidebar-filter__input");
|
||||||
|
|
||||||
|
if (filterInput) {
|
||||||
|
this._scrollTo(0);
|
||||||
|
filterInput.focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
fullscreenComposer() {
|
fullscreenComposer() {
|
||||||
const composer = getOwner(this).lookup("service:composer");
|
const composer = getOwner(this).lookup("service:composer");
|
||||||
if (composer.get("model")) {
|
if (composer.get("model")) {
|
||||||
|
@ -255,4 +255,8 @@ export default class AdminSidebarPanel extends BaseCustomSidebarPanel {
|
|||||||
return defineAdminSection(adminNavSectionData);
|
return defineAdminSection(adminNavSectionData);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get filterable() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,13 @@ export default class BaseCustomSidebarPanel {
|
|||||||
this.hidden || this.#notImplemented();
|
this.hidden || this.#notImplemented();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {boolean} Controls whether the filter is shown
|
||||||
|
*/
|
||||||
|
get filterable() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
#notImplemented() {
|
#notImplemented() {
|
||||||
throw "not implemented";
|
throw "not implemented";
|
||||||
}
|
}
|
||||||
|
@ -17,10 +17,10 @@ export default class SidebarState extends Service {
|
|||||||
@tracked panels = panels;
|
@tracked panels = panels;
|
||||||
@tracked mode = COMBINED_MODE;
|
@tracked mode = COMBINED_MODE;
|
||||||
@tracked displaySwitchPanelButtons = false;
|
@tracked displaySwitchPanelButtons = false;
|
||||||
|
@tracked filter = "";
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(...arguments);
|
super(...arguments);
|
||||||
|
|
||||||
this.#reset();
|
this.#reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,4 +64,8 @@ export default class SidebarState extends Service {
|
|||||||
this.panels = panels;
|
this.panels = panels;
|
||||||
this.mode = COMBINED_MODE;
|
this.mode = COMBINED_MODE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearFilter() {
|
||||||
|
this.filter = "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -344,6 +344,10 @@
|
|||||||
div.discourse-tags {
|
div.discourse-tags {
|
||||||
font-size: var(--font-down-1);
|
font-size: var(--font-down-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-filter {
|
||||||
|
width: calc(100% - 2.35rem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Panel / user-notification-list styles. **not** menu panel sizing styles
|
// Panel / user-notification-list styles. **not** menu panel sizing styles
|
||||||
|
@ -306,3 +306,54 @@
|
|||||||
margin-bottom: 1em;
|
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"
|
user_profile_menu: "%{shortcut} Open user menu"
|
||||||
show_incoming_updated_topics: "%{shortcut} Show updated topics"
|
show_incoming_updated_topics: "%{shortcut} Show updated topics"
|
||||||
search: "%{shortcut} Search"
|
search: "%{shortcut} Search"
|
||||||
|
filter_sidebar: "%{shortcut} Filter sidebar"
|
||||||
help: "%{shortcut} Open keyboard help"
|
help: "%{shortcut} Open keyboard help"
|
||||||
dismiss_new: "%{shortcut} Dismiss New"
|
dismiss_new: "%{shortcut} Dismiss New"
|
||||||
dismiss_topics: "%{shortcut} Dismiss Topics"
|
dismiss_topics: "%{shortcut} Dismiss Topics"
|
||||||
@ -4677,6 +4678,11 @@ en:
|
|||||||
panels:
|
panels:
|
||||||
forum:
|
forum:
|
||||||
label: 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:
|
welcome_topic_banner:
|
||||||
title: "Create your Welcome Topic"
|
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) { PageObjects::Components::NavigationMenu::Sidebar.new }
|
||||||
let(:sidebar_dropdown) { PageObjects::Components::SidebarHeaderDropdown.new }
|
let(:sidebar_dropdown) { PageObjects::Components::SidebarHeaderDropdown.new }
|
||||||
|
let(:filter) { PageObjects::Components::Filter.new }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
SiteSetting.admin_sidebar_enabled_groups = Group::AUTO_GROUPS[:admins]
|
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")
|
expect(sidebar).to have_no_section("admin-nav-section-root")
|
||||||
end
|
end
|
||||||
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
|
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…
x
Reference in New Issue
Block a user