Ir al contenido

Sistema de plugins

Los plugins son un concepto del panel de administración. Se ejecutan en el navegador, dentro de la interfaz de administración de React, y se registran donde la construyes. Nada en esta página llega al backend: un plugin no puede añadir una ruta, un callback o un cron. Para eso, consulta Funciones personalizadas, Callbacks de entidad y Tareas cron.

Los plugins son el mecanismo principal de extensión en el panel. Pueden:

  • Envolver toda la aplicación con un provider (contexto, gestión de estado)
  • Añadir acciones de la página de inicio y widgets
  • Inyectar componentes de vista de colección (barra de herramientas, constructores de columnas)
  • Añadir componentes de formulario (constructores de campos, paneles adicionales)
  • Inyectar o modificar colecciones dinámicamente
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;
}

Cada uno de estos es opcional excepto key. La lista completa de nombres de slots se encuentra en la página de Slots.

  • onMount(context) se ejecuta una vez, cuando la autenticación está lista por primera vez: hay un usuario con sesión iniciada, o se omitió el inicio de sesión.
  • onAuthStateChange(user) se ejecuta en cada cambio de usuario posterior: con el nuevo usuario al iniciar sesión, con null al cerrarla. El plugin sigue montado en ambos casos.
  • onUnmount() se ejecuta cuando <Rebase> se desmonta.

Desde 0.25 lifecycle.onCollectionChange está deprecado. Nada lo llamó nunca, ni en 0.24 ni antes. Un plugin que lo define sigue compilando, y <Rebase> avisa una vez por plugin. Para seguir las filas de una colección, suscríbete desde onMount y cancela la suscripción en 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?.()
}
};

Los plugins se colocan en <Rebase>, junto al cliente. Todo lo que esté debajo —la navegación, las vistas de colección, los formularios— los lee desde allí:

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

Por lo general, los plugins se construyen mediante un hook, por lo que el array se reconstruye en cada renderizado; esto es normal y es la razón por la cual plugins es una prop en lugar de algo que debas memorizar manualmente. Dos plugins con la misma key son un error: <Rebase> registra los duplicados en lugar de descartar uno silenciosamente.

Para una sola contribución no necesitas un plugin en absoluto: <Rebase slots> acepta directamente las mismas entradas SlotContribution.

Solo si has reemplazado <RebaseShell> con las capas subyacentes, la lista de plugins debe pasarse manualmente al controlador de navegación:

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

<RebaseNavigation> realiza exactamente esta llamada por ti, leyendo plugins del controlador de personalización que proporciona <Rebase>. Consulta Avanzado: diseño manual.

Aquí tienes un plugin mínimo que añade una acción a la barra de herramientas en cada colección:

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

Desde 0.25 En 0.24 el wrap de cada plugin recibía el campo integrado, y solo se renderizaba el último envoltorio devuelto.

Los envoltorios de varios plugins se componen, en el orden del array plugins. El wrap de cada plugin recibe, como params.Field, el campo tal como lo dejaron los plugins anteriores, así que el envoltorio del primer plugin es el más interno y el del último el más externo. Un plugin cuyo wrap devuelve null, o cuya guarda enabled devuelve false para el campo, lo pasa sin cambios. Un envoltorio que renderiza Field conserva, por tanto, todos los envoltorios anteriores:

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

Autocompletado de campos impulsado por IA:

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

Añade una acción Autofill a cada formulario y un comando de barra Autocomplete a cada campo markdown, que continúa el texto desde el cursor. Ambos aparecen solo donde el plugin está habilitado: el servicio se declara disponible y getConfigForPath (cuando le pasas uno) permite la colección. Un campo cuyo customProps.editorProps.aiController nombra un controlador propio usa ese. Desde 0.25 Otro plugin puede dar a los campos markdown un controlador de la misma forma, con EditorAIControllerProvider de @rebasepro/cms en un proveedor con alcance de formulario.

Mejora de datos

Los plugins pueden añadir nuevas colecciones dinámicamente:

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

Los plugins pueden modificar colecciones existentes:

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 es síncrono, y el panel de administración lo aplica cuando resuelve sus colecciones, no cada vez que cambia el plugin. Cuando el cambio necesita datos que hay que obtener, obtenlos antes de renderizar <Rebase> y construye el plugin a partir de lo que llegó:

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

Desde 0.25 hooks.modifyCollectionAsync está deprecado. Nada lo llamó nunca, ni en 0.24 ni antes. Un plugin que lo define sigue compilando, y <Rebase> avisa una vez por plugin. Usa modifyCollection como se muestra arriba.