Skip to content

Commit 7a9bdd6

Browse files
Merge pull request #86 from relaticle/fix/standalone-panel-dependency
fix: support standalone Filament usage without panel dependency
2 parents e01707c + e95b899 commit 7a9bdd6

File tree

9 files changed

+270
-8
lines changed

9 files changed

+270
-8
lines changed

composer.json

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,19 @@
2121
],
2222
"require": {
2323
"php": "^8.3",
24-
"filament/filament": "^5.0",
25-
"spatie/laravel-package-tools": "^1.15.0",
26-
"ext-bcmath": "*"
24+
"ext-bcmath": "*",
25+
"filament/actions": "^5.0",
26+
"filament/forms": "^5.0",
27+
"filament/schemas": "^5.0",
28+
"filament/support": "^5.0",
29+
"filament/tables": "^5.0",
30+
"spatie/laravel-package-tools": "^1.15.0"
31+
},
32+
"suggest": {
33+
"filament/filament": "Required for BoardPage, BoardResourcePage, and FlowforgePlugin panel integration (^5.0)"
2734
},
2835
"require-dev": {
36+
"filament/filament": "^5.0",
2937
"larastan/larastan": "^3.0",
3038
"laravel/pint": "^1.0",
3139
"nunomaduro/collision": "^8.0",

src/BoardPage.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
/**
1515
* Board page for standard Filament pages.
1616
* Extends Filament's base Page class with kanban board functionality.
17+
*
18+
* Requires the `filament/filament` package (Panel Builder).
19+
* For standalone usage, use the InteractsWithBoard trait directly.
20+
*
21+
* @see \Relaticle\Flowforge\Concerns\InteractsWithBoard
1722
*/
1823
abstract class BoardPage extends Page implements HasActions, HasBoard, HasForms
1924
{

src/BoardResourcePage.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,15 @@
1616
* Board page for Filament resource pages.
1717
* Extends Filament's resource Page class with kanban board functionality.
1818
*
19+
* Requires the `filament/filament` package (Panel Builder).
20+
* For standalone usage, use the InteractsWithBoard trait directly.
21+
*
1922
* CRITICAL: This class doesn't use InteractsWithRecord trait itself, but child
2023
* classes might. To handle the trait conflict, we override getDefaultActionRecord()
2124
* to intelligently route to either board card records or resource records based
2225
* on whether a recordKey is present in the mounted action context.
26+
*
27+
* @see \Relaticle\Flowforge\Concerns\InteractsWithBoard
2328
*/
2429
abstract class BoardResourcePage extends Page implements HasActions, HasBoard, HasForms
2530
{

src/FlowforgePlugin.php

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
<?php
22

3+
declare(strict_types=1);
4+
35
namespace Relaticle\Flowforge;
46

57
use Filament\Contracts\Plugin;
68
use Filament\Panel;
7-
use Livewire\Livewire;
8-
9-
// use Relaticle\Flowforge\Livewire\KanbanBoard;
109

10+
/**
11+
* Filament Panel plugin for FlowForge.
12+
*
13+
* This class requires the full `filament/filament` package (Panel Builder).
14+
* For standalone Livewire usage without a panel, use the InteractsWithBoard
15+
* trait directly on your Livewire component instead.
16+
*
17+
* @see \Relaticle\Flowforge\Concerns\InteractsWithBoard
18+
*/
1119
class FlowforgePlugin implements Plugin
1220
{
1321
public function getId(): string
@@ -22,7 +30,7 @@ public function register(Panel $panel): void
2230

2331
public function boot(Panel $panel): void
2432
{
25-
// Livewire::component('relaticle.flowforge.livewire.kanban-board', KanbanBoard::class);
33+
//
2634
}
2735

2836
public static function make(): static
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Relaticle\Flowforge\Tests\Fixtures;
6+
7+
use Filament\Actions\Concerns\InteractsWithActions;
8+
use Filament\Actions\Contracts\HasActions;
9+
use Filament\Forms\Concerns\InteractsWithForms;
10+
use Filament\Forms\Contracts\HasForms;
11+
use Livewire\Component;
12+
use Relaticle\Flowforge\Board;
13+
use Relaticle\Flowforge\Column;
14+
use Relaticle\Flowforge\Concerns\InteractsWithBoard;
15+
use Relaticle\Flowforge\Contracts\HasBoard;
16+
17+
/**
18+
* Standalone Livewire component for testing without Filament Panel.
19+
*/
20+
class TestStandaloneBoard extends Component implements HasActions, HasBoard, HasForms
21+
{
22+
use InteractsWithActions;
23+
use InteractsWithBoard {
24+
InteractsWithBoard::getDefaultActionRecord insteadof InteractsWithActions;
25+
}
26+
use InteractsWithForms;
27+
28+
public function board(Board $board): Board
29+
{
30+
return $board
31+
->query(Task::query())
32+
->recordTitleAttribute('title')
33+
->columnIdentifier('status')
34+
->positionIdentifier('order_position')
35+
->columns([
36+
Column::make('todo')->label('To Do')->color('gray'),
37+
Column::make('in_progress')->label('In Progress')->color('blue'),
38+
Column::make('completed')->label('Completed')->color('green'),
39+
]);
40+
}
41+
42+
public function render()
43+
{
44+
return <<<'BLADE'
45+
<div>
46+
{{ $this->board }}
47+
</div>
48+
BLADE;
49+
}
50+
}

tests/Pest.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<?php
22

3+
use Relaticle\Flowforge\Tests\StandaloneTestCase;
34
use Relaticle\Flowforge\Tests\TestCase;
45

5-
pest()->extends(TestCase::class)->in(__DIR__);
6+
pest()->extends(TestCase::class)->in('Feature', 'Unit');
7+
pest()->extends(StandaloneTestCase::class)->in('Standalone');
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Livewire\Livewire;
6+
use Relaticle\Flowforge\Services\DecimalPosition;
7+
use Relaticle\Flowforge\Tests\Fixtures\Task;
8+
use Relaticle\Flowforge\Tests\Fixtures\TestStandaloneBoard;
9+
10+
/**
11+
* @see https://github.com/relaticle/flowforge/issues/84
12+
*/
13+
describe('standalone board rendering', function () {
14+
test('renders board with all columns', function () {
15+
Livewire::test(TestStandaloneBoard::class)
16+
->assertStatus(200)
17+
->assertSee('To Do')
18+
->assertSee('In Progress')
19+
->assertSee('Completed');
20+
});
21+
22+
test('displays cards in correct columns', function () {
23+
Task::factory()->todo()->create(['title' => 'Standalone Todo']);
24+
Task::factory()->inProgress()->create(['title' => 'Standalone In Progress']);
25+
Task::factory()->completed()->create(['title' => 'Standalone Completed']);
26+
27+
Livewire::test(TestStandaloneBoard::class)
28+
->assertSee('Standalone Todo')
29+
->assertSee('Standalone In Progress')
30+
->assertSee('Standalone Completed');
31+
});
32+
});
33+
34+
describe('standalone card movement', function () {
35+
test('moves card to different column', function () {
36+
$task = Task::factory()->todo()->withPosition('65535.0000000000')->create();
37+
38+
Livewire::test(TestStandaloneBoard::class)
39+
->call('moveCard', (string) $task->id, 'in_progress', null, null)
40+
->assertDispatched('kanban-card-moved');
41+
42+
expect($task->fresh()->status)->toBe('in_progress');
43+
});
44+
45+
test('moves card between two cards', function () {
46+
$task1 = Task::factory()->inProgress()->withPosition('65535.0000000000')->create();
47+
$task2 = Task::factory()->inProgress()->withPosition('131070.0000000000')->create();
48+
$taskToMove = Task::factory()->todo()->withPosition('65535.0000000000')->create();
49+
50+
Livewire::test(TestStandaloneBoard::class)
51+
->call('moveCard', (string) $taskToMove->id, 'in_progress', (string) $task1->id, (string) $task2->id)
52+
->assertDispatched('kanban-card-moved');
53+
54+
$movedTask = $taskToMove->fresh();
55+
expect($movedTask->status)->toBe('in_progress')
56+
->and((float) $movedTask->order_position)->toBeGreaterThan(65535)
57+
->and((float) $movedTask->order_position)->toBeLessThan(131070);
58+
});
59+
60+
test('moves card to empty column', function () {
61+
$task = Task::factory()->todo()->withPosition('65535.0000000000')->create();
62+
63+
Livewire::test(TestStandaloneBoard::class)
64+
->call('moveCard', (string) $task->id, 'completed', null, null)
65+
->assertDispatched('kanban-card-moved');
66+
67+
$movedTask = $task->fresh();
68+
expect($movedTask->status)->toBe('completed')
69+
->and((float) $movedTask->order_position)->toBe((float) DecimalPosition::DEFAULT_GAP);
70+
});
71+
});
72+
73+
describe('standalone pagination', function () {
74+
test('loads more items on demand', function () {
75+
Task::factory(30)->todo()->create();
76+
77+
Livewire::test(TestStandaloneBoard::class)
78+
->call('loadMoreItems', 'todo', 20)
79+
->assertDispatched('kanban-items-loaded');
80+
});
81+
});

tests/StandaloneTestCase.php

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Relaticle\Flowforge\Tests;
6+
7+
use BladeUI\Heroicons\BladeHeroiconsServiceProvider;
8+
use BladeUI\Icons\BladeIconsServiceProvider;
9+
use Filament\Actions\ActionsServiceProvider;
10+
use Filament\FilamentManager;
11+
use Filament\Forms\FormsServiceProvider;
12+
use Filament\Infolists\InfolistsServiceProvider;
13+
use Filament\Notifications\NotificationsServiceProvider;
14+
use Filament\Support\SupportServiceProvider;
15+
use Filament\Tables\TablesServiceProvider;
16+
use Illuminate\Database\Eloquent\Factories\Factory;
17+
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
18+
use Livewire\LivewireServiceProvider;
19+
use Orchestra\Testbench\Concerns\WithWorkbench;
20+
use Orchestra\Testbench\TestCase as Orchestra;
21+
use Relaticle\Flowforge\FlowforgeServiceProvider;
22+
use RyanChandler\BladeCaptureDirective\BladeCaptureDirectiveServiceProvider;
23+
24+
/**
25+
* Test case for standalone Livewire usage WITHOUT the Filament Panel Builder.
26+
*
27+
* Excludes FilamentServiceProvider and TestPanelProvider to verify the package
28+
* works without a panel registered.
29+
*
30+
* Note: In a real standalone install (without filament/filament), the Filament
31+
* facade class won't exist and class_exists() guards in filament/tables skip
32+
* panel-dependent calls entirely. In our test environment, filament/filament IS
33+
* installed as a dev dependency, so we register a minimal FilamentManager binding
34+
* to satisfy those guards without configuring any panel.
35+
*/
36+
class StandaloneTestCase extends Orchestra
37+
{
38+
use LazilyRefreshDatabase;
39+
use WithWorkbench;
40+
41+
protected function setUp(): void
42+
{
43+
parent::setUp();
44+
45+
Factory::guessFactoryNamesUsing(
46+
fn (string $modelName) => 'Relaticle\\Flowforge\\Database\\Factories\\' . class_basename($modelName) . 'Factory'
47+
);
48+
49+
// Register minimal FilamentManager binding. This is only needed because
50+
// filament/filament is a dev dependency (for panel tests), making the
51+
// Filament facade class available. The tables package's HasFilters trait
52+
// uses class_exists(Filament::class) to guard tenant-aware session keys,
53+
// which passes in our test env but would be false in a real standalone install.
54+
if (! $this->app->bound('filament')) {
55+
$this->app->scoped('filament', fn () => new FilamentManager);
56+
}
57+
}
58+
59+
protected function getPackageProviders($app): array
60+
{
61+
$providers = [
62+
ActionsServiceProvider::class,
63+
BladeCaptureDirectiveServiceProvider::class,
64+
BladeHeroiconsServiceProvider::class,
65+
BladeIconsServiceProvider::class,
66+
FormsServiceProvider::class,
67+
InfolistsServiceProvider::class,
68+
LivewireServiceProvider::class,
69+
NotificationsServiceProvider::class,
70+
SupportServiceProvider::class,
71+
TablesServiceProvider::class,
72+
FlowforgeServiceProvider::class,
73+
];
74+
75+
sort($providers);
76+
77+
return $providers;
78+
}
79+
80+
protected function defineEnvironment($app): void
81+
{
82+
config()->set('database.default', 'testing');
83+
config()->set('database.connections.testing', [
84+
'driver' => 'sqlite',
85+
'database' => ':memory:',
86+
'prefix' => '',
87+
]);
88+
89+
config()->set('app.key', 'base64:' . base64_encode(random_bytes(32)));
90+
config()->set('session.driver', 'array');
91+
config()->set('session.encrypt', false);
92+
93+
config()->set('view.paths', [
94+
resource_path('views'),
95+
__DIR__ . '/../resources/views',
96+
]);
97+
}
98+
99+
protected function defineDatabaseMigrations(): void
100+
{
101+
$this->loadMigrationsFrom(__DIR__ . '/database/migrations');
102+
}
103+
}

0 commit comments

Comments
 (0)