Skip to content

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: Board

A 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.

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:

TypeDescriptionUse Case
FormDetail/edit viewSingle record editing
ListTable viewBrowsing multiple records
KanbanCard-based viewVisual workflows
CalendarTimeline viewDate-based data
TimeGridEditable rows × time-period entry gridTimesheets, budgets, forecasts (TimeGrid Views). Enterprise.
MapRecords plotted on a mapLocation/address-based data
GanttTimeline with dependenciesScheduling, project tasks (Gantt Views). Enterprise.
PivotCross-tab aggregation tableReporting/analysis (Pivot & Chart)
ChartAggregated chart of recordsReporting/analysis (Pivot & Chart)
TimelineHeatmapRows × time-bucket matrixLoad boards, density grids (TimelineHeatmap). Enterprise.
FieldDeckOffline operator worksheetField/floor “my queue → do the work → sign off” (FieldDeck Views). Enterprise.
DashboardConfigurable home screenKPI cards, charts, lists, goals
SearchFilters and groupingsQuery configuration (Other Views)
WizardTransient-model formMulti-step input before an action (Other Views)
DesignerVisual report/layout designerDocument design
OrgChartHierarchy / org treeReporting lines, tree structures
PortalListCustomer-portal record listPortal record browsing
PortalDetailCustomer-portal record detailPortal 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.

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.

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):

TypeReferenceUsed for
Ganttpagescheduling / project tasks
TimeGridpagetimesheets, budgets, forecasts
TimelineHeatmappageload boards / density grids
FieldDeckpagefield service, maintenance workbench
DocumentBrowserdocuments (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 actionApp
FinancialReport, CustomerStatements, BankReconciliation, PaymentMatchingaccounting
Shopfloormanufacturing shop floor
Scannerbarcode picking
PlanningGridplanning / MPS
ShiftScheduleremployee scheduling
EcoDiffengineering change (PLM)

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.

All views share this structure:

- data_type: UiView
name: contact_form_view
identifier: contact_form_view
type: Form
model: Contact
arch: [...]
FieldTypeDescription
data_typestringAlways "UiView"
namestringView name (usually same as identifier)
identifierstringUnique identifier
typestringView type (Form, List, Kanban, Calendar, Gantt, Pivot, Chart, Search, Wizard, …)
modelstringModel this view displays
archarray/objectView architecture
inherited_viewstringParent view identifier (for inheritance)

The arch contains the view structure:

ElementDescription
rowHorizontal container (12-column grid)
columnVertical container with span (1-12)
tabTab container with label
ElementDescription
fieldData field with widget
statusbarStatus indicator
actionButtonButton triggering model method
linkButtonStatistic button with link
ElementDescription
textStatic text
dividerVisual separator
activitiesActivity timeline

Each element has:

- type: field
name: email
properties:
widget: TextInput
label: Email Address
placeholder: Enter email
visible: Q(type__eq='company')
groups: [admin]
PropertyDescription
typeElement type (structural, at top level)
nameField name (structural, at top level)
spanColumn width 1-12 (structural, at top level)
propertiesAll field configuration (widget, visibility, behavior)

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]
PropertyTypeDescription
widgetStringWidget type to render
visibleBoolean/StringShow/hide element. Can be false or Q-expression. Default: true
readonlyBoolean/StringMake field(s) read-only. Can be true or Q-expression.
requiredBoolean/StringMake field required. Can be true or Q-expression.
groupsArrayRequired 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.

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_view

Two 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 bare default_view / action_ctx.form_identifier does not — an action without a views: 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 wrong model: 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: 10

When loading a view:

  1. Fetch base view by identifier
  2. Find all child views (inherited_view)
  3. Apply inheritance operations
  4. Return combined view

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.

/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 record
/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 form
ViewFields IncludedLayout
ListChar, Selection, ManyToOne, OneToOne, Integer, Float, MonetarySimple table
FormAll fields except JSON, File, Binary, OneToMany2-column grid with status bar
SearchActive filter + group-by for Selection/ManyToOneBasic filters
  1. If a UiView exists in the database for the model, it is used (with inheritance)
  2. If no UiView exists, a dynamic view is generated from the model registry
  • Quick prototyping without creating view definitions
  • Admin/debug access to any model
  • Action methods returning get_action(model="ModelName") for navigation
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)

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 views