← Back to UI Components ← Back to Table of Contents

9.1 Dataverse Grid system

The Dataverse Grid is KMP’s standard server-driven list component. It combines CakePHP authorization and queries, declarative column metadata, saved or system views, tenant-scoped filter option caching, nested Turbo Frames, and the grid-view Stimulus controller.

Use it for searchable or filterable entity lists. Use a normal table for small, static data that does not need pagination, saved views, or partial refresh.

Source map

Concern Owning source
Query, filters, views, sorting, pagination, export app/src/Controller/DataverseGridTrait.php
Column contract app/src/KMP/GridColumns/BaseGridColumns.php and concrete *GridColumns.php classes
Saved-view normalization app/src/KMP/GridViewConfig.php and app/src/Services/GridViewService.php
Conditional association/query planning app/src/KMP/DataverseGridQueryContext.php
Client state and frame navigation app/assets/js/controllers/grid-view-controller.js
Lazy shell and frame responses app/templates/element/dv_grid*.php
Table, rows, actions, toolbar app/templates/element/dataverse_table*.php, grid_view_toolbar.php, and grid_view_tabs.php
CSV output app/src/Services/CsvExportService.php
Stable row targets app/src/KMP/GridRowDomId.php

Grid views and application records live in the active tenant database. Persisted filter options are keyed with TenantAwareCache::tenantScopedKey(); do not bypass that boundary with an unscoped cache key.

Request flow

  1. The index template renders dv_grid with a unique gridKey, outer frameId, and data URL.
  2. That element lazy-loads the outer frame. The controller authorizes and scopes a base query before calling processDataverseGrid().
  3. dv_grid_content renders the toolbar and inner {frameId}-table frame.
  4. Search, filter, sort, view, column, and pagination changes reload only the table frame. The response contains JSON grid state plus the table.
  5. grid-view mirrors relevant query parameters to browser history and handles popstate.

The outer frame owns toolbar metadata; the table response deliberately uses a reduced metadata mode for smaller follow-up responses.

Implement a grid

1. Define columns

Create a class under core or the owning plugin’s src/KMP/GridColumns directory and extend BaseGridColumns.

<?php
declare(strict_types=1);

namespace App\KMP\GridColumns;

final class ExampleGridColumns extends BaseGridColumns
{
    public static function getColumns(): array
    {
        return [
            'name' => [
                'key' => 'name',
                'label' => 'Name',
                'type' => 'string',
                'sortable' => true,
                'searchable' => true,
                'filterable' => true,
                'defaultVisible' => true,
                'required' => true,
                'clickAction' => 'navigate:/examples/view/:id',
                'clickActionPermission' => 'view',
            ],
            'branch_id' => [
                'key' => 'branch_id',
                'label' => 'Branch',
                'type' => 'relation',
                'sortable' => true,
                'filterable' => true,
                'filterType' => 'dropdown',
                'filterOptionsSource' => 'Branches',
                'renderField' => 'branch.name',
                'queryField' => 'Branches.name',
                'filterQueryField' => 'Examples.branch_id',
                'defaultVisible' => true,
                'requiresContain' => ['Branches'],
            ],
        ];
    }
}

Common metadata:

Key Meaning
key, label, type Stable state key, heading, and renderer type
defaultVisible, required Initial visibility and whether the column can be hidden
sortable, searchable, filterable Enable server-side operations
queryField Qualified SQL field for relation sorting/selecting
customSortHandler Static class/method handler for expression-based sorting
renderField Entity/array path used for display and data-mode export
filterQueryField Qualified field used by automatic filtering
filterType Typically dropdown, is-populated, or date-range
filterOptions, filterOptionsSource Static or dynamically loaded dropdown values
lockedFilter, showInFilterMenu Protect a contextual filter or hide it from the add-filter menu
skipAutoFilter, customFilterHandler Opt into grid-specific filtering logic
clickAction, clickActionPermission Cell navigation/modal behavior and authorization requirement
exportable, exportOnly, exportValue CSV inclusion and custom value extraction
requiresContain, requiresFields, requiresComputed Query/enrichment dependencies for active columns

Column keys are the public grid-state contract. Do not rename one without considering saved views and bookmarked query strings.

2. Add the data action

The controller uses DataverseGridTrait. Authorize the model, apply the index scope to the base query, and then process the grid.

