From c00fd3e17d84e074a190da40744d02b944814eda Mon Sep 17 00:00:00 2001 From: David Taylor Date: Thu, 5 Oct 2023 11:56:55 +0100 Subject: [PATCH] FEATURE: Introduce `api.renderInOutlet` (#23719) Until now, plugins/themes had to follow very specific directory structures to set up plugin outlet connectors. This commit introduces a new `api.renderInOutlet` API which makes things much more flexible. Any Ember component definition can be passed to this API, and will then be rendered into the named outlet. For example: ```javascript import MyComponent from "discourse/plugins/my-plugin/components/my-component"; api.renderInOutlet('user-profile-primary', MyComponent); ``` When using this API alongside the gjs file format, components can be defined inline like ```javascript api.renderInOutlet('user-profile-primary', ); ``` --- .../discourse/app/lib/plugin-api.js | 32 +++++++++++++++-- .../discourse/app/lib/plugin-connectors.js | 33 +++++++++++++++-- ...-outlet-test.js => plugin-outlet-test.gjs} | 36 +++++++++++++------ docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md | 6 ++++ 4 files changed, 92 insertions(+), 15 deletions(-) rename app/assets/javascripts/discourse/tests/integration/components/{plugin-outlet-test.js => plugin-outlet-test.gjs} (93%) diff --git a/app/assets/javascripts/discourse/app/lib/plugin-api.js b/app/assets/javascripts/discourse/app/lib/plugin-api.js index 6604b8aca6a..ceb207ba89a 100644 --- a/app/assets/javascripts/discourse/app/lib/plugin-api.js +++ b/app/assets/javascripts/discourse/app/lib/plugin-api.js @@ -77,7 +77,10 @@ import { addUsernameSelectorDecorator } from "discourse/helpers/decorate-usernam import { addWidgetCleanCallback } from "discourse/components/mount-widget"; import deprecated from "discourse-common/lib/deprecated"; import { disableNameSuppression } from "discourse/widgets/poster-name"; -import { extraConnectorClass } from "discourse/lib/plugin-connectors"; +import { + extraConnectorClass, + extraConnectorComponent, +} from "discourse/lib/plugin-connectors"; import { getOwnerWithFallback } from "discourse-common/lib/get-owner"; import { h } from "virtual-dom"; import { includeAttributes } from "discourse/lib/transform-post"; @@ -134,7 +137,7 @@ import { isTesting } from "discourse-common/config/environment"; // 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.12.0"; +export const PLUGIN_API_VERSION = "1.13.0"; // This helper prevents us from applying the same `modifyClass` over and over in test mode. function canModify(klass, type, resolverName, changes) { @@ -940,6 +943,31 @@ class PluginApi { extraConnectorClass(`${outletName}/${connectorName}`, klass); } + /** + * Register a component to be rendered in a particular outlet. + * + * For example, if the outlet is `user-profile-primary`, you could register + * a component like + * + * ```javascript + * import MyComponent from "discourse/plugins/my-plugin/components/my-component"; + * api.renderInOutlet('user-profile-primary', MyComponent); + * ``` + * + * Alternatively, a component could be defined inline using gjs: + * + * ```javascript + * api.renderInOutlet('user-profile-primary', ); + * ``` + * + * @param {string} outletName - Name of plugin outlet to render into + * @param {Component} klass - Component class definition to be rendered + * + */ + renderInOutlet(outletName, klass) { + extraConnectorComponent(outletName, klass); + } + /** * Register a button to display at the bottom of a topic * diff --git a/app/assets/javascripts/discourse/app/lib/plugin-connectors.js b/app/assets/javascripts/discourse/app/lib/plugin-connectors.js index dfa0d0e6b71..c4bbdec1eb7 100644 --- a/app/assets/javascripts/discourse/app/lib/plugin-connectors.js +++ b/app/assets/javascripts/discourse/app/lib/plugin-connectors.js @@ -10,9 +10,11 @@ import templateOnly from "@ember/component/template-only"; let _connectorCache; let _rawConnectorCache; let _extraConnectorClasses = {}; +let _extraConnectorComponents = {}; export function resetExtraClasses() { _extraConnectorClasses = {}; + _extraConnectorComponents = {}; } // Note: In plugins, define a class by path and it will be wired up automatically @@ -21,6 +23,17 @@ export function extraConnectorClass(name, obj) { _extraConnectorClasses[name] = obj; } +export function extraConnectorComponent(outletName, klass) { + if (!hasInternalComponentManager(klass)) { + throw new Error("klass is not an Ember component"); + } + if (outletName.includes("/")) { + throw new Error("invalid outlet name"); + } + _extraConnectorComponents[outletName] ??= []; + _extraConnectorComponents[outletName].push(klass); +} + const OUTLET_REGEX = /^discourse(\/[^\/]+)*?(?