Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Descriptor Files

One YAML file describes one resource: the table, where it mounts, who may reach it, and its list and form.

# descriptors/posts.yaml
entity: posts
path: /posts
title: Posts
permission: acme.blog.manage_posts
list:
  creatable: true
  deletable: true
  order_by: created_at
  order_dir: desc
  no_records_message: No posts yet.
  search: { prompt: Search posts }
  columns:
    title:      { searchable: true }
    status:     { type: status_pill, width: 10% }
    created_at: { type: datetime, align: right }
  filters:
    status: { type: select, options: [{ value: draft, label: Draft }] }
form:
  title: Post
  fields:
    title:  { span: 2/3, rules: [required, { max_length: 120 }] }
    status: { span: 1/3, type: select, options: [{ value: draft, label: Draft }] }
    body:   { type: textarea, rows: 8 }

Register it from your module:

registry.add_resource(laterite_admin::resource!("descriptors/posts.yaml"));

The path is relative to your Cargo.toml. The file is read at compile time: a missing or malformed one fails the build.

Resource keys

KeyDescription
entityThe table both screens read and write. Required.
pathWhere the resource mounts, under your module’s namespace. Required.
titleThe navigation label, and the default title of both screens. Required.
permissionThe permission gating it. Required once a form is present.
id_fieldThe primary key column. Default id.
listThe list screen. Required.
formThe create and edit screen. Optional.
importtrue, or { permission: ... }, to let records be imported into the resource. Needs form. Optional.
relationsRelated records, keyed by name, each a section on the edit page. See Related Records. Needs form. Optional.

entity, path and id_field reach both screens.

Naming

Columns, fields and filters are maps. The key is the name, and they render in the order written:

columns:
  title: {}
  created_at: { type: datetime }

A missing label is made from the key: created_at becomes “Created at”. It is still translatable, and lat i18n extract finds it, as it finds the label of every entry under a column’s labels.

Type keys

An entry carries its own keys and its type’s, side by side:

fields:
  body:   { type: textarea, rows: 8, placeholder: Write here }
  price:  { input: number, min: 0, step: 0.01 }
  status: { type: select, options: [{ value: draft, label: Draft }] }
filters:
  status: { type: select, options: [{ value: draft, label: Draft }] }
TypeKeys
textinput (text, email, tel, number, url), placeholder, counter. A number input adds min, max, step; a url input adds copy.
textarearows, placeholder, grow, counter
selectoptions: a list of { value, label }; search
radiooptions
checklistoptions, select_all, search, expand. See Checklist.
referencesource. The chosen id is stored as a number.
idNone. A record id held as a number, as a hidden input; a relation’s foreign key is one.
repeaterfields, min_items, max_items, display, summary_field, reorder, duplicate
passwordreveal
switch, dateNone
A select filteroptions

A key the type does not read is refused, naming the entry and the keys it accepts.

Checklist

Boxes in groups. The column holds the ticked values as a JSON array of text.

fields:
  topics:
    type: checklist
    rules: [required]        # a box must be ticked
    options:
      - { value: news, label: News }
      - group: Sport
        options:
          - { value: football, label: Football, note: Every league }
          - { value: cricket, label: Cricket }
    select_all: auto
    search: auto
    expand: auto
KeyValuesDefault
optionsA list of { value, label, note } and { group, options }, nested to any depth.None
select_allauto, true, falseauto: past 10 choices
searchauto, true, falseauto: past 20 choices
expandauto, all, noneauto: past 10 choices in more than one group, a group with every box ticked or none starts closed and a group partly ticked starts open

A value listed twice stops the boot, naming it.

Form keys

form:
  enter: off
  confirm_leave: false
  focus: off
  fields:
    title: { rules: [{ max_length: 80 }], counter: true }
    body:  { type: textarea, rows: 8, grow: false }
    token: { type: password, reveal: false }
KeyOnValuesDefault
enterThe formsubmit, offsubmit
confirm_leaveThe formtrue, falsetrue: leaving with changes unsaved asks first
focusThe formauto, offauto: a new record opens with the cursor in its first field, a refused save in the first field it refused
enterA fieldsubmit, off, nextThe form’s
countertext, textareaauto, true, falseauto: with a max_length rule, shown once four fifths of it are used
growtextareatrue, falsetrue: as tall as its text, from rows up to most of the window
revealpasswordtrue, falsetrue: a button shows what was typed
searchselectauto, true, falseauto: past 10 choices, the dropdown is searched. An option with an empty value is what clearing chooses.
displayrepeaterauto, inline, listauto: past 3 fields a row, rows collapse to the line naming them
reorderrepeatertrue, falsetrue: each row moves up and down
duplicaterepeatertrue, falsetrue: each row copies into a new one beneath it

A max_length rule also sets the control’s maxlength.

Dependencies

fields:
  title: {}
  slug:  { preset: title }
  path:  { preset: { field: title, type: url } }
  is_delayed: { type: switch, label: Send later }
  send_at:
    type: date
    trigger: { action: show, field: is_delayed, condition: checked }
  format: { type: select, options: [{ value: csv }, { value: xml }] }
  delimiter:
    trigger: { action: show|empty, field: format, condition: "value[csv]" }
  topics: { type: checklist, options: [{ value: news }, { value: sport }] }
  region:
    trigger: { action: enable, field: "topics[]", condition: "value[sport]" }
KeyValue
presetThe field followed, or { field, type }. Follows it as it is typed, until this field is edited; emptying this field hands it back.
preset.typeslug (default): hello-world. url: /hello-world. file: spaces to hyphens. exact: as typed.
trigger.actionshow, hide, enable, disable, empty, fill[value], several joined by |. A hidden field is left out of the submission.
trigger.fieldThe field watched. name[] watches every ticked value of a checklist.
trigger.conditionchecked, unchecked, value[x], value[x][y] (either), value[] (empty), value[*] (anything), value[csv*] (a prefix).

A field that follows or watches one the form does not have stops the boot. An action or condition the page does not know is refused when the file is read.

Layout

Fields lay out on a twelve-column row and wrap as it fills.

KeyDescription
spanThe share of the row: full (default), 1/2, 1/3, 2/3, 1/4, 3/4, or 1 to 12 columns. left, right, auto and half are halves; third and quarter as named.
breaktrue starts a new row.

Under 1100px a quarter becomes a half; under 768px every field takes the row.

Errors

A key the framework does not know is refused with its line:

error: line 11 column 19: unknown field `searchabel`, expected one of
label, type, sortable, invisible, width, align, permission, searchable

Write only the keys you set; everything else takes its default.

lat doctor parses every descriptors/**/*.yaml in the application and its plugins, so a typo is named without a build.

Read a descriptor at run time

resource! embeds a file. For YAML you loaded yourself, call the function behind it:

let resource = laterite_admin::descriptor::from_yaml(&yaml, "posts.yaml")?;