Zum Inhalt springen

Plugin-System

Plugins sind ein Konzept des Admin-Panels. Sie laufen im Browser, innerhalb des React-Admins, und werden dort registriert, wo Sie es erstellen. Nichts auf dieser Seite greift auf das Backend zu: Ein Plugin kann weder eine Route noch einen Callback oder einen Cron hinzufügen. Siehe hierzu Custom Functions, Entity Callbacks und Cron Jobs.

Plugins sind der primäre Erweiterungsmechanismus im Panel. Sie können:

  • Die gesamte App mit einem Provider kapseln (Kontext, State-Management)
  • Aktionen für die Startseite und Widgets hinzufügen
  • Komponenten für die Collection-Ansicht injizieren (Toolbar, Column Builder)
  • Formular-Komponenten hinzufügen (Field Builder, zusätzliche Panels)
  • Collections dynamisch injizieren oder modifizieren
interface RebasePlugin {
key: string; // Unique identifier
loading?: boolean; // Hold admin content until the plugin is ready
// UI contributions — a flat array, each entry naming its slot.
// This replaced the old per-area objects (homePage, collectionView, form).
slots?: SlotContribution[];
// HOC providers. `scope: "root"` wraps the whole admin below
// RebaseContext; `scope: "form"` wraps each entity form / edit view.
providers?: PluginProvider[];
// Behavioural (non-UI) hooks: collection modification and injection,
// column reordering, navigation entries.
hooks?: PluginHooks;
// Custom field rendering (e.g. data enhancement).
fieldBuilder?: FieldBuilderConfig;
// Views added to the navigation automatically.
views?: AppView[];
// onMount, onAuthStateChange, onUnmount — see below.
lifecycle?: PluginLifecycle;
}

Jede dieser Eigenschaften ist optional, außer key. Die vollständige Liste der Slot-Namen finden Sie auf der Seite Slots.

  • onMount(context) wird einmal ausgeführt, sobald die Authentifizierung zum ersten Mal bereit ist: Ein Benutzer ist angemeldet, oder die Anmeldung wurde übersprungen.
  • onAuthStateChange(user) wird bei jedem späteren Benutzerwechsel ausgeführt: mit dem neuen Benutzer bei einer Anmeldung, mit null bei einer Abmeldung. Das Plugin bleibt dabei in beiden Fällen gemountet.
  • onUnmount() wird ausgeführt, wenn <Rebase> unmountet wird.

Seit 0.25 lifecycle.onCollectionChange ist veraltet (deprecated). Es wurde nie aufgerufen, weder unter 0.24 noch davor. Ein Plugin, das es setzt, kompiliert weiterhin, und <Rebase> warnt einmal pro Plugin. Um die Zeilen einer Collection zu verfolgen, abonnieren Sie in onMount und kündigen in onUnmount:

import type { RebasePlugin } from "@rebasepro/cms-types";
let stopListening: (() => void) | undefined;
const openOrdersPlugin: RebasePlugin = {
key: "open_orders",
lifecycle: {
onMount: (context) => {
stopListening = context.data.collection("orders").listen(
{ where: { status: ["==", "open"] } },
({ data }) => console.log(`${data.length} open orders`)
);
},
onUnmount: () => stopListening?.()
}
};

Plugins werden an <Rebase> übergeben, neben dem Client. Alles darunter – die Navigation, die Collection-Ansichten, die Formulare – liest sie von dort:

import { Rebase, useRebaseAuthController } from "@rebasepro/app";
import { RebaseCMS, RebaseShell } from "@rebasepro/cms";
import { useDataEnhancementPlugin } from "@rebasepro/plugin-ai";
export function App() {
const authController = useRebaseAuthController({ client });
const dataEnhancementPlugin = useDataEnhancementPlugin();
return (
<Rebase
client={client}
authController={authController}
plugins={[dataEnhancementPlugin]}
>
<RebaseCMS collections={collections}/>
<RebaseShell title="My App"/>
</Rebase>
);
}

Plugins werden normalerweise über einen Hook erstellt, sodass das Array bei jedem Render-Vorgang neu aufgebaut wird; das ist in Ordnung und der Grund dafür, dass plugins eine Prop ist und nicht manuell memoriert werden muss. Zwei Plugins mit demselben key sind ein Fehler – <Rebase> protokolliert Duplikate, anstatt sie stillschweigend zu verwerfen.

Für einen einzelnen Beitrag benötigen Sie überhaupt kein Plugin: <Rebase slots> akzeptiert dieselben SlotContribution-Einträge direkt.

Nur wenn Sie <RebaseShell> durch die darunterliegenden Layer ersetzt haben, muss die Plugin-Liste manuell an den Navigation-Controller übergeben werden:

const navigationStateController = useBuildNavigationStateController({
plugins,
collections: () => collections,
// These four are required — the controller resolves navigation against them.
authController,
data,
collectionRegistryController,
urlController
});

<RebaseNavigation> übernimmt genau diesen Aufruf für Sie und liest plugins aus dem Customization-Controller, den <Rebase> bereitstellt. Siehe Erweitert: Manuelles Layout.

