Files
core/src/features/admin/registry.js
T
hykocx fbcaed6816 docs(admin): document active item and breadcrumb logic for nav registration
- add note in DEV.md explaining basePath auto-deduction and breadcrumb slug convention
- update README.md to document new `basePath` param in `registerNavItem` and detail active item/breadcrumb behavior
- update navigation file listing in README.md to include new exported helpers
- implement `getNavItemBasePath` and `findActiveNavContext` in navigation.js
- use `basePath` in AdminSidebar to determine active item via longest-prefix match
- use `basePath` in AdminTop to build breadcrumb with section, item, and action labels
- expose new navigation helpers from admin index.js and registry.js
2026-04-26 19:40:40 -04:00

94 lines
3.4 KiB
JavaScript

/**
* 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/<slug>.
*
* 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()];
}