DEV: Introduce a value transformer front-end plugin API (#27090)
This commit introduces the `valueTransformer`API to safely override values defined in Discourse. Two new plugin APIs are introduced: - `addValueTransformerName` which allows plugins and theme-components to add a new valid transformer name if they want to provide overridable values; - `registerValueTransformer` to register a transformer to override values. It also introduces the function `applyValueTransformer` which can be imported from `discourse/lib/transformer`. This function marks the desired value as overridable and applies the transformer logic. How does it work? ## Marking a value as overridable: To mark a value as overridable, in Discourse core, first the transformer name must be added to `app/assets/javascripts/discourse/app/lib/transformer/registry.js`. For plugins and theme-components, use the plugin API `addValueTransformerName` instead. Then, in your component or class, use the function `applyValueTransformer` to mark the value as overridable and handle the logic: - example: ```js export default class HomeLogo extends Component { @service session; @service site; ... get href() { return applyValueTransformer("home-logo-href", getURL("/")); } ``` ## Overriding a value in plugins or themes To override a value in plugins, themes, or TCs use the plugin API `registerValueTransformer`: - Example: ```js withPluginApi("1.34.0", (api) => { api.registerValueTransformer("example-transformer", ({ value }) => { return "new-value"; }); }); ```
This commit is contained in:
parent
e5dac3b422
commit
9668592aab
|
@ -6,20 +6,11 @@ import { service } from "@ember/service";
|
|||
import PluginOutlet from "discourse/components/plugin-outlet";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import { wantsNewWindow } from "discourse/lib/intercept-click";
|
||||
import { applyValueTransformer } from "discourse/lib/transformer";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
import HomeLogoContents from "./home-logo-contents";
|
||||
|
||||
let hrefCallback;
|
||||
|
||||
export function registerHomeLogoHrefCallback(callback) {
|
||||
hrefCallback = callback;
|
||||
}
|
||||
|
||||
export function clearHomeLogoHrefCallback() {
|
||||
hrefCallback = null;
|
||||
}
|
||||
|
||||
export default class HomeLogo extends Component {
|
||||
@service session;
|
||||
@service site;
|
||||
|
@ -28,11 +19,7 @@ export default class HomeLogo extends Component {
|
|||
darkModeAvailable = this.session.darkModeAvailable;
|
||||
|
||||
get href() {
|
||||
if (hrefCallback) {
|
||||
return hrefCallback();
|
||||
}
|
||||
|
||||
return getURL("/");
|
||||
return applyValueTransformer("home-logo-href", getURL("/"));
|
||||
}
|
||||
|
||||
get showMobileLogo() {
|
||||
|
|
|
@ -7,17 +7,18 @@ import {
|
|||
addExtraUserClasses,
|
||||
renderAvatar,
|
||||
} from "discourse/helpers/user-avatar";
|
||||
import { applyValueTransformer } from "discourse/lib/transformer";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import UserTip from "../../user-tip";
|
||||
import UserStatusBubble from "./user-status-bubble";
|
||||
|
||||
const DEFAULT_AVATAR_SIZE = "medium";
|
||||
|
||||
export default class Notifications extends Component {
|
||||
@service currentUser;
|
||||
@service siteSettings;
|
||||
|
||||
avatarSize = "medium";
|
||||
|
||||
get avatar() {
|
||||
const avatarAttrs = addExtraUserClasses(this.currentUser, {});
|
||||
return htmlSafe(
|
||||
|
@ -32,6 +33,13 @@ export default class Notifications extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
get avatarSize() {
|
||||
return applyValueTransformer(
|
||||
"header-notifications-avatar-size",
|
||||
DEFAULT_AVATAR_SIZE
|
||||
);
|
||||
}
|
||||
|
||||
get _shouldHighlightAvatar() {
|
||||
return (
|
||||
!this.currentUser.read_first_notification &&
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import { _freezeValidTransformerNames } from "discourse/lib/transformer";
|
||||
|
||||
export default {
|
||||
before: "inject-discourse-objects",
|
||||
|
||||
initialize() {
|
||||
_freezeValidTransformerNames();
|
||||
},
|
||||
};
|
|
@ -13,7 +13,6 @@ import { addCategorySortCriteria } from "discourse/components/edit-category-sett
|
|||
import { forceDropdownForMenuPanels as glimmerForceDropdownForMenuPanels } from "discourse/components/glimmer-site-header";
|
||||
import { addGlobalNotice } from "discourse/components/global-notice";
|
||||
import { headerButtonsDAG } from "discourse/components/header";
|
||||
import { registerHomeLogoHrefCallback } from "discourse/components/header/home-logo";
|
||||
import { headerIconsDAG } from "discourse/components/header/icons";
|
||||
import { _addBulkButton } from "discourse/components/modal/topic-bulk-actions";
|
||||
import MountWidget, {
|
||||
|
@ -93,6 +92,10 @@ import {
|
|||
import { registerCustomTagSectionLinkPrefixIcon } from "discourse/lib/sidebar/user/tags-section/base-tag-section-link";
|
||||
import { consolePrefix } from "discourse/lib/source-identifier";
|
||||
import { includeAttributes } from "discourse/lib/transform-post";
|
||||
import {
|
||||
_addTransformerName,
|
||||
_registerTransformer,
|
||||
} from "discourse/lib/transformer";
|
||||
import { registerUserMenuTab } from "discourse/lib/user-menu/tab";
|
||||
import { replaceFormatter } from "discourse/lib/utilities";
|
||||
import { addCardClickListenerSelector } from "discourse/mixins/card-contents-base";
|
||||
|
@ -110,7 +113,6 @@ import { setNewCategoryDefaultColors } from "discourse/routes/new-category";
|
|||
import { setNotificationsLimit } from "discourse/routes/user-notifications";
|
||||
import { addComposerSaveErrorCallback } from "discourse/services/composer";
|
||||
import { attachAdditionalPanel } from "discourse/widgets/header";
|
||||
import { registerHomeLogoHrefCallback as registerHomeLogoHrefCallbackOnWidget } from "discourse/widgets/home-logo";
|
||||
import { addPostClassesCallback } from "discourse/widgets/post";
|
||||
import { addDecorator } from "discourse/widgets/post-cooked";
|
||||
import {
|
||||
|
@ -153,7 +155,7 @@ import { modifySelectKit } from "select-kit/mixins/plugin-api";
|
|||
// 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.33.0";
|
||||
export const PLUGIN_API_VERSION = "1.34.0";
|
||||
|
||||
const DEPRECATED_HEADER_WIDGETS = [
|
||||
"header",
|
||||
|
@ -328,6 +330,69 @@ class PluginApi {
|
|||
return klass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new valid transformer name.
|
||||
*
|
||||
* Use this API to add a new transformer name that can be used in the `registerValueTransformer` API.
|
||||
*
|
||||
* Notice that this API must be used in a pre-initializer, executed before `freeze-valid-transformers`, otherwise it will throw an error:
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* // pre-initializers/my-transformers.js
|
||||
*
|
||||
* export default {
|
||||
* before: "freeze-valid-transformers",
|
||||
*
|
||||
* initialize() {
|
||||
* withPluginApi("1.33.0", (api) => {
|
||||
* api.addValueTransformerName("my-unique-transformer-name");
|
||||
* }),
|
||||
* },
|
||||
* };
|
||||
*
|
||||
* @param name the name of the new transformer
|
||||
*
|
||||
*/
|
||||
addValueTransformerName(name) {
|
||||
_addTransformerName(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a transformer to override values defined in Discourse.
|
||||
*
|
||||
* Example: return a static value
|
||||
* ```
|
||||
* api.registerValueTransformer("example-transformer", () => "value");
|
||||
* ```
|
||||
*
|
||||
* Example: transform the current value
|
||||
* ```
|
||||
* api.registerValueTransformer("example-transformer", ({value}) => value * 10);
|
||||
* ```
|
||||
*
|
||||
* Example: transform the current value based on a context property
|
||||
* ```
|
||||
* api.registerValueTransformer("example-transformer", ({value, context}) => {
|
||||
* if (context.property) {
|
||||
* return value * 10;
|
||||
* }
|
||||
*
|
||||
* return value;
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param {string} transformerName the name of the transformer
|
||||
* @param {function({value, context})} valueCallback callback to be used to transform the value. To avoid potential
|
||||
* errors or unexpected behavior the callback must be a pure function, i.e. return the transform value instead of
|
||||
* mutating the input value, return the same output for the same input and not have any side effects.
|
||||
* @param {*} valueCallback.value the value to be transformed
|
||||
* @param {*} [valueCallback.context] the optional context in which the value is being transformed
|
||||
*/
|
||||
registerValueTransformer(transformerName, valueCallback) {
|
||||
_registerTransformer(transformerName, valueCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* If you want to use custom icons in your discourse application,
|
||||
* you can register a renderer that will return an icon in the
|
||||
|
@ -2015,8 +2080,7 @@ class PluginApi {
|
|||
*
|
||||
*/
|
||||
registerHomeLogoHrefCallback(callback) {
|
||||
registerHomeLogoHrefCallback(callback);
|
||||
registerHomeLogoHrefCallbackOnWidget(callback); // for compatibility with the legacy header
|
||||
_registerTransformer("home-logo-href", ({ value }) => callback(value));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,226 @@
|
|||
import { VALUE_TRANSFORMERS } from "discourse/lib/transformer/registry";
|
||||
import { isTesting } from "discourse-common/config/environment";
|
||||
|
||||
// add core transformer names
|
||||
const validCoreTransformerNames = new Set(
|
||||
VALUE_TRANSFORMERS.map((name) => name.toLowerCase())
|
||||
);
|
||||
|
||||
// do not add anything directly to this set, use addValueTransformerName instead
|
||||
const validPluginTransformerNames = new Set();
|
||||
|
||||
const transformersRegistry = new Map();
|
||||
|
||||
/**
|
||||
* Indicates if the registry is open for registration.
|
||||
*
|
||||
* When the registry is closed, the system accepts adding new transformer names and throws an error when trying to
|
||||
* register a transformer.
|
||||
*
|
||||
* When the registry is open, the system will throw an error if a transformer name is added and will accept registering
|
||||
* transformers to be applied.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
let registryOpened = false;
|
||||
|
||||
/**
|
||||
* Freezes the valid transformers list and open the registry to accept new transform registrations.
|
||||
*
|
||||
* INTERNAL API: to be used only in `initializers/freeze-valid-transformers`
|
||||
*/
|
||||
export function _freezeValidTransformerNames() {
|
||||
registryOpened = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new valid transformer name.
|
||||
*
|
||||
* INTERNAL API: use pluginApi.addValueTransformerName instead.
|
||||
*
|
||||
* DO NOT USE THIS FUNCTION TO ADD CORE TRANSFORMER NAMES. Instead register them directly in the
|
||||
* validCoreTransformerNames set above.
|
||||
*
|
||||
* @param {string} name the name to register
|
||||
*/
|
||||
export function _addTransformerName(name) {
|
||||
if (registryOpened) {
|
||||
throw new Error(
|
||||
"api.registerValueTransformer was called when the system is no longer accepting new names to be added.\n" +
|
||||
`Move your code to a pre-initializer that runs before "freeze-valid-transformers" to avoid this error.`
|
||||
);
|
||||
}
|
||||
|
||||
if (validCoreTransformerNames.has(name)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`api.addValueTransformerName: transformer "${name}" matches an existing core transformer and shouldn't be re-registered using the the API.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (validPluginTransformerNames.has(name)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`api.addValueTransformerName: transformer "${name}" is already registered.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
validPluginTransformerNames.add(name.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a value transformer.
|
||||
*
|
||||
* INTERNAL API: use pluginApi.registerValueTransformer instead.
|
||||
*
|
||||
* @param {string} transformerName the name of the transformer
|
||||
* @param {function({value, context})} callback callback that will transform the value.
|
||||
*/
|
||||
export function _registerTransformer(transformerName, callback) {
|
||||
if (!registryOpened) {
|
||||
throw new Error(
|
||||
"api.registerValueTransformer was called while the system was still accepting new transformer names to be added.\n" +
|
||||
`Move your code to an initializer or a pre-initializer that runs after "freeze-valid-transformers" to avoid this error.`
|
||||
);
|
||||
}
|
||||
|
||||
if (!transformerExists(transformerName)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`api.registerValueTransformer: transformer "${transformerName}" is unknown and will be ignored. ` +
|
||||
"Perhaps you misspelled it?"
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof callback !== "function") {
|
||||
throw new Error(
|
||||
"api.registerValueTransformer requires the valueCallback argument to be a function"
|
||||
);
|
||||
}
|
||||
|
||||
const existingTransformers = transformersRegistry.get(transformerName) || [];
|
||||
|
||||
existingTransformers.push(callback);
|
||||
|
||||
transformersRegistry.set(transformerName, existingTransformers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a transformer to a value
|
||||
*
|
||||
* @param {string} transformerName the name of the transformer applied
|
||||
* @param {*} defaultValue the default value
|
||||
* @param {*} [context] the optional context to pass to the transformer callbacks.
|
||||
*
|
||||
* @returns {*} the transformed value
|
||||
*/
|
||||
export function applyValueTransformer(transformerName, defaultValue, context) {
|
||||
if (!transformerExists(transformerName)) {
|
||||
throw new Error(
|
||||
`applyValueTransformer: transformer name "${transformerName}" does not exist. Did you forget to register it?`
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
typeof (context ?? undefined) !== "undefined" &&
|
||||
!(typeof context === "object" && context.constructor === Object)
|
||||
) {
|
||||
throw (
|
||||
`applyValueTransformer("${transformerName}", ...): context must be a simple JS object or nullish.\n` +
|
||||
"Avoid passing complex objects in the context, like for example, component instances or objects that carry " +
|
||||
"mutable state directly. This can induce users to registry transformers with callbacks causing side effects " +
|
||||
"and mutating the context directly. Inevitably, this leads to fragile integrations."
|
||||
);
|
||||
}
|
||||
|
||||
const transformers = transformersRegistry.get(transformerName);
|
||||
if (!transformers) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
let newValue = defaultValue;
|
||||
|
||||
const transformerPoolSize = transformers.length;
|
||||
for (let i = 0; i < transformerPoolSize; i++) {
|
||||
const valueCallback = transformers[i];
|
||||
newValue = valueCallback({ value: newValue, context });
|
||||
}
|
||||
|
||||
return newValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a transformer name exists
|
||||
*
|
||||
* @param {string} name the name to check
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function transformerExists(name) {
|
||||
return (
|
||||
validCoreTransformerNames.has(name) || validPluginTransformerNames.has(name)
|
||||
);
|
||||
}
|
||||
|
||||
///////// Testing helpers
|
||||
|
||||
/**
|
||||
* Stores the initial state of `registryOpened` to allow the correct reset after a test that needs to manually
|
||||
* override the registry opened state finishes running.
|
||||
*
|
||||
* @type {boolean | null}
|
||||
*/
|
||||
let testRegistryOpenedState = null; // initially set to null bto allow testing if it was initialized
|
||||
|
||||
/**
|
||||
* Opens the transformers registry for registration
|
||||
*
|
||||
* USE ONLY FOR TESTING PURPOSES.
|
||||
*/
|
||||
export function acceptNewTransformerNames() {
|
||||
if (!isTesting()) {
|
||||
throw new Error("Use `acceptNewTransformerNames` only in tests.");
|
||||
}
|
||||
|
||||
if (testRegistryOpenedState === null) {
|
||||
testRegistryOpenedState = registryOpened;
|
||||
}
|
||||
|
||||
registryOpened = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the transformers registry for registration
|
||||
*
|
||||
* USE ONLY FOR TESTING PURPOSES.
|
||||
*/
|
||||
export function acceptTransformerRegistrations() {
|
||||
if (!isTesting()) {
|
||||
throw new Error("Use `acceptTransformerRegistrations` only in tests.");
|
||||
}
|
||||
|
||||
if (testRegistryOpenedState === null) {
|
||||
testRegistryOpenedState = registryOpened;
|
||||
}
|
||||
|
||||
registryOpened = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the transformers initial state
|
||||
*
|
||||
* USE ONLY FOR TESTING PURPOSES.
|
||||
*/
|
||||
export function resetTransformers() {
|
||||
if (!isTesting()) {
|
||||
throw new Error("Use `resetTransformers` only in tests.");
|
||||
}
|
||||
|
||||
if (testRegistryOpenedState !== null) {
|
||||
registryOpened = testRegistryOpenedState;
|
||||
}
|
||||
|
||||
validPluginTransformerNames.clear();
|
||||
transformersRegistry.clear();
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export const VALUE_TRANSFORMERS = Object.freeze([
|
||||
// use only lowercase names
|
||||
"header-notifications-avatar-size",
|
||||
"home-logo-href",
|
||||
]);
|
|
@ -1,22 +1,13 @@
|
|||
// deprecated in favor of components/header/home-logo.gjs
|
||||
import { h } from "virtual-dom";
|
||||
import { wantsNewWindow } from "discourse/lib/intercept-click";
|
||||
import { applyValueTransformer } from "discourse/lib/transformer";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import Session from "discourse/models/session";
|
||||
import { createWidget } from "discourse/widgets/widget";
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
import { iconNode } from "discourse-common/lib/icon-library";
|
||||
|
||||
let hrefCallback;
|
||||
|
||||
export function registerHomeLogoHrefCallback(callback) {
|
||||
hrefCallback = callback;
|
||||
}
|
||||
|
||||
export function clearHomeLogoHrefCallback() {
|
||||
hrefCallback = null;
|
||||
}
|
||||
|
||||
export default createWidget("home-logo", {
|
||||
services: ["session"],
|
||||
tagName: "div.title",
|
||||
|
@ -34,11 +25,10 @@ export default createWidget("home-logo", {
|
|||
href() {
|
||||
const href = this.settings.href;
|
||||
|
||||
if (hrefCallback) {
|
||||
return hrefCallback();
|
||||
}
|
||||
|
||||
return typeof href === "function" ? href() : href;
|
||||
return applyValueTransformer(
|
||||
"home-logo-href",
|
||||
typeof href === "function" ? href() : href
|
||||
);
|
||||
},
|
||||
|
||||
logoUrl(opts = {}) {
|
||||
|
|
|
@ -65,6 +65,7 @@ import {
|
|||
resetHighestReadCache,
|
||||
setTopicList,
|
||||
} from "discourse/lib/topic-list-tracker";
|
||||
import { resetTransformers } from "discourse/lib/transformer";
|
||||
import { clearRewrites } from "discourse/lib/url";
|
||||
import { resetUserMenuTabs } from "discourse/lib/user-menu/tab";
|
||||
import {
|
||||
|
@ -246,6 +247,7 @@ export function testCleanup(container, app) {
|
|||
clearPopupMenuOptions();
|
||||
clearAdditionalAdminSidebarSectionLinks();
|
||||
resetAdminPluginConfigNav();
|
||||
resetTransformers();
|
||||
}
|
||||
|
||||
function cleanupCssGeneratorTags() {
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
import { getOwner } from "@ember/application";
|
||||
import { render } from "@ember/test-helpers";
|
||||
import { module, test } from "qunit";
|
||||
import HomeLogo, {
|
||||
clearHomeLogoHrefCallback as clearComponentHomeLogoHrefCallback,
|
||||
} from "discourse/components/header/home-logo";
|
||||
import HomeLogo from "discourse/components/header/home-logo";
|
||||
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import { query } from "discourse/tests/helpers/qunit-helpers";
|
||||
import { clearHomeLogoHrefCallback as clearWidgetHomeLogoHrefCallback } from "discourse/widgets/home-logo";
|
||||
|
||||
const bigLogo = "/images/d-logo-sketch.png?test";
|
||||
const smallLogo = "/images/d-logo-sketch-small.png?test";
|
||||
|
@ -23,8 +20,6 @@ module("Integration | Component | home-logo", function (hooks) {
|
|||
this.session = getOwner(this).lookup("service:session");
|
||||
this.session.set("darkModeAvailable", null);
|
||||
this.session.set("defaultColorSchemeIsDark", null);
|
||||
clearWidgetHomeLogoHrefCallback();
|
||||
clearComponentHomeLogoHrefCallback();
|
||||
});
|
||||
|
||||
test("basics", async function (assert) {
|
||||
|
|
|
@ -2,12 +2,10 @@
|
|||
import { getOwner } from "@ember/application";
|
||||
import { render } from "@ember/test-helpers";
|
||||
import { module, test } from "qunit";
|
||||
import { clearHomeLogoHrefCallback as clearComponentHomeLogoHrefCallback } from "discourse/components/header/home-logo";
|
||||
import MountWidget from "discourse/components/mount-widget";
|
||||
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import { count, exists, query } from "discourse/tests/helpers/qunit-helpers";
|
||||
import { clearHomeLogoHrefCallback as clearWidgetHomeLogoHrefCallback } from "discourse/widgets/home-logo";
|
||||
|
||||
const bigLogo = "/images/d-logo-sketch.png?test";
|
||||
const smallLogo = "/images/d-logo-sketch-small.png?test";
|
||||
|
@ -23,8 +21,6 @@ module("Integration | Component | Widget | home-logo", function (hooks) {
|
|||
this.session = getOwner(this).lookup("service:session");
|
||||
this.session.set("darkModeAvailable", null);
|
||||
this.session.set("defaultColorSchemeIsDark", null);
|
||||
clearWidgetHomeLogoHrefCallback();
|
||||
clearComponentHomeLogoHrefCallback();
|
||||
});
|
||||
|
||||
test("basics", async function (assert) {
|
||||
|
|
|
@ -0,0 +1,410 @@
|
|||
import EmberObject from "@ember/object";
|
||||
import { setupTest } from "ember-qunit";
|
||||
import { module, test } from "qunit";
|
||||
import sinon from "sinon";
|
||||
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||
import {
|
||||
acceptNewTransformerNames,
|
||||
acceptTransformerRegistrations,
|
||||
applyValueTransformer,
|
||||
transformerExists,
|
||||
} from "discourse/lib/transformer";
|
||||
|
||||
module("Unit | Utility | transformers", function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
module("pluginApi.addValueTransformerName", function (innerHooks) {
|
||||
innerHooks.beforeEach(function () {
|
||||
this.consoleWarnStub = sinon.stub(console, "warn");
|
||||
});
|
||||
|
||||
innerHooks.afterEach(function () {
|
||||
this.consoleWarnStub.restore();
|
||||
});
|
||||
|
||||
test("raises an exception if the system is already accepting transformers to registered", function (assert) {
|
||||
// there is no need to freeze the list of valid transformers because that happen when the test application is
|
||||
// initialized in `setupTest`
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
withPluginApi("1.34.0", (api) => {
|
||||
api.addValueTransformerName("whatever");
|
||||
}),
|
||||
/was called when the system is no longer accepting new names to be added/
|
||||
);
|
||||
});
|
||||
|
||||
test("warns if name is already registered", function (assert) {
|
||||
acceptNewTransformerNames();
|
||||
|
||||
withPluginApi("1.34.0", (api) => {
|
||||
api.addValueTransformerName("home-logo-href"); // existing core transformer
|
||||
|
||||
// testing warning about core transformers
|
||||
assert.strictEqual(
|
||||
this.consoleWarnStub.calledWith(
|
||||
sinon.match(/matches an existing core transformer/)
|
||||
),
|
||||
true,
|
||||
"logs warning to the console about existing core transformer with the same name"
|
||||
);
|
||||
|
||||
// testing warning about plugin transformers
|
||||
this.consoleWarnStub.reset();
|
||||
|
||||
api.addValueTransformerName("new-plugin-transformer"); // first time should go through
|
||||
assert.strictEqual(
|
||||
this.consoleWarnStub.notCalled,
|
||||
true,
|
||||
"did not log warning to the console"
|
||||
);
|
||||
|
||||
api.addValueTransformerName("new-plugin-transformer"); // second time log a warning
|
||||
|
||||
assert.strictEqual(
|
||||
this.consoleWarnStub.calledWith(sinon.match(/is already registered/)),
|
||||
true,
|
||||
"logs warning to the console about transformer already added with the same name"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("adds a new transformer name", function (assert) {
|
||||
acceptNewTransformerNames();
|
||||
|
||||
withPluginApi("1.34.0", (api) => {
|
||||
assert.strictEqual(
|
||||
transformerExists("a-new-plugin-transformer"),
|
||||
false,
|
||||
"initially the transformer does not exists"
|
||||
);
|
||||
api.addValueTransformerName("a-new-plugin-transformer"); // second time log a warning
|
||||
assert.strictEqual(
|
||||
transformerExists("a-new-plugin-transformer"),
|
||||
true,
|
||||
"the new transformer was added"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module("pluginApi.registerValueTransformer", function (innerHooks) {
|
||||
innerHooks.beforeEach(function () {
|
||||
this.consoleWarnStub = sinon.stub(console, "warn");
|
||||
});
|
||||
|
||||
innerHooks.afterEach(function () {
|
||||
this.consoleWarnStub.restore();
|
||||
});
|
||||
|
||||
test("raises an exception if the application the system is still waiting for transformer names to be registered", function (assert) {
|
||||
acceptNewTransformerNames();
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
withPluginApi("1.34.0", (api) => {
|
||||
api.registerValueTransformer("whatever", () => "foo"); // the name doesn't really matter at this point
|
||||
}),
|
||||
/was called while the system was still accepting new transformer names/
|
||||
);
|
||||
});
|
||||
|
||||
test("warns if transformer is unknown", function (assert) {
|
||||
withPluginApi("1.34.0", (api) => {
|
||||
api.registerValueTransformer("whatever", () => "foo");
|
||||
|
||||
// testing warning about core transformers
|
||||
assert.strictEqual(
|
||||
this.consoleWarnStub.calledWith(
|
||||
sinon.match(/is unknown and will be ignored/)
|
||||
),
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("raises an exception if the callback parameter is not a function", function (assert) {
|
||||
assert.throws(
|
||||
() =>
|
||||
withPluginApi("1.34.0", (api) => {
|
||||
api.registerValueTransformer("whatever", "foo");
|
||||
}),
|
||||
/requires the valueCallback argument to be a function/
|
||||
);
|
||||
});
|
||||
|
||||
test("registering a new transformer works", function (assert) {
|
||||
acceptNewTransformerNames();
|
||||
|
||||
withPluginApi("1.34.0", (api) => {
|
||||
api.addValueTransformerName("test-transformer");
|
||||
acceptTransformerRegistrations();
|
||||
|
||||
const transformerWasRegistered = (name) =>
|
||||
applyValueTransformer(name, false);
|
||||
|
||||
assert.strictEqual(
|
||||
transformerWasRegistered("test-transformer"),
|
||||
false,
|
||||
"value did not change. transformer is not registered yet"
|
||||
);
|
||||
|
||||
api.registerValueTransformer("test-transformer", () => true);
|
||||
|
||||
assert.strictEqual(
|
||||
transformerWasRegistered("test-transformer"),
|
||||
true,
|
||||
"the transformer was registered successfully. the value did change."
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module("applyValueTransformer", function (innerHooks) {
|
||||
innerHooks.beforeEach(function () {
|
||||
acceptNewTransformerNames();
|
||||
|
||||
withPluginApi("1.34.0", (api) => {
|
||||
api.addValueTransformerName("test-value1-transformer");
|
||||
api.addValueTransformerName("test-value2-transformer");
|
||||
});
|
||||
|
||||
acceptTransformerRegistrations();
|
||||
});
|
||||
|
||||
test("raises an exception if the transformer name does not exist", function (assert) {
|
||||
assert.throws(
|
||||
() => applyValueTransformer("whatever", "foo"),
|
||||
/does not exist. Did you forget to register it/
|
||||
);
|
||||
});
|
||||
|
||||
test("accepts only simple objects as context", function (assert) {
|
||||
const notThrows = (testCallback) => {
|
||||
try {
|
||||
testCallback();
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
assert.ok(
|
||||
notThrows(() =>
|
||||
applyValueTransformer("test-value1-transformer", "foo")
|
||||
),
|
||||
"it won't throw an error if context is not passed"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
notThrows(() =>
|
||||
applyValueTransformer("test-value1-transformer", "foo", undefined)
|
||||
),
|
||||
"it won't throw an error if context is undefined"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
notThrows(() =>
|
||||
applyValueTransformer("test-value1-transformer", "foo", null)
|
||||
),
|
||||
"it won't throw an error if context is null"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
notThrows(() =>
|
||||
applyValueTransformer("test-value1-transformer", "foo", {
|
||||
pojo: true,
|
||||
property: "foo",
|
||||
})
|
||||
),
|
||||
"it won't throw an error if context is a POJO"
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() => applyValueTransformer("test-value1-transformer", "foo", ""),
|
||||
/context must be a simple JS object/,
|
||||
"it will throw an error if context is a string"
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() => applyValueTransformer("test-value1-transformer", "foo", 0),
|
||||
/context must be a simple JS object/,
|
||||
"it will throw an error if context is a number"
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() => applyValueTransformer("test-value1-transformer", "foo", false),
|
||||
/context must be a simple JS object/,
|
||||
"it will throw an error if context is a boolean value"
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
applyValueTransformer(
|
||||
"test-value1-transformer",
|
||||
"foo",
|
||||
() => "function"
|
||||
),
|
||||
/context must be a simple JS object/,
|
||||
"it will throw an error if context is a function"
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
applyValueTransformer(
|
||||
"test-value1-transformer",
|
||||
"foo",
|
||||
EmberObject.create({
|
||||
test: true,
|
||||
})
|
||||
),
|
||||
/context must be a simple JS object/,
|
||||
"it will throw an error if context is an Ember object"
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
applyValueTransformer(
|
||||
"test-value1-transformer",
|
||||
"foo",
|
||||
EmberObject.create({
|
||||
test: true,
|
||||
})
|
||||
),
|
||||
/context must be a simple JS object/,
|
||||
"it will throw an error if context is an Ember component"
|
||||
);
|
||||
|
||||
class Testable {}
|
||||
assert.throws(
|
||||
() =>
|
||||
applyValueTransformer(
|
||||
"test-value1-transformer",
|
||||
"foo",
|
||||
new Testable()
|
||||
),
|
||||
/context must be a simple JS object/,
|
||||
"it will throw an error if context is an instance of a class"
|
||||
);
|
||||
});
|
||||
|
||||
test("applying the transformer works", function (assert) {
|
||||
class Testable {
|
||||
#value;
|
||||
|
||||
constructor(value) {
|
||||
this.#value = value;
|
||||
}
|
||||
|
||||
get value1() {
|
||||
return applyValueTransformer("test-value1-transformer", this.#value);
|
||||
}
|
||||
|
||||
get value2() {
|
||||
return applyValueTransformer("test-value2-transformer", this.#value);
|
||||
}
|
||||
}
|
||||
|
||||
const testObject1 = new Testable(1);
|
||||
const testObject2 = new Testable(2);
|
||||
|
||||
assert.deepEqual(
|
||||
[
|
||||
testObject1.value1,
|
||||
testObject1.value2,
|
||||
testObject2.value1,
|
||||
testObject2.value2,
|
||||
],
|
||||
[1, 1, 2, 2],
|
||||
"it returns the default values when there are no transformers registered"
|
||||
);
|
||||
|
||||
withPluginApi("1.34.0", (api) => {
|
||||
api.registerValueTransformer("test-value1-transformer", ({ value }) => {
|
||||
return value * 10;
|
||||
});
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
[testObject1.value1, testObject2.value1],
|
||||
[10, 20],
|
||||
"when a transformer was registered, it returns the transformed value"
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
[testObject1.value2, testObject2.value2],
|
||||
[1, 2],
|
||||
"transformer names without transformers registered are not affected"
|
||||
);
|
||||
});
|
||||
|
||||
test("the transformer callback can receive an optional context object", function (assert) {
|
||||
let expectedContext = null;
|
||||
|
||||
withPluginApi("1.34.0", (api) => {
|
||||
api.registerValueTransformer(
|
||||
"test-value1-transformer",
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
({ value, context }) => {
|
||||
expectedContext = context; // this function should be pure, but we're using side effects just for the test
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const value = applyValueTransformer("test-value1-transformer", false, {
|
||||
prop1: true,
|
||||
prop2: false,
|
||||
});
|
||||
|
||||
assert.strictEqual(value, true, "the value was transformed");
|
||||
assert.deepEqual(
|
||||
expectedContext,
|
||||
{
|
||||
prop1: true,
|
||||
prop2: false,
|
||||
},
|
||||
"the callback received the expected context"
|
||||
);
|
||||
});
|
||||
|
||||
test("multiple transformers registered for the same name will be applied in sequence", function (assert) {
|
||||
class Testable {
|
||||
get sequence() {
|
||||
return applyValueTransformer("test-value1-transformer", ["r"]);
|
||||
}
|
||||
}
|
||||
|
||||
const testObject = new Testable();
|
||||
|
||||
assert.deepEqual(
|
||||
testObject.sequence,
|
||||
["r"],
|
||||
`initially the sequence contains only the element "r"`
|
||||
);
|
||||
|
||||
withPluginApi("1.34.0", (api) => {
|
||||
api.registerValueTransformer("test-value1-transformer", ({ value }) => {
|
||||
return ["r", ...value];
|
||||
});
|
||||
api.registerValueTransformer("test-value1-transformer", ({ value }) => {
|
||||
return [...value, "e", "c"];
|
||||
});
|
||||
api.registerValueTransformer("test-value1-transformer", ({ value }) => {
|
||||
return ["o", ...value];
|
||||
});
|
||||
api.registerValueTransformer("test-value1-transformer", ({ value }) => {
|
||||
return ["c", ...value, "t"];
|
||||
});
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
testObject.sequence.join(""),
|
||||
"correct",
|
||||
`the transformers applied in the expected sequence will produce the word "correct"`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -7,6 +7,11 @@ in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.34.0] - 2024-06-06
|
||||
|
||||
- Added `registerValueTransformer` which allows registering a transformer callback to override values defined in Discourse modules
|
||||
- Added `addValueTransformerName` which allows plugins/TCs to register a new transformer to override values defined in their modules
|
||||
|
||||
## [1.33.0] - 2024-06-06
|
||||
|
||||
- Added `addCustomUserFieldValidationCallback` which allows to set a callback to change the validation and user facing message when attempting to save the signup form.
|
||||
|
|
Loading…
Reference in New Issue