Portal Community

Widget Placement Structure

// TypeScript — WidgetPlacement interface
interface WidgetPlacement {
  // Identity
  widgetId: string;          // Unique within the app (e.g., "leads-grid")
  widgetTypeId: string;      // References WidgetDefinition (e.g., "DataGrid")

  // Configuration — overrides the WidgetDefinition defaults
  config: Record<string, unknown>;   // Supports {{ token }} expressions in values

  // Layout position within the parent Pane's grid
  layout: {
    colStart: number;        // 1-12
    colSpan: number;         // 1-12
    rowStart?: number;       // auto if omitted
    rowSpan?: number;        // default 1
  };

  // Per-breakpoint layout overrides
  layoutBreakpoints?: {
    tablet?: Partial<WidgetLayout>;
    mobile?: Partial<WidgetLayout>;
  };

  // Actions — event to action bindings
  actions: Record<string, ActionConfig | ActionConfig[]>;
  // e.g., { onClick: { type: "navigate", target: "/leads/{{ row.id }}" } }

  // Visibility
  visibleTo?: string[];         // Role list, empty = all
  visibilityExpression?: string; // Token expression: "{{ context.roles.includes('admin') }}"

  // Custom JS (optional)
  customJs?: string;           // Sandboxed JS for this widget
}

Placement vs. Definition Config

The config in a Widget Placement is a partial override of the Widget Definition's defaultConfig. Properties not specified in the placement config fall back to the definition's defaults via the configuration cascade.

// Widget Definition defaultConfig for DataGrid:
{
  "pageSize": 20,
  "selectable": false,
  "sortable": true
}

// Widget Placement config (placement overrides only):
{
  "dataSource": "GetLeads",
  "columns": [ ... ],
  "pageSize": 50       // overrides the default of 20
  // selectable and sortable inherit from definition defaults
}

// Effective config at runtime (merged):
{
  "dataSource": "GetLeads",
  "columns": [ ... ],
  "pageSize": 50,      // from placement
  "selectable": false, // from definition default
  "sortable": true     // from definition default
}

Actions on a Placement

Actions are defined per-placement and per-event. The same widget type in different apps can have completely different actions configured — a DataGrid's onRowSelect in the Leads app navigates to a lead detail page, while in the Contacts app it opens a modal.

// Widget Placement actions example
"actions": {
  "onRowSelect": {
    "type": "navigate",
    "target": "/leads/{{ row.id }}"
  },
  "onMount": [
    {
      "type": "set-variable",
      "variable": "currentPage",
      "value": "leads-list"
    }
  ]
}

Visibility Rules

Two complementary visibility mechanisms are available on every placement:

// Simple role list
"visibleTo": ["admin", "manager"]

// Token expression — visibility based on a variable
"visibilityExpression": "{{ variables.showAdvancedControls }}"

// Combined — both must be satisfied (AND logic)
"visibleTo": ["admin"],
"visibilityExpression": "{{ route.id !== undefined }}"
Client-Side vs. Server-Side Enforcement

Widget visibility rules are enforced client-side — a user with the wrong role simply does not see the widget rendered. However, the underlying data API calls are always protected server-side by AIExtension.Service. A hidden widget cannot be "un-hidden" to reveal unauthorized data.