Navigation
Genie renders the application’s left navigation from a single *.navbar.xml model. You describe the
tree once; the engine stores it, filters it per user, and ships the React UI a ready-to-render JSON
tree; badge counts are computed on a separate call so the tree appears instantly. The server never
trusts the client to decide what a user may see.
The tree
Section titled “The tree”A *.navbar.xml file is a tree of four node kinds:
<Navbar> <Item Name="Home" Label="Home" Icon="fa fa-house" Route="/" />
<Module Name="catalog" Label="Catalog" Icon="fa fa-box-open" Route="/object/products"> <Section Label="Master Data"> <Item Name="products" Label="Products" Icon="fa fa-box" Route="/object/products" /> <Item Name="categories" Label="Categories" Route="/object/categories" /> </Section> <Divider /> <Section Label="Reports"> <Item Name="low-stock-report" Label="Low Stock" Route="/object/low-stock-report"> <CountsBadge Color="danger">SELECT COUNT(*) FROM [Inventory].[vw_LowStock]</CountsBadge> </Item> </Section> </Module>
<Module Name="admin" Label="Admin" Icon="fa fa-gear" VisibleIf="User.IsInRole('Admin')"> … </Module> <Module Name="docs" Label="Docs" Icon="fa fa-book" Route="https://github.com" Target="_blank" /></Navbar>| Kind | Role |
|---|---|
<Module> |
a top-level rail entry; may itself be a link and hold children |
<Section> |
a non-clickable grouping heading; hidden when it has no visible children |
<Item> |
a navigable link (Route); may nest child <Item>s |
<Divider> |
a decorative separator |
Routes are client-side paths that map to the UI’s GenieRouter — object grids/records at
/object/{slug} (add ?id={pk} to view a record, /edit?id={pk} / /create for forms), wizards at
/form/{slug}, or /. An external link uses an absolute URL plus Target="_blank". (Legacy
/table/{slug}, /view/{slug}, and /wizard/{slug} routes still resolve — they’re normalized to the
new scheme.)
Stored as one JSON document
Section titled “Stored as one JSON document”At model migration the whole tree is synced into a single NavbarDefinition row (DefinitionJson)
named default. The sync is a full replace — the XML is the source of truth, so removing a node
from the file removes it from the navbar on the next migration. Keep one *.navbar.xml per app.
The one exception is a scope the navbar designer owns
(NavbarDefinition.IsDesignerOwned): there the database is authoritative and sync skips it, logging
that it did. Release the scope from the designer to hand it back to the XML file.
Permission filtering (server-side)
Section titled “Permission filtering (server-side)”GET /api/v1/genie/navbar returns only what the authenticated user is allowed to see. NavbarService
loads the stored tree, then NavbarFilter prunes it against the user’s permissions:
- A leaf link is visible when the user holds List or Execute on the resource its route
targets (the resource name is extracted from the
/object/{name}or/form/{name}route — legacy/table|view/{name}and/genie/wizard/form/{name}are still recognized). Closed by default — an unknown resource is hidden. - A container (
Module, or anItemwith children) is visible only if it has at least one visible descendant. - A
Sectionis visible only with at least one visible child. - Dividers are kept, then leading, trailing and consecutive dividers are pruned so no dangling separators remain.
- A super-admin sees everything.
VisibleIf on a node is a server-evaluated role/expression guard (e.g.
User.IsInRole('Admin')) applied on top of the permission rules.
SQL badges
Section titled “SQL badges”An <Item> may carry one badge whose value is computed server-side from a scalar SELECT:
<CountsBadge>— a numeric count (e.g. pending records).<BulletsBadge>— an indicator dot driven by a count.
<Item Name="purchase-orders" Label="Purchase Orders" Route="/object/purchase-orders"> <BulletsBadge Color="info"> SELECT COUNT(*) FROM [Inventory].[PurchaseOrders] WHERE Status = 'Submitted' AND IsDeleted = 0 </BulletsBadge></Item>Badges are fetched separately from the tree (see the API below) so the navbar renders immediately
even when many items carry badges. NavbarService runs each visible link’s badge query with the
current session parameters (@SessionUserId, @SessionCompanyId, …), concurrently (each on
its own short-lived connection, bounded to a small pool), and ships only the computed number and
colour — the SQL never leaves the server. A badge query that fails is logged and dropped; it
never breaks the navbar.
Navbar designer (System only)
Section titled “Navbar designer (System only)”The same tree can be authored at runtime from /navbar-designer — a visual editor that reads and
writes the stored definition and round-trips it through the very same *.navbar.xml contract. It is
reachable from System → Automation → Navbar Designer in the sidebar.
Three surfaces over one tree:
- Outline — drag to reorder and re-parent; the placement rules are enforced as you drag (modules stay at the root, sections don’t nest, a divider is a leaf). Inline add-child / duplicate / delete.
- Live preview — the rail and contextual sidebar rendered the way the shell would, including
section headings, dividers and badge pills. It is a structural preview: per-user permission
filtering still happens server-side on
GET /navbar. - Items grid — the same tree flattened to
Kind · Name · Label · Parent · Order · Route · Badge · VisibleIf.
Plus a kind-aware properties panel (route picker fed by the registered objects/wizards/tools, icon preview, badge kind/colour/SQL with a Run query preview), a two-way XML pane, and a Checks tab.
Validation
Section titled “Validation”Every save runs NavbarValidator server-side and is rejected when any error is present — the
client-side checks are a convenience, never the gate. Errors: a missing Name, a duplicate Name, a
<Module> nested inside another node, a badge on a non-<Item>, a badge with no SQL. Warnings and
notes cover the quieter traps — a node with neither route nor children (the filter drops it), an empty
section, a root-level <Section> (no rail to sit in), Target="_blank" on an in-app route, badge SQL
that isn’t a plain SELECT, and routes that aren’t /object|/form|/view (whose permission check falls
back to the item’s Name).
Ownership
Section titled “Ownership”A navbar can be owned by its XML file or by the database, never ambiguously by both:
- XML-owned (the default) — model sync replaces the stored tree whenever the file’s hash changes.
- Designer-owned — set by a save from the designer; sync skips the scope so the edit survives the next deploy. Export the XML afterwards if you also want the repo file in step, or hand it back to XML from the banner to return to file-driven navigation.
| Method | Route | Returns |
|---|---|---|
GET |
/api/v1/genie/navbar |
SuccessDataResult<List<NavbarItem>> — the filtered tree, without badge values (returns fast regardless of item count) |
GET |
/api/v1/genie/navbar/badges |
SuccessDataResult<List<NavbarBadgeResult>> — computed badges ({ Name, Kind, Value, Color }), one per visible badge-bearing link |
Each NavbarItem carries Name, Kind, Label, Icon, Route, Target, Order, and Children.
The UI (useNavbar) renders the tree first, then calls /navbar/badges and merges each result onto the
matching item by Name, so badge counts fill in a moment after the navbar appears.
The designer’s own endpoints all sit under /api/v1/genie/navbar/designer and are
[Authorize(Roles = "System")]. Unlike the two above they return the authored tree — badge SQL and
VisibleIf included — so they are never served to a normal user:
| Method | Route | Purpose |
|---|---|---|
GET |
/navbar/designer/scopes |
stored scopes ({ Scope, IsDesignerOwned, UpdatedAt, NodeCount }) |
GET |
/navbar/designer?scope= |
the authored tree for a scope |
PUT |
/navbar/designer |
replace a scope’s whole tree (400 on any validation error) |
POST |
/navbar/designer/validate |
run the save-time checks without saving |
POST |
/navbar/designer/release?scope= |
clear designer ownership (XML sync takes over again) |
DELETE |
/navbar/designer?scope= |
delete a non-default scope |
GET |
/navbar/designer/xml?scope= |
the stored scope serialized to *.navbar.xml |
POST |
/navbar/designer/xml |
parse a posted XML document into a tree (does not save) |
POST |
/navbar/designer/to-xml |
serialize an in-progress tree to XML |
POST |
/navbar/designer/badge-preview |
run one badge query, returning { Value, ElapsedMs, Error } |
GET |
/navbar/designer/routes |
route catalogue for the picker (objects, wizards, built-in tools) |
An item’s Name is also the key a view’s <ParentView> maps to for its breadcrumb root. Worked
example: sample/Inventory/models/navbar/inventory.navbar.xml.