Views
A *.view.xml file describes one object — a grid, an editable form, or a read-only view.
Whether it behaves as one, the other, or all three is inferred from which SQL blocks it declares;
there is no separate “table view” vs “form view” type. Internally this is the unified ObjectView.
The root is <Table> or <Form> (both parse to one ObjectView; the filename is *.view.xml
regardless).
<Table Name="Customer" Slug="customers" Label="Customers" Icon="fa fa-users" FormStyle="Modal"> <Sql PrimaryKey="Id" SortBy="Name" SortDirection="ASC" PageSize="20"> … </Sql> …</Table>Root attributes
Section titled “Root attributes”| Attribute | Notes |
|---|---|
Name (required) |
the view name (used in /object/{name} etc.) |
Slug (required) |
URL-friendly identifier for the view. Always kebab-case (lowercase, words separated by hyphens), e.g. customers, sale-items, user-roles. The router and ViewResolver match a route by either Name or Slug, so the slug is the readable form used in /object/{slug} (grid), /object/{slug}?id={pk} (view), and /object/{slug}/edit|/create (forms). |
Label, Icon |
display label and icon class |
FormStyle |
Inline | Modal | DirectedForm |
TableDisplay |
Default | Block | Flex |
DisableActions |
hides the built-in write controls — Add New, edit, delete, bulk-delete |
ViewAction |
Enabled (default) | Disabled — whether the built-in View button is offered |
DisableFilters, DisableControls, DisableRelativeTime, LoadAsScalar |
feature toggles |
CreateForm, EditForm |
delegate create/edit to another named view |
DataSource |
read this object from a named Genie:ReportingSources datasource instead of the application database — matched case-insensitively; see below |
DisableActions and ViewAction are independent, and neither touches authored
row actions or sub-views:
DisableActions="true"alone → a read-only grid whose records can still be opened.ViewAction="Disabled"alone → records are edited/deleted in place but never opened read-only.- both → a queue whose rows are only acted on through their authored sub-view or row action (this is how the built-in workflow Tasks inbox is configured).
DataSource — reading from a warehouse
Section titled “DataSource — reading from a warehouse”By default an object’s query runs against the application database. DataSource points it at a named
datasource from the host’s Genie:ReportingSources map instead — typically the
ClickHouse warehouse that warehousing replicates into, so a reporting grid
reads analytics-shaped rows without touching the transactional tables.
<Table Name="StockMovementAnalytics" Slug="stock-movement-analytics" Label="Movement Analytics" DataSource="Analytics" DisableActions="true" ViewAction="Disabled"> <Sql PrimaryKey="Id" SortBy="Period" SortDirection="DESC" PageSize="25"> <![CDATA[ SELECT concat(toString(toStartOfMonth(MovedAt)), ':', MovementType) AS Id, toString(toStartOfMonth(MovedAt)) AS Period, MovementType, count() AS Movements, sum(Quantity) AS NetUnits FROM Inventory.wh_Inventory_StockMovements FINAL WHERE IsDeleted = 0 AND CompanyId = {SessionCompanyId:Int64} GROUP BY Period, MovementType ]]> </Sql> <Columns> … </Columns></Table>Three things follow from the attribute:
1. The object is read-only. <InsertSql>, <EditSql>, <DeleteSql>, <SubmitSql> and
<ImportConfig> are rejected when the model is parsed — the file fails to load rather than loading
with a write block that could never run. The write endpoints refuse the object at runtime too, and its
Add/Edit/Delete controls never reach the UI. Export still works, and so do
row actions and sub-views that point at
other objects.
2. The grid’s SQL is built in the source’s dialect. Paging, sorting, filters and the toolbar
quick-search are generated for the engine the source declares. For a ClickHouse source that means
backtick identifiers, LIMIT … OFFSET …, and {name:Type} parameter placeholders — so your <Sql>
must be written in that dialect too, including session parameters: {SessionCompanyId:Int64}, not
@SessionCompanyId. A source declared as SqlServer or PostgreSql keeps its usual @name form.
3. Only this object’s own query moves. Lookup datasets on the object’s editor fields and columns
still resolve against the application database, which is where the reference data lives: the
warehouse row carries ProductId, and the product master that says what that id means does not. That
is what lets an analytics grid show real labels next to warehouse aggregates.
Source names are matched case-insensitively, so DataSource="analytics" finds a
the Analytics ReportingSources entry entry. Spelling them the same in both places is still worth doing — it is
what makes the pairing obvious to the next reader.
An unconfigured source name is an error at request time — naming the source and listing what is configured — rather than a silent fall back to the application database, which would answer the grid with the wrong numbers and look like it worked.
Row-level security applies as usual, but the predicates come from your RBAC configuration as SQL text,
so a filter on a DataSource object must be written in that source’s dialect as well.
Breadcrumb parent (<ParentView>)
Section titled “Breadcrumb parent (<ParentView>)”By default the breadcrumb roots a form/view at the table it was opened from (the runtime origin) or, failing that, at the view’s own name. For an orphan form — one reached via a deep link or a different module, with no originating grid — that produces a self-referential or wrong root crumb.
Declare a logical parent with an optional <ParentView> element. Its Name maps to a navbar
item’s Name; the UI resolves that item’s label and route and uses it as the breadcrumb root.
When present it overrides the runtime origin; when omitted the default behaviour is unchanged.
<Form Name="IncidentForm" Slug="incident-form" Label="Incident"> <ParentView Name="incident" /> <!-- breadcrumb: Home › Incident › Edit --> <Sql> … </Sql> …</Form>If the name matches no navbar item, the breadcrumb falls back to a /object/{name} grid link with a
humanized label. A single parent is supported (not a multi-level chain). See
Navbar for how item Names are declared.
SQL blocks
Section titled “SQL blocks”Capabilities are inferred from which blocks are present — a view with only <Sql> is a read grid; add
<InsertSql>/<EditSql>/<DeleteSql> (or <SubmitSql>) and it gains a form.
| Element | Role |
|---|---|
<Sql PrimaryKey SortBy SortDirection PageSize> |
required primary query. PageSize defaults to 10; SortDirection is ASC/DESC |
<With> |
optional raw CTE prelude for the primary <Sql> only |
<InsertSql> / <EditSql> / <DeleteSql> |
table-style writes (use @Param per field; @Id for the PK) |
<SubmitSql> |
form-style single submit (instead of Insert/Edit/Delete) |
<ScriptBlock> |
server-side script block |
Parameters the submit blocks always receive
Section titled “Parameters the submit blocks always receive”<SubmitSql>, <InsertSql> and <EditSql> are guaranteed a bound parameter for every declared editor
field — with the field’s DefaultValue applied when the caller sent nothing (see
Default values) — so @Field is safe to reference even
for API callers that omit it. Alongside them:
| Parameter | Value |
|---|---|
@Session* (e.g. @SessionUserId, @SessionCompanyId, @SessionTimeZone) |
supplied canonically by the engine; caller-provided values are dropped |
@FormLoadTime |
UTC instant (ISO round-trip) at which the user loaded the form/view — stamped server-side, round-tripped by the client, and falling back to “now” for callers that never loaded a form. Handy for “opened at” audit stamps, dwell time, or staleness heuristics |
The write blocks can drive the client via dispatch callbacks
(return a result set whose first column is FunctionName). The render-era
SELECT 'javascript:…' returns are not executed by the React UI — see the
migration note for the
dispatch-row equivalents.
<With> — CTE prelude
Section titled “<With> — CTE prelude”<With> is prefixed to the primary <Sql> at execution time. When <With> is absent, the
primary <Sql> runs exactly as authored. The block may include the leading with keyword, or may
start directly with the CTE list; Genie normalizes it to one leading WITH before passing the
composed SQL to the existing query builder.
<With> with active_customers as ( SELECT Id, Region FROM Sales.Customers WHERE IsDeleted = 0 ), regional_totals as ( SELECT CustomerId, SUM(Total) AS TotalAmount FROM Sales.Orders GROUP BY CustomerId )</With><Sql PrimaryKey="Id" SortBy="Id"> SELECT c.Id, c.Region, t.TotalAmount FROM active_customers c JOIN regional_totals t ON t.CustomerId = c.Id</Sql>The runtime SQL is the raw With prelude followed by the existing primary select. The table query
builder owns the outer wrapping, filters, search, sorting, count, schema, and paging: it hoists the
WITH prelude above the SELECT * FROM ( … ) subquery wrapper (a CTE can’t live inside a derived
table), so the CTE is in scope for the wrapped query, count, and schema probe alike. One or more CTEs
are supported. <With> does not apply to <InsertSql>, <EditSql>, <DeleteSql>, <SubmitSql>, row
actions, import SQL, or select-field datasets. Any parameters referenced in <With> or <Sql> must
still be allowed by the existing parameter/editor-field/pre-execute-filter rules (session params such
as @SessionCompanyId are available inside the prelude). Worked example (multiple CTEs):
sample/Inventory/models/views/CategoryStockReport.view.xml.
Parameters
Section titled “Parameters”<Parameters> <Parameter Name="Id" Required="false" /></Parameters><Parameters> declares query parameters — the allowlist ParameterSanitizer honours, alongside
editor-field names. Every @parameter a query references must actually be supplied; referencing an
unsupplied parameter raises “Must declare the scalar variable @X”.
Anatomy of a view
Section titled “Anatomy of a view”A full <Table>/<Form> composes these building blocks, each covered on its own page:
- Editor fields & datasets —
<EditorFields>(the create/edit form). - Layout —
<Layout>(how the form is arranged). - Column & field expressions —
<Columns>(grid columns), styling, and the smart field rules. - Actions & row actions —
<Actions>,<RowActions>,<Filters>, toolbar quick-search. - Bulk import & handlers —
<ImportConfig>. - Sub-views —
<SubConfig>(embedded child grids). - CartTable — the inline line-item editor.
Worked example (kitchen-sink): sample/Inventory/models/views/Products.view.xml.