Skip to content

Shell & routing

The shell is what wraps the render surfaces: a router that turns a URL into the right component, a layout with a module rail + sidebar + header, the navbar fed from the engine, the auth screens, and the API client every part shares. createGenieApp wires these together — see Frontend configuration.

GenieRouter reads the current route (via useGenieRoute) and renders the matching component. The core data routes map a name to a hook + surface:

Route Renders Hook
/table/{name}?params GenieTable useGenieTable
/form/{name}?params GenieForm useGenieForm
/view/{name}?Id=… GenieView useGenieForm (read-only)
/wizard/{key}?id=&flow= GenieWizardRunner

Beyond these, the router handles the visual designers (/workflow-designer, /wizard-designer) and the admin tools (/object-explorer, /jobs, /hosted-services) — all lazy-loaded, so their (heavy) chunks download only when the route is opened, and only when the owning module is enabled (a disabled module shows a “not enabled” pane and never triggers the download). Any other non-root path becomes a path route — the extension point for host-registered custom pages — and / is home.

The table/view/form branches are keyed by route so navigating between records remounts the component with fresh state rather than leaking the previous view’s sort/filters. The router also owns the create/edit flow: a top-level table navigates to a /form page by default, while a view whose FormStyle is Modal opens the form in a dialog and refreshes in place on save. After a directed form submits, it returns to the table it was opened from (carried in the URL).

useGenieRoute (in shell/useGenieRoute.ts) parses window.location into a GenieRoute ({ kind, name, params, raw }) and re-renders on popstate and on programmatic navigation.

  • navigate(href, { replace? }) updates history (pushState / replaceState) and dispatches an internal event so the hook re-reads the route — no full-page load.
  • buildGenieHref(kind, name, params) builds a canonical href, e.g. buildGenieHref("table", "Orders", { Status: "Active" })/table/Orders?Status=Active.
  • normalizeGenieHref converts legacy hash routes from the navbar/DB (e.g. #/table/Name/c/k=v) into clean browser paths, so older stored routes keep working.

Routing mode is set by createGenieApp({ routing }): "browser" (default) uses History-API paths and requires the host to serve index.html for unknown paths (SPA fallback); "hash" avoids that server requirement. Auth/account pages always use the hash, independent of this setting.

GenieLayout is the three-column shell: a module rail, a contextual sidebar, and a header, on a has-rail grid. Its behaviour:

  • Modules are derived from the navbar’s top-level items. Clicking a rail module swaps the sidebar (or navigates, for a direct-link module); the active module is inferred from the current route, with a manual selection winning until navigation moves elsewhere.
  • Collapse — the sidebar collapses to a rail-only view (persisted to localStorage); on mobile the rail + sidebar become an off-canvas drawer that auto-closes on navigation. The full-canvas designers auto-collapse the sidebar for room and restore the preference on exit.
  • Header — the hamburger, an optional global search (nav + configured datasets, focused with Ctrl/Cmd-Shift-F when headerSearch is enabled), the accent picker and light/dark toggle (both from useGenieTheme), and a host-supplied right slot (notifications, user menu).
  • Breadcrumb — rendered from the current route + nav items; a per-route error boundary isolates a render failure to its page.

Theming (data-theme / data-accent, persistence, the picker) is covered in Theming.

GenieNavbar renders the contextual sidebar: a module-context header, a menu search, and the active module’s navigation tree (up to three levels, collapsible groups). Typing in the menu search switches to a flat cross-module result list so nothing hides behind module selection. It consumes the navbar JSON from the engine — nodes of kind Section (heading), Divider, or a regular item/group — and renders right-aligned badges: a numeric count pill or a themed status dot. Where the tree comes from (the permission-filtered /navbar endpoint or a host-supplied static tree) is set by navbar.source. See Navigation for the navbar model and Frontend configuration for the config.

The auth experience — login, MFA/TOTP, forgot/reset password, change password, profile — lives under auth/. GenieAuthProvider holds the session; useAuth exposes the current user, roles, and sign-in/out. The auth screens run on hash routes (independent of the data-page routing mode) and their affordances adapt to the server’s enabled features, read from GET /auth/config (which login methods, MFA channels, reCAPTCHA, password rules are on) — so nothing is hardcoded on the client. The login/forgot/reset screens render beside a gradient brand panel customizable via the auth block (see Theming). While the session resolves and the authenticated app-shell chunk downloads, a branded loading splash shows (replaceable via preloader).

GenieApiClient (api/client.ts) is the single fetch layer every part shares. It:

  • Carries the JWT — login returns an access token (not a cookie); the client stores it in localStorage, sends it as Authorization: Bearer …, and (for the WebSocket transport, which can’t set a header) exposes it for the SignalR access_token query.
  • Unwraps the envelope — the engine wraps every response in { Success, Error, Data }; the client returns Data (or throws a GenieApiError carrying the server’s message) so callers work with plain payloads. Error bodies are reduced to a human-readable message; raw JSON is never shown to the user.
  • Sends context headers — the antiforgery token (X-XSRF-TOKEN) on unsafe requests and the browser’s IANA time zone (X-TimeZone) on every request, plus credentials: "include" for the cookie/company/timezone session.
  • Handles 401 — a 401 on any non-auth endpoint fires the registered unauthorized handler (the auth provider signs out and bounces to login); auth-endpoint 401s are left to their callers so a bad login doesn’t loop.
  • Caches structure & de-dupes requests — object metadata is cached in-memory (schemaCacheTtlSeconds), and concurrent identical reads (every GET plus the read-style POSTs) share one in-flight fetch. Mutations are never coalesced. See Request de-duplication.

Because the auth, notification, wizard, workflow, and Hangfire controllers sit as siblings of the /genie base, the client derives those bases from apiBase (e.g. .../auth, .../notification) — so you only ever configure the one apiBase.