From 4e7a75a7ece3205ce9f3f188b5e016bf75a869c0 Mon Sep 17 00:00:00 2001 From: Martin Brennan Date: Wed, 13 Mar 2024 13:15:12 +1000 Subject: [PATCH] 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 --- .../components/admin-plugin-config-area.gjs | 42 +++++++++++++ .../components/admin-plugin-config-page.gjs | 57 +++++++++++++++++ .../admin/addon/controllers/admin-plugins.js | 16 ++--- .../admin/addon/models/admin-plugin.js | 1 + .../admin/addon/routes/admin-plugins-show.js | 16 +++++ .../admin/addon/routes/admin-route-map.js | 3 + .../addon/templates/plugins-show-settings.hbs | 1 + .../admin/addon/templates/plugins-show.hbs | 3 + .../admin/addon/templates/plugins.hbs | 10 ++- .../discourse/app/components/back-button.gjs | 10 +++ .../discourse/app/components/nav-item.gjs | 17 ++++- .../app/lib/sidebar/admin-sidebar.js | 33 ++++++++-- .../admin-plugin-config-area-test.js | 39 ++++++++++++ .../stylesheets/common/admin/plugins.scss | 16 +++++ .../common/components/buttons.scss | 3 + app/controllers/admin/plugins_controller.rb | 12 ++++ app/serializers/admin_plugin_serializer.rb | 14 ++++- config/locales/client.en.yml | 1 + config/routes.rb | 2 + lib/plugin/instance.rb | 22 ++++++- plugins/chat/plugin.rb | 10 +++ plugins/chat/spec/plugin_spec.rb | 18 ++++++ .../requests/application_controller_spec.rb | 2 +- .../assets/stylesheets/details.scss | 9 ++- spec/lib/plugin/instance_spec.rb | 26 ++++++++ .../requests/admin/plugins_controller_spec.rb | 62 +++++++++++++++++++ 26 files changed, 425 insertions(+), 20 deletions(-) create mode 100644 app/assets/javascripts/admin/addon/components/admin-plugin-config-area.gjs create mode 100644 app/assets/javascripts/admin/addon/components/admin-plugin-config-page.gjs create mode 100644 app/assets/javascripts/admin/addon/routes/admin-plugins-show.js create mode 100644 app/assets/javascripts/admin/addon/templates/plugins-show-settings.hbs create mode 100644 app/assets/javascripts/admin/addon/templates/plugins-show.hbs create mode 100644 app/assets/javascripts/discourse/app/components/back-button.gjs create mode 100644 app/assets/javascripts/discourse/tests/integration/components/admin-plugin-config-area-test.js diff --git a/app/assets/javascripts/admin/addon/components/admin-plugin-config-area.gjs b/app/assets/javascripts/admin/addon/components/admin-plugin-config-area.gjs new file mode 100644 index 00000000000..9dc95d727b3 --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/admin-plugin-config-area.gjs @@ -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; + } + } + + +} diff --git a/app/assets/javascripts/admin/addon/components/admin-plugin-config-page.gjs b/app/assets/javascripts/admin/addon/components/admin-plugin-config-page.gjs new file mode 100644 index 00000000000..104fe787a5d --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/admin-plugin-config-page.gjs @@ -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(" "); + } + + +} diff --git a/app/assets/javascripts/admin/addon/controllers/admin-plugins.js b/app/assets/javascripts/admin/addon/controllers/admin-plugins.js index bba71038e87..033f101ca18 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-plugins.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-plugins.js @@ -5,15 +5,11 @@ export default class AdminPluginsController extends Controller { @service router; get adminRoutes() { - return this.allAdminRoutes.filter((route) => - this.routeExists(route.full_location) - ); + return this.allAdminRoutes.filter((route) => this.routeExists(route)); } get brokenAdminRoutes() { - return this.allAdminRoutes.filter( - (route) => !this.routeExists(route.full_location) - ); + return this.allAdminRoutes.filter((route) => !this.routeExists(route)); } get allAdminRoutes() { @@ -25,9 +21,13 @@ export default class AdminPluginsController extends Controller { .filter(Boolean); } - routeExists(routeName) { + routeExists(route) { 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; } catch (e) { return false; diff --git a/app/assets/javascripts/admin/addon/models/admin-plugin.js b/app/assets/javascripts/admin/addon/models/admin-plugin.js index 0fdf16b9460..566f79608e7 100644 --- a/app/assets/javascripts/admin/addon/models/admin-plugin.js +++ b/app/assets/javascripts/admin/addon/models/admin-plugin.js @@ -26,6 +26,7 @@ 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() { diff --git a/app/assets/javascripts/admin/addon/routes/admin-plugins-show.js b/app/assets/javascripts/admin/addon/routes/admin-plugins-show.js new file mode 100644 index 00000000000..be2e123e7a6 --- /dev/null +++ b/app/assets/javascripts/admin/addon/routes/admin-plugins-show.js @@ -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); + }); + } +} diff --git a/app/assets/javascripts/admin/addon/routes/admin-route-map.js b/app/assets/javascripts/admin/addon/routes/admin-route-map.js index 7b871c260fb..0b72e3173ac 100644 --- a/app/assets/javascripts/admin/addon/routes/admin-route-map.js +++ b/app/assets/javascripts/admin/addon/routes/admin-route-map.js @@ -218,6 +218,9 @@ export default function () { { path: "/plugins", resetNamespace: true }, function () { this.route("index", { path: "/" }); + this.route("show", { path: "/:plugin_id" }, function () { + this.route("settings"); + }); } ); }); diff --git a/app/assets/javascripts/admin/addon/templates/plugins-show-settings.hbs b/app/assets/javascripts/admin/addon/templates/plugins-show-settings.hbs new file mode 100644 index 00000000000..196a7a9bb24 --- /dev/null +++ b/app/assets/javascripts/admin/addon/templates/plugins-show-settings.hbs @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/app/assets/javascripts/admin/addon/templates/plugins-show.hbs b/app/assets/javascripts/admin/addon/templates/plugins-show.hbs new file mode 100644 index 00000000000..f679f36873d --- /dev/null +++ b/app/assets/javascripts/admin/addon/templates/plugins-show.hbs @@ -0,0 +1,3 @@ + + {{outlet}} + \ No newline at end of file diff --git a/app/assets/javascripts/admin/addon/templates/plugins.hbs b/app/assets/javascripts/admin/addon/templates/plugins.hbs index cd98a9c94ed..50472ef7aab 100644 --- a/app/assets/javascripts/admin/addon/templates/plugins.hbs +++ b/app/assets/javascripts/admin/addon/templates/plugins.hbs @@ -2,7 +2,15 @@ {{#each this.adminRoutes as |route|}} - + {{#if route.use_new_show_route}} + + {{else}} + + {{/if}} {{/each}} diff --git a/app/assets/javascripts/discourse/app/components/back-button.gjs b/app/assets/javascripts/discourse/app/components/back-button.gjs new file mode 100644 index 00000000000..6372b8f0ce1 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/back-button.gjs @@ -0,0 +1,10 @@ +import { LinkTo } from "@ember/routing"; +import dIcon from "discourse-common/helpers/d-icon"; +import i18n from "discourse-common/helpers/i18n"; + + diff --git a/app/assets/javascripts/discourse/app/components/nav-item.gjs b/app/assets/javascripts/discourse/app/components/nav-item.gjs index 9bbad268b2e..c8ee92aff28 100644 --- a/app/assets/javascripts/discourse/app/components/nav-item.gjs +++ b/app/assets/javascripts/discourse/app/components/nav-item.gjs @@ -24,7 +24,21 @@ export default class NavItem extends Component { 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; } @@ -37,6 +51,7 @@ export default class NavItem extends Component { {{this.contents}} {{else if @route}} {{this.contents}} diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/admin-sidebar.js b/app/assets/javascripts/discourse/app/lib/sidebar/admin-sidebar.js index d61564e8cdd..04bac79a8c4 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/admin-sidebar.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/admin-sidebar.js @@ -16,8 +16,9 @@ export function clearAdditionalAdminSidebarSectionLinks() { } class SidebarAdminSectionLink extends BaseCustomSidebarSectionLink { - constructor({ adminSidebarNavLink }) { + constructor({ adminSidebarNavLink, router }) { super(...arguments); + this.router = router; this.adminSidebarNavLink = adminSidebarNavLink; } @@ -62,9 +63,26 @@ class SidebarAdminSectionLink extends BaseCustomSidebarSectionLink { get title() { 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 { constructor() { super(...arguments); @@ -95,6 +113,7 @@ function defineAdminSection(adminNavSectionData) { (sectionLinkData) => new SidebarAdminSectionLink({ adminSidebarNavLink: sectionLinkData, + router, }) ); } @@ -183,7 +202,12 @@ function pluginAdminRouteLinks() { (pluginAdminRoute) => { return { 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, icon: "cog", }; @@ -203,6 +227,7 @@ export default class AdminSidebarPanel extends BaseCustomSidebarPanel { const siteSettings = getOwnerWithFallback(this).lookup( "service:site-settings" ); + const router = getOwnerWithFallback(this).lookup("service:router"); const session = getOwnerWithFallback(this).lookup("service:session"); if (!currentUser.use_admin_sidebar) { return []; @@ -231,7 +256,7 @@ export default class AdminSidebarPanel extends BaseCustomSidebarPanel { const navConfig = useAdminNavConfig(navMap); return navConfig.map((adminNavSectionData) => { - return defineAdminSection(adminNavSectionData); + return defineAdminSection(adminNavSectionData, router); }); } diff --git a/app/assets/javascripts/discourse/tests/integration/components/admin-plugin-config-area-test.js b/app/assets/javascripts/discourse/tests/integration/components/admin-plugin-config-area-test.js new file mode 100644 index 00000000000..a52b472cbdf --- /dev/null +++ b/app/assets/javascripts/discourse/tests/integration/components/admin-plugin-config-area-test.js @@ -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` + + Test content + + `); + + 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" + ); + }); +}); diff --git a/app/assets/stylesheets/common/admin/plugins.scss b/app/assets/stylesheets/common/admin/plugins.scss index 559d901077c..6e1fbeec107 100644 --- a/app/assets/stylesheets/common/admin/plugins.scss +++ b/app/assets/stylesheets/common/admin/plugins.scss @@ -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%; + } + } + } +} diff --git a/app/assets/stylesheets/common/components/buttons.scss b/app/assets/stylesheets/common/components/buttons.scss index 28da0c477eb..48f9649e1e5 100644 --- a/app/assets/stylesheets/common/components/buttons.scss +++ b/app/assets/stylesheets/common/components/buttons.scss @@ -374,6 +374,9 @@ color: var(--primary); } } + &.back-button { + margin-bottom: 1em; + } } .btn-link { diff --git a/app/controllers/admin/plugins_controller.rb b/app/controllers/admin/plugins_controller.rb index b54ae21887c..215331f3199 100644 --- a/app/controllers/admin/plugins_controller.rb +++ b/app/controllers/admin/plugins_controller.rb @@ -8,4 +8,16 @@ class Admin::PluginsController < Admin::StaffController root: "plugins", ) 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 diff --git a/app/serializers/admin_plugin_serializer.rb b/app/serializers/admin_plugin_serializer.rb index 9b0e488ea6e..743a1b0dcc9 100644 --- a/app/serializers/admin_plugin_serializer.rb +++ b/app/serializers/admin_plugin_serializer.rb @@ -16,7 +16,12 @@ class AdminPluginSerializer < ApplicationSerializer :commit_hash, :commit_url, :meta_url, - :authors + :authors, + :admin_config_nav_routes + + def admin_config_nav_routes + object.admin_config_nav_routes + end def id object.directory_name @@ -67,7 +72,12 @@ class AdminPluginSerializer < ApplicationSerializer return unless route 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 end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index f58559167b5..436e5f55d59 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -213,6 +213,7 @@ en: dismiss: "Dismiss" bootstrap_mode: "Getting started" + back_button: "Back" themes: default_description: "Default" diff --git a/config/routes.rb b/config/routes.rb index 82d11d5382b..11bd8b70bfd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -110,6 +110,8 @@ Discourse::Application.routes.draw do get "" => "admin#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 collection { get "category/:id" => "site_settings#index" } diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index e9f8190331e..a9ef6e128fc 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -45,6 +45,7 @@ end class Plugin::Instance attr_accessor :path, :metadata attr_reader :admin_route + attr_reader :admin_config_nav_routes # Memoized array readers %i[ @@ -105,8 +106,25 @@ class Plugin::Instance Middleware::AnonymousCache.compile_key_builder end - def add_admin_route(label, location) - @admin_route = { label: label, location: location } + def add_admin_route(label, location, opts = {}) + @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 def configurable? diff --git a/plugins/chat/plugin.rb b/plugins/chat/plugin.rb index e63c37ffa63..1e26ecc3575 100644 --- a/plugins/chat/plugin.rb +++ b/plugins/chat/plugin.rb @@ -132,6 +132,16 @@ after_initialize do 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 return false if !SiteSetting.chat_enabled return false if scope.user.blank? diff --git a/plugins/chat/spec/plugin_spec.rb b/plugins/chat/spec/plugin_spec.rb index 74df880676f..0ea5f678aeb 100644 --- a/plugins/chat/spec/plugin_spec.rb +++ b/plugins/chat/spec/plugin_spec.rb @@ -125,6 +125,24 @@ describe Chat do 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 fab!(:chat_channel) { Fabricate(:category_channel) } fab!(:user) diff --git a/plugins/chat/spec/requests/application_controller_spec.rb b/plugins/chat/spec/requests/application_controller_spec.rb index 3313cf26b2f..4ee8442b6f5 100644 --- a/plugins/chat/spec/requests/application_controller_spec.rb +++ b/plugins/chat/spec/requests/application_controller_spec.rb @@ -22,7 +22,7 @@ RSpec.describe ApplicationController do sign_in(admin) get "/latest" 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 diff --git a/plugins/discourse-details/assets/stylesheets/details.scss b/plugins/discourse-details/assets/stylesheets/details.scss index 83e4e3f3c79..741464acc55 100644 --- a/plugins/discourse-details/assets/stylesheets/details.scss +++ b/plugins/discourse-details/assets/stylesheets/details.scss @@ -2,7 +2,8 @@ details { position: relative; .topic-body .cooked &, - .d-editor-preview & { + .d-editor-preview, + &.details__boxed { background-color: var(--primary-very-low); padding: 0.25rem 0.75rem; margin-bottom: 0.5rem; @@ -23,6 +24,12 @@ details { } } +details.details__boxed { + summary { + font-weight: bold; + } +} + details > *, details .lightbox-wrapper { display: none; diff --git a/spec/lib/plugin/instance_spec.rb b/spec/lib/plugin/instance_spec.rb index 60485e36d60..63d759939e7 100644 --- a/spec/lib/plugin/instance_spec.rb +++ b/spec/lib/plugin/instance_spec.rb @@ -999,4 +999,30 @@ TEXT expect(sum).to eq(3) 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 diff --git a/spec/requests/admin/plugins_controller_spec.rb b/spec/requests/admin/plugins_controller_spec.rb index ebe1af53ade..e5edfdc8263 100644 --- a/spec/requests/admin/plugins_controller_spec.rb +++ b/spec/requests/admin/plugins_controller_spec.rb @@ -39,4 +39,66 @@ RSpec.describe Admin::PluginsController do 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