Files
core/src/features/admin
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
..

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.js documenté 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 les dependencies du 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 > Nouveau ou > Modification quand l'URL contient /new ou /edit/:id aprè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