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
This commit is contained in:
@@ -101,6 +101,8 @@ registerNavItem({ id: 'orders', label: 'Commandes', icon: 'ShoppingBag03Icon', h
|
|||||||
registerPage({ slug: 'orders', Component: OrdersPage, title: 'Commandes' });
|
registerPage({ slug: 'orders', Component: OrdersPage, title: 'Commandes' });
|
||||||
```
|
```
|
||||||
|
|
||||||
|
L'item actif dans la sidebar et le fil d'Ariane sont calculés automatiquement à partir d'un `basePath` déduit de `href` (si `href` finit par `/list`, on retire le suffixe). Une convention `slug:edit` / `slug:new` permet de personnaliser les labels du breadcrumb sur les sous-routes — voir [src/features/admin/README.md](../src/features/admin/README.md#item-actif-et-fil-dariane) pour le détail.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// app/layout.js — un seul import suffit ; les side effects enregistrent tout.
|
// app/layout.js — un seul import suffit ; les side effects enregistrent tout.
|
||||||
import './zen.extensions';
|
import './zen.extensions';
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ Ce répertoire fournit l'interface d'administration complète : layout, navigati
|
|||||||
src/features/admin/
|
src/features/admin/
|
||||||
├── index.js buildNavigationSections, registre (Next.js-free)
|
├── index.js buildNavigationSections, registre (Next.js-free)
|
||||||
├── protect.js gardes d'accès
|
├── protect.js gardes d'accès
|
||||||
├── navigation.js buildNavigationSections, buildBottomNavItems
|
├── navigation.js buildNavigationSections, buildBottomNavItems, getNavItemBasePath, findActiveNavContext
|
||||||
├── registry.js registre runtime d'extensions
|
├── registry.js registre runtime d'extensions
|
||||||
├── AdminLayout.server.js layout RSC de l'admin
|
├── AdminLayout.server.js layout RSC de l'admin
|
||||||
├── AdminPage.server.js page RSC racine (protège + collecte les données widgets)
|
├── AdminPage.server.js page RSC racine (protège + collecte les données widgets)
|
||||||
@@ -204,19 +204,39 @@ registerNavItem({
|
|||||||
| `icon` | `string` | Nom d'icône Hugeicons |
|
| `icon` | `string` | Nom d'icône Hugeicons |
|
||||||
| `order` | `number` | Ordre d'affichage (défaut : `0`) |
|
| `order` | `number` | Ordre d'affichage (défaut : `0`) |
|
||||||
|
|
||||||
**`registerNavItem({ id, label, icon, href, sectionId?, order?, position?, permission? })`**
|
**`registerNavItem({ id, label, icon, href, basePath?, sectionId?, order?, position?, permission? })`**
|
||||||
|
|
||||||
| Paramètre | Type | Description |
|
| Paramètre | Type | Description |
|
||||||
|-----------|------|-------------|
|
|-----------|------|-------------|
|
||||||
| `id` | `string` | Identifiant unique de l'entrée |
|
| `id` | `string` | Identifiant unique de l'entrée |
|
||||||
| `label` | `string` | Texte affiché |
|
| `label` | `string` | Texte affiché |
|
||||||
| `icon` | `string` | Nom d'icône Hugeicons |
|
| `icon` | `string` | Nom d'icône Hugeicons |
|
||||||
| `href` | `string` | URL de destination |
|
| `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'`) |
|
| `sectionId` | `string` | Section parente (défaut : `'main'`) |
|
||||||
| `order` | `number` | Ordre d'affichage (défaut : `0`) |
|
| `order` | `number` | Ordre d'affichage (défaut : `0`) |
|
||||||
| `position` | `string` | `'bottom'` pour épingler en bas de la sidebar |
|
| `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. |
|
| `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` :
|
||||||
|
|
||||||
|
```js
|
||||||
|
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
|
### Ajouter une page
|
||||||
|
|||||||
@@ -23,12 +23,15 @@ function resolveIcon(iconNameOrComponent) {
|
|||||||
const AdminSidebar = ({ isMobileMenuOpen, setIsMobileMenuOpen, appName, enabledModules, navigationSections: serverNavigationSections, bottomNavItems = [] }) => {
|
const AdminSidebar = ({ isMobileMenuOpen, setIsMobileMenuOpen, appName, enabledModules, navigationSections: serverNavigationSections, bottomNavItems = [] }) => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
const isItemActive = (item) => {
|
||||||
|
const basePath = item.basePath || item.href;
|
||||||
|
return pathname === item.href || pathname === basePath || pathname.startsWith(basePath + '/');
|
||||||
|
};
|
||||||
|
|
||||||
const [collapsedSections, setCollapsedSections] = useState(() => {
|
const [collapsedSections, setCollapsedSections] = useState(() => {
|
||||||
const initial = new Set();
|
const initial = new Set();
|
||||||
serverNavigationSections.forEach(section => {
|
serverNavigationSections.forEach(section => {
|
||||||
const isActive = section.items.some(item =>
|
const isActive = section.items.some(isItemActive);
|
||||||
pathname === item.href || pathname.startsWith(item.href + '/')
|
|
||||||
);
|
|
||||||
if (!isActive) initial.add(section.id);
|
if (!isActive) initial.add(section.id);
|
||||||
});
|
});
|
||||||
return initial;
|
return initial;
|
||||||
@@ -70,11 +73,34 @@ const AdminSidebar = ({ isMobileMenuOpen, setIsMobileMenuOpen, appName, enabledM
|
|||||||
section.items[0].name.toLowerCase() === section.title.toLowerCase();
|
section.items[0].name.toLowerCase() === section.title.toLowerCase();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Recalcule `current` côté client pour suivre les navigations Link sans
|
||||||
|
// dépendre d'un re-render serveur. Règle alignée sur navigation.js :
|
||||||
|
// match le plus long (href exact > basePath préfixe), un seul item actif.
|
||||||
|
const matchLen = (item) => {
|
||||||
|
if (pathname === item.href) return item.href.length;
|
||||||
|
const basePath = item.basePath || item.href;
|
||||||
|
if (pathname === basePath) return basePath.length;
|
||||||
|
if (pathname.startsWith(basePath + '/')) return basePath.length;
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
let activeItemRef = null;
|
||||||
|
let activeItemLen = 0;
|
||||||
|
for (const section of serverNavigationSections) {
|
||||||
|
for (const item of section.items) {
|
||||||
|
const len = matchLen(item);
|
||||||
|
if (len > activeItemLen) {
|
||||||
|
activeItemRef = item;
|
||||||
|
activeItemLen = len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const navigationSections = serverNavigationSections.map(section => ({
|
const navigationSections = serverNavigationSections.map(section => ({
|
||||||
...section,
|
...section,
|
||||||
items: section.items.map(item => ({
|
items: section.items.map(item => ({
|
||||||
...item,
|
...item,
|
||||||
current: pathname === item.href || pathname.startsWith(item.href + '/')
|
current: item === activeItemRef,
|
||||||
}))
|
}))
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/r
|
|||||||
import { ArrowDown01Icon, ArrowRight01Icon, Menu01Icon, User03Icon, DashboardSquare03Icon, Logout02Icon } from '@zen/core/shared/icons';
|
import { ArrowDown01Icon, ArrowRight01Icon, Menu01Icon, User03Icon, DashboardSquare03Icon, Logout02Icon } from '@zen/core/shared/icons';
|
||||||
import { UserAvatar } from '@zen/core/shared/components';
|
import { UserAvatar } from '@zen/core/shared/components';
|
||||||
import { useRouter, usePathname } from 'next/navigation';
|
import { useRouter, usePathname } from 'next/navigation';
|
||||||
import { getPage, getPages } from '../registry.js';
|
import { getPage } from '../registry.js';
|
||||||
import { useTheme, getThemeIcon } from '@zen/core/themes';
|
import { useTheme, getThemeIcon } from '@zen/core/themes';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
@@ -53,30 +53,59 @@ const AdminTop = ({ isMobileMenuOpen, setIsMobileMenuOpen, user, onLogout, appNa
|
|||||||
return crumbs;
|
return crumbs;
|
||||||
}
|
}
|
||||||
|
|
||||||
const allItems = navigationSections.flatMap(s => s.items);
|
// Localise l'item actif via match le plus long (href exact > basePath préfixe).
|
||||||
const navItem = allItems.find(item => {
|
// On recalcule côté client pour rester aligné sur AdminSidebar et suivre les
|
||||||
const itemSegs = item.href.replace('/admin/', '').split('/').filter(Boolean);
|
// navigations Link sans dépendre d'un re-render serveur du layout.
|
||||||
return itemSegs.length <= segments.length && itemSegs.every((seg, i) => segments[i] === seg);
|
const matchLen = (item) => {
|
||||||
});
|
if (pathname === item.href) return item.href.length;
|
||||||
|
const basePath = item.basePath || item.href;
|
||||||
|
if (pathname === basePath) return basePath.length;
|
||||||
|
if (pathname.startsWith(basePath + '/')) return basePath.length;
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
const itemSegCount = navItem
|
let activeSection = null;
|
||||||
? navItem.href.replace('/admin/', '').split('/').filter(Boolean).length
|
let activeItem = null;
|
||||||
: 1;
|
let activeLen = 0;
|
||||||
const hasSubPage = segments.length > itemSegCount;
|
for (const section of navigationSections) {
|
||||||
|
for (const item of section.items) {
|
||||||
|
const len = matchLen(item);
|
||||||
|
if (len > activeLen) {
|
||||||
|
activeSection = section;
|
||||||
|
activeItem = item;
|
||||||
|
activeLen = len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (navItem) {
|
if (!activeItem) {
|
||||||
crumbs.push({ label: navItem.name, href: hasSubPage ? navItem.href : undefined });
|
// Page enregistrée hors navigation (ex : /admin/profile).
|
||||||
} else if (!hasSubPage) {
|
|
||||||
crumbs.push({ label: pageTitle });
|
crumbs.push({ label: pageTitle });
|
||||||
return crumbs;
|
return crumbs;
|
||||||
}
|
}
|
||||||
|
|
||||||
const subSegment = segments[itemSegCount];
|
// Préfixer la section uniquement si elle ne se résume pas à un item unique
|
||||||
if (subSegment === 'new') {
|
// du même nom (cas « direct link » dans la sidebar — section.title === item.name,
|
||||||
crumbs.push({ label: 'Nouveau' });
|
// ex : section "Utilisateurs" + item "Utilisateurs").
|
||||||
} else if (subSegment === 'edit') {
|
if (activeSection.title !== activeItem.name) {
|
||||||
const page = getPages().find(p => p.slug === `${segments[0]}:edit`);
|
crumbs.push({ label: activeSection.title });
|
||||||
crumbs.push({ label: page?.breadcrumbLabel || page?.title || 'Modifier' });
|
}
|
||||||
|
|
||||||
|
const isExactPage = pathname === activeItem.href;
|
||||||
|
crumbs.push({ label: activeItem.name, href: isExactPage ? undefined : activeItem.href });
|
||||||
|
|
||||||
|
// Action de sous-route : segment juste après le basePath (/edit/:id, /new).
|
||||||
|
const basePath = activeItem.basePath || activeItem.href;
|
||||||
|
const trail = pathname.startsWith(basePath)
|
||||||
|
? pathname.slice(basePath.length).split('/').filter(Boolean)
|
||||||
|
: [];
|
||||||
|
const action = trail[0];
|
||||||
|
if (action === 'new') {
|
||||||
|
const page = getPage(`${segments[0]}:new`);
|
||||||
|
crumbs.push({ label: page?.breadcrumbLabel || page?.title || 'Nouveau' });
|
||||||
|
} else if (action === 'edit') {
|
||||||
|
const page = getPage(`${segments[0]}:edit`);
|
||||||
|
crumbs.push({ label: page?.breadcrumbLabel || page?.title || 'Modification' });
|
||||||
}
|
}
|
||||||
|
|
||||||
return crumbs;
|
return crumbs;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
* Client components sous @zen/core/features/admin/components.
|
* Client components sous @zen/core/features/admin/components.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { buildNavigationSections } from './navigation.js';
|
export { buildNavigationSections, buildBottomNavItems, getNavItemBasePath, findActiveNavContext } from './navigation.js';
|
||||||
export {
|
export {
|
||||||
registerWidget,
|
registerWidget,
|
||||||
registerWidgetFetcher,
|
registerWidgetFetcher,
|
||||||
|
|||||||
@@ -26,6 +26,47 @@ if (isDevkitEnabled()) {
|
|||||||
registerNavItem({ id: 'devkit-icons', label: 'Icônes', icon: 'Image01Icon', href: '/admin/devkit/icons', sectionId: 'devkit', order: 20 });
|
registerNavItem({ id: 'devkit-icons', label: 'Icônes', icon: 'Image01Icon', href: '/admin/devkit/icons', sectionId: 'devkit', order: 20 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zone d'appartenance d'un nav item — toute URL préfixée par ce path est
|
||||||
|
* considérée comme « sous » l'item, même si elle ne correspond pas à `href`.
|
||||||
|
*
|
||||||
|
* Auto-déduction : un href du type `/admin/posts/blogue/list` couvre aussi
|
||||||
|
* `/edit/:id` et `/new` du même parent — on retire `/list` pour obtenir le
|
||||||
|
* basePath. Un module peut surcharger via `registerNavItem({ basePath })`.
|
||||||
|
*/
|
||||||
|
export function getNavItemBasePath(item) {
|
||||||
|
if (item.basePath) return item.basePath;
|
||||||
|
if (item.href.endsWith('/list')) return item.href.replace(/\/list$/, '');
|
||||||
|
return item.href;
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchLength(pathname, item) {
|
||||||
|
if (pathname === item.href) return item.href.length;
|
||||||
|
const basePath = getNavItemBasePath(item);
|
||||||
|
if (pathname === basePath) return basePath.length;
|
||||||
|
if (pathname.startsWith(basePath + '/')) return basePath.length;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sélectionne l'item « actif » pour un pathname donné parmi une liste d'items
|
||||||
|
* (déjà filtrés par permission / position). Le match le plus long gagne :
|
||||||
|
* sur `/admin/posts/blogue/taxonomies/categories`, l'item « Catégories »
|
||||||
|
* (basePath complet) bat l'item « Posts » (basePath parent).
|
||||||
|
*/
|
||||||
|
function pickActiveItem(pathname, items) {
|
||||||
|
let best = null;
|
||||||
|
let bestLen = 0;
|
||||||
|
for (const item of items) {
|
||||||
|
const len = matchLength(pathname, item);
|
||||||
|
if (len > bestLen) {
|
||||||
|
best = item;
|
||||||
|
bestLen = len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build sections for AdminSidebar. Items are sérialisables (pas de composants),
|
* Build sections for AdminSidebar. Items are sérialisables (pas de composants),
|
||||||
* icônes en chaînes résolues côté client.
|
* icônes en chaînes résolues côté client.
|
||||||
@@ -41,14 +82,17 @@ export function buildNavigationSections(pathname, userPermissions = []) {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const activeItem = pickActiveItem(pathname, items);
|
||||||
|
|
||||||
const bySection = new Map();
|
const bySection = new Map();
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const list = bySection.get(item.sectionId) || [];
|
const list = bySection.get(item.sectionId) || [];
|
||||||
list.push({
|
list.push({
|
||||||
name: item.label,
|
name: item.label,
|
||||||
href: item.href,
|
href: item.href,
|
||||||
|
basePath: getNavItemBasePath(item),
|
||||||
icon: item.icon,
|
icon: item.icon,
|
||||||
current: pathname === item.href || pathname.startsWith(item.href + '/'),
|
current: item === activeItem,
|
||||||
});
|
});
|
||||||
bySection.set(item.sectionId, list);
|
bySection.set(item.sectionId, list);
|
||||||
}
|
}
|
||||||
@@ -62,13 +106,45 @@ export function buildNavigationSections(pathname, userPermissions = []) {
|
|||||||
* Build the list of bottom-pinned nav items for AdminSidebar.
|
* Build the list of bottom-pinned nav items for AdminSidebar.
|
||||||
*/
|
*/
|
||||||
export function buildBottomNavItems(pathname) {
|
export function buildBottomNavItems(pathname) {
|
||||||
return getNavItems()
|
const items = getNavItems()
|
||||||
.filter(item => item.position === 'bottom')
|
.filter(item => item.position === 'bottom')
|
||||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||||
.map(item => ({
|
|
||||||
|
const activeItem = pickActiveItem(pathname, items);
|
||||||
|
|
||||||
|
return items.map(item => ({
|
||||||
name: item.label,
|
name: item.label,
|
||||||
href: item.href,
|
href: item.href,
|
||||||
|
basePath: getNavItemBasePath(item),
|
||||||
icon: item.icon,
|
icon: item.icon,
|
||||||
current: pathname === item.href || pathname.startsWith(item.href + '/'),
|
current: item === activeItem,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Localise dans `navigationSections` (forme retournée par `buildNavigationSections`)
|
||||||
|
* l'item marqué comme actif et la section qui le contient. Utilisé par AdminTop
|
||||||
|
* pour construire le fil d'Ariane à partir de la même source de vérité que la
|
||||||
|
* sidebar — pas de double calcul de matching.
|
||||||
|
*
|
||||||
|
* Retourne `{ section, item, isExactPage, basePath }` ou `null`.
|
||||||
|
* - `isExactPage` : on est sur l'URL exacte du nav item (page liste / page racine).
|
||||||
|
* Permet de décider si l'item doit être cliquable dans le breadcrumb.
|
||||||
|
* - `basePath` : zone d'appartenance, sert à isoler le « trail » d'action
|
||||||
|
* (ex : `edit/1`, `new`) qui suit l'item dans le pathname.
|
||||||
|
*/
|
||||||
|
export function findActiveNavContext(pathname, navigationSections) {
|
||||||
|
for (const section of navigationSections) {
|
||||||
|
for (const item of section.items) {
|
||||||
|
if (item.current) {
|
||||||
|
return {
|
||||||
|
section,
|
||||||
|
item,
|
||||||
|
isExactPage: pathname === item.href,
|
||||||
|
basePath: item.basePath || item.href,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ if (!globalThis[REGISTRY_KEY]) {
|
|||||||
globalThis[REGISTRY_KEY] = {
|
globalThis[REGISTRY_KEY] = {
|
||||||
widgetFetchers: new Map(), // id -> async () => data
|
widgetFetchers: new Map(), // id -> async () => data
|
||||||
widgetComponents: new Map(), // id -> { Component, order, permission }
|
widgetComponents: new Map(), // id -> { Component, order, permission }
|
||||||
navItems: new Map(), // id -> { id, label, icon, href, order, sectionId, position, permission }
|
navItems: new Map(), // id -> { id, label, icon, href, basePath, order, sectionId, position, permission }
|
||||||
navSections: new Map(), // id -> { id, title, icon, order }
|
navSections: new Map(), // id -> { id, title, icon, order }
|
||||||
pages: new Map(), // slug -> { slug, Component, title?, breadcrumbLabel? }
|
pages: new Map(), // slug -> { slug, Component, title?, breadcrumbLabel? }
|
||||||
};
|
};
|
||||||
@@ -66,8 +66,8 @@ export function registerNavSection({ id, title, icon, order = 0 }) {
|
|||||||
navSections.set(id, { id, title, icon, order });
|
navSections.set(id, { id, title, icon, order });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerNavItem({ id, label, icon, href, order = 0, sectionId = 'main', position, permission }) {
|
export function registerNavItem({ id, label, icon, href, basePath, order = 0, sectionId = 'main', position, permission }) {
|
||||||
navItems.set(id, { id, label, icon, href, order, sectionId, position, permission });
|
navItems.set(id, { id, label, icon, href, basePath, order, sectionId, position, permission });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNavSections() {
|
export function getNavSections() {
|
||||||
|
|||||||
Reference in New Issue
Block a user