Skip to main content

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

📋 Users · TableView▶ RunID · NAME · EMAIL · STATUS · CITY · ⋮42 · Anna Lefèvre · anna@… ·ActiveParis43 · Marc Dupont · marc@… ·InactiveLyon44 · Léa Martin · lea@… ·ActiveMarseilleDIALOG · User #42⛶ ✕GeneralAddressAuditNAME *Anna LefèvreSTATUS *Active ▾EMAILanna@example.comCITYPAR — Paris ▾ADMIN (visible_when: STATUS = Active)PASSWORD (rule: PASSWORD)••••••••••Cancel💾 SaveGrid · read_querycursor.description → columnsDialog · tabscols, hide_on_add / editaudit auto-tabField widgetpicked from column ruleBOOLEAN · ENUM · LOOKUPPer-field conditionvisible_when / required_whenreads the live form state

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:

FieldPurposePermission
read_queryThe SELECT that drives the grid. Required.sql:<conn>:<read_query>
update_queryThe UPDATE called on Save in edit mode. Optional.sql:<conn>:<update_query> + the query's own writable = true
insert_queryThe INSERT called on Save in add mode. Optional.sql:<conn>:<insert_query> + writable = true
delete_queryThe 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
FieldDescription
idThe tab identifier — used in the URL when the user clicks a tab.
label / lTab title; l = { fr = "..." } translates per language.
colsCSS-grid width. Each field's colspan widens within this.
hide_on_add / hide_on_editDrops the tab entirely in add / edit mode.
fieldsOrdered 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:

FieldEffect
columnThe 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).
hiddenDrops the field.
disabledRenders a read-only echo.
requiredFlags the label with a *.
colspanWidens within the tab's cols grid.
defaultSeeds on add.
hide_on_add / hide_on_editMode-specific visibility.
visible_when / required_when / disabled_whenPer-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_AT
  • AUD_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

FieldPurpose
actionsTop-of-dialog action buttons that fire connector calls. Same shape as the Actions bindings of NomaUBL. Slice 4 — wired runtime pending.
row_menuPer-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

MethodPathPurpose
GET/api/screensEvery 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_when are 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_AT manually from the form — they are filled server-side from the principal and SYSDATE at save time.
  • Cross-connector saves are legitimate. A screen reading from myapp and writing to an audit connector is supported. Pick the connector on the query, not on the screen.