Transform any Laravel model into a production-ready drag-and-drop Kanban board.
π― 3 Integration Patterns - Filament Pages, Resources, or standalone Livewire
β‘ Production-Ready - Handles 100+ cards per column with intelligent pagination
π§ Zero Configuration - Works with your existing models and database
π¨ Fully Customizable - Actions, schemas, filters, and themes
composer require relaticle/flowforge
Important
If you have not set up a custom theme and are using Filament Panels follow the instructions in the Filament Docs first.
After setting up a custom theme add the plugin's views to your theme css file.
/* In your main CSS file (e.g., resources/css/app.css) */
@source "../../../../vendor/relaticle/flowforge/resources/views/**/*.blade.php";
php artisan make:migration add_position_to_tasks_table
// migration
Schema::table('tasks', function (Blueprint $table) {
$table->flowforgePositionColumn('position'); // Handles database-specific collations automatically
});
php artisan flowforge:make-board TaskBoard --model=Task
// AdminPanelProvider.php
->pages([
App\Filament\Pages\TaskBoard::class,
])
π Done! Visit your Filament panel to see your Kanban board in action.
If you need to fix corrupted or missing position data:
php artisan flowforge:repair-positions
- PHP: 8.3+
- Laravel: 11+
- Filament: 4.x
- Database: MySQL, PostgreSQL, SQLite, SQL Server, MariaDB
πΉ Pattern 1: Filament Page (Recommended)
Perfect for dedicated board pages in your admin panel.
<?php
namespace App\Filament\Pages;
use App\Models\Task;
use Relaticle\Flowforge\Board;
use Relaticle\Flowforge\BoardPage;
use Relaticle\Flowforge\Column;
class TaskBoard extends BoardPage
{
protected static ?string $navigationIcon = 'heroicon-o-view-columns';
public function board(Board $board): Board
{
return $board
->query(Task::query())
->columnIdentifier('status')
->positionIdentifier('position')
->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'),
]);
}
}
β
Use when: You want a standalone Kanban page in your admin panel
β
Benefits: Full Filament integration, automatic registration, built-in actions
πΉ Pattern 2: Resource Integration
Integrate with your existing Filament resources. Perfect for campaign management where teams track tasks within campaigns.
<?php
namespace App\Filament\Resources\CampaignResource\Pages;
use App\Filament\Resources\CampaignResource;
use App\Models\Campaign;
use Relaticle\Flowforge\Board;
use Relaticle\Flowforge\BoardResourcePage;
use Relaticle\Flowforge\Column;
class CampaignTaskBoard extends BoardResourcePage
{
protected static string $resource = CampaignResource::class;
public function board(Board $board): Board
{
return $board
->query(
// Get tasks for this specific campaign and current user's team
$this->getRecord()
->tasks()
->whereHas('team', fn($q) => $q->where('id', auth()->user()->current_team_id))
->getQuery()
)
->columnIdentifier('status')
->positionIdentifier('position')
->columns([
Column::make('backlog')->label('Backlog')->color('gray'),
Column::make('in_progress')->label('In Progress')->color('blue'),
Column::make('review')->label('Review')->color('amber'),
Column::make('completed')->label('Completed')->color('green'),
]);
}
}
// Register in your CampaignResource
public static function getPages(): array
{
return [
'index' => Pages\ListCampaigns::route('/'),
'create' => Pages\CreateCampaign::route('/create'),
'edit' => Pages\EditCampaign::route('/{record}/edit'),
'tasks' => Pages\CampaignTaskBoard::route('/{record}/tasks'), // Add this line
];
}
β
Use when: You want to add Kanban to existing Filament resources
β
Benefits: Inherits resource permissions, policies, and global scopes
πΉ Pattern 3: Standalone Livewire
Use outside of Filament or in custom applications.
<?php
namespace App\Livewire;
use App\Models\Task;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Livewire\Component;
use Relaticle\Flowforge\Board;
use Relaticle\Flowforge\Column;
use Relaticle\Flowforge\Concerns\InteractsWithBoard;
use Relaticle\Flowforge\Contracts\HasBoard;
class TaskBoard extends Component implements HasBoard, HasActions, HasForms
{
use InteractsWithBoard;
use InteractsWithActions;
use InteractsWithForms;
public function board(Board $board): Board
{
return $board
->query(Task::query())
->columnIdentifier('status')
->positionIdentifier('position')
->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'),
]);
}
public function render()
{
return view('livewire.task-board');
}
}
{{-- resources/views/livewire/task-board.blade.php --}}
<div>
<h1 class="text-2xl font-bold mb-6">Task Board</h1>
{{ $this->board }}
</div>
β
Use when: Building custom interfaces or non-Filament applications
β
Benefits: Maximum flexibility, custom styling, independent routing
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Schema;
public function board(Board $board): Board
{
return $board
->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'),
]));
}
Column Actions (Create, Bulk Operations)
use Filament\Actions\CreateAction;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Select;
public function board(Board $board): Board
{
return $board
->columnActions([
CreateAction::make()
->label('Add Task')
->model(Task::class)
->form([
TextInput::make('title')->required(),
Select::make('priority')
->options(['low' => 'Low', 'medium' => 'Medium', 'high' => 'High'])
->default('medium'),
])
->mutateFormDataUsing(function (array $data, array $arguments): array {
if (isset($arguments['column'])) {
$data['status'] = $arguments['column'];
$data['position'] = $this->getBoardPositionInColumn($arguments['column']);
}
return $data;
}),
]);
}
Card Actions (Edit, Delete, Custom)
use Filament\Actions\EditAction;
use Filament\Actions\DeleteAction;
public function board(Board $board): Board
{
return $board
->cardActions([
EditAction::make()->model(Task::class),
DeleteAction::make()->model(Task::class),
])
->cardAction('edit'); // Makes cards clickable
}
Advanced Filtering
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Filters\Filter;
public function board(Board $board): Board
{
return $board
->searchable(['title', 'description', 'assignee.name'])
->filters([
SelectFilter::make('priority')
->options(TaskPriority::class)
->multiple(),
SelectFilter::make('assigned_to')
->relationship('assignee', 'name')
->searchable()
->preload(),
Filter::make('overdue')
->label('Overdue')
->query(fn (Builder $query) => $query->where('due_date', '<', now()))
->toggle(),
]);
}
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->string('title'); // Card title
$table->string('status'); // Column identifier
$table->flowforgePositionColumn(); // Drag-and-drop ordering (handles DB-specific collations)
$table->timestamps();
});
// Custom column name
$table->flowforgePositionColumn('sort_order'); // Creates 'sort_order' column instead
The flowforgePositionColumn()
method automatically applies the correct binary collation for each database:
Database | Collation | Purpose |
---|---|---|
MySQL/MariaDB | utf8mb4_bin |
Binary comparison by character code values |
PostgreSQL | C |
Binary byte comparison (POSIX locale) |
SQL Server | Latin1_General_BIN2 |
Unicode code-point comparison |
SQLite | None | Uses BINARY collation by default |
These collations ensure consistent fractional ranking behavior across all database systems.
Testing Your Boards
use Livewire\Livewire;
test('task board renders successfully', function () {
Task::factory()->count(10)->create();
Livewire::test(TaskBoard::class)
->assertSuccessful()
->assertSee('To Do')
->assertSee('In Progress')
->assertSee('Done');
});
test('can move tasks between columns', function () {
$task = Task::factory()->todo()->create();
Livewire::test(TaskBoard::class)
->call('moveCard', $task->id, 'in_progress')
->assertSuccessful();
expect($task->fresh()->status)->toBe('in_progress');
});
- Intelligent Pagination: Efficiently handles 100+ cards per column
- Infinite Scroll: Smooth loading with 80% scroll threshold
- Optimistic UI: Immediate feedback with rollback on errors
- Position Algorithm: Fractional ranking prevents database locks
- Query Optimization: Cursor-based pagination with relationship eager loading
Board Configuration
Method | Description | Required |
---|---|---|
query(Builder) |
Set data source | β |
columnIdentifier(string) |
Status field name | β |
positionIdentifier(string) |
Position field name | β |
columns(array) |
Define board columns | β |
recordTitleAttribute(string) |
Card title field | |
cardSchema(Closure) |
Rich card content | |
cardActions(array) |
Card-level actions | |
columnActions(array) |
Column-level actions | |
searchable(array) |
Enable search | |
filters(array) |
Add filters |
Column Configuration
Column::make('todo')
->label('To Do')
->color('gray') // gray, blue, red, green, amber, purple, pink
->icon('heroicon-o-queue-list')
Common Issues & Solutions
Cause: Missing positionIdentifier
or position column
Solution: Add ->positionIdentifier('position')
and ensure database column exists
Cause: Status values don't match column identifiers
Debug: dd($this->getEloquentQuery()->get())
to verify data
Cause: Missing traits or action configuration
Solution: Ensure your class uses InteractsWithActions
, InteractsWithForms
Cause: Missing position in create actions
Solution: Add $data['position'] = $this->getBoardPositionInColumn($arguments['column']);
Cause: Database issues, manual edits, or migration problems
Solution: Run php artisan flowforge:repair-positions
to fix position data
Contributions are welcome! Please feel free to submit a Pull Request.
- Implementation Guide - Complete developer guide
- Testing Examples - Production-ready test patterns
- Report Issues
MIT License. See LICENSE.md for details.
Built with β€οΈ for the Laravel community