DEV: Add behavior transformers (#27409)
This commit introduces the `behaviorTransformer` API to safely override behaviors defined in Discourse. Two new plugin APIs are introduced: - `addBehaviorTransformerName` which allows plugins and theme-components to add a new valid transformer name if they want to provide overridable behaviors; - `registerBehaviorTransformer` to register a transformer to override behaviors. It also introduces the function `applyBehaviorTransformer` which can be imported from `discourse/lib/transformer`. This is used to mark a callback containing the desired behavior as overridable and applies the transformer logic. How does it work? ## Marking a behavior as overridable: To mark a behavior 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 `addBehaviorTransformerName` instead. Then, in your component or class, use the function `applyBehaviorTransformer` to mark the Behavior as overridable and handle the logic: - example: ```js ... @action loadMore() { applyBehaviorTransformer( "discovery-topic-list-load-more", () => { this.documentTitle.updateContextCount(0); return this.model .loadMore() .then(({ moreTopicsUrl, newTopics } = {}) => { if ( newTopics && newTopics.length && this.bulkSelectHelper?.bulkSelectEnabled ) { this.bulkSelectHelper.addTopics(newTopics); } if (moreTopicsUrl && $(window).height() >= $(document).height()) { this.send("loadMore"); } }); }, { model: this.model } ); }, ... ``` ## Overriding a behavior in plugins or themes To override a behavior in plugins, themes, or TCs use the plugin API `registerBehaviorTransformer`: - Example: ```js withPluginApi("1.35.0", (api) => { api.registerBehaviorTransformer("example-transformer", ({ context, next }) => { console.log('we can introduce new behavior here instead', context); next(); // call next to execute the expected behavior }); }); ```
This commit is contained in:
parent
366dfec16c
commit
7b14cd98c7
|
@ -1,12 +1,15 @@
|
|||
import Component from "@ember/component";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import $ from "jquery";
|
||||
import { applyBehaviorTransformer } from "discourse/lib/transformer";
|
||||
import LoadMore from "discourse/mixins/load-more";
|
||||
import { observes, on } from "discourse-common/utils/decorators";
|
||||
|
||||
export default Component.extend(LoadMore, {
|
||||
classNames: ["contents"],
|
||||
eyelineSelector: ".topic-list-item",
|
||||
appEvents: service(),
|
||||
documentTitle: service(),
|
||||
|
||||
@on("didInsertElement")
|
||||
|
@ -32,21 +35,28 @@ export default Component.extend(LoadMore, {
|
|||
this.documentTitle.updateContextCount(this.incomingCount);
|
||||
},
|
||||
|
||||
actions: {
|
||||
loadMore() {
|
||||
this.documentTitle.updateContextCount(0);
|
||||
this.model.loadMore().then(({ moreTopicsUrl, newTopics } = {}) => {
|
||||
if (
|
||||
newTopics &&
|
||||
newTopics.length &&
|
||||
this.bulkSelectHelper?.bulkSelectEnabled
|
||||
) {
|
||||
this.bulkSelectHelper.addTopics(newTopics);
|
||||
}
|
||||
if (moreTopicsUrl && $(window).height() >= $(document).height()) {
|
||||
this.send("loadMore");
|
||||
}
|
||||
});
|
||||
},
|
||||
@action
|
||||
loadMore() {
|
||||
applyBehaviorTransformer(
|
||||
"discovery-topic-list-load-more",
|
||||
() => {
|
||||
this.documentTitle.updateContextCount(0);
|
||||
return this.model
|
||||
.loadMore()
|
||||
.then(({ moreTopicsUrl, newTopics } = {}) => {
|
||||
if (
|
||||
newTopics &&
|
||||
newTopics.length &&
|
||||
this.bulkSelectHelper?.bulkSelectEnabled
|
||||
) {
|
||||
this.bulkSelectHelper.addTopics(newTopics);
|
||||
}
|
||||
if (moreTopicsUrl && $(window).height() >= $(document).height()) {
|
||||
this.send("loadMore");
|
||||
}
|
||||
});
|
||||
},
|
||||
{ model: this.model }
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -145,7 +145,12 @@
|
|||
<PluginOutlet
|
||||
@name="after-topic-list"
|
||||
@connectorTagName="div"
|
||||
@outletArgs={{hash category=@category tag=@tag}}
|
||||
@outletArgs={{hash
|
||||
category=@category
|
||||
tag=@tag
|
||||
loadingMore=@model.loadingMore
|
||||
canLoadMore=@model.canLoadMore
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</DiscoveryTopicsList>
|
||||
|
|
|
@ -95,6 +95,7 @@ import { includeAttributes } from "discourse/lib/transform-post";
|
|||
import {
|
||||
_addTransformerName,
|
||||
_registerTransformer,
|
||||
transformerTypes,
|
||||
} from "discourse/lib/transformer";
|
||||
import { registerUserMenuTab } from "discourse/lib/user-menu/tab";
|
||||
import { replaceFormatter } from "discourse/lib/utilities";
|
||||
|
@ -155,7 +156,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.34.0";
|
||||
export const PLUGIN_API_VERSION = "1.35.0";
|
||||
|
||||
const DEPRECATED_HEADER_WIDGETS = [
|
||||
"header",
|
||||
|
@ -333,9 +334,97 @@ class PluginApi {
|
|||
}
|
||||
|
||||
/**
|
||||
* Add a new valid transformer name.
|
||||
* Add a new valid behavior transformer name.
|
||||
*
|
||||
* Use this API to add a new transformer name that can be used in the `registerValueTransformer` API.
|
||||
* Use this API to add a new behavior 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.addBehaviorTransformerName("my-unique-transformer-name");
|
||||
* }),
|
||||
* },
|
||||
* };
|
||||
*
|
||||
* @param name the name of the new transformer
|
||||
*
|
||||
*/
|
||||
addBehaviorTransformerName(name) {
|
||||
_addTransformerName(name, transformerTypes.BEHAVIOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a transformer to override behavior defined in Discourse.
|
||||
*
|
||||
* Example: to perform an action before the expected behavior
|
||||
* ```
|
||||
* api.registerBehaviorTransformer("example-transformer", ({next, context}) => {
|
||||
* exampleNewAction(); // action performed before the expected behavior
|
||||
*
|
||||
* next(); //iterates over the transformer queue processing the behaviors
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* Example: to perform an action after the expected behavior
|
||||
* ```
|
||||
* api.registerBehaviorTransformer("example-transformer", ({next, context}) => {
|
||||
* next(); //iterates over the transformer queue processing the behaviors
|
||||
*
|
||||
* exampleNewAction(); // action performed after the expected behavior
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* Example: to use a value returned by the expected behavior to decide if an action must be performed
|
||||
* ```
|
||||
* api.registerBehaviorTransformer("example-transformer", ({next, context}) => {
|
||||
* const expected = next(); //iterates over the transformer queue processing the behaviors
|
||||
*
|
||||
* if (expected === "EXPECTED") {
|
||||
* exampleNewAction(); // action performed after the expected behavior
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* Example: to abort the expected behavior based on a condition
|
||||
* ```
|
||||
* api.registerValueTransformer("example-transformer", ({next, context}) => {
|
||||
* if (context.property) {
|
||||
* // not calling next() on a behavior transformer aborts executing the expected behavior
|
||||
*
|
||||
* return;
|
||||
* }
|
||||
*
|
||||
* next();
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param {string} transformerName the name of the transformer
|
||||
* @param {function({next, context})} behaviorCallback callback to be used to transform or override the behavior.
|
||||
* @param {*} behaviorCallback.next callback that executes the remaining transformer queue producing the expected
|
||||
* behavior. Notice that this includes the default behavior and if next() is not called in your transformer's callback
|
||||
* the default behavior will be completely overridden
|
||||
* @param {*} [behaviorCallback.context] the optional context in which the behavior is being transformed
|
||||
*/
|
||||
registerBehaviorTransformer(transformerName, behaviorCallback) {
|
||||
_registerTransformer(
|
||||
transformerName,
|
||||
transformerTypes.BEHAVIOR,
|
||||
behaviorCallback
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new valid value transformer name.
|
||||
*
|
||||
* Use this API to add a new value 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:
|
||||
*
|
||||
|
@ -357,7 +446,7 @@ class PluginApi {
|
|||
*
|
||||
*/
|
||||
addValueTransformerName(name) {
|
||||
_addTransformerName(name);
|
||||
_addTransformerName(name, transformerTypes.VALUE);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -392,7 +481,11 @@ class PluginApi {
|
|||
* @param {*} [valueCallback.context] the optional context in which the value is being transformed
|
||||
*/
|
||||
registerValueTransformer(transformerName, valueCallback) {
|
||||
_registerTransformer(transformerName, valueCallback);
|
||||
_registerTransformer(
|
||||
transformerName,
|
||||
transformerTypes.VALUE,
|
||||
valueCallback
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2082,7 +2175,11 @@ class PluginApi {
|
|||
*
|
||||
*/
|
||||
registerHomeLogoHrefCallback(callback) {
|
||||
_registerTransformer("home-logo-href", ({ value }) => callback(value));
|
||||
_registerTransformer(
|
||||
"home-logo-href",
|
||||
transformerTypes.VALUE,
|
||||
({ value }) => callback(value)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,13 +1,60 @@
|
|||
import { VALUE_TRANSFORMERS } from "discourse/lib/transformer/registry";
|
||||
import { DEBUG } from "@glimmer/env";
|
||||
import { capitalize } from "@ember/string";
|
||||
import {
|
||||
BEHAVIOR_TRANSFORMERS,
|
||||
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())
|
||||
);
|
||||
const CORE_TRANSFORMER = "CORE";
|
||||
const PLUGIN_TRANSFORMER = "PLUGIN";
|
||||
|
||||
// do not add anything directly to this set, use addValueTransformerName instead
|
||||
const validPluginTransformerNames = new Set();
|
||||
export const transformerTypes = Object.freeze({
|
||||
// key and value must match
|
||||
BEHAVIOR: "BEHAVIOR",
|
||||
VALUE: "VALUE",
|
||||
});
|
||||
|
||||
/**
|
||||
* Valid core transformer names initialization.
|
||||
*
|
||||
* Some checks are performed to ensure there are no repeated names between the multiple transformer types.
|
||||
*
|
||||
* The list can be edited in `discourse/lib/transformer/registry`
|
||||
*/
|
||||
let validTransformerNames = new Map();
|
||||
|
||||
// Initialize the valid transformer names, notice that we perform some checks to ensure the transformer names are
|
||||
// correctly defined, i.e., lowercase and unique.
|
||||
[
|
||||
[BEHAVIOR_TRANSFORMERS, transformerTypes.BEHAVIOR],
|
||||
[VALUE_TRANSFORMERS, transformerTypes.VALUE],
|
||||
].forEach(([list, transformerType]) => {
|
||||
list.forEach((name) => {
|
||||
if (DEBUG) {
|
||||
if (name !== name.toLowerCase()) {
|
||||
throw new Error(
|
||||
`Transformer name "${name}" must be lowercase. Found in ${transformerType} transformers.`
|
||||
);
|
||||
}
|
||||
|
||||
const existingInfo = findTransformerInfoByName(name);
|
||||
|
||||
if (existingInfo) {
|
||||
const candidateName = `${name}/${transformerType.toLowerCase()}`;
|
||||
|
||||
throw new Error(
|
||||
`Transformer name "${candidateName}" can't be added. The transformer ${existingInfo.name} already exists.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
validTransformerNames.set(
|
||||
_normalizeTransformerName(name, transformerType),
|
||||
CORE_TRANSFORMER
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const transformersRegistry = new Map();
|
||||
|
||||
|
@ -33,78 +80,174 @@ export function _freezeValidTransformerNames() {
|
|||
registryOpened = true;
|
||||
}
|
||||
|
||||
function _normalizeTransformerName(name, type) {
|
||||
return `${name}/${type.toLowerCase()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* validTransformerNames set above.
|
||||
*
|
||||
* @param {string} name the name to register
|
||||
* @param {string} transformerType the type of the transformer being added
|
||||
*/
|
||||
export function _addTransformerName(name) {
|
||||
export function _addTransformerName(name, transformerType) {
|
||||
const apiName = `api.add${capitalize(
|
||||
transformerType.toLowerCase()
|
||||
)}TransformerName`;
|
||||
|
||||
if (name !== name.toLowerCase()) {
|
||||
throw new Error(
|
||||
`${apiName}: transformer name "${name}" must be lowercase.`
|
||||
);
|
||||
}
|
||||
|
||||
if (registryOpened) {
|
||||
throw new Error(
|
||||
"api.registerValueTransformer was called when the system is no longer accepting new names to be added.\n" +
|
||||
`${apiName} was called when the system is no longer accepting new names to be added.` +
|
||||
`Move your code to a pre-initializer that runs before "freeze-valid-transformers" to avoid this error.`
|
||||
);
|
||||
}
|
||||
|
||||
if (validCoreTransformerNames.has(name)) {
|
||||
const existingInfo = findTransformerInfoByName(name);
|
||||
|
||||
if (!existingInfo) {
|
||||
validTransformerNames.set(
|
||||
_normalizeTransformerName(name, transformerType),
|
||||
PLUGIN_TRANSFORMER
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (existingInfo.source === CORE_TRANSFORMER) {
|
||||
// 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.`
|
||||
`${apiName}: transformer "${name}" matches existing core transformer "${existingInfo.name}" 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());
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`${apiName}: transformer "${existingInfo.name}" is already registered`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a value transformer.
|
||||
* Registers a transformer.
|
||||
*
|
||||
* INTERNAL API: use pluginApi.registerValueTransformer instead.
|
||||
* INTERNAL API: use pluginApi.registerBehaviorTransformer or pluginApi.registerValueTransformer instead.
|
||||
*
|
||||
* @param {string} transformerName the name of the transformer
|
||||
* @param {function({value, context})} callback callback that will transform the value.
|
||||
* @param {string} transformerType the type of the transformer being registered
|
||||
* @param {function} callback callback that will transform the value.
|
||||
*/
|
||||
export function _registerTransformer(transformerName, callback) {
|
||||
export function _registerTransformer(
|
||||
transformerName,
|
||||
transformerType,
|
||||
callback
|
||||
) {
|
||||
if (!transformerTypes[transformerType]) {
|
||||
throw new Error(`Invalid transformer type: ${transformerType}`);
|
||||
}
|
||||
|
||||
const apiName = `api.register${capitalize(
|
||||
transformerType.toLowerCase()
|
||||
)}Transformer`;
|
||||
|
||||
if (!registryOpened) {
|
||||
throw new Error(
|
||||
"api.registerValueTransformer was called while the system was still accepting new transformer names to be added.\n" +
|
||||
`${apiName} 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)) {
|
||||
const normalizedTransformerName = _normalizeTransformerName(
|
||||
transformerName,
|
||||
transformerType
|
||||
);
|
||||
|
||||
if (!transformerNameExists(normalizedTransformerName)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`api.registerValueTransformer: transformer "${transformerName}" is unknown and will be ignored. ` +
|
||||
"Perhaps you misspelled it?"
|
||||
`${apiName}: transformer "${transformerName}" is unknown and will be ignored. ` +
|
||||
"Is the name correct? Are you using the correct API for the transformer type?"
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof callback !== "function") {
|
||||
throw new Error(
|
||||
"api.registerValueTransformer requires the valueCallback argument to be a function"
|
||||
`${apiName} requires the callback argument to be a function`
|
||||
);
|
||||
}
|
||||
|
||||
const existingTransformers = transformersRegistry.get(transformerName) || [];
|
||||
const existingTransformers =
|
||||
transformersRegistry.get(normalizedTransformerName) || [];
|
||||
|
||||
existingTransformers.push(callback);
|
||||
|
||||
transformersRegistry.set(transformerName, existingTransformers);
|
||||
transformersRegistry.set(normalizedTransformerName, existingTransformers);
|
||||
}
|
||||
|
||||
export function applyBehaviorTransformer(
|
||||
transformerName,
|
||||
defaultCallback,
|
||||
context
|
||||
) {
|
||||
const normalizedTransformerName = _normalizeTransformerName(
|
||||
transformerName,
|
||||
transformerTypes.BEHAVIOR
|
||||
);
|
||||
|
||||
if (!transformerNameExists(normalizedTransformerName)) {
|
||||
throw new Error(
|
||||
`applyBehaviorTransformer: transformer name "${transformerName}" does not exist. ` +
|
||||
"Was the transformer name properly added? Is the transformer name correct? Is the type equals BEHAVIOR? " +
|
||||
"applyBehaviorTransformer can only be used with BEHAVIOR transformers."
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof defaultCallback !== "function") {
|
||||
throw new Error(
|
||||
`applyBehaviorTransformer requires the callback argument to be a function`
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
typeof (context ?? undefined) !== "undefined" &&
|
||||
!(typeof context === "object" && context.constructor === Object)
|
||||
) {
|
||||
throw `applyBehaviorTransformer("${transformerName}", ...): context must be a simple JS object or nullish.`;
|
||||
}
|
||||
|
||||
const transformers = transformersRegistry.get(normalizedTransformerName);
|
||||
const appliedContext = { ...context };
|
||||
if (!appliedContext._unstable_self && this) {
|
||||
appliedContext._unstable_self = this;
|
||||
}
|
||||
|
||||
if (!transformers) {
|
||||
return defaultCallback({ context: appliedContext });
|
||||
}
|
||||
|
||||
const callbackQueue = [...transformers, defaultCallback];
|
||||
|
||||
function nextCallback() {
|
||||
const currentCallback = callbackQueue.shift();
|
||||
|
||||
if (!currentCallback) {
|
||||
return;
|
||||
}
|
||||
|
||||
return currentCallback({ context: appliedContext, next: nextCallback });
|
||||
}
|
||||
|
||||
return nextCallback();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -117,9 +260,16 @@ export function _registerTransformer(transformerName, callback) {
|
|||
* @returns {*} the transformed value
|
||||
*/
|
||||
export function applyValueTransformer(transformerName, defaultValue, context) {
|
||||
if (!transformerExists(transformerName)) {
|
||||
const normalizedTransformerName = _normalizeTransformerName(
|
||||
transformerName,
|
||||
transformerTypes.VALUE
|
||||
);
|
||||
|
||||
if (!transformerNameExists(normalizedTransformerName)) {
|
||||
throw new Error(
|
||||
`applyValueTransformer: transformer name "${transformerName}" does not exist. Did you forget to register it?`
|
||||
`applyValueTransformer: transformer name "${transformerName}" does not exist. ` +
|
||||
"Was the transformer name properly added? Is the transformer name correct? Is the type equals VALUE? " +
|
||||
"applyValueTransformer can only be used with VALUE transformers."
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -135,7 +285,8 @@ export function applyValueTransformer(transformerName, defaultValue, context) {
|
|||
);
|
||||
}
|
||||
|
||||
const transformers = transformersRegistry.get(transformerName);
|
||||
const transformers = transformersRegistry.get(normalizedTransformerName);
|
||||
|
||||
if (!transformers) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
@ -151,20 +302,56 @@ export function applyValueTransformer(transformerName, defaultValue, context) {
|
|||
return newValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} TransformerInfo
|
||||
* @property {string} name - The normalized name of the transformer
|
||||
* @property {string} source - The source of the transformer
|
||||
*/
|
||||
|
||||
/**
|
||||
* Find a transformer info by name, without considering the type
|
||||
*
|
||||
* @param name the name of the transformer
|
||||
*
|
||||
* @returns {TransformerInfo | null} info the transformer info or null if not found
|
||||
*/
|
||||
function findTransformerInfoByName(name) {
|
||||
for (const searchedType of Object.keys(transformerTypes)) {
|
||||
const searchedName = _normalizeTransformerName(name, searchedType);
|
||||
const source = validTransformerNames?.get(searchedName);
|
||||
|
||||
if (source) {
|
||||
return { name: searchedName, source };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a transformer name exists
|
||||
*
|
||||
* @param {string} name the name to check
|
||||
* @returns {boolean}
|
||||
* @param normalizedName the normalized name to check
|
||||
* @returns {boolean} true if the transformer name exists, false otherwise
|
||||
*/
|
||||
export function transformerExists(name) {
|
||||
return (
|
||||
validCoreTransformerNames.has(name) || validPluginTransformerNames.has(name)
|
||||
);
|
||||
function transformerNameExists(normalizedName) {
|
||||
return validTransformerNames.has(normalizedName);
|
||||
}
|
||||
|
||||
///////// Testing helpers
|
||||
|
||||
/**
|
||||
* Check if a transformer was added
|
||||
*
|
||||
* @param {string} name the name to check
|
||||
* @param {string} type the type of the transformer
|
||||
*
|
||||
* @returns {boolean} true if a transformer with the given name and type exists, false otherwise
|
||||
*/
|
||||
export function transformerWasAdded(name, type) {
|
||||
return validTransformerNames.has(_normalizeTransformerName(name, type));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
@ -221,6 +408,15 @@ export function resetTransformers() {
|
|||
registryOpened = testRegistryOpenedState;
|
||||
}
|
||||
|
||||
validPluginTransformerNames.clear();
|
||||
clearPluginTransformers();
|
||||
transformersRegistry.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all transformer names registered using the plugin API
|
||||
*/
|
||||
function clearPluginTransformers() {
|
||||
validTransformerNames = new Map(
|
||||
[...validTransformerNames].filter(([, type]) => type === CORE_TRANSFORMER)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
export const BEHAVIOR_TRANSFORMERS = Object.freeze([
|
||||
// use only lowercase names
|
||||
"discovery-topic-list-load-more",
|
||||
]);
|
||||
|
||||
export const VALUE_TRANSFORMERS = Object.freeze([
|
||||
// use only lowercase names
|
||||
"header-notifications-avatar-size",
|
||||
|
|
|
@ -6,8 +6,10 @@ import { withPluginApi } from "discourse/lib/plugin-api";
|
|||
import {
|
||||
acceptNewTransformerNames,
|
||||
acceptTransformerRegistrations,
|
||||
applyBehaviorTransformer,
|
||||
applyValueTransformer,
|
||||
transformerExists,
|
||||
transformerTypes,
|
||||
transformerWasAdded,
|
||||
} from "discourse/lib/transformer";
|
||||
|
||||
module("Unit | Utility | transformers", function (hooks) {
|
||||
|
@ -31,7 +33,7 @@ module("Unit | Utility | transformers", function (hooks) {
|
|||
withPluginApi("1.34.0", (api) => {
|
||||
api.addValueTransformerName("whatever");
|
||||
}),
|
||||
/was called when the system is no longer accepting new names to be added/
|
||||
/addValueTransformerName was called when the system is no longer accepting new names to be added/
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -44,7 +46,7 @@ module("Unit | Utility | transformers", function (hooks) {
|
|||
// testing warning about core transformers
|
||||
assert.strictEqual(
|
||||
this.consoleWarnStub.calledWith(
|
||||
sinon.match(/matches an existing core transformer/)
|
||||
sinon.match(/matches existing core transformer/)
|
||||
),
|
||||
true,
|
||||
"logs warning to the console about existing core transformer with the same name"
|
||||
|
@ -75,13 +77,19 @@ module("Unit | Utility | transformers", function (hooks) {
|
|||
|
||||
withPluginApi("1.34.0", (api) => {
|
||||
assert.strictEqual(
|
||||
transformerExists("a-new-plugin-transformer"),
|
||||
transformerWasAdded(
|
||||
"a-new-plugin-transformer",
|
||||
transformerTypes.VALUE
|
||||
),
|
||||
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"),
|
||||
transformerWasAdded(
|
||||
"a-new-plugin-transformer",
|
||||
transformerTypes.VALUE
|
||||
),
|
||||
true,
|
||||
"the new transformer was added"
|
||||
);
|
||||
|
@ -130,7 +138,7 @@ module("Unit | Utility | transformers", function (hooks) {
|
|||
withPluginApi("1.34.0", (api) => {
|
||||
api.registerValueTransformer("whatever", "foo");
|
||||
}),
|
||||
/requires the valueCallback argument to be a function/
|
||||
/api.registerValueTransformer requires the callback argument to be a function/
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -176,7 +184,7 @@ module("Unit | Utility | transformers", function (hooks) {
|
|||
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/
|
||||
/does not exist./
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -407,4 +415,825 @@ module("Unit | Utility | transformers", function (hooks) {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
module("pluginApi.addBehaviorTransformerName", 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.addBehaviorTransformerName("whatever");
|
||||
}),
|
||||
/addBehaviorTransformerName 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.addBehaviorTransformerName("home-logo-href"); // existing core transformer
|
||||
|
||||
// testing warning about core transformers
|
||||
assert.strictEqual(
|
||||
this.consoleWarnStub.calledWith(
|
||||
sinon.match(/matches 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.addBehaviorTransformerName("new-plugin-transformer"); // first time should go through
|
||||
assert.strictEqual(
|
||||
this.consoleWarnStub.notCalled,
|
||||
true,
|
||||
"did not log warning to the console"
|
||||
);
|
||||
|
||||
api.addBehaviorTransformerName("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(
|
||||
transformerWasAdded(
|
||||
"a-new-plugin-transformer",
|
||||
transformerTypes.BEHAVIOR
|
||||
),
|
||||
false,
|
||||
"initially the transformer does not exists"
|
||||
);
|
||||
api.addBehaviorTransformerName("a-new-plugin-transformer"); // second time log a warning
|
||||
assert.strictEqual(
|
||||
transformerWasAdded(
|
||||
"a-new-plugin-transformer",
|
||||
transformerTypes.BEHAVIOR
|
||||
),
|
||||
true,
|
||||
"the new transformer was added"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module("pluginApi.registerBehaviorTransformer", 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.registerBehaviorTransformer("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.registerBehaviorTransformer("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.registerBehaviorTransformer("whatever", "foo");
|
||||
}),
|
||||
/api.registerBehaviorTransformer requires the callback argument to be a function/
|
||||
);
|
||||
});
|
||||
|
||||
test("registering a new transformer works", function (assert) {
|
||||
acceptNewTransformerNames();
|
||||
|
||||
withPluginApi("1.34.0", (api) => {
|
||||
api.addBehaviorTransformerName("test-transformer");
|
||||
acceptTransformerRegistrations();
|
||||
|
||||
let value = null;
|
||||
|
||||
const transformerWasRegistered = (name) =>
|
||||
applyBehaviorTransformer(name, () => (value = "DEFAULT_CALLBACK"), {
|
||||
setValue: (v) => (value = v),
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
value,
|
||||
null,
|
||||
"value is null. behavior callback was not executed yet"
|
||||
);
|
||||
|
||||
transformerWasRegistered("test-transformer");
|
||||
assert.strictEqual(
|
||||
value,
|
||||
"DEFAULT_CALLBACK",
|
||||
"value was set by the default callback. transformer is not registered yet"
|
||||
);
|
||||
|
||||
api.registerBehaviorTransformer("test-transformer", ({ context }) =>
|
||||
context.setValue("TRANSFORMED_CALLBACK")
|
||||
);
|
||||
|
||||
transformerWasRegistered("test-transformer");
|
||||
assert.strictEqual(
|
||||
value,
|
||||
"TRANSFORMED_CALLBACK",
|
||||
"the transformer was registered successfully. the value did change."
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module("applyBehaviorTransformer", function (innerHooks) {
|
||||
innerHooks.beforeEach(function () {
|
||||
acceptNewTransformerNames();
|
||||
|
||||
withPluginApi("1.34.0", (api) => {
|
||||
api.addBehaviorTransformerName("test-behavior1-transformer");
|
||||
api.addBehaviorTransformerName("test-behavior2-transformer");
|
||||
});
|
||||
|
||||
acceptTransformerRegistrations();
|
||||
});
|
||||
|
||||
test("raises an exception if the transformer name does not exist", function (assert) {
|
||||
assert.throws(
|
||||
() => applyBehaviorTransformer("whatever", "foo"),
|
||||
/applyBehaviorTransformer: transformer name(.*)does not exist./
|
||||
);
|
||||
});
|
||||
|
||||
test("raises an exception if the callback argument provided is not a function", function (assert) {
|
||||
assert.throws(
|
||||
() => applyBehaviorTransformer("test-behavior1-transformer", "foo"),
|
||||
/requires the callback argument/
|
||||
);
|
||||
});
|
||||
|
||||
test("accepts only simple objects as context", function (assert) {
|
||||
const notThrows = (testCallback) => {
|
||||
try {
|
||||
testCallback();
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
assert.ok(
|
||||
notThrows(() =>
|
||||
applyBehaviorTransformer("test-behavior1-transformer", () => true)
|
||||
),
|
||||
"it won't throw an error if context is not passed"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
notThrows(() =>
|
||||
applyBehaviorTransformer(
|
||||
"test-behavior1-transformer",
|
||||
() => true,
|
||||
undefined
|
||||
)
|
||||
),
|
||||
"it won't throw an error if context is undefined"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
notThrows(() =>
|
||||
applyBehaviorTransformer(
|
||||
"test-behavior1-transformer",
|
||||
() => true,
|
||||
null
|
||||
)
|
||||
),
|
||||
"it won't throw an error if context is null"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
notThrows(() =>
|
||||
applyBehaviorTransformer("test-behavior1-transformer", () => true, {
|
||||
pojo: true,
|
||||
property: "foo",
|
||||
})
|
||||
),
|
||||
"it won't throw an error if context is a POJO"
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
applyBehaviorTransformer(
|
||||
"test-behavior1-transformer",
|
||||
() => true,
|
||||
""
|
||||
),
|
||||
/context must be a simple JS object/,
|
||||
"it will throw an error if context is a string"
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
applyBehaviorTransformer("test-behavior1-transformer", () => true, 0),
|
||||
/context must be a simple JS object/,
|
||||
"it will throw an error if context is a number"
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
applyBehaviorTransformer(
|
||||
"test-behavior1-transformer",
|
||||
() => true,
|
||||
false
|
||||
),
|
||||
/context must be a simple JS object/,
|
||||
"it will throw an error if context is a boolean behavior"
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
applyBehaviorTransformer(
|
||||
"test-behavior1-transformer",
|
||||
() => true,
|
||||
() => "function"
|
||||
),
|
||||
/context must be a simple JS object/,
|
||||
"it will throw an error if context is a function"
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
applyBehaviorTransformer(
|
||||
"test-behavior1-transformer",
|
||||
() => true,
|
||||
EmberObject.create({
|
||||
test: true,
|
||||
})
|
||||
),
|
||||
/context must be a simple JS object/,
|
||||
"it will throw an error if context is an Ember object"
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
applyBehaviorTransformer(
|
||||
"test-behavior1-transformer",
|
||||
() => true,
|
||||
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(
|
||||
() =>
|
||||
applyBehaviorTransformer(
|
||||
"test-behavior1-transformer",
|
||||
() => true,
|
||||
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 value() {
|
||||
return this.#value;
|
||||
}
|
||||
|
||||
multiplyValue() {
|
||||
applyBehaviorTransformer(
|
||||
"test-behavior1-transformer",
|
||||
() => {
|
||||
this.#value *= 2;
|
||||
},
|
||||
{ value: this.#value, setValue: (v) => (this.#value = v) }
|
||||
);
|
||||
}
|
||||
|
||||
incValue() {
|
||||
applyBehaviorTransformer(
|
||||
"test-behavior2-transformer",
|
||||
() => {
|
||||
this.#value += 1;
|
||||
},
|
||||
{ value: this.#value, setValue: (v) => (this.#value = v) }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const testObject1 = new Testable(1);
|
||||
testObject1.multiplyValue();
|
||||
|
||||
const testObject2 = new Testable(2);
|
||||
testObject2.multiplyValue();
|
||||
|
||||
assert.deepEqual(
|
||||
[testObject1.value, testObject2.value],
|
||||
[2, 4],
|
||||
"the default behavior doubles the value"
|
||||
);
|
||||
|
||||
withPluginApi("1.34.0", (api) => {
|
||||
api.registerBehaviorTransformer(
|
||||
"test-behavior1-transformer",
|
||||
({ context }) => {
|
||||
context.setValue(context.value * 10);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
testObject1.multiplyValue();
|
||||
testObject2.multiplyValue();
|
||||
|
||||
assert.deepEqual(
|
||||
[testObject1.value, testObject2.value],
|
||||
[20, 40],
|
||||
"when a transformer was registered, the method now performs1 transformed behavior"
|
||||
);
|
||||
|
||||
testObject1.incValue();
|
||||
testObject2.incValue();
|
||||
|
||||
assert.deepEqual(
|
||||
[testObject1.value, testObject2.value],
|
||||
[21, 41],
|
||||
"transformer names without transformers registered are not affected"
|
||||
);
|
||||
});
|
||||
|
||||
test("applying the transformer works with Promises", async function (assert) {
|
||||
function delayedValue(value) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(value);
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
|
||||
class Testable {
|
||||
#value = null;
|
||||
|
||||
get value() {
|
||||
return this.#value;
|
||||
}
|
||||
|
||||
clearValue() {
|
||||
this.#value = null;
|
||||
}
|
||||
|
||||
#asyncFetchValue() {
|
||||
return delayedValue("slow foo");
|
||||
}
|
||||
|
||||
initializeValue() {
|
||||
return applyBehaviorTransformer(
|
||||
"test-behavior1-transformer",
|
||||
() => {
|
||||
return this.#asyncFetchValue().then((v) => (this.#value = v));
|
||||
},
|
||||
{
|
||||
getValue: () => this.#value,
|
||||
setValue: (v) => (this.#value = v),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const testObject = new Testable();
|
||||
assert.deepEqual(testObject.value, null, "initially the value is null");
|
||||
|
||||
withPluginApi("1.34.0", (api) => {
|
||||
api.registerBehaviorTransformer(
|
||||
"test-behavior1-transformer",
|
||||
({ context, next }) => {
|
||||
return next()
|
||||
.then(() => delayedValue(" was too late"))
|
||||
.then((otherValue) =>
|
||||
context.setValue(context.getValue() + otherValue)
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const done = assert.async();
|
||||
testObject.initializeValue().then(() => {
|
||||
assert.deepEqual(
|
||||
testObject.value,
|
||||
"slow foo was too late",
|
||||
"the value was changed after the async behavior"
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test("applying the transformer works with async/await behavior", async function (assert) {
|
||||
async function delayedValue(value) {
|
||||
return await new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(value);
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
|
||||
class Testable {
|
||||
#value = null;
|
||||
|
||||
get value() {
|
||||
return this.#value;
|
||||
}
|
||||
|
||||
clearValue() {
|
||||
this.#value = null;
|
||||
}
|
||||
|
||||
async #asyncFetchValue() {
|
||||
return await delayedValue("slow foo");
|
||||
}
|
||||
|
||||
async initializeValue() {
|
||||
await applyBehaviorTransformer(
|
||||
"test-behavior1-transformer",
|
||||
async () => {
|
||||
this.#value = await this.#asyncFetchValue();
|
||||
},
|
||||
{
|
||||
getValue: () => this.#value,
|
||||
setValue: (v) => (this.#value = v),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const testObject = new Testable();
|
||||
assert.deepEqual(testObject.value, null, "initially the value is null");
|
||||
|
||||
await testObject.initializeValue();
|
||||
assert.deepEqual(
|
||||
testObject.value,
|
||||
"slow foo",
|
||||
"the value was changed after the async behavior"
|
||||
);
|
||||
|
||||
withPluginApi("1.34.0", (api) => {
|
||||
api.registerBehaviorTransformer(
|
||||
"test-behavior1-transformer",
|
||||
async ({ context, next }) => {
|
||||
await next();
|
||||
const otherValue = await delayedValue(" was too late");
|
||||
|
||||
context.setValue(context.getValue() + otherValue);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
testObject.clearValue();
|
||||
await testObject.initializeValue();
|
||||
assert.deepEqual(
|
||||
testObject.value,
|
||||
"slow foo was too late",
|
||||
"when a transformer was registered, the method now performs transformed behavior"
|
||||
);
|
||||
});
|
||||
|
||||
test("the transformer callback can receive an optional context object", function (assert) {
|
||||
let behavior = null;
|
||||
let expectedContext = null;
|
||||
|
||||
withPluginApi("1.34.0", (api) => {
|
||||
api.registerBehaviorTransformer(
|
||||
"test-behavior1-transformer",
|
||||
({ context }) => {
|
||||
behavior = "ALTERED";
|
||||
expectedContext = context;
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
applyBehaviorTransformer(
|
||||
"test-behavior1-transformer",
|
||||
() => (behavior = "DEFAULT"),
|
||||
{
|
||||
prop1: true,
|
||||
prop2: false,
|
||||
}
|
||||
);
|
||||
|
||||
assert.strictEqual(behavior, "ALTERED", "the behavior was transformed");
|
||||
assert.deepEqual(
|
||||
expectedContext,
|
||||
{
|
||||
prop1: true,
|
||||
prop2: false,
|
||||
},
|
||||
"the callback received the expected context"
|
||||
);
|
||||
});
|
||||
|
||||
test("the transformers can call next to keep moving through the callback queue", function (assert) {
|
||||
class Testable {
|
||||
#value = [];
|
||||
|
||||
resetValue() {
|
||||
this.#value = [];
|
||||
}
|
||||
|
||||
buildValue() {
|
||||
return applyBehaviorTransformer(
|
||||
"test-behavior1-transformer",
|
||||
() => this.#value.push("!"),
|
||||
{ pushValue: (v) => this.#value.push(v) }
|
||||
);
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.#value.join("");
|
||||
}
|
||||
}
|
||||
|
||||
const testObject = new Testable();
|
||||
testObject.buildValue();
|
||||
|
||||
assert.deepEqual(
|
||||
testObject.value,
|
||||
"!",
|
||||
`initially buildValue value only generates "!"`
|
||||
);
|
||||
|
||||
withPluginApi("1.34.0", (api) => {
|
||||
api.registerBehaviorTransformer(
|
||||
"test-behavior1-transformer",
|
||||
({ context, next }) => {
|
||||
context.pushValue("co");
|
||||
next();
|
||||
}
|
||||
);
|
||||
api.registerBehaviorTransformer(
|
||||
"test-behavior1-transformer",
|
||||
({ context, next }) => {
|
||||
context.pushValue("rr");
|
||||
next();
|
||||
}
|
||||
);
|
||||
api.registerBehaviorTransformer(
|
||||
"test-behavior1-transformer",
|
||||
({ context, next }) => {
|
||||
context.pushValue("ect");
|
||||
next();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
testObject.resetValue();
|
||||
testObject.buildValue();
|
||||
|
||||
assert.strictEqual(
|
||||
testObject.value,
|
||||
"correct!",
|
||||
`the transformers applied in the sequence will produce the word "correct!"`
|
||||
);
|
||||
});
|
||||
|
||||
test("when a transformer does not call next() the next transformers in the queue are not processed", function (assert) {
|
||||
class Testable {
|
||||
#value = [];
|
||||
|
||||
resetValue() {
|
||||
this.#value = [];
|
||||
}
|
||||
|
||||
buildValue() {
|
||||
return applyBehaviorTransformer(
|
||||
"test-behavior1-transformer",
|
||||
() => this.#value.push("!"),
|
||||
{ pushValue: (v) => this.#value.push(v) }
|
||||
);
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.#value.join("");
|
||||
}
|
||||
}
|
||||
|
||||
const testObject = new Testable();
|
||||
testObject.buildValue();
|
||||
|
||||
assert.deepEqual(
|
||||
testObject.value,
|
||||
"!",
|
||||
`initially buildValue value only generates "!"`
|
||||
);
|
||||
|
||||
withPluginApi("1.34.0", (api) => {
|
||||
api.registerBehaviorTransformer(
|
||||
"test-behavior1-transformer",
|
||||
({ context }) => {
|
||||
context.pushValue("stopped");
|
||||
}
|
||||
);
|
||||
|
||||
// the transformer below won't be called because next() is not called in the callback of the transformer above
|
||||
api.registerBehaviorTransformer(
|
||||
"test-behavior1-transformer",
|
||||
({ context, next }) => {
|
||||
context.pushValue(" at the end");
|
||||
next();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
testObject.resetValue();
|
||||
testObject.buildValue();
|
||||
|
||||
assert.strictEqual(
|
||||
testObject.value,
|
||||
"stopped",
|
||||
// if the sequence had been executed completely, it would have produced "stopped at the end!"
|
||||
`the transformers applied in the sequence will only produce the word "stopped"`
|
||||
);
|
||||
});
|
||||
|
||||
test("calling next() before the transformed behavior changes the order the queue is executed", function (assert) {
|
||||
class Testable {
|
||||
#value = [];
|
||||
|
||||
resetValue() {
|
||||
this.#value = [];
|
||||
}
|
||||
|
||||
buildValue() {
|
||||
return applyBehaviorTransformer(
|
||||
"test-behavior1-transformer",
|
||||
() => this.#value.push("!"),
|
||||
{ pushValue: (v) => this.#value.push(v) }
|
||||
);
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.#value.join(" ");
|
||||
}
|
||||
}
|
||||
|
||||
const testObject = new Testable();
|
||||
testObject.buildValue();
|
||||
|
||||
assert.deepEqual(
|
||||
testObject.value,
|
||||
"!",
|
||||
`initially buildValue value only generates "!"`
|
||||
);
|
||||
|
||||
withPluginApi("1.34.0", (api) => {
|
||||
api.registerBehaviorTransformer(
|
||||
"test-behavior1-transformer",
|
||||
({ context, next }) => {
|
||||
next();
|
||||
context.pushValue("reverted");
|
||||
}
|
||||
);
|
||||
api.registerBehaviorTransformer(
|
||||
"test-behavior1-transformer",
|
||||
({ context, next }) => {
|
||||
next();
|
||||
context.pushValue("was");
|
||||
}
|
||||
);
|
||||
api.registerBehaviorTransformer(
|
||||
"test-behavior1-transformer",
|
||||
({ context }) => {
|
||||
context.pushValue("order");
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
testObject.resetValue();
|
||||
testObject.buildValue();
|
||||
|
||||
assert.strictEqual(
|
||||
testObject.value,
|
||||
"order was reverted",
|
||||
`the transformers applied in the sequence will produce the expression "order was reverted"`
|
||||
);
|
||||
});
|
||||
|
||||
test("if `this` is set when applying the behavior transformer it is passed in the context as _unstable_self", function (assert) {
|
||||
class Testable {
|
||||
#value = [];
|
||||
|
||||
resetValue() {
|
||||
this.#value = [];
|
||||
}
|
||||
|
||||
buildValue() {
|
||||
return applyBehaviorTransformer.call(
|
||||
this,
|
||||
"test-behavior1-transformer",
|
||||
() => this.#value.push("!")
|
||||
);
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.#value.join(" ");
|
||||
}
|
||||
|
||||
pushValue(v) {
|
||||
this.#value.push(v);
|
||||
}
|
||||
}
|
||||
|
||||
const testObject = new Testable();
|
||||
testObject.buildValue();
|
||||
|
||||
assert.deepEqual(
|
||||
testObject.value,
|
||||
"!",
|
||||
`initially buildValue value only generates "!"`
|
||||
);
|
||||
|
||||
withPluginApi("1.34.0", (api) => {
|
||||
api.registerBehaviorTransformer(
|
||||
"test-behavior1-transformer",
|
||||
({ next, context }) => {
|
||||
context._unstable_self.pushValue("added");
|
||||
next();
|
||||
}
|
||||
);
|
||||
|
||||
api.registerBehaviorTransformer(
|
||||
"test-behavior1-transformer",
|
||||
({ next, context: { _unstable_self } }) => {
|
||||
_unstable_self.pushValue("other");
|
||||
next();
|
||||
}
|
||||
);
|
||||
|
||||
api.registerBehaviorTransformer(
|
||||
"test-behavior1-transformer",
|
||||
({ context: { _unstable_self } }) => {
|
||||
_unstable_self.pushValue("items");
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
testObject.resetValue();
|
||||
testObject.buildValue();
|
||||
|
||||
assert.strictEqual(
|
||||
testObject.value,
|
||||
"added other items",
|
||||
`the transformers used _unstable_self to access the component instance that called applyBehaviorTransformer`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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.35.0] - 2024-07-30
|
||||
|
||||
- Added `registerBehaviorTransformer` which allows registering a transformer callback to override behavior defined in Discourse modules
|
||||
- Added `addBehaviorTransformerName` which allows plugins/TCs to register a new transformer to override behavior defined in their modules
|
||||
|
||||
## [1.34.0] - 2024-06-06
|
||||
|
||||
- Added `registerValueTransformer` which allows registering a transformer callback to override values defined in Discourse modules
|
||||
|
|
Loading…
Reference in New Issue