Builds Kanban board interfaces for Eloquent models with drag-and-drop functionality. Use when creating board pages, configuring columns and cards, implementing drag-and-drop positioning, working with Filament board pages or standalone Livewire boards, or troubleshooting position-related issues.
Use when:
use Illuminate\Database\Schema\Blueprint;
Schema::table('tasks', function (Blueprint $table) {
$table->flowforgePositionColumn(); // DECIMAL(20,10) nullable
$table->unique(['status', 'position']);
});
php artisan flowforge:make-board TaskBoard
use Relaticle\Flowforge\BoardPage;
use Relaticle\Flowforge\Board;
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')
->recordTitleAttribute('title')
->columns([
Column::make('todo', 'To Do')
->icon('heroicon-o-clipboard'),
Column::make('in_progress', 'In Progress')
->icon('heroicon-o-play'),
Column::make('done', 'Done')
->icon('heroicon-o-check'),
]);
}
}
use Relaticle\Flowforge\BoardPage;
class TaskBoard extends BoardPage
{
protected static ?string $navigationIcon = 'heroicon-o-view-columns';
protected static ?string $navigationGroup = 'Tasks';
public function board(Board $board): Board
{
return $board
->query(Task::query()->where('team_id', auth()->user()->team_id))
->columnIdentifier('status')
->positionIdentifier('position')
->columns([...]);
}
}
use Relaticle\Flowforge\BoardResourcePage;
class TaskBoardPage extends BoardResourcePage
{
protected static string $resource = TaskResource::class;
public function board(Board $board): Board
{
return $board
->query($this->getResource()::getEloquentQuery())
->columnIdentifier('status')
->positionIdentifier('position')
->columns([...]);
}
}
Register in resource:
public static function getPages(): array
{
return [
'index' => Pages\ListTasks::route('/'),
'board' => Pages\TaskBoardPage::route('/board'),
];
}
use Livewire\Component;
use Relaticle\Flowforge\Board;
use Relaticle\Flowforge\Contracts\HasBoard;
use Relaticle\Flowforge\Concerns\InteractsWithBoard;
class TaskBoard extends Component implements HasBoard
{
use InteractsWithBoard;
public function board(Board $board): Board
{
return $board
->query(Task::query())
->columnIdentifier('status')
->positionIdentifier('position')
->columns([...]);
}
public function render()
{
return view('livewire.task-board');
}
}
Blade view:
<div>
{{ $this->board }}
</div>
use Relaticle\Flowforge\Column;
->columns([
Column::make('backlog', 'Backlog')
->icon('heroicon-o-inbox')
->color('gray'),
Column::make('todo', 'To Do')
->icon('heroicon-o-clipboard')
->color('info'),
Column::make('in_progress', 'In Progress')
->icon('heroicon-o-play')
->color('warning'),
Column::make('review', 'Review')
->icon('heroicon-o-eye')
->color('primary'),
Column::make('done', 'Done')
->icon('heroicon-o-check-circle')
->color('success'),
])
Use Filament's Schema builder for rich card layouts:
use Filament\Infolists\Components\TextEntry;
use Filament\Infolists\Components\ImageEntry;
use Filament\Schemas\Components\Grid;
->cardSchema([
Grid::make(2)
->schema([
TextEntry::make('title')
->weight('bold'),
TextEntry::make('priority')
->badge()
->color(fn ($state) => match ($state) {
'high' => 'danger',
'medium' => 'warning',
default => 'gray',
}),
]),
TextEntry::make('assignee.name')
->icon('heroicon-o-user'),
TextEntry::make('due_date')
->date()
->icon('heroicon-o-calendar'),
])
->cardsPerColumn(20) // Cards loaded initially
->cardsIncrement(10) // Cards loaded on "Load More"
->searchable(['title', 'description'])
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Filters\TernaryFilter;
->filters([
SelectFilter::make('priority')
->options([
'low' => 'Low',
'medium' => 'Medium',
'high' => 'High',
]),
SelectFilter::make('assignee_id')
->relationship('assignee', 'name')
->searchable()
->preload(),
TernaryFilter::make('is_overdue')
->label('Overdue'),
])
Record Actions (per card):
use Filament\Actions\Action;
use Filament\Actions\EditAction;
use Filament\Actions\DeleteAction;
->recordActions([
EditAction::make()
->url(fn ($record) => route('tasks.edit', $record)),
Action::make('archive')
->icon('heroicon-o-archive-box')
->action(fn ($record) => $record->archive()),
DeleteAction::make(),
])
Column Actions (per column header):
->columnActions([
Action::make('add')
->icon('heroicon-o-plus')
->action(function (array $arguments) {
// $arguments['column'] contains column identifier
Task::create([
'status' => $arguments['column'],
'position' => DecimalPosition::forEmptyColumn(),
]);
}),
])
Flowforge uses DECIMAL(20,10) positions with BCMath precision for reliable ordering.
use Relaticle\Flowforge\Services\DecimalPosition;
// Position between two cards (includes cryptographic jitter)
$position = DecimalPosition::between($afterPosition, $beforePosition);
// Exact midpoint (deterministic, for testing)
$position = DecimalPosition::betweenExact($afterPosition, $beforePosition);
// Position before first card
$position = DecimalPosition::before($firstPosition);
// Position after last card
$position = DecimalPosition::after($lastPosition);
// Initial position for empty column
$position = DecimalPosition::forEmptyColumn();
// Smart positioning (handles nulls)
$position = DecimalPosition::calculate($afterPos, $beforePos);
// Check if rebalancing needed
if (DecimalPosition::needsRebalancing($posA, $posB)) {
// Gap is < 0.0001
}
// Generate evenly-spaced sequence
$positions = DecimalPosition::generateSequence(count: 100);
// In your Livewire component
public function moveCard(
int|string $recordId,
string $toColumn,
?string $afterRecordId = null,
?string $beforeRecordId = null
): void {
// Parent handles position calculation and saving
parent::moveCard($recordId, $toColumn, $afterRecordId, $beforeRecordId);
// Add custom logic after move
$this->dispatch('card-moved');
}
php artisan flowforge:make-board TaskBoard
php artisan flowforge:make-board TaskBoard --resource # For resource page
php artisan flowforge:diagnose-positions "App\Models\Task" status position
Checks for:
php artisan flowforge:rebalance-positions "App\Models\Task" status position
php artisan flowforge:rebalance-positions "App\Models\Task" status position --column=in_progress
php artisan flowforge:repair-positions "App\Models\Task" status position
Offers multiple repair strategies:
Publish config:
php artisan vendor:publish --tag=flowforge-config
config/flowforge.php:
return [
'columns' => [
'default_limit' => 50,
],
'kanban' => [
'initial_cards_count' => 20,
'cards_increment' => 10,
],
'ui' => [
'show_item_counts' => true,
],
];
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('tasks', function (Blueprint $table) {
$table->flowforgePositionColumn();
$table->unique(['status', 'position']);
});
}
};
Important: The unique constraint on [column_identifier, position] is required for concurrent safety.
public function board(Board $board): Board
{
return $board
->query(Task::query()->where('team_id', auth()->user()->team_id))
// ...
}
public function board(Board $board): Board
{
$statuses = Status::ordered()->get();
return $board
->query(Task::query())
->columnIdentifier('status_id')
->positionIdentifier('position')
->columns(
$statuses->map(fn ($status) =>
Column::make($status->id, $status->name)
->icon($status->icon)
->color($status->color)
)->toArray()
);
}
public function board(Board $board): Board
{
return $board
->query(Task::query()->with(['assignee', 'tags', 'project']))
// ...
}
->recordActions([
Action::make('view')
->url(fn ($record) => TaskResource::getUrl('view', ['record' => $record]))
->openUrlInNewTab(),
])
ext-bcmathDECIMAL(20,10) with unique constraint下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer