Views Overview
Views define how data is displayed and edited in Fullfinity’s UI.
Which form does a model open in? (priority)
Section titled “Which form does a model open in? (priority)”Opening a record by model — a method returning get_action(model="Board", record_id=self.id)
— resolves the model’s own Form view: lowest priority wins, ties broken by id. The same
resolution answers “Create & Edit” on a relation field: with no form named, the dialog opens
the related model’s default form, so the number you give a secondary form decides what a picker
on someone else’s screen pops up.
Most models ship one form and never think about this. A model with two must say which is its face:
- data_type: UiView identifier: board_quick_create_form_view priority: 10 # not the model's default — the short create form type: Form model: BoardA quick-create form, a kind-specific variant (an address is a kind of contact, a kit is a kind of BOM), a delivery-log view of a message — none of these is what the model should open as. Give them a higher number and leave the real one at the default.
A tie fails the build. check --only views refuses two forms sharing the lowest priority
for one model, because the winner would otherwise be whatever the database returned first —
and the symptom is a record rendering in the wrong form with most of its fields absent, with
nothing raised anywhere.
Priority does not affect a form named explicitly. ctx: {form_identifier: …}, an action’s
views: list, and a relation widget’s view: all name the view they want and are unaffected
— which is how the non-default forms are still reached where they are genuinely wanted.
View Types
Section titled “View Types”The type field is a Selection whose choices are the registered view types. The
engine ships all of these in core’s UiView.type Selection:
| Type | Description | Use Case |
|---|---|---|
| Form | Detail/edit view | Single record editing |
| List | Table view | Browsing multiple records |
| Kanban | Card-based view | Visual workflows |
| Calendar | Timeline view | Date-based data |
| TimeGrid | Editable rows × time-period entry grid | Timesheets, budgets, forecasts (TimeGrid Views). Enterprise. |
| Map | Records plotted on a map | Location/address-based data |
| Gantt | Timeline with dependencies | Scheduling, project tasks (Gantt Views). Enterprise. |
| Pivot | Cross-tab aggregation table | Reporting/analysis (Pivot & Chart) |
| Chart | Aggregated chart of records | Reporting/analysis (Pivot & Chart) |
| TimelineHeatmap | Rows × time-bucket matrix | Load boards, density grids (TimelineHeatmap). Enterprise. |
| FieldDeck | Offline operator worksheet | Field/floor “my queue → do the work → sign off” (FieldDeck Views). Enterprise. |
| Dashboard | Configurable home screen | KPI cards, charts, lists, goals |
| Search | Filters and groupings | Query configuration (Other Views) |
| Wizard | Transient-model form | Multi-step input before an action (Other Views) |
| Designer | Visual report/layout designer | Document design |
| OrgChart | Hierarchy / org tree | Reporting lines, tree structures |
| PortalList | Customer-portal record list | Portal record browsing |
| PortalDetail | Customer-portal record detail | Portal record view |
Tree is also an accepted value (a legacy alias for List); use List. Types marked
Enterprise only render with an active license — see Enterprise view types.
All view types live in core
Section titled “All view types live in core”Every view type is a value of core’s UiView.type Selection — the single source of
truth. Modules never register their own view types: a view type is a rendering contract
owned by the frontend, so there is no per-module extension of the type choices (no
_selection_add onto UiView.type). A bespoke, one-off screen for a module is authored
as a client action (client_action: on a WindowAction), not as a new UiView type.
Enterprise view types
Section titled “Enterprise view types”Some view types are licensed (Enterprise) features. The gate keys on the view type alone (a compiled, tamper-resistant concept) checked against the license state: without an active license the client shows the upgrade prompt instead of the view — renaming the type to dodge the gate just leaves no renderer, so nothing renders. You can author a view of these types in any module; whether it renders depends on the viewer’s license.
These are authorable primitives — real UiView.type values for which you write
standard arch, exactly like Form or List (each has its own reference page):
| Type | Reference | Used for |
|---|---|---|
| Gantt | page | scheduling / project tasks |
| TimeGrid | page | timesheets, budgets, forecasts |
| TimelineHeatmap | page | load boards / density grids |
| FieldDeck | page | field service, maintenance workbench |
| DocumentBrowser | — | documents (DMS) browser |
The Gantt widget (an embeddable Gantt inside a Form) is Enterprise on the same basis.
Enterprise app screens are client actions, not view types
Section titled “Enterprise app screens are client actions, not view types”Several licensed apps ship a bespoke full-screen experience bound to that app’s model
and flow. These are not UiView type values you can author — you cannot set
type: FinancialReport on a UiView. Each is a client action: a client_action:
value on a WindowAction that names the compiled screen the frontend renders. They are
listed here only so you recognise the name; there is no arch to write for them.
| Client action | App |
|---|---|
| FinancialReport, CustomerStatements, BankReconciliation, PaymentMatching | accounting |
| PlanningGrid | planning / MPS |
| ShiftScheduler | employee scheduling |
| EcoDiff | engineering change (PLM) |
Operator kiosks are routes, not client actions
Section titled “Operator kiosks are routes, not client actions”A till, a shop-floor tablet or a warehouse handset is not a view of the back office, and
it does not render inside the shell at all. Those actions set type: Route and name the
address they open — see Window Actions
for why, and for the route rules.
A view normally sources its data by querying the model’s records. To render
computed / non-record data instead, declare a
data_method in the arch.
Basic Structure
Section titled “Basic Structure”All views share this structure:
- data_type: UiView name: contact_form_view identifier: contact_form_view type: Form model: Contact arch: [...]Fields
Section titled “Fields”| Field | Type | Description |
|---|---|---|
data_type | string | Always "UiView" |
name | string | View name (usually same as identifier) |
identifier | string | Unique identifier |
type | string | View type (Form, List, Kanban, Calendar, Gantt, Pivot, Chart, Search, Wizard, …) |
model | string | Model this view displays |
arch | array/object | View architecture |
inherited_view | string | Parent view identifier (for inheritance) |
Architecture Elements
Section titled “Architecture Elements”The arch contains the view structure:
Layout Elements
Section titled “Layout Elements”| Element | Description |
|---|---|
row | Horizontal container (12-column grid) |
column | Vertical container with span (1-12) |
tab | Tab container with label |
Field Elements
Section titled “Field Elements”| Element | Description |
|---|---|
field | Data field with widget |
statusbar | Status indicator |
actionButton | Button triggering model method |
linkButton | Statistic button with link |
Special Elements
Section titled “Special Elements”| Element | Description |
|---|---|
text | Static text |
divider | Visual separator |
activities | Activity timeline |
type is a closed vocabulary
Section titled “type is a closed vocabulary”A renderer draws the element types it knows and ignores every other one. There is no
fallback element and no console warning, so a type outside the list below would render
as nothing at all — an explanatory note, a rule, or a whole row of buttons simply absent
from the page, discovered only when someone looks at it.
The set is therefore closed and checked when the view is saved (and by
fullfinity-server check --only views). An unknown type is rejected with the nearest real
ones named:
UiView my_form_view: unknown node type 'actions'. A renderer ignores a type it does notknow, so this element would simply not appear. Did you mean: actionButton, accordion?Matching is case-insensitive, because the renderers lowercase the value before
dispatching — actionButton and ActionButton are the same element.
The complete vocabulary:
| Where | Types |
|---|---|
| Layout, any view | row column group paper fieldset accordion divider card |
| Static content | text title alert |
| Form / Wizard | field statusbar actionButton linkButton button inlineButton tabs tab pageTabs propertiesPanel footer stepper |
| Settings form | settingsPanel settingsRow settingsDivider settingsLink pageBuilderPanel |
| List / Kanban | sum activities |
| Search | filter browsepanel |
| Dashboard / FieldDeck | widget fielddeck step timer meter list link |
| Calendar / Map | PopOver QuickCreate |
| Portal (server-rendered) | messages totals signature |
Two spellings that look plausible and are not elements: a button is actionButton,
never action or actions; static text is text with properties.text, never
staticText; and a rule is divider, never separator.
Properties belong to their element
Section titled “Properties belong to their element”properties are checked against the element they are written on, not just against the
global list of known property names. A property that is real but meaningless here — a
method on a row, a nolabel on a text, a widget on a settingsRow — is dropped
by the renderer without a word, so it is reported instead:
UiView my_form_view text: 'nolabel' is not a prop of a 'text' node(it is valid elsewhere, so the renderer drops it here without an error)Two elements keep the full property surface, because their properties depend on something
the element alone does not determine: a field, whose properties follow its widget,
and a dashboard widget, whose properties follow its chart type.
Element Structure
Section titled “Element Structure”Each element has:
- type: field name: email properties: widget: TextInput label: Email Address placeholder: Enter email visible: Q(type__eq='company') groups: [admin]Common Properties
Section titled “Common Properties”| Property | Description |
|---|---|
type | Element type (structural, at top level) |
name | Field name (structural, at top level) |
span | Column width 1-12 (structural, at top level) |
properties | All field configuration (widget, visibility, behavior) |
Element Properties
Section titled “Element Properties”All behavioral and display properties go inside properties:
- properties: widget: TextInput visible: Q(status__eq='draft') readonly: Q(status__neq='draft') required: true groups: [sales_manager]| Property | Type | Description |
|---|---|---|
widget | String | Widget type to render |
visible | Boolean/String | Show/hide element. Can be false or Q-expression. Default: true |
readonly | Boolean/String | Make field(s) read-only. Can be true or Q-expression. |
required | Boolean/String | Make field required. Can be true or Q-expression. |
groups | Array | Required group identifiers for access control. |
Note: Structural attributes (type, name, span, content, title, anchor) stay at the
top level. All behavior and display config goes in properties — and this is enforced: a
behavioral key placed at the node level is read by nothing, so it is rejected when the view is
saved and by check --only views rather than shipping as a rule that never fires. See
Element Properties.
Window Actions
Section titled “Window Actions”Actions connect views to navigation:
- data_type: WindowAction identifier: contacts_action name: Contacts model: Contact modes: Kanban,List,Form default_view: contact_kanban_view views: - - R - - contact_kanban_view - contact_list_view - contact_form_view search_view: contact_search_viewTwo things about that block are checked by check --only views (and the pre-commit hook),
because neither fails loudly on its own:
- The explicit
views:command list is what populates the action’s views. A baredefault_view/action_ctx.form_identifierdoes not — an action without aviews:list raises “Unable to locate view(s)” the moment a menu opens it. - Every view the action names must declare the same
model:. A view pointed at another model still composes, and still renders against the records the action loaded — so a wrongmodel:shows up only as fields silently missing from the screen, never as an error.
Menus create navigation structure:
- data_type: UiMenu identifier: contacts_menu name: Contacts parent: sales_menu action: contacts_action sequence: 10View Resolution
Section titled “View Resolution”When loading a view:
- Fetch base view by identifier
- Find all child views (inherited_view)
- Apply inheritance operations
- Return combined view
Dynamic Views
Section titled “Dynamic Views”If no view is defined, Fullfinity auto-generates one from the model registry. This allows navigation to any model without requiring a WindowAction or UiView definition.
URL Pattern
Section titled “URL Pattern”/app/:rootMenu/m/:model # List view/app/:rootMenu/m/:model/:recordId # Form view for specific record/app/:rootMenu/m/:model/new # Form view for new recordExamples
Section titled “Examples”/app/core_menu/m/Currency # Currency list view/app/core_menu/m/Currency/5 # Currency form for record 5/app/core_menu/m/Currency/new # New Currency formGenerated Views
Section titled “Generated Views”| View | Fields Included | Layout |
|---|---|---|
| List | Char, Selection, ManyToOne, OneToOne, Integer, Float, Monetary | Simple table |
| Form | All fields except JSON, File, Binary, OneToMany | 2-column grid with status bar |
| Search | Active filter + group-by for Selection/ManyToOne | Basic filters |
Priority
Section titled “Priority”- If a UiView exists in the database for the model, it is used (with inheritance)
- If no UiView exists, a dynamic view is generated from the model registry
Use Cases
Section titled “Use Cases”- Quick prototyping without creating view definitions
- Admin/debug access to any model
- Action methods returning
get_action(model="ModelName")for navigation
From Action Methods
Section titled “From Action Methods”async def action_view_logs(self): """Open audit logs without a WindowAction.""" return await get_action(model="AuditLog")
async def action_view_record(self): """Open a specific record dynamically.""" return await get_action(model="SomeModel", record_id=self.related_id)File Organization
Section titled “File Organization”Views are stored in modules/<module>/views/:
views/├── actions.yaml # Window actions├── menus.yaml # Menu items├── contact_views.yaml # Contact views└── product_views.yaml # Product viewsNext Steps
Section titled “Next Steps”- Form Views - Detail/edit views
- List Views - Table views
- Kanban Views - Card-based views
- Pivot & Chart Views - Reporting/analysis views
- Other View Types - Calendar, Search, and Wizard views
- Configuration Forms - The settings DSL
- Dashboard Views - Configurable home screen with widgets
- Stats Banner - Rich visual stats for list/kanban views
- Widgets - Available widgets
- View Inheritance - Extending views