mirror of
https://github.com/discourse/discourse.git
synced 2024-11-25 09:42:07 +08:00
DEV: Single admin plugin page for consistent admin plugin UX (#26024)
This commit adds new plugin show routes (`/admin/plugins/:plugin_id`) as we move towards every plugin having a consistent UI/landing page. As part of this, we are introducing a consistent way for plugins to show an inner sidebar in their config page, via a new plugin API `register_admin_config_nav_routes` This accepts an array of links with a label/text, and an ember route. Once this commit is merged we can start the process of conforming other plugins to follow this pattern, as well as supporting a single-page version of this for simpler plugins that don't require an inner sidebar. Part of /t/122841 internally
This commit is contained in:
parent
0b41b236d7
commit
4e7a75a7ec
|
@ -0,0 +1,42 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { LinkTo } from "@ember/routing";
|
||||||
|
import concatClass from "discourse/helpers/concat-class";
|
||||||
|
import I18n from "discourse-i18n";
|
||||||
|
|
||||||
|
export default class AdminPluginConfigArea extends Component {
|
||||||
|
linkText(navLink) {
|
||||||
|
if (navLink.label) {
|
||||||
|
return I18n.t(navLink.label);
|
||||||
|
} else {
|
||||||
|
return navLink.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
{{#if @innerSidebarNavLinks}}
|
||||||
|
<nav class="admin-nav admin-plugin-inner-sidebar-nav pull-left">
|
||||||
|
<ul class="nav nav-stacked">
|
||||||
|
{{#each @innerSidebarNavLinks as |navLink|}}
|
||||||
|
<li
|
||||||
|
class={{concatClass
|
||||||
|
"admin-plugin-inner-sidebar-nav__item"
|
||||||
|
navLink.route
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LinkTo
|
||||||
|
@route={{navLink.route}}
|
||||||
|
@model={{navLink.model}}
|
||||||
|
title={{this.linkText navLink}}
|
||||||
|
>
|
||||||
|
{{this.linkText navLink}}
|
||||||
|
</LinkTo>
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{{/if}}
|
||||||
|
<section class="admin-plugin-config-area">
|
||||||
|
{{yield}}
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
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 || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
get mainAreaClasses() {
|
||||||
|
let classes = ["admin-plugin-config-page__main-area"];
|
||||||
|
|
||||||
|
if (this.configNavRoutes.length) {
|
||||||
|
classes.push("-with-inner-sidebar");
|
||||||
|
} else {
|
||||||
|
classes.push("-without-inner-sidebar");
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
<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}}
|
||||||
|
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="admin-plugin-config-page__content">
|
||||||
|
<div class={{this.mainAreaClasses}}>
|
||||||
|
<AdminPluginConfigArea
|
||||||
|
@innerSidebarNavLinks={{@plugin.configNavRoutes}}
|
||||||
|
>
|
||||||
|
{{yield}}
|
||||||
|
</AdminPluginConfigArea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
}
|
|
@ -5,15 +5,11 @@ export default class AdminPluginsController extends Controller {
|
||||||
@service router;
|
@service router;
|
||||||
|
|
||||||
get adminRoutes() {
|
get adminRoutes() {
|
||||||
return this.allAdminRoutes.filter((route) =>
|
return this.allAdminRoutes.filter((route) => this.routeExists(route));
|
||||||
this.routeExists(route.full_location)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get brokenAdminRoutes() {
|
get brokenAdminRoutes() {
|
||||||
return this.allAdminRoutes.filter(
|
return this.allAdminRoutes.filter((route) => !this.routeExists(route));
|
||||||
(route) => !this.routeExists(route.full_location)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get allAdminRoutes() {
|
get allAdminRoutes() {
|
||||||
|
@ -25,9 +21,13 @@ export default class AdminPluginsController extends Controller {
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
routeExists(routeName) {
|
routeExists(route) {
|
||||||
try {
|
try {
|
||||||
this.router.urlFor(routeName);
|
if (route.use_new_show_route) {
|
||||||
|
this.router.urlFor(route.full_location, route.location);
|
||||||
|
} else {
|
||||||
|
this.router.urlFor(route.full_location);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -26,6 +26,7 @@ export default class AdminPlugin {
|
||||||
this.version = args.version;
|
this.version = args.version;
|
||||||
this.metaUrl = args.meta_url;
|
this.metaUrl = args.meta_url;
|
||||||
this.authors = args.authors;
|
this.authors = args.authors;
|
||||||
|
this.configNavRoutes = args.admin_config_nav_routes;
|
||||||
}
|
}
|
||||||
|
|
||||||
get snakeCaseName() {
|
get snakeCaseName() {
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import Route from "@ember/routing/route";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
import { sanitize } from "discourse/lib/text";
|
||||||
|
import AdminPlugin from "admin/models/admin-plugin";
|
||||||
|
|
||||||
|
export default class AdminPluginsShowRoute extends Route {
|
||||||
|
@service router;
|
||||||
|
|
||||||
|
model(params) {
|
||||||
|
const pluginId = sanitize(params.plugin_id).substring(0, 100);
|
||||||
|
return ajax(`/admin/plugins/${pluginId}.json`).then((plugin) => {
|
||||||
|
return AdminPlugin.create(plugin);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -218,6 +218,9 @@ export default function () {
|
||||||
{ path: "/plugins", resetNamespace: true },
|
{ path: "/plugins", resetNamespace: true },
|
||||||
function () {
|
function () {
|
||||||
this.route("index", { path: "/" });
|
this.route("index", { path: "/" });
|
||||||
|
this.route("show", { path: "/:plugin_id" }, function () {
|
||||||
|
this.route("settings");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<div class="content-body admin-plugin-config-area__settings"></div>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<AdminPluginConfigPage @plugin={{this.model}}>
|
||||||
|
{{outlet}}
|
||||||
|
</AdminPluginConfigPage>
|
|
@ -2,7 +2,15 @@
|
||||||
<HorizontalOverflowNav class="main-nav nav plugin-nav">
|
<HorizontalOverflowNav class="main-nav nav plugin-nav">
|
||||||
<NavItem @route="adminPlugins.index" @label="admin.plugins.title" />
|
<NavItem @route="adminPlugins.index" @label="admin.plugins.title" />
|
||||||
{{#each this.adminRoutes as |route|}}
|
{{#each this.adminRoutes as |route|}}
|
||||||
<NavItem @route={{route.full_location}} @label={{route.label}} />
|
{{#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}}
|
{{/each}}
|
||||||
</HorizontalOverflowNav>
|
</HorizontalOverflowNav>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { LinkTo } from "@ember/routing";
|
||||||
|
import dIcon from "discourse-common/helpers/d-icon";
|
||||||
|
import i18n from "discourse-common/helpers/i18n";
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<LinkTo class="btn btn-flat back-button" @route={{@route}}>
|
||||||
|
{{dIcon "chevron-left"}}
|
||||||
|
{{i18n "back_button"}}
|
||||||
|
</LinkTo>
|
||||||
|
</template>
|
|
@ -24,7 +24,21 @@ export default class NavItem extends Component {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.args.routeParam && this.router.currentRoute) {
|
// This is needed because the setting route is underneath /admin/plugins/:plugin_id,
|
||||||
|
// but is not a child route of the plugin routes themselves. E.g. discourse-ai
|
||||||
|
// for the plugin ID has its own nested routes defined in the plugin.
|
||||||
|
if (this.router.currentRoute.name === "adminPlugins.show.settings") {
|
||||||
|
return (
|
||||||
|
this.router.currentRoute.parent.params.plugin_id ===
|
||||||
|
this.args.routeParam
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.args.routeParam &&
|
||||||
|
this.router.currentRoute &&
|
||||||
|
this.router.currentRoute.params.filter
|
||||||
|
) {
|
||||||
return this.router.currentRoute.params.filter === this.args.routeParam;
|
return this.router.currentRoute.params.filter === this.args.routeParam;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,6 +51,7 @@ export default class NavItem extends Component {
|
||||||
<LinkTo
|
<LinkTo
|
||||||
@route={{@route}}
|
@route={{@route}}
|
||||||
@model={{@routeParam}}
|
@model={{@routeParam}}
|
||||||
|
@current-when={{this.active}}
|
||||||
>{{this.contents}}</LinkTo>
|
>{{this.contents}}</LinkTo>
|
||||||
{{else if @route}}
|
{{else if @route}}
|
||||||
<LinkTo @route={{@route}}>{{this.contents}}</LinkTo>
|
<LinkTo @route={{@route}}>{{this.contents}}</LinkTo>
|
||||||
|
|
|
@ -16,8 +16,9 @@ export function clearAdditionalAdminSidebarSectionLinks() {
|
||||||
}
|
}
|
||||||
|
|
||||||
class SidebarAdminSectionLink extends BaseCustomSidebarSectionLink {
|
class SidebarAdminSectionLink extends BaseCustomSidebarSectionLink {
|
||||||
constructor({ adminSidebarNavLink }) {
|
constructor({ adminSidebarNavLink, router }) {
|
||||||
super(...arguments);
|
super(...arguments);
|
||||||
|
this.router = router;
|
||||||
this.adminSidebarNavLink = adminSidebarNavLink;
|
this.adminSidebarNavLink = adminSidebarNavLink;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,9 +63,26 @@ class SidebarAdminSectionLink extends BaseCustomSidebarSectionLink {
|
||||||
get title() {
|
get title() {
|
||||||
return this.adminSidebarNavLink.text;
|
return this.adminSidebarNavLink.text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get currentWhen() {
|
||||||
|
// This is needed because the setting route is underneath /admin/plugins/:plugin_id,
|
||||||
|
// but is not a child route of the plugin routes themselves. E.g. discourse-ai
|
||||||
|
// for the plugin ID has its own nested routes defined in the plugin.
|
||||||
|
if (this.router.currentRoute.name === "adminPlugins.show.settings") {
|
||||||
|
if (
|
||||||
|
this.adminSidebarNavLink.route?.includes(
|
||||||
|
this.router.currentRoute.parent.params.plugin_id
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return this.router.currentRoute.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.adminSidebarNavLink.route;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function defineAdminSection(adminNavSectionData) {
|
function defineAdminSection(adminNavSectionData, router) {
|
||||||
const AdminNavSection = class extends BaseCustomSidebarSection {
|
const AdminNavSection = class extends BaseCustomSidebarSection {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(...arguments);
|
super(...arguments);
|
||||||
|
@ -95,6 +113,7 @@ function defineAdminSection(adminNavSectionData) {
|
||||||
(sectionLinkData) =>
|
(sectionLinkData) =>
|
||||||
new SidebarAdminSectionLink({
|
new SidebarAdminSectionLink({
|
||||||
adminSidebarNavLink: sectionLinkData,
|
adminSidebarNavLink: sectionLinkData,
|
||||||
|
router,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -183,7 +202,12 @@ function pluginAdminRouteLinks() {
|
||||||
(pluginAdminRoute) => {
|
(pluginAdminRoute) => {
|
||||||
return {
|
return {
|
||||||
name: `admin_plugin_${pluginAdminRoute.location}`,
|
name: `admin_plugin_${pluginAdminRoute.location}`,
|
||||||
route: `adminPlugins.${pluginAdminRoute.location}`,
|
route: pluginAdminRoute.use_new_show_route
|
||||||
|
? `adminPlugins.show.${pluginAdminRoute.location}`
|
||||||
|
: `adminPlugins.${pluginAdminRoute.location}`,
|
||||||
|
routeModels: pluginAdminRoute.use_new_show_route
|
||||||
|
? [pluginAdminRoute.location]
|
||||||
|
: [],
|
||||||
label: pluginAdminRoute.label,
|
label: pluginAdminRoute.label,
|
||||||
icon: "cog",
|
icon: "cog",
|
||||||
};
|
};
|
||||||
|
@ -203,6 +227,7 @@ export default class AdminSidebarPanel extends BaseCustomSidebarPanel {
|
||||||
const siteSettings = getOwnerWithFallback(this).lookup(
|
const siteSettings = getOwnerWithFallback(this).lookup(
|
||||||
"service:site-settings"
|
"service:site-settings"
|
||||||
);
|
);
|
||||||
|
const router = getOwnerWithFallback(this).lookup("service:router");
|
||||||
const session = getOwnerWithFallback(this).lookup("service:session");
|
const session = getOwnerWithFallback(this).lookup("service:session");
|
||||||
if (!currentUser.use_admin_sidebar) {
|
if (!currentUser.use_admin_sidebar) {
|
||||||
return [];
|
return [];
|
||||||
|
@ -231,7 +256,7 @@ export default class AdminSidebarPanel extends BaseCustomSidebarPanel {
|
||||||
const navConfig = useAdminNavConfig(navMap);
|
const navConfig = useAdminNavConfig(navMap);
|
||||||
|
|
||||||
return navConfig.map((adminNavSectionData) => {
|
return navConfig.map((adminNavSectionData) => {
|
||||||
return defineAdminSection(adminNavSectionData);
|
return defineAdminSection(adminNavSectionData, router);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { render } from "@ember/test-helpers";
|
||||||
|
import { hbs } from "ember-cli-htmlbars";
|
||||||
|
import { module, test } from "qunit";
|
||||||
|
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||||
|
|
||||||
|
module("Integration | Component | admin-plugin-config-area", function (hooks) {
|
||||||
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
|
test("it renders the plugin config nav and content", async function (assert) {
|
||||||
|
this.set("innerSidebarNavLinks", [
|
||||||
|
{
|
||||||
|
route: "adminPlugins.show.discourse-test-plugin.one",
|
||||||
|
label: "admin.title",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: "adminPlugins.show.discourse-test-plugin.two",
|
||||||
|
label: "admin.back_to_forum",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await render(hbs`
|
||||||
|
<AdminPluginConfigArea @innerSidebarNavLinks={{this.innerSidebarNavLinks}}>
|
||||||
|
Test content
|
||||||
|
</AdminPluginConfigArea>
|
||||||
|
`);
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
document.querySelectorAll(".admin-plugin-inner-sidebar-nav__item").length,
|
||||||
|
2,
|
||||||
|
"it renders the correct number of nav items"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
document.querySelector(".admin-plugin-config-area").textContent.trim(),
|
||||||
|
"Test content",
|
||||||
|
"it renders the yielded content"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -78,3 +78,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-plugin-config-page {
|
||||||
|
&__main-area {
|
||||||
|
.admin-detail {
|
||||||
|
padding-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-without-inner-sidebar {
|
||||||
|
.admin-detail {
|
||||||
|
border-left: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -374,6 +374,9 @@
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&.back-button {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-link {
|
.btn-link {
|
||||||
|
|
|
@ -8,4 +8,16 @@ class Admin::PluginsController < Admin::StaffController
|
||||||
root: "plugins",
|
root: "plugins",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
plugin = Discourse.plugins_by_name[params[:plugin_id]]
|
||||||
|
|
||||||
|
# An escape hatch in case a plugin is using an un-prefixed
|
||||||
|
# version of their plugin name for a route.
|
||||||
|
plugin = Discourse.plugins_by_name["discourse-#{params[:plugin_id]}"] if !plugin
|
||||||
|
|
||||||
|
raise Discourse::NotFound if !plugin
|
||||||
|
|
||||||
|
render_serialized(plugin, AdminPluginSerializer, root: nil)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,7 +16,12 @@ class AdminPluginSerializer < ApplicationSerializer
|
||||||
:commit_hash,
|
:commit_hash,
|
||||||
:commit_url,
|
:commit_url,
|
||||||
:meta_url,
|
:meta_url,
|
||||||
:authors
|
:authors,
|
||||||
|
:admin_config_nav_routes
|
||||||
|
|
||||||
|
def admin_config_nav_routes
|
||||||
|
object.admin_config_nav_routes
|
||||||
|
end
|
||||||
|
|
||||||
def id
|
def id
|
||||||
object.directory_name
|
object.directory_name
|
||||||
|
@ -67,7 +72,12 @@ class AdminPluginSerializer < ApplicationSerializer
|
||||||
return unless route
|
return unless route
|
||||||
|
|
||||||
ret = route.slice(:location, :label)
|
ret = route.slice(:location, :label)
|
||||||
ret[:full_location] = "adminPlugins.#{ret[:location]}"
|
if route[:use_new_show_route]
|
||||||
|
ret[:full_location] = "adminPlugins.show.#{ret[:location]}"
|
||||||
|
ret[:use_new_show_route] = true
|
||||||
|
else
|
||||||
|
ret[:full_location] = "adminPlugins.#{ret[:location]}"
|
||||||
|
end
|
||||||
ret
|
ret
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -213,6 +213,7 @@ en:
|
||||||
dismiss: "Dismiss"
|
dismiss: "Dismiss"
|
||||||
|
|
||||||
bootstrap_mode: "Getting started"
|
bootstrap_mode: "Getting started"
|
||||||
|
back_button: "Back"
|
||||||
|
|
||||||
themes:
|
themes:
|
||||||
default_description: "Default"
|
default_description: "Default"
|
||||||
|
|
|
@ -110,6 +110,8 @@ Discourse::Application.routes.draw do
|
||||||
get "" => "admin#index"
|
get "" => "admin#index"
|
||||||
|
|
||||||
get "plugins" => "plugins#index"
|
get "plugins" => "plugins#index"
|
||||||
|
get "plugins/:plugin_id" => "plugins#show"
|
||||||
|
get "plugins/:plugin_id/settings" => "plugins#show"
|
||||||
|
|
||||||
resources :site_settings, only: %i[index update], constraints: AdminConstraint.new do
|
resources :site_settings, only: %i[index update], constraints: AdminConstraint.new do
|
||||||
collection { get "category/:id" => "site_settings#index" }
|
collection { get "category/:id" => "site_settings#index" }
|
||||||
|
|
|
@ -45,6 +45,7 @@ end
|
||||||
class Plugin::Instance
|
class Plugin::Instance
|
||||||
attr_accessor :path, :metadata
|
attr_accessor :path, :metadata
|
||||||
attr_reader :admin_route
|
attr_reader :admin_route
|
||||||
|
attr_reader :admin_config_nav_routes
|
||||||
|
|
||||||
# Memoized array readers
|
# Memoized array readers
|
||||||
%i[
|
%i[
|
||||||
|
@ -105,8 +106,25 @@ class Plugin::Instance
|
||||||
Middleware::AnonymousCache.compile_key_builder
|
Middleware::AnonymousCache.compile_key_builder
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_admin_route(label, location)
|
def add_admin_route(label, location, opts = {})
|
||||||
@admin_route = { label: label, location: location }
|
@admin_route = {
|
||||||
|
label: label,
|
||||||
|
location: location,
|
||||||
|
use_new_show_route: opts.fetch(:use_new_show_route, false),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def register_admin_config_nav_routes(plugin_id, nav)
|
||||||
|
@admin_config_nav_routes =
|
||||||
|
nav.each do |n|
|
||||||
|
if !n.key?(:label) || !n.key?(:route)
|
||||||
|
raise ArgumentError.new(
|
||||||
|
"Admin config nav routes must have a `route` value that matches an Ember route and a `label` value that matches a client I18n key",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
n[:model] = plugin_id
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def configurable?
|
def configurable?
|
||||||
|
|
|
@ -132,6 +132,16 @@ after_initialize do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
add_to_serializer(
|
||||||
|
:admin_plugin,
|
||||||
|
:incoming_chat_webhooks,
|
||||||
|
include_condition: -> { self.name == "chat" },
|
||||||
|
) { Chat::IncomingWebhook.includes(:chat_channel).all }
|
||||||
|
|
||||||
|
add_to_serializer(:admin_plugin, :chat_channels, include_condition: -> { self.name == "chat" }) do
|
||||||
|
Chat::Channel.public_channels
|
||||||
|
end
|
||||||
|
|
||||||
add_to_serializer(:user_card, :can_chat_user) do
|
add_to_serializer(:user_card, :can_chat_user) do
|
||||||
return false if !SiteSetting.chat_enabled
|
return false if !SiteSetting.chat_enabled
|
||||||
return false if scope.user.blank?
|
return false if scope.user.blank?
|
||||||
|
|
|
@ -125,6 +125,24 @@ describe Chat do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "admin plugin serializer extension" do
|
||||||
|
let(:admin) { Fabricate(:admin) }
|
||||||
|
let(:chat_plugin) do
|
||||||
|
Plugin::Instance.parse_from_source(File.join(Rails.root, "plugins", "chat", "plugin.rb"))
|
||||||
|
end
|
||||||
|
let(:serializer) { AdminPluginSerializer.new(chat_plugin, scope: admin.guardian) }
|
||||||
|
|
||||||
|
it "includes all incoming webhooks via :incoming_chat_webhooks" do
|
||||||
|
webhook = Fabricate(:incoming_chat_webhook)
|
||||||
|
expect(serializer.incoming_chat_webhooks).to contain_exactly(webhook)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "includes all chat channels via :chat_channels" do
|
||||||
|
channel = Fabricate(:chat_channel)
|
||||||
|
expect(serializer.chat_channels).to contain_exactly(channel)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "chat oneboxes" do
|
describe "chat oneboxes" do
|
||||||
fab!(:chat_channel) { Fabricate(:category_channel) }
|
fab!(:chat_channel) { Fabricate(:category_channel) }
|
||||||
fab!(:user)
|
fab!(:user)
|
||||||
|
|
|
@ -22,7 +22,7 @@ RSpec.describe ApplicationController do
|
||||||
sign_in(admin)
|
sign_in(admin)
|
||||||
get "/latest"
|
get "/latest"
|
||||||
expect(JSON.parse(preloaded_json["enabledPluginAdminRoutes"])).to include(
|
expect(JSON.parse(preloaded_json["enabledPluginAdminRoutes"])).to include(
|
||||||
{ "label" => "chat.admin.title", "location" => "chat" },
|
{ "label" => "chat.admin.title", "location" => "chat", "use_new_show_route" => false },
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,7 +2,8 @@ details {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.topic-body .cooked &,
|
.topic-body .cooked &,
|
||||||
.d-editor-preview & {
|
.d-editor-preview,
|
||||||
|
&.details__boxed {
|
||||||
background-color: var(--primary-very-low);
|
background-color: var(--primary-very-low);
|
||||||
padding: 0.25rem 0.75rem;
|
padding: 0.25rem 0.75rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
|
@ -23,6 +24,12 @@ details {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
details.details__boxed {
|
||||||
|
summary {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
details > *,
|
details > *,
|
||||||
details .lightbox-wrapper {
|
details .lightbox-wrapper {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
@ -999,4 +999,30 @@ TEXT
|
||||||
expect(sum).to eq(3)
|
expect(sum).to eq(3)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "#register_admin_config_nav_routes" do
|
||||||
|
let(:plugin) { Plugin::Instance.new }
|
||||||
|
|
||||||
|
it "adds the specified plugin id as the 'model' for the route" do
|
||||||
|
plugin.register_admin_config_nav_routes(
|
||||||
|
"discourse-awesome",
|
||||||
|
[{ route: "adminPlugins.show", label: "some.i18n.label" }],
|
||||||
|
)
|
||||||
|
expect(plugin.admin_config_nav_routes).to eq(
|
||||||
|
[{ route: "adminPlugins.show", label: "some.i18n.label", model: "discourse-awesome" }],
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "errors if the route or label is not provided" do
|
||||||
|
expect {
|
||||||
|
plugin.register_admin_config_nav_routes("discourse-awesome", [{ label: "some.i18n.label" }])
|
||||||
|
}.to raise_error(ArgumentError)
|
||||||
|
expect {
|
||||||
|
plugin.register_admin_config_nav_routes(
|
||||||
|
"discourse-awesome",
|
||||||
|
[{ route: "adminPlugins.show" }],
|
||||||
|
)
|
||||||
|
}.to raise_error(ArgumentError)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -39,4 +39,66 @@ RSpec.describe Admin::PluginsController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "#show" do
|
||||||
|
before do
|
||||||
|
spoiler_alert =
|
||||||
|
Plugin::Instance.parse_from_source(
|
||||||
|
File.join(Rails.root, "plugins", "spoiler-alert", "plugin.rb"),
|
||||||
|
)
|
||||||
|
poll =
|
||||||
|
Plugin::Instance.parse_from_source(File.join(Rails.root, "plugins", "poll", "plugin.rb"))
|
||||||
|
|
||||||
|
Discourse.stubs(:plugins_by_name).returns(
|
||||||
|
{ "discourse-spoiler-alert" => spoiler_alert, "poll" => poll },
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "while logged in as an admin" do
|
||||||
|
before { sign_in(admin) }
|
||||||
|
|
||||||
|
it "returns a plugin" do
|
||||||
|
get "/admin/plugins/poll.json"
|
||||||
|
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
expect(response.parsed_body["name"]).to eq("poll")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns a plugin with the discourse- prefix if the prefixless version is queried" do
|
||||||
|
get "/admin/plugins/spoiler-alert.json"
|
||||||
|
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
expect(response.parsed_body["name"]).to eq("spoiler-alert")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "404s if the plugin is not found" do
|
||||||
|
get "/admin/plugins/casino.json"
|
||||||
|
|
||||||
|
expect(response.status).to eq(404)
|
||||||
|
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when logged in as a moderator" do
|
||||||
|
before { sign_in(moderator) }
|
||||||
|
|
||||||
|
it "returns plugins" do
|
||||||
|
get "/admin/plugins/poll.json"
|
||||||
|
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
expect(response.parsed_body["name"]).to eq("poll")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when logged in as a non-staff user" do
|
||||||
|
before { sign_in(user) }
|
||||||
|
|
||||||
|
it "denies access with a 404 response" do
|
||||||
|
get "/admin/plugins/poll.json"
|
||||||
|
|
||||||
|
expect(response.status).to eq(404)
|
||||||
|
expect(response.parsed_body["errors"]).to include(I18n.t("not_found"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue
Block a user