Skip to main content

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​

The summary tab of pet 1, with its Summary, Photo, Orders and History subtabs

  • Per-request navigation nodes. The page appends nodes under the pets node: p<petId> with children summary, photo, orders and history, and o<orderId> under orders when an order is selected. Node ids must start with a letter, so the numeric ids are prefixed. The active tab is pets/p<id>/<tab>, or pets/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 the edit renderer: a pencil that commits an inline edit through a hidden row action.
  • Photo is an html card 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 html card for the selected order.
  • History is the pet's audit entries, newest first.
  • Not found. An unknown pet renders pet-not-found.ftlh inside 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.

Photo-field exception

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​