FieldDeck Views
A FieldDeck is an offline-capable, operator-first view: a person opens it, sees a deck of records assigned to them (their queue), picks one, and works it to completion through steps — capturing a timer, readings, photos and a signature on a phone or tablet, then moving the record to a closing status. It is the primitive behind kiosk worksheets like on-site field service and a maintenance workbench.
Unlike a Form, a FieldDeck is authored for hands-on completion, not editing: it is a fullscreen takeover, it is offline-first (the whole queue is snapshotted so work continues with no connection and syncs when back online), and it drives each record along a status machine toward done.
When to use it
Section titled “When to use it”Reach for a FieldDeck when all of these hold:
- A user processes a queue of records assigned to them, one at a time.
- Each record is completed through a short sequence (do the work → sign off).
- The work happens in the field / on the floor, where connectivity is unreliable.
If you just need to view or edit records, use a Form or List. If the work is desk-bound and always online, a Form with a status bar and action buttons is simpler.
View definition
Section titled “View definition”A FieldDeck is authored in the same YAML as any other view. The arch has a single root
node, type: fielddeck, carrying a card: (how a record looks in the queue) and a
content: (the worksheet):
- data_type: UiView name: field_service_onsite_fielddeck identifier: field_service_onsite_fielddeck type: FieldDeck model: FieldServiceOrder arch: - type: fielddeck offline: true card: - { type: field, name: name } - { type: field, name: state, properties: { widget: Badge } } - { type: field, name: contact } - { type: field, name: scheduled_date } content: - { type: statusbar, name: state } - type: field name: service_description properties: { widget: TextArea, readonly: true, label: Work Requested } # ... transitions, steps, capture nodes ...| Root key | Description |
|---|---|
offline | true snapshots the whole queue for offline work and queues captures for sync. |
card | The record’s appearance in the queue list. Uses the Kanban card grammar. |
content | The worksheet — the nodes shown once a record is opened. |
Content nodes
Section titled “Content nodes”content is authored with the same grammar as a Form — no new DSL, no dotted paths.
A relation renders through its widget’s display value exactly as a List cell does.
Layout & fields
Section titled “Layout & fields”| Node | Description |
|---|---|
row / column / group | Layout containers (as in a Form). |
field | A model field with a widget (TextArea, NumberInput, Switch, SelectCombo, Badge, PhotoCapture, Signature, …). |
statusbar | The record’s workflow status ribbon (bound to the status field). |
PhotoCapture (camera → downscaled image set) and Signature (sign-off pad) are general
form widgets — see Widgets.
Capture nodes
Section titled “Capture nodes”These write back to the record or its related records:
| Node | Captures |
|---|---|
timer | A start/stop worked-time span. start_field: / end_field: name the sibling datetime fields. |
meter | A numeric reading (e.g. an equipment meter). Reads its unit/current from flat sibling payload keys. |
list | Repeated rows of a child model — a spare part, a checklist item, an inspection reading. See below. |
Repeated rows — the list node
Section titled “Repeated rows — the list node”A type: list node captures many rows of a child model offline (spare parts consumed,
checklist items, meter readings). It renders an add-row form from fields and shows recorded
rows via row — reusing the standard field/card grammar, no new DSL:
- type: list name: parts # collection key in the patch + payload title: Parts Used add_label: Add Part empty_text: No parts recorded yet label_field: description # optimistic display label (server recomputes it) fields: # the add-row form - { type: field, name: product_variant, properties: { widget: CatalogCombo, catalog: products, required: true } } - { type: field, name: lot, properties: { widget: CatalogCombo, catalog: lots, filter_by: product_variant } } - { type: field, name: quantity, properties: { widget: NumberInput, default: 1 } } row: # how a recorded row displays - { type: field, name: description } - { type: text, text: "× {quantity}" }CatalogCombo is an offline relation picker: its options come from a named catalog
the bootstrap caches under catalogs (a live query won’t work with no connection). A catalog
entry is { id, name, group?, fixed_qty? }:
filter_by: <field>— filters this picker to options whosegroupmatches the parent field’s chosen option (e.g. a lot picker shows only the chosen product’s lots). A dependent picker only appears — and becomes required — once its parent has options for it.- a chosen option’s
fixed_qtypins the row’s quantity (e.g. a serial → 1).
Persistence is declared on the model, not the view — the name links them. The model
lists its collections in _fielddeck_lists (child model, parent FK, source relation, create
defaults) and fills server-trusted values (cost, a composed description) and rules
(a tracked part needs a lot) in _fielddeck_derive. The view describes what the operator
enters; the model owns how it persists and what to trust.
Actions
Section titled “Actions”An actionButton moves the record along its status machine and/or captures context:
- { type: actionButton, anchor: complete, label: Complete Job, set: { state: Done }, color: green, visible: "Q(state='On Site')" }- { type: actionButton, anchor: start_driving, label: Start Driving, set: { state: En Route }, capture: geo, visible: "Q(state='Scheduled')" }| Key | Description |
|---|---|
set | Fields to write. A value is a literal status ({state: Done}) or a @done / @scrap token resolved to the concrete closing status. |
capture | geo folds a one-shot GPS fix into the sync patch. |
color | A theme color for the button (omit for the default). |
visible | Boolean or Q-expression against the record — hide the button entirely. |
disabled | Boolean or Q-expression — show it greyed instead of hiding it. |
hint | Tooltip; the reason shown on a disabled button. |
anchor | A stable handle, required on every actionButton (see Inheritance). |
On a deck driven with gloves on, one thumb, in the field, disabled is usually the right
gate for a sequence — a button that vanishes moves everything under it, and the operator
re-reads the whole step to find out what changed. A greyed button with a hint keeps the
step’s shape fixed and says what is still outstanding:
- { type: actionButton, anchor: complete, label: Complete Job, set: { state: Done }, disabled: "Q(signature__isnull=True)", hint: Capture the customer signature first. }Keep visible for what genuinely does not belong to this job at this status. Both gates run
in the client against the record it holds, so the server-side method still validates — see
Hiding vs disabling.
Handoff & embedded widgets
Section titled “Handoff & embedded widgets”| Node | Description |
|---|---|
link | A native handoff. href: interpolates {field} from the record (e.g. open directions in the maps app). |
widget | Hosts a non-field registered widget model-agnostically (e.g. a live-location-share control) via its declared method props. |
Break the worksheet into a wizard with type: step. The final step carries final: true;
put the completion actionButton inside the closing step’s content. A step (like any
node) can be visible:-gated so it only appears at the right status:
- type: step title: Work visible: "Q(state='On Site')" content: - { type: timer, start_field: work_started, end_field: work_ended, label: On-site time } - { type: parts }- type: step title: Sign Off final: true visible: "Q(state='On Site')" content: - { type: field, name: photos, properties: { widget: PhotoCapture, label: Proof Photos } } - { type: field, name: signature, properties: { widget: Signature, label: Customer Signature } } - { type: actionButton, anchor: complete, label: Complete Job, set: { state: Done }, color: green }Nodes left outside any step stay inline and always visible (subject to their own
visible:) — the natural home for status transitions that apply before the worksheet
steps unlock.
Conditional display
Section titled “Conditional display”Any node accepts visible: with the same Q-grammar as a Form — the client evaluates
it against the open record. This gates transitions, sections and steps by status
(visible: "Q(state='On Site')").
Status machine
Section titled “Status machine”A FieldDeck drives records toward completion, so it needs to know which field holds the status and which values are terminal. The model’s bootstrap declares this:
| Key | Description |
|---|---|
status_field | The field holding status — a fixed state Selection, or a stage relation to a stage model. |
closed_values | The terminal values (closing state strings, or closing stage ids). A record leaves the queue once its status is one of these. |
status_tokens | Maps the semantic @done / @scrap used in an actionButton set: to concrete values. |
This lets the same view type serve both a fixed-Selection machine
(Scheduled → En Route → On Site → Done) and a dynamic stage model, without the arch
caring which.
Model contract
Section titled “Model contract”A FieldDeck is model-agnostic: its data comes from two methods your model implements (the BFF contract). Nothing else in the engine is FieldDeck-specific.
kiosk_bootstrap()
Section titled “kiosk_bootstrap()”A classmethod returning everything the client needs for the whole session:
async def kiosk_bootstrap(cls): # Runs AS the current user — scope by the record rules that already apply, # NOT a hard-coded "assigned to me" filter (that hides in-progress work and # shows managers nothing). orders = await Order.filter(Q(state__nin=["Done", "Cancelled"])).limit(200).all() return { "jobs": [await o._payload() for o in orders], # field-keyed record payloads "status_field": "state", "closed_values": ["Done", "Cancelled"], "status_tokens": {"done": "Done"}, "config": { ... }, # feature toggles the client honours }Each job in jobs is a field-keyed dict — one key per field name — and a relation is
the framework’s standard {id, display_name} shape, so the client renders it exactly as a
List cell does. Denormalise any value a capture node needs (a meter’s unit/current) to
flat sibling keys.
kiosk_sync(record_id, visit_uuid, patch)
Section titled “kiosk_sync(record_id, visit_uuid, patch)”Applies one visit’s captured changes to a record, idempotently. The client accumulates
captures offline into a patch tagged with a client-generated visit_uuid; a patch whose
uuid was already applied is a no-op that returns current state — so a retried or
twice-synced patch never double-books:
async def kiosk_sync(cls, record_id, visit_uuid, patch=None): order = await Order.filter(id=record_id).first() if visit_uuid in (order.applied_visit_uuids or []): return await order._payload() # already applied — idempotent no-op # ... apply scalar captures, then the status move ...Enforce completion gates (required photo, required signature) server-side on a move to a closing status — the client blocks them too, but the client can be bypassed.
Action & modes
Section titled “Action & modes”Point a WindowAction at the view like any other, listing FieldDeck in modes:
- data_type: WindowAction name: On-Site identifier: field_service_onsite_action model: FieldServiceOrder type: Window default_view: field_service_onsite_fielddeck modes: FieldDeckThe tile opens the fullscreen worksheet directly.
Next steps
Section titled “Next steps”- Views Overview — all view types and enterprise gating
- Form Views — the grammar FieldDeck content reuses
- Kanban Views — the card grammar FieldDeck cards reuse
- Widgets —
PhotoCapture,Signatureand the widget catalog