- 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
Admin
Ce répertoire fournit l'interface d'administration complète : layout, navigation, tableau de bord, gestion des utilisateurs et des rôles. Il expose un registre runtime pour permettre aux projets consommateurs d'ajouter des widgets, des entrées de navigation et des pages sans modifier le core.
Le pattern
zen.extensions.jsdocumenté ici reste valide pour les extensions in-projet (extensions ad hoc spécifiques à une app). Pour distribuer une extension réutilisable comme un package npm, consulter docs/MODULES.md — la même API d'enregistrement s'utilise mais le module est auto-découvert via lesdependenciesdu projet consommateur.
Structure
src/features/admin/
├── index.js buildNavigationSections, registre (Next.js-free)
├── protect.js gardes d'accès
├── navigation.js buildNavigationSections, buildBottomNavItems, getNavItemBasePath, findActiveNavContext
├── registry.js registre runtime d'extensions
├── AdminLayout.server.js layout RSC de l'admin
├── AdminPage.server.js page RSC racine (protège + collecte les données widgets)
├── AdminPage.client.js shell client
├── components/
│ ├── index.js re-export
│ ├── AdminHeader.js
│ ├── AdminShell.js
│ ├── AdminSidebar.js
│ ├── AdminTop.js
│ ├── RoleEditModal.client.js
│ ├── ThemeToggle.js
│ ├── UserCreateModal.client.js
│ └── UserEditModal.client.js
├── devkit/
│ ├── ComponentsPage.client.js
│ ├── DevkitPage.client.js
│ └── IconsPage.client.js
├── pages/
│ ├── ConfirmEmailChangePage.client.js
│ ├── DashboardPage.client.js
│ ├── ProfilePage.client.js
│ ├── RolesPage.client.js
│ ├── SettingsPage.client.js
│ └── UsersPage.client.js
└── widgets/
├── index.client.js auto-registration des widgets core (côté client)
├── index.server.js auto-registration des widgets core (côté serveur)
├── users.client.js widget Utilisateurs (composant)
└── users.server.js widget Utilisateurs (fetcher)
Import
// Gardes RSC — chemin dédié (next/navigation + next/headers, non compatible turbopackIgnore)
import { protectAdmin, isAdmin } from '@zen/core/features/admin/protect';
// Registre et navigation — compatible modules externes
import { buildNavigationSections } from '@zen/core/features/admin';
import {
registerWidget,
registerWidgetFetcher,
registerNavItem,
registerNavSection,
registerPage,
} from '@zen/core/features/admin';
Pages intégrées
| Route | Page |
|---|---|
/admin/dashboard |
Tableau de bord avec widgets |
/admin/users |
Liste, création et gestion des utilisateurs |
/admin/roles |
Gestion des rôles et permissions |
/admin/settings |
Paramètres de l'application |
/admin/profile |
Profil de l'utilisateur connecté |
/admin/confirm-email-change |
Confirmation de changement d'email |
API
protectAdmin(options?)
Garde serveur. Redirige si l'utilisateur n'est pas connecté ou n'a pas la permission ADMIN_ACCESS. Retourne la session courante.
const session = await protectAdmin();
// session.user est disponible
| Option | Type | Défaut | Description |
|---|---|---|---|
redirectTo |
string |
'/auth/login' |
Redirection si non authentifié |
forbiddenRedirect |
string |
'/' |
Redirection si non autorisé |
isAdmin()
Vérifie si l'utilisateur courant a la permission ADMIN_ACCESS. Retourne boolean.
const admin = await isAdmin();
if (!admin) return null;
buildNavigationSections(pathname, userPermissions?)
Construit les sections de navigation pour la sidebar à partir du registre. Marque l'entrée active selon pathname. Les items dont le champ permission n'est pas présent dans userPermissions sont automatiquement exclus ; si tous les items d'une section sont exclus, la section disparaît également.
const permissions = await getUserPermissions(session.user.id);
const sections = buildNavigationSections('/admin/users', permissions);
// [{ id, title, icon, items: [{ name, href, icon, current }] }]
Registre d'extensions
Le registre permet d'ajouter des widgets, des entrées de navigation et des pages sans toucher au core. Les enregistrements se font via des imports à effet de bord dans le layout racine du projet consommateur.
Ajouter un widget
Un widget est composé de deux parties : un fetcher serveur qui collecte les données, et un composant client qui les affiche.
// app/admin/orders/ordersWidget.server.js
import { registerWidgetFetcher } from '@zen/core/features/admin';
import { countOrders } from './orders.server.js';
registerWidgetFetcher('orders', async () => ({
total: await countOrders(),
}));
// app/admin/orders/ordersWidget.client.js
'use client';
import { registerWidget } from '@zen/core/features/admin';
import { StatCard } from '@zen/core/shared/components';
function OrdersWidget({ data, loading }) {
return (
<StatCard
title="Commandes"
value={loading ? '-' : String(data?.total ?? 0)}
loading={loading}
/>
);
}
registerWidget({ id: 'orders', Component: OrdersWidget, order: 20 });
Le composant reçoit data (retour du fetcher) et loading (booléen). Si le fetcher échoue, data est null et loading reste false.
registerWidgetFetcher(id, fetcher)
| Paramètre | Type | Description |
|---|---|---|
id |
string |
Identifiant unique du widget |
fetcher |
async () => object |
Fonction serveur qui retourne les données |
registerWidget({ id, Component, order?, permission? })
| Paramètre | Type | Description |
|---|---|---|
id |
string |
Identifiant unique (doit correspondre au fetcher) |
Component |
ReactComponent |
Composant client affiché dans le tableau de bord |
order |
number |
Position dans la grille (défaut : 0) |
permission |
string |
Clé de permission requise pour voir ce widget (ex. 'users.view'). Le widget est masqué si l'utilisateur ne possède pas cette permission. |
Ajouter une entrée de navigation
import { registerNavSection, registerNavItem } from '@zen/core/features/admin';
registerNavSection({ id: 'commerce', title: 'Commerce', icon: 'ShoppingBag03Icon', order: 30 });
registerNavItem({
id: 'orders',
label: 'Commandes',
icon: 'ShoppingBag03Icon',
href: '/admin/orders',
sectionId: 'commerce',
order: 10,
permission: 'orders.view', // optionnel — masqué si l'utilisateur n'a pas cette permission
});
registerNavSection({ id, title, icon, order? })
| Paramètre | Type | Description |
|---|---|---|
id |
string |
Identifiant unique de la section |
title |
string |
Titre affiché dans la sidebar |
icon |
string |
Nom d'icône Hugeicons |
order |
number |
Ordre d'affichage (défaut : 0) |
registerNavItem({ id, label, icon, href, basePath?, sectionId?, order?, position?, permission? })
| Paramètre | Type | Description |
|---|---|---|
id |
string |
Identifiant unique de l'entrée |
label |
string |
Texte affiché |
icon |
string |
Nom d'icône Hugeicons |
href |
string |
URL de destination (cible du clic dans la sidebar) |
basePath |
string |
Préfixe de routes considérées « sous » cette entrée — utilisé pour souligner l'item actif et construire le fil d'Ariane sur les sous-routes (/edit/:id, /new...). Auto-déduit : si href finit par /list, on retire le /list ; sinon basePath = href. À spécifier explicitement uniquement quand l'auto-déduction ne suffit pas. |
sectionId |
string |
Section parente (défaut : 'main') |
order |
number |
Ordre d'affichage (défaut : 0) |
position |
string |
'bottom' pour épingler en bas de la sidebar |
permission |
string |
Clé de permission requise pour voir cette entrée (ex. 'orders.view'). L'entrée est masquée si l'utilisateur ne possède pas cette permission. |
Item actif et fil d'Ariane
L'item « actif » dans la sidebar est celui dont le basePath matche le préfixe le plus long du pathname courant. Sur /admin/posts/blogue/edit/1, l'item « Posts » (basePath /admin/posts/blogue) est souligné ; sur /admin/posts/blogue/taxonomies/categories, c'est l'item « Catégories » qui gagne car son basePath est plus spécifique.
Le fil d'Ariane se construit automatiquement à partir de cet item :
[icon] > <section.title> > <item.label>— si la section contient plusieurs items[icon] > <item.label>— si la section ne contient que cet item et qu'ils portent le même nom (ex : section "Utilisateurs" + item "Utilisateurs")- Suivi de
> Nouveauou> Modificationquand l'URL contient/newou/edit/:idaprès le basePath.
Pour personnaliser les labels d'action, enregistrer une page avec un slug <root>:new ou <root>:edit :
registerPage({ slug: 'posts:edit', breadcrumbLabel: "Modifier l'article" });
registerPage({ slug: 'posts:new', breadcrumbLabel: 'Nouvel article' });
(<root> = premier segment d'URL après /admin/, ex : posts pour /admin/posts/blogue/edit/1.)
Ajouter une page
import { registerPage } from '@zen/core/features/admin';
import OrdersPage from './OrdersPage.js';
registerPage({
slug: 'orders',
Component: OrdersPage,
title: 'Commandes',
});
La page est rendue sous /admin/<slug>. AdminPage.client.js résout le composant à partir du slug dans les paramètres de route.
registerPage({ slug, Component, title?, breadcrumbLabel? })
| Paramètre | Type | Description |
|---|---|---|
slug |
string |
Segment d'URL sous /admin/ |
Component |
ReactComponent |
Composant client rendu pour cette route |
title |
string |
Titre de la page (optionnel) |
breadcrumbLabel |
string |
Label du fil d'Ariane (optionnel, défaut : title) |
Câbler les extensions dans le projet consommateur
Regrouper tous les enregistrements dans un fichier de point d'entrée unique, puis l'importer une seule fois depuis le layout racine.
// app/zen.extensions.js
import './admin/orders/ordersWidget.server.js';
import './admin/orders/ordersWidget.client.js';
import { registerNavSection, registerNavItem, registerPage } from '@zen/core/features/admin';
import OrdersPage from './admin/orders/OrdersPage.js';
registerNavSection({ id: 'commerce', title: 'Commerce', icon: 'ShoppingBag03Icon', order: 30 });
registerNavItem({ id: 'orders', label: 'Commandes', icon: 'ShoppingBag03Icon', href: '/admin/orders', sectionId: 'commerce', permission: 'orders.view' });
registerPage({ slug: 'orders', Component: OrdersPage, title: 'Commandes' });
// app/layout.js
import './zen.extensions'; // les side effects enregistrent tout
DevKit
Le DevKit est une section de l'admin réservée au développement. Il expose une galerie de composants et un catalogue d'icônes. Il s'active via la variable d'environnement ZEN_DEVKIT_ENABLED=true et n'est jamais rendu en production.
| Route | Contenu |
|---|---|
/admin/devkit/components |
Galerie des composants partagés |
/admin/devkit/icons |
Catalogue d'icônes Hugeicons |
Ajouter un widget core
Les widgets intégrés au core suivent le même pattern que les widgets consommateurs, avec une étape supplémentaire : déclarer les fichiers dans les index d'auto-registration.
// src/features/admin/widgets/myWidget.server.js
import { registerWidgetFetcher } from '../registry.js';
registerWidgetFetcher('myWidget', async () => ({ ... }));
// src/features/admin/widgets/index.server.js
import './myWidget.server.js'; // ajouter cette ligne
// src/features/admin/widgets/myWidget.client.js
'use client';
import { registerWidget } from '../registry.js';
// ...
registerWidget({ id: 'myWidget', Component: MyWidget, order: 20 });
// src/features/admin/widgets/index.client.js
import './myWidget.client.js'; // ajouter cette ligne