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 Component from "@ember/component";
|
||||||
|
import { action } from "@ember/object";
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
import $ from "jquery";
|
import $ from "jquery";
|
||||||
|
import { applyBehaviorTransformer } from "discourse/lib/transformer";
|
||||||
import LoadMore from "discourse/mixins/load-more";
|
import LoadMore from "discourse/mixins/load-more";
|
||||||
import { observes, on } from "discourse-common/utils/decorators";
|
import { observes, on } from "discourse-common/utils/decorators";
|
||||||
|
|
||||||
export default Component.extend(LoadMore, {
|
export default Component.extend(LoadMore, {
|
||||||
classNames: ["contents"],
|
classNames: ["contents"],
|
||||||
eyelineSelector: ".topic-list-item",
|
eyelineSelector: ".topic-list-item",
|
||||||
|
appEvents: service(),
|
||||||
documentTitle: service(),
|
documentTitle: service(),
|
||||||
|
|
||||||
@on("didInsertElement")
|
@on("didInsertElement")
|
||||||
|
@ -32,21 +35,28 @@ export default Component.extend(LoadMore, {
|
||||||
this.documentTitle.updateContextCount(this.incomingCount);
|
this.documentTitle.updateContextCount(this.incomingCount);
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
@action
|
||||||
loadMore() {
|
loadMore() {
|
||||||
this.documentTitle.updateContextCount(0);
|
applyBehaviorTransformer(
|
||||||
this.model.loadMore().then(({ moreTopicsUrl, newTopics } = {}) => {
|
"discovery-topic-list-load-more",
|
||||||
if (
|
() => {
|
||||||
newTopics &&
|
this.documentTitle.updateContextCount(0);
|
||||||
newTopics.length &&
|
return this.model
|
||||||
this.bulkSelectHelper?.bulkSelectEnabled
|
.loadMore()
|
||||||
) {
|
.then(({ moreTopicsUrl, newTopics } = {}) => {
|
||||||
this.bulkSelectHelper.addTopics(newTopics);
|
if (
|
||||||
}
|
newTopics &&
|
||||||
if (moreTopicsUrl && $(window).height() >= $(document).height()) {
|
newTopics.length &&
|
||||||
this.send("loadMore");
|
this.bulkSelectHelper?.bulkSelectEnabled
|
||||||
}
|
) {
|
||||||
});
|
this.bulkSelectHelper.addTopics(newTopics);
|
||||||
},
|
}
|
||||||
|
if (moreTopicsUrl && $(window).height() >= $(document).height()) {
|
||||||
|
this.send("loadMore");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ model: this.model }
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -145,7 +145,12 @@
|
||||||
<PluginOutlet
|
<PluginOutlet
|
||||||
@name="after-topic-list"
|
@name="after-topic-list"
|
||||||
@connectorTagName="div"
|
@connectorTagName="div"
|
||||||
@outletArgs={{hash category=@category tag=@tag}}
|
@outletArgs={{hash
|
||||||
|
category=@category
|
||||||
|
tag=@tag
|
||||||
|
loadingMore=@model.loadingMore
|
||||||
|
canLoadMore=@model.canLoadMore
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</DiscoveryTopicsList>
|
</DiscoveryTopicsList>
|
||||||
|
|
|
@ -95,6 +95,7 @@ import { includeAttributes } from "discourse/lib/transform-post";
|
||||||
import {
|
import {
|
||||||
_addTransformerName,
|
_addTransformerName,
|
||||||
_registerTransformer,
|
_registerTransformer,
|
||||||
|
transformerTypes,
|
||||||
} from "discourse/lib/transformer";
|
} from "discourse/lib/transformer";
|
||||||
import { registerUserMenuTab } from "discourse/lib/user-menu/tab";
|
import { registerUserMenuTab } from "discourse/lib/user-menu/tab";
|
||||||
import { replaceFormatter } from "discourse/lib/utilities";
|
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
|
// docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md whenever you change the version
|
||||||
// using the format described at https://keepachangelog.com/en/1.0.0/.
|
// 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 = [
|
const DEPRECATED_HEADER_WIDGETS = [
|
||||||
"header",
|
"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:
|
* 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) {
|
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
|
* @param {*} [valueCallback.context] the optional context in which the value is being transformed
|
||||||
*/
|
*/
|
||||||
registerValueTransformer(transformerName, valueCallback) {
|
registerValueTransformer(transformerName, valueCallback) {
|
||||||
_registerTransformer(transformerName, valueCallback);
|
_registerTransformer(
|
||||||
|
transformerName,
|
||||||
|
transformerTypes.VALUE,
|
||||||
|
valueCallback
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2082,7 +2175,11 @@ class PluginApi {
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
registerHomeLogoHrefCallback(callback) {
|
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";
|
import { isTesting } from "discourse-common/config/environment";
|
||||||
|
|
||||||
// add core transformer names
|
const CORE_TRANSFORMER = "CORE";
|
||||||
const validCoreTransformerNames = new Set(
|
const PLUGIN_TRANSFORMER = "PLUGIN";
|
||||||
VALUE_TRANSFORMERS.map((name) => name.toLowerCase())
|
|
||||||
);
|
|
||||||
|
|
||||||
// do not add anything directly to this set, use addValueTransformerName instead
|
export const transformerTypes = Object.freeze({
|
||||||
const validPluginTransformerNames = new Set();
|
// 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();
|
const transformersRegistry = new Map();
|
||||||
|
|
||||||
|
@ -33,78 +80,174 @@ export function _freezeValidTransformerNames() {
|
||||||
registryOpened = true;
|
registryOpened = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _normalizeTransformerName(name, type) {
|
||||||
|
return `${name}/${type.toLowerCase()}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a new valid transformer name.
|
* Adds a new valid transformer name.
|
||||||
*
|
*
|
||||||
* INTERNAL API: use pluginApi.addValueTransformerName instead.
|
* INTERNAL API: use pluginApi.addValueTransformerName instead.
|
||||||
*
|
*
|
||||||
* DO NOT USE THIS FUNCTION TO ADD CORE TRANSFORMER NAMES. Instead register them directly in the
|
* 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} 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) {
|
if (registryOpened) {
|
||||||
throw new Error(
|
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.`
|
`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
|
// eslint-disable-next-line no-console
|
||||||
console.warn(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validPluginTransformerNames.has(name)) {
|
// eslint-disable-next-line no-console
|
||||||
// eslint-disable-next-line no-console
|
console.warn(
|
||||||
console.warn(
|
`${apiName}: transformer "${existingInfo.name}" is already registered`
|
||||||
`api.addValueTransformerName: transformer "${name}" is already registered.`
|
);
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
validPluginTransformerNames.add(name.toLowerCase());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 {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) {
|
if (!registryOpened) {
|
||||||
throw new Error(
|
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.`
|
`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
|
// eslint-disable-next-line no-console
|
||||||
console.warn(
|
console.warn(
|
||||||
`api.registerValueTransformer: transformer "${transformerName}" is unknown and will be ignored. ` +
|
`${apiName}: transformer "${transformerName}" is unknown and will be ignored. ` +
|
||||||
"Perhaps you misspelled it?"
|
"Is the name correct? Are you using the correct API for the transformer type?"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof callback !== "function") {
|
if (typeof callback !== "function") {
|
||||||
throw new Error(
|
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);
|
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
|
* @returns {*} the transformed value
|
||||||
*/
|
*/
|
||||||
export function applyValueTransformer(transformerName, defaultValue, context) {
|
export function applyValueTransformer(transformerName, defaultValue, context) {
|
||||||
if (!transformerExists(transformerName)) {
|
const normalizedTransformerName = _normalizeTransformerName(
|
||||||
|
transformerName,
|
||||||
|
transformerTypes.VALUE
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!transformerNameExists(normalizedTransformerName)) {
|
||||||
throw new Error(
|
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) {
|
if (!transformers) {
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
@ -151,20 +302,56 @@ export function applyValueTransformer(transformerName, defaultValue, context) {
|
||||||
return newValue;
|
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
|
* Check if a transformer name exists
|
||||||
*
|
*
|
||||||
* @param {string} name the name to check
|
* @param normalizedName the normalized name to check
|
||||||
* @returns {boolean}
|
* @returns {boolean} true if the transformer name exists, false otherwise
|
||||||
*/
|
*/
|
||||||
export function transformerExists(name) {
|
function transformerNameExists(normalizedName) {
|
||||||
return (
|
return validTransformerNames.has(normalizedName);
|
||||||
validCoreTransformerNames.has(name) || validPluginTransformerNames.has(name)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///////// Testing helpers
|
///////// 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
|
* 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.
|
* override the registry opened state finishes running.
|
||||||
|
@ -221,6 +408,15 @@ export function resetTransformers() {
|
||||||
registryOpened = testRegistryOpenedState;
|
registryOpened = testRegistryOpenedState;
|
||||||
}
|
}
|
||||||
|
|
||||||
validPluginTransformerNames.clear();
|
clearPluginTransformers();
|
||||||
transformersRegistry.clear();
|
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([
|
export const VALUE_TRANSFORMERS = Object.freeze([
|
||||||
// use only lowercase names
|
// use only lowercase names
|
||||||
"header-notifications-avatar-size",
|
"header-notifications-avatar-size",
|
||||||
|
|
|
@ -6,8 +6,10 @@ import { withPluginApi } from "discourse/lib/plugin-api";
|
||||||
import {
|
import {
|
||||||
acceptNewTransformerNames,
|
acceptNewTransformerNames,
|
||||||
acceptTransformerRegistrations,
|
acceptTransformerRegistrations,
|
||||||
|
applyBehaviorTransformer,
|
||||||
applyValueTransformer,
|
applyValueTransformer,
|
||||||
transformerExists,
|
transformerTypes,
|
||||||
|
transformerWasAdded,
|
||||||
} from "discourse/lib/transformer";
|
} from "discourse/lib/transformer";
|
||||||
|
|
||||||
module("Unit | Utility | transformers", function (hooks) {
|
module("Unit | Utility | transformers", function (hooks) {
|
||||||
|
@ -31,7 +33,7 @@ module("Unit | Utility | transformers", function (hooks) {
|
||||||
withPluginApi("1.34.0", (api) => {
|
withPluginApi("1.34.0", (api) => {
|
||||||
api.addValueTransformerName("whatever");
|
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
|
// testing warning about core transformers
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
this.consoleWarnStub.calledWith(
|
this.consoleWarnStub.calledWith(
|
||||||
sinon.match(/matches an existing core transformer/)
|
sinon.match(/matches existing core transformer/)
|
||||||
),
|
),
|
||||||
true,
|
true,
|
||||||
"logs warning to the console about existing core transformer with the same name"
|
"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) => {
|
withPluginApi("1.34.0", (api) => {
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
transformerExists("a-new-plugin-transformer"),
|
transformerWasAdded(
|
||||||
|
"a-new-plugin-transformer",
|
||||||
|
transformerTypes.VALUE
|
||||||
|
),
|
||||||
false,
|
false,
|
||||||
"initially the transformer does not exists"
|
"initially the transformer does not exists"
|
||||||
);
|
);
|
||||||
api.addValueTransformerName("a-new-plugin-transformer"); // second time log a warning
|
api.addValueTransformerName("a-new-plugin-transformer"); // second time log a warning
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
transformerExists("a-new-plugin-transformer"),
|
transformerWasAdded(
|
||||||
|
"a-new-plugin-transformer",
|
||||||
|
transformerTypes.VALUE
|
||||||
|
),
|
||||||
true,
|
true,
|
||||||
"the new transformer was added"
|
"the new transformer was added"
|
||||||
);
|
);
|
||||||
|
@ -130,7 +138,7 @@ module("Unit | Utility | transformers", function (hooks) {
|
||||||
withPluginApi("1.34.0", (api) => {
|
withPluginApi("1.34.0", (api) => {
|
||||||
api.registerValueTransformer("whatever", "foo");
|
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) {
|
test("raises an exception if the transformer name does not exist", function (assert) {
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => applyValueTransformer("whatever", "foo"),
|
() => 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/),
|
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).
|
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
|
## [1.34.0] - 2024-06-06
|
||||||
|
|
||||||
- Added `registerValueTransformer` which allows registering a transformer callback to override values defined in Discourse modules
|
- Added `registerValueTransformer` which allows registering a transformer callback to override values defined in Discourse modules
|
||||||
|
|
Loading…
Reference in New Issue