Skip to content

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.

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.

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 keyDescription
offlinetrue snapshots the whole queue for offline work and queues captures for sync.
cardThe record’s appearance in the queue list. Uses the Kanban card grammar.
contentThe worksheet — the nodes shown once a record is opened.

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.

NodeDescription
row / column / groupLayout containers (as in a Form).
fieldA model field with a widget (TextArea, NumberInput, Switch, SelectCombo, Badge, PhotoCapture, Signature, …).
statusbarThe 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.

These write back to the record or its related records:

NodeCaptures
timerA start/stop worked-time span. start_field: / end_field: name the sibling datetime fields.
meterA numeric reading (e.g. an equipment meter). Reads its unit/current from flat sibling payload keys.
listRepeated rows of a child model — a spare part, a checklist item, an inspection reading. See below.

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 whose group matches 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_qty pins 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.

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')" }
KeyDescription
setFields to write. A value is a literal status ({state: Done}) or a @done / @scrap token resolved to the concrete closing status.
capturegeo folds a one-shot GPS fix into the sync patch.
colorA theme color for the button (omit for the default).
visibleBoolean or Q-expression against the record — hide the button entirely.
disabledBoolean or Q-expression — show it greyed instead of hiding it.
hintTooltip; the reason shown on a disabled button.
anchorA 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.

NodeDescription
linkA native handoff. href: interpolates {field} from the record (e.g. open directions in the maps app).
widgetHosts 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.

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

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:

KeyDescription
status_fieldThe field holding status — a fixed state Selection, or a stage relation to a stage model.
closed_valuesThe terminal values (closing state strings, or closing stage ids). A record leaves the queue once its status is one of these.
status_tokensMaps 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.

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.

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.

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.

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: FieldDeck

The tile opens the fullscreen worksheet directly.

  • Views Overview — all view types and enterprise gating
  • Form Views — the grammar FieldDeck content reuses
  • Kanban Views — the card grammar FieldDeck cards reuse
  • WidgetsPhotoCapture, Signature and the widget catalog