Grid reference¶
Every key a grid definition may carry, as the PHP engine actually reads it.
A grid lives at view/adminhtml/grid/<id>.json inside a module. The file name is the
grid id, and the block is mounted with that id from layout XML:
<referenceContainer name="content">
<block class="Nebula\Grid\Block\Grid" name="nebula.cms_block_listing">
<arguments>
<argument name="grid_id" xsi:type="string">cms_block_listing</argument>
</arguments>
</block>
</referenceContainer>
Grids are mounted, not routed
Forms can claim an admin route from the definition (route). Grids cannot:
there is no route key for a grid, and the page's own layout XML decides where
the grid block appears. The page title also comes from layout XML, not from the
definition.
Top level¶
| Key | Type | Default | What it does |
|---|---|---|---|
id |
string | — | The definition identifier. A base file must set it; an overlay in another module must omit it. |
acl |
string | none — denied | Magento ACL resource guarding the grid, its data endpoint and its export. |
dataSource |
object | — | Which provider fetches the rows. See dataSource. |
settings |
object | {} |
Toolbar, paging, sorting, mass actions, card view. See settings. |
columns |
object (map) | {} |
Column key → column definition. See columns. |
editable |
object | — | Enables the quick-edit drawer. See Quick edit. |
massActions |
array | — | Legacy position. The normalizer moves it to settings.massActions when that key is absent, then deletes it. Declare it under settings. |
Source: code/Nebula/Grid/Block/Grid.php,
code/Nebula/Grid/Model/Definition/GridDefinitionNormalizer.php,
code/Nebula/Component/Model/Authorization/DefinitionAccessControl.php.
acl is default-deny
A definition with no acl, or an empty one, is refused and a warning naming the
definition is logged. It does not fall back to "any signed-in admin". The same
check guards nebula/grid/data and nebula/grid/export, so neither endpoint
serves rows for such a definition. The page around the grid is still guarded by
its own controller's ADMIN_RESOURCE.
The normalizer also strips these keys before anything reads the definition:
_warning, _generated, _meta, visible, urlPath, idField, urlTemplate.
Keys beginning with _ are otherwise accepted (at the top level and on a column) and
ignored.
dataSource¶
{
"dataSource": {
"provider": "grid.collection",
"config": {
"collection": "cms_block.collection"
}
}
}
| Key | Type | Default | What it does |
|---|---|---|---|
provider |
string | — | A registered provider alias, never a class name. Resolved through the data-provider registry; an unknown alias raises UnknownAliasException. |
config |
object | {} |
Forwarded to the provider verbatim, with the grid's columns injected under config['columns'] so the provider can check what is declared. |
config.collection |
string | — | Read by the generic grid.collection provider: a collection alias registered in CollectionRegistry (DI), not an FQCN. An unknown alias logs a warning and renders an empty grid. |
config.method |
string | — | An argument-less method called on the collection right after it is created, such as addTaxClass, useOnlyActual or addCustomerDetails. A method that takes an argument, such as addStoreFilter, raises an uncaught ArgumentCountError. See Data providers. |
config.scope |
object | — | Binds the grid to one parent record: {"field": "<collection field>", "param": "<request param>"}. With scope declared and the request carrying no value, the grid renders empty rather than every row. The param is carried on every server-rendered link and on the export URL. |
config.attributes |
array | — | Read only by the product provider: extra EAV attributes to select. |
Source: code/Nebula/Grid/Model/GridDataLoader.php,
code/Nebula/Grid/Model/DataProvider/CollectionProvider.php,
code/Nebula/Grid/Block/Grid.php (getScopeParams()).
config is free-form, so a custom provider may read any keys it likes; the ones above
are what the shipped providers read.
settings¶
| Key | Type | Default | What it does |
|---|---|---|---|
pageSize |
integer | 20 |
Rows per page when the request names none. |
pageSizes |
array<int> | [10, 20, 50] |
The page-size choices in the toolbar. |
defaultSort.field |
string | "" |
Column key sorted on first load. |
defaultSort.direction |
asc/desc |
asc |
Direction for that column. Anything else falls back to asc. |
emptyState.title |
string | No records found |
Heading of the "no rows" panel. |
emptyState.description |
string | Try adjusting your search or filter criteria |
Body of that panel. Worth overriding whenever emptiness has a cause the reader cannot guess. |
massActions |
false | array |
[] |
Presence of at least one action adds the row checkboxes and the mass-action bar. false disables them. See Mass actions. |
massActionIdField |
string | id |
Row field whose value each checkbox submits (review_id, indexer_id, …). |
rowUrl |
string | boolean | derived | Route template for a whole-row click, with {{field}} placeholders taken from the row. true (or absent) means "borrow the row's own opening action"; false opts out. |
rowClick |
edit | quickEdit | none |
see below | What clicking a data row does. |
addButton |
object | — | The primary "add" button. See Add button. |
buttons |
array | [] |
Extra toolbar buttons: {label, url, variant: primary\|secondary\|danger, confirm}. |
cardView.breakpoint |
sm/md/lg |
sm |
Below this width the table is swapped for stacked cards. |
cardView.primary |
string | inferred | Column key used as the card title. |
cardView.badge |
string | inferred | Column key shown as the card's trailing chip. |
cardView.secondary |
array<string> | inferred | Column keys shown as card subtitles. Everything left over goes under "More". |
Source: code/Nebula/Grid/Block/Grid.php,
code/Nebula/Grid/Model/MassActionLoader.php,
code/Nebula/Grid/Model/AddButtonUrlBuilder.php.
Clickable rows¶
A clickable row is the default, not an opt-in. getRowClickMode() resolves it like this:
- A grid with a working quick-edit drawer defaults to
quickEdit; every other grid defaults toedit. rowClickin the definition overrides that default.quickEditon a grid that is not inline-editable degrades toedit;editwith no usable row URL degrades tonone.
When rowUrl is not a string, the engine derives one from the actions column: the
first action that is a GET, carries no confirm, and whose controller action opens
a record (edit, view, new, index, or a name containing detail). Anything
else — a POST, a confirmed action, a Magento-computed actions column — yields no row
URL, and such grids state rowUrl themselves or go without.
{
"settings": {
"rowUrl": "sales/order_create/index/customer_id/{{entity_id}}",
"rowClick": "edit"
}
}
Add button¶
{
"settings": {
"addButton": {
"label": "Add New Block",
"url": "cms/block/new",
"acl": "Magento_Cms::save"
}
}
}
| Key | Type | Default | What it does |
|---|---|---|---|
label |
string | — | Button text. |
url |
string | — | Admin route for the single-action form of the button. |
options |
array | — | Turns the button into a menu: [{"label": …, "url": …}, …]. |
acl |
string | shown | Optional extra ACL. Unlike the grid's own acl this is opt-in: without it the button is always shown, because the grid ACL already authorized the page. |
{{defaultAttributeSet}} inside an option URL is replaced with the catalog_product
default attribute set id before render:
{
"addButton": {
"label": "Add Product",
"options": [
{"label": "Simple Product", "url": "catalog/product/new/set/{{defaultAttributeSet}}/type/simple"},
{"label": "Configurable Product", "url": "catalog/product/new/set/{{defaultAttributeSet}}/type/configurable"}
]
}
}
Mass actions¶
{
"settings": {
"massActions": [
{
"id": "delete",
"label": "Delete",
"url": "cms/block/massDelete",
"confirm": "Are you sure you want to delete the selected blocks?"
}
]
}
}
| Key | Type | Default | What it does |
|---|---|---|---|
id |
string | — | Identifier; also what an overlay matches on to patch or $remove the action. |
label |
string | — | Menu entry text. |
url |
string | — | Admin route the selection is posted to. |
field |
string | ids |
Name of the POST array the selected ids submit under (rendered as field[]). Reviews use reviews, Cache Management cache_types. |
namespace |
string | — | For grids whose vendor controller reads the selection through Magento's UI MassAction\Filter: the stock ui_component name. Pair it with "field": "selected". |
confirm |
string | generic | Confirmation text. Without it the modal asks a generic question built from the label. |
developerOnly |
boolean | false |
The action is dropped entirely unless Magento runs in developer mode. For operations that are risky on a live store (cache enable/disable). |
additional |
array | — | Extra inputs collected in the confirmation modal before the post. |
additional[].name |
string | — | POST field name. |
additional[].label |
string | — | Field label in the modal. |
additional[].type |
select |
— | The only widget the modal renders. |
additional[].options |
object | — | Inline value → label map. |
additional[].optionsSource |
string | — | Option-source alias resolved to a value → label map when options is absent. |
Source: code/Nebula/Grid/Model/MassActionLoader.php,
code/Nebula/Grid/view/adminhtml/templates/grid/mass-action-modal.phtml.
{
"id": "approve",
"label": "Update Status",
"url": "review/product/massUpdateStatus",
"field": "reviews",
"additional": [
{"name": "status", "label": "Status", "type": "select", "optionsSource": "magento.review.status"}
]
}
Columns¶
columns is a map, not a list: the key is the row field, and the value describes
how it renders, filters and sorts.
| Key | Type | Default | What it does |
|---|---|---|---|
label |
string | humanized key | Header text (already translated). An actions column with no label gets "Actions". |
type |
string | text |
One of the ten registered column types. An unknown type logs a warning and renders as text. |
renderer |
string | — | snippet.<id> renders through a snippet; a value containing :: (Vendor_Module::column/x.phtml) renders through that template, though the schema accepts only the snippet. form. Either one wins over type. |
template |
string | — | A template id, equivalent to the :: form of renderer. Read by the dispatcher; not accepted by the schema. |
position |
integer | 0 |
Sort order of the columns. |
sortable |
boolean | false |
Renders the header as a sort link. |
filter |
false | string |
false |
Which filter widget appears under the header. See Filters. |
filterOptions |
object | — | value → label map for a select filter, for the select column type, and for the quick-edit editor. |
filterOptionsSource |
string | — | Option-source alias resolved into filterOptions when that key is empty. Labels from a source are treated as data and are not passed through the translator. |
filterSource |
string | — | Selection-source alias a picker filter searches and resolves against. |
searchable |
boolean | false |
Includes the column in the toolbar's free-text search (one OR group of LIKE conditions). |
index |
string | column key | DB field behind the column: used for the displayed value, the filter and the sort. |
filter_index |
string | index |
A fully-qualified expression to filter on (rt.status_id), when it differs from index. |
sortIndex |
string | index |
Expression to sort on, when it differs. |
timezone |
boolean | true |
false keeps a date column out of store-timezone conversion, for display and for the filter bounds. |
format |
string | M j, Y |
PHP date format for a date column. |
href |
string | # |
Target of a link column; {{field}} placeholders are interpolated from the row. |
options |
object | — | Badge map: value → {label, class} where class is success, error, warning, info or neutral. |
actions |
array | — | Row actions: {id, label, url, method: get\|post, confirm}; {{field}} in a URL is interpolated from the row. id is the overlay merge key, and every action should carry one. |
emptyText |
string | — | Substituted into the row when the value is empty, so the column's own renderer still escapes it. |
hideable |
boolean | true |
false keeps the column out of the toolbar's column picker, so it can never be hidden. The actions column is never hideable. |
hiddenByDefault |
boolean | false |
The column starts hidden. It stays declared, filterable, and present in the picker; the user's own choice wins from then on. |
hideBelow |
sm/md/lg/xl |
— | Responsive priority: the column drops out below that breakpoint. |
editable |
true | object |
false |
Opts the column into the quick-edit drawer. See Quick edit. |
Source: code/Nebula/Grid/Model/ColumnLoader.php,
code/Nebula/Grid/Renderer/Column/ColumnDispatcher.php,
code/Nebula/Grid/Block/Grid.php,
code/Nebula/Grid/Model/DataProvider/CollectionProvider.php,
code/Nebula/Grid/view/adminhtml/templates/grid/header.phtml.
Column types¶
Ten types are registered. A third-party module adds its own by contributing to the
columnRenderers map in DI.
text — the default; escaped plain value.
select — renders the label for the stored value, using filterOptions as the map.
{"visibility": {"label": "Visibility", "type": "select", "filter": "select",
"filterOptions": {"1": "Not Visible Individually", "4": "Catalog, Search"}, "position": 20}}
badge — a coloured chip, from the options map.
{"status": {"label": "Status", "type": "badge",
"options": {"1": {"label": "Enabled", "class": "success"}, "0": {"label": "Disabled", "class": "neutral"}},
"position": 30}}
boolean — a fixed Yes/No chip pair. No options to declare.
price — formatted in the store's currency.
date — formatted with format, in the store timezone unless timezone is false.
{"created_at": {"label": "Created", "type": "date", "format": "M j, Y g:i A", "filter": "date", "position": 60}}
thumbnail — a small square image from a URL value.
link — an anchor whose text is the cell value and whose target is href.
{"file_name": {"label": "File", "type": "link", "href": "admin/export/download/file/{{file_name}}", "position": 70}}
actions — the trailing button group. Always last in practice ("position": 999).
{"actions": {"label": "", "type": "actions", "position": 999,
"actions": [
{"id": "edit", "label": "Edit", "url": "cms/block/edit/block_id/{{block_id}}"},
{"id": "delete", "label": "Delete", "url": "cms/block/delete/block_id/{{block_id}}", "method": "post",
"confirm": "Delete this block?"}
]}}
Give every row action an id
id is the merge key, not optional decoration. It must match ^[a-z][a-z0-9_]*$.
Model\Definition\Merger merges two lists of actions entry by entry on id as soon as
either list has an entry carrying one. When neither list does, the overlay replaces the
base list wholesale, so an overlay that meant to add one action deletes the shipped Edit
and Delete. id is also what {"id": "delete", "$remove": true} matches on. Nothing
renders it: ActionsRenderer reads only label, url, method and confirm, and the
schema does not require it, so a grid without ids lints and looks right until someone
overlays it.
html — the value is written unescaped.
html does not escape
Only use it for values the server itself produced. Anything a merchant or a
customer can type belongs in text.
For anything the ten types cannot express, point the column at a snippet or a template instead of a type:
{"is_active": {"label": "Status", "renderer": "snippet.status_badge", "filter": "select", "position": 40}}
See Snippets for what a snippet receives.
Filters¶
filter picks the widget under the header. Filtering is applied by the provider, and
a filter on a column the grid does not declare is ignored.
false (or absent) — no filter for this column.
filter: false hides the widget, it does not block the filter
filter controls the widget only. The filter allowlist is the columns map, so any
declared column can still be filtered by a request that names it, through
?filters[<key>]=… or a saved view. A column with no filter key is matched as
LIKE %value%, because the provider defaults the type to text when it resolves the
condition. The guarantee is only that an undeclared key is ignored. A column whose value
must never be searchable should not be declared as a column at all.
text — a text box; matches LIKE %value%.
select — a dropdown; matches exactly (eq). Options come from filterOptions, or
from filterOptionsSource when that is empty. A stored value with no matching option
is still shown, labelled "(no longer listed)", so a stale saved view does not read as
"no filter".
{"store_id": {"label": "Store View", "type": "text", "filter": "select",
"filterOptionsSource": "nebula.store.view", "position": 30}}
range — a From/To pair of number inputs. The provider reads <key>_from and
<key>_to and builds one condition.
date — a From/To pair of date inputs, read from the same _from/_to keys. The
bounds are expanded to the whole day and converted from the store timezone to UTC,
unless the column sets "timezone": false.
picker — a searchable popover for values whose option list is too large for a
<select>. The column names a selection source in filterSource, and the schema
rejects a picker filter that does not; the popover
searches nebula/selection/search?source=<alias> (paged, 100 rows per page maximum),
and the same source resolves the stored id back into a label for the closed control.
The endpoint's ACL is the source's own.
{"category_id": {"label": "Categories", "type": "text", "sortable": false,
"filter": "picker", "filterSource": "catalog_category",
"hideable": false, "emptyText": "Not assigned", "position": 45}}
Source: code/Nebula/Grid/view/adminhtml/templates/grid/filters.phtml,
code/Nebula/Grid/Model/DataProvider/CollectionProvider.php,
code/Nebula/Grid/Model/PickerLabelResolver.php,
code/Nebula/Grid/Controller/Adminhtml/Selection/Search.php.
Quick edit¶
editable turns on the quick-edit drawer: a real form, rendered by the form layer, that
saves one row without leaving the grid.
| Key | Type | Default | What it does |
|---|---|---|---|
identifierField |
string | — | Required. Row field the record is found by. It can never itself be edited. |
acl |
string | — | Required. The write resource. It is never defaulted to the grid's read acl. |
repository |
string | — | Repository FQCN used to load and save the record. |
model |
string | — | Model FQCN, for entities with no service contract. |
resource |
string | — | Resource-model FQCN. Required alongside model. |
extraFields |
object | — | Fields the drawer shows that are not columns at all. Each key carries a whole form-field definition, handed to the form layer untouched, so any field type and any field key the form reference accepts works here. Only position is defaulted, and type is required. |
Source: code/Nebula/Grid/Block/Grid.php (isInlineEditable()),
code/Nebula/Grid/Model/InlineEdit/RowEditor.php,
code/Nebula/Grid/Model/InlineEdit/QuickEditDefinitionBuilder.php.
The drawer needs either repository, or model and resource. Missing either
route — or identifierField, or acl — makes the grid non-editable, and the reason is
logged.
Which columns may be edited:
- a column with
"editable": false(the default) never; - a column that can produce an option map (
boolean, or any column withfilterOptions/filterOptionsSource, or its owneditable.options) renders as a select — whatever its display type, so a status rendered through a badge snippet is still editable; - otherwise only a column of type
text; - never the
identifierField.
Per-column edit config:
| Key | Type | Default | What it does |
|---|---|---|---|
required |
boolean | false |
Enforced by the server on save. |
maxLength |
integer | — | Enforced by the server on save. |
options |
object | filterOptions |
Overrides the option map for the editor. |
optionsSource |
string | — | Read only when the server validates a posted value. The drawer builds its select from editable.options or the column's filterOptions, so a column whose only map is optionsSource is not editable. Declare filterOptionsSource on the column, or editable.options inline. |
{
"editable": {
"repository": "Magento\\Cms\\Api\\BlockRepositoryInterface",
"identifierField": "block_id",
"acl": "Magento_Cms::save",
"extraFields": {
"content": {"type": "wysiwyg", "label": "Content"}
}
},
"columns": {
"title": {
"label": "Title", "type": "text", "filter": "text", "position": 20,
"editable": {"required": true, "maxLength": 255}
}
}
}
A quick-edit save is a partial merge: only the posted keys change, every other column of the record is left alone. Posted keys that no editable column or extra field accounts for are dropped and logged as tampering. The record is read back from the database after the save, and the grid re-requests the page, so what it shows afterwards is what was stored.
The server validates less than the drawer shows
On an extra field the server keeps only required and maxLength, which is all
RowEditor::quickEditFields() reads out of the definition. Any other rule an extra field
declares is enforced in the browser and nowhere else. The per-column edit config above is
the same story, and is the whole server-side contract.
The two endpoints¶
The drawer fetches its body with a GET to nebula/gridEdit/panel, carrying grid_id and
row_id, where row_id is the row's identifierField value. It answers
{"success": true, "html": "…"}, the fields rendered by the real form field renderers, or
{"success": false, "message": "…"} with one of Malformed request., Nothing on this row is
editable. or Could not open the editor. Check the admin error log.
Saving posts to nebula/gridEdit/save, form-encoded, not JSON, because Magento's form-key
validator reads form_key as a request param and cannot see it inside a JSON body. The post
carries form_key, grid_id, row_id and data, where data is a JSON string of only the
fields that changed since the drawer opened. On success the answer is
{"success": true, "values": {…}}. Those values are the editable columns and the extra
fields, read back from the database after the save. The grid does not repaint from them:
it re-requests the page from the server, so the row comes back through the same renderers
as the first paint. On failure it is
{"success": false, "message": "…", "errors": {"<field>": "<message>"}}, and the drawer stays
open with the typing intact.
Both endpoints demand both resources
_isAllowed() on each controller checks the grid's read acl and editable.acl, not
either one. An admin who holds the read resource but not the write one never sees the
quick-edit control, because isInlineEditable() checks the write resource too; the control
is absent, not disabled. A request aimed straight at either route is refused by Magento, and
because that refusal is not the JSON shape the client expects, the drawer falls back to its
own wording: Could not open the editor. on the panel fetch and
Something went wrong. on the save.
Source: code/Nebula/Grid/Controller/Adminhtml/GridEdit/Panel.php,
code/Nebula/Grid/Controller/Adminhtml/GridEdit/Save.php.
Saved views¶
Saved views are not JSON-driven — there is no definition key for them, and nothing to declare. Every grid gets them.
An admin filters, sorts and sizes the grid, then saves that state under a name. Views
are per user and per grid, stored in the database, and offered in the toolbar's Views
menu, where they can be renamed (through Nebula's own prompt dialog, not the browser's),
deleted, or marked as the default. A default view seeds the filters, sort and page size
of a fresh navigation to the grid only: as soon as the admin touches a filter, a sort,
the page size, or clears everything, the request carries nvViewApplied=1 and the
default view stops re-seeding — otherwise an admin who cleared every filter could never
see an unfiltered grid.
The three endpoints¶
The Views menu drives three JSON POST endpoints, wired from the block as getSaveViewUrl(),
getDeleteViewUrl() and getSetDefaultViewUrl().
| Route | Params | Answer |
|---|---|---|
nebula/gridView/save |
grid_id, name, config, optional view_id |
{"ok": true, "view": {…}, "views": […]} |
nebula/gridView/delete |
grid_id, view_id |
{"ok": true, "views": […]} |
nebula/gridView/setDefault |
grid_id, view_id |
{"ok": true, "views": […]} |
name is required, trimmed, and capped at 255 characters. config is the view state as a JSON
string. With view_id, save renames or updates that view; without it, it creates one.
setDefault clears any other default for that admin and grid, then sets this one, in a single
transaction. Each view object is {view_id, name, is_default, config}. A failure answers
{"ok": false, "error": "…"} rather than an HTTP error status.
config is normalized before it is stored. Only filters, sort, pageSize and
hiddenColumns survive. sort keeps field and direction, and a direction that is not
exactly desc becomes asc. pageSize must be a positive integer, and hiddenColumns a list
of strings. Every other key is dropped, and input that does not decode becomes {}.
The grid's own acl does not guard these
None of the three declares an ADMIN_RESOURCE, so all three sit on the base
Magento_Backend::admin resource every admin holds. Saving your own view is a personal
preference, not an ACL-gated action. Ownership is what protects the data: every call is
scoped to the signed-in admin user, and a view_id belonging to another admin raises the
same generic View not found. as one that does not exist, so a view id cannot be used to
probe for other admins' views.
Source: code/Nebula/Grid/Controller/Adminhtml/GridView/Save.php,
code/Nebula/Grid/Controller/Adminhtml/GridView/Delete.php,
code/Nebula/Grid/Controller/Adminhtml/GridView/SetDefault.php,
code/Nebula/Grid/Model/GridViewManager.php.
Export¶
Every grid exports; there is no key to enable it. The toolbar renders one Export link
and it always carries format=csv. XML is supported by the route but has no control in
the UI: request nebula/grid/export?grid_id=<id>&format=xml yourself. A
settings.buttons entry can point at that route, but its URL is static, so it exports
the whole grid rather than what is on screen.
- The current search, filters and sort are carried on the export URL, so what is exported is what is on screen. A record-scoped grid carries its scope param too.
- The
actionscolumn is excluded. Every other column is exported in definition order, with its label as the header. - CSV cells beginning with
=,+,-,@, tab or carriage return are prefixed with an apostrophe, so a spreadsheet cannot execute an exported value as a formula. - The export is bounded. Interactive pages are capped at 200 rows; an export raises the ceiling to 100000 rows and logs a warning when the result set is larger, so a truncated export is visible in the log. The picker search endpoint is capped at 100 rows per page.
Source: code/Nebula/Grid/Controller/Adminhtml/Grid/Export.php,
code/Nebula/Grid/Model/DataProvider/CollectionProvider.php,
code/Nebula/Grid/Controller/Adminhtml/Selection/Search.php.
The request contract¶
A provider is handed the definition's dataSource.config (with columns injected) and
this parameter map:
| Param | Type | Source | Notes |
|---|---|---|---|
page |
integer | ?page |
At least 1. |
pageSize |
integer | ?pageSize |
Else the default view's, else settings.pageSize, else 20. |
sort |
string | ?sort |
Else the default view's, else settings.defaultSort.field. |
sortDir |
string | ?dir |
asc or desc; anything else becomes asc. |
filters |
object | ?filters[<key>] |
Else the default view's filters, else empty. Range and date filters arrive as <key>_from / <key>_to. |
search |
string | ?search |
The toolbar's free-text box; matched against searchable columns. |
export |
boolean | export route | Set only by nebula/grid/export; raises the page-size ceiling. |
The grid also carries nvViewApplied=1 on every link it renders once the current
request is an explicit interaction (see Saved views), and carries the
dataSource.config.scope param by name.
Source: code/Nebula/Grid/Model/GridDataLoader.php,
code/Nebula/Grid/Block/Grid.php.
The three guarantees¶
GridRequestApplier is the one place a request is allowed to touch a collection, and a
custom provider should use it rather than writing its own loop. It states three rules:
- A request can only sort by a column the grid declares. The sort key is checked against the declared columns before any DB identifier is derived from it.
- A request can only filter on a column the grid declares. Same check, same reason.
declaredFilters()offers the guard on its own, for a provider that must keep its own filter loop (EAV attributes, a filter no collection can express) — iterate that instead of the raw request. Range keys survive as themselves, soprice_fromis kept for the columnprice. - A page is always bounded. A missing, zero or negative
pageSizemeans "use the default" (20), never "unbounded"; the ceiling is 200 interactive, 100000 on export.
Source: code/Nebula/Grid/Model/DataProvider/GridRequestApplier.php.
A full grid¶
Adapted from the shipped CMS block listing: a generic collection provider, an add button, a picker-free set of filters, a badge rendered through a snippet, a date column, an actions column, and inline editing with one extra field.
{
"id": "cms_block_listing",
"acl": "Magento_Cms::block",
"dataSource": {
"provider": "grid.collection",
"config": {
"collection": "cms_block.collection"
}
},
"settings": {
"pageSize": 20,
"pageSizes": [10, 20, 50],
"defaultSort": {"field": "block_id", "direction": "desc"},
"addButton": {"label": "Add New Block", "url": "cms/block/new"},
"massActions": [
{
"id": "delete",
"label": "Delete",
"url": "cms/block/massDelete",
"confirm": "Are you sure you want to delete the selected blocks?"
}
],
"massActionIdField": "block_id",
"cardView": {"breakpoint": "sm", "primary": "title", "badge": "is_active"}
},
"editable": {
"repository": "Magento\\Cms\\Api\\BlockRepositoryInterface",
"identifierField": "block_id",
"acl": "Magento_Cms::save",
"extraFields": {
"content": {"type": "wysiwyg", "label": "Content"}
}
},
"columns": {
"block_id": {
"label": "ID",
"type": "text",
"sortable": true,
"filter": false,
"hideBelow": "md",
"position": 10
},
"title": {
"label": "Title",
"type": "text",
"sortable": true,
"filter": "text",
"searchable": true,
"position": 20,
"editable": {"required": true, "maxLength": 255}
},
"identifier": {
"label": "Identifier",
"type": "text",
"sortable": true,
"filter": "text",
"searchable": true,
"position": 30,
"editable": {"required": true, "maxLength": 255}
},
"is_active": {
"label": "Status",
"renderer": "snippet.status_badge",
"sortable": true,
"filter": "select",
"filterOptions": {"1": "Enabled", "0": "Disabled"},
"position": 40
},
"creation_time": {
"label": "Created",
"type": "date",
"sortable": true,
"filter": "date",
"hiddenByDefault": true,
"position": 50
},
"update_time": {
"label": "Modified",
"type": "date",
"sortable": true,
"filter": "date",
"position": 60
},
"actions": {
"label": "",
"type": "actions",
"hideable": false,
"position": 999,
"actions": [
{"id": "edit", "label": "Edit", "url": "cms/block/edit/block_id/{{block_id}}"}
]
}
}
}
Accepted by the schema but not read¶
bin/magento nebula:lint will pass these, and the engine will ignore them. Do not
build on them.
dataSource.config.identifierParam— no grid provider reads it. (The quick-edit drawer sets its own internally.)settings.cardView.columns— the card layout is driven byprimary,badgeandsecondaryonly.- Mass-action
additional[].typeaccepts onlyselect, which is also the only widget the modal renders.
Read by the engine, rejected by the schema¶
These work at runtime but fail bin/magento nebula:lint, because every schema is
strict (additionalProperties: false). Treat them as unsupported.
- top-level
massActions— the normalizer still relocates it, but declare it undersettings.massActions; - column
template, and arenderernaming a template rather than a snippet: the schema'srendereraccepts onlysnippet.<id>, so a column that needs a template of its own lints only when the template is wrapped in a snippet; - column
filterOptionsTranslate: the filter template honoursfalseto keep an inlinefilterOptionsmap out of the translator, andColumnLoadersets it automatically on any map it resolves fromfilterOptionsSource. The grid schema has no such property, so declaring it by hand does not lint; $removeon a column, a row action or a mass action — the merger honours it, the grid schema has no such property, so an overlay that removes something does not lint;settings.cardView.detail— neither read nor accepted.
Column keys beginning with _ (_magentoColumnClass, _actionsKey, _indexField)
are accepted by the schema and read by the actions renderer. They exist for the
ui_component bridge; a hand-written grid should not use them.