Petstore: Pet Detail
The pet detail page is where a console page gets a nested identity. One pet has four tabs (Summary, Photo, Orders and
History) and each tab is its own URL under /console/pets/{id}/{tab}. The Orders tab goes one level deeper:
/console/pets/{id}/orders/{orderId} selects one of the pet's orders. The navigation tree grows to match, so this
is also the petstore's example of depth-4 navigation with nodes that are added per request.
What this page shows

- Per-request navigation nodes. The page appends nodes under the
petsnode:p<petId>with childrensummary,photo,ordersandhistory, ando<orderId>underorderswhen an order is selected. Node ids must start with a letter, so the numeric ids are prefixed. The active tab ispets/p<id>/<tab>, orpets/p<id>/orders/o<orderId>at depth 4. - Summary is a one-row client-mode table over
GET /console/pets/{id}/row. The name and price cells use theeditrenderer: a pencil that commits an inline edit through a hidden row action. - Photo is an
htmlcard holding an<object>that points at the API's own photo endpoint, with fallback text when no photo is uploaded. Admins also get an "Upload photo" card; for other viewers the template leaves it out on the server. - Orders is a table of this pet's orders with a row detail, plus an
htmlcard for the selected order. - History is the pet's audit entries, newest first.
- Not found. An unknown pet renders
pet-not-found.ftlhinside the chrome with a 404 status, rather than an error page. An unknown tab, or an order that does not exist or belongs to another pet, is a plain 404.
The bare URL /console/pets/{id} answers with a 303 to the Summary tab.
Inline edit stages a change
The pencil does not write to the pet. It posts to POST /console/pets/{id}/stage, which records a pending
change that someone reviews on the Pending changes page before it is applied. The body is either {field, value} or
the inline-edit envelope {fields: {<field>: value}} with exactly one field.
Staging accepts name, price, species, status and tags; the Summary table's pencil cells edit name and price.
The sold-pet case is handled by the actions' visibleWhen rule: a sold pet shows no edit actions.
Every editable Pet field stages into pending changes except the photo. The photo is not stageable: it is a binary
payload, so there is no meaningful before-and-after to show in a review, and no sensible way to discard half of
an upload. Staging it is refused with HTTP 400 and the message
Field 'photo' cannot be staged; use one of: name, price, species, status, tags.
The Photo tab edits the photo directly, through the API's own PUT /petstore/pets/{id}/photo.
Template
The template is pet.ftlh, one template for all four tabs, switched on a tab model attribute. The nodes and the
Summary card, trimmed:
<@page tab=activeNav toolkit="views">
<@node under="pets" id="p${pet.id?c}" label=pet.name href="/console/pets/${pet.id?c}/summary">
<@node id="summary" label="Summary" href="/console/pets/${pet.id?c}/summary"/>
<@node id="orders" label="Orders" href="/console/pets/${pet.id?c}/orders">
<#if order??><@node id="o${order.id?c}" label="Order ${order.id?c}" href="/console/pets/${pet.id?c}/orders/${order.id?c}"/></#if>
</@node>
</@node>
<#if tab == "summary">
<@card type="datatables" id="pet" title=pet.name>
{ rowType: 'Pet', dataMode: 'client', dataUrl: '/console/pets/${pet.id?c}/row',
columns: [ { key: 'name', label: 'Name', render: { id: 'edit', meta: { action: 'rename', column: 'name', input: 'text' } } } ],
rowActions: [ { id: 'rename', label: 'Rename', endpoint: '/console/pets/{id}/stage', method: 'POST', present: 'inline' } ] }
</@card>
Builder twin
PetsPageBuilders.pet takes the pet, the tab, the active nav path, the selected order and the viewer's roles, and
returns a PageSpec. The roles go into the page facts, so a client-side rule could read them too.
PageSpec.create().tab(activeNav).toolkit("views").title(pet.getName())
.facts(Map.of("viewer", Map.of("roles", roles)))
.navUnder("pets", n -> {
var node = n.add("p" + id, pet.getName(), href + "/summary");
node.add("summary", "Summary", href + "/summary");
var orders = node.add("orders", "Orders", href + "/orders");
if (order != null)
orders.add("o" + order.getId(), "Order " + order.getId(), href + "/orders/" + order.getId());
});
Request any tab with ?builder to render it through this path.
Source and tests
Source: juneau-petstore/juneau-petstore-core, classes PetsRest (the /{id}/{tab} and /{id}/orders/{orderId}
operations), PetsPageBuilders (in the pages package), PendingChanges (in the service package, which owns the
stageable field list), and the templates pet.ftlh, pet-not-found.ftlh and fragments/pet-photo.ftlh.
Tests: PetDetail_Test (redirect, depth 3 and 4, unknown pet, tab and order, inline-edit envelope, photo upload
visibility), PetsRest_Test (c05_photoIsNotStageable), PendingChanges_Test and PetstoreTwins_Test.
See also
- Twin pattern: see 19.17. Template and Builder Twins.
- Petstore: Pets: the table this page is reached from.
- Petstore: Orders: where an order id links back to.
- Petstore: Pending Changes: where a staged edit is reviewed and applied.
- Rich Table Views: Actions: row actions and the
editrenderer's hidden action. - Admin-Console Chrome: the navigation tree and the page contract.