Automatic Menus & Navigation

Learn how to declaratively register pages in menus and automatically hide them based on user roles.

Overview

In JopiJS, you don't manually build large sidebar menus. Instead, each page declares where it belongs in the navigation. JopiJS then aggregates this data and, most importantly, filters it automatically based on the user's permissions.

This system provides:

  • Declarative Registration: Link a page to a menu directly from its route folder.
  • Automatic RBAC: If a user is not authorized to see a page (via Marker Files), the menu entry won't even exist for them.
  • Modular Menus: Modules can add items to existing menus without touching the core layout.

Installation

The automatic menu system requires the installation of the @jopijs/jopimod_menu module.

npm install @jopijs/jopimod_menu --save

# After install, remember to sync virtual paths
npm run postinstall

1. Registering a Page in a Menu

To add a page to a menu, you use a config.ts file inside the route folder. This file allows you to specify the navigation path (including submenus).

Project Structure:

src/ mod_catalog/
└── @routes/ 
    └── admin/              
        └── add-product/
            ├── page.tsx
            ├── needRole_admin.cond  # <--- Visibility is tied to this!
            └── config.ts            # <--- Registration logic

The Configuration Logic (config.ts) You use the config object to "push" your page into one or more menus.

src/mod_catalog/@routes/admin/add-product/config.ts
import { JopiRouteConfig } from "jopijs";

export default function (config: JopiRouteConfig) {
    // Add to the sidebar under "Inventory" > "New Product"
    config.menu_addToLeftMenu(["Inventory", "New Product"]);

    // You can also add it to other menus simultaneously
    config.menu_addToTopMenu(["Direct Actions", "Add Product"]);
}

2. Automatic Role Filtering

The magic of JopiJS navigation is that it is secure by default.

If your route folder contains a marker file like needRole_admin.cond:

  1. Server-side: The route is protected at the API/Page level.
  2. Client-side: The menu system automatically checks if the current user has the admin role.
  3. Result: If the user lacks the role, the menu item is purged from the data returned to the UI.

Zero Boilerplate: You never have to write if (user.isAdmin) { ... } in your navigation code. JopiJS handles the lookup and filtering before your component even renders.


3. Consuming the Menu Data

To display the menu in your layout, use the useMenu hook provided by the @jopijs/jopimod_menu module.

The Menu Data Structure

The hook returns an array of objects implementing the MenuItem interface:

MenuItem Interface
export interface MenuItem {
    key: string;             // The unique key for the menu entry
    items?: MenuItem[];      // Submenu items (recursive)
    isActive?: boolean;      // True if the current URL matches this item
    title?: string;          // The display name
    url?: string;            // The link destination
    icon?: React.FC<any>;    // An optional React component for the icon

    // If you want to customize the breadcrumb informations.
    breadcrumb?: string[] | React.FC<any>;
    
    [key: string]: any;      // Catch-all for extra metadata
}

React Implementation Example

Here is how you can render a simple sidebar. Note that menus can be recursive (using the items property).

src/mod_layout/ui/Sidebar/index.tsx
import useMenu from "@hooks/jopijs.menu.useMenu";
import MenuNames from "@/lib/jopijs.menu.MenuNames";

export default function Sidebar() {
    const menu = useMenu(MenuNames.LEFT_MENU);

    return (
        <aside>
            {menu.map(item => (
                <div key={item.key} className={item.isActive ? "active" : ""}>
                    {item.url ? (
                        <a href={item.url}>{item.title}</a>
                    ) : (
                        <span>{item.title}</span>
                    )}

                    {/* Rendering submenus if they exist */}
                    {item.items && (
                        <ul className="submenu">
                            {item.items.map(sub => (
                                <li key={sub.key}>
                                    <a href={sub.url}>{sub.title}</a>
                                </li>
                            ))}
                        </ul>
                    )}
                </div>
            ))}
        </aside>
    );
}

4. Configuration Overrides

When a module overrides a route folder, it can also override the configuration.

  • Total Replace: If your overriding folder contains a config.ts and has a high.priority marker, it completely replaces the original configuration.
  • Inheritance: If you override a page but don't provide a config.ts, the original module's configuration is preserved.

Example: Moving a core page to a different menu

src/ mod_rebranding/
└── @routes/ 
    └── admin/ add-product/
        ├── config.ts        # New menu paths
        └── high.priority    # Ensures this config wins

Summary

  • Use config.ts to register routes in a menu hierarchy.
  • Navigation visibility is automatically synchronized with your Role-Based Access Control.
  • Use useMenu to retrieve data that is already filtered for the current user's roles.
  • This creates a truly modular system where menus grow and shrink automatically as modules are added or removed.