FEATURE: More flexible admin plugin config nav definition (#26254)

This commit changes the API for registering the plugin config
page nav configuration from a server-side to a JS one;
there is no need for it to be server-side.

It also makes some changes to allow for 2 different ways of displaying
navigation for plugin pages, depending on complexity:

* TOP - This is the best mode for simple plugins without a lot of different
  custom configuration pages, and it reuses the grey horizontal nav bar
  already used for admins.
* SIDEBAR - This is better for more complex plugins; likely this won't
  be used in the near future, but it's readily available if needed

There is a new AdminPluginConfigNavManager service too to manage which
plugin the admin is actively viewing, otherwise we would have trouble
hiding the main plugin nav for admins when viewing a single plugin.
This commit is contained in:
Martin Brennan
2024-03-21 13:42:06 +10:00
committed by GitHub
parent e5566b8519
commit 70f7c0ee6f
17 changed files with 268 additions and 120 deletions

View File

@@ -1,9 +1,12 @@
import Component from "@glimmer/component";
import { LinkTo } from "@ember/routing";
import { inject as service } from "@ember/service";
import concatClass from "discourse/helpers/concat-class";
import I18n from "discourse-i18n";
export default class AdminPluginConfigArea extends Component {
@service adminPluginNavManager;
linkText(navLink) {
if (navLink.label) {
return I18n.t(navLink.label);
@@ -13,10 +16,13 @@ export default class AdminPluginConfigArea extends Component {
}
<template>
{{#if @innerSidebarNavLinks}}
{{#if this.adminPluginNavManager.isSidebarMode}}
<nav class="admin-nav admin-plugin-inner-sidebar-nav pull-left">
<ul class="nav nav-stacked">
{{#each @innerSidebarNavLinks as |navLink|}}
{{#each
this.adminPluginNavManager.currentConfigNav.links
as |navLink|
}}
<li
class={{concatClass
"admin-plugin-inner-sidebar-nav__item"

View File

@@ -1,19 +1,18 @@
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
import HorizontalOverflowNav from "discourse/components/horizontal-overflow-nav";
import NavItem from "discourse/components/nav-item";
import i18n from "discourse-common/helpers/i18n";
import AdminPluginConfigArea from "./admin-plugin-config-area";
export default class extends Component {
@service currentUser;
get configNavRoutes() {
return this.args.plugin.configNavRoutes || [];
}
@service adminPluginNavManager;
get mainAreaClasses() {
let classes = ["admin-plugin-config-page__main-area"];
if (this.configNavRoutes.length) {
if (this.adminPluginNavManager.isSidebarMode) {
classes.push("-with-inner-sidebar");
} else {
classes.push("-without-inner-sidebar");
@@ -22,32 +21,61 @@ export default class extends Component {
return classes.join(" ");
}
linkText(navLink) {
if (navLink.label) {
return i18n(navLink.label);
} else {
return navLink.text;
}
}
<template>
<div class="admin-plugin-config-page">
<div class="admin-plugin-config-page__metadata">
<h2>
{{@plugin.nameTitleized}}
</h2>
<p>
{{@plugin.about}}
{{#if @plugin.linkUrl}}
|
<a
href={{@plugin.linkUrl}}
rel="noopener noreferrer"
target="_blank"
>
{{i18n "admin.plugins.learn_more"}}
</a>
{{/if}}
{{#if this.adminPluginNavManager.isTopMode}}
<div class="admin-controls">
<HorizontalOverflowNav
class="nav-pills action-list main-nav nav plugin-nav"
>
{{#each
this.adminPluginNavManager.currentConfigNav.links
as |navLink|
}}
<NavItem
@route={{navLink.route}}
@i18nLabel={{this.linkText navLink}}
title={{this.linkText navLink}}
class="admin-plugin-config-page__top-nav-item"
>
{{this.linkText navLink}}
</NavItem>
{{/each}}
</HorizontalOverflowNav>
</div>
{{/if}}
</p>
<div class="admin-plugin-config-page__metadata">
<div class="admin-plugin-config-area__metadata-title">
<h2>
{{@plugin.nameTitleized}}
</h2>
<p>
{{@plugin.about}}
{{#if @plugin.linkUrl}}
|
<a
href={{@plugin.linkUrl}}
rel="noopener noreferrer"
target="_blank"
>
{{i18n "admin.plugins.learn_more"}}
</a>
{{/if}}
</p>
</div>
</div>
<div class="admin-plugin-config-page__content">
<div class={{this.mainAreaClasses}}>
<AdminPluginConfigArea
@innerSidebarNavLinks={{@plugin.configNavRoutes}}
>
<AdminPluginConfigArea>
{{yield}}
</AdminPluginConfigArea>
</div>

View File

@@ -2,6 +2,7 @@ import Controller from "@ember/controller";
import { service } from "@ember/service";
export default class AdminPluginsController extends Controller {
@service adminPluginNavManager;
@service router;
get adminRoutes() {
@@ -21,6 +22,13 @@ export default class AdminPluginsController extends Controller {
.filter(Boolean);
}
get showTopNav() {
return (
!this.adminPluginNavManager.currentPlugin ||
this.adminPluginNavManager.isSidebarMode
);
}
routeExists(route) {
try {
if (route.use_new_show_route) {

View File

@@ -26,7 +26,6 @@ export default class AdminPlugin {
this.version = args.version;
this.metaUrl = args.meta_url;
this.authors = args.authors;
this.configNavRoutes = args.admin_config_nav_routes;
}
get snakeCaseName() {

View File

@@ -6,6 +6,7 @@ import AdminPlugin from "admin/models/admin-plugin";
export default class AdminPluginsShowRoute extends Route {
@service router;
@service adminPluginNavManager;
model(params) {
const pluginId = sanitize(params.plugin_id).substring(0, 100);
@@ -13,4 +14,12 @@ export default class AdminPluginsShowRoute extends Route {
return AdminPlugin.create(plugin);
});
}
afterModel(model) {
this.adminPluginNavManager.currentPlugin = model;
}
deactivate() {
this.adminPluginNavManager.currentPlugin = null;
}
}

View File

@@ -0,0 +1,28 @@
import { tracked } from "@glimmer/tracking";
import Service, { service } from "@ember/service";
import {
configNavForPlugin,
PLUGIN_NAV_MODE_SIDEBAR,
PLUGIN_NAV_MODE_TOP,
} from "discourse/lib/admin-plugin-config-nav";
export default class AdminPluginNavManager extends Service {
@service currentUser;
@tracked currentPlugin;
get currentUserUsingAdminSidebar() {
return this.currentUser?.use_admin_sidebar;
}
get currentConfigNav() {
return configNavForPlugin(this.currentPlugin.id);
}
get isSidebarMode() {
return this.currentConfigNav.mode === PLUGIN_NAV_MODE_SIDEBAR;
}
get isTopMode() {
return this.currentConfigNav.mode === PLUGIN_NAV_MODE_TOP;
}
}

View File

@@ -1,20 +1,22 @@
{{#if this.model.length}}
<h3>{{i18n "admin.plugins.installed"}}</h3>
<AdminPluginsList @plugins={{this.model}} />
{{else}}
<p>{{i18n "admin.plugins.none_installed"}}</p>
{{/if}}
<div class="admin-plugins-list-container">
{{#if this.model.length}}
<h2>{{i18n "admin.plugins.installed"}}</h2>
<AdminPluginsList @plugins={{this.model}} />
{{else}}
<p>{{i18n "admin.plugins.none_installed"}}</p>
{{/if}}
<p class="admin-plugins-howto">
<a href="https://meta.discourse.org/t/install-a-plugin/19157">
{{i18n "admin.plugins.howto"}}
</a>
</p>
<p class="admin-plugins-howto">
<a href="https://meta.discourse.org/t/install-a-plugin/19157">
{{i18n "admin.plugins.howto"}}
</a>
</p>
<span>
<PluginOutlet
@name="admin-below-plugins-index"
@connectorTagName="div"
@outletArgs={{hash model=this.model}}
/>
</span>
<span>
<PluginOutlet
@name="admin-below-plugins-index"
@connectorTagName="div"
@outletArgs={{hash model=this.model}}
/>
</span>
</div>

View File

@@ -1,19 +1,21 @@
<div class="admin-controls">
<HorizontalOverflowNav class="main-nav nav plugin-nav">
<NavItem @route="adminPlugins.index" @label="admin.plugins.title" />
{{#each this.adminRoutes as |route|}}
{{#if route.use_new_show_route}}
<NavItem
@route={{route.full_location}}
@label={{route.label}}
@routeParam={{route.location}}
/>
{{else}}
<NavItem @route={{route.full_location}} @label={{route.label}} />
{{/if}}
{{/each}}
</HorizontalOverflowNav>
</div>
{{#if this.showTopNav}}
<div class="admin-controls">
<HorizontalOverflowNav class="main-nav nav plugin-nav">
<NavItem @route="adminPlugins.index" @label="admin.plugins.title" />
{{#each this.adminRoutes as |route|}}
{{#if route.use_new_show_route}}
<NavItem
@route={{route.full_location}}
@label={{route.label}}
@routeParam={{route.location}}
/>
{{else}}
<NavItem @route={{route.full_location}} @label={{route.label}} />
{{/if}}
{{/each}}
</HorizontalOverflowNav>
</div>
{{/if}}
<div class="admin-container">
{{#each this.brokenAdminRoutes as |route|}}