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" + ); + } + ); + }); +});