Skip to content

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:

  1. A grid with a working quick-edit drawer defaults to quickEdit; every other grid defaults to edit.
  2. rowClick in the definition overrides that default.
  3. quickEdit on a grid that is not inline-editable degrades to edit; edit with no usable row URL degrades to none.

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.

{"sku": {"label": "SKU", "type": "text", "sortable": true, "filter": "text", "position": 10}}

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.

{"is_active": {"label": "Active", "type": "boolean", "position": 40}}

price — formatted in the store's currency.

{"price": {"label": "Price", "type": "price", "sortable": true, "filter": "range", "position": 50}}

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.

{"thumbnail": {"label": "", "type": "thumbnail", "position": 5}}

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.

{"rendered": {"label": "Preview", "type": "html", "position": 80}}

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.

{"id": {"label": "ID", "type": "text", "filter": false, "position": 10}}

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%.

{"title": {"label": "Title", "type": "text", "filter": "text", "searchable": true, "position": 20}}

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.

{"price": {"label": "Price", "type": "price", "filter": "range", "position": 40}}

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.

{"created_at": {"label": "Created", "type": "date", "filter": "date", "position": 50}}

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 with filterOptions / filterOptionsSource, or its own editable.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 actions column 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:

  1. 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.
  2. 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, so price_from is kept for the column price.
  3. A page is always bounded. A missing, zero or negative pageSize means "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 by primary, badge and secondary only.
  • Mass-action additional[].type accepts only select, 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 under settings.massActions;
  • column template, and a renderer naming a template rather than a snippet: the schema's renderer accepts only snippet.<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 honours false to keep an inline filterOptions map out of the translator, and ColumnLoader sets it automatically on any map it resolves from filterOptionsSource. The grid schema has no such property, so declaring it by hand does not lint;
  • $remove on 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.