Skip to content

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

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.

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.

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

A full <Table>/<Form> composes these building blocks, each covered on its own page:

Worked example (kitchen-sink): sample/Inventory/models/views/Products.view.xml.