Portal Community

The Three Levels

1

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 }
2

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
3

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

// 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 in Cascade

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.