Model presentation metadata
Declare a model's list_display, search, list_filter, inline_edit and readonly columns ONCE on the model with #[umbral(...)] markers, and every plugin — admin, REST — reads them, with per-plugin overrides still available.
Model presentation metadata
A model's display and query intent — which columns show in a list, which are searchable, which are filters — used to be declared per plugin, far from the fields. You listed columns again in AdminModel::new(...).list_display(...).search_fields(...) for every model, and the REST plugin couldn't see any of it. Add a field and you edit two places.
Declare it once on the model instead. Per-field #[umbral(...)] markers are aggregated by #[derive(Model)] into model-level metadata that any plugin reads:
#[derive(Model, sqlx::FromRow, Serialize, Deserialize)]#[umbral(table = "software")]struct Software { id: i64, #[umbral(string, list_display, search)] name: String, #[umbral(list_display, list_filter)] category: String, #[umbral(search)] description: String, #[umbral(readonly)] slug: String,}The markers
| Marker | Declares | Read by |
|---|---|---|
#[umbral(list_display)] | Columns to show in a list/table view, in field order | Admin changelist columns |
#[umbral(search)] | Columns a free-text search matches | Admin search box, REST ?search= |
#[umbral(list_filter)] | Columns offered as list filters / facets | Admin filter dialog |
#[umbral(inline_edit)] | Columns editable inline in the list | Admin inline edit |
#[umbral(readonly)] | Columns rendered read-only in forms | Admin write guard |
The label side — a human __str__ for FK chips and titles — is the separate #[umbral(str = "...")] template.
Plugins read it automatically
Admin auto-derives its config
REST inherits searchable columns
Override per plugin
Model metadata is the default, not a mandate. An explicit plugin config always wins:
// This admin config overrides the model's declared list_display / search.AdminModel::new("software") .list_display(&["name", "category", "slug"]) .search_fields(&["name"])// And REST's ResourceConfig::search_fields overrides the inherited set.ResourceConfig::new("software").search_fields(&["name", "description"])The aggregated arrays carry the column name (honoring #[sqlx(rename = "...")]), so the metadata matches what a plugin queries and renders. An empty array means "not declared" — the consumer falls back to its own default (e.g. the admin's #[umbral(string)]-based list_display heuristic).
See the design rationale in planning/gaps4.md #95.