diff --git a/README.md b/README.md index c27b6e1..9c30d05 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,23 @@ public function panel(Panel $panel): Panel } ``` +## Excluding Soft Deleted Records + +If your models use soft deletes, you can exclude trashed records from the count with the `withoutTrashed` method on the plugin. + +```php +use Awcodes\Overlook\OverlookPlugin; + +public function panel(Panel $panel): Panel +{ + return $panel + ->plugins([ + OverlookPlugin::make() + ->withoutTrashed(), + ]); +} +``` + ## Sorting the Items By default, the items will be sorted in the order they are registered with Filament or as provided in the `includes` method. You can change this to sort them alphabetically with the `alphabetical` method on the plugin. diff --git a/src/OverlookPlugin.php b/src/OverlookPlugin.php index 15e8dba..caab238 100644 --- a/src/OverlookPlugin.php +++ b/src/OverlookPlugin.php @@ -29,6 +29,8 @@ class OverlookPlugin implements Plugin protected array|Closure|null $icons = null; + protected bool|Closure|null $withoutTrashed = null; + public static function make(): self { return app(self::class); @@ -143,4 +145,16 @@ public function getIcons(): array { return $this->evaluate($this->icons) ?? []; } + + public function withoutTrashed(bool|Closure|null $condition = true): static + { + $this->withoutTrashed = $condition; + + return $this; + } + + public function shouldExcludeTrashed(): bool + { + return $this->evaluate($this->withoutTrashed) ?? false; + } } diff --git a/src/Widgets/OverlookWidget.php b/src/Widgets/OverlookWidget.php index 36350e4..a05aee5 100644 --- a/src/Widgets/OverlookWidget.php +++ b/src/Widgets/OverlookWidget.php @@ -8,6 +8,7 @@ use Awcodes\Overlook\OverlookPlugin; use Exception; use Filament\Widgets\Widget; +use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Number; class OverlookWidget extends Widget @@ -71,7 +72,7 @@ public function getData(): array ? $includes : filament()->getCurrentOrDefaultPanel()->getResources(); - return collect($rawResources)->filter(fn ($resource): bool => ! in_array($resource, $excludes))->transform(function ($resource) use ($icons): ?array { + return collect($rawResources)->filter(fn ($resource): bool => ! in_array($resource, $excludes))->transform(function ($resource) use ($plugin, $icons): ?array { $customIcon = array_search($resource, $icons); @@ -79,6 +80,10 @@ public function getData(): array $widgetQuery = $res->getEloquentQuery(); + if ($plugin->shouldExcludeTrashed() && in_array(SoftDeletes::class, class_uses_recursive($widgetQuery->getModel()))) { + $widgetQuery = $widgetQuery->withoutTrashed(); + } + if ($res instanceof CustomizeOverlookWidget) { $rawCount = $res->getOverlookWidgetQuery($widgetQuery)->count(); $title = $res->getOverlookWidgetTitle(); diff --git a/tests/src/Feature/WidgetTest.php b/tests/src/Feature/WidgetTest.php index bc0260f..eaa6ef8 100644 --- a/tests/src/Feature/WidgetTest.php +++ b/tests/src/Feature/WidgetTest.php @@ -83,3 +83,50 @@ && $data[0]['name'] === 'Unverified Users'; }); }); + +it('excludes soft deleted records when withoutTrashed is enabled', function () { + // setUp creates 1 user for authentication + User::factory()->count(3)->create(); + User::factory()->count(2)->create(['deleted_at' => now()]); + + $this->panel + ->plugins([ + OverlookPlugin::make() + ->withoutTrashed() + ->includes([ + UserResource::class, + ]), + ]) + ->widgets([ + OverlookWidget::class, + ]); + + // 1 (from setUp) + 3 (created) = 4 non-trashed users + livewire(OverlookWidget::class) + ->assertViewHas('data', function ($data) { + return $data[0]['count'] === '4'; + }); +}); + +it('excludes soft deleted records by default due to SoftDeletes global scope', function () { + // setUp creates 1 user for authentication + User::factory()->count(3)->create(); + User::factory()->count(2)->create(['deleted_at' => now()]); + + $this->panel + ->plugins([ + OverlookPlugin::make() + ->includes([ + UserResource::class, + ]), + ]) + ->widgets([ + OverlookWidget::class, + ]); + + // Default SoftDeletes global scope excludes trashed: 1 (from setUp) + 3 = 4 + livewire(OverlookWidget::class) + ->assertViewHas('data', function ($data) { + return $data[0]['count'] === '4'; + }); +}); diff --git a/tests/src/Fixtures/Models/User.php b/tests/src/Fixtures/Models/User.php index 9a694ad..3bdcfbb 100644 --- a/tests/src/Fixtures/Models/User.php +++ b/tests/src/Fixtures/Models/User.php @@ -8,6 +8,7 @@ use Filament\Models\Contracts\FilamentUser; use Filament\Panel; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; @@ -15,6 +16,7 @@ class User extends Authenticatable implements FilamentUser { use HasFactory; use Notifiable; + use SoftDeletes; protected $guarded = []; diff --git a/tests/src/TestCase.php b/tests/src/TestCase.php index 063b7c9..27f66d7 100644 --- a/tests/src/TestCase.php +++ b/tests/src/TestCase.php @@ -19,6 +19,7 @@ use Filament\Tables\TablesServiceProvider; use Filament\Widgets\WidgetsServiceProvider; use Illuminate\Foundation\Testing\LazilyRefreshDatabase; +use Illuminate\Support\Facades\Schema; use Livewire\LivewireServiceProvider; use Orchestra\Testbench\Concerns\WithWorkbench; use Orchestra\Testbench\TestCase as Orchestra; @@ -36,6 +37,15 @@ protected function setUp(): void $this->actingAs(User::factory()->create()); } + protected function afterRefreshingDatabase(): void + { + if (! Schema::hasColumn('users', 'deleted_at')) { + Schema::table('users', function ($table) { + $table->softDeletes(); + }); + } + } + protected function getPackageProviders($app): array { $providers = [ diff --git a/tests/src/Unit/PluginTest.php b/tests/src/Unit/PluginTest.php index 3ef9e3c..7158682 100644 --- a/tests/src/Unit/PluginTest.php +++ b/tests/src/Unit/PluginTest.php @@ -170,3 +170,18 @@ expect(Filament::getPlugin('awcodes/overlook')->getIncludes()) ->toContain('Awcodes\Overlook\Tests\Fixtures\Resources\Users\UserResource'); }); + +it('sets withoutTrashed', function (bool|Closure|null $condition) { + $this->panel + ->plugins([ + OverlookPlugin::make()->withoutTrashed($condition), + ]); + + expect(Filament::getPlugin('awcodes/overlook')->shouldExcludeTrashed()) + ->toBe($condition); +})->with([ + true, + fn () => true, + false, + fn () => false, +]);