Screens
A Screen wraps a SQL connector query with everything the UI needs to render a business object: the grid (the read_query), the optional CRUD queries, the modal form definition, the audit flag and the row menu. One Screen per business object — grid, tabs, fields and actions all live in a single TOML entry.
Screens live in config/screens.toml, organised under [screens.<app>.<id>]. They are hot-reloadable with the rest of the config.
At a glance
Defining a screen
[screens.myapp.users]
label = "Users"
description = "Application users"
read_query = "users_get" # required
update_query = "users_put"
insert_query = "users_post"
delete_query = "users_delete"
auto_load = true
audit = true # adds the AUD audit tab to the dialog
editable = true # row click → modal open
uploadable = true # enables the Excel-import path
[[screens.myapp.users.dialog.tabs]]
id = "general"
label = "General"
cols = 2
fields = [
{ column = "ID", hidden = true },
{ column = "NAME", required = true, colspan = 2 },
{ column = "EMAIL" },
{ column = "STATUS" },
{ column = "CITY" },
{ column = "ADMIN", visible_when = [{ field = "STATUS", value = "Y" }] },
{ column = "PASSWORD", hide_on_edit = true },
]
connector is optional — defaults to the connector that owns the read_query. Cross-connector screens (one connector to read, another to write) are allowed.
Queries
A Screen binds up to four queries:
| Field | Purpose | Permission |
|---|---|---|
read_query | The SELECT that drives the grid. Required. | sql:<conn>:<read_query> |
update_query | The UPDATE called on Save in edit mode. Optional. | sql:<conn>:<update_query> + the query's own writable = true |
insert_query | The INSERT called on Save in add mode. Optional. | sql:<conn>:<insert_query> + writable = true |
delete_query | The DELETE called on Delete from the row menu. Optional. | sql:<conn>:<delete_query> + writable = true |
The set the caller can actually run is reported back on GET /api/screens/{app}/{id} — the React UI hides the Save / Add / Delete buttons accordingly.
Dialog
A dialog describes the modal form. It is inline on the screen — no second table to look up.
Tabs
[[screens.myapp.users.dialog.tabs]]
id = "general"
label = "General"
cols = 2 # grid width (2 = two-column layout)
hide_on_add = false
hide_on_edit = false
| Field | Description |
|---|---|
id | The tab identifier — used in the URL when the user clicks a tab. |
label / l | Tab title; l = { fr = "..." } translates per language. |
cols | CSS-grid width. Each field's colspan widens within this. |
hide_on_add / hide_on_edit | Drops the tab entirely in add / edit mode. |
fields | Ordered list of ScreenField. |
Fields
fields = [
{ column = "NAME", required = true },
{ column = "STATUS", required = true, default = "Y" },
{ column = "PASSWORD", hide_on_edit = true, hidden_in_view = true },
{ column = "ADMIN", visible_when = [{ field = "STATUS", value = "Y" }],
required_when = [{ field = "ROLE", value = ["admin", "owner"] }] },
]
Per field:
| Field | Effect |
|---|---|
column | The result column from read_query. The widget is picked from that column's rule (BOOLEAN → checkbox, ENUM → SearchSelect, LOOKUP → SearchSelect, plus date / number / text from format/type). |
hidden | Drops the field. |
disabled | Renders a read-only echo. |
required | Flags the label with a *. |
colspan | Widens within the tab's cols grid. |
default | Seeds on add. |
hide_on_add / hide_on_edit | Mode-specific visibility. |
visible_when / required_when / disabled_when | Per-field conditions (see below). |
Per-field conditions
Each *_when rule references another field on the same dialog (not a server filter). The predicate holds when that field's current form value equals value (or is in value when a list). Rules are AND-ed; a non-empty list whose predicates all pass fires the rule.
The static flags (visible: false, required: false, disabled: false) act as the fallback when the corresponding *_when list is empty.
Audit
Setting audit = true enables the auto-generated Audit tab on the dialog. It exposes:
AUD_CREATED_BY/AUD_CREATED_ATAUD_UPDATED_BY/AUD_UPDATED_AT
Resolved server-side at save time from principal.username and SYSDATE. The tab is read-only.
Actions and row menu
| Field | Purpose |
|---|---|
actions | Top-of-dialog action buttons that fire connector calls. Same shape as the Actions bindings of NomaUBL. Slice 4 — wired runtime pending. |
row_menu | Per-row ⋮ menu in the grid. Slice 6 — wired runtime pending. |
These appear under the Screen schema today; the runtime is ahead in NomaUBL and is being ported. See the docs/PLAN.md of the framework repository for the order of slices.
REST endpoints
| Method | Path | Purpose |
|---|---|---|
GET | /api/screens | Every accessible screen per app (list view — no dialog body, no actions). |
GET | /api/screens/{app} | All accessible screens for one app. 404 when nothing survives. |
GET | /api/screens/{app}/{id} | The full screen, including dialog, actions, row_menu. |
The set is permission-pruned: a screen whose read_query the caller cannot run is dropped from GET /api/screens and surfaces 403 on the per-id route.
Tips & best practices
- One screen per business object. Resist the temptation to bundle several reads into the same screen. The grid is fast; another screen with its own dialog is cleaner than a dialog with twelve tabs.
- Mark every dialog field with a real
required. It saves a round-trip — the dialog refuses to save until the required fields are filled, instead of letting the backend reject the row. - Per-field conditions are evaluated live.
visible_when/required_when/disabled_whenare attached to the field itself, AND-ed together, and re-evaluated each time the form changes. Easy to keep predictable: each rule references another field on the same dialog. - Audit auto-resolves user + timestamp. Do not bind
AUD_CREATED_BY/AUD_UPDATED_ATmanually from the form — they are filled server-side from the principal andSYSDATEat save time. - Cross-connector saves are legitimate. A screen reading from
myappand writing to an audit connector is supported. Pick the connector on the query, not on the screen.