mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-02 19:45:51 +01:00
Currently, the only way to explore available extension points is through our documentation or by browsing the source code. Once you find them, there is no guard rails and the usage is prone to user errors. This new api allows the declaration of extension points as types in code. This way, exposing an extension point is as easy as exporting it from a module. Both the implementation and the developer who uses the extension point work with the same shared type that allows auto-completion and type-checks for safety. This feature is backwards-compatible as the generic methods all have sensible defaults for the type parameters. Co-authored-by: Sebastian Sdorra <sebastian.sdorra@cloudogu.com> Co-authored-by: Eduard Heimbuch <eduard.heimbuch@cloudogu.com>
144 lines
4.4 KiB
Markdown
144 lines
4.4 KiB
Markdown
---
|
|
title: UI-Extensions
|
|
subtitle: How to extend the SCM-Manager UI with plugins
|
|
---
|
|
|
|
UI-Extensions contains the building blocks for the [SCM-Manager](https://scm-manager.org) ui extension system.
|
|
|
|
## Extensions and ExtensionPoints
|
|
|
|
Extension points are spots in the ui, where the ui could be extended or modified.
|
|
An extension point requires a unique name and is represented as [React](https://reactjs.org/) component.
|
|
|
|
Example:
|
|
|
|
```xml
|
|
<div>
|
|
<h2>Repository</h2>
|
|
<ExtensionPoint name="repo.details" />
|
|
</div>
|
|
```
|
|
|
|
We can register an extension, in the form of a [React](https://reactjs.org/) component, to the "repo.details" extension point, by using the binder:
|
|
|
|
```javascript
|
|
import { binder } from "@scm-manager/ui-extensions";
|
|
|
|
const Rtfm = () => {
|
|
return <strong>Read the f*** manual</strong>;
|
|
};
|
|
|
|
binder.bind("repo.details", Rtfm);
|
|
```
|
|
|
|
The ExtensionPoint will now find and render the Rtfm component.
|
|
|
|
### Render multiple extensions
|
|
|
|
An extension point can render multiple extensions at one. This can be done with the renderAll parameter:
|
|
|
|
|
|
```javascript
|
|
<div>
|
|
<h2>Repository</h2>
|
|
<ExtensionPoint name="repo.details" renderAll={true} />
|
|
</div>
|
|
```
|
|
|
|
Now we can bind multiple components to the same extension point.
|
|
|
|
```javascript
|
|
const Rtfm = () => {
|
|
return <strong>Read the f*** manual</strong>;
|
|
};
|
|
|
|
const RealyRtfm = () => {
|
|
return <h1>Read the f*** manual</h1>;
|
|
};
|
|
|
|
binder.bind("repo.details", Rtfm);
|
|
binder.bind("repo.details", RealyRtfm);
|
|
```
|
|
|
|
### Passing props to extensions
|
|
|
|
An extension point author can pass React properties to the extensions. This can be done with the `props` property:
|
|
|
|
```javascript
|
|
<div>
|
|
<ExtensionPoint name="repo.title" props={{name: "myrepo"}} />
|
|
</div>
|
|
```
|
|
|
|
The extension becomes now the defined react properties as input:
|
|
|
|
```javascript
|
|
const Title = (props) => {
|
|
return <h1>Repository {props.name}</h1>;
|
|
};
|
|
|
|
binder.bind("repo.title", Title);
|
|
```
|
|
|
|
### Defaults
|
|
|
|
An ExtensionPoint is able to render a default, if no extension is bound to the ExtensionPoint.
|
|
The default can be passed as React children:
|
|
|
|
```javascript
|
|
<ExtensionPoint name="repo.title">
|
|
<h1>Default Title</h1>
|
|
</ExtensionPoint>
|
|
```
|
|
|
|
### Conditional rendering
|
|
|
|
An extension can specify a predicate function to the binder.
|
|
This function becomes the props of the ExtensionPoint as input and only if the predicate returns true the extension will be rendered:
|
|
|
|
```javascript
|
|
const GitAvatar = () => {
|
|
return <img src="/git/avatar.png" alt="git avatar" />;
|
|
};
|
|
|
|
binder.bind("repo.avatar", GitAvatar, (props) => props.type === "git");
|
|
```
|
|
|
|
```javascript
|
|
<ExtensionPoint name="repo.avatar" props={type: "git"} />
|
|
```
|
|
|
|
### Typings
|
|
|
|
Both extension points and extensions can share a common typescript type to define the contract between them.
|
|
This includes the `name`, the type of `props` passed to the predicate and what `type` the extensions themselves can be.
|
|
|
|
Example:
|
|
```typescript
|
|
type CalculatorExtensionPoint = ExtensionPointDefinition<"extension.calculator", (input: number[]) => number, undefined>;
|
|
|
|
const sum = (a: number, b: number) => a + b;
|
|
binder.bind<CalculatorExtensionPoint>("extension.calculator", (input: number[]) => input.reduce(sum, 0));
|
|
const calculator = binder.getExtension<CalculatorExtensionPoint>("extension.calculator");
|
|
const result = calculator([1, 2, 3]);
|
|
```
|
|
|
|
In this example, we use the base type `ExtensionPointDefinition<name, type, props>` to declare a new extension point.
|
|
|
|
As we do not need a predicate, we can define the `props` type parameter as `undefined`. This allows us to skip the `props` parameter in the
|
|
`getExtension` method and the `predicate` parameter in the `bind` method.
|
|
|
|
When using `bind` to define an extension or `getExtension` to retrieve an extension, we can pass the new type as a type parameter.
|
|
By doing this, we allow typescript to help us with type-checks and offer us type-completion.
|
|
|
|
Negative Example:
|
|
```typescript
|
|
type CalculatorExtensionPoint = ExtensionPointDefinition<"extension.calculator", (input: number[]) => number, undefined>;
|
|
|
|
const sum = (a: number, b: number) => a + b;
|
|
binder.bind<CalculatorExtensionPoint>("extension.calculato", (input: number[]) => input.reduce(sum, 0));
|
|
```
|
|
|
|
This code for example, would lead to a compile time type error because we made a typo in the `name` of the extension when binding it.
|
|
If we had used the `bind` method without the type parameter, we would not have gotten an error but run into problems at runtime.
|