FEATURE: filter additional keywords for the sidebar (#26148)

With the new admin sidebar restructure, we have a link to "Installed plugins". We would like to ensure that when the admin is searching for a plugin name like "akismet" or "automation" this link will be visible. Also when entering the plugins page, related plugins should be highlighted.
This commit is contained in:
Krzysztof Kotlarek 2024-03-14 12:28:08 +11:00 committed by GitHub
parent bbb18fa2ce
commit 9afb0b29f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 150 additions and 44 deletions

View File

@ -12,7 +12,7 @@ import { ADMIN_PANEL } from "discourse/lib/sidebar/panels";
// TODO (martin) (2024-02-01) Remove this experimental UI.
export default class AdminConfigAreaSidebarExperiment extends Component {
@service adminSidebarExperimentStateManager;
@service adminSidebarStateManager;
@service toasts;
@service router;
@tracked editedNavConfig;
@ -46,7 +46,7 @@ export default class AdminConfigAreaSidebarExperiment extends Component {
@action
loadDefaultNavConfig() {
const savedConfig = this.adminSidebarExperimentStateManager.navConfig;
const savedConfig = this.adminSidebarStateManager.navConfig;
this.editedNavConfig = savedConfig
? JSON.stringify(savedConfig, null, 2)
: this.defaultAdminNav;
@ -116,7 +116,7 @@ export default class AdminConfigAreaSidebarExperiment extends Component {
}
#saveConfig(config) {
this.adminSidebarExperimentStateManager.navConfig = config;
this.adminSidebarStateManager.navConfig = config;
resetPanelSections(
ADMIN_PANEL,
useAdminNavConfig(config),

View File

@ -14,6 +14,7 @@ import PluginCommitHash from "./plugin-commit-hash";
export default class AdminPluginsListItem extends Component {
@service session;
@service currentUser;
@service sidebarState;
@action
async togglePluginEnabled(plugin) {
@ -30,9 +31,22 @@ export default class AdminPluginsListItem extends Component {
}
}
get isAdminSearchFiltered() {
if (!this.sidebarState.filter) {
return false;
}
return this.args.plugin.nameTitleizedLower.match(this.sidebarState.filter);
}
<template>
<tr data-plugin-name={{@plugin.name}}>
<td class="admin-plugins-list__row">
<tr
data-plugin-name={{@plugin.name}}
class={{concat
"admin-plugins-list__row"
(if this.isAdminSearchFiltered "-admin-search-filtered")
}}
>
<td class="admin-plugins-list__name-details">
<div class="admin-plugins-list__name-with-badges">
<div class="admin-plugins-list__name">
{{#if @plugin.linkUrl}}

View File

@ -1,4 +1,4 @@
import { tracked } from "@glimmer/tracking";
import { cached, tracked } from "@glimmer/tracking";
import { capitalize } from "@ember/string";
import I18n from "discourse-i18n";
@ -53,6 +53,7 @@ export default class AdminPlugin {
return "plugins";
}
@cached
get nameTitleized() {
// The category name is better in a lot of cases, as it's a human-inputted
// translation, and we can handle things like SAML instead of showing them
@ -79,6 +80,11 @@ export default class AdminPlugin {
return name;
}
@cached
get nameTitleizedLower() {
return this.nameTitleized.toLowerCase();
}
get author() {
if (this.isOfficial || this.isDiscourseOwned) {
return I18n.t("admin.plugins.author", { author: "Discourse" });

View File

@ -1,5 +1,6 @@
import { tracked } from "@glimmer/tracking";
import { service } from "@ember/service";
import PreloadStore from "discourse/lib/preload-store";
import { ADMIN_PANEL, MAIN_PANEL } from "discourse/lib/sidebar/panels";
import DiscourseRoute from "discourse/routes/discourse";
import I18n from "discourse-i18n";
@ -7,7 +8,9 @@ import I18n from "discourse-i18n";
export default class AdminRoute extends DiscourseRoute {
@service sidebarState;
@service siteSettings;
@service store;
@service currentUser;
@service adminSidebarStateManager;
@tracked initialSidebarState;
titleToken() {
@ -24,6 +27,13 @@ export default class AdminRoute extends DiscourseRoute {
this.controllerFor("application").setProperties({
showTop: false,
});
const visiblePlugins = PreloadStore.get("visiblePlugins");
if (visiblePlugins) {
this.adminSidebarStateManager.keywords.admin_installed_plugins = {
navigation: visiblePlugins.mapBy("name"),
};
}
}
deactivate(transition) {

View File

@ -1,7 +1,10 @@
import { tracked } from "@glimmer/tracking";
import Service from "@ember/service";
import { TrackedObject } from "@ember-compat/tracked-built-ins";
import KeyValueStore from "discourse/lib/key-value-store";
export default class AdminSidebarExperimentStateManager extends Service {
export default class AdminSidebarStateManager extends Service {
@tracked keywords = new TrackedObject();
STORE_NAMESPACE = "discourse_admin_sidebar_experiment_";
store = new KeyValueStore(this.STORE_NAMESPACE);

View File

@ -27,8 +27,14 @@ export default class SidebarApiSection extends Component {
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);
return (
link.text.toString().toLowerCase().match(this.sidebarState.filter) ||
link.keywords.navigation.some((keyword) =>
keyword.match(this.sidebarState.filter)
)
);
});
}
}

View File

@ -16,10 +16,11 @@ export function clearAdditionalAdminSidebarSectionLinks() {
}
class SidebarAdminSectionLink extends BaseCustomSidebarSectionLink {
constructor({ adminSidebarNavLink, router }) {
constructor({ adminSidebarNavLink, adminSidebarStateManager, router }) {
super(...arguments);
this.router = router;
this.adminSidebarNavLink = adminSidebarNavLink;
this.adminSidebarStateManager = adminSidebarStateManager;
}
get name() {
@ -80,14 +81,26 @@ class SidebarAdminSectionLink extends BaseCustomSidebarSectionLink {
return this.adminSidebarNavLink.route;
}
get keywords() {
return (
this.adminSidebarStateManager.keywords[this.adminSidebarNavLink.name] || {
navigation: [],
}
);
}
}
function defineAdminSection(adminNavSectionData, router) {
function defineAdminSection(
adminNavSectionData,
adminSidebarStateManager,
router
) {
const AdminNavSection = class extends BaseCustomSidebarSection {
constructor() {
super(...arguments);
this.adminNavSectionData = adminNavSectionData;
this.hideSectionHeader = adminNavSectionData.hideSectionHeader;
this.adminSidebarStateManager = adminSidebarStateManager;
}
get sectionLinks() {
@ -113,6 +126,7 @@ function defineAdminSection(adminNavSectionData, router) {
(sectionLinkData) =>
new SidebarAdminSectionLink({
adminSidebarNavLink: sectionLinkData,
adminSidebarStateManager: this.adminSidebarStateManager,
router,
})
);
@ -198,21 +212,21 @@ export function addAdminSidebarSectionLink(sectionName, link) {
}
function pluginAdminRouteLinks() {
return (PreloadStore.get("enabledPluginAdminRoutes") || []).map(
(pluginAdminRoute) => {
return (PreloadStore.get("visiblePlugins") || [])
.filter((plugin) => plugin.admin_route && plugin.enabled)
.map((plugin) => {
return {
name: `admin_plugin_${pluginAdminRoute.location}`,
route: pluginAdminRoute.use_new_show_route
? `adminPlugins.show.${pluginAdminRoute.location}`
: `adminPlugins.${pluginAdminRoute.location}`,
routeModels: pluginAdminRoute.use_new_show_route
? [pluginAdminRoute.location]
name: `admin_plugin_${plugin.admin_route.location}`,
route: plugin.admin_route.use_new_show_route
? `adminPlugins.show.${plugin.admin_route.location}`
: `adminPlugins.${plugin.admin_route.location}`,
routeModels: plugin.admin_route.use_new_show_route
? [plugin.admin_route.location]
: [],
label: pluginAdminRoute.label,
label: plugin.admin_route.label,
icon: "cog",
};
}
);
});
}
export default class AdminSidebarPanel extends BaseCustomSidebarPanel {
@ -233,11 +247,11 @@ export default class AdminSidebarPanel extends BaseCustomSidebarPanel {
return [];
}
this.adminSidebarExperimentStateManager = getOwnerWithFallback(this).lookup(
"service:admin-sidebar-experiment-state-manager"
this.adminSidebarStateManager = getOwnerWithFallback(this).lookup(
"service:admin-sidebar-state-manager"
);
const savedConfig = this.adminSidebarExperimentStateManager.navConfig;
const savedConfig = this.adminSidebarStateManager.navConfig;
const navMap = savedConfig || ADMIN_NAV_MAP;
if (!session.get("safe_mode")) {
@ -253,10 +267,22 @@ export default class AdminSidebarPanel extends BaseCustomSidebarPanel {
});
}
navMap.forEach((section) =>
section.links.forEach((link) => {
if (link.keywords) {
this.adminSidebarStateManager.keywords[link.name] = link.keywords;
}
})
);
const navConfig = useAdminNavConfig(navMap);
return navConfig.map((adminNavSectionData) => {
return defineAdminSection(adminNavSectionData, router);
return defineAdminSection(
adminNavSectionData,
this.adminSidebarStateManager,
router
);
});
}

View File

@ -42,7 +42,7 @@ acceptance("Admin - Plugins", function (needs) {
assert
.dom(
"table.admin-plugins-list tr .admin-plugins-list__row .admin-plugins-list__name-with-badges .admin-plugins-list__name"
"table.admin-plugins-list .admin-plugins-list__row .admin-plugins-list__name-details .admin-plugins-list__name-with-badges .admin-plugins-list__name"
)
.hasText("Some Test Plugin", "displays the plugin in the table");

View File

@ -13,10 +13,14 @@ acceptance("Admin Sidebar - Sections", function (needs) {
});
needs.hooks.beforeEach(() => {
PreloadStore.store("enabledPluginAdminRoutes", [
PreloadStore.store("visiblePlugins", [
{
location: "index",
label: "admin.plugins.title",
name: "plugin title",
admin_route: {
location: "index",
label: "admin.plugins.title",
enabled: true,
},
},
]);
});
@ -83,7 +87,7 @@ acceptance("Admin Sidebar - Sections", function (needs) {
assert.ok(
exists(
".sidebar-section[data-section-name='admin-nav-section-plugins'] .sidebar-section-link-wrapper[data-list-item-name=\"admin_plugin_index\"]"
".sidebar-section[data-section-name='admin-nav-section-plugins'] .sidebar-section-link-wrapper[data-list-item-name=\"admin_installed_plugins\"]"
),
"the admin plugin route is added to the plugins section"
);

View File

@ -9,16 +9,16 @@
.admin-plugins-list {
@media screen and (min-width: 550px) {
tr {
.admin-plugins-list__row {
grid-template-columns: 0.25fr repeat(4, 1fr);
}
}
@include breakpoint(mobile-extra-large) {
tr {
.admin-plugins-list__row {
grid-template-columns: 0.25fr repeat(3, 1fr);
}
.admin-plugins-list {
&__row {
&__name-details {
grid-column-start: 2;
grid-column-end: -1;
}
@ -41,6 +41,10 @@
}
}
.admin-plugins-list__row-admin-search-filtered {
background-color: var(--primary-low);
}
&__author {
font-size: var(--font-down-2);
padding: 0 0 0.25em 0;

View File

@ -673,8 +673,18 @@ class ApplicationController < ActionController::Base
# Used to show plugin-specific admin routes in the sidebar.
store_preloaded(
"enabledPluginAdminRoutes",
MultiJson.dump(Discourse.plugins_sorted_by_name.filter_map(&:admin_route)),
"visiblePlugins",
MultiJson.dump(
Discourse
.plugins_sorted_by_name(enabled_only: false)
.map do |plugin|
{
name: plugin.name.downcase,
admin_route: plugin.admin_route,
enabled: plugin.enabled?,
}
end,
),
)
end
end

View File

@ -18,20 +18,28 @@ RSpec.describe ApplicationController do
end
context "when user is admin" do
it "has correctly loaded preloaded data for enabledPluginAdminRoutes" do
it "has correctly loaded preloaded data for visiblePlugins" do
sign_in(admin)
get "/latest"
expect(JSON.parse(preloaded_json["enabledPluginAdminRoutes"])).to include(
{ "label" => "chat.admin.title", "location" => "chat", "use_new_show_route" => false },
expect(JSON.parse(preloaded_json["visiblePlugins"])).to include(
{
"name" => "chat",
"admin_route" => {
"label" => "chat.admin.title",
"location" => "chat",
"use_new_show_route" => false,
},
"enabled" => true,
},
)
end
end
context "when user is not admin" do
it "does not include preloaded data for enabledPluginAdminRoutes" do
it "does not include preloaded data for visiblePlugins" do
sign_in(user)
get "/latest"
expect(preloaded_json["enabledPluginAdminRoutes"]).to eq(nil)
expect(preloaded_json["visiblePlugins"]).to eq(nil)
end
end
end

View File

@ -1295,7 +1295,7 @@ RSpec.describe ApplicationController do
"topicTrackingStates",
"topicTrackingStateMeta",
"fontMap",
"enabledPluginAdminRoutes",
"visiblePlugins",
],
)
end
@ -1309,9 +1309,9 @@ RSpec.describe ApplicationController do
)
end
it "has correctly loaded enabledPluginAdminRoutes" do
it "has correctly loaded visiblePlugins" do
get "/latest"
expect(JSON.parse(preloaded_json["enabledPluginAdminRoutes"])).to eq([])
expect(JSON.parse(preloaded_json["visiblePlugins"])).to eq([])
end
end
end

View File

@ -17,7 +17,9 @@ describe "Admin Plugins List", type: :system, js: true do
visit "/admin/plugins"
plugin_row =
find(".admin-plugins-list tr[data-plugin-name=\"spoiler-alert\"] td.admin-plugins-list__row")
find(
".admin-plugins-list tr[data-plugin-name=\"spoiler-alert\"] td.admin-plugins-list__name-details",
)
expect(plugin_row).to have_css(
".admin-plugins-list__name-with-badges .admin-plugins-list__name",
text: "Spoiler Alert",

View File

@ -92,4 +92,17 @@ describe "Admin Revamp | Sidebar Navigation", type: :system do
expect(links.count).to eq(3)
expect(links.map(&:text)).to eq(["Appearance", "Preview Summary", "Server Setup"])
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")
filter.filter("csp_extension")
links = page.all(".sidebar-section-link-content-text")
expect(links.count).to eq(1)
expect(links.map(&:text)).to eq(["Installed"])
end
end