relaticle/flowforge

Flowforge is a lightweight Kanban board package for Filament that works with existing Eloquent models.

Fund package maintenance!
Relaticle

Installs: 3 491

Dependents: 0

Suggesters: 0

Security: 0

Stars: 215

Watchers: 5

Forks: 12

v2.0.1 2025-08-25 17:53 UTC

README

Transform any Laravel model into a production-ready drag-and-drop Kanban board.

Latest Version Total Downloads PHP 8.3+ Filament 4 Tests

Flowforge Kanban Board

Why Flowforge?

๐ŸŽฏ 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

๐Ÿš€ Quick Start (90 seconds)

1. Install Package

composer require relaticle/flowforge

2. Include CSS Assets

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";

3. Add Position Column

php artisan make:migration add_position_to_tasks_table
// migration
Schema::table('tasks', function (Blueprint $table) {
    $table->flowforgePositionColumn('position'); // Handles database-specific collations automatically
});

3. Generate Board

php artisan flowforge:make-board TaskBoard --model=Task

4. Register Page

// AdminPanelProvider.php
->pages([
    App\Filament\Pages\TaskBoard::class,
])

๐ŸŽ‰ Done! Visit your Filament panel to see your Kanban board in action.

5. Repair Positions (Optional)

If you need to fix corrupted or missing position data:

php artisan flowforge:repair-positions

๐Ÿ“‹ Requirements

  • PHP: 8.3+
  • Laravel: 11+
  • Filament: 4.x
  • Database: MySQL, PostgreSQL, SQLite, SQL Server, MariaDB

๐ŸŽฏ Integration Patterns

๐Ÿ”น 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

๐ŸŽจ Customization

Rich Card Content

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'),
        ]));
}

Actions and Interactions

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
}

Search and Filtering

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(),
        ]);
}

๐Ÿ—๏ธ Database Schema

Required Fields

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

Database-Specific Collations

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

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');
});

๐Ÿš€ Performance Features

  • 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

๐ŸŽ›๏ธ API Reference

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')

๐Ÿ› Troubleshooting

Common Issues & Solutions

Cards not draggable

Cause: Missing positionIdentifier or position column
Solution: Add ->positionIdentifier('position') and ensure database column exists

Empty board

Cause: Status values don't match column identifiers
Debug: dd($this->getEloquentQuery()->get()) to verify data

Actions not working

Cause: Missing traits or action configuration
Solution: Ensure your class uses InteractsWithActions, InteractsWithForms

New cards appear randomly

Cause: Missing position in create actions
Solution: Add $data['position'] = $this->getBoardPositionInColumn($arguments['column']);

Corrupted or missing position data

Cause: Database issues, manual edits, or migration problems
Solution: Run php artisan flowforge:repair-positions to fix position data

๐Ÿค 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