From 49060e6686e1b89dc4c3ca9fd4806f3a66cf55dc Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Tue, 20 Aug 2024 12:36:45 +1000 Subject: [PATCH] UI nested instead (#5125) * Support multiple calls to PluginApi.patch.instead for a component. Allow calling the original/chained function from the hook function. * Add example of new usage of instead * Update documentation --- .../react-component/src/testReact.scss | 13 +++++- .../react-component/src/testReact.tsx | 8 ++++ ui/v2.5/src/docs/en/Manual/UIPluginApi.md | 4 +- ui/v2.5/src/patch.tsx | 42 ++++++++++++++++--- 4 files changed, 59 insertions(+), 8 deletions(-) diff --git a/pkg/plugin/examples/react-component/src/testReact.scss b/pkg/plugin/examples/react-component/src/testReact.scss index 2ca6631b8..695473795 100644 --- a/pkg/plugin/examples/react-component/src/testReact.scss +++ b/pkg/plugin/examples/react-component/src/testReact.scss @@ -33,4 +33,15 @@ .scene-performer-popover .image-thumbnail { margin: 1em; } - \ No newline at end of file + +.example-react-component-custom-overlay { + display: block; + font-weight: 900; + height: 100%; + opacity: 0.25; + position: absolute; + text-align: center; + top: 0; + width: 100%; + z-index: 8; +} \ No newline at end of file diff --git a/pkg/plugin/examples/react-component/src/testReact.tsx b/pkg/plugin/examples/react-component/src/testReact.tsx index 127920eff..c29f9c3dd 100644 --- a/pkg/plugin/examples/react-component/src/testReact.tsx +++ b/pkg/plugin/examples/react-component/src/testReact.tsx @@ -132,10 +132,18 @@ interface IPluginApi { ); } + function Overlays() { + return Custom overlay; + } + PluginApi.patch.instead("SceneCard.Details", function (props: any, _: any, original: any) { return ; }); + PluginApi.patch.instead("SceneCard.Overlays", function (props: any, _: any, original: (props: any) => any) { + return <>{original({...props})}; + }); + const TestPage: React.FC = () => { const componentsLoading = PluginApi.hooks.useLoadComponents([PluginApi.loadableComponents.SceneCard]); diff --git a/ui/v2.5/src/docs/en/Manual/UIPluginApi.md b/ui/v2.5/src/docs/en/Manual/UIPluginApi.md index fd96cc52d..1357e86cc 100644 --- a/ui/v2.5/src/docs/en/Manual/UIPluginApi.md +++ b/ui/v2.5/src/docs/en/Manual/UIPluginApi.md @@ -117,12 +117,12 @@ Returns `void`. #### `PluginApi.patch.instead` -Registers a replacement function for a component. The provided function will be called with the arguments passed to the original render function, plus the original render function as the last argument. An error will be thrown if the component already has a replacement function registered. +Registers a replacement function for a component. The provided function will be called with the arguments passed to the original render function, plus the next render function as the last argument. Replacement functions will be called in the order that they are registered. If a replacement function does not call the next render function then the following replacement functions will not be called or applied. | Parameter | Type | Description | |-----------|------|-------------| | `component` | `string` | The name of the component to patch. | -| `fn` | `Function` | The replacement function. It accepts the same arguments as the original render function, plus the original render function, and is expected to return the replacement component. | +| `fn` | `Function` | The replacement function. It accepts the same arguments as the original render function, plus the next render function, and is expected to return the replacement component. | Returns `void`. diff --git a/ui/v2.5/src/patch.tsx b/ui/v2.5/src/patch.tsx index 83b9ef4fe..7f329b89a 100644 --- a/ui/v2.5/src/patch.tsx +++ b/ui/v2.5/src/patch.tsx @@ -10,7 +10,7 @@ export const components: Record = { }; const beforeFns: Record = {}; -const insteadFns: Record = {}; +const insteadFns: Record = {}; const afterFns: Record = {}; // patch functions @@ -23,11 +23,14 @@ export function before(component: string, fn: Function) { beforeFns[component].push(fn); } +// registers a patch to a function. Instead functions receive the original arguments, +// plus the next function to call. In order for all instead functions to be called, +// it is expected that the provided next() function will be called. export function instead(component: string, fn: Function) { - if (insteadFns[component]) { - throw new Error("instead has already been called for " + component); + if (!insteadFns[component]) { + insteadFns[component] = []; } - insteadFns[component] = fn; + insteadFns[component].push(fn); } export function after(component: string, fn: Function) { @@ -51,6 +54,35 @@ export function RegisterComponent( return fn; } +/* eslint-disable @typescript-eslint/no-explicit-any */ +function runInstead( + fns: Function[], + targetFn: Function, + thisArg: any, + argArray: any[] +) { + if (!fns.length) { + return targetFn.apply(thisArg, argArray); + } + + let i = 1; + function next(): any { + if (i >= fns.length) { + return targetFn; + } + + const thisTarget = fns[i++]; + return new Proxy(thisTarget, { + apply: function (target, ctx, args) { + return target.apply(ctx, args.concat(next())); + }, + }); + } + + return fns[0].apply(thisArg, argArray.concat(next())); +} +/* eslint-enable @typescript-eslint/no-explicit-any */ + // patches a function to implement the before/instead/after functionality export function PatchFunction(name: string, fn: T) { return new Proxy(fn, { @@ -61,7 +93,7 @@ export function PatchFunction(name: string, fn: T) { args = beforeFn.apply(ctx, args); } if (insteadFns[name]) { - result = insteadFns[name].apply(ctx, args.concat(target)); + result = runInstead(insteadFns[name], target, ctx, args); } else { result = target.apply(ctx, args); }