Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Filament Tree View Laravel Package

openplain/filament-tree-view

A drag-and-drop tree view component for Filament resources that makes managing hierarchical data feel native, with depth control, auto or batch save modes, custom fields, actions, dark mode, and accessible keyboard navigation.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and smoke-test in one sitting (15 minutes):

composer require openplain/filament-tree-view
php artisan filament:assets

Then, in order:

  1. Migration — the package needs two columns it doesn't create for you:
$table->foreignId('parent_id')->nullable()->constrained('categories');
$table->integer('order')->default(0);

If your table already exists, write a follow-up migration. For UUID keys, use $table->uuid('parent_id')->nullable()->index() instead — foreignId won't work.

  1. Model trait — use Openplain\FilamentTreeView\Concerns\HasTreeStructure;. This is the piece most people underestimate: it wires up staudenmeir/laravel-adjacency-list (so you get descendants(), ancestors(), roots() for free) and adds cascade-delete behaviour. Read the trait source before adopting it on a table with sensitive data.

  2. Resource tree() method — a sibling to form() and table(). Fields are mandatory; there is no sensible default rendering.

public static function tree(Tree $tree): Tree
{
    return $tree->fields([TextField::make('name')]);
}
  1. Page — one class, no body:
class TreeCategories extends TreePage
{
    protected static string $resource = CategoryResource::class;
}

Point your index route at it (or add it as an extra page and keep table() around during the transition).

First use case to pick: a taxonomy/tag/category/FAQ/menu editor owned by 1–3 admins. That's where the package earns its keep today. Avoid making it the only interface to a large shared dataset until you've validated the save semantics with real users.

Where to look first when something's off: vendor/openplain/filament-tree-view/src/ — the Tree, Field, and Page classes are small enough to read end-to-end in one pass, and you will need to, because the README is the only documentation.

Implementation Patterns

Decide save mode on day one, not later. The default is manual save (Save/Cancel buttons). For a tree your users will edit in bursts, ->autoSave() removes a whole class of "I dragged it and nothing happened" support tickets. For anything where a mis-drop is expensive, keep manual and make the save button prominent.

// small, low-risk taxonomy
return $tree->fields([...])->autoSave();

// structural / destructive hierarchies
return $tree->fields([...]); // manual save is the default

TreePage vs TreeRelationPage. Use TreeRelationPage when the hierarchy belongs to a parent record and you want it inside the parent's sub-navigation. It needs three statics — $resource, $relationship, $relatedResource — and the tree config is read from the related resource, not the parent. That asymmetry trips people up; put the tree() method on the related resource.

Compose node rendering as a compact row, not a card. Fields are horizontal; two or three max reads well:

->fields([
    TextField::make('name')->weight(FontWeight::Medium)->dimWhenInactive(),
    TextField::make('slug')->color('gray')->size('sm')->limit(30),
    IconField::make('is_active')->alignEnd(),
])

dimWhenInactive() defaults to an is_active column — if your column is named differently (status, published), pass it explicitly or use dimWhen('status', 'draft'). Silent no-ops here are a common "why isn't it greyed out" moment.

Actions: prefer URL-based EditAction for full records, modal actions for quick edits. Modal editing keeps the user in the tree, which is the whole point of the component.

->recordActions([
    EditAction::make()->url(fn ($record) => static::getUrl('edit', ['record' => $record])),
    Action::make('rename')
        ->form([TextInput::make('name')->required()])
        ->fillForm(fn ($record) => ['name' => $record->name])
        ->action(fn ($record, array $data) => $record->update($data)),
    DeleteAction::make()->modalDescription(fn ($record) =>
        ($n = $record->descendants()->count())
            ? "{$n} descendants will also be deleted."
            : 'Delete this item?'
    ),
])

Reuse the model helpers elsewhere. The trait is arguably more valuable than the UI. Build breadcrumbs, sitemaps, and menu caches with $node->ancestors(), and use ->maxDepth() on the tree purely as a UI guard rather than a data invariant.

Scope queries per tenant/team. If your model is tenant-scoped, use the tree's query customization to constrain roots, otherwise cross-tenant drag targets become possible. Verify by creating two tenants and dragging between them in staging — drag-and-drop is exactly the kind of thing that bypasses your usual policy assumptions.

Large trees: ->collapsed() plus a lower ->maxDepth() is the pragmatic combination. ->reorderable(false) gives you a read-only visualisation for a "view hierarchy" page without a second component.

Testing: assert on the database, not the DOM. Do a Livewire test that calls the reorder/save endpoint (or just invoke the underlying update), then assert parent_id and order on the affected rows, plus that no cycles exist. Add one test that drags a parent under its own descendant and asserts it is rejected — this is the safety property you're relying on the package to enforce.

Gotchas and Tips

  • No search, filters, or pagination. This replaces the Table's presentation, not its query surface. If your admins currently filter by status to find a record, they lose that. Keep a Table page alongside, or accept a worse workflow.
  • order defaults to 0 for every new row. Existing data and freshly created records will all tie at 0 and fall back to whatever secondary sort the query uses. Backfill order (per parent) before the first render, or ordering looks random and drag results look arbitrary.
  • maxDepth is a drag-time guard, not a data constraint. Seeders, imports, API endpoints, and tinker can all create a depth-11 node. Enforce the same limit in a model observer or form validation if depth actually matters downstream.
  • Circular-reference prevention lives in the UI layer. Direct $node->parent_id = $descendant->id writes will happily corrupt the tree. Add a saving hook that checks $node->ancestors()->contains($newParent) if the model is writable outside Filament.
  • Cascade delete is a trait feature and a footgun. Deleting a category removes its whole subtree. That's usually right for taxonomies and usually wrong for anything with references from other tables. Check FK onDelete behaviour and whether you're using SoftDeletes — soft-deleted children of a hard-deleted parent is a nasty state.
  • Manual save silently discards navigation. If a user drags and then clicks a nav item, changes are lost with no prompt unless you add one. Another argument for autoSave() on low-risk trees.
  • Re-run php artisan filament:assets after every package and Filament upgrade. Stale published JS is the number one cause of "drag and drop just stopped working" — check public/ before you debug anything else.
  • Filament 4 vs 5 type hints differ (Filament\Schemas\Schema vs the older Form namespace). Copy-pasting the README into a mismatched install yields confusing signature errors. Match the snippet to your installed major version.
  • Authorisation is per-action. DeleteAction and your custom actions respect policies only if the policy exists; the tree itself does not add a gate around reordering. If "who may restructure the hierarchy" differs from "who may edit records," wrap the page with a canAccess() or disable reorderable() for those roles.
  • Eager-load relationships used by fields. formatStateUsing and closures that touch $record->owner->name inside a node renderer will N+1 across the whole tree. Constrain the tree query with with().
  • Bus factor. 41 stars, zero dependents, one maintainer, and a README-only docs story. Pin the exact version in composer.json, read the changelog before bumping, and budget a day to vendor the source if this becomes load-bearing. For a throwaway internal tool, fine; for a core admin surface, treat it as code you own.
  • Debugging drag-and-drop: if a drop does nothing, check (a) published assets, (b) whether the drop crosses maxDepth, (c) the browser console for Livewire request errors, (d) the actual parent_id/order values in the DB after a successful-looking drag. That last check distinguishes "UI didn't persist" from "persisted, then re-render sorted it back."
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route