-
-
Save binaryfire/8a1bb35648c9d509c8f649c4a4e64d05 to your computer and use it in GitHub Desktop.
Flowforge - Custom Fields Integration Example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace App\Filament\Clusters\Management\Pages; | |
use App\Models\Management\Opportunity; | |
use BackedEnum; | |
use Exception; | |
use Filament\Actions\CreateAction; | |
use Filament\Actions\EditAction; | |
use Filament\Forms\Components\Select; | |
use Filament\Forms\Components\TextInput; | |
use Filament\Infolists\Components\TextEntry; | |
use Filament\Schemas\Schema; | |
use Filament\Support\Enums\Width; | |
use Illuminate\Database\Eloquent\Builder; | |
use Illuminate\Support\Facades\DB; | |
use Relaticle\CustomFields\Facades\CustomFields; | |
use Relaticle\CustomFields\Models\CustomField; | |
use Relaticle\Flowforge\Board; | |
use Relaticle\Flowforge\BoardPage; | |
use Relaticle\Flowforge\Column; | |
use UnitEnum; | |
final class OpportunityBoard extends BoardPage | |
{ | |
protected static ?string $navigationLabel = 'Opportunity Board'; | |
protected static ?string $title = 'Opportunities'; | |
protected static string | null | UnitEnum $navigationGroup = 'Flowforge'; | |
protected static string | null | BackedEnum $navigationIcon = 'heroicon-o-briefcase'; | |
public function board(Board $board): Board | |
{ | |
return $board | |
->query($this->getOpportunityQuery()) | |
->columnIdentifier('cfv.string_value') | |
->positionIdentifier('position') | |
->recordTitleAttribute('title') | |
->columns($this->getDynamicColumns()) | |
->columnActions([ | |
CreateAction::make() | |
->label('Add Opportunity') | |
->iconButton() | |
->icon('heroicon-o-plus') | |
->model(Opportunity::class) | |
->modalWidth(Width::Large) | |
->schema([ | |
TextInput::make('title') | |
->required() | |
->columnSpanFull(), | |
Select::make('user_id') | |
->label('Assign to') | |
->relationship('user', 'name') | |
->searchable() | |
->preload() | |
->required() | |
->placeholder('Unassigned') | |
->columnSpanFull(), | |
CustomFields::form()->forModel(Opportunity::class)->except(['status'])->build(), | |
]) | |
->mutateDataUsing(function (array $data, array $arguments): array { | |
if (isset($arguments['column'])) { | |
$data['position'] = $this->getBoardPositionInColumn($arguments['column']); | |
} | |
return $data; | |
}) | |
->after(function (Opportunity $record, array $arguments): void { | |
if (isset($arguments['column'])) { | |
$this->updateCustomFieldStatus($record, $arguments['column']); | |
} | |
}), | |
]) | |
->cardActions([ | |
EditAction::make() | |
->label('Edit') | |
->icon('heroicon-o-pencil') | |
->modalWidth(Width::Large) | |
->schema([ | |
TextInput::make('title') | |
->required() | |
->columnSpanFull(), | |
Select::make('user_id') | |
->label('Assign to') | |
->relationship('user', 'name') | |
->searchable() | |
->preload() | |
->required() | |
->placeholder('Unassigned') | |
->columnSpanFull(), | |
CustomFields::form()->forModel(Opportunity::class)->except(['status'])->build(), | |
]), | |
]) | |
->cardSchema(function (Schema $schema): Schema { | |
return $schema | |
->components([ | |
TextEntry::make('user.name') | |
->hiddenLabel() | |
->badge() | |
->color('gray') | |
->icon('heroicon-o-user') | |
->placeholder('Unassigned'), | |
CustomFields::infolist()->forSchema($schema)->except(['status'])->build(), | |
]); | |
}); | |
} | |
public function moveCard( | |
string $cardId, | |
string $targetColumnId, | |
?string $afterCardId = null, | |
?string $beforeCardId = null | |
): void { | |
$opportunity = Opportunity::query()->findOrFail($cardId); | |
$newPosition = $this->calculatePositionBetweenCards($afterCardId, $beforeCardId, $targetColumnId); | |
// Use transaction for atomicity | |
DB::transaction(function () use ($opportunity, $targetColumnId, $newPosition) { | |
$this->updateCustomFieldStatus($opportunity, $targetColumnId); | |
$opportunity->update(['position' => $newPosition]); | |
}); | |
$this->dispatch('kanban-card-moved', [ | |
'cardId' => $cardId, | |
'columnId' => $targetColumnId, | |
'position' => $newPosition, | |
]); | |
} | |
private function getOpportunityQuery(): Builder | |
{ | |
return Opportunity::query() | |
->with('user', 'customFieldValues.customField') | |
->join('custom_field_values as cfv', 'management_opportunities.id', '=', 'cfv.entity_id') | |
->join('custom_fields as cf', 'cfv.custom_field_id', '=', 'cf.id') | |
->where('cfv.entity_type', Opportunity::class) | |
->where('cf.code', 'status') | |
->select('management_opportunities.*', 'cfv.string_value as board_status'); | |
} | |
private function getStatusCustomField(): ?CustomField | |
{ | |
return CustomField::where('code', 'status') | |
->where('entity_type', Opportunity::class) | |
->first(); | |
} | |
private function updateCustomFieldStatus(Opportunity $opportunity, string $status): void | |
{ | |
$statusField = $this->getStatusCustomField(); | |
if (! $statusField) { | |
return; | |
} | |
DB::table('custom_field_values') | |
->updateOrInsert([ | |
'entity_type' => Opportunity::class, | |
'entity_id' => $opportunity->id, | |
'custom_field_id' => $statusField->id, | |
], [ | |
'string_value' => $status, | |
]); | |
} | |
/** | |
* @return array<Column> | |
* | |
* @throws Exception | |
*/ | |
protected function getDynamicColumns(): array | |
{ | |
$customField = $this->getStatusCustomField(); | |
if (! $customField) { | |
return []; | |
} | |
$options = $customField->options()->orderBy('sort_order')->get(); | |
$columns = []; | |
foreach ($options as $option) { | |
$columns[] = Column::make($option->name) | |
->label($option->name); | |
} | |
return $columns; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment