Detail pages
A detail page shows one record, read-only, at GET /admin/{slug}/{id}. A resource declares it
with ResourceDef::view: a Schema built from the same fields and layout blocks as a form, which
the panel builds once when it mounts. The view defaults to the form, so a resource with a form has
a detail page showing the form’s fields read-only. A form with fields that must stay off the
detail page sets a view with a subset or Schema::empty(). Set it to show other fields or another
layout:
#![allow(unused)]
fn main() {
impl Resource for PostResource {
// …
fn declare() -> ResourceDef<Self> {
let c = PostForm::controls();
ResourceDef::new()
// …
.view(Schema::new(Section::new("Post").schema((
c.title,
c.body.multiline(6),
c.status,
))))
}
}
}
A non-empty view adds a View action to each row. An empty one, Schema::empty(), turns the
detail page off: the route answers 404 and no row links to it. A NoForm resource has an empty
view unless it declares one.
What the page shows
The header carries the record’s title, a link back to the list, and an Edit link when the resource
has a form and the policy allows Update of this record. Below it come the view’s fields, then any
free-form content, then the related tables.
Each field renders its label and its stored value, never a control:
- a text field shows the value as text, typed values included;
- a choice shows the label of the matching option, or the stored value when none matches, so a relationship choice shows the stored key, not the related record’s name;
- a file field shows the stored path as a link, under the rules in File uploads;
- an embedded enum shows its variant’s name and that variant’s fields;
- layout blocks keep their structure.
Where values come from. A field the record form binds shows the form’s value for it, the
same value the edit form starts with. view_values(cx, record) supplies every other key, as a map
from field name to display text; when both supply a key, the form’s value wins. A NoForm
resource supplies every key there:
#![allow(unused)]
fn main() {
pub fn view_values(_cx: &Cx, audit: &Audit) -> HashMap<String, String> {
HashMap::from([
("action".to_string(), audit.action.clone()),
("created_at".to_string(), audit.created_at.to_string()),
])
}
}
To show something that is not a column’s own value, such as the author’s name behind
author_id, render it as free-form content.
A field no source fills renders (missing), and fails a debug_assert! in debug builds.
Loading. The record loads through view_query — query by default — with the tenant scope
applied. Include there every relation view_values or view_content reads:
#![allow(unused)]
fn main() {
impl Resource for PostResource {
// …
fn view_query(cx: &Cx) -> Query<List<Post>> {
let author: Include<Post, Author> = Post::fields().author().into();
Self::query(cx).include(author)
}
}
}
An unknown id and an id outside the request’s tenant are the same 404; a record the policy may not
View is a 403.
Title. record_label sets the page title; without it the title is the resource’s label() and
the record’s key, such as “Post 3f2a…”:
#![allow(unused)]
fn main() {
pub fn record_label(_cx: &Cx, post: &Post) -> Option<String> {
Some(post.title.clone())
}
}
public_url(cx, record) adds a “View public post” link to the header when it returns a URL.
Free-form content
view_content(cx, record) renders anything that is not a field, such as a word count, below the
fields:
#![allow(unused)]
fn main() {
pub fn view_content<'a>(cx: &'a Cx, post: &Post) -> Option<BoxView<'a>> {
let words = post.body.split_whitespace().count();
Some(
view! {
cx =>
<p class="text-sm text-muted-foreground">(format!("{words} words"))</p>
}
.boxed(),
)
}
}
The returned view may borrow cx but not the record: compute what you need from the record first.
Related tables
ResourceDef::relation adds a related resource whose rows belong to a record. Each renders on
the record’s detail and edit pages as that resource’s own list table, narrowed to the record:
#![allow(unused)]
fn main() {
impl Resource for PostResource {
// …
fn declare() -> ResourceDef<Self> {
ResourceDef::new()
// …
// The related model's foreign key, which holds the post's primary key.
.relation(Relation::has_many::<CommentResource>(
Comment::fields().post_id(),
))
}
}
}
The foreign key’s type is the owner’s primary key type, or its Option for a nullable foreign
key; any other type does not compile. The table is
CommentResource’s — its columns, search, sort, filters and pager — over its tenant-scoped query
plus post_id = <this post>. It is titled with the related resource’s plural label, or
.label(..). The panel must register the related resource too: mounting refuses a relation to
one it does not.
- Policy. The related resource’s policies apply as on its own list: no section renders when
its policy refuses
ViewAny, or when it is tenant-scoped and the request has no tenant, and each row keeps only the actions its record allows. - Detail page versus edit page. On the detail page the table is read-only: rows keep only
their View action. On the edit page rows also carry Edit and Delete, the table has bulk delete,
and a create button appears when the related resource has a form and its policy allows
Create. - Creating from the parent. The create button opens the related resource’s create page with
the owner preselected, as in
/admin/comments/create?post_id=…. The create page accepts such a parameter only for a relationship choice; the value is a default, and the submitted form is validated like any other create. - Returning. Writes started from a related table carry
?return=and redirect back to the page they started on. The panel followsreturnonly to a path under its own prefix. - URL parameters. Each related table’s parameters are prefixed with the related resource’s
slug —
?comments.q=,?comments.sort=,?comments.after=— so several tables share one page. - Live search. A related table whose resource declares
live_search()stays live: sorting, searching, filtering and paging re-render that table in place. Without JavaScript its links and forms fall back to full page loads.
Mounting the panel refuses a relation to a resource the panel does not register, and two relations of one resource to the same related resource.