Skip to content

Grouping

Records of a list can be grouped by a field. Each group has a header with its value and the number of its records, and can be collapsed. It works in the table and in the cards.

public function collection(Collection $collection): Collection
{
return $collection
->columns([
TextColumn::make('title')->sortable()->searchable(),
TextColumn::make('stage')->labels(self::STAGES)->sortable(),
TextColumn::make('owner_id')->user(),
])
->groups(['stage', 'owner_id'], default: 'stage');
}

You decide which fields can be used and with which one the list starts (default, optional). Users choose from these fields in the toolbar or switch the grouping off. Other fields are rejected by the server.

  • Change the grouping: it applies until they leave the list. It is not remembered like the column choice.
  • Save it in a view: a saved view keeps the grouping, also “no grouping”. A view saved before the collection had groups starts with the default.
  • Collapse groups: remembered in the browser until the tab is closed, also when the results are reloaded. Only a hash of the value is stored.

Records in collapsed groups cannot be selected: “select all” skips them, and collapsing a group removes its records from the selection. Bulk actions never apply to records the user cannot see.

Presets set the grouping with group():

Preset::make('by-owner', 'By owner')->group('owner_id'),
Preset::make('flat', 'All deals')->group(null),

Without group() a preset uses the default of the collection.

  • The column must be a field of the table: columns with state() or queryColumn() and CountColumn are rejected with an exception.
  • The header shows the value like the column does (labels, the title of a relationship, dates), never an internal id. Empty values form their own group.
  • Groups are sorted by the stored value, not by the label: relationship() columns by key, labels() by the key of the label. Within a group the chosen sorting applies. Sorted by the group field itself, its direction applies.
  • The number counts all records of the group with the current search and filters, also those on other pages. Like the total of the list it does not know the record level rules; rows that should never be listed belong into scopeQuery().
  • A group can continue on the next page, its header is repeated there.
  • A page runs one additional count query per group shown. Group by fields with few values: grouping by an almost unique field is possible, but costs one query per record.
  • Exports have the order of the list. The API ignores the grouping.