DEV: Convert PluginOutlet wrapper to a Glimmer component

PluginConnector remains a Classic Component, so this commit does not require any changes from plugin/theme developers.

Two shims are introduced for backwards compatibility:

- The component variable passed to shouldRender is replaced with a helperContext instance which includes all the common injections (the new PluginOutlet component instance does not have any of these)

- A custom component manager is introduced so that parentView continues to work. Using parentView was never really intended as an API, so it's now deprecated and will print a warning to the console. Users should switch to using the outlet's explicit arguments, or data from a service (e.g. the Router service).
This commit is contained in:
David Taylor 2023-01-31 10:55:05 +00:00
parent 96a6bb69b5
commit 30025a96f3
3 changed files with 138 additions and 31 deletions

View File

@ -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"}}
<PluginOutlet @name="evil-trout" />
```
Then any handlebars files you create in the `connectors/evil-trout` directory
@ -29,26 +40,74 @@ import {
Will insert <b>Hello World</b> 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;
}
}

View File

@ -1,9 +1,28 @@
{{#each this.connectors as |c|}}
<PluginConnector
@connector={{c}}
@args={{this.args}}
@deprecatedArgs={{this.deprecatedArgs}}
@class={{c.classNames}}
@tagName={{this.connectorTagName}}
/>
{{/each}}
{{#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.
}}
<this.wrapperComponent @tagName={{@tagName}}>
{{#each this.connectors as |c|}}
<PluginConnector
@connector={{c}}
@args={{this.outletArgs}}
@deprecatedArgs={{@deprecatedArgs}}
@class={{c.classNames}}
@tagName={{or @connectorTagName ""}}
/>
{{/each}}
</this.wrapperComponent>
{{else}}
{{! The modern path: no wrapper element = no classic component }}
{{#each this.connectors as |c|}}
<PluginConnector
@connector={{c}}
@args={{this.outletArgs}}
@deprecatedArgs={{@deprecatedArgs}}
@class={{c.classNames}}
@tagName={{or @connectorTagName ""}}
/>
{{/each}}
{{/if}}

View File

@ -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`<span class='hello-username'>{{parentView.parentView.class}}</span>`
);
});
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"
);
}
);
});
});