Template JSON schema
A template is plain JSON stored raw in templates.source. The render layer reads it, resolves it to a React component from the registry, and fills it with the data-processor result. Nothing gets compiled or executed, so it stays safe and editable without a redeploy.
The JSON is strictly validated against the contract at both save and render time. Unknown keys at any level are rejected, and the widget box shows a JSON-vs-contract diff instead of rendering something wrong.
Shape
The current format is namespaced into three blocks: template, display, and one data.<kind> block for the widget itself.
{
"schemaVersion": 1,
"template": {
"name": "PHPReactionReport/Widget/ListingTableTemplate",
"version": 1,
},
"display": {
"title": "Inventory by supplier",
"colSpan": 4, // integer 1–12 (12 = full width)
"variant": "full", // "full" | "sidebox"
},
"data": {
"listing": {
/* the widget's own config, see below */
},
},
}display may also be written flat at the top level, and the whole legacy flat
form (fields at the top level, no data/display) still parses and is
treated as a listing block. Prefer the namespaced form above for new
templates.
template, name and version
Canonical names have no leading @. The version is a colon suffix or the { name, version } object. A leading @ and the legacy @N suffix are still accepted on input, but don’t write them, the @ gets URL-encoded when passed around.
"template": "PHPReactionReport/Widget/ListingTableTemplate" // LATEST
"template": "PHPReactionReport/Widget/ListingTableTemplate:2" // version 2
"template": { "name": "PHPReactionReport/Widget/ListingTableTemplate", "version": 2 }Omitting the version resolves to the highest registered version. The base name must resolve in the registry:
| Kind | Template name | Data block |
|---|---|---|
| listing | PHPReactionReport/Widget/ListingTableTemplate | data.listing |
| html | PHPReactionReport/Widget/HTMLTemplate | data.html |
| summary | PHPReactionReport/Widget/SummaryTemplate | data.summary |
| meter | PHPReactionReport/Widget/AuxiliaryMeterTemplate | data.meter |
| gauge | PHPReactionReport/Widget/GaugeTemplate | data.gauge |
| graph | PHPReactionReport/Widget/GraphTemplate | data.graph |
listing and html render fully today. summary, meter, gauge, and graph are registered too, but their data-block schema is reserved (see Other templates).
Dynamic values, $data
The dynamic values come straight from the data processor: $data resolves to the DP’s results columns (from the metadata endpoint) and each row is a populate result.
"$data"(bare) ascolumns/headers: “I don’t know the columns, pull them all from the DP result”. Legacy alias:"$results"."$data.<field>": pull one value from the current row, used in a columnmapor indata.html.sourceMap. Single level only.
data.listing
Dynamic columns
Pull every column the data processor returns:
{
"schemaVersion": 1,
"template": {
"name": "PHPReactionReport/Widget/ListingTableTemplate",
"version": 1,
},
"display": { "title": "Products", "colSpan": 12 },
"data": { "listing": { "columns": "$data", "headers": "$data" } },
}Explicit columns
Each column is a field name (shorthand) or a { name, map, header, type } def. map pulls the value with $data.<field> and defaults to $data.<name>. A column def accepts only those four keys.
"data": {
"listing": {
"columns": [
"market", // = { name: "market", map: "$data.market" }
{ "name": "amt", "map": "$data.amount",
"header": "Amount",
"type": { "type": "money", "currencyField": "currency" } }
],
"headers": { "market": "Market" }, // optional; label defaults to the name
"columnTypes": { "updated_at": "datetime" }
}
}If a column has no header / headers entry, its own name is used as the label.
Column types
type is string | number | decimal | percent | money | date | datetime | time, or an object form for precision/currency:
"columns": [
{ "name": "Inv_Amount", "map": "$data.Inv_Amount", "type": { "type": "money", "currency": "CAD" } },
{ "name": "Inv_Amount_rate", "map": "$data.Inv_Amount_rate", "type": { "type": "percent", "precision": 1 } },
{ "name": "updated_at", "map": "$data.updated_at", "type": "datetime" }
]The shorthand columnTypes: { "column": "type" } map is also accepted.
Currencies are not hardcoded. money formatting is driven by GET /fee_currencies, using each currency’s symbol, position, and separators. Use { "type": "money", "currencyField": "currency" } to format each row in its own currency code; an unknown/absent code falls back to USD.
Listing extras
All of these live inside data.listing and render today (progression and nested go through ReportListingAdvanced).
"data": { "listing": {
"columns": ["label", { "name": "qty", "map": "$data.qty", "type": "number" }],
// backend search box above the listing; writes ?search= to the URL.
// `fields` is advisory, the API decides what's searchable.
"search": { "enabled": true, "fields": ["Code", "name"] },
// render-time sign flip. Prefer fixing the sign in the DP.
"transforms": { "qty": "invert" }
} }Progression bar, a per-row colored bar from a numeric field, colored by ratio zone:
"data": { "listing": {
"columns": ["name", { "name": "achieved", "map": "$data.achieved", "type": "number" }],
"progression": {
"field": "achieved",
"maxField": "target", // or a constant "max": 100
"zones": [
{ "upTo": 0.5, "color": "#e5484d" },
{ "upTo": 0.8, "color": "#f5b800" },
{ "upTo": 1.0, "color": "#30a46c" }
]
}
} }Nested tree, indent rows from a parent pointer, with expand/collapse:
"data": { "listing": {
"columns": "$data",
"nested": {
"idField": "id",
"parentField": "parent_id",
"labelField": "name",
"defaultExpanded": false
}
} }data.html
The HTML template renders markup. sourceMap is the only key, either literal markup or a $data.<field> pulled from the DP row.
{
"schemaVersion": 1,
"template": { "name": "PHPReactionReport/Widget/HTMLTemplate", "version": 1 },
"display": { "title": "Inventory links", "colSpan": 6 },
"data": { "html": { "sourceMap": "<div>…</div>" } },
}// dynamic markup from the DP (row.html)
"data": { "html": { "sourceMap": "$data.html" } }Other templates
These components are registered and render, but their data.<kind> block schema is reserved: the block is whitelisted so a descriptor authoring it passes validation, but the field-to-render wiring is a scheduled follow-up. Author them like listing (same $data tokens and column shape) until each is wired.
| Template | Name | Block | Purpose |
|---|---|---|---|
| Summary | PHPReactionReport/Widget/SummaryTemplate | data.summary | KVS / title-value rollups |
| Meter | PHPReactionReport/Widget/AuxiliaryMeterTemplate | data.meter | Auxiliary meter |
| Gauge | PHPReactionReport/Widget/GaugeTemplate | data.gauge | Single-value gauge (sum / count) |
| Graph | PHPReactionReport/Widget/GraphTemplate | data.graph | Line / bar chart |
Each will follow the same { literal | "$data.<field>" } value pattern as listing and HTML.