-
-
Notifications
You must be signed in to change notification settings - Fork 34
Fix/resource page record binding #63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,7 +4,9 @@ | |||||||||||||||||||||
|
|
||||||||||||||||||||||
| namespace Relaticle\Flowforge; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| use Filament\Actions\Action; | ||||||||||||||||||||||
| use Filament\Actions\Contracts\HasActions; | ||||||||||||||||||||||
| use Filament\Actions\Exceptions\ActionNotResolvableException; | ||||||||||||||||||||||
| use Filament\Forms\Contracts\HasForms; | ||||||||||||||||||||||
| use Filament\Resources\Pages\Page; | ||||||||||||||||||||||
| use Relaticle\Flowforge\Concerns\BaseBoard; | ||||||||||||||||||||||
|
|
@@ -13,10 +15,75 @@ | |||||||||||||||||||||
| /** | ||||||||||||||||||||||
| * Board page for Filament resource pages. | ||||||||||||||||||||||
| * Extends Filament's resource Page class with kanban board functionality. | ||||||||||||||||||||||
| * | ||||||||||||||||||||||
| * CRITICAL: This class doesn't use InteractsWithRecord trait itself, but child | ||||||||||||||||||||||
| * classes might. To handle the trait conflict, we override getDefaultActionRecord() | ||||||||||||||||||||||
| * to intelligently route to either board card records or resource records based | ||||||||||||||||||||||
| * on whether a recordKey is present in the mounted action context. | ||||||||||||||||||||||
| */ | ||||||||||||||||||||||
| abstract class BoardResourcePage extends Page implements HasActions, HasBoard, HasForms | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| use BaseBoard; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| protected string $view = 'flowforge::filament.pages.board-page'; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /** | ||||||||||||||||||||||
| * Override Filament's action resolution to detect and route board actions. | ||||||||||||||||||||||
| * | ||||||||||||||||||||||
| * This method intercepts the action resolution flow to check if an action | ||||||||||||||||||||||
| * is a board action (has recordKey in context). If so, it routes to | ||||||||||||||||||||||
| * resolveBoardAction() which properly handles the record resolution, | ||||||||||||||||||||||
| * similar to how table actions are handled via resolveTableAction(). | ||||||||||||||||||||||
| * | ||||||||||||||||||||||
| * This mirrors the logic in InteractsWithActions::resolveActions() but adds | ||||||||||||||||||||||
| * board action detection. | ||||||||||||||||||||||
| * | ||||||||||||||||||||||
| * @param array<array<string, mixed>> $actions | ||||||||||||||||||||||
| * @return array<Action> | ||||||||||||||||||||||
| * | ||||||||||||||||||||||
| * @throws ActionNotResolvableException | ||||||||||||||||||||||
| */ | ||||||||||||||||||||||
| protected function resolveActions(array $actions): array | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| $resolvedActions = []; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| foreach ($actions as $actionNestingIndex => $action) { | ||||||||||||||||||||||
| if (blank($action['name'] ?? null)) { | ||||||||||||||||||||||
| throw new \Filament\Actions\Exceptions\ActionNotResolvableException('An action tried to resolve without a name.'); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Check if this is a board CARD action (has recordKey in context) | ||||||||||||||||||||||
| // Column actions have 'column' in arguments, not recordKey | ||||||||||||||||||||||
| // This detection happens BEFORE schema/table action detection | ||||||||||||||||||||||
| $recordKey = $action['context']['recordKey'] ?? null; | ||||||||||||||||||||||
| $columnId = $action['arguments']['column'] ?? null; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
||||||||||||||||||||||
| // Validation: recordKey and columnId should be mutually exclusive. | |
| // If both are present, this is an invalid/ambiguous action context. | |
| if (filled($recordKey) && filled($columnId)) { | |
| throw new \Filament\Actions\Exceptions\ActionNotResolvableException( | |
| "Ambiguous board action: both 'recordKey' and 'column' are present. " . | |
| "Actions must have either 'recordKey' (for card actions) or 'column' (for column actions), not both." | |
| ); | |
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -286,6 +286,39 @@ public function getBoardQuery(): ?Builder | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return $this->getBoard()->getQuery(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Resolve a board action (similar to resolveTableAction). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| protected function resolveBoardAction(array $action, array $parentActions): ?Action | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $resolvedAction = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (count($parentActions)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $parentAction = end($parentActions); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $resolvedAction = $parentAction->getModalAction($action['name']); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $resolvedAction = $this->cachedActions[$action['name']] ?? null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (! $resolvedAction) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $recordKey = $action['context']['recordKey'] ?? $action['arguments']['recordKey'] ?? null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (filled($recordKey)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $board = $this->getBoard(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $query = $board->getQuery(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ($query) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $record = (clone $query)->find($recordKey); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $resolvedAction->record($record); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return $resolvedAction; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+290
to
+321
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Resolve a board action (similar to resolveTableAction). | |
| */ | |
| protected function resolveBoardAction(array $action, array $parentActions): ?Action | |
| { | |
| $resolvedAction = null; | |
| if (count($parentActions)) { | |
| $parentAction = end($parentActions); | |
| $resolvedAction = $parentAction->getModalAction($action['name']); | |
| } else { | |
| $resolvedAction = $this->cachedActions[$action['name']] ?? null; | |
| } | |
| if (! $resolvedAction) { | |
| return null; | |
| } | |
| $recordKey = $action['context']['recordKey'] ?? $action['arguments']['recordKey'] ?? null; | |
| if (filled($recordKey)) { | |
| $board = $this->getBoard(); | |
| $query = $board->getQuery(); | |
| if ($query) { | |
| $record = (clone $query)->find($recordKey); | |
| $resolvedAction->record($record); | |
| } | |
| } | |
| return $resolvedAction; | |
| } | |
| * | |
| */ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Relaticle\Flowforge\Tests\Fixtures; | ||
|
|
||
| use Filament\Resources\Pages\Concerns\InteractsWithRecord; | ||
| use Relaticle\Flowforge\Board; | ||
| use Relaticle\Flowforge\BoardResourcePage; | ||
| use Relaticle\Flowforge\Column; | ||
|
|
||
| /** | ||
| * Test fixture for BoardResourcePage that uses InteractsWithRecord. | ||
| * Replicates the GitHub issue #37 scenario where a project has many tasks. | ||
| */ | ||
| class TestBoardResourcePage extends BoardResourcePage | ||
| { | ||
| use InteractsWithRecord; | ||
|
|
||
| protected static string $resource = TestResource::class; | ||
|
|
||
| public function mount(int | string $record): void | ||
| { | ||
| $this->record = $this->resolveRecord($record); | ||
| } | ||
|
|
||
| public function board(Board $board): Board | ||
| { | ||
| // Use $this->getRecord() to scope tasks to this project | ||
| return $board | ||
| ->query($this->getRecord()->tasks()->getQuery()) | ||
| ->recordTitleAttribute('title') | ||
| ->columnIdentifier('status') | ||
| ->positionIdentifier('order_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'), | ||
| ]); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Relaticle\Flowforge\Tests\Fixtures; | ||
|
|
||
| use Filament\Resources\Resource; | ||
|
|
||
| /** | ||
| * Minimal test resource for TestBoardResourcePage. | ||
| */ | ||
| class TestResource extends Resource | ||
| { | ||
| protected static ?string $model = Project::class; | ||
|
|
||
| protected static ?string $slug = 'test-projects'; | ||
|
|
||
| public static function getPages(): array | ||
| { | ||
| return [ | ||
| 'board' => TestBoardResourcePage::route('/{record}/board'), | ||
| ]; | ||
| } | ||
| } | ||
|
Comment on lines
+1
to
+24
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code uses a fully qualified exception class name instead of relying on the imported ActionNotResolvableException at line 9. For consistency, use the imported class name instead of the fully qualified name.