Portal Community

Example 1: Computed Summary Stats from DataGrid

Attach this to a DataGrid widget's onDataChange event to compute totals and averages that display in Stat widgets on the same page:

// DataGrid widget — onDataChange
function onDataChange(widget, variables, actions, context) {
  const rows = widget.data?.rows ?? [];

  const total = rows.reduce((s, r) => s + (r.dealValue ?? 0), 0);
  const avg = rows.length > 0 ? total / rows.length : 0;
  const highValue = rows.filter(r => r.dealValue > 50000).length;

  variables.set('dealTotal', total);
  variables.set('dealAverage', Math.round(avg));
  variables.set('highValueCount', highValue);
}

// Stat widgets on the same page display:
// {{ variables.dealTotal | currency }}
// {{ variables.dealAverage | currency }}
// {{ variables.highValueCount }}

Example 2: Custom Table Sorting

When the built-in column sort is not sufficient (e.g., natural-order string sort), use Custom JS to pre-sort data and write it to a variable that the grid binds to:

// DataGrid — onDataChange — natural sort on "version" field
function onDataChange(widget, variables, actions, context) {
  const rows = [...(widget.data?.rows ?? [])];

  // Natural sort: "2.10.0" > "2.9.0" (unlike lexicographic)
  rows.sort(function(a, b) {
    return a.version.localeCompare(b.version, undefined, { numeric: true });
  });

  variables.set('sortedRows', rows);
}

// DataGrid config — binds rows to the sorted variable
// "dataSource": "variables.sortedRows"

Example 3: Dynamic Chart Configuration

Transform flat data into a chart-ready series format on the fly:

// Chart widget — onDataChange
function onDataChange(widget, variables, actions, context) {
  const rows = widget.data?.rows ?? [];

  // Group by month
  const byMonth = {};
  rows.forEach(function(r) {
    const month = r.closedDate.substring(0, 7);  // "2026-01"
    byMonth[month] = (byMonth[month] ?? 0) + r.dealValue;
  });

  const categories = Object.keys(byMonth).sort();
  const values = categories.map(m => byMonth[m]);

  variables.set('chartCategories', categories);
  variables.set('chartSeries', [{ name: 'Revenue', data: values }]);
}

// Chart widget binds:
// categories: {{ variables.chartCategories }}
// series:     {{ variables.chartSeries }}

Example 4: Role-Conditional Navigation After Row Click

// DataGrid — onUserAction (row click)
function onUserAction(widget, variables, actions, context) {
  const row = widget.selectedRow;
  if (!row) return;

  const isAdmin = context.roles.includes('admin');
  const isOwner = row.assignedUserId === context.userId;

  if (isAdmin || isOwner) {
    // Admins and owners go to the edit page
    actions.navigate('/leads/' + row.id + '/edit');
  } else {
    // Others go to the read-only detail page
    actions.navigate('/leads/' + row.id);
  }
}

Example 5: Computed Form Validation

// Form widget — onUserAction (before submit)
function onUserAction(widget, variables, actions, context) {
  const values = widget.formValues;

  // Custom cross-field validation: end date must be after start date
  const start = new Date(values.startDate);
  const end = new Date(values.endDate);

  if (end <= start) {
    variables.set('formError', 'End date must be after start date');
    actions.showNotification('End date must be after start date', 'warning');
    return;   // Stop — don't proceed to submit
  }

  variables.set('formError', null);
  // Proceed — the no-code submit action chain fires after this custom JS
}

Example 6: Selecting All Rows and Triggering Bulk Action

// "Select All + Archive" button — onUserAction
function onUserAction(widget, variables, actions, context) {
  const rows = widget.data?.rows ?? [];
  const activeIds = rows
    .filter(r => r.status === 'active')
    .map(r => r.id);

  if (activeIds.length === 0) {
    actions.showNotification('No active records to archive', 'info');
    return;
  }

  actions.triggerWorkflow('BulkArchive', {
    ids: activeIds,
    archivedBy: context.userId
  }).then(function(result) {
    actions.showNotification(
      result.archivedCount + ' records archived',
      'success'
    );
  });
}