List Views
List views display records in a table format.
Basic List
Section titled “Basic List”- data_type: UiView identifier: contact_list_view type: List model: Contact arch: - content: - type: field name: name properties: {widget: Text} - type: field name: email properties: {widget: Text} - type: field name: phone properties: {widget: Text} - type: field name: active properties: {widget: Badge}Column Configuration
Section titled “Column Configuration”Basic Column
Section titled “Basic Column”- type: field name: name properties: widget: TextColumn with Options
Section titled “Column with Options”- type: field name: status properties: widget: Badge colors: Draft: gray Active: green Archived: redColumn Properties
Section titled “Column Properties”List columns support visibility, readonly, required, and group-based access control. These work with both boolean values and Q-expressions for conditional behavior.
| Property | Type | Description |
|---|---|---|
visible | Boolean/String | Hide column or use Q-expression. Default: true |
readonly | Boolean/String | Make cells read-only or use Q-expression |
not_applicable | Boolean/String | Per-row: this field is meaningless for this row (not merely empty or locked). Renders a dimmed — on a shaded cell, and never an input; when it matches every row the column is dropped entirely. See Not applicable |
na_reason | String | Tooltip shown on a not_applicable cell. Default: “Not applicable” |
required | Boolean/String | Make cells required or use Q-expression |
groups | Array | User must be in one of these groups to see the column |
mobile | Boolean | Include field in mobile card view. Default: true |
default_hidden | Boolean | Column exists but starts hidden; the user can reveal it from the columns menu. Default: false |
locked | Boolean | Column can’t be hidden — always shown and kept out of the columns menu. Default: false |
color_field | String | Name of a sibling field holding a color (a Mantine color name like orange, or a hex value). The cell’s text is tinted that color per row — leave the field empty to show no tint. Works on text/number/monetary cells; a Badge keeps using its own colors map. |
nolabel | Boolean | Blanks the column header. Without it, an empty label falls back to the field’s description — set nolabel: true for an icon-only / self-explanatory column (e.g. a SummaryIndicator). Default: false |
Conditional cell color
Section titled “Conditional cell color”To draw the eye to specific rows without adding a flag column, point a column at a sibling field that computes a color. The field is fetched automatically even though it isn’t shown as its own column:
- type: field name: days_overdue properties: widget: NumberInput color_field: overdue_colorHere overdue_color is a (usually non-stored, computed) field that returns e.g. "orange" when the row needs attention and empty otherwise — so only those rows’ values are tinted.
Hide Column
Section titled “Hide Column”- type: field name: internal_code properties: widget: Text visible: falsevisible on a column is a question about the column, never about one row — a column present
on one row and absent on the next leaves a ragged grid whose click targets move as you scan down
it. So it takes four forms, and the last one is the one to know:
| Form | Answered |
|---|---|
visible: false | at the column, once |
visible: Q(_parent.state='Draft') | once, against the embedding record |
visible_setting: Q(use_packaging=True) | once, against configuration |
visible: Q(display_type='Part') (row-rooted) | against every row — the column is shown if any row wants it, and dropped when none does |
The last one only drops the column: a row that fails the rule still renders its value, because
that is a cell question and not_applicable is what answers it.
The two compose — a column is dropped if either rule says no row needs it.
Group-Restricted Column
Section titled “Group-Restricted Column”- type: field name: cost_price properties: widget: Text groups: [inventory.manager, admin]Mobile-Only Fields
Section titled “Mobile-Only Fields”On tablet/mobile devices, List views are rendered as card layouts (Kanban-style). Use mobile: false to exclude columns that are useful in desktop tables but not needed on compact mobile cards:
- type: field name: internal_notes properties: widget: Text mobile: falseThis field will appear in the desktop table view but be hidden in the mobile card layout.
Conditional Readonly (per-row)
Section titled “Conditional Readonly (per-row)”For inline editable lists, readonly can be evaluated against each row’s data:
- type: field name: quantity properties: widget: NumberInput readonly: Q(status__neq='Draft')Not applicable (per-row)
Section titled “Not applicable (per-row)”A column is shown or hidden for the whole list — there is no per-row column visibility, and there shouldn’t be: a column present on one row and absent on the next leaves a ragged grid whose click targets move as you scan down it. So when a column is meaningful for some rows and meaningless for others, the column keeps its place and the cell says which it is:
- type: field name: lot_name properties: widget: TextInput not_applicable: Q(tracking='None') na_reason: Not lot-trackedThe rule is evaluated per row against the row’s values (and _parent.* for an embedded list),
exactly like readonly. A cell it matches renders a dimmed em-dash on a shaded ground with
na_reason as its tooltip, and is never editable — the value would be meaningless for that row,
so not_applicable implies readonly whether or not you also declared it.
The two signals do different jobs, which is why the cell gets both: the dash carries the meaning (“nothing belongs here”, the same mark a read-only empty form field shows), and the shading carries the scanning — the eye groups the inapplicable cells and skips them, so a mixed grid stops reading as half-filled-in. Only the tint changes; borders and row height don’t, so column alignment is untouched.
Reach for it wherever a lines grid mixes kinds of row. Use it instead of leaving a blank cell,
which reads as an entry the user forgot to make, and instead of readonly alone, which says
“you may not edit this” rather than “there is nothing to edit.”
A column no row needs is dropped for you
Section titled “A column no row needs is dropped for you”not_applicable describes the mixed list, and that is the only thing you author. When the rule
comes back true for every row of an embedded lines grid, the framework drops the column
outright — header included — and hands its width back to the columns that carry values. A grid
whose Packaging and Packs columns held a sixth of the table to say “not applicable” forty times
now shows neither, and the Product cell gets the room. (A row-rooted visible: is rolled up the
same way — see below.)
The two cases are different questions, which is why one draws a dash and the other disappears:
| Applies to… | What you see | Why |
|---|---|---|
| some rows | column stays, inapplicable cells shaded with na_reason | the column exists for the other rows; dropping it per row would leave a ragged grid |
| no row | no column at all | there is no ragged grid to fear and no information in the column — only width taken from columns that have some |
The roll-up is live: it re-answers as rows are added, edited and removed, before any save, so
a line whose product has just been picked can bring a column back the moment its ui_effect
lands. It is judged over the whole collection, not the rows currently on screen — a rule that
applies to line 90 keeps the column while lines 1–20 are showing, and typing in the grid’s search
box never reshapes the table. An empty grid keeps its columns as authored: “no row needs
this” and “there are no rows yet” are different statements, and a document you are about to type
the first line into should show the shape it is going to have.
Three things to expect:
- The roll-up never second-guesses your rule — it only asks it of every row, so a column
arrives the moment the first row makes it apply. On a sale order, Packaging (
Q(has_packagings =False)) appears as soon as a line’s product offers a pack level, while Packs (Q(packaging__isnull=True)) arrives with the first pack actually chosen. Write the per-row rule for the cell it governs and let the column follow; if you want two columns to travel together, they need to be answering the same question in the first place. - A row-rooted
visible: Q(...)is rolled up the same way, from the other end: the column is dropped when no row wants it.visible: Q(display_type='Part')on a repair order’s Operation and Lot columns drops both from an order made only of section lines;Q(tracking__in=['Lot', 'Serial'])drops Lot from a production whose components are untracked. See Hide Column for the four formsvisibletakes. - A column dropped this way leaves the columns menu too, so the chooser never offers a checkbox that reveals nothing.
On mobile the list renders as cards, where per-row omission is correct and is what you get: a card whose field is not applicable leaves the row out entirely rather than showing a dash.
Sub-line (under:)
Section titled “Sub-line (under:)”A line model routinely carries more fields than one table row can show at a readable width. Past roughly eight columns the grid stops being a grid: every value is ellipsised, combos have no room for their chevron, and the fields a user actually negotiates on (price, discount) are the same width as ones they touch once a year.
under: moves a field out of the columns and renders it as a second line inside another
column’s cell — the way a document prints a line: the product, then its description beneath
in smaller type.
- type: field name: product_variant properties: widget: DataCombo label: Product- type: field name: description properties: widget: TextArea under: product_variant # renders under the Product cell, not as a column hide_if_same_as: product_variantThe value of under is the name of a field that is a column in the same view. That column’s
cell is where the sub-line is drawn, so it has to be there; naming a field the view doesn’t show
is rejected at view load and by ./fullfinity-server check --only views, because the field would
otherwise never appear at all. A sub-line cannot host a sub-line of its own.
A sub-line is a different place to draw a field, not a different way to edit one: it renders
through the same cell machinery as a column, shows the same display value at rest, becomes the
same inline editor when the row is edited, and honours readonly, required, filter,
not_applicable and ctx identically. Clicking it puts the row into edit with that field
focused, exactly as clicking a cell does — no extra click, no navigation.
At rest it is drawn only where it has something to say. A row whose sub-line field is empty renders as a plain single-height row, so a grid of forty lines shows the three that carry a description instead of ruling a mostly-empty column down the page. It does not depend on edit rights, so a locked or read-only record still shows its values. While the row is being edited the field is always drawn — you cannot fill in what isn’t there.
Applicability is per row. This is the sub-line’s real advantage over a column, and it’s why the two rules above finally do what they read like:
not_applicable: Q(...)— a column must keep its place and draw a dash, because the column exists for the other rows. A sub-line is simply not drawn on the rows it doesn’t apply to.visible: Q(...)— as a column a row-rootedQis an any-row question: it decides whether the column exists at all, and every row that stays renders its value (Hide Column above). On a sub-line it is evaluated per row, so the field is drawn on the rows that want it and omitted on the rest — the granularity such a rule reads like.
Nothing reflows when this changes: column widths are fitted from the columns alone, so a sub-line field takes no part in width fitting.
placeholder: names the empty editor. A grid draws no captions, so an empty sub-line under a
value is an unlabelled box — the placeholder is what says what belongs there. Leave it off and
the field’s own label is used, which is usually what you want (“Description”); set it when a
prompt reads better than a noun (placeholder: Add a note for the customer…).
hide_if_same_as: <other field> suppresses the sub-line at rest when it would print exactly
what the named field already prints on the same row — a description seeded from its product
holds the product’s name verbatim, and printing it again directly under that name is text the
reader skips past on every line. Compared on rendered text (a relation by its display name),
and ignored while the row is edited so the text stays there to be re-worded. It applies only to
a sub-line field; on a column it does nothing, and saying so is a view error rather than a
silent no-op.
What belongs on a sub-line: free text that qualifies the value above it (a line description), an override on top of a default, a field empty on most rows. What doesn’t: the values the document is about — quantity, price, the line total — which belong in columns where they line up down the page and can be scanned and summed; anything that doesn’t qualify one particular column (put it in the row’s panel); and child collections, which are not scalar fields (the panel again).
A sub-line does not appear in the column picker and is never personalized away — it is part of its host cell, not a narrow column a user might widen. On mobile it needs no special handling: a card is already a two-dimensional layout, and the field renders in it like any other.
Conditional Required (per-row)
Section titled “Conditional Required (per-row)”- type: field name: shipping_address properties: widget: DataCombo required: Q(requires_shipping__eq=true)Personalized Columns
Section titled “Personalized Columns”Every List lets each user choose which columns to show and how wide they are, from the
columns menu (the ⋮ button at the end of the header row). Those choices are remembered
per user, in the browser — reopening the List restores the user’s column selection and
any widths they dragged. Nothing is stored server-side, so it never affects other users and
needs no configuration. (A List embedded in a form as a widget: List is keyed by its
relation, so the same list view keeps independent choices in each place it’s embedded.)
On a standalone List, the columns menu also lets each user add any other field of the
model that they’re allowed to see — not just the columns the view declares. Added columns are
fetched on demand and remembered like any other choice; a search box helps find a field, and
a field the user can’t access (via groups) is never offered. (Embedded lists inside a form
are limited to the columns the view declares — their rows are live, unsaved form data, so a
column can’t be added mid-edit.)
You control the starting point with two column properties:
default_hidden: true— the column ships hidden. It’s still listed in the columns menu, so a user who wants it can switch it on. Use this for detail columns that are useful but clutter the default table.locked: true— the column can never be hidden. It’s always shown and doesn’t appear in the columns menu. Use this for the identifying column (e.g.name).
- type: field name: email properties: widget: Text default_hidden: true- type: field name: name properties: widget: Text locked: trueField-level security still applies: a column the user isn’t allowed to see (via groups)
is never offered in the menu and can’t be revealed.
Common List Widgets
Section titled “Common List Widgets”| Widget | Description |
|---|---|
Text | Plain text display |
Badge | Colored badge |
Tags | Multiple tags |
Avatar | User avatar |
Image | Image thumbnail; add zoom: true to pop an enlarged preview when the thumbnail is hovered |
Checkbox | Boolean checkbox |
Rating | Star rating |
Default widget per field type
Section titled “Default widget per field type”A column doesn’t have to declare a widget — if you omit it, the list renders a sensible
default based on the field’s type, and it adapts to whether the cell is read-only or being
inline-edited:
| Field type | Read-only display | Inline edit |
|---|---|---|
Char, Text | plain text | text input |
Integer, Float | number | number input |
Monetary | formatted amount | number input |
Boolean | checkbox | checkbox |
Date / Datetime | formatted date | date picker |
Selection | Badge | select dropdown |
ManyToOne / OneToOne | linked name | combo |
ManyToMany | Tags | multi-select |
Declaring a widget always overrides the default. Types with no meaningful flat-column
representation — OneToMany, File, Binary, JSON — have no default and render
empty: to show one of these in a list, give it an explicit widget (e.g. Image for a
file). This keeps the cell from dumping a raw object/array/blob as text.
Editable Lists
Section titled “Editable Lists”Enable inline editing with editable: true at the arch root level:
- data_type: UiView type: List model: OrderLine arch: - editable: true insertPosition: bottom content: - type: field name: product properties: {widget: DataCombo} - type: field name: quantity properties: {widget: NumberInput}| Property | Description |
|---|---|
editable | true to enable inline editing |
insertPosition | Where new rows are added: "bottom" (default) or "top" |
editable is a boolean — the insert position is a separate key. Writing
editable: top does not move anything; it just isn’t true, so the list loses
inline editing altogether.
When the list is reorderable (an explicit reorderable:, or a sequence field
on an editable list), a newly added row is given a sequence past the end — or
before the start, for insertPosition: top — so it keeps the position it was
added at once saved, instead of taking the field’s default and re-sorting into
the middle of the list on the next read.
Section & note rows
Section titled “Section & note rows”An editable list can carry structural rows that group the real rows —
full-width section headings and free-text note rows — like the section
and note lines of a sales order. Turn it on with sectionAndNote on the arch
root, set to the name of the field that stores the row’s text:
- data_type: UiView type: List model: OrderLine arch: - editable: true sectionAndNote: description # the field holding the section/note text content: - type: field name: display_type properties: {visible: false} - type: field name: description properties: {widget: TextInput} - type: field name: quantity properties: {widget: NumberInput}Requirements and behavior:
- The model must have a
display_typefield (aSelection) whose value is"Section"or"Note"for structural rows (any other value — e.g."Question","Product"— is a normal row). Include it as avisible: falsecolumn so the client can read it. - The field named by
sectionAndNoteholds the text shown for a section/note. Section rows render as a full-width heading; note rows as full-width muted text. Normal rows render the columns as usual. - The list footer gains Add a section and Add a note links (alongside Add a line) that create the corresponding structural row.
Restricting to sections or notes
Section titled “Restricting to sections or notes”Some lists want section separators but not free-text notes (or vice-versa). Two boolean arch-root flags gate which structural rows can be added:
| Flag | Effect |
|---|---|
sectionsOnly: true | Offer Add a section, hide Add a note |
notesOnly: true | Offer Add a note, hide Add a section |
Set neither to offer both (the default). They only affect the add affordances — existing rows of either kind still render.
arch: - editable: true sectionAndNote: title sectionsOnly: true # sections only — no note rows content: - type: field name: display_type properties: {visible: false} - type: field name: title properties: {widget: TextInput}Badge Colors
Section titled “Badge Colors”- type: field name: priority properties: widget: Badge size: sm colors: Low: gray Medium: blue High: orange Urgent: redRelated Fields
Section titled “Related Fields”Display related model fields:
- type: field name: customer properties: widget: DataComboThe display name of the related record is shown.
Image Column
Section titled “Image Column”- type: field name: image properties: widget: Image h: 40 w: 40 fit: coverRunning a method from a list
Section titled “Running a method from a list”Three nodes run a model method from a List. They differ only in what the click is aimed at:
| Node | Declared | Renders | Runs on |
|---|---|---|---|
inlineButton | inside content | the row’s ⋮ menu | that one row |
button | inside content | its own button column, on every row | that one row |
actionButton | top level of arch | the view’s toolbar | the ticked rows |
Row actions
Section titled “Row actions”A row action always arrives with exactly one record, so the method it calls is an ordinary instance method — it never has to work out what was selected:
arch:- content: - type: field name: name - type: field name: state - type: inlineButton anchor: quote_send properties: label: Send icon: Send method: action_send auto_refetch: true visible: Q(state='Draft') groups: [sales_manager_group]visible is evaluated per row, so the action is offered only where it applies rather
than failing after the click. Two things to know when writing that rule:
- It reads the row that was fetched, and a list fetches the fields its columns declare.
A rule naming a field no column shows needs that field in the arch as a hidden one —
properties: {visible: false}keeps it out of the table while keeping it in the fetch. - A
buttoncolumn renders on every row regardless; itsvisibleis not a per-row rule. UseinlineButtonwhenever the action doesn’t apply to every row.
Offered but not available (disabled)
Section titled “Offered but not available (disabled)”disabled takes the same true/false/Q grammar and is also evaluated per row — the
action stays listed, greyed, instead of disappearing:
- type: inlineButton anchor: quote_send properties: label: Send method: action_send disabled: Q(email__isnull=True) hint: This customer has no email address yet.Prefer it to visible when disappearing would make the rows read inconsistently — every row
in a list is scanned against its neighbours, so an action that comes and going down the
column is harder to trust than one that is visibly unavailable on some rows. In a row menu
the hint renders as a second line under the label; on a button column it becomes the
cell’s tooltip. That column is in fact the one place disabled is the only per-row rule
available, since the column itself is structural.
Full guidance on choosing between the two: Hiding vs disabling.
Selection actions
Section titled “Selection actions”A toolbar actionButton runs against the rows the user ticked, so its method receives the
whole selection (for record in self:):
arch:- type: actionButton anchor: mark_paid properties: label: Mark Paid icon: Check method: action_mark_paid- content: - type: field name: nameThe button stays hidden until at least one row is ticked — you don’t declare that. A
method written async def action_mark_paid(self) can only run against records, and the
framework reads that off the signature when it builds the view. A cls-first method needs
no selection, so its button is always offered.
Override it with instance: when the signature can’t tell the truth — a cls-first method
that reads context["active_ids"] is a selection action (instance: true), and false
forces a self-first one to stay offered.
Reach for a toolbar action only when acting on several records at once is the point.
When one record is enough, a row action is the better shape: it can’t be clicked with
nothing selected, can’t be aimed at records the method would reject, and needs no guard for
either case. Button properties (confirm, auto_refetch, icon, color, groups) are
the same ones documented under Action Buttons.
Complete Example
Section titled “Complete Example”- data_type: UiView identifier: product_list_view type: List model: Product arch: - content: - type: field name: image properties: widget: Image h: 40 w: 40 fit: contain - type: field name: name properties: widget: Text fw: '600' - type: field name: sku properties: widget: Text c: dimmed - type: field name: category properties: widget: Text - type: field name: price properties: widget: Text - type: field name: stock_quantity properties: widget: Text - type: field name: active properties: widget: Badge colors: {'true': green, 'false': gray}Aggregates (footer totals & group subtotals)
Section titled “Aggregates (footer totals & group subtotals)”A List can total a column two ways:
-
Footer total — set a column’s
aggregateproperty. A totals row appears at the bottom, summing (or averaging/min/max) that column across the whole filtered dataset, not just the loaded page.- type: fieldname: total_amountproperties: {widget: Monetary, aggregate: sum} -
Group subtotal — when the list is grouped, declare
group_aggregateson the arch root and each group header shows its subtotal.- type: Listgroup_aggregates:- {field: total_amount, type: sum}content: [...]
Aggregate types: sum, average, min, max, count.
Money aggregates are currency-correct
Section titled “Money aggregates are currency-correct”For a Monetary column the total is resolved to a single currency for you — raw
amounts in different currencies are never added together:
- One currency in the set → total shown in that currency (exact sum, no conversion).
- Multiple currencies → each amount is converted to the active company’s currency and the total is shown in it, using the recorded exchange rates. Individual rows keep their own currency.
This holds for both footer totals and group subtotals, and matches Kanban
header aggregates. Embedded (in-form) list footers total live in
the browser as you edit lines: they show the currency when all lines share one, and
show — if lines somehow span multiple currencies (there are no exchange rates in the
form to convert with).
Drag-and-Drop Row Reordering
Section titled “Drag-and-Drop Row Reordering”List views can enable drag-and-drop row reordering via the reorderable property. When enabled, a drag handle column appears and users can reorder rows by dragging.
Enabling Reordering
Section titled “Enabling Reordering”- data_type: UiView type: List model: CrmStage arch: - reorderable: sequence content: - type: field name: sequence - type: field name: name - type: field name: probabilityReorderable Property Values
Section titled “Reorderable Property Values”| Value | Description |
|---|---|
false or omitted | Drag-and-drop disabled (default) |
"sequence" | Enable reordering, update the sequence field |
"priority" | Enable reordering, update the priority field |
| Any field name | Enable reordering, update that Integer field |
Automatic Disabling
Section titled “Automatic Disabling”Drag-and-drop reordering is automatically disabled when:
- List is grouped (group-by is active)
- Column sorting is active (user clicked a column header to sort)
- Inline editing mode is active
- Action context has
editable: false
Embedded List (in Forms)
Section titled “Embedded List (in Forms)”Lists inside form views for related records:
- type: field name: order_lines properties: widget: List create: true delete: true editable: true view: order_line_inline_view ctx: form_identifier: order_line_form_viewEmbedded List Options
Section titled “Embedded List Options”| Option | Description |
|---|---|
create | Allow adding new records |
delete | Allow deleting records — true/false, or a Q(...) rule evaluated per row (see below) |
editable | true for inline editing, "modal" for modal editing |
insertPosition | Where new items are added: "bottom" (default) or "top" |
view | List view to use |
ctx | Additional context (form_identifier) |
Deleting only some rows (delete: Q(...))
Section titled “Deleting only some rows (delete: Q(...))”delete accepts a Q-expression as well as a boolean. The rule is evaluated per row,
against that row’s own values (and _parent.<field> for the enclosing form’s), exactly like
a cell’s readonly/visible/required — so a grid can let the user remove the rows they
added while keeping the ones the record came with:
- type: field name: lines properties: widget: List editable: true delete: Q(is_manual=True) # only rows flagged as user-added show the delete icon view: my_line_list_viewTwo things to know:
- A rule can only take deletion away. It is ANDed with the existing gate — model
permissions, a locked (controlled-edit) parent, a read-only embed — so it never grants a
delete those withhold.
delete: falsestill removes the affordance outright. - Name a field the row actually carries. The rule is evaluated client-side against the
row, so every field it references must be a column of the embedded list view (a
visible: falsecolumn is fine — the value is still fetched). Fields are validated against the child model when the view loads.
The rule applies wherever a row can be removed: the grid’s delete icon, and the record
modal a row opens in editable: "modal" / on mobile.
What removing a row does to the record
Section titled “What removing a row does to the record”Removal follows the relation, not the column definition:
| Field type | Removing a row | Why |
|---|---|---|
OneToMany | Deletes the child record | The children are the parent’s — a line, an address, a config sub-row has no life of its own |
ManyToMany | Detaches the link only; the record survives | The record belongs to the target model; only the link is yours |
So a delete: true embed of a OneToMany is genuinely destructive: the row leaves the grid
and the record is gone when the form is saved. If the child is referenced elsewhere by a
RESTRICT foreign key, the save reports that error rather than removing it.
Removal is staged, not immediate — nothing happens until the parent form is saved, and Discard abandons it. A row that was never saved (one just added to the grid) is simply dropped; there is no record to destroy.
If a collection’s members genuinely outlive the parent, it is a ManyToMany, not a
OneToMany whose removal should spare them — model the relation for what it is rather
than reaching for a per-view opt-out. To show children without offering removal at all,
use delete: false.
Row panel (detail: true)
Section titled “Row panel (detail: true)”Some fields belong to the line but not to the scan: a per-line override, a child collection,
a note nobody reads while checking totals. detail: true keeps them off the columns and puts
them in the row’s panel — opened from a bare icon in the row’s actions column.
- type: field name: analytic_distribution properties: detail: true widget: List # a child collection is fine here view: analytic_distribution_list_view label: Analytic Distribution editable: trueThe panel is an extension of the grid, not a form over the record. Its fields render through the same cell the columns do, bound to the same row, which is the whole point:
- They are declared in this view, so they are fetched with the row and written by the document’s save. There is no second save, and nothing to keep in sync.
- They obey the same rules as the columns beside them —
readonly,required,not_applicable, and the controlled-edit freeze a parent record puts on its children. A field the grid has locked is locked in the panel, because it is the same cell. - A row that has never been saved works — the panel edits the row in the form, not a record fetched by id.
Opening the panel puts the row into edit, exactly as clicking a cell does. The icon tints
when the row already carries something in the panel, so a grid shows which lines have one
without opening any of them. Per-row rules apply as they do to a sub-line: a field that is
not_applicable to this row, or whose visible: Q(...) is false for it, simply isn’t drawn.
The panel is a flat list of fields — label above value, wrapping at a readable width. It is the row’s overflow, not a second layout to design; a field carrying its own grid takes the full width. If a panel is growing a layout, that is usually a sign the record wants its own form rather than more panel.
What belongs in the panel: child collections (the only place they can go — a collection is
not a scalar and cannot be a column or a sub-line), per-line overrides, and
fields a user touches once in twenty lines. What doesn’t: anything worth scanning down the
page, which is what columns are for; and text that qualifies one particular column, which is
what under: is for.
Embedded List Reordering
Section titled “Embedded List Reordering”Embedded lists (OneToMany fields in forms) also support drag-and-drop reordering. Configure reorderable on the referenced list view:
- data_type: UiView identifier: order_line_inline_view type: List model: OrderLine arch: - reorderable: sequence content: - type: field name: sequence - type: field name: product - type: field name: quantityThen reference this view in your form:
- type: field name: order_lines properties: widget: List editable: true view: order_line_inline_viewKey differences from main List views:
- Updates form state directly (no API call on drag)
- Sequence field is updated for all rows in form state
- Disabled while editing a row inline
Responsive Behavior (Mobile/Tablet)
Section titled “Responsive Behavior (Mobile/Tablet)”On tablet and mobile devices (≤1024px), List views automatically render as a card layout (similar to Kanban) for better touch interaction and readability.
How It Works
Section titled “How It Works”- Desktop: Displays as a traditional data table with columns
- Tablet/Mobile: Displays as stacked cards, with each row becoming a card
Card Layout Conversion
Section titled “Card Layout Conversion”When no explicit card layout is provided, the List columns are converted automatically:
- The first visible column becomes the card title (bold, larger, no label)
- Every remaining column becomes a labelled row — the column’s label on the left, its value on the right — so each value is named instead of stacked anonymously
- Input widgets are converted to display widgets (e.g.,
TextInput→Text);Badge,Monetary,Tags/MultiCombo,Avatar,Ratingare preserved - Fields with
visible: false,mobile: false, ordefault_hidden: trueare excluded
This automatic layout is always readable, but generic. For a line/table field where the mobile card deserves a designed layout (a receipt-style line, grouped metrics), author a card: block instead — see below.
Controlling Mobile Display
Section titled “Controlling Mobile Display”Use the mobile property to control which fields appear in the mobile card:
- arch: - content: - type: field name: name properties: {widget: Text} - type: field name: customer properties: {widget: Text} - type: field name: total properties: {widget: Text} - type: field name: internal_ref properties: {widget: Text, mobile: false} - type: field name: notes properties: {widget: Text, mobile: false}In this example, name, customer, and total appear on mobile cards, while internal_ref and notes are desktop-only.
Custom Card Layout (card:)
Section titled “Custom Card Layout (card:)”For full control over the mobile card of a List — most useful for an embedded lines field (a OneToMany/ManyToMany rendered as widget: List inside a Form) — add a card: block on the List arch root. It uses the same arch grammar as a Kanban card (row / column with span, horizontal group, static text, and field elements with any display widget), so it has the full expressiveness of a Kanban card while living right next to the columns it complements. Desktop keeps rendering the columns; the card: block is used only on the mobile/tablet card.
Elements support visible: Q(...) (evaluated per row), so one card can branch on the row’s data — e.g. a product line versus a section/note line.
- data_type: UiView identifier: order_line_list_view type: List model: OrderLine arch: - editable: true card: - type: row content: - type: column span: 12 content: # Product name (left) and line total (right), on one row - type: group properties: justify: space-between wrap: false content: - type: field name: product properties: {widget: Text, size: sm, fw: '600'} - type: field name: subtotal properties: {widget: Monetary, size: sm, fw: '700'} # Compact "qty uom × unit price" metrics + tax badge - type: group properties: {gap: 6} content: - type: field name: quantity properties: {widget: Text, size: xs, c: dimmed} - type: field name: uom properties: {widget: Text, size: xs, c: dimmed} - type: text properties: {text: '×', size: xs, c: dimmed} - type: field name: unit_price properties: {widget: Monetary, size: xs, c: dimmed} - type: field name: taxes properties: {widget: MultiCombo, size: xs} content: - type: field name: product properties: {widget: DataCombo, label: Product} # ...remaining columns...The mobile card layout is resolved in this order: an explicit card: block wins; otherwise a Kanban view of the same model (if one is preloaded) is used; otherwise the columns are auto-converted to the labelled layout above.
Custom Mobile Layout (top-level lists)
Section titled “Custom Mobile Layout (top-level lists)”For a top-level List view (an app screen, not an embedded lines field), you can alternatively define a separate Kanban view. The system checks for a Kanban view before converting the List view:
- data_type: UiView identifier: order_kanban_view type: Kanban model: Order arch: - type: row content: - type: column span: 12 content: - type: field name: number properties: {widget: Text, size: sm, fw: '600'} - type: field name: customer properties: {widget: Text, size: xs, c: dimmed} - type: field name: total properties: {widget: Text, size: xs}Next Steps
Section titled “Next Steps”- Stats Banner - Rich visual stats above list views
- Kanban Views - Card-based views
- Widgets - All available widgets