Skip to content

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.

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.)

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.

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 an Item with children) is visible only if it has at least one visible descendant.
  • A Section is 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.

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.

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.

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).

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.