FIX: Admin sidebar fixes and custom link registration (#25200)

This commit adds some more links to the admin sidebar and
removes some to give it more parity with the old nav structure.

This also adds the `addAdminSidebarSectionLink` plugin API to
replace the admin-menu plugin outlet, which is used by plugins
like docker-manager to add links to the old admin nav.
This commit is contained in:
Martin Brennan 2024-01-12 11:55:26 +10:00 committed by GitHub
parent 52511a3260
commit de88fc26df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 286 additions and 27 deletions

View File

@ -84,12 +84,16 @@ export default class AdminConfigAreaSidebarExperiment extends Component {
return;
}
// Using the private `_routerMicrolib` is not ideal, but Ember doesn't provide
// any other way for us to easily check for route validity.
try {
this.router._router._routerMicrolib.recognizer.handlersFor(
link.route
);
this.validRouteNames.add(link.route);
} catch {
} catch (err) {
// eslint-disable-next-line no-console
console.debug("[AdminSidebarExperiment]", err);
invalidRoutes.push(link.route);
}
});

View File

@ -33,7 +33,6 @@
{{/if}}
<NavItem @route="adminPlugins" @label="admin.plugins.title" />
{{/if}}
{{! TODO: What do we do with this?? How many plugins are using? }}
<PluginOutlet @name="admin-menu" />
</ul>
</div>

View File

