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 Nestedset Laravel Package

wsmallnews/filament-nestedset

Filament nestedset tree plugin built on kalnoy/nestedset, with multi-language and multi-tenancy support, unlimited nesting levels, tabbed switching, and a parent select field for elegant tree management.

View on GitHub
Deep Wiki
Context7

Getting Started

The package sits between kalnoy/nestedset (the data layer) and Filament (the UI), so a working tree needs three things wired up: tree columns on the table, the NodeTrait, and a page that extends NestedsetPage. Budget an hour for the first one.

composer require wsmallnews/filament-nestedset:^2.0
php artisan vendor:publish --tag="sn-filament-nestedset-config"
# optional
php artisan vendor:publish --tag="sn-filament-nestedset-translations"
php artisan vendor:publish --tag="sn-filament-nestedset-views"

Then the model and schema:

class Category extends Model
{
    use \Kalnoy\Nestedset\NodeTrait;
}

// new table
$table->nestedSet();          // adds _lft, _rgt, parent_id

// existing table
Schema::table('categories', fn (Blueprint $table) => $table->nestedSet());

Finally, generate the page and give it a label attribute:

php artisan make:filament-nestedset-page
class Categories extends NestedsetPage
{
    protected static ?string $model = Category::class;
    protected static string $recordTitleAttribute = 'name';

    protected function schema(array $arguments): array
    {
        return [
            TextInput::make('name')->required(),
        ];
    }
}

Where to look first, in order:

  1. config/sn-filament-nestedset.php — only five keys, but three of them (allow_delete_parent, create_action_modal_show_parent_select, show_create_child_node_action_in_row) directly change what a user can do in the UI.
  2. The generated page stub — it shows the exact signature of schema(), createSchema()/editSchema(), and infolistSchema().
  3. Wsmallnews\FilamentNestedset\Pages\NestedsetPage itself. With 22 stars and a README that stops where the interesting parts begin, the base class is the real documentation. Keep it open in a split pane; $arguments in particular is worth a one-time dd() to see what context you actually get per action.
  4. codewithdennis/filament-select-tree if you want to reuse the ParentSelect field outside the auto-generated create modal.

The canonical first use case is a flat-ish taxonomy — categories, navigation menus, org units — where the admin needs to reorder and reparent without a developer. That is exactly the 80% case this plugin is built for and it works well there.

Implementation Patterns

Split schemas only when you actually need to. schema() is used for both create and edit; override createSchema() and editSchema() when, say, the parent select should be settable on creation but locked on edit. Don't split pre-emptively — two schemas drift.

Level limits are a UI-level guard rail.

protected static ?int $level = 3;

// or dynamic — e.g. tenant-dependent
public function getLevel(): ?int
{
    return $this->tenant->max_depth;
}

Infolist for row metadata. Anything that isn't the title belongs in infolistSchema() rather than being concatenated into getRecordLabel() — it keeps the label clean and the layout consistent.

protected function infolistSchema(): array
{
    return [
        TextEntry::make('status')->badge(),
        TextEntry::make('updated_at')->since(),
    ];
}

protected static string $infolistHiddenEndpoint = 'lg';   // default 'md'
protected static Alignment $infolistAlignment = Alignment::Left;

Tabs are scoping, not filtering. This is the most important thing to internalise: each tab is a kalnoy/nestedset scope. The package appends the active tab's value to the scope attributes automatically, which means the tab field must be listed in the model's getScopeAttributes() — and the tree is genuinely separate per tab, not a filtered view of one tree.

// Page
protected static ?string $tabFieldName = 'type';

public function getTabs(): array
{
    return [
        'web'  => Tab::make()->label('Website'),
        'shop' => Tab::make()->label('Shop'),
    ];
}

// Model
public function getScopeAttributes(): array
{
    return ['type', 'team_id'];
}

Multi-tenancy follows the same rule. If the panel is tenant-aware, add team_id to getScopeAttributes() and you inherit tenant isolation for free. If a particular page should not be tenant-scoped (shared global navigation, for example), opt out explicitly:

protected static bool $isScopedToTenant = false;

Stack additional scope dimensions with nestedScoped(). Same contract: whatever key you return must be in getScopeAttributes().