public function gridData(CsvExportService $csvExportService)
{
    $baseQuery = $this->Examples->find()->contain(['Branches']);
    $baseQuery = $this->Authorization->applyScope($baseQuery, 'index');

    $result = $this->processDataverseGrid([
        'gridKey' => 'Examples.index.main',
        'gridColumnsClass' => ExampleGridColumns::class,
        'baseQuery' => $baseQuery,
        'tableName' => 'Examples',
        'defaultSort' => ['Examples.name' => 'asc'],
        'defaultPageSize' => 25,
        'canExportCsv' => true,
    ]);

    if (!empty($result['isCsvExport'])) {
        return $this->handleCsvExport(
            $result,
            $csvExportService,
            'examples',
        );
    }

    $this->renderDataverseGridResponse(
        $result,
        'examples-grid',
        'examples',
    );
}

renderDataverseGridResponse() selects dv_grid_content or dv_grid_table from the Turbo-Frame request header. Some older controllers set the same view variables and templates manually; new work should use the helper when its response shape fits.

For expensive associations, call resolveDataverseGridQueryContext() before building the query and load only dependencies required by visible, searched, filtered, or sorted columns. See MembersController::gridData() for a current example.

3. Render the shell

<?= $this->element('dv_grid', [
    'gridKey' => 'Examples.index.main',
    'frameId' => 'examples-grid',
    'dataUrl' => $this->Url->build(['action' => 'gridData']),
]) ?>

The frameId must be unique on the page. Embedded grids need a stable context-specific ID and data route. Preserve the convention {frameId} / {frameId}-table.

Processing options

processDataverseGrid() requires gridKey, gridColumnsClass, baseQuery, tableName, and defaultSort. Important optional keys are:

Option Default / contract
defaultPageSize 25
disablePagination false; only use with a query already bounded by date or another hard limit
systemViews, defaultSystemView, queryCallback Enable code-defined views
showAllTab True without system views; false with system views
canAddViews True without system views; false with system views
canFilter, canExportCsv, showFilterPills, showViewTabs, enableColumnPicker UI/processing feature switches
lockedFilters Filter keys a user cannot remove or clear
enableBulkSelection, bulkSelection, bulkActions Accessible row selection and bulk actions
bulkSelectionDataFields, bulkSelectionDisabledField, bulkSelectionHideDisabledControl Bulk-action row data and disabled-row behavior
metadataMode Normally inferred: full for outer responses and table for inner-frame responses

canFilter: false rejects user-provided filters but still applies filters defined by the selected system view. Authorization scope is never a grid option: apply it to baseQuery before processing.

Views, filters, and sorting

Saved views are member-specific records keyed by gridKey. System views come from getSystemViews() on the column class. Both can define filters, visible columns, sort, search, and page size. View selection and user preferences are resolved on the server; do not trust client JSON as authorization or scope.

Custom renderers using dv_custom can reuse grid_view_tabs.php when they enable showViewTabs and canAddViews. Keep the grid-state JSON inside the {frameId}-table frame so the shared controller can refresh the tabs. Transient navigation such as a calendar’s year, month, week, or display mode belongs in sticky query state rather than a saved view, so selecting a view does not reopen an outdated date range.

Calendar mode links live in the static toolbar. The calendar controller synchronizes their selected styling, visible checkmark, and aria-current from the successfully rendered inner frame on connection. Event-title and Details links leave the calendar frame with data-turbo-frame="_top" to open a full gathering page.

GridViewConfig accepts flat filters and nested expression trees. Supported operators are eq, neq, gt, gte, lt, lte, contains, startsWith, endsWith, in, notIn, isNull, isNotNull, and dateRange. Sort directions are asc and desc. Normalized page sizes are between 10 and 100.

Use column lockedFilter metadata or the processing lockedFilters option for embedded context such as a member ID. A locked UI filter is not an authorization boundary; the base query and policy scope must enforce the same constraint.

Dynamic dropdown sources can be:

The trait caches these per request and in the tenant-scoped grid_filter_options cache.

CSV export

?export=csv returns an export result instead of paginated data. handleCsvExport() authorizes the table’s export action and supports:

Value resolution prefers exportValue, then renderField, then the direct column value. Export only data the current identity is authorized to view; hide PII columns before calling the trait, as MembersGridColumns::setIncludePii() does.

Row actions and Turbo Streams

Rows receive stable IDs derived from the table frame: examples-grid-table becomes examples-grid-row-{id}. TurboResponseTrait can replace or remove one row after a modal save and can fall back to replacing the table frame. See Hotwire navigation for the response recipe.

Accessibility and security checklist

Verification

For PHP behavior, run the focused controller/service test and PHPCS on changed PHP files. For grid-view or element behavior, run:

cd app
npm run test:js
npm run dev

Use the relevant Playwright journey for nested frames, filters/history, modal streams, bulk actions, or keyboard/focus behavior.

Further reference