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
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 actionApp
FinancialReport, CustomerStatements, BankReconciliation, PaymentMatchingaccounting
PlanningGridplanning / MPS
ShiftScheduleremployee scheduling
EcoDiffengineering 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.

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

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 not
know, 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:

WhereTypes
Layout, any viewrow column group paper fieldset accordion divider card
Static contenttext title alert
Form / Wizardfield statusbar actionButton linkButton button inlineButton tabs tab pageTabs propertiesPanel footer stepper
Settings formsettingsPanel settingsRow settingsDivider settingsLink pageBuilderPanel
List / Kanbansum activities
Searchfilter browsepanel
Dashboard / FieldDeckwidget fielddeck step timer meter list link
Calendar / MapPopOver 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 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.

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