Configuration Cascade
Widget configuration is resolved from three levels — widget definition defaults, app-level overrides, and pane-level placement config. Deeper levels always win.
The Three Levels
Level 1: Widget Definition Defaults
The defaultConfig in the Widget Definition. These are the factory defaults — what the widget uses if no override is provided at any level. Lowest priority.
// DataGrid defaultConfig
{ "pageSize": 20, "selectable": false, "sortable": true }
Level 2: App-Level Override (via Widget Registry Admin)
Platform administrators can configure tenant-level overrides for specific widget types. These apply to all placements of that widget type within the tenant — useful for branding, default pagination sizes, or enforcing data source patterns.
// Tenant-level override for DataGrid
{ "pageSize": 50 } // All DataGrids in this tenant default to 50 rows
Level 3: Pane-Level Placement Config
What you configure in the App Studio Properties Editor for a specific widget instance. Highest priority — always wins over levels 1 and 2.
// Specific placement config
{ "dataSource": "GetLeads", "pageSize": 100 }
Merge Example
// Level 1 — Widget Definition defaultConfig:
{
"pageSize": 20,
"selectable": false,
"sortable": true,
"filterable": false,
"exportable": false
}
// Level 2 — Tenant-level admin override:
{
"pageSize": 50, // override: change default page size for all grids
"filterable": true // override: enable filtering tenant-wide
}
// Level 3 — This specific placement config:
{
"dataSource": "GetLeads",
"columns": [...],
"pageSize": 100 // override: this grid shows 100 rows
}
// ─────────────────────────────────────────
// Effective config at runtime (merged):
{
"dataSource": "GetLeads", // from Level 3
"columns": [...], // from Level 3
"pageSize": 100, // Level 3 wins over Level 2 (50) and Level 1 (20)
"selectable": false, // from Level 1 (no override)
"sortable": true, // from Level 1 (no override)
"filterable": true, // from Level 2 (tenant override)
"exportable": false // from Level 1 (no override)
}
Deep Merge vs. Shallow Merge
The cascade uses deep merge for nested objects and arrays are replaced (not merged):
- Scalar values (
string,number,boolean): deeper level replaces shallower - Objects: deep merge — each key is merged recursively
- Arrays: deeper level completely replaces the array (no array item merging)
// If defaultConfig has a columns array:
defaultConfig.columns = [{ field: "id" }, { field: "name" }]
// And placement config has columns:
placement.columns = [{ field: "status" }]
// Result: placement replaces the entire array
effectiveColumns = [{ field: "status" }]
// NOT: [{ field: "id" }, { field: "name" }, { field: "status" }]
Token expressions ({{ }}) are resolved after the cascade merge. All three levels can use token expressions in their config values — they are preserved as strings through the merge and evaluated at render time against the live app state.