pub struct ResourceDef<R: Resource> { /* private fields */ }Expand description
What a Resource declares: its names, navigation, policy, tenancy, table, form, detail page,
relations and actions.
Resource::declare returns one, and Panel::resource_with
adjusts it for one panel. Every setting has a default, so a resource sets only what differs:
use tablo_core::{ModelColumns, ReadOnly, Resource, ResourceDef, TernaryFilter, lens};
impl Resource for PostResource {
type Model = Post;
type Form = PostForm;
fn declare() -> ResourceDef<Self> {
ResourceDef::new()
.label("Blog post")
.policy(ReadOnly)
.table(Post::table().filters(TernaryFilter::new(lens!(Post.featured))))
}
}The panel builds the def once when it mounts, binding the paths it names to the database schema, and serves the result to every request.
Implementations§
Source§impl<R: Resource> ResourceDef<R>
impl<R: Resource> ResourceDef<R>
Sourcepub fn new() -> Self
pub fn new() -> Self
A def with every default: the policy denies all, rows belong to no tenant, the model’s
ModelColumns give the table and the detail page, and the record
form gives the form.
Sourcepub fn slug(self, slug: impl Into<String>) -> Self
pub fn slug(self, slug: impl Into<String>) -> Self
The URL slug for the resource’s pages: "users" mounts the list at {panel prefix}/users.
Defaults to the resource type’s name without a trailing Resource, pluralized and
kebab-cased: UserResource → users, BlogPostResource → blog-posts.
Sourcepub fn label(self, label: impl Into<Text>) -> Self
pub fn label(self, label: impl Into<Text>) -> Self
One record’s name, the noun in the “Create {label}” and “Edit {label}” titles.
Defaults to the message resource-{model} of the panel’s catalogs, else the model’s type
name in sentence case: BlogPost reads resource-blog-post, else Blog post, and
APIKey reads resource-api-key, else API key.
Sourcepub fn plural_label(self, label: impl Into<Text>) -> Self
pub fn plural_label(self, label: impl Into<Text>) -> Self
The list title and sidebar label.
Defaults to the plural of the label: a plain label pluralized in English,
Category reading Categories and Person People, and a message’s attribute plural.
The default label’s message is resource-{model}, so its plural is
resource-{model}.plural, else the derived English noun pluralized.
resource-blog-post = Article
.plural = ArticlesSourcepub fn icon(self, icon: IconData) -> Self
pub fn icon(self, icon: IconData) -> Self
The icon of the resource’s sidebar entry, replacing the icon of a
navigation item.
Where the sidebar entry sorts among the panel’s entries, lowest first; defaults to 0, and
entries of equal order keep their registration order. It replaces the order of a
navigation item.
The labelled sidebar group the entry renders in, such as "Content"; defaults to none,
above every group. It replaces the group of a navigation item.
Replaces the sidebar entry, for one that links somewhere other than the list page:
NavigationItem::at("Drafts", "/admin/posts?f.status=draft"). icon,
navigation_order,
navigation_group and
navigation_badge still apply to it.
Badge the sidebar entry with the number of rows of scoped_query
that filter matches, so a tenant counts its own rows. The panel counts them each time its
sidebar renders, on a page load and on each runtime rerun such as a sort or a page step,
one query per badge run together, and shows no badge for zero. It replaces the
badge of a navigation item.
fn declare() -> tablo_core::ResourceDef<Self> {
// "Posts 3" while three drafts wait.
tablo_core::ResourceDef::new().navigation_badge(Post::fields().draft().eq(true))
}Sourcepub fn policy(self, policy: impl Policy<R::Model>) -> Self
pub fn policy(self, policy: impl Policy<R::Model>) -> Self
Authorizes every handler; defaults to Deny. Row scoping belongs in
Resource::query, not the policy.
Sourcepub fn policy_fn(
self,
policy: impl Fn(&Cx, Ability<'_, R::Model>) -> bool + Send + Sync + 'static,
) -> Self
pub fn policy_fn( self, policy: impl Fn(&Cx, Ability<'_, R::Model>) -> bool + Send + Sync + 'static, ) -> Self
Authorizes every handler with a closure, as policy does, inferring the
closure’s argument types from the def’s model:
use tablo_core::{Ability, Panel};
Panel::new("portal").resource_with::<PostResource>(|def| {
// No update, delete, restore or record action on a locked post.
def.policy_fn(|_cx, ability| ability.written_record().is_none_or(|post| !post.locked))
});In Resource::declare, the chain on ResourceDef::new() learns its resource from the
return type, after the closure is checked, so a closure that reads the record names the
ability’s type, |_cx, ability: Ability<'_, Post>|, or the chain starts from
ResourceDef::<Self>::new().
policy takes a closure too, but infers neither argument type: Rust infers
a closure’s signature from an Fn bound, not from Policy’s blanket impl.
Sourcepub fn soft_delete(self, lens: Lens<R::Model, Option<Timestamp>>) -> Self
pub fn soft_delete(self, lens: Lens<R::Model, Option<Timestamp>>) -> Self
Deletes by stamping lens’s column with the time instead of removing the row, as in
.soft_delete(lens!(Post.deleted_at)), and keeps the stamped records out of every loader
unless it asks for them.
The list hides the trashed records behind a trash control, and offers Restore, which
empties the column, and Force delete, which removes the row, on each one: the policy decides
who may through Ability::Restore and Ability::ForceDelete. The detail page opens a
trashed record; its edit page and every other action refuse one until it is restored.
scoped_query admits the live records only, and
scoped_query_in the ones a Trashed scope
names, each under the tenant filter.
The unique pre-check probes the trashed records too, since a unique index covers them. Mounting the panel refuses a lens that is not one field of the model, and a record form that writes the column.
Sourcepub fn field_access<T>(
self,
path: impl Into<Path<R::Model, T>>,
access: FieldAccess,
) -> Self
pub fn field_access<T>( self, path: impl Into<Path<R::Model, T>>, access: FieldAccess, ) -> Self
Restricts who views and who changes the field path names, as in
.field_access(lens!(Employee.salary), FieldAccess::new().view(is_hr)): see
FieldAccess.
The rule restricts the field of its model within the panel: every resource the panel mounts over the model drops what reads a field the request may not view, the table’s and the detail page’s columns, the filters and the grouping, the export’s columns, the searches of the list and of the relationship choices over those resources, and the relation columns showing it. Each form renders nothing for the field, or a disabled control for one it may view but not change, and refuses a submission posting either with 400. A create the field needs a value for, with no default, is refused.
Hiding a belongs_to relation hides its foreign keys, and hiding a key hides the relation.
Mounting the panel refuses a path that is not one field of the model, a second declaration
over one field, a primary key with a view rule, a record title or a form condition reading
a restricted field, and a column or a filter that does not say which fields it
reads, and the declarations FieldAccessFault
lists that would show a restricted record no panel can mask.
A validation error on a field the request may not view shows as an alert that a hidden field refuses the save, without its message.
Sourcepub fn tenancy(self, tenancy: Tenancy<R::Model>) -> Self
pub fn tenancy(self, tenancy: Tenancy<R::Model>) -> Self
How rows belong to a tenant. A resource that declares none is served to every tenant, and
mounting the panel refuses it when its model has a TenantId column.
Sourcepub fn table(self, table: Table<R::Model>) -> Self
pub fn table(self, table: Table<R::Model>) -> Self
The list table; defaults to the model’s (ModelColumns::table).
Sourcepub fn form(self, form: Schema<R::Form>) -> Self
pub fn form(self, form: Schema<R::Form>) -> Self
Arranges the create and edit forms: the record form’s own controls, from its
controls(), in layout blocks.
A control the schema does not place follows the ones it does, in the record form’s declaration order and outside every layout block, so the default form renders one control per field, and a form that adjusts one control places only that one:
use tablo_core::{Resource, ResourceDef, Schema};
impl Resource for UserResource {
type Model = User;
type Form = UserForm;
fn declare() -> ResourceDef<Self> {
// `email`, then `name`.
ResourceDef::new().form(Schema::new(UserForm::controls().email.email()))
}
}Sourcepub fn detail(self, detail: Detail<R::Model>) -> Self
pub fn detail(self, detail: Detail<R::Model>) -> Self
The detail page: columns in layout blocks; defaults to the model’s
(ModelColumns::detail), and Detail::empty turns the page off: its route answers
404 and no row links to it. A non-empty detail adds a View action to each row.
The page, at {list}/{key}, carries in its header the record’s title, a link back to the
list, the actions placed on Places::DETAIL and Delete as the
policy allows them on the record, and Edit when the resource has a form and the policy
allows Update. The related tables follow its columns.
The record loads through the resource’s tenant-scoped query with every
relation the columns declare, once each, wherever its block sits; no other relation
loads. An unknown key and a key outside the request’s tenant are the same 404, and a
record the policy refuses View is a 403. The form does not shape the
page: a resource that arranges its form declares a detail to show the same arrangement.
Sourcepub fn record_title<T>(self, lens: Lens<R::Model, T>) -> Self
pub fn record_title<T>(self, lens: Lens<R::Model, T>) -> Self
Titles each record with the text column lens reads, as in
.record_title(lens!(Post.title)): the detail page’s heading, each option of a
relationship choice over this resource, and each cell
of a RelationColumn::of it.
A record whose column is empty, and every record by default, is titled with the
label and its key, as in Blog Post 3f2a…. A relationship choice over this
resource also searches the column, besides the table’s searchable columns.
A title is display text, not a key: two records can share one, so it never replaces the primary key that keys the table’s rows and the action routes. A lens names one of the model’s own columns, never a relation, so the title reads on every record however it was loaded; a title built from several columns is a column the model stores.
Sourcepub fn public_link(
self,
link: impl Fn(&Cx, &R::Model) -> Option<PublicLink> + Send + Sync + 'static,
) -> Self
pub fn public_link( self, link: impl Fn(&Cx, &R::Model) -> Option<PublicLink> + Send + Sync + 'static, ) -> Self
Links a record’s public page from its detail and edit pages, for each record link
returns one for; defaults to no link.
The edit page loads the record without relations, so a link that reads one renders on the detail page only when its relation is loaded there.
Sourcepub fn relation(self, relation: Relation<R::Model>) -> Self
pub fn relation(self, relation: Relation<R::Model>) -> Self
Adds a related resource, rendered as its table narrowed to the record on the detail and edit pages.
Sourcepub fn action<A: Action<R>>(self) -> Self
pub fn action<A: Action<R>>(self) -> Self
Adds the custom Action A after the ones already declared.
The policy decides who may run it, through
Ability::RunAny and Ability::Run. An
A::NAME that is not a single URL path segment does not compile.
Sourcepub fn header_action<A: HeaderAction>(self) -> Self
pub fn header_action<A: HeaderAction>(self) -> Self
Adds the HeaderAction A, which acts on no record, to the list page’s header, after
the ones already declared.
The policy decides who may run it, through Ability::ViewAny
and Ability::RunHeader with its name, which no other action
of the resource may share. An A::NAME that is not
a single URL path segment does not compile.
struct RecountTags;
impl HeaderAction for RecountTags {
type Input = ();
const NAME: &'static str = "recount/tags";
async fn run(_: &Cx, _: (), _: &mut dyn toasty::Executor) -> Result<tablo_core::Outcome, tablo_core::WriteError> {
Ok(tablo_core::Outcome::done())
}
}
let _def = ResourceDef::<PostResource>::new().header_action::<RecountTags>();Sourcepub fn create_column<T>(self, path: impl Into<Path<R::Model, T>>) -> Self
pub fn create_column<T>(self, path: impl Into<Path<R::Model, T>>) -> Self
Declares a column an overriding Resource::create_record sets itself, beyond the form’s
fields: .create_column(lens!(Post.slug)). Call it once per column.
Mounting the panel refuses a path that names no one field of the model, and the tenant column, which the framework stamps.
Sourcepub fn edit_only(self) -> Self
pub fn edit_only(self) -> Self
Serves the edit page but no create page, for records that arrive another way: an import, a seed, an action.
A resource with a record form creates through it unless it opts out here; the policy decides who may create, per request, but never whether the page exists. Without the page, mounting skips the check that a create fills every non-nullable column, and the list and the relations offer no create button.
Sourcepub fn importable(self) -> Self
pub fn importable(self) -> Self
Serves a CSV import beside the create page: the list links Import CSV to a page that takes a file whose header names the form’s keys, and creates one record per row through the create form, all in one transaction or none.
Each row is checked as a create submission is, under the same policy, field access and tenant. Mounting refuses an importable resource that serves no create page.
- The first row names the columns, each by its form key or its field’s label in any case:
the controls the request may change, but file fields, repeaters and controls disabled on
the create page. A header naming any other column, or a label two fields carry, refuses
the file. Download template at
{list}/-/import/templatereturns the header row. - Each cell posts what the create form’s control would: an option’s value,
trueorfalsefor a checkbox, a date and time in the request’s time zone. A blank cell posts a blank value, and a column the header leaves out takes the field’sdefault. A multiple choice takes one column per value, its key repeated; no other column may repeat. - The file is UTF-8 text with commas or semicolons between cells, of at most
MAX_IMPORT_ROWSrows, 500 cells a row and 10 MiB. A row whose every cell is blank is skipped. Resource::before_commitreceives each row’sChange::Createinside the transaction. A rowResource::create_recordrefuses rolls back to a savepoint taken before it, so the rows after it are checked without its writes. When any row is refused nothing is written, and the page lists each refusal by row number, as a spreadsheet numbers it, and column. Once every row passes, the import lands on the list andResource::after_commitreceives each row’s change, in file order.
The transaction lasts the whole file: on SQLite it holds the write lock that long. The
import reads what the form posts, not what the export writes: an exported $1,234.50,
Yes or option label does not import back.
Source§impl<R: Resource> ResourceDef<R>
impl<R: Resource> ResourceDef<R>
Sourcepub fn global_search(self, search: GlobalSearch<R::Model>) -> Self
pub fn global_search(self, search: GlobalSearch<R::Model>) -> Self
Declares how the resource’s records show in the panel’s global search: the details under each result’s title. Without it a result shows its title alone.
The search box in the panel’s topbar searches every resource the panel mounts unless the
resource opts out with without_global_search. It matches
a record when each word of the term is in one of the table’s searchable columns or in the
record_title column, which also titles each result. It lists only
the records the resource’s list would, and none to a request the list refuses: one the
policy refuses ViewAny, or one without a tenant on a tenant-scoped resource. Each
result opens the record’s detail page, or its edit page on a resource with an empty
detail; a record the policy refuses View, or Update for the edit
page, is left out.
See Panel::without_global_search for the panel’s
side.
Sourcepub fn without_global_search(self) -> Self
pub fn without_global_search(self) -> Self
Leaves the resource’s records out of the panel’s global search.