Aller au contenu

Système de plugins

Les plugins sont un concept propre au panneau d’administration. Ils s’exécutent dans le navigateur, au sein de l’admin React, et sont enregistrés là où vous le construisez. Rien dans cette page ne concerne le backend : un plugin ne peut pas ajouter de route, de callback ou de tâche cron. Pour cela, consultez Custom Functions, Entity Callbacks et Cron Jobs.

Les plugins constituent le mécanisme d’extension principal du panneau. Ils peuvent :

  • Envelopper l’ensemble de l’application avec un provider (contexte, gestion d’état)
  • Ajouter des actions sur la page d’accueil et des widgets
  • Injecter des composants de vue de collection (barre d’outils, générateurs de colonnes)
  • Ajouter des composants de formulaire (générateurs de champs, panneaux supplémentaires)
  • Injecter ou modifier des collections dynamiquement
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;
}

Chacun de ces éléments est optionnel à l’exception de key. La liste complète des noms d’emplacements (slots) se trouve sur la page Slots.

  • onMount(context) s’exécute une fois, lorsque l’authentification est prête pour la première fois : un utilisateur est connecté, ou la connexion a été ignorée.
  • onAuthStateChange(user) s’exécute à chaque changement d’utilisateur ultérieur : avec le nouvel utilisateur lors d’une connexion, avec null lors d’une déconnexion. Le plugin reste monté dans les deux cas.
  • onUnmount() s’exécute lorsque <Rebase> est démonté.

Depuis 0.25 lifecycle.onCollectionChange est déprécié. Rien ne l’a jamais appelé, en 0.24 comme avant. Un plugin qui le définit compile toujours, et <Rebase> avertit une seule fois par plugin. Pour suivre les lignes d’une collection, abonnez-vous depuis onMount et désabonnez-vous dans 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?.()
}
};

Les plugins se placent sur <Rebase>, aux côtés du client. Tout ce qui se trouve en dessous — la navigation, les vues de collection, les formulaires — les lit à partir de là :

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>
);
}

Les plugins sont généralement construits par un hook, de sorte que le tableau est reconstruit à chaque rendu ; cela ne pose aucun problème, et c’est la raison pour laquelle plugins est une prop plutôt qu’un élément à mémoïser manuellement. Deux plugins avec la même key constituent une erreur — <Rebase> journalise les doublons plutôt que d’en ignorer un silencieusement.

Pour une contribution unique, vous n’avez pas du tout besoin d’un plugin : <Rebase slots> accepte directement les mêmes entrées SlotContribution.

La liste des plugins ne doit être transmise manuellement dans le contrôleur de navigation que si vous avez remplacé <RebaseShell> par les couches sous-jacentes :

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

<RebaseNavigation> effectue exactement cet appel pour vous, en lisant plugins depuis le contrôleur de personnalisation fourni par <Rebase>. Consultez Advanced: manual layout.

Voici un plugin minimal qui ajoute une action de barre d’outils à chaque collection :

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
}
};
}

Depuis 0.25 En 0.24, le wrap de chaque plugin recevait le champ intégré, et seul le dernier wrapper renvoyé était affiché.

Les wrappers de plusieurs plugins se composent, dans l’ordre du tableau plugins. Le wrap de chaque plugin reçoit, sous params.Field, le champ tel que les plugins précédents l’ont laissé : le wrapper du premier plugin est donc le plus interne et celui du dernier le plus externe. Un plugin dont le wrap renvoie null, ou dont la garde enabled renvoie false pour le champ, le transmet inchangé. Un wrapper qui affiche Field conserve donc tous les wrappers qui le précèdent :

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
}
};

Plugin d’enrichissement des données (Data Enhancement)

Section intitulée « Plugin d’enrichissement des données (Data Enhancement) »

Autocomplétion de champs alimentée par l’IA :

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

Il ajoute une action Autofill à chaque formulaire, et une commande slash Autocomplete à chaque champ markdown, qui poursuit le texte à partir du curseur. Les deux n’apparaissent que là où le plugin est activé : le service se déclare disponible et getConfigForPath (si vous en passez un) autorise la collection. Un champ dont customProps.editorProps.aiController nomme un contrôleur qui lui est propre utilise celui-là. Depuis 0.25 Un autre plugin peut donner un contrôleur aux champs markdown de la même façon, avec EditorAIControllerProvider de @rebasepro/cms dans un fournisseur limité au formulaire.

Data enhancement

Les plugins peuvent ajouter dynamiquement de nouvelles collections :

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

Les plugins peuvent modifier des collections existantes :

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 est synchrone, et l’administration l’applique lorsqu’elle résout ses collections, et non à chaque changement du plugin. Lorsque la modification nécessite des données à récupérer, récupérez-les avant d’afficher <Rebase> et construisez le plugin à partir de ce qui est revenu :

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>
);
}

Depuis 0.25 hooks.modifyCollectionAsync est déprécié. Rien ne l’a jamais appelé, en 0.24 comme avant. Un plugin qui le définit compile toujours, et <Rebase> avertit une seule fois par plugin. Utilisez modifyCollection comme ci-dessus.