/** * Registre unique pour étendre l'admin sans modifier le core. * * Trois types d'extensions : * - widget : une tuile du tableau de bord. Côté serveur on enregistre un fetcher * (registerWidgetFetcher), côté client le Composant (registerWidget). * - navItem : une entrée de la sidebar admin (section optionnelle pour grouper). * - page : un composant rendu sous /admin/. * * Les Maps sont stockées sur `globalThis` via `Symbol.for` pour survivre : * 1. au hot-reload de Next.js dev (sinon les enregistrements disparaissent). * 2. à la double-instanciation du fichier — l'instrumentation hook tourne en * Node natif (require ESM), tandis que les Server Components passent par * le bundle Turbopack/Webpack. Sans `globalThis`, les nav items poussés * par `register-server.js` au boot ne seraient pas visibles côté Server * Component qui rend la sidebar — la sidebar resterait vide. */ const REGISTRY_KEY = Symbol.for('__ZEN_ADMIN_REGISTRY__'); if (!globalThis[REGISTRY_KEY]) { globalThis[REGISTRY_KEY] = { widgetFetchers: new Map(), // id -> async () => data widgetComponents: new Map(), // id -> { Component, order, permission } navItems: new Map(), // id -> { id, label, icon, href, basePath, order, sectionId, position, permission } navSections: new Map(), // id -> { id, title, icon, order } pages: new Map(), // slug -> { slug, Component, title?, breadcrumbLabel? } }; } const { widgetFetchers, widgetComponents, navItems, navSections, pages } = globalThis[REGISTRY_KEY]; // ---- Widgets --------------------------------------------------------------- export function registerWidgetFetcher(id, fetcher) { widgetFetchers.set(id, fetcher); } export function registerWidget({ id, Component, order = 0, permission }) { widgetComponents.set(id, { Component, order, permission }); } export function getWidgets() { return [...widgetComponents.entries()] .map(([id, v]) => ({ id, ...v })) .sort((a, b) => a.order - b.order); } // Un fetcher qui échoue n'empêche pas les autres de produire leur donnée. export async function collectWidgetData() { const entries = [...widgetFetchers.entries()]; const results = await Promise.allSettled( entries.map(async ([id, fetch]) => [id, await fetch()]) ); const out = {}; for (const r of results) { if (r.status === 'fulfilled') { const [id, data] = r.value; out[id] = data; } } return out; } // ---- Navigation ------------------------------------------------------------ export function registerNavSection({ id, title, icon, order = 0 }) { navSections.set(id, { id, title, icon, order }); } export function registerNavItem({ id, label, icon, href, basePath, order = 0, sectionId = 'main', position, permission }) { navItems.set(id, { id, label, icon, href, basePath, order, sectionId, position, permission }); } export function getNavSections() { return [...navSections.values()].sort((a, b) => a.order - b.order); } export function getNavItems() { return [...navItems.values()].sort((a, b) => a.order - b.order); } // ---- Pages ----------------------------------------------------------------- export function registerPage({ slug, Component, title, breadcrumbLabel }) { pages.set(slug, { slug, Component, title, breadcrumbLabel }); } export function getPage(slug) { return pages.get(slug); } export function getPages() { return [...pages.values()]; }