Hier ist ein minimales Plugin, das jeder Collection eine Toolbar-Aktion hinzufügt:

import type { RebasePlugin } from "@rebasepro/cms-types";
function useMyPlugin(): RebasePlugin {
return {
key: "my_plugin",
// `slots` is a flat array of contributions, each naming its slot.
// See the Slots page for the full list of slot names.
slots: [
{ slot: "collection.actions", Component: MyToolbarAction }
],
// `fieldBuilder` is top-level and takes a `wrap` function that returns
// a *component* (or null to leave the field as it is) — it is not
// a render function and no longer lives under `form`.
fieldBuilder: {
wrap: ({ property }) =>
property.propertyConfig === "my_custom_field" ? MyCustomField : null
}
};
}

Seit 0.25 Unter 0.24 erhielt das wrap jedes Plugins das integrierte Feld, und nur der zuletzt zurückgegebene Wrapper wurde gerendert.

Wrapper aus mehreren Plugins werden in der Reihenfolge des plugins-Arrays komponiert. Das wrap jedes Plugins erhält als params.Field das Feld so, wie die Plugins davor es hinterlassen haben, sodass der Wrapper des ersten Plugins am innersten liegt und der des letzten am äußersten. Ein Plugin, dessen wrap null zurückgibt oder dessen enabled-Guard für das Feld false zurückgibt, reicht es unverändert weiter. Ein Wrapper, der Field rendert, behält daher jeden Wrapper davor:

import type { RebasePlugin } from "@rebasepro/cms-types";
import { Typography } from "@rebasepro/ui";
const descriptionPlugin: RebasePlugin = {
key: "description_below",
fieldBuilder: {
// `Field` is the built-in field, or the earlier plugins' wrapper of it.
wrap: ({ Field, property }) => property.description
? (props) => (
<>
<Field {...props}/>
<Typography variant="caption" color="secondary">
{property.description}
</Typography>
</>
)
: null
}
};

KI-gestützte Feld-Autovervollständigung:

import { useDataEnhancementPlugin } from "@rebasepro/plugin-ai";
const enhancementPlugin = useDataEnhancementPlugin();

Es fügt jedem Formular eine Autofill-Aktion hinzu und jedem Markdown-Feld einen Autocomplete-Slash-Befehl, der den Text ab dem Cursor fortsetzt. Beide erscheinen nur dort, wo das Plugin aktiviert ist: Der Dienst meldet sich als verfügbar und getConfigForPath (sofern Sie eines übergeben) erlaubt die Collection. Ein Feld, dessen customProps.editorProps.aiController einen eigenen Controller nennt, verwendet diesen. Seit 0.25 Ein anderes Plugin kann Markdown-Feldern auf dieselbe Weise einen Controller geben, mit EditorAIControllerProvider aus @rebasepro/cms in einem formularbezogenen Provider.

Data enhancement

Plugins können dynamisch neue Collections hinzufügen:

hooks: {
// Receives the resolved collections and returns the full list to use.
injectCollections: (collections) => [...collections, auditLogCollection]
}

Plugins können bestehende Collections modifizieren:

hooks: {
// Receives one collection, returns the modified one.
modifyCollection: (collection) => ({
...collection,
properties: {
...collection.properties,
last_modified_by: {
type: "string",
name: "Modified By",
admin: { readOnly: true }
}
}
})
}

modifyCollection ist synchron, und das Admin-Panel wendet es an, wenn es seine Collections auflöst, nicht jedes Mal, wenn sich das Plugin ändert. Wenn die Änderung Daten braucht, die Sie abrufen müssen, rufen Sie sie ab, bevor Sie <Rebase> rendern, und bauen Sie das Plugin aus dem, was zurückkam:

import { useEffect, useMemo, useState } from "react";
import { Rebase, useRebaseAuthController } from "@rebasepro/app";
import { RebaseCMS, RebaseShell } from "@rebasepro/cms";
import type { RebasePlugin } from "@rebasepro/cms-types";
import { CircularProgressCenter } from "@rebasepro/ui";
export function App() {
const authController = useRebaseAuthController({ client });
const [labels, setLabels] = useState<Record<string, string>>();
useEffect(() => {
fetch("/collection-labels.json")
.then((response) => response.json())
.then(setLabels);
}, []);
const labelsPlugin = useMemo<RebasePlugin | undefined>(() => labels && {
key: "collection_labels",
hooks: {
modifyCollection: (collection) => ({
...collection,
name: labels[collection.slug] ?? collection.name
})
}
}, [labels]);
if (!labelsPlugin) return <CircularProgressCenter/>;
return (
<Rebase client={client} authController={authController} plugins={[labelsPlugin]}>
<RebaseCMS collections={collections}/>
<RebaseShell title="My App"/>
</Rebase>
);
}

Seit 0.25 hooks.modifyCollectionAsync ist veraltet (deprecated). Es wurde nie aufgerufen, weder unter 0.24 noch davor. Ein Plugin, das es setzt, kompiliert weiterhin, und <Rebase> warnt einmal pro Plugin. Verwenden Sie modifyCollection wie oben.