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. // TODO (martin) (2024-02-01) Remove this experimental UI.
export default class AdminConfigAreaSidebarExperiment extends Component { export default class AdminConfigAreaSidebarExperiment extends Component {
@service adminSidebarExperimentStateManager; @service adminSidebarStateManager;
@service toasts; @service toasts;
@service router; @service router;
@tracked editedNavConfig; @tracked editedNavConfig;
@ -46,7 +46,7 @@ export default class AdminConfigAreaSidebarExperiment extends Component {
@action @action
loadDefaultNavConfig() { loadDefaultNavConfig() {
const savedConfig = this.adminSidebarExperimentStateManager.navConfig; const savedConfig = this.adminSidebarStateManager.navConfig;
this.editedNavConfig = savedConfig this.editedNavConfig = savedConfig
? JSON.stringify(savedConfig, null, 2) ? JSON.stringify(savedConfig, null, 2)
: this.defaultAdminNav; : this.defaultAdminNav;
@ -116,7 +116,7 @@ export default class AdminConfigAreaSidebarExperiment extends Component {
} }
#saveConfig(config) { #saveConfig(config) {
this.adminSidebarExperimentStateManager.navConfig = config; this.adminSidebarStateManager.navConfig = config;
resetPanelSections( resetPanelSections(
ADMIN_PANEL, ADMIN_PANEL,
useAdminNavConfig(config), useAdminNavConfig(config),

View File

@ -14,6 +14,7 @@ import PluginCommitHash from "./plugin-commit-hash";
export default class AdminPluginsListItem extends Component { export default class AdminPluginsListItem extends Component {
@service session; @service session;
@service currentUser; @service currentUser;
@service sidebarState;
@action @action
async togglePluginEnabled(plugin) { 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> <template>
<tr data-plugin-name={{@plugin.name}}> <tr
<td class="admin-plugins-list__row"> 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-with-badges">
<div class="admin-plugins-list__name"> <div class="admin-plugins-list__name">
{{#if @plugin.linkUrl}} {{#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 { capitalize } from "@ember/string";
import I18n from "discourse-i18n"; import I18n from "discourse-i18n";
@ -53,6 +53,7 @@ export default class AdminPlugin {
return "plugins"; return "plugins";
} }
@cached
get nameTitleized() { get nameTitleized() {
// The category name is better in a lot of cases, as it's a human-inputted // 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 // translation, and we can handle things like SAML instead of showing them
@ -79,6 +80,11 @@ export default class AdminPlugin {
return name; return name;
} }
@cached
get nameTitleizedLower() {
return this.nameTitleized.toLowerCase();
}
get author() { get author() {
if (this.isOfficial || this.isDiscourseOwned) { if (this.isOfficial || this.isDiscourseOwned) {
return I18n.t("admin.plugins.author", { author: "Discourse" }); return I18n.t("admin.plugins.author", { author: "Discourse" });

View File

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

View File

@ -1,7 +1,10 @@
import { tracked } from "@glimmer/tracking";
import Service from "@ember/service"; import Service from "@ember/service";
import { TrackedObject } from "@ember-compat/tracked-built-ins";
import KeyValueStore from "discourse/lib/key-value-store"; 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_NAMESPACE = "discourse_admin_sidebar_experiment_";
store = new KeyValueStore(this.STORE_NAMESPACE); 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)) { if (this.section.text.toLowerCase().match(this.sidebarState.filter)) {
return this.section.links; return this.section.links;
} }
return this.section.links.filter((link) => { 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 { class SidebarAdminSectionLink extends BaseCustomSidebarSectionLink {
constructor({ adminSidebarNavLink, router }) { constructor({ adminSidebarNavLink, adminSidebarStateManager, router }) {
super(...arguments); super(...arguments);
this.router = router; this.router = router;
this.adminSidebarNavLink = adminSidebarNavLink; this.adminSidebarNavLink = adminSidebarNavLink;
this.adminSidebarStateManager = adminSidebarStateManager;
} }
get name() { get name() {
@ -80,14 +81,26 @@ class SidebarAdminSectionLink extends BaseCustomSidebarSectionLink {
return this.adminSidebarNavLink.route; 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 { const AdminNavSection = class extends BaseCustomSidebarSection {
constructor() { constructor() {
super(...arguments); super(...arguments);
this.adminNavSectionData = adminNavSectionData; this.adminNavSectionData = adminNavSectionData;
this.hideSectionHeader = adminNavSectionData.hideSectionHeader; this.hideSectionHeader = adminNavSectionData.hideSectionHeader;
this.adminSidebarStateManager = adminSidebarStateManager;
} }
get sectionLinks() { get sectionLinks() {
@ -113,6 +126,7 @@ function defineAdminSection(adminNavSectionData, router) {
(sectionLinkData) => (sectionLinkData) =>
new SidebarAdminSectionLink({ new SidebarAdminSectionLink({
adminSidebarNavLink: sectionLinkData, adminSidebarNavLink: sectionLinkData,
adminSidebarStateManager: this.adminSidebarStateManager,
router, router,
}) })
); );
@ -198,21 +212,21 @@ export function addAdminSidebarSectionLink(sectionName, link) {
} }
function pluginAdminRouteLinks() { function pluginAdminRouteLinks() {
return (PreloadStore.get("enabledPluginAdminRoutes") || []).map( return (PreloadStore.get("visiblePlugins") || [])
(pluginAdminRoute) => { .filter((plugin) => plugin.admin_route && plugin.enabled)
.map((plugin) => {
return { return {
name: `admin_plugin_${pluginAdminRoute.location}`, name: `admin_plugin_${plugin.admin_route.location}`,
route: pluginAdminRoute.use_new_show_route route: plugin.admin_route.use_new_show_route
? `adminPlugins.show.${pluginAdminRoute.location}` ? `adminPlugins.show.${plugin.admin_route.location}`
: `adminPlugins.${pluginAdminRoute.location}`, : `adminPlugins.${plugin.admin_route.location}`,
routeModels: pluginAdminRoute.use_new_show_route routeModels: plugin.admin_route.use_new_show_route
? [pluginAdminRoute.location] ? [plugin.admin_route.location]
: [], : [],
label: pluginAdminRoute.label, label: plugin.admin_route.label,
icon: "cog", icon: "cog",
}; };
} });
);
} }
export default class AdminSidebarPanel extends BaseCustomSidebarPanel { export default class AdminSidebarPanel extends BaseCustomSidebarPanel {
@ -233,11 +247,11 @@ export default class AdminSidebarPanel extends BaseCustomSidebarPanel {
return []; return [];
} }
this.adminSidebarExperimentStateManager = getOwnerWithFallback(this).lookup( this.adminSidebarStateManager = getOwnerWithFallback(this).lookup(
"service:admin-sidebar-experiment-state-manager" "service:admin-sidebar-state-manager"
); );
const savedConfig = this.adminSidebarExperimentStateManager.navConfig; const savedConfig = this.adminSidebarStateManager.navConfig;
const navMap = savedConfig || ADMIN_NAV_MAP; const navMap = savedConfig || ADMIN_NAV_MAP;
if (!session.get("safe_mode")) { 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); const navConfig = useAdminNavConfig(navMap);
return navConfig.map((adminNavSectionData) => { 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 assert
.dom( .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"); .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(() => { needs.hooks.beforeEach(() => {
PreloadStore.store("enabledPluginAdminRoutes", [ PreloadStore.store("visiblePlugins", [
{ {
location: "index", name: "plugin title",
label: "admin.plugins.title", admin_route: {
location: "index",
label: "admin.plugins.title",
enabled: true,
},
}, },
]); ]);
}); });
@ -83,7 +87,7 @@ acceptance("Admin Sidebar - Sections", function (needs) {
assert.ok( assert.ok(
exists( 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" "the admin plugin route is added to the plugins section"
); );

View File

@ -9,16 +9,16 @@
.admin-plugins-list { .admin-plugins-list {
@media screen and (min-width: 550px) { @media screen and (min-width: 550px) {
tr { .admin-plugins-list__row {
grid-template-columns: 0.25fr repeat(4, 1fr); grid-template-columns: 0.25fr repeat(4, 1fr);
} }
} }
@include breakpoint(mobile-extra-large) { @include breakpoint(mobile-extra-large) {
tr { .admin-plugins-list__row {
grid-template-columns: 0.25fr repeat(3, 1fr); grid-template-columns: 0.25fr repeat(3, 1fr);
} }
.admin-plugins-list { .admin-plugins-list {
&__row { &__name-details {
grid-column-start: 2; grid-column-start: 2;
grid-column-end: -1; grid-column-end: -1;
} }
@ -41,6 +41,10 @@
} }
} }
.admin-plugins-list__row-admin-search-filtered {
background-color: var(--primary-low);
}
&__author { &__author {
font-size: var(--font-down-2); font-size: var(--font-down-2);
padding: 0 0 0.25em 0; 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. # Used to show plugin-specific admin routes in the sidebar.
store_preloaded( store_preloaded(
"enabledPluginAdminRoutes", "visiblePlugins",
MultiJson.dump(Discourse.plugins_sorted_by_name.filter_map(&:admin_route)), 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
end end

View File

@ -18,20 +18,28 @@ RSpec.describe ApplicationController do
end end
context "when user is admin" do 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) sign_in(admin)
get "/latest" get "/latest"
expect(JSON.parse(preloaded_json["enabledPluginAdminRoutes"])).to include( expect(JSON.parse(preloaded_json["visiblePlugins"])).to include(
{ "label" => "chat.admin.title", "location" => "chat", "use_new_show_route" => false }, {
"name" => "chat",
"admin_route" => {
"label" => "chat.admin.title",
"location" => "chat",
"use_new_show_route" => false,
},
"enabled" => true,
},
) )
end end
end end
context "when user is not admin" do 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) sign_in(user)
get "/latest" get "/latest"
expect(preloaded_json["enabledPluginAdminRoutes"]).to eq(nil) expect(preloaded_json["visiblePlugins"]).to eq(nil)
end end
end end
end end

View File

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

View File

@ -17,7 +17,9 @@ describe "Admin Plugins List", type: :system, js: true do
visit "/admin/plugins" visit "/admin/plugins"
plugin_row = 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( expect(plugin_row).to have_css(
".admin-plugins-list__name-with-badges .admin-plugins-list__name", ".admin-plugins-list__name-with-badges .admin-plugins-list__name",
text: "Spoiler Alert", 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.count).to eq(3)
expect(links.map(&:text)).to eq(["Appearance", "Preview Summary", "Server Setup"]) expect(links.map(&:text)).to eq(["Appearance", "Preview Summary", "Server Setup"])
end 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 end