openplain/filament-tree-view
Drag-and-drop tree view for Filament resources to manage hierarchical data. Built on Laravel Adjacency List and Atlassian Pragmatic Drag & Drop. Supports depth limits, auto or batch save, custom fields, actions, dark mode, accessibility, and safe moves.
Installation:
composer require openplain/filament-tree-view
php artisan filament:assets
Database Setup:
Add parent_id (nullable foreign key) and order columns to your migration:
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->foreignId('parent_id')->nullable()->constrained();
$table->integer('order')->default(0);
$table->timestamps();
});
Model Setup: Add the trait to your model:
use Openplain\FilamentTreeView\Concerns\HasTreeStructure;
class Category extends Model
{
use HasTreeStructure;
// ...
}
Resource Integration:
Add tree() method to your Filament resource:
public static function tree(Tree $tree): Tree
{
return $tree->fields([
TextField::make('name'),
]);
}
Page Creation:
Create a page extending TreePage:
class TreeCategories extends TreePage
{
protected static string $resource = CategoryResource::class;
}
Replace a standard table() with tree() for hierarchical data management. Example:
public static function tree(Tree $tree): Tree
{
return $tree
->fields([
TextField::make('title'),
IconField::make('is_published'),
])
->recordActions([
EditAction::make(),
DeleteAction::make(),
]);
}
Configuration:
TextField/IconField (required)maxDepth(), collapsed(), autoSave()recordActions()Page Integration:
TreePage for standalone treesTreeRelationPage for related records (e.g., /{parent}/children)Data Management:
parent_id and order columnsautoSave()) or manual save modes available// In parent resource
public static function getPages(): array
{
return [
'children' => Pages\ManageChildren::route('/{record}/children'),
];
}
// In child resource
class ManageChildren extends TreeRelationPage
{
protected static string $relationship = 'children';
}
TextField::make('name')
->weight(FontWeight::Bold)
->color('primary')
->dimWhenInactive('is_active')
// Bulk actions
->bulkActions([
Action::make('publish')
->action(fn (Collection $records) => $records->update(['is_published' => true])),
]),
// Conditional actions
->recordActions(fn (Category $record): array => [
DeleteAction::make()->visible(fn () => $record->children()->count() === 0),
]),
// Filter roots only
->query(fn (Builder $query) => $query->whereNull('parent_id')),
// Custom ordering
->query(fn (Builder $query) => $query->orderBy('name')),
Missing Fields:
Fields are required for TreeView->fields([])Circular References:
maxDepth() to prevent deep nesting or validate moves in beforeSave()Performance with Large Trees:
collapsed() by default and implement lazy-loadingUUID Primary Keys:
id columngetLocalKeyName() and ensure parent_id uses UUID typeCheck Tree Structure:
// Verify parent/child relationships
$category->children()->count();
$category->parent?->name;
Inspect Queries: Enable Laravel query logging to verify tree queries:
DB::enableQueryLog();
$tree->getRecords();
dd(DB::getQueryLog());
Clear Caches: After model/trait changes:
php artisan optimize:clear
php artisan view:clear
Custom Save Logic:
Override beforeSave() in your model:
public function beforeSave(Tree $tree, array $data): void
{
// Validate moves
if ($data['parent_id'] === $this->id) {
throw new \Exception('Cannot move node under itself');
}
}
Custom Field Types:
Extend TreeField for custom fields:
class StatusField extends TreeField
{
protected string $view = 'filament-tree-view::fields.status';
}
Custom Styling: Publish assets and override SCSS:
php artisan vendor:publish --tag=filament-tree-view-assets
Then modify resources/css/filament-tree-view.css.
Auto-Save Behavior:
autoSave() triggers on every drag operationmaxDepth()Depth Calculation:
depth attribute is calculated on-the-flyRelation Pages:
$relationship in TreeRelationPage matches your model's relation nameprotected static string $relationship = 'subcategories';Combine with Filament Actions:
->recordActions([
Action::make('duplicate')
->action(fn (Category $record) => $record->replicate()),
]),
Add Search:
->query(fn (Builder $query) => $query->where('name', 'like', '%'.$search.'%')),
Dark Mode Support: The package automatically adapts to Filament's dark mode—no extra config needed.
Accessibility:
Testing:
Use the TreeTestCase helper for testing:
use Openplain\FilamentTreeView\Testing\TreeTestCase;
class CategoryResourceTest extends TreeTestCase
{
public function test_tree_reordering()
{
$this->actingAs($user)
->drag('node-1', 'node-2')
->assertTreeOrder(['node-2', 'node-1']);
}
}
How can I help you explore Laravel packages today?