public function nestedScoped(): array
{
    return ['category_id' => $this->categoryId];
}

Narrow the dataset with getEloquentQuery(). Note the signature takes the query as a parameter and returns it — different from Filament's static getEloquentQuery(). Match the package's signature exactly or your constraint silently does nothing.

public function getEloquentQuery($query)
{
    return $query->where('status', 'normal');
}

Standalone Livewire usage. For a front-end tree (public sitemap, docs sidebar) you don't need the page at all — use the component and listen for the events. Leaves emit sn-filament-nestedset-leaf-click, non-leaves emit sn-filament-nestedset-node-click; pair that with getRecordUrl() when you want real navigation and leave it alone (default href="JavaScript:;") when you want pure selection.

Boost integration. After install and after every composer update, run php artisan boost:update --discover so the package's AI guidelines land in boost.json/CLAUDE.md. Given how thin the prose docs are, this is the cheapest way to get accurate generated code.

Gotchas and Tips

Adding nestedSet() to a populated table leaves every row with empty _lft/_rgt. The migration only adds columns. You must build the tree afterwards — create a root and save children through the trait, or run a fix-tree pass grouped by scope (Category::fixTree() per scope) for imported data. Symptom: the page renders everything flat, or reordering throws. Do this before showing the page to anyone.

The scope contract is enforced nowhere. If type is in getTabs() but missing from getScopeAttributes(), tabs will appear to work while all data lives in one tree — and switching tabs will move nodes into the wrong _lft/_rgt range. Likewise, any query you write outside the page must be scoped: use Category::scoped(['type' => 'web', 'team_id' => $id]), never a bare Category::all(), or cross-scope _lft/_rgt values collide and corrupt the tree.

Deletion config is a safety rail, so read it carefully before relaxing it. allow_delete_parent => false blocks deleting any node with children; allow_delete_root => false blocks root deletion even when the former is true. On kalnoy/nestedset, deleting a node removes its descendants — so flipping allow_delete_parent to true in production turns one misclick into a subtree wipe. Add a confirmation modal and a policy check if you enable it.

The two create-related config keys are coupled. If you set create_action_modal_show_parent_select => false, leave show_create_child_node_action_in_row => true, otherwise users have no way to create a child at all. Pick one flow and make it consistent per panel.

Custom Filament themes need autoload_assets => false plus a manual @import of the package CSS in your theme file. Otherwise you get either double-loaded styles or an unstyled drag handle, depending on your build.

$level is presentation, not enforcement. It limits what the UI offers; a form submitted from elsewhere, a seeder, or an import can still push a node past the limit. If depth is a business rule, validate it in the model or a saving hook.

Drag-and-drop writes immediately, one gesture at a time. Two admins reordering the same tree concurrently will fight over _lft/_rgt. For low-traffic admin panels this is fine; for anything busier, gate reordering behind a policy ability and consider a lock, or at minimum surface a "tree may have changed, refresh" nudge.

Authorisation is yours to wire. NestedsetPage is a Page, not a Resource, so there's no automatic policy plumbing for the row actions. Override canAccess() for page entry, and check abilities inside your action callbacks for create/delete/move. Multi-tenant panes make this easy to forget — a $isScopedToTenant = false page is readable by every tenant.

getRecordLabel() returns HtmlString — escape user content yourself if labels come from a rich-text or user-editable field. It's the one obvious XSS surface in the plugin.

$arguments in schema() is not documented. Dump it once on both create and edit; the shape differs, and conditional field visibility (->visible(fn ($record) => ...)) depends on knowing what's in there.

This is a small, young package (22 stars, MIT, features borrowed from 15web/filament-tree). Pin to an exact minor in composer.json, read CHANGELOG.md before every bump, and expect to read source rather than docs when something misbehaves. The trade-off is worthwhile: you get a Filament-native tree UI in an afternoon instead of building drag-and-drop reordering on top of kalnoy/nestedset yourself. Just don't treat the published config as the complete feature surface — the base page class and the NestedsetPage/Livewire component pair expose more hooks (view, recordView, getHasActive, showLevel, emptyLabel) than the README lets on, and those are what you'll reach for the first time a client asks for "one small change".

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.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator