Skip to content

Tree view

Records with a parent of the same table – projects with phases and work packages, organisational units, categories – can be shown as a tree: children are indented under their parent and can be opened and closed. The tree is a view next to the table and the cards, with the same columns, filters, search, actions and selection.

public function collection(Collection $collection): Collection
{
return $collection
->columns([
TextColumn::make('title')->sortable()->searchable(),
TextColumn::make('status')->labels(self::STATUSES)->badge(),
ImageColumn::make('owner_id')->label('Responsible')->user(),
TextColumn::make('due_on')->date(),
])
->views([Collection::VIEW_TABLE, Collection::VIEW_TREE], default: Collection::VIEW_TREE)
->tree(parent: 'parent_id', order: 'position', expand: 1, maxDepth: 4, reorderable: true);
}
Argument Default Meaning
parent – the column with the id of the parent record; null for roots
order none a number column for the order among siblings
expand 1 how many levels are open when the list is loaded: 0 only the roots, 1 the roots open with their children visible, …
maxDepth 10 the most levels a record can be moved into (at most 20)
reorderable false users who may update records move them, see Moving records

Collection::VIEW_TREE in views() without tree() throws a LogicException. The model needs parent and order in its $allowedFields when records are moved.

  • Opening and closing with the arrow in front of the first column, or with the keyboard. A closed record shows the number of its children. What a user opens and closes is sent with every request of the list, so it stays when they sort, change the page or move a record.

  • Pages count the roots: 25 per page means 25 roots with their opened children.

  • Sorting a column orders the siblings, not the whole list. Without a sorted column siblings follow the order column, then their creation.

  • Search and filters show the matching records together with their ancestors, so every match is seen in its place. Ancestors that do not match are greyed out; pages count the matches. Moving is not offered while searching or filtering.

  • Add below: when the form has a field for the parent column with fromQuery(), each row offers a button that opens the create page with the parent preset:

    BelongsTo::make('parent_id')->relationship(WorkPackageModel::class, 'title')->fromQuery('parent'),
  • Grouping does not apply to the tree; the tree is not offered in related lists, which belong to one parent already.

The tree is a treegrid: rows carry aria-level and aria-expanded.

Key Effect
↑ ↓, Home End previous, next, first, last row
→ open the record, or go to its first child
← close the record, or go to its parent
Enter open the detail page
Alt + arrow move the record up, down, in, out

With reorderable: true each row has a Move menu for users who may update the record:

Move Result
Up, Down swaps places with the sibling before or after it (only with order)
Indent becomes the last child of the sibling before it
Outdent follows its former parent, among the siblings of the parent

Without order siblings follow the sorting of the list, e.g. by name: records are only indented and outdented, which changes their parent.

Children move along. The server checks every move again:

  • the user must be allowed to update the record and, when its parent changes, the new parent (permission and policies);
  • moves that are not possible – the first record up, a root out, more levels than maxDepth – are refused (422);
  • with order, the order column of the siblings is numbered 1, 2, 3, …; only records whose place changes are saved, each with Resource::updateColumn(), so validation and the audit log apply. These moves cannot create a cycle.

Opened levels are loaded with one query per level; to count the children of closed records one more level is loaded. Records are read through the resource – tenant, scopeQuery() and the policies of single records apply, and a record whose parent the user may not see becomes a root. At most 1,000 records are loaded per request; beyond that a hint asks to close records or to search. The tree suits structures of a few thousand records, not hundreds of thousands.