solution-forest/filament-tree
Filament Tree adds a drag-and-drop hierarchical tree UI to Filament Admin for managing nested data (menus, categories, org charts) with unlimited depth. Works with Resources, Pages, and Widgets, plus customizable actions, icons, and translations.
Install the package:
composer require solution-forest/filament-tree
php artisan filament:assets
Create a migration with tree columns:
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->treeColumns(); // Adds parent_id, order, title
$table->timestamps();
});
Add the ModelTree trait to your model:
use SolutionForest\FilamentTree\Concern\ModelTree;
class Category extends Model
{
use ModelTree;
}
Generate a tree widget/page:
php artisan make:filament-tree-widget CategoryWidget --model=Category
Embed a tree widget in a Filament resource page:
// In your resource's ListRecords page
protected function getHeaderWidgets(): array
{
return [CategoryWidget::class];
}
// app/Filament/Widgets/CategoryWidget.php
class CategoryWidget extends \SolutionForest\FilamentTree\Widgets\Tree
{
protected static string $model = Category::class;
protected static int $maxDepth = 3;
protected function getFormSchema(): array
{
return [
TextInput::make('title')->required(),
];
}
}
// app/Filament/Pages/CategoryTree.php
class CategoryTree extends \SolutionForest\FilamentTree\Pages\TreePage
{
protected static string $resource = CategoryResource::class;
protected function getTreeToolbarActions(): array
{
return [
CreateAction::make()->label('Add Category'),
ExportAction::make()->label('Export Tree'),
];
}
public function getTreeRecordTitle(?Category $record): string
{
return "[{$record->id}] {$record->title}";
}
}
php artisan make:filament-tree-page CategoryTree --resource=Category
Register in resource:
public static function getPages(): array
{
return [
'tree' => Pages\CategoryTree::route('/tree'),
];
}
$maxDepth to control nesting levelsgetNodeCollapsedState()Translatable trait with Spatie TranslatablegetTreeQuery() in widgets/pagesParent ID Default Value
parent_id->default(0)parent_id->default(-1) (required for root nodes)Missing Asset Registration
php artisan filament:assets after installationColumn Name Mismatches
public function determineParentColumnName(): string
{
return 'parent_category_id';
}
Depth Limitations
$maxDepth to prevent infinite nesting:
protected static int $maxDepth = 5;
dd($record->getTree()) to inspect relationships.env:
DB_LOG_QUERIES=true
php artisan optimize:clear
Custom Tree Icons
public function getTreeRecordIcon(?Category $record): ?string
{
return match ($record->type) {
'folder' => 'heroicon-o-folder',
'file' => 'heroicon-o-document',
default => null,
};
}
Conditional Node Display
public function getTreeRecordTitle(?Category $record): string
{
if (!$record->is_active) {
return "🚫 {$record->title}";
}
return $record->title;
}
Toolbar Actions
protected function getTreeToolbarActions(): array
{
return [
Action::make('custom-action')
->label('Custom Action')
->action(fn () => $this->dispatchBrowserEvent('custom-event')),
];
}
-1 (not null or 0)determineTitleColumnName() if customizedEager Load Relationships
protected function getTreeQuery(): Builder
{
return Category::with('children')->query();
}
Limit Depth
protected static int $maxDepth = 3;
Collapse Nodes by Default
public function getNodeCollapsedState(?Category $record): bool
{
return $record->getDepth() > 1;
}
Add Traits
use SolutionForest\FilamentTree\Concern\TreeRecords\Translatable;
use Spatie\Translatable\HasTranslations;
class Category extends Model
{
use HasTranslations, ModelTree;
protected $translatable = ['title'];
}
Configure Locales
public function getTranslatableLocales(): array
{
return ['en', 'fr', 'es'];
}
Override defaults in your model:
public function determineOrderColumnName(): string
{
return 'sort_order';
}
public function determineParentColumnName(): string
{
return 'parent_category_id';
}
getTreeToolbarActions()Shift to move nodes between branchesCtrl/Cmd to copy nodes (if implemented)How can I help you explore Laravel packages today?