Datagrid Component
DataGrid is a component that displays data in a grid format. It supports pagination, sorting, and filtering. The component can be used to display data from a GraphQL query.
Row-aware dots menu disabled state
props.dotsMenu.items[].disabled can be a template expression. It is evaluated with both component props and the current row data, so row fields can control whether an action is disabled.
dotsMenu:
items:
- label: Release
disabled: "{{ eval status !== 'Ready' || isLocked }}"
onClick:
- action: workflow
workflow: release-order
In the example, status and isLocked are read from the row.
View Types
The DataGrid supports three view types: table (default), collection, and list.
| View Type | Description |
|---|---|
table | Traditional table grid with rows and columns (default) |
collection | Responsive grid of cards, ideal for visual browsing |
list | Compact single-column list using MUI List components |
Table View (Default)
The standard table view displays data in rows and columns:
views:
- name: tableView
displayName:
en-US: Table View
viewType: table # or omit - table is default
includeEntityKeysInExport: true # default; force PK/entity keys into exports for re-import matching
columns:
- name: name
- name: email
- name: status
Export identity keys: table views support includeEntityKeysInExport. It defaults to true and forces entity key/primary key columns into exports so rows can be matched by ID during re-import. Set it to false only when exports must omit internal IDs.
Collection View
Collection view displays data as a responsive grid of cards:
views:
- name: cardView
displayName:
en-US: Card View
viewType: collection
columns:
- name: name
- name: email
- name: status
collection:
itemSize:
xs: 12 # 1 card per row on mobile
sm: 6 # 2 cards per row on small screens
md: 4 # 3 cards per row on medium screens
lg: 3 # 4 cards per row on large screens
spacing: 3
dividers: false
children: # Optional custom template
- component: card
props:
elevation: 2
children:
- component: cardContent
children:
- component: text
props:
variant: h6
value: "{{item.name}}"
- component: text
props:
variant: body2
color: textSecondary
value: "{{item.email}}"
Collection Properties
| Property | Type | Default | Description |
|---|---|---|---|
children | array | auto-generated | Custom YAML template for each item |
itemName | string | 'item' | Variable name for current item |
itemSize | number | object | { xs: 12, sm: 6, md: 4, lg: 3 } | Grid column size |
spacing | number | 3 | Gap between items |
emptyMessage | string | 'No items to display' | Empty state message |
List View
List view displays data in a compact, single-column format:
views:
- name: listView
displayName:
en-US: List View
viewType: list
columns:
- name: name
- name: email
- name: department
- name: status
list:
dense: true
dividers: true
primaryField: name
secondaryField: email
avatarField: avatarUrl
List Properties
| Property | Type | Default | Description |
|---|---|---|---|
children | array | auto-generated | Custom YAML template for each item |
itemName | string | 'item' | Variable name for current item |
dividers | boolean | false | Show dividers between items |
dense | boolean | false | Use compact spacing |
disablePadding | boolean | false | Remove list padding |
emptyMessage | string | 'No items to display' | Empty state message |
primaryField | string | - | Field for primary text |
secondaryField | string | - | Field for secondary text |
avatarField | string | - | Field for avatar URL |
List View with Custom Template
views:
- name: customList
viewType: list
columns:
- name: title
- name: description
list:
dividers: true
children:
- component: listItemText
props:
primary: "{{item.title}}"
secondary: "{{item.description}}"
primaryTypographyProps:
fontWeight: bold
Multiple View Types
Allow users to switch between different view types:
component: dataGrid
name: productsGrid
props:
options:
query: products
entityKeys:
- productId
enableViews: true
views:
- name: table
displayName:
en-US: Table
viewType: table
columns:
- name: name
- name: sku
- name: price
- name: stock
- name: cards
displayName:
en-US: Cards
viewType: collection
columns:
- name: name
- name: sku
- name: price
collection:
itemSize:
xs: 12
sm: 6
md: 4
- name: list
displayName:
en-US: List
viewType: list
columns:
- name: name
- name: sku
- name: price
list:
dense: true
dividers: true
primaryField: name
secondaryField: sku
Example
component: dataGrid
name:
inputs:
props:
refreshHandler: "countries"
enableStore: true # enable store for the datagrid, Row data will be placed in the store
views:
- name: allCountries
viewType: "grid" # collection, grid, Default is grid
paginationPosition: bottom # bottom (default) or top
itemComponent: # render item for collection
component: card
props:
title: "{{ item.name }}"
description: "{{ item.countryCode }}"
image: "{{ item.image }}"
actions:
- component: button
props:
label: "View" # action to execute when the button is clicked
onClick:
- navigate: "countries/{{ item.countryCode }}"
displayName:
en-US: All Countries
enableEdit: true
enableSelect: "Multiple"
onRowClick: # action to execute when a row is clicked (override default action)"
columns:
- name: countryCode
label:
en-US: Country code
- name: name
label:
en-US: Name
editor:
type: text
showAs:
component: text
props:
value: "{{ name }}"
- name: created
label:
en-US: Created
- name: createdByUser.userName
label:
en-US: Created by
- name: lastModified
label:
en-US: Last modified
- name: lastModifiedByUser.userName
label:
en-US: Last modified by
- name: customField
value: "{{ item.customField }}"
filter:
search: "{{ search }}" # default search for the data view
orderBy: # default sorting for the datagrid
- name: countryCode
direction: ASC
childViews: # child views for the datagrid
- name: states # field name / resolver for GraphQL query
onRowClick: # action to execute when a row is clicked
columns:
- name: stateCode
label:
en-US: State code
options:
query: countries
rootEntityName: Country
enableToolbar: true # show toolbar in the datagrid, default is true
enableColumns: true # enable column selection in the datagrid, default is true
enableFilter: true # enable filtering in the datagrid, default is true
enableSearch: true # enable search in the datagrid, default is true
entityKeys:
- countryCode
enableDynamicGrid: true
navigationType: browser # browser or store,
editableOptions:
onNewRow: # mutation to create a new record
mutation:
onRowChange: # mutation to update a record
mutation:
onRowDelete: # mutation to delete a record
mutation:
itemTemplate: ?
countryCode: "{{ data.countryCode }}"
onDataLoad:
- name: "setCountryCode"
args:
countryCode: "{{ data[0].countryCode }}"
onEditClick:
navigate: countries/{{ countryCode }}
onRowClick:
dialog:
name: updateCountryDialog
props:
permission: System/Countries/Update
title:
en-US: "Update Country"
countryCode: "{{ countryCode }}"
organizationId: "{{ organizationId }}"
component:
layout:
component: layout
props:
children:
- component: Countries/UpdateCountry
defaultView: allCountries
toolbar:
- component: dropdown
props:
label:
en-US: Actions
name: actionscountries
icon: activity
options:
variant: secondary
items:
- label:
en-US: Import Countries
onClick: []
- label:
en-US: Export Countries
onClick: []
children:
Default View Search
A view can define a default search term that seeds the DataGrid's search box when the grid first loads. Use it to pre-populate search from a route parameter or a parent component input, or to give a view a sensible starting search — for example, opening a view already narrowed to a specific tenant, customer, or reference number.
The property sits at the view level, alongside filter and orderBy:
component: dataGrid
name: ordersGrid
props:
views:
- name: recentOrders
displayName:
en-US: Recent Orders
columns:
- name: orderNumber
- name: customerName
- name: status
search: "{{ search }}" # default search for the view, seeded from a page variable
orderBy:
- name: created
direction: DESC
Property
| Property | Type | Default | Description |
|---|---|---|---|
search | string | - | Default search term applied when the grid initializes. Supports template expressions resolved against page variables, inputs, and store. |
Resolution Priority
The initial search term is resolved with highest priority first:
- URL parameter —
{gridName}_search(or the name set viaoptions.overrides.searchParamName), e.g. when returning to a shared or bookmarked link - View default — the
searchof the initially active view (e.g. thedefaultView), only if it resolves to a non-empty string - Empty — no search applied
The template is resolved once, at grid initialization, and the resolved value is trimmed before use — if it is empty or whitespace after trimming, no default is applied. A static string is also valid (for example, search: "pending").
Behavior Details
| Scenario | Behavior |
|---|---|
| Grid loads, no URL search parameter | View default is resolved and seeds the search box |
Grid loads with {gridName}_search in the URL | URL parameter wins; the view default is ignored |
| Template resolves to empty/undefined | No default is applied; the grid loads unsearched |
| User types a new term | User input wins and behaves like normal search (written back to the URL parameter) |
| User clears the search (✕) | Search stays cleared for the session; the default does not re-apply |
| User switches views | The current search term carries over; view defaults apply at initial load only |
| Navigation away and back | The term persists via the URL parameter, like any user-entered search |
| Saved views (SaveViewDialog) | Unaffected — search is available in YAML view definitions only |
Example: Seeding Search from a Route Parameter
# Page receives ?search=ACME and passes it to the grid as a variable
component: dataGrid
name: customersGrid
props:
views:
- name: allCustomers
displayName:
en-US: All Customers
columns:
- name: name
- name: email
search: "{{ search }}"
options:
query: customers
rootEntityName: Customer
enableSearch: true
When the page is opened with ?search=ACME, the grid loads with ACME in the search box and the data already filtered. The user can refine or clear it like any manual search.
Best Practices
- The seeded term is visible and clearable by the user. For an invisible, permanent restriction on the view's data, use the view's
filterinstead. - Don't combine a
searchdefault withenableSearch: false— the user would have no way to see or remove the active search term. - Prefer template expressions over hard-coded terms when the default depends on navigation context (route parameters, parent component inputs).
Change Tracking & Row Highlights
The DataGrid supports automatic change tracking that detects new and updated rows across refreshes, highlighting them visually. Highlights accumulate across multiple refresh cycles, with each highlighted row having its own per-row TTL (Time-To-Live) measured in refresh cycles.
Enabling Change Tracking
Set enableChangeTracking: true in the DataGrid options along with a refreshHandler and refreshInterval:
component: dataGrid
name: pickupOrdersGrid
props:
refreshHandler: pickupOrders
options:
query: pickupOrders
entityKeys:
- orderId
enableChangeTracking: true
enableRefresh: true
refreshInterval: 30000
highlightNew: true
highlightUpdated: true
highlightForRefreshes: 5
Change Tracking Options
| Property | Type | Default | Description |
|---|---|---|---|
enableChangeTracking | boolean | true | Enable/disable change tracking |
highlightNew | boolean | true | Highlight newly added rows |
highlightUpdated | boolean | true | Highlight rows with updated values |
highlightForRefreshes | number | 1 | Per-row TTL — number of refresh cycles a highlight persists |
How It Works
- Snapshot: On each data fetch, the DataGrid takes a snapshot of the current data.
- Comparison: On the next refresh (auto-refresh or external), the new data is compared against the snapshot using
entityKeysto match rows. - Highlight accumulation: New and updated rows are added to a highlight map with a fresh TTL. Existing highlights have their TTL decremented by 1 each cycle. When a row's TTL reaches 0, its highlight is removed.
- Per-row independence: Each row's TTL is independent — older highlights expire first while newer ones persist.
Per-Row TTL Example
With highlightForRefreshes: 3:
Refresh #0 → Initial load, 20 rows, snapshot taken
Refresh #1 → 5 new rows arrived → A(3) B(3) C(3) D(3) E(3)
Refresh #2 → 1 new row arrived → A(2) B(2) C(2) D(2) E(2) F(3)
Refresh #3 → nothing new → A(1) B(1) C(1) D(1) E(1) F(2)
Refresh #4 → 2 new rows arrived → F(1) G(3) H(3) (A–E expired)
Refresh #5 → nothing new → G(2) H(2) (F expired)
External Refresh with Highlight Options
The refresh action now supports an optional options object to control highlighting behavior per-call. These options override the grid-level defaults for that specific refresh:
- refresh: "pickupOrders"
options:
highlightNew: true
highlightForRefreshes: 1
See Actions — refresh for details.
Refresh Options Resolution Order
Options are resolved with highest priority first:
- Refresh action
options— per-call override (external refresh only) - DataGrid
options— grid-level defaults (auto-refresh always uses these) - Global defaults —
highlightNew: true,highlightUpdated: true,highlightForRefreshes: 1
Behavior Details
| Scenario | Behavior |
|---|---|
| Auto-refresh (polling timer) | Grid-level defaults apply; highlights accumulate |
| External refresh action (no options) | Grid-level defaults apply |
| External refresh action (with options) | Action options override grid defaults |
| Row re-highlighted before TTL expires | TTL resets to fresh value |
| Pagination change | Comparison skipped; TTLs are not decremented |
| Filter/search/view change | All highlights cleared; snapshot reset |
| Cross-window refresh | Options propagate via postMessage; each window accumulates independently |
Disabling Highlights on a Specific Refresh
- refresh: "pickupOrders"
options:
highlightNew: false
highlightUpdated: false
Export Column Configuration
The DataGrid automatically exposes column export configuration (headers and column mappings) to the context store whenever the active view changes. This enables external components — such as export buttons — to access the current grid's visible column names and their human-readable labels.
How It Works
When a view's viewColumns change, the DataGrid writes to the context store:
| Store Key | Type | Description |
|---|---|---|
{gridName}.headers | string[] | Ordered list of column paths for the current view (export field keys) |
{gridName}.columnMappings | Record<string, string> | Map from column path → resolved display label |
{gridName}.exportTemplates | Record<string, string> | Map from column path → export template expression for columns that define exportTemplate |
Excluded columns: excludeFromQuery columns, value columns, hidden columns (isHidden: true), and invisible columns (isVisible: false). Template variables in isHidden/isVisible are resolved using current local state.
Entity keys: By default, the grid prepends any missing options.entityKeys to the export header list so exported rows can be re-imported with ID-first matching. This keeps edited business-key fields from creating duplicates on import. Disable this per saved view or grid with includeEntityKeysInExport: false when a clean cross-tenant/template export should not carry IDs.
Label resolution: String/number → used directly; localized object → en-US preferred, fallback to first locale; null → falls back to column name.
These values can be passed directly to Utilities/Export@1 as headers and columnMappings. Pair them with workflow exportTemplates when a displayed grid column needs a computed export value that is not a simple flattened field path.
# Export button reading column config from context
- name: exportButton
component: button
componentProps:
props:
label: Export CSV
action:
- export:
query: "orders"
headers: "{{store.ordersGrid.headers}}"
columnMappings: "{{store.ordersGrid.columnMappings}}"
exportTemplates: "{{store.ordersGrid.exportTemplates}}"
Save View Dialog
The SaveViewDialog provides a UI for managing DataGrid views: saving/updating views, creating new views, reordering columns via drag-and-drop, renaming column display names, toggling column visibility, and deleting views.
Stability During Auto-Refresh
The SaveViewDialog only initializes its internal state when the dialog opens — not on every view change. This prevents auto-refresh cycles from resetting a user's in-progress edits. The useEffect that populates dialog state is gated on the open prop.
View Switching After Save
When a user saves or creates a view:
- A
justSavedViewRefflag prevents the view-selection effect from reverting to the old URL-parameter view - The views list refreshes with
skipCache: trueso the newly saved view appears immediately - The flag is cleared on the next render cycle
The same skipCache bypass is used when deleting a view.
Density Toggle
The DataGrid supports a density toggle in the toolbar that lets users switch between three row density modes: standard, comfortable, and compact. Each mode adjusts row height, column widths, and UI element sizes.
Density Modes
| Mode | Row Height | Min Column Width | Max Column Width | Best For |
|---|---|---|---|---|
standard | 50px | 150px | 300px | Normal use, readability |
comfortable | 40px | 120px | 250px | Moderate data density |
compact | 28px | 60px | 150px | Maximum data on screen |
In compact mode, checkboxes, expand icons, dots menu columns, and sticky column controls are all proportionally smaller.
Enabling the Density Toggle
The density toggle appears in the toolbar by default unless explicitly disabled. Control it at two levels:
Per-grid — via DataGrid options:
component: dataGrid
name: ordersGrid
props:
options:
enableDensity: true # default: true
Per-tenant (white-label) — via the dataGrid config in the white-label configuration:
// In white-label client config
export const config: WhiteLabelConfig = {
// ...
dataGrid: {
enableDensity: true, // Show/hide the toggle globally
defaultDensity: "comfortable", // Default mode for all grids
},
};
White-Label DataGrid Configuration
| Property | Type | Default | Description |
|---|---|---|---|
dataGrid.enableDensity | boolean | false | Show the density toggle in grid toolbars |
dataGrid.defaultDensity | 'standard' | 'comfortable' | 'compact' | 'standard' | Default density mode for all grids |
Density Persistence
The selected density is persisted per grid in localStorage using the key dataGrid_density_{gridName}. This means:
- Each grid remembers its own density preference
- The preference survives page refreshes and browser restarts
- If no preference is stored, the white-label default is used (falling back to
'standard')
Resolution Order
- localStorage — per-grid user preference (highest priority)
- White-label config —
dataGrid.defaultDensity(tenant default) - Global default —
'standard'
Filter Persistence
By default, the DataGrid persists filter state (both filter column selections and filter values) across route navigation within the same browser session. This means that when a user applies filters on a grid, navigates away to another page, and then returns, their filters are automatically restored.
How It Works
Filter state is managed by a singleton Zustand store (gridFilterStore) that lives outside the component tree. Unlike per-page state (which resets on navigation), this store survives route changes because it is not tied to any UiContextProvider.
- Filter columns (which filter inputs are visible) are persisted when the user adds or removes filter columns, and when view defaults are first applied.
- Filter values (the actual selected/entered filter criteria) are persisted every time the user changes a filter.
- On mount, the DataGrid reads any previously persisted state for its
gridNameand initializes with those values instead of empty defaults.
Behavior Details
| Scenario | Behavior |
|---|---|
| User sets filters, navigates away, returns | Filters are restored automatically |
| User opens grid for the first time | View default filters apply as normal |
| View has default filter columns, user hasn't customized | Defaults are persisted on first load so they're available on return |
| User adds/removes filter columns | Changes are persisted immediately |
| Page refresh (full reload) | Filters reset (store is in-memory only) |
Filter persistence is in-memory only — it does not survive a full page refresh or new browser tab. It is designed specifically for SPA route navigation within a single session.
Responsive Layout
The DataGrid toolbar and filter panel adapt automatically to different screen sizes using MUI Grid breakpoints.
Toolbar
On mobile (xs), the View Selector and Search Input each take the full row width, stacking vertically for easy touch access. On small screens (sm) and above, they revert to auto-width and share the toolbar row side by side. The search input enforces a minimum width of 20ch on sm+ to remain usable.
| Element | xs (mobile) | sm and above |
|---|---|---|
| View Selector | Full width (12 columns) | Auto width |
| Search Input | Full width (12 columns) | Flexible, min-width 20ch |
Filter Inputs
Filter inputs in the filter bar use a responsive grid so that more filters are visible on wider screens without horizontal scrolling:
| Breakpoint | Columns per filter | Filters per row |
|---|---|---|
| xs (mobile) | 12 | 1 |
| sm | 12 | 1 |
| md | 6 | 2 |
| lg | 4 | 3 |
| xl | 3 | 4 |
This layout applies automatically to all filter columns — no YAML configuration is required.
Default-Expanded Child Rows
Set props.options.defaultExpandedRows: true to seed expanded state for every parent row on the first data load when the current view defines childViews. This is useful for master/detail grids where nested rows should be visible immediately.
Unlike the old autoExpand behavior, defaultExpandedRows only expands rows that have not been seen before. If a user collapses a row, refreshes do not reopen it. When a new parent row appears after refresh, that new row is expanded once. Switching views or changing the grid identity resets the seen-row set.
component: dataGrid
name: ordersGrid
props:
options:
query: orders
rootEntityName: Order
entityKeys: [orderId]
defaultExpandedRows: true
views:
- name: orders
columns:
- name: orderNumber
childViews:
- name: containerCommodities
columns:
- name: commodityName
Export Column Overrides
Columns can define export-specific behavior without affecting rendering:
exportPathchanges the key/path emitted for the column during export.exportTemplateformats the exported value with the same template context used by the grid.- Both options may be declared at the top level of the column or under
props. - Export filtering respects
isHidden,props.isHidden,isVisible, andprops.isVisibleafter template evaluation.
columns:
- name: customer.displayName
label: { en-US: Customer }
exportPath: customerName
- name: totalAmount
label: { en-US: Total }
exportTemplate: "{{ formatCurrency totalAmount currencyCode }}"
- name: internalNotes
isVisible: "{{ eval includeInternalColumns }}"
DataGrid Options
The options object is used to configure the datagrid. The options object has the following properties:
query- The name of the GraphQL query to use to fetch data for the datagrid.rootEntityName- The name of the root entity in the GraphQL query.entityKeys- An array of entity keys to use to identify the entity in the datagrid.enableDynamicGrid- A boolean value that indicates whether the datagrid is dynamic.navigationType- The type of navigation to use when navigating to a new page.editableOptions- An object that contains the editable options for the datagrid.onDataLoad- An array of actions to execute when the data is loaded.onEditClick- An action to execute when the edit button is clicked.onRowClick- An action to execute when a row is clicked.defaultView- The default view to display in the datagrid.defaultExpandedRows- Pre-expands parent rows on the first data load when the active view haschildViews. User collapses/expansions are preserved on later refreshes.toolbar- An array of toolbar items to display in the datagrid.items- An array of items to display in the datagrid. As alternative toqueryproperty.
View Pagination Position
Each view can place the pagination controls at the bottom of the grid (default) or above the rows. Use paginationPosition: top for dense operational screens where users need paging controls before scrolling through the table.
views:
- name: allOrders
displayName:
en-US: All Orders
paginationPosition: top
columns:
- name: orderNumber
- name: customer.name
The setting is persisted when users save view settings from the DataGrid view dialog.
Columns
The columns object is an array of objects that define the columns in the datagrid. Each object in the columns array has a name property that defines the field name or resolver for the GraphQL query.
Column Properties
name- The name of the field or resolver for the GraphQL query.label- An object that contains the labels for the column.editor- An object that defines the editor for the column.showAs- An object that defines how to display the column value.value- Value column. GraphQL query will ignore this column, and the value will be computed from thevalueproperty.isHidden- Boolean or template expression that hides the column. Hidden columns are excluded from export.isVisible- Boolean or template expression; when false, the column is excluded from export.exportPath- Export key/path override. Can be set at the column top level or underprops.exportPath.exportTemplate- Template used to format the exported value. Can be set at the column top level or underprops.exportTemplate.props.allowOrderBy- Boolean to enable/disable sorting on this column (default:true).props.allowUnsort- Boolean to allow removing the sort on 3rd click (default:true). Whenfalse, clicking toggles between ASC and DESC only. Can also be set at the view level viaallowUnsorton the datagrid props.props.orderByProperty- Optional property name to use for sorting instead of the column name. Useful when displaying a formatted value but sorting by an underlying field (e.g., display customer name but sort by customer ID).subQueries- Additional GraphQL field paths to include in the query without rendering them as columns. Use this whenshowAs, row actions, exports, or conditional styles need extra nested data that is not visible as its own column.props.allowFilter- Boolean to enable/disable filtering on this column.props.filterByProperty- Optional property name to use for filtering instead of the column name. Alias forfilter.fieldName- useful when you don't need a custom filter component.props.filter- Filter configuration object:component- Component to use for the filter input (e.g., custom select, date picker).fieldName- Optional property name to use for filtering instead of the column name (takes precedence overfilterByProperty).props- Additional properties passed to the filter component.
enableEdit- Boolean to enable inline cell editing for this column.editor- Component config rendered viaComponentRenderwhenenableEditis true. Uses the same{ component, props }pattern asshowAs.onEdit- Action array executed sequentially when the cell value changes.exportPath- Optional property path to use when exporting this column. Takes precedence over bothpathandnamefor building export headers and column mappings. Useful when the column uses a GraphQL resolver expression that differs from the desired export field path.exportTemplate- Optional template used by export tooling for this column. The grid exposes these as{gridName}.exportTemplatesalongside headers and column mappings.includeEntityKeysInExport- Optional view-level override for whether missingoptions.entityKeysare forced into export headers. If omitted, the grid-leveloptions.includeEntityKeysInExportis used; if both are omitted, entity keys are included.
Fetching Hidden Data with subQueries
Use subQueries when a rendered column needs related fields that are not themselves visible columns. The DataGrid adds these field paths to the generated GraphQL selection, but they do not appear in the table or column picker.
columns:
- name: orderNumber
label:
en-US: Order #
subQueries:
- customer.name
- customer.primaryContact.email
showAs:
component: text
props:
value: "{{ orderNumber }} — {{ customer.name }}"
subQueries are preserved when a column is added through the view settings dialog.
Inline Cell Editing
Enable inline editing of individual cells directly in the DataGrid. Editable columns render their editor component in the cell (always visible, no click-to-toggle). On value change, the onEdit action pipeline executes — typically a GraphQL mutation followed by a notification.
Column Configuration
| Property | Type | Description |
|---|---|---|
enableEdit | boolean | Enables inline editing for this column |
editor | { component, props } | Component rendered via ComponentRender (same pattern as showAs) |
onEdit | action[] | Action array executed sequentially when the value changes |
When both showAs and editor (with enableEdit: true) are defined on a column, the editor takes priority for rendering.
Available Variables in onEdit Actions
| Variable | Description |
|---|---|
{{ changedValues }} | The new value after editing |
{{ value }} | The original value before editing |
| Row data fields | All row data fields (e.g., {{ orderId }}, {{ trackingNumber }}) |
Example: Editable DateTime Column
columns:
- name: customValues.ETA
label:
en-US: ETA
enableEdit: true
editor:
component: field
props:
type: datetime
onEdit:
- mutation:
command: |
mutation UpdateOrderMutation($input: UpdateOrderInput!) {
updateOrder(input: $input) {
order { orderId }
}
}
variables:
input:
organizationId: "{{number organizationId }}"
orderId: "{{number orderId }}"
values:
customValues:
ETA: "{{ changedValues }}"
onSuccess:
- notification:
message:
en-US: "ETA has been updated to {{ changedValues }}"
showAs:
component: text
props:
value: "{{ format customValues.ETA L }}"
showAs is still defined for non-edit contexts (e.g., export, print). When enableEdit: true, the editor renders instead.
Example: Custom Component as Editor
Any component registered in ComponentRender can be used as an editor:
columns:
- name: customValues.returnLocation
label:
en-US: Return Location
enableEdit: true
editor:
component: TRTImport/Terminals/Select
onEdit:
- mutation:
command: |
mutation UpdateOrderMutation($input: UpdateOrderInput!) {
updateOrder(input: $input) {
order { orderId }
}
}
variables:
input:
organizationId: "{{number organizationId }}"
orderId: "{{number orderId }}"
values:
customValues:
returnLocation: "{{ changedValues }}"
onSuccess:
- notification:
message:
en-US: "Return Location has been updated"
Entity Field Integration
Entity fields store enableEdit, editor, and onEdit inside props (the backend entity schema accepts known root-level properties; props is a flexible dictionary):
entities:
- name: TRTImportPickupOrder
fields:
- name: customValues.ETA
displayName:
en-US: ETA
fieldType: enhanced-rangedatetime
props:
enableEdit: true
editor:
component: field
props:
type: datetime
onEdit:
- mutation:
command: |
mutation UpdateOrderMutation($input: UpdateOrderInput!) {
updateOrder(input: $input) {
order { orderId }
}
}
variables:
input:
organizationId: "{{number organizationId }}"
orderId: "{{number orderId }}"
values:
customValues:
ETA: "{{ changedValues }}"
onSuccess:
- notification:
message:
en-US: "ETA has been updated to {{ changedValues }}"
When entity fields are loaded, enableEdit/editor/onEdit are extracted from props and promoted to column-level properties.
How It Works
- Rendering:
row.tsxcheckscolumn.enableEdit && column.editorbeforeshowAs. The editor config is passed toComponentRender. - Form context: When any column has
enableEdit: true, the DataGrid automatically wraps content with a React Hook FormFormProvider. - Value flow: Row data →
variables.value→ ControllerdefaultValue. User changes triggerhandleChange→ updates form + dispatchesonEditactions. - Action pipeline:
onEditis a standard action array (same asonClick, dots menu items, etc.). Supportsmutation,workflow,notification,navigate,dialog, and more. - Saved views: Edit properties are persisted when saving custom views and merged with original YAML definitions on load.
Pinned Views
Users can pin a view as their default. When a pinned view is set, the DataGrid loads that view automatically instead of the first view in the list. Pinned view preferences are stored per-user and persist across sessions.
Sort Reset (allowUnsort)
By default, column sorting cycles through ASC → DESC → OFF (unsorted) on successive clicks. Set allowUnsort: false at the view level or per-column via props.allowUnsort to restrict sorting to ASC ↔ DESC only (no reset to unsorted).
Editable Options (Prototype Phase)
To enable editing on the datagrid, you need to provide the editableOptions object in the options object and set the enableEdit property to true in the views object.
The editableOptions object has two properties: onRowChange and onRowDelete. Both properties are objects that have a mutation property. The mutation property is a mutation that will be executed when the row is changed or deleted.
Column editors can be defined in the columns object. The editor object is a field component that will be used to edit the column value. The editor object has a type property that defines the type of editor to use.
views:
- name: allCountries
enableEdit: true
columns:
- name: countryCode
label:
en-US: Country code
editor:
# field props for the editor
type: text
editableOptions:
onNewRow: # mutation to create a new record
mutation:
onRowChange: # mutation to update a record
mutation:
onRowDelete: # mutation to delete a record
mutation:
Filter Persistence
By default, the DataGrid persists filter state (both filter column selections and filter values) across route navigation within the same browser session. When a user applies filters on a grid, navigates away, and returns, their filters are automatically restored.
How It Works
Filter state is managed by a singleton Zustand store (gridFilterStore) that lives outside the component tree. Unlike per-page state (which resets on navigation), this store survives route changes because it is not tied to any UiContextProvider.
- Filter columns (which filter inputs are visible) are persisted when the user adds or removes filter columns, and when view defaults are first applied.
- Filter values (the actual selected/entered filter criteria) are persisted every time the user changes a filter.
- On mount, the DataGrid reads any previously persisted state for the current grid + view combination and initializes with those values.
View-Scoped Persistence
Filter state is scoped per grid + view combination using the key format {gridName}:{viewId}. This means switching between views (e.g., "All Orders" vs. "Open Orders") maintains independent filter state for each view. This prevents filters from one view bleeding into another.
Clear Filters
A clear filters button (✕ icon) appears next to the filter bar when filters are active. Clicking it:
- Clears all persisted filter state for the current grid + view
- Restores the view's original default filters (as defined in the view configuration)
- Resets the filter UI to its initial state
Behavior Details
| Scenario | Behavior |
|---|---|
| User sets filters, navigates away, returns | Filters are restored automatically (for that specific view) |
| User opens grid for the first time | View default filters apply as normal |
| View has default filter columns, user hasn't customized | Defaults are persisted on first load |
| User adds/removes filter columns | Changes are persisted immediately |
| User switches views | Each view has independent filter state |
| User clicks clear filters button | Filters reset to view defaults, persisted state cleared |
| Page refresh (full reload) | Filters reset (store is in-memory only) |
Filter persistence is in-memory only — it does not survive a full page refresh or new browser tab. It is designed specifically for SPA route navigation within a single session.
Filter Field Name Escaping
Filter field names that contain bracket notation — such as entity path expressions with sub-entity qualifiers — require special handling to prevent conflicts with React Hook Form (RHF). RHF normally interprets brackets ([, ]) as array/object access and dots (.) as nested path separators, which breaks filter field names that use these characters as part of the entity path syntax.
How It Works
The filter system automatically escapes and unescapes field names using placeholder tokens:
| Character | Scope | Placeholder | Purpose |
|---|---|---|---|
[ | Always | __LBRACKET__ | Prevents RHF array/object interpretation |
] | Always | __RBRACKET__ | Prevents RHF array/object interpretation |
. | Inside brackets only | __DOT__ | Prevents RHF nested path splitting inside qualifiers |
" | Inside brackets only | __QUOTE__ | Preserves quoted string values inside qualifiers |
Dots outside brackets are intentionally left unescaped — RHF creates nested objects from them, and the filter system flattens these back into dot-separated keys when reading values.
Example
A filter field name targeting a sub-entity with a qualifier and a quoted value:
trackingEvents[eventDefinition.eventName:"Yard Scan"].eventDate
Is escaped to:
trackingEvents__LBRACKET__eventDefinition__DOT__eventName:__QUOTE__Yard Scan__QUOTE____RBRACKET__.eventDate
When filter values are read back, the system:
- Flattens any nested objects created by RHF from unescaped dots
- Restores all placeholder tokens to their original characters
When This Applies
This escaping is transparent — you do not need to handle it manually. It applies automatically when:
- A filter
fieldNameorfilterByPropertyuses bracket notation (e.g.,orderCommodities[order.orderType:ParcelShipment].field) - The bracket content contains dots or quoted strings (e.g.,
[eventDefinition.eventName:"Yard Scan"])
If your filter field names do not contain brackets, no escaping occurs.
Selectable
To enable selection on the datagrid, you need to set the enableSelect property to Single or Multiple in the views object.
When selection is enabled, the selected rows will be available in the gridName.selectedItems property in store.
Child Views
Child views are used to display nested data in the datagrid. The childViews object is an array of objects that define the child views for the datagrid. Each object in the childViews array has a name property that defines the field name or resolver for the GraphQL query.
Styling
The DataGrid component supports conditional styling for rows and columns based on data values, with support for light and dark themes.
Row Styles
Configure row styles using the rowStyles property within each view:
views:
- name: allCountries
rowStyles:
# Conditional styles based on row data
conditions:
- condition: "{{ isEquals item.status 'active' }}"
className: "row-active"
style:
light:
backgroundColor: "#e8f5e9"
borderColor: "#4caf50"
dark:
backgroundColor: "#1b5e20"
borderColor: "#66bb6a"
- condition: "{{ moreThan item.amount 10000 }}"
className: "row-high-value"
style:
light:
backgroundColor: "#fff3e0"
color: "#e65100"
fontWeight: "bold"
dark:
backgroundColor: "#3e2723"
color: "#ffab40"
fontWeight: "bold"
- condition: "{{ eval item.daysOverdue > 30 }}"
className: "row-overdue"
style:
light:
backgroundColor: "#ffebee"
opacity: 0.9
dark:
backgroundColor: "#4a1419"
opacity: 0.9
# Use a field value as className
classNameField: "item.rowStyleClass"
# Style function for complex logic
styleFunction: "{{ eval getRowStyle(item) }}"
Column Styles
Extend column definitions with style properties:
columns:
- name: status
label:
en-US: Status
styles:
# Conditional cell styles
conditions:
- condition: "{{ isEquals value 'completed' }}"
className: "cell-success"
style:
light:
backgroundColor: "#d4edda"
color: "#155724"
borderRadius: "4px"
padding: "4px 8px"
dark:
backgroundColor: "#1e4620"
color: "#82e284"
borderRadius: "4px"
padding: "4px 8px"
- condition: "{{ isEquals value 'pending' }}"
className: "cell-warning"
style:
light:
backgroundColor: "#fff3cd"
color: "#856404"
borderRadius: "4px"
padding: "4px 8px"
dark:
backgroundColor: "#3d2f00"
color: "#ffd54f"
borderRadius: "4px"
padding: "4px 8px"
- condition: "{{ isEquals value 'failed' }}"
className: "cell-danger"
style:
light:
backgroundColor: "#f8d7da"
color: "#721c24"
borderRadius: "4px"
padding: "4px 8px"
dark:
backgroundColor: "#4a1419"
color: "#ff8a95"
borderRadius: "4px"
padding: "4px 8px"
# Column-wide style
className: "column-status"
align: "center" # left, center, right
width: "120px"
minWidth: "80px"
maxWidth: "200px"
Complete Styling Example
component: dataGrid
name: ordersGrid
props:
views:
- name: allOrders
displayName:
en-US: All Orders
# Row styling configuration
rowStyles:
conditions:
- condition: "{{ isEquals item.priority 'urgent' }}"
className: "row-urgent"
style:
light:
backgroundColor: "#ffebee"
borderLeft: "4px solid #f44336"
fontWeight: "600"
dark:
backgroundColor: "#4a1419"
borderLeft: "4px solid #ff5252"
fontWeight: "600"
- condition: "{{ moreThan item.totalAmount 50000 }}"
className: "row-high-value"
style:
light:
backgroundColor: "#e8f5e9"
borderLeft: "4px solid #4caf50"
fontWeight: "600"
dark:
backgroundColor: "#1b5e20"
borderLeft: "4px solid #66bb6a"
fontWeight: "600"
- condition: "{{ moreThan item.daysOverdue 0 }}"
className: "row-overdue"
style:
light:
backgroundColor: "#fff3e0"
borderLeft: "4px solid #ff9800"
opacity: 0.9
dark:
backgroundColor: "#3e2723"
borderLeft: "4px solid #ffab40"
opacity: 0.9
# Date comparison examples
- condition: "{{ moreThan (daysAgo item.dueDate) 0 }}"
className: "row-past-due"
style:
light:
backgroundColor: "#ffcdd2"
borderLeft: "4px solid #d32f2f"
dark:
backgroundColor: "#5d1418"
borderLeft: "4px solid #ff5252"
- condition: "{{ lessThan (daysUntil item.expiryDate) 7 }}"
className: "row-expiring-soon"
style:
light:
backgroundColor: "#fff9c4"
borderLeft: "4px solid #fbc02d"
dark:
backgroundColor: "#3d3200"
borderLeft: "4px solid #ffd54f"
- condition: "{{ moreThan (dateDiff item.actualDate item.plannedDate) 5 }}"
className: "row-delayed"
style:
light:
backgroundColor: "#ffccbc"
borderLeft: "4px solid #d84315"
dark:
backgroundColor: "#3e2723"
borderLeft: "4px solid #ff6e40"
# Column styling configuration
columns:
- name: orderNumber
label:
en-US: Order #
styles:
className: "column-order-number"
align: "center"
style:
light:
fontWeight: "600"
color: "#1976d2"
dark:
fontWeight: "600"
color: "#90caf9"
- name: status
label:
en-US: Status
styles:
align: "center"
conditions:
- condition: "{{ isEquals value 'delivered' }}"
style:
light:
backgroundColor: "#d4edda"
color: "#155724"
borderRadius: "4px"
padding: "4px 8px"
dark:
backgroundColor: "#1e4620"
color: "#82e284"
borderRadius: "4px"
padding: "4px 8px"
- condition: "{{ isEquals value 'pending' }}"
style:
light:
backgroundColor: "#fff3cd"
color: "#856404"
borderRadius: "4px"
padding: "4px 8px"
dark:
backgroundColor: "#3d2f00"
color: "#ffd54f"
borderRadius: "4px"
padding: "4px 8px"
- condition: "{{ isEquals value 'cancelled' }}"
style:
light:
backgroundColor: "#f8d7da"
color: "#721c24"
borderRadius: "4px"
padding: "4px 8px"
dark:
backgroundColor: "#4a1419"
color: "#ff8a95"
borderRadius: "4px"
padding: "4px 8px"
- name: totalAmount
label:
en-US: Total
styles:
align: "right"
width: "150px"
conditions:
- condition: "{{ moreThan value 50000 }}"
style:
light:
color: "#2e7d32"
fontWeight: "600"
dark:
color: "#66bb6a"
fontWeight: "600"
- condition: "{{ moreThan value 100000 }}"
style:
light:
color: "#1b5e20"
fontWeight: "bold"
fontSize: "1.1em"
dark:
color: "#4caf50"
fontWeight: "bold"
fontSize: "1.1em"
options:
query: orders
rootEntityName: Order
Style Properties Reference
Row Style Properties
conditions: Array of conditional styles based on row dataclassName: CSS class name to applyclassNameField: Field path to get className from row datastyleFunction: JavaScript function to compute styles dynamicallystyle: Style object withlightanddarktheme variants
Column Style Properties
conditions: Array of conditional styles based on cell valueclassName: CSS class name for the columnalign: Text alignment (left, center, right)width: Fixed column widthminWidth: Minimum column widthmaxWidth: Maximum column widthstyle: Style object withlightanddarktheme variants
Available Style Conditions
{{ isEquals item.field 'value' }}: Check equality{{ moreThan item.field 100 }}: Check if greater than{{ lessThan item.field 100 }}: Check if less than{{ isTrue item.field }}: Check boolean value{{ isNullOrEmpty item.field }}: Check null or empty{{ any item.fields }}: Check if any value is true{{ eval expression }}: Evaluate custom JavaScript expression (⚠️ Not recommended due to performance issues)
Date Comparison Functions
{{ dateDiff date1 date2 }}: Days between dates (positive if date1 > date2){{ daysBetween date1 date2 }}: Absolute days between dates{{ daysUntil futureDate }}: Days until future date from now{{ daysAgo pastDate }}: Days since past date until now{{ isDateBefore date1 date2 }}: Check if date1 is before date2{{ isDateAfter date1 date2 }}: Check if date1 is after date2{{ now() }}: Current date for comparisons
Date Styling Examples
# Style if due date has passed
condition: "{{ moreThan (daysAgo item.dueDate) 0 }}"
# Style if expiring within 7 days
condition: "{{ lessThan (daysUntil item.expiryDate) 7 }}"
# Style if delivery is delayed by more than 3 days
condition: "{{ moreThan (dateDiff item.actualDelivery item.plannedDelivery) 3 }}"
# Style if created more than 30 days ago
condition: "{{ moreThan (daysAgo item.createdDate) 30 }}"
Common CSS Properties
backgroundColor: Background colorcolor: Text colorfontSize: Font sizefontWeight: Font weight (normal, bold, 600, etc.)borderLeft,borderRight,borderTop,borderBottom: Border stylesborderRadius: Rounded cornerspadding: Cell paddingopacity: Transparency (0-1)textAlign: Text alignmenttextDecoration: Underline, strikethrough, etc.