Skip to content

HeaderLines

Summary

HeaderLines is a Card with an attached editable grid — used for documents like Sales Orders and Purchase Orders. The header contains form fields; the grid contains line items.

What the client does with HeaderLines

The client renders the card normally. The user presses Ctrl+L to open the editable grid as an overlay on the card's lower portion (Esc closes it). The user edits both header fields and grid cells. Ctrl+S saves everything together.

┌─ Sales Order ─────────────────────────────┐
│ No............... [SO-1001              ] │
│ Customer......... [The Cannon Group     ] │
│ Posting Date..... [10-03-2026           ] │
├───────────────────────────────────────────┤
│ Type    No.     Description    Qty  Amount│
│ ─────── ─────── ──────────── ──── ────────│
│ Item    1000    Bicycle         2  2990.00│
│ Item    1100    Chain           4   162.00│
│                                           │
└───────────────────────────────────────────┘

Building a HeaderLines screen

Route

GET /screen/sales_order/{id} → ScreenContract

Response structure

json
{
  "layout": "HeaderLines",
  "title": "Sales Order - SO-1001",
  "lines_overlay_pct": 65,
  "sections": [
    {
      "id": "header",
      "label": "Sales Order",
      "fields": [
        {
          "id": "no",
          "label": "No.",
          "type": "Text",
          "value": "SO-1001",
          "editable": false
        },
        {
          "id": "sell_to_name",
          "label": "Customer",
          "type": "Text",
          "value": "The Cannon Group"
        },
        {
          "id": "posting_date",
          "label": "Posting Date",
          "type": "Date",
          "value": "10-03-2026"
        }
      ]
    }
  ],
  "lines": {
    "columns": [
      {
        "id": "type",
        "label": "Type",
        "type": "Option",
        "width": 10,
        "editable": true,
        "options": [
          "",
          "Item",
          "Resource",
          "G/L Account",
          "Text"
        ]
      },
      {
        "id": "no",
        "label": "No.",
        "type": "Text",
        "width": 10,
        "editable": true,
        "lookup": {
          "endpoint": "/lookup/no",
          "display_field": "no",
          "validate": "/validate/no",
          "display": "modal"
        }
      },
      {
        "id": "description",
        "label": "Description",
        "type": "Text",
        "width": "fill",
        "editable": true
      },
      {
        "id": "unit_of_measure",
        "label": "UoM",
        "type": "Text",
        "width": 6,
        "editable": false
      },
      {
        "id": "quantity",
        "label": "Quantity",
        "type": "Decimal",
        "width": 8,
        "align": "right",
        "editable": true
      },
      {
        "id": "unit_price",
        "label": "Unit Price",
        "type": "Decimal",
        "width": 12,
        "align": "right",
        "editable": true
      },
      {
        "id": "discount_pct",
        "label": "Disc. %",
        "type": "Decimal",
        "width": 8,
        "align": "right",
        "editable": true
      },
      {
        "id": "line_amount",
        "label": "Amount",
        "type": "Decimal",
        "width": 12,
        "align": "right",
        "editable": false
      }
    ],
    "rows": [
      {
        "index": 0,
        "values": [
          "Item",
          "1000",
          "Bicycle",
          "PCS",
          "2",
          "1,495.00",
          "0.00",
          "2,990.00"
        ]
      },
      {
        "index": 1,
        "values": [
          "Item",
          "1100",
          "Chain",
          "PCS",
          "4",
          "45.00",
          "10.00",
          "162.00"
        ]
      },
      {
        "index": 2,
        "values": [
          "",
          "",
          "",
          "",
          "",
          "",
          "",
          ""
        ]
      }
    ],
    "editable": true,
    "row_count": 3
  },
  "actions": {
    "save": "/screen/sales_order/SO-1001/save",
    "delete": "/screen/sales_order/SO-1001/delete"
  }
}

lines_overlay_pct

Controls how much of the screen the grid overlay covers when opened with Ctrl+L:

ValueResult
0Not used (for Card/List layouts that have no lines)
0–100Overlay covers this percentage of the body area
Default50

Set lines_open: true to open the overlay automatically when the screen loads. By default the overlay starts closed and the user opens it with Ctrl+L.

For full-screen editable grids without a card header (journals, batch entry), use the Grid layout instead.

Editable grid columns

Grid columns use the same structure as list columns, but with editable: true on columns the user can modify:

json
{
  "id": "quantity",
  "label": "Quantity",
  "type": "Decimal",
  "width": 8,
  "align": "right",
  "editable": true
}

Each column supports:

  • type — determines editing behavior (Text allows free typing, Option cycles, Decimal filters input)
  • options — for Option-type columns, the available choices
  • lookup — for columns with relational lookups (e.g., Item No.)
  • validation — client-side validation rules for cells
  • quick_entry — when false, Enter skips this column (fast data entry). See Quick Entry on grid columns.
  • formula — arithmetic expression for live calculated columns. See Calculated columns.

Context-dependent lookups

Grid columns can depend on other columns for lookups. For example, the "No." column might look up Items, Resources, or G/L Accounts depending on the "Type" column. Define a context array on the column's lookup to enable this.

See Context-dependent lookups for the full mechanism.

Empty trailing rows

Include at least one empty row at the end of the grid. This gives the user a place to start adding new lines. The row values should be empty strings:

json
{
  "index": 2,
  "values": [
    "",
    "",
    "",
    "",
    "",
    "",
    "",
    ""
  ]
}

How saves work with HeaderLines

When the user presses Ctrl+S, the client sends both header changes and the full grid data:

json
{
  "screen_id": "sales_order",
  "record_id": "SO-1001",
  "changes": {
    "sell_to_name": "Acme Corporation"
  },
  "lines": [
    [
      "Item",
      "1000",
      "Bicycle",
      "PCS",
      "2",
      "1,495.00",
      "0.00",
      "2,990.00"
    ],
    [
      "Item",
      "1100",
      "Chain",
      "PCS",
      "4",
      "45.00",
      "10.00",
      "162.00"
    ],
    [
      "",
      "",
      "",
      "",
      "",
      "",
      "",
      ""
    ]
  ]
}

Column order is the contract. The lines array uses the same column ordering as your columns array. Parse by position, not by name. Empty trailing rows are included — your server should ignore them.

See Saving for the full save protocol.