diff --git a/app/assets/javascripts/discourse/app/components/plugin-outlet.js b/app/assets/javascripts/discourse/app/components/plugin-outlet.js
index 09b51fe3bcf..f73fbbcd12e 100644
--- a/app/assets/javascripts/discourse/app/components/plugin-outlet.js
+++ b/app/assets/javascripts/discourse/app/components/plugin-outlet.js
@@ -1,8 +1,19 @@
-import Component from "@ember/component";
+import GlimmerComponentWithDeprecatedParentView from "discourse/components/glimmer-component-with-deprecated-parent-view";
+import ClassicComponent from "@ember/component";
+
import {
buildArgsWithDeprecations,
renderedConnectorsFor,
} from "discourse/lib/plugin-connectors";
+import { helperContext } from "discourse-common/lib/helpers";
+import deprecated from "discourse-common/lib/deprecated";
+import { get } from "@ember/object";
+import { cached } from "@glimmer/tracking";
+
+const PARENT_VIEW_DEPRECATION_MSG =
+ "parentView should not be used within plugin outlets. Use the available outlet arguments, or inject a service which can provide the context you need.";
+const GET_DEPRECATION_MSG =
+ "Plugin outlet context is no longer an EmberObject - using `get()` is deprecated.";
/**
A plugin outlet is an extension point for templates where other templates can
@@ -13,7 +24,7 @@ import {
If your handlebars template has:
```handlebars
- {{plugin-outlet name="evil-trout"}}
+
```
Then any handlebars files you create in the `connectors/evil-trout` directory
@@ -29,26 +40,74 @@ import {
Will insert Hello World at that point in the template.
- ## Disabling
-
- If a plugin returns a disabled status, the outlets will not be wired up for it.
- The list of disabled plugins is returned via the `Site` singleton.
-
**/
-export default Component.extend({
- tagName: "",
- connectorTagName: "",
- connectors: null,
- init() {
- this._super(...arguments);
- const name = this.name;
- if (name) {
- const args = buildArgsWithDeprecations(
- this.args || {},
- this.deprecatedArgs || {}
- );
- this.set("connectors", renderedConnectorsFor(name, args, this));
+export default class PluginOutletComponent extends GlimmerComponentWithDeprecatedParentView {
+ context = {
+ ...helperContext(),
+ get parentView() {
+ return this.parentView;
+ },
+ get() {
+ deprecated(GET_DEPRECATION_MSG, {
+ id: "discourse.plugin-outlet-context-get",
+ });
+ return get(this, ...arguments);
+ },
+ };
+
+ constructor() {
+ super(...arguments);
+
+ this.connectors = renderedConnectorsFor(
+ this.args.name,
+ this.outletArgsWithDeprecations,
+ this.context
+ );
+ }
+
+ // Traditionally, pluginOutlets had an argument named 'args'. However, that name is reserved
+ // in recent versions of ember so we need to migrate to outletArgs
+ @cached
+ get outletArgs() {
+ return this.args.outletArgs || this.args.args || {};
+ }
+
+ @cached
+ get outletArgsWithDeprecations() {
+ if (!this.args.deprecatedArgs) {
+ return this.outletArgs;
}
- },
-});
+
+ return buildArgsWithDeprecations(
+ this.outletArgs,
+ this.args.deprecatedArgs || {}
+ );
+ }
+
+ get parentView() {
+ deprecated(`${PARENT_VIEW_DEPRECATION_MSG} (outlet: ${this.args.name})`, {
+ id: "discourse.plugin-outlet-parent-view",
+ });
+ return this._parentView;
+ }
+ set parentView(value) {
+ this._parentView = value;
+ }
+
+ // Older plugin outlets have a `tagName` which we need to preserve for backwards-compatibility
+ get wrapperComponent() {
+ return PluginOutletWithTagNameWrapper;
+ }
+}
+
+class PluginOutletWithTagNameWrapper extends ClassicComponent {
+ // Overridden parentView to make this wrapper 'transparent'
+ // Calling this will trigger the deprecation notice in PluginOutletComponent
+ get parentView() {
+ return this._parentView.parentView;
+ }
+ set parentView(value) {
+ this._parentView = value;
+ }
+}
diff --git a/app/assets/javascripts/discourse/app/templates/components/plugin-outlet.hbs b/app/assets/javascripts/discourse/app/templates/components/plugin-outlet.hbs
index f5d83c4bbf4..900b9effa43 100644
--- a/app/assets/javascripts/discourse/app/templates/components/plugin-outlet.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/plugin-outlet.hbs
@@ -1,9 +1,28 @@
-{{#each this.connectors as |c|}}
-
-{{/each}}
\ No newline at end of file
+{{#if @tagName}}
+ {{!
+ Older outlets have a wrapper tagName. RFC0389 proposes an interface for dynamic tag names, which we may want to use in future.
+ But for now, this classic component wrapper takes care of the tagName.
+ }}
+
+ {{#each this.connectors as |c|}}
+
+ {{/each}}
+
+{{else}}
+ {{! The modern path: no wrapper element = no classic component }}
+ {{#each this.connectors as |c|}}
+
+ {{/each}}
+{{/if}}
\ No newline at end of file
diff --git a/app/assets/javascripts/discourse/tests/acceptance/plugin-outlet-parent-view-test.js b/app/assets/javascripts/discourse/tests/acceptance/plugin-outlet-parent-view-test.js
new file mode 100644
index 00000000000..12683437209
--- /dev/null
+++ b/app/assets/javascripts/discourse/tests/acceptance/plugin-outlet-parent-view-test.js
@@ -0,0 +1,29 @@
+import { acceptance, query } from "discourse/tests/helpers/qunit-helpers";
+import { hbs } from "ember-cli-htmlbars";
+import { test } from "qunit";
+import { visit } from "@ember/test-helpers";
+import { withSilencedDeprecationsAsync } from "discourse-common/lib/deprecated";
+import { registerTemporaryModule } from "discourse/tests/helpers/temporary-module-helper";
+
+acceptance("Plugin Outlet - Deprecated parentView", function (needs) {
+ needs.hooks.beforeEach(function () {
+ registerTemporaryModule(
+ "discourse/templates/connectors/user-profile-primary/hello",
+ hbs`{{parentView.parentView.class}}`
+ );
+ });
+
+ test("Can access parentview", async function (assert) {
+ await withSilencedDeprecationsAsync(
+ "discourse.plugin-outlet-parent-view",
+ async () => {
+ await visit("/u/eviltrout");
+ assert.strictEqual(
+ query(".hello-username").innerText,
+ "user-main",
+ "it renders a value from parentView.parentView"
+ );
+ }
+ );
+ });
+});