FEATURE: Admin plugin list redesign (#24363)

* Remove checkmark for official plugins
* Add author for plugin, which is By Discourse for all discourse
  and discourse-org github plugins
* Link to meta topic instead of github repo
* Add experimental flag for plugin metadata and show this as a
  badge on the plugin list if present

---------

Co-authored-by: chapoi <101828855+chapoi@users.noreply.github.com>
This commit is contained in:
Martin Brennan 2023-11-21 09:37:11 +10:00 committed by GitHub
parent 10b546d8c7
commit e37fb3042d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 320 additions and 119 deletions

View File

@ -0,0 +1,115 @@
import Component from "@glimmer/component";
import { concat, fn, hash } from "@ember/helper";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { LinkTo } from "@ember/routing";
import { inject as service } from "@ember/service";
import DToggleSwitch from "discourse/components/d-toggle-switch";
import { popupAjaxError } from "discourse/lib/ajax-error";
import icon from "discourse-common/helpers/d-icon";
import i18n from "discourse-common/helpers/i18n";
import SiteSetting from "admin/models/site-setting";
import PluginCommitHash from "./plugin-commit-hash";
export default class AdminPluginsListItem extends Component {
@service session;
@service currentUser;
@action
async togglePluginEnabled(plugin) {
const oldValue = plugin.enabled;
const newValue = !oldValue;
try {
plugin.enabled = newValue;
await SiteSetting.update(plugin.enabledSetting, newValue);
this.session.requiresRefresh = true;
} catch (err) {
plugin.enabled = oldValue;
popupAjaxError(err);
}
}
<template>
<tr data-plugin-name={{@plugin.name}}>
<td class="admin-plugins-list__row">
<div class="admin-plugins-list__name-with-badges">
<div class="admin-plugins-list__name">
{{#if @plugin.linkUrl}}
<a
href={{@plugin.linkUrl}}
rel="noopener noreferrer"
target="_blank"
>{{@plugin.nameTitleized}}</a>
{{else}}
{{@plugin.nameTitleized}}
{{/if}}
</div>
<div class="badges">
{{#if @plugin.isExperimental}}
<span
class="admin-plugins-list__badge -experimental"
title={{i18n "admin.plugins.experimental"}}
>
{{i18n "admin.plugins.experimental_badge"}}
</span>
{{/if}}
</div>
</div>
<div class="admin-plugins-list__author">
{{@plugin.author}}
{{#if @plugin.isOfficial}}
<span title={{i18n "admin.plugins.official"}}>{{icon
"fab-discourse"
}}</span>
{{/if}}
</div>
<div class="admin-plugins-list__about">
{{@plugin.about}}
{{#if @plugin.linkUrl}}
<a
href={{@plugin.linkUrl}}
rel="noopener noreferrer"
target="_blank"
>
{{i18n "learn_more"}}
</a>
{{/if}}
</div>
</td>
<td class="admin-plugins-list__version">
<div class="label">{{i18n "admin.plugins.version"}}</div>
{{@plugin.version}}<br />
<PluginCommitHash @plugin={{@plugin}} />
</td>
<td class="admin-plugins-list__enabled">
<div class="label">{{i18n "admin.plugins.enabled"}}</div>
{{#if @plugin.enabledSetting}}
<DToggleSwitch
@state={{@plugin.enabled}}
{{on "click" (fn this.togglePluginEnabled @plugin)}}
/>
{{else}}
<DToggleSwitch @state={{@plugin.enabled}} disabled={{true}} />
{{/if}}
</td>
<td class="admin-plugins-list__settings">
{{#if this.currentUser.admin}}
{{#if @plugin.hasSettings}}
<LinkTo
class="btn-default btn btn-icon-text"
@route="adminSiteSettingsCategory"
@model={{@plugin.settingCategoryName}}
@query={{hash filter=(concat "plugin:" @plugin.name)}}
data-plugin-setting-button={{@plugin.name}}
>
{{icon "cog"}}
{{i18n "admin.plugins.change_settings_short"}}
</LinkTo>
{{/if}}
{{/if}}
</td>
</tr>
</template>
}

View File

@ -0,0 +1,23 @@
import Component from "@glimmer/component";
import i18n from "discourse-common/helpers/i18n";
import AdminPluginsListItem from "./admin-plugins-list-item";
export default class AdminPluginsList extends Component {
<template>
<table class="admin-plugins-list grid">
<thead>
<tr>
<th>{{i18n "admin.plugins.name"}}</th>
<th>{{i18n "admin.plugins.version"}}</th>
<th>{{i18n "admin.plugins.enabled"}}</th>
<th></th>
</tr>
</thead>
<tbody>
{{#each @plugins as |plugin|}}
<AdminPluginsListItem @plugin={{plugin}} />
{{/each}}
</tbody>
</table>
</template>
}

View File

@ -1,4 +1,5 @@
import { tracked } from "@glimmer/tracking";
import { capitalize } from "@ember/string";
import I18n from "discourse-i18n";
export default class AdminPlugin {
@ -18,10 +19,13 @@ export default class AdminPlugin {
this.hasSettings = args.has_settings;
this.id = args.id;
this.isOfficial = args.is_official;
this.isDiscourseOwned = args.is_discourse_owned;
this.isExperimental = args.is_experimental;
this.name = args.name;
this.url = args.url;
this.version = args.version;
this.metaUrl = args.meta_url;
this.authors = args.authors;
}
get settingCategoryName() {
@ -44,4 +48,25 @@ export default class AdminPlugin {
return "plugins";
}
get nameTitleized() {
return this.name
.split("-")
.map((word) => {
return capitalize(word);
})
.join(" ");
}
get author() {
if (this.isOfficial || this.isDiscourseOwned) {
return I18n.t("admin.plugins.author", { author: "Discourse" });
}
return I18n.t("admin.plugins.author", { author: this.authors });
}
get linkUrl() {
return this.metaUrl || this.url;
}
}

View File

@ -1,88 +1,15 @@
{{#if this.model.length}}
<h3>{{i18n "admin.plugins.installed"}}</h3>
<table class="admin-plugins grid">
<thead>
<tr>
<th></th>
<th>{{i18n "admin.plugins.name"}}</th>
<th>{{i18n "admin.plugins.version"}}</th>
<th>{{i18n "admin.plugins.enabled"}}</th>
<th></th>
</tr>
</thead>
<tbody>
{{#each this.model as |plugin|}}
<tr data-plugin-name={{plugin.name}}>
<td>
{{#if plugin.isOfficial}}
{{d-icon
"check-circle"
title="admin.plugins.official"
class="admin-plugins-official-badge"
}}
{{/if}}
</td>
<td class="plugin-name">
<div class="name">
{{#if plugin.url}}
<a
href={{plugin.url}}
rel="noopener noreferrer"
target="_blank"
>{{plugin.name}} {{d-icon "external-link-alt"}}</a>
{{else}}
{{plugin.name}}
{{/if}}
</div>
<div class="about">
{{plugin.about}}
</div>
</td>
<td class="version">
<div class="label">{{i18n "admin.plugins.version"}}</div>
{{plugin.version}}<br />
<PluginCommitHash @plugin={{plugin}} />
</td>
<td class="col-enabled">
<div class="label">{{i18n "admin.plugins.enabled"}}</div>
{{#if plugin.enabledSetting}}
<DToggleSwitch
@state={{plugin.enabled}}
{{on "click" (fn this.togglePluginEnabled plugin)}}
/>
{{else}}
<DToggleSwitch @state={{plugin.enabled}} disabled={{true}} />
{{/if}}
</td>
<td class="settings">
{{#if this.currentUser.admin}}
{{#if plugin.hasSettings}}
<LinkTo
class="btn-default btn btn-icon-text"
@route="adminSiteSettingsCategory"
@model={{plugin.settingCategoryName}}
@query={{hash filter=(concat "plugin:" plugin.name)}}
data-plugin-setting-button={{plugin.name}}
>
{{d-icon "cog"}}
{{i18n "admin.plugins.change_settings_short"}}
</LinkTo>
{{/if}}
{{/if}}
</td>
</tr>
{{/each}}
</tbody>
</table>
<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

View File

@ -41,22 +41,27 @@ acceptance("Admin - Plugins", function (needs) {
await visit("/admin/plugins");
assert
.dom("table.admin-plugins tr .plugin-name .name")
.hasText("some-test-plugin", "displays the plugin in the table");
.dom(
"table.admin-plugins-list tr .admin-plugins-list__row .admin-plugins-list__name-with-badges .admin-plugins-list__name"
)
.hasText("Some Test Plugin", "displays the plugin in the table");
assert
.dom(".admin-plugins .admin-container .alert-error")
.exists("shows an error for unknown routes");
assert
.dom("table.admin-plugins tr .version a.commit-hash")
.dom(
"table.admin-plugins-list tr .admin-plugins-list__version a.commit-hash"
)
.hasAttribute(
"href",
"https://github.com/username/some-test-plugin/commit/1234567890abcdef",
"displays a commit hash with a link to commit url"
);
const toggleSelector = "table.admin-plugins tr .col-enabled button";
const toggleSelector =
"table.admin-plugins-list tr .admin-plugins-list__enabled button";
assert
.dom(toggleSelector)
.hasAttribute("aria-checked", "true", "displays the plugin as enabled");

View File

@ -6,18 +6,8 @@
margin-left: auto;
}
}
td.plugin-name {
.name {
font-weight: bold;
}
}
td.version {
.commit-hash {
color: var(--primary-low-mid);
font-size: var(--font-down-1);
}
}
.grid {
.admin-plugins-list {
@media screen and (min-width: 550px) {
tr {
grid-template-columns: 0.25fr repeat(4, 1fr);
@ -27,30 +17,65 @@
tr {
grid-template-columns: 0.25fr repeat(3, 1fr);
}
td.plugin-name {
grid-column-start: 2;
grid-column-end: -1;
}
td.settings {
grid-row: 2;
grid-column-start: 4;
text-align: right;
button {
display: flex;
.admin-plugins-list {
&__row {
grid-column-start: 2;
grid-column-end: -1;
}
&__settings {
grid-row: 2;
grid-column-start: 4;
text-align: right;
button {
display: flex;
}
}
&__version {
grid-row: 2;
grid-column-start: 3;
}
&__enabled {
grid-row: 2;
grid-column-start: 2;
}
}
td.version {
grid-row: 2;
grid-column-start: 3;
}
&__author {
font-size: var(--font-down-1);
padding: 0.25em 0;
}
&__name-with-badges {
display: flex;
}
&__name {
font-weight: bold;
margin-right: 0.5em;
}
&__badge {
font-weight: 400;
font-size: var(--font-down-2);
background-color: var(--primary-low);
color: var(--primary-medium);
padding: 0.2em 0.8em;
.d-icon {
font-size: 0.8em;
}
td.col-enabled {
grid-row: 2;
grid-column-start: 2;
& + .admin-plugins-list__badge {
margin-left: 0.5em;
}
}
&__version {
.commit-hash {
color: var(--primary-low-mid);
font-size: var(--font-down-1);
}
}
}
}
.admin-plugins-official-badge {
color: var(--success);
}

View File

@ -11,9 +11,12 @@ class AdminPluginSerializer < ApplicationSerializer
:enabled_setting,
:has_settings,
:is_official,
:is_experimental,
:is_discourse_owned,
:commit_hash,
:commit_url,
:meta_url
:meta_url,
:authors
def id
object.directory_name
@ -35,6 +38,10 @@ class AdminPluginSerializer < ApplicationSerializer
object.metadata.url
end
def authors
object.metadata.authors
end
def enabled
object.enabled?
end
@ -72,6 +79,14 @@ class AdminPluginSerializer < ApplicationSerializer
Plugin::Metadata::OFFICIAL_PLUGINS.include?(object.name)
end
def is_experimental
object.metadata.experimental
end
def is_discourse_owned
object.discourse_owned?
end
def commit_hash
object.commit_hash
end

View File

@ -2057,7 +2057,7 @@ en:
other {# errors/minute}
}.
learn_more: "learn more…"
learn_more: "Learn more…"
mute: Mute
unmute: Unmute
@ -5076,8 +5076,11 @@ en:
not_enabled: "N"
change_settings_short: "Settings"
howto: "How do I install plugins?"
official: "Official Plugin"
official: "Official Discourse Plugin"
experimental: "Experimental Plugin"
broken_route: "Unable to configure link to '%{name}'. Ensure ad-blockers are disabled and try reloading the page."
author: "By %{author}"
experimental_badge: "experimental"
navigation_menu:
sidebar: "Sidebar"

View File

@ -516,6 +516,14 @@ class Plugin::Instance
@git_repo ||= GitRepo.new(directory, name)
end
def discourse_owned?
parsed_commit_url = UrlHelper.relaxed_parse(self.commit_url)
return false if !parsed_commit_url
github_org = parsed_commit_url.path.split("/")[1]
(github_org == "discourse" || github_org == "discourse-org") &&
parsed_commit_url.host == "github.com"
end
# A proxy to `DiscourseEvent.on` which does nothing if the plugin is disabled
def on(event_name, &block)
DiscourseEvent.on(event_name) { |*args, **kwargs| block.call(*args, **kwargs) if enabled? }

View File

@ -111,6 +111,7 @@ class Plugin::Metadata
required_version
transpile_js
meta_topic_id
experimental
]
attr_accessor(*FIELDS)

View File

@ -4,6 +4,7 @@
# about: Add checklist support to Discourse
# version: 1.0
# authors: Discourse Team
# meta_topic_id: 36362
# url: https://github.com/discourse/discourse/tree/main/plugins/checklist
enabled_site_setting :checklist_enabled

View File

@ -4,6 +4,7 @@
# about: Adds markdown.it footnote support to Discourse
# version: 1.0
# authors: Discourse Team
# meta_topic_id: 84533
# url: https://github.com/discourse/discourse/tree/main/plugins/footnote
enabled_site_setting :enable_markdown_footnotes

View File

@ -2,6 +2,7 @@
# name: spoiler-alert
# about: Uses the Spoiler Alert plugin to blur text when spoiling it.
# meta_topic_id: 12650
# version: 1.1.0
# authors: Discourse Team
# url: https://github.com/discourse/discourse/tree/main/plugins/spoiler-alert

View File

@ -2,6 +2,7 @@
# name: styleguide
# about: Preview how Widgets are Styled in Discourse
# meta_topic_id: 167293
# version: 0.2
# author: Robin Ward

View File

@ -14,12 +14,12 @@ RSpec.describe Plugin::Instance do
expect(plugin.name).to eq("plugin-name")
expect(plugin.path).to eq("#{Rails.root}/spec/fixtures/plugins/my_plugin/plugin.rb")
git_repo = plugin.git_repo
plugin.git_repo.stubs(:latest_local_commit).returns("123456")
plugin.git_repo.stubs(:url).returns("http://github.com/discourse/discourse-plugin")
expect(plugin.commit_hash).to eq("123456")
expect(plugin.commit_url).to eq("http://github.com/discourse/discourse-plugin/commit/123456")
expect(plugin.discourse_owned?).to eq(true)
end
it "does not blow up on missing directory" do
@ -28,6 +28,23 @@ RSpec.describe Plugin::Instance do
end
end
describe "git repo details" do
describe ".discourse_owned?" do
it "returns true if the plugin is on github in discourse-org or discourse orgs" do
plugin = Plugin::Instance.find_all("#{Rails.root}/spec/fixtures/plugins")[3]
plugin.git_repo.stubs(:latest_local_commit).returns("123456")
plugin.git_repo.stubs(:url).returns("http://github.com/discourse/discourse-plugin")
expect(plugin.discourse_owned?).to eq(true)
plugin.git_repo.stubs(:url).returns("http://github.com/discourse-org/discourse-plugin")
expect(plugin.discourse_owned?).to eq(true)
plugin.git_repo.stubs(:url).returns("http://github.com/someguy/someguy-plugin")
expect(plugin.discourse_owned?).to eq(false)
end
end
end
describe "enabling/disabling" do
it "is enabled by default" do
expect(Plugin::Instance.new.enabled?).to eq(true)

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
describe "Admin Plugins List", type: :system, js: true do
fab!(:current_user) { Fabricate(:admin) }
before { sign_in(current_user) }
let(:spoiler_alert_plugin) do
plugins = Plugin::Instance.find_all("#{Rails.root}/plugins")
plugins.find { |p| p.name == "spoiler-alert" }
end
xit "shows the list of plugins" do
visit "/admin/plugins"
plugin_row = find(".admin-plugins-list tr[data-plugin-name=\"spoiler-alert\"]")
expect(plugin_row).to have_css(
"td.admin-plugins-list__row .admin-plugins__name-with-badges .admin-plugins__name",
text: "Spoiler Alert",
)
expect(plugin_row).to have_css(
"td.admin-plugins-list__row .admin-plugins__author",
text: I18n.t("admin_js.admin.plugins.author", { author: "Discourse" }),
)
expect(plugin_row).to have_css(
"td.admin-plugins-list__row .admin-plugins__name-with-badges .admin-plugins__name a[href=\"https://meta.discourse.org/t/12650\"]",
)
expect(plugin_row).to have_css(
"td.admin-plugins-list__row .admin-plugins__about",
text: spoiler_alert_plugin.metadata.about,
)
end
end