diff --git a/app/assets/javascripts/discourse/app/components/header/contents.gjs b/app/assets/javascripts/discourse/app/components/header/contents.gjs
index 4e6d8943235..e6ee1beea75 100644
--- a/app/assets/javascripts/discourse/app/components/header/contents.gjs
+++ b/app/assets/javascripts/discourse/app/components/header/contents.gjs
@@ -2,6 +2,7 @@ import Component from "@glimmer/component";
import { hash } from "@ember/helper";
import { service } from "@ember/service";
import { and } from "truth-helpers";
+import deprecatedOutletArgument from "discourse/helpers/deprecated-outlet-argument";
import BootstrapModeNotice from "../bootstrap-mode-notice";
import PluginOutlet from "../plugin-outlet";
import HomeLogo from "./home-logo";
@@ -28,10 +29,19 @@ export default class Contents extends Component {
{{#if this.site.desktopView}}
{{#if @sidebarEnabled}}
@@ -67,10 +77,19 @@ export default class Contents extends Component {
{{yield}}
@@ -78,19 +97,37 @@ export default class Contents extends Component {
diff --git a/app/assets/javascripts/discourse/app/components/plugin-connector.js b/app/assets/javascripts/discourse/app/components/plugin-connector.js
index 392af8d3cd8..d95e6571788 100644
--- a/app/assets/javascripts/discourse/app/components/plugin-connector.js
+++ b/app/assets/javascripts/discourse/app/components/plugin-connector.js
@@ -1,7 +1,9 @@
import Component from "@ember/component";
import { computed, defineProperty } from "@ember/object";
-import { buildArgsWithDeprecations } from "discourse/lib/plugin-connectors";
-import deprecated from "discourse-common/lib/deprecated";
+import {
+ buildArgsWithDeprecations,
+ deprecatedArgumentValue,
+} from "discourse/lib/plugin-connectors";
import { afterRender } from "discourse-common/utils/decorators";
let _decorators = {};
@@ -30,19 +32,23 @@ export default Component.extend({
});
const deprecatedArgs = this.deprecatedArgs || {};
+ const connectorInfo = {
+ outletName: this.connector?.outletName,
+ connectorName: this.connector?.connectorName,
+ classModuleName: this.connector?.classModuleName,
+ templateModule: this.connector?.templateModule,
+ layoutName: this.layoutName,
+ };
+
Object.keys(deprecatedArgs).forEach((key) => {
defineProperty(
this,
key,
computed("deprecatedArgs", () => {
- deprecated(
- `The ${key} property is deprecated, but is being used in ${this.layoutName}`,
- {
- id: "discourse.plugin-connector.deprecated-arg",
- }
- );
-
- return (this.deprecatedArgs || {})[key];
+ return deprecatedArgumentValue(deprecatedArgs[key], {
+ ...connectorInfo,
+ argumentName: key,
+ });
})
);
});
@@ -56,7 +62,11 @@ export default Component.extend({
}
}
- const merged = buildArgsWithDeprecations(args, deprecatedArgs);
+ const merged = buildArgsWithDeprecations(
+ args,
+ deprecatedArgs,
+ connectorInfo
+ );
connectorClass?.setupComponent?.call(this, merged, this);
},
diff --git a/app/assets/javascripts/discourse/app/components/plugin-outlet.js b/app/assets/javascripts/discourse/app/components/plugin-outlet.js
index 41bf1ee24ea..d0ae53f15fa 100644
--- a/app/assets/javascripts/discourse/app/components/plugin-outlet.js
+++ b/app/assets/javascripts/discourse/app/components/plugin-outlet.js
@@ -127,7 +127,8 @@ export default class PluginOutletComponent extends GlimmerComponentWithDeprecate
return buildArgsWithDeprecations(
this.outletArgs,
- this.args.deprecatedArgs || {}
+ this.args.deprecatedArgs || {},
+ { outletName: this.args.name }
);
}
diff --git a/app/assets/javascripts/discourse/app/helpers/deprecated-outlet-argument.js b/app/assets/javascripts/discourse/app/helpers/deprecated-outlet-argument.js
new file mode 100644
index 00000000000..bd634dc496c
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/helpers/deprecated-outlet-argument.js
@@ -0,0 +1,39 @@
+export default function deprecatedOutletArgument(options) {
+ return new DeprecatedOutletArgument(options);
+}
+
+export function isDeprecatedOutletArgument(value) {
+ return value instanceof DeprecatedOutletArgument;
+}
+
+class DeprecatedOutletArgument {
+ #message;
+ #silence;
+ #valueRef;
+
+ constructor(options) {
+ this.#message = options.message;
+ this.#valueRef = () => options.value;
+ this.#silence = options.silence;
+
+ this.options = {
+ id: options.id || "discourse.plugin-connector.deprecated-arg",
+ since: options.since,
+ dropFrom: options.dropFrom,
+ url: options.url,
+ raiseError: options.raiseError,
+ };
+ }
+
+ get message() {
+ return this.#message;
+ }
+
+ get silence() {
+ return this.#silence;
+ }
+
+ get value() {
+ return this.#valueRef();
+ }
+}
diff --git a/app/assets/javascripts/discourse/app/lib/plugin-connectors.js b/app/assets/javascripts/discourse/app/lib/plugin-connectors.js
index 9a9115aab65..11b4065945c 100644
--- a/app/assets/javascripts/discourse/app/lib/plugin-connectors.js
+++ b/app/assets/javascripts/discourse/app/lib/plugin-connectors.js
@@ -4,7 +4,10 @@ import {
setComponentTemplate,
} from "@glimmer/manager";
import templateOnly from "@ember/component/template-only";
-import deprecated from "discourse-common/lib/deprecated";
+import { isDeprecatedOutletArgument } from "discourse/helpers/deprecated-outlet-argument";
+import deprecated, {
+ withSilencedDeprecations,
+} from "discourse-common/lib/deprecated";
import { buildRawConnectorCache } from "discourse-common/lib/raw-templates";
let _connectorCache;
@@ -235,24 +238,61 @@ export function rawConnectorsFor(outletName) {
return _rawConnectorCache[outletName] || [];
}
-export function buildArgsWithDeprecations(args, deprecatedArgs) {
+export function buildArgsWithDeprecations(args, deprecatedArgs, opts = {}) {
const output = {};
Object.keys(args).forEach((key) => {
Object.defineProperty(output, key, { value: args[key] });
});
- Object.keys(deprecatedArgs).forEach((key) => {
- Object.defineProperty(output, key, {
+ Object.keys(deprecatedArgs).forEach((argumentName) => {
+ Object.defineProperty(output, argumentName, {
get() {
- deprecated(`${key} is deprecated`, {
- id: "discourse.plugin-connector.deprecated-arg",
- });
+ const deprecatedArg = deprecatedArgs[argumentName];
- return deprecatedArgs[key];
+ return deprecatedArgumentValue(deprecatedArg, {
+ ...opts,
+ argumentName,
+ });
},
});
});
return output;
}
+
+export function deprecatedArgumentValue(deprecatedArg, options) {
+ if (!isDeprecatedOutletArgument(deprecatedArg)) {
+ throw new Error(
+ "deprecated argument is not defined properly, use helper `deprecatedOutletArgument` from discourse/helpers/deprecated-outlet-argument"
+ );
+ }
+
+ let message = deprecatedArg.message;
+ if (!message) {
+ if (options.outletName) {
+ message = `outlet arg \`${options.argumentName}\` is deprecated on the outlet \`${options.outletName}\``;
+ } else {
+ message = `${options.argumentName} is deprecated`;
+ }
+ }
+
+ const connectorModule =
+ options.classModuleName || options.templateModule || options.connectorName;
+
+ if (connectorModule) {
+ message += ` [used on connector ${connectorModule}]`;
+ } else if (options.layoutName) {
+ message += ` [used on ${options.layoutName}]`;
+ }
+
+ if (!deprecatedArg.silence) {
+ deprecated(message, deprecatedArg.options);
+ return deprecatedArg.value;
+ }
+
+ return withSilencedDeprecations(deprecatedArg.silence, () => {
+ deprecated(message, deprecatedArg.options);
+ return deprecatedArg.value;
+ });
+}
diff --git a/app/assets/javascripts/discourse/tests/integration/components/plugin-outlet-test.gjs b/app/assets/javascripts/discourse/tests/integration/components/plugin-outlet-test.gjs
index 5f657feaffc..cd6a55ffc49 100644
--- a/app/assets/javascripts/discourse/tests/integration/components/plugin-outlet-test.gjs
+++ b/app/assets/javascripts/discourse/tests/integration/components/plugin-outlet-test.gjs
@@ -7,6 +7,7 @@ import hbs from "htmlbars-inline-precompile";
import { module, test } from "qunit";
import sinon from "sinon";
import PluginOutlet from "discourse/components/plugin-outlet";
+import deprecatedOutletArgument from "discourse/helpers/deprecated-outlet-argument";
import {
extraConnectorClass,
extraConnectorComponent,
@@ -14,10 +15,14 @@ import {
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { query } from "discourse/tests/helpers/qunit-helpers";
import { registerTemporaryModule } from "discourse/tests/helpers/temporary-module-helper";
-import {
+import deprecated, {
withSilencedDeprecations,
withSilencedDeprecationsAsync,
} from "discourse-common/lib/deprecated";
+import {
+ disableRaiseOnDeprecation,
+ enableRaiseOnDeprecation,
+} from "../../helpers/raise-on-deprecation";
const TEMPLATE_PREFIX = "discourse/plugins/some-plugin/templates/connectors";
const CLASS_PREFIX = "discourse/plugins/some-plugin/connectors";
@@ -423,6 +428,172 @@ module("Integration | Component | plugin-outlet", function (hooks) {
"other outlet is left untouched"
);
});
+
+ module("deprecated arguments", function (innerHooks) {
+ innerHooks.beforeEach(function () {
+ this.consoleWarnStub = sinon.stub(console, "warn");
+ disableRaiseOnDeprecation();
+ });
+
+ innerHooks.afterEach(function () {
+ this.consoleWarnStub.restore();
+ enableRaiseOnDeprecation();
+ });
+
+ test("deprecated parameters with default message", async function (assert) {
+ await render(
+
+ );
+
+ // deprecated argument still works
+ assert.dom(".conditional-render").exists("renders conditional outlet");
+
+ assert.strictEqual(
+ this.consoleWarnStub.callCount,
+ 1,
+ "console warn was called once"
+ );
+ assert.strictEqual(
+ this.consoleWarnStub.calledWith(
+ "Deprecation notice: outlet arg `shouldDisplay` is deprecated on the outlet `test-name` [deprecation id: discourse.plugin-connector.deprecated-arg]"
+ ),
+ true,
+ "logs the default message to the console"
+ );
+ });
+
+ test("deprecated parameters with custom deprecation data", async function (assert) {
+ await render(
+
+ );
+
+ // deprecated argument still works
+ assert.dom(".conditional-render").exists("renders conditional outlet");
+
+ assert.strictEqual(
+ this.consoleWarnStub.callCount,
+ 1,
+ "console warn was called once"
+ );
+ assert.strictEqual(
+ this.consoleWarnStub.calledWith(
+ sinon.match(/The 'shouldDisplay' is deprecated on this test/)
+ ),
+ true,
+ "logs the custom deprecation message to the console"
+ );
+ assert.strictEqual(
+ this.consoleWarnStub.calledWith(
+ sinon.match(
+ /deprecation id: discourse.plugin-connector.deprecated-arg.test/
+ )
+ ),
+ true,
+ "logs custom deprecation id"
+ );
+ assert.strictEqual(
+ this.consoleWarnStub.calledWith(
+ sinon.match(/deprecated since Discourse 3.3.0.beta4-dev/)
+ ),
+ true,
+ "logs deprecation since information"
+ );
+ assert.strictEqual(
+ this.consoleWarnStub.calledWith(
+ sinon.match(/removal in Discourse 3.4.0/)
+ ),
+ true,
+ "logs dropFrom information"
+ );
+ });
+
+ test("silence nested deprecations", async function (assert) {
+ const deprecatedData = {
+ get display() {
+ deprecated("Test message", {
+ id: "discourse.deprecation-that-should-not-be-logged",
+ });
+ return true;
+ },
+ };
+
+ await render(
+
+ );
+
+ // deprecated argument still works
+ assert.dom(".conditional-render").exists("renders conditional outlet");
+
+ assert.strictEqual(
+ this.consoleWarnStub.callCount,
+ 1,
+ "console warn was called once"
+ );
+ assert.strictEqual(
+ this.consoleWarnStub.calledWith(
+ sinon.match(
+ /deprecation id: discourse.deprecation-that-should-not-be-logged/
+ )
+ ),
+ false,
+ "does not log silence deprecation"
+ );
+ assert.strictEqual(
+ this.consoleWarnStub.calledWith(
+ sinon.match(
+ /deprecation id: discourse.plugin-connector.deprecated-arg/
+ )
+ ),
+ true,
+ "logs expected deprecation"
+ );
+ });
+
+ test("unused arguments", async function (assert) {
+ await render(
+
+ );
+
+ // deprecated argument still works
+ assert.dom(".conditional-render").exists("renders conditional outlet");
+
+ assert.strictEqual(
+ this.consoleWarnStub.callCount,
+ 0,
+ "console warn not called"
+ );
+ });
+ });
});
module(
@@ -434,10 +605,8 @@ module(
registerTemporaryModule(
`${TEMPLATE_PREFIX}/test-name/my-connector`,
hbs`
- {{@outletArgs.hello}}{{this.hello}}
- `
+ {{@outletArgs.hello}}
+ {{this.hello}}`
);
});
@@ -583,6 +752,205 @@ module(
assert.dom(".outletArgHelloValue").doesNotExist();
});
+
+ module("deprecated arguments", function (innerHooks) {
+ innerHooks.beforeEach(function () {
+ this.consoleWarnStub = sinon.stub(console, "warn");
+ disableRaiseOnDeprecation();
+ });
+
+ innerHooks.afterEach(function () {
+ this.consoleWarnStub.restore();
+ enableRaiseOnDeprecation();
+ });
+
+ test("using classic PluginConnector by default", async function (assert) {
+ await render(hbs`
+
+ `);
+
+ // deprecated argument still works
+ assert.dom(".outletArgHelloValue").hasText("world");
+ assert.dom(".thisHelloValue").hasText("world");
+
+ assert.strictEqual(
+ this.consoleWarnStub.callCount,
+ 2,
+ "console warn was called twice"
+ );
+ assert.strictEqual(
+ this.consoleWarnStub.calledWith(
+ "Deprecation notice: outlet arg `hello` is deprecated on the outlet `test-name` [deprecation id: discourse.plugin-connector.deprecated-arg]"
+ ),
+ true,
+ "logs the expected message for @outletArgs.hello"
+ );
+ assert.strictEqual(
+ this.consoleWarnStub.calledWith(
+ "Deprecation notice: outlet arg `hello` is deprecated on the outlet `test-name` [used on connector discourse/plugins/some-plugin/templates/connectors/test-name/my-connector] [deprecation id: discourse.plugin-connector.deprecated-arg]"
+ ),
+ true,
+ "logs the expected message for this.hello"
+ );
+ });
+
+ test("using templateOnly by default when @defaultGlimmer=true", async function (assert) {
+ await render(hbs`
+
+ `);
+
+ // deprecated argument still works
+ assert.dom(".outletArgHelloValue").hasText("world");
+ assert.dom(".thisHelloValue").hasText(""); // `this.` unavailable in templateOnly components
+
+ assert.strictEqual(
+ this.consoleWarnStub.callCount,
+ 1,
+ "console warn was called once"
+ );
+ assert.strictEqual(
+ this.consoleWarnStub.calledWith(
+ "Deprecation notice: outlet arg `hello` is deprecated on the outlet `test-name` [deprecation id: discourse.plugin-connector.deprecated-arg]"
+ ),
+ true,
+ "logs the expected message for @outletArgs.hello"
+ );
+ assert.strictEqual(
+ this.consoleWarnStub.calledWith(
+ "Deprecation notice: outlet arg `hello` is deprecated on the outlet `test-name` [used on connector discourse/plugins/some-plugin/templates/connectors/test-name/my-connector] [deprecation id: discourse.plugin-connector.deprecated-arg]"
+ ),
+ false,
+ "does not log the message for this.hello"
+ );
+ });
+
+ test("using simple object when provided", async function (assert) {
+ registerTemporaryModule(`${CLASS_PREFIX}/test-name/my-connector`, {
+ setupComponent(args, component) {
+ component.reopen({
+ get hello() {
+ return args.hello + " from setupComponent";
+ },
+ });
+ },
+ });
+
+ await render(hbs`
+
+ `);
+
+ // deprecated argument still works
+ assert.dom(".outletArgHelloValue").hasText("world");
+ assert.dom(".thisHelloValue").hasText("world from setupComponent");
+
+ assert.strictEqual(
+ this.consoleWarnStub.callCount,
+ 2,
+ "console warn was called twice"
+ );
+ assert.strictEqual(
+ this.consoleWarnStub.calledWith(
+ "Deprecation notice: outlet arg `hello` is deprecated on the outlet `test-name` [deprecation id: discourse.plugin-connector.deprecated-arg]"
+ ),
+ true,
+ "logs the expected message for @outletArgs.hello"
+ );
+ assert.strictEqual(
+ this.consoleWarnStub.calledWith(
+ "Deprecation notice: outlet arg `hello` is deprecated on the outlet `test-name` [used on connector discourse/plugins/some-plugin/connectors/test-name/my-connector] [deprecation id: discourse.plugin-connector.deprecated-arg]"
+ ),
+ true,
+ "logs the expected message for this.hello"
+ );
+ });
+
+ test("using custom component class if provided", async function (assert) {
+ registerTemporaryModule(
+ `${CLASS_PREFIX}/test-name/my-connector`,
+ class MyOutlet extends Component {
+ get hello() {
+ return this.args.outletArgs.hello + " from custom component";
+ }
+ }
+ );
+
+ await render(hbs`
+
+ `);
+
+ // deprecated argument still works
+ assert.dom(".outletArgHelloValue").hasText("world");
+ assert.dom(".thisHelloValue").hasText("world from custom component");
+
+ assert.strictEqual(
+ this.consoleWarnStub.callCount,
+ 2,
+ "console warn was called twice"
+ );
+ assert.strictEqual(
+ this.consoleWarnStub.calledWith(
+ "Deprecation notice: outlet arg `hello` is deprecated on the outlet `test-name` [deprecation id: discourse.plugin-connector.deprecated-arg]"
+ ),
+ true,
+ "logs the expected message for @outletArgs.hello"
+ );
+ assert.strictEqual(
+ this.consoleWarnStub.calledWith(
+ "Deprecation notice: outlet arg `hello` is deprecated on the outlet `test-name` [deprecation id: discourse.plugin-connector.deprecated-arg]"
+ ),
+ true,
+ "logs the expected message for this.hello"
+ );
+ });
+
+ test("using custom templateOnly() if provided", async function (assert) {
+ registerTemporaryModule(
+ `${CLASS_PREFIX}/test-name/my-connector`,
+ templateOnly()
+ );
+
+ await render(hbs`
+
+ `);
+
+ // deprecated argument still works
+ assert.dom(".outletArgHelloValue").hasText("world");
+ assert.dom(".thisHelloValue").hasText(""); // `this.` unavailable in templateOnly components
+
+ assert.strictEqual(
+ this.consoleWarnStub.callCount,
+ 1,
+ "console warn was called twice"
+ );
+ assert.strictEqual(
+ this.consoleWarnStub.calledWith(
+ "Deprecation notice: outlet arg `hello` is deprecated on the outlet `test-name` [deprecation id: discourse.plugin-connector.deprecated-arg]"
+ ),
+ true,
+ "logs the expected message for @outletArgs.hello"
+ );
+ });
+
+ test("unused arguments", async function (assert) {
+ await render(hbs`
+
+ `);
+
+ // deprecated argument still works
+ assert.dom(".outletArgHelloValue").hasText("world");
+ assert.dom(".thisHelloValue").hasText("world");
+
+ assert.strictEqual(
+ this.consoleWarnStub.callCount,
+ 0,
+ "console warn was called twice"
+ );
+ });
+ });
}
);