relaticle / flowforge
Flowforge is a lightweight Kanban board package for Filament that works with existing Eloquent models.
Fund package maintenance!
Relaticle
Requires
- php: ^8.3
- filament/filament: ^4.0
- spatie/laravel-package-tools: ^1.15.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.0
- nunomaduro/collision: ^8.0
- orchestra/testbench: ^9.0
- pestphp/pest: ^3.0
- pestphp/pest-plugin-arch: ^3.0
- pestphp/pest-plugin-laravel: ^3.0
- pestphp/pest-plugin-livewire: ^3.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- spatie/laravel-ray: ^1.26
README
Transform any Laravel model into a drag-and-drop Kanban board in minutes.

Why Flowforge?
✅ Works with existing models - No new tables or migrations required
🚀 2-minute setup - From installation to working board
🎯 Filament-native - Integrates seamlessly with your admin panel
Note: For Filament v3 compatibility, use version 1.x of this package.
⚠️ Beta Warning: This is a beta version (2.x) and may contain breaking changes.
Quick Start
Install and create your first Kanban board:
composer require relaticle/flowforge php artisan flowforge:make-board TaskBoard --model=Task
That's it! Add the generated page to your Filament panel and you have a working Kanban board.
📋 Show complete example
<?php namespace App\Filament\Pages; use App\Models\Task; use Illuminate\Database\Eloquent\Builder; use Relaticle\Flowforge\Board; use Relaticle\Flowforge\BoardPage; use Relaticle\Flowforge\Column; class TaskBoardPage extends BoardPage { protected static ?string $navigationIcon = 'heroicon-o-view-columns'; public function getEloquentQuery(): Builder { return Task::query(); } public function board(Board $board): Board { return $board ->query($this->getEloquentQuery()) ->recordTitleAttribute('title') ->columnIdentifier('status') ->reorderBy('order_column') ->columns([ Column::make('todo')->label('To Do')->color('gray'), Column::make('in_progress')->label('In Progress')->color('blue'), Column::make('completed')->label('Completed')->color('green'), ]); } }
Requirements
- PHP: 8.3+
- Laravel: 11+
- Filament: 4.x
Features
Feature | Description |
---|---|
🔄 Model Agnostic | Works with any Eloquent model |
🏗️ No New Tables | Uses your existing database structure |
🖱️ Drag & Drop | Intuitive card movement between columns |
⚡ Minimal Setup | 2 methods = working board |
🎨 Customizable | Colors, properties, actions |
📱 Responsive | Works on all screen sizes |
🔍 Built-in Search | Find cards instantly |
Installation & Setup
1. Install the Package
composer require relaticle/flowforge
2. Prepare Your Model
Your model needs these fields:
- Title field (e.g.,
title
,name
) - Status field (e.g.,
status
,state
) - Order field (e.g.,
order_column
) - for drag & drop
Example migration:
Schema::create('tasks', function (Blueprint $table) { $table->id(); $table->string('title'); $table->string('status')->default('todo'); $table->integer('order_column')->nullable(); $table->timestamps(); });
3. Generate Board Page
php artisan flowforge:make-board TaskBoard --model=Task
4. Register with Filament
// app/Providers/Filament/AdminPanelProvider.php ->pages([ App\Filament\Pages\TaskBoardPage::class, ])
Done! Visit your Filament panel to see your new Kanban board.
Configuration Examples
Basic Read-Only Board
Perfect for dashboards and overview pages:
public function board(Board $board): Board { return $board ->query($this->getEloquentQuery()) ->recordTitleAttribute('title') ->columnIdentifier('status') ->columns([ Column::make('backlog')->label('Backlog'), Column::make('active')->label('Active'), Column::make('done')->label('Done')->color('green'), ]); }
Interactive Board with Actions
Add create and edit capabilities:
use Filament\Actions\CreateAction; use Filament\Actions\EditAction; use Filament\Actions\DeleteAction; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Select; public function board(Board $board): Board { return $board ->query($this->getEloquentQuery()) ->recordTitleAttribute('title') ->columnIdentifier('status') ->reorderBy('order_column') ->columns([ Column::make('todo')->label('To Do')->color('gray'), Column::make('in_progress')->label('In Progress')->color('blue'), Column::make('completed')->label('Completed')->color('green'), ]) ->columnActions([ CreateAction::make('create') ->label('Add Task') ->icon('heroicon-o-plus') ->model(Task::class) ->form([ TextInput::make('title')->required(), Select::make('priority') ->options(['low' => 'Low', 'medium' => 'Medium', 'high' => 'High']) ->default('medium'), ]) ->mutateFormDataUsing(function (array $data, string $columnId): array { $data['status'] = $columnId; return $data; }), ]) ->cardActions([ EditAction::make('edit')->model(Task::class), DeleteAction::make('delete')->model(Task::class), ]) ->cardAction('edit'); // Make cards clickable }
Advanced Board with Schema
Use Filament's Schema system for rich card content:
use Filament\Infolists\Components\TextEntry; use Filament\Schemas\Schema; public function board(Board $board): Board { return $board ->query($this->getEloquentQuery()) ->recordTitleAttribute('title') ->columnIdentifier('status') ->cardSchema(fn (Schema $schema) => $schema ->components([ TextEntry::make('priority') ->badge() ->color(fn ($state) => match ($state) { 'high' => 'danger', 'medium' => 'warning', 'low' => 'success', default => 'gray', }), TextEntry::make('due_date') ->date() ->icon('heroicon-o-calendar'), TextEntry::make('assignee.name') ->icon('heroicon-o-user') ->placeholder('Unassigned'), ]) ) ->columns([...]); }
API Reference
Board Configuration Methods
Method | Description | Required |
---|---|---|
query(Builder|Closure) |
Set the data source | ✅ |
recordTitleAttribute(string) |
Field used for card titles | ✅ |
columnIdentifier(string) |
Field that determines column placement | ✅ |
columns(array) |
Define board columns | ✅ |
reorderBy(string, string) |
Enable drag & drop with field and direction | |
cardSchema(Closure) |
Configure card content with Filament Schema | |
cardActions(array) |
Actions for individual cards | |
columnActions(array) |
Actions for column headers | |
cardAction(string) |
Default action when cards are clicked | |
searchable(array) |
Enable search across specified fields |
Livewire Methods (Available in your BoardPage)
Method | Description | Usage |
---|---|---|
updateRecordsOrderAndColumn(string, array) |
Handle drag & drop updates | Automatic |
loadMoreItems(string, ?int) |
Load more cards for pagination | Automatic |
getBoardRecord(int|string) |
Get single record by ID | Manual |
getBoardColumnRecords(string) |
Get all records for a column | Manual |
getBoardColumnRecordCount(string) |
Count records in a column | Manual |
Available Colors
gray
, red
, orange
, yellow
, green
, blue
, indigo
, purple
, pink
Troubleshooting
🔧 Cards not draggable
Cause: Missing order column or reorderBy configuration Solution:
- Add integer column to your migration:
$table->integer('order_column')->nullable();
- Add
->reorderBy('order_column')
to your board configuration - Ensure your model's
$fillable
includes the order column
📭 Empty board showing
Cause: Query returns no results or status field mismatch Debug steps:
- Check query:
dd($this->getEloquentQuery()->get());
- Verify status values match column names exactly
- Check database field type (string vs enum)
❌ Actions not working
Cause: Missing Filament traits or action configuration Solution:
- Ensure your BoardPage implements
HasActions
,HasForms
- Use these traits in your class:
use InteractsWithActions; use InteractsWithForms; use InteractsWithBoard;
- Configure actions properly with
->model(YourModel::class)
🔄 Drag & drop updates not saving
Cause: Missing primary key handling or invalid field names Solution:
- Ensure your model uses standard primary key or override
getKeyName()
- Check status field accepts the column identifier values
- Verify order column exists and is fillable
💥 "No default Filament panel" error
Cause: Missing panel configuration in tests/development Solution: Add to your panel provider:
Panel::make()->default()->id('admin')
🎨 Styling not loading
Cause: Assets not built or registered Solution:
- Run
npm run build
to compile assets - Ensure Filament can load the assets with proper permissions
Real-World Examples
Complete Task Management Board
<?php namespace App\Filament\Pages; use App\Models\Task; use Filament\Actions\CreateAction; use Filament\Actions\EditAction; use Filament\Actions\DeleteAction; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\DatePicker; use Filament\Infolists\Components\TextEntry; use Filament\Schemas\Schema; use Illuminate\Database\Eloquent\Builder; use Relaticle\Flowforge\Board; use Relaticle\Flowforge\BoardPage; use Relaticle\Flowforge\Column; class TaskBoardPage extends BoardPage { protected static ?string $navigationIcon = 'heroicon-o-view-columns'; protected static ?string $navigationLabel = 'Task Board'; public function getEloquentQuery(): Builder { return Task::query()->with('assignee'); } public function board(Board $board): Board { return $board ->query($this->getEloquentQuery()) ->recordTitleAttribute('title') ->columnIdentifier('status') ->reorderBy('order_column', 'desc') ->searchable(['title', 'description', 'assignee.name']) ->columns([ Column::make('todo')->label('📋 To Do')->color('gray'), Column::make('in_progress')->label('🔄 In Progress')->color('blue'), Column::make('review')->label('👁️ Review')->color('purple'), Column::make('completed')->label('✅ Completed')->color('green'), ]) ->cardSchema(fn (Schema $schema) => $schema ->components([ TextEntry::make('priority') ->badge() ->color(fn ($state) => match ($state) { 'high' => 'danger', 'medium' => 'warning', 'low' => 'success', default => 'gray', }), TextEntry::make('due_date') ->date() ->icon('heroicon-o-calendar') ->color('orange'), TextEntry::make('assignee.name') ->icon('heroicon-o-user') ->placeholder('Unassigned'), ]) ) ->columnActions([ CreateAction::make('create') ->label('Add Task') ->icon('heroicon-o-plus') ->model(Task::class) ->form([ TextInput::make('title')->required(), Select::make('priority') ->options(['low' => 'Low', 'medium' => 'Medium', 'high' => 'High']) ->default('medium'), DatePicker::make('due_date'), ]) ->mutateFormDataUsing(function (array $data, string $columnId): array { $data['status'] = $columnId; return $data; }), ]) ->cardActions([ EditAction::make('edit') ->model(Task::class) ->form([ TextInput::make('title')->required(), Select::make('status') ->options([ 'todo' => 'To Do', 'in_progress' => 'In Progress', 'review' => 'Review', 'completed' => 'Completed', ]), Select::make('priority') ->options(['low' => 'Low', 'medium' => 'Medium', 'high' => 'High']), DatePicker::make('due_date'), ]), DeleteAction::make('delete') ->model(Task::class) ->requiresConfirmation(), ]) ->cardAction('edit'); // Make cards clickable to edit } }
Required Database Schema
CREATE TABLE tasks ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) NOT NULL, status VARCHAR(255) DEFAULT 'todo', order_column INT NULL, -- Required for drag & drop priority VARCHAR(255) DEFAULT 'medium', due_date DATE NULL, assignee_id BIGINT UNSIGNED NULL, created_at TIMESTAMP NULL, updated_at TIMESTAMP NULL );
Testing Your Board
// tests/Feature/TaskBoardTest.php use Livewire\Livewire; test('task board renders successfully', function () { Task::create(['title' => 'Test Task', 'status' => 'todo']); Livewire::test(TaskBoardPage::class) ->assertSuccessful() ->assertSee('Test Task') ->assertSee('To Do'); }); test('can move tasks between columns', function () { $task = Task::create(['title' => 'Test Task', 'status' => 'todo']); Livewire::test(TaskBoardPage::class) ->call('updateRecordsOrderAndColumn', 'completed', [$task->getKey()]) ->assertSuccessful(); expect($task->fresh()->status)->toBe('completed'); });
Need Help?
- 📖 Documentation (coming soon)
- 🐛 Report Issues
- 💬 Discussions
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT License. See LICENSE.md for details.
Built with ❤️ for the Laravel community