#$pageAdmin
#Import
1import { $pageAdmin } from "@alepha/ui/admin";
#Overview
$pageNav already parented to AdminRouter's /admin shell: the
one-call form of "a page inside the shared admin shell". It exists because
the alternative (injecting AdminRouter and writing
parent: this.admin.layout by hand on every page) puts the rules of that
composition nowhere an author will read them.
It carries the $ prefix to sit with the framework's other declarations at
a call site, but it is a plain function wrapping $pageNav (which in turn
wraps $page) rather than a createPrimitive primitive. Nothing about
its lifecycle differs from declaring $page yourself.
The page appears in the shell's sidebar with no separate registration
step: useNavEntries walks the parent chain and reads each page's own
nav, the same as any other page hung off AdminRouter.layout.
Its tab reads Admin - <title>, the way every built-in page's does: a
static head.title or the title a head function returns is prefixed,
with the prefix in the reader's language (admin.title). The title itself
is the application's to translate: a static one is used as written.
When the page names no nav.label and no label, its unprefixed static
title becomes the label, so its sidebar entry and breadcrumb do not carry
the prefix.
Calling this registers AdminRouter. Declaring even one page this way
mounts the whole /admin shell, including its fourteen built-in pages (Users,
Sessions, Jobs, …): an admin page without the admin shell around it is
not a thing. This is the intended reading, but it is a real side effect:
an application that wants /admin to carry only its own pages, with none
of the built-ins, must build its own layout page instead of reaching for
this helper.
Stay below order: 1000, or declare your own nav.group. The
built-ins are parked in a reserved high band, Identity (1000-1003) and
System (1010-1016), precisely so an application's own pages come first:
the conventional order: 100 sorts above them. useNavEntries sorts groups
by their smallest member, so only an order of 1000 or more sinks a page in
among the built-ins.
Gate with can: () => this.someApi.someAction.can(), not with
permission alone. A permission named by this page's own $secure is
self-declaring: $secure registers it into SecurityProvider at
definition time, so an admin holding the * wildcard is granted it
whether or not any controller backing the page exists, and the entry would
stay visible over a dead API. An action name resolves against
/api/_links, a registry built only from actions the server actually
registered, so a page whose backend never shipped never appears for
anyone to be granted.
1class ShopAdminRouter {2 products = $pageAdmin({3 path: "/products",4 nav: { label: "Catalogue", icon: <Gem />, group: "Commerce", order: 100 },5 can: () => this.productApi.commerceAdminProductList.can(),6 lazy: () => import("./AdminProducts.tsx"),7 });8}