@ -7,6 +7,13 @@ import {
import { ADMIN_PANEL } from "discourse/services/sidebar-state";
import I18n from "discourse-i18n";
let additionalAdminSidebarSectionLinks = {};
// For testing.
export function clearAdditionalAdminSidebarSectionLinks() {
additionalAdminSidebarSectionLinks = {};
}
function defineAdminSectionLink(BaseCustomSidebarSectionLink) {
const SidebarAdminSectionLink = class extends BaseCustomSidebarSectionLink {
constructor({ adminSidebarNavLink }) {
@ -132,12 +139,6 @@ export function useAdminNavConfig(navMap) {
label: "admin.users.title",
icon: "users",
},
{
name: "admin_reports",
route: "adminReports",
label: "admin.dashboard.reports_tab",
icon: "chart-pie",
},
{
name: "admin_badges",
route: "adminBadges",
@ -148,7 +149,18 @@ export function useAdminNavConfig(navMap) {
},
];
return adminNavSections.concat(navMap);
navMap = adminNavSections.concat(navMap);
for (const [sectionName, additionalLinks] of Object.entries(
additionalAdminSidebarSectionLinks
)) {
const section = navMap.findBy("name", sectionName);
if (section && additionalLinks.length) {
section.links.push(...additionalLinks);
}
}
return navMap;
}
let adminSectionLinkClass = null;
@ -172,16 +184,68 @@ export function buildAdminSidebar(navConfig) {
});
}
// This is used for a plugin API.
export function addAdminSidebarSectionLink(sectionName, link) {
if (!additionalAdminSidebarSectionLinks.hasOwnProperty(sectionName)) {
additionalAdminSidebarSectionLinks[sectionName] = [];
}
// make the name extra-unique
link.name = `admin_additional_${sectionName}_${link.name}`;
if (!link.href && !link.route) {
// eslint-disable-next-line no-console
console.debug(
"[AdminSidebar]",
`Custom link ${sectionName}_${link.name} must have either href or route`
);
return;
}
if (!link.label && !link.text) {
// eslint-disable-next-line no-console
console.debug(
"[AdminSidebar]",
`Custom link ${sectionName}_${link.name} must have either label (which is an I18n key) or text`
);
return;
}
// label must be valid, don't want broken [XYZ translation missing]
if (link.label && typeof I18n.lookup(link.label) !== "string") {
// eslint-disable-next-line no-console
console.debug(
"[AdminSidebar]",
`Custom link ${sectionName}_${link.name} must have a valid I18n label, got ${link.label}`
);
return;
}
additionalAdminSidebarSectionLinks[sectionName].push(link);
}
function pluginAdminRouteLinks() {
return (PreloadStore.get("enabledPluginAdminRoutes") || []).map(
(pluginAdminRoute) => {
return {
name: `admin_plugin_${pluginAdminRoute.location}`,
route: `adminPlugins.${pluginAdminRoute.location}`,
label: pluginAdminRoute.label,
icon: "cog",
};
}
);
}
export default {
name: "admin-sidebar-initializer",
initialize(owner) {
this.currentUser = owner.lookup("service:current-user");
this.siteSettings = owner.lookup("service:site-settings");
if (!this.currentUser?.staff) {
return;
}
if (
!this.currentUser?.staff ||
!this.siteSettings.userInAnyGroups(
"admin_sidebar_enabled_groups",
this.currentUser
@ -205,16 +269,7 @@ export default {
const savedConfig = this.adminSidebarExperimentStateManager.navConfig;
const navMap = savedConfig || ADMIN_NAV_MAP;
const enabledPluginAdminRoutes =
PreloadStore.get("enabledPluginAdminRoutes") || [];
enabledPluginAdminRoutes.forEach((pluginAdminRoute) => {
navMap.findBy("name", "admin_plugins").links.push({
name: `admin_plugin_${pluginAdminRoute.location}`,
route: `adminPlugins.${pluginAdminRoute.location}`,
label: pluginAdminRoute.label,
icon: "cog",
});
});
navMap.findBy("name", "plugins").links.push(...pluginAdminRouteLinks());
if (this.siteSettings.experimental_form_templates) {
navMap.findBy("name", "customize").links.push({

View File

@ -41,6 +41,7 @@ import {
} from "discourse/helpers/category-link";
import { addUsernameSelectorDecorator } from "discourse/helpers/decorate-username-selector";
import { registerCustomAvatarHelper } from "discourse/helpers/user-avatar";
import { addAdminSidebarSectionLink } from "discourse/instance-initializers/admin-sidebar";
import { addBeforeAuthCompleteCallback } from "discourse/instance-initializers/auth-complete";
import { addPopupMenuOption } from "discourse/lib/composer/custom-popup-menu-options";
import { registerDesktopNotificationHandler } from "discourse/lib/desktop-notifications";
@ -145,7 +146,7 @@ import { modifySelectKit } from "select-kit/mixins/plugin-api";
// docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md whenever you change the version
// using the format described at https://keepachangelog.com/en/1.0.0/.
export const PLUGIN_API_VERSION = "1.23.0";
export const PLUGIN_API_VERSION = "1.24.0";
// This helper prevents us from applying the same `modifyClass` over and over in test mode.
function canModify(klass, type, resolverName, changes) {
@ -2298,6 +2299,35 @@ class PluginApi {
addSidebarPanel(func);
}
/**
* EXPERIMENTAL. Do not use.
* Support for adding links to specific admin sidebar sections.
*
* This is intended to replace the admin-menu plugin outlet from
* the old admin horizontal nav.
*
* ```
* api.addAdminSidebarSectionLink("root", {
* name: "unique_link_name",
* label: "admin.some.i18n.label.key",
* route: "(optional) emberRouteId",
* href: "(optional) can be used instead of the route",
* }
* ```
* @param {String} sectionName - The name of the admin sidebar section to add the link to.
* @param {Object} link - A link object representing a section link for the sidebar.
* @param {string} link.name - The name of the link. Needs to be dasherized and lowercase.
* @param {string} link.title - The title attribute for the link.
* @param {string} link.text - The text to display for the link.
* @param {string} [link.route] - The Ember route name to generate the href attribute for the link.
* @param {string} [link.href] - The href attribute for the link.
* @param {string} [link.icon] - The FontAwesome icon to display for the link.
*/
addAdminSidebarSectionLink(sectionName, link) {
addAdminSidebarSectionLink(sectionName, link);
}
/**
* EXPERIMENTAL. Do not use.
* Support for setting a Sidebar panel.

View File

@ -1,6 +1,8 @@
import getURL from "discourse-common/lib/get-url";
export const ADMIN_NAV_MAP = [
{
name: "admin_plugins",
name: "plugins",
route: "adminPlugins.index",
label: "admin.plugins.title",
links: [
@ -14,7 +16,7 @@ export const ADMIN_NAV_MAP = [
},
{
name: "email",
text: "Email",
text: "Emails",
links: [
{
name: "admin_email",
@ -52,6 +54,18 @@ export const ADMIN_NAV_MAP = [
label: "admin.email.rejected",
icon: "ban",
},
{
name: "admin_email_preview_summary",
route: "adminEmail.previewDigest",
label: "admin.email.preview_digest",
icon: "notification.private_message",
},
{
name: "admin_email_advanced_test",
route: "adminEmail.advancedTest",
label: "admin.email.advanced_test.title",
icon: "wrench",
},
],
},
{
@ -88,6 +102,12 @@ export const ADMIN_NAV_MAP = [
label: "admin.logs.search_logs.title",
icon: "search",
},
{
name: "admin_logs_error_logs",
href: getURL("/logs"),
label: "admin.logs.logster.title",
icon: "external-link-alt",
},
],
},
{

View File

@ -0,0 +1,145 @@
import { visit } from "@ember/test-helpers";
import { test } from "qunit";
import { AUTO_GROUPS } from "discourse/lib/constants";
import { withPluginApi } from "discourse/lib/plugin-api";
import PreloadStore from "discourse/lib/preload-store";
import { acceptance, exists } from "discourse/tests/helpers/qunit-helpers";
acceptance("Admin Sidebar - Sections", function (needs) {
needs.user({
admin: true,
groups: [AUTO_GROUPS.admins],
});
needs.settings({
admin_sidebar_enabled_groups: "1",
});
needs.hooks.beforeEach(() => {
PreloadStore.store("enabledPluginAdminRoutes", [
{
location: "index",
label: "admin.plugins.title",
},
]);
});
test("default sections are loaded", async function (assert) {
await visit("/admin");
assert.ok(
exists(".sidebar-section[data-section-name='admin-nav-section-root']"),
"root section is displayed"
);
assert.ok(
exists(".sidebar-section[data-section-name='admin-nav-section-plugins']"),
"plugins section is displayed"
);
assert.ok(
exists(".sidebar-section[data-section-name='admin-nav-section-email']"),
"email section is displayed"
);
assert.ok(
exists(".sidebar-section[data-section-name='admin-nav-section-logs']"),
"logs section is displayed"
);
assert.ok(
exists(
".sidebar-section[data-section-name='admin-nav-section-customize']"
),
"customize section is displayed"
);
assert.ok(
exists(".sidebar-section[data-section-name='admin-nav-section-api']"),
"api section is displayed"
);
assert.ok(
exists(".sidebar-section[data-section-name='admin-nav-section-backups']"),
"backups section is displayed"
);
});
test("enabled plugin admin routes have links added", async function (assert) {
await visit("/admin");
assert.ok(
exists(
".sidebar-section[data-section-name='admin-nav-section-plugins'] .sidebar-section-link-wrapper[data-list-item-name=\"admin_plugin_index\"]"
),
"the admin plugin route is added to the plugins section"
);
});
});
acceptance("Admin Sidebar - Sections - Plugin API", function (needs) {
needs.user({
admin: true,
groups: [AUTO_GROUPS.admins],
});
needs.settings({
admin_sidebar_enabled_groups: "1",
});
needs.hooks.beforeEach(() => {
withPluginApi("1.24.0", (api) => {
api.addAdminSidebarSectionLink("root", {
name: "test_section_link",
label: "admin.plugins.title",
route: "adminPlugins.index",
icon: "cog",
});
api.addAdminSidebarSectionLink("root", {
name: "test_section_link_no_route_or_href",
label: "admin.plugins.title",
icon: "cog",
});
api.addAdminSidebarSectionLink("root", {
name: "test_section_link_no_label_or_text",
route: "adminPlugins.index",
icon: "cog",
});
api.addAdminSidebarSectionLink("root", {
name: "test_section_link_invalid_label",
label: "blahblah.i18n",
route: "adminPlugins.index",
icon: "cog",
});
});
});
test("additional valid links can be added to a section with the plugin API", async function (assert) {
await visit("/admin");
assert.ok(
exists(
".sidebar-section[data-section-name='admin-nav-section-root'] .sidebar-section-link-wrapper[data-list-item-name=\"admin_additional_root_test_section_link\"]"
),
"link is appended to the root section"
);
assert.notOk(
exists(
".sidebar-section[data-section-name='admin-nav-section-root'] .sidebar-section-link-wrapper[data-list-item-name=\"admin_additional_root_test_section_link_no_route_or_href\"]"
),
"invalid link that has no route or href is not appended to the root section"
);
assert.notOk(
exists(
".sidebar-section[data-section-name='admin-nav-section-root'] .sidebar-section-link-wrapper[data-list-item-name=\"admin_additional_root_test_section_link_no_label_or_text\"]"
),
"invalid link that has no label or text is not appended to the root section"
);
assert.notOk(
exists(
".sidebar-section[data-section-name='admin-nav-section-root'] .sidebar-section-link-wrapper[data-list-item-name=\"admin_additional_root_test_section_link_invalid_label\"]"
),
"invalid link with an invalid I18n key is not appended to the root section"
);
});
});

View File

@ -28,6 +28,7 @@ import { resetUserMenuProfileTabItems } from "discourse/components/user-menu/pro
import { resetCustomPostMessageCallbacks } from "discourse/controllers/topic";
import { clearHTMLCache } from "discourse/helpers/custom-html";
import { resetUsernameDecorators } from "discourse/helpers/decorate-username-selector";
import { clearAdditionalAdminSidebarSectionLinks } from "discourse/instance-initializers/admin-sidebar";
import { resetBeforeAuthCompleteCallbacks } from "discourse/instance-initializers/auth-complete";
import { clearPopupMenuOptions } from "discourse/lib/composer/custom-popup-menu-options";
import { clearDesktopNotificationHandlers } from "discourse/lib/desktop-notifications";
@ -240,6 +241,7 @@ export function testCleanup(container, app) {
clearBulkButtons();
resetBeforeAuthCompleteCallbacks();
clearPopupMenuOptions();
clearAdditionalAdminSidebarSectionLinks();
}
function cleanupCssGeneratorTags() {

View File

@ -7,7 +7,11 @@ in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.22.0] - 2024-01-03
## [1.24.0] - 2024-01-08
- Added `addAdminSidebarSectionLink` which is used to add a link to a specific admin sidebar section, as a replacement for the `admin-menu` plugin outlet. This only has an effect if the `admin_sidebar_enabled_groups` site setting is in use, which enables the new admin nav sidebar.
## [1.23.0] - 2024-01-03
### Added